Skip to content

Use tight end positions for lazily closed blocks (sourcePositions) - #142

Merged
jgm merged 2 commits into
jgm:mainfrom
dereuromark:fix/lazy-close-end-positions
Jul 19, 2026
Merged

jgm merged 2 commits into
jgm:mainfrom
dereuromark:fix/lazy-close-end-positions

Conversation

@dereuromark

Copy link
Copy Markdown
Contributor

Fixes #141.

With sourcePositions: true, blocks that close lazily when the parser recognizes the following construct (lists, tables, block quotes, definition lists, footnotes) reported pos.end from the parse offset at close time. That included the separator blank line, and for lists the end could equal the next sibling's start:

parse("- a\n- b\n\nAfter.\n", {sourcePositions: true})
// before: bullet_list end = 4:1:9, identical to the following para's start
// after:  bullet_list end = 3:0:7

This change tightens those containers to end at their last child's end position - the convention paragraphs and code fences already follow (paragraph "One.\n" ends at 2:0:4, its trailing newline). Separator blank lines now belong to no block, and sibling ranges no longer overlap. List items are tightened as well, so a parent list's range always contains its last item (previously the tightened list could end before the lazily closed item).

Same approach as the earlier end-position fixes in #83 (list_item) and #82 (section); those behaviors are unchanged.

Tests: new cases in src/parse.spec.ts assert exact start/end for block quote, table, bullet list, definition list, and footnote, plus the following paragraph's positions and non-overlap/containment. One expectation in test/sourcepos.test encoded the old overshoot (list_item (1:2:1-2:1:5) - ending at the first character of the next item's line) and was updated to the tight value. Full suite passes.

Blocks that close when the parser recognizes the following construct
(lists, tables, block quotes, definition lists, footnotes) reported
their end position from the parse offset at close time. That included
the separator blank line, and for lists the end could equal the next
sibling's start, so sibling ranges overlapped and a parent list could
end before its last item.

Tighten these containers to end at their last child's end position, the
same convention paragraphs and code fences already follow. List items
are tightened as well so the parent range always contains them.

Closes jgm#141.
Comment thread src/parse.ts Outdated
Comment thread src/parse.ts Outdated
Comment thread src/parse.ts Outdated
Comment on lines +292 to +299
const tightenListEnd = function(node: Container): void {
if (!node.pos) {
return;
}
const end = getLastBlockEnd(node);
if (end) {
node.pos.end = end;
}

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of having a separate function just for lists, why not apply the tightenContainerEnd function to the list_item nodes? Then, when we get to the list nodes, the last child will already have an appropriate end position and we won't need to recurse into its children...

Since -list_item events fire before their parent -list, the last child
already carries a tight end position when the list closes. The recursive
getLastBlockEnd and the fallback to earlier siblings were unnecessary;
one function handles all containers.
@dereuromark

Copy link
Copy Markdown
Contributor Author

You were right that the recursion was unnecessary: since -list_item events always fire before their parent -list, the last child already carries a tight end position by the time the list closes. So getLastBlockEnd and the separate list function are gone, and the fallback loop over earlier siblings is gone too - if the last child has no position, we now leave the container's end untouched rather than guessing from an earlier sibling.

The whole thing is now a single small function:

const tightenContainerEnd = function(node: Container): void {
  const lastChild = node.children[node.children.length - 1];
  if (node.pos && lastChild && lastChild.pos) {
    node.pos.end = lastChild.pos.end;
  }
}

I kept it as a named function rather than inlining the conditional, since it is called from five handlers (list, list_item, block_quote, table, footnote). Full suite still passes.

@jgm

jgm commented Jul 19, 2026

Copy link
Copy Markdown
Owner

Much better!

@jgm
jgm merged commit 7af0dbb into jgm:main Jul 19, 2026
1 check passed
@dereuromark
dereuromark deleted the fix/lazy-close-end-positions branch July 19, 2026 09:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

sourcePositions: end positions of lazy-closing blocks (bullet_list, table, block_quote) overshoot into the separator blank line / next block

2 participants