From d7756230fb3dc58ac27138d9dbe5a9c6f7d49d53 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Thu, 14 Mar 2024 00:02:18 -0400 Subject: [PATCH] More tests. Split into two extensions Split into two extensions as single-line and multiline are different syntaxes. Simplified a lot of logic and probably cleaner as their own NPM packages (eventually). --- shared/naturalcrit/markdown.js | 153 ++++++++--------------- tests/markdown/marked-extensions.test.js | 44 +++++-- 2 files changed, 86 insertions(+), 111 deletions(-) diff --git a/shared/naturalcrit/markdown.js b/shared/naturalcrit/markdown.js index 4bc14e19a..01fdbac96 100644 --- a/shared/naturalcrit/markdown.js +++ b/shared/naturalcrit/markdown.js @@ -294,117 +294,74 @@ const superSubScripts = { } }; -const definitionLists = { - name : 'definitionLists', // Block because style display: block +const definitionListsInline = { + name : 'definitionListsInline', level : 'block', - start(src) { return src.match(/^.*?::.*\n\n/m)?.index; }, // Hint to Marked.js to stop and check for a match + start(src) { return src.match(/^[^\n]*?::[^\n]*/m)?.index; }, // Hint to Marked.js to stop and check for a match tokenizer(src, tokens) { - const regex = /^([^\n]*?:?)\n?::(.*)(?:\n|$)|^.*(?:\n|$)/ym; + const regex = /^([^\n]*?)::([^\n]*)(?:\n|$)/ym; let match; let endIndex = 0; - const allDefinitions = []; - let currentDefinition = {}; - let inList = false; - let lastEmpty = false; - let inlineDefinitions = false; - let appending = false; + const definitions = []; while (match = regex.exec(src)) { - // If we aren't actively in a DL and we match just text, bail. - // If the last loop bailed with lastEmpty and we match just text, bail - if(((!inList) || (lastEmpty)) && (typeof match[1] == 'undefined') && (typeof match[2] == 'undefined')) { - break; - } - endIndex += match[0].length; - - // Check to see if this a match containing the start of a DD. - if(match[0].indexOf('::') > -1) { - inList = true; - // Check and see if we are currently in line appending mode, if so, match[1] should be - // appended to the last entry instead of being used as the next DT. - if(appending) { - const lastPos = typeof currentDefinition.dd.length !== 'undefined' ? currentDefinition.dd.length - 1 : 0; - currentDefinition.dd[lastPos] = `${currentDefinition.dd[lastPos]} ${match[1]?.trim()}`; - match[1] = ''; - } - appending = false; - // Check for a DT value. - if(match[1]?.trim()?.length>0) { - if(currentDefinition?.dt?.length) { - currentDefinition.dd.forEach((dd)=>{ - currentDefinition.ddo.push(this.lexer.inlineTokens(dd)); - }); - allDefinitions.push(currentDefinition); - currentDefinition = {}; - } - currentDefinition = { - dt : this.lexer.inlineTokens(match[1].trim()), - dd : [], - ddo : [] - }; - } else if(_.isEmpty(currentDefinition)) { - return; - } - // Test for a DD value. - if(match[2].trim().length>0) { - if((match[1]?.length > 0) && (match[0].indexOf('\n') > match[1]?.length)) { // Inline Style DD - currentDefinition.dd = currentDefinition.dd.concat([match[2].trim()]); - currentDefinition.dd.forEach((dd)=>{ - currentDefinition.ddo.push(this.lexer.inlineTokens(dd)); - }); - allDefinitions.push(currentDefinition); - inlineDefinitions = true; - currentDefinition = {}; - continue; - } - // Multi-line style DDs - const newDefinitions = _.flatten(match[2].split('\n::').filter((item)=>item).map((s)=>s.trim())); - if(newDefinitions?.length) { - currentDefinition.dd = currentDefinition.dd.concat(newDefinitions); - } - } else { - currentDefinition.dd.push(''); - } - lastEmpty = false; - } else if(inList) { // Regular line that might mark the end of a line. - appending = false; - if(inlineDefinitions) { - endIndex -= match[0].length; - break; - } - if(match[0].trim().length == 0) { - if(lastEmpty) { - continue; - } else { - lastEmpty = true; - } - } else { - lastEmpty = false; - const lastPos = typeof currentDefinition.dd.length !== 'undefined' ? currentDefinition.dd.length - 1 : 0; - currentDefinition.dd[lastPos] = `${currentDefinition.dd[lastPos]} ${match[0].trim()}`; - appending = true; - } - } - } - if(currentDefinition.hasOwnProperty('dt')) { - currentDefinition.dd.forEach((dd)=>{ - currentDefinition.ddo.push(this.lexer.inlineTokens(dd)); + definitions.push({ + dt : this.lexer.inlineTokens(match[1].trim()), + dd : this.lexer.inlineTokens(match[2].trim()) }); - allDefinitions.push(currentDefinition); + endIndex = regex.lastIndex; } - if(allDefinitions.length) { + if(definitions.length) { return { - type : 'definitionLists', - raw : src.slice(0, endIndex), - inlineDefinitions, - definitions : allDefinitions + type : 'definitionListsInline', + raw : src.slice(0, endIndex), + definitions + }; + } + }, + renderer(token) { + return `
${token.definitions.reduce((html, def)=>{ + return `${html}
${this.parser.parseInline(def.dt)}
` + + `
${this.parser.parseInline(def.dd)}
`; + }, '')}
`; + } +}; + +const definitionListsMultiline = { + name : 'definitionListsMultiline', + level : 'block', + start(src) { return src.match(/^[^\n]*\n::/m)?.index; }, // Hint to Marked.js to stop and check for a match + tokenizer(src, tokens) { + const regex = /(\n?\n?(?!::)[^\n]+?(?=\n::))|\n::(.(?:.|\n)*?(?=(?:\n::)|(?:\n\n)|$))/y; + let match; + let endIndex = 0; + const definitions = []; + while (match = regex.exec(src)) { + if(match[1]) { + definitions.push({ + dt : this.lexer.inlineTokens(match[1].trim()), + dds : [] + }); + } + if(match[2]) { + definitions[definitions.length - 1].dds.push( + this.lexer.inlineTokens(match[2].trim().replace(/\s/g,' ')) + ) + } + endIndex = regex.lastIndex; + } + if(definitions.length) { + return { + type : 'definitionListsMultiline', + raw : src.slice(0, endIndex), + definitions }; } }, renderer(token) { let returnVal = `
`; token.definitions.forEach((def)=>{ - let dds = def.ddo.map((s)=>{ - return `${token.inlineDefinitions ? '' : '\n'}
${this.parser.parseInline(s).trim()}
`; + let dds = def.dds.map((s)=>{ + return `\n
${this.parser.parseInline(s).trim()}
`; }).join(''); returnVal += `
${this.parser.parseInline(def.dt)}
${dds}\n`; }); @@ -658,7 +615,7 @@ function MarkedVariables() { //^=====--------------------< Variable Handling >-------------------=====^// Marked.use(MarkedVariables()); -Marked.use({ extensions: [mustacheSpans, mustacheDivs, mustacheInjectInline, definitionLists, superSubScripts] }); +Marked.use({ extensions: [mustacheSpans, mustacheDivs, mustacheInjectInline, definitionListsInline, definitionListsMultiline, superSubScripts] }); Marked.use(mustacheInjectBlock); Marked.use({ renderer: renderer, tokenizer: tokenizer, mangle: false }); Marked.use(MarkedExtendedTables(), MarkedGFMHeadingId(), MarkedSmartypantsLite()); diff --git a/tests/markdown/marked-extensions.test.js b/tests/markdown/marked-extensions.test.js index b4e5dee5c..8d6c3c1c4 100644 --- a/tests/markdown/marked-extensions.test.js +++ b/tests/markdown/marked-extensions.test.js @@ -3,59 +3,77 @@ const Markdown = require('naturalcrit/markdown.js'); describe('Inline Definition Lists', ()=>{ + test('No Term 1 Definition', function() { + const source = ':: My First Definition\n\n'; + const rendered = Markdown.render(source).trim(); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('
My First Definition
'); + }); + test('Single Definition Term', function() { const source = 'My term :: My First Definition\n\n'; - const rendered = Markdown.render(source); + const rendered = Markdown.render(source).trim(); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('
My term
My First Definition
'); }); test('Multiple Definition Terms', function() { const source = 'Term 1::Definition of Term 1\nTerm 2::Definition of Term 2\n\n'; - const rendered = Markdown.render(source); - expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('
Term 1
Definition of Term 1
\n
Term 2
Definition of Term 2
'); + const rendered = Markdown.render(source).trim(); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('
Term 1
Definition of Term 1
Term 2
Definition of Term 2
'); }); }); describe('Multiline Definition Lists', ()=>{ test('Single Term, Single Definition', function() { const source = 'Term 1\n::Definition 1\n\n'; - const rendered = Markdown.render(source); + const rendered = Markdown.render(source).trim(); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('
Term 1
\n
Definition 1
'); }); test('Single Term, Plural Definitions', function() { const source = 'Term 1\n::Definition 1\n::Definition 2\n\n'; - const rendered = Markdown.render(source); + const rendered = Markdown.render(source).trim(); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('
Term 1
\n
Definition 1
\n
Definition 2
'); }); test('Multiple Term, Single Definitions', function() { const source = 'Term 1\n::Definition 1\n\nTerm 2\n::Definition 1\n\n'; - const rendered = Markdown.render(source); + const rendered = Markdown.render(source).trim(); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('
Term 1
\n
Definition 1
\n
Term 2
\n
Definition 1
'); }); test('Multiple Term, Plural Definitions', function() { const source = 'Term 1\n::Definition 1\n::Definition 2\n\nTerm 2\n::Definition 1\n::Definition 2\n\n'; - const rendered = Markdown.render(source); + const rendered = Markdown.render(source).trim(); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('
Term 1
\n
Definition 1
\n
Definition 2
\n
Term 2
\n
Definition 1
\n
Definition 2
'); }); test('Single Term, Single multi-line definition', function() { const source = 'Term 1\n::Definition 1\nand more and\nmore and more\n\n'; - const rendered = Markdown.render(source); + const rendered = Markdown.render(source).trim(); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('
Term 1
\n
Definition 1 and more and more and more
'); }); test('Single Term, Plural multi-line definitions', function() { - const source = 'Term 1\n::Definition 1\nand more and more\n::Definition 2\n\n::Definition 3\n\n'; - const rendered = Markdown.render(source); - expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('
Term 1
\n
Definition 1 and more and more
\n
Definition 2
\n
Definition 3
'); + const source = 'Term 1\n::Definition 1\nand more and more\n::Definition 2\nand more\nand more\n::Definition 3\n\n'; + const rendered = Markdown.render(source).trim(); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('
Term 1
\n
Definition 1 and more and more
\n
Definition 2 and more and more
\n
Definition 3
'); }); test('Multiple Term, Single multi-line definition', function() { - const source = 'Term 1\n::Definition 1\nand more and more\n\nTerm 2\n::Definition 1\n\n::Definition 2\n\n'; - const rendered = Markdown.render(source); + const source = 'Term 1\n::Definition 1\nand more and more\n\nTerm 2\n::Definition 1\n::Definition 2\n\n'; + const rendered = Markdown.render(source).trim(); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('
Term 1
\n
Definition 1 and more and more
\n
Term 2
\n
Definition 1
\n
Definition 2
'); }); + + test('Multiple Term, Single multi-line definition, followed by an inline dl', function() { + const source = 'Term 1\n::Definition 1\nand more and more\n\nTerm 2\n::Definition 1\n::Definition 2\n\n::Inline Definition (no term)'; + const rendered = Markdown.render(source).trim(); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('
Term 1
\n
Definition 1 and more and more
\n
Term 2
\n
Definition 1
\n
Definition 2
Inline Definition (no term)
'); + }); + + test('Multiple Term, Single multi-line definition, followed by paragraph', function() { + const source = 'Term 1\n::Definition 1\nand more and more\n\nTerm 2\n::Definition 1\n::Definition 2\n\nParagraph'; + const rendered = Markdown.render(source).trim(); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('
Term 1
\n
Definition 1 and more and more
\n
Term 2
\n
Definition 1
\n
Definition 2

Paragraph

'); + }); });