diff --git a/docs/extensions/refman_links.py b/docs/extensions/refman_links.py
new file mode 100644
index 000000000..857d2cbcb
--- /dev/null
+++ b/docs/extensions/refman_links.py
@@ -0,0 +1,108 @@
+from pathlib import Path
+from json import loads
+import re
+
+from hotdoc.core.exceptions import HotdocSourceException
+from hotdoc.core.extension import Extension
+from hotdoc.core.tree import Page
+from hotdoc.core.project import Project
+from hotdoc.run_hotdoc import Application
+from hotdoc.core.formatter import Formatter
+from hotdoc.utils.loggable import Logger, warn, info
+
+import typing as T
+
+if T.TYPE_CHECKING:
+ import argparse
+
+Logger.register_warning_code('unknown-refman-link', HotdocSourceException, 'refman-links')
+
+class RefmanLinksExtension(Extension):
+ extension_name = 'refman-links'
+ argument_prefix = 'refman'
+
+ def __init__(self, app: Application, project: Project):
+ self.project: Project
+ super().__init__(app, project)
+ self._data_file: T.Optional[Path] = None
+ self._data: T.Dict[str, str] = {}
+
+ @staticmethod
+ def add_arguments(parser: 'argparse.ArgumentParser'):
+ group = parser.add_argument_group(
+ 'Refman links',
+ 'Custom Meson extension',
+ )
+
+ # Add Arguments with `group.add_argument(...)`
+ group.add_argument(
+ f'--refman-data-file',
+ help="JSON file with the mappings to replace",
+ default=None,
+ )
+
+ def parse_config(self, config: T.Dict[str, T.Any]) -> None:
+ super().parse_config(config)
+ self._data_file = config.get('refman_data_file')
+
+ def _formatting_page_cb(self, formatter: Formatter, page: Page) -> None:
+ ''' Replace Meson refman tags
+
+ Links of the form [[function]] are automatically replaced
+ with valid links to the correct URL. To reference objects / types use the
+ [[@object]] syntax.
+ '''
+ link_regex = re.compile(r'\[\[#?@?([ \n\t]*[a-zA-Z0-9_]+[ \n\t]*\.)*[ \n\t]*[a-zA-Z0-9_]+[ \n\t]*\]\]', re.MULTILINE)
+ for m in link_regex.finditer(page.formatted_contents):
+ i = m.group()
+ obj_id: str = i[2:-2]
+ obj_id = re.sub(r'[ \n\t]', '', obj_id) # Remove whitespaces
+
+ # Marked as inside a code block?
+ in_code_block = False
+ if obj_id.startswith('#'):
+ in_code_block = True
+ obj_id = obj_id[1:]
+
+ if obj_id not in self._data:
+ warn('unknown-refman-link', f'{Path(page.name).name}: Unknown Meson refman link: "{obj_id}"')
+ continue
+
+ # Just replaces [[!file.id]] paths with the page file (no fancy HTML)
+ if obj_id.startswith('!'):
+ page.formatted_contents = page.formatted_contents.replace(i, self._data[obj_id])
+ continue
+
+ # Fancy links for functions and methods
+ text = obj_id
+ if text.startswith('@'):
+ text = text[1:]
+ else:
+ text = text + '()'
+ if not in_code_block:
+ text = f'{text}
'
+ link = f'{text}'
+ page.formatted_contents = page.formatted_contents.replace(i, link)
+
+ def setup(self) -> None:
+ super().setup()
+
+ if not self._data_file:
+ info('Meson refman extension DISABLED')
+ return
+
+ raw = Path(self._data_file).read_text(encoding='utf-8')
+ self._data = loads(raw)
+
+ # Register formater
+ for ext in self.project.extensions.values():
+ ext = T.cast(Extension, ext)
+ ext.formatter.formatting_page_signal.connect(self._formatting_page_cb)
+ info('Meson refman extension LOADED')
+
+ @staticmethod
+ def get_dependencies() -> T.List[T.Type[Extension]]:
+ return [] # In case this extension has dependencies on other extensions
+
+def get_extension_classes() -> T.List[T.Type[Extension]]:
+ return [RefmanLinksExtension]
diff --git a/docs/meson.build b/docs/meson.build
index 9bd80ba7c..fcb4f7f5f 100644
--- a/docs/meson.build
+++ b/docs/meson.build
@@ -18,13 +18,14 @@ docs_gen = custom_target(
refman_gen = custom_target(
'gen_refman',
input: files('sitemap.txt'),
- output: 'configured_sitemap.txt',
+ output: ['configured_sitemap.txt', 'refman_links.json'],
depfile: 'reman_dep.d',
command: [
find_program('./genrefman.py'),
'-g', 'md',
'-s', '@INPUT@',
- '-o', '@OUTPUT@',
+ '-o', '@OUTPUT0@',
+ '--link-defs', '@OUTPUT1@',
'--depfile', '@DEPFILE@',
'--force-color',
],
@@ -33,7 +34,7 @@ refman_gen = custom_target(
hotdoc = import('hotdoc')
documentation = hotdoc.generate_doc(meson.project_name(),
project_version: meson.project_version(),
- sitemap: refman_gen,
+ sitemap: refman_gen[0],
build_by_default: true,
depends: docs_gen,
index: 'markdown/index.md',
@@ -46,6 +47,8 @@ documentation = hotdoc.generate_doc(meson.project_name(),
edit_on_github_repository: 'https://github.com/mesonbuild/meson',
syntax_highlighting_activate: true,
keep_markup_in_code_blocks: true,
+ extra_extension: meson.current_source_dir() / 'extensions' / 'refman_links.py',
+ refman_data_file: refman_gen[1],
)
run_target('upload',
diff --git a/docs/refman/generatorbase.py b/docs/refman/generatorbase.py
index 517c59262..e4041747b 100644
--- a/docs/refman/generatorbase.py
+++ b/docs/refman/generatorbase.py
@@ -15,7 +15,7 @@
from abc import ABCMeta, abstractmethod
import typing as T
-from .model import ReferenceManual, Function, Object, ObjectType, NamedObject
+from .model import ReferenceManual, Function, Method, Object, ObjectType, NamedObject
_N = T.TypeVar('_N', bound=NamedObject)
@@ -37,7 +37,11 @@ class GeneratorBase(metaclass=ABCMeta):
@staticmethod
def sorted_and_filtered(raw: T.List[_N]) -> T.List[_N]:
- return sorted([x for x in raw if not x.hidden], key=lambda x: x.name)
+ def key_fn(fn: Function) -> str:
+ if isinstance(fn, Method):
+ return f'1_{fn.obj.name}.{fn.name}'
+ return f'0_{fn.name}'
+ return sorted([x for x in raw if not x.hidden], key=key_fn)
@property
def functions(self) -> T.List[Function]:
diff --git a/docs/refman/generatormd.py b/docs/refman/generatormd.py
index 831b6816f..6aa0d7873 100644
--- a/docs/refman/generatormd.py
+++ b/docs/refman/generatormd.py
@@ -14,6 +14,7 @@
from .generatorbase import GeneratorBase
import re
+import json
from .model import (
ReferenceManual,
@@ -72,10 +73,11 @@ def code_block(code: str) -> str:
return f'
{code}
'
class GeneratorMD(GeneratorBase):
- def __init__(self, manual: ReferenceManual, sitemap_out: Path, sitemap_in: Path) -> None:
+ def __init__(self, manual: ReferenceManual, sitemap_out: Path, sitemap_in: Path, link_def_out: Path) -> None:
super().__init__(manual)
self.sitemap_out = sitemap_out.resolve()
self.sitemap_in = sitemap_in.resolve()
+ self.link_def_out = link_def_out.resolve()
self.out_dir = self.sitemap_out.parent
self.generated_files: T.Dict[str, str] = {}
@@ -88,29 +90,6 @@ class GeneratorMD(GeneratorBase):
parts = [re.sub(r'[0-9]+_', '', x) for x in parts]
return f'{"_".join(parts)}.{extension}'
- def _object_from_ref(self, ref_str: str) -> T.Union[Function, Object]:
- ids = ref_str.split('.')
- ids = [x.strip() for x in ids]
- assert len(ids) in [1, 2], f'Invalid object id "{ref_str}"'
- assert not (ids[0].startswith('@') and len(ids) == 2), f'Invalid object id "{ref_str}"'
- if ids[0].startswith('@'):
- for obj in self.objects:
- if obj.name == ids[0][1:]:
- return obj
- if len(ids) == 2:
- for obj in self.objects:
- if obj.name != ids[0]:
- continue
- for m in obj.methods:
- if m.name == ids[1]:
- return m
- raise RuntimeError(f'Unknown method {ids[1]} in object {ids[0]}')
- raise RuntimeError(f'Unknown object {ids[0]}')
- for func in self.functions:
- if func.name == ids[0]:
- return func
- raise RuntimeError(f'Unknown function or object {ids[0]}')
-
def _gen_object_file_id(self, obj: Object) -> str:
'''
Deterministically generate a unique file ID for the Object.
@@ -122,52 +101,25 @@ class GeneratorMD(GeneratorBase):
return f'{base}.{obj.name}'
return f'root.{_OBJ_ID_MAP[obj.obj_type]}.{obj.name}'
- def _link_to_object(self, obj: T.Union[Function, Object], text: T.Optional[str] = None) -> str:
+ def _link_to_object(self, obj: T.Union[Function, Object], in_code_block: bool = False) -> str:
'''
- Generate a link to the function/method/object documentation.
-
- The generated link is an HTML link (text) instead of
- a Markdown link, so that the generated links can be used in custom
- (or rather manual) code blocks.
+ Generate a palaceholder tag for the the function/method/object documentation.
+ This tag is then replaced in the custom hotdoc plugin.
'''
+ prefix = '#' if in_code_block else ''
if isinstance(obj, Object):
- text = text or f'{obj.name}
'
- link = self._gen_filename(self._gen_object_file_id(obj), extension="html")
+ return f'[[{prefix}@{obj.name}]]'
elif isinstance(obj, Method):
- text = text or f'`{obj.obj.name}.{obj.name}()`
'
- file = self._gen_filename(self._gen_object_file_id(obj.obj), extension="html")
- link = f'{file}#{obj.obj.name}{obj.name}'
+ return f'[[{prefix}{obj.obj.name}.{obj.name}]]'
elif isinstance(obj, Function):
- text = text or f'`{obj.name}()`
'
- link = f'{self._gen_filename("root.functions", extension="html")}#{obj.name}'
+ return f'[[{prefix}{obj.name}]]'
else:
raise RuntimeError(f'Invalid argument {obj}')
- return f'{text}'
def _write_file(self, data: str, file_id: str) -> None:#
- ''' Write the data to disk.
+ ''' Write the data to disk ans store the id for the generated data '''
- Additionally, links of the form [[function]] are automatically replaced
- with valid links to the correct URL. To reference objects / types use the
- [[@object]] syntax.
-
- Placeholders with the syntax [[!file_id]] will be replaced with the
- corresponding generated markdown file.
- '''
self.generated_files[file_id] = self._gen_filename(file_id)
-
- # Replace [[func_name]] and [[obj.method_name]] with links
- link_regex = re.compile(r'\[\[[^\]]+\]\]')
- matches = link_regex.findall(data)
- for i in matches:
- obj_id: str = i[2:-2]
- if obj_id.startswith('!'):
- link_file_id = obj_id[1:]
- data = data.replace(i, self._gen_filename(link_file_id))
- else:
- obj = self._object_from_ref(obj_id)
- data = data.replace(i, self._link_to_object(obj))
-
out_file = self.out_dir / self.generated_files[file_id]
out_file.write_text(data, encoding='ascii')
mlog.log('Generated', mlog.bold(out_file.name))
@@ -193,11 +145,11 @@ class GeneratorMD(GeneratorBase):
# Actual generator functions
def _gen_func_or_method(self, func: Function) -> FunctionDictType:
- def render_type(typ: Type) -> str:
+ def render_type(typ: Type, in_code_block: bool = False) -> str:
def data_type_to_str(dt: DataTypeInfo) -> str:
- base = self._link_to_object(dt.data_type, f'{dt.data_type.name}')
+ base = self._link_to_object(dt.data_type, in_code_block)
if dt.holds:
- return f'{base}[{render_type(dt.holds)}]'
+ return f'{base}[{render_type(dt.holds, in_code_block)}]'
return base
assert typ.resolved
return ' | '.join([data_type_to_str(x) for x in typ.resolved])
@@ -208,11 +160,11 @@ class GeneratorMD(GeneratorBase):
def render_signature() -> str:
# Skip a lot of computations if the function does not take any arguments
if not any([func.posargs, func.optargs, func.kwargs, func.varargs]):
- return f'{render_type(func.returns)} {func.name}()'
+ return f'{render_type(func.returns, True)} {func.name}()'
signature = dedent(f'''\
# {self.brief(func)}
- {render_type(func.returns)} {func.name}(
+ {render_type(func.returns, True)} {func.name}(
''')
# Calculate maximum lengths of the type and name
@@ -229,7 +181,7 @@ class GeneratorMD(GeneratorBase):
# Generate some common strings
def prepare(arg: ArgBase) -> T.Tuple[str, str, str, str]:
- type_str = render_type(arg.type)
+ type_str = render_type(arg.type, True)
type_len = len_stripped(type_str)
type_space = ' ' * (max_type_len - type_len)
name_space = ' ' * (max_name_len - len(arg.name))
@@ -362,6 +314,7 @@ class GeneratorMD(GeneratorBase):
return ret
data = {
+ 'root': self._gen_filename('root'),
'elementary': gen_obj_links(self.elementary),
'returned': gen_obj_links(self.returned),
'builtins': gen_obj_links(self.builtins),
@@ -369,11 +322,13 @@ class GeneratorMD(GeneratorBase):
'functions': [{'indent': '', 'link': self._link_to_object(x), 'brief': self.brief(x)} for x in self.functions],
}
+ dummy = {'root': self._gen_filename('root')}
+
self._write_template(data, 'root')
- self._write_template({'name': 'Elementary types'}, f'root.{_OBJ_ID_MAP[ObjectType.ELEMENTARY]}', 'dummy')
- self._write_template({'name': 'Builtin objects'}, f'root.{_OBJ_ID_MAP[ObjectType.BUILTIN]}', 'dummy')
- self._write_template({'name': 'Returned objects'}, f'root.{_OBJ_ID_MAP[ObjectType.RETURNED]}', 'dummy')
- self._write_template({'name': 'Modules'}, f'root.{_OBJ_ID_MAP[ObjectType.MODULE]}', 'dummy')
+ self._write_template({**dummy, 'name': 'Elementary types'}, f'root.{_OBJ_ID_MAP[ObjectType.ELEMENTARY]}', 'dummy')
+ self._write_template({**dummy, 'name': 'Builtin objects'}, f'root.{_OBJ_ID_MAP[ObjectType.BUILTIN]}', 'dummy')
+ self._write_template({**dummy, 'name': 'Returned objects'}, f'root.{_OBJ_ID_MAP[ObjectType.RETURNED]}', 'dummy')
+ self._write_template({**dummy, 'name': 'Modules'}, f'root.{_OBJ_ID_MAP[ObjectType.MODULE]}', 'dummy')
def generate(self) -> None:
@@ -384,6 +339,7 @@ class GeneratorMD(GeneratorBase):
self._write_object(obj)
self._root_refman_docs()
self._configure_sitemap()
+ self._generate_link_def()
def _configure_sitemap(self) -> None:
'''
@@ -403,3 +359,25 @@ class GeneratorMD(GeneratorBase):
indent = base_indent + '\t' * k.count('.')
out += f'{indent}{self.generated_files[k]}\n'
self.sitemap_out.write_text(out, encoding='utf-8')
+
+ def _generate_link_def(self) -> None:
+ '''
+ Generate the link definition file for the refman_links hotdoc
+ plugin. The plugin is then responsible for replacing the [[tag]]
+ tags with custom HTML elements.
+ '''
+ data: T.Dict[str, str] = {}
+
+ # Objects and methods
+ for obj in self.objects:
+ obj_file = self._gen_filename(self._gen_object_file_id(obj), extension='html')
+ data[f'@{obj.name}'] = obj_file
+ for m in obj.methods:
+ data[f'{obj.name}.{m.name}'] = f'{obj_file}#{obj.name}{m.name}'
+
+ # Functions
+ funcs_file = self._gen_filename('root.functions', extension='html')
+ for fn in self.functions:
+ data[fn.name] = f'{funcs_file}#{fn.name}'
+
+ self.link_def_out.write_text(json.dumps(data, indent=2), encoding='utf-8')
diff --git a/docs/refman/main.py b/docs/refman/main.py
index f4b307669..cb040cebe 100644
--- a/docs/refman/main.py
+++ b/docs/refman/main.py
@@ -34,6 +34,7 @@ def main() -> int:
parser.add_argument('-g', '--generator', type=str, choices=['print', 'pickle', 'md'], 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('--link-defs', type=Path, help='Output file for the MD generator link definition file')
parser.add_argument('--depfile', type=Path, default=None, help='Set to generate a depfile')
parser.add_argument('--force-color', action='store_true', help='Force enable colors')
args = parser.parse_args()
@@ -51,7 +52,7 @@ def main() -> int:
generators: T.Dict[str, T.Callable[[], GeneratorBase]] = {
'print': lambda: GeneratorPrint(refMan),
'pickle': lambda: GeneratorPickle(refMan, args.out),
- 'md': lambda: GeneratorMD(refMan, args.out, args.sitemap),
+ 'md': lambda: GeneratorMD(refMan, args.out, args.sitemap, args.link_defs),
}
generator = generators[args.generator]()
diff --git a/docs/refman/templates/dummy.mustache b/docs/refman/templates/dummy.mustache
index b6dc35202..ddb090e44 100644
--- a/docs/refman/templates/dummy.mustache
+++ b/docs/refman/templates/dummy.mustache
@@ -4,5 +4,5 @@ render-subpages: true
...
# {{name}}
-See the [root manual document]([[!root]]) for
+See the [root manual document]({{root}}) for
a general overview.
diff --git a/docs/refman/templates/root.functions.mustache b/docs/refman/templates/root.functions.mustache
index aa0230db2..33fba5952 100644
--- a/docs/refman/templates/root.functions.mustache
+++ b/docs/refman/templates/root.functions.mustache
@@ -6,7 +6,7 @@ render-subpages: false
# Functions
This document lists all functions available in `meson.build` files.
-See the [root manual document]([[!root]]) for
+See the [root manual document]({{root}}) for
an overview of all features.
{{#functions}}