rapier-markdown-kit 1.2.8 → 1.2.9
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/dist/agent/will.mjs +0 -6
- package/dist/images/gif.mjs +0 -3
- package/dist/images/playback.mjs +0 -10
- package/dist/kit/assets.mjs +0 -1
- package/dist/kit/conformance/measurer.mjs +0 -0
- package/dist/kit/conformance/run.mjs +1 -8
- package/dist/kit/export-fonts.mjs +0 -14
- package/dist/kit/ledger/authored-origin.mjs +0 -5
- package/dist/kit/ledger/authorship.mjs +0 -2
- package/dist/kit/ledger/carried.mjs +4 -7
- package/dist/kit/ledger/data.mjs +0 -8
- package/dist/kit/ledger/format.mjs +0 -25
- package/dist/kit/ledger/hash.mjs +11 -7
- package/dist/kit/ledger/history.mjs +10 -29
- package/dist/kit/ledger/inverse-source.mjs +0 -46
- package/dist/kit/ledger/journal-records.mjs +24 -56
- package/dist/kit/ledger/merge.mjs +0 -27
- package/dist/kit/ledger/text-merge.mjs +0 -20
- package/dist/kit/ledger/transport.mjs +0 -47
- package/dist/kit/ledger.mjs +0 -4
- package/dist/kit/markdown-it.mjs +0 -3
- package/dist/kit/pdf-glyphs.mjs +1 -10
- package/dist/kit/pdf-source.mjs +5 -5
- package/dist/kit/pdf-vectors.mjs +0 -3
- package/dist/kit/print-capitals.mjs +0 -4
- package/dist/kit/render-entry.mjs +0 -1
- package/dist/kit/render-ink.mjs +0 -5
- package/dist/kit/render-markdown.mjs +12 -117
- package/dist/kit/render-print.mjs +1 -47
- package/dist/kit/render-sanitize.mjs +7 -59
- package/dist/kit/render-styles.mjs +0 -7
- package/dist/kit/render-work.mjs +0 -12
- package/dist/kit/render.mjs +18 -173
- package/dist/layout/hyphenation.mjs +0 -23
- package/dist/layout/line-plan.mjs +0 -20
- package/dist/layout/model.mjs +0 -55
- package/dist/layout/paragraph-fit.mjs +50 -60
- package/dist/layout/preparation.mjs +23 -56
- package/dist/layout/prepared-paragraph.mjs +0 -25
- package/dist/layout/typography.mjs +103 -41
- package/dist/packages/rapier-diff/concerns.mjs +0 -16
- package/dist/packages/rapier-diff/diff.mjs +0 -7
- package/dist/packages/rapier-diff/index.mjs +0 -1
- package/dist/packages/rapier-diff/matching.mjs +0 -19
- package/dist/packages/rapier-diff/merge.mjs +0 -9
- package/dist/packages/rapier-diff/moves.mjs +0 -7
- package/dist/packages/rapier-diff/structure.mjs +0 -2
- package/dist/packages/rapier-diff/tokens.mjs +0 -6
- package/dist/packages/rapier-diff/walk.mjs +0 -12
- package/dist/spec/document-fonts.mjs +0 -9
- package/dist/spec/document-settings.mjs +4 -23
- package/dist/spec/frontmatter.mjs +0 -1
- package/dist/spec/html-blocks.mjs +0 -36
- package/dist/spec/md-assets.mjs +2 -61
- package/dist/spec/md-layout.mjs +0 -34
- package/dist/spec/md-marks.mjs +0 -51
- package/dist/spec/md-source.mjs +0 -16
- package/dist/spec/word-sidecar.mjs +0 -2
- package/package.json +1 -1
- package/style.css +32 -16
|
@@ -3,11 +3,6 @@ import {clearCache, setLocale} from '../agent/vendor/pretext/layout.js';
|
|
|
3
3
|
import {prepareRichInline} from '../agent/vendor/pretext/rich-inline.js';
|
|
4
4
|
import {prepareParagraphCandidateSteps, createParagraphObserver} from './prepared-paragraph.mjs';
|
|
5
5
|
|
|
6
|
-
// One metric authority for this loaded vendor graph. Document/face/paragraph identities are
|
|
7
|
-
// weak; a width is never part of source preparation. A separate captured fitting operation
|
|
8
|
-
// completes its requested interval facts before its immutable plan is ready. A font-string
|
|
9
|
-
// cache must be retired when the actual face changes, even if a replacement kept its family
|
|
10
|
-
// and all its CSS descriptors.
|
|
11
6
|
const documents = new WeakMap(), faces = new WeakMap(), paragraphs = new WeakMap();
|
|
12
7
|
let faceId = 0, generation = 0, language;
|
|
13
8
|
const descriptors = ['family', 'style', 'weight', 'stretch', 'unicodeRange', 'status',
|
|
@@ -17,8 +12,10 @@ const finite = value => typeof value === 'number' && Number.isFinite(value);
|
|
|
17
12
|
|
|
18
13
|
function clearMetrics() { clearCache(); return ++generation; }
|
|
19
14
|
|
|
15
|
+
let turn = null;
|
|
20
16
|
export function captureFontAuthority(document = globalThis.document) {
|
|
21
17
|
if (!document || typeof document !== 'object') return generation;
|
|
18
|
+
if (turn?.document === document && turn.generation === generation) return generation;
|
|
22
19
|
const previous = documents.get(document), declared = previous?.declared || new Set();
|
|
23
20
|
const entries = [];
|
|
24
21
|
try {
|
|
@@ -34,12 +31,13 @@ export function captureFontAuthority(document = globalThis.document) {
|
|
|
34
31
|
documents.set(document, {signature, declared});
|
|
35
32
|
clearMetrics();
|
|
36
33
|
}
|
|
34
|
+
if (!turn) queueMicrotask(() => { turn = null; });
|
|
35
|
+
turn = {document, generation};
|
|
37
36
|
return generation;
|
|
38
37
|
}
|
|
39
38
|
|
|
40
|
-
// Several hosts can observe the same FontFaceSet event. Its observed identity retires metrics
|
|
41
|
-
// once. A non-DOM host has no observable face set and explicitly announces its own replacement.
|
|
42
39
|
export function invalidateRichMetrics(document = globalThis.document) {
|
|
40
|
+
turn = null;
|
|
43
41
|
return document ? captureFontAuthority(document) : clearMetrics();
|
|
44
42
|
}
|
|
45
43
|
|
|
@@ -51,25 +49,27 @@ function familyName(value) {
|
|
|
51
49
|
.toLowerCase();
|
|
52
50
|
}
|
|
53
51
|
|
|
54
|
-
// A CSS family with no declared FontFace can intentionally resolve through its fallback list.
|
|
55
|
-
// For declared custom faces, a FontFaceSet.check() alone is insufficient: it can report true
|
|
56
|
-
// after a failed face has fallen back. Keep that failure and same-name replacement observable.
|
|
57
52
|
export function fontReady(document, font, text = ' ') {
|
|
58
53
|
if (!document) return true;
|
|
59
54
|
if (!document.fonts || document.fonts.status === 'loading') return false;
|
|
60
55
|
try {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const names =
|
|
65
|
-
const
|
|
66
|
-
for (const
|
|
67
|
-
const
|
|
68
|
-
if (
|
|
56
|
+
const parts = /^(?:(normal|italic|oblique(?:\s+-?[\d.]+deg)?)\s+)?(?:(\d+)\s+)?(?:\d*\.)?\d+px(?:\s*\/\s*[^\s]+)?\s+(.+)$/.exec(font);
|
|
57
|
+
if (!parts) return false;
|
|
58
|
+
const style = parts[1] || 'normal', weight = Number(parts[2] || 400);
|
|
59
|
+
const names = (parts[3].match(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[^,]+/g) || []).map(familyName);
|
|
60
|
+
const declared = documents.get(document)?.declared || new Set(), faces = [...document.fonts];
|
|
61
|
+
for (const name of names) {
|
|
62
|
+
const own = faces.filter(face => familyName(face.family) === name);
|
|
63
|
+
if (!own.length) return !declared.has(name);
|
|
64
|
+
const fitting = own.filter(face => {
|
|
65
|
+
const [low, high = low] = String(face.weight).split(/\s+/).map(Number);
|
|
66
|
+
return (face.style === 'normal') === (style === 'normal') && !(weight < low) && !(weight > high);
|
|
67
|
+
});
|
|
68
|
+
const pool = fitting.length ? fitting : own;
|
|
69
|
+
if (pool.some(face => face.status === 'loading')) return false;
|
|
70
|
+
if (pool.some(face => face.status === 'loaded')) return true;
|
|
71
|
+
if (pool.some(face => face.status === 'unloaded')) { document.fonts.load(font, text || ' ').catch(() => {}); return false; }
|
|
69
72
|
}
|
|
70
|
-
// A deleted declared face is unavailable even when check() now succeeds with a fallback.
|
|
71
|
-
// Its expected name belongs to this document lifetime, not an individual projection root.
|
|
72
|
-
for (const family of documents.get(document)?.declared || []) if (names.has(family) && !present.has(family)) return false;
|
|
73
73
|
return true;
|
|
74
74
|
} catch (_) { return false; }
|
|
75
75
|
}
|
|
@@ -81,17 +81,11 @@ function validLanguage(value) {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
function supportedDocument(document) {
|
|
84
|
-
if (!document) return true;
|
|
84
|
+
if (!document) return true;
|
|
85
85
|
const agent = document.defaultView?.navigator?.userAgent ?? globalThis.navigator?.userAgent ?? '';
|
|
86
86
|
return /(?:^|\s)(?:HeadlessChrome|Chrome|Chromium)\/\d+/.test(agent) && !/\bCriOS\//.test(agent);
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
// The pinned API accepts font shorthand, letter spacing, content language and two break modes.
|
|
90
|
-
// Canvas supplies its default shaping. It has no public feature/variation/kerning controls or
|
|
91
|
-
// paragraph-direction input; these styles retain the native selectable paragraph instead.
|
|
92
|
-
// `nowrap` here is for an atomic item; the paragraph itself still needs normal or pre-wrap.
|
|
93
|
-
// The app's inherited `pretty` is its existing greedy picture-flow policy, not an implementation
|
|
94
|
-
// of native pretty optimization. A host's explicit request for that optimization stays native.
|
|
95
89
|
export function supportsRichStyle(style, contentLanguage = '') {
|
|
96
90
|
if (!style || !validLanguage(contentLanguage)) return false;
|
|
97
91
|
const oneOf = (key, values) => style[key] == null || style[key] === '' || values.includes(style[key]);
|
|
@@ -158,8 +152,6 @@ function finish(work) {
|
|
|
158
152
|
}
|
|
159
153
|
|
|
160
154
|
function* prepareSteps(items, options, document) {
|
|
161
|
-
// FontFaceSet.check depends on the font and the used character set. Check each shaped
|
|
162
|
-
// font once across its runs; the scope is the sole font-readiness authority.
|
|
163
155
|
const usedFonts = new Map();
|
|
164
156
|
for (const item of items) if (item.text !== undefined) usedFonts.set(item.font, (usedFonts.get(item.font) || '') + item.text);
|
|
165
157
|
for (const [font, text] of usedFonts) {
|
|
@@ -167,8 +159,6 @@ function* prepareSteps(items, options, document) {
|
|
|
167
159
|
yield;
|
|
168
160
|
}
|
|
169
161
|
try {
|
|
170
|
-
// Locale selects content language for this preparation only. Existing handles already own
|
|
171
|
-
// their measured language; switching paragraphs must not invalidate those handles.
|
|
172
162
|
if (language !== options.language) { setLocale(options.language); language = options.language; }
|
|
173
163
|
if (options.paragraphBreaks && (options.paragraphBreaks[0] !== 0 || options.paragraphBreaks.at(-1) !== items.length ||
|
|
174
164
|
options.paragraphBreaks.some((value, index, all) => !Number.isInteger(value) || value < 0 || value > items.length || index > 0 && value < all[index - 1]))) return null;
|
|
@@ -177,8 +167,6 @@ function* prepareSteps(items, options, document) {
|
|
|
177
167
|
const work = prepareParagraphCandidateSteps(items, {...options, document});
|
|
178
168
|
try {
|
|
179
169
|
for (;;) {
|
|
180
|
-
// Another consumer may have selected a different content language between
|
|
181
|
-
// frames. Every resumed step uses this request's original language again.
|
|
182
170
|
if (language !== options.language) { setLocale(options.language); language = options.language; }
|
|
183
171
|
const step = work.next();
|
|
184
172
|
if (step.done) { handle = step.value; break; }
|
|
@@ -200,8 +188,6 @@ export function prepareRichParagraph(items, options = {}) {
|
|
|
200
188
|
return source && settings ? finish(prepareSteps(source, settings, document)) : null;
|
|
201
189
|
}
|
|
202
190
|
|
|
203
|
-
// This metadata is Rapier's original input snapshot, never a reconstruction of opaque vendor
|
|
204
|
-
// fields. It exists only while its handle does and is used for exact source containment.
|
|
205
191
|
export function paragraphItems(handle) { return paragraphs.get(handle)?.items ?? null; }
|
|
206
192
|
|
|
207
193
|
export function createPreparationScope(document = globalThis.document) {
|
|
@@ -230,8 +216,6 @@ export function createPreparationScope(document = globalThis.document) {
|
|
|
230
216
|
tickets.set(owner, ticket);
|
|
231
217
|
try {
|
|
232
218
|
for (;;) {
|
|
233
|
-
// A forgotten/replaced source request or changed face cannot publish a result
|
|
234
|
-
// after suspension. Until completion, no ready handle is installed for its owner.
|
|
235
219
|
if (tickets.get(owner) !== ticket || captureFontAuthority(document) !== birth) return null;
|
|
236
220
|
const step = work.next();
|
|
237
221
|
if (step.done) {
|
|
@@ -249,8 +233,6 @@ export function createPreparationScope(document = globalThis.document) {
|
|
|
249
233
|
let slotFor = null, readKnown = null, staged = false, active = false;
|
|
250
234
|
try {
|
|
251
235
|
if (!supportedDocument(document) || !owner || typeof owner !== 'object' && typeof owner !== 'function') return null;
|
|
252
|
-
// Preparation, fitting and forgetting share one outstanding request for this owner.
|
|
253
|
-
// Replacing a fit keeps its immutable source geometry, but retires the old plan work.
|
|
254
236
|
tickets.delete(owner);
|
|
255
237
|
const birth = captureFontAuthority(document), source = paragraphs.get(handle);
|
|
256
238
|
if (owners.get(owner) !== handle || !source || source.generation !== birth ||
|
|
@@ -270,17 +252,12 @@ export function createPreparationScope(document = globalThis.document) {
|
|
|
270
252
|
!Number.isInteger(from) || from < -1 || !Number.isInteger(to) || to < -1) return -2;
|
|
271
253
|
const cuts = handle.segments[segment]?.words[word]?.breaks;
|
|
272
254
|
if (!cuts || from >= cuts.length || to >= cuts.length || from < 0 && to < 0 || from >= 0 && to >= 0 && from >= to) return -2;
|
|
273
|
-
if (from >= 0 && to >= 0) return -1;
|
|
255
|
+
if (from >= 0 && to >= 0) return -1;
|
|
274
256
|
const cut = cuts[from < 0 ? to : from];
|
|
275
257
|
if (!Number.isInteger(cut.slot) || cut.slot < 0 || cut.slot >= handle.cutCount) return -2;
|
|
276
|
-
// Slots come only from the captured geometry's final legal cuts. A request
|
|
277
|
-
// can select coordinates, but cannot supply a cache key or a metric buffer.
|
|
278
258
|
return cut.slot * 2 + (from < 0 ? 0 : 1);
|
|
279
259
|
};
|
|
280
260
|
readKnown = request => {
|
|
281
|
-
// This callback belongs only to active numerical work. A warm edge reads an
|
|
282
|
-
// exact scalar without a generator crossing, FontFaceSet scan or observation.
|
|
283
|
-
// Current source/font authority is checked at each actual resume and finish.
|
|
284
261
|
if (!active || tickets.get(owner) !== ticket || generation !== birth) return undefined;
|
|
285
262
|
const slot = slotFor(request?.segment, request?.word, request?.from, request?.to);
|
|
286
263
|
if (slot < 0 || tickets.get(owner) !== ticket || generation !== birth) return undefined;
|
|
@@ -298,7 +275,6 @@ export function createPreparationScope(document = globalThis.document) {
|
|
|
298
275
|
const step = work.next(answer);
|
|
299
276
|
answer = undefined;
|
|
300
277
|
if (step.done) {
|
|
301
|
-
// A host measurement or another synchronous caller may replace this owner too.
|
|
302
278
|
if (!currentFit()) return null;
|
|
303
279
|
const plan = step.value;
|
|
304
280
|
if (!plan || !Array.isArray(plan.lines)) return null;
|
|
@@ -306,9 +282,6 @@ export function createPreparationScope(document = globalThis.document) {
|
|
|
306
282
|
Object.freeze(plan.lines);
|
|
307
283
|
Object.freeze(plan);
|
|
308
284
|
if (!currentFit()) return null;
|
|
309
|
-
// The private buffer holds at most two exact numbers per legal cut. It
|
|
310
|
-
// never escapes and is never written after publication. A later fit must
|
|
311
|
-
// copy it before staging a miss; cancellation cannot poison this snapshot.
|
|
312
285
|
if (staged) source.edges = facts;
|
|
313
286
|
return plan;
|
|
314
287
|
}
|
|
@@ -322,8 +295,6 @@ export function createPreparationScope(document = globalThis.document) {
|
|
|
322
295
|
if (answer !== undefined && !Number.isNaN(answer)) continue;
|
|
323
296
|
}
|
|
324
297
|
selectLanguage();
|
|
325
|
-
// No observer exists for a plan proved from whole-word facts. Its temporary
|
|
326
|
-
// vendor metric objects are owned only by this operation, never by the handle.
|
|
327
298
|
observe ??= createParagraphObserver(source.items, handle, source.options.language);
|
|
328
299
|
answer = observe({segment, word, from, to});
|
|
329
300
|
if (!finite(answer) || answer < 0) return null;
|
|
@@ -357,13 +328,9 @@ export function createPreparationScope(document = globalThis.document) {
|
|
|
357
328
|
current,
|
|
358
329
|
invalidateFonts: () => invalidateRichMetrics(document),
|
|
359
330
|
forget: owner => { owners.delete(owner); tickets.delete(owner); },
|
|
360
|
-
// The existing document/style owner calls this when replacing or retiring its content,
|
|
361
|
-
// not for width reflow. Upstream caches outlive handles, so dropping a WeakMap is not enough.
|
|
362
331
|
retire({documentChanged = false} = {}) {
|
|
363
332
|
owners = new WeakMap();
|
|
364
333
|
tickets = new WeakMap();
|
|
365
|
-
// Disposing a projection must not erase a still-live peer's missing custom-family
|
|
366
|
-
// authority. Only the host's whole-document identity transition resets that history.
|
|
367
334
|
if (documentChanged && document) documents.delete(document);
|
|
368
335
|
const before = generation;
|
|
369
336
|
captureFontAuthority(document);
|
|
@@ -5,10 +5,6 @@ import {getEngineProfile, getPreparationLanguage, getFontMeasurement, parseFontS
|
|
|
5
5
|
getTextWidth, getEmojiCorrection, textMayContainEmoji, measureWithLetterSpacing} from '../agent/vendor/pretext/measurement.js';
|
|
6
6
|
import {KIND_BITS, TEXT, SPACE, SOFT_HYPHEN, ZERO_WIDTH_BREAK, ZERO_WIDTH_GLUE, OBJECT, SPACED, UNBROKEN} from '../agent/vendor/pretext/analysis.js';
|
|
7
7
|
|
|
8
|
-
// The sole pin-specific bridge to bac1774c0f8b7a92e44b9da43b6d5443d55d0d9b.
|
|
9
|
-
// Preparation seals source, whole-word geometry and legal edges. A separate captured fit
|
|
10
|
-
// observes only requested edge intervals before sealing its plan. Neither phase exports a
|
|
11
|
-
// vendor handle, a mutable metric object or a function that can measure after readiness.
|
|
12
8
|
const opening = /^[\p{Ps}\p{Pi}\p{Pf}"']+/u;
|
|
13
9
|
const closing = /[\p{Pe}\p{Pi}\p{Pf}"'.,;:!?]+$/u;
|
|
14
10
|
const empty = values => values == null || !values.some(value => value !== null && value !== 0);
|
|
@@ -28,8 +24,6 @@ function sourceMeasurer(items, text, offsets, language) {
|
|
|
28
24
|
const metrics = [];
|
|
29
25
|
return (start, end, suffix = '') => {
|
|
30
26
|
if (start >= end) return 0;
|
|
31
|
-
// The last positive overlap owns the suffix. Duplicate offsets from empty runs
|
|
32
|
-
// must not move it into an empty run or the next font.
|
|
33
27
|
let lo = 0, hi = items.length;
|
|
34
28
|
while (lo < hi) {
|
|
35
29
|
const mid = lo + hi >>> 1;
|
|
@@ -51,16 +45,12 @@ function sourceMeasurer(items, text, offsets, language) {
|
|
|
51
45
|
? measureWithLetterSpacing(body, item.letterSpacing, metric.emoji, metric.font)
|
|
52
46
|
: getTextWidth(body, metric.font, metric.emoji);
|
|
53
47
|
if (measured === null) return NaN;
|
|
54
|
-
// Keep the original per-run shaping and left-to-right addition order.
|
|
55
48
|
width += measured + (item.break === 'never' ? item.extraWidth : 0);
|
|
56
49
|
}
|
|
57
50
|
return width;
|
|
58
51
|
};
|
|
59
52
|
}
|
|
60
53
|
|
|
61
|
-
// Only the preparation scope calls this, while its source/font ticket is live. The
|
|
62
|
-
// short-lived observer owns the vendor metrics and context rows; the sealed geometry
|
|
63
|
-
// retains only immutable coordinates, corrections and its resolved shaping language.
|
|
64
54
|
export function createParagraphObserver(items, geometry) {
|
|
65
55
|
const measure = sourceMeasurer(items, geometry.text, geometry.offsets, geometry.language);
|
|
66
56
|
const rows = new Map();
|
|
@@ -73,8 +63,6 @@ export function createParagraphObserver(items, geometry) {
|
|
|
73
63
|
for (let at = start.contextAfter; at < contexts.length; at++) correction += contexts[at].delta;
|
|
74
64
|
return measure(start.offset, word.end) + correction + start.startExtra;
|
|
75
65
|
}
|
|
76
|
-
// For a fixed start the fitter visits increasing ends. Extend that row's exact
|
|
77
|
-
// left-to-right sum; no total-minus-prefix arithmetic changes its rounding.
|
|
78
66
|
let row = rows.get(start);
|
|
79
67
|
if (!row || end.offset < row.end) rows.set(start, row = {at: start.contextInside, sum: 0, end: start.offset});
|
|
80
68
|
while (row.at < contexts.length && contexts[row.at].end <= end.offset) row.sum += contexts[row.at++].delta;
|
|
@@ -99,8 +87,6 @@ export function* prepareParagraphCandidateSteps(items, options) {
|
|
|
99
87
|
if (!source.some(item => item.text)) continue;
|
|
100
88
|
const flow = prepareRichInlineSegments(source, {whiteSpace: 'normal', wordBreak: options.wordBreak});
|
|
101
89
|
const data = flow.data, flags = data.segmentFlags, units = [];
|
|
102
|
-
// These are object-edge contracts that this text-only projection cannot paint. Refuse
|
|
103
|
-
// before making a plan, rather than dropping an advance or inventing a native equivalent.
|
|
104
90
|
if (!empty(data.items?.insideExtras) || !empty(data.items?.fillExtras) || !empty(data.items?.openingEdges)) return null;
|
|
105
91
|
for (let index = 0; index < flags.length; index++) {
|
|
106
92
|
const kind = flags[index] & KIND_BITS;
|
|
@@ -138,8 +124,6 @@ export function* prepareParagraphCandidateSteps(items, options) {
|
|
|
138
124
|
finish();
|
|
139
125
|
yield;
|
|
140
126
|
|
|
141
|
-
// Native language data contributes only legal source positions. Every advance,
|
|
142
|
-
// including the displayed discretionary glyph, still comes from Pretext below.
|
|
143
127
|
const automatic = paragraphHyphens(chunk.words, text, items, offsets, options.language, options.document);
|
|
144
128
|
if (!automatic) return null;
|
|
145
129
|
for (const word of chunk.words) for (const offset of automatic.get(word) || []) {
|
|
@@ -147,8 +131,6 @@ export function* prepareParagraphCandidateSteps(items, options) {
|
|
|
147
131
|
}
|
|
148
132
|
yield;
|
|
149
133
|
|
|
150
|
-
// Whole words and optical edges use the same font/language/letter-spacing cache
|
|
151
|
-
// as ordinary wrapping. Discretionary edges belong to the captured fit phase.
|
|
152
134
|
let measuredWords = 0;
|
|
153
135
|
for (const word of chunk.words) {
|
|
154
136
|
if (word.protected) word.breaks = [];
|
|
@@ -157,11 +139,6 @@ export function* prepareParagraphCandidateSteps(items, options) {
|
|
|
157
139
|
const contexts = [];
|
|
158
140
|
let measuredEdges = 0;
|
|
159
141
|
if (joined) {
|
|
160
|
-
// A joined word and its cut edges are separate shaping observations. Retain the
|
|
161
|
-
// engine's contextual advances (including positive following-space kerning and
|
|
162
|
-
// punctuation trims) when replacing the sum of pieces with the joined advance.
|
|
163
|
-
// A leading combining-only segment already belongs to that joined observation;
|
|
164
|
-
// its mark-context correction must not be added a second time.
|
|
165
142
|
for (let at = word.first; at <= word.last; at++) {
|
|
166
143
|
const unit = units[at];
|
|
167
144
|
if (unit.kind !== TEXT || /^\p{M}+$/u.test(text.slice(unit.start, unit.end))) continue;
|
|
@@ -174,8 +151,6 @@ export function* prepareParagraphCandidateSteps(items, options) {
|
|
|
174
151
|
let autoBefore = word.first - 1, autoAfter = word.first, contextAfter = 0, contextInside = 0, prefixContext = 0;
|
|
175
152
|
for (const cut of word.breaks) {
|
|
176
153
|
if (cut.kind === 'soft' || cut.kind === 'auto') {
|
|
177
|
-
// The projection's explicit glyph policy: the text font's hyphen-minus.
|
|
178
|
-
// Its whole shaped prefix is measured below; no fallback-family search.
|
|
179
154
|
cut.glyph = '-';
|
|
180
155
|
}
|
|
181
156
|
let before = cut.at, after = cut.at + 1;
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import {createPreparationScope, supportsRichStyle, captureFontAuthority} from './preparation.mjs';
|
|
3
3
|
import {createParagraphFitter} from './paragraph-fit.mjs';
|
|
4
4
|
|
|
5
|
-
// Layout projections keep the original text in place. The factory also runs in an exported page.
|
|
6
5
|
export function createTypography() {
|
|
7
6
|
const fitter = createParagraphFitter();
|
|
8
7
|
const mark = 'data-rapier-typography', boundary = mark + '-break', hang = mark + '-hang';
|
|
@@ -11,7 +10,7 @@ export function createTypography() {
|
|
|
11
10
|
const exclude = 'code,kbd,samp,math,script,style,.math-rendered,[hidden]';
|
|
12
11
|
const candidates = 'p[data-md-hyphens="auto"],p[data-md-lines],p[data-md-hanging="on"],p[' + mark + ']';
|
|
13
12
|
const properties = ['--md-drop-cap-font', '--md-hang-start'];
|
|
14
|
-
const states = new WeakMap(), documents = new WeakMap(), live = new Set();
|
|
13
|
+
const states = new WeakMap(), documents = new WeakMap(), live = new Set(), refused = new WeakMap();
|
|
15
14
|
function scope(document) {
|
|
16
15
|
let value = documents.get(document);
|
|
17
16
|
if (!value) { value = createPreparationScope(document); documents.set(document, value); }
|
|
@@ -36,6 +35,9 @@ export function createTypography() {
|
|
|
36
35
|
return end > start ? {start, end, text: text.slice(start, end)} : null;
|
|
37
36
|
}
|
|
38
37
|
|
|
38
|
+
const flowFloat = '.rapier-flow-float';
|
|
39
|
+
const own = paragraph => [paragraph, ...[...paragraph.querySelectorAll('*')].filter(element => !element.closest(flowFloat))];
|
|
40
|
+
|
|
39
41
|
function paragraphs(root, selector = candidates) {
|
|
40
42
|
return [...(root?.matches?.(selector) ? [root] : []), ...(root?.querySelectorAll?.(selector) || [])];
|
|
41
43
|
}
|
|
@@ -63,7 +65,6 @@ export function createTypography() {
|
|
|
63
65
|
return item ? {node: item.node, offset: Math.max(0, Math.min(item.node.length, offset - item.start))} : null;
|
|
64
66
|
}
|
|
65
67
|
|
|
66
|
-
// This data-only guard is safe on an active edit surface, including during composition.
|
|
67
68
|
function protectInitial(root) {
|
|
68
69
|
for (const paragraph of paragraphs(root, 'p[data-md-lines],p[data-md-hanging="on"],p[' + protectedInitial + ']')) {
|
|
69
70
|
const initial = initialRange(paragraph), letter = initial && [...segment(initial.text)].at(-1);
|
|
@@ -94,7 +95,6 @@ export function createTypography() {
|
|
|
94
95
|
return paragraph.hasAttribute('data-md-lines') ? Math.max(parseFloat(style.lineHeight) || 0, parseFloat(style.fontSize) * 1.7) : Infinity;
|
|
95
96
|
}
|
|
96
97
|
|
|
97
|
-
// Returns a textContent coordinate, independent of markup, selected fonts or a floated initial.
|
|
98
98
|
function firstLineEnd(paragraph) {
|
|
99
99
|
if (!paragraph?.isConnected || !paragraph.getClientRects?.().length) return null;
|
|
100
100
|
const view = paragraph.ownerDocument.defaultView;
|
|
@@ -141,8 +141,6 @@ export function createTypography() {
|
|
|
141
141
|
if (anchor.node.isConnected && focus.node.isConnected) saved.selection.setBaseAndExtent(anchor.node, anchor.offset, focus.node, focus.offset);
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
// A native initial cannot span inline element boundaries. Move only its opening punctuation
|
|
145
|
-
// into the letter's text node; the paragraph's text and all original elements stay in place.
|
|
146
144
|
function unifyInitial(paragraph, state, initial, letterStart) {
|
|
147
145
|
const items = runs(paragraph), at = point(items, letterStart);
|
|
148
146
|
if (!at || at.node.parentElement?.closest(exclude)) return;
|
|
@@ -172,15 +170,14 @@ export function createTypography() {
|
|
|
172
170
|
if (cursor !== move.next || text !== move.after) return;
|
|
173
171
|
restore.push({move, pieces});
|
|
174
172
|
}
|
|
175
|
-
// A changed projection belongs to its current writer; restoration never replaces newer text.
|
|
176
173
|
for (const {move, pieces} of restore) { move.node.data = move.before; for (const piece of pieces.slice(1)) piece.remove(); }
|
|
177
174
|
}
|
|
178
175
|
|
|
179
|
-
function clear(root, {forget = true} = {}) {
|
|
176
|
+
function clear(root, {forget = true, keepSelection = true} = {}) {
|
|
180
177
|
const list = paragraphs(root, 'p[' + mark + '],p[' + protectedInitial + ']');
|
|
181
178
|
for (const paragraph of list) {
|
|
182
179
|
if (!paragraph.hasAttribute(mark)) { paragraph.removeAttribute(protectedInitial); continue; }
|
|
183
|
-
const selected = selectionBefore(paragraph);
|
|
180
|
+
const selected = keepSelection ? selectionBefore(paragraph) : null;
|
|
184
181
|
const state = states.get(paragraph);
|
|
185
182
|
state?.work?.return();
|
|
186
183
|
state?.fitWork?.return();
|
|
@@ -204,9 +201,6 @@ export function createTypography() {
|
|
|
204
201
|
}
|
|
205
202
|
}
|
|
206
203
|
|
|
207
|
-
// Native clipboard HTML must see the original inline tree. Clear the selected
|
|
208
|
-
// projection before the browser/host captures its Range, retaining current metrics.
|
|
209
|
-
// The host schedules ordinary refinement again after the synchronous clipboard event.
|
|
210
204
|
function clearSelection(root) {
|
|
211
205
|
const selection=(root?.ownerDocument || root)?.getSelection?.();
|
|
212
206
|
if (!selection?.rangeCount || selection.isCollapsed) return 0;
|
|
@@ -220,15 +214,12 @@ export function createTypography() {
|
|
|
220
214
|
return selected.length;
|
|
221
215
|
}
|
|
222
216
|
|
|
223
|
-
// Optical alignment uses the available margin; paint containment and narrow pages must not
|
|
224
|
-
// clip a large quoted initial. Offscreen measurement stages have no viewport clipping edge.
|
|
225
217
|
function hangingEdge(paragraph, style, box) {
|
|
226
218
|
const view = paragraph.ownerDocument.defaultView, rtl = style.direction === 'rtl';
|
|
227
219
|
if (style.visibility !== 'visible' || box.right <= 0 || box.left >= view.innerWidth) return null;
|
|
228
220
|
let edge = rtl ? view.innerWidth : 0;
|
|
229
221
|
if (view.matchMedia('print').matches) {
|
|
230
222
|
const content = paragraph.closest('.md-render')?.getBoundingClientRect();
|
|
231
|
-
// The PDF painter clips at the content area even when a physical page margin exists.
|
|
232
223
|
if (content?.width > 0) edge = rtl ? Math.min(edge, content.right) : Math.max(edge, content.left);
|
|
233
224
|
}
|
|
234
225
|
for (let element = paragraph; element; element = element.parentElement) {
|
|
@@ -302,8 +293,6 @@ export function createTypography() {
|
|
|
302
293
|
for (const element of parents) {
|
|
303
294
|
const computed = element === paragraph ? baseStyle : view.getComputedStyle(element);
|
|
304
295
|
const s = Object.fromEntries([...shaping, ...admission].map(name => [name, computed[name]]));
|
|
305
|
-
// The fit's own CSS suppresses native rewrapping. Source attributes and ancestor
|
|
306
|
-
// whitespace remain in the key; clearing reads the current unprojected styles anew.
|
|
307
296
|
if (prior?.plan) for (const name of ['whiteSpace', 'hyphens', 'hangingPunctuation', 'textWrapStyle'])
|
|
308
297
|
s[name] = prior.saved.styles.get(element)?.[name] ?? s[name];
|
|
309
298
|
const before = view.getComputedStyle(element, '::before').content, after = view.getComputedStyle(element, '::after').content;
|
|
@@ -331,7 +320,8 @@ export function createTypography() {
|
|
|
331
320
|
['white-space','text-wrap','hyphens','hanging-punctuation','word-spacing'].some(name => element.style.getPropertyPriority(name))) return null;
|
|
332
321
|
const s = inspection.styles.get(element), atom = element !== paragraph && (/^(CODE|KBD|SAMP)$/.test(element.tagName) || s.whiteSpace === 'nowrap');
|
|
333
322
|
if (s.hyphenateCharacter && s.hyphenateCharacter !== 'auto') return null;
|
|
334
|
-
if (!supportsRichStyle({...s, hyphens:'manual', hangingPunctuation:'none', whiteSpace: atom ? 'nowrap' : s.whiteSpace
|
|
323
|
+
if (!supportsRichStyle({...s, hyphens:'manual', hangingPunctuation:'none', whiteSpace: atom ? 'nowrap' : s.whiteSpace,
|
|
324
|
+
lineBreak: s.lineBreak === 'after-white-space' ? 'auto' : s.lineBreak}, language) ||
|
|
335
325
|
!atom && s.whiteSpace !== 'normal') return null;
|
|
336
326
|
const extras = ['paddingLeft','paddingRight','borderLeftWidth','borderRightWidth','marginLeft','marginRight']
|
|
337
327
|
.reduce((sum,name) => sum + Math.abs(parseFloat(s[name]) || 0),0);
|
|
@@ -383,6 +373,56 @@ export function createTypography() {
|
|
|
383
373
|
return {width,indent,left:hanging?Math.max(0,left-clipLeft):0,right:hanging?Math.max(0,clipRight-right):0};
|
|
384
374
|
}
|
|
385
375
|
|
|
376
|
+
function excluded(shape, a, b) {
|
|
377
|
+
const {points, box} = shape;
|
|
378
|
+
if (!points.length) return b > 0 && a < box.height ? [0, box.width] : null;
|
|
379
|
+
let low = Infinity, high = -Infinity;
|
|
380
|
+
for (let index = 0; index < points.length; index++) {
|
|
381
|
+
const [x1, y1] = points[index], [x2, y2] = points[(index + 1) % points.length];
|
|
382
|
+
const top = Math.min(y1, y2), bottom = Math.max(y1, y2);
|
|
383
|
+
if (bottom <= a || top >= b) continue;
|
|
384
|
+
for (const y of [Math.max(a, top), Math.min(b, bottom)]) {
|
|
385
|
+
const x = y1 === y2 ? null : x1 + (x2 - x1) * (y - y1) / (y2 - y1);
|
|
386
|
+
if (x == null) { low = Math.min(low, x1, x2); high = Math.max(high, x1, x2); }
|
|
387
|
+
else { low = Math.min(low, x); high = Math.max(high, x); }
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return high > low ? [low, high] : null;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function floatMeasures(paragraph, style, box, width) {
|
|
394
|
+
const view = paragraph.ownerDocument.defaultView;
|
|
395
|
+
const floats = [...paragraph.children].filter(element => element.matches(flowFloat) && view.getComputedStyle(element).cssFloat !== 'none');
|
|
396
|
+
if (!floats.length) return null;
|
|
397
|
+
const lineHeight = parseFloat(style.lineHeight);
|
|
398
|
+
if (!(lineHeight > 0)) return {refuse: true};
|
|
399
|
+
const top = box.top + (parseFloat(style.borderTopWidth) || 0) + (parseFloat(style.paddingTop) || 0);
|
|
400
|
+
const left = box.left + (parseFloat(style.borderLeftWidth) || 0) + (parseFloat(style.paddingLeft) || 0);
|
|
401
|
+
const right = box.right - (parseFloat(style.borderRightWidth) || 0) - (parseFloat(style.paddingRight) || 0);
|
|
402
|
+
const shapes = floats.map(element => {
|
|
403
|
+
const computed = view.getComputedStyle(element), bounds = element.getBoundingClientRect();
|
|
404
|
+
const points = [...computed.shapeOutside.matchAll(/(-?[\d.]+)px\s+(-?[\d.]+)px/g)].map(match => [+match[1], +match[2]]);
|
|
405
|
+
return {side: computed.cssFloat, box: bounds, points, top: bounds.top - (parseFloat(computed.marginTop) || 0),
|
|
406
|
+
bottom: bounds.bottom + (parseFloat(computed.marginBottom) || 0)};
|
|
407
|
+
});
|
|
408
|
+
const bottom = Math.max(...shapes.map(shape => shape.bottom)), widths = [];
|
|
409
|
+
for (let line = 0; top + line * lineHeight < bottom && line < 512; line++) {
|
|
410
|
+
const a = top + line * lineHeight, b = a + lineHeight;
|
|
411
|
+
let start = 0, end = 0;
|
|
412
|
+
for (const shape of shapes) {
|
|
413
|
+
if (b <= shape.top || a >= shape.bottom) continue;
|
|
414
|
+
const span = excluded(shape, a - shape.box.top, b - shape.box.top);
|
|
415
|
+
if (!span) continue;
|
|
416
|
+
if (shape.side === 'left') start = Math.max(start, shape.box.left + span[1] - left);
|
|
417
|
+
else end = Math.max(end, right - (shape.box.left + span[0]));
|
|
418
|
+
}
|
|
419
|
+
widths.push(start || end ? Math.floor((width - start - end - 1) * 64) / 64 : width);
|
|
420
|
+
}
|
|
421
|
+
while (widths.length && widths.at(-1) === width) widths.pop();
|
|
422
|
+
return {widths, lineHeight, key: shapes.map(shape => [shape.side, shape.box.left - left, shape.box.top - top, shape.box.width,
|
|
423
|
+
shape.box.height, shape.points.join(' ')].join(':')).join('|')};
|
|
424
|
+
}
|
|
425
|
+
|
|
386
426
|
const topology = plan => plan && plan.lines.map(line => [line.start,line.end,line.next,line.kind,line.glyph].join(':')).join('|');
|
|
387
427
|
function updateProjection(projection, plan) {
|
|
388
428
|
for (const {node,property,line,sign} of projection.bindings) {
|
|
@@ -415,8 +455,6 @@ export function createTypography() {
|
|
|
415
455
|
if (first.opening && line.start === first.start) operations.push({start:first.start,end:first.openEnd,opening:true,line:lineIndex});
|
|
416
456
|
if (last.closing && line.end === last.end) operations.push({start:last.closeStart,end:last.end,closing:true,line:lineIndex});
|
|
417
457
|
}
|
|
418
|
-
// Sweep interval endpoints once. Each original text node is split from right to left;
|
|
419
|
-
// links, comments, IDs and inline ancestors never move or get reconstructed.
|
|
420
458
|
const events = new Map();
|
|
421
459
|
const event = offset => { let value = events.get(offset); if (!value) { value={start:[],end:[]}; events.set(offset,value); } return value; };
|
|
422
460
|
for (const op of operations) { event(op.start).start.push(op); event(op.end).end.push(op); }
|
|
@@ -435,8 +473,6 @@ export function createTypography() {
|
|
|
435
473
|
pieces.push({item,start,end,operations:[...active]}); start=end;
|
|
436
474
|
}
|
|
437
475
|
}
|
|
438
|
-
// Validate every zero-source insertion before the first write, so a refused source
|
|
439
|
-
// coordinate cannot leave a partially projected paragraph behind.
|
|
440
476
|
const sourceEnds = new Set(pieces.map(piece => piece.end));
|
|
441
477
|
if (inserts.some(insert => !sourceEnds.has(insert.offset))) return false;
|
|
442
478
|
const ends = new Map();
|
|
@@ -472,7 +508,21 @@ export function createTypography() {
|
|
|
472
508
|
paragraph.setAttribute(fitMark,'');return projection;
|
|
473
509
|
}
|
|
474
510
|
|
|
475
|
-
function
|
|
511
|
+
function laidAsPlanned(paragraph, job) {
|
|
512
|
+
const style = job.view.getComputedStyle(paragraph), box = paragraph.getBoundingClientRect();
|
|
513
|
+
const inner = box.height - (parseFloat(style.paddingTop) || 0) - (parseFloat(style.paddingBottom) || 0) -
|
|
514
|
+
(parseFloat(style.borderTopWidth) || 0) - (parseFloat(style.borderBottomWidth) || 0);
|
|
515
|
+
return Math.abs(inner - job.plan.lines.length * job.beside.lineHeight) < 1;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function editKey(paragraph, generation) {
|
|
519
|
+
const page = paragraph.ownerDocument.documentElement;
|
|
520
|
+
return [generation, paragraph.innerHTML, ...[...paragraph.attributes].map(item => item.name + '=' + item.value),
|
|
521
|
+
...[...page.attributes].map(item => item.name + '=' + item.value), paragraph.ownerDocument.body?.className,
|
|
522
|
+
paragraph.ownerDocument.defaultView.innerWidth].join('\u0000');
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function apply(root, {all=false, editing=false}={}) {
|
|
476
526
|
const started = performance.now();
|
|
477
527
|
const generation = captureFontAuthority(root?.ownerDocument || globalThis.document);
|
|
478
528
|
for (const paragraph of live) {
|
|
@@ -481,55 +531,63 @@ export function createTypography() {
|
|
|
481
531
|
}
|
|
482
532
|
const jobs=[];let pending=false,changed=0,released=0,preparedCount=0;
|
|
483
533
|
for (const paragraph of paragraphs(root)) {
|
|
484
|
-
if (paragraph.closest('table,pre,code,h1,h2,h3,h4,h5,h6
|
|
485
|
-
|
|
534
|
+
if (paragraph.closest('table,pre,code,h1,h2,h3,h4,h5,h6')) {clear(paragraph);continue;}
|
|
535
|
+
if (paragraph.closest('.block-edit')) {
|
|
536
|
+
if (!editing) continue;
|
|
537
|
+
if (paragraph.getAttribute('data-md-align')!=='justify' || paragraph.getAttribute('data-md-hyphens')!=='auto') {
|
|
538
|
+
if (paragraph.hasAttribute(mark)) clear(paragraph);
|
|
539
|
+
protectInitial(paragraph);continue;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
else if (paragraph.isContentEditable && !paragraph.closest('.block-read')) {clear(paragraph);continue;}
|
|
486
543
|
if (!paragraph.isConnected || !all && paragraph.checkVisibility && !paragraph.checkVisibility({contentVisibilityAuto:true})) continue;
|
|
487
544
|
const view=paragraph.ownerDocument.defaultView;
|
|
488
545
|
if (!view || !paragraph.getClientRects().length) continue;
|
|
546
|
+
if (editing && states.get(paragraph)?.editKey===editKey(paragraph,generation)) continue;
|
|
489
547
|
const style=view.getComputedStyle(paragraph),box=paragraph.getBoundingClientRect(),prior=states.get(paragraph);
|
|
490
548
|
if (!all && (box.bottom < -100 || box.top > view.innerHeight+100)) continue;
|
|
491
549
|
const hanging=paragraph.getAttribute('data-md-hanging')==='on',size=dimensions(paragraph,style,box,hanging);
|
|
492
550
|
if (!(size.width>0)) continue;
|
|
493
|
-
const
|
|
551
|
+
const beside=floatMeasures(paragraph,style,box,size.width);
|
|
552
|
+
if (beside?.refuse) {clear(paragraph);continue;}
|
|
553
|
+
if (beside?.widths.some(width=>!(width>0))) {clear(paragraph);continue;}
|
|
554
|
+
if (beside) size.widths=beside.widths;
|
|
555
|
+
const parents=prior?.parents || own(paragraph);
|
|
494
556
|
const inspection=inspect(paragraph,parents,prior,style);
|
|
495
557
|
const reusable=prior?.html===paragraph.innerHTML && prior.key===inspection.key && (!prior.geometry || scope(paragraph.ownerDocument).current(prior.geometry));
|
|
496
|
-
const sizeKey=[size.width,size.indent,size.left,size.right].join('|');
|
|
558
|
+
const sizeKey=[size.width,size.indent,size.left,size.right,beside?.key||''].join('|');
|
|
559
|
+
if (refused.get(paragraph)===sizeKey+'\u0000'+paragraph.textContent) {if (paragraph.hasAttribute(mark)) clear(paragraph);continue;}
|
|
497
560
|
if (reusable && !prior.pending && prior.sizeKey===sizeKey) continue;
|
|
498
|
-
// Preparation survives reflow; a suspended fit belongs to its captured width,
|
|
499
|
-
// indent and margins. Cancel only that plan when dimensions have changed.
|
|
500
561
|
const fitWork=reusable && prior.sizeKey===sizeKey?prior.fitWork:null;
|
|
501
562
|
if (prior?.fitWork && prior.fitWork!==fitWork) {prior.fitWork.return();prior.fitWork=null;}
|
|
502
563
|
if (!all && jobs.length===4) {if (!reusable) clear(paragraph);pending=true;continue;}
|
|
503
|
-
jobs.push({paragraph,view,style,inspection,size,sizeKey,hanging,selected:selectionBefore(paragraph),
|
|
564
|
+
jobs.push({paragraph,view,style,inspection,size,sizeKey,hanging,beside,selected:selectionBefore(paragraph),
|
|
504
565
|
cap:paragraph.hasAttribute('data-md-lines'),justified:paragraph.getAttribute('data-md-align')==='justify' && paragraph.getAttribute('data-md-hyphens')==='auto',
|
|
505
566
|
saved:reusable?prior.saved:null,geometry:reusable?prior.geometry:null,prepared:reusable?prior.prepared:null,
|
|
506
567
|
work:reusable?prior.work:null,fitWork,
|
|
507
568
|
prior,reusable,needsClear:paragraph.hasAttribute(mark)});
|
|
508
569
|
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
for (const job of jobs) if (job.needsClear && !job.saved) clear(job.paragraph);
|
|
570
|
+
if (editing && jobs.some(job=>job.selected && !job.reusable)) root.ownerDocument.getSelection()?.removeAllRanges();
|
|
571
|
+
for (const job of jobs) if (job.needsClear && !job.saved) clear(job.paragraph,{keepSelection:!editing});
|
|
512
572
|
function stateFor(job) {
|
|
513
573
|
return {properties:new Map(),moves:[],saved:job.saved,geometry:job.geometry,prepared:job.prepared,plan:job.plan,
|
|
514
574
|
work:job.work,fitWork:job.fitWork,parents:job.inspection.parents,key:job.inspection.key,generation:job.inspection.generation,sizeKey:job.sizeKey};
|
|
515
575
|
}
|
|
516
576
|
function suspend(job) {
|
|
517
577
|
const state=stateFor(job);
|
|
518
|
-
// A reusable projection may remain readable while a new width is being fitted.
|
|
519
|
-
// Retain its restoration custody until it is updated or explicitly cleared.
|
|
520
578
|
if (job.reusable && job.prior) for (const name of ['properties','moves','projection','plan']) state[name]=job.prior[name];
|
|
521
579
|
state.pending=true;state.html=job.paragraph.innerHTML;
|
|
522
580
|
states.set(job.paragraph,state);live.add(job.paragraph);job.paragraph.setAttribute(mark,'');pending=true;
|
|
523
581
|
}
|
|
524
|
-
// Both preparation and fitting yield through the same captured source/font owner.
|
|
525
|
-
// Native text remains readable until its plan is ready. Each continuation
|
|
526
|
-
// rereads the current source/style/font and width; it cannot publish a suspended plan.
|
|
527
|
-
// A single native shaping call can exceed this allowance, so this is not a deadline.
|
|
528
582
|
for (let jobIndex = 0; jobIndex < jobs.length; jobIndex++) {
|
|
529
583
|
if (!all && jobIndex && performance.now() - started >= 8) {pending=true;break;}
|
|
530
584
|
const job = jobs[jobIndex];
|
|
531
585
|
if (!job.saved && job.justified) {
|
|
532
|
-
if (job.needsClear)
|
|
586
|
+
if (job.needsClear) {
|
|
587
|
+
const elements=own(job.paragraph),parents=job.inspection.parents;
|
|
588
|
+
if (!editing || !job.prior?.saved || elements.length!==parents.length || elements.some((element,index)=>element!==parents[index]))
|
|
589
|
+
job.inspection=inspect(job.paragraph,elements,null,job.view.getComputedStyle(job.paragraph));
|
|
590
|
+
}
|
|
533
591
|
job.saved=snapshot(job.paragraph,job.inspection);
|
|
534
592
|
if (job.saved) {
|
|
535
593
|
job.work=scope(job.paragraph.ownerDocument).prepareSteps(job.paragraph,job.saved.inline,{policy:'paragraph',language:job.saved.language,
|
|
@@ -557,7 +615,7 @@ export function createTypography() {
|
|
|
557
615
|
if (job.fitWork) {suspend(job);continue;}
|
|
558
616
|
}
|
|
559
617
|
job.projection=job.reusable && job.plan && job.prior?.projection?.topology===topology(job.plan) ? job.prior.projection : null;
|
|
560
|
-
if (job.needsClear && job.saved && !job.projection) clear(job.paragraph,{forget:false});
|
|
618
|
+
if (job.needsClear && job.saved && !job.projection) clear(job.paragraph,{forget:false,keepSelection:!editing});
|
|
561
619
|
const {paragraph,view,hanging,cap,justified}=job;
|
|
562
620
|
if (!cap && !hanging && !justified) {restoreSelection(paragraph,job.selected);continue;}
|
|
563
621
|
const state=stateFor(job);
|
|
@@ -571,10 +629,14 @@ export function createTypography() {
|
|
|
571
629
|
}
|
|
572
630
|
if (job.projection) {updateProjection(job.projection,job.plan);state.projection=job.projection;}
|
|
573
631
|
else if (job.plan) state.projection=project(paragraph,job.saved,job.geometry,job.plan);
|
|
632
|
+
if (state.projection && job.beside && !laidAsPlanned(paragraph,job)) {
|
|
633
|
+
refused.set(paragraph,job.sizeKey+'\u0000'+paragraph.textContent);clear(paragraph);restoreSelection(paragraph,job.selected);continue;
|
|
634
|
+
}
|
|
574
635
|
if (state.projection) released+=job.plan.lines.filter(line=>line.kind!=='end').length;
|
|
575
636
|
else if (hanging) hangPunctuation(paragraph,state,job.style,hangingEdge(paragraph,job.style,paragraph.getBoundingClientRect()));
|
|
576
637
|
state.html=paragraph.innerHTML;
|
|
577
638
|
restoreSelection(paragraph,job.selected);changed++;
|
|
639
|
+
if (editing) state.editKey=editKey(paragraph,generation);
|
|
578
640
|
}
|
|
579
641
|
return {changed,released,pending,prepared:preparedCount};
|
|
580
642
|
}
|