@furifont/core 0.3.0 → 0.4.1

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