npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

pugneum

v1.2.0

Published

Clean template language for writing HTML

Readme

Pugneum

Clean HTML templates for static sites.

Installation

npm install pugneum

Syntax

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:

pugneum

Link 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 ![photo sunset](loading="lazy" class="hero")
<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 content

A 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 Hello

extends 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 Page

Included .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.css

Paths 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-feed

Add 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 | |---|---| | \@( | @( | | \!( | !( | | \*( | *( | | \_( | _( | | \`( | `( | | \~( | ~( | | \&( | &( | | \^( | ^( | | \%( | %( | | \,( | ,( | | \?( | ?( | | \@[ | @[ | | \![ | ![ | | \^[ | ^[ | | \#{ | #{ | | \#( | #( |

Tag names that collide with keywords can be escaped: \extends produces a literal <extends> tag.

Programming interface

const pg = require('pugneum');

let html = pg.render('h1 Hello, world!');
let html = pg.renderFile('page.pg');

Options

| Option | Default | Description | | --- | --- | --- | | filename | | Path to source file, required for includes and extends | | basedir | | Base directory for absolute include/extends paths | | filters | | Object mapping filter names to filter functions | | filterOptions | | Per-filter options object, keyed by filter name |

License

MIT