├── .dockerignore ├── .eslintignore ├── .eslintrc.js ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── bundlewatch.yml │ ├── ci.yml │ ├── codeql-analysis.yml │ └── release.yml ├── .gitignore ├── .npm-upgrade.json ├── .prettierignore ├── .releaserc ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── Makefile ├── Procfile ├── README.md ├── __mocks__ ├── .eslintrc └── copy-to-clipboard.js ├── __tests__ ├── .eslintrc ├── Glossary.test.tsx ├── __snapshots__ │ ├── compilers.test.ts.snap │ ├── html-block-parser.test.js.snap │ ├── index.test.js.snap │ └── link-parsers.test.js.snap ├── 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 │ │ ├── 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-callouts-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-block-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-code-blocks-without-surprises-2-snap.png │ │ ├── markdown-test-js-visual-regression-tests-rdmd-syntax-renders-embeds-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-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-html-blocks-style-tags-and-style-attributes-with-safe-mode-off-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-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-mdx-components-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-table-of-contents-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-tables-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-tutorial-tile-without-surprises-1-snap.png │ │ └── markdown-test-js-visual-regression-tests-rdmd-syntax-renders-vars-test-without-surprises-1-snap.png │ ├── markdown.test.js │ └── setup.js ├── compilers.test.ts ├── compilers │ ├── callout.test.ts │ ├── code-tabs.test.js │ ├── compatability.test.tsx │ ├── escape.test.js │ ├── gemoji.test.ts │ ├── html-block.test.ts │ ├── images.test.ts │ ├── links.test.ts │ ├── plain.test.ts │ ├── reusable-content.test.js │ ├── tables.test.js │ ├── variable.test.ts │ └── yaml.test.js ├── components │ ├── Callout.test.tsx │ ├── Code.test.tsx │ ├── CodeTabs.test.tsx │ ├── Glossary.test.tsx │ ├── HTMLBlock.test.tsx │ ├── Image.test.tsx │ ├── TableOfContents.test.jsx │ ├── Variable.test.tsx │ ├── __snapshots__ │ │ ├── TableOfContents.test.jsx.snap │ │ └── index.test.ts.snap │ └── index.test.ts ├── custom-components │ └── index.test.tsx ├── fixtures │ ├── callout-tests.md │ ├── child-tests.mdx │ ├── code-block-tests.md │ ├── export-tests.mdx │ ├── image-block-no-attrs.md │ ├── image-block-no-attrs.mdx │ ├── image-tests.mdx │ ├── sanitizing-tests.md │ ├── table-of-contents-tests.md │ ├── tailwind-root-tests.mdx │ ├── tutorial-tile.mdx │ └── variable-tests.md ├── helpers.ts ├── html-block-parser.test.js ├── index.test.js ├── lib │ ├── __snapshots__ │ │ └── owlmoji.test.ts.snap │ ├── exports │ │ ├── index.test.ts │ │ └── input │ │ │ ├── multipleExports.mdx │ │ │ ├── singleExport.mdx │ │ │ └── weirdExports.mdx │ ├── hast.test.ts │ ├── mdast │ │ ├── __snapshots__ │ │ │ ├── anchor.test.tsx.snap │ │ │ └── index.test.ts.snap │ │ ├── anchor.test.tsx │ │ ├── esm │ │ │ ├── in.mdx │ │ │ └── out.json │ │ ├── images │ │ │ └── inline │ │ │ │ ├── in.mdx │ │ │ │ └── out.json │ │ ├── index.test.ts │ │ ├── null-attributes │ │ │ ├── in.mdx │ │ │ └── out.json │ │ ├── tables │ │ │ ├── in.mdx │ │ │ └── out.json │ │ ├── variables-with-spaces │ │ │ ├── in.mdx │ │ │ └── out.json │ │ └── variables │ │ │ ├── in.mdx │ │ │ └── out.json │ ├── owlmoji.test.ts │ ├── plain.test.ts │ ├── plain │ │ ├── custom-components.test.ts │ │ └── html-blocks.test.ts │ ├── registerCustomComponents.test.jsx │ ├── run.test.tsx │ └── tags.test.ts ├── link-parsers.test.js ├── matchers.ts ├── migration │ ├── emphasis.test.ts │ ├── html-blocks.test.ts │ ├── html-comments.test.ts │ ├── html-entities.test.ts │ ├── image.test.ts │ ├── link-reference.test.ts │ ├── magic-block.test.ts │ └── tables.test.ts ├── parsers │ ├── __snapshots__ │ │ ├── callouts.test.js.snap │ │ ├── compact-headings.test.js.snap │ │ └── escape.test.js.snap │ ├── callouts.test.js │ ├── compact-headings.test.js │ ├── escape.test.js │ └── gemoji.test.ts ├── plugins │ └── toc.test.tsx ├── react.test.tsx ├── table-flattening │ └── index.test.js ├── transformers │ ├── __snapshots__ │ │ └── table-cell-inline-code.test.js.snap │ ├── callouts.test.ts │ ├── code-tabs.test.ts │ ├── embeds.test.ts │ ├── images.test.ts │ ├── readme-components.test.ts │ ├── readme-to-mdx.test.ts │ ├── table-cell-inline-code.test.js │ └── variables.test.tsx ├── types.d.ts └── variables │ └── index.test.tsx ├── assets └── img │ └── emojis │ ├── owlbert-books.png │ ├── owlbert-mask.png │ ├── owlbert-reading.png │ ├── owlbert-thinking.png │ └── owlbert.png ├── babel.config.js ├── components ├── Accordion │ ├── index.tsx │ └── style.scss ├── Anchor.jsx ├── Callout │ ├── index.tsx │ └── style.scss ├── Cards │ ├── index.tsx │ └── style.scss ├── Code │ ├── index.tsx │ └── style.scss ├── CodeTabs │ ├── index.tsx │ └── style.scss ├── Columns │ ├── index.tsx │ └── style.scss ├── Embed │ ├── index.tsx │ └── style.scss ├── Glossary │ ├── index.tsx │ └── style.scss ├── HTMLBlock │ └── index.tsx ├── Heading │ ├── index.tsx │ └── style.scss ├── Image │ ├── index.tsx │ └── style.scss ├── Table │ ├── index.tsx │ └── style.scss ├── TableOfContents │ ├── index.tsx │ └── style.scss ├── Tabs │ ├── index.tsx │ └── style.scss ├── TailwindRoot │ └── index.tsx ├── TailwindStyle │ └── index.tsx ├── TutorialTile.tsx └── index.ts ├── contexts ├── BaseUrl.js ├── CodeOpts.ts ├── GlossaryTerms.ts ├── Theme.ts └── index.tsx ├── docs ├── built-in-components.mdx ├── callouts.md ├── code-blocks.md ├── custom-css.md ├── embeds.md ├── features.md ├── getting-started.md ├── headings.md ├── images.md ├── lists.md ├── mdx-components.mdx ├── mermaid.md ├── syntax-extensions.md └── tables.md ├── enums.ts ├── errors └── mdx-syntax-error.ts ├── example ├── App.tsx ├── Doc.tsx ├── Form.tsx ├── Header.tsx ├── RenderError.tsx ├── Root.tsx ├── components.ts ├── demo.scss ├── docs.ts ├── favicon.ico ├── img │ ├── nyt-thumbnail.jpg │ ├── pizzabro.jpg │ └── readme-logo-white-on-blue.png ├── index.html ├── index.legacy.jsx ├── index.tsx └── styles │ ├── header.scss │ ├── methods │ └── _merge-multiple.scss │ ├── mixins │ ├── dark-mode.scss │ └── expand.scss │ ├── theme.scss │ └── vars.scss ├── index.tsx ├── jest-puppeteer.config.js ├── jest.config.js ├── lib ├── ErrorBoundary.js ├── ast-processor.ts ├── compile.ts ├── createElement │ └── index.js ├── exports.ts ├── hast.ts ├── index.ts ├── mdast.ts ├── mdastV6.ts ├── mdx.ts ├── migrate.ts ├── owlmoji.ts ├── plain.ts ├── registerCustomComponents.js ├── run.tsx ├── styles.ts ├── tags.ts └── utils │ └── makeUseMdxComponents.ts ├── options.js ├── package-lock.json ├── package.json ├── processor ├── compile │ ├── callout.ts │ ├── code-tabs.ts │ ├── compatibility.ts │ ├── embed.ts │ ├── gemoji.ts │ ├── html-block.ts │ ├── index.ts │ ├── plain.ts │ ├── table.ts │ └── yaml.js ├── migration │ ├── emphasis.ts │ ├── images.ts │ ├── index.ts │ ├── linkReference.ts │ └── table-cell.ts ├── plugin │ ├── section-anchor-id.js │ ├── table-flattening.js │ └── toc.ts ├── transform │ ├── callouts.ts │ ├── code-tabs.ts │ ├── compatability.ts │ ├── div.ts │ ├── embeds.ts │ ├── gemoji+.ts │ ├── handle-missing-components.ts │ ├── images.ts │ ├── index.ts │ ├── inject-components.ts │ ├── mdx-to-hast.ts │ ├── mermaid.ts │ ├── readme-components.ts │ ├── readme-to-mdx.ts │ ├── reusable-content.js │ ├── table-cell-inline-code.js │ ├── tables-to-jsx.ts │ ├── tailwind.tsx │ └── variables.ts └── utils.ts ├── sanitize.schema.js ├── scripts └── perf-test.js ├── stylelint.config.js ├── styles ├── components.scss ├── gfm.scss ├── main.scss └── mixins │ └── dark-mode.scss ├── tsconfig.json ├── types.d.ts ├── utils ├── consts.ts ├── tailwind-compiler.ts └── user.ts ├── vitest-setup.js ├── vitest.config.mts ├── vitest.d.ts ├── webpack.common.js ├── webpack.config.js └── webpack.dev.js /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | coverage 3 | dist 4 | example/index* 5 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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@v4 11 | - uses: actions/setup-node@v4 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 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push] 3 | 4 | jobs: 5 | Test: 6 | if: "!contains(github.event.head_commit.message, 'SKIP CI')" 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | node-version: 11 | - lts/-1 12 | - lts/* 13 | - latest 14 | react: [16, 17, 18] 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: Use Node.js ${{ matrix.node-version }} 19 | uses: actions/setup-node@v4 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | 23 | - name: Install dependencies 24 | run: npm ci 25 | 26 | - name: Install React <18 deps 27 | if: matrix.react == '16' || matrix.react == '17' 28 | run: npm i react@${{ matrix.react }} react-dom@${{ matrix.react }} @testing-library/react@12 29 | 30 | - name: Run tests 31 | run: npm test 32 | 33 | visual: 34 | name: 'Visual Tests' 35 | if: "!contains(github.event.head_commit.message, 'SKIP CI')" 36 | runs-on: ubuntu-latest 37 | strategy: 38 | matrix: 39 | node-version: [20.x, 22.x] 40 | react: [18] 41 | 42 | steps: 43 | - uses: actions/checkout@v4 44 | 45 | - name: Run visual tests (node ${{ matrix.node-version }}) 46 | run: make ci 47 | 48 | - name: Upload snapshot diffs 49 | uses: actions/upload-artifact@v4 50 | if: ${{ failure() }} 51 | with: 52 | name: snapshots-diffs 53 | path: __tests__/browser/__image_snapshots__/__diff_output__ 54 | 55 | - name: Update regression test snapshots 56 | if: ${{ failure() }} 57 | run: make updateSnapshot 58 | 59 | - name: Upload snapshots 60 | uses: actions/upload-artifact@v4 61 | if: ${{ failure() }} 62 | with: 63 | name: image-snapshots 64 | path: __tests__/browser/__image_snapshots__/ 65 | -------------------------------------------------------------------------------- /.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@v4 23 | 24 | - name: Initialize CodeQL 25 | uses: github/codeql-action/init@v3 26 | 27 | - name: Perform CodeQL Analysis 28 | uses: github/codeql-action/analyze@v3 29 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - beta 7 | 8 | jobs: 9 | Release: 10 | if: "!contains(github.event.head_commit.message, 'SKIP CI')" 11 | runs-on: ubuntu-latest 12 | steps: 13 | # Setup the git repo & Node environemnt. 14 | # 15 | - name: Checkout branch (${{ github.ref }}) 16 | uses: actions/checkout@v4 17 | with: 18 | persist-credentials: false # install breaks with persistant creds! 19 | 20 | - name: Setup node 21 | uses: actions/setup-node@v4 22 | with: 23 | node-version: 20 24 | 25 | - name: Update npm 26 | run: npm i -g npm@10 27 | 28 | - name: Install dependencies 29 | run: | 30 | npm ci 31 | npm run build --if-present 32 | env: 33 | PUPPETEER_SKIP_DOWNLOAD: true 34 | 35 | # Build, version, and tag a new release. 36 | # 37 | - name: Publish release 38 | run: npm run release # configured in .releaserc 39 | env: 40 | GH_TOKEN: ${{ secrets.GH_TOKEN }} # auth push to remote repo 41 | NPM_TOKEN: ${{ secrets.NPM_TOKEN || secrets.GH_TOKEN }} # auth publish to registry 42 | 43 | # Push release changes to the remote. 44 | # 45 | - name: Push to remote 46 | uses: ad-m/github-push-action@master 47 | with: 48 | github_token: ${{ secrets.GITHUB_TOKEN }} 49 | branch: ${{ github.ref }} 50 | 51 | # Merge @latest release back to @next. 52 | # 53 | - name: Sync to next 54 | if: "github.ref == 'refs/heads/main'" 55 | uses: ad-m/github-push-action@master 56 | with: 57 | github_token: ${{ secrets.GITHUB_TOKEN }} 58 | branch: ${{ github.ref }} 59 | continue-on-error: true 60 | 61 | # Sync docs to rdmd.readme.io 62 | # 63 | - name: Sync docs to rdmd.readme.io 64 | uses: readmeio/rdme@v9 65 | with: 66 | rdme: docs ./docs --key=${{ secrets.RDME_KEY }} --version=2 67 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | } -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | === 3 | 4 | 5 | 6 | ### Commit Conventions 7 | 8 | When pushing or merging PRs in to main, your commit messages should follow the [Angular commit conventions](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#-git-commit-guidelines). At it's simplest, this looks something like `{type}: change this, add that`, where the commit `{type}` can be one of the following: 9 | 10 | | Commit Type | Description | 11 | | :--- | :--- | 12 | | `build` | creating a new release | 13 | | `chore` | assorted minor changes | 14 | | `ci` | updates related to the ci process | 15 | | `docs` | documentation updates | 16 | | `feat` | new elements; major features and updates | 17 | | `fix` | bug fixes; security updates | 18 | | `perf` | performance improvements | 19 | | `refactor` | general refactors | 20 | | `revert` | reverting a previous commit | 21 | | `style` | aesthetic changes | 22 | | `test` | adding or updating existing tests | 23 | 24 | You can also optionally note the `{scope}` of your changes in an additional parenthetical. If your changes require a longer description, feel free to add a commit message with further details! Combining all of these together, you might end up with something like: 25 | 26 | ```text 27 | feat(api-explorer): add color variants 28 | 29 | - some more details 30 | - about the changes 31 | ``` 32 | 33 | ### Visual Regression Tests 34 | 35 | If you update the docs or the rendering changes, you'll need to update the snapshots. As most environments font configs are different, the simplest thing to do is grab the updated snapshots from the **Artifacts** section of your GitHub Actions workflow run (see [this failed workflow run](https://github.com/readmeio/markdown/actions/runs/1994189147) for an example). 36 | 37 | After a failed test, the updated snapshots should be available for download in the `image-snapshots` artifact. To update the snapshots, unzip the `image-snapshots` file and load its images into the `__tests__/browser/ci` directory. You can also view the (somewhat chaotic) image diffs in the `image-diffs` artifact. 38 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | -include .env 2 | 3 | .DEFAULT_GOAL := help 4 | .PHONY: help 5 | .EXPORT_ALL_VARIABLES: 6 | 7 | DOCKER_WORKSPACE := "/markdown" 8 | MOUNTS = --volume ${PWD}:${DOCKER_WORKSPACE} \ 9 | --volume ${DOCKER_WORKSPACE}/node_modules 10 | 11 | build: 12 | docker build -t markdown $(dockerfile) --build-arg REACT_VERSION=${REACT_VERSION} . 13 | 14 | # This lets us call `make run test.browser`. Make expects cmdline args 15 | # to be targets. So this creates noop targets out of args. Copied from 16 | # SO. 17 | ifeq (run,$(firstword $(MAKECMDGOALS))) 18 | RUN_ARGS := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS)) 19 | $(eval $(RUN_ARGS):;@:) 20 | endif 21 | 22 | run: build ## Run npm scripts in a docker container. (default: make test.browser) 23 | docker run -it --rm ${MOUNTS} markdown $(RUN_ARGS) 24 | 25 | ci: build ## CI runner for `npm run test.browser -- --ci` 26 | # We don't mount root because CI doesn't care about live changes, 27 | # except for grabbing the snapshot diffs, so we mount __tests__. 28 | # Mounting root would break `make emoji` in the Dockerfile. 29 | docker run -i \ 30 | --volume ${PWD}/__tests__:${DOCKER_WORKSPACE}/__tests__ \ 31 | --env NODE_VERSION=${NODE_VERSION} \ 32 | markdown test.browser -- --ci 33 | 34 | # I would like this to be `updateSnapshots` but I think it's better to 35 | # be consistent with jest. 36 | updateSnapshot: build ## Run `npm run test.browser -- --updateSnapshot` 37 | docker run -i --rm ${MOUNTS} markdown test.browser -- --updateSnapshot 38 | 39 | shell: build ## Docker shell. 40 | docker run -it --rm ${MOUNTS} --entrypoint /bin/bash markdown 41 | 42 | help: ## Show this help. 43 | @grep -E '^[a-zA-Z._-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 44 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: npx http-server example --port $PORT 2 | -------------------------------------------------------------------------------- /__mocks__/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@readme/eslint-config/testing/jest" 3 | } 4 | -------------------------------------------------------------------------------- /__mocks__/copy-to-clipboard.js: -------------------------------------------------------------------------------- 1 | const copy = jest.fn(() => true); 2 | 3 | module.exports = copy; 4 | -------------------------------------------------------------------------------- /__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 | -------------------------------------------------------------------------------- /__tests__/Glossary.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, fireEvent, screen } from '@testing-library/react'; 2 | import React from 'react'; 3 | 4 | import { Glossary } from '../components/Glossary'; 5 | 6 | test('should output a glossary item if the term exists', () => { 7 | const term = 'acme'; 8 | const definition = 'This is a definition'; 9 | const { container } = render(acme); 10 | 11 | const trigger = container.querySelector('.GlossaryItem-trigger'); 12 | expect(trigger).toHaveTextContent(term); 13 | if (trigger) { 14 | fireEvent.mouseEnter(trigger); 15 | } 16 | const tooltipContent = screen.getByText(definition, { exact: false }); 17 | expect(tooltipContent).toHaveTextContent(`${term} - ${definition}`); 18 | }); 19 | 20 | test('should be case insensitive', () => { 21 | const term = 'aCme'; 22 | const definition = 'This is a definition'; 23 | const { container } = render(acme); 24 | 25 | const trigger = container.querySelector('.GlossaryItem-trigger'); 26 | expect(trigger).toHaveTextContent('acme'); 27 | if (trigger) { 28 | fireEvent.mouseEnter(trigger); 29 | } 30 | const tooltipContent = screen.getByText(definition, { exact: false }); 31 | expect(tooltipContent).toHaveTextContent(`${term} - ${definition}`); 32 | }); 33 | 34 | test('should output the term if the definition does not exist', () => { 35 | const term = 'something'; 36 | const { container } = render({term}); 37 | 38 | expect(container.querySelector('.GlossaryItem-trigger')).not.toBeInTheDocument(); 39 | expect(container.querySelector('span')).toHaveTextContent(term); 40 | }); 41 | -------------------------------------------------------------------------------- /__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 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/html-block-parser.test.js.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`Parse html block > parses an html block 1`] = ` 4 | { 5 | "children": [ 6 | { 7 | "children": [ 8 | { 9 | "attributes": [], 10 | "children": [ 11 | { 12 | "position": { 13 | "end": { 14 | "column": 21, 15 | "line": 2, 16 | "offset": 21, 17 | }, 18 | "start": { 19 | "column": 6, 20 | "line": 2, 21 | "offset": 6, 22 | }, 23 | }, 24 | "type": "text", 25 | "value": "Some block html", 26 | }, 27 | ], 28 | "name": "div", 29 | "position": { 30 | "end": { 31 | "column": 27, 32 | "line": 2, 33 | "offset": 27, 34 | }, 35 | "start": { 36 | "column": 1, 37 | "line": 2, 38 | "offset": 1, 39 | }, 40 | }, 41 | "type": "mdxJsxTextElement", 42 | }, 43 | ], 44 | "position": { 45 | "end": { 46 | "column": 27, 47 | "line": 2, 48 | "offset": 27, 49 | }, 50 | "start": { 51 | "column": 1, 52 | "line": 2, 53 | "offset": 1, 54 | }, 55 | }, 56 | "type": "paragraph", 57 | }, 58 | ], 59 | "position": { 60 | "end": { 61 | "column": 5, 62 | "line": 3, 63 | "offset": 32, 64 | }, 65 | "start": { 66 | "column": 1, 67 | "line": 1, 68 | "offset": 0, 69 | }, 70 | }, 71 | "type": "root", 72 | } 73 | `; 74 | -------------------------------------------------------------------------------- /__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/b812eeedba99b513a04f42f577ce724513fc2b06/__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 -------------------------------------------------------------------------------- /__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/b812eeedba99b513a04f42f577ce724513fc2b06/__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-callouts-without-surprises-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readmeio/markdown/b812eeedba99b513a04f42f577ce724513fc2b06/__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-child-tests-without-surprises-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readmeio/markdown/b812eeedba99b513a04f42f577ce724513fc2b06/__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-block-tests-without-surprises-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readmeio/markdown/b812eeedba99b513a04f42f577ce724513fc2b06/__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-code-blocks-without-surprises-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readmeio/markdown/b812eeedba99b513a04f42f577ce724513fc2b06/__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-code-blocks-without-surprises-2-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readmeio/markdown/b812eeedba99b513a04f42f577ce724513fc2b06/__tests__/browser/__image_snapshots__/markdown-test-js-visual-regression-tests-rdmd-syntax-renders-code-blocks-without-surprises-2-snap.png -------------------------------------------------------------------------------- /__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/b812eeedba99b513a04f42f577ce724513fc2b06/__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-export-tests-without-surprises-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readmeio/markdown/b812eeedba99b513a04f42f577ce724513fc2b06/__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-features-without-surprises-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readmeio/markdown/b812eeedba99b513a04f42f577ce724513fc2b06/__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/b812eeedba99b513a04f42f577ce724513fc2b06/__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-html-blocks-style-tags-and-style-attributes-with-safe-mode-off-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readmeio/markdown/b812eeedba99b513a04f42f577ce724513fc2b06/__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 -------------------------------------------------------------------------------- /__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/b812eeedba99b513a04f42f577ce724513fc2b06/__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/b812eeedba99b513a04f42f577ce724513fc2b06/__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-images-without-surprises-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readmeio/markdown/b812eeedba99b513a04f42f577ce724513fc2b06/__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/b812eeedba99b513a04f42f577ce724513fc2b06/__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-mdx-components-without-surprises-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readmeio/markdown/b812eeedba99b513a04f42f577ce724513fc2b06/__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-mermaid-without-surprises-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readmeio/markdown/b812eeedba99b513a04f42f577ce724513fc2b06/__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-table-of-contents-tests-without-surprises-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readmeio/markdown/b812eeedba99b513a04f42f577ce724513fc2b06/__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-tables-tests-without-surprises-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readmeio/markdown/b812eeedba99b513a04f42f577ce724513fc2b06/__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-tables-without-surprises-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readmeio/markdown/b812eeedba99b513a04f42f577ce724513fc2b06/__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-tailwind-root-tests-without-surprises-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readmeio/markdown/b812eeedba99b513a04f42f577ce724513fc2b06/__tests__/browser/__image_snapshots__/markdown-test-js-visual-regression-tests-rdmd-syntax-renders-tailwind-root-tests-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/b812eeedba99b513a04f42f577ce724513fc2b06/__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-vars-test-without-surprises-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readmeio/markdown/b812eeedba99b513a04f42f577ce724513fc2b06/__tests__/browser/__image_snapshots__/markdown-test-js-visual-regression-tests-rdmd-syntax-renders-vars-test-without-surprises-1-snap.png -------------------------------------------------------------------------------- /__tests__/browser/markdown.test.js: -------------------------------------------------------------------------------- 1 | /* global page */ 2 | 3 | // eslint-disable-next-line no-promise-executor-return 4 | const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); 5 | 6 | describe('visual regression tests', () => { 7 | describe('rdmd syntax', () => { 8 | beforeEach(async () => { 9 | // The ToC disappears somewhere below 1200, 1175-ish? 10 | await page.setViewport({ width: 1400, height: 800 }); 11 | }); 12 | 13 | const docs = [ 14 | 'callouts', 15 | 'calloutTests', 16 | 'childTests', 17 | 'codeBlocks', 18 | // skipping this because they sporadically failure with network timing 19 | // issues 20 | // 'embeds', 21 | 'exportTests', 22 | // 'features', 23 | 'headings', 24 | 'images', 25 | 'imageTests', 26 | // 'lists', 27 | 'mdxComponents', 28 | // 'mermaid', 29 | 'tables', 30 | 'codeBlockTests', 31 | 'tableOfContentsTests', 32 | 'tailwindRootTests', 33 | 'tutorialTile', 34 | 'varsTest', 35 | ]; 36 | 37 | it.each(docs)( 38 | 'renders "%s" without surprises', 39 | async doc => { 40 | const uri = `http://localhost:9966/#/${doc}?ci=true&darkModeDataAttribute=true`; 41 | await page.goto(uri, { waitUntil: 'networkidle0' }); 42 | await sleep(5000); 43 | 44 | const image = await page.screenshot({ fullPage: true }); 45 | 46 | expect(image).toMatchImageSnapshot(); 47 | }, 48 | 10000, 49 | ); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /__tests__/browser/setup.js: -------------------------------------------------------------------------------- 1 | import { toMatchImageSnapshot } from 'jest-image-snapshot'; 2 | 3 | expect.extend({ toMatchImageSnapshot }); 4 | -------------------------------------------------------------------------------- /__tests__/compilers.test.ts: -------------------------------------------------------------------------------- 1 | import { mdast, mdx } from '../index'; 2 | 3 | describe('ReadMe Flavored Blocks', () => { 4 | it('Embed', () => { 5 | const txt = '[Embedded meta links.](https://nyti.me/s/gzoa2xb2v3 "@embed")'; 6 | const ast = mdast(txt); 7 | const out = mdx(ast); 8 | expect(out).toMatchSnapshot(); 9 | }); 10 | 11 | it('Emojis', () => { 12 | expect(mdx(mdast(':smiley:'))).toMatchInlineSnapshot(` 13 | ":smiley: 14 | " 15 | `); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /__tests__/compilers/code-tabs.test.js: -------------------------------------------------------------------------------- 1 | import { mdast, mdx } from '../../index'; 2 | 3 | describe('code-tabs compiler', () => { 4 | it('compiles code tabs', () => { 5 | const markdown = `\`\`\` 6 | const works = true; 7 | \`\`\` 8 | \`\`\` 9 | const cool = true; 10 | \`\`\` 11 | `; 12 | 13 | expect(mdx(mdast(markdown))).toBe(markdown); 14 | }); 15 | 16 | it('compiles code tabs with metadata', () => { 17 | const markdown = `\`\`\`js Testing 18 | const works = true; 19 | \`\`\` 20 | \`\`\`js 21 | const cool = true; 22 | \`\`\` 23 | `; 24 | 25 | expect(mdx(mdast(markdown))).toBe(markdown); 26 | }); 27 | 28 | it("doesnt't mess with joining other blocks", () => { 29 | const markdown = `\`\`\` 30 | const works = true; 31 | \`\`\` 32 | \`\`\` 33 | const cool = true; 34 | \`\`\` 35 | 36 | ## Hello! 37 | 38 | I should stay here 39 | `; 40 | 41 | expect(mdx(mdast(markdown))).toBe(markdown); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /__tests__/compilers/escape.test.js: -------------------------------------------------------------------------------- 1 | import { mdast, mdx } 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 | -------------------------------------------------------------------------------- /__tests__/compilers/gemoji.test.ts: -------------------------------------------------------------------------------- 1 | import { mdast, mdx } from '../../index'; 2 | 3 | describe('gemoji compiler', () => { 4 | it('should compile back to a shortcode', () => { 5 | const markdown = 'This is a gemoji :joy:.'; 6 | 7 | expect(mdx(mdast(markdown)).trimEnd()).toStrictEqual(markdown); 8 | }); 9 | 10 | it('should compile owlmoji back to a shortcode', () => { 11 | const markdown = ':owlbert:'; 12 | 13 | expect(mdx(mdast(markdown)).trimEnd()).toStrictEqual(markdown); 14 | }); 15 | 16 | it('should compile font-awsome emojis back to a shortcode', () => { 17 | const markdown = ':fa-readme:'; 18 | 19 | expect(mdx(mdast(markdown)).trimEnd()).toStrictEqual(markdown); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /__tests__/compilers/html-block.test.ts: -------------------------------------------------------------------------------- 1 | import { mdast, mdx } from '../../index'; 2 | 3 | describe('html-block compiler', () => { 4 | it('compiles html blocks within containers', () => { 5 | const markdown = ` 6 | > 🚧 It compiles! 7 | > 8 | > {\` 9 | > Hello, World! 10 | > \`} 11 | `; 12 | 13 | expect(mdx(mdast(markdown)).trim()).toBe(markdown.trim()); 14 | }); 15 | 16 | it('compiles html blocks preserving newlines', () => { 17 | const markdown = ` 18 | {\` 19 |

20 | const foo = () => {
21 |   const bar = {
22 |     baz: 'blammo'
23 |   }
24 | 
25 |   return bar
26 | }
27 | 
28 | \`}
29 | `; 30 | 31 | expect(mdx(mdast(markdown)).trim()).toBe(markdown.trim()); 32 | }); 33 | 34 | it('adds newlines for readability', () => { 35 | const markdown = '{`

Hello, World!

`}
'; 36 | const expected = `{\` 37 |

Hello, World!

38 | \`}
`; 39 | 40 | expect(mdx(mdast(markdown)).trim()).toBe(expected.trim()); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /__tests__/compilers/images.test.ts: -------------------------------------------------------------------------------- 1 | import { mdast, mdx } from '../../index'; 2 | 3 | describe('image compiler', () => { 4 | it('correctly serializes an image back to markdown', () => { 5 | const txt = '![alt text](/path/to/image.png)'; 6 | 7 | expect(mdx(mdast(txt))).toMatch(txt); 8 | }); 9 | 10 | it('correctly serializes an inline image back to markdown', () => { 11 | const txt = 'Forcing it to be inline: ![alt text](/path/to/image.png)'; 12 | 13 | expect(mdx(mdast(txt))).toMatch(txt); 14 | }); 15 | 16 | it('correctly serializes an Image component back to MDX', () => { 17 | const doc = 'alt text'; 18 | 19 | expect(mdx(mdast(doc))).toMatch(doc); 20 | }); 21 | 22 | it('ignores empty (undefined, null, or "") attributes', () => { 23 | const doc = ''; 24 | 25 | expect(mdx(mdast(doc))).toMatch(''); 26 | }); 27 | 28 | it('correctly serializes an Image component with expression attributes back to MDX', () => { 29 | const doc = ''; 30 | 31 | expect(mdx(mdast(doc))).toMatch('![](/path/to/image.png)'); 32 | 33 | const doc2 = ''; 34 | 35 | expect(mdx(mdast(doc2))).toMatch(''); 36 | }); 37 | 38 | it('correctly serializes an Image component with an undefined expression attributes back to MDX', () => { 39 | const doc = ''; 40 | 41 | expect(mdx(mdast(doc))).toMatch('![]()'); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /__tests__/compilers/links.test.ts: -------------------------------------------------------------------------------- 1 | import { mdast, mdx } from '../../index'; 2 | 3 | describe('link compiler', () => { 4 | it('compiles links without extra attributes', () => { 5 | const markdown = 'ReadMe'; 6 | 7 | expect(mdx(mdast(markdown)).trim()).toBe('[ReadMe](https://readme.com)'); 8 | }); 9 | 10 | it('compiles links with extra attributes', () => { 11 | const markdown = 'ReadMe'; 12 | 13 | expect(mdx(mdast(markdown)).trim()).toBe(markdown); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /__tests__/compilers/plain.test.ts: -------------------------------------------------------------------------------- 1 | import type { Paragraph, Root, RootContent } from 'mdast'; 2 | 3 | import { mdx } from '../../index'; 4 | 5 | describe('plain compiler', () => { 6 | it('compiles plain nodes', () => { 7 | const md = "- this is and isn't a list"; 8 | const ast: Root = { 9 | type: 'root', 10 | children: [ 11 | { 12 | type: 'paragraph', 13 | children: [ 14 | { 15 | type: 'plain', 16 | value: md, 17 | }, 18 | ], 19 | } as Paragraph, 20 | ], 21 | }; 22 | 23 | expect(mdx(ast)).toBe(`${md}\n`); 24 | }); 25 | 26 | it('compiles plain nodes and does not escape characters', () => { 27 | const md = ''; 28 | const ast: Root = { 29 | type: 'root', 30 | children: [ 31 | { 32 | type: 'paragraph', 33 | children: [ 34 | { 35 | type: 'plain', 36 | value: md, 37 | }, 38 | ], 39 | } as Paragraph, 40 | ], 41 | }; 42 | 43 | expect(mdx(ast)).toBe(`${md}\n`); 44 | }); 45 | 46 | it('compiles plain nodes at the root level', () => { 47 | const md = "- this is and isn't a list"; 48 | const ast: Root = { 49 | type: 'root', 50 | children: [ 51 | { 52 | type: 'plain', 53 | value: md, 54 | }, 55 | ] as RootContent[], 56 | }; 57 | 58 | expect(mdx(ast)).toBe(`${md}\n`); 59 | }); 60 | 61 | it('compiles plain nodes in an inline context', () => { 62 | const ast: Root = { 63 | type: 'root', 64 | children: [ 65 | { 66 | type: 'paragraph', 67 | children: [ 68 | { type: 'text', value: 'before' }, 69 | { 70 | type: 'plain', 71 | value: ' plain ', 72 | }, 73 | { type: 'text', value: 'after' }, 74 | ], 75 | }, 76 | ] as RootContent[], 77 | }; 78 | 79 | expect(mdx(ast)).toBe('before plain after\n'); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /__tests__/compilers/reusable-content.test.js: -------------------------------------------------------------------------------- 1 | import { mdast, mdx } from '../../index'; 2 | 3 | describe.skip('reusable content compiler', () => { 4 | it('writes an undefined reusable content block as a tag', () => { 5 | const doc = ''; 6 | const tree = mdast(doc); 7 | 8 | expect(mdx(tree)).toMatch(doc); 9 | }); 10 | 11 | it('writes a defined reusable content block as a tag', () => { 12 | const tags = { 13 | Defined: '# Whoa', 14 | }; 15 | const doc = ''; 16 | const tree = mdast(doc, { reusableContent: { tags } }); 17 | 18 | expect(tree.children[0].children[0].type).toBe('heading'); 19 | expect(mdx(tree)).toMatch(doc); 20 | }); 21 | 22 | it('writes a defined reusable content block with multiple words as a tag', () => { 23 | const tags = { 24 | MyCustomComponent: '# Whoa', 25 | }; 26 | const doc = ''; 27 | const tree = mdast(doc, { reusableContent: { tags } }); 28 | 29 | expect(tree.children[0].children[0].type).toBe('heading'); 30 | expect(mdx(tree)).toMatch(doc); 31 | }); 32 | 33 | describe('serialize = false', () => { 34 | it('writes a reusable content block as content', () => { 35 | const tags = { 36 | Defined: '# Whoa', 37 | }; 38 | const doc = ''; 39 | const string = mdx(doc, { reusableContent: { tags, serialize: false } }); 40 | 41 | expect(string).toBe('# Whoa\n'); 42 | }); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /__tests__/compilers/variable.test.ts: -------------------------------------------------------------------------------- 1 | import * as rmdx from '../../index'; 2 | 3 | describe('variable compiler', () => { 4 | it('compiles back to the original mdx', () => { 5 | const mdx = ` 6 | ## Hello! 7 | 8 | {user.name} 9 | 10 | ### Bye bye! 11 | `; 12 | const tree = rmdx.mdast(mdx); 13 | 14 | expect(rmdx.mdx(tree).trim()).toStrictEqual(mdx.trim()); 15 | }); 16 | 17 | it('with spaces in a variable, it compiles back to the original', () => { 18 | const mdx = '{user["oh no"]}'; 19 | const tree = rmdx.mdast(mdx); 20 | 21 | expect(rmdx.mdx(tree).trim()).toStrictEqual(mdx.trim()); 22 | }); 23 | 24 | it('with dashes in a variable name, it compiles back to the original', () => { 25 | const mdx = '{user["oh-no"]}'; 26 | const tree = rmdx.mdast(mdx); 27 | 28 | expect(rmdx.mdx(tree).trim()).toStrictEqual(mdx.trim()); 29 | }); 30 | 31 | it('with unicode in the variable name, it compiles back to the original', () => { 32 | const mdx = '{user.nuñez}'; 33 | const tree = rmdx.mdast(mdx); 34 | 35 | expect(rmdx.mdx(tree).trim()).toStrictEqual(mdx.trim()); 36 | }); 37 | 38 | it('with quotes in the variable name, it compiles back to the original', () => { 39 | const mdx = '{user[`"\'wth`]}'; 40 | const tree = rmdx.mdast(mdx); 41 | 42 | expect(rmdx.mdx(tree).trim()).toStrictEqual(mdx.trim()); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /__tests__/compilers/yaml.test.js: -------------------------------------------------------------------------------- 1 | import { mdast, mdx } 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 | -------------------------------------------------------------------------------- /__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 | -------------------------------------------------------------------------------- /__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__/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 | -------------------------------------------------------------------------------- /__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__/components/Image.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import React from 'react'; 3 | 4 | import Image from '../../components/Image'; 5 | 6 | describe('Image', () => { 7 | it('should render', () => { 8 | render(); 9 | 10 | expect(screen.getByRole('img')).toMatchInlineSnapshot(` 11 | 20 | `); 21 | }); 22 | 23 | it('should render as a figure/figcaption if it has a caption', () => { 24 | render( 25 | 31 | ); 32 | 33 | expect(screen.getByRole('button')).toMatchInlineSnapshot(` 34 | 40 | 43 | 52 |
53 | A pizza bro 54 |
55 |
56 |
57 | `); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /__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 | -------------------------------------------------------------------------------- /__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 | -------------------------------------------------------------------------------- /__tests__/fixtures/callout-tests.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Callouts Tests' 3 | category: 5fdf9fc9c2a7ef443e937315 4 | hidden: true 5 | --- 6 | 7 | Default 8 | Success 9 | Info 10 | Warn 11 | Error 12 | 13 | > 👍 Success 14 | > 15 | > This is the success callout. 16 | 17 | > 📘 Info 18 | > 19 | > This is the info callout. 20 | 21 | > 🚧 Warn 22 | > 23 | > This is the warn callout. 24 | 25 | > ❗ Error 26 | > 27 | > This is the error callout. 28 | 29 | > 👎 Markdown in callouts 30 | > 31 | > Unordered List 32 | > 33 | > - List Item 1 34 | > - List Item 2 35 | 36 | 37 | MDX Callout 38 | 39 | --- 40 | 41 | With Markdown support. 42 | 43 | 44 | > ❗ 45 | > 46 | > Description Only 47 | 48 | > ❔ Title Only _with italics_ 49 | -------------------------------------------------------------------------------- /__tests__/fixtures/child-tests.mdx: -------------------------------------------------------------------------------- 1 | 2 | Step One 3 | Step Two 4 | 5 | -------------------------------------------------------------------------------- /__tests__/fixtures/code-block-tests.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Code Block Tests' 3 | category: 5fdf9fc9c2a7ef443e937315 4 | hidden: true 5 | --- 6 | 7 | # Basics 8 | 9 | ### Simple 10 | 11 | ```php 12 | ; 13 | ``` 14 | ```js 15 | console.log('Hello world!'); 16 | ``` 17 | 18 | ### Tab Meta 19 | 20 | ```Zed 21 | Tab Number Zero 22 | ``` 23 | ```One 24 | Tab Number One 25 | ``` 26 | 27 | ### Lang Meta 28 | 29 | ```js English 30 | console.log('Hello world!'); 31 | ``` 32 | ```js French 33 | console.log('Bonjour le monde!'); 34 | ``` 35 | ```js German 36 | console.log('Hallo welt!'); 37 | ``` 38 | 39 | # Breakage 40 | 41 | ### Block Separator 👍 42 | 43 | ##### Section One 44 | 45 | ```Plain 46 | console.log("zed"); 47 | ``` 48 | 49 | ##### Section Two 50 | 51 | ```js Highlighted 52 | console.log('one'); 53 | ``` 54 | 55 | `Hello` the `world`? 56 | 57 | ### Inline Separator 👍 58 | 59 | **Section One** 60 | 61 | ```Plain 62 | console.log("zed"); 63 | ``` 64 | 65 | **Section Two** 66 | 67 | ```js Highlighted 68 | console.log('one'); 69 | ``` 70 | 71 | ### Plain-Text Separator 72 | 73 | Section One 74 | 75 | ```Plain 76 | console.log("zed"); 77 | ``` 78 | 79 | Section **Two** 80 | 81 | ```js Highlighted 82 | console.log('one'); 83 | ``` 84 | 85 | # Block Wraps 86 | 87 | ### List-Internal 88 | 89 | - ```Name 90 | {{company_name}} 91 | ``` 92 | ```Email 93 | {{company_email}} 94 | ``` 95 | ```URL 96 | {{company_url}} 97 | ``` 98 | -------------------------------------------------------------------------------- /__tests__/fixtures/export-tests.mdx: -------------------------------------------------------------------------------- 1 | ## Same component file, but different export name 2 | 3 | ``` 4 | - 5 | - 6 | ``` 7 | 8 | - 9 | - 10 | -------------------------------------------------------------------------------- /__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 | -------------------------------------------------------------------------------- /__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 | -------------------------------------------------------------------------------- /__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__/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 | -------------------------------------------------------------------------------- /__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 | -------------------------------------------------------------------------------- /__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__/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 | -------------------------------------------------------------------------------- /__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__/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 | }); -------------------------------------------------------------------------------- /__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 | -------------------------------------------------------------------------------- /__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/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 | -------------------------------------------------------------------------------- /__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/mdast/__snapshots__/anchor.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`convert anchor tag > converts anchor tag to link node 1`] = ` 4 | { 5 | "children": [ 6 | { 7 | "children": [ 8 | { 9 | "children": [ 10 | { 11 | "position": { 12 | "end": { 13 | "column": 41, 14 | "line": 2, 15 | "offset": 41, 16 | }, 17 | "start": { 18 | "column": 35, 19 | "line": 2, 20 | "offset": 35, 21 | }, 22 | }, 23 | "type": "text", 24 | "value": "ReadMe", 25 | }, 26 | ], 27 | "position": { 28 | "end": { 29 | "column": 50, 30 | "line": 2, 31 | "offset": 50, 32 | }, 33 | "start": { 34 | "column": 1, 35 | "line": 2, 36 | "offset": 1, 37 | }, 38 | }, 39 | "type": "link", 40 | "url": "https://readme.com", 41 | }, 42 | ], 43 | "position": { 44 | "end": { 45 | "column": 50, 46 | "line": 2, 47 | "offset": 50, 48 | }, 49 | "start": { 50 | "column": 1, 51 | "line": 2, 52 | "offset": 1, 53 | }, 54 | }, 55 | "type": "paragraph", 56 | }, 57 | ], 58 | "position": { 59 | "end": { 60 | "column": 5, 61 | "line": 3, 62 | "offset": 55, 63 | }, 64 | "start": { 65 | "column": 1, 66 | "line": 1, 67 | "offset": 0, 68 | }, 69 | }, 70 | "type": "root", 71 | } 72 | `; 73 | -------------------------------------------------------------------------------- /__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__/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 | -------------------------------------------------------------------------------- /__tests__/lib/mdast/images/inline/in.mdx: -------------------------------------------------------------------------------- 1 | This should work Captioned. 2 | -------------------------------------------------------------------------------- /__tests__/lib/mdast/images/inline/out.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "root", 3 | "children": [ 4 | { 5 | "type": "paragraph", 6 | "children": [ 7 | { 8 | "type": "text", 9 | "value": "This should work " 10 | }, 11 | { 12 | "align": "left", 13 | "border": true, 14 | "src": "https://media.giphy.com/media/3o7TKz9bX9v6hZ8NSA/giphy.gif", 15 | "width": "50%", 16 | "alt": "", 17 | "children": [ 18 | { 19 | "type": "text", 20 | "value": "Captioned" 21 | } 22 | ], 23 | "title": null, 24 | "type": "image-block", 25 | "data": { 26 | "hName": "img", 27 | "hProperties": { 28 | "align": "left", 29 | "border": true, 30 | "src": "https://media.giphy.com/media/3o7TKz9bX9v6hZ8NSA/giphy.gif", 31 | "width": "50%", 32 | "alt": "", 33 | "children": [ 34 | { 35 | "type": "text", 36 | "value": "Captioned", 37 | "position": { 38 | "start": { 39 | "line": 1, 40 | "column": 129, 41 | "offset": 128 42 | }, 43 | "end": { 44 | "line": 1, 45 | "column": 138, 46 | "offset": 137 47 | } 48 | } 49 | } 50 | ], 51 | "title": null 52 | } 53 | } 54 | }, 55 | { 56 | "type": "text", 57 | "value": "." 58 | } 59 | ] 60 | } 61 | ] 62 | } 63 | -------------------------------------------------------------------------------- /__tests__/lib/mdast/null-attributes/in.mdx: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /__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/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__/lib/mdast/variables-with-spaces/in.mdx: -------------------------------------------------------------------------------- 1 | Hello, { user['this is cursed'] } 2 | -------------------------------------------------------------------------------- /__tests__/lib/mdast/variables-with-spaces/out.json: -------------------------------------------------------------------------------- 1 | { 2 | "children": [ 3 | { 4 | "children": [ 5 | { 6 | "position": { 7 | "end": { 8 | "column": 8, 9 | "line": 1, 10 | "offset": 7 11 | }, 12 | "start": { 13 | "column": 1, 14 | "line": 1, 15 | "offset": 0 16 | } 17 | }, 18 | "type": "text", 19 | "value": "Hello, " 20 | }, 21 | { 22 | "data": { 23 | "hName": "Variable", 24 | "hProperties": { 25 | "name": "this is cursed" 26 | } 27 | }, 28 | "position": { 29 | "end": { 30 | "column": 19, 31 | "line": 1, 32 | "offset": 18 33 | }, 34 | "start": { 35 | "column": 8, 36 | "line": 1, 37 | "offset": 7 38 | } 39 | }, 40 | "type": "readme-variable", 41 | "value": "{ user['this is cursed'] }" 42 | } 43 | ], 44 | "position": { 45 | "end": { 46 | "column": 19, 47 | "line": 1, 48 | "offset": 18 49 | }, 50 | "start": { 51 | "column": 1, 52 | "line": 1, 53 | "offset": 0 54 | } 55 | }, 56 | "type": "paragraph" 57 | } 58 | ], 59 | "position": { 60 | "end": { 61 | "column": 1, 62 | "line": 2, 63 | "offset": 19 64 | }, 65 | "start": { 66 | "column": 1, 67 | "line": 1, 68 | "offset": 0 69 | } 70 | }, 71 | "type": "root" 72 | } 73 | -------------------------------------------------------------------------------- /__tests__/lib/mdast/variables/in.mdx: -------------------------------------------------------------------------------- 1 | Hello, {user.name} 2 | -------------------------------------------------------------------------------- /__tests__/lib/mdast/variables/out.json: -------------------------------------------------------------------------------- 1 | { 2 | "children": [ 3 | { 4 | "children": [ 5 | { 6 | "position": { 7 | "end": { 8 | "column": 8, 9 | "line": 1, 10 | "offset": 7 11 | }, 12 | "start": { 13 | "column": 1, 14 | "line": 1, 15 | "offset": 0 16 | } 17 | }, 18 | "type": "text", 19 | "value": "Hello, " 20 | }, 21 | { 22 | "data": { 23 | "hName": "Variable", 24 | "hProperties": { 25 | "name": "name" 26 | } 27 | }, 28 | "position": { 29 | "end": { 30 | "column": 19, 31 | "line": 1, 32 | "offset": 18 33 | }, 34 | "start": { 35 | "column": 8, 36 | "line": 1, 37 | "offset": 7 38 | } 39 | }, 40 | "type": "readme-variable", 41 | "value": "{user.name}" 42 | } 43 | ], 44 | "position": { 45 | "end": { 46 | "column": 19, 47 | "line": 1, 48 | "offset": 18 49 | }, 50 | "start": { 51 | "column": 1, 52 | "line": 1, 53 | "offset": 0 54 | } 55 | }, 56 | "type": "paragraph" 57 | } 58 | ], 59 | "position": { 60 | "end": { 61 | "column": 1, 62 | "line": 2, 63 | "offset": 19 64 | }, 65 | "start": { 66 | "column": 1, 67 | "line": 1, 68 | "offset": 0 69 | } 70 | }, 71 | "type": "root" 72 | } 73 | -------------------------------------------------------------------------------- /__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 | -------------------------------------------------------------------------------- /__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 | -------------------------------------------------------------------------------- /__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 | -------------------------------------------------------------------------------- /__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__/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__/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__/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 | -------------------------------------------------------------------------------- /__tests__/migration/html-blocks.test.ts: -------------------------------------------------------------------------------- 1 | import { migrate } from '../helpers'; 2 | 3 | describe('migrating html blocks', () => { 4 | it('correctly escapes back ticks', () => { 5 | const md = ` 6 | [block:html] 7 | { 8 | "html": "\`example.com\`" 9 | } 10 | [/block] 11 | `; 12 | 13 | const mdx = migrate(md); 14 | expect(mdx).toMatchInlineSnapshot(` 15 | "{\` 16 | \\\`example.com\\\` 17 | \`} 18 | " 19 | `); 20 | }); 21 | 22 | it('does not escape already escaped backticks', () => { 23 | const md = ` 24 | [block:html] 25 | { 26 | "html": "${'\\\\`example.com\\\\`'}" 27 | } 28 | [/block] 29 | `; 30 | 31 | const mdx = migrate(md); 32 | expect(mdx).toMatchInlineSnapshot(` 33 | "{\` 34 | \\\`example.com\\\` 35 | \`} 36 | " 37 | `); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /__tests__/migration/html-comments.test.ts: -------------------------------------------------------------------------------- 1 | import { migrate } from '../helpers'; 2 | 3 | describe('migrating html comments', () => { 4 | it('migrates escaped html comments', () => { 5 | const md = ` 6 | 22 | `; 23 | 24 | const mdx = migrate(md); 25 | expect(mdx).toMatchInlineSnapshot(` 26 | "{/* 27 | 28 | 29 | 30 | ## Walkthrough 31 | 32 | 33 | [block:html] 34 | { 35 | "html": "
" 36 | } 37 | [/block] 38 | 39 | 40 | 41 |
42 | 43 | */} 44 | " 45 | `); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /__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 | -------------------------------------------------------------------------------- /__tests__/migration/image.test.ts: -------------------------------------------------------------------------------- 1 | import { migrate } from '../helpers'; 2 | 3 | describe('migrating images', () => { 4 | it('compiles images', () => { 5 | const md = ` 6 | [block:image] 7 | { 8 | "images": [ 9 | { 10 | "image": [ 11 | "https://fastly.picsum.photos/id/507/200/300.jpg?hmac=v0NKvUrOWTKZuZFmMlLN_7-RdRgeF-qFLeBGXpufxgg", 12 | "", 13 | "" 14 | ], 15 | "align": "center", 16 | "border": true 17 | } 18 | ] 19 | } 20 | [/block] 21 | `; 22 | 23 | const mdx = migrate(md); 24 | expect(mdx).toMatchInlineSnapshot(` 25 | " 26 | " 27 | `); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /__tests__/migration/link-reference.test.ts: -------------------------------------------------------------------------------- 1 | import { migrate } from '../helpers'; 2 | 3 | describe('mdx migration of link references', () => { 4 | it('compiles link references correctly', () => { 5 | const md = '[wat_wat]'; 6 | 7 | const mdx = migrate(md); 8 | expect(mdx).toMatchInlineSnapshot(` 9 | "\\[wat\\_wat] 10 | " 11 | `); 12 | }); 13 | 14 | it('compiles link references with defintions correctly', () => { 15 | const md = ` 16 | [wat_wat] 17 | 18 | [wat_wat]: https://wat.com 19 | `; 20 | 21 | const mdx = migrate(md); 22 | expect(mdx).toMatchInlineSnapshot(` 23 | "[wat\\_wat][wat_wat] 24 | 25 | [wat_wat]: https://wat.com 26 | " 27 | `); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /__tests__/migration/magic-block.test.ts: -------------------------------------------------------------------------------- 1 | import { migrate } from '../helpers'; 2 | 3 | describe('migrating magic blocks', () => { 4 | it('compiles magic blocks without enough newlines', () => { 5 | const md = ` 6 | [block:api-header] 7 | { 8 | "title": "About cBEYONData" 9 | } 10 | [/block] 11 | [ Overview of cBEYONData ](/docs/about-cbeyondata) 12 | [block:api-header] 13 | { 14 | "title": "About CFO Control Control Tower" 15 | } 16 | [/block] 17 | [Overview of CFO Control Tower](https://docs.cfocontroltower.com/docs/about-cfo-control-tower) 18 | [block:image] 19 | { 20 | "images": [ 21 | { 22 | "image": [ 23 | "https://files.readme.io/569fe58-Intro_Image02.png", 24 | "Intro Image02.png", 25 | 1280, 26 | 118, 27 | "#eaeaed" 28 | ], 29 | "sizing": "full", 30 | "caption": "cBEYONData.com" 31 | } 32 | ] 33 | } 34 | [/block] 35 | 36 | [block:callout] 37 | { 38 | "type": "danger", 39 | "title": "CONFIDENTIAL", 40 | "body": "*This documentation is confidential and proprietary information of cBEYONData LLC.* " 41 | } 42 | [/block] 43 | `; 44 | const mdx = migrate(md); 45 | expect(mdx).toMatchInlineSnapshot(` 46 | "## About cBEYONData 47 | 48 | [ Overview of cBEYONData ](/docs/about-cbeyondata) 49 | 50 | ## About CFO Control Control Tower 51 | 52 | [Overview of CFO Control Tower](https://docs.cfocontroltower.com/docs/about-cfo-control-tower) 53 | 54 | > ❗️ CONFIDENTIAL 55 | > 56 | > *This documentation is confidential and proprietary information of cBEYONData LLC.* 57 | " 58 | `); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /__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 | -------------------------------------------------------------------------------- /__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 | -------------------------------------------------------------------------------- /__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 | -------------------------------------------------------------------------------- /__tests__/parsers/gemoji.test.ts: -------------------------------------------------------------------------------- 1 | import { mdast } from '../../index'; 2 | 3 | describe('gemoji parser', () => { 4 | it('should output an emoji node for a known emoji', () => { 5 | const markdown = 'This is a gemoji :joy:.'; 6 | const tree = mdast(markdown); 7 | 8 | expect(tree.children[0].children[1]).toMatchInlineSnapshot(` 9 | { 10 | "name": "joy", 11 | "type": "gemoji", 12 | "value": "😂", 13 | } 14 | `); 15 | }); 16 | 17 | it('should output an image node for a readme emoji', () => { 18 | const markdown = 'This is a gemoji :owlbert:.'; 19 | 20 | expect(mdast(markdown).children[0].children[1]).toMatchInlineSnapshot(` 21 | { 22 | "alt": ":owlbert:", 23 | "data": { 24 | "hProperties": { 25 | "align": "absmiddle", 26 | "className": "emoji", 27 | "height": "20", 28 | "width": "20", 29 | }, 30 | }, 31 | "title": ":owlbert:", 32 | "type": "image", 33 | "url": "/public/img/emojis/owlbert.png", 34 | } 35 | `); 36 | }); 37 | 38 | it('should output an for a font awesome icon', () => { 39 | const markdown = 'This is a gemoji :fa-lock:.'; 40 | const tree = mdast(markdown); 41 | 42 | expect(tree.children[0].children[1]).toMatchInlineSnapshot(` 43 | { 44 | "data": { 45 | "hName": "i", 46 | "hProperties": { 47 | "className": [ 48 | "fa-regular", 49 | "fa-lock", 50 | ], 51 | }, 52 | }, 53 | "type": "i", 54 | "value": "fa-lock", 55 | } 56 | `); 57 | }); 58 | 59 | it('should output nothing for unknown emojis', () => { 60 | const markdown = 'This is a gemoji :unknown-emoji:.'; 61 | 62 | expect(mdast(markdown).children[0].children[0].value).toMatch(/:unknown-emoji:/); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /__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 | -------------------------------------------------------------------------------- /__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 | -------------------------------------------------------------------------------- /__tests__/transformers/__snapshots__/table-cell-inline-code.test.js.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`tableCellInlineCode > preserves escaped pipe chars inside text table cells 1`] = `undefined`; 4 | 5 | exports[`tableCellInlineCode > splits table cells when inline code contains "unescaped" pipe chars 1`] = `undefined`; 6 | 7 | exports[`tableCellInlineCode > unescapes escaped pipe chars inside inline code within table cells 1`] = `undefined`; 8 | 9 | exports[`tableCellInlineCode > unescapes escaped pipe chars inside inline code within table headers 1`] = `undefined`; 10 | -------------------------------------------------------------------------------- /__tests__/transformers/callouts.test.ts: -------------------------------------------------------------------------------- 1 | import { mdast } from '../../index'; 2 | 3 | describe('callouts transformer', () => { 4 | it('can parse callouts', () => { 5 | const md = ` 6 | > 🚧 It works! 7 | > 8 | > And, it no longer deletes your content! 9 | `; 10 | const tree = mdast(md); 11 | 12 | expect(tree.children[0].type).toBe('rdme-callout'); 13 | expect(tree.children[0].children[0].type).toBe('paragraph'); 14 | expect(tree.children[0].children[0].children[0].value).toBe('It works!'); 15 | }); 16 | 17 | it('can parse callouts with markdown in the heading', () => { 18 | const md = ` 19 | > 🚧 It **works!** 20 | > 21 | > And, it no longer deletes your content! 22 | `; 23 | const tree = mdast(md); 24 | 25 | expect(tree.children[0].children[0].children[1].type).toBe('strong'); 26 | }); 27 | 28 | it('can parse callouts with markdown in the heading immediately following the emoji', () => { 29 | const md = ` 30 | > 🚧 **It works!** 31 | > 32 | > And, it no longer deletes your content! 33 | `; 34 | const tree = mdast(md); 35 | 36 | expect(tree.children[0].data.hProperties.empty).toBeUndefined(); 37 | expect(tree.children[0].children[0].children[1].type).toBe('strong'); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /__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 | -------------------------------------------------------------------------------- /__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 | -------------------------------------------------------------------------------- /__tests__/transformers/readme-to-mdx.test.ts: -------------------------------------------------------------------------------- 1 | import { mdx } from '../../index'; 2 | 3 | describe('readme-to-mdx transformer', () => { 4 | it('converts a tutorial tile to MDX', () => { 5 | const ast = { 6 | type: 'root', 7 | children: [ 8 | { 9 | type: 'tutorial-tile', 10 | backgroundColor: 'red', 11 | emoji: '🦉', 12 | id: 'test-id', 13 | link: 'http://example.com', 14 | slug: 'test-id', 15 | title: 'Test', 16 | }, 17 | ], 18 | }; 19 | 20 | expect(mdx(ast)).toMatchInlineSnapshot(` 21 | " 22 | " 23 | `); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /__tests__/transformers/table-cell-inline-code.test.js: -------------------------------------------------------------------------------- 1 | import { hast, mdx, mdast } from '../../index'; 2 | 3 | describe('tableCellInlineCode', () => { 4 | it.skip('unescapes escaped pipe chars inside inline code within table headers', () => { 5 | const doc = ` 6 | | \`one \\| two \\| three \\| four\` | two | 7 | | :- | :- | 8 | `; 9 | 10 | const tree = hast(doc); 11 | expect(tree).toMatchSnapshot(); 12 | }); 13 | 14 | it.skip('unescapes escaped pipe chars inside inline code within table cells', () => { 15 | const doc = ` 16 | | | | 17 | | :- | :- | 18 | | \`one \\| two \\| three \\| four\` | two | 19 | `; 20 | 21 | const tree = hast(doc); 22 | expect(tree).toMatchSnapshot(); 23 | }); 24 | 25 | it.skip('preserves escaped pipe chars inside text table cells', () => { 26 | const doc = ` 27 | | these \\| stay \\| escaped \\| inside \\| a single cell | | 28 | | :- | :- | 29 | `; 30 | 31 | const tree = hast(doc); 32 | expect(tree).toMatchSnapshot(); 33 | }); 34 | 35 | it.skip('splits table cells when inline code contains "unescaped" pipe chars', () => { 36 | const doc = ` 37 | | \`this | splits | up | to | more | cells\` | two | 38 | | :- | :- | 39 | `; 40 | 41 | const tree = hast(doc); 42 | expect(tree).toMatchSnapshot(); 43 | }); 44 | 45 | it.skip('preserves the escaped pipe character when re-serializing from mdast', () => { 46 | const doc = ` 47 | | \`one \\| two \\| three \\| four\` | two | 48 | | :- | :- | 49 | `; 50 | 51 | const tree = mdast(doc); 52 | expect(mdx(tree)).toMatchInlineSnapshot(` 53 | "| \`one \\| two \\| three \\| four\` | two | 54 | | :- | :- | 55 | " 56 | `); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /__tests__/transformers/variables.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import React from 'react'; 3 | 4 | import * as rmdx from '../../index'; 5 | import { execute } from '../helpers'; 6 | 7 | describe('variables transformer', () => { 8 | it('renders user variables', () => { 9 | const mdx = '{user.name}'; 10 | const variables = { 11 | user: { 12 | name: 'Test User', 13 | }, 14 | }; 15 | const Content = execute(mdx, { variables }) as () => React.ReactNode; 16 | 17 | render(); 18 | 19 | expect(screen.findByText('Test User')).toBeDefined(); 20 | }); 21 | 22 | it('renders user variables in a phrasing context', () => { 23 | const mdx = 'Hello, {user.name}!'; 24 | const variables = { 25 | user: { 26 | name: 'Test User', 27 | }, 28 | }; 29 | const Content = execute(mdx, { variables }) as () => React.ReactNode; 30 | 31 | render(); 32 | 33 | expect(screen.findByText('Test User')).toBeDefined(); 34 | }); 35 | 36 | it('parses variables into the mdast', () => { 37 | const mdx = '{user.name}'; 38 | 39 | // @ts-expect-error - custom matcher types aren't set up right 40 | expect(rmdx.mdast(mdx)).toStrictEqualExceptPosition({ 41 | children: [ 42 | { 43 | value: '{user.name}', 44 | data: { 45 | hName: 'Variable', 46 | hProperties: { 47 | name: 'name', 48 | }, 49 | }, 50 | type: 'readme-variable', 51 | }, 52 | ], 53 | type: 'root', 54 | }); 55 | }); 56 | 57 | it('does not parse regular expressions into variables', () => { 58 | const mdx = '{notUser.name}'; 59 | 60 | expect(rmdx.mdast(mdx).children[0].type).toBe('mdxFlowExpression'); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /__tests__/types.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.mdx?raw' { 2 | const value: string; 3 | export default value; 4 | } 5 | -------------------------------------------------------------------------------- /__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 | -------------------------------------------------------------------------------- /assets/img/emojis/owlbert-books.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readmeio/markdown/b812eeedba99b513a04f42f577ce724513fc2b06/assets/img/emojis/owlbert-books.png -------------------------------------------------------------------------------- /assets/img/emojis/owlbert-mask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readmeio/markdown/b812eeedba99b513a04f42f577ce724513fc2b06/assets/img/emojis/owlbert-mask.png -------------------------------------------------------------------------------- /assets/img/emojis/owlbert-reading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readmeio/markdown/b812eeedba99b513a04f42f577ce724513fc2b06/assets/img/emojis/owlbert-reading.png -------------------------------------------------------------------------------- /assets/img/emojis/owlbert-thinking.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readmeio/markdown/b812eeedba99b513a04f42f577ce724513fc2b06/assets/img/emojis/owlbert-thinking.png -------------------------------------------------------------------------------- /assets/img/emojis/owlbert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readmeio/markdown/b812eeedba99b513a04f42f577ce724513fc2b06/assets/img/emojis/owlbert.png -------------------------------------------------------------------------------- /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 ? 3 : 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /components/Callout/index.tsx: -------------------------------------------------------------------------------- 1 | import emojiRegex from 'emoji-regex'; 2 | import * as React from 'react'; 3 | 4 | interface Props extends React.PropsWithChildren> { 5 | attributes?: Record; 6 | empty?: boolean; 7 | icon?: string; 8 | theme: string; 9 | } 10 | 11 | export const themes: Record = { 12 | 'error': 'error', 13 | 'default': 'default', 14 | 'info': 'info', 15 | 'okay': 'okay', 16 | 'warn': 'warn', 17 | '\uD83D\uDCD8': 'info', 18 | '\uD83D\uDEA7': 'warn', 19 | '\u26A0\uFE0F': 'warn', 20 | '\uD83D\uDC4D': 'okay', 21 | '\u2705': 'okay', 22 | '\u2757\uFE0F': 'error', 23 | '\u2757': 'error', 24 | '\uD83D\uDED1': 'error', 25 | '\u2049\uFE0F': 'error', 26 | '\u203C\uFE0F': 'error', 27 | '\u2139\uFE0F': 'info', 28 | '\u26A0': 'warn', 29 | }; 30 | 31 | export const defaultIcons = { 32 | info: '\uD83D\uDCD8', 33 | warn: '\uD83D\uDEA7', 34 | okay: '\uD83D\uDC4D', 35 | error: '\u2757\uFE0F', 36 | }; 37 | 38 | const Callout = (props: Props) => { 39 | const { attributes, children, theme = 'default', empty } = props; 40 | const icon = props.icon || defaultIcons[theme] || '❗'; 41 | const isEmoji = emojiRegex().test(icon); 42 | 43 | return ( 44 | // @ts-expect-error -- theme is not a valid attribute 45 | // eslint-disable-next-line react/jsx-props-no-spreading, react/no-unknown-property 46 |
47 |

48 | {isEmoji ? {icon} : } 49 | {empty || React.Children.toArray(children)[0]} 50 |

51 | {React.Children.toArray(children).slice(1)} 52 |
53 | ); 54 | }; 55 | 56 | export default Callout; 57 | -------------------------------------------------------------------------------- /components/Cards/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import './style.scss'; 4 | 5 | interface CardProps 6 | extends React.PropsWithChildren<{ 7 | href?: string; 8 | icon?: string; 9 | iconColor?: string; 10 | target?: string; 11 | title?: string; 12 | }> {} 13 | 14 | export const Card = ({ children, href, icon, iconColor, target, title }: CardProps) => { 15 | const Tag = href ? 'a' : 'div'; 16 | return ( 17 | 18 | {icon && } 19 | {title &&

{title}

} 20 |
{children}
21 |
22 | ); 23 | }; 24 | 25 | interface CardsGripProps extends React.PropsWithChildren<{ columns?: number }> {} 26 | 27 | const CardsGrid = ({ columns = 2, children }: CardsGripProps) => { 28 | // eslint-disable-next-line no-param-reassign 29 | columns = columns >= 2 ? columns : 2; 30 | return ( 31 |
32 | {children} 33 |
34 | ); 35 | }; 36 | 37 | export default CardsGrid; 38 | -------------------------------------------------------------------------------- /components/Cards/style.scss: -------------------------------------------------------------------------------- 1 | @import '/styles/mixins/dark-mode.scss'; 2 | 3 | $iphone-plus: 414px; 4 | 5 | .CardsGrid { 6 | --Card-bg-color: rgba(var(--color-bg-page-rgb, white), 1); 7 | --Card-bg-color-hover: var(--color-bg-hover, #{rgba(black, 0.05)}); 8 | --Card-border-color: var(--color-border-default, rgba(black, 0.1)); 9 | --Card-shadow: 0 1px 2px #{rgba(black, 0.05)}, 0 2px 5px #{rgba(black, 0.02)}; 10 | --Card-content-color: var(--color-text-muted, #4f5a66); 11 | --Card-icon-color: var(--project-color-primary, inherit); 12 | --Card-title-color: var(--color-text-default, #384248); 13 | 14 | @include dark-mode { 15 | --Card-icon-color: var(--color-primary-inverse, inherit); 16 | } 17 | 18 | display: grid; 19 | gap: 20px; 20 | 21 | .Card { 22 | padding: 15px; 23 | padding-bottom: 0; 24 | backdrop-filter: blur(20px); 25 | background: var(--Card-bg-color); 26 | border: 1px solid var(--Card-border-color); 27 | border-radius: 5px; 28 | box-shadow: var(--Card-shadow); 29 | 30 | &-top { 31 | display: inline-flex; 32 | flex-direction: row; 33 | } 34 | 35 | &-icon { 36 | color: var(--Card-icon-color); 37 | font-size: 20px; 38 | } 39 | 40 | &-title { 41 | color: var(--Card-title-color); 42 | font-weight: 600; 43 | margin-top: 10px; 44 | 45 | &:first-child { 46 | margin-top: 0; 47 | } 48 | } 49 | 50 | &-content { 51 | color: var(--Card-content-color); 52 | } 53 | } 54 | 55 | a.Card:not([href='']) { 56 | text-decoration: none; 57 | color: inherit; 58 | 59 | &:hover { 60 | background: var(--Card-bg-color-hover); 61 | } 62 | } 63 | 64 | @media (max-width: $iphone-plus) { 65 | grid-template-columns: 1fr !important; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /components/Glossary/style.scss: -------------------------------------------------------------------------------- 1 | .GlossaryItem { 2 | &-trigger { 3 | border-bottom: 1px solid #737c83; 4 | border-style: dotted; 5 | border-top: none; 6 | border-left: none; 7 | border-right: none; 8 | cursor: pointer; 9 | } 10 | 11 | &-tooltip-content { 12 | --Glossary-bg: var(--color-bg-page, var(--white)); 13 | --Glossary-color: var(--color-text-default, var(--gray20)); 14 | --Glossary-shadow: var( 15 | --box-shadow-menu-light, 16 | 0 5px 10px rgba(0, 0, 0, 0.05), 17 | 0 2px 6px rgba(0, 0, 0, 0.025), 18 | 0 1px 3px rgba(0, 0, 0, 0.025) 19 | ); 20 | 21 | /* what the dark-mode mixin does in the readme project */ 22 | [data-color-mode='dark'] & { 23 | --Glossary-bg: var(--gray15); 24 | --Glossary-color: var(--color-text-default, var(--white)); 25 | --Glossary-shadow: var( 26 | --box-shadow-menu-dark, 27 | 0 1px 3px rgba(0, 0, 0, 0.025), 28 | 0 2px 6px rgba(0, 0, 0, 0.025), 29 | 0 5px 10px rgba(0, 0, 0, 0.05) 30 | ); 31 | } 32 | 33 | @media (prefers-color-scheme: dark) { 34 | [data-color-mode='auto'] & { 35 | --Glossary-bg: var(--Tooltip-bg, var(--gray0)); 36 | --Glossary-color: var(--color-text-default, var(--white)); 37 | --Glossary-shadow: var( 38 | --box-shadow-menu-dark, 39 | 0 1px 3px rgba(0, 0, 0, 0.025), 40 | 0 2px 6px rgba(0, 0, 0, 0.025), 41 | 0 5px 10px rgba(0, 0, 0, 0.05) 42 | ); 43 | } 44 | } 45 | 46 | background-color: var(--Glossary-bg); 47 | border: 1px solid #{var(--color-border-default, rgba(black, 0.1))}; 48 | border-radius: var(--border-radius); 49 | box-shadow: var(--Glossary-shadow); 50 | color: var(--Glossary-color); 51 | font-size: 15px; 52 | font-weight: 400; 53 | line-height: 1.5; 54 | padding: 15px; 55 | text-align: left; 56 | width: 350px; 57 | } 58 | 59 | &-term { 60 | font-style: italic; 61 | } 62 | } 63 | 64 | .tippy-box { 65 | // needed for tippy's default animation 66 | &[data-animation='fade'][data-state='hidden'] { 67 | opacity: 0; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /components/HTMLBlock/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | 3 | const MATCH_SCRIPT_TAGS = /]*>([\s\S]*?)<\/script *>\n?/gim; 4 | 5 | const extractScripts = (html: string = ''): [string, () => void] => { 6 | const scripts: string[] = []; 7 | let match: RegExpExecArray | null; 8 | while ((match = MATCH_SCRIPT_TAGS.exec(html)) !== null) { 9 | scripts.push(match[1]); 10 | } 11 | const cleaned = html.replace(MATCH_SCRIPT_TAGS, ''); 12 | // eslint-disable-next-line no-eval 13 | return [cleaned, () => scripts.map(js => window.eval(js))]; 14 | }; 15 | 16 | interface Props { 17 | children: React.ReactElement | string; 18 | runScripts?: boolean | string; 19 | safeMode?: boolean; 20 | } 21 | 22 | const HTMLBlock = ({ children = '', runScripts, safeMode = false }: Props) => { 23 | if (typeof children !== 'string') { 24 | throw new TypeError('HTMLBlock: children must be a string'); 25 | } 26 | 27 | const html = children; 28 | // eslint-disable-next-line no-param-reassign 29 | runScripts = typeof runScripts !== 'boolean' ? runScripts === 'true' : runScripts; 30 | 31 | const [cleanedHtml, exec] = extractScripts(html); 32 | 33 | useEffect(() => { 34 | if (typeof window !== 'undefined' && typeof runScripts === 'boolean' && runScripts) exec(); 35 | }, [runScripts, exec]); 36 | 37 | if (safeMode) { 38 | return ( 39 |
40 |         {html}
41 |       
42 | ); 43 | } 44 | 45 | return
; 46 | }; 47 | 48 | export default HTMLBlock; 49 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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: -.8rem; 24 | padding: .8rem .2rem .8rem 0 !important; 25 | font-size: .8rem !important; 26 | text-decoration: none; 27 | color: inherit; 28 | transform: translateX(-100%); 29 | transition: .2s ease; 30 | &:hover { 31 | opacity: 1; 32 | } 33 | } 34 | &:not(:hover) .heading-anchor-icon { 35 | opacity: 0; 36 | } 37 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /components/Table/style.scss: -------------------------------------------------------------------------------- 1 | @mixin markdown-table( 2 | $row: #fff, 3 | $head: #f6f8fa, 4 | $stripe: #fbfcfd, 5 | $edges: #dfe2e5, 6 | ) { 7 | table { 8 | display: table; 9 | border-collapse: collapse; 10 | border-spacing: 0; 11 | width: 100%; 12 | color: var(--table-text); 13 | 14 | thead { 15 | color: var(--table-head-text, inherit); 16 | } 17 | 18 | thead tr { 19 | background: var(--table-head, #{$head}); 20 | } 21 | 22 | tr { 23 | background-color: var(--table-row, #{$row}); 24 | & + tr { 25 | border-top: 1px solid var(--table-edges, #{$edges}); 26 | } 27 | } 28 | 29 | th, 30 | thead td { 31 | font-weight: 600; 32 | &:empty { 33 | padding: 0; 34 | } 35 | } 36 | 37 | td, 38 | th { 39 | padding: 0; 40 | color: inherit; 41 | vertical-align: middle; 42 | border: 1px solid var(--table-edges, #{$edges}); 43 | padding: 6px 13px; 44 | > :first-child, > :only-child { margin-top: 0 !important } 45 | > :last-child, > :only-child { margin-bottom: 0 !important } 46 | } 47 | 48 | &:not(.plain) tr:nth-child(2n) { 49 | background-color: var(--table-stripe, #{$stripe}); 50 | } 51 | } 52 | } 53 | 54 | .markdown-body { 55 | @include markdown-table; 56 | 57 | .rdmd-table { 58 | $border-wrap-width: 1px; 59 | 60 | & { 61 | display: block; 62 | position: relative; 63 | } 64 | 65 | &-inner { 66 | box-sizing: content-box; 67 | min-width: 100%; 68 | overflow: auto; 69 | width: 100%; 70 | } 71 | 72 | table { 73 | border: 1px solid var(--table-edges, #dfe2e5); 74 | 75 | &:only-child { 76 | margin: 0 !important; 77 | 78 | thead th { 79 | background: inherit; 80 | } 81 | 82 | td:last-child, 83 | th:last-child { 84 | border-right: none; 85 | } 86 | 87 | thead tr, 88 | thead th:last-child { 89 | box-shadow: 3px 0 0 var(--table-head); 90 | } 91 | } 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /components/TableOfContents/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function TableOfContents({ children }: React.PropsWithChildren) { 4 | return ( 5 |
17 | ); 18 | } 19 | 20 | export default TableOfContents; 21 | -------------------------------------------------------------------------------- /components/TableOfContents/style.scss: -------------------------------------------------------------------------------- 1 | .toc-list { 2 | .glossary-tooltip { 3 | pointer-events: none; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /components/TutorialTile.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 TutorialTile = () => { 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 TutorialTile; 47 | -------------------------------------------------------------------------------- /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 Table } from './Table'; 14 | export { default as TableOfContents } from './TableOfContents'; 15 | export { default as Tabs, Tab } from './Tabs'; 16 | export { default as TailwindRoot } from './TailwindRoot'; 17 | export { default as TailwindStyle } from './TailwindStyle'; 18 | export { default as TutorialTile } from './TutorialTile'; 19 | -------------------------------------------------------------------------------- /contexts/BaseUrl.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const BaseUrlContext = React.createContext('/'); 4 | 5 | export default BaseUrlContext; 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /contexts/Theme.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | const ThemeContext = createContext<'dark' | 'light'>('light'); 4 | 5 | export default ThemeContext; 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/built-in-components.mdx: -------------------------------------------------------------------------------- 1 | ## Built-In Components 2 | 3 | ### Accordion 4 | 5 | 6 | Lorem ipsum dolor sit amet, **consectetur adipiscing elit.** Ut enim ad minim veniam, quis nostrud exercitation 7 | ullamco. Excepteur sint occaecat cupidatat non proident! 8 | 9 | 10 | --- 11 | 12 | ### Cards 13 | 14 | 15 | 16 | Neque porro quisquam est qui dolorem ipsum quia 17 | 18 | 19 | *Lorem ipsum dolor sit amet, consectetur adipiscing elit* 20 | 21 | 22 | > Ut enim ad minim veniam, quis nostrud ullamco 23 | 24 | 25 | **Excepteur sint occaecat cupidatat non proident** 26 | 27 | 28 | 29 | --- 30 | 31 | ### Tabs 32 | 33 | 34 | Welcome to the content that you can only see inside the first Tab. 35 | Here's content that's only inside the second Tab. 36 | Here's content that's only inside the third Tab. 37 | 38 | 39 | --- 40 | 41 | ### Columns 42 | 43 | 44 | 45 | Neque porro quisquam est qui dolorem ipsum quia 46 | 47 | 48 | *Lorem ipsum dolor sit amet, consectetur adipiscing elit* 49 | 50 | 51 | > Ut enim ad minim veniam, quis nostrud ullamco 52 | 53 | 54 | -------------------------------------------------------------------------------- /docs/features.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'And more...' 3 | category: 5fdf7610134322007389a6ed 4 | excerpt: 'Additional Markdown features of the ReadMe platform implementation.' 5 | hidden: false 6 | --- 7 | 8 | We've also built a lot of great features right in to the ReadMe app, which work on top of our markdown engine to give your developers a best-in-class documentation experience. These features aren't all baked in to the new engine itself, but they're worth mentioning nonetheless! 9 | 10 | ## Data Replacement 11 | 12 | #### User Variables 13 | 14 | If you've set up JWT logins and user variables in your ReadMe project, you can use the included `user` variable. So if you're logged in to and have a `name` variable set, then this... 15 | 16 | ``` 17 | Hi, my name is **{user.name}**! 18 | ``` 19 | 20 | ...should expand to this: “Hi, my name is **{user.name}**!” 21 | 22 | #### Glossary Terms 23 | 24 | Did you know you can define various technical terms for your ReadMe project? Using our glossary feature, these terms can be used anywhere in your Markdown! Just use the built in Glossary component: 25 | 26 | ``` 27 | Both **exogenous** and **endogenous** are long words. 28 | ``` 29 | 30 | Which expands to: “Both **exogenous** and **endogenous** are long words.” 31 | 32 | #### Emoji Shortcodes 33 | 34 | You can use GitHub-style emoji short codes (feat. Owlbert!) 35 | 36 | ``` 37 | :sparkles: :owlbert-reading: 38 | ``` 39 | 40 | This expands out to: “:sparkles: :owlbert-reading:” 41 | 42 | ## Generative Semantics 43 | 44 | - Markup-based TOC generation. 45 | - Auto-generated [heading anchors](doc:headings#section-incremented-anchors). 46 | 47 | ## Known Issues 48 | 49 | - Variable and glossary term expansions are rendered even when they've been manually escaped by the author. 50 | -------------------------------------------------------------------------------- /docs/headings.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Headings' 3 | category: 5fdf7610134322007389a6ed 4 | hidden: false 5 | --- 6 | 7 | ## Examples 8 | 9 | > ### Heading Block 3 10 | > 11 | > #### Heading Block 4 12 | > 13 | > ##### Heading Block 5 14 | > 15 | > ###### Heading Block 6 16 | 17 | ## Edge Cases 18 | 19 | ### Heading Styles 20 | 21 | ####Compact Notation 22 | 23 | Headers are denoted using a space-separated `#` prefix. While the space is technically required in most standard Markdown implementations, some processors allow for a compact notation as well. ReadMe supports this style, so writing this 24 | 25 | ``` 26 | ###A Valid Heading 27 | 28 | Lorem ipsum dolor etc. 29 | ``` 30 | 31 | > 🛑 32 | > Compact headings must be followed by two line breaks before the following block. 33 | 34 | #### ATX-Style Notation #### 35 | 36 | If you prefer, you can "wrap" headers with hashes rather than simply prefixing them: 37 | 38 | ``` 39 | ## ATX Headings are Valid ## 40 | ``` 41 | 42 | #### Underline Notation 43 | 44 | For top-level headings, the underline notation is valid: 45 | 46 | ``` 47 | Heading One 48 | =========== 49 | 50 | Heading Two 51 | --- 52 | ``` 53 | 54 | ### Incremented Anchors 55 | 56 | Occasionally, a single doc might contain multiple headings with the same text, which can cause the generated anchor links to conflict. ReadMe's new markdown processor normalizes heading anchors by auto-incrementing similar heading's IDs. Try it out by clicking on this section header _or_ the following sub-section title: 57 | 58 | #### Incremented Heading Anchors 59 | 60 | #### Incremented Heading Anchors 61 | -------------------------------------------------------------------------------- /docs/images.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Images" 3 | category: 5fdf7610134322007389a6ed 4 | hidden: false 5 | --- 6 | ## Syntax 7 | ``` 8 | ![Alt text](https://cdn.path.to/some/image.jpg "This is some image...") 9 | ``` 10 | 11 | ## Examples 12 | 13 | ![Bro eats pizza and makes an OK gesture.](https://files.readme.io/6f52e22-man-eating-pizza-and-making-an-ok-gesture.jpg "Pizza Face") 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /docs/lists.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Lists" 3 | category: 5fdf7610134322007389a6ed 4 | hidden: false 5 | --- 6 | 7 | [block:api-header] 8 | { 9 | "title": "Syntax" 10 | } 11 | [/block] 12 | ```shell Bullet Lists 13 | - Item Zed 14 | - Nested Item # indented 2 spaces 15 | * Item Alt # alternate bullet syntax 16 | ``` 17 | ```shell Numeric Lists 18 | 1. Item Zed 19 | 1. Nested Numeric # indented 3 spaces 20 | 2. Item One 21 | ``` 22 | ```shell Check Lists 23 | - [ ] Open Item 24 | - [x] Item Done 25 | ``` 26 | [block:api-header] 27 | { 28 | "title": "Examples" 29 | } 30 | [/block] 31 |
32 | Bulleted List
33 | 34 | - Item Zed 35 | * Nested Item 36 | * Nested Item 37 | - Item One 38 | - Item Two 39 | 40 |
41 |
42 | Ordered List
43 | 44 | 1. Item Zed 45 | 1. Nested Numeric 46 | 1. Nested Numeric 47 | 1. Item One 48 | 2. Item Two 49 | 50 |
51 |
52 | Check List
53 | 54 | - [ ] Task Zed 55 | - [x] Task One 56 | - [ ] Task Two 57 | 58 |
59 | 60 | ## Edge Cases 61 | 62 | ### Split Lists 63 | 64 | Seamlessly insert content blocks in between list items: 65 | 66 | 1. Item Zed 67 | 68 | > Sit excepturi doloremque deserunt maiores quam voluptatibus cupiditate delectus perferendis, ratione cum impedit rem recusandae inventore quibusdam et, tenetur aspernatur asperiores reiciendis soluta. 69 | 70 | 1. Item One 71 | 72 | ```javascript 73 | console.log('hello world') 74 | ``` 75 | 76 | 1. Item Two 77 | 78 | ### Auto-Ordering 79 | 80 | Writing this will yield a properly ordered list: 81 | 82 | 1. Item Zed 83 | 1. Item One 84 | 1. Item Two 85 | 86 | Starting an ordered list with any number will increment continuously from that point, like so: 87 | 88 | 98. Starting in media res 89 | 98. Another list item 90 | 98. Yet another item 91 | [block:html] 92 | { 93 | "html": "" 94 | } 95 | [/block] 96 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/syntax-extensions.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Flavored Syntax" 3 | category: 5fdf7610134322007389a6ec 4 | excerpt: "Specs and examples of ReadMe's (restrained) Markdown syntax extensions." 5 | hidden: false 6 | --- 7 | Custom Blocks 8 | --- 9 | 10 | ### Code Tabs 11 | 12 | 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) 13 | 14 | ### Callouts 15 | 16 | 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) 17 | 18 | ### Embeds 19 | 20 | Embedded content is written as a simple Markdown link, with a title of "@embed". [**Syntax & examples**.](doc:embeds) 21 | 22 | Standard Markdown 23 | --- 24 | 25 | The engine also supports all standard markdown constructs, as well as CommonMark options, and most GitHub syntax extensions. 26 | 27 | - [**Tables**](doc:tables) 28 | - [**Lists**](doc:lists) 29 | - [**Headings**](doc:headings) 30 | - [**Images**](doc:images) 31 | - **Decorations** (link, strong, and emphasis tags, etc.) 32 | -------------------------------------------------------------------------------- /docs/tables.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Tables' 3 | category: 5fdf7610134322007389a6ed 4 | hidden: false 5 | --- 6 | 7 | ## Syntax 8 | 9 | ```markdown 10 | | Left | Center | Right | 11 | |:-----|:--------:|------:| 12 | | L0 | **bold** | $1600 | 13 | | L1 | `code` | $12 | 14 | | L2 | _italic_ | $1 | 15 | ``` 16 | 17 | ### Examples 18 | 19 | This example also shows off custom theming! 20 | 21 | | Left | Center | Right | 22 | | :--- | :------: | ----: | 23 | | L0 | **bold** | $1600 | 24 | | L1 | `code` | $12 | 25 | | L2 | _italic_ | $1 | 26 | 27 | ## Custom CSS 28 | 29 | Tables have been simplified to mirror a more standard implementation. We've also set up CSS variable-based theming, which should make it easier to add custom styles. 30 | 31 | ```scss CSS Variables 32 | .markdown-body .rdmd-table { 33 | --table-text: black; 34 | --table-head: #5b1c9f; 35 | --table-head-text: white; 36 | --table-stripe: #f0eaf7; 37 | --table-edges: rgba(34, 5, 64, 0.5); 38 | --table-row: white; 39 | } 40 | ``` 41 | ```scss CSS Selectors 42 | /* Table 43 | */ 44 | .markdown-body .rdmd-table table {} 45 | 46 | /* Rows 47 | */ 48 | .markdown-body .rdmd-table tr {} 49 | .markdown-body .rdmd-table thead tr {} 50 | /* header row's background */ 51 | .markdown-body .rdmd-table tr:nth-child(2n) {} 52 | /* striped rows' background */ 53 | 54 | /* Cells 55 | */ 56 | .markdown-body .rdmd-table th {} 57 | .markdown-body .rdmd-table td {} 58 | ``` 59 | 60 | export const stylesheet = ` 61 | .markdown-body .rdmd-table { 62 | --table-text: black; 63 | --table-head: #5b1c9f; 64 | --table-head-text: white; 65 | --table-stripe: #f0eaf7; 66 | --table-edges: rgba(34, 5, 64, .5); 67 | --table-row: white; 68 | } 69 | 70 | #rdmd-demo .markdown-body .rdmd-table thead tr { 71 | box-shadow: none; 72 | } 73 | 74 | #rdmd-demo .markdown-body .rdmd-table thead tr th:last-child { 75 | box-shadow: none; 76 | } 77 | `; 78 | 79 | 82 | -------------------------------------------------------------------------------- /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 | reusableContent = 'reusable-content', 14 | tableau = 'tableau', 15 | tutorialTile = 'tutorial-tile', 16 | variable = 'readme-variable', 17 | } 18 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /example/components.ts: -------------------------------------------------------------------------------- 1 | const components = { 2 | Demo: ` 3 | ## This is a Demo Component! 4 | 5 | > 📘 It can render JSX components! 6 | `, 7 | Test: ` 8 | export const Test = ({ color = 'thistle' } = {}) => { 9 | return
10 | Hello, World! 11 |
; 12 | }; 13 | 14 | export default Test; 15 | `, 16 | MultipleExports: ` 17 | export const One = () => "One"; 18 | 19 | export const Two = () => "Two"; 20 | `, 21 | TailwindRootTest: ` 22 | export const StyledComponent = () => { 23 | return
24 | Hello, World! 25 |
; 26 | } 27 | `, 28 | Steps: ` 29 | export const Step = ({ children }) => { 30 | return ( 31 |
32 |
33 | {children} 34 |
35 |
36 | ); 37 | }; 38 | 39 |
40 | {props.children} 41 |
42 | `, 43 | 44 | DarkMode: ` 45 | import { useState } from 'react'; 46 | 47 | export const DarkMode = () => { 48 | const [mode, setMode] = useState('dark'); 49 | 50 | return ( 51 |
52 | 59 |
63 | {mode} Mode Component 64 |
65 |
66 | ) 67 | } 68 | `, 69 | }; 70 | 71 | export default components; 72 | -------------------------------------------------------------------------------- /example/docs.ts: -------------------------------------------------------------------------------- 1 | import calloutTests from '../__tests__/fixtures/callout-tests.md'; 2 | import childTests from '../__tests__/fixtures/child-tests.mdx'; 3 | import codeBlockTests from '../__tests__/fixtures/code-block-tests.md'; 4 | import exportTests from '../__tests__/fixtures/export-tests.mdx'; 5 | import imageTests from '../__tests__/fixtures/image-tests.mdx'; 6 | import sanitizingTests from '../__tests__/fixtures/sanitizing-tests.md'; 7 | import tableOfContentsTests from '../__tests__/fixtures/table-of-contents-tests.md'; 8 | import tailwindRootTests from '../__tests__/fixtures/tailwind-root-tests.mdx'; 9 | import tutorialTile from '../__tests__/fixtures/tutorial-tile.mdx'; 10 | import varsTest from '../__tests__/fixtures/variable-tests.md'; 11 | import builtInComponents from '../docs/built-in-components.mdx'; 12 | import callouts from '../docs/callouts.md'; 13 | import codeBlocks from '../docs/code-blocks.md'; 14 | import embeds from '../docs/embeds.md'; 15 | import features from '../docs/features.md'; 16 | import gettingStarted from '../docs/getting-started.md'; 17 | import headings from '../docs/headings.md'; 18 | import images from '../docs/images.md'; 19 | import lists from '../docs/lists.md'; 20 | import mdxComponents from '../docs/mdx-components.mdx'; 21 | import mermaid from '../docs/mermaid.md'; 22 | import tables from '../docs/tables.md'; 23 | 24 | const lowerCase = (str: string) => 25 | str.replaceAll(/([a-z])([A-Z])/g, (_: string, p1: string, p2: string) => `${p1} ${p2.toLowerCase()}`); 26 | 27 | const fixtures = Object.entries({ 28 | calloutTests, 29 | callouts, 30 | childTests, 31 | codeBlockTests, 32 | codeBlocks, 33 | embeds, 34 | exportTests, 35 | features, 36 | gettingStarted, 37 | headings, 38 | images, 39 | imageTests, 40 | lists, 41 | mdxComponents, 42 | builtInComponents, 43 | mermaid, 44 | sanitizingTests, 45 | tableOfContentsTests, 46 | tables, 47 | tailwindRootTests, 48 | tutorialTile, 49 | varsTest, 50 | }).reduce((memo, [sym, doc]) => { 51 | const name = lowerCase(sym); 52 | memo[sym] = { name, doc }; 53 | return memo; 54 | }, {}); 55 | 56 | export default fixtures; 57 | -------------------------------------------------------------------------------- /example/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readmeio/markdown/b812eeedba99b513a04f42f577ce724513fc2b06/example/favicon.ico -------------------------------------------------------------------------------- /example/img/nyt-thumbnail.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readmeio/markdown/b812eeedba99b513a04f42f577ce724513fc2b06/example/img/nyt-thumbnail.jpg -------------------------------------------------------------------------------- /example/img/pizzabro.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readmeio/markdown/b812eeedba99b513a04f42f577ce724513fc2b06/example/img/pizzabro.jpg -------------------------------------------------------------------------------- /example/img/readme-logo-white-on-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readmeio/markdown/b812eeedba99b513a04f42f577ce724513fc2b06/example/img/readme-logo-white-on-blue.png -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Markdown Demo 5 | 6 | 7 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 { compile, exports, hast, run, mdast, mdastV6, mdx, migrate, plain, remarkPlugins, tags } from './lib'; 16 | export { default as Owlmoji } from './lib/owlmoji'; 17 | export { Components, utils }; 18 | export { tailwindCompiler } from './utils/tailwind-compiler'; 19 | -------------------------------------------------------------------------------- /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: 45000, 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/ErrorBoundary.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class ErrorBoundary extends React.Component { 4 | constructor(props) { 5 | super(props); 6 | this.state = { hasError: false }; 7 | } 8 | 9 | static getDerivedStateFromError(error) { 10 | // eslint-disable-next-line no-console 11 | console.error(error); 12 | return { hasError: true }; 13 | } 14 | 15 | render() { 16 | if (this.state.hasError) { 17 | return null; 18 | } 19 | 20 | // eslint-disable-next-line react/prop-types 21 | return this.props.children; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 migrate } from './migrate'; 11 | export { default as plain } from './plain'; 12 | export { default as run } from './run'; 13 | export { default as tags } from './tags'; 14 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /lib/mdx.ts: -------------------------------------------------------------------------------- 1 | import type { Root as HastRoot } from 'hast'; 2 | import type { Root as MdastRoot } from 'mdast'; 3 | 4 | import rehypeRemark from 'rehype-remark'; 5 | import remarkGfm from 'remark-gfm'; 6 | import remarkMdx from 'remark-mdx'; 7 | import remarkStringify from 'remark-stringify'; 8 | import { unified } from 'unified'; 9 | 10 | import compilers from '../processor/compile'; 11 | import { compatabilityTransfomer, divTransformer, readmeToMdx, tablesToJsx } from '../processor/transform'; 12 | 13 | export const mdx = (tree: HastRoot | MdastRoot, { hast = false, ...opts } = {}) => { 14 | const processor = unified() 15 | .use(hast ? rehypeRemark : undefined) 16 | .use(remarkMdx) 17 | .use(remarkGfm) 18 | .use(divTransformer) 19 | .use(readmeToMdx) 20 | .use(tablesToJsx) 21 | .use(compatabilityTransfomer) 22 | .use(compilers) 23 | .use(remarkStringify, opts); 24 | 25 | // @ts-expect-error - @todo: coerce the processor and tree to the correct 26 | // type depending on the value of hast 27 | return processor.stringify(processor.runSync(tree)); 28 | }; 29 | 30 | export default mdx; 31 | -------------------------------------------------------------------------------- /lib/migrate.ts: -------------------------------------------------------------------------------- 1 | import mdastV6 from './mdastV6'; 2 | import mdx from './mdx'; 3 | 4 | const migrate = (doc: string, { rdmd }): string => { 5 | return ( 6 | mdx(mdastV6(doc, { rdmd })) 7 | .replaceAll(/ /g, ' ') 8 | // @note: I'm not sure what's happening, but I think mdx is converting an 9 | // 'a' to 'a' as a means of escaping it. I think this helps with 10 | // parsing weird cases. 11 | .replaceAll(/a/g, 'a') 12 | ); 13 | }; 14 | 15 | export default migrate; 16 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/styles.ts: -------------------------------------------------------------------------------- 1 | const styles = () => {}; 2 | 3 | export default styles; 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | ...headings, 28 | ...more, 29 | }; 30 | 31 | return (() => components) as unknown as UseMdxComponents; 32 | }; 33 | 34 | export default makeUseMDXComponents; 35 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /processor/compile/gemoji.ts: -------------------------------------------------------------------------------- 1 | import type { Gemoji } from '../../types'; 2 | 3 | const gemoji = (node: Gemoji) => `:${node.name}:`; 4 | 5 | export default gemoji; 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /processor/compile/index.ts: -------------------------------------------------------------------------------- 1 | import { NodeTypes } from '../../enums'; 2 | 3 | import callout from './callout'; 4 | import codeTabs from './code-tabs'; 5 | import compatibility from './compatibility'; 6 | import embed from './embed'; 7 | import gemoji from './gemoji'; 8 | import htmlBlock from './html-block'; 9 | import plain from './plain'; 10 | 11 | function compilers() { 12 | const data = this.data(); 13 | 14 | const toMarkdownExtensions = data.toMarkdownExtensions || (data.toMarkdownExtensions = []); 15 | 16 | const handlers = { 17 | [NodeTypes.callout]: callout, 18 | [NodeTypes.codeTabs]: codeTabs, 19 | [NodeTypes.embedBlock]: embed, 20 | [NodeTypes.emoji]: gemoji, 21 | [NodeTypes.glossary]: compatibility, 22 | [NodeTypes.htmlBlock]: htmlBlock, 23 | [NodeTypes.reusableContent]: compatibility, 24 | embed: compatibility, 25 | escape: compatibility, 26 | figure: compatibility, 27 | html: compatibility, 28 | i: compatibility, 29 | plain, 30 | yaml: compatibility, 31 | }; 32 | 33 | toMarkdownExtensions.push({ extensions: [{ handlers }] }); 34 | } 35 | 36 | export default compilers; 37 | -------------------------------------------------------------------------------- /processor/compile/plain.ts: -------------------------------------------------------------------------------- 1 | import type { Plain } from '../../types'; 2 | 3 | const plain = (node: Plain) => node.value; 4 | 5 | export default plain; 6 | -------------------------------------------------------------------------------- /processor/compile/table.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readmeio/markdown/b812eeedba99b513a04f42f577ce724513fc2b06/processor/compile/table.ts -------------------------------------------------------------------------------- /processor/compile/yaml.js: -------------------------------------------------------------------------------- 1 | module.exports = function YamlCompiler() { 2 | const { Compiler } = this; 3 | const { visitors } = Compiler.prototype; 4 | 5 | visitors.yaml = function compile(node) { 6 | return `---\n${node.value}\n---`; 7 | }; 8 | }; 9 | -------------------------------------------------------------------------------- /processor/migration/emphasis.ts: -------------------------------------------------------------------------------- 1 | import type { Emphasis, Node, Parent, Root, Strong, Text } from 'mdast'; 2 | 3 | import { visit, EXIT } from 'unist-util-visit'; 4 | 5 | const strongTest = (node: Node): node is Emphasis | Strong => ['emphasis', 'strong'].includes(node.type); 6 | 7 | const addSpaceBefore = (index: number, parent: Parent) => { 8 | if (!(index > 0 && parent.children[index - 1])) return; 9 | 10 | const prev = parent.children[index - 1]; 11 | if (!('value' in prev) || prev.value.endsWith(' ') || prev.type === 'escape') return; 12 | 13 | parent.children.splice(index, 0, { type: 'text', value: ' ' }); 14 | }; 15 | 16 | const addSpaceAfter = (index: number, parent: Parent) => { 17 | if (!(index < parent.children.length - 1 && parent.children[index + 1])) return; 18 | 19 | const nextChild = parent.children[index + 1]; 20 | if (!('value' in nextChild) || nextChild.value.startsWith(' ')) return; 21 | 22 | parent.children.splice(index + 1, 0, { type: 'text', value: ' ' }); 23 | }; 24 | 25 | const trimEmphasis = (node: Emphasis | Strong, index: number, parent: Parent) => { 26 | let trimmed = false; 27 | 28 | visit(node, 'text', (child: Text) => { 29 | const newValue = child.value.trimStart(); 30 | 31 | if (newValue !== child.value) { 32 | trimmed = true; 33 | child.value = newValue; 34 | } 35 | 36 | return EXIT; 37 | }); 38 | 39 | visit( 40 | node, 41 | 'text', 42 | (child: Text) => { 43 | const newValue = child.value.trimEnd(); 44 | 45 | if (newValue !== child.value) { 46 | trimmed = true; 47 | child.value = newValue; 48 | } 49 | 50 | return EXIT; 51 | }, 52 | true, 53 | ); 54 | 55 | if (trimmed) { 56 | addSpaceBefore(index, parent); 57 | addSpaceAfter(index, parent); 58 | } 59 | }; 60 | 61 | const emphasisTransfomer = () => (tree: Root) => { 62 | visit(tree, strongTest, trimEmphasis); 63 | 64 | return tree; 65 | }; 66 | 67 | export default emphasisTransfomer; 68 | -------------------------------------------------------------------------------- /processor/migration/images.ts: -------------------------------------------------------------------------------- 1 | import type { Image } from 'mdast'; 2 | 3 | import { visit } from 'unist-util-visit'; 4 | 5 | interface ImageBlock extends Image { 6 | data?: { 7 | hProperties?: { 8 | border?: boolean; 9 | className?: string; 10 | }; 11 | }; 12 | } 13 | 14 | const imageTransformer = () => tree => { 15 | visit(tree, 'image', (image: ImageBlock) => { 16 | if (image.data?.hProperties?.className === 'border') { 17 | image.data.hProperties.border = true; 18 | } 19 | }); 20 | }; 21 | 22 | export default imageTransformer; 23 | -------------------------------------------------------------------------------- /processor/migration/index.ts: -------------------------------------------------------------------------------- 1 | import emphasisTransformer from './emphasis'; 2 | import imagesTransformer from './images'; 3 | import linkReferenceTransformer from './linkReference'; 4 | import tableCellTransformer from './table-cell'; 5 | 6 | const transformers = [emphasisTransformer, imagesTransformer, linkReferenceTransformer, tableCellTransformer]; 7 | 8 | export default transformers; 9 | -------------------------------------------------------------------------------- /processor/migration/linkReference.ts: -------------------------------------------------------------------------------- 1 | import type { Definition, LinkReference, Root, Text } from 'mdast'; 2 | 3 | import { visit } from 'unist-util-visit'; 4 | 5 | const linkReferenceTransformer = 6 | () => 7 | (tree: Root): Root => { 8 | visit(tree, 'linkReference', (node: LinkReference, index, parent) => { 9 | const definitions = {}; 10 | 11 | visit(tree, 'definition', (def: Definition) => { 12 | definitions[def.identifier] = def; 13 | }); 14 | 15 | if (node.label === node.identifier && parent) { 16 | if (!(node.identifier in definitions)) { 17 | parent.children[index] = { 18 | type: 'text', 19 | value: `[${node.label}]`, 20 | position: node.position, 21 | } as Text; 22 | } 23 | } 24 | }); 25 | 26 | return tree; 27 | }; 28 | 29 | export default linkReferenceTransformer; 30 | -------------------------------------------------------------------------------- /processor/plugin/section-anchor-id.js: -------------------------------------------------------------------------------- 1 | const kebabCase = require('lodash.kebabcase'); 2 | const flatMap = require('unist-util-flatmap'); 3 | 4 | const matchTag = /^h[1-6]$/g; 5 | 6 | /** Concat a deep text value from an AST node's children 7 | */ 8 | const getTexts = node => { 9 | let text = ''; 10 | flatMap(node, kid => { 11 | text += kid.type === 'text' ? kid.value : ''; 12 | return [kid]; 13 | }); 14 | return text; 15 | }; 16 | 17 | /** Adds an empty
next to all headings 18 | * for backwards-compatibility with how we used to do slugs. 19 | */ 20 | function transformer(ast) { 21 | return flatMap(ast, node => { 22 | if (matchTag.test(node.tagName)) { 23 | // Parse the node texts to construct 24 | // a backwards-compatible anchor ID. 25 | const text = getTexts(node); 26 | const id = `section-${kebabCase(text)}`; 27 | 28 | if (id && !node?.properties?.id) { 29 | // Use the compat anchor ID as fallback if 30 | // GitHubs slugger returns an empty string. 31 | node.properties.id = id; 32 | } 33 | 34 | // Create and append a compat anchor element 35 | // to the section heading. 36 | const anchor = { 37 | type: 'element', 38 | tagName: 'div', 39 | properties: { id, className: 'heading-anchor_backwardsCompatibility' }, 40 | }; 41 | if (node.children) node.children.unshift(anchor); 42 | else node.children = [anchor]; 43 | } 44 | return [node]; 45 | }); 46 | } 47 | 48 | module.exports = () => transformer; 49 | -------------------------------------------------------------------------------- /processor/plugin/table-flattening.js: -------------------------------------------------------------------------------- 1 | const flatMap = require('unist-util-flatmap'); 2 | 3 | const collectValues = ({ value, children }) => { 4 | if (value) return value; 5 | if (children) return children.flatMap(collectValues); 6 | return ''; 7 | }; 8 | 9 | const valuesToString = node => { 10 | const values = collectValues(node); 11 | return Array.isArray(values) ? values.join(' ') : values; 12 | }; 13 | 14 | // Flattens table values and adds them as a seperate, easily-accessible key within children 15 | function transformer(ast) { 16 | return flatMap(ast, node => { 17 | if (node.tagName === 'table') { 18 | const [header, body] = node.children; 19 | // hAST tables are deeply nested with an innumerable amount of children 20 | // This is necessary to pullout all the relevant strings 21 | return [ 22 | { 23 | ...node, 24 | children: [ 25 | { 26 | ...node.children[0], 27 | value: valuesToString(header), 28 | }, 29 | { 30 | ...node.children[1], 31 | value: valuesToString(body), 32 | }, 33 | ], 34 | }, 35 | ]; 36 | } 37 | 38 | return [node]; 39 | }); 40 | } 41 | 42 | module.exports = () => transformer; 43 | module.exports.tableFlattening = transformer; 44 | -------------------------------------------------------------------------------- /processor/transform/callouts.ts: -------------------------------------------------------------------------------- 1 | import type { Blockquote, Root } from 'mdast'; 2 | import type { Callout } from 'types'; 3 | 4 | import emojiRegex from 'emoji-regex'; 5 | import { visit } from 'unist-util-visit'; 6 | 7 | import { themes } from '../../components/Callout'; 8 | import { NodeTypes } from '../../enums'; 9 | 10 | const regex = `^(${emojiRegex().source}|⚠)(\\s+|$)`; 11 | 12 | const calloutTransformer = () => { 13 | return (tree: Root) => { 14 | visit(tree, 'blockquote', (node: Blockquote | Callout) => { 15 | if (!(node.children[0].type === 'paragraph' && node.children[0].children[0].type === 'text')) return; 16 | 17 | const startText = node.children[0].children[0].value; 18 | const [match, icon] = startText.match(regex) || []; 19 | 20 | if (icon && match) { 21 | const heading = startText.slice(match.length); 22 | const empty = !heading.length && node.children[0].children.length === 1; 23 | const theme = themes[icon] || 'default'; 24 | 25 | node.children[0].children[0].value = heading; 26 | 27 | Object.assign(node, { 28 | type: NodeTypes.callout, 29 | data: { 30 | hName: 'Callout', 31 | hProperties: { 32 | icon, 33 | ...(empty && { empty }), 34 | theme, 35 | }, 36 | }, 37 | }); 38 | } 39 | }); 40 | }; 41 | }; 42 | 43 | export default calloutTransformer; 44 | -------------------------------------------------------------------------------- /processor/transform/code-tabs.ts: -------------------------------------------------------------------------------- 1 | import type { BlockContent, Code, Node } from 'mdast'; 2 | import type { CodeTabs } from 'types'; 3 | 4 | import { visit } from 'unist-util-visit'; 5 | 6 | import { NodeTypes } from '../../enums'; 7 | 8 | const isCode = (node: Node): node is Code => node?.type === 'code'; 9 | 10 | const codeTabsTransformer = 11 | ({ copyButtons }: { copyButtons?: boolean } = {}) => 12 | (tree: Node) => { 13 | visit(tree, 'code', (node: Code) => { 14 | const { lang, meta, value } = node; 15 | node.data = { 16 | hProperties: { lang, meta, value, copyButtons }, 17 | }; 18 | }); 19 | 20 | visit(tree, 'code', (node: Code, index: number, parent: BlockContent) => { 21 | if (parent.type === 'code-tabs' || !('children' in parent)) return; 22 | 23 | const length = parent.children.length; 24 | const children = [node]; 25 | let walker = index + 1; 26 | 27 | while (walker <= length) { 28 | const sibling = parent.children[walker]; 29 | if (!isCode(sibling)) break; 30 | 31 | const olderSibling = parent.children[walker - 1]; 32 | if (olderSibling.position.end.offset + sibling.position.start.column !== sibling.position.start.offset) break; 33 | 34 | children.push(sibling); 35 | // eslint-disable-next-line no-plusplus 36 | walker++; 37 | } 38 | 39 | // If there is a single code block, and it has either a title or a 40 | // language set, let's display it by wrapping it in a code tabs block. 41 | // Othewise, we can leave early! 42 | if (children.length === 1 && !(node.lang || node.meta)) return; 43 | 44 | const codeTabs: CodeTabs = { 45 | type: NodeTypes.codeTabs, 46 | children, 47 | data: { 48 | hName: 'CodeTabs', 49 | }, 50 | position: { 51 | start: children[0].position.start, 52 | end: children[children.length - 1].position.end, 53 | }, 54 | }; 55 | 56 | parent.children.splice(index, children.length, codeTabs); 57 | }); 58 | 59 | return tree; 60 | }; 61 | 62 | export default codeTabsTransformer; 63 | -------------------------------------------------------------------------------- /processor/transform/compatability.ts: -------------------------------------------------------------------------------- 1 | import type { Emphasis, Image, Strong, Node, Parent } from 'mdast'; 2 | import type { Transform } from 'mdast-util-from-markdown'; 3 | 4 | import { phrasing } from 'mdast-util-phrasing'; 5 | import { EXIT, SKIP, visit } from 'unist-util-visit'; 6 | 7 | const strongTest = (node: Node): node is Emphasis | Strong => ['emphasis', 'strong'].includes(node.type); 8 | 9 | const compatibilityTransfomer = (): Transform => tree => { 10 | const trimEmphasis = (node: Emphasis | Strong) => { 11 | visit(node, 'text', child => { 12 | child.value = child.value.trim(); 13 | return EXIT; 14 | }); 15 | 16 | return node; 17 | }; 18 | 19 | visit(tree, strongTest, node => { 20 | trimEmphasis(node); 21 | return SKIP; 22 | }); 23 | 24 | visit(tree, 'image', (node: Image, index: number, parent: Parent) => { 25 | if (phrasing(parent) || !parent.children.every(child => child.type === 'image' || !phrasing(child))) return; 26 | 27 | parent.children.splice(index, 1, { type: 'paragraph', children: [node] }); 28 | }); 29 | 30 | return tree; 31 | }; 32 | 33 | export default compatibilityTransfomer; 34 | -------------------------------------------------------------------------------- /processor/transform/div.ts: -------------------------------------------------------------------------------- 1 | import type { TutorialTile } from '../../types'; 2 | import type { Node, Parent } from 'mdast'; 3 | import type { Transform } from 'mdast-util-from-markdown'; 4 | 5 | import { visit } from 'unist-util-visit'; 6 | 7 | import { NodeTypes } from '../../enums'; 8 | 9 | const divTransformer = (): Transform => tree => { 10 | visit(tree, 'div', (node: Node, index, parent: Parent) => { 11 | const type = node.data?.hName; 12 | 13 | switch (type) { 14 | // Check if the div is a tutorial-tile in disguise 15 | case NodeTypes.tutorialTile: 16 | { 17 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 18 | const { hName, hProperties, ...rest } = node.data; 19 | const tile = { 20 | ...rest, 21 | type: NodeTypes.tutorialTile, 22 | } as TutorialTile; 23 | parent.children.splice(index, 1, tile); 24 | } 25 | break; 26 | // idk what this is and/or just make it a paragraph 27 | default: 28 | node.type = type || 'paragraph'; 29 | } 30 | }); 31 | 32 | return tree; 33 | }; 34 | 35 | export default divTransformer; 36 | -------------------------------------------------------------------------------- /processor/transform/embeds.ts: -------------------------------------------------------------------------------- 1 | import type { Embed, EmbedBlock } from '../../types'; 2 | import type { Paragraph, Parents, Node, Text } from 'mdast'; 3 | 4 | import { visit } from 'unist-util-visit'; 5 | 6 | import { NodeTypes } from '../../enums'; 7 | 8 | const isEmbed = (node: Node): node is Embed => 'title' in node && node.title === '@embed'; 9 | 10 | const embedTransformer = () => { 11 | return (tree: Node) => { 12 | visit(tree, 'paragraph', (node: Paragraph, i: number, parent: Parents) => { 13 | const [child] = node.children; 14 | if (!isEmbed(child)) return; 15 | 16 | const { url, title } = child; 17 | const label = (child.children[0] as Text).value; 18 | 19 | const newNode = { 20 | type: NodeTypes.embedBlock, 21 | label, 22 | title, 23 | url, 24 | data: { 25 | hProperties: { 26 | url, 27 | title: label ?? title, 28 | }, 29 | hName: 'embed', 30 | }, 31 | position: node.position, 32 | } as EmbedBlock; 33 | 34 | parent.children.splice(i, 1, newNode); 35 | }); 36 | }; 37 | }; 38 | 39 | export default embedTransformer; 40 | -------------------------------------------------------------------------------- /processor/transform/gemoji+.ts: -------------------------------------------------------------------------------- 1 | import type { FaEmoji, Gemoji } from '../../types'; 2 | import type { Image, Root } from 'mdast'; 3 | 4 | import { findAndReplace } from 'mdast-util-find-and-replace'; 5 | 6 | import { NodeTypes } from '../../enums'; 7 | import Owlmoji from '../../lib/owlmoji'; 8 | 9 | const regex = /:(?\+1|[-\w]+):/g; 10 | 11 | const gemojiReplacer = (_, name: string) => { 12 | switch (Owlmoji.kind(name)) { 13 | case 'gemoji': { 14 | const node: Gemoji = { 15 | type: NodeTypes.emoji, 16 | value: Owlmoji.nameToEmoji[name], 17 | name, 18 | }; 19 | 20 | return node; 21 | } 22 | case 'fontawesome': { 23 | const node: FaEmoji = { 24 | type: NodeTypes.i, 25 | value: name, 26 | data: { 27 | hName: 'i', 28 | hProperties: { 29 | className: ['fa-regular', name], 30 | }, 31 | }, 32 | }; 33 | 34 | return node; 35 | } 36 | case 'owlmoji': { 37 | const node: Image = { 38 | type: 'image', 39 | title: `:${name}:`, 40 | alt: `:${name}:`, 41 | url: `/public/img/emojis/${name}.png`, 42 | data: { 43 | hProperties: { 44 | className: 'emoji', 45 | align: 'absmiddle', 46 | height: '20', 47 | width: '20', 48 | }, 49 | }, 50 | }; 51 | 52 | return node; 53 | } 54 | default: 55 | return false; 56 | } 57 | }; 58 | 59 | const gemojiTransformer = () => (tree: Root) => { 60 | findAndReplace(tree, [regex, gemojiReplacer]); 61 | 62 | return tree; 63 | }; 64 | 65 | export default gemojiTransformer; 66 | -------------------------------------------------------------------------------- /processor/transform/handle-missing-components.ts: -------------------------------------------------------------------------------- 1 | import type { CompileOpts } from '../../lib/compile'; 2 | import type { Parents } from 'mdast'; 3 | import type { Transform } from 'mdast-util-from-markdown'; 4 | import type { MdxJsxFlowElement, MdxJsxTextElement } from 'mdast-util-mdx'; 5 | 6 | import { visit } from 'unist-util-visit'; 7 | 8 | import * as Components from '../../components'; 9 | import mdast from '../../lib/mdast'; 10 | import { getExports, isMDXElement } from '../utils'; 11 | 12 | type HandleMissingComponentsProps = Pick; 13 | 14 | const handleMissingComponents = 15 | ({ components, missingComponents }: HandleMissingComponentsProps): Transform => 16 | tree => { 17 | const allComponents = new Set([ 18 | ...getExports(tree), 19 | ...Object.keys(Components), 20 | ...Object.keys(components), 21 | ...Object.values(components).flatMap(doc => getExports(mdast(doc))), 22 | 'Variable', 23 | 'TutorialTile', 24 | ]); 25 | 26 | visit( 27 | tree, 28 | isMDXElement, 29 | (node: MdxJsxFlowElement | MdxJsxTextElement, index: number, parent: Parents) => { 30 | if (allComponents.has(node.name) || node.name.match(/^[a-z]/)) return; 31 | 32 | if (missingComponents === 'throw') { 33 | throw new Error( 34 | `Expected component \`${node.name}\` to be defined: you likely forgot to import, pass, or provide it.`, 35 | ); 36 | } 37 | 38 | parent.children.splice(index, 1); 39 | }, 40 | true, 41 | ); 42 | }; 43 | 44 | export default handleMissingComponents; 45 | -------------------------------------------------------------------------------- /processor/transform/images.ts: -------------------------------------------------------------------------------- 1 | import type { ImageBlock } from '../../types'; 2 | import type { Node, Paragraph, Parents, Image } from 'mdast'; 3 | import type { MdxJsxFlowElement } from 'mdast-util-mdx'; 4 | 5 | import { visit } from 'unist-util-visit'; 6 | 7 | import { NodeTypes } from '../../enums'; 8 | import { mdast } from '../../lib'; 9 | import { getAttrs } from '../utils'; 10 | 11 | const isImage = (node: Node): node is Image => node.type === 'image'; 12 | 13 | const imageTransformer = () => (tree: Node) => { 14 | visit(tree, 'paragraph', (node: Paragraph, i: number, parent: Parents) => { 15 | // check if inline 16 | if (parent.type !== 'root' || node.children?.length > 1) return; 17 | 18 | const child = node.children[0]; 19 | if (!isImage(child)) return; 20 | 21 | const { alt, url, title } = child; 22 | 23 | const attrs = { 24 | alt, 25 | title, 26 | children: [], 27 | src: url, 28 | }; 29 | 30 | const newNode: ImageBlock = { 31 | type: NodeTypes.imageBlock, 32 | ...attrs, 33 | /* 34 | * @note: Using data.hName here means that we don't have to transform 35 | * this to an MdxJsxFlowElement, and rehype will transform it correctly 36 | */ 37 | data: { 38 | hName: 'img', 39 | hProperties: attrs, 40 | }, 41 | position: node.position, 42 | }; 43 | 44 | parent.children.splice(i, 1, newNode); 45 | }); 46 | 47 | const isImageBlock = (node: MdxJsxFlowElement) => node.name === 'Image'; 48 | 49 | visit(tree, isImageBlock, (node: MdxJsxFlowElement) => { 50 | const attrs = getAttrs(node); 51 | 52 | if (attrs.caption) { 53 | // @ts-expect-error - @todo: figure out how to coerce RootContent[] to 54 | // the correct type 55 | node.children = mdast(attrs.caption).children; 56 | } 57 | }); 58 | 59 | return tree; 60 | }; 61 | 62 | export default imageTransformer; 63 | -------------------------------------------------------------------------------- /processor/transform/index.ts: -------------------------------------------------------------------------------- 1 | import calloutTransformer from './callouts'; 2 | import codeTabsTransformer from './code-tabs'; 3 | import compatabilityTransfomer from './compatability'; 4 | import divTransformer from './div'; 5 | import embedTransformer from './embeds'; 6 | import gemojiTransformer from './gemoji+'; 7 | import handleMissingComponents from './handle-missing-components'; 8 | import imageTransformer from './images'; 9 | import injectComponents from './inject-components'; 10 | import mdxToHast from './mdx-to-hast'; 11 | import mermaidTransformer from './mermaid'; 12 | import readmeComponentsTransformer from './readme-components'; 13 | import readmeToMdx from './readme-to-mdx'; 14 | import tablesToJsx from './tables-to-jsx'; 15 | import tailwindTransformer from './tailwind'; 16 | import variablesTransformer from './variables'; 17 | 18 | export { 19 | compatabilityTransfomer, 20 | divTransformer, 21 | injectComponents, 22 | mdxToHast, 23 | mermaidTransformer, 24 | readmeComponentsTransformer, 25 | readmeToMdx, 26 | tablesToJsx, 27 | tailwindTransformer, 28 | handleMissingComponents, 29 | variablesTransformer, 30 | }; 31 | 32 | export const defaultTransforms = { 33 | calloutTransformer, 34 | codeTabsTransformer, 35 | embedTransformer, 36 | imageTransformer, 37 | gemojiTransformer, 38 | }; 39 | 40 | export default Object.values(defaultTransforms); 41 | -------------------------------------------------------------------------------- /processor/transform/inject-components.ts: -------------------------------------------------------------------------------- 1 | import type { MdastComponents } from '../../types'; 2 | import type { Parents } from 'mdast'; 3 | import type { Transform } from 'mdast-util-from-markdown'; 4 | import type { MdxJsxFlowElement, MdxJsxTextElement } from 'mdast-util-mdx'; 5 | 6 | import { visit } from 'unist-util-visit'; 7 | 8 | import { isMDXElement } from '../utils'; 9 | 10 | interface Options { 11 | components?: MdastComponents; 12 | } 13 | 14 | const inject = 15 | ({ components }: Options = {}) => 16 | (node: MdxJsxFlowElement | MdxJsxTextElement, index: number, parent: Parents) => { 17 | if (!(node.name in components)) return; 18 | 19 | const { children } = components[node.name]; 20 | parent.children.splice(index, children.length, ...children); 21 | }; 22 | 23 | const injectComponents = (opts: Options) => (): Transform => tree => { 24 | visit(tree, isMDXElement, inject(opts)); 25 | 26 | return tree; 27 | }; 28 | 29 | export default injectComponents; 30 | -------------------------------------------------------------------------------- /processor/transform/mdx-to-hast.ts: -------------------------------------------------------------------------------- 1 | import type { Parents } from 'mdast'; 2 | import type { Transform } from 'mdast-util-from-markdown'; 3 | import type { MdxJsxFlowElement, MdxJsxTextElement } from 'mdast-util-mdx'; 4 | 5 | import { visit } from 'unist-util-visit'; 6 | 7 | import * as Components from '../../components'; 8 | import { getAttrs, isMDXElement } from '../utils'; 9 | 10 | const setData = (node: MdxJsxFlowElement | MdxJsxTextElement, index: number, parent: Parents) => { 11 | if (!node.name) return; 12 | if (!(node.name in Components)) return; 13 | 14 | parent.children[index] = { 15 | ...node, 16 | data: { 17 | hName: node.name, 18 | hProperties: getAttrs(node), 19 | }, 20 | }; 21 | }; 22 | 23 | const mdxToHast = (): Transform => tree => { 24 | visit(tree, isMDXElement, setData); 25 | 26 | return tree; 27 | }; 28 | 29 | export default mdxToHast; 30 | -------------------------------------------------------------------------------- /processor/transform/mermaid.ts: -------------------------------------------------------------------------------- 1 | import type { Element } from 'hast'; 2 | import type { Node } from 'unist'; 3 | 4 | import { visit } from 'unist-util-visit'; 5 | 6 | const mermaidTransformer = () => (tree: Node) => { 7 | visit(tree, 'element', (node: Element) => { 8 | if (node.tagName !== 'pre' || node.children.length !== 1) return; 9 | 10 | const [child] = node.children; 11 | if (child.type === 'element' && child.tagName === 'code' && child.properties.lang === 'mermaid') { 12 | node.properties = { 13 | ...node.properties, 14 | className: ['mermaid', ...((node.properties.className as string[]) || [])], 15 | }; 16 | } 17 | }); 18 | 19 | return tree; 20 | }; 21 | 22 | export default mermaidTransformer; 23 | -------------------------------------------------------------------------------- /processor/transform/reusable-content.js: -------------------------------------------------------------------------------- 1 | import { visit } from 'unist-util-visit'; 2 | 3 | import { type } from '../parse/reusable-content-parser'; 4 | 5 | function reusableContent() { 6 | const { wrap = true } = this.data('reusableContent'); 7 | 8 | return tree => { 9 | if (wrap) return tree; 10 | 11 | visit(tree, type, (node, index, parent) => { 12 | parent.children.splice(index, 1, ...node.children); 13 | }); 14 | 15 | return tree; 16 | }; 17 | } 18 | 19 | export default reusableContent; 20 | -------------------------------------------------------------------------------- /processor/transform/table-cell-inline-code.js: -------------------------------------------------------------------------------- 1 | import { SKIP, visit } from 'unist-util-visit'; 2 | 3 | const rxEscapedPipe = /\\\|/g; 4 | 5 | /** 6 | * HAST Transformer that finds all inline code nodes within table cells and 7 | * unescapes any escaped pipe chars so that the editor outputs them without 8 | * escape chars. 9 | * 10 | * This appears to be a bug with remark-parse < ~8 11 | */ 12 | const tableCellInlineCode = () => tree => { 13 | visit(tree, [{ tagName: 'th' }, { tagName: 'td' }], tableCellNode => { 14 | visit(tableCellNode, { tagName: 'code' }, inlineCodeNode => { 15 | const textNode = inlineCodeNode.children[0]; 16 | 17 | if (textNode && rxEscapedPipe.test(textNode.value)) { 18 | textNode.value = textNode.value.replace(rxEscapedPipe, '|'); 19 | } 20 | }); 21 | 22 | return SKIP; 23 | }); 24 | }; 25 | 26 | export default tableCellInlineCode; 27 | -------------------------------------------------------------------------------- /processor/transform/tailwind.tsx: -------------------------------------------------------------------------------- 1 | import type { PhrasingContent, BlockContent, Root } from 'mdast'; 2 | import type { MdxJsxFlowElement, MdxJsxTextElement } from 'mdast-util-mdx'; 3 | import type { Plugin } from 'unified'; 4 | import type { VFile } from 'vfile'; 5 | 6 | import { visit, SKIP } from 'unist-util-visit'; 7 | 8 | import { isMDXElement, toAttributes, getExports } from '../utils'; 9 | 10 | interface TailwindRootOptions { 11 | components: Record; 12 | parseRoot?: boolean; 13 | } 14 | 15 | type Visitor = 16 | | ((node: MdxJsxFlowElement, index: number, parent: BlockContent) => undefined | void) 17 | | ((node: MdxJsxTextElement, index: number, parent: PhrasingContent) => undefined | void); 18 | 19 | const injectTailwindRoot = 20 | ({ components = {} }): Visitor => 21 | (node, index, parent) => { 22 | if (!('name' in node)) return; 23 | if (!(node.name in components)) return; 24 | if (!('children' in parent)) return; 25 | 26 | const attrs = { 27 | flow: node.type === 'mdxJsxFlowElement', 28 | }; 29 | 30 | const wrapper = { 31 | type: node.type, 32 | name: 'TailwindRoot', 33 | attributes: toAttributes(attrs), 34 | children: [node], 35 | }; 36 | 37 | parent.children.splice(index, 1, wrapper); 38 | 39 | // eslint-disable-next-line consistent-return 40 | return SKIP; 41 | }; 42 | 43 | const tailwind: Plugin<[TailwindRootOptions]> = 44 | ({ components }) => 45 | (tree: Root, vfile: VFile) => { 46 | const localComponents = getExports(tree).reduce((acc, name) => { 47 | acc[name] = String(vfile); 48 | return acc; 49 | }, {}); 50 | 51 | visit(tree, isMDXElement, injectTailwindRoot({ components: { ...components, ...localComponents } })); 52 | 53 | return tree; 54 | }; 55 | 56 | export default tailwind; 57 | -------------------------------------------------------------------------------- /sanitize.schema.js: -------------------------------------------------------------------------------- 1 | import { defaultSchema } from 'hast-util-sanitize/lib/schema'; 2 | 3 | const createSchema = ({ safeMode } = {}) => { 4 | const schema = JSON.parse(JSON.stringify(defaultSchema)); 5 | 6 | // Sanitization Schema Defaults 7 | schema.clobberPrefix = ''; 8 | 9 | schema.tagNames.push('span'); 10 | schema.attributes['*'].push('class', 'className', 'align'); 11 | if (!safeMode) { 12 | schema.attributes['*'].push('style'); 13 | } 14 | 15 | schema.tagNames.push('rdme-pin'); 16 | 17 | schema.tagNames.push('rdme-embed'); 18 | schema.attributes['rdme-embed'] = [ 19 | 'url', 20 | 'provider', 21 | 'html', 22 | 'title', 23 | 'href', 24 | 'iframe', 25 | 'width', 26 | 'height', 27 | 'image', 28 | 'favicon', 29 | 'align', 30 | ]; 31 | 32 | schema.attributes.a = ['href', 'title', 'class', 'className', 'download']; 33 | 34 | schema.tagNames.push('figure'); 35 | schema.tagNames.push('figcaption'); 36 | 37 | schema.tagNames.push('input'); // allow GitHub-style todo lists 38 | schema.ancestors.input = ['li']; 39 | 40 | return schema; 41 | }; 42 | 43 | export default createSchema; 44 | -------------------------------------------------------------------------------- /scripts/perf-test.js: -------------------------------------------------------------------------------- 1 | const childProcess = require('child_process'); 2 | const { Blob } = require('node:buffer'); 3 | 4 | const rdmd = require('..'); 5 | 6 | const mdBuffer = childProcess.execSync('cat ./docs/*', { encoding: 'utf8' }); 7 | 8 | const createDoc = bytes => { 9 | let doc = ''; 10 | 11 | while (new Blob([doc]).size < bytes) { 12 | const start = Math.ceil(Math.random() * mdBuffer.length); 13 | doc += mdBuffer.slice(start, start + bytes); 14 | } 15 | 16 | return doc; 17 | }; 18 | 19 | // https://stackoverflow.com/a/14919494 20 | function humanFileSize(bytes, si = false, dp = 1) { 21 | const thresh = si ? 1000 : 1024; 22 | 23 | if (Math.abs(bytes) < thresh) { 24 | return `${bytes} B`; 25 | } 26 | 27 | const units = si 28 | ? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] 29 | : ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']; 30 | let u = -1; 31 | const r = 10 ** dp; 32 | 33 | do { 34 | // eslint-disable-next-line no-param-reassign 35 | bytes /= thresh; 36 | // eslint-disable-next-line no-plusplus 37 | ++u; 38 | } while (Math.round(Math.abs(bytes) * r) / r >= thresh && u < units.length - 1); 39 | 40 | return `${bytes.toFixed(dp)} ${units[u]}`; 41 | } 42 | 43 | const max = 8; 44 | 45 | console.log('n : string size : duration'); 46 | 47 | new Array(max).fill(0).forEach((_, i) => { 48 | const bytes = 10 ** i; 49 | const doc = createDoc(bytes); 50 | const then = Date.now(); 51 | 52 | rdmd.mdast(doc); 53 | const duration = Date.now() - then; 54 | 55 | console.log(`${i} : ${humanFileSize(new Blob([doc]).size)} : ${duration / 1000} s`); 56 | }); 57 | -------------------------------------------------------------------------------- /stylelint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: '@readme/stylelint-config', 3 | rules: { 4 | 'alpha-value-notation': null, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /styles/components.scss: -------------------------------------------------------------------------------- 1 | @import '../components/Image/style'; 2 | @import '../components/Table/style'; 3 | @import '../components/TableOfContents/style'; 4 | @import '../components/Code/style'; 5 | @import '../components/CodeTabs/style'; 6 | @import '../components/Callout/style'; 7 | @import '../components/Heading/style'; 8 | @import '../components/Embed/style'; 9 | @import '../components/Glossary/style'; 10 | -------------------------------------------------------------------------------- /styles/main.scss: -------------------------------------------------------------------------------- 1 | @import './gfm'; 2 | @import './components'; 3 | 4 | :root { 5 | // --markdown-radius: 3px; 6 | // --markdown-edge: #eee; 7 | --markdown-text: inherit; 8 | --markdown-title: inherit; 9 | --markdown-title-font: inherit; 10 | --markdown-font: inherit; 11 | --markdown-font-size: inherit; 12 | --markdown-line-height: 1.5; 13 | } 14 | 15 | .field-description, 16 | .markdown-body { 17 | @include gfm; 18 | 19 | font-size: var(--markdown-font-size, 14px); 20 | } 21 | -------------------------------------------------------------------------------- /styles/mixins/dark-mode.scss: -------------------------------------------------------------------------------- 1 | /* We’re planning to move this in to the monorepo in which case 2 | we could share the dark mode mix in. Kelly is planning to take 3 | some time to make that move so can we add a comment here to 4 | circle back on this at that point? 5 | 6 | - Rafe 7 | April 2025 8 | */ 9 | 10 | @mixin dark-mode($global: false) { 11 | $root: &; 12 | 13 | @if not $root { 14 | [data-color-mode='dark'] { 15 | @content; 16 | } 17 | 18 | [data-color-mode='auto'], 19 | [data-color-mode='system'] { 20 | @media (prefers-color-scheme: dark) { 21 | @content; 22 | } 23 | } 24 | } @else if $global { 25 | :global([data-color-mode='dark']) & { 26 | @content; 27 | } 28 | 29 | :global([data-color-mode='auto']) &, 30 | :global([data-color-mode='system']) & { 31 | @media (prefers-color-scheme: dark) { 32 | @content; 33 | } 34 | } 35 | } @else { 36 | [data-color-mode='dark'] & { 37 | @content; 38 | } 39 | 40 | [data-color-mode='auto'] &, 41 | [data-color-mode='system'] & { 42 | @media (prefers-color-scheme: dark) { 43 | @content; 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "baseUrl": "./", 5 | "checkJs": false, 6 | "declaration": true, 7 | "isolatedModules": true, 8 | "jsx": "react", 9 | "lib": [ 10 | "ES2022", 11 | "DOM", // Loaded to our global type availability for access to `fetch`. 12 | "DOM.iterable" 13 | ], 14 | "module": "es2022", 15 | "moduleResolution": "Bundler", 16 | "outDir": "dist", 17 | "resolveJsonModule": true, 18 | "sourceMap": true, 19 | "target": "ES2022" 20 | }, 21 | "include": ["./index.ts", "./options.js", "./components", "./contexts", "./example", "./lib", "./processor"], 22 | "exclude": ["node_modules", "dist"] 23 | } 24 | -------------------------------------------------------------------------------- /utils/consts.ts: -------------------------------------------------------------------------------- 1 | export const tailwindPrefix = 'readme-tailwind'; 2 | -------------------------------------------------------------------------------- /utils/user.ts: -------------------------------------------------------------------------------- 1 | interface Default { 2 | default: string; 3 | name: string; 4 | } 5 | 6 | export interface Variables { 7 | defaults: Default[]; 8 | user: Record; 9 | } 10 | 11 | const User = (variables?: Variables) => { 12 | const { user = {}, defaults = [] } = variables || {}; 13 | 14 | return new Proxy(user, { 15 | get(target, attribute) { 16 | if (typeof attribute === 'symbol') { 17 | return ''; 18 | } 19 | 20 | if (attribute in target) { 21 | return target[attribute]; 22 | } 23 | 24 | const def = defaults.find((d: Default) => d.name === attribute); 25 | 26 | return def ? def.default : attribute.toUpperCase(); 27 | }, 28 | }); 29 | }; 30 | 31 | export default User; 32 | -------------------------------------------------------------------------------- /vitest-setup.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import '@testing-library/jest-dom'; 3 | import '@testing-library/jest-dom/vitest'; 4 | 5 | import './__tests__/matchers'; 6 | -------------------------------------------------------------------------------- /vitest.config.mts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import react from '@vitejs/plugin-react'; 3 | import { defineConfig } from 'vitest/config'; 4 | 5 | export default defineConfig({ 6 | plugins: [react()], 7 | test: { 8 | environment: 'jsdom', 9 | exclude: ['**/node_modules/**', '**/dist/**'], 10 | globals: true, 11 | setupFiles: ['./vitest-setup.js'], 12 | workspace: [ 13 | { 14 | extends: true, 15 | test: { 16 | exclude: ['__tests__/browser'], 17 | name: 'rdmd', 18 | }, 19 | }, 20 | ], 21 | }, 22 | }); 23 | -------------------------------------------------------------------------------- /vitest.d.ts: -------------------------------------------------------------------------------- 1 | interface CustomMatchers { 2 | toStrictEqualExceptPosition: () => R; 3 | } 4 | 5 | declare module 'vitest' { 6 | interface Assertion extends CustomMatchers {} 7 | interface AsymmetricMatchersContaining extends CustomMatchers {} 8 | } 9 | -------------------------------------------------------------------------------- /webpack.common.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies 2 | */ 3 | const ExtractCSS = require('mini-css-extract-plugin'); 4 | const sass = require('sass'); 5 | const webpack = require('webpack'); 6 | 7 | module.exports = { 8 | plugins: [ 9 | new ExtractCSS({ 10 | filename: '[name].css', 11 | }), 12 | new webpack.ProvidePlugin({ 13 | Buffer: ['buffer', 'Buffer'], 14 | }), 15 | new webpack.ProvidePlugin({ 16 | process: 'process/browser', 17 | }), 18 | ], 19 | module: { 20 | rules: [ 21 | { 22 | test: /\.tsx?$/, 23 | use: { 24 | loader: 'ts-loader', 25 | }, 26 | exclude: /node_modules/, 27 | }, 28 | { 29 | test: /\.jsx?$/, 30 | exclude: /node_modules\/(?!@readme\/[\w-]+\/)/, 31 | use: { 32 | loader: 'babel-loader', 33 | }, 34 | }, 35 | { 36 | test: /\.m?js$/, 37 | include: /node_modules/, 38 | type: 'javascript/auto', 39 | resolve: { 40 | fullySpecified: false, 41 | }, 42 | }, 43 | { test: /tailwindcss\/.*\.css$/, type: 'asset/source' }, 44 | { 45 | test: /\.css$/, 46 | exclude: /tailwindcss\/.*\.css$/, 47 | use: [ExtractCSS.loader, 'css-loader'], 48 | }, 49 | { 50 | test: /\.scss$/, 51 | use: [ 52 | ExtractCSS.loader, 53 | 'css-loader', 54 | { 55 | loader: 'sass-loader', 56 | options: { 57 | implementation: sass, 58 | }, 59 | }, 60 | ], 61 | }, 62 | { 63 | test: /\.(ttf|eot|svg|woff(2)?)(\?[a-z0-9=&.]+)?$/, 64 | exclude: /(node_modules)/, 65 | use: { 66 | loader: 'file-loader', 67 | options: { 68 | name: 'dist/fonts/[hash].[ext]', 69 | }, 70 | }, 71 | }, 72 | ], 73 | }, 74 | resolve: { 75 | extensions: ['.js', '.json', '.jsx', '.ts', '.tsx', '.md', '.css'], 76 | fallback: { buffer: require.resolve('buffer'), util: require.resolve('util/') }, 77 | }, 78 | }; 79 | -------------------------------------------------------------------------------- /webpack.dev.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies 2 | */ 3 | const path = require('path'); 4 | 5 | const webpack = require('webpack'); 6 | const { merge } = require('webpack-merge'); 7 | 8 | const common = require('./webpack.common'); 9 | 10 | const config = merge(common, { 11 | entry: { 12 | demo: './example/index.tsx', 13 | }, 14 | output: { 15 | path: path.resolve(__dirname, 'example/'), 16 | filename: '[name].js', 17 | }, 18 | devServer: { 19 | static: './example', 20 | compress: true, 21 | port: 9966, 22 | hot: true, 23 | }, 24 | devtool: 'eval', 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.(txt|mdx?)$/i, 29 | type: 'asset/source', 30 | }, 31 | ], 32 | }, 33 | plugins: [ 34 | new webpack.ProvidePlugin({ 35 | process: 'process/browser', 36 | }), 37 | ], 38 | resolve: { 39 | fallback: { 40 | fs: require.resolve('browserify-fs'), 41 | path: require.resolve('path-browserify'), 42 | stream: require.resolve('stream-browserify'), 43 | }, 44 | }, 45 | }); 46 | 47 | module.exports = config; 48 | --------------------------------------------------------------------------------