summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--_exts/typescriptdomain.py545
1 files changed, 545 insertions, 0 deletions
diff --git a/_exts/typescriptdomain.py b/_exts/typescriptdomain.py
new file mode 100644
index 00000000..73a5adaa
--- /dev/null
+++ b/_exts/typescriptdomain.py
@@ -0,0 +1,545 @@
+"""
+TypeScript domain.
+
+:copyright: Copyright 2019 by Taler Systems SA
+:license: LGPLv3+
+:author: Florian Dold
+"""
+
+import re
+
+from docutils import nodes
+from typing import List, Optional, Iterable, Dict, Tuple
+from typing import cast
+
+from pygments.lexers import get_lexer_by_name
+from pygments.filter import Filter
+from pygments.token import Literal, Text, Operator, Keyword, Name, Number
+from pygments.token import Comment, Token, _TokenType
+from pygments.token import *
+from pygments.lexer import RegexLexer, bygroups, include
+from pygments.formatters import HtmlFormatter
+
+from docutils import nodes
+from docutils.nodes import Element, Node
+
+from sphinx.roles import XRefRole
+from sphinx.domains import Domain, ObjType, Index
+from sphinx.directives import directives
+from sphinx.util.docutils import SphinxDirective
+from sphinx.util.nodes import make_refnode
+from sphinx.util import logging
+from sphinx.highlighting import PygmentsBridge
+from sphinx.builders.html import StandaloneHTMLBuilder
+from sphinx.pygments_styles import SphinxStyle
+
+logger = logging.getLogger(__name__)
+
+
+class TypeScriptDefinition(SphinxDirective):
+ """
+ Directive for a code block with special highlighting or line numbering
+ settings.
+ """
+
+ has_content = True
+ required_arguments = 1
+ optional_arguments = 0
+ final_argument_whitespace = False
+ option_spec = {
+ 'force': directives.flag,
+ 'linenos': directives.flag,
+ 'dedent': int,
+ 'lineno-start': int,
+ 'emphasize-lines': directives.unchanged_required,
+ 'caption': directives.unchanged_required,
+ 'class': directives.class_option,
+ }
+
+ def run(self) -> List[Node]:
+ document = self.state.document
+ code = '\n'.join(self.content)
+ location = self.state_machine.get_source_and_line(self.lineno)
+
+ linespec = self.options.get('emphasize-lines')
+ if linespec:
+ try:
+ nlines = len(self.content)
+ hl_lines = parselinenos(linespec, nlines)
+ if any(i >= nlines for i in hl_lines):
+ logger.warning(
+ __('line number spec is out of range(1-%d): %r') %
+ (nlines, self.options['emphasize-lines']),
+ location=location
+ )
+
+ hl_lines = [x + 1 for x in hl_lines if x < nlines]
+ except ValueError as err:
+ return [document.reporter.warning(err, line=self.lineno)]
+ else:
+ hl_lines = None
+
+ if 'dedent' in self.options:
+ location = self.state_machine.get_source_and_line(self.lineno)
+ lines = code.split('\n')
+ lines = dedent_lines(
+ lines, self.options['dedent'], location=location
+ )
+ code = '\n'.join(lines)
+
+ literal = nodes.literal_block(code, code) # type: Element
+ if 'linenos' in self.options or 'lineno-start' in self.options:
+ literal['linenos'] = True
+ literal['classes'] += self.options.get('class', [])
+ literal['force'] = 'force' in self.options
+ literal['language'] = "tsref"
+ extra_args = literal['highlight_args'] = {}
+ if hl_lines is not None:
+ extra_args['hl_lines'] = hl_lines
+ if 'lineno-start' in self.options:
+ extra_args['linenostart'] = self.options['lineno-start']
+ self.set_source_info(literal)
+
+ caption = self.options.get('caption')
+ if caption:
+ try:
+ literal = container_wrapper(self, literal, caption)
+ except ValueError as exc:
+ return [document.reporter.warning(exc, line=self.lineno)]
+
+ tsid = "tsref-type-" + self.arguments[0]
+ literal['ids'].append(tsid)
+
+ tsname = self.arguments[0]
+ ts = self.env.get_domain('ts')
+ ts.add_object('type', tsname, self.env.docname, tsid)
+
+ return [literal]
+
+
+class TypeScriptDomain(Domain):
+ """TypeScript domain."""
+
+ name = 'ts'
+ label = 'TypeScript'
+
+ directives = {
+ 'def': TypeScriptDefinition,
+ }
+
+ roles = {
+ 'type':
+ XRefRole(
+ lowercase=False, warn_dangling=True, innernodeclass=nodes.inline
+ ),
+ }
+
+ dangling_warnings = {
+ 'type': 'undefined TypeScript type: %(target)s',
+ }
+
+ def resolve_xref(
+ self, env, fromdocname, builder, typ, target, node, contnode
+ ):
+ try:
+ info = self.objects[(str(typ), str(target))]
+ except KeyError:
+ logger.warn("type {}/{} not found".format(typ, target))
+ return None
+ else:
+ anchor = "tsref-type-{}".format(str(target))
+ title = typ.upper() + ' ' + target
+ return make_refnode(
+ builder, fromdocname, info[0], anchor, contnode, title
+ )
+
+ def resolve_any_xref(
+ self, env, fromdocname, builder, target, node, contnode
+ ):
+ """Resolve the pending_xref *node* with the given *target*.
+
+ The reference comes from an "any" or similar role, which means that Sphinx
+ don't know the type.
+
+ For now sphinxcontrib-httpdomain doesn't resolve any xref nodes.
+
+ :return:
+ list of tuples ``('domain:role', newnode)``, where ``'domain:role'``
+ is the name of a role that could have created the same reference,
+ """
+ ret = []
+ try:
+ info = self.objects[("type", str(target))]
+ except KeyError:
+ pass
+ else:
+ anchor = "tsref-type-{}".format(str(target))
+ title = "TYPE" + ' ' + target
+ node = make_refnode(
+ builder, fromdocname, info[0], anchor, contnode, title
+ )
+ ret.append(("ts:type", node))
+ return ret
+
+ @property
+ def objects(self) -> Dict[Tuple[str, str], Tuple[str, str]]:
+ return self.data.setdefault(
+ 'objects', {}
+ ) # (objtype, name) -> docname, labelid
+
+ def add_object(
+ self, objtype: str, name: str, docname: str, labelid: str
+ ) -> None:
+ self.objects[objtype, name] = (docname, labelid)
+
+
+class BetterTypeScriptLexer(RegexLexer):
+ """
+ For `TypeScript <https://www.typescriptlang.org/>`_ source code.
+ """
+
+ name = 'TypeScript'
+ aliases = ['ts']
+ filenames = ['*.ts']
+ mimetypes = ['text/x-typescript']
+
+ flags = re.DOTALL
+ tokens = {
+ 'commentsandwhitespace': [(r'\s+', Text), (r'<!--', Comment),
+ (r'//.*?\n', Comment.Single),
+ (r'/\*.*?\*/', Comment.Multiline)],
+ 'slashstartsregex': [
+ include('commentsandwhitespace'),
+ (
+ r'/(\\.|[^[/\\\n]|\[(\\.|[^\]\\\n])*])+/'
+ r'([gim]+\b|\B)', String.Regex, '#pop'
+ ), (r'(?=/)', Text, ('#pop', 'badregex')), (r'', Text, '#pop')
+ ],
+ 'badregex': [(r'\n', Text, '#pop')],
+ 'typeexp': [
+ (r'[a-zA-Z0-9_?.$]+', Keyword.Type),
+ (r'\s+', Text),
+ (r'[|]', Text),
+ (r'\n', Text, "#pop"),
+ (r';', Text, "#pop"),
+ (r'', Text, "#pop"),
+ ],
+ 'root': [
+ (r'^(?=\s|/|<!--)', Text, 'slashstartsregex'),
+ include('commentsandwhitespace'),
+ (
+ r'\+\+|--|~|&&|\?|:|\|\||\\(?=\n)|'
+ r'(<<|>>>?|==?|!=?|[-<>+*%&\|\^/])=?', Operator,
+ 'slashstartsregex'
+ ),
+ (r'[{(\[;,]', Punctuation, 'slashstartsregex'),
+ (r'[})\].]', Punctuation),
+ (
+ r'(for|in|while|do|break|return|continue|switch|case|default|if|else|'
+ r'throw|try|catch|finally|new|delete|typeof|instanceof|void|'
+ r'this)\b', Keyword, 'slashstartsregex'
+ ),
+ (
+ r'(var|let|const|with|function)\b', Keyword.Declaration,
+ 'slashstartsregex'
+ ),
+ (
+ r'(abstract|boolean|byte|char|class|const|debugger|double|enum|export|'
+ r'extends|final|float|goto|implements|import|int|interface|long|native|'
+ r'package|private|protected|public|short|static|super|synchronized|throws|'
+ r'transient|volatile)\b', Keyword.Reserved
+ ),
+ (r'(true|false|null|NaN|Infinity|undefined)\b', Keyword.Constant),
+ (
+ r'(Array|Boolean|Date|Error|Function|Math|netscape|'
+ r'Number|Object|Packages|RegExp|String|sun|decodeURI|'
+ r'decodeURIComponent|encodeURI|encodeURIComponent|'
+ r'Error|eval|isFinite|isNaN|parseFloat|parseInt|document|this|'
+ r'window)\b', Name.Builtin
+ ),
+ # Match stuff like: module name {...}
+ (
+ r'\b(module)(\s*)(\s*[a-zA-Z0-9_?.$][\w?.$]*)(\s*)',
+ bygroups(Keyword.Reserved, Text, Name.Other,
+ Text), 'slashstartsregex'
+ ),
+ # Match variable type keywords
+ (r'\b(string|bool|number)\b', Keyword.Type),
+ # Match stuff like: constructor
+ (r'\b(constructor|declare|interface|as|AS)\b', Keyword.Reserved),
+ # Match stuff like: super(argument, list)
+ (
+ r'(super)(\s*)\(([a-zA-Z0-9,_?.$\s]+\s*)\)',
+ bygroups(Keyword.Reserved, Text), 'slashstartsregex'
+ ),
+ # Match stuff like: function() {...}
+ (r'([a-zA-Z_?.$][\w?.$]*)\(\) \{', Name.Other, 'slashstartsregex'),
+ # Match stuff like: (function: return type)
+ (
+ r'([a-zA-Z0-9_?.$][\w?.$]*)(\s*:\s*)',
+ bygroups(Name.Other, Text), 'typeexp'
+ ),
+ # Match stuff like: type Foo = Bar | Baz
+ (
+ r'\b(type)(\s*)([a-zA-Z0-9_?.$]+)(\s*)(=)(\s*)',
+ bygroups(
+ Keyword.Reserved, Text, Name.Other, Text, Operator, Text
+ ), 'typeexp'
+ ),
+ (r'[$a-zA-Z_][a-zA-Z0-9_]*', Name.Other),
+ (r'[0-9][0-9]*\.[0-9]+([eE][0-9]+)?[fd]?', Number.Float),
+ (r'0x[0-9a-fA-F]+', Number.Hex),
+ (r'[0-9]+', Number.Integer),
+ (r'"(\\\\|\\"|[^"])*"', String.Double),
+ (r"'(\\\\|\\'|[^'])*'", String.Single),
+ ]
+ }
+
+
+# Map from token id to props.
+# Properties can't be added to tokens
+# since they derive from Python's tuple.
+token_props = {}
+
+
+class LinkFilter(Filter):
+ def __init__(self, app, **options):
+ self.app = app
+ Filter.__init__(self, **options)
+
+ def _filter_one_literal(self, ttype, value):
+ last = 0
+ for m in re.finditer(literal_reg, value):
+ pre = value[last:m.start()]
+ if pre:
+ yield ttype, pre
+ t = copy_token(ttype)
+ tok_setprop(t, "is_literal", True)
+ yield t, m.group(1)
+ last = m.end()
+ post = value[last:]
+ if post:
+ yield ttype, post
+
+
+ def filter(self, lexer, stream):
+ for ttype, value in stream:
+ if ttype in Token.Keyword.Type:
+ t = copy_token(ttype)
+ tok_setprop(t, "xref", value.strip())
+ tok_setprop(t, "is_identifier", True)
+ yield t, value
+ elif ttype in Token.Comment:
+ last = 0
+ for m in re.finditer(link_reg, value):
+ pre = value[last:m.start()]
+ if pre:
+ yield from self._filter_one_literal(ttype, pre)
+ t = copy_token(ttype)
+ x1, x2 = m.groups()
+ x0 = m.group(0)
+ if x2 is None:
+ caption = x1.strip()
+ xref = x1.strip()
+ else:
+ caption = x1.strip()
+ xref = x2.strip()
+ tok_setprop(t, "xref", xref)
+ tok_setprop(t, "caption", caption)
+ if x0.endswith("_"):
+ tok_setprop(t, "trailing_underscore", True)
+ yield t, m.group(1)
+ last = m.end()
+ post = value[last:]
+ if post:
+ yield from self._filter_one_literal(ttype, post)
+ else:
+ yield ttype, value
+
+
+_escape_html_table = {
+ ord('&'): u'&amp;',
+ ord('<'): u'&lt;',
+ ord('>'): u'&gt;',
+ ord('"'): u'&quot;',
+ ord("'"): u'&#39;',
+}
+
+
+class LinkingHtmlFormatter(HtmlFormatter):
+ def __init__(self, **kwargs):
+ super(LinkingHtmlFormatter, self).__init__(**kwargs)
+ self._builder = kwargs['_builder']
+ self._bridge = kwargs['_bridge']
+
+ def _get_value(self, value, tok):
+ xref = tok_getprop(tok, "xref")
+ caption = tok_getprop(tok, "caption")
+
+ if tok_getprop(tok, "is_literal"):
+ return '<span style="font-weight: bolder">%s</span>' % (value,)
+
+ if tok_getprop(tok, "trailing_underscore"):
+ logger.warn(
+ "{}:{}: code block contains xref to '{}' with unsupported trailing underscore"
+ .format(self._bridge.path, self._bridge.line, xref)
+ )
+
+ if tok_getprop(tok, "is_identifier"):
+ if xref.startswith('"'):
+ return value
+ if xref in ("number", "object", "string", "boolean", "any", "true", "false"):
+ return value
+
+ if xref is None:
+ return value
+ content = caption if caption is not None else value
+ ts = self._builder.env.get_domain('ts')
+ r1 = ts.objects.get(("type", xref), None)
+ if r1 is not None:
+ rel_uri = self._builder.get_relative_uri(self._bridge.docname, r1[0]) + "#" + r1[1]
+ return '<a style="color:inherit;text-decoration:underline" href="%s">%s</a>' % (
+ rel_uri, content
+ )
+
+ std = self._builder.env.get_domain('std')
+ r2 = std.labels.get(xref.lower(), None)
+ if r2 is not None:
+ rel_uri = self._builder.get_relative_uri(self._bridge.docname, r2[0]) + "#" + r2[1]
+ return '<a style="color:inherit;text-decoration:underline" href="%s">%s</a>' % (
+ rel_uri, content
+ )
+ r3 = std.anonlabels.get(xref.lower(), None)
+ if r3 is not None:
+ rel_uri = self._builder.get_relative_uri(self._bridge.docname, r3[0]) + "#" + r3[1]
+ return '<a style="color:inherit;text-decoration:underline" href="%s">%s</a>' % (
+ rel_uri, content
+ )
+
+ logger.warn("{}:{}: code block contains unresolved xref '{}'".format(self._bridge.path, self._bridge.line, xref))
+
+ return value
+
+
+ def _fmt(self, value, tok):
+ cls = self._get_css_class(tok)
+ value = self._get_value(value, tok)
+ if cls is None or cls == "":
+ return value
+ return '<span class="%s">%s</span>' % (cls, value)
+
+ def _format_lines(self, tokensource):
+ """
+ Just format the tokens, without any wrapping tags.
+ Yield individual lines.
+ """
+ lsep = self.lineseparator
+ escape_table = _escape_html_table
+
+ line = ''
+ for ttype, value in tokensource:
+ link = get_annotation(ttype, "link")
+
+ parts = value.translate(escape_table).split('\n')
+
+ if len(parts) == 0:
+ # empty token, usually should not happen
+ pass
+ elif len(parts) == 1:
+ # no newline before or after token
+ line += self._fmt(parts[0], ttype)
+ else:
+ line += self._fmt(parts[0], ttype)
+ yield 1, line + lsep
+ for part in parts[1:-1]:
+ yield 1, self._fmt(part, ttype) + lsep
+ line = self._fmt(parts[-1], ttype)
+
+ if line:
+ yield 1, line + lsep
+
+
+class MyPygmentsBridge(PygmentsBridge):
+ def __init__(self, builder, trim_doctest_flags):
+ self.dest = "html"
+ self.trim_doctest_flags = trim_doctest_flags
+ self.formatter_args = {
+ 'style': SphinxStyle,
+ '_builder': builder,
+ '_bridge': self
+ }
+ self.formatter = LinkingHtmlFormatter
+ self.builder = builder
+ self.path = None
+ self.line = None
+ self.docname = None
+
+ def highlight_block(
+ self, source, lang, opts=None, force=False, location=None, **kwargs
+ ):
+ docname, line = location
+ self.line = line
+ self.path = self.builder.env.doc2path(docname)
+ self.docname = docname
+ return super().highlight_block(
+ source, lang, opts, force, location, **kwargs
+ )
+
+
+class MyHtmlBuilder(StandaloneHTMLBuilder):
+ name = "html-linked"
+
+ def init_highlighter(self):
+ if self.config.pygments_style is not None:
+ style = self.config.pygments_style
+ elif self.theme:
+ style = self.theme.get_confstr('theme', 'pygments_style', 'none')
+ else:
+ style = 'sphinx'
+ self.highlighter = MyPygmentsBridge(
+ self, self.config.trim_doctest_flags
+ )
+
+
+def get_annotation(tok, key):
+ if not hasattr(tok, "kv"):
+ return None
+ return tok.kv.get(key)
+
+
+def copy_token(tok):
+ new_tok = _TokenType(tok)
+ # This part is very fragile against API changes ...
+ new_tok.subtypes = set(tok.subtypes)
+ new_tok.parent = tok.parent
+ return new_tok
+
+
+def tok_setprop(tok, key, value):
+ tokid = id(tok)
+ e = token_props.get(tokid)
+ if e is None:
+ e = token_props[tokid] = (tok, {})
+ _, kv = e
+ kv[key] = value
+
+
+def tok_getprop(tok, key):
+ tokid = id(tok)
+ e = token_props.get(tokid)
+ if e is None:
+ return None
+ _, kv = e
+ return kv.get(key)
+
+
+link_reg = re.compile(r"(?<!`)`([^`<]+)\s*(?:<([^>]+)>)?\s*`_?")
+literal_reg = re.compile(r"``([^`]+)``")
+
+
+def setup(app):
+ lexer = BetterTypeScriptLexer()
+ lexer.add_filter(LinkFilter(app))
+ app.add_lexer('tsref', lexer)
+ app.add_domain(TypeScriptDomain)
+ app.add_builder(MyHtmlBuilder)