feat: improve stats dashboard and annotation settings

This commit is contained in:
2026-03-15 21:18:35 -07:00
parent 650e95cdc3
commit 04682a02cc
75 changed files with 3420 additions and 619 deletions

View File

@@ -20,23 +20,20 @@ function makeContext(ankiConnect: unknown): {
return { context, warnings };
}
test('modern invalid nPlusOne.highlightEnabled warns modern key and does not fallback to legacy', () => {
test('modern invalid knownWords.highlightEnabled warns modern key and does not fallback to legacy', () => {
const { context, warnings } = makeContext({
behavior: { nPlusOneHighlightEnabled: true },
nPlusOne: { highlightEnabled: 'yes' },
nPlusOne: { highlightEnabled: true },
knownWords: { highlightEnabled: 'yes' },
});
applyAnkiConnectResolution(context);
assert.equal(
context.resolved.ankiConnect.nPlusOne.highlightEnabled,
DEFAULT_CONFIG.ankiConnect.nPlusOne.highlightEnabled,
);
assert.ok(warnings.some((warning) => warning.path === 'ankiConnect.nPlusOne.highlightEnabled'));
assert.equal(
warnings.some((warning) => warning.path === 'ankiConnect.behavior.nPlusOneHighlightEnabled'),
false,
context.resolved.ankiConnect.knownWords.highlightEnabled,
DEFAULT_CONFIG.ankiConnect.knownWords.highlightEnabled,
);
assert.ok(warnings.some((warning) => warning.path === 'ankiConnect.knownWords.highlightEnabled'));
assert.equal(warnings.some((warning) => warning.path === 'ankiConnect.nPlusOne.highlightEnabled'), false);
});
test('normalizes ankiConnect tags by trimming and deduping', () => {
@@ -53,18 +50,18 @@ test('normalizes ankiConnect tags by trimming and deduping', () => {
);
});
test('warns and falls back for invalid nPlusOne.decks entries', () => {
test('warns and falls back for invalid knownWords.decks entries', () => {
const { context, warnings } = makeContext({
nPlusOne: { decks: ['Core Deck', 123] },
knownWords: { decks: ['Core Deck', 123] },
});
applyAnkiConnectResolution(context);
assert.deepEqual(
context.resolved.ankiConnect.nPlusOne.decks,
DEFAULT_CONFIG.ankiConnect.nPlusOne.decks,
context.resolved.ankiConnect.knownWords.decks,
DEFAULT_CONFIG.ankiConnect.knownWords.decks,
);
assert.ok(warnings.some((warning) => warning.path === 'ankiConnect.nPlusOne.decks'));
assert.ok(warnings.some((warning) => warning.path === 'ankiConnect.knownWords.decks'));
});
test('accepts valid proxy settings', () => {

View File

@@ -42,12 +42,13 @@ export function applyAnkiConnectResolution(context: ResolveContext): void {
]);
const {
knownWords: _knownWordsConfigFromAnkiConnect,
nPlusOne: _nPlusOneConfigFromAnkiConnect,
ai: _ankiAiConfig,
...ankiConnectWithoutNPlusOne
...ankiConnectWithoutKnownWordsOrNPlusOne
} = ac as Record<string, unknown>;
const ankiConnectWithoutLegacy = Object.fromEntries(
Object.entries(ankiConnectWithoutNPlusOne).filter(([key]) => !legacyKeys.has(key)),
Object.entries(ankiConnectWithoutKnownWordsOrNPlusOne).filter(([key]) => !legacyKeys.has(key)),
);
context.resolved.ankiConnect = {
@@ -67,6 +68,9 @@ export function applyAnkiConnectResolution(context: ResolveContext): void {
? (ac.media as (typeof context.resolved)['ankiConnect']['media'])
: {}),
},
knownWords: {
...context.resolved.ankiConnect.knownWords,
},
behavior: {
...context.resolved.ankiConnect.behavior,
...(isObject(ac.behavior)
@@ -620,81 +624,126 @@ export function applyAnkiConnectResolution(context: ResolveContext): void {
);
}
const knownWordsConfig = isObject(ac.knownWords) ? (ac.knownWords as Record<string, unknown>) : {};
const nPlusOneConfig = isObject(ac.nPlusOne) ? (ac.nPlusOne as Record<string, unknown>) : {};
const nPlusOneHighlightEnabled = asBoolean(nPlusOneConfig.highlightEnabled);
if (nPlusOneHighlightEnabled !== undefined) {
context.resolved.ankiConnect.nPlusOne.highlightEnabled = nPlusOneHighlightEnabled;
const knownWordsHighlightEnabled = asBoolean(knownWordsConfig.highlightEnabled);
const legacyNPlusOneHighlightEnabled = asBoolean(nPlusOneConfig.highlightEnabled);
if (knownWordsHighlightEnabled !== undefined) {
context.resolved.ankiConnect.knownWords.highlightEnabled = knownWordsHighlightEnabled;
} else if (knownWordsConfig.highlightEnabled !== undefined) {
context.warn(
'ankiConnect.knownWords.highlightEnabled',
knownWordsConfig.highlightEnabled,
context.resolved.ankiConnect.knownWords.highlightEnabled,
'Expected boolean.',
);
context.resolved.ankiConnect.knownWords.highlightEnabled =
DEFAULT_CONFIG.ankiConnect.knownWords.highlightEnabled;
} else if (legacyNPlusOneHighlightEnabled !== undefined) {
context.resolved.ankiConnect.knownWords.highlightEnabled = legacyNPlusOneHighlightEnabled;
context.warn(
'ankiConnect.nPlusOne.highlightEnabled',
nPlusOneConfig.highlightEnabled,
DEFAULT_CONFIG.ankiConnect.knownWords.highlightEnabled,
'Legacy key is deprecated; use ankiConnect.knownWords.highlightEnabled',
);
} else if (nPlusOneConfig.highlightEnabled !== undefined) {
context.warn(
'ankiConnect.nPlusOne.highlightEnabled',
nPlusOneConfig.highlightEnabled,
context.resolved.ankiConnect.nPlusOne.highlightEnabled,
context.resolved.ankiConnect.knownWords.highlightEnabled,
'Expected boolean.',
);
context.resolved.ankiConnect.nPlusOne.highlightEnabled =
DEFAULT_CONFIG.ankiConnect.nPlusOne.highlightEnabled;
context.resolved.ankiConnect.knownWords.highlightEnabled =
DEFAULT_CONFIG.ankiConnect.knownWords.highlightEnabled;
} else {
const legacyNPlusOneHighlightEnabled = asBoolean(behavior.nPlusOneHighlightEnabled);
if (legacyNPlusOneHighlightEnabled !== undefined) {
context.resolved.ankiConnect.nPlusOne.highlightEnabled = legacyNPlusOneHighlightEnabled;
const legacyBehaviorNPlusOneHighlightEnabled = asBoolean(behavior.nPlusOneHighlightEnabled);
if (legacyBehaviorNPlusOneHighlightEnabled !== undefined) {
context.resolved.ankiConnect.knownWords.highlightEnabled =
legacyBehaviorNPlusOneHighlightEnabled;
context.warn(
'ankiConnect.behavior.nPlusOneHighlightEnabled',
behavior.nPlusOneHighlightEnabled,
DEFAULT_CONFIG.ankiConnect.nPlusOne.highlightEnabled,
'Legacy key is deprecated; use ankiConnect.nPlusOne.highlightEnabled',
DEFAULT_CONFIG.ankiConnect.knownWords.highlightEnabled,
'Legacy key is deprecated; use ankiConnect.knownWords.highlightEnabled',
);
} else {
context.resolved.ankiConnect.nPlusOne.highlightEnabled =
DEFAULT_CONFIG.ankiConnect.nPlusOne.highlightEnabled;
context.resolved.ankiConnect.knownWords.highlightEnabled =
DEFAULT_CONFIG.ankiConnect.knownWords.highlightEnabled;
}
}
const nPlusOneRefreshMinutes = asNumber(nPlusOneConfig.refreshMinutes);
const hasValidNPlusOneRefreshMinutes =
nPlusOneRefreshMinutes !== undefined &&
Number.isInteger(nPlusOneRefreshMinutes) &&
nPlusOneRefreshMinutes > 0;
if (nPlusOneRefreshMinutes !== undefined) {
if (hasValidNPlusOneRefreshMinutes) {
context.resolved.ankiConnect.nPlusOne.refreshMinutes = nPlusOneRefreshMinutes;
const knownWordsRefreshMinutes = asNumber(knownWordsConfig.refreshMinutes);
const legacyNPlusOneRefreshMinutes = asNumber(nPlusOneConfig.refreshMinutes);
const hasValidKnownWordsRefreshMinutes =
knownWordsRefreshMinutes !== undefined &&
Number.isInteger(knownWordsRefreshMinutes) &&
knownWordsRefreshMinutes > 0;
const hasValidLegacyNPlusOneRefreshMinutes =
legacyNPlusOneRefreshMinutes !== undefined &&
Number.isInteger(legacyNPlusOneRefreshMinutes) &&
legacyNPlusOneRefreshMinutes > 0;
if (knownWordsRefreshMinutes !== undefined) {
if (hasValidKnownWordsRefreshMinutes) {
context.resolved.ankiConnect.knownWords.refreshMinutes = knownWordsRefreshMinutes;
} else {
context.warn(
'ankiConnect.knownWords.refreshMinutes',
knownWordsConfig.refreshMinutes,
context.resolved.ankiConnect.knownWords.refreshMinutes,
'Expected a positive integer.',
);
context.resolved.ankiConnect.knownWords.refreshMinutes =
DEFAULT_CONFIG.ankiConnect.knownWords.refreshMinutes;
}
} else if (legacyNPlusOneRefreshMinutes !== undefined) {
if (hasValidLegacyNPlusOneRefreshMinutes) {
context.resolved.ankiConnect.knownWords.refreshMinutes = legacyNPlusOneRefreshMinutes;
context.warn(
'ankiConnect.nPlusOne.refreshMinutes',
nPlusOneConfig.refreshMinutes,
DEFAULT_CONFIG.ankiConnect.knownWords.refreshMinutes,
'Legacy key is deprecated; use ankiConnect.knownWords.refreshMinutes',
);
} else {
context.warn(
'ankiConnect.nPlusOne.refreshMinutes',
nPlusOneConfig.refreshMinutes,
context.resolved.ankiConnect.nPlusOne.refreshMinutes,
context.resolved.ankiConnect.knownWords.refreshMinutes,
'Expected a positive integer.',
);
context.resolved.ankiConnect.nPlusOne.refreshMinutes =
DEFAULT_CONFIG.ankiConnect.nPlusOne.refreshMinutes;
context.resolved.ankiConnect.knownWords.refreshMinutes =
DEFAULT_CONFIG.ankiConnect.knownWords.refreshMinutes;
}
} else if (asNumber(behavior.nPlusOneRefreshMinutes) !== undefined) {
const legacyNPlusOneRefreshMinutes = asNumber(behavior.nPlusOneRefreshMinutes);
const legacyBehaviorNPlusOneRefreshMinutes = asNumber(behavior.nPlusOneRefreshMinutes);
const hasValidLegacyRefreshMinutes =
legacyNPlusOneRefreshMinutes !== undefined &&
Number.isInteger(legacyNPlusOneRefreshMinutes) &&
legacyNPlusOneRefreshMinutes > 0;
legacyBehaviorNPlusOneRefreshMinutes !== undefined &&
Number.isInteger(legacyBehaviorNPlusOneRefreshMinutes) &&
legacyBehaviorNPlusOneRefreshMinutes > 0;
if (hasValidLegacyRefreshMinutes) {
context.resolved.ankiConnect.nPlusOne.refreshMinutes = legacyNPlusOneRefreshMinutes;
context.resolved.ankiConnect.knownWords.refreshMinutes =
legacyBehaviorNPlusOneRefreshMinutes;
context.warn(
'ankiConnect.behavior.nPlusOneRefreshMinutes',
behavior.nPlusOneRefreshMinutes,
DEFAULT_CONFIG.ankiConnect.nPlusOne.refreshMinutes,
'Legacy key is deprecated; use ankiConnect.nPlusOne.refreshMinutes',
DEFAULT_CONFIG.ankiConnect.knownWords.refreshMinutes,
'Legacy key is deprecated; use ankiConnect.knownWords.refreshMinutes',
);
} else {
context.warn(
'ankiConnect.behavior.nPlusOneRefreshMinutes',
behavior.nPlusOneRefreshMinutes,
context.resolved.ankiConnect.nPlusOne.refreshMinutes,
context.resolved.ankiConnect.knownWords.refreshMinutes,
'Expected a positive integer.',
);
context.resolved.ankiConnect.nPlusOne.refreshMinutes =
DEFAULT_CONFIG.ankiConnect.nPlusOne.refreshMinutes;
context.resolved.ankiConnect.knownWords.refreshMinutes =
DEFAULT_CONFIG.ankiConnect.knownWords.refreshMinutes;
}
} else {
context.resolved.ankiConnect.nPlusOne.refreshMinutes =
DEFAULT_CONFIG.ankiConnect.nPlusOne.refreshMinutes;
context.resolved.ankiConnect.knownWords.refreshMinutes =
DEFAULT_CONFIG.ankiConnect.knownWords.refreshMinutes;
}
const nPlusOneMinSentenceWords = asNumber(nPlusOneConfig.minSentenceWords);
@@ -720,72 +769,137 @@ export function applyAnkiConnectResolution(context: ResolveContext): void {
DEFAULT_CONFIG.ankiConnect.nPlusOne.minSentenceWords;
}
const nPlusOneMatchMode = asString(nPlusOneConfig.matchMode);
const legacyNPlusOneMatchMode = asString(behavior.nPlusOneMatchMode);
const hasValidNPlusOneMatchMode =
nPlusOneMatchMode === 'headword' || nPlusOneMatchMode === 'surface';
const hasValidLegacyMatchMode =
const knownWordsMatchMode = asString(knownWordsConfig.matchMode);
const legacyNPlusOneMatchMode = asString(nPlusOneConfig.matchMode);
const legacyBehaviorNPlusOneMatchMode = asString(behavior.nPlusOneMatchMode);
const hasValidKnownWordsMatchMode =
knownWordsMatchMode === 'headword' || knownWordsMatchMode === 'surface';
const hasValidLegacyNPlusOneMatchMode =
legacyNPlusOneMatchMode === 'headword' || legacyNPlusOneMatchMode === 'surface';
if (hasValidNPlusOneMatchMode) {
context.resolved.ankiConnect.nPlusOne.matchMode = nPlusOneMatchMode;
} else if (nPlusOneMatchMode !== undefined) {
const hasValidLegacyMatchMode =
legacyBehaviorNPlusOneMatchMode === 'headword' || legacyBehaviorNPlusOneMatchMode === 'surface';
if (hasValidKnownWordsMatchMode) {
context.resolved.ankiConnect.knownWords.matchMode = knownWordsMatchMode;
} else if (knownWordsMatchMode !== undefined) {
context.warn(
'ankiConnect.nPlusOne.matchMode',
nPlusOneConfig.matchMode,
DEFAULT_CONFIG.ankiConnect.nPlusOne.matchMode,
'ankiConnect.knownWords.matchMode',
knownWordsConfig.matchMode,
DEFAULT_CONFIG.ankiConnect.knownWords.matchMode,
"Expected 'headword' or 'surface'.",
);
context.resolved.ankiConnect.nPlusOne.matchMode = DEFAULT_CONFIG.ankiConnect.nPlusOne.matchMode;
context.resolved.ankiConnect.knownWords.matchMode =
DEFAULT_CONFIG.ankiConnect.knownWords.matchMode;
} else if (legacyNPlusOneMatchMode !== undefined) {
if (hasValidLegacyNPlusOneMatchMode) {
context.resolved.ankiConnect.knownWords.matchMode = legacyNPlusOneMatchMode;
context.warn(
'ankiConnect.nPlusOne.matchMode',
nPlusOneConfig.matchMode,
DEFAULT_CONFIG.ankiConnect.knownWords.matchMode,
'Legacy key is deprecated; use ankiConnect.knownWords.matchMode',
);
} else {
context.warn(
'ankiConnect.nPlusOne.matchMode',
nPlusOneConfig.matchMode,
context.resolved.ankiConnect.knownWords.matchMode,
"Expected 'headword' or 'surface'.",
);
context.resolved.ankiConnect.knownWords.matchMode =
DEFAULT_CONFIG.ankiConnect.knownWords.matchMode;
}
} else if (legacyBehaviorNPlusOneMatchMode !== undefined) {
if (hasValidLegacyMatchMode) {
context.resolved.ankiConnect.nPlusOne.matchMode = legacyNPlusOneMatchMode;
context.resolved.ankiConnect.knownWords.matchMode = legacyBehaviorNPlusOneMatchMode;
context.warn(
'ankiConnect.behavior.nPlusOneMatchMode',
behavior.nPlusOneMatchMode,
DEFAULT_CONFIG.ankiConnect.nPlusOne.matchMode,
'Legacy key is deprecated; use ankiConnect.nPlusOne.matchMode',
DEFAULT_CONFIG.ankiConnect.knownWords.matchMode,
'Legacy key is deprecated; use ankiConnect.knownWords.matchMode',
);
} else {
context.warn(
'ankiConnect.behavior.nPlusOneMatchMode',
behavior.nPlusOneMatchMode,
context.resolved.ankiConnect.nPlusOne.matchMode,
context.resolved.ankiConnect.knownWords.matchMode,
"Expected 'headword' or 'surface'.",
);
context.resolved.ankiConnect.nPlusOne.matchMode =
DEFAULT_CONFIG.ankiConnect.nPlusOne.matchMode;
context.resolved.ankiConnect.knownWords.matchMode =
DEFAULT_CONFIG.ankiConnect.knownWords.matchMode;
}
} else {
context.resolved.ankiConnect.nPlusOne.matchMode = DEFAULT_CONFIG.ankiConnect.nPlusOne.matchMode;
context.resolved.ankiConnect.knownWords.matchMode =
DEFAULT_CONFIG.ankiConnect.knownWords.matchMode;
}
const nPlusOneDecks = nPlusOneConfig.decks;
if (Array.isArray(nPlusOneDecks)) {
const normalizedDecks = nPlusOneDecks
const knownWordsDecks = knownWordsConfig.decks;
const legacyNPlusOneDecks = nPlusOneConfig.decks;
if (Array.isArray(knownWordsDecks)) {
const normalizedDecks = knownWordsDecks
.filter((entry): entry is string => typeof entry === 'string')
.map((entry) => entry.trim())
.filter((entry) => entry.length > 0);
if (normalizedDecks.length === nPlusOneDecks.length) {
context.resolved.ankiConnect.nPlusOne.decks = [...new Set(normalizedDecks)];
} else if (nPlusOneDecks.length > 0) {
if (normalizedDecks.length === knownWordsDecks.length) {
context.resolved.ankiConnect.knownWords.decks = [...new Set(normalizedDecks)];
} else if (knownWordsDecks.length > 0) {
context.warn(
'ankiConnect.nPlusOne.decks',
nPlusOneDecks,
context.resolved.ankiConnect.nPlusOne.decks,
'ankiConnect.knownWords.decks',
knownWordsDecks,
context.resolved.ankiConnect.knownWords.decks,
'Expected an array of strings.',
);
context.resolved.ankiConnect.knownWords.decks = DEFAULT_CONFIG.ankiConnect.knownWords.decks;
} else {
context.resolved.ankiConnect.nPlusOne.decks = [];
context.resolved.ankiConnect.knownWords.decks = [];
}
} else if (nPlusOneDecks !== undefined) {
} else if (knownWordsDecks !== undefined) {
context.warn(
'ankiConnect.nPlusOne.decks',
nPlusOneDecks,
context.resolved.ankiConnect.nPlusOne.decks,
'ankiConnect.knownWords.decks',
knownWordsDecks,
context.resolved.ankiConnect.knownWords.decks,
'Expected an array of strings.',
);
context.resolved.ankiConnect.nPlusOne.decks = [];
context.resolved.ankiConnect.knownWords.decks = DEFAULT_CONFIG.ankiConnect.knownWords.decks;
} else if (Array.isArray(legacyNPlusOneDecks)) {
const normalizedDecks = legacyNPlusOneDecks
.filter((entry): entry is string => typeof entry === 'string')
.map((entry) => entry.trim())
.filter((entry) => entry.length > 0);
if (normalizedDecks.length === legacyNPlusOneDecks.length) {
context.resolved.ankiConnect.knownWords.decks = [...new Set(normalizedDecks)];
context.warn(
'ankiConnect.nPlusOne.decks',
legacyNPlusOneDecks,
DEFAULT_CONFIG.ankiConnect.knownWords.decks,
'Legacy key is deprecated; use ankiConnect.knownWords.decks',
);
} else if (legacyNPlusOneDecks.length > 0) {
context.warn(
'ankiConnect.nPlusOne.decks',
legacyNPlusOneDecks,
context.resolved.ankiConnect.knownWords.decks,
'Expected an array of strings.',
);
context.resolved.ankiConnect.knownWords.decks = DEFAULT_CONFIG.ankiConnect.knownWords.decks;
} else {
context.resolved.ankiConnect.knownWords.decks = [];
context.warn(
'ankiConnect.nPlusOne.decks',
legacyNPlusOneDecks,
DEFAULT_CONFIG.ankiConnect.knownWords.decks,
'Legacy key is deprecated; use ankiConnect.knownWords.decks',
);
}
} else if (legacyNPlusOneDecks !== undefined) {
context.warn(
'ankiConnect.nPlusOne.decks',
legacyNPlusOneDecks,
context.resolved.ankiConnect.knownWords.decks,
'Expected an array of strings.',
);
context.resolved.ankiConnect.knownWords.decks = DEFAULT_CONFIG.ankiConnect.knownWords.decks;
}
const nPlusOneHighlightColor = asColor(nPlusOneConfig.nPlusOne);
@@ -801,17 +915,34 @@ export function applyAnkiConnectResolution(context: ResolveContext): void {
context.resolved.ankiConnect.nPlusOne.nPlusOne = DEFAULT_CONFIG.ankiConnect.nPlusOne.nPlusOne;
}
const nPlusOneKnownWordColor = asColor(nPlusOneConfig.knownWord);
if (nPlusOneKnownWordColor !== undefined) {
context.resolved.ankiConnect.nPlusOne.knownWord = nPlusOneKnownWordColor;
const knownWordsColor = asColor(knownWordsConfig.color);
const legacyNPlusOneKnownWordColor = asColor(nPlusOneConfig.knownWord);
if (knownWordsColor !== undefined) {
context.resolved.ankiConnect.knownWords.color = knownWordsColor;
} else if (knownWordsConfig.color !== undefined) {
context.warn(
'ankiConnect.knownWords.color',
knownWordsConfig.color,
context.resolved.ankiConnect.knownWords.color,
'Expected a hex color value.',
);
context.resolved.ankiConnect.knownWords.color = DEFAULT_CONFIG.ankiConnect.knownWords.color;
} else if (legacyNPlusOneKnownWordColor !== undefined) {
context.resolved.ankiConnect.knownWords.color = legacyNPlusOneKnownWordColor;
context.warn(
'ankiConnect.nPlusOne.knownWord',
nPlusOneConfig.knownWord,
DEFAULT_CONFIG.ankiConnect.knownWords.color,
'Legacy key is deprecated; use ankiConnect.knownWords.color',
);
} else if (nPlusOneConfig.knownWord !== undefined) {
context.warn(
'ankiConnect.nPlusOne.knownWord',
nPlusOneConfig.knownWord,
context.resolved.ankiConnect.nPlusOne.knownWord,
context.resolved.ankiConnect.knownWords.color,
'Expected a hex color value.',
);
context.resolved.ankiConnect.nPlusOne.knownWord = DEFAULT_CONFIG.ankiConnect.nPlusOne.knownWord;
context.resolved.ankiConnect.knownWords.color = DEFAULT_CONFIG.ankiConnect.knownWords.color;
}
if (