// html-text.js — a remote Note's HTML → plain text, for an observation's body (hyper-social #82, I1 S2).
//
// ⛔ PLAIN TEXT, NEVER MARKUP. The result lands in a favela record body that the surface renders and escapes; a
// remote server's HTML must never become markup on our side. Block boundaries survive as newlines, entities are
// decoded ONCE (so `&` stays `&`), script/style contents are not text, and the result is bounded.
const ENTITIES = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", nbsp: ' ' }
function decodeEntities (s) {
return s.replace(/&(#x[0-9a-f]+|#[0-9]+|[a-z]+);/gi, (m, e) => {
if (e[0] === '#') {
const code = e[1] === 'x' || e[1] === 'X' ? parseInt(e.slice(2), 16) : parseInt(e.slice(1), 10)
return Number.isFinite(code) && code > 0 && code < 0x110000 ? String.fromCodePoint(code) : m
}
const v = ENTITIES[e.toLowerCase()]
return v === undefined ? m : v
})
}
export function htmlToText (html, { max = 4000 } = {}) {
if (typeof html !== 'string' || !html) return ''
const text = html
.replace(/<(script|style)\b[^>]*>[\s\S]*?<\/\1\s*>/gi, '')
.replace(/<br\s*\/?>/gi, '\n')
.replace(/<\/p\s*>\s*<p\b[^>]*>/gi, '\n\n')
.replace(/<\/?(p|div|blockquote|li|ul|ol|h[1-6])\b[^>]*>/gi, '\n')
.replace(/<[^>]*>/g, '')
return decodeEntities(text)
.replace(/[ \t]+\n/g, '\n')
.replace(/\n{3,}/g, '\n\n')
.trim()
.slice(0, max)
}
export default { htmlToText }