├── .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 [](https://travis-ci.org/arve0/markdown-it-header-sections) [](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 |
147 | - foo
148 |
149 |
Header 2
150 | Lorem?
151 |
152 | - bar
153 |
154 |
Last header
155 | Markdown rules!
156 |
157 |
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 |
--------------------------------------------------------------------------------