import { linkifyHtml } from './linkify'
/**
* Lightweight message formatting for the ticket chat.
*
* Messages stay PLAIN TEXT in iDempiere (r_requestupdate.Result) with
* markdown-style markers, so every other consumer — list teasers, e-mail
* notifications, the mobile app, search — keeps showing readable text. Only the
* chat view renders them:
*
* **bold** →
* _italic_ / *italic* →
* - item / * item → - (also "• item")
* 1. item / 1) item →
-
* http(s)://… / www.… → (via linkifyHtml)
* [label](https://…) → (what the WYSIWYG composer emits)
* newline →
, blank line → small paragraph gap
*
* Safety: every text segment goes through linkifyHtml, which HTML-escapes it
* and only injects the tags it builds itself. Inline markers are applied
* AFTER escaping and never inside those tags, so the result is safe for
* `v-html` — the only markup emitted is strong/em/ul/ol/li/br/a and the gap div.
*/
// Inline markers, applied to already-escaped HTML (never inside …).
// Boundaries keep `_` inside words/URLs (snake_case, file_name.pdf) untouched.
const BOLD_RE = /\*\*(\S(?:[^*\n]*?\S)?)\*\*/g
const ITALIC_STAR_RE = /(^|[\s(>])\*(\S(?:[^*\n]*?\S)?)\*(?=$|[\s).,!?:;<])/g
const ITALIC_UNDERSCORE_RE = /(^|[\s(>])_(\S(?:[^_\n]*?\S)?)_(?=$|[\s).,!?:;<])/g
const applyInlineMarkers = (html: string): string => html
.replace(BOLD_RE, '$1')
.replace(ITALIC_STAR_RE, '$1$2')
.replace(ITALIC_UNDERSCORE_RE, '$1$2')
const escapeHtml = (s: string): string => s
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''')
// Markdown links — what the WYSIWYG composer emits for a pasted/auto-linked
// URL ("[label](https://…)"). Bare URLs are handled by linkifyHtml below.
const MD_LINK_RE = /\[([^\]\n]+)\]\((https?:\/\/[^\s)]+)\)/g
// Escape + linkify one segment, then format the non-link parts only.
const inlinePlain = (segment: string): string => linkifyHtml(segment)
.split(/(]*>.*?<\/a>)/)
.map((part, i) => (i % 2 === 1 ? part : applyInlineMarkers(part)))
.join('')
const inline = (line: string): string => {
let out = ''
let last = 0
MD_LINK_RE.lastIndex = 0
let m: RegExpExecArray | null
while ((m = MD_LINK_RE.exec(line)) !== null) {
out += inlinePlain(line.slice(last, m.index))
out += `${applyInlineMarkers(escapeHtml(m[1] ?? ''))}`
last = m.index + m[0].length
}
out += inlinePlain(line.slice(last))
return out
}
const BULLET_RE = /^\s*[-*•]\s+(.*)$/
const NUMBER_RE = /^\s*\d+[.)]\s+(.*)$/
export const formatChatHtml = (input: any): string => {
const text = input == null ? '' : String(input).replace(/\r\n?/g, '\n')
const lines = text.split('\n')
let out = ''
let list: 'ul' | 'ol' | null = null
let needBr = false
for (const raw of lines) {
const bullet = raw.match(BULLET_RE)
const number = bullet ? null : raw.match(NUMBER_RE)
const kind: 'ul' | 'ol' | null = bullet ? 'ul' : number ? 'ol' : null
if (kind) {
if (list !== kind) {
if (list) out += `${list}>`
out += `<${kind} class="msg-list">`
list = kind
}
out += ` - ${inline((bullet || number)?.[1] ?? '')}
`
needBr = false
continue
}
if (list) {
out += `${list}>`
list = null
needBr = false
}
// Blank line = paragraph break → a small gap rather than a full empty
// line, so editor output ("\n\n" between paragraphs) stays compact.
if (!raw.trim()) {
out += ''
needBr = false
continue
}
if (needBr) out += '
'
out += inline(raw)
needBr = true
}
if (list) out += `${list}>`
return out
}
export default formatChatHtml