mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-09-21 05:16:20 -07:00
fix(config): validate AnkiConnect and field grouping settings (#257)
This commit is contained in:
@@ -0,0 +1,4 @@
|
|||||||
|
type: fixed
|
||||||
|
area: config
|
||||||
|
|
||||||
|
- Validate direct AnkiConnect, Kiku, and Senren settings before admitting them to runtime config, with warnings and defaults for invalid values.
|
||||||
@@ -19,6 +19,8 @@ This project is built primarily for [Kiku](https://kiku.youyoumu.my.id/) and [La
|
|||||||
|
|
||||||
AnkiConnect listens on `http://127.0.0.1:8765` by default. If you changed the port in AnkiConnect's settings, update `ankiConnect.url` in your SubMiner config.
|
AnkiConnect listens on `http://127.0.0.1:8765` by default. If you changed the port in AnkiConnect's settings, update `ankiConnect.url` in your SubMiner config.
|
||||||
|
|
||||||
|
AnkiConnect and Kiku/Senren settings follow the [configuration validation rules](/configuration#configuration-file): invalid values produce a warning and fall back to the option's default. Use JSON booleans such as `true`, not strings such as `"true"`, and a positive number for `ankiConnect.pollingRate`.
|
||||||
|
|
||||||
## Auto-enrichment transport
|
## Auto-enrichment transport
|
||||||
|
|
||||||
When you add a word via Yomitan, SubMiner detects the new card and fills in the sentence, audio, and image fields automatically. Two detection methods are available:
|
When you add a word via Yomitan, SubMiner detects the new card and fills in the sentence, audio, and image fields automatically. Two detection methods are available:
|
||||||
|
|||||||
@@ -2818,6 +2818,29 @@ test('forces Senren off when Kiku is also enabled and validates Senren fieldGrou
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('warns and falls back when isSenren.enabled is not boolean', () => {
|
||||||
|
const dir = makeTempDir();
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(dir, 'config.jsonc'),
|
||||||
|
`{
|
||||||
|
"ankiConnect": {
|
||||||
|
"isSenren": { "enabled": "true" }
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
'utf-8',
|
||||||
|
);
|
||||||
|
|
||||||
|
const service = new ConfigService(dir);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
service.getConfig().ankiConnect.isSenren.enabled,
|
||||||
|
DEFAULT_CONFIG.ankiConnect.isSenren.enabled,
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
service.getWarnings().some((warning) => warning.path === 'ankiConnect.isSenren.enabled'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('accepts valid ankiConnect knownWords deck object', () => {
|
test('accepts valid ankiConnect knownWords deck object', () => {
|
||||||
const dir = makeTempDir();
|
const dir = makeTempDir();
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
|
|||||||
@@ -32,6 +32,101 @@ test('media timing review is disabled by default and accepts a boolean override'
|
|||||||
assert.deepEqual(enabledContext.warnings, []);
|
assert.deepEqual(enabledContext.warnings, []);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('invalid direct and field-grouping Anki values warn and keep defaults', () => {
|
||||||
|
const { context, warnings } = makeContext({
|
||||||
|
enabled: 'true',
|
||||||
|
url: 8765,
|
||||||
|
pollingRate: '3000',
|
||||||
|
deck: ['Mining'],
|
||||||
|
isKiku: {
|
||||||
|
enabled: 1,
|
||||||
|
fieldGrouping: 'sometimes',
|
||||||
|
deleteDuplicateInAuto: 'false',
|
||||||
|
},
|
||||||
|
isSenren: {
|
||||||
|
enabled: 'true',
|
||||||
|
fieldGrouping: false,
|
||||||
|
deleteDuplicateInAuto: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
applyAnkiConnectResolution(context);
|
||||||
|
|
||||||
|
assert.equal(context.resolved.ankiConnect.enabled, DEFAULT_CONFIG.ankiConnect.enabled);
|
||||||
|
assert.equal(context.resolved.ankiConnect.url, DEFAULT_CONFIG.ankiConnect.url);
|
||||||
|
assert.equal(context.resolved.ankiConnect.pollingRate, DEFAULT_CONFIG.ankiConnect.pollingRate);
|
||||||
|
assert.equal(context.resolved.ankiConnect.deck, DEFAULT_CONFIG.ankiConnect.deck);
|
||||||
|
assert.deepEqual(context.resolved.ankiConnect.isKiku, DEFAULT_CONFIG.ankiConnect.isKiku);
|
||||||
|
assert.deepEqual(context.resolved.ankiConnect.isSenren, DEFAULT_CONFIG.ankiConnect.isSenren);
|
||||||
|
assert.deepEqual(
|
||||||
|
warnings.map((warning) => warning.path),
|
||||||
|
[
|
||||||
|
'ankiConnect.enabled',
|
||||||
|
'ankiConnect.url',
|
||||||
|
'ankiConnect.pollingRate',
|
||||||
|
'ankiConnect.deck',
|
||||||
|
'ankiConnect.isKiku.enabled',
|
||||||
|
'ankiConnect.isKiku.deleteDuplicateInAuto',
|
||||||
|
'ankiConnect.isKiku.fieldGrouping',
|
||||||
|
'ankiConnect.isSenren.enabled',
|
||||||
|
'ankiConnect.isSenren.deleteDuplicateInAuto',
|
||||||
|
'ankiConnect.isSenren.fieldGrouping',
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('accepts valid direct and field-grouping Anki values', () => {
|
||||||
|
const { context, warnings } = makeContext({
|
||||||
|
enabled: false,
|
||||||
|
url: 'http://127.0.0.1:9876',
|
||||||
|
pollingRate: 750,
|
||||||
|
deck: 'Mining',
|
||||||
|
isKiku: {
|
||||||
|
enabled: true,
|
||||||
|
fieldGrouping: 'manual',
|
||||||
|
deleteDuplicateInAuto: false,
|
||||||
|
},
|
||||||
|
isSenren: {
|
||||||
|
enabled: false,
|
||||||
|
fieldGrouping: 'disabled',
|
||||||
|
deleteDuplicateInAuto: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
applyAnkiConnectResolution(context);
|
||||||
|
|
||||||
|
assert.equal(context.resolved.ankiConnect.enabled, false);
|
||||||
|
assert.equal(context.resolved.ankiConnect.url, 'http://127.0.0.1:9876');
|
||||||
|
assert.equal(context.resolved.ankiConnect.pollingRate, 750);
|
||||||
|
assert.equal(context.resolved.ankiConnect.deck, 'Mining');
|
||||||
|
assert.deepEqual(context.resolved.ankiConnect.isKiku, {
|
||||||
|
enabled: true,
|
||||||
|
fieldGrouping: 'manual',
|
||||||
|
deleteDuplicateInAuto: false,
|
||||||
|
});
|
||||||
|
assert.deepEqual(context.resolved.ankiConnect.isSenren, {
|
||||||
|
enabled: false,
|
||||||
|
fieldGrouping: 'disabled',
|
||||||
|
deleteDuplicateInAuto: false,
|
||||||
|
});
|
||||||
|
assert.deepEqual(warnings, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ignores unknown Anki keys without warning or admitting them to resolved config', () => {
|
||||||
|
const { context, warnings } = makeContext({
|
||||||
|
futureOption: { enabled: true },
|
||||||
|
isKiku: { futureGroupingOption: 'future' },
|
||||||
|
isSenren: { futureGroupingOption: 'future' },
|
||||||
|
});
|
||||||
|
|
||||||
|
applyAnkiConnectResolution(context);
|
||||||
|
|
||||||
|
assert.equal(Object.hasOwn(context.resolved.ankiConnect, 'futureOption'), false);
|
||||||
|
assert.equal(Object.hasOwn(context.resolved.ankiConnect.isKiku, 'futureGroupingOption'), false);
|
||||||
|
assert.equal(Object.hasOwn(context.resolved.ankiConnect.isSenren, 'futureGroupingOption'), false);
|
||||||
|
assert.deepEqual(warnings, []);
|
||||||
|
});
|
||||||
|
|
||||||
test('modern media duration accepts zero as the disabled cap sentinel', () => {
|
test('modern media duration accepts zero as the disabled cap sentinel', () => {
|
||||||
const disabledCap = makeContext({ media: { maxMediaDuration: 0 } });
|
const disabledCap = makeContext({ media: { maxMediaDuration: 0 } });
|
||||||
applyAnkiConnectResolution(disabledCap.context);
|
applyAnkiConnectResolution(disabledCap.context);
|
||||||
|
|||||||
@@ -19,11 +19,11 @@ export function applyAnkiConnectResolution(context: ResolveContext): void {
|
|||||||
const media = isObject(ankiConnect.media) ? ankiConnect.media : {};
|
const media = isObject(ankiConnect.media) ? ankiConnect.media : {};
|
||||||
const metadata = isObject(ankiConnect.metadata) ? ankiConnect.metadata : {};
|
const metadata = isObject(ankiConnect.metadata) ? ankiConnect.metadata : {};
|
||||||
|
|
||||||
initializeAnkiConnectResolution(context, ankiConnect);
|
initializeAnkiConnectResolution(context);
|
||||||
applyAnkiModernResolution(context, ankiConnect, behavior, media);
|
applyAnkiModernResolution(context, ankiConnect, behavior, media);
|
||||||
applyAnkiLegacyResolution(context, ankiConnect, behavior, fields, media, metadata);
|
applyAnkiLegacyResolution(context, ankiConnect, behavior, fields, media, metadata);
|
||||||
applyAnkiKnownWordsResolution(context, ankiConnect, behavior);
|
applyAnkiKnownWordsResolution(context, ankiConnect, behavior);
|
||||||
applyAnkiKikuResolution(context);
|
applyAnkiKikuResolution(context, ankiConnect);
|
||||||
applyAnkiSenrenResolution(context);
|
applyAnkiSenrenResolution(context, ankiConnect);
|
||||||
applyAnkiLapisKikuResolution(context, ankiConnect);
|
applyAnkiLapisKikuResolution(context, ankiConnect);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { DEFAULT_CONFIG } from '../../definitions';
|
||||||
|
import type { ResolveContext } from '../context';
|
||||||
|
import { asBoolean, asString } from '../shared';
|
||||||
|
import { applyModernValue, asPositiveNumber } from './modern-value';
|
||||||
|
|
||||||
|
export function applyAnkiBaseResolution(
|
||||||
|
context: ResolveContext,
|
||||||
|
ankiConnect: Record<string, unknown>,
|
||||||
|
): void {
|
||||||
|
applyModernValue(
|
||||||
|
context,
|
||||||
|
ankiConnect,
|
||||||
|
'enabled',
|
||||||
|
'ankiConnect.enabled',
|
||||||
|
asBoolean,
|
||||||
|
DEFAULT_CONFIG.ankiConnect.enabled,
|
||||||
|
(value) => {
|
||||||
|
context.resolved.ankiConnect.enabled = value;
|
||||||
|
},
|
||||||
|
'Expected boolean.',
|
||||||
|
);
|
||||||
|
applyModernValue(
|
||||||
|
context,
|
||||||
|
ankiConnect,
|
||||||
|
'url',
|
||||||
|
'ankiConnect.url',
|
||||||
|
asString,
|
||||||
|
DEFAULT_CONFIG.ankiConnect.url,
|
||||||
|
(value) => {
|
||||||
|
context.resolved.ankiConnect.url = value;
|
||||||
|
},
|
||||||
|
'Expected string.',
|
||||||
|
);
|
||||||
|
applyModernValue(
|
||||||
|
context,
|
||||||
|
ankiConnect,
|
||||||
|
'pollingRate',
|
||||||
|
'ankiConnect.pollingRate',
|
||||||
|
asPositiveNumber,
|
||||||
|
DEFAULT_CONFIG.ankiConnect.pollingRate,
|
||||||
|
(value) => {
|
||||||
|
context.resolved.ankiConnect.pollingRate = value;
|
||||||
|
},
|
||||||
|
'Expected positive number.',
|
||||||
|
);
|
||||||
|
applyModernValue(
|
||||||
|
context,
|
||||||
|
ankiConnect,
|
||||||
|
'deck',
|
||||||
|
'ankiConnect.deck',
|
||||||
|
asString,
|
||||||
|
DEFAULT_CONFIG.ankiConnect.deck,
|
||||||
|
(value) => {
|
||||||
|
context.resolved.ankiConnect.deck = value;
|
||||||
|
},
|
||||||
|
'Expected string.',
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { DEFAULT_CONFIG } from '../../definitions';
|
||||||
|
import type { ResolveContext } from '../context';
|
||||||
|
import { asBoolean, isObject } from '../shared';
|
||||||
|
import { applyModernValue } from './modern-value';
|
||||||
|
|
||||||
|
type FieldGroupingConfigKey = 'isKiku' | 'isSenren';
|
||||||
|
|
||||||
|
export function applyFieldGroupingConfigResolution(
|
||||||
|
context: ResolveContext,
|
||||||
|
ankiConnect: Record<string, unknown>,
|
||||||
|
key: FieldGroupingConfigKey,
|
||||||
|
): void {
|
||||||
|
const source = ankiConnect[key];
|
||||||
|
if (!isObject(source)) {
|
||||||
|
if (source !== undefined) {
|
||||||
|
context.warn(
|
||||||
|
`ankiConnect.${key}`,
|
||||||
|
source,
|
||||||
|
DEFAULT_CONFIG.ankiConnect[key],
|
||||||
|
'Expected object.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const booleanKey of ['enabled', 'deleteDuplicateInAuto'] as const) {
|
||||||
|
applyModernValue(
|
||||||
|
context,
|
||||||
|
source,
|
||||||
|
booleanKey,
|
||||||
|
`ankiConnect.${key}.${booleanKey}`,
|
||||||
|
asBoolean,
|
||||||
|
DEFAULT_CONFIG.ankiConnect[key][booleanKey],
|
||||||
|
(value) => {
|
||||||
|
context.resolved.ankiConnect[key][booleanKey] = value;
|
||||||
|
},
|
||||||
|
'Expected boolean.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
applyModernValue(
|
||||||
|
context,
|
||||||
|
source,
|
||||||
|
'fieldGrouping',
|
||||||
|
`ankiConnect.${key}.fieldGrouping`,
|
||||||
|
(value) => (value === 'auto' || value === 'manual' || value === 'disabled' ? value : undefined),
|
||||||
|
DEFAULT_CONFIG.ankiConnect[key].fieldGrouping,
|
||||||
|
(value) => {
|
||||||
|
context.resolved.ankiConnect[key].fieldGrouping = value;
|
||||||
|
},
|
||||||
|
'Expected auto, manual, or disabled.',
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,55 +1,8 @@
|
|||||||
import type { ResolveContext } from '../context';
|
import type { ResolveContext } from '../context';
|
||||||
import { isObject } from '../shared';
|
|
||||||
|
|
||||||
const LEGACY_KEYS = new Set([
|
|
||||||
'wordField',
|
|
||||||
'audioField',
|
|
||||||
'imageField',
|
|
||||||
'sentenceField',
|
|
||||||
'miscInfoField',
|
|
||||||
'miscInfoPattern',
|
|
||||||
'generateAudio',
|
|
||||||
'generateImage',
|
|
||||||
'imageType',
|
|
||||||
'imageFormat',
|
|
||||||
'imageQuality',
|
|
||||||
'imageMaxWidth',
|
|
||||||
'imageMaxHeight',
|
|
||||||
'animatedFps',
|
|
||||||
'animatedMaxWidth',
|
|
||||||
'animatedMaxHeight',
|
|
||||||
'animatedCrf',
|
|
||||||
'syncAnimatedImageToWordAudio',
|
|
||||||
'audioPadding',
|
|
||||||
'fallbackDuration',
|
|
||||||
'maxMediaDuration',
|
|
||||||
'overwriteAudio',
|
|
||||||
'overwriteImage',
|
|
||||||
'mediaInsertMode',
|
|
||||||
'highlightWord',
|
|
||||||
'notificationType',
|
|
||||||
'autoUpdateNewCards',
|
|
||||||
]);
|
|
||||||
|
|
||||||
export function initializeAnkiConnectResolution(
|
|
||||||
context: ResolveContext,
|
|
||||||
ankiConnect: Record<string, unknown>,
|
|
||||||
): void {
|
|
||||||
const {
|
|
||||||
knownWords: _knownWordsConfigFromAnkiConnect,
|
|
||||||
nPlusOne: _nPlusOneConfigFromAnkiConnect,
|
|
||||||
ai: _ankiAiConfig,
|
|
||||||
...ankiConnectWithoutKnownWordsOrNPlusOne
|
|
||||||
} = ankiConnect;
|
|
||||||
const ankiConnectWithoutLegacy = Object.fromEntries(
|
|
||||||
Object.entries(ankiConnectWithoutKnownWordsOrNPlusOne).filter(([key]) => !LEGACY_KEYS.has(key)),
|
|
||||||
);
|
|
||||||
|
|
||||||
|
export function initializeAnkiConnectResolution(context: ResolveContext): void {
|
||||||
context.resolved.ankiConnect = {
|
context.resolved.ankiConnect = {
|
||||||
...context.resolved.ankiConnect,
|
...context.resolved.ankiConnect,
|
||||||
...(isObject(ankiConnectWithoutLegacy)
|
|
||||||
? (ankiConnectWithoutLegacy as Partial<(typeof context.resolved)['ankiConnect']>)
|
|
||||||
: {}),
|
|
||||||
fields: {
|
fields: {
|
||||||
...context.resolved.ankiConnect.fields,
|
...context.resolved.ankiConnect.fields,
|
||||||
},
|
},
|
||||||
@@ -73,15 +26,9 @@ export function initializeAnkiConnectResolution(
|
|||||||
},
|
},
|
||||||
isKiku: {
|
isKiku: {
|
||||||
...context.resolved.ankiConnect.isKiku,
|
...context.resolved.ankiConnect.isKiku,
|
||||||
...(isObject(ankiConnect.isKiku)
|
|
||||||
? (ankiConnect.isKiku as (typeof context.resolved)['ankiConnect']['isKiku'])
|
|
||||||
: {}),
|
|
||||||
},
|
},
|
||||||
isSenren: {
|
isSenren: {
|
||||||
...context.resolved.ankiConnect.isSenren,
|
...context.resolved.ankiConnect.isSenren,
|
||||||
...(isObject(ankiConnect.isSenren)
|
|
||||||
? (ankiConnect.isSenren as (typeof context.resolved)['ankiConnect']['isSenren'])
|
|
||||||
: {}),
|
|
||||||
},
|
},
|
||||||
lapisKiku: {
|
lapisKiku: {
|
||||||
...context.resolved.ankiConnect.lapisKiku,
|
...context.resolved.ankiConnect.lapisKiku,
|
||||||
|
|||||||
@@ -1,19 +1,9 @@
|
|||||||
import { DEFAULT_CONFIG } from '../../definitions';
|
|
||||||
import type { ResolveContext } from '../context';
|
import type { ResolveContext } from '../context';
|
||||||
|
import { applyFieldGroupingConfigResolution } from './field-grouping-config';
|
||||||
|
|
||||||
export function applyAnkiKikuResolution(context: ResolveContext): void {
|
export function applyAnkiKikuResolution(
|
||||||
if (
|
context: ResolveContext,
|
||||||
context.resolved.ankiConnect.isKiku.fieldGrouping !== 'auto' &&
|
ankiConnect: Record<string, unknown>,
|
||||||
context.resolved.ankiConnect.isKiku.fieldGrouping !== 'manual' &&
|
): void {
|
||||||
context.resolved.ankiConnect.isKiku.fieldGrouping !== 'disabled'
|
applyFieldGroupingConfigResolution(context, ankiConnect, 'isKiku');
|
||||||
) {
|
|
||||||
context.warn(
|
|
||||||
'ankiConnect.isKiku.fieldGrouping',
|
|
||||||
context.resolved.ankiConnect.isKiku.fieldGrouping,
|
|
||||||
DEFAULT_CONFIG.ankiConnect.isKiku.fieldGrouping,
|
|
||||||
'Expected auto, manual, or disabled.',
|
|
||||||
);
|
|
||||||
context.resolved.ankiConnect.isKiku.fieldGrouping =
|
|
||||||
DEFAULT_CONFIG.ankiConnect.isKiku.fieldGrouping;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { ResolveContext } from '../context';
|
import type { ResolveContext } from '../context';
|
||||||
import { isObject } from '../shared';
|
import { isObject } from '../shared';
|
||||||
import { applyAiResolution } from './ai';
|
import { applyAiResolution } from './ai';
|
||||||
|
import { applyAnkiBaseResolution } from './base';
|
||||||
import { applyLapisResolution } from './lapis';
|
import { applyLapisResolution } from './lapis';
|
||||||
import { applyModernBehaviorResolution } from './modern-behavior';
|
import { applyModernBehaviorResolution } from './modern-behavior';
|
||||||
import { applyModernFieldsResolution } from './modern-fields';
|
import { applyModernFieldsResolution } from './modern-fields';
|
||||||
@@ -18,6 +19,7 @@ export function applyAnkiModernResolution(
|
|||||||
const fields = isObject(ankiConnect.fields) ? ankiConnect.fields : {};
|
const fields = isObject(ankiConnect.fields) ? ankiConnect.fields : {};
|
||||||
const metadata = isObject(ankiConnect.metadata) ? ankiConnect.metadata : {};
|
const metadata = isObject(ankiConnect.metadata) ? ankiConnect.metadata : {};
|
||||||
|
|
||||||
|
applyAnkiBaseResolution(context, ankiConnect);
|
||||||
applyModernFieldsResolution(context, fields);
|
applyModernFieldsResolution(context, fields);
|
||||||
applyModernMediaResolution(context, media);
|
applyModernMediaResolution(context, media);
|
||||||
applyModernBehaviorResolution(context, behavior);
|
applyModernBehaviorResolution(context, behavior);
|
||||||
|
|||||||
@@ -1,21 +1,11 @@
|
|||||||
import { DEFAULT_CONFIG } from '../../definitions';
|
|
||||||
import type { ResolveContext } from '../context';
|
import type { ResolveContext } from '../context';
|
||||||
|
import { applyFieldGroupingConfigResolution } from './field-grouping-config';
|
||||||
|
|
||||||
export function applyAnkiSenrenResolution(context: ResolveContext): void {
|
export function applyAnkiSenrenResolution(
|
||||||
if (
|
context: ResolveContext,
|
||||||
context.resolved.ankiConnect.isSenren.fieldGrouping !== 'auto' &&
|
ankiConnect: Record<string, unknown>,
|
||||||
context.resolved.ankiConnect.isSenren.fieldGrouping !== 'manual' &&
|
): void {
|
||||||
context.resolved.ankiConnect.isSenren.fieldGrouping !== 'disabled'
|
applyFieldGroupingConfigResolution(context, ankiConnect, 'isSenren');
|
||||||
) {
|
|
||||||
context.warn(
|
|
||||||
'ankiConnect.isSenren.fieldGrouping',
|
|
||||||
context.resolved.ankiConnect.isSenren.fieldGrouping,
|
|
||||||
DEFAULT_CONFIG.ankiConnect.isSenren.fieldGrouping,
|
|
||||||
'Expected auto, manual, or disabled.',
|
|
||||||
);
|
|
||||||
context.resolved.ankiConnect.isSenren.fieldGrouping =
|
|
||||||
DEFAULT_CONFIG.ankiConnect.isSenren.fieldGrouping;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Kiku and Senren field grouping write incompatible markup into the same note
|
// Kiku and Senren field grouping write incompatible markup into the same note
|
||||||
// fields, so only one may be active; Kiku wins to preserve pre-existing setups.
|
// fields, so only one may be active; Kiku wins to preserve pre-existing setups.
|
||||||
|
|||||||
Reference in New Issue
Block a user