@ssobig/writer-cli 0.3.2 → 0.3.4
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/package.json +1 -1
- package/templates/mystery-v1/authoring-view-preference.js +25 -2
- package/templates/mystery-v1/component-asset-operations.js +5 -4
- package/templates/mystery-v1/component-catalog-contract.js +3 -3
- package/templates/mystery-v1/component-field-contracts.js +37 -0
- package/templates/mystery-v1/component-renderers.js +1 -1
- package/templates/mystery-v1/component-storage-contract.js +3 -1
- package/templates/mystery-v1/markdown-document-model.js +15 -1
- package/templates/mystery-v1/markdown-image-editor.js +24 -8
- package/templates/mystery-v1/markdown-live-editor.js +275 -13
- package/templates/mystery-v1/markdown-toolbar.js +514 -0
- package/templates/mystery-v1/timeline-model.js +62 -1
- package/tools/writer-cli/package-lock.json +2 -2
- package/tools/writer-cli/package.json +1 -1
|
@@ -2,17 +2,22 @@
|
|
|
2
2
|
const commonJs = typeof module === "object" && module.exports;
|
|
3
3
|
const CodeMirror6 = commonJs ? null : root?.WriterCodeMirror6;
|
|
4
4
|
const markdownDocumentModel = commonJs ? require("./markdown-document-model.js") : root?.WriterMarkdownDocumentModel;
|
|
5
|
-
const
|
|
5
|
+
const toolbarApi = commonJs ? require("./markdown-toolbar.js") : root?.WriterMarkdownToolbar;
|
|
6
|
+
const api = factory(CodeMirror6, markdownDocumentModel, toolbarApi);
|
|
6
7
|
if (commonJs) module.exports = api;
|
|
7
8
|
if (root) root.WriterMarkdownLiveEditor = api;
|
|
8
|
-
})(typeof globalThis !== "undefined" ? globalThis : this, function (CodeMirror6, markdownDocumentModel) {
|
|
9
|
+
})(typeof globalThis !== "undefined" ? globalThis : this, function (CodeMirror6, markdownDocumentModel, toolbarApi) {
|
|
9
10
|
"use strict";
|
|
10
11
|
|
|
11
12
|
const INLINE_MARKERS = Object.freeze([
|
|
13
|
+
{ marker: "<u>", close: "</u>", type: "underline" },
|
|
14
|
+
{ marker: "***", type: "strong-emphasis" },
|
|
12
15
|
{ marker: "++", type: "brand" },
|
|
13
16
|
{ marker: "**", type: "strong" },
|
|
14
17
|
{ marker: "~~", type: "strikethrough" },
|
|
15
|
-
{ marker: "==", type: "highlight" }
|
|
18
|
+
{ marker: "==", type: "highlight" },
|
|
19
|
+
{ marker: "*", type: "emphasis" },
|
|
20
|
+
{ marker: "_", type: "emphasis" }
|
|
16
21
|
]);
|
|
17
22
|
function isEscapedAt(source, index) {
|
|
18
23
|
let backslashes = 0;
|
|
@@ -113,19 +118,25 @@
|
|
|
113
118
|
continue;
|
|
114
119
|
}
|
|
115
120
|
const definition = INLINE_MARKERS.find(item => source.startsWith(item.marker, index));
|
|
116
|
-
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)) {
|
|
117
124
|
index += 1;
|
|
118
125
|
continue;
|
|
119
126
|
}
|
|
120
127
|
const contentFrom = index + definition.marker.length;
|
|
121
|
-
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
|
+
}
|
|
122
133
|
const crossesBlockBoundary = closeFrom > contentFrom && /\n[ \t]*\n/.test(source.slice(contentFrom, closeFrom));
|
|
123
134
|
if (closeFrom <= contentFrom || crossesBlockBoundary || (closeFrom > 0 && source[closeFrom - 1] === "\\")) {
|
|
124
135
|
index += definition.marker.length;
|
|
125
136
|
continue;
|
|
126
137
|
}
|
|
127
|
-
const to = closeFrom +
|
|
128
|
-
|
|
138
|
+
const to = closeFrom + closeMarker.length;
|
|
139
|
+
const token = {
|
|
129
140
|
type: definition.type,
|
|
130
141
|
marker: definition.marker,
|
|
131
142
|
from: index,
|
|
@@ -134,7 +145,11 @@
|
|
|
134
145
|
contentTo: closeFrom,
|
|
135
146
|
closeFrom,
|
|
136
147
|
to
|
|
137
|
-
}
|
|
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);
|
|
138
153
|
index = to;
|
|
139
154
|
}
|
|
140
155
|
return tokens;
|
|
@@ -149,6 +164,45 @@
|
|
|
149
164
|
});
|
|
150
165
|
}
|
|
151
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
|
+
|
|
152
206
|
function selectionTouchesRange(selection, from, to) {
|
|
153
207
|
return selection.from === selection.to
|
|
154
208
|
? selection.head >= from && selection.head <= to
|
|
@@ -225,6 +279,10 @@
|
|
|
225
279
|
};
|
|
226
280
|
documentModel.blocks.forEach(sourceBlock => {
|
|
227
281
|
if (sourceBlock.type === "blank") return;
|
|
282
|
+
if (sourceBlock.type === "horizontalRule") {
|
|
283
|
+
assignBlock("horizontalRule", [sourceBlock.lineIndex]);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
228
286
|
if (sourceBlock.type === "image") {
|
|
229
287
|
assignBlock("image", [sourceBlock.lineIndex], { assetId: sourceBlock.assetId });
|
|
230
288
|
return;
|
|
@@ -532,6 +590,30 @@
|
|
|
532
590
|
return values.join(" ");
|
|
533
591
|
}
|
|
534
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
|
+
|
|
535
617
|
function mount(options = {}) {
|
|
536
618
|
const cm = CodeMirror6;
|
|
537
619
|
if (!cm?.EditorState || !cm?.StateEffect || !cm?.EditorView || !cm?.Decoration || !cm?.StateField) {
|
|
@@ -539,8 +621,22 @@
|
|
|
539
621
|
}
|
|
540
622
|
if (!options.parent) throw new Error("Markdown 편집기 표시 영역이 없습니다.");
|
|
541
623
|
let destroyed = false;
|
|
624
|
+
let toolbar = null;
|
|
542
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
|
+
};
|
|
543
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
|
+
});
|
|
544
640
|
const editableField = cm.StateField.define({
|
|
545
641
|
create: () => options.readOnly !== true,
|
|
546
642
|
update(value, transaction) {
|
|
@@ -553,7 +649,7 @@
|
|
|
553
649
|
const images = imageEditor?.create({
|
|
554
650
|
cm, model: markdownDocumentModel, assets: options.assets, parent: options.parent,
|
|
555
651
|
onIdle: () => setTimeout(() => {
|
|
556
|
-
if (!destroyed && !images?.hasPending() && !options.parent.contains(ownerDocument.activeElement)) options.onBlur?.();
|
|
652
|
+
if (!destroyed && !images?.hasPending() && !toolbar?.ownsFocus() && !options.parent.contains(ownerDocument.activeElement)) options.onBlur?.();
|
|
557
653
|
}, 0),
|
|
558
654
|
canInsert: (source, from, to) => !markdownDocumentModel.parseDocument(source).blocks.some(block => block.type === "table" && from <= block.to && to >= block.from)
|
|
559
655
|
});
|
|
@@ -568,6 +664,35 @@
|
|
|
568
664
|
return focused;
|
|
569
665
|
}
|
|
570
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
|
+
}
|
|
571
696
|
const buildDecorations = state => {
|
|
572
697
|
const selections = state.field(focusField)
|
|
573
698
|
? state.selection.ranges.map(range => ({ anchor: range.anchor, head: range.head }))
|
|
@@ -589,6 +714,10 @@
|
|
|
589
714
|
attributes.style = `--writer-md-table-column-count:${columnCount};--writer-md-table-column-width:${columnWidth}%;--writer-md-table-min-width:${minimumWidth}em`;
|
|
590
715
|
}
|
|
591
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
|
+
}
|
|
592
721
|
if (line.blockType === "table") {
|
|
593
722
|
line.tableCells.forEach((cell, columnIndex) => {
|
|
594
723
|
if (cell.rawFrom >= cell.rawTo) return;
|
|
@@ -646,7 +775,10 @@
|
|
|
646
775
|
inclusiveEnd: false
|
|
647
776
|
}).range(from, to));
|
|
648
777
|
}
|
|
649
|
-
if (line.
|
|
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) {
|
|
650
782
|
const from = line.from + line.listMarkerFrom;
|
|
651
783
|
const to = line.from + line.listMarkerTo;
|
|
652
784
|
ranges.push(cm.Decoration.mark({
|
|
@@ -669,7 +801,8 @@
|
|
|
669
801
|
},
|
|
670
802
|
update(decorations, transaction) {
|
|
671
803
|
const focusChanged = transaction.effects.some(effect => effect.is(focusEffect));
|
|
672
|
-
|
|
804
|
+
const editableChanged = transaction.effects.some(effect => effect.is(editableEffect));
|
|
805
|
+
return transaction.docChanged || transaction.selection || focusChanged || editableChanged ? buildDecorations(transaction.state) : decorations;
|
|
673
806
|
},
|
|
674
807
|
provide: field => cm.EditorView.decorations.from(field)
|
|
675
808
|
});
|
|
@@ -1002,6 +1135,16 @@
|
|
|
1002
1135
|
if (!transaction.docChanged) return true;
|
|
1003
1136
|
if (transaction.isUserEvent?.("undo") || transaction.isUserEvent?.("redo")) return true;
|
|
1004
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
|
+
}
|
|
1005
1148
|
const tables = tableCellContexts(source);
|
|
1006
1149
|
if (!tables.length) return true;
|
|
1007
1150
|
const changes = [];
|
|
@@ -1097,7 +1240,12 @@
|
|
|
1097
1240
|
cm.EditorState.changeFilter.of(transaction => !transaction.startState.readOnly),
|
|
1098
1241
|
...(images?.extensions || []),
|
|
1099
1242
|
cm.EditorView.editorAttributes.of({ class: "writer-markdown-live-editor" }),
|
|
1100
|
-
|
|
1243
|
+
tableHistoryBoundary,
|
|
1244
|
+
cm.history({
|
|
1245
|
+
joinToEvent: (transaction, adjacent) => adjacent
|
|
1246
|
+
&& !transaction.startState.field(tableHistoryBoundary)
|
|
1247
|
+
&& !transaction.effects.some(effect => effect.is(tableOperationEffect))
|
|
1248
|
+
}),
|
|
1101
1249
|
cm.keymap.of([
|
|
1102
1250
|
{ key: "Escape", run() { options.onEscape?.(); return true; } },
|
|
1103
1251
|
{ key: "Enter", run: handleEnter },
|
|
@@ -1126,7 +1274,9 @@
|
|
|
1126
1274
|
"aria-label": String(options.ariaLabel || "Markdown 편집")
|
|
1127
1275
|
}),
|
|
1128
1276
|
cm.EditorView.updateListener.of(update => {
|
|
1277
|
+
if (update.docChanged) updateCounter(update.state.doc);
|
|
1129
1278
|
if (update.docChanged && !replacingValue) options.onChange?.(update.state.doc.toString());
|
|
1279
|
+
toolbar?.refresh();
|
|
1130
1280
|
}),
|
|
1131
1281
|
cm.EditorView.domEventHandlers({
|
|
1132
1282
|
mousedown: handleTableCellMouseDown,
|
|
@@ -1139,14 +1289,119 @@
|
|
|
1139
1289
|
blur() {
|
|
1140
1290
|
view.dispatch({ effects: focusEffect.of(false) });
|
|
1141
1291
|
setTimeout(() => {
|
|
1142
|
-
if (!destroyed && !images?.hasPending() && !options.parent.contains(ownerDocument.activeElement)) options.onBlur?.();
|
|
1292
|
+
if (!destroyed && !images?.hasPending() && !toolbar?.ownsFocus() && !options.parent.contains(ownerDocument.activeElement)) options.onBlur?.();
|
|
1143
1293
|
}, 0);
|
|
1144
1294
|
}
|
|
1145
1295
|
})
|
|
1146
1296
|
]
|
|
1147
1297
|
});
|
|
1148
1298
|
const view = new cm.EditorView({ state, parent: options.parent });
|
|
1299
|
+
updateCounter(state.doc);
|
|
1300
|
+
options.parent.appendChild(counter);
|
|
1149
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);
|
|
1150
1405
|
options.parent.toggleAttribute("data-markdown-readonly", view.state.readOnly);
|
|
1151
1406
|
return Object.freeze({
|
|
1152
1407
|
setEditable(editable) {
|
|
@@ -1198,14 +1453,20 @@
|
|
|
1198
1453
|
destroy() {
|
|
1199
1454
|
if (destroyed) return;
|
|
1200
1455
|
destroyed = true;
|
|
1456
|
+
toolbar?.destroy();
|
|
1457
|
+
options.parent.removeEventListener("keydown",onToolbarKey);
|
|
1458
|
+
options.parent.removeEventListener("compositionstart",onComposition);
|
|
1459
|
+
options.parent.removeEventListener("compositionend",onCompositionEnd);
|
|
1201
1460
|
options.parent.removeAttribute("data-markdown-readonly");
|
|
1202
1461
|
images?.destroy();
|
|
1203
1462
|
view.destroy();
|
|
1463
|
+
counter.remove();
|
|
1204
1464
|
}
|
|
1205
1465
|
});
|
|
1206
1466
|
}
|
|
1207
1467
|
|
|
1208
1468
|
return Object.freeze({
|
|
1469
|
+
characterCount,
|
|
1209
1470
|
inlineTokens,
|
|
1210
1471
|
tableEscapedPipeBackslashes,
|
|
1211
1472
|
tableCellEdgeWhitespaceRanges,
|
|
@@ -1218,6 +1479,7 @@
|
|
|
1218
1479
|
syntheticTableCellMaterialization,
|
|
1219
1480
|
safeTableCellInput,
|
|
1220
1481
|
safeTableCellReplacement,
|
|
1482
|
+
tableOperation,
|
|
1221
1483
|
previewModel,
|
|
1222
1484
|
mount
|
|
1223
1485
|
});
|