import { glob } from 'glob'; import { Config } from './conf'; import { promises as fs } from 'fs'; import { dirname, join as path_join } from 'path'; import { build_env_scope } from './env'; import { process_frontmatter } from '@doc-utils/markdown2html'; import { load_layout, Context, render_template, load_partials } from './template'; import { DateTime } from 'luxon'; import assert = require('assert'); interface BuildState { conf: Config; seen_files: Set; env: Record; partials?: Record; layouts: Record; made_directories: Set; build_time: { iso: string; rfc2822: string; }; } export async function build_docs_project(conf: Config) { const now = DateTime.now(); const state: BuildState = { conf, seen_files: new Set(), env: build_env_scope(conf), layouts: Object.create(null), made_directories: new Set(), build_time: { iso: now.toISO(), rfc2822: now.toRFC2822(), }, } if (conf.input.raw) { await copy_raw_files(state); } if (conf.input.text) { await render_text_file_templates(state); } if (conf.input.markdown) { // } // todo... } async function copy_raw_files(state: BuildState) { const promises: Promise[] = [ ]; const files = await glob(state.conf.input.raw, { absolute: true, cwd: state.conf.input.root, }); for (const in_file of files) { if (state.seen_files.has(in_file)) { continue; } state.seen_files.add(in_file); const out_file = map_input_file_to_output_file(state, in_file); promises.push( fs.copyFile(in_file, await out_file, 0o600) ); } await Promise.all(promises); } async function render_text_file_templates(state: BuildState) { const promises: Promise[] = [ ]; const files = await glob(state.conf.input.text, { absolute: true, cwd: state.conf.input.root, }); if (! state.partials) { state.partials = await load_partials(state.conf); } for (const in_file of files) { if (state.seen_files.has(in_file)) { continue; } state.seen_files.add(in_file); promises.push( render_text_file_template(state, in_file) ); } await Promise.all(promises); } async function render_text_file_template(state: BuildState, in_file: string) { const out_file = map_input_file_to_output_file(state, in_file); const { frontmatter, document } = process_frontmatter(await fs.readFile(in_file, 'utf8')); let layout: string; const layout_file = frontmatter?.layout; if (layout_file) { if (! state.layouts[layout_file]) { state.layouts[layout_file] = await load_layout(state.conf, layout_file); } layout = state.layouts[layout_file]; } const context: Context = { env: state.env, page: frontmatter, build_time: state.build_time, }; const rendered = render_template(document, context, layout, structuredClone(state.partials)); await fs.writeFile(await out_file, rendered, 'utf8'); } async function map_input_file_to_output_file(state: BuildState, in_file: string, remove_exts?: string[], add_ext?: string) { assert(in_file.startsWith(state.conf.input.root), 'input file expected to be inside input root'); let out_file = path_join(state.conf.output.root, in_file.slice(state.conf.input.root.length)); if (remove_exts) { for (const ext of remove_exts) { if (out_file.endsWith(ext)) { out_file = out_file.slice(0, -ext.length); break; } } } if (add_ext) { out_file += add_ext; } const dir = dirname(out_file); if (! state.made_directories.has(dir)) { state.made_directories.add(dir); await fs.mkdir(dir, { mode: 0o700, recursive: true, }); } return out_file; }