diff --git a/pages/cms/satzung.vue b/pages/cms/satzung.vue index 1caf010..d861175 100644 --- a/pages/cms/satzung.vue +++ b/pages/cms/satzung.vue @@ -183,12 +183,15 @@ async function loadCurrentSatzung() { lastUpdated.value = new Date().toLocaleDateString('de-DE') } if (satzung?.content) { - satzungContent.value = satzung.content + // Stelle sicher, dass der Inhalt als String geladen wird + const content = typeof satzung.content === 'string' ? satzung.content : String(satzung.content || '') + satzungContent.value = content } else { satzungContent.value = '' } } catch (e) { console.error('Fehler beim Laden der aktuellen Satzung:', e) + satzungContent.value = '' } } diff --git a/server/api/cms/satzung-upload.post.js b/server/api/cms/satzung-upload.post.js index e850b3c..43f5bde 100644 --- a/server/api/cms/satzung-upload.post.js +++ b/server/api/cms/satzung-upload.post.js @@ -161,74 +161,154 @@ export default defineEventHandler(async (event) => { // PDF-Text zu HTML konvertieren function convertTextToHtml(text) { // Text bereinigen und strukturieren - let html = text + let cleaned = text .replace(/\r\n/g, '\n') // Windows-Zeilenumbrüche normalisieren .replace(/\r/g, '\n') // Mac-Zeilenumbrüche normalisieren - .replace(/\n\s*\n/g, '\n\n') // Mehrfache Zeilenumbrüche reduzieren .trim() - // Seitenzahlen und Seitenfuß entfernen (z.B. "Seite 2 von 4", "-2-") - html = html + // Seitenzahlen und Seitenfuß entfernen + cleaned = cleaned .replace(/^Seite\s+\d+\s+von\s+\d+.*$/gm, '') .replace(/^-+\d+-+\s*$/gm, '') + .replace(/\n\s*-+\d+-+\s*\n/g, '\n') + .replace(/\s*-+\d+-+\s*/g, '') + .replace(/zuletzt geändert am \d{2}\.\d{2}\.\d{4}.*$/gm, '') - // Überschriften erkennen und formatieren - html = html.replace(/^(Vereinssatzung|Satzung)$/gm, '

$1

') - html = html.replace(/^(§\s*\d+[^§\n]*)$/gm, '

$1

') + // Zeilenweise aufteilen und leere Zeilen filtern + let rawLines = cleaned.split('\n').map(l => l.trim()).filter(l => { + if (!l || l.length === 0) return false + if (/^-+\d+-+$/.test(l)) return false + if (/^Seite\s+\d+\s+von\s+\d+/.test(l)) return false + return true + }) - // Absätze erstellen - html = html.split('\n\n').map(paragraph => { - paragraph = paragraph.trim() - if (!paragraph) return '' + // ============================================================ + // SCHRITT 1: Zusammengehörige Zeilen zusammenführen + // pdftotext trennt oft Nummer/Prefix und Inhalt auf zwei Zeilen + // ============================================================ + const merged = [] + for (let j = 0; j < rawLines.length; j++) { + const line = rawLines[j] + const next = j + 1 < rawLines.length ? rawLines[j + 1] : null - // Überschriften nicht als Paragraphen behandeln - if (paragraph.match(/^/) || paragraph.match(/^§\s*\d+/)) { - return paragraph + // Fall 1: "§ 1" (nur Paragraphennummer) + nächste Zeile ist der Titel + // z.B. "§ 1" + "Name, Sitz und Zweck" → "§ 1 Name, Sitz und Zweck" + if (/^§\s*\d+\s*$/.test(line) && next && !next.match(/^§/) && !next.match(/^\d+\.\s/)) { + merged.push(line + ' ' + next) + j++ // nächste Zeile überspringen + continue } - // Spezielle Behandlung für Aufzählungen mit a), b), c) ... - if (paragraph.match(/^[a-z]\)\s*$/mi)) { - const lines = paragraph.split('\n').map(l => l.trim()).filter(Boolean) - const items = [] - let current = '' + // Fall 2: "1." (nur Nummer mit Punkt) + nächste Zeile ist der Text + // z.B. "1." + "Der Harheimer TC..." → "1. Der Harheimer TC..." + if (/^\d+\.\s*$/.test(line) && next) { + merged.push(line + ' ' + next) + j++ + continue + } + + // Fall 3: "a)" (nur Buchstabe mit Klammer) + nächste Zeile ist der Text + // z.B. "a)" + "Die Bestimmungen..." → "a) Die Bestimmungen..." + if (/^[a-z]\)\s*$/i.test(line) && next) { + merged.push(line + ' ' + next) + j++ + continue + } + + // Keine Zusammenführung nötig + merged.push(line) + } + + // ============================================================ + // SCHRITT 2: HTML-Elemente erzeugen + // ============================================================ + const result = [] + let i = 0 + + while (i < merged.length) { + const line = merged[i] + + // Überschriften erkennen (§1, § 2, etc.) + if (line.match(/^§\s*\d+/)) { + result.push(`

${line}

`) + i++ + continue + } + + // Prüfe ob wir eine Liste mit a), b), c) haben + // Suche nach einem Muster wie "2. Text:" gefolgt von "a) ...", "b) ...", etc. + if (line.match(/^\d+\.\s+.*:$/) && i + 1 < merged.length && merged[i + 1].match(/^[a-z]\)\s+/i)) { + // Einleitender Text für die Liste (ohne Nummer) + const introText = line.replace(/^\d+\.\s+/, '') + const listItems = [] + i++ - for (const line of lines) { - if (/^[a-z]\)\s*$/i.test(line)) { - // neuer Aufzählungspunkt, vorherigen abschließen - if (current) items.push(current.trim()) - current = line - } else { - // Text zum aktuellen Aufzählungspunkt hinzufügen - current += (current ? ' ' : '') + line + // Sammle alle Listenpunkte a), b), c) ... + while (i < merged.length && merged[i].match(/^[a-z]\)\s+/i)) { + const itemText = merged[i].replace(/^[a-z]\)\s+/i, '').trim() + if (itemText) { + listItems.push(itemText) } + i++ } - if (current) items.push(current.trim()) - - const listItems = items.map(item => { - return `
  • ${item}
  • ` - }).join('') - - return `` + + if (listItems.length > 0) { + const listHtml = listItems.map(item => `
  • ${item}
  • `).join('') + result.push(`

    ${introText}

    `) + } else { + result.push(`

    ${line}

    `) + } + continue } - - // Allgemeine Listen erkennen (Bullet "•", Bindestrich- oder Nummern-Listen) - if (paragraph.includes('•') || paragraph.match(/^[\-•]\s/m) || paragraph.match(/^\d+\.\s/m)) { - const listItems = paragraph.split(/\n/).map(item => { - item = item.trim() - if (item.match(/^[•-]\s/) || item.match(/^\d+\.\s/)) { - return `
  • ${item.replace(/^[•-]\s/, '').replace(/^\d+\.\s/, '')}
  • ` + + // Einzelne Listenpunkte a), b), c) erkennen + if (line.match(/^[a-z]\)\s+/i)) { + const items = [] + while (i < merged.length && merged[i].match(/^[a-z]\)\s+/i)) { + const itemText = merged[i].replace(/^[a-z]\)\s+/i, '').trim() + if (itemText) { + items.push(itemText) } - return item ? `
  • ${item}
  • ` : '' - }).filter(Boolean).join('') - return `` + i++ + } + if (items.length > 0) { + const listHtml = items.map(item => `
  • ${item}
  • `).join('') + result.push(``) + } + continue + } + + // Nummerierte Listen (1., 2., 3.) - aber nur wenn mehrere aufeinander folgen + if (line.match(/^\d+\.\s+/) && i + 1 < merged.length && merged[i + 1].match(/^\d+\.\s+/)) { + const items = [] + while (i < merged.length && merged[i].match(/^\d+\.\s+/)) { + const itemText = merged[i].replace(/^\d+\.\s+/, '').trim() + // Prüfe ob es eine Einleitung für eine Unterliste ist (endet mit ":") + if (itemText.endsWith(':') && i + 1 < merged.length && merged[i + 1].match(/^[a-z]\)\s+/i)) { + break // Wird oben als Einleitung + Unterliste behandelt + } + if (itemText) { + items.push(itemText) + } + i++ + } + if (items.length > 0) { + const listHtml = items.map(item => `
  • ${item}
  • `).join('') + result.push(`
      ${listHtml}
    `) + } + continue } // Normale Absätze - return `

    ${paragraph.replace(/\n/g, '
    ')}

    ` - }).join('\n') + result.push(`

    ${line}

    `) + i++ + } - // Mehrfache Zeilenumbrüche entfernen - html = html.replace(/\n{3,}/g, '\n\n') + let html = result.join('\n') - return html + // Leere Absätze entfernen + html = html.replace(/

    \s*<\/p>/g, '') + html = html.replace(/

    <\/p>/g, '') + + return html.trim() }