@kecan0406/aninames 0.0.0-stage → 0.2.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.
- package/LICENSE +21 -0
- package/README.md +90 -2
- package/lib/client/chunks.d.ts +27 -0
- package/lib/client/chunks.js +124 -0
- package/lib/client/engine.d.ts +20 -0
- package/lib/client/engine.js +244 -0
- package/lib/client/format.d.ts +77 -0
- package/lib/client/format.js +220 -0
- package/lib/client/index.d.ts +3 -0
- package/lib/client/index.js +2 -0
- package/lib/client/names.d.ts +45 -0
- package/lib/client/names.js +239 -0
- package/lib/client/order.d.ts +1 -0
- package/lib/client/order.js +13 -0
- package/lib/client/source.d.ts +14 -0
- package/lib/client/source.js +180 -0
- package/lib/match.d.ts +9 -0
- package/lib/match.js +58 -0
- package/lib/model.d.ts +34 -0
- package/lib/model.js +5 -0
- package/lib/text.d.ts +24 -0
- package/lib/text.js +611 -0
- package/package.json +34 -4
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdir, readdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { USER_AGENT } from "../model.js";
|
|
5
|
+
import { manifestPaths, parsePath, readManifest, TABLES } from "./format.js";
|
|
6
|
+
export class Missing extends Error {
|
|
7
|
+
}
|
|
8
|
+
const revalidators = new WeakMap();
|
|
9
|
+
function checked(path) {
|
|
10
|
+
if (!parsePath(path))
|
|
11
|
+
throw new Error(`aninames: ${JSON.stringify(path)} is not a chunk file name`);
|
|
12
|
+
return path;
|
|
13
|
+
}
|
|
14
|
+
export function httpSource(base, options = {}) {
|
|
15
|
+
const root = base.endsWith('/') ? base : `${base}/`;
|
|
16
|
+
const request = async (path, signal, extra = {}) => {
|
|
17
|
+
const url = new URL(path, root);
|
|
18
|
+
const response = await (options.fetch ?? fetch)(url, {
|
|
19
|
+
headers: { 'user-agent': USER_AGENT, ...options.headers, ...extra },
|
|
20
|
+
...(signal ? { signal } : {}),
|
|
21
|
+
});
|
|
22
|
+
if (response.status === 404)
|
|
23
|
+
throw new Missing(`aninames: ${url} is not there (404)`);
|
|
24
|
+
if (!response.ok && response.status !== 304)
|
|
25
|
+
throw new Error(`aninames: ${url} answered ${response.status}`);
|
|
26
|
+
return response;
|
|
27
|
+
};
|
|
28
|
+
const source = {
|
|
29
|
+
manifest: async (signal) => (await request('manifest.json', signal)).text(),
|
|
30
|
+
file: async (path, signal) => new Uint8Array(await (await request(checked(path), signal)).arrayBuffer()),
|
|
31
|
+
};
|
|
32
|
+
revalidators.set(source, async ({ etag, modified }, signal) => {
|
|
33
|
+
const response = await request('manifest.json', signal, {
|
|
34
|
+
...(etag ? { 'if-none-match': etag } : {}),
|
|
35
|
+
...(modified ? { 'if-modified-since': modified } : {}),
|
|
36
|
+
});
|
|
37
|
+
if (response.status === 304)
|
|
38
|
+
return undefined;
|
|
39
|
+
return {
|
|
40
|
+
text: await response.text(),
|
|
41
|
+
etag: response.headers.get('etag'),
|
|
42
|
+
modified: response.headers.get('last-modified'),
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
return source;
|
|
46
|
+
}
|
|
47
|
+
const missing = (error) => error?.code === 'ENOENT';
|
|
48
|
+
export function fileSource(dir) {
|
|
49
|
+
return {
|
|
50
|
+
async manifest(signal) {
|
|
51
|
+
try {
|
|
52
|
+
return await readFile(join(dir, 'manifest.json'), { encoding: 'utf8', ...(signal ? { signal } : {}) });
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (missing(error))
|
|
56
|
+
throw new Error(`aninames: ${join(dir, 'manifest.json')} is missing; run mise run chunk`);
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
async file(path, signal) {
|
|
61
|
+
try {
|
|
62
|
+
return new Uint8Array(await readFile(join(dir, checked(path)), signal ? { signal } : {}));
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
if (missing(error))
|
|
66
|
+
throw new Missing(`aninames: ${join(dir, path)} is missing`);
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function cachedOf(value) {
|
|
73
|
+
if (typeof value !== 'object' || value === null)
|
|
74
|
+
return undefined;
|
|
75
|
+
const { text, etag, modified, checked } = value;
|
|
76
|
+
if (typeof text !== 'string' || typeof checked !== 'number')
|
|
77
|
+
return undefined;
|
|
78
|
+
return {
|
|
79
|
+
text,
|
|
80
|
+
checked,
|
|
81
|
+
etag: typeof etag === 'string' ? etag : null,
|
|
82
|
+
modified: typeof modified === 'string' ? modified : null,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
async function put(path, data) {
|
|
86
|
+
const part = `${path}.${randomUUID()}.part`;
|
|
87
|
+
try {
|
|
88
|
+
await mkdir(dirname(path), { recursive: true });
|
|
89
|
+
await writeFile(part, data);
|
|
90
|
+
await rename(part, path);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
await rm(part, { force: true });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async function prune(dir, text) {
|
|
97
|
+
const named = manifestPaths(readManifest(text));
|
|
98
|
+
for (const layer of await readdir(dir).catch(() => [])) {
|
|
99
|
+
for (const table of TABLES) {
|
|
100
|
+
const folder = join(dir, layer, table);
|
|
101
|
+
for (const file of await readdir(folder).catch(() => [])) {
|
|
102
|
+
const path = `${layer}/${table}/${file}`;
|
|
103
|
+
if (parsePath(path) && !named.has(path))
|
|
104
|
+
await rm(join(folder, file), { force: true });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
export function cachedSource(source, dir, options = {}) {
|
|
110
|
+
const maxAge = options.maxAge ?? 86_400_000;
|
|
111
|
+
const state = join(dir, 'manifest.cache.json');
|
|
112
|
+
let stale = false;
|
|
113
|
+
const read = async () => {
|
|
114
|
+
try {
|
|
115
|
+
return cachedOf(JSON.parse(await readFile(state, 'utf8')));
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
const fetchFresh = async (cached, signal) => {
|
|
122
|
+
const revalidate = revalidators.get(source);
|
|
123
|
+
if (revalidate)
|
|
124
|
+
return revalidate(cached ?? { etag: null, modified: null }, signal);
|
|
125
|
+
return { text: await source.manifest(signal), etag: null, modified: null };
|
|
126
|
+
};
|
|
127
|
+
return {
|
|
128
|
+
async manifest(signal) {
|
|
129
|
+
const cached = await read();
|
|
130
|
+
if (cached && !stale && Date.now() - cached.checked < maxAge)
|
|
131
|
+
return cached.text;
|
|
132
|
+
let fresh;
|
|
133
|
+
try {
|
|
134
|
+
fresh = await fetchFresh(cached, signal);
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
if (cached && !signal?.aborted)
|
|
138
|
+
return cached.text;
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
stale = false;
|
|
142
|
+
if (!fresh) {
|
|
143
|
+
if (!cached)
|
|
144
|
+
throw new Error('aninames: the server answered 304 to a manifest request without validators');
|
|
145
|
+
await put(state, JSON.stringify({ ...cached, checked: Date.now() }));
|
|
146
|
+
return cached.text;
|
|
147
|
+
}
|
|
148
|
+
readManifest(fresh.text);
|
|
149
|
+
await put(state, JSON.stringify({ ...fresh, checked: Date.now() }));
|
|
150
|
+
if (fresh.text !== cached?.text)
|
|
151
|
+
await prune(dir, fresh.text);
|
|
152
|
+
return fresh.text;
|
|
153
|
+
},
|
|
154
|
+
async file(path, signal) {
|
|
155
|
+
const target = join(dir, checked(path));
|
|
156
|
+
try {
|
|
157
|
+
return new Uint8Array(await readFile(target));
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
if (!missing(error))
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
163
|
+
let bytes;
|
|
164
|
+
try {
|
|
165
|
+
bytes = await source.file(path, signal);
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
if (error instanceof Missing)
|
|
169
|
+
stale = true;
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
172
|
+
const hash = createHash('sha256').update(bytes).digest('hex');
|
|
173
|
+
if (!hash.startsWith(parsePath(path)?.hash ?? '-')) {
|
|
174
|
+
throw new Error(`aninames: ${path} arrived with other contents (sha256 ${hash.slice(0, 12)})`);
|
|
175
|
+
}
|
|
176
|
+
await put(target, bytes);
|
|
177
|
+
return bytes;
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
}
|
package/lib/match.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { NameKind } from './model.ts';
|
|
2
|
+
export declare const TOP = "\uDBFF\uDFFF";
|
|
3
|
+
export declare function weightOf(row: {
|
|
4
|
+
kind: NameKind;
|
|
5
|
+
variant: number;
|
|
6
|
+
support: number;
|
|
7
|
+
}): number;
|
|
8
|
+
export declare function typing(q: string): [string, string][];
|
|
9
|
+
export declare function swapLayout(query: string): string | undefined;
|
package/lib/match.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { convertHangulToQwerty, convertQwertyToHangul } from 'es-hangul';
|
|
2
|
+
export const TOP = '\u{10FFFF}';
|
|
3
|
+
const NAME_WEIGHT = {
|
|
4
|
+
main: 1,
|
|
5
|
+
official: 1,
|
|
6
|
+
reading: 1,
|
|
7
|
+
synonym: 0.95,
|
|
8
|
+
short: 0.95,
|
|
9
|
+
redirect: 0.92,
|
|
10
|
+
section: 0.6,
|
|
11
|
+
generated: 0.85,
|
|
12
|
+
};
|
|
13
|
+
const WEAK = new Set(['synonym', 'short', 'redirect', 'section']);
|
|
14
|
+
export function weightOf(row) {
|
|
15
|
+
return NAME_WEIGHT[row.kind] * (row.variant ? 0.9 : 1) * (row.support <= 1 && WEAK.has(row.kind) ? 0.93 : 1);
|
|
16
|
+
}
|
|
17
|
+
const INITIAL_JAMO = 'ㄱㄲㄴㄷㄸㄹㅁㅂㅃㅅㅆㅇㅈㅉㅊㅋㅌㅍㅎ';
|
|
18
|
+
export function typing(q) {
|
|
19
|
+
const ranges = [[q, `${q}${TOP}`]];
|
|
20
|
+
const chars = [...q];
|
|
21
|
+
const last = chars.at(-1) ?? '';
|
|
22
|
+
const head = chars.slice(0, -1).join('');
|
|
23
|
+
const code = (last.codePointAt(0) ?? 0) - 0xac00;
|
|
24
|
+
if (code >= 0 && code < 11172 && code % 28 === 0) {
|
|
25
|
+
ranges.push([
|
|
26
|
+
`${head}${String.fromCodePoint(0xac00 + code + 1)}`,
|
|
27
|
+
`${head}${String.fromCodePoint(0xac00 + code + 27)}${TOP}`,
|
|
28
|
+
]);
|
|
29
|
+
}
|
|
30
|
+
const conjoined = (last.codePointAt(0) ?? 0) - 0x1100;
|
|
31
|
+
const initial = conjoined >= 0 && conjoined < 19 ? conjoined : INITIAL_JAMO.indexOf(last);
|
|
32
|
+
if (initial >= 0 && head) {
|
|
33
|
+
ranges.push([
|
|
34
|
+
`${head}${String.fromCodePoint(0xac00 + initial * 588)}`,
|
|
35
|
+
`${head}${String.fromCodePoint(0xac00 + initial * 588 + 587)}${TOP}`,
|
|
36
|
+
]);
|
|
37
|
+
}
|
|
38
|
+
const kana = /^([\p{Script=Hiragana}\p{Script=Katakana}ー]+)[a-z]+$/u.exec(q)?.[1];
|
|
39
|
+
if (kana)
|
|
40
|
+
ranges.push([kana, `${kana}${TOP}`]);
|
|
41
|
+
return ranges;
|
|
42
|
+
}
|
|
43
|
+
export function swapLayout(query) {
|
|
44
|
+
const text = query.trim();
|
|
45
|
+
try {
|
|
46
|
+
if (/[a-z]/i.test(text) && !/[\p{Script=Hangul}\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}]/u.test(text)) {
|
|
47
|
+
const hangul = text.replace(/[a-z]+/gi, (run) => convertQwertyToHangul(run));
|
|
48
|
+
return /[ㄱ-ㅎㅏ-ㅣ]/.test(hangul) || !/\p{Script=Hangul}/u.test(hangul) ? undefined : hangul;
|
|
49
|
+
}
|
|
50
|
+
if (/\p{Script=Hangul}/u.test(text) && !/[a-z\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}]/iu.test(text)) {
|
|
51
|
+
return text.replace(/[\p{Script=Hangul}]+/gu, (run) => convertHangulToQwerty(run)).toLowerCase();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
package/lib/model.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type Kind = 'work' | 'character';
|
|
2
|
+
export type Layer = 'core' | 'kowiki' | 'unlicensed';
|
|
3
|
+
export declare const LAYERS: readonly Layer[];
|
|
4
|
+
export declare const USER_AGENT = "aninames/0.1 (+https://github.com/kecan0406/aninames)";
|
|
5
|
+
export type NameKind = 'main' | 'official' | 'synonym' | 'short' | 'reading' | 'redirect' | 'section' | 'generated';
|
|
6
|
+
export type Namespace = 'wikidata' | 'mal' | 'anilist' | 'anidb' | 'kitsu' | 'ann' | 'animeplanet' | 'anisearch' | 'livechart' | 'simkl' | 'animecountdown' | 'tmdb' | 'vndb' | 'bangumi' | 'danbooru' | 'kowiki' | 'anissia';
|
|
7
|
+
export type Ref = `${Namespace}:${string}`;
|
|
8
|
+
export interface Name {
|
|
9
|
+
text: string;
|
|
10
|
+
lang?: string;
|
|
11
|
+
kind: NameKind;
|
|
12
|
+
}
|
|
13
|
+
export interface Entry {
|
|
14
|
+
id: Ref;
|
|
15
|
+
kind?: Kind;
|
|
16
|
+
attach?: boolean;
|
|
17
|
+
refs?: Ref[];
|
|
18
|
+
names: Name[];
|
|
19
|
+
works?: Ref[];
|
|
20
|
+
related?: Ref[];
|
|
21
|
+
link?: string[];
|
|
22
|
+
rank?: number;
|
|
23
|
+
adult?: boolean;
|
|
24
|
+
type?: string;
|
|
25
|
+
year?: number;
|
|
26
|
+
}
|
|
27
|
+
export interface Source {
|
|
28
|
+
name: string;
|
|
29
|
+
layer: Layer;
|
|
30
|
+
license: string;
|
|
31
|
+
fetch(raw: string): Promise<void>;
|
|
32
|
+
stage(raw: string): AsyncIterable<Entry> | Iterable<Entry>;
|
|
33
|
+
}
|
|
34
|
+
export declare function ref(ns: Namespace, id: string | number): Ref;
|
package/lib/model.js
ADDED
package/lib/text.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export declare function toHiragana(text: string): string;
|
|
2
|
+
export declare function isKana(text: string): boolean;
|
|
3
|
+
export declare function hasHangul(text: string): boolean;
|
|
4
|
+
export declare function hasKanji(text: string): boolean;
|
|
5
|
+
export declare function kanaToRomaji(kana: string): string;
|
|
6
|
+
export declare function hangulToLatin(text: string): string;
|
|
7
|
+
export declare function foldLatin(text: string): string;
|
|
8
|
+
export declare function soundKeys(text: string): string[];
|
|
9
|
+
export declare function flat(text: string): string;
|
|
10
|
+
export declare function latinKey(text: string): string;
|
|
11
|
+
export declare function partKey(token: string): string;
|
|
12
|
+
export declare function qualifiers(text: string): {
|
|
13
|
+
base: string;
|
|
14
|
+
quals: string[];
|
|
15
|
+
};
|
|
16
|
+
type HangulStyle = 'standard' | 'fan' | 'fan-long';
|
|
17
|
+
export declare function kanaToHangul(kana: string, style: HangulStyle): string;
|
|
18
|
+
export declare function initials(text: string): string;
|
|
19
|
+
export declare function editDistance(a: string, b: string, limit: number): number;
|
|
20
|
+
export declare function hangulForms(kana: string): {
|
|
21
|
+
text: string;
|
|
22
|
+
preferred: boolean;
|
|
23
|
+
}[];
|
|
24
|
+
export {};
|