Macana

Deno module

Macana is initially designed as a TypeScript module for Deno. While this is tedious, you can tweak more options compared to the CLI usage.

System requirements

In order to build your Vault with Macana as a Deno module, you need Deno on your system.

Macana is developed and tested on Deno v1.41. Earlier versions of Deno runtime may not be compatible with Macana.

The build script

Overview

The most simple build script this document shows as a demonstration, will do:

  1. Create file system reader / writer
  2. Create content parser for Markdown and JSONCanvas
  3. Scan and Parse documents inside Vault
  4. Build and Write HTML files and asset files

Here is the final form. Read the following sections if you're not sure what these lines are doing.

// build.ts
import {
	DenoFsReader,
	DenoFsWriter,
	DefaultTreeBuilder,
	fileExtensions,
	ignoreDotfiles,
	removeExtFromMetadata,
	defaultDocumentAt,
	ObsidianMarkdownParser,
	JSONCanvasParser,
	oneof,
	DefaultThemeBuilder,
} from "https://deno.land/x/macana@v0.2.0/mod.ts"

const fileSystemReader = new DenoFsReader(new URL("./contents", import.meta.url));

const fileSystemWriter = new DenoFsWriter(new URL("./.dist", import.meta.url));

const treeBuilder = new DefaultTreeBuilder({
	defaultLanguage: "en",
	ignore: [ignoreDotfiles],
	strategies: [
		fileExtensions([".md", ".canvas"]),
		removeExtFromMetadata(),
		defaultDocumentAt(["About", "Author.md"]),
	],
	resolveShortestPathWhenPossible: true,
});

const documentTree = await treeBuilder.build({
	fileSystemReader,
	contentParser: oneof(
		new JSONCanvasParser(),
		new ObsidianMarkdownParser({
			frontmatter: true,
		}),
	),
})

const pageBuilder = new DefaultThemeBuilder({
	siteName: "<YOUR WEBSITE TITLE>",
	copyright: "<COPYRIGHT TEXT>",
	faviconSvg: ["Assets", "favicon.svg"],
	faviconPng: ["Assets", "favicon.png"],
	siteLogo: ["Assets", "logo.png"],
})

await pageBuilder.build({
	documentTree,
	fileSystemReader,
	fileSystemWriter,
})

FileSystem Reader

Macana uses abstraction layer for file I/O. FileSystem Reader provides listing directory and reading file contents capability.

import { DenoFsReader } from "https://deno.land/x/macana@v0.2.0/mod.ts";

const fileSystemReader = new DenoFsReader(new URL("./contents", import.meta.url));

// ---

// = ./contents
const root = await fileSystemReader.getRootDirectory();

// = entries of ./contents
const entries = await root.read();

DenoFsReader, which uses Deno's native file system I/O, restricts access to the given root directory (constructor parameter). Thanks to this, you can safely limit the scope of read permission to the reader's root directory (constructor parameter).

$ deno run --allow-read=contents build.ts

FileSystem Writer

FileSystem Writer provides capability to write to files and create directories.

import { DenoFsWriter } from "https://deno.land/x/macana@v0.2.0/mod.ts";

const fileSystemWriter = new DenoFsWriter(new URL("./.dist", import.meta.url));

// ---

const text = new TextEncoder().encode("Hello, World!\n");

await fileSystemWriter.write(["foo", "bar.txt"], text);

// .dist/foo/bar.txt with content "Hello, World!" created.

As with DenoFsReader, DenoFsWriter restricts file I/O scope too. You can limit the scope of write permission to the writer's root directory (constructor parameter).

$ deno run --allow-write=.dist build.ts

Macana exports some useful function to wrap the FileSystem Writer for additional functionality.

Precompress

precompress function adds precompress functionality to the FileSystem Writer. The resulted file formats are compatible with Caddy's precompressed directive.

import { DenoFsWriter, precompress } from "https://deno.land/x/macana@v0.2.0/mod.ts";

const fileSystemWriter = precompress()(
	new DenoFsWriter(new URL("./.dist", import.meta.url))
);

// ---

const text = new TextEncoder().encode(`console.log("Hello, World!");`);

// fileSystemWriter writes:
// ./.dist/index.js
// ./.dist/index.js.br
// ./.dist/index.js.gz
// ./.dist/index.js.zst
await fileSystemWriter.write(["index.js"], text);

Overwrite prevention

noOverwrite function skips redundant write to the same file. In addition to that, if it detects the writes to different content to the same file, it aborts the build in order to prevent producing inconsistent build output.

import { DenoFsWriter, noOverwrite } from "https://deno.land/x/macana@v0.2.0/mod.ts";

const fileSystemWriter = noOverwrite(
	new DenoFsWriter(new URL("./.dist", import.meta.url))
);

// ---

const text = new TextEncoder().encode(`console.log("Hello, World!");`);

await fileSystemWriter.write(["index.js"], text); // This performs file I/O
await fileSystemWriter.write(["index.js"], text); // This does not

For performance reasons, you should use this function.

Tree Builder

Tree Builder is responsible for scanning Vault files and directories, and building a document tree. This has the most tuning knob, because Obsidian uses no convention on directory structure: what to include or exclude, how to manage multi language documents, whether the title should include file extension... the possibility of choices and preferences is too large.

In order not to constrain too much on what you can do, Macana does very little by default. Uses filename as a title as-is, first found document as a default document, tries to parse every file as a document, etc.

import {
	DefaultTreeBuilder,
	ObsidianMarkdownParser,
	JSONCanvasParser,
	oneof
} from "https://deno.land/x/macana@v0.2.0/mod.ts";

// This works, but you may (probably) want to tune it further
const treeBuilder = new DefaultTreeBuilder({
	defaultLanguage: "en",
});

const documentTree = await treeBuilder.build({
	fileSystemReader,
	contentParser: oneof(
		new JSONCanvasParser(),
		new ObsidianMarkdownParser({
			frontmatter: true,
		}),
	),
})

Users modify and restrict this permissive behavior by strategies. Strategy is a function that takes a file or a directory then tells the Tree Builder to skip the file or returns metadata.

import {
	// Restrict which files to be treated as a document, based on extension
	fileExtensions,
	// Remove file extension part from metadata
	removeExtFromMetadata,
	// Explicitly tell which document is the default document
	defaultDocumentAt,
	// Treat certain directories as language directory - for multi language Vault
	langDir,
	// Use file timestamps as creation/update date
	// (does not work with Git, though)
	useFileSystemTimestamps,
} from "https://deno.land/x/macana@v0.2.0/mod.ts";

const treeBuilder = new DefaultTreeBuilder({
	defaultLanguage: "en",
	strategies: [
		fileExtensions([".md"]),
		// ...
	],
})

const documentTree = await treeBuilder.build({
	// ...
})

Page Builder

Page Builder builds HTML and other assets from a document tree. If you don't like how Macana's default theme builder works, write your own.

import { DefaultThemeBuilder } from "https://deno.land/x/macana@v0.2.0/mod.ts";

const pageBuilder = new DefaultThemeBuilder({
	siteName: "<YOUR WEBSITE TITLE>",
	copyright: "<COPYRIGHT TEXT>",
	faviconSvg: ["Assets", "favicon.svg"],
	faviconPng: ["Assets", "favicon.png"],
	siteLogo: ["Assets", "logo.png"],
})

await pageBuilder.build({
	documentTree,
	fileSystemReader,
	fileSystemWriter,
})

While this document throughly uses Assets/ as the directory for assets, you can use whatever you want: you can even place your asset files at the top level of your Vault directory. However, every assets needs to be inside the Vault directory. Otherwise FileSystem Reader cannot access to the files.

Running the script

Since all of these are plain simple JavaScript / TypeScript, you can simply run deno run with minimum permission flags.

$ deno run --allow-read=. --allow-write=<OUTPUT DIR> build.ts

If you do not want to type this lengthy command every time you build, define it as a Deno task.

// deno.jsonc
{
  "tasks": {
    "build": "deno run --allow-read=. --allow-write=<OUTPUT DIR> build.ts"
  }
}