From 21854f24a5f74b8df5e5824c73fdb7c558503715 Mon Sep 17 00:00:00 2001 From: Pauli Virtanen Date: Sat, 18 Nov 2023 20:04:06 +0200 Subject: [PATCH] doc: generate man-pages also for pipewire modules Use pandoc + some processing to convert Doxygen html output to man pages. Requires pandoc & python for building. Generates manpages: libpipewire-modules.7, libpipewire-module-*.7 --- doc/meson.build | 30 ++++++ doc/module-man-rst.py | 159 +++++++++++++++++++++++++++++++ man/libpipewire-modules.7.rst.in | 56 +++++++++++ man/meson.build | 9 ++ man/pipewire.1.rst.in | 1 + man/pipewire.conf.5.rst.in | 1 + meson.build | 9 ++ 7 files changed, 265 insertions(+) create mode 100644 doc/module-man-rst.py create mode 100644 man/libpipewire-modules.7.rst.in diff --git a/doc/meson.build b/doc/meson.build index 4975917f4..c3fe1eea3 100644 --- a/doc/meson.build +++ b/doc/meson.build @@ -166,3 +166,33 @@ html_target = custom_target('pipewire-docs', command: [ doxygen, doxyfile ], install: true, install_dir: docdir) + + +if generate_module_manpages + module_man_rst_py = meson.project_source_root() / 'doc' / 'module-man-rst.py' + module_man_defines = [] + foreach m : manpage_conf.keys() + if m != 'LIBPIPEWIRE_MODULES' + module_man_defines += ['-D', m, manpage_conf.get(m)] + endif + endforeach + + foreach m : module_sources + name = m.split('.c').get(0) + file = 'libpipewire-' + name + '.7' + + rst = custom_target(file + '.rst', + command : [python, module_man_rst_py, pandoc, name, '@INPUT@' ] + module_man_defines, + input : [ html_target ], + depend_files : [ module_man_rst_py ], + output : file + '.rst', + capture : true + ) + custom_target(file, + output : file, + input : rst, + command : [rst2man, '@INPUT@', '@OUTPUT@'], + install : true, + install_dir : get_option('mandir') / 'man7') + endforeach +endif diff --git a/doc/module-man-rst.py b/doc/module-man-rst.py new file mode 100644 index 000000000..0aa68c731 --- /dev/null +++ b/doc/module-man-rst.py @@ -0,0 +1,159 @@ +#!/usr/bin/python3 +""" +Convert Doxygen HTML documentation for a PipeWire module to RST. +""" +import argparse +import html, html.parser +import re +from pathlib import Path +from subprocess import check_output + +TEMPLATE = """ +{name} +{name_underline} + +{subtitle_underline} +{subtitle} +{subtitle_underline} + +:Manual section: 7 +:Manual group: PipeWire + +DESCRIPTION +----------- + +{content} + +AUTHORS +------- + +The PipeWire Developers <{PACKAGE_BUGREPORT}>; +PipeWire is available from {PACKAGE_URL} + +SEE ALSO +-------- + +``pipewire(1)``, +``pipewire.conf(5)``, +``libpipewire-modules(7)`` +""" + + +def main(): + p = argparse.ArgumentParser(description=__doc__.strip()) + p.add_argument( + "-D", + "--define", + nargs=2, + action="append", + dest="define", + default=[], + ) + p.add_argument("pandoc") + p.add_argument("module") + p.add_argument("htmldir", type=Path) + args = p.parse_args() + + page = args.module.lower().replace("-", "_") + src = args.htmldir / f"page_{page}.html" + + # Pick content block only + parser = DoxyParser() + with open(src, "r") as f: + parser.feed(f.read()) + data = "".join(parser.content) + + # Produce output + content = check_output( + [args.pandoc, "-f", "html", "-t", "rst"], input=data, encoding="utf-8" + ) + + if not content.strip(): + content = "Undocumented." + + name = f"libpipewire-{args.module}" + subtitle = "PipeWire module" + + env = dict( + content=content, + name=name, + name_underline="#" * len(name), + subtitle=subtitle, + subtitle_underline="-" * len(subtitle), + ) + + for k, v in args.define: + env[k] = v + + print(TEMPLATE.format(**env)) + + +def replace_pw_key(key): + key = key.lower().replace("_", ".") + if key in ("protocol", "access", "client.access") or key.startswith("sec."): + return f"pipewire.{key}" + return key + + +class DoxyParser(html.parser.HTMLParser): + """ + Capture div.textblock, and: + - Convert div.fragment to pre + - Convert a[@href="page_module_XXX.html"] to libpipewire-module-xxx(7) + """ + + def __init__(self): + super().__init__() + self.content = [] + self.stack = [] + + def feed(self, data): + try: + super().feed(data) + except EOFError: + pass + + def handle_starttag(self, tag, attrs): + attrs = dict(attrs) + + if self.stack: + if self.stack[-1] is None: + self.stack.append(None) + return + + if tag == "div" and attrs.get("class") == "fragment": + tag = "pre" + attrs = dict() + elif tag == "a" and attrs.get("href").startswith("page_module_"): + module = attrs["href"].replace("page_module_", "libpipewire-module-") + module = module.replace(".html", "").replace("_", "-") + self.content.append(f"{module}(7)") + self.stack.append(None) + return + + attrstr = " ".join(f'{k}="{html.escape(v)}"' for k, v in attrs.items()) + self.content.append(f"<{tag} {attrstr}>") + self.stack.append(tag) + elif tag == "div" and attrs.get("class") == "textblock": + self.stack.append(tag) + + def handle_endtag(self, tag): + if len(self.stack) == 1: + raise EOFError() + elif self.stack: + otag = self.stack.pop() + if otag is not None: + self.content.append(f"") + + def handle_data(self, data): + if self.stack and self.stack[-1] is not None: + if self.stack[-1] == "a": + m = re.match(r"^(PW|SPA)_KEY_([A-Z_]+)$", data) + if m: + data = replace_pw_key(m.group(2)) + + self.content.append(html.escape(data)) + + +if __name__ == "__main__": + main() diff --git a/man/libpipewire-modules.7.rst.in b/man/libpipewire-modules.7.rst.in new file mode 100644 index 000000000..a8251d992 --- /dev/null +++ b/man/libpipewire-modules.7.rst.in @@ -0,0 +1,56 @@ +libpipewire-modules +################### + +---------------- +PipeWire modules +---------------- + +:Manual section: 7 +:Manual group: PipeWire + +DESCRIPTION +=========== + +A PipeWire module is effectively a PipeWire client running inside +``pipewire(1)`` which can host multiple modules. Usually modules are +loaded when they are listed in the configuration files. For example +the default configuration file loads several modules: + +:: + + context.modules = [ + ... + # The native communication protocol. + { name = libpipewire-module-protocol-native } + + # The profile module. Allows application to access profiler + # and performance data. It provides an interface that is used + # by pw-top and pw-profiler. + { name = libpipewire-module-profiler } + + # Allows applications to create metadata objects. It creates + # a factory for Metadata objects. + { name = libpipewire-module-metadata } + + # Creates a factory for making devices that run in the + # context of the PipeWire server. + { name = libpipewire-module-spa-device-factory } + ... + ] + +KNOWN MODULES +============= + +- @LIBPIPEWIRE_MODULES@ + +AUTHORS +======= + +The PipeWire Developers <@PACKAGE_BUGREPORT@>; PipeWire is available from @PACKAGE_URL@ + +SEE ALSO +======== + +``pipewire(1)``, +``pipewire.conf(5)``, + diff --git a/man/meson.build b/man/meson.build index a1fddb05b..79df3dca0 100644 --- a/man/meson.build +++ b/man/meson.build @@ -6,6 +6,14 @@ manpage_conf.set('PACKAGE_BUGREPORT', 'https://gitlab.freedesktop.org/pipewire/p manpage_conf.set('PIPEWIRE_CONFIG_DIR', pipewire_configdir) manpage_conf.set('PIPEWIRE_CONFDATADIR', pipewire_confdatadir) +module_manpage_list = [] +foreach m : module_sources + name = m.split('.c').get(0) + module_manpage_list += ['``libpipewire-' + name + '(7)``'] +endforeach + +manpage_conf.set('LIBPIPEWIRE_MODULES', '\n- '.join(module_manpage_list)) + manpages = [ 'pipewire.1.rst.in', 'pipewire-pulse.1.rst.in', @@ -22,6 +30,7 @@ manpages = [ 'pw-mon.1.rst.in', 'pw-profiler.1.rst.in', 'pw-top.1.rst.in', + 'libpipewire-modules.7.rst.in', ] if get_option('pipewire-jack').allowed() diff --git a/man/pipewire.1.rst.in b/man/pipewire.1.rst.in index 8b6383969..8f5aebd04 100644 --- a/man/pipewire.1.rst.in +++ b/man/pipewire.1.rst.in @@ -52,3 +52,4 @@ SEE ALSO ``pw-mon(1)``, ``pw-cat(1)``, ``pw-cli(1)``, +``libpipewire-modules(7)``, diff --git a/man/pipewire.conf.5.rst.in b/man/pipewire.conf.5.rst.in index fb8144bd4..d5d39c258 100644 --- a/man/pipewire.conf.5.rst.in +++ b/man/pipewire.conf.5.rst.in @@ -110,3 +110,4 @@ SEE ALSO ``pipewire(1)``, ``pw-mon(1)``, +``libpipewire-modules(7)``, diff --git a/meson.build b/meson.build index e21fca8b0..8c6552402 100644 --- a/meson.build +++ b/meson.build @@ -501,6 +501,15 @@ subdir('man') doxygen = find_program('doxygen', required : get_option('docs')) if doxygen.found() + generate_module_manpages = get_option('docs').enabled() and get_option('man').enabled() + if generate_manpages + pymod = import('python') + python = pymod.find_installation('python3', required: generate_module_manpages) + pandoc = find_program('pandoc', required: generate_module_manpages) + generate_module_manpages = python.found() and pandoc.found() + endif + summary({'Module manpage generation': generate_module_manpages}, bool_yn: true) + subdir('doc') endif