Initial commit: Nexus Mission Control Platform
- ASP.NET Core 10 Backend (JWT Auth, Agent config API) - Vue 3 Frontend (Dashboard, Team, Agents, Config Editor) - PostgreSQL Database - Docker Compose setup - Mission Control Dashboard redesign
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* Simple markdown-to-HTML renderer for Nexus.
|
||||
* Handles common constructs without external dependencies.
|
||||
*/
|
||||
export function renderMarkdown(text: string): string {
|
||||
let html = text
|
||||
|
||||
// Escape HTML entities first
|
||||
html = html
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
|
||||
// Code blocks (fenced) - process before other inline rules
|
||||
html = html.replace(/```(\w*)\n([\s\S]*?)```/g, (_match, lang: string, code: string) => {
|
||||
const langAttr = lang ? ` class="language-${lang}"` : ''
|
||||
return `<pre><code${langAttr}>${code.trim()}</code></pre>`
|
||||
})
|
||||
|
||||
// Inline code
|
||||
html = html.replace(/`([^`]+)`/g, '<code>$1</code>')
|
||||
|
||||
// Horizontal rules
|
||||
html = html.replace(/^---$/gm, '<hr>')
|
||||
|
||||
// Headers
|
||||
html = html.replace(/^###### (.+)$/gm, '<h6>$1</h6>')
|
||||
html = html.replace(/^##### (.+)$/gm, '<h5>$1</h5>')
|
||||
html = html.replace(/^#### (.+)$/gm, '<h4>$1</h4>')
|
||||
html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>')
|
||||
html = html.replace(/^## (.+)$/gm, '<h2>$1</h2>')
|
||||
html = html.replace(/^# (.+)$/gm, '<h1>$1</h1>')
|
||||
|
||||
// Bold
|
||||
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
||||
|
||||
// Italic
|
||||
html = html.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
||||
|
||||
// Links [text](url) — block dangerous URL schemes
|
||||
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match: string, text: string, url: string) => {
|
||||
const lower = url.trim().toLowerCase()
|
||||
if (lower.startsWith('javascript:') || lower.startsWith('data:') || lower.startsWith('vbscript:')) {
|
||||
return `<a href="#" class="blocked-url" title="Blocked: ${text}">[blocked] ${text}</a>`
|
||||
}
|
||||
return `<a href="${url}" target="_blank" rel="noopener noreferrer">${text}</a>`
|
||||
})
|
||||
|
||||
// Unordered lists
|
||||
html = html.replace(/^- (.+)$/gm, '<li>$1</li>')
|
||||
html = html.replace(/((?:<li>.*<\/li>\n?)+)/g, '<ul>$1</ul>')
|
||||
|
||||
// Paragraphs: wrap remaining lines that aren't already wrapped
|
||||
const lines = html.split('\n')
|
||||
const result: string[] = []
|
||||
let inBlock = false
|
||||
let blockTag = ''
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i]
|
||||
const trimmed = line.trim()
|
||||
|
||||
if (!trimmed) {
|
||||
if (inBlock) {
|
||||
result.push(`</${blockTag}>`)
|
||||
inBlock = false
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Already wrapped in a block-level tag
|
||||
if (/^<(h[1-6]|ul|ol|pre|hr|li|p)/.test(trimmed) || /^<\/(ul|ol|pre)>/.test(trimmed) || /^<(ul|ol|pre)>/.test(trimmed)) {
|
||||
if (inBlock) {
|
||||
result.push(`</${blockTag}>`)
|
||||
inBlock = false
|
||||
}
|
||||
result.push(line)
|
||||
continue
|
||||
}
|
||||
|
||||
// Closing tag
|
||||
if (/^<\//.test(trimmed)) {
|
||||
if (inBlock) {
|
||||
result.push(`</${blockTag}>`)
|
||||
inBlock = false
|
||||
}
|
||||
result.push(line)
|
||||
continue
|
||||
}
|
||||
|
||||
if (!inBlock) {
|
||||
inBlock = true
|
||||
blockTag = 'p'
|
||||
result.push(`<${blockTag}>${trimmed}</${blockTag}>`)
|
||||
} else {
|
||||
// Within paragraph
|
||||
result.pop() // Remove previous closing tag
|
||||
const prevContent = result[result.length - 1]?.replace(/^<p>/, '').replace(/<\/p>$/, '') ?? ''
|
||||
result[result.length - 1] = `<p>${prevContent} ${trimmed}</p>`
|
||||
}
|
||||
}
|
||||
|
||||
if (inBlock) {
|
||||
result.push(`</${blockTag}>`)
|
||||
}
|
||||
|
||||
return result.join('\n')
|
||||
}
|
||||
Reference in New Issue
Block a user