diff options
author | Jussi Pakkanen <jpakkane@gmail.com> | 2022-01-07 18:55:07 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-01-07 18:55:07 +0200 |
commit | 8b54ee58efe5dc8f2cb23a1e54578090243bc599 (patch) | |
tree | 6ec94ebe2dfe402d22296ced40bb81789c6fb4d4 | |
parent | 484a389bbe186120f3328b6c47c86b087f32a530 (diff) | |
parent | 2c4a98f851c4a363cb8dd83b2270243ce2990442 (diff) | |
download | meson-8b54ee58efe5dc8f2cb23a1e54578090243bc599.zip meson-8b54ee58efe5dc8f2cb23a1e54578090243bc599.tar.gz meson-8b54ee58efe5dc8f2cb23a1e54578090243bc599.tar.bz2 |
Merge pull request #9773 from annacrombie/master
Add a man page backend to refman
-rw-r--r-- | .github/workflows/website.yml | 7 | ||||
-rw-r--r-- | docs/markdown/snippets/refman_man_page_backend.md | 4 | ||||
-rw-r--r-- | docs/meson.build | 16 | ||||
-rw-r--r-- | docs/refman/generatorbase.py | 5 | ||||
-rw-r--r-- | docs/refman/generatorjson.py | 7 | ||||
-rw-r--r-- | docs/refman/generatorman.py | 382 | ||||
-rw-r--r-- | docs/refman/main.py | 4 |
7 files changed, 417 insertions, 8 deletions
diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index a71ee33..e3d3821 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -61,4 +61,11 @@ jobs: file: docs/_build/reference_manual.json tag: ${{ github.ref }} if: ${{ github.event_name == 'release' }} + - name: Release the current man docs + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: docs/_build/meson-reference.3 + tag: ${{ github.ref }} + if: ${{ github.event_name == 'release' }} diff --git a/docs/markdown/snippets/refman_man_page_backend.md b/docs/markdown/snippets/refman_man_page_backend.md new file mode 100644 index 0000000..2a774e2 --- /dev/null +++ b/docs/markdown/snippets/refman_man_page_backend.md @@ -0,0 +1,4 @@ +## Add a man page backend to refman + +The refman docs (function and object reference) can now be generated as a man +page. diff --git a/docs/meson.build b/docs/meson.build index 53d4392..00e90ed 100644 --- a/docs/meson.build +++ b/docs/meson.build @@ -63,6 +63,22 @@ refman_json = custom_target( ], ) +refman_man = custom_target( + 'gen_refman_man', + build_by_default: true, + input: refman_binary, + output: 'meson-reference.3', + command: [ + genrefman, + '-l', 'pickle', + '-g', 'man', + '-i', '@INPUT@', + '-o', '@OUTPUT@', + '--force-color', + '--no-modules', + ], +) + test('validate_docs', find_program('./jsonvalidator.py'), args: [refman_json]) hotdoc_prog = find_program('hotdoc', version: '>=0.13.7') diff --git a/docs/refman/generatorbase.py b/docs/refman/generatorbase.py index e93166f..08ce492 100644 --- a/docs/refman/generatorbase.py +++ b/docs/refman/generatorbase.py @@ -43,6 +43,11 @@ class GeneratorBase(metaclass=ABCMeta): return f'0_{fn.name}' return sorted([x for x in raw if not x.hidden], key=key_fn) + @staticmethod + def _extract_meson_version() -> str: + from mesonbuild.coredata import version + return version + @property def functions(self) -> T.List[Function]: return GeneratorBase.sorted_and_filtered(self.manual.functions) diff --git a/docs/refman/generatorjson.py b/docs/refman/generatorjson.py index f5164d4..9f92342 100644 --- a/docs/refman/generatorjson.py +++ b/docs/refman/generatorjson.py @@ -92,13 +92,6 @@ class GeneratorJSON(GeneratorBase): 'methods': {x.name: self._generate_function(x) for x in self.sorted_and_filtered(obj.methods)}, } - def _extract_meson_version(self) -> str: - # Hack around python relative imports to get to the Meson version - import sys - sys.path.append(Path(__file__).resolve().parents[2].as_posix()) - from mesonbuild.coredata import version - return version - def generate(self) -> None: data: J.Root = { 'version_major': J.VERSION_MAJOR, diff --git a/docs/refman/generatorman.py b/docs/refman/generatorman.py new file mode 100644 index 0000000..dfd211f --- /dev/null +++ b/docs/refman/generatorman.py @@ -0,0 +1,382 @@ +import re +from pathlib import Path + +from .generatorbase import GeneratorBase +from .model import ( + ReferenceManual, + Function, + Object, + PosArg, + VarArgs, + Kwarg, +) + +import typing as T + + +class ManPage: + def __init__(self, path: Path): + self.path = path + self.text = "" + + def reset_font(self) -> None: + self.text += ".P\n" + + def title(self, name: str, section: int) -> None: + import datetime + + date = datetime.date.today() + self.reset_font() + self.text += f'.TH "{name}" "{section}" "{date}"\n' + + def section(self, name: str) -> None: + self.reset_font() + self.text += f".SH {name}\n" + + def subsection(self, name: str) -> None: + self.reset_font() + self.text += f".SS {name}\n" + + def par(self, text: str) -> None: + self.reset_font() + self.text += f"{text}\n" + + def indent(self, amount: int = 4) -> None: + self.text += f".RS {amount}\n" + + def unindent(self) -> None: + self.text += ".RE\n" + + def br(self) -> None: + self.text += ".br\n" + + def nl(self) -> None: + self.text += "\n" + + def line(self, text: str) -> None: + if text and text[0] in [".", "'"]: + self.text += "\\" + self.text += f"{text}\n" + + def inline(self, text: str) -> None: + self.text += f"{text}" + + def write(self) -> None: + self.path.write_text(self.text, encoding="utf-8") + + @staticmethod + def bold(text: str) -> str: + return f"\\fB{text}\\fR" + + @staticmethod + def italic(text: str) -> str: + return f"\\fI{text}\\fR" + + +class GeneratorMan(GeneratorBase): + def __init__( + self, manual: ReferenceManual, out: Path, enable_modules: bool + ) -> None: + super().__init__(manual) + self.out = out + self.enable_modules = enable_modules + self.links: T.List[str] = [] + + def generate_description(self, page: ManPage, desc: str) -> None: + def italicise(match: T.Match[str]) -> str: + v = match.group(1) + if v[0] == "@": + v = v[1:] + + return ManPage.italic(v) + + desc = re.sub(re.compile(r"\[\[(.*?)\]\]", re.DOTALL), italicise, desc) + + def linkify(match: T.Match[str]) -> str: + replacement = ManPage.italic(match.group(1)) + + if match.group(2)[0] != "#": + if match.group(2) in self.links: + num = self.links.index(match.group(2)) + else: + self.links.append(match.group(2)) + num = len(self.links) + + replacement += f"[{num}]" + + return replacement + + desc = re.sub(re.compile(r"\[(.*?)\]\((.*?)\)", re.DOTALL), linkify, desc) + + def bold(match: T.Match[str]) -> str: + return ManPage.bold(match.group(1)) + + desc = re.sub(re.compile(r"\*(.*?)\*"), bold, desc) + + isCode = False + for chunk in desc.split("```"): + if isCode: + page.indent() + lines = chunk.strip().split("\n") + if lines[0] == "meson": + lines = lines[1:] + + for line in lines: + page.line(line) + page.br() + page.unindent() + else: + inList = False + for line in chunk.strip().split("\n"): + if len(line) == 0: + page.nl() + if inList: + page.nl() + inList = False + elif line[0:2] in ["- ", "* "]: + if inList: + page.nl() + page.br() + else: + inList = True + + page.inline(line.strip() + " ") + elif inList and line[0] == " ": + page.inline(line.strip() + " ") + else: + inList = False + page.line(line) + + if inList: + page.nl() + + isCode = not isCode + + def function_name(self, f: Function, o: Object = None) -> str: + name = "" + if o is not None: + name += f"{o.name}." + + name += f.name + return name + + def generate_function_signature( + self, page: ManPage, f: Function, o: Object = None + ) -> None: + args = [] + + if f.posargs: + args += [arg.name for arg in f.posargs] + + if f.varargs: + args += [f.varargs.name + "..."] + + if f.optargs: + args += [f"[{arg.name}]" for arg in f.optargs] + + for kwarg in self.sorted_and_filtered(list(f.kwargs.values())): + kw = kwarg.name + ":" + if kwarg.default: + kw += " " + ManPage.bold(kwarg.default) + args += [kw] + + ret = ManPage.italic(f.returns.raw) + " " + + prefix = f"{ret}{self.function_name(f, o)}(" + sig = ", ".join(args) + suffix = ")" + + if len(prefix) + len(sig) + len(suffix) > 70: + page.line(prefix) + page.br() + page.indent() + for arg in args: + page.line(arg + ",") + page.br() + page.unindent() + page.line(suffix) + else: + page.line(prefix + sig + suffix) + + def base_info( + self, x: T.Union[PosArg, VarArgs, Kwarg, Function, Object] + ) -> T.List[str]: + info = [] + if x.deprecated: + info += [ManPage.bold("deprecated") + f" since {x.deprecated}"] + if x.since: + info += [f"since {x.since}"] + + return info + + def generate_function_arg( + self, + page: ManPage, + arg: T.Union[PosArg, VarArgs, Kwarg], + isOptarg: bool = False, + ) -> None: + required = ( + arg.required + if isinstance(arg, Kwarg) + else not isOptarg and not isinstance(arg, VarArgs) + ) + + page.line(ManPage.bold(arg.name)) + + info = [ManPage.italic(arg.type.raw)] + + if required: + info += [ManPage.bold("required")] + if isinstance(arg, (PosArg, Kwarg)) and arg.default: + info += [f"default: {arg.default}"] + if isinstance(arg, VarArgs): + mn = 0 if arg.min_varargs < 0 else arg.min_varargs + mx = "N" if arg.max_varargs < 0 else arg.max_varargs + info += [f"{mn}...{mx} times"] + + info += self.base_info(arg) + + page.line(", ".join(info)) + + page.br() + page.indent(2) + self.generate_description(page, arg.description.strip()) + page.unindent() + page.nl() + + def generate_function_argument_section( + self, + page: ManPage, + name: str, + args: T.Sequence[T.Union[PosArg, VarArgs, Kwarg]], + isOptarg: bool = False, + ) -> None: + if not args: + return + + page.line(ManPage.bold(name)) + page.indent() + for arg in args: + self.generate_function_arg(page, arg, isOptarg) + page.unindent() + + def generate_sub_sub_section( + self, page: ManPage, name: str, text: T.List[str], process: bool = True + ) -> None: + page.line(ManPage.bold(name)) + page.indent() + if process: + for line in text: + self.generate_description(page, line.strip()) + else: + page.line("\n\n".join([line.strip() for line in text])) + page.unindent() + + def generate_function(self, page: ManPage, f: Function, obj: Object = None) -> None: + page.subsection(self.function_name(f, obj) + "()") + page.indent(0) + + page.line(ManPage.bold("SYNOPSIS")) + page.indent() + self.generate_function_signature(page, f, obj) + + info = self.base_info(f) + if info: + page.nl() + page.line(", ".join(info)) + page.unindent() + page.nl() + + self.generate_sub_sub_section(page, "DESCRIPTION", [f.description]) + page.nl() + + self.generate_function_argument_section(page, "POSARGS", f.posargs) + if f.varargs: + self.generate_function_argument_section(page, "VARARGS", [f.varargs]) + self.generate_function_argument_section(page, "OPTARGS", f.optargs, True) + self.generate_function_argument_section( + page, "KWARGS", self.sorted_and_filtered(list(f.kwargs.values())) + ) + + if f.notes: + self.generate_sub_sub_section(page, "NOTES", f.notes) + if f.warnings: + self.generate_sub_sub_section(page, "WARNINGS", f.warnings) + if f.example: + self.generate_sub_sub_section(page, "EXAMPLE", [f.example]) + + page.unindent() + + def generate_object(self, page: ManPage, obj: Object) -> None: + page.subsection(obj.name) + page.indent(2) + + info = self.base_info(obj) + if info: + page.line(", ".join(info)) + page.br() + + if obj.extends: + page.line(ManPage.bold("extends: ") + obj.extends) + page.br() + + ret = [x.name for x in self.sorted_and_filtered(obj.returned_by)] + if ret: + page.line(ManPage.bold("returned_by: ") + ", ".join(ret)) + page.br() + + ext = [x.name for x in self.sorted_and_filtered(obj.extended_by)] + if ext: + page.line(ManPage.bold("extended_by: ") + ", ".join(ext)) + page.br() + + page.nl() + + self.generate_description(page, obj.description.strip()) + page.nl() + + if obj.notes: + self.generate_sub_sub_section(page, "NOTES", obj.notes) + if obj.warnings: + self.generate_sub_sub_section(page, "WARNINGS", obj.warnings) + if obj.example: + self.generate_sub_sub_section(page, "EXAMPLE", [obj.example]) + + page.unindent() + + def generate(self) -> None: + page = ManPage(self.out) + + page.title("meson-reference", 3) + + page.section("NAME") + page.par( + f"meson-reference v{self._extract_meson_version()}" + + " - a reference for meson functions and objects" + ) + + page.section("DESCRIPTION") + self.generate_description( + page, + """This manual is divided into two sections, *FUNCTIONS* and *OBJECTS*. *FUNCTIONS* contains a reference for all meson functions and methods. Methods are denoted by [[object_name]].[[method_name]](). *OBJECTS* contains additional information about each object.""", + ) + + page.section("FUNCTIONS") + for f in self.sorted_and_filtered(self.functions): + self.generate_function(page, f) + + for obj in self.sorted_and_filtered(self.objects): + for f in self.sorted_and_filtered(obj.methods): + self.generate_function(page, f, obj) + + page.section("OBJECTS") + for obj in self.sorted_and_filtered(self.objects): + self.generate_object(page, obj) + + page.section("SEE ALSO") + for i in range(len(self.links)): + link = self.links[i] + page.line(f"[{i + 1}] {link}") + page.br() + + page.write() diff --git a/docs/refman/main.py b/docs/refman/main.py index 5bc40f7..e2654c9 100644 --- a/docs/refman/main.py +++ b/docs/refman/main.py @@ -27,13 +27,14 @@ from .generatorjson import GeneratorJSON from .generatorprint import GeneratorPrint from .generatorpickle import GeneratorPickle from .generatormd import GeneratorMD +from .generatorman import GeneratorMan meson_root = Path(__file__).absolute().parents[2] def main() -> int: parser = argparse.ArgumentParser(description='Meson reference manual generator') parser.add_argument('-l', '--loader', type=str, default='yaml', choices=['yaml', 'pickle'], help='Information loader backend') - parser.add_argument('-g', '--generator', type=str, choices=['print', 'pickle', 'md', 'json'], required=True, help='Generator backend') + parser.add_argument('-g', '--generator', type=str, choices=['print', 'pickle', 'md', 'json', 'man'], required=True, help='Generator backend') parser.add_argument('-s', '--sitemap', type=Path, default=meson_root / 'docs' / 'sitemap.txt', help='Path to the input sitemap.txt') parser.add_argument('-o', '--out', type=Path, required=True, help='Output directory for generated files') parser.add_argument('-i', '--input', type=Path, default=meson_root / 'docs' / 'yaml', help='Input path for the selected loader') @@ -59,6 +60,7 @@ def main() -> int: 'pickle': lambda: GeneratorPickle(refMan, args.out), 'md': lambda: GeneratorMD(refMan, args.out, args.sitemap, args.link_defs, not args.no_modules), 'json': lambda: GeneratorJSON(refMan, args.out, not args.no_modules), + 'man': lambda: GeneratorMan(refMan, args.out, not args.no_modules), } generator = generators[args.generator]() |