Skip to content

Commit

Permalink
feat: mdx-less reusable content (#811)
Browse files Browse the repository at this point in the history
Adds support for Reusable Content!

This PR adds a transformer to convert `html` nodes into `reusable-content` nodes. If the so named content node has been passed in via `reusableContent`, then the parsed `mdsat` is inserted into place. This PR also adds a compiler for writing back out to `md`.

The syntax is meant to match html/JSX so we have compatibility with future MDX.
  • Loading branch information
kellyjosephprice authored Aug 28, 2023
1 parent a3d1ca5 commit cdebefd
Show file tree
Hide file tree
Showing 11 changed files with 174 additions and 8 deletions.
21 changes: 21 additions & 0 deletions __tests__/flavored-compilers/reusable-content.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { mdast, md } from '../../index';

describe('reusable content compiler', () => {
it('writes an undefined reusable content block back to markdown', () => {
const doc = '<RMReusableContent name="undefined" />';
const tree = mdast(doc);

expect(md(tree)).toMatch(doc);
});

it('writes a defined reusable content block back to markdown', () => {
const reusableContent = {
defined: '# Whoa',
};
const doc = '<RMReusableContent name="defined" />';
const tree = mdast(doc, { reusableContent });

expect(tree.children[0].children[0].type).toBe('heading');
expect(md(tree)).toMatch(doc);
});
});
1 change: 1 addition & 0 deletions __tests__/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ test('it should have the proper utils exports', () => {
paddedTable: true,
},
normalize: true,
reusableContent: {},
safeMode: false,
settings: { position: false },
theme: 'light',
Expand Down
42 changes: 42 additions & 0 deletions __tests__/transformers/__snapshots__/reusable-content.test.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`reusable content transfomer should replace a reusable content block if the block is provided 1`] = `
Object {
"children": Array [
Object {
"children": Array [
Object {
"type": "text",
"value": "Test",
},
],
"data": Object {
"hProperties": Object {
"id": "test",
},
"id": "test",
},
"depth": 1,
"type": "heading",
},
Object {
"children": Array [
Object {
"children": Array [
Object {
"type": "text",
"value": "link",
},
],
"title": null,
"type": "link",
"url": "http://example.com",
},
],
"type": "paragraph",
},
],
"name": "test",
"type": "reusable-content",
}
`;
45 changes: 45 additions & 0 deletions __tests__/transformers/reusable-content.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { mdast } from '../../index';

describe('reusable content transfomer', () => {
it('should replace a reusable content block if the block is provided', () => {
const reusableContent = {
test: `
# Test
[link](http://example.com)
`,
};
const md = `
Before
<RMReusableContent name="test" />
After
`;

const tree = mdast(md, { reusableContent });

expect(tree.children[0].children[0].value).toBe('Before');
expect(tree.children[1]).toMatchSnapshot();
expect(tree.children[2].children[0].value).toBe('After');
});

it('should insert an empty node if the reusable content block is not defined', () => {
const md = '<RMReusableContent name="not-defined" />';
const tree = mdast(md);

expect(tree.children[0].type).toBe('reusable-content');
expect(tree.children[0].children).toStrictEqual([]);
});

it('does not expand reusable content recursively', () => {
const reusableContent = {
test: '<RMReusableContent name="test" />',
};
const md = '<RMReusableContent name="test" />';
const tree = mdast(md, { reusableContent });

expect(tree.children[0].children[0].type).toBe('reusable-content');
expect(tree.children[0].children[0].children).toStrictEqual([]);
});
});
4 changes: 2 additions & 2 deletions docs/callout-tests.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
---
title: "Callouts Tests"
title: 'Callouts Tests'
category: 5fdf9fc9c2a7ef443e937315
hidden: true
---

> 👍 Success
>
>
> <a href="http://www.google.com">Vitae</a> <span>reprehenderit</span> at aliquid error voluptates eum dignissimos.
> 👎 Block Regression
Expand Down
26 changes: 21 additions & 5 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,22 +85,38 @@ export const utils = {
calloutIcons,
};

/**
* Pre-parse reusable content blocks. Note that they do not pass the content
* blocks recursively.
*/
const parseReusableContent = ({ reusableContent, ...opts }) => {
const parsedReusableContent = Object.entries(reusableContent).reduce((memo, [name, content]) => {
// eslint-disable-next-line no-use-before-define
memo[name] = mdast(content, opts).children;
return memo;
}, {});

return [parsedReusableContent, opts];
};

/**
* Core markdown to mdast processor
*/
export function processor(opts = {}) {
[, opts] = setup('', opts);
const { sanitize } = opts;
export function processor(userOpts = {}) {
const [, parsedOpts] = setup('', userOpts);
const { sanitize } = parsedOpts;
const [reusableContent, opts] = parseReusableContent(parsedOpts);

return unified()
.use(remarkParse, opts.markdownOptions)
.use(remarkFrontmatter, ['yaml', 'toml'])
.data('settings', opts.settings)
.data('compatibilityMode', opts.compatibilityMode)
.data('alwaysThrow', opts.alwaysThrow)
.data('reusableContent', reusableContent)
.use(!opts.correctnewlines ? remarkBreaks : () => {})
.use(CustomParsers.map(parser => parser.sanitize?.(sanitize) || parser))
.use(...remarkTransformers)
.use(remarkTransformers)
.use(remarkSlug)
.use(remarkDisableTokenizers, opts.disableTokenizers);
}
Expand Down Expand Up @@ -134,7 +150,7 @@ export function htmlProcessor(opts = {}) {
.use(remarkRehype, { allowDangerousHtml: true })
.use(rehypeRaw)
.use(rehypeSanitize, sanitize)
.use(...rehypeTransformers);
.use(rehypeTransformers);
}

export function plain(text, opts = {}, components = {}) {
Expand Down
1 change: 1 addition & 0 deletions options.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const options = {
},
lazyImages: true,
normalize: true,
reusableContent: {},
safeMode: false,
settings: {
position: false,
Expand Down
1 change: 1 addition & 0 deletions processor/compile/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export { default as rdmeEmbedCompiler } from './embed';
export { default as rdmeGlossaryCompiler } from './glossary';
export { default as rdmePinCompiler } from './pin';
export { default as rdmeVarCompiler } from './var';
export { default as reusableContentCompiler } from './reusable-content';
export { default as tableCompiler } from './table';
export { default as tableHeadCompiler } from './table-head';
export { default as yamlCompiler } from './yaml';
8 changes: 8 additions & 0 deletions processor/compile/reusable-content.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { type, tag } from '../transform/reusable-content';

export default function ReusableContentCompiler() {
const { Compiler } = this;
const { visitors } = Compiler.prototype;

visitors[type] = node => `<${tag} name="${node.name}" />`;
}
3 changes: 2 additions & 1 deletion processor/transform/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import reusableContent from './reusable-content';
import singleCodeTabs from './single-code-tabs';
import tableCellInlineCode from './table-cell-inline-code';

export const remarkTransformers = [singleCodeTabs];
export const remarkTransformers = [singleCodeTabs, reusableContent];
export const rehypeTransformers = [tableCellInlineCode];
30 changes: 30 additions & 0 deletions processor/transform/reusable-content.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { visit } from 'unist-util-visit';

export const type = 'reusable-content';
export const tag = 'RMReusableContent';

const regexp = new RegExp(`^\\s*<${tag} name="(?<name>.*)" />\\s*$`);

const reusableContentTransformer = function () {
const reusableContent = this.data('reusableContent');

return tree => {
visit(tree, 'html', (node, index, parent) => {
const result = regexp.exec(node.value);
if (!result || !result.groups.name) return;

const { name } = result.groups;
const block = {
type,
name,
children: name in reusableContent ? reusableContent[name] : [],
};

parent.children[index] = block;
});

return tree;
};
};

export default reusableContentTransformer;

0 comments on commit cdebefd

Please sign in to comment.