@flighthq/textshaper 0.1.0
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/_textShaperHooks.d.ts +4 -0
- package/dist/_textShaperHooks.d.ts.map +1 -0
- package/dist/_textShaperHooks.js +5 -0
- package/dist/_textShaperHooks.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/textShaper.d.ts +5 -0
- package/dist/textShaper.d.ts.map +1 -0
- package/dist/textShaper.js +28 -0
- package/dist/textShaper.js.map +1 -0
- package/dist/textShaperCache.d.ts +9 -0
- package/dist/textShaperCache.d.ts.map +1 -0
- package/dist/textShaperCache.js +54 -0
- package/dist/textShaperCache.js.map +1 -0
- package/dist/textShaperCluster.d.ts +5 -0
- package/dist/textShaperCluster.d.ts.map +1 -0
- package/dist/textShaperCluster.js +73 -0
- package/dist/textShaperCluster.js.map +1 -0
- package/dist/textShaperItemize.d.ts +4 -0
- package/dist/textShaperItemize.d.ts.map +1 -0
- package/dist/textShaperItemize.js +161 -0
- package/dist/textShaperItemize.js.map +1 -0
- package/dist/textShaperPool.d.ts +4 -0
- package/dist/textShaperPool.d.ts.map +1 -0
- package/dist/textShaperPool.js +24 -0
- package/dist/textShaperPool.js.map +1 -0
- package/dist/textShaperRun.d.ts +15 -0
- package/dist/textShaperRun.d.ts.map +1 -0
- package/dist/textShaperRun.js +123 -0
- package/dist/textShaperRun.js.map +1 -0
- package/dist/textShaperSignals.d.ts +5 -0
- package/dist/textShaperSignals.d.ts.map +1 -0
- package/dist/textShaperSignals.js +27 -0
- package/dist/textShaperSignals.js.map +1 -0
- package/package.json +38 -0
- package/src/_textShaperHooks.test.ts +69 -0
- package/src/textShaper.test.ts +42 -0
- package/src/textShaperCache.test.ts +161 -0
- package/src/textShaperCluster.test.ts +113 -0
- package/src/textShaperItemize.test.ts +114 -0
- package/src/textShaperPool.test.ts +36 -0
- package/src/textShaperRun.test.ts +363 -0
- package/src/textShaperSignals.test.ts +84 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"textShaperRun.js","sourceRoot":"","sources":["../src/textShaperRun.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { TextShaperSignals } from '@flighthq/types';
|
|
2
|
+
export declare function disposeTextShaperSignals(): void;
|
|
3
|
+
export declare function enableTextShaperSignals(): TextShaperSignals;
|
|
4
|
+
export declare function getTextShaperSignals(): TextShaperSignals | null;
|
|
5
|
+
//# sourceMappingURL=textShaperSignals.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"textShaperSignals.d.ts","sourceRoot":"","sources":["../src/textShaperSignals.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAqB,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAI5E,wBAAgB,wBAAwB,IAAI,IAAI,CAK/C;AAED,wBAAgB,uBAAuB,IAAI,iBAAiB,CAW3D;AAED,wBAAgB,oBAAoB,IAAI,iBAAiB,GAAG,IAAI,CAE/D"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { clearSignal, createSignal } from '@flighthq/signals';
|
|
2
|
+
import { _setTextShaperBackendHook } from './_textShaperHooks';
|
|
3
|
+
export function disposeTextShaperSignals() {
|
|
4
|
+
if (_signals === null)
|
|
5
|
+
return;
|
|
6
|
+
clearSignal(_signals.onBackendChanged);
|
|
7
|
+
_signals = null;
|
|
8
|
+
_setTextShaperBackendHook(null);
|
|
9
|
+
}
|
|
10
|
+
export function enableTextShaperSignals() {
|
|
11
|
+
if (_signals !== null)
|
|
12
|
+
return _signals;
|
|
13
|
+
_signals = {
|
|
14
|
+
onBackendChanged: createSignal(),
|
|
15
|
+
};
|
|
16
|
+
_setTextShaperBackendHook((backend) => {
|
|
17
|
+
if (_signals !== null) {
|
|
18
|
+
_signals.onBackendChanged.emit(backend);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
return _signals;
|
|
22
|
+
}
|
|
23
|
+
export function getTextShaperSignals() {
|
|
24
|
+
return _signals;
|
|
25
|
+
}
|
|
26
|
+
let _signals = null;
|
|
27
|
+
//# sourceMappingURL=textShaperSignals.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"textShaperSignals.js","sourceRoot":"","sources":["../src/textShaperSignals.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAG9D,OAAO,EAAE,yBAAyB,EAAE,MAAM,oBAAoB,CAAC;AAE/D,MAAM,UAAU,wBAAwB;IACtC,IAAI,QAAQ,KAAK,IAAI;QAAE,OAAO;IAC9B,WAAW,CAAC,QAAQ,CAAC,gBAAgB,CAAC,CAAC;IACvC,QAAQ,GAAG,IAAI,CAAC;IAChB,yBAAyB,CAAC,IAAI,CAAC,CAAC;AAClC,CAAC;AAED,MAAM,UAAU,uBAAuB;IACrC,IAAI,QAAQ,KAAK,IAAI;QAAE,OAAO,QAAQ,CAAC;IACvC,QAAQ,GAAG;QACT,gBAAgB,EAAE,YAAY,EAA+C;KAC9E,CAAC;IACF,yBAAyB,CAAC,CAAC,OAAO,EAAE,EAAE;QACpC,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;YACtB,QAAQ,CAAC,gBAAgB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC1C,CAAC;IACH,CAAC,CAAC,CAAC;IACH,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,MAAM,UAAU,oBAAoB;IAClC,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,IAAI,QAAQ,GAA6B,IAAI,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flighthq/textshaper",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"src/**/*.test.ts",
|
|
16
|
+
"!dist/**/*.test.js",
|
|
17
|
+
"!dist/**/*.test.d.ts",
|
|
18
|
+
"!dist/**/*.test.js.map",
|
|
19
|
+
"!dist/**/*.test.d.ts.map"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc -b",
|
|
23
|
+
"clean": "tsc -b --clean",
|
|
24
|
+
"test": "vitest run --config vitest.config.ts",
|
|
25
|
+
"test:watch": "vitest --watch --config vitest.config.ts",
|
|
26
|
+
"prepack": "npm run clean && npm run clean:dist && npm run build",
|
|
27
|
+
"clean:dist": "tsx ../../scripts/clean-package-dist.ts"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@flighthq/signals": "0.1.0",
|
|
31
|
+
"@flighthq/types": "0.1.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"typescript": "^5.3.0"
|
|
35
|
+
},
|
|
36
|
+
"description": "Text-shaping seam: measure/shape text into advances over a swappable backend (canvas default, HarfBuzz future)",
|
|
37
|
+
"sideEffects": false
|
|
38
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { TextShaperBackend } from '@flighthq/types';
|
|
2
|
+
|
|
3
|
+
import { _setTextShaperBackendHook, _textShaperBackendHook } from './_textShaperHooks';
|
|
4
|
+
import { setTextShaperBackend } from './textShaper';
|
|
5
|
+
|
|
6
|
+
const _testBackend: TextShaperBackend = {
|
|
7
|
+
measureText: (text) => text.length,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
// Restore module state so an installed hook does not leak across tests.
|
|
12
|
+
_setTextShaperBackendHook(null);
|
|
13
|
+
setTextShaperBackend(null);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe('_setTextShaperBackendHook', () => {
|
|
17
|
+
it('installs the hook so setTextShaperBackend invokes it with the new backend', () => {
|
|
18
|
+
let received: TextShaperBackend | null = _testBackend;
|
|
19
|
+
let calls = 0;
|
|
20
|
+
_setTextShaperBackendHook((backend) => {
|
|
21
|
+
received = backend;
|
|
22
|
+
calls++;
|
|
23
|
+
});
|
|
24
|
+
setTextShaperBackend(_testBackend);
|
|
25
|
+
expect(calls).toBe(1);
|
|
26
|
+
expect(received).toBe(_testBackend);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('passes null through to the hook when the backend is cleared', () => {
|
|
30
|
+
let received: TextShaperBackend | null = _testBackend;
|
|
31
|
+
_setTextShaperBackendHook((backend) => {
|
|
32
|
+
received = backend;
|
|
33
|
+
});
|
|
34
|
+
setTextShaperBackend(null);
|
|
35
|
+
expect(received).toBeNull();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('exposes the installed hook on the _textShaperBackendHook slot', () => {
|
|
39
|
+
expect(_textShaperBackendHook).toBeNull();
|
|
40
|
+
const hook = (): void => {};
|
|
41
|
+
_setTextShaperBackendHook(hook);
|
|
42
|
+
expect(_textShaperBackendHook).toBe(hook);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('clears the hook when passed null so setTextShaperBackend no longer dispatches', () => {
|
|
46
|
+
let calls = 0;
|
|
47
|
+
_setTextShaperBackendHook(() => {
|
|
48
|
+
calls++;
|
|
49
|
+
});
|
|
50
|
+
_setTextShaperBackendHook(null);
|
|
51
|
+
expect(_textShaperBackendHook).toBeNull();
|
|
52
|
+
setTextShaperBackend(_testBackend);
|
|
53
|
+
expect(calls).toBe(0);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('replaces a previously installed hook (last write wins)', () => {
|
|
57
|
+
let firstCalls = 0;
|
|
58
|
+
let secondCalls = 0;
|
|
59
|
+
_setTextShaperBackendHook(() => {
|
|
60
|
+
firstCalls++;
|
|
61
|
+
});
|
|
62
|
+
_setTextShaperBackendHook(() => {
|
|
63
|
+
secondCalls++;
|
|
64
|
+
});
|
|
65
|
+
setTextShaperBackend(_testBackend);
|
|
66
|
+
expect(firstCalls).toBe(0);
|
|
67
|
+
expect(secondCalls).toBe(1);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { TextShaperBackend } from '@flighthq/types';
|
|
2
|
+
|
|
3
|
+
import { getTextShaperBackend, measureText, setTextShaperBackend } from './textShaper';
|
|
4
|
+
|
|
5
|
+
afterEach(() => {
|
|
6
|
+
setTextShaperBackend(null);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
describe('getTextShaperBackend', () => {
|
|
10
|
+
it('returns null before a backend is set', () => {
|
|
11
|
+
expect(getTextShaperBackend()).toBeNull();
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe('measureText', () => {
|
|
16
|
+
it('returns -1 when no backend is registered', () => {
|
|
17
|
+
expect(measureText('hello', {})).toBe(-1);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('delegates to the active backend', () => {
|
|
21
|
+
setTextShaperBackend({ measureText: (text) => text.length * 7 });
|
|
22
|
+
expect(measureText('abc', {})).toBe(21);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('setTextShaperBackend', () => {
|
|
27
|
+
it('stores the backend and clears it with null', () => {
|
|
28
|
+
const backend: TextShaperBackend = { measureText: (text) => text.length };
|
|
29
|
+
setTextShaperBackend(backend);
|
|
30
|
+
expect(getTextShaperBackend()).toBe(backend);
|
|
31
|
+
setTextShaperBackend(null);
|
|
32
|
+
expect(getTextShaperBackend()).toBeNull();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('replaces an existing backend (last write wins, no throw)', () => {
|
|
36
|
+
const first: TextShaperBackend = { measureText: () => 1 };
|
|
37
|
+
const second: TextShaperBackend = { measureText: () => 2 };
|
|
38
|
+
setTextShaperBackend(first);
|
|
39
|
+
setTextShaperBackend(second);
|
|
40
|
+
expect(getTextShaperBackend()).toBe(second);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import type { ShapedRun, TextShaperBackend } from '@flighthq/types';
|
|
2
|
+
|
|
3
|
+
import { setTextShaperBackend } from './textShaper';
|
|
4
|
+
import {
|
|
5
|
+
clearTextShaperCache,
|
|
6
|
+
createTextShaperCache,
|
|
7
|
+
disposeTextShaperCache,
|
|
8
|
+
shapeTextRunCached,
|
|
9
|
+
} from './textShaperCache';
|
|
10
|
+
|
|
11
|
+
const _stubRun: ShapedRun = {
|
|
12
|
+
advanceWidth: 15,
|
|
13
|
+
direction: 'LeftToRight',
|
|
14
|
+
font: null,
|
|
15
|
+
glyphCount: 2,
|
|
16
|
+
glyphs: [],
|
|
17
|
+
script: 'Latn',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function _makeCountingBackend(): { backend: TextShaperBackend; readonly calls: number } {
|
|
21
|
+
let calls = 0;
|
|
22
|
+
return {
|
|
23
|
+
backend: {
|
|
24
|
+
measureText: () => 0,
|
|
25
|
+
shapeRun: () => {
|
|
26
|
+
calls++;
|
|
27
|
+
return { ..._stubRun };
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
get calls() {
|
|
31
|
+
return calls;
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
setTextShaperBackend(null);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('clearTextShaperCache', () => {
|
|
41
|
+
it('removes all cached entries', () => {
|
|
42
|
+
const { backend } = _makeCountingBackend();
|
|
43
|
+
setTextShaperBackend(backend);
|
|
44
|
+
const cache = createTextShaperCache();
|
|
45
|
+
shapeTextRunCached(cache, 'hi', {});
|
|
46
|
+
expect(cache._entries.size).toBe(1);
|
|
47
|
+
clearTextShaperCache(cache);
|
|
48
|
+
expect(cache._entries.size).toBe(0);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('cache remains usable after clearing', () => {
|
|
52
|
+
const { backend } = _makeCountingBackend();
|
|
53
|
+
setTextShaperBackend(backend);
|
|
54
|
+
const cache = createTextShaperCache();
|
|
55
|
+
shapeTextRunCached(cache, 'hi', {});
|
|
56
|
+
clearTextShaperCache(cache);
|
|
57
|
+
shapeTextRunCached(cache, 'hi', {});
|
|
58
|
+
expect(cache._entries.size).toBe(1);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('createTextShaperCache', () => {
|
|
63
|
+
it('returns an empty cache', () => {
|
|
64
|
+
const cache = createTextShaperCache();
|
|
65
|
+
expect(cache._entries.size).toBe(0);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('allocates a new cache on each call', () => {
|
|
69
|
+
expect(createTextShaperCache()).not.toBe(createTextShaperCache());
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('disposeTextShaperCache', () => {
|
|
74
|
+
it('clears all entries', () => {
|
|
75
|
+
const { backend } = _makeCountingBackend();
|
|
76
|
+
setTextShaperBackend(backend);
|
|
77
|
+
const cache = createTextShaperCache();
|
|
78
|
+
shapeTextRunCached(cache, 'hi', {});
|
|
79
|
+
disposeTextShaperCache(cache);
|
|
80
|
+
expect(cache._entries.size).toBe(0);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('shapeTextRunCached', () => {
|
|
85
|
+
it('returns null when no backend is set', () => {
|
|
86
|
+
const cache = createTextShaperCache();
|
|
87
|
+
expect(shapeTextRunCached(cache, 'hi', {})).toBeNull();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('returns null when the backend is advances-only', () => {
|
|
91
|
+
setTextShaperBackend({ measureText: (t) => t.length });
|
|
92
|
+
const cache = createTextShaperCache();
|
|
93
|
+
expect(shapeTextRunCached(cache, 'hi', {})).toBeNull();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('returns a ShapedRun on success', () => {
|
|
97
|
+
const { backend } = _makeCountingBackend();
|
|
98
|
+
setTextShaperBackend(backend);
|
|
99
|
+
const cache = createTextShaperCache();
|
|
100
|
+
const run = shapeTextRunCached(cache, 'hi', {});
|
|
101
|
+
expect(run).not.toBeNull();
|
|
102
|
+
expect(run!.glyphCount).toBe(2);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('returns the same object on the second call (cache hit)', () => {
|
|
106
|
+
const { backend } = _makeCountingBackend();
|
|
107
|
+
setTextShaperBackend(backend);
|
|
108
|
+
const cache = createTextShaperCache();
|
|
109
|
+
const r1 = shapeTextRunCached(cache, 'hi', {});
|
|
110
|
+
const r2 = shapeTextRunCached(cache, 'hi', {});
|
|
111
|
+
expect(r1).toBe(r2);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('calls the backend once for repeated identical inputs', () => {
|
|
115
|
+
const tracker = _makeCountingBackend();
|
|
116
|
+
setTextShaperBackend(tracker.backend);
|
|
117
|
+
const cache = createTextShaperCache();
|
|
118
|
+
shapeTextRunCached(cache, 'hi', {});
|
|
119
|
+
shapeTextRunCached(cache, 'hi', {});
|
|
120
|
+
expect(tracker.calls).toBe(1);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('calls the backend again for different text', () => {
|
|
124
|
+
const tracker = _makeCountingBackend();
|
|
125
|
+
setTextShaperBackend(tracker.backend);
|
|
126
|
+
const cache = createTextShaperCache();
|
|
127
|
+
shapeTextRunCached(cache, 'hi', {});
|
|
128
|
+
shapeTextRunCached(cache, 'bye', {});
|
|
129
|
+
expect(tracker.calls).toBe(2);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('calls the backend again for different format', () => {
|
|
133
|
+
const tracker = _makeCountingBackend();
|
|
134
|
+
setTextShaperBackend(tracker.backend);
|
|
135
|
+
const cache = createTextShaperCache();
|
|
136
|
+
shapeTextRunCached(cache, 'hi', { size: 12 });
|
|
137
|
+
shapeTextRunCached(cache, 'hi', { size: 16 });
|
|
138
|
+
expect(tracker.calls).toBe(2);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('calls the backend again for different options direction', () => {
|
|
142
|
+
const tracker = _makeCountingBackend();
|
|
143
|
+
setTextShaperBackend(tracker.backend);
|
|
144
|
+
const cache = createTextShaperCache();
|
|
145
|
+
shapeTextRunCached(cache, 'hi', {}, { direction: 'LeftToRight' });
|
|
146
|
+
shapeTextRunCached(cache, 'hi', {}, { direction: 'RightToLeft' });
|
|
147
|
+
expect(tracker.calls).toBe(2);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('does not cache null results (no backend)', () => {
|
|
151
|
+
const cache = createTextShaperCache();
|
|
152
|
+
// First call: no backend, returns null, should not be cached.
|
|
153
|
+
expect(shapeTextRunCached(cache, 'hi', {})).toBeNull();
|
|
154
|
+
expect(cache._entries.size).toBe(0);
|
|
155
|
+
// Install a backend; now the same call should succeed.
|
|
156
|
+
const { backend } = _makeCountingBackend();
|
|
157
|
+
setTextShaperBackend(backend);
|
|
158
|
+
expect(shapeTextRunCached(cache, 'hi', {})).not.toBeNull();
|
|
159
|
+
expect(cache._entries.size).toBe(1);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { ShapedRun } from '@flighthq/types';
|
|
2
|
+
|
|
3
|
+
import { getCaretPositionsForRun, getClusterForIndex, getIndexRangeForCluster } from './textShaperCluster';
|
|
4
|
+
|
|
5
|
+
function _makeRun(glyphs: ReadonlyArray<{ cluster: number; xAdvance: number }>): ShapedRun {
|
|
6
|
+
return {
|
|
7
|
+
advanceWidth: glyphs.reduce((s, g) => s + g.xAdvance, 0),
|
|
8
|
+
direction: 'LeftToRight',
|
|
9
|
+
font: null,
|
|
10
|
+
glyphCount: glyphs.length,
|
|
11
|
+
glyphs: glyphs.map((g) => ({
|
|
12
|
+
cluster: g.cluster,
|
|
13
|
+
glyphId: g.cluster,
|
|
14
|
+
xAdvance: g.xAdvance,
|
|
15
|
+
xOffset: 0,
|
|
16
|
+
yAdvance: 0,
|
|
17
|
+
yOffset: 0,
|
|
18
|
+
})),
|
|
19
|
+
script: 'Latn',
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('getCaretPositionsForRun', () => {
|
|
24
|
+
it('returns [0] for an empty run', () => {
|
|
25
|
+
const run = _makeRun([]);
|
|
26
|
+
expect(getCaretPositionsForRun(run)).toEqual([0]);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('returns glyphCount + 1 positions starting at 0', () => {
|
|
30
|
+
const run = _makeRun([
|
|
31
|
+
{ cluster: 0, xAdvance: 8 },
|
|
32
|
+
{ cluster: 1, xAdvance: 7 },
|
|
33
|
+
{ cluster: 2, xAdvance: 9 },
|
|
34
|
+
]);
|
|
35
|
+
const pos = getCaretPositionsForRun(run);
|
|
36
|
+
expect(pos).toHaveLength(4);
|
|
37
|
+
expect(pos[0]).toBe(0);
|
|
38
|
+
expect(pos[1]).toBe(8);
|
|
39
|
+
expect(pos[2]).toBe(15);
|
|
40
|
+
expect(pos[3]).toBe(24);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('last position equals the run advance width', () => {
|
|
44
|
+
const run = _makeRun([
|
|
45
|
+
{ cluster: 0, xAdvance: 5 },
|
|
46
|
+
{ cluster: 1, xAdvance: 3 },
|
|
47
|
+
]);
|
|
48
|
+
const pos = getCaretPositionsForRun(run);
|
|
49
|
+
expect(pos[pos.length - 1]).toBe(run.advanceWidth);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('getClusterForIndex', () => {
|
|
54
|
+
it('returns -1 for an empty run', () => {
|
|
55
|
+
expect(getClusterForIndex(_makeRun([]), 0)).toBe(-1);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('returns -1 for a negative string index', () => {
|
|
59
|
+
const run = _makeRun([{ cluster: 0, xAdvance: 8 }]);
|
|
60
|
+
expect(getClusterForIndex(run, -1)).toBe(-1);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('returns the cluster of the glyph covering the string index', () => {
|
|
64
|
+
const run = _makeRun([
|
|
65
|
+
{ cluster: 0, xAdvance: 8 },
|
|
66
|
+
{ cluster: 2, xAdvance: 7 },
|
|
67
|
+
{ cluster: 4, xAdvance: 9 },
|
|
68
|
+
]);
|
|
69
|
+
expect(getClusterForIndex(run, 0)).toBe(0);
|
|
70
|
+
expect(getClusterForIndex(run, 1)).toBe(0);
|
|
71
|
+
expect(getClusterForIndex(run, 2)).toBe(2);
|
|
72
|
+
expect(getClusterForIndex(run, 3)).toBe(2);
|
|
73
|
+
expect(getClusterForIndex(run, 4)).toBe(4);
|
|
74
|
+
expect(getClusterForIndex(run, 10)).toBe(4);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('getIndexRangeForCluster', () => {
|
|
79
|
+
it('returns null for an empty run', () => {
|
|
80
|
+
expect(getIndexRangeForCluster(_makeRun([]), 0)).toBeNull();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('returns null for a cluster not present in the run', () => {
|
|
84
|
+
const run = _makeRun([{ cluster: 0, xAdvance: 8 }]);
|
|
85
|
+
expect(getIndexRangeForCluster(run, 5)).toBeNull();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('returns [cluster, nextCluster) for a non-final cluster', () => {
|
|
89
|
+
const run = _makeRun([
|
|
90
|
+
{ cluster: 0, xAdvance: 8 },
|
|
91
|
+
{ cluster: 2, xAdvance: 7 },
|
|
92
|
+
{ cluster: 4, xAdvance: 9 },
|
|
93
|
+
]);
|
|
94
|
+
expect(getIndexRangeForCluster(run, 0)).toEqual([0, 2]);
|
|
95
|
+
expect(getIndexRangeForCluster(run, 2)).toEqual([2, 4]);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('uses stringLength for the final cluster end when provided', () => {
|
|
99
|
+
const run = _makeRun([
|
|
100
|
+
{ cluster: 0, xAdvance: 8 },
|
|
101
|
+
{ cluster: 2, xAdvance: 7 },
|
|
102
|
+
]);
|
|
103
|
+
expect(getIndexRangeForCluster(run, 2, 6)).toEqual([2, 6]);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('estimates final cluster end as cluster + 1 when stringLength is absent', () => {
|
|
107
|
+
const run = _makeRun([
|
|
108
|
+
{ cluster: 0, xAdvance: 8 },
|
|
109
|
+
{ cluster: 2, xAdvance: 7 },
|
|
110
|
+
]);
|
|
111
|
+
expect(getIndexRangeForCluster(run, 2)).toEqual([2, 3]);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { ShapedRun, ShapeRunOptions, TextFormat } from '@flighthq/types';
|
|
2
|
+
|
|
3
|
+
import { setTextShaperBackend } from './textShaper';
|
|
4
|
+
import { itemizeText, shapeTextRuns } from './textShaperItemize';
|
|
5
|
+
|
|
6
|
+
const _emptyRun: ShapedRun = {
|
|
7
|
+
advanceWidth: 0,
|
|
8
|
+
direction: 'LeftToRight',
|
|
9
|
+
font: null,
|
|
10
|
+
glyphCount: 0,
|
|
11
|
+
glyphs: [],
|
|
12
|
+
script: 'Latn',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function _makeShapingBackend() {
|
|
16
|
+
return {
|
|
17
|
+
measureText: (t: string) => t.length * 8,
|
|
18
|
+
shapeRun: (_t: string, _f: Readonly<TextFormat>, opts?: Readonly<ShapeRunOptions>): ShapedRun => ({
|
|
19
|
+
..._emptyRun,
|
|
20
|
+
direction: opts?.direction ?? 'LeftToRight',
|
|
21
|
+
script: opts?.script ?? 'Latn',
|
|
22
|
+
}),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
setTextShaperBackend(null);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('itemizeText', () => {
|
|
31
|
+
it('returns empty array for empty string', () => {
|
|
32
|
+
expect(itemizeText('', {})).toEqual([]);
|
|
33
|
+
});
|
|
34
|
+
it('returns a single run for a pure Latin string', () => {
|
|
35
|
+
const items = itemizeText('Hello', {});
|
|
36
|
+
expect(items).toHaveLength(1);
|
|
37
|
+
expect(items[0].start).toBe(0);
|
|
38
|
+
expect(items[0].end).toBe(5);
|
|
39
|
+
expect(items[0].script).toBe('Latn');
|
|
40
|
+
expect(items[0].direction).toBe('LeftToRight');
|
|
41
|
+
});
|
|
42
|
+
it('splits Arabic from Latin', () => {
|
|
43
|
+
// ASCII 'AB' (LTR/Latin) followed by Arabic 'سلام' (U+0633 U+0644 U+0627 U+0645, RTL/Arab)
|
|
44
|
+
const arabic = 'سلام';
|
|
45
|
+
const mixed = 'AB' + arabic;
|
|
46
|
+
const items = itemizeText(mixed, {});
|
|
47
|
+
expect(items).toHaveLength(2);
|
|
48
|
+
expect(items[0].direction).toBe('LeftToRight');
|
|
49
|
+
expect(items[0].script).toBe('Latn');
|
|
50
|
+
expect(items[1].direction).toBe('RightToLeft');
|
|
51
|
+
expect(items[1].script).toBe('Arab');
|
|
52
|
+
});
|
|
53
|
+
it('treats neutral characters (space) as belonging to the current run', () => {
|
|
54
|
+
const items = itemizeText('Hello World', {});
|
|
55
|
+
// Space is neutral; it should be absorbed into the Latin run, not create a boundary.
|
|
56
|
+
expect(items).toHaveLength(1);
|
|
57
|
+
expect(items[0].start).toBe(0);
|
|
58
|
+
expect(items[0].end).toBe(11);
|
|
59
|
+
});
|
|
60
|
+
it('respects explicit direction override from options', () => {
|
|
61
|
+
const items = itemizeText('Hello', {}, { direction: 'RightToLeft' });
|
|
62
|
+
// Latin characters are LTR strong; the explicit direction in options sets the base but
|
|
63
|
+
// strong LTR chars override.
|
|
64
|
+
expect(items).toHaveLength(1);
|
|
65
|
+
expect(items[0].direction).toBe('LeftToRight');
|
|
66
|
+
});
|
|
67
|
+
it('covers entire string (no gaps between items)', () => {
|
|
68
|
+
const arabic = 'سلام';
|
|
69
|
+
const text = 'Hi ' + arabic + ' end';
|
|
70
|
+
const items = itemizeText(text, {});
|
|
71
|
+
const covered = items.reduce((s, item) => s + (item.end - item.start), 0);
|
|
72
|
+
expect(covered).toBe(text.length);
|
|
73
|
+
// Verify contiguous coverage
|
|
74
|
+
for (let i = 1; i < items.length; i++) {
|
|
75
|
+
expect(items[i].start).toBe(items[i - 1].end);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('shapeTextRuns', () => {
|
|
81
|
+
it('returns empty array for empty string', () => {
|
|
82
|
+
expect(shapeTextRuns('', {})).toEqual([]);
|
|
83
|
+
});
|
|
84
|
+
it('returns empty array when no backend is set', () => {
|
|
85
|
+
expect(shapeTextRuns('Hello', {})).toEqual([]);
|
|
86
|
+
});
|
|
87
|
+
it('returns empty array when backend is advances-only', () => {
|
|
88
|
+
setTextShaperBackend({ measureText: (t: string) => t.length });
|
|
89
|
+
expect(shapeTextRuns('Hello', {})).toEqual([]);
|
|
90
|
+
});
|
|
91
|
+
it('returns one ShapedRun per item for a mixed-script string', () => {
|
|
92
|
+
setTextShaperBackend(_makeShapingBackend());
|
|
93
|
+
const arabic = 'سلام';
|
|
94
|
+
const runs = shapeTextRuns('AB' + arabic, {});
|
|
95
|
+
expect(runs).toHaveLength(2);
|
|
96
|
+
expect(runs[0].direction).toBe('LeftToRight');
|
|
97
|
+
expect(runs[1].direction).toBe('RightToLeft');
|
|
98
|
+
});
|
|
99
|
+
it('passes per-item direction and script to shapeRun', () => {
|
|
100
|
+
const captured: { direction?: string; script?: string }[] = [];
|
|
101
|
+
setTextShaperBackend({
|
|
102
|
+
measureText: () => 0,
|
|
103
|
+
shapeRun: (_t: string, _f: Readonly<TextFormat>, opts?: Readonly<ShapeRunOptions>): ShapedRun => {
|
|
104
|
+
captured.push({ direction: opts?.direction, script: opts?.script });
|
|
105
|
+
return { ..._emptyRun };
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
const arabic = 'سلام';
|
|
109
|
+
shapeTextRuns('AB' + arabic, {});
|
|
110
|
+
expect(captured[0].direction).toBe('LeftToRight');
|
|
111
|
+
expect(captured[1].direction).toBe('RightToLeft');
|
|
112
|
+
expect(captured[1].script).toBe('Arab');
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { acquireShapedRun, releaseShapedRun } from './textShaperPool';
|
|
2
|
+
|
|
3
|
+
describe('acquireShapedRun', () => {
|
|
4
|
+
it('returns a ShapedRun with the expected shape', () => {
|
|
5
|
+
const run = acquireShapedRun();
|
|
6
|
+
expect(typeof run.advanceWidth).toBe('number');
|
|
7
|
+
expect(Array.isArray(run.glyphs)).toBe(true);
|
|
8
|
+
releaseShapedRun(run);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('returns a previously released run from the pool', () => {
|
|
12
|
+
const run = acquireShapedRun();
|
|
13
|
+
releaseShapedRun(run);
|
|
14
|
+
const reacquired = acquireShapedRun();
|
|
15
|
+
// The same object reference should come back when the pool had one entry.
|
|
16
|
+
expect(reacquired).toBe(run);
|
|
17
|
+
releaseShapedRun(reacquired);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('allocates a new run when the pool is empty', () => {
|
|
21
|
+
// Drain any pooled entry.
|
|
22
|
+
const r1 = acquireShapedRun();
|
|
23
|
+
const r2 = acquireShapedRun();
|
|
24
|
+
// Both are valid ShapedRuns.
|
|
25
|
+
expect(r1).not.toBe(r2);
|
|
26
|
+
releaseShapedRun(r1);
|
|
27
|
+
releaseShapedRun(r2);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('releaseShapedRun', () => {
|
|
32
|
+
it('accepts a run without throwing', () => {
|
|
33
|
+
const run = acquireShapedRun();
|
|
34
|
+
expect(() => releaseShapedRun(run)).not.toThrow();
|
|
35
|
+
});
|
|
36
|
+
});
|