From 9fb0f3771826743d410c78cb46e8fd676da16649 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Tue, 30 Apr 2024 23:52:19 -0400 Subject: [PATCH 01/10] Pass CSS props from curly spans/divs as an object for finer control --- shared/naturalcrit/markdown.js | 92 +++++++++++++++++++++++++++------- 1 file changed, 75 insertions(+), 17 deletions(-) diff --git a/shared/naturalcrit/markdown.js b/shared/naturalcrit/markdown.js index f72955bf3..34fa17c47 100644 --- a/shared/naturalcrit/markdown.js +++ b/shared/naturalcrit/markdown.js @@ -99,13 +99,13 @@ const mustacheSpans = { if(match) { //Find closing delimiter let blockCount = 0; - let tags = ''; + let tags = {}; let endTags = 0; let endToken = 0; let delim; while (delim = inlineRegex.exec(match[0])) { - if(!tags) { - tags = `${processStyleTags(delim[0].substring(2))}`; + if(_.isEmpty(tags)) { + tags = processStyleTags(delim[0].substring(2)); endTags = delim[0].length; } if(delim[0].startsWith('{{')) { @@ -134,7 +134,14 @@ const mustacheSpans = { } }, renderer(token) { - return `${this.parser.parseInline(token.tokens)}`; // parseInline to turn child tokens into HTML } }; @@ -149,13 +156,13 @@ const mustacheDivs = { if(match) { //Find closing delimiter let blockCount = 0; - let tags = ''; + let tags = {}; let endTags = 0; let endToken = 0; let delim; while (delim = blockRegex.exec(match[0])?.[0].trim()) { - if(!tags) { - tags = `${processStyleTags(delim.substring(2))}`; + if(_.isEmpty(tags)) { + tags = processStyleTags(delim.substring(2)); endTags = delim.length + src.indexOf(delim); } if(delim.startsWith('{{')) { @@ -183,7 +190,14 @@ const mustacheDivs = { } }, renderer(token) { - return `
${this.parser.parse(token.tokens)}
`; // parse to turn child tokens into HTML } }; @@ -199,10 +213,10 @@ const mustacheInjectInline = { if(!lastToken || lastToken.type == 'mustacheInjectInline') return false; - const tags = `${processStyleTags(match[1])}`; + const tags = processStyleTags(match[1]); lastToken.originalType = lastToken.type; lastToken.type = 'mustacheInjectInline'; - lastToken.tags = tags; + lastToken.injectedTags = tags; return { type : 'text', // Should match "name" above raw : match[0], // Text to consume from the source @@ -213,9 +227,22 @@ const mustacheInjectInline = { renderer(token) { token.type = token.originalType; const text = this.parser.parseInline([token]); + const originalTags = extractHTMLStyleTags(text); + const injectedTags = token.injectedTags; + const tags = { + id : originalTags.id || injectedTags.id, + classes : [originalTags.classes, injectedTags.classes].join(' '), + styles : [originalTags.styles, injectedTags.styles].join(' '), + attributes : [originalTags.attributes, injectedTags.attributes].join(' ') + }; const openingTag = /(<[^\s<>]+)([^\n<>]*>.*)/s.exec(text); if(openingTag) { - return `${openingTag[1]} class="${token.tags}${openingTag[2]}`; + return `${openingTag[1]}` + + `${tags.id ? ` id="${tags.id}"` : ''}` + + `${tags.classes ? ` class="${tags.classes}"` : ''}` + + `${tags.styles ? ` style="${tags.styles}"` : ''}` + + `${tags.attributes ? ` ${tags.attributes}` : ''}` + + `${openingTag[2]}`; // parse to turn child tokens into HTML } return text; } @@ -235,7 +262,7 @@ const mustacheInjectBlock = { return false; lastToken.originalType = 'mustacheInjectBlock'; - lastToken.tags = `${processStyleTags(match[1])}`; + lastToken.injectedTags = processStyleTags(match[1]); return { type : 'mustacheInjectBlock', // Should match "name" above raw : match[0], // Text to consume from the source @@ -249,9 +276,22 @@ const mustacheInjectBlock = { } token.type = token.originalType; const text = this.parser.parse([token]); + const originalTags = extractHTMLStyleTags(text); + const injectedTags = token.injectedTags; + const tags = { + id : originalTags.id || injectedTags.id, + classes : [originalTags.classes, injectedTags.classes].join(' '), + styles : [originalTags.styles, injectedTags.styles].join(' '), + attributes : [originalTags.attributes, injectedTags.attributes].join(' ') + }; const openingTag = /(<[^\s<>]+)([^\n<>]*>.*)/s.exec(text); if(openingTag) { - return `${openingTag[1]} class="${token.tags}${openingTag[2]}`; + return `${openingTag[1]}` + + `${tags.id ? ` id="${tags.id}"` : ''}` + + `${tags.classes ? ` class="${tags.classes}"` : ''}` + + `${tags.styles ? ` style="${tags.styles}"` : ''}` + + `${tags.attributes ? ` ${tags.attributes}` : ''}` + + `${openingTag[2]}`; // parse to turn child tokens into HTML } return text; } @@ -692,10 +732,28 @@ const processStyleTags = (string)=>{ const attributes = _.remove(tags, (tag)=>(tag.includes('='))).map((tag)=>tag.replace(/="?([^"]*)"?/g, '="$1"')); const styles = tags?.length ? tags.map((tag)=>tag.replace(/:"?([^"]*)"?/g, ':$1;').trim()) : []; - return `${classes?.length ? ` ${classes.join(' ')}` : ''}"` + - `${id ? ` id="${id}"` : ''}` + - `${styles?.length ? ` style="${styles.join(' ')}"` : ''}` + - `${attributes?.length ? ` ${attributes.join(' ')}` : ''}`; + return { + id : id, + classes : classes.join(' '), + styles : styles.join(' '), + attributes : attributes.join(' ') + }; +}; + +const extractHTMLStyleTags = (htmlString)=> { + const id = htmlString.match(/id="([^"]*)"/)?.[1] || null; + const classes = htmlString.match(/class="([^"]*)"/)?.[1] || null; + const styles = htmlString.match(/style="([^"]*)"/)?.[1] || null; + const attributes = htmlString.match(/([a-z]+="[^"]*)"/g) + ?.filter(attr => !attr.startsWith('class="') && !attr.startsWith('style="') && !attr.startsWith('id="')) + .join(' ') || null; + + return { + id, + classes, + styles, + attributes + }; }; const globalVarsList = {}; From 94905f815108d613b614690cc31c33c9ab3335ce Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Fri, 3 May 2024 12:12:55 -0400 Subject: [PATCH 02/10] Back to passing all tests (not the "failing" tests yet) --- shared/naturalcrit/markdown.js | 82 +++++++++++++++----------- tests/markdown/mustache-syntax.test.js | 2 +- tests/markdown/variables.test.js | 2 +- 3 files changed, 49 insertions(+), 37 deletions(-) diff --git a/shared/naturalcrit/markdown.js b/shared/naturalcrit/markdown.js index 34fa17c47..d93f73cea 100644 --- a/shared/naturalcrit/markdown.js +++ b/shared/naturalcrit/markdown.js @@ -135,12 +135,12 @@ const mustacheSpans = { }, renderer(token) { const tags = token.tags; - tags.classes = ['inline-block', tags.classes].join(' '); + tags.classes = ['inline-block', tags.classes].join(' ').trim(); return ` `${key}="${value}"`).join(' ')}` : ''}` + `>${this.parser.parseInline(token.tokens)}`; // parseInline to turn child tokens into HTML } }; @@ -191,13 +191,13 @@ const mustacheDivs = { }, renderer(token) { const tags = token.tags; - tags.classes = ['block', tags.classes].join(' '); + tags.classes = ['block', tags.classes].join(' ').trim(); return `${this.parser.parse(token.tokens)}`; // parse to turn child tokens into HTML + `${tags.classes ? ` class="${tags.classes}"` : ''}` + + `${tags.id ? ` id="${tags.id}"` : ''}` + + `${tags.styles ? ` style="${tags.styles}"` : ''}` + + `${tags.attributes ? ` ${Object.entries(tags.attributes).map(([key, value]) => `${key}="${value}"`).join(' ')}` : ''}` + + `>${this.parser.parse(token.tokens)}`; // parse to turn child tokens into HTML } }; @@ -230,18 +230,18 @@ const mustacheInjectInline = { const originalTags = extractHTMLStyleTags(text); const injectedTags = token.injectedTags; const tags = { - id : originalTags.id || injectedTags.id, - classes : [originalTags.classes, injectedTags.classes].join(' '), - styles : [originalTags.styles, injectedTags.styles].join(' '), - attributes : [originalTags.attributes, injectedTags.attributes].join(' ') + id : injectedTags.id || originalTags.id || null, + classes : [originalTags.classes, injectedTags.classes].join(' ').trim() || null, + styles : [originalTags.styles, injectedTags.styles].join(' ').trim() || null, + attributes : Object.assign(originalTags.attributes ?? {}, injectedTags.attributes ?? {}) }; - const openingTag = /(<[^\s<>]+)([^\n<>]*>.*)/s.exec(text); + const openingTag = /(<[^\s<>]+)[^\n<>]*(>.*)/s.exec(text); if(openingTag) { return `${openingTag[1]}` + - `${tags.id ? ` id="${tags.id}"` : ''}` + `${tags.classes ? ` class="${tags.classes}"` : ''}` + + `${tags.id ? ` id="${tags.id}"` : ''}` + `${tags.styles ? ` style="${tags.styles}"` : ''}` + - `${tags.attributes ? ` ${tags.attributes}` : ''}` + + `${tags.attributes ? ` ${Object.entries(tags.attributes).map(([key, value]) => `${key}="${value}"`).join(' ')}` : ''}` + `${openingTag[2]}`; // parse to turn child tokens into HTML } return text; @@ -279,18 +279,18 @@ const mustacheInjectBlock = { const originalTags = extractHTMLStyleTags(text); const injectedTags = token.injectedTags; const tags = { - id : originalTags.id || injectedTags.id, - classes : [originalTags.classes, injectedTags.classes].join(' '), - styles : [originalTags.styles, injectedTags.styles].join(' '), - attributes : [originalTags.attributes, injectedTags.attributes].join(' ') + id : injectedTags.id || originalTags.id || null, + classes : [originalTags.classes, injectedTags.classes].join(' ').trim() || null, + styles : [originalTags.styles, injectedTags.styles].join(' ').trim() || null, + attributes : Object.assign(originalTags.attributes ?? {}, injectedTags.attributes ?? {}) }; - const openingTag = /(<[^\s<>]+)([^\n<>]*>.*)/s.exec(text); + const openingTag = /(<[^\s<>]+)[^\n<>]*(>.*)/s.exec(text); if(openingTag) { return `${openingTag[1]}` + - `${tags.id ? ` id="${tags.id}"` : ''}` + `${tags.classes ? ` class="${tags.classes}"` : ''}` + + `${tags.id ? ` id="${tags.id}"` : ''}` + `${tags.styles ? ` style="${tags.styles}"` : ''}` + - `${tags.attributes ? ` ${tags.attributes}` : ''}` + + `${tags.attributes ? ` ${Object.entries(tags.attributes).map(([key, value]) => `${key}="${value}"`).join(' ')}` : ''}` + `${openingTag[2]}`; // parse to turn child tokens into HTML } return text; @@ -727,16 +727,23 @@ const processStyleTags = (string)=>{ //TODO: can we simplify to just split on commas? const tags = string.match(/(?:[^, ":=]+|[:=](?:"[^"]*"|))+/g); - const id = _.remove(tags, (tag)=>tag.startsWith('#')).map((tag)=>tag.slice(1))[0]; - const classes = _.remove(tags, (tag)=>(!tag.includes(':')) && (!tag.includes('='))); - const attributes = _.remove(tags, (tag)=>(tag.includes('='))).map((tag)=>tag.replace(/="?([^"]*)"?/g, '="$1"')); - const styles = tags?.length ? tags.map((tag)=>tag.replace(/:"?([^"]*)"?/g, ':$1;').trim()) : []; + const id = _.remove(tags, (tag)=>tag.startsWith('#')).map((tag)=>tag.slice(1))[0] || null; + const classes = _.remove(tags, (tag)=>(!tag.includes(':')) && (!tag.includes('='))).join(' ') || null; + const attributes = _.remove(tags, (tag)=>(tag.includes('='))).map((tag)=>tag.replace(/="?([^"]*)"?/g, '="$1"')) + ?.filter(attr => !attr.startsWith('class="') && !attr.startsWith('style="') && !attr.startsWith('id="')) + .reduce((obj, attr) => { + let [key, value] = attr.split("="); + value = value.replace(/"/g, ''); + obj[key] = value; + return obj; + }, {}) || null; + const styles = tags?.length ? tags.map((tag)=>tag.replace(/:"?([^"]*)"?/g, ':$1;').trim()).join(' ') : null; return { id : id, - classes : classes.join(' '), - styles : styles.join(' '), - attributes : attributes.join(' ') + classes : classes, + styles : styles, + attributes : _.isEmpty(attributes) ? null : attributes }; }; @@ -746,13 +753,18 @@ const extractHTMLStyleTags = (htmlString)=> { const styles = htmlString.match(/style="([^"]*)"/)?.[1] || null; const attributes = htmlString.match(/([a-z]+="[^"]*)"/g) ?.filter(attr => !attr.startsWith('class="') && !attr.startsWith('style="') && !attr.startsWith('id="')) - .join(' ') || null; + .reduce((obj, attr) => { + let [key, value] = attr.split("="); + value = value.replace(/"/g, ''); + obj[key] = value; + return obj; + }, {}) || null; return { - id, - classes, - styles, - attributes + id : id, + classes : classes, + styles : styles, + attributes : _.isEmpty(attributes) ? null : attributes }; }; diff --git a/tests/markdown/mustache-syntax.test.js b/tests/markdown/mustache-syntax.test.js index 835bcc575..f156771cb 100644 --- a/tests/markdown/mustache-syntax.test.js +++ b/tests/markdown/mustache-syntax.test.js @@ -306,7 +306,7 @@ describe('Injection: When an injection tag follows an element', ()=>{ it('Renders an image with added attributes', function() { const source = `![homebrew mug](https://i.imgur.com/hMna6G0.png) {position:absolute,bottom:20px,left:130px,width:220px,a="b and c",d=e}`; const rendered = Markdown.render(source).trimReturns(); - expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`

homebrew mug

`); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`

homebrew mug

`); }); }); diff --git a/tests/markdown/variables.test.js b/tests/markdown/variables.test.js index c909dafec..e6018e19f 100644 --- a/tests/markdown/variables.test.js +++ b/tests/markdown/variables.test.js @@ -329,7 +329,7 @@ describe('Normal Links and Images', ()=>{ const source = `![alt text](url){width:100px}`; const rendered = Markdown.render(source).trimReturns(); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent` -

alt text

`.trimReturns()); +

alt text

`.trimReturns()); }); it('Renders normal links', function() { From bb8ade435d4a41735337e6321deda34191c9bba4 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Fri, 3 May 2024 12:40:03 -0400 Subject: [PATCH 03/10] Now passing many previously failing tests --- package.json | 8 ++++---- shared/naturalcrit/markdown.js | 4 ++-- tests/markdown/mustache-syntax.test.js | 24 ++++++++++++------------ 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index df7f700ef..a30a986b0 100644 --- a/package.json +++ b/package.json @@ -27,10 +27,10 @@ "test:dev": "jest --verbose --watch", "test:basic": "jest tests/markdown/basic.test.js --verbose", "test:variables": "jest tests/markdown/variables.test.js --verbose", - "test:mustache-syntax": "jest '.*(mustache-syntax).*' --verbose --noStackTrace", - "test:mustache-syntax:inline": "jest '.*(mustache-syntax).*' -t '^Inline:.*' --verbose --noStackTrace", - "test:mustache-syntax:block": "jest '.*(mustache-syntax).*' -t '^Block:.*' --verbose --noStackTrace", - "test:mustache-syntax:injection": "jest '.*(mustache-syntax).*' -t '^Injection:.*' --verbose --noStackTrace", + "test:mustache-syntax": "jest .*(mustache-syntax).* --verbose --noStackTrace", + "test:mustache-syntax:inline": "jest .*(mustache-syntax).* -t '^Inline:.*' --verbose --noStackTrace", + "test:mustache-syntax:block": "jest .*(mustache-syntax).* -t '^Block:.*' --verbose --noStackTrace", + "test:mustache-syntax:injection": "jest .*(mustache-syntax).* -t '^Injection:.*' --verbose --noStackTrace", "test:definition-lists": "jest tests/markdown/definition-lists.test.js --verbose --noStackTrace", "test:route": "jest tests/routes/static-pages.test.js --verbose", "phb": "node scripts/phb.js", diff --git a/shared/naturalcrit/markdown.js b/shared/naturalcrit/markdown.js index d93f73cea..33bae12d6 100644 --- a/shared/naturalcrit/markdown.js +++ b/shared/naturalcrit/markdown.js @@ -241,7 +241,7 @@ const mustacheInjectInline = { `${tags.classes ? ` class="${tags.classes}"` : ''}` + `${tags.id ? ` id="${tags.id}"` : ''}` + `${tags.styles ? ` style="${tags.styles}"` : ''}` + - `${tags.attributes ? ` ${Object.entries(tags.attributes).map(([key, value]) => `${key}="${value}"`).join(' ')}` : ''}` + + `${!_.isEmpty(tags.attributes) ? ` ${Object.entries(tags.attributes).map(([key, value]) => `${key}="${value}"`).join(' ')}` : ''}` + `${openingTag[2]}`; // parse to turn child tokens into HTML } return text; @@ -290,7 +290,7 @@ const mustacheInjectBlock = { `${tags.classes ? ` class="${tags.classes}"` : ''}` + `${tags.id ? ` id="${tags.id}"` : ''}` + `${tags.styles ? ` style="${tags.styles}"` : ''}` + - `${tags.attributes ? ` ${Object.entries(tags.attributes).map(([key, value]) => `${key}="${value}"`).join(' ')}` : ''}` + + `${!_.isEmpty(tags.attributes) ? ` ${Object.entries(tags.attributes).map(([key, value]) => `${key}="${value}"`).join(' ')}` : ''}` + `${openingTag[2]}`; // parse to turn child tokens into HTML } return text; diff --git a/tests/markdown/mustache-syntax.test.js b/tests/markdown/mustache-syntax.test.js index f156771cb..d1d64a9de 100644 --- a/tests/markdown/mustache-syntax.test.js +++ b/tests/markdown/mustache-syntax.test.js @@ -243,13 +243,13 @@ describe(`Block: When using the Block syntax {{tags\\ntext\\n}}`, ()=>{ describe('Injection: When an injection tag follows an element', ()=>{ // FIXME: Most of these fail because injections currently replace attributes, rather than append to. Or just minor extra whitespace issues. describe('and that element is an inline-block', ()=>{ - it.failing('Renders a span "text" with no injection', function() { + it('Renders a span "text" with no injection', function() { const source = '{{ text}}{}'; const rendered = Markdown.render(source); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('text'); }); - it.failing('Renders a span "text" with injected Class name', function() { + it('Renders a span "text" with injected Class name', function() { const source = '{{ text}}{ClassName}'; const rendered = Markdown.render(source); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('text'); @@ -261,31 +261,31 @@ describe('Injection: When an injection tag follows an element', ()=>{ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('text'); }); - it.failing('Renders a span "text" with injected style', function() { + it('Renders a span "text" with injected style', function() { const source = '{{ text}}{color:red}'; const rendered = Markdown.render(source); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('text'); }); - it.failing('Renders a span "text" with injected style using a string variable', function() { + it('Renders a span "text" with injected style using a string variable', function() { const source = `{{ text}}{--stringVariable:"'string'"}`; const rendered = Markdown.render(source); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`text`); }); - it.failing('Renders a span "text" with two injected styles', function() { + it('Renders a span "text" with two injected styles', function() { const source = '{{ text}}{color:red,background:blue}'; const rendered = Markdown.render(source); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('text'); }); - it.failing('Renders an emphasis element with injected Class name', function() { + it('Renders an emphasis element with injected Class name', function() { const source = '*emphasis*{big}'; const rendered = Markdown.render(source).trimReturns(); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('

emphasis

'); }); - it.failing('Renders a code element with injected style', function() { + it('Renders a code element with injected style', function() { const source = '`code`{background:gray}'; const rendered = Markdown.render(source).trimReturns(); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('

code

'); @@ -311,19 +311,19 @@ describe('Injection: When an injection tag follows an element', ()=>{ }); describe('and that element is a block', ()=>{ - it.failing('renders a div "text" with no injection', function() { + it('renders a div "text" with no injection', function() { const source = '{{\ntext\n}}\n{}'; const rendered = Markdown.render(source).trimReturns(); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('

text

'); }); - it.failing('renders a div "text" with injected Class name', function() { + it('renders a div "text" with injected Class name', function() { const source = '{{\ntext\n}}\n{ClassName}'; const rendered = Markdown.render(source).trimReturns(); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('

text

'); }); - it.failing('renders a div "text" with injected style', function() { + it('renders a div "text" with injected style', function() { const source = '{{\ntext\n}}\n{color:red}'; const rendered = Markdown.render(source).trimReturns(); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('

text

'); @@ -354,7 +354,7 @@ describe('Injection: When an injection tag follows an element', ()=>{ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('

text

'); }); - it.failing('renders a table with injected class name', function() { + it('renders a table with injected class name', function() { const source = dedent`| Experience Points | Level | |:------------------|:-----:| | 0 | 1 | @@ -384,7 +384,7 @@ describe('Injection: When an injection tag follows an element', ()=>{ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('

text

{secondInjection}

'); }); - it.failing('renders a div nested into another div, the inner with class=innerDiv and the other class=outerDiv', function() { + it('renders a div nested into another div, the inner with class=innerDiv and the other class=outerDiv', function() { const source = dedent`{{ outer text {{ From 1a33e6e631fbc649d271ec3c525dd084b79c16a1 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Fri, 3 May 2024 12:48:19 -0400 Subject: [PATCH 04/10] Passing almost all tests --- tests/markdown/mustache-syntax.test.js | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/markdown/mustache-syntax.test.js b/tests/markdown/mustache-syntax.test.js index d1d64a9de..f42cbbac8 100644 --- a/tests/markdown/mustache-syntax.test.js +++ b/tests/markdown/mustache-syntax.test.js @@ -255,10 +255,10 @@ describe('Injection: When an injection tag follows an element', ()=>{ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('text'); }); - it.failing('Renders a span "text" with injected attribute', function() { + it('Renders a span "text" with injected attribute', function() { const source = '{{ text}}{a="b and c"}'; const rendered = Markdown.render(source); - expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('text'); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('text'); }); it('Renders a span "text" with injected style', function() { @@ -291,16 +291,16 @@ describe('Injection: When an injection tag follows an element', ()=>{ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('

code

'); }); - it.failing('Renders an image element with injected style', function() { + it('Renders an image element with injected style', function() { const source = '![alt text](http://i.imgur.com/hMna6G0.png){position:absolute}'; const rendered = Markdown.render(source).trimReturns(); - expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('

homebrew mug

'); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('

alt text

'); }); it.failing('Renders an element modified by only the first of two consecutive injections', function() { const source = '{{ text}}{color:red}{background:blue}'; const rendered = Markdown.render(source).trimReturns(); - expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('

text{background:blue}

'); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('text{background:blue}'); }); it('Renders an image with added attributes', function() { @@ -329,29 +329,29 @@ describe('Injection: When an injection tag follows an element', ()=>{ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('

text

'); }); - it.failing('renders a div "text" with two injected styles', function() { + it('renders a div "text" with two injected styles', function() { const source = dedent`{{ text }} {color:red,background:blue}`; const rendered = Markdown.render(source).trimReturns(); - expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`

text

`); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`

text

`); }); - it.failing('renders a div "text" with injected variable string', function() { + it('renders a div "text" with injected variable string', function() { const source = dedent`{{ text }} {--stringVariable:"'string'"}`; const rendered = Markdown.render(source).trimReturns(); - expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`

text

`); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`

text

`); }); - it.failing('renders an h2 header "text" with injected class name', function() { + it('renders an h2 header "text" with injected class name', function() { const source = dedent`## text {ClassName}`; const rendered = Markdown.render(source).trimReturns(); - expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('

text

'); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('

text

'); }); it('renders a table with injected class name', function() { @@ -376,12 +376,12 @@ describe('Injection: When an injection tag follows an element', ()=>{ // expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`...`); // FIXME: expect this to be injected into
    ? Currently injects into last
  • // }); - it.failing('renders an h2 header "text" with injected class name, and "secondInjection" as regular text on the next line.', function() { + it('renders an h2 header "text" with injected class name, and "secondInjection" as regular text on the next line.', function() { const source = dedent`## text {ClassName} {secondInjection}`; const rendered = Markdown.render(source).trimReturns(); - expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('

    text

    {secondInjection}

    '); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('

    text

    {secondInjection}

    '); }); it('renders a div nested into another div, the inner with class=innerDiv and the other class=outerDiv', function() { From 193cde092599480d84648090c4769b6a152a8c2c Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Fri, 3 May 2024 14:31:34 -0400 Subject: [PATCH 05/10] Passes all tests --- shared/naturalcrit/markdown.js | 5 ++++- tests/markdown/mustache-syntax.test.js | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/shared/naturalcrit/markdown.js b/shared/naturalcrit/markdown.js index 33bae12d6..497cc895d 100644 --- a/shared/naturalcrit/markdown.js +++ b/shared/naturalcrit/markdown.js @@ -218,13 +218,16 @@ const mustacheInjectInline = { lastToken.type = 'mustacheInjectInline'; lastToken.injectedTags = tags; return { - type : 'text', // Should match "name" above + type : 'mustacheInjectInline', // Should match "name" above raw : match[0], // Text to consume from the source text : '' }; } }, renderer(token) { + if(!token.originalType){ + return; + } token.type = token.originalType; const text = this.parser.parseInline([token]); const originalTags = extractHTMLStyleTags(text); diff --git a/tests/markdown/mustache-syntax.test.js b/tests/markdown/mustache-syntax.test.js index f42cbbac8..c210167b6 100644 --- a/tests/markdown/mustache-syntax.test.js +++ b/tests/markdown/mustache-syntax.test.js @@ -297,10 +297,10 @@ describe('Injection: When an injection tag follows an element', ()=>{ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('

    alt text

    '); }); - it.failing('Renders an element modified by only the first of two consecutive injections', function() { + it('Renders an element modified by only the first of two consecutive injections', function() { const source = '{{ text}}{color:red}{background:blue}'; const rendered = Markdown.render(source).trimReturns(); - expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('text{background:blue}'); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('

    text{background:blue}

    '); }); it('Renders an image with added attributes', function() { From 660a586a7caae1e22fa71fa669d48271ab68f664 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Fri, 3 May 2024 15:01:04 -0400 Subject: [PATCH 06/10] Clarify comments --- shared/naturalcrit/markdown.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/naturalcrit/markdown.js b/shared/naturalcrit/markdown.js index 497cc895d..3a9de6288 100644 --- a/shared/naturalcrit/markdown.js +++ b/shared/naturalcrit/markdown.js @@ -50,7 +50,7 @@ renderer.html = function (html) { return html; }; -// Don't wrap {{ Divs or {{ empty Spans in

    tags +// Don't wrap {{ Spans alone on a line, or {{ Divs in

    tags renderer.paragraph = function(text){ let match; if(text.startsWith(' Date: Fri, 3 May 2024 15:01:21 -0400 Subject: [PATCH 07/10] Try to make tests work on both circleCI and windows --- package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index a30a986b0..42b11fbd0 100644 --- a/package.json +++ b/package.json @@ -27,10 +27,10 @@ "test:dev": "jest --verbose --watch", "test:basic": "jest tests/markdown/basic.test.js --verbose", "test:variables": "jest tests/markdown/variables.test.js --verbose", - "test:mustache-syntax": "jest .*(mustache-syntax).* --verbose --noStackTrace", - "test:mustache-syntax:inline": "jest .*(mustache-syntax).* -t '^Inline:.*' --verbose --noStackTrace", - "test:mustache-syntax:block": "jest .*(mustache-syntax).* -t '^Block:.*' --verbose --noStackTrace", - "test:mustache-syntax:injection": "jest .*(mustache-syntax).* -t '^Injection:.*' --verbose --noStackTrace", + "test:mustache-syntax": "jest \".*(mustache-syntax).*\" --verbose --noStackTrace", + "test:mustache-syntax:inline": "jest \".*(mustache-syntax).*\" -t '^Inline:.*' --verbose --noStackTrace", + "test:mustache-syntax:block": "jest \".*(mustache-syntax).*\" -t '^Block:.*' --verbose --noStackTrace", + "test:mustache-syntax:injection": "jest \".*(mustache-syntax).*\" -t '^Injection:.*' --verbose --noStackTrace", "test:definition-lists": "jest tests/markdown/definition-lists.test.js --verbose --noStackTrace", "test:route": "jest tests/routes/static-pages.test.js --verbose", "phb": "node scripts/phb.js", From dce86580cea97c0f2244b623aa0b063aee949cf0 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Fri, 3 May 2024 17:02:12 -0400 Subject: [PATCH 08/10] Added additional tests for injection --- shared/naturalcrit/markdown.js | 2 +- tests/markdown/mustache-syntax.test.js | 81 +++++++++++++++++++++++++- 2 files changed, 79 insertions(+), 4 deletions(-) diff --git a/shared/naturalcrit/markdown.js b/shared/naturalcrit/markdown.js index 3a9de6288..1c44c57ca 100644 --- a/shared/naturalcrit/markdown.js +++ b/shared/naturalcrit/markdown.js @@ -754,7 +754,7 @@ const extractHTMLStyleTags = (htmlString)=> { const id = htmlString.match(/id="([^"]*)"/)?.[1] || null; const classes = htmlString.match(/class="([^"]*)"/)?.[1] || null; const styles = htmlString.match(/style="([^"]*)"/)?.[1] || null; - const attributes = htmlString.match(/([a-z]+="[^"]*)"/g) + const attributes = htmlString.match(/[a-zA-Z]+="[^"]*"/g) ?.filter(attr => !attr.startsWith('class="') && !attr.startsWith('style="') && !attr.startsWith('id="')) .reduce((obj, attr) => { let [key, value] = attr.split("="); diff --git a/tests/markdown/mustache-syntax.test.js b/tests/markdown/mustache-syntax.test.js index c210167b6..8ca12519e 100644 --- a/tests/markdown/mustache-syntax.test.js +++ b/tests/markdown/mustache-syntax.test.js @@ -279,6 +279,36 @@ describe('Injection: When an injection tag follows an element', ()=>{ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('text'); }); + it('Renders a span "text" with its own ID, overwritten with an injected ID', function() { + const source = '{{#oldId text}}{#newId}'; + const rendered = Markdown.render(source); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('text'); + }); + + it('Renders a span "text" with its own attributes, overwritten with an injected attribute, plus a new one', function() { + const source = '{{attrA="old",attrB="old" text}}{attrA="new",attrC="new"}'; + const rendered = Markdown.render(source); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('text'); + }); + + it('Renders a span "text" with its own attributes, overwritten with an injected attribute, ignoring "class", "style", and "id"', function() { + const source = '{{attrA="old",attrB="old" text}}{attrA="new",attrC="new",class="new",style="new",id="new"}'; + const rendered = Markdown.render(source); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('text'); + }); + + it('Renders a span "text" with its own styles, appended with injected styles', function() { + const source = '{{color:blue,height:10px text}}{width:10px,color:red}'; + const rendered = Markdown.render(source); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('text'); + }); + + it('Renders a span "text" with its own classes, appended with injected classes', function() { + const source = '{{classA,classB text}}{classA,classC}'; + const rendered = Markdown.render(source); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('text'); + }); + it('Renders an emphasis element with injected Class name', function() { const source = '*emphasis*{big}'; const rendered = Markdown.render(source).trimReturns(); @@ -340,13 +370,58 @@ describe('Injection: When an injection tag follows an element', ()=>{ it('renders a div "text" with injected variable string', function() { const source = dedent`{{ - text - }} - {--stringVariable:"'string'"}`; + text + }} + {--stringVariable:"'string'"}`; const rendered = Markdown.render(source).trimReturns(); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`

    text

    `); }); + it('Renders a span "text" with its own ID, overwritten with an injected ID', function() { + const source = dedent`{{#oldId + text + }} + {#newId}`; + const rendered = Markdown.render(source).trimReturns(); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('

    text

    '); + }); + + it('Renders a span "text" with its own attributes, overwritten with an injected attribute, plus a new one', function() { + const source = dedent`{{attrA="old",attrB="old" + text + }} + {attrA="new",attrC="new"}`; + const rendered = Markdown.render(source).trimReturns(); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('

    text

    '); + }); + + it('Renders a span "text" with its own attributes, overwritten with an injected attribute, ignoring "class", "style", and "id"', function() { + const source = dedent`{{attrA="old",attrB="old" + text + }} + {attrA="new",attrC="new",class="new",style="new",id="new"}`; + const rendered = Markdown.render(source).trimReturns(); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('

    text

    '); + }); + + it('Renders a span "text" with its own styles, appended with injected styles', function() { + const source = dedent`{{color:blue,height:10px + text + }} + {width:10px,color:red}`; + const rendered = Markdown.render(source).trimReturns(); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('

    text

    '); + }); + + it('Renders a span "text" with its own classes, appended with injected classes', function() { + const source = dedent`{{classA,classB + text + }} + {classA,classC}`; + const rendered = Markdown.render(source).trimReturns(); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('

    text

    '); + }); + it('renders an h2 header "text" with injected class name', function() { const source = dedent`## text {ClassName}`; From 11f1e3fee8d6718e205b6f31cfef8d024debc305 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 4 May 2024 01:23:59 +0000 Subject: [PATCH 09/10] Bump eslint-plugin-jest from 28.2.0 to 28.5.0 Bumps [eslint-plugin-jest](https://github.com/jest-community/eslint-plugin-jest) from 28.2.0 to 28.5.0. - [Release notes](https://github.com/jest-community/eslint-plugin-jest/releases) - [Changelog](https://github.com/jest-community/eslint-plugin-jest/blob/main/CHANGELOG.md) - [Commits](https://github.com/jest-community/eslint-plugin-jest/compare/v28.2.0...v28.5.0) --- updated-dependencies: - dependency-name: eslint-plugin-jest dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package-lock.json | 10 +++++----- package.json | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 31295f14a..8a22dc0f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,7 +49,7 @@ }, "devDependencies": { "eslint": "^8.57.0", - "eslint-plugin-jest": "^28.2.0", + "eslint-plugin-jest": "^28.5.0", "eslint-plugin-react": "^7.34.1", "jest": "^29.7.0", "jest-expect-message": "^1.1.3", @@ -5748,12 +5748,12 @@ } }, "node_modules/eslint-plugin-jest": { - "version": "28.2.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-28.2.0.tgz", - "integrity": "sha512-yRDti/a+f+SMSmNTiT9/M/MzXGkitl8CfzUxnpoQcTyfq8gUrXMriVcWU36W1X6BZSUoyUCJrDAWWUA2N4hE5g==", + "version": "28.5.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-28.5.0.tgz", + "integrity": "sha512-6np6DGdmNq/eBbA7HOUNV8fkfL86PYwBfwyb8n23FXgJNTR8+ot3smRHjza9LGsBBZRypK3qyF79vMjohIL8eQ==", "dev": true, "dependencies": { - "@typescript-eslint/utils": "^6.0.0" + "@typescript-eslint/utils": "^6.0.0 || ^7.0.0" }, "engines": { "node": "^16.10.0 || ^18.12.0 || >=20.0.0" diff --git a/package.json b/package.json index 42b11fbd0..06a755b0f 100644 --- a/package.json +++ b/package.json @@ -120,7 +120,7 @@ }, "devDependencies": { "eslint": "^8.57.0", - "eslint-plugin-jest": "^28.2.0", + "eslint-plugin-jest": "^28.5.0", "eslint-plugin-react": "^7.34.1", "jest": "^29.7.0", "jest-expect-message": "^1.1.3", From dd31c0138b954cfae9029b7d6a09804d8cf47cc7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 4 May 2024 01:24:32 +0000 Subject: [PATCH 10/10] Bump @googleapis/drive from 8.7.0 to 8.8.0 Bumps [@googleapis/drive](https://github.com/googleapis/google-api-nodejs-client) from 8.7.0 to 8.8.0. - [Release notes](https://github.com/googleapis/google-api-nodejs-client/releases) - [Changelog](https://github.com/googleapis/google-api-nodejs-client/blob/main/release-please-config.json) - [Commits](https://github.com/googleapis/google-api-nodejs-client/compare/drive-v8.7.0...drive-v8.8.0) --- updated-dependencies: - dependency-name: "@googleapis/drive" dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 31295f14a..6cf925c2c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "@babel/plugin-transform-runtime": "^7.24.3", "@babel/preset-env": "^7.24.5", "@babel/preset-react": "^7.24.1", - "@googleapis/drive": "^8.7.0", + "@googleapis/drive": "^8.8.0", "body-parser": "^1.20.2", "classnames": "^2.5.1", "codemirror": "^5.65.6", @@ -1983,9 +1983,9 @@ } }, "node_modules/@googleapis/drive": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/@googleapis/drive/-/drive-8.7.0.tgz", - "integrity": "sha512-XAi6kfySIU4H3ivX2DpzTDce5UhNke5NxEWCL6tySEdcVqx+cmXJmkMqwfOAHJalEB5s9PPfdLBU29Xd5XlLSQ==", + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/@googleapis/drive/-/drive-8.8.0.tgz", + "integrity": "sha512-EOZ9GZCOUdej9PJVnkai7qu5RPyFLYse8FlpgijzfnZPOACXWFf4XOFuAuMcMw4Zue8xPhAPHu1qYcy8u362Xw==", "dependencies": { "googleapis-common": "^7.0.0" }, diff --git a/package.json b/package.json index 42b11fbd0..e4e4ba5c9 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,7 @@ "@babel/plugin-transform-runtime": "^7.24.3", "@babel/preset-env": "^7.24.5", "@babel/preset-react": "^7.24.1", - "@googleapis/drive": "^8.7.0", + "@googleapis/drive": "^8.8.0", "body-parser": "^1.20.2", "classnames": "^2.5.1", "codemirror": "^5.65.6",