@ssobig/writer-cli 0.2.2 → 0.3.2

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 (39) hide show
  1. package/README.md +19 -0
  2. package/asset-repository.js +7 -2
  3. package/config.js +2 -1
  4. package/package.json +1 -1
  5. package/templates/mystery-v1/authoring-view-preference.js +22 -5
  6. package/templates/mystery-v1/character-perspective-preview.js +3 -3
  7. package/templates/mystery-v1/codemirror6-runtime.min.js +28 -0
  8. package/templates/mystery-v1/component-asset-operations.js +19 -6
  9. package/templates/mystery-v1/component-catalog-contract.js +26 -35
  10. package/templates/mystery-v1/component-draft-operations.js +19 -8
  11. package/templates/mystery-v1/component-field-contracts.js +127 -51
  12. package/templates/mystery-v1/component-id-policy.js +1 -1
  13. package/templates/mystery-v1/component-manager.js +21 -11
  14. package/templates/mystery-v1/component-navigation-counts.js +9 -9
  15. package/templates/mystery-v1/component-registry.js +11 -5
  16. package/templates/mystery-v1/component-renderers.js +12 -8
  17. package/templates/mystery-v1/component-storage-contract.js +28 -9
  18. package/templates/mystery-v1/markdown-document-model.js +444 -0
  19. package/templates/mystery-v1/markdown-image-editor.js +320 -0
  20. package/templates/mystery-v1/markdown-live-editor.js +1224 -0
  21. package/templates/mystery-v1/page-header.js +2 -1
  22. package/templates/mystery-v1/timeline-model.js +235 -0
  23. package/tools/writer-cli/package-lock.json +2 -2
  24. package/tools/writer-cli/package.json +1 -1
  25. package/tools/writer-cli/skills/ssobig-writer-cli/SKILL.md +13 -14
  26. package/tools/writer-cli/skills/ssobig-writer-cli/references/assets-checkpoints.md +5 -1
  27. package/tools/writer-cli/skills/ssobig-writer-cli/references/errors.md +4 -1
  28. package/tools/writer-cli/skills/ssobig-writer-cli/references/install-auth.md +3 -3
  29. package/tools/writer-cli/skills/ssobig-writer-cli/references/investigation-board.md +9 -0
  30. package/tools/writer-cli/skills/ssobig-writer-cli/references/layout-spec.md +130 -0
  31. package/tools/writer-cli/skills/ssobig-writer-cli/references/projects-components.md +8 -2
  32. package/tools/writer-cli/skills/ssobig-writer-cli/references/read-search.md +12 -2
  33. package/tools/writer-cli/src/agent-service.cjs +3 -1
  34. package/tools/writer-cli/src/command-registry.cjs +24 -20
  35. package/tools/writer-cli/src/commands.cjs +106 -3
  36. package/tools/writer-cli/src/domain.cjs +296 -1
  37. package/tools/writer-cli/src/gateway.cjs +14 -0
  38. package/tools/writer-cli/src/mutations.cjs +167 -1
  39. package/tools/writer-cli/src/project-import.cjs +27 -22
@@ -0,0 +1,1224 @@
1
+ (function (root, factory) {
2
+ const commonJs = typeof module === "object" && module.exports;
3
+ const CodeMirror6 = commonJs ? null : root?.WriterCodeMirror6;
4
+ const markdownDocumentModel = commonJs ? require("./markdown-document-model.js") : root?.WriterMarkdownDocumentModel;
5
+ const api = factory(CodeMirror6, markdownDocumentModel);
6
+ if (commonJs) module.exports = api;
7
+ if (root) root.WriterMarkdownLiveEditor = api;
8
+ })(typeof globalThis !== "undefined" ? globalThis : this, function (CodeMirror6, markdownDocumentModel) {
9
+ "use strict";
10
+
11
+ const INLINE_MARKERS = Object.freeze([
12
+ { marker: "++", type: "brand" },
13
+ { marker: "**", type: "strong" },
14
+ { marker: "~~", type: "strikethrough" },
15
+ { marker: "==", type: "highlight" }
16
+ ]);
17
+ function isEscapedAt(source, index) {
18
+ let backslashes = 0;
19
+ for (let cursor = index - 1; cursor >= 0 && source[cursor] === "\\"; cursor -= 1) backslashes += 1;
20
+ return backslashes % 2 === 1;
21
+ }
22
+ function backtickRunLength(source, index) {
23
+ let cursor = index;
24
+ while (source[cursor] === "`") cursor += 1;
25
+ return cursor - index;
26
+ }
27
+ function codeTokenAt(source, index) {
28
+ if (source[index] !== "`" || isEscapedAt(source, index)) return null;
29
+ const markerLength = backtickRunLength(source, index);
30
+ const contentFrom = index + markerLength;
31
+ let cursor = contentFrom;
32
+ while (cursor < source.length) {
33
+ if (source[cursor] !== "`") {
34
+ cursor += 1;
35
+ continue;
36
+ }
37
+ const closeLength = backtickRunLength(source, cursor);
38
+ if (isEscapedAt(source, cursor)) {
39
+ cursor += closeLength;
40
+ continue;
41
+ }
42
+ if (closeLength === markerLength) {
43
+ if (cursor <= contentFrom || /\n[ \t]*\n/.test(source.slice(contentFrom, cursor))) return null;
44
+ return {
45
+ type: "code",
46
+ marker: "`".repeat(markerLength),
47
+ from: index,
48
+ openTo: contentFrom,
49
+ contentFrom,
50
+ contentTo: cursor,
51
+ closeFrom: cursor,
52
+ to: cursor + markerLength
53
+ };
54
+ }
55
+ cursor += closeLength;
56
+ }
57
+ return null;
58
+ }
59
+ function tableEscapedPipeBackslashes(cell) {
60
+ const raw = String(cell?.raw ?? "");
61
+ const rawFrom = Number(cell?.rawFrom);
62
+ if (!Number.isSafeInteger(rawFrom) || !raw) return [];
63
+ const ranges = [];
64
+ let index = 0;
65
+ while (index < raw.length) {
66
+ if (raw[index] !== "\\") {
67
+ index += 1;
68
+ continue;
69
+ }
70
+ let cursor = index;
71
+ while (raw[cursor] === "\\") cursor += 1;
72
+ const count = cursor - index;
73
+ if (raw[cursor] === "|" && count % 2 === 1) {
74
+ const from = rawFrom + cursor - 1;
75
+ ranges.push({ from, to: from + 1 });
76
+ }
77
+ index = cursor + (raw[cursor] === "|" ? 1 : 0);
78
+ }
79
+ return ranges;
80
+ }
81
+ function tableCellEdgeWhitespaceRanges(cell) {
82
+ const rawFrom = Number(cell?.rawFrom);
83
+ const from = Number(cell?.from);
84
+ const to = Number(cell?.to);
85
+ const rawTo = Number(cell?.rawTo);
86
+ if (![rawFrom, from, to, rawTo].every(Number.isSafeInteger)) return [];
87
+ const ranges = [];
88
+ if (rawFrom < from) ranges.push({ from: rawFrom, to: from });
89
+ if (to < rawTo) ranges.push({ from: to, to: rawTo });
90
+ return ranges;
91
+ }
92
+ function lineAt(starts, offset) {
93
+ let low = 0;
94
+ let high = starts.length - 1;
95
+ while (low <= high) {
96
+ const middle = (low + high) >> 1;
97
+ if (starts[middle] <= offset) low = middle + 1;
98
+ else high = middle - 1;
99
+ }
100
+ return Math.max(0, high);
101
+ }
102
+
103
+ function inlineTokens(source) {
104
+ const tokens = [];
105
+ let index = 0;
106
+ while (index < source.length) {
107
+ if (source[index] === "`" && !isEscapedAt(source, index)) {
108
+ const token = codeTokenAt(source, index);
109
+ if (token) {
110
+ tokens.push(token);
111
+ index = token.to;
112
+ } else index += backtickRunLength(source, index);
113
+ continue;
114
+ }
115
+ const definition = INLINE_MARKERS.find(item => source.startsWith(item.marker, index));
116
+ if (!definition || (index > 0 && source[index - 1] === "\\")) {
117
+ index += 1;
118
+ continue;
119
+ }
120
+ const contentFrom = index + definition.marker.length;
121
+ const closeFrom = source.indexOf(definition.marker, contentFrom);
122
+ const crossesBlockBoundary = closeFrom > contentFrom && /\n[ \t]*\n/.test(source.slice(contentFrom, closeFrom));
123
+ if (closeFrom <= contentFrom || crossesBlockBoundary || (closeFrom > 0 && source[closeFrom - 1] === "\\")) {
124
+ index += definition.marker.length;
125
+ continue;
126
+ }
127
+ const to = closeFrom + definition.marker.length;
128
+ tokens.push({
129
+ type: definition.type,
130
+ marker: definition.marker,
131
+ from: index,
132
+ openTo: contentFrom,
133
+ contentFrom,
134
+ contentTo: closeFrom,
135
+ closeFrom,
136
+ to
137
+ });
138
+ index = to;
139
+ }
140
+ return tokens;
141
+ }
142
+
143
+ function normalizeSelections(selections, sourceLength) {
144
+ const values = Array.isArray(selections) ? selections : [{ anchor: 0, head: 0 }];
145
+ return values.map(selection => {
146
+ const anchor = Math.max(0, Math.min(sourceLength, Number(selection?.anchor) || 0));
147
+ const head = Math.max(0, Math.min(sourceLength, Number(selection?.head) || 0));
148
+ return { anchor, head, from: Math.min(anchor, head), to: Math.max(anchor, head) };
149
+ });
150
+ }
151
+
152
+ function selectionTouchesRange(selection, from, to) {
153
+ return selection.from === selection.to
154
+ ? selection.head >= from && selection.head <= to
155
+ : selection.to >= from && selection.from <= to;
156
+ }
157
+
158
+ function offsetInlineToken(token, offset) {
159
+ return {
160
+ ...token,
161
+ from: token.from + offset,
162
+ openTo: token.openTo + offset,
163
+ contentFrom: token.contentFrom + offset,
164
+ contentTo: token.contentTo + offset,
165
+ closeFrom: token.closeFrom + offset,
166
+ to: token.to + offset
167
+ };
168
+ }
169
+
170
+ function tableAwareInlineTokens(source, tableContexts) {
171
+ const tokens = [];
172
+ let cursor = 0;
173
+ [...tableContexts].sort((left, right) => left.from - right.from).forEach(table => {
174
+ if (cursor < table.from) {
175
+ tokens.push(...inlineTokens(source.slice(cursor, table.from)).map(token => offsetInlineToken(token, cursor)));
176
+ }
177
+ [...table.cells].sort((left, right) => left.from - right.from).forEach(cell => {
178
+ tokens.push(...inlineTokens(source.slice(cell.from, cell.to)).map(token => ({
179
+ ...offsetInlineToken(token, cell.from),
180
+ tableCell: true
181
+ })));
182
+ });
183
+ cursor = Math.max(cursor, table.to);
184
+ });
185
+ if (cursor < source.length) {
186
+ tokens.push(...inlineTokens(source.slice(cursor)).map(token => offsetInlineToken(token, cursor)));
187
+ }
188
+ return tokens.sort((left, right) => left.from - right.from || left.to - right.to);
189
+ }
190
+
191
+ function lineSyntax(text) {
192
+ if (!markdownDocumentModel?.parseLine) throw new Error("Markdown 문서 모델을 초기화하지 못했습니다.");
193
+ return markdownDocumentModel.parseLine(text);
194
+ }
195
+
196
+ function layoutLines(source) {
197
+ if (!markdownDocumentModel?.parseDocument) throw new Error("Markdown 문서 모델을 초기화하지 못했습니다.");
198
+ const documentModel = markdownDocumentModel.parseDocument(source, { preserveBlankLines: true });
199
+ const lines = documentModel.lines.map(line => ({
200
+ ...line,
201
+ blockId: -1,
202
+ blockType: "",
203
+ blockStart: false,
204
+ blockEnd: false,
205
+ nestedQuote: false,
206
+ firstListItem: false
207
+ }));
208
+ const blocks = [];
209
+ let nextBlockId = 0;
210
+ let nextListGroup = 0;
211
+ const assignBlock = (type, lineIndices, extra = {}) => {
212
+ const validLineIndices = lineIndices.filter(index => lines[index]);
213
+ if (!validLineIndices.length) return null;
214
+ const block = { id: nextBlockId++, type, lines: validLineIndices, ...extra };
215
+ blocks.push(block);
216
+ validLineIndices.forEach((lineIndex, index) => {
217
+ const line = lines[lineIndex];
218
+ line.blockId = block.id;
219
+ line.blockType = type;
220
+ line.blockStart = index === 0;
221
+ line.blockEnd = index === validLineIndices.length - 1;
222
+ Object.assign(line, extra);
223
+ });
224
+ return block;
225
+ };
226
+ documentModel.blocks.forEach(sourceBlock => {
227
+ if (sourceBlock.type === "blank") return;
228
+ if (sourceBlock.type === "image") {
229
+ assignBlock("image", [sourceBlock.lineIndex], { assetId: sourceBlock.assetId });
230
+ return;
231
+ }
232
+ if (sourceBlock.type === "heading") {
233
+ assignBlock(`heading${sourceBlock.level}`, [sourceBlock.lineIndex]);
234
+ return;
235
+ }
236
+ if (sourceBlock.type === "paragraph" || sourceBlock.type === "quote") {
237
+ assignBlock(sourceBlock.type, sourceBlock.lineIndices);
238
+ return;
239
+ }
240
+ if (sourceBlock.type === "table") {
241
+ const tableBlock = assignBlock("table", sourceBlock.lineIndices, {
242
+ tableColumnCount: sourceBlock.columnCount,
243
+ tableAlignments: sourceBlock.alignments,
244
+ tableFrom: sourceBlock.from,
245
+ tableTo: sourceBlock.to
246
+ });
247
+ if (!tableBlock) return;
248
+ Object.assign(tableBlock, {
249
+ from: sourceBlock.from,
250
+ to: sourceBlock.to,
251
+ columnCount: sourceBlock.columnCount,
252
+ alignments: sourceBlock.alignments
253
+ });
254
+ [sourceBlock.header, sourceBlock.delimiter, ...sourceBlock.rows].forEach(row => {
255
+ const line = lines[row.lineIndex];
256
+ if (!line) return;
257
+ line.tableRole = row.role;
258
+ line.tableCells = row.cells;
259
+ line.tablePipes = row.pipes;
260
+ line.tableRaw = row.raw;
261
+ line.tableHasLeadingPipe = row.hasLeadingPipe;
262
+ line.tableHasTrailingPipe = row.hasTrailingPipe;
263
+ line.tableEscapedPipeBackslashes = row.cells.flatMap(tableEscapedPipeBackslashes);
264
+ line.tableEmptyCellColumns = row.cells.filter(cell => !cell.synthetic && !cell.text).map(cell => cell.columnIndex);
265
+ line.tableSyntheticCellColumns = row.cells.filter(cell => cell.synthetic).map(cell => cell.columnIndex);
266
+ });
267
+ return;
268
+ }
269
+ if (sourceBlock.type !== "list") return;
270
+ const listGroup = ++nextListGroup;
271
+ const markerWidth = Math.max(1, Number(sourceBlock.markerWidth) || 1);
272
+ sourceBlock.items.forEach((item, itemIndex) => {
273
+ const itemBlock = assignBlock("listItem", [item.lineIndex], {
274
+ listGroup,
275
+ listMarkerWidth: markerWidth,
276
+ firstListItem: itemIndex === 0,
277
+ listEnd: itemIndex === sourceBlock.items.length - 1
278
+ });
279
+ item.children.forEach(child => {
280
+ if (child.type !== "quote") return;
281
+ assignBlock("quote", child.lineIndices, { nestedQuote: true, listGroup });
282
+ });
283
+ if (itemBlock) itemBlock.children = item.children;
284
+ });
285
+ });
286
+
287
+ return { lines, blocks };
288
+ }
289
+
290
+ function tableCellContexts(value) {
291
+ const source = String(value ?? "").replace(/\r\n?/g, "\n");
292
+ const layout = layoutLines(source);
293
+ return layout.blocks.filter(block => block.type === "table").map(block => {
294
+ const rows = block.lines
295
+ .map(lineIndex => layout.lines[lineIndex])
296
+ .filter(line => line && line.tableRole !== "delimiter")
297
+ .map((line, rowIndex) => ({
298
+ line,
299
+ rowIndex,
300
+ cells: line.tableCells.map(cell => ({ cell, line, rowIndex, columnIndex: cell.columnIndex }))
301
+ }));
302
+ const cells = rows.flatMap(row => row.cells);
303
+ const lines = block.lines.map(lineIndex => layout.lines[lineIndex]).filter(Boolean);
304
+ return { block, lines, rows, cells };
305
+ });
306
+ }
307
+
308
+ function tableCellContextFromTables(tables, position) {
309
+ const offset = Math.max(0, Number(position) || 0);
310
+ for (const table of tables) {
311
+ for (const context of table.cells) {
312
+ const { cell, line, columnIndex } = context;
313
+ const firstCell = columnIndex === 0;
314
+ const lastCell = columnIndex === line.tableCells.length - 1;
315
+ const touchesContent = offset >= cell.rawFrom && offset <= cell.rawTo;
316
+ const touchesLeadingPipe = firstCell && line.tablePipes?.[0]?.from === offset;
317
+ const trailingPipe = line.tablePipes?.at(-1);
318
+ const touchesTrailingPipe = lastCell && (
319
+ trailingPipe?.from === offset || trailingPipe?.to === offset
320
+ );
321
+ if (touchesContent || touchesLeadingPipe || touchesTrailingPipe) return { ...context, table };
322
+ }
323
+ }
324
+ return null;
325
+ }
326
+
327
+ function tableCellContextAt(value, position) {
328
+ return tableCellContextFromTables(tableCellContexts(value), position);
329
+ }
330
+
331
+ function tableSelectionCrossesStructure(value, selection) {
332
+ const source = String(value ?? "").replace(/\r\n?/g, "\n");
333
+ const anchor = Math.max(0, Math.min(source.length, Number(selection?.anchor) || 0));
334
+ const head = Math.max(0, Math.min(source.length, Number(selection?.head) || 0));
335
+ const from = Math.min(anchor, head);
336
+ const to = Math.max(anchor, head);
337
+ if (from === to) return false;
338
+ const tables = tableCellContexts(source);
339
+ const start = tableCellContextFromTables(tables, from);
340
+ const end = tableCellContextFromTables(tables, to);
341
+ for (const table of tables) {
342
+ if (to <= table.block.from || from >= table.block.to) continue;
343
+ if (!start || !end) return true;
344
+ const sameTable = start.table.block.from === table.block.from && end.table.block.from === table.block.from;
345
+ const sameCell = start.line.index === end.line.index && start.columnIndex === end.columnIndex;
346
+ const insideRawCell = from >= start.cell.rawFrom && to <= start.cell.rawTo;
347
+ if (!sameTable || !sameCell || !insideRawCell) return true;
348
+ }
349
+ return false;
350
+ }
351
+
352
+ function tableCellNavigationDestination(value, selection, direction) {
353
+ const anchor = Math.max(0, Number(selection?.anchor) || 0);
354
+ const head = Math.max(0, Number(selection?.head) || 0);
355
+ if (anchor !== head) return null;
356
+ const tables = tableCellContexts(value);
357
+ const current = tableCellContextFromTables(tables, head);
358
+ if (!current) return null;
359
+ const { table, cell, rowIndex, columnIndex } = current;
360
+ const cellLength = Math.max(0, cell.to - cell.from);
361
+ const contentOffset = Math.max(0, Math.min(cellLength, head - cell.from));
362
+ const flatIndex = table.cells.findIndex(context => (
363
+ context.line.index === current.line.index && context.columnIndex === columnIndex
364
+ ));
365
+ const destination = (targetContext, position) => targetContext
366
+ ? { position, context: { ...targetContext, table } }
367
+ : null;
368
+ let target = null;
369
+
370
+ if (direction === "left") {
371
+ if (head > cell.from) return null;
372
+ target = table.cells[flatIndex - 1] || null;
373
+ return destination(target, target?.cell.to);
374
+ }
375
+ if (direction === "right") {
376
+ if (head < cell.to) return null;
377
+ target = table.cells[flatIndex + 1] || null;
378
+ return destination(target, target?.cell.from);
379
+ }
380
+ if (direction === "previous") {
381
+ target = table.cells[flatIndex - 1] || null;
382
+ return destination(target, target?.cell.to);
383
+ }
384
+ if (direction === "next") {
385
+ target = table.cells[flatIndex + 1] || null;
386
+ return destination(target, target?.cell.from);
387
+ }
388
+ if (direction === "up" || direction === "down") {
389
+ const rowStep = direction === "up" ? -1 : 1;
390
+ for (let targetRow = rowIndex + rowStep; targetRow >= 0 && targetRow < table.rows.length; targetRow += rowStep) {
391
+ target = table.rows[targetRow].cells.find(context => context.columnIndex === columnIndex) || null;
392
+ if (!target) continue;
393
+ return destination(target, Math.min(target.cell.to, target.cell.from + contentOffset));
394
+ }
395
+ }
396
+ return null;
397
+ }
398
+
399
+ function tableCellNavigationTarget(value, selection, direction) {
400
+ return tableCellNavigationDestination(value, selection, direction)?.position ?? null;
401
+ }
402
+
403
+ function syntheticTableCellMaterialization(context) {
404
+ if (!context?.cell?.synthetic || !context.line?.tableCells) return null;
405
+ const firstSyntheticColumn = context.line.tableCells.findIndex(cell => cell.synthetic);
406
+ if (firstSyntheticColumn < 0 || context.columnIndex < firstSyntheticColumn) return null;
407
+ const pipeCount = context.columnIndex - firstSyntheticColumn + (context.line.tableHasTrailingPipe ? 0 : 1);
408
+ const insert = "|".repeat(Math.max(0, pipeCount));
409
+ return {
410
+ from: context.line.to,
411
+ insert,
412
+ position: context.line.to + insert.length
413
+ };
414
+ }
415
+
416
+ function safeTableCellInput(value, precedingBackslashes = 0) {
417
+ const source = String(value ?? "");
418
+ let result = "";
419
+ let trailingBackslashes = Math.max(0, Number(precedingBackslashes) || 0);
420
+ for (let index = 0; index < source.length; index += 1) {
421
+ const character = source[index];
422
+ if (character === "\r") {
423
+ if (source[index + 1] === "\n") index += 1;
424
+ result += " ";
425
+ trailingBackslashes = 0;
426
+ continue;
427
+ }
428
+ if (character === "\n") {
429
+ result += " ";
430
+ trailingBackslashes = 0;
431
+ continue;
432
+ }
433
+ if (character === "|") {
434
+ if (trailingBackslashes % 2 === 0) result += "\\";
435
+ result += character;
436
+ trailingBackslashes = 0;
437
+ continue;
438
+ }
439
+ result += character;
440
+ trailingBackslashes = character === "\\" ? trailingBackslashes + 1 : 0;
441
+ }
442
+ return result;
443
+ }
444
+
445
+ function safeTableCellReplacement(value, from, to, insertedValue) {
446
+ const source = String(value ?? "");
447
+ const changeFrom = Math.max(0, Math.min(source.length, Number(from) || 0));
448
+ const changeTo = Math.max(changeFrom, Math.min(source.length, Number(to) || 0));
449
+ let precedingBackslashes = 0;
450
+ for (let cursor = changeFrom - 1; cursor >= 0 && source[cursor] === "\\"; cursor -= 1) precedingBackslashes += 1;
451
+ let safeText = safeTableCellInput(insertedValue, precedingBackslashes);
452
+ let suffixBackslashes = 0;
453
+ for (let cursor = changeTo; cursor < source.length && source[cursor] === "\\"; cursor += 1) suffixBackslashes += 1;
454
+ const suffixPipe = changeTo + suffixBackslashes;
455
+ if (source[suffixPipe] !== "|") return safeText;
456
+
457
+ let originalBackslashes = 0;
458
+ for (let cursor = suffixPipe - 1; cursor >= 0 && source[cursor] === "\\"; cursor -= 1) originalBackslashes += 1;
459
+ let insertedBackslashes = 0;
460
+ for (let cursor = safeText.length - 1; cursor >= 0 && safeText[cursor] === "\\"; cursor -= 1) insertedBackslashes += 1;
461
+ const keepsPrecedingRun = safeText.length === insertedBackslashes;
462
+ const resultingBackslashes = suffixBackslashes + insertedBackslashes + (keepsPrecedingRun ? precedingBackslashes : 0);
463
+ if (resultingBackslashes % 2 !== originalBackslashes % 2) safeText += "\\";
464
+ return safeText;
465
+ }
466
+
467
+ function previewModel(value, selections = [{ anchor: 0, head: 0 }]) {
468
+ const source = String(value ?? "").replace(/\r\n?/g, "\n");
469
+ const starts = markdownDocumentModel.lineStarts(source);
470
+ const normalizedSelections = normalizeSelections(selections, source.length);
471
+ const activeLines = new Set();
472
+ normalizedSelections.forEach(selection => {
473
+ const first = lineAt(starts, selection.from);
474
+ const last = lineAt(starts, selection.to);
475
+ for (let line = first; line <= last; line += 1) activeLines.add(line);
476
+ });
477
+ const layout = layoutLines(source);
478
+ layout.lines.forEach(line => { line.active = activeLines.has(line.index); });
479
+ const tableContexts = layout.blocks.filter(block => block.type === "table").map(block => ({
480
+ block,
481
+ from: block.from,
482
+ to: block.to,
483
+ cells: block.lines.flatMap(lineIndex => (
484
+ layout.lines[lineIndex]?.tableCells?.filter(cell => !cell.synthetic) || []
485
+ ))
486
+ }));
487
+ tableContexts.forEach(({ block }) => {
488
+ block.active = normalizedSelections.some(selection => selectionTouchesRange(selection, block.from, block.to));
489
+ block.lines.forEach(lineIndex => {
490
+ const line = layout.lines[lineIndex];
491
+ if (!line) return;
492
+ line.tableActiveCellColumns = line.tableCells
493
+ .filter(cell => !cell.synthetic && normalizedSelections.some(selection => (
494
+ selectionTouchesRange(selection, cell.rawFrom, cell.rawTo)
495
+ )))
496
+ .map(cell => cell.columnIndex);
497
+ });
498
+ });
499
+ const tokens = tableAwareInlineTokens(source, tableContexts)
500
+ .filter(token => !layout.lines.some(line => line.blockType === "image" && token.from < line.to && token.to > line.from))
501
+ .map(token => ({
502
+ ...token,
503
+ active: token.tableCell !== true && normalizedSelections.some(selection => selectionTouchesRange(selection, token.from, token.to))
504
+ }));
505
+ return { source, starts, lines: layout.lines, blocks: layout.blocks, tokens };
506
+ }
507
+
508
+ function classNames(line) {
509
+ const values = ["writer-md-line"];
510
+ if (line.empty) values.push("writer-md-empty-line");
511
+ if (line.active) values.push("writer-md-active-line");
512
+ if (line.blockStart) values.push("writer-md-block-start");
513
+ if (line.blockEnd) values.push("writer-md-block-end");
514
+ if (line.blockType) {
515
+ const blockClass = line.blockType
516
+ .replace(/([a-z])([A-Z])/g, "$1-$2")
517
+ .replace(/(\d+)$/, "-$1")
518
+ .toLowerCase();
519
+ values.push(`writer-md-${blockClass}`);
520
+ }
521
+ if (line.listType) values.push(`writer-md-list-${line.listType}`);
522
+ if (line.nestedQuote) values.push("writer-md-nested-quote");
523
+ if (line.firstListItem) values.push("writer-md-first-list-item");
524
+ if (line.blockType === "table") {
525
+ values.push("writer-md-table-preview-row");
526
+ if (line.tableActiveCellColumns?.length) values.push("writer-md-table-has-active-cell");
527
+ if (line.tableRole) values.push(`writer-md-table-${line.tableRole}-row`);
528
+ if (line.tableColumnCount) values.push(`writer-md-table-columns-${line.tableColumnCount}`);
529
+ if (line.tableEmptyCellColumns?.length) values.push("writer-md-table-has-empty-cells");
530
+ if (line.tableSyntheticCellColumns?.length) values.push("writer-md-table-has-synthetic-cells");
531
+ }
532
+ return values.join(" ");
533
+ }
534
+
535
+ function mount(options = {}) {
536
+ const cm = CodeMirror6;
537
+ if (!cm?.EditorState || !cm?.StateEffect || !cm?.EditorView || !cm?.Decoration || !cm?.StateField) {
538
+ throw new Error("Markdown 편집기를 초기화하지 못했습니다.");
539
+ }
540
+ if (!options.parent) throw new Error("Markdown 편집기 표시 영역이 없습니다.");
541
+ let destroyed = false;
542
+ const ownerDocument = options.parent.ownerDocument || document;
543
+ const editableEffect = cm.StateEffect.define();
544
+ const editableField = cm.StateField.define({
545
+ create: () => options.readOnly !== true,
546
+ update(value, transaction) {
547
+ for (const effect of transaction.effects) if (effect.is(editableEffect)) value = effect.value;
548
+ return value;
549
+ }
550
+ });
551
+ let replacingValue = false;
552
+ const imageEditor = typeof globalThis !== "undefined" ? globalThis.WriterMarkdownImageEditor : null;
553
+ const images = imageEditor?.create({
554
+ cm, model: markdownDocumentModel, assets: options.assets, parent: options.parent,
555
+ onIdle: () => setTimeout(() => {
556
+ if (!destroyed && !images?.hasPending() && !options.parent.contains(ownerDocument.activeElement)) options.onBlur?.();
557
+ }, 0),
558
+ canInsert: (source, from, to) => !markdownDocumentModel.parseDocument(source).blocks.some(block => block.type === "table" && from <= block.to && to >= block.from)
559
+ });
560
+ const focusEffect = cm.StateEffect.define();
561
+ const syntheticCellMaterializationEffect = cm.StateEffect.define();
562
+ const focusField = cm.StateField.define({
563
+ create() { return false; },
564
+ update(focused, transaction) {
565
+ for (const effect of transaction.effects) {
566
+ if (effect.is(focusEffect)) focused = Boolean(effect.value);
567
+ }
568
+ return focused;
569
+ }
570
+ });
571
+ const buildDecorations = state => {
572
+ const selections = state.field(focusField)
573
+ ? state.selection.ranges.map(range => ({ anchor: range.anchor, head: range.head }))
574
+ : [];
575
+ const model = previewModel(state.doc.toString(), selections);
576
+ const ranges = [];
577
+ const syntaxDecoration = active => cm.Decoration.mark({
578
+ class: active ? "writer-md-syntax" : "writer-md-syntax-hidden"
579
+ });
580
+ model.lines.forEach(line => {
581
+ if (line.blockType === "image" && images) return;
582
+ const attributes = { class: classNames(line) };
583
+ if (!state.doc.length && options.placeholder) attributes["data-placeholder"] = String(options.placeholder);
584
+ if (line.listType) attributes.style = `--writer-md-list-marker-width:${line.listMarkerWidth || 1}ch`;
585
+ if (line.blockType === "table") {
586
+ const columnCount = line.tableColumnCount || 1;
587
+ const minimumWidth = Math.max(14, Math.min(72, columnCount * 7));
588
+ const columnWidth = Number((100 / columnCount).toFixed(6));
589
+ attributes.style = `--writer-md-table-column-count:${columnCount};--writer-md-table-column-width:${columnWidth}%;--writer-md-table-min-width:${minimumWidth}em`;
590
+ }
591
+ ranges.push(cm.Decoration.line({ attributes }).range(line.from));
592
+ if (line.blockType === "table") {
593
+ line.tableCells.forEach((cell, columnIndex) => {
594
+ if (cell.rawFrom >= cell.rawTo) return;
595
+ const alignment = ["left", "center", "right"].includes(line.tableAlignments?.[columnIndex])
596
+ ? line.tableAlignments[columnIndex]
597
+ : "left";
598
+ const positionClasses = [
599
+ columnIndex === 0 ? "writer-md-table-cell-first" : "",
600
+ columnIndex === line.tableCells.length - 1 ? "writer-md-table-cell-last" : "",
601
+ line.tableActiveCellColumns?.includes(columnIndex) ? "writer-md-table-cell-active" : ""
602
+ ].filter(Boolean).join(" ");
603
+ ranges.push(cm.Decoration.mark({
604
+ class: `writer-md-table-cell writer-md-table-align-${alignment}${positionClasses ? ` ${positionClasses}` : ""}`,
605
+ attributes: {
606
+ style: `--writer-md-table-cell-column:${columnIndex + 1}`,
607
+ "data-writer-md-table-cell-from": String(cell.from),
608
+ "data-writer-md-table-cell-to": String(cell.to)
609
+ },
610
+ inclusiveEnd: false
611
+ }).range(cell.rawFrom, cell.rawTo));
612
+ tableCellEdgeWhitespaceRanges(cell).forEach(whitespace => {
613
+ ranges.push(cm.Decoration.mark({
614
+ class: "writer-md-table-cell-edge-whitespace",
615
+ inclusiveEnd: false
616
+ }).range(whitespace.from, whitespace.to));
617
+ });
618
+ });
619
+ line.tablePipes.forEach(pipe => {
620
+ ranges.push(cm.Decoration.mark({
621
+ class: "writer-md-table-pipe",
622
+ inclusiveEnd: false
623
+ }).range(pipe.from, pipe.to));
624
+ });
625
+ line.tableEscapedPipeBackslashes.forEach(backslash => {
626
+ ranges.push(cm.Decoration.mark({
627
+ class: "writer-md-table-escaped-pipe-backslash",
628
+ inclusiveEnd: false
629
+ }).range(backslash.from, backslash.to));
630
+ });
631
+ }
632
+ if (line.headingMarkerFrom >= 0) {
633
+ const from = line.from + line.headingMarkerFrom;
634
+ const to = line.from + line.headingMarkerTo;
635
+ ranges.push(syntaxDecoration(line.active).range(from, to));
636
+ } else if (line.quoteMarkerFrom >= 0) {
637
+ const from = line.from + line.quoteMarkerFrom;
638
+ const to = line.from + line.quoteMarkerTo;
639
+ ranges.push(syntaxDecoration(line.active).range(from, to));
640
+ }
641
+ if (line.listIndentTo > 0) {
642
+ const from = line.from;
643
+ const to = line.from + line.listIndentTo;
644
+ ranges.push(cm.Decoration.mark({
645
+ class: "writer-md-list-leading-indent",
646
+ inclusiveEnd: false
647
+ }).range(from, to));
648
+ }
649
+ if (line.listMarkerFrom >= 0) {
650
+ const from = line.from + line.listMarkerFrom;
651
+ const to = line.from + line.listMarkerTo;
652
+ ranges.push(cm.Decoration.mark({
653
+ class: "writer-md-list-marker",
654
+ inclusiveEnd: false
655
+ }).range(from, to));
656
+ }
657
+ });
658
+ model.tokens.forEach(token => {
659
+ ranges.push(cm.Decoration.mark({ class: `writer-md-${token.type}` }).range(token.contentFrom, token.contentTo));
660
+ const markerDecoration = syntaxDecoration(token.active);
661
+ ranges.push(markerDecoration.range(token.from, token.openTo));
662
+ ranges.push(markerDecoration.range(token.closeFrom, token.to));
663
+ });
664
+ return cm.Decoration.set(ranges, true);
665
+ };
666
+ const previewField = cm.StateField.define({
667
+ create(state) {
668
+ return buildDecorations(state);
669
+ },
670
+ update(decorations, transaction) {
671
+ const focusChanged = transaction.effects.some(effect => effect.is(focusEffect));
672
+ return transaction.docChanged || transaction.selection || focusChanged ? buildDecorations(transaction.state) : decorations;
673
+ },
674
+ provide: field => cm.EditorView.decorations.from(field)
675
+ });
676
+ const buildAtomicRanges = state => {
677
+ const model = previewModel(state.doc.toString(), []);
678
+ const ranges = [];
679
+ const atomic = cm.Decoration.mark({});
680
+ model.lines.filter(line => line.blockType === "table").forEach(line => {
681
+ if (line.tableRole === "delimiter") {
682
+ if (line.to > line.from) ranges.push(atomic.range(line.from, line.to));
683
+ return;
684
+ }
685
+ line.tablePipes.forEach(pipe => ranges.push(atomic.range(pipe.from, pipe.to)));
686
+ line.tableCells.forEach(cell => {
687
+ tableCellEdgeWhitespaceRanges(cell).forEach(whitespace => {
688
+ ranges.push(atomic.range(whitespace.from, whitespace.to));
689
+ });
690
+ });
691
+ line.tableEscapedPipeBackslashes.forEach(backslash => ranges.push(atomic.range(backslash.from, backslash.to)));
692
+ });
693
+ model.tokens.filter(token => token.tableCell === true).forEach(token => {
694
+ ranges.push(atomic.range(token.from, token.openTo));
695
+ ranges.push(atomic.range(token.closeFrom, token.to));
696
+ });
697
+ return cm.Decoration.set(ranges, true);
698
+ };
699
+ const tableAtomicField = cm.StateField.define({
700
+ create(state) {
701
+ return buildAtomicRanges(state);
702
+ },
703
+ update(ranges, transaction) {
704
+ return transaction.docChanged ? buildAtomicRanges(transaction.state) : ranges;
705
+ },
706
+ provide: field => cm.EditorView.atomicRanges.from(field, ranges => () => ranges)
707
+ });
708
+ const tableCellPointerPosition = (view, cellElement, event, from, to) => {
709
+ const nativePosition = view.posAtCoords({ x: event.clientX, y: event.clientY });
710
+ const documentRef = cellElement.ownerDocument;
711
+ const walker = documentRef.createTreeWalker(cellElement, 4);
712
+ let closestPosition = null;
713
+ let closestDistance = Number.POSITIVE_INFINITY;
714
+ for (let textNode = walker.nextNode(); textNode; textNode = walker.nextNode()) {
715
+ const parent = textNode.parentElement;
716
+ if (parent?.closest?.(".writer-md-syntax-hidden,.writer-md-table-cell-edge-whitespace,.writer-md-table-escaped-pipe-backslash")) continue;
717
+ for (let offset = 0; offset <= textNode.nodeValue.length; offset += 1) {
718
+ let position = null;
719
+ try {
720
+ position = view.posAtDOM(textNode, offset);
721
+ } catch {
722
+ continue;
723
+ }
724
+ if (!Number.isSafeInteger(position) || position < from || position > to) continue;
725
+ const range = documentRef.createRange();
726
+ range.setStart(textNode, offset);
727
+ range.collapse(true);
728
+ const rect = range.getBoundingClientRect();
729
+ const verticalDistance = event.clientY < rect.top
730
+ ? rect.top - event.clientY
731
+ : event.clientY > rect.bottom
732
+ ? event.clientY - rect.bottom
733
+ : 0;
734
+ const distance = (verticalDistance * 10000) + Math.abs(event.clientX - rect.left);
735
+ if (distance >= closestDistance) continue;
736
+ closestDistance = distance;
737
+ closestPosition = position;
738
+ }
739
+ }
740
+ if (Number.isSafeInteger(closestPosition)) return closestPosition;
741
+ if (Number.isSafeInteger(nativePosition) && nativePosition >= from && nativePosition <= to) {
742
+ return nativePosition;
743
+ }
744
+
745
+ const rect = cellElement.getBoundingClientRect();
746
+ const ratio = rect.width > 0
747
+ ? Math.max(0, Math.min(1, (event.clientX - rect.left) / rect.width))
748
+ : 0;
749
+ return Math.max(from, Math.min(to, from + Math.round((to - from) * ratio)));
750
+ };
751
+ let tablePointerStart = null;
752
+ const handleTableCellMouseDown = (event, view) => {
753
+ tablePointerStart = null;
754
+ if (event.button !== 0 || event.detail !== 1 || event.shiftKey) return false;
755
+ const cellElement = event.target?.closest?.(".writer-md-table-cell");
756
+ if (!cellElement || !view.dom.contains(cellElement)) return false;
757
+ const from = Number(cellElement.getAttribute("data-writer-md-table-cell-from"));
758
+ const to = Number(cellElement.getAttribute("data-writer-md-table-cell-to"));
759
+ if (!Number.isSafeInteger(from) || !Number.isSafeInteger(to) || from > to) return false;
760
+ const position = tableCellPointerPosition(view, cellElement, event, from, to);
761
+ view.focus();
762
+ tablePointerStart = { clientX: event.clientX, clientY: event.clientY, from, to, position };
763
+ return false;
764
+ };
765
+ const tableCellWordSelection = (source, from, to, position) => {
766
+ const text = source.slice(from, to);
767
+ const relativePosition = Math.max(0, Math.min(text.length, position - from));
768
+ if (typeof Intl?.Segmenter === "function") {
769
+ const segments = new Intl.Segmenter(undefined, { granularity: "word" }).segment(text);
770
+ for (const segment of segments) {
771
+ const segmentFrom = segment.index;
772
+ const segmentTo = segment.index + segment.segment.length;
773
+ if (relativePosition < segmentFrom || relativePosition > segmentTo || !segment.segment.trim()) continue;
774
+ return { anchor: from + segmentFrom, head: from + segmentTo };
775
+ }
776
+ }
777
+ let wordFrom = relativePosition;
778
+ let wordTo = relativePosition;
779
+ while (wordFrom > 0 && !/\s/u.test(text[wordFrom - 1])) wordFrom -= 1;
780
+ while (wordTo < text.length && !/\s/u.test(text[wordTo])) wordTo += 1;
781
+ return wordFrom === wordTo
782
+ ? { anchor: Math.max(from, Math.min(to, position)), head: Math.max(from, Math.min(to, position)) }
783
+ : { anchor: from + wordFrom, head: from + wordTo };
784
+ };
785
+ const handleTableCellMouseUp = (event, view) => {
786
+ const start = tablePointerStart;
787
+ tablePointerStart = null;
788
+ if (event.button !== 0 || event.shiftKey) return false;
789
+ if (event.detail === 2) {
790
+ const cellElement = event.target?.closest?.(".writer-md-table-cell");
791
+ if (!cellElement || !view.dom.contains(cellElement)) return false;
792
+ const from = Number(cellElement.getAttribute("data-writer-md-table-cell-from"));
793
+ const to = Number(cellElement.getAttribute("data-writer-md-table-cell-to"));
794
+ if (!Number.isSafeInteger(from) || !Number.isSafeInteger(to) || from > to) return false;
795
+ const position = tableCellPointerPosition(view, cellElement, event, from, to);
796
+ event.preventDefault();
797
+ view.dispatch({
798
+ selection: tableCellWordSelection(view.state.doc.toString(), from, to, position),
799
+ scrollIntoView: true
800
+ });
801
+ return true;
802
+ }
803
+ if (!start || event.detail !== 1) return false;
804
+ if (Math.hypot(event.clientX - start.clientX, event.clientY - start.clientY) > 4) return false;
805
+ const cellElement = event.target?.closest?.(".writer-md-table-cell");
806
+ if (!cellElement || !view.dom.contains(cellElement)) return false;
807
+ const from = Number(cellElement.getAttribute("data-writer-md-table-cell-from"));
808
+ const to = Number(cellElement.getAttribute("data-writer-md-table-cell-to"));
809
+ if (from !== start.from || to !== start.to) return false;
810
+ event.preventDefault();
811
+ view.dispatch({ selection: { anchor: start.position }, scrollIntoView: true });
812
+ return true;
813
+ };
814
+ const tableViewIsComposing = view => Boolean(view.composing || view.compositionStarted);
815
+ const continueListMarkup = cm.insertNewlineContinueMarkupCommand({ nonTightLists: false });
816
+ const moveToTableDestination = (view, destination) => {
817
+ if (!destination || !Number.isSafeInteger(destination.position)) return false;
818
+ const materialization = syntheticTableCellMaterialization(destination.context);
819
+ if (materialization?.insert) {
820
+ view.dispatch({
821
+ changes: {
822
+ from: materialization.from,
823
+ to: materialization.from,
824
+ insert: materialization.insert
825
+ },
826
+ selection: { anchor: materialization.position },
827
+ effects: syntheticCellMaterializationEffect.of({
828
+ tableFrom: destination.context.table.block.from,
829
+ lineIndex: destination.context.line.index,
830
+ columnIndex: destination.context.columnIndex,
831
+ ...materialization
832
+ }),
833
+ userEvent: "input.type",
834
+ scrollIntoView: true
835
+ });
836
+ return true;
837
+ }
838
+ view.dispatch({ selection: { anchor: destination.position }, scrollIntoView: true });
839
+ return true;
840
+ };
841
+ const moveTableCell = direction => view => {
842
+ if (tableViewIsComposing(view)) return false;
843
+ const selection = view.state.selection.main;
844
+ if (!selection.empty) return false;
845
+ const source = view.state.doc.toString();
846
+ const current = tableCellContextAt(source, selection.head);
847
+ const destination = tableCellNavigationDestination(
848
+ source,
849
+ { anchor: selection.anchor, head: selection.head },
850
+ direction
851
+ );
852
+ if (!destination || (destination.position === selection.head && !destination.context.cell.synthetic)) {
853
+ const atProtectedOuterBoundary = current && (
854
+ (direction === "left" && selection.head <= current.cell.from && current.columnIndex === 0 && current.rowIndex === 0)
855
+ || (
856
+ direction === "right"
857
+ && selection.head >= current.cell.to
858
+ && current.rowIndex === current.table.rows.length - 1
859
+ && current.columnIndex === current.table.rows.at(-1)?.cells.at(-1)?.columnIndex
860
+ )
861
+ );
862
+ return Boolean(atProtectedOuterBoundary);
863
+ }
864
+ return moveToTableDestination(view, destination);
865
+ };
866
+ const protectTableBoundary = direction => view => {
867
+ if (tableViewIsComposing(view)) return false;
868
+ const selection = view.state.selection.main;
869
+ const source = view.state.doc.toString();
870
+ if (!selection.empty) {
871
+ return tableSelectionCrossesStructure(source, { anchor: selection.anchor, head: selection.head });
872
+ }
873
+ const current = tableCellContextAt(source, selection.head);
874
+ if (!current) return false;
875
+ const pipeOffset = direction === "backward" ? selection.head - 1 : selection.head;
876
+ const atVisibleBoundary = direction === "backward"
877
+ ? selection.head <= current.cell.from
878
+ : selection.head >= current.cell.to;
879
+ const atStructuralPipe = current.line.tablePipes.some(pipe => pipe.from === pipeOffset);
880
+ if (!atVisibleBoundary && !atStructuralPipe) return false;
881
+ const destination = tableCellNavigationDestination(
882
+ source,
883
+ { anchor: selection.anchor, head: selection.head },
884
+ direction === "backward" ? "previous" : "next"
885
+ );
886
+ if (destination && (
887
+ destination.position !== selection.head || destination.context.cell.synthetic
888
+ )) {
889
+ moveToTableDestination(view, destination);
890
+ }
891
+ return true;
892
+ };
893
+ const enterTableCell = view => {
894
+ if (tableViewIsComposing(view)) return false;
895
+ const selection = view.state.selection.main;
896
+ if (!selection.empty || !tableCellContextAt(view.state.doc.toString(), selection.head)) return false;
897
+ const destination = tableCellNavigationDestination(
898
+ view.state.doc.toString(),
899
+ { anchor: selection.anchor, head: selection.head },
900
+ "down"
901
+ );
902
+ if (destination && (destination.position !== selection.head || destination.context.cell.synthetic)) {
903
+ moveToTableDestination(view, destination);
904
+ }
905
+ return true;
906
+ };
907
+ const handleEnter = view => enterTableCell(view) || continueListMarkup(view);
908
+ const replaceWithSafeTableCellInput = (view, from, to, text, userEvent = "input.type") => {
909
+ if (tableViewIsComposing(view)) return false;
910
+ const source = view.state.doc.toString();
911
+ if (tableSelectionCrossesStructure(source, { anchor: from, head: to })) return true;
912
+ const tables = tableCellContexts(source);
913
+ const start = tableCellContextFromTables(tables, from);
914
+ const end = tableCellContextFromTables(tables, to);
915
+ if (!start || !end || start.line.index !== end.line.index || start.columnIndex !== end.columnIndex) return false;
916
+ const safeText = safeTableCellReplacement(source, from, to, text);
917
+ if (safeText === text) return false;
918
+ view.dispatch({
919
+ changes: { from, to, insert: safeText },
920
+ selection: { anchor: from + safeText.length },
921
+ userEvent,
922
+ scrollIntoView: true
923
+ });
924
+ return true;
925
+ };
926
+ const tableInputHandler = cm.EditorView.inputHandler.of((view, from, to, text) => (
927
+ replaceWithSafeTableCellInput(view, from, to, text)
928
+ ));
929
+ const handleTableBeforeInput = (event, view) => {
930
+ if (event.isComposing || tableViewIsComposing(view)) return false;
931
+ const selection = view.state.selection.main;
932
+ const crossesStructure = tableSelectionCrossesStructure(
933
+ view.state.doc.toString(),
934
+ { anchor: selection.anchor, head: selection.head }
935
+ );
936
+ if (crossesStructure && /^(?:insert|delete)/.test(event.inputType)) {
937
+ event.preventDefault();
938
+ return true;
939
+ }
940
+ if (!["insertText", "insertReplacementText"].includes(event.inputType) || typeof event.data !== "string") {
941
+ return false;
942
+ }
943
+ if (!replaceWithSafeTableCellInput(view, selection.from, selection.to, event.data, "input.type")) return false;
944
+ event.preventDefault();
945
+ return true;
946
+ };
947
+ const handleTablePaste = (event, view) => {
948
+ if (tableViewIsComposing(view)) return false;
949
+ const text = event.clipboardData?.getData("text/plain");
950
+ if (typeof text !== "string") return false;
951
+ const selection = view.state.selection.main;
952
+ if (!replaceWithSafeTableCellInput(view, selection.from, selection.to, text, "input.paste")) return false;
953
+ event.preventDefault();
954
+ return true;
955
+ };
956
+ const validatesSyntheticCellMaterialization = (transaction, tables, request, changes) => {
957
+ if (!request || changes.length !== 1) return false;
958
+ const table = tables.find(candidate => candidate.block.from === request.tableFrom);
959
+ const context = table?.cells.find(candidate => (
960
+ candidate.line.index === request.lineIndex && candidate.columnIndex === request.columnIndex
961
+ ));
962
+ const expected = syntheticTableCellMaterialization(context && { ...context, table });
963
+ const change = changes[0];
964
+ if (
965
+ !expected?.insert
966
+ || request.from !== expected.from
967
+ || request.insert !== expected.insert
968
+ || request.position !== expected.position
969
+ || change.fromA !== expected.from
970
+ || change.toA !== expected.from
971
+ || change.insert !== expected.insert
972
+ ) return false;
973
+
974
+ const nextTable = tableCellContexts(transaction.newDoc.toString())
975
+ .find(candidate => candidate.block.from === table.block.from);
976
+ const nextLine = nextTable?.lines.find(line => line.index === context.line.index);
977
+ const nextTarget = nextLine?.tableCells?.[context.columnIndex];
978
+ if (
979
+ !nextTable
980
+ || nextTable.block.columnCount !== table.block.columnCount
981
+ || nextTable.rows.length !== table.rows.length
982
+ || nextTable.lines.length !== table.lines.length
983
+ || nextTable.block.alignments.join("|") !== table.block.alignments.join("|")
984
+ || !nextTarget?.synthetic
985
+ || nextTarget.from !== expected.position
986
+ || nextLine.tableHasLeadingPipe !== context.line.tableHasLeadingPipe
987
+ || !nextLine.tableHasTrailingPipe
988
+ ) return false;
989
+
990
+ const existingText = context.line.tableCells.filter(cell => !cell.synthetic).map(cell => cell.text);
991
+ const nextText = nextLine.tableCells.slice(0, existingText.length).map(cell => cell.text);
992
+ if (existingText.join("\u0000") !== nextText.join("\u0000")) return false;
993
+ if (nextLine.tableCells.slice(0, context.columnIndex).some(cell => cell.synthetic)) return false;
994
+ const oldPipes = context.line.tablePipes.map(pipe => pipe.from);
995
+ const nextPipes = nextLine.tablePipes.map(pipe => pipe.from);
996
+ const addedPipes = Array.from({ length: expected.insert.length }, (_, index) => expected.from + index);
997
+ return nextPipes.length === oldPipes.length + addedPipes.length
998
+ && oldPipes.every((position, index) => nextPipes[index] === position)
999
+ && addedPipes.every((position, index) => nextPipes[oldPipes.length + index] === position);
1000
+ };
1001
+ const tableStructureChangeFilter = cm.EditorState.changeFilter.of(transaction => {
1002
+ if (!transaction.docChanged) return true;
1003
+ if (transaction.isUserEvent?.("undo") || transaction.isUserEvent?.("redo")) return true;
1004
+ const source = transaction.startState.doc.toString();
1005
+ const tables = tableCellContexts(source);
1006
+ if (!tables.length) return true;
1007
+ const changes = [];
1008
+ transaction.changes.iterChanges((fromA, toA, fromB, toB, inserted) => {
1009
+ changes.push({ fromA, toA, fromB, toB, insert: inserted.toString() });
1010
+ });
1011
+ const materializationEffects = transaction.effects.filter(effect => (
1012
+ effect.is(syntheticCellMaterializationEffect)
1013
+ ));
1014
+ if (materializationEffects.length) {
1015
+ return materializationEffects.length === 1 && validatesSyntheticCellMaterialization(
1016
+ transaction,
1017
+ tables,
1018
+ materializationEffects[0].value,
1019
+ changes
1020
+ );
1021
+ }
1022
+ const touchedTables = tables.filter(table => {
1023
+ const guardFrom = Math.max(0, table.block.from - 1);
1024
+ let guardTo = Math.min(source.length, table.block.to + 1);
1025
+ const nextLineStart = table.block.to + 1;
1026
+ if (source[table.block.to] === "\n" && nextLineStart < source.length && source[nextLineStart] !== "\n") {
1027
+ const nextBreak = source.indexOf("\n", nextLineStart);
1028
+ guardTo = nextBreak < 0 ? source.length : nextBreak;
1029
+ }
1030
+ return changes.some(change => (
1031
+ change.fromA === change.toA
1032
+ ? change.fromA >= guardFrom && change.fromA <= guardTo
1033
+ : change.fromA < guardTo && change.toA > guardFrom
1034
+ ));
1035
+ });
1036
+ if (!touchedTables.length) return true;
1037
+
1038
+ for (const table of touchedTables) {
1039
+ const delimiter = table.lines.find(line => line.tableRole === "delimiter");
1040
+ for (const change of changes) {
1041
+ if (
1042
+ change.fromA === change.toA
1043
+ && delimiter
1044
+ && change.fromA >= delimiter.from
1045
+ && change.fromA <= delimiter.to
1046
+ ) return false;
1047
+ if (change.fromA === change.toA) continue;
1048
+ const structuralRanges = table.lines.flatMap((line, lineIndex) => {
1049
+ const ranges = line.tableRole === "delimiter"
1050
+ ? [{ from: line.from, to: line.to }]
1051
+ : [...line.tablePipes];
1052
+ if (lineIndex < table.lines.length - 1) ranges.push({ from: line.to, to: line.to + 1 });
1053
+ return ranges;
1054
+ });
1055
+ if (table.block.from > 0 && source[table.block.from - 1] === "\n") {
1056
+ structuralRanges.push({ from: table.block.from - 1, to: table.block.from });
1057
+ }
1058
+ if (table.block.to < source.length && source[table.block.to] === "\n") {
1059
+ structuralRanges.push({ from: table.block.to, to: table.block.to + 1 });
1060
+ }
1061
+ if (structuralRanges.some(range => change.fromA < range.to && change.toA > range.from)) {
1062
+ return false;
1063
+ }
1064
+ }
1065
+ }
1066
+
1067
+ const nextTables = tableCellContexts(transaction.newDoc.toString());
1068
+ return touchedTables.every(table => {
1069
+ const mappedFrom = transaction.changes.mapPos(table.block.from, -1);
1070
+ const mappedTo = transaction.changes.mapPos(table.block.to, 1);
1071
+ const nextTable = nextTables.find(candidate => candidate.block.from === mappedFrom);
1072
+ if (!nextTable) return false;
1073
+ if (nextTable.block.to !== mappedTo) return false;
1074
+ if (nextTable.block.columnCount !== table.block.columnCount) return false;
1075
+ if (nextTable.rows.length !== table.rows.length) return false;
1076
+ if (nextTable.lines.length !== table.lines.length) return false;
1077
+ if (nextTable.block.alignments.join("|") !== table.block.alignments.join("|")) return false;
1078
+ return nextTable.lines.every((line, index) => {
1079
+ const previousLine = table.lines[index];
1080
+ if (line.tableRole !== previousLine?.tableRole) return false;
1081
+ if (line.from !== transaction.changes.mapPos(previousLine.from, -1)) return false;
1082
+ if (line.to !== transaction.changes.mapPos(previousLine.to, 1)) return false;
1083
+ if (line.tableHasLeadingPipe !== previousLine.tableHasLeadingPipe) return false;
1084
+ if (line.tableRole === "delimiter" && line.tableRaw !== previousLine.tableRaw) return false;
1085
+ const mappedPipes = previousLine.tablePipes.map(pipe => transaction.changes.mapPos(pipe.from, 1));
1086
+ return mappedPipes.length === line.tablePipes.length
1087
+ && mappedPipes.every((position, pipeIndex) => position === line.tablePipes[pipeIndex]?.from);
1088
+ });
1089
+ });
1090
+ });
1091
+ const state = cm.EditorState.create({
1092
+ doc: String(options.value ?? "").replace(/\r\n?/g, "\n"),
1093
+ extensions: [
1094
+ editableField,
1095
+ cm.EditorState.readOnly.compute([editableField], state => !state.field(editableField)),
1096
+ cm.EditorView.editable.compute([editableField], state => state.field(editableField)),
1097
+ cm.EditorState.changeFilter.of(transaction => !transaction.startState.readOnly),
1098
+ ...(images?.extensions || []),
1099
+ cm.EditorView.editorAttributes.of({ class: "writer-markdown-live-editor" }),
1100
+ cm.history(),
1101
+ cm.keymap.of([
1102
+ { key: "Escape", run() { options.onEscape?.(); return true; } },
1103
+ { key: "Enter", run: handleEnter },
1104
+ { key: "Shift-Enter", run: enterTableCell },
1105
+ { key: "Backspace", run: protectTableBoundary("backward") },
1106
+ { key: "Backspace", run: cm.deleteMarkupBackward },
1107
+ { key: "Delete", run: protectTableBoundary("forward") },
1108
+ { key: "ArrowLeft", run: moveTableCell("left") },
1109
+ { key: "ArrowRight", run: moveTableCell("right") },
1110
+ { key: "ArrowUp", run: moveTableCell("up") },
1111
+ { key: "ArrowDown", run: moveTableCell("down") },
1112
+ { key: "Tab", run: moveTableCell("next"), shift: moveTableCell("previous") },
1113
+ ...cm.defaultKeymap,
1114
+ ...cm.historyKeymap
1115
+ ]),
1116
+ cm.markdown({ addKeymap: false }),
1117
+ cm.EditorView.lineWrapping,
1118
+ tableInputHandler,
1119
+ tableStructureChangeFilter,
1120
+ focusField,
1121
+ previewField,
1122
+ tableAtomicField,
1123
+ cm.EditorView.contentAttributes.of({
1124
+ role: "textbox",
1125
+ "aria-multiline": "true",
1126
+ "aria-label": String(options.ariaLabel || "Markdown 편집")
1127
+ }),
1128
+ cm.EditorView.updateListener.of(update => {
1129
+ if (update.docChanged && !replacingValue) options.onChange?.(update.state.doc.toString());
1130
+ }),
1131
+ cm.EditorView.domEventHandlers({
1132
+ mousedown: handleTableCellMouseDown,
1133
+ mouseup: handleTableCellMouseUp,
1134
+ beforeinput: handleTableBeforeInput,
1135
+ paste: handleTablePaste,
1136
+ focus() {
1137
+ if (!view.state.readOnly) view.dispatch({ effects: focusEffect.of(true) });
1138
+ },
1139
+ blur() {
1140
+ view.dispatch({ effects: focusEffect.of(false) });
1141
+ setTimeout(() => {
1142
+ if (!destroyed && !images?.hasPending() && !options.parent.contains(ownerDocument.activeElement)) options.onBlur?.();
1143
+ }, 0);
1144
+ }
1145
+ })
1146
+ ]
1147
+ });
1148
+ const view = new cm.EditorView({ state, parent: options.parent });
1149
+ images?.attach(view);
1150
+ options.parent.toggleAttribute("data-markdown-readonly", view.state.readOnly);
1151
+ return Object.freeze({
1152
+ setEditable(editable) {
1153
+ view.dispatch({ effects: [editableEffect.of(Boolean(editable)), focusEffect.of(Boolean(editable) && view.hasFocus)] });
1154
+ options.parent.toggleAttribute("data-markdown-readonly", !editable);
1155
+ },
1156
+ setValue(value) {
1157
+ const text = String(value ?? "").replace(/\r\n?/g, "\n");
1158
+ if (text === view.state.doc.toString()) return;
1159
+ replacingValue = true;
1160
+ try { view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: text }, filter: false }); }
1161
+ finally { replacingValue = false; }
1162
+ },
1163
+ hasPendingImages: () => images?.hasPending() || false,
1164
+ getValue: () => view.state.doc.toString(),
1165
+ getSelection: () => view.state.selection.ranges.map(range => ({ anchor: range.anchor, head: range.head })),
1166
+ focusAtPosition(position, options = {}) {
1167
+ const anchor = Math.max(0, Math.min(view.state.doc.length, Number(position) || 0));
1168
+ view.dispatch({ selection: { anchor }, scrollIntoView: options.scrollIntoView !== false });
1169
+ view.focus();
1170
+ return anchor;
1171
+ },
1172
+ focusAtEnd() {
1173
+ view.dispatch({ selection: { anchor: view.state.doc.length }, scrollIntoView: true });
1174
+ view.focus();
1175
+ },
1176
+ focusAtClientPoint(clientX, clientY) {
1177
+ const anchor = this.positionAtClientPoint(clientX, clientY);
1178
+ view.dispatch({ selection: { anchor } });
1179
+ view.focus();
1180
+ return anchor;
1181
+ },
1182
+ positionAtClientPoint(clientX, clientY) {
1183
+ let position = null;
1184
+ if (Number.isFinite(clientX) && Number.isFinite(clientY)) {
1185
+ const cellElement = ownerDocument.elementFromPoint?.(clientX, clientY)?.closest?.(".writer-md-table-cell");
1186
+ if (cellElement && view.dom.contains(cellElement)) {
1187
+ const from = Number(cellElement.getAttribute("data-writer-md-table-cell-from"));
1188
+ const to = Number(cellElement.getAttribute("data-writer-md-table-cell-to"));
1189
+ if (Number.isSafeInteger(from) && Number.isSafeInteger(to) && from <= to) {
1190
+ position = tableCellPointerPosition(view, cellElement, { clientX, clientY }, from, to);
1191
+ }
1192
+ }
1193
+ if (position == null) position = view.posAtCoords({ x: clientX, y: clientY }, false);
1194
+ }
1195
+ const anchor = position == null ? view.state.doc.length : position;
1196
+ return anchor;
1197
+ },
1198
+ destroy() {
1199
+ if (destroyed) return;
1200
+ destroyed = true;
1201
+ options.parent.removeAttribute("data-markdown-readonly");
1202
+ images?.destroy();
1203
+ view.destroy();
1204
+ }
1205
+ });
1206
+ }
1207
+
1208
+ return Object.freeze({
1209
+ inlineTokens,
1210
+ tableEscapedPipeBackslashes,
1211
+ tableCellEdgeWhitespaceRanges,
1212
+ lineSyntax,
1213
+ layoutLines,
1214
+ tableCellContexts,
1215
+ tableCellContextAt,
1216
+ tableSelectionCrossesStructure,
1217
+ tableCellNavigationTarget,
1218
+ syntheticTableCellMaterialization,
1219
+ safeTableCellInput,
1220
+ safeTableCellReplacement,
1221
+ previewModel,
1222
+ mount
1223
+ });
1224
+ });