pugneum
v1.2.0
Published
Clean template language for writing HTML
Readme
Pugneum
Clean HTML templates for static sites.
Installation
npm install pugneumSyntax
Pugneum is a clean, whitespace sensitive syntax for writing HTML. Here is a simple example:
html(lang="en")
head
title Example
script(type='text/javascript').
if (foo) {
bar(1 + 5);
}
body
h1 Pugneum
#container.centered
p.
Pugneum is a terse and simple templating language
with a focus on static pure HTML web sites.That code compiles to:
<html lang="en">
<head>
<title>Example</title>
<script type="text/javascript">
if (foo) {
bar(1 + 5);
}
</script>
</head>
<body>
<h1>Pugneum</h1>
<div class="centered" id="container">
<p>
Pugneum is a terse and simple templating language
with a focus on static pure HTML web sites.
</p>
</div>
</body>
</html>Pugneum is a variant of pug, modified to be fully static. All dynamic features have been removed. Only the clean language remains.
Text
Piped text
Use | at the start of a line to add text content:
p
| This is a paragraph
| with two lines.<p>This is a paragraph
with two lines.</p>Block text
A trailing . after a tag makes all indented content text:
p.
This entire block is text.
No tags are parsed here.<p>This entire block is text.
No tags are parsed here.</p>This is useful for preserving content in script or style tags:
script(type='text/javascript').
if (foo) {
bar();
}<script type="text/javascript">if (foo) {
bar();
}</script>Tag interpolation
Use #(tag content) to insert tags inline within text:
p This is #(strong very) important.
p Click #(a(href="/help") here) for help.<p>This is <strong>very</strong> important.</p>
<p>Click <a href="/help">here</a> for help.</p>Comments
Buffered comments appear in the HTML output:
// This comment is visible.
p Hello<!-- This comment is visible.-->
<p>Hello</p>Unbuffered comments (with -) are removed:
//- This is only in the source.
p Hello<p>Hello</p>Block comments indent their content:
//
This is a
block comment.<!--This is a
block comment.-->Doctype
Use doctype html to emit an HTML5 doctype declaration:
doctype html
html
head
title Page
body
p Hello<!DOCTYPE html><html><head><title>Page</title></head><body><p>Hello</p></body></html>Place it at the top of your root template.
Templates that extend a layout with doctype html
inherit the declaration automatically.
Usage
The command line utility requires a pugneum.json file to work:
{
"inputDirectory": "pg/files",
"outputDirectory": "example.com",
"baseDirectory": "pg"
}Committing this file to version control is recommended.
Once it exists, the pugneum templates can be compiled to HTML with a command line tool:
pugneumLink shorthand
The @() shorthand generates <a> tags inline:
p Visit @(https://example.com our site) for details.
p @(/contact Contact us)<p>Visit <a href="https://example.com">our site</a> for details.</p>
<p><a href="/contact">Contact us</a></p>If no text is provided, the URL is used as the link text.
Escape with \@( to output a literal @(.
Image shorthand
The !() shorthand generates <img> tags inline:
p See !(/photo.jpg a lovely photo) below.
p !(/logo.png Logo)(class="logo" loading="lazy")<p>See <img src="/photo.jpg" alt="a lovely photo"> below.</p>
<p><img class="logo" src="/logo.png" alt="Logo" loading="lazy"></p>If no alt text is provided, an empty alt="" is used (decorative image).
Custom attributes can be appended after the shorthand in parentheses.
Escape with \!( to output a literal !(.
Reference links
Define URLs once and reference them throughout the template:
references
docs https://docs.example.com
repo https://github.com/example/project
p Read @[docs the documentation] or browse @[repo the source].
p @[docs](class="external" target="_blank")<p>Read <a href="https://docs.example.com">the documentation</a>
or browse <a href="https://github.com/example/project">the source</a>.</p>
<p><a class="external" href="https://docs.example.com" target="_blank">docs</a></p>If no link text is given, the default text from the definition is used. If no default text was defined, the reference name is used. Define default text after the URL:
references
docs https://docs.com Documentation@[docs] renders as "Documentation". Explicit text overrides:
@[docs click here] renders as "click here".
References can be defined anywhere in the file, including via include.
Reference images
Like reference links, but for images. Uses ![ref alt] with URLs from a references block:
references
logo /images/logo.png
photo /images/sunset.jpg
p Our logo: ![logo Pugneum logo]
p <p>Our logo: <img src="/images/logo.png" alt="Pugneum logo"></p>
<p><img class="hero" src="/images/sunset.jpg" alt="sunset" loading="lazy"></p>If no alt text is given, the default text from the definition
is used. If no default text was defined, an empty alt=""
is used (decorative image).
Custom attributes can be appended after the shorthand in parentheses.
Escape with \![ to output a literal ![.
Strong shorthand
The *() shorthand generates <strong> tags inline:
p This is *(important) information.
p Nested: *(click @(/url here) now)<p>This is <strong>important</strong> information.</p>
<p>Nested: <strong>click <a href="/url">here</a> now</strong></p>Balanced parentheses in content are handled by depth tracking.
Escape with \*( to output a literal *(.
Emphasis shorthand
The _() shorthand generates <em> tags inline:
p Please use _(caution) here.
p Combined: *(really _(very) important)<p>Please use <em>caution</em> here.</p>
<p>Combined: <strong>really <em>very</em> important</strong></p>Escape with \_( to output a literal _(.
Code shorthand
The `() shorthand generates <code> tags inline.
Content is literal — no inner shorthand processing:
p Use `(git status) to check.
p Call `(printf("hello")) carefully.<p>Use <code>git status</code> to check.</p>
<p>Call <code>printf("hello")</code> carefully.</p>Balanced parentheses in code work via depth tracking.
Escape with \`( to output a literal `(.
Del shorthand
The ~() shorthand generates <del> tags for deleted/struck text:
p This feature is ~(deprecated).<p>This feature is <del>deprecated</del>.</p>Escape with \~( to output a literal ~(.
Ins shorthand
The &() shorthand generates <ins> tags for inserted text,
complementing ~() for deletions:
p Returns ~(NULL) &(nullptr) now.<p>Returns <del>NULL</del> <ins>nullptr</ins> now.</p>Escape with \&( to output a literal &(.
Abbr shorthand
The ?() shorthand generates <abbr> tags with title expansion:
p The ?(HTML Hypertext Markup Language) standard.
p Uses ?(CSS) for styling.<p>The <abbr title="Hypertext Markup Language">HTML</abbr> standard.</p>
<p>Uses <abbr>CSS</abbr> for styling.</p>First word is the visible abbreviation, rest is the title.
Without expansion text, generates <abbr> with no title.
Escape with \?( to output a literal ?(.
Sup and sub shorthands
^() generates <sup>, ,() generates <sub>:
p Footnote^(1) and x^(2) + H,(2)O.<p>Footnote<sup>1</sup> and x<sup>2</sup> + H<sub>2</sub>O.</p>Escape with \^(, \,( to output literal ^(, ,(.
Kbd shorthand
The %() shorthand generates <kbd> tags for keyboard input:
p Press %(Ctrl+C) to copy.<p>Press <kbd>Ctrl+C</kbd> to copy.</p>Escape with \%( to output a literal %(.
Inline tag shorthand
The #() shorthand wraps text in any tag:
p Click the #(button Start) to begin.
p This is #(mark highlighted) text.<p>Click the <button>Start</button> to begin.</p>
<p>This is <mark>highlighted</mark> text.</p>The first word is the tag name, the rest is content.
Attributes are supported: #(a(href="/help") click here).
Mixin calls work too: #(+mixin(args)).
Escape with \#( for literal output.
Footnotes
Define footnotes in a footnotes block and reference them
with ^[name]. Footnotes are numbered by order of first
appearance and generate bidirectional anchors with DPUB-ARIA
accessibility roles:
p The tricolor algorithm^[gc] is fundamental.
footnotes
gc Introduced by Dijkstra in 1978.<p>The tricolor algorithm<sup><a href="#footnote-gc"
id="footnote-reference-gc" role="doc-noteref">[1]</a></sup>
is fundamental.</p>
<section role="doc-endnotes">
<ol>
<li id="footnote-gc" role="doc-endnote">
Introduced by Dijkstra in 1978.
<a href="#footnote-reference-gc" role="doc-backlink">↩</a>
</li>
</ol>
</section>Multi-line definitions use indented content:
footnotes
gc-tricolor Short note.
gc-history
McCarthy's original Lisp used mark-and-sweep.
See @[mccarthy] for the original paper.Repeated references show the same number with multiple back-links. Footnote content supports all inline shorthands.
Table filter
The :table filter parses pipe-delimited table syntax and
generates full HTML tables with support for alignment,
attributes, captions, colgroups, and structural sections:
:table(class="data")
caption System calls
| Name | Count | Description |
| :--- | ----: | :---: |
| read | 100 | Read from fd |
| write | 50 | Write to fd |See the table filter package for full syntax documentation.
Table of contents
The toc keyword generates a table of contents from headings
that have explicit id attributes. Headings without IDs are
excluded — you opt in per heading:
h1 My Article
toc
h2#background Background
p Some text.
h3#prior-work Prior work
p More text.
h2#design Design<nav role="doc-toc" aria-label="Table of contents">
<ol>
<li><a href="#background">Background</a>
<ol>
<li><a href="#prior-work">Prior work</a></li>
</ol>
</li>
<li><a href="#design">Design</a></li>
</ol>
</nav>The ToC appears where the toc keyword is placed.
Only headings with #id are included — you control
exactly which sections appear.
Template inheritance
Templates can extend a layout and override named blocks.
A layout defines replaceable regions with block:
//- layout.pg
doctype html
html
head
block title
title Default Title
body
block contentA page extends it and fills the blocks:
//- page.pg
extends layout.pg
block title
title My Page
block content
h1 Hello
p Welcome.Compiling page.pg produces:
<!DOCTYPE html>
<html>
<head>
<title>My Page</title>
</head>
<body>
<h1>Hello</h1>
<p>Welcome.</p>
</body>
</html>Blocks can be appended or prepended instead of replaced:
extends layout.pg
append title
meta(name="description" content="My page")
block content
p Helloextends must be the first statement in the file.
Blocks not overridden keep their default content.
Includes
Insert the contents of another file with include:
html
head
include partials/head.pg
body
h1 My PageIncluded .pg files are parsed as pugneum.
Non-.pg files are included as raw text.
Include with a filter to transform the content:
head
style
include:verbatim styles.cssPaths starting with / are resolved from basedir.
Relative paths resolve from the including file's directory.
Include from npm packages with @:
include @pugneum-mixins-blog/citation.pg
+citation
block quote
p To be or not to be.Install the package first: npm install pugneum-mixins-blog.
The path resolves from the package root.
Filters
Filters transform blocks of text within templates.
Apply a filter with :filtername:
:highlight.js(language=javascript)
function hello() {
console.log('Hello!');
}Built-in filters:
| Filter | Package | Description |
|---|---|---|
| :highlight.js | pugneum-filter-highlight.js | Syntax highlighting via highlight.js |
| :prismjs | pugneum-filter-prismjs | Syntax highlighting via Prism |
| :table | pugneum-filter-table | Pipe-delimited table syntax |
| :verbatim | built-in | Pass-through, no transformation |
Install filter packages separately:
npm install pugneum-filter-highlight.js
Custom filters can be registered via the filters option
in the programming interface.
Mixins
Mixins define reusable template fragments with parameters:
mixin button(url text)
a(href="#{url}" class="btn") #{text}
+button(/home Home)
+button(/about About)<a class="btn" href="/home">Home</a>
<a class="btn" href="/about">About</a>Variables can be used in both text content and attribute values
with the #{name} syntax. Escape with \#{ for literal output.
Mixins can be called inline within text using #(+mixin(args)):
p Click the #(+icon(settings)) button to open preferences.
p I am #(+b(very)) #(+b(happy)) today.Mixins can also receive block content from the caller:
mixin card(title)
.card
h2 #{title}
.card-body
block
+card(Welcome)
p This is the card body content.<div class="card">
<h2>Welcome</h2>
<div class="card-body">
<p>This is the card body content.</p>
</div>
</div>Named blocks
When a mixin needs multiple content areas, named blocks provide multiple slots that callers fill independently:
mixin citation
figure
blockquote
block quote
figcaption
cite
block source
| Anonymous
+citation
block quote
p To be or not to be.
block source
| Shakespeare,
|
time(datetime="1600") circa 1600<figure>
<blockquote>
<p>To be or not to be.</p>
</blockquote>
<figcaption>
<cite>Shakespeare, <time datetime="1600">circa 1600</time></cite>
</figcaption>
</figure>Each slot can have default content.
Omitted slots use their defaults;
the source slot above defaults to "Anonymous".
A mixin may use both an unnamed block and named blocks.
Caller content not inside a named block fills the unnamed slot.
At the call site, block name replaces the slot's default content.
append name adds after it and prepend name adds before it,
mirroring template inheritance:
mixin nav
nav
block links
a(href="/") Home
+nav
append links
a(href="/about") About<nav><a href="/">Home</a><a href="/about">About</a></nav>Conditional rendering with given
given name renders its subtree only if the caller provides
content for the named block. This enables wrapper elements
that disappear when a slot is unused:
mixin quote(url?)
figure
blockquote(cite="#{url?}")
block
given source
figcaption
cite
block source
+quote(https://example.com)
| Quoted text.
block source
a(href='https://example.com') Author
+quote
| No attribution needed.<figure><blockquote cite="https://example.com">Quoted text.</blockquote><figcaption><cite><a href="https://example.com">Author</a></cite></figcaption></figure>
<figure><blockquote>No attribution needed.</blockquote></figure>The second quote has no <figcaption> — given source suppressed
the entire subtree because the caller didn't provide block source.
Use \given to create an HTML element named given.
Feeds
Generate Atom and RSS feeds from compiled HTML. Install the optional feed package:
npm install pugneum-feedAdd a feeds key to pugneum.json:
{
"inputDirectory": "pg",
"outputDirectory": "site",
"feeds": {
"url": "https://example.com"
}
}The feed generator reads compiled HTML to extract article metadata.
Articles are discovered from elements with data-published-at
attributes on the index page. Feed title, author, and description
are extracted from standard HTML meta elements.
See the pugneum-feed package for full configuration.
Escaping
Prefix any shorthand sigil with \ to output it literally:
| Escape | Output |
|---|---|
| \@( | @( |
| \!( | !( |
| \*( | *( |
| \_( | _( |
| \`( | `( |
| \~( | ~( |
| \&( | &( |
| \^( | ^( |
| \%( | %( |
| \,( | ,( |
| \?( | ?( |
| \@[ | @[ |
| \