@kenjura/ursa 0.90.1 → 0.95.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/CHANGELOG.md +78 -0
- package/README.md +102 -0
- package/bin/ursa.js +14 -3
- package/meta/templates/default-template/default.css +1574 -1323
- package/meta/templates/default-template/index.html +175 -129
- package/meta/templates/default-template/lightbox.css +225 -216
- package/meta/templates/default-template/lightbox.js +3 -0
- package/meta/templates/default-template/menu.js +9 -6
- package/meta/templates/default-template/toc-generator.js +58 -0
- package/meta/templates/default-template/widgets.js +88 -15
- package/package.json +2 -1
- package/src/dev.js +30 -1
- package/src/helper/__test__/breadcrumbs.test.js +71 -0
- package/src/helper/__test__/contentHash.test.js +114 -0
- package/src/helper/__test__/folderConfig.test.js +89 -0
- package/src/helper/automenu.js +13 -91
- package/src/helper/breadcrumbs.js +21 -6
- package/src/helper/build/__test__/autoIndex.test.js +135 -1
- package/src/helper/build/autoIndex.js +115 -46
- package/src/helper/build/ursaMetadata.js +3 -26
- package/src/helper/contentHash.js +54 -1
- package/src/helper/folderConfig.js +34 -4
- package/src/helper/menuLabels.js +136 -0
- package/src/helper/ursaVersion.js +26 -0
- package/src/jobs/__test__/generateJsonOnly.test.js +154 -0
- package/src/jobs/generate.js +302 -181
- package/meta/default.css +0 -1206
- package/meta/menu.js +0 -898
- package/meta/sectionify.js +0 -46
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,81 @@
|
|
|
1
|
+
# 0.95.0
|
|
2
|
+
2026-09-06
|
|
3
|
+
|
|
4
|
+
`generate --json-only` builds the data and skips the site.
|
|
5
|
+
|
|
6
|
+
ursa emits a `.json` beside every document and a `<dir>.json` record list beside every directory, and those files are useful on their own — an application can ingest a docs repo at build time and never serve the HTML. Getting them, though, meant paying for the whole site: rendering every page, resizing every image, bundling the React runtime, and writing a full-text index nobody would query. On the system8 docs that is 2.85s and 88 MB to obtain 8.8 MB of JSON.
|
|
7
|
+
|
|
8
|
+
- **`--json-only` (`-j`) emits only the data files.** Skipped: HTML, XML, images and their previews, meta/template assets, the React runtime, per-folder CSS/JS bundles, static file copying (fonts, audio, video, PDFs), the search and full-text indices, `menu-data.json`, `recent-activity.json`, and auto-generated index pages. On the system8 docs: **1.12s and 8.8 MB**, down from 2.85s and 88 MB.
|
|
9
|
+
- **The JSON is byte-identical to a full build's**, not merely similar — verified across all 1,475 content files of the system8 docs, and asserted in the test suite. Every step the mode skips operates on the assembled *page*: `bodyHtml` in the JSON is the pre-template render, and `transformImageTags`, `markInactiveLinks` and `resolveRelativeUrls` only ever rewrote the finished HTML. So there is nothing for image processing or template bundling to contribute to it.
|
|
10
|
+
- **The directory record lists are kept.** They are the one thing a data consumer most wants — `character/powers.json` is the list of every power with its frontmatter — so only the directory's *listing page* is skipped, never its `.json`.
|
|
11
|
+
|
|
12
|
+
Mixing modes against one source tree is safe. The `.ursa` hash cache lives in the source and is shared, so the per-document output check now asks only for the outputs the current mode emits: a JSON-only run after a full build skips work (the `.json` is present and identical either way), and a full build after a JSON-only run regenerates, because its `.html` and `.xml` are missing. That check already existed for a related reason — one hash cache serving several output directories — and it extends to modes for free.
|
|
13
|
+
|
|
14
|
+
Two pieces of build state are deliberately not written by a JSON-only run. The dependency graph is skipped because registration lives inside the page assembly the mode skips, so saving would replace a full build's graph with an empty one. The watch-mode cache is skipped because it would be seeded with unbundled templates and an empty image map, which would make a later single-file regeneration emit an unstyled page.
|
|
15
|
+
|
|
16
|
+
# 0.94.0
|
|
17
|
+
2026-09-04
|
|
18
|
+
|
|
19
|
+
`config.json { hidden: true }` now actually ignores a folder.
|
|
20
|
+
|
|
21
|
+
It was documented as "hide from menu and don't generate files", and it did neither reliably. `generate` filtered articles and directories, but every other category was derived from an unfiltered list, so a hidden folder still had its images, fonts, audio and video copied into the output and its hand-written HTML carried across. Auto-index listings never consulted the setting at all, so a hidden folder was listed — with working-looking links — in its parent's index. `serve` rendered its pages on request, which is the worst version of the bug: the page works all through development and 404s in production, exactly the failure the setting exists to prevent.
|
|
22
|
+
|
|
23
|
+
- **One filter, applied once.** `generate` now drops hidden paths from the whole source file list, ahead of classification, instead of re-checking in each category. Articles, directories, images, media and hand-written HTML all inherit it, so no category can be missed — which is how images and media came to be copied out of hidden folders in the first place.
|
|
24
|
+
- **Auto-indices skip hidden folders**, in all three listing paths: from source, from output, and the fallback index generated for a folder without one. The output-scanning path checks the *source* tree, so a folder hidden after it was generated does not reappear from stale files left in `output/`.
|
|
25
|
+
- **A folder holding nothing but a hidden subfolder is no longer treated as having content**, and so is not linked as if it had pages.
|
|
26
|
+
- **`serve` returns 404 for anything under a hidden folder** — documents, images, `config.json` itself — from a single gate ahead of the static-file fallbacks. `serve` and `generate` now agree.
|
|
27
|
+
- **The menu check no longer depends on the folder having children**, so an empty hidden folder is skipped too.
|
|
28
|
+
|
|
29
|
+
`isFolderHidden()` accepts file paths as well as directories: the walk begins at the path itself, and a file has no `config.json` of its own, so its ancestors decide. That is what lets one predicate filter a mixed list of files and directories. Its shallow companion `isFolderSelfHidden()` tests one folder without a docroot, for the auto-index builders, which know only the folder they are listing.
|
|
30
|
+
|
|
31
|
+
Files already written to `output/` before a folder was hidden are still not deleted — nothing links to them any more, but removing them needs `--clean`. That is the general "generate never deletes" behaviour, unchanged here.
|
|
32
|
+
|
|
33
|
+
# 0.93.0
|
|
34
|
+
2026-09-03
|
|
35
|
+
|
|
36
|
+
`menu-label` now renames a folder everywhere it appears, not just in the sidebar.
|
|
37
|
+
|
|
38
|
+
A folder could already override its menu label — `menu-label` in its `index.md` frontmatter, or `label` in its `config.json` — and the site-wide menu honoured it. Nothing else did. A folder called `bnw` labelled "BNW - Brave New World" in the sidebar was still "Bnw" in the auto-index listing of its parent, "Bnw" in the `<h1>` and `<title>` of its own generated index page, and "Bnw" in every breadcrumb trail passing through it. The label was doing a quarter of its job, and there was no way to fix the other three without renaming the folder on disk.
|
|
39
|
+
|
|
40
|
+
- **Auto-index listings resolve labels the same way the menu does**: `menu-label` frontmatter, then `config.json` `label`, then the folder name. This covers all three listing paths — the inline listing from `generate-auto-index: true`, the fallback index generated for folders without one, and the output-scanning variant.
|
|
41
|
+
- **Individual documents honour `menu-label` too**, so a single article can be renamed in a listing without renaming its file.
|
|
42
|
+
- **Auto-generated index pages take the folder's label** for their `<h1>` and `<title>`, instead of the raw folder name.
|
|
43
|
+
- **Breadcrumbs use folder labels** for every folder segment in the trail. The current page's own crumb now prefers its `menu-label` over its `title`; with neither, nothing changes.
|
|
44
|
+
- **`menu-sort-as` orders auto-index listings**, matching how it already orders the menu. Folders still sort ahead of files.
|
|
45
|
+
|
|
46
|
+
One naming change reaches folders with no label at all. Auto-index entries and breadcrumbs used to title-case names by lowercasing everything after the first letter, so a folder named `SoL` rendered as "Sol" and `WWII` as "Wwii". They now use the menu's own rule, which leaves interior capitals alone — the two places agree, and the disagreement they had was the bug.
|
|
47
|
+
|
|
48
|
+
The resolution rules live in `helper/menuLabels.js`, which the menu, the auto-index and the breadcrumbs all import, so the three cannot drift apart again.
|
|
49
|
+
|
|
50
|
+
Auto-index pages still have no dependency edge to the documents they list, so on a warm incremental rebuild a label edit in one folder does not regenerate a sibling listing that names it. That predates this change — adding or renaming a document went stale the same way — and `--clean` or a full generate is unaffected.
|
|
51
|
+
|
|
52
|
+
# 0.92.0
|
|
53
|
+
2026-08-27
|
|
54
|
+
|
|
55
|
+
Upgrading ursa now invalidates the build cache, so an upgrade takes effect without `--clean`.
|
|
56
|
+
|
|
57
|
+
`.ursa/` caches a content hash per source document, and a document whose hash is unchanged is skipped entirely. But the hash only describes the *source*. It says nothing about the templates, renderers and asset bundles that turned that source into HTML — all of which live in ursa itself. So installing a new ursa over a warm cache left every unchanged document frozen at whatever the previous version produced: new template markup didn't appear, renderer fixes didn't apply, and even the ursa version in the page footer stayed at the old number. The workaround was to remember to run `--clean` after every upgrade, which is most of what made `--clean` feel mandatory in the first place.
|
|
58
|
+
|
|
59
|
+
- **`.ursa/` is stamped with the ursa version that wrote it.** On a mismatch the whole directory is discarded — hashes, dependency graph, nav cache and search index together — and the build starts cold. Upgrades and downgrades both count; so does a cache left by a version too old to have written a stamp, and a stamp that can't be parsed.
|
|
60
|
+
- **Matching stamps cost nothing.** A warm rebuild on the same version still skips every unchanged document, exactly as before.
|
|
61
|
+
- **`--clean` is unaffected**, and leaves behind a stamp the next run accepts, so it no longer costs an extra cold build.
|
|
62
|
+
|
|
63
|
+
This was the last unimplemented rule in the 0.76.0 cache-invalidation list; the other four (document, inherited `style.css`/`menu.md`, template, static asset) were already in place.
|
|
64
|
+
|
|
65
|
+
# 0.91.0
|
|
66
|
+
2026-08-27
|
|
67
|
+
|
|
68
|
+
Ursa's built-in CSS is scoped with `@scope` and layered with `@layer`, so a site's own stylesheet no longer has to fight it.
|
|
69
|
+
|
|
70
|
+
Styling a site had turned into a specificity war. Ursa's stylesheet is loaded before the site's, but it styles content through selectors like `article#main-content h1` — an ID and two elements — so a site author writing the obvious `h1 { … }` lost, and kept losing until they either copied Ursa's selectors or reached for `!important`. Worse, rules meant for the frame leaked into documents and rules meant for documents leaked into the frame, because everything shared one flat global scope.
|
|
71
|
+
|
|
72
|
+
- **Content styles are layered.** Everything Ursa applies to a document body — headings, images, figures, the article box, the sticky headings — now lives in `@layer ursa.content`. A site's `style.css` is unlayered, and an unlayered declaration beats a layered one regardless of specificity, so `h1 { position: static }` or `#main-content { width: 1000px }` in a site stylesheet simply wins, at any specificity.
|
|
73
|
+
- **Chrome is scoped, not layered.** The top bar, menus, widgets, search and footer sit in `@scope (body) to (#main-content > *, article > *, .ursa-unstyled)`, so none of it can reach into a document's own markup — and, being unlayered, a stray `a { … }` in a site stylesheet still can't wreck the navigation. Chrome's selectors are unchanged, so overriding them works the way it always did — with a more specific selector, since a scoped rule now wins a specificity tie against an unscoped one. Breadcrumbs, the image hover controls and the lightbox get their own scopes on the same terms, since they are Ursa's furniture even though they render inside — or on top of — the document.
|
|
74
|
+
- **`class="ursa-unstyled"`** on any element puts it and everything inside it outside every one of Ursa's scopes. Not "override the defaults" — Ursa's CSS does not apply in there at all, down to the strike-through on dead links. The lightbox leaves images in there alone too, rather than injecting controls it has no styles for.
|
|
75
|
+
- **Nothing looks different.** The reorganisation is a pure cascade change. Verified by diffing every computed property and every bounding box of every element, before and after, across four pages, both colour schemes, desktop and mobile widths, and eight interaction states (top menu, collapsed and open side menu, stuck headings, open widgets, search results, inactive links, open lightbox): zero differences outside `.ursa-unstyled`.
|
|
76
|
+
|
|
77
|
+
Requires `@scope`: Chrome 118+, Safari 17.4+, Firefox 128+.
|
|
78
|
+
|
|
1
79
|
# 0.90.1
|
|
2
80
|
2026-08-29
|
|
3
81
|
|
package/README.md
CHANGED
|
@@ -67,6 +67,35 @@ Start a development server that:
|
|
|
67
67
|
- `--whitelist, -w` - Path to whitelist file containing patterns for files to include
|
|
68
68
|
- `--exclude, -e` - Folders to exclude: comma-separated paths relative to source, or path to file with one folder per line
|
|
69
69
|
- `--clean` - Delete the `.ursa` cache folder and clear output directory, forcing full regeneration
|
|
70
|
+
- `--json-only, -j` - Emit only the `.json` data files (generate command only)
|
|
71
|
+
|
|
72
|
+
### JSON-Only Builds
|
|
73
|
+
|
|
74
|
+
`--json-only` builds the data and skips the site:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
ursa content --json-only --output data
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
What it emits: every document's `<name>.json` and every directory's `<dir>.json`
|
|
81
|
+
record list — the same files a normal build writes, byte for byte.
|
|
82
|
+
|
|
83
|
+
What it skips: HTML, XML, images and their previews, meta/template assets, the
|
|
84
|
+
React runtime, per-folder CSS/JS bundles, static file copying (fonts, audio,
|
|
85
|
+
video, PDFs), the search and full-text indices, `menu-data.json`,
|
|
86
|
+
`recent-activity.json`, and auto-generated index pages.
|
|
87
|
+
|
|
88
|
+
This is for pipelines that consume ursa's JSON as data rather than publishing a
|
|
89
|
+
site — ingesting a docs repo into an application at build time, for example.
|
|
90
|
+
Everything skipped operates on the assembled *page*; the JSON's `bodyHtml` is
|
|
91
|
+
the pre-template render, which none of those steps touch. That is why the output
|
|
92
|
+
is identical rather than merely similar.
|
|
93
|
+
|
|
94
|
+
Mixing modes against one source tree is safe. The two share the `.ursa` hash
|
|
95
|
+
cache, but each asks only for the outputs its own mode emits, so a full build
|
|
96
|
+
following a JSON-only build still writes the HTML it is missing. A JSON-only
|
|
97
|
+
build does not write the dependency graph or seed the watch cache, so it cannot
|
|
98
|
+
degrade a later `serve`.
|
|
70
99
|
|
|
71
100
|
### Whitelist File Format
|
|
72
101
|
|
|
@@ -124,6 +153,44 @@ old-content/v1
|
|
|
124
153
|
test/fixtures
|
|
125
154
|
```
|
|
126
155
|
|
|
156
|
+
### Ignoring a Folder
|
|
157
|
+
|
|
158
|
+
To keep a folder out of the build permanently — working notes, prompt
|
|
159
|
+
scratchpads, raw source material — put a `config.json` in it:
|
|
160
|
+
|
|
161
|
+
```json
|
|
162
|
+
{
|
|
163
|
+
"hidden": true
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
The folder and everything beneath it then take no part in the build:
|
|
168
|
+
|
|
169
|
+
- no HTML is rendered from its documents
|
|
170
|
+
- its images, fonts and other static assets are not copied to the output
|
|
171
|
+
- it does not appear in the sidebar menu, in any auto-index, or in breadcrumbs
|
|
172
|
+
- its text is not added to the search index
|
|
173
|
+
- `ursa serve` returns 404 for anything under it, matching what `generate`
|
|
174
|
+
produces
|
|
175
|
+
|
|
176
|
+
The files stay where they are in the source tree; the site simply behaves as
|
|
177
|
+
though they were not there.
|
|
178
|
+
|
|
179
|
+
This differs from `--exclude` in scope and in lifetime: `--exclude` is a flag on
|
|
180
|
+
one invocation, useful for a one-off or a per-environment build, while
|
|
181
|
+
`config.json` travels with the content and applies to every build and every
|
|
182
|
+
person who checks the repo out.
|
|
183
|
+
|
|
184
|
+
`config.json` accepts a few other keys, all of which apply to the folder it
|
|
185
|
+
sits in:
|
|
186
|
+
|
|
187
|
+
| Key | Type | Meaning |
|
|
188
|
+
| --- | --- | --- |
|
|
189
|
+
| `hidden` | boolean | Ignore this folder and its subtree entirely (above) |
|
|
190
|
+
| `label` | string | Name to show for this folder in menus and indices |
|
|
191
|
+
| `icon` | string | URL of an icon to show beside it in the menu |
|
|
192
|
+
| `openMenuItems` | string[] | Root `config.json` only: folders to expand by default |
|
|
193
|
+
|
|
127
194
|
### Large Workloads
|
|
128
195
|
|
|
129
196
|
For sites with many documents (hundreds or thousands), you may need to increase Node.js memory limits:
|
|
@@ -216,6 +283,41 @@ your-project/
|
|
|
216
283
|
└── output/ # Generated site (created automatically)
|
|
217
284
|
```
|
|
218
285
|
|
|
286
|
+
## Styling a Site
|
|
287
|
+
|
|
288
|
+
Drop a `style.css` (or `style-ursa.css`, or `_style.css`) in any source folder and it applies to every document in that folder and below. Every such file from the docroot down to the document's own folder is included, nearest last, so a deeper file overrides a shallower one.
|
|
289
|
+
|
|
290
|
+
Ursa's own stylesheet is scoped and layered so that your CSS wins without a fight:
|
|
291
|
+
|
|
292
|
+
- **Document content** — headings, images, figures, the article box itself — is styled inside `@layer ursa.content`. Your stylesheet is unlayered, and an unlayered rule beats a layered one no matter how specific it is, so a plain `h1 { position: static }` or `#main-content { width: 1000px }` overrides whatever Ursa sets. No `article#main-content h1` escalation, no `!important`.
|
|
293
|
+
- **Chrome** — the top bar, menus, widgets, search, footer, breadcrumbs, image hover controls and lightbox — is scoped but *not* layered, so a broad rule like `a { color: … }` in your stylesheet cannot bleed into the navigation. Overriding chrome works as it always did: use a more specific selector than the built-in one.
|
|
294
|
+
- **Nothing built in reaches into content it shouldn't.** Chrome rules stop at the article's children; content rules stop at the article's edge.
|
|
295
|
+
- **`class="ursa-unstyled"`** on any element puts it and its descendants outside every one of Ursa's scopes — none of Ursa's CSS applies in there at all, and the lightbox leaves images in there alone.
|
|
296
|
+
|
|
297
|
+
This uses the CSS `@scope` and `@layer` rules: Chrome 118+, Safari 17.4+, Firefox 128+.
|
|
298
|
+
|
|
299
|
+
### Styling one widget at a time
|
|
300
|
+
|
|
301
|
+
The two widget panels are shared containers — the right-hand one holds the table
|
|
302
|
+
of contents, search and profile in turn — so styling `.widget-dropdown` styles
|
|
303
|
+
all of them at once. While a panel is open it carries `data-active-widget` naming
|
|
304
|
+
whichever widget is showing, and the attribute is removed when it closes, so a
|
|
305
|
+
site can give each one its own treatment:
|
|
306
|
+
|
|
307
|
+
```css
|
|
308
|
+
/* Only the table of contents; search and profile keep the default panel. */
|
|
309
|
+
.widget-dropdown[data-active-widget="toc"] {
|
|
310
|
+
background: rgba(20, 24, 28, 0.78);
|
|
311
|
+
backdrop-filter: blur(10px);
|
|
312
|
+
}
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
Both panels carry it: `#widget-dropdown` for the right-hand widgets (`toc`,
|
|
316
|
+
`search`, `profile`) and `#widget-dropdown-left` for the left-hand ones
|
|
317
|
+
(`recent-activity`, `suggested`). The value is the widget's `data-widget` name.
|
|
318
|
+
Ursa uses this hook itself, to lay the TOC out along the bottom of the viewport
|
|
319
|
+
on a narrow screen.
|
|
320
|
+
|
|
219
321
|
## Auto-Index Generation
|
|
220
322
|
|
|
221
323
|
Ursa automatically generates index pages for folders that don't have one. You can also explicitly control auto-index generation in your index documents using frontmatter:
|
package/bin/ursa.js
CHANGED
|
@@ -54,6 +54,12 @@ yargs(hideBin(process.argv))
|
|
|
54
54
|
.option('promote-changelog', {
|
|
55
55
|
describe: 'Path to a markdown file to render at the output root (sibling of index.html)',
|
|
56
56
|
type: 'string'
|
|
57
|
+
})
|
|
58
|
+
.option('json-only', {
|
|
59
|
+
alias: 'j',
|
|
60
|
+
describe: 'Emit only the .json data files — no HTML, XML, images, static assets, search indices or menu data',
|
|
61
|
+
type: 'boolean',
|
|
62
|
+
default: false
|
|
57
63
|
});
|
|
58
64
|
},
|
|
59
65
|
async (argv) => {
|
|
@@ -64,7 +70,8 @@ yargs(hideBin(process.argv))
|
|
|
64
70
|
const exclude = argv.exclude || null;
|
|
65
71
|
const clean = argv.clean;
|
|
66
72
|
const promoteChangelog = argv['promote-changelog'] || null;
|
|
67
|
-
|
|
73
|
+
const jsonOnly = argv['json-only'];
|
|
74
|
+
|
|
68
75
|
console.log(`Generating site from ${source} to ${output} using meta from ${meta}`);
|
|
69
76
|
if (whitelist) {
|
|
70
77
|
console.log(`Using whitelist: ${whitelist}`);
|
|
@@ -75,7 +82,10 @@ yargs(hideBin(process.argv))
|
|
|
75
82
|
if (clean) {
|
|
76
83
|
console.log(`Clean build: ignoring cached hashes`);
|
|
77
84
|
}
|
|
78
|
-
|
|
85
|
+
if (jsonOnly) {
|
|
86
|
+
console.log(`JSON-only build: emitting .json data files only`);
|
|
87
|
+
}
|
|
88
|
+
|
|
79
89
|
let promoted = { stagedFile: null, cleanup: async () => {} };
|
|
80
90
|
try {
|
|
81
91
|
promoted = await stagePromotedChangelog({ changelogPath: promoteChangelog, sourceDir: source });
|
|
@@ -85,7 +95,8 @@ yargs(hideBin(process.argv))
|
|
|
85
95
|
_output: output,
|
|
86
96
|
_whitelist: whitelist,
|
|
87
97
|
_exclude: exclude,
|
|
88
|
-
_clean: clean
|
|
98
|
+
_clean: clean,
|
|
99
|
+
_jsonOnly: jsonOnly
|
|
89
100
|
});
|
|
90
101
|
console.log('Site generation completed successfully!');
|
|
91
102
|
} catch (error) {
|