Pop-up dictionary browser extension for language learning. Successor to Yomichan. (PERSONAL FORK)
1/*
2 * Copyright (C) 2023-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 {AnkiNoteBuilder} from '../../ext/js/data/anki-note-builder.js';
19import {createAnkiNoteData} from '../../ext/js/data/anki-note-data-creator.js';
20import {getStandardFieldMarkers} from '../../ext/js/data/anki-template-util.js';
21import {AnkiTemplateRenderer} from '../../ext/js/templates/anki-template-renderer.js';
22
23/**
24 * @param {import('dictionary').DictionaryEntryType} type
25 * @returns {import('settings').AnkiFields}
26 */
27function createTestFields(type) {
28 /** @type {import('settings').AnkiFields} */
29 const fields = {};
30 for (const marker of getStandardFieldMarkers(type)) {
31 fields[marker] = {value: `{${marker}}`, overwriteMode: 'coalesce'};
32 }
33 return fields;
34}
35
36/**
37 * @param {import('dictionary').DictionaryEntry} dictionaryEntry
38 * @param {import('settings').ResultOutputMode} mode
39 * @param {string} styles
40 * @returns {import('anki-templates').NoteData}
41 * @throws {Error}
42 */
43export function createTestAnkiNoteData(dictionaryEntry, mode, styles = '') {
44 const marker = '{marker}';
45 /** @type {Map<string, string>} */
46 const dictionaryStylesMap = new Map();
47 if (styles !== '') {
48 dictionaryStylesMap.set('Test Dictionary 2', styles);
49 }
50 /** @type {import('anki-templates-internal').CreateDetails} */
51 const data = {
52 dictionaryEntry,
53 resultOutputMode: mode,
54 cardFormat: {
55 type: 'term',
56 name: 'test',
57 deck: 'deck',
58 model: 'model',
59 fields: {},
60 icon: 'big-circle',
61 },
62 glossaryLayoutMode: 'default',
63 compactTags: false,
64 context: {
65 url: 'url:',
66 sentence: {text: '', offset: 0},
67 documentTitle: 'title',
68 query: 'query',
69 fullQuery: 'fullQuery',
70 },
71 media: {},
72 dictionaryStylesMap,
73 };
74 return createAnkiNoteData(marker, data);
75}
76
77/**
78 * @param {import('dictionary').DictionaryEntry[]} dictionaryEntries
79 * @param {import('settings').ResultOutputMode} mode
80 * @param {string} template
81 * @param {?import('vitest').ExpectStatic} expect
82 * @param {string} styles
83 * @returns {Promise<import('anki').NoteFields[]>}
84 */
85export async function getTemplateRenderResults(dictionaryEntries, mode, template, expect, styles = '') {
86 const ankiTemplateRenderer = new AnkiTemplateRenderer(document, window);
87 await ankiTemplateRenderer.prepare();
88 const clozePrefix = 'cloze-prefix';
89 const clozeSuffix = 'cloze-suffix';
90 const results = [];
91 for (const dictionaryEntry of dictionaryEntries) {
92 let source = '';
93 switch (dictionaryEntry.type) {
94 case 'kanji':
95 source = dictionaryEntry.character;
96 break;
97 case 'term':
98 if (dictionaryEntry.headwords.length > 0 && dictionaryEntry.headwords[0].sources.length > 0) {
99 source = dictionaryEntry.headwords[0].sources[0].originalText;
100 }
101 break;
102 }
103 const api = new MinimalApi();
104 const ankiNoteBuilder = new AnkiNoteBuilder(api, ankiTemplateRenderer.templateRenderer);
105 const context = {
106 url: 'url:',
107 sentence: {
108 text: `${clozePrefix}${source}${clozeSuffix}`,
109 offset: clozePrefix.length,
110 },
111 documentTitle: 'title',
112 query: 'query',
113 fullQuery: 'fullQuery',
114 };
115 /** @type {Map<string, string>} */
116 const dictionaryStylesMap = new Map();
117 if (styles) {
118 dictionaryStylesMap.set('Test Dictionary 2', styles);
119 }
120 /** @type {import('anki-note-builder').CreateNoteDetails} */
121 const details = {
122 dictionaryEntry,
123 cardFormat: {
124 type: dictionaryEntry.type,
125 name: 'test',
126 deck: 'deckName',
127 model: 'modelName',
128 fields: createTestFields(dictionaryEntry.type),
129 icon: 'big-circle',
130 },
131 context,
132 template,
133 tags: ['yomitan'],
134 duplicateScope: 'collection',
135 duplicateScopeCheckAllModels: false,
136 resultOutputMode: mode,
137 glossaryLayoutMode: 'default',
138 compactTags: false,
139 requirements: [],
140 mediaOptions: null,
141 dictionaryStylesMap,
142 };
143 const {note: {fields: noteFields}, errors} = await ankiNoteBuilder.createNote(details);
144 for (const error of errors) {
145 console.error(error);
146 }
147 if (expect !== null) {
148 expect(errors.length).toStrictEqual(0);
149 }
150 results.push(noteFields);
151 }
152
153 return results;
154}
155
156class MinimalApi {
157 /**
158 * @type {import('anki-note-builder.js').MinimalApi['injectAnkiNoteMedia']}
159 */
160 async injectAnkiNoteMedia() {
161 throw new Error('Not supported');
162 }
163
164 /**
165 * @type {import('anki-note-builder.js').MinimalApi['parseText']}
166 */
167 async parseText() {
168 throw new Error('Not supported');
169 }
170}