1 | 'use strict';
|
2 |
|
3 | var _ = require('underscore')
|
4 | , anchor = require('anchor-markdown-header')
|
5 | , updateSection = require('update-section');
|
6 |
|
7 | var 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 |
|
11 | function matchesStart(line) {
|
12 | return (/<!-- START doctoc generated TOC /).test(line);
|
13 | }
|
14 |
|
15 | function matchesEnd(line) {
|
16 | return (/<!-- END doctoc generated TOC /).test(line);
|
17 | }
|
18 |
|
19 | function notNull(x) { return x !== null; }
|
20 |
|
21 | function addAnchor(mode, header) {
|
22 | header.anchor = anchor(header.name, mode, header.instance);
|
23 | return header;
|
24 | }
|
25 |
|
26 |
|
27 | function getHashedHeaders (lines) {
|
28 | var inCodeBlock = false;
|
29 |
|
30 |
|
31 | function normalize(header) {
|
32 | return header.replace(/[ #]+$/, '');
|
33 | }
|
34 |
|
35 |
|
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 |
|
56 | function getUnderlinedHeaders (lines) {
|
57 |
|
58 |
|
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 |
|
79 | function 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 |
|
98 | function getLinesToToc (lines, currentToc, info) {
|
99 | if (!currentToc) return lines;
|
100 |
|
101 | var tocableStart = 0;
|
102 |
|
103 |
|
104 |
|
105 | if (info.hasEnd) tocableStart = info.endIdx;
|
106 |
|
107 | return lines.slice(tocableStart);
|
108 | }
|
109 |
|
110 | exports = 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 |
|
153 | exports.start = start;
|
154 | exports.end = end;
|