@seamlet/astro 0.7.0 → 0.8.1
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 +3 -1
- package/dist/bridge.d.ts +3 -3
- package/dist/bridge.js +324 -12
- package/dist/chunk-27XIZEVS.js +705 -0
- package/dist/{content-DeBXwSmg.d.ts → content-6k-E7NWf.d.ts} +1 -1
- package/dist/{messages-ClePO8N_.d.ts → messages-Dx0jNy3N.d.ts} +76 -4
- package/dist/{parse-content-DqToE6I6.d.ts → parse-content-C9ldnmvj.d.ts} +1 -1
- package/dist/protocol.d.ts +16 -4
- package/dist/protocol.js +25 -3
- package/dist/schema.d.ts +4 -4
- package/dist/site.d.ts +1 -1
- package/package.json +1 -1
- package/dist/chunk-4YLS3VPT.js +0 -160
|
@@ -0,0 +1,705 @@
|
|
|
1
|
+
import {
|
|
2
|
+
formatFieldPath,
|
|
3
|
+
normalizedLine,
|
|
4
|
+
readRichText,
|
|
5
|
+
resolveFieldPath
|
|
6
|
+
} from "./chunk-ZUJEM3P7.js";
|
|
7
|
+
|
|
8
|
+
// src/bridge/editor-shortcuts.ts
|
|
9
|
+
function shortcutOf(event) {
|
|
10
|
+
const hasCommandKey = event.metaKey || event.ctrlKey;
|
|
11
|
+
if (!hasCommandKey || event.altKey) {
|
|
12
|
+
return void 0;
|
|
13
|
+
}
|
|
14
|
+
switch (event.code) {
|
|
15
|
+
case "KeyS":
|
|
16
|
+
return "save";
|
|
17
|
+
case "KeyZ":
|
|
18
|
+
return event.shiftKey ? "redo" : "undo";
|
|
19
|
+
case "KeyY":
|
|
20
|
+
return "redo";
|
|
21
|
+
default:
|
|
22
|
+
return void 0;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// src/bridge/image-fields.ts
|
|
27
|
+
function imageFieldAt(schema, path) {
|
|
28
|
+
const target = path === void 0 ? void 0 : resolveFieldPath(schema, path);
|
|
29
|
+
const isImage = target?.kind === "field" && target.field.kind === "image";
|
|
30
|
+
return isImage ? { path: target.path, field: target.field } : void 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// src/bridge/messages.ts
|
|
34
|
+
var PROTOCOL_VERSION = 2;
|
|
35
|
+
var EDITOR_KEYS = /* @__PURE__ */ new Set([
|
|
36
|
+
"save",
|
|
37
|
+
"undo",
|
|
38
|
+
"redo",
|
|
39
|
+
"escape"
|
|
40
|
+
]);
|
|
41
|
+
var TEXT_FORMATS = /* @__PURE__ */ new Set([
|
|
42
|
+
"bold",
|
|
43
|
+
"italic",
|
|
44
|
+
"list"
|
|
45
|
+
]);
|
|
46
|
+
var TYPING_OUTCOMES = /* @__PURE__ */ new Set([
|
|
47
|
+
"kept",
|
|
48
|
+
"reverted"
|
|
49
|
+
]);
|
|
50
|
+
function readPageMessage(data) {
|
|
51
|
+
if (!isRecord(data)) return void 0;
|
|
52
|
+
return isPageMessage(data) ? data : void 0;
|
|
53
|
+
}
|
|
54
|
+
function readEditorMessage(data) {
|
|
55
|
+
if (!isRecord(data)) return void 0;
|
|
56
|
+
return isEditorMessage(data) ? data : void 0;
|
|
57
|
+
}
|
|
58
|
+
function isPageMessage(message) {
|
|
59
|
+
switch (message["type"]) {
|
|
60
|
+
case "ready":
|
|
61
|
+
return isNumber(message["protocol"]);
|
|
62
|
+
case "file-drag-end":
|
|
63
|
+
case "rendered":
|
|
64
|
+
case "render-failed":
|
|
65
|
+
return true;
|
|
66
|
+
case "layout":
|
|
67
|
+
return isLayout(message["layout"]);
|
|
68
|
+
case "hover":
|
|
69
|
+
case "file-drag":
|
|
70
|
+
return isOptional(message["path"], isString);
|
|
71
|
+
case "click":
|
|
72
|
+
return isOptional(message["target"], isClickTarget);
|
|
73
|
+
case "link-click":
|
|
74
|
+
return isString(message["href"]);
|
|
75
|
+
case "key":
|
|
76
|
+
return isString(message["key"]) && EDITOR_KEYS.has(message["key"]);
|
|
77
|
+
case "file-drop":
|
|
78
|
+
return isString(message["path"]) && message["file"] instanceof File;
|
|
79
|
+
case "typing-started":
|
|
80
|
+
case "typing-refused":
|
|
81
|
+
return isString(message["path"]);
|
|
82
|
+
case "typed":
|
|
83
|
+
return isString(message["path"]) && isString(message["text"]);
|
|
84
|
+
case "rich-typed":
|
|
85
|
+
return isString(message["path"]) && isRichText(message["text"]);
|
|
86
|
+
case "typed-selection":
|
|
87
|
+
return isString(message["path"]) && isOptional(message["selection"], isTypedSelection);
|
|
88
|
+
case "typing-ended":
|
|
89
|
+
return isString(message["path"]) && isString(message["outcome"]) && TYPING_OUTCOMES.has(message["outcome"]);
|
|
90
|
+
default:
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function isEditorMessage(message) {
|
|
95
|
+
switch (message["type"]) {
|
|
96
|
+
case "connect":
|
|
97
|
+
return isRecord(message["schema"]);
|
|
98
|
+
case "render":
|
|
99
|
+
return isRecord(message["content"]);
|
|
100
|
+
case "start-typing":
|
|
101
|
+
return isString(message["path"]) && isString(message["text"]) && typeof message["isSingleLine"] === "boolean" && isPoint(message["point"]);
|
|
102
|
+
case "start-rich-typing":
|
|
103
|
+
return isString(message["path"]) && isRichText(message["text"]) && typeof message["isInline"] === "boolean" && isPoint(message["point"]);
|
|
104
|
+
case "format-typed":
|
|
105
|
+
return isString(message["format"]) && TEXT_FORMATS.has(message["format"]);
|
|
106
|
+
case "link-typed":
|
|
107
|
+
return isOptional(message["href"], isString);
|
|
108
|
+
case "stop-typing":
|
|
109
|
+
return true;
|
|
110
|
+
case "scroll-to":
|
|
111
|
+
return isFieldPath(message["path"]);
|
|
112
|
+
default:
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function isLayout(value) {
|
|
117
|
+
if (!isRecord(value)) return false;
|
|
118
|
+
const markers = value["markers"];
|
|
119
|
+
return Array.isArray(markers) && markers.every(isMarkerBox) && isViewport(value["viewport"]) && isOptional(value["link"], isLinkBox);
|
|
120
|
+
}
|
|
121
|
+
function isMarkerBox(value) {
|
|
122
|
+
return isRecord(value) && isString(value["path"]) && isBox(value["box"]);
|
|
123
|
+
}
|
|
124
|
+
function isLinkBox(value) {
|
|
125
|
+
return isRecord(value) && isString(value["href"]) && isBox(value["box"]);
|
|
126
|
+
}
|
|
127
|
+
function isClickTarget(value) {
|
|
128
|
+
return isRecord(value) && isString(value["path"]) && isPoint(value["point"]);
|
|
129
|
+
}
|
|
130
|
+
function isRichText(value) {
|
|
131
|
+
return readRichText(value) !== void 0;
|
|
132
|
+
}
|
|
133
|
+
function isTypedSelection(value) {
|
|
134
|
+
return isRecord(value) && isBox(value["box"]) && typeof value["isCollapsed"] === "boolean" && isTextFormats(value["formats"]);
|
|
135
|
+
}
|
|
136
|
+
function isTextFormats(value) {
|
|
137
|
+
return isRecord(value) && typeof value["isBold"] === "boolean" && typeof value["isItalic"] === "boolean" && typeof value["isList"] === "boolean" && isOptional(value["href"], isString);
|
|
138
|
+
}
|
|
139
|
+
function isBox(value) {
|
|
140
|
+
return isRecord(value) && isNumber(value["top"]) && isNumber(value["left"]) && isNumber(value["width"]) && isNumber(value["height"]);
|
|
141
|
+
}
|
|
142
|
+
function isViewport(value) {
|
|
143
|
+
return isRecord(value) && isNumber(value["width"]) && isNumber(value["height"]);
|
|
144
|
+
}
|
|
145
|
+
function isPoint(value) {
|
|
146
|
+
return isRecord(value) && isNumber(value["x"]) && isNumber(value["y"]);
|
|
147
|
+
}
|
|
148
|
+
function isFieldPath(value) {
|
|
149
|
+
return Array.isArray(value) && value.every((segment) => isString(segment) || isNumber(segment));
|
|
150
|
+
}
|
|
151
|
+
function isOptional(value, isPresent) {
|
|
152
|
+
return value === void 0 || isPresent(value);
|
|
153
|
+
}
|
|
154
|
+
function isRecord(value) {
|
|
155
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
156
|
+
}
|
|
157
|
+
function isString(value) {
|
|
158
|
+
return typeof value === "string";
|
|
159
|
+
}
|
|
160
|
+
function isNumber(value) {
|
|
161
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// src/bridge/file-drags.ts
|
|
165
|
+
function carriesFiles(event) {
|
|
166
|
+
return event.dataTransfer?.types.includes("Files") === true;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// src/bridge/marked-paths.ts
|
|
170
|
+
function closestMarkedPath(path, isMarked) {
|
|
171
|
+
for (let length = path.length; length > 0; length -= 1) {
|
|
172
|
+
const candidate = formatFieldPath(path.slice(0, length));
|
|
173
|
+
if (isMarked(candidate)) return candidate;
|
|
174
|
+
}
|
|
175
|
+
return void 0;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// src/bridge/rich-text-edits.ts
|
|
179
|
+
function isCollapsed(selection) {
|
|
180
|
+
return selection.start.line === selection.end.line && selection.start.offset === selection.end.offset;
|
|
181
|
+
}
|
|
182
|
+
function formatsAt(text, selection) {
|
|
183
|
+
const lines = flatLinesOf(text);
|
|
184
|
+
const spans = isCollapsed(selection) ? spansAtCursor(lines, selection.start) : selectedSpans(lines, selection);
|
|
185
|
+
const hasSpans = spans.length > 0;
|
|
186
|
+
const firstHref = spans[0]?.href;
|
|
187
|
+
const isInOneLink = hasSpans && spans.every((span) => span.href === firstHref);
|
|
188
|
+
const selectedLines = lines.slice(
|
|
189
|
+
selection.start.line,
|
|
190
|
+
selection.end.line + 1
|
|
191
|
+
);
|
|
192
|
+
return {
|
|
193
|
+
isBold: hasSpans && spans.every((span) => span.bold === true),
|
|
194
|
+
isItalic: hasSpans && spans.every((span) => span.italic === true),
|
|
195
|
+
isList: selectedLines.length > 0 && selectedLines.every((line) => line.kind === "item"),
|
|
196
|
+
href: isInOneLink ? firstHref : void 0
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
function linkSelectionAt(text, point) {
|
|
200
|
+
const line = flatLinesOf(text)[point.line];
|
|
201
|
+
if (line === void 0) return void 0;
|
|
202
|
+
const bounds = spanBoundsOf(line.spans);
|
|
203
|
+
const index = bounds.findIndex(
|
|
204
|
+
(bound) => point.offset >= bound.from && point.offset <= bound.to
|
|
205
|
+
);
|
|
206
|
+
const href = line.spans[index]?.href;
|
|
207
|
+
if (href === void 0) return void 0;
|
|
208
|
+
const isSameLink = (at) => line.spans[at]?.href === href;
|
|
209
|
+
let first = index;
|
|
210
|
+
while (isSameLink(first - 1)) first -= 1;
|
|
211
|
+
let last = index;
|
|
212
|
+
while (isSameLink(last + 1)) last += 1;
|
|
213
|
+
return {
|
|
214
|
+
start: { line: point.line, offset: bounds[first]?.from ?? 0 },
|
|
215
|
+
end: { line: point.line, offset: bounds[last]?.to ?? 0 }
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
function toggleMark(text, selection, mark) {
|
|
219
|
+
const lines = flatLinesOf(text);
|
|
220
|
+
const selected = selectedSpans(lines, selection);
|
|
221
|
+
const hasMarkEverywhere = selected.length > 0 && selected.every((span) => span[mark] === true);
|
|
222
|
+
return withSelectedSpans(
|
|
223
|
+
lines,
|
|
224
|
+
selection,
|
|
225
|
+
(span) => hasMarkEverywhere ? withoutMark(span, mark) : { ...span, [mark]: true }
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
function setLink(text, selection, href) {
|
|
229
|
+
return withSelectedSpans(
|
|
230
|
+
flatLinesOf(text),
|
|
231
|
+
selection,
|
|
232
|
+
(span) => spanOf(span.text, { ...span, href })
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
function toggleList(text, selection) {
|
|
236
|
+
const lines = flatLinesOf(text);
|
|
237
|
+
const isSelected = (index) => index >= selection.start.line && index <= selection.end.line;
|
|
238
|
+
const isListEverywhere = lines.every(
|
|
239
|
+
(line, index) => !isSelected(index) || line.kind === "item"
|
|
240
|
+
);
|
|
241
|
+
const kind = isListEverywhere ? "paragraph" : "item";
|
|
242
|
+
const changed = lines.map(
|
|
243
|
+
(line, index) => isSelected(index) ? { ...line, kind } : line
|
|
244
|
+
);
|
|
245
|
+
return blocksOf(changed);
|
|
246
|
+
}
|
|
247
|
+
function flatLinesOf(text) {
|
|
248
|
+
return text.flatMap(
|
|
249
|
+
(block) => block.kind === "paragraph" ? [{ kind: "paragraph", spans: block.spans }] : block.items.map((spans) => ({ kind: "item", spans }))
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
function blocksOf(lines) {
|
|
253
|
+
return lines.reduce((blocks, line) => {
|
|
254
|
+
const previous = blocks.at(-1);
|
|
255
|
+
if (line.kind === "paragraph") {
|
|
256
|
+
return [...blocks, { kind: "paragraph", spans: line.spans }];
|
|
257
|
+
}
|
|
258
|
+
if (previous?.kind === "list") {
|
|
259
|
+
const list = {
|
|
260
|
+
kind: "list",
|
|
261
|
+
items: [...previous.items, line.spans]
|
|
262
|
+
};
|
|
263
|
+
return [...blocks.slice(0, -1), list];
|
|
264
|
+
}
|
|
265
|
+
return [...blocks, { kind: "list", items: [line.spans] }];
|
|
266
|
+
}, []);
|
|
267
|
+
}
|
|
268
|
+
function selectedSpans(lines, selection) {
|
|
269
|
+
return lines.flatMap((line, index) => {
|
|
270
|
+
const range = rangeInLine(line, index, selection);
|
|
271
|
+
return range === void 0 ? [] : piecesIn(line.spans, range).inside;
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
function spansAtCursor(lines, point) {
|
|
275
|
+
const spans = lines[point.line]?.spans ?? [];
|
|
276
|
+
const bounds = spanBoundsOf(spans);
|
|
277
|
+
const index = bounds.findIndex(
|
|
278
|
+
(bound) => point.offset > bound.from && point.offset <= bound.to
|
|
279
|
+
);
|
|
280
|
+
const span = spans[index === -1 ? 0 : index];
|
|
281
|
+
return span === void 0 ? [] : [span];
|
|
282
|
+
}
|
|
283
|
+
function spanBoundsOf(spans) {
|
|
284
|
+
return spans.reduce((bounds, span) => {
|
|
285
|
+
const from = bounds.at(-1)?.to ?? 0;
|
|
286
|
+
return [...bounds, { from, to: from + span.text.length }];
|
|
287
|
+
}, []);
|
|
288
|
+
}
|
|
289
|
+
function withSelectedSpans(lines, selection, change) {
|
|
290
|
+
const changed = lines.map((line, index) => {
|
|
291
|
+
const range = rangeInLine(line, index, selection);
|
|
292
|
+
if (range === void 0) return line;
|
|
293
|
+
const { before, inside, after } = piecesIn(line.spans, range);
|
|
294
|
+
const spans = normalizedLine([...before, ...inside.map(change), ...after]);
|
|
295
|
+
return { ...line, spans };
|
|
296
|
+
});
|
|
297
|
+
return blocksOf(changed);
|
|
298
|
+
}
|
|
299
|
+
function rangeInLine(line, index, selection) {
|
|
300
|
+
const isOutside = index < selection.start.line || index > selection.end.line;
|
|
301
|
+
if (isOutside) return void 0;
|
|
302
|
+
const length = lengthOf(line.spans);
|
|
303
|
+
const from = index === selection.start.line ? selection.start.offset : 0;
|
|
304
|
+
const to = index === selection.end.line ? selection.end.offset : length;
|
|
305
|
+
const range = { from: clamp(from, length), to: clamp(to, length) };
|
|
306
|
+
return range.from < range.to ? range : void 0;
|
|
307
|
+
}
|
|
308
|
+
function piecesIn(spans, range) {
|
|
309
|
+
const pieces = spans.reduce(
|
|
310
|
+
(state, span) => {
|
|
311
|
+
const end = state.at + span.text.length;
|
|
312
|
+
const edges = [range.from, range.to].filter(
|
|
313
|
+
(edge) => edge > state.at && edge < end
|
|
314
|
+
);
|
|
315
|
+
const parts = cutAt(
|
|
316
|
+
span,
|
|
317
|
+
edges.map((edge) => edge - state.at)
|
|
318
|
+
);
|
|
319
|
+
return { at: end, cut: [...state.cut, ...parts] };
|
|
320
|
+
},
|
|
321
|
+
{ at: 0, cut: [] }
|
|
322
|
+
).cut;
|
|
323
|
+
return pieces.reduce(
|
|
324
|
+
(state, piece) => {
|
|
325
|
+
const end = state.at + piece.text.length;
|
|
326
|
+
const place = placeOf(state.at, end, range);
|
|
327
|
+
return { ...state, at: end, [place]: [...state[place], piece] };
|
|
328
|
+
},
|
|
329
|
+
{ at: 0, before: [], inside: [], after: [] }
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
function placeOf(start, end, range) {
|
|
333
|
+
if (end <= range.from) return "before";
|
|
334
|
+
if (start >= range.to) return "after";
|
|
335
|
+
return "inside";
|
|
336
|
+
}
|
|
337
|
+
function cutAt(span, offsets) {
|
|
338
|
+
const bounds = [0, ...offsets, span.text.length];
|
|
339
|
+
return bounds.slice(1).map((end, index) => ({
|
|
340
|
+
...span,
|
|
341
|
+
text: span.text.slice(bounds[index], end)
|
|
342
|
+
}));
|
|
343
|
+
}
|
|
344
|
+
function withoutMark(span, mark) {
|
|
345
|
+
return spanOf(span.text, { ...span, [mark]: false });
|
|
346
|
+
}
|
|
347
|
+
function spanOf(text, marks) {
|
|
348
|
+
return {
|
|
349
|
+
text,
|
|
350
|
+
...marks.bold === true ? { bold: true } : {},
|
|
351
|
+
...marks.italic === true ? { italic: true } : {},
|
|
352
|
+
...marks.href === void 0 ? {} : { href: marks.href }
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
function lengthOf(spans) {
|
|
356
|
+
return spans.reduce((length, span) => length + span.text.length, 0);
|
|
357
|
+
}
|
|
358
|
+
function clamp(offset, length) {
|
|
359
|
+
return Math.min(Math.max(offset, 0), length);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// src/bridge/rich-text-dom.ts
|
|
363
|
+
var LINE_BREAK = "\n";
|
|
364
|
+
var BOLD_TAGS = /* @__PURE__ */ new Set(["b", "strong"]);
|
|
365
|
+
var ITALIC_TAGS = /* @__PURE__ */ new Set(["i", "em"]);
|
|
366
|
+
var BLOCK_TAGS = /* @__PURE__ */ new Set(["p", "div", "h1", "h2", "h3", "h4", "blockquote"]);
|
|
367
|
+
var LIST_TAGS = /* @__PURE__ */ new Set(["ul", "ol"]);
|
|
368
|
+
var LINE_SELECTOR = "p, li";
|
|
369
|
+
var COLLAPSIBLE_WHITESPACE = /[\t\n\f\r ]+/gu;
|
|
370
|
+
var LEADING_WHITESPACE = /^[\t\n\f\r ]+/u;
|
|
371
|
+
var TRAILING_WHITESPACE = /[\t\n\f\r ]+$/u;
|
|
372
|
+
var SPACE = " ";
|
|
373
|
+
function renderRichText(element, text) {
|
|
374
|
+
const document = element.ownerDocument;
|
|
375
|
+
const blocks = text.map((block) => blockElement(document, block));
|
|
376
|
+
element.replaceChildren(...blocks);
|
|
377
|
+
}
|
|
378
|
+
function renderInlineRichText(element, text) {
|
|
379
|
+
const document = element.ownerDocument;
|
|
380
|
+
const lines = text.flatMap(
|
|
381
|
+
(block) => block.kind === "paragraph" ? [block.spans] : block.items
|
|
382
|
+
);
|
|
383
|
+
const nodes = lines.flatMap((line, index) => [
|
|
384
|
+
...index > 0 ? [document.createElement("br")] : [],
|
|
385
|
+
...lineNodes(document, line)
|
|
386
|
+
]);
|
|
387
|
+
element.replaceChildren(...nodes);
|
|
388
|
+
}
|
|
389
|
+
function readRichTextFrom(element) {
|
|
390
|
+
return blocksOf(readLines(element, []).lines);
|
|
391
|
+
}
|
|
392
|
+
function readSelectionFrom(element) {
|
|
393
|
+
const selection = element.ownerDocument.getSelection();
|
|
394
|
+
if (selection === null) return void 0;
|
|
395
|
+
const anchor = selection.anchorNode;
|
|
396
|
+
const focus = selection.focusNode;
|
|
397
|
+
if (anchor === null || focus === null) return void 0;
|
|
398
|
+
const points = [
|
|
399
|
+
{ node: anchor, offset: selection.anchorOffset },
|
|
400
|
+
{ node: focus, offset: selection.focusOffset }
|
|
401
|
+
];
|
|
402
|
+
const [first, second] = readLines(element, points).points;
|
|
403
|
+
if (first === void 0 || second === void 0) return void 0;
|
|
404
|
+
return isBefore(second, first) ? { start: second, end: first } : { start: first, end: second };
|
|
405
|
+
}
|
|
406
|
+
function selectIn(element, selection) {
|
|
407
|
+
const range = rangeOf(element, selection);
|
|
408
|
+
if (range === void 0) return;
|
|
409
|
+
element.ownerDocument.getSelection()?.setBaseAndExtent(
|
|
410
|
+
range.startContainer,
|
|
411
|
+
range.startOffset,
|
|
412
|
+
range.endContainer,
|
|
413
|
+
range.endOffset
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
function rangeOf(element, selection) {
|
|
417
|
+
const start = domPointOf(element, selection.start, "forward");
|
|
418
|
+
const end = domPointOf(element, selection.end, "backward");
|
|
419
|
+
if (start === void 0 || end === void 0) return void 0;
|
|
420
|
+
const range = element.ownerDocument.createRange();
|
|
421
|
+
range.setStart(start.node, start.offset);
|
|
422
|
+
range.setEnd(end.node, end.offset);
|
|
423
|
+
return range;
|
|
424
|
+
}
|
|
425
|
+
function insertPlainText(element, words) {
|
|
426
|
+
const document = element.ownerDocument;
|
|
427
|
+
const selection = document.getSelection();
|
|
428
|
+
const range = selection?.rangeCount === 1 ? selection.getRangeAt(0) : void 0;
|
|
429
|
+
const isInElement = range !== void 0 && element.contains(range.commonAncestorContainer);
|
|
430
|
+
if (selection === null || !isInElement) return;
|
|
431
|
+
const inserted = [...wordsNode(document, words).childNodes];
|
|
432
|
+
const last = inserted.at(-1);
|
|
433
|
+
range.deleteContents();
|
|
434
|
+
inserted.reverse().forEach((node) => {
|
|
435
|
+
range.insertNode(node);
|
|
436
|
+
});
|
|
437
|
+
if (last !== void 0) range.setStartAfter(last);
|
|
438
|
+
range.collapse(true);
|
|
439
|
+
selection.removeAllRanges();
|
|
440
|
+
selection.addRange(range);
|
|
441
|
+
}
|
|
442
|
+
function blockElement(document, block) {
|
|
443
|
+
if (block.kind === "paragraph") {
|
|
444
|
+
const paragraph = document.createElement("p");
|
|
445
|
+
paragraph.append(...lineNodes(document, block.spans));
|
|
446
|
+
return paragraph;
|
|
447
|
+
}
|
|
448
|
+
const list = document.createElement("ul");
|
|
449
|
+
const items = block.items.map((item) => {
|
|
450
|
+
const listItem = document.createElement("li");
|
|
451
|
+
listItem.append(...lineNodes(document, item));
|
|
452
|
+
return listItem;
|
|
453
|
+
});
|
|
454
|
+
list.append(...items);
|
|
455
|
+
return list;
|
|
456
|
+
}
|
|
457
|
+
function lineNodes(document, line) {
|
|
458
|
+
return line.map((span) => spanNode(document, span));
|
|
459
|
+
}
|
|
460
|
+
function spanNode(document, span) {
|
|
461
|
+
const words = wordsNode(document, span.text);
|
|
462
|
+
const italic = span.italic === true ? wrapped(document, "em", words) : words;
|
|
463
|
+
const bold = span.bold === true ? wrapped(document, "strong", italic) : italic;
|
|
464
|
+
if (span.href === void 0) return bold;
|
|
465
|
+
const link = wrapped(document, "a", bold);
|
|
466
|
+
link.setAttribute("href", span.href);
|
|
467
|
+
return link;
|
|
468
|
+
}
|
|
469
|
+
function wordsNode(document, text) {
|
|
470
|
+
const fragment = document.createDocumentFragment();
|
|
471
|
+
text.split(LINE_BREAK).forEach((line, index) => {
|
|
472
|
+
if (index > 0) fragment.append(document.createElement("br"));
|
|
473
|
+
fragment.append(document.createTextNode(line));
|
|
474
|
+
});
|
|
475
|
+
return fragment;
|
|
476
|
+
}
|
|
477
|
+
function wrapped(document, tag, content) {
|
|
478
|
+
const element = document.createElement(tag);
|
|
479
|
+
element.append(content);
|
|
480
|
+
return element;
|
|
481
|
+
}
|
|
482
|
+
function readLines(element, points) {
|
|
483
|
+
const reader = createLineReader(points);
|
|
484
|
+
element.childNodes.forEach((child, index) => {
|
|
485
|
+
reader.markChildPoint(element, index);
|
|
486
|
+
readTopLevel(reader, child);
|
|
487
|
+
});
|
|
488
|
+
reader.markChildPoint(element, element.childNodes.length);
|
|
489
|
+
return reader.finish();
|
|
490
|
+
}
|
|
491
|
+
function readTopLevel(reader, node) {
|
|
492
|
+
const tag = tagOf(node);
|
|
493
|
+
if (tag !== void 0 && LIST_TAGS.has(tag)) {
|
|
494
|
+
[...node.childNodes].forEach((item, index) => {
|
|
495
|
+
reader.markChildPoint(node, index);
|
|
496
|
+
if (tagOf(item) !== "li") return;
|
|
497
|
+
reader.startLine("item");
|
|
498
|
+
readInline(reader, item, {});
|
|
499
|
+
reader.endLine();
|
|
500
|
+
});
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
if (tag !== void 0 && BLOCK_TAGS.has(tag)) {
|
|
504
|
+
reader.startLine("paragraph");
|
|
505
|
+
readInline(reader, node, {});
|
|
506
|
+
reader.endLine();
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
reader.continueLooseLine();
|
|
510
|
+
readInline(reader, node, {});
|
|
511
|
+
}
|
|
512
|
+
function readInline(reader, node, marks) {
|
|
513
|
+
if (node.nodeType === node.TEXT_NODE) {
|
|
514
|
+
const text = (node.textContent ?? "").replace(
|
|
515
|
+
COLLAPSIBLE_WHITESPACE,
|
|
516
|
+
SPACE
|
|
517
|
+
);
|
|
518
|
+
reader.addText(node, text, marks);
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
const tag = tagOf(node);
|
|
522
|
+
if (tag === void 0) return;
|
|
523
|
+
if (tag === "br") {
|
|
524
|
+
reader.addText(void 0, LINE_BREAK, marks);
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
const inner = marksOf(node, tag, marks);
|
|
528
|
+
node.childNodes.forEach((child, index) => {
|
|
529
|
+
reader.markChildPoint(node, index);
|
|
530
|
+
readInline(reader, child, inner);
|
|
531
|
+
});
|
|
532
|
+
reader.markChildPoint(node, node.childNodes.length);
|
|
533
|
+
}
|
|
534
|
+
function createLineReader(points) {
|
|
535
|
+
const lines = [];
|
|
536
|
+
const found = points.map(
|
|
537
|
+
() => void 0
|
|
538
|
+
);
|
|
539
|
+
let isLooseOpen = false;
|
|
540
|
+
const current = () => {
|
|
541
|
+
const line = lines.at(-1);
|
|
542
|
+
if (line !== void 0) return line;
|
|
543
|
+
const first = { kind: "paragraph", spans: [], length: 0 };
|
|
544
|
+
lines.push(first);
|
|
545
|
+
return first;
|
|
546
|
+
};
|
|
547
|
+
const mark = (isAt, offsetIn) => {
|
|
548
|
+
points.forEach((point, index) => {
|
|
549
|
+
if (found[index] !== void 0 || !isAt(point)) return;
|
|
550
|
+
found[index] = {
|
|
551
|
+
line: lines.length - 1,
|
|
552
|
+
offset: current().length + offsetIn(point)
|
|
553
|
+
};
|
|
554
|
+
});
|
|
555
|
+
};
|
|
556
|
+
return {
|
|
557
|
+
startLine: (kind) => {
|
|
558
|
+
lines.push({ kind, spans: [], length: 0 });
|
|
559
|
+
isLooseOpen = false;
|
|
560
|
+
},
|
|
561
|
+
endLine: () => {
|
|
562
|
+
isLooseOpen = false;
|
|
563
|
+
},
|
|
564
|
+
continueLooseLine: () => {
|
|
565
|
+
if (isLooseOpen) return;
|
|
566
|
+
lines.push({ kind: "paragraph", spans: [], length: 0 });
|
|
567
|
+
isLooseOpen = true;
|
|
568
|
+
},
|
|
569
|
+
addText: (node, text, marks) => {
|
|
570
|
+
current();
|
|
571
|
+
if (node !== void 0)
|
|
572
|
+
mark(
|
|
573
|
+
(point) => point.node === node,
|
|
574
|
+
(point) => point.offset
|
|
575
|
+
);
|
|
576
|
+
const line = current();
|
|
577
|
+
line.spans.push({ text, ...marks });
|
|
578
|
+
line.length += text.length;
|
|
579
|
+
},
|
|
580
|
+
markChildPoint: (parent, index) => {
|
|
581
|
+
current();
|
|
582
|
+
mark(
|
|
583
|
+
(point) => point.node === parent && point.offset === index,
|
|
584
|
+
() => 0
|
|
585
|
+
);
|
|
586
|
+
},
|
|
587
|
+
finish: () => finishedLines(lines, found)
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
function finishedLines(lines, found) {
|
|
591
|
+
const trimmed = lines.map((line) => ({
|
|
592
|
+
kind: line.kind,
|
|
593
|
+
spans: trimmedLine(line.spans)
|
|
594
|
+
}));
|
|
595
|
+
const keptIndexes = trimmed.flatMap(
|
|
596
|
+
(line, index) => line.spans.length > 0 ? [index] : []
|
|
597
|
+
);
|
|
598
|
+
const kept = keptIndexes.flatMap((index) => trimmed[index] ?? []);
|
|
599
|
+
const points = found.map(
|
|
600
|
+
(point) => point === void 0 ? void 0 : keptPoint(point, keptIndexes, kept)
|
|
601
|
+
);
|
|
602
|
+
return { lines: kept, points };
|
|
603
|
+
}
|
|
604
|
+
function keptPoint(point, keptIndexes, kept) {
|
|
605
|
+
const sameLine = keptIndexes.indexOf(point.line);
|
|
606
|
+
if (sameLine !== -1) {
|
|
607
|
+
const length = lengthOf2(kept[sameLine]?.spans ?? []);
|
|
608
|
+
return { line: sameLine, offset: Math.min(point.offset, length) };
|
|
609
|
+
}
|
|
610
|
+
const nextLine = keptIndexes.findIndex((index) => index > point.line);
|
|
611
|
+
if (nextLine !== -1) return { line: nextLine, offset: 0 };
|
|
612
|
+
const lastLine = kept.length - 1;
|
|
613
|
+
if (lastLine < 0) return void 0;
|
|
614
|
+
return { line: lastLine, offset: lengthOf2(kept[lastLine]?.spans ?? []) };
|
|
615
|
+
}
|
|
616
|
+
function trimmedLine(spans) {
|
|
617
|
+
const line = normalizedLine(spans);
|
|
618
|
+
const last = line.at(-1);
|
|
619
|
+
const withoutClosingBreak = last !== void 0 && last.text.endsWith(LINE_BREAK) ? [...line.slice(0, -1), { ...last, text: last.text.slice(0, -1) }] : line;
|
|
620
|
+
const isBlank = withoutClosingBreak.every((span) => span.text.trim() === "");
|
|
621
|
+
return isBlank ? [] : withoutEdgeWhitespace(withoutClosingBreak);
|
|
622
|
+
}
|
|
623
|
+
function withoutEdgeWhitespace(spans) {
|
|
624
|
+
const lastIndex = spans.length - 1;
|
|
625
|
+
const trimmed = spans.map((span, index) => {
|
|
626
|
+
const withoutLeading = index === 0 ? span.text.replace(LEADING_WHITESPACE, "") : span.text;
|
|
627
|
+
const text = index === lastIndex ? withoutLeading.replace(TRAILING_WHITESPACE, "") : withoutLeading;
|
|
628
|
+
return { ...span, text };
|
|
629
|
+
});
|
|
630
|
+
return normalizedLine(trimmed);
|
|
631
|
+
}
|
|
632
|
+
function domPointOf(element, point, lean) {
|
|
633
|
+
const line = lineElementsOf(element)[point.line];
|
|
634
|
+
if (line === void 0) return void 0;
|
|
635
|
+
const walker = element.ownerDocument.createTreeWalker(
|
|
636
|
+
line,
|
|
637
|
+
NodeFilter.SHOW_ALL
|
|
638
|
+
);
|
|
639
|
+
let left = point.offset;
|
|
640
|
+
for (let node = walker.nextNode(); node !== null; node = walker.nextNode()) {
|
|
641
|
+
if (node.nodeType === node.TEXT_NODE) {
|
|
642
|
+
const length = node.textContent?.length ?? 0;
|
|
643
|
+
const isHere = lean === "forward" ? left < length : left <= length;
|
|
644
|
+
if (isHere) return { node, offset: left };
|
|
645
|
+
left -= length;
|
|
646
|
+
} else if (tagOf(node) === "br") {
|
|
647
|
+
if (left === 0) return beforeNode(node);
|
|
648
|
+
left -= 1;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
return { node: line, offset: line.childNodes.length };
|
|
652
|
+
}
|
|
653
|
+
function lineElementsOf(element) {
|
|
654
|
+
const lines = [...element.querySelectorAll(LINE_SELECTOR)];
|
|
655
|
+
return lines.length > 0 ? lines : [element];
|
|
656
|
+
}
|
|
657
|
+
function beforeNode(node) {
|
|
658
|
+
const parent = node.parentNode;
|
|
659
|
+
if (parent === null) return void 0;
|
|
660
|
+
return {
|
|
661
|
+
node: parent,
|
|
662
|
+
offset: [...parent.childNodes].indexOf(node)
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
function marksOf(element, tag, marks) {
|
|
666
|
+
const href = tag === "a" ? element.getAttribute("href") : null;
|
|
667
|
+
return {
|
|
668
|
+
...marks,
|
|
669
|
+
...BOLD_TAGS.has(tag) ? { bold: true } : {},
|
|
670
|
+
...ITALIC_TAGS.has(tag) ? { italic: true } : {},
|
|
671
|
+
...href === null ? {} : { href }
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
function tagOf(node) {
|
|
675
|
+
return node.nodeType === node.ELEMENT_NODE ? node.localName : void 0;
|
|
676
|
+
}
|
|
677
|
+
function isBefore(first, second) {
|
|
678
|
+
return first.line < second.line || first.line === second.line && first.offset < second.offset;
|
|
679
|
+
}
|
|
680
|
+
function lengthOf2(spans) {
|
|
681
|
+
return spans.reduce((length, span) => length + span.text.length, 0);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
export {
|
|
685
|
+
shortcutOf,
|
|
686
|
+
imageFieldAt,
|
|
687
|
+
PROTOCOL_VERSION,
|
|
688
|
+
readPageMessage,
|
|
689
|
+
readEditorMessage,
|
|
690
|
+
carriesFiles,
|
|
691
|
+
closestMarkedPath,
|
|
692
|
+
isCollapsed,
|
|
693
|
+
formatsAt,
|
|
694
|
+
linkSelectionAt,
|
|
695
|
+
toggleMark,
|
|
696
|
+
setLink,
|
|
697
|
+
toggleList,
|
|
698
|
+
renderRichText,
|
|
699
|
+
renderInlineRichText,
|
|
700
|
+
readRichTextFrom,
|
|
701
|
+
readSelectionFrom,
|
|
702
|
+
selectIn,
|
|
703
|
+
rangeOf,
|
|
704
|
+
insertPlainText
|
|
705
|
+
};
|