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

v2.0.0

Published

Clean template language for writing HTML

Readme

Pugneum

Clean HTML templates for static sites.

Installation

npm install pugneum

Pugneum requires Node.js 22 or newer. Contributors running this repository's test and release tooling need Node.js 22.5 or newer and npm 10.9.9.

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>

The HTML output blocks throughout this document are indented and wrapped for readability. Pugneum does not add indentation or whitespace between elements, but it preserves newlines that are part of authored text, raw includes, and filter output. The tags, attributes, escaping, and ordering shown are exact; presentation-only indentation and wrapping are not byte-for-byte.

Pugneum is a variant of pug, modified to be fully static. All dynamic features have been removed. Only the clean language remains.

Structural whitespace—indentation, separators, and spacing within attribute lists—uses ASCII spaces, tabs, or physical line breaks. Non-ASCII whitespace is preserved in authored text and quoted attribute values, but rejected at a structural boundary with PUGNEUM:NON_ASCII_WHITESPACE.

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>

Inline tag shorthand

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>

The first word is the tag name and the rest is its content. Attributes are supported, as in #(a(href="/help") click here), and a mixin call can be inserted with #(+mixin(args)). Escape #( as \#( for literal output.

All block and inline tag names must begin with an ASCII letter. Their remaining characters may be ASCII letters, digits, underscores, hyphens, or colons, with a hyphen or colon only between word characters. Names such as x-card and svg:path are valid; digit- or underscore-led names fail with PUGNEUM:INVALID_TAG_NAME instead of producing markup that browsers do not parse as the requested element.

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 as opaque source; their contents are not evaluated:

//- 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"
}

baseDirectory is the include/extends confinement root. The CLI defaults it to inputDirectory, so both relative and /-prefixed template references stay inside the input project unless an explicit broader root is configured. An optional top-level compilationLimits object overrides the documented per-resource defaults for the entire directory build, including feeds. Unknown keys and values that are not non-negative safe integers are configuration errors.

The same boundary is named basedir in the JavaScript API and loader options. In other words, CLI configuration baseDirectory is passed to the compiler as basedir; inputDirectory and outputDirectory remain traversal/input and publication roots, respectively. These spellings are retained for compatibility, not separate security boundaries.

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

All configured paths are interpreted relative to the directory where the command runs. inputDirectory and outputDirectory are required non-empty strings. baseDirectory is optional; an omitted or empty value defaults to inputDirectory. A feeds object enables feed generation unless its enabled property is false; this requires the optional pugneum-feed package.

The command recursively visits the input tree in deterministic name order and compiles files whose names end in the case-sensitive .pg extension. It mirrors their relative directories under outputDirectory and changes the extension to .html. Nested symlink entries are skipped, as are files without the .pg extension. A configured symlinked input root itself is followed. If the output directory is inside the input tree, that output subtree is excluded from traversal. A .pg entry must be a regular file; a special file such as a FIFO is rejected without being opened.

Directory publication is transactional across pages and feeds. Pugneum first renders every page and optional feed into a private sibling staging directory; the published output remains untouched until all generation succeeds. It then commits the complete file set through one rollback-protected regular-file batch. A later commit failure restores replaced and removed files, removes fresh destinations, and leaves the previous manifest unchanged.

Successful builds write .pugneum-manifest.json, a reserved, versioned list of paths owned by Pugneum. On the next successful build, paths that disappeared from that list are removed in the same transaction. Files not listed in the manifest—such as copied stylesheets, images, or other user-managed assets—are preserved. Missing manifest paths are harmless; a malformed, unsupported, or unsafe manifest aborts publication rather than guessing which files are owned. The manifest and staged files are bounded by the shared compilation limits.

pugneum --help (or -h) prints usage, and pugneum --version (or -v) prints the installed version. Successful compilation is silent except for warnings. Warnings and errors go to stderr; warnings collected from earlier files are still emitted if a later file fails. When feeds are configured but pugneum-feed is not installed, the CLI warns, skips feed generation, and still succeeds.

The CLI uses these exit statuses:

| Status | Meaning | | ---: | --- | | 0 | Successful build, help, or version output | | 1 | Invalid argument, configuration, or input/output boundary | | 2 | Path not found | | 3 | Permission denied | | 4 | A directory was required | | 5 | A file was required | | 6 | Pugneum template error | | 7 | Feed generation error |

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 literal link text: shorthand-looking characters inside that fallback label are not parsed as markup. Supplying text after the URL explicitly opts that label into ordinary inline parsing. The URL must be nonempty. 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>

The image source must be nonempty. 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. Custom reference-link attributes cannot set href; that value always comes from the matching reference definition. Conflicts, including case variants such as HREF, are rejected as duplicate attributes.

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. They cannot set src or alt, which are owned by reference resolution; case variants such as SRC and ALT are rejected as duplicate attributes. 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 %(.

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:

references
  mccarthy https://example.com/mccarthy McCarthy's paper

p Details^[gc-tricolor] and history^[gc-history].

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. A footnote name is one or more ASCII letters, digits, hyphens, or underscores; definitions and references use exactly the same grammar, without surrounding or internal whitespace.

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     |

Install it with npm install pugneum-filter-table. 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. A heading is included when it has an explicit, usable string id, whether written with #id shorthand or an id="..." attribute. Empty IDs and IDs made only of ASCII whitespace are excluded.

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:

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

block 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. Textual raw includes normalize LF, CRLF, and CR line endings to LF before insertion or non-binary filtering; binary include filters receive exact bytes.

An included Pugneum template can use yield to choose where a block supplied by the caller is inserted:

//- wrapper.pg
article
  yield
include wrapper.pg
  p Included content.

Supplying a block to a template with no yield is an error. If the included template has several yield sites, each receives an independent copy of the caller block.

Include with a filter to transform the content:

head
  style
    include:verbatim styles.css

Include filters may have only text or html output types and must return a string. Chained include filters such as include:outer:inner file.txt run from right to left. If the innermost descriptor declares binary: true, it receives the exact file Buffer; every outer filter receives the preceding string result. Filter result types are preserved at every edge: text is escaped once when it crosses into html/structured output or reaches the document, while html stays raw unless a later text filter promises literal text. Repeated text stages do not double-escape. Ordinary block filters additionally support pugneum and syntax output types.

Paths starting with / are resolved from basedir. Relative paths resolve from the including file's directory. They must remain inside basedir when it is set, or inside the entry file's directory when it is omitted. The CLI always supplies its input/build boundary. Trusted callers that intentionally need unrestricted relative paths must opt out explicitly with allowUncontainedPathsForTrustedInput: true.

Include from npm packages with @:

include @pugneum-mixins/quote.pg

+quote(https://example.com/quotation)
  | To be or not to be.
  block caption
    +citation
      block attribution
        | Example Author
      block title
        | Example Work

Install the package first: npm install pugneum-mixins. The spelling @pkg/file.pg addresses an unscoped package; use a doubled prefix for a scoped package, as in @@scope/pkg/file.pg for @scope/pkg. Lookup begins in the including project's node_modules. If the package has an exports map, the requested .pg subpath must be exported; the package manifest itself does not need to be exported. The resolved target is contained to 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!');
  }

Only :verbatim is bundled with Pugneum. The other filters in this table are optional packages:

| Filter | Availability | 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 | bundled | Pass-through, no transformation |

Install the optional filter packages you use, for example:

npm install pugneum-filter-highlight.js pugneum-filter-prismjs pugneum-filter-table

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>

Inside a mixin, variables can be used in both text content and attribute values with the #{name} syntax; the names refer to the mixin's arguments. Outside a mixin there is no variable scope, so #{name} is an error. Escape with \#{ for literal output.

Each invocation has only its own parameter bindings: a callee does not implicitly capture variables from its caller. Forward a value explicitly by using #{name} in a nested call argument:

mixin label(text)
  strong #{text}

mixin button(text)
  button
    +label(#{text})

+button(Save)

Call-argument substitution is single-pass. Escape the marker as \#{name} when a nested call should receive the literal text #{name}.

Mixins can be called inline within text using #(+mixin(args)):

mixin icon(name)
  span(class="icon icon-#{name}" aria-hidden="true")

mixin b(text)
  strong #{text}

p Click the #(+icon(settings)) button to open preferences.
p I am #(+b(very)) #(+b(happy)) today.

Mixin call arguments are separated by ASCII spaces, tabs, or newlines. Balanced parentheses can be nested inside an unquoted argument that contains no separator whitespace, for example +transform(calc(1+2)). Quote an argument to preserve whitespace; the outer quotes are removed and a backslash escapes the next quoted character, as in +label('Status (ready)').

Declarations take effect in source order. A declaration evaluated inside a mixin invocation may shadow an outer declaration, but that local binding ends when the invocation returns. Unused-mixin warnings are tracked per declaration, including same-name redefinitions.

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 quotation
  figure
    blockquote
      block quote
    figcaption
      block attribution
        | Anonymous
      | ,&#32;
      cite
        block title
          | Untitled

+quotation
  block quote
    p To be or not to be.
  block attribution
    | William Shakespeare
  block title
    | Hamlet
<figure>
  <blockquote>
    <p>To be or not to be.</p>
  </blockquote>
  <figcaption>
    William Shakespeare, <cite>Hamlet</cite>
  </figcaption>
</figure>

Each slot can have default content. Omitted slots use their defaults; the attribution and title slots above default to "Anonymous" and "Untitled".

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 a block with that name. Presence is what matters: an explicitly provided but empty block name still counts. This enables wrapper elements that disappear when a slot is omitted:

mixin quote(source?)
  figure
    blockquote(cite="#{source?}")
      block
    given caption
      figcaption
        block caption

+quote(https://example.com/quotation)
  | Quoted text.
  block caption
    | Example Author,&#32;
    cite
      a(href='https://example.com/work') Example Work

+quote
  | No attribution needed.
<figure><blockquote cite="https://example.com/quotation">Quoted text.</blockquote><figcaption>Example Author, <cite><a href="https://example.com/work">Example Work</a></cite></figcaption></figure>
<figure><blockquote>No attribution needed.</blockquote></figure>

The second quote has no <figcaption> — given caption suppressed the entire subtree because the caller didn't provide block caption.

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",
    "atom": "feeds/site.atom.xml",
    "rss": "feeds/site.rss.xml"
  }
}

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 metadata. Feed and entry titles must be non-empty; Atom entries need either an article author or the feed-level fallback. Configured atom and rss names control both their output paths and public self links. An optional ISO-8601 buildDate pins the RSS build time and invalid-publication fallbacks; otherwise one build-start instant is shared by both formats.

See the pugneum-feed package for full configuration.

Escaping

In ordinary or pipeless Pugneum text, prefix a shorthand sigil with \ to output it literally:

| Escape | Output | |---|---| | \@( | @( | | \!( | !( | | \*( | *( | | \_( | _( | | \`( | `( | | \~( | ~( | | \&( | &( | | \^( | ^( | | \%( | %( | | \,( | ,( | | \?( | ?( | | \@[ | @[ | | \![ | ![ | | \^[ | ^[ | | \#{ | #{ | | \#( | #( |

The \#{ escape is also recognized in attribute values and literal code shorthand. In those contexts it suppresses variable interpolation and outputs #{ without the backslash.

Filter bodies are literal input to the selected filter, rather than Pugneum text. The lexer passes \#{ through unchanged there; any further handling is defined by that filter.

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

Programming interface

const pg = require('pugneum');

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

render(source, options) and renderFile(filename, options) synchronously return an HTML string. renderFile decodes the entry file as fatal UTF-8 and supplies its absolute filename to the compiler. Text sources reject malformed UTF-8 and ASCII controls other than tab, LF, and CR; invalid input cannot be silently replaced with U+FFFD or preserve NUL into HTML. Compiler failures use coded Pugneum errors, invalid public argument types use TypeError, and an entry-file read retains its Node.js filesystem error.

Options

| Option | Default | Description | | --- | --- | --- | | filename | | Entry source path, used for diagnostics and required by the default resolver for relative includes and extends; renderFile sets it automatically | | basedir | Entry file directory for relative paths | Explicit confinement root for absolute and relative filesystem includes/extends; required for absolute paths | | allowUncontainedPathsForTrustedInput | false | Explicitly disable the inferred entry-directory boundary for trusted templates and resolver results; cannot be combined with basedir | | filters | | Object mapping filter names to filter objects {type, filter}, where type is one of text/html/pugneum/syntax and filter(input, attrs) returns the transformed output | | filterOptions | | Per-filter options object, keyed by filter name | | resolve | Default filesystem/package resolver | Synchronous hook (requestedPath, includingFilename, options) => resolvedPath for dependency resolution | | read | fs.readFileSync | Synchronous hook (resolvedPath, options) => Buffer \| Uint8Array \| string for dependency reads | | canonicalize | Real path or resolved virtual name | Hook (resolvedPath, options) => identity that gives aliases a stable identity for cycle detection | | dependencyCache | | A Map scoped to one immutable multi-render build; reuses canonical dependency bytes and pre-load parsed ASTs while cloning each attachment. Reuse only with stable hooks, parser options, and inputs | | maxLoadDepth | 256 | Maximum include/extends dependency depth; an integer from 0 through 256 | | maxLinkDepth | 256 | Maximum linker composition depth; an integer from 0 through 256 | | compilationLimits | Generous local-build defaults | Exact non-negative safe-integer overrides for source/dependency/AST/materialization/filter/mixin/diagnostic/feed/output resources | | compilationContext | New context per call | Shared cumulative context returned by createCompilationContext(); cannot be combined with compilationLimits | | warnings | Automatic stderr emission | Mutable array to collect non-fatal diagnostics. If supplied, the caller owns emission and Pugneum does not write warnings to stderr |

Relative filesystem dependencies first resolve from the including file and must remain within the explicit basedir or the inferred entry-file directory. Only allowUncontainedPathsForTrustedInput: true disables that inferred root. Absolute dependencies require basedir. Installed @-prefixed library includes use package resolution and do not require an entry filename; their targets are confined to the package root.

pg.createCompilationContext(overrides?) creates the same budget used by the CLI. Pass it as compilationContext to several render/renderFile calls to bound the whole build rather than each page independently. pg.DEFAULT_COMPILATION_LIMITS exposes the frozen defaults. The shared context charges source and generated bytes, dependency edges, AST validation and traversal, cloned/yielded nodes, filter depth/invocations, mixin calls, diagnostics, feed entries, and final HTML/feed bytes. A limit failure has code PUGNEUM:COMPILATION_LIMIT_EXCEEDED plus resource, attempted, and limit.

Diagnostics

Template and compiler failures are thrown with a PUGNEUM:-prefixed code and a formatted message. When source information is available, diagnostics also expose filename, line, column, the plain msg, and a non-enumerable source. Source-byte failures additionally expose a zero-based byteOffset. Versioned JSON retains severity, short and display messages, and location but omits raw source unless trusted code explicitly calls toJSON({includeSource: true}). Public boundary violations instead use TypeError, and direct entry-file I/O retains standard Node.js error codes.

When warnings is omitted, render and renderFile collect warnings, remove duplicates, and print them to stderr automatically. They also print warnings collected before a later hard failure. Supplying a mutable array transfers ownership to the caller and keeps the library silent:

const pg = require('pugneum');
const warnings = [];

const html = pg.render('a(href=‘/docs’) Docs', {
  filename: 'page.pg',
  warnings,
});

pg.emitWarnings(warnings);

emitWarnings(warnings) is the stable formatter used by the CLI. It validates the complete array before writing anything, deduplicates records by code, filename, line, column, and formatted message, and writes each distinct warning to stderr. It removes a leading PUGNEUM: from the displayed header, but does not mutate the records. A record requires non-empty string code and string message fields; filename, line, and column are optional.

For a multi-page build, createWarningCollector() returns a mutable array that keeps only the first occurrence of each warning identity as records are pushed. It can be passed as warnings and later to emitWarnings, avoiding retention of duplicate formatted diagnostics while preserving first-seen order.

Compiler warnings currently use these codes:

| Code | Meaning | | --- | --- | | PUGNEUM:TYPOGRAPHIC_QUOTE_DELIMITER | A typographic quote was used literally where an ASCII attribute quote was likely intended | | PUGNEUM:DUPLICATE_ID | The final document contains the same string ID more than once | | PUGNEUM:IMG_WITHOUT_ALT | An img element has no alt attribute | | PUGNEUM:UNUSED_REFERENCE | A reference definition is not used | | PUGNEUM:UNUSED_FOOTNOTE | A footnote definition is not referenced | | PUGNEUM:EMPTY_TOC | A toc has no eligible headings | | PUGNEUM:UNUSED_MIXIN | An entry-file mixin is never called |

License

MIT