feat(anki): add configurable word card type for Kiku/Lapis (#175)

This commit is contained in:
2026-07-31 17:17:29 -07:00
committed by GitHub
parent 89ed675935
commit b204d4dd6e
31 changed files with 753 additions and 218 deletions
+60 -15
View File
@@ -1,3 +1,13 @@
import type { CardKind, WordCardKind } from '../types/anki';
import { createLogger } from '../logger';
import {
CARD_KIND_FLAG_FIELDS,
DEFAULT_WORD_CARD_KIND,
resolveWordCardKindSetting,
} from './card-kinds';
const log = createLogger('anki').child('integration.note-fields');
export interface NoteFieldValueInfo {
fields: Record<string, { value: string }>;
}
@@ -16,22 +26,57 @@ export function hasNoteFieldValue(noteInfo: NoteFieldValueInfo, preferredName: s
return (getNoteFieldValue(noteInfo, preferredName) ?? '').trim().length > 0;
}
export function shouldMarkWordAndSentenceCard(
noteInfo: NoteFieldValueInfo,
sentenceCardConfig: { lapisEnabled: boolean; kikuEnabled: boolean },
): boolean {
if (!sentenceCardConfig.lapisEnabled && !sentenceCardConfig.kikuEnabled) {
return false;
}
/** Flags set only by an explicit mine action; a note carrying one is not a word card. */
const EXPLICIT_CARD_FLAG_FIELDS = [CARD_KIND_FLAG_FIELDS.sentence, CARD_KIND_FLAG_FIELDS.audio];
const wordAndSentenceValue = getNoteFieldValue(noteInfo, 'IsWordAndSentenceCard');
if (wordAndSentenceValue === null) {
return false;
const warnedMissingFlagFields = new Set<CardKind>();
function warnMissingFlagFieldOnce(wordCardKind: CardKind, flagField: string): void {
if (wordCardKind === DEFAULT_WORD_CARD_KIND || warnedMissingFlagFields.has(wordCardKind)) {
// The default kind is also the fallback for plain note types, so its absence is expected.
return;
}
if (wordAndSentenceValue.trim().length > 0) {
return true;
}
return (
!hasNoteFieldValue(noteInfo, 'IsSentenceCard') && !hasNoteFieldValue(noteInfo, 'IsAudioCard')
warnedMissingFlagFields.add(wordCardKind);
log.warn(
`Word card type "${wordCardKind}" is configured but the note has no ${flagField} field; leaving card type flags unchanged.`,
);
}
/**
* Card kind to flag when SubMiner fills a word card's sentence, or null to leave the
* card-kind flags alone. Kiku/Lapis only: other note types have no such fields.
*/
export function resolveWordCardKind(
noteInfo: NoteFieldValueInfo,
sentenceCardConfig: {
lapisEnabled: boolean;
kikuEnabled: boolean;
wordCardKind?: WordCardKind;
},
): CardKind | null {
if (!sentenceCardConfig.lapisEnabled && !sentenceCardConfig.kikuEnabled) {
return null;
}
const wordCardKind = resolveWordCardKindSetting(sentenceCardConfig.wordCardKind);
if (wordCardKind === 'none') {
return null;
}
const flagField = CARD_KIND_FLAG_FIELDS[wordCardKind];
const flagValue = getNoteFieldValue(noteInfo, flagField);
if (flagValue === null) {
// Note type has no flag field for the configured kind.
warnMissingFlagFieldOnce(wordCardKind, flagField);
return null;
}
if (flagValue.trim().length > 0) {
return wordCardKind;
}
const alreadyExplicitCard = EXPLICIT_CARD_FLAG_FIELDS.some(
(fieldName) =>
fieldName.toLowerCase() !== flagField.toLowerCase() && hasNoteFieldValue(noteInfo, fieldName),
);
return alreadyExplicitCard ? null : wordCardKind;
}