Pop-up dictionary browser extension for language learning. Successor to Yomichan. (PERSONAL FORK)
1/*
2 * Copyright (C) 2023-2025 Yomitan Authors
3 * Copyright (C) 2021-2022 Yomichan Authors
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <https://www.gnu.org/licenses/>.
17 */
18
19import {isObjectNotArray} from '../core/object-utilities.js';
20
21/** @type {RegExp} @readonly */
22const markerPattern = /\{([\p{Letter}\p{Number}_-]+)\}/gu;
23
24/**
25 * Gets the root deck name of a full deck name. If the deck is a root deck,
26 * the same name is returned. Nested decks are separated using '::'.
27 * @param {string} deckName A string of the deck name.
28 * @returns {string} A string corresponding to the name of the root deck.
29 */
30export function getRootDeckName(deckName) {
31 const index = deckName.indexOf('::');
32 return index >= 0 ? deckName.substring(0, index) : deckName;
33}
34
35/**
36 * Checks whether or not any marker is contained in a string.
37 * @param {string} string A string to check.
38 * @returns {boolean} `true` if the text contains an Anki field marker, `false` otherwise.
39 */
40export function stringContainsAnyFieldMarker(string) {
41 const result = markerPattern.test(string);
42 markerPattern.lastIndex = 0;
43 return result;
44}
45
46/**
47 * Gets a list of all markers that are contained in a string.
48 * @param {string} string A string to check.
49 * @returns {string[]} An array of marker strings.
50 */
51export function getFieldMarkers(string) {
52 const pattern = markerPattern;
53 const markers = [];
54 while (true) {
55 const match = pattern.exec(string);
56 if (match === null) { break; }
57 markers.push(match[1]);
58 }
59 return markers;
60}
61
62/**
63 * Returns a regular expression which can be used to find markers in a string.
64 * @param {boolean} global Whether or not the regular expression should have the global flag.
65 * @returns {RegExp} A new `RegExp` instance.
66 */
67export function cloneFieldMarkerPattern(global) {
68 return new RegExp(markerPattern.source, global ? 'gu' : 'u');
69}
70
71/**
72 * Checks whether or not a note object is valid.
73 * @param {import('anki').Note} note A note object to check.
74 * @returns {boolean} `true` if the note is valid, `false` otherwise.
75 */
76export function isNoteDataValid(note) {
77 if (!isObjectNotArray(note)) { return false; }
78 const {fields, deckName, modelName} = note;
79 return (
80 typeof deckName === 'string' && deckName.length > 0 &&
81 typeof modelName === 'string' && modelName.length > 0 &&
82 Object.entries(fields).length > 0
83 );
84}
85
86export const INVALID_NOTE_ID = -1;
87
88
89/**
90 * @param {string} prefix
91 * @param {string} extension
92 * @param {number} timestamp
93 * @returns {string}
94 */
95export function generateAnkiNoteMediaFileName(prefix, extension, timestamp) {
96 let fileName = prefix;
97
98 fileName += `_${ankNoteDateToString(new Date(timestamp))}`;
99 fileName += extension;
100
101 fileName = replaceInvalidFileNameCharacters(fileName);
102
103 return fileName;
104}
105
106/**
107 * @param {string} fileName
108 * @returns {string}
109 */
110function replaceInvalidFileNameCharacters(fileName) {
111 // eslint-disable-next-line no-control-regex
112 return fileName.replace(/[<>:"/\\|?*\u0000-\u001F]/g, '-');
113}
114
115/**
116 * @param {Date} date
117 * @returns {string}
118 */
119function ankNoteDateToString(date) {
120 const year = date.getUTCFullYear();
121 const month = date.getUTCMonth().toString().padStart(2, '0');
122 const day = date.getUTCDate().toString().padStart(2, '0');
123 const hours = date.getUTCHours().toString().padStart(2, '0');
124 const minutes = date.getUTCMinutes().toString().padStart(2, '0');
125 const seconds = date.getUTCSeconds().toString().padStart(2, '0');
126 const milliseconds = date.getUTCMilliseconds().toString().padStart(3, '0');
127 return `${year}-${month}-${day}-${hours}-${minutes}-${seconds}-${milliseconds}`;
128}