Files
Momento/memento-note/lib/clip/extract-article.ts
Antigravity 0ee2afbd62
Some checks failed
CI / Deploy production (on server) (push) Has been cancelled
CI / Lint, Unit Tests & Build (push) Has been cancelled
fix(extension): sync robuste content/sidepanel, diagnostic panel, preview propre v0.4.4
- content.js : écrit la sélection dans chrome.storage.session/local en plus du message runtime
- sidepanel.js : lit le storage en fallback, loggue chaque étape, affiche un diagnostic panel
- sidepanel.js : avertissement explicite si le content script n'est pas détecté
- sidepanel.js : sépare le rendu Markdown du résumé et l'affichage texte brut de l'extrait
- sidepanel.css : agrandit la zone de prévisualisation
- i18n : nouvelles clés contentScriptMissing, diagnosticsTitle, copyDiagnostics
- locales : correction FR (Retour, Enregistrement, Note, etc.) + version 0.4.4 partout
- extract-article.ts : nettoie textContent des métadonnées (dates, temps de lecture, sources)
- analyze/route.ts : excerpt propre via makeExcerpt
- builds Chrome Store et Firefox regénérés en 0.4.4
2026-07-18 16:56:51 +00:00

117 lines
4.1 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
}
const META_PATTERNS = [
// Dates/heures avec "mis à jour / updated / actualizado / aktualisiert"
/\b(mis à jour|updated at|actualizado|aktualisiert am|aggiornato alle)\b[\s:|-]*.*/gi,
// Temps de lecture
/\b(temps de lecture|reading time|lesedauer|tiempo de lectura|tempo di lettura|czas czytania|время чтения|阅读时间|読了時間)\b[\s:|-]*.*/gi,
// Lignes de type "Source - Aujourd'hui à 09:15 | mis à jour à 11:07"
/^(.*?\s[-|]\s)(aujourd[']?hui|today|hoy|heute|oggi|vandaag|dzisiaj|сегодня|今日)\b.*$/gim,
// Heures seules type "09:15" ou "09:15 | mis à jour..."
/^\s*\d{1,2}:\d{2}(\s*[-|]\s*.*)?$/gm,
]
export function cleanClipText(text: string): string {
let cleaned = text.replace(/\r\n/g, '\n').replace(/\n{3,}/g, '\n\n')
for (const pattern of META_PATTERNS) {
cleaned = cleaned.replace(pattern, '')
}
return cleaned
.split('\n')
.map((line) => line.trim())
.filter((line) => line.length > 0)
.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>`
}