wip: magazine tile layout, film descriptions, dev script fixes

- MagazineGrid: New Yorker tile layout with wide/half/dark/banner tiles
- Extract AI film descriptions into synopsis field
- Fix section extraction to handle ### headers
- Strip emojis from magazine titles and content
- FilmDetail: conditional synopsis with "Why watch" header
- FilmCard: synopsis preview for external films
- Consolidate dev server to single script
- Fix dev.sh for macOS bash 3.2 compatibility

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-03 00:27:51 +00:00
co-authored by Claude Opus 4.6
parent c82b4ef54f
commit 2f27cb86c4
7 changed files with 313 additions and 213 deletions
+38 -12
View File
@@ -115,8 +115,8 @@ function addSection(
content: string,
seen: Set<string>,
): void {
const t = title.trim().replace(/\s+/g, ' ').slice(0, 150)
const c = content.trim().replace(/\n{2,}/g, '\n\n').slice(0, MAGAZINE_CONTENT_MAX)
const t = title.trim().replace(/[\p{Emoji_Presentation}\p{Extended_Pictographic}]\s*/gu, '').replace(/^#+\s*/, '').replace(/\s+/g, ' ').slice(0, 150)
const c = content.trim().replace(/[\p{Emoji_Presentation}\p{Extended_Pictographic}]\s*/gu, '').replace(/\n{2,}/g, '\n\n').slice(0, MAGAZINE_CONTENT_MAX)
if (t.length < 2 || c.length < 15) return
const key = `${t.slice(0, 50)}`
if (seen.has(key)) return
@@ -143,11 +143,11 @@ function extractMagazineSections(text: string): MagazineSection[] {
const seen = new Set<string>()
const blockContents = new Set<string>() // avoid duplicate bullets already in ## blocks
// 1. ## Heading blocks: full content until next ## or **Section**
const headingRe = /^##\s*[^\n]*?(?:⚡|🔥|📌|✨)?\s*(.+?)\n\n([\s\S]+?)(?=\n##\s|\n\*\*[^*]+\*\*[🟠🔴🟢]?|\n\nFor deeper|\n\nThis is being|\z)/gim
// 1. ## or ### Heading blocks: full content until next heading or **Section**
const headingRe = /^#{2,3}\s*[^\n]*?(?:⚡|🔥|📌|✨|📺|⭐|🔄|🎬|🎭|🎵|🎧|📖|💡|🔍|🌍|💭|🧠|🔬|📊|🏆|📝)?\s*(.+?)\n\n([\s\S]+?)(?=\n#{2,3}\s|\n\*\*[^*]+\*\*[🟠🔴🟢]?|\n\nFor deeper|\n\nThis is being|\n\n---|\n\n\*\*TL;DR|\z)/gim
let m: RegExpExecArray | null
while ((m = headingRe.exec(text)) !== null) {
const title = m[1].trim().replace(/^[⚡🔥📌✨]\s*/, '').split(':')[0].trim()
const title = m[1].trim().replace(/[\p{Emoji_Presentation}\p{Extended_Pictographic}]\s*/gu, '').split(':')[0].trim()
const content = m[2].trim()
if (content.length > 20) {
blockContents.add(content)
@@ -156,15 +156,15 @@ function extractMagazineSections(text: string): MagazineSection[] {
}
// 2. **Pro/ Anti camp** blocks with their bullets
const campRe = /\*\*([^*]+(?:camp| side| view)[^*]*)\*\*[🟠🔴🟢]?\s*\n([\s\S]+?)(?=\n\*\*[^*]+(?:camp| side)[^*]*\*\*|\n##\s|\n\nThis is|\n\nFor deeper|\z)/gim
const campRe = /\*\*([^*]+(?:camp| side| view)[^*]*)\*\*[🟠🔴🟢]?\s*\n([\s\S]+?)(?=\n\*\*[^*]+(?:camp| side)[^*]*\*\*|\n#{2,3}\s|\n\nThis is|\n\nFor deeper|\z)/gim
while ((m = campRe.exec(text)) !== null) {
const title = m[1].trim()
const content = m[2].trim()
if (content.length > 15) addSection(sections, title, content, seen)
}
// 3. Bullets: - **Title**: Content (skip if already in a ## block)
const bulletRe = /^\s*[-]\s*\*\*([^*]+)\*\*\s*[:\u2014\u2013]\s*(.+?)(?=\n\s*[-]\s*\*\*|\n\s*[-]\s+\w|\n\n##\s|\n\*\*[^*]+\*\*[🟠🔴]?|\z)/gms
// 3. Bullets: - **Title**: Content (skip if already in a ## or ### block)
const bulletRe = /^\s*[-]\s*\*\*([^*]+)\*\*\s*[:\u2014\u2013]\s*(.+?)(?=\n\s*[-]\s*\*\*|\n\s*[-]\s+\w|\n\n#{2,3}\s|\n\*\*[^*]+\*\*[🟠🔴]?|\z)/gms
while ((m = bulletRe.exec(text)) !== null) {
const raw = m[2].trim().replace(/\n+/g, ' ')
if (raw.length < 15) continue
@@ -173,14 +173,14 @@ function extractMagazineSections(text: string): MagazineSection[] {
}
// 4. - **Name** (Role) description — attributed quotes
const attrRe = /^\s*[-]\s*\*\*([^*]+)\*\*\s*\(([^)]+)\)\s+([^-\n].+?)(?=\n\s*[-]|\n\*\*|\n##\s|\z)/gms
const attrRe = /^\s*[-]\s*\*\*([^*]+)\*\*\s*\(([^)]+)\)\s+([^-\n].+?)(?=\n\s*[-]|\n\*\*|\n#{2,3}\s|\z)/gms
while ((m = attrRe.exec(text)) !== null) {
const content = m[3].trim().replace(/\n+/g, ' ')
if (content.length > 20) addSection(sections, `${m[1].trim()} (${m[2].trim()})`, content, seen)
}
// 5. Intro paragraph before first ## or ---
const introMatch = /^([\s\S]+?)(?=\n##\s|\n---\s*\n|\n-\s+\*\*[^*]+\*\*\s*[:\u2014])/m.exec(text)
// 5. Intro paragraph before first ## / ### or ---
const introMatch = /^([\s\S]+?)(?=\n#{2,3}\s|\n---\s*\n|\n-\s+\*\*[^*]+\*\*\s*[:\u2014])/m.exec(text)
if (introMatch) {
const intro = introMatch[1].trim().replace(/^[#*_\s-]+/gm, '').trim()
if (intro.length > 60 && !seen.has('Summary')) {
@@ -415,6 +415,32 @@ export function useContentPanel() {
.filter((f): f is Film => !!f)
}
function extractDescriptionForTag(text: string, matchIndex: number, matchLength: number): string {
const prevNewline = text.lastIndexOf('\n', matchIndex - 1)
const lineStart = prevNewline === -1 ? 0 : prevNewline + 1
const nextNewline = text.indexOf('\n', matchIndex + matchLength)
const lineEnd = nextNewline === -1 ? text.length : nextNewline
let line = text.slice(lineStart, lineEnd)
// Remove the tag itself
line = line.replace(text.slice(matchIndex, matchIndex + matchLength), '')
// Remove other content tags on the same line
line = line.replace(/\[\[(?:film|song|podcast)(?:_ext)?:[^\]]*\]\]/g, '')
// Remove leading bullet/list markers
line = line.replace(/^\s*[-*•]\s*/, '').replace(/^\s*\d+\.\s*/, '')
// Remove **Bold Title** followed by separator
line = line.replace(/\*\*[^*]+\*\*\s*[-–—:]\s*/, '').replace(/\*\*[^*]+\*\*\s*/, '')
// Remove stray markdown bold/italic
line = line.replace(/\*\*/g, '').replace(/\*/g, '')
// Remove parenthetical (Year) duplicating tag data
line = line.replace(/\(\d{4}\)\s*/g, '')
// Clean separators at edges
line = line.replace(/^[\s\-–—:,]+/, '').replace(/[\s\-–—:,]+$/, '')
const result = line.trim().slice(0, 300)
return result.length >= 10 ? result : ''
}
function extractExternalFilms(text: string): Film[] {
const films: Film[] = []
const seen = new Set<string>()
@@ -432,7 +458,7 @@ export function useContentPanel() {
title,
year,
posterUrl: generatePosterFallback(title, year),
synopsis: '',
synopsis: extractDescriptionForTag(text, match.index, match[0].length),
genres: [],
rating: 0,
runtime: 0,