/** * @typedef {import('unified').PluggableList} PluggableList * @typedef {import('unified').Plugin<[unknown]>} Plugin * * @typedef {import('./script/info.js').Check} Check * @typedef {import('./script/info.js').PluginInfo} PluginInfo */ import assert from 'node:assert/strict' import test from 'node:test' import {controlPictures} from 'control-pictures' import {remark} from 'remark' import remarkDirective from 'remark-directive' import remarkFrontmatter from 'remark-frontmatter' import remarkGfm from 'remark-gfm' import remarkLint from 'remark-lint' import remarkLintFinalNewline from 'remark-lint-final-newline' import remarkLintNoHeadingPunctuation from 'remark-lint-no-heading-punctuation' import remarkLintNoMultipleToplevelHeadings from 'remark-lint-no-multiple-toplevel-headings' import remarkLintNoUndefinedReferences from 'remark-lint-no-undefined-references' import remarkMath from 'remark-math' import remarkMdx from 'remark-mdx' import {lintRule} from 'unified-lint-rule' import {removePosition} from 'unist-util-remove-position' import {VFile} from 'vfile' import {plugins} from './script/info.js' test('remark-lint', async function (t) { await t.test('should expose the public api', async function () { assert.deepEqual(Object.keys(await import('remark-lint')).sort(), [ 'default' ]) }) const value = [ '# A heading', '', '# Another main heading.', '', '', '', '# Another main heading.' ].join('\n') await t.test('should support `remark-lint` last', async function () { const file = await remark() .use(remarkLintNoHeadingPunctuation) .use(remarkLintNoMultipleToplevelHeadings) .use(remarkLint) .process({path: 'virtual.md', value}) assert.deepEqual(file.messages.map(String), [ 'virtual.md:3:1-3:24: Unexpected character `.` at end of heading, remove it', 'virtual.md:3:1-3:24: Unexpected duplicate toplevel heading, exected a single heading with rank `1`' ]) }) await t.test('should support `remark-lint` first', async function () { const file = await remark() .use(remarkLint) .use(remarkLintNoHeadingPunctuation) .use(remarkLintNoMultipleToplevelHeadings) .process({path: 'virtual.md', value}) assert.deepEqual(file.messages.map(String), [ 'virtual.md:3:1-3:24: Unexpected character `.` at end of heading, remove it', 'virtual.md:3:1-3:24: Unexpected duplicate toplevel heading, exected a single heading with rank `1`' ]) }) await t.test('should support no rules', async function () { const file = await remark().use(remarkLint).process('.') assert.deepEqual(file.messages, []) }) await t.test('should support successful rules', async function () { const file = await remark().use(remarkLintFinalNewline).process('') assert.deepEqual(file.messages, []) }) await t.test('should support a list with a severity', async function () { const file = await remark().use(remarkLintFinalNewline, [2]).process('.') assert.deepEqual(file.messages.map(jsonClone), [ { column: 2, fatal: true, line: 1, message: 'Unexpected missing final newline character, expected line feed (`\\n`) at end of file', name: '1:2', place: {column: 2, line: 1, offset: 1}, reason: 'Unexpected missing final newline character, expected line feed (`\\n`) at end of file', ruleId: 'final-newline', source: 'remark-lint', url: 'https://github.com/remarkjs/remark-lint/tree/main/packages/remark-lint-final-newline#readme' } ]) }) await t.test('should support a boolean (`true`)', async function () { const file = await remark().use(remarkLintFinalNewline, true).process('.') assert.deepEqual(file.messages.map(String), [ '1:2: Unexpected missing final newline character, expected line feed (`\\n`) at end of file' ]) }) await t.test('should support a boolean (`false`)', async function () { const file = await remark().use(remarkLintFinalNewline, false).process('.') assert.deepEqual(file.messages, []) }) await t.test( 'should support a list with a boolean severity (true, for on)', async function () { const file = await remark() .use(remarkLintFinalNewline, [true]) .process('.') assert.deepEqual(file.messages.map(String), [ '1:2: Unexpected missing final newline character, expected line feed (`\\n`) at end of file' ]) } ) await t.test( 'should support a list with boolean severity (false, for off)', async function () { const file = await remark() .use(remarkLintFinalNewline, [false]) .process('.') assert.deepEqual(file.messages, []) } ) await t.test( 'should support a list with string severity (`error`)', async function () { const file = await remark() .use(remarkLintFinalNewline, ['error']) .process('.') assert.deepEqual(file.messages.map(jsonClone), [ { column: 2, fatal: true, line: 1, message: 'Unexpected missing final newline character, expected line feed (`\\n`) at end of file', name: '1:2', place: {column: 2, line: 1, offset: 1}, reason: 'Unexpected missing final newline character, expected line feed (`\\n`) at end of file', ruleId: 'final-newline', source: 'remark-lint', url: 'https://github.com/remarkjs/remark-lint/tree/main/packages/remark-lint-final-newline#readme' } ]) } ) await t.test( 'should support a list with string severity (`on`)', async function () { const file = await remark() .use(remarkLintFinalNewline, ['on']) .process('.') assert.deepEqual(file.messages.map(jsonClone), [ { column: 2, fatal: false, line: 1, message: 'Unexpected missing final newline character, expected line feed (`\\n`) at end of file', name: '1:2', place: {column: 2, line: 1, offset: 1}, reason: 'Unexpected missing final newline character, expected line feed (`\\n`) at end of file', ruleId: 'final-newline', source: 'remark-lint', url: 'https://github.com/remarkjs/remark-lint/tree/main/packages/remark-lint-final-newline#readme' } ]) } ) await t.test( 'should support a list with string severity (`warn`)', async function () { const file = await remark() .use(remarkLintFinalNewline, ['warn']) .process('.') assert.deepEqual(file.messages.map(jsonClone), [ { column: 2, fatal: false, line: 1, message: 'Unexpected missing final newline character, expected line feed (`\\n`) at end of file', name: '1:2', place: {column: 2, line: 1, offset: 1}, reason: 'Unexpected missing final newline character, expected line feed (`\\n`) at end of file', ruleId: 'final-newline', source: 'remark-lint', url: 'https://github.com/remarkjs/remark-lint/tree/main/packages/remark-lint-final-newline#readme' } ]) } ) await t.test( 'should support a list with string severity (`off`)', async function () { const file = await remark() .use(remarkLintFinalNewline, ['off']) .process('.') assert.deepEqual(file.messages, []) } ) await t.test( 'should fail on incorrect severities (too high)', async function () { assert.throws(function () { remark().use(remarkLintFinalNewline, [3]).freeze() }, /^Error: Incorrect severity `3` for `final-newline`, expected 0, 1, or 2$/) } ) await t.test( 'should fail on incorrect severities (too low)', async function () { assert.throws(function () { remark().use(remarkLintFinalNewline, [-1]).freeze() }, /^Error: Incorrect severity `-1` for `final-newline`, expected 0, 1, or 2$/) } ) await t.test( 'should support regex as options (remark-lint-no-undefined-references)', async function () { const file = await remark() .use(remarkLintNoUndefinedReferences, {allow: [/^b\./i]}) .process({ path: 'virtual.md', value: ['[foo][b.c]', '', '[bar][b]'].join('\n') }) assert.deepEqual(file.messages.map(String), [ 'virtual.md:3:1-3:9: Unexpected reference to undefined definition, expected corresponding definition (`b`) for a link or escaped opening bracket (`\\[`) for regular text' ]) } ) await t.test( 'should support meta as a string (unified-lint-rule)', async function () { const file = await remark() .use( lintRule('test:rule', function (_, file) { file.message('Test message') }), ['warn'] ) .process('.') assert.deepEqual(file.messages.map(jsonClone), [ { fatal: false, message: 'Test message', name: '1:1', reason: 'Test message', ruleId: 'rule', source: 'test' } ]) } ) }) test('plugins', async function (t) { for (const plugin of plugins) { await t.test(plugin.name, async function (t) { await assertPlugin(plugin, t) }) } }) /** * @param {PluginInfo} info * Info. * @param {any} t * Test context. * @returns {Promise} * Nothing. */ // type-coverage:ignore-next-line -- `TestContext` not exposed from `node:test`. async function assertPlugin(info, t) { /** @type {{default: Plugin}} */ const pluginModule = await import(info.name) const plugin = pluginModule.default for (const check of info.checks) { const name = check.name + ':' + check.configuration // type-coverage:ignore-next-line -- `TestContext` not exposed from `node:test`. await t.test(name, async function () { await assertCheck(plugin, info, check) }) } } /** * @param {Plugin} plugin * Plugin. * @param {PluginInfo} info * info. * @param {Check} check * Check. * @returns {Promise} * Nothing. */ async function assertCheck(plugin, info, check) { /** @type {{config: unknown}} */ const {config} = JSON.parse(check.configuration) /** @type {PluggableList} */ const extras = [] const value = controlPictures(check.input) if (check.directive) extras.push(remarkDirective) if (check.frontmatter) extras.push(remarkFrontmatter) if (check.gfm) extras.push(remarkGfm) if (check.math) extras.push(remarkMath) if (check.mdx) extras.push(remarkMdx) const file = await remark() .use(plugin, config) .use(extras) .process(new VFile({path: check.name, value})) for (const message of file.messages) { assert.equal(message.ruleId, info.ruleId) assert.equal( message.url, 'https://github.com/remarkjs/remark-lint/tree/main/packages/remark-lint-' + info.ruleId + '#readme' ) } assert.deepEqual( file.messages.map(String).map(function (value) { return value.slice(value.indexOf(':') + 1) }), check.output ) if (!check.positionless) { const file = await remark() .use(function () { return function (tree) { removePosition(tree) } }) .use(plugin, config) .use(extras) .process(new VFile({path: check.name, value})) assert.deepEqual(file.messages, []) } } /** * @param {unknown} d * Value. * @returns {unknown} * Cloned value. */ function jsonClone(d) { return JSON.parse(JSON.stringify(d)) }