├── .babelrc ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── README.md ├── build └── index.html ├── config ├── jsdom_setup.js ├── lib.webpack.config.js ├── loaders.js └── tests.webpack.config.babel.js ├── example ├── Main.js └── components │ ├── App.js │ ├── App.sass │ ├── Layout.js │ ├── Layout.sass │ ├── Page.js │ ├── Page.sass │ └── SuperComponent │ ├── SuperComponent.js │ ├── SuperComponent.sass │ └── index.js ├── package.json └── src ├── MarkdownIt.js ├── MarkdownPart.js ├── __tests__ ├── MarkdownIt.spec.js └── MarkdownPart.spec.js ├── index.js └── utils ├── markdown.js └── parsemd.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react", "stage-0"], 3 | "env": { 4 | "LIB": { 5 | "plugins": [ 6 | ["webpack-loaders", {"config": "./config/lib.webpack.config.js"}] 7 | ] 8 | }, 9 | "TESTS": { 10 | "plugins": [ 11 | ["webpack-loaders", {"config": "./config/tests.webpack.config.babel.js", "verbose": false}] 12 | ] 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "extends": "airbnb", 4 | "env": { 5 | "browser": true, 6 | "mocha": true, 7 | "node": true 8 | }, 9 | "parser": "babel-eslint", 10 | "rules": { 11 | "no-nested-ternary": 0, 12 | "react/prefer-stateless-function": 0, 13 | "react/prop-types": 0 14 | }, 15 | "plugins": [ 16 | "react" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /lib 3 | /dist 4 | /build/* 5 | !/build/index.html 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /.*/ 2 | /.* 3 | .DS_Store 4 | *.log 5 | src 6 | test 7 | example 8 | config 9 | /appveyor.yml 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - stable 4 | cache: 5 | directories: 6 | - node_modules 7 | script: 8 | - npm run lint 9 | - npm run test 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/istarkov/react-components-markdown.svg?branch=master)](https://travis-ci.org/istarkov/react-components-markdown) 2 | 3 | # Notice 4 | 5 | This library is highly outdated, IMO [markdown-in-js](https://github.com/threepointone/markdown-in-js) is much more better, please use it. 6 | 7 | # React Components Markdown 8 | 9 | React component to render markdown with React components inside 10 | 11 | ## Example 12 | 13 | This library allow to create interactive documentation from md files. 14 | 15 | i.e. this [html-hint README.md](https://github.com/istarkov/html-hint/blob/master/README.md) 16 | 17 | become this [html-hint README with examples](http://istarkov.github.io/html-hint/) 18 | 19 | in just few lines of [React code](https://github.com/istarkov/html-hint/blob/master/example/components/Page.js#L15-L32) 20 | 21 | (_this library github.io example is also build with this library_) 22 | 23 | ## How to 24 | 25 | Create `readme.md` for your component 26 | 27 | ```md 28 | # Component Help 29 | 30 | Lorem ipsum dolor sit amet, consectetur adipiscing elit 31 | [My super example](http://istarkov.github.io/html-hint/#exampleSuperMain) 32 | Duis aute irure dolor in reprehenderit 33 | [My super example with props](http://istarkov.github.io/html-hint/#exampleSuperProps) 34 | ``` 35 | 36 | Use it in React 37 | 38 | ```javascript 39 | import readme from './readme.md'; 40 | import Markdown from 'react-components-markdown'; 41 | import MySuperReactExample from './my-super-react-example'; 42 | 43 | export default () => ( 44 | } 46 | exampleSuperProps={} 47 | > 48 | {content} 49 | 50 | ); 51 | ``` 52 | 53 | Component will replace all refs to github.io into React elements, 54 | mapping hashes to `Markdown` element props. 55 | 56 | Also you can define a custom match regexp 57 | 58 | ```javascript 59 | 72 | HELLO AFRICA 73 | ) 74 | ``` 75 | 76 | [My super example, click to view](http://istarkov.github.io/react-components-markdown/#exampleMain) 77 | 78 | Or just 79 | 80 | ```javascript 81 | ( ) 82 | ``` 83 | 84 | [My super example 2, click to view](http://istarkov.github.io/react-components-markdown/#exampleSecondary) 85 | 86 | 87 | ## Styling 88 | 89 | If you are using or want to use [css-modules](https://github.com/css-modules/css-modules) 90 | 91 | Setup your webpack loader for css as [here](https://github.com/istarkov/react-components-markdown/blob/master/config/loaders.js#L38-L44) 92 | 93 | Install `github-markdown-css` and `highlight.js` for default styles. 94 | 95 | Import styles from installed libraries 96 | 97 | ```javascript 98 | import githubCss from 'github-markdown-css/github-markdown.css'; 99 | // There are many color schemas for highlight choose any 100 | import hlJsCss from 'highlight.js/styles/github.css'; 101 | 102 | // combine styles into one 103 | const styles = {...githubCss, ...hlJsCss}; 104 | 105 | .... 106 | // use styles as Markdown property 107 | ( 108 | 2 | 3 | 4 | React components markdown 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /config/jsdom_setup.js: -------------------------------------------------------------------------------- 1 | const jsdom = require('jsdom').jsdom; 2 | 3 | const exposedProperties = ['window', 'navigator', 'document']; 4 | 5 | global.document = jsdom(''); 6 | global.window = document.defaultView; 7 | 8 | Object.keys(document.defaultView).forEach((property) => { 9 | if (typeof global[property] === 'undefined') { 10 | exposedProperties.push(property); 11 | global[property] = document.defaultView[property]; 12 | } 13 | }); 14 | 15 | global.navigator = { 16 | userAgent: 'node.js', 17 | }; 18 | -------------------------------------------------------------------------------- /config/lib.webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); // eslint-disable-line no-var 2 | var autoprefixer = require('autoprefixer'); // eslint-disable-line no-var 3 | var ExtractTextPlugin = require('extract-text-webpack-plugin'); // eslint-disable-line no-var 4 | 5 | module.exports = { 6 | output: { 7 | libraryTarget: 'commonjs2', 8 | path: path.join(__dirname, '../lib'), 9 | }, 10 | postcss: [ 11 | autoprefixer({ browsers: ['last 2 versions'] }), 12 | ], 13 | plugins: [ 14 | new ExtractTextPlugin(`${path.parse(process.argv[2]).name}.css`), 15 | ], 16 | module: { 17 | loaders: [ 18 | { 19 | test: /\.css$/, 20 | loader: ExtractTextPlugin.extract( 21 | 'style-loader', 22 | [ 23 | 'css-loader?modules&importLoaders=1&localIdentName=[name]__[local]__[hash:5]', 24 | 'postcss-loader', 25 | ] 26 | ), 27 | }, 28 | ], 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /config/loaders.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); // eslint-disable-line no-var 2 | var autoprefixer = require('autoprefixer'); // eslint-disable-line no-var 3 | var webpack = require('webpack'); // eslint-disable-line no-var 4 | 5 | var envObj = Object.keys(process.env) // eslint-disable-line no-var 6 | .reduce((r, k) => Object.assign({}, r, { 7 | [k]: JSON.stringify(process.env[k]), 8 | }), {}); 9 | 10 | var alias = { // eslint-disable-line no-var 11 | 'react-components-markdown': path.join(__dirname, '../src'), 12 | }; 13 | 14 | module.exports = { 15 | resolve: { 16 | alias: alias, // eslint-disable-line 17 | }, 18 | postcss: [ 19 | autoprefixer({ browsers: ['last 2 versions'] }), 20 | ], 21 | plugins: [ 22 | new webpack.DefinePlugin({ 23 | 'process.env': envObj, 24 | }), 25 | ], 26 | module: { 27 | loaders: [ 28 | { 29 | test: /\.sass$/, 30 | loaders: [ 31 | 'style-loader', 32 | 'css-loader?modules&importLoaders=2&localIdentName=[name]__[local]__[hash:5]', 33 | 'postcss-loader', 34 | 'sass-loader?precision=10&indentedSyntax=sass', 35 | ], 36 | }, 37 | { 38 | test: /\.css$/, 39 | loaders: [ 40 | 'style-loader', 41 | 'css-loader?modules&importLoaders=1&localIdentName=[name]__[local]__[hash:5]', 42 | 'postcss-loader', 43 | ], 44 | }, 45 | { 46 | test: /\.svg$/, 47 | loaders: ['url-loader?limit=7000'], 48 | }, 49 | { 50 | test: /\.md$/, 51 | loaders: ['raw-loader'], 52 | }, 53 | ], 54 | }, 55 | }; 56 | -------------------------------------------------------------------------------- /config/tests.webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import autoprefixer from 'autoprefixer'; 3 | 4 | const alias = { 5 | 'react-components-markdown': path.join(__dirname, '../src'), 6 | }; 7 | 8 | export default { 9 | output: { 10 | libraryTarget: 'commonjs2', 11 | }, 12 | resolve: { 13 | alias, 14 | }, 15 | postcss: [ 16 | autoprefixer({ browsers: ['last 2 versions'] }), 17 | ], 18 | module: { 19 | loaders: [ 20 | { 21 | test: /\.css$/, 22 | loaders: [ 23 | 'style-loader', 24 | 'css-loader?modules&importLoaders=1&localIdentName=[name]__[local]__[hash:5]', 25 | 'postcss-loader', 26 | ], 27 | }, 28 | ], 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /example/Main.js: -------------------------------------------------------------------------------- 1 | // file: main.jsx 2 | // import 'babel-polyfill'; 3 | import React from 'react'; 4 | import { render } from 'react-dom'; 5 | import App from './components/App'; 6 | 7 | const mountNode = document.getElementById('app'); 8 | 9 | render(, mountNode); 10 | -------------------------------------------------------------------------------- /example/components/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import 'normalize.css/normalize.css'; 4 | import './App.sass'; 5 | 6 | import Layout from './Layout.js'; 7 | import Page from './Page.js'; 8 | 9 | // This class is needed for hmr 10 | export default class App extends Component { 11 | render() { 12 | return ( 13 | 14 | ); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /example/components/App.sass: -------------------------------------------------------------------------------- 1 | :global(#app) 2 | height: 100% 3 | 4 | html, body 5 | height: 100% 6 | font-size: 14px 7 | 8 | html 9 | box-sizing: border-box 10 | 11 | *, *:before, *:after 12 | box-sizing: inherit 13 | -------------------------------------------------------------------------------- /example/components/Layout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import compose from 'recompose/compose'; 3 | import defaultProps from 'recompose/defaultProps'; 4 | import layoutStyles from './Layout.sass'; 5 | 6 | // for hmr to work I need the first class to extend Component 7 | export const layoutComp = ({ 8 | styles: { layout, header, main, footer, logo }, 9 | children, 10 | }) => ( 11 |
12 |
13 |

14 | React Components Markdown 15 |

16 | 21 |
22 |
23 | {children} 24 |
25 | 38 |
39 | ); 40 | 41 | export const layoutHOC = compose( 42 | defaultProps({ 43 | styles: layoutStyles, 44 | }) 45 | ); 46 | 47 | export default layoutHOC(layoutComp); 48 | -------------------------------------------------------------------------------- /example/components/Layout.sass: -------------------------------------------------------------------------------- 1 | .layout 2 | display: flex 3 | min-height: 100vh 4 | flex-direction: column 5 | margin: 0 1px 0 1px 6 | 7 | .header 8 | height: 3em 9 | // background-color: #004336 10 | border-bottom: 1px solid #ccc 11 | color: #333 12 | display: flex 13 | align-items: center 14 | justify-content: space-between 15 | padding: 0 10px 0 10px 16 | a 17 | color: #333 18 | 19 | .logo 20 | width: 1.3em 21 | height: 1.3em 22 | margin: 0.3em 23 | background-size: contain 24 | background-repeat: no-repeat 25 | background-image: url('https://avatars2.githubusercontent.com/u/5077042?v=3&s=40') 26 | 27 | .main 28 | flex: 1 29 | // padding: 20px 30 | display: flex 31 | justify-content: center 32 | 33 | .footer 34 | height: 3em 35 | border-top: 1px solid #ccc 36 | // background-color: #004336 37 | color: #333 38 | display: flex 39 | align-items: center 40 | justify-content: center 41 | a 42 | color: #333 43 | -------------------------------------------------------------------------------- /example/components/Page.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import compose from 'recompose/compose'; 3 | import defaultProps from 'recompose/defaultProps'; 4 | import Markdown from 'react-components-markdown'; 5 | import pageStyles from './Page.sass'; 6 | import mdContent from '../../README.md'; 7 | import SuperComponent from './SuperComponent'; 8 | 9 | export const page = ({ styles, content }) => ( 10 |
11 | 14 | HELLO AFRICA 15 | 16 | } 17 | exampleSecondary={ 18 | 19 | } 20 | > 21 | {content} 22 | 23 |
24 | ); 25 | 26 | export const pageHOC = compose( 27 | defaultProps({ 28 | styles: pageStyles, 29 | content: mdContent, 30 | }), 31 | ); 32 | 33 | export default pageHOC(page); 34 | -------------------------------------------------------------------------------- /example/components/Page.sass: -------------------------------------------------------------------------------- 1 | .main 2 | flex-basis: 1024px 3 | padding: 30px 4 | border-left: 1px solid #ccc 5 | border-right: 1px solid #ccc 6 | 7 | .bigMargin 8 | margin-top: 80px 9 | margin-bottom: 80px 10 | -------------------------------------------------------------------------------- /example/components/SuperComponent/SuperComponent.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import superComponentStyles from './SuperComponent.sass'; 3 | 4 | export default ({ children = 'DEFAULT TEXT', styles = superComponentStyles }) => ( 5 |
6 |
7 | This is my super component 8 |
9 | {children} 10 |
11 | ); 12 | -------------------------------------------------------------------------------- /example/components/SuperComponent/SuperComponent.sass: -------------------------------------------------------------------------------- 1 | .main 2 | margin: 20px 3 | border: 5px solid red 4 | width: 300px 5 | height: 60px 6 | display: flex 7 | justify-content: center 8 | align-items: center 9 | flex-direction: column 10 | font-size: 24px 11 | 12 | .title 13 | font-size: 12px 14 | -------------------------------------------------------------------------------- /example/components/SuperComponent/index.js: -------------------------------------------------------------------------------- 1 | export default from './SuperComponent'; 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-components-markdown", 3 | "version": "0.2.0", 4 | "description": "", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "start": "kotatsu serve --port 4000 --config ./config/loaders.js --presets es2015,stage-0,react,react-hmre ./example/Main.js", 8 | "example-build": "NODE_ENV=production kotatsu build client --minify --config ./config/loaders.js --presets es2015,stage-0,react ./example/Main.js -o build", 9 | "concat-css": "rimraf ./lib/markdown.css && cat ./lib/*.css > ./lib/markdown.css", 10 | "build-lib": "BABEL_DISABLE_CACHE=1 NODE_ENV=LIB babel ./src --ignore __tests__ --out-dir lib", 11 | "build": "rimraf ./lib && npm run build-lib && npm run concat-css", 12 | "prepublish": "npm run build", 13 | "lint": "eslint ./src ./example", 14 | "src-test": "BABEL_DISABLE_CACHE=1 NODE_ENV=TESTS mocha --compilers js:babel-register --require ./config/jsdom_setup.js --recursive './src/**/*.spec.js'", 15 | "test": "npm run src-test" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/istarkov/react-components-markdown.git" 20 | }, 21 | "author": "istarkov https://github.com/istarkov", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/istarkov/react-components-markdown/issues" 25 | }, 26 | "homepage": "https://github.com/istarkov/react-components-markdown#readme", 27 | "peerDependencies": { 28 | "react": "^0.14.0 || ^15.0.0" 29 | }, 30 | "devDependencies": { 31 | "autoprefixer": "^6.3.1", 32 | "babel-cli": "^6.4.5", 33 | "babel-eslint": "^6.0.0-beta.6", 34 | "babel-plugin-webpack-loaders": "^0.4.0", 35 | "babel-polyfill": "^6.5.0", 36 | "babel-preset-es2015": "^6.3.13", 37 | "babel-preset-react": "^6.3.13", 38 | "babel-preset-react-hmre": "^1.0.1", 39 | "babel-preset-stage-0": "^6.3.13", 40 | "babel-register": "^6.6.0", 41 | "classnames": "^2.2.3", 42 | "core-js": "^2.1.0", 43 | "css-loader": "^0.23.1", 44 | "enzyme": "^2.0.0", 45 | "eslint": "^2.4.0", 46 | "eslint-config-airbnb": "^6.2.0", 47 | "eslint-plugin-react": "^5.0.0", 48 | "expect": "^1.14.0", 49 | "extract-text-webpack-plugin": "^1.0.1", 50 | "file-loader": "^0.8.5", 51 | "github-markdown-css": "^2.2.1", 52 | "jsdom": "^8.1.0", 53 | "kotatsu": "^0.13.0", 54 | "mocha": "^2.4.5", 55 | "node-sass": "^3.4.2", 56 | "normalize.css": "^4.0.0", 57 | "postcss-loader": "^0.8.0", 58 | "raw-loader": "^0.5.1", 59 | "react": "^15.0.0", 60 | "react-addons-test-utils": "^15.0.0", 61 | "react-dom": "^15.0.0", 62 | "react-motion": "^0.4.2", 63 | "rimraf": "^2.5.1", 64 | "sass-loader": "^4.0.0", 65 | "style-loader": "^0.13.0", 66 | "url-loader": "^0.5.7", 67 | "webpack": "^1.12.13" 68 | }, 69 | "dependencies": { 70 | "highlight.js": "^9.1.0", 71 | "lodash": "^4.3.0", 72 | "markdown-it": "^6.0.0", 73 | "recompose": "^0.15.0" 74 | }, 75 | "optionalDependencies": { 76 | "fsevents": "^1.0.8" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/MarkdownIt.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import compose from 'recompose/compose'; 3 | import defaultProps from 'recompose/defaultProps'; 4 | import mapPropsOnChange from 'recompose/mapPropsOnChange'; 5 | import mapProps from 'recompose/mapProps'; 6 | import pure from 'recompose/pure'; 7 | import parsemd from './utils/parsemd'; 8 | import MarkdownPart from './MarkdownPart'; 9 | 10 | export const markdownIt = ({ mdAndComps, wrap, styles }) => ( 11 |
12 | { 13 | mdAndComps 14 | .map(({ md, component, componentKey }) => ([ 15 | md && {md}, 16 | wrap({ component, componentKey }), 17 | ])) 18 | } 19 |
20 | ); 21 | 22 | export const markdownItHOC = compose( 23 | defaultProps({ 24 | anchorOffset: -90, 25 | componentRe: /\[[^\]]*\][^\(]*\(.*github\.io.*#([\w]+)\)/, 26 | }), 27 | mapPropsOnChange( 28 | ['anchorOffset'], 29 | ({ anchorOffset }) => ({ 30 | wrap: ({ component, componentKey }) => ( 31 | [ 32 | , 41 | component, 42 | ] 43 | ), 44 | }) 45 | ), 46 | mapPropsOnChange( 47 | ['children'], 48 | ({ children, componentRe }) => ({ 49 | mdAndCompKeys: parsemd(componentRe, children), 50 | }) 51 | ), 52 | pure, 53 | mapProps(({ ...props, mdAndCompKeys }) => ({ 54 | ...props, 55 | mdAndComps: mdAndCompKeys 56 | .map(({ md, componentKey }) => ({ 57 | md, 58 | componentKey, 59 | component: props[componentKey], 60 | })), 61 | })) 62 | ); 63 | 64 | export default markdownItHOC(markdownIt); 65 | -------------------------------------------------------------------------------- /src/MarkdownPart.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import compose from 'recompose/compose'; 3 | import defaultProps from 'recompose/defaultProps'; 4 | import mapPropsOnChange from 'recompose/mapPropsOnChange'; 5 | 6 | import githubCss from 'github-markdown-css/github-markdown.css'; 7 | import hlJsCss from 'highlight.js/styles/github.css'; 8 | 9 | import markdown from './utils/markdown'; 10 | 11 | export const markdownComp = ({ html, styles }) => ( 12 |
16 | ); 17 | 18 | const re = /(<.+?class=)"([^"]+)"/gi; 19 | 20 | export const markdownHOC = compose( 21 | defaultProps({ 22 | styles: { 23 | ...githubCss, 24 | ...hlJsCss, 25 | }, 26 | }), 27 | mapPropsOnChange( 28 | ['children'], 29 | ({ children }) => ({ 30 | html: markdown(children), 31 | }) 32 | ), 33 | mapPropsOnChange( 34 | ['html', 'styles'], 35 | ({ ...props, html, styles }) => ({ 36 | ...props, 37 | html: html 38 | .replace( 39 | re, 40 | (match, p1, className) => `${p1}"${styles[className] || className}"` 41 | ), 42 | }) 43 | ) 44 | ); 45 | 46 | export default markdownHOC(markdownComp); 47 | -------------------------------------------------------------------------------- /src/__tests__/MarkdownIt.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import React from 'react'; 3 | import { mount } from 'enzyme'; 4 | import Markdown from '../index'; 5 | 6 | describe('Markdown test', () => { 7 | it('should render Components and Markdown', () => { 8 | const component = mount( 9 | EXAMPLE
} 12 | > 13 | {` 14 | # hello 15 | [example](http://blablabla.github.io/react-components-markdown/#example) 16 | world 17 | `} 18 |
19 | ); 20 | 21 | const exampleComponent = component.find('.example').first(); 22 | expect(exampleComponent.text()).toEqual('EXAMPLE'); 23 | 24 | const mds = component.find('.md-body').map((c) => c.html()); 25 | expect(mds).toEqual([ 26 | '

hello

\n
', 27 | '

world

\n
', 28 | ]); 29 | }); 30 | 31 | it('should update components on prop changes', () => { 32 | const component = mount( 33 | EXAMPLE} 35 | > 36 | {` 37 | # hello 38 | [example](http://blablabla.github.io/react-components-markdown/#example) 39 | world 40 | `} 41 | 42 | ); 43 | 44 | component.setProps({ example:
CHANGED
}); 45 | const changedComponent = component.find('.changed').first(); 46 | expect(changedComponent.text()).toEqual('CHANGED'); 47 | }); 48 | 49 | it('should allow different regexes', () => { 50 | const component = mount( 51 | EXAMPLE} 53 | componentRe={/---([\w]+)/} 54 | > 55 | {` 56 | # hello 57 | ---example 58 | world 59 | `} 60 | 61 | ); 62 | 63 | const exampleComponent = component.find('.example').first(); 64 | expect(exampleComponent.text()).toEqual('EXAMPLE'); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /src/__tests__/MarkdownPart.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import React from 'react'; 3 | import { mount, render } from 'enzyme'; 4 | import MarkdownPart from '../MarkdownPart'; 5 | import hlJsCss from 'highlight.js/styles/github.css'; 6 | 7 | describe('MarkdownPart test', () => { 8 | // ------------------------------------------------------------------------------------ 9 | // Use jsdom rendering 10 | // ------------------------------------------------------------------------------------ 11 | 12 | it('should render MarkdownPart', () => { 13 | const component = mount( 14 | {'# header'} 15 | ); 16 | 17 | const innerDiv = component.find('div').first(); 18 | // can't use contains here because of dangerouslySetInnerHTML 19 | expect(innerDiv.html()).toEqual('

header

\n
'); 20 | }); 21 | 22 | it('should update content', () => { 23 | const component = mount( 24 | {'# header'} 25 | ); 26 | 27 | component.setProps({ children: 'bar' }); 28 | 29 | const innerDiv = component.find('div').first(); 30 | expect(innerDiv.html()).toEqual('

bar

\n
'); 31 | }); 32 | 33 | it('should update content on style changes', () => { 34 | const component = mount( 35 | {'# header'} 36 | ); 37 | 38 | component.setProps({ styles: { 'markdown-body': 'md-body-new' } }); 39 | 40 | const innerDiv = component.find('div').first(); 41 | expect(innerDiv.html()).toEqual('

header

\n
'); 42 | }); 43 | 44 | // ------------------------------------------------------------------------------------ 45 | // Use html rendering 46 | // ------------------------------------------------------------------------------------ 47 | 48 | it('should draw code', () => { 49 | const component = render( 50 | {` 51 | \`\`\`javascript 52 | const x = 'Hello World'; 53 | \`\`\` 54 | `} 55 | 56 | ); 57 | 58 | const innerDiv = component.find(`.${hlJsCss['hljs-string']}`); 59 | expect(innerDiv.html()).toEqual(''Hello World''); 60 | }); 61 | 62 | it('should set default classNames if style is empty object', () => { 63 | const component = render( 64 | {` 65 | \`\`\`javascript 66 | const x = 'Hello World'; 67 | \`\`\` 68 | `} 69 | 70 | ); 71 | 72 | const innerDiv = component.find('.hljs-string'); 73 | expect(innerDiv.html()).toEqual(''Hello World''); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export default from './MarkdownIt'; 2 | -------------------------------------------------------------------------------- /src/utils/markdown.js: -------------------------------------------------------------------------------- 1 | import hljs from 'highlight.js'; 2 | import markdownIt from 'markdown-it'; 3 | 4 | const markdown = (() => { 5 | const md = markdownIt({ 6 | highlight(str, lang) { 7 | if (lang && hljs.getLanguage(lang)) { 8 | try { 9 | return hljs.highlight(lang, str, true).value; 10 | } catch (__) {/* */} 11 | } 12 | return ( 13 | `${md.utils.escapeHtml(str)}` 14 | ); 15 | }, 16 | }); 17 | 18 | return (src) => md.render(src); 19 | })(); 20 | 21 | export default markdown; 22 | -------------------------------------------------------------------------------- /src/utils/parsemd.js: -------------------------------------------------------------------------------- 1 | import RegExp from 'core-js/fn/regexp/constructor'; 2 | import flatMap from 'lodash/flatMap'; 3 | import zipWith from 'lodash/zipWith'; 4 | 5 | // TODO rewrite on md ast, this is wrong in some situations 6 | export default (componentRe, str) => { 7 | const regexpG = new RegExp(componentRe, 'g'); 8 | 9 | const strArray = str.split('```'); 10 | 11 | const keys = strArray 12 | .reduce( 13 | (r, s, index) => ([ 14 | ...r, 15 | ...( 16 | index % 2 === 0 17 | ? s.match(regexpG) || [] 18 | : [] 19 | ), 20 | ]), 21 | [] 22 | ); 23 | 24 | const data = keys.reduce( 25 | (r, key) => flatMap(r, (v) => v.split(key)), 26 | [str] 27 | ); 28 | 29 | return zipWith( 30 | data, 31 | keys 32 | .map(key => key.match(componentRe)[1]), 33 | (md, componentKey) => ({ md, componentKey }) 34 | ); 35 | }; 36 | --------------------------------------------------------------------------------