mirror of
https://github.com/Jermolene/TiddlyWiki5
synced 2024-12-27 10:30:28 +00:00
540 lines
15 KiB
JavaScript
540 lines
15 KiB
JavaScript
/*\
|
|
title: $:/plugins/tiddlywiki/markdown/markdown-it-tiddlywiki.js
|
|
type: application/javascript
|
|
module-type: library
|
|
|
|
Wraps up the markdown-it parser for use as a Parser in TiddlyWiki
|
|
|
|
\*/
|
|
(function(){
|
|
|
|
/*jslint node: true, browser: true */
|
|
/*global $tw: false */
|
|
"use strict";
|
|
|
|
var md;
|
|
var pluginOpts;
|
|
|
|
var TWMarkReplacements = {
|
|
"{" : "{",
|
|
"[" : "[",
|
|
"$" : "$"
|
|
};
|
|
|
|
var TWMarkRegEx = /[{[$]/g;
|
|
function encodeTWMark(match) {
|
|
return TWMarkReplacements[match];
|
|
}
|
|
|
|
// escpae {, [ and $ in string s
|
|
function escapeTWMarks(s) {
|
|
s = String(s);
|
|
TWMarkRegEx.lastIndex = 0;
|
|
return s.replace(TWMarkRegEx,encodeTWMark);
|
|
}
|
|
|
|
// escape anything that could be interpreted as transclusion or syslink
|
|
function render_code_inline(tokens,idx,options,env,slf) {
|
|
tokens[idx].attrJoin('class','_codified_');
|
|
return '<code' + slf.renderAttrs(tokens[idx]) + '>'
|
|
+ escapeTWMarks(md.utils.escapeHtml(tokens[idx].content))
|
|
+ '</code>';
|
|
}
|
|
|
|
function render_code_block(tokens,idx) {
|
|
return '<$codeblock code=e"' + md.utils.escapeHtml(tokens[idx].content) + '" language=""/>\n';
|
|
}
|
|
|
|
function render_fence(tokens,idx) {
|
|
var info = tokens[idx].info ? md.utils.unescapeAll(tokens[idx].info).trim() : '';
|
|
return '<$codeblock code=e"' + md.utils.escapeHtml(tokens[idx].content) + '" language="' + info.split(/(\s+)/g)[0] + '"/>\n';
|
|
}
|
|
|
|
// add a blank line after opening tag to activate TW block parsing
|
|
function render_paragraph_open(tokens,idx) {
|
|
return tokens[idx].hidden ? '' : '<p>\n\n';
|
|
}
|
|
|
|
function render_paragraph_close(tokens,idx) {
|
|
return tokens[idx].hidden ? '' : '\n</p>\n';
|
|
}
|
|
|
|
// Replace footnote links with "qualified" internal links
|
|
function render_footnote_ref(tokens,idx,options,env,slf) {
|
|
var id = slf.rules.footnote_anchor_name(tokens,idx,options,env,slf);
|
|
var caption = slf.rules.footnote_caption(tokens,idx,options,env,slf);
|
|
var refid = id;
|
|
|
|
if(tokens[idx].meta.subId > 0) {
|
|
refid += ':' + tokens[idx].meta.subId;
|
|
}
|
|
return '<a class="footnote-ref" href=<<qualify "##fn' + id + '">> id=<<qualify "#fnref' + refid + '">>>' + caption + '</a>';
|
|
}
|
|
|
|
function render_footnote_open(tokens,idx,options,env,slf) {
|
|
var id = slf.rules.footnote_anchor_name(tokens,idx,options,env,slf);
|
|
|
|
if(tokens[idx].meta.subId > 0) {
|
|
id += ':' + tokens[idx].meta.subId;
|
|
}
|
|
return '<li id=<<qualify "#fn' + id + '">> class="footnote-item">';
|
|
}
|
|
|
|
function render_footnote_anchor(tokens,idx,options,env,slf) {
|
|
var id = slf.rules.footnote_anchor_name(tokens,idx,options,env,slf);
|
|
|
|
if(tokens[idx].meta.subId > 0) {
|
|
id += ':' + tokens[idx].meta.subId;
|
|
}
|
|
|
|
// append variation selector to prevent display as Apple Emoji on iOS
|
|
return '<a href=<<qualify "##fnref' + id + '">> class="footnote-backref">\u21A5\uFE0E</a>';
|
|
}
|
|
|
|
// do not un-escape html entities and escape characters
|
|
function render_text_special(tokens,idx) {
|
|
if(tokens[idx].info === 'entity') {
|
|
return tokens[idx].markup;
|
|
}
|
|
return escapeTWMarks(md.utils.escapeHtml(tokens[idx].content));
|
|
}
|
|
|
|
function render_tw_expr(tokens,idx) {
|
|
return tokens[idx].content;
|
|
}
|
|
|
|
// Overwrite default: attribute values can be either a string or {type;, value:}.
|
|
// 1) string attr val: render in e"..." format so HTML entities can be decoded.
|
|
// 2) object attr val: render value as is.
|
|
function render_token_attrs(token) {
|
|
var i, l, result;
|
|
|
|
if(!token.attrs) { return ''; }
|
|
|
|
result = '';
|
|
|
|
for(i=0, l=token.attrs.length; i<l; i++) {
|
|
if(typeof token.attrs[i][1] === "object" && token.attrs[i][1] !== null) {
|
|
result += ' ' + md.utils.escapeHtml(token.attrs[i][0]) + '=' + token.attrs[i][1].value;
|
|
} else {
|
|
result += ' ' + md.utils.escapeHtml(token.attrs[i][0]) + '=e"' + md.utils.escapeHtml(token.attrs[i][1]) + '"';
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// given tw parsing rule and starting pos, returns match index or undefined
|
|
// assumes pos >= 0
|
|
function findNextMatch(ruleinfo,pos) {
|
|
// ruleinfo.matchIndex needs to be -1 at the start of inline state
|
|
if(ruleinfo.matchIndex < pos) {
|
|
ruleinfo.matchIndex = ruleinfo.rule.findNextMatch(pos);
|
|
}
|
|
|
|
return ruleinfo.matchIndex;
|
|
}
|
|
|
|
// Add inline rule "macrocall" to parse <<macroname ...>>
|
|
var MacroCallRegEx = /<<([^\s>"'=]+)[^>]*>>/g;
|
|
function tw_macrocallinline(state,silent) {
|
|
var match, max, pos = state.pos;
|
|
|
|
// Check start
|
|
max = state.posMax;
|
|
if(state.src.charCodeAt(pos) !== 0x3C || state.src.charCodeAt(pos+1) !== 0x3C /* << */|| pos + 3 >= max) {
|
|
return false;
|
|
}
|
|
|
|
MacroCallRegEx.lastIndex = pos;
|
|
match = MacroCallRegEx.exec(state.src);
|
|
if(!match || match.index !== pos) { return false; }
|
|
|
|
if(!silent) {
|
|
var token = state.push('tw_expr','',0);
|
|
token.content = state.src.slice(pos,pos+match[0].length);
|
|
}
|
|
state.pos = MacroCallRegEx.lastIndex;
|
|
return true;
|
|
}
|
|
|
|
// parse transclusion elements
|
|
function tw_transcludeinline(state,silent) {
|
|
var ruleinfo = pluginOpts.inlineRules.transcludeinline;
|
|
|
|
var pos = state.pos;
|
|
var matchIndex = findNextMatch(ruleinfo,pos);
|
|
if(matchIndex === undefined || matchIndex !== pos) {
|
|
return false;
|
|
}
|
|
|
|
if(!silent) {
|
|
var token = state.push('tw_expr','',0);
|
|
token.content = state.src.slice(pos,pos+ruleinfo.rule.match[0].length);
|
|
}
|
|
state.pos += ruleinfo.rule.match[0].length;
|
|
return true;
|
|
}
|
|
|
|
// parse filtered transclusion elements
|
|
function tw_filteredtranscludeinline(state,silent) {
|
|
var ruleinfo = pluginOpts.inlineRules.filteredtranscludeinline;
|
|
|
|
var pos = state.pos;
|
|
var matchIndex = findNextMatch(ruleinfo,pos);
|
|
if(matchIndex === undefined || matchIndex !== pos) {
|
|
return false;
|
|
}
|
|
|
|
if(!silent) {
|
|
var token = state.push('tw_expr','',0);
|
|
if(state.linkLevel > 0) {
|
|
var filter = ruleinfo.rule.match[1];
|
|
token.content = '<$text text={{{' + filter + '}}}/>';
|
|
} else {
|
|
token.content = state.src.slice(pos,pos+ruleinfo.rule.match[0].length);
|
|
}
|
|
}
|
|
state.pos += ruleinfo.rule.match[0].length;
|
|
return true;
|
|
}
|
|
|
|
// based on markdown-it html_block()
|
|
var WidgetTagRegEx = [/^<\/?\$[a-zA-Z0-9\-\$\.]+(?=(\s|\/?>|$))/, /^$/];
|
|
function tw_block(state,startLine,endLine,silent) {
|
|
var i, nextLine, token, lineText,
|
|
pos = state.bMarks[startLine] + state.tShift[startLine],
|
|
max = state.eMarks[startLine];
|
|
|
|
// if it's indented more than 3 spaces, it should be a code block
|
|
if(state.sCount[startLine] - state.blkIndent >= 4) { return false; }
|
|
|
|
if(!state.md.options.html) { return false; }
|
|
|
|
if(state.src.charCodeAt(pos) !== 0x3C/* < */) { return false; }
|
|
|
|
lineText = state.src.slice(pos,max);
|
|
|
|
if(!WidgetTagRegEx[0].test(lineText)) { return false; }
|
|
|
|
if(silent) {
|
|
// don't let widgets interrupt a paragrpah
|
|
return false;
|
|
}
|
|
|
|
nextLine = startLine + 1;
|
|
|
|
// If we are here - we detected HTML block.
|
|
// Let's roll down till block end.
|
|
if(!WidgetTagRegEx[1].test(lineText)) {
|
|
for(; nextLine < endLine; nextLine++) {
|
|
if(state.sCount[nextLine] < state.blkIndent) { break; }
|
|
|
|
pos = state.bMarks[nextLine] + state.tShift[nextLine];
|
|
max = state.eMarks[nextLine];
|
|
lineText = state.src.slice(pos,max);
|
|
|
|
if(WidgetTagRegEx[1].test(lineText)) {
|
|
if(lineText.length !== 0) { nextLine++; }
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
state.line = nextLine;
|
|
|
|
token = state.push('html_block','',0);
|
|
token.map = [ startLine, nextLine ];
|
|
token.content = state.getLines(startLine,nextLine,state.blkIndent,true);
|
|
|
|
return true;
|
|
}
|
|
|
|
// parse [img[...]] elements
|
|
function tw_image(state,silent) {
|
|
var ruleinfo = pluginOpts.inlineRules.image;
|
|
|
|
// ignore at parseLinkLabel stage; will be recognized in tokenize()
|
|
if(state.parsingLinkLabel > 0) {
|
|
return false;
|
|
}
|
|
|
|
var pos = state.pos;
|
|
var matchIndex = findNextMatch(ruleinfo,pos);
|
|
if(matchIndex === undefined || matchIndex !== pos) {
|
|
return false;
|
|
}
|
|
|
|
if(!silent) {
|
|
var twNode = ruleinfo.rule.parse()[0];
|
|
var token = state.push('$image','$image',0);
|
|
$tw.utils.each(twNode.attributes,function(attr,id) {
|
|
switch(attr.type) {
|
|
case "filtered":
|
|
token.attrSet(id,{ type: "filtered", value: "{{{" + attr.filter + "}}}" });
|
|
break;
|
|
case "indirect":
|
|
token.attrSet(id,{ type: "indirect", value: "{{" + attr.textReference + "}}" });
|
|
break;
|
|
case "macro":
|
|
token.attrSet(id,{ type: "macro", value: ruleinfo.rule.parser.source.substring(attr.value.start,attr.value.end) });
|
|
break;
|
|
default:
|
|
token.attrSet(id,attr.value);
|
|
}
|
|
});
|
|
token.markup = 'tw_image';
|
|
}
|
|
state.pos = ruleinfo.rule.parser.pos;
|
|
return true;
|
|
}
|
|
|
|
// parse [[link]] elements
|
|
function tw_prettylink(state,silent) {
|
|
var ruleinfo = pluginOpts.inlineRules.prettylink;
|
|
|
|
// skip if in link label
|
|
if(state.linkLevel > 0 || state.parsingLinkLabel > 0) {
|
|
return false;
|
|
}
|
|
|
|
var pos = state.pos;
|
|
var matchIndex = findNextMatch(ruleinfo,pos);
|
|
if(matchIndex === undefined || matchIndex !== pos) {
|
|
return false;
|
|
}
|
|
|
|
if(!silent) {
|
|
var twNode = ruleinfo.rule.parse()[0];
|
|
var tag = (twNode.type==='link' ? '$link' : 'a');
|
|
// push a link_open token so markdown's core.linkify will ignore
|
|
var token = state.push('link_open',tag,1);
|
|
|
|
$tw.utils.each(twNode.attributes,function(attr,id) {
|
|
token.attrSet(id,attr.value);
|
|
});
|
|
token.attrJoin('class','_codified_');
|
|
token.markup = 'tw_prettylink';
|
|
|
|
state.linkLevel++;
|
|
token = state.push('text','',0);
|
|
token.content = twNode.children[0].text;
|
|
state.linkLevel--;
|
|
|
|
token = state.push('link_close',tag,-1);
|
|
token.markup = 'tw_prettylink';
|
|
}
|
|
state.pos = ruleinfo.rule.parser.pos;
|
|
return true;
|
|
}
|
|
|
|
function tw_prettyextlink(state,silent) {
|
|
var ruleinfo = pluginOpts.inlineRules.prettyextlink;
|
|
|
|
// skip if in link label
|
|
if(state.linkLevel > 0 || state.parsingLinkLabel > 0) {
|
|
return false;
|
|
}
|
|
|
|
var pos = state.pos;
|
|
var matchIndex = findNextMatch(ruleinfo,pos);
|
|
if(matchIndex === undefined || matchIndex !== pos) {
|
|
return false;
|
|
}
|
|
|
|
if(!silent) {
|
|
var twNode = ruleinfo.rule.parse()[0];
|
|
var token = state.push('link_open','a',1);
|
|
|
|
$tw.utils.each(twNode.attributes,function(attr,id) {
|
|
token.attrSet(id,attr.value);
|
|
});
|
|
token.attrJoin('class','_codified_');
|
|
token.markup = 'tw_prettyextlink';
|
|
|
|
state.linkLevel++;
|
|
token = state.push('text','',0);
|
|
token.content = twNode.children[0].text;
|
|
state.linkLevel--;
|
|
|
|
token = state.push('link_close','a',-1);
|
|
token.markup = 'tw_prettyextlink';
|
|
}
|
|
state.pos = ruleinfo.rule.parser.pos;
|
|
return true;
|
|
}
|
|
|
|
var TWCloseTagRegEx = /<\/\$[A-Za-z0-9\-\$\.]+\s*>/gm;
|
|
function extendHtmlInline(origRule) {
|
|
return function(state,silent) {
|
|
if(origRule(state,silent)) {
|
|
return true;
|
|
}
|
|
|
|
var token, pos = state.pos;
|
|
var parseTag = $tw.Wiki.parsers['text/vnd.tiddlywiki'].prototype.inlineRuleClasses.html.prototype.parseTag;
|
|
var tag = parseTag(state.src,pos,{});
|
|
if(tag) {
|
|
if(!silent) {
|
|
token = state.push('html_inline','',0);
|
|
token.content = state.src.slice(pos,tag.end);
|
|
}
|
|
state.pos = tag.end;
|
|
return true;
|
|
}
|
|
|
|
TWCloseTagRegEx.lastIndex = pos;
|
|
var match = TWCloseTagRegEx.exec(state.src);
|
|
if(!match || match.index !== pos) {
|
|
return false;
|
|
}
|
|
|
|
if(!silent) {
|
|
token = state.push('html_inline','',0);
|
|
token.content = state.src.slice(pos,pos + match[0].length);
|
|
}
|
|
state.pos = TWCloseTagRegEx.lastIndex;
|
|
return true;
|
|
};
|
|
}
|
|
|
|
function extendParseLinkLabel(origFunc) {
|
|
return function(state,start,disableNested) {
|
|
if(state.parsingLinkLabel === undefined) {
|
|
state.parsingLinkLabel = 0;
|
|
}
|
|
state.parsingLinkLabel++;
|
|
var labelEnd = origFunc(state,start,disableNested);
|
|
state.parsingLinkLabel--;
|
|
return labelEnd;
|
|
};
|
|
}
|
|
|
|
// reset each tw inline rule to initial inline state
|
|
function extendInlineParse(thisArg,origFunc,twInlineRules) {
|
|
return function(str,md,env,outTokens) {
|
|
var i, ruleinfo, key;
|
|
for(key in twInlineRules) {
|
|
ruleinfo = twInlineRules[key];
|
|
ruleinfo.rule.parser.source = str;
|
|
ruleinfo.rule.parser.sourceLength = str.length;
|
|
ruleinfo.rule.parser.pos = 0; // not used
|
|
ruleinfo.matchIndex = -1;
|
|
}
|
|
origFunc.call(thisArg,str,md,env,outTokens);
|
|
}
|
|
}
|
|
|
|
/// post processing ///
|
|
|
|
function wikify(state) {
|
|
var href, title, src, alt;
|
|
var tagStack = [];
|
|
|
|
state.tokens.forEach(function(blockToken) {
|
|
if(blockToken.type === 'inline' && blockToken.children) {
|
|
blockToken.children.forEach(function(token) {
|
|
switch(token.type) {
|
|
case 'link_open':
|
|
if(token.markup === 'tw_prettylink' || token.markup === 'tw_prettyextlink') {
|
|
return;
|
|
}
|
|
href = token.attrGet('href');
|
|
if(href[0] === '#') {
|
|
token.tag = '$link';
|
|
href = $tw.utils.decodeURIComponentSafe(href.substring(1));
|
|
title = token.attrGet('title');
|
|
token.attrs = [['to', href], ['class', '_codified_']];
|
|
if(title) {
|
|
token.attrSet('tooltip',title);
|
|
}
|
|
} else {
|
|
token.attrSet('target','_blank');
|
|
token.attrJoin('class','tc-tiddlylink-external');
|
|
token.attrJoin('class','_codified_');
|
|
token.attrSet('rel','noopener noreferrer');
|
|
}
|
|
tagStack.push(token.tag);
|
|
break;
|
|
case 'link_close':
|
|
if(token.markup === 'tw_prettylink' || token.markup === 'tw_prettyextlink') {
|
|
return;
|
|
}
|
|
token.tag = tagStack.pop();
|
|
break;
|
|
case 'image':
|
|
token.tag = '$image';
|
|
src = token.attrGet('src');
|
|
alt = token.attrGet('alt');
|
|
title = token.attrGet('title');
|
|
|
|
token.attrs[token.attrIndex('src')][0] = 'source';
|
|
if(src[0] === '#') {
|
|
src = $tw.utils.decodeURIComponentSafe(src.substring(1));
|
|
token.attrSet('source',src);
|
|
}
|
|
if(title) {
|
|
token.attrs[token.attrIndex('title')][0] = 'tooltip';
|
|
}
|
|
break;
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
module.exports = function tiddlyWikiPlugin(markdown,options) {
|
|
var defaults = {
|
|
renderWikiText: false,
|
|
blockRules: {},
|
|
inlineRules: {}
|
|
};
|
|
|
|
md = markdown;
|
|
pluginOpts = md.utils.assign({},defaults,options||{});
|
|
|
|
md.renderer.rules.code_inline = render_code_inline;
|
|
md.renderer.rules.code_block = render_code_block;
|
|
md.renderer.rules.fence = render_fence;
|
|
md.renderer.rules.paragraph_open = render_paragraph_open;
|
|
md.renderer.rules.paragraph_close = render_paragraph_close;
|
|
md.renderer.rules.footnote_ref = render_footnote_ref;
|
|
md.renderer.rules.footnote_open = render_footnote_open;
|
|
md.renderer.rules.footnote_anchor = render_footnote_anchor;
|
|
md.renderer.rules.text_special = render_text_special;
|
|
md.renderer.rules.tw_expr = render_tw_expr;
|
|
md.renderer.renderAttrs = render_token_attrs;
|
|
|
|
if(pluginOpts.renderWikiText) {
|
|
md.helpers.parseLinkLabel = extendParseLinkLabel(md.helpers.parseLinkLabel);
|
|
|
|
if(pluginOpts.inlineRules.image) {
|
|
md.inline.ruler.after('link','tw_image',tw_image);
|
|
}
|
|
if(pluginOpts.inlineRules.prettyextlink) {
|
|
md.inline.ruler.after('link','tw_prettyextlink',tw_prettyextlink);
|
|
}
|
|
if(pluginOpts.inlineRules.prettylink) {
|
|
md.inline.ruler.after('link','tw_prettylink',tw_prettylink);
|
|
}
|
|
if(pluginOpts.inlineRules.filteredtranscludeinline) {
|
|
md.inline.ruler.before('html_inline','tw_filteredtranscludeinline',tw_filteredtranscludeinline);
|
|
}
|
|
if(pluginOpts.inlineRules.transcludeinline) {
|
|
md.inline.ruler.before('html_inline','tw_transcludeinline',tw_transcludeinline);
|
|
}
|
|
|
|
md.inline.ruler.before('html_inline','tw_macrocallinline',tw_macrocallinline);
|
|
md.inline.ruler.at('html_inline',extendHtmlInline(md.inline.ruler.__rules__[md.inline.ruler.__find__('html_inline')].fn));
|
|
md.block.ruler.after('html_block','tw_block',tw_block,{
|
|
alt: [ 'paragraph', 'reference', 'blockquote' ]
|
|
});
|
|
md.inline.parse = extendInlineParse(md.inline,md.inline.parse,options.inlineRules);
|
|
}
|
|
|
|
md.core.ruler.disable('text_join');
|
|
md.core.ruler.push('wikify',wikify);
|
|
};
|
|
|
|
})();
|