Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions core/modules/parsers/wikiparser/rules/conditional.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,9 @@ exports.parseIfClause = function(filterCondition) {
var reEndString = "\\<\\%\\s*(endif)\\s*\\%\\>|\\<\\%\\s*(else)\\s*\\%\\>|\\<\\%\\s*(elseif)\\s+([\\s\\S]+?)\\%\\>",
ex;
if(hasLineBreak) {
ex = this.parser.parseBlocksTerminatedExtended(reEndString);
// Let the terminator claim the newline before the marker, or it is
// swallowed into the preceding paragraph text node
ex = this.parser.parseBlocksTerminatedExtended("(?:\\r?\\n)?(?:" + reEndString + ")");
} else {
var reEnd = new RegExp(reEndString,"mg");
ex = this.parser.parseInlineRunTerminatedExtended(reEnd,{eatTerminator: true});
Expand All @@ -97,7 +99,7 @@ exports.parseIfClause = function(filterCondition) {
var reEndString = "\\<\\%\\s*(endif)\\s*\\%\\>",
ex;
if(hasLineBreak) {
ex = this.parser.parseBlocksTerminatedExtended(reEndString);
ex = this.parser.parseBlocksTerminatedExtended("(?:\\r?\\n)?(?:" + reEndString + ")");
} else {
var reEnd = new RegExp(reEndString,"mg");
ex = this.parser.parseInlineRunTerminatedExtended(reEnd,{eatTerminator: true});
Expand Down
2 changes: 1 addition & 1 deletion core/modules/parsers/wikiparser/rules/fnprocdef.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ exports.parse = function() {
var reEnd;
if(this.match[5]) {
// If so, it is a multiline definition and the end of the body is marked with \end
reEnd = new RegExp("((:?^|\\r?\\n)[^\\S\\n\\r]*\\\\end[^\\S\\n\\r]*(?:" + $tw.utils.escapeRegExp(this.match[2]) + ")?\\s*?(?:$|\\r?\\n))","mg");
reEnd = new RegExp("((?:^|\\r?\\n)[^\\S\\n\\r]*\\\\end[^\\S\\n\\r]*(?:" + $tw.utils.escapeRegExp(this.match[2]) + ")?\\s*?(?:$|\\r?\\n))","mg");
} else {
// Otherwise, the end of the definition is marked by the end of the line
reEnd = /($|\r?\n)/mg;
Expand Down
4 changes: 3 additions & 1 deletion core/modules/parsers/wikiparser/rules/html.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@ exports.parse = function() {
if(!tag.isSelfClosing && $tw.config.htmlVoidElements.indexOf(tag.tag) === -1) {
var reEndString = "</" + $tw.utils.escapeRegExp(tag.tag) + ">";
if(hasLineBreak) {
tag.children = this.parser.parseBlocks(reEndString);
// Let the terminator claim the newline before the close tag, or it
// is swallowed into the preceding paragraph text node
tag.children = this.parser.parseBlocks("(?:\\r?\\n)?" + reEndString);
} else {
var reEnd = new RegExp("(" + reEndString + ")","mg");
tag.children = this.parser.parseInlineRun(reEnd,{eatTerminator: true});
Expand Down
4 changes: 3 additions & 1 deletion core/modules/parsers/wikiparser/rules/quoteblock.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ exports.init = function(parser) {
exports.parse = function() {
var classes = ["tc-quote"];
// Get all the details of the match
var reEndString = "^\\s*" + this.match[1] + "(?!<)";
// Anchor from the newline, not the line start: with "^\s*" the newline
// before the marker stays inside the preceding paragraph text node
var reEndString = "(?:^|\\r?\\n)\\s*" + this.match[1] + "(?!<)";
// Move past the <s
this.parser.pos = this.matchRegExp.lastIndex;
// Parse any classes, whitespace and then the optional cite itself
Expand Down
4 changes: 3 additions & 1 deletion core/modules/parsers/wikiparser/rules/styleblock.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ exports.init = function(parser) {
};

exports.parse = function() {
var reEndString = "^@@(?:\\r?\\n)?";
// Anchor from the newline, not the line start: with "^@@" the newline
// before the marker stays inside the preceding paragraph text node
var reEndString = "(?:^|\\r?\\n)@@(?:\\r?\\n)?";
var classes = [], styles = [];
do {
// Get the class and style
Expand Down
56 changes: 56 additions & 0 deletions editions/test/tiddlers/tests/test-wikitext-block-terminators.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*\
title: test-wikitext-block-terminators.js
type: application/javascript
tags: [[$:/tags/test-spec]]

Regression tests: block terminators must claim the newline before their
closing marker.

A single newline before a block closing marker belongs to the terminator,
not to the preceding paragraph. Before the fix the ^-anchored terminators
of quoteblock and styleblock (and the unanchored ones of html and
conditional) matched AFTER the newline, so the paragraph text node ended
with an invisible "\n" while the blank line form (foo\n\n<<<) did not.
These tests are red without the terminator fix in the core rules
quoteblock.js, styleblock.js, html.js and conditional.js, and green with
it.

Reproduce in the browser F12 console, e.g. for the quoteblock case:

$tw.wiki.parseText("text/vnd.tiddlywiki","<<<\nfoo\n<<<").tree
// inspect: tree[0] is the blockquote, tree[0].children[0] the p,
// its first child must be {type: "text", text: "foo"} without "\n"

\*/

"use strict";

describe("WikiText block terminator tests", function() {

var wiki = $tw.test.wiki();

var parse = function(text) {
return wiki.parseText("text/vnd.tiddlywiki",text).tree;
};

it("should not swallow the newline before a quoteblock terminator", function() {
// the single newline and blank line forms must agree
expect(parse("<<<\nfoo\n<<<")[0].children[0].children[0].text).toBe("foo");
expect(parse("<<<\nfoo\n\n<<<")[0].children[0].children[0].text).toBe("foo");
});

it("should not swallow the newline before a styleblock terminator", function() {
// the rule returns a void wrapper around the styled blocks
expect(parse("@@.myClass\nfoo\n@@")[0].children[0].children[0].text).toBe("foo");
});

it("should not swallow the newline before an html close tag", function() {
expect(parse("<div>\n\nfoo\n</div>")[0].children[0].children[0].text).toBe("foo");
});

it("should not swallow the newline before a conditional terminator", function() {
// the block body sits in the synthesized $list-template
expect(parse("<%if [[x]]%>\n\nfoo\n<%endif%>")[0].children[0].children[0].children[0].text).toBe("foo");
});

});
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
change-category: developer
change-type: bugfix
created: 20260712232208000
description: Paragraph text before a block closing marker no longer carries a trailing newline
github-contributors: pmario
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/issues/9907
modified: 20260712232208000
release: 5.5.0
tags: $:/tags/ChangeNote
title: $:/changenotes/5.5.0/#9907
type: text/vnd.tiddlywiki

Block terminators now claim the single newline before their closing marker, so the preceding paragraph text node ends without it, exactly as it does with a blank line before the marker. For consistency, `quoteblock`, `styleblock`, HTML block bodies and conditional block clauses now behave like `codeblock`, `typedblock` and `\procedure` definitions. Rendered output is unchanged.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
title: $:/changenotes/5.5.0/#9907/impacts/text-end-positions
changenote: $:/changenotes/5.5.0/#9907
created: 20260712232208000
modified: 20260712232208000
tags: $:/tags/ImpactNote
description: Text and end positions of paragraph nodes before block terminators have changed
impact-type: compatibility-break

Paragraph text nodes inside quote, style, HTML and conditional blocks no longer end with an invisible newline; their `end` positions shrink accordingly. All block constructs now yield the same paragraph text, as the blank-line form and `codeblock`, `typedblock` and `\procedure` already did. Rendered output is unchanged.
Loading