nnw-theme 2.2.0 → 2.4.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 +2 -2
- package/THIRD_PARTY_NOTICES.md +2 -2
- package/assets/catalog/mastodon.toml +50 -0
- package/assets/catalog/media-rss.toml +79 -0
- package/assets/catalog/podcast.toml +52 -0
- package/assets/enclosures.js +113 -0
- package/assets/footnotes.js +39 -6
- package/assets/guide/design-checklist.md +6 -0
- package/assets/guide/fixtures.md +64 -4
- package/assets/guide/skill.md +2 -1
- package/assets/guide/theme-format.md +48 -2
- package/assets/netnewswire/Mac/main_mac.js +5 -0
- package/assets/netnewswire/Shared/core.css +25 -7
- package/assets/netnewswire/Shared/main.js +73 -1
- package/assets/netnewswire/Shared/newsfoot.js +24 -27
- package/assets/netnewswire/iOS/main_ios.js +1 -1
- package/assets/netnewswire/netnewswire.json +7 -7
- package/dist/browser.js +30 -6
- package/dist/commands/check.js +26 -7
- package/dist/commands/preview.js +2 -2
- package/dist/commands/render.js +2 -2
- package/dist/commands/screenshot.js +7 -2
- package/dist/commands.js +12 -2
- package/dist/delivery.js +516 -0
- package/dist/enclosures.js +299 -0
- package/dist/project.js +11 -2
- package/dist/render.js +220 -21
- package/lldb/nnwdump.py +87 -5
- package/package.json +1 -1
|
@@ -63,6 +63,30 @@ img.twemoji, img.emoji {
|
|
|
63
63
|
width: auto;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
/* Enclosures: audio and video players and images after the article body */
|
|
67
|
+
|
|
68
|
+
.nnw-enclosures {
|
|
69
|
+
margin-top: 1.5em;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.nnw-enclosure {
|
|
73
|
+
margin: 1em 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.nnw-enclosure audio {
|
|
77
|
+
width: 100%;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.nnw-enclosure video,
|
|
81
|
+
.nnw-enclosure img {
|
|
82
|
+
height: auto;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.nnw-enclosure-caption {
|
|
86
|
+
margin: 0.5em 0 0;
|
|
87
|
+
font-size: 0.9em;
|
|
88
|
+
}
|
|
89
|
+
|
|
66
90
|
/* Hide the external link at the bottom of Daring Fireball posts */
|
|
67
91
|
|
|
68
92
|
.x-netnewswire-hide {
|
|
@@ -128,25 +152,19 @@ img[src*="/img/social/"] {
|
|
|
128
152
|
position: absolute;
|
|
129
153
|
display: block;
|
|
130
154
|
padding: 0em 1em;
|
|
131
|
-
margin: 1em
|
|
155
|
+
margin: 1em 0;
|
|
132
156
|
top: 0.75em;
|
|
133
157
|
max-width: 44em;
|
|
134
158
|
border-radius: 0.3em;
|
|
135
159
|
box-sizing: border-box;
|
|
136
160
|
}
|
|
137
161
|
|
|
138
|
-
.newsfoot-footnote-popover {
|
|
139
|
-
left: calc(-1 * (50vw - 1em));
|
|
140
|
-
right: calc(-1 * (50vw - 1em));
|
|
141
|
-
}
|
|
142
|
-
|
|
143
162
|
.newsfoot-footnote-popover-arrow {
|
|
144
163
|
content: '';
|
|
145
164
|
display: block;
|
|
146
165
|
width: 1em;
|
|
147
166
|
position: absolute;
|
|
148
167
|
top: -0.5em;
|
|
149
|
-
left: calc(50% - 0.5em);
|
|
150
168
|
height: 1em !important;
|
|
151
169
|
transform: rotate(45deg);
|
|
152
170
|
z-index:0;
|
|
@@ -69,12 +69,23 @@ function constrainBodyRelativeIframes() {
|
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
// An image served from the timeline’s copy falls back to its original URL when the copy can’t be loaded.
|
|
73
|
+
// Registered at document start, and capturing, since error events don’t bubble.
|
|
74
|
+
document.addEventListener("error", event => {
|
|
75
|
+
const element = event.target;
|
|
76
|
+
if (element instanceof HTMLImageElement && element.hasAttribute("data-nnw-src")) {
|
|
77
|
+
const originalSrc = element.getAttribute("data-nnw-src");
|
|
78
|
+
element.removeAttribute("data-nnw-src");
|
|
79
|
+
element.src = originalSrc;
|
|
80
|
+
}
|
|
81
|
+
}, true);
|
|
82
|
+
|
|
72
83
|
// Convert all Feedbin proxy images to be used as src, otherwise change image locations to be absolute if not already
|
|
73
84
|
function convertImgSrc() {
|
|
74
85
|
document.querySelectorAll("img").forEach(element => {
|
|
75
86
|
if (element.hasAttribute("data-canonical-src")) {
|
|
76
87
|
element.src = element.getAttribute("data-canonical-src")
|
|
77
|
-
} else if (!/^[a-z]+\:\/\//i.test(element.src)) {
|
|
88
|
+
} else if (!element.hasAttribute("data-nnw-src") && !/^[a-z]+\:\/\//i.test(element.src)) {
|
|
78
89
|
element.src = new URL(element.src, document.baseURI).href;
|
|
79
90
|
}
|
|
80
91
|
});
|
|
@@ -169,6 +180,67 @@ function stopMediaPlayback() {
|
|
|
169
180
|
});
|
|
170
181
|
}
|
|
171
182
|
|
|
183
|
+
// The element at the top of the viewport, how far through it the viewport top is,
|
|
184
|
+
// and any remaining distance. A scroll offset alone means nothing once the article
|
|
185
|
+
// reflows at a different width.
|
|
186
|
+
let scrollAnchorElement = null;
|
|
187
|
+
let scrollAnchorFraction = 0;
|
|
188
|
+
let scrollAnchorOffset = 0;
|
|
189
|
+
|
|
190
|
+
const scrollAnchorSampleSpacing = 16;
|
|
191
|
+
|
|
192
|
+
// False for a container hit in the gap between blocks: it has element children
|
|
193
|
+
// and only whitespace text of its own.
|
|
194
|
+
function isScrollAnchorCandidate(element) {
|
|
195
|
+
if (element.children.length === 0) {
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
for (const node of element.childNodes) {
|
|
199
|
+
if (node.nodeType === Node.TEXT_NODE && node.textContent.trim() !== "") {
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function findScrollAnchorElement() {
|
|
207
|
+
const x = window.innerWidth / 2;
|
|
208
|
+
const maximumSampleDepth = window.innerHeight / 4;
|
|
209
|
+
let firstHit = null;
|
|
210
|
+
for (let y = 1; y <= maximumSampleDepth; y += scrollAnchorSampleSpacing) {
|
|
211
|
+
const element = document.elementFromPoint(x, y);
|
|
212
|
+
if (!element) {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (isScrollAnchorCandidate(element)) {
|
|
216
|
+
return element;
|
|
217
|
+
}
|
|
218
|
+
if (!firstHit) {
|
|
219
|
+
firstHit = element;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return firstHit;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function recordScrollAnchor() {
|
|
226
|
+
scrollAnchorElement = findScrollAnchorElement();
|
|
227
|
+
if (!scrollAnchorElement) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const rect = scrollAnchorElement.getBoundingClientRect();
|
|
231
|
+
scrollAnchorFraction = rect.height > 0 ? Math.min(Math.max(-rect.top / rect.height, 0), 1) : 0;
|
|
232
|
+
scrollAnchorOffset = -(rect.top + scrollAnchorFraction * rect.height);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Scrolls back to the same place in the recorded element after a reflow.
|
|
236
|
+
function restoreScrollAnchor() {
|
|
237
|
+
if (!scrollAnchorElement || !scrollAnchorElement.isConnected) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const rect = scrollAnchorElement.getBoundingClientRect();
|
|
241
|
+
window.scrollTo(0, window.scrollY + rect.top + scrollAnchorFraction * rect.height + scrollAnchorOffset);
|
|
242
|
+
}
|
|
243
|
+
|
|
172
244
|
function error() {
|
|
173
245
|
document.body.innerHTML = "error";
|
|
174
246
|
}
|
|
@@ -15,17 +15,6 @@
|
|
|
15
15
|
return el;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
/** @type {<T extends any[]>(fn: (...args: T) => void, t: number) => ((...args: T) => void)} */
|
|
19
|
-
function debounce(f, ms) {
|
|
20
|
-
let t = Date.now();
|
|
21
|
-
return (...args) => {
|
|
22
|
-
const now = Date.now();
|
|
23
|
-
if (now - t < ms) return;
|
|
24
|
-
t = now;
|
|
25
|
-
f(...args);
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
|
|
29
18
|
const clsPrefix = "newsfoot-footnote-";
|
|
30
19
|
const CONTAINER_CLS = `${clsPrefix}container`;
|
|
31
20
|
const POPOVER_CLS = `${clsPrefix}popover`;
|
|
@@ -76,7 +65,7 @@
|
|
|
76
65
|
}
|
|
77
66
|
document.addEventListener("click", this.clickoutHandler, {capture: true});
|
|
78
67
|
|
|
79
|
-
this.resizeHandler =
|
|
68
|
+
this.resizeHandler = () => this.reposition();
|
|
80
69
|
window.addEventListener("resize", this.resizeHandler);
|
|
81
70
|
}
|
|
82
71
|
|
|
@@ -92,23 +81,31 @@
|
|
|
92
81
|
|
|
93
82
|
reposition() {
|
|
94
83
|
const refRect = this.fnref.getBoundingClientRect();
|
|
95
|
-
const
|
|
96
|
-
const
|
|
97
|
-
const
|
|
98
|
-
const
|
|
84
|
+
const containerRect = this.fnref.closest(`.${CONTAINER_CLS}`).getBoundingClientRect();
|
|
85
|
+
const articleBody = this.fnref.closest(".articleBody");
|
|
86
|
+
const visibleRect = articleBody ? articleBody.getBoundingClientRect() : document.documentElement.getBoundingClientRect();
|
|
87
|
+
const maximumWidth = stripPx(this.style.maxWidth);
|
|
88
|
+
const popoverWidth = Math.min(visibleRect.width, maximumWidth);
|
|
89
|
+
const refCenter = refRect.left + (refRect.width / 2);
|
|
90
|
+
const minimumLeft = visibleRect.left;
|
|
91
|
+
const maximumLeft = visibleRect.right - popoverWidth;
|
|
92
|
+
const popoverLeft = Math.min(
|
|
93
|
+
Math.max(refCenter - (popoverWidth / 2), minimumLeft),
|
|
94
|
+
maximumLeft
|
|
95
|
+
);
|
|
99
96
|
|
|
100
|
-
|
|
101
|
-
|
|
97
|
+
// Keep the popover inside the visible article column while centering it on the
|
|
98
|
+
// footnote reference whenever there is enough room.
|
|
99
|
+
this.popover.style.left = `${popoverLeft - containerRect.left}px`;
|
|
100
|
+
this.popover.style.width = `${popoverWidth}px`;
|
|
102
101
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
110
|
-
this.popover.style.transform = `translate(${offset}px)`;
|
|
111
|
-
this.arrow.style.transform = `translate(${-offset}px) rotate(45deg)`;
|
|
102
|
+
const arrowWidth = this.arrow.clientWidth;
|
|
103
|
+
const arrowCenter = refCenter - popoverLeft;
|
|
104
|
+
const insetArrowCenter = Math.min(
|
|
105
|
+
Math.max(arrowCenter, arrowWidth),
|
|
106
|
+
popoverWidth - arrowWidth
|
|
107
|
+
);
|
|
108
|
+
this.arrow.style.left = `${insetArrowCenter - (arrowWidth / 2)}px`;
|
|
112
109
|
}
|
|
113
110
|
}
|
|
114
111
|
|
|
@@ -37,7 +37,7 @@ class ImageViewer {
|
|
|
37
37
|
const rect = this.img.getBoundingClientRect();
|
|
38
38
|
|
|
39
39
|
// Instead of trying to convert to canvas (which fails with CORS),
|
|
40
|
-
// send the
|
|
40
|
+
// send the image's src URL. Swift checks a routed timeline image's signature.
|
|
41
41
|
const message = {
|
|
42
42
|
x: rect.x,
|
|
43
43
|
y: rect.y,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"release": "mac-7.
|
|
3
|
-
"commit": "
|
|
2
|
+
"release": "mac-7.3b4",
|
|
3
|
+
"commit": "e530be1ab19f68fe322850bf5fb95bca4b36fb91",
|
|
4
4
|
"files": [
|
|
5
5
|
{
|
|
6
6
|
"destination": "LICENSE",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
{
|
|
11
11
|
"destination": "Mac/main_mac.js",
|
|
12
12
|
"source": "Mac/MainWindow/Detail/main_mac.js",
|
|
13
|
-
"sha256": "
|
|
13
|
+
"sha256": "51c8812b9063d70aeb9a163a224bc83954f127dfb78736b2016d6572dd9ab63f"
|
|
14
14
|
},
|
|
15
15
|
{
|
|
16
16
|
"destination": "Mac/page.html",
|
|
@@ -20,22 +20,22 @@
|
|
|
20
20
|
{
|
|
21
21
|
"destination": "Shared/core.css",
|
|
22
22
|
"source": "Shared/Article Rendering/core.css",
|
|
23
|
-
"sha256": "
|
|
23
|
+
"sha256": "1cc7d654559b3cd6db1202391c6fcfb8041eaae6fc81850df1be72ebb6921090"
|
|
24
24
|
},
|
|
25
25
|
{
|
|
26
26
|
"destination": "Shared/main.js",
|
|
27
27
|
"source": "Shared/Article Rendering/main.js",
|
|
28
|
-
"sha256": "
|
|
28
|
+
"sha256": "1c3c1084e295f83668a427b8ae89813a1df621cfb053e8192bbad3f044be3581"
|
|
29
29
|
},
|
|
30
30
|
{
|
|
31
31
|
"destination": "Shared/newsfoot.js",
|
|
32
32
|
"source": "Shared/Article Rendering/newsfoot.js",
|
|
33
|
-
"sha256": "
|
|
33
|
+
"sha256": "8e0a5fd14b2b5e3b57e6d420388afa2999a8df8af56f957c16303834ad53ed61"
|
|
34
34
|
},
|
|
35
35
|
{
|
|
36
36
|
"destination": "iOS/main_ios.js",
|
|
37
37
|
"source": "iOS/Resources/main_ios.js",
|
|
38
|
-
"sha256": "
|
|
38
|
+
"sha256": "ab60f228e06b11589aefaac579a5ea42d1ff5ecf43aab44e2eba7679a18d6ef2"
|
|
39
39
|
},
|
|
40
40
|
{
|
|
41
41
|
"destination": "iOS/page.html",
|
package/dist/browser.js
CHANGED
|
@@ -10,6 +10,8 @@ import { extname, join, normalize, sep } from "node:path";
|
|
|
10
10
|
import { packagePath, ThemeError } from "./project.js";
|
|
11
11
|
import { pyReprList } from "./pyformat.js";
|
|
12
12
|
const FOOTNOTE_CHECK = readFileSync(packagePath("assets", "footnotes.js"), "utf8");
|
|
13
|
+
const ENCLOSURE_CHECK = readFileSync(packagePath("assets", "enclosures.js"), "utf8");
|
|
14
|
+
const ENCLOSURES_MACRO = "[[enclosures]]";
|
|
13
15
|
const LIVE_RELOAD = readFileSync(packagePath("assets", "live-reload.js"), "utf8");
|
|
14
16
|
const NOT_INSTALLED = "WebKit is not installed; run `npx nnw-theme@2 setup`";
|
|
15
17
|
const CONTENT_TYPES = {
|
|
@@ -148,6 +150,10 @@ export function failures(state) {
|
|
|
148
150
|
found.push("article content is missing or unreadable");
|
|
149
151
|
if (state.unresolved)
|
|
150
152
|
found.push("unresolved theme macro");
|
|
153
|
+
if (state.literalEnclosures) {
|
|
154
|
+
found.push("[[enclosures]] shows as text in NetNewsWire releases without enclosure support; " +
|
|
155
|
+
"hide its element when it holds no .nnw-enclosures");
|
|
156
|
+
}
|
|
151
157
|
if (state.overflow)
|
|
152
158
|
found.push("horizontal document overflow");
|
|
153
159
|
if (state.brokenImages.length)
|
|
@@ -156,6 +162,7 @@ export function failures(state) {
|
|
|
156
162
|
found.push(`images drawn at zero size: ${pyReprList(state.collapsedImages)}`);
|
|
157
163
|
if (state.unloadedImages.length)
|
|
158
164
|
found.push(`images still loading: ${pyReprList(state.unloadedImages)}`);
|
|
165
|
+
found.push(...(state.enclosures ?? []));
|
|
159
166
|
found.push(...(state.footnotes ?? []));
|
|
160
167
|
if (state.blocked.length)
|
|
161
168
|
found.push(`external requests: ${pyReprList(state.blocked)}`);
|
|
@@ -178,16 +185,19 @@ async function loadImages(timeout) {
|
|
|
178
185
|
new Promise((resolve) => setTimeout(resolve, timeout)),
|
|
179
186
|
]);
|
|
180
187
|
}
|
|
181
|
-
// Runs in the page after its screenshot.
|
|
182
|
-
|
|
188
|
+
// Runs in the page after its screenshot. With olderNetNewsWire, the template's
|
|
189
|
+
// [[enclosures]] is expected in the markup and fails only if it shows as text.
|
|
190
|
+
function pageState({ olderNetNewsWire, macro }) {
|
|
183
191
|
const article = document.querySelector(".articleBody");
|
|
184
192
|
const root = document.documentElement;
|
|
193
|
+
const html = document.body.innerHTML;
|
|
185
194
|
return {
|
|
186
195
|
article: Boolean(article),
|
|
187
196
|
textLength: (article?.innerText || "").trim().length,
|
|
188
197
|
// Body only: macOS CSS legitimately keeps the font-size macro literal. Match
|
|
189
198
|
// whole macro names, so a theme script's regex such as /[[(]/ is no macro.
|
|
190
|
-
unresolved: /\[\[[A-Za-z0-9_-]+\]\]/.test(
|
|
199
|
+
unresolved: /\[\[[A-Za-z0-9_-]+\]\]/.test(olderNetNewsWire ? html.replaceAll(macro, "") : html),
|
|
200
|
+
literalEnclosures: olderNetNewsWire && document.body.innerText.includes(macro),
|
|
191
201
|
overflow: root.scrollWidth > root.clientWidth + 1,
|
|
192
202
|
brokenImages: [...document.images]
|
|
193
203
|
.filter((image) => image.complete && image.naturalWidth === 0)
|
|
@@ -222,8 +232,9 @@ function pageState() {
|
|
|
222
232
|
*
|
|
223
233
|
* expectations maps a fixture name to its [expect.footnotes] table. They describe
|
|
224
234
|
* the theme's own footnote handling, so they are skipped with theme scripts off.
|
|
235
|
+
* warnings, when given, receives each target's warnings by slug.
|
|
225
236
|
*/
|
|
226
|
-
export async function checkPages(site, targets, progress, expectations = {}) {
|
|
237
|
+
export async function checkPages(site, targets, progress, expectations = {}, warnings) {
|
|
227
238
|
const results = {};
|
|
228
239
|
const screenshots = join(site, "screenshots");
|
|
229
240
|
mkdirSync(screenshots, { recursive: true });
|
|
@@ -240,6 +251,8 @@ export async function checkPages(site, targets, progress, expectations = {}) {
|
|
|
240
251
|
const state = await checkPage(browser, server.url, url, target, join(screenshots, `${target.slug}.png`), footnotes);
|
|
241
252
|
const found = failures(state);
|
|
242
253
|
results[target.slug] = found;
|
|
254
|
+
if (warnings)
|
|
255
|
+
warnings[target.slug] = state.warnings ?? [];
|
|
243
256
|
progress?.finish(index, target, found);
|
|
244
257
|
}
|
|
245
258
|
}
|
|
@@ -284,10 +297,21 @@ async function checkPage(browser, origin, url, target, screenshot, footnotes) {
|
|
|
284
297
|
await page.goto(url, { waitUntil: "networkidle" });
|
|
285
298
|
await page.evaluate(loadImages, 10_000);
|
|
286
299
|
await page.screenshot({ path: screenshot, fullPage: !target.card });
|
|
287
|
-
const state = await page.evaluate(pageState
|
|
300
|
+
const state = await page.evaluate(pageState, {
|
|
301
|
+
olderNetNewsWire: target.olderNetNewsWire,
|
|
302
|
+
macro: ENCLOSURES_MACRO,
|
|
303
|
+
});
|
|
304
|
+
const enclosures = (await page.evaluate(`(${ENCLOSURE_CHECK})()`));
|
|
288
305
|
// Last: it clicks footnotes open, so it must not disturb the screenshot.
|
|
289
306
|
const found = (await page.evaluate(`(${FOOTNOTE_CHECK})(${JSON.stringify(footnotes)})`));
|
|
290
|
-
return {
|
|
307
|
+
return {
|
|
308
|
+
...state,
|
|
309
|
+
enclosures,
|
|
310
|
+
footnotes: found.failures,
|
|
311
|
+
warnings: found.warnings,
|
|
312
|
+
blocked,
|
|
313
|
+
pageErrors,
|
|
314
|
+
};
|
|
291
315
|
}
|
|
292
316
|
finally {
|
|
293
317
|
await context.close();
|
package/dist/commands/check.js
CHANGED
|
@@ -6,14 +6,23 @@ import { interactive, openInBrowser, promptConfirm } from "../interactive.js";
|
|
|
6
6
|
import { version } from "../main.js";
|
|
7
7
|
import { buildArchive } from "../package.js";
|
|
8
8
|
import { findRoot, findTheme, fixturePath, footnoteExpectations, readFixture, repositoryFixtures, ThemeError, themeStem, } from "../project.js";
|
|
9
|
-
import { checkTargets, extraFixtures, renderSite, writeGallery, } from "../render.js";
|
|
9
|
+
import { allFixtures, checkTargets, extraFixtures, placesEnclosures, readDeliveries, renderSite, writeGallery, } from "../render.js";
|
|
10
10
|
import { staleStubs } from "../stubs.js";
|
|
11
11
|
import { printWarnings } from "./package.js";
|
|
12
12
|
import { CheckProgress } from "./progress.js";
|
|
13
|
+
/**
|
|
14
|
+
* Each fixture's [expect.footnotes]. A fixture that models a sync service, with
|
|
15
|
+
* base_url or deliveries, fails a footnote link that never became a marker unless it
|
|
16
|
+
* sets all_converted = false; any other fixture only warns about one, unless it sets
|
|
17
|
+
* all_converted = true.
|
|
18
|
+
*/
|
|
13
19
|
export function expectations(root, targets) {
|
|
14
20
|
const result = {};
|
|
15
21
|
for (const name of new Set(targets.map((target) => target.fixture))) {
|
|
16
|
-
|
|
22
|
+
const fixture = readFixture(fixturePath(root, name));
|
|
23
|
+
const expected = footnoteExpectations(fixture, `fixtures/${name}.toml`);
|
|
24
|
+
expected.all_converted ??= "base_url" in fixture || "deliveries" in fixture;
|
|
25
|
+
result[name] = expected;
|
|
17
26
|
}
|
|
18
27
|
return result;
|
|
19
28
|
}
|
|
@@ -22,7 +31,7 @@ export function fixtureHint(root) {
|
|
|
22
31
|
if (repositoryFixtures(root).length)
|
|
23
32
|
return undefined;
|
|
24
33
|
return ("Hint: this theme has no fixtures of its own. `npx nnw-theme@2 fixture list` " +
|
|
25
|
-
"shows ready-made ones modeled on WordPress, Ghost, Substack, and
|
|
34
|
+
"shows ready-made ones modeled on WordPress, Ghost, Substack, Medium, and Mastodon markup.");
|
|
26
35
|
}
|
|
27
36
|
async function offerToOpen(index, choice) {
|
|
28
37
|
const open = choice ?? (interactive() && (await promptConfirm("Open the preview?", true)));
|
|
@@ -34,7 +43,11 @@ export default async function check({ values }) {
|
|
|
34
43
|
const theme = findTheme(root);
|
|
35
44
|
console.log(`nnw-theme ${version()}`);
|
|
36
45
|
printWarnings(staleStubs(root));
|
|
37
|
-
const
|
|
46
|
+
const extras = extraFixtures(root);
|
|
47
|
+
const targets = checkTargets(extras, {
|
|
48
|
+
enclosuresMacro: placesEnclosures(theme),
|
|
49
|
+
deliveries: readDeliveries(root, allFixtures(extras)),
|
|
50
|
+
});
|
|
38
51
|
const expected = expectations(root, targets);
|
|
39
52
|
const progress = new CheckProgress(targets.length);
|
|
40
53
|
progress.status("Validating and packaging the theme…");
|
|
@@ -46,23 +59,29 @@ export default async function check({ values }) {
|
|
|
46
59
|
progress.status(`Rendering ${targets.length} pages…`);
|
|
47
60
|
const site = renderSite(root, theme, targets);
|
|
48
61
|
let results;
|
|
62
|
+
const warned = {};
|
|
49
63
|
try {
|
|
50
|
-
results = await checkPages(site, targets, progress, expected);
|
|
64
|
+
results = await checkPages(site, targets, progress, expected, warned);
|
|
51
65
|
}
|
|
52
66
|
finally {
|
|
53
67
|
progress.done();
|
|
54
68
|
}
|
|
55
69
|
writeGallery(site, themeStem(theme), targets, results);
|
|
56
70
|
const failures = targets.flatMap((target) => (results[target.slug] ?? []).map((message) => `${target.label}: ${message}`));
|
|
71
|
+
const warnings = targets.flatMap((target) => (warned[target.slug] ?? []).map((message) => `Warning: ${target.label}: ${message}`));
|
|
72
|
+
for (const warning of warnings)
|
|
73
|
+
console.log(warning);
|
|
74
|
+
const warningLines = warnings.map((warning) => `${warning}\n`).join("");
|
|
57
75
|
const passed = targets.filter((target) => !results[target.slug]?.length).length;
|
|
58
76
|
const packageName = basename(archive.path);
|
|
59
77
|
const footer = `nnw-theme ${version()}\n`;
|
|
60
78
|
const report = join(root, "build", "check-report.txt");
|
|
61
79
|
if (failures.length) {
|
|
62
|
-
writeFileSync(report, `FAIL\n${failures.join("\n")}\n${footer}`, "utf8");
|
|
80
|
+
writeFileSync(report, `FAIL\n${failures.join("\n")}\n${warningLines}${footer}`, "utf8");
|
|
63
81
|
}
|
|
64
82
|
else {
|
|
65
|
-
writeFileSync(report, `PASS\n${targets.length} WebKit renders checked\nPackage: ${packageName}\n
|
|
83
|
+
writeFileSync(report, `PASS\n${targets.length} WebKit renders checked\nPackage: ${packageName}\n` +
|
|
84
|
+
`${warningLines}${footer}`, "utf8");
|
|
66
85
|
console.log(`PASS: ${targets.length} WebKit renders and ${packageName}`);
|
|
67
86
|
}
|
|
68
87
|
const hint = fixtureHint(root);
|
package/dist/commands/preview.js
CHANGED
|
@@ -3,7 +3,7 @@ import { relative, sep } from "node:path";
|
|
|
3
3
|
import { serve } from "../browser.js";
|
|
4
4
|
import { openInBrowser } from "../interactive.js";
|
|
5
5
|
import { findRoot, findTheme } from "../project.js";
|
|
6
|
-
import {
|
|
6
|
+
import { renderSite, repositoryTargets } from "../render.js";
|
|
7
7
|
/** Whether a changed path is theme or fixture content the gallery is built from. */
|
|
8
8
|
export function watched(root, path) {
|
|
9
9
|
const parts = relative(root, path).split(sep);
|
|
@@ -15,7 +15,7 @@ export function watched(root, path) {
|
|
|
15
15
|
export default async function preview({ values }) {
|
|
16
16
|
const root = findRoot();
|
|
17
17
|
const theme = findTheme(root);
|
|
18
|
-
const build = () => renderSite(root, theme,
|
|
18
|
+
const build = () => renderSite(root, theme, repositoryTargets(root));
|
|
19
19
|
const site = build();
|
|
20
20
|
const server = await serve(site, { live: true });
|
|
21
21
|
console.log(`Preview: ${server.url}`);
|
package/dist/commands/render.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { findRoot, findTheme, ThemeError } from "../project.js";
|
|
2
|
-
import {
|
|
2
|
+
import { renderSite, repositoryTargets } from "../render.js";
|
|
3
3
|
export default function render({ positionals }) {
|
|
4
4
|
const root = findRoot();
|
|
5
5
|
const theme = findTheme(root);
|
|
6
|
-
let targets =
|
|
6
|
+
let targets = repositoryTargets(root);
|
|
7
7
|
if (positionals.length) {
|
|
8
8
|
const requested = new Set(positionals);
|
|
9
9
|
targets = targets.filter((target) => requested.has(target.fixture));
|
|
@@ -6,12 +6,13 @@ import { extraFixtures, FIXTURE_NAMES, RenderTarget, renderSite, } from "../rend
|
|
|
6
6
|
import { expectations } from "./check.js";
|
|
7
7
|
// Any fixture on any platform: a screenshot is a deliberate one-off, not the check matrix.
|
|
8
8
|
function selectTarget(root, values) {
|
|
9
|
-
const { fixture, platform, appearance, card } = values;
|
|
9
|
+
const { fixture, platform, appearance, card, delivery } = values;
|
|
10
10
|
if (![...Object.keys(FIXTURE_NAMES), ...extraFixtures(root)].includes(fixture)) {
|
|
11
11
|
throw new ThemeError(`unknown fixture: ${fixture}`);
|
|
12
12
|
}
|
|
13
13
|
return new RenderTarget(fixture, platform, appearance, {
|
|
14
14
|
card: !!card,
|
|
15
|
+
delivery: delivery,
|
|
15
16
|
});
|
|
16
17
|
}
|
|
17
18
|
export default async function screenshot({ values }) {
|
|
@@ -19,7 +20,11 @@ export default async function screenshot({ values }) {
|
|
|
19
20
|
const theme = findTheme(root);
|
|
20
21
|
const target = selectTarget(root, values);
|
|
21
22
|
const site = renderSite(root, theme, [target]);
|
|
22
|
-
const
|
|
23
|
+
const warned = {};
|
|
24
|
+
const expected = expectations(root, [target]);
|
|
25
|
+
const results = await checkPages(site, [target], undefined, expected, warned);
|
|
26
|
+
for (const warning of warned[target.slug] ?? [])
|
|
27
|
+
console.log(`Warning: ${warning}`);
|
|
23
28
|
const failures = results[target.slug] ?? [];
|
|
24
29
|
if (failures.length) {
|
|
25
30
|
throw new ThemeError(`screenshot checks failed:\n- ${failures.join("\n- ")}`);
|
package/dist/commands.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// The one description of the command line: the parser, help, and shell completion
|
|
2
2
|
// are all generated from it, so they cannot drift apart.
|
|
3
|
+
import { DELIVERIES } from "./delivery.js";
|
|
3
4
|
export const PROGRAM = "nnw-theme";
|
|
4
5
|
export const DESCRIPTION = "Create, preview, check, and publish a NetNewsWire theme.";
|
|
5
6
|
export const GUIDE_TOPICS = [
|
|
@@ -18,7 +19,10 @@ export const BUILT_IN_FIXTURE_NAMES = ["article", "kitchen-sink"];
|
|
|
18
19
|
*/
|
|
19
20
|
export const CATALOG_FIXTURES = {
|
|
20
21
|
ghost: "Ghost cards, galleries, and lazy images",
|
|
22
|
+
mastodon: "Mastodon post: mentions, hashtags, custom emoji, image enclosures",
|
|
21
23
|
medium: "Medium figures, unsized images, and footer",
|
|
24
|
+
"media-rss": "Image, video, and YouTube enclosures with captions",
|
|
25
|
+
podcast: "Podcast audio enclosure and episode artwork",
|
|
22
26
|
substack: "Substack images, widgets, and footnotes",
|
|
23
27
|
wordpress: "WordPress block and classic editor markup",
|
|
24
28
|
};
|
|
@@ -110,7 +114,7 @@ export const COMMANDS = [
|
|
|
110
114
|
name: "screenshot",
|
|
111
115
|
summary: "check one case in WebKit and save its image",
|
|
112
116
|
description: "Render and check a single case in WebKit and save its full-page screenshot: any " +
|
|
113
|
-
"fixture, on any platform. With --card, capture an 800x552 viewport at 2x, the " +
|
|
117
|
+
"fixture, on any platform, directly or --delivery through a sync service. With --card, capture an 800x552 viewport at 2x, the " +
|
|
114
118
|
"shape of the marketplace card. With --promote, it becomes " +
|
|
115
119
|
"screenshots/theme-preview.png, the marketplace card.",
|
|
116
120
|
options: [
|
|
@@ -123,6 +127,12 @@ export const COMMANDS = [
|
|
|
123
127
|
},
|
|
124
128
|
{ name: "platform", type: "string", choices: ["mac", "iphone", "ipad"], default: "mac" },
|
|
125
129
|
{ name: "appearance", type: "string", choices: ["light", "dark"], default: "light" },
|
|
130
|
+
{
|
|
131
|
+
name: "delivery",
|
|
132
|
+
type: "string",
|
|
133
|
+
choices: DELIVERIES,
|
|
134
|
+
help: "render the fixture as this sync service delivers it",
|
|
135
|
+
},
|
|
126
136
|
{ name: "card", type: "boolean", help: "capture the marketplace card's shape at 2x" },
|
|
127
137
|
{ name: "promote", type: "boolean", help: "use it as the marketplace screenshot" },
|
|
128
138
|
],
|
|
@@ -134,7 +144,7 @@ export const COMMANDS = [
|
|
|
134
144
|
},
|
|
135
145
|
{
|
|
136
146
|
name: "fixture",
|
|
137
|
-
summary: "add fixtures modeled on WordPress, Ghost, Substack, and
|
|
147
|
+
summary: "add fixtures modeled on WordPress, Ghost, Substack, Medium, and Mastodon",
|
|
138
148
|
description: "Copy ready-made fixtures with markup modeled on what major publishing platforms " +
|
|
139
149
|
"send into fixtures/, where preview and check pick them up. Each one adds four " +
|
|
140
150
|
"renders to every check.",
|