@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.
- package/README.md +19 -0
- package/asset-repository.js +7 -2
- package/config.js +2 -1
- package/package.json +1 -1
- package/templates/mystery-v1/authoring-view-preference.js +22 -5
- package/templates/mystery-v1/character-perspective-preview.js +3 -3
- package/templates/mystery-v1/codemirror6-runtime.min.js +28 -0
- package/templates/mystery-v1/component-asset-operations.js +19 -6
- package/templates/mystery-v1/component-catalog-contract.js +26 -35
- package/templates/mystery-v1/component-draft-operations.js +19 -8
- package/templates/mystery-v1/component-field-contracts.js +127 -51
- package/templates/mystery-v1/component-id-policy.js +1 -1
- package/templates/mystery-v1/component-manager.js +21 -11
- package/templates/mystery-v1/component-navigation-counts.js +9 -9
- package/templates/mystery-v1/component-registry.js +11 -5
- package/templates/mystery-v1/component-renderers.js +12 -8
- package/templates/mystery-v1/component-storage-contract.js +28 -9
- package/templates/mystery-v1/markdown-document-model.js +444 -0
- package/templates/mystery-v1/markdown-image-editor.js +320 -0
- package/templates/mystery-v1/markdown-live-editor.js +1224 -0
- package/templates/mystery-v1/page-header.js +2 -1
- package/templates/mystery-v1/timeline-model.js +235 -0
- package/tools/writer-cli/package-lock.json +2 -2
- package/tools/writer-cli/package.json +1 -1
- package/tools/writer-cli/skills/ssobig-writer-cli/SKILL.md +13 -14
- package/tools/writer-cli/skills/ssobig-writer-cli/references/assets-checkpoints.md +5 -1
- package/tools/writer-cli/skills/ssobig-writer-cli/references/errors.md +4 -1
- package/tools/writer-cli/skills/ssobig-writer-cli/references/install-auth.md +3 -3
- package/tools/writer-cli/skills/ssobig-writer-cli/references/investigation-board.md +9 -0
- package/tools/writer-cli/skills/ssobig-writer-cli/references/layout-spec.md +130 -0
- package/tools/writer-cli/skills/ssobig-writer-cli/references/projects-components.md +8 -2
- package/tools/writer-cli/skills/ssobig-writer-cli/references/read-search.md +12 -2
- package/tools/writer-cli/src/agent-service.cjs +3 -1
- package/tools/writer-cli/src/command-registry.cjs +24 -20
- package/tools/writer-cli/src/commands.cjs +106 -3
- package/tools/writer-cli/src/domain.cjs +296 -1
- package/tools/writer-cli/src/gateway.cjs +14 -0
- package/tools/writer-cli/src/mutations.cjs +167 -1
- 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
|
+
});
|