import YAML from "js-yaml"; import { INDEX_TEMPLATE, SETTINGS_TEMPLATE } from "./PAGE_TEMPLATES.ts"; import type { SpacePrimitives } from "./spaces/space_primitives.ts"; import { cleanupJSON } from "../plug-api/lib/json.ts"; import { type Config, defaultConfig, type DynamicAttributeDefinitionConfig, } from "../type/config.ts"; import type { DataStore, DynamicAttributeDefinitions, ObjectDecorators, } from "$lib/data/datastore.ts"; import { parseExpression } from "$common/expression_parser.ts"; import type { System } from "$lib/plugos/system.ts"; import type { ConfigObject } from "../plugs/index/config.ts"; import { deepObjectMerge } from "@silverbulletmd/silverbullet/lib/json"; const yamlConfigRegex = /^(```+|~~~+)(ya?ml|space-config)\r?\n([\S\s]+?)\1/m; /** * Parses YAML config from a Markdown string. * @param configMarkdown - The Markdown string containing the YAML config. * @returns An object representing the parsed YAML config. */ export function parseYamlConfig(configMarkdown: string): { [key: string]: any; } { const match = yamlConfigRegex.exec(configMarkdown); if (!match) { return {}; } const yaml = match[3]; try { return YAML.load(yaml) as { [key: string]: any; }; } catch (e: any) { console.error("Error parsing SETTINGS as YAML", e.message); return {}; } } /** * Loads space-configs from a system using the `index` plug. * @param system - The system object * @returns A promise that resolves to merged configs from the system. */ async function loadConfigsFromSystem( system: System, ): Promise { if (!system.loadedPlugs.has("index")) { console.warn("Index plug not loaded yet, falling back to default config"); return defaultConfig; } // We'll start witht the default config let fullConfig: any = { ...defaultConfig }; // Then merge in all plug-based configs for (const plugDef of system.loadedPlugs.values()) { if (plugDef.manifest?.config) { const plugConfig = cleanupJSON(plugDef.manifest.config); fullConfig = deepObjectMerge(fullConfig, plugConfig); } } // Query all space-configs const allConfigs: ConfigObject[] = await system.invokeFunction( "index.queryObjects", ["space-config", {}], ); // Now let's intelligently merge them for (const config of allConfigs) { let configObject = { [config.key]: config.value }; configObject = cleanupJSON(configObject); fullConfig = deepObjectMerge(fullConfig, configObject); } // And clean up the JSON (expand .-separated paths, convert dates to strings) return cleanupJSON(fullConfig); } /** * Ensures that the settings and index page exist in the given space. * If they don't exist, default settings and index page will be created. * @param space - The SpacePrimitives object representing the space. * @returns A promise that resolves to the built-in settings. */ export async function ensureAndLoadSettingsAndIndex( space: SpacePrimitives, system?: System, ): Promise { let configText: string | undefined; let config = defaultConfig; try { configText = new TextDecoder().decode( (await space.readFile("SETTINGS.md")).data, ); } catch (e: any) { if (e.message === "Not found") { console.log("No SETTINGS found, creating default SETTINGS"); await space.writeFile( "SETTINGS.md", new TextEncoder().encode(SETTINGS_TEMPLATE), true, ); } else { console.error("Error reading SETTINGS", e.message); console.warn("Falling back to default SETTINGS"); return defaultConfig; } configText = SETTINGS_TEMPLATE; // Ok, then let's also check the index page try { await space.getFileMeta("index.md"); } catch (e: any) { console.log( "No index page found, creating default index page", e.message, ); // This should trigger indexing of the configs too await space.writeFile( "index.md", new TextEncoder().encode(INDEX_TEMPLATE), ); } } if (system) { // If we're NOT in SB_SYNC_ONLY, we can load settings from the index (ideal case) config = await loadConfigsFromSystem(system); } else { // If we are in SB_SYNC_ONLY, this is best effort, and we can only support settings in the SETTINGS.md file config = parseYamlConfig(configText) as Config; config = cleanupJSON(config); config = { ...defaultConfig, ...config }; // console.log("Loaded settings from SETTINGS.md", config); } // Some essential sanity checking if (typeof config.indexPage !== "string") { console.warn("indexPage is not a string, falling back to default"); config.indexPage = "index"; } return config; } export function updateObjectDecorators( config: Config, ds: DataStore, ) { if (config.objectDecorators) { // Reload object decorators const newDecorators: ObjectDecorators[] = []; for ( const decorator of config.objectDecorators ) { try { newDecorators.push({ where: parseExpression(decorator.where), attributes: objectAttributeToExpressions(decorator.attributes), }); } catch (e: any) { console.error( "Error parsing object decorator", decorator, "got error", e.message, ); } } // console.info(`Loaded ${newDecorators.length} object decorators`); ds.objectDecorators = newDecorators; } } function objectAttributeToExpressions( dynamicAttributes: DynamicAttributeDefinitionConfig, ): DynamicAttributeDefinitions { const result: DynamicAttributeDefinitions = {}; for (const [key, value] of Object.entries(dynamicAttributes)) { if (typeof value === "string") { result[key] = parseExpression(value); } else { result[key] = objectAttributeToExpressions(value); } } return result; }