@open-mercato/core 0.4.5-develop-974adb54b3 → 0.4.5-develop-3f7d1d7925

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
+ import sanitizeHtml from "sanitize-html";
2
3
  const SIGNATURE_PATTERNS = [
3
4
  /^--\s*$/m,
4
5
  /^Sent from my (iPhone|iPad|Android|Galaxy|Samsung|Pixel)/m,
@@ -57,7 +58,11 @@ function stripQuotedReplies(text) {
57
58
  return cleanLines.join("\n").trimEnd();
58
59
  }
59
60
  function stripHtml(html) {
60
- return html.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "").replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "").replace(/<br\s*\/?>/gi, "\n").replace(/<\/p>/gi, "\n\n").replace(/<\/div>/gi, "\n").replace(/<\/tr>/gi, "\n").replace(/<\/li>/gi, "\n").replace(/<[^>]+>/g, "").replace(/&nbsp;/gi, " ").replace(/&amp;/gi, "&").replace(/&lt;/gi, "<").replace(/&gt;/gi, ">").replace(/&quot;/gi, '"').replace(/&#39;/gi, "'").replace(/\n{3,}/g, "\n\n").trim();
61
+ const sanitized = sanitizeHtml(html, {
62
+ allowedTags: [],
63
+ allowedAttributes: {}
64
+ });
65
+ return sanitized.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
61
66
  }
62
67
  function normalizeText(text) {
63
68
  return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\t/g, " ").replace(/ {2,}/g, " ").replace(/\n{3,}/g, "\n\n").trim();
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/inbox_ops/lib/emailParser.ts"],
4
- "sourcesContent": ["import { createHash } from 'node:crypto'\nimport type { InboxEmail, ThreadMessage } from '../data/entities'\n\nexport interface ParsedEmail {\n messageId?: string | null\n from: { name?: string; email: string }\n to: { name?: string; email: string }[]\n subject: string\n replyTo?: string | null\n inReplyTo?: string | null\n references?: string[] | null\n rawText?: string | null\n rawHtml?: string | null\n cleanedText: string\n threadMessages: ThreadMessage[]\n detectedLanguage?: string | null\n contentHash: string\n}\n\nconst SIGNATURE_PATTERNS = [\n /^--\\s*$/m,\n /^Sent from my (iPhone|iPad|Android|Galaxy|Samsung|Pixel)/m,\n /^Get Outlook for/m,\n /^_{10,}/m,\n /^Regards,?\\s*$/m,\n /^Best,?\\s*$/m,\n /^Thanks,?\\s*$/m,\n /^Cheers,?\\s*$/m,\n /^Kind regards,?\\s*$/m,\n /^Best regards,?\\s*$/m,\n]\n\nconst QUOTE_PATTERNS = [\n /^On .+ wrote:\\s*$/m,\n /^>+\\s/m,\n /^From:\\s/m,\n /^-{3,}\\s*Original Message\\s*-{3,}/m,\n /^-{3,}\\s*Forwarded message\\s*-{3,}/m,\n /^Begin forwarded message:/m,\n]\n\nconst DATE_HEADER_PATTERN = /^Date:\\s*(.+)$/m\n\nfunction stripSignature(text: string): string {\n const lines = text.split('\\n')\n let cutIndex = lines.length\n for (let i = lines.length - 1; i >= 0; i--) {\n const line = lines[i]\n if (SIGNATURE_PATTERNS.some((p) => p.test(line))) {\n cutIndex = i\n break\n }\n }\n return lines.slice(0, cutIndex).join('\\n').trimEnd()\n}\n\nfunction stripQuotedReplies(text: string): string {\n const lines = text.split('\\n')\n const cleanLines: string[] = []\n let inQuote = false\n\n for (const line of lines) {\n if (QUOTE_PATTERNS.some((p) => p.test(line))) {\n inQuote = true\n continue\n }\n if (inQuote && line.startsWith('>')) {\n continue\n }\n if (inQuote && line.trim() === '') {\n continue\n }\n if (inQuote && !line.startsWith('>') && line.trim().length > 0) {\n inQuote = false\n }\n if (!inQuote) {\n cleanLines.push(line)\n }\n }\n\n return cleanLines.join('\\n').trimEnd()\n}\n\nfunction stripHtml(html: string): string {\n return html\n .replace(/<style[^>]*>[\\s\\S]*?<\\/style>/gi, '')\n .replace(/<script[^>]*>[\\s\\S]*?<\\/script>/gi, '')\n .replace(/<br\\s*\\/?>/gi, '\\n')\n .replace(/<\\/p>/gi, '\\n\\n')\n .replace(/<\\/div>/gi, '\\n')\n .replace(/<\\/tr>/gi, '\\n')\n .replace(/<\\/li>/gi, '\\n')\n .replace(/<[^>]+>/g, '')\n .replace(/&nbsp;/gi, ' ')\n .replace(/&amp;/gi, '&')\n .replace(/&lt;/gi, '<')\n .replace(/&gt;/gi, '>')\n .replace(/&quot;/gi, '\"')\n .replace(/&#39;/gi, \"'\")\n .replace(/\\n{3,}/g, '\\n\\n')\n .trim()\n}\n\nfunction normalizeText(text: string): string {\n return text\n .replace(/\\r\\n/g, '\\n')\n .replace(/\\r/g, '\\n')\n .replace(/\\t/g, ' ')\n .replace(/ {2,}/g, ' ')\n .replace(/\\n{3,}/g, '\\n\\n')\n .trim()\n}\n\nfunction generateContentHash(subject: string, from: string, text: string): string {\n const input = `${subject}|${from}|${text.slice(0, 500)}`\n const normalized = input.toLowerCase().replace(/\\s+/g, ' ').trim()\n return createHash('sha256').update(normalized).digest('hex')\n}\n\nfunction parseAddressField(value: string | undefined | null): { name?: string; email: string } {\n if (!value) return { email: '' }\n const match = value.match(/^(.+?)\\s*<([^>]+)>$/)\n if (match) {\n return { name: match[1].trim().replace(/^[\"']|[\"']$/g, ''), email: match[2].trim().toLowerCase() }\n }\n return { email: value.trim().toLowerCase() }\n}\n\nfunction parseAddressListField(value: string | string[] | undefined | null): { name?: string; email: string }[] {\n if (!value) return []\n const list = Array.isArray(value) ? value : value.split(',')\n return list.map((v) => parseAddressField(v.trim())).filter((a) => a.email)\n}\n\nfunction parseDateFromBlock(block: string): string {\n const match = block.match(DATE_HEADER_PATTERN)\n if (match) {\n const parsed = new Date(match[1].trim())\n if (!Number.isNaN(parsed.getTime())) {\n return parsed.toISOString()\n }\n }\n return new Date().toISOString()\n}\n\nfunction parseInlineHeaders(block: string): {\n from: { name?: string; email: string }\n to: { name?: string; email: string }[]\n subject?: string\n bodyStart: number\n} {\n const lines = block.split('\\n')\n let from: { name?: string; email: string } = { email: '' }\n let to: { name?: string; email: string }[] = []\n let subject: string | undefined\n let lastHeaderLine = -1\n\n for (let i = 0; i < lines.length && i < 10; i++) {\n const line = lines[i]\n const fromMatch = line.match(/^From:\\s*(.+)$/i)\n if (fromMatch) {\n from = parseAddressField(fromMatch[1].trim())\n lastHeaderLine = i\n continue\n }\n const toMatch = line.match(/^To:\\s*(.+)$/i)\n if (toMatch) {\n to = parseAddressListField(toMatch[1].trim())\n lastHeaderLine = i\n continue\n }\n const subjectMatch = line.match(/^Subject:\\s*(.+)$/i)\n if (subjectMatch) {\n subject = subjectMatch[1].trim()\n lastHeaderLine = i\n continue\n }\n if (line.match(/^Date:\\s/i) || line.match(/^CC:\\s/i)) {\n lastHeaderLine = i\n continue\n }\n if (lastHeaderLine >= 0 && line.trim() === '') {\n lastHeaderLine = i\n break\n }\n if (lastHeaderLine >= 0) break\n }\n\n return { from, to, subject, bodyStart: lastHeaderLine + 1 }\n}\n\nfunction splitThread(text: string): ThreadMessage[] {\n const separator = /(?:^|\\n)(?:-{3,}\\s*(?:Original Message|Forwarded message)\\s*-{3,}|(?:On .+ wrote:))\\s*\\n/gm\n const parts = text.split(separator).filter((p) => p.trim())\n\n if (parts.length <= 1) {\n return [{\n from: { email: '' },\n to: [],\n date: new Date().toISOString(),\n body: normalizeText(text),\n contentType: 'text',\n isForwarded: false,\n }]\n }\n\n return parts.map((part, index) => {\n if (index === 0) {\n return {\n from: { email: '' } as { name?: string; email: string },\n to: [] as { name?: string; email: string }[],\n date: parseDateFromBlock(part),\n body: normalizeText(part),\n contentType: 'text' as const,\n isForwarded: false,\n }\n }\n\n const headers = parseInlineHeaders(part)\n const bodyLines = part.split('\\n').slice(headers.bodyStart)\n return {\n from: headers.from,\n to: headers.to,\n subject: headers.subject,\n date: parseDateFromBlock(part),\n body: normalizeText(bodyLines.join('\\n')),\n contentType: 'text' as const,\n isForwarded: true,\n }\n })\n}\n\nexport function parseInboundEmail(payload: {\n from?: string\n to?: string | string[]\n subject?: string\n text?: string\n html?: string\n messageId?: string\n replyTo?: string\n inReplyTo?: string\n references?: string | string[]\n}): ParsedEmail {\n const fromParsed = parseAddressField(payload.from)\n const toParsed = parseAddressListField(payload.to)\n const subject = payload.subject?.trim() || '(no subject)'\n\n const rawText = payload.text || null\n const rawHtml = payload.html || null\n\n let textContent = rawText || ''\n if (!textContent && rawHtml) {\n textContent = stripHtml(rawHtml)\n }\n\n const normalized = normalizeText(textContent)\n const withoutSignature = stripSignature(normalized)\n const cleanedText = stripQuotedReplies(withoutSignature)\n\n const threadMessages = splitThread(normalized)\n if (threadMessages.length > 0 && fromParsed.email) {\n threadMessages[0].from = fromParsed\n threadMessages[0].to = toParsed\n threadMessages[0].subject = subject\n }\n\n const contentHash = generateContentHash(\n subject,\n fromParsed.email,\n textContent,\n )\n\n const references = payload.references\n ? (Array.isArray(payload.references) ? payload.references : payload.references.split(/\\s+/))\n : null\n\n return {\n messageId: payload.messageId || null,\n from: fromParsed,\n to: toParsed,\n subject,\n replyTo: payload.replyTo || null,\n inReplyTo: payload.inReplyTo || null,\n references,\n rawText,\n rawHtml,\n cleanedText,\n threadMessages,\n // TODO: Phase 2 \u2014 detect language via LLM or `franc` library\n detectedLanguage: null,\n contentHash,\n }\n}\n\nexport function extractParticipantsFromThread(\n email: InboxEmail,\n): { name: string; email: string; role: string }[] {\n const seen = new Set<string>()\n const participants: { name: string; email: string; role: string }[] = []\n\n // Pre-seed with the inbox's own address so it's excluded from participants\n const inboxAddress = (email.toAddress || '').toLowerCase()\n if (inboxAddress) seen.add(inboxAddress)\n\n const addParticipant = (name: string, addr: string, role: string) => {\n const key = addr.toLowerCase()\n if (!key || seen.has(key)) return\n seen.add(key)\n participants.push({ name, email: key, role })\n }\n\n if (email.threadMessages) {\n for (const msg of email.threadMessages) {\n if (msg.from?.email) {\n addParticipant(msg.from.name || '', msg.from.email, 'other')\n }\n if (msg.to) {\n for (const to of msg.to) {\n addParticipant(to.name || '', to.email, 'other')\n }\n }\n if (msg.cc) {\n for (const cc of msg.cc) {\n addParticipant(cc.name || '', cc.email, 'other')\n }\n }\n }\n }\n\n if (email.forwardedByAddress) {\n addParticipant(email.forwardedByName || '', email.forwardedByAddress, 'seller')\n }\n\n return participants\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { createHash } from 'node:crypto'\nimport sanitizeHtml from 'sanitize-html'\nimport type { InboxEmail, ThreadMessage } from '../data/entities'\n\nexport interface ParsedEmail {\n messageId?: string | null\n from: { name?: string; email: string }\n to: { name?: string; email: string }[]\n subject: string\n replyTo?: string | null\n inReplyTo?: string | null\n references?: string[] | null\n rawText?: string | null\n rawHtml?: string | null\n cleanedText: string\n threadMessages: ThreadMessage[]\n detectedLanguage?: string | null\n contentHash: string\n}\n\nconst SIGNATURE_PATTERNS = [\n /^--\\s*$/m,\n /^Sent from my (iPhone|iPad|Android|Galaxy|Samsung|Pixel)/m,\n /^Get Outlook for/m,\n /^_{10,}/m,\n /^Regards,?\\s*$/m,\n /^Best,?\\s*$/m,\n /^Thanks,?\\s*$/m,\n /^Cheers,?\\s*$/m,\n /^Kind regards,?\\s*$/m,\n /^Best regards,?\\s*$/m,\n]\n\nconst QUOTE_PATTERNS = [\n /^On .+ wrote:\\s*$/m,\n /^>+\\s/m,\n /^From:\\s/m,\n /^-{3,}\\s*Original Message\\s*-{3,}/m,\n /^-{3,}\\s*Forwarded message\\s*-{3,}/m,\n /^Begin forwarded message:/m,\n]\n\nconst DATE_HEADER_PATTERN = /^Date:\\s*(.+)$/m\n\nfunction stripSignature(text: string): string {\n const lines = text.split('\\n')\n let cutIndex = lines.length\n for (let i = lines.length - 1; i >= 0; i--) {\n const line = lines[i]\n if (SIGNATURE_PATTERNS.some((p) => p.test(line))) {\n cutIndex = i\n break\n }\n }\n return lines.slice(0, cutIndex).join('\\n').trimEnd()\n}\n\nfunction stripQuotedReplies(text: string): string {\n const lines = text.split('\\n')\n const cleanLines: string[] = []\n let inQuote = false\n\n for (const line of lines) {\n if (QUOTE_PATTERNS.some((p) => p.test(line))) {\n inQuote = true\n continue\n }\n if (inQuote && line.startsWith('>')) {\n continue\n }\n if (inQuote && line.trim() === '') {\n continue\n }\n if (inQuote && !line.startsWith('>') && line.trim().length > 0) {\n inQuote = false\n }\n if (!inQuote) {\n cleanLines.push(line)\n }\n }\n\n return cleanLines.join('\\n').trimEnd()\n}\n\nfunction stripHtml(html: string): string {\n const sanitized = sanitizeHtml(html, {\n allowedTags: [],\n allowedAttributes: {},\n })\n\n return sanitized\n .replace(/\\r\\n/g, '\\n')\n .replace(/\\r/g, '\\n')\n .replace(/\\n{3,}/g, '\\n\\n')\n .trim()\n}\n\nfunction normalizeText(text: string): string {\n return text\n .replace(/\\r\\n/g, '\\n')\n .replace(/\\r/g, '\\n')\n .replace(/\\t/g, ' ')\n .replace(/ {2,}/g, ' ')\n .replace(/\\n{3,}/g, '\\n\\n')\n .trim()\n}\n\nfunction generateContentHash(subject: string, from: string, text: string): string {\n const input = `${subject}|${from}|${text.slice(0, 500)}`\n const normalized = input.toLowerCase().replace(/\\s+/g, ' ').trim()\n return createHash('sha256').update(normalized).digest('hex')\n}\n\nfunction parseAddressField(value: string | undefined | null): { name?: string; email: string } {\n if (!value) return { email: '' }\n const match = value.match(/^(.+?)\\s*<([^>]+)>$/)\n if (match) {\n return { name: match[1].trim().replace(/^[\"']|[\"']$/g, ''), email: match[2].trim().toLowerCase() }\n }\n return { email: value.trim().toLowerCase() }\n}\n\nfunction parseAddressListField(value: string | string[] | undefined | null): { name?: string; email: string }[] {\n if (!value) return []\n const list = Array.isArray(value) ? value : value.split(',')\n return list.map((v) => parseAddressField(v.trim())).filter((a) => a.email)\n}\n\nfunction parseDateFromBlock(block: string): string {\n const match = block.match(DATE_HEADER_PATTERN)\n if (match) {\n const parsed = new Date(match[1].trim())\n if (!Number.isNaN(parsed.getTime())) {\n return parsed.toISOString()\n }\n }\n return new Date().toISOString()\n}\n\nfunction parseInlineHeaders(block: string): {\n from: { name?: string; email: string }\n to: { name?: string; email: string }[]\n subject?: string\n bodyStart: number\n} {\n const lines = block.split('\\n')\n let from: { name?: string; email: string } = { email: '' }\n let to: { name?: string; email: string }[] = []\n let subject: string | undefined\n let lastHeaderLine = -1\n\n for (let i = 0; i < lines.length && i < 10; i++) {\n const line = lines[i]\n const fromMatch = line.match(/^From:\\s*(.+)$/i)\n if (fromMatch) {\n from = parseAddressField(fromMatch[1].trim())\n lastHeaderLine = i\n continue\n }\n const toMatch = line.match(/^To:\\s*(.+)$/i)\n if (toMatch) {\n to = parseAddressListField(toMatch[1].trim())\n lastHeaderLine = i\n continue\n }\n const subjectMatch = line.match(/^Subject:\\s*(.+)$/i)\n if (subjectMatch) {\n subject = subjectMatch[1].trim()\n lastHeaderLine = i\n continue\n }\n if (line.match(/^Date:\\s/i) || line.match(/^CC:\\s/i)) {\n lastHeaderLine = i\n continue\n }\n if (lastHeaderLine >= 0 && line.trim() === '') {\n lastHeaderLine = i\n break\n }\n if (lastHeaderLine >= 0) break\n }\n\n return { from, to, subject, bodyStart: lastHeaderLine + 1 }\n}\n\nfunction splitThread(text: string): ThreadMessage[] {\n const separator = /(?:^|\\n)(?:-{3,}\\s*(?:Original Message|Forwarded message)\\s*-{3,}|(?:On .+ wrote:))\\s*\\n/gm\n const parts = text.split(separator).filter((p) => p.trim())\n\n if (parts.length <= 1) {\n return [{\n from: { email: '' },\n to: [],\n date: new Date().toISOString(),\n body: normalizeText(text),\n contentType: 'text',\n isForwarded: false,\n }]\n }\n\n return parts.map((part, index) => {\n if (index === 0) {\n return {\n from: { email: '' } as { name?: string; email: string },\n to: [] as { name?: string; email: string }[],\n date: parseDateFromBlock(part),\n body: normalizeText(part),\n contentType: 'text' as const,\n isForwarded: false,\n }\n }\n\n const headers = parseInlineHeaders(part)\n const bodyLines = part.split('\\n').slice(headers.bodyStart)\n return {\n from: headers.from,\n to: headers.to,\n subject: headers.subject,\n date: parseDateFromBlock(part),\n body: normalizeText(bodyLines.join('\\n')),\n contentType: 'text' as const,\n isForwarded: true,\n }\n })\n}\n\nexport function parseInboundEmail(payload: {\n from?: string\n to?: string | string[]\n subject?: string\n text?: string\n html?: string\n messageId?: string\n replyTo?: string\n inReplyTo?: string\n references?: string | string[]\n}): ParsedEmail {\n const fromParsed = parseAddressField(payload.from)\n const toParsed = parseAddressListField(payload.to)\n const subject = payload.subject?.trim() || '(no subject)'\n\n const rawText = payload.text || null\n const rawHtml = payload.html || null\n\n let textContent = rawText || ''\n if (!textContent && rawHtml) {\n textContent = stripHtml(rawHtml)\n }\n\n const normalized = normalizeText(textContent)\n const withoutSignature = stripSignature(normalized)\n const cleanedText = stripQuotedReplies(withoutSignature)\n\n const threadMessages = splitThread(normalized)\n if (threadMessages.length > 0 && fromParsed.email) {\n threadMessages[0].from = fromParsed\n threadMessages[0].to = toParsed\n threadMessages[0].subject = subject\n }\n\n const contentHash = generateContentHash(\n subject,\n fromParsed.email,\n textContent,\n )\n\n const references = payload.references\n ? (Array.isArray(payload.references) ? payload.references : payload.references.split(/\\s+/))\n : null\n\n return {\n messageId: payload.messageId || null,\n from: fromParsed,\n to: toParsed,\n subject,\n replyTo: payload.replyTo || null,\n inReplyTo: payload.inReplyTo || null,\n references,\n rawText,\n rawHtml,\n cleanedText,\n threadMessages,\n // TODO: Phase 2 \u2014 detect language via LLM or `franc` library\n detectedLanguage: null,\n contentHash,\n }\n}\n\nexport function extractParticipantsFromThread(\n email: InboxEmail,\n): { name: string; email: string; role: string }[] {\n const seen = new Set<string>()\n const participants: { name: string; email: string; role: string }[] = []\n\n // Pre-seed with the inbox's own address so it's excluded from participants\n const inboxAddress = (email.toAddress || '').toLowerCase()\n if (inboxAddress) seen.add(inboxAddress)\n\n const addParticipant = (name: string, addr: string, role: string) => {\n const key = addr.toLowerCase()\n if (!key || seen.has(key)) return\n seen.add(key)\n participants.push({ name, email: key, role })\n }\n\n if (email.threadMessages) {\n for (const msg of email.threadMessages) {\n if (msg.from?.email) {\n addParticipant(msg.from.name || '', msg.from.email, 'other')\n }\n if (msg.to) {\n for (const to of msg.to) {\n addParticipant(to.name || '', to.email, 'other')\n }\n }\n if (msg.cc) {\n for (const cc of msg.cc) {\n addParticipant(cc.name || '', cc.email, 'other')\n }\n }\n }\n }\n\n if (email.forwardedByAddress) {\n addParticipant(email.forwardedByName || '', email.forwardedByAddress, 'seller')\n }\n\n return participants\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.4.5-develop-974adb54b3",
3
+ "version": "0.4.5-develop-3f7d1d7925",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -207,12 +207,13 @@
207
207
  }
208
208
  },
209
209
  "dependencies": {
210
- "@open-mercato/shared": "0.4.5-develop-974adb54b3",
210
+ "@open-mercato/shared": "0.4.5-develop-3f7d1d7925",
211
211
  "@types/semver": "^7.5.8",
212
212
  "@xyflow/react": "^12.6.0",
213
213
  "ai": "^6.0.0",
214
214
  "date-fns": "^4.1.0",
215
215
  "date-fns-tz": "^3.2.0",
216
+ "sanitize-html": "^2.17.1",
216
217
  "semver": "^7.6.3"
217
218
  },
218
219
  "devDependencies": {
@@ -1,4 +1,5 @@
1
1
  import { createHash } from 'node:crypto'
2
+ import sanitizeHtml from 'sanitize-html'
2
3
  import type { InboxEmail, ThreadMessage } from '../data/entities'
3
4
 
4
5
  export interface ParsedEmail {
@@ -82,21 +83,14 @@ function stripQuotedReplies(text: string): string {
82
83
  }
83
84
 
84
85
  function stripHtml(html: string): string {
85
- return html
86
- .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
87
- .replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
88
- .replace(/<br\s*\/?>/gi, '\n')
89
- .replace(/<\/p>/gi, '\n\n')
90
- .replace(/<\/div>/gi, '\n')
91
- .replace(/<\/tr>/gi, '\n')
92
- .replace(/<\/li>/gi, '\n')
93
- .replace(/<[^>]+>/g, '')
94
- .replace(/&nbsp;/gi, ' ')
95
- .replace(/&amp;/gi, '&')
96
- .replace(/&lt;/gi, '<')
97
- .replace(/&gt;/gi, '>')
98
- .replace(/&quot;/gi, '"')
99
- .replace(/&#39;/gi, "'")
86
+ const sanitized = sanitizeHtml(html, {
87
+ allowedTags: [],
88
+ allowedAttributes: {},
89
+ })
90
+
91
+ return sanitized
92
+ .replace(/\r\n/g, '\n')
93
+ .replace(/\r/g, '\n')
100
94
  .replace(/\n{3,}/g, '\n\n')
101
95
  .trim()
102
96
  }