3
0
Fork 0
forked from mirrors/nixpkgs

nixos-render-docs: track links in manpages

for the longest time we completely dropped link targets in
configuration.nix.5.  let's stop doing this now and instead provide a
footnote for each link in a given option, numbered locally per option.

we will currently duplicate the link for <labelless-links> because it
makes it easier to get the collection of all links in a given option.
this may not be useful enough, so over time we might decide to drop the
footnotes for such links.
This commit is contained in:
pennae 2023-02-02 06:15:59 +01:00 committed by pennae
parent 3c7fd940ba
commit 78052a22cb
4 changed files with 58 additions and 7 deletions

View file

@ -81,9 +81,11 @@ class ManpageRenderer(Renderer):
# mainly used by the options manpage converter to not emit extra quotes in defaults
# and examples where it's already clear from context that the following text is code.
inline_code_is_quoted: bool = True
link_footnotes: Optional[list[str]] = None
_href_targets: dict[str, str]
_link_stack: list[str]
_do_parbreak_stack: list[bool]
_list_stack: list[List]
_font_stack: list[str]
@ -92,6 +94,7 @@ class ManpageRenderer(Renderer):
parser: Optional[markdown_it.MarkdownIt] = None):
super().__init__(manpage_urls, parser)
self._href_targets = href_targets
self._link_stack = []
self._do_parbreak_stack = []
self._list_stack = []
self._font_stack = []
@ -154,6 +157,7 @@ class ManpageRenderer(Renderer):
def link_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict,
env: MutableMapping[str, Any]) -> str:
href = cast(str, token.attrs['href'])
self._link_stack.append(href)
text = ""
if tokens[i + 1].type == 'link_close' and href in self._href_targets:
# TODO error or warning if the target can't be resolved
@ -162,8 +166,17 @@ class ManpageRenderer(Renderer):
return f"\\fB{text}\0 <"
def link_close(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict,
env: MutableMapping[str, Any]) -> str:
href = self._link_stack.pop()
text = ""
if self.link_footnotes is not None:
try:
idx = self.link_footnotes.index(href) + 1
except ValueError:
self.link_footnotes.append(href)
idx = len(self.link_footnotes)
text = "\\fR" + man_escape(f"[{idx}]")
self._font_stack.pop()
return f">\0 {self._font_stack[-1]}"
return f">\0 {text}{self._font_stack[-1]}"
def list_item_open(self, token: Token, tokens: Sequence[Token], i: int, options: OptionsDict,
env: MutableMapping[str, Any]) -> str:
self._enter_block()

View file

@ -148,12 +148,15 @@ class BaseConverter(Converter):
return [ l for part in blocks for l in part ]
def _render_option(self, name: str, option: dict[str, Any]) -> RenderedOption:
try:
return RenderedOption(option['loc'], self._convert_one(option))
except Exception as e:
raise Exception(f"Failed to render option {name}") from e
def add_options(self, options: dict[str, Any]) -> None:
for (name, option) in options.items():
try:
self._options[name] = RenderedOption(option['loc'], self._convert_one(option))
except Exception as e:
raise Exception(f"Failed to render option {name}") from e
self._options[name] = self._render_option(name, option)
@abstractmethod
def finalize(self) -> str: raise NotImplementedError()
@ -277,11 +280,19 @@ class ManpageConverter(BaseConverter):
__option_block_separator__ = ".sp"
_options_by_id: dict[str, str]
_links_in_last_description: Optional[list[str]] = None
def __init__(self, revision: str, markdown_by_default: bool):
self._options_by_id = {}
super().__init__({}, revision, markdown_by_default)
def _render_option(self, name: str, option: dict[str, Any]) -> RenderedOption:
assert isinstance(self._md.renderer, OptionsManpageRenderer)
links = self._md.renderer.link_footnotes = []
result = super()._render_option(name, option)
self._md.renderer.link_footnotes = None
return result._replace(links=links)
def add_options(self, options: dict[str, Any]) -> None:
for (k, v) in options.items():
self._options_by_id[f'#{make_xml_id(f"opt-{k}")}'] = k
@ -356,6 +367,17 @@ class ManpageConverter(BaseConverter):
".RS 4",
]
result += opt.lines
if links := opt.links:
result.append(self.__option_block_separator__)
md_links = ""
for i in range(0, len(links)):
md_links += "\n" if i > 0 else ""
if links[i].startswith('#opt-'):
md_links += f"{i+1}. see the {{option}}`{self._options_by_id[links[i]]}` option"
else:
md_links += f"{i+1}. " + md_escape(links[i])
result.append(self._render(md_links))
result.append(".RE")
result += [

View file

@ -7,7 +7,9 @@ from markdown_it.utils import OptionsDict
OptionLoc = str | dict[str, str]
Option = dict[str, str | dict[str, str] | list[OptionLoc]]
RenderedOption = NamedTuple('RenderedOption', [('loc', list[str]),
('lines', list[str])])
class RenderedOption(NamedTuple):
loc: list[str]
lines: list[str]
links: Optional[list[str]] = None
RenderFn = Callable[[Token, Sequence[Token], int, OptionsDict, MutableMapping[str, Any]], str]

View file

@ -27,3 +27,17 @@ def test_expand_link_targets() -> None:
c = Converter({}, { '#foo1': "bar", "#foo2": "bar" })
assert (c._render("[a](#foo1) [](#foo2) [b](#bar1) [](#bar2)") ==
"\\fBa\\fR \\fBbar\\fR \\fBb\\fR \\fB\\fR")
def test_collect_links() -> None:
c = Converter({}, { '#foo': "bar" })
assert isinstance(c._md.renderer, nixos_render_docs.manpage.ManpageRenderer)
c._md.renderer.link_footnotes = []
assert c._render("[a](link1) [b](link2)") == "\\fBa\\fR[1]\\fR \\fBb\\fR[2]\\fR"
assert c._md.renderer.link_footnotes == ['link1', 'link2']
def test_dedup_links() -> None:
c = Converter({}, { '#foo': "bar" })
assert isinstance(c._md.renderer, nixos_render_docs.manpage.ManpageRenderer)
c._md.renderer.link_footnotes = []
assert c._render("[a](link) [b](link)") == "\\fBa\\fR[1]\\fR \\fBb\\fR[1]\\fR"
assert c._md.renderer.link_footnotes == ['link']