Publication IA: - 4 templates (magazine, brief, essay, simple) avec CSS riche - Rewrite IA (article/exercises/tutorial/reference/mixed) - Modération avec timeout 12s + fallback safe - Quotas publish_enhance par tier (basic=2, pro=15, business=100) - Détection contenu stale (hash) - Migration DB publishedContent/publishedTemplate/publishedSourceHash Fixes: - cheerio v1.2: Element -> AnyNode (domhandler), decodeEntities cast - _isShared ajouté au type Note (champ virtuel serveur) - callout colors PDF export: extraction fonction pure testable - admin/published: guard note.userId null - Cmd+S fonctionne en mode dialog (pas seulement fullPage) i18n: - 23 clés publish* traduites dans les 15 locales - Extension Web Clipper: 13 locales mise à jour Tests: - callout-colors.test.ts (6 tests) - note-visible-in-view.test.ts (5 tests) - entitlements.test.ts + byok-entitlements.test.ts: mock usageLog + unstubAllEnvs - 199/199 tests passent Tracker: user-stories.md sync avec sprint-status.yaml
117 lines
3.9 KiB
TypeScript
117 lines
3.9 KiB
TypeScript
import { createHash } from 'crypto'
|
|
import type { PublishEnhanceSpec, PublishRewriteSpec, PublishTemplateId } from './types'
|
|
import { extractPublishImageUrls, isAllowedImageSrc, processNoteHtmlForPublish } from './process-note-html'
|
|
|
|
function esc(text: string): string {
|
|
return text
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
}
|
|
|
|
function heroFigure(src: string): string {
|
|
if (!isAllowedImageSrc(src)) return ''
|
|
return `<figure class="pub-hero"><img src="${esc(src)}" alt="" class="pub-hero-img" loading="eager" decoding="async" /></figure>`
|
|
}
|
|
|
|
function galleryFigures(urls: string[]): string {
|
|
const items = urls.map((src) => {
|
|
if (!isAllowedImageSrc(src)) return ''
|
|
return `<figure class="pub-gallery-item"><img src="${esc(src)}" alt="" class="pub-gallery-img" loading="lazy" decoding="async" /></figure>`
|
|
}).filter(Boolean).join('')
|
|
if (!items) return ''
|
|
return `<div class="pub-gallery" aria-label="Images">${items}</div>`
|
|
}
|
|
|
|
function removeFirstImageFromHtml(html: string): string {
|
|
return html.replace(/<figure[^>]*>[\s\S]*?<img[^>]*>[\s\S]*?<\/figure>|<img[^>]*>/i, '')
|
|
}
|
|
|
|
/* ─── Mode éditorial (texte original + habillage) ──────────────────────── */
|
|
export function renderPublishedTemplate(
|
|
spec: PublishEnhanceSpec,
|
|
template: PublishTemplateId,
|
|
sourceHtml: string,
|
|
): string {
|
|
const images = extractPublishImageUrls(sourceHtml)
|
|
const bodySource = images[0] ? removeFirstImageFromHtml(sourceHtml) : sourceHtml
|
|
const processedBody = processNoteHtmlForPublish(bodySource)
|
|
const hero = images[0] ? heroFigure(images[0]) : ''
|
|
const gallery = images.length > 1 ? galleryFigures(images.slice(1)) : ''
|
|
|
|
const summary = esc(spec.summary)
|
|
const pullQuote = spec.pullQuote
|
|
? `<blockquote class="pub-pull-quote"><p>${esc(spec.pullQuote)}</p></blockquote>`
|
|
: ''
|
|
const epigraph = spec.epigraph
|
|
? `<p class="pub-epigraph">${esc(spec.epigraph)}</p>`
|
|
: ''
|
|
const keyPoints = spec.keyPoints?.length
|
|
? `<div class="pub-key-points">
|
|
<p class="pub-key-points-label">Points clés</p>
|
|
<ul>${spec.keyPoints.map((k) => `<li>${esc(k)}</li>`).join('')}</ul>
|
|
</div>`
|
|
: ''
|
|
|
|
const bodyBlock = `<div class="pub-body-source">${processedBody}</div>`
|
|
|
|
if (template === 'brief') {
|
|
return `<div class="pub-tpl pub-tpl-brief">
|
|
${hero}
|
|
${summary ? `<div class="pub-brief-lead"><p>${summary}</p></div>` : ''}
|
|
${keyPoints}
|
|
${bodyBlock}
|
|
${gallery}
|
|
</div>`
|
|
}
|
|
|
|
if (template === 'essay') {
|
|
return `<div class="pub-tpl pub-tpl-essay">
|
|
${hero}
|
|
${epigraph}
|
|
${summary ? `<p class="pub-essay-summary">${summary}</p>` : ''}
|
|
${bodyBlock}
|
|
${gallery}
|
|
</div>`
|
|
}
|
|
|
|
return `<div class="pub-tpl pub-tpl-magazine">
|
|
${hero}
|
|
${summary ? `<p class="pub-magazine-dek">${summary}</p>` : ''}
|
|
${pullQuote}
|
|
${bodyBlock}
|
|
${gallery}
|
|
</div>`
|
|
}
|
|
|
|
/* ─── Mode réécriture (HTML sémantique IA) ─────────────────────────────── */
|
|
export function renderRewrittenTemplate(
|
|
spec: PublishRewriteSpec,
|
|
template: PublishTemplateId,
|
|
sourceHtml: string,
|
|
): string {
|
|
const images = extractPublishImageUrls(sourceHtml)
|
|
const hero = images[0] ? heroFigure(images[0]) : ''
|
|
const gallery = images.length > 1 ? galleryFigures(images.slice(1)) : ''
|
|
|
|
const summary = spec.summary
|
|
? `<p class="pub-rewrite-summary">${esc(spec.summary)}</p>`
|
|
: ''
|
|
|
|
const processedBody = processNoteHtmlForPublish(spec.body)
|
|
|
|
return `<div class="pub-tpl pub-tpl-${template} pub-tpl-rewrite" data-content-type="${spec.contentType}">
|
|
${hero}
|
|
${summary}
|
|
<div class="pub-rewrite-body">
|
|
${processedBody}
|
|
</div>
|
|
${gallery}
|
|
</div>`
|
|
}
|
|
|
|
export function computePublishedSourceHash(content: string): string {
|
|
return createHash('sha256').update(content || '').digest('hex').slice(0, 16)
|
|
}
|