blob: bb8a1b1b801fad12905ba1967b85105dbb9f9ebb [file] [log] [blame]
# 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
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 =['git', 'rev-parse', 'HEAD'],
# 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']:
'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 '' % (
_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)
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)
# 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 =
text =
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):
_XrefPattern(r'@{([^}|]*)(\|[^}]+)?}'), 'xref', 175)