1
0
mirror of https://github.com/Jermolene/TiddlyWiki5 synced 2024-09-29 23:40:45 +00:00
TiddlyWiki5/plugins/tiddlywiki/markdown/wrapper.js
RJ Skerry-Ryan 8d9dc0cd29
Markdown: Don't emit paragraph tags when a paragraph is "tight". (#5848)
* markdown: Don't emit paragraph tags when a paragraph is "tight".

Motivation: Since the upgrade to remarkable.js (#3876), lists are rendered as
HTML like this:

```
<ul>
  <li><p>One</p></li>
  <li><p>Two</p></li>
  <li><p>Three</p></li>
</ul>
```

The paragraph nodes insert blocks that break the visual flow of the list and are
unexpected e.g. compared to WikiText markup's rendering of a bulleted list.

Solution: remarkable.js annotates certain paragraph nodes as "tight", and in the
bulleted list case, the paragraph nodes wrapping the text of each list item are
marked tight.

remarkable uses the tight property to [elide paragraph tags in its
renderer](58b6945f20/lib/rules.js (L136-L142)).

This change implements the equivalent logic in TiddlyWiki's markdown rendering:
If a paragraph is marked tight, then we elide the `<p>` tag wrapping its children.

* Use ES5 Array.concat instead of ES6 spread operator.
2021-07-06 11:33:12 +01:00

330 lines
9.0 KiB
JavaScript
Executable File

/*\
title: $:/plugins/tiddlywiki/markdown/wrapper.js
type: application/javascript
module-type: parser
Wraps up the remarkable parser for use as a Parser in TiddlyWiki
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
var r = require("$:/plugins/tiddlywiki/markdown/remarkable.js");
var Remarkable = r.Remarkable,
linkify = r.linkify,
utils = r.utils;
///// Set up configuration options /////
function parseAsBoolean(tiddlerName) {
return $tw.wiki.getTiddlerText(tiddlerName).toLowerCase() === "true";
}
var pluginOpts = {
linkNewWindow: parseAsBoolean("$:/config/markdown/linkNewWindow"),
renderWikiText: parseAsBoolean("$:/config/markdown/renderWikiText"),
renderWikiTextPragma: $tw.wiki.getTiddlerText("$:/config/markdown/renderWikiTextPragma").trim()
};
var remarkableOpts = {
breaks: parseAsBoolean("$:/config/markdown/breaks"),
quotes: $tw.wiki.getTiddlerText("$:/config/markdown/quotes"),
typographer: parseAsBoolean("$:/config/markdown/typographer")
};
var accumulatingTypes = {
"text": true,
"softbreak": true
};
var md = new Remarkable(remarkableOpts);
// If tiddlywiki/katex plugin is present, use remarkable-katex to enable katex support.
if ($tw.modules.titles["$:/plugins/tiddlywiki/katex/katex.min.js"]) {
var rk = require("$:/plugins/tiddlywiki/markdown/remarkable-katex.js");
md = md.use(rk);
}
if (parseAsBoolean("$:/config/markdown/linkify")) {
md = md.use(linkify);
}
function findTagWithType(nodes, startPoint, type, level) {
for (var i = startPoint; i < nodes.length; i++) {
if (nodes[i].type === type && nodes[i].level === level) {
return i;
}
}
return false;
}
/**
* Remarkable creates nodes that look like:
* [
* { type: 'paragraph_open'},
* { type: 'inline', content: 'Hello World', children:[{type: 'text', content: 'Hello World'}]},
* { type: 'paragraph_close'}
* ]
*
* But TiddlyWiki wants the Parser (https://tiddlywiki.com/dev/static/Parser.html) to emit nodes like:
*
* [
* { type: 'element', tag: 'p', children: [{type: 'text', text: 'Hello World'}]}
* ]
*/
function convertNodes(remarkableTree, isStartOfInline) {
let out = [];
var accumulatedText = '';
function withChildren(currentIndex, currentLevel, closingType, nodes, callback) {
var j = findTagWithType(nodes, currentIndex + 1, closingType, currentLevel);
if (j === false) {
console.error("Failed to find a " + closingType + " node after position " + currentIndex);
console.log(nodes);
return currentIndex + 1;
}
let children = convertNodes(nodes.slice(currentIndex + 1, j));
callback(children);
return j;
}
function wrappedElement(elementTag, currentIndex, currentLevel, closingType, nodes) {
return withChildren(currentIndex, currentLevel, closingType, nodes, function(children) {
out.push({
type: "element",
tag: elementTag,
children: children
});
});
}
for (var i = 0; i < remarkableTree.length; i++) {
var currentNode = remarkableTree[i];
switch (currentNode.type) {
case "paragraph_open":
// If the paragraph is a "tight" layout paragraph, don't wrap children in a <p> tag.
if (currentNode.tight) {
i = withChildren(i, currentNode.level, "paragraph_close", remarkableTree, function(children) {
Array.prototype.push.apply(out, children);
});
} else {
i = wrappedElement("p", i, currentNode.level, "paragraph_close", remarkableTree);
}
break;
case "heading_open":
i = wrappedElement("h" + currentNode.hLevel, i, currentNode.level, "heading_close", remarkableTree);
break;
case "bullet_list_open":
i = wrappedElement("ul", i, currentNode.level, "bullet_list_close", remarkableTree);
break;
case "ordered_list_open":
i = wrappedElement('ol', i, currentNode.level,'ordered_list_close', remarkableTree);
break;
case "list_item_open":
i = wrappedElement("li", i, currentNode.level, "list_item_close", remarkableTree);
break;
case "link_open":
i = withChildren(i, currentNode.level, "link_close", remarkableTree, function(children) {
if (currentNode.href[0] !== "#") {
// External link
var attributes = {
class: { type: "string", value: "tc-tiddlylink-external" },
href: { type: "string", value: currentNode.href },
rel: { type: "string", value: "noopener noreferrer" }
};
if (pluginOpts.linkNewWindow) {
attributes.target = { type: "string", value: "_blank" };
}
out.push({
type: "element",
tag: "a",
attributes: attributes,
children: children
});
} else {
// Internal link
out.push({
type: "link",
attributes: {
to: { type: "string", value: decodeURI(currentNode.href.substr(1)) }
},
children: children
});
}
});
break;
case "code":
out.push({
type: "element",
tag: currentNode.block ? "pre" : "code",
children: [{ type: "text", text: currentNode.content }]
});
break;
case "fence":
out.push({
type: "codeblock",
attributes: {
language: { type: "string", value: currentNode.params },
code: { type: "string", value: currentNode.content }
}
});
break;
case "image":
out.push({
type: "image",
attributes: {
tooltip: { type: "string", value: currentNode.alt },
source: { type: "string", value: decodeURIComponent(currentNode.src) }
}
});
break;
case "softbreak":
if (remarkableOpts.breaks) {
out.push({
type: "element",
tag: "br",
});
} else {
accumulatedText = accumulatedText + '\n';
}
break;
case "hardbreak":
out.push({
type: "element",
tag: "br",
});
break;
case "th_open":
case "td_open":
var elementTag = currentNode.type.slice(0, 2);
i = withChildren(i, currentNode.level, elementTag + "_close", remarkableTree, function(children) {
var attributes = {};
if (currentNode.align) {
attributes.style = { type: "string", value: "text-align:" + currentNode.align };
}
out.push({
type: "element",
tag: elementTag,
attributes: attributes,
children: children
});
});
break;
case "hr":
out.push({
type: 'element',
tag: 'hr',
});
break;
case "inline":
out = out.concat(convertNodes(currentNode.children, true));
break;
case "text":
// We need to merge this text block with the upcoming text block and parse it all together.
accumulatedText = accumulatedText + currentNode.content;
break;
case "katex":
out.push({
type: "latex",
attributes: {
text: { type: "text", value: currentNode.content },
displayMode: { type: "text", value: currentNode.block ? "true" : "false" }
}
});
break;
default:
if (currentNode.type.substr(currentNode.type.length - 5) === "_open") {
var tagName = currentNode.type.substr(0, currentNode.type.length - 5);
i = wrappedElement(tagName, i, currentNode.level, tagName + "_close", remarkableTree);
} else {
console.error("Unknown node type: " + currentNode.type, currentNode);
out.push({
type: "text",
text: currentNode.content
});
}
break;
}
// We test to see if we process the block now, or if there's
// more to accumulate first.
if (accumulatedText
&& (
remarkableOpts.breaks ||
(i+1) >= remarkableTree.length ||
!accumulatingTypes[remarkableTree[i+1].type]
)
) {
// The Markdown compiler thinks this is just text.
// Hand off to the WikiText parser to see if there's more to render
// But only if it's configured to, and we have more than whitespace
if (!pluginOpts.renderWikiText || accumulatedText.match(/^\s*$/)) {
out.push({
type: "text",
text: accumulatedText
});
} else {
// If we're inside a block element (div, p, td, h1), and this is the first child in the tree,
// handle as a block-level parse. Otherwise not.
var parseAsInline = !(isStartOfInline && i === 0);
var textToParse = accumulatedText;
if (pluginOpts.renderWikiTextPragma !== "") {
textToParse = pluginOpts.renderWikiTextPragma + "\n" + textToParse;
}
var wikiParser = $tw.wiki.parseText("text/vnd.tiddlywiki", textToParse, {
parseAsInline: parseAsInline
});
var rs = wikiParser.tree;
// If we parsed as a block, but the root element the WikiText parser gave is a paragraph,
// we should discard the paragraph, since the way Remarkable nests its nodes, this "inline"
// node is always inside something else that's a block-level element
if (!parseAsInline
&& rs.length === 1
&& rs[0].type === "element"
&& rs[0].tag === "p"
) {
rs = rs[0].children;
}
// If the original text element started with a space, add it back in
if (rs.length > 0
&& rs[0].type === "text"
&& (accumulatedText[0] === " " || accumulatedText[0] === "\n")
) {
rs[0].text = " " + rs[0].text;
}
out = out.concat(rs);
}
accumulatedText = '';
}
}
return out;
}
var MarkdownParser = function(type, text, options) {
var tree = md.parse(text, {});
//console.debug(tree);
tree = convertNodes(tree);
//console.debug(tree);
this.tree = tree;
};
exports["text/x-markdown"] = MarkdownParser;
})();