furifont 0.1.0 → 0.1.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.
Files changed (54) hide show
  1. package/dist/cli.d.mts +1 -0
  2. package/dist/cli.mjs +53 -0
  3. package/dist/client/200.html +1 -0
  4. package/dist/client/404.html +1 -0
  5. package/dist/client/_nuxt/AmQakyTh.js +1 -0
  6. package/dist/client/_nuxt/BDNMzG2s.js +1 -0
  7. package/dist/client/_nuxt/Bc3GUY-S.js +3 -0
  8. package/dist/client/_nuxt/Bs3Pv9Qv.js +1 -0
  9. package/dist/client/_nuxt/C4NVi0HP.js +75 -0
  10. package/dist/client/_nuxt/CC4_XjFP.js +3 -0
  11. package/dist/client/_nuxt/CEWHii-_.js +3 -0
  12. package/dist/client/_nuxt/CEo6aVYN.js +1 -0
  13. package/dist/client/_nuxt/CQTZHv-y.js +1 -0
  14. package/dist/client/_nuxt/CRzLMJPS.js +1 -0
  15. package/dist/client/_nuxt/Cg7TlFyt.js +1 -0
  16. package/dist/client/_nuxt/DA1XwGcq.js +1 -0
  17. package/dist/client/_nuxt/DGhFAXuQ.js +1 -0
  18. package/dist/client/_nuxt/DLl_pn_F.js +58 -0
  19. package/dist/client/_nuxt/DSYkQlQa.js +1 -0
  20. package/dist/client/_nuxt/DT7XmrQw.js +132 -0
  21. package/dist/client/_nuxt/DTC_KzWZ.js +79 -0
  22. package/dist/client/_nuxt/DhRy_lmV.js +1 -0
  23. package/dist/client/_nuxt/DmOlRIb5.js +2 -0
  24. package/dist/client/_nuxt/DmlUWPPW.js +3 -0
  25. package/dist/client/_nuxt/Dz56DRZ7.js +59 -0
  26. package/dist/client/_nuxt/HB7Qu17x.js +1 -0
  27. package/dist/client/_nuxt/JRbXhaR6.js +1 -0
  28. package/dist/client/_nuxt/builds/latest.json +1 -0
  29. package/dist/client/_nuxt/builds/meta/1bd12827-b513-45af-9638-872afc8faff4.json +1 -0
  30. package/dist/client/_nuxt/entry.CsCxOCYm.css +1 -0
  31. package/dist/client/_nuxt/error-404.DqUWOYf4.css +1 -0
  32. package/dist/client/_nuxt/error-500.BYTKxx4O.css +1 -0
  33. package/dist/client/_nuxt/g9q6jbAq.js +1 -0
  34. package/dist/client/_nuxt/jjE7wdAL.js +1 -0
  35. package/dist/client/_nuxt/nNFr3PcB.js +2 -0
  36. package/dist/client/_nuxt/nQF5Cd64.js +3 -0
  37. package/dist/client/_nuxt/y07Wd5I9.js +1 -0
  38. package/dist/client/customize/index.html +1 -0
  39. package/dist/client/docs/index.html +1 -0
  40. package/dist/client/favicon.svg +4 -0
  41. package/dist/client/furifont.user.js +69 -0
  42. package/dist/client/index.html +1 -0
  43. package/dist/client/logo.svg +4 -0
  44. package/dist/client/og.png +0 -0
  45. package/dist/client/okurigana.json +5339 -0
  46. package/dist/client/reference/index.html +1 -0
  47. package/dist/client/version.json +1 -0
  48. package/dist/config-BgEXAgR6.mjs +181 -0
  49. package/dist/devframe.d.mts +5 -0
  50. package/dist/devframe.mjs +24 -0
  51. package/dist/index.d.mts +162 -0
  52. package/dist/index.mjs +4 -0
  53. package/dist/studio-DLNpk_rE.mjs +137 -0
  54. package/package.json +5 -5
@@ -0,0 +1 @@
1
+ <!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Furifont: furigana built into the font</title><script type="importmap">{"imports":{"#entry":"/_nuxt/CC4_XjFP.js"}}</script><link rel="stylesheet" href="/_nuxt/entry.CsCxOCYm.css" crossorigin><link rel="modulepreload" as="script" crossorigin href="/_nuxt/CC4_XjFP.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/y07Wd5I9.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/DmlUWPPW.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/DmOlRIb5.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/Cg7TlFyt.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/CRzLMJPS.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/CEWHii-_.js"><link rel="modulepreload" as="script" crossorigin href="/_nuxt/BDNMzG2s.js"><script type="module" src="/_nuxt/CC4_XjFP.js" crossorigin></script><link rel="prefetch" as="script" crossorigin href="/_nuxt/DLl_pn_F.js"><link rel="prefetch" as="script" crossorigin href="/_nuxt/Dz56DRZ7.js"><link rel="prefetch" as="script" crossorigin href="/_nuxt/DT7XmrQw.js"><link rel="prefetch" as="script" crossorigin href="/_nuxt/jjE7wdAL.js"><link rel="prefetch" as="script" crossorigin href="/_nuxt/Bs3Pv9Qv.js"><link rel="prefetch" as="script" crossorigin href="/_nuxt/DA1XwGcq.js"><link rel="prefetch" as="style" crossorigin href="/_nuxt/error-404.DqUWOYf4.css"><link rel="prefetch" as="script" crossorigin href="/_nuxt/CEo6aVYN.js"><link rel="prefetch" as="style" crossorigin href="/_nuxt/error-500.BYTKxx4O.css"><link rel="prefetch" as="script" crossorigin href="/_nuxt/DhRy_lmV.js"><meta name="description" content="A Japanese font that shows furigana by itself."><meta property="og:type" content="website"><meta property="og:url" content="https://furifont.antfu.me"><meta property="og:title" content="Furifont: furigana built into the font"><meta property="og:description" content="A Japanese font that shows furigana by itself."><meta property="og:image" content="https://furifont.antfu.me/og.png"><meta property="og:image:width" content="2400"><meta property="og:image:height" content="1260"><meta property="og:image:alt" content="Furifont: furigana built into the font"><meta name="twitter:card" content="summary_large_image"><meta name="twitter:title" content="Furifont: furigana built into the font"><meta name="twitter:description" content="A Japanese font that shows furigana by itself."><meta name="twitter:image" content="https://furifont.antfu.me/og.png"><link rel="icon" type="image/svg+xml" href="favicon.svg"></head><body><div id="__nuxt"></div><div id="teleports"></div><script>window.__NUXT__={};window.__NUXT__.config={public:{devCustomizeBase:"/__furifont/"},app:{baseURL:"/",buildId:"1bd12827-b513-45af-9638-872afc8faff4",buildAssetsDir:"/_nuxt/",cdnURL:""}}</script><script type="application/json" data-nuxt-data="nuxt-app" data-ssr="false" id="__NUXT_DATA__">[{"prerenderedAt":1,"serverRendered":2},1792041185169,false]</script></body></html>
@@ -0,0 +1 @@
1
+ {"version":"0.1.2"}
@@ -0,0 +1,181 @@
1
+ import { access, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { basename, join, resolve } from "node:path";
4
+ import process from "node:process";
5
+ import { BASE_FONTS, FONT_VERSION, compileFont, readFontInfo, toWoff2 } from "@furifont/compiler";
6
+ import { brotli, subsetWasm } from "@furifont/compiler/node";
7
+ import { DEFAULTS, PlanOptionsSchema, createFontPlanner, withNfWindow } from "@furifont/core";
8
+ import { loadDictionary } from "@furifont/dictionary";
9
+ import { randomUUID } from "node:crypto";
10
+ import * as v from "valibot";
11
+ //#region package.json
12
+ var name = "furifont";
13
+ var version = "0.1.2";
14
+ var description = "Build Japanese fonts that show furigana by themselves, from the command line or a local studio";
15
+ var homepage = "https://github.com/antfu/furifont#readme";
16
+ //#endregion
17
+ //#region src/base-fonts.ts
18
+ /** Downloads in flight, so builds asking for the same font at once share one. */
19
+ const downloads = /* @__PURE__ */ new Map();
20
+ async function download(font, cacheDir, file) {
21
+ await mkdir(cacheDir, { recursive: true });
22
+ const response = await fetch(font.url);
23
+ if (!response.ok) throw new Error(`Downloading ${font.name} failed: ${response.status} ${response.statusText}`);
24
+ const part = `${file}.${randomUUID()}.part`;
25
+ try {
26
+ await writeFile(part, new Uint8Array(await response.arrayBuffer()));
27
+ await rename(part, file);
28
+ } finally {
29
+ await rm(part, { force: true });
30
+ }
31
+ return file;
32
+ }
33
+ /** Downloads a base font into `cacheDir` once, returning its path. */
34
+ async function fetchBaseFont(font, cacheDir) {
35
+ const file = join(cacheDir, basename(new URL(font.url).pathname));
36
+ if (await access(file).then(() => true, () => false)) return file;
37
+ let pending = downloads.get(file);
38
+ if (!pending) {
39
+ pending = download(font, cacheDir, file).finally(() => downloads.delete(file));
40
+ downloads.set(file, pending);
41
+ }
42
+ return pending;
43
+ }
44
+ //#endregion
45
+ //#region src/build.ts
46
+ function cacheDir() {
47
+ return join(process.env.XDG_CACHE_HOME ?? join(homedir(), ".cache"), "furifont");
48
+ }
49
+ /** The base font's path: a named one, such as Source Han Serif JP Bold, is downloaded once into the cache. */
50
+ function resolveBase(base, cwd) {
51
+ const known = BASE_FONTS.find((f) => f.name === base);
52
+ return known ? fetchBaseFont(known, cacheDir()) : Promise.resolve(resolve(cwd, base));
53
+ }
54
+ let dictionary;
55
+ let planner;
56
+ const coverages = /* @__PURE__ */ new Map();
57
+ /** The bundled dictionary, loaded once. */
58
+ function getDictionary() {
59
+ return dictionary ??= loadDictionary();
60
+ }
61
+ /** Plans a font for `config`; with `previewText`, only what that text needs. */
62
+ async function planWithBase(config, cwd, previewText) {
63
+ planner ??= getDictionary().then(createFontPlanner);
64
+ const base = await resolveBase(config.base, cwd);
65
+ let coverage = coverages.get(base);
66
+ if (!coverage) coverages.set(base, coverage = readFile(base).then((font) => new Set(readFontInfo(font).codepoints)));
67
+ return {
68
+ ...(await planner)(config, await coverage, previewText),
69
+ base
70
+ };
71
+ }
72
+ /** `<Family>-<Style>`, or a variable font's `<Family>`: its one file has every style. */
73
+ function fileStem(config, { style, variable }) {
74
+ const family = config.family.replaceAll(" ", "");
75
+ return variable ? family : `${family}-${style.replaceAll(" ", "")}`;
76
+ }
77
+ /** The reproducible-builds convention: SOURCE_DATE_EPOCH, in seconds, stands for now. */
78
+ function buildDate() {
79
+ const epoch = process.env.SOURCE_DATE_EPOCH;
80
+ return epoch ? /* @__PURE__ */ new Date(Number(epoch) * 1e3) : /* @__PURE__ */ new Date();
81
+ }
82
+ async function compileOpenType({ plan, base }, config, onProgress) {
83
+ return compileFont({
84
+ plan,
85
+ base: await readFile(base),
86
+ subsetWasm,
87
+ version: config.version ?? version,
88
+ date: buildDate(),
89
+ onProgress
90
+ });
91
+ }
92
+ /**
93
+ * Builds the OpenType font (`.otf`, or `.ttf` on a TrueType base) and `.woff2`,
94
+ * as `config.formats` lists, and the selector reference `.ivs.json` into `outDir`.
95
+ */
96
+ async function buildFont(config, options) {
97
+ const planned = await planWithBase(config, options.cwd);
98
+ const built = await compileOpenType(planned, config, options.onProgress);
99
+ const { font } = built;
100
+ const outputs = [];
101
+ if (config.formats.includes("otf")) outputs.push([built.outlines === "truetype" ? "ttf" : "otf", font]);
102
+ if (config.formats.includes("woff2")) {
103
+ options.onProgress?.({
104
+ step: "woff2",
105
+ done: null,
106
+ total: null
107
+ });
108
+ outputs.push(["woff2", await toWoff2(font, brotli)]);
109
+ }
110
+ outputs.push(["ivs.json", new TextEncoder().encode(`${JSON.stringify(planned.report.ivs)}\n`)]);
111
+ const stem = fileStem(config, built);
112
+ await mkdir(options.outDir, { recursive: true });
113
+ const files = [];
114
+ for (const [ext, data] of outputs) {
115
+ const path = join(options.outDir, `${stem}.${ext}`);
116
+ await writeFile(path, data);
117
+ files.push({
118
+ name: `${stem}.${ext}`,
119
+ path,
120
+ bytes: data.length
121
+ });
122
+ }
123
+ return {
124
+ files,
125
+ report: planned.report
126
+ };
127
+ }
128
+ /** Compiles a small font covering only `text`, for a live preview. */
129
+ async function previewFont(config, text, cwd) {
130
+ const planned = await planWithBase(config, cwd, text);
131
+ const { font, variable } = await compileOpenType(planned, config);
132
+ return {
133
+ font,
134
+ report: planned.report,
135
+ variable
136
+ };
137
+ }
138
+ /**
139
+ * Brotli quality 8 takes under a second where the WOFF2's quality 11 takes
140
+ * half a minute, and its output is this much larger: within 2% from a test
141
+ * subset to full fonts with place names. Lower qualities drift with size.
142
+ */
143
+ const FAST_WOFF2_RATIO = .885;
144
+ /** Compiles the OpenType font `config` builds, and estimates its WOFF2 from a fast Brotli pass. */
145
+ async function estimateFont(config, cwd) {
146
+ const { font } = await compileOpenType(await planWithBase(config, cwd), config);
147
+ return {
148
+ otf: font.length,
149
+ woff2: Math.round((await brotli(font, 8)).length * FAST_WOFF2_RATIO)
150
+ };
151
+ }
152
+ //#endregion
153
+ //#region src/config.ts
154
+ const CONFIG_FILE = "furifont.config.json";
155
+ /** The fonts a build can write. */
156
+ const FONT_FORMATS = ["otf", "woff2"];
157
+ const FurifontConfigSchema = withNfWindow(v.object({
158
+ ...PlanOptionsSchema.entries,
159
+ /** A Source Han Sans JP weight (downloaded once), or the path of another OpenType font. */
160
+ base: v.optional(v.pipe(v.string(), v.nonEmpty()), DEFAULTS.build.base),
161
+ /** The fonts a build writes, beside the selector reference `.ivs.json`. */
162
+ formats: v.optional(v.pipe(v.array(v.picklist(FONT_FORMATS)), v.minLength(1)), () => [...DEFAULTS.build.formats]),
163
+ /**
164
+ * The font's version, `major.minor.patch` (minor below 10, patch below 100).
165
+ * Left out, it is Furifont's own at build time, so a saved config does not pin it.
166
+ */
167
+ version: v.optional(v.pipe(v.string(), v.regex(FONT_VERSION, "version must be major.minor.patch, with a minor below 10 and a patch below 100")))
168
+ }));
169
+ /** Reads `furifont.config.json` from `cwd` (or `path`), filling in every default. */
170
+ async function loadConfig(cwd, path = CONFIG_FILE) {
171
+ const file = resolve(cwd, path);
172
+ const text = await readFile(file, "utf8").catch((error) => {
173
+ if (error.code === "ENOENT" && path === "furifont.config.json") return "{}";
174
+ throw error;
175
+ });
176
+ const result = v.safeParse(FurifontConfigSchema, JSON.parse(text));
177
+ if (!result.success) throw new Error(`${file}: ${v.summarize(result.issues)}`);
178
+ return result.output;
179
+ }
180
+ //#endregion
181
+ export { buildFont as a, planWithBase as c, homepage as d, name as f, loadConfig as i, previewFont as l, FONT_FORMATS as n, estimateFont as o, version as p, FurifontConfigSchema as r, getDictionary as s, CONFIG_FILE as t, description as u };
@@ -0,0 +1,5 @@
1
+ //#region src/devframe.d.ts
2
+ /** The studio: the app's SPA, built into `dist/client`, talking to the RPC functions of `studio.ts`. */
3
+ declare const _default: import("devframe").DevframeDefinition;
4
+ //#endregion
5
+ export { _default as default };
@@ -0,0 +1,24 @@
1
+ import { d as homepage, f as name, p as version, u as description } from "./config-BgEXAgR6.mjs";
2
+ import { n as STUDIO_ID, r as setupStudio } from "./studio-DLNpk_rE.mjs";
3
+ import { defineDevframe } from "devframe";
4
+ import { fileURLToPath } from "node:url";
5
+ //#region src/devframe.ts
6
+ /** The studio: the app's SPA, built into `dist/client`, talking to the RPC functions of `studio.ts`. */
7
+ var devframe_default = defineDevframe({
8
+ id: STUDIO_ID,
9
+ name: "Furifont",
10
+ version,
11
+ packageName: name,
12
+ description,
13
+ homepage,
14
+ icon: "ph:text-aa-duotone",
15
+ clientAssets: fileURLToPath(new URL("./client", import.meta.url)),
16
+ importMetaUrl: import.meta.url,
17
+ cli: {
18
+ command: "furifont",
19
+ port: 7392
20
+ },
21
+ setup: setupStudio
22
+ });
23
+ //#endregion
24
+ export { devframe_default as default };
@@ -0,0 +1,162 @@
1
+ import { CompileProgress } from "@furifont/compiler";
2
+ import { DEFAULTS, PREBUILT, PlanReport, PlanReport as PlanReport$1 } from "@furifont/core";
3
+ import * as v from "valibot";
4
+ import "devframe";
5
+ //#region src/config.d.ts
6
+ export declare const CONFIG_FILE = "furifont.config.json";
7
+ export declare const FurifontConfigSchema: v.SchemaWithPipe<readonly [v.ObjectSchema<{
8
+ /** A Source Han Sans JP weight (downloaded once), or the path of another OpenType font. */
9
+ readonly base: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.NonEmptyAction<string, undefined>]>, "Source Han Sans JP VF">;
10
+ /** The fonts a build writes, beside the selector reference `.ivs.json`. */
11
+ readonly formats: v.OptionalSchema<v.SchemaWithPipe<readonly [v.ArraySchema<v.PicklistSchema<readonly ["otf", "woff2"], undefined>, undefined>, v.MinLengthAction<("otf" | "woff2")[], 1, undefined>]>, () => ("otf" | "woff2")[]>;
12
+ /**
13
+ * The font's version, `major.minor.patch` (minor below 10, patch below 100).
14
+ * Left out, it is Furifont's own at build time, so a saved config does not pin it.
15
+ */
16
+ readonly version: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "version must be major.minor.patch, with a minor below 10 and a patch below 100">]>, undefined>;
17
+ readonly family: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.NonEmptyAction<string, undefined>, v.CheckAction<string, "Source is a reserved font name of the base font">]>, "Furifont Sans">;
18
+ readonly rubyScript: v.OptionalSchema<v.PicklistSchema<readonly ["hiragana", "katakanaOn", "hepburn"], undefined>, "hiragana">;
19
+ readonly nfMin: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>, v.MaxValueAction<number, 48, undefined>]>, 1>;
20
+ readonly nfMax: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>, v.MaxValueAction<number, 48, undefined>]>, 48>;
21
+ readonly selectTags: v.OptionalSchema<v.ArraySchema<v.PicklistSchema<readonly ["ichi1", "ichi2", "news1", "news2", "spec1", "spec2", "gai1", "gai2"], undefined>, undefined>, () => ("ichi1" | "news1" | "spec1" | "gai1")[]>;
22
+ readonly skipJlpt: v.OptionalSchema<v.PicklistSchema<readonly ["N5", "N4", "N3", "N2", "N1"], undefined>, undefined>;
23
+ readonly places: v.OptionalSchema<v.BooleanSchema<undefined>, true>;
24
+ readonly isolated: v.OptionalSchema<v.BooleanSchema<undefined>, true>;
25
+ readonly isolatedMinShare: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.MinValueAction<number, 0.5, undefined>, v.MaxValueAction<number, 1, undefined>]>, 0.8>;
26
+ readonly words: v.OptionalSchema<v.ObjectSchema<{
27
+ readonly add: v.OptionalSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, readonly []>;
28
+ readonly remove: v.OptionalSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, readonly []>;
29
+ readonly pin: v.OptionalSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, readonly []>;
30
+ }, undefined>, {}>;
31
+ readonly metrics: v.OptionalSchema<v.ObjectSchema<{
32
+ readonly rubyScale: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.MinValueAction<number, 0.2, undefined>, v.MaxValueAction<number, 0.8, undefined>]>, 0.5>;
33
+ readonly rubyGap: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.MinValueAction<number, 0, undefined>, v.MaxValueAction<number, 0.5, undefined>]>, 0.05>;
34
+ readonly compressionFloor: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.MinValueAction<number, 0.3, undefined>, v.MaxValueAction<number, 1, undefined>]>, 0.8>;
35
+ readonly maxRubyWidth: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.MinValueAction<number, 1, undefined>, v.MaxValueAction<number, 3, undefined>]>, 1.6>;
36
+ }, undefined>, {}>;
37
+ }, undefined>, v.CheckAction<{
38
+ base: string;
39
+ formats: ("otf" | "woff2")[];
40
+ version?: string | undefined;
41
+ family: string;
42
+ rubyScript: "hiragana" | "katakanaOn" | "hepburn";
43
+ nfMin: number;
44
+ nfMax: number;
45
+ selectTags: ("ichi1" | "news1" | "spec1" | "gai1" | "ichi2" | "news2" | "spec2" | "gai2")[];
46
+ skipJlpt?: "N5" | "N4" | "N3" | "N2" | "N1" | undefined;
47
+ places: boolean;
48
+ isolated: boolean;
49
+ isolatedMinShare: number;
50
+ words: {
51
+ add: string[];
52
+ remove: string[];
53
+ pin: string[];
54
+ };
55
+ metrics: {
56
+ rubyScale: number;
57
+ rubyGap: number;
58
+ compressionFloor: number;
59
+ maxRubyWidth: number;
60
+ };
61
+ }, string>]>;
62
+ type FurifontConfig = v.InferOutput<typeof FurifontConfigSchema>;
63
+ type FurifontConfigInput = v.InferInput<typeof FurifontConfigSchema>;
64
+ /** Reads `furifont.config.json` from `cwd` (or `path`), filling in every default. */
65
+ export declare function loadConfig(cwd: string, path?: string): Promise<FurifontConfig>;
66
+ //#endregion
67
+ //#region src/build.d.ts
68
+ interface BuiltFile {
69
+ name: string;
70
+ path: string;
71
+ bytes: number;
72
+ }
73
+ interface BuildResult {
74
+ files: BuiltFile[];
75
+ report: PlanReport$1;
76
+ }
77
+ /**
78
+ * Builds the OpenType font (`.otf`, or `.ttf` on a TrueType base) and `.woff2`,
79
+ * as `config.formats` lists, and the selector reference `.ivs.json` into `outDir`.
80
+ */
81
+ export declare function buildFont(config: FurifontConfig, options: {
82
+ cwd: string;
83
+ outDir: string;
84
+ onProgress?: (p: CompileProgress) => void;
85
+ }): Promise<BuildResult>;
86
+ /** Compiles a small font covering only `text`, for a live preview. */
87
+ export declare function previewFont(config: FurifontConfig, text: string, cwd: string): Promise<{
88
+ font: Uint8Array;
89
+ report: PlanReport$1;
90
+ variable: boolean;
91
+ }>;
92
+ /** Bytes of the fonts a build would write. */
93
+ interface SizeEstimate {
94
+ otf: number;
95
+ woff2: number;
96
+ }
97
+ /** Compiles the OpenType font `config` builds, and estimates its WOFF2 from a fast Brotli pass. */
98
+ export declare function estimateFont(config: FurifontConfig, cwd: string): Promise<SizeEstimate>;
99
+ //#endregion
100
+ //#region src/studio.d.ts
101
+ /** The RPC namespace the app's studio calls into. */
102
+ export declare const STUDIO_ID = "furifont";
103
+ /** Streaming channel carrying {@link BuildEvent}s. */
104
+ export declare const BUILD_CHANNEL = "build";
105
+ interface StudioState {
106
+ config: FurifontConfigInput;
107
+ configPath: string;
108
+ outDir: string;
109
+ sources: Record<string, string>;
110
+ /** Fonts the build downloads by name. */
111
+ baseFonts: readonly string[];
112
+ }
113
+ interface PreviewResult {
114
+ /** The preview font, base64-encoded OpenType. */
115
+ font: string;
116
+ /** Whether it has every weight, which the preview can then set. */
117
+ variable: boolean;
118
+ stats: PlanReport$1['stats'];
119
+ ms: number;
120
+ }
121
+ type BuildEvent = {
122
+ type: 'progress';
123
+ step: string;
124
+ done: number | null;
125
+ total: number | null;
126
+ } | {
127
+ type: 'done';
128
+ files: {
129
+ name: string;
130
+ url: string;
131
+ bytes: number;
132
+ }[];
133
+ stats: PlanReport$1['stats'];
134
+ } | {
135
+ type: 'error';
136
+ message: string;
137
+ };
138
+ interface WordMatch {
139
+ form: string;
140
+ readings: {
141
+ notation: string;
142
+ nf: number;
143
+ priority: string;
144
+ }[];
145
+ }
146
+ /** The wire contract of the functions below, as the studio page calls them. */
147
+ interface StudioFunctions {
148
+ 'furifont:state': () => Promise<StudioState>;
149
+ 'furifont:report': (config: FurifontConfigInput) => Promise<PlanReport$1>;
150
+ 'furifont:preview': (config: FurifontConfigInput, text: string) => Promise<PreviewResult>;
151
+ 'furifont:estimate': (config: FurifontConfigInput) => Promise<SizeEstimate>;
152
+ 'furifont:build': (config: FurifontConfigInput) => {
153
+ streamId: string;
154
+ };
155
+ 'furifont:save': (config: FurifontConfigInput) => Promise<string>;
156
+ 'furifont:search': (query: string) => Promise<WordMatch[]>;
157
+ }
158
+ declare module 'devframe' {
159
+ interface DevframeRpcServerFunctions extends StudioFunctions {}
160
+ }
161
+ //#endregion
162
+ export { type BuildEvent, type BuildResult, type BuiltFile, DEFAULTS, type FurifontConfig, type FurifontConfigInput, PREBUILT, type PlanReport, type PreviewResult, type SizeEstimate, type StudioFunctions, type StudioState, type WordMatch };
package/dist/index.mjs ADDED
@@ -0,0 +1,4 @@
1
+ import { a as buildFont, i as loadConfig, l as previewFont, o as estimateFont, r as FurifontConfigSchema, t as CONFIG_FILE } from "./config-BgEXAgR6.mjs";
2
+ import { n as STUDIO_ID, t as BUILD_CHANNEL } from "./studio-DLNpk_rE.mjs";
3
+ import { DEFAULTS, PREBUILT } from "@furifont/core";
4
+ export { BUILD_CHANNEL, CONFIG_FILE, DEFAULTS, FurifontConfigSchema, PREBUILT, STUDIO_ID, buildFont, estimateFont, loadConfig, previewFont };
@@ -0,0 +1,137 @@
1
+ import { a as buildFont, c as planWithBase, i as loadConfig, l as previewFont, o as estimateFont, r as FurifontConfigSchema, s as getDictionary, t as CONFIG_FILE } from "./config-BgEXAgR6.mjs";
2
+ import { mkdir, writeFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { BASE_FONTS } from "@furifont/compiler";
5
+ import { formatNotation, hanRuns, parseFurigana } from "@furifont/core";
6
+ import * as v from "valibot";
7
+ import { Buffer } from "node:buffer";
8
+ import { defineRpcFunction } from "devframe";
9
+ //#region src/studio.ts
10
+ /** The RPC namespace the app's studio calls into. */
11
+ const STUDIO_ID = "furifont";
12
+ /** Streaming channel carrying {@link BuildEvent}s. */
13
+ const BUILD_CHANNEL = "build";
14
+ /** Where built fonts are served from, relative to the studio. */
15
+ const FONTS_PATH = "/fonts/";
16
+ const SEARCH_LIMIT = 40;
17
+ async function searchWords(query) {
18
+ const matches = /* @__PURE__ */ new Map();
19
+ for (const entry of (await getDictionary()).entries) {
20
+ if (!entry.form.includes(query)) continue;
21
+ let match = matches.get(entry.form);
22
+ if (!match) {
23
+ if (matches.size === SEARCH_LIMIT) break;
24
+ matches.set(entry.form, match = {
25
+ form: entry.form,
26
+ readings: []
27
+ });
28
+ }
29
+ const spans = parseFurigana(entry.furigana);
30
+ if (hanRuns(entry.form, spans).length) match.readings.push({
31
+ notation: formatNotation(entry.form, spans),
32
+ nf: entry.nf,
33
+ priority: entry.priority
34
+ });
35
+ }
36
+ return [...matches.values()].filter((m) => m.readings.length).sort((a, b) => a.form.length - b.form.length);
37
+ }
38
+ /** Every function the studio page calls; registered under {@link STUDIO_ID}. */
39
+ async function setupStudio(ctx) {
40
+ const scope = ctx.scope(STUDIO_ID);
41
+ const outDir = join(ctx.cwd, "fonts");
42
+ await mkdir(outDir, { recursive: true });
43
+ ctx.views.hostStatic(FONTS_PATH, outDir);
44
+ const channel = scope.rpc.streaming.create(BUILD_CHANNEL, { replayWindow: 256 });
45
+ const parse = (input) => v.parse(FurifontConfigSchema, input);
46
+ const Text = v.pipe(v.string(), v.maxLength(2e3));
47
+ const Query = v.pipe(v.string(), v.trim(), v.nonEmpty(), v.maxLength(20));
48
+ const functions = [
49
+ defineRpcFunction({
50
+ name: "state",
51
+ type: "query",
52
+ jsonSerializable: true,
53
+ handler: async () => ({
54
+ config: await loadConfig(ctx.cwd),
55
+ configPath: join(ctx.cwd, CONFIG_FILE),
56
+ outDir,
57
+ sources: (await getDictionary()).sources,
58
+ baseFonts: BASE_FONTS.map((f) => f.name)
59
+ })
60
+ }),
61
+ defineRpcFunction({
62
+ name: "report",
63
+ type: "query",
64
+ jsonSerializable: true,
65
+ handler: async (options) => (await planWithBase(parse(options), ctx.cwd)).report
66
+ }),
67
+ defineRpcFunction({
68
+ name: "preview",
69
+ type: "action",
70
+ jsonSerializable: true,
71
+ handler: async (options, text) => {
72
+ const started = performance.now();
73
+ const { font, report, variable } = await previewFont(parse(options), v.parse(Text, text), ctx.cwd);
74
+ return {
75
+ font: Buffer.from(font).toString("base64"),
76
+ variable,
77
+ stats: report.stats,
78
+ ms: Math.round(performance.now() - started)
79
+ };
80
+ }
81
+ }),
82
+ defineRpcFunction({
83
+ name: "estimate",
84
+ type: "query",
85
+ jsonSerializable: true,
86
+ handler: (options) => estimateFont(parse(options), ctx.cwd)
87
+ }),
88
+ defineRpcFunction({
89
+ name: "build",
90
+ type: "action",
91
+ jsonSerializable: true,
92
+ handler: (options) => {
93
+ const parsed = parse(options);
94
+ const stream = channel.start();
95
+ buildFont(parsed, {
96
+ cwd: ctx.cwd,
97
+ outDir,
98
+ onProgress: (p) => stream.write({
99
+ type: "progress",
100
+ ...p
101
+ })
102
+ }).then(({ files, report }) => stream.write({
103
+ type: "done",
104
+ files: files.map((f) => ({
105
+ name: f.name,
106
+ url: `.${FONTS_PATH}${f.name}`,
107
+ bytes: f.bytes
108
+ })),
109
+ stats: report.stats
110
+ })).catch((error) => stream.write({
111
+ type: "error",
112
+ message: error.message
113
+ })).finally(() => stream.close());
114
+ return { streamId: stream.id };
115
+ }
116
+ }),
117
+ defineRpcFunction({
118
+ name: "save",
119
+ type: "action",
120
+ jsonSerializable: true,
121
+ handler: async (options) => {
122
+ const path = join(ctx.cwd, CONFIG_FILE);
123
+ await writeFile(path, `${JSON.stringify(parse(options), null, 2)}\n`);
124
+ return path;
125
+ }
126
+ }),
127
+ defineRpcFunction({
128
+ name: "search",
129
+ type: "query",
130
+ jsonSerializable: true,
131
+ handler: (query) => searchWords(v.parse(Query, query))
132
+ })
133
+ ];
134
+ for (const fn of functions) scope.rpc.register(fn);
135
+ }
136
+ //#endregion
137
+ export { STUDIO_ID as n, setupStudio as r, BUILD_CHANNEL as t };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "furifont",
3
3
  "type": "module",
4
- "version": "0.1.0",
4
+ "version": "0.1.2",
5
5
  "description": "Build Japanese fonts that show furigana by themselves, from the command line or a local studio",
6
6
  "author": "Anthony Fu <anthonyfu117@hotmail.com>",
7
7
  "license": "MIT",
@@ -36,11 +36,11 @@
36
36
  "dist"
37
37
  ],
38
38
  "dependencies": {
39
- "@furifont/compiler": "0.1.0",
40
- "@furifont/core": "0.1.0",
41
- "@furifont/dictionary": "0.1.0",
39
+ "@furifont/compiler": "0.1.2",
40
+ "@furifont/core": "0.1.2",
41
+ "@furifont/dictionary": "0.1.2",
42
42
  "cac": "^7.1.0",
43
- "devframe": "^1.2.4",
43
+ "devframe": "^1.2.5",
44
44
  "valibot": "^1.5.0"
45
45
  },
46
46
  "scripts": {