feedkeeper 0.26.2 → 0.26.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "feedkeeper",
3
- "version": "0.26.2",
3
+ "version": "0.26.4",
4
4
  "description": "A lightweight, self-hosted RSS reader and remote MCP server for your news, blogs, and AI workflows.",
5
5
  "keywords": [
6
6
  "rss",
@@ -76,12 +76,28 @@ function removeReaderNoise(root, title, stats) {
76
76
  const headline = normalized(title ?? "");
77
77
  const seen = new Set();
78
78
  let leading = 0;
79
+ let reachedText = false;
79
80
  for (const block of leafBlocks(root)) {
80
81
  const text = flat(block.textContent);
81
82
  if (!text)
82
83
  continue;
83
84
  const position = leading++;
84
85
  const heading = /^H[1-3]$/.test(block.tagName);
86
+ // Before the first real paragraph: a standalone date and a section chip repeat what the page head says.
87
+ if (!reachedText && text.length >= 80)
88
+ reachedText = true;
89
+ if (!reachedText && position < 12) {
90
+ const anchorsInBlock = block.querySelectorAll("a");
91
+ const chip = anchorsInBlock.length === 1 && flat(anchorsInBlock[0].textContent) === text && text.length <= 30 && text.split(" ").length <= 3;
92
+ if (chip) {
93
+ drop(block, "section-chip");
94
+ continue;
95
+ }
96
+ if (/^(\d{1,2}\.\s?\d{1,2}\.\s?\d{2,4}|[A-Z][a-z]{2,8}\.? \d{1,2}, \d{4})([,\s•·|-]+\d{1,2}[:.]\d{2}( Uhr)?)?$/.test(text)) {
97
+ drop(block, "dateline");
98
+ continue;
99
+ }
100
+ }
85
101
  if (headline && normalized(text) === headline && (position < 4 || (heading && position < 14))) {
86
102
  drop(block, "title-repeat");
87
103
  continue;
@@ -228,9 +244,37 @@ export function cleanReaderHtml(html, title, stats = {}) {
228
244
  if (!html)
229
245
  return html;
230
246
  const root = JSDOM.fragment(html);
247
+ const images = resolveCustomImages(root, stats);
231
248
  const noise = removeReaderNoise(root, title, stats);
232
249
  const typography = removeTypography(root, stats);
233
- return noise || typography ? serialize(root) : html;
250
+ return images || noise || typography ? serialize(root) : html;
251
+ }
252
+ /**
253
+ * Some publishers wrap a picture in their own element and let a script fill the image: `<a-img src="/imgs/…">`
254
+ * around an `<img>` that only holds a grey placeholder. Without the script no reader would ever show the picture, so
255
+ * the wrapper's address takes the placeholder's place and the wrapper, which means nothing outside the page, goes.
256
+ */
257
+ function resolveCustomImages(root, stats) {
258
+ let changed = false;
259
+ for (const wrapper of [...root.querySelectorAll("a-img")]) {
260
+ const source = (wrapper.getAttribute("src") ?? "").trim();
261
+ if (source && !/^data:/i.test(source)) {
262
+ let image = wrapper.querySelector("img");
263
+ if (!image) {
264
+ image = wrapper.ownerDocument.createElement("img");
265
+ image.setAttribute("alt", wrapper.getAttribute("alt") ?? "");
266
+ wrapper.append(image);
267
+ }
268
+ const current = (image.getAttribute("src") ?? "").trim();
269
+ if (!current || /^data:/i.test(current))
270
+ image.setAttribute("src", source);
271
+ }
272
+ wrapper.replaceWith(...wrapper.childNodes);
273
+ changed = true;
274
+ }
275
+ if (changed)
276
+ stats["custom-image"] = (stats["custom-image"] ?? 0) + 1;
277
+ return changed;
234
278
  }
235
279
  const CREDIT_PATTERNS = [
236
280
  /^(©|\(c\)\s|copyright\b)/i,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feedkeeper/server",
3
- "version": "0.26.2",
3
+ "version": "0.26.4",
4
4
  "private": true,
5
5
  "type": "module"
6
6
  }