caml-mkdn
v0.0.12
Published
Colon Attribute Markup Lanugage -- a YAML-like markup syntax for (semantic) attributes in markdown.
Maintainers
Readme
caml-mkdn
CAML is a Colon Attribute Markup Language similar to YAML with some key differences:
- Slims down the syntax by removing the need for separators (
---). - Can be sprinkled throughout a markdown file (similar to markdown footnotes).
- Focuses on single level sequence collections (e.g. does not support object values, just arrays).
- Supports
[[wikiref]]values. - Can be rendered as a wikipedia-style infobox alongside the rest of your markdown.
- Supports markdown inside of strings.
🕸 Weave a semantic web in your 🎋 WikiBonsai digital garden.
Install
Install with npm:
npm install caml-mkdnUse
import * as caml from 'caml-mkdn';
let text = `
:key::value
data-view-key::data-view-value
:another-key::val1,val2,val3
:yet-another-key::
- 1
- 2
- 3
And some content!
`;
let payload = caml.load(text);
console.log(payload.data);
// should produce:
// {
// key: 'value',
// data-view-key: 'data-view-value',
// another-key: ['val1', 'val2', 'val3'],
// yet-another-key: [1, 2, 3],
// }
console.log(payload.content);
// should produce:
// 'And some content!'Note: To use commas (,) inside of singular caml string value, make sure to surround the value with single or double quotes ('', "") so that the comma is not used to create a list value. Commas inside of list string values is not (yet) supported.
API
buildHTML(attrs: AttrBoxData, opts?: CamlBuildHTMLOpts): string
Builds a complete attrbox from a collection of caml attrs -- the data-to-HTML entry point for SSGs and other routed consumers (the caml twin of wikirefs' buildHTML). It owns its class assembly internally, so routed output stays true to the attrbox contract. The streaming render plugins (markdown-it-caml, marked-caml, remark-caml) do NOT call this -- they own their own class assembly, sharing only the slugify primitive below. Returns the empty string for an empty collection. See caml-spec for the full contract.
import { buildHTML } from 'caml-mkdn';
import type { AttrBoxData } from 'caml-mkdn';
const attrs: AttrBoxData = {
'title': [{
type: 'string',
string: 'How To Read A Book',
value: 'How To Read A Book',
}],
// scanned 'True' displays as written, even though the typed value is `true`
'draft': [{
type: 'boolean',
string: 'True',
value: true,
}],
};
const html: string = buildHTML(attrs);
// html = '<aside class="attrbox">\n'
// + '<dl>\n'
// + '<div class="attr-item">\n'
// + '<dt class="key__title">title</dt>\n'
// + '<dd><span class="attr string">How To Read A Book</span></dd>\n'
// + '</div>\n'
// + '<div class="attr-item">\n'
// + '<dt class="key__draft">draft</dt>\n'
// + '<dd><span class="attr boolean">True</span></dd>\n'
// + '</div>\n'
// + '</dl>\n'
// + '</aside>\n'string is the value's scanned text, value its typed load — display uses the string verbatim (the author's casing survives), EXCEPT multi-line strings, which display the loaded value (block-scalar indentation/chomping applied) with newlines as <br>.
The <dt> shows the raw key and carries its key__<slug> css class (the hook for type-sensitive syntax highlights); value spans carry structural classes only (attr + the value type). Wiki values render as plain string spans -- caml never resolves wikirefs; a co-registered wikirefs plugin upgrades those spans to links. A routed consumer with wikirefs co-registered may instead pass a pre-rendered dd body via the item's html slot (e.g. the anchor built by wikirefs' own buildHTML):
const attrs: AttrBoxData = {
'title': [{
type: 'wiki',
string: '[[how-to-read-a-book]]',
value: 'how-to-read-a-book', // resolve()'s wiki payload: value = the FILENAME
html: '<a class="attr wiki" href="/how-to-read-a-book" data-href="/how-to-read-a-book">how-to-read-a-book</a>',
}],
};
// -> '<dd><a class="attr wiki" href="/how-to-read-a-book" data-href="/how-to-read-a-book">how-to-read-a-book</a></dd>'Options
cssNames: Partial<CamlCssNames>
Rename any class token: attrbox (the <aside> wrapper), attrItem (the per-key <div>), attr (the value span's structural class), and key (the <dt> class prefix, default key__).
buildWikiValue: (raw: string) => string | null
The wikirefs delegation slot: wiki-typed items (without a per-item html override) are offered here as their raw scanned text -- caml never parses them. Return the dd body, or null to fall back to the standalone string span. wikirefs ships the matching plug (attrValueBuilder), making a single call render a full attrbox with resolved wiki anchors:
import { buildHTML } from 'caml-mkdn';
import { attrValueBuilder } from 'wikirefs';
const html: string = buildHTML(attrs, {
buildWikiValue: attrValueBuilder({ resolveUri, resolveTitle, resolveDocType }),
});slugify(key: string): string
The one css primitive caml-mkdn shares. Each render plugin (markdown-it-caml, marked-caml, remark-caml) -- and buildHTML above -- owns its class-string assembly, driven by its own cssNames options; they import just this slug so a key name slugs identically as key__<slug> (caml attrbox) and reftype__<slug> (wikirefs attrbox) for the shared type→color pipeline:
import { slugify } from 'caml-mkdn';
slugify('My Key'); // 'my-key'
// a plugin builds its dt class inline: cssNames.key + slugify(key) -> 'key__my-key'The key__ prefix (the default cssNames.key) namespaces user-space key names away from the structural classes (a key literally named attr must not collide). Colors are a stylesheet concern: consumers inject .key__<slug> { color: ... } rules from their own attr schema -- no resolution happens at render time.
dump(attrs: any, opts?: DumpOpts): string
Serializes object as a CAML document. Similar to js-yaml's dump().
import { dump } from 'caml-mkdn';
import type { CamlDumpOpts } from 'caml-mkdn';
const attrs: Record<string, any> = {
title: 'My Document',
tags: ['tag1', 'tag2', 'tag3'],
};
const result: string = dump(attrs);
// result = ': title :: My Document\n'
// + ': tags ::\n'
// + ' - tag1\n'
// + ' - tag2\n'
// + ' - tag3\n'
const compact: string = dump(attrs, { format: 'none', listFormat: 'comma', prefix: true });
// compact = ':title::My Document\n'
// + ':tags::tag1,tag2,tag3\n'Options
format: 'pretty' | 'pad' | 'none'
The format CAML attributes should be printed in. Choices are 'pretty', 'pad', and 'none':
'none': Will just dump the text with no added whitespace.- ex:
:this-is-a-really-long-key::value :short-key:: value :comma-list::1,2,3 :mkdn-list:: - 1 - 2 - 3'pad': Will pad with a single whitespace around text and special chars.- ex:
: this-is-a-really-long-key :: value : short-key :: value : comma-list :: 1, 2, 3 : mkdn-list :: - 1 - 2 - 3'pretty': Will pad as well as determine the longest key length and pad all other keys with the same amount of spaces to achieve a "pretty" print feel.- ex:
: this-is-a-really-long-key :: value : short-key :: value : comma-list :: 1, 2, 3 : mkdn-list :: - 1 - 2 - 3
listFormat: 'comma' | 'mkdn'
Dump CAML attribute lists by comma-separation or mkdn-list-separation.
- comma-separated:
: comma-list :: 1, 2, 3 - Mkdn-separation:
: mkdn-list :: - 1 - 2 - 3
Note: multi-line strings in lists are only supported with 'mkdn' format. When multiLine is set and a list item contains newlines, it is serialized as a block scalar:
dump({ tags: ['first', 'line one\nline two\n'] }, { listFormat: 'mkdn', multiLine: 'literal', chomp: 'clip' });
// : tags ::
// - first
// - |
// line one
// line twoprefix: boolean
Whether or not to use the colon : prefix when dumping CAML attributes.
- With:
: key :: value - Without:
key :: value
multiLine: 'none' | 'literal' | 'folded'
How to serialize string values that contain newlines.
'none'(default): dumps the value as-is (newlines appear inline).'literal': emits a|block scalar with indented content (preserves newlines).'folded': emits a>block scalar, wrapping long lines at ~72 characters (replaces newlines with spaces).
const attrs = { poem: 'roses are red\nviolets are blue\n' };
dump(attrs, { multiLine: 'literal' });
// : poem :: |
// roses are red
// violets are blue
dump(attrs, { multiLine: 'folded' });
// : poem :: >
// roses are red violets are bluechomp: 'clip' | 'strip' | 'keep'
Controls trailing newline behavior for multi-line block scalars. Only applies when multiLine is 'literal' or 'folded'.
'clip'(default): emits|or>— adds a single trailing newline.'strip': emits|-or>-— no trailing newline.'keep': emits|+or>+— preserves all trailing newlines from the value.
const attrs = { notes: 'line one\nline two' };
dump(attrs, { multiLine: 'literal', chomp: 'clip' });
// : notes :: |
// line one
// line two
dump(attrs, { multiLine: 'literal', chomp: 'strip' });
// : notes :: |-
// line one
// line two
dump(attrs, { multiLine: 'literal', chomp: 'keep' });
// : notes :: |+
// line one
// line twoindent: number
Number of spaces to indent multi-line block content. Defaults to 2. Only applies when multiLine is 'literal' or 'folded'.
load(content: string): CamlLoadPayload
Load a content string, parse CAML attributes, and store attributes in data and the rest of the content string in content. Similar to graymatter.
import { load } from 'caml-mkdn';
import type { CamlLoadPayload } from 'caml-mkdn';
const payload: CamlLoadPayload = load(
': title :: My Document\n'
+ ': tags :: tag1, tag2, tag3\n'
+ '\n'
+ 'And some content!\n'
);
// payload = {
// data: {
// title: 'My Document',
// tags: ['tag1', 'tag2', 'tag3'],
// },
// content: 'And some content!',
// }resolve(value: string): CamlValData
Take a CAML attribute value as a string, parse it, and return CamlValData.
import { resolve } from 'caml-mkdn';
import type { CamlValData } from 'caml-mkdn';
const str: CamlValData = resolve('hello');
// str = { type: 'string', string: 'hello', value: 'hello' }
const num: CamlValData = resolve('42');
// num = { type: 'int', string: '42', value: 42 }
const bool: CamlValData = resolve('true');
// bool = { type: 'bool', string: 'true', value: true }
const nil: CamlValData = resolve('null');
// nil = { type: 'null', string: 'null', value: null }CamlValData looks like:
interface CamlValData {
type: string; // a string description of the value's type
string: string; // a string representation of the value
value: null // the literal parsed value
| boolean
| number
| bigint
| Date
| string;
}rename(content: string, oldKey: string, newKey: string, opts?: UpdateOpts): [number, number, string] | string | undefined
#todo -- look at tendr-app/.../ripple-sync/reftype.ts
Find a CAML attribute and rename its key. Returns undefined if the key is not found. Whitespace around the :: marker is preserved.
#todo
update(content: string, key: string, newVal: string, opts?: UpdateOpts): [number, number, string] | string | undefined
Find a CAML attribute by key and replace its value. Returns undefined if the key is not found. Whitespace around the :: marker is preserved.
import { update } from 'caml-mkdn';
import type { UpdateOpts } from 'caml-mkdn';
// default format: 'content' — returns the full string with the replacement applied
const content = update('attr::old value\n', 'attr', 'new value');
// content = 'attr::new value\n'
// format: 'offsets' — returns [start, end, replacementText]
const offsets = update('attr::old value\n', 'attr', 'new value', { format: 'offsets' });
// offsets = [0, 15, 'attr::new value']
// type-aware matching
const dated = update(': date :: 2001-12-14\n', 'date', '2022-11-14', { type: 'timestamp' });
// dated = ': date :: 2022-11-14\n'Options
format: 'content' | 'offsets'
'content'(default): Returns the full content string with the value replaced inline.'offsets': Returns[start, end, replacementText]— the character offsets and the replacement string, useful for editor integrations.
type?: string
Constrain the match to a specific value type (e.g. 'timestamp', 'int', 'bool'). If omitted, matches any value.
scan(content: string, opts?: CamlScanOpts): CamlScanResult[]
Scan a given content string and return an array of descriptions of all valid CAML attribute constructs. Each result groups a key with its values.
import { scan } from 'caml-mkdn';
import type { CamlScanResult } from 'caml-mkdn';
const results: CamlScanResult[] = scan(': title :: My Document\n: count :: 42\n');
// results = [
// {
// key: { text: 'title', start: 2 },
// vals: [
// { type: 'string', val: { text: 'My Document', start: 11 } },
// ],
// },
// {
// key: { text: 'count', start: 25 },
// vals: [
// { type: 'int', val: { text: '42', start: 34 } },
// ],
// },
// ]Options
skipEsc: boolean
Whether or not to skip escaped CAML construct instances; set to true by default.
true(default): CAML inside backticks, code spans, and fenced code blocks is ignored.false: All CAML constructs are returned regardless of escaping.
const results: CamlScanResult[] = scan('`:attr::value`\n', { skipEsc: true });
// results = [
// {
// key: { text: 'attr', start: 1 },
// vals: [
// { type: 'string', val: { text: 'value', start: 7 } },
// ],
// },
// ]Types
interface CamlScanOpts {
skipEsc?: boolean;
}
interface ScanTxt {
text: string;
start: number;
}
interface CamlScanResVal {
type: string;
val: ScanTxt;
}
interface CamlScanResult {
key: ScanTxt;
vals: CamlScanResVal[];
}