nnw-theme 2.0.0 → 2.0.2
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/THIRD_PARTY_NOTICES.md +2 -2
- package/assets/fixtures/kitchen-sink.toml +4 -0
- package/assets/guide/theme-format.md +2 -2
- package/assets/netnewswire/Shared/core.css +4 -0
- package/assets/netnewswire/Shared/main.js +21 -4
- package/assets/netnewswire/Shared/newsfoot.js +35 -6
- package/assets/netnewswire/netnewswire.json +5 -5
- package/dist/browser.js +22 -0
- package/dist/render.js +6 -2
- package/package.json +1 -1
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Third-party notices
|
|
2
2
|
|
|
3
3
|
The published package includes rendering files from
|
|
4
|
-
[NetNewsWire](https://github.com/Ranchero-Software/NetNewsWire), release `mac-7.
|
|
5
|
-
commit `
|
|
4
|
+
[NetNewsWire](https://github.com/Ranchero-Software/NetNewsWire), release `mac-7.2b4`,
|
|
5
|
+
commit `b7f4d8c7d273e4247c5d033ee47014805072297b`, under `assets/netnewswire/`.
|
|
6
6
|
`netnewswire.json` in the source repository records the SHA-256 of every file, and the
|
|
7
7
|
build verifies each one, including NetNewsWire’s MIT license, which ships beside them as
|
|
8
8
|
`assets/netnewswire/LICENSE`. These files are used only to preview themes; they are
|
|
@@ -29,6 +29,10 @@ body = """
|
|
|
29
29
|
<table><thead><tr><th>Viewport</th><th>Purpose</th><th>Expected behavior</th></tr></thead><tbody><tr><td>macOS</td><td>Reading</td><td>Comfortable measure</td></tr><tr><td>iPhone</td><td>Small screen</td><td>No document overflow</td></tr><tr><td>iPad</td><td>Large text</td><td>Stable hierarchy</td></tr></tbody></table>
|
|
30
30
|
<h2>Media and footnotes</h2>
|
|
31
31
|
<figure><img alt="Abstract green and cream test pattern" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1200' height='675' viewBox='0 0 1200 675'%3E%3Crect width='1200' height='675' fill='%23d8d2c6'/%3E%3Ccircle cx='380' cy='337' r='240' fill='%232f6655'/%3E%3Cpath d='M0 560L1200 180V675H0Z' fill='%23f7f5f0'/%3E%3C/svg%3E"><figcaption>A synthetic image checks wide-media scaling.</figcaption></figure>
|
|
32
|
+
<p>Embedded players come in the shapes NetNewsWire’s <code>main.js</code> handles: a fixed-size frame that keeps its proportions as the column narrows, a full-width player whose height comes only from an inline <code>aspect-ratio</code>, and a hidden tracking frame that must take no space.</p>
|
|
33
|
+
<iframe width="560" height="315" title="Fixed-size video embed" srcdoc="<body style='margin:0;display:grid;place-items:center;height:100vh;background:#8a8f98;color:#fff;font:600 16px system-ui'>560 × 315 video embed"></iframe>
|
|
34
|
+
<iframe width="560" height="315" style="width: 100%; height: auto; aspect-ratio: 16 / 9;" title="Aspect-ratio video embed" srcdoc="<body style='margin:0;display:grid;place-items:center;height:100vh;background:#8a8f98;color:#fff;font:600 16px system-ui'>16:9 aspect-ratio video embed"></iframe>
|
|
35
|
+
<iframe style="display: none;" title="Hidden tracking frame" srcdoc=""></iframe>
|
|
32
36
|
<p>The note marker uses common generated markup.<sup id="fnref:1"><a href="#fn:1" rel="footnote">1</a></sup></p>
|
|
33
37
|
<div class="footnotes"><ol><li id="fn:1"><p>Footnote content should remain readable in place and in NetNewsWire’s popover. <a href="#fnref:1" class="reversefootnote">↩</a></p></li></ol></div>
|
|
34
38
|
<!-- SC_OFF --><div class="md"><p>A comment-wrapped paragraph, as Reddit sends every post.</p></div><!-- SC_ON -->
|
|
@@ -39,8 +39,8 @@ comment early and shows the rest of it as text. `check` fails on them. NetNewsWi
|
|
|
39
39
|
`:root`; a theme with its own typography should not, because that shorthand resets
|
|
40
40
|
`font-family` and `line-height`.
|
|
41
41
|
|
|
42
|
-
The renderer pins NetNewsWire `mac-7.
|
|
43
|
-
`
|
|
42
|
+
The renderer pins NetNewsWire `mac-7.2b4` commit
|
|
43
|
+
`b7f4d8c7d273e4247c5d033ee47014805072297b`. It uses upstream `core.css`, macOS and
|
|
44
44
|
iOS page skeletons, and `main.js`, `main_mac.js`, `main_ios.js`, and `newsfoot.js`.
|
|
45
45
|
These files ship inside the `nnw-theme` package, verified by SHA-256, so previews work
|
|
46
46
|
offline. iPhone and iPad share the iOS skeleton. WebKit native
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// Here we are making iframes responsive. Particularly useful for inline Youtube videos.
|
|
2
2
|
function wrapFrames() {
|
|
3
3
|
document.querySelectorAll("iframe").forEach(element => {
|
|
4
|
+
if (getComputedStyle(element).display === "none") {
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
4
7
|
if (parseInt(element.height) > 0) {
|
|
5
8
|
preserveAspectRatioOfFixedSizeFrame(element);
|
|
6
9
|
return;
|
|
@@ -39,7 +42,12 @@ function stripStylesFromElement(element, propertiesToStrip) {
|
|
|
39
42
|
function stripStyles() {
|
|
40
43
|
document.getElementsByTagName("body")[0].querySelectorAll("style, link[rel=stylesheet]").forEach(element => element.remove());
|
|
41
44
|
// Removing "background" and "font" will also remove properties that would be reflected in them, e.g., "background-color" and "font-family"
|
|
42
|
-
|
|
45
|
+
// The stylesheet forces images to height: auto, so an author aspect-ratio box can't be filled by its image — content after the box overlaps the overflowing image.
|
|
46
|
+
// Iframes keep their aspect-ratio: without it an iframe with no pixel height collapses to the default 150px.
|
|
47
|
+
document.getElementsByTagName("body")[0].querySelectorAll("[style]").forEach(element => {
|
|
48
|
+
const isIframe = element.tagName === "IFRAME";
|
|
49
|
+
stripStylesFromElement(element, isIframe ? ["color", "background", "font", "max-width", "max-height", "position"] : ["color", "background", "font", "max-width", "max-height", "position", "aspect-ratio"]);
|
|
50
|
+
});
|
|
43
51
|
}
|
|
44
52
|
|
|
45
53
|
// Constrain the height of iframes whose heights are defined relative to the document body to be at most
|
|
@@ -167,11 +175,13 @@ function error() {
|
|
|
167
175
|
|
|
168
176
|
// Takes into account absoluting of URLs.
|
|
169
177
|
function isLocalFootnote(target) {
|
|
170
|
-
return target.hash.startsWith("#fn") && target.href.indexOf(document.baseURI) === 0;
|
|
178
|
+
return (target.hash.startsWith("#fn") || target.hash.startsWith("#footnote")) && target.href.indexOf(document.baseURI) === 0;
|
|
171
179
|
}
|
|
172
180
|
|
|
173
181
|
function styleLocalFootnotes() {
|
|
174
|
-
|
|
182
|
+
// Multimarkdown-style: <sup><a href="#fn…">. Substack-style: a bare
|
|
183
|
+
// <a id="footnote-anchor-N" href="#footnote-N"> with no <sup> wrapper.
|
|
184
|
+
for (elem of document.querySelectorAll("sup > a[href*='#fn'], sup > div > a[href*='#fn'], a[id^='footnote-anchor-']")) {
|
|
175
185
|
if (isLocalFootnote(elem)) {
|
|
176
186
|
elem.classList.add("footnote");
|
|
177
187
|
}
|
|
@@ -202,17 +212,24 @@ function removeWpSmiley() {
|
|
|
202
212
|
}
|
|
203
213
|
}
|
|
204
214
|
|
|
215
|
+
// <https://github.com/Ranchero-Software/NetNewsWire/issues/3501>
|
|
216
|
+
function removeInstapaperIgnoreElements() {
|
|
217
|
+
document.querySelectorAll(".instapaper_ignore").forEach(element => element.remove());
|
|
218
|
+
}
|
|
219
|
+
|
|
205
220
|
function processPage() {
|
|
221
|
+
// stripStyles must run first — wrapFrames sets aspect-ratio on fixed-size iframes, which stripStyles would remove.
|
|
222
|
+
stripStyles();
|
|
206
223
|
wrapFrames();
|
|
207
224
|
wrapTables();
|
|
208
225
|
inlineVideos();
|
|
209
|
-
stripStyles();
|
|
210
226
|
sizeTailwindSVGs();
|
|
211
227
|
constrainBodyRelativeIframes();
|
|
212
228
|
convertImgSrc();
|
|
213
229
|
flattenPreElements();
|
|
214
230
|
styleLocalFootnotes();
|
|
215
231
|
removeWpSmiley()
|
|
232
|
+
removeInstapaperIgnoreElements();
|
|
216
233
|
postRenderProcessing();
|
|
217
234
|
}
|
|
218
235
|
|
|
@@ -125,12 +125,37 @@
|
|
|
125
125
|
if (!target.hash) return;
|
|
126
126
|
return decodeURIComponent(target.hash.substring(1));
|
|
127
127
|
}
|
|
128
|
-
/**
|
|
128
|
+
/**
|
|
129
|
+
* @type {{
|
|
130
|
+
* fnref(target:HTMLAnchorElement): string|undefined,
|
|
131
|
+
* content(targetElement:HTMLElement): string
|
|
132
|
+
* }[]}
|
|
133
|
+
*/
|
|
129
134
|
const footnoteFormats = [
|
|
135
|
+
{ // Substack: the ref <a id="footnote-anchor-N" href="#footnote-N">
|
|
136
|
+
// points at a backlink <a id="footnote-N">N</a> whose note text lives
|
|
137
|
+
// in the sibling elements that follow it, not inside the anchor.
|
|
138
|
+
fnref(target) {
|
|
139
|
+
if (!target.matches("a[id^='footnote-anchor-']")) return;
|
|
140
|
+
return idFromHash(target);
|
|
141
|
+
},
|
|
142
|
+
content(targetElement) {
|
|
143
|
+
let html = "";
|
|
144
|
+
let node = targetElement.nextElementSibling;
|
|
145
|
+
while (node && !node.matches("a[id^='footnote-']")) {
|
|
146
|
+
html += node.outerHTML;
|
|
147
|
+
node = node.nextElementSibling;
|
|
148
|
+
}
|
|
149
|
+
return html;
|
|
150
|
+
}
|
|
151
|
+
},
|
|
130
152
|
{ // Multimarkdown
|
|
131
153
|
fnref(target) {
|
|
132
154
|
if (!target.matches(".footnote")) return;
|
|
133
155
|
return idFromHash(target);
|
|
156
|
+
},
|
|
157
|
+
content(targetElement) {
|
|
158
|
+
return targetElement.innerHTML;
|
|
134
159
|
}
|
|
135
160
|
}
|
|
136
161
|
];
|
|
@@ -140,22 +165,26 @@
|
|
|
140
165
|
if (!(ev.target && ev.target instanceof HTMLAnchorElement)) return;
|
|
141
166
|
|
|
142
167
|
let targetId = undefined;
|
|
168
|
+
let format = undefined;
|
|
143
169
|
for(const f of footnoteFormats) {
|
|
144
170
|
targetId = f.fnref(ev.target);
|
|
145
|
-
if (targetId)
|
|
171
|
+
if (targetId) {
|
|
172
|
+
format = f;
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
146
175
|
}
|
|
147
176
|
if (targetId === undefined) return;
|
|
148
|
-
|
|
177
|
+
|
|
149
178
|
// Only override the default behaviour when we know we can find the
|
|
150
179
|
// target element
|
|
151
180
|
const targetElement = document.getElementById(targetId);
|
|
152
181
|
if (targetElement === null) return;
|
|
153
|
-
|
|
182
|
+
|
|
154
183
|
ev.preventDefault();
|
|
155
184
|
|
|
156
185
|
installContainer(ev.target);
|
|
157
|
-
|
|
158
|
-
void new Footnote(targetElement
|
|
186
|
+
|
|
187
|
+
void new Footnote(format.content(targetElement), ev.target);
|
|
159
188
|
});
|
|
160
189
|
|
|
161
190
|
// Handle clicks on the footnote reverse link
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"release": "mac-7.
|
|
3
|
-
"commit": "
|
|
2
|
+
"release": "mac-7.2b4",
|
|
3
|
+
"commit": "b7f4d8c7d273e4247c5d033ee47014805072297b",
|
|
4
4
|
"files": [
|
|
5
5
|
{
|
|
6
6
|
"destination": "LICENSE",
|
|
@@ -20,17 +20,17 @@
|
|
|
20
20
|
{
|
|
21
21
|
"destination": "Shared/core.css",
|
|
22
22
|
"source": "Shared/Article Rendering/core.css",
|
|
23
|
-
"sha256": "
|
|
23
|
+
"sha256": "d67552259cb45261e1aacf0c1af6bc39b560b084c7b9e3943191907e0977cd4d"
|
|
24
24
|
},
|
|
25
25
|
{
|
|
26
26
|
"destination": "Shared/main.js",
|
|
27
27
|
"source": "Shared/Article Rendering/main.js",
|
|
28
|
-
"sha256": "
|
|
28
|
+
"sha256": "847dc1f06ec0e7f96aee1004fd6186771db907445347cf0371abc67158aadb5a"
|
|
29
29
|
},
|
|
30
30
|
{
|
|
31
31
|
"destination": "Shared/newsfoot.js",
|
|
32
32
|
"source": "Shared/Article Rendering/newsfoot.js",
|
|
33
|
-
"sha256": "
|
|
33
|
+
"sha256": "828e6889bd976ebce417d808ac726dd9252a801946d77e32de207ba4a14e21fa"
|
|
34
34
|
},
|
|
35
35
|
{
|
|
36
36
|
"destination": "iOS/main_ios.js",
|
package/dist/browser.js
CHANGED
|
@@ -154,6 +154,8 @@ export function failures(state) {
|
|
|
154
154
|
found.push(`broken images: ${pyReprList(state.brokenImages)}`);
|
|
155
155
|
if (state.collapsedImages.length)
|
|
156
156
|
found.push(`images drawn at zero size: ${pyReprList(state.collapsedImages)}`);
|
|
157
|
+
if (state.unloadedImages.length)
|
|
158
|
+
found.push(`images still loading: ${pyReprList(state.unloadedImages)}`);
|
|
157
159
|
found.push(...(state.footnotes ?? []));
|
|
158
160
|
if (state.blocked.length)
|
|
159
161
|
found.push(`external requests: ${pyReprList(state.blocked)}`);
|
|
@@ -161,6 +163,21 @@ export function failures(state) {
|
|
|
161
163
|
found.push(`page errors: ${pyReprList(state.pageErrors)}`);
|
|
162
164
|
return found;
|
|
163
165
|
}
|
|
166
|
+
// Runs in the page before its screenshot. A loading="lazy" image outside the viewport
|
|
167
|
+
// never starts loading in a full-page screenshot, so it would draw as blank space and
|
|
168
|
+
// escape the image checks; NetNewsWire loads it as the reader scrolls, so load every
|
|
169
|
+
// image now and wait (up to timeout ms) for each to finish or fail.
|
|
170
|
+
async function loadImages(timeout) {
|
|
171
|
+
const images = [...document.images];
|
|
172
|
+
for (const image of images)
|
|
173
|
+
if (image.loading === "lazy")
|
|
174
|
+
image.loading = "eager";
|
|
175
|
+
const settled = images.map((image) => image.decode().catch(() => { }));
|
|
176
|
+
await Promise.race([
|
|
177
|
+
Promise.all(settled),
|
|
178
|
+
new Promise((resolve) => setTimeout(resolve, timeout)),
|
|
179
|
+
]);
|
|
180
|
+
}
|
|
164
181
|
// Runs in the page after its screenshot.
|
|
165
182
|
function pageState() {
|
|
166
183
|
const article = document.querySelector(".articleBody");
|
|
@@ -194,6 +211,10 @@ function pageState() {
|
|
|
194
211
|
return box.width < 1 || box.height < 1;
|
|
195
212
|
})
|
|
196
213
|
.map((image) => image.currentSrc || image.src),
|
|
214
|
+
// Still loading after loadImages: neither drawn nor covered by the checks above.
|
|
215
|
+
unloadedImages: [...document.images]
|
|
216
|
+
.filter((image) => !image.complete)
|
|
217
|
+
.map((image) => image.currentSrc || image.src),
|
|
197
218
|
};
|
|
198
219
|
}
|
|
199
220
|
/**
|
|
@@ -260,6 +281,7 @@ async function checkPage(browser, origin, url, target, screenshot, footnotes) {
|
|
|
260
281
|
const page = await context.newPage();
|
|
261
282
|
page.on("pageerror", (error) => pageErrors.push(String(error)));
|
|
262
283
|
await page.goto(url, { waitUntil: "networkidle" });
|
|
284
|
+
await page.evaluate(loadImages, 10_000);
|
|
263
285
|
await page.screenshot({ path: screenshot, fullPage: true });
|
|
264
286
|
const state = await page.evaluate(pageState);
|
|
265
287
|
// Last: it clicks footnotes open, so it must not disturb the screenshot.
|
package/dist/render.js
CHANGED
|
@@ -81,7 +81,8 @@ const SCENARIOS = [
|
|
|
81
81
|
"Stress test",
|
|
82
82
|
"The kitchen-sink fixture: a very long headline, byline, and feed name; inline " +
|
|
83
83
|
"formatting; nested lists; an unbroken identifier that exposes horizontal " +
|
|
84
|
-
"overflow; a quotation, code block, table, figure, footnote, and HTML comments
|
|
84
|
+
"overflow; a quotation, code block, table, figure, video embeds, footnote, and HTML comments " +
|
|
85
|
+
"in the body.",
|
|
85
86
|
],
|
|
86
87
|
[
|
|
87
88
|
"large-text",
|
|
@@ -295,7 +296,10 @@ function injectScripts(page, inputs, platform) {
|
|
|
295
296
|
scripts.push(IOS_LABEL_SHIM);
|
|
296
297
|
scripts.push(read(join(inputs, "Shared", "main.js")), read(ios ? join(inputs, "iOS", "main_ios.js") : join(inputs, "Mac", "main_mac.js")), read(join(inputs, "Shared", "newsfoot.js")));
|
|
297
298
|
const bundle = scripts.map((script) => `<script>\n${script}\n</script>`).join("\n");
|
|
298
|
-
|
|
299
|
+
// The theme and fixture are already substituted into the page, so an earlier
|
|
300
|
+
// </body> can be article text; the skeleton's own closing tag is the last one.
|
|
301
|
+
const end = page.lastIndexOf("</body>");
|
|
302
|
+
return `${page.slice(0, end)}${bundle}\n${page.slice(end)}`;
|
|
299
303
|
}
|
|
300
304
|
function isFile(path) {
|
|
301
305
|
try {
|