postext-pdf 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__/canvasShaping.test.d.ts +2 -0
- package/dist/__tests__/canvasShaping.test.d.ts.map +1 -0
- package/dist/__tests__/canvasShaping.test.js +258 -0
- package/dist/__tests__/canvasShaping.test.js.map +1 -0
- package/dist/__tests__/canvasWords.test.d.ts +2 -0
- package/dist/__tests__/canvasWords.test.d.ts.map +1 -0
- package/dist/__tests__/canvasWords.test.js +66 -0
- package/dist/__tests__/canvasWords.test.js.map +1 -0
- package/dist/__tests__/declaredFiles.test.d.ts +2 -0
- package/dist/__tests__/declaredFiles.test.d.ts.map +1 -0
- package/dist/__tests__/declaredFiles.test.js +435 -0
- package/dist/__tests__/declaredFiles.test.js.map +1 -0
- package/dist/__tests__/designTextRtl.test.js +17 -6
- package/dist/__tests__/designTextRtl.test.js.map +1 -1
- package/dist/__tests__/glyphPositions.d.ts +25 -0
- package/dist/__tests__/glyphPositions.d.ts.map +1 -0
- package/dist/__tests__/glyphPositions.js +288 -0
- package/dist/__tests__/glyphPositions.js.map +1 -0
- package/dist/__tests__/japaneseShaping.test.js +3 -3
- package/dist/__tests__/japaneseShaping.test.js.map +1 -1
- package/dist/__tests__/listMarkerShaping.test.d.ts +2 -0
- package/dist/__tests__/listMarkerShaping.test.d.ts.map +1 -0
- package/dist/__tests__/listMarkerShaping.test.js +64 -0
- package/dist/__tests__/listMarkerShaping.test.js.map +1 -0
- package/dist/__tests__/pdfWorker.test.js +27 -0
- package/dist/__tests__/pdfWorker.test.js.map +1 -1
- package/dist/__tests__/scriptContextHan.test.js +3 -3
- package/dist/__tests__/scriptContextHan.test.js.map +1 -1
- package/dist/__tests__/shapingLanguageDocument.test.js +1 -1
- package/dist/__tests__/shapingLanguageDocument.test.js.map +1 -1
- package/dist/__tests__/shapingLanguageRunsDocument.test.js +158 -8
- package/dist/__tests__/shapingLanguageRunsDocument.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 +77 -0
- package/dist/__tests__/trackedLetters.test.js.map +1 -0
- package/dist/canvasWords.d.ts +55 -0
- package/dist/canvasWords.d.ts.map +1 -0
- package/dist/canvasWords.js +160 -0
- package/dist/canvasWords.js.map +1 -0
- package/dist/faceFiles.d.ts +93 -5
- package/dist/faceFiles.d.ts.map +1 -1
- package/dist/faceFiles.js +256 -10
- package/dist/faceFiles.js.map +1 -1
- package/dist/fontCache.d.ts +92 -11
- package/dist/fontCache.d.ts.map +1 -1
- package/dist/fontCache.js +82 -17
- package/dist/fontCache.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/pdf-backend/blockRender.d.ts.map +1 -1
- package/dist/pdf-backend/blockRender.js +63 -23
- package/dist/pdf-backend/blockRender.js.map +1 -1
- package/dist/pdf-backend/index.d.ts +3 -2
- package/dist/pdf-backend/index.d.ts.map +1 -1
- package/dist/pdf-backend/index.js +9 -3
- package/dist/pdf-backend/index.js.map +1 -1
- package/dist/pdf-backend/primitives.d.ts +55 -5
- package/dist/pdf-backend/primitives.d.ts.map +1 -1
- package/dist/pdf-backend/primitives.js +206 -25
- package/dist/pdf-backend/primitives.js.map +1 -1
- package/dist/pdf-backend/renderResourceBlock.d.ts.map +1 -1
- package/dist/pdf-backend/renderResourceBlock.js +27 -7
- package/dist/pdf-backend/renderResourceBlock.js.map +1 -1
- package/dist/pdf-backend/shapedText.d.ts +4 -0
- package/dist/pdf-backend/shapedText.d.ts.map +1 -1
- package/dist/pdf-backend/shapedText.js +15 -4
- package/dist/pdf-backend/shapedText.js.map +1 -1
- package/dist/pdf-backend/svgVector.js +1 -1
- package/dist/pdf-backend/svgVector.js.map +1 -1
- package/dist/pdf-backend/verticalText.d.ts.map +1 -1
- package/dist/pdf-backend/verticalText.js +7 -4
- package/dist/pdf-backend/verticalText.js.map +1 -1
- package/dist/pdf-backend/wordPieces.d.ts +22 -41
- package/dist/pdf-backend/wordPieces.d.ts.map +1 -1
- package/dist/pdf-backend/wordPieces.js +75 -62
- package/dist/pdf-backend/wordPieces.js.map +1 -1
- package/dist/shapingLanguage.d.ts +16 -0
- package/dist/shapingLanguage.d.ts.map +1 -1
- package/dist/shapingLanguage.js +50 -20
- package/dist/shapingLanguage.js.map +1 -1
- package/dist/unicodeRange.d.ts +19 -0
- package/dist/unicodeRange.d.ts.map +1 -0
- package/dist/unicodeRange.js +68 -0
- package/dist/unicodeRange.js.map +1 -0
- package/dist/worker/client.d.ts +1 -1
- package/dist/worker/client.d.ts.map +1 -1
- package/dist/worker/client.js +5 -2
- package/dist/worker/client.js.map +1 -1
- package/dist/worker/pdf.worker.js +7 -0
- package/dist/worker/pdf.worker.js.map +1 -1
- package/dist/worker/protocol.d.ts +6 -0
- package/dist/worker/protocol.d.ts.map +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The words a browser's canvas shapes a string in (postext #770).
|
|
3
|
+
*
|
|
4
|
+
* Chrome does not hand a string to its shaper whole. It cuts it into words
|
|
5
|
+
* and shapes each alone, so that a word met again is answered from a cache
|
|
6
|
+
* (Blink's `CachingWordShapeIterator`): nothing a font does between two
|
|
7
|
+
* glyphs crosses from one word to the next, neither a kerned pair nor a
|
|
8
|
+
* ligature, and each word is given the script of its own letters. The
|
|
9
|
+
* layout was measured that way, and the canvas paints that way. A shaper
|
|
10
|
+
* handed the whole string does otherwise:
|
|
11
|
+
*
|
|
12
|
+
* - Source Serif 4 kerns a space against the `W` after it: `Wang Wei` in a
|
|
13
|
+
* running head came out 0.06 em narrower than its measure;
|
|
14
|
+
* - Noto Sans JP kerns kana against kana (`える` 0.03 em, `われ` 0.05 em):
|
|
15
|
+
* a line of 36 characters ended 1.4 pt short of its measure, where the
|
|
16
|
+
* canvas kerns only a character and the mark after it (`が、`, `す。`);
|
|
17
|
+
* - Noto Sans SC draws the dash of `9704—2012` in its Chinese form, a full
|
|
18
|
+
* em, when the word is shaped with the Latin words beside it, and 0.89
|
|
19
|
+
* em wide alone, as measured;
|
|
20
|
+
* - Noto Serif JP draws `“` in its Japanese form beside a Han character
|
|
21
|
+
* in one run, and in its default form as the word of its own it is on
|
|
22
|
+
* the canvas;
|
|
23
|
+
* - Aref Ruqaa sets a space 0.146 em wide in Arabic text and 0.333 em in
|
|
24
|
+
* any other: the space between two Arabic words is a word of its own on
|
|
25
|
+
* the canvas, with no letter and so no script, and a title of three
|
|
26
|
+
* words came out 0.37 em short.
|
|
27
|
+
*
|
|
28
|
+
* The cuts, as measured in Chrome 154 (widths of strings against the sum
|
|
29
|
+
* of their parts, in faces that kern across the cut or draw a mark
|
|
30
|
+
* otherwise by script):
|
|
31
|
+
*
|
|
32
|
+
* - a space (U+0020) and a tab are words of their own. No other space
|
|
33
|
+
* cuts: a no-break space stays in its word;
|
|
34
|
+
* - a string with no character above U+00FF is cut there only;
|
|
35
|
+
* - a CJK character (a Han character, a kana, a CJK mark or symbol, a
|
|
36
|
+
* full-width form, an enclosed number, an emoji) ends the word before
|
|
37
|
+
* it and opens one that holds it, the marks and modifier letters after
|
|
38
|
+
* it (`ー`, `々`, a voicing mark) and the CJK characters of no script
|
|
39
|
+
* after it (`、`, `。`, `」`): `が、` is one word, `える` two. A word that
|
|
40
|
+
* opens with such a mark (`「`, `ー`) takes the first character with a
|
|
41
|
+
* script after it as well.
|
|
42
|
+
*
|
|
43
|
+
* The symbols counted as CJK were read off the canvas ({@link CJK_BASE}):
|
|
44
|
+
* the characters of no script after which `n` + character + `—` keeps the
|
|
45
|
+
* default dash in Noto Serif SC under `zh`, whose dash takes the Chinese
|
|
46
|
+
* form anywhere in a Latin word. The letters (Han, kana, Bopomofo, the
|
|
47
|
+
* full-width and half-width forms, Roman numerals) cannot be read that
|
|
48
|
+
* way and are Blink's own ranges; Hangul is not among them.
|
|
49
|
+
*/
|
|
50
|
+
/** The characters with no script, by block, that end a word (measured). */
|
|
51
|
+
const SYMBOLS = '2020-2021 2030 203B-203C 2042 2047-2049 2051 2100 2103 2105 2109-210A 2113 2116 2121 213B 2150-2152 2156-215A 2189 2194-2195 2307 2312 231A-231B 2329 23BE-23CC 23CE 23E9-23EC 23F0 23F3 2423 2460-2492 249C-24FF 25A0-25A2 25AA-25AB 25B1-25B3 25B6-25B7 25BC-25BD 25C0-25C1 25C6-25C7 25C9 25CB-25CC 25CE-25D3 25E2-25E6 25EF 25FD-25FE 2600-2603 2605-2606 260E 2614-2617 261D 2620 2640 2642 2648-2653 2660-266F 2672-267D 267F 2693 2695-2696 26A0-26A1 26A7 26AA-26AB 26BD-26BE 26C4-26C5 26CE 26D3-26D4 26EA 26F2-26F3 26F5 26F9-26FA 26FD 2705 2708 270A-270D 2713 271A 2728 273F-2740 2744 274C 274E 2753-2757 2763-2764 2776-277F 2795-2797 27A1 27B0 27BF 2B1A-2B1C 2B50 2B55 2FF0-3004 3006 3008-3020 3030 3036-3037 303C-303F 3190-319F 31C0-31E5 31EF 3220-325F 327F-32CF 32FF 3358-33FF FE10-FE12 FE19 FE30-FE52 FE54-FE66 FE68-FE6B FF01-FF0C FF0E-FF1A FF1D FF1F-FF20 FF3B-FF3D FF3F FF5B-FF65 FFE0-FFE2 FFE4-FFE6 FFE8-FFEE';
|
|
52
|
+
/** The letters and numbers: Å, Roman numerals, Han, kana, Bopomofo,
|
|
53
|
+
* circled and squared kana, full-width Latin, half-width kana and Hangul,
|
|
54
|
+
* the emoji and the Han of the upper planes. */
|
|
55
|
+
const LETTERS = '212B 2160-216B 2170-217B 217F 2E80-2FDF 3007 3021-3029 3038-303A 3041-3096 309F 30A1-30FA 30FF 3105-312F 31A0-31BF 32D0-32FE 3300-3357 3400-4DBF 4E00-9FFF F900-FAFF FF21-FF3A FF41-FF5A FF66-FF6F FF71-FF9D FFA0-FFDC 1F000-1FAFF 20000-3FFFF';
|
|
56
|
+
/** The marks and modifier letters of the CJK blocks: they open a CJK word
|
|
57
|
+
* when a string starts with one, and never end a word. */
|
|
58
|
+
const MODIFIERS = '3005 302A-302D 3031-3035 303B 3099-309E 30FC-30FE FF3E FF40 FF70 FF9E-FF9F FFE3';
|
|
59
|
+
function ranges(list) {
|
|
60
|
+
const out = [];
|
|
61
|
+
for (const part of list.split(' ')) {
|
|
62
|
+
const [lo, hi = lo] = part.split('-');
|
|
63
|
+
out.push(parseInt(lo, 16), parseInt(hi, 16));
|
|
64
|
+
}
|
|
65
|
+
// By first code point, for the search.
|
|
66
|
+
const pairs = [];
|
|
67
|
+
for (let i = 0; i < out.length; i += 2)
|
|
68
|
+
pairs.push([out[i], out[i + 1]]);
|
|
69
|
+
pairs.sort((a, b) => a[0] - b[0]);
|
|
70
|
+
return pairs.flat();
|
|
71
|
+
}
|
|
72
|
+
const CJK_BASE = ranges(`${SYMBOLS} ${LETTERS}`);
|
|
73
|
+
const CJK_MODIFIERS = ranges(MODIFIERS);
|
|
74
|
+
function inRanges(table, cp) {
|
|
75
|
+
let lo = 0;
|
|
76
|
+
let hi = table.length / 2 - 1;
|
|
77
|
+
while (lo <= hi) {
|
|
78
|
+
const mid = (lo + hi) >> 1;
|
|
79
|
+
if (cp < table[mid * 2])
|
|
80
|
+
hi = mid - 1;
|
|
81
|
+
else if (cp > table[mid * 2 + 1])
|
|
82
|
+
lo = mid + 1;
|
|
83
|
+
else
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
/** No CJK character is below this. */
|
|
89
|
+
const FIRST_CJK = 0x2020;
|
|
90
|
+
/** A CJK character that ends the word before it. */
|
|
91
|
+
function isCjkBase(cp) {
|
|
92
|
+
return cp >= FIRST_CJK && inRanges(CJK_BASE, cp);
|
|
93
|
+
}
|
|
94
|
+
/** A CJK character: one that opens a CJK word when a word starts with it. */
|
|
95
|
+
function isCjk(cp) {
|
|
96
|
+
return cp >= FIRST_CJK && (inRanges(CJK_BASE, cp) || inRanges(CJK_MODIFIERS, cp));
|
|
97
|
+
}
|
|
98
|
+
function isDelimiter(cp) {
|
|
99
|
+
return cp === 0x20 || cp === 0x09;
|
|
100
|
+
}
|
|
101
|
+
/** A mark, a modifier letter or a modifier symbol, and the zero width
|
|
102
|
+
* joiner: they stay in the word of the character before them. */
|
|
103
|
+
const JOINS_RE = /^[\p{M}\p{Lm}\p{Sk}\u200D]$/u;
|
|
104
|
+
/** A character with no script of its own. */
|
|
105
|
+
const NO_SCRIPT_RE = /^[\p{Script=Common}\p{Script=Inherited}]$/u;
|
|
106
|
+
/** A string a browser keeps in eight bits. */
|
|
107
|
+
const LATIN_1_RE = /^[\u0000-\u00FF]*$/;
|
|
108
|
+
/** Anything a string may be cut at: a space, a tab, a character from the
|
|
109
|
+
* first CJK one on. */
|
|
110
|
+
const MAY_CUT_RE = /[ \t\u2020-\uFFFF]/;
|
|
111
|
+
/**
|
|
112
|
+
* `text` cut into the words the canvas shapes it in (see the module
|
|
113
|
+
* comment); the string itself, alone, when it is one word.
|
|
114
|
+
*/
|
|
115
|
+
export function canvasWords(text) {
|
|
116
|
+
if (text.length < 2 || !MAY_CUT_RE.test(text))
|
|
117
|
+
return [text];
|
|
118
|
+
const latin1 = LATIN_1_RE.test(text);
|
|
119
|
+
const words = [];
|
|
120
|
+
const n = text.length;
|
|
121
|
+
const width = (cp) => (cp > 0xFFFF ? 2 : 1);
|
|
122
|
+
for (let start = 0; start < n;) {
|
|
123
|
+
const first = text.codePointAt(start);
|
|
124
|
+
let end = start + width(first);
|
|
125
|
+
if (isDelimiter(first)) {
|
|
126
|
+
// A word of its own.
|
|
127
|
+
}
|
|
128
|
+
else if (latin1 || !isCjk(first)) {
|
|
129
|
+
while (end < n) {
|
|
130
|
+
const cp = text.codePointAt(end);
|
|
131
|
+
if (isDelimiter(cp) || (!latin1 && isCjkBase(cp)))
|
|
132
|
+
break;
|
|
133
|
+
end += width(cp);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
let scripted = !NO_SCRIPT_RE.test(String.fromCodePoint(first));
|
|
138
|
+
while (end < n) {
|
|
139
|
+
const cp = text.codePointAt(end);
|
|
140
|
+
const ch = String.fromCodePoint(cp);
|
|
141
|
+
if (JOINS_RE.test(ch)) {
|
|
142
|
+
end += width(cp);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (!isCjk(cp))
|
|
146
|
+
break;
|
|
147
|
+
if (!NO_SCRIPT_RE.test(ch)) {
|
|
148
|
+
if (scripted)
|
|
149
|
+
break;
|
|
150
|
+
scripted = true;
|
|
151
|
+
}
|
|
152
|
+
end += width(cp);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
words.push(text.slice(start, end));
|
|
156
|
+
start = end;
|
|
157
|
+
}
|
|
158
|
+
return words.length === 1 ? [text] : words;
|
|
159
|
+
}
|
|
160
|
+
//# sourceMappingURL=canvasWords.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"canvasWords.js","sourceRoot":"","sources":["../src/canvasWords.ts"],"names":[],"mappings":"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"}
|
package/dist/faceFiles.d.ts
CHANGED
|
@@ -12,30 +12,92 @@
|
|
|
12
12
|
* text object, so a line of Han with a Latin word paints from three or four
|
|
13
13
|
* slices and each slice keeps only the glyphs set in it.
|
|
14
14
|
*
|
|
15
|
+
* Which file sets a character two files hold (postext #770): a browser
|
|
16
|
+
* takes it from the `@font-face` rule declared last among those whose
|
|
17
|
+
* `unicode-range` holds it, and Fontsource's files overlap. A Japanese
|
|
18
|
+
* family repeats the ASCII letters in its `latin` file and in its numbered
|
|
19
|
+
* slices, and a pen declares the slices after `latin`: Chrome 154 sets the
|
|
20
|
+
* letters of `Shift_JIS` from slice 118 and the `_` from slice 113 of Noto
|
|
21
|
+
* Serif JP, 4252 thousandths of the em wide, where the `latin` file alone
|
|
22
|
+
* kerns `t_` and `_J` and sets it 4318 wide. A provider that hands each
|
|
23
|
+
* file with its range and its place in the declaration (`PdfFontFile`)
|
|
24
|
+
* gets each character from the file the browser took it from
|
|
25
|
+
* ({@link FaceFiles.declaredFile}), in a document shaped in its own
|
|
26
|
+
* language; one that hands bare files gets it from the first file with a
|
|
27
|
+
* glyph, as before, and so does a document that keeps the host's language.
|
|
28
|
+
*
|
|
15
29
|
* Characters a face lacks in every file are recorded as they are shaped
|
|
16
30
|
* ({@link noteMissingGlyphs}), for the `missingGlyph` warning.
|
|
17
31
|
*/
|
|
18
32
|
import type { PDFFont } from 'pdf-lib';
|
|
33
|
+
/** What a provider says of a file of a face (`PdfFontFile`): the
|
|
34
|
+
* characters its `@font-face` rule answers for, and where the rule is
|
|
35
|
+
* declared among the rules of the face. */
|
|
36
|
+
export interface FileDeclaration {
|
|
37
|
+
unicodeRange?: string;
|
|
38
|
+
order?: number;
|
|
39
|
+
}
|
|
19
40
|
/** The files of one face, in the order their glyphs are looked up. */
|
|
20
41
|
export declare class FaceFiles {
|
|
21
42
|
readonly files: PDFFont[];
|
|
22
43
|
private readonly faces;
|
|
23
44
|
/** First file holding each code point, or null for none. */
|
|
24
45
|
private readonly first;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
46
|
+
/** The range each file was given with; a file with none answers for
|
|
47
|
+
* every character. */
|
|
48
|
+
private readonly ranges;
|
|
49
|
+
/** Where each file is declared: the number it was given with, else its
|
|
50
|
+
* place in the list counted down, the first file being the one
|
|
51
|
+
* declared last. */
|
|
52
|
+
private readonly orders;
|
|
53
|
+
/** The files from the one declared last to the one declared first. */
|
|
54
|
+
private lastDeclaredFirst;
|
|
55
|
+
/** The file a browser sets each code point in, or null for none. */
|
|
56
|
+
private readonly declared;
|
|
57
|
+
private hasRanges;
|
|
58
|
+
constructor(files: readonly PDFFont[], declarations?: ReadonlyMap<PDFFont, FileDeclaration>);
|
|
59
|
+
/** Append a file (a slice fetched later), with what the provider says
|
|
60
|
+
* of it; a file already listed is kept where it is, as it was given. */
|
|
61
|
+
add(file: PDFFont, declaration?: FileDeclaration): void;
|
|
62
|
+
/** Whether the provider gave a file of the face with its range: the
|
|
63
|
+
* face is then read as a browser reads its `@font-face` rules
|
|
64
|
+
* ({@link declaredFile}). */
|
|
65
|
+
get ranged(): boolean;
|
|
29
66
|
/** Whether `file` has a glyph for `cp`. */
|
|
30
67
|
covers(file: PDFFont, cp: number): boolean;
|
|
68
|
+
/** Whether `file` answers for `cp` as a browser reads it: its range
|
|
69
|
+
* holds the character and the file has a glyph for it. Chrome 154 skips
|
|
70
|
+
* a file whose range claims a character it has no glyph for and takes
|
|
71
|
+
* the next one down, and never draws a glyph a file has outside its
|
|
72
|
+
* range. */
|
|
73
|
+
private answers;
|
|
31
74
|
/** The first file with a glyph for `cp`, or null when none has one. */
|
|
32
75
|
fileFor(cp: number): PDFFont | null;
|
|
76
|
+
/** The files a browser tries in turn: the one declared last first.
|
|
77
|
+
* Files of one number keep the order they were given in. */
|
|
78
|
+
private byDeclaration;
|
|
79
|
+
/**
|
|
80
|
+
* The file a browser sets `cp` in: the one declared last among those
|
|
81
|
+
* whose range holds it and that have a glyph for it. Null when none
|
|
82
|
+
* does: the browser then leaves the family, for the next one of the
|
|
83
|
+
* `font-family` list or a font of the system.
|
|
84
|
+
*/
|
|
85
|
+
declaredFile(cp: number): PDFFont | null;
|
|
86
|
+
/**
|
|
87
|
+
* The file a browser sets a letter and the marks on it in (`cps`, the
|
|
88
|
+
* letter first): the one declared last that answers for every one of
|
|
89
|
+
* them. With Source Serif 4 declared first and Noto Serif JP after it
|
|
90
|
+
* for `a` alone, Chrome sets `a` in the second (557 thousandths) and
|
|
91
|
+
* `a` + U+0308 whole in the first (541). Null when no file sets them
|
|
92
|
+
* all.
|
|
93
|
+
*/
|
|
94
|
+
declaredFileOfAll(cps: readonly number[]): PDFFont | null;
|
|
33
95
|
}
|
|
34
96
|
/** Register the files of a face under its first file, the font the
|
|
35
97
|
* renderers are handed. A first file already registered (two faces the
|
|
36
98
|
* provider answered with the same files) keeps its list, which takes the
|
|
37
99
|
* new files too; the list in force is returned. */
|
|
38
|
-
export declare function registerFaceFiles(primary: PDFFont, files: readonly PDFFont[]): FaceFiles;
|
|
100
|
+
export declare function registerFaceFiles(primary: PDFFont, files: readonly PDFFont[], declarations?: ReadonlyMap<PDFFont, FileDeclaration>): FaceFiles;
|
|
39
101
|
/** The files of the face whose first file is `font`, when it has several. */
|
|
40
102
|
export declare function faceFilesOf(font: PDFFont): FaceFiles | undefined;
|
|
41
103
|
/** A run of text and the file it is set in. */
|
|
@@ -46,6 +108,9 @@ export interface FileRun {
|
|
|
46
108
|
* and the text it was cut out of does (`besideLetterOf`, #764): the
|
|
47
109
|
* caller shapes the run inside `withBesideLetter`. */
|
|
48
110
|
beside?: number;
|
|
111
|
+
/** The run is a word, or part of one, of a string with a character
|
|
112
|
+
* above U+00FF ({@link wordRuns}): with no letter it has no script. */
|
|
113
|
+
wide?: true;
|
|
49
114
|
}
|
|
50
115
|
/**
|
|
51
116
|
* `text` cut into runs by the file of `font`'s face that has a glyph for
|
|
@@ -85,8 +150,31 @@ export interface FileRun {
|
|
|
85
150
|
* `خالة.»`, which the `latin` file draws), says the letter whose script
|
|
86
151
|
* the browser shaped it in (`beside`, #764), in a document shaped in its
|
|
87
152
|
* own language.
|
|
153
|
+
*
|
|
154
|
+
* A face given with the ranges of its files is cut as a browser cuts it
|
|
155
|
+
* ({@link declaredRuns}, #770) in a document shaped in its own language,
|
|
156
|
+
* and none of the rules above that guess at the browser's file apply.
|
|
88
157
|
*/
|
|
89
158
|
export declare function fileRuns(font: PDFFont, text: string, rtl?: boolean): FileRun[];
|
|
159
|
+
/**
|
|
160
|
+
* `text`, a string the canvas was handed whole (a line of a running head,
|
|
161
|
+
* a word of the body), cut into the runs it is shaped in: its words
|
|
162
|
+
* (`canvasWords.ts`, #770), each cut by file ({@link fileRuns}). The
|
|
163
|
+
* canvas shapes every word alone, so no kerned pair and no ligature
|
|
164
|
+
* crosses a space or stands between two CJK characters, and a word takes
|
|
165
|
+
* the script of its own letters: one with no letter, a space among them,
|
|
166
|
+
* has none in a string not kept in eight bits (`wide`).
|
|
167
|
+
*
|
|
168
|
+
* {@link fileRuns} itself, the string shaped whole as up to postext-pdf
|
|
169
|
+
* 1.29, for a string of one word, in a document that keeps the host's
|
|
170
|
+
* language, and with a script in force (#725: a word with no letter the
|
|
171
|
+
* layout measured in the script of its context).
|
|
172
|
+
*/
|
|
173
|
+
export declare function wordRuns(font: PDFFont, text: string, rtl?: boolean): FileRun[];
|
|
174
|
+
/** Run `fn`, the shaping of `run` ({@link wordRuns}), with what the run
|
|
175
|
+
* says of its script in force: the letter it was cut from beside, and
|
|
176
|
+
* whether it is a word of a string not kept in eight bits. */
|
|
177
|
+
export declare function inRun<T>(run: FileRun, fn: () => T): T;
|
|
90
178
|
/** `text`'s width at `size` (points), each run measured in its file. */
|
|
91
179
|
export declare function widthOfTextAtSize(font: PDFFont, text: string, size: number): number;
|
|
92
180
|
/** Record the characters of `text` that `font` has no glyph for (it draws
|
package/dist/faceFiles.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"faceFiles.d.ts","sourceRoot":"","sources":["../src/faceFiles.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"faceFiles.d.ts","sourceRoot":"","sources":["../src/faceFiles.ts"],"names":[],"mappings":"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"}
|
package/dist/faceFiles.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { mirroredCodePoint } from 'postext';
|
|
2
2
|
import { isDefaultIgnorable, isFallbackHandled, substituteGlyph } from './pdf-backend/fallbackSpaces';
|
|
3
|
-
import { besideLetterOf, shapingScriptLetter } from './shapingLanguage';
|
|
3
|
+
import { besideLetterOf, shapesAsCanvas, shapingScript, shapingScriptLetter, withBesideLetter, withinWideText } from './shapingLanguage';
|
|
4
|
+
import { canvasWords } from './canvasWords';
|
|
5
|
+
import { parseUnicodeRange, rangesHold } from './unicodeRange';
|
|
4
6
|
function coverageFace(font) {
|
|
5
7
|
const face = font.embedder?.font;
|
|
6
8
|
return face && typeof face.hasGlyphForCodePoint === 'function' ? face : undefined;
|
|
@@ -17,27 +19,63 @@ export class FaceFiles {
|
|
|
17
19
|
faces = new Map();
|
|
18
20
|
/** First file holding each code point, or null for none. */
|
|
19
21
|
first = new Map();
|
|
20
|
-
|
|
22
|
+
/** The range each file was given with; a file with none answers for
|
|
23
|
+
* every character. */
|
|
24
|
+
ranges = new Map();
|
|
25
|
+
/** Where each file is declared: the number it was given with, else its
|
|
26
|
+
* place in the list counted down, the first file being the one
|
|
27
|
+
* declared last. */
|
|
28
|
+
orders = new Map();
|
|
29
|
+
/** The files from the one declared last to the one declared first. */
|
|
30
|
+
lastDeclaredFirst;
|
|
31
|
+
/** The file a browser sets each code point in, or null for none. */
|
|
32
|
+
declared = new Map();
|
|
33
|
+
hasRanges = false;
|
|
34
|
+
constructor(files, declarations) {
|
|
21
35
|
this.files = [];
|
|
22
36
|
for (const file of files)
|
|
23
|
-
this.add(file);
|
|
37
|
+
this.add(file, declarations?.get(file));
|
|
24
38
|
}
|
|
25
|
-
/** Append a file (a slice fetched later)
|
|
26
|
-
* where it is. */
|
|
27
|
-
add(file) {
|
|
39
|
+
/** Append a file (a slice fetched later), with what the provider says
|
|
40
|
+
* of it; a file already listed is kept where it is, as it was given. */
|
|
41
|
+
add(file, declaration) {
|
|
28
42
|
if (this.faces.has(file))
|
|
29
43
|
return;
|
|
44
|
+
this.orders.set(file, declaration?.order ?? -this.files.length);
|
|
30
45
|
this.files.push(file);
|
|
31
46
|
this.faces.set(file, coverageFace(file));
|
|
32
|
-
|
|
47
|
+
const ranges = declaration?.unicodeRange === undefined ? undefined : parseUnicodeRange(declaration.unicodeRange);
|
|
48
|
+
if (ranges) {
|
|
49
|
+
this.ranges.set(file, ranges);
|
|
50
|
+
this.hasRanges = true;
|
|
51
|
+
}
|
|
52
|
+
// A code point no file had may be in this one, and a file declared
|
|
53
|
+
// later than the one that set a character takes it.
|
|
33
54
|
for (const [cp, hit] of this.first)
|
|
34
55
|
if (hit === null)
|
|
35
56
|
this.first.delete(cp);
|
|
57
|
+
this.declared.clear();
|
|
58
|
+
this.lastDeclaredFirst = undefined;
|
|
59
|
+
}
|
|
60
|
+
/** Whether the provider gave a file of the face with its range: the
|
|
61
|
+
* face is then read as a browser reads its `@font-face` rules
|
|
62
|
+
* ({@link declaredFile}). */
|
|
63
|
+
get ranged() {
|
|
64
|
+
return this.hasRanges;
|
|
36
65
|
}
|
|
37
66
|
/** Whether `file` has a glyph for `cp`. */
|
|
38
67
|
covers(file, cp) {
|
|
39
68
|
return this.faces.get(file)?.hasGlyphForCodePoint(cp) ?? false;
|
|
40
69
|
}
|
|
70
|
+
/** Whether `file` answers for `cp` as a browser reads it: its range
|
|
71
|
+
* holds the character and the file has a glyph for it. Chrome 154 skips
|
|
72
|
+
* a file whose range claims a character it has no glyph for and takes
|
|
73
|
+
* the next one down, and never draws a glyph a file has outside its
|
|
74
|
+
* range. */
|
|
75
|
+
answers(file, cp) {
|
|
76
|
+
const ranges = this.ranges.get(file);
|
|
77
|
+
return (ranges === undefined || rangesHold(ranges, cp)) && this.covers(file, cp);
|
|
78
|
+
}
|
|
41
79
|
/** The first file with a glyph for `cp`, or null when none has one. */
|
|
42
80
|
fileFor(cp) {
|
|
43
81
|
const hit = this.first.get(cp);
|
|
@@ -53,20 +91,54 @@ export class FaceFiles {
|
|
|
53
91
|
this.first.set(cp, found);
|
|
54
92
|
return found;
|
|
55
93
|
}
|
|
94
|
+
/** The files a browser tries in turn: the one declared last first.
|
|
95
|
+
* Files of one number keep the order they were given in. */
|
|
96
|
+
byDeclaration() {
|
|
97
|
+
if (!this.lastDeclaredFirst) {
|
|
98
|
+
const at = new Map(this.files.map((file, i) => [file, i]));
|
|
99
|
+
this.lastDeclaredFirst = [...this.files].sort((a, b) => this.orders.get(b) - this.orders.get(a) || at.get(a) - at.get(b));
|
|
100
|
+
}
|
|
101
|
+
return this.lastDeclaredFirst;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* The file a browser sets `cp` in: the one declared last among those
|
|
105
|
+
* whose range holds it and that have a glyph for it. Null when none
|
|
106
|
+
* does: the browser then leaves the family, for the next one of the
|
|
107
|
+
* `font-family` list or a font of the system.
|
|
108
|
+
*/
|
|
109
|
+
declaredFile(cp) {
|
|
110
|
+
const hit = this.declared.get(cp);
|
|
111
|
+
if (hit !== undefined)
|
|
112
|
+
return hit;
|
|
113
|
+
const found = this.byDeclaration().find((file) => this.answers(file, cp)) ?? null;
|
|
114
|
+
this.declared.set(cp, found);
|
|
115
|
+
return found;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* The file a browser sets a letter and the marks on it in (`cps`, the
|
|
119
|
+
* letter first): the one declared last that answers for every one of
|
|
120
|
+
* them. With Source Serif 4 declared first and Noto Serif JP after it
|
|
121
|
+
* for `a` alone, Chrome sets `a` in the second (557 thousandths) and
|
|
122
|
+
* `a` + U+0308 whole in the first (541). Null when no file sets them
|
|
123
|
+
* all.
|
|
124
|
+
*/
|
|
125
|
+
declaredFileOfAll(cps) {
|
|
126
|
+
return this.byDeclaration().find((file) => cps.every((cp) => this.answers(file, cp))) ?? null;
|
|
127
|
+
}
|
|
56
128
|
}
|
|
57
129
|
const faceFiles = new WeakMap();
|
|
58
130
|
/** Register the files of a face under its first file, the font the
|
|
59
131
|
* renderers are handed. A first file already registered (two faces the
|
|
60
132
|
* provider answered with the same files) keeps its list, which takes the
|
|
61
133
|
* new files too; the list in force is returned. */
|
|
62
|
-
export function registerFaceFiles(primary, files) {
|
|
134
|
+
export function registerFaceFiles(primary, files, declarations) {
|
|
63
135
|
const known = faceFiles.get(primary);
|
|
64
136
|
if (known) {
|
|
65
137
|
for (const file of files)
|
|
66
|
-
known.add(file);
|
|
138
|
+
known.add(file, declarations?.get(file));
|
|
67
139
|
return known;
|
|
68
140
|
}
|
|
69
|
-
const list = new FaceFiles([primary, ...files]);
|
|
141
|
+
const list = new FaceFiles([primary, ...files], declarations);
|
|
70
142
|
faceFiles.set(primary, list);
|
|
71
143
|
return list;
|
|
72
144
|
}
|
|
@@ -113,11 +185,17 @@ export function faceFilesOf(font) {
|
|
|
113
185
|
* `خالة.»`, which the `latin` file draws), says the letter whose script
|
|
114
186
|
* the browser shaped it in (`beside`, #764), in a document shaped in its
|
|
115
187
|
* own language.
|
|
188
|
+
*
|
|
189
|
+
* A face given with the ranges of its files is cut as a browser cuts it
|
|
190
|
+
* ({@link declaredRuns}, #770) in a document shaped in its own language,
|
|
191
|
+
* and none of the rules above that guess at the browser's file apply.
|
|
116
192
|
*/
|
|
117
193
|
export function fileRuns(font, text, rtl = false) {
|
|
118
194
|
const list = faceFilesOf(font);
|
|
119
195
|
if (!list)
|
|
120
196
|
return [{ font, text }];
|
|
197
|
+
if (setsAsDeclared(list))
|
|
198
|
+
return besideLetters(declaredRuns(list, font, text, rtl), text, font);
|
|
121
199
|
const letter = shapingScriptLetter();
|
|
122
200
|
if (letter !== undefined) {
|
|
123
201
|
const file = list.fileFor(letter);
|
|
@@ -177,6 +255,11 @@ export function fileRuns(font, text, rtl = false) {
|
|
|
177
255
|
else
|
|
178
256
|
runs.push({ font, text: pending });
|
|
179
257
|
}
|
|
258
|
+
return besideLetters(runs, text, font);
|
|
259
|
+
}
|
|
260
|
+
/** `runs`, the runs `text` was cut into, each saying the letter it was
|
|
261
|
+
* cut from beside when it has none; one run in `font` for no run. */
|
|
262
|
+
function besideLetters(runs, text, font) {
|
|
180
263
|
if (runs.length > 1) {
|
|
181
264
|
// The runs follow each other through the text.
|
|
182
265
|
let at = 0;
|
|
@@ -189,6 +272,169 @@ export function fileRuns(font, text, rtl = false) {
|
|
|
189
272
|
}
|
|
190
273
|
return runs.length > 0 ? runs : [{ font, text }];
|
|
191
274
|
}
|
|
275
|
+
/**
|
|
276
|
+
* Whether the text being painted takes each character from the file a
|
|
277
|
+
* browser takes it from: a face whose provider gave its files with their
|
|
278
|
+
* ranges, in a document shaped in its own language.
|
|
279
|
+
*
|
|
280
|
+
* The page chose the file of a character the same way whatever language
|
|
281
|
+
* it shaped the text in, but a document that keeps the host's language is
|
|
282
|
+
* shaped string by string, each run in the script it shows, as it always
|
|
283
|
+
* was, and prints the same bytes. Cut by the browser's files, its strings
|
|
284
|
+
* fall into more runs with no letter, which that shaping gives no script:
|
|
285
|
+
* in Noto Serif SC the space is another slice's than the letters and the
|
|
286
|
+
* digits, so `GB/T 15834` became `GB/T`, a space and `15834`, and the
|
|
287
|
+
* digits alone took the proportional forms of the default script, 0.80 pt
|
|
288
|
+
* short of the tabular ones the page measured in a Latin string (15.75
|
|
289
|
+
* pt, the Cookbook's Chinese official document). `renderToPdf` drops what
|
|
290
|
+
* the files say when no document of the render is shaped in its own
|
|
291
|
+
* language (`withBareFiles`); this is for a page of one that is not, in a
|
|
292
|
+
* render that has one.
|
|
293
|
+
*/
|
|
294
|
+
function setsAsDeclared(list) {
|
|
295
|
+
return list.ranged && shapesAsCanvas();
|
|
296
|
+
}
|
|
297
|
+
/** A combining mark: it is set in the file of the letter it stands on. */
|
|
298
|
+
const MARK_RE = /^\p{M}$/u;
|
|
299
|
+
/**
|
|
300
|
+
* `text` cut into runs by the file a browser sets each character in, for
|
|
301
|
+
* a face given with the ranges of its files (postext #770). As measured
|
|
302
|
+
* in Chrome 154 (two files that draw the same letters at other widths,
|
|
303
|
+
* declared as one family, on a canvas in a page and in a worker):
|
|
304
|
+
*
|
|
305
|
+
* - a character is set in the file declared last among those whose range
|
|
306
|
+
* holds it: of two `FontFace` objects, the one added to the font set
|
|
307
|
+
* last, whichever was made or loaded first; of two `@font-face` rules,
|
|
308
|
+
* the later one in the document; and a `FontFace` added by script
|
|
309
|
+
* rather than any rule of a style sheet, whichever came first;
|
|
310
|
+
* - a file with no range answers for every character;
|
|
311
|
+
* - a file whose range claims a character it has no glyph for is passed
|
|
312
|
+
* over for the next one down, and a glyph a file has outside its range
|
|
313
|
+
* is never drawn;
|
|
314
|
+
* - nothing stays in a file because the character before it was set
|
|
315
|
+
* there: `Aa` with the capitals in one file is `A` from it and `a` from
|
|
316
|
+
* the other, 1269 thousandths, where either file alone gives 1253 or
|
|
317
|
+
* 1274. A space goes to the file whose range holds it, though every
|
|
318
|
+
* file has one;
|
|
319
|
+
* - a letter and the marks on it are set in one file, the one declared
|
|
320
|
+
* last that answers for them all. (Chrome also composes the pair where
|
|
321
|
+
* a file answers for the composed character: `o` + U+0304 is the `ō` of
|
|
322
|
+
* `latin-ext`. The PDF's shapers compose nothing, so here the pair goes
|
|
323
|
+
* to a file that has both.)
|
|
324
|
+
*
|
|
325
|
+
* So no character follows the one before it here, an opening bracket does
|
|
326
|
+
* not go with what it opens and a word shaped in a script is not looked up
|
|
327
|
+
* by a letter of that script: the browser did none of that. What stays is
|
|
328
|
+
* what the shapers need: a zero width joiner or non-joiner with the letter
|
|
329
|
+
* it joins, an invisible character in the run it stands in.
|
|
330
|
+
*
|
|
331
|
+
* A character no file answers for was drawn by the browser in another
|
|
332
|
+
* family; here it is set as in a face given without ranges: in the run
|
|
333
|
+
* before it when that file has a glyph, else in the first file that has
|
|
334
|
+
* one (its mirror's, in a right-to-left run), and reported as missing
|
|
335
|
+
* when none has.
|
|
336
|
+
*/
|
|
337
|
+
function declaredRuns(list, font, text, rtl) {
|
|
338
|
+
const runs = [];
|
|
339
|
+
let current;
|
|
340
|
+
// Joining controls waiting for the letter after them.
|
|
341
|
+
let pending = '';
|
|
342
|
+
let previous = 0;
|
|
343
|
+
// The file of the letter whose marks follow, up to `wholeEnd`.
|
|
344
|
+
let whole = null;
|
|
345
|
+
let wholeEnd = 0;
|
|
346
|
+
const chars = [...text];
|
|
347
|
+
for (let at = 0; at < chars.length; at++) {
|
|
348
|
+
const ch = chars[at];
|
|
349
|
+
const cp = ch.codePointAt(0);
|
|
350
|
+
if (isJoiningControl(cp) && (!current || !joinsOnto(previous))) {
|
|
351
|
+
pending += ch;
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
let file;
|
|
355
|
+
if (at < wholeEnd) {
|
|
356
|
+
file = whole;
|
|
357
|
+
}
|
|
358
|
+
else if (current && isDefaultIgnorable(cp)) {
|
|
359
|
+
file = current.font;
|
|
360
|
+
}
|
|
361
|
+
else if (MARK_RE.test(ch)) {
|
|
362
|
+
// A mark whose letter no file sets with it: beside its letter when
|
|
363
|
+
// that file has a glyph for it.
|
|
364
|
+
file = current && list.covers(current.font, cp) ? current.font : list.declaredFile(cp);
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
let end = at + 1;
|
|
368
|
+
while (end < chars.length && MARK_RE.test(chars[end]))
|
|
369
|
+
end++;
|
|
370
|
+
if (end > at + 1) {
|
|
371
|
+
whole = list.declaredFileOfAll(chars.slice(at, end).map((c) => c.codePointAt(0)));
|
|
372
|
+
wholeEnd = whole ? end : 0;
|
|
373
|
+
}
|
|
374
|
+
file = (at < wholeEnd ? whole : null) ?? list.declaredFile(cp);
|
|
375
|
+
}
|
|
376
|
+
if (!file) {
|
|
377
|
+
const mirror = rtl ? mirroredCodePoint(cp) : cp;
|
|
378
|
+
const shown = mirror !== cp && list.fileFor(mirror) ? mirror : cp;
|
|
379
|
+
file = current && (list.covers(current.font, shown) || isDefaultIgnorable(cp)) ? current.font : list.fileFor(shown) ?? current?.font ?? font;
|
|
380
|
+
}
|
|
381
|
+
if (current && current.font === file) {
|
|
382
|
+
current.text += pending + ch;
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
current = { font: file, text: pending + ch };
|
|
386
|
+
runs.push(current);
|
|
387
|
+
}
|
|
388
|
+
pending = '';
|
|
389
|
+
previous = cp;
|
|
390
|
+
}
|
|
391
|
+
if (pending) {
|
|
392
|
+
if (current)
|
|
393
|
+
current.text += pending;
|
|
394
|
+
else
|
|
395
|
+
runs.push({ font, text: pending });
|
|
396
|
+
}
|
|
397
|
+
return runs;
|
|
398
|
+
}
|
|
399
|
+
/** A string a browser keeps in eight bits: no character above U+00FF. */
|
|
400
|
+
const LATIN_1_RE = /^[\u0000-\u00FF]*$/;
|
|
401
|
+
/**
|
|
402
|
+
* `text`, a string the canvas was handed whole (a line of a running head,
|
|
403
|
+
* a word of the body), cut into the runs it is shaped in: its words
|
|
404
|
+
* (`canvasWords.ts`, #770), each cut by file ({@link fileRuns}). The
|
|
405
|
+
* canvas shapes every word alone, so no kerned pair and no ligature
|
|
406
|
+
* crosses a space or stands between two CJK characters, and a word takes
|
|
407
|
+
* the script of its own letters: one with no letter, a space among them,
|
|
408
|
+
* has none in a string not kept in eight bits (`wide`).
|
|
409
|
+
*
|
|
410
|
+
* {@link fileRuns} itself, the string shaped whole as up to postext-pdf
|
|
411
|
+
* 1.29, for a string of one word, in a document that keeps the host's
|
|
412
|
+
* language, and with a script in force (#725: a word with no letter the
|
|
413
|
+
* layout measured in the script of its context).
|
|
414
|
+
*/
|
|
415
|
+
export function wordRuns(font, text, rtl = false) {
|
|
416
|
+
if (!shapesAsCanvas() || shapingScript() !== undefined)
|
|
417
|
+
return fileRuns(font, text, rtl);
|
|
418
|
+
const words = canvasWords(text);
|
|
419
|
+
if (words.length === 1)
|
|
420
|
+
return fileRuns(font, text, rtl);
|
|
421
|
+
const wide = !LATIN_1_RE.test(text);
|
|
422
|
+
const runs = [];
|
|
423
|
+
for (const word of words) {
|
|
424
|
+
for (const run of fileRuns(font, word, rtl)) {
|
|
425
|
+
if (wide)
|
|
426
|
+
run.wide = true;
|
|
427
|
+
runs.push(run);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return runs;
|
|
431
|
+
}
|
|
432
|
+
/** Run `fn`, the shaping of `run` ({@link wordRuns}), with what the run
|
|
433
|
+
* says of its script in force: the letter it was cut from beside, and
|
|
434
|
+
* whether it is a word of a string not kept in eight bits. */
|
|
435
|
+
export function inRun(run, fn) {
|
|
436
|
+
return run.wide ? withinWideText(true, () => withBesideLetter(run.beside, fn)) : withBesideLetter(run.beside, fn);
|
|
437
|
+
}
|
|
192
438
|
/** Opening brackets and initial quotation marks (`Ps`, `Pi`). */
|
|
193
439
|
const OPENING_RE = /^[\p{Ps}\p{Pi}]$/u;
|
|
194
440
|
/** The file the first character after a row of opening marks (from `from`)
|