feedkeeper 0.35.7 → 0.35.8

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.35.7",
3
+ "version": "0.35.8",
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",
@@ -3,6 +3,7 @@ import { EMBED_RULES, resolveEmbeds } from "./embeds/index.js";
3
3
  import { embedHeadings } from "./embeds/headings.js";
4
4
  import { boilerplate } from "./noise/boilerplate.js";
5
5
  import { loadingPlaceholders } from "./noise/loading.js";
6
+ import { pageChrome } from "./noise/pageChrome.js";
6
7
  import { trackingPixels } from "./noise/pixels.js";
7
8
  import { permalinkGlyphs } from "./noise/permalinkGlyphs.js";
8
9
  import { sidebars } from "./noise/sidebars.js";
@@ -12,7 +13,7 @@ import { teaserLinks } from "./noise/teaserLinks.js";
12
13
  * tags that are removed next), then the clutter around the story. The tests check each rule against its fixtures in
13
14
  * `test/fixtures/content/<rule name>` and that running it twice changes nothing. See `docs/content-rules.md`.
14
15
  */
15
- export const PAGE_RULES = [lazyImages, ...EMBED_RULES, embedHeadings, sidebars, loadingPlaceholders, trackingPixels, boilerplate, teaserLinks, permalinkGlyphs];
16
+ export const PAGE_RULES = [lazyImages, ...EMBED_RULES, embedHeadings, pageChrome, sidebars, loadingPlaceholders, trackingPixels, boilerplate, teaserLinks, permalinkGlyphs];
16
17
  export { resolveEmbeds };
17
18
  /** What a fetched page goes through before its scripts, frames and `noscript` blocks are removed. */
18
19
  export function cleanPage(root, stats = {}) {
@@ -0,0 +1,38 @@
1
+ import { count, flat, pruneEmpty } from "../shared.js";
2
+ /** A paragraph this long is prose, not a menu entry: an element that holds one is never page chrome, whatever it is called. */
3
+ const PROSE_CHARACTERS = 200;
4
+ /** What only a site's own navigation contains. */
5
+ const NAVIGATION = "nav, [role='navigation'], [role='menubar'], menu";
6
+ /** A page header holds the site's navigation, or even the page's main area; an article's own header holds a headline and a byline. */
7
+ const PAGE_HEADER_PARTS = `${NAVIGATION}, main`;
8
+ function holdsProse(element) {
9
+ return [...element.querySelectorAll("p")].some(paragraph => flat(paragraph.textContent).length >= PROSE_CHARACTERS);
10
+ }
11
+ /**
12
+ * Some publishers put their whole page header into the text of an article: a menu of magazines with empty entries
13
+ * (their icons are drawn by a stylesheet), service links, the logo and lists of the publisher's other pages. A reader
14
+ * shows all of it before the first line of the story. A header that contains navigation, and any navigation of its own, are
15
+ * the site and not the story: they go, unless prose sits inside, which means the markup is broken and wraps the article.
16
+ */
17
+ export const pageChrome = {
18
+ name: "page-chrome",
19
+ description: "A page header that holds the site's navigation, and any navigation element, are removed from the text of an article.",
20
+ apply(root, stats) {
21
+ let changed = false;
22
+ const remove = (element) => {
23
+ if (!element.parentNode || holdsProse(element))
24
+ return;
25
+ const parent = element.parentNode;
26
+ element.remove();
27
+ pruneEmpty(parent);
28
+ count(stats, "page-chrome");
29
+ changed = true;
30
+ };
31
+ for (const header of [...root.querySelectorAll("header")].filter(candidate => candidate.querySelector(PAGE_HEADER_PARTS)))
32
+ remove(header);
33
+ for (const navigation of [...root.querySelectorAll(NAVIGATION)])
34
+ remove(navigation);
35
+ return changed;
36
+ },
37
+ };
38
+ //# sourceMappingURL=pageChrome.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 { pageChrome } from "./content/noise/pageChrome.js";
10
11
  import { permalinkGlyphs } from "./content/noise/permalinkGlyphs.js";
11
12
  import { teaserLinks } from "./content/noise/teaserLinks.js";
12
13
  import { markVideoTeasers } from "./content/noise/videoTeasers.js";
@@ -102,6 +103,8 @@ function removeReaderNoise(root, title, stats, author) {
102
103
  changed = true;
103
104
  if (trackingPixels.apply(root, stats))
104
105
  changed = true;
106
+ if (pageChrome.apply(root, stats))
107
+ changed = true;
105
108
  for (const selector of BREADCRUMB_MARKERS) {
106
109
  try {
107
110
  root.querySelectorAll(selector).forEach(element => drop(element, "breadcrumb"));
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feedkeeper/server",
3
- "version": "0.35.7",
3
+ "version": "0.35.8",
4
4
  "private": true,
5
5
  "type": "module"
6
6
  }