mnfst 0.5.233 → 0.5.234

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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  (function () {
4
4
 
5
- const MANIFEST_BUILD_VERSION = '0.5.233';
5
+ const MANIFEST_BUILD_VERSION = '0.5.234';
6
6
 
7
7
  /* Manifest Data Sources - Appwrite Integration */
8
8
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  (function () {
4
4
 
5
- const MANIFEST_BUILD_VERSION = '0.5.233';
5
+ const MANIFEST_BUILD_VERSION = '0.5.234';
6
6
 
7
7
  /* Manifest Data Sources - Configuration */
8
8
 
@@ -186,6 +186,14 @@
186
186
  }
187
187
  const sanitizeFor = (el, html) => !window.ManifestRichText ? sanitizeHTML(html)
188
188
  : authoredRich(el) ? window.ManifestRichText.sanitize(html, true, true) : leafClean(html, el);
189
+ // Text written into an element. A leaf shows exactly what is logged; a rich field owns its markup:
190
+ // it is written only when it differs as logged, then settles (keeping its editing affordances)
191
+ function writeText(el, html) {
192
+ const safe = sanitizeFor(el, html);
193
+ if (el.innerHTML === safe || (authoredRich(el) && sanitizeFor(el, el.innerHTML.trim()) === safe)) return;
194
+ el.innerHTML = safe;
195
+ if (el._rt && typeof el._rt.settle === 'function') el._rt.settle();
196
+ }
189
197
 
190
198
  function sanitizeHTML(html) {
191
199
  const t = document.createElement('template'); t.innerHTML = String(html == null ? '' : html);
@@ -529,7 +537,7 @@
529
537
  const base = area._baseText || {}, baseC = area._baseClass || {}, baseS = area._baseStyle || {};
530
538
  const pick = (prop, fallback) => iv[prop] !== undefined ? iv[prop] : (mv[prop] !== undefined ? mv[prop] : fallback);
531
539
  const text = pick('text', base[p]), cls = pick('class', baseC[p]), sty = pick('style', baseS[p]);
532
- if (text !== undefined) { const safe = sanitizeFor(el, text); if (el.innerHTML !== safe) el.innerHTML = safe; }
540
+ if (text !== undefined) writeText(el, text);
533
541
  if (cls !== undefined) writeClass(el, cls);
534
542
  if (sty !== undefined && el.getAttribute('style') !== sty) { if (sty === '') el.removeAttribute('style'); else el.setAttribute('style', sty); }
535
543
  });
@@ -540,7 +548,7 @@
540
548
  if (path == null) return false;
541
549
  if (prop === 'text') value = sanitizeFor(el, value);
542
550
  if (before === undefined) before = prop === 'text' ? el._preEdit : prop === 'style' ? el._preStyle : el._preClass;
543
- if (before === value) return false;
551
+ if (before === value || (prop === 'text' && before != null && sanitizeFor(el, before) === value)) return false;
544
552
  const sig = nodeSig(el);
545
553
  pushDelta(scope === 'main' ? { kind: 'cmp-main', component, path, prop, value, before, sig, region } : { kind: 'cmp-inst', component, region, path, prop, value, before, sig });
546
554
  applyComponentState();
@@ -640,7 +648,7 @@
640
648
 
641
649
  function applyStaticProp(el, prop, v) {
642
650
  if (v === undefined) return;
643
- if (prop === 'text') { const safe = sanitizeFor(el, v); if (el.innerHTML !== safe) el.innerHTML = safe; return; }
651
+ if (prop === 'text') return writeText(el, v);
644
652
  if (attrRefusal(prop, v, el.tagName)) return;
645
653
  if (prop === 'class') return writeClass(el, v);
646
654
  if (prop === 'style') return writeStyle(el, v);
@@ -862,8 +870,9 @@
862
870
  const pre = prop === 'text' ? el._preEdit : prop === 'class' ? el._preClass : prop === 'style' ? el._preStyle : undefined;
863
871
  before = pre !== undefined ? pre : fallback;
864
872
  }
865
- if (before === value) return false;
866
- if (prop === 'text' && document.activeElement !== el && el.innerHTML.trim() !== value) el.innerHTML = value;
873
+ if (before === value || (prop === 'text' && before != null && sanitizeFor(el, before) === value)) return false;
874
+ // A rich field owns its markup: the logged value is never written back into it
875
+ if (prop === 'text' && document.activeElement !== el && !authoredRich(el)) writeText(el, value);
867
876
  const last = log[cursor - 1];
868
877
  if (opts.coalesce && last && cursor === log.length && last.kind === 'st-node' && last.region === region && last.path === path && last.prop === prop && Date.now() - (stamps.get(last) || 0) < COALESCE_MS) {
869
878
  if (last.before === value) { log.pop(); cursor--; } else { last.value = value; stamps.set(last, Date.now()); }
@@ -901,7 +910,7 @@
901
910
  value = String(value);
902
911
  if (el && el._editRich) value = sanitizeFor(el, value);
903
912
  const before = el ? el._preEdit : undefined;
904
- if (before === value) return false;
913
+ if (before === value || (el && el._editRich && before != null && sanitizeFor(el, before) === value)) return false;
905
914
  pushDelta({ kind: 'data-val', region: key(area), source, id, field, value, before });
906
915
  applyDataValues();
907
916
  return true;
@@ -2050,7 +2059,7 @@
2050
2059
  area.querySelectorAll('[data-rich-text]:not([data-rich-text-bound])').forEach(el => {
2051
2060
  if (el._editRt || !capOf(el, 'text') || !/^[@~]/.test(el.getAttribute('data-edit-key') || '')) return;
2052
2061
  if (el._richBound) return; el._richBound = true;
2053
- el.addEventListener('focusin', () => { el._preEdit = el.innerHTML.trim(); });
2062
+ el.addEventListener('focusin', () => { el._preEdit = leafText(el); });
2054
2063
  el.addEventListener('blur', () => {
2055
2064
  const a = regionOf(el) || area; // the region the element sits in NOW, not where it was armed
2056
2065
  const commit = classify(a) === 'component' ? commitComponentNode : commitStaticNode;
@@ -2307,12 +2316,25 @@
2307
2316
  el._cssvar.persist = !!(modifiers && modifiers.includes('persist'));
2308
2317
  cssvarControls.add(el);
2309
2318
  }
2319
+ // A colour input takes #rrggbb only: any CSS colour (a theme's oklch(), var() resolved) is read off a canvas pixel
2320
+ let colorCanvas = null;
2321
+ function hexColor(v) {
2322
+ if (/^#[0-9a-f]{6}$/i.test(v)) return v.toLowerCase();
2323
+ try {
2324
+ colorCanvas = colorCanvas || document.createElement('canvas');
2325
+ const g = colorCanvas.getContext && colorCanvas.getContext('2d', { willReadFrequently: true });
2326
+ if (!g) return null;
2327
+ const probe = document.createElement('i'); probe.style.color = v; if (!probe.style.color) return null;
2328
+ g.clearRect(0, 0, 1, 1); g.fillStyle = '#000'; g.fillStyle = v; g.fillRect(0, 0, 1, 1);
2329
+ return '#' + [...g.getImageData(0, 0, 1, 1).data.slice(0, 3)].map(x => x.toString(16).padStart(2, '0')).join('');
2330
+ } catch { return null; }
2331
+ }
2310
2332
  function syncCssVarInput(el) {
2311
2333
  const cv = el._cssvar; if (!cv || !('value' in el)) return;
2312
2334
  const cur = getComputedStyle(themeTargetEl(cv.scope)).getPropertyValue(cv.var).trim();
2313
2335
  if (!cur) return;
2314
- const next = el.dataset.unit ? String(parseFloat(cur)) : cur;
2315
- if (el.value !== next) el.value = next;
2336
+ const next = el.type === 'color' ? hexColor(cur) : el.dataset.unit ? String(parseFloat(cur)) : cur;
2337
+ if (next != null && el.value !== next) el.value = next;
2316
2338
  }
2317
2339
  function armCssVar(el) {
2318
2340
  const cv = el._cssvar; if (!cv || !cv.var) return;
@@ -2922,7 +2944,8 @@
2922
2944
  ancestor opts it out. Text that is still being typed keeps the browser's own
2923
2945
  undo until it commits on blur; fields and x-rich-text keep theirs always. A leaf
2924
2946
  on Rich Text uses Rich Text's history while it has steps (richHistoryKey). */
2925
- const leafText = (el) => el._editField && !el._editRich ? el.textContent : el._editRt ? sanitizeFor(el, el.innerHTML.trim()) : el.innerHTML.trim();
2947
+ // Text as it would be logged: every baseline and comparison goes through the same sanitizer
2948
+ const leafText = (el) => el._editField && !el._editRich ? el.textContent : sanitizeFor(el, el.innerHTML.trim());
2926
2949
  const hotkeysOff = (el) => !!el.closest('[data-edit-hotkeys="off"]');
2927
2950
  // A focused leaf's baseline follows history rewrites under the caret.
2928
2951
  function rebaseFocused() {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "manifest.appwrite.auth.js": "sha384-dsIWnH0qCAGSzAXMGrd8fvOrVl4tOIE6j5tC35JPTrQXZ56qLD36ARyvhR7vi3Id",
3
- "manifest.appwrite.data.js": "sha384-ovIuPS7AiQP/dP+NHcF9QaB3YK3dlqOLWN9pYX/RHofc2PLrZZuDjfEFiyNReuBu",
3
+ "manifest.appwrite.data.js": "sha384-pj/2ZCUMXz3zTWPZyICJHfCPnz5srho9URrw0Sl0nfWuvh5KqvoT/g3Kg4uyv7yl",
4
4
  "manifest.appwrite.presences.js": "sha384-JyVDWFT69b7Svq8J8p9tNWe2v9PZNQBY5UyhtOl1vnuHFAX0CRQ3MA9SiKq5wzNh",
5
5
  "manifest.bindings.js": "sha384-lPtMf8Ykaic9gNVZKq8Scia1881ZeMAyxer3WCIkUNwwrgzGBWto1OXADUwjvORF",
6
6
  "manifest.charts.js": "sha384-gEAv/98+29RbhvNkiNWTNyBPbLTFr2Ld15wLu29FLCL57j1cwGChRrL9Pfk7dBtT",
@@ -11,24 +11,24 @@
11
11
  "manifest.combobox.js": "sha384-MILTkrgsTzsYUjGo4ZuADqV85Xs4GmfB4xexfe9vbJMIoQTZ42W4zs+DTaVSPEsx",
12
12
  "manifest.components.js": "sha384-JalISaBBWNFl3ggx92Hb3B52EpwwG3gzgra6Q+js0OgZ8vrFKMD48CuBox2N9A5T",
13
13
  "manifest.computed.js": "sha384-GYML/e81gQfN+iDbqhPUgny8bINCyL+SrbTofoGk4hlCnLLjv51mCv42JMqq+ku5",
14
- "manifest.data.js": "sha384-cemls8IyDcg61WdlIOT60HmQS7aCef4ifwqnXfw7kA6Jp5D2PdXazWRRzR6QHx10",
14
+ "manifest.data.js": "sha384-+wRqEd8FFPqaSX5gZhb66OgkAHTiSQDFK6W1agFMceKtd5nVL7OLh6p5gC96AJ3Z",
15
15
  "manifest.datepicker.js": "sha384-XnbeURni2X91ozVzatQcSjYYIS4MoW12dBRJnYAGBr0nCQ624heH5s+xtQSpLZX6",
16
16
  "manifest.defer.js": "sha384-pMRGqQKiEoJhpUcD40QeM4jEHau7sz9Xn/YCpiUamT1T4xJBZDSiYX+xnVrSWoq+",
17
17
  "manifest.device.js": "sha384-g+txmrEszYtuOvRelTp/c1wSNR2AtllVI9rx5NPPD7DzhVUnMNughonZykv2xlBW",
18
18
  "manifest.dropdowns.js": "sha384-acufUps7R9WjqB4XXW5JLACqniuHJGM4CNTb2rF4B2Q/jv7ZRmsAwfr2McanOz3s",
19
- "manifest.edit.js": "sha384-bOvEFgjau1t30LPHKBGkcJ3jH8tRUy1k8qpyYs8GbB6gUJ/R9JQCzeaF5fsKlzu3",
19
+ "manifest.edit.js": "sha384-XUtJ+EXXYh+5afgYuVpXurjvylSP/o72Y4mQR5wxdkIFQ/NDFBVvRFAfbPXMInZc",
20
20
  "manifest.export.js": "sha384-Rx6XvqBZc4apzP/FwcDr6yIln4/zNa08Q4ZqBZcEqUz1aNZQfl4h0+Ae7mqCQyuG",
21
21
  "manifest.icons.js": "sha384-kKOINpX70YzNO3FvkV6umrmO3f0KIu0DNbwB+ej55a7y/lhU7v/myQtXL0bwCTbt",
22
22
  "manifest.localization.js": "sha384-EImsIw8Z3l13V5eH9ZJF/yfsOfIvR1oPRNSArANkPji7V8O3IRf6zMwsNiAGj3Hk",
23
23
  "manifest.markdown.js": "sha384-heyDR40LXHaAmAuA+fa381uFwShk1oAqpFFY6E2Qq4T3VN0TQfRqyc++MF40K5Hj",
24
24
  "manifest.native.js": "sha384-g+txmrEszYtuOvRelTp/c1wSNR2AtllVI9rx5NPPD7DzhVUnMNughonZykv2xlBW",
25
25
  "manifest.payments.js": "sha384-8pofk2zhxtapPMpwnFyTelPMrYhShNhDCE722uQh/EYFvZFuPLmSdgunqsarVQjv",
26
- "manifest.rich.text.js": "sha384-QpPHG4uWjJSJoTPY8yOtoIFoVmHbuYPhtghBU0gc2l0PJBc25cNPuNcD4+55Qv3i",
26
+ "manifest.rich.text.js": "sha384-yJNb+EIumFIJx2KiKmLojwyHLCKwIn+48noxh3gUuZ10YZOvJJoanzAU7NLIBF1s",
27
27
  "manifest.router.js": "sha384-QkKhRaCbdXEOxhj+ojT4jeLUCtQepBnqP+QWTIWdb6kUWD+mVzPN8hf6jhgschpW",
28
28
  "manifest.slides.js": "sha384-3uRTkyK9XPLmnxI2+igZlpi4EyPlU/7IHj5j3BZJJ2KN455vXyk99fiXV3feO/XY",
29
29
  "manifest.status.js": "sha384-MBO345ab7H6Uq1X2dZLt4Ge1Gcra+Z2qYApvlAeIV8CJhDDJrz2FPXpp+Kcp8zcM",
30
30
  "manifest.svg.js": "sha384-qr/8H9+2cIhSXNbRWRtIdrDlTOxKlNJqwEefHvCYoiBNW/kKhPTFa109Orv/EBnb",
31
- "manifest.sw.js": "sha384-vXYxaf8oKd2LT8C9xRmFmEXhh0qDLv0set07o/hI4hkOvpS+CTDdC5UNECLRrSWa",
31
+ "manifest.sw.js": "sha384-alPjmWaugQV/aCb6l7hVITwZ6Z5k47SJ9jHXmNUN093iUu8ikbVggwLSGNb84Ib1",
32
32
  "manifest.tabs.js": "sha384-OIIcyaQnGS0eNYk4zgAFEDboNYect7THvUd9D0V6nqCeDOxQcoiqLrBEmmWVTTet",
33
33
  "manifest.tailwind.js": "sha384-r/Jvqb+Piz21jUts10BbVAJoKkShOdJRVFpywyi4HPIi/OfOJq5CtPyWAPbdkMgt",
34
34
  "manifest.toasts.js": "sha384-3zkWxzw693pxg3QDFsoYCA3CqYaLKmBaMArULXODD66Utolpewl0OAeTB92aWUVv",
@@ -92,18 +92,14 @@
92
92
  if (n.nodeType !== 1) return textEdge(last ? n.previousSibling : n.nextSibling, last);
93
93
  if (n.tagName === 'BR') return '\n';
94
94
  if (n.tagName === 'IMG') return last ? ')' : '!';
95
- if (MARKS[n.tagName] || n.tagName === 'A') return last ? '*' : '*'; // a delimiter or bracket: punctuation
95
+ if (n.tagName === 'A') return last ? ')' : '[';
96
+ if (MARKS[n.tagName]) return MARKS[n.tagName][0]; // its delimiter
96
97
  const t = n.textContent; return t ? (last ? t[t.length - 1] : t[0]) : textEdge(last ? n.previousSibling : n.nextSibling, last);
97
98
  };
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
99
  function inlineRaw(node, ctx) {
104
100
  let out = '';
105
101
  node.childNodes.forEach(n => {
106
- if (n.nodeType === 3) { out += esc(n.textContent); return; }
102
+ if (n.nodeType === 3) { out += ctx.link ? esc(n.textContent).replace(/[()]/g, '\\$&') : esc(n.textContent); return; } // in link text, no "(" can start a destination
107
103
  if (n.nodeType !== 1) return;
108
104
  const tag = n.tagName;
109
105
  if (tag === 'BR') { out += ' \n'; return; }
@@ -112,8 +108,9 @@
112
108
  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
109
  if (tag === 'A') {
114
110
  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);
111
+ const tt = title ? title.replace(/\n/g, ' ') : '';
112
+ const t = !tt ? '' : !tt.includes('"') ? ` "${tt.replace(/\\/g, '\\\\')}"` : !tt.includes("'") ? ` '${tt.replace(/\\/g, '\\\\')}'` : ` "${tt.replace(/[\\"]/g, '\\$&')}"`;
113
+ out += h && !unsafeUrl(h) ? `[${inlineRaw(n, { ...ctx, prev: '[', next: ']', link: true, trims: false })}](${mdUrl(h)}${t})` : inlineRaw(n, ctx);
117
114
  return;
118
115
  }
119
116
  let m = MARKS[tag];
@@ -121,40 +118,46 @@
121
118
  if (tag === 'CODE') {
122
119
  // A fence longer than any backtick run inside; padded where the code starts or ends with one
123
120
  const code = n.textContent; if (!code) return;
121
+ // Code holding a backtick needs a longer fence, which renderers do not all read alike
122
+ // around emphasis or in link text: written as HTML
123
+ if (code.includes('`')) { out += '<code>' + esc(code) + '</code>'; return; }
124
124
  const fence = '`'.repeat(Math.max(0, ...(code.match(/`+/g) || []).map(r => r.length)) + 1);
125
125
  const pad = /^`|`$/.test(code) || (/^ /.test(code) && / $/.test(code) && code.trim()) ? ' ' : '';
126
126
  out += fence + pad + code + pad + fence;
127
127
  return;
128
128
  }
129
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 });
130
+ let body = inlineRaw(n, { ...ctx, prev: m[0], next: m[0], trims: true });
131
131
  // Delimiters hug the text: spaces go outside
132
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
133
  const after = textEdge(n.nextSibling, false);
135
- const prev = lead ? ' ' : (out ? out[out.length - 1] : ctx.prev), next = trail ? ' ' : (after == null ? ctx.next : after);
134
+ // What will be against the delimiters: a space moved out of a markdown parent's edge is
135
+ // not there, the parent's delimiter is
136
+ const prev = lead ? (!out && ctx.trims ? ctx.prev : ' ') : (out ? out[out.length - 1] : ctx.prev);
137
+ const next = trail ? (after == null && ctx.trims ? ctx.next : ' ') : (after == null ? ctx.next : after);
136
138
  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;
139
+ // Delimiters every renderer reads the same way: whitespace or plain punctuation outside them
140
+ // (an enclosing mark's same delimiter too), text inside them that starts and ends plainly.
141
+ // Anything else (letters, another delimiter or an HTML tag against them) is written as HTML.
142
+ const isS = (c) => !c || /\s/.test(c), isD = (c) => c === '*' || c === '_' || c === '~';
143
+ // Punctuation outside only where text is inside: the delimiter can then only open (or close)
144
+ // Letters outside only between letters (intraword, as in foo**bar**), and only outside any other * mark
145
+ const wordy = (c) => !!c && !isS(c) && !MD_PUNCT.test(c);
146
+ const outside = (c, fromCtx, inner) => isS(c) || (fromCtx && c === m[0]) || (MD_PUNCT.test(c) && !isD(c) && c !== '<' && c !== '>' && c !== '`' && wordy(inner))
147
+ || (wordy(c) && wordy(inner) && !ctx.star && m[0] === '*' && !n.querySelector('strong, b, em, i'));
148
+ const f = core[0], l = core[core.length - 1], bare = core.replace(new RegExp('^\\' + m[0] + '+|\\' + m[0] + '+$', 'g'), '');
149
+ const clean = outside(prev, !lead && !out, bare[0]) && outside(next, !trail && after == null, bare[bare.length - 1])
150
+ && !isS(f) && !isS(l) && f !== '\\' && !/\\.$/.test(core) && f !== '<' && l !== '>'
151
+ && !(isD(f) && f !== m[0]) && !(isD(l) && l !== m[0])
152
+ // runs merged at both ends (***a* b *c***) only around one inner mark (***all***)
153
+ && !(f === m[0] && l === m[0] && [...n.childNodes].filter(c => c.nodeType === 1 || (c.nodeType === 3 && c.nodeValue.trim())).length > 1);
154
+ if ((m[0] === '*' || m[0] === '~') && (ctx.html === 'all' || (ctx.html === 'touch' && nonspace) || !clean)) {
155
+ const t = tag === 'STRONG' || tag === 'B' ? 'strong' : m[0] === '~' ? 's' : 'em';
156
+ // spaces stay inside a tag: what is against its children stays as computed
157
+ out += `<${t}>` + inlineRaw(n, { ...ctx, prev: '>', next: '<', trims: false }) + `</${t}>`;
141
158
  return;
142
159
  }
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*)$/);
160
+ if (m[0] === '*') [, lead, core, trail] = inlineRaw(n, { ...ctx, prev: '*', next: '*', star: true, trims: true }).match(/^(\s*)([\s\S]*?)(\s*)$/);
158
161
  out += lead + m + core + m + trail;
159
162
  });
160
163
  return out;
@@ -163,13 +166,15 @@
163
166
  // Inline markdown for a node, checked by reading it back: in the rare arrangement where a
164
167
  // * or _ run would pair differently, em/strong touching text go as HTML, else all of them
165
168
  function inline(node) {
166
- const want = shapeOf(node), ctx = { prev: '', next: '', under: false, star: false, html: '' };
169
+ const want = shapeOf(node), ctx = { prev: '', next: '', star: false, link: false, html: '' };
167
170
  let first = null;
168
171
  for (const html of ['', 'touch', 'all']) {
169
172
  const md = inlineRaw(node, { ...ctx, html });
170
173
  if (first == null) first = md;
174
+ // Read back as this reader does and as other markdown renderers (x-markdown) do
171
175
  const t = document.createElement('template'); t.innerHTML = inlineHtml(md);
172
- if (shapeOf(t.content) === want) return md;
176
+ const u = document.createElement('template'); u.innerHTML = inlineHtml(md, { strict: true });
177
+ if (shapeOf(t.content) === want && shapeOf(u.content) === want) return md;
173
178
  }
174
179
  return first; // what no form can carry (an unsafe link, say) is not a reason to change notation
175
180
  }
@@ -403,7 +408,7 @@
403
408
  const url = m.url, body = s.slice(open, close);
404
409
  flush();
405
410
  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>`);
411
+ else parts.push(unsafeUrl(url) || !url ? inlineHtml(body, { raw: opts.raw, strict: opts.strict }) : `<a href="${escHtml(url)}"${m.title != null ? ` title="${escHtml(m.title)}"` : ''}>${inlineHtml(body, { raw: opts.raw, strict: opts.strict })}</a>`);
407
412
  i = m.end; continue;
408
413
  }
409
414
  }
@@ -425,6 +430,10 @@
425
430
  const left = !isSpace(after) && (!PUNCT.test(after) || isSpace(before) || PUNCT.test(before));
426
431
  const right = !isSpace(before) && (!PUNCT.test(before) || isSpace(after) || PUNCT.test(after));
427
432
  open = left && (!right || PUNCT.test(before)); close = right && (!left || PUNCT.test(after));
433
+ } else if (opts.strict) {
434
+ // CommonMark's flanking, punctuation weighed (what other markdown renderers do)
435
+ open = !isSpace(after) && (!PUNCT.test(after) || isSpace(before) || PUNCT.test(before));
436
+ close = !isSpace(before) && (!PUNCT.test(before) || isSpace(after) || PUNCT.test(after));
428
437
  } else { open = !isSpace(after); close = !isSpace(before); }
429
438
  if (c === '~' && run.length !== 2) { open = close = false; }
430
439
  if (!open && !close) { text += run; i += run.length; continue; }
@@ -545,8 +554,19 @@
545
554
  });
546
555
  walk(t.content);
547
556
  mergeAdjacent(t.content);
557
+ if (allowBlocks) dropTrailingEmpty(t.content);
548
558
  return t.innerHTML;
549
559
  }
560
+ // The empty paragraph after a closing list, table or code block is an editing affordance only:
561
+ // values never end in one (nor in several)
562
+ function dropTrailingEmpty(root) {
563
+ for (let n = root.lastChild; n;) {
564
+ const prev = n.previousSibling;
565
+ if ((n.nodeType === 3 && !n.nodeValue.trim()) || (n.nodeType === 1 && n.tagName === 'P' && !n.textContent.trim() && !n.querySelector('img, input'))) n.remove();
566
+ else break;
567
+ n = prev;
568
+ }
569
+ }
550
570
 
551
571
  const markdownHtml = (md) => sanitize(fromMarkdown(md), true);
552
572
 
@@ -1193,39 +1213,48 @@
1193
1213
  return null;
1194
1214
  }
1195
1215
 
1216
+ // Sizes are written only once the pointer actually drags: a click on a border writes nothing
1196
1217
  function startResize(e, hit) {
1197
1218
  const cell = hit.cell, table = cell.closest('table');
1198
1219
  const area = cell.closest('[data-rich-text]');
1199
1220
  e.preventDefault();
1200
- area.setAttribute('data-rich-text-resizing', hit.axis);
1201
-
1202
- let apply;
1203
- if (hit.axis === 'x') {
1204
- const group = ensureCols(table);
1205
- const { col } = gridPosition(cell);
1206
- const index = Math.min(col + (cell.colSpan || 1) - 1, group.children.length - 1);
1207
- const target = group.children[index];
1208
- const start = parseFloat(target.style.width) || cell.getBoundingClientRect().width;
1209
- const from = e.clientX;
1210
- // The outer border sizes the table too
1211
- const outer = index === group.children.length - 1;
1212
- const tableStart = table.getBoundingClientRect().width;
1213
- if (outer) table.style.width = Math.round(tableStart) + 'px';
1214
- apply = (ev) => {
1215
- const delta = ev.clientX - from;
1216
- target.style.width = Math.max(28, Math.round(start + delta)) + 'px';
1217
- if (outer) table.style.width = Math.max(60, Math.round(tableStart + delta)) + 'px';
1218
- };
1219
- } else {
1220
- const row = cell.parentElement;
1221
- const start = row.getBoundingClientRect().height, from = e.clientY;
1222
- apply = (ev) => { row.style.height = Math.max(20, Math.round(start + ev.clientY - from)) + 'px'; };
1223
- }
1221
+ const fromX = e.clientX, fromY = e.clientY;
1222
+ let apply = null;
1223
+ const begin = () => {
1224
+ area.setAttribute('data-rich-text-resizing', hit.axis);
1225
+ if (hit.axis === 'x') {
1226
+ const group = ensureCols(table);
1227
+ const { col } = gridPosition(cell);
1228
+ const index = Math.min(col + (cell.colSpan || 1) - 1, group.children.length - 1);
1229
+ const target = group.children[index];
1230
+ const start = parseFloat(target.style.width) || cell.getBoundingClientRect().width;
1231
+ // The outer border sizes the table too
1232
+ const outer = index === group.children.length - 1;
1233
+ const tableStart = table.getBoundingClientRect().width;
1234
+ if (outer) table.style.width = Math.round(tableStart) + 'px';
1235
+ return (ev) => {
1236
+ const delta = ev.clientX - fromX;
1237
+ target.style.width = Math.max(28, Math.round(start + delta)) + 'px';
1238
+ if (outer) table.style.width = Math.max(60, Math.round(tableStart + delta)) + 'px';
1239
+ };
1240
+ }
1241
+ const row = cell.parentElement, start = row.getBoundingClientRect().height;
1242
+ return (ev) => { row.style.height = Math.max(20, Math.round(start + ev.clientY - fromY)) + 'px'; };
1243
+ };
1224
1244
 
1225
- const move = (ev) => { ev.preventDefault(); apply(ev); };
1245
+ const move = (ev) => {
1246
+ ev.preventDefault();
1247
+ if (!apply) {
1248
+ if (Math.abs(ev.clientX - fromX) < 3 && Math.abs(ev.clientY - fromY) < 3) return;
1249
+ record(area);
1250
+ apply = begin();
1251
+ }
1252
+ apply(ev);
1253
+ };
1226
1254
  const up = () => {
1227
1255
  document.removeEventListener('pointermove', move);
1228
1256
  document.removeEventListener('pointerup', up);
1257
+ if (!apply) return;
1229
1258
  area.removeAttribute('data-rich-text-resizing');
1230
1259
  area.dispatchEvent(new Event('input', { bubbles: true }));
1231
1260
  };
@@ -1668,7 +1697,13 @@
1668
1697
  insertHTML(`<table data-rich-text-new><tbody>${row.repeat(rows)}</tbody></table><p><br></p>`);
1669
1698
  // The caret goes into the first cell, so typing and table commands start there
1670
1699
  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); }
1700
+ if (made) {
1701
+ made.removeAttribute('data-rich-text-new');
1702
+ // One empty line after it to land on, not one per insertion (from inside a table, it lands after it)
1703
+ const blank = (e) => e && e.tagName === 'P' && !e.textContent.trim() && !e.querySelector('img');
1704
+ for (let n = made.nextElementSibling; blank(n) && blank(n.nextElementSibling);) { const x = n.nextElementSibling; x.remove(); }
1705
+ const first = made.querySelector('td, th'); if (first) caretIn(first, 0);
1706
+ }
1672
1707
  }
1673
1708
 
1674
1709
  // A trailing table, rule or similar block gets a line after it to land on
@@ -1698,8 +1733,21 @@
1698
1733
  }
1699
1734
 
1700
1735
  // A value styles the selection; an empty one removes that style from it (spans left bare go)
1736
+ // A style span covering exactly what a style span around it covers is folded into it, so one span
1737
+ // carries the combined styles and a re-applied value replaces the old one instead of nesting
1701
1738
  function styleSpan(prop, v) {
1702
- if (v != null && v !== '') return wrapInline('span', { [prop]: v });
1739
+ if (v != null && v !== '') {
1740
+ const made = wrapInline('span', { [prop]: v }).map(sp => {
1741
+ for (let p = sp.parentElement; p && p.tagName === 'SPAN' && !p.hasAttribute('data-rich-text') && [...p.attributes].every(a => a.name === 'style') && [...p.childNodes].filter(c => c.nodeType === 1 || c.nodeValue.length).length === 1; p = sp.parentElement) {
1742
+ for (let i = 0; i < sp.style.length; i++) { const name = sp.style.item(i); p.style.setProperty(name, sp.style.getPropertyValue(name)); }
1743
+ sp.replaceWith(...sp.childNodes);
1744
+ sp = p;
1745
+ }
1746
+ return sp;
1747
+ });
1748
+ if (made.length) { const r = document.createRange(), last = made[made.length - 1]; r.setStart(made[0], 0); r.setEnd(last, last.childNodes.length); const s = sel(); s.removeAllRanges(); s.addRange(r); }
1749
+ return made;
1750
+ }
1703
1751
  const nodes = selectedTextNodes(), spans = new Set();
1704
1752
  nodes.forEach(n => { for (let el = elementAt(n); el && el.closest && !el.hasAttribute('data-rich-text'); el = el.parentElement) if (el.tagName === 'SPAN' && el.style[prop]) spans.add(el); });
1705
1753
  spans.forEach(s => { s.style[prop] = ''; if (!s.getAttribute('style')) s.removeAttribute('style'); if (!s.attributes.length) unwrapEl(s); });
@@ -2143,7 +2191,10 @@
2143
2191
  }
2144
2192
 
2145
2193
  // Native Enter inside code and for Shift+Enter; Chrome writes <br> pairs in a <pre>
2194
+ // A code block's lines: in its <code> (the shape markdown gives), else in the <pre> itself
2195
+ const codeBox = (pre) => { const c = pre.firstElementChild; return c && c.tagName === 'CODE' && pre.children.length === 1 && !pre.textContent.replace(c.textContent, '').trim() ? c : pre; };
2146
2196
  const codeEndsBlank = (pre) => {
2197
+ pre = codeBox(pre);
2147
2198
  const last = pre.lastChild;
2148
2199
  if (last && last.nodeName === 'BR' && last.previousSibling && last.previousSibling.nodeName === 'BR') return true;
2149
2200
  return /\n[^\S\n]*$/.test(pre.textContent);
@@ -2214,10 +2265,13 @@
2214
2265
  // An empty last line leaves the code block (bounded to the area)
2215
2266
  const pre = holder.closest('pre');
2216
2267
  if (pre && pre !== area && area.contains(pre)) {
2217
- while (pre.lastChild && (pre.lastChild.nodeName === 'BR' || !pre.lastChild.textContent.trim())) pre.lastChild.remove();
2218
- if (/\n[^\S\n]*$/.test(pre.textContent)) pre.textContent = pre.textContent.replace(/\n[^\S\n]*$/, '');
2219
- const p = document.createElement('p');
2220
- pre.after(p);
2268
+ const box = codeBox(pre);
2269
+ while (box.lastChild && (box.lastChild.nodeName === 'BR' || !box.lastChild.textContent.trim())) box.lastChild.remove();
2270
+ if (/\n[^\S\n]*$/.test(box.textContent)) box.textContent = box.textContent.replace(/\n[^\S\n]*$/, '');
2271
+ // onto the empty line after it, if there is one
2272
+ const after = pre.nextElementSibling, blank = after && after.tagName === 'P' && !after.textContent.trim() && !after.querySelector('img');
2273
+ const p = blank ? after : document.createElement('p');
2274
+ if (!blank) pre.after(p);
2221
2275
  if (!pre.textContent.length) pre.remove();
2222
2276
  caretIn(p, 0);
2223
2277
  return;
@@ -2256,7 +2310,14 @@
2256
2310
  if (holder.tagName === 'HR') { caretIn(next, 0); return; }
2257
2311
 
2258
2312
  // A finished ``` line opens the code block
2259
- if (holder.tagName === 'PRE') { next.remove(); caretIn(holder, 0); return; }
2313
+ // (its text in a <code>, the shape a code block read from markdown has)
2314
+ if (holder.tagName === 'PRE') {
2315
+ next.remove();
2316
+ let code = holder.querySelector(':scope > code');
2317
+ if (!code) { code = document.createElement('code'); code.append(...holder.childNodes); holder.appendChild(code); }
2318
+ caretIn(code, 0);
2319
+ return;
2320
+ }
2260
2321
 
2261
2322
  if (holder.tagName === 'LI') {
2262
2323
  const item = next.tagName === 'LI' ? next : retagBlock(next, 'li');
@@ -2497,6 +2558,28 @@
2497
2558
  queueMicrotask(() => { beating = false; const now = surface(); if (now === lastSurface) return; lastSurface = now; pulse.n++; });
2498
2559
  };
2499
2560
 
2561
+ // Any CSS colour (oklch(), a name, rgb()) as #rrggbb, read off a canvas pixel; null when it cannot be
2562
+ let colorCanvas = null;
2563
+ const hexCache = new Map();
2564
+ function hexColor(v) {
2565
+ if (v == null || v === '') return null;
2566
+ const s = String(v).trim();
2567
+ if (/^#[0-9a-f]{6}$/i.test(s)) return s.toLowerCase();
2568
+ if (hexCache.has(s)) return hexCache.get(s);
2569
+ let out = null;
2570
+ try {
2571
+ colorCanvas = colorCanvas || document.createElement('canvas');
2572
+ const g = colorCanvas.getContext && colorCanvas.getContext('2d', { willReadFrequently: true });
2573
+ if (g) {
2574
+ g.clearRect(0, 0, 1, 1); g.fillStyle = '#000'; g.fillStyle = s; g.fillRect(0, 0, 1, 1);
2575
+ const [r, gr, b] = g.getImageData(0, 0, 1, 1).data;
2576
+ out = '#' + [r, gr, b].map(x => x.toString(16).padStart(2, '0')).join('');
2577
+ }
2578
+ } catch { out = null; }
2579
+ hexCache.set(s, out);
2580
+ return out;
2581
+ }
2582
+
2500
2583
  // Reflect caret state onto every control
2501
2584
  function sync() {
2502
2585
  beat();
@@ -2510,9 +2593,10 @@
2510
2593
  c.setAttribute('aria-disabled', String(!usable));
2511
2594
  if (usable && (all || spec.reflect) && 'value' in c && c !== document.activeElement) {
2512
2595
  const v = all ? area.style.getPropertyValue(ALL_VARS[cfg.id]).trim() : judged(area, () => spec.reflect());
2513
- // Never blank a <select> with a value it doesn't offer
2596
+ // Never blank a <select> with a value it doesn't offer; a colour input takes #rrggbb only
2514
2597
  const offered = c.tagName !== 'SELECT' || [...c.options].some(o => o.value === v);
2515
- if (v != null && offered && c.value !== v) c.value = v;
2598
+ const val = c.type === 'color' ? hexColor(v) : v;
2599
+ if (val != null && offered && c.value !== val) c.value = val;
2516
2600
  }
2517
2601
  if (c.tagName === 'BUTTON') c.setAttribute('aria-pressed', String(c.hasAttribute('data-rich-text-active')));
2518
2602
  });
@@ -16,7 +16,7 @@
16
16
 
17
17
  // Build stamps the published version here; unstamped (dev) falls back to the
18
18
  // registration URL's `v`, then 'dev'.
19
- const BUILD_VERSION = '0.5.233';
19
+ const BUILD_VERSION = '0.5.234';
20
20
  const CACHE_PREFIX = 'mnfst-sw:';
21
21
  const META_CACHE = CACHE_PREFIX + 'meta';
22
22
  // Sized to hold a full precache.json warm-up (PRECACHE_MAX_ENTRIES server-side)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mnfst",
3
- "version": "0.5.233",
3
+ "version": "0.5.234",
4
4
  "private": false,
5
5
  "workspaces": [
6
6
  "templates/starter",
@@ -60,6 +60,7 @@
60
60
  "entities": "4.5.0",
61
61
  "glob": "^11.0.2",
62
62
  "happy-dom": "^20.11.6",
63
+ "marked": "15.0.12",
63
64
  "puppeteer": "^24.15.0",
64
65
  "rimraf": "^5.0.0",
65
66
  "rollup": "^4.24.2",