From 73c745263d69902c08b609cf527bab6fe43c8430 Mon Sep 17 00:00:00 2001 From: Michael M Slusarz Date: Tue, 18 Nov 2025 18:51:36 -0700 Subject: [PATCH 01/10] global: Refactor data parsing bootstrap code It only took a year, but finally figured out that vite provides a plugin/hook (configResolved()) that allows us to execute code AFTER the configuration has been fully parsed and BEFORE the page transformations start. This is the correct place to force full loading of the various data files, since they need to pre-scan all the files for, e.g., frontmatter data needed to do proper inter-page linking. This allows us to use core javascript async/await functionality, rather than the somewhat hackish previous method of using "import-sync" module. --- .vitepress/config.js | 4 +- lib/data/doveadm.data.js | 2 +- lib/data/event_categories.data.js | 2 +- lib/data/event_reasons.data.js | 2 +- lib/data/lua.data.js | 4 +- lib/data/settings.data.js | 2 +- lib/dovecot_vitepress_init.js | 16 +++ lib/events.js | 2 +- lib/markdown.js | 157 ++++++++++++------------------ lib/utility.js | 22 +++-- package-lock.json | 40 ++------ package.json | 1 - 12 files changed, 109 insertions(+), 145 deletions(-) create mode 100644 lib/dovecot_vitepress_init.js diff --git a/.vitepress/config.js b/.vitepress/config.js index 48541fc75..87e5c5851 100644 --- a/.vitepress/config.js +++ b/.vitepress/config.js @@ -4,6 +4,7 @@ import { pagefindPlugin } from 'vitepress-plugin-pagefind' import { generateSidebar } from 'vitepress-sidebar' import { dovecotMdExtend } from '../lib/markdown.js' import { getExcludes } from '../lib/utility.js' +import dovecotVitepressInit from '../lib/dovecot_vitepress_init.js' const base = '/2.4' const base_url = 'https://doc.dovecot.org' @@ -62,7 +63,8 @@ export default defineConfig({ chunkSizeWarningLimit: 1000, }, plugins: [ - pagefindPlugin() + pagefindPlugin(), + dovecotVitepressInit() ], }, diff --git a/lib/data/doveadm.data.js b/lib/data/doveadm.data.js index 63fd222af..217dd5bc8 100644 --- a/lib/data/doveadm.data.js +++ b/lib/data/doveadm.data.js @@ -154,7 +154,7 @@ async function normalizeDoveadm(doveadm) { export default addWatchPaths({ async load() { return await normalizeDoveadm( - structuredClone(loadData('doveadm').doveadm) + structuredClone((await loadData('doveadm')).doveadm) ) } }) diff --git a/lib/data/event_categories.data.js b/lib/data/event_categories.data.js index 7a72fdbb3..37edcb12a 100644 --- a/lib/data/event_categories.data.js +++ b/lib/data/event_categories.data.js @@ -14,7 +14,7 @@ async function normalizeEventCategories(categories) { export default addWatchPaths({ async load() { return await normalizeEventCategories( - structuredClone(loadData('event_categories').categories) + structuredClone((await loadData('event_categories')).categories) ) } }) diff --git a/lib/data/event_reasons.data.js b/lib/data/event_reasons.data.js index 15f8f2abf..5f3cdb11f 100644 --- a/lib/data/event_reasons.data.js +++ b/lib/data/event_reasons.data.js @@ -14,7 +14,7 @@ async function normalizeEventReasons(reasons) { export default addWatchPaths({ async load() { return await normalizeEventReasons( - structuredClone(loadData('event_reasons').reasons) + structuredClone((await loadData('event_reasons')).reasons) ) } }) diff --git a/lib/data/lua.data.js b/lib/data/lua.data.js index e42f2ee86..3c7697de8 100644 --- a/lib/data/lua.data.js +++ b/lib/data/lua.data.js @@ -31,7 +31,7 @@ async function normalizeLuaFunctions(lua) { /* Merge information from Dovecot settings. */ if (v2.dovecot_setting) { if (!set) { - set = structuredClone(loadData('settings').settings) + set = structuredClone((await loadData('settings')).settings) } if (!v2.type) { @@ -84,7 +84,7 @@ async function normalizeLuaVariables(lua) { export default addWatchPaths({ async load() { - const data = loadData('lua') + const data = await loadData('lua') return { constants: await normalizeLuaConstants(data.lua_constants), diff --git a/lib/data/settings.data.js b/lib/data/settings.data.js index a3c67aa44..06747d758 100644 --- a/lib/data/settings.data.js +++ b/lib/data/settings.data.js @@ -131,7 +131,7 @@ async function normalizeSettings(settings) { export default addWatchPaths({ async load() { return await normalizeSettings( - structuredClone(loadData('settings').settings) + structuredClone((await loadData('settings')).settings) ) } }) diff --git a/lib/dovecot_vitepress_init.js b/lib/dovecot_vitepress_init.js new file mode 100644 index 000000000..8a690dcb2 --- /dev/null +++ b/lib/dovecot_vitepress_init.js @@ -0,0 +1,16 @@ +import { dovecotMdInit } from './markdown.js' + +export default function dovecotVitepressInit() { + return { + name: 'dovecot-vitepress-init', + async configResolved(config) { + console.log('\n✅ Config resolved!') + + /* We need to synchronously initialize markdown, + * since we need to pre-populate various internal + * tables (e.g. links). */ + await dovecotMdInit() + console.log('\n✅ Markdown initialized!') + }, + } +} diff --git a/lib/events.js b/lib/events.js index 20ccf728d..b7f0a42f1 100644 --- a/lib/events.js +++ b/lib/events.js @@ -122,7 +122,7 @@ async function normalizeEvents(events, global_inherits, inherits) { } export async function loadEvents() { - const data = loadData('events') + const data = await loadData('events') return await normalizeEvents( structuredClone(data.events), structuredClone(data.global_inherits), diff --git a/lib/markdown.js b/lib/markdown.js index ac98a2c9d..f36dc408a 100644 --- a/lib/markdown.js +++ b/lib/markdown.js @@ -5,7 +5,56 @@ import path from 'path' import { createMarkdownRenderer } from 'vitepress' import { dovecotSetting, frontmatterIter, loadData } from './utility.js' -export function dovecotMdExtend(md) { +let md_conf = false +export async function dovecotMdInit() { + if (md_conf) { + return md_conf + } + + md_conf = { + doveadm: (await loadData('doveadm')).doveadm, + events: (await loadData('events')).events, + links: await (async () => { + const links = {} + const rewrites = globalThis.VITEPRESS_CONFIG.rewrites.map + + frontmatterIter(Object.keys(rewrites), function (f, data) { + if (!data.dovecotlinks) { + return + } + + for (const [k, v] of Object.entries(data.dovecotlinks)) { + if (links[k]) { + throw new Error("Duplicate Dovecot Link key: " + k) + } + + links[k] = { + url: resolveURL(rewrites[f].substring(0, rewrites[f].lastIndexOf('.')) + '.html') + } + + if ((typeof v) == 'object') { + links[k].text = v.text + if (v.hash) { + links[k].url += '#' + v.hash + } + } else { + links[k].text = v + } + } + }) + + return { + ...links, ...((await loadData('links_overrides')).links_overrides) + } + })(), + settings: (await loadData('settings')).settings, + updates: (await loadData('updates')).updates + } + + return md_conf +} + +export async function dovecotMdExtend(md) { md.use(containerPlugin, 'todo', { render: function(tokens, idx) { if (tokens[idx].nesting === 1) { @@ -16,7 +65,7 @@ export function dovecotMdExtend(md) { } }) md.use(deflistPlugin) - md.use(dovecot_markdown) + md.use(dovecot_markdown, await dovecotMdInit()) return md } @@ -26,7 +75,7 @@ export async function getVitepressMd() { if (vitepress_md === null) { const config = globalThis.VITEPRESS_CONFIG - vitepress_md = dovecotMdExtend(await createMarkdownRenderer( + vitepress_md = await dovecotMdExtend(await createMarkdownRenderer( config.srcDir, config.markdown, config.site.base, @@ -40,7 +89,7 @@ export async function getVitepressMd() { /* This is a dovecot markdown extension to support the "[[...]]" syntax. * Much of this is copied from existing markdown-it plugins. See, e.g., * https://github.com/markdown-it/markdown-it-sub/blob/master/index.mjs */ -function dovecot_markdown(md) { +function dovecot_markdown(md, opts) { function process_brackets(state, silent) { const max = state.posMax const start = state.pos @@ -130,8 +179,6 @@ function dovecot_markdown(md) { let page = mode switch (mode) { case 'doveadm': - initDoveadm() - if (!opts.doveadm[env.inner]) { if (!Object.values(opts.doveadm).find((x) => (x.man == 'doveadm-' + env.inner))) { handle_error('doveadm link missing: ' + env.inner) @@ -141,8 +188,6 @@ function dovecot_markdown(md) { break case 'event': - initEvents() - if (!opts.events[env.inner]) { handle_error('event link missing: ' + env.inner) return '' @@ -152,8 +197,6 @@ function dovecot_markdown(md) { case 'setting': case 'setting_text': - initSettings() - /* Settings names can have brackets, so we need to unescape * input for purposes of searching settings keys. */ const search_str = env.inner.replaceAll('>', '>') @@ -175,14 +218,12 @@ function dovecot_markdown(md) { let url = '#' env.inner = false - initDovecotLinks() - - if (!opts.dovecotlinks[parts[1]]) { + if (!opts.links[parts[1]]) { handle_error('Dovecot link missing: ' + parts[1]) return '' } - const d = opts.dovecotlinks[parts[1]] + const d = opts.links[parts[1]] env.inner = parts[2] ? parts[2] : (d.text ? d.text : false) return '' @@ -287,8 +328,6 @@ function dovecot_markdown(md) { case 'changed': case 'deprecated': case 'removed': - initUpdates() - if (!opts.updates[env.args]) { handle_error('Missing updates entry for: ' + env.args) return env.args @@ -369,56 +408,6 @@ function dovecot_markdown(md) { console.error(msg) } - function initDoveadm() { - if (!opts.doveadm) { - opts.doveadm = loadData('doveadm').doveadm - } - } - - function initDovecotLinks() { - if (opts.dovecotlinks) { - return - } - - const links = {} - const rewrites = globalThis.VITEPRESS_CONFIG.rewrites.map - - frontmatterIter(Object.keys(rewrites), function (f, data) { - if (!data.dovecotlinks) { - return - } - - for (const [k, v] of Object.entries(data.dovecotlinks)) { - if (links[k]) { - throw new Error("Duplicate Dovecot Link key: " + k) - } - - links[k] = { - url: resolveURL(rewrites[f].substring(0, rewrites[f].lastIndexOf('.')) + '.html') - } - - if ((typeof v) == 'object') { - links[k].text = v.text - if (v.hash) { - links[k].url += '#' + v.hash - } - } else { - links[k].text = v - } - } - }) - - opts.dovecotlinks = { - ...links, ...(loadData('links_overrides').links_overrides) - } - } - - function initEvents() { - if (!opts.events) { - opts.events = loadData('events').events - } - } - function initManFiles() { if (!opts.man) { opts.man = dovecotSetting('man_paths').flatMap((x) => { @@ -444,37 +433,17 @@ function dovecot_markdown(md) { } } - function initSettings() { - if (!opts.settings) { - opts.settings = loadData('settings').settings - } - } - - function initUpdates() { - if (!opts.updates) { - opts.updates = loadData('updates').updates - } - } - - function resolveURL(url) { - if (!('url_rewrite' in opts)) { - opts.url_rewrite = dovecotSetting('url_rewrite') - } - - const new_url = - (opts.base.endsWith('/') ? opts.base.slice(0, -1) : opts.base) + - '/' + url - return (opts.url_rewrite) ? opts.url_rewrite(new_url) : new_url - } - - const opts = { - base: globalThis.VITEPRESS_CONFIG.site.base - } - md.inline.ruler.after('emphasis', 'dovecot_brackets', process_brackets) md.renderer.rules.dovecot_open = dovecot_open md.renderer.rules.dovecot_body = dovecot_body md.renderer.rules.dovecot_close = dovecot_close +} - opts.resolveURL = resolveURL +export function resolveURL(url) { + const base = globalThis.VITEPRESS_CONFIG.site.base + const url_rewrite = dovecotSetting('url_rewrite') + const new_url = + (base.endsWith('/') ? base.slice(0, -1) : base) + + '/' + url + return (url_rewrite) ? url_rewrite(new_url) : new_url } diff --git a/lib/utility.js b/lib/utility.js index 07352df8f..d5543b8d9 100644 --- a/lib/utility.js +++ b/lib/utility.js @@ -3,7 +3,6 @@ import fg from 'fast-glob' import fs from 'fs' import matter from 'gray-matter' -import importSync from 'import-sync' import { dirname } from 'path' import { fileURLToPath } from 'url' @@ -27,16 +26,21 @@ export function normalizeArrayData(data, keys) { return data } -export function loadData(id) { - const path = globalThis.VITEPRESS_CONFIG.userConfig.themeConfig.dovecot?.data_paths?.[id] - ?? ('../data/' + id + '.js') +const dataOb = {} +export async function loadData(id) { + if (!dataOb[id]) { + const path = globalThis.VITEPRESS_CONFIG.userConfig.themeConfig.dovecot?.data_paths?.[id] + ?? ('../data/' + id + '.js') - try { - return importSync(__dirname + '/' + path) - } catch (e) { - throw new Error('Unable to import module (' + __dirname + '/' + - path + '):' + e) + try { + dataOb[id] = await import(__dirname + '/' + path) + } catch (e) { + throw new Error('Unable to import module (' + __dirname + '/' + + path + '):' + e) + } } + + return dataOb[id] } function _dovecotSetting(name, setting) { diff --git a/package-lock.json b/package-lock.json index dc9e419a9..23d8f509d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,6 @@ "dayjs": "^1.11.13", "fast-glob": "^3.3.3", "git-commit-info": "^2.0.2", - "import-sync": "^2.2.3", "markdown-it-container": "^4.0.0", "markdown-it-deflist": "^3.0.0", "markdown-it-mathjax3": "^4.3.2", @@ -176,6 +175,7 @@ "integrity": "sha512-KL1zWTzrlN4MSiaK1ea560iCA/UewMbS4ZsLQRPoDTWyrbDKVbztkPwwv764LAqgXk0fvkNZvJ3IelcK7DqhjQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@algolia/client-common": "5.20.0", "@algolia/requester-browser-xhr": "5.20.0", @@ -765,16 +765,6 @@ "node": ">=12" } }, - "node_modules/@httptoolkit/esm": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@httptoolkit/esm/-/esm-3.3.1.tgz", - "integrity": "sha512-XvWsT5qskZQoiHgg0kEoIonB+Zj/0T/W0rosjzyPuY++iBwO5c9fMfgvPBCffwY3cTrTD4KYpTPUEtLD0I1lmQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/@iconify-json/simple-icons": { "version": "1.2.21", "resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.21.tgz", @@ -1707,6 +1697,7 @@ "integrity": "sha512-groO71Fvi5SWpxjI9Ia+chy0QBwT61mg6yxJV27f5YFf+Mw+STT75K6SHySpP8Co5LsCrtsbCH5dJZSRtkSKaQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@algolia/client-abtesting": "5.20.0", "@algolia/client-analytics": "5.20.0", @@ -2548,6 +2539,7 @@ "integrity": "sha512-xx560wGBk7seZ6y933idtjJQc1l+ck+pI3sKvhKozdBV1dRZoKhkW5xoCaFv9tQiX5RH1xfSxjuNu6g+lmN/gw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "tabbable": "^6.2.0" } @@ -2617,18 +2609,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/fuse.js": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.0.0.tgz", - "integrity": "sha512-14F4hBIxqKvD4Zz/XjDc3y94mNZN6pRv3U13Udo0lNLCWRBUsrMv2xwcF/y/Z5sV6+FQW+/ow68cHpm4sunt8Q==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "peer": true, - "engines": { - "node": ">=10" - } - }, "node_modules/get-intrinsic": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", @@ -3070,16 +3050,6 @@ "node": ">=8.12.0" } }, - "node_modules/import-sync": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/import-sync/-/import-sync-2.2.3.tgz", - "integrity": "sha512-ZnF84+eGjetsXwYEuFZADO4eCYr1ngBo6UK476Oq4q6dkiDM1TN+6D5iQ5/e3erCyjo7O6xT3xHE6xdtCgDYhw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@httptoolkit/esm": "^3.3.1" - } - }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -3551,6 +3521,7 @@ "integrity": "sha512-TX3GW5NjmupgFtMJGRauioMbbkGsOXAAt1DZ/rzzYmTHqzkO1rNAdiMD4NiruurToPApn2kYy76x02QN26qr2w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "juice": "^8.0.0", "mathjax-full": "^3.2.0" @@ -4496,6 +4467,7 @@ "integrity": "sha512-8KPLGT5g9s+olKMRTU9LFekLizkVIu9tes90O1/aigJ0T5LmyPqTzGJrETnSw3meSYg58YH7JTzhTTW/3z6VAw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "pagefind": "lib/runner/bin.cjs" }, @@ -5539,6 +5511,7 @@ "integrity": "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -5773,6 +5746,7 @@ "integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.13", "@vue/compiler-sfc": "3.5.13", diff --git a/package.json b/package.json index 4792a0830..297a69541 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,6 @@ "dayjs": "^1.11.13", "fast-glob": "^3.3.3", "git-commit-info": "^2.0.2", - "import-sync": "^2.2.3", "markdown-it-container": "^4.0.0", "markdown-it-deflist": "^3.0.0", "markdown-it-mathjax3": "^4.3.2", From 8aa034157ef4fa8e182ea97ab7a3cbd9f0ceb5e5 Mon Sep 17 00:00:00 2001 From: Michael M Slusarz Date: Wed, 13 Aug 2025 19:08:11 -0400 Subject: [PATCH 02/10] settings: Abstract data parsing code out into reusable library/module --- lib/data/settings.data.js | 135 +------------------------------------ lib/settings.js | 137 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+), 132 deletions(-) diff --git a/lib/data/settings.data.js b/lib/data/settings.data.js index 06747d758..ad7adf2a9 100644 --- a/lib/data/settings.data.js +++ b/lib/data/settings.data.js @@ -1,137 +1,8 @@ -import { addWatchPaths, loadData, normalizeArrayData } from '../utility.js' -import { getVitepressMd } from '../markdown.js' - -function wrapInTag(str, tag) { - if (tag) - return `<${tag}>${str}` - return str -} - -/* Resolve links in given parameter. If no singular link is detected, it is - * rendered with the provided tag surrounding the value. */ -function normalizeString(md, str, tag = null) { - let out = '' - - if (str) { - /* FIXME: This makes the following .startsWith() call work, but might - * lead to type-specific errors, e.g. String({}) yields - * '[object Object]'. This still needs to be verified manually. */ - out = String(str) - if (!out.startsWith('[[')) { - out = wrapInTag(out, tag) - } - return md.renderInline(out) - } - - return str -} - -/* Mark a plain item as an inter-settings dovecot-specific link, i.e. - * [[setting,]]. Don't process already marked links. */ -function normalizeArray(md, arr) { - if (arr) { - return arr.map(entry => ( - md.renderInline( - entry.startsWith('[[') - ? entry - : `[[setting,${entry}]]` - ) - )) - } - - return arr -} - -async function normalizeSettings(settings) { - const data = normalizeArrayData( - settings, - ['dependencies', 'seealso', 'tags', 'values_enum'] - ) - - const md = await getVitepressMd() - - for (const [k, v] of Object.entries(data)) { - if (!v) { - delete data[k] - continue - } - - /* Style default entry. */ - if (!!v.default) { - if (['string', 'number'].includes(typeof v.default) || - v.default instanceof String) - v.default = normalizeString(md, v.default, 'code') - else { - let out = normalizeString(md, v.default.value ?? '', 'code') - if (out.length > 0) - out += '
' - if (!!v.default.text) - out += `${normalizeString(md, v.default.text ?? '')}` - v.default = out - } - } - - /* Add list of dependencies. */ - v.dependencies = normalizeArray(md, v.dependencies) - - /* Add markdown to seealso settings. */ - v.seealso = normalizeArray(md, v.seealso) - - /* Plugin. */ - if (v.plugin) { - v.plugin = [ v.plugin ].flat() - v.plugin_link = v.plugin.map((x) => - md.renderInline('[[plugin,' + x + ']]') - ).join(', ') - } - - /* There can be multiple value entries. */ - if (!Array.isArray(v.values)) { - v.values = [ v.values ] - } - - for (const v2 of v.values) { - if (!v2) { - throw new Error("Incorrect value type for " + k) - } - - if (v2.default_required && (v.default === undefined)) { - throw new Error("Default value missing for " + k) - } - if (v2.enum_required && !v.values_enum) { - throw new Error("Enum array missing for " + k) - } - - v2.url = md.renderInline(v2.url) - - if (v2.no_default) { - v.no_default = true - } - } - - for (const k2 of ['added', 'changed', 'deprecated', 'removed']) { - if (v[k2]) { - const changes = [] - for (const[k3, v3] of Object.entries(v[k2])) { - changes.push({ - text: v3 ? md.render(v3.trim()) : null, - version: md.renderInline('[[' + k2 + ',' + k3 + ']]') - }) - } - v[k2] = changes - } - } - - v.text = md.render(v.text.trim()) - } - - return data -} +import { addWatchPaths } from '../utility.js' +import { loadSettings } from '../settings.js' export default addWatchPaths({ async load() { - return await normalizeSettings( - structuredClone((await loadData('settings')).settings) - ) + return await loadSettings() } }) diff --git a/lib/settings.js b/lib/settings.js index 27a5ff65c..1afe3042f 100644 --- a/lib/settings.js +++ b/lib/settings.js @@ -1,3 +1,6 @@ +import { getVitepressMd } from './markdown.js' +import { loadData, normalizeArrayData } from './utility.js' + /* List of Dovecot settings value types. */ export const setting_types = { BOOLEAN: { @@ -91,3 +94,137 @@ export const setting_types = { url: '[[link,settings_groups_includes]]' } } + +function wrapInTag(str, tag) { + if (tag) + return `<${tag}>${str}` + return str +} + +/* Resolve links in given parameter. If no singular link is detected, it is + * rendered with the provided tag surrounding the value. */ +function normalizeString(md, str, tag = null) { + let out = '' + + if (str) { + /* FIXME: This makes the following .startsWith() call work, + * but might lead to type-specific errors, e.g. String({}) + * yields '[object Object]'. This still needs to be verified + * manually. */ + out = String(str) + if (!out.startsWith('[[')) { + out = wrapInTag(out, tag) + } + return md.renderInline(out) + } + + return str +} + +/* Mark a plain item as an inter-settings dovecot-specific link, i.e. + * [[setting,]]. Don't process already marked links. */ +function normalizeArray(md, arr) { + if (arr) { + return arr.map(entry => ( + md.renderInline( + entry.startsWith('[[') + ? entry + : `[[setting,${entry}]]` + ) + )) + } + + return arr +} + +async function normalizeSettings(settings) { + const data = normalizeArrayData( + settings, + ['dependencies', 'seealso', 'tags', 'values_enum'] + ) + + const md = await getVitepressMd() + + for (const [k, v] of Object.entries(data)) { + if (!v) { + delete data[k] + continue + } + + /* Style default entry. */ + if (!!v.default) { + if (['string', 'number'].includes(typeof v.default) || + v.default instanceof String) + v.default = normalizeString(md, v.default, 'code') + else { + let out = normalizeString(md, v.default.value ?? '', 'code') + if (out.length > 0) + out += '
' + if (!!v.default.text) + out += `${normalizeString(md, v.default.text ?? '')}` + v.default = out + } + } + + /* Add list of dependencies. */ + v.dependencies = normalizeArray(md, v.dependencies) + + /* Add markdown to seealso settings. */ + v.seealso = normalizeArray(md, v.seealso) + + /* Plugin. */ + if (v.plugin) { + v.plugin = [ v.plugin ].flat() + v.plugin_link = v.plugin.map((x) => + md.renderInline('[[plugin,' + x + ']]') + ).join(', ') + } + + /* There can be multiple value entries. */ + if (!Array.isArray(v.values)) { + v.values = [ v.values ] + } + + for (const v2 of v.values) { + if (!v2) { + throw new Error("Incorrect value type for " + k) + } + + if (v2.default_required && (v.default === undefined)) { + throw new Error("Default value missing for " + k) + } + if (v2.enum_required && !v.values_enum) { + throw new Error("Enum array missing for " + k) + } + + v2.url = md.renderInline(v2.url) + + if (v2.no_default) { + v.no_default = true + } + } + + for (const k2 of ['added', 'changed', 'deprecated', 'removed']) { + if (v[k2]) { + const changes = [] + for (const[k3, v3] of Object.entries(v[k2])) { + changes.push({ + text: v3 ? md.render(v3.trim()) : null, + version: md.renderInline('[[' + k2 + ',' + k3 + ']]') + }) + } + v[k2] = changes + } + } + + v.text = md.render(v.text.trim()) + } + + return data +} + +export async function loadSettings() { + return await normalizeSettings( + structuredClone((await loadData('settings')).settings) + ) +} From 04251fec006c8ddecb1f1a6be241e54c69fee2eb Mon Sep 17 00:00:00 2001 From: Michael M Slusarz Date: Wed, 13 Aug 2025 19:06:14 -0400 Subject: [PATCH 03/10] lua: Abstract data parsing code out into reusable library/module --- lib/data/lua.data.js | 93 ++------------------------------------------ lib/lua.js | 89 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 90 deletions(-) create mode 100644 lib/lua.js diff --git a/lib/data/lua.data.js b/lib/data/lua.data.js index 3c7697de8..24b7f2ef0 100644 --- a/lib/data/lua.data.js +++ b/lib/data/lua.data.js @@ -1,95 +1,8 @@ -import { getVitepressMd } from '../markdown.js' -import { addWatchPaths, loadData } from '../utility.js' - -async function normalizeLuaConstants(lua) { - const md = await getVitepressMd() - const out = {} - - for (const v of lua.values()) { - if (v.text) { - v.text = md.render(v.text) - } - - for (const tag of v.tags) { - const v2 = structuredClone(v) - v2.tags = tag - out[tag + '.' + v.name] = v2 - } - } - - return out -} - -async function normalizeLuaFunctions(lua) { - const md = await getVitepressMd() - let set = false - const out = {} - - for (const v of lua.values()) { - if (v.args) { - for (const [k2, v2] of Object.entries(v.args)) { - /* Merge information from Dovecot settings. */ - if (v2.dovecot_setting) { - if (!set) { - set = structuredClone((await loadData('settings')).settings) - } - - if (!v2.type) { - v2.type = set[v2.dovecot_setting].values?.label - } - - if (!v2.text) { - v2.text = set[v2.dovecot_setting].text.trim() - } - - if (v2.default === undefined) { - v2.default = set[v2.dovecot_setting].default - } - } - - v2.text = md.render(v2.text) - } - } - - v.text = md.render(v.text) - - for (const tag of v.tags) { - const v2 = structuredClone(v) - v2.tags = tag - out[tag + '.' + v.name] = v2 - } - } - - return out -} - -async function normalizeLuaVariables(lua) { - const md = await getVitepressMd() - const out = {} - - for (const v of lua.values()) { - if (v.text) { - v.text = md.render(v.text) - } - - for (const tag of v.tags) { - const v2 = structuredClone(v) - v2.tags = tag - out[tag + '.' + v.name] = v2 - } - } - - return out -} +import { addWatchPaths } from '../utility.js' +import { loadLua } from '../lua.js' export default addWatchPaths({ async load() { - const data = await loadData('lua') - - return { - constants: await normalizeLuaConstants(data.lua_constants), - functions: await normalizeLuaFunctions(data.lua_functions), - variables: await normalizeLuaVariables(data.lua_variables) - } + return await loadLua() } }) diff --git a/lib/lua.js b/lib/lua.js new file mode 100644 index 000000000..a81f6d2c9 --- /dev/null +++ b/lib/lua.js @@ -0,0 +1,89 @@ +import { getVitepressMd } from './markdown.js' +import { loadData } from './utility.js' + +async function normalizeLuaConstants(lua) { + const md = await getVitepressMd() + const out = {} + + for (const v of lua.values()) { + if (v.text) { + v.text = md.render(v.text) + } + + for (const tag of v.tags) { + const v2 = structuredClone(v) + v2.tags = tag + out[tag + '.' + v.name] = v2 + } + } + + return out +} + +async function normalizeLuaFunctions(lua) { + const set = structuredClone((await loadData('settings')).settings) + const md = await getVitepressMd() + const out = {} + + for (const v of lua.values()) { + if (v.args) { + for (const [k2, v2] of Object.entries(v.args)) { + /* Merge information from Dovecot settings. */ + if (v2.dovecot_setting) { + if (!v2.type) { + v2.type = set[v2.dovecot_setting].values?.label + } + + if (!v2.text) { + v2.text = set[v2.dovecot_setting].text.trim() + } + + if (v2.default === undefined) { + v2.default = set[v2.dovecot_setting].default + } + } + + v2.text = md.render(v2.text) + } + } + + v.text = md.render(v.text) + + for (const tag of v.tags) { + const v2 = structuredClone(v) + v2.tags = tag + out[tag + '.' + v.name] = v2 + } + } + + return out +} + +async function normalizeLuaVariables(lua) { + const md = await getVitepressMd() + const out = {} + + for (const v of lua.values()) { + if (v.text) { + v.text = md.render(v.text) + } + + for (const tag of v.tags) { + const v2 = structuredClone(v) + v2.tags = tag + out[tag + '.' + v.name] = v2 + } + } + + return out +} + +export async function loadLua() { + const data = await loadData('lua') + + return { + constants: await normalizeLuaConstants(data.lua_constants), + functions: await normalizeLuaFunctions(data.lua_functions), + variables: await normalizeLuaVariables(data.lua_variables) + } +} From 6014fa1863c275df4a24283225ceb1f9117f010f Mon Sep 17 00:00:00 2001 From: Michael M Slusarz Date: Wed, 13 Aug 2025 19:04:11 -0400 Subject: [PATCH 04/10] event_reasons: Abstract data parsing code out into reusable library/module --- lib/data/event_reasons.data.js | 18 +++--------------- lib/event_reasons.js | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 15 deletions(-) create mode 100644 lib/event_reasons.js diff --git a/lib/data/event_reasons.data.js b/lib/data/event_reasons.data.js index 5f3cdb11f..2ab1c327b 100644 --- a/lib/data/event_reasons.data.js +++ b/lib/data/event_reasons.data.js @@ -1,20 +1,8 @@ -import { getVitepressMd } from '../markdown.js' -import { addWatchPaths, loadData } from '../utility.js' - -async function normalizeEventReasons(reasons) { - const md = await getVitepressMd() - - for (const [k, v] of Object.entries(reasons)) { - v.description = md.renderInline(v.description) - } - - return reasons -} +import { addWatchPaths } from '../utility.js' +import { loadEventReasons } from '../event_reasons.js' export default addWatchPaths({ async load() { - return await normalizeEventReasons( - structuredClone((await loadData('event_reasons')).reasons) - ) + return await loadEventReasons() } }) diff --git a/lib/event_reasons.js b/lib/event_reasons.js new file mode 100644 index 000000000..f195b567f --- /dev/null +++ b/lib/event_reasons.js @@ -0,0 +1,18 @@ +import { getVitepressMd } from './markdown.js' +import { loadData } from './utility.js' + +async function normalizeEventReasons(reasons) { + const md = await getVitepressMd() + + for (const [k, v] of Object.entries(reasons)) { + v.description = md.renderInline(v.description) + } + + return reasons +} + +export async function loadEventReasons() { + return await normalizeEventReasons( + structuredClone((await loadData('event_reasons')).reasons) + ) +} From b9c5300514cf772fd9838ffbc28bd86e63db1556 Mon Sep 17 00:00:00 2001 From: Michael M Slusarz Date: Wed, 13 Aug 2025 19:02:27 -0400 Subject: [PATCH 05/10] event_categories: Abstract data parsing code out into reusable library/module --- lib/data/event_categories.data.js | 18 +++--------------- lib/event_categories.js | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 15 deletions(-) create mode 100644 lib/event_categories.js diff --git a/lib/data/event_categories.data.js b/lib/data/event_categories.data.js index 37edcb12a..c543f006f 100644 --- a/lib/data/event_categories.data.js +++ b/lib/data/event_categories.data.js @@ -1,20 +1,8 @@ -import { getVitepressMd } from '../markdown.js' -import { addWatchPaths, loadData } from '../utility.js' - -async function normalizeEventCategories(categories) { - const md = await getVitepressMd() - - for (const [k, v] of Object.entries(categories)) { - v.description = md.renderInline(v.description) - } - - return categories -} +import { loadEventCategories } from '../event_categories.js' +import { addWatchPaths } from '../utility.js' export default addWatchPaths({ async load() { - return await normalizeEventCategories( - structuredClone((await loadData('event_categories')).categories) - ) + return await loadEventCategories() } }) diff --git a/lib/event_categories.js b/lib/event_categories.js new file mode 100644 index 000000000..04a1686a6 --- /dev/null +++ b/lib/event_categories.js @@ -0,0 +1,18 @@ +import { getVitepressMd } from './markdown.js' +import { loadData } from './utility.js' + +async function normalizeEventCategories(categories) { + const md = await getVitepressMd() + + for (const [k, v] of Object.entries(categories)) { + v.description = md.renderInline(v.description) + } + + return categories +} + +export async function loadEventCategories() { + return await normalizeEventCategories( + structuredClone((await loadData('event_categories')).categories) + ) +} From bef3b66e3190816b4b86805656009c1a36e38f51 Mon Sep 17 00:00:00 2001 From: Michael M Slusarz Date: Wed, 13 Aug 2025 18:59:46 -0400 Subject: [PATCH 06/10] doveadm: Abstract data parsing code out into reusable library/module --- lib/data/doveadm.data.js | 158 +------------------------------------- lib/doveadm.js | 159 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 162 insertions(+), 155 deletions(-) diff --git a/lib/data/doveadm.data.js b/lib/data/doveadm.data.js index 217dd5bc8..326414dcf 100644 --- a/lib/data/doveadm.data.js +++ b/lib/data/doveadm.data.js @@ -1,160 +1,8 @@ -import { doveadm_arg_types, doveadm_flag_types, getDoveadmCmdLine } from '../doveadm.js' -import { getVitepressMd } from '../markdown.js' -import { addWatchPaths, loadData, normalizeArrayData } from '../utility.js' -import camelCase from 'camelcase' -import slugify from '@sindresorhus/slugify' - -const doveadm_userargs = { - 'all-users': { - cli: 'A', - example: false, - type: doveadm_arg_types.BOOL, - text: `Apply operation to all users.` - }, - 'socket-path': { - cli: 'S', - example: '/var/run/dovecot/doveadm-server', - type: doveadm_arg_types.STRING, - text: `Path to doveadm socket.` - }, - user: { - cli: 'u', - example: 'username', - type: doveadm_arg_types.STRING, - text: `UID of user to apply operation to.`, - }, -} - -const doveadm_userfileargs = { - /* Hidden from documentation. - 'trans-flags': { - type: doveadm_arg_types.INTEGER, - text: `Transaction flags.` - }, - */ - 'user-file': { - cli: 'F', - type: doveadm_arg_types.STRING, - text: `A filename. If set, fetch usernames from file. One username per line.` - }, -} - -function typeToString(type) { - switch (type) { - case doveadm_arg_types.ARRAY: - return 'array' - case doveadm_arg_types.BOOL: - return 'boolean' - case doveadm_arg_types.INTEGER: - return 'integer' - case doveadm_arg_types.STRING: - return 'string' - case doveadm_arg_types.SEARCH_QUERY: - return 'search_query' - case doveadm_arg_types.ISTREAM: - return 'istream' - } -} - -function argToHttpParam(arg) { - return arg.split('-').reduce((s, c) => - s + (c.charAt(0).toUpperCase() + c.slice(1))) -} - -async function normalizeDoveadm(doveadm) { - const md = await getVitepressMd() - - for (const [k, v] of Object.entries(doveadm)) { - if (!v) { - delete doveadm[k] - continue - } - - if (v.flags && (v.flags & doveadm_flag_types.USER)) { - v.args = { ...v.args, ...doveadm_userargs } - } - - if (v.flags && (v.flags & doveadm_flag_types.USERFILE)) { - v.args = { ...v.args, ...doveadm_userfileargs } - } - - /* Convert 'man' entry into a markdown link to man page. - * We will add the hash to all URLs for simplicity; for those man - * pages that don't have individual command names, this will just - * be ignored by the browser. */ - if (v.man) { - v.man_link = md.renderInline('[[man,' + v.man + ',' + slugify(k) + ']]') - } - - /* Change entries. */ - for (const k2 of ['added', 'changed', 'deprecated', 'removed']) { - if (v[k2]) { - const changes = [] - for (const[k3, v3] of Object.entries(v[k2])) { - changes.push({ - text: v3 ? md.render(v3.trim()) : null, - version: md.renderInline('[[' + k2 + ',' + k3 + ']]') - }) - } - v[k2] = changes - } - } - - /* Response values. */ - if (v.response) { - if (v.response.text) { - v.response.text = md.render(String(v.response.text)) - } - } else { - delete v['response'] - } - - /* Text Description. */ - if (v.text) { - v.text = md.render(v.text) - } - - /* Cmd line arguments. */ - v.usage = k + (v.args ? ' ' + getDoveadmCmdLine(v.args) : '') - - if (v.args) { - const args = [] - for (const [k2, v2] of Object.entries(v.args)) { - if (!v2.hidden) { - args.push({ - /* Undefined examples will resolve to undefined. */ - cli_only: v2.cli_only, - example: v2.example, - flag: v2.cli ? '-' + v2.cli : (v2.positional ? k2 : '--' + k2), - param: argToHttpParam(k2), - type: typeToString(v2.type), - text: v2.text ? md.render(v2.text) : null - }) - } - } - v.args = args - } - if (!v.args || !v.args.length) { - delete v['args'] - } - - /* HTTP API info. */ - v.http_cmd = camelCase(k) - } - - return { - doveadm: normalizeArrayData( - doveadm, - ['tags'] - ), - http_api_link: md.renderInline('[[link,doveadm_http_api,HTTP API]]') - } -} +import { addWatchPaths } from '../utility.js' +import { loadDoveadm } from '../doveadm.js' export default addWatchPaths({ async load() { - return await normalizeDoveadm( - structuredClone((await loadData('doveadm')).doveadm) - ) + return await loadDoveadm() } }) diff --git a/lib/doveadm.js b/lib/doveadm.js index fadac3b96..50f1ce30d 100644 --- a/lib/doveadm.js +++ b/lib/doveadm.js @@ -1,3 +1,8 @@ +import { getVitepressMd } from './markdown.js' +import { loadData, normalizeArrayData } from './utility.js' +import camelCase from 'camelcase' +import slugify from '@sindresorhus/slugify' + /* List of Doveadm argument value types. */ export const doveadm_arg_types = { ARRAY: 1, @@ -68,3 +73,157 @@ export function getDoveadmCmdLine(args) { return ret.trim() } + +const doveadm_userargs = { + 'all-users': { + cli: 'A', + example: false, + type: doveadm_arg_types.BOOL, + text: `Apply operation to all users.` + }, + 'socket-path': { + cli: 'S', + example: '/var/run/dovecot/doveadm-server', + type: doveadm_arg_types.STRING, + text: `Path to doveadm socket.` + }, + user: { + cli: 'u', + example: 'username', + type: doveadm_arg_types.STRING, + text: `UID of user to apply operation to.`, + }, +} + +const doveadm_userfileargs = { + /* Hidden from documentation. + 'trans-flags': { + type: doveadm_arg_types.INTEGER, + text: `Transaction flags.` + }, + */ + 'user-file': { + cli: 'F', + type: doveadm_arg_types.STRING, + text: `A filename. If set, fetch usernames from file. One username per line.` + }, +} + +function typeToString(type) { + switch (type) { + case doveadm_arg_types.ARRAY: + return 'array' + case doveadm_arg_types.BOOL: + return 'boolean' + case doveadm_arg_types.INTEGER: + return 'integer' + case doveadm_arg_types.STRING: + return 'string' + case doveadm_arg_types.SEARCH_QUERY: + return 'search_query' + case doveadm_arg_types.ISTREAM: + return 'istream' + } +} + +function argToHttpParam(arg) { + return arg.split('-').reduce((s, c) => + s + (c.charAt(0).toUpperCase() + c.slice(1))) +} + +async function normalizeDoveadm(doveadm) { + const md = await getVitepressMd() + + for (const [k, v] of Object.entries(doveadm)) { + if (!v) { + delete doveadm[k] + continue + } + + if (v.flags && (v.flags & doveadm_flag_types.USER)) { + v.args = { ...v.args, ...doveadm_userargs } + } + + if (v.flags && (v.flags & doveadm_flag_types.USERFILE)) { + v.args = { ...v.args, ...doveadm_userfileargs } + } + + /* Convert 'man' entry into a markdown link to man page. + * We will add the hash to all URLs for simplicity; for those + * man pages that don't have individual command names, this + * will just be ignored by the browser. */ + if (v.man) { + v.man_link = md.renderInline('[[man,' + v.man + ',' + slugify(k) + ']]') + } + + /* Change entries. */ + for (const k2 of ['added', 'changed', 'deprecated', 'removed']) { + if (v[k2]) { + const changes = [] + for (const[k3, v3] of Object.entries(v[k2])) { + changes.push({ + text: v3 ? md.render(v3.trim()) : null, + version: md.renderInline('[[' + k2 + ',' + k3 + ']]') + }) + } + v[k2] = changes + } + } + + /* Response values. */ + if (v.response) { + if (v.response.text) { + v.response.text = md.render(String(v.response.text)) + } + } else { + delete v['response'] + } + + /* Text Description. */ + if (v.text) { + v.text = md.render(v.text) + } + + /* Cmd line arguments. */ + v.usage = k + (v.args ? ' ' + getDoveadmCmdLine(v.args) : '') + + if (v.args) { + const args = [] + for (const [k2, v2] of Object.entries(v.args)) { + if (!v2.hidden) { + args.push({ + cli_only: v2.cli_only, + /* Undefined examples will + * resolve to undefined. */ + example: v2.example, + flag: v2.cli ? '-' + v2.cli : (v2.positional ? k2 : '--' + k2), + param: argToHttpParam(k2), + type: typeToString(v2.type), + text: v2.text ? md.render(v2.text) : null + }) + } + } + v.args = args + } + if (!v.args || !v.args.length) { + delete v['args'] + } + + /* HTTP API info. */ + v.http_cmd = camelCase(k) + } + + return { + doveadm: normalizeArrayData( + doveadm, + ['tags'] + ), + http_api_link: md.renderInline('[[link,doveadm_http_api,HTTP API]]') + } +} + +export async function loadDoveadm() { + return await normalizeDoveadm( + structuredClone((await loadData('doveadm')).doveadm) + ) +} From fe13c5d3023d93a53ca8a1dd5e3083b0139f954a Mon Sep 17 00:00:00 2001 From: Michael M Slusarz Date: Wed, 19 Nov 2025 14:33:51 -0700 Subject: [PATCH 07/10] datafiles: Add ability to download raw "data files" used to build documentation Providing static versions of the fully parsed/transformed data files used to build the documentation may be useful, since it allows things like, e.g., parsing the settings or doveadm config files for use in automated coding and tasks. --- .github/actions/spelling/expect.txt | 2 + .gitignore | 1 + docs/core/datafiles.md | 40 ++++++++++++++++++ lib/data/datafiles.data.js | 16 ++++++++ lib/datafiles.js | 64 +++++++++++++++++++++++++++++ lib/dovecot_vitepress_init.js | 31 ++++++++++++-- lib/utility.js | 6 +-- 7 files changed, 154 insertions(+), 6 deletions(-) create mode 100644 docs/core/datafiles.md create mode 100644 lib/data/datafiles.data.js create mode 100644 lib/datafiles.js diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 2ad17e9ee..ad09cdd99 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -119,6 +119,7 @@ cydir cyrusimap datacenter DATAERR +datafiles datalake datastack datastax @@ -541,6 +542,7 @@ noout nopassword nopipelining nordirplus +noreferrer noscheme noselect nosep diff --git a/.gitignore b/.gitignore index 1e5d613ed..3fcc33693 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ lib/data/*.mjs .vitepress/local.js .vitepress/.temp /man/ +public/datafiles/ diff --git a/docs/core/datafiles.md b/docs/core/datafiles.md new file mode 100644 index 000000000..3d66d2088 --- /dev/null +++ b/docs/core/datafiles.md @@ -0,0 +1,40 @@ +--- +layout: doc +title: Raw Data Files +--- + + + +# Raw Data Files + +This page provides download links to the raw data files used to generate the +Dovecot documentation. + +The format and content of each data file is available by looking at the data +file source in GitHub (links for each file below). + +## Data File List + + + + + + + + + + + + + + +
FileSource
+ + {{ file.name }} + + + Source +
diff --git a/lib/data/datafiles.data.js b/lib/data/datafiles.data.js new file mode 100644 index 000000000..222c09eee --- /dev/null +++ b/lib/data/datafiles.data.js @@ -0,0 +1,16 @@ +import { dataFileList } from '../datafiles.js' + +const filedata = [] +for (const d of dataFileList) { + filedata.push({ + download: d.download, + github: d.github, + name: d.name + }) +} + +export default { + load() { + return { files: filedata } + } +} diff --git a/lib/datafiles.js b/lib/datafiles.js new file mode 100644 index 000000000..e73dc61bd --- /dev/null +++ b/lib/datafiles.js @@ -0,0 +1,64 @@ +import path from 'path' +import { lib_dirname } from './utility.js' + +const download_base = 'datafiles' +const github_base = 'https://github.com/dovecot/documentation' + +export const publicDataDir = path.resolve(lib_dirname, `../public/${download_base}`) + +export const dataFileList = [ + createEntry( + 'doveadm.js', + async () => { + const { loadDoveadm } = await import(path.resolve(lib_dirname, './doveadm.js')) + return await loadDoveadm() + } + ), + createEntry( + 'event_categories.js', + async () => { + const { loadEventCategories } = await import(path.resolve(lib_dirname, './event_categories.js')) + return await loadEventCategories() + } + ), + createEntry( + 'event_reasons.js', + async () => { + const { loadEventReasons } = await import(path.resolve(lib_dirname, './event_reasons.js')) + return await loadEventReasons() + } + ), + createEntry( + 'events.js', + async () => { + const { loadEvents } = await import(path.resolve(lib_dirname, './events.js')) + return await loadEvents() + } + ), + createEntry( + 'lua.js', + async () => { + const { loadLua } = await import(path.resolve(lib_dirname, './lua.js')) + return await loadLua() + } + ), + createEntry( + 'settings.js', + async () => { + const { loadSettings } = await import(path.resolve(lib_dirname, './settings.js')) + return await loadSettings() + } + ) +] + +function createEntry(id, func) { + const json = `${path.basename(id, '.js')}.json` + + return { + data: func, + download: `/${download_base}/${json}`, + github: `${github_base}/blob/main/data/${id}`, + json: path.resolve(publicDataDir, json), + name: json + } +} diff --git a/lib/dovecot_vitepress_init.js b/lib/dovecot_vitepress_init.js index 8a690dcb2..41558fe7e 100644 --- a/lib/dovecot_vitepress_init.js +++ b/lib/dovecot_vitepress_init.js @@ -1,16 +1,41 @@ +import { dataFileList, publicDataDir } from './datafiles.js' import { dovecotMdInit } from './markdown.js' +import fs from 'fs' + +let has_run = false export default function dovecotVitepressInit() { return { name: 'dovecot-vitepress-init', async configResolved(config) { - console.log('\n✅ Config resolved!') + /*** Init Dovecot Markdown. ***/ /* We need to synchronously initialize markdown, * since we need to pre-populate various internal * tables (e.g. links). */ await dovecotMdInit() - console.log('\n✅ Markdown initialized!') - }, + console.log('\n✅ Dovecot Markdown initialized.') + + /*** Create static downloadable data files. ***/ + + if (has_run) { + return + } + has_run = true + + /* Clean old data files (if they exist) and prepare directory. */ + fs.rmSync(publicDataDir, { force: true, recursive: true }); + fs.mkdirSync(publicDataDir, { recursive: true }); + console.log(`✅ Data files: Created ${publicDataDir}.`) + + /* Build the data files. */ + for (const d of dataFileList) { + fs.writeFileSync( + d.json, + JSON.stringify(await d.data(), null, 2) + ) + console.log(`✅ Data files: Generated ${d.json}.`) + } + } } } diff --git a/lib/utility.js b/lib/utility.js index d5543b8d9..c8a880506 100644 --- a/lib/utility.js +++ b/lib/utility.js @@ -7,7 +7,7 @@ import { dirname } from 'path' import { fileURLToPath } from 'url' const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); +export const lib_dirname = dirname(__filename); export function normalizeArrayData(data, keys) { for (const [k, v] of Object.entries(data)) { @@ -33,9 +33,9 @@ export async function loadData(id) { ?? ('../data/' + id + '.js') try { - dataOb[id] = await import(__dirname + '/' + path) + dataOb[id] = await import(lib_dirname + '/' + path) } catch (e) { - throw new Error('Unable to import module (' + __dirname + '/' + + throw new Error('Unable to import module (' + lib_dirname + '/' + path + '):' + e) } } From ffe2e150b1de3915af9c2ff5e938b0121dc2c100 Mon Sep 17 00:00:00 2001 From: Michael M Slusarz Date: Thu, 20 Nov 2025 19:08:29 -0700 Subject: [PATCH 08/10] settings: Optimize data chunk There's ~4000 empty keys, so just need to refactor a few locations to ignore missing keys and we can substantially reduce the size of the chunk. --- components/SettingsComponent.vue | 12 ++++----- data/settings.js | 3 +-- lib/settings.js | 15 ++++++++--- package-lock.json | 44 +++++++++++++++++++++++++++----- package.json | 1 + 5 files changed, 57 insertions(+), 18 deletions(-) diff --git a/components/SettingsComponent.vue b/components/SettingsComponent.vue index a5814fc48..e3388cd1e 100644 --- a/components/SettingsComponent.vue +++ b/components/SettingsComponent.vue @@ -22,10 +22,10 @@ const d = Object.fromEntries(Object.entries(data).filter(([k, v]) => /* Filter entries (by plugin or tag). */ ((!props.plugin && !tag) || (props.plugin && - (v.plugin && v.plugin.includes(props.plugin))) || + v.plugin?.includes(props.plugin)) || (tag && tag.find((t) => - (v.plugin && v.plugin.includes(t)) || - (v.tags.includes(t))) + v.plugin?.includes(t) || + v.tags?.includes(t)) )) && /* Apply filter. */ ((filter == 'all') || @@ -88,13 +88,13 @@ const d = Object.fromEntries(Object.entries(data).filter(([k, v]) => - + Allowed Values {{ v }} - + Dependencies
    @@ -102,7 +102,7 @@ const d = Object.fromEntries(Object.entries(data).filter(([k, v]) =>
- + See Also
    diff --git a/data/settings.js b/data/settings.js index cb3377d0f..e9160b2e9 100644 --- a/data/settings.js +++ b/data/settings.js @@ -2131,8 +2131,7 @@ fts_header_includes { fts_header_includes: { plugin: 'fts', values: setting_types.BOOLLIST, - seealso: [ 'fts_header_excludes' ], - text: `` + seealso: [ 'fts_header_excludes' ] }, fts_search_timeout: { diff --git a/lib/settings.js b/lib/settings.js index 1afe3042f..5fe85d66b 100644 --- a/lib/settings.js +++ b/lib/settings.js @@ -1,5 +1,6 @@ import { getVitepressMd } from './markdown.js' import { loadData, normalizeArrayData } from './utility.js' +import cleanDeep from 'clean-deep' /* List of Dovecot settings value types. */ export const setting_types = { @@ -217,14 +218,22 @@ async function normalizeSettings(settings) { } } - v.text = md.render(v.text.trim()) + if (v.text) { + v.text = md.render(v.text.trim()) + } } return data } export async function loadSettings() { - return await normalizeSettings( - structuredClone((await loadData('settings')).settings) + return cleanDeep( + await normalizeSettings( + structuredClone((await loadData('settings')).settings) + ), + // Clean empty arrays and null values. + // Maintain empty string (""), since it is used for + // certain default settings. + { emptyStrings: false } ) } diff --git a/package-lock.json b/package-lock.json index 23d8f509d..1a328562b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "devDependencies": { "@sindresorhus/slugify": "^2.2.1", "camelcase": "^8.0.0", + "clean-deep": "^3.4.0", "commander": "^13.1.0", "dayjs": "^1.11.13", "fast-glob": "^3.3.3", @@ -175,7 +176,6 @@ "integrity": "sha512-KL1zWTzrlN4MSiaK1ea560iCA/UewMbS4ZsLQRPoDTWyrbDKVbztkPwwv764LAqgXk0fvkNZvJ3IelcK7DqhjQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@algolia/client-common": "5.20.0", "@algolia/requester-browser-xhr": "5.20.0", @@ -1697,7 +1697,6 @@ "integrity": "sha512-groO71Fvi5SWpxjI9Ia+chy0QBwT61mg6yxJV27f5YFf+Mw+STT75K6SHySpP8Co5LsCrtsbCH5dJZSRtkSKaQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@algolia/client-abtesting": "5.20.0", "@algolia/client-analytics": "5.20.0", @@ -1997,6 +1996,21 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/clean-deep": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/clean-deep/-/clean-deep-3.4.0.tgz", + "integrity": "sha512-Lo78NV5ItJL/jl+B5w0BycAisaieJGXK1qYi/9m4SjR8zbqmrUtO7Yhro40wEShGmmxs/aJLI/A+jNhdkXK8mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash.isempty": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.transform": "^4.6.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2539,7 +2553,6 @@ "integrity": "sha512-xx560wGBk7seZ6y933idtjJQc1l+ck+pI3sKvhKozdBV1dRZoKhkW5xoCaFv9tQiX5RH1xfSxjuNu6g+lmN/gw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "tabbable": "^6.2.0" } @@ -3466,6 +3479,27 @@ "node": ">=0.10.0" } }, + "node_modules/lodash.isempty": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.isempty/-/lodash.isempty-4.4.0.tgz", + "integrity": "sha512-oKMuF3xEeqDltrGMfDxAPGIVMSSRv8tbRSODbrs4KGsRRLEhrW8N8Rd4DRgB2+621hY8A8XwwrTVhXWpxFvMzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.transform": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.transform/-/lodash.transform-4.6.0.tgz", + "integrity": "sha512-LO37ZnhmBVx0GvOU/caQuipEh4GN82TcWv3yHlebGDgOxbxiwwzW5Pcx2AcvpIv2WmvmSMoC492yQFNhy/l/UQ==", + "dev": true, + "license": "MIT" + }, "node_modules/longest-streak": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", @@ -3521,7 +3555,6 @@ "integrity": "sha512-TX3GW5NjmupgFtMJGRauioMbbkGsOXAAt1DZ/rzzYmTHqzkO1rNAdiMD4NiruurToPApn2kYy76x02QN26qr2w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "juice": "^8.0.0", "mathjax-full": "^3.2.0" @@ -4467,7 +4500,6 @@ "integrity": "sha512-8KPLGT5g9s+olKMRTU9LFekLizkVIu9tes90O1/aigJ0T5LmyPqTzGJrETnSw3meSYg58YH7JTzhTTW/3z6VAw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "pagefind": "lib/runner/bin.cjs" }, @@ -5511,7 +5543,6 @@ "integrity": "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -5746,7 +5777,6 @@ "integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.13", "@vue/compiler-sfc": "3.5.13", diff --git a/package.json b/package.json index 297a69541..e56a015b6 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "devDependencies": { "@sindresorhus/slugify": "^2.2.1", "camelcase": "^8.0.0", + "clean-deep": "^3.4.0", "commander": "^13.1.0", "dayjs": "^1.11.13", "fast-glob": "^3.3.3", From 402ee47015d0cbb4409134c0e2c0303225229e99 Mon Sep 17 00:00:00 2001 From: Michael M Slusarz Date: Thu, 20 Nov 2025 19:18:57 -0700 Subject: [PATCH 09/10] events: Optimize data chunk --- components/EventsComponent.vue | 2 +- data/events.js | 2 +- lib/events.js | 15 ++++++++++----- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/components/EventsComponent.vue b/components/EventsComponent.vue index a383622e0..ec3d40add 100644 --- a/components/EventsComponent.vue +++ b/components/EventsComponent.vue @@ -14,7 +14,7 @@ const d = Object.fromEntries(Object.entries(data).filter(([k, v]) => (v.root && v.root == props.root)) || (props.tag && ((v.root && v.root == props.tag) || - (v.tags.includes(props.tag))))) + (v.tags?.includes(props.tag))))) ).sort()) diff --git a/data/events.js b/data/events.js index f99910ec1..eb4ea9a79 100644 --- a/data/events.js +++ b/data/events.js @@ -29,7 +29,7 @@ export const events = { // Event(s) this field inherits fields from. // Can be a single event or an array of events. - inherit: '', + // inherit: '', // List of fields emitted. Keys are field names, values are // descriptions (string) or an object with multiple keys (see below for diff --git a/lib/events.js b/lib/events.js index b7f0a42f1..f6f47f619 100644 --- a/lib/events.js +++ b/lib/events.js @@ -1,5 +1,6 @@ import { loadData, normalizeArrayData } from './utility.js' import { getVitepressMd } from './markdown.js' +import cleanDeep from 'clean-deep' /* Take the events list and normalize entries and process inheritance. */ function processInherit(i, ob) { @@ -115,7 +116,9 @@ async function normalizeEvents(events, global_inherits, inherits) { v.fields = fields /* Prefix the list of changes to the event's description. */ - v.text = v.text ? md.render(`${appendListOfUpdates(v)}\n${v.text}`) : null + if (v.text) { + v.text = md.render(`${appendListOfUpdates(v)}\n${v.text}`) + } } return events @@ -123,9 +126,11 @@ async function normalizeEvents(events, global_inherits, inherits) { export async function loadEvents() { const data = await loadData('events') - return await normalizeEvents( - structuredClone(data.events), - structuredClone(data.global_inherits), - structuredClone(data.inherits) + return cleanDeep( + await normalizeEvents( + structuredClone(data.events), + structuredClone(data.global_inherits), + structuredClone(data.inherits) + ) ) } From 1e82e00c33102d6de116b4a0c0adb6d253c0c855 Mon Sep 17 00:00:00 2001 From: Michael M Slusarz Date: Thu, 20 Nov 2025 19:30:05 -0700 Subject: [PATCH 10/10] doveadm: Optimize data chunk --- components/DoveadmComponent.vue | 2 +- data/doveadm.js | 2 +- lib/doveadm.js | 7 +++++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/components/DoveadmComponent.vue b/components/DoveadmComponent.vue index 050cec2b9..33c45e9d6 100644 --- a/components/DoveadmComponent.vue +++ b/components/DoveadmComponent.vue @@ -15,7 +15,7 @@ const d = Object.fromEntries(Object.entries(data.doveadm).filter(([k, v]) => (v.plugin && v.plugin == props.plugin)) || (props.tag && ((v.plugin && v.plugin == props.tag) || - (v.tags.includes(props.tag)))) + (v.tags?.includes(props.tag)))) ).sort()) const cliComponent = ref({}) diff --git a/data/doveadm.js b/data/doveadm.js index 79d41d14f..f985117b1 100644 --- a/data/doveadm.js +++ b/data/doveadm.js @@ -24,7 +24,7 @@ export const doveadm = { // If set, will use as command example argument. // i.e., for HTTP API requests, this argument will be added // to the example argument string. - example: false, + // example: "foo", // If true, this is an optional positional argument. // optional: true, diff --git a/lib/doveadm.js b/lib/doveadm.js index 50f1ce30d..e5f11a0e1 100644 --- a/lib/doveadm.js +++ b/lib/doveadm.js @@ -1,6 +1,7 @@ import { getVitepressMd } from './markdown.js' import { loadData, normalizeArrayData } from './utility.js' import camelCase from 'camelcase' +import cleanDeep from 'clean-deep' import slugify from '@sindresorhus/slugify' /* List of Doveadm argument value types. */ @@ -223,7 +224,9 @@ async function normalizeDoveadm(doveadm) { } export async function loadDoveadm() { - return await normalizeDoveadm( - structuredClone((await loadData('doveadm')).doveadm) + return cleanDeep( + await normalizeDoveadm( + structuredClone((await loadData('doveadm')).doveadm) + ) ) }