1
1
import StateBlock from 'markdown-it/lib/rules_block/state_block' ;
2
2
import { MarkdownItPluginCb } from '../typings' ;
3
+ import Token from 'markdown-it/lib/token' ;
3
4
4
5
const pluginName = 'yfm_table' ;
5
6
const pipeChar = 0x7c ; // |
@@ -91,12 +92,17 @@ class StateIterator {
91
92
}
92
93
}
93
94
94
- function getTableRows (
95
+ interface RowPositions {
96
+ rows : [ number , number , [ Stats , Stats ] [ ] ] [ ] ;
97
+ endOfTable : number | null ;
98
+ }
99
+
100
+ function getTableRowPositions (
95
101
state : StateBlock ,
96
102
startPosition : number ,
97
103
endPosition : number ,
98
104
startLine : number ,
99
- ) {
105
+ ) : RowPositions {
100
106
let endOfTable = null ;
101
107
let tableLevel = 0 ;
102
108
let currentRow : [ Stats , Stats ] [ ] = [ ] ;
@@ -210,6 +216,144 @@ function getTableRows(
210
216
return { rows, endOfTable} ;
211
217
}
212
218
219
+ /**
220
+ * Removes the specified attribute from attributes in the content of a token.
221
+ *
222
+ * @param {Token } contentToken - The target token.
223
+ * @param {string } attr - The attribute to be removed from the token content.
224
+ *
225
+ * @return {void }
226
+ */
227
+ function removeAttrFromTokenContent ( contentToken : Token , attr : string ) : void {
228
+ // Replace the attribute in the token content with an empty string.
229
+ const blockRegex = / \s * \{ [ ^ } ] * } / ;
230
+ const allAttrs = contentToken . content . match ( blockRegex ) ;
231
+ if ( ! allAttrs ) {
232
+ return ;
233
+ }
234
+ let replacedContent = allAttrs [ 0 ] . replace ( `.${ attr } ` , '' ) ;
235
+ if ( replacedContent . trim ( ) === '{}' ) {
236
+ replacedContent = '' ;
237
+ }
238
+ contentToken . content = contentToken . content . replace ( allAttrs [ 0 ] , replacedContent ) ;
239
+ }
240
+
241
+ /**
242
+ * Extracts the class attribute from the given content token and applies it to the tdOpenToken.
243
+ * Preserves other attributes.
244
+ *
245
+ * @param {Token } contentToken - Search the content of this token for the class.
246
+ * @param {Token } tdOpenToken - Parent td_open token. Extracted class is applied to this token.
247
+ * @returns {void }
248
+ */
249
+ function extractAndApplyClassFromToken ( contentToken : Token , tdOpenToken : Token ) : void {
250
+ // Regex to find class attribute in any position within brackets
251
+ const classAttrRegex = / (?< = \{ [ ^ } ] * ) \. ( [ - _ a - z A - Z 0 - 9 ] + ) / g;
252
+ const classAttrMatch = classAttrRegex . exec ( contentToken . content ) ;
253
+ if ( classAttrMatch ) {
254
+ const classAttr = classAttrMatch [ 1 ] ;
255
+ tdOpenToken . attrSet ( 'class' , classAttr ) ;
256
+ removeAttrFromTokenContent ( contentToken , classAttr ) ;
257
+ }
258
+ }
259
+
260
+ const COLSPAN_SYMBOL = '>' ;
261
+ const ROWSPAN_SYMBOL = '^' ;
262
+
263
+ /**
264
+ * Traverses through the content map, applying row/colspan attributes and marking the special cells for deletion.
265
+ * Upon encountering a symbol denoting a row span or a column span, proceed backwards in row or column
266
+ * until text cell is found. Upon finding the text cell, store the colspan or rowspan value.
267
+ * During the backward traversal, if the same symbol is encountered, increment the value of rowspan/colspan.
268
+ * Colspan symbol is ignored for the first column. Rowspan symbol is ignored for the first row
269
+ *
270
+ * @param contentMap string[][]
271
+ * @param tokenMap Token[][]
272
+ * @return {void }
273
+ */
274
+ const applySpans = ( contentMap : string [ ] [ ] , tokenMap : Token [ ] [ ] ) : void => {
275
+ for ( let i = 0 ; i < contentMap . length ; i ++ ) {
276
+ for ( let j = 0 ; j < contentMap [ 0 ] . length ; j ++ ) {
277
+ if ( contentMap [ i ] [ j ] === COLSPAN_SYMBOL ) {
278
+ // skip the first column
279
+ if ( j === 0 ) {
280
+ continue ;
281
+ }
282
+ tokenMap [ i ] [ j ] . meta = { markForDeletion : true } ;
283
+ let colspanFactor = 2 ;
284
+ // traverse columns backwards
285
+ for ( let col = j - 1 ; col >= 0 ; col -- ) {
286
+ if ( contentMap [ i ] [ col ] === COLSPAN_SYMBOL ) {
287
+ colspanFactor ++ ;
288
+ tokenMap [ i ] [ col ] . meta = { markForDeletion : true } ;
289
+ } else if ( contentMap [ i ] [ col ] === ROWSPAN_SYMBOL ) {
290
+ // Do nothing, this should be applied on the row that's being extended
291
+ break ;
292
+ } else {
293
+ tokenMap [ i ] [ col ] . attrSet ( 'colspan' , colspanFactor . toString ( ) ) ;
294
+ break ;
295
+ }
296
+ }
297
+ }
298
+
299
+ if ( contentMap [ i ] [ j ] === ROWSPAN_SYMBOL ) {
300
+ // skip the first row
301
+ if ( i === 0 ) {
302
+ continue ;
303
+ }
304
+ tokenMap [ i ] [ j ] . meta = { markForDeletion : true } ;
305
+ let rowSpanFactor = 2 ;
306
+ // traverse rows upward
307
+ for ( let row = i - 1 ; row >= 0 ; row -- ) {
308
+ if ( contentMap [ row ] [ j ] === ROWSPAN_SYMBOL ) {
309
+ rowSpanFactor ++ ;
310
+ tokenMap [ row ] [ j ] . meta = { markForDeletion : true } ;
311
+ } else if ( contentMap [ row ] [ j ] === COLSPAN_SYMBOL ) {
312
+ break ;
313
+ } else {
314
+ tokenMap [ row ] [ j ] . attrSet ( 'rowspan' , rowSpanFactor . toString ( ) ) ;
315
+ break ;
316
+ }
317
+ }
318
+ }
319
+ }
320
+ }
321
+ } ;
322
+
323
+ /**
324
+ * Removes td_open and matching td_close tokens and the content within them
325
+ *
326
+ * @param {number } tableStart - The index of the start of the table in the state tokens array.
327
+ * @param {Token[] } tokens - The array of tokens from state.
328
+ * @returns {void }
329
+ */
330
+ const clearTokens = ( tableStart : number , tokens : Token [ ] ) : void => {
331
+ // use splices array to avoid modifying the tokens array during iteration
332
+ const splices : number [ ] [ ] = [ ] ;
333
+ for ( let i = tableStart ; i < tokens . length ; i ++ ) {
334
+ if ( tokens [ i ] . meta ?. markForDeletion ) {
335
+ // Use unshift instead of push so that the splices indexes are in reverse order.
336
+ // Reverse order guarantees that we don't mess up the indexes while removing the items.
337
+ splices . unshift ( [ i ] ) ;
338
+ const level = tokens [ i ] . level ;
339
+ // find matching td_close with the same level
340
+ for ( let j = i + 1 ; j < tokens . length ; j ++ ) {
341
+ if ( tokens [ j ] . type === 'yfm_td_close' && tokens [ j ] . level === level ) {
342
+ splices [ 0 ] . push ( j ) ;
343
+ break ;
344
+ }
345
+ }
346
+ }
347
+ }
348
+ splices . forEach ( ( [ start , end ] ) => {
349
+ // check that we have both start and end defined
350
+ // it's possible we didn't find td_close index
351
+ if ( start && end ) {
352
+ tokens . splice ( start , end - start + 1 ) ;
353
+ }
354
+ } ) ;
355
+ } ;
356
+
213
357
const yfmTable : MarkdownItPluginCb = ( md ) => {
214
358
md . block . ruler . before (
215
359
'code' ,
@@ -232,7 +376,12 @@ const yfmTable: MarkdownItPluginCb = (md) => {
232
376
return true ;
233
377
}
234
378
235
- const { rows, endOfTable} = getTableRows ( state , startPosition , endPosition , startLine ) ;
379
+ const { rows, endOfTable} = getTableRowPositions (
380
+ state ,
381
+ startPosition ,
382
+ endPosition ,
383
+ startLine ,
384
+ ) ;
236
385
237
386
if ( ! endOfTable ) {
238
387
token = state . push ( '__yfm_lint' , '' , 0 ) ;
@@ -247,6 +396,7 @@ const yfmTable: MarkdownItPluginCb = (md) => {
247
396
state . lineMax = endOfTable ;
248
397
state . line = startLine ;
249
398
399
+ const tableStart = state . tokens . length ;
250
400
token = state . push ( 'yfm_table_open' , 'table' , 1 ) ;
251
401
token . map = [ startLine , endOfTable ] ;
252
402
@@ -255,9 +405,18 @@ const yfmTable: MarkdownItPluginCb = (md) => {
255
405
256
406
const maxRowLength = Math . max ( ...rows . map ( ( [ , , cols ] ) => cols . length ) ) ;
257
407
408
+ // cellsMaps is a 2-D map of all td_open tokens in the table.
409
+ // cellsMap is used to access the table cells by [row][column] coordinates
410
+ const cellsMap : Token [ ] [ ] = [ ] ;
411
+
412
+ // contentMap is a 2-D map of the text content within cells in the table.
413
+ // To apply spans, traverse the contentMap and modify the cells from cellsMap
414
+ const contentMap : string [ ] [ ] = [ ] ;
415
+
258
416
for ( let i = 0 ; i < rows . length ; i ++ ) {
259
417
const [ rowLineStarts , rowLineEnds , cols ] = rows [ i ] ;
260
-
418
+ cellsMap . push ( [ ] ) ;
419
+ contentMap . push ( [ ] ) ;
261
420
const rowLength = cols . length ;
262
421
263
422
token = state . push ( 'yfm_tr_open' , 'tr' , 1 ) ;
@@ -266,6 +425,7 @@ const yfmTable: MarkdownItPluginCb = (md) => {
266
425
for ( let j = 0 ; j < cols . length ; j ++ ) {
267
426
const [ begin , end ] = cols [ j ] ;
268
427
token = state . push ( 'yfm_td_open' , 'td' , 1 ) ;
428
+ cellsMap [ i ] . push ( token ) ;
269
429
token . map = [ begin . line , end . line ] ;
270
430
271
431
const oldTshift = state . tShift [ begin . line ] ;
@@ -279,14 +439,23 @@ const yfmTable: MarkdownItPluginCb = (md) => {
279
439
state . lineMax = end . line + 1 ;
280
440
281
441
state . md . block . tokenize ( state , begin . line , end . line + 1 ) ;
442
+ const contentToken = state . tokens [ state . tokens . length - 2 ] ;
443
+
444
+ // In case of ">" within a cell without whitespace it gets consumed as a blockquote.
445
+ // To handle that, check markup as well
446
+ const content = contentToken . content . trim ( ) || contentToken . markup . trim ( ) ;
447
+ contentMap [ i ] . push ( content ) ;
448
+
449
+ token = state . push ( 'yfm_td_close' , 'td' , - 1 ) ;
450
+ state . tokens [ state . tokens . length - 1 ] . map = [ end . line , end . line + 1 ] ;
282
451
283
452
state . lineMax = oldLineMax ;
284
453
state . tShift [ begin . line ] = oldTshift ;
285
454
state . bMarks [ begin . line ] = oldBMark ;
286
455
state . eMarks [ end . line ] = oldEMark ;
287
456
288
- token = state . push ( 'yfm_td_close' , 'td' , - 1 ) ;
289
- state . tokens [ state . tokens . length - 1 ] . map = [ end . line , end . line + 1 ] ;
457
+ const rowTokens = cellsMap [ cellsMap . length - 1 ] ;
458
+ extractAndApplyClassFromToken ( contentToken , rowTokens [ rowTokens . length - 1 ] ) ;
290
459
}
291
460
292
461
if ( rowLength < maxRowLength ) {
@@ -300,6 +469,9 @@ const yfmTable: MarkdownItPluginCb = (md) => {
300
469
token = state . push ( 'yfm_tr_close' , 'tr' , - 1 ) ;
301
470
}
302
471
472
+ applySpans ( contentMap , cellsMap ) ;
473
+ clearTokens ( tableStart , state . tokens ) ;
474
+
303
475
token = state . push ( 'yfm_tbody_close' , 'tbody' , - 1 ) ;
304
476
305
477
token = state . push ( 'yfm_table_close' , 'table' , - 1 ) ;
0 commit comments