from pathlib import Path from json import loads import os 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.core.symbols import * 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. ''' for key, value in self._data.items(): path = os.path.relpath(value, self.app.config.get_invoke_dir()).split('#')[0] if path == page.link.ref: if key.startswith('@'): res = self.create_symbol( ClassSymbol, display_name=key[1:], filename=path, unique_name=key) res.link = Link(value, res.display_name, res.unique_name) elif '.' in key: res = self.create_symbol( MethodSymbol, parameters=[], display_name=key.split('.')[-1], parent_name=f'@{key.split(".")[-2]}', filename=path, unique_name=key) res.link = Link(value, key, res.unique_name) else: res = self.create_symbol( FunctionSymbol, parameters=[], display_name=key, filename=path, unique_name=key) res.link = Link(value, res.display_name, res.unique_name) page.symbols.append(res) 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(1) 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:] elif in_code_block: if m.group(3) != '(': text = text + '()' else: text = text + '()' if not in_code_block: text = f'{text}' link = f'{text}' page.formatted_contents = page.formatted_contents.replace(i, link, 1) 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 formatter 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') def create_symbol(self, *args, **kwargs): kwargs['language'] = 'meson' return super(RefmanLinksExtension, self).create_symbol(*args, **kwargs) @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]