Files
Momento/memento-note/lib/clip/extract-article.ts
Antigravity 4946c0d44a
All checks were successful
CI / Lint, Unit Tests & Build (push) Successful in 5m44s
CI / Deploy production (on server) (push) Successful in 24s
fix(extension): permissions universelles Store + nettoyage article v0.4.5
- build-chrome-store.mjs / build-firefox.mjs : host_permissions et content_scripts matches
  passent à http://*/* + https://*/* au lieu de memento-note.com uniquement.
  Un Web Clipper a besoin d'accéder à tous les sites ; le manifest Store
  bloquait l'injection du content script sur n'importe quel article.
- extract-article.ts : refonte de cleanClipText en filtrage par ligne avec
  patterns élargis (mis à jour, temps de lecture, aujourd'hui à HH:MM,
  source - date, publié le, etc.).
- Bump version 0.4.5 dans manifest, locales, README et builds.
2026-07-18 17:04:25 +00:00

126 lines
4.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Readability } from '@mozilla/readability'
import { JSDOM } from 'jsdom'
import DOMPurify from 'isomorphic-dompurify'
import {
applyRtlToHtmlBlocks,
readPageLocaleFromHtml,
resolveClipLocale,
wrapClipArticleHtml,
type ClipLocaleHint,
} from '@/lib/clip/rtl-content'
export interface ExtractedArticle {
title: string
content: string
textContent: string
excerpt: string
locale: ClipLocaleHint
}
// Lignes de métadonnées qu'on veut retirer du texte extrait.
// On teste ligne par ligne pour ne pas tronquer du vrai contenu en plein milieu.
const META_LINE_PATTERNS = [
// "mis à jour", "updated at", "actualizado", "aktualisiert", "aggiornato"
/\b(mis à jour|updated at|updated|actualizado|actualizada|aktualisiert|aktualisiert am|aggiornato alle|aggiornato)\b/i,
// "Temps de lecture", "Reading time", etc.
/\b(temps de lecture|reading time|lesedauer|tiempo de lectura|tempo di lettura|czas czytania|время чтения|阅读时间|読了時間)\b/i,
// "Aujourd'hui à 09:15", "Today at 09:15", etc.
/\b(aujourd[']?hui|today|hoy|heute|oggi|vandaag|dzisiaj|сегодня|今日)\b.*\d{1,2}:\d{2}/i,
// "Le Progrès - Aujourd'hui à 09:15 | mis à jour..."
/^.*\s[-|]\s.*\d{1,2}:\d{2}/i,
// Heure seule "09:15" ou "09:15 | ..."
/^\s*\d{1,2}:\d{2}\s*([-|].*)?$/i,
// "Publié le ...", "Published on ..."
/\b(publié le|publié|published on|published|veröffentlicht am|publicado el|publicado)\b/i,
// "Source : ...", "Par ..." en début de ligne
/^\s*(source\s*:|par\s+|by\s+|auteur\s*:|author\s*:)/i,
]
function isMetaLine(line: string): boolean {
return META_LINE_PATTERNS.some((pattern) => pattern.test(line))
}
export function cleanClipText(text: string): string {
return text
.replace(/\r\n/g, '\n')
.split('\n')
.map((line) => line.trim())
.filter((line) => line.length > 0 && !isMetaLine(line))
.join('\n')
.replace(/\n{3,}/g, '\n\n')
.trim()
}
export function makeExcerpt(text: string, maxLen = 500): string {
if (text.length <= maxLen) return text
// Préférer couper à la fin d'une phrase.
const sentenceEnd = text.lastIndexOf('.', maxLen)
if (sentenceEnd > maxLen * 0.6) return text.slice(0, sentenceEnd + 1).trim()
// Sinon couper au dernier espace.
const space = text.lastIndexOf(' ', maxLen)
if (space > maxLen * 0.6) return text.slice(0, space).trim() + '…'
return text.slice(0, maxLen).trim() + '…'
}
export function extractArticleFromHtml(html: string, pageUrl: string): ExtractedArticle | null {
const dom = new JSDOM(html, { url: pageUrl })
const reader = new Readability(dom.window.document)
const article = reader.parse()
if (!article) return null
const pageLocale = readPageLocaleFromHtml(html)
const readabilityDir = article.dir?.toLowerCase() === 'rtl' ? 'rtl' : 'ltr'
const readabilityLang = article.lang?.split('-')[0]?.toLowerCase()
const locale = resolveClipLocale(
pageUrl,
article.title || '',
article.textContent || '',
)
const mergedLocale: ClipLocaleHint = {
direction:
readabilityDir === 'rtl' || pageLocale.direction === 'rtl' || locale.direction === 'rtl'
? 'rtl'
: 'ltr',
lang:
(readabilityLang === 'fa' || readabilityLang === 'ar' || readabilityLang === 'he'
? readabilityLang
: undefined) ||
locale.lang ||
pageLocale.lang,
}
const sanitized = DOMPurify.sanitize(article.content || '')
const rtlBlocks = applyRtlToHtmlBlocks(sanitized, mergedLocale)
const content = wrapClipArticleHtml(rtlBlocks, mergedLocale)
const cleanText = cleanClipText(article.textContent || '')
return {
title: (article.title || '').trim(),
content,
textContent: cleanText,
excerpt: makeExcerpt(cleanText, 500),
locale: mergedLocale,
}
}
export function clipFooterLocaleTag(lang?: string): string {
if (lang === 'fa') return 'fa-IR'
if (lang === 'ar') return 'ar'
if (lang === 'he') return 'he-IL'
return 'fr-FR'
}
export function buildClipSourceFooter(domain: string, date: Date, localeTag = 'fr-FR'): string {
const formatted = date.toLocaleDateString(localeTag, { day: 'numeric', month: 'long', year: 'numeric' })
const isRtl = localeTag.startsWith('fa') || localeTag.startsWith('ar') || localeTag.startsWith('he')
const label =
localeTag.startsWith('fa')
? `برگرفته از ${domain}${formatted}`
: localeTag.startsWith('ar')
? `مقتبس من ${domain}${formatted}`
: `Extrait de ${domain} le ${formatted}`
const dirAttr = isRtl ? ' dir="rtl"' : ''
return `<hr/><p${dirAttr}><small>${DOMPurify.sanitize(label)}</small></p>`
}