Keep the task list marker as literal text when item content is a setext heading - #1124
Keep the task list marker as literal text when item content is a setext heading#1124hdimer wants to merge 6 commits into
Conversation
A task list marker is appended as the first child of a list item's paragraph before the paragraph is parsed. If that paragraph turns out to be a setext heading, the body is rewritten to a heading but the marker stayed inside it, producing <h2><input type=checkbox>a</h2>. Per GFM a task list marker is only valid when the item content is a paragraph, so drop the leading marker when the paragraph becomes a setext heading. A marker on an earlier paragraph is unaffected. Fixes pulldown-cmark#1115
|
Thanks for the contribution. +1 for AI disclosure. This test is not what I expected: The square brackets should be preserved (and not just reinserted, the parsing should be canceled completely in this case). That said, I'm not sure how to accomplish that cleanly. |
|
GitHub's own behavior seems correct, and we should try to match it, I think? - [ ] a
-
- [ ] b
=
- [ ] c
x
|
|
Alternatively, we could try to match what pandoc does, and just put the task marker outside the header. Both answers seem defensible to me. Just, please don't silently eat the |
…ng it Dropping the marker node silently ate the `[ ]` (review feedback on pulldown-cmark#1115). Instead cancel the task-list interpretation and render the marker's own source bytes, plus the whitespace that separated it from the text, as literal text. `- [ ] a` over a setext rule now becomes `<h2>[ ] a</h2>`. Only the whitespace run is absorbed (not up to the next node), so a following backslash escape still resolves normally.
|
Thanks both, agreed the marker shouldn't vanish. I pushed a revision that cancels the task-list interpretation and keeps the marker's own It reuses the marker node's existing span (only growing it across the separating whitespace) rather than synthesizing a new string, so it's the "cancel completely" variant rather than a reinsertion. A following backslash escape isn't pulled in ( One edge worth flagging: since the brackets become a single literal text node, they no longer take part in reference-link resolution, so |
|
I'm not picky about which implementation we follow, as long as we actually do it. Since GitHub resolves link defs, we should too if we're following their lead. - [x] h1
=
[x]: https://example.com
|
Ah, about that... Even if you have a perfectly normal checked task, it will prioritize the link because this is done in postprocessing.
But yes, if we reject the |
|
Makes sense, thanks. So the agreed behavior: reject My current commit converts the marker to a single literal-text node, which is inert. To get link-eligibility I'll re-scan the marker's span as inline content instead, so it yields the usual Sound right? If so I'll push that version. |
When a list item's content turns out to be a setext heading, the task-list marker is not valid (pulldown-cmark#1115). Instead of freezing `[x]` as inert text, split it back into MaybeLinkOpen / text / MaybeLinkClose so a matching `[x]:` reference resolves as a link, matching GitHub's postprocessing order (review on pulldown-cmark#1124). A whitespace-only label (`[ ]`) can't match a definition and stays literal.
|
Pushed that version. When the task-list interpretation is canceled (item content is a setext heading), |
|
I found another problem with this PR. It turns out, you can reproduce the same bug using tables, not just headings. - [x] | one | two | three |
|-----|-----|-------|------|
| one | two | three | four |GitHub:
pulldown-cmark: $ cargo run --release -- --enable-tables --enable-tasklists
Finished `release` profile [optimized] target(s) in 0.07s
Running `target/release/pulldown-cmark --enable-tables --enable-tasklists`
- [x] | one | two | three
|-----|-----|-------
| one | two | three
<ul>
<li><table><thead><tr><th>one</th><th>two</th><th>three</th></tr></thead><tbody>
<tr><td>one</td><td>two</td><td>three</td></tr>
</tbody></table>
</li>
</ul>And definition lists have a slightly different, but still basically similar, bug: $ cargo run --release -- --enable-definition-list --enable-tasklists
Finished `release` profile [optimized] target(s) in 0.07s
Running `target/release/pulldown-cmark --enable-tables --enable-tasklists`
- [x] a
: b
<ul>
<li>
<dl>
<dt><input disabled="" type="checkbox" checked=""/>
a</dt>
<dd>b</dd>
</dl>
</li>
</ul> |
A task list marker is only valid when the item content is a paragraph. The setext heading case was handled; tables and definition list titles have the same problem. For a table, the marker's bytes are ordinary row content, so scan the header row from the marker's start rather than from the paragraph's. That counts the marker as a leading cell and makes the header and delimiter column counts agree the way GitHub's do. For a definition list title, reuse the setext cancellation, now extracted into cancel_task_list_marker.
Cancelling left the marker's bytes outside the heading/title node's own range, so offset iteration reported children starting before their parent. The table path already grew its node; do the same in cancel_task_list_marker so all three paths agree. Also pin the two cases with no coverage: a leading pipe now shifts the header count enough that no table forms at all, and a reference definition resolving inside a table header cell.
Find the marker's `[` by scanning the leading spaces scan_task_list_marker already bounds, rather than searching for the byte. Drop a spec example that behaved the same before the fix, and cover the node ranges, which nothing else asserted.
|
Both reproduce, both fixed. Tables were the more interesting one. The marker's bytes are ordinary row content, so I moved the header-row scan back to the marker's start instead of the paragraph's. That means the marker counts as a leading cell, which is what makes your example agree with GitHub: four header cells ( Worth flagging the flip side, since it's a behavior change beyond the bug: Definition lists just reuse the setext cancellation, so One thing I noticed while doing this: cancelling left the marker's bytes outside the heading's own range, so Full suite + |
While the test cases are based on the earlier pulldown-cmark#1124, the implementation is different, because it avoids special-casing particular nodes where task lists are disallowed. Instead, it includes the task list markers in the tree, but parses as if it weren't, and removed the text from the tree if it's not needed.
|
Closed in favor of #1135. |
Fixes #1115.
With
ENABLE_TASKLISTS, a task list marker is appended as the first child of the item's paragraph before the paragraph is parsed. If that paragraph turns out to be a setext heading (-underline), the body is rewritten toHeadingbut the marker stayed inside, producing<h2><input type="checkbox">a</h2>.Per GFM a task list marker is only valid when the item content is a paragraph, so drop the leading marker when the paragraph becomes a setext heading. A marker on an earlier paragraph (when only a later block becomes a heading) is unaffected.
Regression tests cover the reported case, the earlier-paragraph case, and heading attributes.
Used AI assistance on this; I reviewed and tested it.