@furifont/core 0.2.0 → 0.4.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.
Files changed (3) hide show
  1. package/dist/index.d.mts +156 -128
  2. package/dist/index.mjs +545 -321
  3. package/package.json +1 -1
package/dist/index.d.mts CHANGED
@@ -10,14 +10,8 @@ export declare const DEFAULTS: {
10
10
  readonly plan: {
11
11
  readonly family: "Furifont Sans";
12
12
  readonly rubyScript: "hiragana";
13
- /** Newspaper frequency buckets, nf01 to nf48: the window of words that get ruby. All of them: it adds about 400 glyphs. */
14
- readonly nfMin: 1;
15
- readonly nfMax: 48;
16
- /** JMdict priority tags that select a word whatever its bucket: the first tier of each list, which JMdict marks as common. */
17
- readonly selectTags: readonly ["ichi1", "news1", "spec1", "gai1"];
18
- /** Uses in Tatoeba's example sentences that select a word whatever its bucket: about 1,000 more words, and 50 glyphs. */
19
- readonly minTatoeba: 3;
20
- readonly places: true;
13
+ /** No word is left bare for being common; every word of every source gets ruby. */
14
+ readonly skip: 0;
21
15
  readonly isolated: true;
22
16
  readonly isolatedMinShare: 0.8;
23
17
  /** Sizes in em, relative to the base font. */
@@ -71,7 +65,7 @@ export declare const PREBUILT: {
71
65
  readonly style: "VF";
72
66
  readonly weights: readonly [250, 900];
73
67
  };
74
- /** Options set on every prebuilt font over the defaults above, such as `{ nfMax: 24 }`. */
68
+ /** Options set on every prebuilt font over the defaults above, such as `{ take: 20000 }`. */
75
69
  readonly options: {};
76
70
  };
77
71
  /**
@@ -85,76 +79,31 @@ export declare const VERSION_PROBE: {
85
79
  readonly selector: 65024;
86
80
  };
87
81
  //#endregion
88
- //#region src/dictionary.d.ts
89
- /** One (written form, reading) pair, as compiled by `@furifont/dictionary`. */
90
- interface DictionaryEntry {
91
- form: string;
92
- /** JmdictFurigana alignment: `0:に;1:ほん`, or `0-1:きょう` for a reading spanning characters. */
93
- furigana: string;
94
- /** The form's newspaper frequency bucket (nf01..nf48), 0 when unranked. */
95
- nf: number;
96
- /** The reading's JMdict priority tags, space separated; empty for uncommon readings. */
97
- priority: string;
98
- /** Position of the reading among the form's readings, in JMdict order. */
99
- order: number;
100
- /** `来る`-type verb, whose kanji reads differently as it conjugates. */
101
- conjugatesReading?: boolean;
102
- /** The JLPT level whose word list has this pair: 5 for N5 down to 1 for N1. */
103
- jlpt?: number;
104
- /** How many times Tatoeba's example sentences use the pair, spelled this way. */
105
- tatoeba?: number;
106
- }
107
- interface Dictionary {
108
- entries: DictionaryEntry[];
109
- /** Kanji to its on'yomi, in hiragana. */
110
- onyomi: Record<string, string[]>;
111
- /** Source datasets and the version each was compiled from. */
112
- sources: Record<string, string>;
113
- /** Written forms never to annotate, such as 月, whose reading alone is a guess; users' removals add to them. */
114
- bare?: string[];
115
- /** Curated default readings in inline notation, for words the data ranks wrong; pins override them. */
116
- defaults?: string[];
117
- /** Place names, each with the one reading to show; common words take precedence over them. */
118
- places?: {
119
- form: string;
120
- furigana: string;
121
- }[];
122
- }
123
- /** A ruby annotation over characters `[start, end)` of a run. */
124
- interface RubySpan {
125
- start: number;
126
- end: number;
127
- ruby: string;
128
- }
129
- /** A maximal sequence of Han characters with its reading, the unit a shaper sees. */
130
- interface HanRun {
131
- text: string;
132
- spans: RubySpan[];
133
- }
134
- export declare function isHan(char: string): boolean;
135
- export declare function parseFurigana(furigana: string): RubySpan[];
136
- /**
137
- * Splits a form into its Han runs. Browsers itemize text by script before
138
- * shaping, so a font only ever sees one run at a time: okurigana and other
139
- * kana are never visible as context. Runs with an unannotated kanji or an
140
- * annotation leaking outside the run are dropped.
141
- */
142
- export declare function hanRuns(form: string, spans: readonly RubySpan[]): HanRun[];
143
- /**
144
- * Parses the inline notation for custom words: `日[に]本[ほん]` for one reading
145
- * per kanji, `今日[きょう]` for a reading over several kanji, kana as written.
146
- */
147
- export declare function parseNotation(notation: string): {
148
- form: string;
149
- spans: RubySpan[];
150
- };
151
- export declare function formatNotation(text: string, spans: readonly RubySpan[]): string;
152
- //#endregion
153
82
  //#region src/schema.d.ts
154
83
  export declare const RUBY_SCRIPTS: readonly ["hiragana", "katakanaOn", "hepburn"];
155
- export declare const PRIORITY_TAGS: readonly ["ichi1", "ichi2", "news1", "news2", "spec1", "spec2", "gai1", "gai2"];
84
+ /** The dictionary's manually maintained sources. */
85
+ export declare const MANUAL_SOURCES: readonly ["mahjong-yaku", "jr-stations", "yojijukugo", "japan-post"];
86
+ type ManualSource = typeof MANUAL_SOURCES[number];
87
+ /**
88
+ * The sources a plan takes words from: any JMdict word; JMdict's priority
89
+ * lists (both tiers of each) and its newspaper ranking; words Tatoeba's
90
+ * sentences use at least three times; JMnedict's place names; the manual lists.
91
+ */
92
+ export declare const WORD_SOURCES: readonly ["jmdict", "ichi", "news", "spec", "gai", "nf", "tatoeba", "place", "mahjong-yaku", "jr-stations", "yojijukugo", "japan-post"];
93
+ type WordSource = typeof WORD_SOURCES[number];
156
94
  /** Easiest first. */
157
95
  export declare const JLPT_LEVELS: readonly ["N5", "N4", "N3", "N2", "N1"];
96
+ /**
97
+ * Every source a reading may have, a bit each in this order: the word
98
+ * sources, then the easiest JLPT level listing it, `inflecting` (a kanji read
99
+ * before okurigana in a JMdict word no list tags, such as 奉る) and `default`,
100
+ * the dictionary's curated default.
101
+ */
102
+ export declare const SOURCES: readonly ["jmdict", "ichi", "news", "spec", "gai", "nf", "tatoeba", "place", "mahjong-yaku", "jr-stations", "yojijukugo", "japan-post", "N5", "N4", "N3", "N2", "N1", "inflecting", "default"];
103
+ type Source = typeof SOURCES[number];
104
+ export declare const sourceBit: (source: Source) => number;
105
+ /** The sources a mask of `SOURCES` bits names. */
106
+ export declare const sourcesIn: (mask: number) => Source[];
158
107
  type RubyScript = typeof RUBY_SCRIPTS[number];
159
108
  /** Sizes in em, relative to the base font. */
160
109
  declare const RubyMetricsSchema: v.ObjectSchema<{
@@ -168,38 +117,17 @@ declare const RubyMetricsSchema: v.ObjectSchema<{
168
117
  readonly maxRubyWidth: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.MinValueAction<number, 1, undefined>, v.MaxValueAction<number, 3, undefined>]>, 1.5>;
169
118
  }, undefined>;
170
119
  type RubyMetrics = v.InferOutput<typeof RubyMetricsSchema>;
171
- /** `schema`, checking that its frequency window `nfMin..nfMax` holds a bucket; for schemas that extend the options too. */
172
- export declare function withNfWindow<T extends v.GenericSchema<unknown, {
173
- nfMin: number;
174
- nfMax: number;
175
- }>>(schema: T): v.SchemaWithPipe<readonly [T, v.CheckAction<v.InferOutput<T>, string>]>;
176
- export declare const PlanOptionsSchema: v.SchemaWithPipe<readonly [v.ObjectSchema<{
120
+ export declare const PlanOptionsSchema: v.ObjectSchema<{
177
121
  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">;
178
122
  readonly rubyScript: v.OptionalSchema<v.PicklistSchema<readonly ["hiragana", "katakanaOn", "hepburn"], undefined>, "hiragana">;
179
- /**
180
- * Words whose newspaper frequency bucket is below this stay bare: the most
181
- * common ones, which a reader knows. Tags don't bring them back.
182
- */
183
- 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>;
184
- /** Words whose newspaper frequency bucket is at most this are annotated. */
185
- 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>;
186
- /**
187
- * JMdict priority tags that select a word whatever its frequency bucket:
188
- * `spec1` catches words like 日本 that the newspaper ranking misses.
189
- */
190
- readonly selectTags: v.OptionalSchema<v.ArraySchema<v.PicklistSchema<readonly ["ichi1", "ichi2", "news1", "news2", "spec1", "spec2", "gai1", "gai2"], undefined>, undefined>, () => ("ichi1" | "news1" | "spec1" | "gai1")[]>;
191
- /**
192
- * Words of two or more kanji that Tatoeba's example sentences use at least
193
- * this many times are annotated whatever their bucket: everyday words that
194
- * no list tags, such as 留守中, which would otherwise be bare or read as a
195
- * place name (国中 くになか, not くにじゅう). The sentences also decide how a
196
- * lone kanji reads, where they use it alone often enough. 0 turns them off.
197
- */
198
- readonly minTatoeba: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>, 3>;
123
+ /** The sources whose words get ruby. */
124
+ readonly sources: v.OptionalSchema<v.ArraySchema<v.PicklistSchema<readonly ["jmdict", "ichi", "news", "spec", "gai", "nf", "tatoeba", "place", "mahjong-yaku", "jr-stations", "yojijukugo", "japan-post"], undefined>, undefined>, () => ("mahjong-yaku" | "jr-stations" | "yojijukugo" | "japan-post" | "jmdict" | "ichi" | "news" | "spec" | "gai" | "nf" | "tatoeba" | "place")[]>;
125
+ /** The most common words left bare, by score: a reader knows them. Lone kanji count too. */
126
+ readonly skip: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>, 0>;
127
+ /** How many words get ruby after those skipped, by score; all of them when left out. */
128
+ readonly take: v.OptionalSchema<v.SchemaWithPipe<readonly [v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>, v.MinValueAction<number, 1, undefined>]>, undefined>;
199
129
  /** Words on the JLPT lists of this level and every easier one stay bare. */
200
130
  readonly skipJlpt: v.OptionalSchema<v.PicklistSchema<readonly ["N5", "N4", "N3", "N2", "N1"], undefined>, undefined>;
201
- /** Annotate place names from the dictionary where no common word claims the run. */
202
- readonly places: v.OptionalSchema<v.BooleanSchema<undefined>, true>;
203
131
  /** Annotate a kanji alone in its run, or one no word claims after another kanji (a suffix); when off, only selectors give them ruby. */
204
132
  readonly isolated: v.OptionalSchema<v.BooleanSchema<undefined>, true>;
205
133
  /** A kanji alone in its run is annotated only when one reading has this share of the evidence. */
@@ -222,29 +150,7 @@ export declare const PlanOptionsSchema: v.SchemaWithPipe<readonly [v.ObjectSchem
222
150
  /** Widest a ruby may get, overhanging its base symmetrically. */
223
151
  readonly maxRubyWidth: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.MinValueAction<number, 1, undefined>, v.MaxValueAction<number, 3, undefined>]>, 1.5>;
224
152
  }, undefined>, {}>;
225
- }, undefined>, v.CheckAction<{
226
- family: string;
227
- rubyScript: "hiragana" | "hepburn" | "katakanaOn";
228
- nfMin: number;
229
- nfMax: number;
230
- selectTags: ("ichi1" | "news1" | "spec1" | "gai1" | "ichi2" | "news2" | "spec2" | "gai2")[];
231
- minTatoeba: number;
232
- skipJlpt?: "N5" | "N4" | "N3" | "N2" | "N1" | undefined;
233
- places: boolean;
234
- isolated: boolean;
235
- isolatedMinShare: number;
236
- words: {
237
- add: string[];
238
- remove: string[];
239
- pin: string[];
240
- };
241
- metrics: {
242
- rubyScale: number;
243
- rubyGap: number;
244
- compressionFloor: number;
245
- maxRubyWidth: number;
246
- };
247
- }, string>]>;
153
+ }, undefined>;
248
154
  type PlanOptions = v.InferOutput<typeof PlanOptionsSchema>;
249
155
  type PlanOptionsInput = v.InferInput<typeof PlanOptionsSchema>;
250
156
  export declare function resolveOptions(input?: PlanOptionsInput): PlanOptions;
@@ -298,6 +204,128 @@ export declare const PlanSchema: v.ObjectSchema<{
298
204
  }, undefined>;
299
205
  type Plan = v.InferOutput<typeof PlanSchema>;
300
206
  type PlanGlyph = Plan['glyphs'][number];
207
+ /**
208
+ * The bounds of a reading's score, 0 to 100: above `tagged`, a word JMdict's
209
+ * lists tag, by its weight; above `used`, one Tatoeba's sentences use (20 and
210
+ * its uses, up to 50); above `listed`, one a manual list (15) or JMnedict (10)
211
+ * names; any other JMdict word scores `listed`.
212
+ */
213
+ export declare const SCORE: {
214
+ readonly tagged: 50;
215
+ readonly used: 20;
216
+ readonly listed: 5;
217
+ };
218
+ /**
219
+ * The score of a tagged reading of corpus weight `weight`, at most 1: 1 / its
220
+ * newspaper bucket, or a lone kanji's share of its uses. It is 100 for 1, 92
221
+ * for 1/2, 54 for 1/48; a weight too small to score above `tagged` scores 0.
222
+ */
223
+ export declare function scoreOf(weight: number): number;
224
+ /** The corpus weight of a reading's score, which `scoreOf` gives; 0 for one no list tags. */
225
+ export declare const weightOf: (score: number) => number;
226
+ /** A reading's score, higher for a more common one, and its sources as a mask of `SOURCES` bits. */
227
+ type Reading = [score: number, sources: number];
228
+ type Table = Record<string, Record<string, Reading>>;
229
+ /**
230
+ * The compiled dictionary, the contract between `@furifont/dictionary` and
231
+ * the planner: readings by the Han run a font sees, each with its score and
232
+ * sources. A run's readings come ranked, the default first. Run readings are
233
+ * JmdictFurigana alignments within the run.
234
+ */
235
+ export declare const DictionarySchema: v.ObjectSchema<{
236
+ /** Source datasets and the version each was compiled from. */
237
+ readonly versions: v.RecordSchema<v.StringSchema<undefined>, v.StringSchema<undefined>, undefined>;
238
+ /** The bit of each source in the readings' masks. */
239
+ readonly sources: v.CustomSchema<Record<string, number>, v.ErrorMessage<v.CustomIssue> | undefined>;
240
+ /** Kanji to its on'yomi, in hiragana. */
241
+ readonly onyomi: v.RecordSchema<v.StringSchema<undefined>, v.ArraySchema<v.StringSchema<undefined>, undefined>, undefined>;
242
+ /** Runs of two or more kanji, then their alignments: `{ 日本: { '0-1:にほん': [75, 61] } }`. */
243
+ readonly runs: v.CustomSchema<Table, v.ErrorMessage<v.CustomIssue> | undefined>;
244
+ /** Kanji alone in a run, then their readings: `{ 来: { く: [98, 2101] } }`. */
245
+ readonly kanji: v.CustomSchema<Table, v.ErrorMessage<v.CustomIssue> | undefined>;
246
+ /** Written forms never to annotate, such as 月, whose reading alone is a guess; users' removals add to them. */
247
+ readonly bare: v.ArraySchema<v.StringSchema<undefined>, undefined>;
248
+ }, undefined>;
249
+ type Dictionary = v.InferOutput<typeof DictionarySchema>;
250
+ /** The file `DictionarySchema` reads: an entry a line. */
251
+ export declare function formatDictionary(dictionary: Dictionary): string;
252
+ //#endregion
253
+ //#region src/dictionary.d.ts
254
+ /** One (written form, reading) pair of JMdict, aligned by JmdictFurigana. */
255
+ interface DictionaryEntry {
256
+ form: string;
257
+ /** JmdictFurigana alignment: `0:に;1:ほん`, or `0-1:きょう` for a reading spanning characters. */
258
+ furigana: string;
259
+ /** The form's newspaper frequency bucket (nf01..nf48), 0 when unranked. */
260
+ nf: number;
261
+ /** The reading's JMdict priority tags, space separated; empty for uncommon readings. */
262
+ priority: string;
263
+ /** Position of the reading among the form's readings, in JMdict order. */
264
+ order: number;
265
+ /** `来る`-type verb, whose kanji reads differently as it conjugates. */
266
+ conjugatesReading?: boolean;
267
+ /** The JLPT level whose word list has this pair: 5 for N5 down to 1 for N1. */
268
+ jlpt?: number;
269
+ /** How many times Tatoeba's example sentences use the pair, spelled this way. */
270
+ tatoeba?: number;
271
+ }
272
+ /** Everything `fuseDictionary` merges into a `Dictionary`. */
273
+ interface DictionarySources {
274
+ versions: Record<string, string>;
275
+ onyomi: Record<string, string[]>;
276
+ entries: DictionaryEntry[];
277
+ /** Place names, each with the one reading to show; common words take precedence over them. */
278
+ places: {
279
+ form: string;
280
+ furigana: string;
281
+ }[];
282
+ /** Word lists kept by hand, by source name, in inline notation; like place names, they fill runs no common word claims. */
283
+ manual: Partial<Record<ManualSource, string[]>>;
284
+ /** Curated default readings in inline notation, for words the data ranks wrong; pins override them. */
285
+ defaults: string[];
286
+ bare: string[];
287
+ }
288
+ /** A ruby annotation over characters `[start, end)` of a run. */
289
+ interface RubySpan {
290
+ start: number;
291
+ end: number;
292
+ ruby: string;
293
+ }
294
+ /** A maximal sequence of Han characters with its reading, the unit a shaper sees. */
295
+ interface HanRun {
296
+ text: string;
297
+ spans: RubySpan[];
298
+ }
299
+ export declare function isHan(char: string): boolean;
300
+ /** Spans of a JmdictFurigana alignment; a loop, not splits, as the planner parses the 400,000 readings of the dictionary as it starts. */
301
+ export declare function parseFurigana(furigana: string): RubySpan[];
302
+ /**
303
+ * Splits a form into its Han runs. Browsers itemize text by script before
304
+ * shaping, so a font only ever sees one run at a time: okurigana and other
305
+ * kana are never visible as context. Runs with an unannotated kanji or an
306
+ * annotation leaking outside the run are dropped.
307
+ */
308
+ export declare function hanRuns(form: string, spans: readonly RubySpan[]): HanRun[];
309
+ /**
310
+ * Parses the inline notation for custom words: `日[に]本[ほん]` for one reading
311
+ * per kanji, `今日[きょう]` for a reading over several kanji, kana as written.
312
+ */
313
+ export declare function parseNotation(notation: string): {
314
+ form: string;
315
+ spans: RubySpan[];
316
+ };
317
+ export declare function formatNotation(text: string, spans: readonly RubySpan[]): string;
318
+ //#endregion
319
+ //#region src/fuse.d.ts
320
+ /** Spans as a JmdictFurigana alignment: `0:に;1:ほん`, `0-1:きょう`. */
321
+ export declare function formatFurigana(spans: readonly RubySpan[]): string;
322
+ /**
323
+ * Merges every source into the readings of the Han runs a font sees, each
324
+ * reading with its score and sources. What no plan can use is left out: a
325
+ * word's runs with an unannotated kanji, lone kanji of the lists or of words
326
+ * no list tags but before okurigana, and the on'yomi of kanji no reading uses.
327
+ */
328
+ export declare function fuseDictionary(sources: DictionarySources): Dictionary;
301
329
  //#endregion
302
330
  //#region src/glyphs.d.ts
303
331
  export declare function glyphName(codepoint: number): string;
@@ -356,10 +384,10 @@ interface PlanResult {
356
384
  report: PlanReport;
357
385
  }
358
386
  /**
359
- * Indexes the dictionary once into Han runs, then plans fonts from it. Every
387
+ * Indexes the compiled dictionary once, then plans fonts from it. Every
360
388
  * decision about readings happens here: which words get ruby, which reading
361
389
  * is the default, which longer words must be protected from a shorter match.
362
390
  */
363
391
  export declare function createFontPlanner(dictionary: Dictionary): (options: PlanOptions, coverage: ReadonlySet<number>, previewText?: string) => PlanResult;
364
392
  //#endregion
365
- export type { Dictionary, DictionaryEntry, HanRun, Plan, PlanGlyph, PlanOptions, PlanOptionsInput, PlanReport, PlanResult, RubyMetrics, RubyScript, RubySpan };
393
+ export type { Dictionary, DictionaryEntry, DictionarySources, HanRun, ManualSource, Plan, PlanGlyph, PlanOptions, PlanOptionsInput, PlanReport, PlanResult, Reading, RubyMetrics, RubyScript, RubySpan, Source, WordSource };