@ssobig/writer-cli 0.3.0 → 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 +11 -4
- package/asset-repository.js +7 -2
- package/package.json +1 -1
- package/templates/mystery-v1/codemirror6-runtime.min.js +1 -1
- package/templates/mystery-v1/component-asset-operations.js +14 -2
- package/templates/mystery-v1/component-field-contracts.js +98 -2
- package/templates/mystery-v1/component-navigation-counts.js +6 -1
- 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 +979 -105
- 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 +12 -13
- 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 +7 -1
- package/tools/writer-cli/skills/ssobig-writer-cli/references/read-search.md +12 -2
- package/tools/writer-cli/src/command-registry.cjs +24 -23
- package/tools/writer-cli/src/commands.cjs +22 -1
- package/tools/writer-cli/src/domain.cjs +46 -0
- package/tools/writer-cli/src/project-import.cjs +15 -1
|
@@ -1,25 +1,94 @@
|
|
|
1
1
|
(function (root, factory) {
|
|
2
|
-
const
|
|
3
|
-
|
|
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;
|
|
4
7
|
if (root) root.WriterMarkdownLiveEditor = api;
|
|
5
|
-
})(typeof globalThis !== "undefined" ? globalThis : this, function (CodeMirror6) {
|
|
8
|
+
})(typeof globalThis !== "undefined" ? globalThis : this, function (CodeMirror6, markdownDocumentModel) {
|
|
6
9
|
"use strict";
|
|
7
10
|
|
|
8
11
|
const INLINE_MARKERS = Object.freeze([
|
|
9
12
|
{ marker: "++", type: "brand" },
|
|
10
13
|
{ marker: "**", type: "strong" },
|
|
11
14
|
{ marker: "~~", type: "strikethrough" },
|
|
12
|
-
{ marker: "==", type: "highlight" }
|
|
13
|
-
{ marker: "`", type: "code" }
|
|
15
|
+
{ marker: "==", type: "highlight" }
|
|
14
16
|
]);
|
|
15
|
-
function
|
|
16
|
-
|
|
17
|
-
for (let
|
|
18
|
-
|
|
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;
|
|
19
56
|
}
|
|
20
|
-
return
|
|
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;
|
|
21
91
|
}
|
|
22
|
-
|
|
23
92
|
function lineAt(starts, offset) {
|
|
24
93
|
let low = 0;
|
|
25
94
|
let high = starts.length - 1;
|
|
@@ -35,6 +104,14 @@
|
|
|
35
104
|
const tokens = [];
|
|
36
105
|
let index = 0;
|
|
37
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
|
+
}
|
|
38
115
|
const definition = INLINE_MARKERS.find(item => source.startsWith(item.marker, index));
|
|
39
116
|
if (!definition || (index > 0 && source[index - 1] === "\\")) {
|
|
40
117
|
index += 1;
|
|
@@ -78,29 +155,49 @@
|
|
|
78
155
|
: selection.to >= from && selection.from <= to;
|
|
79
156
|
}
|
|
80
157
|
|
|
81
|
-
function
|
|
82
|
-
const heading = text.match(/^( {0,3})(#{1,4})(?:[ \t]+|$)/);
|
|
83
|
-
const quote = text.match(/^( {0,3})>[ \t]?/);
|
|
84
|
-
const unordered = text.match(/^([-*])[ \t]+(.+)$/);
|
|
85
|
-
const ordered = text.match(/^(\d+[.)])[ \t]+(.+)$/);
|
|
158
|
+
function offsetInlineToken(token, offset) {
|
|
86
159
|
return {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
|
94
167
|
};
|
|
95
168
|
}
|
|
96
169
|
|
|
97
|
-
function
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
from
|
|
102
|
-
|
|
103
|
-
|
|
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,
|
|
104
201
|
blockId: -1,
|
|
105
202
|
blockType: "",
|
|
106
203
|
blockStart: false,
|
|
@@ -109,83 +206,267 @@
|
|
|
109
206
|
firstListItem: false
|
|
110
207
|
}));
|
|
111
208
|
const blocks = [];
|
|
112
|
-
let paragraph = null;
|
|
113
|
-
let quote = null;
|
|
114
|
-
let listType = "";
|
|
115
|
-
let listGroup = 0;
|
|
116
209
|
let nextBlockId = 0;
|
|
117
|
-
|
|
118
|
-
|
|
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 };
|
|
119
215
|
blocks.push(block);
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
+
});
|
|
124
224
|
return block;
|
|
125
225
|
};
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
lines.forEach(line => {
|
|
133
|
-
if (line.quoteMarkerFrom >= 0) {
|
|
134
|
-
paragraph = null;
|
|
135
|
-
const nestedQuote = Boolean(listType);
|
|
136
|
-
if (!quote || quote.nestedQuote !== nestedQuote || quote.listGroup !== listGroup) {
|
|
137
|
-
quote = openBlock("quote", line, { nestedQuote, listGroup: nestedQuote ? listGroup : 0 });
|
|
138
|
-
} else appendBlock(quote, line);
|
|
139
|
-
line.nestedQuote = nestedQuote;
|
|
226
|
+
documentModel.blocks.forEach(sourceBlock => {
|
|
227
|
+
if (sourceBlock.type === "blank") return;
|
|
228
|
+
if (sourceBlock.type === "image") {
|
|
229
|
+
assignBlock("image", [sourceBlock.lineIndex], { assetId: sourceBlock.assetId });
|
|
140
230
|
return;
|
|
141
231
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
paragraph = null;
|
|
145
|
-
listType = "";
|
|
232
|
+
if (sourceBlock.type === "heading") {
|
|
233
|
+
assignBlock(`heading${sourceBlock.level}`, [sourceBlock.lineIndex]);
|
|
146
234
|
return;
|
|
147
235
|
}
|
|
148
|
-
if (
|
|
149
|
-
|
|
150
|
-
listType = "";
|
|
151
|
-
openBlock(`heading${line.headingLevel}`, line);
|
|
236
|
+
if (sourceBlock.type === "paragraph" || sourceBlock.type === "quote") {
|
|
237
|
+
assignBlock(sourceBlock.type, sourceBlock.lineIndices);
|
|
152
238
|
return;
|
|
153
239
|
}
|
|
154
|
-
if (
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
+
});
|
|
161
267
|
return;
|
|
162
268
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
+
});
|
|
166
285
|
});
|
|
167
286
|
|
|
168
|
-
blocks
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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 };
|
|
181
305
|
});
|
|
306
|
+
}
|
|
182
307
|
|
|
183
|
-
|
|
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;
|
|
184
465
|
}
|
|
185
466
|
|
|
186
467
|
function previewModel(value, selections = [{ anchor: 0, head: 0 }]) {
|
|
187
468
|
const source = String(value ?? "").replace(/\r\n?/g, "\n");
|
|
188
|
-
const starts = lineStarts(source);
|
|
469
|
+
const starts = markdownDocumentModel.lineStarts(source);
|
|
189
470
|
const normalizedSelections = normalizeSelections(selections, source.length);
|
|
190
471
|
const activeLines = new Set();
|
|
191
472
|
normalizedSelections.forEach(selection => {
|
|
@@ -193,12 +474,34 @@
|
|
|
193
474
|
const last = lineAt(starts, selection.to);
|
|
194
475
|
for (let line = first; line <= last; line += 1) activeLines.add(line);
|
|
195
476
|
});
|
|
196
|
-
const layout = layoutLines(source
|
|
477
|
+
const layout = layoutLines(source);
|
|
197
478
|
layout.lines.forEach(line => { line.active = activeLines.has(line.index); });
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
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
|
+
))
|
|
201
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
|
+
}));
|
|
202
505
|
return { source, starts, lines: layout.lines, blocks: layout.blocks, tokens };
|
|
203
506
|
}
|
|
204
507
|
|
|
@@ -215,8 +518,17 @@
|
|
|
215
518
|
.toLowerCase();
|
|
216
519
|
values.push(`writer-md-${blockClass}`);
|
|
217
520
|
}
|
|
521
|
+
if (line.listType) values.push(`writer-md-list-${line.listType}`);
|
|
218
522
|
if (line.nestedQuote) values.push("writer-md-nested-quote");
|
|
219
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
|
+
}
|
|
220
532
|
return values.join(" ");
|
|
221
533
|
}
|
|
222
534
|
|
|
@@ -228,7 +540,25 @@
|
|
|
228
540
|
if (!options.parent) throw new Error("Markdown 편집기 표시 영역이 없습니다.");
|
|
229
541
|
let destroyed = false;
|
|
230
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
|
+
});
|
|
231
560
|
const focusEffect = cm.StateEffect.define();
|
|
561
|
+
const syntheticCellMaterializationEffect = cm.StateEffect.define();
|
|
232
562
|
const focusField = cm.StateField.define({
|
|
233
563
|
create() { return false; },
|
|
234
564
|
update(focused, transaction) {
|
|
@@ -248,7 +578,57 @@
|
|
|
248
578
|
class: active ? "writer-md-syntax" : "writer-md-syntax-hidden"
|
|
249
579
|
});
|
|
250
580
|
model.lines.forEach(line => {
|
|
251
|
-
|
|
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
|
+
}
|
|
252
632
|
if (line.headingMarkerFrom >= 0) {
|
|
253
633
|
const from = line.from + line.headingMarkerFrom;
|
|
254
634
|
const to = line.from + line.headingMarkerTo;
|
|
@@ -258,6 +638,22 @@
|
|
|
258
638
|
const to = line.from + line.quoteMarkerTo;
|
|
259
639
|
ranges.push(syntaxDecoration(line.active).range(from, to));
|
|
260
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
|
+
}
|
|
261
657
|
});
|
|
262
658
|
model.tokens.forEach(token => {
|
|
263
659
|
ranges.push(cm.Decoration.mark({ class: `writer-md-${token.type}` }).range(token.contentFrom, token.contentTo));
|
|
@@ -277,46 +673,494 @@
|
|
|
277
673
|
},
|
|
278
674
|
provide: field => cm.EditorView.decorations.from(field)
|
|
279
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
|
+
});
|
|
280
1091
|
const state = cm.EditorState.create({
|
|
281
1092
|
doc: String(options.value ?? "").replace(/\r\n?/g, "\n"),
|
|
282
1093
|
extensions: [
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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" }),
|
|
287
1100
|
cm.history(),
|
|
288
1101
|
cm.keymap.of([
|
|
289
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") },
|
|
290
1113
|
...cm.defaultKeymap,
|
|
291
1114
|
...cm.historyKeymap
|
|
292
1115
|
]),
|
|
293
|
-
cm.markdown(),
|
|
1116
|
+
cm.markdown({ addKeymap: false }),
|
|
294
1117
|
cm.EditorView.lineWrapping,
|
|
1118
|
+
tableInputHandler,
|
|
1119
|
+
tableStructureChangeFilter,
|
|
295
1120
|
focusField,
|
|
296
1121
|
previewField,
|
|
1122
|
+
tableAtomicField,
|
|
297
1123
|
cm.EditorView.contentAttributes.of({
|
|
298
1124
|
role: "textbox",
|
|
299
1125
|
"aria-multiline": "true",
|
|
300
1126
|
"aria-label": String(options.ariaLabel || "Markdown 편집")
|
|
301
1127
|
}),
|
|
302
1128
|
cm.EditorView.updateListener.of(update => {
|
|
303
|
-
if (update.docChanged) options.onChange?.(update.state.doc.toString());
|
|
1129
|
+
if (update.docChanged && !replacingValue) options.onChange?.(update.state.doc.toString());
|
|
304
1130
|
}),
|
|
305
1131
|
cm.EditorView.domEventHandlers({
|
|
1132
|
+
mousedown: handleTableCellMouseDown,
|
|
1133
|
+
mouseup: handleTableCellMouseUp,
|
|
1134
|
+
beforeinput: handleTableBeforeInput,
|
|
1135
|
+
paste: handleTablePaste,
|
|
306
1136
|
focus() {
|
|
307
|
-
view.dispatch({ effects: focusEffect.of(true) });
|
|
1137
|
+
if (!view.state.readOnly) view.dispatch({ effects: focusEffect.of(true) });
|
|
308
1138
|
},
|
|
309
1139
|
blur() {
|
|
310
1140
|
view.dispatch({ effects: focusEffect.of(false) });
|
|
311
1141
|
setTimeout(() => {
|
|
312
|
-
if (!destroyed && !
|
|
1142
|
+
if (!destroyed && !images?.hasPending() && !options.parent.contains(ownerDocument.activeElement)) options.onBlur?.();
|
|
313
1143
|
}, 0);
|
|
314
1144
|
}
|
|
315
1145
|
})
|
|
316
1146
|
]
|
|
317
1147
|
});
|
|
318
1148
|
const view = new cm.EditorView({ state, parent: options.parent });
|
|
1149
|
+
images?.attach(view);
|
|
1150
|
+
options.parent.toggleAttribute("data-markdown-readonly", view.state.readOnly);
|
|
319
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,
|
|
320
1164
|
getValue: () => view.state.doc.toString(),
|
|
321
1165
|
getSelection: () => view.state.selection.ranges.map(range => ({ anchor: range.anchor, head: range.head })),
|
|
322
1166
|
focusAtPosition(position, options = {}) {
|
|
@@ -330,21 +1174,51 @@
|
|
|
330
1174
|
view.focus();
|
|
331
1175
|
},
|
|
332
1176
|
focusAtClientPoint(clientX, clientY) {
|
|
333
|
-
const
|
|
334
|
-
? view.posAtCoords({ x: clientX, y: clientY }, false)
|
|
335
|
-
: null;
|
|
336
|
-
const anchor = position == null ? view.state.doc.length : position;
|
|
1177
|
+
const anchor = this.positionAtClientPoint(clientX, clientY);
|
|
337
1178
|
view.dispatch({ selection: { anchor } });
|
|
338
1179
|
view.focus();
|
|
339
1180
|
return anchor;
|
|
340
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
|
+
},
|
|
341
1198
|
destroy() {
|
|
342
1199
|
if (destroyed) return;
|
|
343
1200
|
destroyed = true;
|
|
1201
|
+
options.parent.removeAttribute("data-markdown-readonly");
|
|
1202
|
+
images?.destroy();
|
|
344
1203
|
view.destroy();
|
|
345
1204
|
}
|
|
346
1205
|
});
|
|
347
1206
|
}
|
|
348
1207
|
|
|
349
|
-
return Object.freeze({
|
|
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
|
+
});
|
|
350
1224
|
});
|