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 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` / `LiveReload` lines. Handy when you run several Poops instances side by side and only want to see which one is compiling:
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
- "site": {
188
- "title": "Poops",
189
- "description": "A super simple bundler for simple web projects."
190
- },
191
- "data": ["data/links.json", "data/poops.yaml"],
192
- "includePaths": ["_layouts", "_partials"]
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
- "site": {
530
- "title": "Poops + Tailwind",
531
- "description": "A Tailwind CSS example for Poops"
532
- },
533
- "includePaths": ["_layouts", "_partials"]
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
- "site": {
582
- "title": "My Awesome Site",
583
- "description": "This is my awesome site"
584
- },
585
- "data": ["data/links.json", "data/other.yaml"],
586
- "includePaths": ["_includes"],
587
- "baseURL": "/blog"
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
- "site": {
601
- "title": "My Awesome Site",
602
- "description": "This is my awesome site"
603
- },
604
- "data": ["_data/links.json", "_data/other.yaml"],
605
- "includePaths": ["_layouts", "_partials"]
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({ timeDateFormat, markupOut }) {}
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
- "collections": [
738
- "changelog",
739
- {
740
- "name": "blog",
741
- "paginate": 5,
742
- "sort": { "by": "title", "order": "asc" }
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
- "site": {
810
- "pagination": {
811
- "title": "{title} β€” Seite {n}",
812
- "prev": "ZurΓΌck",
813
- "next": "Weiter",
814
- "of": "von"
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 `timeDateFormat` config option.
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
- "site": {
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
- "output": "search-index.json",
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
- "output": "sitemap.xml"
1293
+ "out": "sitemap.xml"
1246
1294
  },
1247
1295
  "llms": {
1248
- "output": "llms.txt",
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
- - `output` β€” output filename, written to the markup output directory
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 `output` 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.
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 `output`, `userAgent`, `allow`/`disallow` (a path or array of paths), and `sitemap` (an explicit URL, or `false` to omit the line):
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
- "output": "robots.txt",
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
- "output": "nav.json",
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
- - `output` β€” output filename, written to the markup output directory
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", "output": "blog/feed.rss" }
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
- - `output` β€” 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.
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
- Sets up a livereload server for your project.
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
- "livereload": {
1626
- "exclude": ["some_directory/**/*", "some_other_directory/**/*"]
1627
- }
1644
+ "serve": { "base": "dist" },
1645
+ "livereload": true
1628
1646
  }
1629
1647
  ```
1630
1648
 
1631
- In order for Livereload to work, you need to add the following script snippet to your HTML files in your development environment:
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
- ```html
1634
- <script>
1635
- document.write(
1636
- '<script src="http://' +
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
- Be mindful of the port, if you have specified a custom port, you need to change the port in the snippet as well.
1661
+ The browser reconnects on its own, so restarting Poops picks the open tabs back
1662
+ up without touching them.
1645
1663
 
1646
- You can also use a browser extension for livereload, for instance here is one for [Chrome](https://chrome.google.com/webstore/detail/livereload/jnihajbhpnppcggbcgedagnkighmdlei?hl=en). You can find also extensions for Firefox and Opera, but NOT for Safari.
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
- frontMatter.filePath = path.relative(process.cwd(), file)
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({ timeDateFormat, markupOut }) {
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 || timeDateFormat
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
- const lang = this.lang
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({ timeDateFormat, markupOut }) {
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 || timeDateFormat
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
- const code = body()
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
 
@@ -58,20 +58,53 @@ function escapeHtml(str) {
58
58
  return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')
59
59
  }
60
60
 
61
- export function highlightCode(code, language) {
62
- if (language) {
63
- const lang = language.toLowerCase().trim()
64
- if (hljs.getLanguage(lang)) {
65
- return hljs.highlight(code, { language: lang }).value
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
- const highlighted = highlightCode(text, lang)
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
  }