Tofu/scripts/import-recipes/odt/extractOdtText.js
2026-09-18 01:41:02 +02:00

37 lines
1.5 KiB
JavaScript

import { decodeXmlEntities } from '#import-recipes/odt/decodeXmlEntities.js'
// Either a whole tag, or the run of text sitting between two tags.
const TOKEN_PATTERN = /<[^>]*>|[^<]+/g
const TAG_NAME_PATTERN = /^<\/?([\w:.-]+)/
const SPACE_COUNT_PATTERN = /text:c="(\d+)"/
// Turns the `content.xml` of an ODT document into the lines it displays.
//
// Every `<text:p>` and `<text:h>` boundary starts a new line, and so does a
// manual line break: someone who typed an ingredient list with Shift+Enter gets
// one entry per ingredient, which is exactly what the extraction step needs.
// Styling, tracked changes, bookmarks and other markup carry no text of their
// own and are skipped. Empty lines are dropped.
export function extractOdtText(contentXml) {
if (typeof contentXml !== 'string') return []
const lines = []
let current = ''
for (const token of contentXml.match(TOKEN_PATTERN) ?? []) {
if (token[0] !== '<') {
current += decodeXmlEntities(token)
continue
}
const tagName = (TAG_NAME_PATTERN.exec(token) ?? [])[1] ?? ''
if (tagName === 'text:p' || tagName === 'text:h' || tagName === 'text:line-break') {
lines.push(current)
current = ''
} else if (tagName === 'text:tab') {
current += '\t'
} else if (tagName === 'text:s') {
const count = Number.parseInt((SPACE_COUNT_PATTERN.exec(token) ?? [])[1] ?? '1', 10)
current += ' '.repeat(Number.isInteger(count) && count > 0 ? count : 1)
}
}
lines.push(current)
return lines.map((line) => line.trim()).filter((line) => line.length > 0)
}