nnw-theme 2.1.0 → 2.3.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/media-rss.toml +79 -0
- package/assets/catalog/podcast.toml +52 -0
- package/assets/enclosures.js +113 -0
- package/assets/guide/design-checklist.md +2 -0
- package/assets/guide/fixtures.md +37 -2
- package/assets/guide/publishing.md +6 -0
- package/assets/guide/skill.md +2 -1
- package/assets/guide/theme-format.md +46 -2
- package/assets/netnewswire/Shared/core.css +24 -0
- package/assets/netnewswire/netnewswire.json +3 -3
- package/assets/stubs/.github/workflows/screenshot.yml +7 -3
- package/dist/browser.js +21 -6
- package/dist/commands/check.js +4 -2
- package/dist/commands/screenshot.js +7 -8
- package/dist/commands.js +7 -2
- package/dist/enclosures.js +299 -0
- package/dist/render.js +160 -14
- package/lldb/nnwdump.py +75 -5
- package/package.json +1 -1
package/lldb/nnwdump.py
CHANGED
|
@@ -4,6 +4,12 @@ Stopped at the final `return d` of ArticleRenderer.articleSubstitutions(), this
|
|
|
4
4
|
reads the substitution dictionary NetNewsWire built for the selected article and
|
|
5
5
|
writes it as a TOML fixture. `npx nnw-theme@2 capture` prints the setup steps.
|
|
6
6
|
|
|
7
|
+
NetNewsWire with enclosure support renders an article's enclosures into that
|
|
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
|
|
10
|
+
and image instead, as [[attachments]] tables and image_url/thumbnail_url, so nnw-theme
|
|
11
|
+
renders them for whichever template previews the fixture.
|
|
12
|
+
|
|
7
13
|
Values cross the debugger boundary base64-encoded, so nothing is truncated or
|
|
8
14
|
mis-escaped. HTML values are written as '''literal''' strings, so fixtures stay
|
|
9
15
|
readable and diff cleanly.
|
|
@@ -16,6 +22,7 @@ helpers can be tested without a debugger.
|
|
|
16
22
|
from __future__ import annotations
|
|
17
23
|
|
|
18
24
|
import base64
|
|
25
|
+
import json
|
|
19
26
|
import optparse
|
|
20
27
|
import shlex
|
|
21
28
|
from pathlib import Path
|
|
@@ -24,6 +31,20 @@ USAGE = "nnwdump [--var NAME] [--article NAME] [--no-icon] [OUTPUT.toml]"
|
|
|
24
31
|
DEFAULT_OUTPUT = "fixtures/nnw-capture.toml"
|
|
25
32
|
# Sorted last so the short metadata stays at the top of the file.
|
|
26
33
|
_HTML_KEYS = {"body"}
|
|
34
|
+
_ENCLOSURES_KEY = "enclosures"
|
|
35
|
+
# ArticleRenderer's body for a template without [[enclosures]]: <div>BODY</div>ENCLOSURES.
|
|
36
|
+
_WRAPPED_BODY_TAIL = '</div><div class="nnw-enclosures">'
|
|
37
|
+
# Attachment's Codable keys, in fixture order, with their fixture names.
|
|
38
|
+
_ATTACHMENT_KEYS = (
|
|
39
|
+
("url", "url"),
|
|
40
|
+
("mimeType", "mime_type"),
|
|
41
|
+
("title", "title"),
|
|
42
|
+
("sizeInBytes", "size_in_bytes"),
|
|
43
|
+
("durationInSeconds", "duration_in_seconds"),
|
|
44
|
+
("width", "width"),
|
|
45
|
+
("height", "height"),
|
|
46
|
+
("medium", "medium"),
|
|
47
|
+
)
|
|
27
48
|
|
|
28
49
|
|
|
29
50
|
def resolve_output(argument: str) -> Path:
|
|
@@ -56,13 +77,49 @@ def toml_line(key: str, value: str) -> str:
|
|
|
56
77
|
return f'{key} = "{_toml_basic(value)}"'
|
|
57
78
|
|
|
58
79
|
|
|
59
|
-
def
|
|
80
|
+
def without_enclosures(pairs: list[tuple[str, str]]) -> list[tuple[str, str]]:
|
|
81
|
+
"""The substitutions as they were before NetNewsWire rendered the enclosures."""
|
|
82
|
+
values = dict(pairs)
|
|
83
|
+
if _ENCLOSURES_KEY not in values:
|
|
84
|
+
body = values.get("body", "")
|
|
85
|
+
at = body.rfind(_WRAPPED_BODY_TAIL)
|
|
86
|
+
if body.startswith("<div>") and at != -1:
|
|
87
|
+
values["body"] = body[len("<div>") : at]
|
|
88
|
+
values.pop(_ENCLOSURES_KEY, None)
|
|
89
|
+
return list(values.items())
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def attachments_from_json(text: str) -> list[dict]:
|
|
93
|
+
"""Attachment JSON, as [Attachment].json() writes it, as fixture tables."""
|
|
94
|
+
tables = []
|
|
95
|
+
for attachment in json.loads(text or "[]") or []:
|
|
96
|
+
table = {
|
|
97
|
+
name: attachment[key]
|
|
98
|
+
for key, name in _ATTACHMENT_KEYS
|
|
99
|
+
if attachment.get(key) is not None
|
|
100
|
+
}
|
|
101
|
+
if table.get("url"):
|
|
102
|
+
tables.append(table)
|
|
103
|
+
return tables
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _toml_value_line(key: str, value) -> str:
|
|
107
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
108
|
+
return toml_line(key, str(value))
|
|
109
|
+
return f"{key} = {value}"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def fixture_text(pairs: list[tuple[str, str]], name: str, attachments: list[dict] = ()) -> str:
|
|
60
113
|
ordered = sorted(pairs, key=lambda pair: (pair[0] in _HTML_KEYS, pair[0]))
|
|
61
114
|
header = (
|
|
62
115
|
"# NetNewsWire article captured with nnwdump.\n"
|
|
63
116
|
f"# Preview with: npx nnw-theme@2 render {name}\n\n"
|
|
64
117
|
)
|
|
65
|
-
|
|
118
|
+
text = header + "\n".join(toml_line(key, value) for key, value in ordered) + "\n"
|
|
119
|
+
for table in attachments:
|
|
120
|
+
lines = [_toml_value_line(key, value) for key, value in table.items()]
|
|
121
|
+
text += "\n[[attachments]]\n" + "\n".join(lines) + "\n"
|
|
122
|
+
return text
|
|
66
123
|
|
|
67
124
|
|
|
68
125
|
def _evaluate(interpreter, expression: str):
|
|
@@ -127,19 +184,32 @@ def nnwdump(debugger, command, result, internal_dict):
|
|
|
127
184
|
result.SetError(f"`{options.var}` is empty; is this the substitution dictionary?")
|
|
128
185
|
return
|
|
129
186
|
|
|
187
|
+
# Older NetNewsWire has no attachments to read; capture the article without them.
|
|
188
|
+
pairs = without_enclosures(pairs)
|
|
189
|
+
attachments = []
|
|
190
|
+
raw, _ = _evaluate(interpreter, f'({options.article}.attachments ?? []).json() ?? "[]"')
|
|
191
|
+
if raw:
|
|
192
|
+
attachments = attachments_from_json(raw)
|
|
193
|
+
for key, link in (("image_url", "imageLink"), ("thumbnail_url", "thumbnailLink")):
|
|
194
|
+
value, _ = _evaluate(interpreter, f'{options.article}.{link} ?? ""')
|
|
195
|
+
if value:
|
|
196
|
+
pairs.append((key, value))
|
|
197
|
+
|
|
130
198
|
note = ""
|
|
199
|
+
if attachments:
|
|
200
|
+
note = f" and {len(attachments)} attachment{'s' if len(attachments) > 1 else ''}"
|
|
131
201
|
if not options.no_icon:
|
|
132
202
|
icon = _icon_data_url(interpreter, options.article)
|
|
133
203
|
if icon:
|
|
134
204
|
pairs = [(key, value) for key, value in pairs if key != "avatar_src"]
|
|
135
205
|
pairs.append(("avatar_src", icon))
|
|
136
|
-
note
|
|
206
|
+
note += " with the feed's icon"
|
|
137
207
|
else:
|
|
138
|
-
note
|
|
208
|
+
note += "; the feed has no icon, so previews generate one"
|
|
139
209
|
|
|
140
210
|
try:
|
|
141
211
|
output.parent.mkdir(parents=True, exist_ok=True)
|
|
142
|
-
output.write_text(fixture_text(pairs, output.stem), encoding="utf-8")
|
|
212
|
+
output.write_text(fixture_text(pairs, output.stem, attachments), encoding="utf-8")
|
|
143
213
|
except OSError as error:
|
|
144
214
|
result.SetError(f"could not write {output}: {error}")
|
|
145
215
|
return
|