nnw-theme 2.3.0 → 2.5.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/validate.js CHANGED
@@ -2,7 +2,7 @@ import { existsSync, lstatSync, readdirSync, readFileSync, statSync } from "node
2
2
  import { dirname, join } from "node:path";
3
3
  import { Parser } from "htmlparser2";
4
4
  import { PlistReal, parsePlist } from "./plist.js";
5
- import { PLACEHOLDER_MARKER, REQUIRED_THEME_FILES, readPlist, ThemeError, themeStem, } from "./project.js";
5
+ import { PLACEHOLDER_MARKER, REQUIRED_THEME_FILES, readPlist, TEMPLATE_7_3_FILE, ThemeError, themeStem, } from "./project.js";
6
6
  import { pyRepr } from "./pyformat.js";
7
7
  import { urlsplit } from "./urlparse.js";
8
8
  import { readZip } from "./zip.js";
@@ -67,6 +67,7 @@ function isRemote(value) {
67
67
  }
68
68
  function isAllowedFile(name) {
69
69
  return (REQUIRED_THEME_FILES.includes(name) ||
70
+ name === TEMPLATE_7_3_FILE ||
70
71
  OPTIONAL_THEME_FILE.test(name.toUpperCase()));
71
72
  }
72
73
  function isInt(value) {
@@ -148,38 +149,40 @@ export function validateSource(theme, { allowRemoteMedia = false } = {}) {
148
149
  const metadataReport = validateMetadata(readPlist(theme), stem);
149
150
  report.errors.push(...metadataReport.errors);
150
151
  report.warnings.push(...metadataReport.warnings);
151
- const templatePath = join(theme, "template.html");
152
- if (isRegularFile(templatePath)) {
153
- const template = readFileSync(templatePath, "utf8");
154
- if (!template.includes("[[")) {
155
- report.warnings.push("template.html contains no NetNewsWire macros");
156
- }
157
- const commented = commentedMacros(template);
158
- if (commented.length) {
159
- report.errors.push(`template.html has macros inside an HTML comment (${commented[0]}` +
160
- `${commented.length > 1 ? ` and ${commented.length - 1} more` : ""}). ` +
161
- "NetNewsWire substitutes them there too, so an article containing --> " +
162
- "ends the comment early and the rest shows as text; " +
163
- "write macro names without double brackets");
164
- }
165
- const references = resourceReferences(template);
166
- if (/<style\b[^>]*>[\s\S]*?@import\s/i.test(template)) {
167
- report.errors.push("CSS @import is not allowed in template.html");
168
- }
169
- for (const [tag, attribute, value] of references) {
170
- if (!value ||
171
- ["data:", "#", "mailto:", "tel:"].some((prefix) => value.startsWith(prefix))) {
172
- continue;
152
+ for (const templateName of ["template.html", TEMPLATE_7_3_FILE]) {
153
+ const templatePath = join(theme, templateName);
154
+ if (isRegularFile(templatePath)) {
155
+ const template = readFileSync(templatePath, "utf8");
156
+ if (!template.includes("[[")) {
157
+ report.warnings.push(`${templateName} contains no NetNewsWire macros`);
173
158
  }
174
- if (tag === "script" || tag === "stylesheet") {
175
- report.errors.push(`external ${tag} is not allowed: ${value}`);
159
+ const commented = commentedMacros(template);
160
+ if (commented.length) {
161
+ report.errors.push(`${templateName} has macros inside an HTML comment (${commented[0]}` +
162
+ `${commented.length > 1 ? ` and ${commented.length - 1} more` : ""}). ` +
163
+ "NetNewsWire substitutes them there too, so an article containing --> " +
164
+ "ends the comment early and the rest shows as text; " +
165
+ "write macro names without double brackets");
176
166
  }
177
- else if (isRemote(value)) {
178
- remoteMessage(report, `remote theme-owned ${tag} may not load in NetNewsWire: ${value}`, allowRemoteMedia);
167
+ const references = resourceReferences(template);
168
+ if (/<style\b[^>]*>[\s\S]*?@import\s/i.test(template)) {
169
+ report.errors.push(`CSS @import is not allowed in ${templateName}`);
179
170
  }
180
- else if (!value.includes("[[")) {
181
- report.errors.push("bundle-local resource references do not work in NetNewsWire: " +
182
- `${attribute}=${pyRepr(value)}`);
171
+ for (const [tag, attribute, value] of references) {
172
+ if (!value ||
173
+ ["data:", "#", "mailto:", "tel:"].some((prefix) => value.startsWith(prefix))) {
174
+ continue;
175
+ }
176
+ if (tag === "script" || tag === "stylesheet") {
177
+ report.errors.push(`external ${tag} is not allowed: ${value}`);
178
+ }
179
+ else if (isRemote(value)) {
180
+ remoteMessage(report, `remote theme-owned ${tag} may not load in NetNewsWire: ${value}`, allowRemoteMedia);
181
+ }
182
+ else if (!value.includes("[[")) {
183
+ report.errors.push("bundle-local resource references do not work in NetNewsWire: " +
184
+ `${attribute}=${pyRepr(value)}`);
185
+ }
183
186
  }
184
187
  }
185
188
  }
package/lldb/nnwdump.py CHANGED
@@ -6,9 +6,11 @@ writes it as a TOML fixture. `npx nnw-theme@2 capture` prints the setup steps.
6
6
 
7
7
  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
- [[enclosures]] macro. nnwdump keeps neither. It records the article's attachments
9
+ [[enclosures]] macro, and 7.3b6 and later add `primary_media`. nnwdump keeps none of them. 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
@@ -32,6 +34,7 @@ DEFAULT_OUTPUT = "fixtures/nnw-capture.toml"
32
34
  # Sorted last so the short metadata stays at the top of the file.
33
35
  _HTML_KEYS = {"body"}
34
36
  _ENCLOSURES_KEY = "enclosures"
37
+ _PRIMARY_MEDIA_KEY = "primary_media"
35
38
  # ArticleRenderer's body for a template without [[enclosures]]: <div>BODY</div>ENCLOSURES.
36
39
  _WRAPPED_BODY_TAIL = '</div><div class="nnw-enclosures">'
37
40
  # Attachment's Codable keys, in fixture order, with their fixture names.
@@ -86,6 +89,7 @@ def without_enclosures(pairs: list[tuple[str, str]]) -> list[tuple[str, str]]:
86
89
  if body.startswith("<div>") and at != -1:
87
90
  values["body"] = body[len("<div>") : at]
88
91
  values.pop(_ENCLOSURES_KEY, None)
92
+ values.pop(_PRIMARY_MEDIA_KEY, None)
89
93
  return list(values.items())
90
94
 
91
95
 
@@ -103,6 +107,12 @@ def attachments_from_json(text: str) -> list[dict]:
103
107
  return tables
104
108
 
105
109
 
110
+ def base_url_pairs(pairs: list[tuple[str, str]], base_url: str) -> list[tuple[str, str]]:
111
+ """base_url, when the rendered page's base differs from preferred_link."""
112
+ preferred = dict(pairs).get("preferred_link", "")
113
+ return [] if base_url == preferred else [("base_url", base_url)]
114
+
115
+
106
116
  def _toml_value_line(key: str, value) -> str:
107
117
  if isinstance(value, bool) or not isinstance(value, int):
108
118
  return toml_line(key, str(value))
@@ -194,6 +204,10 @@ def nnwdump(debugger, command, result, internal_dict):
194
204
  value, _ = _evaluate(interpreter, f'{options.article}.{link} ?? ""')
195
205
  if value:
196
206
  pairs.append((key, value))
207
+ # The renderer's own base: the article's link, else the feed's home page.
208
+ base_url, _ = _evaluate(interpreter, 'self.baseURL?.absoluteString ?? ""')
209
+ if base_url is not None:
210
+ pairs.extend(base_url_pairs(pairs, base_url))
197
211
 
198
212
  note = ""
199
213
  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.5.0",
4
4
  "description": "Create, preview, check, and publish a NetNewsWire theme.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Dave Marquard",