@softure-ai/core 0.1.10 → 0.1.11

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.
Files changed (52) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +31 -1
  3. package/dist/calendar-day.d.ts +3 -0
  4. package/dist/calendar-day.d.ts.map +1 -1
  5. package/dist/calendar-day.js +5 -0
  6. package/dist/calendar-day.js.map +1 -1
  7. package/dist/config.d.ts +7 -0
  8. package/dist/config.d.ts.map +1 -1
  9. package/dist/config.js +2 -0
  10. package/dist/config.js.map +1 -1
  11. package/dist/format.d.ts +26 -3
  12. package/dist/format.d.ts.map +1 -1
  13. package/dist/format.js +82 -10
  14. package/dist/format.js.map +1 -1
  15. package/dist/index.d.ts +3 -2
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +3 -2
  18. package/dist/index.js.map +1 -1
  19. package/dist/memoize.d.ts +16 -0
  20. package/dist/memoize.d.ts.map +1 -0
  21. package/dist/memoize.js +81 -0
  22. package/dist/memoize.js.map +1 -0
  23. package/dist/messages/en.d.ts +15 -0
  24. package/dist/messages/en.d.ts.map +1 -1
  25. package/dist/messages/en.js +5 -0
  26. package/dist/messages/en.js.map +1 -1
  27. package/dist/messages/index.d.ts +1 -1
  28. package/dist/messages/index.d.ts.map +1 -1
  29. package/dist/messages/index.js +1 -1
  30. package/dist/messages/index.js.map +1 -1
  31. package/dist/messages/pl.d.ts.map +1 -1
  32. package/dist/messages/pl.js +5 -0
  33. package/dist/messages/pl.js.map +1 -1
  34. package/dist/og/index.d.ts +2 -0
  35. package/dist/og/index.d.ts.map +1 -0
  36. package/dist/og/index.js +4 -0
  37. package/dist/og/index.js.map +1 -0
  38. package/dist/og/og-fonts.d.ts +37 -0
  39. package/dist/og/og-fonts.d.ts.map +1 -0
  40. package/dist/og/og-fonts.js +95 -0
  41. package/dist/og/og-fonts.js.map +1 -0
  42. package/package.json +6 -1
  43. package/src/calendar-day.ts +4 -0
  44. package/src/config.ts +9 -0
  45. package/src/format.ts +89 -11
  46. package/src/index.ts +14 -1
  47. package/src/memoize.ts +89 -0
  48. package/src/messages/en.ts +5 -0
  49. package/src/messages/index.ts +1 -1
  50. package/src/messages/pl.ts +5 -0
  51. package/src/og/index.ts +12 -0
  52. package/src/og/og-fonts.ts +127 -0
@@ -0,0 +1,127 @@
1
+ // The loader of an OG card's fonts: each source is read once, checked to be a font Satori draws (.ttf,
2
+ // .otf, .woff; not .woff2), and kept for the life of the loader. A failed read is not kept, so a fixed
3
+ // file is picked up by the next call. It imports no Next code, so a command-line check can run it too.
4
+ // `@softure-ai/seo/server` and the blog re-export it.
5
+ import { readFile } from "node:fs/promises";
6
+ import { isAbsolute, resolve } from "node:path";
7
+
8
+ /** The weights Satori (`next/og`) draws: static fonts only, in steps of 100. */
9
+ export const OG_FONT_WEIGHTS = [100, 200, 300, 400, 500, 600, 700, 800, 900] as const;
10
+
11
+ export type OgFontWeight = (typeof OG_FONT_WEIGHTS)[number];
12
+
13
+ /** A font to read: a path (from the loader's root, or absolute) or an https URL, and how the card names it. */
14
+ export interface OgFontSource {
15
+ readonly name: string;
16
+ readonly src: string;
17
+ readonly weight?: OgFontWeight;
18
+ readonly style?: "normal" | "italic";
19
+ }
20
+
21
+ /** A font for the card, as `ImageResponse` takes it. */
22
+ export interface OgFont {
23
+ readonly name: string;
24
+ readonly data: ArrayBuffer;
25
+ readonly weight?: OgFontWeight;
26
+ readonly style?: "normal" | "italic";
27
+ }
28
+
29
+ export type OgFontsResult = { readonly ok: true; readonly fonts: readonly OgFont[] } | { readonly ok: false; readonly error: string };
30
+
31
+ type BytesResult = { readonly ok: true; readonly data: ArrayBuffer } | { readonly ok: false; readonly reason: string };
32
+
33
+ export interface OgFontLoaderOptions {
34
+ /** Where a relative path starts: the app's root. */
35
+ readonly root: string;
36
+ readonly readFile?: (path: string) => Promise<Uint8Array>;
37
+ readonly fetchImpl?: typeof fetch;
38
+ /** What a message names the list by, before `[index]`. Default: `OG card: fonts`. */
39
+ readonly label?: string;
40
+ }
41
+
42
+ const DEFAULT_LABEL = "OG card: fonts";
43
+
44
+ /** The first four bytes of each font format Satori parses. */
45
+ const FONT_SIGNATURES = ["wOFF", "OTTO", "\u0000\u0001\u0000\u0000", "true"];
46
+ const WOFF2_SIGNATURE = "wOF2";
47
+
48
+ function toArrayBuffer(bytes: Uint8Array): ArrayBuffer {
49
+ const copy = new Uint8Array(bytes.byteLength);
50
+ copy.set(bytes);
51
+ return copy.buffer;
52
+ }
53
+
54
+ function checkFontBytes(bytes: Uint8Array): BytesResult {
55
+ const signature = String.fromCharCode(...bytes.subarray(0, 4));
56
+ if (signature === WOFF2_SIGNATURE) return { ok: false, reason: "a .woff2 font, which the card cannot read; use the .woff or .ttf file" };
57
+ if (!FONT_SIGNATURES.includes(signature)) return { ok: false, reason: "not a .ttf, .otf or .woff font" };
58
+ return { ok: true, data: toArrayBuffer(bytes) };
59
+ }
60
+
61
+ function getErrorCode(error: unknown): string {
62
+ if (typeof error === "object" && error !== null && "code" in error && typeof error.code === "string") return error.code;
63
+ return error instanceof Error ? error.message : String(error);
64
+ }
65
+
66
+ /** A loader with its own cache, keyed by the resolved path or URL. */
67
+ export function createOgFontLoader(options: OgFontLoaderOptions): (fonts: readonly OgFontSource[]) => Promise<OgFontsResult> {
68
+ const read = options.readFile ?? readFile;
69
+ const fetchImpl = options.fetchImpl ?? fetch;
70
+ const label = options.label ?? DEFAULT_LABEL;
71
+ const cache = new Map<string, Promise<BytesResult>>();
72
+
73
+ async function readSource(location: string, isUrl: boolean): Promise<BytesResult> {
74
+ if (!isUrl) {
75
+ try {
76
+ return checkFontBytes(await read(location));
77
+ } catch (error) {
78
+ return { ok: false, reason: `the file cannot be read (${getErrorCode(error)})` };
79
+ }
80
+ }
81
+ let response: Response;
82
+ try {
83
+ response = await fetchImpl(location);
84
+ } catch (error) {
85
+ return { ok: false, reason: `the request failed (${getErrorCode(error)})` };
86
+ }
87
+ if (!response.ok) return { ok: false, reason: `the server answered ${response.status}` };
88
+ try {
89
+ return checkFontBytes(new Uint8Array(await response.arrayBuffer()));
90
+ } catch (error) {
91
+ return { ok: false, reason: `the answer could not be read (${getErrorCode(error)})` };
92
+ }
93
+ }
94
+
95
+ function getBytes(location: string, isUrl: boolean): Promise<BytesResult> {
96
+ const cached = cache.get(location);
97
+ if (cached !== undefined) return cached;
98
+ const pending = readSource(location, isUrl).then((result) => {
99
+ if (!result.ok) cache.delete(location);
100
+ return result;
101
+ });
102
+ cache.set(location, pending);
103
+ return pending;
104
+ }
105
+
106
+ return async (fonts) => {
107
+ const located = fonts.map((font) => {
108
+ const isUrl = font.src.startsWith("https://");
109
+ return { font, isUrl, location: isUrl || isAbsolute(font.src) ? font.src : resolve(options.root, font.src) };
110
+ });
111
+ const results = await Promise.all(located.map(async (entry) => ({ ...entry, result: await getBytes(entry.location, entry.isUrl) })));
112
+ const loaded: OgFont[] = [];
113
+ for (const [index, { font, location, result }] of results.entries()) {
114
+ if (!result.ok) return { ok: false, error: `${label}[${index}] "${font.src}": ${result.reason} (${location}).` };
115
+ loaded.push({ name: font.name, data: result.data, weight: font.weight, style: font.style });
116
+ }
117
+ return { ok: true, fonts: loaded };
118
+ };
119
+ }
120
+
121
+ let loadFromWorkingDirectory: ((fonts: readonly OgFontSource[]) => Promise<OgFontsResult>) | undefined;
122
+
123
+ /** The fonts for an app's own OG card, read from the working directory (the app's root, where Next runs) and cached for the process. */
124
+ export function loadOgFonts(fonts: readonly OgFontSource[]): Promise<OgFontsResult> {
125
+ loadFromWorkingDirectory ??= createOgFontLoader({ root: process.cwd() });
126
+ return loadFromWorkingDirectory(fonts);
127
+ }