Repository: naturalcrit/homebrewery
Pull request #3446 (2024-05-01 04:02:33)

Make Curly Injectors append properties, not overwrite
Precursor to the Emojis PR: #3405 

Currently, applying curly injectors to an object fully overwrites any inline CSS properties:

```
{{myClass,#myID,myStyle:red Test}}{color:blue}
```

Results in:

```
<span style="color:blue">Test</span>
```

This PR simply modifies the behavior of curly injectors to append to, rather than overwrite the properties of the preceding token.

```
<span id="myID" class="myClass" style="myStyle:red; color:blue">Test</span>
```

Generally should make little difference, but would be handy for elements like emojis that rely on preserving their initial CSS classes: `:an-emoji:{color:blue}`


Likely has some edge case bugs to work out.


Patch:
diff --git a/package.json b/package.json
index df7f700e..42b11fbd 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 f72955bf..1c44c57c 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 <p> tags
+// Don't wrap {{ Spans alone on a line, or {{ Divs in <p> tags
 renderer.paragraph = function(text){
 	let match;
 	if(text.startsWith('<div') || text.startsWith('</div'))
@@ -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 `<span class="inline-block${token.tags}>${this.parser.parseInline(token.tokens)}</span>`; // parseInline to turn child tokens into HTML
+		const tags = token.tags;
+		tags.classes = ['inline-block', tags.classes].join(' ').trim();
+		return `<span` +
+			`${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.parseInline(token.tokens)}</span>`; // 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 `<div class="block${token.tags}>${this.parser.parse(token.tokens)}</div>`; // parseInline to turn child tokens into HTML
+		const tags = token.tags;
+		tags.classes = ['block', tags.classes].join(' ').trim();
+		return `<div` +
+			`${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)}</div>`; // parse to turn child tokens into HTML
 	}
 };
 
@@ -199,23 +213,39 @@ 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
+				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 openingTag = /(<[^\s<>]+)([^\n<>]*>.*)/s.exec(text);
+		const originalTags = extractHTMLStyleTags(text);
+		const injectedTags = token.injectedTags;
+		const tags = {
+			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);
 		if(openingTag) {
-			return `${openingTag[1]} class="${token.tags}${openingTag[2]}`;
+			return `${openingTag[1]}` +
+				`${tags.classes    ? ` class="${tags.classes}"` : ''}` +
+				`${tags.id         ? ` id="${tags.id}"`         : ''}` +
+				`${tags.styles     ? ` style="${tags.styles}"`  : ''}` +
+				`${!_.isEmpty(tags.attributes) ? ` ${Object.entries(tags.attributes).map(([key, value]) => `${key}="${value}"`).join(' ')}` : ''}` +
+				`${openingTag[2]}`; // parse to turn child tokens into HTML
 		}
 		return text;
 	}
@@ -235,7 +265,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 +279,22 @@ const mustacheInjectBlock = {
 			}
 			token.type = token.originalType;
 			const text = this.parser.parse([token]);
-			const openingTag = /(<[^\s<>]+)([^\n<>]*>.*)/s.exec(text);
+			const originalTags = extractHTMLStyleTags(text);
+			const injectedTags = token.injectedTags;
+			const tags = {
+				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);
 			if(openingTag) {
-				return `${openingTag[1]} class="${token.tags}${openingTag[2]}`;
+				return `${openingTag[1]}` +
+					`${tags.classes    ? ` class="${tags.classes}"` : ''}` +
+					`${tags.id         ? ` id="${tags.id}"`         : ''}` +
+					`${tags.styles     ? ` style="${tags.styles}"`  : ''}` +
+					`${!_.isEmpty(tags.attributes) ? ` ${Object.entries(tags.attributes).map(([key, value]) => `${key}="${value}"`).join(' ')}` : ''}` +
+					`${openingTag[2]}`; // parse to turn child tokens into HTML
 			}
 			return text;
 		}
@@ -687,15 +730,45 @@ 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 `${classes?.length ? ` ${classes.join(' ')}`        : ''}"` +
-		`${id                   ? ` id="${id}"`                  : ''}` +
-		`${styles?.length       ? ` style="${styles.join(' ')}"` : ''}` +
-		`${attributes?.length   ? ` ${attributes.join(' ')}`     : ''}`;
+	return {
+		id         : id,
+		classes    : classes,
+		styles     : styles,
+		attributes : _.isEmpty(attributes) ? null : attributes
+	};
+};
+
+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-zA-Z]+="[^"]*"/g)
+		?.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;
+
+	return {
+		id         : id,
+		classes    : classes,
+		styles     : styles,
+		attributes : _.isEmpty(attributes) ? null : attributes
+	};
 };
 
 const globalVarsList    = {};


Test patch:
diff --git a/tests/markdown/mustache-syntax.test.js b/tests/markdown/mustache-syntax.test.js
index 835bcc57..8ca12519 100644
--- a/tests/markdown/mustache-syntax.test.js
+++ b/tests/markdown/mustache-syntax.test.js
@@ -243,61 +243,91 @@ 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('<span class="inline-block">text</span>');
 		});
 
-		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('<span class="inline-block ClassName">text</span>');
 		});
 
-		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('<span a="b and c" class="inline-block ">text</span>');
+			expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" a="b and c">text</span>');
 		});
 
-		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('<span class="inline-block" style="color:red;">text</span>');
 		});
 
-		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(`<span class="inline-block" style="--stringVariable:'string';">text</span>`);
 		});
 
-		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('<span class="inline-block" style="color:red; background:blue;">text</span>');
 		});
 
-		it.failing('Renders an emphasis element with injected Class name', function() {
+		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('<span class="inline-block" id="newId">text</span>');
+		});
+
+		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('<span class="inline-block" attrA="new" attrB="old" attrC="new">text</span>');
+		});
+
+		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('<span class="inline-block" attrA="new" attrB="old" attrC="new">text</span>');
+		});
+
+		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('<span class="inline-block" style="color:blue; height:10px; width:10px; color:red;">text</span>');
+		});
+
+		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('<span class="inline-block classA classB classA classC">text</span>');
+		});
+
+		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('<p><em class="big">emphasis</em></p>');
 		});
 
-		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('<p><code style="background:gray;">code</code></p>');
 		});
 
-		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('<p><img src="http://i.imgur.com/hMna6G0.png" alt="homebrew mug" style="position:absolute;"></p>');
+			expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p><img style="position:absolute;" src="http://i.imgur.com/hMna6G0.png" alt="alt text"></p>');
 		});
 
-		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('<p><span class="inline-block" style="color:red;">text</span>{background:blue}</p>');
@@ -306,55 +336,100 @@ 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(`<p><img class="" style="position:absolute; bottom:20px; left:130px; width:220px;" a="b and c" d="e" src="https://i.imgur.com/hMna6G0.png" alt="homebrew mug"></p>`);
+			expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p><img style="position:absolute; bottom:20px; left:130px; width:220px;" src="https://i.imgur.com/hMna6G0.png" alt="homebrew mug" a="b and c" d="e"></p>`);
 		});
 	});
 
 	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('<div class="block"><p>text</p></div>');
 		});
 
-		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('<div class="block ClassName"><p>text</p></div>');
 		});
 
-		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('<div class="block" style="color:red;"><p>text</p></div>');
 		});
 
-		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(`<div class="block" style="color:red; background:blue"><p>text</p></div>`);
+			expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block" style="color:red; background:blue;"><p>text</p></div>`);
 		});
 
-		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'"}`;
+														text
+														}}
+														{--stringVariable:"'string'"}`;
+			const rendered = Markdown.render(source).trimReturns();
+			expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block" style="--stringVariable:'string';"><p>text</p></div>`);
+		});
+
+		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('<div class="block" id="newId"><p>text</p></div>');
+		});
+
+		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('<div class="block" attrA="new" attrB="old" attrC="new"><p>text</p></div>');
+		});
+
+		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('<div class="block" attrA="new" attrB="old" attrC="new"><p>text</p></div>');
+		});
+
+		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('<div class="block" style="color:blue; height:10px; width:10px; color:red;"><p>text</p></div>');
+		});
+
+		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(`<div class="block" style="--stringVariable:'string'"><p>text</p></div>`);
+			expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<div class="block classA classB classA classC"><p>text</p></div>');
 		});
 
-		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('<h2 class="ClassName">text</h2>');
+			expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<h2 class="ClassName" id="text">text</h2>');
 		});
 
-		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     |
@@ -376,15 +451,15 @@ describe('Injection: When an injection tag follows an element', ()=>{
 		// 	expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`...`);   // FIXME: expect this to be injected into <ul>?  Currently injects into last <li>
 		// });
 
-		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('<h2 class="ClassName">text</h2><p>{secondInjection}</p>');
+			expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<h2 class="ClassName" id="text">text</h2><p>{secondInjection}</p>');
 		});
 
-		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
 			{{
diff --git a/tests/markdown/variables.test.js b/tests/markdown/variables.test.js
index c909dafe..e6018e19 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`
-			<p><img class="" style="width:100px;" src="url" alt="alt text"></p>`.trimReturns());
+			<p><img style="width:100px;" src="url" alt="alt text"></p>`.trimReturns());
 	});
 
 	it('Renders normal links', function() {