@beltype/cicero 0.1.6 → 0.1.7
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/seam/index.js +3 -1
- package/dist/src/capture.d.ts +17 -1
- package/dist/src/capture.js +127 -68
- package/dist/src/chase.js +2 -0
- package/dist/src/document-state.d.ts +27 -0
- package/dist/src/document-state.js +47 -0
- package/dist/src/driver.d.ts +10 -3
- package/dist/src/driver.js +98 -39
- package/dist/src/fragmentation.d.ts +1 -1
- package/dist/src/fragmentation.js +1 -1
- package/dist/src/fragments.d.ts +2 -2
- package/dist/src/fragments.js +49 -10
- package/dist/src/gauge.d.ts +4 -0
- package/dist/src/gauge.js +14 -2
- package/dist/src/hyphen-patterns/en-us.d.ts +1 -0
- package/dist/src/hyphen-patterns/en-us.js +2 -0
- package/dist/src/hyphen-patterns/it.d.ts +21 -0
- package/dist/src/hyphen-patterns/it.js +52 -0
- package/dist/src/hyphenate.d.ts +5 -2
- package/dist/src/hyphenate.js +14 -6
- package/dist/src/measure.d.ts +5 -3
- package/dist/src/measure.js +15 -10
- package/dist/src/reads.js +1 -1
- package/dist/src/stonehand.js +13 -8
- package/dist/src/word-space.d.ts +8 -0
- package/dist/src/word-space.js +15 -0
- package/package.json +1 -1
package/dist/src/driver.js
CHANGED
|
@@ -15,18 +15,18 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { Ledger, createEntry } from './ledger.js';
|
|
17
17
|
import { assertRenderConditions, awaitCloneImages, awaitFonts, awaitImages, createMeasureContext } from './measure.js';
|
|
18
|
-
import { takeReport } from './gauge.js';
|
|
18
|
+
import { holdsAnonymousBlocks, takeReport } from './gauge.js';
|
|
19
19
|
import { adjudicate, adjudicateFlow, firstBelowTheEdge, isAbsolutelyPositioned, isOutOfFlow, isMonolith, neverCutInside, reachesBelowTheEdge, processionBottoms, fitsPage } from './stonehand.js';
|
|
20
20
|
import { composeDocument, applyRecordsUnder } from './compose-dom.js';
|
|
21
21
|
import { CompositionCache } from './composition-key.js';
|
|
22
|
-
import { readGeometryTable, effectivePageName, styleSheetsOf, registerGcpmProperties, sheetsUseHas, sheetReach } from './capture.js';
|
|
22
|
+
import { readGeometryTable, effectivePageName, pageRulesOf, styleSheetsOf, registerGcpmProperties, sheetsUseHas, sheetReach } from './capture.js';
|
|
23
23
|
import { snapshotOfStyle } from './forcing-memo.js';
|
|
24
24
|
import { cloneFlowPiece, declineRepeats, mayRepeat, mutateToContinuation, truncateFrom } from './fragments.js';
|
|
25
25
|
import { cutAfter, divides, elAtIn, flattenResume, indexBlocks, inFlowIndex, inRemainders, normalizePath, pathOf, placeAt, remaindersOf } from './positions.js';
|
|
26
|
-
import { documentOpsOf, counterStateAt, stringAssignmentsOf, runningAssignmentsOf, namedStringsForPage } from './document-state.js';
|
|
26
|
+
import { documentOpsOf, counterStateAt, pageNumberAt, withoutPageCounters, stringAssignmentsOf, runningAssignmentsOf, namedStringsForPage } from './document-state.js';
|
|
27
27
|
import { RUNNING_PROP } from './gcpm-encoding.js';
|
|
28
28
|
import { ARTIFACT, hasWord } from './data-beltype.js';
|
|
29
|
-
import { blockChildrenOf, footerBeforeRows, isFixedBox, SIDE_BY_SIDE, sourceOf } from './word-space.js';
|
|
29
|
+
import { blockChildrenOf, footerBeforeRows, headerAfterRows, isFixedBox, SIDE_BY_SIDE, sourceOf } from './word-space.js';
|
|
30
30
|
import { changedPairs, settleReads } from './reads.js';
|
|
31
31
|
import { converge, holderPage, namesNoPlace, priorKeys, restartPage } from './resume.js';
|
|
32
32
|
import { answerHas, boxesWhoseWordsMoved } from './has-words.js';
|
|
@@ -37,6 +37,21 @@ function toPx(len) {
|
|
|
37
37
|
throw new Error(`unsupported page length: ${len}`);
|
|
38
38
|
return parseFloat(m[1]) * PX_PER[m[2]];
|
|
39
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* The message a table shape refuses with (*thead-after-rows-and-tall-footer-lose-rows*, 2026-11-07): the table, by its id or class or its first words, and the shape. Both shapes lost rows with no message, and a refusal by name is the detector until the address can say where §17.2 lays the group out.
|
|
42
|
+
*
|
|
43
|
+
* @remarks The two shapes, both groups whose words and box disagree in order. A header group written after the rows is laid out above them (CSS 2.1 §17.2) while its words stand after them in the word space: one that repeats is walked first and shown by its copy on every page before its words, and one too tall to repeat, in a divided table, is refused, because which page shows it is the open ruling on §17.2's display order in the word space. A footer group written before the rows is walked where it is laid out, last and whole, and one taller than the page area could be placed nowhere, so the table ended on its first page.
|
|
44
|
+
*
|
|
45
|
+
* @param group - the source `thead` or `tfoot`.
|
|
46
|
+
* @param shape - the shape, in words.
|
|
47
|
+
* @param remedy - what the author can write instead.
|
|
48
|
+
*/
|
|
49
|
+
function refusedTable(group, shape, remedy = 'write the groups in HTML\'s order: thead, tbody, tfoot') {
|
|
50
|
+
const table = group.closest('table');
|
|
51
|
+
const words = (table?.textContent ?? '').trim().replace(/\s+/g, ' ').slice(0, 40);
|
|
52
|
+
const name = table?.id ? `<table id="${table.id}">` : table?.className ? `<table class="${table.className}">` : `the table beginning "${words}"`;
|
|
53
|
+
return `cicero: ${name} has ${shape}, which this engine cannot divide across pages yet; it refuses rather than lose rows. To print it, ${remedy}.`;
|
|
54
|
+
}
|
|
40
55
|
/** The nearest copy the engine repeats (`artifact`) standing ahead of this element among its siblings — the table's header, in front of its rows — or null. */
|
|
41
56
|
const artifactBefore = (el) => {
|
|
42
57
|
for (let s = el?.previousElementSibling; s; s = s.previousElementSibling)
|
|
@@ -44,6 +59,13 @@ const artifactBefore = (el) => {
|
|
|
44
59
|
return s;
|
|
45
60
|
return null;
|
|
46
61
|
};
|
|
62
|
+
/** The header group written after this element among its siblings, which §17.2 lays out ahead of it all the same (`headerAfterRows`), or null. It is a copy on the pages before its words and the original in a feed, which pours it whole, and it stands ahead of the rows either way. */
|
|
63
|
+
const headerCopyAfter = (el) => {
|
|
64
|
+
for (let s = el?.nextElementSibling; s; s = s.nextElementSibling)
|
|
65
|
+
if (headerAfterRows(s))
|
|
66
|
+
return s;
|
|
67
|
+
return null;
|
|
68
|
+
};
|
|
47
69
|
/** Does a copy the engine repeats (`artifact`) stand ahead of this element among its siblings — the table's header, in front of its rows? */
|
|
48
70
|
export const standsAfterArtifact = (el) => artifactBefore(el) !== null;
|
|
49
71
|
/**
|
|
@@ -60,9 +82,10 @@ export function gapAbove(box, report, win) {
|
|
|
60
82
|
for (let child = box, a = box.parent; a; child = a, a = a.parent) {
|
|
61
83
|
const cs = win.getComputedStyle(a.el);
|
|
62
84
|
// A copy the engine repeats ahead of the child (the table's header) is furniture with a footprint, not a spent lead (*the header repeats*): the level owes only what stands above the copy, which under separated borders is the table's `border-spacing` above its first row, and Chromium draws none above a repeated header (*a repeated header joins the rows as Chromium prints them*, 2026-10-22). Under collapsed borders the copy's top is the table's own and nothing is owed.
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
85
|
+
const late = headerCopyAfter(child.el);
|
|
86
|
+
if (late || standsAfterArtifact(child.el)) {
|
|
87
|
+
// The header's copy, never a footer's: a footer written before its rows stands among the siblings too, and is laid out at the foot (*the footer before the rows*). A header written after its rows is laid out ahead of them, and its copy is the one.
|
|
88
|
+
let copy = cs.borderCollapse === 'separate' ? late ?? artifactBefore(child.el) : null;
|
|
66
89
|
while (copy && !/^THEAD$/i.test(copy.tagName))
|
|
67
90
|
copy = artifactBefore(copy);
|
|
68
91
|
const box = copy ? report.boxOf(copy) : null;
|
|
@@ -93,28 +116,25 @@ export function pageNameDownFrom(el, { styleOf, runBefore, blockChildren = block
|
|
|
93
116
|
return effectivePageName(el);
|
|
94
117
|
}
|
|
95
118
|
/**
|
|
96
|
-
* Read the document's @page geometry from the CSSOM — every sheet the document wears, adopted ones included (`capture.
|
|
119
|
+
* Read the document's @page geometry from the CSSOM — every sheet the document wears, adopted ones included, and the rules inside its layers and conditions (`capture.ts`'s `pageRulesOf`). Returns page and content box sizes in px.
|
|
97
120
|
*
|
|
98
121
|
* **Margins only, and that is now a stated limit rather than the whole story** *(a page box has a border and a padding, 2026-09-07)*. The capture models a page's `border` and `padding` and subtracts both from the page area; this reader does not, so a document declaring either gets a content box too large here. Its one caller is `packages/essay`'s benchmark, over a corpus that writes `size` and margins into its page rules and nothing else — so the difference is unreachable there. It is not widened rather than left quietly wrong: this is the legacy single-geometry reader, and a second implementation of the page area is exactly what `readGeometryTable` exists to be the only one of.
|
|
99
122
|
*/
|
|
100
123
|
export function readPageGeometry(doc) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const margins = { top: toPx(mt), right: toPx(mr), bottom: toPx(mb), left: toPx(ml) };
|
|
116
|
-
return { pageW, pageH, margins, contentW: pageW - margins.left - margins.right, contentH: pageH - margins.top - margins.bottom };
|
|
117
|
-
}
|
|
124
|
+
const { layered, unlayered } = pageRulesOf(doc);
|
|
125
|
+
for (const rule of [...unlayered, ...layered]) {
|
|
126
|
+
const text = rule.cssText;
|
|
127
|
+
const size = /size:\s*([^;}]+)/.exec(text)?.[1].trim().split(/\s+/) ?? [];
|
|
128
|
+
const marginRaw = /(?:^|[\s{;])margin:\s*([^;}]+)/.exec(text)?.[1].trim().split(/\s+/) ??
|
|
129
|
+
['top', 'right', 'bottom', 'left'].map((side) => new RegExp(`margin-${side}:\\s*([^;}]+)`).exec(text)?.[1].trim() ?? '0px');
|
|
130
|
+
if (size.length === 0)
|
|
131
|
+
throw new Error('@page without an explicit size — the corpus always pins geometry (I5)');
|
|
132
|
+
const [w, h] = size.length === 1 ? [size[0], size[0]] : size;
|
|
133
|
+
const [mt, mr = mt, mb = mt, ml = mr] = marginRaw;
|
|
134
|
+
const pageW = toPx(w);
|
|
135
|
+
const pageH = toPx(h);
|
|
136
|
+
const margins = { top: toPx(mt), right: toPx(mr), bottom: toPx(mb), left: toPx(ml) };
|
|
137
|
+
return { pageW, pageH, margins, contentW: pageW - margins.left - margins.right, contentH: pageH - margins.top - margins.bottom };
|
|
118
138
|
}
|
|
119
139
|
throw new Error('no @page rule found');
|
|
120
140
|
}
|
|
@@ -218,7 +238,7 @@ export const combineParity = (earlier, later) => later ?? earlier;
|
|
|
218
238
|
*
|
|
219
239
|
* @param blocks - the top-level blocks, in document order.
|
|
220
240
|
*/
|
|
221
|
-
export function forcingIndexOf(blocks, { styleOf, pageNameOf, cutOf, posOf, posOfCut = null, isMonolith: monolith = isMonolith, blockChildren = blockChildrenOf }) {
|
|
241
|
+
export function forcingIndexOf(blocks, { styleOf, pageNameOf, cutOf, posOf, posOfCut = null, isMonolith: monolith = isMonolith, blockChildren = blockChildrenOf, extentOf = null }) {
|
|
222
242
|
const events = [];
|
|
223
243
|
const blockStart = [];
|
|
224
244
|
/**
|
|
@@ -233,9 +253,25 @@ export function forcingIndexOf(blocks, { styleOf, pageNameOf, cutOf, posOf, posO
|
|
|
233
253
|
const k = events.length;
|
|
234
254
|
events.push(e);
|
|
235
255
|
const inner = sealedBy >= 0 ? sealedBy : neverCutInside(el, st) ? k : -1;
|
|
236
|
-
if (!parallel && !monolith(el, st))
|
|
237
|
-
|
|
238
|
-
|
|
256
|
+
if (!parallel && !monolith(el, st)) {
|
|
257
|
+
const kids = blockChildren(el);
|
|
258
|
+
const runs = !!extentOf && holdsAnonymousBlocks(st.display ?? '');
|
|
259
|
+
const row = rowish || SIDE_BY_SIDE.has(st.display ?? '');
|
|
260
|
+
for (let i = 0; i < kids.length; i++) {
|
|
261
|
+
visit(kids[i], b, inner, row);
|
|
262
|
+
if (!runs)
|
|
263
|
+
continue;
|
|
264
|
+
// A run of lines after this child is an anonymous block box, and the box a forced `break-after` closing here is pending into: an event at its first word, in the word space of the box that holds it (*the anonymous box is in the procession*). Words standing between the child's end and the next child's start, or this box's end, are the run; none is no box.
|
|
265
|
+
const ext = extentOf;
|
|
266
|
+
const own = ext(el);
|
|
267
|
+
const done = ext(kids[i]);
|
|
268
|
+
const word = done.before + done.count;
|
|
269
|
+
const next = kids[i + 1];
|
|
270
|
+
if ((next ? ext(next).before : own.before + own.count) <= word)
|
|
271
|
+
continue;
|
|
272
|
+
events.push({ b, el, cut: cutOf(el, b, word - own.before), name: e.name, before: false, beforeParity: null, after: false, afterParity: null, end: events.length + 1, pos: e.pos + word - own.before, inFlow: true, sealedBy: inner, rowish: row, runAfter: kids[i] });
|
|
273
|
+
}
|
|
274
|
+
}
|
|
239
275
|
e.after = !parallel && isForced(st.breakAfter); // a float's own break-after was never read, and still is not
|
|
240
276
|
e.afterParity = parityOf(st.breakAfter);
|
|
241
277
|
e.end = events.length;
|
|
@@ -262,7 +298,7 @@ export function forcingIndexOf(blocks, { styleOf, pageNameOf, cutOf, posOf, posO
|
|
|
262
298
|
const e = events[k];
|
|
263
299
|
const before = !namesOnly && e.before;
|
|
264
300
|
// §4.3's combination at the one break point where two forced values meet: the `break-after` pending into the box, and the box's own `break-before`, the later element in the flow, whose parity wins where it has one. The address is a copy, so no page can reach into the index through it.
|
|
265
|
-
return { b: e.b, namedSwitch: e.name !== name, el: e.el, cut: { ...e.cut, path: [...e.cut.path] }, parity: combineParity(pending?.parity ?? null, before ? e.beforeParity : null), rowish: e.rowish };
|
|
301
|
+
return { b: e.b, namedSwitch: e.name !== name, el: e.el, cut: { ...e.cut, path: [...e.cut.path] }, parity: combineParity(pending?.parity ?? null, before ? e.beforeParity : null), rowish: e.rowish, runAfter: e.runAfter };
|
|
266
302
|
};
|
|
267
303
|
/** The first event strictly after the cursor — a binary search, because a bare address never precedes the one before it in walk order — or `n` where there is none. */
|
|
268
304
|
const firstAfter = (cursorCut) => {
|
|
@@ -318,6 +354,8 @@ export function forcingIndexOf(blocks, { styleOf, pageNameOf, cutOf, posOf, posO
|
|
|
318
354
|
k = events[e.sealedBy].end;
|
|
319
355
|
else if (!e.inFlow)
|
|
320
356
|
k = e.end;
|
|
357
|
+
else if (e.runAfter)
|
|
358
|
+
k++; // a run's place is the grab's alone: the Cast-off cuts at a box, and moving forward only grows the feed
|
|
321
359
|
else
|
|
322
360
|
return { b: e.b, el: e.el, cut: { ...e.cut, path: [...e.cut.path] } };
|
|
323
361
|
}
|
|
@@ -622,16 +660,18 @@ async function paginateOnce(doc, { composed = false, compositionCache = new Comp
|
|
|
622
660
|
/** The grab's events, indexed here, once (*the page asks the index*, 2026-09-25) — beside the document's own counters and for the same reason: the source is never written (*the source document is never written*, C5), so what the grab reads about a box is the same on every page, and the walk that asked it of the tree on every page cost the BGD manual 66 ms a page whatever the cursor. `forcingIndexOf` above carries the question and its rules; the page asks `forcing.find`. */
|
|
623
661
|
// The memo, if the seam offered one and the sheets leave it standing (*the forced-break index is kept across runs*, 2026-10-09; settled once for the call by `settledMemo`): the reads below are answered from it where an element was read before, and read from the browser and kept where not.
|
|
624
662
|
memo?.restart();
|
|
663
|
+
const extentOf = (el) => ({ before: index.wordsBefore(el), count: index.wordCountIn(el) });
|
|
625
664
|
const forcing = memo
|
|
626
665
|
? forcingIndexOf(blocks, {
|
|
627
666
|
styleOf: (el) => memo.facts(el, () => snapshotOfStyle(win.getComputedStyle(el))),
|
|
628
667
|
pageNameOf: (el) => memo.pageName(el, () => effectivePageName(el)),
|
|
629
668
|
blockChildren: (el) => memo.blockChildren(el, () => blockChildrenOf(el)),
|
|
630
|
-
cutOf: (el, b) => cutTo(el, b, { rawPath: pathOf(el, blocks[b], b, isFixedBox, (e) => memo.indexIn(e, () => inFlowIndex(e))) }),
|
|
669
|
+
cutOf: (el, b, word = null) => cutTo(el, b, { word, rawPath: pathOf(el, blocks[b], b, isFixedBox, (e) => memo.indexIn(e, () => inFlowIndex(e))) }),
|
|
631
670
|
posOf: (el, b) => index.posOf(b, index.wordsBefore(el)),
|
|
632
|
-
posOfCut
|
|
671
|
+
posOfCut,
|
|
672
|
+
extentOf
|
|
633
673
|
})
|
|
634
|
-
: forcingIndexOf(blocks, { styleOf: (el) => win.getComputedStyle(el), pageNameOf: effectivePageName, cutOf: (el, b) => cutTo(el, b), posOf: (el, b) => index.posOf(b, index.wordsBefore(el)), posOfCut });
|
|
674
|
+
: forcingIndexOf(blocks, { styleOf: (el) => win.getComputedStyle(el), pageNameOf: effectivePageName, cutOf: (el, b, word = null) => cutTo(el, b, { word }), posOf: (el, b) => index.posOf(b, index.wordsBefore(el)), posOfCut, extentOf });
|
|
635
675
|
performance.measure('cicero:forcing-index', { start: t, end: (t = performance.now()), detail: { events: forcing.size, ...(memo ? { memoHits: memo.hits, memoMisses: memo.misses } : {}) } });
|
|
636
676
|
/**
|
|
637
677
|
* The effective page name at an address: the deepest in-flow block that begins there, then css-page-3 §8.1's ancestor walk from it. A page may begin inside a block whose interior named a different page, so the cursor's block is not the thing to ask.
|
|
@@ -720,10 +760,18 @@ async function paginateOnce(doc, { composed = false, compositionCache = new Comp
|
|
|
720
760
|
* @param who - the caller, for the error.
|
|
721
761
|
*/
|
|
722
762
|
const cutCloneAt = (clone, cloneOfSource, place, who) => {
|
|
723
|
-
const target = cloneOfSource.get(place.el);
|
|
763
|
+
const target = cloneOfSource.get(place.runAfter ?? place.el);
|
|
724
764
|
if (!target || !clone.contains(target))
|
|
725
765
|
throw new Error(`${who} could not address its own place in block ${place.b} — a defect to file, not a bail to hide`);
|
|
726
|
-
|
|
766
|
+
if (!place.runAfter)
|
|
767
|
+
return void truncateFrom(clone, target, { hollow });
|
|
768
|
+
// A run has no element: the feed is cut at the first node after the block child it follows, which stands in the run, so the child stays whole and the run leaves with everything after it.
|
|
769
|
+
let at = target;
|
|
770
|
+
while (!at.nextSibling && at.parentNode && at !== clone)
|
|
771
|
+
at = at.parentNode;
|
|
772
|
+
if (!at.nextSibling)
|
|
773
|
+
throw new Error(`${who} found no run after its block child in block ${place.b}: a defect to file, not a bail to hide`);
|
|
774
|
+
truncateFrom(clone, at.nextSibling, { hollow });
|
|
727
775
|
};
|
|
728
776
|
const ledger = new Ledger();
|
|
729
777
|
// The cursor is the document's own first address — the path to block zero, which divides nothing — plus its projection and whatever flows are still running beside it.
|
|
@@ -735,11 +783,13 @@ async function paginateOnce(doc, { composed = false, compositionCache = new Comp
|
|
|
735
783
|
/** The conditions of page `pageIndex`, which begins at `cut` (projected to `pos`) — {@link conditionsFor} with the cursor made a parameter, so a resumed run can write the conditions of a page it keeps from the address that page began at (*the run resumes from an entry*, build two, 2026-10-08). */
|
|
736
784
|
const conditionsAt = (pageIndex, cut, pos, { blank = false, carried = null } = {}) => {
|
|
737
785
|
const name = nameAt(cut);
|
|
738
|
-
const
|
|
786
|
+
const away = remaindersOf(carried, index.endOf);
|
|
787
|
+
// The run's count, or an author's reset of the page counter counted on from the page it begins on (`document-state.js`'s `pageNumberAt`). Entry `i` closes page `i`, so page `i` begins where entry `i - 1` ended.
|
|
788
|
+
const pageNumber = pageNumberAt(documentOps, pageIndex, pos, { pages: (i) => ({ start: i === 0 ? 0 : account[i - 1].pos ?? 0, blank: account[i].blank === true }), blank, away });
|
|
739
789
|
const parity = parityAt(pageIndex);
|
|
740
790
|
const geo = table.pageFor({ name, first: pageIndex === 0, parity, blank });
|
|
741
|
-
// The document's counters as they stand at this page's first word — strictly before it, which is §6.1's *spans the page break* in the flat position space (`document-state.js`) — less what the flows carried onto this page have still to show, which stands before that word and is on this page or after it (beltype#43).
|
|
742
|
-
const counters = counterStateAt(documentOps, pos,
|
|
791
|
+
// The document's counters as they stand at this page's first word — strictly before it, which is §6.1's *spans the page break* in the flat position space (`document-state.js`) — less what the flows carried onto this page have still to show, which stands before that word and is on this page or after it (beltype#43). The page's own two are not among them: they are the host's, and `pageNumber` carries the reset.
|
|
792
|
+
const counters = withoutPageCounters(counterStateAt(documentOps, pos, away));
|
|
743
793
|
return Object.freeze({ ...geo, name, pageNumber, parity, blank, counters, dpr: win.devicePixelRatio });
|
|
744
794
|
};
|
|
745
795
|
/**
|
|
@@ -778,7 +828,7 @@ async function paginateOnce(doc, { composed = false, compositionCache = new Comp
|
|
|
778
828
|
const source = blocks[b];
|
|
779
829
|
const cut = b === first ? cursor.cut : null;
|
|
780
830
|
const ends = !!place && b === place.b;
|
|
781
|
-
if (ends && place.cut.path.length === 1)
|
|
831
|
+
if (ends && place.cut.path.length === 1 && place.cut.word == null)
|
|
782
832
|
break; // the place is this block's own start: the whole block leaves, exactly as the item-granular grab always cut
|
|
783
833
|
const { clone } = cloneFlowPiece(source, { start: divides(cut) ? cut : null, end: ends ? place.cut : null, items: itemsIn(b), hollow, into: cloneOfSource }); // no composition: the tail is cut first, then the hints
|
|
784
834
|
if (ends)
|
|
@@ -1013,6 +1063,8 @@ async function paginateOnce(doc, { composed = false, compositionCache = new Comp
|
|
|
1013
1063
|
if (!mayRepeat(src))
|
|
1014
1064
|
continue;
|
|
1015
1065
|
decided.add(src);
|
|
1066
|
+
if (footerBeforeRows(src) && g.getBoundingClientRect().height > conditions.contentH)
|
|
1067
|
+
throw new Error(refusedTable(src, 'a footer group taller than the page area, written before its rows', 'write the footer after the rows, where it is divided like them, or keep it within the page area'));
|
|
1016
1068
|
const repeat = g.getBoundingClientRect().height <= conditions.contentH / 4;
|
|
1017
1069
|
const b = blocks.findIndex((block) => block === src || block.contains(src));
|
|
1018
1070
|
madeHere.push({ path: pathOf(src, blocks[b], b), repeat });
|
|
@@ -1416,6 +1468,13 @@ async function paginateOnce(doc, { composed = false, compositionCache = new Comp
|
|
|
1416
1468
|
if (!fits && hasGroups) {
|
|
1417
1469
|
const ct = container.getBoundingClientRect().top;
|
|
1418
1470
|
const edge = pageStart + conditions.contentH;
|
|
1471
|
+
// A header written after its rows that does not repeat, in a table the page's edge divides, is refused (*thead-after-rows-and-tall-footer-lose-rows*, 2026-11-07): its words are on the table's last fragment and its box above the first, and which page shows it is the open ruling on §17.2's display order in the word space. Undivided, the one fragment holds both.
|
|
1472
|
+
for (const h of container.querySelectorAll('thead')) {
|
|
1473
|
+
const src = sourceOf(h);
|
|
1474
|
+
const r = h.closest('table')?.getBoundingClientRect();
|
|
1475
|
+
if (src && r && declined.has(src) && headerAfterRows(src) && r.top - ct < edge && r.bottom - ct > edge)
|
|
1476
|
+
throw new Error(refusedTable(src, 'a header group written after its rows, too tall to repeat'));
|
|
1477
|
+
}
|
|
1419
1478
|
for (const f of container.querySelectorAll('tfoot')) {
|
|
1420
1479
|
const src = sourceOf(f);
|
|
1421
1480
|
// A footer written before its rows that does not repeat is still reserved where it is shown: its words stand before the rows, so it is on the table's first fragment and nowhere after (*the footer before the rows*, 2026-10-15; Chromium shows it on the last, which the address cannot say).
|
|
@@ -20,5 +20,5 @@ export { assertRenderConditions, awaitCloneImages, awaitFonts, createMeasureCont
|
|
|
20
20
|
export { divides, elAt, extensionPx, indexBlocks, normalizePath, pageContaining, pageIntervals, pathOf, placeAt } from './positions.ts';
|
|
21
21
|
export { present, presentation } from './presenters.ts';
|
|
22
22
|
export { cssString, findPageSites, settleReads } from './reads.ts';
|
|
23
|
-
export { adjudicate, fitsPage, isOutOfFlow, neverCutInside, processionBottoms, reachesBelowTheEdge } from './stonehand.ts';
|
|
23
|
+
export { adjudicate, firstBelowTheEdge, fitsPage, isOutOfFlow, neverCutInside, processionBottoms, reachesBelowTheEdge } from './stonehand.ts';
|
|
24
24
|
export { BREAKING_SPACE, copyOf, isFixedBox, isInlineBox, isLineBreak, noteSource } from './word-space.ts';
|
|
@@ -19,5 +19,5 @@ export { assertRenderConditions, awaitCloneImages, awaitFonts, createMeasureCont
|
|
|
19
19
|
export { divides, elAt, extensionPx, indexBlocks, normalizePath, pageContaining, pageIntervals, pathOf, placeAt } from './positions.js';
|
|
20
20
|
export { present, presentation } from './presenters.js';
|
|
21
21
|
export { cssString, findPageSites, settleReads } from './reads.js';
|
|
22
|
-
export { adjudicate, fitsPage, isOutOfFlow, neverCutInside, processionBottoms, reachesBelowTheEdge } from './stonehand.js';
|
|
22
|
+
export { adjudicate, firstBelowTheEdge, fitsPage, isOutOfFlow, neverCutInside, processionBottoms, reachesBelowTheEdge } from './stonehand.js';
|
|
23
23
|
export { BREAKING_SPACE, copyOf, isFixedBox, isInlineBox, isLineBreak, noteSource } from './word-space.js';
|
package/dist/src/fragments.d.ts
CHANGED
|
@@ -96,10 +96,10 @@ export declare function mayRepeat(src: Element): boolean;
|
|
|
96
96
|
* **`keep` is the first fragment's door into the same surgery** (*the first fragment is constructed*, 2026-09-15). A shell continues a box whose own content closed on the page before, so page one wants everything after that box gone and the box itself standing — the one difference from the grab's reading, where the boundary box is what resumes. With `keep`, the boundary element stays and is dressed as a fragment that ends there, which it is.
|
|
97
97
|
*
|
|
98
98
|
* @param clone - the top-level block's clone, already hinted and already consumed to the cursor.
|
|
99
|
-
* @param el - the boundary
|
|
99
|
+
* @param el - the boundary node, addressed in the clone's own coordinates: an element, or the first node of a run of lines, which has no element (*the anonymous box is in the procession*).
|
|
100
100
|
* @param opts - `keep`: the boundary element stays in the fragment (and is dressed), rather than leaving with its successors. `hollow`: what leaves is kept hollow (`putAway`).
|
|
101
101
|
*/
|
|
102
|
-
export declare function truncateFrom(clone: Element, el:
|
|
102
|
+
export declare function truncateFrom(clone: Element, el: ChildNode, { keep, hollow }?: {
|
|
103
103
|
keep?: boolean;
|
|
104
104
|
hollow?: boolean;
|
|
105
105
|
}): Element;
|
package/dist/src/fragments.js
CHANGED
|
@@ -317,7 +317,7 @@ function liftRepeatedHeader(group) {
|
|
|
317
317
|
* **`keep` is the first fragment's door into the same surgery** (*the first fragment is constructed*, 2026-09-15). A shell continues a box whose own content closed on the page before, so page one wants everything after that box gone and the box itself standing — the one difference from the grab's reading, where the boundary box is what resumes. With `keep`, the boundary element stays and is dressed as a fragment that ends there, which it is.
|
|
318
318
|
*
|
|
319
319
|
* @param clone - the top-level block's clone, already hinted and already consumed to the cursor.
|
|
320
|
-
* @param el - the boundary
|
|
320
|
+
* @param el - the boundary node, addressed in the clone's own coordinates: an element, or the first node of a run of lines, which has no element (*the anonymous box is in the procession*).
|
|
321
321
|
* @param opts - `keep`: the boundary element stays in the fragment (and is dressed), rather than leaving with its successors. `hollow`: what leaves is kept hollow (`putAway`).
|
|
322
322
|
*/
|
|
323
323
|
export function truncateFrom(clone, el, { keep = false, hollow = false } = {}) {
|
|
@@ -377,22 +377,59 @@ function charRanges(word, from, to) {
|
|
|
377
377
|
}
|
|
378
378
|
return out;
|
|
379
379
|
}
|
|
380
|
+
/** Whether the whitespace of a text node stands in its layout (css-text-4 §4.1): where it collapses it is nothing at a line's edge, and where it is preserved each newline draws a line. */
|
|
381
|
+
const preservesSpace = (t) => !!t.parentElement && /^(pre|pre-wrap|pre-line|break-spaces)$/.test(sourceStyle(t.parentElement)?.whiteSpace ?? '');
|
|
382
|
+
/** Take a stretch of a cut's whitespace out of the fragment: leave it where it collapses, so layout reads the data exactly as before, and remove it where it is preserved, or a continuation opens on the empty lines of every line cut away. Either way the record says it is not shown. */
|
|
383
|
+
function dropSpace(t, from, to) {
|
|
384
|
+
if (to <= from)
|
|
385
|
+
return;
|
|
386
|
+
if (preservesSpace(t))
|
|
387
|
+
cutRanges(t, [{ start: from, end: to }]);
|
|
388
|
+
else
|
|
389
|
+
leaveOut(t, from, to);
|
|
390
|
+
}
|
|
380
391
|
/**
|
|
381
|
-
*
|
|
392
|
+
* The indentation of the line a resume word opens: the whitespace before it back to a newline, in characters. Read before the words before it go, because only they tell the space that opens a line from the space that followed a word. A cut at a soft wrap has no newline in that gap and no indentation, the space hanging at the end of the line before; a line after a `<br>` looks the same and loses its indentation at the top of a page.
|
|
393
|
+
*/
|
|
394
|
+
function indentationBefore(clone, node, start) {
|
|
395
|
+
let text = '';
|
|
396
|
+
for (const t of textsIn(clone)) {
|
|
397
|
+
if (t === node) {
|
|
398
|
+
text += t.data.slice(0, start);
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
401
|
+
text += t.data;
|
|
402
|
+
}
|
|
403
|
+
let i = text.length;
|
|
404
|
+
while (i > 0 && BREAKING_SPACE.test(text[i - 1]))
|
|
405
|
+
i--;
|
|
406
|
+
const newline = text.lastIndexOf('\n');
|
|
407
|
+
return newline < i ? 0 : text.length - newline - 1;
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Say that everything before the resume point is held and not shown (*the engine names the source point*, 2026-10-13). Consuming blanks the words and leaves the whitespace between them, so a continuation's data still begins with the spaces of every word cut away; those are the source's characters and no part of the interval, and so are the whitespace before the first kept word and every text node that stands wholly before it. Where the whitespace collapses these are records only, and where it is preserved they leave the data (`dropSpace`), all but the indentation of the line the page opens on.
|
|
382
411
|
*
|
|
383
412
|
* @param resume - the node holding the first kept character, or null when nothing is kept.
|
|
413
|
+
* @param indent - the characters immediately before it to keep, from {@link indentationBefore}.
|
|
384
414
|
*/
|
|
385
|
-
function leaveOutBefore(clone, resume) {
|
|
415
|
+
function leaveOutBefore(clone, resume, indent) {
|
|
416
|
+
const lead = [];
|
|
386
417
|
for (const t of textsIn(clone)) {
|
|
387
418
|
if (t === resume)
|
|
388
419
|
break;
|
|
389
|
-
|
|
420
|
+
lead.push({ node: t, end: t.data.length });
|
|
390
421
|
}
|
|
391
422
|
if (resume) {
|
|
392
423
|
let at = 0;
|
|
393
424
|
while (at < resume.data.length && BREAKING_SPACE.test(resume.data[at]))
|
|
394
425
|
at++;
|
|
395
|
-
|
|
426
|
+
lead.push({ node: resume, end: at });
|
|
427
|
+
}
|
|
428
|
+
let left = indent;
|
|
429
|
+
for (const { node, end } of lead.reverse()) {
|
|
430
|
+
const kept = preservesSpace(node) ? Math.min(left, end) : 0;
|
|
431
|
+
left -= kept;
|
|
432
|
+
dropSpace(node, 0, end - kept);
|
|
396
433
|
}
|
|
397
434
|
}
|
|
398
435
|
/** The mirror: everything after the last kept character is held and not shown.@param keep - the node holding the last kept character. */
|
|
@@ -400,14 +437,14 @@ function leaveOutAfter(clone, keep) {
|
|
|
400
437
|
let end = keep.data.length;
|
|
401
438
|
while (end > 0 && BREAKING_SPACE.test(keep.data[end - 1]))
|
|
402
439
|
end--;
|
|
403
|
-
leaveOut(keep, end, keep.data.length);
|
|
404
440
|
let after = false;
|
|
405
|
-
for (const t of textsIn(clone)) {
|
|
441
|
+
for (const t of [...textsIn(clone)]) {
|
|
406
442
|
if (after)
|
|
407
|
-
|
|
443
|
+
dropSpace(t, 0, t.data.length);
|
|
408
444
|
else if (t === keep)
|
|
409
445
|
after = true;
|
|
410
446
|
}
|
|
447
|
+
dropSpace(keep, end, keep.data.length);
|
|
411
448
|
}
|
|
412
449
|
/** The text nodes under an element, in document order — by `childNodes`, as `joinedWords` reads, so the DOM-free suites' trees serve. */
|
|
413
450
|
function* textsIn(el) {
|
|
@@ -426,7 +463,9 @@ function consumeLeadingWords(clone, n, char = 0, hollow = false) {
|
|
|
426
463
|
// A resume inside word n (*a line inside a word is a break point*, 2026-09-23): its first `char` characters were shown on the page before, and go with the words before it. The page resumes at the part holding the first character left.
|
|
427
464
|
const inside = char > 0 && n < words.length ? charRanges(words[n], 0, char) : [];
|
|
428
465
|
const resumeWord = words[Math.min(n, words.length - 1)];
|
|
429
|
-
const
|
|
466
|
+
const resumePart = n < words.length ? partAt(resumeWord, char) ?? resumeWord.parts[0] : null;
|
|
467
|
+
const resumeNode = resumePart?.node ?? null;
|
|
468
|
+
const indent = resumePart && char === 0 ? indentationBefore(clone, resumePart.node, resumePart.start) : 0;
|
|
430
469
|
// Blank the consumed words' exact character ranges, one pass a node (`word-space.js`' `cutRanges`, which records what went).
|
|
431
470
|
const perNode = new Map();
|
|
432
471
|
for (const part of [...words.slice(0, n).flatMap((w) => w.parts), ...inside]) {
|
|
@@ -436,7 +475,7 @@ function consumeLeadingWords(clone, n, char = 0, hollow = false) {
|
|
|
436
475
|
}
|
|
437
476
|
for (const [node, ranges] of perNode)
|
|
438
477
|
cutRanges(node, ranges);
|
|
439
|
-
leaveOutBefore(clone, resumeNode);
|
|
478
|
+
leaveOutBefore(clone, resumeNode, indent);
|
|
440
479
|
// Structural pass: an element with no remaining words, standing entirely before the resume point, is spent — its edges adjoin the cut (§5.2) and it leaves the fragment.
|
|
441
480
|
const spent = [];
|
|
442
481
|
(function walk(el) {
|
package/dist/src/gauge.d.ts
CHANGED
|
@@ -45,6 +45,8 @@ export type Box = {
|
|
|
45
45
|
anonymous?: boolean;
|
|
46
46
|
/** A table's footer group written before its rows (`footerBeforeRows`), moved to the end of the table's procession, where §17.2 lays it out. */
|
|
47
47
|
laidOutLast?: boolean;
|
|
48
|
+
/** A table's header group written after its rows (`headerAfterRows`), moved to the front of the table's procession, where §17.2 lays it out. */
|
|
49
|
+
laidOutFirst?: boolean;
|
|
48
50
|
};
|
|
49
51
|
/** One line box, recovered arithmetically from the rects clustered onto it and the line-heights they are set on (`computeLines`). */
|
|
50
52
|
export type LineBox = {
|
|
@@ -95,6 +97,8 @@ export type Tracks = {
|
|
|
95
97
|
};
|
|
96
98
|
/** The sealed report {@link takeReport} hands back. */
|
|
97
99
|
export type MeasureReport = ReturnType<typeof takeReport>;
|
|
100
|
+
/** A `display` whose inline content CSS 2.1 §9.2.1.1 wraps in anonymous block boxes — a block container's. A flex or grid container makes a run an anonymous item and a table an anonymous cell, which are other boxes and other questions. */
|
|
101
|
+
export declare const holdsAnonymousBlocks: (display: string) => boolean;
|
|
98
102
|
/**
|
|
99
103
|
* Read one settled layout into an immutable snapshot — obtaining it seals the mutation phase (*render-identical conditions*, I5, by type).
|
|
100
104
|
*
|
package/dist/src/gauge.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* @packageDocumentation
|
|
16
16
|
*/
|
|
17
17
|
import { BREAK, selectorOf } from './data-beltype.js';
|
|
18
|
-
import { blockChildrenOf, footerBeforeRows, isAtom, isEngineText, isInlineBox, isLineBreak, mayBeAutoHyphenated, rangeOf, READS_ITS_CHILDREN, rowsAmong, SIDE_BY_SIDE, wordRuns } from './word-space.js';
|
|
18
|
+
import { blockChildrenOf, footerBeforeRows, headerAfterRows, isAtom, isEngineText, isInlineBox, isLineBreak, mayBeAutoHyphenated, rangeOf, READS_ITS_CHILDREN, rowsAmong, SIDE_BY_SIDE, wordRuns } from './word-space.js';
|
|
19
19
|
/** The Compositor's own `<br>`, found by the browser and never by splitting the attribute here (the walkers below run per box). */
|
|
20
20
|
const BREAK_SELECTOR = `br${selectorOf(BREAK)}`;
|
|
21
21
|
/**
|
|
@@ -28,7 +28,7 @@ const sharesLine = (aTop, aBottom, bTop, bBottom) => Math.min(aBottom, bBottom)
|
|
|
28
28
|
/** A character a word is made of: anything `BREAKING_SPACE` does not divide at. */
|
|
29
29
|
const WORD_CHAR = /[\S ]/;
|
|
30
30
|
/** A `display` whose inline content CSS 2.1 §9.2.1.1 wraps in anonymous block boxes — a block container's. A flex or grid container makes a run an anonymous item and a table an anonymous cell, which are other boxes and other questions. */
|
|
31
|
-
const holdsAnonymousBlocks = (display) => !/flex|grid|-webkit-box/.test(display) && (!/table/.test(display) || /^table-(cell|caption)$/.test(display));
|
|
31
|
+
export const holdsAnonymousBlocks = (display) => !/flex|grid|-webkit-box/.test(display) && (!/table/.test(display) || /^table-(cell|caption)$/.test(display));
|
|
32
32
|
const GAP_MIN = 0.75; // px — a third rect-reading rule, learned from the differential: half-leading float arithmetic leaves sub-pixel residue between a text box's last line and its border, and reporting that residue as a gap fabricates Class C cuts that bypass keeps. Real gaps (padding, margins, constrained heights) are ≥ 1px; noise stays under half a pixel.
|
|
33
33
|
/**
|
|
34
34
|
* Read one settled layout into an immutable snapshot — obtaining it seals the mutation phase (*render-identical conditions*, I5, by type).
|
|
@@ -91,6 +91,18 @@ export function takeReport(container, { contentHeight }) {
|
|
|
91
91
|
list.splice(last, 0, footer); // `last` moved down by one with the removal, so this is the place after it
|
|
92
92
|
footer.laidOutLast = moved = true;
|
|
93
93
|
}
|
|
94
|
+
// The header stands first, the mirror (*thead-after-rows-and-tall-footer-lose-rows*, 2026-11-07): §17.2 lays a `table-header-group` out *"before all other rows and row groups"*, so one written after them is moved before the first. Its words stay after the rows in the word space, which is why the Stonehand offers no place inside it or right after it, and why the driver refuses one that does not repeat: only a copy can show it on a page before its words.
|
|
95
|
+
for (const list of [box.blockChildren, box.children]) {
|
|
96
|
+
const h = list.findIndex((c) => headerAfterRows(c.el));
|
|
97
|
+
if (h < 0)
|
|
98
|
+
continue;
|
|
99
|
+
const first = list.findIndex((c) => /^table-row(-group)?$/.test(win.getComputedStyle(c.el).display));
|
|
100
|
+
if (first < 0 || first > h)
|
|
101
|
+
continue;
|
|
102
|
+
const [header] = list.splice(h, 1);
|
|
103
|
+
list.splice(first, 0, header);
|
|
104
|
+
header.laidOutFirst = moved = true;
|
|
105
|
+
}
|
|
94
106
|
}
|
|
95
107
|
/**
|
|
96
108
|
* The anonymous boxes (*the anonymous box is in the procession*, 2026-09-29; CSS 2.1 §9.2.1.1). A block container holding a block-level box beside inline content wraps each maximal run of that content in an anonymous block box, and css-break-3 §4.1 divides the result like any procession: a Class A point between the run and the block beside it, Class B points between its lines, and no Class C point where it stands, since a line is child content. The procession used to be the element tree's block children alone, so a run had no box: its lines were read as nothing but the distance they opened, a start gap before the first block child and an end gap after the last, and a page edge crossing thirteen trailing lines shelled them blank (`packages/essay/cases/76-the-line-before-the-first-block`).
|
|
@@ -7,6 +7,8 @@ export default {
|
|
|
7
7
|
id: ['en-us', 'en'],
|
|
8
8
|
leftmin: 2,
|
|
9
9
|
rightmin: 3,
|
|
10
|
+
// The word is its ASCII letters: hyphen.tex's patterns know no other, so an accented word yields nothing, as before. What stands around it, punctuation, quotation marks, brackets and a possessive's `'s`, moves no offset (Codeberg issue 6): until 2026-11-07 `computer,` had no candidates and `computer` had them. An apostrophe inside a word is not a letter here, where Italian reads an elision, since the patterns were built for TeX, which reads `don't` as two words.
|
|
11
|
+
word: /^(\P{L}*)([a-zA-Z]+)((?:['’]s)?\P{L}*)$/u,
|
|
10
12
|
// hyphen.tex's own exception list: words the patterns get wrong, hyphenations given literally.
|
|
11
13
|
exceptions: ['as-so-ciate', 'as-so-ciates', 'dec-li-na-tion', 'oblig-a-tory', 'phil-an-thropic', 'present', 'presents', 'project', 'projects', 'reci-procity', 're-cog-ni-zance', 'ref-or-ma-tion', 'ret-ri-bu-tion', 'ta-ble'],
|
|
12
14
|
patterns: [
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vendored Italian hyphenation patterns, hyph-it.tex version 4.9 of 2014-04-22 from the hyph-utf8 package (github.com/hyphenation/tex-hyphen), which follow the Recommendation UNI 6461. TeX's word boundary `.` is written `_` here, as `src/hyphenate.js` expects. Data, not code: the trie is built by src/hyphenate.js.
|
|
3
|
+
*
|
|
4
|
+
* Copyright (C) 2008-2011 Claudio Beccari, available under LPPL 1.3 or later or under the MIT licence, taken here under MIT:
|
|
5
|
+
*
|
|
6
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
7
|
+
*
|
|
8
|
+
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
9
|
+
*
|
|
10
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
11
|
+
*
|
|
12
|
+
* @packageDocumentation
|
|
13
|
+
*/
|
|
14
|
+
declare const _default: {
|
|
15
|
+
id: string[];
|
|
16
|
+
leftmin: number;
|
|
17
|
+
rightmin: number;
|
|
18
|
+
word: RegExp;
|
|
19
|
+
patterns: string[];
|
|
20
|
+
};
|
|
21
|
+
export default _default;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vendored Italian hyphenation patterns, hyph-it.tex version 4.9 of 2014-04-22 from the hyph-utf8 package (github.com/hyphenation/tex-hyphen), which follow the Recommendation UNI 6461. TeX's word boundary `.` is written `_` here, as `src/hyphenate.js` expects. Data, not code: the trie is built by src/hyphenate.js.
|
|
3
|
+
*
|
|
4
|
+
* Copyright (C) 2008-2011 Claudio Beccari, available under LPPL 1.3 or later or under the MIT licence, taken here under MIT:
|
|
5
|
+
*
|
|
6
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
7
|
+
*
|
|
8
|
+
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
9
|
+
*
|
|
10
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
11
|
+
*
|
|
12
|
+
* @packageDocumentation
|
|
13
|
+
*/
|
|
14
|
+
export default ({
|
|
15
|
+
id: ['it-it', 'it'],
|
|
16
|
+
leftmin: 2,
|
|
17
|
+
rightmin: 2,
|
|
18
|
+
// Accents and elisions are ordinary in Italian, and a word in running text may carry its punctuation: the core is what the patterns read, apostrophes included, because they have patterns of their own.
|
|
19
|
+
word: /^(\P{L}*)([\p{L}'’]+?)(\P{L}*)$/u,
|
|
20
|
+
patterns: [
|
|
21
|
+
"_a3p2n", "_anti1", "_anti3m2n", "_bio1", "_ca4p3s", "_circu2m1", "_contro1", "_di2s3cine", "_e2x1eu", "_fran2k3", "_free3", "_li3p2sa",
|
|
22
|
+
"_narco1", "_opto1", "_orto3p2", "_para1", "_ph2l", "_ph2r", "_poli3p2", "_pre1", "_p2s", "_re1i2scr", "_sha2re3", "_tran2s3c",
|
|
23
|
+
"_tran2s3d", "_tran2s3l", "_tran2s3n", "_tran2s3p", "_tran2s3r", "_tran2s3t", "_su2b3lu", "_su2b3r", "_wa2g3n", "_wel2t1", "2'2", "a1ia",
|
|
24
|
+
"a1ie", "a1io", "a1iu", "a1uo", "a1ya", "2at_", "e1iu", "e2w", "o1ia", "o1ie", "o1io", "o1iu",
|
|
25
|
+
"1b", "2bb", "2bc", "2bd", "2bf", "2bm", "2bn", "2bp", "2bs", "2bt", "2bv", "b2l",
|
|
26
|
+
"b2r", "2b_", "2b'", "1c", "2cb", "2cc", "2cd", "2cf", "2ck", "2cm", "2cn", "2cq",
|
|
27
|
+
"2cs", "2ct", "2cz", "2chh", "c2h", "2ch_", "2ch'_", "2ch''_", "2chb", "ch2r", "2chn", "c2l",
|
|
28
|
+
"c2r", "2c_", "2c'", "_c2", "1d", "2db", "2dd", "2dg", "2dl", "2dm", "2dn", "2dp",
|
|
29
|
+
"d2r", "2ds", "2dt", "2dv", "2dw", "2d_", "2d'", "_d2", "1f", "2fb", "2fg", "2ff",
|
|
30
|
+
"2fn", "f2l", "f2r", "2fs", "2ft", "2f_", "2f'", "1g", "2gb", "2gd", "2gf", "2gg",
|
|
31
|
+
"g2h", "g2l", "2gm", "g2n", "2gp", "g2r", "2gs", "2gt", "2gv", "2gw", "2gz", "2gh2t",
|
|
32
|
+
"2g_", "2g'", "_h2", "1h", "2hb", "2hd", "2hh", "hi3p2n", "h2l", "2hm", "2hn", "2hr",
|
|
33
|
+
"2hv", "2h_", "2h'", "_j2", "1j", "2j_", "2j'", "_k2", "1k", "2kg", "2kf", "k2h",
|
|
34
|
+
"2kk", "k2l", "2km", "k2r", "2ks", "2kt", "2k_", "2k'", "1l", "2lb", "2lc", "2ld",
|
|
35
|
+
"2l3f2", "2lg", "l2h", "l2j", "2lk", "2ll", "2lm", "2ln", "2lp", "2lq", "2lr", "2ls",
|
|
36
|
+
"2lt", "2lv", "2lw", "2lz", "2l_", "2l'_", "2l''", "1m", "2mb", "2mc", "2mf", "2ml",
|
|
37
|
+
"2mm", "2mn", "2mp", "2mq", "2mr", "2ms", "2mt", "2mv", "2mw", "2m_", "2m'", "1n",
|
|
38
|
+
"2nb", "2nc", "2nd", "2nf", "2ng", "2nk", "2nl", "2nm", "2nn", "2np", "2nq", "2nr",
|
|
39
|
+
"2ns", "n2s3fer", "2nt", "2nv", "2nz", "n2g3n", "2nheit", "2n_", "2n'", "1p", "2pd", "p2h",
|
|
40
|
+
"p2l", "2pn", "3p2ne", "2pp", "p2r", "2ps", "3p2sic", "2pt", "2pz", "2p_", "2p'", "1q",
|
|
41
|
+
"2qq", "2q_", "2q'", "1r", "2rb", "2rc", "2rd", "2rf", "r2h", "2rg", "2rk", "2rl",
|
|
42
|
+
"2rm", "2rn", "2rp", "2rq", "2rr", "2rs", "2rt", "r2t2s3", "2rv", "2rx", "2rw", "2rz",
|
|
43
|
+
"2r_", "2r'", "1s2", "2shm", "2sh_", "2sh'", "2s3s", "s4s3m", "2s3p2n", "2stb", "2stc", "2std",
|
|
44
|
+
"2stf", "2stg", "2stm", "2stn", "2stp", "2sts", "2stt", "2stv", "2sz", "4s_", "4s'_", "4s''",
|
|
45
|
+
"_t2", "1t", "2tb", "2tc", "2td", "2tf", "2tg", "t2h", "2th_", "t2l", "2tm", "2tn",
|
|
46
|
+
"2tp", "t2r", "t2s", "3t2sch", "2tt", "t2t3s", "2tv", "2tw", "t2z", "2tzk", "tz2s", "2t_",
|
|
47
|
+
"2t'_", "2t''", "1v", "2vc", "v2l", "v2r", "2vv", "2v_", "2v'_", "2v''", "1w", "w2h",
|
|
48
|
+
"wa2r", "2w1y", "2w_", "2w'", "1x", "2xb", "2xc", "2xf", "2xh", "2xm", "2xp", "2xt",
|
|
49
|
+
"2xw", "2x_", "2x'", "y1ou", "y1i", "1z", "2zb", "2zd", "2zl", "2zn", "2zp", "2zt",
|
|
50
|
+
"2zs", "2zv", "2zz", "2z_", "2z'_", "2z''", "_z2",
|
|
51
|
+
]
|
|
52
|
+
});
|
package/dist/src/hyphenate.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* @packageDocumentation
|
|
9
9
|
*/
|
|
10
|
-
/** A pattern set as a language module supplies it
|
|
10
|
+
/** A pattern set as a language module supplies it, as `hyphen-patterns/en-us.js` and `hyphen-patterns/it.js` do. */
|
|
11
11
|
export type LanguageData = {
|
|
12
12
|
/** Every `lang` value the set answers, lower-case. */
|
|
13
13
|
id: string[];
|
|
@@ -17,6 +17,8 @@ export type LanguageData = {
|
|
|
17
17
|
patterns: string[];
|
|
18
18
|
/** Hyphenations given literally, `-` at every division. */
|
|
19
19
|
exceptions?: string[];
|
|
20
|
+
/** What counts as a word, as three groups: what stands before it, the word the patterns read, and what stands after it. Absent, plain ASCII letters and nothing around them. */
|
|
21
|
+
word?: RegExp;
|
|
20
22
|
};
|
|
21
23
|
/** A registered language: the trie built once. A trie node maps a letter to its child node and `'$'` to the digit list of the pattern ending there, which is why its values are untyped. */
|
|
22
24
|
export type Language = {
|
|
@@ -25,6 +27,7 @@ export type Language = {
|
|
|
25
27
|
exceptions: Map<string, string>;
|
|
26
28
|
leftmin: number;
|
|
27
29
|
rightmin: number;
|
|
30
|
+
word: RegExp;
|
|
28
31
|
};
|
|
29
32
|
/** css-text-4 §6.3.4's three values; a null or an absence is `auto`. */
|
|
30
33
|
export type HyphenateLimits = {
|
|
@@ -53,4 +56,4 @@ export declare function resolveLimits(lang: Pick<Language, 'leftmin' | 'rightmin
|
|
|
53
56
|
*
|
|
54
57
|
* @param lang - from `languageFor`; null yields no candidates.
|
|
55
58
|
*/
|
|
56
|
-
export declare function hyphenationOffsets(
|
|
59
|
+
export declare function hyphenationOffsets(text: string, lang: Language | null, limits?: HyphenateLimits): number[];
|