@santjc/react-pretext 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/README.md +347 -0
- package/dist/chunk-4TMIB6R6.js +51 -0
- package/dist/chunk-6P7OEVAC.js +51 -0
- package/dist/editorial.d.ts +163 -0
- package/dist/editorial.js +684 -0
- package/dist/index.d.ts +157 -0
- package/dist/index.js +393 -0
- package/dist/pretext.d.ts +1 -0
- package/dist/pretext.js +2 -0
- package/dist/usePreparedText-DmUr2kss.d.ts +20 -0
- package/package.json +58 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { P as PrepareOptions, u as usePreparedText } from './usePreparedText-DmUr2kss.js';
|
|
2
|
+
export { U as UsePreparedTextInput, a as UsePreparedTextResult } from './usePreparedText-DmUr2kss.js';
|
|
3
|
+
import * as react from 'react';
|
|
4
|
+
import { CSSProperties } from 'react';
|
|
5
|
+
import { PreparedTextWithSegments, PreparedText } from '@chenglou/pretext';
|
|
6
|
+
|
|
7
|
+
type ElementWidthResult<T extends HTMLElement> = {
|
|
8
|
+
ref: (node: T | null) => void;
|
|
9
|
+
width: number;
|
|
10
|
+
node: T | null;
|
|
11
|
+
};
|
|
12
|
+
declare function useElementWidth<T extends HTMLElement = HTMLDivElement>(): ElementWidthResult<T>;
|
|
13
|
+
|
|
14
|
+
type PretextTypographyShorthandInput = {
|
|
15
|
+
font: string;
|
|
16
|
+
lineHeight: number;
|
|
17
|
+
width?: number;
|
|
18
|
+
family?: never;
|
|
19
|
+
size?: never;
|
|
20
|
+
weight?: never;
|
|
21
|
+
};
|
|
22
|
+
type PretextTypographyObjectInput = {
|
|
23
|
+
family: string;
|
|
24
|
+
size: number;
|
|
25
|
+
weight?: number | string;
|
|
26
|
+
lineHeight: number;
|
|
27
|
+
width?: number;
|
|
28
|
+
font?: never;
|
|
29
|
+
};
|
|
30
|
+
type PretextTypographyInput = PretextTypographyShorthandInput | PretextTypographyObjectInput;
|
|
31
|
+
type PretextTypography = {
|
|
32
|
+
font: string;
|
|
33
|
+
lineHeight: number;
|
|
34
|
+
width?: number;
|
|
35
|
+
style: CSSProperties;
|
|
36
|
+
};
|
|
37
|
+
declare function createPretextTypography(input: PretextTypographyInput): PretextTypography;
|
|
38
|
+
|
|
39
|
+
type UseMeasuredTextInput = {
|
|
40
|
+
text: string;
|
|
41
|
+
width?: number;
|
|
42
|
+
font?: string;
|
|
43
|
+
lineHeight?: number;
|
|
44
|
+
typography?: PretextTypography;
|
|
45
|
+
prepareOptions?: PrepareOptions;
|
|
46
|
+
enableProfiling?: boolean;
|
|
47
|
+
enabled?: boolean;
|
|
48
|
+
};
|
|
49
|
+
type UseMeasuredTextResult = {
|
|
50
|
+
prepared: ReturnType<typeof usePreparedText>['prepared'];
|
|
51
|
+
height: number;
|
|
52
|
+
lineCount: number;
|
|
53
|
+
prepareMs?: number;
|
|
54
|
+
isReady: boolean;
|
|
55
|
+
};
|
|
56
|
+
declare function useMeasuredText({ text, width, font, lineHeight, typography, prepareOptions, enableProfiling, enabled, }: UseMeasuredTextInput): UseMeasuredTextResult;
|
|
57
|
+
|
|
58
|
+
type UsePreparedSegmentsInput = {
|
|
59
|
+
text: string;
|
|
60
|
+
font: string;
|
|
61
|
+
options?: PrepareOptions;
|
|
62
|
+
enabled?: boolean;
|
|
63
|
+
};
|
|
64
|
+
type UsePreparedSegmentsResult = {
|
|
65
|
+
prepared: PreparedTextWithSegments | null;
|
|
66
|
+
isReady: boolean;
|
|
67
|
+
};
|
|
68
|
+
declare function usePreparedSegments({ text, font, options, enabled }: UsePreparedSegmentsInput): UsePreparedSegmentsResult;
|
|
69
|
+
|
|
70
|
+
type UsePretextLayoutInput = {
|
|
71
|
+
prepared: PreparedText | null;
|
|
72
|
+
width: number;
|
|
73
|
+
lineHeight: number;
|
|
74
|
+
enabled?: boolean;
|
|
75
|
+
};
|
|
76
|
+
type UsePretextLayoutResult = {
|
|
77
|
+
height: number;
|
|
78
|
+
lineCount: number;
|
|
79
|
+
isReady: boolean;
|
|
80
|
+
};
|
|
81
|
+
declare function usePretextLayout({ prepared, width, lineHeight, enabled }: UsePretextLayoutInput): UsePretextLayoutResult;
|
|
82
|
+
|
|
83
|
+
type UsePretextLinesInput = {
|
|
84
|
+
prepared: PreparedTextWithSegments | null;
|
|
85
|
+
width: number;
|
|
86
|
+
lineHeight: number;
|
|
87
|
+
enabled?: boolean;
|
|
88
|
+
};
|
|
89
|
+
type UsePretextLinesResult = {
|
|
90
|
+
height: number;
|
|
91
|
+
lineCount: number;
|
|
92
|
+
lines: Array<{
|
|
93
|
+
text: string;
|
|
94
|
+
width: number;
|
|
95
|
+
start: {
|
|
96
|
+
segmentIndex: number;
|
|
97
|
+
graphemeIndex: number;
|
|
98
|
+
};
|
|
99
|
+
end: {
|
|
100
|
+
segmentIndex: number;
|
|
101
|
+
graphemeIndex: number;
|
|
102
|
+
};
|
|
103
|
+
}>;
|
|
104
|
+
isReady: boolean;
|
|
105
|
+
};
|
|
106
|
+
declare function usePretextLines({ prepared, width, lineHeight, enabled }: UsePretextLinesInput): UsePretextLinesResult;
|
|
107
|
+
|
|
108
|
+
type UseTruncatedTextInput = {
|
|
109
|
+
text: string;
|
|
110
|
+
width?: number;
|
|
111
|
+
font?: string;
|
|
112
|
+
lineHeight?: number;
|
|
113
|
+
typography?: PretextTypography;
|
|
114
|
+
prepareOptions?: PrepareOptions;
|
|
115
|
+
maxLines: number;
|
|
116
|
+
ellipsis?: string;
|
|
117
|
+
enabled?: boolean;
|
|
118
|
+
};
|
|
119
|
+
type UseTruncatedTextResult = {
|
|
120
|
+
text: string;
|
|
121
|
+
didTruncate: boolean;
|
|
122
|
+
visibleLineCount: number;
|
|
123
|
+
fullLineCount: number;
|
|
124
|
+
height: number;
|
|
125
|
+
isReady: boolean;
|
|
126
|
+
};
|
|
127
|
+
declare function useTruncatedText({ text, width, font, lineHeight, typography, prepareOptions, maxLines, ellipsis, enabled, }: UseTruncatedTextInput): UseTruncatedTextResult;
|
|
128
|
+
|
|
129
|
+
type PTextTag = 'p' | 'div' | 'span' | 'h1' | 'h2' | 'h3';
|
|
130
|
+
type PTextMeasure = {
|
|
131
|
+
width: number;
|
|
132
|
+
height: number;
|
|
133
|
+
lineCount: number;
|
|
134
|
+
};
|
|
135
|
+
type PTextOwnProps = {
|
|
136
|
+
as?: PTextTag;
|
|
137
|
+
children: string;
|
|
138
|
+
prepareOptions?: PrepareOptions;
|
|
139
|
+
onMeasure?: (result: PTextMeasure) => void;
|
|
140
|
+
};
|
|
141
|
+
type PTextExplicitMeasureProps = {
|
|
142
|
+
font: string;
|
|
143
|
+
lineHeight: number;
|
|
144
|
+
width?: number;
|
|
145
|
+
typography?: PretextTypography;
|
|
146
|
+
};
|
|
147
|
+
type PTextTypographyProps = {
|
|
148
|
+
typography: PretextTypography;
|
|
149
|
+
font?: string;
|
|
150
|
+
lineHeight?: number;
|
|
151
|
+
width?: number;
|
|
152
|
+
};
|
|
153
|
+
type PTextProps = PTextOwnProps & (PTextExplicitMeasureProps | PTextTypographyProps) & React.HTMLAttributes<HTMLElement>;
|
|
154
|
+
type PTextElement = HTMLParagraphElement | HTMLDivElement | HTMLSpanElement | HTMLHeadingElement;
|
|
155
|
+
declare const PText: react.ForwardRefExoticComponent<PTextProps & react.RefAttributes<PTextElement>>;
|
|
156
|
+
|
|
157
|
+
export { type ElementWidthResult, PText, type PTextMeasure, type PTextProps, type PTextTag, PrepareOptions, type PretextTypography, type PretextTypographyInput, type PretextTypographyObjectInput, type PretextTypographyShorthandInput, type UseMeasuredTextInput, type UseMeasuredTextResult, type UsePreparedSegmentsInput, type UsePreparedSegmentsResult, type UsePretextLayoutInput, type UsePretextLayoutResult, type UsePretextLinesInput, type UsePretextLinesResult, type UseTruncatedTextInput, type UseTruncatedTextResult, createPretextTypography, useElementWidth, useMeasuredText, usePreparedSegments, usePreparedText, usePretextLayout, usePretextLines, useTruncatedText };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useElementWidth,
|
|
3
|
+
usePreparedSegments
|
|
4
|
+
} from "./chunk-6P7OEVAC.js";
|
|
5
|
+
|
|
6
|
+
// src/core/hooks/usePreparedText.ts
|
|
7
|
+
import { prepare, profilePrepare } from "@chenglou/pretext";
|
|
8
|
+
import { useMemo } from "react";
|
|
9
|
+
function usePreparedText({ text, font, options, enableProfiling = false, enabled = true }) {
|
|
10
|
+
const whiteSpace = options?.whiteSpace;
|
|
11
|
+
return useMemo(() => {
|
|
12
|
+
if (!enabled || text.length === 0 || font.length === 0) {
|
|
13
|
+
return {
|
|
14
|
+
prepared: null,
|
|
15
|
+
isReady: false
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
const normalizedOptions = whiteSpace === void 0 ? void 0 : { whiteSpace };
|
|
19
|
+
return {
|
|
20
|
+
prepared: prepare(text, font, normalizedOptions),
|
|
21
|
+
...enableProfiling ? { prepareMs: profilePrepare(text, font, normalizedOptions).totalMs } : {},
|
|
22
|
+
isReady: true
|
|
23
|
+
};
|
|
24
|
+
}, [enableProfiling, enabled, font, text, whiteSpace]);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// src/core/hooks/usePretextLayout.ts
|
|
28
|
+
import { layout } from "@chenglou/pretext";
|
|
29
|
+
import { useMemo as useMemo2 } from "react";
|
|
30
|
+
function usePretextLayout({ prepared, width, lineHeight, enabled = true }) {
|
|
31
|
+
return useMemo2(() => {
|
|
32
|
+
if (!enabled || prepared === null || width <= 0) {
|
|
33
|
+
return {
|
|
34
|
+
height: 0,
|
|
35
|
+
lineCount: 0,
|
|
36
|
+
isReady: false
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const result = layout(prepared, width, lineHeight);
|
|
40
|
+
return {
|
|
41
|
+
height: result.height,
|
|
42
|
+
lineCount: result.lineCount,
|
|
43
|
+
isReady: true
|
|
44
|
+
};
|
|
45
|
+
}, [enabled, lineHeight, prepared, width]);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// src/core/hooks/useMeasuredText.ts
|
|
49
|
+
function useMeasuredText({
|
|
50
|
+
text,
|
|
51
|
+
width,
|
|
52
|
+
font,
|
|
53
|
+
lineHeight,
|
|
54
|
+
typography,
|
|
55
|
+
prepareOptions,
|
|
56
|
+
enableProfiling = false,
|
|
57
|
+
enabled = true
|
|
58
|
+
}) {
|
|
59
|
+
const resolvedFont = font ?? typography?.font;
|
|
60
|
+
const resolvedLineHeight = lineHeight ?? typography?.lineHeight;
|
|
61
|
+
const resolvedWidth = width ?? typography?.width;
|
|
62
|
+
if (resolvedFont === void 0 || resolvedLineHeight === void 0) {
|
|
63
|
+
throw new Error("useMeasuredText requires `font` and `lineHeight`, either directly or via `typography`.");
|
|
64
|
+
}
|
|
65
|
+
if (resolvedWidth === void 0) {
|
|
66
|
+
throw new Error("useMeasuredText requires `width`, either directly or via `typography`.");
|
|
67
|
+
}
|
|
68
|
+
const prepared = usePreparedText({
|
|
69
|
+
text,
|
|
70
|
+
font: resolvedFont,
|
|
71
|
+
options: prepareOptions,
|
|
72
|
+
enableProfiling,
|
|
73
|
+
enabled
|
|
74
|
+
});
|
|
75
|
+
const layout3 = usePretextLayout({
|
|
76
|
+
prepared: prepared.prepared,
|
|
77
|
+
width: resolvedWidth,
|
|
78
|
+
lineHeight: resolvedLineHeight,
|
|
79
|
+
enabled
|
|
80
|
+
});
|
|
81
|
+
return {
|
|
82
|
+
prepared: prepared.prepared,
|
|
83
|
+
height: layout3.height,
|
|
84
|
+
lineCount: layout3.lineCount,
|
|
85
|
+
prepareMs: prepared.prepareMs,
|
|
86
|
+
isReady: layout3.isReady
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// src/core/hooks/usePretextLines.ts
|
|
91
|
+
import { layoutWithLines } from "@chenglou/pretext";
|
|
92
|
+
import { useMemo as useMemo3 } from "react";
|
|
93
|
+
function usePretextLines({ prepared, width, lineHeight, enabled = true }) {
|
|
94
|
+
return useMemo3(() => {
|
|
95
|
+
if (!enabled || prepared === null || width <= 0) {
|
|
96
|
+
return {
|
|
97
|
+
height: 0,
|
|
98
|
+
lineCount: 0,
|
|
99
|
+
lines: [],
|
|
100
|
+
isReady: false
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
const result = layoutWithLines(prepared, width, lineHeight);
|
|
104
|
+
return {
|
|
105
|
+
height: result.height,
|
|
106
|
+
lineCount: result.lineCount,
|
|
107
|
+
lines: result.lines,
|
|
108
|
+
isReady: true
|
|
109
|
+
};
|
|
110
|
+
}, [enabled, lineHeight, prepared, width]);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// src/core/hooks/useTruncatedText.ts
|
|
114
|
+
import { layout as layout2, prepare as prepare2 } from "@chenglou/pretext";
|
|
115
|
+
import { useMemo as useMemo4 } from "react";
|
|
116
|
+
var graphemeSegmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
|
|
117
|
+
var wordLikePattern = /[\p{L}\p{N}\p{M}]/u;
|
|
118
|
+
var whitespacePattern = /\s/u;
|
|
119
|
+
function getSegmentGraphemes(segment, cache) {
|
|
120
|
+
const cached = cache.get(segment);
|
|
121
|
+
if (cached !== void 0) {
|
|
122
|
+
return cached;
|
|
123
|
+
}
|
|
124
|
+
const graphemes = Array.from(graphemeSegmenter.segment(segment), ({ segment: grapheme }) => grapheme);
|
|
125
|
+
cache.set(segment, graphemes);
|
|
126
|
+
return graphemes;
|
|
127
|
+
}
|
|
128
|
+
function getTextUpToCursor(prepared, end) {
|
|
129
|
+
const graphemeCache = /* @__PURE__ */ new Map();
|
|
130
|
+
let result = "";
|
|
131
|
+
for (let segmentIndex = 0; segmentIndex < end.segmentIndex; segmentIndex += 1) {
|
|
132
|
+
result += prepared.segments[segmentIndex] ?? "";
|
|
133
|
+
}
|
|
134
|
+
if (end.graphemeIndex > 0) {
|
|
135
|
+
const segment = prepared.segments[end.segmentIndex];
|
|
136
|
+
if (segment !== void 0) {
|
|
137
|
+
result += getSegmentGraphemes(segment, graphemeCache).slice(0, end.graphemeIndex).join("");
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return result;
|
|
141
|
+
}
|
|
142
|
+
function isWordLike(grapheme) {
|
|
143
|
+
return grapheme !== void 0 && wordLikePattern.test(grapheme);
|
|
144
|
+
}
|
|
145
|
+
function preferWordBoundary(graphemes, count) {
|
|
146
|
+
if (count <= 0 || count >= graphemes.length) {
|
|
147
|
+
return count;
|
|
148
|
+
}
|
|
149
|
+
if (!isWordLike(graphemes[count - 1]) || !isWordLike(graphemes[count])) {
|
|
150
|
+
return count;
|
|
151
|
+
}
|
|
152
|
+
let boundary = count;
|
|
153
|
+
while (boundary > 0 && isWordLike(graphemes[boundary - 1])) {
|
|
154
|
+
boundary -= 1;
|
|
155
|
+
}
|
|
156
|
+
while (boundary > 0 && whitespacePattern.test(graphemes[boundary - 1] ?? "")) {
|
|
157
|
+
boundary -= 1;
|
|
158
|
+
}
|
|
159
|
+
return boundary === 0 ? count : boundary;
|
|
160
|
+
}
|
|
161
|
+
function buildCandidateText(graphemes, count, ellipsis) {
|
|
162
|
+
const base = graphemes.slice(0, count).join("").trimEnd();
|
|
163
|
+
if (ellipsis.length === 0) {
|
|
164
|
+
return base;
|
|
165
|
+
}
|
|
166
|
+
return base.length === 0 ? ellipsis : `${base}${ellipsis}`;
|
|
167
|
+
}
|
|
168
|
+
function useTruncatedText({
|
|
169
|
+
text,
|
|
170
|
+
width,
|
|
171
|
+
font,
|
|
172
|
+
lineHeight,
|
|
173
|
+
typography,
|
|
174
|
+
prepareOptions,
|
|
175
|
+
maxLines,
|
|
176
|
+
ellipsis = "\u2026",
|
|
177
|
+
enabled = true
|
|
178
|
+
}) {
|
|
179
|
+
const resolvedFont = font ?? typography?.font;
|
|
180
|
+
const resolvedLineHeight = lineHeight ?? typography?.lineHeight;
|
|
181
|
+
const resolvedWidth = width ?? typography?.width;
|
|
182
|
+
if (resolvedFont === void 0 || resolvedLineHeight === void 0) {
|
|
183
|
+
throw new Error("useTruncatedText requires `font` and `lineHeight`, either directly or via `typography`.");
|
|
184
|
+
}
|
|
185
|
+
if (resolvedWidth === void 0) {
|
|
186
|
+
throw new Error("useTruncatedText requires `width`, either directly or via `typography`.");
|
|
187
|
+
}
|
|
188
|
+
if (!Number.isInteger(maxLines) || maxLines <= 0) {
|
|
189
|
+
throw new Error("useTruncatedText requires `maxLines` to be a positive integer.");
|
|
190
|
+
}
|
|
191
|
+
const prepared = usePreparedSegments({
|
|
192
|
+
text,
|
|
193
|
+
font: resolvedFont,
|
|
194
|
+
options: prepareOptions,
|
|
195
|
+
enabled
|
|
196
|
+
});
|
|
197
|
+
const fullLayout = usePretextLines({
|
|
198
|
+
prepared: prepared.prepared,
|
|
199
|
+
width: resolvedWidth,
|
|
200
|
+
lineHeight: resolvedLineHeight,
|
|
201
|
+
enabled
|
|
202
|
+
});
|
|
203
|
+
const normalizedWhiteSpace = prepareOptions?.whiteSpace;
|
|
204
|
+
return useMemo4(() => {
|
|
205
|
+
if (!enabled || !prepared.isReady || !fullLayout.isReady || prepared.prepared === null) {
|
|
206
|
+
return {
|
|
207
|
+
text: "",
|
|
208
|
+
didTruncate: false,
|
|
209
|
+
visibleLineCount: 0,
|
|
210
|
+
fullLineCount: 0,
|
|
211
|
+
height: 0,
|
|
212
|
+
isReady: false
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
if (fullLayout.lineCount <= maxLines) {
|
|
216
|
+
return {
|
|
217
|
+
text,
|
|
218
|
+
didTruncate: false,
|
|
219
|
+
visibleLineCount: fullLayout.lineCount,
|
|
220
|
+
fullLineCount: fullLayout.lineCount,
|
|
221
|
+
height: fullLayout.height,
|
|
222
|
+
isReady: true
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
const lastVisibleLine = fullLayout.lines[maxLines - 1];
|
|
226
|
+
if (lastVisibleLine === void 0) {
|
|
227
|
+
return {
|
|
228
|
+
text: "",
|
|
229
|
+
didTruncate: true,
|
|
230
|
+
visibleLineCount: 0,
|
|
231
|
+
fullLineCount: fullLayout.lineCount,
|
|
232
|
+
height: 0,
|
|
233
|
+
isReady: true
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
const visibleText = getTextUpToCursor(prepared.prepared, lastVisibleLine.end);
|
|
237
|
+
const graphemes = Array.from(graphemeSegmenter.segment(visibleText), ({ segment }) => segment);
|
|
238
|
+
const measurementCache = /* @__PURE__ */ new Map();
|
|
239
|
+
const normalizedOptions = normalizedWhiteSpace === void 0 ? void 0 : { whiteSpace: normalizedWhiteSpace };
|
|
240
|
+
const measureCandidate = (candidateText) => {
|
|
241
|
+
const cached = measurementCache.get(candidateText);
|
|
242
|
+
if (cached !== void 0) {
|
|
243
|
+
return cached;
|
|
244
|
+
}
|
|
245
|
+
const nextResult = candidateText.length === 0 ? { lineCount: 0, height: 0 } : layout2(prepare2(candidateText, resolvedFont, normalizedOptions), resolvedWidth, resolvedLineHeight);
|
|
246
|
+
measurementCache.set(candidateText, nextResult);
|
|
247
|
+
return nextResult;
|
|
248
|
+
};
|
|
249
|
+
let low = 0;
|
|
250
|
+
let high = graphemes.length;
|
|
251
|
+
let bestCount = 0;
|
|
252
|
+
while (low <= high) {
|
|
253
|
+
const mid = Math.floor((low + high) / 2);
|
|
254
|
+
const candidate = buildCandidateText(graphemes, mid, ellipsis);
|
|
255
|
+
const candidateResult = measureCandidate(candidate);
|
|
256
|
+
if (candidateResult.lineCount <= maxLines) {
|
|
257
|
+
bestCount = mid;
|
|
258
|
+
low = mid + 1;
|
|
259
|
+
} else {
|
|
260
|
+
high = mid - 1;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const preferredCount = preferWordBoundary(graphemes, bestCount);
|
|
264
|
+
const truncatedText = buildCandidateText(graphemes, preferredCount, ellipsis);
|
|
265
|
+
const truncatedLayout = measureCandidate(truncatedText);
|
|
266
|
+
return {
|
|
267
|
+
text: truncatedText,
|
|
268
|
+
didTruncate: true,
|
|
269
|
+
visibleLineCount: truncatedLayout.lineCount,
|
|
270
|
+
fullLineCount: fullLayout.lineCount,
|
|
271
|
+
height: truncatedLayout.height,
|
|
272
|
+
isReady: true
|
|
273
|
+
};
|
|
274
|
+
}, [
|
|
275
|
+
ellipsis,
|
|
276
|
+
enabled,
|
|
277
|
+
fullLayout.height,
|
|
278
|
+
fullLayout.isReady,
|
|
279
|
+
fullLayout.lineCount,
|
|
280
|
+
fullLayout.lines,
|
|
281
|
+
maxLines,
|
|
282
|
+
normalizedWhiteSpace,
|
|
283
|
+
prepared.isReady,
|
|
284
|
+
prepared.prepared,
|
|
285
|
+
resolvedFont,
|
|
286
|
+
resolvedLineHeight,
|
|
287
|
+
resolvedWidth,
|
|
288
|
+
text
|
|
289
|
+
]);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// src/core/lib/typography.ts
|
|
293
|
+
function isShorthandInput(input) {
|
|
294
|
+
return "font" in input;
|
|
295
|
+
}
|
|
296
|
+
function resolveFont(input) {
|
|
297
|
+
if (isShorthandInput(input)) {
|
|
298
|
+
if (input.font.trim().length === 0) {
|
|
299
|
+
throw new Error("createPretextTypography requires a non-empty `font` string.");
|
|
300
|
+
}
|
|
301
|
+
return input.font;
|
|
302
|
+
}
|
|
303
|
+
if (input.family.trim().length === 0) {
|
|
304
|
+
throw new Error("createPretextTypography requires a non-empty `family` value.");
|
|
305
|
+
}
|
|
306
|
+
if (!Number.isFinite(input.size) || input.size <= 0) {
|
|
307
|
+
throw new Error("createPretextTypography requires `size` to be a positive number.");
|
|
308
|
+
}
|
|
309
|
+
return [input.weight === void 0 ? void 0 : `${input.weight}`, `${input.size}px`, input.family].filter(Boolean).join(" ");
|
|
310
|
+
}
|
|
311
|
+
function createPretextTypography(input) {
|
|
312
|
+
const font = resolveFont(input);
|
|
313
|
+
const { lineHeight, width } = input;
|
|
314
|
+
if (!Number.isFinite(lineHeight) || lineHeight <= 0) {
|
|
315
|
+
throw new Error("createPretextTypography requires `lineHeight` to be a positive number.");
|
|
316
|
+
}
|
|
317
|
+
const style = {
|
|
318
|
+
font,
|
|
319
|
+
lineHeight: `${lineHeight}px`
|
|
320
|
+
};
|
|
321
|
+
if (width !== void 0) {
|
|
322
|
+
style.width = `${width}px`;
|
|
323
|
+
}
|
|
324
|
+
return {
|
|
325
|
+
font,
|
|
326
|
+
lineHeight,
|
|
327
|
+
width,
|
|
328
|
+
style
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// src/core/components/PText.tsx
|
|
333
|
+
import { forwardRef, useCallback, useEffect } from "react";
|
|
334
|
+
import { jsx } from "react/jsx-runtime";
|
|
335
|
+
function assignRef(ref, value) {
|
|
336
|
+
if (typeof ref === "function") {
|
|
337
|
+
ref(value);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
if (ref) {
|
|
341
|
+
ref.current = value;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
function PTextInner({ as, children, font, lineHeight, prepareOptions, typography, width, onMeasure, style, ...rest }, forwardedRef) {
|
|
345
|
+
const { ref: observedRef, width: observedWidth } = useElementWidth();
|
|
346
|
+
const explicitWidth = width ?? typography?.width;
|
|
347
|
+
const resolvedWidth = explicitWidth ?? observedWidth;
|
|
348
|
+
const resolvedFont = font ?? typography?.font;
|
|
349
|
+
const resolvedLineHeight = lineHeight ?? typography?.lineHeight;
|
|
350
|
+
if (resolvedFont === void 0 || resolvedLineHeight === void 0) {
|
|
351
|
+
throw new Error("PText requires `font` and `lineHeight`, either directly or via `typography`.");
|
|
352
|
+
}
|
|
353
|
+
const { prepared } = usePreparedText({ text: children, font: resolvedFont, options: prepareOptions });
|
|
354
|
+
const result = usePretextLayout({ prepared, width: resolvedWidth, lineHeight: resolvedLineHeight });
|
|
355
|
+
const typographyStyle = createPretextTypography({
|
|
356
|
+
font: resolvedFont,
|
|
357
|
+
lineHeight: resolvedLineHeight,
|
|
358
|
+
width: explicitWidth
|
|
359
|
+
}).style;
|
|
360
|
+
const composedRef = useCallback(
|
|
361
|
+
(node) => {
|
|
362
|
+
if (explicitWidth === void 0) {
|
|
363
|
+
observedRef(node);
|
|
364
|
+
}
|
|
365
|
+
assignRef(forwardedRef, node);
|
|
366
|
+
},
|
|
367
|
+
[explicitWidth, forwardedRef, observedRef]
|
|
368
|
+
);
|
|
369
|
+
useEffect(() => {
|
|
370
|
+
if (onMeasure === void 0) {
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
onMeasure({
|
|
374
|
+
width: resolvedWidth,
|
|
375
|
+
height: result.height,
|
|
376
|
+
lineCount: result.lineCount
|
|
377
|
+
});
|
|
378
|
+
}, [onMeasure, resolvedWidth, result.height, result.lineCount]);
|
|
379
|
+
const Tag = as ?? "p";
|
|
380
|
+
return /* @__PURE__ */ jsx(Tag, { ref: composedRef, style: { ...typographyStyle, ...style }, ...rest, children });
|
|
381
|
+
}
|
|
382
|
+
var PText = forwardRef(PTextInner);
|
|
383
|
+
export {
|
|
384
|
+
PText,
|
|
385
|
+
createPretextTypography,
|
|
386
|
+
useElementWidth,
|
|
387
|
+
useMeasuredText,
|
|
388
|
+
usePreparedSegments,
|
|
389
|
+
usePreparedText,
|
|
390
|
+
usePretextLayout,
|
|
391
|
+
usePretextLines,
|
|
392
|
+
useTruncatedText
|
|
393
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '@chenglou/pretext';
|
package/dist/pretext.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { PreparedText } from '@chenglou/pretext';
|
|
2
|
+
|
|
3
|
+
type PrepareOptions = {
|
|
4
|
+
whiteSpace?: 'normal' | 'pre-wrap';
|
|
5
|
+
};
|
|
6
|
+
type UsePreparedTextInput = {
|
|
7
|
+
text: string;
|
|
8
|
+
font: string;
|
|
9
|
+
options?: PrepareOptions;
|
|
10
|
+
enableProfiling?: boolean;
|
|
11
|
+
enabled?: boolean;
|
|
12
|
+
};
|
|
13
|
+
type UsePreparedTextResult = {
|
|
14
|
+
prepared: PreparedText | null;
|
|
15
|
+
prepareMs?: number;
|
|
16
|
+
isReady: boolean;
|
|
17
|
+
};
|
|
18
|
+
declare function usePreparedText({ text, font, options, enableProfiling, enabled }: UsePreparedTextInput): UsePreparedTextResult;
|
|
19
|
+
|
|
20
|
+
export { type PrepareOptions as P, type UsePreparedTextInput as U, type UsePreparedTextResult as a, usePreparedText as u };
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@santjc/react-pretext",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Simple React wrapper over @chenglou/pretext for deterministic text measurement before paint, with an advanced editorial subpath.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"module": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"sideEffects": false,
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./pretext": {
|
|
17
|
+
"types": "./dist/pretext.d.ts",
|
|
18
|
+
"import": "./dist/pretext.js"
|
|
19
|
+
},
|
|
20
|
+
"./editorial": {
|
|
21
|
+
"types": "./dist/editorial.d.ts",
|
|
22
|
+
"import": "./dist/editorial.js"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist"
|
|
27
|
+
],
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"react",
|
|
33
|
+
"pretext",
|
|
34
|
+
"text-measurement",
|
|
35
|
+
"typography",
|
|
36
|
+
"layout",
|
|
37
|
+
"line-count",
|
|
38
|
+
"virtualized-list",
|
|
39
|
+
"accordion-height",
|
|
40
|
+
"masonry-layout",
|
|
41
|
+
"responsive-text",
|
|
42
|
+
"editorial-layout",
|
|
43
|
+
"newspaper-layout",
|
|
44
|
+
"text-flow"
|
|
45
|
+
],
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "tsup src/index.ts src/pretext.ts src/editorial.ts --format esm --dts",
|
|
48
|
+
"lint": "eslint src",
|
|
49
|
+
"test": "vitest run --config vitest.config.ts"
|
|
50
|
+
},
|
|
51
|
+
"peerDependencies": {
|
|
52
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
53
|
+
"react-dom": "^18.0.0 || ^19.0.0"
|
|
54
|
+
},
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"@chenglou/pretext": "^0.0.3"
|
|
57
|
+
}
|
|
58
|
+
}
|