├── .gitignore ├── .travis.yml ├── README.md ├── demo.js ├── index.js ├── package.json └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | - "node" 5 | notifications: 6 | email: 7 | on_success: change 8 | on_failure: change 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # markdown-it-header-sections [![Build Status](https://travis-ci.org/arve0/markdown-it-header-sections.svg)](https://travis-ci.org/arve0/markdown-it-header-sections) [![npm version](https://badge.fury.io/js/markdown-it-header-sections.svg)](http://badge.fury.io/js/markdown-it-header-sections) 2 | 3 | 4 | Renders this markdown 5 | ```md 6 | # Header 1 7 | Text. 8 | ### Header 2 9 | Lorem? 10 | ## Header 3 11 | Ipsum. 12 | # Last header 13 | Markdown rules! 14 | ``` 15 | 16 | to this output (without indentation) 17 | ```html 18 |
19 |

Header 1

20 |

Text.

21 |
22 |

Header 2

23 |

Lorem?

24 |
25 |
26 |

Header 3

27 |

Ipsum.

28 |
29 |
30 |
31 |

Last header

32 |

Markdown rules!

33 |
34 | ``` 35 | 36 | If you add [attrs], [anchor] or any other plugin that adds attributes to header-tokens, sections will have the same attributes (which is useful for styling). 37 | 38 | E.g., with [attrs] enabled before header-sections: 39 | 40 | ```js 41 | var md = require('markdown-it')() 42 | .use(require('markdown-it-attrs')) 43 | .use(require('markdown-it-header-sections')) 44 | ``` 45 | 46 | this markdown 47 | ```md 48 | # great stuff {.jumbotron} 49 | lorem 50 | 51 | click me {.btn .btn-default} 52 | ``` 53 | 54 | renders to 55 | ```md 56 |
57 |

great stuff

58 |

lorem

59 |

click me

60 |
61 | ``` 62 | 63 | ## Install 64 | ``` 65 | npm install markdown-it-header-sections 66 | ``` 67 | 68 | ## Usage 69 | ```js 70 | var md = require('markdown-it')(); 71 | md.use(require('markdown-it-header-sections')); 72 | 73 | var src = '# first header\n'; 74 | src += 'lorem\n\n' 75 | src += '## second header\n'; 76 | src += 'ipsum'; 77 | 78 | console.log(md.render(src)); 79 | ``` 80 | 81 | [demo as jsfiddle](https://jsfiddle.net/arve0/5dn54cow/1/) 82 | 83 | 84 | [attrs]: https://github.com/arve0/markdown-it-attrs 85 | [anchor]: https://github.com/valeriangalliat/markdown-it-anchor 86 | -------------------------------------------------------------------------------- /demo.js: -------------------------------------------------------------------------------- 1 | const md = require('markdown-it')() 2 | .use(require('markdown-it-attrs')) 3 | .use(require('markdown-it-header-sections')); 4 | 5 | const src = `# great stuff {.jumbotron} 6 | lorem 7 | 8 | ## section 2 9 | click me {.btn .btn-default}`; 10 | 11 | console.log(md.render(src)); 12 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function headerSections(md) { 3 | 4 | function addSections(state) { 5 | var tokens = []; // output 6 | var Token = state.Token; 7 | var sections = []; 8 | var nestedLevel = 0; 9 | 10 | function openSection(attrs) { 11 | var t = new Token('section_open', 'section', 1); 12 | t.block = true; 13 | t.attrs = attrs && attrs.map(function (attr) { return [attr[0], attr[1]] }); // copy 14 | return t; 15 | } 16 | 17 | function closeSection() { 18 | var t = new Token('section_close', 'section', -1); 19 | t.block = true; 20 | return t; 21 | } 22 | 23 | function closeSections(section) { 24 | while (last(sections) && section.header <= last(sections).header) { 25 | sections.pop(); 26 | tokens.push(closeSection()); 27 | } 28 | } 29 | 30 | function closeSectionsToCurrentNesting(nesting) { 31 | while (last(sections) && nesting < last(sections).nesting) { 32 | sections.pop(); 33 | tokens.push(closeSection()); 34 | } 35 | } 36 | 37 | function closeAllSections() { 38 | while (sections.pop()) { 39 | tokens.push(closeSection()); 40 | } 41 | } 42 | 43 | for (var i = 0, l = state.tokens.length; i < l; i++) { 44 | var token = state.tokens[i]; 45 | 46 | // record level of nesting 47 | if (token.type.search('heading') !== 0) { 48 | nestedLevel += token.nesting; 49 | } 50 | if (last(sections) && nestedLevel < last(sections).nesting) { 51 | closeSectionsToCurrentNesting(nestedLevel); 52 | } 53 | 54 | // add sections before headers 55 | if (token.type == 'heading_open') { 56 | var section = { 57 | header: headingLevel(token.tag), 58 | nesting: nestedLevel 59 | }; 60 | if (last(sections) && section.header <= last(sections).header) { 61 | closeSections(section); 62 | } 63 | tokens.push(openSection(token.attrs)); 64 | if (token.attrIndex('id') !== -1) { 65 | // remove ID from token 66 | token.attrs.splice(token.attrIndex('id'), 1); 67 | } 68 | sections.push(section); 69 | } 70 | 71 | tokens.push(token); 72 | } // end for every token 73 | closeAllSections(); 74 | 75 | state.tokens = tokens; 76 | } 77 | 78 | md.core.ruler.push('header_sections', addSections); 79 | 80 | }; 81 | 82 | function headingLevel(header) { 83 | return parseInt(header.charAt(1)); 84 | } 85 | 86 | function last(arr) { 87 | return arr.slice(-1)[0]; 88 | } 89 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "markdown-it-header-sections", 3 | "version": "1.0.0", 4 | "description": "add sections to markdown headers", 5 | "main": "index.js", 6 | "dependencies": {}, 7 | "devDependencies": { 8 | "markdown-it": "^5.0.2", 9 | "markdown-it-attrs": "^0.1.3", 10 | "mocha": "^2.2.5", 11 | "multiline": "^1.0.2" 12 | }, 13 | "scripts": { 14 | "test": "mocha", 15 | "prepublish": "mocha", 16 | "postpublish": "git push && git push --tags" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/arve0/markdown-it-header-sections.git" 21 | }, 22 | "keywords": [ 23 | "markdown-it", 24 | "markdown-it-plugin" 25 | ], 26 | "author": "Arve Seljebu", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/arve0/markdown-it-header-sections/issues" 30 | }, 31 | "homepage": "https://github.com/arve0/markdown-it-header-sections#readme", 32 | "tonicExampleFilename": "demo.js" 33 | } 34 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | 2 | var assert = require('assert'); 3 | var Md = require('markdown-it'); 4 | var multiline = require('multiline'); 5 | var headerSections = require('./'); 6 | 7 | var origEqual = assert.equal; 8 | assert.equal = function(actual, expected) { 9 | var r = /\r/g 10 | return origEqual(actual.replace(r, ''), expected.replace(r, '')); 11 | } 12 | 13 | describe('markdown-it-header-sections', function(){ 14 | 15 | var md; 16 | var simpleSrc = multiline.stripIndent(function(){/* 17 | # header 18 | lorem 19 | */}); 20 | 21 | beforeEach(function(){ 22 | md = Md(); 23 | }); 24 | 25 | it('should add sections to headers', function(){ 26 | var expected = multiline.stripIndent(function(){/* 27 |
28 |

header

29 |

lorem

30 |
31 | 32 | */}); 33 | md.use(headerSections); 34 | var res = md.render(simpleSrc); 35 | assert.equal(res, expected); 36 | }); 37 | 38 | it('should add header attributes from other plugins', function(){ 39 | var src = multiline.stripIndent(function(){/* 40 | # header {.red} 41 | lorem 42 | */}); 43 | var expected = multiline.stripIndent(function(){/* 44 |
45 |

header

46 |

lorem

47 |
48 | 49 | */}); 50 | md.use(require('markdown-it-attrs')); 51 | md.use(headerSections); 52 | var res = md.render(src); 53 | assert.equal(res, expected); 54 | }); 55 | 56 | it('should close sections when a new header is of same or lower level', function(){ 57 | var src = multiline.stripIndent(function(){/* 58 | # asdf 59 | lorem 60 | # fdsa 61 | ipsum 62 | */}); 63 | var expected = multiline.stripIndent(function(){/* 64 |
65 |

asdf

66 |

lorem

67 |
68 |
69 |

fdsa

70 |

ipsum

71 |
72 | 73 | */}); 74 | md.use(headerSections); 75 | var res = md.render(src); 76 | assert.equal(res, expected); 77 | }); 78 | 79 | it('should nest sections', function(){ 80 | var src = multiline.stripIndent(function(){/* 81 | # Header 1 82 | Text. 83 | ### Header 2 84 | Lorem? 85 | ## Header 3 86 | Ipsum. 87 | # Last header 88 | Markdown rules! 89 | */}); 90 | var expected = multiline.stripIndent(function(){/* 91 |
92 |

Header 1

93 |

Text.

94 |
95 |

Header 2

96 |

Lorem?

97 |
98 |
99 |

Header 3

100 |

Ipsum.

101 |
102 |
103 |
104 |

Last header

105 |

Markdown rules!

106 |
107 | 108 | */}); 109 | md.use(headerSections); 110 | var res = md.render(src); 111 | assert.equal(res, expected); 112 | }); 113 | 114 | it('should parse incorrect order of headers', function(){ 115 | var src = multiline.stripIndent(function(){/* 116 | #### Header 4 117 | Text. 118 | ### Header 3 119 | Hello! 120 | */}); 121 | var expected = multiline.stripIndent(function(){/* 122 |
123 |

Header 4

124 |

Text.

125 |
126 |
127 |

Header 3

128 |

Hello!

129 |
130 | 131 | */}); 132 | md.use(headerSections); 133 | var res = md.render(src); 134 | assert.equal(res, expected); 135 | }); 136 | it('should handle sections in list', function(){ 137 | var src = multiline.stripIndent(function(){/* 138 | - foo 139 | ### Header 2 140 | Lorem? 141 | - bar 142 | ## Last header 143 | Markdown rules! 144 | */}); 145 | var expected = multiline.stripIndent(function(){/* 146 | 158 | 159 | */}); 160 | md.use(headerSections); 161 | var res = md.render(src); 162 | assert.equal(res, expected); 163 | }); 164 | 165 | it('should close sections when a new header is of same level', function(){ 166 | var src = multiline.stripIndent(function(){/* 167 | ### asdf 168 | lorem 169 | ### fdsa 170 | ipsum 171 | */}); 172 | var expected = multiline.stripIndent(function(){/* 173 |
174 |

asdf

175 |

lorem

176 |
177 |
178 |

fdsa

179 |

ipsum

180 |
181 | 182 | */}); 183 | md.use(headerSections); 184 | var res = md.render(src); 185 | assert.equal(res, expected); 186 | }); 187 | 188 | it('should move, not copy, ID from header', function(){ 189 | var src = multiline.stripIndent(function(){/* 190 | # asdf {#asdf} 191 | qwerty 192 | */}); 193 | var expected = multiline.stripIndent(function(){/* 194 |
195 |

asdf

196 |

qwerty

197 |
198 | 199 | */}); 200 | md.use(require('markdown-it-attrs')); 201 | md.use(headerSections); 202 | var res = md.render(src); 203 | assert.equal(res, expected); 204 | }); 205 | }); 206 | --------------------------------------------------------------------------------