mnfst 0.5.232 → 0.5.233
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/lib/manifest.appwrite.data.js +1 -1
- package/lib/manifest.d.ts +4 -2
- package/lib/manifest.data.js +1 -1
- package/lib/manifest.edit.js +96 -30
- package/lib/manifest.integrity.json +6 -6
- package/lib/manifest.js +44 -3
- package/lib/manifest.rich.text.js +524 -141
- package/lib/manifest.sw.js +1 -1
- package/package.json +1 -1
|
@@ -19,6 +19,37 @@
|
|
|
19
19
|
const MARKS = { STRONG: '**', B: '**', EM: '*', I: '*', S: '~~', DEL: '~~', CODE: '`' };
|
|
20
20
|
const INLINE_OK = new Set([...Object.keys(INLINE_TAGS).map(t => t.toUpperCase()), 'BR', 'WBR', 'A', 'IMG']);
|
|
21
21
|
|
|
22
|
+
/* ---- Adjacent marks ----
|
|
23
|
+
Neighbouring inline elements alike (same tag and attributes; for markdown, the same
|
|
24
|
+
delimiter) are one: merged, children first, the selection kept on the nodes it was in. */
|
|
25
|
+
const sameInline = (a, b) => !!b && b.nodeType === 1 && a.tagName === b.tagName
|
|
26
|
+
&& a.attributes.length === b.attributes.length && [...a.attributes].every(x => b.getAttribute(x.name) === x.value);
|
|
27
|
+
const sameDelimiter = (a, b) => !!b && b.nodeType === 1 && (a.tagName === 'A' ? sameInline(a, b) : !!MARKS[a.tagName] && MARKS[a.tagName] === MARKS[b.tagName]);
|
|
28
|
+
function mergeAdjacent(root, same = sameInline) {
|
|
29
|
+
const r = root.ownerDocument === document && root.isConnected ? range() : null;
|
|
30
|
+
const keep = r && root.contains(r.startContainer) ? [r.startContainer, r.startOffset, r.endContainer, r.endOffset] : null;
|
|
31
|
+
let any = false;
|
|
32
|
+
const walk = (p) => {
|
|
33
|
+
let merged = false;
|
|
34
|
+
[...p.children].forEach(walk);
|
|
35
|
+
for (let c = p.firstChild; c; c = c.nextSibling) {
|
|
36
|
+
while (c.nodeType === 1 && INLINE_OK.has(c.tagName) && !/^(BR|WBR|IMG)$/.test(c.tagName)) {
|
|
37
|
+
const gaps = [];
|
|
38
|
+
let n = c.nextSibling;
|
|
39
|
+
while (n && n.nodeType === 3 && !n.length) { gaps.push(n); n = n.nextSibling; } // empty text between them is nothing
|
|
40
|
+
if (!same(c, n)) break;
|
|
41
|
+
const base = c.childNodes.length;
|
|
42
|
+
if (keep) for (const i of [0, 2]) { if (gaps.includes(keep[i])) { keep[i] = c; keep[i + 1] = base; } else if (keep[i] === n) { keep[i] = c; keep[i + 1] += base; } }
|
|
43
|
+
gaps.forEach(g => g.remove()); c.append(...n.childNodes); n.remove(); merged = any = true;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (merged) walk(p);
|
|
47
|
+
};
|
|
48
|
+
walk(root);
|
|
49
|
+
if (any && keep && keep[0].isConnected && keep[2].isConnected) try { const nr = document.createRange(); nr.setStart(keep[0], keep[1]); nr.setEnd(keep[2], keep[3]); const s = sel(); s.removeAllRanges(); s.addRange(nr); } catch { }
|
|
50
|
+
return any;
|
|
51
|
+
}
|
|
52
|
+
|
|
22
53
|
/* ---- URL policy ---- */
|
|
23
54
|
// No script schemes; data: only as an image.
|
|
24
55
|
function unsafeUrl(url, image) {
|
|
@@ -32,16 +63,44 @@
|
|
|
32
63
|
|
|
33
64
|
/* ---- HTML → Markdown ---- */
|
|
34
65
|
const ENTITY = /&(?=#?[a-z0-9]+;)/gi;
|
|
35
|
-
const
|
|
66
|
+
const MD_PUNCT = /[!-\/:-@\[-`{-~\u2000-\u206f\u2e00-\u2e7f]/;
|
|
67
|
+
// An _ between letters or digits cannot open or close emphasis: it is left as written
|
|
68
|
+
const esc = (t) => t.replace(/[\\`*_~[\]<>]/g, (c, i) => c === '_' && /[\p{L}\p{N}]/u.test(t[i - 1] || '') && /[\p{L}\p{N}]/u.test(t[i + 1] || '') ? c : '\\' + c).replace(ENTITY, '\\&');
|
|
36
69
|
// Line starts that would parse as a heading, list or rule
|
|
37
70
|
const escLead = (text) => text.split('\n').map(l => l
|
|
38
71
|
.replace(/^(\s*)(#{1,6})(?=\s|$)/, '$1\\$2')
|
|
39
72
|
.replace(/^(\s*)([-+])(?=\s)/, '$1\\$2')
|
|
40
73
|
.replace(/^(\s*)-(?=-{2,}\s*$)/, '$1\\-')
|
|
41
74
|
.replace(/^(\s*)(\d+)\.(?=\s)/, '$1$2\\.')).join('\n');
|
|
42
|
-
|
|
75
|
+
// A link destination that reads back to the same URL: spaces and angle brackets encoded,
|
|
76
|
+
// backslashes and unbalanced parentheses escaped (balanced ones stay, as CommonMark reads them)
|
|
77
|
+
const mdUrl = (u) => {
|
|
78
|
+
const s = String(u).replace(/[\s<>]/g, c => '%' + c.charCodeAt(0).toString(16).toUpperCase().padStart(2, '0'));
|
|
79
|
+
const open = [], loose = new Set();
|
|
80
|
+
for (let i = 0; i < s.length; i++) { if (s[i] === '(') open.push(i); else if (s[i] === ')') { if (open.length) open.pop(); else loose.add(i); } }
|
|
81
|
+
open.forEach(i => loose.add(i));
|
|
82
|
+
return s.split('').map((c, i) => c === '\\' ? '\\\\' : loose.has(i) ? '\\' + c : c).join('').replace(ENTITY, '\\&');
|
|
83
|
+
};
|
|
43
84
|
|
|
44
|
-
|
|
85
|
+
// `ctx`: the characters just outside this content, and whether an `_` mark encloses it.
|
|
86
|
+
// em/strong use _/__ where no word character is outside them (and none encloses them), so
|
|
87
|
+
// their delimiters cannot pair with an inner * run; *, ** otherwise.
|
|
88
|
+
const wordChar = (c) => !!c && !/\s/.test(c) && !MD_PUNCT.test(c);
|
|
89
|
+
const textEdge = (n, last) => {
|
|
90
|
+
if (!n) return null;
|
|
91
|
+
if (n.nodeType === 3) return n.nodeValue ? (last ? n.nodeValue[n.nodeValue.length - 1] : n.nodeValue[0]) : textEdge(last ? n.previousSibling : n.nextSibling, last);
|
|
92
|
+
if (n.nodeType !== 1) return textEdge(last ? n.previousSibling : n.nextSibling, last);
|
|
93
|
+
if (n.tagName === 'BR') return '\n';
|
|
94
|
+
if (n.tagName === 'IMG') return last ? ')' : '!';
|
|
95
|
+
if (MARKS[n.tagName] || n.tagName === 'A') return last ? '*' : '*'; // a delimiter or bracket: punctuation
|
|
96
|
+
const t = n.textContent; return t ? (last ? t[t.length - 1] : t[0]) : textEdge(last ? n.previousSibling : n.nextSibling, last);
|
|
97
|
+
};
|
|
98
|
+
// A * mark inside with text right against it: its delimiters could close an enclosing * mark
|
|
99
|
+
const riskyInside = (n) => [...n.querySelectorAll('strong, b, em, i')].some(d => {
|
|
100
|
+
const before = d.previousSibling ? textEdge(d.previousSibling, true) : null, after = d.nextSibling ? textEdge(d.nextSibling, false) : null;
|
|
101
|
+
return (before != null && !/\s/.test(before)) || (after != null && !/\s/.test(after));
|
|
102
|
+
});
|
|
103
|
+
function inlineRaw(node, ctx) {
|
|
45
104
|
let out = '';
|
|
46
105
|
node.childNodes.forEach(n => {
|
|
47
106
|
if (n.nodeType === 3) { out += esc(n.textContent); return; }
|
|
@@ -49,15 +108,87 @@
|
|
|
49
108
|
const tag = n.tagName;
|
|
50
109
|
if (tag === 'BR') { out += ' \n'; return; }
|
|
51
110
|
if (tag === 'IMG') { const src = n.getAttribute('src') || ''; out += src && !unsafeUrl(src, true) ? `})` : ''; return; }
|
|
52
|
-
if (tag === 'A'
|
|
53
|
-
|
|
54
|
-
if (
|
|
55
|
-
|
|
56
|
-
|
|
111
|
+
if (tag === 'A' && /(^|[^\\])!$/.test(out) && n.getAttribute('href')) out = out.slice(0, -1) + '\\!'; // not an image
|
|
112
|
+
if (tag === 'A' && !n.hasAttribute('title') && n.children.length === 0 && /^[a-z][a-z0-9+.-]{1,31}:[^\s<>]*$/i.test(n.getAttribute('href') || '') && n.textContent === n.getAttribute('href') && !unsafeUrl(n.textContent)) { out += '<' + n.textContent + '>'; return; }
|
|
113
|
+
if (tag === 'A') {
|
|
114
|
+
const h = n.getAttribute('href') || '', title = n.getAttribute('title');
|
|
115
|
+
const t = title != null ? ` "${title.replace(/[\\"]/g, '\\$&').replace(/\n/g, ' ')}"` : '';
|
|
116
|
+
out += h && !unsafeUrl(h) ? `[${inlineRaw(n, { ...ctx, prev: '[', next: ']', under: ctx.under, star: ctx.star })}](${mdUrl(h)}${t})` : inlineRaw(n, ctx);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
let m = MARKS[tag];
|
|
120
|
+
if (!m) { out += inlineRaw(n, ctx); return; }
|
|
121
|
+
if (tag === 'CODE') {
|
|
122
|
+
// A fence longer than any backtick run inside; padded where the code starts or ends with one
|
|
123
|
+
const code = n.textContent; if (!code) return;
|
|
124
|
+
const fence = '`'.repeat(Math.max(0, ...(code.match(/`+/g) || []).map(r => r.length)) + 1);
|
|
125
|
+
const pad = /^`|`$/.test(code) || (/^ /.test(code) && / $/.test(code) && code.trim()) ? ' ' : '';
|
|
126
|
+
out += fence + pad + code + pad + fence;
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (!n.textContent.trim()) { out += inlineRaw(n, ctx); return; }
|
|
130
|
+
let body = inlineRaw(n, { ...ctx, prev: m[0], next: m[0], under: ctx.under });
|
|
131
|
+
// Delimiters hug the text: spaces go outside
|
|
132
|
+
let [, lead, core, trail] = body.match(/^(\s*)([\s\S]*?)(\s*)$/);
|
|
133
|
+
// _ where * would touch text (it could pair with an enclosing *), or encloses a mark that does
|
|
134
|
+
const after = textEdge(n.nextSibling, false);
|
|
135
|
+
const prev = lead ? ' ' : (out ? out[out.length - 1] : ctx.prev), next = trail ? ' ' : (after == null ? ctx.next : after);
|
|
136
|
+
const nonspace = /\S/.test(prev || '') || /\S/.test(next || '');
|
|
137
|
+
if (m[0] === '*' && (ctx.html === 'all' || (ctx.html === 'touch' && nonspace))) {
|
|
138
|
+
const t = tag === 'STRONG' || tag === 'B' ? 'strong' : 'em';
|
|
139
|
+
[, lead, core, trail] = inlineRaw(n, { ...ctx, prev: '>', next: '<', star: false }).match(/^(\s*)([\s\S]*?)(\s*)$/);
|
|
140
|
+
out += lead + `<${t}>` + core + `</${t}>` + trail;
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const allowed = m[0] === '*' && !ctx.under && !wordChar(prev) && !wordChar(next) && (!!lead || !/(^|[^\\])_$/.test(out));
|
|
144
|
+
// * unless it would not read back: right after another * run, around a * mark with text
|
|
145
|
+
// against it, or with text against it inside a * mark
|
|
146
|
+
const touching = (!lead && /\S$/.test(out)) || (!trail && after != null && /\S/.test(after));
|
|
147
|
+
if (allowed && ((!lead && /(^|[^\\])\*$/.test(out)) || riskyInside(n) || (ctx.star && touching))) {
|
|
148
|
+
m = m.replace(/\*/g, '_');
|
|
149
|
+
[, lead, core, trail] = inlineRaw(n, { ...ctx, prev: '_', next: '_', under: true, star: ctx.star }).match(/^(\s*)([\s\S]*?)(\s*)$/);
|
|
150
|
+
} else if (m[0] === '*' && ctx.star && touching) {
|
|
151
|
+
// Inside a * mark with text against it, a * run could close the enclosing one: no
|
|
152
|
+
// markdown form says this, so it is written as HTML (which markdown carries)
|
|
153
|
+
const t = tag === 'STRONG' || tag === 'B' ? 'strong' : 'em';
|
|
154
|
+
[, lead, core, trail] = inlineRaw(n, { ...ctx, prev: '>', next: '<', under: ctx.under, star: false }).match(/^(\s*)([\s\S]*?)(\s*)$/);
|
|
155
|
+
out += lead + `<${t}>` + core + `</${t}>` + trail;
|
|
156
|
+
return;
|
|
157
|
+
} else if (m[0] === '*') [, lead, core, trail] = inlineRaw(n, { ...ctx, prev: '*', next: '*', under: ctx.under, star: true }).match(/^(\s*)([\s\S]*?)(\s*)$/);
|
|
158
|
+
out += lead + m + core + m + trail;
|
|
57
159
|
});
|
|
58
160
|
return out;
|
|
59
161
|
}
|
|
60
162
|
|
|
163
|
+
// Inline markdown for a node, checked by reading it back: in the rare arrangement where a
|
|
164
|
+
// * or _ run would pair differently, em/strong touching text go as HTML, else all of them
|
|
165
|
+
function inline(node) {
|
|
166
|
+
const want = shapeOf(node), ctx = { prev: '', next: '', under: false, star: false, html: '' };
|
|
167
|
+
let first = null;
|
|
168
|
+
for (const html of ['', 'touch', 'all']) {
|
|
169
|
+
const md = inlineRaw(node, { ...ctx, html });
|
|
170
|
+
if (first == null) first = md;
|
|
171
|
+
const t = document.createElement('template'); t.innerHTML = inlineHtml(md);
|
|
172
|
+
if (shapeOf(t.content) === want) return md;
|
|
173
|
+
}
|
|
174
|
+
return first; // what no form can carry (an unsafe link, say) is not a reason to change notation
|
|
175
|
+
}
|
|
176
|
+
// Every character with the formatting it shows (strong/em/strike on spaces show nothing)
|
|
177
|
+
function shapeOf(root) {
|
|
178
|
+
const out = [], KIND = { STRONG: 'b', B: 'b', EM: 'i', I: 'i', S: 's', DEL: 's', CODE: 'c' };
|
|
179
|
+
const walk = (n, f) => n.childNodes.forEach(c => {
|
|
180
|
+
if (c.nodeType === 3) { for (const ch of c.nodeValue) out.push(ch + (/\s/.test(ch) ? f.filter(x => x[0] !== 'b' && x[0] !== 'i' && x[0] !== 's') : f).join()); return; }
|
|
181
|
+
if (c.nodeType !== 1) return;
|
|
182
|
+
if (c.tagName === 'BR') { out.push('\n'); return; }
|
|
183
|
+
if (c.tagName === 'IMG') { const src = c.getAttribute('src') || ''; if (src && !unsafeUrl(src, true)) out.push('img:' + src + '|' + (c.getAttribute('alt') || '')); return; }
|
|
184
|
+
const href = c.tagName === 'A' ? c.getAttribute('href') || '' : '';
|
|
185
|
+
const k = KIND[c.tagName], h = href && !unsafeUrl(href) ? 'a:' + href : null;
|
|
186
|
+
walk(c, [...new Set([...f, ...(k ? [k] : []), ...(h ? [h] : [])])].sort());
|
|
187
|
+
});
|
|
188
|
+
walk(root, []);
|
|
189
|
+
return out.join('\u0001').replace(/^(\s(\u0001|$))+|(\u0001\s)+$/g, '');
|
|
190
|
+
}
|
|
191
|
+
|
|
61
192
|
const preText = (n) => [...n.childNodes].map(c =>
|
|
62
193
|
c.nodeName === 'BR' ? '\n' : c.nodeType === 3 ? c.nodeValue : preText(c)).join('');
|
|
63
194
|
|
|
@@ -66,7 +197,7 @@
|
|
|
66
197
|
if (node.nodeType !== 1) return '';
|
|
67
198
|
const tag = node.tagName;
|
|
68
199
|
if (tag === 'HR') return '---';
|
|
69
|
-
if (tag === 'PRE') return '
|
|
200
|
+
if (tag === 'PRE') return '```' + (node.getAttribute('data-lang') || '').replace(/[^\w+#.-]/g, '') + '\n' + preText(node).replace(/\n+$/, '') + '\n```';
|
|
70
201
|
if (/^H[1-6]$/.test(tag)) return '#'.repeat(+tag[1]) + ' ' + inline(node).trim();
|
|
71
202
|
if (tag === 'BLOCKQUOTE') return blocks(node, depth).split('\n').map(l => ('> ' + l).trimEnd()).join('\n');
|
|
72
203
|
if (tag === 'TABLE') return tableLines(node).map(cells => escLead(cells.map(cellMarkdown).join(CELL_SEP))).join('\n\n');
|
|
@@ -110,11 +241,18 @@
|
|
|
110
241
|
return parts.join('\n\n');
|
|
111
242
|
}
|
|
112
243
|
|
|
113
|
-
|
|
244
|
+
// Markdown carries a mark once: one inside another with the same delimiter adds nothing
|
|
245
|
+
const flattenMarks = (root) => [...root.querySelectorAll('*')].forEach(el => {
|
|
246
|
+
const m = MARKS[el.tagName]; if (!m) return;
|
|
247
|
+
for (let p = el.parentElement; p && p !== root; p = p.parentElement) if (MARKS[p.tagName] === m) { el.replaceWith(...el.childNodes); return; }
|
|
248
|
+
});
|
|
249
|
+
const mdCopy = (root) => { const c = root.cloneNode(true); flattenMarks(c); mergeAdjacent(c, sameDelimiter); return c; };
|
|
250
|
+
const toMarkdown = (root) => blocks(mdCopy(root), 0).replace(/\n{3,}/g, '\n\n').trim();
|
|
114
251
|
// Single-line values never parse block syntax, so line starts need no escape
|
|
115
252
|
const inlineMarkdown = (root) => {
|
|
116
253
|
const flat = document.createElement('div');
|
|
117
254
|
flat.innerHTML = flattenLines(root.innerHTML, true);
|
|
255
|
+
flattenMarks(flat); mergeAdjacent(flat, sameDelimiter);
|
|
118
256
|
return inline(flat).trim();
|
|
119
257
|
};
|
|
120
258
|
|
|
@@ -181,19 +319,146 @@
|
|
|
181
319
|
/* ---- Markdown → HTML ---- */
|
|
182
320
|
const escHtml = (s) => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
|
183
321
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
322
|
+
/* Inline markdown: a small CommonMark-style reader. Code spans, links and images are found
|
|
323
|
+
left to right; runs of * _ ~ are delimiters, matched innermost first with CommonMark's
|
|
324
|
+
rules (the rule of 3 included). `*` and `~~` open before any non-space and close after
|
|
325
|
+
one (CommonMark also weighs punctuation); `_` keeps CommonMark's word-boundary rules. */
|
|
326
|
+
const PUNCT = /[!-\/:-@\[-`{-~ -⸀-]/;
|
|
327
|
+
const isSpace = (c) => !c || /\s/.test(c);
|
|
328
|
+
// opts.raw: backslashes are text and no inline HTML is read (markdown being typed);
|
|
329
|
+
// opts.after: the character after the source, for the flanking of a run that ends it
|
|
330
|
+
function inlineHtml(src, opts = {}) {
|
|
331
|
+
const s = String(src == null ? '' : src);
|
|
332
|
+
const parts = [], delims = [];
|
|
333
|
+
let text = '';
|
|
334
|
+
const flush = () => { if (text) { parts.push(escHtml(text)); text = ''; } };
|
|
335
|
+
// A code span's closing run: the next backtick run of the same length
|
|
336
|
+
const codeEnd = (from, len) => {
|
|
337
|
+
for (let j = from; j < s.length;) {
|
|
338
|
+
const k = s.indexOf('`', j); if (k < 0) return -1;
|
|
339
|
+
const n = s.slice(k).match(/^`+/)[0].length;
|
|
340
|
+
if (n === len) return k;
|
|
341
|
+
j = k + n;
|
|
342
|
+
}
|
|
343
|
+
return -1;
|
|
344
|
+
};
|
|
345
|
+
// A link destination from the "(" at `at`: <…>, or balanced parentheses; an optional title is skipped
|
|
346
|
+
const destination = (at) => {
|
|
347
|
+
let j = at + 1, url;
|
|
348
|
+
while (s[j] === ' ' || s[j] === '\t') j++;
|
|
349
|
+
if (s[j] === '<') {
|
|
350
|
+
const e = s.indexOf('>', j); if (e < 0 || /[\n<]/.test(s.slice(j + 1, e))) return null;
|
|
351
|
+
url = s.slice(j + 1, e); j = e + 1;
|
|
352
|
+
} else {
|
|
353
|
+
const from = j;
|
|
354
|
+
for (let depth = 0; j < s.length; j++) {
|
|
355
|
+
const c = s[j];
|
|
356
|
+
if (c === '\\' && /[!-\/:-@\[-`{-~]/.test(s[j + 1] || '')) { j++; continue; }
|
|
357
|
+
if (/[\s\x00-\x1f]/.test(c)) break;
|
|
358
|
+
if (c === '(') depth++;
|
|
359
|
+
else if (c === ')') { if (!depth) break; depth--; }
|
|
360
|
+
}
|
|
361
|
+
url = s.slice(from, j);
|
|
362
|
+
}
|
|
363
|
+
const gap = j;
|
|
364
|
+
while (/[ \t\n]/.test(s[j] || '')) j++;
|
|
365
|
+
const q = s[j];
|
|
366
|
+
let title = null;
|
|
367
|
+
if (j > gap && (q === '"' || q === "'" || q === '(')) {
|
|
368
|
+
const close = q === '(' ? ')' : q;
|
|
369
|
+
let e = j + 1; for (; e < s.length && s[e] !== close; e++) if (s[e] === '\\') e++;
|
|
370
|
+
if (e >= s.length) return null;
|
|
371
|
+
title = s.slice(j + 1, e).replace(/\\([!-\/:-@\[-`{-~])/g, '$1');
|
|
372
|
+
j = e + 1; while (/[ \t\n]/.test(s[j] || '')) j++;
|
|
373
|
+
}
|
|
374
|
+
if (s[j] !== ')') return null;
|
|
375
|
+
return { url: url.replace(/\\([!-\/:-@\[-`{-~])/g, '$1'), title, end: j + 1 };
|
|
376
|
+
};
|
|
377
|
+
// A bracket's matching close, skipping escapes and code spans
|
|
378
|
+
const closeBracket = (from) => {
|
|
379
|
+
for (let i = from, depth = 0; i < s.length; i++) {
|
|
380
|
+
const c = s[i];
|
|
381
|
+
if (c === '\\') { i++; continue; }
|
|
382
|
+
if (c === '`') { const n = s.slice(i).match(/^`+/)[0].length, e = codeEnd(i + n, n); i = (e < 0 ? i + n : e + n) - 1; continue; }
|
|
383
|
+
if (c === '[') depth++;
|
|
384
|
+
else if (c === ']') { if (!depth) return i; depth--; }
|
|
385
|
+
}
|
|
386
|
+
return -1;
|
|
387
|
+
};
|
|
388
|
+
for (let i = 0; i < s.length;) {
|
|
389
|
+
const c = s[i];
|
|
390
|
+
if (c === '\\' && !opts.raw && i + 1 < s.length && /[!-\/:-@\[-`{-~]/.test(s[i + 1])) { text += s[i + 1]; i += 2; continue; }
|
|
391
|
+
if (c === '`') {
|
|
392
|
+
const run = s.slice(i).match(/^`+/)[0], end = codeEnd(i + run.length, run.length);
|
|
393
|
+
if (end < 0) { text += run; i += run.length; continue; }
|
|
394
|
+
let code = s.slice(i + run.length, end).replace(/\n/g, ' ');
|
|
395
|
+
if (code.length > 1 && code[0] === ' ' && code[code.length - 1] === ' ' && code.trim()) code = code.slice(1, -1);
|
|
396
|
+
flush(); parts.push(`<code>${escHtml(code)}</code>`);
|
|
397
|
+
i = end + run.length; continue;
|
|
398
|
+
}
|
|
399
|
+
if (c === '[' || (c === '!' && s[i + 1] === '[')) {
|
|
400
|
+
const img = c === '!', open = i + (img ? 2 : 1), close = closeBracket(open);
|
|
401
|
+
const m = close > 0 && s[close + 1] === '(' && destination(close + 1);
|
|
402
|
+
if (m) {
|
|
403
|
+
const url = m.url, body = s.slice(open, close);
|
|
404
|
+
flush();
|
|
405
|
+
if (img) parts.push(unsafeUrl(url, true) ? '' : `<img src="${escHtml(url)}" alt="${escHtml(body.replace(/\\([!-\/:-@\[-`{-~])/g, '$1'))}">`);
|
|
406
|
+
else parts.push(unsafeUrl(url) || !url ? inlineHtml(body, { raw: opts.raw }) : `<a href="${escHtml(url)}"${m.title != null ? ` title="${escHtml(m.title)}"` : ''}>${inlineHtml(body, { raw: opts.raw })}</a>`);
|
|
407
|
+
i = m.end; continue;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
// The inline HTML this writer uses where markdown has no form (raw tags, as CommonMark passes them)
|
|
411
|
+
if (c === '<' && !opts.raw) {
|
|
412
|
+
// Inline HTML for marks markdown has (as CommonMark passes it); other tags stay text
|
|
413
|
+
const t = s.slice(i).match(/^<(\/?)(strong|em|b|i|s|del|code)>/i);
|
|
414
|
+
if (t) { const tag = { b: 'strong', i: 'em', del: 's' }[t[2].toLowerCase()] || t[2].toLowerCase(); flush(); parts.push(`<${t[1]}${tag}>`); i += t[0].length; continue; }
|
|
415
|
+
// An autolink: <scheme:…>
|
|
416
|
+
const a = s.slice(i).match(/^<([a-z][a-z0-9+.-]{1,31}:[^\s<>]*)>/i);
|
|
417
|
+
if (a && !unsafeUrl(a[1])) { flush(); parts.push(`<a href="${escHtml(a[1])}">${escHtml(a[1])}</a>`); i += a[0].length; continue; }
|
|
418
|
+
}
|
|
419
|
+
if (c === '\n' && / {2,}$/.test(text)) { text = text.replace(/ +$/, ''); flush(); parts.push('<br>'); i++; continue; }
|
|
420
|
+
if (c === '*' || c === '_' || c === '~') {
|
|
421
|
+
const run = s.slice(i).match(c === '*' ? /^\*+/ : c === '_' ? /^_+/ : /^~+/)[0];
|
|
422
|
+
const before = i ? s[i - 1] : '', after = i + run.length < s.length ? s[i + run.length] : (opts.after || '');
|
|
423
|
+
let open, close;
|
|
424
|
+
if (c === '_') {
|
|
425
|
+
const left = !isSpace(after) && (!PUNCT.test(after) || isSpace(before) || PUNCT.test(before));
|
|
426
|
+
const right = !isSpace(before) && (!PUNCT.test(before) || isSpace(after) || PUNCT.test(after));
|
|
427
|
+
open = left && (!right || PUNCT.test(before)); close = right && (!left || PUNCT.test(after));
|
|
428
|
+
} else { open = !isSpace(after); close = !isSpace(before); }
|
|
429
|
+
if (c === '~' && run.length !== 2) { open = close = false; }
|
|
430
|
+
if (!open && !close) { text += run; i += run.length; continue; }
|
|
431
|
+
flush();
|
|
432
|
+
const d = { ch: c, count: run.length, orig: run.length, open, close };
|
|
433
|
+
parts.push(d); delims.push(d);
|
|
434
|
+
i += run.length; continue;
|
|
435
|
+
}
|
|
436
|
+
text += c; i++;
|
|
437
|
+
}
|
|
438
|
+
flush();
|
|
439
|
+
// Innermost first: each closer takes the nearest opener it can pair with
|
|
440
|
+
for (let ci = 0; ci < delims.length; ci++) {
|
|
441
|
+
const closer = delims[ci];
|
|
442
|
+
while (closer.close && closer.count) {
|
|
443
|
+
let oi = ci - 1;
|
|
444
|
+
for (; oi >= 0; oi--) {
|
|
445
|
+
const o = delims[oi];
|
|
446
|
+
if (o.ch !== closer.ch || !o.open || !o.count) continue;
|
|
447
|
+
if (closer.ch === '~') { if (o.count === 2 && closer.count === 2) break; continue; }
|
|
448
|
+
if ((o.close || closer.open) && (o.orig + closer.orig) % 3 === 0 && !(o.orig % 3 === 0 && closer.orig % 3 === 0)) continue;
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
if (oi < 0) break;
|
|
452
|
+
const opener = delims[oi], use = closer.ch === '~' ? 2 : (opener.count >= 2 && closer.count >= 2 ? 2 : 1);
|
|
453
|
+
const tag = closer.ch === '~' ? 's' : use === 2 ? 'strong' : 'em';
|
|
454
|
+
const a = parts.indexOf(opener), b = parts.indexOf(closer);
|
|
455
|
+
const inner = parts.splice(a + 1, b - a - 1).map(p => typeof p === 'string' ? p : p.ch.repeat(p.count)).join('');
|
|
456
|
+
parts.splice(a + 1, 0, `<${tag}>${inner}</${tag}>`);
|
|
457
|
+
for (let k = oi + 1; k < ci; k++) delims[k].count = 0; // delimiters inside are text now (already rendered)
|
|
458
|
+
opener.count -= use; closer.count -= use;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return parts.map(p => typeof p === 'string' ? p : p.ch.repeat(p.count)).join('');
|
|
197
462
|
}
|
|
198
463
|
|
|
199
464
|
// Four spaces per nesting level.
|
|
@@ -220,7 +485,7 @@
|
|
|
220
485
|
function fromMarkdown(md) {
|
|
221
486
|
const lines = String(md == null ? '' : md).replace(/\r\n?/g, '\n').split('\n');
|
|
222
487
|
const out = [];
|
|
223
|
-
let para = [], list = null, quote = null, fence = null;
|
|
488
|
+
let para = [], list = null, quote = null, fence = null, lang = '';
|
|
224
489
|
const flushPara = () => { if (para.length) { out.push(`<p>${inlineHtml(para.join('\n'))}</p>`); para = []; } };
|
|
225
490
|
const flushList = () => { if (!list) return; for (let i = 0; i < list.length;) { const [html, next] = renderList(list, i, list[i].depth); out.push(html); i = next > i ? next : i + 1; } list = null; };
|
|
226
491
|
const flushQuote = () => { if (quote) { out.push(`<blockquote>${fromMarkdown(quote.join('\n'))}</blockquote>`); quote = null; } };
|
|
@@ -228,8 +493,8 @@
|
|
|
228
493
|
|
|
229
494
|
for (const raw of lines) {
|
|
230
495
|
const line = raw.replace(/[^\S\n]+$/, m => m.length >= 2 ? ' ' : '');
|
|
231
|
-
if (fence !== null) { if (
|
|
232
|
-
if (/^```/.test(line)) { flushAll(); fence = []; continue; }
|
|
496
|
+
if (fence !== null) { if (/^```\s*$/.test(line)) { out.push(`<pre${lang ? ` data-lang="${escHtml(lang)}"` : ''}><code>${escHtml(fence.join('\n'))}</code></pre>`); fence = null; } else fence.push(raw); continue; }
|
|
497
|
+
if (/^```/.test(line)) { flushAll(); fence = []; lang = (line.slice(3).trim().match(/^[\w+#.-]+/) || [''])[0]; continue; }
|
|
233
498
|
if (!line.trim()) { flushAll(); continue; }
|
|
234
499
|
const h = line.match(/^(#{1,6})\s+(.*)$/);
|
|
235
500
|
if (h) { flushAll(); out.push(`<h${h[1].length}>${inlineHtml(h[2])}</h${h[1].length}>`); continue; }
|
|
@@ -245,7 +510,7 @@
|
|
|
245
510
|
flushList(); flushQuote();
|
|
246
511
|
para.push(line);
|
|
247
512
|
}
|
|
248
|
-
if (fence) out.push(`<pre><code>${escHtml(fence.join('\n'))}</code></pre>`);
|
|
513
|
+
if (fence) out.push(`<pre${lang ? ` data-lang="${escHtml(lang)}"` : ''}><code>${escHtml(fence.join('\n'))}</code></pre>`);
|
|
249
514
|
flushAll();
|
|
250
515
|
return out.join('');
|
|
251
516
|
}
|
|
@@ -268,9 +533,10 @@
|
|
|
268
533
|
const meta = INLINE_TAGS[c.tagName.toLowerCase()];
|
|
269
534
|
[...c.attributes].forEach(a => {
|
|
270
535
|
const n = a.name.toLowerCase();
|
|
271
|
-
if (c.tagName === 'A' && n === 'href' && !unsafeUrl(a.value)) return;
|
|
536
|
+
if (c.tagName === 'A' && ((n === 'href' && !unsafeUrl(a.value)) || n === 'title')) return;
|
|
272
537
|
if (c.tagName === 'IMG' && (n === 'alt' || (n === 'src' && !unsafeUrl(a.value, true)))) return;
|
|
273
538
|
if (c.tagName === 'INPUT' && (n === 'type' || n === 'checked')) return;
|
|
539
|
+
if (c.tagName === 'PRE' && n === 'data-lang' && /^[\w+#.-]{1,40}$/.test(a.value)) return; // a code fence's language
|
|
274
540
|
if (/^(TD|TH)$/.test(c.tagName) && (n === 'colspan' || n === 'rowspan') && /^[1-9]\d{0,2}$/.test(a.value)) return;
|
|
275
541
|
if (meta && meta.attr === n && (n !== 'dir' || /^(ltr|rtl|auto)$/i.test(a.value))) return;
|
|
276
542
|
if (n === 'style' && keepStyle && !/expression|url\s*\(|javascript:|@import/i.test(a.value)) return;
|
|
@@ -278,6 +544,7 @@
|
|
|
278
544
|
});
|
|
279
545
|
});
|
|
280
546
|
walk(t.content);
|
|
547
|
+
mergeAdjacent(t.content);
|
|
281
548
|
return t.innerHTML;
|
|
282
549
|
}
|
|
283
550
|
|
|
@@ -392,6 +659,28 @@
|
|
|
392
659
|
return pre.toString().length;
|
|
393
660
|
}
|
|
394
661
|
|
|
662
|
+
// The selection as text offsets in root, and back
|
|
663
|
+
function textSpan(root) {
|
|
664
|
+
const r = range(); if (!r || !root.contains(r.startContainer) || !root.contains(r.endContainer)) return null;
|
|
665
|
+
const at = (node, off) => { const pre = document.createRange(); pre.selectNodeContents(root); pre.setEnd(node, off); return pre.toString().length; };
|
|
666
|
+
return [at(r.startContainer, r.startOffset), at(r.endContainer, r.endOffset)];
|
|
667
|
+
}
|
|
668
|
+
function selectSpan(root, [from, to]) {
|
|
669
|
+
const point = (offset, end) => {
|
|
670
|
+
const w = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
671
|
+
let seen = 0, last = null;
|
|
672
|
+
for (let n = w.nextNode(); n; n = w.nextNode()) {
|
|
673
|
+
last = n;
|
|
674
|
+
if (end ? seen + n.length >= offset : seen + n.length > offset) return [n, offset - seen];
|
|
675
|
+
seen += n.length;
|
|
676
|
+
}
|
|
677
|
+
return last ? [last, last.length] : [root, 0];
|
|
678
|
+
};
|
|
679
|
+
const r = document.createRange(), a = point(from, false), b = point(to, true);
|
|
680
|
+
r.setStart(a[0], a[1]); r.setEnd(b[0], b[1]);
|
|
681
|
+
const s = sel(); s.removeAllRanges(); s.addRange(r);
|
|
682
|
+
}
|
|
683
|
+
|
|
395
684
|
function caretIn(root, offset) {
|
|
396
685
|
const s = sel(), r = document.createRange();
|
|
397
686
|
if (offset == null) offset = 0;
|
|
@@ -630,7 +919,9 @@
|
|
|
630
919
|
const cellAt = () => ancestor('td, th') || (cellDrag && cellDrag.anchor.isConnected ? cellDrag.anchor : null);
|
|
631
920
|
const tableAt = () => { const c = cellAt(); return c && c.closest('table'); };
|
|
632
921
|
const rowsOf = (table) => [...table.rows];
|
|
633
|
-
const emptyCell = () => { const td = document.createElement(
|
|
922
|
+
const emptyCell = (tag = 'td') => { const td = document.createElement(tag); td.appendChild(document.createElement('br')); return td; };
|
|
923
|
+
// A header row (in a thead, or of header cells only) is padded with header cells
|
|
924
|
+
const rowCellTag = (row) => row.parentElement && row.parentElement.tagName === 'THEAD' || (row.cells.length && [...row.cells].every(c => c.tagName === 'TH')) ? 'th' : 'td';
|
|
634
925
|
|
|
635
926
|
// Occupancy grid: every slot maps to the cell covering it, spans included
|
|
636
927
|
function tableGrid(table) {
|
|
@@ -674,7 +965,7 @@
|
|
|
674
965
|
const width = Math.max(0, ...grid.map(r => r.length));
|
|
675
966
|
rows.forEach((row, r) => {
|
|
676
967
|
const have = (grid[r] || []).filter(Boolean).length;
|
|
677
|
-
for (let i = have; i < width; i++) row.appendChild(emptyCell());
|
|
968
|
+
for (let i = have; i < width; i++) row.appendChild(emptyCell(rowCellTag(row)));
|
|
678
969
|
});
|
|
679
970
|
if (!table.querySelector('tbody, thead')) {
|
|
680
971
|
const body = document.createElement('tbody');
|
|
@@ -783,13 +1074,14 @@
|
|
|
783
1074
|
const cols = cell.colSpan || 1, rows = cell.rowSpan || 1;
|
|
784
1075
|
if (cols < 2 && rows < 2) return;
|
|
785
1076
|
cell.removeAttribute('colspan'); cell.removeAttribute('rowspan');
|
|
786
|
-
|
|
1077
|
+
// A header cell splits into header cells; rows below take their row's kind
|
|
1078
|
+
for (let i = 1; i < cols; i++) cell.after(emptyCell(cell.tagName.toLowerCase()));
|
|
787
1079
|
if (rows > 1) {
|
|
788
1080
|
const table = cell.closest('table'), { row, col } = gridPosition(cell);
|
|
789
1081
|
for (let r = 1; r < rows; r++) {
|
|
790
1082
|
const target = rowsOf(table)[row + r]; if (!target) break;
|
|
791
1083
|
const at = cellCovering(target, col);
|
|
792
|
-
const fresh = emptyCell();
|
|
1084
|
+
const fresh = emptyCell(rowCellTag(target));
|
|
793
1085
|
if (at) at.before(fresh); else target.appendChild(fresh);
|
|
794
1086
|
}
|
|
795
1087
|
}
|
|
@@ -1278,6 +1570,13 @@
|
|
|
1278
1570
|
if (!frag.lastChild) return;
|
|
1279
1571
|
record(area);
|
|
1280
1572
|
r.deleteContents();
|
|
1573
|
+
// A caret between a block field's blocks (an empty field clicked) goes into the block beside it
|
|
1574
|
+
const c0 = range();
|
|
1575
|
+
if (c0 && c0.startContainer === area && !(area._rt && area._rt.inline)) {
|
|
1576
|
+
const into = (b) => b && b.nodeType === 1 && /^(P|H[1-6]|BLOCKQUOTE|PRE)$/.test(b.tagName);
|
|
1577
|
+
const before = area.childNodes[c0.startOffset - 1], after = area.childNodes[c0.startOffset];
|
|
1578
|
+
if (into(before)) caretIn(before, before.textContent.length); else if (into(after)) caretIn(after, 0);
|
|
1579
|
+
}
|
|
1281
1580
|
// An empty block's placeholder <br> goes; at a block's start, so does the content's leading space
|
|
1282
1581
|
const holder = blockEl() || area;
|
|
1283
1582
|
if (!holder.textContent.length && !holder.querySelector('img')) holder.querySelectorAll('br').forEach(b => b.remove());
|
|
@@ -1287,7 +1586,16 @@
|
|
|
1287
1586
|
}
|
|
1288
1587
|
let end = frag.lastChild;
|
|
1289
1588
|
if (!end) return;
|
|
1589
|
+
const lastTop = end;
|
|
1290
1590
|
const cr = range(); if (cr && area.contains(cr.startContainer)) cr.insertNode(frag); else area.appendChild(frag);
|
|
1591
|
+
// Pasted content ending in a mark: the caret sits after it, as after a conversion
|
|
1592
|
+
if (lastTop.nodeType === 1 && INLINE_OK.has(lastTop.tagName) && !/^(BR|WBR|IMG)$/.test(lastTop.tagName) && lastTop.isConnected) {
|
|
1593
|
+
caretAt(tailOf(lastTop), 0);
|
|
1594
|
+
area._exit = lastTop;
|
|
1595
|
+
record(area);
|
|
1596
|
+
area.dispatchEvent(new Event('input', { bubbles: true }));
|
|
1597
|
+
return;
|
|
1598
|
+
}
|
|
1291
1599
|
// The caret ends inside the last text, never at an element boundary (an IME composition there is left behind)
|
|
1292
1600
|
const s = sel(), nr = document.createRange();
|
|
1293
1601
|
if (end.nodeType === 1) { const w = document.createTreeWalker(end, NodeFilter.SHOW_TEXT); let t = null; for (let n = w.nextNode(); n; n = w.nextNode()) t = n; if (t) end = t; }
|
|
@@ -1357,7 +1665,10 @@
|
|
|
1357
1665
|
const n = (v) => Math.max(1, Math.min(20, parseInt(v, 10) || 3));
|
|
1358
1666
|
const cols = n(parts[0]), rows = n(parts[1]);
|
|
1359
1667
|
const row = `<tr>${'<td><br></td>'.repeat(cols)}</tr>`;
|
|
1360
|
-
insertHTML(`<table><tbody>${row.repeat(rows)}</tbody></table><p><br></p>`);
|
|
1668
|
+
insertHTML(`<table data-rich-text-new><tbody>${row.repeat(rows)}</tbody></table><p><br></p>`);
|
|
1669
|
+
// The caret goes into the first cell, so typing and table commands start there
|
|
1670
|
+
const made = document.querySelector('table[data-rich-text-new]');
|
|
1671
|
+
if (made) { made.removeAttribute('data-rich-text-new'); const first = made.querySelector('td, th'); if (first) caretIn(first, 0); }
|
|
1361
1672
|
}
|
|
1362
1673
|
|
|
1363
1674
|
// A trailing table, rule or similar block gets a line after it to land on
|
|
@@ -1412,6 +1723,17 @@
|
|
|
1412
1723
|
const next = values && values[k];
|
|
1413
1724
|
if (next) area.style.setProperty(v, next); else area.style.removeProperty(v);
|
|
1414
1725
|
});
|
|
1726
|
+
const allKey = (area) => JSON.stringify(allStyle(area));
|
|
1727
|
+
// .all styles set from code: every history entry takes the change, so undo never removes it
|
|
1728
|
+
function rebaseAll(area, was, now) {
|
|
1729
|
+
const h = area._history; if (!h) return;
|
|
1730
|
+
const keys = [...new Set([...Object.keys(was), ...Object.keys(now)])].filter(k => was[k] !== now[k]);
|
|
1731
|
+
h.stack.forEach(e => {
|
|
1732
|
+
const o = e.all ? JSON.parse(e.all) : {};
|
|
1733
|
+
keys.forEach(k => { if (now[k]) o[k] = now[k]; else delete o[k]; });
|
|
1734
|
+
e.all = JSON.stringify(Object.fromEntries(Object.keys(ALL_VARS).filter(k => o[k]).map(k => [k, o[k]])));
|
|
1735
|
+
});
|
|
1736
|
+
}
|
|
1415
1737
|
|
|
1416
1738
|
/* ---- Command table ---- */
|
|
1417
1739
|
const COMMANDS = Object.create(null);
|
|
@@ -1592,20 +1914,10 @@
|
|
|
1592
1914
|
{ re: /^\d+\.\s+/, make: (b) => listify(b, 'ol') },
|
|
1593
1915
|
{ re: /^\[([ xX])\]\s+/, make: (b, m) => listify(b, 'ul', /[xX]/.test(m[1])) },
|
|
1594
1916
|
{ re: /^>\s+/, make: (b) => retagBlock(b, 'blockquote') },
|
|
1595
|
-
{ re:
|
|
1917
|
+
{ re: /^```([\w+#.-]{0,40})\s*$/, make: (b, m) => { const pre = retagBlock(b, 'pre'); if (m[1]) pre.setAttribute('data-lang', m[1]); return pre; } },
|
|
1596
1918
|
{ re: /^(-{3,}|\*{3,}|_{3,})\s*$/, make: (b) => { const hr = document.createElement('hr'); b.replaceWith(hr); return hr; } }
|
|
1597
1919
|
];
|
|
1598
1920
|
|
|
1599
|
-
const INLINE_RULES = [
|
|
1600
|
-
{ re: /\*\*([^*]+)\*\*/, tag: 'strong' },
|
|
1601
|
-
{ re: /__([^_]+)__/, tag: 'strong' },
|
|
1602
|
-
{ re: /(^|[^*])\*([^*\s][^*]*)\*/, tag: 'em', pre: 1, content: 2 },
|
|
1603
|
-
{ re: /~~([^~]+)~~/, tag: 'del' },
|
|
1604
|
-
{ re: /`([^`]+)`/, tag: 'code' },
|
|
1605
|
-
{ re: /!\[([^\]]*)\]\(([^)\s]+)\)/, tag: 'img', alt: 1, src: 2 },
|
|
1606
|
-
{ re: /\[([^\]]+)\]\(([^)\s]+)\)/, tag: 'a', href: 2 }
|
|
1607
|
-
];
|
|
1608
|
-
|
|
1609
1921
|
function retagBlock(b, tag) {
|
|
1610
1922
|
const n = document.createElement(tag);
|
|
1611
1923
|
while (b.firstChild) n.appendChild(b.firstChild);
|
|
@@ -1640,44 +1952,35 @@
|
|
|
1640
1952
|
}
|
|
1641
1953
|
}
|
|
1642
1954
|
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
let best = null;
|
|
1657
|
-
for (const rule of INLINE_RULES) {
|
|
1658
|
-
const m = text.match(rule.re); if (!m) continue;
|
|
1659
|
-
const skip = rule.pre ? m[rule.pre].length : 0;
|
|
1660
|
-
const start = m.index + skip;
|
|
1661
|
-
if (!best || start < best.start) best = { rule, m, start, len: m[0].length - skip };
|
|
1662
|
-
}
|
|
1663
|
-
return best;
|
|
1664
|
-
}
|
|
1665
|
-
|
|
1666
|
-
// Only a pattern ending exactly at the caret; earliest start wins (image over link)
|
|
1667
|
-
function endInline(text) {
|
|
1668
|
-
let best = null;
|
|
1669
|
-
for (const rule of INLINE_RULES) {
|
|
1670
|
-
const re = new RegExp(rule.re.source, 'g');
|
|
1671
|
-
let m;
|
|
1672
|
-
while ((m = re.exec(text))) {
|
|
1673
|
-
if (m.index + m[0].length !== text.length) continue;
|
|
1674
|
-
const skip = rule.pre ? m[rule.pre].length : 0;
|
|
1675
|
-
const start = m.index + skip;
|
|
1676
|
-
if (!best || start < best.start) best = { rule, m, start, len: m[0].length - skip };
|
|
1677
|
-
break;
|
|
1678
|
-
}
|
|
1955
|
+
// Markdown typed in a run of inline content (up to the caret, or all of it) re-read with the
|
|
1956
|
+
// markdown reader, so what converts while typing is what a saved value reads back as. Elements
|
|
1957
|
+
// already there go through as opaque characters. Null when nothing new would be made.
|
|
1958
|
+
const ATOM = 0xE000;
|
|
1959
|
+
function reparse(box, until, upto, after) {
|
|
1960
|
+
const kids = [...box.childNodes], end = until ? kids.indexOf(until) : kids.length;
|
|
1961
|
+
let from = 0;
|
|
1962
|
+
for (let i = 0; i < end; i++) if (kids[i].nodeType === 1 && (kids[i].tagName === 'BR' || BLOCK.includes(kids[i].tagName))) from = i + 1;
|
|
1963
|
+
const run = kids.slice(from, end), atoms = [];
|
|
1964
|
+
let src = '';
|
|
1965
|
+
for (const n of run) {
|
|
1966
|
+
if (n.nodeType === 3) { if (/[\uE000-\uF8FF]/.test(n.nodeValue)) return null; src += n.nodeValue; }
|
|
1967
|
+
else if (n.nodeType === 1) { if (atoms.length > 6000) return null; src += String.fromCharCode(ATOM + atoms.length); atoms.push(n); }
|
|
1679
1968
|
}
|
|
1680
|
-
|
|
1969
|
+
if (until) src += until.nodeValue.slice(0, upto);
|
|
1970
|
+
if (!/[*_~`\[]/.test(src)) return null;
|
|
1971
|
+
const t = document.createElement('template');
|
|
1972
|
+
t.innerHTML = inlineHtml(src, { raw: true, after });
|
|
1973
|
+
const made = [...t.content.querySelectorAll('*')];
|
|
1974
|
+
if (!made.length) return null;
|
|
1975
|
+
// Elements already there land whole and once, never in a code span or an attribute
|
|
1976
|
+
if (made.some(e => [...e.attributes].some(x => /[\uE000-\uF8FF]/.test(x.value)))) return null;
|
|
1977
|
+
const texts = []; const w = document.createTreeWalker(t.content, NodeFilter.SHOW_TEXT);
|
|
1978
|
+
for (let n = w.nextNode(); n; n = w.nextNode()) if (/[\uE000-\uF8FF]/.test(n.nodeValue)) { if (n.parentElement && n.parentElement.closest('code')) return null; texts.push(n); }
|
|
1979
|
+
const found = texts.flatMap(n => n.nodeValue.match(/[\uE000-\uF8FF]/g)).map(c => c.charCodeAt(0) - ATOM);
|
|
1980
|
+
if (found.length !== atoms.length || new Set(found).size !== atoms.length) return null;
|
|
1981
|
+
// The elements move in only when the conversion is taken
|
|
1982
|
+
const fill = () => texts.forEach(n => n.replaceWith(...n.nodeValue.split(/([\uE000-\uF8FF])/).filter(Boolean).map(p => p.length === 1 && p.charCodeAt(0) >= ATOM && p.charCodeAt(0) - ATOM < atoms.length ? atoms[p.charCodeAt(0) - ATOM] : document.createTextNode(p))));
|
|
1983
|
+
return { run, frag: t.content, fill };
|
|
1681
1984
|
}
|
|
1682
1985
|
|
|
1683
1986
|
function convertInlineAtCaret(area) {
|
|
@@ -1686,17 +1989,24 @@
|
|
|
1686
1989
|
if (!node || node.nodeType !== 3 || !node.parentElement) return false;
|
|
1687
1990
|
const host = node.parentElement.closest('code, pre, a');
|
|
1688
1991
|
if (host && area.contains(host)) return false;
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
const
|
|
1992
|
+
if (!/[*_~`)]/.test(node.nodeValue[r.startOffset - 1] || '')) return false;
|
|
1993
|
+
const next = node.nodeValue[r.startOffset] != null ? node.nodeValue[r.startOffset] : textEdge(node.nextSibling, false);
|
|
1994
|
+
const hit = reparse(node.parentElement, node, r.startOffset, next == null ? '' : next);
|
|
1995
|
+
// Only a construct closed by the character just typed, and whole: an opener with marker
|
|
1996
|
+
// left over (`**test*`, so far) waits for the rest of the closer
|
|
1997
|
+
const last = hit && hit.frag.lastChild, typed = node.nodeValue[r.startOffset - 1];
|
|
1998
|
+
if (!last || last.nodeType !== 1 || (!last.textContent && last.tagName !== 'IMG')) return false;
|
|
1999
|
+
const before = last.previousSibling;
|
|
2000
|
+
if (/[*_~]/.test(typed) && before && before.nodeType === 3 && before.nodeValue.endsWith(typed)) return false;
|
|
1692
2001
|
record(area);
|
|
1693
|
-
const tail = node.splitText(
|
|
1694
|
-
|
|
1695
|
-
|
|
2002
|
+
const tail = node.splitText(r.startOffset);
|
|
2003
|
+
hit.run.forEach(n => n.remove()); node.remove();
|
|
2004
|
+
hit.fill();
|
|
2005
|
+
tail.parentNode.insertBefore(hit.frag, tail);
|
|
1696
2006
|
const s = sel(), nr = document.createRange();
|
|
1697
2007
|
nr.setStart(tail, 0); nr.collapse(true);
|
|
1698
2008
|
s.removeAllRanges(); s.addRange(nr);
|
|
1699
|
-
area._exit =
|
|
2009
|
+
area._exit = last.tagName === 'IMG' ? null : last;
|
|
1700
2010
|
record(area);
|
|
1701
2011
|
return true;
|
|
1702
2012
|
}
|
|
@@ -1775,21 +2085,26 @@
|
|
|
1775
2085
|
return true;
|
|
1776
2086
|
}
|
|
1777
2087
|
|
|
2088
|
+
// A finished line's markdown, in each run of inline content (marks' own content first)
|
|
1778
2089
|
function convertInline(root) {
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
const tail = cur.splitText(hit.start);
|
|
1789
|
-
tail.deleteData(0, hit.len);
|
|
1790
|
-
tail.parentNode.insertBefore(el, tail);
|
|
1791
|
-
cur = tail;
|
|
2090
|
+
[...root.children].forEach(c => { if (!/^(CODE|PRE|A|BR|IMG)$/.test(c.tagName)) convertInline(c); });
|
|
2091
|
+
if (root.closest('code, pre, a')) return;
|
|
2092
|
+
const segments = [];
|
|
2093
|
+
let start = root.firstChild;
|
|
2094
|
+
for (let n = root.firstChild; ; n = n ? n.nextSibling : null) {
|
|
2095
|
+
if (!n || (n.nodeType === 1 && (n.tagName === 'BR' || BLOCK.includes(n.tagName)))) {
|
|
2096
|
+
if (start !== n) segments.push([start, n]);
|
|
2097
|
+
if (!n) break;
|
|
2098
|
+
start = n.nextSibling;
|
|
1792
2099
|
}
|
|
2100
|
+
}
|
|
2101
|
+
segments.forEach(([first, stop]) => {
|
|
2102
|
+
const box = document.createElement('span'), anchor = document.createComment('');
|
|
2103
|
+
first.before(anchor);
|
|
2104
|
+
for (let n = first; n && n !== stop;) { const nx = n.nextSibling; box.appendChild(n); n = nx; }
|
|
2105
|
+
const hit = reparse(box, null, 0, '');
|
|
2106
|
+
if (hit) { hit.fill(); box.replaceChildren(hit.frag); }
|
|
2107
|
+
anchor.replaceWith(...box.childNodes);
|
|
1793
2108
|
});
|
|
1794
2109
|
}
|
|
1795
2110
|
|
|
@@ -1972,16 +2287,16 @@
|
|
|
1972
2287
|
const h = area._history; if (!h || h.restoring) return;
|
|
1973
2288
|
clearTimeout(h.timer); h.timer = null;
|
|
1974
2289
|
if (area._rt && area._rt.settle) area._rt.settle();
|
|
1975
|
-
const html = area.innerHTML;
|
|
2290
|
+
const html = area.innerHTML, all = allKey(area);
|
|
1976
2291
|
// Unchanged: the entry learns where the next edit starts (`before`, for undo);
|
|
1977
2292
|
// `caret` stays where the edit that produced it ended (for redo)
|
|
1978
|
-
if (h.stack[h.at] && h.stack[h.at].html === html) {
|
|
2293
|
+
if (h.stack[h.at] && h.stack[h.at].html === html && h.stack[h.at].all === all) {
|
|
1979
2294
|
const caret = offsetIn(area);
|
|
1980
2295
|
if (caret != null) h.stack[h.at].before = caret;
|
|
1981
2296
|
return;
|
|
1982
2297
|
}
|
|
1983
2298
|
h.stack.length = h.at + 1;
|
|
1984
|
-
h.stack.push({ html, caret: offsetIn(area) });
|
|
2299
|
+
h.stack.push({ html, caret: offsetIn(area), all });
|
|
1985
2300
|
if (h.stack.length > HISTORY_CAP) h.stack.shift();
|
|
1986
2301
|
h.at = h.stack.length - 1;
|
|
1987
2302
|
}
|
|
@@ -2003,12 +2318,14 @@
|
|
|
2003
2318
|
function step(area, by) {
|
|
2004
2319
|
const h = area._history; if (!h) return;
|
|
2005
2320
|
// Land a pending burst; walking history alone leaves each entry's carets as recorded
|
|
2006
|
-
if (h.timer || !h.stack[h.at] || h.stack[h.at].html !== area.innerHTML) record(area);
|
|
2321
|
+
if (h.timer || !h.stack[h.at] || h.stack[h.at].html !== area.innerHTML || h.stack[h.at].all !== allKey(area)) record(area);
|
|
2007
2322
|
const next = h.at + by;
|
|
2008
2323
|
if (next < 0 || next >= h.stack.length) return;
|
|
2009
2324
|
h.at = next; h.restoring = true;
|
|
2010
2325
|
const entry = h.stack[next];
|
|
2011
2326
|
area.innerHTML = entry.html;
|
|
2327
|
+
// .all styles are steps too
|
|
2328
|
+
if (entry.all != null && entry.all !== allKey(area)) { setAllStyle(area, JSON.parse(entry.all)); area.dispatchEvent(new CustomEvent('rich-text:all', { bubbles: true, detail: allStyle(area) })); }
|
|
2012
2329
|
if (area._rt && area._rt.settle) { area._rt.settle(); entry.html = area.innerHTML; }
|
|
2013
2330
|
area.focus();
|
|
2014
2331
|
caretIn(area, by < 0 && entry.before != null ? entry.before : entry.caret);
|
|
@@ -2142,8 +2459,10 @@
|
|
|
2142
2459
|
}
|
|
2143
2460
|
if (!allows(area, id, ctrl)) return;
|
|
2144
2461
|
if (all) {
|
|
2462
|
+
record(area);
|
|
2145
2463
|
area.style.setProperty(ALL_VARS[id], id === 'size' ? px(arg) : arg);
|
|
2146
2464
|
if (!arg) area.style.removeProperty(ALL_VARS[id]);
|
|
2465
|
+
record(area);
|
|
2147
2466
|
area.dispatchEvent(new CustomEvent('rich-text:all', { bubbles: true, detail: allStyle(area) }));
|
|
2148
2467
|
sync();
|
|
2149
2468
|
return;
|
|
@@ -2151,7 +2470,12 @@
|
|
|
2151
2470
|
const arming = INLINE_TAGS[id] && !hasSelection();
|
|
2152
2471
|
if (arming) { spec.run(arg, area); sync(); return; }
|
|
2153
2472
|
if (!spec.history) record(area);
|
|
2473
|
+
const before = textSpan(area);
|
|
2154
2474
|
spec.run(arg, area);
|
|
2475
|
+
// A command that rebuilt the blocks under the selection (a block style) leaves it where it was
|
|
2476
|
+
const after = range();
|
|
2477
|
+
if (before && before[1] > before[0] && (!after || !area.contains(after.startContainer) || after.collapsed)) selectSpan(area, before);
|
|
2478
|
+
mergeAdjacent(area);
|
|
2155
2479
|
normalizeLists(area);
|
|
2156
2480
|
if (!spec.history) record(area);
|
|
2157
2481
|
saveRange(area);
|
|
@@ -2159,8 +2483,23 @@
|
|
|
2159
2483
|
sync();
|
|
2160
2484
|
}
|
|
2161
2485
|
|
|
2486
|
+
// $rich's reads (value, selection, can, active, …) depend on one reactive beat, bumped
|
|
2487
|
+
// (once a microtask) whenever the selection, value or formatting may have changed
|
|
2488
|
+
let pulse = null, beating = false;
|
|
2489
|
+
const watch = () => { if (pulse) void pulse.n; };
|
|
2490
|
+
// Bumped only when what $rich reports changed, so an effect that reads and writes it settles
|
|
2491
|
+
let lastSurface = null;
|
|
2492
|
+
const selKey = (x) => x ? [x.collapsed, x.text, x.x, x.y, x.width, x.height].join() : '';
|
|
2493
|
+
const surface = () => [...areas].map(a => [a === lastFocused, a === lastFocused ? String(textSpan(a)) : '', a.innerHTML, allKey(a), selKey(a._selection), a._pending ? [...a._pending.tags].join() : ''].join('\u0001')).join('\u0002');
|
|
2494
|
+
const beat = () => {
|
|
2495
|
+
if (!pulse || beating) return;
|
|
2496
|
+
beating = true;
|
|
2497
|
+
queueMicrotask(() => { beating = false; const now = surface(); if (now === lastSurface) return; lastSurface = now; pulse.n++; });
|
|
2498
|
+
};
|
|
2499
|
+
|
|
2162
2500
|
// Reflect caret state onto every control
|
|
2163
2501
|
function sync() {
|
|
2502
|
+
beat();
|
|
2164
2503
|
controls.forEach(c => {
|
|
2165
2504
|
const cfg = c._rt, area = resolve(c);
|
|
2166
2505
|
const usable = !!area && allows(area, cfg.id, cfg);
|
|
@@ -2235,6 +2574,7 @@
|
|
|
2235
2574
|
if (!window.Alpine || !Alpine.directive) return;
|
|
2236
2575
|
// Alpine walks only x-data trees: a page with none still gets its fields
|
|
2237
2576
|
if (document.body && !document.body.hasAttribute('x-data')) document.body.setAttribute('x-data', '{}');
|
|
2577
|
+
if (Alpine.reactive && !pulse) pulse = Alpine.reactive({ n: 0 });
|
|
2238
2578
|
listen();
|
|
2239
2579
|
|
|
2240
2580
|
Alpine.directive('rich-text', (el, { expression, modifiers }, { effect, evaluateLater, evaluate, cleanup }) => {
|
|
@@ -2252,20 +2592,37 @@
|
|
|
2252
2592
|
Alpine.directive('rich-text-menu', (el, _, { cleanup }) => { if (!el._rtMenu) selectionMenu(el, cleanup); });
|
|
2253
2593
|
|
|
2254
2594
|
// $rich: the area this element sits in, else the one controls would resolve to
|
|
2595
|
+
// Unresolved, it depends on the beat, so it resolves once a field is there
|
|
2255
2596
|
const richMagic = (el) => {
|
|
2256
|
-
const
|
|
2257
|
-
|
|
2597
|
+
const a = fieldFor(el);
|
|
2598
|
+
if (!a) watch();
|
|
2258
2599
|
return a ? a._rt.api : idleApi();
|
|
2259
2600
|
};
|
|
2260
2601
|
Alpine.magic('rich', richMagic);
|
|
2261
2602
|
// The loader's stand-in answered $rich until now: it hands over, and effects that read it
|
|
2262
2603
|
// run again once adopted areas exist
|
|
2263
2604
|
window.__manifestRichMagic = richMagic;
|
|
2264
|
-
if (window.__manifestRichStub) setTimeout(() => { window.__manifestRichStub.v++; }, 0);
|
|
2605
|
+
if (window.__manifestRichStub) setTimeout(() => { flushStubWrites(); window.__manifestRichStub.v++; }, 0);
|
|
2265
2606
|
|
|
2266
2607
|
adopt();
|
|
2267
2608
|
}
|
|
2268
2609
|
|
|
2610
|
+
// $rich's field for an element: the one it sits in, else the one controls would resolve to
|
|
2611
|
+
const fieldFor = (el) => { const own = el.closest('[data-rich-text]'); return own && areas.has(own) ? own : resolve(el); };
|
|
2612
|
+
// Writes made on the loader's stand-in ($rich.value = … before Rich Text arrived) land on
|
|
2613
|
+
// their element's field once it exists
|
|
2614
|
+
function flushStubWrites() {
|
|
2615
|
+
const w = window.__manifestRichStubWrites; if (!w || !w.length) return;
|
|
2616
|
+
for (let i = 0; i < w.length;) {
|
|
2617
|
+
const [ref, k, v] = w[i], el = ref && typeof ref.deref === 'function' ? ref.deref() : ref;
|
|
2618
|
+
if (!el || !el.isConnected) { w.splice(i, 1); continue; }
|
|
2619
|
+
const a = fieldFor(el);
|
|
2620
|
+
if (!a) { i++; continue; }
|
|
2621
|
+
w.splice(i, 1);
|
|
2622
|
+
try { a._rt.api[k] = v; } catch { }
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
|
|
2269
2626
|
// Loaded after Alpine started: elements it already walked get their x-rich-text*
|
|
2270
2627
|
// attributes re-set, which Alpine handles as newly added directives
|
|
2271
2628
|
function adopt() {
|
|
@@ -2318,12 +2675,40 @@
|
|
|
2318
2675
|
t.style.setProperty('--rich-text-selection-center', px(shape.x + shape.width / 2));
|
|
2319
2676
|
}
|
|
2320
2677
|
|
|
2678
|
+
// While a field holds a selection, scrolling and resizing re-report its geometry (once a frame)
|
|
2679
|
+
let tracked = null, trackFrame = 0;
|
|
2680
|
+
const untrack = () => {
|
|
2681
|
+
if (!tracked) return;
|
|
2682
|
+
tracked = null;
|
|
2683
|
+
window.removeEventListener('scroll', follow, { capture: true });
|
|
2684
|
+
window.removeEventListener('resize', follow);
|
|
2685
|
+
};
|
|
2686
|
+
function follow() {
|
|
2687
|
+
if (trackFrame) return;
|
|
2688
|
+
trackFrame = requestAnimationFrame(() => {
|
|
2689
|
+
trackFrame = 0;
|
|
2690
|
+
const area = tracked, shape = area && area.isConnected ? selectionOf(area) : null;
|
|
2691
|
+
if (!shape || shape.collapsed) return untrack();
|
|
2692
|
+
report(area);
|
|
2693
|
+
});
|
|
2694
|
+
}
|
|
2695
|
+
function track(area, on) {
|
|
2696
|
+
if (!on) { if (tracked === area) untrack(); return; }
|
|
2697
|
+
if (!tracked) {
|
|
2698
|
+
window.addEventListener('scroll', follow, { capture: true, passive: true });
|
|
2699
|
+
window.addEventListener('resize', follow, { passive: true });
|
|
2700
|
+
}
|
|
2701
|
+
tracked = area;
|
|
2702
|
+
}
|
|
2703
|
+
|
|
2321
2704
|
function report(area) {
|
|
2322
2705
|
const live = sel();
|
|
2323
2706
|
if (cellDrag && live && live.isCollapsed) { const c = cellOf(live.anchorNode); if (c) cellDrag = null; }
|
|
2324
2707
|
if (area.querySelector('table')) paintCells(area);
|
|
2325
2708
|
const shape = selectionOf(area);
|
|
2326
2709
|
area._selection = shape;
|
|
2710
|
+
beat();
|
|
2711
|
+
track(area, !!shape && !shape.collapsed);
|
|
2327
2712
|
place(area, shape);
|
|
2328
2713
|
area.toggleAttribute('data-rich-text-selected', !!shape && !shape.collapsed);
|
|
2329
2714
|
document.documentElement.toggleAttribute('data-rich-text-selected', !!shape && !shape.collapsed);
|
|
@@ -2335,29 +2720,10 @@
|
|
|
2335
2720
|
el._rtMenu = true;
|
|
2336
2721
|
if (!el.hasAttribute('popover')) el.setAttribute('popover', 'manual');
|
|
2337
2722
|
el.setAttribute('data-rich-text-menu', '');
|
|
2338
|
-
let shown = null, frame = 0;
|
|
2339
|
-
const follow = () => {
|
|
2340
|
-
if (frame) return;
|
|
2341
|
-
frame = requestAnimationFrame(() => { frame = 0; if (shown && shown.isConnected) place(shown, selectionOf(shown)); });
|
|
2342
|
-
};
|
|
2343
|
-
const track = (area) => {
|
|
2344
|
-
if (!shown) {
|
|
2345
|
-
window.addEventListener('scroll', follow, { capture: true, passive: true });
|
|
2346
|
-
window.addEventListener('resize', follow, { passive: true });
|
|
2347
|
-
}
|
|
2348
|
-
shown = area;
|
|
2349
|
-
};
|
|
2350
|
-
const untrack = () => {
|
|
2351
|
-
if (!shown) return;
|
|
2352
|
-
shown = null;
|
|
2353
|
-
window.removeEventListener('scroll', follow, { capture: true });
|
|
2354
|
-
window.removeEventListener('resize', follow);
|
|
2355
|
-
};
|
|
2356
2723
|
const onSel = (e) => {
|
|
2357
2724
|
const area = elementAt(e.target) && elementAt(e.target).closest('[data-rich-text]');
|
|
2358
2725
|
if (!area || resolve(el) !== area) return;
|
|
2359
2726
|
const want = !!e.detail && !e.detail.collapsed;
|
|
2360
|
-
if (want) track(area); else untrack();
|
|
2361
2727
|
try {
|
|
2362
2728
|
if (want && !el.matches(':popover-open')) el.showPopover();
|
|
2363
2729
|
if (!want && el.matches(':popover-open')) el.hidePopover();
|
|
@@ -2366,7 +2732,6 @@
|
|
|
2366
2732
|
document.addEventListener('rich-text:selection', onSel);
|
|
2367
2733
|
cleanup(() => {
|
|
2368
2734
|
document.removeEventListener('rich-text:selection', onSel);
|
|
2369
|
-
untrack(); if (frame) cancelAnimationFrame(frame);
|
|
2370
2735
|
try { el.hidePopover(); } catch { }
|
|
2371
2736
|
});
|
|
2372
2737
|
}
|
|
@@ -2523,8 +2888,8 @@
|
|
|
2523
2888
|
}
|
|
2524
2889
|
}
|
|
2525
2890
|
|
|
2526
|
-
el._history = { stack: [{ html: el.innerHTML, caret: 0 }], at: 0, timer: null, restoring: false };
|
|
2527
|
-
const resetHistory = () => { clearTimeout(el._history.timer); el._history.timer = null; el._history.stack = [{ html: el.innerHTML, caret: 0 }]; el._history.at = 0; };
|
|
2891
|
+
el._history = { stack: [{ html: el.innerHTML, caret: 0, all: allKey(el) }], at: 0, timer: null, restoring: false };
|
|
2892
|
+
const resetHistory = () => { clearTimeout(el._history.timer); el._history.timer = null; el._history.stack = [{ html: el.innerHTML, caret: 0, all: allKey(el) }]; el._history.at = 0; };
|
|
2528
2893
|
|
|
2529
2894
|
// Empty flag for the placeholder; a wiped document restarts as a paragraph
|
|
2530
2895
|
function settle() {
|
|
@@ -2544,7 +2909,9 @@
|
|
|
2544
2909
|
if (el.querySelector('table')) normalizeTables(el);
|
|
2545
2910
|
if (format !== 'plain') looseInline(el);
|
|
2546
2911
|
ensureTrailingBlock(el);
|
|
2547
|
-
|
|
2912
|
+
// A code block just opened (``` + Enter) holds the caret: it stays
|
|
2913
|
+
const r = range(), first = el.firstElementChild, opened = !!first && first.tagName === 'PRE' && !!r && first.contains(r.startContainer);
|
|
2914
|
+
if (!opened && !el.textContent.trim() && !el.querySelector('img, table, hr')) {
|
|
2548
2915
|
if (el.children.length !== 1 || el.firstElementChild.tagName !== 'P') el.replaceChildren();
|
|
2549
2916
|
if (!el.firstChild) { el.innerHTML = '<p><br></p>'; if (document.activeElement === el) caretIn(el.firstElementChild, 0); }
|
|
2550
2917
|
el.toggleAttribute('data-rich-text-empty', true);
|
|
@@ -2555,6 +2922,8 @@
|
|
|
2555
2922
|
writing = true; write(v); writing = false;
|
|
2556
2923
|
resetHistory();
|
|
2557
2924
|
settle();
|
|
2925
|
+
el._selection = selectionOf(el);
|
|
2926
|
+
sync();
|
|
2558
2927
|
};
|
|
2559
2928
|
|
|
2560
2929
|
const lineBreak = () => { record(el); insertBreak(el); record(el); el.dispatchEvent(new Event('input', { bubbles: true })); };
|
|
@@ -2608,7 +2977,7 @@
|
|
|
2608
2977
|
on('input', (e) => {
|
|
2609
2978
|
// Mid-composition the IME owns the text
|
|
2610
2979
|
if (!writing && e.data && !e.isComposing) {
|
|
2611
|
-
if (applyPending(el, e.data)) commit();
|
|
2980
|
+
if (applyPending(el, e.data)) { mergeAdjacent(el); commit(); }
|
|
2612
2981
|
if (leaveMark(el, e.data)) commit();
|
|
2613
2982
|
if (autoformat && convertInlineAtCaret(el)) commit();
|
|
2614
2983
|
}
|
|
@@ -2622,6 +2991,8 @@
|
|
|
2622
2991
|
on('compositionend', (e) => {
|
|
2623
2992
|
if (!writing && e.data && applyPending(el, e.data)) { commit(); sync(); }
|
|
2624
2993
|
else if (!writing && e.data && el._exit && leaveMark(el, e.data)) { commit(); sync(); }
|
|
2994
|
+
// A closing marker committed through an IME converts as a typed one does
|
|
2995
|
+
else if (!writing && autoformat && e.data && /[*_~`)]$/.test(e.data) && convertInlineAtCaret(el)) { commit(); sync(); }
|
|
2625
2996
|
if (!writing) relaxSpaces(el);
|
|
2626
2997
|
});
|
|
2627
2998
|
on('change', e => {
|
|
@@ -2762,7 +3133,7 @@
|
|
|
2762
3133
|
});
|
|
2763
3134
|
|
|
2764
3135
|
const api = {
|
|
2765
|
-
get value() { return read(); },
|
|
3136
|
+
get value() { watch(); return read(); },
|
|
2766
3137
|
// The value it already holds keeps the markup and caret; history restarts either way
|
|
2767
3138
|
set value(v) {
|
|
2768
3139
|
if (v !== read()) write(v);
|
|
@@ -2772,15 +3143,25 @@
|
|
|
2772
3143
|
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
2773
3144
|
el._history.restoring = false;
|
|
2774
3145
|
},
|
|
2775
|
-
get all() { return allStyle(el); },
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
3146
|
+
get all() { watch(); return allStyle(el); },
|
|
3147
|
+
// Page styling, not an edit: no history step (the history takes it as its baseline)
|
|
3148
|
+
set all(v) {
|
|
3149
|
+
if (!allStyles) return;
|
|
3150
|
+
const was = allStyle(el);
|
|
3151
|
+
setAllStyle(el, v);
|
|
3152
|
+
const now = allStyle(el);
|
|
3153
|
+
if (JSON.stringify(now) === JSON.stringify(was)) return;
|
|
3154
|
+
rebaseAll(el, was, now);
|
|
3155
|
+
el.dispatchEvent(new CustomEvent('rich-text:all', { bubbles: true, detail: now }));
|
|
3156
|
+
sync();
|
|
3157
|
+
},
|
|
3158
|
+
get selection() { watch(); return el._selection || null; },
|
|
3159
|
+
get link() { watch(); return linkHref(); },
|
|
3160
|
+
set link(v) { if ((v == null ? '' : String(v)) === (linkHref() || '')) return; run(el, 'a', v); },
|
|
2780
3161
|
focus: () => el.focus(),
|
|
2781
3162
|
run: (id, arg) => run(el, id, arg),
|
|
2782
|
-
active: (id) => allows(el, id) && COMMANDS[id].active(undefined, el),
|
|
2783
|
-
can: (id) => allows(el, id),
|
|
3163
|
+
active: (id) => { watch(); return allows(el, id) && COMMANDS[id].active(undefined, el); },
|
|
3164
|
+
can: (id) => { watch(); return allows(el, id); },
|
|
2784
3165
|
selectAll: () => { el.focus(); selectNode(el); saveRange(el); sync(); },
|
|
2785
3166
|
// The newest step in this field's history (a pending typing burst lands first)
|
|
2786
3167
|
newest: () => { const h = el._history; if (h.timer || h.stack[h.at].html !== el.innerHTML) record(el); return h.stack[h.stack.length - 1].html; },
|
|
@@ -2792,12 +3173,14 @@
|
|
|
2792
3173
|
|
|
2793
3174
|
el._rt = { format, inline: single, breaks: lines, noAll: !allStyles, api, settle };
|
|
2794
3175
|
areas.add(el);
|
|
3176
|
+
if (window.__manifestRichStubWrites && window.__manifestRichStubWrites.length) setTimeout(flushStubWrites, 0);
|
|
2795
3177
|
settle();
|
|
2796
|
-
el._history.stack[0].html = el.innerHTML;
|
|
3178
|
+
el._history.stack[0].html = el.innerHTML; el._history.stack[0].all = allKey(el);
|
|
2797
3179
|
setTimeout(sync, 0);
|
|
2798
3180
|
|
|
2799
3181
|
const destroy = () => {
|
|
2800
3182
|
if (!areas.has(el)) return;
|
|
3183
|
+
if (tracked === el) untrack();
|
|
2801
3184
|
ac.abort();
|
|
2802
3185
|
clearTimeout(el._history && el._history.timer);
|
|
2803
3186
|
if (lastFocused === el) release(el);
|