diff --git a/bikeshed/InputSource.py b/bikeshed/InputSource.py
index 29b67941aa..346a7fe99c 100644
--- a/bikeshed/InputSource.py
+++ b/bikeshed/InputSource.py
@@ -28,19 +28,27 @@ def lines(self) -> list[line.Line]:
offset = 0
for i, text in enumerate(self.rawLines, 1):
lineNo = i + offset
- # The early HTML parser runs before Markdown,
- # and in some cases removes linebreaks that were present
- # in the source. When properly invoked, it inserts
- # a special PUA char for each of these omitted linebreaks,
- # so I can remove them here and properly increment the
- # line number.
+ # The early HTML parser can change how nodes print,
+ # so they occupy a different number of lines than they
+ # had in the source. Markdown parser needs to know
+ # the correct source lines, tho, so when this happens,
+ # the nodes will insert special PUA chars to indicate that.
+ # I can remove them here and properly adjust the line number.
# Current known causes of this:
# * line-ending -- turned into em dashes
# * multi-line start tags
+ # * multi-line markdown code spans;
+ # - the text loses its newlines
+ # - the original text goes into an attribute on the start
+ # tag now
ilcc = constants.incrementLineCountChar
+ dlcc = constants.decrementLineCountChar
if ilcc in text:
offset += text.count(ilcc)
text = text.replace(ilcc, "")
+ if dlcc in text:
+ offset -= text.count(dlcc)
+ text = text.replace(dlcc, "")
ret.append(line.Line(lineNo, text))
diff --git a/bikeshed/constants.py b/bikeshed/constants.py
index 08e5a37f73..98fae99b96 100644
--- a/bikeshed/constants.py
+++ b/bikeshed/constants.py
@@ -11,4 +11,5 @@
macroStartChar = "\uebbb"
macroEndChar = "\uebbc"
incrementLineCountChar = "\uebbd"
+decrementLineCountChar = "\uebbf"
bsComment = ""
diff --git a/bikeshed/h/parser.py b/bikeshed/h/parser.py
index 7eac37a53d..2550be69cb 100644
--- a/bikeshed/h/parser.py
+++ b/bikeshed/h/parser.py
@@ -40,64 +40,83 @@ def fromSpec(doc: t.SpecT) -> ParseConfig:
def nodesFromHtml(data: str, config: ParseConfig, startLine: int = 1) -> t.Generator[ParserNode, None, None]:
i = 0
- s = Stream(data, startLine=startLine)
- dt, i = parseDoctype(s, i)
- if dt is not Failure:
+ s = Stream(data, startLine=startLine, config=config)
+ dt, i = parseDoctype(s, i).t2
+ if dt is not None:
yield dt
text = ""
textI = 0
- node: ParserNode | None = None
+ node: ParserNode | list[ParserNode] | None
+ lastNode: ParserNode | None = None
while not s.eof(i):
- node, i = parseNode(s, i, config, lastNode=node)
- if node is Failure:
+ node, i = parseNode(s, i).t2
+ if node is None:
text += s[i]
i += 1
else:
if text:
startLine = s.line(textI)
endLine = startLine + len(text.split("\n")) - 1
- yield Text(startLine, endLine, text)
+ yield Text(startLine, endLine, text).curlifyApostrophes(lastNode)
text = ""
- yield t.cast("ParserNode", node)
+ if isinstance(node, list):
+ for n in node:
+ if isinstance(n, Text):
+ yield n.curlifyApostrophes(lastNode)
+ lastNode = None
+ else:
+ yield n
+ lastNode = n
+ else:
+ yield node
+ lastNode = node
textI = i
if text:
startLine = s.line(textI)
endLine = startLine + len(text.split("\n")) - 1
- yield Text(startLine, endLine, text)
+ yield Text(startLine, endLine, text).curlifyApostrophes(lastNode)
def parseNode(
s: Stream,
start: int,
- config: ParseConfig,
- lastNode: ParserNode | None = None,
-) -> Result:
+) -> Result[ParserNode | list[ParserNode]]:
+ """
+ Parses one Node from the start of the stream.
+ Might return multiple nodes, as a list.
+ Failure means the list doesn't start with anything special
+ (it'll just be text),
+ but Text *can* be validly returned sometimes as the Node.
+ """
if s.eof(start):
return Result.fail(start)
+ node: ParserNode | list[ParserNode] | None
+
if s[start] == "&":
- ch, i = parseCharRef(s, start)
- if ch is not Failure:
+ ch, i = parseCharRef(s, start).t2
+ if ch is not None:
node = Text(text=f"{ord(ch)};", line=s.line(start), endLine=s.line(i - 1))
return Result(node, i)
if s[start] == "<":
- node, i = parseAngleStart(s, start, config)
- if node is not Failure:
+ node, i = parseAngleStart(s, start).t2
+ if node is not None:
return Result(node, i)
# This isn't quite correct to handle here,
# but it'll have to wait until I munge
# the markdown and HTML parsers together.
+ el: ParserNode | None
if s[start] in ("`", "~"):
- el, i = parseFencedCodeBlock(s, start)
- if el is not Failure:
+ el, i = parseFencedCodeBlock(s, start).t2
+ if el is not None:
return Result(el, i)
- if config.markdown:
+ if s.config.markdown:
if s[start] == "`":
- el, i = parseCodeSpan(s, start)
- if el is not Failure:
- return Result(el, i)
+ els, i = parseCodeSpan(s, start).t2
+ if els is not None:
+ return Result(els, i)
if s[start : start + 2] == "\\`":
node = Text(
line=s.line(start),
@@ -105,14 +124,14 @@ def parseNode(
text="`",
)
return Result(node, start + 2)
- if config.css:
+ if s.config.css:
if s[start] == "'":
- el, i = parseCSSMaybe(s, start)
- if el is not Failure:
+ el, i = parseCSSMaybe(s, start).t2
+ if el is not None:
return Result(el, i)
if s[start] == "[" and s[start - 1] != "[":
- el, i = parseMacro(s, start)
- if el is not Failure:
+ el, i = parseMacro(s, start).t2
+ if el is not None:
return Result(el, i)
if s[start : start + 2] == "\\[":
if s[start + 2].isalpha() or s[start + 2].isdigit():
@@ -130,25 +149,8 @@ def parseNode(
text=text,
)
return Result(node, start + 2)
- match, i = s.matchRe(start, curlyApostropheRe)
- if match is not Failure:
- node = Text(
- line=s.line(start),
- endLine=s.line(i - 1),
- text=s[start] + "’" + s[start + 2],
- )
- return Result(node, i)
- if isinstance(lastNode, (EndTag, RawElement, WholeElement)):
- match, i = s.matchRe(start, curlyAposAfterElement)
- if match is not Failure:
- node = Text(
- line=s.line(start),
- endLine=s.line(i - 1),
- text="’" + s[start + 1],
- )
- return Result(node, i)
- match, i = s.matchRe(start, emdashRe)
- if match is not Failure:
+ match, i = s.matchRe(start, emdashRe).t2
+ if match is not None:
# Fix line-ending em dashes, or --, by moving the previous line up, so no space.
node = Text(
line=s.line(start),
@@ -160,27 +162,27 @@ def parseNode(
return Result.fail(start)
-curlyApostropheRe = re.compile(r"\w'\w")
-curlyAposAfterElement = re.compile(r"'\w")
emdashRe = re.compile(r"(?:(? Result:
+def parseAngleStart(s: Stream, start: int) -> Result[ParserNode | list[ParserNode]]:
# Assuming the stream starts with an <
i = start + 1
- comment, i = parseComment(s, start)
- if comment is not Failure:
+ comment, i = parseComment(s, start).t2
+ if comment is not None:
return Result(comment, i)
- startTag, i = parseStartTag(s, start)
- if startTag is not Failure:
+ startTag, i = parseStartTag(s, start).t2
+ if startTag is not None:
+ if isinstance(startTag, SelfClosedTag):
+ return Result(startTag, i)
if startTag.tag == "pre":
- el, endI = parseMetadataBlock(s, start)
- if el is not Failure:
+ el, endI = parseMetadataBlock(s, start).t2
+ if el is not None:
return Result(el, endI)
if isDatablockPre(startTag):
- text, i = parseRawPreToEnd(s, i)
- if text is Failure:
+ text, i = parseRawPreToEnd(s, i).t2
+ if text is None:
return Result.fail(start)
el = RawElement(
line=startTag.line,
@@ -191,8 +193,8 @@ def parseAngleStart(s: Stream, start: int, config: ParseConfig) -> Result:
)
return Result(el, i)
if startTag.tag == "script":
- text, i = parseScriptToEnd(s, i)
- if text is Failure:
+ text, i = parseScriptToEnd(s, i).t2
+ if text is None:
return Result.fail(start)
el = RawElement(
line=startTag.line,
@@ -203,8 +205,8 @@ def parseAngleStart(s: Stream, start: int, config: ParseConfig) -> Result:
)
return Result(el, i)
elif startTag.tag == "style":
- text, i = parseStyleToEnd(s, i)
- if text is Failure:
+ text, i = parseStyleToEnd(s, i).t2
+ if text is None:
return Result.fail(start)
el = RawElement(
line=startTag.line,
@@ -215,8 +217,8 @@ def parseAngleStart(s: Stream, start: int, config: ParseConfig) -> Result:
)
return Result(el, i)
elif startTag.tag == "xmp":
- text, i = parseXmpToEnd(s, i)
- if text is Failure:
+ text, i = parseXmpToEnd(s, i).t2
+ if text is None:
return Result.fail(start)
el = RawElement(
line=startTag.line,
@@ -229,14 +231,14 @@ def parseAngleStart(s: Stream, start: int, config: ParseConfig) -> Result:
else:
return Result(startTag, i)
- endTag, i = parseEndTag(s, start)
- if endTag is not Failure:
+ endTag, i = parseEndTag(s, start).t2
+ if endTag is not None:
return Result(endTag, i)
- if config.css:
- el, i = parseCSSProduction(s, start)
- if el is not Failure:
- return Result(el, i)
+ if s.config.css:
+ els, i = parseCSSProduction(s, start).t2
+ if els is not None:
+ return Result(els, i)
return Result.fail(start)
@@ -275,6 +277,7 @@ def initialDocumentParse(text: str, config: ParseConfig, startLine: int = 1) ->
def strFromNodes(nodes: t.Iterable[ParserNode], withIlcc: bool = False) -> str:
strs = []
ilcc = constants.incrementLineCountChar
+ dlcc = constants.decrementLineCountChar
for node in nodes:
if isinstance(node, Comment):
# Serialize comments as a standardized, recognizable sequence
@@ -285,10 +288,13 @@ def strFromNodes(nodes: t.Iterable[ParserNode], withIlcc: bool = False) -> str:
continue
s = str(node)
if withIlcc:
- numLines = s.count("\n")
- diffLineNo = node.endLine - node.line
- if diffLineNo > numLines:
- s += ilcc * (diffLineNo - numLines)
+ outputExtraLines = s.count("\n")
+ sourceExtraLines = node.endLine - node.line
+ diff = sourceExtraLines - outputExtraLines
+ if diff > 0:
+ s += ilcc * diff
+ elif diff < 0:
+ s += dlcc * -diff
strs.append(s)
return "".join(strs)
@@ -381,9 +387,12 @@ def __str__(self) -> str:
return f"{self.s.loc(self.index)} {self.details}"
+ResultT = t.TypeVar("ResultT")
+
+
@dataclass
-class Result:
- value: t.Any
+class Result(t.Generic[ResultT]):
+ value: ResultT | None
end: int
err: Failure | None = None
@@ -392,26 +401,28 @@ def valid(self) -> bool:
return self.err is None
@staticmethod
- def fail(index: int) -> Result:
+ def fail(index: int) -> Result[ResultT]:
return Result(None, index, Failure())
@staticmethod
- def parseerror(s: Stream, index: int, details: str) -> Result:
+ def parseerror(s: Stream, index: int, details: str) -> Result[ResultT]:
return Result(None, index, ParseFailure(details, s, index))
- def __iter__(self) -> t.Iterator[t.Any]:
+ @property
+ def t2(self) -> tuple[ResultT | None, int]:
# By default, mask the error for simpler detection
- # via `is Failure`
+ # via `is None`
# (treating the class itself as a unique marker value)
if self.err:
- value = Failure
+ value = None
else:
value = self.value
- return iter((value, self.end))
+ return (value, self.end)
- def showErr(self) -> tuple[t.Any, int, Failure | None]:
+ @property
+ def t3(self) -> tuple[ResultT | None, int, Failure | None]:
if self.err:
- value = Failure
+ value = None
else:
value = self.value
return (value, self.end, self.err)
@@ -419,13 +430,17 @@ def showErr(self) -> tuple[t.Any, int, Failure | None]:
class Stream:
_chars: str
+ _len: int
_lineBreaks: list[int]
startLine: int
+ config: ParseConfig
- def __init__(self, chars: str, startLine: int = 1) -> None:
+ def __init__(self, chars: str, config: ParseConfig, startLine: int = 1) -> None:
self._chars = chars
+ self._len = len(chars)
self._lineBreaks = []
self.startLine = startLine
+ self.config = config
for i, char in enumerate(chars):
if char == "\n":
self._lineBreaks.append(i)
@@ -437,7 +452,7 @@ def __getitem__(self, key: int | slice) -> str:
return ""
def eof(self, index: int) -> bool:
- return index >= len(self._chars)
+ return index >= self._len
def line(self, index: int) -> int:
# Zero-based line index
@@ -454,7 +469,7 @@ def col(self, index: int) -> int:
def loc(self, index: int) -> str:
return f"{self.line(index)}:{self.col(index)}"
- def skipTo(self, start: int, text: str) -> Result:
+ def skipTo(self, start: int, text: str) -> Result[str]:
# Skip forward until encountering `text`.
# Produces the text encountered before this point.
i = start
@@ -468,21 +483,21 @@ def skipTo(self, start: int, text: str) -> Result:
else:
return Result.fail(start)
- def matchRe(self, start: int, pattern: re.Pattern) -> Result:
+ def matchRe(self, start: int, pattern: re.Pattern) -> Result[re.Match]:
match = pattern.match(self._chars, start)
if match:
return Result(match, match.end())
else:
return Result.fail(start)
- def searchRe(self, start: int, pattern: re.Pattern) -> Result:
+ def searchRe(self, start: int, pattern: re.Pattern) -> Result[re.Match]:
match = pattern.search(self._chars, start)
if match:
return Result(match, match.end())
else:
return Result.fail(start)
- def skipToNextLine(self, start: int) -> Result:
+ def skipToNextLine(self, start: int) -> Result[str]:
# Skips to the next line.
# Produces the leftover text on the current line.
textAfter = self.remainingTextOnLine(start)
@@ -539,6 +554,17 @@ class Text(ParserNode):
def __str__(self) -> str:
return self.text
+ def curlifyApostrophes(self, lastNode: ParserNode | None) -> Text:
+ if (
+ self.text[0] == "'"
+ and isinstance(lastNode, (EndTag, RawElement, SelfClosedTag))
+ and re.match(r"'\w", self.text)
+ ):
+ self.text = "’" + self.text[1:]
+ if "'" in self.text:
+ self.text = re.sub(r"(\w)'(\w)", r"\1’\2", self.text)
+ return self
+
@dataclass
class Doctype(ParserNode):
@@ -578,6 +604,43 @@ def clone(self, **kwargs: t.Any) -> StartTag:
return dataclasses.replace(self, **kwargs)
+@dataclass
+class SelfClosedTag(ParserNode):
+ tag: str
+ attrs: dict[str, str] = field(default_factory=dict)
+ classes: set[str] = field(default_factory=set)
+
+ def __str__(self) -> str:
+ s = f"<{self.tag} bs-line-number={self.line}"
+ for k, v in sorted(self.attrs.items()):
+ if k == "bs-line-number":
+ continue
+ s += f' {k}="{escapeAttr(v)}"'
+ if self.classes:
+ s += f' class="{" ".join(sorted(self.classes))}"'
+ s += f">{self.tag}>"
+ return s
+
+ def finalize(self) -> SelfClosedTag:
+ if "class" in self.attrs:
+ self.classes = set(self.attrs["class"].split())
+ del self.attrs["class"]
+ return self
+
+ def clone(self, **kwargs: t.Any) -> SelfClosedTag:
+ return dataclasses.replace(self, **kwargs)
+
+ @classmethod
+ def fromStartTag(cls: t.Type[SelfClosedTag], tag: StartTag) -> SelfClosedTag:
+ return cls(
+ line=tag.line,
+ endLine=tag.endLine,
+ tag=tag.tag,
+ attrs=tag.attrs,
+ classes=tag.classes,
+ )
+
+
@dataclass
class EndTag(ParserNode):
tag: str
@@ -594,6 +657,10 @@ def __str__(self) -> str:
return f""
+# RawElement is for things like tag,
# with text contents in the Result.
@@ -999,7 +1056,7 @@ def parseScriptToEnd(s: Stream, start: int) -> Result:
assert False
-def parseStyleToEnd(s: Stream, start: int) -> Result:
+def parseStyleToEnd(s: Stream, start: int) -> Result[str]:
# Identical to parseScriptToEnd
i = start
@@ -1015,7 +1072,7 @@ def parseStyleToEnd(s: Stream, start: int) -> Result:
assert False
-def parseXmpToEnd(s: Stream, start: int) -> Result:
+def parseXmpToEnd(s: Stream, start: int) -> Result[str]:
# Identical to parseScriptToEnd
i = start
@@ -1031,7 +1088,7 @@ def parseXmpToEnd(s: Stream, start: int) -> Result:
assert False
-def parseRawPreToEnd(s: Stream, start: int) -> Result:
+def parseRawPreToEnd(s: Stream, start: int) -> Result[str]:
# Identical to parseScriptToEnd
i = start
@@ -1047,35 +1104,49 @@ def parseRawPreToEnd(s: Stream, start: int) -> Result:
assert False
-def parseCSSProduction(s: Stream, start: int) -> Result:
+def parseCSSProduction(s: Stream, start: int) -> Result[list[ParserNode]]:
if s[start : start + 2] != "<<":
return Result.fail(start)
- i = start + 2
+ textStart = start + 2
- text, i = s.skipTo(i, ">>")
- if text is Failure:
+ text, textEnd = s.skipTo(textStart, ">>").t2
+ if text is None:
+ m.die("Saw the start of a CSS production (like <>), but couldn't find the end.", lineNum=s.loc(start))
return Result.fail(start)
if "\n" in text:
+ m.die(
+ "Saw the start of a CSS production (like <>), but couldn't find the end on the same line.",
+ lineNum=s.loc(start),
+ )
return Result.fail(start)
- i += 2
+ if "<" in text or ">" in text:
+ m.die(
+ "It seems like you wrote a CSS production (like <>), but there's more markup inside of it, or you didn't close it properly.",
+ lineNum=s.loc(start),
+ )
+ return Result.fail(start)
+ nodeEnd = textEnd + 2
startTag = StartTag(
line=s.line(start),
endLine=s.line(start),
tag="fake-production-placeholder",
- attrs={"bs-autolink-syntax": s[start:i], "class": "production", "bs-opaque": ""},
+ attrs={"bs-autolink-syntax": s[start:nodeEnd], "class": "production", "bs-opaque": ""},
).finalize()
- el = WholeElement(
- line=startTag.line,
- tag=startTag.tag,
- startTag=startTag,
+ contents = Text(
+ line=s.line(textStart),
+ endLine=s.line(textEnd),
text=text,
- endLine=s.line(i - 1),
)
- return Result(el, i)
+ endTag = EndTag(
+ line=s.line(textEnd),
+ endLine=s.line(nodeEnd - 1),
+ tag=startTag.tag,
+ )
+ return Result([startTag, contents, endTag], nodeEnd)
-def parseCSSMaybe(s: Stream, start: int) -> Result:
+def parseCSSMaybe(s: Stream, start: int) -> Result[RawElement]:
# Maybes can cause parser issues,
# like ''/px'',
# but also can contain other markup that would split the text,
@@ -1084,8 +1155,8 @@ def parseCSSMaybe(s: Stream, start: int) -> Result:
return Result.fail(start)
i = start + 2
- text, i = s.skipTo(i, "''")
- if text is Failure:
+ text, i = s.skipTo(i, "''").t2
+ if text is None:
return Result.fail(start)
if "\n" in text:
return Result.fail(start)
@@ -1127,13 +1198,13 @@ def parseCSSMaybe(s: Stream, start: int) -> Result:
codeSpanEnd2Re = re.compile(r"(.*?[^`])(``)([^`]|$)")
-def parseCodeSpan(s: Stream, start: int) -> Result:
+def parseCodeSpan(s: Stream, start: int) -> Result[list[ParserNode]]:
if s[start - 1] == "`" and s[start - 2 : start] != "\\`":
return Result.fail(start)
if s[start] != "`":
return Result.fail(start)
- match, i = s.matchRe(start, codeSpanStartRe)
- assert match is not Failure
+ match, i = s.matchRe(start, codeSpanStartRe).t2
+ assert match is not None
ticks = match.group(0)
contentStart = i
@@ -1143,8 +1214,8 @@ def parseCodeSpan(s: Stream, start: int) -> Result:
endRe = codeSpanEnd2Re
else:
endRe = re.compile(r"([^`])(" + ticks + ")([^`]|$)")
- match, _ = s.searchRe(i, endRe)
- if match is Failure:
+ match, _ = s.searchRe(i, endRe).t2
+ if match is None:
# Allowed to be unmatched, they're just ticks then.
return Result.fail(start)
contentEnd = match.end(1)
@@ -1157,34 +1228,39 @@ def parseCodeSpan(s: Stream, start: int) -> Result:
# (So you can put ticks at the start/end of your code span.)
text = text[1:-1]
- el = WholeElement(
+ startTag = StartTag(
line=s.line(start),
+ endLine=s.line(contentStart - 1),
tag="code",
- text=text,
- startTag=StartTag(
- line=s.line(start),
- endLine=s.line(start),
- tag="code",
- attrs={"bs-autolink-syntax": s[start:i], "bs-opaque": ""},
- ),
+ attrs={"bs-autolink-syntax": s[start:i], "bs-opaque": ""},
+ )
+ content = Text(
+ line=s.line(contentStart),
+ endLine=s.line(contentEnd - 1),
+ text=escapeHTML(text),
+ )
+ endTag = EndTag(
+ line=s.line(contentEnd),
endLine=s.line(i - 1),
+ tag=startTag.tag,
)
- return Result(el, i)
+ return Result([startTag, content, endTag], i)
fencedStartRe = re.compile(r"`{3,}|~{3,}")
-def parseFencedCodeBlock(s: Stream, start: int) -> Result:
+def parseFencedCodeBlock(s: Stream, start: int) -> Result[RawElement]:
if s.precedingTextOnLine(start).strip() != "":
return Result.fail(start)
- match, i = s.matchRe(start, fencedStartRe)
- if match is Failure:
+ match, i = s.matchRe(start, fencedStartRe).t2
+ if match is None:
return Result.fail(start)
openingFence = match.group(0)
- infoString, i = s.skipToNextLine(i)
+ infoString, i = s.skipToNextLine(i).t2
+ assert infoString is not None
infoString = infoString.strip()
if "`" in infoString:
# This isn't allowed, because it collides with inline code spans.
@@ -1196,10 +1272,10 @@ def parseFencedCodeBlock(s: Stream, start: int) -> Result:
# at least as long, so just search for the opening
# fence itself, as a start.
- text, i = s.skipTo(i, openingFence)
+ text, i = s.skipTo(i, openingFence).t2
# No ending fence in the rest of the document
- if text is Failure:
+ if text is None:
m.die("Hit EOF while parsing fenced code block.", lineNum=s.line(start))
contents += s[i:]
i = len(s._chars)
@@ -1217,15 +1293,16 @@ def parseFencedCodeBlock(s: Stream, start: int) -> Result:
# and markdown parsers.
# Consume the whole fence, since it can be longer.
- endingFence, i = s.matchRe(i, fencedStartRe)
+ endingFence, i = s.matchRe(i, fencedStartRe).t2
break
# Otherwise I just hit a line that happens to have
# a fence lookalike- on it, but not closing this one.
# Skip the fence and continue.
- text, i = s.matchRe(i, fencedStartRe)
- contents += text
+ match, i = s.matchRe(i, fencedStartRe).t2
+ assert match is not None
+ contents += match.group(0)
# At this point i is past the end of the code block.
tag = StartTag(
@@ -1250,15 +1327,15 @@ def parseFencedCodeBlock(s: Stream, start: int) -> Result:
macroRe = re.compile(r"\[([A-Z\d-]*[A-Z][A-Z\d-]*)(\??)\]")
-def parseMacro(s: Stream, start: int) -> Result:
+def parseMacro(s: Stream, start: int) -> Result[Macro]:
# Macros all look like `[FOO]` or `[FOO?]`:
# uppercase ASCII, possibly with a ? suffix,
# tightly wrapped by square brackets.
if s[start] != "[":
return Result.fail(start)
- match, i = s.matchRe(start, macroRe)
- if match is Failure:
+ match, i = s.matchRe(start, macroRe).t2
+ if match is None:
return Result.fail(start)
optional = match.group(2) == "?"
macro = Macro(
@@ -1274,7 +1351,7 @@ def parseMacro(s: Stream, start: int) -> Result:
metadataXmpEndRe = re.compile(r"(.*)")
-def parseMetadataBlock(s: Stream, start: int) -> Result:
+def parseMetadataBlock(s: Stream, start: int) -> Result[RawElement]:
# Metadata blocks aren't actually HTML elements,
# they're line-based BSF-Markdown constructs
# and contain unparsed text.
@@ -1283,8 +1360,10 @@ def parseMetadataBlock(s: Stream, start: int) -> Result:
# Metadata blocks must have their start/end tags
# on the left margin, completely unindented.
return Result.fail(start)
- startTag, i = parseStartTag(s, start)
- if startTag is Failure:
+ startTag, i = parseStartTag(s, start).t2
+ if startTag is None:
+ return Result.fail(start)
+ if isinstance(startTag, SelfClosedTag):
return Result.fail(start)
if startTag.tag not in ("pre", "xmp"):
return Result.fail(start)
@@ -1294,7 +1373,8 @@ def parseMetadataBlock(s: Stream, start: int) -> Result:
startTag.tag = startTag.tag.lower()
# Definitely in a metadata block now
- line, i = s.skipToNextLine(i)
+ line, i = s.skipToNextLine(i).t2
+ assert line is not None
if line.strip() != "":
m.die("Significant text on the same line as the metadata start tag isn't allowed.", lineNum=s.line(start))
contents = line
@@ -1303,7 +1383,8 @@ def parseMetadataBlock(s: Stream, start: int) -> Result:
if s.eof(i):
m.die("Hit EOF while trying to parse a metadata block.", lineNum=s.line(start))
break
- line, i = s.skipToNextLine(i)
+ line, i = s.skipToNextLine(i).t2
+ assert line is not None
match = endPattern.match(line)
if not match:
contents += line
diff --git a/bikeshed/t.py b/bikeshed/t.py
index c56ba0b554..5f1cd505d1 100644
--- a/bikeshed/t.py
+++ b/bikeshed/t.py
@@ -3,7 +3,8 @@
from __future__ import annotations
# The only three things that should be available during runtime.
-from typing import TYPE_CHECKING, cast, overload
+# ...except I need these too, to declare a generic class.
+from typing import TYPE_CHECKING, Generic, TypeVar, cast, overload
if TYPE_CHECKING:
from typing import (
@@ -16,7 +17,6 @@
Deque,
FrozenSet,
Generator,
- Generic,
Iterable,
Iterator,
Literal,
@@ -28,13 +28,13 @@
Protocol,
Sequence,
TextIO,
+ Type,
TypeAlias,
TypedDict,
TypeGuard,
- TypeVar,
)
- from _typeshed import SupportsKeysAndGetItem
+ from _typeshed import Self, SupportsKeysAndGetItem
from lxml import etree
from typing_extensions import (
NotRequired,
diff --git a/tests/github/jfbastien/papers/source/P1152R1.console.txt b/tests/github/jfbastien/papers/source/P1152R1.console.txt
index e69de29bb2..3cac1112df 100644
--- a/tests/github/jfbastien/papers/source/P1152R1.console.txt
+++ b/tests/github/jfbastien/papers/source/P1152R1.console.txt
@@ -0,0 +1 @@
+LINE 527:32: Saw the start of a CSS production (like <>), but couldn't find the end on the same line.
diff --git a/tests/github/jfbastien/papers/source/P1152R2.console.txt b/tests/github/jfbastien/papers/source/P1152R2.console.txt
index e69de29bb2..c291ba35c0 100644
--- a/tests/github/jfbastien/papers/source/P1152R2.console.txt
+++ b/tests/github/jfbastien/papers/source/P1152R2.console.txt
@@ -0,0 +1,2 @@
+LINE 382:42: Saw the start of a CSS production (like <>), but couldn't find the end on the same line.
+LINE 695:21: Saw the start of a CSS production (like <>), but couldn't find the end on the same line.
diff --git a/tests/github/jfbastien/papers/source/P1152R3.console.txt b/tests/github/jfbastien/papers/source/P1152R3.console.txt
index e69de29bb2..ce1b0e2f2d 100644
--- a/tests/github/jfbastien/papers/source/P1152R3.console.txt
+++ b/tests/github/jfbastien/papers/source/P1152R3.console.txt
@@ -0,0 +1,2 @@
+LINE 392:42: Saw the start of a CSS production (like <>), but couldn't find the end on the same line.
+LINE 705:21: Saw the start of a CSS production (like <>), but couldn't find the end on the same line.
diff --git a/tests/github/jfbastien/papers/source/P1152R4.console.txt b/tests/github/jfbastien/papers/source/P1152R4.console.txt
index e69de29bb2..5c01becd8a 100644
--- a/tests/github/jfbastien/papers/source/P1152R4.console.txt
+++ b/tests/github/jfbastien/papers/source/P1152R4.console.txt
@@ -0,0 +1,2 @@
+LINE 408:42: Saw the start of a CSS production (like <>), but couldn't find the end on the same line.
+LINE 732:21: Saw the start of a CSS production (like <>), but couldn't find the end on the same line.