diff --git a/README.md b/README.md index 9605266d3..e6edaf151 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ Extends [`RunOptions`](https://mdxjs.com/packages/mdx/#runoptions) - `components` (`Record`, optional) -- An object of tag names to executed components. - `imports` (`Record`, optional) -- An object of modules to import. - `terms` (`GlossaryTerm[]`, optional) -- `variables` (`Variables`, optional) +- `variables` (`Variables`, optional) -- An object containing [user variables}(https://github.com/readmeio/variable). ### `RMDXModule` diff --git a/__tests__/browser/__image_snapshots__/markdown-test-js-visual-regression-tests-rdmd-syntax-renders-embeds-without-surprises-1-snap.png b/__tests__/browser/__image_snapshots__/markdown-test-js-visual-regression-tests-rdmd-syntax-renders-embeds-without-surprises-1-snap.png index 24ddd6c83..2e3a9e36e 100644 Binary files a/__tests__/browser/__image_snapshots__/markdown-test-js-visual-regression-tests-rdmd-syntax-renders-embeds-without-surprises-1-snap.png and b/__tests__/browser/__image_snapshots__/markdown-test-js-visual-regression-tests-rdmd-syntax-renders-embeds-without-surprises-1-snap.png differ diff --git a/__tests__/browser/__image_snapshots__/markdown-test-js-visual-regression-tests-rdmd-syntax-renders-vars-test-without-surprises-1-snap.png b/__tests__/browser/__image_snapshots__/markdown-test-js-visual-regression-tests-rdmd-syntax-renders-vars-test-without-surprises-1-snap.png index 747a49618..351446f1b 100644 Binary files a/__tests__/browser/__image_snapshots__/markdown-test-js-visual-regression-tests-rdmd-syntax-renders-vars-test-without-surprises-1-snap.png and b/__tests__/browser/__image_snapshots__/markdown-test-js-visual-regression-tests-rdmd-syntax-renders-vars-test-without-surprises-1-snap.png differ diff --git a/__tests__/compilers/compatability.test.ts b/__tests__/compilers/compatability.test.ts index 71d8c2391..fd71393f8 100644 --- a/__tests__/compilers/compatability.test.ts +++ b/__tests__/compilers/compatability.test.ts @@ -2,21 +2,6 @@ import { mdx } from '../../index'; import * as rdmd from '@readme/markdown-legacy'; describe('compatability with RDMD', () => { - it('compiles variable nodes', () => { - const ast = { - type: 'readme-variable', - text: 'parliament', - data: { - hName: 'readme-variable', - hProperties: { - variable: 'parliament', - }, - }, - }; - - expect(mdx(ast).trim()).toBe(''); - }); - it('compiles glossary nodes', () => { const ast = { type: 'readme-glossary-item', diff --git a/__tests__/compilers/variable.test.ts b/__tests__/compilers/variable.test.ts new file mode 100644 index 000000000..f6232e3b9 --- /dev/null +++ b/__tests__/compilers/variable.test.ts @@ -0,0 +1,16 @@ +import * as rmdx from '../../index'; + +describe('variable compiler', () => { + it('compiles back to the ', () => { + const mdx = ` +## Hello! + +{user.name} + +### Bye bye! + `; + const tree = rmdx.mdast(mdx); + + expect(rmdx.mdx(tree).trim()).toStrictEqual(mdx.trim()); + }); +}); diff --git a/__tests__/transformers/variables.test.tsx b/__tests__/transformers/variables.test.tsx new file mode 100644 index 000000000..8c45378d4 --- /dev/null +++ b/__tests__/transformers/variables.test.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import * as rmdx from '../../index'; +import { execute } from '../helpers'; +import { render, screen } from '@testing-library/react'; + +describe('variables transformer', () => { + it('renders user variables', async () => { + const mdx = '{user.name}'; + const variables = { + user: { + name: 'Test User', + }, + }; + const Content = await execute(mdx, { variables }); + + render(); + + expect(screen.findByText('Test User')).toBeDefined(); + }); + + it('renders user variables in a phrasing context', async () => { + const mdx = 'Hello, {user.name}!'; + const variables = { + user: { + name: 'Test User', + }, + }; + const Content = await execute(mdx, { variables }); + + render(); + + expect(screen.findByText('Test User')).toBeDefined(); + }); + + it('parses variables into the mdast', () => { + const mdx = `{user.name}`; + + // @ts-ignore + expect(rmdx.mdast(mdx)).toStrictEqualExceptPosition({ + children: [ + { + children: [], + data: { + hName: 'Variable', + hProperties: { + name: 'name', + }, + }, + type: 'readme-variable', + }, + ], + type: 'root', + }); + }); +}); diff --git a/__tests__/variable-parser.test.js b/__tests__/variable-parser.test.js deleted file mode 100644 index 2cbdccaf3..000000000 --- a/__tests__/variable-parser.test.js +++ /dev/null @@ -1,200 +0,0 @@ -import remarkParse from 'remark-parse'; -import unified from 'unified'; - -import parser from '../processor/parse/variable-parser'; - -test.skip('should output a variable node', () => { - const markdown = 'This is a test <>.'; - const ast = { - type: 'root', - children: [ - { - type: 'paragraph', - children: [ - { type: 'text', value: 'This is a test ' }, - { - type: 'readme-variable', - text: 'apiKey', - data: { - hName: 'readme-variable', - hProperties: { - variable: 'apiKey', - }, - }, - }, - { type: 'text', value: '.' }, - ], - }, - ], - }; - - expect(unified().use(remarkParse).use(parser).data('settings', { position: false }).parse(markdown)).toStrictEqual( - ast - ); -}); - -test.skip('should output a glossary node', () => { - const markdown = 'This is a test <>.'; - const ast = { - type: 'root', - children: [ - { - type: 'paragraph', - children: [ - { type: 'text', value: 'This is a test ' }, - { - type: 'readme-glossary-item', - data: { - hName: 'readme-glossary-item', - hProperties: { - term: 'item', - }, - }, - }, - { type: 'text', value: '.' }, - ], - }, - ], - }; - - expect(unified().use(remarkParse).use(parser).data('settings', { position: false }).parse(markdown)).toStrictEqual( - ast - ); -}); - -test.skip('should allow whitespace in glossary names', () => { - const markdown = 'This is a test <>.'; - const ast = { - type: 'root', - children: [ - { - type: 'paragraph', - children: [ - { type: 'text', value: 'This is a test ' }, - { - type: 'readme-glossary-item', - data: { - hName: 'readme-glossary-item', - hProperties: { - term: 'item name', - }, - }, - }, - { type: 'text', value: '.' }, - ], - }, - ], - }; - - expect(unified().use(remarkParse).use(parser).data('settings', { position: false }).parse(markdown)).toStrictEqual( - ast - ); -}); - -test.skip('should allow underscored glossary terms', () => { - const markdown = 'This is a test <>.'; - const ast = { - type: 'root', - children: [ - { - type: 'paragraph', - children: [ - { type: 'text', value: 'This is a test ' }, - { - type: 'readme-glossary-item', - data: { - hName: 'readme-glossary-item', - hProperties: { - term: 'underscored_term', - }, - }, - }, - { type: 'text', value: '.' }, - ], - }, - ], - }; - - expect(unified().use(remarkParse).use(parser).data('settings', { position: false }).parse(markdown)).toStrictEqual( - ast - ); -}); - -test.skip('should allow numeric characters in glossary terms', () => { - const markdown = 'This is a test <>.'; - const ast = { - type: 'root', - children: [ - { - type: 'paragraph', - children: [ - { type: 'text', value: 'This is a test ' }, - { - type: 'readme-glossary-item', - data: { - hName: 'readme-glossary-item', - hProperties: { - term: 'P2P 123 Abc', - }, - }, - }, - { type: 'text', value: '.' }, - ], - }, - ], - }; - - expect(unified().use(remarkParse).use(parser).data('settings', { position: false }).parse(markdown)).toStrictEqual( - ast - ); -}); - -test.skip('should allow non-english glossary terms', () => { - const markdown = 'This is a test <>.'; - const ast = { - type: 'root', - children: [ - { - type: 'paragraph', - children: [ - { type: 'text', value: 'This is a test ' }, - { - type: 'readme-glossary-item', - data: { - hName: 'readme-glossary-item', - hProperties: { - term: 'ラベル', - }, - }, - }, - { type: 'text', value: '.' }, - ], - }, - ], - }; - - expect(unified().use(remarkParse).use(parser).data('settings', { position: false }).parse(markdown)).toStrictEqual( - ast - ); -}); - -test.skip('should allow escape variables to remain', () => { - const markdown = 'This is a test escaped key \\<>.'; - const ast = { - type: 'root', - children: [ - { - type: 'paragraph', - children: [ - { type: 'text', value: 'This is a test escaped key ' }, - { type: 'text', value: '<>' }, - { type: 'text', value: '.' }, - ], - }, - ], - }; - - expect(unified().use(remarkParse).use(parser).data('settings', { position: false }).parse(markdown)).toStrictEqual( - ast - ); -}); diff --git a/components/Code/index.tsx b/components/Code/index.tsx index 003c2d97b..1add62749 100644 --- a/components/Code/index.tsx +++ b/components/Code/index.tsx @@ -55,7 +55,7 @@ const Code = (props: CodeProps) => { }; const code = value ?? (Array.isArray(children) ? children[0] : children) ?? ''; - const highlightedCode = syntaxHighlighter && code ? syntaxHighlighter(code, language, codeOpts) : code; + const highlightedCode = syntaxHighlighter && code ? syntaxHighlighter(code, language, codeOpts, { mdx: true }) : code; return ( <> diff --git a/docs/variable-tests.md b/docs/variable-tests.md index f034aac7b..f2abebb8b 100644 --- a/docs/variable-tests.md +++ b/docs/variable-tests.md @@ -4,16 +4,20 @@ category: 5fdf9fc9c2a7ef443e937315 hidden: true --- - and `` and: +This is the variable `defvar`: {user.defvar} + +Ok, but this one is defined: {user.email} + +It **does** render in code blocks: ``` - +{user.defvar} ``` -and +And if you don't want that, you can escape it: -```js -const xyz = ""; +``` +\{user.defvar} ``` ## Glossary Items diff --git a/example/Doc.tsx b/example/Doc.tsx index 223b92a3d..c85b0a90b 100644 --- a/example/Doc.tsx +++ b/example/Doc.tsx @@ -11,6 +11,15 @@ const components = { > 📘 It can render JSX components! `, + Test: ` +export const Test = ({ color = 'thistle' } = {}) => { + return
+ Hello, World! +
; +}; + +export default Test; + `, }; const executedComponents = {}; @@ -18,13 +27,6 @@ Object.entries(components).forEach(async ([tag, body]) => { executedComponents[tag] = await mdx.run(mdx.compile(body)); }); -const variables = { - user: { - email: 'test@example.com', - }, - defaults: [], -}; - const terms = [ { term: 'demo', @@ -40,6 +42,19 @@ const terms = [ }, ]; +const variables = { + user: { + email: 'kelly@readme.io', + name: 'kelly joseph price', + }, + defaults: [ + { + name: 'defvar', + default: '(default value for defvar)', + }, + ], +}; + const Doc = () => { const { fixture } = useParams(); const [searchParams] = useSearchParams(); diff --git a/package-lock.json b/package-lock.json index e6fd9d3d6..8c4384f1f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@mdx-js/mdx": "^3.0.0", "@readme/emojis": "^5.1.0", - "@readme/syntax-highlighter": "^13.0.0", + "@readme/syntax-highlighter": "^13.1.0", "copy-to-clipboard": "^3.3.2", "core-js": "^3.36.1", "debug": "^4.3.4", @@ -5731,37 +5731,28 @@ } }, "node_modules/@readme/syntax-highlighter": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/@readme/syntax-highlighter/-/syntax-highlighter-13.0.0.tgz", - "integrity": "sha512-Gkp2azxf5drSp5KNwHEib/38ai6fcSvpiyfjPW1pNdU92/OBYKd5i1I1F7VHnZPc5PsbNBB9DOz7BwCzk4gROA==", + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/@readme/syntax-highlighter/-/syntax-highlighter-13.1.0.tgz", + "integrity": "sha512-qJbKxYNGKIL8D1G10WsCfhAWjbtb+7x5RU8YWMbH/3sno2dVPuYhjwMOuAlu55UzJ9u5/TfGiZuAUpg1RqTGSw==", "dependencies": { "codemirror": "^5.54.0", "codemirror-graphql": "1.0.2", "prop-types": "^15.7.2", - "react-codemirror2": "^7.2.1" + "react-codemirror2": "^8.0.0" }, "engines": { "node": ">=18" }, "peerDependencies": { - "@readme/variable": "15.x || 16.x", + "@readme/variable": "^16.2.0", "react": "16.x || 17.x || 18.x", "react-dom": "16.x || 17.x || 18.x" } }, - "node_modules/@readme/syntax-highlighter/node_modules/react-codemirror2": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/react-codemirror2/-/react-codemirror2-7.3.0.tgz", - "integrity": "sha512-gCgJPXDX+5iaPolkHAu1YbJ92a2yL7Je4TuyO3QEqOtI/d6mbEk08l0oIm18R4ctuT/Sl87X63xIMBnRQBXYXA==", - "peerDependencies": { - "codemirror": "5.x", - "react": ">=15.5 <=17.x" - } - }, "node_modules/@readme/variable": { - "version": "16.1.0", - "resolved": "https://registry.npmjs.org/@readme/variable/-/variable-16.1.0.tgz", - "integrity": "sha512-f4FDP4YE92Wjtie4IFw89FaM4Vgy4vkhsOxcozqRl6/7X2uPvAMVku4s3z7FKsKf0p52LgoeYkESnweUGMjexQ==", + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/@readme/variable/-/variable-16.2.0.tgz", + "integrity": "sha512-Kx9oDBKIkUHAdOuq6751WcBd0/bJPWQsQrwbpwzSPwR966cyxm4FmkHOQ6xlNrUG3MjcEGHdyHnjBwiqC5igKw==", "peer": true, "dependencies": { "classnames": "^2.2.6", @@ -26539,6 +26530,15 @@ "node": ">=0.10.0" } }, + "node_modules/react-codemirror2": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/react-codemirror2/-/react-codemirror2-8.0.0.tgz", + "integrity": "sha512-JIbhXoghvX0BrasIoCQvRxBPIU78plfjF1Buz0gaMFvZXwEDjkCYBkQhucoOtudQ7ikbB1jJUnmCsutElti7yA==", + "peerDependencies": { + "codemirror": "5.x", + "react": ">=15.5 <=18.x" + } + }, "node_modules/react-dom": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", diff --git a/package.json b/package.json index 1e06d966f..a7e61072b 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "dependencies": { "@mdx-js/mdx": "^3.0.0", "@readme/emojis": "^5.1.0", - "@readme/syntax-highlighter": "^13.0.0", + "@readme/syntax-highlighter": "^13.1.0", "copy-to-clipboard": "^3.3.2", "core-js": "^3.36.1", "debug": "^4.3.4", diff --git a/processor/compile/compatibility.ts b/processor/compile/compatibility.ts index 2c717aa71..fab585bac 100644 --- a/processor/compile/compatibility.ts +++ b/processor/compile/compatibility.ts @@ -5,7 +5,6 @@ import { toXml } from 'xast-util-to-xml'; import { NodeTypes } from '../../enums'; type CompatNodes = - | { type: NodeTypes.variable; text: string } | { type: NodeTypes.glossary; data: { hProperties: { term: string } } } | { type: NodeTypes.glossary; data: { hName: 'Glossary' }; children: [{ type: 'text'; value: string }] } | { type: NodeTypes.reusableContent; tag: string } @@ -30,8 +29,6 @@ const html = (node: Html) => { const compatibility = (node: CompatNodes) => { switch (node.type) { - case NodeTypes.variable: - return ``; case NodeTypes.glossary: // @ts-expect-error const term = node.data?.hProperties?.term || node.children[0].value; diff --git a/processor/compile/index.ts b/processor/compile/index.ts index b687be0eb..16a0234a6 100644 --- a/processor/compile/index.ts +++ b/processor/compile/index.ts @@ -5,6 +5,7 @@ import gemoji from './gemoji'; import htmlBlock from './html-block'; import image from './image'; import compatibility from './compatibility'; +import variable from './variable'; import { NodeTypes } from '../../enums'; function compilers() { @@ -14,14 +15,14 @@ function compilers() { const handlers = { [NodeTypes.callout]: callout, - [NodeTypes.emoji]: gemoji, [NodeTypes.codeTabs]: codeTabs, [NodeTypes.embedBlock]: embed, + [NodeTypes.emoji]: gemoji, + [NodeTypes.glossary]: compatibility, [NodeTypes.htmlBlock]: htmlBlock, [NodeTypes.imageBlock]: image, - [NodeTypes.variable]: compatibility, - [NodeTypes.glossary]: compatibility, [NodeTypes.reusableContent]: compatibility, + [NodeTypes.variable]: variable, escape: compatibility, html: compatibility, }; diff --git a/processor/compile/variable.ts b/processor/compile/variable.ts new file mode 100644 index 000000000..4537ad71b --- /dev/null +++ b/processor/compile/variable.ts @@ -0,0 +1,5 @@ +import { Variable } from '../../types'; + +const variable = (node: Variable) => `{user.${node.data.hProperties.name}}`; + +export default variable; diff --git a/processor/transform/index.ts b/processor/transform/index.ts index 65dcac75e..eba60096a 100644 --- a/processor/transform/index.ts +++ b/processor/transform/index.ts @@ -6,7 +6,15 @@ import gemojiTransformer from './gemoji+'; import injectComponents from './inject-components'; import readmeComponentsTransformer from './readme-components'; import readmeToMdx from './readme-to-mdx'; +import variablesTransformer from './variables'; -export { readmeComponentsTransformer, readmeToMdx, injectComponents }; +export { readmeComponentsTransformer, readmeToMdx, injectComponents, variablesTransformer }; -export default [calloutTransformer, codeTabsTransfromer, embedTransformer, imageTransformer, gemojiTransformer]; +export default [ + calloutTransformer, + codeTabsTransfromer, + embedTransformer, + imageTransformer, + gemojiTransformer, + variablesTransformer, +]; diff --git a/processor/transform/readme-components.ts b/processor/transform/readme-components.ts index bb0af441b..92d6c3c12 100644 --- a/processor/transform/readme-components.ts +++ b/processor/transform/readme-components.ts @@ -52,7 +52,6 @@ const coerceJsxToMd = const { alt = '', url, title = null } = getAttrs>(node); const attrs = getAttrs(node); - const mdNode: ImageBlock = { alt, position, @@ -119,7 +118,7 @@ const coerceJsxToMd = : { data: { hName: node.name, - ...(hProperties ?? hProperties), + ...(Object.keys(hProperties).length && { hProperties }), }, }), position: node.position, diff --git a/processor/transform/variables.ts b/processor/transform/variables.ts new file mode 100644 index 000000000..a6c6992cd --- /dev/null +++ b/processor/transform/variables.ts @@ -0,0 +1,32 @@ +import { Transform } from 'mdast-util-from-markdown'; +import { MdxJsxTextElement } from 'mdast-util-mdx-jsx'; + +import { visit } from 'unist-util-visit'; + +const variables = (): Transform => tree => { + visit(tree, (node, index, parent) => { + if (!['mdxFlowExpression', 'mdxTextExpression'].includes(node.type) || !('value' in node)) return; + + const match = node.value.match(/^user\.(?.*)$/); + if (!match) return; + + let variable = { + type: 'mdxJsxTextElement', + name: 'Variable', + attributes: [ + { + type: 'mdxJsxAttribute', + name: 'name', + value: match.groups.value, + }, + ], + children: [], + } as MdxJsxTextElement; + + parent.children.splice(index, 1, variable); + }); + + return tree; +}; + +export default variables; diff --git a/types.d.ts b/types.d.ts index f3b659cec..63813cbf0 100644 --- a/types.d.ts +++ b/types.d.ts @@ -93,6 +93,15 @@ interface TutorialTile extends Node { type: NodeTypes.tutorialTile; } +interface Variable extends Node { + data: Data & { + hName: 'Variable'; + hProperties: { + name: string; + }; + }; +} + declare module 'mdast' { interface BlockContentMap { [NodeTypes.callout]: Callout; @@ -129,6 +138,11 @@ interface TocList extends Element { children: TocListItem[]; } +interface Variables { + user: Record; + defaults: { name: string; default: string }[]; +} + interface TocListItem extends Element { tagName: 'li'; children: (TocList | TocEntry)[]; diff --git a/webpack.dev.js b/webpack.dev.js index c026856b6..06446c14d 100644 --- a/webpack.dev.js +++ b/webpack.dev.js @@ -21,6 +21,7 @@ const config = merge(common, { port: 9966, hot: true, }, + devtool: 'eval', module: { rules: [ {