import JSZip from 'jszip'
function esc(str) {
return String(str)
.replace(/&/g, '&').replace(//g, '>')
.replace(/"/g, '"').replace(/'/g, ''')
}
function nodeToOdt(node) {
if (!node) return ''
const ch = () => (node.content || []).map(nodeToOdt).join('')
switch (node.type) {
case 'doc': return ch()
case 'paragraph': return `${ch()}\n`
case 'heading': {
const l = node.attrs?.level || 1
return `${ch()}\n`
}
case 'text': {
let t = esc(node.text || '')
const marks = node.marks || []
const bold = marks.some(m => m.type === 'bold')
const italic = marks.some(m => m.type === 'italic')
if (bold && italic) return `${t}`
if (bold) return `${t}`
if (italic) return `${t}`
return t
}
case 'bulletList': return (node.content || []).map(item =>
(item.content || []).map(p =>
`${(p.content || []).map(nodeToOdt).join('')}\n`
).join('')
).join('')
case 'orderedList': return (node.content || []).map(item =>
(item.content || []).map(p =>
`${(p.content || []).map(nodeToOdt).join('')}\n`
).join('')
).join('')
case 'blockquote': return (node.content || []).map(n =>
n.type === 'paragraph'
? `${(n.content || []).map(nodeToOdt).join('')}\n`
: nodeToOdt(n)
).join('')
case 'hardBreak': return ''
case 'image': return '' // ODT image embedding is complex — skipped
default: return ch()
}
}
const STYLES = `
`
export async function generateOdt(story) {
const zip = new JSZip()
const doc = typeof story.content === 'string' ? JSON.parse(story.content) : (story.content || {})
const title = story.title || 'Untitled'
const body = nodeToOdt(doc)
zip.file('mimetype', 'application/vnd.oasis.opendocument.text', { compression: 'STORE' })
zip.file('META-INF/manifest.xml', `
`)
zip.file('meta.xml', `
${esc(title)}
`)
zip.file('styles.xml', `
${STYLES}
`)
zip.file('content.xml', `
${esc(title)}
${body}
`)
return zip.generateAsync({ type: 'nodebuffer' })
}