sommark
v5.3.0
Published
SomMark is a template language that compiles to multiple output formats — HTML, JSON, YAML, TOML, CSV, Markdown, XML, and more.
Maintainers
Readme
SomMark
SomMark is a template language that compiles to multiple output formats — HTML, JSON, YAML, TOML, CSV, Markdown, XML, and more.
Write your content once with a single consistent block syntax. Add loops, compile-time logic, and file imports. Compile to whatever format your project needs.
v5 is the official stable release and the recommended version to start with. SomMark has reached its main goal — a single consistent block syntax that compiles to any output format. v5 is the last major version. Future releases will be minor updates or patches.
Install
# CLI
npm install -g sommark
# Use it with your project
npm install sommarkShowcase
team.smark (HTML mapper)
[import = Card: "./components/Card.smark" !]
${
const team = [
{ name: "Adam", role: "Founder", avatar: "adam.jpg" },
{ name: "Hawa", role: "Engineer", avatar: "hawa.jpg" },
{ name: "Ilham", role: "Designer", avatar: "ilham.jpg" },
];
const year = new Date().getFullYear();
}$
[main = class: "team-page"]
[h1]Our Team[end:h1]
[section = class: "grid"]
[for-each = ${ team }$, as: "member"]
[Card = name: ${ member.name }$, role: ${ member.role }$]
[img = src: ${ member.avatar }$, alt: ${ member.name }$ !]
[end:Card]
[end:for-each]
[end:section]
[footer]© ${ year }$ SomMark[end:footer]
[end:main]components/Card.smark
[div = class: "card"]
[h2]v{name}[end:h2]
[p]v{role}[end:p]
[slot!]
[end:div]sommark --html team.smark<main class="team-page">
<h1>Our Team</h1>
<section class="grid">
<div class="card">
<h2>Adam</h2>
<p>Founder</p>
<img src="adam.jpg" alt="Adam">
</div>
<div class="card">
<h2>Hawa</h2>
<p>Engineer</p>
<img src="hawa.jpg" alt="Hawa">
</div>
<div class="card">
<h2>Ilham</h2>
<p>Designer</p>
<img src="ilham.jpg" alt="Ilham">
</div>
</section>
<footer>© 2026 SomMark</footer>
</main>Output formats
| Format | CLI Flag |
| -------- | -------------- |
| HTML | --html |
| Markdown | --markdown |
| MDX | --mdx |
| JSON | --json |
| JSONC | --jsonc |
| YAML | --yaml |
| TOML | --toml |
| CSV | --csv |
| XML | --xml |
| Text | --text |
Features
Blocks
Every tag is a block. Open with [name], close with [end:name]. A plain [end] also works, but [end:name] is recommended for readability.
[article = class: "post"]
[h1]Hello, SomMark[end:h1]
[p]Structured content, zero guesswork.[end:p]
[hr!]
[end:article]Self-closing blocks carry no body — append ! inside the tag:
[br!]
[img = src: "logo.png", alt: "Logo" !]
[input = type: "text", name: "email" !]Compile-time JavaScript
${ }$ runs JavaScript at build time inside a sandboxed QuickJS VM. The static keyword is optional — ${ expr }$ and static ${ expr }$ are identical.
The example below targets the HTML mapper:
${
import pkg from "./package.json";
const built = new Date().toISOString();
}$
[footer]
${ pkg.name }$ v${ pkg.version }$ — built ${ built }$
[end:footer]Variables declared in one ${ }$ block are available in all blocks that follow in the same file.
Loops
[for-each] iterates over any array. The current item is accessed via the as: alias. ${ i }$ gives the zero-based index.
The example below targets the HTML mapper:
${
const links = ["Home", "Docs", "Blog", "Contact"];
}$
[nav]
[ul]
[for-each = ${ links }$, as: "link"]
[li][a = href: "#"]${ link }$[end:a][end:li]
[end:for-each]
[end:ul]
[end:nav]<nav>
<ul>
<li><a href="#">Home</a></li>
<li><a href="#">Docs</a></li>
<li><a href="#">Blog</a></li>
<li><a href="#">Contact</a></li>
</ul>
</nav>Shorthand key-value for data formats
When targeting JSON, YAML, or TOML, write key-value pairs using the tag name as the key. Type is inferred automatically — numbers stay numbers, true/false become booleans, everything else becomes a string.
The example below targets the JSON mapper:
[Object]
[username = "Adam" !]
[age = 25 !]
[score = 9.8 !]
[isAdmin = false !]
[deletedAt = null !]
[end:Object]{
"username": "Adam",
"age": 25,
"score": 9.8,
"isAdmin": false,
"deletedAt": null
}Body form also works — useful for longer values:
[Object]
[bio]Software developer based in Hargeisa.[end:bio]
[end:Object]Generate config from data with a loop. The example below targets the YAML mapper:
${
const services = [
{ name: "api", port: 8080, replicas: 3 },
{ name: "worker", port: 8081, replicas: 2 },
];
}$
[seq = "services"]
[for-each = ${ services }$, as: "s"]
[map-item]
[name = ${ s.name }$ !]
[port = ${ s.port }$ !]
[replicas = ${ s.replicas }$ !]
[end:map-item]
[end:for-each]
[end:seq]services:
- name: "api"
port: 8080
replicas: 3
- name: "worker"
port: 8081
replicas: 2Placeholders
Inject values at build time from your JavaScript config using p{}. Add a fallback with | in case a value is not set.
The example below targets the HTML mapper:
[div = class: "env-badge"]
Environment: p{NODE_ENV}
[end:div]
[footer]Built with p{engine | "SomMark"}[end:footer]new SomMark({
src,
format: "html",
placeholders: { NODE_ENV: "production" }
});Module system
Split templates into reusable .smark files. All [import] declarations must appear at the top of the file.
Static injection — insert a module with no props or body content:
[import = Nav: "./components/Nav.smark" !]
[import = Footer: "./components/Footer.smark" !]
[body]
[$use-module = Nav !]
[main]
[p]Page content.[end:p]
[end:main]
[$use-module = Footer !]
[end:body]Component block — pass props and body content. Inside the component, v{} reads the props and [slot] marks where the body goes:
[import = Card: "./components/Card.smark" !]
[Card = title: "Featured Post"]
This content fills the slot inside the card.
[end:Card]Card.smark:
[div = class: "card"]
[h2]v{title}[end:h2]
[div = class: "body"]
[slot!]
[end:div]
[end:div]Comments
Completely removed at build time — never appear in the output:
# single-line comment
###
multi-line comment
write as much as you need
###CLI
Global
sommark -h, --help # show help message
sommark -v, --version # show version
sommark init # generate smark.config.js
sommark show config [file] # show resolved config
sommark show --path-config [file] # show path to active config file
sommark color on|off # help on enabling terminal colorsTranspile
sommark --html input.smark # compile to HTML
sommark --markdown input.smark # compile to Markdown
sommark --mdx input.smark # compile to MDX
sommark --xml input.smark # compile to XML
sommark --json input.smark # compile to JSON
sommark --jsonc input.smark # compile to JSONC
sommark --toml input.smark # compile to TOML
sommark --yaml input.smark # compile to YAML
sommark --csv input.smark # compile to CSV
sommark --text input.smark # compile to plain text
sommark --lex input.smark # print lexer token stream
sommark --parse input.smark # print parser ASTOutput
sommark --html -p input.smark # print output to console
sommark --html input.smark -o name ./dir/ # set output filename and directoryBrowser bundle
sommark bundle ./dist/ # copy full bundle (JS + WASM)
sommark bundle ./dist/ --lite # lite bundle — no WASM
sommark bundle ./dist/ --only-lexer # lexer only
sommark bundle ./dist/ --only-parser # lexer + parser onlyProgrammatic API
Node.js
import SomMark from "sommark";
const sm = new SomMark({
src,
format: "html",
placeholders: { title: "My App" }
});
const output = await sm.transpile();Browser
import SomMark, { resolveBaseDir, renderCompiledHTML } from "sommark/browser";
const src = await fetch("./main.smark").then(r => r.text());
const sm = new SomMark({
src,
format: "html",
baseDir: resolveBaseDir("./templates/"),
});
renderCompiledHTML(document.getElementById("app"), await sm.transpile());Configuration
Run sommark init to generate smark.config.js:
export default {
format: "html",
removeComments: true,
placeholders: {},
importAliases: { "@": "./" },
fallbackTarget: true,
outputValidator: null,
baseDir: null,
showSpinner: true,
security: {
allowRaw: true,
maxDepth: 5,
timeout: 5000,
allowFetch: true,
allowHttp: false,
allowedOrigins: [],
allowedExtensions: [],
sanitize: null,
},
outputDir: "./",
outputFile: "output",
};Security
All ${ }$ expressions run inside a QuickJS sandbox — isolated from the host process:
| Setting | Default | Description |
| ------------------- | ------- | ----------------------------------------- |
| allowRaw | true | Allow JavaScript in compile-time blocks |
| maxDepth | 5 | Maximum import nesting depth |
| timeout | 5000 | Max execution time per script (ms) |
| allowFetch | true | Allow network requests from scripts |
| allowHttp | false | Block plain HTTP — HTTPS only by default |
| allowedOrigins | [] | Restrict fetch to specific domains |
| allowedExtensions | [] | Restrict which file types can be imported |
| sanitize | null | Custom function to sanitize HTML output |
Custom mappers
Define your own tags and rendering logic with the Mapper API:
import SomMark, { Mapper } from "sommark";
const mapper = new Mapper();
mapper.register("alert", function({ content, props }) {
const type = props.type || "info";
return `<div class="alert alert-${type}">${content}</div>`;
});
const sm = new SomMark({
src: `[alert = type: "warning"]Check your config.[end:alert]`,
format: "html",
mapperFile: mapper,
});Full reference: docs/api/Mapper
Documentation
| Topic | Location |
| ---------------- | ------------------------------------------ |
| Syntax Reference | docs/syntax/ |
| Output Formats | docs/languages/ |
| Core API | docs/api/Core/ |
| Browser API | docs/api/Browser/ |
| Mapper API | docs/api/Mapper/ |
| Sandbox API | docs/api/Sandbox/ |
| CLI Guide | docs/cli/ |
| Configuration | docs/cli/config.md |
License
MIT — Adam Elmi
