markdown-it-adv-table 0.1.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/dist/table.js ADDED
@@ -0,0 +1,387 @@
1
+ import { Lexer, unwrapLiteral } from "./lexer.js";
2
+ /**
3
+ * TableSpec keeps table attributes
4
+ */
5
+ export class TableSpec {
6
+ attr;
7
+ styles;
8
+ headerRows;
9
+ headerCols;
10
+ colspecs;
11
+ constructor(attr) {
12
+ this.attr = attr;
13
+ this.headerRows = parseInt(attr["header-rows"] ?? "0");
14
+ this.headerCols = parseInt(attr["header-columns"] ?? "0");
15
+ this.colspecs = new ColSpecs(attr.cols ?? "");
16
+ this.styles = TableSpec.parseStyle(attr.style ?? "");
17
+ }
18
+ get numCols() {
19
+ return this.colspecs.numCols;
20
+ }
21
+ static parseStyle(style) {
22
+ return style.split(",").map((s) => s.trim());
23
+ }
24
+ static parseInfoString(info) {
25
+ const lexer = new Lexer(info);
26
+ const tokens = [];
27
+ const wordRe = /^[a-zA-Z][a-zA-Z0-9_\-]+/;
28
+ const result = {};
29
+ let cp = 0;
30
+ while (cp != Lexer.EOF) {
31
+ lexer.skipWhitespace();
32
+ cp = lexer.peek();
33
+ if (Lexer.isAlphabet(cp)) {
34
+ tokens.push(lexer.consumeRe(wordRe));
35
+ }
36
+ else if (Lexer.cmp(cp, "\"")) {
37
+ tokens.push(lexer.consumeLiteral("\""));
38
+ }
39
+ else if (Lexer.cmp(cp, "=")) {
40
+ tokens.push(lexer.consume());
41
+ }
42
+ else if (Lexer.isEOF(cp)) {
43
+ break;
44
+ }
45
+ else {
46
+ // unknown symbol?
47
+ tokens.push(lexer.consume());
48
+ }
49
+ }
50
+ ;
51
+ const pairedKeys = TableSpec.awareKeys();
52
+ let lc = 0;
53
+ while (tokens[lc] !== undefined) {
54
+ const token = tokens[lc];
55
+ if (pairedKeys.includes(token)) {
56
+ const key = consume();
57
+ consume("=");
58
+ const value = consume();
59
+ result[key] = value;
60
+ }
61
+ else {
62
+ // ignore unknown token
63
+ consume();
64
+ }
65
+ }
66
+ return result;
67
+ /** consume one token from token stream */
68
+ function consume(text) {
69
+ const token = tokens[lc];
70
+ if (text && token !== text) {
71
+ throw new Error(`parse error: expected ${text} but found ${token}`);
72
+ }
73
+ lc++;
74
+ return token;
75
+ }
76
+ }
77
+ static awareKeys() {
78
+ const k = {
79
+ cols: "",
80
+ align: "",
81
+ "header-columns": "",
82
+ "header-rows": "",
83
+ style: "",
84
+ format: ""
85
+ };
86
+ return Object.keys(k);
87
+ }
88
+ }
89
+ /**
90
+ * ColSpecs keeps all column specifications
91
+ */
92
+ export class ColSpecs {
93
+ numCols;
94
+ specs;
95
+ constructor(colspec) {
96
+ if (colspec.startsWith("\"") || colspec.includes(",")) {
97
+ // colspec must be "1,1,1" not 1,1,1
98
+ // we can try parse 1,1,1 .... but since this is an invalid syntax,
99
+ // the parser won't recognize unwrapped commma-separated values.
100
+ const specs = unwrapLiteral(colspec, "\"").split(",");
101
+ this.numCols = Math.max(1, specs.length);
102
+ this.specs = specs.map(ColSpecs.parseColSpec);
103
+ }
104
+ else if (/^\d+/.test(colspec)) {
105
+ this.numCols = Math.max(1, parseInt(colspec, 10));
106
+ this.specs = Array(this.numCols).fill({});
107
+ }
108
+ else {
109
+ this.numCols = 0;
110
+ this.specs = Array(this.numCols).fill({});
111
+ }
112
+ }
113
+ colSpec(col) {
114
+ return this.specs[col] || {};
115
+ }
116
+ colWidth(col) {
117
+ const spec = this.colSpec(col);
118
+ if (spec.width) {
119
+ return parseInt(spec.width, 10);
120
+ }
121
+ return 1;
122
+ }
123
+ colWidthRatio(col) {
124
+ let totalWidth = 0;
125
+ for (let c = 0; c < this.numCols; c++) {
126
+ totalWidth += this.colWidth(c);
127
+ }
128
+ return this.colWidth(col) / totalWidth;
129
+ }
130
+ /** Forcefully re-initailize this instance. */
131
+ lazyInit(cols) {
132
+ this.numCols = cols;
133
+ this.specs = Array(cols).fill({});
134
+ }
135
+ /** Parse colspec for single column */
136
+ static parseColSpec(spec) {
137
+ let pos = 0;
138
+ const attr = {};
139
+ while (pos < spec.length) {
140
+ const char = spec[pos];
141
+ if (char === "^") {
142
+ consume();
143
+ attr.align = "center";
144
+ }
145
+ else if (char === "<") {
146
+ consume();
147
+ attr.align = "left";
148
+ }
149
+ else if (char === ">") {
150
+ consume();
151
+ attr.align = "right";
152
+ }
153
+ else if (char >= "0" && char <= "9") {
154
+ attr.width = consumeNumber();
155
+ break;
156
+ }
157
+ else {
158
+ consume();
159
+ }
160
+ }
161
+ return attr;
162
+ function consume() {
163
+ return spec[pos++];
164
+ }
165
+ function consumeNumber() {
166
+ const match = spec.slice(pos).match(/^\d+/);
167
+ if (!match)
168
+ throw new Error("cannot consume number");
169
+ return match[0];
170
+ }
171
+ }
172
+ }
173
+ export class TableCell {
174
+ row;
175
+ col;
176
+ attr;
177
+ constructor(row, col, attr) {
178
+ this.row = row;
179
+ this.col = col;
180
+ this.attr = attr;
181
+ }
182
+ get colspan() {
183
+ return this.attr.colspan || 1;
184
+ }
185
+ get rowspan() {
186
+ return this.attr.rowspan || 1;
187
+ }
188
+ is(row, col) {
189
+ return this.row === row && this.col === col;
190
+ }
191
+ isIn(row, col, rspan, cspan) {
192
+ return (this.row <= row && this.row + this.rowspan > row &&
193
+ this.col <= col && this.col + this.colspan > col);
194
+ }
195
+ contains(row, col) {
196
+ return (this.row <= row && this.row + this.rowspan > row &&
197
+ this.col <= col && this.col + this.colspan > col);
198
+ }
199
+ }
200
+ /**
201
+ * A state machine to track cell position and its attributes
202
+ */
203
+ export class CellState {
204
+ static _initialPos = [-1, -1];
205
+ _tableSpec;
206
+ _pos;
207
+ _cells;
208
+ constructor(tableSpec) {
209
+ this._tableSpec = tableSpec;
210
+ this._pos = [...CellState._initialPos];
211
+ this._cells = [];
212
+ }
213
+ get numCols() {
214
+ return this._tableSpec.numCols;
215
+ }
216
+ get pos() {
217
+ return this._pos;
218
+ }
219
+ get row() {
220
+ return this._pos[0];
221
+ }
222
+ set row(num) {
223
+ this._pos[0] = num;
224
+ }
225
+ get col() {
226
+ return this._pos[1];
227
+ }
228
+ set col(num) {
229
+ this._pos[1] = num;
230
+ }
231
+ /** Starting from the top-left cell, find the next free cell. **/
232
+ next() {
233
+ if (this.row === -1) {
234
+ this.row = 0;
235
+ this.col = 0;
236
+ }
237
+ else {
238
+ do {
239
+ this.col++;
240
+ if (this.col >= this.numCols) {
241
+ this.col = 0;
242
+ this.row++;
243
+ }
244
+ } while (this.isSpanned(this.row, this.col));
245
+ }
246
+ return {
247
+ row: this.row,
248
+ col: this.col,
249
+ };
250
+ }
251
+ /**
252
+ * Get cell attribute. If the cell is spanned by the other, it will return
253
+ * the belonging cell. Returns undefined if no attribute is set.
254
+ */
255
+ get(row, col) {
256
+ for (const span of this._cells) {
257
+ if (span.contains(row, col)) {
258
+ return span;
259
+ }
260
+ }
261
+ return undefined;
262
+ }
263
+ /**
264
+ * Set cell attribute. If the cell is already spanned by the other,
265
+ * it will update the belonging cell. Modifying the span size will
266
+ * wipe attributes from the cells being affected.
267
+ */
268
+ set(row, col, attr) {
269
+ for (const span of this._cells) {
270
+ if (span.contains(row, col)) {
271
+ Object.assign(span.attr, attr);
272
+ this.wipe(span.row, span.col, span.rowspan, span.colspan, true);
273
+ return;
274
+ }
275
+ }
276
+ this._cells.push(new TableCell(row, col, attr));
277
+ }
278
+ wipe(row, col, rowspan, colspan, keepOrigin = false) {
279
+ this._cells = this._cells.filter(c => {
280
+ return keepOrigin
281
+ ? c.isIn(row, col, rowspan, colspan) && !c.is(row, col)
282
+ : c.isIn(row, col, rowspan, colspan);
283
+ });
284
+ }
285
+ /** Return true if this cell is spanned (includes self spanning) */
286
+ isSpanned(row, col) {
287
+ for (const span of this._cells) {
288
+ if (!span.is(row, col) && span.contains(row, col)) {
289
+ return true;
290
+ }
291
+ }
292
+ return false;
293
+ }
294
+ /** Return the cell which covers the given cell (excludes self span)*/
295
+ spannedBy(row, col) {
296
+ for (const span of this._cells) {
297
+ if (!span.is(row, col) && span.contains(row, col)) {
298
+ return span;
299
+ }
300
+ }
301
+ return undefined;
302
+ }
303
+ /** Return true if this cell is the left most cell **/
304
+ isLeftMost(row, col) {
305
+ if (col <= 0) {
306
+ return true; // left most, or out of range
307
+ }
308
+ else {
309
+ const span = this.get(row, col);
310
+ return span !== undefined && span.col === 0;
311
+ }
312
+ }
313
+ isRightMost(row, col) {
314
+ if (col >= this.numCols - 1) {
315
+ return true; // right most, or out of range
316
+ }
317
+ else {
318
+ const span = this.get(row, col);
319
+ return span !== undefined && (span.col + span.colspan) === this.numCols;
320
+ }
321
+ }
322
+ isRowFirst(row, col) {
323
+ if (col < 0)
324
+ return false;
325
+ if (col === 0) {
326
+ // If this cell is spanned by the other cell, it is not a row starter
327
+ return this.spannedBy(row, col) === undefined;
328
+ }
329
+ // if this cell is spanned by the other cell,
330
+ // always return false.
331
+ if (this.spannedBy(row, col) !== undefined) {
332
+ return false;
333
+ }
334
+ while (col > 0) {
335
+ const span = this.spannedBy(row, col - 1);
336
+ // recursively check left cell
337
+ if (span !== undefined) {
338
+ if (span.col < col) {
339
+ if (span.row === row) {
340
+ return false; // spanning cell is in the same row
341
+ }
342
+ else {
343
+ col = span.col;
344
+ }
345
+ }
346
+ else {
347
+ throw new Error("never happen. check implementation");
348
+ }
349
+ }
350
+ else {
351
+ return false;
352
+ }
353
+ }
354
+ // all cells in left are spanned, so this cell is the row-first cell
355
+ return true;
356
+ }
357
+ isRowLast(row, col) {
358
+ if (col >= this.numCols)
359
+ return false;
360
+ if (col === this.numCols - 1) {
361
+ // If this cell is spanned by the other cell, it is not a row closer.
362
+ return this.spannedBy(row, col) === undefined;
363
+ }
364
+ // if this cell is spanned by the other cell,
365
+ // always return false.
366
+ if (this.spannedBy(row, col) !== undefined) {
367
+ return false;
368
+ }
369
+ while (col < this.numCols - 1) {
370
+ const span = this.spannedBy(row, col + 1);
371
+ // recursively check right cell
372
+ if (span !== undefined) {
373
+ if (span.col + span.rowspan > col) {
374
+ col = span.col + span.rowspan;
375
+ }
376
+ else {
377
+ throw new Error("never happen. check implementation");
378
+ }
379
+ }
380
+ else {
381
+ return false;
382
+ }
383
+ }
384
+ // all cells in right are spanned, so this cell is the row-last cell
385
+ return true;
386
+ }
387
+ }
@@ -0,0 +1,4 @@
1
+ export type DeepPartial<T> = {
2
+ [P in keyof T]?: T[P] extends Array<infer U> ? Array<DeepPartial<U>> : T[P] extends object ? T[P] extends Function ? T[P] : DeepPartial<T[P]> : T[P];
3
+ };
4
+ export declare function deepmerge<T, U>(target: Partial<T>, source?: Partial<U>): T & U;
package/dist/utils.js ADDED
@@ -0,0 +1,47 @@
1
+ function isPrimitive(value) {
2
+ return (value === null
3
+ || typeof value === "string"
4
+ || typeof value === "number"
5
+ || typeof value === "boolean"
6
+ || typeof value === "symbol"
7
+ || typeof value === "bigint"
8
+ || typeof value === "undefined");
9
+ }
10
+ function isPlainObject(obj) {
11
+ return Object.prototype.toString.call(obj) === "[object Object]"
12
+ && Object.getPrototypeOf(obj) === Object.prototype;
13
+ }
14
+ export function deepmerge(target, source) {
15
+ const seen = new WeakSet();
16
+ function merge(target, source) {
17
+ if (seen.has(source)) {
18
+ return undefined;
19
+ }
20
+ if (isPrimitive(source)) {
21
+ return source;
22
+ }
23
+ if (Array.isArray(source)) {
24
+ seen.add(source);
25
+ return source.map(item => merge(undefined, item));
26
+ }
27
+ if (isPlainObject(source)) {
28
+ seen.add(source);
29
+ const result = isPlainObject(target) ? { ...target } : {};
30
+ for (const key of Object.keys(source)) {
31
+ // Skip dangerous keys
32
+ if (key === "__proto__" || key === "constructor" || key === "prototype") {
33
+ continue;
34
+ }
35
+ const value = source[key];
36
+ const merged = merge(result[key], value);
37
+ if (merged !== undefined) {
38
+ result[key] = merged;
39
+ }
40
+ }
41
+ return result;
42
+ }
43
+ return undefined;
44
+ }
45
+ return merge(target, source);
46
+ }
47
+ //# sourceMappingURL=utils.js.map
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "markdown-it-adv-table",
3
+ "version": "0.1.0",
4
+ "description": "Markdown syntax extension for better table support",
5
+ "keywords": [
6
+ "markdown",
7
+ "markdown-it",
8
+ "markdown-it-plugin",
9
+ "table",
10
+ "csv"
11
+ ],
12
+ "homepage": "https://github.com/yamavol/markdown-it-adv-table#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/yamavol/markdown-it-adv-table/issues"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/yamavol/markdown-it-adv-table.git"
19
+ },
20
+ "license": "MIT",
21
+ "author": "yamavol",
22
+ "type": "module",
23
+ "main": "dist/index.js",
24
+ "types": "dist/index.d.ts",
25
+ "directories": {
26
+ "doc": "docs",
27
+ "test": "test"
28
+ },
29
+ "files": [
30
+ "dist/**/*.js",
31
+ "dist/**/*.d.ts"
32
+ ],
33
+ "scripts": {
34
+ "build": "tsc",
35
+ "build:dist": "tsc --build tsconfig.dist.json",
36
+ "test": "vitest",
37
+ "coverage": "vitest run --coverage",
38
+ "lint": "eslint src",
39
+ "lint:fix": "eslint src --fix"
40
+ },
41
+ "devDependencies": {
42
+ "@stylistic/eslint-plugin": "^4.2.0",
43
+ "@types/markdown-it": "^14.1.2",
44
+ "@types/node": "^22.14.1",
45
+ "@vitest/coverage-v8": "^3.1.1",
46
+ "eslint": "^9.24.0",
47
+ "eslint-plugin-import": "^2.31.0",
48
+ "markdown-it": "^14.1.0",
49
+ "typescript": "^5.8.3",
50
+ "typescript-eslint": "^8.29.1",
51
+ "vitest": "^3.1.1"
52
+ }
53
+ }