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/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 fixture_text(pairs: list[tuple[str, str]], name: str) -> str:
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
- return header + "\n".join(toml_line(key, value) for key, value in ordered) + "\n"
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 = " with the feed's icon"
206
+ note += " with the feed's icon"
137
207
  else:
138
- note = "; the feed has no icon, so previews generate one"
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nnw-theme",
3
- "version": "2.1.0",
3
+ "version": "2.3.0",
4
4
  "description": "Create, preview, check, and publish a NetNewsWire theme.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Dave Marquard",