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/README.md +2 -2
- package/THIRD_PARTY_NOTICES.md +2 -2
- package/assets/catalog/mastodon.toml +50 -0
- package/assets/enclosures.js +6 -4
- package/assets/footnotes.js +39 -6
- package/assets/guide/design-checklist.md +6 -1
- package/assets/guide/fixtures.md +40 -7
- package/assets/guide/skill.md +2 -2
- package/assets/guide/theme-format.md +42 -7
- package/assets/netnewswire/Mac/main_mac.js +5 -0
- package/assets/netnewswire/Shared/core.css +20 -7
- package/assets/netnewswire/Shared/main.js +86 -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/body-images.js +209 -0
- package/dist/browser.js +29 -12
- package/dist/commands/check.js +25 -8
- 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/enclosures.js +176 -42
- package/dist/project.js +13 -2
- package/dist/render.js +145 -31
- package/dist/validate.js +33 -30
- package/lldb/nnwdump.py +16 -2
- package/package.json +1 -1
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
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
175
|
-
|
|
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
|
-
|
|
178
|
-
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
|
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:
|