From 4047e341e289ba0b78bc72ae243a7c4176984381 Mon Sep 17 00:00:00 2001 From: Enoch Ma Date: Mon, 2 Feb 2026 19:13:39 +0100 Subject: [PATCH] fix: quanta magazine (#21031) * exclude economist * fix: use String.fromCodePoint instead of fromCharCode * delete old version * Update lib/routes/quantamagazine/archive.ts Co-authored-by: Tony * obtain the author information differently --------- --- .../quantamagazine/archive.js | 81 ---------- lib/routes/quantamagazine/archive.ts | 139 ++++++++++++++++++ lib/routes/quantamagazine/namespace.ts | 7 + 3 files changed, 146 insertions(+), 81 deletions(-) delete mode 100644 lib/routes-deprecated/quantamagazine/archive.js create mode 100644 lib/routes/quantamagazine/archive.ts create mode 100644 lib/routes/quantamagazine/namespace.ts diff --git a/lib/routes-deprecated/quantamagazine/archive.js b/lib/routes-deprecated/quantamagazine/archive.js deleted file mode 100644 index ad22dc748..000000000 --- a/lib/routes-deprecated/quantamagazine/archive.js +++ /dev/null @@ -1,81 +0,0 @@ -const cheerio = require('cheerio'); -const got = require('@/utils/got'); - -module.exports = async (ctx) => { - const url = `https://feeder.co/discover/c1d270a6db/quantamagazine-org`; - - const res = await got.get(url); - const $ = cheerio.load(res.data); - const list = $('.card-title').get(); - - function batchRemove($, array) { - for (const element of array) { - $(element).remove(); - } - } - - function batchRemoveAll($, array) { - for (const element of array) { - $(element).each((index, item) => { - $(item).remove(); - }); - } - } - - const out = await Promise.all( - list.map(async (item) => { - const $ = cheerio.load(item); - const title = $('.discover-feed-item-link').text(); - const address = $('.discover-feed-item-link').attr('href'); - const cache = await ctx.cache.get(address); - if (cache) { - return JSON.parse(cache); - } - const res = await got.get(address); - const capture = cheerio.load(res.data); - - const infos = ['.post__footer', '.post__title__author-date']; - batchRemove(capture, infos); - - const author = capture('h3.mv05').text(); - const time = capture('.align-c.mb075 > p > em').text(); - - const unnecessary = ['.header-spacer', '.scale1.mha', '.post__title__author-date', '.post__aside--divider']; - batchRemove(capture, unnecessary); - - const disturbing = ['.hide-on-print', '.post__aside__pullquote', 'aside.post__sidebar.hide']; - batchRemoveAll(capture, disturbing); - - let contents = capture('#postBody').html(); - if (contents !== null) { - const latex = contents.replaceAll(/\$latex([\S\s]+?)\$/g, ''); - contents = latex.replaceAll(/
?/g, (omit, src, cap) => { - const imgUrl = src.replaceAll(/\\([^nu])/g, '$1'); - const img = ''; - - const noBS = cap.replaceAll(/\\([^nu])/g, '$1'); - const removeNL = noBS.replaceAll('\\n', ''); - const caption = removeNL.replaceAll(/\\u(\d{1,3}[a-z]\d?|\d{4}?)/g, (omit, s) => String.fromCharCode(Number.parseInt(s, 16))); - const inset = '
' + img + '
' + caption + '
' + '
'; - return inset; - }); - } - - const single = { - title, - author, - description: contents, - link: address, - guid: address, - pubDate: new Date(time).toUTCString(), - }; - ctx.cache.set(address, JSON.stringify(single)); - return single; - }) - ); - ctx.state.data = { - title: 'Quanta Magazine', - link: `https://www.quantamagazine.org/`, - item: out, - }; -}; diff --git a/lib/routes/quantamagazine/archive.ts b/lib/routes/quantamagazine/archive.ts new file mode 100644 index 000000000..c9bf893db --- /dev/null +++ b/lib/routes/quantamagazine/archive.ts @@ -0,0 +1,139 @@ +import { load } from 'cheerio'; + +import type { Route } from '@/types'; +import cache from '@/utils/cache'; +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; + +const rootUrl = 'https://www.quantamagazine.org'; + +const processArticleContent = (html: string | null, articleLink?: string): string => { + if (!html) { + return ''; + } + + // Handle LaTeX formulas + let processed = html.replaceAll(/\$latex([\S\s]+?)\$/g, ''); + + // Handle embedded images with captions + processed = processed.replaceAll(/
?/g, (_match, src, cap) => { + const imgUrl = src.replaceAll(/\\([^nu])/g, '$1'); + const img = ``; + + const noBS = cap.replaceAll(/\\([^nu])/g, '$1'); + const removeNL = noBS.replaceAll(String.raw`\n`, ''); + const caption = removeNL.replaceAll(/\\u(\d{1,3}[a-z]\d?|\d{4}?)/g, (_omit, s) => String.fromCodePoint(Number.parseInt(s, 16))); + + return `
${img}
${caption}
`; + }); + + // Handle lottie-player animations + // Multiple lottie-players might exist (desktop/mobile versions) - replace all with placeholders first + const lottieMatches = [...processed.matchAll(/]*src="([^"]+)"[^>]*><\/lottie-player>/g)]; + const uniqueAnimations = new Set(); + + // Replace each lottie-player, but track unique animations by filename + for (const match of lottieMatches) { + const src = match[1]; + // Extract animation name (without Desktop/Mobile suffix) + const animName = + src + .split('/') + .pop() + ?.replace(/-(Desktop|Mobile).*\.json$/, '') || 'animation'; + + if (uniqueAnimations.has(animName)) { + // Duplicate (mobile/desktop variant): just remove it + processed = processed.replace(match[0], ''); + } else { + // First occurrence: replace with badge that links to the article + uniqueAnimations.add(animName); + const linkUrl = articleLink || rootUrl; + const badgeImg = 'https://img.shields.io/badge/🎬-View_Interactive_Animation-0066CC?style=for-the-badge'; + const replacement = `

View Interactive Animation

`; + processed = processed.replace(match[0], replacement); + } + } + + return processed; +}; + +export const handler = async (ctx) => { + const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 20; + + const apiUrl = `${rootUrl}/wp-json/wp/v2/posts`; + const posts = await ofetch(apiUrl, { + query: { + per_page: limit, + page: 1, + _embed: 'author', + }, + }); + + const items = await Promise.all( + posts.map((item) => + cache.tryGet(item.link, async () => { + // Get author name from embedded data + const authorName = item._embedded?.author?.[0]?.name || ''; + + // Fetch full article content from the page + const response = await ofetch(item.link, { + parseResponse: (txt) => txt, + }); + const $ = load(response); + + // Remove unnecessary elements + $('.header-spacer, .scale1.mha, .post__title__author-date, .post__aside--divider').remove(); + $('.hide-on-print, .post__aside__pullquote, aside.post__sidebar.hide, nav[data-glide-el]').remove(); + $('.post__footer, .post__title__author-date').remove(); + // Remove video placeholder images (the poster is already in the video element) + $('.iframe-placeholder').remove(); + + const contents = processArticleContent($('#postBody').html(), item.link); + + return { + title: item.title.rendered, + author: authorName, + description: contents, + link: item.link, + guid: item.link, + pubDate: parseDate(item.date), + }; + }) + ) + ); + + return { + title: 'Quanta Magazine', + link: rootUrl, + item: items, + }; +}; + +export const route: Route = { + path: '/archive', + name: 'Archive', + url: 'quantamagazine.org', + maintainers: ['emdoe'], + handler, + example: '/quantamagazine/archive', + parameters: {}, + description: 'Get the latest articles from Quanta Magazine.', + categories: ['new-media'], + + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['quantamagazine.org'], + target: '/archive', + }, + ], +}; diff --git a/lib/routes/quantamagazine/namespace.ts b/lib/routes/quantamagazine/namespace.ts new file mode 100644 index 000000000..cdd4c7af8 --- /dev/null +++ b/lib/routes/quantamagazine/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Quanta Magazine', + url: 'quantamagazine.org', + lang: 'en', +};