dwarfs/cmake/render_manpage.py
2024-08-02 16:22:41 +02:00

329 lines
10 KiB
Python

import mistletoe
import sys
class RenderContext:
def __init__(self):
self.line = 0
self.level = 0
self.indent = 4
self.section = None
def get_indent(self, add=0):
return self.indent * (max(0, self.level + add))
class Element:
def __init__(self, tags, content=None, comment=None):
self.tags = tags
self.content = content
self.comment = comment
def __repr__(self):
return f"Element({self.tags}, {self.content}, {self.comment})"
def tags_to_style(self):
style = []
if "b" in self.tags:
style.append("fmt::emphasis::bold")
if "i" in self.tags:
style.append("fmt::emphasis::italic")
if "head" in self.tags:
style.append(
"fmt::fg(fmt::terminal_color::bright_green) | fmt::emphasis::bold"
)
if "code" in self.tags:
style.append(
"fmt::fg(fmt::terminal_color::bright_blue) | fmt::emphasis::bold"
)
if "block" in self.tags:
style.append("fmt::emphasis::faint")
if len(style) == 0:
return "{}"
return " | ".join(style)
def render(self, context):
return f'{{{self.tags_to_style()} /* {self.tags} */, R"({self.content})"}} /* {self.comment} */'
def apply(elements, *tags):
return [Element({*tags, *e.tags}, e.content, e.comment) for e in elements]
class Line:
def __init__(self, elements=None, indent_first=0, indent=None, comment=None):
self.elements = [] if elements is None else elements
self.indent_first = indent_first
self.indent = indent_first if indent is None else indent
self.comment = comment
def split(self):
rv = []
cur = []
indent_first = self.indent_first
for e in self.elements:
if e.comment == "line break":
rv.append(Line(cur, indent_first, self.indent, comment=self.comment))
indent_first = self.indent
cur = []
else:
cur.append(e)
rv.append(Line(cur, indent_first, self.indent, comment=self.comment))
return rv
def join(self):
rv = []
for e in self.elements:
if e.comment == "line break":
rv.append(Element(set(), " ", "whitespace"))
else:
rv.append(e)
return Line(rv, self.indent_first, self.indent, comment=self.comment)
def render(self, context):
template = (
"constexpr uint32_t const line{0}_indent_first{{{2}}};\n"
"constexpr uint32_t const line{0}_indent_next{{{3}}};\n"
"constexpr std::array<element, {1}> const line{0}_elements{{{{\n"
"{4}"
"}}}};\n\n"
)
rv = ""
if self.comment is not None:
rv += "// " + self.comment + "\n"
rv += template.format(
context.line,
len(self.elements),
self.indent_first,
self.indent,
"".join([f" {e.render(context)},\n" for e in self.elements]),
)
context.line += 1
return rv
class Paragraph:
def __init__(self, elements, comment=None):
self.elements = elements
self.comment = comment
def render(self, context, indent_first=None, indent=None):
if indent_first is None:
indent_first = context.get_indent()
if indent is None:
indent = indent_first
line = Line(self.elements, indent_first, indent, comment=self.comment)
if context.section is not None and context.section == "SYNOPSIS":
lines = line.split()
else:
lines = [line.join()]
return "".join([l.render(context) for l in lines]) + Line().render(context)
class Heading:
def __init__(self, elements, level, section=None, comment=None):
self.elements = elements
self.level = level
self.section = section
self.comment = comment
def __repr__(self):
return f"Heading({self.level}, {self.section}, {self.comment})"
def render(self, context):
context.level = 2
indent = context.get_indent(-2 if self.level <= 2 else -1)
context.section = self.section
rv = Line(apply(self.elements, "head"), indent, comment=self.comment).render(
context
)
if self.level == 1:
rv += Line().render(context)
return rv
class ListItem:
def __init__(self, paragraphs, comment=None):
self.paragraphs = paragraphs
self.comment = comment
def __repr__(self):
return f"ListItem({self.paragraphs}, {self.comment})"
def render(self, context):
rv = ""
for i, p in enumerate(self.paragraphs):
handled = False
if i == 0:
content_len = 0
for i in range(len(p.elements) - 1):
cur = p.elements[i]
nxt = p.elements[i + 1]
content_len += len(cur.content)
if cur.content.endswith(":") and nxt.comment == "line break":
cur.content = cur.content[:-1]
content_len -= 1
if content_len < (2 * context.indent - 1):
cur.content += " " * (2 * context.indent - content_len)
p.elements = p.elements[: i + 1] + p.elements[i + 2 :]
rv += p.render(
context,
indent_first=context.get_indent(),
indent=context.get_indent(2),
)
else:
rv += Line(
p.elements[:i + 1], context.get_indent(), comment=p.comment
).render(context)
p.elements = p.elements[i + 2 :]
rv += p.render(context, indent_first=context.get_indent(2))
handled = True
break
if not handled:
rv += p.render(context)
return rv
class BlockCode:
def __init__(self, element):
self.element = element
def render(self, context):
lines = self.element.content.split("\n")
rv = ""
for line in lines:
rv += Line(
[Element({"block"}, line)],
context.get_indent(1),
comment=self.element.comment,
).render(context)
return rv
class ManpageRenderer(mistletoe.base_renderer.BaseRenderer):
def __init__(self, document_name, *extras, **kwargs):
self.__document_name = document_name
super().__init__(*extras, **kwargs)
def render_inner(self, token, tags=None):
rv = []
for child in token.children:
c = self.render(child)
if isinstance(c, list):
rv.extend(c)
else:
rv.append(c)
if tags is not None:
for child in rv:
child.tags.update(tags)
return rv
@staticmethod
def render_thematic_break(token):
raise NotImplementedError
@staticmethod
def render_line_break(token):
return Element(set(), "", "line break")
def render_inline_code(self, token):
assert len(token.children) == 1
return Element({"code"}, token.children[0].content, "inline code")
def render_raw_text(self, token, escape=True):
return Element(set(), token.content, "raw text")
def render_strikethrough(self, token):
raise NotImplementedError
def render_escape_sequence(self, token):
return self.render_inner(token)
def render_strong(self, token):
return self.render_inner(token, {"b"})
def render_emphasis(self, token):
return self.render_inner(token, {"i"})
def render_image(self, token):
raise NotImplementedError
def render_heading(self, token):
inner = self.render_inner(token, {"h{}".format(token.level)})
section = None
if len(inner) == 1:
assert isinstance(inner[0], Element)
section = inner[0].content
return Heading(inner, token.level, section)
def render_paragraph(self, token):
inner = self.render_inner(token)
return Paragraph(inner, "paragraph")
def render_block_code(self, token):
inner = self.render_inner(token)
assert len(inner) == 1
assert isinstance(inner[0], Element)
return BlockCode(inner[0])
def render_list(self, token):
return [self.render(child) for child in token.children]
def render_list_item(self, token):
inner = self.render_inner(token)
assert isinstance(inner, list)
assert isinstance(inner[0], Paragraph)
return ListItem(inner, "list item")
def render_table(self, token):
raise NotImplementedError
def render_table_row(self, token):
raise NotImplementedError
def render_math(self, token):
raise NotImplementedError
def render_table_cell(self, token):
raise NotImplementedError
def render_document(self, token):
rv = """#include <array>
#include <dwarfs/tool/manpage.h>
namespace dwarfs::tool::manpage {
namespace {
"""
ctx = RenderContext()
for child in token.children:
r = self.render(child)
rv += f"#if 0\n{r}\n#endif\n"
if isinstance(r, list):
for e in r:
rv += e.render(ctx)
else:
rv += r.render(ctx)
rv += f"constexpr std::array<line, {ctx.line}> const document_array{{{{\n"
for i in range(ctx.line):
rv += f" {{line{i}_indent_first, line{i}_indent_next, line{i}_elements}},\n"
rv += f"""}}}};
}} // namespace
document get_{self.__document_name}_manpage() {{ return document_array; }}
}} // namespace dwarfs::tool::manpage
"""
return rv
doc_name = sys.argv[1]
input_file = sys.argv[2]
output_file = sys.argv[3]
with open(input_file, "r") as fin:
with ManpageRenderer(doc_name) as renderer:
doc = renderer.render(mistletoe.Document(fin))
with open(output_file, "w") as fout:
fout.write(doc)