@wikibonsai/almanac
v0.0.1
Published
A type system (doctypes + reltypes) for markdown-based PKM systems.
Maintainers
Readme
almanac
Type system (doctypes + reltypes) that reads doctypes (t.doc.toml) and reltypes (t.rel.toml) and defines the type system. Supports type resolution, affixes, placeholders, and validation.
Supports both TOML and YAML formats.
📔 Check your types in your 🎋 WikiBonsai digital garden.
Install
npm install almanacUsage
Load
Load doctype and reltype definitions from TOML or YAML manifest files. Both loaders return an empty object if the file doesn't exist, so you can fall back to built-in defaults.
import type { DocTypeData, RelTypeData } from 'almanac';
const docData: DocTypeData = loadDocTypes('./t.doc.toml');
// docData = {
// type_tree: 'default\n'
// + ' entry\n'
// + ' book\n'
// + ' scifi\n'
// + ' ...',
// entry: { path: '/bonsai/entries/', emoji: '🏷', attrs: ['hypernym', 'synonym', ...] },
// book: { path: '/books/', prefix: 'bk.', attrs: ['title', 'author', ...] },
// ...
// }loadRelTypes()
import { loadDocTypes, loadRelTypes } from 'almanac';
const relData: RelTypeData = loadRelTypes('./t.rel.toml');
// relData = {
// types: [
// { name: 'antonym', kind: 'attr', sync: 'antonym' },
// { name: 'hypernym', kind: 'attr', sync: 'hyponym' },
// { name: 'id', kind: 'attr', computed: true },
// { name: 'title', kind: 'attr' },
// ...
// ],
// }Filtering
Specific types can be filtered for targeting types by relationship kind:
import type { RelTypeEntry } from 'almanac';
// filter types by kind
const attrTypes: RelTypeEntry[] = relData.types?.filter((t: RelTypeEntry) => t.kind === 'attr') || [];
// attrTypes = [
// { name: 'antonym', kind: 'attr', sync: 'antonym' },
// { name: 'author', kind: 'attr' },
// ...
// ]
const linkTypes: RelTypeEntry[] = relData.types?.filter((t: RelTypeEntry) => t.kind === 'link') || [];
// linkTypes = [] // no default link typesResolve
Resolve a filename to its doctype. Resolution follows a precedence order: prefix > attr > path. Returns the default type when no match is found.
resolve()
import { resolve } from 'almanac';
import type { DocOptsIndex } from 'almanac';
const typeOpts: DocOptsIndex = docData as DocOptsIndex;
const type: string = resolve(typeOpts, 'e.my-entry');
// type = 'entry' (matched prefix 'e.')Affixes
Inspect and manipulate type prefixes and suffixes. These handle both literal strings and placeholder-based patterns (like :date.).
prefixes() / suffixes()
import { prefixes, suffixes } from 'almanac';
const allPrefixes: string[] = prefixes(typeOpts);
// allPrefixes = ['i.', 'e.', 'bk.', 'p.']
const allSuffixes: string[] = suffixes(typeOpts);
// allSuffixes = []hasAffix()
import { hasAffix } from 'almanac';
const [unfixed, affixed]: [string, string] = hasAffix(typeOpts, 'e.my-entry');
// unfixed = 'my-entry', affixed = 'e.'addAffixes() / stripAffixes()
import { addAffixes, stripAffixes } from 'almanac';
const withPrefix: string = addAffixes('my-entry', 'e.', undefined);
// withPrefix = 'e.my-entry'
const stripped: string = stripAffixes('e.my-entry', 'e.', undefined);
// stripped = 'my-entry'Placeholders
Prefix and suffix strings can contain placeholders that are filled at runtime or matched via regex:
| Placeholder | Description | Example Output |
|-------------|---------------------|----------------|
| :id | Generated ID | abc123 |
| :date | ISO date | 2024-01-15 |
| :year | 4-digit year | 2024 |
| :month | 2-digit month | 01 |
| :day | 2-digit day | 15 |
| :hour | 2-digit hour | 09 |
| :minute | 2-digit minute | 05 |
fillPlaceholderData()
import { fillPlaceholderData } from 'almanac';
const filled: string = fillPlaceholderData(':date.daily', {
date: new Date(2024, 0, 15),
});
// filled = '2024-01-15.daily'convertPlaceholderToRegex()
import { convertPlaceholderToRegex } from 'almanac';
const regex: RegExp = convertPlaceholderToRegex(':date.', 'prefix');
// regex = /^\d{4}-\d{2}-\d{2}\./Checks
Validates doctype and reltype data for internal consistency. Returns a CheckResult with errors (broken invariants) and warnings (suspicious but not broken).
| Skip name | Level | Description |
|-----------|-------|-------------|
| sync-pairs | error | Sync targets must exist and sync back reciprocally |
| type-overrides | warning | Named sections should match a type in the types array |
| type-tree | warning | Tree nodes and flat sections should match; skips HTML comments |
| doctype-attrs | warning | Attrs in doctype definitions should exist in reltype types |
check()
import { check } from 'almanac';
import type { CheckResult } from 'almanac';
const result: CheckResult = check(docData, relData);
// result = {
// errors: [{ rule: 'sync-pairs', message: '"foo" syncs to "bar" but "bar" does not exist' }],
// warnings: [{ rule: 'type-overrides', message: 'Named section [stale] does not match any type' }],
// }Skip specific validators with the skip option:
const skipped: CheckResult = check(docData, relData, { skip: ['sync-pairs'] });
// skipped = { errors: [...], warnings: [...] } // sync-pairs errors omittedDocType File Structure
# t.doc.toml
# type hierarchy defined as a semtree string
type_tree = """
default
entry
book
scifi
non-fiction
person
"""
# type definitions (flat sections)
[entry]
prefix = "e."
color = "#FF0000"
[book]
path = "/books/"
prefix = "bk."
attrs = ['title', 'subtitle', 'author']
[scifi]
path = "/books/sci-fi"
[daily]
prefix = ":date."
path = "/daily"DocType Type Properties
attr— attribute name used to identify this type in file metadata. String.attrs— polymorphic: array of inline attribute names (e.g.['title', 'author']) or a string template reference (e.g."t.book"). Template takes precedence at runtime if both exist.color— display color. Hex string, e.g."#FF0000".emoji— display emoji. String, e.g."🏷".template— template file reference. String.root— root path override. String.suffix— filename suffix. String.path— directory path for this type. String, e.g."/books/".prefix— filename prefix. String, e.g."e.","bk.", or placeholder":date.".
Type Resolution
Doctypes are resolved with the following precedence:
prefix— filename starts with the type's prefixattrs— file attributes contain the type keypath— file is in the type's directory
Why type_tree instead of TOML or YAML nesting?
Type hierarchy is defined as a semtree string rather than TOML or YAML nesting to avoid namespace collisions between type names and property names. With nested sections, book.color could mean "book's color property" or "a child type called color under book." With a semtree string for hierarchy and flat sections for properties, there is no ambiguity.
Doc kind/status defaults
Doc kind categories (default, template, media, zombie) and status categories (orphan, isolate) are config-level concerns —- see trug's [doc] section.
RelType File Structure
# t.rel.toml
types = [
# name kind computed sync
{ name = "antonym" , kind = "attr", sync = "antonym" },
{ name = "author" , kind = "attr" },
{ name = "hypernym" , kind = "attr", sync = "hyponym" },
{ name = "hyponym" , kind = "attr", sync = "hypernym" },
{ name = "id" , kind = "attr", computed = true },
{ name = "synonym" , kind = "attr", sync = "synonym" },
{ name = "title" , kind = "attr" },
]
# types with extra properties pulled out into their own sections:
# [author]
# target = "person"
# color = "#FF0000"
#
# [hypernym]
# target = "entry"
# color = "#FF0000"RelType Type Properties
computed— (optional) system-managed attribute. Boolean. See[attr]in trug's config for details.kind— relationship kind."attr"(CAML attribute / wikiattr),"link"(typed wikilink), or"ref"(attr+link— appears as both an attribute and a wikilink).name— the type identifier. String.sync— (optional) bidirectional sync partner. String. Symmetric when self-referencing (e.g.synonymsyncs withsynonym). Asymmetric when cross-referencing (e.g.hypernymsyncs withhyponymand vice versa) — editing one side auto-updates the other.target— (optional) constrains which doctype this reltype can point to. String, e.g.authortargetsperson.
