Skip to content

nanolaba/readme-generator

[ en | ru ]

Nanolaba Readme Generator (NRG) - Automated Markdown Documentation Tool

NRG continuous integration build status NRG release on Maven Central NRG is open source under the Apache 2.0 license Built with Java 8+

NRG demo: generating README.md and README.ru.md from a single .src.md template

Stop hand-editing the same README in 5 languages. NRG generates README.md, README.ru.md, README.zh.md, and any other localized variant from one .src.md source. The built-in CI drift-check guarantees they never silently drift apart again.

NRG is a README generator and Markdown template engine that builds multi-language README files from a single .src.md source. Open-source Java 8+, ships as a CLI, a Maven plugin, a GitHub Action, and a Java library.

Overview

Using Nanolaba Readme Generator (NRG), you can:

  • Generate professional README files in multiple languages
  • Automate documentation with dynamic templates
  • Create maintainable Markdown with variables and widgets
  • Streamline GitHub project documentation

💡 Example: This document was generated from this template. Try our Quick Start Guide to begin!

Key Features

  • Multi-language READMEs - Support for EN/ZN/RU and any other languages
  • CI drift detection - the --check flag (CLI) and mode: check (GitHub Action) fail the build with a unified diff if generated .md files drift from the template — so a contributor's hand-edit can never silently land in main
  • Smart Variables - Reusable content blocks
  • Prebuilt Widgets - Table of contents, file import, TODOs, alerts, badges, and more
  • LaTeX math - Reliable formula rendering via $…$ / $$…$$ or an SVG fallback for places where GitHub's native MathJax breaks
  • Flexible Integration - CLI, Maven plugin, or Java library
  • Extensibility - Supports the ability to create custom widgets for content generation

💡 Nanolaba Readme Generator (NRG) is written in Java and requires Java 8 or higher to run.

The latest stable version of the program is 1.2. The current development version is 1.3-SNAPSHOT.

Used by

This very README is generated with NRG — see README.src.md. The same template is also used to keep README.ru.md in sync.

Table of contents

Quick start

Prerequisites: Java 8 or higher (download).

Step 1: Create a minimal template (README.src.md)

<!--@nrg.languages=en,ru-->

# Hello<!--en-->
# Привет<!--ru-->

English text<!--en-->
Русский текст<!--ru-->

Lines tagged <!--en--> go to README.md; lines tagged <!--ru--> go to README.ru.md.

Step 2: Generate the files

Option A — CLI. Download the standalone jar, unzip it, then run:

nrg -f /path/to/README.src.md

Option B — Maven plugin. Add this to your pom.xml (full configuration in the Maven plugin section below):

<plugin>
    <groupId>com.nanolaba</groupId>
    <artifactId>nrg-maven-plugin</artifactId>
    <version>1.2</version>
    <configuration>
        <file><item>README.src.md</item></file>
    </configuration>
    <executions>
        <execution>
            <phase>compile</phase>
            <goals><goal>create-files</goal></goals>
        </execution>
    </executions>
</plugin>

Step 3: Check the result

Two files appear next to the template — README.md and README.ru.md:

README.mdREADME.ru.md
# Hello

English text
# Привет

Русский текст

What comes next

  • Variables, language constructs, and escapes — see Template syntax.
  • Built-in widgets (table of contents, import, languages, date, todo, alert, badge, math, exec, if, fileTree) — see Widgets.
Full template example (all widgets)
<!--@nrg.languages=en,ru-->
<!--@nrg.defaultLanguage=en-->

<!--@title=**${en:'Hello, World!', ru:'Привет, Мир!'}**-->
<!--@version=1.0-->

${widget:languages}

${widget:badge(type='maven-central', coordinates='com.example:my-project')}

# ${title}<!--toc.ignore-->

Last updated: ${widget:date}

${widget:tableOfContents(title = "${en:'Table of contents', ru:'Содержание'}", ordered = "true")}

## Part 1<!--toc.ignore-->

### Chapter 1<!--toc.ignore-->

English text<!--en-->
Русский текст<!--ru-->

${widget:alert(type='note', text='${en:'Heads up!', ru:'Обратите внимание!'}')}

The area of a circle is ${widget:math(expr='\\pi r^2')}.

${widget:todo(text="${en:'Document the next chapter', ru:'Описать следующую главу'}")}

${widget:if(cond='endsWith(${version}, -SNAPSHOT)')}
This is a development build.
${widget:endIf}

${widget:exec(cmd='git rev-parse --short HEAD', codeblock='text')}

${widget:fileTree(path='src/main/java', depth='2', exclude='target,*.class')}

${widget:import(path='path/to/your/file/another-info.src.md')}

Usage

Using the Command Line Interface

Nanolaba Readme Generator (NRG) is written in Java and requires Java 8 or higher to run. Install Java if it’s not already present on your system.

Download the latest stable version of the application.

Unzip the downloaded archive. If you're using a Unix-like system, make the nrg.sh file executable:

chmod +x nrg.sh  

Now you can run the program to generate the files:

nrg -f /path/to/README.src.md

To see the list of available options for the console application, type:

nrg --help

Verifying generated files (CI mode)

Use --check to verify that files on disk match what NRG would generate right now. The flag is meant for CI / pre-commit hooks: no files are written, a missing or out-of-date target file prints a diff to stderr, and the process exits with status 1.

nrg --check -f README.src.md && echo "README is up to date"

--check validates every language configured via nrg.languages and is mutually exclusive with --stdout.

--check-paths <pattern> (repeatable) limits the check to outputs whose path matches one of the supplied glob patterns. Patterns use the same glob: syntax as multi-file source globs (**/ matches zero or more directories) and are resolved against the current working directory. Outputs that don't match any pattern are skipped from both the diff and the missing-file check — useful when only some generated files are tracked in git and the rest are regenerated by a bot. A pattern that matches nothing prints a stderr WARN (still exits 0) so typos do not silently disable the check. --check-paths requires --check.

nrg --check --check-paths README.md -f README.src.md

Validating source templates

Use --validate to scan the template (and every reachable ${widget:import}-imported file) for authoring mistakes without generating any output. v1 covers four classes of error:

  • unregistered widget names (${widget:doesNotExist}),
  • language markers <!--xx--> whose code is not in nrg.languages,
  • ${widget:import(path='...')} paths that do not exist on disk,
  • unbalanced <!--nrg.ignore.begin--> / <!--nrg.ignore.end--> pairs.
nrg --validate -f README.src.md && echo "Template is clean"

Each diagnostic is printed as ERROR: file.src.md:LINE: message. With no errors, NRG exits silently with status 0. With at least one error, all diagnostics are printed to stderr and the process exits with status 1. --validate is mutually exclusive with --check and --stdout.

Print to stdout

Use --stdout to stream generated output to standard output instead of writing files to disk. Combine with --language <code> to print only a single language variant; without it, every configured variant is printed, prefixed with a separator line like === README.ru.md === so the output can be split by downstream tools.

nrg --stdout -f README.src.md
nrg --stdout --language en -f README.src.md

The --language flag is only meaningful with --stdout — using it on its own logs a warning and the flag is ignored.

Logging verbosity

Control how much NRG prints to the console with --log-level. Accepted values are trace, debug, info (default), warn, and error — each level suppresses messages below it. The environment variable NRG_LOG_LEVEL is consulted when --log-level is not supplied, which is convenient for CI and the Maven plugin. Invalid values abort the run with a usage error on stderr.

nrg --log-level warn -f /path/to/README.src.md
NRG_LOG_LEVEL=warn nrg -f /path/to/README.src.md

Customising output filenames

Use --file-name-pattern <PATTERN> to override the output filename layout. Placeholders: <base> (source filename without .src.md), <lang> (language code as written), <LANG> (uppercased). Patterns may contain / separators — intermediate directories are created on demand. Equivalent to setting <!--@nrg.fileNamePattern=PATTERN--> in the template; the CLI flag wins. Use --default-language-file-name-pattern <PATTERN> to override the default language only (mirrors <!--@nrg.defaultLanguageFileNamePattern=PATTERN-->). Per-language overrides (<!--@nrg.fileNamePattern.<lang>=...-->) are template-only and have no CLI counterpart.

nrg --file-name-pattern '<base>_<LANG>.md' -f README.src.md
nrg --file-name-pattern 'docs/<lang>/<base>.md' --default-language-file-name-pattern '<base>.md' -f README.src.md

Line endings

--line-ending=auto|lf|crlf controls the line-ending convention of the generated output. Default auto preserves the existing on-disk file's convention (CRLF stays CRLF, LF stays LF) and falls back to the platform default for first-time generation. Use lf or crlf to pin the output unconditionally — useful for Windows contributors regenerating Linux-LF repos and vice versa.

nrg --line-ending lf -f README.src.md
nrg --line-ending crlf -f README.src.md

In --check mode, auto ignores LE-only differences against the on-disk file (regen would have preserved the existing convention anyway), so a mixed-OS contributor base does not trip CI. Explicit lf / crlf does flag a mismatch — that's the user-asked-for invariant.

Header customisation

By default every generated file opens with two HTML comment lines warning contributors not to hand-edit the output. Two flags override that: --no-header strips the comment entirely; --header-text "..." replaces it with arbitrary text (use \n for line breaks, also \r, \t, \\). The two flags are mutually exclusive. Equivalent template properties <!--@nrg.noHeader=true--> and <!--@nrg.headerText=...--> work the same way; the CLI flag wins when both are set.

nrg --no-header -f README.src.md
nrg --header-text '<!-- See /wiki for editing rules -->\n<!-- Auto-generated; do not edit -->' -f README.src.md

Multiple files and glob patterns

Pass several source files in one invocation, either as explicit positional arguments or as glob:-style patterns:

nrg README.src.md docs/Guide.src.md
nrg "docs/**/*.src.md"

Glob expansion uses java.nio.file.PathMatcher with glob: syntax, so behavior is identical on Windows, Linux, and macOS regardless of the shell. Quote the pattern to keep it from being expanded by the shell first. The legacy -f <file> flag is still supported as a single-file alias and is mutually exclusive with positional arguments — passing both prints an error and exits with status 1.

By default each input is processed independently: an error on one file is logged but the rest still run, and the overall exit code is 1 if any file failed. Pass --fail-fast to stop on the first non-zero result.

nrg --fail-fast "docs/**/*.src.md"

A pattern that matches no files logs a warning. If the entire input set matches nothing, the run exits with status 1 and the message No source files matched any of the supplied patterns.

Use as maven plugin

Add the following code to your pom.xml:

<plugins>
    <plugin>
        <groupId>com.nanolaba</groupId>
        <artifactId>nrg-maven-plugin</artifactId>
        <version>1.2</version>
        <configuration>
            <file>
                <item>README.src.md</item>
                <item>another-file.src.md</item>
            </file>
            <logLevel>warn</logLevel>
            <widgets>
                <widget>com.example.MyWidget</widget>
                <widget>com.example.OtherWidget</widget>
            </widgets>
            <check>false</check>
            <failFast>false</failFast>
            <lineEnding>auto</lineEnding>
            <noHeader>false</noHeader>
        </configuration>
        <dependencies>
            <dependency>
                <groupId>com.example</groupId>
                <artifactId>my-widgets</artifactId>
                <version>1.0.0</version>
            </dependency>
        </dependencies>
        <executions>
            <execution>
                <phase>compile</phase>
                <goals>
                    <goal>create-files</goal>
                </goals>
            </execution>
        </executions>
    </plugin>
</plugins>

Each <file> entry can be a literal path or a glob pattern (same glob: syntax as the CLI). Multi-doc projects can replace a long list with <file>**/*.src.md</file> and let NRG expand it. Set <failFast>true</failFast> (or -DfailFast=true) to abort on the first non-zero result; default false preserves today's behavior of aggregating diagnostics across every matched file.

<noHeader>true</noHeader> (or -DnoHeader=true) drops the auto-generated two-line head comment from every output file; <headerText>...</headerText> (or -DheaderText="...") replaces it with arbitrary text. Use \n for line breaks. The two parameters are mutually exclusive — passing both fails the build with a CLI parse error.

Note

Multi-module (aggregator) projects: the create-files goal is declared with inheritByDefault = false, so child modules of a <packaging>pom</packaging> aggregator do not re-run NRG in their own working directory by default (where README.src.md typically does not exist). Declare the plugin once in the parent POM and the goal runs only at the root. A child module that genuinely needs its own README generation must opt in explicitly with <inherited>true</inherited> in its own POM.

The <widgets> entries must name public classes that implement NRGWidget and declare a public no-argument constructor, and their artifact must be listed under the plugin's own <dependencies> so Maven can resolve them. On a name collision, POM-declared widgets override those declared via the nrg.widgets template property.

Set <check>true</check> (or pass -Dcheck=true on the command line) to run the plugin in verification mode: no files are written, and the build fails with a MojoExecutionException and a diff in the log when the generated output diverges from the committed files. Handy for a mvn verify step in CI to guard against stale READMEs.

Set <validate>true</validate> (or pass -Dvalidate=true) to scan the templates for authoring mistakes (unknown widgets, missing imports, undeclared language markers, unbalanced ignore-blocks) without generating any output. The build fails with a MojoExecutionException when diagnostics are reported. Mutually exclusive with <check>.

<fileNamePattern> and <defaultLanguageFileNamePattern> (or -DfileNamePattern=... / -DdefaultLanguageFileNamePattern=...) mirror the --file-name-pattern / --default-language-file-name-pattern CLI flags and override nrg.fileNamePattern / nrg.defaultLanguageFileNamePattern template properties when set.

To use SNAPSHOT versions, you also need to add the following code to your pom.xml:

<pluginRepositories>
    <pluginRepository>
        <id>central.sonatype.com-snapshot</id>
        <url>https://central.sonatype.com/repository/maven-snapshots</url>
        <releases>
            <enabled>false</enabled>
        </releases>
        <snapshots>
            <enabled>true</enabled>
            <updatePolicy>always</updatePolicy>
        </snapshots>
    </pluginRepository>
</pluginRepositories>

Use as a GitHub Action

NRG ships as a composite GitHub Action so any repository can regenerate multi-language README files in CI without installing Maven or Java locally. The action optionally sets up Java, downloads the requested NRG release zip, extracts nrg.jar, and runs it against the templates you list — README maintenance becomes a single workflow step.

Quickstart

- uses: actions/checkout@v4
- uses: nanolaba/nrg-action@v1
  with:
    file: README.src.md

Inputs

Name Description Default
file Path to the .src.md template (relative to working-directory). Use files for multiple.
files Multi-line list of templates (one per line). Mutually exclusive with file.
charset Source file encoding. UTF-8
mode Operation mode: generate, check, or validate. generate
check-paths Multi-line list of glob patterns (one per line) limiting which generated outputs mode: check compares against on-disk files. Requires mode: check.
nrg-version NRG release tag (e.g. v1.0) or latest. latest
java-version JDK version for actions/setup-java. Ignored when setup-java=false. 17
java-distribution JDK distribution for actions/setup-java. temurin
setup-java Whether the action should install Java itself. Set to "false" if Java is already set up earlier in the job. true
log-level NRG log verbosity: trace, debug, info, warn, or error. info
working-directory Directory in which NRG runs. .

mode semantics:

  • generate writes README.md, README.ru.md, … to disk (default).
  • check is a read-only verification: if files on disk differ from what NRG would generate, it exits with a non-zero status and prints a unified diff. Use this on pull requests.
  • validate scans the template for authoring mistakes (unknown widgets, undeclared language markers, missing imports, unbalanced ignore-blocks). No files are written.

Outputs

Name Description
version Resolved NRG version (e.g. v1.0). Useful with nrg-version=latest.
changed-files Newline-separated list of files written or modified by NRG.

Examples

Basic generate

Regenerate the README on every push to main:

name: Regenerate README
on:
  push:
    branches: [main]
permissions:
  contents: read
jobs:
  regenerate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: nanolaba/nrg-action@v1
        with:
          file: README.src.md
Drift check on PR

Fail the build when a contributor edits README.md directly instead of regenerating it from README.src.md:

name: README drift check
on:
  pull_request:
    paths: ['**/*.src.md', '**/*.md']
permissions:
  contents: read
jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: nanolaba/nrg-action@v1
        with:
          file: README.src.md
          mode: check
Drift check on a subset of outputs

When only some generated files are tracked in git (e.g. only the canonical README.md is committed and translations are bot-managed), check-paths limits the comparison to those files. Patterns are cwd-relative glob: globs (**/ matches zero or more directories). A pattern that matches no generated output prints a WARN to stderr but still exits 0, so typos do not silently disable the check:

- uses: nanolaba/nrg-action@v1
  with:
    file: README.src.md
    mode: check
    check-paths: |
      README.md
      docs/canonical/*.md

Validate-only and auto-commit-via-PR recipes are in the nrg-action/examples directory of this repository.

Multi-file projects

Pass a multi-line list to the files: input (one path per line). All files are processed in a single action invocation; the jar is downloaded only once. file and files are mutually exclusive — set exactly one.

- uses: nanolaba/nrg-action@v1
  with:
    files: |
      README.src.md
      docs/CONTRIBUTING.src.md

Skipping the built-in setup-java

If the surrounding workflow already installs Java (for example, for a Maven build), opt out of the built-in actions/setup-java@v4 step. Composite-action inputs are strings, so use the quoted "false", not the YAML boolean:

- uses: actions/setup-java@v4
  with:
    distribution: temurin
    java-version: '21'
- uses: nanolaba/nrg-action@v1
  with:
    file: README.src.md
    setup-java: 'false'

Pinning the action version

Use @v1 for auto-updates within the v1 major (recommended), @v1.0 to lock to a single minor, or @<full-sha> to pin a specific commit (most secure; required by some supply-chain policies).

Troubleshooting

Most CI failures fall into three buckets: download issues (verify the nrg-version exists on the Releases page), zip-layout regressions (file an issue if nrg.jar not found inside … appears), and platform quirks. The most common quirk is line-ending drift on windows-latestmode: check reports diffs that do not appear locally.

  • Line-ending drift on Windows runners — add a .gitattributes line * text=auto eol=lf and re-commit the regenerated files.
  • Windows: unzip: command not found — git-bash on windows-latest normally ships unzip; on rare images install it via choco install unzip in a preceding step.

The action is published as a standalone repository nanolaba/nrg-action (tags v1.0 and rolling v1) — that is the address consumers reference via uses:. This nrg-action/ subdirectory is the dev workspace where the action and its regression tests live; releases are mirrored to the standalone repo. A GitHub Marketplace listing is the next step.

Use as a java-library

Maven (pom.xml)

<dependency>
    <groupId>com.nanolaba</groupId>
    <artifactId>readme-generator</artifactId>
    <version>1.2</version>
</dependency>  

Gradle (build.gradle)

implementation 'com.nanolaba:readme-generator:1.2'

Manual download

Get the JAR from Maven Central. Add it to your project's classpath.

After this, you can call the file generation function in your project by passing the same parameters as in the console application, for example:

NRG.main("-f","path-to-file","--charset","UTF-8");

An alternative approach — and a more flexible one for configuring program behavior — is to use the Generator class:

package com.nanolaba.nrg.examples;

import com.nanolaba.nrg.core.GenerationResult;
import com.nanolaba.nrg.core.Generator;
import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

public class GeneratorExample {

    public static void main(String[] args) throws IOException {

        Generator generator = new Generator(new File("template.md"), StandardCharsets.UTF_8);

        for (GenerationResult generationResult : generator.getResults()) {

            FileUtils.write(
                    new File("result." + generationResult.getLanguage() + ".md"),
                    generationResult.getContent(),
                    StandardCharsets.UTF_8);
        }
    }
}

Template syntax

Variables

The template syntax supports the use of variables. Variables are defined using the following construct:

<!--@variable_name=variable value-->

The output of variable values is done using the following construct:

${variable_name}

To display a construct like ${...} without replacing it with the variable's value, precede it with the '\' character:

\${variable_name}
Usage exampleResult
<!--@app_name=My Application-->
<!--@app_version=**1.0.1**-->
<!--@app_descr=This is *${app_name}* version ${app_version}-->
${app_name} version ${app_version}
${app_descr}
\${app_descr}
My Application version **1.0.1**
This is *My Application* version **1.0.1**
${app_descr}

Backslash escapes

After every substitution and widget has run, the root generator does one final pass over the output and strips a backslash in only these three patterns; every other \\X reaches the output verbatim.

In your template In the output Use it to
\$ $ suppress any \${…} reference (property / language / env / pom / npm / gradle / widget). Applies to any \$, not only \${…}.
<\!-- <!-- suppress any HTML-comment marker — language tag, nrg.ignore, nrg.freeze, property declaration. Use it to put a literal <!--…--> into the output.
<!--\@ <!--@ render <!--@key=value--> cleanly inside an example — cosmetic only, does not stop NRG from parsing the line as a real property declaration. To actually suppress parsing, escape the comment opener with rule 2: <\!--@key=value-->.

Markdown's own escape sequences (\(, \), \_, \*, \\, \`, etc.) are not on this list — they pass through NRG unchanged and reach the markdown renderer untouched.

Tip

Rule 1 strips the backslash from any \$, not only \${…}. To keep a literal \$ in the output (e.g. to render [\$] in a markdown link without triggering a property lookup), write \\$ in the template — the trailing \$ becomes $, the leading \ survives.

Properties

Using the syntax for setting variable values in the template, you can specify application properties, for example:

<!--@nrg.languages=en,ru-->
<!--@nrg.defaultLanguage=en-->

Available properties:

nrg.languages

For each language, except the default language, a file will be generated with the name in the format source.language.md, where source is the name of the original file and language is the name of the language. The default value of this property is "en"

nrg.defaultLanguage

The language in which the main documentation file will be generated. The language name should be included in the list defined by the property nrg.languages. The default value of this property is the first element in the nrg.languages list.

nrg.widgets

Comma-separated fully-qualified class names of custom NRGWidget implementations to register alongside the built-ins. Each class must be on the runtime classpath and declare a public no-argument constructor. Equivalent to the CLI --widgets <FQCN,FQCN,...> flag and the <widgets> parameter of the Maven plugin.

nrg.pom.path

Override the pom.xml location used by ${pom.NAME} substitution. Relative paths are resolved against the source-file directory; absolute paths are used as-is. Defaults to pom.xml next to the source file. Only consulted when the template uses ${pom.…} references.

nrg.npm.path

Override the package.json location used by ${npm.NAME} substitution. Relative paths are resolved against the source-file directory; absolute paths are used as-is. Defaults to package.json next to the source file. Only consulted when the template uses ${npm.…} references.

nrg.gradle.path

Override the Gradle location used by ${gradle.NAME} substitution. May point at either a directory (containing gradle.properties and/or build.gradle{,.kts}) or an explicit build script file. Relative paths are resolved against the source-file directory; absolute paths are used as-is. Defaults to the source-file directory. Only consulted when the template uses ${gradle.…} references.

nrg.fileNamePattern

Output filename pattern applied to all languages. Placeholders: <base> (source filename without .src.md), <lang> (language code as written), <LANG> (language code upper-cased). May include / separators — intermediate directories are created on demand. Defaults to <base>.md for the default language and <base>.<lang>.md for the others. Examples: <base>_<LANG>.md, <base>-<lang>.md, docs/<lang>/<base>.md.

nrg.defaultLanguageFileNamePattern

Override nrg.fileNamePattern for the default language only. Useful when the default language should keep the bare README.md while other languages get a suffix: nrg.fileNamePattern=<base>_<LANG>.md plus nrg.defaultLanguageFileNamePattern=<base>.md.

nrg.fileNamePattern.<lang>

Per-language override (e.g. nrg.fileNamePattern.zh-CN=README_<LANG>.md). Beats both nrg.fileNamePattern and nrg.defaultLanguageFileNamePattern for that exact language. Most-specific-first resolution: per-language → default-language → global → built-in. If two configured languages would resolve to the same output path, generation aborts.

Per-language overrides

Any property may declare a per-language override by suffixing its name with .<lang>. When the template is rendered for a specific language, ${name} first resolves to the value of name.<lang> if defined, otherwise it falls back to the bare name. If neither is defined, the literal ${name} is left in the output.

<!--@nrg.languages=en,ru,ja-->
<!--@screenshot.en=./public/show-en.png-->
<!--@screenshot.ru=./public/show-ru.png-->
<!--@screenshot=./public/show.png-->

<img src="${screenshot}" />

Result for en: <img src="./public/show-en.png" />
Result for ru: <img src="./public/show-ru.png" />
Result for ja (no per-language override): <img src="./public/show.png" />

The same convention is also used by built-in NRG properties such as nrg.fileNamePattern.<lang>.

Environment variables

Inside any ${…} reference, the reserved env. namespace pulls a value from the process environment. Resolution happens before language and property substitution, so ${env.NAME} works in raw body text, inside <!--@key=value--> declaration values, and inside widget parameters.

${env.BUILD_NUMBER}
${env.RELEASE_URL:https://github.com/nanolaba/readme-generator/releases}
<!--@buildNumber=${env.BUILD}-->
${widget:badge(type='custom', label='build', message='${env.BUILD_NUMBER:unknown}', color='blue')}

Behaviour:

  • ${env.NAME} — substitutes the value of NAME from the environment. If unset, logs one warning per distinct name per generation and renders an empty string.
  • ${env.NAME:default} — substitutes the env value when set (even if empty), otherwise the literal default after the first :.
  • Names must match the POSIX identifier pattern [A-Za-z_][A-Za-z0-9_]*. Dotted names like ${app.version} fall through to the regular property resolver.
  • Backslash escapes work as for any other ${…} reference: \\${env.NAME} renders as the literal text.

Warning

The substitution reads whatever System.getenv() exposes. On shared CI machines, treat the generated README as exposing every environment variable it references — do not template ${env.AWS_SECRET_…} into a public document.

Maven POM values

The reserved pom. namespace inside ${…} reads values directly from the project's pom.xml. Resolution happens after env substitution but before language and property substitution, so the same ${pom.…} reference works in body text, in <!--@key=value--> declaration values, and inside widget parameters.

${pom.version}
${pom.groupId}:${pom.artifactId}:${pom.version}
${pom.scm.url}
${pom.parent.version}
${pom.properties.java.version}
${pom.version:0.0.0-SNAPSHOT}
<!--@coords=${pom.groupId}:${pom.artifactId}-->

Behaviour:

  • The path is interpreted as a walk from the implicit <project> root: pom.X reads <X>, pom.X.Y reads <X><Y>, and so on.
  • pom.properties.KEY is a flat-map lookup: the remainder of the path is used verbatim as the <properties> child element name (so dotted keys like java.version work).
  • pom.parent.X reads the local <parent> block as written. Cross-file parent POM traversal is out of scope.
  • For unqualified pom.groupId, pom.version, and pom.name: if the element is absent under <project>, the value is taken from <project><parent> (Maven's standard inheritance rules).
  • POM values may themselves reference ${prop}, ${project.X}, and ${env.NAME} / ${env.NAME:default} — NRG resolves a single pass against the same POM and the template-level env provider.
  • ${pom.path:default} substitutes the literal default after the first : when the path is missing. Without a default, the substitution renders empty and one warning per distinct path is logged.
  • Backslash escapes work as for any other ${…} reference: \\${pom.version} renders as the literal text.
  • The pom.xml location defaults to the source-file directory; override with <!--@nrg.pom.path=relative/or/absolute/pom.xml-->.

npm package values

The reserved npm. namespace inside ${…} reads values directly from the project's package.json. Resolution happens after pom substitution but before language and property substitution, so the same ${npm.…} reference works in body text, in <!--@key=value--> declaration values, and inside widget parameters.

${npm.version}
${npm.name}
${npm.dependencies.lodash}
${npm.version:0.0.0-SNAPSHOT}
<!--@coords=${npm.name}@${npm.version}-->

Behaviour:

  • The path is interpreted as a walk from the JSON root: npm.X reads the top-level field, npm.X.Y walks into the nested object, and so on.
  • String, number, and boolean leaves are stringified. Object, array, and null leaves render empty (with a warning).
  • ${npm.path:default} substitutes the literal default after the first : when the path is missing. Without a default, the substitution renders empty and one warning per distinct path is logged.
  • Backslash escapes work as for any other ${…} reference: \\${npm.version} renders as the literal text.
  • The package.json location defaults to the source-file directory; override with <!--@nrg.npm.path=relative/or/absolute/package.json-->.

Gradle values

The reserved gradle. namespace inside ${…} reads values from the project's gradle.properties (flat key=value lookup) and falls back to regex extraction of version / group from build.gradle or build.gradle.kts. Resolution happens after npm substitution but before language and property substitution.

${gradle.version}
${gradle.group}
${gradle.kotlin.version}
${gradle.version:0.0.0-SNAPSHOT}

Behaviour:

  • gradle.X first looks up the verbatim key X in gradle.properties. If not found and X is version or group, NRG regex-extracts X = '...' from the build script (works for both Groovy and Kotlin DSLs).
  • gradle.properties always wins over the build script when the same key is defined in both places.
  • Other Gradle DSL constructs are intentionally not parsed — define values in gradle.properties if you need them in the README.
  • ${gradle.path:default}, backslash escapes, and warn-once-per-missing-path semantics match ${pom.…} and ${npm.…}.
  • Override the Gradle location with <!--@nrg.gradle.path=core/--> (a directory) or <!--@nrg.gradle.path=core/build.gradle.kts--> (an explicit file).

Multilanguage support

To write text in different languages, there are two methods available. The first one involves using comments at the end of the line, for example:

Some text<!--en-->
Некоторый текст<!--ru-->

The second method involves using a special construct:

${en:"Some text", ru:"Некоторый текст"}
${en:'Some text', ru:'Некоторый текст'} 

To escape quotes, use character doubling, for example:

  • ${en:'It''s working'}It's working
  • ${en:"Text with ""quotes"""}Text with "quotes"

Ignoring content

To mark a fragment as an author note that must not appear in any generated file, use the nrg.ignore markers. They work in the root template and inside imported files.

  • <!--nrg.ignore--> — drops the entire line containing the marker.
  • <!--nrg.ignore.begin--> ... <!--nrg.ignore.end--> — drops all lines of the block, including the markers themselves.

If an <!--nrg.ignore.begin--> has no matching <!--nrg.ignore.end-->, an error is logged and everything from the opening marker to the end of the file is dropped. A lone <!--nrg.ignore.end--> without a preceding begin is also logged as an error and removed from the output.

Usage exampleResult
Visible line.
This is a TODO<!--nrg.ignore-->
<!--nrg.ignore.begin-->
Author notes that should not leak
into the generated README.
<!--nrg.ignore.end-->
Another visible line.
Visible line.
Another visible line.

Frozen regions

Frozen regions let NRG-generated files coexist with third-party tools that mutate the generated file directly — for example akhilmhdh/contributors-readme-action, GitHub Sponsors widgets, or RSS embedders. On regeneration, NRG copies the current on-disk content between the freeze markers into the freshly-generated output instead of materialising whatever the template contains in the same span.

## Contributors

<!--nrg.freeze id="contributors"-->
<!-- readme: contributors -start -->
<!-- contents managed by akhilmhdh/contributors-readme-action -->
<!-- readme: contributors -end -->
<!--/nrg.freeze-->

The block content in the template is a bootstrap placeholder: it lands in the output only the first time the file is generated (when no on-disk version exists). On every subsequent run, NRG reads the existing output file, finds the matching id, and splices its current content into the new output — so any external edits inside the block survive regeneration. Edits outside the block are still overwritten as usual.

Attributes:

id — required, non-empty, must be unique within a template.

source-lang — ${en:'optional. Names a single language declared in nrg.languages. When present, the block content for every language is sourced from that one language\'s output file.', ru:'опциональный. Указывает один язык из nrg.languages. Когда указан, содержимое блока для каждого языка берётся из выходного файла этого одного языка.'}

The source-lang mode covers the common case where an external tool only writes to one language's output (e.g. contributors-action only knows about README.md), but the resulting content (an HTML table of avatars, etc.) is language-agnostic and should appear in every language file:

<!--nrg.freeze id="contributors" source-lang="en"-->
placeholder
<!--/nrg.freeze-->

Modes:

Attributes Behaviour
id="X" Each language file is independent: when generating README.md NRG reads the freeze content from README.md, when generating README.ru.md from README.ru.md.
id="X" source-lang="en" The block appears in every language file, but the content for all of them is sourced from the en output (README.md).

Restricting a freeze to one language:

<!--nrg.freeze--> itself has no lang attribute. To make a freeze block appear only in one language, wrap it in a ${widget:if} block driven by a per-language property:

<!--@onlyEn.en=1-->

${widget:if(cond='${onlyEn}')}
<!--nrg.freeze id="ru-only-block"-->
placeholder
<!--/nrg.freeze-->
${widget:endIf}

When generating README.md (en), ${onlyEn} resolves to 1 (truthy) and the block stays. When generating README.ru.md, ${onlyEn} resolves to the empty string (falsy) and the entire block is dropped before NRG even sees the freeze markers.

Important properties:

  • Open and close markers must each be on their own line.
  • Markers must not nest. Nesting is a validation error.
  • Disk content is opaque to NRG: ${...} references and <!--@key=value--> declarations inside the freeze block content from disk are not interpreted. Only the bootstrap placeholder in the template goes through the rendering pipeline (once, at first generation).
  • Markers themselves are written to the output verbatim, including original whitespace — they have to be there for the next regeneration to find the block.
  • Freeze blocks work transparently across ${widget:import(...)} — markers in imported files bubble up to the root output and are resolved against the root output file.

Validation:

The following are template authoring errors — they fail generation with exit 1 and are reported by --validate:

  • missing or empty id;
  • duplicate id within the same template;
  • unbalanced markers (open without close, or stray close);
  • nested freeze blocks;
  • source-lang referencing a language not declared in nrg.languages;
  • unknown attributes (only id and source-lang are allowed).

The following are on-disk anomalies caused by the external tool or manual edits — they emit LOG.warn once and fall back to the bootstrap placeholder, without aborting generation:

  • id declared in the template not found in the on-disk file;
  • malformed disk block (e.g. missing close);
  • duplicate id on disk — the first occurrence wins;
  • source-lang file does not exist on disk yet (treated as bootstrap).

Widgets

Widgets allow you to insert programmatically generated text into a document. If you are using Nanolaba Readme Generator (NRG) as a Java library, you can write your own widget. How to do this is explained in the Advanced Features section.

Widget 'languages'

This component allows you to generate links to other versions of a document (written in other languages).

Usage example Result Displayed result
${widget:languages} 
[ **en** | [ru](README.ru.md) ]

[ en | ru ]


Widget 'import'

This component enables text import from another document, code file, or template. Optionally selects a fragment by line range or named region, and wraps the result in a fenced code block.

Basic usage example
${widget:import(path='path/to/your/file/document.txt')} 
${widget:import(path='path/to/your/file/document.txt', charset='windows-1251')} 
${widget:import(path='path/to/your/file/template.src.md')} 
${widget:import(path='path/to/your/file/template.src.md', run-generator='false')}
Code import example
${widget:import(path='Foo.java', region='example', wrap='true')} 
${widget:import(path='Foo.java', lines='10-20', wrap='true')} 
${widget:import(path='Foo.java', lines='10-20,30-35', wrap='true')} 
${widget:import(path='Foo.java', lang='go', wrap='true')} 
${widget:import(path='Foo.java', region='example', wrap='true', dedent='false')}

Widget parameters:

Name Description Default value
path Path to the imported file
charset File encoding UTF-8
run-generator Should the system perform text generation when importing template files true
lines Line range(s) to extract: e.g. 10-20, 10-, -20, 15, 10-20,30-35
region Name of a region marked in the source file
wrap Wrap output in a code fence: true, false false
lang Language tag for the fence; auto detects from file extension auto
dedent Strip common leading whitespace: auto, true, false auto
heading-offset Shift ATX heading levels by an integer (clamped to 1..6); cannot combine with wrap='true' 0
url HTTP(S) URL to fetch (mutually exclusive with path); requires nrg.allowRemoteImports=true
cache Cache TTL: <int>{s,m,h,d} or none (e.g. 1h, 7d) none
timeout HTTP timeout: <int>{s,m,h,d} (cannot be none) 60s
sha256 64 hex chars; verifies fetched bytes; recommended for reproducibility

When importing a template file, generation is performed using variables declared in the parent file. This allows defining global variables in the root file and reusing them across all imported templates.

Wrapping in a code fence

By default (wrap='false'), the widget emits the selected content as-is, which preserves the behavior of existing template-composition imports (*.src.md files). To wrap a code fragment in a fenced block, set wrap='true' explicitly. The fence language is taken from lang, or detected from the file extension when lang='auto' (the default).

Auto-dedent

When dedent='auto' (the default), common leading whitespace is stripped automatically if lines or region is set, and left untouched otherwise. Use dedent='true' or dedent='false' to force the behavior explicitly.

Region markers

Mark a region in the source file using nrg:begin:NAME and nrg:end:NAME tokens inside any comment. The matching is language-agnostic — the widget recognizes the markers regardless of the surrounding comment syntax:

Comment style examples
// nrg:begin:example          (Java, JavaScript, Kotlin, Go, Rust, C, C++, C#)
<!-- nrg:begin:example -->    (HTML, XML, Markdown)
/* nrg:begin:example */       (CSS, C-style block comments)
-- nrg:begin:example          (SQL, Lua, Haskell)
# nrg:begin:example           (Python, Ruby, Bash, YAML, TOML)

Region names match the pattern [A-Za-z0-9_-]+. Region markers are stripped from the output. Nested regions are supported — when extracting an outer region, inner region markers are also stripped from the output.

Heading offset

heading-offset='N' shifts every ATX heading (#, ##, …, ######) in the imported content by N levels — useful when an imported .src.md owns its own heading hierarchy but is being included under a parent section. Levels are clamped to [1, 6]; clamped headings emit a single aggregated warning per import call. Lines inside fenced code blocks (``` or ~~~) are not shifted, so a # bash comment inside a fence stays a # bash comment. Setext-style headings (==== / ----) and indented (4-space) code blocks are not detected — prefer ATX headings and fenced code blocks in imports that use this parameter. Combining heading-offset with a non-zero value and wrap='true' fails the build.

Remote imports

The url parameter fetches content over HTTP(S) and is mutually exclusive with path. Remote imports are opt-in: the template must set nrg.allowRemoteImports=true as a standard NRG property marker, otherwise any url= invocation fails the build with a clear error.

The cache directory defaults to ~/.nrg/cache and can be overridden by setting the nrg.cacheDir template property to the desired path. The cache parameter sets the TTL using the grammar <int>{s,m,h,d} (or none to disable caching), and timeout accepts the same grammar but cannot be none (default 60s).

The sha256 parameter (64 hex chars) is strongly recommended — it pins the fetched bytes for reproducible builds and supply-chain safety. When sha256 is omitted, NRG logs an INFO line with the actual hash so it can be copied back into the widget call. For CI gates, set the system property -Dnrg.requireSha256ForRemote=true (default false) — remote imports without sha256 will then fail the build.

If the network is unreachable but a cached response exists, NRG uses it (even if stale relative to cache TTL) and logs a warning; without any cache entry the build fails.

Secure remote import example
${widget:import(url='https://raw.githubusercontent.com/myorg/shared-docs/main/CONTRIBUTING.md',
                 cache='1h',
                 sha256='abc123...')}

Error semantics

All import errors — both local and remote — now fail the build with a non-zero exit code. This is a behavior change from earlier NRG versions, where local-import errors silently produced empty content. The only non-throw cases are stale-cache fallback when the network is unreachable (warn-only) and cache filesystem hiccups (cache is skipped, fetch continues).


Widget 'tableOfContents'

This component allows you to generate a table of contents for a document. The table of contents is created from headers formed using the hashtag symbol (#). Headers located above the widget in the text are ignored.

If you need to exclude a header from the table of contents, you should mark it with a comment <!--toc.ignore-->.

Lines that look like headings but are part of a fenced code block (opened with three or more backticks or tildes), of an indented code block (4+ leading spaces), of an inline code span, or of a backslash-escaped heading are not treated as headings and do not appear in the table of contents.

Usage example (README.src.md)
# Title of the document

## Abstract

${widget:tableOfContents(title = "${en:'Table of contents', ru:'Содержание'}", ordered = "true")}

## Part 1

### Chapter 1

### Chapter 2

### Chapter 3

### Ignored Chapter<!--toc.ignore-->

## Part 2

## Part 3
Result (README.md)
# Title of the document

## Abstract

## Table of contents

1. [Part 1](#part-1)
    1. [Chapter 1](#chapter-1)
    2. [Chapter 2](#chapter-2)
    3. [Chapter 3](#chapter-3)
2. [Part 2](#part-2)
3. [Part 3](#part-3)

## Part 1

### Chapter 1

### Chapter 2

### Chapter 3

### Ignored Chapter

## Part 2

## Part 3

Widget parameters:

Name Description Default value
title Title of the table of contents
ordered Should the items in the table of contents be numbered false
min-depth Minimum heading level to include (1–6). Headings shallower than this are skipped. 1 includes top-level # headings. 2
max-depth Maximum heading level to include (1–6). Headings deeper than this are skipped. 6
min-items Minimum number of headings (after depth and <!--toc.ignore--> filters) required to render the widget. Below this threshold the widget produces no output (no title, no items). 1
anchor-style Anchor-slugification style: github (default), gitlab, or bitbucket. GitLab preserves underscores and does not collapse consecutive hyphens; Bitbucket prefixes anchors with markdown-header-. An unknown value logs an error and causes the widget to render nothing. github
numbering-style Counter-prefix style when ordered=true: default (today's 1. markers — byte-identical to omitting the parameter), hierarchical dotted (1, 1.1, 1.1.1), legal (1., 1.1., 1.1.1.), appendix (A, A.1, A.1.1), or flat global counters arabic / roman / roman-upper / alpha / alpha-upper. Unknown values log an error and fall back to default. No effect when ordered=false. default
start First top-level counter value. Type-matched to numbering-style: digits for dotted / legal / arabic, roman numeral for roman / roman-upper, single letter for alpha / alpha-upper / appendix. Invalid input logs an error and falls back to the natural first value. Ignored when numbering-style=default.

Numbering styles:

Set numbering-style=... (with ordered="true") to pick a counter shape — hierarchical for outline-style references, flat for short summaries.

Usage — `numbering-style="dotted"` (README.src.md)
${widget:tableOfContents(ordered = "true", numbering-style = "dotted", min-depth = "1")}

# Introduction

## Setup

## Usage
Result (README.md)
- 1 [Introduction](#introduction)
    - 1.1 [Setup](#setup)
    - 1.2 [Usage](#usage)
Usage — `numbering-style="legal"` (README.src.md)
${widget:tableOfContents(ordered = "true", numbering-style = "legal", min-depth = "1")}

# Introduction

## Setup

## Usage
Result (README.md)
- 1. [Introduction](#introduction)
    - 1.1. [Setup](#setup)
    - 1.2. [Usage](#usage)
Usage — `numbering-style="appendix"` (README.src.md)
${widget:tableOfContents(ordered = "true", numbering-style = "appendix", min-depth = "1")}

# Appendix One

## Tables

# Appendix Two
Result (README.md)
- A [Appendix One](#appendix-one)
    - A.1 [Tables](#tables)
- B [Appendix Two](#appendix-two)
Usage — `numbering-style="arabic"` (README.src.md)
${widget:tableOfContents(ordered = "true", numbering-style = "arabic")}

## Introduction

## Setup

## Usage
Result (README.md)
- 1 [Introduction](#introduction)
- 2 [Setup](#setup)
- 3 [Usage](#usage)

Widget 'date'

This component allows you to insert the current date into a document.

Usage exampleResult
Last updated: ${widget:date}
Last updated: 01.05.2026 19:23:24
${widget:date(pattern = 'dd.MM.yyyy')}
01.05.2026

Widget parameters:

Name Description Default value
pattern Pattern according to which the date will be formatted dd.MM.yyyy HH:mm:ss

You can read more about date pattern syntax in the Java documentation.


Widget 'todo'

This component allows you to insert prominently highlighted text into the document, indicating that work on this fragment has not yet been done.

Usage example
${widget:todo(text="${en:'Example message', ru:'Пример сообщения'}")}
Result
<pre>📌 ⌛ Example message</pre>
Displayed result
📌 ⌛ Example message

Widget parameters:

Name Description Default value
text Displayed text Not done yet...

Widget 'alert'

This component renders a GitHub-flavored alert block (> [!NOTE], > [!WARNING], and so on) so you don't have to hand-type blockquote syntax in your source templates.

Usage exampleResult
${widget:alert(type = 'note', text = 'Hello')}
> [!NOTE]
> Hello
${widget:alert(type = 'warning', text = 'Line 1\nLine 2')}
> [!WARNING]
> Line 1
> Line 2

Widget parameters:

Name Description Default value
type Alert kind: note, tip, important, warning, or caution (case-insensitive). Unknown values log an error and produce empty output.
text Body of the alert. Use \n to split into multiple quoted lines and \\ for a literal backslash. Language-substitution constructs in the outer template are resolved before the widget runs, so per-language text works naturally. ''

Widget 'badge'

This component generates Markdown image links for shields.io badges of common project-status flavors (Maven Central version, license, GitHub release / stars / workflow) and a free-form custom variant.

Usage example:

${widget:badge(type = 'maven-central', coordinates = 'com.nanolaba:readme-generator')}

Result:

Maven Central

Supported types and their parameters:

type Required parameters Optional parameters
maven-central coordinates — Maven coordinates groupId:artifactId. alt — override the default alt-text Maven Central.
license value — license identifier (e.g. Apache-2.0). url — link target; omitted → non-clickable badge. alt — override the default alt-text License: <value>.
github-release repo — repository owner/name. alt — override the default alt-text GitHub release.
github-stars repo — repository owner/name. alt — override the default alt-text GitHub stars.
github-workflow repo — repository owner/name; workflow — workflow filename (e.g. ci.yml). name — alt text; defaults to the workflow filename without extension. branch — filter by branch, appended as ?branch=.... alt — override the alt-text (wins over name).
custom label — left side of the badge; message — right side of the badge; color — shields.io color keyword or hex. url — link target; omitted → non-clickable badge. alt — override the default alt-text (defaults to label).

The optional alt parameter sets the Markdown image alt-text without changing the visible badge label rendered by shields.io. Useful for SEO and accessibility — search engines and screen readers see phrases like NRG continuous integration build status instead of bare labels like CI. Empty alt='' falls back to the type's default.

Unknown type values and missing required parameters log an error and produce no output.


Widget 'math'

This component renders LaTeX math formulas. GitHub's inline $…$ / block $$…$$ support can be flaky around \text, punctuation, or table cells — when that bites, switch to the SVG renderer to get a pre-rendered image via a LaTeX-to-SVG endpoint.

Inline LaTeX with the default native renderer:

${widget:math(expr = '\\pi r^2')}

Block-level LaTeX (use display = 'block' to wrap with $$…$$):

${widget:math(expr = '\\sum_{i=0}^{n} x_i', display = 'block')}

SVG fallback (renderer = 'svg') for cases where GitHub's native MathJax mis-parses the formula. The full time-dependent Schrödinger equation, with nested fractions, partial derivatives, and Greek letters, renders as a single image that GitHub displays inline:

${widget:math(expr = 'i\\hbar\\,\\frac{\\partial\\Psi}{\\partial t}=-\\frac{\\hbar^2}{2m}\\,\\nabla^2\\Psi+V\\Psi', renderer = 'svg', display = 'block')}

Rendered result:

i\hbar,\frac{\partial\Psi}{\partial t}=-\frac{\hbar^2}{2m},\nabla^2\Psi+V\Psi

Widget parameters:

Name Description Default value
expr LaTeX source. Every backslash must be doubled (so \\pi produces a single \pi, \\sum produces \sum, and so on). Missing or empty values log an error and produce no output.
display inline renders $…$; block renders $$…$$ for the native renderer, or prepends \\displaystyle for the svg renderer. Unknown values log an error and produce no output. inline
renderer native emits GitHub MathJax delimiters. svg emits a Markdown image that links to a LaTeX-to-SVG service — use it when native rendering mis-parses the formula. Unknown values log an error and produce no output. native
alt Alt text used by the svg renderer. Defaults to the raw expression. expr
service URL prefix of the LaTeX-to-SVG endpoint; the URL-encoded expression is appended to it. Used only by the svg renderer. https://latex.codecogs.com/svg.image?

Tips / caveats:

  • Curly braces { / } inside expr work as in LaTeX (subscripts, superscripts, \\text{…}, \\frac{…}{…}).
  • Raw ( and ) are not allowed inside expr because the widget-tag parser uses them as delimiters — wrap them with \\left( / \\right) for LaTeX grouping.
  • The svg renderer depends on an external service, so generated images break if the endpoint disappears. Self-host or pin a known-good URL via service for long-lived docs.
  • Pre-rendering, MathML output, and LaTeX linting are out of scope.

Widget 'exec'

This component runs an external command and embeds its stdout into the generated document — useful for pasting in --help output, version dumps, a CLI banner, or whatever else a build tool will print.

Warning

Opt-in for security. Execution is disabled by default. The widget only runs commands when the caller explicitly asks for it via the --allow-exec CLI flag (or <allowExec>true</allowExec> in the Maven plugin). Running nrg over an untrusted template without that flag is safe: every ${widget:exec(...)} call logs an error and renders empty output.

Usage exampleBehaviour
${widget:exec(cmd = 'java -jar nrg.jar --help')}

Runs java -jar nrg.jar --help, inlines stdout verbatim (with trailing whitespace trimmed).

${widget:exec(cmd = 'git rev-parse --short HEAD', codeblock = 'text')}

Runs the command and wraps stdout in a fenced code block tagged text.

${widget:exec(cmd = './scripts/list-langs.sh', cwd = 'docs', timeout = '5')}

Runs the script from the docs/ sub-directory of the source file, kills it if it exceeds 5 seconds.

Widget parameters:

Name Description Default value
cmd Command line. Whitespace-split into argv; no shell interpolation, so pipes, redirects, and variable expansion do not work. Missing or blank values log an error and produce no output.
cwd Working directory. Relative paths are resolved against the source-file directory; absolute paths are used as-is. Missing directory logs an error and produces no output. source-file directory
timeout Maximum duration in seconds (positive integer). The subprocess is force-killed on timeout and a warning is logged; output is empty. 30
trim true strips trailing whitespace/newlines from stdout; false preserves them. true
codeblock When present, wraps stdout in a fenced code block with this language tag (codeblock="" wraps without a tag). When absent, stdout is inlined raw. absent (no wrapping)

Enabling execution:

  • CLI: pass the --allow-exec flag to the nrg command.
  • Maven: add <allowExec>true</allowExec> to the nrg-maven-plugin configuration (or set the -DallowExec=true property).
  • Library: call generator.getConfig().setExecAllowed(true) before the first getResult(...) call.

Error handling:

  • Non-zero exit → error in the log (with exit code and stderr snippet) + empty output.
  • Command not found / IO error → error in the log + empty output.
  • Timeout → warning in the log + empty output.
  • Invalid timeout or trim → error in the log + empty output (command is not run).

Widget 'if'

This is a block widget: it spans an opening ${widget:if(cond='…')} tag and a matching ${widget:endIf} tag, and decides whether the lines between them appear in the generated output. When the condition is false the entire block — including any inner widgets — is dropped before the per-line pipeline runs, so widgets in dead branches never execute.

The block is kept when ${devVersion} ends with -SNAPSHOT; otherwise the entire block (the markers and the body) is removed:

${widget:if(cond='endsWith(${devVersion}, -SNAPSHOT)')}
> Snapshot build — expect breaking changes.
${widget:endIf}

Combines short-circuit && with != and ! — the right side is not even resolved when the left is false:

${widget:if(cond='${env.CI}!=true && !${dryRun}')}
This message only appears outside CI and outside dry runs.
${widget:endIf}

startsWith / endsWith are case-sensitive; an empty needle is always true:

${widget:if(cond='startsWith(${repoUrl}, https://github.com/) || startsWith(${repoUrl}, git@github.com:)')}
Hosted on GitHub.
${widget:endIf}

Condition grammar (precedence low → high):

Form Meaning
X truthy — true iff X.trim() is non-empty
!X falsy — true iff X.trim() is empty
X == Y equality (trim each side; quoted strings preserve whitespace)
X != Y inequality
A && B and (short-circuit)
A || B or (short-circuit)
(expr) grouping
startsWith(h, n) true iff h.startsWith(n); case-sensitive
endsWith(h, n) true iff h.endsWith(n); case-sensitive

Operands are placeholders (${…}), quoted strings ('…' or "…" with doubled-quote escape), or bare strings. Quoted strings preserve whitespace and protect operator characters; placeholders inside quoted strings are still resolved.

Type rules:

  • Every value is a string; there are no numbers, booleans, or null.
  • A ${msg} resolving to a && b is treated as opaque text — operators inside placeholder values are not reinterpreted as boolean operators.
  • No implicit case folding or numeric coercion: ${env.CI}==True does not match the env value true. Normalise upstream.

Errors:

  • An unclosed ${widget:if} block is reported via LOG.error and everything from the outermost open marker to EOF is dropped from the output.
  • A stray ${widget:endIf} (no matching open) is reported via LOG.error and the marker line is dropped.
  • A malformed condition (unbalanced parens, trailing operators, unknown function names) is reported via LOG.error and the block is treated as if the condition were false.

Out of scope (v1): numeric comparisons (>, <, …), regex matching, else/elif, scripting engine integration, ${…} resolution inside the default of ${env.X:default} references on the right-hand side of an ==.


Widget 'fileTree'

This component renders a tree -L-style directory listing with Unicode box-drawing characters. Use it to embed an always-current view of a folder's structure into the README without hand-maintaining ASCII art.

Lists the directory contents one level deep, wrapped in a fenced code block:

${widget:fileTree(path = 'nrg/src/main/java/com/nanolaba/nrg/widgets', depth = '1')}

Two-level listing with build artefacts and IDE folders excluded via comma-separated globs:

${widget:fileTree(path = '.', depth = '2', exclude = 'target,.idea,.git,*.class')}

Three-level directory-only outline, emitted raw without a code fence:

${widget:fileTree(path = 'nrg/src', depth = '3', dirsOnly = 'true', codeblock = 'false')}

Live example — ${widget:fileTree(path='../../nrg/src/', dirsOnly = 'true', depth='3')}:

src
├── main
│   ├── assembly
│   ├── java
│   │   └── com
│   └── resources
└── test
    ├── java
    │   └── com
    └── resources
        ├── ImportWidgetTest
        ├── LanguagesWidgetTest
        └── fixtures

Widget parameters:

Name Description Default value
path Directory to list. Relative paths are resolved against the source-file directory; absolute paths are used as-is. Missing or non-directory paths log an error and produce no output.
depth Recursion limit (positive integer). 1 lists only direct children; 2 descends one level deeper, and so on. 2
exclude Comma-separated glob patterns. Each pattern is matched against both the entry name and the entry's path relative to path — so target skips a folder named target anywhere, and sub/drop.txt targets one specific file. (none)
dirsOnly true lists directories only; files are hidden. false
codeblock true wraps the output in a fenced code block; false emits raw text. true

Behaviour:

  • Entries are sorted directories-first, then alphabetically within each group, for stable byte-exact output that survives --check.
  • Glob syntax follows java.nio.file.PathMatcher (*, ?, **, {a,b}, [abc]).
  • Symbolic links are followed as regular directories or files; cycles are not detected — keep depth finite.

Advanced features

Creating a widget

To create a widget, you need to implement the NRGWidget interface or extend an existing widget (e.g., DefaultWidget):

public class ExampleWidget extends DefaultWidget {

    @Override
    public String getName() {
        return "exampleWidget";
    }

    @Override
    public String getBody(WidgetTag widgetTag, GeneratorConfig config, String language) {
        String parameters = widgetTag.getParameters();
        Map<String, String> map = NRGUtil.parseParametersLine(parameters);

        return "Hello, " + map.get("name") + "!";
    }
}

Now you can use the widget in your template:

${widget:exampleWidget(name='World')}

Before running the generator, the widget must be registered with NRG. There are two ways to do this:

Option 1: via the NRG.addWidget static method:

NRG.addWidget(new ExampleWidget());
NRG.main("--charset", "UTF-8", "-f", "/path/to/your/file.src.md");

Option 2: via the Generator class constructor that accepts a widget list:

import com.nanolaba.nrg.core.GenerationResult;
import com.nanolaba.nrg.core.Generator;

import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Collections;

Generator generator = new Generator(
        new File("README.src.md"),
        StandardCharsets.UTF_8,
        Collections.singletonList(new ExampleWidget()));

Collection<GenerationResult> results = generator.getResults();

Option 3: register widgets directly from the template via the nrg.widgets property, or from the CLI via --widgets (with --classpath pointing to the JAR that contains the classes):

<!--@nrg.widgets=com.acme.widgets.Tag,com.acme.widgets.Banner-->
nrg --classpath my-widgets.jar --widgets com.acme.widgets.Tag,com.acme.widgets.Banner -f README.src.md

Each widget class must be public and expose a public no-argument constructor. NRG falls back to a helpful error message on the console if the class cannot be found, doesn't implement NRGWidget, or throws during instantiation.

Related projects

Other tools in the same space — useful if Nanolaba Readme Generator (NRG) does not fit your stack or workflow. ✅ = supported, ➖ = partial, ❌ = not supported.

Feature NRG ml-md doctoc embedme cog gitdown md-magic remark
Stack Java 8 Python Node Node Python Node Node Node
Multi-lang output¹
File imports
Auto TOC
Variables
Custom widgets ✅²
Maven plugin
GitHub Action
Frozen regions³

¹ one source → many language files (README.md, README.ru.md, …).
² cog runs arbitrary Python — extensible by definition, but no widget API.
³ preserve content written by external tools (contributors-readme-action, sponsors widgets, RSS) across regenerations.

Changelog

This section summarises the main user-visible changes in each release. For full details, see the git history.

Unreleased (1.3-SNAPSHOT)

1.2

  • Scope --check to specific files: new --check-paths CLI flag, <checkPaths> Maven plugin parameter, and check-paths GitHub Action input limit drift detection to outputs matching the supplied glob: patterns (literal paths or globs, repeatable). Useful for the "only the canonical README is tracked, translations are regenerated by a bot" workflow — currently the choice is "commit every generated file" (drift-check works) or "commit none" (drift-check unusable). Patterns are cwd-relative, follow the same **/ semantics as multi-file source globs, and a typoed path that matches nothing emits a stderr WARN so silent misconfiguration is visible. --check-paths requires --check; without the filter, every declared language is checked as before. Closes #53.
  • Customisable head comment: new --no-header CLI flag (and matching <noHeader>true</noHeader> Maven plugin parameter) suppresses the auto-generated two-line <!-- This file was automatically generated... --> block at the top of every output file; --header-text "..." (<headerText>...</headerText>) replaces it with arbitrary text — \n, \r, \t, \\ are interpreted, so a multi-line custom header is one flag away. The two flags are mutually exclusive. Equivalent template properties <!--@nrg.noHeader=true--> and <!--@nrg.headerText=...--> work identically; CLI / Maven values win on conflict. Lets maintainers running their own auto-doc tooling (e.g. per-file @LastEditTime HTML comments) opt out of NRG's signature without forking the generation step. Closes #51.
  • badge widget — optional alt= parameter: every type (maven-central, license, github-release, github-stars, github-workflow, custom) now accepts an optional alt='...' that overrides the auto-derived Markdown alt-text without changing the visible badge label rendered by shields.io. Useful for SEO and screen-reader accessibility — phrase-style alts like NRG continuous integration build status carry semantic signal that bare CI does not. Empty alt='' falls back to the type's default. Closes #52.
  • Preserve original line endings on regeneration: NRG now detects the existing on-disk output file's CRLF/LF convention and writes the regenerated content with the same convention, instead of always emitting System.lineSeparator(). Mixed-ending files normalise to LF. New CLI flag --line-ending=auto|lf|crlf (default auto) and matching Maven plugin <lineEnding> parameter override detection. In --check mode, auto ignores LE-only differences so a CRLF-vs-LF contributor mismatch no longer trips CI; explicit lf / crlf still flag a mismatch (that's the explicitly-requested invariant). Closes #48.
  • nrg-maven-plugin no longer inherits its create-files execution into child modules by default: the goal is declared with inheritByDefault = false, so multi-module (aggregator) builds run README generation only at the root POM where README.src.md lives — child modules no longer re-invoke NRG in their own directory and spam warnings (or fail) over a missing source file. Soft-breaking only for projects that intentionally relied on per-child re-execution; opt back in with <inherited>true</inherited> in the child's POM. Closes #49.
  • tableOfContents widget — numbering-style and start parameters: when ordered='true', pick a counter shape from nine values — default (today's 1. markers, byte-identical to omitting the parameter), hierarchical dotted (1, 1.1, 1.1.1), legal (1., 1.1., 1.1.1.), appendix (A, A.1, A.1.1), or flat global counters arabic / roman / roman-upper / alpha / alpha-upper. Optional start='3' (or 'C' for letter-based styles) seeds the top-level counter, useful for appendices following a numbered main body. min-depth clipping renumbers from the visible top, so dropping shallow levels never leaves dangling sub-counters. Unknown values log an error and fall back to default — typos won't blank out the TOC. Closes #43.

1.1

  • Multi-file source input: the CLI now accepts multiple positional source files and glob: patterns (nrg "docs/**/*.src.md" A.src.md B.src.md); -f remains a single-file alias and is rejected when mixed with positional args. Each input gets its own Generator. New --fail-fast short-circuits on the first non-zero per-file result; the default aggregates so every file's diagnostics surface in one run. With --stdout, per-file separators are emitted whenever the total output count exceeds one. Empty per-pattern matches log a warning; an overall zero-match exits 1. The Maven plugin's <file> entries accept the same glob syntax, and a new <failFast> parameter (default false) maps to --fail-fast. Closes #32.
  • Frozen regions: <!--nrg.freeze id="..."--><!--/nrg.freeze--> block markers preserve content written by external tools (contributors-action, sponsors widgets, RSS embedders) into the generated readme across NRG regenerations. The template body between markers is a one-time bootstrap placeholder used only when the on-disk output file does not yet exist; subsequent regenerations splice the on-disk content back in. Optional source-lang="X" attribute redirects every language's freeze content to one source language's output file. Authoring errors (missing/duplicate id, unbalanced or nested markers, source-lang outside nrg.languages, unknown attributes) fail the build with exit 1 and are reported by --validate. On-disk anomalies (malformed blocks, missing ids) emit a one-time WARN and fall back to the template placeholder. Closes #41.
  • Fixed: the tableOfContents widget no longer treats #-prefixed lines inside fenced code blocks (``` / ~~~), inline code spans, indented code blocks, or backslash-escaped headings as real headers, so ordered-list numbering and getPreviousHeader() are no longer poisoned by bash comments inside code samples. Closes #46.
  • Per-language variable overrides: any <!--@name=value--> property can now declare a per-language sibling <!--@name.<lang>=value-->, and ${name} rendered for that language picks the language-specific value first, falling back to the bare key. Closes the use case behind issue #42 (per-language image paths) without introducing a new widget. Soft-breaking only for templates that intentionally relied on name and name.<lang> being independent properties.
  • ${npm.NAME} / ${gradle.NAME} substitution: read values directly from package.json (dotted-path lookup like ${npm.version} or ${npm.dependencies.lodash}) and from gradle.properties + build.gradle{,.kts} (${gradle.version}, ${gradle.group}, plus arbitrary keys from gradle.properties). Mirrors ${pom.NAME} semantics: shell-style defaults, warn-once-per-missing-path, backslash escapes. File locations default to the source-file directory and are configurable via <!--@nrg.npm.path=...--> / <!--@nrg.gradle.path=...-->.
  • Configurable output filenames: <!--@nrg.fileNamePattern=PATTERN--> controls the output filename layout via the placeholders <base>, <lang>, <LANG>. Patterns may contain / separators (docs/<lang>/<base>.md) — intermediate directories are created on demand. <!--@nrg.defaultLanguageFileNamePattern=PATTERN--> overrides the default language only; per-language <!--@nrg.fileNamePattern.<lang>=PATTERN--> overrides win for the matching language. Mirrored by --file-name-pattern / --default-language-file-name-pattern CLI flags and <fileNamePattern> / <defaultLanguageFileNamePattern> Maven plugin parameters. Generation aborts when two languages collide on the same output path. The languages widget now emits relative links so cross-directory layouts work.
  • import widget — heading-offset parameter: shifts ATX heading levels in the imported content by an integer (clamped to [1, 6]), so an imported .src.md can nest cleanly under a parent section without editing its source. Skips fenced code blocks; cannot be combined with wrap='true'. Closes #35.

1.0

  • import widget: added the lines, region, wrap, lang, and dedent parameters for fine-grained inclusion of source files.
  • Added <!--nrg.ignore--> and paired <!--nrg.ignore.begin--> / <!--nrg.ignore.end--> markers for removing author notes from generated output (also inside imported files).
  • tableOfContents widget: added the min-depth and max-depth parameters to limit which heading levels appear in the table of contents.
  • tableOfContents widget: added the min-items parameter — the widget now skips rendering entirely (title included) when fewer than this many headings survive the filters.
  • tableOfContents widget: added the anchor-style parameter (github | gitlab | bitbucket) to match the slugification rules of the target hosting platform.
  • Log levels: added the --log-level CLI flag (trace|debug|info|warn|error, default info), the NRG_LOG_LEVEL environment variable fallback, and a matching <logLevel> Maven plugin parameter.
  • --stdout flag: new CLI flag that streams generated output to standard output instead of writing files; pair with --language <code> to select a single variant.
  • Custom widgets from CLI and templates: the nrg.widgets template property and the --widgets / --classpath CLI flags let users register custom NRGWidget implementations without a custom launcher.
  • Custom widgets in the Maven plugin: the nrg-maven-plugin gains a <widgets> parameter; invalid entries fail the build with a descriptive MojoExecutionException. POM widgets override template-declared ones on name collision.
  • Widget resolution now prefers the last registration on name collision, so user widgets shadow built-ins with the same name.
  • --check flag: CI-friendly verification mode that compares generated output against files on disk, prints a diff to stderr on mismatch, and exits with status 1. The nrg-maven-plugin exposes it via <check> and fails the build with a MojoExecutionException.
  • alert widget: renders GitHub-flavored alert blocks (> [!NOTE], > [!WARNING], > [!TIP], > [!IMPORTANT], > [!CAUTION]) from a single ${widget:alert(type='...', text='...')} call, with \n escapes for multi-line body text.
  • badge widget: renders shields.io badges for maven-central, license, github-release, github-stars, and a free-form custom variant — no more hand-crafted URLs.
  • math widget: renders LaTeX formulas via GitHub's native $…$ / $$…$$ delimiters or as ![alt](…) images through a LaTeX-to-SVG service (default: latex.codecogs.com).
  • exec widget (opt-in): runs an external command and embeds its stdout. Disabled by default; enable with --allow-exec (CLI) or <allowExec>true</allowExec> (Maven plugin). Supports cwd, timeout, trim, and codeblock parameters.
  • ${env.NAME} substitution: read environment variables directly from any template position with shell-style defaults (${env.NAME:fallback}). Works in body text, <!--@key=value--> declaration values, and widget parameter values. Missing variables log a warning and render empty.
  • ${pom.NAME} substitution: read values from the project pom.xml via a Maven-style dotted path (${pom.version}, ${pom.groupId}:${pom.artifactId}, ${pom.parent.version}, ${pom.properties.java.version}). Supports shell-style defaults, parent inheritance for groupId / version / name, and one-level POM-internal interpolation for ${prop}, ${project.*}, and ${env.NAME}. POM path defaults to the source-file directory; override via <!--@nrg.pom.path=...-->.
  • if block widget: ${widget:if(cond='…')}${widget:endIf} conditionally drops a block of lines from the output. Supports a small string-only DSL: truthy / falsy, == / !=, && / ||, !, parentheses, and startsWith / endsWith. Two-phase evaluation (parse-then-resolve) keeps ${…}-resolved values opaque, preventing operator injection. Inner widgets in dropped branches never execute.
  • --validate flag: scans the source template and every reachable ${widget:import}-imported file for authoring mistakes (unknown widgets, undeclared language markers, missing import paths, unbalanced <!--nrg.ignore.begin--> / <!--nrg.ignore.end--> pairs) without generating any output. Exits 0 silently on a clean template, 1 with a file:line: message list otherwise. Mutually exclusive with --check and --stdout. The Maven plugin exposes it as <validate>true</validate> and fails the build with a MojoExecutionException.
  • fileTree widget: renders a tree -L-style directory listing with Unicode box-drawing characters. Supports depth, comma-separated exclude globs (matched against entry name and relative path), dirsOnly, and codeblock parameters. Entries are sorted directories-first then alphabetically for stable byte-exact output.
  • Widget parameters may now contain { and } (LaTeX-friendly); the tag regex now delimits parameters by ( / ) instead of }.
  • Fixed: the languages widget now produces correct link targets when rendered inside an imported fragment.

0.3

  • Published under an open-source license.
  • Table of contents: overhauled heading-to-anchor generation to match GitHub's rules, with Unicode-aware slugification.
  • Fixed: TOC links were malformed for headings containing colons or commas.
  • Removed stray console output from the tableOfContents widget.

0.2

  • Added the import widget for including external *.src.md files into a template.
  • Added ExampleWidget to the test sources as a reference implementation for custom widgets.

0.1

First public release.

  • Core template engine with property declarations (<!--@key=value-->) and ${var} substitution.
  • Multi-language output via ${en:'…', ru:'…'} constructs and language-tagged lines (<!--en-->, <!--ru-->).
  • Built-in widgets: languages, tableOfContents, date, todo.
  • <!--toc.ignore--> marker for excluding headings from the table of contents.
  • Escape-character support in variables and widget parameters, including doubled-quote escaping.
  • CLI with -f, --charset, --version, and -h flags; launcher scripts nrg.sh / nrg.bat; jar-with-dependencies assembly.
  • Maven plugin (nrg-maven-plugin) with the create-files goal.
  • Java 8 compatibility and publication to Maven Central.

Feedback & Support

We value your input! Here are the best ways to connect with us:

Community Support

Direct Communication

  • Email: nrg@nanolaba.com (for sensitive matters or private discussions)
  • Security Issues: see SECURITY.md for the full disclosure policy. Use email and prefix the subject with "[SECURITY]".

Contribution Guide

See CONTRIBUTING.md for the full contributor workflow and CODE_OF_CONDUCT.md for community expectations.

Before submitting feedback:

  1. Check existing issues to avoid duplicates
  2. Use clear, descriptive titles for your requests

We welcome all constructive feedback to make NRG better!


If Nanolaba Readme Generator (NRG) keeps your multi-language READMEs in sync — please give it a ⭐ on GitHub. It's the single biggest signal that lands NRG in front of the next maintainer with the same problem.


Last updated: 01.05.2026

Packages

 
 
 

Contributors

Languages