FILES / README.md
| Name | Last Commit | Timestamp | Size |
|---|---|---|---|
| .github | ad9c389 h test: replace the cloned-blog CI job with a committed fixture |
26-08-24 11:38 | - |
| bin | 5479d3a h Take the markdown parser as a submodule instead of vendoring it |
26-09-05 10:06 | - |
| share | 4853a27 h Sit the theme control on the header line, without a box |
26-09-06 09:36 | - |
| test | b70d686 h Take the parser's escape-by-default contract |
26-09-05 10:21 | - |
| .gitignore | b4efcdc h chore: do not track agent worktrees |
26-08-25 11:02 | 174 B |
| .gitmodules | 5479d3a h Take the markdown parser as a submodule instead of vendoring it |
26-09-05 10:06 | 138 B |
| .shellcheckrc | e11b323 h ci: add test workflow, shellcheck baseline, auditable image, VERSION |
26-08-20 09:50 | 818 B |
| Dockerfile | e11b323 h ci: add test workflow, shellcheck baseline, auditable image, VERSION |
26-08-20 09:50 | 1 KB |
| LICENSE.txt | 1e61a7e h Update license |
21-04-06 21:58 | 1 KB |
| Makefile | e11b323 h ci: add test workflow, shellcheck baseline, auditable image, VERSION |
26-08-20 09:50 | 734 B |
| README.md | 5a5d303 h Name a nav in its own file, count what it hides, label the tag row |
26-08-26 13:20 | 12 KB |
| VERSION | e11b323 h ci: add test workflow, shellcheck baseline, auditable image, VERSION |
26-08-20 09:50 | 6 B |
| action.yml | e11b323 h ci: add test workflow, shellcheck baseline, auditable image, VERSION |
26-08-20 09:50 | 452 B |
Jenny
Jenny is a static blog generator. Its aim is to work with minimal requirements and a small footprint while providing some modern conveniences.
- [x] Lightweight default theme
- [x] Markdown with Footnotes support, care of a modified md2html.awk
- [x] Basic pagination with fixed page numbers
- [x] Plug your own Markdown parser
- [x] Heredocs-based template syntax
- [x] Draft/ignore support (by leaving out the date)
- [x] Forward-posting, i.e. ignores posts with dates in the future
- [x] Tags support
- [x] Modifiable installation prefix
- [x] Define run-time settings in command arguments
- [x] Include static directories in build
- [x] Unique URL slug generation
- [x] Render only changed or new posts
- [x] RSS/Atom feed
- [x] Tests
- [x] Navs: page-declared navigation and theme-baked site navs
Getting started, four ways
1. You can install jenny to your local bin folder
git clone https://github.com/hmngwy/jenny.git
make install
2. Or, skip installation and use Docker, all references to the command jenny is interchangeable with the below
docker run -it -v $PWD:/blog ghcr.io/hmngwy/jenny:latest
The Docker image contains Multimarkdown 6 so you can have MARKDOWN_COMMAND="multimarkdown " in your .blogrc.
3. Or, as a Github Action to automatically build your blog
This repository is also the Github Action source, simply reference it in your Worflow steps, below is an example.
on: [push]
jobs:
build_job:
runs-on: ubuntu-latest
name: Build with Jenny
steps:
- uses: actions/checkout@v2
- uses: hmngwy/jenny@master
4. Or, fork this template repo
You can fork the hmngwy/jenny-template repo to start using jenny without installing a thing.
Setting up a Blog
Prepare the directory for your blog posts.
mkdir ./blog && cd blog
Use the subcommand below to initialize your blog folder with a .blogrc file and a sample first post.
jenny init
Creating a Blog Post
A dated filename will be recognized as a published post, everything else is a draft and ignored, the format is:
YYYY-MM-DD a-published-post.md
Create a file with a date so that Jenny recognizes it as a published post.
cat <<EOT >> "$(date +%Y-%m-%d) hello-world.md"
# Hello World
Jenny is a static blog generator using bash, sed, and awk.
EOT
Building the Blog
Run jenny on your blog directory.
CLI Arguments
To override .blogrc settings at run-time use command line arguments. Other options are also available, use jenny -h to display the message below:
jenny usage:
-d ./dir Override the build directory setting
-l ./dir Override layout directory setting
-p 10 Override posts per page setting
-m mmd Override the markdown parser command setting
-v Display more information during building
-n Ignore the .bloglock file when building
-c Empty out the build folder prior to building
-f Force a full rebuild without emptying the lock
-h Show this help message
--version Show the version and exit
Handy Shortcuts
# Publish file with current date
jenny publish filename.md
# Edit file without typing date/full filename
jenny edit partial-filename
# Serve the build directory at http://localhost:8000/ (port is optional)
jenny serve
jenny serve 9000
jenny serve roots the server at DIST, so relative links behave exactly as they will once deployed. It uses python3 -m http.server, falling back to python 2 or busybox.
Customize defualt behaviors with .blogrc settings
To configure posts per page:
POSTS_PER_PAGE=10
To use your own markdown parser, set MARKDOWN_COMMAND:
MARKDOWN_COMMAND=multimarkdown
If the parser exits non-zero, or produces no output for a non-empty post, the build fails loudly rather than writing an empty page.
To cap the number of items in the feed (default 20):
FEED_MAX_ITEMS=20
To control how post URLs are written, set URL_STYLE:
URL_STYLE=html # /post/slug.html (default)
URL_STYLE=extensionless # /post/slug
html works on any static server, including jenny serve and a bare nginx.
extensionless suits hosts that treat the extensionless path as canonical — Cloudflare Pages, Netlify, Vercel. Those hosts permanently redirect /post/slug.html to /post/slug, so leaving the default there costs every internal link a redirect hop.
The file written to disk always keeps .html in either mode; those hosts resolve /post/slug by looking for slug.html. Do not use extensionless on a server that serves files literally — the links will 404.
To run a script after the build process, write a post_hook function in .blogrc:
function post_hook() {
echo "Done!"
}
To set a different build folder, set DIST:
DIST=~/blog/.dist
To copy directories into the build folder:
STATIC_DIRS="images files"
The 404 page
Every build writes a 404.html. Static hosts that resolve extensionless URLs — Cloudflare Pages, Netlify, Vercel — serve index.html for any path they cannot match when no 404 page exists, so a typo returns HTTP 200 with a copy of your front page.
It is rendered from layout/404.sh like any other page, so it picks up your blog title. A custom LAYOUT_DIR without a 404.sh simply does not get one — jenny will not inject a page whose styling would not match your theme. Copy the shipped one into your layout directory to customise it.
Post Formatting
Titles are Markdown H1s. Multimarkdown footnotes syntax supported in the vendored parser.
To use tags add the below into a post where tagname is both filename and URL friendly:
tags: tagname anothertag
Navs
Jenny supports two kinds of navigation:
Page-declared navs are content: a page declares which nav it belongs to, and that nav appears on the page. Use these to show structure within a section (e.g. a tutorial's chapters).
Nav: tutorial-toc
# Chapter 1
...
The nav file navs/tutorial-toc.tsv is tab-separated labels and site-root-relative paths:
Introduction /tutorial/intro
Chapter 1 /tutorial/ch1
Chapter 2 /tutorial/ch2
Template navs are presentation: a layout renders a nav without any page mentioning it, so the same links appear on every page without each page asking for them.
The default theme ships render_footer_nav, which reads navs/site-footer.tsv, but does not call it — the default theme renders no footer. It is kept as a worked example of the mechanism: call it from your own layout, or write your own reader, to put a theme-declared nav on every page.
Comments (#) and blank lines are ignored, so you can annotate a nav file without the notes appearing on the page:
# reviewed 2026-08; drop the legal links when the new ones land
Home /
About /about
Naming a nav
A line beginning with ### is the nav's own name. The default theme shows it on the collapsed nav on a phone, alongside a count of the entries behind it; without one it says "Contents".
### Study Contents
Introduction /study/intro
Method /study/method
A file names itself once. A second ### is ignored — a file naming itself twice is being edited, not renamed, and taking the last one silently would hide the duplicate.
Sections
A line beginning with ## is a section: a label that names the group of entries under it. Sections are flat — a section groups the entries that follow it and sections do not contain other sections.
## Site structure
Home /
About /about
## Legal
Privacy /privacy
Terms /terms
## rather than # so that existing nav files do not change meaning: comments were always dropped, and turning every one of them into a visible heading would change what already-written navs render.
Nesting
An entry indented by two spaces is a child of the entry above it. Nesting applies to entries, not sections, and can go as deep as you like:
## Guide
Overview /guide
Setup /guide/setup
Requirements /guide/setup/reqs
Usage /guide/usage
Back at top /top
Depth comes from each line's own indentation, so an entry returns to any level simply by being indented that far. Indent with spaces, not tabs — tab is the field separator between a label and its path.
The default theme indents each level and stops adding indent past the third, so a deep entry stays inside the sidebar column rather than running out of it. A theme that wants a different treatment can style .nav-depth-0 through .nav-depth-3, or ignore depth entirely.
What a nav means
A nav carries no meaning of its own. It is an ordered list of names and places, with grouping and depth attached; what to do with that is the template's decision. The default page layout renders it as a sidebar with section labels and indented children. The footer nav in the same file reads the same structure and deliberately flattens it, because a footer is a row of links.
Customizing template navs
If you customize the layout, you can add template navs to your own templates. Source the nav helper and call read_nav with --optional:
source "${JENNY_LIB_DIR}/nav.sh"
my_nav=$(read_nav "${NAVS_DIR}" "my-nav" --optional)
Note: a typo in a nav name (e.g., "my-nav" vs. "my-navv") will silently render nothing, not fail the build. Validate nav names during theme development.
Theme Customization
From the project folder, copy the layout folder contents and modify to your liking, mind the template tags.
Let Jenny know where to find your customized layout files.
cat <<EOT >> .blogrc LAYOUT_DIR=~/blog/.layout EOT
## Plugs 🔌
Sites built with `jenny`.
Me - [@hmngwy](https://github.com/hmngwy): [manilafunctional.com](https://manilafunctional.com/)<sup>[src](https://github.com/hmngwy/blog)</sup>
Send a PR my way if you want to be on here.
## Alternative Installation Modes
To soft install or symlink into your bin folder, good for contributing/development:
sudo make sym-install [PREFIX=$PWD][prefix=$pwd]
To install into a custom location do: ```make install [PREFIX=~/.local]```
To uninstall, in the project folder run: ```make uninstall```
## Credits
- Layout inspired by n-o-d-e.net
- Some colors from Solarized by Ethan Schoonover
- Makefile inspired by [moebiuseye/skf](https://github.com/moebiuseye/skf)
## Running the tests
The suite is plain bash -- no test framework to install. It runs against the
working tree, so no `make install` is needed first.
test/run.sh
| file | covers |
| --- | --- |
| `test/units.sh` | `helpers.sh` functions in isolation |
| `test/smoke.sh` | init, build, and the overall output structure |
| `test/regressions.sh` | one case per fixed bug |
`test/lib/assert.bash` also provides a link checker: it walks every generated
HTML file, extracts each `href`/`src`, and stats the target with `DIST` as the
document root. Both the smoke and regression suites assert that no link is
broken.
CI runs the suite on Linux with gawk, on Linux with mawk (stricter about
dynamic regexes), and on macOS, plus a bash 3.2 leg for the system bash that
ships there.
## License
MIT License
Copyright (c) 2024
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
| # Jenny | |
| _Jenny_ is a static blog generator. Its aim is to work with minimal requirements and a small footprint while providing some modern conveniences. | |
| - [x] Lightweight default theme | |
| - [x] Markdown with Footnotes support, care of a modified [md2html.awk](https://bitbucket.org/yiyus/md2html.awk) | |
| - [x] Basic pagination with fixed page numbers | |
| - [x] Plug your own Markdown parser | |
| - [x] Heredocs-based template syntax | |
| - [x] Draft/ignore support (by leaving out the date) | |
| - [x] Forward-posting, i.e. ignores posts with dates in the future | |
| - [x] Tags support | |
| - [x] Modifiable installation prefix | |
| - [x] Define run-time settings in command arguments | |
| - [x] Include static directories in build | |
| - [x] Unique URL slug generation | |
| - [x] Render only changed or new posts | |
| - [x] RSS/Atom feed | |
| - [x] Tests | |
| - [x] Navs: page-declared navigation and theme-baked site navs | |
| [](https://github.com/hmngwy/jenny/actions/workflows/publish-image.yml) | |
| ## Getting started, four ways | |
| #### 1. You can install `jenny` to your local bin folder | |
| ``` | |
| git clone https://github.com/hmngwy/jenny.git | |
| make install | |
| ``` | |
| #### 2. Or, skip installation and use Docker, all references to the command `jenny` is interchangeable with the below | |
| ``` | |
| docker run -it -v $PWD:/blog ghcr.io/hmngwy/jenny:latest | |
| ``` | |
| The Docker image contains Multimarkdown 6 so you can have `MARKDOWN_COMMAND="multimarkdown "` in your `.blogrc`. | |
| #### 3. Or, as a Github Action to automatically build your blog | |
| This repository is also the Github Action source, simply reference it in your Worflow steps, below is an example. | |
| ``` | |
| on: [push] | |
| jobs: | |
| build_job: | |
| runs-on: ubuntu-latest | |
| name: Build with Jenny | |
| steps: | |
| - uses: actions/checkout@v2 | |
| - uses: hmngwy/jenny@master | |
| ``` | |
| #### 4. Or, fork this template repo | |
| You can fork the [hmngwy/jenny-template](https://github.com/hmngwy/jenny-template) repo to start using `jenny` without installing a thing. | |
| --- | |
| ## Setting up a Blog | |
| Prepare the directory for your blog posts. | |
| ``` | |
| mkdir ./blog && cd blog | |
| ``` | |
| Use the subcommand below to initialize your blog folder with a `.blogrc` file and a sample first post. | |
| ``` | |
| jenny init | |
| ``` | |
| ## Creating a Blog Post | |
| A dated filename will be recognized as a published post, everything else is a draft and ignored, the format is: | |
| ``` | |
| YYYY-MM-DD a-published-post.md | |
| ``` | |
| Create a file with a date so that Jenny recognizes it as a published post. | |
| ``` | |
| cat <<EOT >> "$(date +%Y-%m-%d) hello-world.md" | |
| # Hello World | |
| Jenny is a static blog generator using bash, sed, and awk. | |
| EOT | |
| ``` | |
| --- | |
| ## Building the Blog | |
| Run `jenny` on your blog directory. | |
| ## CLI Arguments | |
| To override `.blogrc` settings at run-time use command line arguments. Other options are also available, use `jenny -h` to display the message below: | |
| ``` | |
| jenny usage: | |
| -d ./dir Override the build directory setting | |
| -l ./dir Override layout directory setting | |
| -p 10 Override posts per page setting | |
| -m mmd Override the markdown parser command setting | |
| -v Display more information during building | |
| -n Ignore the .bloglock file when building | |
| -c Empty out the build folder prior to building | |
| -f Force a full rebuild without emptying the lock | |
| -h Show this help message | |
| --version Show the version and exit | |
| ``` | |
| ### Handy Shortcuts | |
| ```bash | |
| # Publish file with current date | |
| jenny publish filename.md | |
| # Edit file without typing date/full filename | |
| jenny edit partial-filename | |
| # Serve the build directory at http://localhost:8000/ (port is optional) | |
| jenny serve | |
| jenny serve 9000 | |
| ``` | |
| `jenny serve` roots the server at `DIST`, so relative links behave exactly as | |
| they will once deployed. It uses `python3 -m http.server`, falling back to | |
| python 2 or busybox. | |
| ## Customize defualt behaviors with .blogrc settings | |
| To configure posts per page: | |
| ``` | |
| POSTS_PER_PAGE=10 | |
| ``` | |
| To use your own markdown parser, set `MARKDOWN_COMMAND`: | |
| ``` | |
| MARKDOWN_COMMAND=multimarkdown | |
| ``` | |
| If the parser exits non-zero, or produces no output for a non-empty post, | |
| the build fails loudly rather than writing an empty page. | |
| To cap the number of items in the feed (default 20): | |
| ``` | |
| FEED_MAX_ITEMS=20 | |
| ``` | |
| To control how post URLs are written, set `URL_STYLE`: | |
| ``` | |
| URL_STYLE=html # /post/slug.html (default) | |
| URL_STYLE=extensionless # /post/slug | |
| ``` | |
| `html` works on any static server, including `jenny serve` and a bare nginx. | |
| `extensionless` suits hosts that treat the extensionless path as canonical -- | |
| Cloudflare Pages, Netlify, Vercel. Those hosts permanently redirect | |
| `/post/slug.html` to `/post/slug`, so leaving the default there costs every | |
| internal link a redirect hop. | |
| The file written to disk always keeps `.html` in either mode; those hosts | |
| resolve `/post/slug` by looking for `slug.html`. Do not use `extensionless` | |
| on a server that serves files literally -- the links will 404. | |
| To run a script after the build process, write a `post_hook` function in .blogrc: | |
| ``` | |
| function post_hook() { | |
| echo "Done!" | |
| } | |
| ``` | |
| To set a different build folder, set `DIST`: | |
| ``` | |
| DIST=~/blog/.dist | |
| ``` | |
| To copy directories into the build folder: | |
| ``` | |
| STATIC_DIRS="images files" | |
| ``` | |
| ## The 404 page | |
| Every build writes a `404.html`. Static hosts that resolve extensionless URLs | |
| -- Cloudflare Pages, Netlify, Vercel -- serve `index.html` for any path they | |
| cannot match when no 404 page exists, so a typo returns HTTP 200 with a copy | |
| of your front page. | |
| It is rendered from `layout/404.sh` like any other page, so it picks up your | |
| blog title. A custom `LAYOUT_DIR` without a `404.sh` simply does not get one | |
| -- jenny will not inject a page whose styling would not match your theme. | |
| Copy the shipped one into your layout directory to customise it. | |
| ## Post Formatting | |
| Titles are Markdown H1s. Multimarkdown footnotes syntax supported in the vendored parser. | |
| To use tags add the below into a post where `tagname` is both filename and URL friendly: | |
| ``` | |
| tags: tagname anothertag | |
| ``` | |
| ## Navs | |
| Jenny supports two kinds of navigation: | |
| **Page-declared navs** are content: a page declares which nav it belongs to, and that nav appears on the page. Use these to show structure within a section (e.g. a tutorial's chapters). | |
| ``` | |
| Nav: tutorial-toc | |
| # Chapter 1 | |
| ... | |
| ``` | |
| The nav file `navs/tutorial-toc.tsv` is tab-separated labels and site-root-relative paths: | |
| ``` | |
| Introduction /tutorial/intro | |
| Chapter 1 /tutorial/ch1 | |
| Chapter 2 /tutorial/ch2 | |
| ``` | |
| **Template navs** are presentation: a layout renders a nav without any page mentioning it, so the same links appear on every page without each page asking for them. | |
| The default theme ships `render_footer_nav`, which reads `navs/site-footer.tsv`, but does not call it — the default theme renders no footer. It is kept as a worked example of the mechanism: call it from your own layout, or write your own reader, to put a theme-declared nav on every page. | |
| Comments (`#`) and blank lines are ignored, so you can annotate a nav file | |
| without the notes appearing on the page: | |
| ``` | |
| # reviewed 2026-08; drop the legal links when the new ones land | |
| Home / | |
| About /about | |
| ``` | |
| ### Naming a nav | |
| A line beginning with `###` is the nav's own name. The default theme shows it | |
| on the collapsed nav on a phone, alongside a count of the entries behind it; | |
| without one it says "Contents". | |
| ``` | |
| ### Study Contents | |
| Introduction /study/intro | |
| Method /study/method | |
| ``` | |
| A file names itself once. A second `###` is ignored — a file naming itself | |
| twice is being edited, not renamed, and taking the last one silently would | |
| hide the duplicate. | |
| ### Sections | |
| A line beginning with `##` is a section: a label that names the group of | |
| entries under it. Sections are flat — a section groups the entries that follow | |
| it and sections do not contain other sections. | |
| ``` | |
| ## Site structure | |
| Home / | |
| About /about | |
| ## Legal | |
| Privacy /privacy | |
| Terms /terms | |
| ``` | |
| `##` rather than `#` so that existing nav files do not change meaning: comments | |
| were always dropped, and turning every one of them into a visible heading would | |
| change what already-written navs render. | |
| ### Nesting | |
| An entry indented by two spaces is a child of the entry above it. Nesting | |
| applies to entries, not sections, and can go as deep as you like: | |
| ``` | |
| ## Guide | |
| Overview /guide | |
| Setup /guide/setup | |
| Requirements /guide/setup/reqs | |
| Usage /guide/usage | |
| Back at top /top | |
| ``` | |
| Depth comes from each line's own indentation, so an entry returns to any level | |
| simply by being indented that far. Indent with spaces, not tabs — tab is the | |
| field separator between a label and its path. | |
| The default theme indents each level and stops adding indent past the third, | |
| so a deep entry stays inside the sidebar column rather than running out of it. | |
| A theme that wants a different treatment can style `.nav-depth-0` through | |
| `.nav-depth-3`, or ignore depth entirely. | |
| ### What a nav means | |
| A nav carries no meaning of its own. It is an ordered list of names and places, | |
| with grouping and depth attached; what to do with that is the template's | |
| decision. The default page layout renders it as a sidebar with section labels | |
| and indented children. The footer nav in the same file reads the same | |
| structure and deliberately flattens it, because a footer is a row of links. | |
| ### Customizing template navs | |
| If you customize the layout, you can add template navs to your own templates. Source the nav helper and call `read_nav` with `--optional`: | |
| ```bash | |
| source "${JENNY_LIB_DIR}/nav.sh" | |
| my_nav=$(read_nav "${NAVS_DIR}" "my-nav" --optional) | |
| ``` | |
| Note: a typo in a nav name (e.g., "my-nav" vs. "my-navv") will silently render nothing, not fail the build. Validate nav names during theme development. | |
| ## Theme Customization | |
| From the project folder, copy the layout folder contents and modify to your liking, mind the template tags. | |
| ```cp -R ./share/jenny/layout ~/blog/.layout``` | |
| Let Jenny know where to find your customized layout files. | |
| ``` | |
| cat <<EOT >> .blogrc | |
| LAYOUT_DIR=~/blog/.layout | |
| EOT | |
| ``` | |
| ## Plugs 🔌 | |
| Sites built with `jenny`. | |
| Me - [@hmngwy](https://github.com/hmngwy): [manilafunctional.com](https://manilafunctional.com/)<sup>[src](https://github.com/hmngwy/blog)</sup> | |
| Send a PR my way if you want to be on here. | |
| ## Alternative Installation Modes | |
| To soft install or symlink into your bin folder, good for contributing/development: | |
| ``` | |
| sudo make sym-install [PREFIX=$PWD] | |
| ``` | |
| To install into a custom location do: ```make install [PREFIX=~/.local]``` | |
| To uninstall, in the project folder run: ```make uninstall``` | |
| ## Credits | |
| - Layout inspired by n-o-d-e.net | |
| - Some colors from Solarized by Ethan Schoonover | |
| - Makefile inspired by [moebiuseye/skf](https://github.com/moebiuseye/skf) | |
| ## Running the tests | |
| The suite is plain bash -- no test framework to install. It runs against the | |
| working tree, so no `make install` is needed first. | |
| ``` | |
| test/run.sh | |
| ``` | |
| | file | covers | | |
| | --- | --- | | |
| | `test/units.sh` | `helpers.sh` functions in isolation | | |
| | `test/smoke.sh` | init, build, and the overall output structure | | |
| | `test/regressions.sh` | one case per fixed bug | | |
| `test/lib/assert.bash` also provides a link checker: it walks every generated | |
| HTML file, extracts each `href`/`src`, and stats the target with `DIST` as the | |
| document root. Both the smoke and regression suites assert that no link is | |
| broken. | |
| CI runs the suite on Linux with gawk, on Linux with mawk (stricter about | |
| dynamic regexes), and on macOS, plus a bash 3.2 leg for the system bash that | |
| ships there. | |
| ## License | |
| MIT License | |
| Copyright (c) 2024 | |
| Permission is hereby granted, free of charge, to any person obtaining a copy | |
| of this software and associated documentation files (the "Software"), to deal | |
| in the Software without restriction, including without limitation the rights | |
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| copies of the Software, and to permit persons to whom the Software is | |
| furnished to do so, subject to the following conditions: | |
| The above copyright notice and this permission notice shall be included in all | |
| copies or substantial portions of the Software. | |
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
| SOFTWARE. |