@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,196 @@
|
|
|
1
|
+
import type { TextLayoutResult, TextSelectionRectangle } from '@flighthq/types';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
computeRichTextCharIndexAtPoint,
|
|
5
|
+
computeRichTextLineMetrics,
|
|
6
|
+
getRichTextCharBoundaries,
|
|
7
|
+
getRichTextFirstCharInParagraph,
|
|
8
|
+
getRichTextLineIndexAtPoint,
|
|
9
|
+
getRichTextLineIndexOfChar,
|
|
10
|
+
getRichTextLineLength,
|
|
11
|
+
getRichTextLineOffset,
|
|
12
|
+
getRichTextLineText,
|
|
13
|
+
getRichTextLinkAtPoint,
|
|
14
|
+
getRichTextParagraphLength,
|
|
15
|
+
getRichTextSelectionRectangles,
|
|
16
|
+
} from './richTextQuery';
|
|
17
|
+
|
|
18
|
+
// Two-line layout: 'abc' on line 0 (indices 0-3), 'defg' on line 1 (indices 3-7).
|
|
19
|
+
function createLayout(): TextLayoutResult {
|
|
20
|
+
return {
|
|
21
|
+
groups: [
|
|
22
|
+
{
|
|
23
|
+
ascent: 10,
|
|
24
|
+
descent: 2,
|
|
25
|
+
endIndex: 3,
|
|
26
|
+
format: {},
|
|
27
|
+
height: 12,
|
|
28
|
+
leading: 0,
|
|
29
|
+
lineIndex: 0,
|
|
30
|
+
offsetX: 0,
|
|
31
|
+
offsetY: 2,
|
|
32
|
+
positions: [10, 10, 10],
|
|
33
|
+
startIndex: 0,
|
|
34
|
+
width: 30,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
ascent: 10,
|
|
38
|
+
descent: 2,
|
|
39
|
+
endIndex: 7,
|
|
40
|
+
format: { url: 'https://example.com' },
|
|
41
|
+
height: 12,
|
|
42
|
+
leading: 0,
|
|
43
|
+
lineIndex: 1,
|
|
44
|
+
offsetX: 0,
|
|
45
|
+
offsetY: 16,
|
|
46
|
+
positions: [10, 10, 10, 10],
|
|
47
|
+
startIndex: 3,
|
|
48
|
+
width: 40,
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
lineAscents: [10, 10],
|
|
52
|
+
lineDescents: [2, 2],
|
|
53
|
+
lineHeights: [14, 14],
|
|
54
|
+
lineLeadings: [0, 0],
|
|
55
|
+
lineWidths: [30, 40],
|
|
56
|
+
numLines: 2,
|
|
57
|
+
textHeight: 28,
|
|
58
|
+
textWidth: 40,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
describe('computeRichTextCharIndexAtPoint', () => {
|
|
63
|
+
it('returns the index at a point within a group', () => {
|
|
64
|
+
expect(computeRichTextCharIndexAtPoint(createLayout(), 25, 5)).toBe(2);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('selects the closest line by y distance', () => {
|
|
68
|
+
expect(computeRichTextCharIndexAtPoint(createLayout(), 5, 100)).toBe(7);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('computeRichTextLineMetrics', () => {
|
|
73
|
+
it('returns metrics for a valid line', () => {
|
|
74
|
+
const metrics = computeRichTextLineMetrics(createLayout(), 0);
|
|
75
|
+
expect(metrics).not.toBeNull();
|
|
76
|
+
expect(metrics!.ascent).toBe(10);
|
|
77
|
+
expect(metrics!.descent).toBe(2);
|
|
78
|
+
expect(metrics!.width).toBe(30);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('returns null for a non-existent line', () => {
|
|
82
|
+
expect(computeRichTextLineMetrics(createLayout(), 5)).toBeNull();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('getRichTextCharBoundaries', () => {
|
|
87
|
+
it('fills out with the bounding box of the character', () => {
|
|
88
|
+
const out = { x: 0, y: 0, width: 0, height: 0 };
|
|
89
|
+
const found = getRichTextCharBoundaries(out as never, createLayout(), 1);
|
|
90
|
+
expect(found).toBe(true);
|
|
91
|
+
expect(out.x).toBe(10);
|
|
92
|
+
expect(out.y).toBe(2);
|
|
93
|
+
expect(out.width).toBe(10);
|
|
94
|
+
expect(out.height).toBe(12);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('returns false for an out-of-range index', () => {
|
|
98
|
+
const out = { x: 0, y: 0, width: 0, height: 0 };
|
|
99
|
+
expect(getRichTextCharBoundaries(out as never, createLayout(), 99)).toBe(false);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('getRichTextFirstCharInParagraph', () => {
|
|
104
|
+
it('returns 0 at the start of the first paragraph', () => {
|
|
105
|
+
expect(getRichTextFirstCharInParagraph('hello\nworld', 3)).toBe(0);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('returns the position after the newline for a later paragraph', () => {
|
|
109
|
+
expect(getRichTextFirstCharInParagraph('hello\nworld', 8)).toBe(6);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('getRichTextLineIndexAtPoint', () => {
|
|
114
|
+
it('returns 0 for a y value within the first line', () => {
|
|
115
|
+
expect(getRichTextLineIndexAtPoint(createLayout(), 5)).toBe(0);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('returns 1 for a y value within the second line', () => {
|
|
119
|
+
expect(getRichTextLineIndexAtPoint(createLayout(), 18)).toBe(1);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe('getRichTextLineIndexOfChar', () => {
|
|
124
|
+
it('returns 0 for a character on the first line', () => {
|
|
125
|
+
expect(getRichTextLineIndexOfChar(createLayout(), 1)).toBe(0);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('returns 1 for a character on the second line', () => {
|
|
129
|
+
expect(getRichTextLineIndexOfChar(createLayout(), 5)).toBe(1);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe('getRichTextLineLength', () => {
|
|
134
|
+
it('returns the character count of the requested line', () => {
|
|
135
|
+
expect(getRichTextLineLength(createLayout(), 0)).toBe(3);
|
|
136
|
+
expect(getRichTextLineLength(createLayout(), 1)).toBe(4);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('returns 0 for a non-existent line', () => {
|
|
140
|
+
expect(getRichTextLineLength(createLayout(), 5)).toBe(0);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe('getRichTextLineOffset', () => {
|
|
145
|
+
it('returns the start index of the requested line', () => {
|
|
146
|
+
expect(getRichTextLineOffset(createLayout(), 0)).toBe(0);
|
|
147
|
+
expect(getRichTextLineOffset(createLayout(), 1)).toBe(3);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe('getRichTextLineText', () => {
|
|
152
|
+
it('returns the text slice for the requested line', () => {
|
|
153
|
+
expect(getRichTextLineText('abcdefg', createLayout(), 0)).toBe('abc');
|
|
154
|
+
expect(getRichTextLineText('abcdefg', createLayout(), 1)).toBe('defg');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('returns empty string for a non-existent line', () => {
|
|
158
|
+
expect(getRichTextLineText('abcdefg', createLayout(), 5)).toBe('');
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe('getRichTextLinkAtPoint', () => {
|
|
163
|
+
it('returns the url when the point hits a group with a url', () => {
|
|
164
|
+
expect(getRichTextLinkAtPoint(createLayout(), 10, 18)).toBe('https://example.com');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('returns null when the point does not hit a linked group', () => {
|
|
168
|
+
expect(getRichTextLinkAtPoint(createLayout(), 10, 5)).toBeNull();
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe('getRichTextParagraphLength', () => {
|
|
173
|
+
it('returns the length including the newline', () => {
|
|
174
|
+
expect(getRichTextParagraphLength('hello\nworld', 3)).toBe(6);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('returns to end of string when there is no trailing newline', () => {
|
|
178
|
+
expect(getRichTextParagraphLength('hello\nworld', 8)).toBe(5);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
describe('getRichTextSelectionRectangles', () => {
|
|
183
|
+
it('fills out with one rectangle per intersected group', () => {
|
|
184
|
+
const out: TextSelectionRectangle[] = [];
|
|
185
|
+
getRichTextSelectionRectangles(out, 1, 5, createLayout());
|
|
186
|
+
expect(out).toHaveLength(2);
|
|
187
|
+
expect(out[0].lineIndex).toBe(0);
|
|
188
|
+
expect(out[1].lineIndex).toBe(1);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('returns empty for a collapsed selection', () => {
|
|
192
|
+
const out: TextSelectionRectangle[] = [];
|
|
193
|
+
getRichTextSelectionRectangles(out, 2, 2, createLayout());
|
|
194
|
+
expect(out).toHaveLength(0);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { RectangleLike, TextLayoutResult } from '@flighthq/types';
|
|
2
|
+
|
|
3
|
+
import type { TextBoundsSpec } from './textBounds';
|
|
4
|
+
import {
|
|
5
|
+
computeTextBoundsHeight,
|
|
6
|
+
computeTextBoundsOffsetX,
|
|
7
|
+
computeTextBoundsRectangle,
|
|
8
|
+
computeTextBoundsWidth,
|
|
9
|
+
TEXT_BOUNDS_GUTTER,
|
|
10
|
+
} from './textBounds';
|
|
11
|
+
import { TEXT_LAYOUT_GUTTER } from './textLayout';
|
|
12
|
+
|
|
13
|
+
function createSpec(spec: Partial<TextBoundsSpec> = {}): TextBoundsSpec {
|
|
14
|
+
return {
|
|
15
|
+
autoSize: 'none',
|
|
16
|
+
height: 100,
|
|
17
|
+
width: 200,
|
|
18
|
+
wordWrap: false,
|
|
19
|
+
...spec,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function createLayout(layout: Partial<TextLayoutResult> = {}): TextLayoutResult {
|
|
24
|
+
return {
|
|
25
|
+
groups: [],
|
|
26
|
+
lineAscents: [],
|
|
27
|
+
lineDescents: [],
|
|
28
|
+
lineHeights: [],
|
|
29
|
+
lineLeadings: [],
|
|
30
|
+
lineWidths: [],
|
|
31
|
+
numLines: 1,
|
|
32
|
+
textHeight: 20,
|
|
33
|
+
textWidth: 50,
|
|
34
|
+
...layout,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe('computeTextBoundsHeight', () => {
|
|
39
|
+
it('uses spec height when autoSize is none', () => {
|
|
40
|
+
expect(computeTextBoundsHeight(createSpec({ autoSize: 'none', height: 80 }), createLayout())).toBe(80);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('uses text height plus gutters when autoSize is enabled', () => {
|
|
44
|
+
expect(computeTextBoundsHeight(createSpec({ autoSize: 'left' }), createLayout({ textHeight: 18 }))).toBe(22);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('computeTextBoundsOffsetX', () => {
|
|
49
|
+
it('returns zero for left and none anchoring', () => {
|
|
50
|
+
expect(
|
|
51
|
+
computeTextBoundsOffsetX(createSpec({ autoSize: 'left', width: 120 }), createLayout({ textWidth: 30 })),
|
|
52
|
+
).toBe(0);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('shifts the box fully into the slack for right anchoring', () => {
|
|
56
|
+
expect(
|
|
57
|
+
computeTextBoundsOffsetX(createSpec({ autoSize: 'right', width: 120 }), createLayout({ textWidth: 30 })),
|
|
58
|
+
).toBe(86);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('splits the slack for center anchoring', () => {
|
|
62
|
+
expect(
|
|
63
|
+
computeTextBoundsOffsetX(createSpec({ autoSize: 'center', width: 120 }), createLayout({ textWidth: 30 })),
|
|
64
|
+
).toBe(43);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('computeTextBoundsRectangle', () => {
|
|
69
|
+
it('fills the declared box at the origin when autoSize is none', () => {
|
|
70
|
+
const out: RectangleLike = { height: 0, width: 0, x: 0, y: 0 };
|
|
71
|
+
computeTextBoundsRectangle(out, createSpec({ autoSize: 'none', width: 120, height: 80 }), createLayout());
|
|
72
|
+
expect(out).toEqual({ height: 80, width: 120, x: 0, y: 0 });
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('fills the measured box with the anchor offset under autoSize', () => {
|
|
76
|
+
const out: RectangleLike = { height: 0, width: 0, x: 0, y: 0 };
|
|
77
|
+
computeTextBoundsRectangle(
|
|
78
|
+
out,
|
|
79
|
+
createSpec({ autoSize: 'right', width: 120 }),
|
|
80
|
+
createLayout({ textWidth: 30, textHeight: 18 }),
|
|
81
|
+
);
|
|
82
|
+
expect(out).toEqual({ height: 22, width: 34, x: 86, y: 0 });
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('computeTextBoundsWidth', () => {
|
|
87
|
+
it('uses spec width when autoSize is none', () => {
|
|
88
|
+
expect(computeTextBoundsWidth(createSpec({ autoSize: 'none', width: 120 }), createLayout())).toBe(120);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('uses spec width when wordWrap is enabled', () => {
|
|
92
|
+
expect(computeTextBoundsWidth(createSpec({ autoSize: 'left', width: 120, wordWrap: true }), createLayout())).toBe(
|
|
93
|
+
120,
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('uses text width plus gutters when autoSize is enabled', () => {
|
|
98
|
+
expect(computeTextBoundsWidth(createSpec({ autoSize: 'left' }), createLayout({ textWidth: 30 }))).toBe(34);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('TEXT_BOUNDS_GUTTER', () => {
|
|
103
|
+
it('equals TEXT_LAYOUT_GUTTER (de-duplicated constant)', () => {
|
|
104
|
+
expect(TEXT_BOUNDS_GUTTER).toBe(TEXT_LAYOUT_GUTTER);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('is a positive number', () => {
|
|
108
|
+
expect(TEXT_BOUNDS_GUTTER).toBeGreaterThan(0);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getTextFormatAscent,
|
|
3
|
+
getTextFormatDescent,
|
|
4
|
+
getTextFormatHeight,
|
|
5
|
+
getTextFormatLeading,
|
|
6
|
+
mergeTextFormat,
|
|
7
|
+
} from './textFormat';
|
|
8
|
+
|
|
9
|
+
describe('getTextFormatAscent', () => {
|
|
10
|
+
it('returns size when specified', () => {
|
|
11
|
+
expect(getTextFormatAscent({ size: 24 })).toBe(24);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('returns 12 when size is absent', () => {
|
|
15
|
+
expect(getTextFormatAscent({})).toBe(12);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('getTextFormatDescent', () => {
|
|
20
|
+
it('returns 18.5% of size', () => {
|
|
21
|
+
expect(getTextFormatDescent({ size: 100 })).toBeCloseTo(18.5);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('uses default size of 12 when absent', () => {
|
|
25
|
+
expect(getTextFormatDescent({})).toBeCloseTo(12 * 0.185);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('getTextFormatHeight', () => {
|
|
30
|
+
it('sums ascent, descent and leading', () => {
|
|
31
|
+
expect(getTextFormatHeight({ size: 10, leading: 4 })).toBeCloseTo(10 + 10 * 0.185 + 4);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('treats absent leading as zero', () => {
|
|
35
|
+
expect(getTextFormatHeight({ size: 10 })).toBeCloseTo(10 + 10 * 0.185);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('getTextFormatLeading', () => {
|
|
40
|
+
it('returns leading when specified', () => {
|
|
41
|
+
expect(getTextFormatLeading({ leading: 6 })).toBe(6);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('returns 0 when absent', () => {
|
|
45
|
+
expect(getTextFormatLeading({})).toBe(0);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('mergeTextFormat', () => {
|
|
50
|
+
it('applies non-null override fields onto base', () => {
|
|
51
|
+
const result = mergeTextFormat({ size: 12, bold: false }, { bold: true, color: 0xff0000 });
|
|
52
|
+
expect(result).toMatchObject({ size: 12, bold: true, color: 0xff0000 });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('skips null and undefined override fields', () => {
|
|
56
|
+
const result = mergeTextFormat({ size: 12 }, { size: undefined, bold: undefined });
|
|
57
|
+
expect(result.size).toBe(12);
|
|
58
|
+
expect(result.bold).toBeUndefined();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('does not mutate the base object', () => {
|
|
62
|
+
const base = { size: 12 };
|
|
63
|
+
mergeTextFormat(base, { size: 24 });
|
|
64
|
+
expect(base.size).toBe(12);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createTextFormatRange } from './textFormatRange';
|
|
2
|
+
|
|
3
|
+
describe('createTextFormatRange', () => {
|
|
4
|
+
it('creates a range with the given fields', () => {
|
|
5
|
+
const fmt = { size: 16, bold: true };
|
|
6
|
+
const range = createTextFormatRange(fmt, 0, 10);
|
|
7
|
+
expect(range).toEqual({ format: fmt, start: 0, end: 10 });
|
|
8
|
+
});
|
|
9
|
+
});
|