feedkeeper 0.35.6 → 0.35.7
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
|
@@ -4,6 +4,7 @@ import { embedHeadings } from "./embeds/headings.js";
|
|
|
4
4
|
import { boilerplate } from "./noise/boilerplate.js";
|
|
5
5
|
import { loadingPlaceholders } from "./noise/loading.js";
|
|
6
6
|
import { trackingPixels } from "./noise/pixels.js";
|
|
7
|
+
import { permalinkGlyphs } from "./noise/permalinkGlyphs.js";
|
|
7
8
|
import { sidebars } from "./noise/sidebars.js";
|
|
8
9
|
import { teaserLinks } from "./noise/teaserLinks.js";
|
|
9
10
|
/**
|
|
@@ -11,7 +12,7 @@ import { teaserLinks } from "./noise/teaserLinks.js";
|
|
|
11
12
|
* tags that are removed next), then the clutter around the story. The tests check each rule against its fixtures in
|
|
12
13
|
* `test/fixtures/content/<rule name>` and that running it twice changes nothing. See `docs/content-rules.md`.
|
|
13
14
|
*/
|
|
14
|
-
export const PAGE_RULES = [lazyImages, ...EMBED_RULES, embedHeadings, sidebars, loadingPlaceholders, trackingPixels, boilerplate, teaserLinks];
|
|
15
|
+
export const PAGE_RULES = [lazyImages, ...EMBED_RULES, embedHeadings, sidebars, loadingPlaceholders, trackingPixels, boilerplate, teaserLinks, permalinkGlyphs];
|
|
15
16
|
export { resolveEmbeds };
|
|
16
17
|
/** What a fetched page goes through before its scripts, frames and `noscript` blocks are removed. */
|
|
17
18
|
export function cleanPage(root, stats = {}) {
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { count, flat, pruneEmpty } from "../shared.js";
|
|
2
|
+
/** One to three characters that are neither letters nor digits: "★", "∞", "¶", "#", "↩". Non-breaking spaces around them count as blanks. */
|
|
3
|
+
const GLYPH_ONLY = /^[^\p{L}\p{N}]{1,3}$/u;
|
|
4
|
+
/** Whether any text follows the element in the document: only the tail of a story is looked at. */
|
|
5
|
+
function hasTextAfter(root, element) {
|
|
6
|
+
const document = root.nodeType === 9 ? root : root.ownerDocument;
|
|
7
|
+
if (!document)
|
|
8
|
+
return false;
|
|
9
|
+
const walker = document.createTreeWalker(root, 4);
|
|
10
|
+
let reached = false;
|
|
11
|
+
for (let node = walker.nextNode(); node; node = walker.nextNode()) {
|
|
12
|
+
if (element.contains(node)) {
|
|
13
|
+
reached = true;
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
if (reached && flat(node.textContent) !== "")
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Some blogs end every post with a permanent link that is only a symbol ("★", "∞", "¶"): a link to the post itself, of no use inside a reader.
|
|
23
|
+
* The block must hold nothing but that one link, come after the text of the story and have nothing after it. A symbol inside a sentence,
|
|
24
|
+
* a link with a picture and a block with more text stay.
|
|
25
|
+
*/
|
|
26
|
+
export const permalinkGlyphs = {
|
|
27
|
+
name: "permalink-glyphs",
|
|
28
|
+
description: "A block at the end of a story that holds only one link made of a single symbol such as \"★\" or \"¶\" is removed.",
|
|
29
|
+
apply(root, stats) {
|
|
30
|
+
let changed = false;
|
|
31
|
+
const total = flat(root.textContent).length;
|
|
32
|
+
for (const block of [...root.querySelectorAll("p, div")].reverse()) {
|
|
33
|
+
if (!root.contains(block) || block.querySelector("p, div, img, picture, video, iframe"))
|
|
34
|
+
continue;
|
|
35
|
+
const links = block.querySelectorAll("a[href]");
|
|
36
|
+
if (links.length !== 1)
|
|
37
|
+
continue;
|
|
38
|
+
const text = (block.textContent ?? "").replace(/[\s\u00a0]+/g, "");
|
|
39
|
+
if (!GLYPH_ONLY.test(text) || flat(links[0].textContent).replace(/[\s\u00a0]+/g, "") !== text)
|
|
40
|
+
continue;
|
|
41
|
+
if (total - text.length < 120 || hasTextAfter(root, block))
|
|
42
|
+
continue;
|
|
43
|
+
const parent = block.parentNode;
|
|
44
|
+
block.remove();
|
|
45
|
+
pruneEmpty(parent);
|
|
46
|
+
count(stats, "permalink-glyphs");
|
|
47
|
+
changed = true;
|
|
48
|
+
}
|
|
49
|
+
return changed;
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
//# sourceMappingURL=permalinkGlyphs.js.map
|
|
@@ -7,6 +7,7 @@ import { SIDEBAR_HEADINGS } from "./content/patterns/sidebars/index.js";
|
|
|
7
7
|
import { resolveEmbeds } from "./content/embeds/index.js";
|
|
8
8
|
import { loadingPlaceholders } from "./content/noise/loading.js";
|
|
9
9
|
import { trackingPixels } from "./content/noise/pixels.js";
|
|
10
|
+
import { permalinkGlyphs } from "./content/noise/permalinkGlyphs.js";
|
|
10
11
|
import { teaserLinks } from "./content/noise/teaserLinks.js";
|
|
11
12
|
import { markVideoTeasers } from "./content/noise/videoTeasers.js";
|
|
12
13
|
import { flat, pruneEmpty } from "./content/shared.js";
|
|
@@ -95,6 +96,8 @@ function removeReaderNoise(root, title, stats, author) {
|
|
|
95
96
|
// A feed's own text ends with "[mehr]": the link to the article is no part of what is read.
|
|
96
97
|
if (teaserLinks.apply(root, stats))
|
|
97
98
|
changed = true;
|
|
99
|
+
if (permalinkGlyphs.apply(root, stats))
|
|
100
|
+
changed = true;
|
|
98
101
|
if (loadingPlaceholders.apply(root, stats))
|
|
99
102
|
changed = true;
|
|
100
103
|
if (trackingPixels.apply(root, stats))
|
|
@@ -42,6 +42,8 @@ export function imageSource(attribute) {
|
|
|
42
42
|
}
|
|
43
43
|
return null;
|
|
44
44
|
}
|
|
45
|
+
/** The body of a tag up to its closing `>`: a `>` inside a quoted value, as in `media="(resolution >= 3x)"`, does not end it. */
|
|
46
|
+
const TAG_BODY = `(?:[^>"']|"[^"]*"|'[^']*')*`;
|
|
45
47
|
function attribute(tag, name) {
|
|
46
48
|
const match = new RegExp(`\\s${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s>]+))`, "i").exec(tag);
|
|
47
49
|
return match ? match[1] ?? match[2] ?? match[3] : null;
|
|
@@ -56,14 +58,14 @@ export function normalizeImageSources(html) {
|
|
|
56
58
|
const withoutSources = tag.replace(/\s+(?:(?:data-(?:lazy-)?)?src(?:set)?|sizes|data-sizes|data-original|data-actualsrc)\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'>]+)/gi, "");
|
|
57
59
|
return withoutSources.replace(/\s*\/?>$/, (end) => ` src="${escaped}"${end}`);
|
|
58
60
|
};
|
|
59
|
-
const pictures = html.replace(
|
|
61
|
+
const pictures = html.replace(new RegExp(`<picture\\b${TAG_BODY}>[\\s\\S]*?<\\/picture\\s*>`, "gi"), (picture) => {
|
|
60
62
|
let fallback = null;
|
|
61
|
-
const withoutSources = picture.replace(
|
|
63
|
+
const withoutSources = picture.replace(new RegExp(`<source\\b${TAG_BODY}>`, "gi"), (tag) => {
|
|
62
64
|
fallback ??= firstSrcsetSource(attribute(tag, "data-lazy-srcset") ?? attribute(tag, "data-srcset") ?? attribute(tag, "srcset"));
|
|
63
65
|
return "";
|
|
64
66
|
});
|
|
65
|
-
return withoutSources.replace(
|
|
67
|
+
return withoutSources.replace(new RegExp(`<img\\b${TAG_BODY}>`, "gi"), (tag) => normalize(tag, fallback));
|
|
66
68
|
});
|
|
67
|
-
return pictures.replace(
|
|
69
|
+
return pictures.replace(new RegExp(`<img\\b${TAG_BODY}>`, "gi"), (tag) => normalize(tag));
|
|
68
70
|
}
|
|
69
71
|
//# sourceMappingURL=imageSources.js.map
|