Browse Source

修复 Markdown 标题、列表及表格渲染

master
pyx 3 days ago
parent
commit
f511572618
  1. 120
      client/dist/chatbot-sdk.js
  2. 2
      client/dist/chatbot-sdk.js.map
  3. 2
      client/dist/chatbot-sdk.min.js
  4. 2
      client/dist/chatbot-sdk.min.js.map
  5. 86
      client/src/markdown.ts
  6. 38
      client/src/styles.ts
  7. 50
      client/tests/markdown.test.ts
  8. 120
      src/main/resources/static/sdk/chatbot-sdk.js
  9. 2
      src/main/resources/static/sdk/chatbot-sdk.js.map
  10. 2
      src/main/resources/static/sdk/chatbot-sdk.min.js
  11. 2
      src/main/resources/static/sdk/chatbot-sdk.min.js.map

120
client/dist/chatbot-sdk.js

@ -1663,6 +1663,42 @@ var ChatbotSDK = (function () {
border-top: 1px solid var(--csk-border);
margin: 12px 0;
}
.csk-md-table-wrap {
max-width: 100%;
overflow-x: auto;
margin: 10px 0;
border: 1px solid var(--csk-border);
border-radius: 8px;
}
.csk-md-table {
width: 100%;
min-width: 360px;
border-collapse: collapse;
font-size: 12.5px;
line-height: 1.5;
}
.csk-md-table th,
.csk-md-table td {
padding: 7px 9px;
border-right: 1px solid var(--csk-border);
border-bottom: 1px solid var(--csk-border);
text-align: left;
vertical-align: top;
}
.csk-md-table th:last-child,
.csk-md-table td:last-child { border-right: none; }
.csk-md-table tbody tr:last-child td { border-bottom: none; }
.csk-md-table th {
background: rgba(var(--csk-primary-rgb), 0.07);
color: #374151;
font-weight: 600;
}
.csk-md-table tbody tr:nth-child(even) {
background: rgba(148, 163, 184, 0.06);
}
.csk-md-table .csk-md-align-left { text-align: left; }
.csk-md-table .csk-md-align-center { text-align: center; }
.csk-md-table .csk-md-align-right { text-align: right; }
/* ========== P2: 会话管理面板 ========== */
.csk-history-panel {
@ -2069,6 +2105,8 @@ var ChatbotSDK = (function () {
.csk-dark .csk-md-code-block { background: #0F0F1A; }
.csk-dark .csk-md-blockquote { background: rgba(var(--csk-primary-rgb), 0.08); }
.csk-dark .csk-md-link { color: var(--csk-primary-light); }
.csk-dark .csk-md-table th { background: rgba(var(--csk-primary-rgb), 0.14); color: #E2E8F0; }
.csk-dark .csk-md-table tbody tr:nth-child(even) { background: rgba(148, 163, 184, 0.05); }
.csk-dark .csk-msg--ai .csk-msg__bubble { background: var(--csk-bg-ai); color: var(--csk-text-ai); border-color: var(--csk-border); }
.csk-dark .csk-welcome__title { color: #E2E8F0; }
.csk-dark .csk-welcome__desc { color: #6B7280; }
@ -3565,7 +3603,8 @@ var ChatbotSDK = (function () {
continue;
}
// 标题
const headingMatch = line.match(/^(#{1,6})\s+(.+)/);
// Accept standard headings and common no-space forms before CJK text or digits.
const headingMatch = line.match(/^ {0,3}(#{1,6})(?:\s+|(?=[\u3400-\u9FFF\d]))(.+)/);
if (headingMatch) {
flushParagraph();
closeList();
@ -3575,7 +3614,37 @@ var ChatbotSDK = (function () {
continue;
}
// 引用
const quoteMatch = line.match(/^>\s?(.*)/);
// GFM table: require a valid separator row to avoid matching ordinary pipe text.
if (i + 1 < lines.length && isTableSeparator(lines[i + 1])) {
const headers = splitTableRow(line);
const separators = splitTableRow(lines[i + 1]);
if (headers.length > 0 && headers.length === separators.length) {
flushParagraph();
closeList();
closeBlockquote();
const alignments = separators.map(getTableAlignment);
result.push('<div class="csk-md-table-wrap"><table class="csk-md-table">');
result.push('<thead><tr>');
headers.forEach((cell, index) => {
result.push(`<th${alignmentAttr(alignments[index])}>${inlineFormat(cell)}</th>`);
});
result.push('</tr></thead><tbody>');
i += 2;
while (i < lines.length && isTableDataRow(lines[i])) {
const cells = splitTableRow(lines[i]);
result.push('<tr>');
headers.forEach((_header, index) => {
result.push(`<td${alignmentAttr(alignments[index])}>${inlineFormat(cells[index] || '')}</td>`);
});
result.push('</tr>');
i++;
}
result.push('</tbody></table></div>');
i--;
continue;
}
}
const quoteMatch = line.match(/^ {0,3}&gt;\s?(.*)/);
if (quoteMatch) {
flushParagraph();
closeList();
@ -3590,7 +3659,7 @@ var ChatbotSDK = (function () {
closeBlockquote();
}
// 无序列表
const ulMatch = line.match(/^[\-\*]\s+(.+)/);
const ulMatch = line.match(/^ {0,3}[\-\*]\s+(.+)/);
if (ulMatch) {
flushParagraph();
closeBlockquote();
@ -3604,7 +3673,7 @@ var ChatbotSDK = (function () {
continue;
}
// 有序列表
const olMatch = line.match(/^\d+\.\s+(.+)/);
const olMatch = line.match(/^ {0,3}\d+\.\s+(.+)/);
if (olMatch) {
flushParagraph();
closeBlockquote();
@ -3660,6 +3729,49 @@ var ChatbotSDK = (function () {
return text;
}
/** 将段落缓冲输出为 <p> */
function isTableSeparator(line) {
const cells = splitTableRow(line);
return cells.length > 0 && cells.every(cell => /^:?-{3,}:?$/.test(cell.trim()));
}
function isTableDataRow(line) {
return line.trim() !== '' && line.includes('|');
}
function splitTableRow(line) {
const trimmed = line.trim().replace(/^\|/, '').replace(/\|$/, '');
if (!trimmed.includes('|'))
return [];
const cells = [];
let cell = '';
for (let i = 0; i < trimmed.length; i++) {
const char = trimmed[i];
if (char === '\\' && trimmed[i + 1] === '|') {
cell += '|';
i++;
}
else if (char === '|') {
cells.push(cell.trim());
cell = '';
}
else {
cell += char;
}
}
cells.push(cell.trim());
return cells;
}
function getTableAlignment(separator) {
const value = separator.trim();
if (value.startsWith(':') && value.endsWith(':'))
return 'center';
if (value.endsWith(':'))
return 'right';
if (value.startsWith(':'))
return 'left';
return '';
}
function alignmentAttr(alignment) {
return alignment ? ` class="csk-md-align-${alignment}"` : '';
}
function flushParagraph() {
if (paragraphBuffer.length > 0) {
result.push(`<p class="csk-md-p">${paragraphBuffer.join('<br>')}</p>`);

2
client/dist/chatbot-sdk.js.map
File diff suppressed because it is too large
View File

2
client/dist/chatbot-sdk.min.js
File diff suppressed because it is too large
View File

2
client/dist/chatbot-sdk.min.js.map
File diff suppressed because it is too large
View File

86
client/src/markdown.ts

@ -69,7 +69,8 @@ export function renderMarkdown(text: string): string {
}
// 标题
const headingMatch = line.match(/^(#{1,6})\s+(.+)/);
// Accept standard headings and common no-space forms before CJK text or digits.
const headingMatch = line.match(/^ {0,3}(#{1,6})(?:\s+|(?=[\u3400-\u9FFF\d]))(.+)/);
if (headingMatch) {
flushParagraph();
closeList();
@ -80,7 +81,41 @@ export function renderMarkdown(text: string): string {
}
// 引用
const quoteMatch = line.match(/^&gt;\s?(.*)/);
// GFM table: require a valid separator row to avoid matching ordinary pipe text.
if (i + 1 < lines.length && isTableSeparator(lines[i + 1])) {
const headers = splitTableRow(line);
const separators = splitTableRow(lines[i + 1]);
if (headers.length > 0 && headers.length === separators.length) {
flushParagraph();
closeList();
closeBlockquote();
const alignments = separators.map(getTableAlignment);
result.push('<div class="csk-md-table-wrap"><table class="csk-md-table">');
result.push('<thead><tr>');
headers.forEach((cell, index) => {
result.push(`<th${alignmentAttr(alignments[index])}>${inlineFormat(cell)}</th>`);
});
result.push('</tr></thead><tbody>');
i += 2;
while (i < lines.length && isTableDataRow(lines[i])) {
const cells = splitTableRow(lines[i]);
result.push('<tr>');
headers.forEach((_header, index) => {
result.push(`<td${alignmentAttr(alignments[index])}>${inlineFormat(cells[index] || '')}</td>`);
});
result.push('</tr>');
i++;
}
result.push('</tbody></table></div>');
i--;
continue;
}
}
const quoteMatch = line.match(/^ {0,3}&gt;\s?(.*)/);
if (quoteMatch) {
flushParagraph();
closeList();
@ -95,7 +130,7 @@ export function renderMarkdown(text: string): string {
}
// 无序列表
const ulMatch = line.match(/^[\-\*]\s+(.+)/);
const ulMatch = line.match(/^ {0,3}[\-\*]\s+(.+)/);
if (ulMatch) {
flushParagraph();
closeBlockquote();
@ -110,7 +145,7 @@ export function renderMarkdown(text: string): string {
}
// 有序列表
const olMatch = line.match(/^\d+\.\s+(.+)/);
const olMatch = line.match(/^ {0,3}\d+\.\s+(.+)/);
if (olMatch) {
flushParagraph();
closeBlockquote();
@ -174,6 +209,49 @@ export function renderMarkdown(text: string): string {
}
/** 将段落缓冲输出为 <p> */
function isTableSeparator(line: string): boolean {
const cells = splitTableRow(line);
return cells.length > 0 && cells.every(cell => /^:?-{3,}:?$/.test(cell.trim()));
}
function isTableDataRow(line: string): boolean {
return line.trim() !== '' && line.includes('|');
}
function splitTableRow(line: string): string[] {
const trimmed = line.trim().replace(/^\|/, '').replace(/\|$/, '');
if (!trimmed.includes('|')) return [];
const cells: string[] = [];
let cell = '';
for (let i = 0; i < trimmed.length; i++) {
const char = trimmed[i];
if (char === '\\' && trimmed[i + 1] === '|') {
cell += '|';
i++;
} else if (char === '|') {
cells.push(cell.trim());
cell = '';
} else {
cell += char;
}
}
cells.push(cell.trim());
return cells;
}
function getTableAlignment(separator: string): '' | 'left' | 'center' | 'right' {
const value = separator.trim();
if (value.startsWith(':') && value.endsWith(':')) return 'center';
if (value.endsWith(':')) return 'right';
if (value.startsWith(':')) return 'left';
return '';
}
function alignmentAttr(alignment: '' | 'left' | 'center' | 'right'): string {
return alignment ? ` class="csk-md-align-${alignment}"` : '';
}
function flushParagraph(): void {
if (paragraphBuffer.length > 0) {
result.push(`<p class="csk-md-p">${paragraphBuffer.join('<br>')}</p>`);

38
client/src/styles.ts

@ -742,6 +742,42 @@ function getStyles(config: ResolvedConfig): string {
border-top: 1px solid var(--csk-border);
margin: 12px 0;
}
.csk-md-table-wrap {
max-width: 100%;
overflow-x: auto;
margin: 10px 0;
border: 1px solid var(--csk-border);
border-radius: 8px;
}
.csk-md-table {
width: 100%;
min-width: 360px;
border-collapse: collapse;
font-size: 12.5px;
line-height: 1.5;
}
.csk-md-table th,
.csk-md-table td {
padding: 7px 9px;
border-right: 1px solid var(--csk-border);
border-bottom: 1px solid var(--csk-border);
text-align: left;
vertical-align: top;
}
.csk-md-table th:last-child,
.csk-md-table td:last-child { border-right: none; }
.csk-md-table tbody tr:last-child td { border-bottom: none; }
.csk-md-table th {
background: rgba(var(--csk-primary-rgb), 0.07);
color: #374151;
font-weight: 600;
}
.csk-md-table tbody tr:nth-child(even) {
background: rgba(148, 163, 184, 0.06);
}
.csk-md-table .csk-md-align-left { text-align: left; }
.csk-md-table .csk-md-align-center { text-align: center; }
.csk-md-table .csk-md-align-right { text-align: right; }
/* ========== P2: 会话管理面板 ========== */
.csk-history-panel {
@ -1148,6 +1184,8 @@ function getStyles(config: ResolvedConfig): string {
.csk-dark .csk-md-code-block { background: #0F0F1A; }
.csk-dark .csk-md-blockquote { background: rgba(var(--csk-primary-rgb), 0.08); }
.csk-dark .csk-md-link { color: var(--csk-primary-light); }
.csk-dark .csk-md-table th { background: rgba(var(--csk-primary-rgb), 0.14); color: #E2E8F0; }
.csk-dark .csk-md-table tbody tr:nth-child(even) { background: rgba(148, 163, 184, 0.05); }
.csk-dark .csk-msg--ai .csk-msg__bubble { background: var(--csk-bg-ai); color: var(--csk-text-ai); border-color: var(--csk-border); }
.csk-dark .csk-welcome__title { color: #E2E8F0; }
.csk-dark .csk-welcome__desc { color: #6B7280; }

50
client/tests/markdown.test.ts

@ -148,3 +148,53 @@ describe('renderMarkdown - 复合内容', () => {
expect(pCount).toBe(2);
});
});
describe('renderMarkdown - GFM tables and tolerant headings', () => {
it('accepts a no-space heading before CJK text', () => {
expect(renderMarkdown('##\u4e00\u3001\u5e72\u51bb\u8d27/\u751f\u9c9c\u6a21\u5757')).toContain(
'<h2 class="csk-md-h2">\u4e00\u3001\u5e72\u51bb\u8d27/\u751f\u9c9c\u6a21\u5757</h2>',
);
});
it('accepts a no-space heading before a digit', () => {
expect(renderMarkdown('##1 purchase warehouse')).toContain(
'<h2 class="csk-md-h2">1 purchase warehouse</h2>',
);
});
it('accepts up to three leading spaces for block syntax', () => {
const result = renderMarkdown(
' ## 1\uFE0F\u20E3 purchase warehouse\n\n - **field**: warehouseid',
);
expect(result).toContain('<h2 class="csk-md-h2">1\uFE0F\u20E3 purchase warehouse</h2>');
expect(result).toContain('<ul class="csk-md-ul">');
expect(result).toContain('<strong>field</strong>');
});
it('does not treat an English hashtag as a heading', () => {
expect(renderMarkdown('#topic')).toContain('<p class="csk-md-p">#topic</p>');
});
it('renders a GFM table with alignment and inline formatting', () => {
const result = renderMarkdown(
'|\u5b57\u6bb5|\u8bf4\u660e|\u6821\u9a8c\u89c4\u5219|\n|---|:---:|---:|\n|**warehouseId**|ID|required|',
);
expect(result).toContain('<table class="csk-md-table">');
expect(result).toContain('<th>\u5b57\u6bb5</th>');
expect(result).toContain('<th class="csk-md-align-center">\u8bf4\u660e</th>');
expect(result).toContain('<td class="csk-md-align-right">required</td>');
expect(result).toContain('<strong>warehouseId</strong>');
});
it('keeps table cell HTML escaped', () => {
const result = renderMarkdown('|field|note|\n|---|---|\n|name|<img src=x onerror=alert(1)>|');
expect(result).not.toContain('<img');
expect(result).toContain('&lt;img');
});
it('does not treat ordinary pipe text as a table', () => {
const result = renderMarkdown('A | B\nnot a separator');
expect(result).not.toContain('<table');
});
});

120
src/main/resources/static/sdk/chatbot-sdk.js

@ -1663,6 +1663,42 @@ var ChatbotSDK = (function () {
border-top: 1px solid var(--csk-border);
margin: 12px 0;
}
.csk-md-table-wrap {
max-width: 100%;
overflow-x: auto;
margin: 10px 0;
border: 1px solid var(--csk-border);
border-radius: 8px;
}
.csk-md-table {
width: 100%;
min-width: 360px;
border-collapse: collapse;
font-size: 12.5px;
line-height: 1.5;
}
.csk-md-table th,
.csk-md-table td {
padding: 7px 9px;
border-right: 1px solid var(--csk-border);
border-bottom: 1px solid var(--csk-border);
text-align: left;
vertical-align: top;
}
.csk-md-table th:last-child,
.csk-md-table td:last-child { border-right: none; }
.csk-md-table tbody tr:last-child td { border-bottom: none; }
.csk-md-table th {
background: rgba(var(--csk-primary-rgb), 0.07);
color: #374151;
font-weight: 600;
}
.csk-md-table tbody tr:nth-child(even) {
background: rgba(148, 163, 184, 0.06);
}
.csk-md-table .csk-md-align-left { text-align: left; }
.csk-md-table .csk-md-align-center { text-align: center; }
.csk-md-table .csk-md-align-right { text-align: right; }
/* ========== P2: 会话管理面板 ========== */
.csk-history-panel {
@ -2069,6 +2105,8 @@ var ChatbotSDK = (function () {
.csk-dark .csk-md-code-block { background: #0F0F1A; }
.csk-dark .csk-md-blockquote { background: rgba(var(--csk-primary-rgb), 0.08); }
.csk-dark .csk-md-link { color: var(--csk-primary-light); }
.csk-dark .csk-md-table th { background: rgba(var(--csk-primary-rgb), 0.14); color: #E2E8F0; }
.csk-dark .csk-md-table tbody tr:nth-child(even) { background: rgba(148, 163, 184, 0.05); }
.csk-dark .csk-msg--ai .csk-msg__bubble { background: var(--csk-bg-ai); color: var(--csk-text-ai); border-color: var(--csk-border); }
.csk-dark .csk-welcome__title { color: #E2E8F0; }
.csk-dark .csk-welcome__desc { color: #6B7280; }
@ -3565,7 +3603,8 @@ var ChatbotSDK = (function () {
continue;
}
// 标题
const headingMatch = line.match(/^(#{1,6})\s+(.+)/);
// Accept standard headings and common no-space forms before CJK text or digits.
const headingMatch = line.match(/^ {0,3}(#{1,6})(?:\s+|(?=[\u3400-\u9FFF\d]))(.+)/);
if (headingMatch) {
flushParagraph();
closeList();
@ -3575,7 +3614,37 @@ var ChatbotSDK = (function () {
continue;
}
// 引用
const quoteMatch = line.match(/^&gt;\s?(.*)/);
// GFM table: require a valid separator row to avoid matching ordinary pipe text.
if (i + 1 < lines.length && isTableSeparator(lines[i + 1])) {
const headers = splitTableRow(line);
const separators = splitTableRow(lines[i + 1]);
if (headers.length > 0 && headers.length === separators.length) {
flushParagraph();
closeList();
closeBlockquote();
const alignments = separators.map(getTableAlignment);
result.push('<div class="csk-md-table-wrap"><table class="csk-md-table">');
result.push('<thead><tr>');
headers.forEach((cell, index) => {
result.push(`<th${alignmentAttr(alignments[index])}>${inlineFormat(cell)}</th>`);
});
result.push('</tr></thead><tbody>');
i += 2;
while (i < lines.length && isTableDataRow(lines[i])) {
const cells = splitTableRow(lines[i]);
result.push('<tr>');
headers.forEach((_header, index) => {
result.push(`<td${alignmentAttr(alignments[index])}>${inlineFormat(cells[index] || '')}</td>`);
});
result.push('</tr>');
i++;
}
result.push('</tbody></table></div>');
i--;
continue;
}
}
const quoteMatch = line.match(/^ {0,3}&gt;\s?(.*)/);
if (quoteMatch) {
flushParagraph();
closeList();
@ -3590,7 +3659,7 @@ var ChatbotSDK = (function () {
closeBlockquote();
}
// 无序列表
const ulMatch = line.match(/^[\-\*]\s+(.+)/);
const ulMatch = line.match(/^ {0,3}[\-\*]\s+(.+)/);
if (ulMatch) {
flushParagraph();
closeBlockquote();
@ -3604,7 +3673,7 @@ var ChatbotSDK = (function () {
continue;
}
// 有序列表
const olMatch = line.match(/^\d+\.\s+(.+)/);
const olMatch = line.match(/^ {0,3}\d+\.\s+(.+)/);
if (olMatch) {
flushParagraph();
closeBlockquote();
@ -3660,6 +3729,49 @@ var ChatbotSDK = (function () {
return text;
}
/** 将段落缓冲输出为 <p> */
function isTableSeparator(line) {
const cells = splitTableRow(line);
return cells.length > 0 && cells.every(cell => /^:?-{3,}:?$/.test(cell.trim()));
}
function isTableDataRow(line) {
return line.trim() !== '' && line.includes('|');
}
function splitTableRow(line) {
const trimmed = line.trim().replace(/^\|/, '').replace(/\|$/, '');
if (!trimmed.includes('|'))
return [];
const cells = [];
let cell = '';
for (let i = 0; i < trimmed.length; i++) {
const char = trimmed[i];
if (char === '\\' && trimmed[i + 1] === '|') {
cell += '|';
i++;
}
else if (char === '|') {
cells.push(cell.trim());
cell = '';
}
else {
cell += char;
}
}
cells.push(cell.trim());
return cells;
}
function getTableAlignment(separator) {
const value = separator.trim();
if (value.startsWith(':') && value.endsWith(':'))
return 'center';
if (value.endsWith(':'))
return 'right';
if (value.startsWith(':'))
return 'left';
return '';
}
function alignmentAttr(alignment) {
return alignment ? ` class="csk-md-align-${alignment}"` : '';
}
function flushParagraph() {
if (paragraphBuffer.length > 0) {
result.push(`<p class="csk-md-p">${paragraphBuffer.join('<br>')}</p>`);

2
src/main/resources/static/sdk/chatbot-sdk.js.map
File diff suppressed because it is too large
View File

2
src/main/resources/static/sdk/chatbot-sdk.min.js
File diff suppressed because it is too large
View File

2
src/main/resources/static/sdk/chatbot-sdk.min.js.map
File diff suppressed because it is too large
View File

Loading…
Cancel
Save