knap
v0.2.2
Published
A flexible template engine for creating Markdown.
Readme
Knap
Knap is a flexible template engine for creating Markdown. It is shared by Obsidian tools, including Web Clipper and Importer.
Knap provides tokenization, parsing, logic, rendering, structured errors, and
filters. Applications supply variables and runtime integrations. Knap uses an
AST interpreter. It does not use eval or execute arbitrary JavaScript.
The Obsidian Web Clipper documentation includes examples of Knap's shared logic, filters, and variable syntax.
Install
pnpm add knapUse
import {
createEngine,
standardFilters,
type TemplateVariables,
} from 'knap';
const engine = createEngine({ filters: standardFilters });
const variables: TemplateVariables = {
title: ' An imported note ',
tags: ['reference', 'reading'],
};
const result = await engine.render(
`# {{ title | trim }}
{% if tags %}Tags: {{ tags | join:", " }}{% endif %}`,
{ variables },
);
if (result.errors.length === 0) {
console.log(result.output);
}Every error contains a stable code, message, line, and column. Use
renderOrThrow() when exceptions fit the calling application better:
const output = await engine.renderOrThrow('{{ title | upper }}', { variables });Filters that deliberately preserve their input after invalid runtime data can
report non-fatal diagnostics in result.warnings. Each warning includes a
stable code, message, filter, line, and column. Warnings do not make
renderOrThrow() throw. Identical warnings from repeated evaluation of the
same filter expression are deduplicated within each render.
Syntax
The syntax is inspired by Twig and Liquid.
{{ title }}
{{ title | upper }}
{{ published | date:"YYYY-MM-DD" }}
{{ First name | trim }}
{% if author %}
By {{ author.name }}
{% elseif site %}
From {{ site }}
{% else %}
Unknown source
{% endif %}
{% for item in links %}
- {{ item.title }}: {{ item.url }}
{% endfor %}
{% set heading = title | upper %}The language supports chained filters, if/elseif/else, for, set,
nested properties, array access, comparisons, boolean operators, nullish
fallbacks, and whitespace control.
Application variables
Values absent from the variables object can be resolved asynchronously. Knap does not know what a browser tab, vault, selector, schema, or model is.
const result = await engine.render('{{ remoteValue | upper }}', {
variables: {},
context: { documentId: 'example' },
resolveVariable: async (name, { context }) => {
if (name === 'remoteValue') {
return loadValue(context.documentId);
}
return undefined;
},
});Local variables take precedence over the resolver.
Filters
Filters are registered explicitly when an engine is created. The standard
registry is available as standardFilters; DOM-dependent filters are available
separately from knap/html.
Parameters follow a filter name after a colon, and filters can be chained with
|:
{{ title | trim | upper }}
{{ published | date:"YYYY-MM-DD" }}Standard filters
| Filter | Purpose |
| --- | --- |
| blockquote | Format text as a Markdown block quote. |
| calc | Apply a basic arithmetic operation to a number. |
| callout | Format content as an Obsidian callout. |
| camel, kebab, pascal, snake | Convert text to the named casing style. |
| capitalize, lower, title, upper | Change text capitalization. |
| date | Parse and format a date. |
| date_modify | Add or subtract a date unit. |
| decode_uri | Decode percent-encoded URI text. |
| duration | Format ISO 8601 durations or a number of seconds. |
| first, last, nth, slice | Select values or ranges from arrays and text. |
| footnote | Format values as Markdown footnotes. |
| fragment_link | Add text-fragment links using a source URL parameter. |
| image | Format a URL as a Markdown image. |
| join, split | Join arrays or split strings. |
| length | Return the length of a value. |
| link, wikilink | Format Markdown links or Obsidian wikilinks. |
| list | Format array-like data as a list. |
| map | Map fields from structured array data. |
| merge | Merge structured values. |
| number_format, round | Format or round numeric values. |
| object | Select or reshape structured object data. |
| remove_attr, strip_attr | Remove selected HTML attributes or all except selected attributes. |
| remove_tags, strip_tags | Remove selected HTML tags or all except selected tags. |
| replace | Apply one or more text or regular-expression replacements. |
| replace_tags | Replace selected HTML tag names. |
| reverse, unique | Reverse or deduplicate array-like data. |
| safe_name | Sanitize text for use as a file name. |
| strip_md, stripmd | Remove Markdown formatting. |
| table | Format structured data as a Markdown table. |
| template | Apply a small value-substitution template to structured data. |
| trim | Remove surrounding whitespace. |
| uncamel | Convert camel-cased text into words. |
| unescape | Unescape encoded text. |
| yaml | Format a value as a YAML-safe scalar. |
Filter metadata, including parameter validation and examples, is exported as
standardFilterMetadata. Invalid filter names and invalid parameters are
reported by engine.validate() and engine.render().
When a filter cannot use runtime input but preserves that input for
compatibility, engine.render() reports a non-fatal structured warning. This
includes values such as an unparseable date or an invalid regular expression.
HTML preset
Import htmlFilters from knap/html to enable:
| Filter | Purpose |
| --- | --- |
| html_to_json | Convert HTML elements into structured JSON values. |
| remove_html | Remove selected HTML elements and their contents. |
These filters are opt-in because they require browser-compatible DOM globals
such as DOMParser; they do not ship in the root runtime graph:
import { createEngine, standardFilters } from 'knap';
import { htmlFilters } from 'knap/html';
const engine = createEngine({
filters: {
...standardFilters,
...htmlFilters,
},
});Custom filters
The engine's filter registry is used for both validation and rendering. Filter parameters use Knap's colon-delimited parameter syntax.
import {
createEngine,
standardFilters,
type TemplateFilter,
} from 'knap';
const markdown: TemplateFilter = (html, param, context) => {
const baseUrl = param?.replace(/^(['"])(.*)\1$/s, '$2');
return convertToMarkdown(html, baseUrl, context);
};
markdown.metadata = {
example: 'markdown:"https://example.com"',
validateParams: param => ({
valid: Boolean(param),
error: 'requires a base URL',
}),
};
const engine = createEngine({
filters: {
...standardFilters,
markdown,
},
});Custom filters may be asynchronous. A filter can also report a non-fatal diagnostic while returning a fallback value:
const lookup: TemplateFilter = async (value, _param, context) => {
const result = await findValue(value);
if (result === undefined) {
context?.reportWarning?.({ message: `Could not find ${value}` });
return value;
}
return result;
};The filter parameter is passed in its serialized Knap form so filters that accept multiple parameters can preserve delimiters and quoting. A custom filter that expects one scalar parameter can normalize surrounding quotes as above.
Host data needed by a custom filter belongs in the generic engine context:
type HostContext = { sourceUrl: string };
const sourceLink: TemplateFilter<HostContext> = (value, _param, filterContext) => {
return `[${value}](${filterContext?.context?.sourceUrl})`;
};
const engine = createEngine<HostContext>({
filters: { ...standardFilters, source_link: sourceLink },
});
await engine.render('{{ title | source_link }}', {
variables: { title: 'Knap' },
context: { sourceUrl: 'https://example.com' },
});API
createEngine({ filters })creates an immutable engine-scoped registry.engine.render(template, input, options?)returns output, structured errors, and non-fatal warnings.engine.renderOrThrow(template, input, options?)returns output or throwsTemplateRenderError.engine.parse(template)returns the AST and parser diagnostics.engine.validate(templateOrAst)validates syntax and the configured filters.tokenize(template),parse(template),validateVariables(ast), andvalidateFilters(ast, metadata)support editor tooling.standardFilterscontains environment-neutral filters.standardFilterMetadatadescribes the standard registry for standalone validation.applyFiltersWithRegistry(value, filterString, registry, context)applies a filter chain when a host needs filter syntax outside a full render.htmlFiltersis available fromknap/html.
Development
pnpm install
pnpm checkKnap is available under the MIT License.
