svelte-streamdown 1.0.8 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +155 -140
- package/dist/Block.svelte +17 -41
- package/dist/Elements/Alert.svelte +1 -2
- package/dist/Elements/Code.svelte +3 -5
- package/dist/Elements/Code.svelte.d.ts +0 -2
- package/dist/Elements/Element.svelte +170 -42
- package/dist/Elements/FootnoteRef.svelte +126 -0
- package/dist/Elements/FootnoteRef.svelte.d.ts +7 -0
- package/dist/Elements/Image.svelte +4 -4
- package/dist/Elements/{A.svelte → Link.svelte} +4 -4
- package/dist/Elements/{A.svelte.d.ts → Link.svelte.d.ts} +3 -3
- package/dist/Elements/Math.svelte +13 -24
- package/dist/Elements/Math.svelte.d.ts +0 -2
- package/dist/Elements/Mermaid.svelte +105 -115
- package/dist/Elements/Mermaid.svelte.d.ts +0 -2
- package/dist/Elements/Table.svelte +1 -1
- package/dist/Elements/index.d.ts +5 -24
- package/dist/Elements/index.js +5 -24
- package/dist/Streamdown.d.ts +52 -10
- package/dist/Streamdown.js +19 -1
- package/dist/Streamdown.svelte +2 -34
- package/dist/Streamdown.svelte.d.ts +1 -14
- package/dist/marked/index.d.ts +6 -10
- package/dist/marked/index.js +29 -17
- package/dist/marked/marked-alert.d.ts +2 -2
- package/dist/marked/marked-alert.js +11 -24
- package/dist/marked/marked-footnotes.d.ts +1 -0
- package/dist/marked/marked-footnotes.js +27 -8
- package/dist/marked/marked-list.d.ts +36 -0
- package/dist/marked/marked-list.js +232 -0
- package/dist/marked/marked-math.js +1 -4
- package/dist/marked/marked-subsup.d.ts +28 -0
- package/dist/marked/marked-subsup.js +45 -0
- package/dist/marked/marked-table.d.ts +59 -0
- package/dist/marked/marked-table.js +495 -0
- package/dist/theme.d.ts +54 -30
- package/dist/theme.js +51 -35
- package/dist/utils/panzoom.svelte.d.ts +17 -18
- package/dist/utils/panzoom.svelte.js +0 -2
- package/dist/utils/parse-incomplete-markdown.js +157 -0
- package/dist/utils/useClickOutside.svelte.d.ts +8 -0
- package/dist/utils/useClickOutside.svelte.js +41 -0
- package/dist/utils/useKeyDown.svelte.d.ts +5 -0
- package/dist/utils/useKeyDown.svelte.js +23 -0
- package/package.json +2 -1
- package/dist/Elements/Blockquote.svelte +0 -28
- package/dist/Elements/Blockquote.svelte.d.ts +0 -9
- package/dist/Elements/Del.svelte +0 -28
- package/dist/Elements/Del.svelte.d.ts +0 -9
- package/dist/Elements/Em.svelte +0 -28
- package/dist/Elements/Em.svelte.d.ts +0 -9
- package/dist/Elements/Heading.svelte +0 -25
- package/dist/Elements/Heading.svelte.d.ts +0 -9
- package/dist/Elements/Hr.svelte +0 -26
- package/dist/Elements/Hr.svelte.d.ts +0 -9
- package/dist/Elements/InlineCode.svelte +0 -28
- package/dist/Elements/InlineCode.svelte.d.ts +0 -9
- package/dist/Elements/Li.svelte +0 -28
- package/dist/Elements/Li.svelte.d.ts +0 -9
- package/dist/Elements/Ol.svelte +0 -30
- package/dist/Elements/Ol.svelte.d.ts +0 -9
- package/dist/Elements/P.svelte +0 -28
- package/dist/Elements/P.svelte.d.ts +0 -9
- package/dist/Elements/Strong.svelte +0 -28
- package/dist/Elements/Strong.svelte.d.ts +0 -9
- package/dist/Elements/Sub.svelte +0 -27
- package/dist/Elements/Sub.svelte.d.ts +0 -8
- package/dist/Elements/Sup.svelte +0 -28
- package/dist/Elements/Sup.svelte.d.ts +0 -8
- package/dist/Elements/Tbody.svelte +0 -24
- package/dist/Elements/Tbody.svelte.d.ts +0 -7
- package/dist/Elements/Td.svelte +0 -28
- package/dist/Elements/Td.svelte.d.ts +0 -9
- package/dist/Elements/Th.svelte +0 -28
- package/dist/Elements/Th.svelte.d.ts +0 -9
- package/dist/Elements/Thead.svelte +0 -27
- package/dist/Elements/Thead.svelte.d.ts +0 -9
- package/dist/Elements/Tr.svelte +0 -27
- package/dist/Elements/Tr.svelte.d.ts +0 -9
- package/dist/Elements/Ul.svelte +0 -28
- package/dist/Elements/Ul.svelte.d.ts +0 -9
- /package/dist/{hightlighter.svelte.d.ts → utils/hightlighter.svelte.d.ts} +0 -0
- /package/dist/{hightlighter.svelte.js → utils/hightlighter.svelte.js} +0 -0
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
// Default configuration options for the extended tables extension
|
|
2
|
+
export const DEFAULT_OPTIONS = {
|
|
3
|
+
useTheadTbody: true,
|
|
4
|
+
useTfoot: false,
|
|
5
|
+
detectFooter: true,
|
|
6
|
+
maxColspan: null,
|
|
7
|
+
handleComplexSpans: true
|
|
8
|
+
};
|
|
9
|
+
// Creates an HTML table cell with appropriate attributes
|
|
10
|
+
export const getTableCell = (text, cell, type, align) => {
|
|
11
|
+
if (!cell.rowspan)
|
|
12
|
+
return '';
|
|
13
|
+
const tag = `<${type}` +
|
|
14
|
+
`${cell.colspan > 1 ? ` colspan=${cell.colspan}` : ''}` +
|
|
15
|
+
`${cell.rowspan > 1 ? ` rowspan=${cell.rowspan}` : ''}` +
|
|
16
|
+
`${align ? ` align=${align}` : ''}>`;
|
|
17
|
+
return `${tag + text}</${type}>\n`;
|
|
18
|
+
};
|
|
19
|
+
function splitRow(src) {
|
|
20
|
+
const out = [];
|
|
21
|
+
let buf = '';
|
|
22
|
+
let esc = false;
|
|
23
|
+
let inCode = false;
|
|
24
|
+
let fence = 0;
|
|
25
|
+
for (let i = 0; i < src.length; i++) {
|
|
26
|
+
const ch = src[i];
|
|
27
|
+
if (esc) {
|
|
28
|
+
buf += ch;
|
|
29
|
+
esc = false;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (ch === '\\') {
|
|
33
|
+
esc = true;
|
|
34
|
+
buf += ch;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (ch === '`') {
|
|
38
|
+
// count backticks
|
|
39
|
+
let run = 1;
|
|
40
|
+
while (i + run < src.length && src[i + run] === '`')
|
|
41
|
+
run++;
|
|
42
|
+
if (!inCode) {
|
|
43
|
+
inCode = true;
|
|
44
|
+
fence = run;
|
|
45
|
+
}
|
|
46
|
+
else if (run >= fence) {
|
|
47
|
+
inCode = false;
|
|
48
|
+
fence = 0;
|
|
49
|
+
}
|
|
50
|
+
buf += src.slice(i, i + run);
|
|
51
|
+
i += run - 1;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (ch === '|' && !inCode) {
|
|
55
|
+
out.push(buf.trim());
|
|
56
|
+
buf = '';
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
buf += ch;
|
|
60
|
+
}
|
|
61
|
+
out.push(buf.trim());
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
// Splits a table row into cells and processes row/column spans
|
|
65
|
+
export const splitCells = (tableRow, count, prevRow = null, maxColspan = null) => {
|
|
66
|
+
// Split by pipe, but handle escaped pipes and empty cells
|
|
67
|
+
const cells = splitRow(tableRow);
|
|
68
|
+
// Remove first/last cell if it's empty (from leading/trailing pipes)
|
|
69
|
+
if (cells.length > 0 && !cells[0])
|
|
70
|
+
cells.shift();
|
|
71
|
+
if (cells.length > 0 && !cells[cells.length - 1])
|
|
72
|
+
cells.pop();
|
|
73
|
+
return processSpans(cells, count, prevRow || [], maxColspan);
|
|
74
|
+
};
|
|
75
|
+
// Process row and column spans in table cells
|
|
76
|
+
const processSpans = (cells, count, prevRow = [], maxColspan = null) => {
|
|
77
|
+
let numCols = 0;
|
|
78
|
+
let i, j, trimmedCell, prevCell;
|
|
79
|
+
const processedCells = [];
|
|
80
|
+
// Track colspan cells that need rowspan
|
|
81
|
+
const colspanCells = new Map();
|
|
82
|
+
// First pass: Process each cell's colspan and merge consecutive empty cells
|
|
83
|
+
let cellIndex = 0;
|
|
84
|
+
const mergedIndices = new Set();
|
|
85
|
+
for (i = 0; i < cells.length; i++) {
|
|
86
|
+
// Skip cells that were merged into previous colspans
|
|
87
|
+
if (mergedIndices.has(i))
|
|
88
|
+
continue;
|
|
89
|
+
trimmedCell = cells[i];
|
|
90
|
+
// Count consecutive empty cells for colspan
|
|
91
|
+
let colspan = 1;
|
|
92
|
+
if (!trimmedCell.trim()) {
|
|
93
|
+
// Count how many consecutive empty cells we have
|
|
94
|
+
let j = i + 1;
|
|
95
|
+
while (j < cells.length && !cells[j].trim()) {
|
|
96
|
+
colspan++;
|
|
97
|
+
mergedIndices.add(j); // Mark as merged
|
|
98
|
+
j++;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Apply maxColspan limit if specified
|
|
102
|
+
if (maxColspan !== null && colspan > maxColspan)
|
|
103
|
+
colspan = maxColspan;
|
|
104
|
+
processedCells[cellIndex] = {
|
|
105
|
+
rowspan: 1,
|
|
106
|
+
colspan: colspan,
|
|
107
|
+
text: trimmedCell.trim().replace(/\\\|/g, '|'),
|
|
108
|
+
position: numCols // Store original column position for better tracking
|
|
109
|
+
};
|
|
110
|
+
numCols += processedCells[cellIndex].colspan;
|
|
111
|
+
cellIndex++;
|
|
112
|
+
}
|
|
113
|
+
// Second pass: Process rowspan by matching cells by position
|
|
114
|
+
for (i = 0; i < processedCells.length; i++) {
|
|
115
|
+
const cell = processedCells[i];
|
|
116
|
+
let cellText = cell.text;
|
|
117
|
+
// Handle Rowspan - cells ending with ^
|
|
118
|
+
if (cellText.slice(-1) === '^' && prevRow.length > 0) {
|
|
119
|
+
// Clean the ^ indicator from the cell text
|
|
120
|
+
cell.text = cellText.slice(0, -1).trim();
|
|
121
|
+
cellText = cell.text;
|
|
122
|
+
let targetFound = false;
|
|
123
|
+
const startPosition = cell.position || 0;
|
|
124
|
+
const endPosition = startPosition + cell.colspan - 1;
|
|
125
|
+
// Try to find a matching cell or combination of cells in previous row
|
|
126
|
+
for (j = 0; j < prevRow.length; j++) {
|
|
127
|
+
prevCell = prevRow[j];
|
|
128
|
+
const prevStartPosition = prevCell.position || 0;
|
|
129
|
+
const prevEndPosition = prevStartPosition + prevCell.colspan - 1;
|
|
130
|
+
// Check for position overlap between cells
|
|
131
|
+
if ((startPosition >= prevStartPosition && startPosition <= prevEndPosition) ||
|
|
132
|
+
(endPosition >= prevStartPosition && endPosition <= prevEndPosition) ||
|
|
133
|
+
(prevStartPosition >= startPosition && prevEndPosition <= endPosition)) {
|
|
134
|
+
// Complex case: Handle rowspan for colspan cells
|
|
135
|
+
if (cell.colspan > 1 && prevCell.colspan > 1) {
|
|
136
|
+
// If the cell spans exactly match, simple case
|
|
137
|
+
if (cell.colspan === prevCell.colspan && cell.position === prevCell.position) {
|
|
138
|
+
cell.rowSpanTarget = prevCell.rowSpanTarget ?? prevCell;
|
|
139
|
+
// Only append text if it's different from the target cell
|
|
140
|
+
const textToAppend = cell.text.slice(0, -1).trim();
|
|
141
|
+
const targetText = cell.rowSpanTarget.text.trim();
|
|
142
|
+
// Don't append if the text is the same or already contained (common case for rowspan indicators)
|
|
143
|
+
if (textToAppend &&
|
|
144
|
+
textToAppend !== targetText &&
|
|
145
|
+
!targetText.includes(textToAppend)) {
|
|
146
|
+
cell.rowSpanTarget.text = targetText + (targetText ? ' ' : '') + textToAppend;
|
|
147
|
+
}
|
|
148
|
+
cell.rowSpanTarget.rowspan += 1;
|
|
149
|
+
cell.rowspan = 0;
|
|
150
|
+
targetFound = true;
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
// More complex case: Track colspan cells that need rowspan for next row
|
|
155
|
+
const key = `${cell.position}-${cell.colspan}`;
|
|
156
|
+
colspanCells.set(key, {
|
|
157
|
+
original: prevCell,
|
|
158
|
+
newCell: cell
|
|
159
|
+
});
|
|
160
|
+
// Keep the cell visible for now, will be merged in rendering
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
// Standard case of single column cell with rowspan
|
|
165
|
+
cell.rowSpanTarget = prevCell.rowSpanTarget ?? prevCell;
|
|
166
|
+
// Only append text if it's different from the target cell
|
|
167
|
+
const textToAppend = cell.text.slice(0, -1).trim();
|
|
168
|
+
const targetText = cell.rowSpanTarget.text.trim();
|
|
169
|
+
// Don't append if the text is the same or already contained (common case for rowspan indicators)
|
|
170
|
+
if (textToAppend && textToAppend !== targetText && !targetText.includes(textToAppend)) {
|
|
171
|
+
cell.rowSpanTarget.text = targetText + (targetText ? ' ' : '') + textToAppend;
|
|
172
|
+
}
|
|
173
|
+
cell.rowSpanTarget.rowspan += 1;
|
|
174
|
+
cell.rowspan = 0;
|
|
175
|
+
targetFound = true;
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// If no target was found but it's a rowspan cell, clean the ^ indicator
|
|
181
|
+
if (!targetFound && cell.rowspan > 0) {
|
|
182
|
+
cell.text = cell.text.slice(0, -1);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// Process any complex colspan+rowspan combinations we tracked
|
|
187
|
+
colspanCells.forEach((spanData) => {
|
|
188
|
+
const { original, newCell } = spanData;
|
|
189
|
+
if (original && newCell) {
|
|
190
|
+
// Here we could apply more sophisticated merging logic
|
|
191
|
+
// For now, just mark that these cells have a relationship
|
|
192
|
+
newCell.complexRowSpan = true;
|
|
193
|
+
newCell.relatedCell = original;
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
// Normalize column count
|
|
197
|
+
return normalizeColumnCount(processedCells, count, numCols);
|
|
198
|
+
};
|
|
199
|
+
// Ensures the row has the correct number of columns
|
|
200
|
+
const normalizeColumnCount = (cells, count, numCols) => {
|
|
201
|
+
// If count is null, don't normalize
|
|
202
|
+
if (count === null)
|
|
203
|
+
return cells;
|
|
204
|
+
if (numCols > count) {
|
|
205
|
+
// We need to keep track of total column count
|
|
206
|
+
let currentColCount = 0;
|
|
207
|
+
const cellsToKeep = [];
|
|
208
|
+
for (const cell of cells) {
|
|
209
|
+
if (currentColCount + cell.colspan <= count) {
|
|
210
|
+
// This cell fits completely
|
|
211
|
+
cellsToKeep.push(cell);
|
|
212
|
+
currentColCount += cell.colspan;
|
|
213
|
+
}
|
|
214
|
+
else if (currentColCount < count) {
|
|
215
|
+
// This cell partially fits - adjust its colspan
|
|
216
|
+
const adjustedCell = { ...cell };
|
|
217
|
+
adjustedCell.colspan = count - currentColCount;
|
|
218
|
+
cellsToKeep.push(adjustedCell);
|
|
219
|
+
currentColCount = count;
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
// This cell doesn't fit at all
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return cellsToKeep;
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
while (numCols < count) {
|
|
230
|
+
cells.push({
|
|
231
|
+
colspan: 1,
|
|
232
|
+
rowspan: 1,
|
|
233
|
+
text: '',
|
|
234
|
+
position: numCols
|
|
235
|
+
});
|
|
236
|
+
numCols += 1;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return cells;
|
|
240
|
+
};
|
|
241
|
+
// Process alignment indicators in table headers
|
|
242
|
+
function processAlignment(alignRow) {
|
|
243
|
+
const alignment = [];
|
|
244
|
+
for (let i = 0; i < alignRow.length; i++) {
|
|
245
|
+
if (/^ *-+: *$/.test(alignRow[i])) {
|
|
246
|
+
alignment[i] = 'right';
|
|
247
|
+
}
|
|
248
|
+
else if (/^ *:-+: *$/.test(alignRow[i])) {
|
|
249
|
+
alignment[i] = 'center';
|
|
250
|
+
}
|
|
251
|
+
else if (/^ *:-+ *$/.test(alignRow[i])) {
|
|
252
|
+
alignment[i] = 'left';
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
alignment[i] = null;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return alignment;
|
|
259
|
+
}
|
|
260
|
+
// Convert working cell to TH
|
|
261
|
+
function workingCellToTH(cell, align) {
|
|
262
|
+
return {
|
|
263
|
+
type: 'th',
|
|
264
|
+
rowspan: cell.rowspan,
|
|
265
|
+
colspan: cell.colspan,
|
|
266
|
+
text: cell.text,
|
|
267
|
+
position: cell.position,
|
|
268
|
+
tokens: cell.tokens,
|
|
269
|
+
rowSpanTarget: cell.rowSpanTarget,
|
|
270
|
+
complexRowSpan: cell.complexRowSpan,
|
|
271
|
+
relatedCell: cell.relatedCell,
|
|
272
|
+
align
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
// Convert working cell to TD
|
|
276
|
+
function workingCellToTD(cell, align) {
|
|
277
|
+
return {
|
|
278
|
+
type: 'td',
|
|
279
|
+
rowspan: cell.rowspan,
|
|
280
|
+
colspan: cell.colspan,
|
|
281
|
+
text: cell.text,
|
|
282
|
+
position: cell.position,
|
|
283
|
+
tokens: cell.tokens,
|
|
284
|
+
rowSpanTarget: cell.rowSpanTarget,
|
|
285
|
+
complexRowSpan: cell.complexRowSpan,
|
|
286
|
+
relatedCell: cell.relatedCell,
|
|
287
|
+
align
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
// Process table rows and add inline tokens to cells
|
|
291
|
+
function processRows(headerRows, bodyRows, alignment, colCount, lexer, maxColspan, detectFooter) {
|
|
292
|
+
const tokens = [];
|
|
293
|
+
// Process header rows
|
|
294
|
+
const processedHeaderRows = [];
|
|
295
|
+
for (let i = 0; i < headerRows.length; i++) {
|
|
296
|
+
const prevRow = i > 0 ? processedHeaderRows[i - 1] : null;
|
|
297
|
+
processedHeaderRows[i] = splitCells(headerRows[i], colCount, prevRow, maxColspan);
|
|
298
|
+
}
|
|
299
|
+
// Convert header rows to THead (only if we have header rows)
|
|
300
|
+
if (processedHeaderRows.length > 0) {
|
|
301
|
+
const theadRows = processedHeaderRows.map((row) => ({
|
|
302
|
+
type: 'tr',
|
|
303
|
+
tokens: row.map((cell) => {
|
|
304
|
+
// Use the cell's position to get the correct alignment
|
|
305
|
+
const cellAlignment = cell.position !== undefined ? alignment[cell.position] : null;
|
|
306
|
+
const th = workingCellToTH(cell, cellAlignment);
|
|
307
|
+
// Add inline tokens
|
|
308
|
+
th.tokens = lexer.inline(th.text, th.tokens);
|
|
309
|
+
return th;
|
|
310
|
+
})
|
|
311
|
+
}));
|
|
312
|
+
tokens.push({
|
|
313
|
+
type: 'thead',
|
|
314
|
+
tokens: theadRows
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
// Process body rows
|
|
318
|
+
if (bodyRows.length > 0) {
|
|
319
|
+
const processedBodyRows = [];
|
|
320
|
+
for (let i = 0; i < bodyRows.length; i++) {
|
|
321
|
+
const prevRow = i > 0 ? processedBodyRows[i - 1] : processedHeaderRows[processedHeaderRows.length - 1];
|
|
322
|
+
processedBodyRows[i] = splitCells(bodyRows[i], colCount, prevRow, maxColspan);
|
|
323
|
+
}
|
|
324
|
+
// Handle footer detection
|
|
325
|
+
let tbodyRows = processedBodyRows;
|
|
326
|
+
let tfootRows = [];
|
|
327
|
+
if (detectFooter && processedBodyRows.length > 0) {
|
|
328
|
+
const lastRowIndex = processedBodyRows.length - 1;
|
|
329
|
+
tfootRows = [processedBodyRows[lastRowIndex]];
|
|
330
|
+
tbodyRows = processedBodyRows.slice(0, lastRowIndex);
|
|
331
|
+
}
|
|
332
|
+
// Convert body rows to TBody if there are any
|
|
333
|
+
if (tbodyRows.length > 0) {
|
|
334
|
+
const tbodyRowTokens = tbodyRows.map((row) => ({
|
|
335
|
+
type: 'tr',
|
|
336
|
+
tokens: row.map((cell) => {
|
|
337
|
+
// Use the cell's position to get the correct alignment
|
|
338
|
+
const cellAlignment = cell.position !== undefined ? alignment[cell.position] : null;
|
|
339
|
+
const td = workingCellToTD(cell, cellAlignment);
|
|
340
|
+
// Add inline tokens
|
|
341
|
+
td.tokens = lexer.inline(td.text, td.tokens);
|
|
342
|
+
return td;
|
|
343
|
+
})
|
|
344
|
+
}));
|
|
345
|
+
tokens.push({
|
|
346
|
+
type: 'tbody',
|
|
347
|
+
tokens: tbodyRowTokens
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
// Convert footer rows to TFoot if there are any
|
|
351
|
+
if (tfootRows.length > 0) {
|
|
352
|
+
const tfootRowTokens = tfootRows.map((row) => ({
|
|
353
|
+
type: 'tr',
|
|
354
|
+
tokens: row.map((cell) => {
|
|
355
|
+
// Use the cell's position to get the correct alignment
|
|
356
|
+
const cellAlignment = cell.position !== undefined ? alignment[cell.position] : null;
|
|
357
|
+
const td = workingCellToTD(cell, cellAlignment);
|
|
358
|
+
// Add inline tokens
|
|
359
|
+
td.tokens = lexer.inline(td.text, td.tokens);
|
|
360
|
+
return td;
|
|
361
|
+
})
|
|
362
|
+
}));
|
|
363
|
+
tokens.push({
|
|
364
|
+
type: 'tfoot',
|
|
365
|
+
tokens: tfootRowTokens
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return tokens;
|
|
370
|
+
}
|
|
371
|
+
// Adds support for extended tables in marked with row spanning, column spanning,
|
|
372
|
+
// multi-row headers, and column alignment
|
|
373
|
+
export function markedTable(options = {}) {
|
|
374
|
+
const config = { ...DEFAULT_OPTIONS, ...options };
|
|
375
|
+
const { detectFooter, maxColspan } = config;
|
|
376
|
+
return {
|
|
377
|
+
extensions: [
|
|
378
|
+
{
|
|
379
|
+
name: 'table',
|
|
380
|
+
level: 'block',
|
|
381
|
+
start(src) {
|
|
382
|
+
// Check for table with potential header alignment
|
|
383
|
+
let match = src.match(/^\n *([^\n ].*\|.*)\n/);
|
|
384
|
+
if (match)
|
|
385
|
+
return match.index;
|
|
386
|
+
// Check for simple table without header alignment
|
|
387
|
+
match = src.match(/^\n *(\|.*\|)\n/);
|
|
388
|
+
if (match)
|
|
389
|
+
return match.index;
|
|
390
|
+
return undefined;
|
|
391
|
+
},
|
|
392
|
+
tokenizer(src) {
|
|
393
|
+
// Try to match table with header and alignment first
|
|
394
|
+
let regex = new RegExp('^' +
|
|
395
|
+
'([^\\n ].*\\|.*\\n(?: *[^\\s].*\\n)*?)' + // Header
|
|
396
|
+
' {0,3}(?:\\| *)?(:?-+:? *(?:\\| *:?-+:? *)*)(?:\\| *)?' + // Header Align
|
|
397
|
+
'(?:\\n((?:(?! *\\n| {0,3}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})' + // Body Cells
|
|
398
|
+
'(?:\\n+|$)| {0,3}#{1,6} | {0,3}>| {4}[^\\n]| {0,3}(?:`{3,}' +
|
|
399
|
+
'(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n| {0,3}(?:[*+-]|1[.)]) |' +
|
|
400
|
+
'<\\/?(?:address|article|aside|base|basefont|blockquote|body|' +
|
|
401
|
+
'caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul)(?: +|\\n|\\/?>)|<(?:script|pre|style|textarea|!--)).*(?:\\n|$))*)\\n*|$)');
|
|
402
|
+
let cap = regex.exec(src);
|
|
403
|
+
let hasHeaderAlignment = true;
|
|
404
|
+
// If no match with header alignment, try table without header alignment
|
|
405
|
+
if (!cap) {
|
|
406
|
+
// Simple regex for tables without header alignment
|
|
407
|
+
regex = /^(\|.*\|(?:\n\|.*\|)*)/;
|
|
408
|
+
cap = regex.exec(src);
|
|
409
|
+
hasHeaderAlignment = false;
|
|
410
|
+
}
|
|
411
|
+
if (!cap)
|
|
412
|
+
return null;
|
|
413
|
+
// Combine all captured groups to get complete table rows
|
|
414
|
+
let allTableContent = cap[1]; // Headers
|
|
415
|
+
if (cap[2])
|
|
416
|
+
allTableContent += '\n' + cap[2]; // Alignment row
|
|
417
|
+
if (cap[3])
|
|
418
|
+
allTableContent += '\n' + cap[3]; // Body rows
|
|
419
|
+
const allRows = allTableContent.replace(/\n$/, '').split('\n');
|
|
420
|
+
let headerRows = [];
|
|
421
|
+
let bodyRows = [];
|
|
422
|
+
let alignRow = [];
|
|
423
|
+
let alignment = [];
|
|
424
|
+
let colCount = 0;
|
|
425
|
+
if (hasHeaderAlignment) {
|
|
426
|
+
// Traditional table with header and alignment
|
|
427
|
+
// Parse all rows and identify which are headers vs body
|
|
428
|
+
let headerEndIndex = -1;
|
|
429
|
+
// Find the FIRST alignment row (contains dashes/underscores/asterisks)
|
|
430
|
+
for (let i = 0; i < allRows.length; i++) {
|
|
431
|
+
const row = allRows[i].trim();
|
|
432
|
+
const isAlignment = /^ *(\| *)?:?-+:? *(\| *:?-+:? *)*(\| *)?$/.test(row);
|
|
433
|
+
// Check if this row matches alignment pattern (contains only |, spaces, and alignment chars)
|
|
434
|
+
if (isAlignment) {
|
|
435
|
+
headerEndIndex = i;
|
|
436
|
+
alignRow = row.replace(/^ *\| *| *\| *$/g, '').split(/ *\| */);
|
|
437
|
+
break; // Stop at the first alignment row
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
if (headerEndIndex === -1) {
|
|
441
|
+
// No alignment row found, treat as simple table
|
|
442
|
+
bodyRows = allRows;
|
|
443
|
+
colCount = bodyRows[0].split('|').filter((cell) => cell.trim() !== '').length;
|
|
444
|
+
alignment = new Array(colCount).fill(null);
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
// Found alignment row, split headers and body
|
|
448
|
+
// Filter out empty rows and the alignment row itself from headers
|
|
449
|
+
headerRows = allRows.slice(0, headerEndIndex).filter((row) => row.trim() !== '');
|
|
450
|
+
bodyRows =
|
|
451
|
+
headerEndIndex + 1 < allRows.length ? allRows.slice(headerEndIndex + 1) : [];
|
|
452
|
+
// Use alignment row length as the authoritative column count
|
|
453
|
+
colCount = alignRow.length;
|
|
454
|
+
// Validate that we have a reasonable table structure
|
|
455
|
+
if (colCount === 0)
|
|
456
|
+
return null;
|
|
457
|
+
// Process alignment
|
|
458
|
+
alignment = processAlignment(alignRow);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
else {
|
|
462
|
+
// Table without header alignment - treat all rows as body rows
|
|
463
|
+
bodyRows = allRows;
|
|
464
|
+
const firstRowCells = bodyRows[0].split('|').filter((cell) => cell.trim() !== '');
|
|
465
|
+
colCount = firstRowCells.length;
|
|
466
|
+
alignment = new Array(colCount).fill(null); // No alignment for tables without headers
|
|
467
|
+
}
|
|
468
|
+
// Detect footer alignment row pattern in body rows (only for tables with header alignment)
|
|
469
|
+
let shouldDetectFooter = false;
|
|
470
|
+
let processedBodyRows = bodyRows;
|
|
471
|
+
if (detectFooter && hasHeaderAlignment && bodyRows.length > 0) {
|
|
472
|
+
// Check if any row matches the alignment pattern (contains only dashes, pipes, colons, and spaces)
|
|
473
|
+
for (let i = bodyRows.length - 1; i >= 0; i--) {
|
|
474
|
+
const row = bodyRows[i];
|
|
475
|
+
if (/^ *\| *:?-+:? *(\| *:?-+:? *)*\| *$/.test(row)) {
|
|
476
|
+
// Found footer alignment row - remove it and enable footer detection
|
|
477
|
+
shouldDetectFooter = true;
|
|
478
|
+
processedBodyRows = bodyRows.slice(0, i).concat(bodyRows.slice(i + 1));
|
|
479
|
+
break;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
// Process all rows and create table sections
|
|
484
|
+
const tokens = processRows(headerRows, processedBodyRows, alignment, colCount, this.lexer, maxColspan, shouldDetectFooter);
|
|
485
|
+
const item = {
|
|
486
|
+
type: 'table',
|
|
487
|
+
tokens,
|
|
488
|
+
raw: cap[0]
|
|
489
|
+
};
|
|
490
|
+
return item;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
]
|
|
494
|
+
};
|
|
495
|
+
}
|