rapier-markdown-kit 1.1.102 → 1.1.104
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +2 -2
- package/README.md +213 -53
- package/dist/agent/vendor/hyphen-en-us/LICENSE +14 -0
- package/dist/agent/vendor/hyphen-en-us/SOURCE.json +33 -0
- package/dist/agent/vendor/hyphen-en-us/hyph-en-us.hyp.txt +14 -0
- package/dist/agent/vendor/hyphen-en-us/hyph-en-us.pat.txt +4938 -0
- package/dist/agent/vendor/hyphen-en-us/patterns.js +25 -0
- package/dist/agent/vendor/pretext/LICENSE +208 -0
- package/dist/agent/vendor/pretext/NOTICE +26 -13
- package/dist/agent/vendor/pretext/SOURCE.json +213 -45
- package/dist/agent/vendor/pretext/analysis.js +675 -1224
- package/dist/agent/vendor/pretext/entry-geometry.js +183 -0
- package/dist/agent/vendor/pretext/gecko-line-breaks.js +688 -0
- package/dist/agent/vendor/pretext/generated/engine-break-data.js +57 -0
- package/dist/agent/vendor/pretext/generated/webkit-generic-families.js +80 -0
- package/dist/agent/vendor/pretext/graphemes.js +98 -0
- package/dist/agent/vendor/pretext/han-kerning.js +253 -0
- package/dist/agent/vendor/pretext/layout.js +352 -290
- package/dist/agent/vendor/pretext/line-break.js +1154 -588
- package/dist/agent/vendor/pretext/line-breaks.js +892 -0
- package/dist/agent/vendor/pretext/line-text.js +81 -39
- package/dist/agent/vendor/pretext/measurement.js +1105 -176
- package/dist/agent/vendor/pretext/prepare.js +807 -0
- package/dist/agent/vendor/pretext/rich-inline.js +1242 -273
- package/dist/agent/will.mjs +2 -0
- package/dist/kit/assets.mjs +5 -1
- package/dist/kit/conformance/run.mjs +4 -4
- package/dist/kit/export-fonts.mjs +26 -11
- package/dist/kit/index.mjs +1 -0
- package/dist/kit/ledger/carried.mjs +9 -7
- package/dist/kit/ledger/data.mjs +73 -13
- package/dist/kit/ledger/format.mjs +166 -53
- package/dist/kit/ledger/hash.mjs +75 -18
- package/dist/kit/ledger/history.mjs +143 -115
- package/dist/kit/ledger/inverse-source.mjs +158 -19
- package/dist/kit/ledger/journal-records.mjs +292 -67
- package/dist/kit/model.mjs +1 -1
- package/dist/kit/pdf-glyphs.mjs +4 -4
- package/dist/kit/render-markdown.mjs +259 -122
- package/dist/kit/render-sanitize.mjs +24 -4
- package/dist/kit/render-work.mjs +99 -20
- package/dist/kit/render.mjs +101 -65
- package/dist/kit/typography.mjs +2 -0
- package/dist/layout/hyphenation.mjs +127 -0
- package/dist/layout/line-plan.mjs +88 -96
- package/dist/layout/model.mjs +61 -80
- package/dist/layout/paragraph-fit.mjs +191 -0
- package/dist/layout/preparation.mjs +373 -0
- package/dist/layout/prepared-paragraph.mjs +223 -0
- package/dist/layout/typography.mjs +345 -111
- package/dist/packages/rapier-diff/index.mjs +288 -196
- package/dist/spec/document-fonts.mjs +13 -4
- package/dist/spec/md-assets.mjs +149 -30
- package/dist/spec/md-layout.mjs +5 -2
- package/dist/spec/md-marks.mjs +218 -3
- package/dist/spec/md-source.mjs +122 -0
- package/package.json +2 -1
- package/style.css +9 -0
- package/dist/agent/vendor/pretext/bidi.js +0 -151
- package/dist/agent/vendor/pretext/generated/bidi-data.js +0 -3831
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
+
import {createPreparationScope, supportsRichStyle, captureFontAuthority} from './preparation.mjs';
|
|
3
|
+
import {createParagraphFitter} from './paragraph-fit.mjs';
|
|
2
4
|
|
|
3
5
|
// Layout projections keep the original text in place. The factory also runs in an exported page.
|
|
4
|
-
export function createTypography() {
|
|
6
|
+
export function createTypography(fitter = createParagraphFitter()) {
|
|
5
7
|
const mark = 'data-rapier-typography', boundary = mark + '-break', hang = mark + '-hang';
|
|
6
8
|
const protectedInitial = mark + '-protected-initial';
|
|
9
|
+
const gapMark = mark + '-gap', fitMark = mark + '-fit';
|
|
7
10
|
const exclude = 'code,kbd,samp,math,script,style,.math-rendered,[hidden]';
|
|
8
11
|
const candidates = 'p[data-md-hyphens="auto"],p[data-md-lines],p[data-md-hanging="on"],p[' + mark + ']';
|
|
9
12
|
const properties = ['--md-drop-cap-font', '--md-hang-start'];
|
|
10
|
-
const states = new WeakMap(), documents = new WeakMap();
|
|
11
|
-
|
|
13
|
+
const states = new WeakMap(), documents = new WeakMap(), live = new Set();
|
|
14
|
+
function scope(document) {
|
|
15
|
+
let value = documents.get(document);
|
|
16
|
+
if (!value) { value = createPreparationScope(document); documents.set(document, value); }
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
let graphemes;
|
|
20
|
+
const segment = text => (graphemes ||= new Intl.Segmenter(undefined, {granularity: 'grapheme'})).segment(text);
|
|
12
21
|
const opening = /^[\p{Ps}\p{Pi}\p{Pf}"']+$/u;
|
|
13
22
|
const closing = /^[\p{Pe}\p{Pi}\p{Pf}"'.,;:!?]+$/u;
|
|
14
23
|
|
|
@@ -18,7 +27,7 @@ export function createTypography() {
|
|
|
18
27
|
const start = /^\s*/u.exec(text)[0].length;
|
|
19
28
|
if (start === text.length) return null;
|
|
20
29
|
let end = start;
|
|
21
|
-
for (const part of
|
|
30
|
+
for (const part of segment(text.slice(start))) {
|
|
22
31
|
if (/^\s/u.test(part.segment)) break;
|
|
23
32
|
end = start + part.index + part.segment.length;
|
|
24
33
|
if (!opening.test(part.segment)) break;
|
|
@@ -30,11 +39,12 @@ export function createTypography() {
|
|
|
30
39
|
return [...(root?.matches?.(selector) ? [root] : []), ...(root?.querySelectorAll?.(selector) || [])];
|
|
31
40
|
}
|
|
32
41
|
|
|
33
|
-
function runs(paragraph) {
|
|
34
|
-
const walker = paragraph.ownerDocument.createTreeWalker(paragraph, 4), result = [];
|
|
42
|
+
function runs(paragraph, hard = null) {
|
|
43
|
+
const walker = paragraph.ownerDocument.createTreeWalker(paragraph, hard ? 5 : 4), result = [];
|
|
35
44
|
let start = 0;
|
|
36
45
|
while (walker.nextNode()) {
|
|
37
46
|
const node = walker.currentNode;
|
|
47
|
+
if (node.nodeType !== 3) { if (node.tagName === 'BR') hard.push(start); continue; }
|
|
38
48
|
if (node.length) result.push({node, start, end: start + node.length});
|
|
39
49
|
start += node.length;
|
|
40
50
|
}
|
|
@@ -55,7 +65,7 @@ export function createTypography() {
|
|
|
55
65
|
// This data-only guard is safe on an active edit surface, including during composition.
|
|
56
66
|
function protectInitial(root) {
|
|
57
67
|
for (const paragraph of paragraphs(root, 'p[data-md-lines],p[data-md-hanging="on"],p[' + protectedInitial + ']')) {
|
|
58
|
-
const initial = initialRange(paragraph), letter = initial && [...
|
|
68
|
+
const initial = initialRange(paragraph), letter = initial && [...segment(initial.text)].at(-1);
|
|
59
69
|
const at = initial && point(runs(paragraph), initial.start + letter.index);
|
|
60
70
|
const enabled = paragraph.hasAttribute('data-md-lines') || paragraph.getAttribute('data-md-hanging') === 'on';
|
|
61
71
|
paragraph.toggleAttribute(protectedInitial, Boolean(enabled && at?.node.parentElement?.closest(exclude)));
|
|
@@ -92,7 +102,7 @@ export function createTypography() {
|
|
|
92
102
|
const items = runs(paragraph), text = paragraph.textContent;
|
|
93
103
|
const initial = paragraph.hasAttribute('data-md-lines') ? initialRange(text) : null;
|
|
94
104
|
let first = null, end = null;
|
|
95
|
-
for (const part of
|
|
105
|
+
for (const part of segment(text)) {
|
|
96
106
|
if (initial && part.index < initial.end) continue;
|
|
97
107
|
if (/^\s+$/u.test(part.segment)) continue;
|
|
98
108
|
const rects = boxes(items, part.index, part.index + part.segment.length, height);
|
|
@@ -108,6 +118,7 @@ export function createTypography() {
|
|
|
108
118
|
function setProperty(paragraph, state, name, value) {
|
|
109
119
|
if (!state.properties.has(name)) state.properties.set(name, [paragraph.style.getPropertyValue(name), paragraph.style.getPropertyPriority(name)]);
|
|
110
120
|
paragraph.style.setProperty(name, value);
|
|
121
|
+
state.properties.get(name)[2] = paragraph.style.getPropertyValue(name);
|
|
111
122
|
}
|
|
112
123
|
|
|
113
124
|
function selectionBefore(paragraph) {
|
|
@@ -164,96 +175,48 @@ export function createTypography() {
|
|
|
164
175
|
for (const {move, pieces} of restore) { move.node.data = move.before; for (const piece of pieces.slice(1)) piece.remove(); }
|
|
165
176
|
}
|
|
166
177
|
|
|
167
|
-
function clear(root) {
|
|
178
|
+
function clear(root, {forget = true} = {}) {
|
|
168
179
|
const list = paragraphs(root, 'p[' + mark + '],p[' + protectedInitial + ']');
|
|
169
180
|
for (const paragraph of list) {
|
|
170
181
|
if (!paragraph.hasAttribute(mark)) { paragraph.removeAttribute(protectedInitial); continue; }
|
|
171
182
|
const selected = selectionBefore(paragraph);
|
|
172
|
-
for (const node of paragraph.querySelectorAll('[' + boundary + '],[' + hang + ']')) node.replaceWith(...node.childNodes);
|
|
173
183
|
const state = states.get(paragraph);
|
|
184
|
+
state?.work?.return();
|
|
185
|
+
state?.fitWork?.return();
|
|
186
|
+
for (const node of paragraph.querySelectorAll('[' + boundary + '],[' + hang + '],[' + gapMark + ']')) node.replaceWith(...node.childNodes);
|
|
174
187
|
restoreInitial(state);
|
|
175
188
|
for (const name of properties) {
|
|
176
189
|
const previous = state?.properties.get(name);
|
|
190
|
+
if (!previous || paragraph.style.getPropertyValue(name) !== previous[2] || paragraph.style.getPropertyPriority(name)) continue;
|
|
177
191
|
if (previous?.[0]) paragraph.style.setProperty(name, previous[0], previous[1]);
|
|
178
192
|
else paragraph.style.removeProperty(name);
|
|
179
193
|
}
|
|
180
194
|
paragraph.removeAttribute(mark); paragraph.removeAttribute(mark + '-ragged');
|
|
195
|
+
paragraph.removeAttribute(fitMark);
|
|
181
196
|
paragraph.removeAttribute(protectedInitial);
|
|
182
197
|
if (!paragraph.getAttribute('style')) paragraph.removeAttribute('style');
|
|
183
198
|
paragraph.normalize();
|
|
184
199
|
states.delete(paragraph);
|
|
200
|
+
live.delete(paragraph);
|
|
201
|
+
if (forget) scope(paragraph.ownerDocument).forget(paragraph);
|
|
185
202
|
restoreSelection(paragraph, selected);
|
|
186
203
|
}
|
|
187
204
|
}
|
|
188
205
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
paragraph.closest('[lang]')?.getAttribute('lang'), fontEpoch(paragraph.ownerDocument)].join('\u0000');
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function insertBoundary(paragraph, items, offset) {
|
|
207
|
-
const at = point(items, offset);
|
|
208
|
-
if (!at || at.node.parentElement?.closest(exclude + ',[' + boundary + ']')) return false;
|
|
209
|
-
const separator = /^[\t\n\r\f ]+/.exec(at.node.data.slice(at.offset));
|
|
210
|
-
if (!separator) return false;
|
|
211
|
-
let text = at.node;
|
|
212
|
-
const end = at.offset + separator[0].length;
|
|
213
|
-
if (end < text.length) text.splitText(end);
|
|
214
|
-
if (at.offset) text = text.splitText(at.offset);
|
|
215
|
-
const node = paragraph.ownerDocument.createElement('span'); node.setAttribute(boundary, '');
|
|
216
|
-
text.before(node); node.append(text);
|
|
217
|
-
return true;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function stretchedLines(paragraph, style, maximum) {
|
|
221
|
-
const text = paragraph.textContent, items = runs(paragraph), height = ordinaryHeight(paragraph, style), lines = [];
|
|
222
|
-
for (const match of text.matchAll(/[\t\n\r\f \u00a0\u202f]+/gu)) {
|
|
223
|
-
const start = point(items, match.index), end = point(items, match.index + match[0].length, true);
|
|
224
|
-
if (start?.node.parentElement?.closest(exclude) || end?.node.parentElement?.closest(exclude)) continue;
|
|
225
|
-
for (const box of boxes(items, match.index, match.index + match[0].length, height)) {
|
|
226
|
-
if (box.width <= maximum + .1 || lines.some(line => sameLine(line.box, box))) continue;
|
|
227
|
-
lines.push({box, end: -1});
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
if (!lines.length) return {items, lines};
|
|
231
|
-
for (const match of text.matchAll(/[^\t\n\r\f ]+/gu)) {
|
|
232
|
-
const rects = boxes(items, match.index, match.index + match[0].length, height);
|
|
233
|
-
if (!rects.length) continue;
|
|
234
|
-
const last = rects[rects.length - 1];
|
|
235
|
-
for (const line of lines) if (sameLine(line.box, last)) line.end = match.index + match[0].length;
|
|
236
|
-
}
|
|
237
|
-
return {items, lines};
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
function justify(paragraph, style) {
|
|
241
|
-
const maximum = parseFloat(style.fontSize) * .6;
|
|
242
|
-
if (!(maximum > 0) || style.writingMode !== 'horizontal-tb') return 0;
|
|
243
|
-
let count = 0;
|
|
244
|
-
// Each released line changes the native pretty-wrap solution. Use only the earliest current
|
|
245
|
-
// line, then measure again so obsolete later endpoints cannot create isolated words.
|
|
246
|
-
for (let pass = 0; pass < 64; pass++) {
|
|
247
|
-
const {items, lines} = stretchedLines(paragraph, style, maximum);
|
|
248
|
-
if (!lines.length) return count;
|
|
249
|
-
const end = lines.map(line => line.end).filter(end => end > 0 && end < paragraph.textContent.trimEnd().length).sort((a, b) => a - b)[0];
|
|
250
|
-
if (!end || !insertBoundary(paragraph, items, end)) break;
|
|
251
|
-
count++;
|
|
252
|
-
}
|
|
253
|
-
// If no safe ordinary-word boundary can release the line, retain the natural paragraph flow.
|
|
254
|
-
for (const node of paragraph.querySelectorAll('[' + boundary + ']')) node.replaceWith(...node.childNodes);
|
|
255
|
-
paragraph.setAttribute(mark + '-ragged', '');
|
|
256
|
-
return 0;
|
|
206
|
+
// Native clipboard HTML must see the original inline tree. Clear the selected
|
|
207
|
+
// projection before the browser/host captures its Range, retaining current metrics.
|
|
208
|
+
// The host schedules ordinary refinement again after the synchronous clipboard event.
|
|
209
|
+
function clearSelection(root) {
|
|
210
|
+
const selection=(root?.ownerDocument || root)?.getSelection?.();
|
|
211
|
+
if (!selection?.rangeCount || selection.isCollapsed) return 0;
|
|
212
|
+
const range=selection.getRangeAt(0), common=range.commonAncestorContainer;
|
|
213
|
+
let within=common.nodeType===1?common:common.parentElement;
|
|
214
|
+
if (!within || !root.contains(within)) within=root;
|
|
215
|
+
else within=within.closest('p['+mark+']') || within;
|
|
216
|
+
const selected=paragraphs(within,'p['+mark+']').filter(paragraph=>
|
|
217
|
+
states.has(paragraph) && root.contains(paragraph) && range.intersectsNode(paragraph));
|
|
218
|
+
for (const paragraph of selected) clear(paragraph,{forget:false});
|
|
219
|
+
return selected.length;
|
|
257
220
|
}
|
|
258
221
|
|
|
259
222
|
// Optical alignment uses the available margin; paint containment and narrow pages must not
|
|
@@ -282,11 +245,11 @@ export function createTypography() {
|
|
|
282
245
|
|
|
283
246
|
function hangPunctuation(paragraph, state, style, edge) {
|
|
284
247
|
if (paragraph.ownerDocument.defaultView.CSS.supports('hanging-punctuation', 'first last allow-end')) return;
|
|
285
|
-
const text = paragraph.textContent, parts = [...
|
|
248
|
+
const text = paragraph.textContent, parts = [...segment(text)], items = runs(paragraph);
|
|
286
249
|
const initial = initialRange(text);
|
|
287
250
|
if (initial && !paragraph.hasAttribute(protectedInitial)) {
|
|
288
251
|
let end = initial.start;
|
|
289
|
-
for (const part of
|
|
252
|
+
for (const part of segment(initial.text)) {
|
|
290
253
|
if (!opening.test(part.segment)) break;
|
|
291
254
|
end = initial.start + part.index + part.segment.length;
|
|
292
255
|
}
|
|
@@ -315,50 +278,321 @@ export function createTypography() {
|
|
|
315
278
|
node.before(wrapper); wrapper.append(node);
|
|
316
279
|
}
|
|
317
280
|
|
|
318
|
-
|
|
319
|
-
|
|
281
|
+
const shaping = ['fontFamily', 'fontSize', 'fontWeight', 'fontStyle', 'fontStretch', 'fontKerning', 'fontOpticalSizing',
|
|
282
|
+
'fontFeatureSettings', 'fontVariationSettings', 'fontVariant', 'fontVariantLigatures', 'fontVariantCaps',
|
|
283
|
+
'fontVariantNumeric', 'fontVariantEastAsian', 'fontVariantPosition', 'fontVariantAlternates', 'fontSizeAdjust',
|
|
284
|
+
'fontSynthesis', 'fontSynthesisWeight', 'fontSynthesisStyle', 'fontSynthesisSmallCaps', 'letterSpacing', 'wordSpacing',
|
|
285
|
+
'textRendering', 'textTransform', 'direction', 'unicodeBidi', 'writingMode', 'verticalAlign', 'lineHeight',
|
|
286
|
+
'whiteSpace', 'wordBreak', 'hyphens', 'hyphenateCharacter', 'lineBreak', 'hangingPunctuation', 'tabSize', 'textWrapStyle'];
|
|
287
|
+
const admission = ['display', 'position', 'cssFloat', 'transform', 'zoom', 'paddingLeft', 'paddingRight',
|
|
288
|
+
'borderLeftWidth', 'borderRightWidth', 'marginLeft', 'marginRight', 'textIndent'];
|
|
289
|
+
const attributes = ['data-md-layout', 'data-md-align', 'data-md-lines', 'data-md-hyphens', 'data-md-caps', 'data-md-hanging', 'style', 'class', 'dir', 'lang'];
|
|
290
|
+
const shapeKey = style => shaping.map(name => style[name]).join('|');
|
|
291
|
+
|
|
292
|
+
function inspect(paragraph, parents, prior, baseStyle) {
|
|
293
|
+
const document = paragraph.ownerDocument, view = document.defaultView, styles = new Map(), keys = [captureFontAuthority(document)];
|
|
294
|
+
let supported = document === globalThis.document;
|
|
295
|
+
for (let element = paragraph.parentElement; element; element = element.parentElement) {
|
|
296
|
+
const s = view.getComputedStyle(element);
|
|
297
|
+
keys.push(s.transform, s.zoom, s.whiteSpace, s.direction, s.writingMode,
|
|
298
|
+
s.hyphens, s.hangingPunctuation, s.textWrapStyle, element.getAttribute('lang'));
|
|
299
|
+
if (s.transform !== 'none' || !['1', 'normal'].includes(s.zoom) || s.whiteSpace !== 'normal') supported = false;
|
|
300
|
+
}
|
|
301
|
+
for (const element of parents) {
|
|
302
|
+
const computed = element === paragraph ? baseStyle : view.getComputedStyle(element);
|
|
303
|
+
const s = Object.fromEntries([...shaping, ...admission].map(name => [name, computed[name]]));
|
|
304
|
+
// The fit's own CSS suppresses native rewrapping. Source attributes and ancestor
|
|
305
|
+
// whitespace remain in the key; clearing reads the current unprojected styles anew.
|
|
306
|
+
if (prior?.plan) for (const name of ['whiteSpace', 'hyphens', 'hangingPunctuation', 'textWrapStyle'])
|
|
307
|
+
s[name] = prior.saved.styles.get(element)?.[name] ?? s[name];
|
|
308
|
+
const before = view.getComputedStyle(element, '::before').content, after = view.getComputedStyle(element, '::after').content;
|
|
309
|
+
styles.set(element, s);
|
|
310
|
+
keys.push(shapeKey(s), ...admission.map(name => s[name]), before, after);
|
|
311
|
+
if (s.transform !== 'none' || !['1','normal'].includes(s.zoom) || s.cssFloat !== 'none' ||
|
|
312
|
+
!['static','relative'].includes(s.position) || element !== paragraph && element.tagName !== 'BR' && s.display !== 'inline' ||
|
|
313
|
+
!['none','normal'].includes(before) || !['none','normal'].includes(after)) supported = false;
|
|
314
|
+
}
|
|
315
|
+
keys.push(...attributes.map(name => paragraph.getAttribute(name)));
|
|
316
|
+
return {key: keys.join('\u0000'), generation:keys[0], styles, parents, supported};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function snapshot(paragraph, inspection) {
|
|
320
|
+
const text = paragraph.textContent, language = paragraph.closest('[lang]')?.getAttribute('lang') ?? '';
|
|
321
|
+
if (!inspection.supported || !text.trim() || paragraph.hasAttribute('data-md-lines') ||
|
|
322
|
+
paragraph.getAttribute('data-md-caps') === 'small' || paragraph.closest('[data-rapier-published-ink]') ||
|
|
323
|
+
/[^\p{Script=Latin}\p{Script=Greek}\p{Script=Cyrillic}\p{Script=Common}\p{Script=Inherited}]/u.test(text) ||
|
|
324
|
+
/[\u061c\u200e\u200f\u2028-\u202e\u2066-\u2069]/u.test(text)) return null;
|
|
325
|
+
const hard = [], items = runs(paragraph, hard), atoms = [];
|
|
326
|
+
for (const element of inspection.parents) {
|
|
327
|
+
if (element !== paragraph && !/^(A|SPAN|EM|I|STRONG|B|DEL|S|MARK|CODE|KBD|SAMP|U|INS|ABBR|BR)$/.test(element.tagName)) return null;
|
|
328
|
+
if (element.hasAttribute('hidden') || element.getAttribute('aria-hidden') === 'true' || element.hasAttribute('data-rapier-source') ||
|
|
329
|
+
element.hasAttribute('lang') && element.getAttribute('lang').toLowerCase() !== language.toLowerCase() ||
|
|
330
|
+
['white-space','text-wrap','hyphens','hanging-punctuation','word-spacing'].some(name => element.style.getPropertyPriority(name))) return null;
|
|
331
|
+
const s = inspection.styles.get(element), atom = element !== paragraph && (/^(CODE|KBD|SAMP)$/.test(element.tagName) || s.whiteSpace === 'nowrap');
|
|
332
|
+
if (s.hyphenateCharacter && s.hyphenateCharacter !== 'auto') return null;
|
|
333
|
+
if (!supportsRichStyle({...s, hyphens:'manual', hangingPunctuation:'none', whiteSpace: atom ? 'nowrap' : s.whiteSpace}, language) ||
|
|
334
|
+
!atom && s.whiteSpace !== 'normal') return null;
|
|
335
|
+
const extras = ['paddingLeft','paddingRight','borderLeftWidth','borderRightWidth','marginLeft','marginRight']
|
|
336
|
+
.reduce((sum,name) => sum + Math.abs(parseFloat(s[name]) || 0),0);
|
|
337
|
+
if (atom) {
|
|
338
|
+
const contained = items.filter(item => element.contains(item.node));
|
|
339
|
+
if (!contained.length || atoms.some(previous => previous.element.contains(element))) continue;
|
|
340
|
+
const start = contained[0].start, end = contained.at(-1).end;
|
|
341
|
+
if (/^[\t\r\n\f ]|[\t\r\n\f ]$/.test(text.slice(start,end)) || hard.some(at => at > start && at < end) ||
|
|
342
|
+
contained.some(item => shapeKey(inspection.styles.get(item.node.parentElement)) !== shapeKey(s))) return null;
|
|
343
|
+
atoms.push({element,start,end,extraWidth:extras});
|
|
344
|
+
} else if (element !== paragraph && extras > .01) return null;
|
|
345
|
+
}
|
|
346
|
+
const inline = [], paragraphBreaks = [0]; let hardIndex = 0;
|
|
347
|
+
for (const run of items) {
|
|
348
|
+
let broke = false;
|
|
349
|
+
while (hardIndex < hard.length && hard[hardIndex] <= run.start) { paragraphBreaks.push(inline.length); hardIndex++; broke = true; }
|
|
350
|
+
const atom = atoms.find(atom => atom.start <= run.start && run.start < atom.end);
|
|
351
|
+
if (atom && atom.start !== run.start) continue;
|
|
352
|
+
const s = inspection.styles.get(run.node.parentElement), end = atom?.end ?? run.end;
|
|
353
|
+
const font = s.fontStyle + ' ' + s.fontWeight + ' ' + s.fontSize + ' ' + s.fontFamily;
|
|
354
|
+
const letterSpacing = parseFloat(s.letterSpacing) || 0, body = text.slice(run.start,end), last = inline.at(-1);
|
|
355
|
+
const hyphenate = s.hyphens === 'auto' && !atom;
|
|
356
|
+
if (s.hyphens === 'none' && body.includes('\u00ad')) return null;
|
|
357
|
+
if (!broke && !atom && last && last.break !== 'never' && last.font === font && last.letterSpacing === letterSpacing && last.hyphenate === hyphenate) last.text += body;
|
|
358
|
+
else inline.push({text:body,font,letterSpacing,hyphenate,...(atom?{break:'never',extraWidth:atom.extraWidth}:{})});
|
|
359
|
+
}
|
|
360
|
+
while (hardIndex++ < hard.length) paragraphBreaks.push(inline.length);
|
|
361
|
+
paragraphBreaks.push(inline.length);
|
|
362
|
+
return {...inspection,text,items,atoms,inline,paragraphBreaks,language};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function dimensions(paragraph, style, box, hanging) {
|
|
366
|
+
const left = box.left + (parseFloat(style.borderLeftWidth)||0) + (parseFloat(style.paddingLeft)||0);
|
|
367
|
+
const right = box.right - (parseFloat(style.borderRightWidth)||0) - (parseFloat(style.paddingRight)||0);
|
|
368
|
+
let clipLeft = 0, clipRight = paragraph.ownerDocument.defaultView.innerWidth;
|
|
369
|
+
if (hanging) for (let element = paragraph; element; element = element.parentElement) {
|
|
370
|
+
const s = paragraph.ownerDocument.defaultView.getComputedStyle(element);
|
|
371
|
+
if (!/^(hidden|clip|scroll|auto)$/.test(s.overflowX) && !/(?:^|\s)(paint|content|strict)(?:\s|$)/.test(s.contain) && s.contentVisibility !== 'auto') continue;
|
|
372
|
+
const bounds = element.getBoundingClientRect();
|
|
373
|
+
clipLeft = Math.max(clipLeft,bounds.left+(parseFloat(s.borderLeftWidth)||0));
|
|
374
|
+
clipRight = Math.min(clipRight,bounds.right-(parseFloat(s.borderRightWidth)||0));
|
|
375
|
+
}
|
|
376
|
+
if (hanging && paragraph.ownerDocument.defaultView.matchMedia('print').matches) {
|
|
377
|
+
const content = paragraph.closest('.md-render')?.getBoundingClientRect();
|
|
378
|
+
if (content) { clipLeft = Math.max(clipLeft,content.left); clipRight = Math.min(clipRight,content.right); }
|
|
379
|
+
}
|
|
380
|
+
const width = Math.floor((right-left)*64)/64;
|
|
381
|
+
const indent = style.textIndent.endsWith('%') ? parseFloat(style.textIndent)*width/100 : parseFloat(style.textIndent)||0;
|
|
382
|
+
return {width,indent,left:hanging?Math.max(0,left-clipLeft):0,right:hanging?Math.max(0,clipRight-right):0};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const topology = plan => plan && plan.lines.map(line => [line.start,line.end,line.next,line.kind,line.glyph].join(':')).join('|');
|
|
386
|
+
function updateProjection(projection, plan) {
|
|
387
|
+
for (const {node,property,line,sign} of projection.bindings) {
|
|
388
|
+
const value = sign * plan.lines[line][property] + 'px';
|
|
389
|
+
const name = property === 'opening' ? 'marginInlineStart' : property === 'closing' ? 'marginInlineEnd' : 'wordSpacing';
|
|
390
|
+
if (node.style[name] !== value) node.style[name] = value;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function project(paragraph, saved, geometry, plan) {
|
|
395
|
+
if (paragraph.textContent !== saved.text) return false;
|
|
396
|
+
const operations = [], inserts = [], bindings = [];
|
|
397
|
+
for (let lineIndex = 0; lineIndex < plan.lines.length; lineIndex++) {
|
|
398
|
+
const line = plan.lines[lineIndex];
|
|
399
|
+
const words = geometry.segments[line.segment].words;
|
|
400
|
+
{
|
|
401
|
+
let start = line.start;
|
|
402
|
+
const add = end => { if (end > start && /[\t\r\n\f ]/.test(saved.text.slice(start,end)))
|
|
403
|
+
operations.push({start,end,spacing:true,line:lineIndex}); };
|
|
404
|
+
for (let index = line.firstWord; index <= line.lastWord; index++) {
|
|
405
|
+
const word = words[index];
|
|
406
|
+
if (!word.protected && !/[\u00a0\u202f\u2060]/.test(saved.text.slice(word.start,word.end))) continue;
|
|
407
|
+
add(word.start); start = word.end;
|
|
408
|
+
}
|
|
409
|
+
add(line.end);
|
|
410
|
+
}
|
|
411
|
+
if (line.kind === 'space') operations.push({start:line.end,end:line.next,boundary:true});
|
|
412
|
+
else if (line.kind !== 'end') inserts.push({offset:line.end,glyph:line.glyph});
|
|
413
|
+
const first = words[line.firstWord], last = words[line.lastWord];
|
|
414
|
+
if (first.opening && line.start === first.start) operations.push({start:first.start,end:first.openEnd,opening:true,line:lineIndex});
|
|
415
|
+
if (last.closing && line.end === last.end) operations.push({start:last.closeStart,end:last.end,closing:true,line:lineIndex});
|
|
416
|
+
}
|
|
417
|
+
// Sweep interval endpoints once. Each original text node is split from right to left;
|
|
418
|
+
// links, comments, IDs and inline ancestors never move or get reconstructed.
|
|
419
|
+
const events = new Map();
|
|
420
|
+
const event = offset => { let value = events.get(offset); if (!value) { value={start:[],end:[]}; events.set(offset,value); } return value; };
|
|
421
|
+
for (const op of operations) { event(op.start).start.push(op); event(op.end).end.push(op); }
|
|
422
|
+
for (const insert of inserts) event(insert.offset);
|
|
423
|
+
const offsets = [...events.keys()].sort((a,b)=>a-b), active = new Set(), pieces = [];
|
|
424
|
+
let cursor = 0;
|
|
425
|
+
for (const item of runs(paragraph)) {
|
|
426
|
+
let start = item.start;
|
|
427
|
+
while (start < item.end) {
|
|
428
|
+
while (cursor < offsets.length && offsets[cursor] <= start) {
|
|
429
|
+
const value = events.get(offsets[cursor++]);
|
|
430
|
+
for (const op of value.end) active.delete(op);
|
|
431
|
+
for (const op of value.start) active.add(op);
|
|
432
|
+
}
|
|
433
|
+
const end = Math.min(item.end,offsets[cursor] ?? item.end);
|
|
434
|
+
pieces.push({item,start,end,operations:[...active]}); start=end;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
// Validate every zero-source insertion before the first write, so a refused source
|
|
438
|
+
// coordinate cannot leave a partially projected paragraph behind.
|
|
439
|
+
const sourceEnds = new Set(pieces.map(piece => piece.end));
|
|
440
|
+
if (inserts.some(insert => !sourceEnds.has(insert.offset))) return false;
|
|
441
|
+
const ends = new Map();
|
|
442
|
+
for (let index=pieces.length-1;index>=0;index--) {
|
|
443
|
+
const piece=pieces[index], {item,start,end}=piece; let node=item.node;
|
|
444
|
+
const from=start-item.start,to=end-item.start;
|
|
445
|
+
if (to<node.length) node.splitText(to);
|
|
446
|
+
if (from) node=node.splitText(from);
|
|
447
|
+
let outer=node;
|
|
448
|
+
if (piece.operations.length) {
|
|
449
|
+
const wrapper=paragraph.ownerDocument.createElement('span'); wrapper.setAttribute(gapMark,'');
|
|
450
|
+
for (const op of piece.operations) {
|
|
451
|
+
if (op.spacing) bindings.push({node:wrapper,property:'wordSpacing',line:op.line,sign:1});
|
|
452
|
+
if (op.boundary) {
|
|
453
|
+
wrapper.setAttribute(mark+'-separator','');
|
|
454
|
+
if (end===op.end) wrapper.setAttribute(boundary,'');
|
|
455
|
+
}
|
|
456
|
+
if (op.opening && start===op.start) {wrapper.setAttribute(hang,'start');bindings.push({node:wrapper,property:'opening',line:op.line,sign:-1});}
|
|
457
|
+
if (op.closing && end===op.end) {wrapper.setAttribute(hang,'end');bindings.push({node:wrapper,property:'closing',line:op.line,sign:-1});}
|
|
458
|
+
}
|
|
459
|
+
node.before(wrapper);wrapper.append(node);outer=wrapper;
|
|
460
|
+
}
|
|
461
|
+
ends.set(end,outer);
|
|
462
|
+
}
|
|
463
|
+
for (const insert of inserts) {
|
|
464
|
+
const at=ends.get(insert.offset);
|
|
465
|
+
const marker=paragraph.ownerDocument.createElement('span');marker.setAttribute(boundary,insert.glyph?'hyphen':'');
|
|
466
|
+
if (insert.glyph) marker.style.setProperty('--md-fit-hyphen',JSON.stringify(insert.glyph));
|
|
467
|
+
at.after(marker);
|
|
468
|
+
}
|
|
469
|
+
const projection = {bindings,topology:topology(plan)};
|
|
470
|
+
updateProjection(projection,plan);
|
|
471
|
+
paragraph.setAttribute(fitMark,'');return projection;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function apply(root, {all=false}={}) {
|
|
475
|
+
const started = performance.now();
|
|
476
|
+
const generation = captureFontAuthority(root?.ownerDocument || globalThis.document);
|
|
477
|
+
for (const paragraph of live) {
|
|
478
|
+
const state = states.get(paragraph);
|
|
479
|
+
if (!paragraph.isConnected || state && state.generation !== generation) clear(paragraph);
|
|
480
|
+
}
|
|
481
|
+
const jobs=[];let pending=false,changed=0,released=0,preparedCount=0;
|
|
320
482
|
for (const paragraph of paragraphs(root)) {
|
|
321
483
|
if (paragraph.closest('table,pre,code,h1,h2,h3,h4,h5,h6,.block-edit') ||
|
|
322
|
-
paragraph.isContentEditable && !paragraph.closest('.block-read')
|
|
323
|
-
|
|
324
|
-
const view
|
|
484
|
+
paragraph.isContentEditable && !paragraph.closest('.block-read')) {clear(paragraph);continue;}
|
|
485
|
+
if (!paragraph.isConnected || !all && paragraph.checkVisibility && !paragraph.checkVisibility({contentVisibilityAuto:true})) continue;
|
|
486
|
+
const view=paragraph.ownerDocument.defaultView;
|
|
325
487
|
if (!view || !paragraph.getClientRects().length) continue;
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
const hanging
|
|
329
|
-
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
|
|
488
|
+
const style=view.getComputedStyle(paragraph),box=paragraph.getBoundingClientRect(),prior=states.get(paragraph);
|
|
489
|
+
if (!all && (box.bottom < -100 || box.top > view.innerHeight+100)) continue;
|
|
490
|
+
const hanging=paragraph.getAttribute('data-md-hanging')==='on',size=dimensions(paragraph,style,box,hanging);
|
|
491
|
+
if (!(size.width>0)) continue;
|
|
492
|
+
const parents=prior?.parents || [paragraph,...paragraph.querySelectorAll('*')];
|
|
493
|
+
const inspection=inspect(paragraph,parents,prior,style);
|
|
494
|
+
const reusable=prior?.html===paragraph.innerHTML && prior.key===inspection.key && (!prior.geometry || scope(paragraph.ownerDocument).current(prior.geometry));
|
|
495
|
+
const sizeKey=[size.width,size.indent,size.left,size.right].join('|');
|
|
496
|
+
if (reusable && !prior.pending && prior.sizeKey===sizeKey) continue;
|
|
497
|
+
// Preparation survives reflow; a suspended fit belongs to its captured width,
|
|
498
|
+
// indent and margins. Cancel only that plan when dimensions have changed.
|
|
499
|
+
const fitWork=reusable && prior.sizeKey===sizeKey?prior.fitWork:null;
|
|
500
|
+
if (prior?.fitWork && prior.fitWork!==fitWork) {prior.fitWork.return();prior.fitWork=null;}
|
|
501
|
+
if (!all && jobs.length===4) {if (!reusable) clear(paragraph);pending=true;continue;}
|
|
502
|
+
jobs.push({paragraph,view,style,inspection,size,sizeKey,hanging,selected:selectionBefore(paragraph),
|
|
503
|
+
cap:paragraph.hasAttribute('data-md-lines'),justified:paragraph.getAttribute('data-md-align')==='justify' && paragraph.getAttribute('data-md-hyphens')==='auto',
|
|
504
|
+
saved:reusable?prior.saved:null,geometry:reusable?prior.geometry:null,prepared:reusable?prior.prepared:null,
|
|
505
|
+
work:reusable?prior.work:null,fitWork,
|
|
506
|
+
prior,reusable,needsClear:paragraph.hasAttribute(mark)});
|
|
507
|
+
}
|
|
508
|
+
// Remove obsolete source/style projections before yielding. Unprepared paragraphs
|
|
509
|
+
// use native layout until their turn; no stale font plan remains on the page.
|
|
510
|
+
for (const job of jobs) if (job.needsClear && !job.saved) clear(job.paragraph);
|
|
511
|
+
function stateFor(job) {
|
|
512
|
+
return {properties:new Map(),moves:[],saved:job.saved,geometry:job.geometry,prepared:job.prepared,plan:job.plan,
|
|
513
|
+
work:job.work,fitWork:job.fitWork,parents:job.inspection.parents,key:job.inspection.key,generation:job.inspection.generation,sizeKey:job.sizeKey};
|
|
514
|
+
}
|
|
515
|
+
function suspend(job) {
|
|
516
|
+
const state=stateFor(job);
|
|
517
|
+
// A reusable projection may remain readable while a new width is being fitted.
|
|
518
|
+
// Retain its restoration custody until it is updated or explicitly cleared.
|
|
519
|
+
if (job.reusable && job.prior) for (const name of ['properties','moves','projection','plan']) state[name]=job.prior[name];
|
|
520
|
+
state.pending=true;state.html=job.paragraph.innerHTML;
|
|
521
|
+
states.set(job.paragraph,state);live.add(job.paragraph);job.paragraph.setAttribute(mark,'');pending=true;
|
|
522
|
+
}
|
|
523
|
+
// Both preparation and fitting yield through the same captured source/font owner.
|
|
524
|
+
// Native text remains readable until its plan is ready. Each continuation
|
|
525
|
+
// rereads the current source/style/font and width; it cannot publish a suspended plan.
|
|
526
|
+
// A single native shaping call can exceed this allowance, so this is not a deadline.
|
|
527
|
+
for (let jobIndex = 0; jobIndex < jobs.length; jobIndex++) {
|
|
528
|
+
if (!all && jobIndex && performance.now() - started >= 8) {pending=true;break;}
|
|
529
|
+
const job = jobs[jobIndex];
|
|
530
|
+
if (!job.saved && job.justified) {
|
|
531
|
+
if (job.needsClear) job.inspection=inspect(job.paragraph,[job.paragraph,...job.paragraph.querySelectorAll('*')],null,job.view.getComputedStyle(job.paragraph));
|
|
532
|
+
job.saved=snapshot(job.paragraph,job.inspection);
|
|
533
|
+
if (job.saved) {
|
|
534
|
+
job.work=scope(job.paragraph.ownerDocument).prepareSteps(job.paragraph,job.saved.inline,{policy:'paragraph',language:job.saved.language,
|
|
535
|
+
whiteSpace:'normal',wordBreak:job.saved.styles.get(job.paragraph).wordBreak,hanging:job.hanging,paragraphBreaks:job.saved.paragraphBreaks});
|
|
536
|
+
if (!all && performance.now()-started>=8) {suspend(job);continue;}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
if (job.work) {
|
|
540
|
+
for (;;) {
|
|
541
|
+
const step=job.work.next();
|
|
542
|
+
if (step.done) {job.geometry=step.value;job.work=null;preparedCount++;break;}
|
|
543
|
+
if (!all && performance.now()-started>=8) break;
|
|
544
|
+
}
|
|
545
|
+
if (job.work || !all && performance.now()-started>=8) {suspend(job);continue;}
|
|
546
|
+
}
|
|
547
|
+
if (job.geometry && !job.prepared) job.prepared=fitter.prepare(job.geometry);
|
|
548
|
+
if (job.prepared && !job.fitWork) job.fitWork=scope(job.paragraph.ownerDocument).fitSteps(job.paragraph,job.geometry,
|
|
549
|
+
known=>fitter.solveSteps(job.prepared,job.size.width,job.size,known));
|
|
550
|
+
if (job.fitWork) {
|
|
551
|
+
for (;;) {
|
|
552
|
+
const step=job.fitWork.next();
|
|
553
|
+
if (step.done) {job.plan=step.value;job.fitWork=null;break;}
|
|
554
|
+
if (!all && performance.now()-started>=8) break;
|
|
555
|
+
}
|
|
556
|
+
if (job.fitWork) {suspend(job);continue;}
|
|
557
|
+
}
|
|
558
|
+
job.projection=job.reusable && job.plan && job.prior?.projection?.topology===topology(job.plan) ? job.prior.projection : null;
|
|
559
|
+
if (job.needsClear && job.saved && !job.projection) clear(job.paragraph,{forget:false});
|
|
560
|
+
const {paragraph,view,hanging,cap,justified}=job;
|
|
561
|
+
if (!cap && !hanging && !justified) {restoreSelection(paragraph,job.selected);continue;}
|
|
562
|
+
const state=stateFor(job);
|
|
563
|
+
states.set(paragraph,state);live.add(paragraph);paragraph.setAttribute(mark,'');
|
|
564
|
+
if (cap || hanging) protectInitial(paragraph);
|
|
340
565
|
if (cap && !paragraph.hasAttribute(protectedInitial)) {
|
|
341
|
-
const initial
|
|
342
|
-
const at
|
|
343
|
-
if (at?.node.parentElement) setProperty(paragraph,
|
|
344
|
-
if (initial && letter) unifyInitial(paragraph,
|
|
566
|
+
const initial=initialRange(paragraph),letter=initial && [...segment(initial.text)].at(-1);
|
|
567
|
+
const at=initial && point(runs(paragraph),initial.start+letter.index);
|
|
568
|
+
if (at?.node.parentElement) setProperty(paragraph,state,'--md-drop-cap-font',view.getComputedStyle(at.node.parentElement).fontFamily);
|
|
569
|
+
if (initial && letter) unifyInitial(paragraph,state,initial,initial.start+letter.index);
|
|
345
570
|
}
|
|
346
|
-
if (
|
|
347
|
-
|
|
348
|
-
if (
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
changed++;
|
|
571
|
+
if (job.projection) {updateProjection(job.projection,job.plan);state.projection=job.projection;}
|
|
572
|
+
else if (job.plan) state.projection=project(paragraph,job.saved,job.geometry,job.plan);
|
|
573
|
+
if (state.projection) released+=job.plan.lines.filter(line=>line.kind!=='end').length;
|
|
574
|
+
else if (hanging) hangPunctuation(paragraph,state,job.style,hangingEdge(paragraph,job.style,paragraph.getBoundingClientRect()));
|
|
575
|
+
state.html=paragraph.innerHTML;
|
|
576
|
+
restoreSelection(paragraph,job.selected);changed++;
|
|
352
577
|
}
|
|
353
|
-
return {changed,
|
|
578
|
+
return {changed,released,pending,prepared:preparedCount};
|
|
354
579
|
}
|
|
355
580
|
|
|
356
|
-
|
|
581
|
+
function retire(root, {documentChanged=false}={}) {
|
|
582
|
+
clear(root);
|
|
583
|
+
for (const paragraph of live) if (!paragraph.isConnected) clear(paragraph);
|
|
584
|
+
scope(root?.ownerDocument || root || globalThis.document).retire({documentChanged});
|
|
585
|
+
}
|
|
586
|
+
const invalidateFonts = document => scope(document || globalThis.document).invalidateFonts();
|
|
587
|
+
return {apply,clear,clearSelection,firstLineEnd,initialRange,protectInitial,invalidateFonts,retire};
|
|
357
588
|
}
|
|
358
589
|
|
|
359
590
|
const typography = createTypography();
|
|
360
591
|
export const apply = typography.apply;
|
|
361
592
|
export const clear = typography.clear;
|
|
593
|
+
export const clearSelection = typography.clearSelection;
|
|
362
594
|
export const firstLineEnd = typography.firstLineEnd;
|
|
363
595
|
export const initialRange = typography.initialRange;
|
|
364
596
|
export const protectInitial = typography.protectInitial;
|
|
597
|
+
export const invalidateFonts = typography.invalidateFonts;
|
|
598
|
+
export const retire = typography.retire;
|