From fde732fcc6e82d84689ce40431fa5aa25b001625 Mon Sep 17 00:00:00 2001 From: Adam Langley Date: Sun, 6 Dec 2020 12:17:47 -0800 Subject: [PATCH] Support linking to steps. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Specifications often have algorithms with many (nested) ordered lists. In those algorithms, steps often refer to other steps by number. But those numbers currently have to be maintained manually and, when steps are inserted, that can lead to a cascade of updated step number references below. Worse, when two changes are in-flight to the same algorithm, the step numbers are almost ensured to go wrong because neither change need textually conflict with the other. This change to bikeshed permits items in an ordered list to be given an ID and referenced by name. 1. This is a traditional item. 1. {#myid} This is an item with an ID. 1. This items references [[#myid]]. The reference is turned into the string “step 2”. This also works for nested steps where the reference will say something like “step 2.1.3”. --- bikeshed/markdown/markdown.py | 39 +-- bikeshed/unsortedJunk.py | 8 +- tests/step-links001.bs | 19 ++ tests/step-links001.html | 434 ++++++++++++++++++++++++++++++++++ 4 files changed, 484 insertions(+), 16 deletions(-) create mode 100644 tests/step-links001.bs create mode 100644 tests/step-links001.html diff --git a/bikeshed/markdown/markdown.py b/bikeshed/markdown/markdown.py index 186784c6df..779c487373 100644 --- a/bikeshed/markdown/markdown.py +++ b/bikeshed/markdown/markdown.py @@ -8,14 +8,14 @@ from ..messages import * -def parse(lines, numSpacesForIndentation, features=None, opaqueElements=None, blockElements=None): +def parse(lines, numSpacesForIndentation, features=None, opaqueElements=None, blockElements=None, itemNumContext=None): fromStrings = False if any(isinstance(l, str) for l in lines): fromStrings = True lines = [Line.Line(-1, l) for l in lines] lines = Line.rectify(lines) tokens = tokenizeLines(lines, numSpacesForIndentation, features, opaqueElements=opaqueElements, blockElements=blockElements) - html = parseTokens(tokens, numSpacesForIndentation) + html = parseTokens(tokens, numSpacesForIndentation, itemNumContext) if fromStrings: return [l.text for l in html] else: @@ -144,10 +144,10 @@ def inlineElementStart(line): elif re.match(r"((\*\s*){3,})$|((-\s*){3,})$|((_\s*){3,})$", line): token = {'type':'rule'} elif re.match(r"-?\d+\.\s", line): - match = re.match(r"(-?\d+)\.\s+(.*)", line) - token = {'type':'numbered', 'text': match.group(2), 'num': int(match.group(1))} + match = re.match(r"(-?\d+)\.\s+(\{#([^ }]+)\})?(\s+)?(.*)", line) + token = {'type':'numbered', 'text': match.group(5), 'num': int(match.group(1)), 'id': match.group(3)} elif re.match(r"-?\d+\.$", line): - token = {'type':'numbered', 'text': "", 'num': int(line[:-1])} + token = {'type':'numbered', 'text': "", 'num': int(line[:-1]), 'id': None} elif re.match(r"[*+-]\s", line): match = re.match(r"[*+-]\s+(.*)", line) token = {'type':'bulleted', 'text': match.group(1)} @@ -275,7 +275,7 @@ def stripPrefix(token, numSpacesForIndentation, len): return text[offset:] -def parseTokens(tokens, numSpacesForIndentation): +def parseTokens(tokens, numSpacesForIndentation, itemNumContext): ''' Token types: eof @@ -313,7 +313,7 @@ def parseTokens(tokens, numSpacesForIndentation): elif stream.currtype() == 'bulleted': lines += parseBulleted(stream) elif stream.currtype() == 'numbered': - lines += parseNumbered(stream, start=stream.currnum()) + lines += parseNumbered(stream, start=stream.currnum(), itemNumContext=itemNumContext) elif stream.currtype() in ("dt", "dd"): lines += parseDl(stream) elif stream.currtype() == "blockquote": @@ -456,7 +456,7 @@ def getItems(stream): return lines -def parseNumbered(stream, start=1): +def parseNumbered(stream, itemNumContext, start=1): prefixLen = stream.currprefixlen() ol_i = stream.currline().i numSpacesForIndentation = stream.numSpacesForIndentation @@ -466,18 +466,19 @@ def parseItem(stream): # Remove the numbered part from the line firstLine = stream.currtext() + "\n" i = stream.currline().i + id = stream.currid() lines = [lineFromStream(stream, firstLine)] while True: stream.advance() # All the conditions that indicate we're *past* the end of the item. if stream.currtype() == 'numbered' and stream.currprefixlen() == prefixLen: - return lines,i + return lines,i,id if stream.currprefixlen() < prefixLen: - return lines,i + return lines,i,id if stream.currtype() == 'blank' and stream.nexttype() != 'numbered' and stream.nextprefixlen() <= prefixLen: - return lines,i + return lines,i,id if stream.currtype() == 'eof': - return lines,i + return lines,i,id # Remove the prefix from each line before adding it. lines.append(lineFromStream(stream, stripPrefix(stream.curr(), numSpacesForIndentation, prefixLen + 1))) @@ -498,10 +499,18 @@ def getItems(stream): lines = [Line.Line(-1, "
    ".format(ol_i))] else: lines = [Line.Line(-1, "
      ".format(start, ol_i))] - for li_lines,i in getItems(stream): - lines.append(Line.Line(-1, "
    1. ".format(i))) - lines.extend(parse(li_lines, numSpacesForIndentation)) + for li_lines,i,id in getItems(stream): + itemNumInContext = None + if id is not None: + itemNumInContext = str(start) + if itemNumContext is not None: + itemNumInContext = itemNumContext + '.' + itemNumInContext + lines.append(Line.Line(-1, "
    2. ".format(i, escapeAttr(id), escapeAttr(itemNumInContext)))) + else: + lines.append(Line.Line(-1, "
    3. ".format(i))) + lines.extend(parse(li_lines, numSpacesForIndentation, itemNumContext=itemNumInContext)) lines.append(Line.Line(-1, "
    4. ")) + start += 1 lines.append(Line.Line(-1, "
    ")) return lines diff --git a/bikeshed/unsortedJunk.py b/bikeshed/unsortedJunk.py index fc8ddc5d48..19dfec44b6 100644 --- a/bikeshed/unsortedJunk.py +++ b/bikeshed/unsortedJunk.py @@ -369,9 +369,15 @@ def addVarClickHighlighting(doc): def fixIntraDocumentReferences(doc): ids = {el.get('id'):el for el in findAll("[id]", doc)} headingIDs = {el.get('id'):el for el in findAll("[id].heading", doc)} + stepIDs = {el.get('id'):el for el in findAll("li[id]", doc)} for el in findAll("a[href^='#']:not([href='#']):not(.self-link):not([data-link-type])", doc): targetID = urllib.parse.unquote(el.get("href")[1:]) - if el.get('data-section') is not None and targetID not in headingIDs: + if targetID in stepIDs and stepIDs[targetID].get('item') is not None: + li = stepIDs[targetID] + text = "step " + li.get('item') + appendChild(el, text) + continue + elif el.get('data-section') is not None and targetID not in headingIDs: die("Couldn't find target document section {0}:\n{1}", targetID, outerHTML(el), el=el) continue elif targetID not in ids: diff --git a/tests/step-links001.bs b/tests/step-links001.bs new file mode 100644 index 0000000000..8abd24b4a6 --- /dev/null +++ b/tests/step-links001.bs @@ -0,0 +1,19 @@ + + +1. An Item. +1. {#foostep} An item with an id. + 1. A subitem. + 1. {#foostepbar} A subitem with an id. +1. Another item. +1. Reference [[#foostepbar]]. +1. Reference top-level [[#foostep]]. diff --git a/tests/step-links001.html b/tests/step-links001.html new file mode 100644 index 0000000000..76e6254741 --- /dev/null +++ b/tests/step-links001.html @@ -0,0 +1,434 @@ + + + + + Step-reference test + + + + + + + + +
    +

    +

    Step-reference test

    +

    Living Standard,

    +
    +
    +
    This version: +
    https://example.com/stepref +
    Editor: +
    Bikeshed authors +
    +
    +
    + +
    +
    +
    +

    Abstract

    +

    Testing step references

    +
    +
    + +
    +
      +
    1. +

      An Item.

      +
    2. +

      An item with an id.

      +
        +
      1. +

        A subitem.

        +
      2. +

        A subitem with an id.

        +
      +
    3. +

      Another item.

      +
    4. +

      Reference step 2.2.

      +
    5. +

      Reference top-level step 2.

      +
    +
    \ No newline at end of file