poops 1.9.8 β†’ 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
  }
@@ -1049,7 +1083,7 @@ All filters are available in both engines. The only syntax difference is how arg
1049
1083
 
1050
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 }}`
1051
1085
 
1052
- - `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.
1053
1087
  - Nunjucks: `{{ "2024-01-15" | date("MMMM D, YYYY") }}`
1054
1088
  - Liquid: `{{ "2024-01-15" | date: "MMMM D, YYYY" }}`
1055
1089
 
@@ -1111,8 +1145,8 @@ All filters are available in both engines. The only syntax difference is how arg
1111
1145
 
1112
1146
  ```json
1113
1147
  "markup": {
1114
- "site": {
1115
- "jsonld": { "@type": "TechArticle" }
1148
+ "options": {
1149
+ "site": { "jsonld": { "@type": "TechArticle" } }
1116
1150
  }
1117
1151
  }
1118
1152
  ```
@@ -1249,17 +1283,17 @@ The string shorthand sets the output filename with default options. For more con
1249
1283
  ```json
1250
1284
  {
1251
1285
  "searchIndex": {
1252
- "output": "search-index.json",
1286
+ "out": "search-index.json",
1253
1287
  "minWordLength": 3,
1254
1288
  "maxKeywords": 20,
1255
1289
  "globalFrequencyCeiling": 0.8,
1256
1290
  "stopWords": "path/to/custom-stop-words.json"
1257
1291
  },
1258
1292
  "sitemap": {
1259
- "output": "sitemap.xml"
1293
+ "out": "sitemap.xml"
1260
1294
  },
1261
1295
  "llms": {
1262
- "output": "llms.txt",
1296
+ "out": "llms.txt",
1263
1297
  "title": "My Site",
1264
1298
  "description": "One-line summary of the site.",
1265
1299
  "intro": "src/llms-intro.md",
@@ -1270,7 +1304,7 @@ The string shorthand sets the output filename with default options. For more con
1270
1304
 
1271
1305
  **Search Index options:**
1272
1306
 
1273
- - `output` β€” output filename, written to the markup output directory
1307
+ - `out` β€” output filename, written to the markup output directory
1274
1308
  - `minWordLength` β€” minimum word length to consider as a keyword (default: `3`)
1275
1309
  - `maxKeywords` β€” maximum keywords per page (default: `20`)
1276
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)
@@ -1302,14 +1336,14 @@ All front matter fields are passed through to the index automatically. Internal
1302
1336
 
1303
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.
1304
1338
 
1305
- 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.
1306
1340
 
1307
- **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):
1308
1342
 
1309
1343
  ```json
1310
1344
  {
1311
1345
  "robots": {
1312
- "output": "robots.txt",
1346
+ "out": "robots.txt",
1313
1347
  "disallow": ["/admin", "/drafts"],
1314
1348
  "sitemap": false
1315
1349
  }
@@ -1344,7 +1378,7 @@ The string shorthand sets the output filename. For docs sites, use the object fo
1344
1378
  ```json
1345
1379
  {
1346
1380
  "nav": {
1347
- "output": "nav.json",
1381
+ "out": "nav.json",
1348
1382
  "root": "docs",
1349
1383
  "collections": "index",
1350
1384
  "home": false
@@ -1354,7 +1388,7 @@ The string shorthand sets the output filename. For docs sites, use the object fo
1354
1388
 
1355
1389
  **Navigation options:**
1356
1390
 
1357
- - `output` β€” output filename, written to the markup output directory
1391
+ - `out` β€” output filename, written to the markup output directory
1358
1392
  - `collections` β€” how to treat collection pages (default `true`):
1359
1393
  - `true` β€” include every collection page, nested under its collection
1360
1394
  - `false` β€” exclude all collection pages (drops a blog's posts from the sidebar)
@@ -1445,7 +1479,7 @@ Generate a subscription feed for a [collection](#collections) β€” no hand-author
1445
1479
  "in": "src/markup",
1446
1480
  "out": "dist",
1447
1481
  "options": {
1448
- "feed": { "collection": "blog", "output": "blog/feed.rss" }
1482
+ "feed": { "collection": "blog", "out": "blog/feed.rss" }
1449
1483
  }
1450
1484
  }
1451
1485
  }
@@ -1454,7 +1488,7 @@ Generate a subscription feed for a [collection](#collections) β€” no hand-author
1454
1488
  **Feed options:**
1455
1489
 
1456
1490
  - `collection` β€” the collection to build the feed from. Omit it to emit a feed for **every** collection.
1457
- - `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.
1458
1492
  - `type` β€” `"rss"` (default) or `"atom"`.
1459
1493
  - `limit` β€” max items, newest first (default `20`).
1460
1494
  - `title` β€” channel title (default `"<Collection> | <site.title>"`).
@@ -1597,69 +1631,37 @@ Sets up a local server for your project.
1597
1631
  Server options:
1598
1632
 
1599
1633
  - `port` - the port on which the server will run
1600
- - `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
1601
1635
 
1602
1636
  If you don't want to run a local server, just remove the `serve` property from the config.
1603
1637
 
1604
1638
  ### Live Reload (optional)
1605
1639
 
1606
- Sets up a livereload server for your project.
1607
-
1608
- Live reload options:
1609
-
1610
- - `port` - the port on which the livereload server will run
1611
- - `exclude` - an array of files and directories to exclude from livereload
1612
- - `extraExts` - an array of extra file extensions (without the dot) that trigger a browser refresh, added to the defaults
1613
- - `exts` - an array of file extensions that replaces the default list entirely
1614
-
1615
- 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:
1616
-
1617
- ```json
1618
- {
1619
- "livereload": {
1620
- "extraExts": ["slim", "njk"]
1621
- }
1622
- }
1623
- ```
1624
-
1625
- `livereload` can only be `true`, which means that it will run on the default port (`35729`) or you can specify a port:
1626
-
1627
- ```json
1628
- {
1629
- "livereload": {
1630
- "port": whateverPortYouWant
1631
- }
1632
- }
1633
- ```
1634
-
1635
- You can also exclude files and directories from livereload:
1640
+ Reloads the browser when a build finishes. It is a switch, nothing more:
1636
1641
 
1637
1642
  ```json
1638
1643
  {
1639
- "livereload": {
1640
- "exclude": ["some_directory/**/*", "some_other_directory/**/*"]
1641
- }
1644
+ "serve": { "base": "dist" },
1645
+ "livereload": true
1642
1646
  }
1643
1647
  ```
1644
1648
 
1645
- In order for Livereload to work, you need to add the following script snippet to your HTML files in your development environment:
1646
-
1647
- ```html
1648
- <script>
1649
- document.write(
1650
- '<script src="http://' +
1651
- (location.host || "localhost").split(":")[0] +
1652
- ':35729/livereload.js?snipver=1"></' +
1653
- "script>",
1654
- );
1655
- </script>
1656
- ```
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.
1657
1655
 
1658
- Be mindful of the port, if you have specified a custom port, you need to change the port in the snippet as well.
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.
1659
1660
 
1660
- 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.
1661
+ The browser reconnects on its own, so restarting Poops picks the open tabs back
1662
+ up without touching them.
1661
1663
 
1662
- 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`.
1663
1665
 
1664
1666
  ### Watch (optional)
1665
1667
 
@@ -1687,6 +1689,14 @@ Same as `watch` property, `includePaths` accepts an array of paths to include. I
1687
1689
  }
1688
1690
  ```
1689
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
+
1690
1700
  ## Why?
1691
1701
 
1692
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
 
@@ -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)
@@ -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)
@@ -28,10 +28,38 @@ function readCacheFile(cachePath) {
28
28
  return null
29
29
  }
30
30
 
31
+ // Every lookup below keys on posix paths (the cache is keyed by path relative
32
+ // to the cache dir). A cache written on Windows carries backslash keys and
33
+ // then nothing ever matches β€” normalize once here rather than at each site.
34
+ // Not toPosix: that is a no-op on a posix host, and a cache generated on
35
+ // Windows can be read on one β€” committed alongside the images it describes,
36
+ // or built in CI. A literal backslash in an image filename is not a thing.
37
+ if (data && data.entries) data.entries = posixKeys(data.entries)
38
+
31
39
  cacheFileCache.set(cachePath, { mtimeMs: stat.mtimeMs, size: stat.size, data })
32
40
  return data
33
41
  }
34
42
 
43
+ function posixKeys(entries) {
44
+ const toSlash = (p) => String(p).replace(/\\/g, '/')
45
+ const out = {}
46
+ for (const [key, value] of Object.entries(entries)) {
47
+ // Variant paths land in srcset attributes, so they need the same treatment
48
+ out[toSlash(key)] = Array.isArray(value.outputs)
49
+ ? { ...value, outputs: value.outputs.map((o) => ({ ...o, path: toSlash(o.path) })) }
50
+ : value
51
+ }
52
+ return out
53
+ }
54
+
55
+ // Containment, the way lib/server.js does it: a plain startsWith(root) also
56
+ // accepts a sibling whose name merely starts with root's β€” `dist-old` for
57
+ // `dist` β€” so a `../` in a template's path argument reads a cache outside the
58
+ // output dir.
59
+ function isInside(dir, root) {
60
+ return dir === root || dir.startsWith(root + path.sep)
61
+ }
62
+
35
63
  export function clearImageCache() {
36
64
  cacheFileCache.clear()
37
65
  }
@@ -45,7 +73,7 @@ export function clearImageCache() {
45
73
  export function getImageEntry(imagePath, outputDir) {
46
74
  const root = path.resolve(outputDir)
47
75
  let dir = path.resolve(root, path.dirname(imagePath))
48
- if (!dir.startsWith(root)) return null
76
+ if (!isInside(dir, root)) return null
49
77
 
50
78
  const target = toPosix(path.relative(root, path.resolve(root, imagePath)))
51
79
  const targetNoExt = target.replace(/\.[^./]+$/, '')
@@ -85,7 +113,7 @@ export function listImages(dirPath, outputDir) {
85
113
  const root = path.resolve(outputDir)
86
114
  const clean = toPosix(dirPath || '').replace(/^\/+|\/+$/g, '')
87
115
  let dir = path.resolve(root, clean)
88
- if (!dir.startsWith(root)) return []
116
+ if (!isInside(dir, root)) return []
89
117
 
90
118
  while (true) {
91
119
  const data = readCacheFile(path.join(dir, CACHE_FILENAME))