あるSEのつぶやき・改

ITやシステム開発などの技術に関する話題を、取り上げたりしています。

NHKニュースでキーワードを含む記事を非表示にするGreasemonkeyスクリプト

NHKニュースで、特定のキーワードを含む記事を非表示にするGreasemonkeyスクリプトです。

ほぼ、ChatGPT に作ってもらいました。

// ==UserScript==
// @name        NHKニュースフィルター
// @namespace   nhk-title-filter
// @version     1.0
// @include     https://www3.nhk.or.jp/news/*
// @run-at      document-end
// @grant       none
// @noframes
// ==/UserScript==

(function () {
  'use strict';

  // キーワードを指定
  const keywords = 'keyword1, keyword2';

  const keywordsToHide = String(keywords)
    .split(/[,\u3001\uFF0C]/)
    .map(s => s.trim())
    .filter(Boolean)
    .map(s => s.normalize('NFKC').toLowerCase());

  if (keywordsToHide.length === 0) return;

  const CARD_SELECTORS = [
    'li', 'article',
    '.entrylist-contents',
    '.content--list-item',
    '.content__item',
    '.module--list-item',
    '.module__listItem',
    '.module-list__item',
    '.content-list-item',
    '.news-list-item'
  ].join(',');

  const MARK_ITEM = 'data-nhk-filtered';
  const MARK_LINK = 'data-nhk-checked';

  const norm = (s) => (s || '').normalize('NFKC').toLowerCase().trim();

  const getTitleFromNode = (root) => {
    let text = root.textContent || '';
    if (!text.trim() && root.getAttribute) {
      text = root.getAttribute('aria-label') || root.getAttribute('title') || '';
    }
    if (!text.trim()) {
      const img = root.querySelector && root.querySelector('img[alt]');
      if (img) text = img.getAttribute('alt') || '';
    }
    return norm(text);
  };

  const matches = (title) =>
    !!title && keywordsToHide.some(kw => kw && title.includes(kw));

  const findCard = (el) => {
    if (!el || el.nodeType !== 1) return null;
    const card = el.closest && el.closest(CARD_SELECTORS);
    if (card) return card;
    let p = el;
    for (let i = 0; i < 5 && p && p !== document.body; i++) {
      if (p.matches && p.matches(CARD_SELECTORS)) return p;
      p = p.parentElement;
    }
    return null;
  };

  function process(scope = document) {
    const links = scope.querySelectorAll('a[href*="/news/html/"]');
    for (const a of links) {
      if (a.getAttribute(MARK_LINK) === '1') continue;
      a.setAttribute(MARK_LINK, '1');

      const title = getTitleFromNode(a);
      if (!matches(title)) continue;

      const card = findCard(a) || a.parentElement;
      if (card && card.getAttribute(MARK_ITEM) !== '1') {
        card.style.display = 'none';
        card.setAttribute(MARK_ITEM, '1');
      }
    }

    const headings = scope.querySelectorAll('h1, h2, h3, .entrylist-contents-title, .content__title, .title, .tit');
    for (const h of headings) {
      if (h.getAttribute(MARK_ITEM) === '1') continue;
      const title = getTitleFromNode(h);
      if (!matches(title)) continue;

      const card = findCard(h);
      if (card) {
        card.style.display = 'none';
        card.setAttribute(MARK_ITEM, '1');
      }
    }
  }

  // initialize
  process();

  const observer = new MutationObserver(muts => {
    for (const m of muts) {
      for (const node of m.addedNodes) {
        if (node && node.nodeType === 1) process(node);
      }
    }
  });
  observer.observe(document.body, { childList: true, subtree: true });
})();