Skip to content

Commit

Permalink
Fix list tightness.
Browse files Browse the repository at this point in the history
According to the specification, blank lines in a block quote doesn't
separate list items:
https://spec.commonmark.org/0.30/#example-320

Therefore, the following example should be tight:

- > - a
  >
- b

The specification also say that link reference definitions can be
children of list items when checking list tightness:
https://spec.commonmark.org/0.30/#example-317

Therefore, the following example should be loose:

- [aaa]: /

  [bbb]: /
- b

This commit fixes those problems with the following strategy:

- Using source end position and start position of adjoining elements to
  check tightness.
  This requires adjusting source end position of some block types to
  exclude trailing blank lines.

- Delaying removal of link reference definitions until the entire document is
  parsed.
  • Loading branch information
taku0 committed Feb 11, 2023
1 parent 9a16ff4 commit 2f8593f
Show file tree
Hide file tree
Showing 3 changed files with 251 additions and 70 deletions.
145 changes: 77 additions & 68 deletions lib/blocks.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,23 +74,10 @@ var peek = function(ln, pos) {

// These are methods of a Parser object, defined below.

// Returns true if block ends with a blank line, descending if needed
// into lists and sublists.
// Returns true if block ends with a blank line.
var endsWithBlankLine = function(block) {
while (block) {
if (block._lastLineBlank) {
return true;
}
var t = block.type;
if (!block._lastLineChecked && (t === "list" || t === "item")) {
block._lastLineChecked = true;
block = block._lastChild;
} else {
block._lastLineChecked = true;
break;
}
}
return false;
return block.next &&
block.sourcepos[1][0] !== block.next.sourcepos[0][0] - 1;
};

// Add a line to the block at the tip. We assume the tip
Expand Down Expand Up @@ -221,6 +208,60 @@ var closeUnmatchedBlocks = function() {
}
};

// Remove link reference definitions from given tree.
var removeLinkReferenceDefinitions = function(parser, tree) {
var event, node;
var walker = tree.walker();
var emptyNodes = [];

while ((event = walker.next())) {
node = event.node;
if (event.entering && node.type === "paragraph") {
var pos;
var hasReferenceDefs = false;

// Try parsing the beginning as link reference definitions;
// Note that link reference definitions must be the beginning of a
// paragraph node since link reference definitions cannot interrupt
// paragraphs.
while (
peek(node._string_content, 0) === C_OPEN_BRACKET &&
(pos = parser.inlineParser.parseReference(
node._string_content,
parser.refmap
))
) {
node._string_content = node._string_content.slice(pos);
hasReferenceDefs = true;
}
if (hasReferenceDefs && isBlank(node._string_content)) {
emptyNodes.push(node);
}
}
}

for (node of emptyNodes) {
node.unlink();
}
};

var trimTrailingSpaces = function(block, includeFinalNewline) {
var lines = block._string_content.split("\n");
// Note that indented code block nor HTML block cannot be empty,
// so lines.length cannot be zero.
while (/^[ \t]*$/.test(lines[lines.length - 1])) {
lines.pop();
}
block._literal = lines.join("\n");
if (includeFinalNewline) {
block._literal = block._literal + "\n";
}
block.sourcepos[1][0] =
block.sourcepos[0][0] + lines.length - 1;
block.sourcepos[1][1] =
block.sourcepos[0][1] + lines[lines.length - 1].length - 1;
}

// 'finalize' is run when the block is closed.
// 'continue' is run to check whether the block is continuing
// at a certain line and offset (e.g. whether a block quote
Expand All @@ -231,7 +272,8 @@ var blocks = {
continue: function() {
return 0;
},
finalize: function() {
finalize: function(parser, block) {
removeLinkReferenceDefinitions(parser, block);
return;
},
canContain: function(t) {
Expand All @@ -247,7 +289,7 @@ var blocks = {
var item = block._firstChild;
while (item) {
// check for non-final list item ending with blank line:
if (endsWithBlankLine(item) && item._next) {
if (item._next && endsWithBlankLine(item)) {
block._listData.tight = false;
break;
}
Expand All @@ -256,8 +298,8 @@ var blocks = {
var subitem = item._firstChild;
while (subitem) {
if (
endsWithBlankLine(subitem) &&
(item._next || subitem._next)
subitem._next &&
endsWithBlankLine(subitem)
) {
block._listData.tight = false;
break;
Expand All @@ -266,6 +308,7 @@ var blocks = {
}
item = item._next;
}
block.sourcepos[1] = block._lastChild.sourcepos[1];
},
canContain: function(t) {
return t === "item";
Expand Down Expand Up @@ -320,7 +363,16 @@ var blocks = {
}
return 0;
},
finalize: function() {
finalize: function(parser, block) {
if (block._lastChild) {
block.sourcepos[1] = block._lastChild.sourcepos[1];
} else {
// Empty list item
block.sourcepos[1][0] = block.sourcepos[0][0];
block.sourcepos[1][1] =
block._listData.markerOffset + block._listData.padding;
}

return;
},
canContain: function(t) {
Expand Down Expand Up @@ -402,10 +454,7 @@ var blocks = {
block._literal = rest;
} else {
// indented
block._literal = block._string_content.replace(
/(\n *)+$/,
"\n"
);
trimTrailingSpaces(block, true);
}
block._string_content = null; // allow GC
},
Expand All @@ -423,7 +472,7 @@ var blocks = {
: 0;
},
finalize: function(parser, block) {
block._literal = block._string_content.replace(/(\n *)+$/, "");
trimTrailingSpaces(block, false);
block._string_content = null; // allow GC
},
canContain: function() {
Expand All @@ -435,24 +484,8 @@ var blocks = {
continue: function(parser) {
return parser.blank ? 1 : 0;
},
finalize: function(parser, block) {
var pos;
var hasReferenceDefs = false;

// try parsing the beginning as link reference definitions:
while (
peek(block._string_content, 0) === C_OPEN_BRACKET &&
(pos = parser.inlineParser.parseReference(
block._string_content,
parser.refmap
))
) {
block._string_content = block._string_content.slice(pos);
hasReferenceDefs = true;
}
if (hasReferenceDefs && isBlank(block._string_content)) {
block.unlink();
}
finalize: function() {
return;
},
canContain: function() {
return false;
Expand Down Expand Up @@ -835,33 +868,9 @@ var incorporateLine = function(ln) {

// finalize any blocks not matched
this.closeUnmatchedBlocks();
if (this.blank && container.lastChild) {
container.lastChild._lastLineBlank = true;
}

t = container.type;

// Block quote lines are never blank as they start with >
// and we don't count blanks in fenced code for purposes of tight/loose
// lists or breaking out of lists. We also don't set _lastLineBlank
// on an empty list item, or if we just closed a fenced block.
var lastLineBlank =
this.blank &&
!(
t === "block_quote" ||
(t === "code_block" && container._isFenced) ||
(t === "item" &&
!container._firstChild &&
container.sourcepos[0][0] === this.lineNumber)
);

// propagate lastLineBlank up through parents:
var cont = container;
while (cont) {
cont._lastLineBlank = lastLineBlank;
cont = cont._parent;
}

if (this.blocks[t].acceptsLines) {
this.addLine();
// if HtmlBlock, check for end condition
Expand Down
2 changes: 0 additions & 2 deletions lib/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,6 @@ var Node = function(nodeType, sourcepos) {
this._prev = null;
this._next = null;
this._sourcepos = sourcepos;
this._lastLineBlank = false;
this._lastLineChecked = false;
this._open = true;
this._string_content = null;
this._literal = null;
Expand Down
Loading

0 comments on commit 2f8593f

Please sign in to comment.