From 4dd58aaad3e09fad05fbb0e84e9f8fa71226f291 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Thu, 21 Dec 2023 17:13:13 -0500 Subject: [PATCH] Ugly ugly initial commit --- client/homebrew/brewRenderer/brewRenderer.jsx | 2 +- shared/naturalcrit/markdown.js | 337 +++++++++++++++++- 2 files changed, 333 insertions(+), 6 deletions(-) diff --git a/client/homebrew/brewRenderer/brewRenderer.jsx b/client/homebrew/brewRenderer/brewRenderer.jsx index fde91fc68..9d3ffe81f 100644 --- a/client/homebrew/brewRenderer/brewRenderer.jsx +++ b/client/homebrew/brewRenderer/brewRenderer.jsx @@ -134,7 +134,7 @@ const BrewRenderer = (props)=>{ return ; } else { cleanPageText += `\n\n \n\\column\n `; //Artificial column break at page end to emulate column-fill:auto (until `wide` is used, when column-fill:balance will reappear) - const html = Markdown.render(cleanPageText); + const html = Markdown.render(cleanPageText, index); return ; } }; diff --git a/shared/naturalcrit/markdown.js b/shared/naturalcrit/markdown.js index 9a52a82b0..e0ed048ad 100644 --- a/shared/naturalcrit/markdown.js +++ b/shared/naturalcrit/markdown.js @@ -5,6 +5,7 @@ const MarkedExtendedTables = require('marked-extended-tables'); const { markedSmartypantsLite: MarkedSmartypantsLite } = require('marked-smartypants-lite'); const { gfmHeadingId: MarkedGFMHeadingId } = require('marked-gfm-heading-id'); const renderer = new Marked.Renderer(); +const tokenizer = new Marked.Tokenizer(); //Processes the markdown within an HTML block if it's just a class-wrapper renderer.html = function (html) { @@ -28,6 +29,11 @@ renderer.paragraph = function(text){ return `

${text}

\n`; }; +//TODO: may not be needed +// Disable default reflink definitions +tokenizer.def = function(){ +} + const mustacheSpans = { name : 'mustacheSpans', level : 'inline', // Is this a block-level or inline-level tokenizer? @@ -266,9 +272,327 @@ const definitionLists = { } }; -Marked.use({ extensions: [mustacheSpans, mustacheDivs, mustacheInjectInline, definitionLists, superSubScripts] }); + +//v=====--------------------< Variable Handling >-------------------=====v// +const consumeBlockDef = { + name : 'consumeBlockDef', + level : 'block', + start(src) {return src.match(/^ {0,3}[!$]?\[(?!\s*\])(?:\\.|[^\[\]\\])+\]:/m)?.index; }, + tokenizer(src, tokens) { + // [ variable name (spaces allowed) ]: Any text, including into newlines (but no fully blank lines) + const regex = /^ {0,3}[!$]?\[((?!\s*\])(?:\\.|[^\[\]\\])+)\]: *((?:\n? *[^\s].*)+)(?=\n+|$)/; + const match = regex.exec(src); + if(match) { + return { + type : 'consumeBlockDef', + raw : match[0], + tokens : this.lexer.inline(`\n${match[0]}`), + }; + } + }, + renderer(token){ + return this.parser.parseInline(token.tokens); + } +}; + +const refLinkBlockDef = { + name : 'refLinkBlockDef', + level : 'inline', + start(src) {return src.match(/^\n {0,3}[!$]?\[(?!\s*\])(?:\\.|[^\[\]\\])+\]:/m)?.index; }, + tokenizer(src, tokens) { + // [ variable name (spaces allowed) ]: Any text, including into newlines (but no fully blank lines) + const regex = /^\n {0,3}[!$]?\[((?!\s*\])(?:\\.|[^\[\]\\])+)\]: *((?:\n? *[^\s].*)+)(?=\n+|$)/; + const match = regex.exec(src); + if(match) { + const label = match[1] ? match[1].trim().toLowerCase().replace(/\s+/g, ' ') : null; // Trim edge spaces and shorten blocks of whitespace to 1 space + const content = match[2] ? match[2].trim().toLowerCase().replace(/[ \t]+/g, ' ') : null; // Trim edge spaces and shorten blocks of whitespace to 1 space + + // url or "title" or 'title' or (title) + const linkRegex = /^([^<\s][^\s]*|<.*?>)(?: ("(?:\\"|[^"])*"|'(?:\\'|[^'])*'|\((?:\\\(|\\\)|[^()])*\)))?$/m + const linkMatch = linkRegex.exec(content); + + let href = null; + let title = null; + if(linkMatch) { + href = linkMatch[1]; + title = linkMatch[2]; + } + + if(!globalLinks[globalPageNumber]) + globalLinks[globalPageNumber] = {}; + + globalLinks[globalPageNumber][label] = { + tokens : this.lexer.blockTokens(content), + content : content, + href : href, + title : title + } + + return { + type : 'refLinkBlockDef', + raw : match[0], + tokens : this.lexer.blockTokens(content), + label : label, + href : href, + title : title + }; + } + }, + renderer(token){ + return; + } +}; + +const consumeBlockCall = { + name : 'consumeBlockCall', + level : 'block', + start(src) {return src.match(/^[!$]?\[(?!\s*\])(?:\\.|[^\[\]\\])+\]/m)?.index; }, + tokenizer(src, tokens) { + // [ variable name (spaces allowed) ] if only this and whitespace, parse var contents as block, else as inline + const regex = /^[!$]?\[((?!\s*\])(?:\\.|[^\[\]\\])+)\][ \t]*(?=\n|$)/; + const match = regex.exec(src); + if(match) { + return { + type : 'consumeBlockCall', + raw : match[0], + tokens : this.lexer.inline(`\n${match[0]}`), //Append \n so refLinkCall can distinguish between block and inline + }; + } + }, + renderer(token){ + return this.parser.parseInline(token.tokens); + } +}; + +const refLinkCall = { + name : 'refLinkCall', + level : 'inline', + start(src) {return src.match(/\n?[!$]?\[(?!\s*\])(?:\\.|[^\[\]\\])+\]/m)?.index;}, + tokenizer(src, tokens) { + // [ variable name (spaces allowed) ]: Any text, including into newlines (but no fully blank lines) + const regex = /^\n?([!$]?)\[((?!\s*\])(?:\\.|[^\[\]\\])+)\]/; + const match = regex.exec(src); + if(match) { + const label = match[2] ? match[2].trim().toLowerCase().replace(/\s+/g, ' ') : null; // Trim edge spaces and shorten blocks of whitespace to 1 space + + const variable = lookupVar(label, globalPageNumber); + + if (variable == undefined) + return; + + let tokens; + let block; + + if(match[0][0] == '\n') { + block = true; + tokens = this.lexer.blockTokens(variable.content); + } + else { + block = false; + tokens = this.lexer.inlineTokens(variable.content); + } + + let format = "link"; + if(match[1] == "!") + format = "image"; + if(match[1] == "$") + format = "variable"; + + return { + type : 'refLinkCall', + label : label, + raw : match[0], + tokens : tokens, + href : variable.href, + title : variable.title, + block : block, + format : format + } + } + }, + renderer(token){ + if(token.format == "link" && token.href) { + return `${token.label}`; + } + if(token.format == "image" && token.href) { + return ``; + } + if(token.format == "variable") { + if(token.block) + return this.parser.parse(token.tokens); + else + return this.parser.parseInline(token.tokens); + } + } +}; + +const refLinkInlineDef = { + name : 'refLinkInlineDef', + level : 'inline', + start(src) {return src.match(/\n?[!$]?\[(?!\s*\])(?:\\.|[^\[\]\\])+\]:\(.*\)/m)?.index;}, + tokenizer(src, tokens) { + // [ variable name (spaces allowed) ]: Any text, including into newlines (but no fully blank lines) + const regex = /^\n?([!$]?)\[((?!\s*\])(?:\\.|[^\[\]\\])+)\]:\((.*)\)/; + const match = regex.exec(src); + if(match) { + const label = match[2] ? match[2].trim().toLowerCase().replace(/\s+/g, ' ') : null; // Trim edge spaces and shorten blocks of whitespace to 1 space + const content = match[3] ? match[3].trim().toLowerCase().replace(/[ \t]+/g, ' ') : null; // Trim edge spaces and shorten blocks of whitespace to 1 space + + // url or "title" or 'title' or (title) + const linkRegex = /^([^<\s][^\s]*|<.*?>)(?: ("(?:\\"|[^"])*"|'(?:\\'|[^'])*'|\((?:\\\(|\\\)|[^()])*\)))?$/m + const linkMatch = linkRegex.exec(content); + + let href = null; + let title = null; + if(linkMatch) { + href = linkMatch[1]; + title = linkMatch[2]; + } + + if(!globalLinks[globalPageNumber]) + globalLinks[globalPageNumber] = {}; + + globalLinks[globalPageNumber][label] = { + tokens : this.lexer.blockTokens(content), + content : content, + href : href, + title : title + } + + const variable = lookupVar(label, globalPageNumber); + + if (variable == undefined) + return; + + const tokens = this.lexer.inlineTokens(variable.content); + + let format = "link"; + if(match[1] == "!") + format = "image"; + if(match[1] == "$") + format = "variable"; + + return { + type : 'refLinkCall', + label : label, + raw : match[0], + tokens : tokens, + href : variable.href, + title : variable.title, + block : false, + format : format + } + } + }, + renderer(token){ + return; + } +}; + +const refLinkInlineNoDef = { + name : 'refLinkInlineNoDef', + level : 'inline', + start(src) {return src.match(/[!$]?\[(?!\s*\])(?:\\.|[^\[\]\\])+\]\(.*\)/m)?.index; }, + tokenizer(src, tokens) { + // [ variable name (spaces allowed) ]: Any text, including into newlines (but no fully blank lines) + const regex = /^([!$]?)\[((?!\s*\])(?:\\.|[^\[\]\\])+)\]\((.*)\)/; + const match = regex.exec(src); + if(match) { + const label = match[2] ? match[2].trim().toLowerCase().replace(/\s+/g, ' ') : null; // Trim edge spaces and shorten blocks of whitespace to 1 space + const content = match[3] ? match[3].trim().toLowerCase().replace(/[ \t]+/g, ' ') : null; // Trim edge spaces and shorten blocks of whitespace to 1 space + + // url or "title" or 'title' or (title) + const linkRegex = /^([^<\s][^\s]*|<.*?>)(?: ("(?:\\"|[^"])*"|'(?:\\'|[^'])*'|\((?:\\\(|\\\)|[^()])*\)))?$/m + const linkMatch = linkRegex.exec(content); + + let href = null; + let title = null; + if(linkMatch) { + href = linkMatch[1]; + title = linkMatch[2]; + } + + const tokens = this.lexer.inlineTokens(content); + + let format = "link"; + if(match[1] == "!") + format = "image"; + if(match[1] == "$") + format = "variable"; + + return { + type : 'refLinkCall', + label : label, + raw : match[0], + tokens : tokens, + href : href, + title : title, + block : false, + format : format + } + } + }, + renderer(token){ + return; + } +}; + +const lookupVar = function(label, index) { + if (globalLinks[index] === undefined) { + return undefined; + } + + while (index >= 0) { + if (globalLinks[index][label] !== undefined) { + return globalLinks[index][label]; + } + index--; + } + + return undefined; +}; + +const generateVarToken = function(firstChar, label, content, href, title) { + + // if(firstChar == "$") { + // return { + // type : 'refLinkCall', + // label : label, + // raw : match[0], + // tokens : tokens, + // href : href, + // title : title, + // block : false, + // format : "variable" + // } + // } + + + + // let format = "link"; + // if(firstChar == "!") + // format = "image"; + + + + // const tokens = this.lexer.inlineTokens(content); + + // return { + // type : 'refLinkCall', + // label : label, + // raw : match[0], + // tokens : tokens, + // href : href, + // title : title, + // block : false, + // format : format + // } +}; +//^=====--------------------< Variable Handling >-------------------=====^// + +Marked.use({ extensions: [mustacheSpans, mustacheDivs, mustacheInjectInline, definitionLists, superSubScripts, refLinkCall, refLinkBlockDef, consumeBlockCall, refLinkInlineDef, refLinkInlineNoDef, consumeBlockDef]}); Marked.use(mustacheInjectBlock); -Marked.use({ renderer: renderer, mangle: false }); +Marked.use({ renderer: renderer, tokenizer: tokenizer, mangle: false }); Marked.use(MarkedExtendedTables(), MarkedGFMHeadingId(), MarkedSmartypantsLite()); //Fix local links in the Preview iFrame to link inside the frame @@ -369,18 +693,21 @@ const processStyleTags = (string)=>{ `${attributes?.length ? ` ${attributes.join(' ')}` : ''}`; }; -let globalLinks = {}; +let globalLinks = {}; +let globalPageNumber = 0; module.exports = { marked : Marked, - render : (rawBrewText)=>{ + render : (rawBrewText, pageNumber=1)=>{ + globalLinks[pageNumber] = {}; //Reset global links for current page, to ensure values are parsed in order + globalPageNumber = pageNumber; + rawBrewText = rawBrewText.replace(/^\\column$/gm, `\n
\n`) .replace(/^(:+)$/gm, (match)=>`${`
`.repeat(match.length)}\n`); const opts = Marked.defaults; rawBrewText = opts.hooks.preprocess(rawBrewText); const tokens = Marked.lexer(rawBrewText, opts); - globalLinks = Object.assign({}, tokens.links); Marked.walkTokens(tokens, opts.walkTokens); const html = Marked.parser(tokens, opts); return opts.hooks.postprocess(html);