@altano/satori-fit-text 1.0.1 → 1.0.2
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
CHANGED
|
@@ -45,10 +45,16 @@ console.log(largestUsableFontSize);
|
|
|
45
45
|
|
|
46
46
|
## Example Uses
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
### Code Sandboxes
|
|
49
|
+
|
|
50
|
+
- [Playground with controls](https://codesandbox.io/p/sandbox/github/altano/npm-packages/tree/main/examples/satori-fit-text-playground?file=%2Fpackage.json%3A1%2C1)
|
|
51
|
+
- [React Component](https://codesandbox.io/s/altano-satori-fit-text-browser-example-j2gvk6)
|
|
52
|
+
- [Node.js CLI App](https://codesandbox.io/p/sandbox/github/altano/npm-packages/tree/main/examples/satori-fit-text-node-cli?file=%2Fpackage.json%3A1%2C1)
|
|
53
|
+
- [Next.js Open Graph Card - Simple](https://codesandbox.io/p/sandbox/altano-satori-fit-text-next-js-demo-sv4flc)
|
|
54
|
+
|
|
55
|
+
### Code
|
|
56
|
+
|
|
50
57
|
- [Next.js Open Graph Card - Complex](https://github.com/altano/alan.norbauer.com/blob/main/src/components/opengraph/image.tsx#L29-L35)
|
|
51
|
-
- [Node.js CLI App (codesandbox)](https://codesandbox.io/p/sandbox/github/altano/npm-packages/tree/main/examples/satori-fit-text-node-cli?file=%2Fpackage.json%3A1%2C1)
|
|
52
58
|
|
|
53
59
|
## Implementation
|
|
54
60
|
|
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
import log from "../log.js";
|
|
2
2
|
import { TextMeasurer } from "./TextMeasurer.js";
|
|
3
3
|
class BrowserTextMeasurer extends TextMeasurer {
|
|
4
|
-
#
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const svg = div.firstElementChild;
|
|
4
|
+
#createSvgElement(svgXML) {
|
|
5
|
+
const wrapper = document.createElement("div");
|
|
6
|
+
wrapper.innerHTML = svgXML;
|
|
7
|
+
const svg = wrapper.firstElementChild;
|
|
9
8
|
if (svg == null || !(svg instanceof SVGSVGElement)) {
|
|
10
9
|
throw new Error(`Child should be svg element`);
|
|
11
10
|
}
|
|
12
11
|
return svg;
|
|
13
12
|
}
|
|
14
13
|
async getDimensions(fontSize) {
|
|
15
|
-
const
|
|
16
|
-
const svg = this.#
|
|
14
|
+
const svgXML = await this.createSvgXmlString(fontSize);
|
|
15
|
+
const svg = this.#createSvgElement(svgXML);
|
|
17
16
|
try {
|
|
18
17
|
document.body.appendChild(svg);
|
|
19
18
|
const dimensions = svg.getBBox();
|
|
20
|
-
const
|
|
19
|
+
const width = dimensions.width + Math.min(dimensions.x, 0);
|
|
20
|
+
const height = dimensions.height + Math.min(dimensions.y, 0);
|
|
21
21
|
log({
|
|
22
22
|
fontSize,
|
|
23
23
|
width: `${width}px`,
|
|
@@ -25,7 +25,7 @@ class BrowserTextMeasurer extends TextMeasurer {
|
|
|
25
25
|
widthFits: `${width}px ${width <= this.maxWidth ? "DOES" : "does NOT"} fit in ${this.maxWidth}`,
|
|
26
26
|
heightFits: `${height}px ${height <= this.maxHeight ? "DOES" : "does NOT"} fit in ${this.maxHeight}`
|
|
27
27
|
});
|
|
28
|
-
return
|
|
28
|
+
return { width, height };
|
|
29
29
|
} finally {
|
|
30
30
|
svg.parentElement?.removeChild?.(svg);
|
|
31
31
|
}
|
|
@@ -14,7 +14,8 @@ class HeadlessTextMeasurer extends TextMeasurer {
|
|
|
14
14
|
async getDimensions(fontSize) {
|
|
15
15
|
await this.#setFontSize(fontSize);
|
|
16
16
|
const dimensions = this.#canvas.bbox();
|
|
17
|
-
const
|
|
17
|
+
const width = dimensions.width + Math.min(dimensions.x, 0);
|
|
18
|
+
const height = dimensions.height + Math.min(dimensions.y, 0);
|
|
18
19
|
log({
|
|
19
20
|
fontSize,
|
|
20
21
|
width: `${width}px`,
|
|
@@ -22,12 +23,12 @@ class HeadlessTextMeasurer extends TextMeasurer {
|
|
|
22
23
|
widthFits: `${width}px ${width <= this.maxWidth ? "DOES" : "does NOT"} fit in ${this.maxWidth}`,
|
|
23
24
|
heightFits: `${height}px ${height <= this.maxHeight ? "DOES" : "does NOT"} fit in ${this.maxHeight}`
|
|
24
25
|
});
|
|
25
|
-
return
|
|
26
|
+
return { width, height };
|
|
26
27
|
}
|
|
27
28
|
async #setFontSize(fontSize) {
|
|
28
|
-
const
|
|
29
|
+
const svgXML = await this.createSvgXmlString(fontSize);
|
|
29
30
|
this.#canvas.clear();
|
|
30
|
-
this.#canvas.svg(
|
|
31
|
+
this.#canvas.svg(svgXML);
|
|
31
32
|
}
|
|
32
33
|
}
|
|
33
34
|
export {
|
|
@@ -9,6 +9,6 @@ export declare abstract class TextMeasurer {
|
|
|
9
9
|
constructor(text: string, font: Font, maxWidth: number, maxHeight: number, lineHeight: number | "normal");
|
|
10
10
|
abstract getDimensions(fontSize: number): Promise<Dimensions>;
|
|
11
11
|
doesSizeFit(fontSize: number): Promise<boolean>;
|
|
12
|
-
protected
|
|
12
|
+
protected createSvgXmlString(fontSize: number): Promise<string>;
|
|
13
13
|
}
|
|
14
14
|
//# sourceMappingURL=TextMeasurer.d.ts.map
|
|
@@ -13,38 +13,27 @@ class TextMeasurer {
|
|
|
13
13
|
const { width, height } = await this.getDimensions(fontSize);
|
|
14
14
|
return width <= this.maxWidth && height <= this.maxHeight;
|
|
15
15
|
}
|
|
16
|
-
async
|
|
16
|
+
async createSvgXmlString(fontSize) {
|
|
17
17
|
const node = {
|
|
18
18
|
type: "div",
|
|
19
|
+
key: "satori-fit-text-measure-node",
|
|
19
20
|
props: {
|
|
20
21
|
style: {
|
|
21
|
-
|
|
22
|
+
// Static styles
|
|
23
|
+
background: "white",
|
|
24
|
+
color: "black",
|
|
22
25
|
display: "flex",
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
height: "100%",
|
|
26
|
+
flex: 1,
|
|
27
|
+
boxSizing: "border-box",
|
|
26
28
|
margin: 0,
|
|
27
|
-
padding: 0
|
|
29
|
+
padding: 0,
|
|
30
|
+
// Dynamic styles based on input
|
|
31
|
+
fontSize,
|
|
32
|
+
fontFamily: this.font.name,
|
|
33
|
+
maxWidth: this.maxWidth,
|
|
34
|
+
lineHeight: this.lineHeight
|
|
28
35
|
},
|
|
29
|
-
children:
|
|
30
|
-
{
|
|
31
|
-
type: "div",
|
|
32
|
-
props: {
|
|
33
|
-
style: {
|
|
34
|
-
// Static styles
|
|
35
|
-
margin: 0,
|
|
36
|
-
padding: 0,
|
|
37
|
-
boxSizing: "border-box",
|
|
38
|
-
// Dynamic styles based on input
|
|
39
|
-
fontSize,
|
|
40
|
-
fontFamily: this.font.name,
|
|
41
|
-
maxWidth: this.maxWidth,
|
|
42
|
-
lineHeight: this.lineHeight
|
|
43
|
-
},
|
|
44
|
-
children: this.text
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
]
|
|
36
|
+
children: this.text
|
|
48
37
|
}
|
|
49
38
|
};
|
|
50
39
|
return satori(node, {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@altano/satori-fit-text",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Fit text to a bounding box in Node or the browser",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fit-text",
|
|
@@ -53,9 +53,8 @@
|
|
|
53
53
|
"jest-image-snapshot": "^6.4.0",
|
|
54
54
|
"prettier": "^3.5.3",
|
|
55
55
|
"react": "^19.1.0",
|
|
56
|
-
"react-dom": "^19.1.0",
|
|
57
56
|
"typescript": "^5.8.2",
|
|
58
|
-
"vite": "^6.2.
|
|
57
|
+
"vite": "^6.2.5",
|
|
59
58
|
"vitest": "^3.1.1",
|
|
60
59
|
"@altano/assets": "0.0.0",
|
|
61
60
|
"@altano/build-config": "0.0.0",
|
|
@@ -68,7 +67,7 @@
|
|
|
68
67
|
"scripts": {
|
|
69
68
|
"build": "tsup --config build-config/tsup.config.node.ts",
|
|
70
69
|
"build:types": "tsc --project tsconfig.declarations.json",
|
|
71
|
-
"check:exports": "attw --pack . --profile esm-only",
|
|
70
|
+
"check:exports": "attw --pack . --profile esm-only --quiet",
|
|
72
71
|
"check:types:src": "tsc --noEmit",
|
|
73
72
|
"check:types:test": "tsc --noEmit --project ./tests/tsconfig.json",
|
|
74
73
|
"check:unused-dependencies": "depcheck",
|
|
@@ -80,7 +79,7 @@
|
|
|
80
79
|
"lint:fix": "TIMING=1 pnpm lint --fix",
|
|
81
80
|
"lint:timing": "TIMING=1 pnpm lint",
|
|
82
81
|
"test:e2e": "playwright test",
|
|
83
|
-
"test:e2e:server": "
|
|
82
|
+
"test:e2e:server": "pnpm --dir ../../examples/satori-fit-text-playground start",
|
|
84
83
|
"test:e2e:ui": "playwright test --ui",
|
|
85
84
|
"test:unit": "vitest --run",
|
|
86
85
|
"test:unit:benchmark": "vitest --run bench",
|