Pop-up dictionary browser extension for language learning. Successor to Yomichan. (PERSONAL FORK)
1/*
2 * Copyright (C) 2024-2025 Yomitan Authors
3 *
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <https://www.gnu.org/licenses/>.
16 */
17
18import {BlobWriter, TextReader, TextWriter, Uint8ArrayReader, ZipReader, ZipWriter} from '@zip.js/zip.js';
19import {readFileSync, readdirSync} from 'fs';
20import {join} from 'path';
21import {parseJson} from './json.js';
22
23/**
24 * Creates a zip archive from the given dictionary directory.
25 * @param {string} dictionaryDirectory
26 * @param {string} [dictionaryName]
27 * @returns {Promise<ArrayBuffer>}
28 */
29export async function createDictionaryArchiveData(dictionaryDirectory, dictionaryName) {
30 const fileNames = readdirSync(dictionaryDirectory);
31 const zipFileWriter = new BlobWriter();
32 // Level 0 compression used since decompression in the node environment is not supported.
33 // See dev/lib/zip.js for more details.
34 const zipWriter = new ZipWriter(zipFileWriter, {
35 level: 0,
36 });
37 for (const fileName of fileNames) {
38 if (/\.json$/.test(fileName)) {
39 const content = readFileSync(join(dictionaryDirectory, fileName), {encoding: 'utf8'});
40 /** @type {import('dictionary-data').Index} */
41 const json = parseJson(content);
42 if (fileName === 'index.json' && typeof dictionaryName === 'string') {
43 json.title = dictionaryName;
44 }
45 await zipWriter.add(fileName, new TextReader(JSON.stringify(json, null, 0)));
46 } else {
47 const content = readFileSync(join(dictionaryDirectory, fileName), {encoding: null});
48 await zipWriter.add(fileName, new Blob([content]).stream());
49 }
50 }
51 const blob = await zipWriter.close();
52 return await blob.arrayBuffer();
53}
54
55/**
56 * @param {import('@zip.js/zip.js').Entry} entry
57 * @returns {Promise<string>}
58 */
59export async function readArchiveEntryDataString(entry) {
60 if (typeof entry.getData === 'undefined') { throw new Error('Cannot get index data'); }
61 return await entry.getData(new TextWriter());
62}
63
64/**
65 * @template [T=unknown]
66 * @param {import('@zip.js/zip.js').Entry} entry
67 * @returns {Promise<T>}
68 */
69export async function readArchiveEntryDataJson(entry) {
70 const indexContent = await readArchiveEntryDataString(entry);
71 return parseJson(indexContent);
72}
73
74/**
75 * @param {ArrayBuffer} data
76 * @returns {Promise<import('@zip.js/zip.js').Entry[]>}
77 */
78export async function getDictionaryArchiveEntries(data) {
79 const zipFileReader = new Uint8ArrayReader(new Uint8Array(data));
80 const zipReader = new ZipReader(zipFileReader);
81 return await zipReader.getEntries();
82}
83
84/**
85 * @template T
86 * @param {import('@zip.js/zip.js').Entry[]} entries
87 * @param {string} fileName
88 * @returns {Promise<T>}
89 */
90export async function getDictionaryArchiveJson(entries, fileName) {
91 const entry = entries.find((item) => item.filename === fileName);
92 if (typeof entry === 'undefined') { throw new Error(`File not found: ${fileName}`); }
93 return await readArchiveEntryDataJson(entry);
94}
95
96/**
97 * @returns {string}
98 */
99export function getIndexFileName() {
100 return 'index.json';
101}
102
103/**
104 * @param {ArrayBuffer} data
105 * @returns {Promise<import('dictionary-data').Index>}
106 */
107export async function getDictionaryArchiveIndex(data) {
108 const entries = await getDictionaryArchiveEntries(data);
109 return await getDictionaryArchiveJson(entries, getIndexFileName());
110}