# Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file # for details. All rights reserved. Use of this source code is governed by a # BSD-style license that can be found in the LICENSE file. # """Support for @{ref|text} reference links in Markdown. This Markdown extension converts @{ref|text} into a link with the given [text] pointing to a particular source code location. [ref] can be one of the following: * package:-scheme URI - it will be resolved using .packages file in the root directory * file path * C++ symbol - will be resolved through xref.json file (see README.md) Usage: markdown.markdown(extensions=[XrefExtension()]) """ import json import logging import os import subprocess from markdown.extensions import Extension from markdown.inlinepatterns import InlineProcessor from markdown.util import etree from typing import Optional from urllib.parse import urlparse _current_commit_hash = subprocess.run(['git', 'rev-parse', 'HEAD'], capture_output=True, encoding='utf8').stdout # Load .packages file into a dictionary. with open('.packages') as packages_file: _packages = dict([ package_mapping.split(':', 1) for package_mapping in packages_file if not package_mapping.startswith('#') ]) # Load xref.json and verify that it was generated for the current commit to # avoid discrepancies in the generated links. with open('xref.json') as json_file: _xrefs = json.load(json_file) if _current_commit_hash != _xrefs['commit']: logging.error( 'xref.json is generated for commit %s while current commit is %s', _xrefs['commit'], _current_commit_hash) def _make_github_uri(file: str, lineno: str = None) -> str: """Generates source link pointing to GitHub""" fragment = '#L%s' % (lineno) if lineno is not None else '' return 'https://github.com/dart-lang/sdk/blob/%s/%s%s' % ( _current_commit_hash, file, fragment) def _file_ref_to_github_uri(file_ref: str) -> str: """Generates source link pointing to GitHub from an xref.json reference.""" (file_idx, line_idx) = file_ref.split(':', 1) return _make_github_uri(_xrefs['files'][int(file_idx)], line_idx) def _resolve_ref_via_xref(ref: str) -> Optional[str]: """Resolve the target of the given reference via xref.json""" if ref in _xrefs['functions']: return _xrefs['functions'][ref] if ref in _xrefs['classes']: return _xrefs['classes'][ref][0] if '::' in ref: (class_name, function_name) = ref.rsplit('::', 1) if class_name in _xrefs['classes'] and len( _xrefs['classes'][class_name]) == 2: return _xrefs['classes'][class_name][1][function_name] logging.error('Failed to resolve xref %s' % ref) return None def _resolve_ref(ref: str) -> Optional[str]: if ref.startswith('package:'): # Resolve as package uri via .packages. uri = urlparse(ref) (package_name, *path_to_file) = uri.path.split('/', 1) package_path = _packages[package_name] if len(path_to_file) == 0: return _make_github_uri(package_path) else: return _make_github_uri(os.path.join(package_path, path_to_file[0])) elif os.path.exists(ref): # Resolve as a file link. return _make_github_uri(_current_commit_hash, ref) else: # Resolve as a C++ symbol via xref.json file_ref = _resolve_ref_via_xref(ref) if file_ref is not None: return _file_ref_to_github_uri(file_ref) class _XrefPattern(InlineProcessor): """InlineProcessor responsible for handling @{ref|text} syntax.""" def handleMatch(self, m, data): ref = m.group(1) text = m.group(2) uri = _resolve_ref(ref) el = etree.Element('a') el.attrib['href'] = uri el.attrib['target'] = 'blank' el.text = text[1:] if text is not None else ref return el, m.start(0), m.end(0) class XrefExtension(Extension): """Markdown extension responsible for expanding @{ref|text} into links.""" def extendMarkdown(self, md): md.inlinePatterns.register( _XrefPattern(r'@{([^}|]*)(\|[^}]+)?}'), 'xref', 175)