(node);
7 |
8 | return `{\`
9 | ${ reformatHTML(html) }
10 | \`}`;
11 | }
12 |
13 | export default htmlBlock;
14 |
--------------------------------------------------------------------------------
/processor/compile/index.ts:
--------------------------------------------------------------------------------
1 | import { NodeTypes } from '../../enums';
2 |
3 | import callout from './callout';
4 | import codeTabs from './code-tabs';
5 | import compatibility from './compatibility';
6 | import embed from './embed';
7 | import gemoji from './gemoji';
8 | import htmlBlock from './html-block';
9 | import plain from './plain';
10 |
11 | function compilers() {
12 | const data = this.data();
13 |
14 | const toMarkdownExtensions = data.toMarkdownExtensions || (data.toMarkdownExtensions = []);
15 |
16 | const handlers = {
17 | [NodeTypes.callout]: callout,
18 | [NodeTypes.codeTabs]: codeTabs,
19 | [NodeTypes.embedBlock]: embed,
20 | [NodeTypes.emoji]: gemoji,
21 | [NodeTypes.glossary]: compatibility,
22 | [NodeTypes.htmlBlock]: htmlBlock,
23 | [NodeTypes.reusableContent]: compatibility,
24 | embed: compatibility,
25 | escape: compatibility,
26 | figure: compatibility,
27 | html: compatibility,
28 | i: compatibility,
29 | plain,
30 | yaml: compatibility,
31 | };
32 |
33 | toMarkdownExtensions.push({ extensions: [{ handlers }] });
34 | }
35 |
36 | export default compilers;
37 |
--------------------------------------------------------------------------------
/processor/compile/plain.ts:
--------------------------------------------------------------------------------
1 | import type { Plain } from '../../types';
2 |
3 | const plain = (node: Plain) => node.value;
4 |
5 | export default plain;
6 |
--------------------------------------------------------------------------------
/processor/compile/table.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/readmeio/markdown/b812eeedba99b513a04f42f577ce724513fc2b06/processor/compile/table.ts
--------------------------------------------------------------------------------
/processor/compile/yaml.js:
--------------------------------------------------------------------------------
1 | module.exports = function YamlCompiler() {
2 | const { Compiler } = this;
3 | const { visitors } = Compiler.prototype;
4 |
5 | visitors.yaml = function compile(node) {
6 | return `---\n${node.value}\n---`;
7 | };
8 | };
9 |
--------------------------------------------------------------------------------
/processor/migration/emphasis.ts:
--------------------------------------------------------------------------------
1 | import type { Emphasis, Node, Parent, Root, Strong, Text } from 'mdast';
2 |
3 | import { visit, EXIT } from 'unist-util-visit';
4 |
5 | const strongTest = (node: Node): node is Emphasis | Strong => ['emphasis', 'strong'].includes(node.type);
6 |
7 | const addSpaceBefore = (index: number, parent: Parent) => {
8 | if (!(index > 0 && parent.children[index - 1])) return;
9 |
10 | const prev = parent.children[index - 1];
11 | if (!('value' in prev) || prev.value.endsWith(' ') || prev.type === 'escape') return;
12 |
13 | parent.children.splice(index, 0, { type: 'text', value: ' ' });
14 | };
15 |
16 | const addSpaceAfter = (index: number, parent: Parent) => {
17 | if (!(index < parent.children.length - 1 && parent.children[index + 1])) return;
18 |
19 | const nextChild = parent.children[index + 1];
20 | if (!('value' in nextChild) || nextChild.value.startsWith(' ')) return;
21 |
22 | parent.children.splice(index + 1, 0, { type: 'text', value: ' ' });
23 | };
24 |
25 | const trimEmphasis = (node: Emphasis | Strong, index: number, parent: Parent) => {
26 | let trimmed = false;
27 |
28 | visit(node, 'text', (child: Text) => {
29 | const newValue = child.value.trimStart();
30 |
31 | if (newValue !== child.value) {
32 | trimmed = true;
33 | child.value = newValue;
34 | }
35 |
36 | return EXIT;
37 | });
38 |
39 | visit(
40 | node,
41 | 'text',
42 | (child: Text) => {
43 | const newValue = child.value.trimEnd();
44 |
45 | if (newValue !== child.value) {
46 | trimmed = true;
47 | child.value = newValue;
48 | }
49 |
50 | return EXIT;
51 | },
52 | true,
53 | );
54 |
55 | if (trimmed) {
56 | addSpaceBefore(index, parent);
57 | addSpaceAfter(index, parent);
58 | }
59 | };
60 |
61 | const emphasisTransfomer = () => (tree: Root) => {
62 | visit(tree, strongTest, trimEmphasis);
63 |
64 | return tree;
65 | };
66 |
67 | export default emphasisTransfomer;
68 |
--------------------------------------------------------------------------------
/processor/migration/images.ts:
--------------------------------------------------------------------------------
1 | import type { Image } from 'mdast';
2 |
3 | import { visit } from 'unist-util-visit';
4 |
5 | interface ImageBlock extends Image {
6 | data?: {
7 | hProperties?: {
8 | border?: boolean;
9 | className?: string;
10 | };
11 | };
12 | }
13 |
14 | const imageTransformer = () => tree => {
15 | visit(tree, 'image', (image: ImageBlock) => {
16 | if (image.data?.hProperties?.className === 'border') {
17 | image.data.hProperties.border = true;
18 | }
19 | });
20 | };
21 |
22 | export default imageTransformer;
23 |
--------------------------------------------------------------------------------
/processor/migration/index.ts:
--------------------------------------------------------------------------------
1 | import emphasisTransformer from './emphasis';
2 | import imagesTransformer from './images';
3 | import linkReferenceTransformer from './linkReference';
4 | import tableCellTransformer from './table-cell';
5 |
6 | const transformers = [emphasisTransformer, imagesTransformer, linkReferenceTransformer, tableCellTransformer];
7 |
8 | export default transformers;
9 |
--------------------------------------------------------------------------------
/processor/migration/linkReference.ts:
--------------------------------------------------------------------------------
1 | import type { Definition, LinkReference, Root, Text } from 'mdast';
2 |
3 | import { visit } from 'unist-util-visit';
4 |
5 | const linkReferenceTransformer =
6 | () =>
7 | (tree: Root): Root => {
8 | visit(tree, 'linkReference', (node: LinkReference, index, parent) => {
9 | const definitions = {};
10 |
11 | visit(tree, 'definition', (def: Definition) => {
12 | definitions[def.identifier] = def;
13 | });
14 |
15 | if (node.label === node.identifier && parent) {
16 | if (!(node.identifier in definitions)) {
17 | parent.children[index] = {
18 | type: 'text',
19 | value: `[${node.label}]`,
20 | position: node.position,
21 | } as Text;
22 | }
23 | }
24 | });
25 |
26 | return tree;
27 | };
28 |
29 | export default linkReferenceTransformer;
30 |
--------------------------------------------------------------------------------
/processor/plugin/section-anchor-id.js:
--------------------------------------------------------------------------------
1 | const kebabCase = require('lodash.kebabcase');
2 | const flatMap = require('unist-util-flatmap');
3 |
4 | const matchTag = /^h[1-6]$/g;
5 |
6 | /** Concat a deep text value from an AST node's children
7 | */
8 | const getTexts = node => {
9 | let text = '';
10 | flatMap(node, kid => {
11 | text += kid.type === 'text' ? kid.value : '';
12 | return [kid];
13 | });
14 | return text;
15 | };
16 |
17 | /** Adds an empty next to all headings
18 | * for backwards-compatibility with how we used to do slugs.
19 | */
20 | function transformer(ast) {
21 | return flatMap(ast, node => {
22 | if (matchTag.test(node.tagName)) {
23 | // Parse the node texts to construct
24 | // a backwards-compatible anchor ID.
25 | const text = getTexts(node);
26 | const id = `section-${kebabCase(text)}`;
27 |
28 | if (id && !node?.properties?.id) {
29 | // Use the compat anchor ID as fallback if
30 | // GitHubs slugger returns an empty string.
31 | node.properties.id = id;
32 | }
33 |
34 | // Create and append a compat anchor element
35 | // to the section heading.
36 | const anchor = {
37 | type: 'element',
38 | tagName: 'div',
39 | properties: { id, className: 'heading-anchor_backwardsCompatibility' },
40 | };
41 | if (node.children) node.children.unshift(anchor);
42 | else node.children = [anchor];
43 | }
44 | return [node];
45 | });
46 | }
47 |
48 | module.exports = () => transformer;
49 |
--------------------------------------------------------------------------------
/processor/plugin/table-flattening.js:
--------------------------------------------------------------------------------
1 | const flatMap = require('unist-util-flatmap');
2 |
3 | const collectValues = ({ value, children }) => {
4 | if (value) return value;
5 | if (children) return children.flatMap(collectValues);
6 | return '';
7 | };
8 |
9 | const valuesToString = node => {
10 | const values = collectValues(node);
11 | return Array.isArray(values) ? values.join(' ') : values;
12 | };
13 |
14 | // Flattens table values and adds them as a seperate, easily-accessible key within children
15 | function transformer(ast) {
16 | return flatMap(ast, node => {
17 | if (node.tagName === 'table') {
18 | const [header, body] = node.children;
19 | // hAST tables are deeply nested with an innumerable amount of children
20 | // This is necessary to pullout all the relevant strings
21 | return [
22 | {
23 | ...node,
24 | children: [
25 | {
26 | ...node.children[0],
27 | value: valuesToString(header),
28 | },
29 | {
30 | ...node.children[1],
31 | value: valuesToString(body),
32 | },
33 | ],
34 | },
35 | ];
36 | }
37 |
38 | return [node];
39 | });
40 | }
41 |
42 | module.exports = () => transformer;
43 | module.exports.tableFlattening = transformer;
44 |
--------------------------------------------------------------------------------
/processor/transform/callouts.ts:
--------------------------------------------------------------------------------
1 | import type { Blockquote, Root } from 'mdast';
2 | import type { Callout } from 'types';
3 |
4 | import emojiRegex from 'emoji-regex';
5 | import { visit } from 'unist-util-visit';
6 |
7 | import { themes } from '../../components/Callout';
8 | import { NodeTypes } from '../../enums';
9 |
10 | const regex = `^(${emojiRegex().source}|⚠)(\\s+|$)`;
11 |
12 | const calloutTransformer = () => {
13 | return (tree: Root) => {
14 | visit(tree, 'blockquote', (node: Blockquote | Callout) => {
15 | if (!(node.children[0].type === 'paragraph' && node.children[0].children[0].type === 'text')) return;
16 |
17 | const startText = node.children[0].children[0].value;
18 | const [match, icon] = startText.match(regex) || [];
19 |
20 | if (icon && match) {
21 | const heading = startText.slice(match.length);
22 | const empty = !heading.length && node.children[0].children.length === 1;
23 | const theme = themes[icon] || 'default';
24 |
25 | node.children[0].children[0].value = heading;
26 |
27 | Object.assign(node, {
28 | type: NodeTypes.callout,
29 | data: {
30 | hName: 'Callout',
31 | hProperties: {
32 | icon,
33 | ...(empty && { empty }),
34 | theme,
35 | },
36 | },
37 | });
38 | }
39 | });
40 | };
41 | };
42 |
43 | export default calloutTransformer;
44 |
--------------------------------------------------------------------------------
/processor/transform/code-tabs.ts:
--------------------------------------------------------------------------------
1 | import type { BlockContent, Code, Node } from 'mdast';
2 | import type { CodeTabs } from 'types';
3 |
4 | import { visit } from 'unist-util-visit';
5 |
6 | import { NodeTypes } from '../../enums';
7 |
8 | const isCode = (node: Node): node is Code => node?.type === 'code';
9 |
10 | const codeTabsTransformer =
11 | ({ copyButtons }: { copyButtons?: boolean } = {}) =>
12 | (tree: Node) => {
13 | visit(tree, 'code', (node: Code) => {
14 | const { lang, meta, value } = node;
15 | node.data = {
16 | hProperties: { lang, meta, value, copyButtons },
17 | };
18 | });
19 |
20 | visit(tree, 'code', (node: Code, index: number, parent: BlockContent) => {
21 | if (parent.type === 'code-tabs' || !('children' in parent)) return;
22 |
23 | const length = parent.children.length;
24 | const children = [node];
25 | let walker = index + 1;
26 |
27 | while (walker <= length) {
28 | const sibling = parent.children[walker];
29 | if (!isCode(sibling)) break;
30 |
31 | const olderSibling = parent.children[walker - 1];
32 | if (olderSibling.position.end.offset + sibling.position.start.column !== sibling.position.start.offset) break;
33 |
34 | children.push(sibling);
35 | // eslint-disable-next-line no-plusplus
36 | walker++;
37 | }
38 |
39 | // If there is a single code block, and it has either a title or a
40 | // language set, let's display it by wrapping it in a code tabs block.
41 | // Othewise, we can leave early!
42 | if (children.length === 1 && !(node.lang || node.meta)) return;
43 |
44 | const codeTabs: CodeTabs = {
45 | type: NodeTypes.codeTabs,
46 | children,
47 | data: {
48 | hName: 'CodeTabs',
49 | },
50 | position: {
51 | start: children[0].position.start,
52 | end: children[children.length - 1].position.end,
53 | },
54 | };
55 |
56 | parent.children.splice(index, children.length, codeTabs);
57 | });
58 |
59 | return tree;
60 | };
61 |
62 | export default codeTabsTransformer;
63 |
--------------------------------------------------------------------------------
/processor/transform/compatability.ts:
--------------------------------------------------------------------------------
1 | import type { Emphasis, Image, Strong, Node, Parent } from 'mdast';
2 | import type { Transform } from 'mdast-util-from-markdown';
3 |
4 | import { phrasing } from 'mdast-util-phrasing';
5 | import { EXIT, SKIP, visit } from 'unist-util-visit';
6 |
7 | const strongTest = (node: Node): node is Emphasis | Strong => ['emphasis', 'strong'].includes(node.type);
8 |
9 | const compatibilityTransfomer = (): Transform => tree => {
10 | const trimEmphasis = (node: Emphasis | Strong) => {
11 | visit(node, 'text', child => {
12 | child.value = child.value.trim();
13 | return EXIT;
14 | });
15 |
16 | return node;
17 | };
18 |
19 | visit(tree, strongTest, node => {
20 | trimEmphasis(node);
21 | return SKIP;
22 | });
23 |
24 | visit(tree, 'image', (node: Image, index: number, parent: Parent) => {
25 | if (phrasing(parent) || !parent.children.every(child => child.type === 'image' || !phrasing(child))) return;
26 |
27 | parent.children.splice(index, 1, { type: 'paragraph', children: [node] });
28 | });
29 |
30 | return tree;
31 | };
32 |
33 | export default compatibilityTransfomer;
34 |
--------------------------------------------------------------------------------
/processor/transform/div.ts:
--------------------------------------------------------------------------------
1 | import type { TutorialTile } from '../../types';
2 | import type { Node, Parent } from 'mdast';
3 | import type { Transform } from 'mdast-util-from-markdown';
4 |
5 | import { visit } from 'unist-util-visit';
6 |
7 | import { NodeTypes } from '../../enums';
8 |
9 | const divTransformer = (): Transform => tree => {
10 | visit(tree, 'div', (node: Node, index, parent: Parent) => {
11 | const type = node.data?.hName;
12 |
13 | switch (type) {
14 | // Check if the div is a tutorial-tile in disguise
15 | case NodeTypes.tutorialTile:
16 | {
17 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
18 | const { hName, hProperties, ...rest } = node.data;
19 | const tile = {
20 | ...rest,
21 | type: NodeTypes.tutorialTile,
22 | } as TutorialTile;
23 | parent.children.splice(index, 1, tile);
24 | }
25 | break;
26 | // idk what this is and/or just make it a paragraph
27 | default:
28 | node.type = type || 'paragraph';
29 | }
30 | });
31 |
32 | return tree;
33 | };
34 |
35 | export default divTransformer;
36 |
--------------------------------------------------------------------------------
/processor/transform/embeds.ts:
--------------------------------------------------------------------------------
1 | import type { Embed, EmbedBlock } from '../../types';
2 | import type { Paragraph, Parents, Node, Text } from 'mdast';
3 |
4 | import { visit } from 'unist-util-visit';
5 |
6 | import { NodeTypes } from '../../enums';
7 |
8 | const isEmbed = (node: Node): node is Embed => 'title' in node && node.title === '@embed';
9 |
10 | const embedTransformer = () => {
11 | return (tree: Node) => {
12 | visit(tree, 'paragraph', (node: Paragraph, i: number, parent: Parents) => {
13 | const [child] = node.children;
14 | if (!isEmbed(child)) return;
15 |
16 | const { url, title } = child;
17 | const label = (child.children[0] as Text).value;
18 |
19 | const newNode = {
20 | type: NodeTypes.embedBlock,
21 | label,
22 | title,
23 | url,
24 | data: {
25 | hProperties: {
26 | url,
27 | title: label ?? title,
28 | },
29 | hName: 'embed',
30 | },
31 | position: node.position,
32 | } as EmbedBlock;
33 |
34 | parent.children.splice(i, 1, newNode);
35 | });
36 | };
37 | };
38 |
39 | export default embedTransformer;
40 |
--------------------------------------------------------------------------------
/processor/transform/gemoji+.ts:
--------------------------------------------------------------------------------
1 | import type { FaEmoji, Gemoji } from '../../types';
2 | import type { Image, Root } from 'mdast';
3 |
4 | import { findAndReplace } from 'mdast-util-find-and-replace';
5 |
6 | import { NodeTypes } from '../../enums';
7 | import Owlmoji from '../../lib/owlmoji';
8 |
9 | const regex = /:(?\+1|[-\w]+):/g;
10 |
11 | const gemojiReplacer = (_, name: string) => {
12 | switch (Owlmoji.kind(name)) {
13 | case 'gemoji': {
14 | const node: Gemoji = {
15 | type: NodeTypes.emoji,
16 | value: Owlmoji.nameToEmoji[name],
17 | name,
18 | };
19 |
20 | return node;
21 | }
22 | case 'fontawesome': {
23 | const node: FaEmoji = {
24 | type: NodeTypes.i,
25 | value: name,
26 | data: {
27 | hName: 'i',
28 | hProperties: {
29 | className: ['fa-regular', name],
30 | },
31 | },
32 | };
33 |
34 | return node;
35 | }
36 | case 'owlmoji': {
37 | const node: Image = {
38 | type: 'image',
39 | title: `:${name}:`,
40 | alt: `:${name}:`,
41 | url: `/public/img/emojis/${name}.png`,
42 | data: {
43 | hProperties: {
44 | className: 'emoji',
45 | align: 'absmiddle',
46 | height: '20',
47 | width: '20',
48 | },
49 | },
50 | };
51 |
52 | return node;
53 | }
54 | default:
55 | return false;
56 | }
57 | };
58 |
59 | const gemojiTransformer = () => (tree: Root) => {
60 | findAndReplace(tree, [regex, gemojiReplacer]);
61 |
62 | return tree;
63 | };
64 |
65 | export default gemojiTransformer;
66 |
--------------------------------------------------------------------------------
/processor/transform/handle-missing-components.ts:
--------------------------------------------------------------------------------
1 | import type { CompileOpts } from '../../lib/compile';
2 | import type { Parents } from 'mdast';
3 | import type { Transform } from 'mdast-util-from-markdown';
4 | import type { MdxJsxFlowElement, MdxJsxTextElement } from 'mdast-util-mdx';
5 |
6 | import { visit } from 'unist-util-visit';
7 |
8 | import * as Components from '../../components';
9 | import mdast from '../../lib/mdast';
10 | import { getExports, isMDXElement } from '../utils';
11 |
12 | type HandleMissingComponentsProps = Pick;
13 |
14 | const handleMissingComponents =
15 | ({ components, missingComponents }: HandleMissingComponentsProps): Transform =>
16 | tree => {
17 | const allComponents = new Set([
18 | ...getExports(tree),
19 | ...Object.keys(Components),
20 | ...Object.keys(components),
21 | ...Object.values(components).flatMap(doc => getExports(mdast(doc))),
22 | 'Variable',
23 | 'TutorialTile',
24 | ]);
25 |
26 | visit(
27 | tree,
28 | isMDXElement,
29 | (node: MdxJsxFlowElement | MdxJsxTextElement, index: number, parent: Parents) => {
30 | if (allComponents.has(node.name) || node.name.match(/^[a-z]/)) return;
31 |
32 | if (missingComponents === 'throw') {
33 | throw new Error(
34 | `Expected component \`${node.name}\` to be defined: you likely forgot to import, pass, or provide it.`,
35 | );
36 | }
37 |
38 | parent.children.splice(index, 1);
39 | },
40 | true,
41 | );
42 | };
43 |
44 | export default handleMissingComponents;
45 |
--------------------------------------------------------------------------------
/processor/transform/images.ts:
--------------------------------------------------------------------------------
1 | import type { ImageBlock } from '../../types';
2 | import type { Node, Paragraph, Parents, Image } from 'mdast';
3 | import type { MdxJsxFlowElement } from 'mdast-util-mdx';
4 |
5 | import { visit } from 'unist-util-visit';
6 |
7 | import { NodeTypes } from '../../enums';
8 | import { mdast } from '../../lib';
9 | import { getAttrs } from '../utils';
10 |
11 | const isImage = (node: Node): node is Image => node.type === 'image';
12 |
13 | const imageTransformer = () => (tree: Node) => {
14 | visit(tree, 'paragraph', (node: Paragraph, i: number, parent: Parents) => {
15 | // check if inline
16 | if (parent.type !== 'root' || node.children?.length > 1) return;
17 |
18 | const child = node.children[0];
19 | if (!isImage(child)) return;
20 |
21 | const { alt, url, title } = child;
22 |
23 | const attrs = {
24 | alt,
25 | title,
26 | children: [],
27 | src: url,
28 | };
29 |
30 | const newNode: ImageBlock = {
31 | type: NodeTypes.imageBlock,
32 | ...attrs,
33 | /*
34 | * @note: Using data.hName here means that we don't have to transform
35 | * this to an MdxJsxFlowElement, and rehype will transform it correctly
36 | */
37 | data: {
38 | hName: 'img',
39 | hProperties: attrs,
40 | },
41 | position: node.position,
42 | };
43 |
44 | parent.children.splice(i, 1, newNode);
45 | });
46 |
47 | const isImageBlock = (node: MdxJsxFlowElement) => node.name === 'Image';
48 |
49 | visit(tree, isImageBlock, (node: MdxJsxFlowElement) => {
50 | const attrs = getAttrs(node);
51 |
52 | if (attrs.caption) {
53 | // @ts-expect-error - @todo: figure out how to coerce RootContent[] to
54 | // the correct type
55 | node.children = mdast(attrs.caption).children;
56 | }
57 | });
58 |
59 | return tree;
60 | };
61 |
62 | export default imageTransformer;
63 |
--------------------------------------------------------------------------------
/processor/transform/index.ts:
--------------------------------------------------------------------------------
1 | import calloutTransformer from './callouts';
2 | import codeTabsTransformer from './code-tabs';
3 | import compatabilityTransfomer from './compatability';
4 | import divTransformer from './div';
5 | import embedTransformer from './embeds';
6 | import gemojiTransformer from './gemoji+';
7 | import handleMissingComponents from './handle-missing-components';
8 | import imageTransformer from './images';
9 | import injectComponents from './inject-components';
10 | import mdxToHast from './mdx-to-hast';
11 | import mermaidTransformer from './mermaid';
12 | import readmeComponentsTransformer from './readme-components';
13 | import readmeToMdx from './readme-to-mdx';
14 | import tablesToJsx from './tables-to-jsx';
15 | import tailwindTransformer from './tailwind';
16 | import variablesTransformer from './variables';
17 |
18 | export {
19 | compatabilityTransfomer,
20 | divTransformer,
21 | injectComponents,
22 | mdxToHast,
23 | mermaidTransformer,
24 | readmeComponentsTransformer,
25 | readmeToMdx,
26 | tablesToJsx,
27 | tailwindTransformer,
28 | handleMissingComponents,
29 | variablesTransformer,
30 | };
31 |
32 | export const defaultTransforms = {
33 | calloutTransformer,
34 | codeTabsTransformer,
35 | embedTransformer,
36 | imageTransformer,
37 | gemojiTransformer,
38 | };
39 |
40 | export default Object.values(defaultTransforms);
41 |
--------------------------------------------------------------------------------
/processor/transform/inject-components.ts:
--------------------------------------------------------------------------------
1 | import type { MdastComponents } from '../../types';
2 | import type { Parents } from 'mdast';
3 | import type { Transform } from 'mdast-util-from-markdown';
4 | import type { MdxJsxFlowElement, MdxJsxTextElement } from 'mdast-util-mdx';
5 |
6 | import { visit } from 'unist-util-visit';
7 |
8 | import { isMDXElement } from '../utils';
9 |
10 | interface Options {
11 | components?: MdastComponents;
12 | }
13 |
14 | const inject =
15 | ({ components }: Options = {}) =>
16 | (node: MdxJsxFlowElement | MdxJsxTextElement, index: number, parent: Parents) => {
17 | if (!(node.name in components)) return;
18 |
19 | const { children } = components[node.name];
20 | parent.children.splice(index, children.length, ...children);
21 | };
22 |
23 | const injectComponents = (opts: Options) => (): Transform => tree => {
24 | visit(tree, isMDXElement, inject(opts));
25 |
26 | return tree;
27 | };
28 |
29 | export default injectComponents;
30 |
--------------------------------------------------------------------------------
/processor/transform/mdx-to-hast.ts:
--------------------------------------------------------------------------------
1 | import type { Parents } from 'mdast';
2 | import type { Transform } from 'mdast-util-from-markdown';
3 | import type { MdxJsxFlowElement, MdxJsxTextElement } from 'mdast-util-mdx';
4 |
5 | import { visit } from 'unist-util-visit';
6 |
7 | import * as Components from '../../components';
8 | import { getAttrs, isMDXElement } from '../utils';
9 |
10 | const setData = (node: MdxJsxFlowElement | MdxJsxTextElement, index: number, parent: Parents) => {
11 | if (!node.name) return;
12 | if (!(node.name in Components)) return;
13 |
14 | parent.children[index] = {
15 | ...node,
16 | data: {
17 | hName: node.name,
18 | hProperties: getAttrs(node),
19 | },
20 | };
21 | };
22 |
23 | const mdxToHast = (): Transform => tree => {
24 | visit(tree, isMDXElement, setData);
25 |
26 | return tree;
27 | };
28 |
29 | export default mdxToHast;
30 |
--------------------------------------------------------------------------------
/processor/transform/mermaid.ts:
--------------------------------------------------------------------------------
1 | import type { Element } from 'hast';
2 | import type { Node } from 'unist';
3 |
4 | import { visit } from 'unist-util-visit';
5 |
6 | const mermaidTransformer = () => (tree: Node) => {
7 | visit(tree, 'element', (node: Element) => {
8 | if (node.tagName !== 'pre' || node.children.length !== 1) return;
9 |
10 | const [child] = node.children;
11 | if (child.type === 'element' && child.tagName === 'code' && child.properties.lang === 'mermaid') {
12 | node.properties = {
13 | ...node.properties,
14 | className: ['mermaid', ...((node.properties.className as string[]) || [])],
15 | };
16 | }
17 | });
18 |
19 | return tree;
20 | };
21 |
22 | export default mermaidTransformer;
23 |
--------------------------------------------------------------------------------
/processor/transform/reusable-content.js:
--------------------------------------------------------------------------------
1 | import { visit } from 'unist-util-visit';
2 |
3 | import { type } from '../parse/reusable-content-parser';
4 |
5 | function reusableContent() {
6 | const { wrap = true } = this.data('reusableContent');
7 |
8 | return tree => {
9 | if (wrap) return tree;
10 |
11 | visit(tree, type, (node, index, parent) => {
12 | parent.children.splice(index, 1, ...node.children);
13 | });
14 |
15 | return tree;
16 | };
17 | }
18 |
19 | export default reusableContent;
20 |
--------------------------------------------------------------------------------
/processor/transform/table-cell-inline-code.js:
--------------------------------------------------------------------------------
1 | import { SKIP, visit } from 'unist-util-visit';
2 |
3 | const rxEscapedPipe = /\\\|/g;
4 |
5 | /**
6 | * HAST Transformer that finds all inline code nodes within table cells and
7 | * unescapes any escaped pipe chars so that the editor outputs them without
8 | * escape chars.
9 | *
10 | * This appears to be a bug with remark-parse < ~8
11 | */
12 | const tableCellInlineCode = () => tree => {
13 | visit(tree, [{ tagName: 'th' }, { tagName: 'td' }], tableCellNode => {
14 | visit(tableCellNode, { tagName: 'code' }, inlineCodeNode => {
15 | const textNode = inlineCodeNode.children[0];
16 |
17 | if (textNode && rxEscapedPipe.test(textNode.value)) {
18 | textNode.value = textNode.value.replace(rxEscapedPipe, '|');
19 | }
20 | });
21 |
22 | return SKIP;
23 | });
24 | };
25 |
26 | export default tableCellInlineCode;
27 |
--------------------------------------------------------------------------------
/processor/transform/tailwind.tsx:
--------------------------------------------------------------------------------
1 | import type { PhrasingContent, BlockContent, Root } from 'mdast';
2 | import type { MdxJsxFlowElement, MdxJsxTextElement } from 'mdast-util-mdx';
3 | import type { Plugin } from 'unified';
4 | import type { VFile } from 'vfile';
5 |
6 | import { visit, SKIP } from 'unist-util-visit';
7 |
8 | import { isMDXElement, toAttributes, getExports } from '../utils';
9 |
10 | interface TailwindRootOptions {
11 | components: Record;
12 | parseRoot?: boolean;
13 | }
14 |
15 | type Visitor =
16 | | ((node: MdxJsxFlowElement, index: number, parent: BlockContent) => undefined | void)
17 | | ((node: MdxJsxTextElement, index: number, parent: PhrasingContent) => undefined | void);
18 |
19 | const injectTailwindRoot =
20 | ({ components = {} }): Visitor =>
21 | (node, index, parent) => {
22 | if (!('name' in node)) return;
23 | if (!(node.name in components)) return;
24 | if (!('children' in parent)) return;
25 |
26 | const attrs = {
27 | flow: node.type === 'mdxJsxFlowElement',
28 | };
29 |
30 | const wrapper = {
31 | type: node.type,
32 | name: 'TailwindRoot',
33 | attributes: toAttributes(attrs),
34 | children: [node],
35 | };
36 |
37 | parent.children.splice(index, 1, wrapper);
38 |
39 | // eslint-disable-next-line consistent-return
40 | return SKIP;
41 | };
42 |
43 | const tailwind: Plugin<[TailwindRootOptions]> =
44 | ({ components }) =>
45 | (tree: Root, vfile: VFile) => {
46 | const localComponents = getExports(tree).reduce((acc, name) => {
47 | acc[name] = String(vfile);
48 | return acc;
49 | }, {});
50 |
51 | visit(tree, isMDXElement, injectTailwindRoot({ components: { ...components, ...localComponents } }));
52 |
53 | return tree;
54 | };
55 |
56 | export default tailwind;
57 |
--------------------------------------------------------------------------------
/sanitize.schema.js:
--------------------------------------------------------------------------------
1 | import { defaultSchema } from 'hast-util-sanitize/lib/schema';
2 |
3 | const createSchema = ({ safeMode } = {}) => {
4 | const schema = JSON.parse(JSON.stringify(defaultSchema));
5 |
6 | // Sanitization Schema Defaults
7 | schema.clobberPrefix = '';
8 |
9 | schema.tagNames.push('span');
10 | schema.attributes['*'].push('class', 'className', 'align');
11 | if (!safeMode) {
12 | schema.attributes['*'].push('style');
13 | }
14 |
15 | schema.tagNames.push('rdme-pin');
16 |
17 | schema.tagNames.push('rdme-embed');
18 | schema.attributes['rdme-embed'] = [
19 | 'url',
20 | 'provider',
21 | 'html',
22 | 'title',
23 | 'href',
24 | 'iframe',
25 | 'width',
26 | 'height',
27 | 'image',
28 | 'favicon',
29 | 'align',
30 | ];
31 |
32 | schema.attributes.a = ['href', 'title', 'class', 'className', 'download'];
33 |
34 | schema.tagNames.push('figure');
35 | schema.tagNames.push('figcaption');
36 |
37 | schema.tagNames.push('input'); // allow GitHub-style todo lists
38 | schema.ancestors.input = ['li'];
39 |
40 | return schema;
41 | };
42 |
43 | export default createSchema;
44 |
--------------------------------------------------------------------------------
/scripts/perf-test.js:
--------------------------------------------------------------------------------
1 | const childProcess = require('child_process');
2 | const { Blob } = require('node:buffer');
3 |
4 | const rdmd = require('..');
5 |
6 | const mdBuffer = childProcess.execSync('cat ./docs/*', { encoding: 'utf8' });
7 |
8 | const createDoc = bytes => {
9 | let doc = '';
10 |
11 | while (new Blob([doc]).size < bytes) {
12 | const start = Math.ceil(Math.random() * mdBuffer.length);
13 | doc += mdBuffer.slice(start, start + bytes);
14 | }
15 |
16 | return doc;
17 | };
18 |
19 | // https://stackoverflow.com/a/14919494
20 | function humanFileSize(bytes, si = false, dp = 1) {
21 | const thresh = si ? 1000 : 1024;
22 |
23 | if (Math.abs(bytes) < thresh) {
24 | return `${bytes} B`;
25 | }
26 |
27 | const units = si
28 | ? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
29 | : ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
30 | let u = -1;
31 | const r = 10 ** dp;
32 |
33 | do {
34 | // eslint-disable-next-line no-param-reassign
35 | bytes /= thresh;
36 | // eslint-disable-next-line no-plusplus
37 | ++u;
38 | } while (Math.round(Math.abs(bytes) * r) / r >= thresh && u < units.length - 1);
39 |
40 | return `${bytes.toFixed(dp)} ${units[u]}`;
41 | }
42 |
43 | const max = 8;
44 |
45 | console.log('n : string size : duration');
46 |
47 | new Array(max).fill(0).forEach((_, i) => {
48 | const bytes = 10 ** i;
49 | const doc = createDoc(bytes);
50 | const then = Date.now();
51 |
52 | rdmd.mdast(doc);
53 | const duration = Date.now() - then;
54 |
55 | console.log(`${i} : ${humanFileSize(new Blob([doc]).size)} : ${duration / 1000} s`);
56 | });
57 |
--------------------------------------------------------------------------------
/stylelint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: '@readme/stylelint-config',
3 | rules: {
4 | 'alpha-value-notation': null,
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/styles/components.scss:
--------------------------------------------------------------------------------
1 | @import '../components/Image/style';
2 | @import '../components/Table/style';
3 | @import '../components/TableOfContents/style';
4 | @import '../components/Code/style';
5 | @import '../components/CodeTabs/style';
6 | @import '../components/Callout/style';
7 | @import '../components/Heading/style';
8 | @import '../components/Embed/style';
9 | @import '../components/Glossary/style';
10 |
--------------------------------------------------------------------------------
/styles/main.scss:
--------------------------------------------------------------------------------
1 | @import './gfm';
2 | @import './components';
3 |
4 | :root {
5 | // --markdown-radius: 3px;
6 | // --markdown-edge: #eee;
7 | --markdown-text: inherit;
8 | --markdown-title: inherit;
9 | --markdown-title-font: inherit;
10 | --markdown-font: inherit;
11 | --markdown-font-size: inherit;
12 | --markdown-line-height: 1.5;
13 | }
14 |
15 | .field-description,
16 | .markdown-body {
17 | @include gfm;
18 |
19 | font-size: var(--markdown-font-size, 14px);
20 | }
21 |
--------------------------------------------------------------------------------
/styles/mixins/dark-mode.scss:
--------------------------------------------------------------------------------
1 | /* We’re planning to move this in to the monorepo in which case
2 | we could share the dark mode mix in. Kelly is planning to take
3 | some time to make that move so can we add a comment here to
4 | circle back on this at that point?
5 |
6 | - Rafe
7 | April 2025
8 | */
9 |
10 | @mixin dark-mode($global: false) {
11 | $root: &;
12 |
13 | @if not $root {
14 | [data-color-mode='dark'] {
15 | @content;
16 | }
17 |
18 | [data-color-mode='auto'],
19 | [data-color-mode='system'] {
20 | @media (prefers-color-scheme: dark) {
21 | @content;
22 | }
23 | }
24 | } @else if $global {
25 | :global([data-color-mode='dark']) & {
26 | @content;
27 | }
28 |
29 | :global([data-color-mode='auto']) &,
30 | :global([data-color-mode='system']) & {
31 | @media (prefers-color-scheme: dark) {
32 | @content;
33 | }
34 | }
35 | } @else {
36 | [data-color-mode='dark'] & {
37 | @content;
38 | }
39 |
40 | [data-color-mode='auto'] &,
41 | [data-color-mode='system'] & {
42 | @media (prefers-color-scheme: dark) {
43 | @content;
44 | }
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowSyntheticDefaultImports": true,
4 | "baseUrl": "./",
5 | "checkJs": false,
6 | "declaration": true,
7 | "isolatedModules": true,
8 | "jsx": "react",
9 | "lib": [
10 | "ES2022",
11 | "DOM", // Loaded to our global type availability for access to `fetch`.
12 | "DOM.iterable"
13 | ],
14 | "module": "es2022",
15 | "moduleResolution": "Bundler",
16 | "outDir": "dist",
17 | "resolveJsonModule": true,
18 | "sourceMap": true,
19 | "target": "ES2022"
20 | },
21 | "include": ["./index.ts", "./options.js", "./components", "./contexts", "./example", "./lib", "./processor"],
22 | "exclude": ["node_modules", "dist"]
23 | }
24 |
--------------------------------------------------------------------------------
/utils/consts.ts:
--------------------------------------------------------------------------------
1 | export const tailwindPrefix = 'readme-tailwind';
2 |
--------------------------------------------------------------------------------
/utils/user.ts:
--------------------------------------------------------------------------------
1 | interface Default {
2 | default: string;
3 | name: string;
4 | }
5 |
6 | export interface Variables {
7 | defaults: Default[];
8 | user: Record;
9 | }
10 |
11 | const User = (variables?: Variables) => {
12 | const { user = {}, defaults = [] } = variables || {};
13 |
14 | return new Proxy(user, {
15 | get(target, attribute) {
16 | if (typeof attribute === 'symbol') {
17 | return '';
18 | }
19 |
20 | if (attribute in target) {
21 | return target[attribute];
22 | }
23 |
24 | const def = defaults.find((d: Default) => d.name === attribute);
25 |
26 | return def ? def.default : attribute.toUpperCase();
27 | },
28 | });
29 | };
30 |
31 | export default User;
32 |
--------------------------------------------------------------------------------
/vitest-setup.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/no-extraneous-dependencies */
2 | import '@testing-library/jest-dom';
3 | import '@testing-library/jest-dom/vitest';
4 |
5 | import './__tests__/matchers';
6 |
--------------------------------------------------------------------------------
/vitest.config.mts:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/no-extraneous-dependencies */
2 | import react from '@vitejs/plugin-react';
3 | import { defineConfig } from 'vitest/config';
4 |
5 | export default defineConfig({
6 | plugins: [react()],
7 | test: {
8 | environment: 'jsdom',
9 | exclude: ['**/node_modules/**', '**/dist/**'],
10 | globals: true,
11 | setupFiles: ['./vitest-setup.js'],
12 | workspace: [
13 | {
14 | extends: true,
15 | test: {
16 | exclude: ['__tests__/browser'],
17 | name: 'rdmd',
18 | },
19 | },
20 | ],
21 | },
22 | });
23 |
--------------------------------------------------------------------------------
/vitest.d.ts:
--------------------------------------------------------------------------------
1 | interface CustomMatchers {
2 | toStrictEqualExceptPosition: () => R;
3 | }
4 |
5 | declare module 'vitest' {
6 | interface Assertion extends CustomMatchers {}
7 | interface AsymmetricMatchersContaining extends CustomMatchers {}
8 | }
9 |
--------------------------------------------------------------------------------
/webpack.common.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/no-extraneous-dependencies
2 | */
3 | const ExtractCSS = require('mini-css-extract-plugin');
4 | const sass = require('sass');
5 | const webpack = require('webpack');
6 |
7 | module.exports = {
8 | plugins: [
9 | new ExtractCSS({
10 | filename: '[name].css',
11 | }),
12 | new webpack.ProvidePlugin({
13 | Buffer: ['buffer', 'Buffer'],
14 | }),
15 | new webpack.ProvidePlugin({
16 | process: 'process/browser',
17 | }),
18 | ],
19 | module: {
20 | rules: [
21 | {
22 | test: /\.tsx?$/,
23 | use: {
24 | loader: 'ts-loader',
25 | },
26 | exclude: /node_modules/,
27 | },
28 | {
29 | test: /\.jsx?$/,
30 | exclude: /node_modules\/(?!@readme\/[\w-]+\/)/,
31 | use: {
32 | loader: 'babel-loader',
33 | },
34 | },
35 | {
36 | test: /\.m?js$/,
37 | include: /node_modules/,
38 | type: 'javascript/auto',
39 | resolve: {
40 | fullySpecified: false,
41 | },
42 | },
43 | { test: /tailwindcss\/.*\.css$/, type: 'asset/source' },
44 | {
45 | test: /\.css$/,
46 | exclude: /tailwindcss\/.*\.css$/,
47 | use: [ExtractCSS.loader, 'css-loader'],
48 | },
49 | {
50 | test: /\.scss$/,
51 | use: [
52 | ExtractCSS.loader,
53 | 'css-loader',
54 | {
55 | loader: 'sass-loader',
56 | options: {
57 | implementation: sass,
58 | },
59 | },
60 | ],
61 | },
62 | {
63 | test: /\.(ttf|eot|svg|woff(2)?)(\?[a-z0-9=&.]+)?$/,
64 | exclude: /(node_modules)/,
65 | use: {
66 | loader: 'file-loader',
67 | options: {
68 | name: 'dist/fonts/[hash].[ext]',
69 | },
70 | },
71 | },
72 | ],
73 | },
74 | resolve: {
75 | extensions: ['.js', '.json', '.jsx', '.ts', '.tsx', '.md', '.css'],
76 | fallback: { buffer: require.resolve('buffer'), util: require.resolve('util/') },
77 | },
78 | };
79 |
--------------------------------------------------------------------------------
/webpack.dev.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/no-extraneous-dependencies
2 | */
3 | const path = require('path');
4 |
5 | const webpack = require('webpack');
6 | const { merge } = require('webpack-merge');
7 |
8 | const common = require('./webpack.common');
9 |
10 | const config = merge(common, {
11 | entry: {
12 | demo: './example/index.tsx',
13 | },
14 | output: {
15 | path: path.resolve(__dirname, 'example/'),
16 | filename: '[name].js',
17 | },
18 | devServer: {
19 | static: './example',
20 | compress: true,
21 | port: 9966,
22 | hot: true,
23 | },
24 | devtool: 'eval',
25 | module: {
26 | rules: [
27 | {
28 | test: /\.(txt|mdx?)$/i,
29 | type: 'asset/source',
30 | },
31 | ],
32 | },
33 | plugins: [
34 | new webpack.ProvidePlugin({
35 | process: 'process/browser',
36 | }),
37 | ],
38 | resolve: {
39 | fallback: {
40 | fs: require.resolve('browserify-fs'),
41 | path: require.resolve('path-browserify'),
42 | stream: require.resolve('stream-browserify'),
43 | },
44 | },
45 | });
46 |
47 | module.exports = config;
48 |
--------------------------------------------------------------------------------