Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ee8b40922 | ||
|
|
7a062a6f54 | ||
|
|
2913aa4eb0 |
@@ -11,6 +11,13 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
|||||||
|
|
||||||
-
|
-
|
||||||
|
|
||||||
|
## [1.0.1] - 2026-09-28
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- A paragraph that follows a definition list, separated from it by a blank line, is no longer swallowed into the `dl` element: the list ends before it, and terms after the paragraph open a list of their own.
|
||||||
|
- A superscript or subscript without a base, the isotope notation of `^3He`, renders over an empty base instead of degrading to an merror.
|
||||||
|
|
||||||
## [1.0.0] - 2026-09-27
|
## [1.0.0] - 2026-09-27
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -81,6 +81,10 @@ func TestDefinitionLists(t *testing.T) {
|
|||||||
"<dl>\n<dt>Term</dt>\n<dd>\n<p>Def</p>\n<p>more</p>\n</dd>\n</dl>\n"},
|
"<dl>\n<dt>Term</dt>\n<dd>\n<p>Def</p>\n<p>more</p>\n</dd>\n</dl>\n"},
|
||||||
{"definition inline content", "Term\n: A *bold* claim",
|
{"definition inline content", "Term\n: A *bold* claim",
|
||||||
"<dl>\n<dt>Term</dt>\n<dd>A <em>bold</em> claim</dd>\n</dl>\n"},
|
"<dl>\n<dt>Term</dt>\n<dd>A <em>bold</em> claim</dd>\n</dl>\n"},
|
||||||
|
{"definition paragraph after list", "Term\n: Definition\n\nParagraph after.",
|
||||||
|
"<dl>\n<dt>Term</dt>\n<dd>Definition</dd>\n</dl>\n<p>Paragraph after.</p>\n"},
|
||||||
|
{"definition paragraph between lists", "Term\n: Def\n\nBetween\n\nTerm 2\n: Def 2",
|
||||||
|
"<dl>\n<dt>Term</dt>\n<dd>Def</dd>\n</dl>\n<p>Between</p>\n<dl>\n<dt>Term 2</dt>\n<dd>Def 2</dd>\n</dl>\n"},
|
||||||
}
|
}
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
|||||||
@@ -614,6 +614,9 @@ func (p *parser) finalise(n *Node) {
|
|||||||
n.content = trimTrailingBlankLines(n.content)
|
n.content = trimTrailingBlankLines(n.content)
|
||||||
}
|
}
|
||||||
case kindList, kindDefList:
|
case kindList, kindDefList:
|
||||||
|
if n.kind == kindDefList {
|
||||||
|
p.liftLeakedTerms(n)
|
||||||
|
}
|
||||||
n.tight = !blocksAreLoose(n.children)
|
n.tight = !blocksAreLoose(n.children)
|
||||||
case kindDocument:
|
case kindDocument:
|
||||||
p.gatherFootnotes(n)
|
p.gatherFootnotes(n)
|
||||||
@@ -623,6 +626,60 @@ func (p *parser) finalise(n *Node) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// liftLeakedTerms moves the paragraphs that never became terms out of a
|
||||||
|
// definition list. A paragraph may sit directly inside the list as a
|
||||||
|
// term waiting for its definition line; when that line never arrives the
|
||||||
|
// paragraph is prose after the list, and the list ends before it, since
|
||||||
|
// a dl element carries nothing but terms and definitions. Terms and
|
||||||
|
// definitions after the leak form a list of their own.
|
||||||
|
func (p *parser) liftLeakedTerms(dl *Node) {
|
||||||
|
leak := -1
|
||||||
|
for i, c := range dl.children {
|
||||||
|
if c.kind == kindParagraph {
|
||||||
|
leak = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if leak < 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
parent := dl.parent
|
||||||
|
base := 0
|
||||||
|
for i, c := range parent.children {
|
||||||
|
if c == dl {
|
||||||
|
base = i + 1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tail := dl.children[leak:]
|
||||||
|
dl.children = dl.children[:leak]
|
||||||
|
insert := func(n *Node) {
|
||||||
|
parent.children = append(parent.children, nil)
|
||||||
|
copy(parent.children[base+1:], parent.children[base:])
|
||||||
|
parent.children[base] = n
|
||||||
|
base++
|
||||||
|
n.parent = parent
|
||||||
|
}
|
||||||
|
var regroup *Node
|
||||||
|
for _, c := range tail {
|
||||||
|
switch c.kind {
|
||||||
|
case kindParagraph:
|
||||||
|
insert(c)
|
||||||
|
regroup = nil
|
||||||
|
case kindDefTerm, kindDefItem:
|
||||||
|
if regroup == nil {
|
||||||
|
regroup = &Node{kind: kindDefList, parent: parent}
|
||||||
|
insert(regroup)
|
||||||
|
}
|
||||||
|
c.parent = regroup
|
||||||
|
regroup.children = append(regroup.children, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if regroup != nil {
|
||||||
|
regroup.tight = !blocksAreLoose(regroup.children)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// trimTrailingBlankLines removes the trailing blank lines of an indented
|
// trimTrailingBlankLines removes the trailing blank lines of an indented
|
||||||
// code block. Every content line carries its newline, so the result of a
|
// code block. Every content line carries its newline, so the result of a
|
||||||
// non-empty block ends with exactly one.
|
// non-empty block ends with exactly one.
|
||||||
|
|||||||
@@ -37,6 +37,12 @@ var corpus = []struct {
|
|||||||
{"sqrt", `\sqrt{x}`, `<msqrt><mi>x</mi></msqrt>`},
|
{"sqrt", `\sqrt{x}`, `<msqrt><mi>x</mi></msqrt>`},
|
||||||
{"root with index", `\sqrt[3]{x}`, `<mroot><mi>x</mi><mn>3</mn></mroot>`},
|
{"root with index", `\sqrt[3]{x}`, `<mroot><mi>x</mi><mn>3</mn></mroot>`},
|
||||||
{"sub and sup", `x_1^2`, `<msubsup><mi>x</mi><mn>1</mn><mn>2</mn></msubsup>`},
|
{"sub and sup", `x_1^2`, `<msubsup><mi>x</mi><mn>1</mn><mn>2</mn></msubsup>`},
|
||||||
|
{"baseless sup", `^3\mathrm{He}`,
|
||||||
|
`<msup><mrow></mrow><mn>3</mn></msup><mrow><mi mathvariant="normal">H</mi><mi mathvariant="normal">e</mi></mrow>`},
|
||||||
|
{"baseless sub", `_1\mathrm{H}`,
|
||||||
|
`<msub><mrow></mrow><mn>1</mn></msub><mi mathvariant="normal">H</mi>`},
|
||||||
|
{"baseless group", `^{-3}`,
|
||||||
|
`<msup><mrow></mrow><mrow><mo>-</mo><mn>3</mn></mrow></msup>`},
|
||||||
{"single digit script", `x^10`, `<msup><mi>x</mi><mn>1</mn></msup><mn>0</mn>`},
|
{"single digit script", `x^10`, `<msup><mi>x</mi><mn>1</mn></msup><mn>0</mn>`},
|
||||||
{"group", `{xy}`, `<mrow><mi>x</mi><mi>y</mi></mrow>`},
|
{"group", `{xy}`, `<mrow><mi>x</mi><mi>y</mi></mrow>`},
|
||||||
{"left right", `\left( x \right)`,
|
{"left right", `\left( x \right)`,
|
||||||
|
|||||||
@@ -128,6 +128,12 @@ func (p *parser) atom() *node {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return p.scripts(n)
|
return p.scripts(n)
|
||||||
|
case tokCaret, tokUnderscore:
|
||||||
|
// A script without a base, the isotope notation of ^3He, takes
|
||||||
|
// an empty base: the script renders over nothing, the way the
|
||||||
|
// notation is printed, and degrading it would refuse notation
|
||||||
|
// the mathematics carries.
|
||||||
|
return p.scripts(el("mrow"))
|
||||||
}
|
}
|
||||||
p.pos++
|
p.pos++
|
||||||
return errorNode(t.text)
|
return errorNode(t.text)
|
||||||
|
|||||||
Reference in New Issue
Block a user