├── .dockerignore ├── .prettierignore ├── Procfile ├── __tests__ ├── lib │ ├── mdast │ │ ├── variables │ │ │ ├── in.mdx │ │ │ └── out.json │ │ ├── null-attributes │ │ │ ├── in.mdx │ │ │ └── out.json │ │ ├── variables-with-spaces │ │ │ ├── in.mdx │ │ │ └── out.json │ │ ├── esm │ │ │ └── in.mdx │ │ ├── html-blocks │ │ │ ├── in.mdx │ │ │ └── out.json │ │ ├── images │ │ │ └── inline │ │ │ │ ├── in.mdx │ │ │ │ └── out.json │ │ ├── anchor.test.tsx │ │ ├── tables │ │ │ └── in.mdx │ │ └── __snapshots__ │ │ │ └── anchor.test.tsx.snap │ ├── exports │ │ ├── input │ │ │ ├── singleExport.mdx │ │ │ ├── multipleExports.mdx │ │ │ └── weirdExports.mdx │ │ └── index.test.ts │ ├── compile.test.ts │ ├── mdxish │ │ ├── mdxish.test.ts │ │ └── gemoji.test.ts │ ├── plain │ │ ├── html-blocks.test.ts │ │ └── custom-components.test.ts │ ├── mdx.test.ts │ ├── hast.test.ts │ ├── tags.test.ts │ ├── owlmoji.test.ts │ ├── mdxishTags.test.ts │ ├── render-mdxish │ │ ├── Glossary.test.tsx │ │ └── CodeTabs.test.tsx │ └── __snapshots__ │ │ └── stripComments.test.ts.snap ├── fixtures │ ├── child-tests.mdx │ ├── export-tests.mdx │ ├── tutorial-tile.mdx │ ├── table-of-contents-tests.md │ ├── sanitizing-tests.md │ ├── variable-tests.md │ ├── image-tests.mdx │ ├── tailwind-root-tests.mdx │ ├── callout-tests.md │ └── code-block-tests.md ├── types.d.ts ├── browser │ ├── setup.js │ └── __image_snapshots__ │ │ ├── markdown-test-js-visual-regression-tests-rdmd-syntax-renders-embeds-without-surprises-1-snap.png │ │ ├── markdown-test-js-visual-regression-tests-rdmd-syntax-renders-images-without-surprises-1-snap.png │ │ ├── markdown-test-js-visual-regression-tests-rdmd-syntax-renders-lists-without-surprises-1-snap.png │ │ ├── markdown-test-js-visual-regression-tests-rdmd-syntax-renders-mermaid-without-surprises-1-snap.png │ │ ├── markdown-test-js-visual-regression-tests-rdmd-syntax-renders-tables-without-surprises-1-snap.png │ │ ├── markdown-test-js-visual-regression-tests-rdmd-syntax-renders-callouts-without-surprises-1-snap.png │ │ ├── markdown-test-js-visual-regression-tests-rdmd-syntax-renders-features-without-surprises-1-snap.png │ │ ├── markdown-test-js-visual-regression-tests-rdmd-syntax-renders-headings-without-surprises-1-snap.png │ │ ├── markdown-test-js-visual-regression-tests-rdmd-syntax-renders-vars-test-without-surprises-1-snap.png │ │ ├── markdown-test-js-visual-regression-tests-rdmd-syntax-renders-child-tests-without-surprises-1-snap.png │ │ ├── markdown-test-js-visual-regression-tests-rdmd-syntax-renders-code-blocks-without-surprises-1-snap.png │ │ ├── markdown-test-js-visual-regression-tests-rdmd-syntax-renders-export-tests-without-surprises-1-snap.png │ │ ├── markdown-test-js-visual-regression-tests-rdmd-syntax-renders-html-tests-without-surprises-1-snap.png │ │ ├── markdown-test-js-visual-regression-tests-rdmd-syntax-renders-image-tests-without-surprises-1-snap.png │ │ ├── markdown-test-js-visual-regression-tests-rdmd-syntax-renders-tables-tests-without-surprises-1-snap.png │ │ ├── markdown-test-js-visual-regression-tests-rdmd-syntax-renders-callout-tests-without-surprises-1-snap.png │ │ ├── markdown-test-js-visual-regression-tests-rdmd-syntax-renders-mdx-components-without-surprises-1-snap.png │ │ ├── markdown-test-js-visual-regression-tests-rdmd-syntax-renders-tutorial-tile-without-surprises-1-snap.png │ │ ├── markdown-test-js-visual-regression-tests-rdmd-syntax-renders-code-block-tests-without-surprises-1-snap.png │ │ ├── markdown-test-js-visual-regression-tests-rdmd-syntax-renders-tailwind-root-tests-without-surprises-1-snap.png │ │ ├── markdown-test-js-visual-regression-tests-rdmd-syntax-renders-table-of-contents-tests-without-surprises-1-snap.png │ │ ├── markdown-test-js-visual-regression-tests-rdmd-syntax-renders-callout-tests-in-legacy-mode-without-surprises-1-snap.png │ │ ├── markdown-test-js-visual-regression-tests-rdmd-syntax-renders-html-blocks-style-tags-and-style-attributes-with-safe-mode-off-1-snap.png │ │ └── markdown-test-js-visual-regression-tests-rdmd-syntax-does-not-render-html-blocks-style-tags-and-style-attributes-with-safe-mode-on-1-snap.png ├── migration │ ├── html-blocks │ │ ├── fixtures │ │ │ ├── html-block-escapes │ │ │ │ ├── out.mdx │ │ │ │ └── in.md │ │ │ ├── html-block-escapes-newlines │ │ │ │ ├── out.mdx │ │ │ │ └── in.md │ │ │ └── html-block-with-brackets │ │ │ │ ├── in.md │ │ │ │ └── out.mdx │ │ └── index.test.ts │ ├── html-entities.test.ts │ ├── callouts.test.ts │ ├── emphasis.test.ts │ ├── html-tags.test.ts │ ├── recipes.test.ts │ ├── html-comments.test.ts │ └── magic-block.test.ts ├── __snapshots__ │ ├── compilers.test.ts.snap │ └── html-block-parser.test.js.snap ├── .eslintrc ├── parsers │ ├── escape.test.js │ ├── compact-headings.test.js │ ├── __snapshots__ │ │ └── escape.test.js.snap │ └── tables.test.ts ├── html-block-parser.test.js ├── components │ ├── Variable.test.tsx │ ├── Glossary.test.tsx │ ├── __snapshots__ │ │ └── TableOfContents.test.jsx.snap │ ├── CodeTabs.test.tsx │ ├── Callout.test.tsx │ ├── Code.test.tsx │ └── Image.test.tsx ├── transformers │ ├── embeds.test.ts │ ├── readme-to-mdx.test.ts │ ├── images.test.ts │ ├── preprocess-jsx-expressions.test.ts │ └── mdxish-component-blocks.test.ts ├── compilers │ ├── escape.test.js │ └── yaml.test.js ├── matchers.ts ├── helpers.ts ├── link-parsers.test.js ├── table-flattening │ └── index.test.js ├── react.test.tsx ├── variables │ └── index.test.tsx ├── compilers.test.ts └── Glossary.test.tsx ├── utils ├── consts.ts └── user.ts ├── .eslintignore ├── lib ├── styles.ts ├── exports.ts ├── mdast.ts ├── createElement │ └── index.js ├── mix.ts ├── tags.ts ├── mdastV6.ts ├── hast.ts ├── mdxishTags.ts ├── index.ts ├── registerCustomComponents.js ├── utils │ ├── migrateComments.ts │ ├── makeUseMdxComponents.ts │ └── mdxish │ │ ├── mdxish-get-component-name.ts │ │ └── mdxish-load-components.ts ├── migrate.ts ├── ast-processor.ts ├── renderMdxish.tsx ├── mdx.ts └── owlmoji.ts ├── __mocks__ ├── .eslintrc └── copy-to-clipboard.js ├── example ├── favicon.ico ├── img │ ├── pizzabro.jpg │ ├── nyt-thumbnail.jpg │ └── readme-logo-white-on-blue.png ├── styles │ ├── mixins │ │ ├── expand.scss │ │ └── dark-mode.scss │ ├── methods │ │ └── _merge-multiple.scss │ └── header.scss ├── index.tsx ├── index.legacy.jsx ├── index.html ├── App.tsx ├── Root.tsx ├── RenderError.tsx ├── Header.tsx └── components.ts ├── assets └── img │ └── emojis │ ├── owlbert.png │ ├── owlbert-books.png │ ├── owlbert-mask.png │ ├── owlbert-reading.png │ └── owlbert-thinking.png ├── components ├── TableOfContents │ ├── style.scss │ └── index.tsx ├── Columns │ ├── style.scss │ └── index.tsx ├── TailwindRoot │ └── index.tsx ├── Table │ └── index.tsx ├── Accordion │ ├── index.tsx │ └── style.scss ├── Heading │ ├── style.scss │ └── index.tsx ├── index.ts ├── Tabs │ ├── style.scss │ └── index.tsx ├── PostmanRunButton │ ├── readme.md │ └── index.tsx ├── Recipe.tsx ├── MCPIntro │ └── index.tsx ├── Glossary │ └── index.tsx ├── Cards │ └── index.tsx └── Callout │ └── index.tsx ├── contexts ├── BaseUrl.js ├── Theme.ts ├── CodeOpts.ts ├── GlossaryTerms.ts └── index.tsx ├── processor ├── compile │ ├── plain.ts │ ├── gemoji.ts │ ├── yaml.js │ ├── html-block.ts │ ├── code-tabs.ts │ ├── embed.ts │ ├── callout.ts │ └── index.ts ├── migration │ ├── index.ts │ ├── images.ts │ ├── linkReference.ts │ └── emphasis.ts ├── transform │ ├── migrate-html-tags.ts │ ├── escape-pipes-in-tables.ts │ ├── reusable-content.js │ ├── migrate-html-blocks.ts │ ├── migrate-callouts.ts │ ├── mermaid.ts │ ├── migrate-link-references.ts │ ├── mdx-to-hast.ts │ ├── inject-components.ts │ ├── validate-mcpintro.ts │ ├── compatability.ts │ ├── embeds.ts │ ├── stripComments.ts │ ├── div.ts │ ├── handle-missing-components.ts │ ├── index.ts │ ├── gemoji+.ts │ └── tailwind.tsx └── plugin │ └── table-flattening.js ├── stylelint.config.js ├── vitest-setup.js ├── .gitignore ├── vitest.d.ts ├── jest-puppeteer.config.js ├── styles ├── main.scss ├── components.scss └── mixins │ └── dark-mode.scss ├── .npm-upgrade.json ├── .github ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── bundlewatch.yml │ ├── codeql-analysis.yml │ ├── ci.yml │ └── release.yml ├── hooks └── useHydrated │ └── index.tsx ├── enums.ts ├── docs ├── images.md ├── mermaid.md ├── mdx-components.mdx ├── syntax-extensions.md ├── headings.md ├── features.md └── tables.md ├── vitest.config.mts ├── .eslintrc.js ├── Dockerfile ├── tsconfig.json ├── LICENSE ├── index.tsx ├── jest.config.js ├── errors └── mdx-syntax-error.ts ├── babel.config.js ├── webpack.dev.js ├── sanitize.schema.js ├── scripts └── perf-test.js └── Makefile /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: npx http-server example --port $PORT 2 | -------------------------------------------------------------------------------- /__tests__/lib/mdast/variables/in.mdx: -------------------------------------------------------------------------------- 1 | Hello, {user.name} 2 | -------------------------------------------------------------------------------- /utils/consts.ts: -------------------------------------------------------------------------------- 1 | export const tailwindPrefix = 'readme-tailwind'; 2 | -------------------------------------------------------------------------------- /__tests__/lib/mdast/null-attributes/in.mdx: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | coverage 3 | dist 4 | example/index* 5 | -------------------------------------------------------------------------------- /lib/styles.ts: -------------------------------------------------------------------------------- 1 | const styles = () => {}; 2 | 3 | export default styles; 4 | -------------------------------------------------------------------------------- /__mocks__/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@readme/eslint-config/testing/jest" 3 | } 4 | -------------------------------------------------------------------------------- /__tests__/lib/mdast/variables-with-spaces/in.mdx: -------------------------------------------------------------------------------- 1 | Hello, { user['this is cursed'] } 2 | -------------------------------------------------------------------------------- /example/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readmeio/markdown/HEAD/example/favicon.ico -------------------------------------------------------------------------------- /__mocks__/copy-to-clipboard.js: -------------------------------------------------------------------------------- 1 | const copy = jest.fn(() => true); 2 | 3 | module.exports = copy; 4 | -------------------------------------------------------------------------------- /example/img/pizzabro.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readmeio/markdown/HEAD/example/img/pizzabro.jpg -------------------------------------------------------------------------------- /assets/img/emojis/owlbert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readmeio/markdown/HEAD/assets/img/emojis/owlbert.png -------------------------------------------------------------------------------- /example/img/nyt-thumbnail.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readmeio/markdown/HEAD/example/img/nyt-thumbnail.jpg -------------------------------------------------------------------------------- /__tests__/fixtures/child-tests.mdx: -------------------------------------------------------------------------------- 1 | 2 | Step One 3 | Step Two 4 | 5 | -------------------------------------------------------------------------------- /__tests__/types.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.mdx?raw' { 2 | const value: string; 3 | export default value; 4 | } 5 | -------------------------------------------------------------------------------- /assets/img/emojis/owlbert-books.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readmeio/markdown/HEAD/assets/img/emojis/owlbert-books.png -------------------------------------------------------------------------------- /assets/img/emojis/owlbert-mask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readmeio/markdown/HEAD/assets/img/emojis/owlbert-mask.png -------------------------------------------------------------------------------- /components/TableOfContents/style.scss: -------------------------------------------------------------------------------- 1 | .toc-list { 2 | .glossary-tooltip { 3 | pointer-events: none; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /assets/img/emojis/owlbert-reading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readmeio/markdown/HEAD/assets/img/emojis/owlbert-reading.png -------------------------------------------------------------------------------- /assets/img/emojis/owlbert-thinking.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readmeio/markdown/HEAD/assets/img/emojis/owlbert-thinking.png -------------------------------------------------------------------------------- /example/img/readme-logo-white-on-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readmeio/markdown/HEAD/example/img/readme-logo-white-on-blue.png -------------------------------------------------------------------------------- /__tests__/browser/setup.js: -------------------------------------------------------------------------------- 1 | import { toMatchImageSnapshot } from 'jest-image-snapshot'; 2 | 3 | expect.extend({ toMatchImageSnapshot }); 4 | -------------------------------------------------------------------------------- /contexts/BaseUrl.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const BaseUrlContext = React.createContext('/'); 4 | 5 | export default BaseUrlContext; 6 | -------------------------------------------------------------------------------- /processor/compile/plain.ts: -------------------------------------------------------------------------------- 1 | import type { Plain } from '../../types'; 2 | 3 | const plain = (node: Plain) => node.value; 4 | 5 | export default plain; 6 | -------------------------------------------------------------------------------- /stylelint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: '@readme/stylelint-config', 3 | rules: { 4 | 'alpha-value-notation': null, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /processor/compile/gemoji.ts: -------------------------------------------------------------------------------- 1 | import type { Gemoji } from '../../types'; 2 | 3 | const gemoji = (node: Gemoji) => `:${node.name}:`; 4 | 5 | export default gemoji; 6 | -------------------------------------------------------------------------------- /__tests__/lib/mdast/esm/in.mdx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Foo = () => { 4 | return
Hello World
; 5 | } 6 | 7 | ## Hey there 8 | -------------------------------------------------------------------------------- /contexts/Theme.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | const ThemeContext = createContext<'dark' | 'light'>('light'); 4 | 5 | export default ThemeContext; 6 | -------------------------------------------------------------------------------- /__tests__/fixtures/export-tests.mdx: -------------------------------------------------------------------------------- 1 | ## Same component file, but different export name 2 | 3 | ``` 4 | - 5 | - 6 | ``` 7 | 8 | - 9 | - 10 | -------------------------------------------------------------------------------- /__tests__/lib/mdast/html-blocks/in.mdx: -------------------------------------------------------------------------------- 1 | {` 2 |
3 |   
4 |     RegexpExtract([Address], "\\\\s*([a-zA-Z]+)", 2)
5 |   
6 | 
7 | `}
8 | -------------------------------------------------------------------------------- /__tests__/migration/html-blocks/fixtures/html-block-escapes/out.mdx: -------------------------------------------------------------------------------- 1 | {` 2 |
RegexpExtract([Address], "\\\\s*([a-zA-Z]+)", 2)
3 | `}
4 | -------------------------------------------------------------------------------- /__tests__/lib/exports/input/singleExport.mdx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Foo = () => { 4 | return
Hello World
; 5 | } 6 | 7 | ## Hey there 8 | -------------------------------------------------------------------------------- /__tests__/lib/mdast/images/inline/in.mdx: -------------------------------------------------------------------------------- 1 | This should work Captioned. 2 | -------------------------------------------------------------------------------- /__tests__/migration/html-blocks/fixtures/html-block-escapes/in.md: -------------------------------------------------------------------------------- 1 | [block:html] 2 | { 3 | "html": "
RegexpExtract([Address], \"\\\\s*([a-zA-Z]+)\", 2)
" 4 | } 5 | [/block] 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /contexts/CodeOpts.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | // used for the copyButtons prop 4 | const CodeOptsContext = createContext(false); 5 | 6 | export default CodeOptsContext; 7 | -------------------------------------------------------------------------------- /__tests__/migration/html-blocks/fixtures/html-block-escapes-newlines/out.mdx: -------------------------------------------------------------------------------- 1 | {` 2 |
3 |    
4 |     RegexpExtract([Address], "\\\\s*([a-zA-Z]+)", 2)
5 |   
6 | 
7 | `}
8 | -------------------------------------------------------------------------------- /__tests__/migration/html-blocks/fixtures/html-block-escapes-newlines/in.md: -------------------------------------------------------------------------------- 1 | [block:html] 2 | { 3 | "html": "
\n   \n    RegexpExtract([Address], \"\\\\s*([a-zA-Z]+)\", 2)\n  \n
" 4 | } 5 | [/block] 6 | -------------------------------------------------------------------------------- /__tests__/migration/html-blocks/fixtures/html-block-with-brackets/in.md: -------------------------------------------------------------------------------- 1 | [block:html] 2 | { 3 | "html": "{`\n
\n  \n    RegexpExtract([Address], \"\\\\s*([a-zA-Z]+)\", 2)\n  \n
\n`}" 4 | } 5 | [/block] 6 | -------------------------------------------------------------------------------- /example/styles/mixins/expand.scss: -------------------------------------------------------------------------------- 1 | @mixin expand($map, $prefix: '') { 2 | // for use with Webpack sass-loader in :export {} 3 | @each $key, $val in $map { 4 | #{unquote($prefix)}#{unquote($key)}: #{$val}; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /lib/exports.ts: -------------------------------------------------------------------------------- 1 | import { getExports } from '../processor/utils'; 2 | 3 | import mdast from './mdast'; 4 | 5 | const exports = (doc: string) => { 6 | return getExports(mdast(doc)); 7 | }; 8 | 9 | export default exports; 10 | -------------------------------------------------------------------------------- /__tests__/migration/html-blocks/fixtures/html-block-with-brackets/out.mdx: -------------------------------------------------------------------------------- 1 | {` 2 | {\` 3 |
 4 |   
 5 |     RegexpExtract([Address], "\\\\s*([a-zA-Z]+)", 2)
 6 |   
 7 | 
8 | \`} 9 | `}
10 | -------------------------------------------------------------------------------- /components/Columns/style.scss: -------------------------------------------------------------------------------- 1 | $iphone-plus: 414px; 2 | 3 | .Columns { 4 | display: grid; 5 | gap: var(--md, 20px); 6 | 7 | @media (max-width: $iphone-plus) { 8 | grid-template-columns: 1fr !important; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/compilers.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`ReadMe Flavored Blocks > Embed 1`] = ` 4 | "[Embedded meta links.](https://nyti.me/s/gzoa2xb2v3 "@embed") 5 | " 6 | `; 7 | -------------------------------------------------------------------------------- /example/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-import-module-exports */ 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | 5 | import App from './App'; 6 | 7 | ReactDOM.render(, document.getElementById('rdmd-demo')); 8 | -------------------------------------------------------------------------------- /__tests__/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@readme/eslint-config/testing/jest.js", "@readme/eslint-config/testing/vitest.js"], 3 | "rules": { 4 | "testing-library/no-container": "off", 5 | "testing-library/no-node-access": "off" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /example/styles/methods/_merge-multiple.scss: -------------------------------------------------------------------------------- 1 | @function map-merge-multiple($maps...) { 2 | $merged-maps: (); 3 | 4 | @each $map in $maps { 5 | $merged-maps: map-merge($merged-maps, $map); 6 | } 7 | 8 | @return $merged-maps; 9 | } 10 | -------------------------------------------------------------------------------- /__tests__/lib/exports/input/multipleExports.mdx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Foo = () => { 4 | return
Hello World
; 5 | } 6 | 7 | export const Bar = () => { 8 | return ; 9 | } 10 | 11 | ## Hey there 12 | -------------------------------------------------------------------------------- /example/index.legacy.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-import-module-exports */ 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | 5 | import Demo from './Demo'; 6 | 7 | ReactDOM.render(, document.getElementById('rdmd-demo')); 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /__tests__/parsers/escape.test.js: -------------------------------------------------------------------------------- 1 | import { mdast } from '../../index'; 2 | 3 | describe('Escape', () => { 4 | it('uses the "escape" type', () => { 5 | const md = '\\¶'; 6 | expect(mdast(md, { settings: { position: true } })).toMatchSnapshot(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | node_modules 3 | 4 | .env 5 | .idea/ 6 | .vscode/ 7 | *.code-* 8 | *.sublime-* 9 | *.stylelintrc.json 10 | 11 | .DS_Store 12 | 13 | __diff_output__ 14 | 15 | example/public/img/emojis 16 | example/demo.js* 17 | example/demo.css 18 | 19 | dist 20 | -------------------------------------------------------------------------------- /__tests__/fixtures/tutorial-tile.mdx: -------------------------------------------------------------------------------- 1 | ## Tutorial Tile 2 | 3 | We render a placeholder in this library, as the actual implemenation is deeply tied to the main app 4 | 5 | 6 | 7 | Moving forward, we're actually going to use `Recipe` 8 | 9 | 10 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /contexts/GlossaryTerms.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | export interface GlossaryTerm { 4 | _id?: string; 5 | definition: string; 6 | term: string; 7 | } 8 | 9 | const GlossaryContext = createContext([]); 10 | 11 | export default GlossaryContext; 12 | -------------------------------------------------------------------------------- /__tests__/html-block-parser.test.js: -------------------------------------------------------------------------------- 1 | import { mdast } from '../index'; 2 | 3 | describe('Parse html block', () => { 4 | it('parses an html block', () => { 5 | const text = ` 6 |
Some block html
7 | `; 8 | 9 | expect(mdast(text)).toMatchSnapshot(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /__tests__/fixtures/table-of-contents-tests.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Table Of Contents Tests' 3 | category: 5fdf9fc9c2a7ef443e937315 4 | hidden: true 5 | --- 6 | 7 | # Variables 8 | 9 | # Glossary Items demo 10 | 11 | ## Custom Components 12 | 13 | 14 | -------------------------------------------------------------------------------- /__tests__/browser/__image_snapshots__/markdown-test-js-visual-regression-tests-rdmd-syntax-renders-embeds-without-surprises-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readmeio/markdown/HEAD/__tests__/browser/__image_snapshots__/markdown-test-js-visual-regression-tests-rdmd-syntax-renders-embeds-without-surprises-1-snap.png -------------------------------------------------------------------------------- /__tests__/browser/__image_snapshots__/markdown-test-js-visual-regression-tests-rdmd-syntax-renders-images-without-surprises-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readmeio/markdown/HEAD/__tests__/browser/__image_snapshots__/markdown-test-js-visual-regression-tests-rdmd-syntax-renders-images-without-surprises-1-snap.png -------------------------------------------------------------------------------- /__tests__/browser/__image_snapshots__/markdown-test-js-visual-regression-tests-rdmd-syntax-renders-lists-without-surprises-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readmeio/markdown/HEAD/__tests__/browser/__image_snapshots__/markdown-test-js-visual-regression-tests-rdmd-syntax-renders-lists-without-surprises-1-snap.png -------------------------------------------------------------------------------- /__tests__/browser/__image_snapshots__/markdown-test-js-visual-regression-tests-rdmd-syntax-renders-mermaid-without-surprises-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readmeio/markdown/HEAD/__tests__/browser/__image_snapshots__/markdown-test-js-visual-regression-tests-rdmd-syntax-renders-mermaid-without-surprises-1-snap.png -------------------------------------------------------------------------------- /__tests__/browser/__image_snapshots__/markdown-test-js-visual-regression-tests-rdmd-syntax-renders-tables-without-surprises-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readmeio/markdown/HEAD/__tests__/browser/__image_snapshots__/markdown-test-js-visual-regression-tests-rdmd-syntax-renders-tables-without-surprises-1-snap.png -------------------------------------------------------------------------------- /__tests__/browser/__image_snapshots__/markdown-test-js-visual-regression-tests-rdmd-syntax-renders-callouts-without-surprises-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readmeio/markdown/HEAD/__tests__/browser/__image_snapshots__/markdown-test-js-visual-regression-tests-rdmd-syntax-renders-callouts-without-surprises-1-snap.png -------------------------------------------------------------------------------- /__tests__/browser/__image_snapshots__/markdown-test-js-visual-regression-tests-rdmd-syntax-renders-features-without-surprises-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readmeio/markdown/HEAD/__tests__/browser/__image_snapshots__/markdown-test-js-visual-regression-tests-rdmd-syntax-renders-features-without-surprises-1-snap.png -------------------------------------------------------------------------------- /__tests__/browser/__image_snapshots__/markdown-test-js-visual-regression-tests-rdmd-syntax-renders-headings-without-surprises-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readmeio/markdown/HEAD/__tests__/browser/__image_snapshots__/markdown-test-js-visual-regression-tests-rdmd-syntax-renders-headings-without-surprises-1-snap.png -------------------------------------------------------------------------------- /__tests__/browser/__image_snapshots__/markdown-test-js-visual-regression-tests-rdmd-syntax-renders-vars-test-without-surprises-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readmeio/markdown/HEAD/__tests__/browser/__image_snapshots__/markdown-test-js-visual-regression-tests-rdmd-syntax-renders-vars-test-without-surprises-1-snap.png -------------------------------------------------------------------------------- /jest-puppeteer.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | launch: { 3 | args: ['--no-sandbox'], 4 | dumpio: true, 5 | }, 6 | server: { 7 | command: 'npm run start', 8 | debug: true, 9 | port: process.env.PORT || 9966, 10 | protocol: 'http-get', 11 | launchTimeout: 60000, 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /__tests__/browser/__image_snapshots__/markdown-test-js-visual-regression-tests-rdmd-syntax-renders-child-tests-without-surprises-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readmeio/markdown/HEAD/__tests__/browser/__image_snapshots__/markdown-test-js-visual-regression-tests-rdmd-syntax-renders-child-tests-without-surprises-1-snap.png -------------------------------------------------------------------------------- /__tests__/browser/__image_snapshots__/markdown-test-js-visual-regression-tests-rdmd-syntax-renders-code-blocks-without-surprises-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readmeio/markdown/HEAD/__tests__/browser/__image_snapshots__/markdown-test-js-visual-regression-tests-rdmd-syntax-renders-code-blocks-without-surprises-1-snap.png -------------------------------------------------------------------------------- /__tests__/browser/__image_snapshots__/markdown-test-js-visual-regression-tests-rdmd-syntax-renders-export-tests-without-surprises-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readmeio/markdown/HEAD/__tests__/browser/__image_snapshots__/markdown-test-js-visual-regression-tests-rdmd-syntax-renders-export-tests-without-surprises-1-snap.png -------------------------------------------------------------------------------- /__tests__/browser/__image_snapshots__/markdown-test-js-visual-regression-tests-rdmd-syntax-renders-html-tests-without-surprises-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readmeio/markdown/HEAD/__tests__/browser/__image_snapshots__/markdown-test-js-visual-regression-tests-rdmd-syntax-renders-html-tests-without-surprises-1-snap.png -------------------------------------------------------------------------------- /__tests__/browser/__image_snapshots__/markdown-test-js-visual-regression-tests-rdmd-syntax-renders-image-tests-without-surprises-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readmeio/markdown/HEAD/__tests__/browser/__image_snapshots__/markdown-test-js-visual-regression-tests-rdmd-syntax-renders-image-tests-without-surprises-1-snap.png -------------------------------------------------------------------------------- /__tests__/browser/__image_snapshots__/markdown-test-js-visual-regression-tests-rdmd-syntax-renders-tables-tests-without-surprises-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readmeio/markdown/HEAD/__tests__/browser/__image_snapshots__/markdown-test-js-visual-regression-tests-rdmd-syntax-renders-tables-tests-without-surprises-1-snap.png -------------------------------------------------------------------------------- /__tests__/browser/__image_snapshots__/markdown-test-js-visual-regression-tests-rdmd-syntax-renders-callout-tests-without-surprises-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readmeio/markdown/HEAD/__tests__/browser/__image_snapshots__/markdown-test-js-visual-regression-tests-rdmd-syntax-renders-callout-tests-without-surprises-1-snap.png -------------------------------------------------------------------------------- /__tests__/browser/__image_snapshots__/markdown-test-js-visual-regression-tests-rdmd-syntax-renders-mdx-components-without-surprises-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readmeio/markdown/HEAD/__tests__/browser/__image_snapshots__/markdown-test-js-visual-regression-tests-rdmd-syntax-renders-mdx-components-without-surprises-1-snap.png -------------------------------------------------------------------------------- /__tests__/browser/__image_snapshots__/markdown-test-js-visual-regression-tests-rdmd-syntax-renders-tutorial-tile-without-surprises-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readmeio/markdown/HEAD/__tests__/browser/__image_snapshots__/markdown-test-js-visual-regression-tests-rdmd-syntax-renders-tutorial-tile-without-surprises-1-snap.png -------------------------------------------------------------------------------- /__tests__/browser/__image_snapshots__/markdown-test-js-visual-regression-tests-rdmd-syntax-renders-code-block-tests-without-surprises-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readmeio/markdown/HEAD/__tests__/browser/__image_snapshots__/markdown-test-js-visual-regression-tests-rdmd-syntax-renders-code-block-tests-without-surprises-1-snap.png -------------------------------------------------------------------------------- /__tests__/browser/__image_snapshots__/markdown-test-js-visual-regression-tests-rdmd-syntax-renders-tailwind-root-tests-without-surprises-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readmeio/markdown/HEAD/__tests__/browser/__image_snapshots__/markdown-test-js-visual-regression-tests-rdmd-syntax-renders-tailwind-root-tests-without-surprises-1-snap.png -------------------------------------------------------------------------------- /__tests__/lib/mdast/anchor.test.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { mdast } from '../../../lib'; 3 | 4 | describe('convert anchor tag', () => { 5 | it('converts anchor tag to link node', () => { 6 | const mdx = ` 7 | ReadMe 8 | `; 9 | 10 | expect(mdast(mdx)).toMatchSnapshot(); 11 | }); 12 | }) 13 | -------------------------------------------------------------------------------- /__tests__/browser/__image_snapshots__/markdown-test-js-visual-regression-tests-rdmd-syntax-renders-table-of-contents-tests-without-surprises-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readmeio/markdown/HEAD/__tests__/browser/__image_snapshots__/markdown-test-js-visual-regression-tests-rdmd-syntax-renders-table-of-contents-tests-without-surprises-1-snap.png -------------------------------------------------------------------------------- /__tests__/browser/__image_snapshots__/markdown-test-js-visual-regression-tests-rdmd-syntax-renders-callout-tests-in-legacy-mode-without-surprises-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readmeio/markdown/HEAD/__tests__/browser/__image_snapshots__/markdown-test-js-visual-regression-tests-rdmd-syntax-renders-callout-tests-in-legacy-mode-without-surprises-1-snap.png -------------------------------------------------------------------------------- /__tests__/browser/__image_snapshots__/markdown-test-js-visual-regression-tests-rdmd-syntax-renders-html-blocks-style-tags-and-style-attributes-with-safe-mode-off-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readmeio/markdown/HEAD/__tests__/browser/__image_snapshots__/markdown-test-js-visual-regression-tests-rdmd-syntax-renders-html-blocks-style-tags-and-style-attributes-with-safe-mode-off-1-snap.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /styles/main.scss: -------------------------------------------------------------------------------- 1 | @import './gfm'; 2 | @import './components'; 3 | 4 | :root { 5 | // --markdown-radius: 3px; 6 | // --markdown-edge: #eee; 7 | --markdown-title: inherit; 8 | } 9 | 10 | body { 11 | -webkit-font-smoothing: antialiased; 12 | } 13 | 14 | .field-description, 15 | .markdown-body { 16 | @include gfm; 17 | 18 | font-size: var(--markdown-font-size, 14px); 19 | } 20 | -------------------------------------------------------------------------------- /__tests__/browser/__image_snapshots__/markdown-test-js-visual-regression-tests-rdmd-syntax-does-not-render-html-blocks-style-tags-and-style-attributes-with-safe-mode-on-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readmeio/markdown/HEAD/__tests__/browser/__image_snapshots__/markdown-test-js-visual-regression-tests-rdmd-syntax-does-not-render-html-blocks-style-tags-and-style-attributes-with-safe-mode-on-1-snap.png -------------------------------------------------------------------------------- /lib/mdast.ts: -------------------------------------------------------------------------------- 1 | import type { MdastOpts } from './ast-processor'; 2 | import type { Root } from 'mdast'; 3 | 4 | import astProcessor from './ast-processor'; 5 | 6 | const mdast = (text: string, opts: MdastOpts = {}): Root => { 7 | const processor = astProcessor(opts); 8 | const tree = processor.parse(text); 9 | 10 | return processor.runSync(tree); 11 | }; 12 | 13 | export default mdast; 14 | -------------------------------------------------------------------------------- /lib/createElement/index.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | 3 | const { CodeTabs } = require('../../components/CodeTabs'); 4 | 5 | const createElement = (type, props, ...children) => { 6 | const rdmdType = type === 'div' && props?.className === 'code-tabs' ? CodeTabs : type; 7 | 8 | return React.createElement(rdmdType, props, ...children); 9 | }; 10 | 11 | module.exports = createElement; 12 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /__tests__/fixtures/sanitizing-tests.md: -------------------------------------------------------------------------------- 1 | 2 | ## Sanitizing `style` tags 3 | 4 | 9 | 10 | 11 | ## Sanitizing `style` attributes 12 | 13 |

fish content

14 | 15 | 16 | ## Sanitizing html blocks 17 | 18 | [block:html] 19 | { 20 | "html": "" 21 | } 22 | [/block] 23 | -------------------------------------------------------------------------------- /components/TailwindRoot/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { tailwindPrefix } from '../../utils/consts'; 4 | 5 | interface Props extends React.PropsWithChildren<{ flow: boolean }> {} 6 | 7 | const TailwindRoot = ({ children, flow }: Props) => { 8 | const Tag = flow ? 'div' : 'span'; 9 | 10 | return {children}; 11 | }; 12 | 13 | export default TailwindRoot; 14 | -------------------------------------------------------------------------------- /__tests__/lib/compile.test.ts: -------------------------------------------------------------------------------- 1 | import { compile } from '../../index'; 2 | 3 | describe('compile', () => { 4 | describe("{ format: 'md' }", () => { 5 | it('returns plain text of markdown components', () => { 6 | const md = '[link to doc](doc:getting-started)'; 7 | 8 | const tree = compile(md, { format: 'md' }); 9 | expect(tree).toMatch(/href: "doc:getting-started"/); 10 | }); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /__tests__/migration/html-entities.test.ts: -------------------------------------------------------------------------------- 1 | import { migrate } from '../helpers'; 2 | 3 | describe('migrating html entities', () => { 4 | it('removes html entity spaces', () => { 5 | const md = ` 6 | { 7 | "json": true 8 | } 9 | `; 10 | const mdx = migrate(md); 11 | 12 | expect(mdx).toMatchInlineSnapshot(` 13 | "\\{\\ 14 | "json": true\\ 15 | } 16 | " 17 | `); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Markdown Demo 5 | 6 | 7 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /__tests__/components/Variable.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import React from 'react'; 3 | 4 | import { execute } from '../helpers'; 5 | 6 | describe('Variable', () => { 7 | it('render a variable', () => { 8 | const md = ''; 9 | const Content = execute(md); 10 | 11 | render(); 12 | 13 | expect(screen.getByText('NAME')).toBeVisible(); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /processor/compile/html-block.ts: -------------------------------------------------------------------------------- 1 | import type { HTMLBlock } from '../../types'; 2 | 3 | import { reformatHTML, getHProps } from '../utils' 4 | 5 | const htmlBlock = (node: HTMLBlock) => { 6 | const { runScripts, html } = getHProps(node); 7 | 8 | return `{\` 9 | ${ reformatHTML(html) } 10 | \`}`; 11 | } 12 | 13 | export default htmlBlock; 14 | -------------------------------------------------------------------------------- /__tests__/components/Glossary.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import React from 'react'; 3 | 4 | import { execute } from '../helpers'; 5 | 6 | describe('Glossary', () => { 7 | it('renders a glossary item', () => { 8 | const md = 'parliament'; 9 | const Content = execute(md); 10 | render(); 11 | 12 | expect(screen.getByText('parliament')).toBeVisible(); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /__tests__/transformers/embeds.test.ts: -------------------------------------------------------------------------------- 1 | import { mdast } from '../../index'; 2 | 3 | describe('embeds transformer', () => { 4 | it('converts a link with a title of "@embed" to an embed-block', () => { 5 | const md = ` 6 | [alt](https://example.com/cool.pdf "@embed") 7 | `; 8 | const tree = mdast(md); 9 | 10 | expect(tree.children[0].type).toBe('embed-block'); 11 | expect(tree.children[0].data.hProperties.title).toBe('alt'); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /.npm-upgrade.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": { 3 | "@hot-loader/react-dom": { 4 | "versions": "17.0.2", 5 | "reason": "staying on react 16" 6 | }, 7 | "codemirror": { 8 | "versions": "", 9 | "reason": "staying on 5" 10 | }, 11 | "react": { 12 | "versions": "18.2.0", 13 | "reason": "staying on 16" 14 | }, 15 | "react-dom": { 16 | "versions": "18.2.0", 17 | "reason": "staying on 16" 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /__tests__/lib/mdast/tables/in.mdx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 21 | 22 | 25 | 26 | 27 | 28 |
5 | Heading One 6 | 9 | Heading Two 10 |
18 | * list item 19 | * list item 20 | 23 | :shrug: 24 |
29 | -------------------------------------------------------------------------------- /__tests__/fixtures/variable-tests.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Variable Tests' 3 | category: 5fdf9fc9c2a7ef443e937315 4 | hidden: true 5 | --- 6 | 7 | This is the variable `defvar`: {user.defvar} 8 | 9 | Ok, but this one is defined: {user.email} 10 | 11 | It **does** render in code blocks: 12 | 13 | ``` 14 | {user.defvar} 15 | ``` 16 | 17 | And if you don't want that, you can escape it: 18 | 19 | ``` 20 | \{user.defvar} 21 | ``` 22 | 23 | ## Glossary Items 24 | 25 | demo 26 | -------------------------------------------------------------------------------- /__tests__/lib/exports/input/weirdExports.mdx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export function Foo() { 4 | return
Hello World
; 5 | } 6 | 7 | export const bar = () => { 8 | return ; 9 | } 10 | export function doSomethingFunction(input) { 11 | return input.trim(); 12 | } 13 | 14 | export const 15 | YELLING = () => {} 16 | export const SingleNewlinesAreAnnoying = () => "hey"; 17 | export let x = 2; 18 | export class MyClass { 19 | } 20 | 21 | ## Hey there 22 | -------------------------------------------------------------------------------- /processor/compile/code-tabs.ts: -------------------------------------------------------------------------------- 1 | import type { CodeTabs } from '../../types'; 2 | 3 | import { NodeTypes } from '../../enums'; 4 | 5 | const codeTabs = (node: CodeTabs, _, state, info) => { 6 | const exit = state.enter(NodeTypes.codeTabs); 7 | const tracker = state.createTracker(info); 8 | state.join.push(() => 0); 9 | const value = state.containerFlow(node, tracker.current()); 10 | state.join.pop(); 11 | 12 | exit(); 13 | return value; 14 | }; 15 | 16 | export default codeTabs; 17 | -------------------------------------------------------------------------------- /processor/transform/migrate-html-tags.ts: -------------------------------------------------------------------------------- 1 | import type { Root } from 'mdast'; 2 | 3 | import { visit } from 'unist-util-visit'; 4 | 5 | // Add more visits to migrate other HTML tags here 6 | const migrateHtmlTags = () => (tree: Root) => { 7 | // A common issue is that
tags are not properly closed 8 | visit(tree, 'html', htmlNode => { 9 | htmlNode.value = htmlNode.value.replaceAll(/)([^>]*?)>/g, ''); 10 | }); 11 | }; 12 | 13 | export default migrateHtmlTags; 14 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | [![PR App][icn]][demo] | Fix RM-XYZ 2 | :-------------------:|:----------: 3 | 4 | ## 🧰 Changes 5 | 6 | Describe your changes in detail. 7 | 8 | ## 🧬 QA & Testing 9 | 10 | - [Broken on production][prod]. 11 | - [Working in this PR app][demo]. 12 | 13 | 14 | [demo]: https://markdown-pr-PR_NUMBER.herokuapp.com 15 | [prod]: https://SUBDOMAIN.readme.io 16 | [icn]: https://user-images.githubusercontent.com/886627/160426047-1bee9488-305a-4145-bb2b-09d8b757d38a.svg 17 | -------------------------------------------------------------------------------- /__tests__/lib/mdxish/mdxish.test.ts: -------------------------------------------------------------------------------- 1 | import { mdxish } from '../../../lib/mdxish'; 2 | 3 | describe('mdxish', () => { 4 | describe('invalid mdx syntax', () => { 5 | it('should render unclosed tags', () => { 6 | const md = '
'; 7 | expect(() => mdxish(md)).not.toThrow(); 8 | }); 9 | 10 | it('should render content in new lines', () => { 11 | const md = `
hello 12 |
`; 13 | expect(() => mdxish(md)).not.toThrow(); 14 | }); 15 | }); 16 | }); -------------------------------------------------------------------------------- /lib/mix.ts: -------------------------------------------------------------------------------- 1 | import type { MdxishOpts } from './mdxish'; 2 | 3 | import rehypeStringify from 'rehype-stringify'; 4 | import { unified } from 'unified'; 5 | 6 | import { mdxish } from './mdxish'; 7 | 8 | /** Wrapper around mdxish that returns an HTML string instead of a HAST tree. */ 9 | const mix = (text: string, opts: MdxishOpts = {}): string => { 10 | const hast = mdxish(text, opts); 11 | return String(unified().use(rehypeStringify).stringify(hast)); 12 | }; 13 | 14 | export default mix; 15 | -------------------------------------------------------------------------------- /processor/transform/escape-pipes-in-tables.ts: -------------------------------------------------------------------------------- 1 | import { SKIP, visit } from 'unist-util-visit'; 2 | 3 | export const escapePipesInTables = () => tree => { 4 | visit(tree, 'table', tableNode => { 5 | visit(tableNode, leaf => { 6 | if (!('value' in leaf)) return; 7 | 8 | if (leaf.value.match(/|/g)) { 9 | // escape only unescaped pipes 10 | leaf.value = leaf.value.replaceAll(/(? { 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 | -------------------------------------------------------------------------------- /__tests__/parsers/compact-headings.test.js: -------------------------------------------------------------------------------- 1 | import { mdast } from '../../index'; 2 | 3 | describe('Compact headings', () => { 4 | it('can parse compact headings', () => { 5 | const heading = '#Compact Heading'; 6 | expect(mdast(heading, { settings: { position: true } })).toMatchSnapshot(); 7 | }); 8 | 9 | it('can parse headings that are not compact', () => { 10 | const heading = '# Non-compact Heading'; 11 | expect(mdast(heading, { settings: { position: true } })).toMatchSnapshot(); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /hooks/useHydrated/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | /** 4 | * A hook that returns whether or not the component has been hydrated. 5 | * Useful for components that should only render in the browser, and not during SSR. 6 | * Waiting to render until after hydration avoids React hydration mismatches. 7 | */ 8 | export default function useHydrated(): boolean { 9 | const [isHydrated, setIsHydrated] = useState(false); 10 | useEffect(() => setIsHydrated(true), []); 11 | return isHydrated; 12 | } 13 | -------------------------------------------------------------------------------- /processor/compile/embed.ts: -------------------------------------------------------------------------------- 1 | import type { EmbedBlock } from 'types'; 2 | 3 | import { formatHProps, getHProps } from '../utils'; 4 | 5 | const embed = (node: EmbedBlock) => { 6 | const attributes = formatHProps(node) 7 | const props = getHProps(node); 8 | 9 | if (node.title !== '@embed') { 10 | return `` 11 | } 12 | 13 | return `[${node.label || ''}](${props.url} "${node.title}")` 14 | } 15 | 16 | export default embed; 17 | -------------------------------------------------------------------------------- /__tests__/lib/mdast/null-attributes/out.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "root", 3 | "children": [ 4 | { 5 | "type": "image-block", 6 | "src": "...", 7 | "border": true, 8 | "alt": "", 9 | "children": [], 10 | "title": null, 11 | "data": { 12 | "hName": "img", 13 | "hProperties": { 14 | "alt": "", 15 | "border": true, 16 | "children": [], 17 | "src": "...", 18 | "title": null 19 | } 20 | } 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /__tests__/lib/plain/html-blocks.test.ts: -------------------------------------------------------------------------------- 1 | import { hast, plain } from '../../../index'; 2 | 3 | const Doc = ` 4 | {\` 5 | 10 | 11 |
12 |
Item 1
13 |
Item 2
14 |
15 | \`}
16 | `; 17 | 18 | describe('plain compiler', () => { 19 | it('should parse html-blocks', () => { 20 | const string = plain(hast(Doc)); 21 | expect(string).toBe('Item 1 Item 2'); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /example/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { HashRouter, Navigate, Route, Routes } from 'react-router-dom'; 3 | 4 | import './demo.scss'; 5 | import docs from './docs'; 6 | import Root from './Root'; 7 | 8 | const App = () => { 9 | return ( 10 | 11 | 12 | } path="/:fixture" /> 13 | } path="*" /> 14 | 15 | 16 | ); 17 | }; 18 | 19 | export default App; 20 | -------------------------------------------------------------------------------- /__tests__/lib/mdxish/gemoji.test.ts: -------------------------------------------------------------------------------- 1 | import { mix } from '../../../lib'; 2 | 3 | describe('gemoji transformer', () => { 4 | it('should transform shortcodes back to emojis', () => { 5 | const md = `🔁 6 | 7 | :smiley: 8 | 9 | :owlbert:`; 10 | const stringHast = mix(md); 11 | expect(stringHast).toMatchInlineSnapshot(` 12 | "

🔁

13 |

😃

14 |

:owlbert:

" 15 | `); 16 | 17 | }); 18 | }); -------------------------------------------------------------------------------- /__tests__/components/__snapshots__/TableOfContents.test.jsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`Table of Contents includes two heading levels 1`] = ` 4 | "" 12 | `; 13 | -------------------------------------------------------------------------------- /components/Table/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface Props extends JSX.IntrinsicAttributes { 4 | align: ('center' | 'left' | 'right')[]; 5 | children: [React.ReactElement]; 6 | } 7 | 8 | const Table = (props: Props) => { 9 | const { children } = props; 10 | 11 | return ( 12 |
13 |
14 | {children}
15 |
16 |
17 | ); 18 | }; 19 | 20 | export default Table; 21 | -------------------------------------------------------------------------------- /lib/tags.ts: -------------------------------------------------------------------------------- 1 | import type { MdxJsxFlowElement, MdxJsxTextElement } from 'mdast-util-mdx'; 2 | 3 | import { visit } from 'unist-util-visit'; 4 | 5 | import { isMDXElement } from '../processor/utils'; 6 | 7 | import mdast from './mdast'; 8 | 9 | const tags = (doc: string) => { 10 | const set = new Set(); 11 | 12 | visit(mdast(doc), isMDXElement, (node: MdxJsxFlowElement | MdxJsxTextElement) => { 13 | if (node.name?.match(/^[A-Z]/)) { 14 | set.add(node.name); 15 | } 16 | }); 17 | 18 | return Array.from(set); 19 | }; 20 | 21 | export default tags; 22 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /enums.ts: -------------------------------------------------------------------------------- 1 | export enum NodeTypes { 2 | callout = 'rdme-callout', 3 | codeTabs = 'code-tabs', 4 | embedBlock = 'embed-block', 5 | emoji = 'gemoji', 6 | figcaption = 'figcaption', 7 | figure = 'figure', 8 | glossary = 'readme-glossary-item', 9 | htmlBlock = 'html-block', 10 | i = 'i', 11 | imageBlock = 'image-block', 12 | plain = 'plain', 13 | recipe = 'recipe', 14 | reusableContent = 'reusable-content', 15 | tableau = 'tableau', 16 | /** @deprecated Deprecated in favor of `recipe`. */ 17 | tutorialTile = 'tutorial-tile', 18 | variable = 'readme-variable', 19 | } 20 | -------------------------------------------------------------------------------- /__tests__/lib/mdast/html-blocks/out.json: -------------------------------------------------------------------------------- 1 | { 2 | "children": [ 3 | { 4 | "children": [ 5 | { 6 | "type": "text", 7 | "value": "
\n  \n    RegexpExtract([Address], \"\\\\\\\\s*([a-zA-Z]+)\", 2)\n  \n
" 8 | } 9 | ], 10 | "data": { 11 | "hName": "html-block", 12 | "hProperties": { 13 | "html": "
\n  \n    RegexpExtract([Address], \"\\\\\\\\s*([a-zA-Z]+)\", 2)\n  \n
" 14 | } 15 | }, 16 | "type": "html-block" 17 | } 18 | ], 19 | "type": "root" 20 | } 21 | -------------------------------------------------------------------------------- /docs/images.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Images 3 | category: 4 | uri: uri-that-does-not-map-to-5fdf7610134322007389a6ed 5 | privacy: 6 | view: public 7 | --- 8 | ## Syntax 9 | ``` 10 | ![Alt text](https://cdn.path.to/some/image.jpg "This is some image...") 11 | ``` 12 | 13 | ## Examples 14 | 15 | ![Bro eats pizza and makes an OK gesture.](https://files.readme.io/6f52e22-man-eating-pizza-and-making-an-ok-gesture.jpg "Pizza Face") 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /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 | projects: [ 13 | { 14 | extends: true, 15 | test: { 16 | exclude: ['__tests__/browser'], 17 | name: 'rdmd', 18 | }, 19 | }, 20 | ], 21 | }, 22 | }); 23 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@readme/eslint-config', '@readme/eslint-config/react', '@readme/eslint-config/typescript'], 3 | root: true, 4 | rules: { 5 | '@typescript-eslint/no-var-requires': 'off', 6 | 'import/extensions': 'off', 7 | 'import/no-extraneous-dependencies': [ 8 | 'warn', 9 | { 10 | devDependencies: [ 11 | '**/*.spec.[tj]s', 12 | '**/*.test.[tj]s', 13 | '**/*.test.[tj]sx', 14 | '**/vitest.*.[tj]s', 15 | '**/webpack..*.js', 16 | './example/**', 17 | ], 18 | }, 19 | ], 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /processor/transform/migrate-html-blocks.ts: -------------------------------------------------------------------------------- 1 | import type { Transform } from 'mdast-util-from-markdown'; 2 | import type { HTMLBlock } from 'types'; 3 | 4 | import { visit } from 'unist-util-visit'; 5 | 6 | const MigrateHtmlBlocks = (): Transform => tree => { 7 | visit(tree, 'html-block', (node: HTMLBlock) => { 8 | const { html, runScripts } = node.data?.hProperties || {}; 9 | const escaped = html.replaceAll(/\\/g, '\\\\'); 10 | 11 | node.data.hProperties = { 12 | ...(runScripts && { runScripts }), 13 | html: escaped, 14 | }; 15 | }); 16 | 17 | return tree; 18 | }; 19 | 20 | export default MigrateHtmlBlocks; 21 | -------------------------------------------------------------------------------- /.github/workflows/bundlewatch.yml: -------------------------------------------------------------------------------- 1 | name: BundleWatch 2 | on: [push] 3 | 4 | jobs: 5 | check: 6 | name: Bundle Watch 7 | runs-on: ubuntu-latest 8 | if: "!contains(github.event.head_commit.message, 'SKIP CI')" 9 | steps: 10 | - uses: actions/checkout@v6 11 | - uses: actions/setup-node@v6 12 | with: 13 | node-version: 20.x 14 | 15 | - name: Update npm 16 | run: npm i -g npm@7 17 | 18 | - name: Install dependencies 19 | run: npm ci 20 | 21 | - name: Build dist files 22 | run: npm run build 23 | 24 | - name: Analyze Bundle 25 | run: npx bundlewatch 26 | -------------------------------------------------------------------------------- /__tests__/components/CodeTabs.test.tsx: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react'; 2 | import React from 'react'; 3 | 4 | import { execute } from '../helpers'; 5 | 6 | describe('CodeTabs', () => { 7 | it.skip('render _all_ its children', () => { 8 | const md = ` 9 | \`\`\` 10 | assert('theme', 'dark'); 11 | \`\`\` 12 | \`\`\` 13 | assert('theme', 'light'); 14 | \`\`\` 15 | `; 16 | const Component = execute(md); 17 | const { container } = render(); 18 | 19 | expect(container).toHaveTextContent("assert('theme', 'dark')"); 20 | expect(container).toHaveTextContent("assert('theme', 'light')"); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /lib/mdastV6.ts: -------------------------------------------------------------------------------- 1 | import type { Root } from 'mdast'; 2 | 3 | import migrationTransformers from '../processor/migration'; 4 | 5 | const migrationNormalize = (doc: string) => { 6 | return doc.replaceAll(/^($/gms, '$1-->'); 7 | }; 8 | 9 | const mdastV6 = (doc: string, { rdmd }): Root => { 10 | const [_normalizedDoc] = rdmd.setup(doc); 11 | const normalizedDoc = migrationNormalize(_normalizedDoc); 12 | 13 | const proc = rdmd.processor().use(migrationTransformers).data('rdmd', rdmd); 14 | 15 | const tree = proc.parse(normalizedDoc); 16 | proc.runSync(tree, normalizedDoc); 17 | 18 | return tree; 19 | }; 20 | 21 | export default mdastV6; 22 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: 'CodeQL' 2 | 3 | on: 4 | push: 5 | branches: [next] 6 | pull_request: 7 | branches: [next] 8 | schedule: 9 | - cron: '0 0 1 * *' 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | steps: 21 | - name: Checkout repository 22 | uses: actions/checkout@v6 23 | 24 | - name: Initialize CodeQL 25 | uses: github/codeql-action/init@v4 26 | 27 | - name: Perform CodeQL Analysis 28 | uses: github/codeql-action/analyze@v4 29 | -------------------------------------------------------------------------------- /components/TableOfContents/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function TableOfContents({ children, heading = 'Table of Contents' }: React.PropsWithChildren<{ heading?: string }>) { 4 | return ( 5 | 17 | ); 18 | } 19 | 20 | export default TableOfContents; 21 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG NODE_VERSION=22.13 2 | FROM node:${NODE_VERSION}-alpine 3 | 4 | ARG NODE_VERSION 5 | ENV NODE_VERSION=$NODE_VERSION 6 | 7 | ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true 8 | ENV PUPPETEER_EXECUTABLE_PATH /usr/bin/chromium-browser 9 | 10 | RUN apk update && apk add \ 11 | make \ 12 | font-noto-emoji \ 13 | font-roboto \ 14 | chromium 15 | 16 | RUN npm install -g npm@10.5 17 | 18 | ENV DOCKER_WORKSPACE=/markdown 19 | WORKDIR ${DOCKER_WORKSPACE} 20 | 21 | COPY package.json package-lock.json ./ 22 | RUN npm install 23 | 24 | COPY . ./ 25 | 26 | RUN mkdir -p __tests__/browser/__image_snapshots__/__diff_output__ 27 | 28 | EXPOSE 9966 29 | 30 | CMD ["test.browser"] 31 | ENTRYPOINT ["npm", "run"] 32 | -------------------------------------------------------------------------------- /__tests__/compilers/escape.test.js: -------------------------------------------------------------------------------- 1 | import { mdast, mdx, mdxish } from '../../index'; 2 | 3 | describe('escape compiler', () => { 4 | it('handles escapes', () => { 5 | const txt = '\\¶'; 6 | 7 | expect(mdx(mdast(txt))).toBe('\\¶\n'); 8 | }); 9 | }); 10 | 11 | describe('mdxish escape compiler', () => { 12 | it('handles escapes', () => { 13 | const txt = '\\¶'; 14 | 15 | const hast = mdxish(txt); 16 | const paragraph = hast.children[0]; 17 | 18 | expect(paragraph.type).toBe('element'); 19 | expect(paragraph.tagName).toBe('p'); 20 | expect(paragraph.children[0].type).toBe('text'); 21 | expect(paragraph.children[0].value).toBe('¶'); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /processor/transform/migrate-callouts.ts: -------------------------------------------------------------------------------- 1 | import type { Root } from 'mdast'; 2 | import type { Plugin, Transformer } from 'unified'; 3 | 4 | import { visit } from 'unist-util-visit'; 5 | 6 | import { wrapHeading } from './callouts'; 7 | 8 | const migrateCallouts: Plugin<[], Root> = (): Transformer => (tree: Root) => { 9 | visit(tree, 'rdme-callout', callout => { 10 | const firstChild = callout.children?.[0]; 11 | // This will retain the value of the node if it is not a paragraph, e.g. an HTML node 12 | if (firstChild && firstChild.type === 'paragraph') { 13 | callout.children[0] = wrapHeading(callout); 14 | } 15 | }); 16 | 17 | return tree; 18 | }; 19 | 20 | export default migrateCallouts; 21 | -------------------------------------------------------------------------------- /__tests__/migration/callouts.test.ts: -------------------------------------------------------------------------------- 1 | import { migrate } from '../helpers'; 2 | 3 | describe('migrating callouts', () => { 4 | it('does not error on callouts with no heading', () => { 5 | const md = '> ℹ️'; 6 | const mdx = migrate(md); 7 | expect(mdx).toMatchInlineSnapshot(` 8 | " 9 | " 10 | `); 11 | }); 12 | 13 | it('retains HTML content that starts a callout body', () => { 14 | const md = `> ⚠️
hello
15 | `; 16 | 17 | const mdx = migrate(md); 18 | expect(mdx).toMatchInlineSnapshot(` 19 | " 20 |
hello
21 |
22 | " 23 | `); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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-render', ...((node.properties.className as string[]) || [])], 15 | }; 16 | } 17 | }); 18 | 19 | return tree; 20 | }; 21 | 22 | export default mermaidTransformer; 23 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /components/Columns/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import './style.scss'; 4 | 5 | export const Column = ({ children }: React.PropsWithChildren) => { 6 | return
{children}
; 7 | }; 8 | 9 | interface Props extends React.PropsWithChildren<{ layout?: '1fr' | 'auto' | 'fixed' }> {} 10 | 11 | const Columns = ({ children, layout = 'auto' }: Props) => { 12 | // eslint-disable-next-line no-param-reassign 13 | layout = layout === 'fixed' ? '1fr' : 'auto'; 14 | const columnsCount = React.Children.count(children); 15 | 16 | return ( 17 |
18 | {children} 19 |
20 | ); 21 | }; 22 | 23 | export default Columns; 24 | -------------------------------------------------------------------------------- /processor/transform/migrate-link-references.ts: -------------------------------------------------------------------------------- 1 | import type { LinkReference, Parents, Root } from 'mdast'; 2 | import type { Plugin } from 'unified'; 3 | 4 | import { visit } from 'unist-util-visit'; 5 | 6 | import { NodeTypes } from '../../enums'; 7 | 8 | const migrateLinkReferences: Plugin<[{ rdmd: { md: (node: LinkReference) => string } }], Root> = ({ rdmd }) => { 9 | 10 | return (tree: Root) => { 11 | visit(tree, 'linkReference', (node: LinkReference, index: number, parent: Parents) => { 12 | if (!('children' in parent)) return; 13 | 14 | parent.children.splice(index, 1, { 15 | type: NodeTypes.plain, 16 | value: rdmd.md(node).trim(), 17 | }); 18 | }); 19 | }; 20 | }; 21 | 22 | export default migrateLinkReferences; 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020, ReadMe 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose 4 | with or without fee is hereby granted, provided that the above copyright notice 5 | and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 8 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 9 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 10 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS 11 | OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 12 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 13 | THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /example/styles/header.scss: -------------------------------------------------------------------------------- 1 | .Header { 2 | &-button, 3 | &-select { 4 | background: transparent; 5 | border: 1px solid transparent; 6 | border-radius: var(--border-radius); 7 | color: var(--color-text-default); 8 | cursor: pointer; 9 | font-family: var(--font-family); 10 | font-size: 13px; 11 | font-weight: var(--font-weight-normal); 12 | padding: var(--xs); 13 | position: relative; 14 | text-align: center; 15 | 16 | &:hover { 17 | background: rgba(var(--color-bg-page-rgb-inverse), 0.05); 18 | } 19 | 20 | &:active, 21 | &:focus { 22 | background: rgba(var(--color-bg-page-rgb-inverse), 0.1); 23 | outline: none; 24 | } 25 | } 26 | 27 | &-select { 28 | appearance: none; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /__tests__/lib/plain/custom-components.test.ts: -------------------------------------------------------------------------------- 1 | import { hast, plain } from '../../../index'; 2 | 3 | describe('plain compiler', () => { 4 | it('should include the title of Accordion', () => { 5 | const mdx = ` 6 | 7 | Body 8 | 9 | `; 10 | 11 | expect(plain(hast(mdx))).toContain('Title Body'); 12 | }); 13 | 14 | it('should include the title of Card', () => { 15 | const mdx = ` 16 | 17 | Body 18 | 19 | `; 20 | 21 | expect(plain(hast(mdx))).toContain('Title Body'); 22 | }); 23 | 24 | it('should include the title of Tab', () => { 25 | const mdx = ` 26 | 27 | Body 28 | 29 | `; 30 | 31 | expect(plain(hast(mdx))).toContain('Title Body'); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /example/Root.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { useSearchParams } from 'react-router-dom'; 3 | 4 | import Doc from './Doc'; 5 | import Form from './Form'; 6 | import Header from './Header'; 7 | 8 | const Root = () => { 9 | const [theme, setTheme] = useState<'dark' | 'light' | 'system'>('system'); 10 | const [searchParams] = useSearchParams(); 11 | const ci = searchParams.has('ci'); 12 | 13 | return ( 14 |
15 | {!ci &&
} 16 |
17 |
18 | {!ci &&
} 19 | 20 |
21 |
22 |
23 | ); 24 | }; 25 | 26 | export default Root; 27 | -------------------------------------------------------------------------------- /__tests__/lib/mdx.test.ts: -------------------------------------------------------------------------------- 1 | import { mdast, mdx } from '../../index'; 2 | 3 | describe('mdx serialization', () => { 4 | it('should not add indentation to JSX comment content when serializing', () => { 5 | const md = ` 6 | {/* 7 | 8 | ## Hey-o 9 | 10 | */} 11 | `; 12 | 13 | const tree = mdast(md, { missingComponents: 'ignore' }); 14 | const serialized = mdx(tree); 15 | 16 | // Extract the comment content line 17 | const commentMatch = serialized.match(/\{\/\*([\s\S]*?)\*\/\}/); 18 | const commentContent = commentMatch?.[1]; 19 | const contentLine = commentContent?.split('\n').find(line => line.includes('## Hey-o')); 20 | 21 | // Check that the line does NOT have leading spaces (indentation) 22 | expect(contentLine).not.toMatch(/^\s+/); 23 | }); 24 | }); 25 | 26 | -------------------------------------------------------------------------------- /__tests__/lib/hast.test.ts: -------------------------------------------------------------------------------- 1 | import { h } from 'hastscript'; 2 | 3 | import { hast } from '../../lib'; 4 | 5 | describe('hast transformer', () => { 6 | it('parses components into the tree', () => { 7 | const md = ` 8 | ## Test 9 | 10 | 11 | `; 12 | const components = { 13 | Example: "## It's coming from within the component!", 14 | }; 15 | 16 | const expected = h( 17 | undefined, 18 | h('h2', { id: 'test' }, 'Test'), 19 | '\n', 20 | h('h2', { id: 'its-coming-from-within-the-component' }, "It's coming from within the component!"), 21 | ); 22 | 23 | // @ts-expect-error - the custom matcher types are not being set up 24 | // correctly 25 | expect(hast(md, { components })).toStrictEqualExceptPosition(expected); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /__tests__/lib/tags.test.ts: -------------------------------------------------------------------------------- 1 | import { tags } from '../../lib'; 2 | 3 | describe('tags', () => { 4 | it('returns custom element names', () => { 5 | const mdx = ''; 6 | 7 | expect(tags(mdx)).toStrictEqual(['TagMe']); 8 | }); 9 | 10 | it('does not return html tags', () => { 11 | const mdx = '
'; 12 | 13 | expect(tags(mdx)).toStrictEqual([]); 14 | }); 15 | 16 | it('returns block and phrasing content', () => { 17 | const mdx = ` 18 | 19 | 20 | This is phrasing: 21 | `; 22 | 23 | expect(tags(mdx)).toStrictEqual(['Block', 'Inline']); 24 | }); 25 | 26 | it('returns a unique set of names', () => { 27 | const mdx = ` 28 | 29 | 30 | 31 | 32 | 33 | `; 34 | 35 | expect(tags(mdx)).toStrictEqual(['Block']); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /__tests__/lib/exports/index.test.ts: -------------------------------------------------------------------------------- 1 | import { exports } from '../../../lib'; 2 | 3 | import multipleExportsMdx from './input/multipleExports.mdx?raw'; 4 | import singleExportMdx from './input/singleExport.mdx?raw'; 5 | import weirdExportsMdx from './input/weirdExports.mdx?raw'; 6 | 7 | 8 | describe('export tags', () => { 9 | it('returns a single export name', () => { 10 | 11 | expect(exports(singleExportMdx)).toStrictEqual(['Foo']); 12 | }); 13 | 14 | it('returns multiple export names', () => { 15 | 16 | expect(exports(multipleExportsMdx)).toStrictEqual(['Foo', 'Bar']); 17 | }); 18 | 19 | it('returns different types of export names', () => { 20 | 21 | expect(exports(weirdExportsMdx)).toStrictEqual(['Foo', 'bar', 'doSomethingFunction', 'YELLING', 'SingleNewlinesAreAnnoying', 'x', 'MyClass']); 22 | }); 23 | }); -------------------------------------------------------------------------------- /index.tsx: -------------------------------------------------------------------------------- 1 | import * as Components from './components'; 2 | import { getHref } from './components/Anchor'; 3 | import { options } from './options'; 4 | import './styles/main.scss'; 5 | 6 | const utils = { 7 | get options() { 8 | return { ...options }; 9 | }, 10 | 11 | getHref, 12 | calloutIcons: {}, 13 | }; 14 | 15 | export { 16 | compile, 17 | exports, 18 | hast, 19 | run, 20 | mdast, 21 | mdastV6, 22 | mdx, 23 | mdxish, 24 | mdxishTags, 25 | migrate, 26 | mix, 27 | plain, 28 | renderMdxish, 29 | remarkPlugins, 30 | stripComments, 31 | tags, 32 | } from './lib'; 33 | export { default as Owlmoji } from './lib/owlmoji'; 34 | export { Components, utils }; 35 | export { tailwindCompiler } from './utils/tailwind-compiler'; 36 | export { regex as gemojiRegex } from './processor/transform/gemoji+'; 37 | -------------------------------------------------------------------------------- /__tests__/fixtures/image-tests.mdx: -------------------------------------------------------------------------------- 1 | We're excited you're here! :blue_heart: 2 | 3 | 4 | Owlbert! 5 | 6 | 7 | 8 | 9 | 10 | 16 | 17 | 18 | 19 | 20 | 26 | 27 | 28 | 34 | 35 |
11 | 12 | 13 | 14 | 15 |
21 | 22 | 23 | 24 | 25 |
29 | 30 | 31 | 32 | 33 |
36 | -------------------------------------------------------------------------------- /lib/hast.ts: -------------------------------------------------------------------------------- 1 | import type { MdastOpts } from './ast-processor'; 2 | import type { MdastComponents } from '../types'; 3 | 4 | import remarkRehype from 'remark-rehype'; 5 | 6 | import { injectComponents, mdxToHast } from '../processor/transform'; 7 | 8 | import astProcessor, { rehypePlugins } from './ast-processor'; 9 | import mdast from './mdast'; 10 | 11 | const hast = (text: string, opts: MdastOpts = {}) => { 12 | const components: MdastComponents = Object.entries(opts.components || {}).reduce((memo, [name, doc]) => { 13 | memo[name] = mdast(doc); 14 | return memo; 15 | }, {}); 16 | 17 | const processor = astProcessor(opts) 18 | .use(injectComponents({ components })) 19 | .use(mdxToHast) 20 | .use(remarkRehype) 21 | .use(rehypePlugins); 22 | 23 | return processor.runSync(processor.parse(text)); 24 | }; 25 | export default hast; 26 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: 'browser', 3 | globalSetup: 'jest-environment-puppeteer/setup', 4 | globalTeardown: 'jest-environment-puppeteer/teardown', 5 | moduleNameMapper: { 6 | '.+\\.scss$': 'identity-obj-proxy', 7 | }, 8 | modulePathIgnorePatterns: ['/__tests__/helpers'], 9 | setupFilesAfterEnv: ['/__tests__/browser/setup.js'], 10 | testEnvironment: 'jest-environment-puppeteer', 11 | testMatch: ['**/__tests__/browser/**/*.test.[jt]s?(x)'], 12 | transformIgnorePatterns: [ 13 | // Since `@readme/variable` doesn't ship any transpiled code, we need to transform it as we're running tests. 14 | '/node_modules/@readme/variable/^.+\\.jsx?$', 15 | // wat 16 | '/node_modules/@babel', 17 | '/node_modules/@jest', 18 | '/node_modules/jest', 19 | ], 20 | }; 21 | -------------------------------------------------------------------------------- /__tests__/matchers.ts: -------------------------------------------------------------------------------- 1 | import type { ExpectationResult } from '@vitest/expect'; 2 | import type { Root, Node } from 'mdast'; 3 | 4 | import { map } from 'unist-util-map'; 5 | 6 | import { expect } from 'vitest'; 7 | 8 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 9 | const removePosition = ({ position, ...node }: Node) => node; 10 | 11 | function toStrictEqualExceptPosition(received: Root, expected: Root): ExpectationResult { 12 | const { equals } = this; 13 | const receivedTrimmed = map(received, removePosition); 14 | const expectedTrimmed = map(expected, removePosition); 15 | 16 | return { 17 | pass: equals(receivedTrimmed, expectedTrimmed), 18 | message: () => 'Expected two trees to be equal!', 19 | actual: receivedTrimmed, 20 | expected: expectedTrimmed, 21 | }; 22 | } 23 | 24 | expect.extend({ toStrictEqualExceptPosition }); 25 | -------------------------------------------------------------------------------- /__tests__/helpers.ts: -------------------------------------------------------------------------------- 1 | import * as rdmd from '@readme/markdown-legacy'; 2 | 3 | import { vi } from 'vitest'; 4 | 5 | import { run, compile, migrate as baseMigrate } from '../index'; 6 | 7 | export const silenceConsole = 8 | (prop: keyof Console = 'error', impl = () => {}) => 9 | fn => { 10 | const spy: ReturnType = vi.spyOn(console, prop); 11 | 12 | try { 13 | spy.mockImplementation(impl); 14 | 15 | return fn(spy); 16 | } finally { 17 | spy?.mockRestore(); 18 | } 19 | }; 20 | 21 | export const execute = (doc: string, compileOpts = {}, runOpts = {}, { getDefault = true } = {}) => { 22 | const code = compile(doc, compileOpts); 23 | const mod = run(code, runOpts); 24 | 25 | return getDefault ? mod.default : mod; 26 | }; 27 | 28 | export const migrate = (doc: string) => { 29 | return baseMigrate(doc, { rdmd }); 30 | }; 31 | -------------------------------------------------------------------------------- /processor/compile/callout.ts: -------------------------------------------------------------------------------- 1 | import type { Callout } from '../../types'; 2 | 3 | import { NodeTypes } from '../../enums'; 4 | 5 | const callout = (node: Callout, _, state, info) => { 6 | const exit = state.enter(NodeTypes.callout); 7 | const tracker = state.createTracker(info); 8 | 9 | tracker.move('> '); 10 | tracker.shift(2); 11 | 12 | // @note: compatability 13 | if (node.data.hProperties.title === '') { 14 | node.children.unshift({ type: 'paragraph', children: [{ type: 'text', value: '' }] }); 15 | } 16 | 17 | const map = (line: string, index: number, blank: boolean) => { 18 | return `>${index === 0 ? ` ${node.data.hProperties.icon}` : ''}${blank ? '' : ' '}${line}`; 19 | }; 20 | 21 | const value = state.indentLines(state.containerFlow(node, tracker.current()), map); 22 | exit(); 23 | 24 | return value; 25 | }; 26 | 27 | export default callout; 28 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /errors/mdx-syntax-error.ts: -------------------------------------------------------------------------------- 1 | import type { VFileMessage } from 'vfile-message'; 2 | 3 | export default class MdxSyntaxError extends SyntaxError { 4 | original: VFileMessage = null; 5 | 6 | constructor(error: VFileMessage, doc: string) { 7 | const { message, line, column, url } = error; 8 | 9 | const messages = [ 10 | `Oh no! We ran into a syntax error at { line: ${line}, column: ${column} }, please see this url for more details: ${url}`, 11 | ]; 12 | 13 | if (typeof line !== 'undefined') { 14 | messages.push(doc.split('\n')[line - 1]); 15 | 16 | if (typeof column !== 'undefined') { 17 | const prefix = new Array(column).map(() => '').join(' '); 18 | messages.push(`${prefix}↑ ${message}`); 19 | } 20 | } 21 | 22 | super(messages.join('\n')); 23 | 24 | this.original = error; 25 | this.name = 'MdxSyntaxError'; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /components/Accordion/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | import './style.scss'; 4 | 5 | interface Props extends React.PropsWithChildren<{ icon?: string; iconColor?: string; title: string }> {} 6 | 7 | const Accordion = ({ children, icon, iconColor, title }: Props) => { 8 | const [isOpen, setIsOpen] = useState(false); 9 | 10 | return ( 11 |
setIsOpen(!isOpen)}> 12 | 13 | 14 | {icon && } 15 | {title} 16 | 17 |
{children}
18 |
19 | ); 20 | }; 21 | 22 | export default Accordion; 23 | 24 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /__tests__/components/Callout.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import React from 'react'; 3 | 4 | import Callout from '../../components/Callout'; 5 | 6 | describe('Callout', () => { 7 | it('render _all_ its children', () => { 8 | render( 9 | 10 |

Title

11 |

First Paragraph

12 |

Second Paragraph

13 |
, 14 | ); 15 | 16 | expect(screen.getByText('Second Paragraph')).toBeVisible(); 17 | }); 18 | 19 | it("doesn't render all its children if it's **empty**", () => { 20 | render( 21 | 22 |

Title

23 |

First Paragraph

24 |

Second Paragraph

25 |
, 26 | ); 27 | 28 | expect(screen.queryByText('Title')).toBeNull(); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /example/styles/mixins/dark-mode.scss: -------------------------------------------------------------------------------- 1 | @mixin dark-mode($global: false) { 2 | $root: &; 3 | 4 | @if not $root { 5 | [data-color-mode='dark'] { 6 | @content; 7 | } 8 | 9 | [data-color-mode='auto'], 10 | [data-color-mode='system'] { 11 | @media (prefers-color-scheme: dark) { 12 | @content; 13 | } 14 | } 15 | } @else if $global { 16 | :global([data-color-mode='dark']) & { 17 | @content; 18 | } 19 | 20 | :global([data-color-mode='auto']) &, 21 | :global([data-color-mode='system']) & { 22 | @media (prefers-color-scheme: dark) { 23 | @content; 24 | } 25 | } 26 | } @else { 27 | [data-color-mode='dark'] & { 28 | @content; 29 | } 30 | 31 | [data-color-mode='auto'] &, 32 | [data-color-mode='system'] & { 33 | @media (prefers-color-scheme: dark) { 34 | @content; 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /__tests__/link-parsers.test.js: -------------------------------------------------------------------------------- 1 | import { mdast } from '../index'; 2 | 3 | test.skip('a link with label', () => { 4 | expect(mdast('[link](http://www.foo.com)')).toMatchSnapshot(); 5 | }); 6 | 7 | test.skip('a link with no url', () => { 8 | expect(mdast('[link]()')).toMatchSnapshot(); 9 | }); 10 | 11 | test.skip('a link ref', () => { 12 | expect(mdast('[link]')).toMatchSnapshot(); 13 | }); 14 | 15 | test.skip('a link ref with reference', () => { 16 | expect(mdast('[link]\n\n[link]: www.example.com')).toMatchSnapshot(); 17 | }); 18 | 19 | test.skip('a bracketed autoLinked url', () => { 20 | expect(mdast('')).toMatchSnapshot(); 21 | }); 22 | 23 | test.skip('a bare autoLinked url', () => { 24 | expect(mdast('http://www.googl.com')).toMatchSnapshot(); 25 | }); 26 | 27 | test.skip('a bare autoLinked url with no protocol', () => { 28 | expect(mdast('www.google.com')).toMatchSnapshot(); 29 | }); 30 | -------------------------------------------------------------------------------- /__tests__/table-flattening/index.test.js: -------------------------------------------------------------------------------- 1 | import { astToPlainText, hast } from '../../index'; 2 | 3 | describe.skip('astToPlainText with tables', () => { 4 | it('includes all cells', () => { 5 | const text = ` 6 | | Col. A | Col. B | Col. C | 7 | |:-------:|:-------:|:-------:| 8 | | Cell A1 | Cell B1 | Cell C1 | 9 | | Cell A2 | Cell B2 | Cell C2 | 10 | | Cell A3 | Cell B3 | Cell C3 |`; 11 | 12 | expect(astToPlainText(hast(text))).toMatchInlineSnapshot( 13 | '"Col. A Col. B Col. C Cell A1 Cell B1 Cell C1 Cell A2 Cell B2 Cell C2 Cell A3 Cell B3 Cell C3"', 14 | ); 15 | }); 16 | 17 | it('includes formatted text', () => { 18 | const text = ` 19 | | *Col. A* | Col. *B* | 20 | |:---------:|:---------:| 21 | | Cell *A1* | *Cell B1* | 22 | | *Cell* A2 | *Cell* B2 |`; 23 | 24 | expect(astToPlainText(hast(text))).toMatchInlineSnapshot('"Col. A Col. B Cell A1 Cell B1 Cell A2 Cell B2"'); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /lib/mdxishTags.ts: -------------------------------------------------------------------------------- 1 | import type { MdxJsxFlowElement, MdxJsxTextElement } from 'mdast-util-mdx'; 2 | 3 | import { remark } from 'remark'; 4 | import { visit } from 'unist-util-visit'; 5 | 6 | import mdxishComponentBlocks from '../processor/transform/mdxish/mdxish-component-blocks'; 7 | import { isMDXElement } from '../processor/utils'; 8 | 9 | import { extractMagicBlocks } from './utils/extractMagicBlocks'; 10 | 11 | const tags = (doc: string) => { 12 | const { replaced: sanitizedDoc } = extractMagicBlocks(doc); 13 | const set = new Set(); 14 | const processor = remark() 15 | .use(mdxishComponentBlocks); 16 | const tree = processor.parse(sanitizedDoc); 17 | 18 | visit(processor.runSync(tree), isMDXElement, (node: MdxJsxFlowElement | MdxJsxTextElement) => { 19 | if (node.name?.match(/^[A-Z]/)) { 20 | set.add(node.name); 21 | } 22 | }); 23 | 24 | return Array.from(set); 25 | }; 26 | 27 | export default tags; 28 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /__tests__/react.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen, waitFor } from '@testing-library/react'; 2 | import userEvent from '@testing-library/user-event'; 3 | import React from 'react'; 4 | 5 | import { execute } from './helpers'; 6 | 7 | describe('import React', () => { 8 | it('allows importing react', async () => { 9 | const mdx = ` 10 | import { useState } from 'react'; 11 | 12 | export default function Counter() { 13 | const [count, setCount] = useState(0) 14 | 15 | return ( 16 |
17 |

You clicked {count} times!

18 | 21 |
22 | ) 23 | } 24 | 25 | 26 | `; 27 | 28 | const Content = execute(mdx); 29 | render(); 30 | 31 | expect(screen.getByText('You clicked 0 times!')).toBeVisible(); 32 | userEvent.click(screen.getByRole('button')); 33 | 34 | await waitFor(() => screen.getByText('You clicked 1 times!')); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /processor/transform/validate-mcpintro.ts: -------------------------------------------------------------------------------- 1 | import type { Transform } from 'mdast-util-from-markdown'; 2 | import type { MdxJsxFlowElement, MdxJsxTextElement } from 'mdast-util-mdx'; 3 | 4 | import { visit } from 'unist-util-visit'; 5 | 6 | import { isMDXElement } from '../utils'; 7 | 8 | /** 9 | * Validates that MCPIntro components have a required url attribute. 10 | * Throws an error during compilation if the attribute is missing. 11 | */ 12 | const validateMCPIntro = (): Transform => tree => { 13 | visit(tree, isMDXElement, (node: MdxJsxFlowElement | MdxJsxTextElement) => { 14 | if (node.name !== 'MCPIntro') return; 15 | 16 | const hasUrlAttribute = node.attributes?.some(attr => 'name' in attr && attr.name === 'url'); 17 | 18 | if (!hasUrlAttribute) { 19 | throw new Error( 20 | 'MCPIntro component requires a "url" attribute. Use the component menu in the editor to insert it correctly.', 21 | ); 22 | } 23 | }); 24 | }; 25 | 26 | export default validateMCPIntro; 27 | -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | export type { MdastOpts } from './ast-processor'; 2 | 3 | export { default as astProcessor, remarkPlugins } from './ast-processor'; 4 | export { default as compile } from './compile'; 5 | export { default as exports } from './exports'; 6 | export { default as hast } from './hast'; 7 | export { default as mdast } from './mdast'; 8 | export { default as mdastV6 } from './mdastV6'; 9 | export { default as mdx } from './mdx'; 10 | export { default as mix } from './mix'; 11 | export { default as mdxish } from './mdxish'; 12 | export type { MdxishOpts } from './mdxish'; 13 | export { default as migrate } from './migrate'; 14 | export { default as plain } from './plain'; 15 | export { default as renderMdxish } from './renderMdxish'; 16 | export type { RenderMdxishOpts } from './renderMdxish'; 17 | export { default as run } from './run'; 18 | export { default as tags } from './tags'; 19 | export { default as mdxishTags } from './mdxishTags'; 20 | export { default as stripComments } from './stripComments'; 21 | -------------------------------------------------------------------------------- /__tests__/compilers/yaml.test.js: -------------------------------------------------------------------------------- 1 | import { mdast, mdx, mix } from '../../index'; 2 | 3 | describe('yaml compiler', () => { 4 | it.skip('correctly writes out yaml', () => { 5 | const txt = ` 6 | --- 7 | title: This is test 8 | author: A frontmatter test 9 | --- 10 | 11 | Document content! 12 | `; 13 | 14 | expect(mdx(mdast(txt))).toBe(`--- 15 | title: This is test 16 | author: A frontmatter test 17 | --- 18 | 19 | Document content! 20 | `); 21 | }); 22 | }); 23 | 24 | describe('mix yaml compiler', () => { 25 | it('correctly handles yaml frontmatter', () => { 26 | // NOTE: the '---' MUST be at the ABSOLUTE BEGINNING of the file, adding a space or newline will break the parser 27 | const txt = `--- 28 | title: This is test 29 | author: A frontmatter test 30 | --- 31 | 32 | Document content! 33 | `; 34 | 35 | const html = mix(txt); 36 | expect(html).not.toContain('---'); 37 | expect(html).not.toContain('title: This is test'); 38 | expect(html).toContain('Document content'); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /components/Heading/style.scss: -------------------------------------------------------------------------------- 1 | .heading.heading { 2 | display: flex; 3 | justify-content: flex-start; 4 | align-items: center; 5 | position: relative; 6 | .heading-text { 7 | flex: 1 100%; 8 | } 9 | .heading-anchor-deprecated { 10 | position: absolute; 11 | top: 0; 12 | } 13 | .heading-anchor { 14 | top: -1rem !important; 15 | } 16 | .heading-anchor, 17 | .heading-anchor-icon { 18 | position: absolute !important; 19 | display: inline !important; 20 | order: -1; 21 | right: 100%; 22 | top: unset !important; 23 | margin-right: -0.8rem; 24 | padding: 0.8rem 0.2rem 0.8rem 0 !important; 25 | font-size: 0.8rem !important; 26 | text-decoration: none; 27 | color: inherit; 28 | transform: translateX(-100%); 29 | transition: 0.2s ease; 30 | &:hover { 31 | opacity: 1; 32 | } 33 | } 34 | &:not(:hover) .heading-anchor-icon { 35 | opacity: 0; 36 | } 37 | } 38 | 39 | .callout .heading.heading .heading-anchor-icon { 40 | display: none !important; 41 | } 42 | 43 | -------------------------------------------------------------------------------- /lib/registerCustomComponents.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign 2 | */ 3 | const kebabCase = require('lodash.kebabcase'); 4 | 5 | const registerCustomComponents = (components, sanitize, prefix = 'x') => 6 | Object.entries(components).reduce((all, [tag, component]) => { 7 | /* Sanitize + prefix element names. 8 | */ 9 | tag = kebabCase(tag); 10 | const isValidOverride = sanitize.tagNames.includes(tag); 11 | const isValidElemName = tag.includes('-'); 12 | if (!(isValidElemName || isValidOverride)) tag = `${prefix}-${tag}`; 13 | 14 | /* Safelist custom tag names. 15 | */ 16 | sanitize.tagNames.push(tag); 17 | 18 | /* Safelist allowed attributes. 19 | */ 20 | if (component.propTypes) 21 | sanitize.attributes[tag] = [].concat(sanitize.attributes[tag], Object.keys(component.propTypes)).filter(Boolean); 22 | 23 | /* Add element to custom component store. 24 | */ 25 | all[tag] = component; 26 | 27 | return all; 28 | }, {}); 29 | 30 | module.exports = registerCustomComponents; 31 | -------------------------------------------------------------------------------- /__tests__/variables/index.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import React from 'react'; 3 | 4 | import { execute } from '../helpers'; 5 | 6 | describe('variables', () => { 7 | it('renders a variable', () => { 8 | const md = '{user.name}'; 9 | const Content = execute(md, {}, { variables: { user: { name: 'Testing' } } }); 10 | 11 | render(); 12 | 13 | expect(screen.getByText('Testing')).toBeVisible(); 14 | }); 15 | 16 | it('renders a default value', () => { 17 | const md = '{user.name}'; 18 | const Content = execute(md); 19 | 20 | render(); 21 | 22 | expect(screen.getByText('NAME')).toBeVisible(); 23 | }); 24 | 25 | it('supports user variables in ESM', () => { 26 | const md = ` 27 | export const Hello = () =>

{user.name}

; 28 | 29 | 30 | `; 31 | const Content = execute(md, {}, { variables: { user: { name: 'Owlbert' } } }); 32 | 33 | render(); 34 | 35 | expect(screen.getByText('Owlbert')).toBeVisible(); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /__tests__/lib/owlmoji.test.ts: -------------------------------------------------------------------------------- 1 | import { nameToEmoji } from 'gemoji'; 2 | 3 | import Owlmoji from '../../lib/owlmoji'; 4 | 5 | describe('Owlmoji', () => { 6 | describe('kind', () => { 7 | it('returns "gemoji" for a gemoji name', () => { 8 | expect(Owlmoji.kind('smile')).toBe('gemoji'); 9 | }); 10 | 11 | it('returns "fontawesome" for a fa- name', () => { 12 | expect(Owlmoji.kind('fa-owl')).toBe('fontawesome'); 13 | }); 14 | 15 | it('returns "owlmoji" for an owlmoji name', () => { 16 | expect(Owlmoji.kind('owlbert')).toBe('owlmoji'); 17 | expect(Owlmoji.kind('owlbert-books')).toBe('owlmoji'); 18 | }); 19 | 20 | it('returns null for an unknown name', () => { 21 | expect(Owlmoji.kind('notarealmoji')).toBeNull(); 22 | }); 23 | }); 24 | 25 | it('exposes nameToEmoji from gemoji', () => { 26 | expect(Owlmoji.nameToEmoji).toBe(nameToEmoji); 27 | expect(Owlmoji.nameToEmoji.smile).toBe('😄'); 28 | }); 29 | 30 | it('owlmoji collection matches the snapshot', () => { 31 | expect(Owlmoji.owlmoji).toMatchSnapshot(); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /components/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Accordion } from './Accordion'; 2 | export { default as Anchor } from './Anchor'; 3 | export { default as Callout } from './Callout'; 4 | export { default as Cards, Card } from './Cards'; 5 | export { default as Code } from './Code'; 6 | export { default as CodeTabs } from './CodeTabs'; 7 | export { default as Columns, Column } from './Columns'; 8 | export { default as Embed } from './Embed'; 9 | export { default as Glossary } from './Glossary'; 10 | export { default as HTMLBlock } from './HTMLBlock'; 11 | export { default as Heading } from './Heading'; 12 | export { default as Image } from './Image'; 13 | export { default as MCPIntro } from './MCPIntro'; 14 | export { default as Table } from './Table'; 15 | export { default as TableOfContents } from './TableOfContents'; 16 | export { default as Tabs, Tab } from './Tabs'; 17 | export { default as TailwindRoot } from './TailwindRoot'; 18 | export { default as TailwindStyle } from './TailwindStyle'; 19 | export { default as Recipe } from './Recipe'; 20 | export { default as PostmanRunButton } from './PostmanRunButton'; 21 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | function isWebTarget(caller) { 2 | return Boolean(caller && caller.target === 'web'); 3 | } 4 | 5 | function isWebpack(caller) { 6 | return Boolean(caller && caller.name === 'babel-loader'); 7 | } 8 | 9 | module.exports = api => { 10 | const web = api.caller(isWebTarget); 11 | const webpack = api.caller(isWebpack); 12 | 13 | return { 14 | presets: [ 15 | [ 16 | '@babel/preset-env', 17 | { 18 | useBuiltIns: web ? 'usage' : undefined, 19 | corejs: web ? 2 : false, 20 | targets: !web ? { node: 'current' } : undefined, 21 | modules: webpack ? false : 'commonjs', 22 | }, 23 | ], 24 | '@babel/preset-react', 25 | '@babel/preset-typescript', 26 | ], 27 | plugins: [ 28 | '@babel/plugin-proposal-class-properties', 29 | '@babel/plugin-proposal-export-default-from', 30 | '@babel/plugin-proposal-object-rest-spread', 31 | '@babel/plugin-proposal-optional-chaining', 32 | '@babel/plugin-proposal-private-methods', 33 | ], 34 | sourceType: 'unambiguous', 35 | }; 36 | }; 37 | -------------------------------------------------------------------------------- /example/RenderError.tsx: -------------------------------------------------------------------------------- 1 | import type { PropsWithChildren } from 'react'; 2 | 3 | import React from 'react'; 4 | 5 | interface Props { 6 | error?: string; 7 | } 8 | 9 | interface State { 10 | hasError: boolean; 11 | message?: string; 12 | } 13 | 14 | class RenderError extends React.Component, State> { 15 | state = { hasError: false, message: null }; 16 | 17 | static getDerivedStateFromError(error: Error) { 18 | return { hasError: true, message: `${error.message}${error.stack}` }; 19 | } 20 | 21 | static componentDidCatch(error: Error, info: { componentStack: string }) { 22 | // eslint-disable-next-line no-console 23 | console.error(error, info.componentStack); 24 | } 25 | 26 | render() { 27 | const { children, error } = this.props; 28 | const { hasError, message } = this.state; 29 | 30 | return hasError || error ? ( 31 |
32 |
33 |           {message || error}
34 |         
35 |
36 | ) : ( 37 | children 38 | ); 39 | } 40 | } 41 | 42 | export default RenderError; 43 | -------------------------------------------------------------------------------- /__tests__/lib/mdxishTags.test.ts: -------------------------------------------------------------------------------- 1 | import { mdxishTags } from '../../lib'; 2 | 3 | describe('mdxishTags', () => { 4 | it('returns custom element names', () => { 5 | const mdx = ''; 6 | 7 | expect(mdxishTags(mdx)).toStrictEqual(['TagMe']); 8 | }); 9 | 10 | it('does not return html tags', () => { 11 | const mdx = '
'; 12 | 13 | expect(mdxishTags(mdx)).toStrictEqual([]); 14 | }); 15 | 16 | it('returns block and phrasing content', () => { 17 | const mdx = ` 18 | 19 | 20 | This is phrasing: 21 | `; 22 | 23 | expect(mdxishTags(mdx)).toStrictEqual(['Block', 'Inline']); 24 | }); 25 | 26 | it('returns a unique set of names', () => { 27 | const mdx = ` 28 | 29 | 30 | 31 | 32 | 33 | `; 34 | 35 | expect(mdxishTags(mdx)).toStrictEqual(['Block']); 36 | }); 37 | 38 | it('ignores magic blocks', () => { 39 | const mdx = ` 40 | [block:html] 41 | { 42 | "html": "" 43 | } 44 | [/block] 45 | 46 | 47 | `; 48 | 49 | expect(mdxishTags(mdx)).toStrictEqual(['Component']); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /contexts/index.tsx: -------------------------------------------------------------------------------- 1 | import type { RunOpts } from '../lib/run'; 2 | 3 | import { VariablesContext } from '@readme/variable'; 4 | import React from 'react'; 5 | 6 | import BaseUrlContext from './BaseUrl'; 7 | import CodeOptsContext from './CodeOpts'; 8 | import GlossaryContext from './GlossaryTerms'; 9 | import ThemeContext from './Theme'; 10 | 11 | type Props = Pick & React.PropsWithChildren; 12 | 13 | const compose = ( 14 | children: React.ReactNode, 15 | ...contexts: [React.Context, unknown][] 16 | ) => { 17 | return contexts.reduce((content, [Context, value]) => { 18 | return {content}; 19 | }, children); 20 | }; 21 | 22 | const Contexts = ({ children, terms = [], variables = { user: {}, defaults: [] }, baseUrl = '/', theme, copyButtons }: Props) => { 23 | return compose(children, [GlossaryContext, terms], [VariablesContext, variables], [BaseUrlContext, baseUrl], [ThemeContext, theme], [CodeOptsContext, copyButtons]); 24 | }; 25 | 26 | export default Contexts; 27 | -------------------------------------------------------------------------------- /components/Tabs/style.scss: -------------------------------------------------------------------------------- 1 | .TabGroup { 2 | &-nav { 3 | border-bottom: solid var(--color-border-default, #{rgba(black, 0.1)}); 4 | margin-bottom: 15px; 5 | } 6 | 7 | &-tab { 8 | background: none; 9 | border: 0; 10 | color: var(--color-text-minimum, #637288); 11 | cursor: pointer; 12 | font-size: 15px; 13 | font-weight: 600; 14 | margin-right: 15px; 15 | padding: 0; 16 | padding-bottom: 10px; 17 | 18 | &_active { 19 | background: none; 20 | border: 0; 21 | border-bottom: solid var(--project-color-primary, var(--color-text-default, #384248)); 22 | color: var(--project-color-primary, var(--color-text-default, #384248)); 23 | font-size: 15px; 24 | font-weight: 600; 25 | margin-right: 15px; 26 | margin-bottom: -2px; 27 | padding: 0; 28 | padding-bottom: 10px; 29 | } 30 | 31 | &:hover { 32 | color: var(--color-text-muted, #4f5a66); 33 | } 34 | } 35 | 36 | &-icon { 37 | color: var(--project-color-primary, inherit); 38 | margin-right: 10px; 39 | } 40 | 41 | .TabContent { 42 | color: var(--color-text-muted, #4f5a66); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /components/Heading/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export type Depth = 1 | 2 | 3 | 4 | 5 | 6; 4 | 5 | interface Props extends React.PropsWithChildren> { 6 | depth: Depth; 7 | tag: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'; 8 | } 9 | 10 | const Heading = ({ tag: Tag = 'h3', depth = 3, id, children, ...attrs }: Props) => { 11 | if (!children) return ''; 12 | 13 | return ( 14 | 15 |
16 |
17 | {children} 18 |
19 | 25 | 26 | ); 27 | }; 28 | 29 | const CreateHeading = (depth: Depth) => { 30 | const HeadingWithDepth = (props: Props) => ; 31 | 32 | return HeadingWithDepth; 33 | }; 34 | 35 | export default CreateHeading; 36 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /components/PostmanRunButton/readme.md: -------------------------------------------------------------------------------- 1 | # Postman Run Button 2 | 3 | Add a Postman Run Button to your documentation, allowing users to fork a Postman collection. This is duped from [the version in our Marketplace](https://github.com/readmeio/marketplace/tree/main/components/PostmanRunButton) so our users can easily insert it via the editor. 4 | 5 | ### Usage 6 | 7 | ```mdx 8 | 12 | ``` 13 | 14 | ### How to Find Your Collection ID and URL 15 | 16 | 1. Open your collection in Postman 17 | 2. Click "Share" button at the top right of your collection 18 | 3. Go to the "Via API" tab 19 | 4. You'll find your Collection ID in the URL or in the API response 20 | 5. The Collection URL contains parameters after the main URL (starting with "entityId=") 21 | 22 | ### Props 23 | 24 | - `collectionId` (required): The ID of your Postman collection (e.g., "123456-abcd-efgh-ijkl") 25 | - `collectionUrl` (required): The URL parameters for your collection, typically in this format: 26 | `entityId=YOUR_COLLECTION_ID&entityType=collection&workspaceId=YOUR_WORKSPACE_ID` 27 | -------------------------------------------------------------------------------- /example/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface HeaderProps { 4 | setTheme: (theme: 'dark' | 'light' | 'system') => void; 5 | theme: 'dark' | 'light' | 'system'; 6 | } 7 | 8 | function Header({ theme, setTheme }: HeaderProps) { 9 | return ( 10 |
11 |
12 | 13 | @readme/rmdx 14 | 15 |

16 | @readme/rmdx 17 |

18 | 19 | Docs 20 | 21 | 26 |
27 |
28 | ); 29 | } 30 | 31 | export default Header; 32 | -------------------------------------------------------------------------------- /__tests__/parsers/__snapshots__/escape.test.js.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`Escape > uses the "escape" type 1`] = ` 4 | { 5 | "children": [ 6 | { 7 | "children": [ 8 | { 9 | "position": { 10 | "end": { 11 | "column": 8, 12 | "line": 1, 13 | "offset": 7, 14 | }, 15 | "start": { 16 | "column": 1, 17 | "line": 1, 18 | "offset": 0, 19 | }, 20 | }, 21 | "type": "text", 22 | "value": "¶", 23 | }, 24 | ], 25 | "position": { 26 | "end": { 27 | "column": 8, 28 | "line": 1, 29 | "offset": 7, 30 | }, 31 | "start": { 32 | "column": 1, 33 | "line": 1, 34 | "offset": 0, 35 | }, 36 | }, 37 | "type": "paragraph", 38 | }, 39 | ], 40 | "position": { 41 | "end": { 42 | "column": 8, 43 | "line": 1, 44 | "offset": 7, 45 | }, 46 | "start": { 47 | "column": 1, 48 | "line": 1, 49 | "offset": 0, 50 | }, 51 | }, 52 | "type": "root", 53 | } 54 | `; 55 | -------------------------------------------------------------------------------- /components/Recipe.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | // We render a placeholder in this library, as the actual implemenation is 4 | // deeply tied to the main app 5 | const Recipe = () => { 6 | const style = { 7 | height: '50px', 8 | border: '1px solid var(--color-border-default, rgba(black, 0.1))', 9 | borderRadius: 'var(--border-radius-lg, 7.5px)', 10 | minWidth: '230px', 11 | display: 'inline-flex', 12 | padding: '10px', 13 | }; 14 | 15 | const placeholderStyle = { 16 | borderRadius: 'var(--border-radius-lg, 7.5px)', 17 | backgroundColor: 'var(--color-skeleton, #384248)', 18 | }; 19 | 20 | const avatarStyle = { 21 | ...placeholderStyle, 22 | height: '30px', 23 | width: '30px', 24 | }; 25 | 26 | const lineStyle = { 27 | ...placeholderStyle, 28 | height: '12px', 29 | width: '150px', 30 | margin: '0 15px', 31 | }; 32 | 33 | return ( 34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | ); 44 | }; 45 | 46 | export default Recipe; 47 | -------------------------------------------------------------------------------- /__tests__/fixtures/tailwind-root-tests.mdx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | ## Styling Custom Components with Tailwind 4 | 5 | ```mdx 6 | 7 | ``` 8 | 9 | 10 | 11 | ## Styling Locally Defined Components with Tailwind 12 | 13 | ```mdx 14 | export const LocalComponent = () =>
Local Component
; 15 | 16 | 17 | ``` 18 | 19 | export const LocalComponent = () =>
Local Component
; 20 | 21 | 22 | 23 | ## Updating Styling Dynamically 24 | 25 | import { useRef, useEffect } from 'react'; 26 | 27 | ```mdx 28 | export const Dynamic = () => { 29 | const ref = useRef(); 30 | 31 | useEffect(() => { 32 | ref.current.classList.add('bg-gray-950', 'text-green-500', 'p-5', 'rounded-xl'); 33 | }, []) 34 | 35 | return
Dynamic Component
36 | }; 37 | ``` 38 | 39 | export const Dynamic = () => { 40 | const ref = useRef(); 41 | 42 | useEffect(() => { 43 | ref.current.classList.add('bg-gray-950', 'text-green-500', 'p-5', 'rounded-xl'); 44 | }, []) 45 | 46 | return
Dynamic Component
47 | }; 48 | 49 | 50 | 51 | ## Dark Mode 52 | 53 | 54 | -------------------------------------------------------------------------------- /lib/utils/migrateComments.ts: -------------------------------------------------------------------------------- 1 | const COMMENT_BLOCK_REGEX = /{\/\*([\s\S]*?)\*\/}/g; 2 | 3 | const migrateComments = (doc: string, migrateDoc: (doc: string, opts) => string, opts): string => { 4 | return doc.replace(COMMENT_BLOCK_REGEX, (match, contents: string) => { 5 | // Preserve leading and trailing whitespace 6 | const leadingWhitespace = contents.match(/^\s*/)?.[0] ?? ''; 7 | const trailingWhitespace = contents.match(/\s*$/)?.[0] ?? ''; 8 | const inner = contents.slice(leadingWhitespace.length, contents.length - trailingWhitespace.length); 9 | 10 | // Skip empty comments 11 | if (!inner.trim()) return match; 12 | 13 | // Compile the inner content through the migration pipeline 14 | let compiled: string; 15 | try { 16 | compiled = migrateDoc(inner, opts); 17 | // Trim trailing whitespace only if inner content had no newlines 18 | if (!/\r|\n/.test(inner)) { 19 | compiled = compiled.trimEnd(); 20 | } 21 | } catch { 22 | return match; 23 | } 24 | 25 | // Recursively process any nested comments 26 | const processed = migrateComments(compiled, migrateDoc, opts); 27 | 28 | return `{/*${leadingWhitespace}${processed}${trailingWhitespace}*/}`; 29 | }); 30 | }; 31 | 32 | export default migrateComments; -------------------------------------------------------------------------------- /lib/migrate.ts: -------------------------------------------------------------------------------- 1 | import migrateCallouts from '../processor/transform/migrate-callouts'; 2 | import migrateHtmlBlocks from '../processor/transform/migrate-html-blocks'; 3 | import migrateHtmlTags from '../processor/transform/migrate-html-tags'; 4 | import migrateLinkReferences from '../processor/transform/migrate-link-references'; 5 | 6 | import mdastV6 from './mdastV6'; 7 | import mdx from './mdx'; 8 | import migrateComments from './utils/migrateComments'; 9 | 10 | const migrateDoc = (doc: string, { rdmd }): string => { 11 | const ast = mdastV6(doc, { rdmd }); 12 | 13 | return ( 14 | mdx(ast, { 15 | remarkTransformers: [migrateCallouts, [migrateLinkReferences, { rdmd }], migrateHtmlTags, migrateHtmlBlocks], 16 | file: doc, 17 | }) 18 | .replaceAll(/ /g, ' ') 19 | // @note: I'm not sure what's happening, but I think mdx is converting an 20 | // 'a' to 'a' as a means of escaping it. I think this helps with 21 | // parsing weird cases. 22 | .replaceAll(/a/g, 'a') 23 | ); 24 | }; 25 | 26 | const migrate = (doc: string, opts): string => { 27 | const migratedDoc = migrateDoc(doc, opts); 28 | const migratedDocAndComments = migrateComments(migratedDoc, migrateDoc, opts); 29 | return migratedDocAndComments; 30 | }; 31 | 32 | export default migrate; 33 | -------------------------------------------------------------------------------- /__tests__/migration/emphasis.test.ts: -------------------------------------------------------------------------------- 1 | import { migrate } from '../helpers'; 2 | 3 | describe('migrating emphasis', () => { 4 | it('trims whitespace surrounding phrasing content (emphasis, strong, etc)', () => { 5 | const md = '** bold ** and _ italic _ and *** bold italic ***'; 6 | 7 | const mdx = migrate(md); 8 | expect(mdx).toMatchInlineSnapshot(` 9 | "**bold** and *italic* and ***bold italic*** 10 | " 11 | `); 12 | }); 13 | 14 | it('moves whitespace surrounding phrasing content (emphasis, strong, etc) to the appropriate place', () => { 15 | const md = '**bold **and also_ italic_ and*** bold italic***aaaaaah'; 16 | 17 | const mdx = migrate(md); 18 | expect(mdx).toMatchInlineSnapshot(` 19 | "**bold** and also *italic* and ***bold italic***aaaaaah 20 | " 21 | `); 22 | }); 23 | 24 | it('migrates a complex case', () => { 25 | const md = 26 | '*the recommended initial action is to**initiate a [reversal operation (rollback)](https://docs.jupico.com/reference/ccrollback) test**. *'; 27 | 28 | const mdx = migrate(md); 29 | expect(mdx).toMatchInlineSnapshot(` 30 | "*the recommended initial action is to**initiate a [reversal operation (rollback)](https://docs.jupico.com/reference/ccrollback) test**.* 31 | " 32 | `); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /components/Tabs/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | import './style.scss'; 4 | 5 | export const Tab = ({ children }: React.PropsWithChildren) => { 6 | return
{children}
; 7 | }; 8 | 9 | interface TabsProps { 10 | children?: React.ReactElement[]; 11 | } 12 | 13 | const Tabs = ({ children }: TabsProps) => { 14 | const [activeTab, setActiveTab] = useState(0); 15 | 16 | return ( 17 |
18 |
19 | 36 |
37 |
{children && children[activeTab]}
38 |
39 | ); 40 | }; 41 | 42 | export default Tabs; 43 | -------------------------------------------------------------------------------- /lib/utils/makeUseMdxComponents.ts: -------------------------------------------------------------------------------- 1 | import type { Depth } from '../../components/Heading'; 2 | import type { UseMdxComponents } from '@mdx-js/mdx'; 3 | import type { MDXComponents } from 'mdx/types'; 4 | 5 | import Variable from '@readme/variable'; 6 | 7 | import * as Components from '../../components'; 8 | 9 | const makeUseMDXComponents = (more: ReturnType = {}): UseMdxComponents => { 10 | const headings = Array.from({ length: 6 }).reduce((map, _, index) => { 11 | map[`h${index + 1}`] = Components.Heading((index + 1) as Depth); 12 | return map; 13 | }, {}) as MDXComponents; 14 | 15 | const components = { 16 | ...Components, 17 | Variable, 18 | code: Components.Code, 19 | embed: Components.Embed, 20 | img: Components.Image, 21 | table: Components.Table, 22 | 'code-tabs': Components.CodeTabs, 23 | 'embed-block': Components.Embed, 24 | 'html-block': Components.HTMLBlock, 25 | 'image-block': Components.Image, 26 | 'table-of-contents': Components.TableOfContents, 27 | // Ensures backwards compatibility with historical TutorialTile component 28 | TutorialTile: Components.Recipe, 29 | ...headings, 30 | ...more, 31 | }; 32 | 33 | return (() => components) as unknown as UseMdxComponents; 34 | }; 35 | 36 | export default makeUseMDXComponents; 37 | -------------------------------------------------------------------------------- /lib/utils/mdxish/mdxish-get-component-name.ts: -------------------------------------------------------------------------------- 1 | import type { CustomComponents } from '../../../types'; 2 | 3 | /** Convert a string to PascalCase */ 4 | function toPascalCase(str: string): string { 5 | return str 6 | .split(/[-_]/) 7 | .map(word => word.charAt(0).toUpperCase() + word.slice(1)) 8 | .join(''); 9 | } 10 | 11 | /** 12 | * Find a component in the components hash using case-insensitive matching. 13 | * Returns the actual key from the map, or null if not found. 14 | * 15 | * Matching priority: 16 | * 1. Exact match 17 | * 2. PascalCase version 18 | * 3. Case-insensitive match 19 | */ 20 | export function getComponentName(componentName: string, components: CustomComponents): string | null { 21 | // 1. Try exact match 22 | if (componentName in components) return componentName; 23 | 24 | // 2. Try PascalCase version 25 | const pascalCase = toPascalCase(componentName); 26 | if (pascalCase in components) return pascalCase; 27 | 28 | // 3. Try case-insensitive match 29 | const lowerName = componentName.toLowerCase(); 30 | const lowerPascal = pascalCase.toLowerCase(); 31 | 32 | return ( 33 | Object.keys(components).find(key => { 34 | const lowerKey = key.toLowerCase(); 35 | return lowerKey === lowerName || lowerKey === lowerPascal; 36 | }) ?? null 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /__tests__/transformers/readme-to-mdx.test.ts: -------------------------------------------------------------------------------- 1 | import type { Recipe } from '../../types'; 2 | import type { Element } from 'hast'; 3 | import type { Root } from 'mdast'; 4 | 5 | import { mdx, mdxish } from '../../index'; 6 | 7 | describe('readme-to-mdx transformer', () => { 8 | it('converts a tutorial tile to MDX', () => { 9 | const ast: Root = { 10 | type: 'root', 11 | children: [ 12 | { 13 | type: 'tutorial-tile', 14 | backgroundColor: 'red', 15 | emoji: '🦉', 16 | id: 'test-id', 17 | link: 'http://example.com', 18 | slug: 'test-id', 19 | title: 'Test', 20 | } as Recipe, 21 | ], 22 | }; 23 | 24 | expect(mdx(ast)).toMatchInlineSnapshot(` 25 | " 26 | " 27 | `); 28 | }); 29 | }); 30 | 31 | describe('mdxish readme-to-mdx transformer', () => { 32 | it('processes Recipe component', () => { 33 | const markdown = ''; 34 | 35 | const hast = mdxish(markdown); 36 | const recipe = hast.children[0] as Element; 37 | 38 | expect(recipe.type).toBe('element'); 39 | expect(recipe.tagName).toBe('Recipe'); 40 | expect(recipe.properties.slug).toBe('test-id'); 41 | expect(recipe.properties.title).toBe('Test'); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /components/MCPIntro/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | // We render a placeholder in this library, as the actual implemenation is 4 | // deeply tied to the main app 5 | const MCPIntro = () => { 6 | const style = { 7 | height: '200px', 8 | border: '1px solid var(--color-border-default, rgba(black, 0.1))', 9 | borderRadius: 'var(--border-radius-lg, 7.5px)', 10 | display: 'flex', 11 | flexDirection: 'column' as const, 12 | padding: '20px', 13 | gap: '15px', 14 | }; 15 | 16 | const placeholderStyle = { 17 | borderRadius: 'var(--border-radius-md, 5px)', 18 | backgroundColor: 'var(--color-skeleton, #384248)', 19 | }; 20 | 21 | const headerStyle = { 22 | ...placeholderStyle, 23 | height: '24px', 24 | width: '200px', 25 | }; 26 | 27 | const lineStyle = { 28 | ...placeholderStyle, 29 | height: '12px', 30 | width: '100%', 31 | }; 32 | 33 | return ( 34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | ); 43 | }; 44 | 45 | export default MCPIntro; 46 | -------------------------------------------------------------------------------- /docs/mermaid.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Mermaid Diagrams 3 | category: 4 | uri: uri-that-does-not-map-to-5fdf7610134322007389a6ed 5 | privacy: 6 | view: public 7 | --- 8 | 9 | ## Examples 10 | 11 | ### Single Code Tab 12 | 13 | ```mermaid 14 | graph LR 15 | A --- B 16 | B-->C[fa:fa-ban forbidden] 17 | B-->D(fa:fa-spinner); 18 | ``` 19 | 20 | ### Multiple Code Tabs 21 | 22 | ```mermaid 23 | pie title Pets adopted by volunteers 24 | "Dogs" : 386 25 | "Cats" : 85 26 | "Rats" : 15 27 | ``` 28 | ```mermaid 29 | stateDiagram-v2 30 | [*] --> Still 31 | Still --> [*] 32 | 33 | Still --> Moving 34 | Moving --> Still 35 | Moving --> Crash 36 | Crash --> [*] 37 | ``` 38 | 39 | ```mermaid diagram 40 | journey 41 | title My working day 42 | section Go to work 43 | Make tea: 5: Me 44 | Go upstairs: 3: Me 45 | Do work: 1: Me, Cat 46 | section Go home 47 | Go downstairs: 5: Me 48 | Sit down: 5: Me 49 | ``` 50 | ```syntax 51 | journey 52 | title My working day 53 | section Go to work 54 | Make tea: 5: Me 55 | Go upstairs: 3: Me 56 | Do work: 1: Me, Cat 57 | section Go home 58 | Go downstairs: 5: Me 59 | Sit down: 5: Me 60 | ``` 61 | -------------------------------------------------------------------------------- /__tests__/fixtures/callout-tests.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Callouts Tests' 3 | category: 5fdf9fc9c2a7ef443e937315 4 | hidden: true 5 | --- 6 | 7 | 8 | ### Default 9 | 10 | 11 | ### Success 12 | 13 | 14 | ### Info 15 | 16 | 17 | ### Warn 18 | 19 | 20 | ### Error 21 | 22 | 23 | No Icon Info 24 | No Icon Error 25 | 26 | No Theme 👍 27 | 28 | > 👍 Success 29 | > 30 | > This is the success callout. 31 | 32 | > 📘 Info 33 | > 34 | > This is the info callout. 35 | 36 | > 🚧 Warn 37 | > 38 | > This is the warn callout. 39 | 40 | > ❗ Error 41 | > 42 | > This is the error callout. 43 | 44 | > 👎 Markdown in callouts 45 | > 46 | > Unordered List 47 | > 48 | > - List Item 1 49 | > - List Item 2 50 | 51 | 52 | ### MDX Callout 53 | 54 | --- 55 | 56 | With Markdown support. 57 | 58 | 59 | > ❗ 60 | > 61 | > Description Only 62 | 63 | > ❔ Title Only _with italics_ 64 | -------------------------------------------------------------------------------- /components/Glossary/index.tsx: -------------------------------------------------------------------------------- 1 | import type { GlossaryTerm } from '../../contexts/GlossaryTerms'; 2 | 3 | import Tooltip from '@tippyjs/react'; 4 | import React, { useContext } from 'react'; 5 | 6 | import GlossaryContext from '../../contexts/GlossaryTerms'; 7 | 8 | interface Props extends React.PropsWithChildren { 9 | term?: string; 10 | terms: GlossaryTerm[]; 11 | } 12 | 13 | const Glossary = ({ children, term: termProp, terms }: Props) => { 14 | const term = (Array.isArray(children) ? children[0] : children) || termProp; 15 | const foundTerm = terms.find(i => term.toLowerCase() === i?.term?.toLowerCase()); 16 | 17 | if (!foundTerm) return {term}; 18 | 19 | return ( 20 | 23 | {foundTerm.term} - {foundTerm.definition} 24 |
25 | } 26 | offset={[-5, 5]} 27 | placement="bottom-start" 28 | > 29 | {term} 30 | 31 | ); 32 | }; 33 | 34 | const GlossaryWithContext = (props: Omit) => { 35 | const terms = useContext(GlossaryContext); 36 | return terms ? : {props.term}; 37 | }; 38 | 39 | export { Glossary, GlossaryContext }; 40 | 41 | export default GlossaryWithContext; 42 | -------------------------------------------------------------------------------- /__tests__/transformers/images.test.ts: -------------------------------------------------------------------------------- 1 | import { mdast } from '../../index'; 2 | 3 | describe('images transformer', () => { 4 | it('converts single children images of paragraphs to an image-block', () => { 5 | const md = ` 6 | ![alt](https://example.com/image.jpg) 7 | `; 8 | const tree = mdast(md); 9 | 10 | expect(tree.children[0].type).toBe('image-block'); 11 | expect(tree.children[0].src).toBe('https://example.com/image.jpg'); 12 | }); 13 | 14 | it('can parse the caption markdown to children', () => { 15 | const md = ` 16 | 17 | `; 18 | const tree = mdast(md); 19 | 20 | expect(tree.children[0].children[0].children[0].type).toBe('strong'); 21 | expect(tree.children[0].children[0].children[2].type).toBe('emphasis'); 22 | }); 23 | 24 | it('can parse attributes', () => { 25 | const md = ` 26 | Some helpful text 34 | `; 35 | const tree = mdast(md); 36 | 37 | expect(tree.children[0].align).toBe('left'); 38 | expect(tree.children[0].alt).toBe('Some helpful text'); 39 | expect(tree.children[0].border).toBe(true); 40 | expect(tree.children[0].title).toBe('Testing'); 41 | expect(tree.children[0].width).toBe('100px'); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /docs/mdx-components.mdx: -------------------------------------------------------------------------------- 1 | ## Tables 2 | 3 | You can use our `Table` component to match the ReadMe theming. 4 | 5 | ```jsx MDX 6 | export const table = [ 7 | ['Left', 'Center', 'Right'], 8 | ['L0', '**bold**', '$1600'], 9 | ['L1', '`code`', '$12'], 10 | ['L2', '_italic_', '$1'], 11 | ]; 12 | 13 | 14 | 15 | 16 | {table[0].map((cell, index) => ( 17 | 18 | ))} 19 | 20 | 21 | 22 | {table.slice(1).map(row => ( 23 | 24 | {table[0].map((cell, index) => ( 25 | 26 | ))} 27 | 28 | ))} 29 | 30 |
{cell}
{cell}
; 31 | ``` 32 | 33 | export const table = [ 34 | ['Left', 'Center', 'Right'], 35 | ['L0', '**bold**', '$1600'], 36 | ['L1', '`code`', '$12'], 37 | ['L2', '_italic_', '$1'], 38 | ]; 39 | 40 | 41 | 42 | 43 | {table[0].map((cell, index) => ( 44 | 45 | ))} 46 | 47 | 48 | 49 | {table.slice(1).map(row => ( 50 | 51 | {row.map((cell, index) => ( 52 | 53 | ))} 54 | 55 | ))} 56 | 57 |
{cell}
{cell}
58 | -------------------------------------------------------------------------------- /lib/ast-processor.ts: -------------------------------------------------------------------------------- 1 | import type { PluggableList } from 'unified'; 2 | 3 | import rehypeSlug from 'rehype-slug'; 4 | import { remark } from 'remark'; 5 | import remarkFrontmatter from 'remark-frontmatter'; 6 | import remarkGfm from 'remark-gfm'; 7 | import remarkMdx from 'remark-mdx'; 8 | 9 | import transformers, { 10 | mermaidTransformer, 11 | readmeComponentsTransformer, 12 | variablesTransformer, 13 | handleMissingComponents, 14 | } from '../processor/transform'; 15 | 16 | export interface MdastOpts { 17 | components?: Record; 18 | missingComponents?: 'ignore' | 'throw'; 19 | remarkPlugins?: PluggableList; 20 | } 21 | 22 | export const remarkPlugins = [remarkFrontmatter, remarkGfm, ...transformers]; 23 | export const rehypePlugins = [rehypeSlug, mermaidTransformer]; 24 | 25 | const astProcessor = (opts: MdastOpts = {}) => { 26 | const components = opts.components || {}; 27 | 28 | let processor = remark() 29 | .use(remarkMdx) 30 | .use(remarkPlugins) 31 | .use(opts.remarkPlugins) 32 | .use(variablesTransformer, { asMdx: false }) 33 | .use(readmeComponentsTransformer({ components })); 34 | 35 | if (['ignore', 'throw'].includes(opts.missingComponents)) { 36 | processor = processor.use(handleMissingComponents, { 37 | components, 38 | missingComponents: opts.missingComponents, 39 | }); 40 | } 41 | 42 | return processor; 43 | }; 44 | 45 | export default astProcessor; 46 | -------------------------------------------------------------------------------- /__tests__/migration/html-tags.test.ts: -------------------------------------------------------------------------------- 1 | import { migrate } from '../helpers'; 2 | 3 | describe('migrating html tags', () => { 4 | describe('br tags', () => { 5 | it('converts unclosed br tags to self-closing', () => { 6 | const md = `This is a line with a break
and another line. 7 | Multiple breaks

in sequence. 8 | Already closed
should remain unchanged.`; 9 | 10 | const mdx = migrate(md); 11 | expect(mdx).toMatchInlineSnapshot(` 12 | "This is a line with a break
and another line.\\ 13 | Multiple breaks

in sequence.\\ 14 | Already closed
should remain unchanged. 15 | " 16 | `); 17 | }); 18 | 19 | it('handles br tags with attributes', () => { 20 | const md = 'Line with styled break
and more text.'; 21 | 22 | const mdx = migrate(md); 23 | expect(mdx).toMatchInlineSnapshot(` 24 | "Line with styled break
and more text. 25 | " 26 | `); 27 | }); 28 | 29 | it('does not change br tags inside code blocks', () => { 30 | const md = `Not a \`
\` tag. 31 | 32 | \`\`\` 33 | Also not a \`
\` tag. 34 | \`\`\``; 35 | 36 | const mdx = migrate(md); 37 | expect(mdx).toMatchInlineSnapshot(` 38 | "Not a \`
\` tag. 39 | 40 | \`\`\` 41 | Also not a \`
\` tag. 42 | \`\`\` 43 | " 44 | `); 45 | }); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /__tests__/migration/recipes.test.ts: -------------------------------------------------------------------------------- 1 | import { migrate } from '../helpers'; 2 | 3 | describe('mdx migration of recipes', () => { 4 | it('compiles recipes correctly', () => { 5 | const md = ` 6 | In a callout: 7 | 8 | > 🚀 Launch Example Code: 9 | > 10 | > [block:tutorial-tile]{"backgroundColor":"#0b1c36","emoji":"👉","id":"67d85229d1ac0900248b3111","link":"https://developer.moneygram.com/v1.0/recipes/amend-modify-receviers-name-headers","slug":"amend-modify-receviers-name-headers","title":"Amend - Modify Recevier's Name - Headers"}[/block] 11 | 12 | Or on a line by itself: 13 | 14 | [block:tutorial-tile] 15 | { 16 | "backgroundColor":"#0b1c36", 17 | "emoji":"👉", 18 | "id":"67d85229d1ac0900248b3111", 19 | "link":"https://developer.moneygram.com/v1.0/recipes/amend-modify-receviers-name-headers", 20 | "slug":"amend-modify-receviers-name-headers", 21 | "title":"Amend - Modify Recevier's Name - Headers" 22 | } 23 | [/block] 24 | `; 25 | 26 | const mdx = migrate(md); 27 | expect(mdx).toMatchInlineSnapshot(` 28 | "In a callout: 29 | 30 | 31 | ### Launch Example Code: 32 | 33 | 34 | 35 | 36 | Or on a line by itself: 37 | 38 | 39 | " 40 | `); 41 | }); 42 | }); -------------------------------------------------------------------------------- /__tests__/components/Code.test.tsx: -------------------------------------------------------------------------------- 1 | import { fireEvent, render, screen } from '@testing-library/react'; 2 | import copy from 'copy-to-clipboard'; 3 | import React from 'react'; 4 | 5 | import { vi } from 'vitest'; 6 | 7 | import Code from '../../components/Code'; 8 | 9 | 10 | const codeProps = { 11 | copyButtons: true, 12 | }; 13 | 14 | vi.mock('@readme/syntax-highlighter', () => ({ 15 | default: code => { 16 | return {code.replace(/<<.*?>>/, 'VARIABLE_SUBSTITUTED')}; 17 | }, 18 | canonical: lang => lang, 19 | })); 20 | 21 | describe.skip('Code', () => { 22 | it.skip('copies the variable interpolated code', () => { 23 | const props = { 24 | children: ['console.log("<>");'], 25 | }; 26 | 27 | const { container } = render(); 28 | 29 | expect(container).toHaveTextContent(/VARIABLE_SUBSTITUTED/); 30 | fireEvent.click(screen.getByRole('button')); 31 | 32 | expect(copy).toHaveBeenCalledWith(expect.stringMatching(/VARIABLE_SUBSTITUTED/)); 33 | }); 34 | 35 | it.skip('does not nest the button inside the code block', () => { 36 | render({'console.log("hi");'}); 37 | const btn = screen.getByRole('button'); 38 | 39 | expect(btn.parentNode?.nodeName.toLowerCase()).not.toBe('code'); 40 | }); 41 | 42 | it.skip('allows undefined children?!', () => { 43 | const { container } = render(); 44 | 45 | expect(container).toHaveTextContent(''); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /__tests__/compilers.test.ts: -------------------------------------------------------------------------------- 1 | import type { Element } from 'hast'; 2 | 3 | import { mdast, mdx, mdxish } from '../index'; 4 | 5 | describe('ReadMe Flavored Blocks', () => { 6 | it('Embed', () => { 7 | const txt = '[Embedded meta links.](https://nyti.me/s/gzoa2xb2v3 "@embed")'; 8 | const ast = mdast(txt); 9 | const out = mdx(ast); 10 | expect(out).toMatchSnapshot(); 11 | }); 12 | 13 | it('Emojis', () => { 14 | expect(mdx(mdast(':smiley:'))).toMatchInlineSnapshot(` 15 | ":smiley: 16 | " 17 | `); 18 | }); 19 | }); 20 | 21 | describe('mdxish ReadMe Flavored Blocks', () => { 22 | it('Embed', () => { 23 | const txt = '[Embedded meta links.](https://nyti.me/s/gzoa2xb2v3 "@embed")'; 24 | const hast = mdxish(txt); 25 | const embed = hast.children[0] as Element; 26 | 27 | expect(embed.type).toBe('element'); 28 | expect(embed.tagName).toBe('embed'); 29 | expect(embed.properties.url).toBe('https://nyti.me/s/gzoa2xb2v3'); 30 | expect(embed.properties.title).toBe('Embedded meta links.'); 31 | }); 32 | 33 | it('Emojis', () => { 34 | const hast = mdxish(':smiley:'); 35 | const paragraph = hast.children[0] as Element; 36 | 37 | expect(paragraph.type).toBe('element'); 38 | expect(paragraph.tagName).toBe('p'); 39 | // gemojiTransformer converts :smiley: to 😃 40 | const textNode = paragraph.children[0]; 41 | expect(textNode.type).toBe('text'); 42 | expect('value' in textNode && textNode.value).toBe('😃'); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /__tests__/lib/render-mdxish/Glossary.test.tsx: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | import { render, screen } from '@testing-library/react'; 3 | import React from 'react'; 4 | 5 | import { vi } from 'vitest'; 6 | 7 | import { mdxish } from '../../../index'; 8 | import renderMdxish from '../../../lib/renderMdxish'; 9 | 10 | describe('Glossary', () => { 11 | // Make sure we don't have any console errors when rendering a glossary item 12 | // which has happened before & crashing the app 13 | // It was because of the engine was converting the Glossary item to nested

tags 14 | // which React was not happy about 15 | let stderrSpy: ReturnType; 16 | let consoleErrorSpy: ReturnType; 17 | 18 | beforeAll(() => { 19 | stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true); 20 | consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); 21 | }); 22 | 23 | afterEach(() => { 24 | stderrSpy.mockRestore(); 25 | consoleErrorSpy.mockRestore(); 26 | }); 27 | 28 | it('renders a glossary item without console errors', () => { 29 | const md = `The term exogenous should show a tooltip on hover. 30 | `; 31 | const tree = mdxish(md); 32 | const mod = renderMdxish(tree); 33 | render(); 34 | expect(screen.getByText('exogenous')).toBeVisible(); 35 | 36 | expect(stderrSpy).not.toHaveBeenCalled(); 37 | expect(consoleErrorSpy).not.toHaveBeenCalled(); 38 | }); 39 | }); -------------------------------------------------------------------------------- /components/Accordion/style.scss: -------------------------------------------------------------------------------- 1 | .Accordion { 2 | background: rgba(var(--color-bg-page-rgb, white), 1); 3 | border: 1px solid var(--color-border-default, rgba(black, 0.1)); 4 | border-radius: 5px; 5 | 6 | &-title { 7 | align-items: center; 8 | background: rgba(var(--color-bg-page-rgb, white), 1); 9 | border: 0; 10 | border-radius: 5px; 11 | color: var(--color-text-default, #384248); 12 | cursor: pointer; 13 | display: flex; 14 | font-size: 16px; 15 | font-weight: 500; 16 | padding: 10px; 17 | position: relative; 18 | text-align: left; 19 | width: 100%; 20 | 21 | &:hover { 22 | background: var(--color-bg-hover, rgba(black, 0.05)); 23 | } 24 | 25 | .Accordion[open] & { 26 | border-bottom-left-radius: 0; 27 | border-bottom-right-radius: 0; 28 | } 29 | 30 | &::marker { 31 | content: ""; 32 | } 33 | 34 | &::-webkit-details-marker { 35 | display: none; 36 | } 37 | } 38 | 39 | &-toggleIcon, 40 | &-toggleIcon_opened { 41 | color: var(--color-text-minimum, #637288); 42 | font-size: 14px; 43 | margin-left: 5px; 44 | margin-right: 10px; 45 | transition: transform 0.1s; 46 | 47 | &_opened { 48 | transform: rotate(90deg); 49 | } 50 | } 51 | 52 | &-icon { 53 | color: var(--project-color-primary, inherit); 54 | margin-right: 10px; 55 | } 56 | 57 | &-content { 58 | color: var(--color-text-muted, #4f5a66); 59 | padding: 5px 15px 0 15px; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /lib/renderMdxish.tsx: -------------------------------------------------------------------------------- 1 | import type { CustomComponents, RMDXModule } from '../types'; 2 | import type { Root } from 'hast'; 3 | 4 | import { extractToc, tocToHast } from '../processor/plugin/toc'; 5 | 6 | import { loadComponents } from './utils/mdxish/mdxish-load-components'; 7 | import { 8 | buildRMDXModule, 9 | createRehypeReactProcessor, 10 | exportComponentsForRehype, 11 | type RenderOpts, 12 | } from './utils/mdxish/mdxish-render-utils'; 13 | 14 | export type { RenderOpts as RenderMdxishOpts }; 15 | 16 | /** 17 | * Converts a HAST tree to a React component. 18 | * @param tree - The HAST tree to convert 19 | * @param opts - The options for the render 20 | * @returns The React component 21 | * 22 | * @see {@link https://github.com/readmeio/rmdx/blob/main/docs/mdxish-flow.md} 23 | */ 24 | const renderMdxish = (tree: Root, opts: RenderOpts = {}): RMDXModule => { 25 | const { components: userComponents = {}, ...contextOpts } = opts; 26 | 27 | const components: CustomComponents = { 28 | ...loadComponents(), 29 | ...userComponents, 30 | }; 31 | 32 | const headings = extractToc(tree, components); 33 | const componentsForRehype = exportComponentsForRehype(components); 34 | const processor = createRehypeReactProcessor(componentsForRehype); 35 | const content = processor.stringify(tree) as React.ReactNode; 36 | 37 | const tocHast = headings.length > 0 ? tocToHast(headings) : null; 38 | 39 | return buildRMDXModule(content, headings, tocHast, contextOpts); 40 | }; 41 | 42 | export default renderMdxish; 43 | -------------------------------------------------------------------------------- /processor/transform/stripComments.ts: -------------------------------------------------------------------------------- 1 | import type { Root } from 'mdast'; 2 | 3 | import { visit, SKIP } from 'unist-util-visit'; 4 | 5 | const HTML_COMMENT_REGEX = //g; 6 | const MDX_COMMENT_REGEX = /\/\*[\s\S]*?\*\//g; 7 | 8 | /** 9 | * A remark plugin to remove comments from Markdown and MDX. 10 | */ 11 | export const stripCommentsTransformer = () => { 12 | return (tree: Root) => { 13 | visit(tree, ['html', 'mdxFlowExpression', 'mdxTextExpression'], (node, index, parent) => { 14 | if (parent && typeof index === 'number') { 15 | // Remove HTML comments 16 | if (node.type === 'html' && HTML_COMMENT_REGEX.test(node.value)) { 17 | const newValue = node.value.replace(HTML_COMMENT_REGEX, '').trim(); 18 | if (newValue) { 19 | node.value = newValue; 20 | } else { 21 | parent.children.splice(index, 1); 22 | return [SKIP, index]; 23 | } 24 | } 25 | 26 | // Remove MDX comments 27 | if ( 28 | (node.type === 'mdxFlowExpression' || node.type === 'mdxTextExpression') && 29 | MDX_COMMENT_REGEX.test(node.value) 30 | ) { 31 | const newValue = node.value.replace(MDX_COMMENT_REGEX, '').trim(); 32 | if (newValue) { 33 | node.value = newValue; 34 | } else { 35 | parent.children.splice(index, 1); 36 | return [SKIP, index]; 37 | } 38 | } 39 | } 40 | 41 | return undefined; 42 | }); 43 | }; 44 | }; 45 | -------------------------------------------------------------------------------- /__tests__/transformers/preprocess-jsx-expressions.test.ts: -------------------------------------------------------------------------------- 1 | import { preprocessJSXExpressions } from '../../processor/transform/mdxish/preprocess-jsx-expressions'; 2 | 3 | describe('preprocessJSXExpressions', () => { 4 | describe('Step 3: Evaluate attribute expressions', () => { 5 | it('should evaluate JSX attribute expressions and convert them to string attributes', () => { 6 | const context = { 7 | baseUrl: 'https://example.com', 8 | userId: '123', 9 | isActive: true, 10 | }; 11 | 12 | const content = 'Link'; 13 | const result = preprocessJSXExpressions(content, context); 14 | 15 | expect(result).toContain('href="https://example.com"'); 16 | expect(result).toContain('id="123"'); 17 | expect(result).toContain('active="true"'); 18 | expect(result).not.toContain('href={baseUrl}'); 19 | expect(result).not.toContain('id={userId}'); 20 | expect(result).not.toContain('active={isActive}'); 21 | }); 22 | 23 | it.each([ 24 | [true, '{"b":1}'], 25 | [false, '{"c":2}'], 26 | ])('should handle nested dictionary attributes when a is %s', (a, expectedJson) => { 27 | const context = { a }; 28 | 29 | const content = '

Link
'; 30 | const result = preprocessJSXExpressions(content, context); 31 | 32 | expect(result).toContain(`foo='${expectedJson}'`); 33 | expect(result).not.toContain('foo={a ? {b: 1} : {c: 2}}'); 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /lib/mdx.ts: -------------------------------------------------------------------------------- 1 | import type { Root as HastRoot } from 'hast'; 2 | import type { Root as MdastRoot } from 'mdast'; 3 | import type { PluggableList } from 'unified'; 4 | import type { VFile } from 'vfile'; 5 | 6 | import rehypeRemark from 'rehype-remark'; 7 | import remarkGfm from 'remark-gfm'; 8 | import remarkMdx from 'remark-mdx'; 9 | import remarkStringify from 'remark-stringify'; 10 | import { unified } from 'unified'; 11 | 12 | import compilers from '../processor/compile'; 13 | import { compatabilityTransfomer, divTransformer, readmeToMdx, tablesToJsx } from '../processor/transform'; 14 | import { escapePipesInTables } from '../processor/transform/escape-pipes-in-tables'; 15 | 16 | interface Opts { 17 | file?: VFile | string; 18 | hast?: boolean; 19 | remarkTransformers?: PluggableList; 20 | } 21 | 22 | export const mdx = ( 23 | tree: HastRoot | MdastRoot, 24 | { hast = false, remarkTransformers = [], file, ...opts }: Opts = {}, 25 | ) => { 26 | const processor = unified() 27 | .use(hast ? rehypeRemark : undefined) 28 | .use(remarkMdx) 29 | .use(remarkGfm) 30 | .use(remarkTransformers) 31 | .use(divTransformer) 32 | .use(readmeToMdx) 33 | .use(tablesToJsx) 34 | .use(compatabilityTransfomer) 35 | .use(escapePipesInTables) 36 | .use(compilers) 37 | .use(remarkStringify, opts); 38 | 39 | // @ts-expect-error - @todo: coerce the processor and tree to the correct 40 | // type depending on the value of hast 41 | return processor.stringify(processor.runSync(tree, file)); 42 | }; 43 | 44 | export default mdx; 45 | -------------------------------------------------------------------------------- /processor/transform/div.ts: -------------------------------------------------------------------------------- 1 | import type { Recipe } 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 | // This transformer has been necessary for migrating legacy markdown files 10 | // where tutorial tiles were wrapped in a div. It also provides a fallback for legacy magic blocks that were never fully supported: 11 | // [block:custom-block] 12 | // { ... } 13 | // [/block] 14 | // This transformer runs before the readme-to-mdx transformer which reshapes the tutorial tile node 15 | // to the Recipe component 16 | const divTransformer = (): Transform => tree => { 17 | visit(tree, 'div', (node: Node, index, parent: Parent) => { 18 | const type = node.data?.hName; 19 | switch (type) { 20 | // Check if the div is a tutorial-tile in disguise 21 | case NodeTypes.tutorialTile: 22 | { 23 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 24 | const { hName, hProperties, ...rest } = node.data; 25 | const tile = { 26 | ...rest, 27 | type: NodeTypes.recipe, 28 | } as Recipe; 29 | parent.children.splice(index, 1, tile); 30 | } 31 | break; 32 | // idk what this is and/or just make it a paragraph 33 | default: 34 | node.type = type || 'paragraph'; 35 | } 36 | }); 37 | 38 | return tree; 39 | }; 40 | 41 | export default divTransformer; 42 | -------------------------------------------------------------------------------- /lib/owlmoji.ts: -------------------------------------------------------------------------------- 1 | import type { Gemoji } from 'gemoji'; 2 | 3 | import { gemoji, nameToEmoji } from 'gemoji'; 4 | 5 | export const owlmoji = [ 6 | { 7 | emoji: '', // This `emoji` property doesn't get consumed, but is required for type consistency 8 | names: ['owlbert'], 9 | tags: ['owlbert'], 10 | description: 'an owlbert for any occasion', 11 | category: 'ReadMe', 12 | }, 13 | { 14 | emoji: '', 15 | names: ['owlbert-books'], 16 | tags: ['owlbert'], 17 | description: 'owlbert carrying books', 18 | category: 'ReadMe', 19 | }, 20 | { 21 | emoji: '', 22 | names: ['owlbert-mask'], 23 | tags: ['owlbert'], 24 | description: 'owlbert with a respirator', 25 | category: 'ReadMe', 26 | }, 27 | { 28 | emoji: '', 29 | names: ['owlbert-reading'], 30 | tags: ['owlbert'], 31 | description: 'owlbert reading', 32 | category: 'ReadMe', 33 | }, 34 | { 35 | emoji: '', 36 | names: ['owlbert-thinking'], 37 | tags: ['owlbert'], 38 | description: 'owlbert thinking', 39 | category: 'ReadMe', 40 | }, 41 | ] satisfies Gemoji[]; 42 | 43 | const owlmojiNames = owlmoji.flatMap(emoji => emoji.names); 44 | 45 | export default class Owlmoji { 46 | static kind = (name: string) => { 47 | if (name in nameToEmoji) return 'gemoji'; 48 | else if (name.match(/^fa-/)) return 'fontawesome'; 49 | else if (owlmojiNames.includes(name)) return 'owlmoji'; 50 | return null; 51 | }; 52 | 53 | static nameToEmoji = nameToEmoji; 54 | 55 | static owlmoji = gemoji.concat(owlmoji); 56 | } 57 | -------------------------------------------------------------------------------- /__tests__/parsers/tables.test.ts: -------------------------------------------------------------------------------- 1 | import { removePosition } from 'unist-util-remove-position'; 2 | 3 | import { mdast } from '../../index'; 4 | 5 | describe('table parser', () => { 6 | describe('unescaping pipes', () => { 7 | it('parses tables with pipes in inline code', () => { 8 | const doc = ` 9 | | | | 10 | | :----------- | :- | 11 | | \`foo \\| bar\` | | 12 | `; 13 | const ast = mdast(doc); 14 | removePosition(ast, { force: true }); 15 | 16 | expect(ast).toMatchSnapshot(); 17 | }); 18 | 19 | it('parses tables with pipes', () => { 20 | const doc = ` 21 | | | | 22 | | :--------- | :- | 23 | | foo \\| bar | | 24 | `; 25 | const ast = mdast(doc); 26 | removePosition(ast, { force: true }); 27 | 28 | expect(ast).toMatchSnapshot(); 29 | }); 30 | 31 | it('parses jsx tables with pipes in inline code', () => { 32 | const doc = ` 33 | 34 | 35 | 36 | 40 | 41 | 44 | 45 | 46 | 47 | 48 | 49 | 52 | 53 | 56 | 57 | 58 |
37 | force 38 | jsx 39 | 42 | 43 |
50 | \`foo | bar\` 51 | 54 | 55 |
59 | `; 60 | 61 | const ast = mdast(doc); 62 | removePosition(ast, { force: true }); 63 | 64 | expect(ast).toMatchSnapshot(); 65 | }); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /docs/syntax-extensions.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Flavored Syntax 3 | category: 4 | uri: uri-that-does-not-map-to-5fdf7610134322007389a6ec 5 | content: 6 | excerpt: Specs and examples of ReadMe's (restrained) Markdown syntax extensions. 7 | privacy: 8 | view: public 9 | --- 10 | Custom Blocks 11 | --- 12 | 13 | ### Code Tabs 14 | 15 | A tabbed interface for displaying multiple code blocks. These are written nearly identically to a series of vanilla markdown code snippets, except for their distinct *lack* of an additional line break separating each subsequent block. [**Syntax & examples**.](doc:code-blocks) 16 | 17 | ### Callouts 18 | 19 | Callouts are very nearly equivalent to standard Markdown block quotes in their syntax, other than some specific requirements on their content: To be considered a “callout”, the block quote must start with an initial emoji, which is used to determine the callout's theme. [**Syntax & examples**.](doc:callouts) 20 | 21 | ### Embeds 22 | 23 | Embedded content is written as a simple Markdown link, with a title of "@embed". [**Syntax & examples**.](doc:embeds) 24 | 25 | Standard Markdown 26 | --- 27 | 28 | The engine also supports all standard markdown constructs, as well as CommonMark options, and most GitHub syntax extensions. 29 | 30 | - [**Tables**](doc:tables) 31 | - [**Lists**](doc:lists) 32 | - [**Headings**](doc:headings) 33 | - [**Images**](doc:images) 34 | - **Decorations** (link, strong, and emphasis tags, etc.) 35 | -------------------------------------------------------------------------------- /components/Cards/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import './style.scss'; 4 | 5 | interface CardProps 6 | extends React.PropsWithChildren<{ 7 | badge?: string; 8 | href?: string; 9 | icon?: string; 10 | iconColor?: string; 11 | kind?: 'card' | 'tile'; 12 | target?: string; 13 | title?: string; 14 | }> {} 15 | 16 | export const Card = ({ badge, children, href, kind = 'card', icon, iconColor, target, title }: CardProps) => { 17 | const Tag = href ? 'a' : 'div'; 18 | 19 | return ( 20 | 21 | {icon && } 22 |
23 | {title && ( 24 |
25 | {title} 26 | {badge && {badge}} 27 | {href &&