nnw-theme 0.0.0 → 1.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.
Files changed (60) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +75 -1
  3. package/THIRD_PARTY_NOTICES.md +14 -0
  4. package/assets/fixtures/article.toml +24 -0
  5. package/assets/fixtures/kitchen-sink.toml +36 -0
  6. package/assets/footnotes.js +174 -0
  7. package/assets/guide/design-checklist.md +19 -0
  8. package/assets/guide/fixtures.md +42 -0
  9. package/assets/guide/publishing.md +34 -0
  10. package/assets/guide/skill.md +44 -0
  11. package/assets/guide/theme-format.md +52 -0
  12. package/assets/netnewswire/LICENSE +21 -0
  13. package/assets/netnewswire/Mac/main_mac.js +43 -0
  14. package/assets/netnewswire/Mac/page.html +12 -0
  15. package/assets/netnewswire/Shared/core.css +186 -0
  16. package/assets/netnewswire/Shared/main.js +221 -0
  17. package/assets/netnewswire/Shared/newsfoot.js +173 -0
  18. package/assets/netnewswire/iOS/main_ios.js +520 -0
  19. package/assets/netnewswire/iOS/page.html +19 -0
  20. package/assets/netnewswire/netnewswire.json +46 -0
  21. package/assets/stubs/.agents/skills/creating-nnw-themes/SKILL.md +22 -0
  22. package/assets/stubs/.agents/skills/creating-nnw-themes/agents/openai.yaml +4 -0
  23. package/assets/stubs/.github/workflows/check.yml +14 -0
  24. package/assets/stubs/.github/workflows/pages.yml +24 -0
  25. package/assets/stubs/.github/workflows/release.yml +24 -0
  26. package/assets/stubs/.github/workflows/screenshot.yml +27 -0
  27. package/assets/stubs/AGENTS.md +24 -0
  28. package/dist/browser.js +227 -0
  29. package/dist/cli.js +3 -0
  30. package/dist/commands/bump.js +23 -0
  31. package/dist/commands/capture.js +26 -0
  32. package/dist/commands/check.js +67 -0
  33. package/dist/commands/completion.js +8 -0
  34. package/dist/commands/guide.js +9 -0
  35. package/dist/commands/init.js +155 -0
  36. package/dist/commands/marketplace.js +22 -0
  37. package/dist/commands/package.js +16 -0
  38. package/dist/commands/preview.js +49 -0
  39. package/dist/commands/progress.js +31 -0
  40. package/dist/commands/release-check.js +45 -0
  41. package/dist/commands/render.js +17 -0
  42. package/dist/commands/screenshot.js +36 -0
  43. package/dist/commands/setup.js +4 -0
  44. package/dist/commands/update.js +7 -0
  45. package/dist/commands.js +175 -0
  46. package/dist/completion.js +203 -0
  47. package/dist/interactive.js +32 -0
  48. package/dist/main.js +229 -0
  49. package/dist/netnewswire.js +84 -0
  50. package/dist/package.js +28 -0
  51. package/dist/plist.js +175 -0
  52. package/dist/project.js +195 -0
  53. package/dist/pyformat.js +82 -0
  54. package/dist/render.js +516 -0
  55. package/dist/stubs.js +49 -0
  56. package/dist/urlparse.js +32 -0
  57. package/dist/validate.js +259 -0
  58. package/dist/zip.js +72 -0
  59. package/lldb/nnwdump.py +151 -0
  60. package/package.json +55 -2
@@ -0,0 +1,46 @@
1
+ {
2
+ "release": "mac-7.1.3",
3
+ "commit": "f2daca0f24f29c08d9cb16e1bdc56293fd6fe286",
4
+ "files": [
5
+ {
6
+ "destination": "LICENSE",
7
+ "source": "LICENSE",
8
+ "sha256": "712d3c84a79fa1dd7e049341797dd638f9517acfe503bb531fb7f0e18cfa1b43"
9
+ },
10
+ {
11
+ "destination": "Mac/main_mac.js",
12
+ "source": "Mac/MainWindow/Detail/main_mac.js",
13
+ "sha256": "3ac1ef46523f6c4d8103fe7dbe6f45486d57a94c3d54d0fcb6211a35215bb676"
14
+ },
15
+ {
16
+ "destination": "Mac/page.html",
17
+ "source": "Mac/MainWindow/Detail/page.html",
18
+ "sha256": "51518b58d5b320355b7e4dbf76484ba27ebf81946e963cb25f0c9821e0c9804b"
19
+ },
20
+ {
21
+ "destination": "Shared/core.css",
22
+ "source": "Shared/Article Rendering/core.css",
23
+ "sha256": "5cd07a57d6511d9e043aaaf4dc6cd544da18b9733725375928edb9b11ae96e6b"
24
+ },
25
+ {
26
+ "destination": "Shared/main.js",
27
+ "source": "Shared/Article Rendering/main.js",
28
+ "sha256": "7a229f330ca4adc7043803f044776f36fb086483d236e39947ce46a0909f7329"
29
+ },
30
+ {
31
+ "destination": "Shared/newsfoot.js",
32
+ "source": "Shared/Article Rendering/newsfoot.js",
33
+ "sha256": "6a921854ef81214eebb983314023a20417e76c64eb78e2c4742815744476ae4f"
34
+ },
35
+ {
36
+ "destination": "iOS/main_ios.js",
37
+ "source": "iOS/Resources/main_ios.js",
38
+ "sha256": "e764183ea8e9ce75df13d268834741b176afdb440eb492efeaba8ccabd7dfb8b"
39
+ },
40
+ {
41
+ "destination": "iOS/page.html",
42
+ "source": "iOS/Resources/page.html",
43
+ "sha256": "c5470f24972ac03d0c9256a6c7dcca4344094e3b7bc2b4a5164321634a966279"
44
+ }
45
+ ]
46
+ }
@@ -0,0 +1,22 @@
1
+ ---
2
+ name: creating-nnw-themes
3
+ description: Create, restyle, preview, test, package, or publish the NetNewsWire theme in this repository. Do not use for generic web design or NetNewsWire app development.
4
+ ---
5
+ <!-- nnw-theme-stub: skill v1 -->
6
+
7
+ # Create NetNewsWire themes
8
+
9
+ Read the full guide first: `npx nnw-theme@1 guide`. Details live in its topics
10
+ (`npx nnw-theme@1 guide theme-format`, `fixtures`, `design-checklist`, `publishing`),
11
+ which update with the tool.
12
+
13
+ 1. If `.nnw-theme-uninitialized` exists, run `npx nnw-theme@1 init` and let the person
14
+ approve the permanent theme identifier.
15
+ 2. Collect a compact design brief: mood, typography, color, reading needs.
16
+ 3. Edit `stylesheet.css` first; change `template.html` only when structure requires it.
17
+ 4. Run `npx nnw-theme@1 render` and inspect `build/preview/` in light and dark on each
18
+ device; explain the change and take the person's reaction.
19
+ 5. Finish with `npx nnw-theme@1 check` and give the person the preview path.
20
+
21
+ Never change the bundle name or `ThemeIdentifier` after the first release. Confirm
22
+ with the person immediately before any release or other GitHub mutation.
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "Create NetNewsWire Themes"
3
+ short_description: "Design, preview, check, and publish a theme"
4
+ default_prompt: "Use $creating-nnw-themes to help me design a readable NetNewsWire theme."
@@ -0,0 +1,14 @@
1
+ # nnw-theme-stub: check v1
2
+ # Runs the nnw-theme release gate: https://github.com/dave-atx/nnw-theme
3
+ name: Theme checks
4
+
5
+ on:
6
+ push:
7
+ pull_request:
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ check:
14
+ uses: dave-atx/nnw-theme/.github/workflows/theme-check.yml@v1
@@ -0,0 +1,24 @@
1
+ # nnw-theme-stub: pages v1
2
+ # Publishes the checked preview gallery when Pages builds from GitHub Actions.
3
+ name: Publish preview pages
4
+
5
+ on:
6
+ workflow_run:
7
+ workflows: [Theme checks]
8
+ types: [completed]
9
+
10
+ permissions:
11
+ contents: read
12
+ actions: read
13
+ pages: write
14
+ id-token: write
15
+
16
+ # One group per branch or tag: a tag pushed with main triggers a second run, which
17
+ # skips its deploy and must not cancel main's.
18
+ concurrency:
19
+ group: pages-${{ github.event.workflow_run.head_branch }}
20
+ cancel-in-progress: true
21
+
22
+ jobs:
23
+ pages:
24
+ uses: dave-atx/nnw-theme/.github/workflows/theme-pages.yml@v1
@@ -0,0 +1,24 @@
1
+ # nnw-theme-stub: release v1
2
+ name: Publish theme
3
+
4
+ on:
5
+ workflow_dispatch:
6
+ inputs:
7
+ tag:
8
+ description: Release tag beginning with v (for example, v1.0.0)
9
+ required: true
10
+ type: string
11
+ notes:
12
+ description: Optional release notes (default generated by git-cliff from .github/cliff.toml, if present)
13
+ required: false
14
+ type: string
15
+
16
+ permissions:
17
+ contents: write
18
+
19
+ jobs:
20
+ release:
21
+ uses: dave-atx/nnw-theme/.github/workflows/theme-release.yml@v1
22
+ with:
23
+ tag: ${{ inputs.tag }}
24
+ notes: ${{ inputs.notes }}
@@ -0,0 +1,27 @@
1
+ # nnw-theme-stub: screenshot v1
2
+ name: Refresh marketplace screenshot
3
+
4
+ on:
5
+ workflow_dispatch:
6
+ inputs:
7
+ fixture:
8
+ description: Fixture to capture
9
+ type: choice
10
+ options: [article, kitchen-sink]
11
+ default: article
12
+ appearance:
13
+ description: Appearance to capture
14
+ type: choice
15
+ options: [light, dark]
16
+ default: light
17
+
18
+ permissions:
19
+ contents: write
20
+ pull-requests: write
21
+
22
+ jobs:
23
+ screenshot:
24
+ uses: dave-atx/nnw-theme/.github/workflows/theme-screenshot.yml@v1
25
+ with:
26
+ fixture: ${{ inputs.fixture }}
27
+ appearance: ${{ inputs.appearance }}
@@ -0,0 +1,24 @@
1
+ <!-- nnw-theme-stub: agents v1 -->
2
+ # Repository guidance
3
+
4
+ This repository is one NetNewsWire theme: the single root `*.nnwtheme` bundle,
5
+ its `fixtures/`, and `screenshots/`. The tooling is the `nnw-theme` npm package;
6
+ run it as `npx nnw-theme@1 <command>`. Never install it globally and never add
7
+ it to this repository. Read `npx nnw-theme@1 guide` before theme work.
8
+
9
+ If `.nnw-theme-uninitialized` exists, run `npx nnw-theme@1 init` first and let
10
+ the user confirm the permanent theme identifier. Prefer CSS changes; change
11
+ `template.html` only when the requested structure or behavior requires it.
12
+ Finish theme work with `npx nnw-theme@1 check` and report the preview path.
13
+
14
+ Do not change `ThemeIdentifier` or the `.nnwtheme` bundle name after the first
15
+ release. Increase the plist `Version` (`npx nnw-theme@1 bump`) before every
16
+ release.
17
+
18
+ If the tooling misbehaves, do not work around it here: report it at
19
+ https://github.com/dave-atx/nnw-theme/issues with the `check` version line.
20
+
21
+ Local edits, renders, checks and packages are safe. Creating a GitHub release,
22
+ opening a pull request, changing repository topics, or otherwise mutating
23
+ GitHub requires the user's explicit intent. Theme and fixture HTML/JavaScript
24
+ are untrusted executable inputs; browser checks stay loopback-only.
@@ -0,0 +1,227 @@
1
+ // WebKit checks through the Playwright library: one browser per run, a fresh context
2
+ // per case. Theme and fixture HTML/JavaScript are untrusted: pages are served from
3
+ // 127.0.0.1 only, every other request is blocked and fails the case, and the only
4
+ // outputs are screenshots and results.
5
+ import { spawn } from "node:child_process";
6
+ import { existsSync, mkdirSync, readFileSync } from "node:fs";
7
+ import { createServer } from "node:http";
8
+ import { createRequire } from "node:module";
9
+ import { extname, join, normalize, sep } from "node:path";
10
+ import { packagePath, ThemeError } from "./project.js";
11
+ import { pyReprList } from "./pyformat.js";
12
+ const FOOTNOTE_CHECK = readFileSync(packagePath("assets", "footnotes.js"), "utf8");
13
+ const NOT_INSTALLED = "WebKit is not installed; run `npx nnw-theme@1 setup`";
14
+ const CONTENT_TYPES = {
15
+ ".html": "text/html; charset=utf-8",
16
+ ".png": "image/png",
17
+ ".css": "text/css; charset=utf-8",
18
+ ".js": "text/javascript; charset=utf-8",
19
+ ".svg": "image/svg+xml",
20
+ ".json": "application/json",
21
+ ".txt": "text/plain; charset=utf-8",
22
+ };
23
+ /** Serve a directory on 127.0.0.1 at a free port. */
24
+ export async function serve(directory, port = 0) {
25
+ const root = normalize(directory);
26
+ const server = createServer((request, response) => {
27
+ let path;
28
+ try {
29
+ path = decodeURIComponent(new URL(request.url ?? "/", "http://127.0.0.1").pathname);
30
+ }
31
+ catch {
32
+ response.writeHead(400).end();
33
+ return;
34
+ }
35
+ if (path.endsWith("/"))
36
+ path += "index.html";
37
+ const file = normalize(join(root, path));
38
+ if (!file.startsWith(root + sep) ||
39
+ (request.method !== "GET" && request.method !== "HEAD")) {
40
+ response.writeHead(404).end();
41
+ return;
42
+ }
43
+ let content;
44
+ try {
45
+ content = readFileSync(file);
46
+ }
47
+ catch {
48
+ response.writeHead(404).end();
49
+ return;
50
+ }
51
+ response.writeHead(200, {
52
+ "Content-Type": CONTENT_TYPES[extname(file).toLowerCase()] ?? "application/octet-stream",
53
+ "Content-Length": content.length,
54
+ "Cache-Control": "no-store",
55
+ });
56
+ response.end(request.method === "HEAD" ? undefined : content);
57
+ });
58
+ await new Promise((resolve, reject) => {
59
+ server.once("error", reject);
60
+ server.listen(port, "127.0.0.1", resolve);
61
+ });
62
+ const address = server.address();
63
+ if (!address || typeof address === "string")
64
+ throw new Error("server has no port");
65
+ return {
66
+ url: `http://127.0.0.1:${address.port}`,
67
+ close: () => new Promise((resolve) => {
68
+ server.closeAllConnections();
69
+ server.close(() => resolve());
70
+ }),
71
+ };
72
+ }
73
+ async function playwright() {
74
+ return await import("playwright-core");
75
+ }
76
+ /** Whether this version's WebKit build is installed. */
77
+ export async function webkitInstalled() {
78
+ const { webkit } = await playwright();
79
+ return existsSync(webkit.executablePath());
80
+ }
81
+ async function launch() {
82
+ const { webkit } = await playwright();
83
+ if (!existsSync(webkit.executablePath()))
84
+ throw new ThemeError(NOT_INSTALLED);
85
+ try {
86
+ return await webkit.launch();
87
+ }
88
+ catch (error) {
89
+ const message = error.message;
90
+ if (/Executable doesn't exist|install/i.test(message) && !/dependencies/i.test(message)) {
91
+ throw new ThemeError(NOT_INSTALLED);
92
+ }
93
+ throw new ThemeError(`could not start WebKit: ${message.split("\n")[0]}`);
94
+ }
95
+ }
96
+ /** Install this version's WebKit build (and, withDeps, its Linux system libraries). */
97
+ export async function setupWebkit({ withDeps = false } = {}) {
98
+ if (!withDeps && (await webkitInstalled())) {
99
+ try {
100
+ await (await launch()).close();
101
+ console.log("WebKit is already installed.");
102
+ return;
103
+ }
104
+ catch {
105
+ // Installed but not runnable: reinstall below.
106
+ }
107
+ }
108
+ console.log(withDeps
109
+ ? "Installing WebKit and its Linux runtime dependencies…"
110
+ : "Installing the WebKit browser used by theme checks…");
111
+ const cli = join(createRequire(import.meta.url).resolve("playwright-core"), "..", "cli.js");
112
+ const args = [cli, "install", ...(withDeps ? ["--with-deps"] : []), "webkit"];
113
+ const status = await new Promise((resolve, reject) => {
114
+ const child = spawn(process.execPath, args, { stdio: "inherit" });
115
+ child.once("error", reject);
116
+ child.once("exit", resolve);
117
+ });
118
+ if (status !== 0)
119
+ throw new ThemeError("WebKit installation failed; see the output above");
120
+ }
121
+ export function failures(state) {
122
+ const found = [];
123
+ if (!state.article || state.textLength < 40)
124
+ found.push("article content is missing or unreadable");
125
+ if (state.unresolved)
126
+ found.push("unresolved theme macro");
127
+ if (state.overflow)
128
+ found.push("horizontal document overflow");
129
+ if (state.brokenImages.length)
130
+ found.push(`broken images: ${pyReprList(state.brokenImages)}`);
131
+ found.push(...(state.footnotes ?? []));
132
+ if (state.blocked.length)
133
+ found.push(`external requests: ${pyReprList(state.blocked)}`);
134
+ if (state.pageErrors.length)
135
+ found.push(`page errors: ${pyReprList(state.pageErrors)}`);
136
+ return found;
137
+ }
138
+ // Runs in the page after its screenshot.
139
+ function pageState() {
140
+ const article = document.querySelector(".articleBody");
141
+ const root = document.documentElement;
142
+ return {
143
+ article: Boolean(article),
144
+ textLength: (article?.innerText || "").trim().length,
145
+ // Body only: macOS CSS legitimately keeps the font-size macro literal. Match
146
+ // whole macro names, so a theme script's regex such as /[[(]/ is no macro.
147
+ unresolved: /\[\[[A-Za-z0-9_-]+\]\]/.test(document.body.innerHTML),
148
+ overflow: root.scrollWidth > root.clientWidth + 1,
149
+ brokenImages: [...document.images]
150
+ .filter((image) => image.complete && image.naturalWidth === 0)
151
+ .map((image) => image.currentSrc || image.src),
152
+ };
153
+ }
154
+ /**
155
+ * Each target's slug mapped to its failures (empty when it passed).
156
+ *
157
+ * expectations maps a fixture name to its [expect.footnotes] table. They describe
158
+ * the theme's own footnote handling, so they are skipped with theme scripts off.
159
+ */
160
+ export async function checkPages(site, targets, progress, expectations = {}) {
161
+ const results = {};
162
+ const screenshots = join(site, "screenshots");
163
+ mkdirSync(screenshots, { recursive: true });
164
+ const server = await serve(site);
165
+ try {
166
+ progress?.status("Starting WebKit…");
167
+ const browser = await launch();
168
+ try {
169
+ for (const [offset, target] of targets.entries()) {
170
+ const index = offset + 1;
171
+ progress?.start(index, target);
172
+ const url = `${server.url}/pages/${encodeURIComponent(target.slug)}.html`;
173
+ const footnotes = target.themeScripts ? (expectations[target.fixture] ?? {}) : {};
174
+ const state = await checkPage(browser, server.url, url, target, join(screenshots, `${target.slug}.png`), footnotes);
175
+ const found = failures(state);
176
+ results[target.slug] = found;
177
+ progress?.finish(index, target, found);
178
+ }
179
+ }
180
+ finally {
181
+ await browser.close();
182
+ }
183
+ }
184
+ finally {
185
+ await server.close();
186
+ }
187
+ return results;
188
+ }
189
+ async function checkPage(browser, origin, url, target, screenshot, footnotes) {
190
+ const context = await browser.newContext({
191
+ viewport: { width: target.width, height: target.height },
192
+ colorScheme: target.appearance,
193
+ serviceWorkers: "block",
194
+ });
195
+ try {
196
+ const blocked = [];
197
+ const pageErrors = [];
198
+ await context.route("**/*", async (route) => {
199
+ const requestURL = route.request().url();
200
+ if (requestURL.startsWith("data:"))
201
+ return route.continue();
202
+ let requestOrigin = null;
203
+ try {
204
+ requestOrigin = new URL(requestURL).origin;
205
+ }
206
+ catch {
207
+ // Not a URL with an origin: blocked below.
208
+ }
209
+ // Whole-origin match: a prefix test would accept 127.0.0.1:PORT.example.net.
210
+ if (requestOrigin === origin)
211
+ return route.continue();
212
+ blocked.push(requestURL);
213
+ return route.abort("blockedbyclient");
214
+ });
215
+ const page = await context.newPage();
216
+ page.on("pageerror", (error) => pageErrors.push(String(error)));
217
+ await page.goto(url, { waitUntil: "networkidle" });
218
+ await page.screenshot({ path: screenshot, fullPage: true });
219
+ const state = await page.evaluate(pageState);
220
+ // Last: it clicks footnotes open, so it must not disturb the screenshot.
221
+ const found = (await page.evaluate(`(${FOOTNOTE_CHECK})(${JSON.stringify(footnotes)})`));
222
+ return { ...state, footnotes: found, blocked, pageErrors };
223
+ }
224
+ finally {
225
+ await context.close();
226
+ }
227
+ }
package/dist/cli.js ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { main } from "./main.js";
3
+ process.exitCode = await main();
@@ -0,0 +1,23 @@
1
+ import { join } from "node:path";
2
+ import { interactive, promptConfirm } from "../interactive.js";
3
+ import { findRoot, findTheme, readPlist, ThemeError, writePlist } from "../project.js";
4
+ export default async function bump({ values }) {
5
+ const root = findRoot();
6
+ const theme = findTheme(root);
7
+ const metadata = readPlist(theme);
8
+ const current = metadata.Version;
9
+ if (typeof current !== "number" || !Number.isInteger(current)) {
10
+ throw new ThemeError("Info.plist Version must be an integer");
11
+ }
12
+ const proposed = current + 1;
13
+ if (!values.yes) {
14
+ if (!interactive())
15
+ throw new ThemeError("pass --yes to increase Version without a terminal");
16
+ if (!(await promptConfirm(`Increase Version from ${current} to ${proposed}?`))) {
17
+ throw new ThemeError("version bump cancelled");
18
+ }
19
+ }
20
+ metadata.Version = proposed;
21
+ writePlist(join(theme, "Info.plist"), metadata);
22
+ console.log(`Version is now ${proposed}. Commit this change before publishing.`);
23
+ }
@@ -0,0 +1,26 @@
1
+ import { join } from "node:path";
2
+ import { findRoot, packagePath } from "../project.js";
3
+ export default function capture() {
4
+ const script = packagePath("lldb", "nnwdump.py");
5
+ const output = join(findRoot(), "fixtures", "my-article.toml");
6
+ console.log(`Capture a real article as a fixture from a NetNewsWire debug build (needs Xcode).
7
+
8
+ 1. Clone https://github.com/Ranchero-Software/NetNewsWire and open it in Xcode.
9
+ In Shared/Article Rendering/ArticleRenderer.swift, set a breakpoint on the
10
+ \`return d\` line at the end of articleSubstitutions(), then run the app.
11
+ 2. Select the article to capture. When the breakpoint stops, load the command
12
+ in the debugger console (once per debug session):
13
+
14
+ command script import ${script}
15
+
16
+ 3. Write the fixture, then let the app continue:
17
+
18
+ nnwdump ${output}
19
+ continue
20
+
21
+ 4. Preview it with \`npx nnw-theme@1 render my-article\`.
22
+
23
+ nnwdump embeds the feed's real icon; pass --no-icon to keep the generated one.
24
+ The debugger's working directory is /, so give nnwdump an absolute path.
25
+ The script lives in the npx cache; rerun capture if that path stops working.`);
26
+ }
@@ -0,0 +1,67 @@
1
+ import { writeFileSync } from "node:fs";
2
+ import { basename, join } from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+ import { checkPages } from "../browser.js";
5
+ import { interactive, openInBrowser, promptConfirm } from "../interactive.js";
6
+ import { version } from "../main.js";
7
+ import { buildArchive } from "../package.js";
8
+ import { findRoot, findTheme, fixturePath, footnoteExpectations, readFixture, ThemeError, themeStem, } from "../project.js";
9
+ import { checkTargets, extraFixtures, renderSite, writeGallery, } from "../render.js";
10
+ import { staleStubs } from "../stubs.js";
11
+ import { printWarnings } from "./package.js";
12
+ import { CheckProgress } from "./progress.js";
13
+ export function expectations(root, targets) {
14
+ const result = {};
15
+ for (const name of new Set(targets.map((target) => target.fixture))) {
16
+ result[name] = footnoteExpectations(readFixture(fixturePath(root, name)), `fixtures/${name}.toml`);
17
+ }
18
+ return result;
19
+ }
20
+ async function offerToOpen(index, choice) {
21
+ const open = choice ?? (interactive() && (await promptConfirm("Open the preview?", true)));
22
+ if (open)
23
+ openInBrowser(pathToFileURL(index).href);
24
+ }
25
+ export default async function check({ values }) {
26
+ const root = findRoot();
27
+ const theme = findTheme(root);
28
+ console.log(`nnw-theme ${version()}`);
29
+ printWarnings(staleStubs(root));
30
+ const targets = checkTargets(extraFixtures(root));
31
+ const expected = expectations(root, targets);
32
+ const progress = new CheckProgress(targets.length);
33
+ progress.status("Validating and packaging the theme…");
34
+ const archive = buildArchive(theme, join(root, "build", "release"), {
35
+ allowRemoteMedia: values["allow-remote-media"] === true,
36
+ });
37
+ progress.done();
38
+ printWarnings(archive.warnings);
39
+ progress.status(`Rendering ${targets.length} pages…`);
40
+ const site = renderSite(root, theme, targets);
41
+ let results;
42
+ try {
43
+ results = await checkPages(site, targets, progress, expected);
44
+ }
45
+ finally {
46
+ progress.done();
47
+ }
48
+ writeGallery(site, themeStem(theme), targets, results);
49
+ const failures = targets.flatMap((target) => (results[target.slug] ?? []).map((message) => `${target.label}: ${message}`));
50
+ const passed = targets.filter((target) => !results[target.slug]?.length).length;
51
+ const packageName = basename(archive.path);
52
+ const footer = `nnw-theme ${version()}\n`;
53
+ const report = join(root, "build", "check-report.txt");
54
+ if (failures.length) {
55
+ writeFileSync(report, `FAIL\n${failures.join("\n")}\n${footer}`, "utf8");
56
+ }
57
+ else {
58
+ writeFileSync(report, `PASS\n${targets.length} WebKit renders checked\nPackage: ${packageName}\n${footer}`, "utf8");
59
+ console.log(`PASS: ${targets.length} WebKit renders and ${packageName}`);
60
+ }
61
+ const index = join(site, "index.html");
62
+ console.log(`Preview: ${index}`);
63
+ await offerToOpen(index, values.open);
64
+ if (failures.length) {
65
+ throw new ThemeError(`${targets.length - passed} of ${targets.length} WebKit renders failed:\n- ${failures.join("\n- ")}`);
66
+ }
67
+ }
@@ -0,0 +1,8 @@
1
+ import { SCRIPTS } from "../completion.js";
2
+ import { ThemeError } from "../project.js";
3
+ export default function completion({ positionals }) {
4
+ const shell = positionals[0];
5
+ if (!shell)
6
+ throw new ThemeError("name a shell: fish, zsh, or bash");
7
+ process.stdout.write(SCRIPTS[shell]());
8
+ }
@@ -0,0 +1,9 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { GUIDE_TOPICS } from "../commands.js";
3
+ import { packagePath } from "../project.js";
4
+ export function guideText(topic = GUIDE_TOPICS[0]) {
5
+ return readFileSync(packagePath("assets", "guide", `${topic}.md`), "utf8");
6
+ }
7
+ export default function guide({ positionals }) {
8
+ process.stdout.write(guideText(positionals[0]));
9
+ }