diff --git a/package.json b/package.json index 3fca8ab..938c009 100644 --- a/package.json +++ b/package.json @@ -39,29 +39,25 @@ "dependencies": { "@types/mdast": "^4.0.0", "hast-util-sanitize": "^5.0.0", - "hast-util-to-html": "^9.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0" }, "devDependencies": { "@types/hast": "^3.0.0", - "@types/tape": "^5.0.0", + "@types/node": "^20.0.0", "c8": "^8.0.0", "commonmark.json": "^0.30.0", - "is-hidden": "^2.0.0", + "hast-util-from-html": "^2.0.0", + "hast-util-to-html": "^9.0.0", "prettier": "^3.0.0", - "rehype-parse": "^9.0.0", - "rehype-stringify": "^10.0.0", - "remark": "^15.0.0", "remark-cli": "^11.0.0", "remark-frontmatter": "^5.0.0", "remark-gfm": "^4.0.0", "remark-github": "^12.0.0", + "remark-parse": "^11.0.0", "remark-preset-wooorm": "^9.0.0", "remark-slug": "^7.0.0", "remark-toc": "^9.0.0", - "tape": "^5.0.0", - "to-vfile": "^8.0.0", "type-coverage": "^2.0.0", "typescript": "^5.0.0", "vfile": "^6.0.0", diff --git a/test/fixtures/blockquote/output.md b/test/fixtures/blockquote/output.md new file mode 100644 index 0000000..4a95075 --- /dev/null +++ b/test/fixtures/blockquote/output.md @@ -0,0 +1,16 @@ +

Block Quote

+
+ +

Paragraph.

+
diff --git a/test/fixtures/code/output.md b/test/fixtures/code/output.md new file mode 100644 index 0000000..f14ae1a --- /dev/null +++ b/test/fixtures/code/output.md @@ -0,0 +1,14 @@ +

Code

+
alert('some JavaScript code.');
+
+
foo bar baz
+
+
alpha bravo charlie
+
+
+
  two spaces
+	one
+		two
+	one
+	  mixed.
+
diff --git a/test/fixtures/entities-named/output.md b/test/fixtures/entities-named/output.md new file mode 100644 index 0000000..071e009 --- /dev/null +++ b/test/fixtures/entities-named/output.md @@ -0,0 +1,31 @@ +

Entities

+

Plain text:

+

AT&T with entity, AT&T with numeric entity, AT&T without entity.

+

Fenced code language flags:

+
Something in the AT&T language
+
+
Something in the AT&T language
+
+
Something in the AT&T language
+
+

Automatic links:

+

http://at&t.com, http://at&t.com, and http://at&t.com.

+

Link href:

+

With entity, numeric entity, without entity.

+

Link title:

+

With entity, numeric entity, without entity.

+

Image src:

+

With entity, numeric entity, without entity.

+

Image alt:

+

AT&T with entity, AT&T with numeric entity, AT&T without entity.

+

Image title:

+

With entity, numeric entity, without entity.

+

Reference link:

+

Entity, Numeric entity, Literal.

+

Entity, Numeric entity, Literal.

+

Reference title:

+

Entity, Numeric entity, Literal.

+

Entity, Numeric entity, Literal.

+

Image Reference alt:

+

AT&T with entity, AT&T with numeric entity, AT&T without entity.

+

Definitions:

diff --git a/test/fixtures/entities-numerical/output.md b/test/fixtures/entities-numerical/output.md new file mode 100644 index 0000000..c4fa09e --- /dev/null +++ b/test/fixtures/entities-numerical/output.md @@ -0,0 +1,31 @@ +

Entities

+

Plain text:

+

AT&T with entity, AT&T with numeric entity, AT&T without entity.

+

Fenced code language flags:

+
Something in the AT&T language
+
+
Something in the AT&T language
+
+
Something in the AT&T language
+
+

Automatic links:

+

http://at&t.com, http://at&t.com, and http://at&t.com.

+

Link href:

+

With entity, numeric entity, without entity.

+

Link title:

+

With entity, numeric entity, without entity.

+

Image src:

+

With entity, numeric entity, without entity.

+

Image alt:

+

AT&T with entity, AT&T with numeric entity, AT&T without entity.

+

Image title:

+

With entity, numeric entity, without entity.

+

Reference link:

+

Entity, Numeric entity, Literal.

+

Entity, Numeric entity, Literal.

+

Reference title:

+

Entity, Numeric entity, Literal.

+

Entity, Numeric entity, Literal.

+

Image Reference alt:

+

AT&T with entity, AT&T with numeric entity, AT&T without entity.

+

Definitions:

diff --git a/test/fixtures/escape-commonmark/output.md b/test/fixtures/escape-commonmark/output.md new file mode 100644 index 0000000..934d342 --- /dev/null +++ b/test/fixtures/escape-commonmark/output.md @@ -0,0 +1,39 @@ +

These should all get escaped:

+

Backslash: \

+

Backtick: `

+

Asterisk: *

+

Underscore: _

+

Left brace: {

+

Right brace: }

+

Left bracket: [

+

Right bracket: ]

+

Left paren: (

+

Right paren: )

+

Greater-than: >

+

Hash: #

+

Period: .

+

Bang: !

+

Plus: +

+

Minus: -

+

GFM:

+

Pipe: |

+

Tilde: ~

+

Commonmark:

+

Quote: "

+

Dollar: $

+

Percentage: %

+

Ampersand: &

+

Single quote: '

+

Comma: ,

+

Forward slash: /

+

Colon: :

+

Semicolon: ;

+

Less-than: <

+

Equals: =

+

Question mark: ?

+

At-sign: @

+

Caret: ^

+

New line:
+only works in paragraphs.

+

Two spaces:
+only works in paragraphs.

diff --git a/test/fixtures/escape/output.md b/test/fixtures/escape/output.md new file mode 100644 index 0000000..934d342 --- /dev/null +++ b/test/fixtures/escape/output.md @@ -0,0 +1,39 @@ +

These should all get escaped:

+

Backslash: \

+

Backtick: `

+

Asterisk: *

+

Underscore: _

+

Left brace: {

+

Right brace: }

+

Left bracket: [

+

Right bracket: ]

+

Left paren: (

+

Right paren: )

+

Greater-than: >

+

Hash: #

+

Period: .

+

Bang: !

+

Plus: +

+

Minus: -

+

GFM:

+

Pipe: |

+

Tilde: ~

+

Commonmark:

+

Quote: "

+

Dollar: $

+

Percentage: %

+

Ampersand: &

+

Single quote: '

+

Comma: ,

+

Forward slash: /

+

Colon: :

+

Semicolon: ;

+

Less-than: <

+

Equals: =

+

Question mark: ?

+

At-sign: @

+

Caret: ^

+

New line:
+only works in paragraphs.

+

Two spaces:
+only works in paragraphs.

diff --git a/test/fixtures/html-sanitize/output.md b/test/fixtures/html-sanitize/output.md new file mode 100644 index 0000000..f59dfe9 --- /dev/null +++ b/test/fixtures/html-sanitize/output.md @@ -0,0 +1,3 @@ +

Foo bar baz qux.

+

heading

+

Alpha bravo charlie.

diff --git a/test/fixtures/html/output.md b/test/fixtures/html/output.md new file mode 100644 index 0000000..6cec140 --- /dev/null +++ b/test/fixtures/html/output.md @@ -0,0 +1,2 @@ +

Alpha

+

Foo bar baz qux.

diff --git a/test/fixtures/images/output.md b/test/fixtures/images/output.md new file mode 100644 index 0000000..11dab25 --- /dev/null +++ b/test/fixtures/images/output.md @@ -0,0 +1,6 @@ +

Example

+

Example

+

+

+

+

diff --git a/test/fixtures/links/output.md b/test/fixtures/links/output.md new file mode 100644 index 0000000..62e2f4a --- /dev/null +++ b/test/fixtures/links/output.md @@ -0,0 +1,6 @@ +

Example

+

Example

+

+

+

+

diff --git a/test/fixtures/list/output.md b/test/fixtures/list/output.md new file mode 100644 index 0000000..bd5dfdb --- /dev/null +++ b/test/fixtures/list/output.md @@ -0,0 +1,36 @@ +

List

+ +
    +
  1. One;
  2. +
  3. Two;
  4. +
+ +
    +
  1. Four.
  2. +
  3. Five.
  4. +
+ +
+
And a rule.
+
diff --git a/test/fixtures/references/output.md b/test/fixtures/references/output.md new file mode 100644 index 0000000..f074a30 --- /dev/null +++ b/test/fixtures/references/output.md @@ -0,0 +1,6 @@ +

References

+

Entities contains some serious entity tests relating to titles and links +in definitions.

+

However, the [missing], [missing][], and [missing][missing] are omitted.

+

However, the ![missing], ![missing][], and ![missing][missing] are omitted.

+

Same goes for [][empty] and ![][empty].

diff --git a/test/fixtures/rule/output.md b/test/fixtures/rule/output.md new file mode 100644 index 0000000..4851e81 --- /dev/null +++ b/test/fixtures/rule/output.md @@ -0,0 +1,4 @@ +

Horizontal Rules

+
+
+
diff --git a/test/fixtures/self-closing/output.md b/test/fixtures/self-closing/output.md new file mode 100644 index 0000000..e6df097 --- /dev/null +++ b/test/fixtures/self-closing/output.md @@ -0,0 +1,4 @@ +

Hello
+world

+
+

Favicon

diff --git a/test/index.js b/test/index.js index 2258722..bdebdd0 100644 --- a/test/index.js +++ b/test/index.js @@ -2,284 +2,294 @@ * @typedef {import('mdast').Root} Root * @typedef {import('mdast').Paragraph} Paragraph * @typedef {import('hast').Element} Element - * @typedef {import('vfile').VFile} VFile + * @typedef {import('unified').Pluggable} Pluggable * @typedef {import('../index.js').Options} Options */ -import path from 'node:path' -import fs from 'node:fs' -import test from 'tape' -import {isHidden} from 'is-hidden' +import assert from 'node:assert/strict' +import fs from 'node:fs/promises' +import process from 'node:process' +import test from 'node:test' import {commonmark} from 'commonmark.json' -import {toVFile} from 'to-vfile' -import {unified} from 'unified' -import {remark} from 'remark' -import remarkParse from 'remark-parse' -import remarkSlug from 'remark-slug' +import {fromHtml} from 'hast-util-from-html' +import {toHtml} from 'hast-util-to-html' import remarkFrontmatter from 'remark-frontmatter' import remarkGfm from 'remark-gfm' import remarkGithub from 'remark-github' +import remarkParse from 'remark-parse' +import remarkSlug from 'remark-slug' import remarkToc from 'remark-toc' -import rehypeParse from 'rehype-parse' -import rehypeStringify from 'rehype-stringify' +import {unified} from 'unified' +import {VFile} from 'vfile' import remarkHtml from '../index.js' -test('remarkHtml', (t) => { - t.doesNotThrow(() => { - remark().use(remarkHtml).freeze() - }, 'should not throw if not passed options') - - const processorDangerous1 = remark().use(remarkHtml, {sanitize: false}) - - t.equal( - // @ts-expect-error: unknown node. - processorDangerous1.stringify({type: 'alpha'}), - '
', - 'should stringify unknown nodes' - ) - - t.equal( - processorDangerous1.stringify({ - // @ts-expect-error: unknown node. - type: 'alpha', - children: [{type: 'strong', children: [{type: 'text', value: 'bravo'}]}] - }), - '
bravo
', - 'should stringify unknown nodes' - ) - - t.equal( - processorDangerous1.stringify({ - // @ts-expect-error: unknown node. - type: 'alpha', - children: [{type: 'text', value: 'bravo'}], - data: { - hName: 'i', - hProperties: {className: 'charlie'}, - hChildren: [{type: 'text', value: 'delta'}] - } - }), - 'delta', - 'should stringify unknown nodes' - ) - - const processorDangerous2 = remark().use(remarkHtml, { - sanitize: false, - handlers: { - /** @param {Paragraph} node */ - paragraph(state, node) { - const head = node.children[0] - - if (head.type === 'text') { - head.value = 'changed' - } - - /** @type {Element} */ - const result = { - type: 'element', - tagName: 'p', - properties: {}, - children: state.all(node) - } - state.patch(node, result) - return state.applyData(node, result) - } - } +test('remarkHtml', async function (t) { + await t.test('should stringify unknown nodes', async function () { + assert.equal( + unified() + .use(remarkParse) + .use(remarkHtml, {sanitize: false}) + .stringify({type: 'alpha'}), + '
' + ) }) - t.equal( - processorDangerous2.processSync('paragraph text').toString(), - '

changed

\n', - 'should allow overriding handlers' - ) - - const processorDangerous3 = remark() - .use( - /** @type {import('unified').Plugin} */ - () => (ast) => { - // @ts-expect-error: assume it exists. - ast.children[0].children[0].data = { - hProperties: {title: 'overwrite'} - } - } + await t.test('should stringify unknown nodes', async function () { + assert.equal( + unified() + .use(remarkParse) + .use(remarkHtml, {sanitize: false}) + .stringify({ + type: 'alpha', + // @ts-expect-error: unknown node. + children: [ + {type: 'strong', children: [{type: 'text', value: 'bravo'}]} + ] + }), + '
bravo
' ) - .use(remarkHtml, {sanitize: false}) - - t.equal( - processorDangerous3 - .processSync('![hello](example.jpg "overwritten")') - .toString(), - '

hello

\n', - 'should patch and merge attributes' - ) + }) - const processorDangerous4 = remark() - .use( - /** @type {import('unified').Plugin} */ - () => (ast) => { - // @ts-expect-error: assume it exists. - ast.children[0].children[0].data = {hName: 'b'} - } + await t.test('should stringify unknown nodes', async function () { + assert.equal( + unified() + .use(remarkParse) + .use(remarkHtml, {sanitize: false}) + .stringify({ + type: 'alpha', + // @ts-expect-error: unknown node. + children: [{type: 'text', value: 'bravo'}], + data: { + hName: 'i', + hProperties: {className: 'charlie'}, + hChildren: [{type: 'text', value: 'delta'}] + } + }), + 'delta' ) - .use(remarkHtml, {sanitize: false}) - - t.equal( - processorDangerous4.processSync('**Bold!**').toString(), - '

Bold!

\n', - 'should overwrite a tag-name' - ) + }) - const processorDangerous5 = remark() - .use( - /** @type {import('unified').Plugin} */ - () => (ast) => { - // @ts-expect-error: assume it exists. - const code = ast.children[0].children[0] - - code.data = { - hChildren: [ - { - type: 'element', - tagName: 'span', - properties: {className: ['token']}, - children: [{type: 'text', value: code.value}] + await t.test('should allow overriding handlers', async function () { + assert.equal( + String( + await unified() + .use(remarkParse) + .use(remarkHtml, { + sanitize: false, + handlers: { + /** @param {Paragraph} node */ + paragraph(state, node) { + const head = node.children[0] + + if (head.type === 'text') { + head.value = 'changed' + } + + /** @type {Element} */ + const result = { + type: 'element', + tagName: 'p', + properties: {}, + children: state.all(node) + } + state.patch(node, result) + return state.applyData(node, result) + } } - ] - } - } + }) + .process('paragraph text') + ), + '

changed

\n' ) - .use(remarkHtml, {sanitize: false}) - - t.equal( - processorDangerous5.processSync('`var`').toString(), - '

var

\n', - 'should overwrite content' - ) + }) - const processorDangerous6 = remark() - .use( - /** @type {import('unified').Plugin} */ - () => (ast) => { - // @ts-expect-error: assume it exists. - const code = ast.children[0].children[0] - - code.data = { - hChildren: [ - { - type: 'element', - tagName: 'output', - properties: {className: ['token']}, - children: [{type: 'text', value: code.value}] + await t.test('should patch and merge attributes', async function () { + assert.equal( + String( + await unified() + .use(remarkParse) + .use( + /** @type {import('unified').Plugin} */ + () => (ast) => { + // @ts-expect-error: assume it exists. + ast.children[0].children[0].data = { + hProperties: {title: 'overwrite'} + } } - ] - } - } + ) + .use(remarkHtml, {sanitize: false}) + .process('![hello](example.jpg "overwritten")') + ), + '

hello

\n' ) - .use(remarkHtml, {sanitize: true}) - - t.equal( - processorDangerous6.processSync('`var`').toString(), - '

var

\n', - 'should not overwrite content in `sanitize` mode' - ) + }) - const processorDangerous7 = remark() - .use( - /** @type {import('unified').Plugin} */ - () => (ast) => { - ast.children[0].data = { - hProperties: {className: 'foo'} - } - } + await t.test('should overwrite a tag-name', async function () { + assert.equal( + String( + await unified() + .use(remarkParse) + .use( + /** @type {import('unified').Plugin} */ + () => (ast) => { + // @ts-expect-error: assume it exists. + ast.children[0].children[0].data = {hName: 'b'} + } + ) + .use(remarkHtml, {sanitize: false}) + .process('**Bold!**') + ), + '

Bold!

\n' ) - .use(remarkHtml, {sanitize: false}) - - t.equal( - processorDangerous7.processSync('```js\nvar\n```\n').toString(), - '
var\n
\n', - 'should overwrite classes on code' - ) - - t.equal( - remark() - .use(remarkHtml) - .processSync('## Hello world') - .toString(), - '

Hello world

\n', - 'should be `sanitation: true` by default' - ) - - t.equal( - remark() - .use(remarkHtml, {sanitize: true}) - .processSync('## Hello world') - .toString(), - '

Hello world

\n', - 'should support sanitation: true' - ) - - t.equal( - remark() - .use(remarkHtml, {sanitize: null}) - .processSync('## Hello world') - .toString(), - '

Hello world

\n', - 'should support sanitation: null' - ) + }) - t.equal( - remark() - .use(remarkHtml, {sanitize: false}) - .processSync('## Hello world') - .toString(), - '

Hello world

\n', - 'should support sanitation: false' - ) + await t.test('should overwrite content', async function () { + assert.equal( + String( + await unified() + .use(remarkParse) + .use( + /** @type {import('unified').Plugin} */ + () => (ast) => { + // @ts-expect-error: assume it exists. + const code = ast.children[0].children[0] + + code.data = { + hChildren: [ + { + type: 'element', + tagName: 'span', + properties: {className: ['token']}, + children: [{type: 'text', value: code.value}] + } + ] + } + } + ) + .use(remarkHtml, {sanitize: false}) + .process('`var`') + ), + '

var

\n' + ) + }) - t.equal( - remark() - .use(remarkHtml, {sanitize: {tagNames: []}}) - .processSync('## Hello world') - .toString(), - 'Hello world\n', - 'should support sanitation schemas' + await t.test( + 'should not overwrite content in `sanitize` mode', + async function () { + assert.equal( + String( + await unified() + .use(remarkParse) + .use( + /** @type {import('unified').Plugin} */ + () => (ast) => { + // @ts-expect-error: assume it exists. + const code = ast.children[0].children[0] + + code.data = { + hChildren: [ + { + type: 'element', + tagName: 'output', + properties: {className: ['token']}, + children: [{type: 'text', value: code.value}] + } + ] + } + } + ) + .use(remarkHtml, {sanitize: true}) + .process('`var`') + ), + '

var

\n' + ) + } ) - t.end() -}) - -// Assert fixtures. -test('Fixtures', (t) => { - const base = path.join('test', 'fixtures') - const files = fs.readdirSync(base) - let index = -1 - - while (++index < files.length) { - const name = files[index] - - if (isHidden(name)) continue + await t.test('should overwrite classes on code', async function () { + assert.equal( + String( + await unified() + .use(remarkParse) + .use( + /** @type {import('unified').Plugin} */ + () => (ast) => { + ast.children[0].data = { + hProperties: {className: 'foo'} + } + } + ) + .use(remarkHtml, {sanitize: false}) + .process('```js\nvar\n```\n') + ), + '
var\n
\n' + ) + }) - const output = String(fs.readFileSync(path.join(base, name, 'output.html'))) - const input = String(fs.readFileSync(path.join(base, name, 'input.md'))) - const file = toVFile({path: name + '.md', value: input}) - let config = {} + await t.test('should be `sanitation: true` by default', async function () { + assert.equal( + String( + await unified() + .use(remarkParse) + .use(remarkHtml) + .process('## Hello world') + ), + '

Hello world

\n' + ) + }) - try { - config = JSON.parse( - String(fs.readFileSync(path.join(base, name, 'config.json'))) - ) - } catch {} + await t.test('should support sanitation: true', async function () { + assert.equal( + String( + await unified() + .use(remarkParse) + .use(remarkHtml, {sanitize: true}) + .process('## Hello world') + ), + '

Hello world

\n' + ) + }) - const result = processSync(file, config) + await t.test('should support sanitation: null', async function () { + assert.equal( + String( + await unified() + .use(remarkParse) + .use(remarkHtml, {sanitize: null}) + .process('## Hello world') + ), + '

Hello world

\n' + ) + }) - t.equal(result, output, 'should work on `' + name + '`') - } + await t.test('should support sanitation: false', async function () { + assert.equal( + String( + await unified() + .use(remarkParse) + .use(remarkHtml, {sanitize: false}) + .process('## Hello world') + ), + '

Hello world

\n' + ) + }) - t.end() + await t.test('should support sanitation schemas', async function () { + assert.equal( + String( + await unified() + .use(remarkParse) + .use(remarkHtml, {sanitize: {tagNames: []}}) + .process('## Hello world') + ), + 'Hello world\n' + ) + }) }) -test('CommonMark', (t) => { - const skip = new Set([623, 624]) +test('CommonMark', async function (t) { + /** @type {Set} */ + const skip = new Set() let start = 0 let index = -1 /** @type {string|undefined} */ @@ -293,76 +303,134 @@ test('CommonMark', (t) => { continue } - if (section !== example.section) { - section = example.section - start = index - } + await t.test( + index + ': ' + example.section + ' (' + (index - start + 1) + ')', + async function () { + if (section !== example.section) { + section = example.section + start = index + } - const actual = unified() - .use(remarkParse) - .use(remarkHtml, {sanitize: false}) - .processSync(example.markdown) - .toString() - - const reformat = unified() - .use(rehypeParse, {fragment: true}) - .use(rehypeStringify) - - // Normalize meaningless stuff, like character references, `
` is `
`, - // etc. - t.equal( - String(reformat.processSync(actual)), - String(reformat.processSync(example.html)), - index + ': ' + example.section + ' (' + (index - start + 1) + ')' + const actual = String( + await unified() + .use(remarkParse) + .use(remarkHtml, {sanitize: false}) + .process(example.markdown) + ) + + // Normalize meaningless stuff, like character references, `
` is `
`, + // etc. + assert.equal( + String(toHtml(fromHtml(actual))), + String(toHtml(fromHtml(actual))) + ) + } ) } +}) + +test('fixtures', async function (t) { + const base = new URL('fixtures/', import.meta.url) + const files = await fs.readdir(base) + let index = -1 + + while (++index < files.length) { + const folder = files[index] + + if (folder.startsWith('.')) continue + + await t.test(folder, async function () { + const folderUrl = new URL(folder + '/', base) + const inputUrl = new URL('input.md', folderUrl) + const outputUrl = new URL('output.html', folderUrl) + const configUrl = new URL('config.json', folderUrl) + const input = String(await fs.readFile(inputUrl)) + /** @type {Options | undefined} */ + let config + /** @type {string} */ + let output + + try { + config = JSON.parse(String(await fs.readFile(configUrl))) + } catch {} + + const actual = String( + await unified() + .use(remarkParse) + // @ts-expect-error: to do. + .use(remarkHtml, config) + .process(input) + ) + + try { + if ('UPDATE' in process.env) { + throw new Error('Updating…') + } + + output = String(await fs.readFile(outputUrl)) + } catch { + output = actual + await fs.writeFile(outputUrl, actual) + } - t.end() + assert.equal(actual, String(output)) + }) + } }) -test('Integrations', (t) => { +test('integrations', async function (t) { + /** @type {Record} */ const integrationMap = { footnotes: remarkGfm, frontmatter: remarkFrontmatter, gfm: remarkGfm, github: remarkGithub, - toc: [remarkSlug, remarkToc] + toc: [ + // @ts-expect-error: legacy. + // To do: remove? + remarkSlug, + remarkToc + ] } - const base = path.join('test', 'integrations') - const files = /** @type {(keyof integrationMap)[]} */ (fs.readdirSync(base)) + const base = new URL('integrations/', import.meta.url) + const files = await fs.readdir(base) let index = -1 while (++index < files.length) { - const name = files[index] + const folder = files[index] + + if (folder.startsWith('.')) continue + + await t.test('should integrate w/ `' + folder + '`', async function () { + const folderUrl = new URL(folder + '/', base) + const inputUrl = new URL('input.md', folderUrl) + const outputUrl = new URL('output.html', folderUrl) + const input = String(await fs.readFile(inputUrl)) + + const actual = String( + await unified() + .use(remarkParse) + // @ts-expect-error: to do. + .use(integrationMap[folder]) + .use(remarkHtml, {sanitize: false}) + .process(new VFile({path: folder + '.md', value: input})) + ) - if (isHidden(name)) continue + /** @type {string} */ + let output - const output = String(fs.readFileSync(path.join(base, name, 'output.html'))) - const input = String(fs.readFileSync(path.join(base, name, 'input.md'))) - const file = toVFile({path: name + '.md', value: input}) - const result = remark() - // @ts-expect-error: fine. - .use(integrationMap[name]) - .use(remarkHtml, {sanitize: false}) - .processSync(file) - .toString() + try { + if ('UPDATE' in process.env) { + throw new Error('Updating…') + } - t.equal(result, output, 'should integrate w/ `' + name + '`') - } + output = String(await fs.readFile(outputUrl)) + } catch { + output = actual + await fs.writeFile(outputUrl, actual) + } - t.end() + assert.equal(actual, String(output)) + }) + } }) - -/** - * @param {VFile} file - * @param {Options} [config] - */ -function processSync(file, config) { - return ( - remark() - // @ts-expect-error: to do: fix. - .use(remarkHtml, config) - .processSync(file) - .toString() - ) -}