UNPKG

insta-toc

Version:

Simultaneously generate, update, and maintain a table of contents for your notes in real time.

137 lines (111 loc) 4.57 kB
import { parseYaml as parseYml } from "obsidian"; import { deepMerge } from "../Utils"; import type { HeadingLevel, LocalTocSettings, ParseLocalTocSettingsResult, TocBlockTitle } from "../types"; import { getDefaultLocalSettings, type InstaTocSettings } from "./Settings"; type PartialLocalTocSettings = Partial<LocalTocSettings> & Record<string, unknown>; function isHeadingLevel(value: any): value is HeadingLevel { return [ 1, 2, 3, 4, 5, 6 ].includes(value); } function sanitizeYaml(value: string): string { return value .replace(/(\w+):(\S)/g, "$1: $2") // Add space after colon: "min:1" → "min: 1" .replace( /(?<indent>^[ \t]*)(?<key>[A-Za-z_][A-Za-z0-9_-]*[ \t]*:)[ \t]*(?:(?<value>[^:\n#]+)[ \t]+)?(?<nextKeyWithValue>[A-Za-z_][A-Za-z0-9_-]*[ \t]*:(?:[ \t]*(?:#.*)?|[ \t]+[^#\n]*(?:#.*)?))$/gm, "$<indent>$<key> $<value>\n$<indent>$<nextKeyWithValue>" ); // Newline before each key: "min: 1 max: 6" → "min: 1\nmax: 6" } function parseYaml<T>(yaml: string): T { return parseYml(yaml) as T; } export function normalizeLocalTocSettings(settings: Partial<LocalTocSettings> | null | undefined): LocalTocSettings { return { title: { name: settings?.title?.name ?? null, level: settings?.title?.level ?? null, center: settings?.title?.center ?? null }, exclude: settings?.exclude ?? null, omit: settings?.omit == null ? null : [ ...settings.omit ], levels: { min: settings?.levels?.min ?? null, max: settings?.levels?.max ?? null } }; } export function parseLocalTocSettingsYaml(yaml: string): ParseLocalTocSettingsResult { const defaults = getDefaultLocalSettings(); if (yaml.trim().length === 0) { return { settings: defaults, errors: [] }; } let parsed: PartialLocalTocSettings; try { parsed = parseYaml(sanitizeYaml(yaml)); } catch (error) { return { settings: defaults, errors: [ `Invalid YAML in insta-toc settings: ${String(error)}` ] }; } const errors: string[] = []; if (parsed.title != null) { if (typeof parsed.title !== "object") { errors.push("'title' must be an object."); } else { const { name, level, center } = parsed.title; if (name != null && typeof name !== "string") { parsed.title.name = String(name); } if (level != null && !isHeadingLevel(level)) { errors.push("'title.level' must be an integer between 1 and 6."); } if (center != null && typeof center !== "boolean") { errors.push("'title.center' must be a boolean."); } } } if (parsed.exclude != null && typeof parsed.exclude !== "string") { parsed.exclude = String(parsed.exclude); } if (parsed.omit != null) { if (!Array.isArray(parsed.omit)) { errors.push("'omit' must be an array of strings."); } else { parsed.omit = parsed.omit.map((item) => item == null ? "" : String(item)); } } if (parsed.levels != null) { if (typeof parsed.levels !== "object") { errors.push("'levels' must be an object."); } else { const { min, max } = parsed.levels; if (min != null && !isHeadingLevel(min)) { errors.push("'levels.min' must be an integer between 1 and 6."); } if (max != null && !isHeadingLevel(max)) { errors.push("'levels.max' must be an integer between 1 and 6."); } if (min != null && max != null && min > max) { errors.push("'levels.min' cannot be greater than 'levels.max'."); } } } if (errors.length > 0) { return { settings: defaults, errors }; } return { settings: normalizeLocalTocSettings(deepMerge<LocalTocSettings>(defaults, parsed, { dedupArrays: true })), errors: [] }; } export function resolveTocTitle( localTocSettings: Pick<LocalTocSettings, "title">, settings: Pick<InstaTocSettings, "tocTitle" | "tocTitleLevel" | "tocTitleCentered"> ): TocBlockTitle | null { const text = localTocSettings.title.name ?? settings.tocTitle; if (text.trim().length === 0) { return null; } return { text, level: localTocSettings.title.level ?? settings.tocTitleLevel, centerOverride: localTocSettings.title.center }; }