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.
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +53 -0
- package/dist/client/200.html +1 -0
- package/dist/client/404.html +1 -0
- package/dist/client/_nuxt/AmQakyTh.js +1 -0
- package/dist/client/_nuxt/BDNMzG2s.js +1 -0
- package/dist/client/_nuxt/Bc3GUY-S.js +3 -0
- package/dist/client/_nuxt/Bs3Pv9Qv.js +1 -0
- package/dist/client/_nuxt/C4NVi0HP.js +75 -0
- package/dist/client/_nuxt/CC4_XjFP.js +3 -0
- package/dist/client/_nuxt/CEWHii-_.js +3 -0
- package/dist/client/_nuxt/CEo6aVYN.js +1 -0
- package/dist/client/_nuxt/CQTZHv-y.js +1 -0
- package/dist/client/_nuxt/CRzLMJPS.js +1 -0
- package/dist/client/_nuxt/Cg7TlFyt.js +1 -0
- package/dist/client/_nuxt/DA1XwGcq.js +1 -0
- package/dist/client/_nuxt/DGhFAXuQ.js +1 -0
- package/dist/client/_nuxt/DLl_pn_F.js +58 -0
- package/dist/client/_nuxt/DSYkQlQa.js +1 -0
- package/dist/client/_nuxt/DT7XmrQw.js +132 -0
- package/dist/client/_nuxt/DTC_KzWZ.js +79 -0
- package/dist/client/_nuxt/DhRy_lmV.js +1 -0
- package/dist/client/_nuxt/DmOlRIb5.js +2 -0
- package/dist/client/_nuxt/DmlUWPPW.js +3 -0
- package/dist/client/_nuxt/Dz56DRZ7.js +59 -0
- package/dist/client/_nuxt/HB7Qu17x.js +1 -0
- package/dist/client/_nuxt/JRbXhaR6.js +1 -0
- package/dist/client/_nuxt/builds/latest.json +1 -0
- package/dist/client/_nuxt/builds/meta/1bd12827-b513-45af-9638-872afc8faff4.json +1 -0
- package/dist/client/_nuxt/entry.CsCxOCYm.css +1 -0
- package/dist/client/_nuxt/error-404.DqUWOYf4.css +1 -0
- package/dist/client/_nuxt/error-500.BYTKxx4O.css +1 -0
- package/dist/client/_nuxt/g9q6jbAq.js +1 -0
- package/dist/client/_nuxt/jjE7wdAL.js +1 -0
- package/dist/client/_nuxt/nNFr3PcB.js +2 -0
- package/dist/client/_nuxt/nQF5Cd64.js +3 -0
- package/dist/client/_nuxt/y07Wd5I9.js +1 -0
- package/dist/client/customize/index.html +1 -0
- package/dist/client/docs/index.html +1 -0
- package/dist/client/favicon.svg +4 -0
- package/dist/client/furifont.user.js +69 -0
- package/dist/client/index.html +1 -0
- package/dist/client/logo.svg +4 -0
- package/dist/client/og.png +0 -0
- package/dist/client/okurigana.json +5339 -0
- package/dist/client/reference/index.html +1 -0
- package/dist/client/version.json +1 -0
- package/dist/config-BgEXAgR6.mjs +181 -0
- package/dist/devframe.d.mts +5 -0
- package/dist/devframe.mjs +24 -0
- package/dist/index.d.mts +162 -0
- package/dist/index.mjs +4 -0
- package/dist/studio-DLNpk_rE.mjs +137 -0
- 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,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 };
|
package/dist/index.d.mts
ADDED
|
@@ -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.
|
|
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.
|
|
40
|
-
"@furifont/core": "0.1.
|
|
41
|
-
"@furifont/dictionary": "0.1.
|
|
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.
|
|
43
|
+
"devframe": "^1.2.5",
|
|
44
44
|
"valibot": "^1.5.0"
|
|
45
45
|
},
|
|
46
46
|
"scripts": {
|