diff --git a/static/js/markdown.js b/static/js/markdown.js index 7c5656d..45a8c83 100644 --- a/static/js/markdown.js +++ b/static/js/markdown.js @@ -29,10 +29,8 @@ const Markdown = { .replace(//g, '>'); - // Phase 4: Block-level processing (split into blocks by blank lines) - const blocks = text.split(/\n{2,}/); - const htmlBlocks = blocks.map(block => this._renderBlock(block.trim())); - text = htmlBlocks.join('\n'); + // Phase 4: Block-level processing (line-by-line to handle single-newline headers/lists) + text = this._renderLines(text); // Phase 5: Restore fenced code blocks codeBlocks.forEach((cb, idx) => { @@ -55,39 +53,63 @@ const Markdown = { return text; }, + _renderLines(text) { + const lines = text.split('\n'); + const out = []; + let para = []; + + const flushPara = () => { + if (para.length) { + out.push(`
${this._renderInline(para.join('
'))}
${this._renderInline(trimmed.replace(/^>\s?/, ''))}`); continue; } + + // Unordered list item + if (/^[\-\*]\s/.test(trimmed)) { flushPara(); out.push(this._renderListItem(trimmed, 'ul')); continue; } + + // Ordered list item + if (/^\d+\.\s/.test(trimmed)) { flushPara(); out.push(this._renderListItem(trimmed, 'ol')); continue; } + + // Regular text — accumulate into paragraph + para.push(trimmed); + } + flushPara(); + return out.join('\n'); + }, + + _renderListItem(line, tag) { + const m = line.match(/^[\s]*(?:[\-\*]|\d+\.)\s+(.*)$/); + const content = m ? m[1] : line; + // Return just the li; caller can wrap in ul/ol if needed + return `
${this._renderInline(inner)}`; - } - - // Unordered list - if (/^[\-\*]\s/.test(block)) { - return this._renderList(block, 'ul'); - } - - // Ordered list - if (/^\d+\.\s/.test(block)) { - return this._renderList(block, 'ol'); - } - - // Paragraph + if (/^(-{3,}|\*{3,}|_{3,})$/.test(block)) return '
${this._renderInline(inner)}`; } + if (/^[\-\*]\s/.test(block)) return this._renderList(block, 'ul'); + if (/^\d+\.\s/.test(block)) return this._renderList(block, 'ol'); return `
${this._renderInline(block)}
`; }, diff --git a/templates/pages/generate.html b/templates/pages/generate.html index ea12df8..95b4bc0 100644 --- a/templates/pages/generate.html +++ b/templates/pages/generate.html @@ -184,48 +184,77 @@ indicator.classList.remove('hidden'); // Clear previous results - document.getElementById('pr-rendered').innerHTML = ''; + document.getElementById('pr-rendered').innerHTML = '正在生成...'; document.getElementById('pr-markdown').textContent = ''; markdownContent = ''; - // Helper: update both columns from current fields const fields = { title: '', type: '', summary: '', details: '', impact: '' }; + let rawText = ''; + let done = false; + function updateColumns() { markdownContent = buildMarkdown(fields); document.getElementById('pr-rendered').innerHTML = renderMarkdown(markdownContent); document.getElementById('pr-markdown').textContent = markdownContent; } + // Try to extract completed fields from partial JSON + function parsePartialJSON(text) { + const result = {}; + for (const key of ['title', 'type', 'summary', 'details', 'impact']) { + // Match "key": "value" where value's closing quote exists + const re = new RegExp('"' + key + '"\\s*:\\s*"((?:[^"\\\\]|\\\\.)*)"'); + const m = text.match(re); + if (m) result[key] = m[1].replace(/\\"/g, '"').replace(/\\n/g, '\n').replace(/\\\\/g, '\\'); + } + return result; + } + SSE.post(`/api/repos/${repoId}/generate`, { base: baseRef, head: headRef, }, { + content(data) { + if (done) return; + rawText += data.content || ''; + // Parse partial JSON to extract fields progressively + const partial = parsePartialJSON(rawText); + let changed = false; + for (const key of ['title', 'type', 'summary', 'details', 'impact']) { + if (partial[key] && partial[key] !== fields[key]) { + fields[key] = partial[key]; + changed = true; + } + } + if (changed) updateColumns(); + }, title(data) { - fields.title += data.content || ''; + fields.title = data.content || fields.title; updateColumns(); document.getElementById('streaming-status').textContent = '解析标题...'; }, type(data) { - fields.type += data.content || ''; + fields.type = data.content || fields.type; updateColumns(); document.getElementById('streaming-status').textContent = '解析类型...'; }, summary(data) { - fields.summary += data.content || ''; + fields.summary = data.content || fields.summary; updateColumns(); document.getElementById('streaming-status').textContent = '解析摘要...'; }, detail(data) { - fields.details += data.content || ''; + fields.details = data.content || fields.details; updateColumns(); document.getElementById('streaming-status').textContent = '解析详细说明...'; }, impact(data) { - fields.impact += data.content || ''; + fields.impact = data.content || fields.impact; updateColumns(); document.getElementById('streaming-status').textContent = '解析影响范围...'; }, done() { + done = true; updateColumns(); indicator.classList.add('hidden'); btn.disabled = false; @@ -233,6 +262,7 @@ btn.classList.remove('opacity-70', 'cursor-not-allowed'); }, error(data) { + done = true; showToast('生成失败: ' + (data.message || '未知错误'), 'error'); indicator.classList.add('hidden'); btn.disabled = false;