├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitattributes ├── .gitignore ├── .nycrc ├── .travis.yml ├── package.json ├── readme.md ├── src ├── build.js ├── index.js └── parser.js ├── test ├── .eslintrc ├── build.spec.js ├── examples │ ├── .eslintrc │ ├── button.js │ ├── hello-world.js │ └── hello-world.md ├── index.spec.js └── parser.spec.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # http://editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | charset = utf-8 9 | end_of_line = lf 10 | indent_size = 2 11 | indent_style = space 12 | insert_final_newline = true 13 | trim_trailing_whitespace = true 14 | 15 | [*{.html,.php}] 16 | indent_style = tab 17 | 18 | [*.md] 19 | trim_trailing_whitespace = false 20 | 21 | [*.yml] 22 | indent_size = 2 23 | indent_style = space 24 | 25 | [{package.json}] 26 | # The indent size used in the `package.json` file cannot be changed 27 | # https://github.com/npm/npm/pull/3180#issuecomment-16336516 28 | indent_size = 2 29 | indent_style = space 30 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage/** 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ "airbnb" ], 3 | "env": { 4 | "node": true 5 | }, 6 | "parserOptions": { 7 | "sourceType": "script" 8 | }, 9 | "rules": { 10 | "strict": [ "error", "global" ], 11 | "consistent-return": "off", 12 | "jsx-a11y/href-no-hash": "off", 13 | "react/jsx-filename-extension": "off", 14 | "comma-dangle": "off" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Folder view configuration files 2 | .DS_Store 3 | Desktop.ini 4 | 5 | # Thumbnail cache files 6 | ._* 7 | Thumbs.db 8 | 9 | # Files that might appear on external disks 10 | .Spotlight-V100 11 | .Trashes 12 | 13 | # npm 14 | node_modules 15 | npm-debug.log 16 | yarn-error.log 17 | 18 | # test files 19 | coverage 20 | 21 | # build and temp folders 22 | dist 23 | .tmp 24 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "reporter": [ 3 | "lcov", 4 | "text" 5 | ], 6 | "all": true, 7 | "exclude": [ 8 | "test/", 9 | "coverage/" 10 | ], 11 | "check-coverage": false, 12 | "lines": 100, 13 | "functions": 100, 14 | "branches": 100, 15 | "statements": 100 16 | } 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | script: 'yarn preversion' 5 | sudo: false 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-markdown-loader", 3 | "version": "1.3.1", 4 | "description": "Webpack loader to render React Components from markdown", 5 | "keywords": [ 6 | "react", 7 | "styleguide", 8 | "webpack", 9 | "loader", 10 | "markdown" 11 | ], 12 | "homepage": "https://github.com/javiercf/react-markdown-loader", 13 | "bugs": "https://github.com/javiercf/react-markdown-loader/issues", 14 | "license": "MIT", 15 | "author": "Javier Cubides ", 16 | "contributors": [ 17 | "Fernando Pasik (https://fernandopasik.com/)" 18 | ], 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/javiercf/react-markdown-loader.git" 22 | }, 23 | "main": "src/index.js", 24 | "scripts": { 25 | "start": "watch 'yarn preversion' src/ test/", 26 | "lint": "eslint .", 27 | "test": "jest", 28 | "preversion": "yarn lint && yarn test" 29 | }, 30 | "dependencies": { 31 | "camelize": "^1.0.0", 32 | "except": "^0.1.3", 33 | "front-matter": "^4.0.2", 34 | "node-prismjs": "^0.1.2", 35 | "remarkable": "^2.0.0" 36 | }, 37 | "devDependencies": { 38 | "eslint": "^6.8.0", 39 | "eslint-config-airbnb": "^18.0.1", 40 | "eslint-plugin-import": "^2.19.1", 41 | "eslint-plugin-jsx-a11y": "^6.2.3", 42 | "eslint-plugin-react": "^7.17.0", 43 | "jest": "^24.9.0", 44 | "prop-types": "^15.7.2", 45 | "react": "^16.12.0", 46 | "watch": "^1.0.2" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | React Markdown 2 | ================== 3 | 4 | [![Build Status][badge-ci]][url-ci] 5 | [![Known Vulnerabilities][badge-sec]][url-sec] 6 | 7 | [![npm version][badge-version]][url-version] 8 | [![npm downloads][badge-downloads]][url-downloads] 9 | [![Dependency Status][badge-deps]][url-deps] 10 | [![peerDependency Status][badge-deps-peer]][url-deps-peer] 11 | [![devDependency Status][badge-deps-dev]][url-deps-dev] 12 | 13 | [badge-ci]: https://travis-ci.org/javiercf/react-markdown-loader.svg?branch=master 14 | [badge-sec]: https://snyk.io/test/github/javiercf/react-markdown-loader/badge.svg?targetFile=package.json 15 | [badge-version]: https://img.shields.io/npm/v/react-markdown-loader.svg 16 | [badge-downloads]: https://img.shields.io/npm/dm/react-markdown-loader.svg 17 | [badge-deps]: https://david-dm.org/javiercf/react-markdown-loader/status.svg 18 | [badge-deps-dev]: https://david-dm.org/javiercf/react-markdown-loader/dev-status.svg 19 | 20 | [url-ci]: https://travis-ci.org/javiercf/react-markdown-loader "Build Status" 21 | [url-sec]: https://snyk.io/test/github/javiercf/react-markdown-loader?targetFile=package.json "Known Vulnerabilities" 22 | [url-version]: https://www.npmjs.com/package/react-markdown-loader "npm version" 23 | [url-downloads]: https://www.npmjs.com/package/react-markdown-loader "npm downloads" 24 | [url-deps]: https://david-dm.org/javiercf/react-markdown-loader "Dependency Status" 25 | [url-deps-dev]: https://david-dm.org/javiercf/react-markdown-loader?type=dev "Dev Dependency Status" 26 | 27 | Webpack loader that parses markdown files and converts them to a React Stateless Component. 28 | It will also parse FrontMatter to import dependencies and render components 29 | along with it’s source code. 30 | 31 | We developed this loader to make the process of creating styleguides for 32 | React components easier. 33 | 34 | ## Usage 35 | 36 | In the FrontMatter you should import the components you want to render 37 | with the component name as a key and it's path as the value 38 | 39 | ```markdown 40 | --- 41 | imports: 42 | HelloWorld: './hello-world.js', 43 | '{ Component1, Component2 }': './components.js' 44 | --- 45 | ``` 46 | 47 | *webpack.config.js* 48 | ```js 49 | module: { 50 | loaders: [ 51 | { 52 | test: /\.md$/, 53 | loader: 'babel!react-markdown' 54 | } 55 | ] 56 | } 57 | ``` 58 | 59 | *hello-world.js* 60 | ```js 61 | import React, { PropTypes } from 'react'; 62 | 63 | /** 64 | * HelloWorld 65 | * @param {Object} props React props 66 | * @returns {JSX} template 67 | */ 68 | export default function HelloWorld(props) { 69 | return ( 70 |
71 | Hello { props.who } 72 |
73 | ); 74 | } 75 | 76 | HelloWorld.propTypes = { 77 | who: PropTypes.string 78 | }; 79 | 80 | HelloWorld.defaultProps = { 81 | who: 'World' 82 | }; 83 | 84 | ``` 85 | In the markdown File add the *render* tag to code fenceblocks you want the 86 | loader to compile as Components this will output the usual highlighted code 87 | and the rendered component. 88 | 89 | *hello-world.md* 90 | 91 |
 92 | 
 93 | ---
 94 | imports:
 95 |   HelloWorld: './hello-world.js'
 96 | ---
 97 | # Hello World
 98 | 
 99 | This is an example component
100 | 
101 | ```render html
102 | <HelloWorld />
103 | ```
104 | 
105 | You can send who to say Hello
106 | 
107 | ```render html
108 | <HelloWorld who="World!!!" />
109 | ```
110 | 
111 | 
112 | 113 | ## Contributing 114 | 115 | Pull requests and stars are always welcome. For bugs and feature requests, [please create an issue](../../issues/new). 116 | 117 | ## Team 118 | 119 | [![Javier Cubides](https://avatars.githubusercontent.com/u/3386811?s=130)](https://github.com/javiercf) | [![Fernando Pasik](https://avatars.githubusercontent.com/u/1301335?s=130)](https://fernandopasik.com) 120 | ---|--- 121 | [Javier Cubides](https://github.com/javiercf) | [Fernando Pasik](https://fernandopasik.com) 122 | 123 | ## License 124 | 125 | MIT (c) 2017 126 | -------------------------------------------------------------------------------- /src/build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const camelize = require('camelize'); 4 | const except = require('except'); 5 | 6 | /** 7 | * @typedef HTMLObject 8 | * @type {Object} 9 | * @property {String} html - HTML parsed from markdown 10 | * @property {Object} imports - Map of dependencies 11 | */ 12 | 13 | /** 14 | * Builds the React Component from markdown content 15 | * with its dependencies 16 | * @param {HTMLObject} markdown - HTML and imports 17 | * @returns {String} - React Component 18 | */ 19 | module.exports = function build(markdown) { 20 | let doImports = 'import React from \'react\';\n'; 21 | const imports = markdown.attributes.imports || {}; 22 | const jsx = markdown.html.replace(/class=/g, 'className='); 23 | 24 | const frontMatterAttributes = except(markdown.attributes, 'imports'); 25 | 26 | // eslint-disable-next-line no-restricted-syntax 27 | for (const variable in imports) { 28 | // eslint-disable-next-line no-prototype-builtins 29 | if (imports.hasOwnProperty(variable)) { 30 | doImports += `import ${variable} from '${imports[variable]}';\n`; 31 | } 32 | } 33 | 34 | return ` 35 | ${doImports} 36 | 37 | export const attributes = ${JSON.stringify(camelize(frontMatterAttributes))}; 38 | export default function() { 39 | return ( 40 |
41 | ${jsx} 42 |
43 | ); 44 | };`; 45 | }; 46 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const build = require('./build.js'); 4 | const parser = require('./parser.js'); 5 | 6 | /** 7 | * Main function 8 | * @param {String} content Markdown file content 9 | */ 10 | module.exports = function loader(content) { 11 | const callback = this.async(); 12 | 13 | parser 14 | .parse(content) 15 | .then(build) 16 | .then((component) => callback(null, component)) 17 | .catch(callback); 18 | }; 19 | -------------------------------------------------------------------------------- /src/parser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const frontMatter = require('front-matter'); 4 | const Prism = require('node-prismjs'); 5 | const { Remarkable, utils } = require('remarkable'); 6 | 7 | const { escapeHtml } = utils; 8 | 9 | const md = new Remarkable(); 10 | 11 | /** 12 | * Wraps the code and jsx in an html component 13 | * for styling it later 14 | * @param {string} exampleRun Code to be run in the styleguide 15 | * @param {string} exampleSrc Source that will be shown as example 16 | * @param {string} langClass CSS class for the code block 17 | * @returns {string} Code block with souce and run code 18 | */ 19 | function codeBlockTemplate(exampleRun, exampleSrc, langClass) { 20 | return ` 21 |
22 |
${exampleRun}
23 |
24 | 25 | ${exampleSrc} 26 | 27 |
28 |
`; 29 | } 30 | 31 | /** 32 | * Parse a code block to have a source and a run code 33 | * @param {String} code - Raw html code 34 | * @param {String} lang - Language indicated in the code block 35 | * @param {String} langPrefix - Language prefix 36 | * @param {Function} highlight - Code highlight function 37 | * @returns {String} Code block with souce and run code 38 | */ 39 | function parseCodeBlock(code, lang, langPrefix, highlight) { 40 | let codeBlock = escapeHtml(code); 41 | 42 | if (highlight) { 43 | codeBlock = highlight(code, lang); 44 | } 45 | 46 | const langClass = !lang ? '' : `${langPrefix}${escape(lang, true)}`; 47 | const jsx = code; 48 | 49 | codeBlock = codeBlock 50 | .replace(/{/g, '{"{"{') 51 | .replace(/}/g, '{"}"}') 52 | .replace(/{"{"{/g, '{"{"}') 53 | .replace(/(\n)/g, '{"\\n"}') 54 | .replace(/class=/g, 'className='); 55 | 56 | return codeBlockTemplate(jsx, codeBlock, langClass); 57 | } 58 | 59 | /** 60 | * @typedef MarkdownObject 61 | * @type {Object} 62 | * @property {Object} attributes - Map of properties from the front matter 63 | * @property {String} body - Markdown 64 | */ 65 | 66 | /** 67 | * @typedef HTMLObject 68 | * @type {Object} 69 | * @property {String} html - HTML parsed from markdown 70 | * @property {Object} imports - Map of dependencies 71 | */ 72 | 73 | /** 74 | * Parse Markdown to HTML with code blocks 75 | * @param {MarkdownObject} markdown - Markdown attributes and body 76 | * @returns {HTMLObject} HTML and imports 77 | */ 78 | function parseMarkdown(markdown) { 79 | return new Promise((resolve, reject) => { 80 | let html; 81 | 82 | const options = { 83 | highlight(code, lang) { 84 | const language = Prism.languages[lang] || Prism.languages.autoit; 85 | return Prism.highlight(code, language); 86 | }, 87 | xhtmlOut: true 88 | }; 89 | 90 | md.set(options); 91 | 92 | md.renderer.rules.fence_custom.render = (tokens, idx, opts) => { 93 | // gets tags applied to fence blocks ```react html 94 | const codeTags = tokens[idx].params.split(/\s+/g); 95 | return parseCodeBlock( 96 | tokens[idx].content, 97 | codeTags[codeTags.length - 1], 98 | opts.langPrefix, 99 | opts.highlight 100 | ); 101 | }; 102 | 103 | try { 104 | html = md.render(markdown.body); 105 | return resolve({ html, attributes: markdown.attributes }); 106 | } catch (err) { 107 | return reject(err); 108 | } 109 | }); 110 | } 111 | 112 | /** 113 | * Extract FrontMatter from markdown 114 | * and return a separate object with keys 115 | * and a markdown body 116 | * @param {String} markdown - Markdown string to be parsed 117 | * @returns {MarkdownObject} Markdown attributes and body 118 | */ 119 | function parseFrontMatter(markdown) { 120 | return frontMatter(markdown); 121 | } 122 | 123 | /** 124 | * Parse markdown, extract the front matter 125 | * and return the body and imports 126 | * @param {String} markdown - Markdown string to be parsed 127 | * @returns {HTMLObject} HTML and imports 128 | */ 129 | function parse(markdown) { 130 | return parseMarkdown(parseFrontMatter(markdown)); 131 | } 132 | 133 | module.exports = { 134 | codeBlockTemplate, 135 | parse, 136 | parseCodeBlock, 137 | parseFrontMatter, 138 | parseMarkdown 139 | }; 140 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/build.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const build = require('../src/build.js'); 6 | const parser = require('../src/parser.js'); 7 | 8 | describe('Build Component', () => { 9 | let component = ''; 10 | const mdFile = path.join(__dirname, './examples/hello-world.md'); 11 | 12 | beforeAll((done) => { 13 | fs.readFile(mdFile, 'utf8', (err, data) => { 14 | if (err) { 15 | return done(err); 16 | } 17 | 18 | parser 19 | .parse(data) 20 | .then((html) => { 21 | component = build(html); 22 | done(); 23 | }) 24 | .catch(done); 25 | }); 26 | }); 27 | 28 | it('add React import', () => { 29 | expect(component).toMatch(/import React from 'react';\n/); 30 | }); 31 | 32 | it('add component imports', () => { 33 | expect(component).toMatch(/import Button from '.\/button.js';\n/); 34 | expect(component).toMatch(/import HelloWorld from '.\/hello-world.js';\n/); 35 | }); 36 | 37 | it('exports the front-matter attributes', () => { 38 | expect(component).toMatch(/export const attributes = {"testFrontMatter":"hello world"}/); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /test/examples/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "sourceType": "module" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/examples/button.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | /** 5 | * Button 6 | * @param {Object} props React props 7 | * @returns {JSX} template 8 | */ 9 | export default function Button({ label }) { 10 | return ; 11 | } 12 | 13 | Button.propTypes = { label: PropTypes.string }; 14 | 15 | Button.defaultProps = { label: 'World' }; 16 | -------------------------------------------------------------------------------- /test/examples/hello-world.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | /** 5 | * Says hello to who 6 | * By default who is World 7 | * @param {Object} props React props 8 | * @returns {JSX} template 9 | */ 10 | export default function HelloWorld({ who }) { 11 | return ( 12 |
13 | Hello 14 | {who} 15 |
16 | ); 17 | } 18 | 19 | HelloWorld.propTypes = { who: PropTypes.string }; 20 | 21 | HelloWorld.defaultProps = { who: 'World' }; 22 | -------------------------------------------------------------------------------- /test/examples/hello-world.md: -------------------------------------------------------------------------------- 1 | --- 2 | test-front-matter: 'hello world' 3 | imports: 4 | Button: './button.js' 5 | HelloWorld: './hello-world.js' 6 | --- 7 | # Hello World 8 | 9 | This is an example component 10 | 11 | ```render html 12 | 13 |