mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-28 06:22:45 -08:00
chore: archive refactor milestones and remove structural quality-gates task
- Remove structural quality gates task and references from task-27 roadmap. - Remove structural-gates-adjacent work from scripts/positioning cleanup context, including check-main-lines adjustments. - Archive completed backlog tasks 11 and 27.7 by moving them to completed directory. - Finish task-27.5 module split by moving/anonymizing anki-integration and renderer positioning files into their dedicated directories and updating paths.
This commit is contained in:
102
src/anki-integration/duplicate.ts
Normal file
102
src/anki-integration/duplicate.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
export interface NoteField {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface NoteInfo {
|
||||
noteId: number;
|
||||
fields: Record<string, NoteField>;
|
||||
}
|
||||
|
||||
export interface DuplicateDetectionDeps {
|
||||
findNotes: (
|
||||
query: string,
|
||||
options?: { maxRetries?: number },
|
||||
) => Promise<unknown>;
|
||||
notesInfo: (noteIds: number[]) => Promise<unknown>;
|
||||
getDeck: () => string | null | undefined;
|
||||
resolveFieldName: (noteInfo: NoteInfo, preferredName: string) => string | null;
|
||||
logWarn: (message: string, error: unknown) => void;
|
||||
}
|
||||
|
||||
export async function findDuplicateNote(
|
||||
expression: string,
|
||||
excludeNoteId: number,
|
||||
noteInfo: NoteInfo,
|
||||
deps: DuplicateDetectionDeps,
|
||||
): Promise<number | null> {
|
||||
let fieldName = "";
|
||||
for (const name of Object.keys(noteInfo.fields)) {
|
||||
if (
|
||||
["word", "expression"].includes(name.toLowerCase()) &&
|
||||
noteInfo.fields[name].value
|
||||
) {
|
||||
fieldName = name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!fieldName) return null;
|
||||
|
||||
const escapedFieldName = escapeAnkiSearchValue(fieldName);
|
||||
const escapedExpression = escapeAnkiSearchValue(expression);
|
||||
const deckPrefix = deps.getDeck()
|
||||
? `"deck:${escapeAnkiSearchValue(deps.getDeck()!)}" `
|
||||
: "";
|
||||
const query = `${deckPrefix}"${escapedFieldName}:${escapedExpression}"`;
|
||||
|
||||
try {
|
||||
const noteIds = (await deps.findNotes(query, { maxRetries: 0 }) as number[]);
|
||||
return await findFirstExactDuplicateNoteId(
|
||||
noteIds,
|
||||
excludeNoteId,
|
||||
fieldName,
|
||||
expression,
|
||||
deps,
|
||||
);
|
||||
} catch (error) {
|
||||
deps.logWarn("Duplicate search failed:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function findFirstExactDuplicateNoteId(
|
||||
candidateNoteIds: number[],
|
||||
excludeNoteId: number,
|
||||
fieldName: string,
|
||||
expression: string,
|
||||
deps: DuplicateDetectionDeps,
|
||||
): Promise<number | null> {
|
||||
const candidates = candidateNoteIds.filter((id) => id !== excludeNoteId);
|
||||
if (candidates.length === 0) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
const normalizedExpression = normalizeDuplicateValue(expression);
|
||||
const chunkSize = 50;
|
||||
return (async () => {
|
||||
for (let i = 0; i < candidates.length; i += chunkSize) {
|
||||
const chunk = candidates.slice(i, i + chunkSize);
|
||||
const notesInfoResult = (await deps.notesInfo(chunk)) as unknown[];
|
||||
const notesInfo = notesInfoResult as NoteInfo[];
|
||||
for (const noteInfo of notesInfo) {
|
||||
const resolvedField = deps.resolveFieldName(noteInfo, fieldName);
|
||||
if (!resolvedField) continue;
|
||||
const candidateValue = noteInfo.fields[resolvedField]?.value || "";
|
||||
if (normalizeDuplicateValue(candidateValue) === normalizedExpression) {
|
||||
return noteInfo.noteId;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
})();
|
||||
}
|
||||
|
||||
function normalizeDuplicateValue(value: string): string {
|
||||
return value.replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
function escapeAnkiSearchValue(value: string): string {
|
||||
return value
|
||||
.replace(/\\/g, "\\\\")
|
||||
.replace(/"/g, '\\"')
|
||||
.replace(/([:*?()[\]{}])/g, "\\$1");
|
||||
}
|
||||
Reference in New Issue
Block a user