typescriptdomain.py (25872B)
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 docutils import nodes 12 from typing import Dict, Iterator, List, Tuple 13 14 from pygments.filter import Filter 15 from pygments.token import ( 16 Comment, 17 Keyword, 18 Name, 19 Number, 20 Operator, 21 Punctuation, 22 String, 23 Text, 24 Token, 25 _TokenType, 26 ) 27 from pygments.lexer import RegexLexer, bygroups, include 28 from pygments.formatters import HtmlFormatter 29 30 from docutils.nodes import Element, Node 31 32 from sphinx.roles import XRefRole 33 from sphinx import addnodes 34 from sphinx.domains import Domain, ObjType 35 from sphinx.directives import ObjectDescription, directives 36 from sphinx.directives.code import ( 37 container_wrapper, 38 dedent_lines, 39 parse_line_num_spec, 40 ) 41 from sphinx.locale import __ 42 from sphinx.util.docutils import SphinxDirective 43 from sphinx.util.nodes import make_refnode 44 from sphinx.util import logging 45 from sphinx.highlighting import PygmentsBridge 46 47 logger = logging.getLogger(__name__) 48 49 50 class TypeScriptDefinition(SphinxDirective): 51 """ 52 Directive for a code block with special highlighting or line numbering 53 settings. 54 """ 55 56 has_content = True 57 required_arguments = 1 58 optional_arguments = 0 59 final_argument_whitespace = False 60 option_spec = { 61 "force": directives.flag, 62 "linenos": directives.flag, 63 "dedent": int, 64 "lineno-start": int, 65 "emphasize-lines": directives.unchanged_required, 66 "caption": directives.unchanged_required, 67 "class": directives.class_option, 68 } 69 70 def run(self) -> List[Node]: 71 document = self.state.document 72 code = "\n".join(self.content) 73 location = self.state_machine.get_source_and_line(self.lineno) 74 75 linespec = self.options.get("emphasize-lines") 76 if linespec: 77 try: 78 nlines = len(self.content) 79 hl_lines = parse_line_num_spec(linespec, nlines) 80 if any(i >= nlines for i in hl_lines): 81 logger.warning( 82 __("line number spec is out of range(1-%d): %r"), 83 nlines, 84 self.options["emphasize-lines"], 85 location=location, 86 ) 87 88 hl_lines = [x + 1 for x in hl_lines if x < nlines] 89 except ValueError as err: 90 return [document.reporter.warning(err, line=self.lineno)] 91 else: 92 hl_lines = None 93 94 if "dedent" in self.options: 95 location = self.state_machine.get_source_and_line(self.lineno) 96 lines = code.splitlines(True) 97 lines = dedent_lines(lines, self.options["dedent"], location=location) 98 code = "".join(lines) 99 100 literal = nodes.literal_block(code, code) # type: Element 101 if "linenos" in self.options or "lineno-start" in self.options: 102 literal["linenos"] = True 103 literal["classes"] += self.options.get("class", []) 104 literal["force"] = "force" in self.options 105 literal["language"] = "tsref" 106 extra_args = literal["highlight_args"] = {} 107 if hl_lines is not None: 108 extra_args["hl_lines"] = hl_lines 109 if "lineno-start" in self.options: 110 extra_args["linenostart"] = self.options["lineno-start"] 111 self.set_source_info(literal) 112 113 caption = self.options.get("caption") 114 if caption: 115 try: 116 literal = container_wrapper(self, literal, caption) 117 except ValueError as exc: 118 return [document.reporter.warning(exc, line=self.lineno)] 119 120 tsid = "tsref-type-" + self.arguments[0] 121 literal["ids"].append(tsid) 122 123 tsname = self.arguments[0] 124 ts = self.env.get_domain("ts") 125 ts.add_object("type", tsname, self.env.docname, tsid) 126 127 return [literal] 128 129 130 class TypeScriptOperation(ObjectDescription): 131 """ 132 Directive for an operation of an operation-based, non-HTTP API 133 (such as the wallet-core client API). 134 135 Renders like an endpoint from sphinxcontrib.httpdomain and registers 136 the operation as a target for the ``ts:op`` cross-reference role: 137 138 .. ts:op:: getBalances 139 :read-only: 140 141 Description of the operation ... 142 143 Note that options and content must share the same indentation; 144 docutils misreads an option line that is indented deeper than 145 the content. 146 """ 147 148 doc_field_types = [] 149 150 option_spec = { 151 "read-only": directives.flag, 152 "deprecated": directives.flag, 153 "noindex": directives.flag, 154 } 155 156 index_label = "wallet-core operation" 157 158 def handle_signature(self, sig, signode): 159 signode += addnodes.desc_name(sig, sig) 160 if "read-only" in self.options: 161 signode += addnodes.desc_annotation("read-only", "read-only") 162 if "deprecated" in self.options: 163 signode += addnodes.desc_annotation("deprecated", "deprecated") 164 signode["fullname"] = sig 165 return sig 166 167 def needs_arglist(self): 168 return False 169 170 def add_target_and_index(self, name, sig, signode): 171 tsid = "tsref-op-" + name 172 signode["ids"].append(tsid) 173 ts = self.env.get_domain("ts") 174 ts.add_object("op", name, self.env.docname, tsid) 175 if "noindex" not in self.options: 176 self.indexnode["entries"].append( 177 ("single", "%s (%s)" % (name, self.index_label), tsid, "", None) 178 ) 179 180 def get_index_text(self, modname, name): 181 return "" 182 183 184 class TypeScriptDomain(Domain): 185 """TypeScript domain.""" 186 187 name = "ts" 188 label = "TypeScript" 189 object_types = { 190 "type": ObjType("type", "type"), 191 "op": ObjType("op", "op"), 192 } 193 initial_data = { 194 "objects": {}, 195 } 196 197 directives = { 198 "def": TypeScriptDefinition, 199 "op": TypeScriptOperation, 200 } 201 202 roles = { 203 "type": XRefRole( 204 lowercase=False, warn_dangling=True, innernodeclass=nodes.inline 205 ), 206 "op": XRefRole( 207 lowercase=False, warn_dangling=True, innernodeclass=nodes.inline 208 ), 209 } 210 211 dangling_warnings = { 212 "type": "undefined TypeScript type: %(target)s", 213 "op": "undefined operation: %(target)s", 214 } 215 216 def resolve_xref(self, env, fromdocname, builder, typ, target, node, contnode): 217 info = self.find_object(str(typ), str(target), fromdocname) 218 if info is None: 219 return None 220 title = typ.upper() + " " + target 221 return make_refnode(builder, fromdocname, info[0], info[1], contnode, title) 222 223 def resolve_any_xref(self, env, fromdocname, builder, target, node, contnode): 224 """Resolve the pending_xref *node* with the given *target*. 225 226 The reference comes from an "any" or similar role, which means that Sphinx 227 don't know the type. 228 229 For now sphinxcontrib-httpdomain doesn't resolve any xref nodes. 230 231 :return: 232 list of tuples ``('domain:role', newnode)``, where ``'domain:role'`` 233 is the name of a role that could have created the same reference, 234 """ 235 ret = [] 236 info = self.find_object("type", str(target), fromdocname) 237 if info is not None: 238 title = "TYPE" + " " + target 239 node = make_refnode(builder, fromdocname, info[0], info[1], contnode, title) 240 ret.append(("ts:type", node)) 241 return ret 242 243 @property 244 def objects(self) -> Dict[Tuple[str, str], List[Tuple[str, str]]]: 245 """Map ``(object type, name)`` to all documents defining it.""" 246 247 objects = self.data.setdefault("objects", {}) 248 # Environments written by the old extension stored just one tuple. 249 for key, value in list(objects.items()): 250 if isinstance(value, tuple): 251 objects[key] = [value] 252 return objects 253 254 def add_object(self, objtype: str, name: str, docname: str, labelid: str) -> None: 255 locations = self.objects.setdefault((objtype, name), []) 256 location = (docname, labelid) 257 if location not in locations: 258 locations.append(location) 259 260 def find_object( 261 self, objtype: str, name: str, fromdocname: str 262 ) -> Tuple[str, str] | None: 263 locations = self.objects.get((objtype, name), []) 264 for location in locations: 265 if location[0] == fromdocname: 266 return location 267 if locations: 268 return sorted(locations)[0] 269 return None 270 271 def clear_doc(self, docname: str) -> None: 272 for key, locations in list(self.objects.items()): 273 remaining = [location for location in locations if location[0] != docname] 274 if remaining: 275 self.objects[key] = remaining 276 else: 277 del self.objects[key] 278 279 def merge_domaindata(self, docnames, otherdata) -> None: 280 for (objtype, name), locations in otherdata.get("objects", {}).items(): 281 if isinstance(locations, tuple): 282 locations = [locations] 283 for docname, labelid in locations: 284 if docname in docnames: 285 self.add_object(objtype, name, docname, labelid) 286 287 def get_objects(self) -> Iterator[Tuple[str, str, str, str, str, int]]: 288 for (objtype, name), locations in self.objects.items(): 289 for docname, labelid in locations: 290 yield name, name, objtype, docname, labelid, 1 291 292 293 class BetterTypeScriptLexer(RegexLexer): 294 """ 295 For `TypeScript <https://www.typescriptlang.org/>`_ source code. 296 """ 297 298 name = "TypeScript" 299 aliases = ["ts"] 300 filenames = ["*.ts"] 301 mimetypes = ["text/x-typescript"] 302 303 flags = re.DOTALL 304 tokens = { 305 "commentsandwhitespace": [ 306 (r"\s+", Text), 307 (r"<!--", Comment), 308 (r"//.*?\n", Comment.Single), 309 (r"/\*.*?\*/", Comment.Multiline), 310 ], 311 "slashstartsregex": [ 312 include("commentsandwhitespace"), 313 ( 314 r"/(\\.|[^[/\\\n]|\[(\\.|[^\]\\\n])*])+/" r"([gim]+\b|\B)", 315 String.Regex, 316 "#pop", 317 ), 318 (r"(?=/)", Text, ("#pop", "badregex")), 319 (r"", Text, "#pop"), 320 ], 321 "badregex": [(r"\n", Text, "#pop")], 322 "typeexp": [ 323 include("commentsandwhitespace"), 324 (r"`(?:\\.|[^`])*`", String.Backtick), 325 (r'"(\\\\|\\"|[^"])*"', String.Double), 326 (r"'(\\\\|\\'|[^'])*'", String.Single), 327 (r";", Punctuation, "#pop"), 328 # Object-property names occur inside inline type literals. Leave 329 # those as ordinary names; their value type remains in this state. 330 (r"[$a-zA-Z_][a-zA-Z0-9_$]*(?=\s*\??\s*:)", Name.Other), 331 (r"[$a-zA-Z_][a-zA-Z0-9_$]*(?:\.[$a-zA-Z_][a-zA-Z0-9_$]*)*", Keyword.Type), 332 (r"[{}()\[\],.?]", Punctuation), 333 (r"[|&<>=:+*\-/]", Operator), 334 (r"[0-9]+", Number.Integer), 335 (r".", Text), 336 ], 337 "heritage": [ 338 include("commentsandwhitespace"), 339 (r"{", Punctuation, "#pop"), 340 (r"[$a-zA-Z_][a-zA-Z0-9_$]*(?:\.[$a-zA-Z_][a-zA-Z0-9_$]*)*", Keyword.Type), 341 (r"[<>,.?\[\]&|]", Punctuation), 342 (r".", Text), 343 ], 344 "root": [ 345 (r"^(?=\s|/|<!--)", Text, "slashstartsregex"), 346 include("commentsandwhitespace"), 347 # TypeScript template literal types and string templates. Full 348 # interpolation highlighting is unnecessary here, but recognizing 349 # the complete literal avoids falling back to relaxed lexing. 350 (r"`(?:\\.|[^`])*`", String.Backtick), 351 # A reserved word can still be an object-property name (for 352 # example ``class``). Enter the type state when its colon arrives. 353 (r"(:)(\s*)", bygroups(Text, Text), "typeexp"), 354 # Resume a multi-line union/intersection after an inline object 355 # member caused the type state to end at its semicolon. 356 (r"([|&])(\s*)", bygroups(Operator, Text), "typeexp"), 357 ( 358 r"\+\+|--|~|&&|\?|:|\|\||\\(?=\n)|" 359 r"(<<|>>>?|==?|!=?|[-<>+*%&\|\^/])=?", 360 Operator, 361 "slashstartsregex", 362 ), 363 (r"[{(\[;,]", Punctuation, "slashstartsregex"), 364 (r"[})\].]", Punctuation), 365 ( 366 r"(for|in|while|do|break|return|continue|switch|case|default|if|else|" 367 r"throw|try|catch|finally|new|delete|typeof|instanceof|void|" 368 r"this)\b", 369 Keyword, 370 "slashstartsregex", 371 ), 372 ( 373 r"(var|let|const|with|function)\b", 374 Keyword.Declaration, 375 "slashstartsregex", 376 ), 377 ( 378 r"(abstract|boolean|byte|char|class|const|debugger|double|enum|export|" 379 r"final|float|goto|import|int|interface|long|native|" 380 r"package|private|protected|public|short|static|super|synchronized|throws|" 381 r"transient|volatile)\b", 382 Keyword.Reserved, 383 ), 384 (r"(true|false|null|NaN|Infinity|undefined)\b", Keyword.Constant), 385 ( 386 r"(Array|Boolean|Date|Error|Function|Math|netscape|" 387 r"Number|Object|Packages|RegExp|String|sun|decodeURI|" 388 r"decodeURIComponent|encodeURI|encodeURIComponent|" 389 r"Error|eval|isFinite|isNaN|parseFloat|parseInt|document|this|" 390 r"window)\b", 391 Name.Builtin, 392 ), 393 # Match stuff like: module name {...} 394 ( 395 r"\b(module)(\s*)(\s*[a-zA-Z0-9_?.$][\w?.$]*)(\s*)", 396 bygroups(Keyword.Reserved, Text, Name.Other, Text), 397 "slashstartsregex", 398 ), 399 # Match variable type keywords 400 (r"\b(string|bool|number)\b", Keyword.Type), 401 # Match stuff like: constructor 402 (r"\b(constructor|declare|interface|as|AS)\b", Keyword.Reserved), 403 # Match interface/class heritage clauses. 404 ( 405 r"\b(extends|implements)(\s+)", 406 bygroups(Keyword.Reserved, Text), 407 "heritage", 408 ), 409 # Match stuff like: super(argument, list) 410 ( 411 r"(super)(\s*)\(([a-zA-Z0-9,_?.$\s]+\s*)\)", 412 bygroups(Keyword.Reserved, Text), 413 "slashstartsregex", 414 ), 415 # Match stuff like: function() {...} 416 (r"([a-zA-Z_?.$][\w?.$]*)\(\) \{", Name.Other, "slashstartsregex"), 417 # Match stuff like: (function: return type) 418 ( 419 r"([a-zA-Z0-9_?.$][\w?.$]*)(\s*:\s*)", 420 bygroups(Name.Other, Text), 421 "typeexp", 422 ), 423 # Match stuff like: type Foo = Bar | Baz 424 ( 425 r"\b(type)(\s+)([$a-zA-Z_][a-zA-Z0-9_$]*)([^=]*)(=)(\s*)", 426 bygroups(Keyword.Reserved, Text, Name.Other, Text, Operator, Text), 427 "typeexp", 428 ), 429 (r"[$a-zA-Z_][a-zA-Z0-9_]*", Name.Other), 430 (r"[0-9][0-9]*\.[0-9]+([eE][0-9]+)?[fd]?", Number.Float), 431 (r"0x[0-9a-fA-F]+", Number.Hex), 432 (r"[0-9]+", Number.Integer), 433 (r'"(\\\\|\\"|[^"])*"', String.Double), 434 (r"'(\\\\|\\'|[^'])*'", String.Single), 435 ], 436 } 437 438 439 # Map from token id to props. 440 # Properties can't be added to tokens 441 # since they derive from Python's tuple. 442 token_props = {} 443 444 445 class LinkFilter(Filter): 446 def _filter_one_literal(self, ttype, value): 447 last = 0 448 for m in re.finditer(literal_reg, value): 449 pre = value[last : m.start()] 450 if pre: 451 yield ttype, pre 452 t = copy_token(ttype) 453 tok_setprop(t, "is_literal", True) 454 yield t, m.group(1) 455 last = m.end() 456 post = value[last:] 457 if post: 458 yield ttype, post 459 460 def filter(self, lexer, stream): 461 for ttype, value in stream: 462 if ttype in Token.Keyword.Type: 463 t = copy_token(ttype) 464 tok_setprop(t, "xref", value.strip()) 465 tok_setprop(t, "is_identifier", True) 466 tok_setprop(t, "optional_xref", True) 467 yield t, value 468 elif ttype in Token.Comment: 469 last = 0 470 for m in re.finditer(link_reg, value): 471 pre = value[last : m.start()] 472 if pre: 473 yield from self._filter_one_literal(ttype, pre) 474 t = copy_token(ttype) 475 x1, x2 = m.groups() 476 x0 = m.group(0) 477 if x2 is None: 478 caption = x1.strip() 479 xref = x1.strip() 480 else: 481 caption = x1.strip() 482 xref = x2.strip() 483 tok_setprop(t, "xref", xref) 484 tok_setprop(t, "caption", caption) 485 if x0.endswith("_"): 486 tok_setprop(t, "trailing_underscore", True) 487 elif x2 is None: 488 # A bare single-backtick span is also how Markdown/JSDoc 489 # writes inline code. Link it when a target exists, but 490 # only diagnose explicit reStructuredText references. 491 tok_setprop(t, "optional_xref", True) 492 yield t, m.group(1) 493 last = m.end() 494 post = value[last:] 495 if post: 496 yield from self._filter_one_literal(ttype, post) 497 else: 498 yield ttype, value 499 500 501 _escape_html_table = { 502 ord("&"): "&", 503 ord("<"): "<", 504 ord(">"): ">", 505 ord('"'): """, 506 ord("'"): "'", 507 } 508 509 510 class LinkingHtmlFormatter(HtmlFormatter): 511 def __init__(self, **kwargs): 512 super(LinkingHtmlFormatter, self).__init__(**kwargs) 513 self._builder = kwargs["_builder"] 514 self._bridge = kwargs["_bridge"] 515 516 def _get_value(self, value, tok): 517 xref = tok_getprop(tok, "xref") 518 caption = tok_getprop(tok, "caption") 519 520 if tok_getprop(tok, "is_literal"): 521 return '<span style="font-weight: bolder">%s</span>' % (value,) 522 523 if tok_getprop(tok, "trailing_underscore"): 524 logger.warning( 525 "{}:{}: code block contains xref to '{}' with unsupported trailing underscore".format( 526 self._bridge.path, self._bridge.line, xref 527 ) 528 ) 529 530 if tok_getprop(tok, "is_identifier"): 531 if not xref or xref.startswith('"'): 532 return value 533 if re.match("^[0-9]+$", xref) is not None: 534 return value 535 536 if self._bridge.docname is None: 537 return value 538 if xref is None: 539 return value 540 content = caption if caption is not None else value 541 ts = self._builder.env.get_domain("ts") 542 r1 = ts.find_object("type", xref, self._bridge.docname) 543 # Qualified type references are currently one lexer token. Link 544 # ``Namespace.Member`` to the closest documented prefix if the member 545 # itself is not registered as a standalone declaration. 546 if r1 is None and tok_getprop(tok, "is_identifier") and "." in xref: 547 parts = xref.split(".") 548 for end in range(len(parts) - 1, 0, -1): 549 r1 = ts.find_object("type", ".".join(parts[:end]), self._bridge.docname) 550 if r1 is not None: 551 break 552 if r1 is not None: 553 rel_uri = ( 554 self._builder.get_relative_uri(self._bridge.docname, r1[0]) 555 + "#" 556 + r1[1] 557 ) 558 return ( 559 '<a style="color:inherit;text-decoration:underline" href="%s">%s</a>' 560 % (rel_uri, content) 561 ) 562 563 if tok_getprop(tok, "is_identifier") and tok_getprop(tok, "optional_xref"): 564 return value 565 566 std = self._builder.env.get_domain("std") 567 r2 = std.labels.get(xref.lower(), None) 568 if r2 is not None: 569 rel_uri = ( 570 self._builder.get_relative_uri(self._bridge.docname, r2[0]) 571 + "#" 572 + r2[1] 573 ) 574 return ( 575 '<a style="color:inherit;text-decoration:underline" href="%s">%s</a>' 576 % (rel_uri, content) 577 ) 578 r3 = std.anonlabels.get(xref.lower(), None) 579 if r3 is not None: 580 rel_uri = ( 581 self._builder.get_relative_uri(self._bridge.docname, r3[0]) 582 + "#" 583 + r3[1] 584 ) 585 return ( 586 '<a style="color:inherit;text-decoration:underline" href="%s">%s</a>' 587 % (rel_uri, content) 588 ) 589 590 if not tok_getprop(tok, "optional_xref"): 591 logger.warning( 592 "{}:{}: code block contains unresolved xref '{}'".format( 593 self._bridge.path, self._bridge.line, xref 594 ) 595 ) 596 597 return value 598 599 def _fmt(self, value, tok): 600 cls = self._get_css_class(tok) 601 value = self._get_value(value, tok) 602 if cls is None or cls == "": 603 return value 604 return '<span class="%s">%s</span>' % (cls, value) 605 606 def _format_lines(self, tokensource): 607 """ 608 Just format the tokens, without any wrapping tags. 609 Yield individual lines. 610 """ 611 lsep = self.lineseparator 612 escape_table = _escape_html_table 613 614 line = "" 615 for ttype, value in tokensource: 616 parts = value.translate(escape_table).split("\n") 617 618 if len(parts) == 0: 619 # empty token, usually should not happen 620 pass 621 elif len(parts) == 1: 622 # no newline before or after token 623 line += self._fmt(parts[0], ttype) 624 else: 625 line += self._fmt(parts[0], ttype) 626 yield 1, line + lsep 627 for part in parts[1:-1]: 628 yield 1, self._fmt(part, ttype) + lsep 629 line = self._fmt(parts[-1], ttype) 630 631 if line: 632 yield 1, line + lsep 633 634 635 class LinkingPygmentsBridge(PygmentsBridge): 636 def __init__(self, builder, style): 637 self.dest = "html" 638 self.latex_engine = None 639 self.formatter_args = { 640 "style": style, 641 "_builder": builder, 642 "_bridge": self, 643 } 644 self.formatter = LinkingHtmlFormatter 645 self.builder = builder 646 self.path = None 647 self.line = None 648 self.docname = None 649 650 def highlight_block( 651 self, source, lang, opts=None, force=False, location=None, **kwargs 652 ): 653 self.path = None 654 self.line = None 655 self.docname = None 656 if isinstance(location, tuple): 657 docname, line = location 658 self.line = line 659 self.path = self.builder.env.doc2path(docname) 660 self.docname = docname 661 elif isinstance(location, Element): 662 self.line = location.line 663 self.path = location.source 664 self.docname = self.builder.env.path2doc(self.path) 665 # The path/line above point at the source file the code block was 666 # written in (used for diagnostics). However, relative links must be 667 # computed against the document that is *currently being written* -- 668 # not the (possibly ``.. include``-d) source file, which may live in a 669 # deeper directory and would inject spurious "../" segments into every 670 # cross reference. Prefer the builder's current output docname. 671 current = getattr(self.builder, "current_docname", None) 672 if current: 673 self.docname = current 674 return super().highlight_block(source, lang, opts, force, location, **kwargs) 675 676 677 def install_linking_highlighters(app): 678 """Wrap the highlighters created by any standard HTML-family builder.""" 679 680 builder = app.builder 681 if builder.format != "html": 682 return 683 684 def replace(highlighter): 685 if highlighter is None: 686 return None 687 style = highlighter.formatter_args["style"] 688 return LinkingPygmentsBridge(builder, style) 689 690 builder.highlighter = replace(builder.highlighter) 691 builder.dark_highlighter = replace(getattr(builder, "dark_highlighter", None)) 692 693 694 def copy_token(tok): 695 new_tok = _TokenType(tok) 696 # This part is very fragile against API changes ... 697 new_tok.subtypes = set(tok.subtypes) 698 new_tok.parent = tok.parent 699 return new_tok 700 701 702 def tok_setprop(tok, key, value): 703 tokid = id(tok) 704 e = token_props.get(tokid) 705 if e is None: 706 e = token_props[tokid] = (tok, {}) 707 _, kv = e 708 kv[key] = value 709 710 711 def tok_getprop(tok, key): 712 tokid = id(tok) 713 e = token_props.get(tokid) 714 if e is None: 715 return None 716 _, kv = e 717 return kv.get(key) 718 719 720 link_reg = re.compile(r"(?<!`)`([^`<]+)\s*(?:<([^>]+)>)?\s*`_?") 721 literal_reg = re.compile(r"``([^`]+)``") 722 723 724 def setup(app): 725 726 class TsrefLexer(BetterTypeScriptLexer): 727 def __init__(self, **options): 728 super().__init__(**options) 729 self.add_filter(LinkFilter()) 730 731 app.add_lexer("tsref", TsrefLexer) 732 app.add_domain(TypeScriptDomain) 733 app.connect("builder-inited", install_linking_highlighters) 734 return { 735 "parallel_read_safe": True, 736 "parallel_write_safe": True, 737 }