fix: collapse markup whitespace so blockquotes render without blank lines

This commit is contained in:
randogoth 2026-10-11 10:43:12 +03:00
parent 204aee899a
commit 4e07c9a305
2 changed files with 112 additions and 36 deletions

View file

@ -155,23 +155,7 @@ void _appendBlocks(XmlElement element, String pageUrl, List<MewsBlock> out) {
out.add(ParagraphBlock(_inline(element, pageUrl, embedded)));
out.addAll(embedded);
case 'blockquote':
final children = <MewsBlock>[];
final embedded = <MewsBlock>[];
// Bare inline content (quotes are often written as plain text) becomes
// a paragraph, like in list items.
final inline = <MewsInline>[];
for (final node in element.children) {
final isInlineNode = node is XmlText ||
(node is XmlElement && _inlineElements.contains(node.name.qualified));
if (isInlineNode) {
_appendInline(node, pageUrl, inline, embedded);
} else if (node is XmlElement) {
_appendBlocks(node, pageUrl, children);
}
}
if (inline.isNotEmpty) children.insert(0, ParagraphBlock(inline));
children.addAll(embedded);
out.add(QuoteBlock(children));
out.add(QuoteBlock(_mixedBlocks(element, pageUrl)));
case 'pre':
out.add(PreBlock(element.innerText));
case 'address':
@ -212,27 +196,42 @@ const _inlineElements = {
'cite', 'abbr', 'dfn', 'img',
};
/// Builds blocks from mixed content: consecutive inline runs (text, links,
/// images) become paragraphs, block-level children append as blocks. Runs
/// that are nothing but markup whitespace never produce a paragraph, so
/// pretty-printed quotes and list items start without a blank line.
List<MewsBlock> _mixedBlocks(XmlElement element, String pageUrl) {
final blocks = <MewsBlock>[];
final embedded = <MewsBlock>[];
final inline = <MewsInline>[];
void flush() {
_dropEdgeWhitespace(inline);
if (inline.isNotEmpty) {
blocks.add(ParagraphBlock(List.of(inline)));
inline.clear();
}
}
for (final node in element.children) {
final isInlineNode = node is XmlText ||
(node is XmlElement && _inlineElements.contains(node.name.qualified));
if (isInlineNode) {
_appendInline(node, pageUrl, inline, embedded);
} else if (node is XmlElement) {
flush();
_appendBlocks(node, pageUrl, blocks);
}
}
flush();
blocks.addAll(embedded);
return blocks;
}
ListBlock _listBlock(XmlElement element, String pageUrl) {
final items = <MewsListItem>[];
for (final li in element.findElements('li')) {
final blocks = <MewsBlock>[];
final embedded = <MewsBlock>[];
// List items commonly hold bare inline content (the dated-link entry
// pattern in spec 6.1), so direct text and inline elements form a
// paragraph while block-level children append as blocks.
final inline = <MewsInline>[];
for (final node in li.children) {
final isInlineNode = node is XmlText ||
(node is XmlElement && _inlineElements.contains(node.name.qualified));
if (isInlineNode) {
_appendInline(node, pageUrl, inline, embedded);
} else if (node is XmlElement) {
_appendBlocks(node, pageUrl, blocks);
}
}
if (inline.isNotEmpty) blocks.insert(0, ParagraphBlock(inline));
blocks.addAll(embedded);
items.add(MewsListItem(blocks));
items.add(MewsListItem(_mixedBlocks(li, pageUrl)));
}
return ListBlock(
ordered: element.name.qualified == 'ol',
@ -395,18 +394,54 @@ MewsFormField _inputField(XmlElement element, String? label) {
// Inline ---------------------------------------------------------------------
/// Pretty-printed markup leaves whitespace runs between tags; browsers treat
/// them as a single space, and whitespace-only runs at the edges of inline
/// content as nothing at all.
final _whitespaceRun = RegExp(r'\s+');
List<MewsInline> _inline(XmlElement element, String pageUrl, [List<MewsBlock>? embedded]) {
final out = <MewsInline>[];
for (final child in element.children) {
_appendInline(child, pageUrl, out, embedded);
}
_dropEdgeWhitespace(out);
return out;
}
/// Trims markup whitespace from the edges of a run: whitespace-only nodes are
/// dropped, and the first/last text node loses its leading/trailing spaces,
/// so indented markup renders no blank first or last lines. Interior
/// whitespace stays: it separates inline elements the author typed apart.
void _dropEdgeWhitespace(List<MewsInline> out) {
bool blank(MewsInline node) => node is MewsText && node.text.trim().isEmpty;
while (out.isNotEmpty && blank(out.first)) {
out.removeAt(0);
}
while (out.isNotEmpty && blank(out.last)) {
out.removeLast();
}
if (out.isNotEmpty && out.first is MewsText) {
final text = (out.first as MewsText).text.trimLeft();
if (text.isEmpty) {
out.removeAt(0);
} else {
out[0] = MewsText(text);
}
}
if (out.isNotEmpty && out.last is MewsText) {
final text = (out.last as MewsText).text.trimRight();
if (text.isEmpty) {
out.removeLast();
} else {
out[out.length - 1] = MewsText(text);
}
}
}
void _appendInline(XmlNode node, String pageUrl, List<MewsInline> out,
[List<MewsBlock>? embedded]) {
if (node is XmlText) {
final text = node.value;
final text = node.value.replaceAll(_whitespaceRun, ' ');
if (text.isNotEmpty) out.add(MewsText(text));
return;
}