@flighthq/textlayout 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/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/richTextContent.d.ts +6 -0
- package/dist/richTextContent.d.ts.map +1 -0
- package/dist/richTextContent.js +435 -0
- package/dist/richTextContent.js.map +1 -0
- package/dist/richTextMetrics.d.ts +9 -0
- package/dist/richTextMetrics.d.ts.map +1 -0
- package/dist/richTextMetrics.js +44 -0
- package/dist/richTextMetrics.js.map +1 -0
- package/dist/richTextQuery.d.ts +14 -0
- package/dist/richTextQuery.d.ts.map +1 -0
- package/dist/richTextQuery.js +205 -0
- package/dist/richTextQuery.js.map +1 -0
- package/dist/textBounds.d.ts +13 -0
- package/dist/textBounds.d.ts.map +1 -0
- package/dist/textBounds.js +42 -0
- package/dist/textBounds.js.map +1 -0
- package/dist/textFormat.d.ts +7 -0
- package/dist/textFormat.d.ts.map +1 -0
- package/dist/textFormat.js +24 -0
- package/dist/textFormat.js.map +1 -0
- package/dist/textFormatRange.d.ts +3 -0
- package/dist/textFormatRange.d.ts.map +1 -0
- package/dist/textFormatRange.js +4 -0
- package/dist/textFormatRange.js.map +1 -0
- package/dist/textLayout.d.ts +8 -0
- package/dist/textLayout.d.ts.map +1 -0
- package/dist/textLayout.js +632 -0
- package/dist/textLayout.js.map +1 -0
- package/dist/textLayoutGroup.d.ts +3 -0
- package/dist/textLayoutGroup.d.ts.map +1 -0
- package/dist/textLayoutGroup.js +17 -0
- package/dist/textLayoutGroup.js.map +1 -0
- package/dist/textLayoutMeasure.d.ts +4 -0
- package/dist/textLayoutMeasure.d.ts.map +1 -0
- package/dist/textLayoutMeasure.js +23 -0
- package/dist/textLayoutMeasure.js.map +1 -0
- package/dist/textLayoutRuntime.d.ts +4 -0
- package/dist/textLayoutRuntime.d.ts.map +1 -0
- package/dist/textLayoutRuntime.js +11 -0
- package/dist/textLayoutRuntime.js.map +1 -0
- package/dist/textLineBreaks.d.ts +8 -0
- package/dist/textLineBreaks.d.ts.map +1 -0
- package/dist/textLineBreaks.js +36 -0
- package/dist/textLineBreaks.js.map +1 -0
- package/dist/textMetrics.d.ts +4 -0
- package/dist/textMetrics.d.ts.map +1 -0
- package/dist/textMetrics.js +12 -0
- package/dist/textMetrics.js.map +1 -0
- package/package.json +38 -0
- package/src/richTextContent.test.ts +201 -0
- package/src/richTextMetrics.test.ts +124 -0
- package/src/richTextQuery.test.ts +196 -0
- package/src/textBounds.test.ts +110 -0
- package/src/textFormat.test.ts +66 -0
- package/src/textFormatRange.test.ts +9 -0
- package/src/textLayout.test.ts +661 -0
- package/src/textLayoutGroup.test.ts +30 -0
- package/src/textLayoutMeasure.test.ts +36 -0
- package/src/textLayoutRuntime.test.ts +30 -0
- package/src/textLineBreaks.test.ts +73 -0
- package/src/textMetrics.test.ts +19 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { measureText, setTextShaperBackend } from '@flighthq/textshaper';
|
|
2
|
+
|
|
3
|
+
import { getTextLayoutMeasureProvider, setTextLayoutMeasureProvider } from './textLayoutMeasure';
|
|
4
|
+
|
|
5
|
+
afterEach(() => {
|
|
6
|
+
setTextLayoutMeasureProvider(null);
|
|
7
|
+
setTextShaperBackend(null);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
describe('getTextLayoutMeasureProvider', () => {
|
|
11
|
+
it('returns null before a provider or shaper backend is set', () => {
|
|
12
|
+
expect(getTextLayoutMeasureProvider()).toBeNull();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('falls back to the textshaper seam when a backend is registered', () => {
|
|
16
|
+
setTextShaperBackend({ measureText: (text) => text.length });
|
|
17
|
+
expect(getTextLayoutMeasureProvider()).toBe(measureText);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('prefers an explicitly set provider over the shaper backend', () => {
|
|
21
|
+
const measure = (text: string) => text.length;
|
|
22
|
+
setTextShaperBackend({ measureText: () => 99 });
|
|
23
|
+
setTextLayoutMeasureProvider(measure);
|
|
24
|
+
expect(getTextLayoutMeasureProvider()).toBe(measure);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('setTextLayoutMeasureProvider', () => {
|
|
29
|
+
it('stores the provider and clears it with null', () => {
|
|
30
|
+
const measure = (text: string) => text.length;
|
|
31
|
+
setTextLayoutMeasureProvider(measure);
|
|
32
|
+
expect(getTextLayoutMeasureProvider()).toBe(measure);
|
|
33
|
+
setTextLayoutMeasureProvider(null);
|
|
34
|
+
expect(getTextLayoutMeasureProvider()).toBeNull();
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { TextLabelRuntime } from '@flighthq/types';
|
|
2
|
+
|
|
3
|
+
import { clearTextLayoutResult, getTextLayoutResult } from './textLayoutRuntime';
|
|
4
|
+
|
|
5
|
+
function createRuntime(): TextLabelRuntime {
|
|
6
|
+
return { textLayout: null } as TextLabelRuntime;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe('clearTextLayoutResult', () => {
|
|
10
|
+
it('clears the attached layout result', () => {
|
|
11
|
+
const runtime = createRuntime();
|
|
12
|
+
getTextLayoutResult(runtime);
|
|
13
|
+
clearTextLayoutResult(runtime);
|
|
14
|
+
expect(runtime.textLayout).toBeNull();
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe('getTextLayoutResult', () => {
|
|
19
|
+
it('attaches a layout result to runtime state', () => {
|
|
20
|
+
const runtime = createRuntime();
|
|
21
|
+
const result = getTextLayoutResult(runtime);
|
|
22
|
+
expect(runtime.textLayout).toBe(result);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('reuses an existing layout result', () => {
|
|
26
|
+
const runtime = createRuntime();
|
|
27
|
+
const result = getTextLayoutResult(runtime);
|
|
28
|
+
expect(getTextLayoutResult(runtime)).toBe(result);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { getTextLineBreakIndex, getTextLineBreaks } from './textLineBreaks';
|
|
2
|
+
|
|
3
|
+
describe('getTextLineBreakIndex', () => {
|
|
4
|
+
it('returns the first break at or after startIndex', () => {
|
|
5
|
+
expect(getTextLineBreakIndex([3, 7, 12], 5)).toBe(7);
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
it('returns the first break when startIndex is 0', () => {
|
|
9
|
+
expect(getTextLineBreakIndex([3, 7], 0)).toBe(3);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('returns an exact match when startIndex equals a break', () => {
|
|
13
|
+
expect(getTextLineBreakIndex([3, 7, 12], 7)).toBe(7);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('returns -1 when no break is at or after startIndex', () => {
|
|
17
|
+
expect(getTextLineBreakIndex([3, 7], 10)).toBe(-1);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('returns -1 for empty break list', () => {
|
|
21
|
+
expect(getTextLineBreakIndex([], 0)).toBe(-1);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('works correctly with a single-element array', () => {
|
|
25
|
+
expect(getTextLineBreakIndex([5], 3)).toBe(5);
|
|
26
|
+
expect(getTextLineBreakIndex([5], 5)).toBe(5);
|
|
27
|
+
expect(getTextLineBreakIndex([5], 6)).toBe(-1);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('correctly finds the first break in a large sorted array (binary search)', () => {
|
|
31
|
+
const breaks = Array.from({ length: 100 }, (_, i) => i * 10);
|
|
32
|
+
expect(getTextLineBreakIndex(breaks, 55)).toBe(60);
|
|
33
|
+
expect(getTextLineBreakIndex(breaks, 60)).toBe(60);
|
|
34
|
+
expect(getTextLineBreakIndex(breaks, 0)).toBe(0);
|
|
35
|
+
expect(getTextLineBreakIndex(breaks, 999)).toBe(-1);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('getTextLineBreaks', () => {
|
|
40
|
+
const out: number[] = [];
|
|
41
|
+
|
|
42
|
+
it('returns empty array for text with no line breaks', () => {
|
|
43
|
+
getTextLineBreaks(out, 'hello world');
|
|
44
|
+
expect(out).toEqual([]);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('finds LF positions', () => {
|
|
48
|
+
getTextLineBreaks(out, 'a\nb\nc');
|
|
49
|
+
expect(out).toEqual([1, 3]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('finds CR positions', () => {
|
|
53
|
+
getTextLineBreaks(out, 'a\rb\rc');
|
|
54
|
+
expect(out).toEqual([1, 3]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('prefers the earlier of CR and LF when both are present', () => {
|
|
58
|
+
getTextLineBreaks(out, 'a\nb\rc');
|
|
59
|
+
expect(out).toEqual([1, 3]);
|
|
60
|
+
getTextLineBreaks(out, 'a\rb\nc');
|
|
61
|
+
expect(out).toEqual([1, 3]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('handles trailing newline', () => {
|
|
65
|
+
getTextLineBreaks(out, 'ab\n');
|
|
66
|
+
expect(out).toEqual([2]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('returns empty array for empty text', () => {
|
|
70
|
+
getTextLineBreaks(out, '');
|
|
71
|
+
expect(out).toEqual([]);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { TextLayoutResult } from '@flighthq/types';
|
|
2
|
+
|
|
3
|
+
import { createTextMetrics, getTextMetrics } from './textMetrics';
|
|
4
|
+
|
|
5
|
+
describe('createTextMetrics', () => {
|
|
6
|
+
it('creates a zeroed metrics object', () => {
|
|
7
|
+
expect(createTextMetrics()).toEqual({ height: 0, numLines: 0, width: 0 });
|
|
8
|
+
});
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
describe('getTextMetrics', () => {
|
|
12
|
+
it('fills the measured content size from a layout, ceiling fractional extents', () => {
|
|
13
|
+
const out = createTextMetrics();
|
|
14
|
+
getTextMetrics(out, { numLines: 2, textHeight: 8.7, textWidth: 12.2 } as unknown as TextLayoutResult);
|
|
15
|
+
expect(out.width).toBe(13);
|
|
16
|
+
expect(out.height).toBe(9);
|
|
17
|
+
expect(out.numLines).toBe(2);
|
|
18
|
+
});
|
|
19
|
+
});
|