-
Notifications
You must be signed in to change notification settings - Fork 201
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This makes Bikeshed process CDDL blocks à la `<pre class=cddl>` as described in #2072 to: - add highlighting (done by Pygments, the code merely sets the right class) - wrap terms in `<dfn>` and `<a>` automatically, making it possible to define them in, or reference them from, the rest of the prose. Most of the logic is copied from the logic used to process IDL blocks. As opposed to IDL definitions, CDDL definitions are not exported by default as most CDDL definitions only apply to the underlying spec. Support for CDDL definitions means 4 new definition types get introduced: - `cddl-type`: roughly the equivalent of an IDL interface. Type definitions do not have a `data-dfn-for` attribute. - `cddl-key`: roughly the equivalent of an IDL attribute. Key definitions always have a `data-dfn-for` attribute that links back to a CDDL type. - `cddl-value`: roughly the equivalent of an enum-value in IDL. Value definitions always have a `data-dfn-for` attribute that links back to a CDDL type or to a CDDL key. - `cddl-parameter`: used for generic parameter names (noting that no known spec uses CDDL generics for the time being). The definition types are prefixed with `cddl-` to avoid collision with other types used for other purpose (e.g., `value` in CSS). The code also collects CDDL definitions to produce a CDDL index at the end of the spec. To accommodate specs like WebDriver BiDi that define two sets of CDDL (remote end and local end), a mechanism gets added to associate CDDL definitions with a given module: 1. The CDDL module must be defined with a `<dfn>` with a `data-cddl-module` attribute set to a shortname for the CDDL module 2. CDDL blocks must add a `data-cddl-module` attribute set to a comma-separated list of CDDL module shortnames they belong to. If a CDDL block does not have that attribute, the code considers it is defined in all CDDL modules. The index is split per module, using the `<dfn>` text as title for each module. Note: Even when modules are used, CDDL definitions in a spec are part of the same namespace, meaning a `foo` rule cannot be defined differently within a single spec for two different CDDL modules. CDDL parsing is done through a hand-made CDDL parser that follows the CDDL grammar defined in RFC 8610, currently sitting under: https://github.com/tidoust/cddlparser To ease authoring, a new shorthand notation gets introduced to reference CDDL: `{^foo^}` is an autolink to a CDDL definition. FWIW, the shorthand was chosen to mean "shortcut to CDDL code" on the grounds that: - `{}` indicates a code block (for IDL) - `^` means "cut" in CDDL CDDL type definitions can become somewhat convoluted, the code does not attempt to be too smart and won't autolink definitions that are too complex.
- Loading branch information
Showing
21 changed files
with
6,184 additions
and
143 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,197 @@ | ||
from __future__ import annotations | ||
|
||
import os | ||
import sys | ||
from typing import get_args | ||
|
||
from . import config, h, t | ||
from . import messages as m | ||
|
||
# TODO: Drop once cddlparser gets published as a Pypi package | ||
basepath = os.path.dirname(os.path.realpath(__file__)) | ||
cddlpath = os.path.abspath(os.path.join(basepath, '..', '..')) | ||
sys.path.append(cddlpath) | ||
import cddlparser | ||
|
||
class CDDLMarker(cddlparser.ast.Marker): | ||
''' | ||
Marker that wraps CDDL definitions and references in <cddl> and <a> blocks | ||
so that cross-referencing logic may take place. | ||
''' | ||
def serializeValue(self, prefix: str, value: str, suffix: str, node: cddlparser.ast.CDDLNode) -> str: | ||
name = prefix + value + suffix | ||
if node.type != 'text' and node.type != 'bytes': | ||
return name | ||
parent = node.parentNode | ||
if isinstance(parent, cddlparser.ast.Memberkey) and node.type == 'text': | ||
# A literal text string also gives rise to a type | ||
# see RFC 8610, section 3.5.1: | ||
# https://datatracker.ietf.org/doc/html/rfc8610#section-3.5.1 | ||
forName = self._getFor(parent.parentNode) | ||
if forName is None: | ||
# Cannot easily link member key back to a definition | ||
return name | ||
else: | ||
# Name of created type should not include the quotes | ||
return '<cddl data-cddl-type="key" data-cddl-for="{}" data-lt="{}">{}</cddl>'.format(h.escapeAttr(forName), h.escapeAttr(value), name) | ||
elif isinstance(parent, cddlparser.ast.Operator) and parent.controller == node: | ||
# Probably a ".default" value. It may be possible to link the value | ||
# back to an enumeration but it's equally possible that this is just | ||
# a string that's not defined anywhere. Let's ignore. | ||
return name | ||
else: | ||
forName = self._getFor(node) | ||
if forName is None: | ||
return name | ||
else: | ||
return '<cddl data-cddl-type="value" data-cddl-for="{}" data-lt="{}">{}</cddl>'.format(h.escapeAttr(forName), h.escapeAttr(name), name) | ||
|
||
def serializeName(self, name: str, node: cddlparser.ast.CDDLNode) -> str: | ||
# The node is a Typename. Such a node may appear in a Rule, a Type, | ||
# a Reference, a Memberkey, a GroupEntry, or GenericParameters | ||
parent = node.parentNode | ||
if isinstance(parent, cddlparser.ast.Rule): | ||
# Rule definition | ||
if parent.assign.type == cddlparser.Tokens.TCHOICEALT or parent.assign.type == cddlparser.Tokens.GCHOICEALT: | ||
# The definition extends a base definition | ||
return '<a data-link-type="cddl" data-link-for="/">{}</a>'.format(name) | ||
else: | ||
return '<cddl data-cddl-type="type" data-lt="{}">{}</cddl>'.format(h.escapeAttr(name), name) | ||
elif isinstance(parent, cddlparser.ast.Memberkey): | ||
# Member definition | ||
if not parent.hasColon: | ||
# The key is actually a reference to a type | ||
if name in get_args(cddlparser.ast.PreludeType): | ||
# From the CDDL prelude, nothing to link to | ||
return name | ||
else: | ||
return '<a data-link-type="cddl" data-link-for="/">{}</a>'.format(name) | ||
forName = self._getFor(parent.parentNode) | ||
if forName is None: | ||
# Cannot easily link member key back to a definition | ||
return name | ||
else: | ||
return '<cddl data-cddl-type="key" data-cddl-for="{}" data-lt="{}">{}</cddl>'.format(h.escapeAttr(forName), h.escapeAttr(name), name) | ||
elif isinstance(parent, cddlparser.ast.GenericParameters): | ||
typename = parent.parentNode | ||
assert isinstance(typename, cddlparser.ast.Typename) | ||
return '<cddl data-cddl-type="parameter" data-cddl-for="{}" data-lt="{}">{}</cddl>'.format(h.escapeAttr(typename.name), h.escapeAttr(name), name) | ||
elif name in get_args(cddlparser.ast.PreludeType): | ||
# Do not link types that come from the CDDL prelude | ||
# defined in RFC 8610 | ||
return name | ||
else: | ||
return '<a data-link-type="cddl" data-link-for="/">{}</a>'.format(name) | ||
|
||
def _getFor(self, node: cddlparser.ast.CDDLNode) -> str | None: | ||
''' | ||
Retrieve the "for" attribute for the node. | ||
''' | ||
parent = node.parentNode | ||
while parent is not None: | ||
if isinstance(parent, cddlparser.ast.Rule): | ||
# Something defined in a rule | ||
return parent.name.name | ||
elif isinstance(parent, cddlparser.ast.GroupEntry) and parent.key is not None: | ||
# A type in a member key definition | ||
parentFor = self._getFor(parent.parentNode) | ||
if parentFor is None: | ||
return parentFor | ||
if isinstance(parent.key.type, cddlparser.ast.Value) and parent.key.type.type == "text": | ||
return parentFor + "/" + parent.key.type.value | ||
elif isinstance(parent.key.type, cddlparser.ast.Typename): | ||
return parentFor + "/" + parent.key.type.name | ||
else: | ||
return None | ||
parent = parent.parentNode | ||
return None | ||
|
||
def markupCDDL(doc: t.SpecT) -> None: | ||
cddlEls = h.findAll("pre.cddl:not([data-no-cddl]), xmp.cddl:not([data-no-cddl])", doc) | ||
|
||
marker = CDDLMarker() | ||
for el in cddlEls: | ||
if h.isNormative(doc, el): | ||
text = h.textContent(el) | ||
try: | ||
ast = cddlparser.parse(text) | ||
h.replaceContents(el, h.parseHTML(ast.serialize(marker))) | ||
except Exception as err: | ||
m.die(f"{err}\nInvalid CDDL block (first 100 characters):\n{text[0:100]}{'...' if len(text) > 100 else ''}") | ||
h.addClass(doc, el, "highlight") | ||
doc.extraJC.addCDDLHighlighting() | ||
|
||
def markupCDDLBlock(pre: t.ElementT, doc: t.SpecT) -> set[t.ElementT]: | ||
''' | ||
Convert <cddl> blocks into "dfn" or links. | ||
''' | ||
localDfns = set() | ||
forcedDfns = [] | ||
for x in (h.treeAttr(pre, "data-dfn-force") or "").split(): | ||
x = x.strip() | ||
if x.endswith("<interface>"): | ||
x = x[:-11] | ||
forcedDfns.append(x) | ||
for el in h.findAll("cddl", pre): | ||
# Prefix CDDL types with "cddl-" to avoid confusion with other | ||
# types (notably CSS ones such as "value") | ||
cddlType = "cddl-" + el.get("data-cddl-type") | ||
assert isinstance(cddlType, str) | ||
url = None | ||
forceDfn = False | ||
ref = None | ||
cddlText = None | ||
for cddlText in (el.get("data-lt") or "").split("|"): | ||
if cddlType == "interface" and cddlText in forcedDfns: | ||
forceDfn = True | ||
linkFors = t.cast("list[str|None]", config.splitForValues(el.get("data-cddl-for", ""))) or [None] | ||
for linkFor in linkFors: | ||
ref = doc.refs.getRef( | ||
cddlType, | ||
cddlText, | ||
linkFor=linkFor, | ||
status="local", | ||
el=el, | ||
error=False, | ||
) | ||
if ref: | ||
url = ref.url | ||
break | ||
if ref: | ||
break | ||
if url is None or forceDfn: | ||
el.tag = "dfn" | ||
el.set("data-dfn-type", cddlType) | ||
del el.attrib["data-cddl-type"] | ||
if el.get("data-cddl-for"): | ||
el.set("data-dfn-for", el.get("data-cddl-for") or "") | ||
del el.attrib["data-cddl-for"] | ||
else: | ||
# Copy over the auto-generated linking text to the manual dfn. | ||
dfn = h.find(url, doc) | ||
# How in the hell does this work, the url is not a selector??? | ||
assert dfn is not None | ||
lts = combineCDDLLinkingTexts(el.get("data-lt"), dfn.get("data-lt")) | ||
dfn.set("data-lt", lts) | ||
localDfns.add(dfn) | ||
|
||
# Reset the <cddl> element to be a link to the manual dfn. | ||
el.tag = "a" | ||
el.set("data-link-type", cddlType) | ||
el.set("data-lt", cddlText) | ||
del el.attrib["data-cddl-type"] | ||
if el.get("data-cddl-for"): | ||
el.set("data-link-for", el.get("data-cddl-for") or "") | ||
del el.attrib["data-cddl-for"] | ||
if el.get("id"): | ||
# ID was defensively added by the Marker. | ||
del el.attrib["id"] | ||
return localDfns | ||
|
||
def combineCDDLLinkingTexts(t1: str | None, t2: str | None) -> str: | ||
t1s = (t1 or "").split("|") | ||
t2s = (t2 or "").split("|") | ||
for lt in t2s: | ||
if lt not in t1s: | ||
t1s.append(lt) | ||
return "|".join(t1s) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.