open-wysiwyg-editor 0.1.0
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/LICENSE +21 -0
- package/README.md +401 -0
- package/dist/chunk-CDPXFV6R.js +3361 -0
- package/dist/chunk-CDPXFV6R.js.map +1 -0
- package/dist/content.css +258 -0
- package/dist/content.min.css +2 -0
- package/dist/headless.cjs +3439 -0
- package/dist/headless.cjs.map +1 -0
- package/dist/headless.d.cts +810 -0
- package/dist/headless.d.ts +810 -0
- package/dist/headless.js +3 -0
- package/dist/headless.js.map +1 -0
- package/dist/index.cjs +5155 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +162 -0
- package/dist/index.d.ts +162 -0
- package/dist/index.js +1701 -0
- package/dist/index.js.map +1 -0
- package/dist/open-wysiwyg-editor.global.js +23 -0
- package/dist/style.css +1020 -0
- package/dist/style.min.css +2 -0
- package/package.json +101 -0
|
@@ -0,0 +1,3361 @@
|
|
|
1
|
+
import DOMPurify from 'dompurify';
|
|
2
|
+
import { DOMParser, Slice, Schema } from 'prosemirror-model';
|
|
3
|
+
import { liftTarget } from 'prosemirror-transform';
|
|
4
|
+
import { PluginKey, EditorState, Plugin, NodeSelection, AllSelection, TextSelection, Selection } from 'prosemirror-state';
|
|
5
|
+
import { EditorView, DecorationSet, Decoration } from 'prosemirror-view';
|
|
6
|
+
import { keymap } from 'prosemirror-keymap';
|
|
7
|
+
import { baseKeymap, setBlockType, lift, wrapIn, toggleMark, chainCommands, exitCode } from 'prosemirror-commands';
|
|
8
|
+
import { inputRules, InputRule, wrappingInputRule, textblockTypeInputRule } from 'prosemirror-inputrules';
|
|
9
|
+
import { liftListItem, wrapInList, sinkListItem, splitListItem } from 'prosemirror-schema-list';
|
|
10
|
+
import { redo, undo, history } from 'prosemirror-history';
|
|
11
|
+
import { dropCursor } from 'prosemirror-dropcursor';
|
|
12
|
+
import { gapCursor } from 'prosemirror-gapcursor';
|
|
13
|
+
import { columnResizing, tableEditing, goToNextCell, splitCell, mergeCells, deleteTable, deleteColumn, deleteRow, addColumnAfter, addColumnBefore, addRowAfter, addRowBefore, toggleHeader, tableNodes } from 'prosemirror-tables';
|
|
14
|
+
|
|
15
|
+
/*! open-wysiwyg-editor | MIT License | https://github.com/alhassan73/open-wysiwyg-editor */
|
|
16
|
+
|
|
17
|
+
// src/core/extension.ts
|
|
18
|
+
function defineExtension(config2, options = config2.defaultOptions ?? {}) {
|
|
19
|
+
return {
|
|
20
|
+
name: config2.name,
|
|
21
|
+
options,
|
|
22
|
+
config: config2,
|
|
23
|
+
configure(partial) {
|
|
24
|
+
return defineExtension(config2, mergeOptions(options, partial));
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function mergeOptions(base, partial) {
|
|
29
|
+
if (!partial) return base;
|
|
30
|
+
const out = { ...base };
|
|
31
|
+
for (const [key, value] of Object.entries(partial)) {
|
|
32
|
+
if (key === "__proto__" || key === "constructor" || key === "prototype") continue;
|
|
33
|
+
out[key] = value;
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
function resolveExtensions(list) {
|
|
38
|
+
const flat = [];
|
|
39
|
+
const visit = (ext) => {
|
|
40
|
+
const children = ext.config.extensions?.(ext.options) ?? [];
|
|
41
|
+
for (const child of children) visit(child);
|
|
42
|
+
flat.push(ext);
|
|
43
|
+
};
|
|
44
|
+
for (const ext of list) visit(ext);
|
|
45
|
+
const byName = /* @__PURE__ */ new Map();
|
|
46
|
+
for (const ext of flat) {
|
|
47
|
+
byName.delete(ext.name);
|
|
48
|
+
byName.set(ext.name, ext);
|
|
49
|
+
}
|
|
50
|
+
return [...byName.values()].map((ext, index) => ({ ext, index })).sort((a, b) => (b.ext.config.priority ?? 100) - (a.ext.config.priority ?? 100) || a.index - b.index).map(({ ext }) => ext);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// src/core/url.ts
|
|
54
|
+
var DEFAULT_PROTOCOLS = ["http", "https", "mailto", "tel"];
|
|
55
|
+
var STRIP = new RegExp("[\0---\u2028\u2029\uFEFF]", "g");
|
|
56
|
+
var SCHEME = /^([a-z][a-z0-9+.-]*):/i;
|
|
57
|
+
var SAFE_DATA_IMAGE = /^data:image\/(?:png|jpe?g|gif|webp|avif);base64,[a-z0-9+/]+=*$/i;
|
|
58
|
+
function sanitizeUrl(input, policy = {}, kind = "link") {
|
|
59
|
+
if (typeof input !== "string") return null;
|
|
60
|
+
const url = input.replace(STRIP, "").trim();
|
|
61
|
+
if (!url) return null;
|
|
62
|
+
const scheme = SCHEME.exec(url.replace(/\s+/g, ""));
|
|
63
|
+
if (!scheme) {
|
|
64
|
+
if (policy.allowRelative === false) return null;
|
|
65
|
+
return url;
|
|
66
|
+
}
|
|
67
|
+
const name = scheme[1].toLowerCase();
|
|
68
|
+
if (name === "data") {
|
|
69
|
+
return kind === "image" && policy.allowDataImages && SAFE_DATA_IMAGE.test(url) ? url : null;
|
|
70
|
+
}
|
|
71
|
+
const allowed = kind === "image" ? ["http", "https"] : policy.protocols ?? DEFAULT_PROTOCOLS;
|
|
72
|
+
return allowed.includes(name) ? url : null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/core/html.ts
|
|
76
|
+
var VOID = /* @__PURE__ */ new Set([
|
|
77
|
+
"area",
|
|
78
|
+
"base",
|
|
79
|
+
"br",
|
|
80
|
+
"col",
|
|
81
|
+
"embed",
|
|
82
|
+
"hr",
|
|
83
|
+
"img",
|
|
84
|
+
"input",
|
|
85
|
+
"link",
|
|
86
|
+
"meta",
|
|
87
|
+
"source",
|
|
88
|
+
"track",
|
|
89
|
+
"wbr"
|
|
90
|
+
]);
|
|
91
|
+
var NAME = /^[a-zA-Z][a-zA-Z0-9-]*$/;
|
|
92
|
+
var ATTR_NAME = /^[a-zA-Z_:][a-zA-Z0-9_.:-]*$/;
|
|
93
|
+
var FORBIDDEN_OUTPUT = /* @__PURE__ */ new Set([
|
|
94
|
+
"script",
|
|
95
|
+
"style",
|
|
96
|
+
"base",
|
|
97
|
+
"meta",
|
|
98
|
+
"link",
|
|
99
|
+
"object",
|
|
100
|
+
"embed",
|
|
101
|
+
"applet",
|
|
102
|
+
"frame",
|
|
103
|
+
"frameset",
|
|
104
|
+
"template",
|
|
105
|
+
"noscript",
|
|
106
|
+
"form"
|
|
107
|
+
]);
|
|
108
|
+
var URL_ATTRS = /* @__PURE__ */ new Set(["href", "src", "cite", "poster", "action", "background"]);
|
|
109
|
+
var DROP_ATTRS = /* @__PURE__ */ new Set(["srcdoc", "srcset", "formaction", "xlink:href", "ping"]);
|
|
110
|
+
var NBSP = new RegExp(String.fromCharCode(160), "g");
|
|
111
|
+
var escapeText = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(NBSP, " ");
|
|
112
|
+
var escapeAttr = (s) => s.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
113
|
+
function renderAttrs(raw, policy) {
|
|
114
|
+
let attrs = "";
|
|
115
|
+
for (const [name, value] of Object.entries(raw)) {
|
|
116
|
+
if (value === null || value === void 0 || value === false) continue;
|
|
117
|
+
const lower = name.toLowerCase();
|
|
118
|
+
if (!ATTR_NAME.test(name) || lower.startsWith("on") || DROP_ATTRS.has(lower)) continue;
|
|
119
|
+
if (URL_ATTRS.has(lower)) {
|
|
120
|
+
const safe = sanitizeUrl(String(value), { ...policy, allowDataImages: true }, lower === "src" ? "image" : "link");
|
|
121
|
+
if (safe !== null) attrs += ` ${name}="${escapeAttr(safe)}"`;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
attrs += value === true || value === "" ? ` ${name}=""` : ` ${name}="${escapeAttr(String(value))}"`;
|
|
125
|
+
}
|
|
126
|
+
return attrs;
|
|
127
|
+
}
|
|
128
|
+
function renderSpec(spec, hole, policy) {
|
|
129
|
+
if (typeof spec === "string") return escapeText(spec);
|
|
130
|
+
if (!Array.isArray(spec)) {
|
|
131
|
+
throw new Error("[open-wysiwyg-editor] toDOM/toHTML must return an array spec to be serializable.");
|
|
132
|
+
}
|
|
133
|
+
const [rawTag, ...rest] = spec;
|
|
134
|
+
const tag = rawTag.includes(" ") ? rawTag.split(" ")[1] : rawTag;
|
|
135
|
+
if (!NAME.test(tag)) throw new Error(`[open-wysiwyg-editor] Invalid tag name "${tag}".`);
|
|
136
|
+
if (FORBIDDEN_OUTPUT.has(tag.toLowerCase())) return "";
|
|
137
|
+
let attrs = "";
|
|
138
|
+
let start = 0;
|
|
139
|
+
const first = rest[0];
|
|
140
|
+
if (first && typeof first === "object" && !Array.isArray(first)) {
|
|
141
|
+
start = 1;
|
|
142
|
+
attrs = renderAttrs(first, policy);
|
|
143
|
+
}
|
|
144
|
+
if (VOID.has(tag.toLowerCase())) return `<${tag}${attrs}>`;
|
|
145
|
+
let inner = "";
|
|
146
|
+
for (let i = start; i < rest.length; i++) {
|
|
147
|
+
const child = rest[i];
|
|
148
|
+
inner += child === 0 ? hole ? hole() : "" : renderSpec(child, hole, policy);
|
|
149
|
+
}
|
|
150
|
+
return `<${tag}${attrs}>${inner}</${tag}>`;
|
|
151
|
+
}
|
|
152
|
+
function renderNodeSpec(node) {
|
|
153
|
+
const spec = node.type.spec;
|
|
154
|
+
const fn = spec.toHTML ?? spec.toDOM;
|
|
155
|
+
if (!fn) throw new Error(`[open-wysiwyg-editor] Node "${node.type.name}" has no toDOM/toHTML.`);
|
|
156
|
+
return fn(node);
|
|
157
|
+
}
|
|
158
|
+
function markParts(mark, inline, policy) {
|
|
159
|
+
const spec = mark.type.spec;
|
|
160
|
+
const fn = spec.toHTML ?? spec.toDOM;
|
|
161
|
+
if (!fn) return ["", ""];
|
|
162
|
+
const token = `\0${Math.random().toString(36).slice(2)}\0`;
|
|
163
|
+
const html = renderSpec(fn(mark, inline), () => token, policy);
|
|
164
|
+
const at = html.indexOf(token);
|
|
165
|
+
return at === -1 ? [html, ""] : [html.slice(0, at), html.slice(at + token.length)];
|
|
166
|
+
}
|
|
167
|
+
function serializeNode(node, policy = {}) {
|
|
168
|
+
if (node.isText) return escapeText(node.text ?? "");
|
|
169
|
+
return renderSpec(renderNodeSpec(node), () => serializeFragment(node.content, policy), policy);
|
|
170
|
+
}
|
|
171
|
+
function serializeFragment(fragment, policy = {}) {
|
|
172
|
+
let out = "";
|
|
173
|
+
const active = [];
|
|
174
|
+
fragment.forEach((node) => {
|
|
175
|
+
if (active.length || node.marks.length) {
|
|
176
|
+
let keep = 0;
|
|
177
|
+
let rendered = 0;
|
|
178
|
+
while (keep < active.length && rendered < node.marks.length) {
|
|
179
|
+
const next = node.marks[rendered];
|
|
180
|
+
if (!next.eq(active[keep].mark) || next.type.spec.spanning === false) break;
|
|
181
|
+
keep++;
|
|
182
|
+
rendered++;
|
|
183
|
+
}
|
|
184
|
+
while (keep < active.length) out += active.pop().close;
|
|
185
|
+
while (rendered < node.marks.length) {
|
|
186
|
+
const mark = node.marks[rendered++];
|
|
187
|
+
const [open, close] = markParts(mark, node.isInline, policy);
|
|
188
|
+
out += open;
|
|
189
|
+
active.push({ mark, close });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
out += serializeNode(node, policy);
|
|
193
|
+
});
|
|
194
|
+
while (active.length) out += active.pop().close;
|
|
195
|
+
return out;
|
|
196
|
+
}
|
|
197
|
+
function docToHTML(doc, policy = {}) {
|
|
198
|
+
return serializeFragment(doc.content, policy);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// src/core/i18n.ts
|
|
202
|
+
var defaultLabels = {
|
|
203
|
+
editorLabel: "Rich text editor",
|
|
204
|
+
toolbar: "Formatting",
|
|
205
|
+
more: "More",
|
|
206
|
+
// Marks
|
|
207
|
+
bold: "Bold",
|
|
208
|
+
italic: "Italic",
|
|
209
|
+
underline: "Underline",
|
|
210
|
+
strike: "Strikethrough",
|
|
211
|
+
code: "Inline code",
|
|
212
|
+
subscript: "Subscript",
|
|
213
|
+
superscript: "Superscript",
|
|
214
|
+
link: "Link",
|
|
215
|
+
unlink: "Remove link",
|
|
216
|
+
removeFormat: "Clear formatting",
|
|
217
|
+
// Blocks
|
|
218
|
+
blockType: "Text style",
|
|
219
|
+
paragraph: "Paragraph",
|
|
220
|
+
heading: "Heading {level}",
|
|
221
|
+
blockquote: "Quote",
|
|
222
|
+
codeBlock: "Code block",
|
|
223
|
+
horizontalRule: "Horizontal line",
|
|
224
|
+
hardBreak: "Line break",
|
|
225
|
+
bulletList: "Bulleted list",
|
|
226
|
+
orderedList: "Numbered list",
|
|
227
|
+
taskList: "Checklist",
|
|
228
|
+
task: "Task",
|
|
229
|
+
toggleTask: "Mark task as done",
|
|
230
|
+
indent: "Increase indent",
|
|
231
|
+
outdent: "Decrease indent",
|
|
232
|
+
// Alignment & direction
|
|
233
|
+
align: "Alignment",
|
|
234
|
+
alignStart: "Align start",
|
|
235
|
+
alignCenter: "Align center",
|
|
236
|
+
alignEnd: "Align end",
|
|
237
|
+
alignJustify: "Justify",
|
|
238
|
+
direction: "Text direction",
|
|
239
|
+
dirLtr: "Left to right",
|
|
240
|
+
dirRtl: "Right to left",
|
|
241
|
+
dirAuto: "Automatic direction",
|
|
242
|
+
// History
|
|
243
|
+
undo: "Undo",
|
|
244
|
+
redo: "Redo",
|
|
245
|
+
// Image
|
|
246
|
+
image: "Image",
|
|
247
|
+
insertImage: "Insert image",
|
|
248
|
+
imageUrl: "Image address (URL)",
|
|
249
|
+
imageUpload: "Upload from device",
|
|
250
|
+
altText: "Alternative text",
|
|
251
|
+
altHelp: `Describe the image for people who can't see it. Leave out "image of".`,
|
|
252
|
+
decorative: "Decorative image (no description needed)",
|
|
253
|
+
caption: "Caption",
|
|
254
|
+
imageSize: "Size",
|
|
255
|
+
sizeSmall: "Small",
|
|
256
|
+
sizeMedium: "Medium",
|
|
257
|
+
sizeLarge: "Large",
|
|
258
|
+
sizeOriginal: "Original",
|
|
259
|
+
// Table
|
|
260
|
+
table: "Table",
|
|
261
|
+
insertTable: "Insert table",
|
|
262
|
+
rows: "Rows",
|
|
263
|
+
columns: "Columns",
|
|
264
|
+
headerRow: "Header row",
|
|
265
|
+
headerColumn: "Header column",
|
|
266
|
+
tableCaption: "Table caption",
|
|
267
|
+
addRowBefore: "Insert row above",
|
|
268
|
+
addRowAfter: "Insert row below",
|
|
269
|
+
addColumnBefore: "Insert column before",
|
|
270
|
+
addColumnAfter: "Insert column after",
|
|
271
|
+
deleteRow: "Delete row",
|
|
272
|
+
deleteColumn: "Delete column",
|
|
273
|
+
deleteTable: "Delete table",
|
|
274
|
+
mergeCells: "Merge cells",
|
|
275
|
+
splitCell: "Split cell",
|
|
276
|
+
tableSize: "{rows} by {cols} table",
|
|
277
|
+
// Link dialog
|
|
278
|
+
linkUrl: "Link address (URL)",
|
|
279
|
+
linkText: "Text to display",
|
|
280
|
+
linkNewTab: "Open in a new tab",
|
|
281
|
+
invalidUrl: "This address is not allowed. Use a web address (https://…), email (mailto:) or phone (tel:).",
|
|
282
|
+
// Dialog
|
|
283
|
+
insert: "Insert",
|
|
284
|
+
save: "Save",
|
|
285
|
+
cancel: "Cancel",
|
|
286
|
+
close: "Close",
|
|
287
|
+
apply: "Apply",
|
|
288
|
+
remove: "Remove",
|
|
289
|
+
// Find & replace
|
|
290
|
+
find: "Find",
|
|
291
|
+
findAndReplace: "Find and replace",
|
|
292
|
+
replace: "Replace",
|
|
293
|
+
replaceWith: "Replace with",
|
|
294
|
+
replaceAll: "Replace all",
|
|
295
|
+
findNext: "Next match",
|
|
296
|
+
findPrevious: "Previous match",
|
|
297
|
+
matchCase: "Match case",
|
|
298
|
+
wholeWord: "Whole words",
|
|
299
|
+
matchCount: { zero: "No matches", one: "{current} of {n} match", other: "{current} of {n} matches" },
|
|
300
|
+
replacedCount: { one: "Replaced {n} match", other: "Replaced {n} matches" },
|
|
301
|
+
// Status
|
|
302
|
+
wordCount: { one: "{n} word", other: "{n} words" },
|
|
303
|
+
charCount: { one: "{n} character", other: "{n} characters" },
|
|
304
|
+
charLimit: "{n} of {limit} characters",
|
|
305
|
+
charLimitReached: "Character limit reached",
|
|
306
|
+
elementPath: "Element path",
|
|
307
|
+
// Announcements
|
|
308
|
+
on: "{name} on",
|
|
309
|
+
off: "{name} off",
|
|
310
|
+
inserted: "{name} inserted",
|
|
311
|
+
pasteCleaned: "Pasted content was cleaned up",
|
|
312
|
+
pasteDropped: "Some pasted content isn't supported and was removed",
|
|
313
|
+
// Help
|
|
314
|
+
shortcuts: "Keyboard shortcuts",
|
|
315
|
+
shortcutsHelp: "Press {shortcut} to move to the toolbar, Escape to return to the text.",
|
|
316
|
+
// Accessibility checker
|
|
317
|
+
a11yChecker: "Accessibility checker",
|
|
318
|
+
a11yIssues: { zero: "No accessibility issues", one: "{n} accessibility issue", other: "{n} accessibility issues" },
|
|
319
|
+
// Placeholder
|
|
320
|
+
placeholder: "Start writing…",
|
|
321
|
+
// Element path & structure
|
|
322
|
+
listItem: "List item",
|
|
323
|
+
tableRow: "Row",
|
|
324
|
+
tableCell: "Cell",
|
|
325
|
+
tableHeader: "Header cell",
|
|
326
|
+
selectElement: "Select {name}",
|
|
327
|
+
// Dialogs & UI
|
|
328
|
+
editLink: "Edit link",
|
|
329
|
+
editImage: "Edit image",
|
|
330
|
+
required: "required",
|
|
331
|
+
altRequired: "Describe the image, or mark it as decorative.",
|
|
332
|
+
urlRequired: "Enter an address.",
|
|
333
|
+
tableOptions: "Table options",
|
|
334
|
+
tableProperties: "Table properties",
|
|
335
|
+
closeFind: "Close find and replace",
|
|
336
|
+
action: "Action",
|
|
337
|
+
shortcut: "Shortcut",
|
|
338
|
+
leaveEditor: "Press Escape, then Tab, to leave the editor from inside a list or table.",
|
|
339
|
+
markdownShortcuts: "Typing shortcuts",
|
|
340
|
+
navigation: "Navigation",
|
|
341
|
+
formatting: "Formatting",
|
|
342
|
+
blocks: "Blocks",
|
|
343
|
+
focusToolbar: "Move focus to the toolbar",
|
|
344
|
+
showShortcuts: "Show keyboard shortcuts",
|
|
345
|
+
openFind: "Find and replace",
|
|
346
|
+
insertLink: "Insert link",
|
|
347
|
+
sourceCode: "Source code",
|
|
348
|
+
sourceCleaned: "Some HTML isn't supported and was removed or simplified",
|
|
349
|
+
// Colors
|
|
350
|
+
textColor: "Text color",
|
|
351
|
+
highlight: "Highlight color",
|
|
352
|
+
automatic: "Automatic",
|
|
353
|
+
noHighlight: "No highlight",
|
|
354
|
+
customColor: "Custom color",
|
|
355
|
+
color: "Color",
|
|
356
|
+
hexValue: "Hex value",
|
|
357
|
+
invalidColor: "Enter a color such as #1a73e8.",
|
|
358
|
+
contrastText: "Contrast on a white background: {ratio}:1",
|
|
359
|
+
contrastBg: "Contrast with dark text: {ratio}:1",
|
|
360
|
+
contrastPass: "Meets WCAG AA for normal text.",
|
|
361
|
+
contrastFail: "Below the 4.5:1 minimum for normal text (WCAG 1.4.3).",
|
|
362
|
+
colorBlack: "Black",
|
|
363
|
+
colorDarkGray: "Dark gray",
|
|
364
|
+
colorDarkRed: "Dark red",
|
|
365
|
+
colorDarkOrange: "Dark orange",
|
|
366
|
+
colorOlive: "Olive",
|
|
367
|
+
colorDarkGreen: "Dark green",
|
|
368
|
+
colorTeal: "Teal",
|
|
369
|
+
colorBlue: "Blue",
|
|
370
|
+
colorPurple: "Purple",
|
|
371
|
+
colorMagenta: "Magenta",
|
|
372
|
+
colorYellow: "Yellow",
|
|
373
|
+
colorGreen: "Green",
|
|
374
|
+
colorPink: "Pink",
|
|
375
|
+
colorOrange: "Orange",
|
|
376
|
+
colorGray: "Gray"
|
|
377
|
+
};
|
|
378
|
+
var arabicLabels = {
|
|
379
|
+
editorLabel: "محرر النصوص",
|
|
380
|
+
toolbar: "التنسيق",
|
|
381
|
+
more: "المزيد",
|
|
382
|
+
bold: "عريض",
|
|
383
|
+
italic: "مائل",
|
|
384
|
+
underline: "تسطير",
|
|
385
|
+
strike: "يتوسطه خط",
|
|
386
|
+
code: "شيفرة سطرية",
|
|
387
|
+
subscript: "منخفض",
|
|
388
|
+
superscript: "مرتفع",
|
|
389
|
+
link: "رابط",
|
|
390
|
+
unlink: "إزالة الرابط",
|
|
391
|
+
removeFormat: "مسح التنسيق",
|
|
392
|
+
blockType: "نمط النص",
|
|
393
|
+
paragraph: "فقرة",
|
|
394
|
+
heading: "عنوان {level}",
|
|
395
|
+
blockquote: "اقتباس",
|
|
396
|
+
codeBlock: "كتلة شيفرة",
|
|
397
|
+
horizontalRule: "خط أفقي",
|
|
398
|
+
hardBreak: "سطر جديد",
|
|
399
|
+
bulletList: "قائمة نقطية",
|
|
400
|
+
orderedList: "قائمة مرقّمة",
|
|
401
|
+
taskList: "قائمة مهام",
|
|
402
|
+
task: "مهمة",
|
|
403
|
+
toggleTask: "تعليم المهمة كمنجزة",
|
|
404
|
+
indent: "زيادة المسافة البادئة",
|
|
405
|
+
outdent: "إنقاص المسافة البادئة",
|
|
406
|
+
align: "المحاذاة",
|
|
407
|
+
alignStart: "محاذاة إلى البداية",
|
|
408
|
+
alignCenter: "توسيط",
|
|
409
|
+
alignEnd: "محاذاة إلى النهاية",
|
|
410
|
+
alignJustify: "ضبط",
|
|
411
|
+
direction: "اتجاه النص",
|
|
412
|
+
dirLtr: "من اليسار إلى اليمين",
|
|
413
|
+
dirRtl: "من اليمين إلى اليسار",
|
|
414
|
+
dirAuto: "اتجاه تلقائي",
|
|
415
|
+
undo: "تراجع",
|
|
416
|
+
redo: "إعادة",
|
|
417
|
+
image: "صورة",
|
|
418
|
+
insertImage: "إدراج صورة",
|
|
419
|
+
imageUrl: "عنوان الصورة (URL)",
|
|
420
|
+
imageUpload: "رفع من الجهاز",
|
|
421
|
+
altText: "النص البديل",
|
|
422
|
+
altHelp: 'صِف الصورة لمن لا يستطيع رؤيتها، دون كتابة "صورة لـ".',
|
|
423
|
+
decorative: "صورة زخرفية (لا تحتاج إلى وصف)",
|
|
424
|
+
caption: "تعليق",
|
|
425
|
+
imageSize: "الحجم",
|
|
426
|
+
sizeSmall: "صغير",
|
|
427
|
+
sizeMedium: "متوسط",
|
|
428
|
+
sizeLarge: "كبير",
|
|
429
|
+
sizeOriginal: "الحجم الأصلي",
|
|
430
|
+
table: "جدول",
|
|
431
|
+
insertTable: "إدراج جدول",
|
|
432
|
+
rows: "الصفوف",
|
|
433
|
+
columns: "الأعمدة",
|
|
434
|
+
headerRow: "صف العناوين",
|
|
435
|
+
headerColumn: "عمود العناوين",
|
|
436
|
+
tableCaption: "عنوان الجدول",
|
|
437
|
+
addRowBefore: "إدراج صف أعلى",
|
|
438
|
+
addRowAfter: "إدراج صف أسفل",
|
|
439
|
+
addColumnBefore: "إدراج عمود قبل",
|
|
440
|
+
addColumnAfter: "إدراج عمود بعد",
|
|
441
|
+
deleteRow: "حذف الصف",
|
|
442
|
+
deleteColumn: "حذف العمود",
|
|
443
|
+
deleteTable: "حذف الجدول",
|
|
444
|
+
mergeCells: "دمج الخلايا",
|
|
445
|
+
splitCell: "تقسيم الخلية",
|
|
446
|
+
tableSize: "جدول {rows} × {cols}",
|
|
447
|
+
linkUrl: "عنوان الرابط (URL)",
|
|
448
|
+
linkText: "النص المعروض",
|
|
449
|
+
linkNewTab: "فتح في علامة تبويب جديدة",
|
|
450
|
+
invalidUrl: "هذا العنوان غير مسموح. استخدم عنوان ويب (https://…) أو بريدًا (mailto:) أو هاتفًا (tel:).",
|
|
451
|
+
insert: "إدراج",
|
|
452
|
+
save: "حفظ",
|
|
453
|
+
cancel: "إلغاء",
|
|
454
|
+
close: "إغلاق",
|
|
455
|
+
apply: "تطبيق",
|
|
456
|
+
remove: "إزالة",
|
|
457
|
+
find: "بحث",
|
|
458
|
+
findAndReplace: "بحث واستبدال",
|
|
459
|
+
replace: "استبدال",
|
|
460
|
+
replaceWith: "استبدال بـ",
|
|
461
|
+
replaceAll: "استبدال الكل",
|
|
462
|
+
findNext: "التطابق التالي",
|
|
463
|
+
findPrevious: "التطابق السابق",
|
|
464
|
+
matchCase: "مطابقة حالة الأحرف",
|
|
465
|
+
wholeWord: "كلمات كاملة",
|
|
466
|
+
matchCount: {
|
|
467
|
+
zero: "لا توجد نتائج",
|
|
468
|
+
one: "{current} من نتيجة واحدة",
|
|
469
|
+
two: "{current} من نتيجتين",
|
|
470
|
+
few: "{current} من {n} نتائج",
|
|
471
|
+
many: "{current} من {n} نتيجة",
|
|
472
|
+
other: "{current} من {n} نتيجة"
|
|
473
|
+
},
|
|
474
|
+
replacedCount: {
|
|
475
|
+
zero: "لم يُستبدل شيء",
|
|
476
|
+
one: "تم استبدال نتيجة واحدة",
|
|
477
|
+
two: "تم استبدال نتيجتين",
|
|
478
|
+
few: "تم استبدال {n} نتائج",
|
|
479
|
+
many: "تم استبدال {n} نتيجة",
|
|
480
|
+
other: "تم استبدال {n} نتيجة"
|
|
481
|
+
},
|
|
482
|
+
wordCount: {
|
|
483
|
+
zero: "لا كلمات",
|
|
484
|
+
one: "كلمة واحدة",
|
|
485
|
+
two: "كلمتان",
|
|
486
|
+
few: "{n} كلمات",
|
|
487
|
+
many: "{n} كلمة",
|
|
488
|
+
other: "{n} كلمة"
|
|
489
|
+
},
|
|
490
|
+
charCount: {
|
|
491
|
+
zero: "لا أحرف",
|
|
492
|
+
one: "حرف واحد",
|
|
493
|
+
two: "حرفان",
|
|
494
|
+
few: "{n} أحرف",
|
|
495
|
+
many: "{n} حرفًا",
|
|
496
|
+
other: "{n} حرف"
|
|
497
|
+
},
|
|
498
|
+
charLimit: "{n} من {limit} حرف",
|
|
499
|
+
charLimitReached: "تم الوصول إلى الحد الأقصى للأحرف",
|
|
500
|
+
elementPath: "مسار العنصر",
|
|
501
|
+
on: "{name}: مُفعّل",
|
|
502
|
+
off: "{name}: غير مُفعّل",
|
|
503
|
+
inserted: "تم إدراج {name}",
|
|
504
|
+
pasteCleaned: "تم تنظيف المحتوى الملصق",
|
|
505
|
+
pasteDropped: "بعض المحتوى الملصق غير مدعوم وتمت إزالته",
|
|
506
|
+
shortcuts: "اختصارات لوحة المفاتيح",
|
|
507
|
+
shortcutsHelp: "اضغط {shortcut} للانتقال إلى شريط الأدوات، وEscape للعودة إلى النص.",
|
|
508
|
+
a11yChecker: "مدقق إمكانية الوصول",
|
|
509
|
+
a11yIssues: {
|
|
510
|
+
zero: "لا توجد مشكلات في إمكانية الوصول",
|
|
511
|
+
one: "مشكلة واحدة في إمكانية الوصول",
|
|
512
|
+
two: "مشكلتان في إمكانية الوصول",
|
|
513
|
+
few: "{n} مشكلات في إمكانية الوصول",
|
|
514
|
+
many: "{n} مشكلة في إمكانية الوصول",
|
|
515
|
+
other: "{n} مشكلة في إمكانية الوصول"
|
|
516
|
+
},
|
|
517
|
+
placeholder: "ابدأ الكتابة…",
|
|
518
|
+
listItem: "عنصر قائمة",
|
|
519
|
+
tableRow: "صف",
|
|
520
|
+
tableCell: "خلية",
|
|
521
|
+
tableHeader: "خلية عنوان",
|
|
522
|
+
selectElement: "تحديد {name}",
|
|
523
|
+
editLink: "تعديل الرابط",
|
|
524
|
+
editImage: "تعديل الصورة",
|
|
525
|
+
required: "مطلوب",
|
|
526
|
+
altRequired: "صِف الصورة، أو حدّدها كصورة زخرفية.",
|
|
527
|
+
urlRequired: "أدخل عنوانًا.",
|
|
528
|
+
tableOptions: "خيارات الجدول",
|
|
529
|
+
tableProperties: "خصائص الجدول",
|
|
530
|
+
closeFind: "إغلاق البحث والاستبدال",
|
|
531
|
+
action: "الإجراء",
|
|
532
|
+
shortcut: "الاختصار",
|
|
533
|
+
leaveEditor: "اضغط Escape ثم Tab لمغادرة المحرر من داخل قائمة أو جدول.",
|
|
534
|
+
markdownShortcuts: "اختصارات الكتابة",
|
|
535
|
+
navigation: "التنقل",
|
|
536
|
+
formatting: "التنسيق",
|
|
537
|
+
blocks: "الكتل",
|
|
538
|
+
focusToolbar: "نقل التركيز إلى شريط الأدوات",
|
|
539
|
+
showShortcuts: "عرض اختصارات لوحة المفاتيح",
|
|
540
|
+
openFind: "بحث واستبدال",
|
|
541
|
+
insertLink: "إدراج رابط",
|
|
542
|
+
sourceCode: "الشيفرة المصدرية",
|
|
543
|
+
sourceCleaned: "بعض شيفرة HTML غير مدعومة وتمت إزالتها أو تبسيطها",
|
|
544
|
+
textColor: "لون النص",
|
|
545
|
+
highlight: "لون التمييز",
|
|
546
|
+
automatic: "تلقائي",
|
|
547
|
+
noHighlight: "بدون تمييز",
|
|
548
|
+
customColor: "لون مخصص",
|
|
549
|
+
color: "اللون",
|
|
550
|
+
hexValue: "القيمة السداسية",
|
|
551
|
+
invalidColor: "أدخل لونًا مثل #1a73e8.",
|
|
552
|
+
contrastText: "التباين على خلفية بيضاء: {ratio}:1",
|
|
553
|
+
contrastBg: "التباين مع نص داكن: {ratio}:1",
|
|
554
|
+
contrastPass: "يستوفي معيار WCAG AA للنص العادي.",
|
|
555
|
+
contrastFail: "أقل من الحد الأدنى 4.5:1 للنص العادي (WCAG 1.4.3).",
|
|
556
|
+
colorBlack: "أسود",
|
|
557
|
+
colorDarkGray: "رمادي داكن",
|
|
558
|
+
colorDarkRed: "أحمر داكن",
|
|
559
|
+
colorDarkOrange: "برتقالي داكن",
|
|
560
|
+
colorOlive: "زيتوني",
|
|
561
|
+
colorDarkGreen: "أخضر داكن",
|
|
562
|
+
colorTeal: "أزرق مخضر",
|
|
563
|
+
colorBlue: "أزرق",
|
|
564
|
+
colorPurple: "بنفسجي",
|
|
565
|
+
colorMagenta: "أرجواني",
|
|
566
|
+
colorYellow: "أصفر",
|
|
567
|
+
colorGreen: "أخضر",
|
|
568
|
+
colorPink: "وردي",
|
|
569
|
+
colorOrange: "برتقالي",
|
|
570
|
+
colorGray: "رمادي"
|
|
571
|
+
};
|
|
572
|
+
var builtInLanguages = [
|
|
573
|
+
{ code: "en", name: "English", dir: "ltr", labels: {} },
|
|
574
|
+
{ code: "ar", name: "العربية", dir: "rtl", labels: arabicLabels }
|
|
575
|
+
];
|
|
576
|
+
var RTL_LANGS = /^(ar|arc|ckb|dv|fa|he|iw|ks|ps|sd|ug|ur|yi)(-|$)/i;
|
|
577
|
+
function resolveLanguage(code, languages = builtInLanguages) {
|
|
578
|
+
const wanted = code ?? (typeof document !== "undefined" ? document.documentElement.lang : "") ?? "en";
|
|
579
|
+
const lower = wanted.toLowerCase();
|
|
580
|
+
return languages.find((l) => l.code.toLowerCase() === lower) ?? languages.find((l) => lower.split("-")[0] === l.code.toLowerCase().split("-")[0]) ?? languages[0] ?? { code: "en", name: "English", dir: "ltr", labels: {} };
|
|
581
|
+
}
|
|
582
|
+
function createI18n(language, overrides = {}) {
|
|
583
|
+
const labels = { ...defaultLabels, ...language.labels, ...overrides };
|
|
584
|
+
let plural = null;
|
|
585
|
+
try {
|
|
586
|
+
plural = new Intl.PluralRules(language.code);
|
|
587
|
+
} catch {
|
|
588
|
+
plural = null;
|
|
589
|
+
}
|
|
590
|
+
const t = (key, vars = {}) => {
|
|
591
|
+
const label = labels[key] ?? defaultLabels[key];
|
|
592
|
+
let text;
|
|
593
|
+
if (label === void 0) text = String(key);
|
|
594
|
+
else if (typeof label === "string") text = label;
|
|
595
|
+
else {
|
|
596
|
+
const n = Number(vars.n ?? 0);
|
|
597
|
+
const form = n === 0 && label.zero ? "zero" : plural?.select(n) ?? "other";
|
|
598
|
+
text = label[form] ?? label.other ?? Object.values(label)[0] ?? key;
|
|
599
|
+
}
|
|
600
|
+
return text.replace(
|
|
601
|
+
/\{(\w+)\}/g,
|
|
602
|
+
(m, name) => name in vars ? formatVar(vars[name], language.code) : m
|
|
603
|
+
);
|
|
604
|
+
};
|
|
605
|
+
return {
|
|
606
|
+
lang: language.code,
|
|
607
|
+
dir: language.dir ?? (RTL_LANGS.test(language.code) ? "rtl" : "ltr"),
|
|
608
|
+
t
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
function formatVar(value, lang) {
|
|
612
|
+
if (typeof value !== "number") return value;
|
|
613
|
+
try {
|
|
614
|
+
return new Intl.NumberFormat(lang).format(value);
|
|
615
|
+
} catch {
|
|
616
|
+
return String(value);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// src/core/trusted-types.ts
|
|
621
|
+
var TRUSTED_TYPES_POLICY_NAME = "open-wysiwyg-editor";
|
|
622
|
+
var REGISTRY = /* @__PURE__ */ Symbol.for("open-wysiwyg-editor.tt-policy");
|
|
623
|
+
var custom;
|
|
624
|
+
function setTrustedTypesPolicy(policy) {
|
|
625
|
+
custom = policy;
|
|
626
|
+
}
|
|
627
|
+
function getTrustedTypesPolicy() {
|
|
628
|
+
if (custom !== void 0) return custom;
|
|
629
|
+
const store = globalThis;
|
|
630
|
+
if (store[REGISTRY] !== void 0) return store[REGISTRY] ?? null;
|
|
631
|
+
const tt = globalThis.trustedTypes;
|
|
632
|
+
let policy = null;
|
|
633
|
+
if (tt && typeof tt.createPolicy === "function") {
|
|
634
|
+
try {
|
|
635
|
+
policy = tt.createPolicy(TRUSTED_TYPES_POLICY_NAME, {
|
|
636
|
+
createHTML: (s) => s,
|
|
637
|
+
// DOMPurify requires this hook; the editor never produces script URLs.
|
|
638
|
+
createScriptURL: () => ""
|
|
639
|
+
});
|
|
640
|
+
} catch {
|
|
641
|
+
policy = null;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
store[REGISTRY] = policy;
|
|
645
|
+
return policy;
|
|
646
|
+
}
|
|
647
|
+
var FORBIDDEN_TAGS = [
|
|
648
|
+
"script",
|
|
649
|
+
"style",
|
|
650
|
+
"iframe",
|
|
651
|
+
"frame",
|
|
652
|
+
"frameset",
|
|
653
|
+
"object",
|
|
654
|
+
"embed",
|
|
655
|
+
"applet",
|
|
656
|
+
"form",
|
|
657
|
+
"input",
|
|
658
|
+
"button",
|
|
659
|
+
"select",
|
|
660
|
+
"textarea",
|
|
661
|
+
"option",
|
|
662
|
+
"base",
|
|
663
|
+
"meta",
|
|
664
|
+
"link",
|
|
665
|
+
"template",
|
|
666
|
+
"noscript",
|
|
667
|
+
"svg",
|
|
668
|
+
"math"
|
|
669
|
+
];
|
|
670
|
+
var DROPPED_CONTENTS = [
|
|
671
|
+
"script",
|
|
672
|
+
"style",
|
|
673
|
+
"template",
|
|
674
|
+
"noscript",
|
|
675
|
+
"iframe",
|
|
676
|
+
"frame",
|
|
677
|
+
"frameset",
|
|
678
|
+
"object",
|
|
679
|
+
"embed",
|
|
680
|
+
"applet",
|
|
681
|
+
"svg",
|
|
682
|
+
"math",
|
|
683
|
+
"select",
|
|
684
|
+
"option",
|
|
685
|
+
"textarea",
|
|
686
|
+
"button",
|
|
687
|
+
"head",
|
|
688
|
+
"title"
|
|
689
|
+
];
|
|
690
|
+
var FORBIDDEN_ATTRS = ["srcdoc", "formaction", "xlink:href", "action", "ping"];
|
|
691
|
+
function config(extra) {
|
|
692
|
+
const policy = getTrustedTypesPolicy();
|
|
693
|
+
return {
|
|
694
|
+
FORBID_TAGS: FORBIDDEN_TAGS,
|
|
695
|
+
FORBID_ATTR: FORBIDDEN_ATTRS,
|
|
696
|
+
// Drop the *contents* of these too (not just the tags): CSS, scripts, form widgets, etc.
|
|
697
|
+
ADD_FORBID_CONTENTS: DROPPED_CONTENTS,
|
|
698
|
+
ALLOW_UNKNOWN_PROTOCOLS: false,
|
|
699
|
+
// Pasted Office/Google Docs content carries formatting in `style`; the schema parser reads
|
|
700
|
+
// only the specific properties it understands and never copies raw style into the document.
|
|
701
|
+
...policy ? { TRUSTED_TYPES_POLICY: policy } : {},
|
|
702
|
+
...extra
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
function stripInertBlocks(html) {
|
|
706
|
+
return html.replace(/<style\b[^>]*>[\s\S]*?<\/style\s*>/gi, "").replace(/<!--\[if[\s\S]*?<!\[endif\]-->/gi, "").replace(/<xml\b[^>]*>[\s\S]*?<\/xml\s*>/gi, "");
|
|
707
|
+
}
|
|
708
|
+
var STYLE_DATA_ATTR = "data-owe-style";
|
|
709
|
+
var STYLE_ATTR = /(<[a-z][^\s/>]*(?:\s+[^\s/>="']+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'=<>`]+))?)*?\s+)style(?=\s*=)/gi;
|
|
710
|
+
function protectStyles(html) {
|
|
711
|
+
return html.replace(STYLE_ATTR, `$1${STYLE_DATA_ATTR}`);
|
|
712
|
+
}
|
|
713
|
+
function restoreStyles(root) {
|
|
714
|
+
const scope = root;
|
|
715
|
+
if (typeof scope.querySelectorAll !== "function") return;
|
|
716
|
+
for (const el of [...scope.querySelectorAll(`[${STYLE_DATA_ATTR}]`)]) {
|
|
717
|
+
const css = el.getAttribute(STYLE_DATA_ATTR) ?? "";
|
|
718
|
+
el.removeAttribute(STYLE_DATA_ATTR);
|
|
719
|
+
if (el.style) el.style.cssText = css;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
function sanitizePastedHTML(html) {
|
|
723
|
+
return DOMPurify.sanitize(stripInertBlocks(html), { ...config(), RETURN_TRUSTED_TYPE: false });
|
|
724
|
+
}
|
|
725
|
+
function sanitizeToFragment(html) {
|
|
726
|
+
const fragment = DOMPurify.sanitize(`<div data-owe-wrap="">${protectStyles(stripInertBlocks(html))}</div>`, {
|
|
727
|
+
...config(),
|
|
728
|
+
RETURN_DOM_FRAGMENT: true
|
|
729
|
+
});
|
|
730
|
+
for (const wrap of [...fragment.children].filter((el) => el.hasAttribute("data-owe-wrap"))) {
|
|
731
|
+
wrap.replaceWith(...wrap.childNodes);
|
|
732
|
+
}
|
|
733
|
+
return fragment;
|
|
734
|
+
}
|
|
735
|
+
function sanitizeHTML(html, options) {
|
|
736
|
+
const purify = options?.window ? DOMPurify(options.window) : DOMPurify;
|
|
737
|
+
return purify.sanitize(html, { ...config(options?.config), RETURN_TRUSTED_TYPE: false });
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// src/core/paste.ts
|
|
741
|
+
var STYLED = `[${STYLE_DATA_ATTR}]`;
|
|
742
|
+
var WORD = /urn:schemas-microsoft-com:office|class=["']?Mso|mso-[a-z-]+:/i;
|
|
743
|
+
var GDOCS = /id=["']?docs-internal-guid/i;
|
|
744
|
+
var MSO_IGNORE = /mso-list\s*:\s*ignore/i;
|
|
745
|
+
var MSO_LEVEL = /mso-list\s*:[^;"]*level(\d+)/i;
|
|
746
|
+
var BULLETS = /^[•·▪●◦–—o§·•▪●◦\-*]$/;
|
|
747
|
+
var ORDERED = /^\(?([0-9]{1,4}|[a-zA-Z]|[ivxlcdmIVXLCDM]{1,6})[.)]$/;
|
|
748
|
+
var BLOCKS = ":scope > :is(p, h1, h2, h3, h4, h5, h6, div, ul, ol, table, blockquote, pre)";
|
|
749
|
+
function cleanPastedHTML(input, policy = {}) {
|
|
750
|
+
const fragment = sanitizeToFragment(input);
|
|
751
|
+
if (!fragment.ownerDocument) return { html: "", dropped: false, cleaned: false };
|
|
752
|
+
const root = fragment.ownerDocument.createElement("div");
|
|
753
|
+
root.append(fragment);
|
|
754
|
+
const isWord = WORD.test(input);
|
|
755
|
+
const isGDocs = GDOCS.test(input);
|
|
756
|
+
let cleaned = false;
|
|
757
|
+
let dropped = false;
|
|
758
|
+
if (isWord) {
|
|
759
|
+
convertWordLists(root);
|
|
760
|
+
for (const el of [...root.querySelectorAll(STYLED)]) {
|
|
761
|
+
if (MSO_IGNORE.test(el.getAttribute(STYLE_DATA_ATTR) ?? "")) el.remove();
|
|
762
|
+
}
|
|
763
|
+
for (const span of [...root.querySelectorAll("span")].reverse()) {
|
|
764
|
+
if (!span.textContent && !span.querySelector("img, br")) span.remove();
|
|
765
|
+
}
|
|
766
|
+
cleaned = true;
|
|
767
|
+
}
|
|
768
|
+
if (isGDocs) {
|
|
769
|
+
for (const el of root.querySelectorAll("[dir]")) el.removeAttribute("dir");
|
|
770
|
+
cleaned = true;
|
|
771
|
+
}
|
|
772
|
+
cleaned = nestLooseLists(root) || cleaned;
|
|
773
|
+
cleaned = nestAriaLevelLists(root) || cleaned;
|
|
774
|
+
for (const img of [...root.querySelectorAll("img")]) {
|
|
775
|
+
if (!sanitizeUrl(img.getAttribute("src"), policy, "image")) {
|
|
776
|
+
img.remove();
|
|
777
|
+
dropped = true;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
for (const font of [...root.querySelectorAll("font")]) font.replaceWith(...font.childNodes);
|
|
781
|
+
return { html: serializeInert(root), dropped, cleaned };
|
|
782
|
+
}
|
|
783
|
+
function serializeInert(container) {
|
|
784
|
+
return container.innerHTML;
|
|
785
|
+
}
|
|
786
|
+
function isWordListParagraph(el) {
|
|
787
|
+
return el.tagName === "P" && /mso-list\s*:/i.test(el.getAttribute(STYLE_DATA_ATTR) ?? "") && !MSO_IGNORE.test(el.getAttribute(STYLE_DATA_ATTR) ?? "");
|
|
788
|
+
}
|
|
789
|
+
function markerOf(p) {
|
|
790
|
+
const ignore = [...p.querySelectorAll(STYLED)].find((el) => MSO_IGNORE.test(el.getAttribute(STYLE_DATA_ATTR) ?? ""));
|
|
791
|
+
const text = (ignore?.textContent ?? "").trim();
|
|
792
|
+
ignore?.remove();
|
|
793
|
+
return text;
|
|
794
|
+
}
|
|
795
|
+
function convertWordLists(root) {
|
|
796
|
+
const parents = /* @__PURE__ */ new Set();
|
|
797
|
+
for (const p of root.querySelectorAll("p")) if (isWordListParagraph(p) && p.parentElement) parents.add(p.parentElement);
|
|
798
|
+
let changed = false;
|
|
799
|
+
for (const parent of parents) {
|
|
800
|
+
const children = [...parent.children];
|
|
801
|
+
for (let i = 0; i < children.length; ) {
|
|
802
|
+
if (!isWordListParagraph(children[i])) {
|
|
803
|
+
i++;
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
806
|
+
const run = [];
|
|
807
|
+
while (i < children.length && isWordListParagraph(children[i])) run.push(children[i++]);
|
|
808
|
+
buildNestedList(run, (p) => {
|
|
809
|
+
const level = Number(MSO_LEVEL.exec(p.getAttribute(STYLE_DATA_ATTR) ?? "")?.[1] ?? 1);
|
|
810
|
+
const marker = markerOf(p);
|
|
811
|
+
const ordered = ORDERED.test(marker) && !BULLETS.test(marker);
|
|
812
|
+
const start = ordered && /^\d+/.test(marker) ? Number.parseInt(marker, 10) : 1;
|
|
813
|
+
return { level, ordered, start };
|
|
814
|
+
});
|
|
815
|
+
changed = true;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
return changed;
|
|
819
|
+
}
|
|
820
|
+
function buildNestedList(run, info) {
|
|
821
|
+
const doc = run[0].ownerDocument;
|
|
822
|
+
const stack = [];
|
|
823
|
+
const anchor = doc.createElement("div");
|
|
824
|
+
run[0].before(anchor);
|
|
825
|
+
for (const el of run) {
|
|
826
|
+
const { level, ordered, start } = info(el);
|
|
827
|
+
while (stack.length && stack[stack.length - 1].level > level) stack.pop();
|
|
828
|
+
let top = stack[stack.length - 1];
|
|
829
|
+
if (!top || top.level < level) {
|
|
830
|
+
const list = doc.createElement(ordered ? "ol" : "ul");
|
|
831
|
+
if (ordered && start > 1) list.setAttribute("start", String(start));
|
|
832
|
+
if (top?.last) top.last.append(list);
|
|
833
|
+
else if (top) top.list.append(doc.createElement("li"), list);
|
|
834
|
+
else anchor.append(list);
|
|
835
|
+
top = { level, list, last: null };
|
|
836
|
+
stack.push(top);
|
|
837
|
+
}
|
|
838
|
+
const li = doc.createElement("li");
|
|
839
|
+
if (el.querySelector(BLOCKS)) {
|
|
840
|
+
li.append(...el.childNodes);
|
|
841
|
+
} else {
|
|
842
|
+
const p = doc.createElement("p");
|
|
843
|
+
p.append(...el.childNodes);
|
|
844
|
+
for (const attr of ["dir", "lang"]) {
|
|
845
|
+
const v = el.getAttribute(attr);
|
|
846
|
+
if (v) p.setAttribute(attr, v);
|
|
847
|
+
}
|
|
848
|
+
li.append(p);
|
|
849
|
+
}
|
|
850
|
+
top.list.append(li);
|
|
851
|
+
top.last = li;
|
|
852
|
+
el.remove();
|
|
853
|
+
}
|
|
854
|
+
anchor.replaceWith(...anchor.childNodes);
|
|
855
|
+
}
|
|
856
|
+
function nestLooseLists(root) {
|
|
857
|
+
let changed = false;
|
|
858
|
+
for (const list of [...root.querySelectorAll("ul > ul, ul > ol, ol > ul, ol > ol")]) {
|
|
859
|
+
const prev = list.previousElementSibling;
|
|
860
|
+
if (prev?.tagName === "LI") prev.append(list);
|
|
861
|
+
else {
|
|
862
|
+
const li = list.ownerDocument.createElement("li");
|
|
863
|
+
list.before(li);
|
|
864
|
+
li.append(list);
|
|
865
|
+
}
|
|
866
|
+
changed = true;
|
|
867
|
+
}
|
|
868
|
+
return changed;
|
|
869
|
+
}
|
|
870
|
+
function nestAriaLevelLists(root) {
|
|
871
|
+
let changed = false;
|
|
872
|
+
for (const list of [...root.querySelectorAll("ul, ol")]) {
|
|
873
|
+
const items = [...list.children].filter((c) => c.tagName === "LI");
|
|
874
|
+
const levels = items.map((li) => Number(li.getAttribute("aria-level") ?? 1));
|
|
875
|
+
if (items.length < 2 || levels.every((l) => l === levels[0])) continue;
|
|
876
|
+
const ordered = list.tagName === "OL";
|
|
877
|
+
const wrapper = list.ownerDocument.createElement("div");
|
|
878
|
+
list.before(wrapper);
|
|
879
|
+
for (const li of items) {
|
|
880
|
+
const item = list.ownerDocument.createElement("div");
|
|
881
|
+
item.append(...li.childNodes);
|
|
882
|
+
item.setAttribute("data-level", li.getAttribute("aria-level") ?? "1");
|
|
883
|
+
wrapper.append(item);
|
|
884
|
+
}
|
|
885
|
+
list.remove();
|
|
886
|
+
buildNestedList([...wrapper.children], (el) => ({
|
|
887
|
+
level: Number(el.getAttribute("data-level")),
|
|
888
|
+
ordered,
|
|
889
|
+
start: 1
|
|
890
|
+
}));
|
|
891
|
+
wrapper.replaceWith(...wrapper.childNodes);
|
|
892
|
+
changed = true;
|
|
893
|
+
}
|
|
894
|
+
return changed;
|
|
895
|
+
}
|
|
896
|
+
function createCommandManager(factories, host) {
|
|
897
|
+
const names = Object.keys(factories);
|
|
898
|
+
const get = (name, args) => factories[name](...args);
|
|
899
|
+
const commands = {};
|
|
900
|
+
const can = {};
|
|
901
|
+
for (const name of names) {
|
|
902
|
+
commands[name] = (...args) => {
|
|
903
|
+
const view = host.view();
|
|
904
|
+
return get(name, args)(view.state, view.dispatch, view);
|
|
905
|
+
};
|
|
906
|
+
can[name] = (...args) => {
|
|
907
|
+
const view = host.view();
|
|
908
|
+
return get(name, args)(view.state, void 0, view);
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
function chain() {
|
|
912
|
+
const queue = [];
|
|
913
|
+
const api = {};
|
|
914
|
+
for (const name of names) {
|
|
915
|
+
api[name] = (...args) => {
|
|
916
|
+
queue.push({ name, args });
|
|
917
|
+
return api;
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
api.run = () => {
|
|
921
|
+
const view = host.view();
|
|
922
|
+
let state = view.state;
|
|
923
|
+
const tr = state.tr;
|
|
924
|
+
let focusTo = null;
|
|
925
|
+
for (const { name, args } of queue) {
|
|
926
|
+
if (name === "focus") {
|
|
927
|
+
focusTo = args[0];
|
|
928
|
+
continue;
|
|
929
|
+
}
|
|
930
|
+
let applied = null;
|
|
931
|
+
const ok = get(name, args)(state, (t) => applied = t, view);
|
|
932
|
+
if (!ok) return false;
|
|
933
|
+
if (applied) state = foldInto(tr, applied, state);
|
|
934
|
+
}
|
|
935
|
+
if (tr.docChanged || tr.selectionSet || tr.storedMarksSet) view.dispatch(tr.scrollIntoView());
|
|
936
|
+
if (focusTo !== null) host.focus(focusTo);
|
|
937
|
+
return true;
|
|
938
|
+
};
|
|
939
|
+
return api;
|
|
940
|
+
}
|
|
941
|
+
return { commands, can, chain };
|
|
942
|
+
}
|
|
943
|
+
function foldInto(target, source, before) {
|
|
944
|
+
for (const step of source.steps) target.step(step);
|
|
945
|
+
if (source.selectionSet) {
|
|
946
|
+
target.setSelection(Selection.fromJSON(target.doc, source.selection.toJSON()));
|
|
947
|
+
}
|
|
948
|
+
if (source.storedMarksSet) target.setStoredMarks(source.storedMarks);
|
|
949
|
+
return before.apply(source);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
// src/core/dom.ts
|
|
953
|
+
function h(tag, props, ...children) {
|
|
954
|
+
const el = document.createElement(tag);
|
|
955
|
+
if (props) {
|
|
956
|
+
for (const [key, value] of Object.entries(props)) {
|
|
957
|
+
if (value === void 0 || value === null || value === false) continue;
|
|
958
|
+
if (key === "className") el.className = String(value);
|
|
959
|
+
else if (key === "style") {
|
|
960
|
+
for (const [prop, v] of Object.entries(value)) {
|
|
961
|
+
if (v != null) el.style.setProperty(prop, v);
|
|
962
|
+
}
|
|
963
|
+
} else if (key.startsWith("on") && typeof value === "function") {
|
|
964
|
+
el.addEventListener(key.slice(2).toLowerCase(), value);
|
|
965
|
+
} else if (value === true) el.setAttribute(key, "");
|
|
966
|
+
else el.setAttribute(key, String(value));
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
for (const child of children.flat()) {
|
|
970
|
+
if (child === null || child === void 0 || child === false) continue;
|
|
971
|
+
el.append(typeof child === "object" ? child : String(child));
|
|
972
|
+
}
|
|
973
|
+
return el;
|
|
974
|
+
}
|
|
975
|
+
var counter = 0;
|
|
976
|
+
var uid = (prefix = "owe") => `${prefix}-${(++counter).toString(36)}${Math.random().toString(36).slice(2, 6)}`;
|
|
977
|
+
function svg(paths, className = "owe-icon") {
|
|
978
|
+
const ns = "http://www.w3.org/2000/svg";
|
|
979
|
+
const el = document.createElementNS(ns, "svg");
|
|
980
|
+
el.setAttribute("viewBox", "0 0 24 24");
|
|
981
|
+
el.setAttribute("aria-hidden", "true");
|
|
982
|
+
el.setAttribute("focusable", "false");
|
|
983
|
+
el.setAttribute("class", className);
|
|
984
|
+
for (const d of paths) {
|
|
985
|
+
const path = document.createElementNS(ns, "path");
|
|
986
|
+
path.setAttribute("d", d);
|
|
987
|
+
el.append(path);
|
|
988
|
+
}
|
|
989
|
+
return el;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
// src/core/env.ts
|
|
993
|
+
var LIB = "open-wysiwyg-editor";
|
|
994
|
+
var isBrowser = () => typeof window !== "undefined" && typeof document !== "undefined";
|
|
995
|
+
function assertBrowser(what) {
|
|
996
|
+
if (!isBrowser()) {
|
|
997
|
+
throw new Error(
|
|
998
|
+
`[${LIB}] ${what} needs a browser DOM. Call it on the client (e.g. in useEffect / onMounted / afterNextRender), or use "${LIB}/server" helpers for server-side rendering.`
|
|
999
|
+
);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
var isMac = () => typeof navigator !== "undefined" && /Mac|iPhone|iPad|iPod/.test(
|
|
1003
|
+
navigator.userAgentData?.platform ?? navigator.platform ?? ""
|
|
1004
|
+
);
|
|
1005
|
+
function warn(message) {
|
|
1006
|
+
if (typeof console !== "undefined") console.warn(`[${LIB}] ${message}`);
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
// src/core/events.ts
|
|
1010
|
+
function createEmitter() {
|
|
1011
|
+
const listeners = /* @__PURE__ */ new Map();
|
|
1012
|
+
const off = (event, listener) => {
|
|
1013
|
+
listeners.get(event)?.delete(listener);
|
|
1014
|
+
};
|
|
1015
|
+
return {
|
|
1016
|
+
on(event, listener) {
|
|
1017
|
+
let set = listeners.get(event);
|
|
1018
|
+
if (!set) listeners.set(event, set = /* @__PURE__ */ new Set());
|
|
1019
|
+
set.add(listener);
|
|
1020
|
+
return () => off(event, listener);
|
|
1021
|
+
},
|
|
1022
|
+
off,
|
|
1023
|
+
emit(event, ...args) {
|
|
1024
|
+
for (const listener of [...listeners.get(event) ?? []]) {
|
|
1025
|
+
try {
|
|
1026
|
+
listener(...args);
|
|
1027
|
+
} catch (error) {
|
|
1028
|
+
console.error("[open-wysiwyg-editor] listener error", error);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
},
|
|
1032
|
+
clear() {
|
|
1033
|
+
listeners.clear();
|
|
1034
|
+
}
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
function buildSchema(extensions, ctx) {
|
|
1038
|
+
const nodes = {};
|
|
1039
|
+
const marks = {};
|
|
1040
|
+
for (const ext of extensions) {
|
|
1041
|
+
Object.assign(nodes, ext.config.nodes?.(ext.options, ctx));
|
|
1042
|
+
Object.assign(marks, ext.config.marks?.(ext.options, ctx));
|
|
1043
|
+
}
|
|
1044
|
+
if (!nodes.doc || !nodes.text) {
|
|
1045
|
+
throw new Error("[open-wysiwyg-editor] The Document and Text extensions are required.");
|
|
1046
|
+
}
|
|
1047
|
+
for (const ext of extensions) {
|
|
1048
|
+
for (const group of ext.config.globalAttributes?.(ext.options) ?? []) {
|
|
1049
|
+
for (const type of group.types) {
|
|
1050
|
+
if (nodes[type]) nodes[type] = withGlobalAttributes(nodes[type], group.attributes);
|
|
1051
|
+
else if (marks[type]) marks[type] = withGlobalAttributes(marks[type], group.attributes);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
const ordered = { doc: nodes.doc };
|
|
1056
|
+
if (nodes.paragraph) ordered.paragraph = nodes.paragraph;
|
|
1057
|
+
for (const [name, spec] of Object.entries(nodes)) if (!(name in ordered)) ordered[name] = spec;
|
|
1058
|
+
const schema = new Schema({ nodes: ordered, marks });
|
|
1059
|
+
schema.cached.urlPolicy = ctx.urlPolicy;
|
|
1060
|
+
return schema;
|
|
1061
|
+
}
|
|
1062
|
+
function withGlobalAttributes(spec, globals) {
|
|
1063
|
+
const entries = Object.entries(globals);
|
|
1064
|
+
const attrs = { ...spec.attrs };
|
|
1065
|
+
for (const [name, g] of entries) attrs[name] = { default: g.default ?? null };
|
|
1066
|
+
const parseDOM = spec.parseDOM?.map((rule) => {
|
|
1067
|
+
if (!("tag" in rule) || !rule.tag) return rule;
|
|
1068
|
+
const tagRule = rule;
|
|
1069
|
+
return {
|
|
1070
|
+
...tagRule,
|
|
1071
|
+
getAttrs(dom) {
|
|
1072
|
+
const base = tagRule.getAttrs ? tagRule.getAttrs(dom) : tagRule.attrs ?? null;
|
|
1073
|
+
if (base === false) return false;
|
|
1074
|
+
const extra = {};
|
|
1075
|
+
for (const [name, g] of entries) {
|
|
1076
|
+
const value = g.parseHTML ? g.parseHTML(dom) : dom.getAttribute(name);
|
|
1077
|
+
if (value !== null && value !== void 0) extra[name] = value;
|
|
1078
|
+
}
|
|
1079
|
+
return { ...tagRule.attrs, ...base, ...extra };
|
|
1080
|
+
}
|
|
1081
|
+
};
|
|
1082
|
+
});
|
|
1083
|
+
const render = (mode, owner, out) => {
|
|
1084
|
+
let extra = {};
|
|
1085
|
+
for (const [name, g] of entries) {
|
|
1086
|
+
const fn = mode === "dom" ? g.renderDOM ?? g.renderHTML : g.renderHTML;
|
|
1087
|
+
const value = owner.attrs[name];
|
|
1088
|
+
const rendered = fn ? fn(value, owner) : value == null ? null : { [name]: String(value) };
|
|
1089
|
+
if (rendered) extra = mergeAttrs(extra, rendered);
|
|
1090
|
+
}
|
|
1091
|
+
return injectAttrs(out, extra);
|
|
1092
|
+
};
|
|
1093
|
+
const toDOM = spec.toDOM;
|
|
1094
|
+
const toHTML = spec.toHTML ?? toDOM;
|
|
1095
|
+
return {
|
|
1096
|
+
...spec,
|
|
1097
|
+
attrs,
|
|
1098
|
+
parseDOM,
|
|
1099
|
+
toDOM: toDOM && ((owner, ...rest) => render("dom", owner, toDOM(owner, ...rest))),
|
|
1100
|
+
toHTML: toHTML && ((owner, ...rest) => render("html", owner, toHTML(owner, ...rest)))
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
function mergeAttrs(a, b) {
|
|
1104
|
+
const out = { ...a };
|
|
1105
|
+
for (const [key, value] of Object.entries(b)) {
|
|
1106
|
+
if (key === "class" && out.class) out.class = `${out.class} ${value}`;
|
|
1107
|
+
else if (key === "style" && out.style) out.style = `${out.style.replace(/;?\s*$/, ";")} ${value}`;
|
|
1108
|
+
else out[key] = value;
|
|
1109
|
+
}
|
|
1110
|
+
return out;
|
|
1111
|
+
}
|
|
1112
|
+
function isAttrs(value) {
|
|
1113
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && !(typeof Node !== "undefined" && value instanceof Node);
|
|
1114
|
+
}
|
|
1115
|
+
function injectAttrs(out, extra) {
|
|
1116
|
+
if (!Array.isArray(out) || Object.keys(extra).length === 0) return out;
|
|
1117
|
+
const [tag, maybeAttrs, ...rest] = out;
|
|
1118
|
+
if (isAttrs(maybeAttrs)) {
|
|
1119
|
+
const clean = Object.fromEntries(
|
|
1120
|
+
Object.entries(maybeAttrs).filter(([, v]) => v !== null && v !== void 0)
|
|
1121
|
+
);
|
|
1122
|
+
return [tag, mergeAttrs(clean, extra), ...rest];
|
|
1123
|
+
}
|
|
1124
|
+
const tail = maybeAttrs === void 0 ? rest : [maybeAttrs, ...rest];
|
|
1125
|
+
return [tag, extra, ...tail];
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// src/core/editor.ts
|
|
1129
|
+
var placeholderKey = new PluginKey("owe-placeholder");
|
|
1130
|
+
function createEditor(options = {}) {
|
|
1131
|
+
assertBrowser("createEditor()");
|
|
1132
|
+
let opts = { ...options };
|
|
1133
|
+
const emitter = createEmitter();
|
|
1134
|
+
const language = resolveLanguage(opts.language, opts.languages);
|
|
1135
|
+
const i18n = createI18n(language, opts.labels);
|
|
1136
|
+
const urlPolicy = opts.urlPolicy ?? {};
|
|
1137
|
+
const extensions = resolveExtensions([CoreExtension, ...opts.extensions ?? []]);
|
|
1138
|
+
const schema = buildSchema(extensions, { t: i18n.t, urlPolicy });
|
|
1139
|
+
const domParser = DOMParser.fromSchema(schema);
|
|
1140
|
+
const parseHTML = (html) => {
|
|
1141
|
+
const fragment = sanitizeToFragment(html);
|
|
1142
|
+
restoreStyles(fragment);
|
|
1143
|
+
return domParser.parse(fragment);
|
|
1144
|
+
};
|
|
1145
|
+
const clipboardParser = Object.create(domParser);
|
|
1146
|
+
clipboardParser.parseSlice = (dom, options2) => {
|
|
1147
|
+
restoreStyles(dom);
|
|
1148
|
+
return domParser.parseSlice(dom, options2);
|
|
1149
|
+
};
|
|
1150
|
+
const target = opts.element ?? null;
|
|
1151
|
+
const textarea = target instanceof HTMLTextAreaElement ? target : null;
|
|
1152
|
+
let initial = opts.content;
|
|
1153
|
+
if (initial === void 0 && target) initial = textarea ? textarea.value : target;
|
|
1154
|
+
const root = h("div", { className: "owe", dir: i18n.dir, lang: i18n.lang });
|
|
1155
|
+
const body = h("div", { className: "owe-body" });
|
|
1156
|
+
const polite = h("div", { className: "owe-sr-only", role: "status", "aria-live": "polite" });
|
|
1157
|
+
const assertive = h("div", { className: "owe-sr-only", role: "alert", "aria-live": "assertive" });
|
|
1158
|
+
root.append(body, polite, assertive);
|
|
1159
|
+
if (textarea) {
|
|
1160
|
+
textarea.hidden = true;
|
|
1161
|
+
textarea.after(root);
|
|
1162
|
+
if (!opts.ariaLabel && !opts.ariaLabelledBy && textarea.labels?.length) {
|
|
1163
|
+
const label = textarea.labels[0];
|
|
1164
|
+
if (!label.id) label.id = uid("owe-label");
|
|
1165
|
+
opts.ariaLabelledBy = label.id;
|
|
1166
|
+
label.addEventListener("click", focusFromLabel);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
let editable = opts.editable !== false;
|
|
1170
|
+
let destroyed = false;
|
|
1171
|
+
let focused = false;
|
|
1172
|
+
const doc = parseContent(initial, true) ?? schema.topNodeType.createAndFill();
|
|
1173
|
+
if (target && !textarea) {
|
|
1174
|
+
target.replaceChildren(root);
|
|
1175
|
+
}
|
|
1176
|
+
const ctxFor = (ext) => ({
|
|
1177
|
+
options: ext.options,
|
|
1178
|
+
schema,
|
|
1179
|
+
editor: editorRef
|
|
1180
|
+
});
|
|
1181
|
+
const plugins = [];
|
|
1182
|
+
const rules = [];
|
|
1183
|
+
const factories = {};
|
|
1184
|
+
const view = new EditorView(body, {
|
|
1185
|
+
state: EditorState.create({ schema, doc }),
|
|
1186
|
+
editable: () => editable,
|
|
1187
|
+
attributes: () => viewAttributes(),
|
|
1188
|
+
dispatchTransaction,
|
|
1189
|
+
// Every paste and drop goes through the same sanitizer as setContent(), plus Office cleanup.
|
|
1190
|
+
clipboardParser,
|
|
1191
|
+
transformPastedHTML: (html) => {
|
|
1192
|
+
const result = cleanPastedHTML(html, urlPolicy);
|
|
1193
|
+
if (result.dropped) announce(i18n.t("pasteDropped"));
|
|
1194
|
+
return result.html;
|
|
1195
|
+
},
|
|
1196
|
+
// Pasting into an *empty* textblock replaces it, so the pasted block keeps its own
|
|
1197
|
+
// attributes (e.g. dir="rtl" from an Arabic document) instead of inheriting the empty one's.
|
|
1198
|
+
transformPasted: (slice, v) => {
|
|
1199
|
+
const { $from, empty } = v.state.selection;
|
|
1200
|
+
if (empty && $from.parent.isTextblock && $from.parent.content.size === 0 && slice.openStart > 0 && slice.content.firstChild?.isBlock) {
|
|
1201
|
+
return new Slice(slice.content, 0, slice.openEnd);
|
|
1202
|
+
}
|
|
1203
|
+
return slice;
|
|
1204
|
+
},
|
|
1205
|
+
handleDOMEvents: {
|
|
1206
|
+
focus: (_v, event) => {
|
|
1207
|
+
focused = true;
|
|
1208
|
+
emitter.emit("focus", event);
|
|
1209
|
+
opts.onFocus?.(editorRef, event);
|
|
1210
|
+
return false;
|
|
1211
|
+
},
|
|
1212
|
+
blur: (_v, event) => {
|
|
1213
|
+
focused = false;
|
|
1214
|
+
emitter.emit("blur", event);
|
|
1215
|
+
opts.onBlur?.(editorRef, event);
|
|
1216
|
+
syncTextarea();
|
|
1217
|
+
return false;
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
});
|
|
1221
|
+
function viewAttributes() {
|
|
1222
|
+
const attrs = {
|
|
1223
|
+
class: "owe-content",
|
|
1224
|
+
role: "textbox",
|
|
1225
|
+
"aria-multiline": "true",
|
|
1226
|
+
"aria-readonly": editable ? "false" : "true"
|
|
1227
|
+
};
|
|
1228
|
+
if (opts.ariaLabelledBy) attrs["aria-labelledby"] = opts.ariaLabelledBy;
|
|
1229
|
+
else attrs["aria-label"] = opts.ariaLabel ?? i18n.t("editorLabel");
|
|
1230
|
+
if (opts.ariaDescribedBy) attrs["aria-describedby"] = opts.ariaDescribedBy;
|
|
1231
|
+
const placeholder = placeholderText();
|
|
1232
|
+
if (placeholder) attrs["aria-placeholder"] = placeholder;
|
|
1233
|
+
if (opts.dir) attrs.dir = opts.dir;
|
|
1234
|
+
if (opts.contentLang) attrs.lang = opts.contentLang;
|
|
1235
|
+
return attrs;
|
|
1236
|
+
}
|
|
1237
|
+
function placeholderText() {
|
|
1238
|
+
if (opts.placeholder === false) return null;
|
|
1239
|
+
return opts.placeholder ?? i18n.t("placeholder");
|
|
1240
|
+
}
|
|
1241
|
+
function dispatchTransaction(tr) {
|
|
1242
|
+
if (destroyed) return;
|
|
1243
|
+
const before = view.state;
|
|
1244
|
+
const next = before.apply(tr);
|
|
1245
|
+
view.updateState(next);
|
|
1246
|
+
emitter.emit("transaction", tr);
|
|
1247
|
+
if (!before.selection.eq(next.selection)) {
|
|
1248
|
+
emitter.emit("selectionUpdate");
|
|
1249
|
+
opts.onSelectionUpdate?.(editorRef);
|
|
1250
|
+
}
|
|
1251
|
+
if (tr.docChanged && !tr.getMeta("owe-silent")) {
|
|
1252
|
+
emitter.emit("update");
|
|
1253
|
+
opts.onUpdate?.(editorRef);
|
|
1254
|
+
scheduleTextareaSync();
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
let syncTimer;
|
|
1258
|
+
function syncTextarea() {
|
|
1259
|
+
clearTimeout(syncTimer);
|
|
1260
|
+
if (textarea && !destroyed) textarea.value = editorRef.getHTML();
|
|
1261
|
+
}
|
|
1262
|
+
function scheduleTextareaSync() {
|
|
1263
|
+
if (!textarea) return;
|
|
1264
|
+
clearTimeout(syncTimer);
|
|
1265
|
+
syncTimer = setTimeout(syncTextarea, 150);
|
|
1266
|
+
}
|
|
1267
|
+
const form = textarea?.form ?? null;
|
|
1268
|
+
form?.addEventListener("submit", syncTextarea, true);
|
|
1269
|
+
form?.addEventListener("formdata", syncTextarea, true);
|
|
1270
|
+
function focusFromLabel(event) {
|
|
1271
|
+
event.preventDefault();
|
|
1272
|
+
editorRef.focus();
|
|
1273
|
+
}
|
|
1274
|
+
let tabEscape = false;
|
|
1275
|
+
const onKeyDownCapture = (event) => {
|
|
1276
|
+
if (event.key === "Escape" && view.dom.contains(event.target)) tabEscape = true;
|
|
1277
|
+
else if (event.key === "Tab" && tabEscape) {
|
|
1278
|
+
tabEscape = false;
|
|
1279
|
+
event.stopPropagation();
|
|
1280
|
+
} else if (event.key !== "Shift") tabEscape = false;
|
|
1281
|
+
};
|
|
1282
|
+
root.addEventListener("keydown", onKeyDownCapture, true);
|
|
1283
|
+
let announceFrame = 0;
|
|
1284
|
+
function announce(message, politeness = "polite") {
|
|
1285
|
+
const region = politeness === "assertive" ? assertive : polite;
|
|
1286
|
+
region.textContent = "";
|
|
1287
|
+
cancelAnimationFrame(announceFrame);
|
|
1288
|
+
announceFrame = requestAnimationFrame(() => {
|
|
1289
|
+
region.textContent = message;
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
function parseContent(content, initialLoad = false) {
|
|
1293
|
+
try {
|
|
1294
|
+
if (content === null || content === void 0 || content === "") {
|
|
1295
|
+
return schema.topNodeType.createAndFill();
|
|
1296
|
+
}
|
|
1297
|
+
if (typeof content === "string") return parseHTML(content);
|
|
1298
|
+
if (typeof HTMLElement !== "undefined" && content instanceof HTMLElement) {
|
|
1299
|
+
return parseHTML(elementHTML(content));
|
|
1300
|
+
}
|
|
1301
|
+
const node = schema.nodeFromJSON(content);
|
|
1302
|
+
node.check();
|
|
1303
|
+
return node;
|
|
1304
|
+
} catch (error) {
|
|
1305
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
1306
|
+
if (initialLoad) warn(`Invalid initial content: ${err.message}`);
|
|
1307
|
+
queueMicrotask(() => {
|
|
1308
|
+
emitter.emit("contentError", err);
|
|
1309
|
+
opts.onContentError?.(err);
|
|
1310
|
+
});
|
|
1311
|
+
return null;
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
const editorRef = buildApi();
|
|
1315
|
+
for (const ext of extensions) {
|
|
1316
|
+
const ctx = ctxFor(ext);
|
|
1317
|
+
Object.assign(factories, ext.config.commands?.(ctx));
|
|
1318
|
+
if (opts.inputRules !== false) rules.push(...ext.config.inputRules?.(ctx) ?? []);
|
|
1319
|
+
}
|
|
1320
|
+
if (rules.length) plugins.push(inputRules({ rules }));
|
|
1321
|
+
for (const ext of extensions) {
|
|
1322
|
+
const map = ext.config.keymap?.(ctxFor(ext));
|
|
1323
|
+
if (map && Object.keys(map).length) plugins.push(keymap(map));
|
|
1324
|
+
}
|
|
1325
|
+
for (const ext of extensions) plugins.push(...ext.config.plugins?.(ctxFor(ext)) ?? []);
|
|
1326
|
+
plugins.push(keymap(baseKeymap), placeholderPlugin());
|
|
1327
|
+
view.updateState(view.state.reconfigure({ plugins }));
|
|
1328
|
+
const manager = createCommandManager(factories, {
|
|
1329
|
+
view: () => view,
|
|
1330
|
+
focus: (position) => editorRef.focus(position)
|
|
1331
|
+
});
|
|
1332
|
+
function placeholderPlugin() {
|
|
1333
|
+
return new Plugin({
|
|
1334
|
+
key: placeholderKey,
|
|
1335
|
+
props: {
|
|
1336
|
+
decorations(state) {
|
|
1337
|
+
const text = placeholderText();
|
|
1338
|
+
const first = state.doc.firstChild;
|
|
1339
|
+
if (!text || state.doc.childCount !== 1 || !first?.isTextblock || first.content.size) return null;
|
|
1340
|
+
return DecorationSet.create(state.doc, [
|
|
1341
|
+
Decoration.node(0, first.nodeSize, { class: "owe-placeholder", "data-placeholder": text })
|
|
1342
|
+
]);
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
function buildApi() {
|
|
1348
|
+
const api = {
|
|
1349
|
+
get root() {
|
|
1350
|
+
return root;
|
|
1351
|
+
},
|
|
1352
|
+
get view() {
|
|
1353
|
+
return view;
|
|
1354
|
+
},
|
|
1355
|
+
schema,
|
|
1356
|
+
get state() {
|
|
1357
|
+
return view.state;
|
|
1358
|
+
},
|
|
1359
|
+
extensions,
|
|
1360
|
+
i18n,
|
|
1361
|
+
get isDestroyed() {
|
|
1362
|
+
return destroyed;
|
|
1363
|
+
},
|
|
1364
|
+
get isEditable() {
|
|
1365
|
+
return editable;
|
|
1366
|
+
},
|
|
1367
|
+
get isEmpty() {
|
|
1368
|
+
const d = view.state.doc;
|
|
1369
|
+
return d.childCount === 1 && !!d.firstChild?.isTextblock && d.firstChild.content.size === 0;
|
|
1370
|
+
},
|
|
1371
|
+
get isFocused() {
|
|
1372
|
+
return focused;
|
|
1373
|
+
},
|
|
1374
|
+
get commands() {
|
|
1375
|
+
return manager.commands;
|
|
1376
|
+
},
|
|
1377
|
+
can: () => manager.can,
|
|
1378
|
+
chain: () => manager.chain(),
|
|
1379
|
+
getHTML: () => docToHTML(view.state.doc, urlPolicy),
|
|
1380
|
+
getJSON: () => view.state.doc.toJSON(),
|
|
1381
|
+
getText: ({ blockSeparator = "\n\n" } = {}) => view.state.doc.textBetween(0, view.state.doc.content.size, blockSeparator),
|
|
1382
|
+
setContent(content, { emitUpdate = false, addToHistory = false } = {}) {
|
|
1383
|
+
const node = parseContent(content);
|
|
1384
|
+
if (!node) return false;
|
|
1385
|
+
const tr = view.state.tr.replaceWith(0, view.state.doc.content.size, node.content);
|
|
1386
|
+
tr.setSelection(Selection.atStart(tr.doc));
|
|
1387
|
+
if (!addToHistory) tr.setMeta("addToHistory", false);
|
|
1388
|
+
if (!emitUpdate) tr.setMeta("owe-silent", true);
|
|
1389
|
+
view.dispatch(tr);
|
|
1390
|
+
if (!emitUpdate) scheduleTextareaSync();
|
|
1391
|
+
return true;
|
|
1392
|
+
},
|
|
1393
|
+
clearContent(o) {
|
|
1394
|
+
api.setContent(null, o);
|
|
1395
|
+
},
|
|
1396
|
+
setEditable(value) {
|
|
1397
|
+
api.setOptions({ editable: value });
|
|
1398
|
+
},
|
|
1399
|
+
focus(position) {
|
|
1400
|
+
if (destroyed) return;
|
|
1401
|
+
if (position !== void 0) {
|
|
1402
|
+
const { doc: d } = view.state;
|
|
1403
|
+
const sel = position === "start" ? Selection.atStart(d) : position === "end" ? Selection.atEnd(d) : TextSelection.near(d.resolve(Math.max(0, Math.min(position, d.content.size))));
|
|
1404
|
+
view.dispatch(view.state.tr.setSelection(sel).scrollIntoView());
|
|
1405
|
+
}
|
|
1406
|
+
view.focus();
|
|
1407
|
+
},
|
|
1408
|
+
blur() {
|
|
1409
|
+
view.dom.blur();
|
|
1410
|
+
},
|
|
1411
|
+
isActive: (name, attrs) => isActive(view.state, schema, name, attrs),
|
|
1412
|
+
getAttributes: (name) => getAttributes(view.state, schema, name),
|
|
1413
|
+
t: i18n.t,
|
|
1414
|
+
announce,
|
|
1415
|
+
on: (event, listener) => emitter.on(event, listener),
|
|
1416
|
+
off: (event, listener) => emitter.off(event, listener),
|
|
1417
|
+
subscribe(listener) {
|
|
1418
|
+
const offTr = emitter.on("transaction", listener);
|
|
1419
|
+
const offOpts = emitter.on("options", listener);
|
|
1420
|
+
return () => {
|
|
1421
|
+
offTr();
|
|
1422
|
+
offOpts();
|
|
1423
|
+
};
|
|
1424
|
+
},
|
|
1425
|
+
setOptions(partial) {
|
|
1426
|
+
if (destroyed) return;
|
|
1427
|
+
opts = { ...opts, ...partial };
|
|
1428
|
+
if (partial.editable !== void 0) editable = partial.editable;
|
|
1429
|
+
view.setProps({});
|
|
1430
|
+
view.dispatch(view.state.tr.setMeta(placeholderKey, true));
|
|
1431
|
+
emitter.emit("options");
|
|
1432
|
+
},
|
|
1433
|
+
destroy() {
|
|
1434
|
+
if (destroyed) return;
|
|
1435
|
+
syncTextarea();
|
|
1436
|
+
for (const ext of extensions) ext.config.onDestroy?.(ctxFor(ext));
|
|
1437
|
+
destroyed = true;
|
|
1438
|
+
clearTimeout(syncTimer);
|
|
1439
|
+
cancelAnimationFrame(announceFrame);
|
|
1440
|
+
root.removeEventListener("keydown", onKeyDownCapture, true);
|
|
1441
|
+
form?.removeEventListener("submit", syncTextarea, true);
|
|
1442
|
+
form?.removeEventListener("formdata", syncTextarea, true);
|
|
1443
|
+
if (textarea) {
|
|
1444
|
+
textarea.labels?.forEach((l) => l.removeEventListener("click", focusFromLabel));
|
|
1445
|
+
textarea.hidden = false;
|
|
1446
|
+
}
|
|
1447
|
+
view.destroy();
|
|
1448
|
+
root.remove();
|
|
1449
|
+
emitter.emit("destroy");
|
|
1450
|
+
opts.onDestroy?.();
|
|
1451
|
+
emitter.clear();
|
|
1452
|
+
}
|
|
1453
|
+
};
|
|
1454
|
+
return api;
|
|
1455
|
+
}
|
|
1456
|
+
for (const ext of extensions) ext.config.onCreate?.(ctxFor(ext));
|
|
1457
|
+
emitter.emit("create");
|
|
1458
|
+
opts.onCreate?.(editorRef);
|
|
1459
|
+
if (opts.autofocus) {
|
|
1460
|
+
editorRef.focus(opts.autofocus === true ? void 0 : opts.autofocus);
|
|
1461
|
+
}
|
|
1462
|
+
return editorRef;
|
|
1463
|
+
}
|
|
1464
|
+
function elementHTML(el) {
|
|
1465
|
+
return el.innerHTML;
|
|
1466
|
+
}
|
|
1467
|
+
function attrsMatch(actual, wanted) {
|
|
1468
|
+
if (!wanted) return true;
|
|
1469
|
+
return Object.entries(wanted).every(([k, v]) => actual[k] === v);
|
|
1470
|
+
}
|
|
1471
|
+
function isActive(state, schema, name, attrs) {
|
|
1472
|
+
const markType = schema.marks[name];
|
|
1473
|
+
if (markType) {
|
|
1474
|
+
const { from, to, empty, $from: $from2 } = state.selection;
|
|
1475
|
+
if (empty) {
|
|
1476
|
+
return (state.storedMarks ?? $from2.marks()).some(
|
|
1477
|
+
(m) => m.type === markType && attrsMatch(m.attrs, attrs)
|
|
1478
|
+
);
|
|
1479
|
+
}
|
|
1480
|
+
let sawText = false;
|
|
1481
|
+
let all = true;
|
|
1482
|
+
state.doc.nodesBetween(from, to, (node) => {
|
|
1483
|
+
if (!all) return false;
|
|
1484
|
+
if (node.isText) {
|
|
1485
|
+
sawText = true;
|
|
1486
|
+
if (!node.marks.some((m) => m.type === markType && attrsMatch(m.attrs, attrs))) all = false;
|
|
1487
|
+
}
|
|
1488
|
+
return true;
|
|
1489
|
+
});
|
|
1490
|
+
return sawText && all;
|
|
1491
|
+
}
|
|
1492
|
+
const nodeType = schema.nodes[name];
|
|
1493
|
+
if (!nodeType) return false;
|
|
1494
|
+
const sel = state.selection;
|
|
1495
|
+
if (sel instanceof NodeSelection && sel.node.type === nodeType) return attrsMatch(sel.node.attrs, attrs);
|
|
1496
|
+
const $from = sel.$from;
|
|
1497
|
+
for (let d = $from.depth; d >= 0; d--) {
|
|
1498
|
+
const node = $from.node(d);
|
|
1499
|
+
if (node.type === nodeType && attrsMatch(node.attrs, attrs)) return true;
|
|
1500
|
+
}
|
|
1501
|
+
return false;
|
|
1502
|
+
}
|
|
1503
|
+
function getAttributes(state, schema, name) {
|
|
1504
|
+
const markType = schema.marks[name];
|
|
1505
|
+
if (markType) {
|
|
1506
|
+
const { from, to, empty, $from } = state.selection;
|
|
1507
|
+
let mark;
|
|
1508
|
+
if (empty) mark = (state.storedMarks ?? $from.marks()).find((m) => m.type === markType);
|
|
1509
|
+
else {
|
|
1510
|
+
state.doc.nodesBetween(from, to, (node) => {
|
|
1511
|
+
mark ?? (mark = node.marks.find((m) => m.type === markType));
|
|
1512
|
+
return !mark;
|
|
1513
|
+
});
|
|
1514
|
+
}
|
|
1515
|
+
return mark ? { ...mark.attrs } : {};
|
|
1516
|
+
}
|
|
1517
|
+
const nodeType = schema.nodes[name];
|
|
1518
|
+
const sel = state.selection;
|
|
1519
|
+
if (sel instanceof NodeSelection && sel.node.type === nodeType) return { ...sel.node.attrs };
|
|
1520
|
+
for (let d = sel.$from.depth; d >= 0; d--) {
|
|
1521
|
+
const node = sel.$from.node(d);
|
|
1522
|
+
if (node.type === nodeType) return { ...node.attrs };
|
|
1523
|
+
}
|
|
1524
|
+
return {};
|
|
1525
|
+
}
|
|
1526
|
+
var unsetAllMarks = (state, dispatch) => {
|
|
1527
|
+
const { from, to, empty } = state.selection;
|
|
1528
|
+
if (empty) {
|
|
1529
|
+
if (dispatch) dispatch(state.tr.setStoredMarks([]));
|
|
1530
|
+
return true;
|
|
1531
|
+
}
|
|
1532
|
+
if (dispatch) dispatch(state.tr.removeMark(from, to, null));
|
|
1533
|
+
return true;
|
|
1534
|
+
};
|
|
1535
|
+
var clearNodes = (state, dispatch) => {
|
|
1536
|
+
const paragraph = state.schema.nodes.paragraph;
|
|
1537
|
+
if (!paragraph) return false;
|
|
1538
|
+
const { from, to } = state.selection;
|
|
1539
|
+
const tr = state.tr;
|
|
1540
|
+
state.doc.nodesBetween(from, to, (node, pos) => {
|
|
1541
|
+
if (!node.isTextblock) return true;
|
|
1542
|
+
let range = tr.doc.resolve(tr.mapping.map(pos)).blockRange(tr.doc.resolve(tr.mapping.map(pos + node.nodeSize)));
|
|
1543
|
+
let target = range ? liftTarget(range) : null;
|
|
1544
|
+
while (range && target !== null) {
|
|
1545
|
+
tr.lift(range, target);
|
|
1546
|
+
range = tr.doc.resolve(tr.mapping.map(pos)).blockRange(tr.doc.resolve(tr.mapping.map(pos + node.nodeSize)));
|
|
1547
|
+
target = range ? liftTarget(range) : null;
|
|
1548
|
+
}
|
|
1549
|
+
if (node.type !== paragraph) tr.setNodeMarkup(tr.mapping.map(pos), paragraph);
|
|
1550
|
+
return false;
|
|
1551
|
+
});
|
|
1552
|
+
if (!tr.docChanged) return false;
|
|
1553
|
+
if (dispatch) dispatch(tr.scrollIntoView());
|
|
1554
|
+
return true;
|
|
1555
|
+
};
|
|
1556
|
+
var CoreExtension = defineExtension({
|
|
1557
|
+
name: "core",
|
|
1558
|
+
priority: 1e3,
|
|
1559
|
+
commands: ({ editor }) => ({
|
|
1560
|
+
focus: (position) => () => {
|
|
1561
|
+
editor.focus(position);
|
|
1562
|
+
return true;
|
|
1563
|
+
},
|
|
1564
|
+
blur: () => () => {
|
|
1565
|
+
editor.blur();
|
|
1566
|
+
return true;
|
|
1567
|
+
},
|
|
1568
|
+
selectAll: () => (state, dispatch) => {
|
|
1569
|
+
if (dispatch) dispatch(state.tr.setSelection(new AllSelection(state.doc)));
|
|
1570
|
+
return true;
|
|
1571
|
+
},
|
|
1572
|
+
unsetAllMarks: () => unsetAllMarks,
|
|
1573
|
+
clearNodes: () => clearNodes
|
|
1574
|
+
})
|
|
1575
|
+
});
|
|
1576
|
+
function markInputRule(regexp, markType, attrs) {
|
|
1577
|
+
return new InputRule(regexp, (state, match, start, end) => {
|
|
1578
|
+
const full = match[1];
|
|
1579
|
+
const text = match[2];
|
|
1580
|
+
if (!full || !text) return null;
|
|
1581
|
+
const fullStart = start + match[0].length - full.length;
|
|
1582
|
+
const textStart = fullStart + full.indexOf(text);
|
|
1583
|
+
const textEnd = textStart + text.length;
|
|
1584
|
+
const $from = state.doc.resolve(fullStart);
|
|
1585
|
+
if (!$from.parent.type.allowsMarkType(markType)) return null;
|
|
1586
|
+
let blocked = false;
|
|
1587
|
+
state.doc.nodesBetween(fullStart, end, (node) => {
|
|
1588
|
+
if (node.marks.some((m) => m.type !== markType && m.type.excludes(markType))) blocked = true;
|
|
1589
|
+
});
|
|
1590
|
+
if (blocked) return null;
|
|
1591
|
+
const tr = state.tr;
|
|
1592
|
+
if (textEnd < end) tr.delete(textEnd, end);
|
|
1593
|
+
if (textStart > fullStart) tr.delete(fullStart, textStart);
|
|
1594
|
+
tr.addMark(fullStart, fullStart + text.length, markType.create(attrs));
|
|
1595
|
+
tr.removeStoredMark(markType);
|
|
1596
|
+
return tr;
|
|
1597
|
+
});
|
|
1598
|
+
}
|
|
1599
|
+
function sequence(...commands) {
|
|
1600
|
+
return (state, dispatch, view) => {
|
|
1601
|
+
let current = state;
|
|
1602
|
+
const trs = [];
|
|
1603
|
+
for (const command of commands) {
|
|
1604
|
+
const ok = command(current, (tr) => {
|
|
1605
|
+
trs.push(tr);
|
|
1606
|
+
current = current.apply(tr);
|
|
1607
|
+
}, view);
|
|
1608
|
+
if (!ok) return false;
|
|
1609
|
+
}
|
|
1610
|
+
if (dispatch && trs.length) {
|
|
1611
|
+
const tr = state.tr;
|
|
1612
|
+
for (const t of trs) for (const step of t.steps) tr.step(step);
|
|
1613
|
+
const last = trs[trs.length - 1];
|
|
1614
|
+
if (last.selectionSet) {
|
|
1615
|
+
tr.setSelection(last.selection.map(tr.doc, tr.mapping.slice(tr.steps.length)));
|
|
1616
|
+
}
|
|
1617
|
+
dispatch(tr.scrollIntoView());
|
|
1618
|
+
}
|
|
1619
|
+
return true;
|
|
1620
|
+
};
|
|
1621
|
+
}
|
|
1622
|
+
function isNodeActive(state, type, attrs) {
|
|
1623
|
+
const { $from, to } = state.selection;
|
|
1624
|
+
for (let d = $from.depth; d >= 0; d--) {
|
|
1625
|
+
const node = $from.node(d);
|
|
1626
|
+
if (node.type !== type) continue;
|
|
1627
|
+
if (!attrs || Object.entries(attrs).every(([k, v]) => node.attrs[k] === v)) {
|
|
1628
|
+
return to <= $from.end(d);
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
return false;
|
|
1632
|
+
}
|
|
1633
|
+
function toggleBlockType(type, fallback, attrs, match = attrs) {
|
|
1634
|
+
return (state, dispatch, view) => isNodeActive(state, type, match) ? setBlockType(fallback)(state, dispatch, view) : setBlockType(type, attrs)(state, dispatch, view);
|
|
1635
|
+
}
|
|
1636
|
+
function toggleWrap(type) {
|
|
1637
|
+
return (state, dispatch, view) => isNodeActive(state, type) ? lift(state, dispatch, view) : wrapIn(type)(state, dispatch, view);
|
|
1638
|
+
}
|
|
1639
|
+
function toggleMarkCommand(type, attrs) {
|
|
1640
|
+
return toggleMark(type, attrs, { removeWhenPresent: false, enterInlineAtoms: true });
|
|
1641
|
+
}
|
|
1642
|
+
function findParent($pos, predicate) {
|
|
1643
|
+
for (let d = $pos.depth; d > 0; d--) {
|
|
1644
|
+
const node = $pos.node(d);
|
|
1645
|
+
if (predicate(node.type)) return { pos: $pos.before(d), depth: d, node };
|
|
1646
|
+
}
|
|
1647
|
+
return null;
|
|
1648
|
+
}
|
|
1649
|
+
var isListType = (type) => type.spec.group?.split(" ").includes("list") ?? false;
|
|
1650
|
+
var liftOutOfLists = (state, dispatch) => {
|
|
1651
|
+
let current = state;
|
|
1652
|
+
const trs = [];
|
|
1653
|
+
for (let guard = 0; guard < 20; guard++) {
|
|
1654
|
+
const list = findParent(current.selection.$from, isListType);
|
|
1655
|
+
if (!list) break;
|
|
1656
|
+
const itemType = list.node.firstChild?.type;
|
|
1657
|
+
if (!itemType) break;
|
|
1658
|
+
const ok = liftListItem(itemType)(current, (tr) => {
|
|
1659
|
+
trs.push(tr);
|
|
1660
|
+
current = current.apply(tr);
|
|
1661
|
+
});
|
|
1662
|
+
if (!ok) break;
|
|
1663
|
+
}
|
|
1664
|
+
if (!trs.length) return false;
|
|
1665
|
+
if (dispatch) {
|
|
1666
|
+
const tr = state.tr;
|
|
1667
|
+
for (const t of trs) for (const step of t.steps) tr.step(step);
|
|
1668
|
+
dispatch(tr);
|
|
1669
|
+
}
|
|
1670
|
+
return true;
|
|
1671
|
+
};
|
|
1672
|
+
function toggleList(listType, itemType) {
|
|
1673
|
+
return (state, dispatch, view) => {
|
|
1674
|
+
const list = findParent(state.selection.$from, isListType);
|
|
1675
|
+
if (list) {
|
|
1676
|
+
if (list.node.type === listType) return liftListItem(itemType)(state, dispatch, view);
|
|
1677
|
+
if (list.node.firstChild?.type === itemType) {
|
|
1678
|
+
if (dispatch) dispatch(state.tr.setNodeMarkup(list.pos, listType, list.node.attrs));
|
|
1679
|
+
return true;
|
|
1680
|
+
}
|
|
1681
|
+
return sequence(liftOutOfLists, wrapInList(listType))(state, dispatch, view);
|
|
1682
|
+
}
|
|
1683
|
+
return wrapInList(listType)(state, dispatch, view);
|
|
1684
|
+
};
|
|
1685
|
+
}
|
|
1686
|
+
function markRange($pos, type) {
|
|
1687
|
+
const parent = $pos.parent;
|
|
1688
|
+
const start = parent.childAfter($pos.parentOffset);
|
|
1689
|
+
const child = start.node ?? parent.childBefore($pos.parentOffset).node;
|
|
1690
|
+
const mark = child?.marks.find((m) => m.type === type);
|
|
1691
|
+
if (!mark) return null;
|
|
1692
|
+
let index = $pos.index();
|
|
1693
|
+
if (!parent.maybeChild(index)?.marks.some((m) => m.eq(mark))) index--;
|
|
1694
|
+
if (index < 0) return null;
|
|
1695
|
+
let startIndex = index;
|
|
1696
|
+
let endIndex = index + 1;
|
|
1697
|
+
while (startIndex > 0 && mark.isInSet(parent.child(startIndex - 1).marks)) startIndex--;
|
|
1698
|
+
while (endIndex < parent.childCount && mark.isInSet(parent.child(endIndex).marks)) endIndex++;
|
|
1699
|
+
let from = $pos.start();
|
|
1700
|
+
for (let i = 0; i < startIndex; i++) from += parent.child(i).nodeSize;
|
|
1701
|
+
let to = from;
|
|
1702
|
+
for (let i = startIndex; i < endIndex; i++) to += parent.child(i).nodeSize;
|
|
1703
|
+
return { from, to, mark };
|
|
1704
|
+
}
|
|
1705
|
+
function setBlockAttrs(types, attrs) {
|
|
1706
|
+
return (state, dispatch) => {
|
|
1707
|
+
const { from, to } = state.selection;
|
|
1708
|
+
const tr = state.tr;
|
|
1709
|
+
state.doc.nodesBetween(from, to, (node, pos) => {
|
|
1710
|
+
if (types.includes(node.type.name) && node.isTextblock) {
|
|
1711
|
+
tr.setNodeMarkup(pos, void 0, { ...node.attrs, ...attrs });
|
|
1712
|
+
}
|
|
1713
|
+
return true;
|
|
1714
|
+
});
|
|
1715
|
+
if (!tr.docChanged) return false;
|
|
1716
|
+
if (dispatch) dispatch(tr);
|
|
1717
|
+
return true;
|
|
1718
|
+
};
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
// src/extensions/attributes.ts
|
|
1722
|
+
var ALIGNMENTS = ["start", "center", "end", "justify"];
|
|
1723
|
+
var TextAlign = defineExtension({
|
|
1724
|
+
name: "textAlign",
|
|
1725
|
+
defaultOptions: { types: ["paragraph", "heading"], output: "style" },
|
|
1726
|
+
globalAttributes: ({ types, output }) => [
|
|
1727
|
+
{
|
|
1728
|
+
types,
|
|
1729
|
+
attributes: {
|
|
1730
|
+
textAlign: {
|
|
1731
|
+
default: null,
|
|
1732
|
+
parseHTML: (el) => {
|
|
1733
|
+
const raw = (el.style.textAlign || el.getAttribute("align") || "").toLowerCase();
|
|
1734
|
+
if (raw === "left" || raw === "right") {
|
|
1735
|
+
const rtl = el.closest("[dir]")?.getAttribute("dir") === "rtl";
|
|
1736
|
+
return raw === "left" !== rtl ? null : "end";
|
|
1737
|
+
}
|
|
1738
|
+
return ALIGNMENTS.includes(raw) && raw !== "start" ? raw : null;
|
|
1739
|
+
},
|
|
1740
|
+
renderHTML: (value) => value ? output === "class" ? { class: `owe-align-${value}` } : { style: `text-align: ${value}` } : null,
|
|
1741
|
+
renderDOM: (value) => value ? { class: `owe-align-${String(value)}` } : null
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
],
|
|
1746
|
+
commands: ({ options }) => ({
|
|
1747
|
+
setTextAlign: (align) => ALIGNMENTS.includes(align) ? setBlockAttrs(options.types, { textAlign: align === "start" ? null : align }) : () => false,
|
|
1748
|
+
unsetTextAlign: () => setBlockAttrs(options.types, { textAlign: null })
|
|
1749
|
+
}),
|
|
1750
|
+
keymap: ({ options }) => ({
|
|
1751
|
+
"Mod-Shift-l": setBlockAttrs(options.types, { textAlign: null }),
|
|
1752
|
+
"Mod-Shift-e": setBlockAttrs(options.types, { textAlign: "center" }),
|
|
1753
|
+
"Mod-Shift-r": setBlockAttrs(options.types, { textAlign: "end" }),
|
|
1754
|
+
"Mod-Shift-j": setBlockAttrs(options.types, { textAlign: "justify" })
|
|
1755
|
+
})
|
|
1756
|
+
});
|
|
1757
|
+
var TextDirection = defineExtension({
|
|
1758
|
+
name: "textDirection",
|
|
1759
|
+
defaultOptions: {
|
|
1760
|
+
types: ["paragraph", "heading", "blockquote", "bulletList", "orderedList", "taskList", "table"],
|
|
1761
|
+
autoDetect: true
|
|
1762
|
+
},
|
|
1763
|
+
globalAttributes: ({ types, autoDetect }) => {
|
|
1764
|
+
const textblocks = types.filter((t) => t === "paragraph" || t === "heading");
|
|
1765
|
+
const containers = types.filter((t) => !textblocks.includes(t));
|
|
1766
|
+
const attr = (auto) => ({
|
|
1767
|
+
dir: {
|
|
1768
|
+
default: null,
|
|
1769
|
+
parseHTML: (el) => {
|
|
1770
|
+
const dir = el.getAttribute("dir")?.toLowerCase();
|
|
1771
|
+
return dir === "ltr" || dir === "rtl" || dir === "auto" ? dir : null;
|
|
1772
|
+
},
|
|
1773
|
+
// Empty blocks inherit the editor direction so the caret starts on the correct side.
|
|
1774
|
+
renderHTML: (value, node) => value ? { dir: String(value) } : auto && "content" in node && node.content.size > 0 ? { dir: "auto" } : null
|
|
1775
|
+
}
|
|
1776
|
+
});
|
|
1777
|
+
return [
|
|
1778
|
+
{ types: textblocks, attributes: attr(autoDetect) },
|
|
1779
|
+
{ types: containers, attributes: attr(false) }
|
|
1780
|
+
];
|
|
1781
|
+
},
|
|
1782
|
+
commands: ({ options }) => ({
|
|
1783
|
+
setTextDirection: (dir) => dir === "ltr" || dir === "rtl" || dir === "auto" ? setBlockAttrs(options.types, { dir }) : () => false,
|
|
1784
|
+
unsetTextDirection: () => setBlockAttrs(options.types, { dir: null })
|
|
1785
|
+
})
|
|
1786
|
+
});
|
|
1787
|
+
var History = defineExtension({
|
|
1788
|
+
name: "history",
|
|
1789
|
+
defaultOptions: { depth: 200, newGroupDelay: 500 },
|
|
1790
|
+
commands: () => ({ undo: () => undo, redo: () => redo }),
|
|
1791
|
+
keymap: () => ({ "Mod-z": undo, "Shift-Mod-z": redo, "Mod-y": redo }),
|
|
1792
|
+
plugins: ({ options }) => [history(options)]
|
|
1793
|
+
});
|
|
1794
|
+
var Dropcursor = defineExtension({
|
|
1795
|
+
name: "dropcursor",
|
|
1796
|
+
plugins: () => [dropCursor({ class: "owe-dropcursor", width: 2 })]
|
|
1797
|
+
});
|
|
1798
|
+
var Gapcursor = defineExtension({
|
|
1799
|
+
name: "gapcursor",
|
|
1800
|
+
plugins: () => [gapCursor()]
|
|
1801
|
+
});
|
|
1802
|
+
var dimension = (value) => {
|
|
1803
|
+
const n = Number.parseInt(value ?? "", 10);
|
|
1804
|
+
return Number.isFinite(n) && n > 0 && n < 2e4 ? n : null;
|
|
1805
|
+
};
|
|
1806
|
+
var Image = defineExtension({
|
|
1807
|
+
name: "image",
|
|
1808
|
+
defaultOptions: { upload: null },
|
|
1809
|
+
nodes: (_options, { urlPolicy }) => ({
|
|
1810
|
+
image: {
|
|
1811
|
+
group: "block",
|
|
1812
|
+
atom: true,
|
|
1813
|
+
draggable: true,
|
|
1814
|
+
attrs: {
|
|
1815
|
+
src: { default: null },
|
|
1816
|
+
// null = not provided (the checker flags it); "" = decorative (ATAG B.2.3).
|
|
1817
|
+
alt: { default: null },
|
|
1818
|
+
title: { default: null },
|
|
1819
|
+
caption: { default: null },
|
|
1820
|
+
width: { default: null },
|
|
1821
|
+
height: { default: null }
|
|
1822
|
+
},
|
|
1823
|
+
parseDOM: [
|
|
1824
|
+
{
|
|
1825
|
+
tag: "figure",
|
|
1826
|
+
getAttrs: (el) => {
|
|
1827
|
+
const img = el.querySelector("img");
|
|
1828
|
+
if (!img) return false;
|
|
1829
|
+
const attrs = readImg(img, urlPolicy);
|
|
1830
|
+
if (!attrs) return false;
|
|
1831
|
+
const caption = el.querySelector("figcaption")?.textContent?.trim();
|
|
1832
|
+
return { ...attrs, caption: caption || null };
|
|
1833
|
+
}
|
|
1834
|
+
},
|
|
1835
|
+
{ tag: "img[src]", getAttrs: (el) => readImg(el, urlPolicy) ?? false }
|
|
1836
|
+
],
|
|
1837
|
+
toDOM: (node) => {
|
|
1838
|
+
const a = node.attrs;
|
|
1839
|
+
const img = [
|
|
1840
|
+
"img",
|
|
1841
|
+
{
|
|
1842
|
+
src: sanitizeUrl(a.src, urlPolicy, "image"),
|
|
1843
|
+
alt: a.alt,
|
|
1844
|
+
title: a.title ?? null,
|
|
1845
|
+
width: a.width ? String(a.width) : null,
|
|
1846
|
+
height: a.height ? String(a.height) : null,
|
|
1847
|
+
loading: "lazy",
|
|
1848
|
+
decoding: "async"
|
|
1849
|
+
}
|
|
1850
|
+
];
|
|
1851
|
+
return a.caption ? ["figure", img, ["figcaption", a.caption]] : img;
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
}),
|
|
1855
|
+
commands: ({ schema }) => {
|
|
1856
|
+
const type = schema.nodes.image;
|
|
1857
|
+
const policy = schema.cached.urlPolicy ?? {};
|
|
1858
|
+
const setImage = (attrs) => (state, dispatch) => {
|
|
1859
|
+
const src = sanitizeUrl(attrs.src, policy, "image");
|
|
1860
|
+
if (!src) return false;
|
|
1861
|
+
const node = type.create({ ...normalize(attrs), src });
|
|
1862
|
+
if (dispatch) dispatch(state.tr.replaceSelectionWith(node).scrollIntoView());
|
|
1863
|
+
return true;
|
|
1864
|
+
};
|
|
1865
|
+
const updateImage = (attrs) => (state, dispatch) => {
|
|
1866
|
+
const sel = state.selection;
|
|
1867
|
+
if (!(sel instanceof NodeSelection) || sel.node.type !== type) return false;
|
|
1868
|
+
const next = { ...sel.node.attrs, ...normalize(attrs) };
|
|
1869
|
+
if (attrs.src !== void 0) {
|
|
1870
|
+
const src = sanitizeUrl(attrs.src, policy, "image");
|
|
1871
|
+
if (!src) return false;
|
|
1872
|
+
next.src = src;
|
|
1873
|
+
}
|
|
1874
|
+
if (dispatch) {
|
|
1875
|
+
const tr = state.tr.setNodeMarkup(sel.from, void 0, next);
|
|
1876
|
+
tr.setSelection(NodeSelection.create(tr.doc, sel.from));
|
|
1877
|
+
dispatch(tr);
|
|
1878
|
+
}
|
|
1879
|
+
return true;
|
|
1880
|
+
};
|
|
1881
|
+
return { setImage, updateImage };
|
|
1882
|
+
}
|
|
1883
|
+
});
|
|
1884
|
+
function readImg(img, policy) {
|
|
1885
|
+
const src = sanitizeUrl(img.getAttribute("src"), policy, "image");
|
|
1886
|
+
if (!src) return null;
|
|
1887
|
+
return {
|
|
1888
|
+
src,
|
|
1889
|
+
alt: img.getAttribute("alt"),
|
|
1890
|
+
title: img.getAttribute("title"),
|
|
1891
|
+
width: dimension(img.getAttribute("width")),
|
|
1892
|
+
height: dimension(img.getAttribute("height"))
|
|
1893
|
+
};
|
|
1894
|
+
}
|
|
1895
|
+
function normalize(attrs) {
|
|
1896
|
+
const out = {};
|
|
1897
|
+
if (attrs.alt !== void 0) out.alt = attrs.alt === null ? null : attrs.alt.trim();
|
|
1898
|
+
if (attrs.title !== void 0) out.title = attrs.title?.trim() || null;
|
|
1899
|
+
if (attrs.caption !== void 0) out.caption = attrs.caption?.trim() || null;
|
|
1900
|
+
if (attrs.width !== void 0) out.width = attrs.width ? Math.round(attrs.width) : null;
|
|
1901
|
+
if (attrs.height !== void 0) out.height = attrs.height ? Math.round(attrs.height) : null;
|
|
1902
|
+
return out;
|
|
1903
|
+
}
|
|
1904
|
+
var itemTypes = ["listItem", "taskItem"];
|
|
1905
|
+
var forItem = (make) => (state, dispatch, view) => {
|
|
1906
|
+
const item = findParent(state.selection.$from, (t) => itemTypes.includes(t.name));
|
|
1907
|
+
return item ? make(item.node.type)(state, dispatch, view) : false;
|
|
1908
|
+
};
|
|
1909
|
+
var liftAtStart = (state, dispatch, view) => {
|
|
1910
|
+
const { $from, empty } = state.selection;
|
|
1911
|
+
if (!empty || $from.parentOffset > 0) return false;
|
|
1912
|
+
const item = findParent($from, (t) => itemTypes.includes(t.name));
|
|
1913
|
+
if (!item || $from.index(item.depth) !== 0) return false;
|
|
1914
|
+
return liftListItem(item.node.type)(state, dispatch, view);
|
|
1915
|
+
};
|
|
1916
|
+
var ListItem = defineExtension({
|
|
1917
|
+
name: "listItem",
|
|
1918
|
+
nodes: () => ({
|
|
1919
|
+
listItem: {
|
|
1920
|
+
content: "paragraph block*",
|
|
1921
|
+
defining: true,
|
|
1922
|
+
parseDOM: [{ tag: "li", priority: 40 }],
|
|
1923
|
+
toDOM: () => ["li", 0]
|
|
1924
|
+
}
|
|
1925
|
+
}),
|
|
1926
|
+
commands: () => ({
|
|
1927
|
+
sinkListItem: () => forItem(sinkListItem),
|
|
1928
|
+
liftListItem: () => forItem(liftListItem)
|
|
1929
|
+
}),
|
|
1930
|
+
keymap: () => ({
|
|
1931
|
+
Enter: forItem(splitListItem),
|
|
1932
|
+
// Tab only acts inside lists and fails at the first item, so focus can always leave the editor.
|
|
1933
|
+
Tab: forItem(sinkListItem),
|
|
1934
|
+
"Shift-Tab": forItem(liftListItem),
|
|
1935
|
+
Backspace: liftAtStart
|
|
1936
|
+
})
|
|
1937
|
+
});
|
|
1938
|
+
var BulletList = defineExtension({
|
|
1939
|
+
name: "bulletList",
|
|
1940
|
+
extensions: () => [ListItem],
|
|
1941
|
+
nodes: () => ({
|
|
1942
|
+
bulletList: {
|
|
1943
|
+
content: "listItem+",
|
|
1944
|
+
group: "block list",
|
|
1945
|
+
parseDOM: [{ tag: "ul", getAttrs: (el) => el.dataset.type === "taskList" ? false : null }],
|
|
1946
|
+
toDOM: () => ["ul", 0]
|
|
1947
|
+
}
|
|
1948
|
+
}),
|
|
1949
|
+
commands: ({ schema }) => ({
|
|
1950
|
+
toggleBulletList: () => toggleList(schema.nodes.bulletList, schema.nodes.listItem)
|
|
1951
|
+
}),
|
|
1952
|
+
keymap: ({ schema }) => ({ "Mod-Shift-8": toggleList(schema.nodes.bulletList, schema.nodes.listItem) }),
|
|
1953
|
+
inputRules: ({ schema }) => [wrappingInputRule(/^\s*([-+*])\s$/, schema.nodes.bulletList)]
|
|
1954
|
+
});
|
|
1955
|
+
var OrderedList = defineExtension({
|
|
1956
|
+
name: "orderedList",
|
|
1957
|
+
extensions: () => [ListItem],
|
|
1958
|
+
nodes: () => ({
|
|
1959
|
+
orderedList: {
|
|
1960
|
+
attrs: { start: { default: 1 }, type: { default: null } },
|
|
1961
|
+
content: "listItem+",
|
|
1962
|
+
group: "block list",
|
|
1963
|
+
parseDOM: [
|
|
1964
|
+
{
|
|
1965
|
+
tag: "ol",
|
|
1966
|
+
getAttrs: (el) => {
|
|
1967
|
+
const start = Number.parseInt(el.getAttribute("start") ?? "1", 10);
|
|
1968
|
+
const type = el.getAttribute("type");
|
|
1969
|
+
return {
|
|
1970
|
+
start: Number.isFinite(start) ? start : 1,
|
|
1971
|
+
type: type && /^[1aAiI]$/.test(type) ? type : null
|
|
1972
|
+
};
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
],
|
|
1976
|
+
toDOM: (node) => [
|
|
1977
|
+
"ol",
|
|
1978
|
+
{
|
|
1979
|
+
start: node.attrs.start === 1 ? null : String(node.attrs.start),
|
|
1980
|
+
type: node.attrs.type
|
|
1981
|
+
},
|
|
1982
|
+
0
|
|
1983
|
+
]
|
|
1984
|
+
}
|
|
1985
|
+
}),
|
|
1986
|
+
commands: ({ schema }) => ({
|
|
1987
|
+
toggleOrderedList: () => toggleList(schema.nodes.orderedList, schema.nodes.listItem)
|
|
1988
|
+
}),
|
|
1989
|
+
keymap: ({ schema }) => ({ "Mod-Shift-7": toggleList(schema.nodes.orderedList, schema.nodes.listItem) }),
|
|
1990
|
+
inputRules: ({ schema }) => [
|
|
1991
|
+
wrappingInputRule(
|
|
1992
|
+
/^(\d+)\.\s$/,
|
|
1993
|
+
schema.nodes.orderedList,
|
|
1994
|
+
(match) => ({ start: Number(match[1]) }),
|
|
1995
|
+
(match, node) => node.childCount + node.attrs.start === Number(match[1])
|
|
1996
|
+
)
|
|
1997
|
+
]
|
|
1998
|
+
});
|
|
1999
|
+
var TaskItem = defineExtension({
|
|
2000
|
+
name: "taskItem",
|
|
2001
|
+
nodes: (_o, { t }) => ({
|
|
2002
|
+
taskItem: {
|
|
2003
|
+
attrs: { checked: { default: false } },
|
|
2004
|
+
content: "paragraph block*",
|
|
2005
|
+
defining: true,
|
|
2006
|
+
parseDOM: [
|
|
2007
|
+
{
|
|
2008
|
+
tag: 'li[data-type="taskItem"]',
|
|
2009
|
+
priority: 51,
|
|
2010
|
+
getAttrs: (el) => ({
|
|
2011
|
+
checked: el.getAttribute("data-checked") === "true" || !!el.querySelector(':scope > input[type="checkbox"][checked], :scope > label > input[checked]')
|
|
2012
|
+
})
|
|
2013
|
+
}
|
|
2014
|
+
],
|
|
2015
|
+
// Output keeps a real, labelled checkbox so readers of the published page get the state.
|
|
2016
|
+
toDOM: (node) => [
|
|
2017
|
+
"li",
|
|
2018
|
+
{ "data-type": "taskItem", "data-checked": node.attrs.checked ? "true" : "false" },
|
|
2019
|
+
[
|
|
2020
|
+
"input",
|
|
2021
|
+
{
|
|
2022
|
+
type: "checkbox",
|
|
2023
|
+
disabled: "",
|
|
2024
|
+
checked: node.attrs.checked ? "" : null,
|
|
2025
|
+
"aria-label": t("task")
|
|
2026
|
+
}
|
|
2027
|
+
],
|
|
2028
|
+
["div", 0]
|
|
2029
|
+
]
|
|
2030
|
+
}
|
|
2031
|
+
}),
|
|
2032
|
+
commands: () => ({}),
|
|
2033
|
+
keymap: () => ({
|
|
2034
|
+
// Ctrl/Cmd+Enter toggles the task at the cursor.
|
|
2035
|
+
"Mod-Enter": (state, dispatch) => {
|
|
2036
|
+
const item = findParent(state.selection.$from, (t) => t.name === "taskItem");
|
|
2037
|
+
if (!item) return false;
|
|
2038
|
+
if (dispatch) dispatch(state.tr.setNodeMarkup(item.pos, void 0, { checked: !item.node.attrs.checked }));
|
|
2039
|
+
return true;
|
|
2040
|
+
}
|
|
2041
|
+
}),
|
|
2042
|
+
plugins: ({ editor }) => [
|
|
2043
|
+
new Plugin({
|
|
2044
|
+
props: {
|
|
2045
|
+
nodeViews: {
|
|
2046
|
+
taskItem: (node, view, getPos) => new TaskItemView(node, view, getPos, editor)
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
})
|
|
2050
|
+
]
|
|
2051
|
+
});
|
|
2052
|
+
var TaskItemView = class {
|
|
2053
|
+
constructor(node, view, getPos, editor) {
|
|
2054
|
+
this.node = node;
|
|
2055
|
+
this.checkbox = h("input", {
|
|
2056
|
+
type: "checkbox",
|
|
2057
|
+
className: "owe-task-checkbox",
|
|
2058
|
+
"aria-label": editor.t("toggleTask")
|
|
2059
|
+
});
|
|
2060
|
+
this.checkbox.checked = node.attrs.checked;
|
|
2061
|
+
this.checkbox.addEventListener("mousedown", (e) => e.preventDefault());
|
|
2062
|
+
this.checkbox.addEventListener("change", () => {
|
|
2063
|
+
const pos = getPos();
|
|
2064
|
+
if (pos === void 0 || !editor.isEditable) {
|
|
2065
|
+
this.checkbox.checked = this.node.attrs.checked;
|
|
2066
|
+
return;
|
|
2067
|
+
}
|
|
2068
|
+
view.dispatch(view.state.tr.setNodeMarkup(pos, void 0, { checked: this.checkbox.checked }));
|
|
2069
|
+
});
|
|
2070
|
+
const label = h("span", { contenteditable: "false", className: "owe-task-control" }, this.checkbox);
|
|
2071
|
+
this.contentDOM = h("div", { className: "owe-task-content" });
|
|
2072
|
+
this.dom = h("li", { "data-type": "taskItem" }, label, this.contentDOM);
|
|
2073
|
+
this.sync();
|
|
2074
|
+
}
|
|
2075
|
+
sync() {
|
|
2076
|
+
this.dom.dataset.checked = String(this.node.attrs.checked);
|
|
2077
|
+
this.checkbox.checked = this.node.attrs.checked;
|
|
2078
|
+
}
|
|
2079
|
+
update(node) {
|
|
2080
|
+
if (node.type !== this.node.type) return false;
|
|
2081
|
+
this.node = node;
|
|
2082
|
+
this.sync();
|
|
2083
|
+
return true;
|
|
2084
|
+
}
|
|
2085
|
+
stopEvent(event) {
|
|
2086
|
+
return event.target === this.checkbox;
|
|
2087
|
+
}
|
|
2088
|
+
ignoreMutation(mutation) {
|
|
2089
|
+
return mutation.type !== "selection" && !this.contentDOM.contains(mutation.target);
|
|
2090
|
+
}
|
|
2091
|
+
};
|
|
2092
|
+
var TaskList = defineExtension({
|
|
2093
|
+
name: "taskList",
|
|
2094
|
+
extensions: () => [TaskItem],
|
|
2095
|
+
nodes: () => ({
|
|
2096
|
+
taskList: {
|
|
2097
|
+
content: "taskItem+",
|
|
2098
|
+
group: "block list",
|
|
2099
|
+
parseDOM: [{ tag: 'ul[data-type="taskList"]', priority: 51 }],
|
|
2100
|
+
toDOM: () => ["ul", { "data-type": "taskList" }, 0]
|
|
2101
|
+
}
|
|
2102
|
+
}),
|
|
2103
|
+
commands: ({ schema }) => ({
|
|
2104
|
+
toggleTaskList: () => toggleList(schema.nodes.taskList, schema.nodes.taskItem)
|
|
2105
|
+
}),
|
|
2106
|
+
keymap: ({ schema }) => ({ "Mod-Shift-9": toggleList(schema.nodes.taskList, schema.nodes.taskItem) }),
|
|
2107
|
+
inputRules: ({ schema }) => [
|
|
2108
|
+
wrappingInputRule(/^\s*\[( |x|X)?\]\s$/, schema.nodes.taskList)
|
|
2109
|
+
]
|
|
2110
|
+
});
|
|
2111
|
+
var notNormal = (value) => value !== "normal" && value !== "400" && value !== "lighter";
|
|
2112
|
+
var Bold = defineExtension({
|
|
2113
|
+
name: "bold",
|
|
2114
|
+
marks: () => ({
|
|
2115
|
+
bold: {
|
|
2116
|
+
parseDOM: [
|
|
2117
|
+
{ tag: "strong" },
|
|
2118
|
+
// Google Docs wraps whole documents in <b style="font-weight:normal">.
|
|
2119
|
+
{ tag: "b", getAttrs: (el) => notNormal(el.style.fontWeight || "bold") && null },
|
|
2120
|
+
{ style: "font-weight=400", clearMark: (m) => m.type.name === "bold" },
|
|
2121
|
+
{ style: "font-weight", getAttrs: (v) => /^(bold(er)?|[6-9]\d{2})$/.test(v) && null }
|
|
2122
|
+
],
|
|
2123
|
+
toDOM: () => ["strong", 0]
|
|
2124
|
+
}
|
|
2125
|
+
}),
|
|
2126
|
+
commands: ({ schema }) => ({ toggleBold: () => toggleMarkCommand(schema.marks.bold) }),
|
|
2127
|
+
keymap: ({ schema }) => ({ "Mod-b": toggleMarkCommand(schema.marks.bold) }),
|
|
2128
|
+
inputRules: ({ schema }) => [
|
|
2129
|
+
markInputRule(/(?:^|[^*\w])(\*\*([^*\s](?:[^*]*[^*\s])?)\*\*)$/, schema.marks.bold),
|
|
2130
|
+
markInputRule(/(?:^|[^_\w])(__([^_\s](?:[^_]*[^_\s])?)__)$/, schema.marks.bold)
|
|
2131
|
+
]
|
|
2132
|
+
});
|
|
2133
|
+
var Italic = defineExtension({
|
|
2134
|
+
name: "italic",
|
|
2135
|
+
marks: () => ({
|
|
2136
|
+
italic: {
|
|
2137
|
+
parseDOM: [
|
|
2138
|
+
{ tag: "em" },
|
|
2139
|
+
{ tag: "i", getAttrs: (el) => el.style.fontStyle !== "normal" && null },
|
|
2140
|
+
{ style: "font-style=normal", clearMark: (m) => m.type.name === "italic" },
|
|
2141
|
+
{ style: "font-style=italic" }
|
|
2142
|
+
],
|
|
2143
|
+
toDOM: () => ["em", 0]
|
|
2144
|
+
}
|
|
2145
|
+
}),
|
|
2146
|
+
commands: ({ schema }) => ({ toggleItalic: () => toggleMarkCommand(schema.marks.italic) }),
|
|
2147
|
+
keymap: ({ schema }) => ({ "Mod-i": toggleMarkCommand(schema.marks.italic) }),
|
|
2148
|
+
inputRules: ({ schema }) => [
|
|
2149
|
+
markInputRule(/(?:^|[^*\w])(\*([^*\s](?:[^*]*[^*\s])?)\*)$/, schema.marks.italic),
|
|
2150
|
+
markInputRule(/(?:^|[^_\w])(_([^_\s](?:[^_]*[^_\s])?)_)$/, schema.marks.italic)
|
|
2151
|
+
]
|
|
2152
|
+
});
|
|
2153
|
+
var Underline = defineExtension({
|
|
2154
|
+
name: "underline",
|
|
2155
|
+
marks: () => ({
|
|
2156
|
+
underline: {
|
|
2157
|
+
parseDOM: [
|
|
2158
|
+
{ tag: "u" },
|
|
2159
|
+
{ tag: "ins" },
|
|
2160
|
+
{ style: "text-decoration", getAttrs: (v) => v.includes("underline") && null },
|
|
2161
|
+
{ style: "text-decoration-line", getAttrs: (v) => v.includes("underline") && null }
|
|
2162
|
+
],
|
|
2163
|
+
toDOM: () => ["u", 0]
|
|
2164
|
+
}
|
|
2165
|
+
}),
|
|
2166
|
+
commands: ({ schema }) => ({ toggleUnderline: () => toggleMarkCommand(schema.marks.underline) }),
|
|
2167
|
+
keymap: ({ schema }) => ({ "Mod-u": toggleMarkCommand(schema.marks.underline) })
|
|
2168
|
+
});
|
|
2169
|
+
var Strike = defineExtension({
|
|
2170
|
+
name: "strike",
|
|
2171
|
+
marks: () => ({
|
|
2172
|
+
strike: {
|
|
2173
|
+
parseDOM: [
|
|
2174
|
+
{ tag: "s" },
|
|
2175
|
+
{ tag: "del" },
|
|
2176
|
+
{ tag: "strike" },
|
|
2177
|
+
{ style: "text-decoration", getAttrs: (v) => v.includes("line-through") && null },
|
|
2178
|
+
{ style: "text-decoration-line", getAttrs: (v) => v.includes("line-through") && null }
|
|
2179
|
+
],
|
|
2180
|
+
toDOM: () => ["s", 0]
|
|
2181
|
+
}
|
|
2182
|
+
}),
|
|
2183
|
+
commands: ({ schema }) => ({ toggleStrike: () => toggleMarkCommand(schema.marks.strike) }),
|
|
2184
|
+
keymap: ({ schema }) => ({ "Mod-Shift-s": toggleMarkCommand(schema.marks.strike) }),
|
|
2185
|
+
inputRules: ({ schema }) => [
|
|
2186
|
+
markInputRule(/(?:^|[^~\w])(~~([^~\s](?:[^~]*[^~\s])?)~~)$/, schema.marks.strike)
|
|
2187
|
+
]
|
|
2188
|
+
});
|
|
2189
|
+
var Code = defineExtension({
|
|
2190
|
+
name: "code",
|
|
2191
|
+
marks: () => ({
|
|
2192
|
+
code: {
|
|
2193
|
+
excludes: "_",
|
|
2194
|
+
code: true,
|
|
2195
|
+
parseDOM: [{ tag: "code" }],
|
|
2196
|
+
toDOM: () => ["code", 0]
|
|
2197
|
+
}
|
|
2198
|
+
}),
|
|
2199
|
+
commands: ({ schema }) => ({ toggleCode: () => toggleMarkCommand(schema.marks.code) }),
|
|
2200
|
+
keymap: ({ schema }) => ({ "Mod-e": toggleMarkCommand(schema.marks.code) }),
|
|
2201
|
+
inputRules: ({ schema }) => [markInputRule(/(?:^|[^`])(`([^`]+)`)$/, schema.marks.code)]
|
|
2202
|
+
});
|
|
2203
|
+
var script = (tag, align, excludes) => ({
|
|
2204
|
+
excludes,
|
|
2205
|
+
parseDOM: [{ tag }, { style: "vertical-align", getAttrs: (v) => v === align && null }],
|
|
2206
|
+
toDOM: () => [tag, 0]
|
|
2207
|
+
});
|
|
2208
|
+
var Subscript = defineExtension({
|
|
2209
|
+
name: "subscript",
|
|
2210
|
+
marks: () => ({ subscript: script("sub", "sub", "superscript") }),
|
|
2211
|
+
commands: ({ schema }) => ({ toggleSubscript: () => toggleMarkCommand(schema.marks.subscript) }),
|
|
2212
|
+
keymap: ({ schema }) => ({ "Mod-,": toggleMarkCommand(schema.marks.subscript) })
|
|
2213
|
+
});
|
|
2214
|
+
var Superscript = defineExtension({
|
|
2215
|
+
name: "superscript",
|
|
2216
|
+
marks: () => ({ superscript: script("sup", "super", "subscript") }),
|
|
2217
|
+
commands: ({ schema }) => ({ toggleSuperscript: () => toggleMarkCommand(schema.marks.superscript) }),
|
|
2218
|
+
keymap: ({ schema }) => ({ "Mod-.": toggleMarkCommand(schema.marks.superscript) })
|
|
2219
|
+
});
|
|
2220
|
+
var Link = defineExtension({
|
|
2221
|
+
name: "link",
|
|
2222
|
+
priority: 1e3,
|
|
2223
|
+
// outermost mark: <a><strong>…</strong></a>
|
|
2224
|
+
defaultOptions: { rel: null, linkOnPaste: true },
|
|
2225
|
+
marks: (options, { urlPolicy }) => ({
|
|
2226
|
+
link: {
|
|
2227
|
+
attrs: {
|
|
2228
|
+
href: { default: null },
|
|
2229
|
+
target: { default: null },
|
|
2230
|
+
title: { default: null }
|
|
2231
|
+
},
|
|
2232
|
+
inclusive: false,
|
|
2233
|
+
parseDOM: [
|
|
2234
|
+
{
|
|
2235
|
+
tag: "a[href]",
|
|
2236
|
+
getAttrs: (el) => {
|
|
2237
|
+
const href = sanitizeUrl(el.getAttribute("href"), urlPolicy);
|
|
2238
|
+
if (!href) return false;
|
|
2239
|
+
return {
|
|
2240
|
+
href,
|
|
2241
|
+
target: el.getAttribute("target") === "_blank" ? "_blank" : null,
|
|
2242
|
+
title: el.getAttribute("title")
|
|
2243
|
+
};
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
],
|
|
2247
|
+
toDOM: (mark) => {
|
|
2248
|
+
const newTab = mark.attrs.target === "_blank";
|
|
2249
|
+
const rel = [newTab ? "noopener noreferrer" : null, options.rel].filter(Boolean).join(" ");
|
|
2250
|
+
return [
|
|
2251
|
+
"a",
|
|
2252
|
+
{
|
|
2253
|
+
href: sanitizeUrl(mark.attrs.href, urlPolicy),
|
|
2254
|
+
target: newTab ? "_blank" : null,
|
|
2255
|
+
rel: rel || null,
|
|
2256
|
+
title: mark.attrs.title
|
|
2257
|
+
},
|
|
2258
|
+
0
|
|
2259
|
+
];
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
}),
|
|
2263
|
+
commands: ({ schema }) => {
|
|
2264
|
+
const type = schema.marks.link;
|
|
2265
|
+
const setLink = (attrs) => (state, dispatch) => {
|
|
2266
|
+
const href = sanitizeUrl(attrs.href, readPolicy(schema));
|
|
2267
|
+
if (!href) return false;
|
|
2268
|
+
const mark = type.create({ href, target: attrs.newTab ? "_blank" : null, title: attrs.title ?? null });
|
|
2269
|
+
const { empty, from, to, $from } = state.selection;
|
|
2270
|
+
const tr = state.tr;
|
|
2271
|
+
if (empty) {
|
|
2272
|
+
const range = markRange($from, type);
|
|
2273
|
+
if (range) {
|
|
2274
|
+
if (attrs.text && attrs.text !== state.doc.textBetween(range.from, range.to)) {
|
|
2275
|
+
const kept = state.doc.resolve(range.from + 1).marks().filter((m) => m.type !== type);
|
|
2276
|
+
tr.replaceWith(range.from, range.to, schema.text(attrs.text, [...kept, mark]));
|
|
2277
|
+
} else {
|
|
2278
|
+
tr.removeMark(range.from, range.to, type).addMark(range.from, range.to, mark);
|
|
2279
|
+
}
|
|
2280
|
+
} else {
|
|
2281
|
+
const text = attrs.text || href;
|
|
2282
|
+
tr.insert(from, schema.text(text, [...$from.marks().filter((m) => m.type !== type), mark]));
|
|
2283
|
+
tr.setSelection(TextSelection.create(tr.doc, from + text.length));
|
|
2284
|
+
}
|
|
2285
|
+
} else if (attrs.text && attrs.text !== state.doc.textBetween(from, to)) {
|
|
2286
|
+
tr.replaceWith(from, to, schema.text(attrs.text, [mark]));
|
|
2287
|
+
} else {
|
|
2288
|
+
tr.removeMark(from, to, type).addMark(from, to, mark);
|
|
2289
|
+
}
|
|
2290
|
+
tr.removeStoredMark(type);
|
|
2291
|
+
if (dispatch) dispatch(tr.scrollIntoView());
|
|
2292
|
+
return true;
|
|
2293
|
+
};
|
|
2294
|
+
const unsetLink = (state, dispatch) => {
|
|
2295
|
+
const { empty, from, to, $from } = state.selection;
|
|
2296
|
+
const range = empty ? markRange($from, type) : { from, to };
|
|
2297
|
+
if (!range) return false;
|
|
2298
|
+
if (dispatch) dispatch(state.tr.removeMark(range.from, range.to, type));
|
|
2299
|
+
return true;
|
|
2300
|
+
};
|
|
2301
|
+
return { setLink, unsetLink: () => unsetLink };
|
|
2302
|
+
},
|
|
2303
|
+
plugins: ({ schema, options }) => options.linkOnPaste ? [
|
|
2304
|
+
new Plugin({
|
|
2305
|
+
props: {
|
|
2306
|
+
handlePaste(view, event) {
|
|
2307
|
+
const { empty, from, to } = view.state.selection;
|
|
2308
|
+
if (empty) return false;
|
|
2309
|
+
const text = event.clipboardData?.getData("text/plain")?.trim() ?? "";
|
|
2310
|
+
if (!/^(https?:\/\/|mailto:)\S+$/i.test(text)) return false;
|
|
2311
|
+
const href = sanitizeUrl(text, readPolicy(schema));
|
|
2312
|
+
if (!href) return false;
|
|
2313
|
+
view.dispatch(view.state.tr.addMark(from, to, schema.marks.link.create({ href })));
|
|
2314
|
+
return true;
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
})
|
|
2318
|
+
] : []
|
|
2319
|
+
});
|
|
2320
|
+
function readPolicy(schema) {
|
|
2321
|
+
return schema.cached.urlPolicy ?? {};
|
|
2322
|
+
}
|
|
2323
|
+
var Document = defineExtension({
|
|
2324
|
+
name: "doc",
|
|
2325
|
+
nodes: () => ({ doc: { content: "block+" } })
|
|
2326
|
+
});
|
|
2327
|
+
var Text = defineExtension({
|
|
2328
|
+
name: "text",
|
|
2329
|
+
nodes: () => ({ text: { group: "inline" } })
|
|
2330
|
+
});
|
|
2331
|
+
var Paragraph = defineExtension({
|
|
2332
|
+
name: "paragraph",
|
|
2333
|
+
priority: 1e3,
|
|
2334
|
+
nodes: () => ({
|
|
2335
|
+
paragraph: {
|
|
2336
|
+
content: "inline*",
|
|
2337
|
+
group: "block",
|
|
2338
|
+
parseDOM: [{ tag: "p" }],
|
|
2339
|
+
toDOM: () => ["p", 0]
|
|
2340
|
+
}
|
|
2341
|
+
}),
|
|
2342
|
+
commands: ({ schema }) => ({
|
|
2343
|
+
setParagraph: () => setBlockType(schema.nodes.paragraph)
|
|
2344
|
+
}),
|
|
2345
|
+
keymap: ({ schema }) => ({ "Mod-Alt-0": setBlockType(schema.nodes.paragraph) })
|
|
2346
|
+
});
|
|
2347
|
+
var Heading = defineExtension({
|
|
2348
|
+
name: "heading",
|
|
2349
|
+
defaultOptions: { levels: [1, 2, 3, 4, 5, 6] },
|
|
2350
|
+
nodes: ({ levels }) => ({
|
|
2351
|
+
heading: {
|
|
2352
|
+
attrs: { level: { default: levels[0] ?? 1, validate: "number" } },
|
|
2353
|
+
content: "inline*",
|
|
2354
|
+
group: "block",
|
|
2355
|
+
defining: true,
|
|
2356
|
+
parseDOM: [1, 2, 3, 4, 5, 6].map((n) => ({
|
|
2357
|
+
tag: `h${n}`,
|
|
2358
|
+
// Levels that aren't enabled collapse to the nearest allowed one (structure is kept).
|
|
2359
|
+
attrs: { level: nearest(levels, n) }
|
|
2360
|
+
})),
|
|
2361
|
+
toDOM: (node) => [`h${nearest(levels, node.attrs.level)}`, 0]
|
|
2362
|
+
}
|
|
2363
|
+
}),
|
|
2364
|
+
commands: ({ schema, options }) => {
|
|
2365
|
+
const type = schema.nodes.heading;
|
|
2366
|
+
const valid = (level) => options.levels.includes(level);
|
|
2367
|
+
return {
|
|
2368
|
+
setHeading: (level) => valid(level) ? setBlockType(type, { level }) : () => false,
|
|
2369
|
+
toggleHeading: (level) => valid(level) ? toggleBlockType(type, schema.nodes.paragraph, { level }) : () => false
|
|
2370
|
+
};
|
|
2371
|
+
},
|
|
2372
|
+
keymap: ({ schema, options }) => Object.fromEntries(
|
|
2373
|
+
options.levels.map((level) => [
|
|
2374
|
+
`Mod-Alt-${level}`,
|
|
2375
|
+
toggleBlockType(schema.nodes.heading, schema.nodes.paragraph, { level })
|
|
2376
|
+
])
|
|
2377
|
+
),
|
|
2378
|
+
inputRules: ({ schema, options }) => [
|
|
2379
|
+
textblockTypeInputRule(/^(#{1,6})\s$/, schema.nodes.heading, (match) => ({
|
|
2380
|
+
level: nearest(options.levels, match[1].length)
|
|
2381
|
+
}))
|
|
2382
|
+
]
|
|
2383
|
+
});
|
|
2384
|
+
function nearest(levels, level) {
|
|
2385
|
+
if (levels.includes(level)) return level;
|
|
2386
|
+
return levels.reduce((best, l) => Math.abs(l - level) < Math.abs(best - level) ? l : best, levels[0] ?? 1);
|
|
2387
|
+
}
|
|
2388
|
+
var Blockquote = defineExtension({
|
|
2389
|
+
name: "blockquote",
|
|
2390
|
+
nodes: () => ({
|
|
2391
|
+
blockquote: {
|
|
2392
|
+
content: "block+",
|
|
2393
|
+
group: "block",
|
|
2394
|
+
defining: true,
|
|
2395
|
+
parseDOM: [{ tag: "blockquote" }],
|
|
2396
|
+
toDOM: () => ["blockquote", 0]
|
|
2397
|
+
}
|
|
2398
|
+
}),
|
|
2399
|
+
commands: ({ schema }) => ({ toggleBlockquote: () => toggleWrap(schema.nodes.blockquote) }),
|
|
2400
|
+
keymap: ({ schema }) => ({ "Mod-Shift-b": toggleWrap(schema.nodes.blockquote) }),
|
|
2401
|
+
inputRules: ({ schema }) => [wrappingInputRule(/^\s*>\s$/, schema.nodes.blockquote)]
|
|
2402
|
+
});
|
|
2403
|
+
var insertHardBreak = (state, dispatch) => {
|
|
2404
|
+
const type = state.schema.nodes.hardBreak;
|
|
2405
|
+
if (!type) return false;
|
|
2406
|
+
if (dispatch) dispatch(state.tr.replaceSelectionWith(type.create()).scrollIntoView());
|
|
2407
|
+
return true;
|
|
2408
|
+
};
|
|
2409
|
+
var HardBreak = defineExtension({
|
|
2410
|
+
name: "hardBreak",
|
|
2411
|
+
nodes: () => ({
|
|
2412
|
+
hardBreak: {
|
|
2413
|
+
inline: true,
|
|
2414
|
+
group: "inline",
|
|
2415
|
+
selectable: false,
|
|
2416
|
+
leafText: () => "\n",
|
|
2417
|
+
parseDOM: [{ tag: "br" }],
|
|
2418
|
+
toDOM: () => ["br"]
|
|
2419
|
+
}
|
|
2420
|
+
}),
|
|
2421
|
+
commands: () => ({ setHardBreak: () => insertHardBreak }),
|
|
2422
|
+
keymap: () => ({
|
|
2423
|
+
"Shift-Enter": chainCommands(exitCode, insertHardBreak),
|
|
2424
|
+
"Mod-Enter": chainCommands(exitCode, insertHardBreak)
|
|
2425
|
+
})
|
|
2426
|
+
});
|
|
2427
|
+
var insertHorizontalRule = (state, dispatch) => {
|
|
2428
|
+
const { horizontalRule, paragraph } = state.schema.nodes;
|
|
2429
|
+
if (!horizontalRule) return false;
|
|
2430
|
+
if (dispatch) {
|
|
2431
|
+
const tr = state.tr.replaceSelectionWith(horizontalRule.create());
|
|
2432
|
+
const after = tr.selection.$from;
|
|
2433
|
+
if (!after.nodeAfter && paragraph) {
|
|
2434
|
+
tr.insert(after.pos, paragraph.create());
|
|
2435
|
+
tr.setSelection(TextSelection.create(tr.doc, after.pos + 1));
|
|
2436
|
+
}
|
|
2437
|
+
dispatch(tr.scrollIntoView());
|
|
2438
|
+
}
|
|
2439
|
+
return true;
|
|
2440
|
+
};
|
|
2441
|
+
var HorizontalRule = defineExtension({
|
|
2442
|
+
name: "horizontalRule",
|
|
2443
|
+
nodes: () => ({
|
|
2444
|
+
horizontalRule: {
|
|
2445
|
+
group: "block",
|
|
2446
|
+
parseDOM: [{ tag: "hr" }],
|
|
2447
|
+
toDOM: () => ["hr"]
|
|
2448
|
+
}
|
|
2449
|
+
}),
|
|
2450
|
+
commands: () => ({ setHorizontalRule: () => insertHorizontalRule }),
|
|
2451
|
+
inputRules: ({ schema }) => [
|
|
2452
|
+
new InputRule(/^(?:---|___\s|\*\*\*\s)$/, (state, _match, start, end) => {
|
|
2453
|
+
const type = schema.nodes.horizontalRule;
|
|
2454
|
+
const $start = state.doc.resolve(start);
|
|
2455
|
+
if (!$start.node(-1).canReplaceWith($start.index(-1), $start.indexAfter(-1), type)) return null;
|
|
2456
|
+
const tr = state.tr.delete(start, end);
|
|
2457
|
+
const pos = tr.mapping.map($start.before());
|
|
2458
|
+
tr.insert(pos, type.create());
|
|
2459
|
+
return tr;
|
|
2460
|
+
})
|
|
2461
|
+
]
|
|
2462
|
+
});
|
|
2463
|
+
var LANGUAGE = /^[a-zA-Z0-9_+#.-]{1,32}$/;
|
|
2464
|
+
var CodeBlock = defineExtension({
|
|
2465
|
+
name: "codeBlock",
|
|
2466
|
+
defaultOptions: { languageClassPrefix: "language-" },
|
|
2467
|
+
nodes: ({ languageClassPrefix }) => ({
|
|
2468
|
+
codeBlock: {
|
|
2469
|
+
attrs: { language: { default: null } },
|
|
2470
|
+
content: "text*",
|
|
2471
|
+
marks: "",
|
|
2472
|
+
group: "block",
|
|
2473
|
+
code: true,
|
|
2474
|
+
defining: true,
|
|
2475
|
+
parseDOM: [
|
|
2476
|
+
{
|
|
2477
|
+
tag: "pre",
|
|
2478
|
+
preserveWhitespace: "full",
|
|
2479
|
+
getAttrs: (dom) => {
|
|
2480
|
+
const cls = (dom.querySelector("code")?.className ?? dom.className) || "";
|
|
2481
|
+
const match = /(?:^|\s)(?:language|lang)-([^\s]+)/.exec(cls);
|
|
2482
|
+
const language = match?.[1] ?? dom.getAttribute("data-language");
|
|
2483
|
+
return { language: language && LANGUAGE.test(language) ? language : null };
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
],
|
|
2487
|
+
toDOM: (node) => {
|
|
2488
|
+
const language = node.attrs.language;
|
|
2489
|
+
const valid = language && LANGUAGE.test(language);
|
|
2490
|
+
return ["pre", ["code", valid ? { class: `${languageClassPrefix}${language}` } : {}, 0]];
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
}),
|
|
2494
|
+
commands: ({ schema }) => ({
|
|
2495
|
+
toggleCodeBlock: (attrs = {}) => toggleBlockType(
|
|
2496
|
+
schema.nodes.codeBlock,
|
|
2497
|
+
schema.nodes.paragraph,
|
|
2498
|
+
{ language: attrs.language && LANGUAGE.test(attrs.language) ? attrs.language : null },
|
|
2499
|
+
{}
|
|
2500
|
+
// any code block counts as active, whatever its language
|
|
2501
|
+
)
|
|
2502
|
+
}),
|
|
2503
|
+
keymap: ({ schema }) => ({
|
|
2504
|
+
"Mod-Alt-c": toggleBlockType(schema.nodes.codeBlock, schema.nodes.paragraph),
|
|
2505
|
+
// Backspace at the start of an empty code block turns it back into a paragraph.
|
|
2506
|
+
Backspace: (state, dispatch) => {
|
|
2507
|
+
const { $from, empty } = state.selection;
|
|
2508
|
+
if (!empty || $from.parent.type !== schema.nodes.codeBlock || $from.parent.content.size) return false;
|
|
2509
|
+
return setBlockType(schema.nodes.paragraph)(state, dispatch);
|
|
2510
|
+
}
|
|
2511
|
+
}),
|
|
2512
|
+
inputRules: ({ schema }) => [
|
|
2513
|
+
textblockTypeInputRule(/^```([a-zA-Z0-9_+#.-]{1,32})?[\s\n]$/, schema.nodes.codeBlock, (match) => ({
|
|
2514
|
+
language: match[1] ?? null
|
|
2515
|
+
}))
|
|
2516
|
+
]
|
|
2517
|
+
});
|
|
2518
|
+
var Table = defineExtension({
|
|
2519
|
+
name: "table",
|
|
2520
|
+
defaultOptions: { resizable: true, maxSize: 100 },
|
|
2521
|
+
nodes: () => {
|
|
2522
|
+
const base = tableNodes({
|
|
2523
|
+
tableGroup: "block",
|
|
2524
|
+
cellContent: "block+",
|
|
2525
|
+
cellAttributes: {
|
|
2526
|
+
scope: {
|
|
2527
|
+
default: null,
|
|
2528
|
+
getFromDOM: (dom) => {
|
|
2529
|
+
const scope = dom.getAttribute("scope");
|
|
2530
|
+
return scope === "col" || scope === "row" ? scope : null;
|
|
2531
|
+
},
|
|
2532
|
+
setDOMAttr: (value, attrs) => {
|
|
2533
|
+
if (value) attrs.scope = value;
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
});
|
|
2538
|
+
const table = {
|
|
2539
|
+
...base.table,
|
|
2540
|
+
content: "tableRow+",
|
|
2541
|
+
attrs: { caption: { default: null } },
|
|
2542
|
+
parseDOM: [
|
|
2543
|
+
{
|
|
2544
|
+
tag: "table",
|
|
2545
|
+
getAttrs: (el) => ({
|
|
2546
|
+
caption: el.querySelector(":scope > caption")?.textContent?.trim() || null
|
|
2547
|
+
})
|
|
2548
|
+
},
|
|
2549
|
+
// The caption is stored as an attribute (read above); don't parse it as cell content.
|
|
2550
|
+
{ tag: "caption", ignore: true }
|
|
2551
|
+
],
|
|
2552
|
+
toDOM: (node) => node.attrs.caption ? ["table", ["caption", node.attrs.caption], ["tbody", 0]] : ["table", ["tbody", 0]]
|
|
2553
|
+
};
|
|
2554
|
+
return {
|
|
2555
|
+
table,
|
|
2556
|
+
tableRow: { ...base.table_row, content: "(tableCell | tableHeader)*" },
|
|
2557
|
+
tableCell: base.table_cell,
|
|
2558
|
+
tableHeader: base.table_header
|
|
2559
|
+
};
|
|
2560
|
+
},
|
|
2561
|
+
commands: ({ schema, options }) => {
|
|
2562
|
+
const insertTable = ({ rows = 3, cols = 3, withHeaderRow = true, caption = null } = {}) => (state, dispatch) => {
|
|
2563
|
+
const r = clamp(rows, 1, options.maxSize);
|
|
2564
|
+
const c = clamp(cols, 1, options.maxSize);
|
|
2565
|
+
const { table, tableRow, tableCell, tableHeader, paragraph } = schema.nodes;
|
|
2566
|
+
const cell = (header) => (header ? tableHeader : tableCell).createAndFill(header ? { scope: "col" } : null);
|
|
2567
|
+
const rowNodes = Array.from(
|
|
2568
|
+
{ length: r },
|
|
2569
|
+
(_, i) => tableRow.create(null, Array.from({ length: c }, () => cell(withHeaderRow && i === 0)))
|
|
2570
|
+
);
|
|
2571
|
+
const node = table.create({ caption: caption?.trim() || null }, rowNodes);
|
|
2572
|
+
if (!dispatch) return true;
|
|
2573
|
+
const tr = state.tr.replaceSelectionWith(node);
|
|
2574
|
+
let tablePos = -1;
|
|
2575
|
+
tr.doc.descendants((n, pos) => {
|
|
2576
|
+
if (n === node) tablePos = pos;
|
|
2577
|
+
return tablePos < 0;
|
|
2578
|
+
});
|
|
2579
|
+
if (tablePos < 0) return false;
|
|
2580
|
+
if (tablePos + node.nodeSize === tr.doc.content.size && paragraph) {
|
|
2581
|
+
tr.insert(tr.doc.content.size, paragraph.create());
|
|
2582
|
+
}
|
|
2583
|
+
tr.setSelection(TextSelection.near(tr.doc.resolve(tablePos + 4)));
|
|
2584
|
+
dispatch(tr.scrollIntoView());
|
|
2585
|
+
return true;
|
|
2586
|
+
};
|
|
2587
|
+
const setTableCaption = (caption) => (state, dispatch) => {
|
|
2588
|
+
const found = findParent(state.selection.$from, (t) => t.name === "table");
|
|
2589
|
+
if (!found) return false;
|
|
2590
|
+
if (dispatch) {
|
|
2591
|
+
dispatch(state.tr.setNodeMarkup(found.pos, void 0, { ...found.node.attrs, caption: caption?.trim() || null }));
|
|
2592
|
+
}
|
|
2593
|
+
return true;
|
|
2594
|
+
};
|
|
2595
|
+
return {
|
|
2596
|
+
insertTable,
|
|
2597
|
+
addRowBefore: () => addRowBefore,
|
|
2598
|
+
addRowAfter: () => addRowAfter,
|
|
2599
|
+
addColumnBefore: () => addColumnBefore,
|
|
2600
|
+
addColumnAfter: () => addColumnAfter,
|
|
2601
|
+
deleteRow: () => deleteRow,
|
|
2602
|
+
deleteColumn: () => deleteColumn,
|
|
2603
|
+
deleteTable: () => deleteTable,
|
|
2604
|
+
mergeCells: () => mergeCells,
|
|
2605
|
+
splitCell: () => splitCell,
|
|
2606
|
+
// The non-deprecated toggle logic (the default exports use the legacy behaviour).
|
|
2607
|
+
toggleHeaderRow: () => toggleHeader("row"),
|
|
2608
|
+
toggleHeaderColumn: () => toggleHeader("column"),
|
|
2609
|
+
setTableCaption,
|
|
2610
|
+
goToNextCell: (direction) => goToNextCell(direction)
|
|
2611
|
+
};
|
|
2612
|
+
},
|
|
2613
|
+
keymap: () => ({
|
|
2614
|
+
// Fails at the last/first cell, so Tab then moves focus out of the editor (no trap).
|
|
2615
|
+
Tab: goToNextCell(1),
|
|
2616
|
+
"Shift-Tab": goToNextCell(-1)
|
|
2617
|
+
}),
|
|
2618
|
+
plugins: ({ options }) => [
|
|
2619
|
+
...options.resizable ? [columnResizing({ cellMinWidth: 48 })] : [],
|
|
2620
|
+
tableEditing(),
|
|
2621
|
+
headerScopePlugin()
|
|
2622
|
+
]
|
|
2623
|
+
});
|
|
2624
|
+
function clamp(n, min, max) {
|
|
2625
|
+
return Math.min(max, Math.max(min, Math.floor(Number.isFinite(n) ? n : min)));
|
|
2626
|
+
}
|
|
2627
|
+
function headerScopePlugin() {
|
|
2628
|
+
return new Plugin({
|
|
2629
|
+
appendTransaction(trs, _old, state) {
|
|
2630
|
+
if (!trs.some((tr2) => tr2.docChanged)) return null;
|
|
2631
|
+
const tr = state.tr;
|
|
2632
|
+
state.doc.descendants((node, pos) => {
|
|
2633
|
+
if (node.type.name !== "table") return node.isBlock;
|
|
2634
|
+
node.forEach((row, rowOffset, rowIndex) => {
|
|
2635
|
+
row.forEach((cell, cellOffset, cellIndex) => {
|
|
2636
|
+
const want = cell.type.name === "tableHeader" ? rowIndex === 0 ? "col" : cellIndex === 0 ? "row" : "col" : null;
|
|
2637
|
+
if (cell.attrs.scope !== want) {
|
|
2638
|
+
tr.setNodeMarkup(pos + 1 + rowOffset + 1 + cellOffset, void 0, { ...cell.attrs, scope: want });
|
|
2639
|
+
}
|
|
2640
|
+
});
|
|
2641
|
+
});
|
|
2642
|
+
return false;
|
|
2643
|
+
});
|
|
2644
|
+
return tr.docChanged ? tr.setMeta("addToHistory", false) : null;
|
|
2645
|
+
}
|
|
2646
|
+
});
|
|
2647
|
+
}
|
|
2648
|
+
var searchKey = new PluginKey("owe-search");
|
|
2649
|
+
var EMPTY = { query: "", caseSensitive: false, wholeWord: false, matches: [], index: -1 };
|
|
2650
|
+
var escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2651
|
+
function findMatches(doc, q) {
|
|
2652
|
+
if (!q.query) return [];
|
|
2653
|
+
let source = escapeRegExp(q.query);
|
|
2654
|
+
if (q.wholeWord) source = `(?<![\\p{L}\\p{N}_\\p{M}])${source}(?![\\p{L}\\p{N}_\\p{M}])`;
|
|
2655
|
+
let re;
|
|
2656
|
+
try {
|
|
2657
|
+
re = new RegExp(source, `g${q.caseSensitive ? "" : "i"}u`);
|
|
2658
|
+
} catch {
|
|
2659
|
+
return [];
|
|
2660
|
+
}
|
|
2661
|
+
const matches = [];
|
|
2662
|
+
doc.descendants((node, pos) => {
|
|
2663
|
+
if (!node.isTextblock) return true;
|
|
2664
|
+
let text = "";
|
|
2665
|
+
node.forEach((child) => {
|
|
2666
|
+
text += child.isText ? child.text : "".repeat(child.nodeSize);
|
|
2667
|
+
});
|
|
2668
|
+
for (const m of text.matchAll(re)) {
|
|
2669
|
+
if (!m[0]) continue;
|
|
2670
|
+
const from = pos + 1 + m.index;
|
|
2671
|
+
matches.push({ from, to: from + m[0].length });
|
|
2672
|
+
if (matches.length > 1e4) return false;
|
|
2673
|
+
}
|
|
2674
|
+
return false;
|
|
2675
|
+
});
|
|
2676
|
+
return matches;
|
|
2677
|
+
}
|
|
2678
|
+
function nearestIndex(matches, pos) {
|
|
2679
|
+
if (!matches.length) return -1;
|
|
2680
|
+
const i = matches.findIndex((m) => m.from >= pos);
|
|
2681
|
+
return i === -1 ? 0 : i;
|
|
2682
|
+
}
|
|
2683
|
+
function getSearchState(state) {
|
|
2684
|
+
return searchKey.getState(state) ?? EMPTY;
|
|
2685
|
+
}
|
|
2686
|
+
var select = (direction) => (state, dispatch) => {
|
|
2687
|
+
const s = getSearchState(state);
|
|
2688
|
+
if (!s.matches.length) return false;
|
|
2689
|
+
let index;
|
|
2690
|
+
if (s.index === -1) index = direction === 1 ? nearestIndex(s.matches, state.selection.from) : s.matches.length - 1;
|
|
2691
|
+
else index = (s.index + direction + s.matches.length) % s.matches.length;
|
|
2692
|
+
const m = s.matches[index];
|
|
2693
|
+
if (dispatch) {
|
|
2694
|
+
dispatch(
|
|
2695
|
+
state.tr.setSelection(TextSelection.create(state.doc, m.from, m.to)).setMeta(searchKey, { type: "index", index }).scrollIntoView()
|
|
2696
|
+
);
|
|
2697
|
+
}
|
|
2698
|
+
return true;
|
|
2699
|
+
};
|
|
2700
|
+
var FindReplace = defineExtension({
|
|
2701
|
+
name: "findReplace",
|
|
2702
|
+
commands: () => ({
|
|
2703
|
+
setSearch: (query) => (state, dispatch) => {
|
|
2704
|
+
if (dispatch) dispatch(state.tr.setMeta(searchKey, { type: "set", query }));
|
|
2705
|
+
return true;
|
|
2706
|
+
},
|
|
2707
|
+
clearSearch: () => (state, dispatch) => {
|
|
2708
|
+
if (dispatch) dispatch(state.tr.setMeta(searchKey, { type: "clear" }));
|
|
2709
|
+
return true;
|
|
2710
|
+
},
|
|
2711
|
+
findNext: () => select(1),
|
|
2712
|
+
findPrevious: () => select(-1),
|
|
2713
|
+
replaceCurrent: (replacement) => (state, dispatch) => {
|
|
2714
|
+
const s = getSearchState(state);
|
|
2715
|
+
const m = s.matches[s.index];
|
|
2716
|
+
if (!m) return false;
|
|
2717
|
+
if (dispatch) {
|
|
2718
|
+
const tr = state.tr;
|
|
2719
|
+
if (replacement) tr.insertText(replacement, m.from, m.to);
|
|
2720
|
+
else tr.delete(m.from, m.to);
|
|
2721
|
+
const next = findMatches(tr.doc, s).findIndex((x) => x.from >= tr.mapping.map(m.to));
|
|
2722
|
+
if (next >= 0) {
|
|
2723
|
+
const nm = findMatches(tr.doc, s)[next];
|
|
2724
|
+
tr.setSelection(TextSelection.create(tr.doc, nm.from, nm.to)).setMeta(searchKey, { type: "index", index: next });
|
|
2725
|
+
}
|
|
2726
|
+
dispatch(tr.scrollIntoView());
|
|
2727
|
+
}
|
|
2728
|
+
return true;
|
|
2729
|
+
},
|
|
2730
|
+
replaceAll: (replacement) => (state, dispatch) => {
|
|
2731
|
+
const s = getSearchState(state);
|
|
2732
|
+
if (!s.matches.length) return false;
|
|
2733
|
+
if (dispatch) {
|
|
2734
|
+
const tr = state.tr;
|
|
2735
|
+
for (const m of [...s.matches].reverse()) {
|
|
2736
|
+
if (replacement) tr.insertText(replacement, m.from, m.to);
|
|
2737
|
+
else tr.delete(m.from, m.to);
|
|
2738
|
+
}
|
|
2739
|
+
dispatch(tr);
|
|
2740
|
+
}
|
|
2741
|
+
return true;
|
|
2742
|
+
}
|
|
2743
|
+
}),
|
|
2744
|
+
plugins: () => [
|
|
2745
|
+
new Plugin({
|
|
2746
|
+
key: searchKey,
|
|
2747
|
+
state: {
|
|
2748
|
+
init: () => EMPTY,
|
|
2749
|
+
apply(tr, prev) {
|
|
2750
|
+
const meta = tr.getMeta(searchKey);
|
|
2751
|
+
if (meta?.type === "clear") return EMPTY;
|
|
2752
|
+
if (meta?.type === "set") {
|
|
2753
|
+
const q = { query: meta.query.query, caseSensitive: !!meta.query.caseSensitive, wholeWord: !!meta.query.wholeWord };
|
|
2754
|
+
const matches = findMatches(tr.doc, q);
|
|
2755
|
+
return { ...q, matches, index: -1 };
|
|
2756
|
+
}
|
|
2757
|
+
if (meta?.type === "index") return { ...prev, index: meta.index };
|
|
2758
|
+
if (tr.docChanged && prev.query) {
|
|
2759
|
+
const matches = findMatches(tr.doc, prev);
|
|
2760
|
+
return { ...prev, matches, index: Math.min(prev.index, matches.length - 1) };
|
|
2761
|
+
}
|
|
2762
|
+
return prev;
|
|
2763
|
+
}
|
|
2764
|
+
},
|
|
2765
|
+
props: {
|
|
2766
|
+
decorations(state) {
|
|
2767
|
+
const s = getSearchState(state);
|
|
2768
|
+
if (!s.matches.length) return null;
|
|
2769
|
+
return DecorationSet.create(
|
|
2770
|
+
state.doc,
|
|
2771
|
+
s.matches.map(
|
|
2772
|
+
(m, i) => Decoration.inline(m.from, m.to, { class: i === s.index ? "owe-find-match owe-find-current" : "owe-find-match" })
|
|
2773
|
+
)
|
|
2774
|
+
);
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
})
|
|
2778
|
+
]
|
|
2779
|
+
});
|
|
2780
|
+
|
|
2781
|
+
// src/core/color.ts
|
|
2782
|
+
var HEX = /^#([0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
|
|
2783
|
+
var RGB_FN = /^rgba?\(\s*(\d{1,3})\s*[, ]\s*(\d{1,3})\s*[, ]\s*(\d{1,3})\s*(?:[,/]\s*(?:0|1|0?\.\d+|\d{1,3}%))?\s*\)$/i;
|
|
2784
|
+
var HSL_FN = /^hsla?\(\s*(\d{1,3}(?:\.\d+)?)(?:deg)?\s*[, ]\s*(\d{1,3}(?:\.\d+)?)%\s*[, ]\s*(\d{1,3}(?:\.\d+)?)%\s*(?:[,/]\s*(?:0|1|0?\.\d+|\d{1,3}%))?\s*\)$/i;
|
|
2785
|
+
var NAMED = {
|
|
2786
|
+
black: "#000000",
|
|
2787
|
+
white: "#ffffff",
|
|
2788
|
+
red: "#ff0000",
|
|
2789
|
+
green: "#008000",
|
|
2790
|
+
blue: "#0000ff",
|
|
2791
|
+
yellow: "#ffff00",
|
|
2792
|
+
orange: "#ffa500",
|
|
2793
|
+
purple: "#800080",
|
|
2794
|
+
gray: "#808080",
|
|
2795
|
+
grey: "#808080",
|
|
2796
|
+
silver: "#c0c0c0",
|
|
2797
|
+
maroon: "#800000",
|
|
2798
|
+
navy: "#000080",
|
|
2799
|
+
teal: "#008080",
|
|
2800
|
+
olive: "#808000",
|
|
2801
|
+
lime: "#00ff00",
|
|
2802
|
+
aqua: "#00ffff",
|
|
2803
|
+
cyan: "#00ffff",
|
|
2804
|
+
fuchsia: "#ff00ff",
|
|
2805
|
+
magenta: "#ff00ff",
|
|
2806
|
+
pink: "#ffc0cb",
|
|
2807
|
+
brown: "#a52a2a",
|
|
2808
|
+
darkred: "#8b0000",
|
|
2809
|
+
darkblue: "#00008b",
|
|
2810
|
+
darkgreen: "#006400"
|
|
2811
|
+
};
|
|
2812
|
+
function hsl2rgb(h2, s, l) {
|
|
2813
|
+
s /= 100;
|
|
2814
|
+
l /= 100;
|
|
2815
|
+
const k = (n) => (n + h2 / 30) % 12;
|
|
2816
|
+
const a = s * Math.min(l, 1 - l);
|
|
2817
|
+
const f = (n) => l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
|
|
2818
|
+
return [Math.round(f(0) * 255), Math.round(f(8) * 255), Math.round(f(4) * 255)];
|
|
2819
|
+
}
|
|
2820
|
+
function parseColor(input) {
|
|
2821
|
+
if (typeof input !== "string") return null;
|
|
2822
|
+
const value = input.trim().toLowerCase();
|
|
2823
|
+
const hex = HEX.exec(NAMED[value] ?? value)?.[1];
|
|
2824
|
+
if (hex) {
|
|
2825
|
+
const full = hex.length <= 4 ? [...hex.slice(0, 3)].map((c) => c + c).join("") : hex.slice(0, 6);
|
|
2826
|
+
return [0, 2, 4].map((i) => Number.parseInt(full.slice(i, i + 2), 16));
|
|
2827
|
+
}
|
|
2828
|
+
const rgb = RGB_FN.exec(value);
|
|
2829
|
+
if (rgb) {
|
|
2830
|
+
const parts2 = rgb.slice(1, 4).map(Number);
|
|
2831
|
+
return parts2.every((n) => n <= 255) ? parts2 : null;
|
|
2832
|
+
}
|
|
2833
|
+
const hsl = HSL_FN.exec(value);
|
|
2834
|
+
if (hsl) return hsl2rgb(Number(hsl[1]) % 360, Math.min(100, Number(hsl[2])), Math.min(100, Number(hsl[3])));
|
|
2835
|
+
return null;
|
|
2836
|
+
}
|
|
2837
|
+
var toHex = ([r, g, b]) => `#${[r, g, b].map((n) => n.toString(16).padStart(2, "0")).join("")}`;
|
|
2838
|
+
function normalizeColor(input) {
|
|
2839
|
+
const rgb = parseColor(input);
|
|
2840
|
+
return rgb ? toHex(rgb) : null;
|
|
2841
|
+
}
|
|
2842
|
+
function luminance([r, g, b]) {
|
|
2843
|
+
const lin = (c) => {
|
|
2844
|
+
const s = c / 255;
|
|
2845
|
+
return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
|
|
2846
|
+
};
|
|
2847
|
+
return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
|
|
2848
|
+
}
|
|
2849
|
+
function contrastRatio(a, b) {
|
|
2850
|
+
const ca = parseColor(a);
|
|
2851
|
+
const cb = parseColor(b);
|
|
2852
|
+
if (!ca || !cb) return null;
|
|
2853
|
+
const [hi, lo] = [luminance(ca), luminance(cb)].sort((x, y) => y - x);
|
|
2854
|
+
return (hi + 0.05) / (lo + 0.05);
|
|
2855
|
+
}
|
|
2856
|
+
var TEXT_PALETTE = [
|
|
2857
|
+
{ name: "colorBlack", value: "#1b1f24" },
|
|
2858
|
+
{ name: "colorDarkGray", value: "#57606a" },
|
|
2859
|
+
{ name: "colorDarkRed", value: "#b3261e" },
|
|
2860
|
+
{ name: "colorDarkOrange", value: "#9a4a00" },
|
|
2861
|
+
{ name: "colorOlive", value: "#6b5b00" },
|
|
2862
|
+
{ name: "colorDarkGreen", value: "#146c2e" },
|
|
2863
|
+
{ name: "colorTeal", value: "#006a6a" },
|
|
2864
|
+
{ name: "colorBlue", value: "#0b57d0" },
|
|
2865
|
+
{ name: "colorPurple", value: "#6f2dbd" },
|
|
2866
|
+
{ name: "colorMagenta", value: "#a1185d" }
|
|
2867
|
+
];
|
|
2868
|
+
var HIGHLIGHT_PALETTE = [
|
|
2869
|
+
{ name: "colorYellow", value: "#fff2a8" },
|
|
2870
|
+
{ name: "colorGreen", value: "#c9f2d0" },
|
|
2871
|
+
{ name: "colorBlue", value: "#d3e3fd" },
|
|
2872
|
+
{ name: "colorPink", value: "#ffd8e6" },
|
|
2873
|
+
{ name: "colorOrange", value: "#ffe0b8" },
|
|
2874
|
+
{ name: "colorPurple", value: "#e8dcff" },
|
|
2875
|
+
{ name: "colorGray", value: "#e6e8eb" }
|
|
2876
|
+
];
|
|
2877
|
+
var NOISE_TEXT = /* @__PURE__ */ new Set(["#000000", "#1b1f24"]);
|
|
2878
|
+
var NOISE_TEXT_RAW = /^(inherit|initial|unset|currentcolor|windowtext|auto|black)$/i;
|
|
2879
|
+
var NOISE_BG = /^(transparent|inherit|initial|unset|none|window|white|#fff(fff)?)$/i;
|
|
2880
|
+
function applyColor(type, color) {
|
|
2881
|
+
return (state, dispatch) => {
|
|
2882
|
+
const value = color === null ? null : normalizeColor(color);
|
|
2883
|
+
if (color !== null && !value) return false;
|
|
2884
|
+
const { from, to, empty } = state.selection;
|
|
2885
|
+
const tr = state.tr;
|
|
2886
|
+
if (empty) {
|
|
2887
|
+
tr.removeStoredMark(type);
|
|
2888
|
+
if (value) tr.addStoredMark(type.create({ color: value }));
|
|
2889
|
+
} else {
|
|
2890
|
+
let applicable = false;
|
|
2891
|
+
state.doc.nodesBetween(from, to, (node, pos, parent) => {
|
|
2892
|
+
if (!node.isInline || !parent?.type.allowsMarkType(type)) return true;
|
|
2893
|
+
applicable = true;
|
|
2894
|
+
const start = Math.max(pos, from);
|
|
2895
|
+
const end = Math.min(pos + node.nodeSize, to);
|
|
2896
|
+
tr.removeMark(start, end, type);
|
|
2897
|
+
if (value) tr.addMark(start, end, type.create({ color: value }));
|
|
2898
|
+
return true;
|
|
2899
|
+
});
|
|
2900
|
+
if (!applicable) return false;
|
|
2901
|
+
}
|
|
2902
|
+
if (dispatch) dispatch(tr);
|
|
2903
|
+
return true;
|
|
2904
|
+
};
|
|
2905
|
+
}
|
|
2906
|
+
var styledView = (tag, prop) => (mark) => {
|
|
2907
|
+
const dom = document.createElement(tag);
|
|
2908
|
+
const color = normalizeColor(mark.attrs.color);
|
|
2909
|
+
if (color) dom.style.setProperty(prop, color);
|
|
2910
|
+
return { dom };
|
|
2911
|
+
};
|
|
2912
|
+
var TextColor = defineExtension({
|
|
2913
|
+
name: "textColor",
|
|
2914
|
+
defaultOptions: { palette: TEXT_PALETTE },
|
|
2915
|
+
marks: () => ({
|
|
2916
|
+
textColor: {
|
|
2917
|
+
attrs: { color: { default: null } },
|
|
2918
|
+
parseDOM: [
|
|
2919
|
+
{
|
|
2920
|
+
style: "color",
|
|
2921
|
+
getAttrs: (raw) => {
|
|
2922
|
+
if (NOISE_TEXT_RAW.test(raw.trim())) return false;
|
|
2923
|
+
const color = normalizeColor(raw);
|
|
2924
|
+
return color && !NOISE_TEXT.has(color) ? { color } : false;
|
|
2925
|
+
}
|
|
2926
|
+
},
|
|
2927
|
+
{
|
|
2928
|
+
tag: "font[color]",
|
|
2929
|
+
getAttrs: (el) => {
|
|
2930
|
+
const color = normalizeColor(el.getAttribute("color"));
|
|
2931
|
+
return color && !NOISE_TEXT.has(color) ? { color } : false;
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2934
|
+
],
|
|
2935
|
+
toDOM: (mark) => {
|
|
2936
|
+
const color = normalizeColor(mark.attrs.color);
|
|
2937
|
+
return ["span", color ? { style: `color: ${color}` } : {}, 0];
|
|
2938
|
+
}
|
|
2939
|
+
}
|
|
2940
|
+
}),
|
|
2941
|
+
commands: ({ schema }) => ({
|
|
2942
|
+
setTextColor: (color) => applyColor(schema.marks.textColor, color),
|
|
2943
|
+
unsetTextColor: () => applyColor(schema.marks.textColor, null)
|
|
2944
|
+
}),
|
|
2945
|
+
plugins: () => [new Plugin({ props: { markViews: { textColor: styledView("span", "color") } } })]
|
|
2946
|
+
});
|
|
2947
|
+
var Highlight = defineExtension({
|
|
2948
|
+
name: "highlight",
|
|
2949
|
+
defaultOptions: { palette: HIGHLIGHT_PALETTE },
|
|
2950
|
+
marks: () => ({
|
|
2951
|
+
highlight: {
|
|
2952
|
+
attrs: { color: { default: null } },
|
|
2953
|
+
parseDOM: [
|
|
2954
|
+
{
|
|
2955
|
+
tag: "mark",
|
|
2956
|
+
getAttrs: (el) => ({
|
|
2957
|
+
color: normalizeColor(el.style.backgroundColor || el.getAttribute("data-color")) ?? null
|
|
2958
|
+
})
|
|
2959
|
+
},
|
|
2960
|
+
...["background-color", "background"].map((style) => ({
|
|
2961
|
+
style,
|
|
2962
|
+
getAttrs: (raw) => {
|
|
2963
|
+
const value = raw.trim();
|
|
2964
|
+
if (NOISE_BG.test(value)) return false;
|
|
2965
|
+
const color = normalizeColor(value);
|
|
2966
|
+
return color && color !== "#ffffff" ? { color } : false;
|
|
2967
|
+
}
|
|
2968
|
+
}))
|
|
2969
|
+
],
|
|
2970
|
+
// <mark> is semantic: assistive tech can report "highlighted" text.
|
|
2971
|
+
toDOM: (mark) => {
|
|
2972
|
+
const color = normalizeColor(mark.attrs.color);
|
|
2973
|
+
return ["mark", color ? { style: `background-color: ${color}` } : {}, 0];
|
|
2974
|
+
}
|
|
2975
|
+
}
|
|
2976
|
+
}),
|
|
2977
|
+
commands: ({ schema, options }) => {
|
|
2978
|
+
const type = schema.marks.highlight;
|
|
2979
|
+
return {
|
|
2980
|
+
setHighlight: (color) => applyColor(type, color ?? options.palette[0]?.value ?? "#fff2a8"),
|
|
2981
|
+
unsetHighlight: () => applyColor(type, null),
|
|
2982
|
+
toggleHighlight: (color) => (state, dispatch, view) => {
|
|
2983
|
+
const { from, to, empty, $from } = state.selection;
|
|
2984
|
+
const active = empty ? !!type.isInSet(state.storedMarks ?? $from.marks()) : state.doc.rangeHasMark(from, to, type);
|
|
2985
|
+
return applyColor(type, active ? null : color ?? options.palette[0]?.value ?? "#fff2a8")(state, dispatch);
|
|
2986
|
+
}
|
|
2987
|
+
};
|
|
2988
|
+
},
|
|
2989
|
+
keymap: ({ editor }) => ({
|
|
2990
|
+
"Mod-Shift-h": () => editor.commands.toggleHighlight()
|
|
2991
|
+
}),
|
|
2992
|
+
plugins: () => [new Plugin({ props: { markViews: { highlight: styledView("mark", "background-color") } } })]
|
|
2993
|
+
});
|
|
2994
|
+
var IntlSegmenter = Intl.Segmenter;
|
|
2995
|
+
var cache = /* @__PURE__ */ new Map();
|
|
2996
|
+
function segmenter(lang, granularity) {
|
|
2997
|
+
if (!IntlSegmenter) return null;
|
|
2998
|
+
const key = `${lang ?? ""}|${granularity}`;
|
|
2999
|
+
let s = cache.get(key);
|
|
3000
|
+
if (!s) {
|
|
3001
|
+
try {
|
|
3002
|
+
s = new IntlSegmenter(lang, { granularity });
|
|
3003
|
+
} catch {
|
|
3004
|
+
s = new IntlSegmenter(void 0, { granularity });
|
|
3005
|
+
}
|
|
3006
|
+
cache.set(key, s);
|
|
3007
|
+
}
|
|
3008
|
+
return s;
|
|
3009
|
+
}
|
|
3010
|
+
function countCharacters(text, lang) {
|
|
3011
|
+
const seg = segmenter(lang, "grapheme");
|
|
3012
|
+
if (!seg) return [...text].length;
|
|
3013
|
+
let n = 0;
|
|
3014
|
+
for (const segment of seg.segment(text)) if (segment) n++;
|
|
3015
|
+
return n;
|
|
3016
|
+
}
|
|
3017
|
+
function countWords(text, lang) {
|
|
3018
|
+
const seg = segmenter(lang, "word");
|
|
3019
|
+
if (!seg) return text.split(/\s+/).filter(Boolean).length;
|
|
3020
|
+
let n = 0;
|
|
3021
|
+
for (const s of seg.segment(text)) if (s.isWordLike) n++;
|
|
3022
|
+
return n;
|
|
3023
|
+
}
|
|
3024
|
+
var docText = (doc) => doc.textBetween(0, doc.content.size, "\n", "\n");
|
|
3025
|
+
var characterCountKey = new PluginKey("owe-character-count");
|
|
3026
|
+
var CharacterCount = defineExtension({
|
|
3027
|
+
name: "characterCount",
|
|
3028
|
+
defaultOptions: { limit: null },
|
|
3029
|
+
plugins: ({ options, editor }) => [
|
|
3030
|
+
new Plugin({
|
|
3031
|
+
key: characterCountKey,
|
|
3032
|
+
filterTransaction(tr, state) {
|
|
3033
|
+
const limit = options.limit;
|
|
3034
|
+
if (!limit || !tr.docChanged || tr.getMeta("addToHistory") === false) return true;
|
|
3035
|
+
const before = countCharacters(docText(state.doc));
|
|
3036
|
+
const after = countCharacters(docText(tr.doc));
|
|
3037
|
+
if (after <= limit || after <= before) return true;
|
|
3038
|
+
editor.announce(editor.t("charLimitReached"), "assertive");
|
|
3039
|
+
return false;
|
|
3040
|
+
}
|
|
3041
|
+
})
|
|
3042
|
+
]
|
|
3043
|
+
});
|
|
3044
|
+
|
|
3045
|
+
// src/extensions/starter-kit.ts
|
|
3046
|
+
var parts = [
|
|
3047
|
+
["link", Link],
|
|
3048
|
+
["heading", Heading],
|
|
3049
|
+
["blockquote", Blockquote],
|
|
3050
|
+
["codeBlock", CodeBlock],
|
|
3051
|
+
["hardBreak", HardBreak],
|
|
3052
|
+
["horizontalRule", HorizontalRule],
|
|
3053
|
+
["bulletList", BulletList],
|
|
3054
|
+
["orderedList", OrderedList],
|
|
3055
|
+
["taskList", TaskList],
|
|
3056
|
+
["image", Image],
|
|
3057
|
+
["table", Table],
|
|
3058
|
+
["bold", Bold],
|
|
3059
|
+
["italic", Italic],
|
|
3060
|
+
["underline", Underline],
|
|
3061
|
+
["strike", Strike],
|
|
3062
|
+
["code", Code],
|
|
3063
|
+
["subscript", Subscript],
|
|
3064
|
+
["superscript", Superscript],
|
|
3065
|
+
["textColor", TextColor],
|
|
3066
|
+
["highlight", Highlight],
|
|
3067
|
+
["textAlign", TextAlign],
|
|
3068
|
+
["textDirection", TextDirection],
|
|
3069
|
+
["history", History],
|
|
3070
|
+
["dropcursor", Dropcursor],
|
|
3071
|
+
["gapcursor", Gapcursor],
|
|
3072
|
+
["findReplace", FindReplace],
|
|
3073
|
+
["characterCount", CharacterCount]
|
|
3074
|
+
];
|
|
3075
|
+
var StarterKit = defineExtension({
|
|
3076
|
+
name: "starterKit",
|
|
3077
|
+
defaultOptions: {},
|
|
3078
|
+
extensions: (options) => [
|
|
3079
|
+
Document,
|
|
3080
|
+
Text,
|
|
3081
|
+
Paragraph,
|
|
3082
|
+
...parts.filter(([key]) => options[key] !== false).map(([key, ext]) => {
|
|
3083
|
+
const config2 = options[key];
|
|
3084
|
+
return config2 ? ext.configure(config2) : ext;
|
|
3085
|
+
})
|
|
3086
|
+
]
|
|
3087
|
+
});
|
|
3088
|
+
|
|
3089
|
+
// src/extensions/html-support.ts
|
|
3090
|
+
var HTML_CONTAINERS = [
|
|
3091
|
+
"div",
|
|
3092
|
+
"section",
|
|
3093
|
+
"article",
|
|
3094
|
+
"aside",
|
|
3095
|
+
"header",
|
|
3096
|
+
"footer",
|
|
3097
|
+
"nav",
|
|
3098
|
+
"main",
|
|
3099
|
+
"address",
|
|
3100
|
+
"hgroup",
|
|
3101
|
+
"details",
|
|
3102
|
+
"dl",
|
|
3103
|
+
"dd",
|
|
3104
|
+
"center",
|
|
3105
|
+
"search"
|
|
3106
|
+
];
|
|
3107
|
+
var HTML_TEXTBLOCKS = ["summary", "dt"];
|
|
3108
|
+
var HTML_INLINE = [
|
|
3109
|
+
"span",
|
|
3110
|
+
"abbr",
|
|
3111
|
+
"cite",
|
|
3112
|
+
"dfn",
|
|
3113
|
+
"kbd",
|
|
3114
|
+
"mark",
|
|
3115
|
+
"q",
|
|
3116
|
+
"small",
|
|
3117
|
+
"time",
|
|
3118
|
+
"var",
|
|
3119
|
+
"samp",
|
|
3120
|
+
"bdi",
|
|
3121
|
+
"bdo",
|
|
3122
|
+
"data",
|
|
3123
|
+
"big",
|
|
3124
|
+
"ruby",
|
|
3125
|
+
"rt",
|
|
3126
|
+
"rp"
|
|
3127
|
+
];
|
|
3128
|
+
var KNOWN_TYPES = [
|
|
3129
|
+
"paragraph",
|
|
3130
|
+
"heading",
|
|
3131
|
+
"blockquote",
|
|
3132
|
+
"bulletList",
|
|
3133
|
+
"orderedList",
|
|
3134
|
+
"listItem",
|
|
3135
|
+
"taskList",
|
|
3136
|
+
"taskItem",
|
|
3137
|
+
"codeBlock",
|
|
3138
|
+
"horizontalRule",
|
|
3139
|
+
"image",
|
|
3140
|
+
"table",
|
|
3141
|
+
"tableRow",
|
|
3142
|
+
"tableCell",
|
|
3143
|
+
"tableHeader",
|
|
3144
|
+
"bold",
|
|
3145
|
+
"italic",
|
|
3146
|
+
"underline",
|
|
3147
|
+
"strike",
|
|
3148
|
+
"code",
|
|
3149
|
+
"subscript",
|
|
3150
|
+
"superscript",
|
|
3151
|
+
"link"
|
|
3152
|
+
];
|
|
3153
|
+
var HTML_SAFE_RULES = [
|
|
3154
|
+
{
|
|
3155
|
+
name: new RegExp(`^(${[...HTML_CONTAINERS, ...HTML_TEXTBLOCKS].join("|")})$`),
|
|
3156
|
+
attributes: ["data-*", "id", "title", "lang", "aria-label", "aria-labelledby", "aria-describedby", "role"],
|
|
3157
|
+
classes: true
|
|
3158
|
+
},
|
|
3159
|
+
{
|
|
3160
|
+
name: new RegExp(`^(${HTML_INLINE.join("|")})$`),
|
|
3161
|
+
attributes: ["data-*", "title", "lang", "dir", "datetime", "value", "cite", "translate"],
|
|
3162
|
+
classes: true
|
|
3163
|
+
},
|
|
3164
|
+
{
|
|
3165
|
+
name: /^(p|h[1-6]|blockquote|ul|ol|li|table|tr|td|th|pre|code|figure|img|a|strong|em|u|s|sub|sup|hr)$/,
|
|
3166
|
+
attributes: ["data-*", "id", "lang", "translate"],
|
|
3167
|
+
classes: true
|
|
3168
|
+
}
|
|
3169
|
+
];
|
|
3170
|
+
var RESERVED = /* @__PURE__ */ new Set([
|
|
3171
|
+
"src",
|
|
3172
|
+
"srcset",
|
|
3173
|
+
"href",
|
|
3174
|
+
"alt",
|
|
3175
|
+
"width",
|
|
3176
|
+
"height",
|
|
3177
|
+
"start",
|
|
3178
|
+
"type",
|
|
3179
|
+
"dir",
|
|
3180
|
+
"colspan",
|
|
3181
|
+
"rowspan",
|
|
3182
|
+
"colwidth",
|
|
3183
|
+
"scope",
|
|
3184
|
+
"title",
|
|
3185
|
+
"data-type",
|
|
3186
|
+
"data-checked",
|
|
3187
|
+
"data-colwidth",
|
|
3188
|
+
"data-language",
|
|
3189
|
+
"open"
|
|
3190
|
+
]);
|
|
3191
|
+
var DENIED = /^(on|xmlns|xlink)|^(srcdoc|formaction|action|ping|background|tabindex|accesskey|autofocus|contenteditable|draggable|hidden|inert|popover|popovertarget|is|slot|part|nonce|style|class)$|^data-(pm|owe)-/i;
|
|
3192
|
+
var URL_ATTRS2 = /* @__PURE__ */ new Set(["cite", "longdesc"]);
|
|
3193
|
+
var ROLES = /* @__PURE__ */ new Set(["note", "region", "group", "figure", "doc-note", "doc-example", "doc-tip", "doc-pullquote", "doc-epigraph", "presentation", "none"]);
|
|
3194
|
+
var CLASS = /^-?[A-Za-z_][\w-]{0,63}$/;
|
|
3195
|
+
var ID = /^[A-Za-z][\w:.-]{0,63}$/;
|
|
3196
|
+
var LANG = /^[A-Za-z]{2,3}(-[A-Za-z0-9]{1,8})*$/;
|
|
3197
|
+
var UNSAFE_VALUE = /[\u0000-\u001f]/;
|
|
3198
|
+
var UNSAFE_CSS = /url\s*\(|expression\s*\(|javascript:|@import|[<>\\]|behavior\s*:|-moz-binding/i;
|
|
3199
|
+
var matchesName = (pattern, name) => typeof pattern === "string" ? pattern.endsWith("*") ? name.startsWith(pattern.slice(0, -1).toLowerCase()) : pattern.toLowerCase() === name : pattern.test(name);
|
|
3200
|
+
var allowedBy = (list, name) => list === true || !!list && list.some((p) => matchesName(p, name));
|
|
3201
|
+
function createHtmlFilter(options, policy = {}) {
|
|
3202
|
+
const allow = options.allow === "safe" ? HTML_SAFE_RULES : options.allow;
|
|
3203
|
+
const deny = options.disallow;
|
|
3204
|
+
const rulesFor = (tag, rules) => rules.filter((r) => matchesName(r.name, tag));
|
|
3205
|
+
const tagAllowed = (tag) => rulesFor(tag, allow).length > 0 && !rulesFor(tag, deny).some((r) => !r.attributes && !r.classes && !r.styles);
|
|
3206
|
+
function permits(tag, kind, name) {
|
|
3207
|
+
const yes = rulesFor(tag, allow).some((r) => allowedBy(r[kind], name));
|
|
3208
|
+
const no = rulesFor(tag, deny).some((r) => allowedBy(r[kind], name));
|
|
3209
|
+
return yes && !no;
|
|
3210
|
+
}
|
|
3211
|
+
function validValue(name, value) {
|
|
3212
|
+
if (value.length > 2e3 || UNSAFE_VALUE.test(value)) return null;
|
|
3213
|
+
if (URL_ATTRS2.has(name)) return sanitizeUrl(value, policy);
|
|
3214
|
+
if (name === "id") {
|
|
3215
|
+
if (!ID.test(value)) return null;
|
|
3216
|
+
if (typeof window !== "undefined" && (value in window || value in document)) return null;
|
|
3217
|
+
}
|
|
3218
|
+
if (name === "lang" && !LANG.test(value)) return null;
|
|
3219
|
+
if (name === "role" && !ROLES.has(value)) return null;
|
|
3220
|
+
if (name === "dir" && !/^(ltr|rtl|auto)$/.test(value)) return null;
|
|
3221
|
+
return value;
|
|
3222
|
+
}
|
|
3223
|
+
function filter(tag, read, names, reserved) {
|
|
3224
|
+
const out = {};
|
|
3225
|
+
for (const raw of names) {
|
|
3226
|
+
const name = raw.toLowerCase();
|
|
3227
|
+
const value = read(raw);
|
|
3228
|
+
if (value === null) continue;
|
|
3229
|
+
if (name === "class") {
|
|
3230
|
+
const classes = value.split(/\s+/).filter((c) => c && CLASS.test(c) && !/^(owe-|ProseMirror)/.test(c) && permits(tag, "classes", c));
|
|
3231
|
+
if (classes.length) out.class = classes.join(" ");
|
|
3232
|
+
continue;
|
|
3233
|
+
}
|
|
3234
|
+
if (name === "style") {
|
|
3235
|
+
const decls = [];
|
|
3236
|
+
for (const decl of value.split(";")) {
|
|
3237
|
+
const i = decl.indexOf(":");
|
|
3238
|
+
if (i < 1) continue;
|
|
3239
|
+
const prop = decl.slice(0, i).trim().toLowerCase();
|
|
3240
|
+
const val = decl.slice(i + 1).trim();
|
|
3241
|
+
if (!/^-?[a-z][a-z-]*$/.test(prop) || !val || UNSAFE_CSS.test(val) || val.length > 200) continue;
|
|
3242
|
+
if (permits(tag, "styles", prop)) decls.push(`${prop}: ${val}`);
|
|
3243
|
+
}
|
|
3244
|
+
if (decls.length) out.style = decls.join("; ");
|
|
3245
|
+
continue;
|
|
3246
|
+
}
|
|
3247
|
+
if (DENIED.test(name) || reserved && RESERVED.has(name)) continue;
|
|
3248
|
+
if (!permits(tag, "attributes", name)) continue;
|
|
3249
|
+
const valid = validValue(name, value);
|
|
3250
|
+
if (valid !== null) out[name] = valid;
|
|
3251
|
+
}
|
|
3252
|
+
return Object.keys(out).length ? out : null;
|
|
3253
|
+
}
|
|
3254
|
+
const fromElement = (el, reserved) => filter(el.tagName.toLowerCase(), (n) => el.getAttribute(n), el.getAttributeNames(), reserved);
|
|
3255
|
+
const revalidate = (tag, value, reserved) => {
|
|
3256
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
3257
|
+
const stored = value;
|
|
3258
|
+
const names = Object.keys(stored).filter((k) => Object.prototype.hasOwnProperty.call(stored, k));
|
|
3259
|
+
return filter(tag, (n) => typeof stored[n] === "string" ? stored[n] : null, names, reserved);
|
|
3260
|
+
};
|
|
3261
|
+
return { tagAllowed, fromElement, revalidate };
|
|
3262
|
+
}
|
|
3263
|
+
var NODE_TAG = {
|
|
3264
|
+
paragraph: "p",
|
|
3265
|
+
blockquote: "blockquote",
|
|
3266
|
+
bulletList: "ul",
|
|
3267
|
+
orderedList: "ol",
|
|
3268
|
+
listItem: "li",
|
|
3269
|
+
taskList: "ul",
|
|
3270
|
+
taskItem: "li",
|
|
3271
|
+
codeBlock: "pre",
|
|
3272
|
+
horizontalRule: "hr",
|
|
3273
|
+
image: "img",
|
|
3274
|
+
table: "table",
|
|
3275
|
+
tableRow: "tr",
|
|
3276
|
+
tableCell: "td",
|
|
3277
|
+
tableHeader: "th",
|
|
3278
|
+
bold: "strong",
|
|
3279
|
+
italic: "em",
|
|
3280
|
+
underline: "u",
|
|
3281
|
+
strike: "s",
|
|
3282
|
+
code: "code",
|
|
3283
|
+
subscript: "sub",
|
|
3284
|
+
superscript: "sup",
|
|
3285
|
+
link: "a"
|
|
3286
|
+
};
|
|
3287
|
+
var tagOf = (owner) => owner.type.name === "heading" ? `h${owner.attrs.level}` : NODE_TAG[owner.type.name] ?? "div";
|
|
3288
|
+
var HtmlSupport = defineExtension({
|
|
3289
|
+
name: "htmlSupport",
|
|
3290
|
+
priority: 90,
|
|
3291
|
+
defaultOptions: { allow: "safe", disallow: [] },
|
|
3292
|
+
nodes: (options, { urlPolicy }) => {
|
|
3293
|
+
const f = createHtmlFilter(options, urlPolicy);
|
|
3294
|
+
const containers = HTML_CONTAINERS.filter(f.tagAllowed);
|
|
3295
|
+
const textblocks = HTML_TEXTBLOCKS.filter(f.tagAllowed);
|
|
3296
|
+
const nodeSpec = (tags, content) => ({
|
|
3297
|
+
attrs: { tag: { default: tags[0] ?? "div" }, attributes: { default: null } },
|
|
3298
|
+
content,
|
|
3299
|
+
group: "block",
|
|
3300
|
+
parseDOM: tags.map((tag) => ({
|
|
3301
|
+
tag,
|
|
3302
|
+
priority: 40,
|
|
3303
|
+
getAttrs: (el) => ({ tag, attributes: f.fromElement(el, false) })
|
|
3304
|
+
})),
|
|
3305
|
+
toDOM: (node) => {
|
|
3306
|
+
const tag = tags.includes(node.attrs.tag) ? node.attrs.tag : "div";
|
|
3307
|
+
const attrs = { ...f.revalidate(tag, node.attrs.attributes, false) ?? {} };
|
|
3308
|
+
if (tag === "details") attrs.open = "";
|
|
3309
|
+
return [tag, attrs, 0];
|
|
3310
|
+
}
|
|
3311
|
+
});
|
|
3312
|
+
return {
|
|
3313
|
+
...containers.length ? { htmlContainer: nodeSpec(containers, "block+") } : {},
|
|
3314
|
+
...textblocks.length ? { htmlTextblock: nodeSpec(textblocks, "inline*") } : {}
|
|
3315
|
+
};
|
|
3316
|
+
},
|
|
3317
|
+
marks: (options, { urlPolicy }) => {
|
|
3318
|
+
const f = createHtmlFilter(options, urlPolicy);
|
|
3319
|
+
const tags = HTML_INLINE.filter(f.tagAllowed);
|
|
3320
|
+
if (!tags.length) return {};
|
|
3321
|
+
return {
|
|
3322
|
+
htmlInline: {
|
|
3323
|
+
attrs: { tag: { default: "span" }, attributes: { default: null } },
|
|
3324
|
+
excludes: "",
|
|
3325
|
+
// different wrappers (and nested spans) can coexist
|
|
3326
|
+
parseDOM: tags.map((tag) => ({
|
|
3327
|
+
tag,
|
|
3328
|
+
priority: 40,
|
|
3329
|
+
getAttrs: (el) => {
|
|
3330
|
+
const attributes = f.fromElement(el, false);
|
|
3331
|
+
if (tag === "span" && !attributes) return false;
|
|
3332
|
+
return { tag, attributes };
|
|
3333
|
+
}
|
|
3334
|
+
})),
|
|
3335
|
+
toDOM: (mark) => {
|
|
3336
|
+
const tag = tags.includes(mark.attrs.tag) ? mark.attrs.tag : "span";
|
|
3337
|
+
return [tag, f.revalidate(tag, mark.attrs.attributes, false) ?? {}, 0];
|
|
3338
|
+
}
|
|
3339
|
+
}
|
|
3340
|
+
};
|
|
3341
|
+
},
|
|
3342
|
+
globalAttributes: (options) => {
|
|
3343
|
+
const f = createHtmlFilter(options);
|
|
3344
|
+
return [
|
|
3345
|
+
{
|
|
3346
|
+
types: KNOWN_TYPES,
|
|
3347
|
+
attributes: {
|
|
3348
|
+
htmlAttributes: {
|
|
3349
|
+
default: null,
|
|
3350
|
+
parseHTML: (el) => f.fromElement(el, true),
|
|
3351
|
+
renderHTML: (value, owner) => f.revalidate(tagOf(owner), value, true)
|
|
3352
|
+
}
|
|
3353
|
+
}
|
|
3354
|
+
}
|
|
3355
|
+
];
|
|
3356
|
+
}
|
|
3357
|
+
});
|
|
3358
|
+
|
|
3359
|
+
export { Blockquote, Bold, BulletList, CharacterCount, Code, CodeBlock, DEFAULT_PROTOCOLS, Document, Dropcursor, FORBIDDEN_ATTRS, FORBIDDEN_TAGS, FindReplace, Gapcursor, HIGHLIGHT_PALETTE, HTML_CONTAINERS, HTML_INLINE, HTML_SAFE_RULES, HTML_TEXTBLOCKS, HardBreak, Heading, Highlight, History, HorizontalRule, HtmlSupport, Image, Italic, Link, ListItem, OrderedList, Paragraph, StarterKit, Strike, Subscript, Superscript, TEXT_PALETTE, TRUSTED_TYPES_POLICY_NAME, Table, TaskItem, TaskList, Text, TextAlign, TextColor, TextDirection, Underline, arabicLabels, builtInLanguages, cleanPastedHTML, contrastRatio, countCharacters, countWords, createEditor, createHtmlFilter, createI18n, defaultLabels, defineExtension, docText, docToHTML, escapeAttr, escapeText, findMatches, findParent, getAttributes, getSearchState, h, isActive, isMac, luminance, markInputRule, markRange, normalizeColor, parseColor, resolveExtensions, resolveLanguage, sanitizeHTML, sanitizePastedHTML, sanitizeUrl, searchKey, sequence, setBlockAttrs, setTrustedTypesPolicy, stripInertBlocks, svg, toHex, toggleBlockType, toggleList, toggleMarkCommand, toggleWrap, uid };
|
|
3360
|
+
//# sourceMappingURL=chunk-CDPXFV6R.js.map
|
|
3361
|
+
//# sourceMappingURL=chunk-CDPXFV6R.js.map
|