Allow heading tags (h1-h6) in ContentSanitizer so news editor headings render
This commit is contained in:
93
resources/js/utils/contentValidation.js
Normal file
93
resources/js/utils/contentValidation.js
Normal file
@@ -0,0 +1,93 @@
|
||||
import { countEmoji, FLOOD_DENSITY_THRESHOLD, FLOOD_COUNT_THRESHOLD } from './emojiFlood'
|
||||
|
||||
const HTML_TAG_RE = /<[a-z][^>]*>/i
|
||||
const MAX_CONTENT_LENGTH = 10000
|
||||
|
||||
function decodeHtmlEntities(value) {
|
||||
const decoded = String(value || '')
|
||||
|
||||
if (typeof document === 'undefined') {
|
||||
return decoded
|
||||
.replace(/ /gi, ' ')
|
||||
.replace(/&/gi, '&')
|
||||
.replace(/</gi, '<')
|
||||
.replace(/>/gi, '>')
|
||||
.replace(/"/gi, '"')
|
||||
.replace(/'/gi, "'")
|
||||
}
|
||||
|
||||
const textarea = document.createElement('textarea')
|
||||
textarea.innerHTML = decoded
|
||||
return textarea.value
|
||||
}
|
||||
|
||||
function stripResidualTags(value) {
|
||||
return String(value || '').replace(/<[^>]+>/g, '')
|
||||
}
|
||||
|
||||
export function normalizeMarkdownLiteContent(value) {
|
||||
const raw = String(value || '')
|
||||
const trimmed = raw.trim()
|
||||
|
||||
if (!trimmed || !HTML_TAG_RE.test(trimmed)) {
|
||||
return raw
|
||||
}
|
||||
|
||||
const normalized = raw
|
||||
.replace(/<\s*a[^>]*href=(['"])(.*?)\1[^>]*>([\s\S]*?)<\s*\/a\s*>/gi, (_, __, href, label) => {
|
||||
const text = stripResidualTags(label).trim() || href
|
||||
return `[${text}](${href})`
|
||||
})
|
||||
.replace(/<\s*(strong|b)(?:\s+[^>]*)?>([\s\S]*?)<\s*\/\s*\1\s*>/gi, (_, __, text) => `**${stripResidualTags(text)}**`)
|
||||
.replace(/<\s*(em|i)(?:\s+[^>]*)?>([\s\S]*?)<\s*\/\s*\1\s*>/gi, (_, __, text) => `*${stripResidualTags(text)}*`)
|
||||
.replace(/<\s*code(?:\s+[^>]*)?>([\s\S]*?)<\s*\/code\s*>/gi, (_, text) => `\`${stripResidualTags(text)}\``)
|
||||
.replace(/<\s*br\s*\/?>/gi, '\n')
|
||||
.replace(/<\s*\/p\s*>/gi, '\n\n')
|
||||
.replace(/<\s*p(?:\s+[^>]*)?>/gi, '')
|
||||
.replace(/<\s*li(?:\s+[^>]*)?>([\s\S]*?)<\s*\/li\s*>/gi, (_, text) => `- ${stripResidualTags(text).trim()}\n`)
|
||||
.replace(/<\s*\/ul\s*>|<\s*\/ol\s*>/gi, '\n')
|
||||
.replace(/<\s*(ul|ol)(?:\s+[^>]*)?>/gi, '')
|
||||
.replace(/<\s*blockquote(?:\s+[^>]*)?>([\s\S]*?)<\s*\/blockquote\s*>/gi, (_, text) => {
|
||||
const lines = stripResidualTags(text)
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)
|
||||
.map((line) => `> ${line}`)
|
||||
return `${lines.join('\n')}\n\n`
|
||||
})
|
||||
.replace(/<[^>]+>/g, '')
|
||||
|
||||
return decodeHtmlEntities(normalized)
|
||||
.replace(/\r\n?/g, '\n')
|
||||
.replace(/[\t ]+\n/g, '\n')
|
||||
.replace(/\n{3,}/g, '\n\n')
|
||||
.trim()
|
||||
}
|
||||
|
||||
export function validateMarkdownLiteContent(value) {
|
||||
const raw = String(value || '')
|
||||
const trimmed = raw.trim()
|
||||
|
||||
if (!trimmed) return []
|
||||
|
||||
const errors = []
|
||||
|
||||
if (trimmed.length > MAX_CONTENT_LENGTH) {
|
||||
errors.push('Content exceeds maximum length of 10,000 characters.')
|
||||
}
|
||||
|
||||
if (HTML_TAG_RE.test(trimmed)) {
|
||||
errors.push('HTML tags are not allowed. Use Markdown formatting instead.')
|
||||
}
|
||||
|
||||
const emojiCount = countEmoji(trimmed)
|
||||
if (emojiCount > FLOOD_COUNT_THRESHOLD) {
|
||||
errors.push('Too many emoji. Please limit emoji usage.')
|
||||
}
|
||||
|
||||
if (emojiCount > 5 && trimmed.length > 0 && (emojiCount / trimmed.length) > FLOOD_DENSITY_THRESHOLD) {
|
||||
errors.push('Content is mostly emoji. Please add some text.')
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
33
resources/js/utils/enhanceFormatting.js
Normal file
33
resources/js/utils/enhanceFormatting.js
Normal file
@@ -0,0 +1,33 @@
|
||||
const dateFormatter = new Intl.DateTimeFormat('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false,
|
||||
timeZone: 'UTC',
|
||||
})
|
||||
|
||||
const numberFormatter = new Intl.NumberFormat('en-US')
|
||||
|
||||
export function formatEnhanceDate(value) {
|
||||
if (!value) return '—'
|
||||
|
||||
const parsed = new Date(value)
|
||||
|
||||
if (Number.isNaN(parsed.getTime())) {
|
||||
return '—'
|
||||
}
|
||||
|
||||
return `${dateFormatter.format(parsed)} UTC`
|
||||
}
|
||||
|
||||
export function formatEnhanceInteger(value) {
|
||||
const parsed = Number(value)
|
||||
|
||||
if (!Number.isFinite(parsed)) {
|
||||
return '0'
|
||||
}
|
||||
|
||||
return numberFormatter.format(parsed)
|
||||
}
|
||||
Reference in New Issue
Block a user