diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4642870..d032510 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,10 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
## [development]
+### 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.
+
### Added
-
diff --git a/internal/markdown/extension_corpus_test.go b/internal/markdown/extension_corpus_test.go
index cf478c4..f1b49c8 100644
--- a/internal/markdown/extension_corpus_test.go
+++ b/internal/markdown/extension_corpus_test.go
@@ -81,6 +81,10 @@ func TestDefinitionLists(t *testing.T) {
"
\n- Term
\n- \n
Def
\nmore
\n \n
\n"},
{"definition inline content", "Term\n: A *bold* claim",
"\n- Term
\n- A bold claim
\n
\n"},
+ {"definition paragraph after list", "Term\n: Definition\n\nParagraph after.",
+ "\n- Term
\n- Definition
\n
\nParagraph after.
\n"},
+ {"definition paragraph between lists", "Term\n: Def\n\nBetween\n\nTerm 2\n: Def 2",
+ "\n- Term
\n- Def
\n
\nBetween
\n\n- Term 2
\n- Def 2
\n
\n"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
diff --git a/internal/markdown/parse.go b/internal/markdown/parse.go
index ff961ae..ae6a6d4 100644
--- a/internal/markdown/parse.go
+++ b/internal/markdown/parse.go
@@ -614,6 +614,9 @@ func (p *parser) finalise(n *Node) {
n.content = trimTrailingBlankLines(n.content)
}
case kindList, kindDefList:
+ if n.kind == kindDefList {
+ p.liftLeakedTerms(n)
+ }
n.tight = !blocksAreLoose(n.children)
case kindDocument:
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
// code block. Every content line carries its newline, so the result of a
// non-empty block ends with exactly one.