poops 1.9.7 β 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +136 -112
- package/lib/markup/collections.js +4 -1
- package/lib/markup/engines/liquid.js +5 -12
- package/lib/markup/engines/nunjucks.js +5 -14
- package/lib/markup/highlight.js +41 -8
- package/lib/markup/image-cache.js +30 -2
- package/lib/markup/indexer.js +46 -23
- package/lib/markup/renderer.js +2 -3
- package/lib/markups.js +38 -21
- package/lib/reactor.js +3 -2
- package/lib/scripts.js +3 -2
- package/lib/server.js +96 -2
- package/lib/styles.js +10 -1
- package/lib/utils/helpers.js +6 -0
- package/package.json +11 -10
- package/poops.js +47 -48
package/README.md
CHANGED
|
@@ -54,6 +54,7 @@ It uses a simple config file where you define your input and output paths and it
|
|
|
54
54
|
- [Live Reload (optional)](#live-reload-optional)
|
|
55
55
|
- [Watch (optional)](#watch-optional)
|
|
56
56
|
- [Include Paths (optional)](#include-paths-optional)
|
|
57
|
+
- [Contributing](#contributing)
|
|
57
58
|
- [Why?](#why)
|
|
58
59
|
|
|
59
60
|
## Features
|
|
@@ -81,6 +82,8 @@ It uses a simple config file where you define your input and output paths and it
|
|
|
81
82
|
|
|
82
83
|
> For a superfast start, you can use the Poops template repository: [π©πͺοΈShitstorm](https://github.com/stamat/shitstorm)
|
|
83
84
|
|
|
85
|
+
Poops requires **Node.js 22 or newer**.
|
|
86
|
+
|
|
84
87
|
You can install Poops globally:
|
|
85
88
|
|
|
86
89
|
```bash
|
|
@@ -108,7 +111,6 @@ or pass a custom config. This is useful when you have multiple environments:
|
|
|
108
111
|
| `--build` | `-b` | Build the project and exit |
|
|
109
112
|
| `--config <path>` | `-c` | Specify the config file |
|
|
110
113
|
| `--port <number>` | `-p` | Specify the server port, overrides config |
|
|
111
|
-
| `--livereload-port <number>` | `-l` | Specify the livereload port, overrides config |
|
|
112
114
|
| `--base-url <path>` | `-u` | Set the base URL prefix for markup, overrides config |
|
|
113
115
|
| `--quiet` | `-q` | Hide the header and the server/livereload info lines |
|
|
114
116
|
|
|
@@ -118,7 +120,7 @@ The `--base-url` flag is particularly useful for CI/CD pipelines where the deplo
|
|
|
118
120
|
poops --build --base-url /blog
|
|
119
121
|
```
|
|
120
122
|
|
|
121
|
-
The `--quiet` flag drops the `π© Poops β vX.Y.Z` header (and its terminal bell) plus the `Local server` / `Network` / `
|
|
123
|
+
The `--quiet` flag drops the `π© Poops β vX.Y.Z` header (and its terminal bell) plus the `Local server` / `Network` / `Live reload` lines. Handy when you run several Poops instances side by side and only want to see which one is compiling:
|
|
122
124
|
|
|
123
125
|
```bash
|
|
124
126
|
poops -q & poops -q -c site/poops.json
|
|
@@ -181,15 +183,17 @@ Just create a `poops.json` file in the root of your project and add the followin
|
|
|
181
183
|
}
|
|
182
184
|
],
|
|
183
185
|
"markup": {
|
|
184
|
-
"engine": "nunjucks",
|
|
185
186
|
"in": "example/src/markup",
|
|
186
187
|
"out": "/",
|
|
187
|
-
"
|
|
188
|
-
"
|
|
189
|
-
"
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
188
|
+
"options": {
|
|
189
|
+
"engine": "nunjucks",
|
|
190
|
+
"site": {
|
|
191
|
+
"title": "Poops",
|
|
192
|
+
"description": "A super simple bundler for simple web projects."
|
|
193
|
+
},
|
|
194
|
+
"data": ["data/links.json", "data/poops.yaml"],
|
|
195
|
+
"includePaths": ["_layouts", "_partials"]
|
|
196
|
+
}
|
|
193
197
|
},
|
|
194
198
|
"copy": [
|
|
195
199
|
{
|
|
@@ -226,7 +230,7 @@ Scripts are bundled with [esbuild](https://esbuild.github.io/). Supports `.js`,
|
|
|
226
230
|
- `minify` - whether to minify the output or not, minification is performed by `esbuild` and is only applied to non-minified files. Default is `false`
|
|
227
231
|
- `justMinified` - whether you want to have a minified file as output only. Removes the non-minified file from the output. Useful for production builds. Default is `false`
|
|
228
232
|
- `format` - the output format, can be `iife` or `esm` or `cjs` - this is a direct esbuild option
|
|
229
|
-
- `target` - the target for the output, can be `es2018` or `es2019` or `es2020` or `esnext` for instance - this is a direct esbuild option
|
|
233
|
+
- `target` - the target for the output, can be `es2018` or `es2019` or `es2020` or `esnext` for instance - this is a direct esbuild option. Default is `es2020`
|
|
230
234
|
- `jsx` - the JSX transform mode, can be `transform` (default) or `automatic`. Use `automatic` for React 17+ JSX runtime which doesn't require importing React in every file - this is a direct esbuild option
|
|
231
235
|
|
|
232
236
|
`scripts` property can accept an array of script configurations or just a single script configuration. If you want to bundle multiple scripts, just add them to the `scripts` array:
|
|
@@ -313,8 +317,6 @@ Each reactor entry has the following properties:
|
|
|
313
317
|
}
|
|
314
318
|
```
|
|
315
319
|
|
|
316
|
-
For backwards compatibility, `"ssg"` is also accepted as a config key β it is treated as an alias for `"reactor"`.
|
|
317
|
-
|
|
318
320
|
In your templates, use the `inject` name to insert the rendered HTML:
|
|
319
321
|
|
|
320
322
|
```html
|
|
@@ -526,11 +528,13 @@ npm i -D postcss @tailwindcss/postcss tailwindcss
|
|
|
526
528
|
"markup": {
|
|
527
529
|
"in": "src/markup",
|
|
528
530
|
"out": "dist",
|
|
529
|
-
"
|
|
530
|
-
"
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
531
|
+
"options": {
|
|
532
|
+
"site": {
|
|
533
|
+
"title": "Poops + Tailwind",
|
|
534
|
+
"description": "A Tailwind CSS example for Poops"
|
|
535
|
+
},
|
|
536
|
+
"includePaths": ["_layouts", "_partials"]
|
|
537
|
+
}
|
|
534
538
|
},
|
|
535
539
|
"serve": { "port": 4040, "base": "/dist" },
|
|
536
540
|
"livereload": true,
|
|
@@ -550,9 +554,31 @@ Then use Tailwind utility classes directly in your markup templates. Tailwind v4
|
|
|
550
554
|
|
|
551
555
|
### Markups
|
|
552
556
|
|
|
557
|
+
`markup` has the same shape as a `scripts` or `styles` entry: `in`, `out`, and
|
|
558
|
+
everything else under `options`.
|
|
559
|
+
|
|
560
|
+
```json
|
|
561
|
+
{
|
|
562
|
+
"markup": {
|
|
563
|
+
"in": "src/markup",
|
|
564
|
+
"out": "dist",
|
|
565
|
+
"options": {
|
|
566
|
+
"engine": "nunjucks",
|
|
567
|
+
"site": { "title": "My Awesome Site" }
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
> **Deprecated:** Poops 1.x also read these keys directly on `markup`
|
|
574
|
+
> (`"markup": { "site": β¦ }`). That still works in 2.x and warns; it stops
|
|
575
|
+
> working in 3.0. Move them into `options`.
|
|
576
|
+
|
|
577
|
+
**Options:**
|
|
578
|
+
|
|
553
579
|
- `engine` (optional) - the template engine to use. Can be `"nunjucks"` (default) or `"liquid"`. [Nunjucks](https://mozilla.github.io/nunjucks/) is a Mozilla template engine inspired by Jinja2. [Liquid](https://liquidjs.com/) is a Shopify-compatible template engine. Both engines support the same tags, filters, collections, search index, sitemap, and navigation tree features documented below.
|
|
554
|
-
- `in` - the input path, can be a directory or a file path, but please just use it as a directory path for now. All files in this directory will be processed and the structure of the directory will be preserved in the output directory with exception to directories that begin with an underscore `_` will be ignored.
|
|
555
|
-
- `out` - the output path, can be only a directory path (for now)
|
|
580
|
+
- `in` (entry, not an option) - the input path, can be a directory or a file path, but please just use it as a directory path for now. All files in this directory will be processed and the structure of the directory will be preserved in the output directory with exception to directories that begin with an underscore `_` will be ignored.
|
|
581
|
+
- `out` (entry, not an option) - the output path, can be only a directory path (for now)
|
|
556
582
|
- `site` (optional) - global data that will be available to all templates in the markup directory. Like site title, description, social media links, etc. You can then use this data in your templates `{{ site.title }}` for instance.
|
|
557
583
|
- `data` (optional) - is an array of JSON or YAML data files, that once loaded will be available to all templates in the markup directory. If you provide a path to a file for instance `links.json` with a `facebook` property, you can then use this data in your templates `{{ links.facebook }}`. The base name of the file will be used as the variable name, with spaces, dashes and dots replaced with underscores. So `the awesome-links.json` will be available as `{{ the_awesome_links.facebook }}` in your templates. The root directory of the data files is `in` directory. So if you have a `data` directory in your `in` directory, you can specify the data files like this `data: ["data/links.json"]`. The same goes for the YAML files.
|
|
558
584
|
- `includePaths` - an array of paths to directories that will be added to the template engine's include paths. Useful if you want to separate template partials and layouts. For instance, if you have a `_includes` directory with a `header.njk` (or `header.liquid`) partial that you want to include in your markup, you can add it to the include paths and then include the templates like this `{% include "header.njk" %}`, without specifying the full path to the partial.
|
|
@@ -578,13 +604,15 @@ Here is a sample markup configuration using the default Nunjucks engine:
|
|
|
578
604
|
"markup": {
|
|
579
605
|
"in": "src/markup",
|
|
580
606
|
"out": "dist",
|
|
581
|
-
"
|
|
582
|
-
"
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
607
|
+
"options": {
|
|
608
|
+
"site": {
|
|
609
|
+
"title": "My Awesome Site",
|
|
610
|
+
"description": "This is my awesome site"
|
|
611
|
+
},
|
|
612
|
+
"data": ["data/links.json", "data/other.yaml"],
|
|
613
|
+
"includePaths": ["_includes"],
|
|
614
|
+
"baseURL": "/blog"
|
|
615
|
+
}
|
|
588
616
|
}
|
|
589
617
|
}
|
|
590
618
|
```
|
|
@@ -594,15 +622,17 @@ To use Liquid instead, set the `engine` property:
|
|
|
594
622
|
```json
|
|
595
623
|
{
|
|
596
624
|
"markup": {
|
|
597
|
-
"engine": "liquid",
|
|
598
625
|
"in": "src/liquid",
|
|
599
626
|
"out": "dist",
|
|
600
|
-
"
|
|
601
|
-
"
|
|
602
|
-
"
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
627
|
+
"options": {
|
|
628
|
+
"engine": "liquid",
|
|
629
|
+
"site": {
|
|
630
|
+
"title": "My Awesome Site",
|
|
631
|
+
"description": "This is my awesome site"
|
|
632
|
+
},
|
|
633
|
+
"data": ["_data/links.json", "_data/other.yaml"],
|
|
634
|
+
"includePaths": ["_layouts", "_partials"]
|
|
635
|
+
}
|
|
606
636
|
}
|
|
607
637
|
}
|
|
608
638
|
```
|
|
@@ -661,7 +691,7 @@ The `engine` option also accepts a module specifier β an npm package name or a
|
|
|
661
691
|
"markup": {
|
|
662
692
|
"in": "src/markup",
|
|
663
693
|
"out": "dist",
|
|
664
|
-
"engine": "poops-shopify"
|
|
694
|
+
"options": { "engine": "poops-shopify" }
|
|
665
695
|
}
|
|
666
696
|
}
|
|
667
697
|
```
|
|
@@ -680,7 +710,7 @@ export default class MyEngine {
|
|
|
680
710
|
get markupExtensions() {
|
|
681
711
|
return "html|liquid|md";
|
|
682
712
|
} // glob alternation of processed extensions
|
|
683
|
-
registerFilters({
|
|
713
|
+
registerFilters({ dateFormat, markupOut }) {}
|
|
684
714
|
registerTags(getOutputDir) {}
|
|
685
715
|
setGlobal(key, value) {}
|
|
686
716
|
removeGlobal(key) {}
|
|
@@ -734,14 +764,16 @@ sort: date
|
|
|
734
764
|
"markup": {
|
|
735
765
|
"in": "src/markup",
|
|
736
766
|
"out": "dist",
|
|
737
|
-
"
|
|
738
|
-
"
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
767
|
+
"options": {
|
|
768
|
+
"collections": [
|
|
769
|
+
"changelog",
|
|
770
|
+
{
|
|
771
|
+
"name": "blog",
|
|
772
|
+
"paginate": 5,
|
|
773
|
+
"sort": { "by": "title", "order": "asc" }
|
|
774
|
+
}
|
|
775
|
+
]
|
|
776
|
+
}
|
|
745
777
|
}
|
|
746
778
|
}
|
|
747
779
|
```
|
|
@@ -806,12 +838,14 @@ Pages 2..N automatically get a distinct `<title>` β `Changelog β Page 2` β
|
|
|
806
838
|
```json
|
|
807
839
|
{
|
|
808
840
|
"markup": {
|
|
809
|
-
"
|
|
810
|
-
"
|
|
811
|
-
"
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
841
|
+
"options": {
|
|
842
|
+
"site": {
|
|
843
|
+
"pagination": {
|
|
844
|
+
"title": "{title} β Seite {n}",
|
|
845
|
+
"prev": "ZurΓΌck",
|
|
846
|
+
"next": "Weiter",
|
|
847
|
+
"of": "von"
|
|
848
|
+
}
|
|
815
849
|
}
|
|
816
850
|
}
|
|
817
851
|
}
|
|
@@ -1023,6 +1057,20 @@ The language argument is optional. If omitted, highlight.js will attempt to auto
|
|
|
1023
1057
|
|
|
1024
1058
|
Registered languages: `javascript`/`js`, `typescript`/`ts`, `css`, `scss`, `html`, `xml`, `json`, `bash`/`sh`, `shell`, `python`/`py`, `ruby`/`rb`, `php`, `java`, `c`, `cpp`, `csharp`/`cs`, `go`, `rust`/`rs`, `yaml`/`yml`, `markdown`/`md`, `sql`, `diff`.
|
|
1025
1059
|
|
|
1060
|
+
**Fence info strings.** Only the first word names the language. Anything after it is carried onto the `<code>` element instead of being dropped: a bare word becomes a class, a `key=value` token becomes a `data-` attribute. This is how a fence marks itself for a later stage β a post-`markup` `exec` script that turns `code.preview` blocks into live demos, for example β without a marker comment in the markdown.
|
|
1061
|
+
|
|
1062
|
+
````md
|
|
1063
|
+
```html preview tab=options widths=375,768
|
|
1064
|
+
<my-element></my-element>
|
|
1065
|
+
```
|
|
1066
|
+
````
|
|
1067
|
+
|
|
1068
|
+
```html
|
|
1069
|
+
<pre><code class="hljs language-html preview" data-tab="options" data-widths="375,768">β¦</code></pre>
|
|
1070
|
+
```
|
|
1071
|
+
|
|
1072
|
+
Values are single tokens β no quotes, no spaces. A trailing `=` with nothing after it emits a valueless attribute (`expanded=` β `data-expanded=""`), for a flag you want to read with `hasAttribute` rather than as a class. The same applies to the `{% highlight %}` tag and the `highlight` filter.
|
|
1073
|
+
|
|
1026
1074
|
#### Custom Filters
|
|
1027
1075
|
|
|
1028
1076
|
All filters are available in both engines. The only syntax difference is how arguments are passed: Nunjucks uses parentheses `| filter("arg")`, Liquid uses a colon `| filter: "arg"`.
|
|
@@ -1035,7 +1083,7 @@ All filters are available in both engines. The only syntax difference is how arg
|
|
|
1035
1083
|
|
|
1036
1084
|
- `markdown` β renders a markdown string to HTML with GitHub Flavored Markdown extras: emoji shortcodes (e.g. `:rocket:` β π), alert callouts (`> [!NOTE]`, `[!TIP]`, `[!IMPORTANT]`, `[!WARNING]`, `[!CAUTION]`, `[!INFO]`) and footnotes (`[^1]`). Code fences are syntax-highlighted and headings get slug `id`s plus permalink anchors. Usage: `{{ "**bold** :rocket:" | markdown }}`
|
|
1037
1085
|
|
|
1038
|
-
- `date` β formats a date string. Uses [dayjs](https://day.js.org/) format tokens. A default format can be set via the `
|
|
1086
|
+
- `date` β formats a date string. Uses [dayjs](https://day.js.org/) format tokens. A default format can be set via the `dateFormat` config option.
|
|
1039
1087
|
- Nunjucks: `{{ "2024-01-15" | date("MMMM D, YYYY") }}`
|
|
1040
1088
|
- Liquid: `{{ "2024-01-15" | date: "MMMM D, YYYY" }}`
|
|
1041
1089
|
|
|
@@ -1097,8 +1145,8 @@ All filters are available in both engines. The only syntax difference is how arg
|
|
|
1097
1145
|
|
|
1098
1146
|
```json
|
|
1099
1147
|
"markup": {
|
|
1100
|
-
"
|
|
1101
|
-
"jsonld": { "@type": "TechArticle" }
|
|
1148
|
+
"options": {
|
|
1149
|
+
"site": { "jsonld": { "@type": "TechArticle" } }
|
|
1102
1150
|
}
|
|
1103
1151
|
}
|
|
1104
1152
|
```
|
|
@@ -1235,17 +1283,17 @@ The string shorthand sets the output filename with default options. For more con
|
|
|
1235
1283
|
```json
|
|
1236
1284
|
{
|
|
1237
1285
|
"searchIndex": {
|
|
1238
|
-
"
|
|
1286
|
+
"out": "search-index.json",
|
|
1239
1287
|
"minWordLength": 3,
|
|
1240
1288
|
"maxKeywords": 20,
|
|
1241
1289
|
"globalFrequencyCeiling": 0.8,
|
|
1242
1290
|
"stopWords": "path/to/custom-stop-words.json"
|
|
1243
1291
|
},
|
|
1244
1292
|
"sitemap": {
|
|
1245
|
-
"
|
|
1293
|
+
"out": "sitemap.xml"
|
|
1246
1294
|
},
|
|
1247
1295
|
"llms": {
|
|
1248
|
-
"
|
|
1296
|
+
"out": "llms.txt",
|
|
1249
1297
|
"title": "My Site",
|
|
1250
1298
|
"description": "One-line summary of the site.",
|
|
1251
1299
|
"intro": "src/llms-intro.md",
|
|
@@ -1256,7 +1304,7 @@ The string shorthand sets the output filename with default options. For more con
|
|
|
1256
1304
|
|
|
1257
1305
|
**Search Index options:**
|
|
1258
1306
|
|
|
1259
|
-
- `
|
|
1307
|
+
- `out` β output filename, written to the markup output directory
|
|
1260
1308
|
- `minWordLength` β minimum word length to consider as a keyword (default: `3`)
|
|
1261
1309
|
- `maxKeywords` β maximum keywords per page (default: `20`)
|
|
1262
1310
|
- `globalFrequencyCeiling` β drop words appearing in more than this fraction of all pages (default: `0.8`, meaning words found in 80%+ of pages are dropped as non-discriminating)
|
|
@@ -1288,14 +1336,14 @@ All front matter fields are passed through to the index automatically. Internal
|
|
|
1288
1336
|
|
|
1289
1337
|
**llms.txt** generates an [`llms.txt`](https://llmstxt.org) β a Markdown index of your pages that LLMs and generative engines (GEO) read to understand your site. It has an `# H1` title, a `> ` blockquote summary, then `- [title](url): description` links grouped by URL path: the first folder is a `## section`, a second folder nests as a `### subsection` under it, and root-level pages fall under a lead "Pages" section. So `docs/config-reference.html` lands directly under `## Docs` while `docs/quick-start/x.html` lands under `### Quick Start` inside it. Collection items (which live under `collection/β¦`) group the same way and are ordered newest-first by their `date`; other sections keep file order. Set `intro` to a Markdown file path (relative to the project root) to insert free-form context between the blockquote and the link sections β a file authored for LLMs, e.g. `llms-intro.md`. Avoid `##` headings in it; they read as sections. (A raw README is a poor fit β badges, install noise and its own headings collide.) `title` and `description` default to your `site.title`/`site.description`; override them (and the lead section name via `sectionTitle`) with the object form. `site.url` makes the links absolute. Collection index/pagination pages are skipped, like the search index.
|
|
1290
1338
|
|
|
1291
|
-
Set `full` to also write a companion full-content file β every page's Markdown body concatenated into one file an LLM can ingest whole (the index is the link map; this is the corpus). `true` names it after `
|
|
1339
|
+
Set `full` to also write a companion full-content file β every page's Markdown body concatenated into one file an LLM can ingest whole (the index is the link map; this is the corpus). `true` names it after `out` with a `-full` suffix (`llms.txt` β `llms-full.txt`, `ai.txt` β `ai-full.txt`); pass a string to set the path yourself. The file opens with a `# Full Documentation Archive for {title}` header, a one-line intro naming the site and a `> ` blockquote of the `description` so a whole-file ingest starts with context, then each page becomes an `# title` (its own leading H1 if it has one) + `URL:` line + body, joined by `---`. Set `fullIntro` to a Markdown file path (from the project root) to insert your own preamble after that header β the `full` counterpart to `intro` (inserted verbatim; a missing file warns and is skipped). Only `.md`/`.markdown` sources qualify (a `.njk`/`.liquid` source is template code, not prose); `noindex` and collection-index pages are dropped. Content is the Markdown **source**, so unrendered `{% raw %}{% β¦ %}{% endraw %}` tags or shortcodes in a body pass through verbatim.
|
|
1292
1340
|
|
|
1293
|
-
**robots.txt** generates a `robots.txt`. The string shorthand writes an allow-all file (`User-agent: *`, empty `Disallow:`) with a `Sitemap:` line pointing at your generated sitemap β absolute when `site.url` is set. The object form takes `
|
|
1341
|
+
**robots.txt** generates a `robots.txt`. The string shorthand writes an allow-all file (`User-agent: *`, empty `Disallow:`) with a `Sitemap:` line pointing at your generated sitemap β absolute when `site.url` is set. The object form takes `out`, `userAgent`, `allow`/`disallow` (a path or array of paths), and `sitemap` (an explicit URL, or `false` to omit the line):
|
|
1294
1342
|
|
|
1295
1343
|
```json
|
|
1296
1344
|
{
|
|
1297
1345
|
"robots": {
|
|
1298
|
-
"
|
|
1346
|
+
"out": "robots.txt",
|
|
1299
1347
|
"disallow": ["/admin", "/drafts"],
|
|
1300
1348
|
"sitemap": false
|
|
1301
1349
|
}
|
|
@@ -1330,7 +1378,7 @@ The string shorthand sets the output filename. For docs sites, use the object fo
|
|
|
1330
1378
|
```json
|
|
1331
1379
|
{
|
|
1332
1380
|
"nav": {
|
|
1333
|
-
"
|
|
1381
|
+
"out": "nav.json",
|
|
1334
1382
|
"root": "docs",
|
|
1335
1383
|
"collections": "index",
|
|
1336
1384
|
"home": false
|
|
@@ -1340,7 +1388,7 @@ The string shorthand sets the output filename. For docs sites, use the object fo
|
|
|
1340
1388
|
|
|
1341
1389
|
**Navigation options:**
|
|
1342
1390
|
|
|
1343
|
-
- `
|
|
1391
|
+
- `out` β output filename, written to the markup output directory
|
|
1344
1392
|
- `collections` β how to treat collection pages (default `true`):
|
|
1345
1393
|
- `true` β include every collection page, nested under its collection
|
|
1346
1394
|
- `false` β exclude all collection pages (drops a blog's posts from the sidebar)
|
|
@@ -1431,7 +1479,7 @@ Generate a subscription feed for a [collection](#collections) β no hand-author
|
|
|
1431
1479
|
"in": "src/markup",
|
|
1432
1480
|
"out": "dist",
|
|
1433
1481
|
"options": {
|
|
1434
|
-
"feed": { "collection": "blog", "
|
|
1482
|
+
"feed": { "collection": "blog", "out": "blog/feed.rss" }
|
|
1435
1483
|
}
|
|
1436
1484
|
}
|
|
1437
1485
|
}
|
|
@@ -1440,7 +1488,7 @@ Generate a subscription feed for a [collection](#collections) β no hand-author
|
|
|
1440
1488
|
**Feed options:**
|
|
1441
1489
|
|
|
1442
1490
|
- `collection` β the collection to build the feed from. Omit it to emit a feed for **every** collection.
|
|
1443
|
-
- `
|
|
1491
|
+
- `out` β the file to write. A bare filename (`feed.xml`, the default) is placed inside the collection's own folder (`blog/feed.xml`); a value with a slash is used as-is under the output directory.
|
|
1444
1492
|
- `type` β `"rss"` (default) or `"atom"`.
|
|
1445
1493
|
- `limit` β max items, newest first (default `20`).
|
|
1446
1494
|
- `title` β channel title (default `"<Collection> | <site.title>"`).
|
|
@@ -1583,69 +1631,37 @@ Sets up a local server for your project.
|
|
|
1583
1631
|
Server options:
|
|
1584
1632
|
|
|
1585
1633
|
- `port` - the port on which the server will run
|
|
1586
|
-
- `base` - the base path of the server, where your HTML files are located
|
|
1634
|
+
- `base` - the base path of the server, where your HTML files are located. Defaults to the markup `out` directory, so most configs can leave it out
|
|
1587
1635
|
|
|
1588
1636
|
If you don't want to run a local server, just remove the `serve` property from the config.
|
|
1589
1637
|
|
|
1590
1638
|
### Live Reload (optional)
|
|
1591
1639
|
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
Live reload options:
|
|
1595
|
-
|
|
1596
|
-
- `port` - the port on which the livereload server will run
|
|
1597
|
-
- `exclude` - an array of files and directories to exclude from livereload
|
|
1598
|
-
- `extraExts` - an array of extra file extensions (without the dot) that trigger a browser refresh, added to the defaults
|
|
1599
|
-
- `exts` - an array of file extensions that replaces the default list entirely
|
|
1600
|
-
|
|
1601
|
-
By default a refresh is triggered by changes to: `html`, `css`, `js`, `png`, `gif`, `jpg`, `php`, `php5`, `py`, `rb`, `erb`, `coffee`. If you work with other file types, for example Slim or Nunjucks templates, add them:
|
|
1602
|
-
|
|
1603
|
-
```json
|
|
1604
|
-
{
|
|
1605
|
-
"livereload": {
|
|
1606
|
-
"extraExts": ["slim", "njk"]
|
|
1607
|
-
}
|
|
1608
|
-
}
|
|
1609
|
-
```
|
|
1610
|
-
|
|
1611
|
-
`livereload` can only be `true`, which means that it will run on the default port (`35729`) or you can specify a port:
|
|
1612
|
-
|
|
1613
|
-
```json
|
|
1614
|
-
{
|
|
1615
|
-
"livereload": {
|
|
1616
|
-
"port": whateverPortYouWant
|
|
1617
|
-
}
|
|
1618
|
-
}
|
|
1619
|
-
```
|
|
1620
|
-
|
|
1621
|
-
You can also exclude files and directories from livereload:
|
|
1640
|
+
Reloads the browser when a build finishes. It is a switch, nothing more:
|
|
1622
1641
|
|
|
1623
1642
|
```json
|
|
1624
1643
|
{
|
|
1625
|
-
"
|
|
1626
|
-
|
|
1627
|
-
}
|
|
1644
|
+
"serve": { "base": "dist" },
|
|
1645
|
+
"livereload": true
|
|
1628
1646
|
}
|
|
1629
1647
|
```
|
|
1630
1648
|
|
|
1631
|
-
|
|
1649
|
+
Live reload rides the `serve` port β there is no second server and no port to
|
|
1650
|
+
configure, so it needs `serve` to be on. With both set, Poops answers
|
|
1651
|
+
`/__poops_reload` as a [server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events)
|
|
1652
|
+
stream and appends a small client script to every HTML page it serves. **Your
|
|
1653
|
+
templates need no snippet.** Nothing is injected into your build output β the
|
|
1654
|
+
script exists only in the response the dev server writes.
|
|
1632
1655
|
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
(location.host || "localhost").split(":")[0] +
|
|
1638
|
-
':35729/livereload.js?snipver=1"></' +
|
|
1639
|
-
"script>",
|
|
1640
|
-
);
|
|
1641
|
-
</script>
|
|
1642
|
-
```
|
|
1656
|
+
A save triggers exactly one reload, after the build that follows it has
|
|
1657
|
+
finished. When everything a build wrote is CSS, the stylesheets are swapped in
|
|
1658
|
+
place instead: the page is not reloaded, so scroll position, open dialogs and
|
|
1659
|
+
form state survive a style edit.
|
|
1643
1660
|
|
|
1644
|
-
|
|
1661
|
+
The browser reconnects on its own, so restarting Poops picks the open tabs back
|
|
1662
|
+
up without touching them.
|
|
1645
1663
|
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
If you don't want to run livereload, just remove the `livereload` property from the config, or set it to false.
|
|
1664
|
+
To turn it off, remove the `livereload` property or set it to `false`.
|
|
1649
1665
|
|
|
1650
1666
|
### Watch (optional)
|
|
1651
1667
|
|
|
@@ -1673,6 +1689,14 @@ Same as `watch` property, `includePaths` accepts an array of paths to include. I
|
|
|
1673
1689
|
}
|
|
1674
1690
|
```
|
|
1675
1691
|
|
|
1692
|
+
## Contributing
|
|
1693
|
+
|
|
1694
|
+
Issues and pull requests are welcome β see [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
1695
|
+
for how to set up, what a PR needs, and how releases are cut.
|
|
1696
|
+
|
|
1697
|
+
Released changes are in [CHANGELOG.md](CHANGELOG.md), written up in full on the
|
|
1698
|
+
[changelog site](https://stamat.info/poops/changelog/).
|
|
1699
|
+
|
|
1676
1700
|
## Why?
|
|
1677
1701
|
|
|
1678
1702
|
Why doesn't anyone maintain GULP anymore? Why does Parcel hate config files? Why are Rollup and Webpack so complex to setup for simple tasks? Vite???? What's going on?
|
|
@@ -33,7 +33,10 @@ export function getSingleCollectionData(markupInDir, collectionName) {
|
|
|
33
33
|
frontMatter.wordcount = wordcount(content)
|
|
34
34
|
frontMatter.excerpt = excerpt(content)
|
|
35
35
|
frontMatter.fileName = path.basename(file)
|
|
36
|
-
|
|
36
|
+
// posix, like the page.filePath a template gets for a non-collection page β
|
|
37
|
+
// both feed "edit this page on GitHub" links, and a Windows backslash in one
|
|
38
|
+
// would produce a broken URL for collection items only
|
|
39
|
+
frontMatter.filePath = toPosix(path.relative(process.cwd(), file))
|
|
37
40
|
frontMatter.collection = collectionName
|
|
38
41
|
frontMatter.url = toPosix(path.join(collectionName, path.basename(frontMatter.filePath)))
|
|
39
42
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import { Liquid } from 'liquidjs'
|
|
4
|
-
import { highlightCode } from '../highlight.js'
|
|
4
|
+
import { highlightCode, codeBlock } from '../highlight.js'
|
|
5
5
|
import { marked, renderMarkdownCached } from '../renderer.js'
|
|
6
6
|
import { discoverImageVariants, parseFrontMatter, groupby, buildImageTag, buildPaginationTag, renderToc, buildJsonLd, buildOpenGraph, buildCanonical, buildBreadcrumb } from '../helpers.js'
|
|
7
7
|
import { getImageExif, listImages } from '../image-cache.js'
|
|
@@ -83,7 +83,7 @@ export default class LiquidEngine {
|
|
|
83
83
|
get indexableExtensions() { return new Set(['.html', '.md', '.liquid']) }
|
|
84
84
|
get markupExtensions() { return 'html|xml|rss|atom|json|liquid|md' }
|
|
85
85
|
|
|
86
|
-
registerFilters({
|
|
86
|
+
registerFilters({ dateFormat, markupOut }) {
|
|
87
87
|
const engine = this.engine
|
|
88
88
|
engine.registerFilter('slugify', (str) => slugify(str))
|
|
89
89
|
engine.registerFilter('humanize', (str) => humanize(str))
|
|
@@ -91,7 +91,7 @@ export default class LiquidEngine {
|
|
|
91
91
|
engine.registerFilter('markdown', (str) => marked.parse(String(str || '')))
|
|
92
92
|
engine.registerFilter('toc', (html) => renderToc(String(html || '')))
|
|
93
93
|
engine.registerFilter('date', (str, template) => {
|
|
94
|
-
const fmt = template ||
|
|
94
|
+
const fmt = template || dateFormat
|
|
95
95
|
if (!fmt) return str
|
|
96
96
|
const date = !str || (typeof str === 'string' && str.trim() === '') ? new Date() : new Date(str)
|
|
97
97
|
return dayjs(date).format(fmt)
|
|
@@ -131,11 +131,7 @@ export default class LiquidEngine {
|
|
|
131
131
|
engine.registerFilter('canonical', (page, site) => buildCanonical(page, site))
|
|
132
132
|
engine.registerFilter('breadcrumb', (page, site, prefix) => buildBreadcrumb(page, site, prefix))
|
|
133
133
|
engine.registerFilter('groupby', (arr, key, datePart) => groupby(arr, key, datePart))
|
|
134
|
-
engine.registerFilter('highlight', (code, lang) =>
|
|
135
|
-
const highlighted = highlightCode(code, lang)
|
|
136
|
-
const langClass = lang ? ` language-${lang}` : ''
|
|
137
|
-
return `<pre><code class="hljs${langClass}">${highlighted}</code></pre>`
|
|
138
|
-
})
|
|
134
|
+
engine.registerFilter('highlight', (code, lang) => codeBlock(highlightCode(code, lang), lang))
|
|
139
135
|
}
|
|
140
136
|
|
|
141
137
|
registerTags(getOutputDir) {
|
|
@@ -347,10 +343,7 @@ function registerHighlightTag(engine) {
|
|
|
347
343
|
},
|
|
348
344
|
* render(ctx) {
|
|
349
345
|
const code = yield this.liquid.renderer.renderTemplates(this.templates, ctx)
|
|
350
|
-
|
|
351
|
-
const highlighted = highlightCode(code, lang)
|
|
352
|
-
const langClass = lang ? ` language-${lang}` : ''
|
|
353
|
-
return `<pre><code class="hljs${langClass}">${highlighted}</code></pre>`
|
|
346
|
+
return codeBlock(highlightCode(code, this.lang), this.lang)
|
|
354
347
|
}
|
|
355
348
|
})
|
|
356
349
|
}
|
|
@@ -6,7 +6,7 @@ import path from 'node:path'
|
|
|
6
6
|
import { discoverImageVariants, parseFrontMatter, groupby, buildImageTag, buildPaginationTag, renderToc, buildJsonLd, buildOpenGraph, buildCanonical, buildBreadcrumb } from '../helpers.js'
|
|
7
7
|
import { getImageExif, listImages } from '../image-cache.js'
|
|
8
8
|
import { toPosix } from '../../utils/helpers.js'
|
|
9
|
-
import { highlightCode } from '../highlight.js'
|
|
9
|
+
import { highlightCode, codeBlock } from '../highlight.js'
|
|
10
10
|
import { marked, renderMarkdownCached } from '../renderer.js'
|
|
11
11
|
import { slugify, humanize } from 'book-of-spells'
|
|
12
12
|
import dayjs from 'dayjs'
|
|
@@ -144,7 +144,7 @@ export default class NunjucksEngine {
|
|
|
144
144
|
get indexableExtensions() { return new Set(['.html', '.md', '.njk']) }
|
|
145
145
|
get markupExtensions() { return 'html|xml|rss|atom|json|njk|md' }
|
|
146
146
|
|
|
147
|
-
registerFilters({
|
|
147
|
+
registerFilters({ dateFormat, markupOut }) {
|
|
148
148
|
const env = this.env
|
|
149
149
|
env.addFilter('slugify', slugify)
|
|
150
150
|
env.addFilter('humanize', humanize)
|
|
@@ -173,7 +173,7 @@ export default class NunjucksEngine {
|
|
|
173
173
|
return new nunjucks.runtime.SafeString(content)
|
|
174
174
|
})
|
|
175
175
|
env.addFilter('date', (str, template) => {
|
|
176
|
-
const fmt = template ||
|
|
176
|
+
const fmt = template || dateFormat
|
|
177
177
|
if (!fmt) return str
|
|
178
178
|
const date = !str || str.trim() === '' ? new Date() : new Date(str)
|
|
179
179
|
return dayjs(date).format(fmt)
|
|
@@ -197,11 +197,7 @@ export default class NunjucksEngine {
|
|
|
197
197
|
env.addFilter('canonical', (page, site) => new nunjucks.runtime.SafeString(buildCanonical(page, site)))
|
|
198
198
|
env.addFilter('breadcrumb', (page, site, prefix) => new nunjucks.runtime.SafeString(buildBreadcrumb(page, site, prefix)))
|
|
199
199
|
env.addFilter('groupby', (arr, key, datePart) => groupby(arr, key, datePart))
|
|
200
|
-
env.addFilter('highlight', (code, lang) =>
|
|
201
|
-
const highlighted = highlightCode(code, lang)
|
|
202
|
-
const langClass = lang ? ` language-${lang}` : ''
|
|
203
|
-
return new nunjucks.runtime.SafeString(`<pre><code class="hljs${langClass}">${highlighted}</code></pre>`)
|
|
204
|
-
})
|
|
200
|
+
env.addFilter('highlight', (code, lang) => new nunjucks.runtime.SafeString(codeBlock(highlightCode(code, lang), lang)))
|
|
205
201
|
}
|
|
206
202
|
|
|
207
203
|
registerTags(getOutputDir) {
|
|
@@ -335,12 +331,7 @@ export class HighlightExtension {
|
|
|
335
331
|
}
|
|
336
332
|
|
|
337
333
|
run(context, lang, body) {
|
|
338
|
-
|
|
339
|
-
const highlighted = highlightCode(code, lang)
|
|
340
|
-
const langClass = lang ? ` language-${lang}` : ''
|
|
341
|
-
return new nunjucks.runtime.SafeString(
|
|
342
|
-
`<pre><code class="hljs${langClass}">${highlighted}</code></pre>`
|
|
343
|
-
)
|
|
334
|
+
return new nunjucks.runtime.SafeString(codeBlock(highlightCode(body(), lang), lang))
|
|
344
335
|
}
|
|
345
336
|
}
|
|
346
337
|
|
package/lib/markup/highlight.js
CHANGED
|
@@ -58,20 +58,53 @@ function escapeHtml(str) {
|
|
|
58
58
|
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
61
|
+
// A fence's info string is everything after the backticks β the language plus
|
|
62
|
+
// any meta words (```html preview). Only the first word names the language, so
|
|
63
|
+
// the rest has to come off before a hljs lookup or a `language-` class: with it
|
|
64
|
+
// attached, getLanguage misses and the block silently falls to auto-detection.
|
|
65
|
+
export function fenceLang(info) {
|
|
66
|
+
return String(info || '').trim().split(/\s+/)[0].toLowerCase()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Everything after the language is the fence's own markup for a later stage:
|
|
70
|
+
// a bare word becomes a class (```html preview β code.preview), `key=value`
|
|
71
|
+
// becomes a data attribute (tab=options β data-tab="options"). Values are
|
|
72
|
+
// single tokens β no quotes, no spaces β which keeps the parser a split and is
|
|
73
|
+
// enough for settings; anything longer belongs in the markdown around the fence.
|
|
74
|
+
export function fenceAttrs(info) {
|
|
75
|
+
const [lang, ...rest] = String(info || '').trim().split(/\s+/).filter(Boolean)
|
|
76
|
+
const classes = ['hljs']
|
|
77
|
+
if (lang) classes.push(`language-${lang.toLowerCase().replace(/[^\w-]/g, '')}`)
|
|
78
|
+
let data = ''
|
|
79
|
+
for (const token of rest) {
|
|
80
|
+
const eq = token.indexOf('=')
|
|
81
|
+
if (eq < 0) {
|
|
82
|
+
const name = token.replace(/[^\w-]/g, '')
|
|
83
|
+
if (name) classes.push(name)
|
|
84
|
+
continue
|
|
66
85
|
}
|
|
86
|
+
const key = token.slice(0, eq).replace(/[^\w-]/g, '')
|
|
87
|
+
if (key) data += ` data-${key}="${escapeHtml(token.slice(eq + 1))}"`
|
|
88
|
+
}
|
|
89
|
+
return ` class="${classes.join(' ')}"${data}`
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// The one place a highlighted fence becomes html. Every renderer and engine
|
|
93
|
+
// goes through it, so they cannot drift on what a fence produces again.
|
|
94
|
+
export function codeBlock(highlighted, info) {
|
|
95
|
+
return `<pre><code${fenceAttrs(info)}>${highlighted}</code></pre>`
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function highlightCode(code, language) {
|
|
99
|
+
const lang = fenceLang(language)
|
|
100
|
+
if (lang && hljs.getLanguage(lang)) {
|
|
101
|
+
return hljs.highlight(code, { language: lang }).value
|
|
67
102
|
}
|
|
68
103
|
return hljs.highlightAuto(code).value
|
|
69
104
|
}
|
|
70
105
|
|
|
71
106
|
export const highlightRenderer = {
|
|
72
107
|
code({ text, lang }) {
|
|
73
|
-
|
|
74
|
-
const langClass = lang ? ` language-${escapeHtml(lang)}` : ''
|
|
75
|
-
return `<pre><code class="hljs${langClass}">${highlighted}</code></pre>\n`
|
|
108
|
+
return `${codeBlock(highlightCode(text, lang), lang)}\n`
|
|
76
109
|
}
|
|
77
110
|
}
|