OverviewFilesCommitsIssuesDocsReleasesPeers

ap-bridge / lib / html-text.js

36 lines · javascript ref:
// 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 }