mardora 1.2.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.
Files changed (138) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +113 -0
  3. package/dist/chunk-3OCUX4OO.js +7690 -0
  4. package/dist/chunk-3OCUX4OO.js.map +1 -0
  5. package/dist/chunk-3ZOCCFDL.cjs +74 -0
  6. package/dist/chunk-3ZOCCFDL.cjs.map +1 -0
  7. package/dist/chunk-7JOEPNEV.cjs +7740 -0
  8. package/dist/chunk-7JOEPNEV.cjs.map +1 -0
  9. package/dist/chunk-BIKZQZ6W.js +33 -0
  10. package/dist/chunk-BIKZQZ6W.js.map +1 -0
  11. package/dist/chunk-EQJESPP2.js +234 -0
  12. package/dist/chunk-EQJESPP2.js.map +1 -0
  13. package/dist/chunk-G4SE26YY.js +70 -0
  14. package/dist/chunk-G4SE26YY.js.map +1 -0
  15. package/dist/chunk-KNDWF2DP.cjs +35 -0
  16. package/dist/chunk-KNDWF2DP.cjs.map +1 -0
  17. package/dist/chunk-MLBEBFHB.cjs +2971 -0
  18. package/dist/chunk-MLBEBFHB.cjs.map +1 -0
  19. package/dist/chunk-P7JFCYU3.js +905 -0
  20. package/dist/chunk-P7JFCYU3.js.map +1 -0
  21. package/dist/chunk-SWFUKJDO.cjs +243 -0
  22. package/dist/chunk-SWFUKJDO.cjs.map +1 -0
  23. package/dist/chunk-WFVCG4LD.cjs +926 -0
  24. package/dist/chunk-WFVCG4LD.cjs.map +1 -0
  25. package/dist/chunk-XL6WFGJT.js +2901 -0
  26. package/dist/chunk-XL6WFGJT.js.map +1 -0
  27. package/dist/editor/index.cjs +277 -0
  28. package/dist/editor/index.cjs.map +1 -0
  29. package/dist/editor/index.d.cts +186 -0
  30. package/dist/editor/index.d.ts +186 -0
  31. package/dist/editor/index.js +4 -0
  32. package/dist/editor/index.js.map +1 -0
  33. package/dist/index.cjs +405 -0
  34. package/dist/index.cjs.map +1 -0
  35. package/dist/index.d.cts +13 -0
  36. package/dist/index.d.ts +13 -0
  37. package/dist/index.js +8 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/lib/index.cjs +12 -0
  40. package/dist/lib/index.cjs.map +1 -0
  41. package/dist/lib/index.d.cts +16 -0
  42. package/dist/lib/index.d.ts +16 -0
  43. package/dist/lib/index.js +3 -0
  44. package/dist/lib/index.js.map +1 -0
  45. package/dist/mardora-DCwjomil.d.cts +640 -0
  46. package/dist/mardora-DCwjomil.d.ts +640 -0
  47. package/dist/plugins/index.cjs +104 -0
  48. package/dist/plugins/index.cjs.map +1 -0
  49. package/dist/plugins/index.d.cts +740 -0
  50. package/dist/plugins/index.d.ts +740 -0
  51. package/dist/plugins/index.js +7 -0
  52. package/dist/plugins/index.js.map +1 -0
  53. package/dist/preview/index.cjs +38 -0
  54. package/dist/preview/index.cjs.map +1 -0
  55. package/dist/preview/index.d.cts +101 -0
  56. package/dist/preview/index.d.ts +101 -0
  57. package/dist/preview/index.js +5 -0
  58. package/dist/preview/index.js.map +1 -0
  59. package/dist/types-NBsaxl4d.d.cts +71 -0
  60. package/dist/types-Pw2SWWAR.d.ts +71 -0
  61. package/package.json +92 -0
  62. package/src/editor/attachments/extension.ts +181 -0
  63. package/src/editor/attachments/format.ts +63 -0
  64. package/src/editor/attachments/index.ts +3 -0
  65. package/src/editor/attachments/types.ts +37 -0
  66. package/src/editor/heading-fold/config.ts +25 -0
  67. package/src/editor/heading-fold/extension.ts +268 -0
  68. package/src/editor/heading-fold/extract.ts +88 -0
  69. package/src/editor/heading-fold/index.ts +5 -0
  70. package/src/editor/heading-fold/theme.ts +85 -0
  71. package/src/editor/heading-fold/types.ts +24 -0
  72. package/src/editor/i18n.ts +13 -0
  73. package/src/editor/icons/index.ts +367 -0
  74. package/src/editor/index.ts +16 -0
  75. package/src/editor/mardora.ts +257 -0
  76. package/src/editor/media-lightbox-theme.ts +146 -0
  77. package/src/editor/media-lightbox.ts +125 -0
  78. package/src/editor/plugin.ts +294 -0
  79. package/src/editor/selection-toolbar/activation.ts +123 -0
  80. package/src/editor/selection-toolbar/commands.ts +279 -0
  81. package/src/editor/selection-toolbar/extension.ts +564 -0
  82. package/src/editor/selection-toolbar/i18n.ts +164 -0
  83. package/src/editor/selection-toolbar/index.ts +7 -0
  84. package/src/editor/selection-toolbar/menu.ts +252 -0
  85. package/src/editor/selection-toolbar/position.ts +43 -0
  86. package/src/editor/selection-toolbar/theme.ts +195 -0
  87. package/src/editor/selection-toolbar/types.ts +155 -0
  88. package/src/editor/slash/default-commands.ts +190 -0
  89. package/src/editor/slash/extension.ts +319 -0
  90. package/src/editor/slash/index.ts +7 -0
  91. package/src/editor/slash/insertions.ts +26 -0
  92. package/src/editor/slash/menu.ts +123 -0
  93. package/src/editor/slash/position.ts +61 -0
  94. package/src/editor/slash/query.ts +33 -0
  95. package/src/editor/slash/theme.ts +113 -0
  96. package/src/editor/slash/types.ts +40 -0
  97. package/src/editor/table-of-contents/extension.ts +202 -0
  98. package/src/editor/table-of-contents/extract.ts +53 -0
  99. package/src/editor/table-of-contents/index.ts +7 -0
  100. package/src/editor/table-of-contents/panel.ts +83 -0
  101. package/src/editor/table-of-contents/slug.ts +50 -0
  102. package/src/editor/table-of-contents/storage.ts +35 -0
  103. package/src/editor/table-of-contents/theme.ts +153 -0
  104. package/src/editor/table-of-contents/types.ts +44 -0
  105. package/src/editor/theme.ts +72 -0
  106. package/src/editor/utils.ts +176 -0
  107. package/src/editor/view-plugin.ts +189 -0
  108. package/src/index.ts +5 -0
  109. package/src/lib/index.ts +2 -0
  110. package/src/lib/input-handler.ts +47 -0
  111. package/src/plugins/code-plugin.theme.ts +545 -0
  112. package/src/plugins/code-plugin.ts +1892 -0
  113. package/src/plugins/emoji-plugin.ts +140 -0
  114. package/src/plugins/heading-plugin.ts +194 -0
  115. package/src/plugins/hr-plugin.ts +102 -0
  116. package/src/plugins/html-plugin.ts +353 -0
  117. package/src/plugins/image-plugin.ts +806 -0
  118. package/src/plugins/index.ts +71 -0
  119. package/src/plugins/inline-plugin.ts +311 -0
  120. package/src/plugins/link-plugin.ts +509 -0
  121. package/src/plugins/list-plugin.ts +492 -0
  122. package/src/plugins/math-plugin.ts +526 -0
  123. package/src/plugins/mermaid-plugin.ts +513 -0
  124. package/src/plugins/paragraph-plugin.ts +38 -0
  125. package/src/plugins/quote-plugin.ts +733 -0
  126. package/src/plugins/table-controls-theme.ts +126 -0
  127. package/src/plugins/table-controls.ts +423 -0
  128. package/src/plugins/table-model.ts +661 -0
  129. package/src/plugins/table-plugin.ts +2111 -0
  130. package/src/preview/context.ts +45 -0
  131. package/src/preview/css-generator.ts +64 -0
  132. package/src/preview/default-renderers.ts +29 -0
  133. package/src/preview/index.ts +29 -0
  134. package/src/preview/preview.ts +41 -0
  135. package/src/preview/renderer.ts +184 -0
  136. package/src/preview/syntax-theme.ts +112 -0
  137. package/src/preview/toc.ts +23 -0
  138. package/src/preview/types.ts +89 -0
@@ -0,0 +1,661 @@
1
+ import { syntaxTree } from "@codemirror/language";
2
+ import { EditorState } from "@codemirror/state";
3
+
4
+ export type Alignment = "left" | "center" | "right";
5
+ export type TableRowKind = "header" | "body";
6
+
7
+ export interface ParsedTable {
8
+ headers: string[];
9
+ alignments: Alignment[];
10
+ rows: string[][];
11
+ }
12
+
13
+ export interface TableCellInfo {
14
+ rowKind: TableRowKind;
15
+ rowIndex: number;
16
+ columnIndex: number;
17
+ from: number;
18
+ to: number;
19
+ contentFrom: number;
20
+ contentTo: number;
21
+ lineFrom: number;
22
+ lineNumber: number;
23
+ rawText: string;
24
+ }
25
+
26
+ export interface TableInfo {
27
+ from: number;
28
+ to: number;
29
+ startLineNumber: number;
30
+ delimiterLineNumber: number;
31
+ endLineNumber: number;
32
+ columnCount: number;
33
+ alignments: Alignment[];
34
+ cellsByRow: TableCellInfo[][];
35
+ headerCells: TableCellInfo[];
36
+ bodyCells: TableCellInfo[][];
37
+ }
38
+
39
+ export const BREAK_TAG = "<br />";
40
+ export const BREAK_TAG_REGEX = /<br\s*\/?>/gi;
41
+ export const DELIMITER_CELL_PATTERN = /^:?-{3,}:?$/;
42
+ /** Returns whether the character at the given index is backslash-escaped. */
43
+ export function isEscaped(text: string, index: number): boolean {
44
+ let slashCount = 0;
45
+ for (let i = index - 1; i >= 0 && text[i] === "\\"; i--) {
46
+ slashCount++;
47
+ }
48
+ return slashCount % 2 === 1;
49
+ }
50
+
51
+ /** Collects the positions of every unescaped pipe character in a line. */
52
+ export function getPipePositions(lineText: string): number[] {
53
+ const positions: number[] = [];
54
+ for (let index = 0; index < lineText.length; index++) {
55
+ if (lineText[index] === "|" && !isEscaped(lineText, index)) {
56
+ positions.push(index);
57
+ }
58
+ }
59
+ return positions;
60
+ }
61
+
62
+ /** Splits a markdown table row into raw cell strings. */
63
+ export function splitTableLine(lineText: string): string[] {
64
+ const cells: string[] = [];
65
+ const trimmed = lineText.trim();
66
+
67
+ if (!trimmed.includes("|")) {
68
+ return [trimmed];
69
+ }
70
+
71
+ let current = "";
72
+ for (let index = 0; index < trimmed.length; index++) {
73
+ const char = trimmed[index]!;
74
+ if (char === "|" && !isEscaped(trimmed, index)) {
75
+ cells.push(current);
76
+ current = "";
77
+ continue;
78
+ }
79
+ current += char;
80
+ }
81
+ cells.push(current);
82
+
83
+ if (trimmed.startsWith("|")) {
84
+ cells.shift();
85
+ }
86
+ if (trimmed.endsWith("|")) {
87
+ cells.pop();
88
+ }
89
+
90
+ return cells;
91
+ }
92
+
93
+ /** Checks whether the given line can participate in a table block. */
94
+ export function isTableRowLine(lineText: string): boolean {
95
+ return getPipePositions(lineText.trim()).length > 0;
96
+ }
97
+
98
+ /** Parses a delimiter cell token into a table alignment value. */
99
+ export function parseAlignment(cell: string): Alignment {
100
+ const trimmed = cell.trim();
101
+ const left = trimmed.startsWith(":");
102
+ const right = trimmed.endsWith(":");
103
+ if (left && right) return "center";
104
+ if (right) return "right";
105
+ return "left";
106
+ }
107
+
108
+ /** Parses the delimiter line and returns per-column alignments. */
109
+ export function parseDelimiterAlignments(lineText: string): Alignment[] | null {
110
+ const cells = splitTableLine(lineText).map((cell) => cell.trim());
111
+ if (cells.length === 0 || !cells.every((cell) => DELIMITER_CELL_PATTERN.test(cell))) {
112
+ return null;
113
+ }
114
+ return cells.map(parseAlignment);
115
+ }
116
+
117
+ /** Splits a table node slice into its table lines and any trailing markdown. */
118
+ export function splitTableAndTrailingMarkdown(markdown: string): { tableMarkdown: string; trailingMarkdown: string } {
119
+ const lines = markdown.split("\n");
120
+ if (lines.length < 2) {
121
+ return { tableMarkdown: markdown, trailingMarkdown: "" };
122
+ }
123
+
124
+ const headerLine = lines[0] || "";
125
+ const delimiterLine = lines[1] || "";
126
+ if (!isTableRowLine(headerLine) || !parseDelimiterAlignments(delimiterLine)) {
127
+ return { tableMarkdown: markdown, trailingMarkdown: "" };
128
+ }
129
+
130
+ let endIndex = 1;
131
+ for (let index = 2; index < lines.length; index++) {
132
+ if (!isTableRowLine(lines[index] || "")) {
133
+ break;
134
+ }
135
+ endIndex = index;
136
+ }
137
+
138
+ return {
139
+ tableMarkdown: lines.slice(0, endIndex + 1).join("\n"),
140
+ trailingMarkdown: lines.slice(endIndex + 1).join("\n"),
141
+ };
142
+ }
143
+
144
+ /** Normalizes every supported `<br>` form to the canonical `<br />` token. */
145
+ export function canonicalizeBreakTags(text: string): string {
146
+ return text.replace(BREAK_TAG_REGEX, BREAK_TAG);
147
+ }
148
+
149
+ /** Escapes literal pipe characters so cell content stays GFM-compatible. */
150
+ export function escapeUnescapedPipes(text: string): string {
151
+ let result = "";
152
+ for (let index = 0; index < text.length; index++) {
153
+ const char = text[index]!;
154
+ if (char === "|" && !isEscaped(text, index)) {
155
+ result += "\\|";
156
+ continue;
157
+ }
158
+ result += char;
159
+ }
160
+ return result;
161
+ }
162
+
163
+ /** Trims and normalizes cell content before it is written back to markdown. */
164
+ export function normalizeCellContent(text: string): string {
165
+ const normalizedBreaks = canonicalizeBreakTags(text.trim());
166
+ if (!normalizedBreaks) {
167
+ return "";
168
+ }
169
+
170
+ const parts = normalizedBreaks.split(BREAK_TAG_REGEX).map((part) => escapeUnescapedPipes(part.trim()));
171
+ if (parts.length === 1) {
172
+ return parts[0] || "";
173
+ }
174
+
175
+ return parts.join(` ${BREAK_TAG} `).trim();
176
+ }
177
+
178
+ /** Measures the visible width of a cell for markdown alignment output. */
179
+ export function renderWidth(text: string): number {
180
+ return canonicalizeBreakTags(text).replace(BREAK_TAG, " ").replace(/\\\|/g, "|").length;
181
+ }
182
+
183
+ /** Pads a cell according to its alignment for normalized markdown output. */
184
+ export function padCell(text: string, width: number, alignment: Alignment): string {
185
+ const safeWidth = Math.max(width, renderWidth(text));
186
+ const difference = safeWidth - renderWidth(text);
187
+ if (difference <= 0) {
188
+ return text;
189
+ }
190
+
191
+ if (alignment === "right") {
192
+ return " ".repeat(difference) + text;
193
+ }
194
+
195
+ if (alignment === "center") {
196
+ const left = Math.floor(difference / 2);
197
+ const right = difference - left;
198
+ return " ".repeat(left) + text + " ".repeat(right);
199
+ }
200
+
201
+ return text + " ".repeat(difference);
202
+ }
203
+
204
+ /** Builds the markdown delimiter token for a column. */
205
+ export function delimiterCell(width: number, alignment: Alignment): string {
206
+ const hyphenCount = Math.max(width, 3);
207
+ if (alignment === "center") {
208
+ return ":" + "-".repeat(Math.max(1, hyphenCount - 2)) + ":";
209
+ }
210
+ if (alignment === "right") {
211
+ return "-".repeat(Math.max(2, hyphenCount - 1)) + ":";
212
+ }
213
+ return "-".repeat(hyphenCount);
214
+ }
215
+
216
+ /** Parses a markdown table block into header, alignment, and body rows. */
217
+ export function parseTableMarkdown(markdown: string): ParsedTable | null {
218
+ const { tableMarkdown } = splitTableAndTrailingMarkdown(markdown);
219
+ const lines = tableMarkdown.split("\n");
220
+ if (lines.length < 2) {
221
+ return null;
222
+ }
223
+
224
+ const headers = splitTableLine(lines[0] || "").map((cell) => cell.trim());
225
+ const alignments = parseDelimiterAlignments(lines[1] || "");
226
+ if (!alignments) {
227
+ return null;
228
+ }
229
+
230
+ const rows = lines
231
+ .slice(2)
232
+ .filter((line) => isTableRowLine(line))
233
+ .map((line) => splitTableLine(line).map((cell) => cell.trim()));
234
+
235
+ return { headers, alignments, rows };
236
+ }
237
+
238
+ /** Expands all rows so the parsed table has a consistent column count. */
239
+ export function normalizeParsedTable(parsed: ParsedTable): ParsedTable {
240
+ const columnCount = Math.max(
241
+ parsed.headers.length,
242
+ parsed.alignments.length,
243
+ ...parsed.rows.map((row) => row.length),
244
+ 1
245
+ );
246
+
247
+ const headers = Array.from({ length: columnCount }, (_, index) => normalizeCellContent(parsed.headers[index] || ""));
248
+ const alignments = Array.from({ length: columnCount }, (_, index) => parsed.alignments[index] || "left");
249
+ const rows = parsed.rows.map((row) =>
250
+ Array.from({ length: columnCount }, (_, index) => normalizeCellContent(row[index] || ""))
251
+ );
252
+
253
+ return { headers, alignments, rows };
254
+ }
255
+
256
+ /** Formats a parsed table back into normalized GFM markdown. */
257
+ export function formatTableMarkdown(parsed: ParsedTable): string {
258
+ const normalized = normalizeParsedTable(parsed);
259
+ const widths = normalized.headers.map((header, index) =>
260
+ Math.max(renderWidth(header), ...normalized.rows.map((row) => renderWidth(row[index] || "")), 3)
261
+ );
262
+
263
+ const formatRow = (cells: string[]) =>
264
+ `| ${cells.map((cell, index) => padCell(cell, widths[index] || 3, normalized.alignments[index] || "left")).join(" | ")} |`;
265
+
266
+ const headerLine = formatRow(normalized.headers);
267
+ const delimiterLine = `| ${normalized.alignments
268
+ .map((alignment, index) => delimiterCell(widths[index] || 3, alignment))
269
+ .join(" | ")} |`;
270
+ const bodyLines = normalized.rows.map((row) => formatRow(row));
271
+
272
+ return [headerLine, delimiterLine, ...bodyLines].join("\n");
273
+ }
274
+
275
+ /** Creates a blank row with the requested number of columns. */
276
+ export function buildEmptyRow(columnCount: number): string[] {
277
+ return Array.from({ length: columnCount }, () => "");
278
+ }
279
+
280
+ /** Finds the visible content bounds inside a raw table cell span. */
281
+ export function getVisibleBounds(rawCellText: string): { startOffset: number; endOffset: number } {
282
+ const leading = rawCellText.length - rawCellText.trimStart().length;
283
+ const trailing = rawCellText.length - rawCellText.trimEnd().length;
284
+ const trimmedLength = rawCellText.trim().length;
285
+
286
+ if (trimmedLength === 0) {
287
+ const placeholderOffset = Math.min(Math.floor(rawCellText.length / 2), Math.max(rawCellText.length - 1, 0));
288
+ return {
289
+ startOffset: placeholderOffset,
290
+ endOffset: Math.min(placeholderOffset + 1, rawCellText.length),
291
+ };
292
+ }
293
+
294
+ return {
295
+ startOffset: leading,
296
+ endOffset: rawCellText.length - trailing,
297
+ };
298
+ }
299
+
300
+ /** Returns whether every cell in a body row is empty. */
301
+ export function isBodyRowEmpty(row: TableCellInfo[]): boolean {
302
+ return row.every((cell) => normalizeCellContent(cell.rawText) === "");
303
+ }
304
+
305
+ /** Converts the live editor table model into a serializable table structure. */
306
+ export function buildTableFromInfo(tableInfo: TableInfo): ParsedTable {
307
+ return {
308
+ headers: tableInfo.headerCells.map((cell) => normalizeCellContent(cell.rawText)),
309
+ alignments: [...tableInfo.alignments],
310
+ rows: tableInfo.bodyCells.map((row) => row.map((cell) => normalizeCellContent(cell.rawText))),
311
+ };
312
+ }
313
+
314
+ /** Maps a logical row index to its physical line index in formatted markdown. */
315
+ export function getRowLineIndex(rowIndex: number): number {
316
+ return rowIndex === 0 ? 0 : rowIndex + 1;
317
+ }
318
+
319
+ /** Resolves the caret anchor for a cell inside normalized table markdown. */
320
+ export function getCellAnchorInFormattedTable(
321
+ formattedTable: string,
322
+ rowIndex: number,
323
+ columnIndex: number,
324
+ offset = 0
325
+ ): number {
326
+ const lines = formattedTable.split("\n");
327
+ const lineIndex = getRowLineIndex(rowIndex);
328
+ const lineText = lines[lineIndex] || "";
329
+ const pipes = getPipePositions(lineText);
330
+
331
+ if (pipes.length < columnIndex + 2) {
332
+ return formattedTable.length;
333
+ }
334
+
335
+ const rawFrom = pipes[columnIndex]! + 1;
336
+ const rawTo = pipes[columnIndex + 1]!;
337
+ const visible = getVisibleBounds(lineText.slice(rawFrom, rawTo));
338
+ const lineOffset = lines.slice(0, lineIndex).reduce((sum, line) => sum + line.length + 1, 0);
339
+ return (
340
+ lineOffset +
341
+ Math.min(rawFrom + visible.startOffset + offset, rawFrom + Math.max(visible.endOffset - 1, visible.startOffset))
342
+ );
343
+ }
344
+
345
+ /** Wraps a table replacement with the required blank spacer lines. */
346
+ export function createTableInsert(state: EditorState, from: number, to: number, tableMarkdown: string) {
347
+ let insert = tableMarkdown;
348
+ let prefixLength = 0;
349
+
350
+ const startLine = state.doc.lineAt(from);
351
+ if (startLine.number === 1 || state.doc.line(startLine.number - 1).text.trim() !== "") {
352
+ insert = "\n" + insert;
353
+ prefixLength = 1;
354
+ }
355
+
356
+ const endLine = state.doc.lineAt(Math.max(from, to));
357
+ if (endLine.number === state.doc.lines || state.doc.line(endLine.number + 1).text.trim() !== "") {
358
+ insert += "\n";
359
+ }
360
+
361
+ return { from, to, insert, prefixLength };
362
+ }
363
+
364
+ /** Builds a live table model from the current editor document. */
365
+ export function readTableInfo(state: EditorState, nodeFrom: number, nodeTo: number): TableInfo | null {
366
+ const startLine = state.doc.lineAt(nodeFrom);
367
+ const endLine = state.doc.lineAt(nodeTo);
368
+ const delimiterLineNumber = startLine.number + 1;
369
+ if (delimiterLineNumber > endLine.number) {
370
+ return null;
371
+ }
372
+
373
+ const delimiterLine = state.doc.line(delimiterLineNumber);
374
+ const alignments = parseDelimiterAlignments(delimiterLine.text);
375
+ if (!alignments) {
376
+ return null;
377
+ }
378
+
379
+ let effectiveEndLineNumber = delimiterLineNumber;
380
+ for (let lineNumber = delimiterLineNumber + 1; lineNumber <= endLine.number; lineNumber++) {
381
+ const line = state.doc.line(lineNumber);
382
+ if (!isTableRowLine(line.text)) {
383
+ break;
384
+ }
385
+ effectiveEndLineNumber = lineNumber;
386
+ }
387
+
388
+ const cellsByRow: TableCellInfo[][] = [];
389
+ for (let lineNumber = startLine.number; lineNumber <= effectiveEndLineNumber; lineNumber++) {
390
+ if (lineNumber === delimiterLineNumber) {
391
+ continue;
392
+ }
393
+
394
+ const line = state.doc.line(lineNumber);
395
+ const pipes = getPipePositions(line.text);
396
+ if (pipes.length < 2) {
397
+ return null;
398
+ }
399
+
400
+ const isHeader = lineNumber === startLine.number;
401
+ const rowIndex = isHeader ? 0 : cellsByRow.length;
402
+ const cells: TableCellInfo[] = [];
403
+
404
+ for (let columnIndex = 0; columnIndex < pipes.length - 1; columnIndex++) {
405
+ const from = line.from + pipes[columnIndex]! + 1;
406
+ const to = line.from + pipes[columnIndex + 1]!;
407
+ const rawText = line.text.slice(pipes[columnIndex]! + 1, pipes[columnIndex + 1]);
408
+ const visible = getVisibleBounds(rawText);
409
+
410
+ cells.push({
411
+ rowKind: isHeader ? "header" : "body",
412
+ rowIndex,
413
+ columnIndex,
414
+ from,
415
+ to,
416
+ contentFrom: from + visible.startOffset,
417
+ contentTo: from + visible.endOffset,
418
+ lineFrom: line.from,
419
+ lineNumber,
420
+ rawText,
421
+ });
422
+ }
423
+
424
+ cellsByRow.push(cells);
425
+ }
426
+
427
+ if (cellsByRow.length === 0) {
428
+ return null;
429
+ }
430
+
431
+ return {
432
+ from: startLine.from,
433
+ to: state.doc.line(effectiveEndLineNumber).to,
434
+ startLineNumber: startLine.number,
435
+ delimiterLineNumber,
436
+ endLineNumber: effectiveEndLineNumber,
437
+ columnCount: cellsByRow[0]!.length,
438
+ alignments: Array.from({ length: cellsByRow[0]!.length }, (_, index) => alignments[index] || "left"),
439
+ cellsByRow,
440
+ headerCells: cellsByRow[0]!,
441
+ bodyCells: cellsByRow.slice(1),
442
+ };
443
+ }
444
+
445
+ /** Finds the table model that contains the given document position. */
446
+ export function getTableInfoAtPosition(state: EditorState, position: number): TableInfo | null {
447
+ let resolved: TableInfo | null = null;
448
+
449
+ syntaxTree(state).iterate({
450
+ enter: (node) => {
451
+ if (resolved || node.name !== "Table") {
452
+ return;
453
+ }
454
+
455
+ const info = readTableInfo(state, node.from, node.to);
456
+ if (info && position >= info.from && position <= info.to) {
457
+ resolved = info;
458
+ }
459
+ },
460
+ });
461
+
462
+ return resolved;
463
+ }
464
+
465
+ /** Returns the table cell containing the given cursor position. */
466
+ export function findCellAtPosition(tableInfo: TableInfo, position: number): TableCellInfo | null {
467
+ for (const row of tableInfo.cellsByRow) {
468
+ for (const cell of row) {
469
+ if (position >= cell.from && position <= cell.to) {
470
+ return cell;
471
+ }
472
+ }
473
+ }
474
+
475
+ for (const row of tableInfo.cellsByRow) {
476
+ for (const cell of row) {
477
+ if (position >= cell.from - 1 && position <= cell.to + 1) {
478
+ return cell;
479
+ }
480
+ }
481
+ }
482
+
483
+ let nearestCell: TableCellInfo | null = null;
484
+ let nearestDistance = Number.POSITIVE_INFINITY;
485
+ for (const row of tableInfo.cellsByRow) {
486
+ for (const cell of row) {
487
+ const distance = Math.min(Math.abs(position - cell.from), Math.abs(position - cell.to));
488
+ if (distance < nearestDistance) {
489
+ nearestCell = cell;
490
+ nearestDistance = distance;
491
+ }
492
+ }
493
+ }
494
+
495
+ return nearestCell;
496
+ }
497
+
498
+ /** Clamps a document position into the editable content span of a cell. */
499
+ export function clampCellPosition(cell: TableCellInfo, position: number): number {
500
+ const cellEnd = Math.max(cell.contentFrom, cell.contentTo);
501
+ return Math.max(cell.contentFrom, Math.min(position, cellEnd));
502
+ }
503
+
504
+ /** Collects all `<br />` token ranges from the current table. */
505
+ export function collectBreakRanges(tableInfo: TableInfo): Array<{ from: number; to: number }> {
506
+ const ranges: Array<{ from: number; to: number }> = [];
507
+
508
+ for (const row of tableInfo.cellsByRow) {
509
+ for (const cell of row) {
510
+ let match: RegExpExecArray | null;
511
+ const regex = new RegExp(BREAK_TAG_REGEX);
512
+ while ((match = regex.exec(cell.rawText)) !== null) {
513
+ ranges.push({
514
+ from: cell.from + match.index,
515
+ to: cell.from + match.index + match[0].length,
516
+ });
517
+ }
518
+ }
519
+ }
520
+
521
+ return ranges;
522
+ }
523
+
524
+ export type InsertSide = "above" | "below";
525
+ export type HorizontalSide = "left" | "right";
526
+ export type MoveDirection = "up" | "down" | "left" | "right";
527
+
528
+ function bodyIndexFromRowIndex(rowIndex: number): number {
529
+ return rowIndex - 1;
530
+ }
531
+
532
+ function columnArrayIndex(columnIndex: number): number {
533
+ return columnIndex - 1;
534
+ }
535
+
536
+ function cloneParsedTable(parsed: ParsedTable): ParsedTable {
537
+ const normalized = normalizeParsedTable(parsed);
538
+ return {
539
+ headers: [...normalized.headers],
540
+ alignments: [...normalized.alignments],
541
+ rows: normalized.rows.map((row) => [...row]),
542
+ };
543
+ }
544
+
545
+ function swapArrayItems<T>(items: T[], left: number, right: number): void {
546
+ const current = items[left]!;
547
+ items[left] = items[right]!;
548
+ items[right] = current;
549
+ }
550
+
551
+ export function insertTableRow(parsed: ParsedTable, rowIndex: number, side: InsertSide): ParsedTable {
552
+ const next = cloneParsedTable(parsed);
553
+ const bodyIndex = Math.max(0, Math.min(bodyIndexFromRowIndex(rowIndex), next.rows.length));
554
+ const insertAt = side === "above" ? bodyIndex : bodyIndex + 1;
555
+ next.rows.splice(Math.max(0, Math.min(insertAt, next.rows.length)), 0, buildEmptyRow(next.headers.length));
556
+ return next;
557
+ }
558
+
559
+ export function moveTableRow(
560
+ parsed: ParsedTable,
561
+ rowIndex: number,
562
+ direction: Extract<MoveDirection, "up" | "down">
563
+ ): ParsedTable {
564
+ const next = cloneParsedTable(parsed);
565
+ const bodyIndex = bodyIndexFromRowIndex(rowIndex);
566
+ const targetIndex = direction === "up" ? bodyIndex - 1 : bodyIndex + 1;
567
+ if (bodyIndex < 0 || bodyIndex >= next.rows.length || targetIndex < 0 || targetIndex >= next.rows.length) {
568
+ return next;
569
+ }
570
+ const current = next.rows[bodyIndex]!;
571
+ next.rows[bodyIndex] = next.rows[targetIndex]!;
572
+ next.rows[targetIndex] = current;
573
+ return next;
574
+ }
575
+
576
+ export function copyTableRow(parsed: ParsedTable, rowIndex: number): ParsedTable {
577
+ const next = cloneParsedTable(parsed);
578
+ const bodyIndex = bodyIndexFromRowIndex(rowIndex);
579
+ if (bodyIndex < 0 || bodyIndex >= next.rows.length) {
580
+ return next;
581
+ }
582
+ next.rows.splice(bodyIndex + 1, 0, [...next.rows[bodyIndex]!]);
583
+ return next;
584
+ }
585
+
586
+ export function deleteTableRow(parsed: ParsedTable, rowIndex: number): ParsedTable {
587
+ const next = cloneParsedTable(parsed);
588
+ const bodyIndex = bodyIndexFromRowIndex(rowIndex);
589
+ if (bodyIndex < 0 || bodyIndex >= next.rows.length) {
590
+ return next;
591
+ }
592
+ if (next.rows.length <= 1) {
593
+ next.rows[0] = buildEmptyRow(next.headers.length);
594
+ return next;
595
+ }
596
+ next.rows.splice(bodyIndex, 1);
597
+ return next;
598
+ }
599
+
600
+ export function insertTableColumn(parsed: ParsedTable, columnIndex: number, side: HorizontalSide): ParsedTable {
601
+ const next = cloneParsedTable(parsed);
602
+ const currentIndex = Math.max(0, Math.min(columnArrayIndex(columnIndex), next.headers.length));
603
+ const insertAt = side === "left" ? currentIndex : currentIndex + 1;
604
+ const safeInsertAt = Math.max(0, Math.min(insertAt, next.headers.length));
605
+ next.headers.splice(safeInsertAt, 0, "");
606
+ next.alignments.splice(safeInsertAt, 0, "left");
607
+ for (const row of next.rows) {
608
+ row.splice(safeInsertAt, 0, "");
609
+ }
610
+ return next;
611
+ }
612
+
613
+ export function moveTableColumn(
614
+ parsed: ParsedTable,
615
+ columnIndex: number,
616
+ direction: Extract<MoveDirection, "left" | "right">
617
+ ): ParsedTable {
618
+ const next = cloneParsedTable(parsed);
619
+ const currentIndex = columnArrayIndex(columnIndex);
620
+ const targetIndex = direction === "left" ? currentIndex - 1 : currentIndex + 1;
621
+ if (currentIndex < 0 || currentIndex >= next.headers.length || targetIndex < 0 || targetIndex >= next.headers.length) {
622
+ return next;
623
+ }
624
+ swapArrayItems(next.headers, currentIndex, targetIndex);
625
+ swapArrayItems(next.alignments, currentIndex, targetIndex);
626
+ for (const row of next.rows) {
627
+ swapArrayItems(row, currentIndex, targetIndex);
628
+ }
629
+ return next;
630
+ }
631
+
632
+ export function copyTableColumn(parsed: ParsedTable, columnIndex: number): ParsedTable {
633
+ const next = cloneParsedTable(parsed);
634
+ const currentIndex = columnArrayIndex(columnIndex);
635
+ if (currentIndex < 0 || currentIndex >= next.headers.length) {
636
+ return next;
637
+ }
638
+ next.headers.splice(currentIndex + 1, 0, next.headers[currentIndex] || "");
639
+ next.alignments.splice(currentIndex + 1, 0, next.alignments[currentIndex] || "left");
640
+ for (const row of next.rows) {
641
+ row.splice(currentIndex + 1, 0, row[currentIndex] || "");
642
+ }
643
+ return next;
644
+ }
645
+
646
+ export function deleteTableColumn(parsed: ParsedTable, columnIndex: number): ParsedTable {
647
+ const next = cloneParsedTable(parsed);
648
+ if (next.headers.length <= 1) {
649
+ return next;
650
+ }
651
+ const currentIndex = columnArrayIndex(columnIndex);
652
+ if (currentIndex < 0 || currentIndex >= next.headers.length) {
653
+ return next;
654
+ }
655
+ next.headers.splice(currentIndex, 1);
656
+ next.alignments.splice(currentIndex, 1);
657
+ for (const row of next.rows) {
658
+ row.splice(currentIndex, 1);
659
+ }
660
+ return next;
661
+ }