SRE.informations
Numbered sections, cross-references and table of contents of self.informations.
The sections of a course are numbered by hand otherwise (## 7. Routes), with a hand-written
table of contents and mentions such as (section 7): inserting a section means renumbering
all of that in every language. A lab calls four functions, imported from SRE.lib_sre like
tr() and instructor(), which only emit plain-text markers; the numbers, the links and
the table of contents are computed when info.json is written (Grade0.save_lab_info():
resolve_informations() on the course, then resolve_text() on the question texts with
the numbering of the course), so there is nothing else to call, and sre check validates the
markers and prints the numbered outline of the course (outline()).
Usage, in NetScheme.__init__::
from SRE.lib_sre import tr, no_tr, info_subtitle, info_tag, info_ref, info_toc
self.informations = (
no_tr("## ") + title + no_tr("\n")
+ tr("**Sommaire**\n") + info_toc()
+ info_subtitle(tr("À quoi sert DHCP"), tag="why")
+ tr("""
Pour communiquer ... (voir la section @@ref:lease@@).
### @@sec@@ Transport et adresses
Le client ... @@tag:ports@@ les ports 67 et 68 ...
""")
+ info_subtitle(tr("La vie d'un bail"), tag="lease")
+ tr("""...""")
)
and in Grade.grade()::
description=tr("Configurez le relais (section @@ref:relay@@ des Informations) ...")
The functions return plain strings (a tr() title keeps its languages), to be added with +
like any text. The markers they return may also be typed by hand inside a tr() block, which
is the natural way for a sub-section or a reference in the middle of a long text:
====================================== ==================================== ==========================================
function marker resolved form in the course
====================================== ==================================== ==========================================
info_subtitle(title, level=1, tag) ## @@sec@@ Title ## <a id="sec.2"></a>2. Title
### @@sec:tag@@ Title ### <a id="tag"></a>2.1. Title
info_tag(tag) @@tag:tag@@ <a id="tag"></a> (number: its section)
info_ref(tag) @@ref:tag@@ [2.1](#tag)
info_ref(tag, title=True) @@ref:tag:title@@ [2.1. Title](#tag)
info_toc(depth=None) @@toc@@ / @@toc:depth@@ nested bullet list of [2.1. Title](#tag)
====================================== ==================================== ==========================================
Rules:
- the level of a typed header is its number of
#minus one (##: level 1); a#header is the lab title and is never numbered; a header without a marker is not numbered either; - a tag is made of letters, digits,
_and-(English, like the identifiers of the lab); it must be unique, and a level cannot be skipped (no####right after a##); - in a question text only
@@ref@@is allowed, and it gives the bare number (2.1): the browser of a question has no anchor to scroll to; - the headers are numbered
1.,2.1.,2.1.3.; a reference is the number without the final dot; the table of contents lists every level (info_toc(depth=1)for the first one), as a nested markdown bullet list indented like the line of its marker; - a marker inside a fenced code block is left as it is;
- do not number a header inside
instructor(): the numbers must be the same in and out of the instructor mode, so a@@sec@@,@@tag@@or@@toc@@standing inside an instructor fragment is dropped (@@ref@@is resolved there); - a text that gets a
@@ref@@throughTranslatedText.format()must hold no literal brace; type the marker inside thetr()text instead; - the languages of a
tr()course must declare the same sections and tags in the same order (sre checkreports a translation that missed a header).
Errors are reported as ValueError when the text is resolved (by sre check, or by
sre start like any lab bug): a @@sec@@ outside a header line, a # header, a level
jump, a tag defined twice, an unknown tag, a numbered header without a title, a @@sec@@ /
@@tag@@ / @@toc@@ in a question text, and languages of one TranslatedText whose
sections or tags differ. A question hash is computed from the marked title, so a renumbering
never changes it.
This module imports nothing from SRE but instructor_text.
1"""Numbered sections, cross-references and table of contents of ``self.informations``. 2 3The sections of a course are numbered by hand otherwise (``## 7. Routes``), with a hand-written 4table of contents and mentions such as ``(section 7)``: inserting a section means renumbering 5all of that in every language. A lab calls four functions, imported from ``SRE.lib_sre`` like 6``tr()`` and ``instructor()``, which only emit plain-text *markers*; the numbers, the links and 7the table of contents are computed when ``info.json`` is written (``Grade0.save_lab_info()``: 8:func:`resolve_informations` on the course, then :func:`resolve_text` on the question texts with 9the numbering of the course), so there is nothing else to call, and ``sre check`` validates the 10markers and prints the numbered outline of the course (:func:`outline`). 11 12Usage, in ``NetScheme.__init__``:: 13 14 from SRE.lib_sre import tr, no_tr, info_subtitle, info_tag, info_ref, info_toc 15 16 self.informations = ( 17 no_tr("## ") + title + no_tr("\\n") 18 + tr("**Sommaire**\\n") + info_toc() 19 + info_subtitle(tr("À quoi sert DHCP"), tag="why") 20 + tr(\"\"\" 21 Pour communiquer ... (voir la section @@ref:lease@@). 22 23 ### @@sec@@ Transport et adresses 24 25 Le client ... @@tag:ports@@ les ports 67 et 68 ... 26 \"\"\") 27 + info_subtitle(tr("La vie d'un bail"), tag="lease") 28 + tr(\"\"\"...\"\"\") 29 ) 30 31and in ``Grade.grade()``:: 32 33 description=tr("Configurez le relais (section @@ref:relay@@ des Informations) ...") 34 35The functions return plain strings (a ``tr()`` title keeps its languages), to be added with ``+`` 36like any text. The markers they return may also be typed by hand inside a ``tr()`` block, which 37is the natural way for a sub-section or a reference in the middle of a long text: 38 39====================================== ==================================== ========================================== 40function marker resolved form in the course 41====================================== ==================================== ========================================== 42``info_subtitle(title, level=1, tag)`` ``## @@sec@@ Title`` ``## <a id="sec.2"></a>2. Title`` 43 ``### @@sec:tag@@ Title`` ``### <a id="tag"></a>2.1. Title`` 44``info_tag(tag)`` ``@@tag:tag@@`` ``<a id="tag"></a>`` (number: its section) 45``info_ref(tag)`` ``@@ref:tag@@`` ``[2.1](#tag)`` 46``info_ref(tag, title=True)`` ``@@ref:tag:title@@`` ``[2.1. Title](#tag)`` 47``info_toc(depth=None)`` ``@@toc@@`` / ``@@toc:depth@@`` nested bullet list of ``[2.1. Title](#tag)`` 48====================================== ==================================== ========================================== 49 50Rules: 51 52- the level of a typed header is its number of ``#`` minus one (``##``: level 1); a ``#`` header 53 is the lab title and is never numbered; a header without a marker is not numbered either; 54- a tag is made of letters, digits, ``_`` and ``-`` (English, like the identifiers of the lab); 55 it must be unique, and a level cannot be skipped (no ``####`` right after a ``##``); 56- in a question text only ``@@ref@@`` is allowed, and it gives the bare number (``2.1``): the 57 browser of a question has no anchor to scroll to; 58- the headers are numbered ``1.``, ``2.1.``, ``2.1.3.``; a reference is the number without the 59 final dot; the table of contents lists every level (``info_toc(depth=1)`` for the first one), 60 as a nested markdown bullet list indented like the line of its marker; 61- a marker inside a fenced code block is left as it is; 62- do not number a header inside ``instructor()``: the numbers must be the same in and out of the 63 instructor mode, so a ``@@sec@@``, ``@@tag@@`` or ``@@toc@@`` standing inside an instructor 64 fragment is dropped (``@@ref@@`` is resolved there); 65- a text that gets a ``@@ref@@`` through ``TranslatedText.format()`` must hold no literal brace; 66 type the marker inside the ``tr()`` text instead; 67- the languages of a ``tr()`` course must declare the same sections and tags in the same order 68 (``sre check`` reports a translation that missed a header). 69 70Errors are reported as :class:`ValueError` when the text is resolved (by ``sre check``, or by 71``sre start`` like any lab bug): a ``@@sec@@`` outside a header line, a ``#`` header, a level 72jump, a tag defined twice, an unknown tag, a numbered header without a title, a ``@@sec@@`` / 73``@@tag@@`` / ``@@toc@@`` in a question text, and languages of one ``TranslatedText`` whose 74sections or tags differ. A question hash is computed from the marked title, so a renumbering 75never changes it. 76 77This module imports nothing from SRE but ``instructor_text``. 78""" 79import re 80 81from .instructor_text import fragment_spans 82 83TAG_RE = re.compile(r'[A-Za-z0-9_-]+') 84MARKER_RE = re.compile(r'@@(?P<kind>sec|tag|ref|toc)(?::(?P<args>[^@\n]*))?@@') 85# what stands before a @@sec@@ marker on its line: the hashes of a markdown header 86_HEADER_RE = re.compile(r'[ \t]*(?P<hashes>#{1,6})[ \t]*') 87_FENCE_RE = re.compile(r'^[ \t]*(?P<fence>`{3,}|~{3,})', re.MULTILINE) 88AUTO_ANCHOR_PREFIX = 'sec.' # id of an untagged section: 'sec.2.1' ('.' never occurs in a tag) 89 90 91def check_tag(tag: str) -> str: 92 """*tag* when it is a valid tag name, else :class:`ValueError`.""" 93 if not isinstance(tag, str) or not TAG_RE.fullmatch(tag): 94 raise ValueError(f"invalid tag {tag!r}: letters, digits, '_' and '-' only") 95 return tag 96 97 98class Target: 99 """What a tag refers to: a section (*level* set) or a plain ``@@tag@@`` anchor (*level* None, 100 *number* and *title* those of the section it stands in).""" 101 102 __slots__ = ('number', 'title', 'anchor', 'level', 'tag') 103 104 def __init__(self, number: str, title: str, anchor: str, level, tag): 105 self.number = number 106 self.title = title 107 self.anchor = anchor 108 self.level = level 109 self.tag = tag 110 111 @property 112 def label(self) -> str: 113 """``2.1. Title``: the entry of the table of contents, the reference with a title.""" 114 return f"{self.number}. {self.title}" 115 116 def __repr__(self): 117 return f"Target({self.number!r}, {self.title!r}, anchor={self.anchor!r}, level={self.level}, tag={self.tag!r})" 118 119 120class Numbering: 121 """The sections and the tags of a course, per language; the first language added is the 122 default one, used for a language the course does not have and for plain strings.""" 123 124 def __init__(self): 125 self._languages = {} # lang -> (sections, targets) 126 127 def add(self, lang, sections: list, targets: dict) -> None: 128 self._languages[lang] = (sections, targets) 129 130 @property 131 def languages(self) -> list: 132 return list(self._languages) 133 134 def _entry(self, lang): 135 if lang in self._languages: 136 return self._languages[lang] 137 if self._languages: 138 return next(iter(self._languages.values())) 139 return [], {} 140 141 def sections(self, lang=None) -> list: 142 """The numbered sections, in text order (:class:`Target` objects).""" 143 return self._entry(lang)[0] 144 145 def targets(self, lang=None) -> dict: 146 """``tag -> Target`` of every tagged section and anchor.""" 147 return self._entry(lang)[1] 148 149 def check_consistent(self) -> None: 150 """Every language must declare the same sections (levels and tags) and anchors, in the 151 same order, else :class:`ValueError` (a translation missed a header).""" 152 reference = None 153 for lang, (sections, targets) in self._languages.items(): 154 signature = _signature(sections, targets) 155 if reference is None: 156 reference = (lang, signature) 157 elif signature != reference[1]: 158 raise ValueError( 159 f"the numbered sections and tags of the languages {reference[0]!r} and {lang!r} " 160 f"differ: {_describe(reference[1])} / {_describe(signature)}") 161 162 163def _signature(sections, targets): 164 items = [('sec', s.level, s.tag) for s in sections] 165 items += [('tag', None, t.tag) for t in targets.values() if t.level is None] 166 return tuple(items) 167 168 169def _describe(signature) -> str: 170 return ', '.join(f"{kind}{':' + tag if tag else ''}{'(' + str(level) + ')' if level else ''}" 171 for kind, level, tag in signature) or 'none' 172 173 174class _Marker: 175 __slots__ = ('kind', 'args', 'start', 'end', 'text', 'in_fragment', 'replacement', 'depth', 'indent', 'alone') 176 177 def __init__(self, match, in_fragment: bool): 178 self.kind = match.group('kind') 179 self.args = match.group('args') 180 self.start, self.end = match.span() 181 self.text = match.group() 182 self.in_fragment = in_fragment 183 self.replacement = None 184 self.depth = None 185 self.indent = '' 186 self.alone = False # nothing else on its line 187 188 189def _code_spans(text: str) -> list: 190 """``(start, end)`` of the fenced code blocks of *text* (an unclosed fence runs to the end).""" 191 spans = [] 192 opened = None # (fence string, start offset) 193 for match in _FENCE_RE.finditer(text): 194 fence = match.group('fence') 195 if opened is None: 196 opened = (fence, match.start()) 197 elif fence[0] == opened[0][0] and len(fence) >= len(opened[0]): 198 spans.append((opened[1], match.end())) 199 opened = None 200 if opened is not None: 201 spans.append((opened[1], len(text))) 202 return spans 203 204 205def _inside(position: int, spans: list) -> bool: 206 return any(start <= position < end for start, end in spans) 207 208 209def _markers(text: str) -> list: 210 """The markers of *text* outside its fenced code blocks, in text order.""" 211 code = _code_spans(text) 212 fragments = fragment_spans(text) 213 return [_Marker(match, _inside(match.start(), fragments)) 214 for match in MARKER_RE.finditer(text) if not _inside(match.start(), code)] 215 216 217def _line_bounds(text: str, position: int): 218 start = text.rfind('\n', 0, position) + 1 219 end = text.find('\n', position) 220 return start, len(text) if end < 0 else end 221 222 223def _skip_blanks(text: str, position: int) -> int: 224 while position < len(text) and text[position] in ' \t': 225 position += 1 226 return position 227 228 229def _parse_ref(marker: _Marker): 230 """``(tag, with_title)`` of a @@ref@@ marker.""" 231 args = marker.args or '' 232 tag, _, option = args.partition(':') 233 if option not in ('', 'title'): 234 raise ValueError(f"invalid marker {marker.text}: expected @@ref:tag@@ or @@ref:tag:title@@") 235 return check_tag(tag), option == 'title' 236 237 238def _number_course(text: str): 239 """First pass over a course: its markers with the replacement of every @@sec@@ / @@tag@@ (the 240 numbering), the sections in order and the ``tag -> Target`` map.""" 241 markers = _markers(text) 242 sections = [] 243 targets = {} 244 counters = [] 245 current = None # the section an anchor stands in 246 247 def register(tag, target): 248 if tag in targets: 249 raise ValueError(f"tag {tag!r} is defined twice") 250 targets[tag] = target 251 252 for marker in markers: 253 line_start, line_end = _line_bounds(text, marker.start) 254 marker.indent = text[line_start:_skip_blanks(text, line_start)] 255 marker.alone = not (text[line_start:marker.start].strip() or text[marker.end:line_end].strip()) 256 if marker.kind == 'sec': 257 after = _skip_blanks(text, marker.end) 258 if marker.in_fragment: 259 marker.start, marker.end, marker.replacement = marker.start, after, '' 260 continue 261 header = _HEADER_RE.fullmatch(text, line_start, marker.start) 262 if header is None: 263 raise ValueError(f"{marker.text} must open the text of a markdown header " 264 f"('## {marker.text} Title'): {text[line_start:line_end].strip()!r}") 265 level = len(header.group('hashes')) - 1 266 title = text[after:line_end].strip() 267 if level == 0: 268 raise ValueError(f"a '#' header is the lab title and cannot be numbered: use '##' " 269 f"for level 1 ({title!r})") 270 if not title: 271 raise ValueError(f"numbered header without a title: {text[line_start:line_end].strip()!r}") 272 if level > len(counters) + 1: 273 raise ValueError(f"section {title!r} is at level {level} but no level {level - 1} " 274 f"section opened before it") 275 counters = counters[:level - 1] + [(counters[level - 1] + 1) if level <= len(counters) else 1] 276 number = '.'.join(str(n) for n in counters) 277 tag = check_tag(marker.args) if marker.args is not None else None 278 current = Target(number, title, tag or f"{AUTO_ANCHOR_PREFIX}{number}", level, tag) 279 sections.append(current) 280 if tag: 281 register(tag, current) 282 marker.end = after 283 marker.replacement = f'<a id="{current.anchor}"></a>{number}. ' 284 elif marker.kind == 'tag': 285 if marker.args is None: 286 raise ValueError(f"invalid marker {marker.text}: expected @@tag:name@@") 287 tag = check_tag(marker.args) 288 if marker.in_fragment: 289 marker.replacement = '' 290 continue 291 register(tag, Target(current.number if current else '', current.title if current else '', 292 tag, None, tag)) 293 marker.replacement = f'<a id="{tag}"></a>' 294 elif marker.kind == 'toc': 295 if marker.args is not None and not marker.args.isdigit(): 296 raise ValueError(f"invalid marker {marker.text}: expected @@toc@@ or @@toc:depth@@") 297 if marker.in_fragment: 298 marker.replacement = '' 299 continue 300 marker.depth = int(marker.args) if marker.args is not None else None 301 if marker.alone: 302 marker.start = line_start # the list lines carry the indentation themselves 303 else: 304 _parse_ref(marker) # validated now, resolved by the second pass 305 return markers, sections, targets 306 307 308def _reference(marker: _Marker, targets: dict, links: bool) -> str: 309 tag, with_title = _parse_ref(marker) 310 target = targets.get(tag) 311 if target is None: 312 raise ValueError(f"unknown tag {tag!r} in {marker.text}") 313 if not target.number: 314 raise ValueError(f"{marker.text}: the anchor {tag!r} stands before the first numbered section") 315 label = target.label if with_title else target.number 316 return f"[{label}](#{target.anchor})" if links else label 317 318 319def _toc(sections: list, depth, indent: str, alone: bool) -> str: 320 """The nested bullet list of the sections of the first *depth* levels, every line indented 321 like the line of the marker; a marker standing inside a paragraph gets a block of its own.""" 322 lines = [f"{indent}{' ' * (s.level - 1)}- [{s.label}](#{s.anchor})" 323 for s in sections if depth is None or s.level <= depth] 324 if not lines: 325 return '' 326 body = '\n'.join(lines) 327 return body if alone else f"\n\n{body}\n\n" 328 329 330def _substitute(text: str, markers: list) -> str: 331 pieces = [] 332 position = 0 333 for marker in markers: 334 pieces.append(text[position:marker.start]) 335 pieces.append(marker.replacement) 336 position = marker.end 337 pieces.append(text[position:]) 338 return ''.join(pieces) 339 340 341def _resolve_course(text: str): 342 markers, sections, targets = _number_course(text) 343 for marker in markers: 344 if marker.kind == 'ref': 345 marker.replacement = _reference(marker, targets, links=True) 346 elif marker.kind == 'toc' and marker.replacement is None: 347 marker.replacement = _toc(sections, marker.depth, marker.indent, marker.alone) 348 return _substitute(text, markers), sections, targets 349 350 351def _resolve_refs(text: str, targets: dict, links: bool) -> str: 352 markers = _markers(text) 353 for marker in markers: 354 if marker.kind != 'ref': 355 raise ValueError(f"{marker.text} is only allowed in the informations text " 356 f"(a question text may hold @@ref@@ only)") 357 marker.replacement = _reference(marker, targets, links) 358 return _substitute(text, markers) 359 360 361def resolve_informations(text): 362 """``(resolved text, numbering)`` of a course: *text* is a ``str`` or a ``TranslatedText`` 363 (resolved language by language, which must agree on the sections).""" 364 numbering = Numbering() 365 if isinstance(text, dict): 366 result = {} 367 for lang, value in text.items(): 368 result[lang], sections, targets = _resolve_course(value) 369 numbering.add(lang, sections, targets) 370 numbering.check_consistent() 371 return type(text)(result), numbering 372 resolved, sections, targets = _resolve_course(text) 373 numbering.add('', sections, targets) 374 return resolved, numbering 375 376 377def resolve_text(text, numbering: Numbering, links: bool = False): 378 """*text* (``str`` or ``TranslatedText``: a question title or description, a state 379 description) with its ``@@ref@@`` markers resolved against *numbering*; a reference is a link 380 only with *links* (the browser showing the text must hold the anchors of the course).""" 381 if isinstance(text, dict): 382 return type(text)({lang: _resolve_refs(value, numbering.targets(lang), links) 383 for lang, value in text.items()}) 384 return _resolve_refs(text, numbering.targets(), links) 385 386 387def has_markers(text) -> bool: 388 """True when *text* (``str`` or dict of strings) holds a marker outside its code blocks.""" 389 if isinstance(text, dict): 390 return any(has_markers(value) for value in text.values()) 391 return isinstance(text, str) and bool(_markers(text)) 392 393 394def outline(numbering: Numbering, lang=None) -> list: 395 """One line per numbered section, indented by level, the tag between brackets: what 396 ``sre check`` prints.""" 397 return [f"{' ' * (s.level - 1)}{s.number}. {s.title}{f' [{s.tag}]' if s.tag else ''}" 398 for s in numbering.sections(lang)] 399 400 401# ── the functions a lab calls (re-exported by SRE.lib_sre) ───────────────────────────────── 402 403_MAX_LEVEL = 5 # '######' is the deepest markdown header 404 405 406def info_subtitle(title, level: int = 1, tag: str = ""): 407 """A numbered header of *level* (1: ``##``, 2: ``###``...) with *title* (a ``str`` or a 408 ``tr()`` text), standing in a paragraph of its own; *tag* names the section for 409 :func:`info_ref`.""" 410 if not isinstance(level, int) or isinstance(level, bool) or not 1 <= level <= _MAX_LEVEL: 411 raise ValueError(f"info_subtitle(): level must be an integer from 1 to {_MAX_LEVEL}, not {level!r}") 412 marker = f"@@sec:{check_tag(tag)}@@" if tag else "@@sec@@" 413 return "\n\n" + "#" * (level + 1) + " " + marker + " " + title + "\n\n" 414 415 416def info_tag(tag: str) -> str: 417 """An anchor at this spot, named *tag*: a reference to it gives the number of the section it 418 stands in and scrolls the course to the spot.""" 419 return f"@@tag:{check_tag(tag)}@@" 420 421 422def info_ref(tag: str, title: bool = False) -> str: 423 """The number of the section tagged *tag* (``2.1``), followed by its title with *title* 424 (``2.1. Title``); a link to the section in the course.""" 425 return f"@@ref:{check_tag(tag)}:title@@" if title else f"@@ref:{check_tag(tag)}@@" 426 427 428def info_toc(depth=None) -> str: 429 """The table of contents of the course, in a paragraph of its own: every numbered section, or 430 the sections of the first *depth* levels.""" 431 if depth is None: 432 return "\n\n@@toc@@\n\n" 433 if not isinstance(depth, int) or isinstance(depth, bool) or depth < 1: 434 raise ValueError(f"info_toc(): depth must be a positive integer, not {depth!r}") 435 return f"\n\n@@toc:{depth}@@\n\n"
92def check_tag(tag: str) -> str: 93 """*tag* when it is a valid tag name, else :class:`ValueError`.""" 94 if not isinstance(tag, str) or not TAG_RE.fullmatch(tag): 95 raise ValueError(f"invalid tag {tag!r}: letters, digits, '_' and '-' only") 96 return tag
tag when it is a valid tag name, else ValueError.
99class Target: 100 """What a tag refers to: a section (*level* set) or a plain ``@@tag@@`` anchor (*level* None, 101 *number* and *title* those of the section it stands in).""" 102 103 __slots__ = ('number', 'title', 'anchor', 'level', 'tag') 104 105 def __init__(self, number: str, title: str, anchor: str, level, tag): 106 self.number = number 107 self.title = title 108 self.anchor = anchor 109 self.level = level 110 self.tag = tag 111 112 @property 113 def label(self) -> str: 114 """``2.1. Title``: the entry of the table of contents, the reference with a title.""" 115 return f"{self.number}. {self.title}" 116 117 def __repr__(self): 118 return f"Target({self.number!r}, {self.title!r}, anchor={self.anchor!r}, level={self.level}, tag={self.tag!r})"
What a tag refers to: a section (level set) or a plain @@tag@@ anchor (level None,
number and title those of the section it stands in).
121class Numbering: 122 """The sections and the tags of a course, per language; the first language added is the 123 default one, used for a language the course does not have and for plain strings.""" 124 125 def __init__(self): 126 self._languages = {} # lang -> (sections, targets) 127 128 def add(self, lang, sections: list, targets: dict) -> None: 129 self._languages[lang] = (sections, targets) 130 131 @property 132 def languages(self) -> list: 133 return list(self._languages) 134 135 def _entry(self, lang): 136 if lang in self._languages: 137 return self._languages[lang] 138 if self._languages: 139 return next(iter(self._languages.values())) 140 return [], {} 141 142 def sections(self, lang=None) -> list: 143 """The numbered sections, in text order (:class:`Target` objects).""" 144 return self._entry(lang)[0] 145 146 def targets(self, lang=None) -> dict: 147 """``tag -> Target`` of every tagged section and anchor.""" 148 return self._entry(lang)[1] 149 150 def check_consistent(self) -> None: 151 """Every language must declare the same sections (levels and tags) and anchors, in the 152 same order, else :class:`ValueError` (a translation missed a header).""" 153 reference = None 154 for lang, (sections, targets) in self._languages.items(): 155 signature = _signature(sections, targets) 156 if reference is None: 157 reference = (lang, signature) 158 elif signature != reference[1]: 159 raise ValueError( 160 f"the numbered sections and tags of the languages {reference[0]!r} and {lang!r} " 161 f"differ: {_describe(reference[1])} / {_describe(signature)}")
The sections and the tags of a course, per language; the first language added is the default one, used for a language the course does not have and for plain strings.
142 def sections(self, lang=None) -> list: 143 """The numbered sections, in text order (:class:`Target` objects).""" 144 return self._entry(lang)[0]
The numbered sections, in text order (Target objects).
146 def targets(self, lang=None) -> dict: 147 """``tag -> Target`` of every tagged section and anchor.""" 148 return self._entry(lang)[1]
tag -> Target of every tagged section and anchor.
150 def check_consistent(self) -> None: 151 """Every language must declare the same sections (levels and tags) and anchors, in the 152 same order, else :class:`ValueError` (a translation missed a header).""" 153 reference = None 154 for lang, (sections, targets) in self._languages.items(): 155 signature = _signature(sections, targets) 156 if reference is None: 157 reference = (lang, signature) 158 elif signature != reference[1]: 159 raise ValueError( 160 f"the numbered sections and tags of the languages {reference[0]!r} and {lang!r} " 161 f"differ: {_describe(reference[1])} / {_describe(signature)}")
Every language must declare the same sections (levels and tags) and anchors, in the
same order, else ValueError (a translation missed a header).
362def resolve_informations(text): 363 """``(resolved text, numbering)`` of a course: *text* is a ``str`` or a ``TranslatedText`` 364 (resolved language by language, which must agree on the sections).""" 365 numbering = Numbering() 366 if isinstance(text, dict): 367 result = {} 368 for lang, value in text.items(): 369 result[lang], sections, targets = _resolve_course(value) 370 numbering.add(lang, sections, targets) 371 numbering.check_consistent() 372 return type(text)(result), numbering 373 resolved, sections, targets = _resolve_course(text) 374 numbering.add('', sections, targets) 375 return resolved, numbering
(resolved text, numbering) of a course: text is a str or a TranslatedText
(resolved language by language, which must agree on the sections).
378def resolve_text(text, numbering: Numbering, links: bool = False): 379 """*text* (``str`` or ``TranslatedText``: a question title or description, a state 380 description) with its ``@@ref@@`` markers resolved against *numbering*; a reference is a link 381 only with *links* (the browser showing the text must hold the anchors of the course).""" 382 if isinstance(text, dict): 383 return type(text)({lang: _resolve_refs(value, numbering.targets(lang), links) 384 for lang, value in text.items()}) 385 return _resolve_refs(text, numbering.targets(), links)
text (str or TranslatedText: a question title or description, a state
description) with its @@ref@@ markers resolved against numbering; a reference is a link
only with links (the browser showing the text must hold the anchors of the course).
388def has_markers(text) -> bool: 389 """True when *text* (``str`` or dict of strings) holds a marker outside its code blocks.""" 390 if isinstance(text, dict): 391 return any(has_markers(value) for value in text.values()) 392 return isinstance(text, str) and bool(_markers(text))
True when text (str or dict of strings) holds a marker outside its code blocks.
395def outline(numbering: Numbering, lang=None) -> list: 396 """One line per numbered section, indented by level, the tag between brackets: what 397 ``sre check`` prints.""" 398 return [f"{' ' * (s.level - 1)}{s.number}. {s.title}{f' [{s.tag}]' if s.tag else ''}" 399 for s in numbering.sections(lang)]
One line per numbered section, indented by level, the tag between brackets: what
sre check prints.
407def info_subtitle(title, level: int = 1, tag: str = ""): 408 """A numbered header of *level* (1: ``##``, 2: ``###``...) with *title* (a ``str`` or a 409 ``tr()`` text), standing in a paragraph of its own; *tag* names the section for 410 :func:`info_ref`.""" 411 if not isinstance(level, int) or isinstance(level, bool) or not 1 <= level <= _MAX_LEVEL: 412 raise ValueError(f"info_subtitle(): level must be an integer from 1 to {_MAX_LEVEL}, not {level!r}") 413 marker = f"@@sec:{check_tag(tag)}@@" if tag else "@@sec@@" 414 return "\n\n" + "#" * (level + 1) + " " + marker + " " + title + "\n\n"
A numbered header of level (1: ##, 2: ###...) with title (a str or a
tr() text), standing in a paragraph of its own; tag names the section for
info_ref().
417def info_tag(tag: str) -> str: 418 """An anchor at this spot, named *tag*: a reference to it gives the number of the section it 419 stands in and scrolls the course to the spot.""" 420 return f"@@tag:{check_tag(tag)}@@"
An anchor at this spot, named tag: a reference to it gives the number of the section it stands in and scrolls the course to the spot.
423def info_ref(tag: str, title: bool = False) -> str: 424 """The number of the section tagged *tag* (``2.1``), followed by its title with *title* 425 (``2.1. Title``); a link to the section in the course.""" 426 return f"@@ref:{check_tag(tag)}:title@@" if title else f"@@ref:{check_tag(tag)}@@"
The number of the section tagged tag (2.1), followed by its title with title
(2.1. Title); a link to the section in the course.
429def info_toc(depth=None) -> str: 430 """The table of contents of the course, in a paragraph of its own: every numbered section, or 431 the sections of the first *depth* levels.""" 432 if depth is None: 433 return "\n\n@@toc@@\n\n" 434 if not isinstance(depth, int) or isinstance(depth, bool) or depth < 1: 435 raise ValueError(f"info_toc(): depth must be a positive integer, not {depth!r}") 436 return f"\n\n@@toc:{depth}@@\n\n"
The table of contents of the course, in a paragraph of its own: every numbered section, or the sections of the first depth levels.