@altano/satori-fit-text 0.1.3 → 1.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.
package/README.md CHANGED
@@ -21,9 +21,11 @@ pnpm add @altano/satori-fit-text
21
21
  import { findLargestUsableFontSize, type Font } from "@altano/satori-fit-text";
22
22
 
23
23
  async function getInter(): Promise<Font> {
24
- const interSemiBoldBuffer = await fetch(`https://cdn.jsdelivr.net/npm/@fontsource/inter@5.0.17/files/inter-latin-600-normal.woff`).then((res) => res.arrayBuffer());
24
+ const interSemiBoldBuffer = await fetch(
25
+ `https://cdn.jsdelivr.net/npm/@fontsource/inter@5.0.17/files/inter-latin-600-normal.woff`,
26
+ ).then((res) => res.arrayBuffer());
25
27
 
26
- const font = {
28
+ return {
27
29
  name: "Inter",
28
30
  data: interSemiBoldBuffer,
29
31
  weight: 600,
@@ -32,7 +34,7 @@ async function getInter(): Promise<Font> {
32
34
 
33
35
  const largestUsableFontSize = await findLargestUsableFontSize({
34
36
  lineHeight: 1,
35
- font: await getFont(),
37
+ font: await getInter(),
36
38
  text: "Some text I want to be as big as possible",
37
39
  maxWidth: 1136,
38
40
  maxHeight: 429,
@@ -46,7 +48,7 @@ console.log(largestUsableFontSize);
46
48
  - [React Component (sandbox)](https://codesandbox.io/s/altano-satori-fit-text-browser-example-j2gvk6)
47
49
  - [Next.js Open Graph Card - Simple (codesandbox)](https://codesandbox.io/p/sandbox/altano-satori-fit-text-next-js-demo-sv4flc)
48
50
  - [Next.js Open Graph Card - Complex](https://github.com/altano/alan.norbauer.com/blob/main/src/components/opengraph/image.tsx#L29-L35)
49
- - [Node.js CLI App (codesandbox)](https://codesandbox.io/p/sandbox/github/altano/npm-packages/tree/main/codesandboxes/satori-fit-text-node-cli?file=%2Fpackage.json%3A1%2C1)
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)
50
52
 
51
53
  ## Implementation
52
54
 
@@ -1,4 +1,5 @@
1
- import { TextMeasurer, type Dimensions } from "./TextMeasurer";
1
+ import { TextMeasurer } from "./TextMeasurer.js";
2
+ import type { Dimensions } from "../types.js";
2
3
  export default class BrowserTextMeasurer extends TextMeasurer {
3
4
  #private;
4
5
  getDimensions(fontSize: number): Promise<Dimensions>;
@@ -1,6 +1,5 @@
1
- import log from "../log";
2
- import { TextMeasurer } from "./TextMeasurer";
3
- import getWidthAdjustedForInlineBleed from "./getWidthAdjustedForInlineBleed";
1
+ import log from "../log.js";
2
+ import { TextMeasurer } from "./TextMeasurer.js";
4
3
  class BrowserTextMeasurer extends TextMeasurer {
5
4
  #getSvgElement(svgText) {
6
5
  const div = document.createElement("div");
@@ -17,17 +16,16 @@ class BrowserTextMeasurer extends TextMeasurer {
17
16
  const svg = this.#getSvgElement(svgText);
18
17
  try {
19
18
  document.body.appendChild(svg);
20
- const { width, height } = svg.getBBox();
21
- const widthAdjusted = getWidthAdjustedForInlineBleed(svg, width);
19
+ const dimensions = svg.getBBox();
20
+ const { width, height } = dimensions;
22
21
  log({
23
22
  fontSize,
24
- width,
25
- widthAdjusted,
26
- height,
27
- widthFits: widthAdjusted <= this.maxWidth,
28
- heightFits: height <= this.maxHeight
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}`
29
27
  });
30
- return { width: widthAdjusted, height };
28
+ return dimensions;
31
29
  } finally {
32
30
  svg.parentElement?.removeChild?.(svg);
33
31
  }
@@ -1,5 +1,6 @@
1
- import type { Font } from "../types";
2
- import { TextMeasurer, type Dimensions } from "./TextMeasurer";
1
+ import type { Font } from "../types.js";
2
+ import { TextMeasurer } from "./TextMeasurer.js";
3
+ import { Dimensions } from "../types.js";
3
4
  export default class HeadlessTextMeasurer extends TextMeasurer {
4
5
  #private;
5
6
  constructor(text: string, font: Font, maxWidth: number, maxHeight: number, lineHeight: number | "normal");
@@ -1,8 +1,7 @@
1
1
  import { SVG, registerWindow } from "@svgdotjs/svg.js";
2
2
  import { createSVGWindow } from "svgdom";
3
- import log from "../log";
4
- import { TextMeasurer } from "./TextMeasurer";
5
- import getWidthAdjustedForInlineBleed from "./getWidthAdjustedForInlineBleed";
3
+ import log from "../log.js";
4
+ import { TextMeasurer } from "./TextMeasurer.js";
6
5
  class HeadlessTextMeasurer extends TextMeasurer {
7
6
  #canvas;
8
7
  constructor(text, font, maxWidth, maxHeight, lineHeight) {
@@ -14,20 +13,16 @@ class HeadlessTextMeasurer extends TextMeasurer {
14
13
  }
15
14
  async getDimensions(fontSize) {
16
15
  await this.#setFontSize(fontSize);
17
- const { width, height } = this.#canvas.bbox();
18
- const widthAdjusted = getWidthAdjustedForInlineBleed(
19
- this.#canvas.node,
20
- width
21
- );
16
+ const dimensions = this.#canvas.bbox();
17
+ const { width, height } = dimensions;
22
18
  log({
23
19
  fontSize,
24
- width,
25
- widthAdjusted,
26
- height,
27
- widthFits: widthAdjusted <= this.maxWidth,
28
- heightFits: height <= this.maxHeight
20
+ width: `${width}px`,
21
+ height: `${height}px`,
22
+ widthFits: `${width}px ${width <= this.maxWidth ? "DOES" : "does NOT"} fit in ${this.maxWidth}`,
23
+ heightFits: `${height}px ${height <= this.maxHeight ? "DOES" : "does NOT"} fit in ${this.maxHeight}`
29
24
  });
30
- return { width: widthAdjusted, height };
25
+ return dimensions;
31
26
  }
32
27
  async #setFontSize(fontSize) {
33
28
  const svgText = await this.generateSvg(fontSize);
@@ -1,5 +1,4 @@
1
- import type { Font } from "../types";
2
- export type Dimensions = Pick<DOMRect, "width" | "height">;
1
+ import type { Dimensions, Font } from "../types.js";
3
2
  export declare abstract class TextMeasurer {
4
3
  #private;
5
4
  protected text: string;
@@ -35,13 +35,6 @@ class TextMeasurer {
35
35
  margin: 0,
36
36
  padding: 0,
37
37
  boxSizing: "border-box",
38
- // We render a border so that we measure the true dimensions of the text.
39
- // Without one, the bounding box might not include significant whitespace.
40
- // For example, if the last line of text has no descenders, the bounding box
41
- // would only extend to the baseline of the last line. With a border, the
42
- // bounding box will include all the whitespace between the baseline and the
43
- // descent of the text.
44
- border: "solid red 1px",
45
38
  // Dynamic styles based on input
46
39
  fontSize,
47
40
  fontFamily: this.font.name,
@@ -1,3 +1,3 @@
1
- import type { TextMeasurer } from "./TextMeasurer";
1
+ import type { TextMeasurer } from "./TextMeasurer.js";
2
2
  export declare function getTextMeasurer(...args: ConstructorParameters<typeof TextMeasurer>): Promise<TextMeasurer>;
3
3
  //# sourceMappingURL=getTextMeasurer.d.ts.map
@@ -1,9 +1,9 @@
1
1
  async function getTextMeasurer(...args) {
2
2
  if (typeof window !== "undefined") {
3
- const Measurer = await import("./BrowserTextMeasurer");
3
+ const Measurer = await import("./BrowserTextMeasurer.js");
4
4
  return new Measurer.default(...args);
5
5
  } else {
6
- const Measurer = await import("./HeadlessTextMeasurer");
6
+ const Measurer = await import("./HeadlessTextMeasurer.js");
7
7
  return new Measurer.default(...args);
8
8
  }
9
9
  }
package/dist/fitText.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Font } from "./types";
1
+ import type { Font } from "./types.js";
2
2
  export type FindOptions = {
3
3
  /**
4
4
  * The text you want to fit
package/dist/fitText.js CHANGED
@@ -1,4 +1,4 @@
1
- import { getTextMeasurer } from "./TextMeasurer/getTextMeasurer";
1
+ import { getTextMeasurer } from "./TextMeasurer/getTextMeasurer.js";
2
2
  async function findLargestUsableFontSize({
3
3
  text,
4
4
  font,
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- export * from "./fitText";
2
- export * from "./types";
1
+ export * from "./fitText.js";
2
+ export * from "./types.js";
3
3
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- export * from "./fitText";
2
- export * from "./types";
1
+ export * from "./fitText.js";
2
+ export * from "./types.js";
package/dist/types.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  import type { Font as SatoriFont } from "satori";
2
+ export type Dimensions = Pick<DOMRect, "width" | "height">;
2
3
  export type Font = SatoriFont;
3
4
  //# sourceMappingURL=types.d.ts.map
package/package.json CHANGED
@@ -1,88 +1,91 @@
1
1
  {
2
2
  "name": "@altano/satori-fit-text",
3
- "version": "0.1.3",
3
+ "version": "1.0.0",
4
4
  "description": "Fit text to a bounding box in Node or the browser",
5
- "type": "module",
6
- "sideEffects": false,
7
- "exports": "./dist/index.js",
8
- "types": "./dist/index.d.ts",
9
- "author": {
10
- "name": "Alan Norbauer",
11
- "email": "altano@gmail.com",
12
- "url": "https://alan.norbauer.com"
13
- },
14
- "license": "ISC",
5
+ "keywords": [
6
+ "fit-text",
7
+ "opengraph",
8
+ "satori",
9
+ "typography"
10
+ ],
11
+ "homepage": "https://github.com/altano/npm-packages/tree/main/packages/satori-fit-text",
15
12
  "repository": {
16
13
  "type": "git",
17
14
  "url": "https://github.com/altano/npm-packages",
18
15
  "directory": "packages/satori-fit-text"
19
16
  },
20
- "homepage": "https://github.com/altano/npm-packages/tree/main/packages/satori-fit-text",
17
+ "license": "ISC",
18
+ "author": {
19
+ "name": "Alan Norbauer",
20
+ "email": "altano@gmail.com",
21
+ "url": "https://alan.norbauer.com"
22
+ },
23
+ "sideEffects": false,
24
+ "type": "module",
25
+ "exports": "./dist/index.js",
26
+ "types": "./dist/index.d.ts",
21
27
  "files": [
22
28
  "assets/",
23
29
  "dist/**/*.js",
24
30
  "dist/**/*.d.ts"
25
31
  ],
26
- "publishConfig": {
27
- "access": "public"
28
- },
29
- "keywords": [
30
- "typography",
31
- "satori",
32
- "opengraph",
33
- "fit-text"
34
- ],
35
32
  "dependencies": {
36
33
  "@svgdotjs/svg.js": "^3.2.0",
37
34
  "debug": "^4.3.4",
38
- "satori": "^0.10.13",
39
- "satori-html": "^0.3.2",
35
+ "satori": "^0.12.2",
40
36
  "svgdom": "^0.1.19"
41
37
  },
42
38
  "devDependencies": {
39
+ "@arethetypeswrong/cli": "^0.15.4",
43
40
  "@fontsource-variable/inter": "^5.0.17",
44
41
  "@fontsource/inter": "^5.0.17",
45
- "@playwright/test": "1.40.0",
42
+ "@playwright/test": "1.47.0",
46
43
  "@resvg/resvg-js": "^2.6.0",
47
44
  "@types/debug": "^4.1.12",
48
45
  "@types/he": "^1.2.3",
49
46
  "@types/jest-image-snapshot": "^6.4.0",
50
- "@types/node": "^20.11.25",
47
+ "@types/node": "^22.13.10",
51
48
  "@types/react": "^18.2.64",
52
49
  "@types/react-dom": "^18.2.21",
53
50
  "@types/svgdom": "^0.1.2",
54
- "@vitest/coverage-v8": "^1.4.0",
55
- "@vitest/ui": "^1.4.0",
51
+ "@vitest/coverage-v8": "^3.0.9",
52
+ "depcheck": "^1.4.7",
56
53
  "jest-image-snapshot": "^6.4.0",
57
- "npm-run-all": "^4.1.5",
58
- "prettier": "^3.2.5",
59
- "react": "^18.2.0",
54
+ "prettier": "^3.3.3",
55
+ "react": "^18.3.1",
60
56
  "react-dom": "^18.2.0",
61
- "typescript": "^5.4.2",
62
- "vite": "^5.2.4",
63
- "vitest": "^1.4.0",
57
+ "typescript": "^5.8.2",
58
+ "vite": "^6.2.3",
59
+ "vitest": "^3.0.9",
64
60
  "@altano/assets": "0.0.0",
65
- "@altano/tiny-async-pool": "3.0.0",
66
- "@altano/tsconfig": "0.0.0"
61
+ "@altano/build-config": "0.0.0",
62
+ "@altano/testing": "0.0.0",
63
+ "@altano/tiny-async-pool": "3.0.2",
64
+ "@altano/tsconfig": "0.0.0",
65
+ "@altano/vitest-plugins": "1.0.0"
66
+ },
67
+ "publishConfig": {
68
+ "access": "public"
67
69
  },
68
70
  "scripts": {
69
71
  "build": "tsup --config build-config/tsup.config.node.ts",
70
72
  "build:types": "tsc --project tsconfig.declarations.json",
73
+ "check:exports": "attw --pack . --ignore-rules=cjs-resolves-to-esm",
74
+ "check:types:src": "tsc --noEmit",
75
+ "check:types:test": "tsc --noEmit --project ./tests/tsconfig.json",
76
+ "check:unused-dependencies": "depcheck",
71
77
  "clean": "rm -rf .turbo && rm -rf .tsbuildinfo && rm -rf node_modules && rm -rf dist",
72
78
  "dev": "pnpm run build --watch",
73
79
  "format": "prettier --check src",
74
80
  "format:fix": "prettier --write src",
75
- "lint": "eslint package.json src/**/*.ts*",
81
+ "lint": "eslint",
76
82
  "lint:fix": "TIMING=1 pnpm lint --fix",
77
83
  "lint:timing": "TIMING=1 pnpm lint",
78
84
  "test:e2e": "playwright test",
79
85
  "test:e2e:server": "vite --strictPort --port 3717 ./tests/e2e",
80
86
  "test:e2e:ui": "playwright test --ui",
81
- "test:unit": "run-s test:unit:run test:unit:benchmark",
87
+ "test:unit": "vitest --run",
82
88
  "test:unit:benchmark": "vitest --run bench",
83
- "test:unit:run": "vitest --run",
84
- "test:unit:watch": "vitest tests/unit/",
85
- "typecheck:src": "tsc --noEmit",
86
- "typecheck:test": "tsc --noEmit --project ./tests/tsconfig.json"
89
+ "test:unit:watch": "vitest"
87
90
  }
88
91
  }
@@ -1,16 +0,0 @@
1
- /**
2
- * Satori will sometimes render text slightly outside the div on the left (e.g.
3
- * x:-2). Having text slightly outside the box and drawing a border means we
4
- * will have a bounding box on the svg that is slightly larger than the actual
5
- * text size (by the amount of bleed).
6
- *
7
- * For example, if text starts at -2 and the div's bounding box is x:0 to
8
- * x:1200, the svg's bounding box will have width=1202px. And that won't
9
- * represent the actual width of the text.
10
- *
11
- * Let's account for this by just reducing the bounding box width by the amount
12
- * the text is bleeding outside the left or right and trust that Satori isn't
13
- * both letting the text bleed AND filling too much space.
14
- */
15
- export default function getWidthAdjustedForInlineBleed(svg: SVGSVGElement, svgWidth: number): number;
16
- //# sourceMappingURL=getWidthAdjustedForInlineBleed.d.ts.map
@@ -1,17 +0,0 @@
1
- function getWidthAdjustedForInlineBleed(svg, svgWidth) {
2
- let smallestXValue = 0;
3
- let largestYValue = svgWidth;
4
- for (const child of svg.children) {
5
- if ("getBBox" in child && typeof child["getBBox"] === "function") {
6
- const { x, y } = child.getBBox();
7
- smallestXValue = Math.min(smallestXValue, x);
8
- largestYValue = Math.max(largestYValue, y);
9
- }
10
- }
11
- const xAdjustment = Math.ceil(Math.abs(smallestXValue));
12
- const yAdjustment = largestYValue <= svgWidth ? 0 : Math.ceil(largestYValue) - svgWidth;
13
- return svgWidth - xAdjustment - yAdjustment;
14
- }
15
- export {
16
- getWidthAdjustedForInlineBleed as default
17
- };