style(tracking-questions): normalize loose markdown in answer content
Repair detached headings, missing list/heading spaces, and pipe-only tables in answer text before rendering, and style the markdown preview so headings, lists, tables, and emphasis match the rest of the detail view. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -740,6 +740,172 @@ function sanitizeAnswerBody(answerText: string | null | undefined): string | nul
|
||||
return sanitized || null
|
||||
}
|
||||
|
||||
function normalizeLooseMarkdownLine(line: string): string {
|
||||
const headingNormalizedLine = line
|
||||
.replace(/^[\u200b-\u200f\ufeff]+/, '')
|
||||
.replace(/^([ \t]{0,12})(?:\\#|#|#|#)/i, '$1#')
|
||||
const answerScopedHeading = headingNormalizedLine.replace(
|
||||
/^([ \t]{0,12})#{1,2}(?!#)\s*(?=\S)/,
|
||||
'$1### ',
|
||||
)
|
||||
|
||||
return answerScopedHeading
|
||||
.replace(/^([ \t]{0,12})(#{1,6})(?=\S)/, '$1$2 ')
|
||||
.replace(/^([ \t]{0,12}#{1,6}\s+)#{1,6}\s*/, '$1')
|
||||
.replace(/^([ \t]{0,12}#{1,6}\s+\d+[.)、])(?=\S)/, '$1 ')
|
||||
.replace(/^(\s*)([-*+])(?=\S)/, '$1$2 ')
|
||||
.replace(/^(\s*)(\d{1,3})([.)])(?=\S)/, '$1$2$3 ')
|
||||
}
|
||||
|
||||
function countMarkdownTablePipes(line: string): number {
|
||||
return (line.match(/\|/g) ?? []).length
|
||||
}
|
||||
|
||||
function splitMarkdownTableCells(line: string): string[] {
|
||||
return line
|
||||
.trim()
|
||||
.replace(/^\|/, '')
|
||||
.replace(/\|$/, '')
|
||||
.split('|')
|
||||
.map((cell) => cell.trim())
|
||||
}
|
||||
|
||||
function isMarkdownTableSeparatorLine(line: string): boolean {
|
||||
const normalized = line.trim()
|
||||
if (!normalized.includes('|')) {
|
||||
return false
|
||||
}
|
||||
|
||||
const cells = splitMarkdownTableCells(normalized)
|
||||
return cells.length >= 2 && cells.every((cell) => /^:?-{3,}:?$/.test(cell))
|
||||
}
|
||||
|
||||
function countMarkdownTableCells(line: string): number {
|
||||
return splitMarkdownTableCells(line).length
|
||||
}
|
||||
|
||||
function normalizeMarkdownTableLine(line: string, targetCellCount?: number): string {
|
||||
let normalized = line.trim()
|
||||
if (!normalized.startsWith('|')) {
|
||||
normalized = `| ${normalized}`
|
||||
}
|
||||
if (!normalized.endsWith('|')) {
|
||||
normalized = `${normalized} |`
|
||||
}
|
||||
|
||||
if (targetCellCount && targetCellCount > 0) {
|
||||
const cells = splitMarkdownTableCells(normalized)
|
||||
if (isMarkdownTableSeparatorLine(normalized)) {
|
||||
while (cells.length < targetCellCount) {
|
||||
cells.push('--------')
|
||||
}
|
||||
} else {
|
||||
while (cells.length < targetCellCount) {
|
||||
cells.push('')
|
||||
}
|
||||
}
|
||||
if (cells.length > targetCellCount) {
|
||||
cells.length = targetCellCount
|
||||
}
|
||||
normalized = `| ${cells.join(' | ')} |`
|
||||
}
|
||||
|
||||
return normalized.replace(/\s*\|\s*/g, ' | ').replace(/\s{2,}/g, ' ').trim()
|
||||
}
|
||||
|
||||
function normalizeLooseMarkdownTables(content: string): string {
|
||||
const detachedTableBlocks = content
|
||||
.replace(/\|\|(?=\s*:?-{3,}:?\s*\|)/g, '|\n|')
|
||||
.replace(
|
||||
/(^|\n)(\s{0,3}#{1,6}\s+[^|\n]{1,80})\|(?=[^|\n]+\|[^\n]*\n\s*\|?\s*:?-{3,}:?\s*\|)/g,
|
||||
'$1$2\n|',
|
||||
)
|
||||
|
||||
const lines = detachedTableBlocks.split('\n').flatMap((line: string) => {
|
||||
const separatorWithFirstRow = line
|
||||
.trim()
|
||||
.match(/^((?:\|\s*:?-{3,}:?\s*){2,}\|?)(\S.*)$/)
|
||||
if (
|
||||
!separatorWithFirstRow?.[1] ||
|
||||
!separatorWithFirstRow[2] ||
|
||||
/^\|*$/.test(separatorWithFirstRow[2].trim())
|
||||
) {
|
||||
return [line]
|
||||
}
|
||||
return [separatorWithFirstRow[1], separatorWithFirstRow[2]]
|
||||
})
|
||||
const normalizedLines: string[] = []
|
||||
let inTable = false
|
||||
let tableCellCount = 0
|
||||
|
||||
for (const line of lines) {
|
||||
const previousIndex = normalizedLines.length - 1
|
||||
if (isMarkdownTableSeparatorLine(line)) {
|
||||
tableCellCount =
|
||||
previousIndex >= 0
|
||||
? Math.max(
|
||||
countMarkdownTableCells(normalizedLines[previousIndex] ?? ''),
|
||||
countMarkdownTableCells(line),
|
||||
)
|
||||
: countMarkdownTableCells(line)
|
||||
if (
|
||||
previousIndex >= 0 &&
|
||||
countMarkdownTablePipes(normalizedLines[previousIndex] ?? '') >= 2
|
||||
) {
|
||||
normalizedLines[previousIndex] = normalizeMarkdownTableLine(
|
||||
normalizedLines[previousIndex] ?? '',
|
||||
tableCellCount,
|
||||
)
|
||||
}
|
||||
normalizedLines.push(normalizeMarkdownTableLine(line, tableCellCount))
|
||||
inTable = true
|
||||
continue
|
||||
}
|
||||
|
||||
if (inTable && countMarkdownTablePipes(line) >= 2) {
|
||||
normalizedLines.push(normalizeMarkdownTableLine(line, tableCellCount))
|
||||
continue
|
||||
}
|
||||
|
||||
inTable = false
|
||||
tableCellCount = 0
|
||||
normalizedLines.push(line)
|
||||
}
|
||||
|
||||
return normalizedLines.join('\n')
|
||||
}
|
||||
|
||||
function normalizeAnswerMarkdown(content: string): string {
|
||||
const headingMarkerNormalized = content
|
||||
.replace(/(^|\n)[\u200b-\u200f\ufeff]*([ \t]{0,12})(?:\\#|#|#|#)/gi, '$1$2#')
|
||||
.replace(/([^\n])(?=[\u200b-\u200f\ufeff]*[ \t]{0,12}(?:\\#|#|#|#|#){1,6}\s*(?:\d{1,3}[.)、]|[一二三四五六七八九十]+[、..]|第[一二三四五六七八九十0-9]+))/gi, '$1\n\n')
|
||||
|
||||
const detachedHeadings = normalizeLooseMarkdownTables(headingMarkerNormalized)
|
||||
.replace(
|
||||
/([^\n])(?=[\u200b-\u200f\ufeff]*[ \t]{0,12}#{1,6}\s*(?:\d{1,3}[.)、]|[一二三四五六七八九十]+[、..]|第[一二三四五六七八九十0-9]+))/g,
|
||||
'$1\n\n',
|
||||
)
|
||||
.replace(/([^\n])(?=#{2,6}\s*\S)/g, '$1\n\n')
|
||||
const lines = detachedHeadings.split('\n')
|
||||
let inFence = false
|
||||
|
||||
const normalizedLines = lines.map((line) => {
|
||||
if (/^\s*(?:```|~~~)/.test(line)) {
|
||||
inFence = !inFence
|
||||
return line
|
||||
}
|
||||
if (inFence) {
|
||||
return line
|
||||
}
|
||||
return normalizeLooseMarkdownLine(line)
|
||||
})
|
||||
|
||||
return normalizedLines
|
||||
.join('\n')
|
||||
.replace(/\n{3,}/g, '\n\n')
|
||||
.trim()
|
||||
}
|
||||
|
||||
function buildInlineCitationMarker(
|
||||
sourceGroupIndex: number,
|
||||
platform: MonitoringQuestionDetailPlatform | null,
|
||||
@@ -854,12 +1020,14 @@ function formatAnswerContent(platform: MonitoringQuestionDetailPlatform | null):
|
||||
})
|
||||
: sanitized
|
||||
|
||||
return withInlineMarkers
|
||||
const normalizedMarkdown = withInlineMarkers
|
||||
.replace(createQwenSourceGroupStripPattern(), '')
|
||||
.replace(createYuanbaoCitationStripPattern(), '')
|
||||
.replace(/[ \t]{2,}/g, ' ')
|
||||
.replace(/\n{3,}/g, '\n\n')
|
||||
.trim()
|
||||
|
||||
return normalizeAnswerMarkdown(normalizedMarkdown)
|
||||
}
|
||||
|
||||
function collectDeepSeekInlineCitationOccurrences(
|
||||
@@ -1721,6 +1889,76 @@ const contentCitationColumns = computed(() => [
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.tracking-question-answer__content :deep(.markdown-preview) {
|
||||
color: #111827;
|
||||
font-size: 15px;
|
||||
line-height: 1.85;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.tracking-question-answer__content :deep(.markdown-preview h1),
|
||||
.tracking-question-answer__content :deep(.markdown-preview h2),
|
||||
.tracking-question-answer__content :deep(.markdown-preview h3),
|
||||
.tracking-question-answer__content :deep(.markdown-preview h4),
|
||||
.tracking-question-answer__content :deep(.markdown-preview h5),
|
||||
.tracking-question-answer__content :deep(.markdown-preview h6) {
|
||||
margin: 20px 0 10px;
|
||||
color: #0f172a;
|
||||
line-height: 1.35;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.tracking-question-answer__content :deep(.markdown-preview h1) {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.tracking-question-answer__content :deep(.markdown-preview h2) {
|
||||
font-size: 21px;
|
||||
}
|
||||
|
||||
.tracking-question-answer__content :deep(.markdown-preview h3) {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.tracking-question-answer__content :deep(.markdown-preview h4),
|
||||
.tracking-question-answer__content :deep(.markdown-preview h5),
|
||||
.tracking-question-answer__content :deep(.markdown-preview h6) {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.tracking-question-answer__content :deep(.markdown-preview p),
|
||||
.tracking-question-answer__content :deep(.markdown-preview ul),
|
||||
.tracking-question-answer__content :deep(.markdown-preview ol),
|
||||
.tracking-question-answer__content :deep(.markdown-preview blockquote),
|
||||
.tracking-question-answer__content :deep(.markdown-preview table) {
|
||||
margin: 0 0 14px;
|
||||
}
|
||||
|
||||
.tracking-question-answer__content :deep(.markdown-preview ul),
|
||||
.tracking-question-answer__content :deep(.markdown-preview ol) {
|
||||
padding-left: 22px;
|
||||
}
|
||||
|
||||
.tracking-question-answer__content :deep(.markdown-preview li + li) {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.tracking-question-answer__content :deep(.markdown-preview strong) {
|
||||
color: #0f172a;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.tracking-question-answer__content :deep(.markdown-preview hr) {
|
||||
margin: 22px 0;
|
||||
}
|
||||
|
||||
.tracking-question-answer__content :deep(.markdown-preview table) {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user