···318318{option}`services.systemd.akkoma.serviceConfig.BindPaths` and
319319{option}`services.systemd.akkoma.serviceConfig.BindReadOnlyPaths` permit access to outside paths
320320through bind mounts. Refer to
321321-[{manpage}`systemd.exec(5)`](https://www.freedesktop.org/software/systemd/man/systemd.exec.html#BindPaths=)
322322-for details.
321321+[`BindPaths=`](https://www.freedesktop.org/software/systemd/man/systemd.exec.html#BindPaths=)
322322+of {manpage}`systemd.exec(5)` for details.
323323324324### Distributed deployment {#modules-services-akkoma-distributed-deployment}
325325
+1-1
nixos/modules/system/boot/networkd.nix
···19481948 Extra command-line arguments to pass to systemd-networkd-wait-online.
19491949 These also affect per-interface `systemd-network-wait-online@` services.
1950195019511951- See [{manpage}`systemd-networkd-wait-online.service(8)`](https://www.freedesktop.org/software/systemd/man/systemd-networkd-wait-online.service.html) for all available options.
19511951+ See {manpage}`systemd-networkd-wait-online.service(8)` for all available options.
19521952 '';
19531953 type = with types; listOf str;
19541954 default = [];
···11+from __future__ import annotations
22+33+import dataclasses as dc
44+import html
55+import itertools
66+77+from typing import cast, get_args, Iterable, Literal, Sequence
88+99+from markdown_it.token import Token
1010+1111+from .utils import Freezeable
1212+1313+# FragmentType is used to restrict structural include blocks.
1414+FragmentType = Literal['preface', 'part', 'chapter', 'section', 'appendix']
1515+1616+# in the TOC all fragments are allowed, plus the all-encompassing book.
1717+TocEntryType = Literal['book', 'preface', 'part', 'chapter', 'section', 'appendix']
1818+1919+def is_include(token: Token) -> bool:
2020+ return token.type == "fence" and token.info.startswith("{=include=} ")
2121+2222+# toplevel file must contain only the title headings and includes, anything else
2323+# would cause strange rendering.
2424+def _check_book_structure(tokens: Sequence[Token]) -> None:
2525+ for token in tokens[6:]:
2626+ if not is_include(token):
2727+ assert token.map
2828+ raise RuntimeError(f"unexpected content in line {token.map[0] + 1}, "
2929+ "expected structural include")
3030+3131+# much like books, parts may not contain headings other than their title heading.
3232+# this is a limitation of the current renderers and TOC generators that do not handle
3333+# this case well even though it is supported in docbook (and probably supportable
3434+# anywhere else).
3535+def _check_part_structure(tokens: Sequence[Token]) -> None:
3636+ _check_fragment_structure(tokens)
3737+ for token in tokens[3:]:
3838+ if token.type == 'heading_open':
3939+ assert token.map
4040+ raise RuntimeError(f"unexpected heading in line {token.map[0] + 1}")
4141+4242+# two include blocks must either be adjacent or separated by a heading, otherwise
4343+# we cannot generate a correct TOC (since there'd be nothing to link to between
4444+# the two includes).
4545+def _check_fragment_structure(tokens: Sequence[Token]) -> None:
4646+ for i, token in enumerate(tokens):
4747+ if is_include(token) \
4848+ and i + 1 < len(tokens) \
4949+ and not (is_include(tokens[i + 1]) or tokens[i + 1].type == 'heading_open'):
5050+ assert token.map
5151+ raise RuntimeError(f"unexpected content in line {token.map[0] + 1}, "
5252+ "expected heading or structural include")
5353+5454+def check_structure(kind: TocEntryType, tokens: Sequence[Token]) -> None:
5555+ wanted = { 'h1': 'title' }
5656+ wanted |= { 'h2': 'subtitle' } if kind == 'book' else {}
5757+ for (i, (tag, role)) in enumerate(wanted.items()):
5858+ if len(tokens) < 3 * (i + 1):
5959+ raise RuntimeError(f"missing {role} ({tag}) heading")
6060+ token = tokens[3 * i]
6161+ if token.type != 'heading_open' or token.tag != tag:
6262+ assert token.map
6363+ raise RuntimeError(f"expected {role} ({tag}) heading in line {token.map[0] + 1}", token)
6464+ for t in tokens[3 * len(wanted):]:
6565+ if t.type != 'heading_open' or not (role := wanted.get(t.tag, '')):
6666+ continue
6767+ assert t.map
6868+ raise RuntimeError(
6969+ f"only one {role} heading ({t.markup} [text...]) allowed per "
7070+ f"{kind}, but found a second in line {t.map[0] + 1}. "
7171+ "please remove all such headings except the first or demote the subsequent headings.",
7272+ t)
7373+7474+ last_heading_level = 0
7575+ for token in tokens:
7676+ if token.type != 'heading_open':
7777+ continue
7878+7979+ # book subtitle headings do not need an id, only book title headings do.
8080+ # every other headings needs one too. we need this to build a TOC and to
8181+ # provide stable links if the manual changes shape.
8282+ if 'id' not in token.attrs and (kind != 'book' or token.tag != 'h2'):
8383+ assert token.map
8484+ raise RuntimeError(f"heading in line {token.map[0] + 1} does not have an id")
8585+8686+ level = int(token.tag[1:]) # because tag = h1..h6
8787+ if level > last_heading_level + 1:
8888+ assert token.map
8989+ raise RuntimeError(f"heading in line {token.map[0] + 1} skips one or more heading levels, "
9090+ "which is currently not allowed")
9191+ last_heading_level = level
9292+9393+ if kind == 'book':
9494+ _check_book_structure(tokens)
9595+ elif kind == 'part':
9696+ _check_part_structure(tokens)
9797+ else:
9898+ _check_fragment_structure(tokens)
9999+100100+@dc.dataclass(frozen=True)
101101+class XrefTarget:
102102+ id: str
103103+ """link label for `[](#local-references)`"""
104104+ title_html: str
105105+ """toc label"""
106106+ toc_html: str | None
107107+ """text for `<title>` tags and `title="..."` attributes"""
108108+ title: str | None
109109+ """path to file that contains the anchor"""
110110+ path: str
111111+ """whether to drop the `#anchor` from links when expanding xrefs"""
112112+ drop_fragment: bool = False
113113+114114+ def href(self) -> str:
115115+ path = html.escape(self.path, True)
116116+ return path if self.drop_fragment else f"{path}#{html.escape(self.id, True)}"
117117+118118+@dc.dataclass
119119+class TocEntry(Freezeable):
120120+ kind: TocEntryType
121121+ target: XrefTarget
122122+ parent: TocEntry | None = None
123123+ prev: TocEntry | None = None
124124+ next: TocEntry | None = None
125125+ children: list[TocEntry] = dc.field(default_factory=list)
126126+ starts_new_chunk: bool = False
127127+128128+ @property
129129+ def root(self) -> TocEntry:
130130+ return self.parent.root if self.parent else self
131131+132132+ @classmethod
133133+ def of(cls, token: Token) -> TocEntry:
134134+ entry = token.meta.get('TocEntry')
135135+ if not isinstance(entry, TocEntry):
136136+ raise RuntimeError('requested toc entry, none found', token)
137137+ return entry
138138+139139+ @classmethod
140140+ def collect_and_link(cls, xrefs: dict[str, XrefTarget], tokens: Sequence[Token]) -> TocEntry:
141141+ result = cls._collect_entries(xrefs, tokens, 'book')
142142+143143+ def flatten_with_parent(this: TocEntry, parent: TocEntry | None) -> Iterable[TocEntry]:
144144+ this.parent = parent
145145+ return itertools.chain([this], *[ flatten_with_parent(c, this) for c in this.children ])
146146+147147+ flat = list(flatten_with_parent(result, None))
148148+ prev = flat[0]
149149+ prev.starts_new_chunk = True
150150+ paths_seen = set([prev.target.path])
151151+ for c in flat[1:]:
152152+ if prev.target.path != c.target.path and c.target.path not in paths_seen:
153153+ c.starts_new_chunk = True
154154+ c.prev, prev.next = prev, c
155155+ prev = c
156156+ paths_seen.add(c.target.path)
157157+158158+ for c in flat:
159159+ c.freeze()
160160+161161+ return result
162162+163163+ @classmethod
164164+ def _collect_entries(cls, xrefs: dict[str, XrefTarget], tokens: Sequence[Token],
165165+ kind: TocEntryType) -> TocEntry:
166166+ # we assume that check_structure has been run recursively over the entire input.
167167+ # list contains (tag, entry) pairs that will collapse to a single entry for
168168+ # the full sequence.
169169+ entries: list[tuple[str, TocEntry]] = []
170170+ for token in tokens:
171171+ if token.type.startswith('included_') and (included := token.meta.get('included')):
172172+ fragment_type_str = token.type[9:].removesuffix('s')
173173+ assert fragment_type_str in get_args(TocEntryType)
174174+ fragment_type = cast(TocEntryType, fragment_type_str)
175175+ for fragment, _path in included:
176176+ entries[-1][1].children.append(cls._collect_entries(xrefs, fragment, fragment_type))
177177+ elif token.type == 'heading_open' and (id := cast(str, token.attrs.get('id', ''))):
178178+ while len(entries) > 1 and entries[-1][0] >= token.tag:
179179+ entries[-2][1].children.append(entries.pop()[1])
180180+ entries.append((token.tag,
181181+ TocEntry(kind if token.tag == 'h1' else 'section', xrefs[id])))
182182+ token.meta['TocEntry'] = entries[-1][1]
183183+184184+ while len(entries) > 1:
185185+ entries[-2][1].children.append(entries.pop()[1])
186186+ return entries[0][1]
···11+from typing import Any
22+33+_frozen_classes: dict[type, type] = {}
44+55+# make a derived class freezable (ie, disallow modifications).
66+# we do this by changing the class of an instance at runtime when freeze()
77+# is called, providing a derived class that is exactly the same except
88+# for a __setattr__ that raises an error when called. this beats having
99+# a field for frozenness and an unconditional __setattr__ that checks this
1010+# field because it does not insert anything into the class dict.
1111+class Freezeable:
1212+ def freeze(self) -> None:
1313+ cls = type(self)
1414+ if not (frozen := _frozen_classes.get(cls)):
1515+ def __setattr__(instance: Any, n: str, v: Any) -> None:
1616+ raise TypeError(f'{cls.__name__} is frozen')
1717+ frozen = type(cls.__name__, (cls,), {
1818+ '__setattr__': __setattr__,
1919+ })
2020+ _frozen_classes[cls] = frozen
2121+ self.__class__ = frozen
···11-import nixos_render_docs
11+import nixos_render_docs as nrd
2233from sample_md import sample1
44···6677import markdown_it
8899-class Converter(nixos_render_docs.md.Converter):
1010- __renderer__ = nixos_render_docs.commonmark.CommonMarkRenderer
99+class Converter(nrd.md.Converter[nrd.commonmark.CommonMarkRenderer]):
1010+ def __init__(self, manpage_urls: Mapping[str, str]):
1111+ super().__init__()
1212+ self._renderer = nrd.commonmark.CommonMarkRenderer(manpage_urls)
11131214# NOTE: in these tests we represent trailing spaces by ` ` and replace them with real space later,
1315# since a number of editors will strip trailing whitespace on save and that would break the tests.