mirror of
https://github.com/Jermolene/TiddlyWiki5
synced 2024-12-29 11:30:28 +00:00
307 lines
8.3 KiB
JavaScript
Executable File
307 lines
8.3 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 (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":
|
|
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;
|
|
|
|
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;
|
|
|
|
})();
|