├── .babelrc ├── .gitignore ├── .npmignore ├── README.md ├── docs ├── default.css ├── index.html └── index.js ├── index.css ├── index.js ├── package.json └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "react", 4 | "es2015", 5 | "stage-0" 6 | ] 7 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea 2 | /docs -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | imports: 3 | import moment from 'moment'; 4 | --- 5 | 6 | # markdown-it-react-loader 7 | 8 | 用Markdown提供一份直观的React文档,有可运行的示例,有示例源代码,有示例的说明。 9 | 10 | 这样用户看起来直观,编写者写起来也直观,维护成本低。 11 | 12 | 经过几番尝试,结合 React 的特点。写了一套处理 Markdown 文件的 webpack loader,可以将 Markdown 转成 React 文件。 13 | 14 | 本md对应生成的文档是[readme.md](https://liyatang.github.io/markdown-it-react-loader/) 15 | 16 | --- 17 | 18 | ## Install 19 | 20 | `npm install markdown-it-react-loader` 21 | 22 | 在 webpack 中加入 loader 23 | 24 | ```js 25 | { 26 | test: /\.md$/, 27 | loader: 'babel!markdown-it-react-loader' 28 | } 29 | ``` 30 | 31 | 随后把md文件当成一个react component去使用即可。比如本工程中的demo 32 | 33 | ```js 34 | import ReadMe from '../README.md'; 35 | ``` 36 | 37 | 如需运行demo `npm install; npm start;` 打开 http://localhost:8080 38 | 39 | ### 样式 40 | 41 | 提供样式文件`index.css`,可直接引入或自定义。 42 | 43 | --- 44 | 45 | ## 语法介绍 46 | 47 | 正常的Markdown语法不影响。有几个需要注意的地方: 48 | 49 | ### 使用示例 50 | 51 | #### 纯渲染 52 | 53 | ::: demo 这是描述这是**描述**,点三角可展开代码。也可以不提供 54 | ```jsx 55 | 56 | ``` 57 | ::: 58 | 59 | ``` 60 | ::: demo 这是描述这是**描述**,点三角可展开代码 61 | ```jsx 62 | 63 | ``` 64 | ::: 65 | ``` 66 | 67 | 注意:渲染到页面的代码语言必须写`jsx`,因为loaders会把语言为`jsx`放入render的jsx内 68 | 69 | #### 引入其他库 70 | 71 | ::: demo 72 | ```jsx 73 |
adfaf{moment().format('YYYY-MM-DD')}
74 | ``` 75 | ::: 76 | 77 | 在md开头添加引入库 78 | 79 | ```js 80 | --- 81 | imports: 82 | import moment from 'moment'; 83 | --- 84 | ``` 85 | 86 | 然后 87 | 88 | ``` 89 | ::: demo moment 90 | ```jsx 91 |
adfaf{moment().format('YYYY-MM-DD')}
92 | ``` 93 | ::: 94 | ``` 95 | 96 | #### 更丰富的交互 97 | 98 | 比如需要 state,需要handleXXX 99 | 100 | ::: demo 更丰富的交互写在js内,这种场景更多 101 | ```js 102 | class Test extends React.Component { 103 | constructor(props) { 104 | super(props); 105 | this.state = { 106 | value: 'hello' 107 | }; 108 | } 109 | handleChange(e){ 110 | this.setState({value: e.target.value}); 111 | } 112 | render(){ 113 | return () 114 | } 115 | } 116 | ``` 117 | ```jsx 118 | 119 | ``` 120 | ::: 121 | 122 | ``` 123 | ::: demo 更丰富的交互写在js内,这种场景更多 124 | ```js 125 | class Test extends React.Component { 126 | constructor(props) { 127 | super(props); 128 | this.state = { 129 | value: 'hello' 130 | }; 131 | } 132 | handleChange(e){ 133 | this.setState({value: e.target.value}); 134 | } 135 | render(){ 136 | return () 137 | } 138 | } 139 | ``` 140 | ```jsx 141 | 142 | ``` 143 | ::: 144 | ``` 145 | 146 | 147 | ### 花括号 (表达式) 148 | 149 | 有意思的是可以用花括号写表达式,比如我要显示 150 | 151 | 当前url是:`{location.href}` 152 | 153 | userAgent是:`{navigator.userAgent}` 154 | 155 | 因而你要用花括号时`{'{}'}`需要写成`{'{\'{}\'}'}` 156 | 157 | ### 代码里面的花括号 158 | 159 | `{'{ }'}`会自动转,无需关注 160 | 161 | ```jsx 162 |
{location.href}
163 | ``` 164 | 165 | --- 166 | 167 | ## 参考 168 | 169 | - [markdown-it](https://github.com/markdown-it/markdown-it) 170 | - [element](https://github.com/ElemeFE/element) 171 | - [react-markdown-loader](https://github.com/javiercf/react-markdown-loader) 172 | 173 | ## 其他 174 | 175 | ### anchor 176 | 177 | github page是不支持browserHistory的,一般路由用hash处理。而锚点也是用hash,会冲突。 178 | 所以只能自己处理。 监听锚的点击,阻止默认事件,然后用你自己的规则处理吧。 179 | 180 | 我是这样做的 https://github.com/gmfe/react-gm/blob/master/demo/index.js 181 | 182 | ### react模块 183 | 184 | 默认已经`import React from 'react';` 185 | -------------------------------------------------------------------------------- /docs/default.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Original highlight.js style (c) Ivan Sagalaev 4 | 5 | */ 6 | 7 | .hljs { 8 | display: block; 9 | overflow-x: auto; 10 | padding: 0.5em; 11 | background: #F0F0F0; 12 | } 13 | 14 | 15 | /* Base color: saturation 0; */ 16 | 17 | .hljs, 18 | .hljs-subst { 19 | color: #444; 20 | } 21 | 22 | .hljs-comment { 23 | color: #888888; 24 | } 25 | 26 | .hljs-keyword, 27 | .hljs-attribute, 28 | .hljs-selector-tag, 29 | .hljs-meta-keyword, 30 | .hljs-doctag, 31 | .hljs-name { 32 | font-weight: bold; 33 | } 34 | 35 | 36 | /* User color: hue: 0 */ 37 | 38 | .hljs-type, 39 | .hljs-string, 40 | .hljs-number, 41 | .hljs-selector-id, 42 | .hljs-selector-class, 43 | .hljs-quote, 44 | .hljs-template-tag, 45 | .hljs-deletion { 46 | color: #880000; 47 | } 48 | 49 | .hljs-title, 50 | .hljs-section { 51 | color: #880000; 52 | font-weight: bold; 53 | } 54 | 55 | .hljs-regexp, 56 | .hljs-symbol, 57 | .hljs-variable, 58 | .hljs-template-variable, 59 | .hljs-link, 60 | .hljs-selector-attr, 61 | .hljs-selector-pseudo { 62 | color: #BC6060; 63 | } 64 | 65 | 66 | /* Language color: hue: 90; */ 67 | 68 | .hljs-literal { 69 | color: #78A960; 70 | } 71 | 72 | .hljs-built_in, 73 | .hljs-bullet, 74 | .hljs-code, 75 | .hljs-addition { 76 | color: #397300; 77 | } 78 | 79 | 80 | /* Meta color: hue: 200 */ 81 | 82 | .hljs-meta { 83 | color: #1f7199; 84 | } 85 | 86 | .hljs-meta-string { 87 | color: #4d99bf; 88 | } 89 | 90 | 91 | /* Misc effects */ 92 | 93 | .hljs-emphasis { 94 | font-style: italic; 95 | } 96 | 97 | .hljs-strong { 98 | font-weight: bold; 99 | } 100 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 36 | 37 | 38 |
39 | 40 | 42 | Star 43 | 44 |
45 |
46 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /docs/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import ReadMe from '../README.md' 4 | 5 | class Index extends React.Component { 6 | render () { 7 | return ( 8 | 9 | ) 10 | } 11 | } 12 | 13 | ReactDOM.render(, document.getElementById('appContainer')) 14 | -------------------------------------------------------------------------------- /index.css: -------------------------------------------------------------------------------- 1 | .doc { 2 | 3 | } 4 | 5 | .doc h1:hover .header-anchor, 6 | .doc h2:hover .header-anchor, 7 | .doc h3:hover .header-anchor, 8 | .doc h4:hover .header-anchor, 9 | .doc h5:hover .header-anchor, 10 | .doc h6:hover .header-anchor{ 11 | visibility: visible; 12 | } 13 | 14 | .doc .header-anchor{ 15 | visibility: hidden; 16 | font-size: 14px; 17 | color: inherit; 18 | float: left; 19 | padding-right: 4px; 20 | margin-left: -14px; 21 | } 22 | 23 | .doc-demo-box { 24 | border: 1px solid #eee; 25 | margin-bottom: 20px; 26 | } 27 | 28 | .doc-demo-box:hover { 29 | box-shadow: 0 0 8px 0 rgba(232, 237, 250, .6), 0 2px 4px 0 rgba(232, 237, 250, .5); 30 | z-index: 10; 31 | } 32 | 33 | .doc-demo-instance { 34 | padding: 10px; 35 | } 36 | 37 | .doc-demo-instance>h4 { 38 | margin: 0 0 10px; 39 | font-size: 14px; 40 | } 41 | 42 | .doc-demo-meta { 43 | } 44 | 45 | .doc-demo-description { 46 | border-top: 1px solid #eee; 47 | padding: 10px; 48 | } 49 | 50 | .doc-demo-description>p { 51 | margin: 0; 52 | } 53 | 54 | .doc-demo-code { 55 | display: none; 56 | border-top: 1px dashed #eee; 57 | padding: 10px; 58 | } 59 | 60 | .doc-demo-code>pre { 61 | background: white; 62 | margin: 0; 63 | padding: 0; 64 | border: none; 65 | } 66 | 67 | .doc-demo-code-btn { 68 | padding: 10px; 69 | border-top: 1px solid #eee; 70 | cursor: pointer; 71 | } 72 | 73 | .doc-demo-code-btn i { 74 | margin: auto; 75 | display: block; 76 | border-bottom: 8px solid transparent; 77 | border-left: 8px solid transparent; 78 | border-top: 8px solid #eee; 79 | border-right: 8px solid transparent; 80 | margin-bottom: -8px; 81 | width: 0; 82 | height: 0; 83 | } 84 | 85 | .doc-demo-box.doc-demo-code-active .doc-demo-code { 86 | display: block; 87 | } 88 | 89 | .doc-demo-box.code-active .doc-demo-code-btn i { 90 | border-top: 8px solid transparent; 91 | border-left: 8px solid transparent; 92 | border-bottom: 8px solid #e0e0e0; 93 | border-right: 8px solid transparent; 94 | margin-top: -8px; 95 | margin-bottom: 0; 96 | } -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const mdContainer = require('markdown-it-container') 2 | const anchor = require('markdown-it-anchor') 3 | const frontMatter = require('front-matter') 4 | const highlight = require('highlight.js') 5 | const slugify = require('transliteration').slugify 6 | 7 | let md = require('markdown-it') 8 | 9 | let options = { 10 | className: 'doc' 11 | } 12 | 13 | md = md('commonmark').enable([ 14 | 'smartquotes' 15 | ]).use(anchor, { 16 | slugify: slugify, 17 | permalink: true, 18 | permalinkBefore: true 19 | }).set({ 20 | highlight (content, languageHint) { 21 | let highlightedContent 22 | 23 | highlight.configure({ 24 | useBR: true, 25 | tabReplace: ' ' 26 | }) 27 | 28 | if (languageHint && highlight.getLanguage(languageHint)) { 29 | try { 30 | highlightedContent = highlight.highlight(languageHint, content).value 31 | } catch (err) { 32 | } 33 | } 34 | 35 | if (!highlightedContent) { 36 | try { 37 | highlightedContent = highlight.highlightAuto(content).value 38 | } catch (err) { 39 | } 40 | } 41 | 42 | // 把代码中的{}转 43 | highlightedContent = highlightedContent.replace(/[\{\}]/g, (match) => `{'${match}'}`) 44 | 45 | // 加上 hljs 46 | highlightedContent = highlightedContent.replace('', '') 47 | 48 | return highlight.fixMarkup(highlightedContent) 49 | } 50 | }) 51 | 52 | const formatModule = (imports, js, jsx) => { 53 | let moduleText = ` 54 | ${imports} 55 | 56 | ${js} 57 | 58 | class MarkdownItReactComponent extends React.Component { 59 | constructor(props){ 60 | super(props); 61 | this.state = {}; 62 | } 63 | handleToggleCode(flag){ 64 | const state = {}; 65 | state['showCode' + flag] = !this.state['showCode' + flag]; 66 | this.setState(state); 67 | } 68 | 69 | render(){ 70 | return ( 71 |
72 | ${jsx} 73 |
74 | ); 75 | } 76 | }; 77 | 78 | export default MarkdownItReactComponent;` 79 | 80 | return moduleText 81 | } 82 | 83 | const formatOpening = (code, description, flag) => { 84 | 85 | const desc = description ? `
${description}
` : '' 86 | 87 | return ( 88 | `
89 |
90 |

Example

91 | ${code} 92 |
93 |
94 | ${desc} 95 |
`) 96 | } 97 | 98 | const formatClosing = (flag) => { 99 | return ( 100 | `
101 |
102 | 103 |
104 |
105 |
`) 106 | } 107 | 108 | module.exports = function (source) { 109 | this.cacheable() 110 | 111 | // init options 112 | // TODO 113 | // Object.assign(options, this.options.markdownItReact ? this.options.markdownItReact() : {}) 114 | 115 | const { body, attributes: { imports: importMap } } = frontMatter(source) 116 | const imports = 'import React from \'react\'; ' + importMap 117 | 118 | let moduleJS = [] 119 | 120 | // 放在这里应该没有问题, 反正是顺序执行的 121 | let flag = '' 122 | 123 | md.use(mdContainer, 'demo', { 124 | validate: function (params) { 125 | return params.trim().match(/^demo\s*(.*)$/) 126 | }, 127 | render: function (tokens, idx) { 128 | // container 从开头到结尾把之间的token跑一遍,其中idx定位到具体的位置 129 | 130 | // 获取描述 131 | const m = tokens[idx].info.trim().match(/^demo\s*(.*)$/) 132 | 133 | // 有此标记代表 ::: 开始 134 | if (tokens[idx].nesting === 1) { 135 | flag = idx 136 | 137 | let jsx = '', i = 1 138 | 139 | // 从 ::: 下一个token开始 140 | let token = tokens[idx + i] 141 | 142 | // 如果没有到结尾 143 | while (token.markup !== ':::') { 144 | // 只认```,其他忽略 145 | if (token.markup === '```') { 146 | if (token.info === 'js') { 147 | // 插入到import后,component前 148 | moduleJS.push(token.content) 149 | } else if (token.info === 'jsx') { 150 | // 插入render内 151 | jsx = token.content 152 | } 153 | } 154 | i++ 155 | token = tokens[idx + i] 156 | } 157 | 158 | // 描述也执行md 159 | return formatOpening(jsx, md.render(m[1]), flag) 160 | } 161 | return formatClosing(flag) 162 | } 163 | }) 164 | 165 | // md 处理过后的字符串含有 class 和 style ,需要再次处理给到react 166 | let content = md.render(body) 167 | .replace(/
/g, '
') 168 | .replace(/
/g, '
') 169 | .replace(/class=/g, 'className=') 170 | 171 | return formatModule(imports, moduleJS.join('\n'), content) 172 | } 173 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "markdown-it-react-loader", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "NODE_ENV=development webpack-dev-server --colors", 8 | "gh-pages": "webpack --color; git add --all; git commit -m 'gh-pages';git push origin master:master;" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/liyatang/markdown-it-react-loader.git" 13 | }, 14 | "author": "", 15 | "license": "ISC", 16 | "bugs": { 17 | "url": "https://github.com/liyatang/markdown-it-react-loader/issues" 18 | }, 19 | "homepage": "https://github.com/liyatang/markdown-it-react-loader#readme", 20 | "dependencies": { 21 | "front-matter": "^3.0.1", 22 | "highlight.js": "^9.13.1", 23 | "markdown-it": "^8.4.2", 24 | "markdown-it-anchor": "^5.0.2", 25 | "markdown-it-container": "^2.0.0", 26 | "markdown-it-regexp": "^0.4.0", 27 | "transliteration": "^2.1.2" 28 | }, 29 | "devDependencies": { 30 | "babel-core": "^6.26.3", 31 | "babel-loader": "^7.1.5", 32 | "babel-preset-es2015": "^6.24.1", 33 | "babel-preset-react": "^6.24.1", 34 | "babel-preset-stage-0": "^6.24.1", 35 | "moment": "^2.24.0", 36 | "path": "^0.12.7", 37 | "react": "^16.7.0", 38 | "react-dom": "^16.7.0", 39 | "webpack": "^4.29.0", 40 | "webpack-cli": "^3.2.1", 41 | "webpack-dev-server": "^3.1.14" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | mode: process.env.NODE_ENV, 5 | entry: './docs/index.js', 6 | output: { 7 | path: path.join(__dirname, 'docs/build'), 8 | filename: '[name].bundle.js', 9 | publicPath: '/markdown-it-react-loader/docs/build/' 10 | }, 11 | module: { 12 | rules: [{ 13 | test: /\.js$/, 14 | loader: 'babel-loader' 15 | }, { 16 | test: /\.md$/, 17 | loader: 'babel-loader!./index.js' 18 | }] 19 | } 20 | } 21 | --------------------------------------------------------------------------------