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
|
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(
|
function buildInlineCitationMarker(
|
||||||
sourceGroupIndex: number,
|
sourceGroupIndex: number,
|
||||||
platform: MonitoringQuestionDetailPlatform | null,
|
platform: MonitoringQuestionDetailPlatform | null,
|
||||||
@@ -854,12 +1020,14 @@ function formatAnswerContent(platform: MonitoringQuestionDetailPlatform | null):
|
|||||||
})
|
})
|
||||||
: sanitized
|
: sanitized
|
||||||
|
|
||||||
return withInlineMarkers
|
const normalizedMarkdown = withInlineMarkers
|
||||||
.replace(createQwenSourceGroupStripPattern(), '')
|
.replace(createQwenSourceGroupStripPattern(), '')
|
||||||
.replace(createYuanbaoCitationStripPattern(), '')
|
.replace(createYuanbaoCitationStripPattern(), '')
|
||||||
.replace(/[ \t]{2,}/g, ' ')
|
.replace(/[ \t]{2,}/g, ' ')
|
||||||
.replace(/\n{3,}/g, '\n\n')
|
.replace(/\n{3,}/g, '\n\n')
|
||||||
.trim()
|
.trim()
|
||||||
|
|
||||||
|
return normalizeAnswerMarkdown(normalizedMarkdown)
|
||||||
}
|
}
|
||||||
|
|
||||||
function collectDeepSeekInlineCitationOccurrences(
|
function collectDeepSeekInlineCitationOccurrences(
|
||||||
@@ -1721,6 +1889,76 @@ const contentCitationColumns = computed(() => [
|
|||||||
margin-bottom: 18px;
|
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 {
|
.custom-scrollbar::-webkit-scrollbar {
|
||||||
width: 6px;
|
width: 6px;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user