-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
-
13
-
14
-
15
-
16
-
17
-
18
-
19
-
20
-
21
-
22
-
23
-
24
-
25
-
26
-
27
-
28
-
29
-
30
-
31
-
32
-
33
-
34
-
35
-
36
-
37
-
38
-
39
-
40
-
41
-
42
-
43
-
44
-
45
-
46
-
47
-
48
-
49
-
50
-
51
-
52
-
53
-
54
-
55
-
56
-
57
-
58
-
59
-
60
-
61
-
62
-
63
-
64
-
65
-
66
-
67
-
68
-
69
-
70
-
71
-
72
-
73
-
74
-
75
-
76
-
77
-
78
-
79
-
80
-
81
-
82
-
83
-
84
-
85
-
86
-
87
-
88
-
89
-
90
-
91
-
92
-
93
-
94
-
95
-
96
-
97
-
98
-
99
-
100
-
101
-
102
-
103
-
104
-
105
-
106
-
107
-
108
-
109
-
110
-
111
-
112
-
113
-
114
-
115
-
116
-
117
-
118
-
119
-
120
-
121
-
122
-
123
-
124
-
125
-
126
-
127
-
128
-
129
-
130
-
131
-
132
-
133
-
134
-
135
-
136
-
137
-
138
-
139
-
140
-
141
-
142
-
143
-
144
-
145
-
146
-
147
-
148
-
149
-
150
-
151
-
152
-
153
-
154
-
155
-
156
-
157
-
158
-
159
-
160
-
161
-
162
-
163
-
164
-
165
-
166
-
167
-
168
-
169
-
170
-
171
-
172
-
173
-
174
-
175
-
176
-
177
-
178
-
179
-
180
-
181
-
182
-
183
-
184
-
185
-
186
-
187
-
188
-
189
-
190
-
191
-
192
-
193
-
194
-
195
-
196
-
197
-
198
-
199
-
200
-
201
-
202
-
203
-
204
-
205
-
206
-
207
-
208
-
209
-
210
-
211
-
212
-
213
-
214
-
215
-
216
-
217
-
218
-
219
-
220
-
221
-
222
-
223
-
224
-
225
-
226
-
227
-
228
-
229
-
230
-
231
-
232
-
233
-
234
-
235
-
236
-
237
-
238
-
239
-
240
-
241
-
242
-
243
-
244
-
245
-
246
-
247
-
248
-
249
-
250
-
251
-
252
-
253
-
254
-
255
-
256
-
257
-
258
-
259
-
260
-
261
-
262
-
263
-
264
-
265
-
266
-
267
-
268
-
269
-
270
-
271
-
272
-
273
-
274
-
275
-
276
-
277
-
278
-
279
-
280
-
281
-
282
-
283
-
284
-
285
-
286
-
287
-
288
-
289
-
290
-
291
-
292
-
293
-
294
-
295
-
296
-
297
-
298
-
299
-
300
-
301
-
302
-
303
-
304
-
305
-
306
-
307
-
308
-
309
-
310
-
311
-
312
-
313
-
314
-
315
-
316
-
317
-
318
-
319
-
320
-
321
-
322
-
323
-
324
-
325
-
326
-
327
-
328
-
329
-
330
-
331
-
332
-
333
-
334
-
335
-
336
-
337
-
338
-
339
-
340
-
341
-
342
-
343
-
344
-
345
-
346
-
347
-
348
-
349
-
350
-
351
-
352
-
353
-
354
-
355
-
356
-
357
-
358
-
359
-
360
-
361
-
362
-
363
-
364
-
365
-
366
-
367
-
368
-
369
-
370
-
371
-
372
-
373
-
374
-
375
-
376
-
377
-
378
-
379
-
380
-
381
-
382
-
383
-
384
-
385
-
386
-
387
-
388
-
389
-
390
-
391
-
392
-
393
-
394
-
395
-
396
-
397
-
398
-
399
-
400
-
401
-
402
-
403
-
404
-
405
-
406
-
407
-
408
-
409
-
410
-
411
-
412
-
413
-
414
-
415
-
416
-
417
-
418
-
419
-
420
-
421
-
422
-
423
-
424
-
425
-
426
-
427
-
428
-
429
-
430
-
431
-
432
-
433
-
434
-
435
-
436
-
437
-
438
-
439
-
440
-
441
-
442
-
443
-
444
-
445
-
446
-
447
-
448
-
449
-
450
-
451
-
452
-
453
-
454
-
455
-
456
-
457
-
458
-
459
-
460
-
461
-
462
-
463
-
464
-
465
-
466
-
467
-
468
-
469
-
470
-
471
-
472
-
473
-
474
-
475
-
476
-
477
-
478
-
479
-
480
-
481
-
482
-
483
-
484
-
485
-
486
-
487
-
488
-
489
-
490
-
491
-
492
-
493
-
494
-
495
-
496
-
497
-
498
-
499
-
500
-
501
-
502
-
503
-
504
-
505
-
506
-
507
-
508
-
509
-
510
-
511
-
512
-
513
-
514
-
515
-
516
-
517
-
518
-
519
-
520
-
521
-
522
-
523
-
524
-
525
-
526
-
527
-
528
-
529
-
530
-
531
-
532
-
533
-
534
-
535
-
536
-
537
-
538
-
539
-
540
-
541
-
542
-
543
-
544
-
545
-
546
-
547
-
548
-
549
-
550
-
551
-
552
-
553
-
554
-
555
-
556
-
557
-
558
-
559
-
560
-
561
-
562
-
563
-
564
-
565
-
566
-
567
-
568
-
569
-
570
-
571
-
572
-
573
-
574
-
575
-
576
-
577
-
578
-
579
-
580
-
581
-
582
-
583
-
584
-
585
-
586
-
587
-
588
-
589
-
590
-
591
-
592
-
593
-
594
-
595
-
596
-
597
-
598
-
599
-
600
-
601
-
602
-
603
-
604
-
605
-
606
-
607
-
608
-
609
-
610
-
611
-
612
-
613
-
614
-
615
-
616
-
617
-
618
-
619
-
620
-
621
-
622
-
623
-
624
-
625
-
626
-
627
-
628
-
629
-
630
-
631
-
632
-
633
-
634
-
635
-
636
-
637
-
638
-
639
-
640
-
641
-
642
-
643
-
644
-
645
-
646
-
647
-
648
-
649
-
650
-
651
-
652
-
653
-
654
-
655
-
656
-
657
-
658
-
659
-
660
-
661
-
662
-
663
-
664
-
665
-
666
-
667
-
668
-
669
-
670
-
671
-
672
-
673
-
674
-
675
-
676
-
677
-
678
-
679
-
680
-
681
-
682
-
683
-
684
-
685
-
686
-
687
-
688
-
689
-
690
-
691
-
692
-
693
-
694
-
695
-
696
-
697
-
698
-
699
-
700
-
701
-
702
// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com>
//
// SPDX-License-Identifier: Apache-2.0
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 "./internal/filesystem_reader/deno_fs.ts";
import { DenoFsWriter } from "./internal/filesystem_writer/deno_fs.ts";
import { noOverwrite } from "./internal/filesystem_writer/no_overwrite.ts";
import { precompress as precompressMiddleware } from "./internal/filesystem_writer/precompress.ts";
import {
DefaultTreeBuilder,
fileExtensions,
ignoreDotfiles,
langDir,
removeExtFromMetadata,
} from "./internal/tree_builder/default_tree_builder.ts";
import type { ContentParser } from "./internal/content_parser/interface.ts";
import { oneof } from "./internal/content_parser/oneof.ts";
import { ObsidianMarkdownParser } from "./internal/content_parser/obsidian_markdown.ts";
import { JSONCanvasParser } from "./internal/content_parser/json_canvas.ts";
import { DefaultThemeBuilder } from "./internal/page_builder/default_theme/mod.ts";
import * as config from "./internal/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",
"base-url",
"og-image",
"user-css",
"not-found-filename",
],
boolean: [
"help",
"verbose",
"json",
"keep-ext",
"precompress",
"disable-markdown",
"disable-jsoncanvas",
"markdown-frontmatter",
"markdown-downlevel-headings",
"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 downlevelHeadings = flags["markdown-downlevel-headings"] ||
configFile.markdown?.downlevelHeadings;
if (downlevelHeadings) {
log.debug("markdown.downlevelHeadings = 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,
downlevel: downlevelHeadings,
}),
].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),
};
}
let isFullBaseURL = false;
const baseURL = flags["base-url"] || configFile.output?.baseURL;
try {
if (baseURL) {
const url = new URL(baseURL, "macana://placeholder");
// If the `baseURL` is valid full URL, it overrides the "macana://placeholder" and
// `protocol` would be one of the given URL.
isFullBaseURL = url.protocol !== "macana:";
}
} catch (error) {
throw new Error(
`baseURL is not valid URL nor path: ${String(error)}`,
{ cause: error },
);
}
let ogImage: { ext: string; data: Uint8Array } | undefined = undefined;
const ogImagePath = flags["og-image"] ||
configFile.metadata?.openGraph?.image;
if (ogImagePath) {
log.debug(`metadata.openGraph.image = "${ogImagePath}"`);
if (isFullBaseURL) {
ogImage = {
ext: path.extname(ogImagePath),
data: Deno.readFileSync(ogImagePath),
};
} else {
log.warn(
"Open Graph image is set but base URL is not full URL: ignoring `metadata.openGraph.image` field.",
);
}
}
const userCSSInput = flags["user-css"] || configFile.output?.userCSS;
const userCSS = userCSSInput
? fileSystemReader.fromFsPath(userCSSInput)
: undefined;
const notFoundPageDisabled =
configFile.output?.notFoundPage?.enabled === false;
if (notFoundPageDisabled) {
log.debug("Not Found page generation is disabled");
}
const notFoundPageFilename = flags["not-found-filename"] ||
configFile.output?.notFoundPage?.filename || "404.html";
if (!notFoundPageDisabled && notFoundPageFilename) {
log.debug(`output.notFoundPage.filename = "${notFoundPageFilename}"`);
}
const pageBuilder = new DefaultThemeBuilder({
siteName,
copyright,
faviconSvg,
faviconPng,
siteLogo,
baseURL,
openGraph: ogImage && {
image: ogImage,
},
userCSS,
notFoundPage: notFoundPageDisabled ? undefined : {
filename: notFoundPageFilename,
},
});
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=.,<CONFIG_PATH> --allow-write=<OUTDIR> macana/cli.ts --config <CONFIG_PATH>
deno run --allow-read=. --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("Permissions")}:
${b("read")}
Macana requires file system read permission for the current directory, ${
b("VAULT_PATH")
}
and ${b("CONFIG_PATH")} (if ${
b("--config")
} option is set). Permission for the current
directory is required due to a technical limitation: there is no way to resolve a relative
path without accessing CWD in Deno.
${b("write")}
Macana requires file system write permission for ${
b("OUTDIR")
} in order to write
generated website files.
${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")}).
--user-css <PATH>
Path to the user provided CSS. This CSS contents will be appended to the final CSS:
this CSS can override every styles. The target file MUST be inside ${
b("VAULT_PATH")
}.
Corresponding config key is ${p("output.userCSS")} (${t("string")}).
--not-found-filename <FILENAME>
File name of Not Found page. Macana generates Not Found page only if ${
b("--base-url")
}
is set.
[default: 404.html]
Corresponding config key is ${p("output.notFoundPage.filename")} (${
t("string")
}).
--base-url <PATH OR URL>
URL or path to base at.
Corresponding config key is ${p("output.baseURL")} (${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")
}).
--markdown-downlevel-headings
Increase Markdown headings level by 1 (e.g. "# Foo" -> "## Foo").
Corresponding config key is ${p("markdown.downlevelHeadings")} (${
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=.,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=. --allow-write=out macana/cli.ts ./vault/.macana/config.json
Generate website without using config file.
deno run \\
--allow-read=. --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(await run(Deno.args, Deno.stdout.isTerminal()));
}