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/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
- constructor(fixture, platform, appearance, { largeText = false, themeScripts = true, olderNetNewsWire = false, card = false } = {}) {
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
- export function normalTargets(extras = []) {
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 Object.keys(FIXTURE_NAMES)) {
222
- for (const platform of PLATFORMS) {
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
- for (const fixture of extras) {
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
- const body = mapping.body ?? "";
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.preferred_link || fixture.feed_link || "",
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 (!known.has(target.scenario) && !scenarios.has(target.scenario)) {
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:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nnw-theme",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "description": "Create, preview, check, and publish a NetNewsWire theme.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Dave Marquard",