@ssobig/writer-cli 0.3.0 → 0.3.3
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/authoring-view-preference.js +25 -2
- package/templates/mystery-v1/codemirror6-runtime.min.js +1 -1
- package/templates/mystery-v1/component-asset-operations.js +19 -6
- package/templates/mystery-v1/component-catalog-contract.js +2 -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-renderers.js +1 -1
- package/templates/mystery-v1/component-storage-contract.js +28 -9
- package/templates/mystery-v1/markdown-document-model.js +458 -0
- package/templates/mystery-v1/markdown-image-editor.js +336 -0
- package/templates/mystery-v1/markdown-live-editor.js +1246 -110
- package/templates/mystery-v1/markdown-toolbar.js +514 -0
- 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,99 @@
|
|
|
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 toolbarApi = commonJs ? require("./markdown-toolbar.js") : root?.WriterMarkdownToolbar;
|
|
6
|
+
const api = factory(CodeMirror6, markdownDocumentModel, toolbarApi);
|
|
7
|
+
if (commonJs) module.exports = api;
|
|
4
8
|
if (root) root.WriterMarkdownLiveEditor = api;
|
|
5
|
-
})(typeof globalThis !== "undefined" ? globalThis : this, function (CodeMirror6) {
|
|
9
|
+
})(typeof globalThis !== "undefined" ? globalThis : this, function (CodeMirror6, markdownDocumentModel, toolbarApi) {
|
|
6
10
|
"use strict";
|
|
7
11
|
|
|
8
12
|
const INLINE_MARKERS = Object.freeze([
|
|
13
|
+
{ marker: "<u>", close: "</u>", type: "underline" },
|
|
14
|
+
{ marker: "***", type: "strong-emphasis" },
|
|
9
15
|
{ marker: "++", type: "brand" },
|
|
10
16
|
{ marker: "**", type: "strong" },
|
|
11
17
|
{ marker: "~~", type: "strikethrough" },
|
|
12
18
|
{ marker: "==", type: "highlight" },
|
|
13
|
-
{ marker: "
|
|
19
|
+
{ marker: "*", type: "emphasis" },
|
|
20
|
+
{ marker: "_", type: "emphasis" }
|
|
14
21
|
]);
|
|
15
|
-
function
|
|
16
|
-
|
|
17
|
-
for (let
|
|
18
|
-
|
|
22
|
+
function isEscapedAt(source, index) {
|
|
23
|
+
let backslashes = 0;
|
|
24
|
+
for (let cursor = index - 1; cursor >= 0 && source[cursor] === "\\"; cursor -= 1) backslashes += 1;
|
|
25
|
+
return backslashes % 2 === 1;
|
|
26
|
+
}
|
|
27
|
+
function backtickRunLength(source, index) {
|
|
28
|
+
let cursor = index;
|
|
29
|
+
while (source[cursor] === "`") cursor += 1;
|
|
30
|
+
return cursor - index;
|
|
31
|
+
}
|
|
32
|
+
function codeTokenAt(source, index) {
|
|
33
|
+
if (source[index] !== "`" || isEscapedAt(source, index)) return null;
|
|
34
|
+
const markerLength = backtickRunLength(source, index);
|
|
35
|
+
const contentFrom = index + markerLength;
|
|
36
|
+
let cursor = contentFrom;
|
|
37
|
+
while (cursor < source.length) {
|
|
38
|
+
if (source[cursor] !== "`") {
|
|
39
|
+
cursor += 1;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const closeLength = backtickRunLength(source, cursor);
|
|
43
|
+
if (isEscapedAt(source, cursor)) {
|
|
44
|
+
cursor += closeLength;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (closeLength === markerLength) {
|
|
48
|
+
if (cursor <= contentFrom || /\n[ \t]*\n/.test(source.slice(contentFrom, cursor))) return null;
|
|
49
|
+
return {
|
|
50
|
+
type: "code",
|
|
51
|
+
marker: "`".repeat(markerLength),
|
|
52
|
+
from: index,
|
|
53
|
+
openTo: contentFrom,
|
|
54
|
+
contentFrom,
|
|
55
|
+
contentTo: cursor,
|
|
56
|
+
closeFrom: cursor,
|
|
57
|
+
to: cursor + markerLength
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
cursor += closeLength;
|
|
19
61
|
}
|
|
20
|
-
return
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
function tableEscapedPipeBackslashes(cell) {
|
|
65
|
+
const raw = String(cell?.raw ?? "");
|
|
66
|
+
const rawFrom = Number(cell?.rawFrom);
|
|
67
|
+
if (!Number.isSafeInteger(rawFrom) || !raw) return [];
|
|
68
|
+
const ranges = [];
|
|
69
|
+
let index = 0;
|
|
70
|
+
while (index < raw.length) {
|
|
71
|
+
if (raw[index] !== "\\") {
|
|
72
|
+
index += 1;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
let cursor = index;
|
|
76
|
+
while (raw[cursor] === "\\") cursor += 1;
|
|
77
|
+
const count = cursor - index;
|
|
78
|
+
if (raw[cursor] === "|" && count % 2 === 1) {
|
|
79
|
+
const from = rawFrom + cursor - 1;
|
|
80
|
+
ranges.push({ from, to: from + 1 });
|
|
81
|
+
}
|
|
82
|
+
index = cursor + (raw[cursor] === "|" ? 1 : 0);
|
|
83
|
+
}
|
|
84
|
+
return ranges;
|
|
85
|
+
}
|
|
86
|
+
function tableCellEdgeWhitespaceRanges(cell) {
|
|
87
|
+
const rawFrom = Number(cell?.rawFrom);
|
|
88
|
+
const from = Number(cell?.from);
|
|
89
|
+
const to = Number(cell?.to);
|
|
90
|
+
const rawTo = Number(cell?.rawTo);
|
|
91
|
+
if (![rawFrom, from, to, rawTo].every(Number.isSafeInteger)) return [];
|
|
92
|
+
const ranges = [];
|
|
93
|
+
if (rawFrom < from) ranges.push({ from: rawFrom, to: from });
|
|
94
|
+
if (to < rawTo) ranges.push({ from: to, to: rawTo });
|
|
95
|
+
return ranges;
|
|
21
96
|
}
|
|
22
|
-
|
|
23
97
|
function lineAt(starts, offset) {
|
|
24
98
|
let low = 0;
|
|
25
99
|
let high = starts.length - 1;
|
|
@@ -35,20 +109,34 @@
|
|
|
35
109
|
const tokens = [];
|
|
36
110
|
let index = 0;
|
|
37
111
|
while (index < source.length) {
|
|
112
|
+
if (source[index] === "`" && !isEscapedAt(source, index)) {
|
|
113
|
+
const token = codeTokenAt(source, index);
|
|
114
|
+
if (token) {
|
|
115
|
+
tokens.push(token);
|
|
116
|
+
index = token.to;
|
|
117
|
+
} else index += backtickRunLength(source, index);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
38
120
|
const definition = INLINE_MARKERS.find(item => source.startsWith(item.marker, index));
|
|
39
|
-
if (
|
|
121
|
+
if (definition?.marker === "_" && /[\p{L}\p{N}_]/u.test(source[index - 1] || "")) { index++; continue; }
|
|
122
|
+
if (definition?.marker === "*" && (source[index-1] === "*" || source[index+1] === "*")) { index++; continue; }
|
|
123
|
+
if (!definition || isEscapedAt(source, index)) {
|
|
40
124
|
index += 1;
|
|
41
125
|
continue;
|
|
42
126
|
}
|
|
43
127
|
const contentFrom = index + definition.marker.length;
|
|
44
|
-
const
|
|
128
|
+
const closeMarker = definition.close || definition.marker;
|
|
129
|
+
let closeFrom = source.indexOf(closeMarker, contentFrom);
|
|
130
|
+
while (closeFrom >= 0 && (isEscapedAt(source, closeFrom) || (definition.marker === "_" && /[\p{L}\p{N}_]/u.test(source[closeFrom + 1] || "")))) {
|
|
131
|
+
closeFrom = source.indexOf(closeMarker, closeFrom + closeMarker.length);
|
|
132
|
+
}
|
|
45
133
|
const crossesBlockBoundary = closeFrom > contentFrom && /\n[ \t]*\n/.test(source.slice(contentFrom, closeFrom));
|
|
46
134
|
if (closeFrom <= contentFrom || crossesBlockBoundary || (closeFrom > 0 && source[closeFrom - 1] === "\\")) {
|
|
47
135
|
index += definition.marker.length;
|
|
48
136
|
continue;
|
|
49
137
|
}
|
|
50
|
-
const to = closeFrom +
|
|
51
|
-
|
|
138
|
+
const to = closeFrom + closeMarker.length;
|
|
139
|
+
const token = {
|
|
52
140
|
type: definition.type,
|
|
53
141
|
marker: definition.marker,
|
|
54
142
|
from: index,
|
|
@@ -57,7 +145,11 @@
|
|
|
57
145
|
contentTo: closeFrom,
|
|
58
146
|
closeFrom,
|
|
59
147
|
to
|
|
60
|
-
}
|
|
148
|
+
};
|
|
149
|
+
if (definition.type === "strong-emphasis") {
|
|
150
|
+
tokens.push({ ...token, type: "strong", marker: "**", openTo: index + 2, contentFrom: index + 2, contentTo: to - 2, closeFrom: to - 2 });
|
|
151
|
+
tokens.push({ ...token, type: "emphasis", marker: "*", from: index + 2, to: to - 2 });
|
|
152
|
+
} else tokens.push(token);
|
|
61
153
|
index = to;
|
|
62
154
|
}
|
|
63
155
|
return tokens;
|
|
@@ -72,35 +164,94 @@
|
|
|
72
164
|
});
|
|
73
165
|
}
|
|
74
166
|
|
|
167
|
+
const characterSegmenter = typeof Intl.Segmenter === "function"
|
|
168
|
+
? new Intl.Segmenter("ko", { granularity: "grapheme" }) : null;
|
|
169
|
+
|
|
170
|
+
// Count the document model, never CodeMirror's virtualized/selected DOM.
|
|
171
|
+
function characterCount(value) {
|
|
172
|
+
const documentModel = markdownDocumentModel.parseDocument(value, { preserveBlankLines: true });
|
|
173
|
+
const fragments = [];
|
|
174
|
+
const visit = blocks => blocks.forEach(block => {
|
|
175
|
+
if (block.type === "paragraph" || block.type === "quote") fragments.push(block.lines.join("\n"));
|
|
176
|
+
else if (block.type === "heading") fragments.push(block.text);
|
|
177
|
+
else if (block.type === "blank") fragments.push(documentModel.lines[block.lineIndex].text);
|
|
178
|
+
else if (block.type === "list") block.items.forEach(item => {
|
|
179
|
+
fragments.push(item.body);
|
|
180
|
+
visit(item.children);
|
|
181
|
+
});
|
|
182
|
+
else if (block.type === "table") [block.header, ...block.rows].forEach(row => {
|
|
183
|
+
row.cells.forEach(cell => fragments.push(cell.text));
|
|
184
|
+
});
|
|
185
|
+
// Images and horizontal rules contain no manuscript characters.
|
|
186
|
+
});
|
|
187
|
+
visit(documentModel.blocks);
|
|
188
|
+
return fragments.reduce((total, text) => {
|
|
189
|
+
const hidden = inlineTokens(text).flatMap(token => [
|
|
190
|
+
{ from: token.from, to: token.openTo },
|
|
191
|
+
{ from: token.closeFrom, to: token.to }
|
|
192
|
+
]).sort((a, b) => a.from - b.from);
|
|
193
|
+
let cursor = 0;
|
|
194
|
+
let visible = "";
|
|
195
|
+
hidden.forEach(range => {
|
|
196
|
+
if (range.from > cursor) visible += text.slice(cursor, range.from);
|
|
197
|
+
cursor = Math.max(cursor, range.to);
|
|
198
|
+
});
|
|
199
|
+
visible = (visible + text.slice(cursor)).replace(/\r?\n/g, "");
|
|
200
|
+
let count = 0;
|
|
201
|
+
for (const ignored of characterSegmenter ? characterSegmenter.segment(visible) : visible) count++;
|
|
202
|
+
return total + count;
|
|
203
|
+
}, 0);
|
|
204
|
+
}
|
|
205
|
+
|
|
75
206
|
function selectionTouchesRange(selection, from, to) {
|
|
76
207
|
return selection.from === selection.to
|
|
77
208
|
? selection.head >= from && selection.head <= to
|
|
78
209
|
: selection.to >= from && selection.from <= to;
|
|
79
210
|
}
|
|
80
211
|
|
|
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]+(.+)$/);
|
|
212
|
+
function offsetInlineToken(token, offset) {
|
|
86
213
|
return {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
214
|
+
...token,
|
|
215
|
+
from: token.from + offset,
|
|
216
|
+
openTo: token.openTo + offset,
|
|
217
|
+
contentFrom: token.contentFrom + offset,
|
|
218
|
+
contentTo: token.contentTo + offset,
|
|
219
|
+
closeFrom: token.closeFrom + offset,
|
|
220
|
+
to: token.to + offset
|
|
94
221
|
};
|
|
95
222
|
}
|
|
96
223
|
|
|
97
|
-
function
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
from
|
|
102
|
-
|
|
103
|
-
|
|
224
|
+
function tableAwareInlineTokens(source, tableContexts) {
|
|
225
|
+
const tokens = [];
|
|
226
|
+
let cursor = 0;
|
|
227
|
+
[...tableContexts].sort((left, right) => left.from - right.from).forEach(table => {
|
|
228
|
+
if (cursor < table.from) {
|
|
229
|
+
tokens.push(...inlineTokens(source.slice(cursor, table.from)).map(token => offsetInlineToken(token, cursor)));
|
|
230
|
+
}
|
|
231
|
+
[...table.cells].sort((left, right) => left.from - right.from).forEach(cell => {
|
|
232
|
+
tokens.push(...inlineTokens(source.slice(cell.from, cell.to)).map(token => ({
|
|
233
|
+
...offsetInlineToken(token, cell.from),
|
|
234
|
+
tableCell: true
|
|
235
|
+
})));
|
|
236
|
+
});
|
|
237
|
+
cursor = Math.max(cursor, table.to);
|
|
238
|
+
});
|
|
239
|
+
if (cursor < source.length) {
|
|
240
|
+
tokens.push(...inlineTokens(source.slice(cursor)).map(token => offsetInlineToken(token, cursor)));
|
|
241
|
+
}
|
|
242
|
+
return tokens.sort((left, right) => left.from - right.from || left.to - right.to);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function lineSyntax(text) {
|
|
246
|
+
if (!markdownDocumentModel?.parseLine) throw new Error("Markdown 문서 모델을 초기화하지 못했습니다.");
|
|
247
|
+
return markdownDocumentModel.parseLine(text);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function layoutLines(source) {
|
|
251
|
+
if (!markdownDocumentModel?.parseDocument) throw new Error("Markdown 문서 모델을 초기화하지 못했습니다.");
|
|
252
|
+
const documentModel = markdownDocumentModel.parseDocument(source, { preserveBlankLines: true });
|
|
253
|
+
const lines = documentModel.lines.map(line => ({
|
|
254
|
+
...line,
|
|
104
255
|
blockId: -1,
|
|
105
256
|
blockType: "",
|
|
106
257
|
blockStart: false,
|
|
@@ -109,83 +260,271 @@
|
|
|
109
260
|
firstListItem: false
|
|
110
261
|
}));
|
|
111
262
|
const blocks = [];
|
|
112
|
-
let paragraph = null;
|
|
113
|
-
let quote = null;
|
|
114
|
-
let listType = "";
|
|
115
|
-
let listGroup = 0;
|
|
116
263
|
let nextBlockId = 0;
|
|
117
|
-
|
|
118
|
-
|
|
264
|
+
let nextListGroup = 0;
|
|
265
|
+
const assignBlock = (type, lineIndices, extra = {}) => {
|
|
266
|
+
const validLineIndices = lineIndices.filter(index => lines[index]);
|
|
267
|
+
if (!validLineIndices.length) return null;
|
|
268
|
+
const block = { id: nextBlockId++, type, lines: validLineIndices, ...extra };
|
|
119
269
|
blocks.push(block);
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
270
|
+
validLineIndices.forEach((lineIndex, index) => {
|
|
271
|
+
const line = lines[lineIndex];
|
|
272
|
+
line.blockId = block.id;
|
|
273
|
+
line.blockType = type;
|
|
274
|
+
line.blockStart = index === 0;
|
|
275
|
+
line.blockEnd = index === validLineIndices.length - 1;
|
|
276
|
+
Object.assign(line, extra);
|
|
277
|
+
});
|
|
124
278
|
return block;
|
|
125
279
|
};
|
|
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;
|
|
280
|
+
documentModel.blocks.forEach(sourceBlock => {
|
|
281
|
+
if (sourceBlock.type === "blank") return;
|
|
282
|
+
if (sourceBlock.type === "horizontalRule") {
|
|
283
|
+
assignBlock("horizontalRule", [sourceBlock.lineIndex]);
|
|
140
284
|
return;
|
|
141
285
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
paragraph = null;
|
|
145
|
-
listType = "";
|
|
286
|
+
if (sourceBlock.type === "image") {
|
|
287
|
+
assignBlock("image", [sourceBlock.lineIndex], { assetId: sourceBlock.assetId });
|
|
146
288
|
return;
|
|
147
289
|
}
|
|
148
|
-
if (
|
|
149
|
-
|
|
150
|
-
listType = "";
|
|
151
|
-
openBlock(`heading${line.headingLevel}`, line);
|
|
290
|
+
if (sourceBlock.type === "heading") {
|
|
291
|
+
assignBlock(`heading${sourceBlock.level}`, [sourceBlock.lineIndex]);
|
|
152
292
|
return;
|
|
153
293
|
}
|
|
154
|
-
if (
|
|
155
|
-
|
|
156
|
-
const firstListItem = line.listType !== listType;
|
|
157
|
-
if (firstListItem) listGroup += 1;
|
|
158
|
-
listType = line.listType;
|
|
159
|
-
line.firstListItem = firstListItem;
|
|
160
|
-
openBlock("listItem", line, { listGroup, firstListItem });
|
|
294
|
+
if (sourceBlock.type === "paragraph" || sourceBlock.type === "quote") {
|
|
295
|
+
assignBlock(sourceBlock.type, sourceBlock.lineIndices);
|
|
161
296
|
return;
|
|
162
297
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
298
|
+
if (sourceBlock.type === "table") {
|
|
299
|
+
const tableBlock = assignBlock("table", sourceBlock.lineIndices, {
|
|
300
|
+
tableColumnCount: sourceBlock.columnCount,
|
|
301
|
+
tableAlignments: sourceBlock.alignments,
|
|
302
|
+
tableFrom: sourceBlock.from,
|
|
303
|
+
tableTo: sourceBlock.to
|
|
304
|
+
});
|
|
305
|
+
if (!tableBlock) return;
|
|
306
|
+
Object.assign(tableBlock, {
|
|
307
|
+
from: sourceBlock.from,
|
|
308
|
+
to: sourceBlock.to,
|
|
309
|
+
columnCount: sourceBlock.columnCount,
|
|
310
|
+
alignments: sourceBlock.alignments
|
|
311
|
+
});
|
|
312
|
+
[sourceBlock.header, sourceBlock.delimiter, ...sourceBlock.rows].forEach(row => {
|
|
313
|
+
const line = lines[row.lineIndex];
|
|
314
|
+
if (!line) return;
|
|
315
|
+
line.tableRole = row.role;
|
|
316
|
+
line.tableCells = row.cells;
|
|
317
|
+
line.tablePipes = row.pipes;
|
|
318
|
+
line.tableRaw = row.raw;
|
|
319
|
+
line.tableHasLeadingPipe = row.hasLeadingPipe;
|
|
320
|
+
line.tableHasTrailingPipe = row.hasTrailingPipe;
|
|
321
|
+
line.tableEscapedPipeBackslashes = row.cells.flatMap(tableEscapedPipeBackslashes);
|
|
322
|
+
line.tableEmptyCellColumns = row.cells.filter(cell => !cell.synthetic && !cell.text).map(cell => cell.columnIndex);
|
|
323
|
+
line.tableSyntheticCellColumns = row.cells.filter(cell => cell.synthetic).map(cell => cell.columnIndex);
|
|
324
|
+
});
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
if (sourceBlock.type !== "list") return;
|
|
328
|
+
const listGroup = ++nextListGroup;
|
|
329
|
+
const markerWidth = Math.max(1, Number(sourceBlock.markerWidth) || 1);
|
|
330
|
+
sourceBlock.items.forEach((item, itemIndex) => {
|
|
331
|
+
const itemBlock = assignBlock("listItem", [item.lineIndex], {
|
|
332
|
+
listGroup,
|
|
333
|
+
listMarkerWidth: markerWidth,
|
|
334
|
+
firstListItem: itemIndex === 0,
|
|
335
|
+
listEnd: itemIndex === sourceBlock.items.length - 1
|
|
336
|
+
});
|
|
337
|
+
item.children.forEach(child => {
|
|
338
|
+
if (child.type !== "quote") return;
|
|
339
|
+
assignBlock("quote", child.lineIndices, { nestedQuote: true, listGroup });
|
|
340
|
+
});
|
|
341
|
+
if (itemBlock) itemBlock.children = item.children;
|
|
342
|
+
});
|
|
166
343
|
});
|
|
167
344
|
|
|
168
|
-
blocks
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
345
|
+
return { lines, blocks };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function tableCellContexts(value) {
|
|
349
|
+
const source = String(value ?? "").replace(/\r\n?/g, "\n");
|
|
350
|
+
const layout = layoutLines(source);
|
|
351
|
+
return layout.blocks.filter(block => block.type === "table").map(block => {
|
|
352
|
+
const rows = block.lines
|
|
353
|
+
.map(lineIndex => layout.lines[lineIndex])
|
|
354
|
+
.filter(line => line && line.tableRole !== "delimiter")
|
|
355
|
+
.map((line, rowIndex) => ({
|
|
356
|
+
line,
|
|
357
|
+
rowIndex,
|
|
358
|
+
cells: line.tableCells.map(cell => ({ cell, line, rowIndex, columnIndex: cell.columnIndex }))
|
|
359
|
+
}));
|
|
360
|
+
const cells = rows.flatMap(row => row.cells);
|
|
361
|
+
const lines = block.lines.map(lineIndex => layout.lines[lineIndex]).filter(Boolean);
|
|
362
|
+
return { block, lines, rows, cells };
|
|
181
363
|
});
|
|
364
|
+
}
|
|
182
365
|
|
|
183
|
-
|
|
366
|
+
function tableCellContextFromTables(tables, position) {
|
|
367
|
+
const offset = Math.max(0, Number(position) || 0);
|
|
368
|
+
for (const table of tables) {
|
|
369
|
+
for (const context of table.cells) {
|
|
370
|
+
const { cell, line, columnIndex } = context;
|
|
371
|
+
const firstCell = columnIndex === 0;
|
|
372
|
+
const lastCell = columnIndex === line.tableCells.length - 1;
|
|
373
|
+
const touchesContent = offset >= cell.rawFrom && offset <= cell.rawTo;
|
|
374
|
+
const touchesLeadingPipe = firstCell && line.tablePipes?.[0]?.from === offset;
|
|
375
|
+
const trailingPipe = line.tablePipes?.at(-1);
|
|
376
|
+
const touchesTrailingPipe = lastCell && (
|
|
377
|
+
trailingPipe?.from === offset || trailingPipe?.to === offset
|
|
378
|
+
);
|
|
379
|
+
if (touchesContent || touchesLeadingPipe || touchesTrailingPipe) return { ...context, table };
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function tableCellContextAt(value, position) {
|
|
386
|
+
return tableCellContextFromTables(tableCellContexts(value), position);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function tableSelectionCrossesStructure(value, selection) {
|
|
390
|
+
const source = String(value ?? "").replace(/\r\n?/g, "\n");
|
|
391
|
+
const anchor = Math.max(0, Math.min(source.length, Number(selection?.anchor) || 0));
|
|
392
|
+
const head = Math.max(0, Math.min(source.length, Number(selection?.head) || 0));
|
|
393
|
+
const from = Math.min(anchor, head);
|
|
394
|
+
const to = Math.max(anchor, head);
|
|
395
|
+
if (from === to) return false;
|
|
396
|
+
const tables = tableCellContexts(source);
|
|
397
|
+
const start = tableCellContextFromTables(tables, from);
|
|
398
|
+
const end = tableCellContextFromTables(tables, to);
|
|
399
|
+
for (const table of tables) {
|
|
400
|
+
if (to <= table.block.from || from >= table.block.to) continue;
|
|
401
|
+
if (!start || !end) return true;
|
|
402
|
+
const sameTable = start.table.block.from === table.block.from && end.table.block.from === table.block.from;
|
|
403
|
+
const sameCell = start.line.index === end.line.index && start.columnIndex === end.columnIndex;
|
|
404
|
+
const insideRawCell = from >= start.cell.rawFrom && to <= start.cell.rawTo;
|
|
405
|
+
if (!sameTable || !sameCell || !insideRawCell) return true;
|
|
406
|
+
}
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function tableCellNavigationDestination(value, selection, direction) {
|
|
411
|
+
const anchor = Math.max(0, Number(selection?.anchor) || 0);
|
|
412
|
+
const head = Math.max(0, Number(selection?.head) || 0);
|
|
413
|
+
if (anchor !== head) return null;
|
|
414
|
+
const tables = tableCellContexts(value);
|
|
415
|
+
const current = tableCellContextFromTables(tables, head);
|
|
416
|
+
if (!current) return null;
|
|
417
|
+
const { table, cell, rowIndex, columnIndex } = current;
|
|
418
|
+
const cellLength = Math.max(0, cell.to - cell.from);
|
|
419
|
+
const contentOffset = Math.max(0, Math.min(cellLength, head - cell.from));
|
|
420
|
+
const flatIndex = table.cells.findIndex(context => (
|
|
421
|
+
context.line.index === current.line.index && context.columnIndex === columnIndex
|
|
422
|
+
));
|
|
423
|
+
const destination = (targetContext, position) => targetContext
|
|
424
|
+
? { position, context: { ...targetContext, table } }
|
|
425
|
+
: null;
|
|
426
|
+
let target = null;
|
|
427
|
+
|
|
428
|
+
if (direction === "left") {
|
|
429
|
+
if (head > cell.from) return null;
|
|
430
|
+
target = table.cells[flatIndex - 1] || null;
|
|
431
|
+
return destination(target, target?.cell.to);
|
|
432
|
+
}
|
|
433
|
+
if (direction === "right") {
|
|
434
|
+
if (head < cell.to) return null;
|
|
435
|
+
target = table.cells[flatIndex + 1] || null;
|
|
436
|
+
return destination(target, target?.cell.from);
|
|
437
|
+
}
|
|
438
|
+
if (direction === "previous") {
|
|
439
|
+
target = table.cells[flatIndex - 1] || null;
|
|
440
|
+
return destination(target, target?.cell.to);
|
|
441
|
+
}
|
|
442
|
+
if (direction === "next") {
|
|
443
|
+
target = table.cells[flatIndex + 1] || null;
|
|
444
|
+
return destination(target, target?.cell.from);
|
|
445
|
+
}
|
|
446
|
+
if (direction === "up" || direction === "down") {
|
|
447
|
+
const rowStep = direction === "up" ? -1 : 1;
|
|
448
|
+
for (let targetRow = rowIndex + rowStep; targetRow >= 0 && targetRow < table.rows.length; targetRow += rowStep) {
|
|
449
|
+
target = table.rows[targetRow].cells.find(context => context.columnIndex === columnIndex) || null;
|
|
450
|
+
if (!target) continue;
|
|
451
|
+
return destination(target, Math.min(target.cell.to, target.cell.from + contentOffset));
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return null;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function tableCellNavigationTarget(value, selection, direction) {
|
|
458
|
+
return tableCellNavigationDestination(value, selection, direction)?.position ?? null;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function syntheticTableCellMaterialization(context) {
|
|
462
|
+
if (!context?.cell?.synthetic || !context.line?.tableCells) return null;
|
|
463
|
+
const firstSyntheticColumn = context.line.tableCells.findIndex(cell => cell.synthetic);
|
|
464
|
+
if (firstSyntheticColumn < 0 || context.columnIndex < firstSyntheticColumn) return null;
|
|
465
|
+
const pipeCount = context.columnIndex - firstSyntheticColumn + (context.line.tableHasTrailingPipe ? 0 : 1);
|
|
466
|
+
const insert = "|".repeat(Math.max(0, pipeCount));
|
|
467
|
+
return {
|
|
468
|
+
from: context.line.to,
|
|
469
|
+
insert,
|
|
470
|
+
position: context.line.to + insert.length
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function safeTableCellInput(value, precedingBackslashes = 0) {
|
|
475
|
+
const source = String(value ?? "");
|
|
476
|
+
let result = "";
|
|
477
|
+
let trailingBackslashes = Math.max(0, Number(precedingBackslashes) || 0);
|
|
478
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
479
|
+
const character = source[index];
|
|
480
|
+
if (character === "\r") {
|
|
481
|
+
if (source[index + 1] === "\n") index += 1;
|
|
482
|
+
result += " ";
|
|
483
|
+
trailingBackslashes = 0;
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
if (character === "\n") {
|
|
487
|
+
result += " ";
|
|
488
|
+
trailingBackslashes = 0;
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
if (character === "|") {
|
|
492
|
+
if (trailingBackslashes % 2 === 0) result += "\\";
|
|
493
|
+
result += character;
|
|
494
|
+
trailingBackslashes = 0;
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
result += character;
|
|
498
|
+
trailingBackslashes = character === "\\" ? trailingBackslashes + 1 : 0;
|
|
499
|
+
}
|
|
500
|
+
return result;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function safeTableCellReplacement(value, from, to, insertedValue) {
|
|
504
|
+
const source = String(value ?? "");
|
|
505
|
+
const changeFrom = Math.max(0, Math.min(source.length, Number(from) || 0));
|
|
506
|
+
const changeTo = Math.max(changeFrom, Math.min(source.length, Number(to) || 0));
|
|
507
|
+
let precedingBackslashes = 0;
|
|
508
|
+
for (let cursor = changeFrom - 1; cursor >= 0 && source[cursor] === "\\"; cursor -= 1) precedingBackslashes += 1;
|
|
509
|
+
let safeText = safeTableCellInput(insertedValue, precedingBackslashes);
|
|
510
|
+
let suffixBackslashes = 0;
|
|
511
|
+
for (let cursor = changeTo; cursor < source.length && source[cursor] === "\\"; cursor += 1) suffixBackslashes += 1;
|
|
512
|
+
const suffixPipe = changeTo + suffixBackslashes;
|
|
513
|
+
if (source[suffixPipe] !== "|") return safeText;
|
|
514
|
+
|
|
515
|
+
let originalBackslashes = 0;
|
|
516
|
+
for (let cursor = suffixPipe - 1; cursor >= 0 && source[cursor] === "\\"; cursor -= 1) originalBackslashes += 1;
|
|
517
|
+
let insertedBackslashes = 0;
|
|
518
|
+
for (let cursor = safeText.length - 1; cursor >= 0 && safeText[cursor] === "\\"; cursor -= 1) insertedBackslashes += 1;
|
|
519
|
+
const keepsPrecedingRun = safeText.length === insertedBackslashes;
|
|
520
|
+
const resultingBackslashes = suffixBackslashes + insertedBackslashes + (keepsPrecedingRun ? precedingBackslashes : 0);
|
|
521
|
+
if (resultingBackslashes % 2 !== originalBackslashes % 2) safeText += "\\";
|
|
522
|
+
return safeText;
|
|
184
523
|
}
|
|
185
524
|
|
|
186
525
|
function previewModel(value, selections = [{ anchor: 0, head: 0 }]) {
|
|
187
526
|
const source = String(value ?? "").replace(/\r\n?/g, "\n");
|
|
188
|
-
const starts = lineStarts(source);
|
|
527
|
+
const starts = markdownDocumentModel.lineStarts(source);
|
|
189
528
|
const normalizedSelections = normalizeSelections(selections, source.length);
|
|
190
529
|
const activeLines = new Set();
|
|
191
530
|
normalizedSelections.forEach(selection => {
|
|
@@ -193,12 +532,34 @@
|
|
|
193
532
|
const last = lineAt(starts, selection.to);
|
|
194
533
|
for (let line = first; line <= last; line += 1) activeLines.add(line);
|
|
195
534
|
});
|
|
196
|
-
const layout = layoutLines(source
|
|
535
|
+
const layout = layoutLines(source);
|
|
197
536
|
layout.lines.forEach(line => { line.active = activeLines.has(line.index); });
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
537
|
+
const tableContexts = layout.blocks.filter(block => block.type === "table").map(block => ({
|
|
538
|
+
block,
|
|
539
|
+
from: block.from,
|
|
540
|
+
to: block.to,
|
|
541
|
+
cells: block.lines.flatMap(lineIndex => (
|
|
542
|
+
layout.lines[lineIndex]?.tableCells?.filter(cell => !cell.synthetic) || []
|
|
543
|
+
))
|
|
201
544
|
}));
|
|
545
|
+
tableContexts.forEach(({ block }) => {
|
|
546
|
+
block.active = normalizedSelections.some(selection => selectionTouchesRange(selection, block.from, block.to));
|
|
547
|
+
block.lines.forEach(lineIndex => {
|
|
548
|
+
const line = layout.lines[lineIndex];
|
|
549
|
+
if (!line) return;
|
|
550
|
+
line.tableActiveCellColumns = line.tableCells
|
|
551
|
+
.filter(cell => !cell.synthetic && normalizedSelections.some(selection => (
|
|
552
|
+
selectionTouchesRange(selection, cell.rawFrom, cell.rawTo)
|
|
553
|
+
)))
|
|
554
|
+
.map(cell => cell.columnIndex);
|
|
555
|
+
});
|
|
556
|
+
});
|
|
557
|
+
const tokens = tableAwareInlineTokens(source, tableContexts)
|
|
558
|
+
.filter(token => !layout.lines.some(line => line.blockType === "image" && token.from < line.to && token.to > line.from))
|
|
559
|
+
.map(token => ({
|
|
560
|
+
...token,
|
|
561
|
+
active: token.tableCell !== true && normalizedSelections.some(selection => selectionTouchesRange(selection, token.from, token.to))
|
|
562
|
+
}));
|
|
202
563
|
return { source, starts, lines: layout.lines, blocks: layout.blocks, tokens };
|
|
203
564
|
}
|
|
204
565
|
|
|
@@ -215,11 +576,44 @@
|
|
|
215
576
|
.toLowerCase();
|
|
216
577
|
values.push(`writer-md-${blockClass}`);
|
|
217
578
|
}
|
|
579
|
+
if (line.listType) values.push(`writer-md-list-${line.listType}`);
|
|
218
580
|
if (line.nestedQuote) values.push("writer-md-nested-quote");
|
|
219
581
|
if (line.firstListItem) values.push("writer-md-first-list-item");
|
|
582
|
+
if (line.blockType === "table") {
|
|
583
|
+
values.push("writer-md-table-preview-row");
|
|
584
|
+
if (line.tableActiveCellColumns?.length) values.push("writer-md-table-has-active-cell");
|
|
585
|
+
if (line.tableRole) values.push(`writer-md-table-${line.tableRole}-row`);
|
|
586
|
+
if (line.tableColumnCount) values.push(`writer-md-table-columns-${line.tableColumnCount}`);
|
|
587
|
+
if (line.tableEmptyCellColumns?.length) values.push("writer-md-table-has-empty-cells");
|
|
588
|
+
if (line.tableSyntheticCellColumns?.length) values.push("writer-md-table-has-synthetic-cells");
|
|
589
|
+
}
|
|
220
590
|
return values.join(" ");
|
|
221
591
|
}
|
|
222
592
|
|
|
593
|
+
function tableOperation(value, selection, command, dimensions = {}) {
|
|
594
|
+
const source = String(value);
|
|
595
|
+
const from = Math.min(selection.anchor, selection.head);
|
|
596
|
+
const to = Math.max(selection.anchor, selection.head);
|
|
597
|
+
const blocks = markdownDocumentModel.parseDocument(source).blocks;
|
|
598
|
+
const table = blocks.find(block => block.type === "table" && from >= block.from && to <= block.to);
|
|
599
|
+
if (command === "table-delete") {
|
|
600
|
+
if (!table) return null;
|
|
601
|
+
// Replace only the table, never adjacent paragraphs or blank lines.
|
|
602
|
+
return { changes: { from: table.from, to: table.to, insert: "" }, selection: { anchor: table.from } };
|
|
603
|
+
}
|
|
604
|
+
if (command !== "table") return null;
|
|
605
|
+
if (!table && blocks.some(block => ["table", "code", "image"].includes(block.type) && from <= block.to && to >= block.from)) return null;
|
|
606
|
+
const columns = dimensions.columns ?? 2;
|
|
607
|
+
const rows = dimensions.rows ?? 3;
|
|
608
|
+
if (!Number.isInteger(columns) || columns < 1 || columns > 12 || !Number.isInteger(rows) || rows < 1 || rows > 30) return null;
|
|
609
|
+
const row = values => `| ${values.join(" | ")} |`;
|
|
610
|
+
const text = [row(Array.from({ length: columns }, (_, index) => `제목 ${index + 1}`)), row(Array(columns).fill("---")), ...Array.from({ length: rows }, () => row(Array(columns).fill("")))].join("\n");
|
|
611
|
+
const position = table ? table.to : to;
|
|
612
|
+
const prefix = position && !source.slice(0, position).endsWith("\n\n") ? (source[position - 1] === "\n" ? "\n" : "\n\n") : "";
|
|
613
|
+
const suffix = source.slice(position).startsWith("\n\n") ? "" : source[position] === "\n" ? "\n" : "\n\n";
|
|
614
|
+
return { changes: { from: position, to: position, insert: prefix + text + suffix }, selection: { anchor: position + prefix.length + 2 } };
|
|
615
|
+
}
|
|
616
|
+
|
|
223
617
|
function mount(options = {}) {
|
|
224
618
|
const cm = CodeMirror6;
|
|
225
619
|
if (!cm?.EditorState || !cm?.StateEffect || !cm?.EditorView || !cm?.Decoration || !cm?.StateField) {
|
|
@@ -227,8 +621,40 @@
|
|
|
227
621
|
}
|
|
228
622
|
if (!options.parent) throw new Error("Markdown 편집기 표시 영역이 없습니다.");
|
|
229
623
|
let destroyed = false;
|
|
624
|
+
let toolbar = null;
|
|
230
625
|
const ownerDocument = options.parent.ownerDocument || document;
|
|
626
|
+
const counter = ownerDocument.createElement("div");
|
|
627
|
+
counter.className = "writer-markdown-character-count";
|
|
628
|
+
const updateCounter = doc => {
|
|
629
|
+
const label = `${characterCount(doc.toString()).toLocaleString("ko-KR")}자`;
|
|
630
|
+
if (counter.textContent !== label) counter.textContent = label;
|
|
631
|
+
};
|
|
632
|
+
const editableEffect = cm.StateEffect.define();
|
|
633
|
+
const tableOperationEffect = cm.StateEffect.define();
|
|
634
|
+
const tableHistoryBoundary = cm.StateField.define({
|
|
635
|
+
create: () => false,
|
|
636
|
+
update(value, transaction) {
|
|
637
|
+
return transaction.docChanged ? transaction.effects.some(effect => effect.is(tableOperationEffect)) : value;
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
const editableField = cm.StateField.define({
|
|
641
|
+
create: () => options.readOnly !== true,
|
|
642
|
+
update(value, transaction) {
|
|
643
|
+
for (const effect of transaction.effects) if (effect.is(editableEffect)) value = effect.value;
|
|
644
|
+
return value;
|
|
645
|
+
}
|
|
646
|
+
});
|
|
647
|
+
let replacingValue = false;
|
|
648
|
+
const imageEditor = typeof globalThis !== "undefined" ? globalThis.WriterMarkdownImageEditor : null;
|
|
649
|
+
const images = imageEditor?.create({
|
|
650
|
+
cm, model: markdownDocumentModel, assets: options.assets, parent: options.parent,
|
|
651
|
+
onIdle: () => setTimeout(() => {
|
|
652
|
+
if (!destroyed && !images?.hasPending() && !toolbar?.ownsFocus() && !options.parent.contains(ownerDocument.activeElement)) options.onBlur?.();
|
|
653
|
+
}, 0),
|
|
654
|
+
canInsert: (source, from, to) => !markdownDocumentModel.parseDocument(source).blocks.some(block => block.type === "table" && from <= block.to && to >= block.from)
|
|
655
|
+
});
|
|
231
656
|
const focusEffect = cm.StateEffect.define();
|
|
657
|
+
const syntheticCellMaterializationEffect = cm.StateEffect.define();
|
|
232
658
|
const focusField = cm.StateField.define({
|
|
233
659
|
create() { return false; },
|
|
234
660
|
update(focused, transaction) {
|
|
@@ -238,6 +664,35 @@
|
|
|
238
664
|
return focused;
|
|
239
665
|
}
|
|
240
666
|
});
|
|
667
|
+
class TaskCheckbox {
|
|
668
|
+
constructor(from, checked, disabled) { this.from = from; this.checked = checked; this.disabled = disabled; }
|
|
669
|
+
eq(other) { return this.from === other.from && this.checked === other.checked && this.disabled === other.disabled; }
|
|
670
|
+
compare(other) { return this === other || this.eq(other); }
|
|
671
|
+
get estimatedHeight() { return -1; }
|
|
672
|
+
get lineBreaks() { return 0; }
|
|
673
|
+
updateDOM() { return false; }
|
|
674
|
+
coordsAt() { return null; }
|
|
675
|
+
destroy() {}
|
|
676
|
+
toDOM(view) {
|
|
677
|
+
const marker = ownerDocument.createElement("span");
|
|
678
|
+
marker.className = "writer-md-list-marker";
|
|
679
|
+
const input = ownerDocument.createElement("input");
|
|
680
|
+
input.type = "checkbox";
|
|
681
|
+
input.className = "writer-md-task-checkbox";
|
|
682
|
+
input.setAttribute("aria-label", "할 일 완료");
|
|
683
|
+
input.checked = this.checked;
|
|
684
|
+
input.disabled = this.disabled;
|
|
685
|
+
input.addEventListener("mousedown", event => event.preventDefault());
|
|
686
|
+
input.addEventListener("change", () => {
|
|
687
|
+
if (view.state.readOnly || view.composing || view.compositionStarted) { input.checked = this.checked; return; }
|
|
688
|
+
view.dispatch({ changes: { from: this.from, to: this.from + 1, insert: input.checked ? "x" : " " }, userEvent: "input.task" });
|
|
689
|
+
view.focus();
|
|
690
|
+
});
|
|
691
|
+
marker.append(input);
|
|
692
|
+
return marker;
|
|
693
|
+
}
|
|
694
|
+
ignoreEvent() { return true; }
|
|
695
|
+
}
|
|
241
696
|
const buildDecorations = state => {
|
|
242
697
|
const selections = state.field(focusField)
|
|
243
698
|
? state.selection.ranges.map(range => ({ anchor: range.anchor, head: range.head }))
|
|
@@ -248,7 +703,61 @@
|
|
|
248
703
|
class: active ? "writer-md-syntax" : "writer-md-syntax-hidden"
|
|
249
704
|
});
|
|
250
705
|
model.lines.forEach(line => {
|
|
251
|
-
|
|
706
|
+
if (line.blockType === "image" && images) return;
|
|
707
|
+
const attributes = { class: classNames(line) };
|
|
708
|
+
if (!state.doc.length && options.placeholder) attributes["data-placeholder"] = String(options.placeholder);
|
|
709
|
+
if (line.listType) attributes.style = `--writer-md-list-marker-width:${line.listMarkerWidth || 1}ch`;
|
|
710
|
+
if (line.blockType === "table") {
|
|
711
|
+
const columnCount = line.tableColumnCount || 1;
|
|
712
|
+
const minimumWidth = Math.max(14, Math.min(72, columnCount * 7));
|
|
713
|
+
const columnWidth = Number((100 / columnCount).toFixed(6));
|
|
714
|
+
attributes.style = `--writer-md-table-column-count:${columnCount};--writer-md-table-column-width:${columnWidth}%;--writer-md-table-min-width:${minimumWidth}em`;
|
|
715
|
+
}
|
|
716
|
+
ranges.push(cm.Decoration.line({ attributes }).range(line.from));
|
|
717
|
+
if (line.blockType === "horizontalRule") {
|
|
718
|
+
ranges.push(syntaxDecoration(line.active).range(line.from, line.to));
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
if (line.blockType === "table") {
|
|
722
|
+
line.tableCells.forEach((cell, columnIndex) => {
|
|
723
|
+
if (cell.rawFrom >= cell.rawTo) return;
|
|
724
|
+
const alignment = ["left", "center", "right"].includes(line.tableAlignments?.[columnIndex])
|
|
725
|
+
? line.tableAlignments[columnIndex]
|
|
726
|
+
: "left";
|
|
727
|
+
const positionClasses = [
|
|
728
|
+
columnIndex === 0 ? "writer-md-table-cell-first" : "",
|
|
729
|
+
columnIndex === line.tableCells.length - 1 ? "writer-md-table-cell-last" : "",
|
|
730
|
+
line.tableActiveCellColumns?.includes(columnIndex) ? "writer-md-table-cell-active" : ""
|
|
731
|
+
].filter(Boolean).join(" ");
|
|
732
|
+
ranges.push(cm.Decoration.mark({
|
|
733
|
+
class: `writer-md-table-cell writer-md-table-align-${alignment}${positionClasses ? ` ${positionClasses}` : ""}`,
|
|
734
|
+
attributes: {
|
|
735
|
+
style: `--writer-md-table-cell-column:${columnIndex + 1}`,
|
|
736
|
+
"data-writer-md-table-cell-from": String(cell.from),
|
|
737
|
+
"data-writer-md-table-cell-to": String(cell.to)
|
|
738
|
+
},
|
|
739
|
+
inclusiveEnd: false
|
|
740
|
+
}).range(cell.rawFrom, cell.rawTo));
|
|
741
|
+
tableCellEdgeWhitespaceRanges(cell).forEach(whitespace => {
|
|
742
|
+
ranges.push(cm.Decoration.mark({
|
|
743
|
+
class: "writer-md-table-cell-edge-whitespace",
|
|
744
|
+
inclusiveEnd: false
|
|
745
|
+
}).range(whitespace.from, whitespace.to));
|
|
746
|
+
});
|
|
747
|
+
});
|
|
748
|
+
line.tablePipes.forEach(pipe => {
|
|
749
|
+
ranges.push(cm.Decoration.mark({
|
|
750
|
+
class: "writer-md-table-pipe",
|
|
751
|
+
inclusiveEnd: false
|
|
752
|
+
}).range(pipe.from, pipe.to));
|
|
753
|
+
});
|
|
754
|
+
line.tableEscapedPipeBackslashes.forEach(backslash => {
|
|
755
|
+
ranges.push(cm.Decoration.mark({
|
|
756
|
+
class: "writer-md-table-escaped-pipe-backslash",
|
|
757
|
+
inclusiveEnd: false
|
|
758
|
+
}).range(backslash.from, backslash.to));
|
|
759
|
+
});
|
|
760
|
+
}
|
|
252
761
|
if (line.headingMarkerFrom >= 0) {
|
|
253
762
|
const from = line.from + line.headingMarkerFrom;
|
|
254
763
|
const to = line.from + line.headingMarkerTo;
|
|
@@ -258,6 +767,25 @@
|
|
|
258
767
|
const to = line.from + line.quoteMarkerTo;
|
|
259
768
|
ranges.push(syntaxDecoration(line.active).range(from, to));
|
|
260
769
|
}
|
|
770
|
+
if (line.listIndentTo > 0) {
|
|
771
|
+
const from = line.from;
|
|
772
|
+
const to = line.from + line.listIndentTo;
|
|
773
|
+
ranges.push(cm.Decoration.mark({
|
|
774
|
+
class: "writer-md-list-leading-indent",
|
|
775
|
+
inclusiveEnd: false
|
|
776
|
+
}).range(from, to));
|
|
777
|
+
}
|
|
778
|
+
if (line.taskStateFrom >= 0 && line.blockType === "listItem") {
|
|
779
|
+
ranges.push(cm.Decoration.widget({ widget: new TaskCheckbox(line.from + line.taskStateFrom, line.taskChecked, state.readOnly), side: -1 }).range(line.from + line.listMarkerFrom));
|
|
780
|
+
ranges.push(syntaxDecoration(false).range(line.from + line.listMarkerFrom, line.from + line.taskMarkerTo));
|
|
781
|
+
} else if (line.listMarkerFrom >= 0) {
|
|
782
|
+
const from = line.from + line.listMarkerFrom;
|
|
783
|
+
const to = line.from + line.listMarkerTo;
|
|
784
|
+
ranges.push(cm.Decoration.mark({
|
|
785
|
+
class: "writer-md-list-marker",
|
|
786
|
+
inclusiveEnd: false
|
|
787
|
+
}).range(from, to));
|
|
788
|
+
}
|
|
261
789
|
});
|
|
262
790
|
model.tokens.forEach(token => {
|
|
263
791
|
ranges.push(cm.Decoration.mark({ class: `writer-md-${token.type}` }).range(token.contentFrom, token.contentTo));
|
|
@@ -273,50 +801,621 @@
|
|
|
273
801
|
},
|
|
274
802
|
update(decorations, transaction) {
|
|
275
803
|
const focusChanged = transaction.effects.some(effect => effect.is(focusEffect));
|
|
276
|
-
|
|
804
|
+
const editableChanged = transaction.effects.some(effect => effect.is(editableEffect));
|
|
805
|
+
return transaction.docChanged || transaction.selection || focusChanged || editableChanged ? buildDecorations(transaction.state) : decorations;
|
|
277
806
|
},
|
|
278
807
|
provide: field => cm.EditorView.decorations.from(field)
|
|
279
808
|
});
|
|
809
|
+
const buildAtomicRanges = state => {
|
|
810
|
+
const model = previewModel(state.doc.toString(), []);
|
|
811
|
+
const ranges = [];
|
|
812
|
+
const atomic = cm.Decoration.mark({});
|
|
813
|
+
model.lines.filter(line => line.blockType === "table").forEach(line => {
|
|
814
|
+
if (line.tableRole === "delimiter") {
|
|
815
|
+
if (line.to > line.from) ranges.push(atomic.range(line.from, line.to));
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
line.tablePipes.forEach(pipe => ranges.push(atomic.range(pipe.from, pipe.to)));
|
|
819
|
+
line.tableCells.forEach(cell => {
|
|
820
|
+
tableCellEdgeWhitespaceRanges(cell).forEach(whitespace => {
|
|
821
|
+
ranges.push(atomic.range(whitespace.from, whitespace.to));
|
|
822
|
+
});
|
|
823
|
+
});
|
|
824
|
+
line.tableEscapedPipeBackslashes.forEach(backslash => ranges.push(atomic.range(backslash.from, backslash.to)));
|
|
825
|
+
});
|
|
826
|
+
model.tokens.filter(token => token.tableCell === true).forEach(token => {
|
|
827
|
+
ranges.push(atomic.range(token.from, token.openTo));
|
|
828
|
+
ranges.push(atomic.range(token.closeFrom, token.to));
|
|
829
|
+
});
|
|
830
|
+
return cm.Decoration.set(ranges, true);
|
|
831
|
+
};
|
|
832
|
+
const tableAtomicField = cm.StateField.define({
|
|
833
|
+
create(state) {
|
|
834
|
+
return buildAtomicRanges(state);
|
|
835
|
+
},
|
|
836
|
+
update(ranges, transaction) {
|
|
837
|
+
return transaction.docChanged ? buildAtomicRanges(transaction.state) : ranges;
|
|
838
|
+
},
|
|
839
|
+
provide: field => cm.EditorView.atomicRanges.from(field, ranges => () => ranges)
|
|
840
|
+
});
|
|
841
|
+
const tableCellPointerPosition = (view, cellElement, event, from, to) => {
|
|
842
|
+
const nativePosition = view.posAtCoords({ x: event.clientX, y: event.clientY });
|
|
843
|
+
const documentRef = cellElement.ownerDocument;
|
|
844
|
+
const walker = documentRef.createTreeWalker(cellElement, 4);
|
|
845
|
+
let closestPosition = null;
|
|
846
|
+
let closestDistance = Number.POSITIVE_INFINITY;
|
|
847
|
+
for (let textNode = walker.nextNode(); textNode; textNode = walker.nextNode()) {
|
|
848
|
+
const parent = textNode.parentElement;
|
|
849
|
+
if (parent?.closest?.(".writer-md-syntax-hidden,.writer-md-table-cell-edge-whitespace,.writer-md-table-escaped-pipe-backslash")) continue;
|
|
850
|
+
for (let offset = 0; offset <= textNode.nodeValue.length; offset += 1) {
|
|
851
|
+
let position = null;
|
|
852
|
+
try {
|
|
853
|
+
position = view.posAtDOM(textNode, offset);
|
|
854
|
+
} catch {
|
|
855
|
+
continue;
|
|
856
|
+
}
|
|
857
|
+
if (!Number.isSafeInteger(position) || position < from || position > to) continue;
|
|
858
|
+
const range = documentRef.createRange();
|
|
859
|
+
range.setStart(textNode, offset);
|
|
860
|
+
range.collapse(true);
|
|
861
|
+
const rect = range.getBoundingClientRect();
|
|
862
|
+
const verticalDistance = event.clientY < rect.top
|
|
863
|
+
? rect.top - event.clientY
|
|
864
|
+
: event.clientY > rect.bottom
|
|
865
|
+
? event.clientY - rect.bottom
|
|
866
|
+
: 0;
|
|
867
|
+
const distance = (verticalDistance * 10000) + Math.abs(event.clientX - rect.left);
|
|
868
|
+
if (distance >= closestDistance) continue;
|
|
869
|
+
closestDistance = distance;
|
|
870
|
+
closestPosition = position;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
if (Number.isSafeInteger(closestPosition)) return closestPosition;
|
|
874
|
+
if (Number.isSafeInteger(nativePosition) && nativePosition >= from && nativePosition <= to) {
|
|
875
|
+
return nativePosition;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
const rect = cellElement.getBoundingClientRect();
|
|
879
|
+
const ratio = rect.width > 0
|
|
880
|
+
? Math.max(0, Math.min(1, (event.clientX - rect.left) / rect.width))
|
|
881
|
+
: 0;
|
|
882
|
+
return Math.max(from, Math.min(to, from + Math.round((to - from) * ratio)));
|
|
883
|
+
};
|
|
884
|
+
let tablePointerStart = null;
|
|
885
|
+
const handleTableCellMouseDown = (event, view) => {
|
|
886
|
+
tablePointerStart = null;
|
|
887
|
+
if (event.button !== 0 || event.detail !== 1 || event.shiftKey) return false;
|
|
888
|
+
const cellElement = event.target?.closest?.(".writer-md-table-cell");
|
|
889
|
+
if (!cellElement || !view.dom.contains(cellElement)) return false;
|
|
890
|
+
const from = Number(cellElement.getAttribute("data-writer-md-table-cell-from"));
|
|
891
|
+
const to = Number(cellElement.getAttribute("data-writer-md-table-cell-to"));
|
|
892
|
+
if (!Number.isSafeInteger(from) || !Number.isSafeInteger(to) || from > to) return false;
|
|
893
|
+
const position = tableCellPointerPosition(view, cellElement, event, from, to);
|
|
894
|
+
view.focus();
|
|
895
|
+
tablePointerStart = { clientX: event.clientX, clientY: event.clientY, from, to, position };
|
|
896
|
+
return false;
|
|
897
|
+
};
|
|
898
|
+
const tableCellWordSelection = (source, from, to, position) => {
|
|
899
|
+
const text = source.slice(from, to);
|
|
900
|
+
const relativePosition = Math.max(0, Math.min(text.length, position - from));
|
|
901
|
+
if (typeof Intl?.Segmenter === "function") {
|
|
902
|
+
const segments = new Intl.Segmenter(undefined, { granularity: "word" }).segment(text);
|
|
903
|
+
for (const segment of segments) {
|
|
904
|
+
const segmentFrom = segment.index;
|
|
905
|
+
const segmentTo = segment.index + segment.segment.length;
|
|
906
|
+
if (relativePosition < segmentFrom || relativePosition > segmentTo || !segment.segment.trim()) continue;
|
|
907
|
+
return { anchor: from + segmentFrom, head: from + segmentTo };
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
let wordFrom = relativePosition;
|
|
911
|
+
let wordTo = relativePosition;
|
|
912
|
+
while (wordFrom > 0 && !/\s/u.test(text[wordFrom - 1])) wordFrom -= 1;
|
|
913
|
+
while (wordTo < text.length && !/\s/u.test(text[wordTo])) wordTo += 1;
|
|
914
|
+
return wordFrom === wordTo
|
|
915
|
+
? { anchor: Math.max(from, Math.min(to, position)), head: Math.max(from, Math.min(to, position)) }
|
|
916
|
+
: { anchor: from + wordFrom, head: from + wordTo };
|
|
917
|
+
};
|
|
918
|
+
const handleTableCellMouseUp = (event, view) => {
|
|
919
|
+
const start = tablePointerStart;
|
|
920
|
+
tablePointerStart = null;
|
|
921
|
+
if (event.button !== 0 || event.shiftKey) return false;
|
|
922
|
+
if (event.detail === 2) {
|
|
923
|
+
const cellElement = event.target?.closest?.(".writer-md-table-cell");
|
|
924
|
+
if (!cellElement || !view.dom.contains(cellElement)) return false;
|
|
925
|
+
const from = Number(cellElement.getAttribute("data-writer-md-table-cell-from"));
|
|
926
|
+
const to = Number(cellElement.getAttribute("data-writer-md-table-cell-to"));
|
|
927
|
+
if (!Number.isSafeInteger(from) || !Number.isSafeInteger(to) || from > to) return false;
|
|
928
|
+
const position = tableCellPointerPosition(view, cellElement, event, from, to);
|
|
929
|
+
event.preventDefault();
|
|
930
|
+
view.dispatch({
|
|
931
|
+
selection: tableCellWordSelection(view.state.doc.toString(), from, to, position),
|
|
932
|
+
scrollIntoView: true
|
|
933
|
+
});
|
|
934
|
+
return true;
|
|
935
|
+
}
|
|
936
|
+
if (!start || event.detail !== 1) return false;
|
|
937
|
+
if (Math.hypot(event.clientX - start.clientX, event.clientY - start.clientY) > 4) return false;
|
|
938
|
+
const cellElement = event.target?.closest?.(".writer-md-table-cell");
|
|
939
|
+
if (!cellElement || !view.dom.contains(cellElement)) return false;
|
|
940
|
+
const from = Number(cellElement.getAttribute("data-writer-md-table-cell-from"));
|
|
941
|
+
const to = Number(cellElement.getAttribute("data-writer-md-table-cell-to"));
|
|
942
|
+
if (from !== start.from || to !== start.to) return false;
|
|
943
|
+
event.preventDefault();
|
|
944
|
+
view.dispatch({ selection: { anchor: start.position }, scrollIntoView: true });
|
|
945
|
+
return true;
|
|
946
|
+
};
|
|
947
|
+
const tableViewIsComposing = view => Boolean(view.composing || view.compositionStarted);
|
|
948
|
+
const continueListMarkup = cm.insertNewlineContinueMarkupCommand({ nonTightLists: false });
|
|
949
|
+
const moveToTableDestination = (view, destination) => {
|
|
950
|
+
if (!destination || !Number.isSafeInteger(destination.position)) return false;
|
|
951
|
+
const materialization = syntheticTableCellMaterialization(destination.context);
|
|
952
|
+
if (materialization?.insert) {
|
|
953
|
+
view.dispatch({
|
|
954
|
+
changes: {
|
|
955
|
+
from: materialization.from,
|
|
956
|
+
to: materialization.from,
|
|
957
|
+
insert: materialization.insert
|
|
958
|
+
},
|
|
959
|
+
selection: { anchor: materialization.position },
|
|
960
|
+
effects: syntheticCellMaterializationEffect.of({
|
|
961
|
+
tableFrom: destination.context.table.block.from,
|
|
962
|
+
lineIndex: destination.context.line.index,
|
|
963
|
+
columnIndex: destination.context.columnIndex,
|
|
964
|
+
...materialization
|
|
965
|
+
}),
|
|
966
|
+
userEvent: "input.type",
|
|
967
|
+
scrollIntoView: true
|
|
968
|
+
});
|
|
969
|
+
return true;
|
|
970
|
+
}
|
|
971
|
+
view.dispatch({ selection: { anchor: destination.position }, scrollIntoView: true });
|
|
972
|
+
return true;
|
|
973
|
+
};
|
|
974
|
+
const moveTableCell = direction => view => {
|
|
975
|
+
if (tableViewIsComposing(view)) return false;
|
|
976
|
+
const selection = view.state.selection.main;
|
|
977
|
+
if (!selection.empty) return false;
|
|
978
|
+
const source = view.state.doc.toString();
|
|
979
|
+
const current = tableCellContextAt(source, selection.head);
|
|
980
|
+
const destination = tableCellNavigationDestination(
|
|
981
|
+
source,
|
|
982
|
+
{ anchor: selection.anchor, head: selection.head },
|
|
983
|
+
direction
|
|
984
|
+
);
|
|
985
|
+
if (!destination || (destination.position === selection.head && !destination.context.cell.synthetic)) {
|
|
986
|
+
const atProtectedOuterBoundary = current && (
|
|
987
|
+
(direction === "left" && selection.head <= current.cell.from && current.columnIndex === 0 && current.rowIndex === 0)
|
|
988
|
+
|| (
|
|
989
|
+
direction === "right"
|
|
990
|
+
&& selection.head >= current.cell.to
|
|
991
|
+
&& current.rowIndex === current.table.rows.length - 1
|
|
992
|
+
&& current.columnIndex === current.table.rows.at(-1)?.cells.at(-1)?.columnIndex
|
|
993
|
+
)
|
|
994
|
+
);
|
|
995
|
+
return Boolean(atProtectedOuterBoundary);
|
|
996
|
+
}
|
|
997
|
+
return moveToTableDestination(view, destination);
|
|
998
|
+
};
|
|
999
|
+
const protectTableBoundary = direction => view => {
|
|
1000
|
+
if (tableViewIsComposing(view)) return false;
|
|
1001
|
+
const selection = view.state.selection.main;
|
|
1002
|
+
const source = view.state.doc.toString();
|
|
1003
|
+
if (!selection.empty) {
|
|
1004
|
+
return tableSelectionCrossesStructure(source, { anchor: selection.anchor, head: selection.head });
|
|
1005
|
+
}
|
|
1006
|
+
const current = tableCellContextAt(source, selection.head);
|
|
1007
|
+
if (!current) return false;
|
|
1008
|
+
const pipeOffset = direction === "backward" ? selection.head - 1 : selection.head;
|
|
1009
|
+
const atVisibleBoundary = direction === "backward"
|
|
1010
|
+
? selection.head <= current.cell.from
|
|
1011
|
+
: selection.head >= current.cell.to;
|
|
1012
|
+
const atStructuralPipe = current.line.tablePipes.some(pipe => pipe.from === pipeOffset);
|
|
1013
|
+
if (!atVisibleBoundary && !atStructuralPipe) return false;
|
|
1014
|
+
const destination = tableCellNavigationDestination(
|
|
1015
|
+
source,
|
|
1016
|
+
{ anchor: selection.anchor, head: selection.head },
|
|
1017
|
+
direction === "backward" ? "previous" : "next"
|
|
1018
|
+
);
|
|
1019
|
+
if (destination && (
|
|
1020
|
+
destination.position !== selection.head || destination.context.cell.synthetic
|
|
1021
|
+
)) {
|
|
1022
|
+
moveToTableDestination(view, destination);
|
|
1023
|
+
}
|
|
1024
|
+
return true;
|
|
1025
|
+
};
|
|
1026
|
+
const enterTableCell = view => {
|
|
1027
|
+
if (tableViewIsComposing(view)) return false;
|
|
1028
|
+
const selection = view.state.selection.main;
|
|
1029
|
+
if (!selection.empty || !tableCellContextAt(view.state.doc.toString(), selection.head)) return false;
|
|
1030
|
+
const destination = tableCellNavigationDestination(
|
|
1031
|
+
view.state.doc.toString(),
|
|
1032
|
+
{ anchor: selection.anchor, head: selection.head },
|
|
1033
|
+
"down"
|
|
1034
|
+
);
|
|
1035
|
+
if (destination && (destination.position !== selection.head || destination.context.cell.synthetic)) {
|
|
1036
|
+
moveToTableDestination(view, destination);
|
|
1037
|
+
}
|
|
1038
|
+
return true;
|
|
1039
|
+
};
|
|
1040
|
+
const handleEnter = view => enterTableCell(view) || continueListMarkup(view);
|
|
1041
|
+
const replaceWithSafeTableCellInput = (view, from, to, text, userEvent = "input.type") => {
|
|
1042
|
+
if (tableViewIsComposing(view)) return false;
|
|
1043
|
+
const source = view.state.doc.toString();
|
|
1044
|
+
if (tableSelectionCrossesStructure(source, { anchor: from, head: to })) return true;
|
|
1045
|
+
const tables = tableCellContexts(source);
|
|
1046
|
+
const start = tableCellContextFromTables(tables, from);
|
|
1047
|
+
const end = tableCellContextFromTables(tables, to);
|
|
1048
|
+
if (!start || !end || start.line.index !== end.line.index || start.columnIndex !== end.columnIndex) return false;
|
|
1049
|
+
const safeText = safeTableCellReplacement(source, from, to, text);
|
|
1050
|
+
if (safeText === text) return false;
|
|
1051
|
+
view.dispatch({
|
|
1052
|
+
changes: { from, to, insert: safeText },
|
|
1053
|
+
selection: { anchor: from + safeText.length },
|
|
1054
|
+
userEvent,
|
|
1055
|
+
scrollIntoView: true
|
|
1056
|
+
});
|
|
1057
|
+
return true;
|
|
1058
|
+
};
|
|
1059
|
+
const tableInputHandler = cm.EditorView.inputHandler.of((view, from, to, text) => (
|
|
1060
|
+
replaceWithSafeTableCellInput(view, from, to, text)
|
|
1061
|
+
));
|
|
1062
|
+
const handleTableBeforeInput = (event, view) => {
|
|
1063
|
+
if (event.isComposing || tableViewIsComposing(view)) return false;
|
|
1064
|
+
const selection = view.state.selection.main;
|
|
1065
|
+
const crossesStructure = tableSelectionCrossesStructure(
|
|
1066
|
+
view.state.doc.toString(),
|
|
1067
|
+
{ anchor: selection.anchor, head: selection.head }
|
|
1068
|
+
);
|
|
1069
|
+
if (crossesStructure && /^(?:insert|delete)/.test(event.inputType)) {
|
|
1070
|
+
event.preventDefault();
|
|
1071
|
+
return true;
|
|
1072
|
+
}
|
|
1073
|
+
if (!["insertText", "insertReplacementText"].includes(event.inputType) || typeof event.data !== "string") {
|
|
1074
|
+
return false;
|
|
1075
|
+
}
|
|
1076
|
+
if (!replaceWithSafeTableCellInput(view, selection.from, selection.to, event.data, "input.type")) return false;
|
|
1077
|
+
event.preventDefault();
|
|
1078
|
+
return true;
|
|
1079
|
+
};
|
|
1080
|
+
const handleTablePaste = (event, view) => {
|
|
1081
|
+
if (tableViewIsComposing(view)) return false;
|
|
1082
|
+
const text = event.clipboardData?.getData("text/plain");
|
|
1083
|
+
if (typeof text !== "string") return false;
|
|
1084
|
+
const selection = view.state.selection.main;
|
|
1085
|
+
if (!replaceWithSafeTableCellInput(view, selection.from, selection.to, text, "input.paste")) return false;
|
|
1086
|
+
event.preventDefault();
|
|
1087
|
+
return true;
|
|
1088
|
+
};
|
|
1089
|
+
const validatesSyntheticCellMaterialization = (transaction, tables, request, changes) => {
|
|
1090
|
+
if (!request || changes.length !== 1) return false;
|
|
1091
|
+
const table = tables.find(candidate => candidate.block.from === request.tableFrom);
|
|
1092
|
+
const context = table?.cells.find(candidate => (
|
|
1093
|
+
candidate.line.index === request.lineIndex && candidate.columnIndex === request.columnIndex
|
|
1094
|
+
));
|
|
1095
|
+
const expected = syntheticTableCellMaterialization(context && { ...context, table });
|
|
1096
|
+
const change = changes[0];
|
|
1097
|
+
if (
|
|
1098
|
+
!expected?.insert
|
|
1099
|
+
|| request.from !== expected.from
|
|
1100
|
+
|| request.insert !== expected.insert
|
|
1101
|
+
|| request.position !== expected.position
|
|
1102
|
+
|| change.fromA !== expected.from
|
|
1103
|
+
|| change.toA !== expected.from
|
|
1104
|
+
|| change.insert !== expected.insert
|
|
1105
|
+
) return false;
|
|
1106
|
+
|
|
1107
|
+
const nextTable = tableCellContexts(transaction.newDoc.toString())
|
|
1108
|
+
.find(candidate => candidate.block.from === table.block.from);
|
|
1109
|
+
const nextLine = nextTable?.lines.find(line => line.index === context.line.index);
|
|
1110
|
+
const nextTarget = nextLine?.tableCells?.[context.columnIndex];
|
|
1111
|
+
if (
|
|
1112
|
+
!nextTable
|
|
1113
|
+
|| nextTable.block.columnCount !== table.block.columnCount
|
|
1114
|
+
|| nextTable.rows.length !== table.rows.length
|
|
1115
|
+
|| nextTable.lines.length !== table.lines.length
|
|
1116
|
+
|| nextTable.block.alignments.join("|") !== table.block.alignments.join("|")
|
|
1117
|
+
|| !nextTarget?.synthetic
|
|
1118
|
+
|| nextTarget.from !== expected.position
|
|
1119
|
+
|| nextLine.tableHasLeadingPipe !== context.line.tableHasLeadingPipe
|
|
1120
|
+
|| !nextLine.tableHasTrailingPipe
|
|
1121
|
+
) return false;
|
|
1122
|
+
|
|
1123
|
+
const existingText = context.line.tableCells.filter(cell => !cell.synthetic).map(cell => cell.text);
|
|
1124
|
+
const nextText = nextLine.tableCells.slice(0, existingText.length).map(cell => cell.text);
|
|
1125
|
+
if (existingText.join("\u0000") !== nextText.join("\u0000")) return false;
|
|
1126
|
+
if (nextLine.tableCells.slice(0, context.columnIndex).some(cell => cell.synthetic)) return false;
|
|
1127
|
+
const oldPipes = context.line.tablePipes.map(pipe => pipe.from);
|
|
1128
|
+
const nextPipes = nextLine.tablePipes.map(pipe => pipe.from);
|
|
1129
|
+
const addedPipes = Array.from({ length: expected.insert.length }, (_, index) => expected.from + index);
|
|
1130
|
+
return nextPipes.length === oldPipes.length + addedPipes.length
|
|
1131
|
+
&& oldPipes.every((position, index) => nextPipes[index] === position)
|
|
1132
|
+
&& addedPipes.every((position, index) => nextPipes[oldPipes.length + index] === position);
|
|
1133
|
+
};
|
|
1134
|
+
const tableStructureChangeFilter = cm.EditorState.changeFilter.of(transaction => {
|
|
1135
|
+
if (!transaction.docChanged) return true;
|
|
1136
|
+
if (transaction.isUserEvent?.("undo") || transaction.isUserEvent?.("redo")) return true;
|
|
1137
|
+
const source = transaction.startState.doc.toString();
|
|
1138
|
+
const tableOperations = transaction.effects.filter(effect => effect.is(tableOperationEffect));
|
|
1139
|
+
if (tableOperations.length) {
|
|
1140
|
+
if (tableOperations.length !== 1 || transaction.startState.readOnly) return false;
|
|
1141
|
+
const request = tableOperations[0].value;
|
|
1142
|
+
const expected = tableOperation(source, transaction.startState.selection.main, request.command, request.dimensions);
|
|
1143
|
+
if (!expected) return false;
|
|
1144
|
+
const actual = [];
|
|
1145
|
+
transaction.changes.iterChanges((from, to, _fromB, _toB, inserted) => actual.push({ from, to, insert: inserted.toString() }));
|
|
1146
|
+
return actual.length === 1 && actual[0].from === expected.changes.from && actual[0].to === expected.changes.to && actual[0].insert === expected.changes.insert;
|
|
1147
|
+
}
|
|
1148
|
+
const tables = tableCellContexts(source);
|
|
1149
|
+
if (!tables.length) return true;
|
|
1150
|
+
const changes = [];
|
|
1151
|
+
transaction.changes.iterChanges((fromA, toA, fromB, toB, inserted) => {
|
|
1152
|
+
changes.push({ fromA, toA, fromB, toB, insert: inserted.toString() });
|
|
1153
|
+
});
|
|
1154
|
+
const materializationEffects = transaction.effects.filter(effect => (
|
|
1155
|
+
effect.is(syntheticCellMaterializationEffect)
|
|
1156
|
+
));
|
|
1157
|
+
if (materializationEffects.length) {
|
|
1158
|
+
return materializationEffects.length === 1 && validatesSyntheticCellMaterialization(
|
|
1159
|
+
transaction,
|
|
1160
|
+
tables,
|
|
1161
|
+
materializationEffects[0].value,
|
|
1162
|
+
changes
|
|
1163
|
+
);
|
|
1164
|
+
}
|
|
1165
|
+
const touchedTables = tables.filter(table => {
|
|
1166
|
+
const guardFrom = Math.max(0, table.block.from - 1);
|
|
1167
|
+
let guardTo = Math.min(source.length, table.block.to + 1);
|
|
1168
|
+
const nextLineStart = table.block.to + 1;
|
|
1169
|
+
if (source[table.block.to] === "\n" && nextLineStart < source.length && source[nextLineStart] !== "\n") {
|
|
1170
|
+
const nextBreak = source.indexOf("\n", nextLineStart);
|
|
1171
|
+
guardTo = nextBreak < 0 ? source.length : nextBreak;
|
|
1172
|
+
}
|
|
1173
|
+
return changes.some(change => (
|
|
1174
|
+
change.fromA === change.toA
|
|
1175
|
+
? change.fromA >= guardFrom && change.fromA <= guardTo
|
|
1176
|
+
: change.fromA < guardTo && change.toA > guardFrom
|
|
1177
|
+
));
|
|
1178
|
+
});
|
|
1179
|
+
if (!touchedTables.length) return true;
|
|
1180
|
+
|
|
1181
|
+
for (const table of touchedTables) {
|
|
1182
|
+
const delimiter = table.lines.find(line => line.tableRole === "delimiter");
|
|
1183
|
+
for (const change of changes) {
|
|
1184
|
+
if (
|
|
1185
|
+
change.fromA === change.toA
|
|
1186
|
+
&& delimiter
|
|
1187
|
+
&& change.fromA >= delimiter.from
|
|
1188
|
+
&& change.fromA <= delimiter.to
|
|
1189
|
+
) return false;
|
|
1190
|
+
if (change.fromA === change.toA) continue;
|
|
1191
|
+
const structuralRanges = table.lines.flatMap((line, lineIndex) => {
|
|
1192
|
+
const ranges = line.tableRole === "delimiter"
|
|
1193
|
+
? [{ from: line.from, to: line.to }]
|
|
1194
|
+
: [...line.tablePipes];
|
|
1195
|
+
if (lineIndex < table.lines.length - 1) ranges.push({ from: line.to, to: line.to + 1 });
|
|
1196
|
+
return ranges;
|
|
1197
|
+
});
|
|
1198
|
+
if (table.block.from > 0 && source[table.block.from - 1] === "\n") {
|
|
1199
|
+
structuralRanges.push({ from: table.block.from - 1, to: table.block.from });
|
|
1200
|
+
}
|
|
1201
|
+
if (table.block.to < source.length && source[table.block.to] === "\n") {
|
|
1202
|
+
structuralRanges.push({ from: table.block.to, to: table.block.to + 1 });
|
|
1203
|
+
}
|
|
1204
|
+
if (structuralRanges.some(range => change.fromA < range.to && change.toA > range.from)) {
|
|
1205
|
+
return false;
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
const nextTables = tableCellContexts(transaction.newDoc.toString());
|
|
1211
|
+
return touchedTables.every(table => {
|
|
1212
|
+
const mappedFrom = transaction.changes.mapPos(table.block.from, -1);
|
|
1213
|
+
const mappedTo = transaction.changes.mapPos(table.block.to, 1);
|
|
1214
|
+
const nextTable = nextTables.find(candidate => candidate.block.from === mappedFrom);
|
|
1215
|
+
if (!nextTable) return false;
|
|
1216
|
+
if (nextTable.block.to !== mappedTo) return false;
|
|
1217
|
+
if (nextTable.block.columnCount !== table.block.columnCount) return false;
|
|
1218
|
+
if (nextTable.rows.length !== table.rows.length) return false;
|
|
1219
|
+
if (nextTable.lines.length !== table.lines.length) return false;
|
|
1220
|
+
if (nextTable.block.alignments.join("|") !== table.block.alignments.join("|")) return false;
|
|
1221
|
+
return nextTable.lines.every((line, index) => {
|
|
1222
|
+
const previousLine = table.lines[index];
|
|
1223
|
+
if (line.tableRole !== previousLine?.tableRole) return false;
|
|
1224
|
+
if (line.from !== transaction.changes.mapPos(previousLine.from, -1)) return false;
|
|
1225
|
+
if (line.to !== transaction.changes.mapPos(previousLine.to, 1)) return false;
|
|
1226
|
+
if (line.tableHasLeadingPipe !== previousLine.tableHasLeadingPipe) return false;
|
|
1227
|
+
if (line.tableRole === "delimiter" && line.tableRaw !== previousLine.tableRaw) return false;
|
|
1228
|
+
const mappedPipes = previousLine.tablePipes.map(pipe => transaction.changes.mapPos(pipe.from, 1));
|
|
1229
|
+
return mappedPipes.length === line.tablePipes.length
|
|
1230
|
+
&& mappedPipes.every((position, pipeIndex) => position === line.tablePipes[pipeIndex]?.from);
|
|
1231
|
+
});
|
|
1232
|
+
});
|
|
1233
|
+
});
|
|
280
1234
|
const state = cm.EditorState.create({
|
|
281
1235
|
doc: String(options.value ?? "").replace(/\r\n?/g, "\n"),
|
|
282
1236
|
extensions: [
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
1237
|
+
editableField,
|
|
1238
|
+
cm.EditorState.readOnly.compute([editableField], state => !state.field(editableField)),
|
|
1239
|
+
cm.EditorView.editable.compute([editableField], state => state.field(editableField)),
|
|
1240
|
+
cm.EditorState.changeFilter.of(transaction => !transaction.startState.readOnly),
|
|
1241
|
+
...(images?.extensions || []),
|
|
1242
|
+
cm.EditorView.editorAttributes.of({ class: "writer-markdown-live-editor" }),
|
|
1243
|
+
tableHistoryBoundary,
|
|
1244
|
+
cm.history({
|
|
1245
|
+
joinToEvent: (transaction, adjacent) => adjacent
|
|
1246
|
+
&& !transaction.startState.field(tableHistoryBoundary)
|
|
1247
|
+
&& !transaction.effects.some(effect => effect.is(tableOperationEffect))
|
|
286
1248
|
}),
|
|
287
|
-
cm.history(),
|
|
288
1249
|
cm.keymap.of([
|
|
289
1250
|
{ key: "Escape", run() { options.onEscape?.(); return true; } },
|
|
1251
|
+
{ key: "Enter", run: handleEnter },
|
|
1252
|
+
{ key: "Shift-Enter", run: enterTableCell },
|
|
1253
|
+
{ key: "Backspace", run: protectTableBoundary("backward") },
|
|
1254
|
+
{ key: "Backspace", run: cm.deleteMarkupBackward },
|
|
1255
|
+
{ key: "Delete", run: protectTableBoundary("forward") },
|
|
1256
|
+
{ key: "ArrowLeft", run: moveTableCell("left") },
|
|
1257
|
+
{ key: "ArrowRight", run: moveTableCell("right") },
|
|
1258
|
+
{ key: "ArrowUp", run: moveTableCell("up") },
|
|
1259
|
+
{ key: "ArrowDown", run: moveTableCell("down") },
|
|
1260
|
+
{ key: "Tab", run: moveTableCell("next"), shift: moveTableCell("previous") },
|
|
290
1261
|
...cm.defaultKeymap,
|
|
291
1262
|
...cm.historyKeymap
|
|
292
1263
|
]),
|
|
293
|
-
cm.markdown(),
|
|
1264
|
+
cm.markdown({ addKeymap: false }),
|
|
294
1265
|
cm.EditorView.lineWrapping,
|
|
1266
|
+
tableInputHandler,
|
|
1267
|
+
tableStructureChangeFilter,
|
|
295
1268
|
focusField,
|
|
296
1269
|
previewField,
|
|
1270
|
+
tableAtomicField,
|
|
297
1271
|
cm.EditorView.contentAttributes.of({
|
|
298
1272
|
role: "textbox",
|
|
299
1273
|
"aria-multiline": "true",
|
|
300
1274
|
"aria-label": String(options.ariaLabel || "Markdown 편집")
|
|
301
1275
|
}),
|
|
302
1276
|
cm.EditorView.updateListener.of(update => {
|
|
303
|
-
if (update.docChanged)
|
|
1277
|
+
if (update.docChanged) updateCounter(update.state.doc);
|
|
1278
|
+
if (update.docChanged && !replacingValue) options.onChange?.(update.state.doc.toString());
|
|
1279
|
+
toolbar?.refresh();
|
|
304
1280
|
}),
|
|
305
1281
|
cm.EditorView.domEventHandlers({
|
|
1282
|
+
mousedown: handleTableCellMouseDown,
|
|
1283
|
+
mouseup: handleTableCellMouseUp,
|
|
1284
|
+
beforeinput: handleTableBeforeInput,
|
|
1285
|
+
paste: handleTablePaste,
|
|
306
1286
|
focus() {
|
|
307
|
-
view.dispatch({ effects: focusEffect.of(true) });
|
|
1287
|
+
if (!view.state.readOnly) view.dispatch({ effects: focusEffect.of(true) });
|
|
308
1288
|
},
|
|
309
1289
|
blur() {
|
|
310
1290
|
view.dispatch({ effects: focusEffect.of(false) });
|
|
311
1291
|
setTimeout(() => {
|
|
312
|
-
if (!destroyed && !
|
|
1292
|
+
if (!destroyed && !images?.hasPending() && !toolbar?.ownsFocus() && !options.parent.contains(ownerDocument.activeElement)) options.onBlur?.();
|
|
313
1293
|
}, 0);
|
|
314
1294
|
}
|
|
315
1295
|
})
|
|
316
1296
|
]
|
|
317
1297
|
});
|
|
318
1298
|
const view = new cm.EditorView({ state, parent: options.parent });
|
|
1299
|
+
updateCounter(state.doc);
|
|
1300
|
+
options.parent.appendChild(counter);
|
|
1301
|
+
images?.attach(view);
|
|
1302
|
+
let toolbarDocument = null;
|
|
1303
|
+
let toolbarParsed = null;
|
|
1304
|
+
let toolbarState = null;
|
|
1305
|
+
let toolbarContext = null;
|
|
1306
|
+
const historyCommands = {
|
|
1307
|
+
undo: cm.historyKeymap.find(binding => binding.key === "Mod-z")?.run,
|
|
1308
|
+
redo: cm.historyKeymap.find(binding => binding.key === "Mod-y")?.run
|
|
1309
|
+
|| cm.historyKeymap.find(binding => binding.key === "Mod-z")?.shift
|
|
1310
|
+
};
|
|
1311
|
+
const formatContext = () => {
|
|
1312
|
+
if (destroyed || view.state.readOnly) return null;
|
|
1313
|
+
if (toolbarState === view.state) return toolbarContext;
|
|
1314
|
+
// Share one parse across buttons and reuse it while only the selection moves.
|
|
1315
|
+
if (toolbarDocument !== view.state.doc) {
|
|
1316
|
+
toolbarDocument = view.state.doc;
|
|
1317
|
+
const source = toolbarDocument.toString();
|
|
1318
|
+
toolbarParsed = { source, tokens: inlineTokens(source), blocks: markdownDocumentModel.parseDocument(source).blocks };
|
|
1319
|
+
}
|
|
1320
|
+
const selection = view.state.selection.main;
|
|
1321
|
+
const multiline = toolbarParsed.source.slice(selection.from, selection.to).includes("\n");
|
|
1322
|
+
const parts = multiline ? toolbarApi.inlineSelectionParts(toolbarParsed.source, selection.from, selection.to, toolbarParsed.tokens) : [];
|
|
1323
|
+
toolbarState = view.state;
|
|
1324
|
+
toolbarContext = {
|
|
1325
|
+
...toolbarParsed,
|
|
1326
|
+
selection,
|
|
1327
|
+
hasInlineText: !multiline || parts.length > 0,
|
|
1328
|
+
// Probe public state commands without dispatching their transactions.
|
|
1329
|
+
// Cache per EditorState so rendering button states never changes history.
|
|
1330
|
+
history: Object.fromEntries(Object.entries(historyCommands).map(([command, run]) => [
|
|
1331
|
+
command, Boolean(run?.({ state: view.state, dispatch() {} }))
|
|
1332
|
+
])),
|
|
1333
|
+
cell: tableCellContextAt(toolbarParsed.source, selection.head),
|
|
1334
|
+
label: options.ariaLabel || "본문 편집",
|
|
1335
|
+
marks: Object.entries(toolbarApi?.marks || {})
|
|
1336
|
+
.filter(([name, marker]) => multiline
|
|
1337
|
+
? parts.length > 0 && parts.every(part => toolbarApi.partMark(part, name, toolbarParsed.tokens))
|
|
1338
|
+
: toolbarParsed.tokens.some(token => (token.marker === marker || (name === "italic" && token.marker === "_")) && selection.from >= token.contentFrom && selection.to <= token.contentTo))
|
|
1339
|
+
.map(([name]) => name)
|
|
1340
|
+
};
|
|
1341
|
+
return toolbarContext;
|
|
1342
|
+
};
|
|
1343
|
+
const formatDisabledReason = command => {
|
|
1344
|
+
const context = formatContext();
|
|
1345
|
+
if (!context) return "본문을 먼저 선택하세요";
|
|
1346
|
+
if (view.composing || view.compositionStarted) return "한글 입력을 마친 뒤 사용하세요";
|
|
1347
|
+
if (view.state.selection.ranges.length !== 1) return "하나의 영역을 선택하세요";
|
|
1348
|
+
if (command === "undo" || command === "redo") return context.history[command]
|
|
1349
|
+
? "" : command === "undo" ? "실행 취소할 변경이 없습니다" : "다시 실행할 변경이 없습니다";
|
|
1350
|
+
const { source, selection, tokens, blocks, cell } = context;
|
|
1351
|
+
if (toolbarApi.marks[command] && selection.empty) return "꾸밀 텍스트를 먼저 선택하세요";
|
|
1352
|
+
if (toolbarApi.marks[command] && !context.hasInlineText) return "꾸밀 텍스트를 먼저 선택하세요";
|
|
1353
|
+
if (command === "table-delete") return tableOperation(source, selection, command) ? "" : "삭제할 표 안을 먼저 선택하세요";
|
|
1354
|
+
if (command === "table") return tableOperation(source, selection, command) ? "" : "한 표 안이나 일반 본문을 선택하세요";
|
|
1355
|
+
if (tokens.some(token => token.type === "code" && selection.from >= token.contentFrom && selection.to <= token.contentTo)) return "코드 밖에서 사용하세요";
|
|
1356
|
+
const touches = block => selection.from <= block.to && selection.to >= block.from;
|
|
1357
|
+
if (blocks.some(block => ["image", "code"].includes(block.type) && touches(block))) return "이미지·코드 블록 밖에서 사용하세요";
|
|
1358
|
+
const tableTouched = blocks.some(block => block.type === "table" && touches(block));
|
|
1359
|
+
if (tableTouched && (!toolbarApi.marks[command] || !cell || selection.from < cell.cell.from || selection.to > cell.cell.to || cell.cell.synthetic)) return "표 안에서는 한 셀의 글자 서식만 바꿀 수 있습니다";
|
|
1360
|
+
if (command === "image") return options.assets?.upload ? "" : "이 입력란에는 이미지 에셋 연결이 아직 없습니다";
|
|
1361
|
+
return "";
|
|
1362
|
+
};
|
|
1363
|
+
toolbar = toolbarApi?.register(ownerDocument, {
|
|
1364
|
+
contains: node => options.parent.contains(node),
|
|
1365
|
+
context: formatContext,
|
|
1366
|
+
disabledReason: formatDisabledReason,
|
|
1367
|
+
focus: () => view.focus(),
|
|
1368
|
+
blur: () => { if (!destroyed && !images?.hasPending()) options.onBlur?.(); },
|
|
1369
|
+
run(command, dimensions) {
|
|
1370
|
+
if (formatDisabledReason(command)) return false;
|
|
1371
|
+
if (command === "table" || command === "table-delete") {
|
|
1372
|
+
const { source, selection } = formatContext();
|
|
1373
|
+
const edit = tableOperation(source, selection, command, dimensions);
|
|
1374
|
+
if (!edit) return false;
|
|
1375
|
+
view.dispatch({ ...edit, effects: tableOperationEffect.of({ command, dimensions }), userEvent: command === "table" ? "input.table" : "delete.table", scrollIntoView: true });
|
|
1376
|
+
view.focus();
|
|
1377
|
+
return true;
|
|
1378
|
+
}
|
|
1379
|
+
if (command === "image") {
|
|
1380
|
+
images?.chooseAsset();
|
|
1381
|
+
return true;
|
|
1382
|
+
}
|
|
1383
|
+
if (command === "undo" || command === "redo") {
|
|
1384
|
+
const run = historyCommands[command];
|
|
1385
|
+
run?.(view);
|
|
1386
|
+
view.focus();
|
|
1387
|
+
return true;
|
|
1388
|
+
}
|
|
1389
|
+
const { source, selection, tokens } = formatContext();
|
|
1390
|
+
const edit = toolbarApi.change(source, selection.anchor, selection.head, command, tokens);
|
|
1391
|
+
if (!edit) return false;
|
|
1392
|
+
view.dispatch({ ...edit, userEvent: "input.format" });
|
|
1393
|
+
view.focus();
|
|
1394
|
+
return true;
|
|
1395
|
+
}
|
|
1396
|
+
});
|
|
1397
|
+
const onToolbarKey = event => {
|
|
1398
|
+
if (event.altKey && event.key === "F10") { event.preventDefault(); toolbar?.focus(); }
|
|
1399
|
+
};
|
|
1400
|
+
const onComposition = () => { toolbar?.refresh(); };
|
|
1401
|
+
const onCompositionEnd = () => { setTimeout(onComposition, 0); };
|
|
1402
|
+
options.parent.addEventListener("keydown", onToolbarKey);
|
|
1403
|
+
options.parent.addEventListener("compositionstart", onComposition);
|
|
1404
|
+
options.parent.addEventListener("compositionend", onCompositionEnd);
|
|
1405
|
+
options.parent.toggleAttribute("data-markdown-readonly", view.state.readOnly);
|
|
319
1406
|
return Object.freeze({
|
|
1407
|
+
setEditable(editable) {
|
|
1408
|
+
view.dispatch({ effects: [editableEffect.of(Boolean(editable)), focusEffect.of(Boolean(editable) && view.hasFocus)] });
|
|
1409
|
+
options.parent.toggleAttribute("data-markdown-readonly", !editable);
|
|
1410
|
+
},
|
|
1411
|
+
setValue(value) {
|
|
1412
|
+
const text = String(value ?? "").replace(/\r\n?/g, "\n");
|
|
1413
|
+
if (text === view.state.doc.toString()) return;
|
|
1414
|
+
replacingValue = true;
|
|
1415
|
+
try { view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: text }, filter: false }); }
|
|
1416
|
+
finally { replacingValue = false; }
|
|
1417
|
+
},
|
|
1418
|
+
hasPendingImages: () => images?.hasPending() || false,
|
|
320
1419
|
getValue: () => view.state.doc.toString(),
|
|
321
1420
|
getSelection: () => view.state.selection.ranges.map(range => ({ anchor: range.anchor, head: range.head })),
|
|
322
1421
|
focusAtPosition(position, options = {}) {
|
|
@@ -330,21 +1429,58 @@
|
|
|
330
1429
|
view.focus();
|
|
331
1430
|
},
|
|
332
1431
|
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;
|
|
1432
|
+
const anchor = this.positionAtClientPoint(clientX, clientY);
|
|
337
1433
|
view.dispatch({ selection: { anchor } });
|
|
338
1434
|
view.focus();
|
|
339
1435
|
return anchor;
|
|
340
1436
|
},
|
|
1437
|
+
positionAtClientPoint(clientX, clientY) {
|
|
1438
|
+
let position = null;
|
|
1439
|
+
if (Number.isFinite(clientX) && Number.isFinite(clientY)) {
|
|
1440
|
+
const cellElement = ownerDocument.elementFromPoint?.(clientX, clientY)?.closest?.(".writer-md-table-cell");
|
|
1441
|
+
if (cellElement && view.dom.contains(cellElement)) {
|
|
1442
|
+
const from = Number(cellElement.getAttribute("data-writer-md-table-cell-from"));
|
|
1443
|
+
const to = Number(cellElement.getAttribute("data-writer-md-table-cell-to"));
|
|
1444
|
+
if (Number.isSafeInteger(from) && Number.isSafeInteger(to) && from <= to) {
|
|
1445
|
+
position = tableCellPointerPosition(view, cellElement, { clientX, clientY }, from, to);
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
if (position == null) position = view.posAtCoords({ x: clientX, y: clientY }, false);
|
|
1449
|
+
}
|
|
1450
|
+
const anchor = position == null ? view.state.doc.length : position;
|
|
1451
|
+
return anchor;
|
|
1452
|
+
},
|
|
341
1453
|
destroy() {
|
|
342
1454
|
if (destroyed) return;
|
|
343
1455
|
destroyed = true;
|
|
1456
|
+
toolbar?.destroy();
|
|
1457
|
+
options.parent.removeEventListener("keydown",onToolbarKey);
|
|
1458
|
+
options.parent.removeEventListener("compositionstart",onComposition);
|
|
1459
|
+
options.parent.removeEventListener("compositionend",onCompositionEnd);
|
|
1460
|
+
options.parent.removeAttribute("data-markdown-readonly");
|
|
1461
|
+
images?.destroy();
|
|
344
1462
|
view.destroy();
|
|
1463
|
+
counter.remove();
|
|
345
1464
|
}
|
|
346
1465
|
});
|
|
347
1466
|
}
|
|
348
1467
|
|
|
349
|
-
return Object.freeze({
|
|
1468
|
+
return Object.freeze({
|
|
1469
|
+
characterCount,
|
|
1470
|
+
inlineTokens,
|
|
1471
|
+
tableEscapedPipeBackslashes,
|
|
1472
|
+
tableCellEdgeWhitespaceRanges,
|
|
1473
|
+
lineSyntax,
|
|
1474
|
+
layoutLines,
|
|
1475
|
+
tableCellContexts,
|
|
1476
|
+
tableCellContextAt,
|
|
1477
|
+
tableSelectionCrossesStructure,
|
|
1478
|
+
tableCellNavigationTarget,
|
|
1479
|
+
syntheticTableCellMaterialization,
|
|
1480
|
+
safeTableCellInput,
|
|
1481
|
+
safeTableCellReplacement,
|
|
1482
|
+
tableOperation,
|
|
1483
|
+
previewModel,
|
|
1484
|
+
mount
|
|
1485
|
+
});
|
|
350
1486
|
});
|