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()
}