taler-docs

Documentation for GNU Taler components, APIs and protocols
Log | Files | Refs | README | LICENSE

typescriptdomain.py (19435B)


      1 """
      2 TypeScript domain.
      3 
      4 :copyright: Copyright 2019 by Taler Systems SA
      5 :license: LGPLv3+
      6 :author: Florian Dold
      7 """
      8 
      9 import re
     10 
     11 from pathlib import Path
     12 
     13 from docutils import nodes
     14 from typing import List, Optional, Iterable, Dict, Tuple
     15 from typing import cast
     16 
     17 from pygments.lexers import get_lexer_by_name
     18 from pygments.filter import Filter
     19 from pygments.token import Literal, Text, Operator, Keyword, Name, Number
     20 from pygments.token import Comment, Token, _TokenType
     21 from pygments.token import *
     22 from pygments.lexer import RegexLexer, bygroups, include
     23 from pygments.formatters import HtmlFormatter
     24 
     25 from docutils import nodes
     26 from docutils.nodes import Element, Node
     27 
     28 from sphinx.roles import XRefRole
     29 from sphinx.domains import Domain, ObjType, Index
     30 from sphinx.directives import directives
     31 from sphinx.util.docutils import SphinxDirective
     32 from sphinx.util.nodes import make_refnode
     33 from sphinx.util import logging
     34 from sphinx.highlighting import PygmentsBridge
     35 from sphinx.builders.html import StandaloneHTMLBuilder
     36 from sphinx.pygments_styles import SphinxStyle
     37 
     38 logger = logging.getLogger(__name__)
     39 
     40 
     41 class TypeScriptDefinition(SphinxDirective):
     42     """
     43     Directive for a code block with special highlighting or line numbering
     44     settings.
     45     """
     46 
     47     has_content = True
     48     required_arguments = 1
     49     optional_arguments = 0
     50     final_argument_whitespace = False
     51     option_spec = {
     52         "force": directives.flag,
     53         "linenos": directives.flag,
     54         "dedent": int,
     55         "lineno-start": int,
     56         "emphasize-lines": directives.unchanged_required,
     57         "caption": directives.unchanged_required,
     58         "class": directives.class_option,
     59     }
     60 
     61     def run(self) -> List[Node]:
     62         document = self.state.document
     63         code = "\n".join(self.content)
     64         location = self.state_machine.get_source_and_line(self.lineno)
     65 
     66         linespec = self.options.get("emphasize-lines")
     67         if linespec:
     68             try:
     69                 nlines = len(self.content)
     70                 hl_lines = parselinenos(linespec, nlines)
     71                 if any(i >= nlines for i in hl_lines):
     72                     logger.warning(
     73                         __("line number spec is out of range(1-%d): %r")
     74                         % (nlines, self.options["emphasize-lines"]),
     75                         location=location,
     76                     )
     77 
     78                 hl_lines = [x + 1 for x in hl_lines if x < nlines]
     79             except ValueError as err:
     80                 return [document.reporter.warning(err, line=self.lineno)]
     81         else:
     82             hl_lines = None
     83 
     84         if "dedent" in self.options:
     85             location = self.state_machine.get_source_and_line(self.lineno)
     86             lines = code.split("\n")
     87             lines = dedent_lines(lines, self.options["dedent"], location=location)
     88             code = "\n".join(lines)
     89 
     90         literal = nodes.literal_block(code, code)  # type: Element
     91         if "linenos" in self.options or "lineno-start" in self.options:
     92             literal["linenos"] = True
     93         literal["classes"] += self.options.get("class", [])
     94         literal["force"] = "force" in self.options
     95         literal["language"] = "tsref"
     96         extra_args = literal["highlight_args"] = {}
     97         if hl_lines is not None:
     98             extra_args["hl_lines"] = hl_lines
     99         if "lineno-start" in self.options:
    100             extra_args["linenostart"] = self.options["lineno-start"]
    101         self.set_source_info(literal)
    102 
    103         caption = self.options.get("caption")
    104         if caption:
    105             try:
    106                 literal = container_wrapper(self, literal, caption)
    107             except ValueError as exc:
    108                 return [document.reporter.warning(exc, line=self.lineno)]
    109 
    110         tsid = "tsref-type-" + self.arguments[0]
    111         literal["ids"].append(tsid)
    112 
    113         tsname = self.arguments[0]
    114         ts = self.env.get_domain("ts")
    115         ts.add_object("type", tsname, self.env.docname, tsid)
    116 
    117         return [literal]
    118 
    119 
    120 class TypeScriptDomain(Domain):
    121     """TypeScript domain."""
    122 
    123     name = "ts"
    124     label = "TypeScript"
    125 
    126     directives = {
    127         "def": TypeScriptDefinition,
    128     }
    129 
    130     roles = {
    131         "type": XRefRole(
    132             lowercase=False, warn_dangling=True, innernodeclass=nodes.inline
    133         ),
    134     }
    135 
    136     dangling_warnings = {
    137         "type": "undefined TypeScript type: %(target)s",
    138     }
    139 
    140     def resolve_xref(self, env, fromdocname, builder, typ, target, node, contnode):
    141         try:
    142             info = self.objects[(str(typ), str(target))]
    143         except KeyError:
    144             logger.warn("type {}/{} not found".format(typ, target))
    145             return None
    146         else:
    147             anchor = "tsref-type-{}".format(str(target))
    148             title = typ.upper() + " " + target
    149             return make_refnode(builder, fromdocname, info[0], anchor, contnode, title)
    150 
    151     def resolve_any_xref(self, env, fromdocname, builder, target, node, contnode):
    152         """Resolve the pending_xref *node* with the given *target*.
    153 
    154         The reference comes from an "any" or similar role, which means that Sphinx
    155         don't know the type.
    156 
    157         For now sphinxcontrib-httpdomain doesn't resolve any xref nodes.
    158 
    159         :return:
    160            list of tuples ``('domain:role', newnode)``, where ``'domain:role'``
    161            is the name of a role that could have created the same reference,
    162         """
    163         ret = []
    164         try:
    165             info = self.objects[("type", str(target))]
    166         except KeyError:
    167             pass
    168         else:
    169             anchor = "tsref-type-{}".format(str(target))
    170             title = "TYPE" + " " + target
    171             node = make_refnode(builder, fromdocname, info[0], anchor, contnode, title)
    172             ret.append(("ts:type", node))
    173         return ret
    174 
    175     @property
    176     def objects(self) -> Dict[Tuple[str, str], Tuple[str, str]]:
    177         return self.data.setdefault(
    178             "objects", {}
    179         )  # (objtype, name) -> docname, labelid
    180 
    181     def add_object(self, objtype: str, name: str, docname: str, labelid: str) -> None:
    182         self.objects[objtype, name] = (docname, labelid)
    183 
    184 
    185 class BetterTypeScriptLexer(RegexLexer):
    186     """
    187     For `TypeScript <https://www.typescriptlang.org/>`_ source code.
    188     """
    189 
    190     name = "TypeScript"
    191     aliases = ["ts"]
    192     filenames = ["*.ts"]
    193     mimetypes = ["text/x-typescript"]
    194 
    195     flags = re.DOTALL
    196     tokens = {
    197         "commentsandwhitespace": [
    198             (r"\s+", Text),
    199             (r"<!--", Comment),
    200             (r"//.*?\n", Comment.Single),
    201             (r"/\*.*?\*/", Comment.Multiline),
    202         ],
    203         "slashstartsregex": [
    204             include("commentsandwhitespace"),
    205             (
    206                 r"/(\\.|[^[/\\\n]|\[(\\.|[^\]\\\n])*])+/" r"([gim]+\b|\B)",
    207                 String.Regex,
    208                 "#pop",
    209             ),
    210             (r"(?=/)", Text, ("#pop", "badregex")),
    211             (r"", Text, "#pop"),
    212         ],
    213         "badregex": [(r"\n", Text, "#pop")],
    214         "typeexp": [
    215             (r"[a-zA-Z0-9_?.$]+", Keyword.Type),
    216             (r"\s+", Text),
    217             (r"[|]", Text),
    218             (r"\n", Text, "#pop"),
    219             (r";", Text, "#pop"),
    220             (r"", Text, "#pop"),
    221         ],
    222         "root": [
    223             (r"^(?=\s|/|<!--)", Text, "slashstartsregex"),
    224             include("commentsandwhitespace"),
    225             (
    226                 r"\+\+|--|~|&&|\?|:|\|\||\\(?=\n)|"
    227                 r"(<<|>>>?|==?|!=?|[-<>+*%&\|\^/])=?",
    228                 Operator,
    229                 "slashstartsregex",
    230             ),
    231             (r"[{(\[;,]", Punctuation, "slashstartsregex"),
    232             (r"[})\].]", Punctuation),
    233             (
    234                 r"(for|in|while|do|break|return|continue|switch|case|default|if|else|"
    235                 r"throw|try|catch|finally|new|delete|typeof|instanceof|void|"
    236                 r"this)\b",
    237                 Keyword,
    238                 "slashstartsregex",
    239             ),
    240             (
    241                 r"(var|let|const|with|function)\b",
    242                 Keyword.Declaration,
    243                 "slashstartsregex",
    244             ),
    245             (
    246                 r"(abstract|boolean|byte|char|class|const|debugger|double|enum|export|"
    247                 r"extends|final|float|goto|implements|import|int|interface|long|native|"
    248                 r"package|private|protected|public|short|static|super|synchronized|throws|"
    249                 r"transient|volatile)\b",
    250                 Keyword.Reserved,
    251             ),
    252             (r"(true|false|null|NaN|Infinity|undefined)\b", Keyword.Constant),
    253             (
    254                 r"(Array|Boolean|Date|Error|Function|Math|netscape|"
    255                 r"Number|Object|Packages|RegExp|String|sun|decodeURI|"
    256                 r"decodeURIComponent|encodeURI|encodeURIComponent|"
    257                 r"Error|eval|isFinite|isNaN|parseFloat|parseInt|document|this|"
    258                 r"window)\b",
    259                 Name.Builtin,
    260             ),
    261             # Match stuff like: module name {...}
    262             (
    263                 r"\b(module)(\s*)(\s*[a-zA-Z0-9_?.$][\w?.$]*)(\s*)",
    264                 bygroups(Keyword.Reserved, Text, Name.Other, Text),
    265                 "slashstartsregex",
    266             ),
    267             # Match variable type keywords
    268             (r"\b(string|bool|number)\b", Keyword.Type),
    269             # Match stuff like: constructor
    270             (r"\b(constructor|declare|interface|as|AS)\b", Keyword.Reserved),
    271             # Match stuff like: super(argument, list)
    272             (
    273                 r"(super)(\s*)\(([a-zA-Z0-9,_?.$\s]+\s*)\)",
    274                 bygroups(Keyword.Reserved, Text),
    275                 "slashstartsregex",
    276             ),
    277             # Match stuff like: function() {...}
    278             (r"([a-zA-Z_?.$][\w?.$]*)\(\) \{", Name.Other, "slashstartsregex"),
    279             # Match stuff like: (function: return type)
    280             (
    281                 r"([a-zA-Z0-9_?.$][\w?.$]*)(\s*:\s*)",
    282                 bygroups(Name.Other, Text),
    283                 "typeexp",
    284             ),
    285             # Match stuff like: type Foo = Bar | Baz
    286             (
    287                 r"\b(type)(\s*)([a-zA-Z0-9_?.$]+)(\s*)(=)(\s*)",
    288                 bygroups(Keyword.Reserved, Text, Name.Other, Text, Operator, Text),
    289                 "typeexp",
    290             ),
    291             (r"[$a-zA-Z_][a-zA-Z0-9_]*", Name.Other),
    292             (r"[0-9][0-9]*\.[0-9]+([eE][0-9]+)?[fd]?", Number.Float),
    293             (r"0x[0-9a-fA-F]+", Number.Hex),
    294             (r"[0-9]+", Number.Integer),
    295             (r'"(\\\\|\\"|[^"])*"', String.Double),
    296             (r"'(\\\\|\\'|[^'])*'", String.Single),
    297         ],
    298     }
    299 
    300 
    301 # Map from token id to props.
    302 # Properties can't be added to tokens
    303 # since they derive from Python's tuple.
    304 token_props = {}
    305 
    306 
    307 class LinkFilter(Filter):
    308     def __init__(self, app, **options):
    309         self.app = app
    310         Filter.__init__(self, **options)
    311 
    312     def _filter_one_literal(self, ttype, value):
    313         last = 0
    314         for m in re.finditer(literal_reg, value):
    315             pre = value[last : m.start()]
    316             if pre:
    317                 yield ttype, pre
    318             t = copy_token(ttype)
    319             tok_setprop(t, "is_literal", True)
    320             yield t, m.group(1)
    321             last = m.end()
    322         post = value[last:]
    323         if post:
    324             yield ttype, post
    325 
    326     def filter(self, lexer, stream):
    327         for ttype, value in stream:
    328             if ttype in Token.Keyword.Type:
    329                 t = copy_token(ttype)
    330                 tok_setprop(t, "xref", value.strip())
    331                 tok_setprop(t, "is_identifier", True)
    332                 yield t, value
    333             elif ttype in Token.Comment:
    334                 last = 0
    335                 for m in re.finditer(link_reg, value):
    336                     pre = value[last : m.start()]
    337                     if pre:
    338                         yield from self._filter_one_literal(ttype, pre)
    339                     t = copy_token(ttype)
    340                     x1, x2 = m.groups()
    341                     x0 = m.group(0)
    342                     if x2 is None:
    343                         caption = x1.strip()
    344                         xref = x1.strip()
    345                     else:
    346                         caption = x1.strip()
    347                         xref = x2.strip()
    348                     tok_setprop(t, "xref", xref)
    349                     tok_setprop(t, "caption", caption)
    350                     if x0.endswith("_"):
    351                         tok_setprop(t, "trailing_underscore", True)
    352                     yield t, m.group(1)
    353                     last = m.end()
    354                 post = value[last:]
    355                 if post:
    356                     yield from self._filter_one_literal(ttype, post)
    357             else:
    358                 yield ttype, value
    359 
    360 
    361 _escape_html_table = {
    362     ord("&"): u"&amp;",
    363     ord("<"): u"&lt;",
    364     ord(">"): u"&gt;",
    365     ord('"'): u"&quot;",
    366     ord("'"): u"&#39;",
    367 }
    368 
    369 
    370 class LinkingHtmlFormatter(HtmlFormatter):
    371     def __init__(self, **kwargs):
    372         super(LinkingHtmlFormatter, self).__init__(**kwargs)
    373         self._builder = kwargs["_builder"]
    374         self._bridge = kwargs["_bridge"]
    375 
    376     def _get_value(self, value, tok):
    377         xref = tok_getprop(tok, "xref")
    378         caption = tok_getprop(tok, "caption")
    379 
    380         if tok_getprop(tok, "is_literal"):
    381             return '<span style="font-weight: bolder">%s</span>' % (value,)
    382 
    383         if tok_getprop(tok, "trailing_underscore"):
    384             logger.warn(
    385                 "{}:{}: code block contains xref to '{}' with unsupported trailing underscore".format(
    386                     self._bridge.path, self._bridge.line, xref
    387                 )
    388             )
    389 
    390         if tok_getprop(tok, "is_identifier"):
    391             if xref.startswith('"'):
    392                 return value
    393             if re.match("^[0-9]+$", xref) is not None:
    394                 return value
    395             if xref in (
    396                 "number",
    397                 "object",
    398                 "string",
    399                 "boolean",
    400                 "any",
    401                 "true",
    402                 "false",
    403                 "null",
    404                 "undefined",
    405                 "Array",
    406                 "unknown",
    407             ):
    408                 return value
    409 
    410         if self._bridge.docname is None:
    411             return value
    412         if xref is None:
    413             return value
    414         content = caption if caption is not None else value
    415         ts = self._builder.env.get_domain("ts")
    416         r1 = ts.objects.get(("type", xref), None)
    417         if r1 is not None:
    418             rel_uri = (
    419                 self._builder.get_relative_uri(self._bridge.docname, r1[0])
    420                 + "#"
    421                 + r1[1]
    422             )
    423             return (
    424                 '<a style="color:inherit;text-decoration:underline" href="%s">%s</a>'
    425                 % (rel_uri, content)
    426             )
    427 
    428         std = self._builder.env.get_domain("std")
    429         r2 = std.labels.get(xref.lower(), None)
    430         if r2 is not None:
    431             rel_uri = (
    432                 self._builder.get_relative_uri(self._bridge.docname, r2[0])
    433                 + "#"
    434                 + r2[1]
    435             )
    436             return (
    437                 '<a style="color:inherit;text-decoration:underline" href="%s">%s</a>'
    438                 % (rel_uri, content)
    439             )
    440         r3 = std.anonlabels.get(xref.lower(), None)
    441         if r3 is not None:
    442             rel_uri = (
    443                 self._builder.get_relative_uri(self._bridge.docname, r3[0])
    444                 + "#"
    445                 + r3[1]
    446             )
    447             return (
    448                 '<a style="color:inherit;text-decoration:underline" href="%s">%s</a>'
    449                 % (rel_uri, content)
    450             )
    451 
    452         logger.warn(
    453             "{}:{}: code block contains unresolved xref '{}'".format(
    454                 self._bridge.path, self._bridge.line, xref
    455             )
    456         )
    457 
    458         return value
    459 
    460     def _fmt(self, value, tok):
    461         cls = self._get_css_class(tok)
    462         value = self._get_value(value, tok)
    463         if cls is None or cls == "":
    464             return value
    465         return '<span class="%s">%s</span>' % (cls, value)
    466 
    467     def _format_lines(self, tokensource):
    468         """
    469         Just format the tokens, without any wrapping tags.
    470         Yield individual lines.
    471         """
    472         lsep = self.lineseparator
    473         escape_table = _escape_html_table
    474 
    475         line = ""
    476         for ttype, value in tokensource:
    477             link = get_annotation(ttype, "link")
    478 
    479             parts = value.translate(escape_table).split("\n")
    480 
    481             if len(parts) == 0:
    482                 # empty token, usually should not happen
    483                 pass
    484             elif len(parts) == 1:
    485                 # no newline before or after token
    486                 line += self._fmt(parts[0], ttype)
    487             else:
    488                 line += self._fmt(parts[0], ttype)
    489                 yield 1, line + lsep
    490                 for part in parts[1:-1]:
    491                     yield 1, self._fmt(part, ttype) + lsep
    492                 line = self._fmt(parts[-1], ttype)
    493 
    494         if line:
    495             yield 1, line + lsep
    496 
    497 
    498 class MyPygmentsBridge(PygmentsBridge):
    499     def __init__(self, builder, trim_doctest_flags):
    500         self.dest = "html"
    501         self.trim_doctest_flags = trim_doctest_flags
    502         self.formatter_args = {
    503             "style": SphinxStyle,
    504             "_builder": builder,
    505             "_bridge": self,
    506         }
    507         self.formatter = LinkingHtmlFormatter
    508         self.builder = builder
    509         self.path = None
    510         self.line = None
    511         self.docname = None
    512 
    513     def highlight_block(
    514         self, source, lang, opts=None, force=False, location=None, **kwargs
    515     ):
    516         if isinstance(location, tuple):
    517             docname, line = location
    518             self.line = line
    519             self.path = self.builder.env.doc2path(docname)
    520             self.docname = docname
    521         elif isinstance(location, Element):
    522             self.line = location.line
    523             self.path = location.source
    524             self.docname = self.builder.env.path2doc(self.path)
    525         return super().highlight_block(source, lang, opts, force, location, **kwargs)
    526 
    527 
    528 class MyHtmlBuilder(StandaloneHTMLBuilder):
    529     name = "html-linked"
    530 
    531     def init_highlighter(self):
    532         if self.config.pygments_style is not None:
    533             style = self.config.pygments_style
    534         elif self.theme:
    535             style = self.theme.get_confstr("theme", "pygments_style", "none")
    536         else:
    537             style = "sphinx"
    538         self.highlighter = MyPygmentsBridge(self, self.config.trim_doctest_flags)
    539         self.dark_highlighter = None
    540 
    541 
    542 def get_annotation(tok, key):
    543     if not hasattr(tok, "kv"):
    544         return None
    545     return tok.kv.get(key)
    546 
    547 
    548 def copy_token(tok):
    549     new_tok = _TokenType(tok)
    550     # This part is very fragile against API changes ...
    551     new_tok.subtypes = set(tok.subtypes)
    552     new_tok.parent = tok.parent
    553     return new_tok
    554 
    555 
    556 def tok_setprop(tok, key, value):
    557     tokid = id(tok)
    558     e = token_props.get(tokid)
    559     if e is None:
    560         e = token_props[tokid] = (tok, {})
    561     _, kv = e
    562     kv[key] = value
    563 
    564 
    565 def tok_getprop(tok, key):
    566     tokid = id(tok)
    567     e = token_props.get(tokid)
    568     if e is None:
    569         return None
    570     _, kv = e
    571     return kv.get(key)
    572 
    573 
    574 link_reg = re.compile(r"(?<!`)`([^`<]+)\s*(?:<([^>]+)>)?\s*`_?")
    575 literal_reg = re.compile(r"``([^`]+)``")
    576 
    577 
    578 def setup(app):
    579 
    580     class TsrefLexer(BetterTypeScriptLexer):
    581         def __init__(self, **options):
    582             super().__init__(**options)
    583             self.add_filter(LinkFilter(app))
    584 
    585     app.add_lexer("tsref", TsrefLexer)
    586     app.add_domain(TypeScriptDomain)
    587     app.add_builder(MyHtmlBuilder)