chore: add build artifacts, map assets, and routing data
This commit is contained in:
@@ -0,0 +1,351 @@
|
||||
import React from 'react';
|
||||
|
||||
interface TacticalMarkdownRendererProps {
|
||||
content: string;
|
||||
isChat?: boolean;
|
||||
}
|
||||
|
||||
// Inline parser for bold, italic, inline-code, and tags
|
||||
const parseInline = (text: string): React.ReactNode[] => {
|
||||
if (!text) return [];
|
||||
|
||||
// Match bold (**text**), inline code (`text`), italic (*text* or _text_)
|
||||
const tokens: React.ReactNode[] = [];
|
||||
let remaining = text;
|
||||
let key = 0;
|
||||
|
||||
while (remaining.length > 0) {
|
||||
// Check for bold **...** or __...__
|
||||
const boldMatch = remaining.match(/^(\*\*|__)(.*?)\1/);
|
||||
if (boldMatch) {
|
||||
tokens.push(
|
||||
<strong
|
||||
key={key++}
|
||||
style={{
|
||||
fontWeight: 800,
|
||||
color: '#0f172a',
|
||||
background: 'rgba(241, 245, 249, 0.6)',
|
||||
padding: '1px 4px',
|
||||
borderRadius: '4px',
|
||||
margin: '0 1px'
|
||||
}}
|
||||
>
|
||||
{parseInline(boldMatch[2])}
|
||||
</strong>
|
||||
);
|
||||
remaining = remaining.slice(boldMatch[0].length);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for inline code `...`
|
||||
const codeMatch = remaining.match(/^`([^`]+)`/);
|
||||
if (codeMatch) {
|
||||
tokens.push(
|
||||
<code
|
||||
key={key++}
|
||||
style={{
|
||||
background: '#e0e7ff',
|
||||
color: '#4338ca',
|
||||
padding: '2px 6px',
|
||||
borderRadius: '4px',
|
||||
fontSize: '0.85em',
|
||||
fontFamily: 'monospace',
|
||||
fontWeight: 700,
|
||||
direction: 'ltr',
|
||||
display: 'inline-block'
|
||||
}}
|
||||
>
|
||||
{codeMatch[1]}
|
||||
</code>
|
||||
);
|
||||
remaining = remaining.slice(codeMatch[0].length);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for italic *...* or _..._
|
||||
const italicMatch = remaining.match(/^(\*|_)(.*?)\1/);
|
||||
if (italicMatch) {
|
||||
tokens.push(
|
||||
<em key={key++} style={{ fontStyle: 'italic', color: '#475569' }}>
|
||||
{parseInline(italicMatch[2])}
|
||||
</em>
|
||||
);
|
||||
remaining = remaining.slice(italicMatch[0].length);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Regular text until the next special character
|
||||
const nextSpecial = remaining.search(/[\*_`]/);
|
||||
if (nextSpecial === -1) {
|
||||
tokens.push(remaining);
|
||||
break;
|
||||
} else if (nextSpecial === 0) {
|
||||
// Stray character, consume it as plain text
|
||||
tokens.push(remaining[0]);
|
||||
remaining = remaining.slice(1);
|
||||
} else {
|
||||
tokens.push(remaining.slice(0, nextSpecial));
|
||||
remaining = remaining.slice(nextSpecial);
|
||||
}
|
||||
}
|
||||
|
||||
return tokens;
|
||||
};
|
||||
|
||||
export const TacticalMarkdownRenderer: React.FC<TacticalMarkdownRendererProps> = ({
|
||||
content,
|
||||
isChat = false
|
||||
}) => {
|
||||
if (!content) return null;
|
||||
|
||||
const lines = content.split('\n');
|
||||
const elements: React.ReactNode[] = [];
|
||||
let currentList: { type: 'ul' | 'ol'; items: React.ReactNode[] } | null = null;
|
||||
let elementKey = 0;
|
||||
|
||||
const flushList = () => {
|
||||
if (currentList) {
|
||||
if (currentList.type === 'ul') {
|
||||
elements.push(
|
||||
<ul
|
||||
key={elementKey++}
|
||||
style={{
|
||||
margin: '8px 0 12px 0',
|
||||
paddingRight: isChat ? '16px' : '20px',
|
||||
paddingLeft: 0,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: isChat ? '4px' : '6px',
|
||||
listStyleType: 'disc'
|
||||
}}
|
||||
>
|
||||
{currentList.items.map((item, idx) => (
|
||||
<li key={idx} style={{ lineHeight: isChat ? 1.6 : 1.85, color: '#1e293b' }}>
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
} else {
|
||||
elements.push(
|
||||
<ol
|
||||
key={elementKey++}
|
||||
style={{
|
||||
margin: '8px 0 12px 0',
|
||||
paddingRight: isChat ? '18px' : '24px',
|
||||
paddingLeft: 0,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: isChat ? '4px' : '6px'
|
||||
}}
|
||||
>
|
||||
{currentList.items.map((item, idx) => (
|
||||
<li key={idx} style={{ lineHeight: isChat ? 1.6 : 1.85, color: '#1e293b', fontWeight: 600 }}>
|
||||
<span style={{ fontWeight: 400 }}>{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
);
|
||||
}
|
||||
currentList = null;
|
||||
}
|
||||
};
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const rawLine = lines[i];
|
||||
const trimmed = rawLine.trim();
|
||||
|
||||
// Empty lines
|
||||
if (!trimmed) {
|
||||
flushList();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Horizontal Rule (--- or *** or ___)
|
||||
if (/^(\-{3,}|\*{3,}|_{3,})$/.test(trimmed)) {
|
||||
flushList();
|
||||
elements.push(
|
||||
<hr
|
||||
key={elementKey++}
|
||||
style={{
|
||||
border: 'none',
|
||||
height: '1px',
|
||||
background: 'linear-gradient(to left, transparent, #cbd5e1, transparent)',
|
||||
margin: isChat ? '10px 0' : '16px 0'
|
||||
}}
|
||||
/>
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Headers
|
||||
const h1Match = trimmed.match(/^#\s+(.+)$/);
|
||||
if (h1Match) {
|
||||
flushList();
|
||||
elements.push(
|
||||
<h1
|
||||
key={elementKey++}
|
||||
style={{
|
||||
fontSize: isChat ? '1.1rem' : '1.35rem',
|
||||
fontWeight: 900,
|
||||
color: '#0f172a',
|
||||
marginTop: isChat ? '10px' : '18px',
|
||||
marginBottom: isChat ? '6px' : '10px',
|
||||
borderBottom: '2px solid #e2e8f0',
|
||||
paddingBottom: '6px'
|
||||
}}
|
||||
>
|
||||
{parseInline(h1Match[1])}
|
||||
</h1>
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const h2Match = trimmed.match(/^##\s+(.+)$/);
|
||||
if (h2Match) {
|
||||
flushList();
|
||||
elements.push(
|
||||
<h2
|
||||
key={elementKey++}
|
||||
style={{
|
||||
fontSize: isChat ? '1rem' : '1.2rem',
|
||||
fontWeight: 900,
|
||||
color: '#1e293b',
|
||||
marginTop: isChat ? '8px' : '16px',
|
||||
marginBottom: isChat ? '4px' : '8px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8
|
||||
}}
|
||||
>
|
||||
<span style={{ width: 4, height: 16, background: '#9333ea', borderRadius: 2, display: 'inline-block' }}></span>
|
||||
<span>{parseInline(h2Match[1])}</span>
|
||||
</h2>
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const h3Match = trimmed.match(/^###\s+(.+)$/);
|
||||
if (h3Match) {
|
||||
flushList();
|
||||
elements.push(
|
||||
<h3
|
||||
key={elementKey++}
|
||||
style={{
|
||||
fontSize: isChat ? '0.92rem' : '1.05rem',
|
||||
fontWeight: 800,
|
||||
color: '#334155',
|
||||
marginTop: isChat ? '8px' : '14px',
|
||||
marginBottom: isChat ? '4px' : '6px',
|
||||
background: isChat ? 'rgba(241, 245, 249, 0.8)' : '#f8fafc',
|
||||
padding: '4px 10px',
|
||||
borderRadius: 6,
|
||||
borderRight: '3px solid #6366f1'
|
||||
}}
|
||||
>
|
||||
{parseInline(h3Match[1])}
|
||||
</h3>
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const h4Match = trimmed.match(/^####\s+(.+)$/);
|
||||
if (h4Match) {
|
||||
flushList();
|
||||
elements.push(
|
||||
<h4
|
||||
key={elementKey++}
|
||||
style={{
|
||||
fontSize: isChat ? '0.86rem' : '0.96rem',
|
||||
fontWeight: 800,
|
||||
color: '#475569',
|
||||
marginTop: isChat ? '6px' : '10px',
|
||||
marginBottom: '4px'
|
||||
}}
|
||||
>
|
||||
{parseInline(h4Match[1])}
|
||||
</h4>
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Unordered List Items (- or * or •)
|
||||
const ulMatch = trimmed.match(/^[-*•]\s+(.+)$/);
|
||||
if (ulMatch) {
|
||||
if (!currentList || currentList.type !== 'ul') {
|
||||
flushList();
|
||||
currentList = { type: 'ul', items: [] };
|
||||
}
|
||||
currentList.items.push(parseInline(ulMatch[1]));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ordered List Items (1. , 2. )
|
||||
const olMatch = trimmed.match(/^(\d+)\.\s+(.+)$/);
|
||||
if (olMatch) {
|
||||
if (!currentList || currentList.type !== 'ol') {
|
||||
flushList();
|
||||
currentList = { type: 'ol', items: [] };
|
||||
}
|
||||
currentList.items.push(parseInline(olMatch[2]));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Blockquote (> ...)
|
||||
const bqMatch = trimmed.match(/^>\s*(.+)$/);
|
||||
if (bqMatch) {
|
||||
flushList();
|
||||
elements.push(
|
||||
<blockquote
|
||||
key={elementKey++}
|
||||
style={{
|
||||
margin: '8px 0',
|
||||
padding: '6px 14px',
|
||||
background: '#f8fafc',
|
||||
borderRight: '4px solid #0071e3',
|
||||
color: '#334155',
|
||||
fontSize: isChat ? '0.84rem' : '0.9rem',
|
||||
lineHeight: 1.7,
|
||||
borderRadius: '0 8px 8px 0'
|
||||
}}
|
||||
>
|
||||
{parseInline(bqMatch[1])}
|
||||
</blockquote>
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Regular Paragraph
|
||||
flushList();
|
||||
elements.push(
|
||||
<p
|
||||
key={elementKey++}
|
||||
style={{
|
||||
margin: isChat ? '4px 0 6px 0' : '6px 0 10px 0',
|
||||
lineHeight: isChat ? 1.7 : 1.9,
|
||||
color: '#1e293b',
|
||||
fontSize: isChat ? '0.86rem' : '0.92rem'
|
||||
}}
|
||||
>
|
||||
{parseInline(trimmed)}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
flushList();
|
||||
|
||||
return (
|
||||
<div
|
||||
className="tactical-markdown-content"
|
||||
style={{
|
||||
direction: 'rtl',
|
||||
textAlign: 'right',
|
||||
fontFamily: 'inherit',
|
||||
color: '#1e293b',
|
||||
wordBreak: 'break-word'
|
||||
}}
|
||||
>
|
||||
{elements}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user