diff --git a/package-lock.json b/package-lock.json
index 31295f14a..6256a8dd7 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",
@@ -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",
@@ -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"
},
@@ -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 df7f700ef..6179a3bab 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",
@@ -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",
@@ -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",
diff --git a/shared/naturalcrit/markdown.js b/shared/naturalcrit/markdown.js
index f72955bf3..1c44c57ca 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
`${key}="${value}"`).join(' ')}` : ''}` +
+ `>${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 `
`${key}="${value}"`).join(' ')}` : ''}` +
+ `>${this.parser.parse(token.tokens)}
`; // 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 = {};
diff --git a/tests/markdown/mustache-syntax.test.js b/tests/markdown/mustache-syntax.test.js
index 4953d0bed..b32876353 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('
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');
});
- 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.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 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();
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
');
});
- it.failing('Renders an image element with injected style', function() {
+ it('Renders an image element with injected style', function() {
const source = '{position:absolute}';
const rendered = Markdown.render(source).trimReturns();
- expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('

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

');
});
- 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}
');
@@ -306,55 +336,100 @@ describe('Injection: When an injection tag follows an element', ()=>{
it('Renders an image with added attributes', function() {
const source = ` {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(`

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

`);
});
});
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('
');
});
- 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('
');
});
- 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('
');
});
- 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(`
`);
+ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`
`);
});
- 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(`
`);
+ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`
`);
});
- it.failing('renders an h2 header "text" with injected class name', function() {
+ 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('
');
+ });
+
+ 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('
');
+ });
+
+ 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('
');
+ });
+
+ 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('
');
+ });
+
+ 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('
');
+ });
+
+ 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.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
? 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.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 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 = `{width:100px}`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
- 
`.trimReturns());
+ 
`.trimReturns());
});
it('Renders normal links', function() {