UNPKG

4.09 kBJavaScriptView Raw
1'use strict';
2
3var _ = require('underscore')
4 , anchor = require('anchor-markdown-header')
5 , updateSection = require('update-section');
6
7var start = '<!-- START doctoc generated TOC please keep comment here to allow auto update -->\n' +
8 '<!-- DON\'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->'
9 , end = '<!-- END doctoc generated TOC please keep comment here to allow auto update -->'
10
11function matchesStart(line) {
12 return (/<!-- START doctoc generated TOC /).test(line);
13}
14
15function matchesEnd(line) {
16 return (/<!-- END doctoc generated TOC /).test(line);
17}
18
19function notNull(x) { return x !== null; }
20
21function addAnchor(mode, header) {
22 header.anchor = anchor(header.name, mode, header.instance);
23 return header;
24}
25
26
27function getHashedHeaders (lines) {
28 var inCodeBlock = false;
29
30 // Turn all headers into '## xxx' even if they were '## xxx ##'
31 function normalize(header) {
32 return header.replace(/[ #]+$/, '');
33 }
34
35 // Find headers of the form '### xxxx xxx xx [###]'
36 return lines
37 .filter(function (x) {
38 if (x.match(/^```/)) {
39 inCodeBlock = !inCodeBlock;
40 }
41 return !inCodeBlock;
42 })
43 .map(function (x, index) {
44 var match = /^(\#{1,8})[ ]*(.+)\r?$/.exec(x);
45
46 return match
47 ? { rank : match[1].length
48 , name : normalize(match[2])
49 , line : index
50 }
51 : null;
52 })
53 .filter(notNull)
54}
55
56function getUnderlinedHeaders (lines) {
57 // Find headers of the form
58 // h1 h2
59 // == --
60
61 return lines
62 .map(function (line, index, lines_) {
63 if (index === 0) return null;
64 var rank;
65
66 if (/^==+ *\r?$/.exec(line)) rank = 1;
67 else if (/^--+ *\r?$/.exec(line)) rank = 2;
68 else return null;
69
70 return {
71 rank : rank,
72 name : lines_[index - 1],
73 line : index - 1
74 };
75 })
76 .filter(notNull)
77}
78
79function countHeaders (headers) {
80 var instances = {};
81
82 for (var i = 0; i < headers.length; i++) {
83 var header = headers[i];
84 var name = header.name;
85
86 if (instances.hasOwnProperty(name)) {
87 instances[name]++;
88 } else {
89 instances[name] = 0;
90 }
91
92 header.instance = instances[name];
93 }
94
95 return headers;
96}
97
98function getLinesToToc (lines, currentToc, info) {
99 if (!currentToc) return lines;
100
101 var tocableStart = 0;
102
103 // when updating an existing toc, we only take the headers into account
104 // that are below the existing toc
105 if (info.hasEnd) tocableStart = info.endIdx;
106
107 return lines.slice(tocableStart);
108}
109
110exports = module.exports = function transform(content, mode) {
111 mode = mode || 'github.com';
112
113 var lines = content.split('\n')
114 , info = updateSection.parse(lines, matchesStart, matchesEnd)
115
116 var currentToc = info.hasStart && lines.slice(info.startIdx, info.endIdx).join('\n')
117 , linesToToc = getLinesToToc(lines, currentToc, info);
118
119 var headers = getHashedHeaders(linesToToc).concat(getUnderlinedHeaders(linesToToc));
120
121 headers.sort(function (a, b) {
122 return a.line - b.line;
123 });
124
125 var allHeaders = countHeaders(headers)
126 , lowestRank = _(allHeaders).chain().pluck('rank').min().value()
127 , linkedHeaders = _(allHeaders).map(addAnchor.bind(null, mode));
128
129 if (linkedHeaders.length === 0) return { transformed: false };
130
131
132 var toc =
133 '**Table of Contents** *generated with [DocToc](http://doctoc.herokuapp.com/)*'
134 + '\n\n'
135 + linkedHeaders
136 .map(function (x) {
137 var indent = _(_.range(x.rank - lowestRank))
138 .reduce(function (acc, x) { return acc + '\t'; }, '');
139
140 return indent + '- ' + x.anchor;
141 })
142 .join('\n')
143 + '\n';
144
145 var wrappedToc = start + '\n' + toc + '\n' + end;
146
147 if (currentToc === toc) return { transformed: false };
148
149 var data = updateSection(lines.join('\n'), wrappedToc, matchesStart, matchesEnd, true);
150 return { transformed : true, data : data, toc: toc, wrappedToc: wrappedToc };
151};
152
153exports.start = start;
154exports.end = end;