const { expect } = require('chai'); const unified = require('unified'); const markdown = require('remark-parse'); const mdStringify = require('remark-html'); const { remarkExtend } = require('../src/remarkExtend.js'); async function expectThrowsAsync(method, errorMessage) { let error = null; try { await method(); } catch (err) { error = err; } expect(error).to.be.an('Error', 'No error was thrown'); if (errorMessage) { expect(error.message).to.equal(errorMessage); } } describe('remarkExtend', () => { it('does no modifications if no action is found', async () => { const input = [ '### Red', '', 'red is the fire', '', '#### More Red', '', 'the sun can get red', ].join('\n'); const extendMd = ''; const output = [ '

Red

', '

red is the fire

', '

More Red

', '

the sun can get red

', '', ].join('\n'); const parser = unified() // .use(markdown) .use(remarkExtend, { extendMd }) .use(mdStringify); const result = await parser.process(input); expect(result.contents).to.equal(output); }); it('can do replacements on the full file', async () => { const input = [ '### Red', // <-- start '', 'red is the fire', '', '#### More Red', '', 'the sun can get red', ].join('\n'); const extendMd = [ // "```js ::replaceFrom(':root')", 'module.exports.replaceSection = (node) => {', ' if (node.value) {', " node.value = node.value.replace(/red/g, 'green').replace(/Red/g, 'Green');", ' }', ' return node;', '};', '```', ].join('\n'); const output = [ '

Green

', // <-- start '

green is the fire

', '

More Green

', '

the sun can get green

', '', ].join('\n'); const parser = unified() // .use(markdown) .use(remarkExtend, { extendMd }) .use(mdStringify); const result = await parser.process(input); expect(result.contents).to.equal(output); }); it('can replace from a starting point downward', async () => { const input = [ '### Red', '', 'red is the fire', '', '### More Red', // <-- start '', 'the sun can get red', ].join('\n'); const extendMd = [ "```js ::replaceFrom('heading[depth=3]:has([value=More Red])')", 'module.exports.replaceSection = (node) => {', ' if (node.value) {', " node.value = node.value.replace(/red/g, 'green').replace(/Red/g, 'Green');", ' }', ' return node;', '};', '```', ].join('\n'); const output = [ '

Red

', '

red is the fire

', '

More Green

', // <-- start '

the sun can get green

', '', ].join('\n'); const parser = unified() // .use(markdown) .use(remarkExtend, { extendMd }) .use(mdStringify); const result = await parser.process(input); expect(result.contents).to.equal(output); }); it('can replace within a range (start point included, endpoint not)', async () => { const input = [ '### Red', '', 'red is the fire', '', 'red is the cross', // <-- start '', 'red is the flag', '', '#### More Red', '', 'the sun can get red', ].join('\n'); const extendMd = [ "```js ::replaceBetween('heading:has([value=Red]) ~ paragraph:nth-of-type(2)', 'heading:has([value=More Red])')", 'module.exports.replaceSection = (node) => {', ' if (node.value) {', " node.value = node.value.replace(/red/g, 'green').replace(/Red/g, 'Green');", ' }', ' return node', '};', ].join('\n'); const output = [ '

Red

', '

red is the fire

', '

green is the cross

', // <-- start '

green is the flag

', '

More Red

', // <-- end '

the sun can get red

', '', ].join('\n'); const parser = unified() // .use(markdown) .use(remarkExtend, { extendMd }) .use(mdStringify); const result = await parser.process(input); expect(result.contents).to.equal(output); }); it('throws if a start selector is not found', async () => { const input = [ // '### Red', ].join('\n'); const extendMd = [ "```js ::replaceFrom('heading:has([value=More Red])')", 'module.exports.replaceSection = (node) => {}', '```', ].join('\n'); const parser = unified() // .use(markdown) .use(remarkExtend, { extendMd }) .use(mdStringify); await expectThrowsAsync( () => parser.process(input), 'The start selector "heading:has([value=More Red])" could not find a matching node.', ); }); it('throws with addition info (if provide as filePath, overrideFilePath) if a start selector is not found', async () => { const input = [ // '### Red', ].join('\n'); const extendMd = [ "```js ::replaceFrom('heading:has([value=More Red])')", 'module.exports.replaceSection = (node) => {}', '```', ].join('\n'); const parser = unified() // .use(markdown) .use(remarkExtend, { extendMd, filePath: '/path/to/input.md', overrideFilePath: '/path/to/override.md', }) .use(mdStringify); await expectThrowsAsync( () => parser.process(input), [ 'The start selector "heading:has([value=More Red])" could not find a matching node.', 'Markdown File: /path/to/input.md', 'Override File: /path/to/override.md', ].join('\n'), ); }); it('throws if a end selector is not found', async () => { const input = [ // '### Red', ].join('\n'); const extendMd = [ "```js ::replaceBetween('heading:has([value=Red])', 'heading:has([value=Red2])')", 'module.exports.replaceSection = (node) => {}', '```', ].join('\n'); const parser = unified().use(markdown).use(remarkExtend, { extendMd }).use(mdStringify); await expectThrowsAsync( () => parser.process(input), 'The end selector "heading:has([value=Red2])" could not find a matching node.', ); }); it('throws with addition info (if provide as filePath, overrideFilePath) if a end selector is not found', async () => { const input = [ // '### Red', ].join('\n'); const extendMd = [ "```js ::replaceBetween('heading:has([value=Red])', 'heading:has([value=Red2])')", 'module.exports.replaceSection = (node) => {}', '```', ].join('\n'); const parser = unified() .use(markdown) .use(remarkExtend, { extendMd, filePath: '/path/to/input.md', overrideFilePath: '/path/to/override.md', }) .use(mdStringify); await expectThrowsAsync( () => parser.process(input), [ 'The end selector "heading:has([value=Red2])" could not find a matching node.', 'Markdown File: /path/to/input.md', 'Override File: /path/to/override.md', ].join('\n'), ); }); it('replaces a single node if replacing between the start and end of the same node', async () => { const input = [ // '### Red', 'red', '### Red', ].join('\n'); const extendMd = [ "```js ::replaceBetween('heading:has([value=Red]) > text', 'heading:has([value=Red]) > text')", 'module.exports.replaceSection = (node) => {', ' if (node.value) {', " node.value = node.value.replace(/red/g, 'green').replace(/Red/g, 'Green');", ' }', ' return node', '};', '```', ].join('\n'); const output = [ '

Green

', // text node "Green" == start == end '

red

', '

Red

', '', ].join('\n'); const parser = unified() // .use(markdown) .use(remarkExtend, { extendMd }) .use(mdStringify); const result = await parser.process(input); expect(result.contents).to.equal(output); }); it('can put something after via "::addMdAfter"', async () => { const input = [ // '### Red', '', 'red is the fire', ].join('\n'); const extendMd = [ '```', "::addMdAfter('heading:has([value=Red])')", '```', '', 'the ocean is blue', ].join('\n'); const output = [ // '

Red

', '

the ocean is blue

', '

red is the fire

', '', ].join('\n'); const parser = unified() // .use(markdown) .use(remarkExtend, { extendMd }) .use(mdStringify); const result = await parser.process(input); expect(result.contents).to.equal(output); }); it('adding stops at the next ::[action]', async () => { const input = [ // '### Red', '', 'red is the fire', '', '### More Red', ].join('\n'); const extendMd = [ '```', "::addMdAfter('heading:has([value=Red])')", '```', '', 'the ocean is blue', '', '```', "::addMdAfter('heading:has([value=More Red])')", '```', '', 'as in the sun is the ultimate red', '', "```js ::replaceBetween('heading:has([value=Red])', 'heading:has([value=Red])')", 'module.exports.replaceSection = (node) => {}', '```', 'content not part of an add so it gets ignored', ].join('\n'); const output = [ // '

Red

', '

the ocean is blue

', '

red is the fire

', '

More Red

', '

as in the sun is the ultimate red

', '', ].join('\n'); const parser = unified() // .use(markdown) .use(remarkExtend, { extendMd }) .use(mdStringify); const result = await parser.process(input); expect(result.contents).to.equal(output); }); it('will throw if trying to immediate run replacements of added content', async () => { const input = [ // '### Red', '', ].join('\n'); const extendMd = [ '```', "::addMdAfter('heading:has([value=Red])')", '```', '', '## Blue', '', "```js ::replaceFrom('heading:has([value=Blue])')", 'module.exports.replaceSection = (node) => {', ' if (node.value) {', " node.value = node.value.replace(/Blue/g, 'Yellow');", ' }', ' return node', '}', '```', ].join('\n'); const parser = unified() // .use(markdown) .use(remarkExtend, { extendMd }) .use(mdStringify); await expectThrowsAsync( () => parser.process(input), 'The start selector "heading:has([value=Blue])" could not find a matching node.', ); }); it(`can put something right at the top via "::addMdAfter(':root')"`, async () => { const input = [ // '### Red', ].join('\n'); const extendMd = [ // '```', "::addMdAfter(':root')", '```', '', '# New Headline', ].join('\n'); const output = [ // '

New Headline

', '

Red

', '', ].join('\n'); const parser = unified() // .use(markdown) .use(remarkExtend, { extendMd }) .use(mdStringify); const result = await parser.process(input); expect(result.contents).to.equal(output); }); it(`can put something right at the bottom via "::addMdAfter(':scope:last-child')"`, async () => { const input = [ // '### Red', '', 'red is the fire', ].join('\n'); const extendMd = [ // '```', "::addMdAfter(':scope:last-child')", '```', '', 'extra text', ].join('\n'); const output = [ // '

Red

', '

red is the fire

', '

extra text

', '', ].join('\n'); const parser = unified() // .use(markdown) .use(remarkExtend, { extendMd }) .use(mdStringify); const result = await parser.process(input); expect(result.contents).to.equal(output); }); it('can put something before via "::addMdBefore"', async () => { const input = [ // '### Red', '', 'red is the fire', '', '### Blue', ].join('\n'); const extendMd = [ '```', "::addMdBefore('heading:has([value=Blue])')", '```', '', 'the ocean is blue', ].join('\n'); const output = [ // '

Red

', '

red is the fire

', '

the ocean is blue

', '

Blue

', '', ].join('\n'); const parser = unified() // .use(markdown) .use(remarkExtend, { extendMd }) .use(mdStringify); const result = await parser.process(input); expect(result.contents).to.equal(output); }); it(`can put something at the end of a "section" via "::addMdBefore('heading:has([value=Red]) ~ heading[depth=3]')"`, async () => { const input = [ // '### Red', '', 'red is the fire', '', '### Blue', ].join('\n'); const extendMd = [ '```', "::addMdBefore('heading:has([value=Red]) ~ heading[depth=3]')", '```', '', 'the sun will be red', ].join('\n'); const output = [ // '

Red

', '

red is the fire

', '

the sun will be red

', '

Blue

', '', ].join('\n'); const parser = unified() // .use(markdown) .use(remarkExtend, { extendMd }) .use(mdStringify); const result = await parser.process(input); expect(result.contents).to.equal(output); }); it(`can put something at the top of the file via '::addMdBefore(":root")'`, async () => { const input = [ // '### Red', '', 'red is the fire', '', '### Blue', ].join('\n'); const extendMd = [ // '```', "::addMdBefore(':root')", '```', '', 'the ocean is blue', ].join('\n'); const output = [ // '

the ocean is blue

', '

Red

', '

red is the fire

', '

Blue

', '', ].join('\n'); const parser = unified() // .use(markdown) .use(remarkExtend, { extendMd }) .use(mdStringify); const result = await parser.process(input); expect(result.contents).to.equal(output); }); it('can remove from a starting point downward', async () => { const input = [ '### Red', '', 'red is the fire', '', '### More Red', // <-- start '', 'the sun can get red', ].join('\n'); const extendMd = [ // '```', "::removeFrom('heading:has([value=More Red])')", '```', ].join('\n'); const output = [ // '

Red

', '

red is the fire

', '', ].join('\n'); const parser = unified() // .use(markdown) .use(remarkExtend, { extendMd }) .use(mdStringify); const result = await parser.process(input); expect(result.contents).to.equal(output); }); it('can remove a range (start point included, endpoint not)', async () => { const input = [ '### Red', // <-- start '', 'red is the fire', '', '### More Red', // <-- end '', 'the sun can get red', ].join('\n'); const extendMd = [ '```', `::removeBetween('heading:has([value=Red])', 'heading:has([value=Red]) ~ heading[depth=3]')`, '```', ].join('\n'); const output = [ '

More Red

', // <-- end '

the sun can get red

', '', ].join('\n'); const parser = unified() // .use(markdown) .use(remarkExtend, { extendMd }) .use(mdStringify); const result = await parser.process(input); expect(result.contents).to.equal(output); }); it('does replacements in order of extendMd', async () => { const input = [ '### Red', // <-- start '', 'red is the fire', '### More', ].join('\n'); const extendMd = [ '```', "::removeBetween('heading:has([value=Red]) + *', 'heading:has([value=Red]) ~ heading')", '```', "```js ::replaceFrom(':root')", 'module.exports.replaceSection = (node) => {', ' if (node.value) {', " node.value = node.value.replace(/red/g, 'green').replace(/Red/g, 'Green');", ' }', ' return node;', '};', '```', "```js ::replaceFrom(':root')", 'module.exports.replaceSection = (node) => {', ' if (node.value) {', " node.value = node.value.replace(/green/g, 'yellow').replace(/Green/g, 'Yellow');", ' }', ' return node;', '};', '```', '```', "::addMdAfter('heading:has([value=Yellow])')", '```', 'This is added', ].join('\n'); const output = [ '

Yellow

', // <-- start '

This is added

', '

More

', '', ].join('\n'); const parser = unified() // .use(markdown) .use(remarkExtend, { extendMd }) .use(mdStringify); const result = await parser.process(input); expect(result.contents).to.equal(output); }); });