Files
SubMiner/src/config/subtitle-style-css-migration.ts
T
sudacode ff4d38e5be migrate subtitle style config to CSS declaration shape
- Flat style keys (fontFamily, fontSize, hoverTokenColor, etc.) consolidated into subtitleStyle.css, secondary.css, and subtitleSidebar.css objects
- Hover token colors migrated to --subtitle-hover-token-color CSS custom properties
- Plugin app-ping now checks result.status (0=running, 1=stopped) to avoid treating transient failures as stopped
- Note-fields note type picker defaults to configured deck's note type before falling back to Kiku/Lapis
- New migration for legacy ankiConnect N+1 config paths
2026-05-18 03:07:39 -07:00

130 lines
3.6 KiB
TypeScript

import type { RawConfig } from '../types/config';
import type { ConfigSettingsPatchOperation } from '../types/settings';
import {
buildSubtitleCssDeclarationObject,
getSubtitleCssManagedConfigPaths,
getSubtitleCssPath,
type SubtitleCssScope,
} from '../settings/subtitle-style-css';
import { applyConfigSettingsPatchToContent } from './settings/jsonc-edit';
const SUBTITLE_CSS_SCOPES: SubtitleCssScope[] = ['primary', 'secondary', 'sidebar'];
const HEX_COLOR_PATTERN = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
export type LegacySubtitleStyleCssMigrationResult =
| {
migrated: true;
content: string;
rawConfig: RawConfig;
}
| {
migrated: false;
content: string;
rawConfig: RawConfig;
};
function isRecord(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === 'object' && !Array.isArray(value);
}
function getValueAtPath(root: unknown, path: string): unknown {
let current = root;
for (const segment of path.split('.')) {
if (!isRecord(current)) return undefined;
current = current[segment];
}
return current;
}
function hasPath(root: unknown, path: string): boolean {
let current = root;
const segments = path.split('.');
for (const [index, segment] of segments.entries()) {
if (!isRecord(current) || !Object.hasOwn(current, segment)) {
return false;
}
if (index === segments.length - 1) {
return true;
}
current = current[segment];
}
return false;
}
function isMigratableLegacySubtitleCssValue(path: string, value: unknown): boolean {
if (path === 'subtitleStyle.hoverTokenColor') {
return typeof value === 'string' && HEX_COLOR_PATTERN.test(value.trim());
}
if (path === 'subtitleStyle.hoverTokenBackgroundColor') {
return typeof value === 'string';
}
return true;
}
export function buildLegacySubtitleStyleCssMigrationOperations(
rawConfig: RawConfig,
): ConfigSettingsPatchOperation[] {
const operations: ConfigSettingsPatchOperation[] = [];
for (const scope of SUBTITLE_CSS_SCOPES) {
const cssPath = getSubtitleCssPath(scope);
const values: Record<string, unknown> = {
[cssPath]: getValueAtPath(rawConfig, cssPath),
};
const legacyPaths = getSubtitleCssManagedConfigPaths(scope).filter(
(legacyPath) =>
hasPath(rawConfig, legacyPath) &&
isMigratableLegacySubtitleCssValue(legacyPath, getValueAtPath(rawConfig, legacyPath)),
);
if (legacyPaths.length === 0) continue;
for (const legacyPath of legacyPaths) {
values[legacyPath] = getValueAtPath(rawConfig, legacyPath);
}
operations.push({
op: 'set',
path: cssPath,
value: buildSubtitleCssDeclarationObject(scope, values),
});
for (const legacyPath of legacyPaths) {
operations.push({ op: 'reset', path: legacyPath });
}
}
return operations;
}
export function applyLegacySubtitleStyleCssMigrationToContent(options: {
content: string;
rawConfig: RawConfig;
}): LegacySubtitleStyleCssMigrationResult {
const operations = buildLegacySubtitleStyleCssMigrationOperations(options.rawConfig);
if (operations.length === 0) {
return {
migrated: false,
content: options.content,
rawConfig: options.rawConfig,
};
}
const result = applyConfigSettingsPatchToContent({
content: options.content,
operations,
previousWarnings: [],
});
if (!result.ok) {
return {
migrated: false,
content: options.content,
rawConfig: options.rawConfig,
};
}
return {
migrated: true,
content: result.content,
rawConfig: result.rawConfig,
};
}