@altano/satori-fit-text 1.0.2 → 2.0.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.
@@ -1,36 +1,41 @@
1
- import log from "../log.js";
1
+ import log_default from "../log.js";
2
2
  import { TextMeasurer } from "./TextMeasurer.js";
3
- class BrowserTextMeasurer extends TextMeasurer {
4
- #createSvgElement(svgXML) {
5
- const wrapper = document.createElement("div");
6
- wrapper.innerHTML = svgXML;
7
- const svg = wrapper.firstElementChild;
8
- if (svg == null || !(svg instanceof SVGSVGElement)) {
9
- throw new Error(`Child should be svg element`);
10
- }
11
- return svg;
12
- }
13
- async getDimensions(fontSize) {
14
- const svgXML = await this.createSvgXmlString(fontSize);
15
- const svg = this.#createSvgElement(svgXML);
16
- try {
17
- document.body.appendChild(svg);
18
- const dimensions = svg.getBBox();
19
- const width = dimensions.width + Math.min(dimensions.x, 0);
20
- const height = dimensions.height + Math.min(dimensions.y, 0);
21
- log({
22
- fontSize,
23
- width: `${width}px`,
24
- height: `${height}px`,
25
- widthFits: `${width}px ${width <= this.maxWidth ? "DOES" : "does NOT"} fit in ${this.maxWidth}`,
26
- heightFits: `${height}px ${height <= this.maxHeight ? "DOES" : "does NOT"} fit in ${this.maxHeight}`
27
- });
28
- return { width, height };
29
- } finally {
30
- svg.parentElement?.removeChild?.(svg);
31
- }
32
- }
33
- }
34
- export {
35
- BrowserTextMeasurer as default
3
+
4
+ //#region src/TextMeasurer/BrowserTextMeasurer.ts
5
+ var BrowserTextMeasurer = class extends TextMeasurer {
6
+ #createSvgElement(svgXML) {
7
+ const wrapper = document.createElement("div");
8
+ wrapper.innerHTML = svgXML;
9
+ const svg = wrapper.firstElementChild;
10
+ if (svg == null || !(svg instanceof SVGSVGElement)) throw new Error(`Child should be svg element`);
11
+ return svg;
12
+ }
13
+ async getDimensions(fontSize) {
14
+ const svgXML = await this.createSvgXmlString(fontSize);
15
+ const svg = this.#createSvgElement(svgXML);
16
+ try {
17
+ document.body.appendChild(svg);
18
+ const dimensions = svg.getBBox();
19
+ const width = dimensions.width + Math.min(dimensions.x, 0);
20
+ const height = dimensions.height + Math.min(dimensions.y, 0);
21
+ log_default({
22
+ fontSize,
23
+ width: `${width}px`,
24
+ height: `${height}px`,
25
+ widthFits: `${width}px ${width <= this.maxWidth ? "DOES" : "does NOT"} fit in ${this.maxWidth}`,
26
+ heightFits: `${height}px ${height <= this.maxHeight ? "DOES" : "does NOT"} fit in ${this.maxHeight}`
27
+ });
28
+ return {
29
+ width,
30
+ height
31
+ };
32
+ } finally {
33
+ svg.parentElement?.removeChild?.(svg);
34
+ }
35
+ }
36
36
  };
37
+ /* v8 ignore stop */
38
+
39
+ //#endregion
40
+ export { BrowserTextMeasurer as default };
41
+ //# sourceMappingURL=BrowserTextMeasurer.js.map
@@ -1,36 +1,42 @@
1
+ import log_default from "../log.js";
2
+ import { TextMeasurer } from "./TextMeasurer.js";
1
3
  import { SVG, registerWindow } from "@svgdotjs/svg.js";
2
4
  import { createSVGWindow } from "svgdom";
3
- import log from "../log.js";
4
- import { TextMeasurer } from "./TextMeasurer.js";
5
- class HeadlessTextMeasurer extends TextMeasurer {
6
- #canvas;
7
- constructor(text, font, maxWidth, maxHeight, lineHeight) {
8
- super(text, font, maxWidth, maxHeight, lineHeight);
9
- const window = createSVGWindow();
10
- const document = window.document;
11
- registerWindow(window, document);
12
- this.#canvas = SVG(document.documentElement);
13
- }
14
- async getDimensions(fontSize) {
15
- await this.#setFontSize(fontSize);
16
- const dimensions = this.#canvas.bbox();
17
- const width = dimensions.width + Math.min(dimensions.x, 0);
18
- const height = dimensions.height + Math.min(dimensions.y, 0);
19
- log({
20
- fontSize,
21
- width: `${width}px`,
22
- height: `${height}px`,
23
- widthFits: `${width}px ${width <= this.maxWidth ? "DOES" : "does NOT"} fit in ${this.maxWidth}`,
24
- heightFits: `${height}px ${height <= this.maxHeight ? "DOES" : "does NOT"} fit in ${this.maxHeight}`
25
- });
26
- return { width, height };
27
- }
28
- async #setFontSize(fontSize) {
29
- const svgXML = await this.createSvgXmlString(fontSize);
30
- this.#canvas.clear();
31
- this.#canvas.svg(svgXML);
32
- }
33
- }
34
- export {
35
- HeadlessTextMeasurer as default
5
+
6
+ //#region src/TextMeasurer/HeadlessTextMeasurer.ts
7
+ var HeadlessTextMeasurer = class extends TextMeasurer {
8
+ #canvas;
9
+ constructor(text, font, maxWidth, maxHeight, lineHeight) {
10
+ super(text, font, maxWidth, maxHeight, lineHeight);
11
+ const window = createSVGWindow();
12
+ const document = window.document;
13
+ registerWindow(window, document);
14
+ this.#canvas = SVG(document.documentElement);
15
+ }
16
+ async getDimensions(fontSize) {
17
+ await this.#setFontSize(fontSize);
18
+ const dimensions = this.#canvas.bbox();
19
+ const width = dimensions.width + Math.min(dimensions.x, 0);
20
+ const height = dimensions.height + Math.min(dimensions.y, 0);
21
+ log_default({
22
+ fontSize,
23
+ width: `${width}px`,
24
+ height: `${height}px`,
25
+ widthFits: `${width}px ${width <= this.maxWidth ? "DOES" : "does NOT"} fit in ${this.maxWidth}`,
26
+ heightFits: `${height}px ${height <= this.maxHeight ? "DOES" : "does NOT"} fit in ${this.maxHeight}`
27
+ });
28
+ return {
29
+ width,
30
+ height
31
+ };
32
+ }
33
+ async #setFontSize(fontSize) {
34
+ const svgXML = await this.createSvgXmlString(fontSize);
35
+ this.#canvas.clear();
36
+ this.#canvas.svg(svgXML);
37
+ }
36
38
  };
39
+
40
+ //#endregion
41
+ export { HeadlessTextMeasurer as default };
42
+ //# sourceMappingURL=HeadlessTextMeasurer.js.map
@@ -1,48 +1,50 @@
1
1
  import satori from "satori";
2
- class TextMeasurer {
3
- constructor(text, font, maxWidth, maxHeight, lineHeight) {
4
- this.text = text;
5
- this.font = font;
6
- this.maxWidth = maxWidth;
7
- this.maxHeight = maxHeight;
8
- this.lineHeight = lineHeight;
9
- this.#fonts = [this.font];
10
- }
11
- #fonts;
12
- async doesSizeFit(fontSize) {
13
- const { width, height } = await this.getDimensions(fontSize);
14
- return width <= this.maxWidth && height <= this.maxHeight;
15
- }
16
- async createSvgXmlString(fontSize) {
17
- const node = {
18
- type: "div",
19
- key: "satori-fit-text-measure-node",
20
- props: {
21
- style: {
22
- // Static styles
23
- background: "white",
24
- color: "black",
25
- display: "flex",
26
- flex: 1,
27
- boxSizing: "border-box",
28
- margin: 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
35
- },
36
- children: this.text
37
- }
38
- };
39
- return satori(node, {
40
- fonts: this.#fonts,
41
- width: this.maxWidth * 2,
42
- height: this.maxHeight * 2
43
- });
44
- }
45
- }
46
- export {
47
- TextMeasurer
2
+
3
+ //#region src/TextMeasurer/TextMeasurer.ts
4
+ const epsilon = 1e-4;
5
+ var TextMeasurer = class {
6
+ #fonts;
7
+ constructor(text, font, maxWidth, maxHeight, lineHeight) {
8
+ this.text = text;
9
+ this.font = font;
10
+ this.maxWidth = maxWidth;
11
+ this.maxHeight = maxHeight;
12
+ this.lineHeight = lineHeight;
13
+ this.#fonts = [this.font];
14
+ }
15
+ async doesSizeFit(fontSize) {
16
+ const { width, height } = await this.getDimensions(fontSize);
17
+ return width - epsilon <= this.maxWidth && height - epsilon <= this.maxHeight;
18
+ }
19
+ async createSvgXmlString(fontSize) {
20
+ const node = {
21
+ type: "div",
22
+ key: "satori-fit-text-measure-node",
23
+ props: {
24
+ style: {
25
+ background: "white",
26
+ color: "black",
27
+ display: "flex",
28
+ flex: 1,
29
+ boxSizing: "border-box",
30
+ margin: 0,
31
+ padding: 0,
32
+ fontSize,
33
+ fontFamily: this.font.name,
34
+ maxWidth: this.maxWidth,
35
+ lineHeight: this.lineHeight
36
+ },
37
+ children: this.text
38
+ }
39
+ };
40
+ return satori(node, {
41
+ fonts: this.#fonts,
42
+ width: this.maxWidth * 2,
43
+ height: this.maxHeight * 2
44
+ });
45
+ }
48
46
  };
47
+
48
+ //#endregion
49
+ export { TextMeasurer };
50
+ //# sourceMappingURL=TextMeasurer.js.map
@@ -1,12 +1,15 @@
1
+ //#region src/TextMeasurer/getTextMeasurer.ts
1
2
  async function getTextMeasurer(...args) {
2
- if (typeof window !== "undefined") {
3
- const Measurer = await import("./BrowserTextMeasurer.js");
4
- return new Measurer.default(...args);
5
- } else {
6
- const Measurer = await import("./HeadlessTextMeasurer.js");
7
- return new Measurer.default(...args);
8
- }
3
+ /* v8 ignore next 3 */
4
+ if (typeof window !== "undefined") {
5
+ const Measurer = await import("./BrowserTextMeasurer.js");
6
+ return new Measurer.default(...args);
7
+ } else {
8
+ const Measurer = await import("./HeadlessTextMeasurer.js");
9
+ return new Measurer.default(...args);
10
+ }
9
11
  }
10
- export {
11
- getTextMeasurer
12
- };
12
+
13
+ //#endregion
14
+ export { getTextMeasurer };
15
+ //# sourceMappingURL=getTextMeasurer.js.map
package/dist/fitText.d.ts CHANGED
@@ -1,43 +1,55 @@
1
- import type { Font } from "./types.js";
2
- export type FindOptions = {
3
- /**
4
- * The text you want to fit
5
- */
6
- text: string;
7
- /**
8
- * The font of your text. Options are the same as [those taken by
9
- * Satori](https://github.com/vercel/satori#fonts), but only one font and not
10
- * an array of fonts is accepted here.
11
- */
12
- font: Font;
13
- /**
14
- * The max width the text must be contained in
15
- */
16
- maxWidth: number;
17
- /**
18
- * The max height the text must be contained in
19
- */
20
- maxHeight: number;
21
- /**
22
- * Optional: the CSS line height factor to use, e.g. 1.1
23
- */
24
- lineHeight?: number | "normal";
25
- /**
26
- * The minimum font size that should be returned, even if smaller fonts fit
27
- */
28
- minFontSize?: number;
29
- /**
30
- * The maximum font size that should be returned, even if larger fonts fit
31
- */
32
- maxFontSize?: number;
33
- /**
34
- * The maximum number of font sizes to try before giving up. Defaults to 1000.
35
- */
36
- maxTries?: number;
1
+ import { Font } from "./types.js";
2
+
3
+ //#region src/fitText.d.ts
4
+ type FindOptions = {
5
+ /**
6
+ * The text you want to fit
7
+ */
8
+ text: string;
9
+ /**
10
+ * The font of your text. Options are the same as [those taken by
11
+ * Satori](https://github.com/vercel/satori#fonts), but only one font and not
12
+ * an array of fonts is accepted here.
13
+ */
14
+ font: Font;
15
+ /**
16
+ * The max width the text must be contained in
17
+ */
18
+ maxWidth: number;
19
+ /**
20
+ * The max height the text must be contained in
21
+ */
22
+ maxHeight: number;
23
+ /**
24
+ * Optional: the CSS line height factor to use, e.g. 1.1
25
+ */
26
+ lineHeight?: number | "normal";
27
+ /**
28
+ * The minimum font size that should be returned, even if smaller fonts fit
29
+ */
30
+ minFontSize?: number;
31
+ /**
32
+ * The maximum font size that should be returned, even if larger fonts fit
33
+ */
34
+ maxFontSize?: number;
35
+ /**
36
+ * The maximum number of font sizes to try before giving up. Defaults to 1000.
37
+ */
38
+ maxTries?: number;
37
39
  };
38
40
  /**
39
- * Given text, dimensions, and some options, returns the largest font size that
40
- * would keep the text contained within the dimensions.
41
- */
42
- export declare function findLargestUsableFontSize({ text, font, maxWidth, maxHeight, lineHeight, minFontSize, maxFontSize, maxTries, }: FindOptions): Promise<number>;
41
+ * Given text, dimensions, and some options, returns the largest font size that
42
+ * would keep the text contained within the dimensions.
43
+ */
44
+ declare function findLargestUsableFontSize({
45
+ text,
46
+ font,
47
+ maxWidth,
48
+ maxHeight,
49
+ lineHeight,
50
+ minFontSize,
51
+ maxFontSize,
52
+ maxTries
53
+ }: FindOptions): Promise<number>; //#endregion
54
+ export { FindOptions, findLargestUsableFontSize };
43
55
  //# sourceMappingURL=fitText.d.ts.map
package/dist/fitText.js CHANGED
@@ -1,41 +1,24 @@
1
1
  import { getTextMeasurer } from "./TextMeasurer/getTextMeasurer.js";
2
- async function findLargestUsableFontSize({
3
- text,
4
- font,
5
- maxWidth,
6
- maxHeight,
7
- lineHeight = "normal",
8
- minFontSize = 1,
9
- maxFontSize = 1e3,
10
- maxTries = 1e3
11
- }) {
12
- if (maxTries < 2) {
13
- throw new Error("`maxTries` must be >= 2");
14
- }
15
- const textMeasurer = await getTextMeasurer(
16
- text,
17
- font,
18
- maxWidth,
19
- maxHeight,
20
- lineHeight
21
- );
22
- for (let tries = 1, lower = minFontSize, upper = maxFontSize; tries < maxTries; ++tries) {
23
- const searchCutoffThreshold = Math.min(1, lower);
24
- if (upper - lower <= searchCutoffThreshold) {
25
- return Math.floor(lower);
26
- }
27
- const sizeCandidate = Math.floor(lower + (upper - lower) / 2);
28
- const doesFit = await textMeasurer.doesSizeFit(sizeCandidate);
29
- if (doesFit) {
30
- lower = sizeCandidate;
31
- } else {
32
- upper = sizeCandidate;
33
- }
34
- }
35
- throw new Error(
36
- `Could not find a valid font size in ${maxTries - 1} attempts. Giving up. Please try again after constraining the search space, e.g. by restricting the min/max font size difference.`
37
- );
2
+
3
+ //#region src/fitText.ts
4
+ /**
5
+ * Given text, dimensions, and some options, returns the largest font size that
6
+ * would keep the text contained within the dimensions.
7
+ */
8
+ async function findLargestUsableFontSize({ text, font, maxWidth, maxHeight, lineHeight = "normal", minFontSize = 1, maxFontSize = 1e3, maxTries = 1e3 }) {
9
+ if (maxTries < 2) throw new Error("`maxTries` must be >= 2");
10
+ const textMeasurer = await getTextMeasurer(text, font, maxWidth, maxHeight, lineHeight);
11
+ for (let tries = 1, lower = minFontSize, upper = maxFontSize; tries < maxTries; ++tries) {
12
+ const searchCutoffThreshold = Math.min(1, lower);
13
+ if (upper - lower <= searchCutoffThreshold) return Math.floor(lower);
14
+ const sizeCandidate = Math.floor(lower + (upper - lower) / 2);
15
+ const doesFit = await textMeasurer.doesSizeFit(sizeCandidate);
16
+ if (doesFit) lower = sizeCandidate;
17
+ else upper = sizeCandidate;
18
+ }
19
+ throw new Error(`Could not find a valid font size in ${maxTries - 1} attempts. Giving up. Please try again after constraining the search space, e.g. by restricting the min/max font size difference.`);
38
20
  }
39
- export {
40
- findLargestUsableFontSize
41
- };
21
+
22
+ //#endregion
23
+ export { findLargestUsableFontSize };
24
+ //# sourceMappingURL=fitText.js.map
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- export * from "./fitText.js";
2
- export * from "./types.js";
3
- //# sourceMappingURL=index.d.ts.map
1
+ import { Dimensions, Font } from "./types.js";
2
+ import { FindOptions, findLargestUsableFontSize } from "./fitText.js";
3
+ export { Dimensions, FindOptions, Font, findLargestUsableFontSize };
package/dist/index.js CHANGED
@@ -1,2 +1,3 @@
1
- export * from "./fitText.js";
2
- export * from "./types.js";
1
+ import { findLargestUsableFontSize } from "./fitText.js";
2
+
3
+ export { findLargestUsableFontSize };
package/dist/log.d.ts CHANGED
@@ -1,4 +1,8 @@
1
1
  import debug from "debug";
2
+
3
+ //#region src/log.d.ts
2
4
  declare const log: debug.Debugger;
3
- export default log;
5
+
6
+ //#endregion
7
+ export { log as default };
4
8
  //# sourceMappingURL=log.d.ts.map
package/dist/log.js CHANGED
@@ -1,6 +1,9 @@
1
1
  import debug from "debug";
2
+
3
+ //#region src/log.ts
2
4
  const log = debug("satori-text-fit");
3
5
  var log_default = log;
4
- export {
5
- log_default as default
6
- };
6
+
7
+ //#endregion
8
+ export { log_default as default };
9
+ //# sourceMappingURL=log.js.map
package/dist/types.d.ts CHANGED
@@ -1,4 +1,9 @@
1
- import type { Font as SatoriFont } from "satori";
2
- export type Dimensions = Pick<DOMRect, "width" | "height">;
3
- export type Font = SatoriFont;
1
+ import { Font as Font$1 } from "satori";
2
+
3
+ //#region src/types.d.ts
4
+ type Dimensions = Pick<DOMRect, "width" | "height">;
5
+ type Font = Font$1;
6
+
7
+ //#endregion
8
+ export { Dimensions, Font };
4
9
  //# sourceMappingURL=types.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@altano/satori-fit-text",
3
- "version": "1.0.2",
3
+ "version": "2.0.0",
4
4
  "description": "Fit text to a bounding box in Node or the browser",
5
5
  "keywords": [
6
6
  "fit-text",
@@ -37,9 +37,9 @@
37
37
  },
38
38
  "devDependencies": {
39
39
  "@arethetypeswrong/cli": "^0.17.4",
40
- "@fontsource-variable/inter": "^5.2.5",
41
- "@fontsource/inter": "^5.2.5",
42
- "@playwright/test": "1.47.0",
40
+ "@fontsource-variable/inter": "5.2.6",
41
+ "@fontsource/inter": "5.2.6",
42
+ "@playwright/test": "1.51.1",
43
43
  "@resvg/resvg-js": "^2.6.2",
44
44
  "@types/debug": "^4.1.12",
45
45
  "@types/he": "^1.2.3",
@@ -48,25 +48,25 @@
48
48
  "@types/react": "^19.0.12",
49
49
  "@types/react-dom": "^19.0.4",
50
50
  "@types/svgdom": "^0.1.2",
51
- "@vitest/coverage-v8": "^3.1.1",
51
+ "@vitest/coverage-v8": "^3.1.4",
52
52
  "depcheck": "^1.4.7",
53
53
  "jest-image-snapshot": "^6.4.0",
54
54
  "prettier": "^3.5.3",
55
55
  "react": "^19.1.0",
56
+ "tsdown": "^0.12.3",
56
57
  "typescript": "^5.8.2",
57
- "vite": "^6.2.5",
58
- "vitest": "^3.1.1",
59
- "@altano/assets": "0.0.0",
60
- "@altano/build-config": "0.0.0",
61
- "@altano/testing": "0.0.0",
62
- "@altano/tsconfig": "0.0.0"
58
+ "vite": "^6.2.6",
59
+ "vitest": "^3.1.4",
60
+ "@altano/assets": "0.0.1",
61
+ "@altano/build-config": "0.0.1",
62
+ "@altano/testing": "0.0.1",
63
+ "@altano/tsconfig": "0.0.1"
63
64
  },
64
65
  "publishConfig": {
65
66
  "access": "public"
66
67
  },
67
68
  "scripts": {
68
- "build": "tsup --config build-config/tsup.config.node.ts",
69
- "build:types": "tsc --project tsconfig.declarations.json",
69
+ "build": "tsdown --config ../build-config/tsdown.config.node.ts",
70
70
  "check:exports": "attw --pack . --profile esm-only --quiet",
71
71
  "check:types:src": "tsc --noEmit",
72
72
  "check:types:test": "tsc --noEmit --project ./tests/tsconfig.json",
@@ -1,7 +0,0 @@
1
- import { TextMeasurer } from "./TextMeasurer.js";
2
- import type { Dimensions } from "../types.js";
3
- export default class BrowserTextMeasurer extends TextMeasurer {
4
- #private;
5
- getDimensions(fontSize: number): Promise<Dimensions>;
6
- }
7
- //# sourceMappingURL=BrowserTextMeasurer.d.ts.map
@@ -1,9 +0,0 @@
1
- import type { Font } from "../types.js";
2
- import { TextMeasurer } from "./TextMeasurer.js";
3
- import { Dimensions } from "../types.js";
4
- export default class HeadlessTextMeasurer extends TextMeasurer {
5
- #private;
6
- constructor(text: string, font: Font, maxWidth: number, maxHeight: number, lineHeight: number | "normal");
7
- getDimensions(fontSize: number): Promise<Dimensions>;
8
- }
9
- //# sourceMappingURL=HeadlessTextMeasurer.d.ts.map
@@ -1,14 +0,0 @@
1
- import type { Dimensions, Font } from "../types.js";
2
- export declare abstract class TextMeasurer {
3
- #private;
4
- protected text: string;
5
- protected font: Font;
6
- protected maxWidth: number;
7
- protected maxHeight: number;
8
- protected lineHeight: number | "normal";
9
- constructor(text: string, font: Font, maxWidth: number, maxHeight: number, lineHeight: number | "normal");
10
- abstract getDimensions(fontSize: number): Promise<Dimensions>;
11
- doesSizeFit(fontSize: number): Promise<boolean>;
12
- protected createSvgXmlString(fontSize: number): Promise<string>;
13
- }
14
- //# sourceMappingURL=TextMeasurer.d.ts.map
@@ -1,3 +0,0 @@
1
- import type { TextMeasurer } from "./TextMeasurer.js";
2
- export declare function getTextMeasurer(...args: ConstructorParameters<typeof TextMeasurer>): Promise<TextMeasurer>;
3
- //# sourceMappingURL=getTextMeasurer.d.ts.map