no message
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
const escapeHtml = (value: string) =>
|
||||
value
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
|
||||
const escapeAttribute = (value: string) => escapeHtml(value)
|
||||
|
||||
const parseLink = (value: string) => {
|
||||
return value.replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, (_match, text: string, url: string) => {
|
||||
const safeUrl = escapeAttribute(url)
|
||||
return `<a href="${safeUrl}">${text}</a>`
|
||||
})
|
||||
}
|
||||
|
||||
const parseInlineMarkdown = (value: string) => {
|
||||
let html = escapeHtml(value)
|
||||
html = parseLink(html)
|
||||
html = html.replace(/`([^`]+)`/g, '<code>$1</code>')
|
||||
html = html.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
|
||||
html = html.replace(/(^|[^\*])\*([^\*\n]+)\*(?!\*)/g, '$1<em>$2</em>')
|
||||
return html
|
||||
}
|
||||
|
||||
export const renderMarkdownToHtml = (value: string) => {
|
||||
const source = String(value || '').replace(/\r\n?/g, '\n').trim()
|
||||
if (!source) return ''
|
||||
|
||||
const lines = source.split('\n')
|
||||
const htmlParts: string[] = []
|
||||
const paragraphLines: string[] = []
|
||||
const codeLines: string[] = []
|
||||
const quoteLines: string[] = []
|
||||
const tableLines: string[] = []
|
||||
let listType: 'ul' | 'ol' | '' = ''
|
||||
let inCodeBlock = false
|
||||
|
||||
const flushParagraph = () => {
|
||||
if (!paragraphLines.length) return
|
||||
htmlParts.push(`<p>${paragraphLines.map((line) => parseInlineMarkdown(line)).join('<br />')}</p>`)
|
||||
paragraphLines.length = 0
|
||||
}
|
||||
|
||||
const closeList = () => {
|
||||
if (!listType) return
|
||||
htmlParts.push(`</${listType}>`)
|
||||
listType = ''
|
||||
}
|
||||
|
||||
const flushQuote = () => {
|
||||
if (!quoteLines.length) return
|
||||
htmlParts.push(
|
||||
`<blockquote>${quoteLines.map((line) => parseInlineMarkdown(line)).join('<br />')}</blockquote>`
|
||||
)
|
||||
quoteLines.length = 0
|
||||
}
|
||||
|
||||
const flushCodeBlock = () => {
|
||||
if (!codeLines.length) return
|
||||
htmlParts.push(`<pre><code>${escapeHtml(codeLines.join('\n'))}</code></pre>`)
|
||||
codeLines.length = 0
|
||||
}
|
||||
|
||||
const parseTableCells = (line: string) => {
|
||||
const cells = line
|
||||
.trim()
|
||||
.replace(/^\|/, '')
|
||||
.replace(/\|$/, '')
|
||||
.split('|')
|
||||
.map((cell) => cell.trim())
|
||||
return cells.filter((_cell, index) => index < cells.length)
|
||||
}
|
||||
|
||||
const isTableRow = (line: string) => /^\|.*\|$/.test(line) && parseTableCells(line).length >= 2
|
||||
|
||||
const isTableSeparatorLine = (line: string) => {
|
||||
if (!isTableRow(line)) return false
|
||||
return parseTableCells(line).every((cell) => /^:?-{3,}:?$/.test(cell))
|
||||
}
|
||||
|
||||
const flushTable = () => {
|
||||
if (!tableLines.length) return
|
||||
|
||||
if (tableLines.length >= 2 && isTableSeparatorLine(tableLines[1])) {
|
||||
const headers = parseTableCells(tableLines[0])
|
||||
const bodyRows = tableLines.slice(2).map(parseTableCells).filter((cells) => cells.length > 0)
|
||||
const thead = `<thead><tr>${headers.map((cell) => `<th>${parseInlineMarkdown(cell)}</th>`).join('')}</tr></thead>`
|
||||
const tbody = bodyRows.length
|
||||
? `<tbody>${bodyRows
|
||||
.map((cells) => `<tr>${cells.map((cell) => `<td>${parseInlineMarkdown(cell)}</td>`).join('')}</tr>`)
|
||||
.join('')}</tbody>`
|
||||
: ''
|
||||
htmlParts.push(`<table>${thead}${tbody}</table>`)
|
||||
} else {
|
||||
htmlParts.push(`<p>${tableLines.map((line) => parseInlineMarkdown(line)).join('<br />')}</p>`)
|
||||
}
|
||||
|
||||
tableLines.length = 0
|
||||
}
|
||||
|
||||
for (const rawLine of lines) {
|
||||
const line = rawLine.trim()
|
||||
|
||||
if (/^```/.test(line)) {
|
||||
flushParagraph()
|
||||
closeList()
|
||||
flushQuote()
|
||||
flushTable()
|
||||
if (inCodeBlock) {
|
||||
flushCodeBlock()
|
||||
}
|
||||
inCodeBlock = !inCodeBlock
|
||||
continue
|
||||
}
|
||||
|
||||
if (inCodeBlock) {
|
||||
codeLines.push(rawLine)
|
||||
continue
|
||||
}
|
||||
|
||||
if (!line) {
|
||||
flushParagraph()
|
||||
closeList()
|
||||
flushQuote()
|
||||
flushTable()
|
||||
continue
|
||||
}
|
||||
|
||||
const headingMatch = line.match(/^(#{1,6})\s+(.*)$/)
|
||||
if (headingMatch) {
|
||||
flushParagraph()
|
||||
closeList()
|
||||
flushQuote()
|
||||
flushTable()
|
||||
const level = Math.min(6, headingMatch[1].length)
|
||||
htmlParts.push(`<h${level}>${parseInlineMarkdown(headingMatch[2])}</h${level}>`)
|
||||
continue
|
||||
}
|
||||
|
||||
const quoteMatch = line.match(/^>\s?(.*)$/)
|
||||
if (quoteMatch) {
|
||||
flushParagraph()
|
||||
closeList()
|
||||
flushTable()
|
||||
quoteLines.push(quoteMatch[1])
|
||||
continue
|
||||
}
|
||||
flushQuote()
|
||||
|
||||
if (/^(-{3,}|\*{3,}|_{3,})$/.test(line)) {
|
||||
flushParagraph()
|
||||
closeList()
|
||||
flushTable()
|
||||
htmlParts.push('<hr />')
|
||||
continue
|
||||
}
|
||||
|
||||
const unorderedMatch = line.match(/^[-*+]\s+(.*)$/)
|
||||
if (unorderedMatch) {
|
||||
flushParagraph()
|
||||
flushTable()
|
||||
if (listType !== 'ul') {
|
||||
closeList()
|
||||
listType = 'ul'
|
||||
htmlParts.push('<ul>')
|
||||
}
|
||||
htmlParts.push(`<li>${parseInlineMarkdown(unorderedMatch[1])}</li>`)
|
||||
continue
|
||||
}
|
||||
|
||||
const orderedMatch = line.match(/^\d+\.\s+(.*)$/)
|
||||
if (orderedMatch) {
|
||||
flushParagraph()
|
||||
flushTable()
|
||||
if (listType !== 'ol') {
|
||||
closeList()
|
||||
listType = 'ol'
|
||||
htmlParts.push('<ol>')
|
||||
}
|
||||
htmlParts.push(`<li>${parseInlineMarkdown(orderedMatch[1])}</li>`)
|
||||
continue
|
||||
}
|
||||
|
||||
if (isTableRow(line)) {
|
||||
flushParagraph()
|
||||
closeList()
|
||||
flushQuote()
|
||||
tableLines.push(line)
|
||||
continue
|
||||
}
|
||||
|
||||
flushTable()
|
||||
closeList()
|
||||
paragraphLines.push(line)
|
||||
}
|
||||
|
||||
flushParagraph()
|
||||
closeList()
|
||||
flushQuote()
|
||||
flushTable()
|
||||
if (inCodeBlock) {
|
||||
flushCodeBlock()
|
||||
}
|
||||
|
||||
return htmlParts.join('')
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
type MatchLike = {
|
||||
competition_id?: number | string
|
||||
league_name?: string
|
||||
}
|
||||
|
||||
const WORLD_CUP_COMPETITION_ID = 61
|
||||
const WORLD_CUP_SOURCE_UTC_OFFSET_SECONDS = 8 * 60 * 60
|
||||
|
||||
export const isWorldCupMatch = (match?: MatchLike | null): boolean => {
|
||||
if (!match) return false
|
||||
return Number(match.competition_id || 0) === WORLD_CUP_COMPETITION_ID
|
||||
|| String(match.league_name || '') === '世界杯'
|
||||
}
|
||||
|
||||
export const resolveMatchTimestamp = (timestamp: number | string | null | undefined, match?: MatchLike | null): number => {
|
||||
const seconds = Number(timestamp || 0)
|
||||
if (!Number.isFinite(seconds) || seconds <= 0) return 0
|
||||
|
||||
// World Cup source fields are UTC clock values saved as server-local Unix seconds.
|
||||
if (isWorldCupMatch(match)) return seconds + WORLD_CUP_SOURCE_UTC_OFFSET_SECONDS
|
||||
|
||||
return seconds
|
||||
}
|
||||
|
||||
export const getLocalMatchDate = (timestamp: number | string | null | undefined, match?: MatchLike | null): Date | null => {
|
||||
const seconds = resolveMatchTimestamp(timestamp, match)
|
||||
if (!seconds) return null
|
||||
return new Date(seconds * 1000)
|
||||
}
|
||||
Reference in New Issue
Block a user