postext 1.29.2 → 1.30.1
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/__tests__/arabicLocale.test.js +5 -5
- package/dist/__tests__/arabicLocale.test.js.map +1 -1
- package/dist/__tests__/bundle/balloonPlacementPin.test.js +5 -4
- package/dist/__tests__/bundle/balloonPlacementPin.test.js.map +1 -1
- package/dist/__tests__/bundle/cjkRulesPin.test.js +8 -6
- package/dist/__tests__/bundle/cjkRulesPin.test.js.map +1 -1
- package/dist/__tests__/bundle/designOverflowPin.test.js +1 -1
- package/dist/__tests__/bundle/flowColumnsPin.test.js +4 -3
- package/dist/__tests__/bundle/flowColumnsPin.test.js.map +1 -1
- package/dist/__tests__/bundle/gridBalancingPin.test.js +5 -4
- package/dist/__tests__/bundle/gridBalancingPin.test.js.map +1 -1
- package/dist/__tests__/bundle/gridBreakRulesPin.test.js +14 -10
- package/dist/__tests__/bundle/gridBreakRulesPin.test.js.map +1 -1
- package/dist/__tests__/bundle/indexGroupSplitPin.test.js +4 -3
- package/dist/__tests__/bundle/indexGroupSplitPin.test.js.map +1 -1
- package/dist/__tests__/bundle/looseOrderPin.test.js +5 -4
- package/dist/__tests__/bundle/looseOrderPin.test.js.map +1 -1
- package/dist/__tests__/bundle/refLabelBreaksPin.test.js +5 -4
- package/dist/__tests__/bundle/refLabelBreaksPin.test.js.map +1 -1
- package/dist/__tests__/bundle/scriptContextPin.test.js +6 -5
- package/dist/__tests__/bundle/scriptContextPin.test.js.map +1 -1
- package/dist/__tests__/bundle/shapingLanguagePin.test.js +142 -46
- package/dist/__tests__/bundle/shapingLanguagePin.test.js.map +1 -1
- package/dist/__tests__/bundle/taskBoxPin.test.js +1 -1
- package/dist/__tests__/bundle/trailingStepPin.test.js +4 -3
- package/dist/__tests__/bundle/trailingStepPin.test.js.map +1 -1
- package/dist/__tests__/bundle/wideFloatStretchPin.test.js +4 -3
- package/dist/__tests__/bundle/wideFloatStretchPin.test.js.map +1 -1
- package/dist/__tests__/bundle.test.js +1 -1
- package/dist/__tests__/cellText.test.d.ts +2 -0
- package/dist/__tests__/cellText.test.d.ts.map +1 -0
- package/dist/__tests__/cellText.test.js +281 -0
- package/dist/__tests__/cellText.test.js.map +1 -0
- package/dist/__tests__/chineseLocale.test.js +13 -11
- package/dist/__tests__/chineseLocale.test.js.map +1 -1
- package/dist/__tests__/cjk/japaneseHtml.test.js +3 -3
- package/dist/__tests__/cjk/japaneseHtml.test.js.map +1 -1
- package/dist/__tests__/cjk/japaneseRuby.test.js +1 -1
- package/dist/__tests__/cjk/japaneseRuby.test.js.map +1 -1
- package/dist/__tests__/cjk/japaneseSegmentLanguage.test.js +3 -3
- package/dist/__tests__/cjk/japaneseSegmentLanguage.test.js.map +1 -1
- package/dist/__tests__/codeBlocks.test.js +1 -1
- package/dist/__tests__/contextText.test.d.ts +2 -0
- package/dist/__tests__/contextText.test.d.ts.map +1 -0
- package/dist/__tests__/contextText.test.js +371 -0
- package/dist/__tests__/contextText.test.js.map +1 -0
- package/dist/__tests__/design/designTextRender.test.js +8 -1
- package/dist/__tests__/design/designTextRender.test.js.map +1 -1
- package/dist/__tests__/exports.test.js +6 -0
- package/dist/__tests__/exports.test.js.map +1 -1
- package/dist/__tests__/fontUsage.test.js +40 -0
- package/dist/__tests__/fontUsage.test.js.map +1 -1
- package/dist/__tests__/hardBreaks.test.js +1 -1
- package/dist/__tests__/headingInlineMarks.test.js +1 -1
- package/dist/__tests__/html-backend.test.js +6 -2
- package/dist/__tests__/html-backend.test.js.map +1 -1
- package/dist/__tests__/japaneseNotes.test.js +4 -2
- package/dist/__tests__/japaneseNotes.test.js.map +1 -1
- package/dist/__tests__/lineEndPieces.test.d.ts +2 -0
- package/dist/__tests__/lineEndPieces.test.d.ts.map +1 -0
- package/dist/__tests__/lineEndPieces.test.js +374 -0
- package/dist/__tests__/lineEndPieces.test.js.map +1 -0
- package/dist/__tests__/mathSizeMigration.test.js +2 -2
- package/dist/__tests__/pipeline/calloutInlineResourceGap.test.js +1 -1
- package/dist/__tests__/pipeline/calloutSplitMinLinesChild.test.js +1 -1
- package/dist/__tests__/pipeline/colonListRoom.test.js +1 -1
- package/dist/__tests__/pipeline/compoundBreaks.test.js +1 -1
- package/dist/__tests__/pipeline/dashBreaks.test.js +1 -1
- package/dist/__tests__/pipeline/headingSplitOrphans.test.js +1 -1
- package/dist/__tests__/pipeline/inlineFigureSpace.test.js +1 -1
- package/dist/__tests__/pipeline/paragraphContainerTail.test.js +1 -1
- package/dist/__tests__/pipeline/raggedOptimal.test.js +1 -1
- package/dist/__tests__/plainGeminateHead.test.d.ts +2 -0
- package/dist/__tests__/plainGeminateHead.test.d.ts.map +1 -0
- package/dist/__tests__/plainGeminateHead.test.js +92 -0
- package/dist/__tests__/plainGeminateHead.test.js.map +1 -0
- package/dist/__tests__/presets/showcaseShapingLanguage.test.d.ts +2 -0
- package/dist/__tests__/presets/showcaseShapingLanguage.test.d.ts.map +1 -0
- package/dist/__tests__/presets/showcaseShapingLanguage.test.js +52 -0
- package/dist/__tests__/presets/showcaseShapingLanguage.test.js.map +1 -0
- package/dist/__tests__/reshapedTracking.test.d.ts +2 -0
- package/dist/__tests__/reshapedTracking.test.d.ts.map +1 -0
- package/dist/__tests__/reshapedTracking.test.js +358 -0
- package/dist/__tests__/reshapedTracking.test.js.map +1 -0
- package/dist/__tests__/rtl/designRunsPainted.test.d.ts +2 -0
- package/dist/__tests__/rtl/designRunsPainted.test.d.ts.map +1 -0
- package/dist/__tests__/rtl/designRunsPainted.test.js +184 -0
- package/dist/__tests__/rtl/designRunsPainted.test.js.map +1 -0
- package/dist/__tests__/rtl/designTextPaint.test.js +5 -1
- package/dist/__tests__/rtl/designTextPaint.test.js.map +1 -1
- package/dist/__tests__/rtl/htmlRtl.test.js +3 -3
- package/dist/__tests__/rtl/htmlRtl.test.js.map +1 -1
- package/dist/__tests__/rtl/listMarkerDirection.test.js +28 -28
- package/dist/__tests__/rtl/listMarkerDirection.test.js.map +1 -1
- package/dist/__tests__/rtl/tocNumberPainted.test.d.ts +2 -0
- package/dist/__tests__/rtl/tocNumberPainted.test.d.ts.map +1 -0
- package/dist/__tests__/rtl/tocNumberPainted.test.js +113 -0
- package/dist/__tests__/rtl/tocNumberPainted.test.js.map +1 -0
- package/dist/__tests__/scriptContext.test.js +141 -1
- package/dist/__tests__/scriptContext.test.js.map +1 -1
- package/dist/__tests__/shapingLanguage.test.js +54 -41
- package/dist/__tests__/shapingLanguage.test.js.map +1 -1
- package/dist/__tests__/trackedLetters.test.d.ts +2 -0
- package/dist/__tests__/trackedLetters.test.d.ts.map +1 -0
- package/dist/__tests__/trackedLetters.test.js +380 -0
- package/dist/__tests__/trackedLetters.test.js.map +1 -0
- package/dist/__tests__/trackedLigatures.test.js +17 -5
- package/dist/__tests__/trackedLigatures.test.js.map +1 -1
- package/dist/__tests__/verseLines.test.js +1 -1
- package/dist/__tests__/wordCacheOrder.test.d.ts +2 -0
- package/dist/__tests__/wordCacheOrder.test.d.ts.map +1 -0
- package/dist/__tests__/wordCacheOrder.test.js +352 -0
- package/dist/__tests__/wordCacheOrder.test.js.map +1 -0
- package/dist/__tests__/wordPieces.test.d.ts +2 -0
- package/dist/__tests__/wordPieces.test.d.ts.map +1 -0
- package/dist/__tests__/wordPieces.test.js +762 -0
- package/dist/__tests__/wordPieces.test.js.map +1 -0
- package/dist/bidi.d.ts +12 -0
- package/dist/bidi.d.ts.map +1 -1
- package/dist/bidi.js +23 -0
- package/dist/bidi.js.map +1 -1
- package/dist/bundle/configVersion.d.ts +49 -23
- package/dist/bundle/configVersion.d.ts.map +1 -1
- package/dist/bundle/configVersion.js +54 -25
- package/dist/bundle/configVersion.js.map +1 -1
- package/dist/canvas-backend/blockRender.d.ts.map +1 -1
- package/dist/canvas-backend/blockRender.js +58 -27
- package/dist/canvas-backend/blockRender.js.map +1 -1
- package/dist/canvas-backend/cellText.d.ts +12 -0
- package/dist/canvas-backend/cellText.d.ts.map +1 -0
- package/dist/canvas-backend/cellText.js +114 -0
- package/dist/canvas-backend/cellText.js.map +1 -0
- package/dist/canvas-backend/chip.d.ts.map +1 -1
- package/dist/canvas-backend/chip.js +2 -1
- package/dist/canvas-backend/chip.js.map +1 -1
- package/dist/canvas-backend/contextState.d.ts +7 -0
- package/dist/canvas-backend/contextState.d.ts.map +1 -1
- package/dist/canvas-backend/contextState.js +7 -0
- package/dist/canvas-backend/contextState.js.map +1 -1
- package/dist/canvas-backend/contextText.d.ts +76 -0
- package/dist/canvas-backend/contextText.d.ts.map +1 -0
- package/dist/canvas-backend/contextText.js +102 -0
- package/dist/canvas-backend/contextText.js.map +1 -0
- package/dist/canvas-backend/headerFooter.d.ts.map +1 -1
- package/dist/canvas-backend/headerFooter.js +8 -1
- package/dist/canvas-backend/headerFooter.js.map +1 -1
- package/dist/canvas-backend/index.d.ts.map +1 -1
- package/dist/canvas-backend/index.js +19 -13
- package/dist/canvas-backend/index.js.map +1 -1
- package/dist/canvas-backend/renderResourceBlock.d.ts.map +1 -1
- package/dist/canvas-backend/renderResourceBlock.js +12 -6
- package/dist/canvas-backend/renderResourceBlock.js.map +1 -1
- package/dist/canvas-backend/scriptText.d.ts.map +1 -1
- package/dist/canvas-backend/scriptText.js +3 -2
- package/dist/canvas-backend/scriptText.js.map +1 -1
- package/dist/canvas-backend/segmentText.d.ts.map +1 -1
- package/dist/canvas-backend/segmentText.js +8 -2
- package/dist/canvas-backend/segmentText.js.map +1 -1
- package/dist/canvas-backend/trackedText.d.ts +75 -1
- package/dist/canvas-backend/trackedText.d.ts.map +1 -1
- package/dist/canvas-backend/trackedText.js +185 -22
- package/dist/canvas-backend/trackedText.js.map +1 -1
- package/dist/canvas-backend/verticalText.d.ts +9 -0
- package/dist/canvas-backend/verticalText.d.ts.map +1 -1
- package/dist/canvas-backend/verticalText.js +29 -7
- package/dist/canvas-backend/verticalText.js.map +1 -1
- package/dist/canvas-backend/wordPieces.d.ts +31 -0
- package/dist/canvas-backend/wordPieces.d.ts.map +1 -0
- package/dist/canvas-backend/wordPieces.js +132 -0
- package/dist/canvas-backend/wordPieces.js.map +1 -0
- package/dist/canvas-backend/wordRuns.d.ts +3 -1
- package/dist/canvas-backend/wordRuns.d.ts.map +1 -1
- package/dist/canvas-backend/wordRuns.js +10 -6
- package/dist/canvas-backend/wordRuns.js.map +1 -1
- package/dist/comics/lettering/__tests__/fitAsPainted.test.d.ts +2 -0
- package/dist/comics/lettering/__tests__/fitAsPainted.test.d.ts.map +1 -0
- package/dist/comics/lettering/__tests__/fitAsPainted.test.js +97 -0
- package/dist/comics/lettering/__tests__/fitAsPainted.test.js.map +1 -0
- package/dist/comics/lettering/shape-text.d.ts.map +1 -1
- package/dist/comics/lettering/shape-text.js +39 -11
- package/dist/comics/lettering/shape-text.js.map +1 -1
- package/dist/defaults/bodyText.d.ts.map +1 -1
- package/dist/defaults/bodyText.js +6 -5
- package/dist/defaults/bodyText.js.map +1 -1
- package/dist/design/bidiText.d.ts +45 -6
- package/dist/design/bidiText.d.ts.map +1 -1
- package/dist/design/bidiText.js +78 -10
- package/dist/design/bidiText.js.map +1 -1
- package/dist/design/cjkText.d.ts +3 -0
- package/dist/design/cjkText.d.ts.map +1 -1
- package/dist/design/cjkText.js +1 -1
- package/dist/design/cjkText.js.map +1 -1
- package/dist/design/layout.d.ts.map +1 -1
- package/dist/design/layout.js +36 -5
- package/dist/design/layout.js.map +1 -1
- package/dist/design/richText.d.ts +9 -1
- package/dist/design/richText.d.ts.map +1 -1
- package/dist/design/richText.js +8 -2
- package/dist/design/richText.js.map +1 -1
- package/dist/fonts/usage.d.ts +2 -0
- package/dist/fonts/usage.d.ts.map +1 -1
- package/dist/fonts/usage.js +18 -0
- package/dist/fonts/usage.js.map +1 -1
- package/dist/html-backend.d.ts.map +1 -1
- package/dist/html-backend.js +664 -84
- package/dist/html-backend.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/knuthPlass/breakpoints.d.ts.map +1 -1
- package/dist/knuthPlass/breakpoints.js +4 -2
- package/dist/knuthPlass/breakpoints.js.map +1 -1
- package/dist/knuthPlass/pretextAdapter.d.ts +6 -3
- package/dist/knuthPlass/pretextAdapter.d.ts.map +1 -1
- package/dist/knuthPlass/pretextAdapter.js +11 -3
- package/dist/knuthPlass/pretextAdapter.js.map +1 -1
- package/dist/knuthPlass/richAdapter.d.ts +13 -1
- package/dist/knuthPlass/richAdapter.d.ts.map +1 -1
- package/dist/knuthPlass/richAdapter.js +53 -5
- package/dist/knuthPlass/richAdapter.js.map +1 -1
- package/dist/knuthPlass/tracking.d.ts.map +1 -1
- package/dist/knuthPlass/tracking.js +3 -2
- package/dist/knuthPlass/tracking.js.map +1 -1
- package/dist/knuthPlass/types.d.ts +6 -0
- package/dist/knuthPlass/types.d.ts.map +1 -1
- package/dist/locale.d.ts +21 -17
- package/dist/locale.d.ts.map +1 -1
- package/dist/locale.js +23 -19
- package/dist/locale.js.map +1 -1
- package/dist/measure/canvas.d.ts +21 -0
- package/dist/measure/canvas.d.ts.map +1 -1
- package/dist/measure/canvas.js +165 -85
- package/dist/measure/canvas.js.map +1 -1
- package/dist/measure/cjkCompose.d.ts.map +1 -1
- package/dist/measure/cjkCompose.js +31 -5
- package/dist/measure/cjkCompose.js.map +1 -1
- package/dist/measure/plain.d.ts.map +1 -1
- package/dist/measure/plain.js +31 -6
- package/dist/measure/plain.js.map +1 -1
- package/dist/measure/pretextLanguage.d.ts.map +1 -1
- package/dist/measure/pretextLanguage.js +49 -7
- package/dist/measure/pretextLanguage.js.map +1 -1
- package/dist/measure/rich.d.ts +18 -1
- package/dist/measure/rich.d.ts.map +1 -1
- package/dist/measure/rich.js +200 -29
- package/dist/measure/rich.js.map +1 -1
- package/dist/measure/script.d.ts +17 -1
- package/dist/measure/script.d.ts.map +1 -1
- package/dist/measure/script.js +17 -1
- package/dist/measure/script.js.map +1 -1
- package/dist/measure/trackedLigatures.d.ts +59 -0
- package/dist/measure/trackedLigatures.d.ts.map +1 -0
- package/dist/measure/trackedLigatures.js +144 -0
- package/dist/measure/trackedLigatures.js.map +1 -0
- package/dist/pipeline/build.d.ts.map +1 -1
- package/dist/pipeline/build.js +4 -3
- package/dist/pipeline/build.js.map +1 -1
- package/dist/pipeline/calloutLayout.d.ts.map +1 -1
- package/dist/pipeline/calloutLayout.js +26 -7
- package/dist/pipeline/calloutLayout.js.map +1 -1
- package/dist/pipeline/lists.d.ts +13 -4
- package/dist/pipeline/lists.d.ts.map +1 -1
- package/dist/pipeline/lists.js +49 -12
- package/dist/pipeline/lists.js.map +1 -1
- package/dist/pipeline/toc.d.ts.map +1 -1
- package/dist/pipeline/toc.js +33 -9
- package/dist/pipeline/toc.js.map +1 -1
- package/dist/types.d.ts +35 -26
- package/dist/types.d.ts.map +1 -1
- package/dist/wordPieces.d.ts +120 -0
- package/dist/wordPieces.d.ts.map +1 -0
- package/dist/wordPieces.js +319 -0
- package/dist/wordPieces.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,762 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { renderPageToCanvas } from '../canvas-backend';
|
|
3
|
+
import { renderToHtml } from '../html-backend';
|
|
4
|
+
import { buildDocument } from '../pipeline';
|
|
5
|
+
import { clearMeasurementCache } from '../measure';
|
|
6
|
+
import { joinedWordPieces, lineWordSegments, lineWordSegmentsInOrder } from '../wordPieces';
|
|
7
|
+
import * as engine from '../index';
|
|
8
|
+
// #770: the pieces of a word. A paragraph with a style in it is cut word
|
|
9
|
+
// by word, and a piece of a word is as wide as the word grew by it; a
|
|
10
|
+
// plain paragraph's pieces are each as wide as they are alone. Painted one
|
|
11
|
+
// by one, the pieces of the first kind show a hole or an overlap wherever
|
|
12
|
+
// the face draws across a cut: a ligature, a kerned pair, the dot of a
|
|
13
|
+
// Catalan `l·l`. The canvas, the HTML and the PDF set such pieces as one
|
|
14
|
+
// text, by one rule (`wordPieces.ts`).
|
|
15
|
+
/** The language of the page that hosts the layout and the canvas: what a
|
|
16
|
+
* context left at `inherit` measures and paints in. */
|
|
17
|
+
let host = 'en';
|
|
18
|
+
const primary = (lang) => lang.toLowerCase().split('-')[0];
|
|
19
|
+
const sizeOf = (font) => parseFloat(/(\d*\.?\d+)px/.exec(font)?.[1] ?? '10');
|
|
20
|
+
/**
|
|
21
|
+
* The advance of `text` in a face that draws across its letters, as the
|
|
22
|
+
* measured ones do: every letter half an em; the middle dot 0.6 em between
|
|
23
|
+
* two `l` in Catalan and 0.25 em anywhere else (Crimson Pro); two `f`
|
|
24
|
+
* side by side a ligature 0.2 em narrower; `y` and `o` kerned 0.1 em; a
|
|
25
|
+
* hyphen after `e` kerned 0.06 em.
|
|
26
|
+
*/
|
|
27
|
+
function faceWidth(text, font, lang) {
|
|
28
|
+
const catalan = primary(lang) === 'ca';
|
|
29
|
+
const chars = Array.from(text);
|
|
30
|
+
let ems = 0;
|
|
31
|
+
chars.forEach((ch, i) => {
|
|
32
|
+
const prev = chars[i - 1];
|
|
33
|
+
const next = chars[i + 1];
|
|
34
|
+
if (ch === ' ' || ch === '\u00A0')
|
|
35
|
+
ems += 0.25;
|
|
36
|
+
else if (ch === '\u00B7')
|
|
37
|
+
ems += catalan && prev === 'l' && next === 'l' ? 0.6 : 0.25;
|
|
38
|
+
else
|
|
39
|
+
ems += 0.5;
|
|
40
|
+
const pair = `${prev ?? ''}${ch}`;
|
|
41
|
+
if (pair === 'ff')
|
|
42
|
+
ems -= 0.2;
|
|
43
|
+
if (pair === 'yo')
|
|
44
|
+
ems -= 0.1;
|
|
45
|
+
if (pair === 'e-')
|
|
46
|
+
ems -= 0.06;
|
|
47
|
+
});
|
|
48
|
+
return ems * sizeOf(font);
|
|
49
|
+
}
|
|
50
|
+
const inLanguage = (lang) => (lang === 'inherit' ? host : lang);
|
|
51
|
+
/** The measuring context of the layout: an `OffscreenCanvas` of the page. */
|
|
52
|
+
class MeasureContext {
|
|
53
|
+
shorthand = '10px Punt';
|
|
54
|
+
lang = 'inherit';
|
|
55
|
+
letterSpacing = '0px';
|
|
56
|
+
get font() {
|
|
57
|
+
return this.shorthand;
|
|
58
|
+
}
|
|
59
|
+
set font(value) {
|
|
60
|
+
this.shorthand = value;
|
|
61
|
+
}
|
|
62
|
+
measureText(s) {
|
|
63
|
+
return { width: faceWidth(s, this.shorthand, inLanguage(this.lang)) };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const globals = globalThis;
|
|
67
|
+
globals.OffscreenCanvasRenderingContext2D = MeasureContext;
|
|
68
|
+
globals.OffscreenCanvas = class {
|
|
69
|
+
getContext() {
|
|
70
|
+
return new MeasureContext();
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
/** A painting context that records every text with the place, the font,
|
|
74
|
+
* the language and the letter-spacing it is painted with, and its advance
|
|
75
|
+
* in that state. `save()` keeps the state, as a canvas does. */
|
|
76
|
+
function paintContext() {
|
|
77
|
+
const painted = [];
|
|
78
|
+
const stack = [];
|
|
79
|
+
let state = { lang: 'inherit', spacing: 0, font: '10px Punt', direction: 'ltr', textAlign: 'start', textBaseline: 'alphabetic', fillStyle: '' };
|
|
80
|
+
const widthNow = (s) => faceWidth(s, state.font, inLanguage(state.lang)) + state.spacing * Array.from(s).length;
|
|
81
|
+
const api = {
|
|
82
|
+
save() {
|
|
83
|
+
stack.push(state);
|
|
84
|
+
state = { ...state };
|
|
85
|
+
},
|
|
86
|
+
restore() {
|
|
87
|
+
const saved = stack.pop();
|
|
88
|
+
if (saved)
|
|
89
|
+
state = saved;
|
|
90
|
+
},
|
|
91
|
+
measureText: (s) => ({ width: widthNow(s) }),
|
|
92
|
+
fillText(text, x, y) {
|
|
93
|
+
if (text.trim())
|
|
94
|
+
painted.push({ text, x, y, width: widthNow(text), font: state.font, lang: state.lang, spacing: state.spacing, fill: state.fillStyle });
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
const ctx = new Proxy({}, {
|
|
98
|
+
get(own, key) {
|
|
99
|
+
if (typeof key === 'string' && Object.hasOwn(own, key))
|
|
100
|
+
return own[key];
|
|
101
|
+
if (key === 'letterSpacing')
|
|
102
|
+
return `${state.spacing}px`;
|
|
103
|
+
if (typeof key === 'string' && key in api)
|
|
104
|
+
return api[key];
|
|
105
|
+
if (typeof key === 'string' && key in state)
|
|
106
|
+
return state[key];
|
|
107
|
+
return () => undefined;
|
|
108
|
+
},
|
|
109
|
+
set(own, key, value) {
|
|
110
|
+
if (key === 'letterSpacing') {
|
|
111
|
+
const m = /^(-?\d*\.?\d+(?:e[+-]?\d+)?)px$/.exec(String(value));
|
|
112
|
+
if (m)
|
|
113
|
+
state.spacing = Number(m[1]);
|
|
114
|
+
}
|
|
115
|
+
else if (typeof key === 'string' && key in state)
|
|
116
|
+
state[key] = value;
|
|
117
|
+
else if (typeof key === 'string')
|
|
118
|
+
own[key] = value;
|
|
119
|
+
return true;
|
|
120
|
+
},
|
|
121
|
+
has(_, key) {
|
|
122
|
+
return typeof key === 'string' && (key in api || key in state || key === 'letterSpacing');
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
return { ctx: ctx, painted };
|
|
126
|
+
}
|
|
127
|
+
function paint(doc) {
|
|
128
|
+
const { ctx, painted } = paintContext();
|
|
129
|
+
const canvas = { width: 0, height: 0, getContext: () => ctx };
|
|
130
|
+
renderPageToCanvas(doc.pages[0], doc, canvas);
|
|
131
|
+
return painted;
|
|
132
|
+
}
|
|
133
|
+
beforeEach(() => {
|
|
134
|
+
host = 'en';
|
|
135
|
+
clearMeasurementCache();
|
|
136
|
+
});
|
|
137
|
+
afterEach(() => {
|
|
138
|
+
delete globals.document;
|
|
139
|
+
});
|
|
140
|
+
const pt = (value) => ({ value, unit: 'pt' });
|
|
141
|
+
const OWN = { shapingLanguage: 'document' };
|
|
142
|
+
// What a document stored before postext 1.30 reads with; a configuration
|
|
143
|
+
// that names neither is shaped in its own language (#770).
|
|
144
|
+
const HOST = { shapingLanguage: 'host' };
|
|
145
|
+
function config(locale, width, extra = {}) {
|
|
146
|
+
return {
|
|
147
|
+
locale,
|
|
148
|
+
page: { width: pt(width + 40), height: pt(700), dpi: 72, margins: { top: pt(40), bottom: pt(40), left: pt(20), right: pt(20) } },
|
|
149
|
+
layout: { layoutType: 'single' },
|
|
150
|
+
header: { elements: [] },
|
|
151
|
+
footer: { elements: [] },
|
|
152
|
+
...extra,
|
|
153
|
+
bodyText: { fontFamily: 'Punt', fontSize: pt(10), lineHeight: pt(14), firstLineIndent: pt(0), textAlign: 'justify', hyphenation: { enabled: false }, ...extra.bodyText },
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
const build = (markdown, cfg, resources = []) => buildDocument({ markdown, resources }, cfg);
|
|
157
|
+
const paragraphs = (doc) => doc.blocks.filter((b) => b.type === 'paragraph');
|
|
158
|
+
const textSegments = (line) => (line.segments ?? []).filter((s) => s.kind !== 'space');
|
|
159
|
+
const round = (n) => Math.round(n * 1000) / 1000;
|
|
160
|
+
// A Catalan paragraph is cut at the dictionary's syllables (`ins|tal·|la|ció`:
|
|
161
|
+
// an author's soft hyphen beside the dot is no place to end a line).
|
|
162
|
+
const CATALAN = { hyphenation: { enabled: true, locale: 'ca' } };
|
|
163
|
+
// A paragraph with a word in italics is cut word by word.
|
|
164
|
+
const STYLED = 'La instal\u00B7laci\u00F3 de la il\u00B7luminaci\u00F3 col\u00B7lectiva *fou* una il\u00B7lusi\u00F3 paral\u00B7lela del col\u00B7legi.';
|
|
165
|
+
const PLAIN = STYLED.replace(/\*/g, '');
|
|
166
|
+
// With no dictionary the places a line may end inside a word are the
|
|
167
|
+
// author's soft hyphens: a ligature, a kerned pair and a kerned hyphen
|
|
168
|
+
// across them.
|
|
169
|
+
const SHY = '\u00AD';
|
|
170
|
+
const LATIN = `An of${SHY}fi${SHY}ci${SHY}na *was* a Toy${SHY}ota of se${SHY}da and sa${SHY}la.`;
|
|
171
|
+
/** The widths of a word's pieces as a line carries them, in ems of 10 px. */
|
|
172
|
+
const piecesOf = (doc, first, count) => {
|
|
173
|
+
for (const block of paragraphs(doc))
|
|
174
|
+
for (const line of block.lines) {
|
|
175
|
+
const segs = textSegments(line);
|
|
176
|
+
const at = segs.findIndex((s) => s.text === first);
|
|
177
|
+
if (at >= 0)
|
|
178
|
+
return segs.slice(at, at + count).map((s) => [s.text, round(s.width / 10)]);
|
|
179
|
+
}
|
|
180
|
+
return [];
|
|
181
|
+
};
|
|
182
|
+
/**
|
|
183
|
+
* Every text painted on the lines of `block` against the boxes it covers,
|
|
184
|
+
* matched by their characters: `off` holds the ones not as wide as their
|
|
185
|
+
* boxes (the piece that opens a line after a break inside a word left
|
|
186
|
+
* out: it keeps the width the word had beyond the break).
|
|
187
|
+
*/
|
|
188
|
+
function boxesAgainstInk(block, painted) {
|
|
189
|
+
const texts = [];
|
|
190
|
+
const off = [];
|
|
191
|
+
block.lines.forEach((line, li) => {
|
|
192
|
+
const calls = painted.filter((p) => Math.abs(p.y - line.baseline) < 0.01);
|
|
193
|
+
// A line painted as one text holds its word spaces.
|
|
194
|
+
const segs = line.segments ?? [];
|
|
195
|
+
let si = 0;
|
|
196
|
+
for (let ci = 0; ci < calls.length;) {
|
|
197
|
+
while (segs[si]?.kind === 'space')
|
|
198
|
+
si++;
|
|
199
|
+
let box = 0;
|
|
200
|
+
let boxText = '';
|
|
201
|
+
let ink = 0;
|
|
202
|
+
let inkText = '';
|
|
203
|
+
const opensLine = !segs.slice(0, si).some((s) => s.kind !== 'space');
|
|
204
|
+
do {
|
|
205
|
+
if (boxText.length <= inkText.length) {
|
|
206
|
+
if (si >= segs.length)
|
|
207
|
+
throw new Error(`line ${li} "${line.text}" is painted as ${calls.map((c) => c.text).join('|')}`);
|
|
208
|
+
box += segs[si].width;
|
|
209
|
+
boxText += segs[si++].text;
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
if (ci >= calls.length)
|
|
213
|
+
throw new Error(`line ${li} "${line.text}" is painted as ${calls.map((c) => c.text).join('|')}`);
|
|
214
|
+
ink += calls[ci].width;
|
|
215
|
+
inkText += calls[ci++].text;
|
|
216
|
+
}
|
|
217
|
+
} while (boxText !== inkText);
|
|
218
|
+
texts.push(inkText);
|
|
219
|
+
const tail = opensLine && li > 0 && block.lines[li - 1].hyphenated;
|
|
220
|
+
if (!tail && Math.abs(ink - box) > 0.001)
|
|
221
|
+
off.push(`${inkText}: box ${round(box)}, set ${round(ink)}`);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
return { texts, off };
|
|
225
|
+
}
|
|
226
|
+
describe('which pieces are set as one text (joinedWordPieces)', () => {
|
|
227
|
+
const width = (lang) => (text) => faceWidth(text, '10px Punt', lang);
|
|
228
|
+
/** The pieces of `word` cut at `|`, as wide as the word grew by each. */
|
|
229
|
+
const grown = (word, lang) => {
|
|
230
|
+
const parts = word.split('|');
|
|
231
|
+
let before = 0;
|
|
232
|
+
return parts.map((text, i) => {
|
|
233
|
+
const upTo = width(lang)(parts.slice(0, i + 1).join(''));
|
|
234
|
+
const piece = { text, width: upTo - before };
|
|
235
|
+
before = upTo;
|
|
236
|
+
return piece;
|
|
237
|
+
});
|
|
238
|
+
};
|
|
239
|
+
/** The same, each as wide as it is alone. */
|
|
240
|
+
const alone = (word, lang) => word.split('|').map((text) => ({ text, width: width(lang)(text) }));
|
|
241
|
+
it('the pieces of a word measured whole, where one alone is not as wide as its place', () => {
|
|
242
|
+
// The dot of `tal·` is narrow alone; `la` has the room of the wide one.
|
|
243
|
+
const catalan = grown('ins|tal\u00B7|la|ci\u00F3', 'ca');
|
|
244
|
+
expect(catalan.map((p) => round(p.width))).toEqual([15, 17.5, 13.5, 15]);
|
|
245
|
+
expect(joinedWordPieces(catalan, width('ca'))).toEqual([[0, 4]]);
|
|
246
|
+
// A ligature and a kerned pair across a cut.
|
|
247
|
+
expect(joinedWordPieces(grown('of|fi|ci|na', 'en'), width('en'))).toEqual([[0, 4]]);
|
|
248
|
+
expect(joinedWordPieces(grown('Toy|ota', 'en'), width('en'))).toEqual([[0, 2]]);
|
|
249
|
+
});
|
|
250
|
+
it('not the pieces measured one by one, nor a word with nothing across its cuts', () => {
|
|
251
|
+
expect(joinedWordPieces(alone('ins|tal\u00B7|la|ci\u00F3', 'ca'), width('ca'))).toEqual([]);
|
|
252
|
+
expect(joinedWordPieces(alone('Toy|ota', 'en'), width('en'))).toEqual([]);
|
|
253
|
+
expect(joinedWordPieces(grown('ca|sa|da', 'en'), width('en'))).toEqual([]);
|
|
254
|
+
// In another language than the one measured in the word is not as
|
|
255
|
+
// wide whole as its pieces together: they stay as they are.
|
|
256
|
+
expect(joinedWordPieces(grown('ins|tal\u00B7|la|ci\u00F3', 'ca'), width('en'))).toEqual([]);
|
|
257
|
+
expect(joinedWordPieces([{ text: 'one', width: 15 }], width('en'))).toEqual([]);
|
|
258
|
+
});
|
|
259
|
+
it('the rest of a word that opens a line is one text, off by what its first piece carries from the line before', () => {
|
|
260
|
+
// `la|ci\u00F3` opens a line after `instal-`: `la` keeps the room of the
|
|
261
|
+
// dot that stayed on the line before, 3.5 px more than it takes, and
|
|
262
|
+
// `laci\u00F3` is short of the two places by as much.
|
|
263
|
+
const tail = grown('ins|tal\u00B7|la|ci\u00F3', 'ca').slice(2);
|
|
264
|
+
expect(tail.map((p) => round(p.width))).toEqual([13.5, 15]);
|
|
265
|
+
expect(width('ca')('laci\u00F3')).toBe(25);
|
|
266
|
+
expect(joinedWordPieces(tail, width('ca'), true)).toEqual([[0, 2]]);
|
|
267
|
+
// A longer rest, with a kerned pair across its last cut.
|
|
268
|
+
const longTail = grown('pel\u00B7|l\u00ED|cu|lay|o', 'ca').slice(1);
|
|
269
|
+
expect(longTail.map((p) => round(p.width))).toEqual([13.5, 10, 15, 4]);
|
|
270
|
+
expect(joinedWordPieces(longTail, width('ca'), true)).toEqual([[0, 4]]);
|
|
271
|
+
// Not when the text is off by anything else.
|
|
272
|
+
expect(joinedWordPieces(tail, (text) => width('ca')(text) + (text.length > 3 ? 1 : 0), true)).toEqual([]);
|
|
273
|
+
// Nor from a later piece on, nor inside a line: only the piece that
|
|
274
|
+
// opens a line carries the end of the line before.
|
|
275
|
+
const later = [{ text: 'ca', width: 10 }, ...tail];
|
|
276
|
+
expect(joinedWordPieces(later, width('ca'), true)).toEqual([]);
|
|
277
|
+
expect(joinedWordPieces(tail, width('ca'))).toEqual([]);
|
|
278
|
+
});
|
|
279
|
+
it('the longest stretch that is as wide as measured: a touching word measured alone stays out', () => {
|
|
280
|
+
// `Toy|ote` and, touching it, a hyphen measured alone: the face kerns
|
|
281
|
+
// `e-`, so the three are not as wide as one text; the first two are.
|
|
282
|
+
const touching = [...grown('Toy|ote', 'en'), { text: '-', width: 5 }];
|
|
283
|
+
expect(joinedWordPieces(touching, width('en'))).toEqual([[0, 2]]);
|
|
284
|
+
});
|
|
285
|
+
it('the same width within a twentieth of a percent', () => {
|
|
286
|
+
const pieces = grown('Toy|ota', 'en');
|
|
287
|
+
const near = (text) => width('en')(text) * (text === 'Toyota' ? 1.0004 : 1);
|
|
288
|
+
const far = (text) => width('en')(text) * (text === 'Toyota' ? 1.001 : 1);
|
|
289
|
+
expect(joinedWordPieces(pieces, near)).toEqual([[0, 2]]);
|
|
290
|
+
expect(joinedWordPieces(pieces, far)).toEqual([]);
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
describe('the segments a line is set from (lineWordSegments)', () => {
|
|
294
|
+
const advance = (lang) => (seg, text) => faceWidth(text, seg.bold ? 'bold 10px Punt' : '10px Punt', seg.lang ?? lang);
|
|
295
|
+
const text = (t, width, extra = {}) => ({ kind: 'text', text: t, width, ...extra });
|
|
296
|
+
const space = { kind: 'space', text: ' ', width: 2.5 };
|
|
297
|
+
const lineOf = (segments, extra = {}) => ({
|
|
298
|
+
text: segments.map((s) => s.text).join(''), bbox: { x: 0, y: 0, width: segments.reduce((w, s) => w + s.width, 0), height: 14 }, baseline: 11, segments, ...extra,
|
|
299
|
+
});
|
|
300
|
+
const set = (line, lang = 'ca', composed = false) => lineWordSegments(line, composed, advance(lang)).map((s) => [s.text, round(s.width)]);
|
|
301
|
+
it('joins the pieces of a word in one style, and leaves the line as it is where nothing is joined', () => {
|
|
302
|
+
const line = lineOf([text('La', 10), space, text('ins', 15), text('tal\u00B7', 17.5), text('la', 13.5), text('ci\u00F3', 15), space, text('del', 15)]);
|
|
303
|
+
expect(set(line)).toEqual([['La', 10], [' ', 2.5], ['instal\u00B7laci\u00F3', 61], [' ', 2.5], ['del', 15]]);
|
|
304
|
+
// The line's own segments are not touched.
|
|
305
|
+
expect(line.segments).toHaveLength(8);
|
|
306
|
+
const plain = lineOf([text('ins', 15), text('tal\u00B7', 17.5), text('la', 10), text('ci\u00F3', 15)]);
|
|
307
|
+
expect(lineWordSegments(plain, false, advance('ca'))).toBe(plain.segments);
|
|
308
|
+
});
|
|
309
|
+
it('a joined word keeps its style, its link and its language; pieces in two styles are two words', () => {
|
|
310
|
+
const link = { href: 'https://example.org/', italic: true };
|
|
311
|
+
const linked = lineOf([text('of', 10, link), text('fi', 8, link), text('ci', 10, link), text('na', 10, link)]);
|
|
312
|
+
expect(lineWordSegments(linked, false, advance('en'))).toEqual([{ kind: 'text', text: 'officina', width: 38, ...link }]);
|
|
313
|
+
// A regular `f` and a bold one are two tokens, measured one by one: no
|
|
314
|
+
// ligature was measured, none is set.
|
|
315
|
+
const twoStyles = lineOf([text('of', 10), text('f', 5, { bold: true }), text('ice', 15)]);
|
|
316
|
+
expect(lineWordSegments(twoStyles, false, advance('en'))).toBe(twoStyles.segments);
|
|
317
|
+
// And two runs in one style with different links are not one word.
|
|
318
|
+
const twoLinks = lineOf([text('Toy', 15, { href: 'a' }), text('ota', 14, { href: 'b' })]);
|
|
319
|
+
expect(lineWordSegments(twoLinks, false, advance('en'))).toBe(twoLinks.segments);
|
|
320
|
+
// Nor two pieces in two colours, nor what stands on either side of a
|
|
321
|
+
// chip or of a note's marker.
|
|
322
|
+
const twoColours = lineOf([text('Toy', 15, { color: '#c00' }), text('ota', 14, { color: '#00c' })]);
|
|
323
|
+
expect(lineWordSegments(twoColours, false, advance('en'))).toBe(twoColours.segments);
|
|
324
|
+
const aroundChip = lineOf([text('Toy', 15), { kind: 'chip', text: 'x', width: 20 }, text('ota', 14)]);
|
|
325
|
+
expect(lineWordSegments(aroundChip, false, advance('en'))).toBe(aroundChip.segments);
|
|
326
|
+
const marker = lineOf([text('Toy', 15), text('ota', 14), text('1', 4, { footnoteId: 'a', script: 'sup', fontString: '6px Punt', baselineShift: -3 })]);
|
|
327
|
+
expect(set(marker, 'en')).toEqual([['Toyota', 29], ['1', 4]]);
|
|
328
|
+
// A run in its own language is asked for in it.
|
|
329
|
+
const run = lineOf([text('tal\u00B7', 17.5, { lang: 'ca' }), text('la', 13.5, { lang: 'ca' })]);
|
|
330
|
+
expect(lineWordSegments(run, false, advance('en'))).toEqual([{ kind: 'text', text: 'tal\u00B7la', width: 31, lang: 'ca' }]);
|
|
331
|
+
});
|
|
332
|
+
it('leaves alone a line of the CJK composer, a directed line and what carries more than a style', () => {
|
|
333
|
+
const pieces = (extra = {}) => [text('Toy', 15, extra), text('ota', 14, extra)];
|
|
334
|
+
const composed = lineOf(pieces());
|
|
335
|
+
expect(lineWordSegments(composed, true, advance('en'))).toBe(composed.segments);
|
|
336
|
+
const ordered = lineOf(pieces(), { order: [0, 1] });
|
|
337
|
+
expect(lineWordSegments(ordered, false, advance('en'))).toBe(ordered.segments);
|
|
338
|
+
for (const extra of [{ rtl: true }, { scriptContext: 'Latn' }, { refResourceId: 'fig' }, { footnoteId: 'n' }, { tracking: 1 }, { runs: [{ text: 'Toy' }] }]) {
|
|
339
|
+
const line = lineOf(pieces(extra));
|
|
340
|
+
expect(lineWordSegments(line, false, advance('en')), Object.keys(extra)[0]).toBe(line.segments);
|
|
341
|
+
}
|
|
342
|
+
// Ideographs are measured one by one.
|
|
343
|
+
const han = lineOf([text('\u6392', 10), text('\u7248', 10)]);
|
|
344
|
+
let asked = 0;
|
|
345
|
+
expect(lineWordSegments(han, false, () => (asked++, 11))).toBe(han.segments);
|
|
346
|
+
expect(asked).toBe(0);
|
|
347
|
+
});
|
|
348
|
+
it('an Arabic word is never set in pieces, nor asked about', () => {
|
|
349
|
+
const word = text('\u0643\u062A\u0627\u0628\u064B\u0627', 30, { rtl: true, runs: [{ text: '\u0643\u062A\u0627' }, { text: '\u0628\u064B\u0627', bold: true }] });
|
|
350
|
+
const line = lineOf([text('\u0630\u0647\u0628', 15, { rtl: true }), space, word], { order: [2, 1, 0] });
|
|
351
|
+
let asked = 0;
|
|
352
|
+
expect(lineWordSegments(line, false, () => (asked++, 0))).toBe(line.segments);
|
|
353
|
+
expect(asked).toBe(0);
|
|
354
|
+
// A Latin word in pieces on a line with an Arabic one stays as it is:
|
|
355
|
+
// the line's order counts its segments.
|
|
356
|
+
const mixed = lineOf([text('Toy', 15), text('ota', 14), space, text('\u0630\u0647\u0628', 15, { rtl: true })]);
|
|
357
|
+
expect(lineWordSegments(mixed, false, advance('en'))).toBe(mixed.segments);
|
|
358
|
+
});
|
|
359
|
+
it('the rest of a word that opens a line is one segment, as wide as the layout measured its pieces', () => {
|
|
360
|
+
// `la|ci\u00F3 del`: after `instal-` on the line before.
|
|
361
|
+
const tail = lineOf([text('la', 13.5), text('ci\u00F3', 15), space, text('del', 15)]);
|
|
362
|
+
expect(set(tail)).toEqual([['laci\u00F3', 28.5], [' ', 2.5], ['del', 15]]);
|
|
363
|
+
// The same pieces after another word are not the rest of anything.
|
|
364
|
+
const inside = lineOf([text('del', 15), space, text('la', 13.5), text('ci\u00F3', 15)]);
|
|
365
|
+
expect(lineWordSegments(inside, false, advance('ca'))).toBe(inside.segments);
|
|
366
|
+
});
|
|
367
|
+
it('the hyphen of a line end is set apart where the layout measured it apart', () => {
|
|
368
|
+
// `paral·le-`: the last piece is as wide as the word grew by it, plus
|
|
369
|
+
// a hyphen measured alone; the face kerns `e-`, so the word with its
|
|
370
|
+
// hyphen is not as wide as that.
|
|
371
|
+
const head = lineOf([text('pa', 10), text('ral\u00B7', 17.5), text('le-', 18.5)], { hyphenated: true });
|
|
372
|
+
expect(set(head)).toEqual([['paral\u00B7le', 41], ['-', 5]]);
|
|
373
|
+
// One piece with its hyphen.
|
|
374
|
+
const one = lineOf([text('La', 10), space, text('se-', 15)], { hyphenated: true });
|
|
375
|
+
expect(set(one)).toEqual([['La', 10], [' ', 2.5], ['se', 10], ['-', 5]]);
|
|
376
|
+
// Not where the piece with its hyphen is as wide as measured (a face
|
|
377
|
+
// that does not kern the pair, a hyphen the word carries).
|
|
378
|
+
const exact = lineOf([text('La', 10), space, text('sa-', 15)], { hyphenated: true });
|
|
379
|
+
expect(lineWordSegments(exact, false, advance('ca'))).toBe(exact.segments);
|
|
380
|
+
// Nor inside a line.
|
|
381
|
+
const inside = lineOf([text('se-', 15), space, text('La', 10)]);
|
|
382
|
+
expect(lineWordSegments(inside, false, advance('ca'))).toBe(inside.segments);
|
|
383
|
+
});
|
|
384
|
+
it('is exported by the package, for the PDF to apply the same rule', () => {
|
|
385
|
+
expect(engine.lineWordSegments).toBe(lineWordSegments);
|
|
386
|
+
expect(engine.joinedWordPieces).toBe(joinedWordPieces);
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
describe('a styled paragraph is laid out with its words measured whole, a plain one piece by piece', () => {
|
|
390
|
+
it('the Catalan dot is in the piece after it, the ligature in the second of its letters', () => {
|
|
391
|
+
const styled = build(STYLED, config('ca', 600, { bodyText: { ...OWN, ...CATALAN } }));
|
|
392
|
+
expect(piecesOf(styled, 'ins', 4)).toEqual([['ins', 1.5], ['tal\u00B7', 1.75], ['la', 1.35], ['ci\u00F3', 1.5]]);
|
|
393
|
+
const plain = build(PLAIN, config('ca', 600, { bodyText: { ...OWN, ...CATALAN } }));
|
|
394
|
+
expect(piecesOf(plain, 'ins', 4)).toEqual([['ins', 1.5], ['tal\u00B7', 1.75], ['la', 1], ['ci\u00F3', 1.5]]);
|
|
395
|
+
const latin = build(LATIN, config('en', 600, { bodyText: OWN }));
|
|
396
|
+
expect(piecesOf(latin, 'of', 4)).toEqual([['of', 1], ['fi', 0.8], ['ci', 1], ['na', 1]]);
|
|
397
|
+
expect(piecesOf(latin, 'Toy', 2)).toEqual([['Toy', 1.5], ['ota', 1.4]]);
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
describe('the canvas paints the pieces of a word measured whole as one text', () => {
|
|
401
|
+
it('one fillText for the word, where its first piece stands, as wide as its pieces together', () => {
|
|
402
|
+
const doc = build(STYLED, config('ca', 600, { bodyText: { ...OWN, ...CATALAN } }));
|
|
403
|
+
const block = paragraphs(doc)[0];
|
|
404
|
+
expect(block.lines).toHaveLength(1);
|
|
405
|
+
const painted = paint(doc);
|
|
406
|
+
const texts = painted.map((p) => p.text);
|
|
407
|
+
expect(texts.filter((t) => t === 'instal\u00B7laci\u00F3')).toHaveLength(1);
|
|
408
|
+
for (const piece of ['ins', 'tal\u00B7', 'ci\u00F3'])
|
|
409
|
+
expect(texts, piece).not.toContain(piece);
|
|
410
|
+
// No dot is painted without the `l` after it.
|
|
411
|
+
expect(texts.filter((t) => t.endsWith('\u00B7'))).toEqual([]);
|
|
412
|
+
// `La`, a word space, then the word: at the place of its first piece.
|
|
413
|
+
const line = block.lines[0];
|
|
414
|
+
const word = painted.find((p) => p.text === 'instal\u00B7laci\u00F3');
|
|
415
|
+
expect(round(word.x)).toBe(round(line.bbox.x + line.segments[0].width + line.segments[1].width));
|
|
416
|
+
expect(round(word.width)).toBe(61);
|
|
417
|
+
expect(word.lang).toBe('ca');
|
|
418
|
+
// And the next word where the layout put it.
|
|
419
|
+
const next = painted[painted.indexOf(word) + 1];
|
|
420
|
+
expect(next.text).toBe('de');
|
|
421
|
+
expect(round(next.x)).toBe(round(word.x + 61 + line.segments[6].width));
|
|
422
|
+
expect(boxesAgainstInk(block, painted).off).toEqual([]);
|
|
423
|
+
});
|
|
424
|
+
it('a ligature and a kerned pair across a cut are painted, a word with nothing across its cuts stays in pieces', () => {
|
|
425
|
+
const doc = build(LATIN, config('en', 600, { bodyText: OWN }));
|
|
426
|
+
const painted = paint(doc);
|
|
427
|
+
const texts = painted.map((p) => p.text);
|
|
428
|
+
expect(texts).toContain('officina');
|
|
429
|
+
expect(texts).toContain('Toyota');
|
|
430
|
+
expect(texts).not.toContain('fi');
|
|
431
|
+
expect(texts).toContain('sa');
|
|
432
|
+
expect(texts).toContain('la.');
|
|
433
|
+
expect(round(painted.find((p) => p.text === 'officina').width)).toBe(38);
|
|
434
|
+
expect(boxesAgainstInk(paragraphs(doc)[0], painted).off).toEqual([]);
|
|
435
|
+
});
|
|
436
|
+
it('a document shaped in the host\'s language on a Catalan page too', () => {
|
|
437
|
+
host = 'ca';
|
|
438
|
+
const doc = build(STYLED, config('ca', 600, { bodyText: { ...HOST, ...CATALAN } }));
|
|
439
|
+
expect(engine.shapingLanguageOf(doc.config)).toBeUndefined();
|
|
440
|
+
expect(piecesOf(doc, 'ins', 4)).toEqual([['ins', 1.5], ['tal\u00B7', 1.75], ['la', 1.35], ['ci\u00F3', 1.5]]);
|
|
441
|
+
const painted = paint(doc);
|
|
442
|
+
expect(painted.filter((p) => p.text === 'instal\u00B7laci\u00F3').map((p) => [p.lang, round(p.width)])).toEqual([['inherit', 61]]);
|
|
443
|
+
expect(boxesAgainstInk(paragraphs(doc)[0], painted).off).toEqual([]);
|
|
444
|
+
// On a page in another language the layout measured the narrow dot,
|
|
445
|
+
// and the pieces are as wide alone as their places: nothing to join.
|
|
446
|
+
host = 'en';
|
|
447
|
+
clearMeasurementCache();
|
|
448
|
+
const english = build(STYLED, config('ca', 600, { bodyText: { ...HOST, ...CATALAN } }));
|
|
449
|
+
const texts = paint(english).map((p) => p.text);
|
|
450
|
+
expect(texts).toContain('tal\u00B7');
|
|
451
|
+
expect(boxesAgainstInk(paragraphs(english)[0], paint(english)).off).toEqual([]);
|
|
452
|
+
});
|
|
453
|
+
it('a plain paragraph, measured piece by piece, is painted piece by piece', () => {
|
|
454
|
+
const doc = build(PLAIN, config('ca', 200, { bodyText: { ...OWN, ...CATALAN } }));
|
|
455
|
+
const painted = paint(doc);
|
|
456
|
+
const block = paragraphs(doc)[0];
|
|
457
|
+
// A justified line of it with a dot at a cut: every piece at its place.
|
|
458
|
+
const justified = block.lines.find((l) => !l.isLastLine && textSegments(l).some((s) => s.text.endsWith('\u00B7')));
|
|
459
|
+
expect(justified).toBeDefined();
|
|
460
|
+
const onLine = painted.filter((p) => Math.abs(p.y - justified.baseline) < 0.01).map((p) => p.text);
|
|
461
|
+
expect(onLine.join('')).toBe(textSegments(justified).map((s) => s.text).join(''));
|
|
462
|
+
expect(onLine.filter((t) => t.includes('l\u00B7l'))).toEqual([]);
|
|
463
|
+
expect(onLine.some((t) => t.endsWith('\u00B7'))).toBe(true);
|
|
464
|
+
expect(boxesAgainstInk(block, painted).off).toEqual([]);
|
|
465
|
+
});
|
|
466
|
+
it('a ragged line is one text where that text is as wide as its words were measured, and its pieces where it is not', () => {
|
|
467
|
+
// A last line in one style with a kerned pair across a cut, measured
|
|
468
|
+
// piece by piece: as one text it would be 1 px short.
|
|
469
|
+
const kerned = build(`Una casa de sa${SHY}la i un Toy${SHY}ota.`, config('en', 400, { bodyText: { ...OWN, textAlign: 'left' } }));
|
|
470
|
+
const line = paragraphs(kerned)[0].lines[0];
|
|
471
|
+
expect(textSegments(line).map((s) => s.text)).toContain('Toy');
|
|
472
|
+
const pieces = paint(kerned);
|
|
473
|
+
expect(pieces.map((p) => p.text)).toEqual(textSegments(line).map((s) => s.text));
|
|
474
|
+
expect(boxesAgainstInk(paragraphs(kerned)[0], pieces).off).toEqual([]);
|
|
475
|
+
// Nothing across its cuts: one fillText, as before.
|
|
476
|
+
const even = build(`Una casa de sa${SHY}la i un ca${SHY}mi.`, config('en', 400, { bodyText: { ...OWN, textAlign: 'left' } }));
|
|
477
|
+
expect(paint(even).map((p) => p.text)).toEqual(['Una casa de sala i un cami.']);
|
|
478
|
+
});
|
|
479
|
+
it('a ragged line that ends on an added hyphen with no two pieces touching: the hyphen apart, as measured', () => {
|
|
480
|
+
// A word wider than the measure is cut at its soft hyphen and `se-`
|
|
481
|
+
// ends the first line: the face kerns the hyphen 0.6 px into the `e`,
|
|
482
|
+
// and the layout measured the two apart. As one text the line was
|
|
483
|
+
// 0.6 px short of its boxes.
|
|
484
|
+
for (const [mode, bodyText] of [['document', OWN], ['host', HOST]]) {
|
|
485
|
+
clearMeasurementCache();
|
|
486
|
+
const doc = build(`Una casa de se${SHY}dadadadadadada.`, config('en', 70, { bodyText: { ...bodyText, textAlign: 'left' } }));
|
|
487
|
+
const block = paragraphs(doc)[0];
|
|
488
|
+
const line = block.lines[0];
|
|
489
|
+
expect(line.hyphenated, mode).toBe(true);
|
|
490
|
+
expect(textSegments(line).map((s) => s.text), mode).toEqual(['Una', 'casa', 'de', 'se-']);
|
|
491
|
+
const pieces = paint(doc).filter((p) => Math.abs(p.y - line.baseline) < 0.01);
|
|
492
|
+
expect(pieces.map((p) => p.text), mode).toEqual(['Una', 'casa', 'de', 'se', '-']);
|
|
493
|
+
expect(boxesAgainstInk(block, paint(doc)).off, mode).toEqual([]);
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
it('a paragraph broken inside its words: every text as wide as its boxes, the hyphen where it was measured', () => {
|
|
497
|
+
let broken = 0;
|
|
498
|
+
let hyphens = 0;
|
|
499
|
+
let rests = 0;
|
|
500
|
+
const paragraphsOf = [
|
|
501
|
+
[`${STYLED} `.repeat(3), 'ca', { ...OWN, ...CATALAN }],
|
|
502
|
+
[`${LATIN} `.repeat(4), 'en', OWN],
|
|
503
|
+
];
|
|
504
|
+
for (const [markdown, locale, bodyText] of paragraphsOf) {
|
|
505
|
+
for (const measure of [150, 158, 166, 174, 182, 190, 198, 206, 214, 222]) {
|
|
506
|
+
clearMeasurementCache();
|
|
507
|
+
const doc = build(markdown, config(locale, measure, { bodyText }));
|
|
508
|
+
const block = paragraphs(doc)[0];
|
|
509
|
+
const painted = paint(doc);
|
|
510
|
+
const { texts, off } = boxesAgainstInk(block, painted);
|
|
511
|
+
expect(off, `${locale} at ${measure}`).toEqual([]);
|
|
512
|
+
broken += block.lines.filter((l) => l.hyphenated).length;
|
|
513
|
+
hyphens += painted.filter((p) => p.text === '-').length;
|
|
514
|
+
// The rest of a broken word, in several pieces, as one text.
|
|
515
|
+
block.lines.forEach((line, li) => {
|
|
516
|
+
const first = painted.find((p) => Math.abs(p.y - line.baseline) < 0.01);
|
|
517
|
+
const pieces = textSegments(line);
|
|
518
|
+
if (li > 0 && block.lines[li - 1].hyphenated && first && pieces.length > 1 && first.text === pieces[0].text + pieces[1].text)
|
|
519
|
+
rests++;
|
|
520
|
+
});
|
|
521
|
+
// No dot is painted without the `l` after it.
|
|
522
|
+
expect(texts.filter((t) => t.endsWith('\u00B7')), `${locale} at ${measure}`).toEqual([]);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
expect(broken).toBeGreaterThan(10);
|
|
526
|
+
expect(rests).toBeGreaterThan(0);
|
|
527
|
+
// Some line ended on a hyphen the face kerns: set apart, as measured.
|
|
528
|
+
expect(hyphens).toBeGreaterThan(0);
|
|
529
|
+
});
|
|
530
|
+
it('a tracked line: the word once, with the tracking it was measured with', () => {
|
|
531
|
+
// Lines whose word spaces alone would be too wide take tracking.
|
|
532
|
+
const doc = build(`${LATIN} `.repeat(4), config('en', 236, { bodyText: { ...OWN, maxWordSpacing: 1.02, minWordSpacing: 0.98, maxJustifyTracking: 80 } }));
|
|
533
|
+
const block = paragraphs(doc)[0];
|
|
534
|
+
const tracked = block.lines.filter((l) => (l.letterSpacing ?? 0) !== 0);
|
|
535
|
+
expect(tracked.length).toBeGreaterThan(0);
|
|
536
|
+
const painted = paint(doc);
|
|
537
|
+
const onTracked = painted.filter((p) => tracked.some((l) => Math.abs(p.y - l.baseline) < 0.01));
|
|
538
|
+
expect(onTracked.some((p) => p.spacing !== 0 && /^(officina|Toyota)/.test(p.text))).toBe(true);
|
|
539
|
+
expect(onTracked.map((p) => p.text)).not.toContain('fi');
|
|
540
|
+
expect(boxesAgainstInk(block, painted).off).toEqual([]);
|
|
541
|
+
});
|
|
542
|
+
it('a caption and a table cell too', () => {
|
|
543
|
+
const table = {
|
|
544
|
+
id: 't', typeId: 'table', kind: 'table', createdAt: 0, updatedAt: 0, placement: { position: 'here' },
|
|
545
|
+
caption: 'La instal\u00B7laci\u00F3 *fou* una il\u00B7lusi\u00F3.',
|
|
546
|
+
table: { model: { headerRowCount: 1, rows: [[{ content: 'Sa' }, { content: 'Lo' }], [{ content: 'Una **casa** i la instal\u00B7laci\u00F3 del sal\u00F3.' }, { content: 'Da' }]] } },
|
|
547
|
+
};
|
|
548
|
+
const doc = build('Una casa.\n\n::resource{id="t"}\n', config('ca', 400, { bodyText: { ...OWN, ...CATALAN } }), [table]);
|
|
549
|
+
// These lines carry their words whole; a caption or a cell cut at its
|
|
550
|
+
// syllables carries them as the body does. Put the pieces in.
|
|
551
|
+
const resource = doc.pages[0].columns[0].blocks.find((b) => b.resourceBlock).resourceBlock;
|
|
552
|
+
const cut = (line) => {
|
|
553
|
+
const at = line.segments.findIndex((s) => s.text === 'instal\u00B7laci\u00F3');
|
|
554
|
+
expect(at).toBeGreaterThan(0);
|
|
555
|
+
line.segments = [...line.segments.slice(0, at), ...[['ins', 15], ['tal\u00B7', 17.5], ['la', 13.5], ['ci\u00F3', 15]].map(([text, width]) => ({ kind: 'text', text: text, width: width })), ...line.segments.slice(at + 1)];
|
|
556
|
+
};
|
|
557
|
+
cut(resource.captionLines[0]);
|
|
558
|
+
cut(resource.table.cells[2].lines[0]);
|
|
559
|
+
const painted = paint(doc);
|
|
560
|
+
expect(painted.filter((p) => p.text === 'instal\u00B7laci\u00F3').map((p) => round(p.width))).toEqual([61, 61]);
|
|
561
|
+
expect(painted.map((p) => p.text)).not.toContain('tal\u00B7');
|
|
562
|
+
const html = renderToHtml(doc);
|
|
563
|
+
expect(html.match(/>instal\u00B7laci\u00F3</g)).toHaveLength(2);
|
|
564
|
+
expect(html).not.toContain('>tal\u00B7<');
|
|
565
|
+
});
|
|
566
|
+
});
|
|
567
|
+
describe('the HTML sets the pieces of a word measured whole in one box', () => {
|
|
568
|
+
const boxes = (html) => [...html.matchAll(/<span[^>]*position:absolute[^>]*>(?:<span[^>]*>|<a[^>]*>)?([^<]+)</g)].map((m) => m[1]);
|
|
569
|
+
it('one positioned span for the word, at the place of its first piece', () => {
|
|
570
|
+
const doc = build(STYLED, config('ca', 600, { bodyText: { ...OWN, ...CATALAN } }));
|
|
571
|
+
const html = renderToHtml(doc);
|
|
572
|
+
const texts = boxes(html);
|
|
573
|
+
expect(texts.filter((t) => t === 'instal\u00B7laci\u00F3')).toHaveLength(1);
|
|
574
|
+
for (const piece of ['ins', 'tal\u00B7', 'ci\u00F3'])
|
|
575
|
+
expect(texts, piece).not.toContain(piece);
|
|
576
|
+
expect(texts.filter((t) => t.endsWith('\u00B7'))).toEqual([]);
|
|
577
|
+
const line = paragraphs(doc)[0].lines[0];
|
|
578
|
+
const left = (line.segments[0].width + line.segments[1].width).toFixed(3);
|
|
579
|
+
expect(html).toContain(`<span style="position:absolute;left:${left}px;top:0;white-space:pre;">instal\u00B7laci\u00F3</span>`);
|
|
580
|
+
// The word in italics is one box as before, and the word spaces are
|
|
581
|
+
// still text between the words.
|
|
582
|
+
expect(html).toMatch(/font:italic[^>]*>fou</);
|
|
583
|
+
expect(texts.filter((t) => t === ' ').length).toBeGreaterThanOrEqual(line.segments.filter((s) => s.kind === 'space').length);
|
|
584
|
+
// A ligature and a kerned pair across a cut.
|
|
585
|
+
const latin = boxes(renderToHtml(build(LATIN, config('en', 600, { bodyText: OWN }))));
|
|
586
|
+
expect(latin).toContain('officina');
|
|
587
|
+
expect(latin).toContain('Toyota');
|
|
588
|
+
expect(latin).not.toContain('fi');
|
|
589
|
+
});
|
|
590
|
+
it('a plain paragraph keeps a box per piece', () => {
|
|
591
|
+
const texts = boxes(renderToHtml(build(PLAIN, config('ca', 600, { bodyText: { ...OWN, ...CATALAN } }))));
|
|
592
|
+
for (const piece of ['ins', 'tal\u00B7', 'ci\u00F3'])
|
|
593
|
+
expect(texts, piece).toContain(piece);
|
|
594
|
+
expect(texts).not.toContain('instal\u00B7laci\u00F3');
|
|
595
|
+
});
|
|
596
|
+
it('a link holds its word whole: one anchor around one box', () => {
|
|
597
|
+
const doc = build('Una **casa** i la [instal\u00B7laci\u00F3 del col\u00B7legi](https://example.org/) del sal\u00F3.', config('ca', 600, { bodyText: { ...OWN, ...CATALAN } }));
|
|
598
|
+
const html = renderToHtml(doc);
|
|
599
|
+
const anchor = /<a href="https:\/\/example\.org\/"[^>]*>(.*?)<\/a>/.exec(html)?.[1] ?? '';
|
|
600
|
+
expect(boxes(anchor).filter((t) => t.trim())).toEqual(['instal\u00B7laci\u00F3', 'del', 'col\u00B7legi']);
|
|
601
|
+
// The link's segments are the line's own: its pieces keep their boxes
|
|
602
|
+
// for whoever reads the layout.
|
|
603
|
+
const linked = paragraphs(doc)[0].lines.flatMap((l) => l.segments).filter((s) => s.href !== undefined && s.kind === 'text');
|
|
604
|
+
expect(linked.map((s) => s.text).slice(0, 4)).toEqual(['ins', 'tal\u00B7', 'la', 'ci\u00F3']);
|
|
605
|
+
expect(linked.map((s) => s.text).join('')).toBe('instal\u00B7laci\u00F3delcol\u00B7legi');
|
|
606
|
+
// On the canvas the word is one text in the link's colour.
|
|
607
|
+
const painted = paint(doc).filter((p) => p.text === 'instal\u00B7laci\u00F3');
|
|
608
|
+
expect(painted).toHaveLength(1);
|
|
609
|
+
});
|
|
610
|
+
it('a document shaped in the host\'s language: on its page, not where no page is known', () => {
|
|
611
|
+
host = 'ca';
|
|
612
|
+
const doc = build(STYLED, config('ca', 600, { bodyText: { ...HOST, ...CATALAN } }));
|
|
613
|
+
// In a worker nobody knows the language of the page that will show
|
|
614
|
+
// the text: its pieces stay as they were.
|
|
615
|
+
expect(boxes(renderToHtml(doc))).toContain('tal\u00B7');
|
|
616
|
+
// On the page that lays it out and shows it.
|
|
617
|
+
globals.document = {};
|
|
618
|
+
const texts = boxes(renderToHtml(doc));
|
|
619
|
+
expect(texts.filter((t) => t === 'instal\u00B7laci\u00F3')).toHaveLength(1);
|
|
620
|
+
expect(texts).not.toContain('tal\u00B7');
|
|
621
|
+
});
|
|
622
|
+
it('an Arabic paragraph is written as it was', () => {
|
|
623
|
+
const arabic = '\u0630\u0647\u0628 *\u0627\u0644\u0648\u0644\u062F* \u0625\u0644\u0649 \u0627\u0644\u0645\u062F\u0631\u0633\u0629 \u0648\u0643\u062A\u0628 \u0643\u062A\u0627**\u0628**\u064B\u0627 \u0639\u0646 \u0627\u0644\u0639\u0631\u0628\u064A\u0651\u0629.';
|
|
624
|
+
const doc = build(arabic, config('ar', 160, { direction: 'rtl', bodyText: OWN }));
|
|
625
|
+
const segments = paragraphs(doc)[0].lines.flatMap((l) => l.segments);
|
|
626
|
+
expect(segments.some((s) => s.runs !== undefined)).toBe(true);
|
|
627
|
+
// One box per word of the layout, in its order; the word in two styles
|
|
628
|
+
// one shaped run that opens with its first letters.
|
|
629
|
+
const words = segments.filter((s) => s.kind === 'text');
|
|
630
|
+
const texts = boxes(renderToHtml(doc)).filter((t) => t.trim());
|
|
631
|
+
expect(texts).toHaveLength(words.length);
|
|
632
|
+
words.forEach((word, i) => expect(word.text.startsWith(texts[i]), word.text).toBe(true));
|
|
633
|
+
const painted = paint(doc).map((p) => p.text);
|
|
634
|
+
for (const word of words)
|
|
635
|
+
expect(painted, word.text).toContain(word.text);
|
|
636
|
+
});
|
|
637
|
+
});
|
|
638
|
+
describe('a line that carries an order (lineWordSegmentsInOrder)', () => {
|
|
639
|
+
const advance = (seg, text) => faceWidth(text, '10px Punt', seg.lang ?? 'en');
|
|
640
|
+
const text = (t, width, extra = {}) => ({ kind: 'text', text: t, width, ...extra });
|
|
641
|
+
const space = () => ({ kind: 'space', text: ' ', width: 2.5 });
|
|
642
|
+
const lineOf = (segments, extra = {}) => ({
|
|
643
|
+
text: segments.map((s) => s.text).join(''), bbox: { x: 0, y: 0, width: segments.reduce((w, s) => w + s.width, 0), height: 14 }, baseline: 11, segments, ...extra,
|
|
644
|
+
});
|
|
645
|
+
const AR = '\u0643\u062A\u0627\u0628';
|
|
646
|
+
const texts = (segments) => segments.map((s) => [s.text, round(s.width)]);
|
|
647
|
+
it('a left-to-right line of a mirrored page: the word where its pieces stood, the order written again', () => {
|
|
648
|
+
// `Toy|ota was`, advanced from the sheet's right: the last word first.
|
|
649
|
+
const line = lineOf([text('Toy', 15), text('ota', 14), space(), text('was', 15)], { order: [3, 2, 1, 0] });
|
|
650
|
+
const set = lineWordSegmentsInOrder(line, false, advance);
|
|
651
|
+
expect(texts(set.segments)).toEqual([['Toyota', 29], [' ', 2.5], ['was', 15]]);
|
|
652
|
+
expect(set.order).toEqual([2, 1, 0]);
|
|
653
|
+
// Each segment once, at the place its pieces had along the line.
|
|
654
|
+
expect(line.segments).toHaveLength(4);
|
|
655
|
+
});
|
|
656
|
+
it('a Latin word among right-to-left words: joined, its neighbours in their places', () => {
|
|
657
|
+
const line = lineOf([text(AR, 20, { rtl: true }), space(), text('Toy', 15, { level: 2 }), text('ota', 14, { level: 2 }), space(), text(AR, 20, { rtl: true })], { order: [5, 4, 2, 3, 1, 0] });
|
|
658
|
+
const set = lineWordSegmentsInOrder(line, false, advance);
|
|
659
|
+
expect(texts(set.segments)).toEqual([[AR, 20], [' ', 2.5], ['Toyota', 29], [' ', 2.5], [AR, 20]]);
|
|
660
|
+
expect(set.order).toEqual([4, 3, 2, 1, 0]);
|
|
661
|
+
expect(set.segments[2].level).toBe(2);
|
|
662
|
+
expect(set.segments[0]).toBe(line.segments[0]);
|
|
663
|
+
});
|
|
664
|
+
it('pieces that do not stand side by side along the line, or at two levels, stay apart', () => {
|
|
665
|
+
const apart = lineOf([text('Toy', 15), text('ota', 14), space(), text('was', 15)], { order: [0, 3, 2, 1] });
|
|
666
|
+
const same = lineWordSegmentsInOrder(apart, false, advance);
|
|
667
|
+
expect(same.segments).toBe(apart.segments);
|
|
668
|
+
expect(same.order).toBe(apart.order);
|
|
669
|
+
const levels = lineOf([text('Toy', 15, { level: 2 }), text('ota', 14)], { order: [0, 1] });
|
|
670
|
+
expect(lineWordSegmentsInOrder(levels, false, advance).segments).toBe(levels.segments);
|
|
671
|
+
const rtl = lineOf([text('Toy', 15, { rtl: true }), text('ota', 14, { rtl: true })], { order: [1, 0] });
|
|
672
|
+
expect(lineWordSegmentsInOrder(rtl, false, advance).segments).toBe(rtl.segments);
|
|
673
|
+
});
|
|
674
|
+
it('the hyphen of a line end, set apart: after its word the way the line reads', () => {
|
|
675
|
+
// `se-` is measured as `se` and a hyphen apart, 15 px; set as one text
|
|
676
|
+
// the face kerns the pair.
|
|
677
|
+
const mirrored = lineOf([text('the', 15), space(), text('se-', 15)], { order: [2, 1, 0], hyphenated: true });
|
|
678
|
+
const set = lineWordSegmentsInOrder(mirrored, false, advance);
|
|
679
|
+
expect(texts(set.segments)).toEqual([['the', 15], [' ', 2.5], ['se', 10], ['-', 5]]);
|
|
680
|
+
// From the sheet's right: the hyphen, the word it ends, the space, `the`.
|
|
681
|
+
expect(set.order).toEqual([3, 2, 1, 0]);
|
|
682
|
+
const forward = lineOf([text('the', 15), space(), text('se-', 15)], { order: [0, 1, 2], hyphenated: true });
|
|
683
|
+
expect(lineWordSegmentsInOrder(forward, false, advance).order).toEqual([0, 1, 2, 3]);
|
|
684
|
+
// A line that does not tell which way its Latin text runs is left as it is.
|
|
685
|
+
const alone = lineOf([text('se-', 15)], { order: [0], hyphenated: true });
|
|
686
|
+
expect(lineWordSegmentsInOrder(alone, false, advance).segments).toBe(alone.segments);
|
|
687
|
+
});
|
|
688
|
+
it('a line with no order is set as lineWordSegments sets it, a line of the composer as it is', () => {
|
|
689
|
+
const line = lineOf([text('Toy', 15), text('ota', 14)]);
|
|
690
|
+
const set = lineWordSegmentsInOrder(line, false, advance);
|
|
691
|
+
expect(texts(set.segments)).toEqual([['Toyota', 29]]);
|
|
692
|
+
expect(set.order).toBeUndefined();
|
|
693
|
+
expect(set.segments).toEqual(lineWordSegments(line, false, advance));
|
|
694
|
+
const composed = lineOf([text('Toy', 15), text('ota', 14)], { order: [0, 1] });
|
|
695
|
+
expect(lineWordSegmentsInOrder(composed, true, advance)).toEqual({ segments: composed.segments, order: composed.order });
|
|
696
|
+
// The rule the PDF applies is unchanged: a directed line as it is.
|
|
697
|
+
expect(lineWordSegments(composed, false, advance)).toBe(composed.segments);
|
|
698
|
+
});
|
|
699
|
+
it('the canvas and the HTML set a joined word of a right-to-left page once, in its place', () => {
|
|
700
|
+
// An English sentence on a right-to-left page: its line carries an
|
|
701
|
+
// order, and `Toy|ota` was measured whole (a word in italics beside it).
|
|
702
|
+
const doc = build(`A Toy${SHY}ota *was* here.`, config('ar', 600, { direction: 'rtl', bodyText: { ...OWN, textAlign: 'left' } }));
|
|
703
|
+
const line = paragraphs(doc)[0].lines[0];
|
|
704
|
+
expect(line.order).toBeDefined();
|
|
705
|
+
expect(textSegments(line).map((s) => s.text)).toEqual(expect.arrayContaining(['Toy', 'ota']));
|
|
706
|
+
const painted = paint(doc).filter((p) => /Toy|ota/.test(p.text));
|
|
707
|
+
expect(painted.map((p) => p.text)).toEqual(['Toyota']);
|
|
708
|
+
expect(painted[0].width).toBeCloseTo(29, 6);
|
|
709
|
+
globals.document = {};
|
|
710
|
+
const html = renderToHtml(doc);
|
|
711
|
+
expect(html).toContain('>Toyota<');
|
|
712
|
+
expect(html).not.toContain('>ota<');
|
|
713
|
+
});
|
|
714
|
+
});
|
|
715
|
+
describe('the HTML sets a text of several words as the canvas measured it', () => {
|
|
716
|
+
const header = { elements: [{ kind: 'text', id: 'rh', content: 'The Art of War', fontFamily: 'Punt', fontSize: pt(8), overflow: 'wrap', align: 'start', placement: { anchor: { to: 'container', edge: 'top-left' }, size: { width: 'fill' } } }] };
|
|
717
|
+
it('a line of design text: a box per word, each where the canvas puts it, in a line box as wide as the line', () => {
|
|
718
|
+
const html = renderToHtml(build('Body.', config('en', 300, { header, bodyText: OWN })));
|
|
719
|
+
const box = (x, text) => `<span style="position:absolute;left:${x.toFixed(3)}px;top:0;">${text}</span>`;
|
|
720
|
+
// Letters of 4 px and spaces of 2 px at 8 px.
|
|
721
|
+
expect(html).toContain(`line-height:1;white-space:pre;width:50.000px;">${box(0, 'The')}${box(12, ' ')}${box(14, 'Art')}${box(26, ' ')}${box(28, 'of')}${box(36, ' ')}${box(38, 'War')}</span>`);
|
|
722
|
+
// Read without its markup the head is its text.
|
|
723
|
+
expect(html.replace(/<[^>]+>/g, '')).toContain('The Art of War');
|
|
724
|
+
});
|
|
725
|
+
it('where the language of the page is not known: one box, each space in a box of its own', () => {
|
|
726
|
+
const html = renderToHtml(build('Body.', config('en', 300, { header, bodyText: HOST })));
|
|
727
|
+
const apart = '<span style="display:inline-block;"> </span>';
|
|
728
|
+
expect(html).toContain(`line-height:1;white-space:pre;">The${apart}Art${apart}of${apart}War</span>`);
|
|
729
|
+
expect(html.replace(/<[^>]+>/g, '')).toContain('The Art of War');
|
|
730
|
+
});
|
|
731
|
+
it('a word of a line is written as it was: one box, its text alone', () => {
|
|
732
|
+
const html = renderToHtml(build('Two words.', config('en', 300, { bodyText: { ...OWN, textAlign: 'left' } })));
|
|
733
|
+
expect(html).toContain('<span style="position:absolute;left:0.000px;top:0;white-space:pre;">Two</span>');
|
|
734
|
+
expect(html).not.toContain('display:inline-block');
|
|
735
|
+
expect(html).not.toContain('top:0;">');
|
|
736
|
+
});
|
|
737
|
+
});
|
|
738
|
+
describe('a doubled dash given the forms of Han text is set alone where it was measured alone', () => {
|
|
739
|
+
const DASHES = '\u2014\u2014';
|
|
740
|
+
/** A line whose first word is the pair, as the layout gives it the
|
|
741
|
+
* forms of Han text: its box `width` px wide (10: two dashes alone). */
|
|
742
|
+
const withDashes = (width = 10, shaping = OWN) => {
|
|
743
|
+
const doc = build('xx and here', config('en', 300, { bodyText: { ...shaping, textAlign: 'left' } }));
|
|
744
|
+
const seg = paragraphs(doc)[0].lines[0].segments[0];
|
|
745
|
+
Object.assign(seg, { text: DASHES, width, scriptContext: 'Hani' });
|
|
746
|
+
return doc;
|
|
747
|
+
};
|
|
748
|
+
it('no mark beside it on the page whose canvas sets it alone at that width', () => {
|
|
749
|
+
globals.document = {};
|
|
750
|
+
const html = renderToHtml(withDashes());
|
|
751
|
+
expect(html).toContain(`>${DASHES}</span>`);
|
|
752
|
+
expect(html).not.toContain('font-size:0');
|
|
753
|
+
});
|
|
754
|
+
it('the mark where the word alone is not as wide as measured, and where the language is not known', () => {
|
|
755
|
+
globals.document = {};
|
|
756
|
+
expect(renderToHtml(withDashes(13))).toContain('font-size:0');
|
|
757
|
+
delete globals.document;
|
|
758
|
+
const host = withDashes(10, HOST);
|
|
759
|
+
expect(renderToHtml(host)).toContain('font-size:0');
|
|
760
|
+
});
|
|
761
|
+
});
|
|
762
|
+
//# sourceMappingURL=wordPieces.test.js.map
|