Allow heading tags (h1-h6) in ContentSanitizer so news editor headings render

This commit is contained in:
2026-06-04 07:52:57 +02:00
parent 0b33a1b074
commit 15870ddb1f
191 changed files with 15453 additions and 1786 deletions

View File

@@ -0,0 +1,312 @@
// Vitest uses globals (configured in vite.config.mjs: test.globals = true).
// Pure logic tests for the news image prompt builder helpers.
// These helpers live in StudioNewsEditor.jsx — this file duplicates them
// so they can be tested in isolation without pulling in the full editor bundle.
// ── Minimal copies of the prompt-builder helpers ──────────────────────────
const NEWS_PROMPT_TYPE_MOODS = {
announcement: 'Futuristic',
release: 'Software Release',
editorial: 'Editorial',
opinion: 'Editorial',
tutorial: 'Clean Instructional',
platform_update: 'Modern Tech',
event: 'Futuristic',
challenge: 'Futuristic',
interview: 'Editorial',
spotlight: 'Editorial',
archive: 'Retro Tech',
industry_news: 'Modern Tech',
review: 'Modern Tech',
roundup: 'Modern Tech',
}
const NEWS_PROMPT_TYPE_ADDONS = {
release: 'Use a glossy software-release poster style with product UI panels, feature highlights, and a polished launch atmosphere.',
announcement: 'Use a clean announcement-poster style with a strong headline, clear hero image, and supporting modules that communicate the main update quickly.',
editorial: 'Use a refined editorial magazine-cover style with a strong visual concept, cleaner composition, and slightly more cinematic atmosphere.',
opinion: 'Use a refined editorial magazine-cover style with a strong visual concept, cleaner composition, and slightly more cinematic atmosphere.',
event: 'Use a conference or event-poster style with keynote energy, glowing screens, stage-like lighting, and a premium event atmosphere.',
tutorial: 'Use a clear structured instructional poster style with organized UI panels, workflow callouts, and helpful visual hierarchy.',
platform_update: 'Use a modern platform-update style with system UI visuals, feature modules, and a polished ecosystem presentation.',
archive: 'Use a retro-tech editorial style inspired by early 2000s computer magazines, with classic hardware, vintage UI influences, and modern polished lighting.',
}
const NEWS_PROMPT_KEYWORD_PATTERNS = [
{
keywords: ['apple', 'wwdc', 'ios', 'macos', 'iphone', 'ipad', 'swift'],
addon: 'Use a sleek developer-conference atmosphere with modern device screens, app ecosystem visuals, and a premium keynote mood.',
},
{
keywords: ['google', 'gemini', 'google i/o', 'android', 'pixel', 'tensorflow'],
addon: 'Use a colorful futuristic creative AI studio style with glowing panels, image and video creation tools, search elements, and generative media visuals.',
},
{
keywords: ['intel', 'amd', 'processor', 'cpu', 'gpu', 'nvidia', 'radeon', 'chip'],
addon: 'Use a retro computing hardware feature style with processor chips, technical callouts, old-school PC references, and magazine-cover energy.',
},
{
keywords: ['skin', 'theme', 'desktop', 'customize', 'customization', 'rainmeter', 'widget'],
addon: 'Use a desktop customization promo style with theme previews, icon panels, widget windows, and a glossy desktop software aesthetic.',
},
{
keywords: ['ai', 'artificial intelligence', 'llm', 'chatgpt', 'openai', 'midjourney', 'stable diffusion', 'generative'],
addon: 'Use a colorful futuristic creative AI studio style with glowing panels, generative media outputs, neural network visuals, and advanced AI tool interfaces.',
},
]
function resolveNewsPromptHeadline(data) {
return String(data.title || data.meta_title || '').trim() || 'Skinbase News'
}
function resolveNewsPromptSubheadline(data) {
const raw = String(data.excerpt || data.meta_description || '').replace(/<[^>]*>/g, '').trim()
if (raw) {
const words = raw.split(/\s+/)
return words.slice(0, 18).join(' ') + (words.length > 18 ? '…' : '')
}
const plain = String(data.content || '').replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim()
if (plain) {
const sentence = plain.split(/[.!?]/)[0].trim()
if (sentence.length > 10) {
const words = sentence.split(/\s+/)
return words.slice(0, 18).join(' ')
}
}
return 'Latest technology and creative industry update'
}
function resolveNewsPromptTopic(data) {
const parts = []
const cat = String(data.category || '').trim()
if (cat) parts.push(cat)
const tagList = (Array.isArray(data.tag_names) ? data.tag_names : []).slice(0, 5).filter(Boolean)
if (tagList.length) parts.push(tagList.join(', '))
if (!parts.length) {
const words = String(data.title || '').split(/\s+/).filter((w) => w.length > 3).slice(0, 4)
if (words.length) parts.push(words.join(' '))
}
return parts.join(' · ') || 'Technology and digital culture news'
}
function resolveNewsPromptType(data) {
const raw = String(data.type || '').trim()
if (!raw) return 'News'
return raw.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase())
}
function resolveNewsPromptMood(data) {
const type = String(data.type || '').toLowerCase().replace(/\s+/g, '_')
return NEWS_PROMPT_TYPE_MOODS[type] || 'Modern Tech'
}
function resolveNewsPromptTypeAddon(data) {
const type = String(data.type || '').toLowerCase().replace(/\s+/g, '_')
return NEWS_PROMPT_TYPE_ADDONS[type] || ''
}
function resolveNewsPromptKeywordAddon(data) {
const title = String(data.title || '').toLowerCase()
const tags = (Array.isArray(data.tag_names) ? data.tag_names : []).join(' ').toLowerCase()
const category = String(data.category || '').toLowerCase()
const combined = `${title} ${tags} ${category}`
const addons = []
for (const pattern of NEWS_PROMPT_KEYWORD_PATTERNS) {
if (pattern.keywords.some((kw) => combined.includes(kw))) {
addons.push(pattern.addon)
}
}
return [...new Set(addons)].join('\n')
}
function buildNewsImagePrompt(data) {
const headline = resolveNewsPromptHeadline(data)
const subheadline = resolveNewsPromptSubheadline(data)
const topic = resolveNewsPromptTopic(data)
const newsType = resolveNewsPromptType(data)
const mood = resolveNewsPromptMood(data)
const typeAddon = resolveNewsPromptTypeAddon(data)
const keywordAddon = resolveNewsPromptKeywordAddon(data)
const lines = [
'Create a premium Skinbase news cover image in 16:9 aspect ratio.',
'',
`Headline: "${headline}"`,
`Subheadline: "${subheadline}"`,
`Topic: ${topic}`,
`News type: ${newsType}`,
`Mood: ${mood}`,
]
if (typeAddon) lines.push('', typeAddon)
if (keywordAddon) lines.push('', keywordAddon)
return lines.join('\n')
}
// ─────────────────────────────────────────────────────────────────────────────
describe('resolveNewsPromptHeadline', () => {
it('returns title when present', () => {
expect(resolveNewsPromptHeadline({ title: 'My Title' })).toBe('My Title')
})
it('falls back to meta_title', () => {
expect(resolveNewsPromptHeadline({ title: '', meta_title: 'Meta Title' })).toBe('Meta Title')
})
it('returns fallback when both are empty', () => {
expect(resolveNewsPromptHeadline({})).toBe('Skinbase News')
})
})
describe('resolveNewsPromptSubheadline', () => {
it('returns excerpt truncated to 18 words', () => {
const long = 'word '.repeat(25).trim()
const result = resolveNewsPromptSubheadline({ excerpt: long })
expect(result.endsWith('…')).toBe(true)
expect(result.split(/\s+/).filter((w) => !w.startsWith('…') && w !== '…').length).toBeLessThanOrEqual(18)
})
it('returns excerpt as-is when short', () => {
expect(resolveNewsPromptSubheadline({ excerpt: 'Short excerpt.' })).toBe('Short excerpt.')
})
it('strips HTML tags from excerpt', () => {
expect(resolveNewsPromptSubheadline({ excerpt: '<p>Clean text</p>' })).toBe('Clean text')
})
it('returns fallback when all fields are empty', () => {
expect(resolveNewsPromptSubheadline({})).toBe('Latest technology and creative industry update')
})
it('falls back to meta_description when excerpt is missing', () => {
expect(resolveNewsPromptSubheadline({ meta_description: 'Meta desc' })).toBe('Meta desc')
})
})
describe('resolveNewsPromptTopic', () => {
it('includes category and tags', () => {
const result = resolveNewsPromptTopic({ category: 'Tech News', tag_names: ['AI', 'Google'] })
expect(result).toContain('Tech News')
expect(result).toContain('AI')
expect(result).toContain('Google')
})
it('falls back to title keywords when no category or tags', () => {
const result = resolveNewsPromptTopic({ title: 'Some Long Title With Words' })
expect(result.length).toBeGreaterThan(0)
})
it('returns generic fallback when all are empty', () => {
expect(resolveNewsPromptTopic({})).toBe('Technology and digital culture news')
})
it('caps tags at 5', () => {
const data = { tag_names: ['a', 'b', 'c', 'd', 'e', 'f', 'g'] }
const result = resolveNewsPromptTopic(data)
expect(result.split(',').length).toBeLessThanOrEqual(5)
})
})
describe('resolveNewsPromptType', () => {
it('capitalizes and humanizes underscored types', () => {
expect(resolveNewsPromptType({ type: 'platform_update' })).toBe('Platform Update')
})
it('returns "News" when type is missing', () => {
expect(resolveNewsPromptType({})).toBe('News')
})
it('handles simple types correctly', () => {
expect(resolveNewsPromptType({ type: 'editorial' })).toBe('Editorial')
expect(resolveNewsPromptType({ type: 'release' })).toBe('Release')
})
})
describe('resolveNewsPromptMood', () => {
it('returns correct mood for known types', () => {
expect(resolveNewsPromptMood({ type: 'archive' })).toBe('Retro Tech')
expect(resolveNewsPromptMood({ type: 'tutorial' })).toBe('Clean Instructional')
expect(resolveNewsPromptMood({ type: 'announcement' })).toBe('Futuristic')
expect(resolveNewsPromptMood({ type: 'release' })).toBe('Software Release')
})
it('returns Modern Tech fallback for unknown type', () => {
expect(resolveNewsPromptMood({ type: 'unknown_type' })).toBe('Modern Tech')
expect(resolveNewsPromptMood({})).toBe('Modern Tech')
})
})
describe('resolveNewsPromptTypeAddon', () => {
it('returns addon text for known types', () => {
expect(resolveNewsPromptTypeAddon({ type: 'release' })).toContain('software-release poster')
expect(resolveNewsPromptTypeAddon({ type: 'event' })).toContain('conference or event-poster')
expect(resolveNewsPromptTypeAddon({ type: 'archive' })).toContain('retro-tech editorial')
})
it('returns empty string for unknown type', () => {
expect(resolveNewsPromptTypeAddon({ type: 'unknown' })).toBe('')
expect(resolveNewsPromptTypeAddon({})).toBe('')
})
})
describe('resolveNewsPromptKeywordAddon', () => {
it('detects apple/WWDC keyword in title', () => {
const result = resolveNewsPromptKeywordAddon({ title: 'Apple WWDC 2026 Keynote', tag_names: [] })
expect(result).toContain('developer-conference atmosphere')
})
it('detects AI keyword in tags', () => {
const result = resolveNewsPromptKeywordAddon({ title: 'New Update', tag_names: ['AI', 'Gemini'] })
expect(result).toContain('creative AI studio')
})
it('detects desktop customization keyword', () => {
const result = resolveNewsPromptKeywordAddon({ title: 'Best desktop customization tools', tag_names: [] })
expect(result).toContain('desktop customization promo')
})
it('returns empty string when no keywords match', () => {
const result = resolveNewsPromptKeywordAddon({ title: 'Random Article', tag_names: ['general'] })
expect(result).toBe('')
})
it('deduplicates addons when multiple patterns match the same text', () => {
// "AI" matches both ai pattern; ensure no duplicates
const result = resolveNewsPromptKeywordAddon({ title: 'ai ai ai', tag_names: ['ai', 'artificial intelligence'] })
const lines = result.split('\n').filter(Boolean)
expect(lines.length).toBe(new Set(lines).size)
})
})
describe('buildNewsImagePrompt', () => {
it('includes the article headline in the prompt', () => {
const prompt = buildNewsImagePrompt({ title: 'Google I/O 2026 Keynote', type: 'announcement' })
expect(prompt).toContain('Google I/O 2026 Keynote')
})
it('includes the type addon for release articles', () => {
const prompt = buildNewsImagePrompt({ title: 'App Launch', type: 'release' })
expect(prompt).toContain('software-release poster')
})
it('includes the keyword addon when tags match', () => {
const prompt = buildNewsImagePrompt({ title: 'New skin', tag_names: ['desktop', 'theme'], type: 'release' })
expect(prompt).toContain('desktop customization promo')
})
it('uses fallback headline when title is missing', () => {
const prompt = buildNewsImagePrompt({})
expect(prompt).toContain('Skinbase News')
})
it('contains the 16:9 aspect ratio instruction', () => {
const prompt = buildNewsImagePrompt({ title: 'Test' })
expect(prompt).toContain('16:9')
})
it('contains the mood line', () => {
const prompt = buildNewsImagePrompt({ title: 'Tutorial Article', type: 'tutorial' })
expect(prompt).toContain('Clean Instructional')
})
})