nnw-theme 2.3.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/footnotes.js +39 -6
- package/assets/guide/design-checklist.md +4 -0
- package/assets/guide/fixtures.md +35 -4
- package/assets/guide/skill.md +2 -2
- package/assets/guide/theme-format.md +4 -2
- package/assets/netnewswire/Mac/main_mac.js +5 -0
- package/assets/netnewswire/Shared/core.css +1 -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 +12 -2
- package/dist/commands/check.js +24 -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 +10 -2
- package/dist/delivery.js +516 -0
- package/dist/project.js +11 -2
- package/dist/render.js +109 -15
- package/lldb/nnwdump.py +13 -1
- package/package.json +1 -1
package/dist/render.js
CHANGED
|
@@ -3,6 +3,7 @@ import { mkdirSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync,
|
|
|
3
3
|
import { extname, isAbsolute, join, relative } from "node:path";
|
|
4
4
|
import { decodeHTML } from "entities";
|
|
5
5
|
import { Parser } from "htmlparser2";
|
|
6
|
+
import { DELIVERIES, DELIVERY_NAMES, deliver } from "./delivery.js";
|
|
6
7
|
import { enclosuresHTML, fixtureAttachments, fixtureImageURL, placeEnclosures, } from "./enclosures.js";
|
|
7
8
|
import { renderingInputs } from "./netnewswire.js";
|
|
8
9
|
import { fixturePath, readFixture, repositoryFixtures, ThemeError, themeStem, } from "./project.js";
|
|
@@ -156,7 +157,9 @@ export class RenderTarget {
|
|
|
156
157
|
olderNetNewsWire;
|
|
157
158
|
/** A 2x capture of the card viewport alone, for the marketplace card. */
|
|
158
159
|
card;
|
|
159
|
-
|
|
160
|
+
/** The sync service the article arrives through, rewritten as it delivers it. */
|
|
161
|
+
delivery;
|
|
162
|
+
constructor(fixture, platform, appearance, { largeText = false, themeScripts = true, olderNetNewsWire = false, card = false, delivery = undefined, } = {}) {
|
|
160
163
|
this.fixture = fixture;
|
|
161
164
|
this.platform = platform;
|
|
162
165
|
this.appearance = appearance;
|
|
@@ -165,6 +168,7 @@ export class RenderTarget {
|
|
|
165
168
|
this.themeScripts = themeScripts;
|
|
166
169
|
this.olderNetNewsWire = olderNetNewsWire;
|
|
167
170
|
this.card = card;
|
|
171
|
+
this.delivery = delivery;
|
|
168
172
|
}
|
|
169
173
|
get ios() {
|
|
170
174
|
return this.platform === "iphone" || this.platform === "ipad";
|
|
@@ -181,6 +185,8 @@ export class RenderTarget {
|
|
|
181
185
|
parts.push("Article JavaScript off");
|
|
182
186
|
if (this.olderNetNewsWire)
|
|
183
187
|
parts.push("older NetNewsWire");
|
|
188
|
+
if (this.delivery)
|
|
189
|
+
parts.push(`via ${DELIVERY_NAMES[this.delivery]}`);
|
|
184
190
|
if (this.card)
|
|
185
191
|
parts.push("card");
|
|
186
192
|
return parts.join(" · ");
|
|
@@ -192,6 +198,8 @@ export class RenderTarget {
|
|
|
192
198
|
return "article-javascript-off";
|
|
193
199
|
if (this.olderNetNewsWire)
|
|
194
200
|
return "older-netnewswire";
|
|
201
|
+
if (this.delivery)
|
|
202
|
+
return `${this.fixture} via ${DELIVERY_NAMES[this.delivery]}`;
|
|
195
203
|
return this.fixture;
|
|
196
204
|
}
|
|
197
205
|
get slug() {
|
|
@@ -202,6 +210,8 @@ export class RenderTarget {
|
|
|
202
210
|
parts.push("no-article-js");
|
|
203
211
|
if (this.olderNetNewsWire)
|
|
204
212
|
parts.push("older-netnewswire");
|
|
213
|
+
if (this.delivery)
|
|
214
|
+
parts.push(this.delivery);
|
|
205
215
|
if (this.card)
|
|
206
216
|
parts.push("card");
|
|
207
217
|
return parts.join("-");
|
|
@@ -216,32 +226,67 @@ export function extraFixtures(root) {
|
|
|
216
226
|
}
|
|
217
227
|
return extras;
|
|
218
228
|
}
|
|
219
|
-
|
|
229
|
+
/** A fixture's deliveries = [...] key, validated: the services to also render it through. */
|
|
230
|
+
export function fixtureDeliveries(fixture, name) {
|
|
231
|
+
const value = fixture.deliveries;
|
|
232
|
+
if (value === undefined)
|
|
233
|
+
return [];
|
|
234
|
+
if (!Array.isArray(value) ||
|
|
235
|
+
!value.every((item) => DELIVERIES.includes(item)) ||
|
|
236
|
+
new Set(value).size !== value.length) {
|
|
237
|
+
throw new ThemeError(`${name}: deliveries must list distinct services from ${DELIVERIES.join(", ")}`);
|
|
238
|
+
}
|
|
239
|
+
return value;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Each fixture's deliveries = [...], for those that list any. fixtures are the names
|
|
243
|
+
* to read: the template's own and the extras.
|
|
244
|
+
*/
|
|
245
|
+
export function readDeliveries(root, fixtures) {
|
|
246
|
+
const result = {};
|
|
247
|
+
for (const name of fixtures) {
|
|
248
|
+
const deliveries = fixtureDeliveries(readFixture(fixturePath(root, name)), `fixtures/${name}.toml`);
|
|
249
|
+
if (deliveries.length)
|
|
250
|
+
result[name] = deliveries;
|
|
251
|
+
}
|
|
252
|
+
return result;
|
|
253
|
+
}
|
|
254
|
+
/** The fixtures a repository renders: the template's own, then its extras. */
|
|
255
|
+
export function allFixtures(extras) {
|
|
256
|
+
return [...Object.keys(FIXTURE_NAMES), ...extras];
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Every fixture's matrix. A fixture that lists deliveries is also rendered through
|
|
260
|
+
* each service, on Mac in light and iPhone in dark, right after its own cases.
|
|
261
|
+
*/
|
|
262
|
+
export function normalTargets(extras = [], deliveries = {}) {
|
|
220
263
|
const targets = [];
|
|
221
|
-
for (const fixture of
|
|
222
|
-
|
|
264
|
+
for (const fixture of allFixtures(extras)) {
|
|
265
|
+
const platforms = fixture in FIXTURE_NAMES ? PLATFORMS : EXTRA_PLATFORMS;
|
|
266
|
+
for (const platform of platforms) {
|
|
223
267
|
for (const appearance of APPEARANCES) {
|
|
224
268
|
targets.push(new RenderTarget(fixture, platform, appearance));
|
|
225
269
|
}
|
|
226
270
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
for (const platform of EXTRA_PLATFORMS) {
|
|
230
|
-
for (const appearance of APPEARANCES) {
|
|
231
|
-
targets.push(new RenderTarget(fixture, platform, appearance));
|
|
232
|
-
}
|
|
271
|
+
for (const delivery of deliveries[fixture] ?? []) {
|
|
272
|
+
targets.push(new RenderTarget(fixture, "mac", "light", { delivery }), new RenderTarget(fixture, "iphone", "dark", { delivery }));
|
|
233
273
|
}
|
|
234
274
|
}
|
|
235
275
|
return targets;
|
|
236
276
|
}
|
|
277
|
+
/** A repository's normal matrix, including each fixture's deliveries. */
|
|
278
|
+
export function repositoryTargets(root) {
|
|
279
|
+
const extras = extraFixtures(root);
|
|
280
|
+
return normalTargets(extras, readDeliveries(root, allFixtures(extras)));
|
|
281
|
+
}
|
|
237
282
|
/**
|
|
238
283
|
* Everything check renders. A template that places [[enclosures]] itself also gets
|
|
239
284
|
* the older-NetNewsWire cases, since releases before enclosure support leave that
|
|
240
285
|
* macro as text in every article.
|
|
241
286
|
*/
|
|
242
|
-
export function checkTargets(extras = [], { enclosuresMacro = false } = {}) {
|
|
287
|
+
export function checkTargets(extras = [], { enclosuresMacro = false, deliveries = {} } = {}) {
|
|
243
288
|
const targets = [
|
|
244
|
-
...normalTargets(extras),
|
|
289
|
+
...normalTargets(extras, deliveries),
|
|
245
290
|
new RenderTarget("kitchen-sink", "mac", "light", { largeText: true }),
|
|
246
291
|
new RenderTarget("kitchen-sink", "iphone", "light", { largeText: true }),
|
|
247
292
|
new RenderTarget("article", "mac", "light", { themeScripts: false }),
|
|
@@ -411,7 +456,9 @@ function fixtureMapping(fixture, target, template, media) {
|
|
|
411
456
|
if (!mapping.dateline_style) {
|
|
412
457
|
mapping.dateline_style = mapping.title ? "articleDateline" : "articleDatelineTitle";
|
|
413
458
|
}
|
|
414
|
-
|
|
459
|
+
let body = mapping.body ?? "";
|
|
460
|
+
if (target.delivery)
|
|
461
|
+
body = deliver(body, target.delivery, mapping.preferred_link ?? "");
|
|
415
462
|
mapping.body = offlineMedia(body, media);
|
|
416
463
|
// NetNewsWire before enclosure support ignores attachments and leaves the macro.
|
|
417
464
|
if (!target.olderNetNewsWire) {
|
|
@@ -486,6 +533,42 @@ function isFile(path) {
|
|
|
486
533
|
return false;
|
|
487
534
|
}
|
|
488
535
|
}
|
|
536
|
+
/**
|
|
537
|
+
* Article.baseURL, made absolute as NetNewsWire does: no fragment, and nothing unless
|
|
538
|
+
* http or https.
|
|
539
|
+
*/
|
|
540
|
+
export function articleBaseURL(value) {
|
|
541
|
+
let url;
|
|
542
|
+
try {
|
|
543
|
+
url = new URL(value);
|
|
544
|
+
}
|
|
545
|
+
catch {
|
|
546
|
+
return "";
|
|
547
|
+
}
|
|
548
|
+
if (url.protocol !== "http:" && url.protocol !== "https:")
|
|
549
|
+
return "";
|
|
550
|
+
url.hash = "";
|
|
551
|
+
return url.href;
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* The page's base URL: the fixture's base_url when it gives one, else its link. An
|
|
555
|
+
* article synced through the Reader API (FreshRSS, Miniflux, Inoreader) has only an
|
|
556
|
+
* external link, so NetNewsWire falls back to the feed's home page: a delivered case
|
|
557
|
+
* uses feed_link, unless base_url gives the home page the service reports instead.
|
|
558
|
+
*/
|
|
559
|
+
function pageBaseURL(fixture, target) {
|
|
560
|
+
const name = `fixtures/${target.fixture}.toml`;
|
|
561
|
+
const baseURL = fixture.base_url;
|
|
562
|
+
if (baseURL !== undefined && typeof baseURL !== "string") {
|
|
563
|
+
throw new ThemeError(`${name}: base_url must be a string`);
|
|
564
|
+
}
|
|
565
|
+
if (baseURL !== undefined)
|
|
566
|
+
return articleBaseURL(baseURL);
|
|
567
|
+
if (target.delivery && typeof fixture.feed_link === "string" && fixture.feed_link) {
|
|
568
|
+
return articleBaseURL(fixture.feed_link);
|
|
569
|
+
}
|
|
570
|
+
return pyStr(fixture.preferred_link || fixture.feed_link || "");
|
|
571
|
+
}
|
|
489
572
|
export function renderPage(theme, fixture, target, snapshot = renderingInputs().path, media) {
|
|
490
573
|
const pagePath = join(snapshot, target.ios ? "iOS" : "Mac", "page.html");
|
|
491
574
|
const required = [
|
|
@@ -513,7 +596,7 @@ export function renderPage(theme, fixture, target, snapshot = renderingInputs().
|
|
|
513
596
|
title: fixture.title ?? "Theme preview",
|
|
514
597
|
style,
|
|
515
598
|
body,
|
|
516
|
-
baseURL: fixture
|
|
599
|
+
baseURL: pageBaseURL(fixture, target),
|
|
517
600
|
windowScrollY: "0",
|
|
518
601
|
});
|
|
519
602
|
page = injectScripts(page, snapshot, target.platform);
|
|
@@ -603,7 +686,18 @@ export function writeGallery(site, themeName, targets, results = null) {
|
|
|
603
686
|
const known = new Set(RESERVED_FIXTURE_NAMES);
|
|
604
687
|
const scenarios = new Map(SCENARIOS.map(([scenario, heading, text]) => [scenario, [heading, text]]));
|
|
605
688
|
for (const target of targets) {
|
|
606
|
-
if (
|
|
689
|
+
if (known.has(target.scenario) || scenarios.has(target.scenario))
|
|
690
|
+
continue;
|
|
691
|
+
if (target.delivery) {
|
|
692
|
+
const service = DELIVERY_NAMES[target.delivery];
|
|
693
|
+
scenarios.set(target.scenario, [
|
|
694
|
+
target.scenario,
|
|
695
|
+
`fixtures/${target.fixture}.toml as ${service} delivers it through the Reader ` +
|
|
696
|
+
`API: its body rewritten by ${service}'s sanitizer, and the feed's home page ` +
|
|
697
|
+
"as the page's base URL.",
|
|
698
|
+
]);
|
|
699
|
+
}
|
|
700
|
+
else {
|
|
607
701
|
scenarios.set(target.scenario, [target.fixture, `Your fixtures/${target.fixture}.toml.`]);
|
|
608
702
|
}
|
|
609
703
|
}
|
package/lldb/nnwdump.py
CHANGED
|
@@ -8,7 +8,9 @@ NetNewsWire with enclosure support renders an article's enclosures into that
|
|
|
8
8
|
dictionary: as `enclosures`, or appended to a wrapped `body` when the template has no
|
|
9
9
|
[[enclosures]] macro. nnwdump keeps neither. It records the article's attachments
|
|
10
10
|
and image instead, as [[attachments]] tables and image_url/thumbnail_url, so nnw-theme
|
|
11
|
-
renders them for whichever template previews the fixture.
|
|
11
|
+
renders them for whichever template previews the fixture. When the page's base URL
|
|
12
|
+
differs from the article's link, as for an article synced through the Reader API, it
|
|
13
|
+
also records base_url.
|
|
12
14
|
|
|
13
15
|
Values cross the debugger boundary base64-encoded, so nothing is truncated or
|
|
14
16
|
mis-escaped. HTML values are written as '''literal''' strings, so fixtures stay
|
|
@@ -103,6 +105,12 @@ def attachments_from_json(text: str) -> list[dict]:
|
|
|
103
105
|
return tables
|
|
104
106
|
|
|
105
107
|
|
|
108
|
+
def base_url_pairs(pairs: list[tuple[str, str]], base_url: str) -> list[tuple[str, str]]:
|
|
109
|
+
"""base_url, when the rendered page's base differs from preferred_link."""
|
|
110
|
+
preferred = dict(pairs).get("preferred_link", "")
|
|
111
|
+
return [] if base_url == preferred else [("base_url", base_url)]
|
|
112
|
+
|
|
113
|
+
|
|
106
114
|
def _toml_value_line(key: str, value) -> str:
|
|
107
115
|
if isinstance(value, bool) or not isinstance(value, int):
|
|
108
116
|
return toml_line(key, str(value))
|
|
@@ -194,6 +202,10 @@ def nnwdump(debugger, command, result, internal_dict):
|
|
|
194
202
|
value, _ = _evaluate(interpreter, f'{options.article}.{link} ?? ""')
|
|
195
203
|
if value:
|
|
196
204
|
pairs.append((key, value))
|
|
205
|
+
# The renderer's own base: the article's link, else the feed's home page.
|
|
206
|
+
base_url, _ = _evaluate(interpreter, 'self.baseURL?.absoluteString ?? ""')
|
|
207
|
+
if base_url is not None:
|
|
208
|
+
pairs.extend(base_url_pairs(pairs, base_url))
|
|
197
209
|
|
|
198
210
|
note = ""
|
|
199
211
|
if attachments:
|