Files
Momento/memento-note/lib/publish/template-render.ts
Antigravity 96e7902f01
Some checks failed
CI / Lint, Unit Tests & Build (push) Failing after 1m22s
CI / Deploy production (on server) (push) Has been skipped
feat: publication IA (magazine/brief/essay) + fixes critique
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
2026-06-28 07:32:57 +00:00

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, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
}
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)
}