nnw-theme 2.0.1 → 2.0.3
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/render.js +40 -5
- package/package.json +3 -3
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/render.js
CHANGED
|
@@ -2,11 +2,11 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { decodeHTML } from "entities";
|
|
5
|
+
import { Parser } from "htmlparser2";
|
|
5
6
|
import { renderingInputs } from "./netnewswire.js";
|
|
6
7
|
import { fixturePath, readFixture, repositoryFixtures, ThemeError, themeStem, } from "./project.js";
|
|
7
8
|
import { formatG, pyStr } from "./pyformat.js";
|
|
8
9
|
const MACRO_RE = /\[\[([a-zA-Z0-9_-]+)\]\]/g;
|
|
9
|
-
const SCRIPT_RE = /<script\b[^>]*>[\s\S]*?<\/script>/gi;
|
|
10
10
|
// A whole <img> or <source> tag; quoted attribute values may contain ">".
|
|
11
11
|
const MEDIA_TAG_RE = /<(img|source)\b((?:[^>"']|"[^"]*"|'[^']*')*)>/gi;
|
|
12
12
|
const ATTRIBUTE_RE = /([^\s"'=<>/]+)(?:\s*=\s*("[^"]*"|'[^']*'|[^\s"'=<>`]+))?/g;
|
|
@@ -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,41 @@ 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)}`;
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Remove every <script> element as WebKit's tokenizer finds them, including end
|
|
306
|
+
* tags like "</script >" and a script left open to the end of the document.
|
|
307
|
+
*/
|
|
308
|
+
export function removeScripts(html) {
|
|
309
|
+
const ranges = [];
|
|
310
|
+
let start = -1;
|
|
311
|
+
const parser = new Parser({
|
|
312
|
+
onopentag(name) {
|
|
313
|
+
if (name === "script")
|
|
314
|
+
start = parser.startIndex;
|
|
315
|
+
},
|
|
316
|
+
onclosetag(name, isImplied) {
|
|
317
|
+
if (name !== "script" || start < 0)
|
|
318
|
+
return;
|
|
319
|
+
const end = isImplied
|
|
320
|
+
? html.length
|
|
321
|
+
: html.indexOf(">", parser.endIndex) + 1 || html.length;
|
|
322
|
+
ranges.push([start, end]);
|
|
323
|
+
start = -1;
|
|
324
|
+
},
|
|
325
|
+
});
|
|
326
|
+
parser.end(html);
|
|
327
|
+
let result = "";
|
|
328
|
+
let offset = 0;
|
|
329
|
+
for (const [from, to] of ranges) {
|
|
330
|
+
result += html.slice(offset, from);
|
|
331
|
+
offset = to;
|
|
332
|
+
}
|
|
333
|
+
return result + html.slice(offset);
|
|
299
334
|
}
|
|
300
335
|
function isFile(path) {
|
|
301
336
|
try {
|
|
@@ -315,7 +350,7 @@ export function renderPage(theme, fixture, target, snapshot = renderingInputs().
|
|
|
315
350
|
if (!required.every(isFile)) {
|
|
316
351
|
throw new ThemeError("bundled NetNewsWire rendering inputs are incomplete");
|
|
317
352
|
}
|
|
318
|
-
const skeleton = read(pagePath)
|
|
353
|
+
const skeleton = removeScripts(read(pagePath));
|
|
319
354
|
const stylesheet = read(join(theme, "stylesheet.css"));
|
|
320
355
|
// styleSubstitutions() is empty on macOS, so the macro must stay literal there.
|
|
321
356
|
const styleSubstitutions = {};
|
|
@@ -327,7 +362,7 @@ export function renderPage(theme, fixture, target, snapshot = renderingInputs().
|
|
|
327
362
|
const template = read(join(theme, "template.html"));
|
|
328
363
|
let body = substitute(template, fixtureMapping(fixture, target));
|
|
329
364
|
if (!target.themeScripts)
|
|
330
|
-
body = body
|
|
365
|
+
body = removeScripts(body);
|
|
331
366
|
let page = substitute(skeleton, {
|
|
332
367
|
title: fixture.title ?? "Theme preview",
|
|
333
368
|
style,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nnw-theme",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.3",
|
|
4
4
|
"description": "Create, preview, check, and publish a NetNewsWire theme.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Dave Marquard",
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
"entities": "8.1.0",
|
|
45
45
|
"fflate": "0.8.3",
|
|
46
46
|
"htmlparser2": "12.0.0",
|
|
47
|
-
"playwright-core": "1.64.0
|
|
48
|
-
"smol-toml": "1.
|
|
47
|
+
"playwright-core": "1.64.0",
|
|
48
|
+
"smol-toml": "1.9.1"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@biomejs/biome": "2.5.14",
|