diff --git a/examples/vue/src/App.vue b/examples/vue/src/App.vue index 5dca6a29b..47127c10f 100644 --- a/examples/vue/src/App.vue +++ b/examples/vue/src/App.vue @@ -1,17 +1,30 @@ diff --git a/examples/vue/src/test.mdx b/examples/vue/src/test.mdx index e46f900ca..72bb16dfc 100644 --- a/examples/vue/src/test.mdx +++ b/examples/vue/src/test.mdx @@ -2,4 +2,4 @@ Here’s more stuff -
+
diff --git a/package.json b/package.json index 7e64a92a4..7f23c81d6 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "@mdx-js/mdx": "^1.5.8", "@mdx-js/react": "^1.5.8", "@mdx-js/test-util": "^1.5.8", - "@mdx-js/vue": "^1.5.8", + "@mdx-js/vue": "^1.5.9", "@pkgr/rollup": "0.10.3", "@vue/babel-preset-jsx": "1.1.2", "@vue/test-utils": "1.0.0-beta.33", @@ -109,9 +109,7 @@ "jest": { "testPathIgnorePatterns": [ "/.cache/", - "/examples/create-react-app", - "/packages/vue", - "/packages/vue-loader" + "/examples/create-react-app" ], "testEnvironment": "node" }, diff --git a/packages/vue-loader/index.js b/packages/vue-loader/index.js index 4ef255a07..075ae772f 100644 --- a/packages/vue-loader/index.js +++ b/packages/vue-loader/index.js @@ -14,19 +14,28 @@ module.exports = async function (content) { return callback(err) } - const code = `// vue babel plugin doesn't support the pragma replacement -import {mdx} from '@mdx-js/vue' + const code = `// vue babel plugin doesn't support pragma replacement +import { mdx } from '@mdx-js/vue' let h; ${result} export default { name: 'Mdx', - render(vueCreateElement) { - h = mdx.bind({vueCreateElement}) - return MDXContent({}) + inject: { + $mdxComponents: { + default: () => () => ({}) + } + }, + computed: { + components() { + return this.$mdxComponents() + } + }, + render(createElement) { + h = mdx.bind({ createElement, components: this.components }) + return MDXContent({ components: this.components }) } } ` - return callback(null, code) } diff --git a/packages/vue-loader/package.json b/packages/vue-loader/package.json index 71e896d3f..e07280d1a 100644 --- a/packages/vue-loader/package.json +++ b/packages/vue-loader/package.json @@ -15,7 +15,8 @@ "Tim Neutkens ", "Matija Marohnić ", "Titus Wormer (https://wooorm.com)", - "JounQin (https://www.1stg.me)" + "JounQin (https://www.1stg.me)", + "Jonathan Bakebwa (https://jbakebwa.dev)" ], "license": "MIT", "files": [ diff --git a/packages/vue-loader/test/test.js b/packages/vue-loader/test/test.js index 19d3b9bf3..220891e5a 100644 --- a/packages/vue-loader/test/test.js +++ b/packages/vue-loader/test/test.js @@ -1,55 +1,28 @@ const path = require('path') -const webpack = require('webpack') -const MemoryFs = require('memory-fs') -const {VueJSXCompiler} = require('@mdx-js/vue') +const fs = require('fs').promises +const {transformAsync} = require('@babel/core') -const testFixture = fixture => { - const fileName = `./${fixture}` +const loader = require('..') - const compiler = webpack({ - context: __dirname, - entry: `./${fixture}`, - output: { - path: path.resolve(__dirname), - filename: 'bundle.js' - }, - node: { - fs: 'empty' - }, - module: { - rules: [ - { - test: /\.md?$/, - use: [ - { - loader: 'babel-loader', - options: { - presets: ['@babel/preset-env'], - plugins: ['transform-vue-jsx'] - } - }, - { - loader: path.resolve(__dirname, '..'), - options: { - compilers: [VueJSXCompiler] - } - } - ] - } - ] - } - }) +const testFixture = async fixture => { + const str = await fs.readFile(path.join(__dirname, fixture), 'utf-8') - compiler.outputFileSystem = new MemoryFs() + let result + await loader.bind({ + async: () => (err, res) => { + if (err) { + throw err + } - return new Promise((resolve, reject) => { - compiler.run((err, stats) => { - if (err) reject(err) - const module = stats.toJson().modules.find(m => m.name === fileName) - .source - resolve(module) - }) + result = res + } + })(str) + const {code} = await transformAsync(result, { + presets: ['@babel/preset-env'], + plugins: ['transform-vue-jsx'] }) + + return code } test('it loads markdown and returns a component', async () => { diff --git a/packages/vue/.babelrc b/packages/vue/.babelrc deleted file mode 100644 index aacb3cf68..000000000 --- a/packages/vue/.babelrc +++ /dev/null @@ -1,12 +0,0 @@ -{ - "presets": [ - [ - "@babel/env", - { - "corejs": 2, - "useBuiltIns": "usage" - } - ], - "@vue/babel-preset-jsx" - ] -} diff --git a/packages/vue/package.json b/packages/vue/package.json index 68694b765..1623ad7f7 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -2,29 +2,41 @@ "name": "@mdx-js/vue", "version": "1.5.9", "description": "MDX support for Vue components", - "repository": "mdx-js/mdx", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - }, - "contributors": [ - "JounQin (https://www.1stg.me)" - ], - "license": "MIT", "main": "dist/cjs.js", "module": "dist/esm.js", + "license": "MIT", + "author": "Jonathan Bakebwa (https://jbakebwa.dev)", "files": [ "dist" ], + "scripts": { + "dev": "watch 'yarn build' src", + "build": "yarn build:cjs && yarn build:es", + "build:cjs": "cross-env NODE_ENV=production BABEL_ENV=cjs babel src -d dist --copy-files", + "build:es": "cross-env NODE_ENV=production BABEL_ENV=es babel src -d dist/es --copy-files", + "test": "jest" + }, + "devDependencies": { + "@babel/cli": "^7.8.4", + "@babel/core": "^7.9.0", + "@babel/preset-env": "^7.9.5", + "cross-env": "^7.0.2", + "watch": "^1.0.2" + }, + "babel": { + "presets": [ + "@babel/preset-env" + ], + "exclude": [ + "node_modules" + ] + }, "keywords": [ "markdown", "vue", "mdx", "remark" ], - "scripts": { - "no-test": "jest" - }, "jest": { "testEnvironment": "jsdom" } diff --git a/packages/vue/src/create-element.js b/packages/vue/src/create-element.js index 20bed515d..979851fa5 100644 --- a/packages/vue/src/create-element.js +++ b/packages/vue/src/create-element.js @@ -1,3 +1,61 @@ +/** + * MDX default components + */ +const DEFAULTS = { + inlineCode: 'code', + wrapper: 'div' +} + +/** + * Renders final tag/component + * @param {Vue.Component|String} type Element or tag to render + * @param {Object|Array} props Props and attributes for element + * @param {Array} children Array of child nodes for component + * @returns {Vue.VNode} VNode of final rendered element + */ export default function(type, props, children) { - return this.vueCreateElement(type, props, children) + + const h = this.createElement + const components = this.components + const defaults = Object.keys(DEFAULTS) + + let tag + let elProps = props + + // We check context to see if the element/tag + // is provided in the MDXProvider context. + if (Object.keys(components).includes(type)) { + // We check to see if props is of type object. + // If it is, then we pass them into the MDXContext component + const componentProps = typeof props === 'object' ? props : undefined + tag = components[type](componentProps) + + } else if (defaults.includes(type)) { + + tag = DEFAULTS[type] + // Remove components object from attrs + const { components, ...attrs } = elProps.attrs + elProps = { + attrs + } + + // Render final tag if component is not provided in context + } else { + tag = type + + if (['a', 'input', 'img'].includes(tag)) { + const { attrs, ...domProps } = elProps + const data = { + attrs: attrs, + domProps + } + + elProps = { + ...elProps, + ...data + } + } + } + + return h(tag, elProps, children) } diff --git a/packages/vue/src/index.js b/packages/vue/src/index.js index 7e4d13a4e..b63c739f6 100644 --- a/packages/vue/src/index.js +++ b/packages/vue/src/index.js @@ -1 +1,2 @@ -export {default as mdx} from './create-element' +export { default as mdx } from './create-element'; +export { default as MDXProvider } from './mdx-provider' \ No newline at end of file diff --git a/packages/vue/src/mdx-hast-to-vue-jsx.js b/packages/vue/src/mdx-hast-to-vue-jsx.js deleted file mode 100644 index f4f598faa..000000000 --- a/packages/vue/src/mdx-hast-to-vue-jsx.js +++ /dev/null @@ -1,105 +0,0 @@ -function toVueJSX(node, parentNode = {}, options = {}) { - let children = '' - - if (node.type === 'root') { - const importNodes = [] - const exportNodes = [] - const jsxNodes = [] - let layout - for (const childNode of node.children) { - if (childNode.type === 'import') { - importNodes.push(childNode) - continue - } - - if (childNode.type === 'export') { - if (childNode.default) { - layout = childNode.value - .replace(/^export\s+default\s+/, '') - .replace(/;\s*$/, '') - continue - } - - exportNodes.push(childNode) - continue - } - - jsxNodes.push(childNode) - } - - return ( - importNodes.map(childNode => toVueJSX(childNode, node)).join('\n') + - '\n' + - exportNodes.map(childNode => toVueJSX(childNode, node)).join('\n') + - '\n' + - (options.skipExport ? '' : toVueExport(layout, jsxNodes, node)) - ) - } - - // Recursively walk through children - if (node.children) { - children = node.children - .map(childNode => toVueJSX(childNode, node)) - .join('') - } - - if (node.type === 'comment') { - return node.value.replace('', '*/}') - } - - if (node.type === 'element') { - let props = '' - - if (Array.isArray(node.properties.className)) { - node.properties.className = node.properties.className.join(' ') - } - - if (Object.keys(node.properties).length > 0) { - props = JSON.stringify(node.properties) - } - - return `${children}` - } - - // Wraps all text nodes except new lines inside template string, so that we don't run into escaping issues. - if (node.type === 'text') { - return node.value === '\n' - ? node.value - : '{`' + node.value.replace(/`/g, '\\`').replace(/\$/g, '\\$') + '`}' - } - - if (node.type === 'import' || node.type === 'export' || node.type === 'jsx') { - return node.value - } -} - -function toVueExport(layout, jsxNodes, node) { - return ` - export default { - props: { - components: { - type: Object, - default: {} - } - }, - render() { - return ( - - ${jsxNodes.map(childNode => toVueJSX(childNode, node)).join('')} - - ); - } - } - ` -} - -export function VueJSXCompiler(options = {}) { - this.Compiler = tree => { - return toVueJSX(tree, {}, options) - } -} diff --git a/packages/vue/src/mdx-provider.js b/packages/vue/src/mdx-provider.js index 11b81b8e8..98439b928 100644 --- a/packages/vue/src/mdx-provider.js +++ b/packages/vue/src/mdx-provider.js @@ -1,14 +1,26 @@ -export default { +/** + * MDXProvider component + * + * This component custom components from the user and + * provides them to the context tree for all output markdown. + * + */ +const MDXProvider = { + name: 'MDXProvider', props: { - components: Object, - required: true + components: { + type: Object, + default: () => ({}) + }, }, provide() { return { - contextComponents: this.components - } + $mdxComponents: () => this.components + }; }, - render() { - return
{this.$slots.default}
+ render(h) { + return h('div', this.$slots.default); } -} +}; + +export default MDXProvider diff --git a/packages/vue/src/mdx-tag.js b/packages/vue/src/mdx-tag.js deleted file mode 100644 index c09d16bf3..000000000 --- a/packages/vue/src/mdx-tag.js +++ /dev/null @@ -1,38 +0,0 @@ -export default { - props: { - name: String, - components: { - type: Object, - default: () => ({}) - }, - props: { - type: Object, - default: () => ({}) - }, - Layout: Object, - layoutProps: { - type: Object, - default: () => ({}) - } - }, - inject: { - contextComponents: { - default: {} - } - }, - render() { - if (this.Layout) { - return ( - - {this.$slots.default} - - ) - } - const Component = - this.components[this.name] || - this.contextComponents[this.name] || - this.name - const childProps = {...this.props} - return {this.$slots.default} - } -} diff --git a/packages/vue/test/compiler.test.js b/packages/vue/test/compiler.test.js deleted file mode 100644 index ef457e106..000000000 --- a/packages/vue/test/compiler.test.js +++ /dev/null @@ -1,137 +0,0 @@ -import {parse} from '@babel/core' -import mdx from '@mdx-js/mdx' -import fs from 'fs' -import path from 'path' -import {select} from 'hast-util-select' -import {VueJSXCompiler} from '../src/mdx-hast-to-vue-jsx' - -const fixtureBlogPost = fs.readFileSync( - path.join(__dirname, './fixtures/blog-post.md') -) - -const parseCode = code => - parse(code, { - plugins: ['transform-vue-jsx'] - }) - -const mdxWithVueCompiler = (md, options = {}) => - mdx(md, { - ...options, - ...{compilers: [VueJSXCompiler]} - }) - -it('Should output parsable JSX with Vue', async () => { - const result = await mdxWithVueCompiler('Hello World') - parseCode(result) -}) - -it('Should output parseable JSX when using < or >', async () => { - const result = await mdxWithVueCompiler(` - # Hello, MDX - - I <3 Markdown and JSX - `) - parseCode(result) -}) - -it('Should compile sample blog post', async () => { - const result = await mdxWithVueCompiler(fixtureBlogPost) - parseCode(result) -}) - -it('Should render blockquote correctly', async () => { - const result = await mdxWithVueCompiler('> test\n\n> `test`') - parseCode(result) -}) - -it('Should render HTML inside inlineCode correctly', async () => { - const result = await mdxWithVueCompiler('`
`') - expect( - result.includes( - '{`
`}' - ) - ).toBeTruthy() -}) - -it.skip('Should support comments', async () => { - const result = await mdxWithVueCompiler(` -A paragraph - -\`\`\`md - -\`\`\` - -
- {/* a nested JSX comment */} - -
- `) - - expect(result.includes('')).toBeTruthy() - expect(result.includes('{/* a nested JSX comment */}')).toBeTruthy() - expect(result.includes('{/* a nested Markdown comment */}')).toBeTruthy() -}) - -it('Should not include export wrapper if skipExport is true', async () => { - const result = await mdxWithVueCompiler('> test\n\n> `test`', { - skipExport: true - }) - - expect( - result.includes(` - export default { - render() { - `) - ).toBeFalsy() -}) - -it('Should recognize components as properties', async () => { - const result = await mdxWithVueCompiler('# Hello\n\n') - expect( - result.includes( - '{`Hello`}\n' - ) - ).toBeTruthy() -}) - -it('Should render elements without wrapping blank new lines', async () => { - const result = await mdxWithVueCompiler(` - | Test | Table | - | :--- | :---- | - | Col1 | Col2 |`) - - expect(result.includes('{`\n`}')).toBe(false) -}) - -it('Should await and render async plugins', async () => { - const result = await mdxWithVueCompiler(fixtureBlogPost, { - rehypePlugins: [ - () => tree => - (() => { - const headingNode = select('h1', tree) - const textNode = headingNode.children[0] - textNode.value = textNode.value.toUpperCase() - })() - ] - }) - - expect(result).toMatch(/HELLO, WORLD!/) -}) - -it('Should parse and render footnotes', async () => { - const result = await mdxWithVueCompiler( - 'This is a paragraph with a [^footnote]\n\n[^footnote]: Here is the footnote' - ) - - expect( - result.includes( - '' - ) - ) - - expect( - result.includes( - '' - ) - ) -}, 10000) diff --git a/packages/vue/test/fixtures/blog-post.md b/packages/vue/test/fixtures/blog-post.md deleted file mode 100644 index 66eb2dad1..000000000 --- a/packages/vue/test/fixtures/blog-post.md +++ /dev/null @@ -1,41 +0,0 @@ -import { Baz } from './Fixture' -import { Buz } from './Fixture' - -export const foo = { - hi: `Fudge ${Baz.displayName || 'Baz'}`, - authors: [ - 'fred', - 'sally' - ] -} - -# Hello, world! - - -I'm an awesome paragraph. - - - - - hi - {hello} - {/* another commment */} - - -``` -test codeblock -``` - -```js -module.exports = 'test' -``` - -```sh -npm i -g foo -``` - -| Test | Table | -| :--- | :---- | -| Col1 | Col2 | - -export default ({children}) =>
{children}
diff --git a/packages/vue/test/mdx-components.js b/packages/vue/test/mdx-components.js new file mode 100644 index 000000000..a87b9f62a --- /dev/null +++ b/packages/vue/test/mdx-components.js @@ -0,0 +1,57 @@ +const components = { + inlineCode: props => ({ + name: 'InlineCode', + render(h) { + return h('code', { + attrs: { + id: 'mdx-code', + style: { + color: 'currentColor', + fontFamily: 'monospace, mono', + fontSize: '0.85em' + } + }, + domProps: { + ...props + } + }, this.$slots.default) + } + }), + h1: props => ({ + name: 'Heading', + render(h) { + return h('h1', { + attrs: { + id: 'mdx-h1', + style: { + fontWeight: 'bold', + } + }, + domProps: { + ...props + } + }, this.$slots.default) + } + }), + blockquote: props => ({ + name: 'BlockQuote', + render(h) { + return h('blockquote', { + attrs: { + id: 'mdx-blockquote', + style: { + borderLeft: '5px solid tomato', + borderRadius: '0.5rem', + marginTop: '3rem', + marginBottom: '3rem', + } + }, + domProps: { + ...props + } + }, this.$slots.default) + } + }), +} + +export default components diff --git a/packages/vue/test/mdx-provider.test.js b/packages/vue/test/mdx-provider.test.js index 9bd217831..5ce23f06a 100644 --- a/packages/vue/test/mdx-provider.test.js +++ b/packages/vue/test/mdx-provider.test.js @@ -1,33 +1,36 @@ -import {mount} from '@vue/test-utils' -import {MDXProvider, MDXTag} from '../src' +import { shallowMount } from '@vue/test-utils' +import MDXProvider from '../src/mdx-provider' +import components from './mdx-components' -const H1Tag = { - render() { - const data = {style: {color: 'green'}} - return

{this.$slots.default}

+/** + * Skipped because @vue/test-utils requires jsdom-global installed. + * @todo Add unit test setup for vue components with @vue/test-utils or @testing-library/vue + */ +xdescribe('===== MDXProvider Component =====', () => { + let mdxProvider + const ChildComponent = { + inject: ['$mdxComponents'], + render: h => h('div', {}) } -} -const Layout = { - render() { - return
{this.$slots.default}
- } -} + it('should be a Vue component', () => { + mdxProvider = shallowMount(MDXProvider, { + slots: { + default: [ChildComponent] + } + }) + expect(mdxProvider.isVueInstance()).toBeTruthy() + }) -it('Should allow components to be passed via context', () => { - const components = {h1: H1Tag} - const TestComponent = { - render() { - return ( - - - Hello World! - - - ) - } - } - const wrapper = mount(TestComponent) - expect(wrapper.html()).toMatch(/id="layout"/) - expect(wrapper.html()).toMatch(/style="color: green;"/) + it('should provide mdx components object to child components', () => { + mdxProvider = shallowMount(MDXProvider, { + slots: { + default: [ChildComponent] + }, + propsData: { + components, + } + }) + expect(mdxProvider.find(ChildComponent).vm.$mdxComponents()).toBe(components) + }) }) diff --git a/packages/vue/test/mdx-tag.test.js b/packages/vue/test/mdx-tag.test.js deleted file mode 100644 index 44e5d295a..000000000 --- a/packages/vue/test/mdx-tag.test.js +++ /dev/null @@ -1,52 +0,0 @@ -import {mount} from '@vue/test-utils' -import {MDXTag} from '../src' - -const H1Tag = { - render() { - return

{this.$slots.default}

- } -} - -const Layout = { - props: ['id'], - render() { - return
{this.$slots.default}
- } -} - -it('Should render the desired component', () => { - const wrapper = mount(MDXTag, { - propsData: { - name: 'h1', - components: {h1: H1Tag} - }, - slots: { - default: 'Hello World!' - } - }) - expect(wrapper.isVueInstance()).toBeTruthy() - expect(wrapper.html()).toMatch(/style="color: green;"/) -}) - -it('Should render the Layout component', () => { - const components = {h1: H1Tag} - const MDXTagWithLayout = { - render() { - return ( - - - Hello World! - - - ) - } - } - const wrapper = mount(MDXTagWithLayout) - expect(wrapper.html()).toMatch(/id="layout"/) - expect(wrapper.html()).toMatch(/style="color: green;"/) -}) diff --git a/yarn.lock b/yarn.lock index 766e39686..115fde972 100644 --- a/yarn.lock +++ b/yarn.lock @@ -45,7 +45,7 @@ dependencies: cross-fetch "3.0.4" -"@babel/cli@^7.5.5": +"@babel/cli@^7.5.5", "@babel/cli@^7.8.4": version "7.8.4" resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.8.4.tgz#505fb053721a98777b2b175323ea4f090b7d3c1c" integrity sha512-XXLgAm6LBbaNxaGhMAznXXaxtCWfuv6PIDJ9Alsy9JYTOh+j2jJz+L/162kkfU1j/pTSxK1xGmlwI4pdIMkoag== @@ -6947,9 +6947,9 @@ caniuse-api@^3.0.0: lodash.uniq "^4.5.0" caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000844, caniuse-lite@^1.0.30000884, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30000989, caniuse-lite@^1.0.30001017, caniuse-lite@^1.0.30001035, caniuse-lite@^1.0.30001039, caniuse-lite@^1.0.30001043: - version "1.0.30001045" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001045.tgz#a770df9de36ad6ca0c34f90eaa797a2dbbb1b619" - integrity sha512-Y8o2Iz1KPcD6FjySbk1sPpvJqchgxk/iow0DABpGyzA1UeQAuxh63Xh0Enj5/BrsYbXtCN32JmR4ZxQTCQ6E6A== + version "1.0.30001046" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001046.tgz#7a06d3e8fd8aa7f4d21c9a2e313f35f2d06b013e" + integrity sha512-CsGjBRYWG6FvgbyGy+hBbaezpwiqIOLkxQPY4A4Ea49g1eNsnQuESB+n4QM0BKii1j80MyJ26Ir5ywTQkbRE4g== capture-exit@^1.2.0: version "1.2.0" @@ -8179,6 +8179,13 @@ create-react-context@^0.2.2, create-react-context@^0.2.3: fbjs "^0.8.0" gud "^1.0.0" +cross-env@^7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.2.tgz#bd5ed31339a93a3418ac4f3ca9ca3403082ae5f9" + integrity sha512-KZP/bMEOJEDCkDQAyRhu3RL2ZO/SUVrxQVI0G3YEQ+OLbRA3c6zgixe8Mq8a/z7+HKlNEjo8oiLUs8iRijY2Rw== + dependencies: + cross-spawn "^7.0.1" + cross-fetch@2.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-2.2.2.tgz#a47ff4f7fc712daba8f6a695a11c948440d45723" @@ -26454,6 +26461,14 @@ warning@^4.0.1, warning@^4.0.3: dependencies: loose-envify "^1.0.0" +watch@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/watch/-/watch-1.0.2.tgz#340a717bde765726fa0aa07d721e0147a551df0c" + integrity sha1-NApxe952Vyb6CqB9ch4BR6VR3ww= + dependencies: + exec-sh "^0.2.0" + minimist "^1.2.0" + watch@~0.18.0: version "0.18.0" resolved "https://registry.yarnpkg.com/watch/-/watch-0.18.0.tgz#28095476c6df7c90c963138990c0a5423eb4b986"