[ en | ru ]
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.
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!
- Multi-language READMEs - Support for EN/ZN/RU and any other languages
- CI drift detection - the
--checkflag (CLI) andmode: check(GitHub Action) fail the build with a unified diff if generated.mdfiles drift from the template — so a contributor's hand-edit can never silently land inmain - 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.
This very README is generated with NRG — see README.src.md. The same template is also used to keep README.ru.md in sync.
- 1 Quick start
- 2 Usage
- 2.1 Using the Command Line Interface
- 2.1.1 Verifying generated files (CI mode)
- 2.1.2 Validating source templates
- 2.1.3 Print to stdout
- 2.1.4 Logging verbosity
- 2.1.5 Customising output filenames
- 2.1.6 Line endings
- 2.1.7 Header customisation
- 2.1.8 Multiple files and glob patterns
- 2.2 Use as maven plugin
- 2.3 Use as a GitHub Action
- 2.3.1 Quickstart
- 2.3.2 Inputs
- 2.3.3 Outputs
- 2.3.4 Examples
- 2.3.4.1 Basic generate
- 2.3.4.2 Drift check on PR
- 2.3.4.3 Drift check on a subset of outputs
- 2.3.5 Multi-file projects
- 2.3.6 Skipping the built-in setup-java
- 2.3.7 Pinning the action version
- 2.3.8 Troubleshooting
- 2.4 Use as a java-library
- 2.1 Using the Command Line Interface
- 3 Template syntax
- 3.1 Variables
- 3.2 Backslash escapes
- 3.3 Properties
- 3.4 Per-language overrides
- 3.5 Environment variables
- 3.6 Maven POM values
- 3.7 npm package values
- 3.8 Gradle values
- 3.9 Multilanguage support
- 3.10 Ignoring content
- 3.11 Frozen regions
- 3.12 Widgets
- 3.12.1 Widget 'languages'
- 3.12.2 Widget 'import'
- 3.12.3 Widget 'tableOfContents'
- 3.12.4 Widget 'date'
- 3.12.5 Widget 'todo'
- 3.12.6 Widget 'alert'
- 3.12.7 Widget 'badge'
- 3.12.8 Widget 'math'
- 3.12.9 Widget 'exec'
- 3.12.10 Widget 'if'
- 3.12.11 Widget 'fileTree'
- 4 Advanced features
- 5 Related projects
- 6 Changelog
- 7 Feedback & Support
- 7.1 Community Support
- 7.2 Direct Communication
- 7.3 Contribution Guide
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.mdOption 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.md | README.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')}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.mdTo see the list of available options for the console application, type:
nrg --helpUse --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.mdUse --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 innrg.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.
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.mdThe --language flag is only meaningful with --stdout — using it
on its own logs a warning and the flag is ignored.
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.mdUse --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-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.mdIn --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.
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.mdPass 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.
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>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.
- uses: actions/checkout@v4
- uses: nanolaba/nrg-action@v1
with:
file: README.src.md| 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:
generatewritesREADME.md,README.ru.md, … to disk (default).checkis 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.validatescans the template for authoring mistakes (unknown widgets, undeclared language markers, missing imports, unbalanced ignore-blocks). No files are written.
| 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. |
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.mdFail 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: checkWhen 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/*.mdValidate-only and auto-commit-via-PR recipes are in the nrg-action/examples directory of this repository.
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.mdIf 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'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).
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-latest — mode: check reports diffs that do not appear locally.
- Line-ending drift on Windows runners — add a
.gitattributesline* text=auto eol=lfand re-commit the regenerated files. - Windows:
unzip: command not found— git-bash onwindows-latestnormally shipsunzip; on rare images install it viachoco install unzipin 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.
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);
}
}
}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 example | Result |
|---|---|
<!--@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} |
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.
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.
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>.
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 ofNAMEfrom 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.
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.Xreads<X>,pom.X.Yreads<X><Y>, and so on. pom.properties.KEYis a flat-map lookup: the remainder of the path is used verbatim as the<properties>child element name (so dotted keys likejava.versionwork).pom.parent.Xreads the local<parent>block as written. Cross-file parent POM traversal is out of scope.- For unqualified
pom.groupId,pom.version, andpom.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.xmllocation defaults to the source-file directory; override with<!--@nrg.pom.path=relative/or/absolute/pom.xml-->.
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.Xreads the top-level field,npm.X.Ywalks into the nested object, and so on. - String, number, and boolean leaves are stringified. Object, array, and
nullleaves 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.jsonlocation defaults to the source-file directory; override with<!--@nrg.npm.path=relative/or/absolute/package.json-->.
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.Xfirst looks up the verbatim keyXingradle.properties. If not found andXisversionorgroup, NRG regex-extractsX = '...'from the build script (works for both Groovy and Kotlin DSLs).gradle.propertiesalways 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.propertiesif 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).
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"
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 example | Result |
|---|---|
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 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
idwithin the same template; - unbalanced markers (open without close, or stray close);
- nested freeze blocks;
source-langreferencing a language not declared innrg.languages;- unknown attributes (only
idandsource-langare 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:
iddeclared in the template not found in the on-disk file;- malformed disk block (e.g. missing close);
- duplicate
idon disk — the first occurrence wins; source-langfile does not exist on disk yet (treated as bootstrap).
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.
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 ] |
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 |
|---|
|
| Code import example |
|---|
|
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 |
|---|
|
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 |
|---|
|
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).
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) |
This component allows you to insert the current date into a document.
| Usage example | Result |
|---|---|
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.
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... |
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 example | Result |
|---|---|
${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. |
'' |
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:
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.
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:
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
{/}insideexprwork as in LaTeX (subscripts, superscripts,\\text{…},\\frac{…}{…}). - Raw
(and)are not allowed insideexprbecause 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
servicefor long-lived docs. - Pre-rendering, MathML output, and LaTeX linting are out of scope.
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 example | Behaviour |
|---|---|
${widget:exec(cmd = 'java -jar nrg.jar --help')} |
Runs |
${widget:exec(cmd = 'git rev-parse --short HEAD', codeblock = 'text')} |
Runs the command and wraps stdout in a fenced code block tagged |
${widget:exec(cmd = './scripts/list-langs.sh', cwd = 'docs', timeout = '5')} |
Runs the script from the |
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-execflag to thenrgcommand. - Maven: add
<allowExec>true</allowExec>to thenrg-maven-pluginconfiguration (or set the-DallowExec=trueproperty). - Library: call
generator.getConfig().setExecAllowed(true)before the firstgetResult(...)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
timeoutortrim→ error in the log + empty output (command is not run).
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 toa && bis treated as opaque text — operators inside placeholder values are not reinterpreted as boolean operators. - No implicit case folding or numeric coercion:
${env.CI}==Truedoes not match the env valuetrue. Normalise upstream.
Errors:
- An unclosed
${widget:if}block is reported viaLOG.errorand everything from the outermost open marker to EOF is dropped from the output. - A stray
${widget:endIf}(no matching open) is reported viaLOG.errorand the marker line is dropped. - A malformed condition (unbalanced parens, trailing operators, unknown function names) is reported via
LOG.errorand 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 ==.
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
depthfinite.
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.mdEach 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.
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.
This section summarises the main user-visible changes in each release. For full details, see the git history.
- Scope
--checkto specific files: new--check-pathsCLI flag,<checkPaths>Maven plugin parameter, andcheck-pathsGitHub Action input limit drift detection to outputs matching the suppliedglob: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-pathsrequires--check; without the filter, every declared language is checked as before. Closes #53. - Customisable head comment: new
--no-headerCLI 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@LastEditTimeHTML comments) opt out of NRG's signature without forking the generation step. Closes #51. badgewidget — optionalalt=parameter: every type (maven-central,license,github-release,github-stars,github-workflow,custom) now accepts an optionalalt='...'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 likeNRG continuous integration build statuscarry semantic signal that bareCIdoes not. Emptyalt=''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(defaultauto) and matching Maven plugin<lineEnding>parameter override detection. In--checkmode,autoignores LE-only differences so a CRLF-vs-LF contributor mismatch no longer trips CI; explicitlf/crlfstill flag a mismatch (that's the explicitly-requested invariant). Closes #48. nrg-maven-pluginno longer inherits itscreate-filesexecution into child modules by default: the goal is declared withinheritByDefault = false, so multi-module (aggregator) builds run README generation only at the root POM whereREADME.src.mdlives — 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.tableOfContentswidget —numbering-styleandstartparameters: whenordered='true', pick a counter shape from nine values —default(today's1.markers, byte-identical to omitting the parameter), hierarchicaldotted(1,1.1,1.1.1),legal(1.,1.1.,1.1.1.),appendix(A,A.1,A.1.1), or flat global countersarabic/roman/roman-upper/alpha/alpha-upper. Optionalstart='3'(or'C'for letter-based styles) seeds the top-level counter, useful for appendices following a numbered main body.min-depthclipping renumbers from the visible top, so dropping shallow levels never leaves dangling sub-counters. Unknown values log an error and fall back todefault— typos won't blank out the TOC. Closes #43.
- 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);-fremains a single-file alias and is rejected when mixed with positional args. Each input gets its ownGenerator. New--fail-fastshort-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 exits1. The Maven plugin's<file>entries accept the same glob syntax, and a new<failFast>parameter (defaultfalse) 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. Optionalsource-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-langoutsidenrg.languages, unknown attributes) fail the build with exit1and 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
tableOfContentswidget 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 andgetPreviousHeader()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 onnameandname.<lang>being independent properties. ${npm.NAME}/${gradle.NAME}substitution: read values directly frompackage.json(dotted-path lookup like${npm.version}or${npm.dependencies.lodash}) and fromgradle.properties+build.gradle{,.kts}(${gradle.version},${gradle.group}, plus arbitrary keys fromgradle.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-patternCLI flags and<fileNamePattern>/<defaultLanguageFileNamePattern>Maven plugin parameters. Generation aborts when two languages collide on the same output path. Thelanguageswidget now emits relative links so cross-directory layouts work. importwidget —heading-offsetparameter: shifts ATX heading levels in the imported content by an integer (clamped to[1, 6]), so an imported.src.mdcan nest cleanly under a parent section without editing its source. Skips fenced code blocks; cannot be combined withwrap='true'. Closes #35.
importwidget: added thelines,region,wrap,lang, anddedentparameters 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). tableOfContentswidget: added themin-depthandmax-depthparameters to limit which heading levels appear in the table of contents.tableOfContentswidget: added themin-itemsparameter — the widget now skips rendering entirely (title included) when fewer than this many headings survive the filters.tableOfContentswidget: added theanchor-styleparameter (github|gitlab|bitbucket) to match the slugification rules of the target hosting platform.- Log levels: added the
--log-levelCLI flag (trace|debug|info|warn|error, defaultinfo), theNRG_LOG_LEVELenvironment variable fallback, and a matching<logLevel>Maven plugin parameter. --stdoutflag: 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.widgetstemplate property and the--widgets/--classpathCLI flags let users register customNRGWidgetimplementations without a custom launcher. - Custom widgets in the Maven plugin: the
nrg-maven-plugingains a<widgets>parameter; invalid entries fail the build with a descriptiveMojoExecutionException. 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.
--checkflag: CI-friendly verification mode that compares generated output against files on disk, prints a diff to stderr on mismatch, and exits with status1. Thenrg-maven-pluginexposes it via<check>and fails the build with aMojoExecutionException.alertwidget: renders GitHub-flavored alert blocks (> [!NOTE],> [!WARNING],> [!TIP],> [!IMPORTANT],> [!CAUTION]) from a single${widget:alert(type='...', text='...')}call, with\nescapes for multi-line body text.badgewidget: renders shields.io badges formaven-central,license,github-release,github-stars, and a free-formcustomvariant — no more hand-crafted URLs.mathwidget: renders LaTeX formulas via GitHub's native$…$/$$…$$delimiters or asimages through a LaTeX-to-SVG service (default:latex.codecogs.com).execwidget (opt-in): runs an external command and embeds its stdout. Disabled by default; enable with--allow-exec(CLI) or<allowExec>true</allowExec>(Maven plugin). Supportscwd,timeout,trim, andcodeblockparameters.${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 projectpom.xmlvia a Maven-style dotted path (${pom.version},${pom.groupId}:${pom.artifactId},${pom.parent.version},${pom.properties.java.version}). Supports shell-style defaults, parent inheritance forgroupId/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=...-->.ifblock widget:${widget:if(cond='…')}…${widget:endIf}conditionally drops a block of lines from the output. Supports a small string-only DSL: truthy / falsy,==/!=,&&/||,!, parentheses, andstartsWith/endsWith. Two-phase evaluation (parse-then-resolve) keeps${…}-resolved values opaque, preventing operator injection. Inner widgets in dropped branches never execute.--validateflag: 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. Exits0silently on a clean template,1with afile:line: messagelist otherwise. Mutually exclusive with--checkand--stdout. The Maven plugin exposes it as<validate>true</validate>and fails the build with aMojoExecutionException.fileTreewidget: renders atree -L-style directory listing with Unicode box-drawing characters. Supportsdepth, comma-separatedexcludeglobs (matched against entry name and relative path),dirsOnly, andcodeblockparameters. 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
languageswidget now produces correct link targets when rendered inside an imported fragment.
- 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
tableOfContentswidget.
- Added the
importwidget for including external*.src.mdfiles into a template. - Added
ExampleWidgetto the test sources as a reference implementation for custom widgets.
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-hflags; launcher scriptsnrg.sh/nrg.bat; jar-with-dependencies assembly. - Maven plugin (
nrg-maven-plugin) with thecreate-filesgoal. - Java 8 compatibility and publication to Maven Central.
We value your input! Here are the best ways to connect with us:
- GitHub Discussions - Ask questions, share ideas, and discuss best practices
- GitHub Issues - For bug reports and feature requests
- 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]".
See CONTRIBUTING.md for the full contributor workflow and CODE_OF_CONDUCT.md for community expectations.
Before submitting feedback:
- Check existing issues to avoid duplicates
- 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
