Changes
12 changed files (+956/-282)
-
-
@@ -34,4 +34,4 @@ run: "deno test --allow-read=. --allow-write=./filesystem_writer/.test"- name: Lint run: "deno lint" - name: Perform type-check run: "deno check docs/build.ts" run: "deno check cli.ts"
-
-
-
@@ -2,11 +2,587 @@ // SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com>// // SPDX-License-Identifier: Apache-2.0 export function run(): number { console.error("Not implemented"); return 1; import * as log from "./deps/deno.land/std/log/mod.ts"; import * as cli from "./deps/deno.land/std/cli/mod.ts"; import * as colors from "./deps/deno.land/std/fmt/colors.ts"; import * as jsonc from "./deps/deno.land/std/jsonc/mod.ts"; import * as path from "./deps/deno.land/std/path/mod.ts"; import { DenoFsReader } from "./filesystem_reader/deno_fs.ts"; import { DenoFsWriter } from "./filesystem_writer/deno_fs.ts"; import { noOverwrite } from "./filesystem_writer/no_overwrite.ts"; import { precompress as precompressMiddleware } from "./filesystem_writer/precompress.ts"; import { DefaultTreeBuilder, fileExtensions, ignoreDotfiles, langDir, removeExtFromMetadata, } from "./tree_builder/default_tree_builder.ts"; import type { ContentParser } from "./content_parser/interface.ts"; import { oneof } from "./content_parser/oneof.ts"; import { ObsidianMarkdownParser } from "./content_parser/obsidian_markdown.ts"; import { JSONCanvasParser } from "./content_parser/json_canvas.ts"; import { DefaultThemeBuilder } from "./page_builder/default_theme/mod.ts"; import * as config from "./cli/config.ts"; function prettyLogFormatter( useColors: boolean, verbose: boolean, ): log.FormatterFunction { function id<T>(x: T): T { return x; } function pipe<T>(a: (x: T) => T, b: (x: T) => T): (x: T) => T { return (x) => b(a(x)); } const ts = useColors ? pipe(colors.black, colors.dim) : id; return (record) => { let level: (str: string) => string = id; let msg: (str: string) => string = id; let payload: (str: string) => string = id; if (useColors) { switch (record.level) { case log.LogLevels.NOTSET: level = colors.dim; msg = colors.dim; payload = colors.dim; break; case log.LogLevels.DEBUG: level = colors.gray; msg = colors.gray; payload = pipe(colors.dim, colors.black); break; case log.LogLevels.INFO: level = colors.blue; msg = pipe(colors.bold, colors.black); payload = colors.black; break; case log.LogLevels.WARN: level = colors.yellow; msg = colors.black; payload = colors.black; break; case log.LogLevels.ERROR: level = colors.red; msg = colors.black; payload = colors.black; break; case log.LogLevels.CRITICAL: level = pipe(colors.red, colors.bold); msg = pipe(colors.bold, colors.red); payload = colors.red; break; } } let ret = colors.reset("\n") + level(record.levelName) + colors.reset(" ") + ts(record.datetime.toISOString()) + colors.reset("\n") + msg(record.msg); if (verbose) { for (const arg of record.args) { const text = Deno.inspect(arg, { colors: useColors, compact: false, }); for (const line of text.split("\n")) { ret += colors.reset("\n ") + payload(line); } } } return ret; }; } function parseColorPreference(value?: string): "auto" | "never" | "always" { if (!value) { return "auto"; } switch (value) { case "auto": case "never": case "always": return value; default: throw new Error(`"${value}" is not a valid color option value`); } } export async function run( args: readonly string[], isStdoutTerminal: boolean, ): Promise<number> { const flags = cli.parseArgs(args.slice(), { string: [ "log", "color", "config", "out", "copyright", "name", "favicon-png", "favicon-svg", "logo-image", "doc-ext", "lang", ], boolean: [ "help", "verbose", "json", "keep-ext", "precompress", "disable-markdown", "disable-jsoncanvas", "markdown-frontmatter", "shortest-path-when-possible", ], alias: { help: ["h"], }, }); const color = parseColorPreference(flags.color); const isColoredStdout = color === "always" || (color === "auto" && isStdoutTerminal); if (flags.help) { console.log(help(isColoredStdout)); return 0; } let level: log.LevelName = "INFO"; if (flags.log) { const upper = flags.log.toUpperCase() as log.LevelName; const found = log.LogLevelNames.includes(upper); if (found) { level = upper; } else { log.critical( `"${flags.log}" is not a valid log level. ` + `Available log levels are: ${log.LogLevelNames.join(", ")}`, ); return 1; } } log.setup({ handlers: { default: new log.ConsoleHandler(level, { formatter: flags.json ? log.jsonFormatter : prettyLogFormatter(isColoredStdout, flags.verbose), useColors: isColoredStdout, }), }, loggers: { default: { level: level, handlers: ["default"], }, macana: { level: "DEBUG", handlers: ["default"], }, }, }); const configFile = flags.config ? parseConfigFile(flags.config) : {}; try { const start = performance.now(); const arg0 = typeof flags._[0] !== "undefined" ? String(flags._[0]) : null; const inputPath = arg0 || configFile.input?.path; if (!inputPath) { throw new Error( "Input path (VAULT_PATH argument, `input.path`) is required", ); } await Deno.permissions.request({ name: "read", path: inputPath, }); log.debug(`Using "${inputPath}" as source directory`, { inputPath, }); const fileSystemReader = new DenoFsReader(inputPath); const outputPath = flags.out || configFile.output?.path; if (!outputPath) { throw new Error("Output path (--out, `output.path` is required"); } await Deno.permissions.request({ name: "write", path: outputPath, }); await Deno.mkdir(outputPath, { recursive: true }); log.debug(`Using "${outputPath}" as output directory`, { outputPath, }); let fileSystemWriter = noOverwrite(new DenoFsWriter(outputPath)); const precompress = flags.precompress || configFile.output?.precompress; if (precompress) { log.debug("output.precompress = true"); fileSystemWriter = precompressMiddleware()(fileSystemWriter); } const defaultLanguage = flags.lang || configFile.documents?.defaultLanguage || "en"; log.debug(`Use "${defaultLanguage}" as default language`, { defaultLanguage, languages: configFile.documents?.languages, }); const keepExtension = flags["keep-ext"] || configFile.documents?.title?.keepExtension; if (keepExtension) { log.debug("documents.title.keepExtension = true"); } const resolveShortestPathWhenPossible = flags["shortest-path-when-possible"] || configFile.documents?.resolveShortestPathWhenPossible; if (resolveShortestPathWhenPossible) { log.debug("documents.resolveShortestPathWhenPossible = true"); } const markdownDisabled = flags["disable-markdown"] || configFile.markdown?.enabled === false; if (markdownDisabled) { log.debug("documents.markdown.enabled = false"); } const yamlFrontmatter = flags["markdown-frontmatter"] || configFile.markdown?.yamlFrontmatter; if (yamlFrontmatter) { log.debug("markdown.yamlFrontmatter = true"); } const jsonCanvasDisabled = flags["disable-jsoncanvas"] || configFile.jsonCanvas?.enabled === false; if (jsonCanvasDisabled) { log.debug("documents.jsonCanvas.enabled = false"); } const parsers: readonly ContentParser[] = [ jsonCanvasDisabled ? null : new JSONCanvasParser(), markdownDisabled ? null : new ObsidianMarkdownParser({ frontmatter: yamlFrontmatter }), ].filter((p): p is NonNullable<typeof p> => !!p); if (parsers.length === 0) { throw new Error( "You can't disable both Markdown and JSONCanvas documents", ); } const treeBuilder = new DefaultTreeBuilder({ defaultLanguage, ignore: [ignoreDotfiles], strategies: [ fileExtensions([ markdownDisabled ? null : ".md", jsonCanvasDisabled ? null : ".canvas", ].filter((s): s is string => !!s)), configFile.documents?.languages ? langDir( Object.fromEntries( Object.entries(configFile.documents.languages).map( ([lang, { title }]) => [lang, title], ), ), ) : null, keepExtension ? null : removeExtFromMetadata(), ].filter((s): s is NonNullable<typeof s> => !!s), resolveShortestPathWhenPossible, }); const contentParser = oneof(...parsers); const siteName = flags.name || configFile.metadata?.name; if (!siteName) { throw new Error("Site name (--name, `metadata.name`) is required"); } log.debug(`metadata.name = ${siteName}`); const copyright = flags.copyright || configFile.metadata?.copyright; if (!copyright) { throw new Error( "Copyright text (--copyright, `metadata.copyright`) is required", ); } log.debug(`metadata.copyright = ${copyright}`); let faviconSvg: Uint8Array | undefined = undefined; const faviconSvgPath = flags["favicon-svg"] || configFile.metadata?.favicon?.svg; if (faviconSvgPath) { log.debug(`Reads favicon SVG from "${faviconSvgPath}"`); faviconSvg = Deno.readFileSync(faviconSvgPath); } let faviconPng: Uint8Array | undefined = undefined; const faviconPngPath = flags["favicon-png"] || configFile.metadata?.favicon?.png; if (faviconPngPath) { log.debug(`Reads favicon PNG from "${faviconPngPath}"`); faviconPng = Deno.readFileSync(faviconPngPath); } let siteLogo: { ext: string; binary: Uint8Array } | undefined = undefined; const siteLogoPath = flags["logo-image"] || configFile.metadata?.logoImage; if (siteLogoPath) { log.debug(`Reads site logo image fron "${siteLogoPath}"`); siteLogo = { ext: path.extname(siteLogoPath), binary: Deno.readFileSync(siteLogoPath), }; } const pageBuilder = new DefaultThemeBuilder({ siteName, copyright, faviconSvg, faviconPng, siteLogo, }); const documentTree = await treeBuilder.build({ fileSystemReader, contentParser, }); await pageBuilder.build({ documentTree, fileSystemReader, fileSystemWriter, }); const duration = performance.now() - start; log.info(`Generated website in ${duration}ms`, { duration, }); return 0; } catch (error) { log.critical(`Build aborted due to an error: ${error}`, { error, }); return 1; } } function help(isColorEnabled: boolean): string { const id = (s: string) => s; const title = isColorEnabled ? (s: string) => colors.underline(colors.bold(s)) : id; const b = isColorEnabled ? colors.bold : id; const p = isColorEnabled ? colors.blue : id; const t = isColorEnabled ? colors.green : id; const v = isColorEnabled ? colors.red : id; return colors.reset(` macana/cli.ts - Generate static website from Obsidian Vault. ${title("Usage")}: deno run --allow-read=<VAULT_PATH>,<CONFIG_PATH> --allow-write=<OUTDIR> macana/cli.ts --config <CONFIG_PATH> deno run --allow-read=<VAULT_PATH> --allow-write=<OUTDIR> macana/cli.ts [OPTIONS] <VAULT_PATH> ${title("Arguments")}: VAULT_PATH Path to the Vault directory. This is required if "--config" option is not present. Corresponding config key is ${p("input.path")} (${t("string")}). ${title("Options")}: -h, --help Print this help text to stdout then exit. --log <debug|info|warn|error|critical> Set the lowest log level to output. [default: info] --json Output logs as JSON Lines. --color <always|never|auto> When to output ANSI escape sequences in the log output. --verbose Output log payload alongside log message. If ${b("--json")} option is set, this flag is always on. --config <PATH> Use config JSON or JSONC file at PATH. CLI options takes precedence over ones defined in the config file. Most of the build options are configurable via both CLI options and config file. Macana parse the file as JSON if the file name ends with ".json" and as JSONC (JSON with Comment) if the file name ends with ".jsonc". Use JSONC if you want to use trailing comma and/or comments. However, due to technical limitation, ${ p("documents.languages") } cannot be set via CLI options. This option is a key-value object, where key is a directory name for the language directory and value is ${t("{ title: string; lang: string; }")}. ${ p("documents.languages[language].title") } is a display title of the directory and ${ p("documents.lang[language].lang") } is a language code that directory indicates. For example, when a Vault has "en/" and you set ${v('{ en: { title: "English", lang: "en-US" }')} to ${ p("documents.lang") }, the directory will be displayed as "English" and it and its content will be shown as "lang=en-US". --out <PATH> Path to the output directory. Macana creates the target directory if it does not exist at the path. Use slash ("/") for path separator regardless of platform. Corresponding config key is ${p("output.path")} (${t("string")}). --copyright <TEXT> Copyright text to display on the generated website. Corresponding config key is ${p("metadata.copyright")} (${t("string")}). --name <TEXT> Name of the generated website. Corresponding config key is ${p("metadata.name")} (${t("string")}). --favicon-png <PATH> Path for PNG favicon image. ${b("The file needs to be inside the VAULT_PATH")}. Corresponding config key is ${p("metadata.favicon.png")} (${t("string")}). --favicon-svg <PATH> Path for SVG favicon image. ${b("The file needs to be inside the VAULT_PATH")}. Corresponding config key is ${p("metadata.favicon.svg")} (${t("string")}). --logo-image <PATH> Image file to use as a logo image. ${b("The file needs to be inside the VAULT_PATH")}. Corresponding config key is ${p("metadata.logoImage")} (${t("string")}). --keep-ext Keep file extension in document title. Corresponding config key is ${p("documents.title.keepExtension")} (${ t("boolean") }). --lang <LANG> Set default language for the website. [default: en] Corresponding config key is ${p("documents.defaultLanguage")} (${ t("string") }). --shortest-path-when-possible Enable "Shortest path when possible" resolution. Corresponding config key is ${ p("documents.resolveShortestPathWhenPossible") } (${t("boolean")}). --precompress Compress .html/.css/.js files using gzip,brotli,zstd and write the compressed files alongside the original files. For example, if the website has "index.html", Macana writes "index.html", "index.html.gz", "index.html.br" and "index.html.zstd". This output format is for useful if your web server supports serving precompressed files. Corresponding config key is ${p("documents.precompress")} (${t("boolean")}). --disable-markdown Disable parsing of Markdown files (.md). To configure this via config file, set ${p("markdown.enabled")} to ${ v("false") }. --disable-jsoncanvas Disable parsing of JSON Canvas files (.canvas). To configure this via config file, set ${p("jsonCanvas.enabled")} to ${ v("false") }. --markdown-frontmatter Parse YAML frontmatter in Markdown files. Corresponding config key is ${p("markdown.yamlFrontmatter")} (${ t("boolean") }). ${title("Examples")}: Generate website from Vault located at "./vault/", with config file "./macana.json" then write it under "./out". deno run --allow-read=vault,macana.json --allow-write=out macana/cli.ts ./macana.json Same as the above, but the config file is at "./vault/.macana/config.json". deno run --allow-read=vault --allow-write=out macana/cli.ts ./vault/.macana/config.json Generate website without using config file. deno run \\ --allow-read=vault --allow-write=out \\ macana/cli.ts \\ --out ./out \\ --name "Foo Bar" \\ --copyright "Copyright 2020 John Doe" \\ ./vault `).trim(); } function parseConfigFile(configPath: string): config.MacanaConfig { const ext = path.extname(configPath); let x: unknown; switch (ext) { case ".json": { x = JSON.parse(Deno.readTextFileSync(configPath)); break; } case ".jsonc": { x = jsonc.parse(Deno.readTextFileSync(configPath), { allowTrailingComma: true, }); break; } default: { throw new Error( `Unknown config object, file extension needs to be either of .json or .jsonc (got ${ext})`, ); } } return config.parse(x, configPath); } if (import.meta.main) { Deno.exit(run()); Deno.exit(await run(Deno.args, Deno.stdout.isTerminal())); }
-
-
cli/config.ts (new)
-
@@ -0,0 +1,111 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // // SPDX-License-Identifier: Apache-2.0 import * as path from "../deps/deno.land/std/path/mod.ts"; import * as parser from "./parser.ts"; export interface MacanaConfig { input?: { path?: string; }; output?: { path?: string; precompress?: boolean; }; metadata?: { name?: string; favicon?: { svg?: string; png?: string; }; copyright?: string; logoImage?: string; }; documents?: { defaultLanguage?: string; languages?: Record<string, { title: string; lang: string }>; resolveShortestPathWhenPossible?: boolean; title?: { keepExtension?: boolean; }; }; markdown?: { enabled?: boolean; yamlFrontmatter?: boolean; }; jsonCanvas?: { enabled?: boolean; }; } function pathParser(configFilePath: string) { return parser.map( parser.string({ nonEmpty: true, trim: true }), (p) => path.join(configFilePath, p), ); } function configParser(configFilePath: string): parser.Parser<MacanaConfig> { const fsPathParser = pathParser(path.dirname(configFilePath)); return parser.object({ input: parser.object({ path: fsPathParser, }), output: parser.object({ path: fsPathParser, precompress: parser.boolean, }), metadata: parser.object({ name: parser.string({ nonEmpty: true }), favicon: parser.object({ svg: fsPathParser, png: fsPathParser, }), copyright: parser.string(), logoImage: fsPathParser, }), documents: parser.object({ defaultLanguage: parser.string({ nonEmpty: true }), languages: parser.record( parser.string({ nonEmpty: true }), parser.object({ title: parser.string({ nonEmpty: true }), lang: parser.string({ nonEmpty: true }), }, { optional: false, }), ), resolveShortestPathWhenPossible: parser.boolean, title: parser.object({ keepExtension: parser.boolean, }), }), markdown: parser.object({ enabled: parser.boolean, yamlFrontmatter: parser.boolean, }), jsonCanvas: parser.object({ enabled: parser.boolean, }), }); } export function parse(x: unknown, filePath: string): MacanaConfig { return parser.parse(x, configParser(filePath)); }
-
-
cli/parser.ts (new)
-
@@ -0,0 +1,159 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // // SPDX-License-Identifier: Apache-2.0 interface Context { fieldPath: readonly string[]; } function getFieldName(ctx: Context): string { if (ctx.fieldPath.length === 0) { return "<rootObject>"; } return ctx.fieldPath.join("."); } class MacanaCLIConfigParsingError extends Error {} class FieldConstraintError extends MacanaCLIConfigParsingError { constructor(ctx: Context, beWhat: string) { super( `Invalid config: "${getFieldName(ctx)}" MUST be ${beWhat}`, ); } } class UnexpectedFieldTypeError extends MacanaCLIConfigParsingError { constructor(ctx: Context, expected: string, found: string) { super( `Invalid config: "${ getFieldName(ctx) }" MUST be ${expected} but found ${found}`, ); } } export type Parser<T> = (x: unknown, ctx: Context) => T; export function parse<T>(x: unknown, parser: Parser<T>): T { return parser(x, { fieldPath: [], }); } export interface ObjectParserOptions { optional?: boolean; } export function object<T>( fields: { [K in keyof T]: Parser<T[K]> }, options?: { optional: true }, ): Parser<Partial<T>>; export function object<T>( fields: { [K in keyof T]: Parser<T[K]> }, options: { optional: false }, ): Parser<T>; export function object<T>( fields: { [K in keyof T]: Parser<T[K]> }, { optional = true }: ObjectParserOptions = {}, ): Parser<Partial<T>> { return (x, ctx) => { if (typeof x !== "object") { throw new UnexpectedFieldTypeError(ctx, "object", typeof x); } if (!x) { throw new UnexpectedFieldTypeError(ctx, "object", "null"); } const obj: Partial<T> = {}; for (const fieldName in fields) { if (fieldName in x) { obj[fieldName] = fields[fieldName]((x as T)[fieldName], { fieldPath: [...ctx.fieldPath, fieldName], }); continue; } if (!optional) { throw new MacanaCLIConfigParsingError( `Invalid config: "${ getFieldName(ctx) }" MUST have "${fieldName}" field`, ); } } return obj; }; } export function record<K extends string, V>( keyParser: Parser<K>, valueParser: Parser<V>, ): Parser<Record<K, V>> { return (x, ctx) => { if (typeof x !== "object") { throw new UnexpectedFieldTypeError(ctx, "object(record)", typeof x); } if (!x) { throw new UnexpectedFieldTypeError(ctx, "object(record)", "null"); } const rec: Partial<Record<K, V>> = {}; for (const key in x) { const parsedKey = keyParser(key, { fieldPath: [...ctx.fieldPath, "(key)"], }); const parsedValue = valueParser((x as Record<K, V>)[key as K], { fieldPath: [...ctx.fieldPath, key], }); rec[parsedKey] = parsedValue; } return rec as Record<K, V>; }; } export interface StringParserOptions { nonEmpty?: boolean; trim?: boolean; } export function string( { nonEmpty = false, trim = false }: StringParserOptions = {}, ): Parser<string> { return (x, ctx) => { if (typeof x !== "string") { throw new UnexpectedFieldTypeError(ctx, "string", typeof x); } const value = trim ? x.trim() : x; if (nonEmpty && !value) { throw new FieldConstraintError(ctx, "non-empty string"); } return value; }; } export const boolean: Parser<boolean> = (x, ctx) => { if (typeof x !== "boolean") { throw new UnexpectedFieldTypeError(ctx, "boolean", typeof x); } return x; }; export function map<A, B>(parser: Parser<A>, fn: (a: A) => B): Parser<B> { return (x, ctx) => { return fn(parser(x, ctx)); }; }
-
-
-
@@ -17,7 +17,7 @@ "exclude": ["require-await", "require-yield", "no-fallthrough"]} }, "tasks": { "build-docs": "deno run --allow-read=docs --allow-write=docs/.dist docs/build.ts", "build-docs": "deno run --allow-read=docs --allow-write=docs/.dist cli.ts --config docs/.macana/config.jsonc", "serve-docs": "cd docs/.dist && deno run --allow-read=. --allow-net https://deno.land/std/http/file_server.ts ." } }
-
-
-
@@ -184,7 +184,12 @@ "https://deno.land/std@0.223.0/log/mod.ts": "650c53c2c5d9eb05210c4ec54184ecb5bd24fb32ce28e65fad039853978f53f3","https://deno.land/std@0.223.0/log/rotating_file_handler.ts": "a6e7c712e568b618303273ff95483f6ab86dec0a485c73c2e399765f752b5aa8", "https://deno.land/std@0.223.0/log/setup.ts": "42425c550da52c7def7f63a4fcc1ac01a4aec6c73336697a640978d6a324e7a6", "https://deno.land/std@0.223.0/log/warn.ts": "f1a6bc33a481f231a0257e6d66e26c2e695b931d5e917d8de4f2b825778dfd4e", "https://deno.land/std@0.224.0/assert/assert.ts": "09d30564c09de846855b7b071e62b5974b001bb72a4b797958fe0660e7849834", "https://deno.land/std@0.224.0/assert/assertion_error.ts": "ba8752bd27ebc51f723702fac2f54d3e94447598f54264a6653d6413738a8917", "https://deno.land/std@0.224.0/fmt/colors.ts": "508563c0659dd7198ba4bbf87e97f654af3c34eb56ba790260f252ad8012e1c5", "https://deno.land/std@0.224.0/json/common.ts": "33f1a4f39a45e2f9f357823fd0b5cf88b63fb4784b8c9a28f8120f70a20b23e9", "https://deno.land/std@0.224.0/jsonc/mod.ts": "1756f094e00894ec27416b4fcccbcf445e73892a83cf1937de3aad7de2d5da7c", "https://deno.land/std@0.224.0/jsonc/parse.ts": "06fbe10f0bb0cba684f7902bf7de5126b16eb0e5a82220c98a4b86675c7f9cff", "https://deno.land/x/brotli@0.1.7/mod.ts": "08b913e51488b6e7fa181f2814b9ad087fdb5520041db0368f8156bfa45fd73e", "https://deno.land/x/brotli@0.1.7/wasm.js": "77771b89e89ec7ff6e3e0939a7fb4f9b166abec3504cec0532ad5c127d6f35d2", "https://deno.land/x/lz4@v0.1.2/mod.ts": "4decfc1a3569d03fd1813bd39128b71c8f082850fe98ecfdde20025772916582",
-
-
-
@@ -0,0 +1,5 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // // SPDX-License-Identifier: Apache-2.0 export * from "https://deno.land/std@0.224.0/jsonc/mod.ts";
-
-
-
@@ -0,0 +1,39 @@{ "input": { "path": "../" }, "output": { "path": "../.dist" }, "documents": { "defaultLanguage": "en-US", "languages": { "en": { "title": "English", "lang": "en-US" }, // Just for demonstration. "ja": { "title": "日本語", "lang": "ja" } }, // This Vault enables "Shortest Path When Possible" option. "resolveShortestPathWhenPossible": true }, "metadata": { "name": "Macana", // Append your copyright text for your forked build. "copyright": "© 2024 Shota FUJI. CC BY 4.0.", "favicon": { // Just using logo vector image. "svg": "../Assets/logo.svg", "png": "../Assets/logo-64x64.png" }, "logoImage": "../Assets/logo.svg" }, "markdown": { // Some pages have creation/update date in YAML frontmatter. "yamlFrontmatter": true } }
-
-
docs/build.ts (deleted)
-
@@ -1,251 +0,0 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // // SPDX-License-Identifier: Apache-2.0 /** * @internal * * Build script for Macana's document website. * * @module */ import * as log from "../deps/deno.land/std/log/mod.ts"; import * as cli from "../deps/deno.land/std/cli/mod.ts"; import * as colors from "../deps/deno.land/std/fmt/colors.ts"; import { DenoFsReader } from "../filesystem_reader/deno_fs.ts"; import { DenoFsWriter } from "../filesystem_writer/deno_fs.ts"; import { DefaultTreeBuilder, fileExtensions, ignoreDotfiles, langDir, removeExtFromMetadata, } from "../tree_builder/default_tree_builder.ts"; import { ObsidianMarkdownParser } from "../content_parser/obsidian_markdown.ts"; import { JSONCanvasParser } from "../content_parser/json_canvas.ts"; import { oneof } from "../content_parser/oneof.ts"; import { DefaultThemeBuilder } from "../page_builder/default_theme/mod.ts"; export async function build() { const outDir = new URL("./.dist", import.meta.url); await Deno.permissions.request({ name: "write", path: outDir, }); const srcDir = new URL(".", import.meta.url); await Deno.permissions.request({ name: "read", path: srcDir, }); await Deno.mkdir(outDir, { recursive: true }); const fileSystemReader = new DenoFsReader(srcDir); const fileSystemWriter = new DenoFsWriter(outDir); const treeBuilder = new DefaultTreeBuilder({ defaultLanguage: "en", ignore: [ignoreDotfiles], strategies: [ fileExtensions([".md", ".canvas"]), removeExtFromMetadata(), langDir({ en: "English", ja: "日本語", }, true), ], resolveShortestPathWhenPossible: true, }); const contentParser = oneof( new JSONCanvasParser(), new ObsidianMarkdownParser({ frontmatter: true }), ); const pageBuilder = new DefaultThemeBuilder({ siteName: "Macana", copyright: "© 2024 Shota FUJI. This document is licensed under CC BY 4.0", faviconSvg: ["Assets", "logo.svg"], faviconPng: ["Assets", "logo-64x64.png"], siteLogo: ["Assets", "logo.svg"], }); const documentTree = await treeBuilder.build({ fileSystemReader, contentParser, }); await pageBuilder.build({ documentTree, fileSystemReader, fileSystemWriter, }); } function prettyLogFormatter( useColors: boolean, verbose: boolean, ): log.FormatterFunction { function id<T>(x: T): T { return x; } function pipe<T>(a: (x: T) => T, b: (x: T) => T): (x: T) => T { return (x) => b(a(x)); } const ts = useColors ? pipe(colors.black, colors.dim) : id; return (record) => { let level: (str: string) => string = id; let msg: (str: string) => string = id; let payload: (str: string) => string = id; if (useColors) { switch (record.level) { case log.LogLevels.NOTSET: level = colors.dim; msg = colors.dim; payload = colors.dim; break; case log.LogLevels.DEBUG: level = colors.gray; msg = colors.gray; payload = pipe(colors.dim, colors.black); break; case log.LogLevels.INFO: level = colors.blue; msg = pipe(colors.bold, colors.black); payload = colors.black; break; case log.LogLevels.WARN: level = colors.yellow; msg = colors.black; payload = colors.black; break; case log.LogLevels.ERROR: level = colors.red; msg = colors.black; payload = colors.black; break; case log.LogLevels.CRITICAL: level = pipe(colors.red, colors.bold); msg = pipe(colors.bold, colors.red); payload = colors.red; break; } } let ret = colors.reset("\n") + level(record.levelName) + colors.reset(" ") + ts(record.datetime.toISOString()) + colors.reset("\n") + msg(record.msg); if (verbose) { for (const arg of record.args) { const text = Deno.inspect(arg, { colors: useColors, compact: false, }); for (const line of text.split("\n")) { ret += colors.reset("\n ") + payload(line); } } } return ret; }; } if (import.meta.main) { const args = cli.parseArgs(Deno.args, { string: ["log"], boolean: ["json", "verbose", "help"], alias: { "help": ["h"], }, }); const useColors = Deno.stdout.isTerminal(); if (args.help) { const title = useColors ? (s: string) => colors.underline(colors.bold(s)) : (s: string) => s; console.log(` docs/build.ts Build Macana's documentation website using Macana. ${title("Usage")}: deno run --allow-read=docs --allow-write=docs/.dist docs/build.ts [OPTIONS] ${title("Options")}: -h, --help Print this help text to stdout and exit. --log=<debug|info|warn|error|critical> Set the lowest log level to output. By default, docs/build.ts logs INFO, WARN, ERROR, and CRITICAL level logs. --json Output logs as JSON Lines. --verbose Output additional information alongisde log messages. This does not take effect when ${colors.bold("--json")} is set. `.trim()); Deno.exit(0); } let level: log.LevelName = "INFO"; if (args.log) { const upper = args.log.toUpperCase() as log.LevelName; const found = log.LogLevelNames.includes(upper); if (found) { level = upper; } else { log.critical( `"${args.log}" is not a valid log level.\n` + ` Available log levels are: ${log.LogLevelNames.join(", ")}`, ); Deno.exit(1); } } log.setup({ handlers: { default: new log.ConsoleHandler(level, { formatter: args.json ? log.jsonFormatter : prettyLogFormatter(useColors, args.verbose), useColors, }), }, loggers: { macana: { level: "DEBUG", handlers: ["default"], }, }, }); try { const start = performance.now(); await build(); const duration = performance.now() - start; log.info(`Complete docs build, elapsed ${duration}ms`, { duration, }); } catch (error) { log.critical(`Build aborted due to an error: ${error}`, { error, }); Deno.exit(1); } }
-
-
-
@@ -71,7 +71,7 @@ You can inspect (call-tree, memory, etc) using V8 Inspector Protocol via `--inspect-brk` flag.Read more about the flag [here](https://dotland.deno.dev/manual@v1.33.1/basics/debugging_your_code). ``` $ deno run --inspect-brk --allow-read=docs --allow-write=docs/.dist docs/build.ts $ deno run --inspect-brk --allow-read=docs --allow-write=docs/.dist cli.ts --config docs/.macana/config.jsonc ``` The above command prints Inspector URL to your terminal. Open the URL with a debugger client supporting V8 Inspector Protocol, and hit the record button.
-
@@ -79,4 +79,4 @@You can replace `--inspect-brk` flag with `--inspect-wait` flag, if you don't want profiler. However, as Macana is short-lived program, you can't use the simple `--inspect` flag. It's difficult to connect to the debugger during building this document, which is under 500ms even in GitHub Actions runner. It's difficult to connect to the debugger during building this document, which is under 500ms even in GitHub Actions runner.
-
-
-
@@ -1,5 +1,5 @@Macana is primarily designed as a TypeScript module for Deno. With this approach, you write your build script and import Macana from that file. Macana is initially designed as a TypeScript module for Deno. While this is tedious, you can tweak more options compared to the CLI. ## System requirements
-
-
-
@@ -128,17 +128,20 @@/** * Path to the SVG file to use as a favicon from the root directory (FileSystem Reader). */ faviconSvg?: readonly string[]; faviconSvg?: readonly string[] | Uint8Array; /** * Path to the PNG file to use as a favicon from the root directory (FileSystem Reader). */ faviconPng?: readonly string[]; faviconPng?: readonly string[] | Uint8Array; /** * Path to the website's logo or icon image from the root directory (FileSystem Reader). */ siteLogo?: readonly string[]; siteLogo?: readonly string[] | { ext: string; binary: Uint8Array; }; } /**
-
@@ -150,9 +153,12 @@ * FileSystem Reader.*/ export class DefaultThemeBuilder implements PageBuilder { #copyright: string; #faviconSvg?: readonly string[]; #faviconPng?: readonly string[]; #siteLogo?: readonly string[]; #faviconSvg?: readonly string[] | Uint8Array; #faviconPng?: readonly string[] | Uint8Array; #siteLogo?: readonly string[] | { ext: string; binary: Uint8Array; }; #siteName: string; constructor(
-
@@ -195,27 +201,51 @@const root = await fileSystemReader.getRootDirectory(); if (this.#faviconSvg) { assets.faviconSvg = this.#faviconSvg; await fileSystemWriter.write( assets.faviconSvg, await (await root.openFile(this.#faviconSvg)).read(), ); if (this.#faviconSvg instanceof Uint8Array) { assets.faviconSvg = [".assets", "favicon.svg"]; await fileSystemWriter.write( assets.faviconSvg, this.#faviconSvg, ); } else { assets.faviconSvg = this.#faviconSvg; await fileSystemWriter.write( assets.faviconSvg, await (await root.openFile(this.#faviconSvg)).read(), ); } } if (this.#faviconPng) { assets.faviconPng = this.#faviconPng; await fileSystemWriter.write( assets.faviconPng, await (await root.openFile(this.#faviconPng)).read(), ); if (this.#faviconPng instanceof Uint8Array) { assets.faviconPng = [".assets", "favicon.png"]; await fileSystemWriter.write( assets.faviconPng, this.#faviconPng, ); } else { assets.faviconPng = this.#faviconPng; await fileSystemWriter.write( assets.faviconPng, await (await root.openFile(this.#faviconPng)).read(), ); } } if (this.#siteLogo) { assets.siteLogo = this.#siteLogo; await fileSystemWriter.write( assets.siteLogo, await (await root.openFile(this.#siteLogo)).read(), ); if (this.#siteLogo instanceof Array) { assets.siteLogo = this.#siteLogo; await fileSystemWriter.write( assets.siteLogo, await (await root.openFile(this.#siteLogo)).read(), ); } else { assets.siteLogo = [".assets", `logo${this.#siteLogo.ext}`]; await fileSystemWriter.write( assets.siteLogo, this.#siteLogo.binary, ); } } const defaultPage = [...documentTree.defaultDocument.path, ""].join("/");
-