UNPKG

11.2 kBJavaScriptView Raw
1// Process footnotes
2//
3'use strict';
4
5////////////////////////////////////////////////////////////////////////////////
6// Renderer partials
7
8function render_footnote_anchor_name(tokens, idx, options, env/*, slf*/) {
9 var n = Number(tokens[idx].meta.id + 1).toString();
10
11 if (tokens[idx].meta.subId > 0) {
12 n += ':' + tokens[idx].meta.subId;
13 }
14
15 var prefix = '';
16
17 if (typeof env.docId === 'string') {
18 prefix = '-' + env.docId + '-';
19 }
20
21 return prefix + n;
22}
23
24function render_footnote_caption(tokens, idx/*, options, env, slf*/) {
25 var n = Number(tokens[idx].meta.id + 1).toString();
26
27 if (tokens[idx].meta.subId > 0) {
28 n += ':' + tokens[idx].meta.subId;
29 }
30
31 return '[' + n + ']';
32}
33
34function render_footnote_ref(tokens, idx, options, env, slf) {
35 var id = slf.rules.footnote_anchor_name(tokens, idx, options, env, slf);
36 var caption = slf.rules.footnote_caption(tokens, idx, options, env, slf);
37
38 return '<sup class="footnote-ref"><a href="#fn' + id + '" id="fnref' + id + '">' + caption + '</a></sup>';
39}
40
41function render_footnote_block_open(tokens, idx, options) {
42 return (options.xhtmlOut ? '<hr class="footnotes-sep" />\n' : '<hr class="footnotes-sep">\n') +
43 '<section class="footnotes">\n' +
44 '<ol class="footnotes-list">\n';
45}
46
47function render_footnote_block_close() {
48 return '</ol>\n</section>\n';
49}
50
51function render_footnote_open(tokens, idx, options, env, slf) {
52 var id = slf.rules.footnote_anchor_name(tokens, idx, options, env, slf);
53
54 return '<li id="fn' + id + '" class="footnote-item">';
55}
56
57function render_footnote_close() {
58 return '</li>\n';
59}
60
61function render_footnote_anchor(tokens, idx, options, env, slf) {
62 var id = slf.rules.footnote_anchor_name(tokens, idx, options, env, slf);
63
64 /* ↩ with escape code to prevent display as Apple Emoji on iOS */
65 return ' <a href="#fnref' + id + '" class="footnote-backref">\u21a9\uFE0E</a>';
66}
67
68
69module.exports = function footnote_plugin(md) {
70 var parseLinkLabel = md.helpers.parseLinkLabel,
71 isSpace = md.utils.isSpace;
72
73 md.renderer.rules.footnote_ref = render_footnote_ref;
74 md.renderer.rules.footnote_block_open = render_footnote_block_open;
75 md.renderer.rules.footnote_block_close = render_footnote_block_close;
76 md.renderer.rules.footnote_open = render_footnote_open;
77 md.renderer.rules.footnote_close = render_footnote_close;
78 md.renderer.rules.footnote_anchor = render_footnote_anchor;
79
80 // helpers (only used in other rules, no tokens are attached to those)
81 md.renderer.rules.footnote_caption = render_footnote_caption;
82 md.renderer.rules.footnote_anchor_name = render_footnote_anchor_name;
83
84 // Process footnote block definition
85 function footnote_def(state, startLine, endLine, silent) {
86 var oldBMark, oldTShift, oldSCount, oldParentType, pos, label, token,
87 initial, offset, ch, posAfterColon,
88 start = state.bMarks[startLine] + state.tShift[startLine],
89 max = state.eMarks[startLine];
90
91 // line should be at least 5 chars - "[^x]:"
92 if (start + 4 > max) { return false; }
93
94 if (state.src.charCodeAt(start) !== 0x5B/* [ */) { return false; }
95 if (state.src.charCodeAt(start + 1) !== 0x5E/* ^ */) { return false; }
96
97 for (pos = start + 2; pos < max; pos++) {
98 if (state.src.charCodeAt(pos) === 0x20) { return false; }
99 if (state.src.charCodeAt(pos) === 0x5D /* ] */) {
100 break;
101 }
102 }
103
104 if (pos === start + 2) { return false; } // no empty footnote labels
105 if (pos + 1 >= max || state.src.charCodeAt(++pos) !== 0x3A /* : */) { return false; }
106 if (silent) { return true; }
107 pos++;
108
109 if (!state.env.footnotes) { state.env.footnotes = {}; }
110 if (!state.env.footnotes.refs) { state.env.footnotes.refs = {}; }
111 label = state.src.slice(start + 2, pos - 2);
112 state.env.footnotes.refs[':' + label] = -1;
113
114 token = new state.Token('footnote_reference_open', '', 1);
115 token.meta = { label: label };
116 token.level = state.level++;
117 state.tokens.push(token);
118
119 oldBMark = state.bMarks[startLine];
120 oldTShift = state.tShift[startLine];
121 oldSCount = state.sCount[startLine];
122 oldParentType = state.parentType;
123
124 posAfterColon = pos;
125 initial = offset = state.sCount[startLine] + pos - (state.bMarks[startLine] + state.tShift[startLine]);
126
127 while (pos < max) {
128 ch = state.src.charCodeAt(pos);
129
130 if (isSpace(ch)) {
131 if (ch === 0x09) {
132 offset += 4 - offset % 4;
133 } else {
134 offset++;
135 }
136 } else {
137 break;
138 }
139
140 pos++;
141 }
142
143 state.tShift[startLine] = pos - posAfterColon;
144 state.sCount[startLine] = offset - initial;
145
146 state.bMarks[startLine] = posAfterColon;
147 state.blkIndent += 4;
148 state.parentType = 'footnote';
149
150 if (state.sCount[startLine] < state.blkIndent) {
151 state.sCount[startLine] += state.blkIndent;
152 }
153
154 state.md.block.tokenize(state, startLine, endLine, true);
155
156 state.parentType = oldParentType;
157 state.blkIndent -= 4;
158 state.tShift[startLine] = oldTShift;
159 state.sCount[startLine] = oldSCount;
160 state.bMarks[startLine] = oldBMark;
161
162 token = new state.Token('footnote_reference_close', '', -1);
163 token.level = --state.level;
164 state.tokens.push(token);
165
166 return true;
167 }
168
169 // Process inline footnotes (^[...])
170 function footnote_inline(state, silent) {
171 var labelStart,
172 labelEnd,
173 footnoteId,
174 token,
175 tokens,
176 max = state.posMax,
177 start = state.pos;
178
179 if (start + 2 >= max) { return false; }
180 if (state.src.charCodeAt(start) !== 0x5E/* ^ */) { return false; }
181 if (state.src.charCodeAt(start + 1) !== 0x5B/* [ */) { return false; }
182
183 labelStart = start + 2;
184 labelEnd = parseLinkLabel(state, start + 1);
185
186 // parser failed to find ']', so it's not a valid note
187 if (labelEnd < 0) { return false; }
188
189 // We found the end of the link, and know for a fact it's a valid link;
190 // so all that's left to do is to call tokenizer.
191 //
192 if (!silent) {
193 if (!state.env.footnotes) { state.env.footnotes = {}; }
194 if (!state.env.footnotes.list) { state.env.footnotes.list = []; }
195 footnoteId = state.env.footnotes.list.length;
196
197 state.md.inline.parse(
198 state.src.slice(labelStart, labelEnd),
199 state.md,
200 state.env,
201 tokens = []
202 );
203
204 token = state.push('footnote_ref', '', 0);
205 token.meta = { id: footnoteId };
206
207 state.env.footnotes.list[footnoteId] = { tokens: tokens };
208 }
209
210 state.pos = labelEnd + 1;
211 state.posMax = max;
212 return true;
213 }
214
215 // Process footnote references ([^...])
216 function footnote_ref(state, silent) {
217 var label,
218 pos,
219 footnoteId,
220 footnoteSubId,
221 token,
222 max = state.posMax,
223 start = state.pos;
224
225 // should be at least 4 chars - "[^x]"
226 if (start + 3 > max) { return false; }
227
228 if (!state.env.footnotes || !state.env.footnotes.refs) { return false; }
229 if (state.src.charCodeAt(start) !== 0x5B/* [ */) { return false; }
230 if (state.src.charCodeAt(start + 1) !== 0x5E/* ^ */) { return false; }
231
232 for (pos = start + 2; pos < max; pos++) {
233 if (state.src.charCodeAt(pos) === 0x20) { return false; }
234 if (state.src.charCodeAt(pos) === 0x0A) { return false; }
235 if (state.src.charCodeAt(pos) === 0x5D /* ] */) {
236 break;
237 }
238 }
239
240 if (pos === start + 2) { return false; } // no empty footnote labels
241 if (pos >= max) { return false; }
242 pos++;
243
244 label = state.src.slice(start + 2, pos - 1);
245 if (typeof state.env.footnotes.refs[':' + label] === 'undefined') { return false; }
246
247 if (!silent) {
248 if (!state.env.footnotes.list) { state.env.footnotes.list = []; }
249
250 if (state.env.footnotes.refs[':' + label] < 0) {
251 footnoteId = state.env.footnotes.list.length;
252 state.env.footnotes.list[footnoteId] = { label: label, count: 0 };
253 state.env.footnotes.refs[':' + label] = footnoteId;
254 } else {
255 footnoteId = state.env.footnotes.refs[':' + label];
256 }
257
258 footnoteSubId = state.env.footnotes.list[footnoteId].count;
259 state.env.footnotes.list[footnoteId].count++;
260
261 token = state.push('footnote_ref', '', 0);
262 token.meta = { id: footnoteId, subId: footnoteSubId, label: label };
263 }
264
265 state.pos = pos;
266 state.posMax = max;
267 return true;
268 }
269
270 // Glue footnote tokens to end of token stream
271 function footnote_tail(state) {
272 var i, l, j, t, lastParagraph, list, token, tokens, current, currentLabel,
273 insideRef = false,
274 refTokens = {};
275
276 if (!state.env.footnotes) { return; }
277
278 state.tokens = state.tokens.filter(function (tok) {
279 if (tok.type === 'footnote_reference_open') {
280 insideRef = true;
281 current = [];
282 currentLabel = tok.meta.label;
283 return false;
284 }
285 if (tok.type === 'footnote_reference_close') {
286 insideRef = false;
287 // prepend ':' to avoid conflict with Object.prototype members
288 refTokens[':' + currentLabel] = current;
289 return false;
290 }
291 if (insideRef) { current.push(tok); }
292 return !insideRef;
293 });
294
295 if (!state.env.footnotes.list) { return; }
296 list = state.env.footnotes.list;
297
298 token = new state.Token('footnote_block_open', '', 1);
299 state.tokens.push(token);
300
301 for (i = 0, l = list.length; i < l; i++) {
302 token = new state.Token('footnote_open', '', 1);
303 token.meta = { id: i, label: list[i].label };
304 state.tokens.push(token);
305
306 if (list[i].tokens) {
307 tokens = [];
308
309 token = new state.Token('paragraph_open', 'p', 1);
310 token.block = true;
311 tokens.push(token);
312
313 token = new state.Token('inline', '', 0);
314 token.children = list[i].tokens;
315 token.content = '';
316 tokens.push(token);
317
318 token = new state.Token('paragraph_close', 'p', -1);
319 token.block = true;
320 tokens.push(token);
321
322 } else if (list[i].label) {
323 tokens = refTokens[':' + list[i].label];
324 }
325
326 state.tokens = state.tokens.concat(tokens);
327 if (state.tokens[state.tokens.length - 1].type === 'paragraph_close') {
328 lastParagraph = state.tokens.pop();
329 } else {
330 lastParagraph = null;
331 }
332
333 t = list[i].count > 0 ? list[i].count : 1;
334 for (j = 0; j < t; j++) {
335 token = new state.Token('footnote_anchor', '', 0);
336 token.meta = { id: i, subId: j, label: list[i].label };
337 state.tokens.push(token);
338 }
339
340 if (lastParagraph) {
341 state.tokens.push(lastParagraph);
342 }
343
344 token = new state.Token('footnote_close', '', -1);
345 state.tokens.push(token);
346 }
347
348 token = new state.Token('footnote_block_close', '', -1);
349 state.tokens.push(token);
350 }
351
352 md.block.ruler.before('reference', 'footnote_def', footnote_def, { alt: [ 'paragraph', 'reference' ] });
353 md.inline.ruler.after('image', 'footnote_inline', footnote_inline);
354 md.inline.ruler.after('footnote_inline', 'footnote_ref', footnote_ref);
355 md.core.ruler.after('inline', 'footnote_tail', footnote_tail);
356};