UNPKG

13.4 kBJavaScriptView Raw
1'use strict';
2var DFA = require('./lib/dfa.js');
3
4module.exports = function multimd_table_plugin(md, options) {
5 var defaults = {
6 multiline: false,
7 rowspan: false,
8 headerless: false,
9 multibody: true
10 };
11 options = md.utils.assign({}, defaults, options || {});
12
13 function scan_bound_indices(state, line) {
14 /**
15 * Naming convention of positional variables
16 * - list-item
17 * ·········longtext······\n
18 * ^head ^start ^end ^max
19 */
20 var start = state.bMarks[line] + state.sCount[line],
21 head = state.bMarks[line] + state.blkIndent,
22 end = state.skipSpacesBack(state.eMarks[line], head),
23 bounds = [], pos, posjump,
24 escape = false, code = false;
25
26 /* Scan for valid pipe character position */
27 for (pos = start; pos < end; pos++) {
28 switch (state.src.charCodeAt(pos)) {
29 case 0x5c /* \ */:
30 escape = true; break;
31 case 0x60 /* ` */:
32 posjump = state.skipChars(pos, 0x60) - 1;
33 /* make \` closes the code sequence, but not open it;
34 the reason is that `\` is correct code block */
35 /* eslint-disable-next-line brace-style */
36 if (posjump > pos) { pos = posjump; }
37 else if (code || !escape) { code = !code; }
38 escape = false; break;
39 case 0x7c /* | */:
40 if (!code && !escape) { bounds.push(pos); }
41 escape = false; break;
42 default:
43 escape = false; break;
44 }
45 }
46 if (bounds.length === 0) return bounds;
47
48 /* Pad in newline characters on last and this line */
49 if (bounds[0] > head) { bounds.unshift(head - 1); }
50 if (bounds[bounds.length - 1] < end - 1) { bounds.push(end); }
51
52 return bounds;
53 }
54
55 function table_caption(state, silent, line) {
56 var meta = { text: null, label: null },
57 start = state.bMarks[line] + state.sCount[line],
58 max = state.eMarks[line],
59 capRE = /^\[([^\[\]]+)\](\[([^\[\]]+)\])?\s*$/,
60 matches = state.src.slice(start, max).match(capRE);
61
62 if (!matches) { return false; }
63 if (silent) { return true; }
64 // TODO eliminate capRE by simple checking
65
66 meta.text = matches[1];
67 meta.label = matches[2] || matches[1];
68 meta.label = meta.label.toLowerCase().replace(/\W+/g, '');
69
70 return meta;
71 }
72
73 function table_row(state, silent, line) {
74 var meta = { bounds: null, multiline: null },
75 bounds = scan_bound_indices(state, line),
76 start, pos, oldMax;
77
78 if (bounds.length < 2) { return false; }
79 if (silent) { return true; }
80
81 meta.bounds = bounds;
82
83 /* Multiline. Scan boundaries again since it's very complicated */
84 if (options.multiline) {
85 start = state.bMarks[line] + state.sCount[line];
86 pos = state.eMarks[line] - 1; /* where backslash should be */
87 meta.multiline = (state.src.charCodeAt(pos) === 0x5C/* \ */);
88 if (meta.multiline) {
89 oldMax = state.eMarks[line];
90 state.eMarks[line] = state.skipSpacesBack(pos, start);
91 meta.bounds = scan_bound_indices(state, line);
92 state.eMarks[line] = oldMax;
93 }
94 }
95
96 return meta;
97 }
98
99 function table_separator(state, silent, line) {
100 var meta = { aligns: [], wraps: [] },
101 bounds = scan_bound_indices(state, line),
102 sepRE = /^:?(-+|=+):?\+?$/,
103 c, text, align;
104
105 /* Only separator needs to check indents */
106 if (state.sCount[line] - state.blkIndent >= 4) { return false; }
107 if (bounds.length === 0) { return false; }
108
109 for (c = 0; c < bounds.length - 1; c++) {
110 text = state.src.slice(bounds[c] + 1, bounds[c + 1]).trim();
111 if (!sepRE.test(text)) { return false; }
112
113 meta.wraps.push(text.charCodeAt(text.length - 1) === 0x2B/* + */);
114 align = ((text.charCodeAt(0) === 0x3A/* : */) << 4) |
115 (text.charCodeAt(text.length - 1 - meta.wraps[c]) === 0x3A);
116 switch (align) {
117 case 0x00: meta.aligns.push(''); break;
118 case 0x01: meta.aligns.push('right'); break;
119 case 0x10: meta.aligns.push('left'); break;
120 case 0x11: meta.aligns.push('center'); break;
121 }
122 }
123 if (silent) { return true; }
124 return meta;
125 }
126
127 function table_empty(state, silent, line) {
128 return state.isEmpty(line);
129 }
130
131 function table(state, startLine, endLine, silent) {
132 /**
133 * Regex pseudo code for table:
134 * caption? header+ separator (data+ empty)* data+ caption?
135 *
136 * We use DFA to emulate this plugin. Types with lower precedence are
137 * set-minus from all the formers. Noted that separator should have higher
138 * precedence than header or data.
139 * | state | caption separator header data empty | --> lower precedence
140 * | 0x10100 | 1 0 1 0 0 |
141 */
142 var tableDFA = new DFA(),
143 grp = 0x10, mtr = -1,
144 token, tableToken, trToken,
145 colspan, leftToken,
146 rowspan, upTokens = [],
147 tableLines, tgroupLines,
148 tag, text, range, r, c, b;
149
150 if (startLine + 2 > endLine) { return false; }
151
152 /**
153 * First pass: validate and collect info into table token. IR is stored in
154 * markdown-it `token.meta` to be pushed later. table/tr open tokens are
155 * generated here.
156 */
157 tableToken = new state.Token('table_open', 'table', 1);
158 tableToken.meta = { sep: null, cap: null, tr: [] };
159
160 tableDFA.set_highest_alphabet(0x10000);
161 tableDFA.set_initial_state(0x10100);
162 tableDFA.set_accept_states([ 0x10010, 0x10011, 0x00000 ]);
163 tableDFA.set_match_alphabets({
164 0x10000: table_caption.bind(this, state, true),
165 0x01000: table_separator.bind(this, state, true),
166 0x00100: table_row.bind(this, state, true),
167 0x00010: table_row.bind(this, state, true),
168 0x00001: table_empty.bind(this, state, true)
169 });
170 tableDFA.set_transitions({
171 0x10100: { 0x10000: 0x00100, 0x00100: 0x01100 },
172 0x00100: { 0x00100: 0x01100 },
173 0x01100: { 0x01000: 0x10010, 0x00100: 0x01100 },
174 0x10010: { 0x10000: 0x00000, 0x00010: 0x10011 },
175 0x10011: { 0x10000: 0x00000, 0x00010: 0x10011, 0x00001: 0x10010 }
176 });
177 if (options.headerless) {
178 tableDFA.set_initial_state(0x11100);
179 tableDFA.update_transition(0x11100,
180 { 0x10000: 0x01100, 0x01000: 0x10010, 0x00100: 0x01100 }
181 );
182 trToken = new state.Token('table_fake_header_row', 'tr', 1);
183 trToken.meta = Object(); // avoid trToken.meta.grp throws exception
184 }
185 if (!options.multibody) {
186 tableDFA.update_transition(0x10010,
187 { 0x10000: 0x00000, 0x00010: 0x10010 } // 0x10011 is never reached
188 );
189 }
190 /* Don't mix up DFA `_state` and markdown-it `state` */
191 tableDFA.set_actions(function (_line, _state, _type) {
192 // console.log(_line, _state.toString(16), _type.toString(16)) // for test
193 switch (_type) {
194 case 0x10000:
195 if (tableToken.meta.cap) { break; }
196 tableToken.meta.cap = table_caption(state, false, _line);
197 tableToken.meta.cap.map = [ _line, _line + 1 ];
198 tableToken.meta.cap.first = (_line === startLine);
199 break;
200 case 0x01000:
201 tableToken.meta.sep = table_separator(state, false, _line);
202 tableToken.meta.sep.map = [ _line, _line + 1 ];
203 trToken.meta.grp |= 0x01; // previously assigned at case 0x00110
204 grp = 0x10;
205 break;
206 case 0x00100:
207 case 0x00010:
208 trToken = new state.Token('tr_open', 'tr', 1);
209 trToken.map = [ _line, _line + 1 ];
210 trToken.meta = table_row(state, false, _line);
211 trToken.meta.type = _type;
212 trToken.meta.grp = grp;
213 grp = 0x00;
214 tableToken.meta.tr.push(trToken);
215 /* Multiline. Merge trTokens as an entire multiline trToken */
216 if (options.multiline) {
217 if (trToken.meta.multiline && mtr < 0) {
218 /* Start line of multiline row. mark this trToken */
219 mtr = tableToken.meta.tr.length - 1;
220 } else if (!trToken.meta.multiline && mtr >= 0) {
221 /* End line of multiline row. merge forward until the marked trToken */
222 token = tableToken.meta.tr[mtr];
223 token.meta.mbounds = tableToken.meta.tr
224 .slice(mtr).map(function (tk) { return tk.meta.bounds; });
225 token.map[1] = trToken.map[1];
226 tableToken.meta.tr = tableToken.meta.tr.slice(0, mtr + 1);
227 mtr = -1;
228 }
229 }
230 break;
231 case 0x00001:
232 trToken.meta.grp |= 0x01;
233 grp = 0x10;
234 break;
235 }
236 });
237
238 if (tableDFA.execute(startLine, endLine) === false) { return false; }
239 // if (!tableToken.meta.sep) { return false; } // always evaluated true
240 if (!tableToken.meta.tr.length) { return false; } // false under headerless corner case
241 if (silent) { return true; }
242
243 /* Last data row cannot be detected. not stored to trToken outside? */
244 tableToken.meta.tr[tableToken.meta.tr.length - 1].meta.grp |= 0x01;
245
246
247 /**
248 * Second pass: actually push the tokens into `state.tokens`.
249 * thead/tbody/th/td open tokens and all closed tokens are generated here;
250 * thead/tbody are generally called tgroup; td/th are generally called tcol.
251 */
252 tableToken.map = tableLines = [ startLine, 0 ];
253 tableToken.block = true;
254 tableToken.level = state.level++;
255 state.tokens.push(tableToken);
256
257 if (tableToken.meta.cap) {
258 token = state.push('caption_open', 'caption', 1);
259 token.map = tableToken.meta.cap.map;
260 token.attrs = [ [ 'id', tableToken.meta.cap.label ] ];
261
262 token = state.push('inline', '', 0);
263 token.content = tableToken.meta.cap.text;
264 token.map = tableToken.meta.cap.map;
265 token.children = [];
266
267 token = state.push('caption_close', 'caption', -1);
268 }
269
270 for (r = 0; r < tableToken.meta.tr.length; r++) {
271 leftToken = new state.Token('table_fake_tcol_open', '', 1);
272
273 /* Push in thead/tbody and tr open tokens */
274 trToken = tableToken.meta.tr[r];
275 // console.log(trToken.meta); // for test
276 if (trToken.meta.grp & 0x10) {
277 tag = (trToken.meta.type === 0x00100) ? 'thead' : 'tbody';
278 token = state.push(tag + '_open', tag, 1);
279 token.map = tgroupLines = [ trToken.map[0], 0 ]; // array ref
280 upTokens = [];
281 }
282 trToken.block = true;
283 trToken.level = state.level++;
284 state.tokens.push(trToken);
285
286 /* Push in th/td tokens */
287 for (c = 0; c < trToken.meta.bounds.length - 1; c++) {
288 range = [ trToken.meta.bounds[c] + 1, trToken.meta.bounds[c + 1] ];
289 text = state.src.slice.apply(state.src, range);
290
291 if (text === '') {
292 colspan = leftToken.attrGet('colspan');
293 leftToken.attrSet('colspan', colspan === null ? 2 : colspan + 1);
294 continue;
295 }
296 if (options.rowspan && upTokens[c] && text.trim() === '^^') {
297 rowspan = upTokens[c].attrGet('rowspan');
298 upTokens[c].attrSet('rowspan', rowspan === null ? 2 : rowspan + 1);
299 continue;
300 }
301
302 tag = (trToken.meta.type === 0x00100) ? 'th' : 'td';
303 token = state.push(tag + '_open', tag, 1);
304 token.map = trToken.map;
305 token.attrs = [];
306 if (tableToken.meta.sep.aligns[c]) {
307 token.attrs.push([ 'style', 'text-align:' + tableToken.meta.sep.aligns[c] ]);
308 }
309 if (tableToken.meta.sep.wraps[c]) {
310 token.attrs.push([ 'class', 'extend' ]);
311 }
312 leftToken = upTokens[c] = token;
313
314 /* Multiline. Join the text and feed into markdown-it blockParser. */
315 if (options.multiline && trToken.meta.multiline && trToken.meta.mbounds) {
316 text = [ text.trimRight() ];
317 for (b = 1; b < trToken.meta.mbounds.length; b++) {
318 /* Line with N bounds has cells indexed from 0 to N-2 */
319 if (c > trToken.meta.mbounds[b].length - 2) { continue; }
320 range = [ trToken.meta.mbounds[b][c] + 1, trToken.meta.mbounds[b][c + 1] ];
321 text.push(state.src.slice.apply(state.src, range).trimRight());
322 }
323 state.md.block.parse(text.join('\n'), state.md, state.env, state.tokens);
324 } else {
325 token = state.push('inline', '', 0);
326 token.content = text.trim();
327 token.map = trToken.map;
328 token.children = [];
329 }
330
331 token = state.push(tag + '_close', tag, -1);
332 }
333
334 /* Push in tr and thead/tbody closed tokens */
335 state.push('tr_close', 'tr', -1);
336 if (trToken.meta.grp & 0x01) {
337 tag = (trToken.meta.type === 0x00100) ? 'thead' : 'tbody';
338 token = state.push(tag + '_close', tag, -1);
339 tgroupLines[1] = trToken.map[1];
340 }
341 }
342
343 tableLines[1] = Math.max(
344 tgroupLines[1],
345 tableToken.meta.sep.map[1],
346 tableToken.meta.cap ? tableToken.meta.cap.map[1] : -1
347 );
348 token = state.push('table_close', 'table', -1);
349
350 state.line = tableLines[1];
351 return true;
352 }
353
354 md.block.ruler.at('table', table, { alt: [ 'paragraph', 'reference' ] });
355};
356
357/* vim: set ts=2 sw=2 et: */