fix(workspace): group text and code blocks cleanly under headings on paste
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 58s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 58s
This commit is contained in:
1 parent
c85f31eee5
commit
554029a099
2 files changed
+195
-45
No files matched your search
@@ -245,66 +245,167 @@ export const OutlinerTree = forwardRef<OutlinerTreeHandle, OutlinerTreeProps>(({
|
|||||||
detectedSpaceStep = 4;
|
detectedSpaceStep = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse line indentation and markdown prefixes
|
// Parse lines into structured blocks
|
||||||
const parsed = rawLines.map((line) => {
|
interface ParsedPasteItem {
|
||||||
|
type: BlockType;
|
||||||
|
checked?: boolean;
|
||||||
|
content: string;
|
||||||
|
indent: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed: ParsedPasteItem[] = [];
|
||||||
|
let currentItem: ParsedPasteItem | null = null;
|
||||||
|
let inCodeBlock = false;
|
||||||
|
let afterHeading = false;
|
||||||
|
|
||||||
|
const pushCurrentItem = () => {
|
||||||
|
if (currentItem) {
|
||||||
|
currentItem.content = currentItem.content.trimEnd();
|
||||||
|
parsed.push(currentItem);
|
||||||
|
currentItem = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let i = 0; i < rawLines.length; i++) {
|
||||||
|
const rawLine = rawLines[i];
|
||||||
|
|
||||||
|
// 1. Inside a fenced code block
|
||||||
|
if (inCodeBlock) {
|
||||||
|
if (currentItem) {
|
||||||
|
currentItem.content += '\n' + rawLine;
|
||||||
|
}
|
||||||
|
if (rawLine.trim().startsWith('```')) {
|
||||||
|
inCodeBlock = false;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Starts a code fence
|
||||||
|
if (rawLine.trim().startsWith('```')) {
|
||||||
|
inCodeBlock = true;
|
||||||
|
if (afterHeading) {
|
||||||
|
afterHeading = false;
|
||||||
|
pushCurrentItem();
|
||||||
|
currentItem = {
|
||||||
|
type: 'text',
|
||||||
|
content: rawLine,
|
||||||
|
indent: 0,
|
||||||
|
};
|
||||||
|
} else if (currentItem) {
|
||||||
|
currentItem.content += (currentItem.content.endsWith('\n') ? '' : '\n') + rawLine;
|
||||||
|
} else {
|
||||||
|
currentItem = {
|
||||||
|
type: 'text',
|
||||||
|
content: rawLine,
|
||||||
|
indent: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Measure leading indent
|
||||||
let tabs = 0;
|
let tabs = 0;
|
||||||
let spaces = 0;
|
let spaces = 0;
|
||||||
let i = 0;
|
let charIdx = 0;
|
||||||
while (i < line.length) {
|
while (charIdx < rawLine.length) {
|
||||||
if (line[i] === '\t') {
|
if (rawLine[charIdx] === '\t') {
|
||||||
tabs++;
|
tabs++;
|
||||||
i++;
|
charIdx++;
|
||||||
} else if (line[i] === ' ') {
|
} else if (rawLine[charIdx] === ' ') {
|
||||||
spaces++;
|
spaces++;
|
||||||
i++;
|
charIdx++;
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const rawContent = line.slice(i);
|
const strippedLine = rawLine.slice(charIdx);
|
||||||
const indent = tabs + Math.floor(spaces / detectedSpaceStep);
|
const lineIndent = tabs + Math.floor(spaces / detectedSpaceStep);
|
||||||
|
|
||||||
// Todo: - [ ] or - [x] or * [ ] or * [x]
|
// Blank lines
|
||||||
const todoMatch = rawContent.match(/^[-*]\s*\[([ xX])\]\s*(.*)$/);
|
if (strippedLine.trim() === '') {
|
||||||
if (todoMatch) {
|
if (currentItem) {
|
||||||
return {
|
currentItem.content += '\n';
|
||||||
type: 'todo' as BlockType,
|
}
|
||||||
checked: todoMatch[1].toLowerCase() === 'x',
|
continue;
|
||||||
content: todoMatch[2],
|
|
||||||
indent,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Headings: #, ##, ###
|
// Headings: #, ##, ###
|
||||||
const h3Match = rawContent.match(/^###\s+(.*)$/);
|
const h3Match = strippedLine.match(/^###\s+(.*)$/);
|
||||||
if (h3Match) {
|
const h2Match = strippedLine.match(/^##\s+(.*)$/);
|
||||||
return { type: 'h3' as BlockType, content: h3Match[1], indent };
|
const h1Match = strippedLine.match(/^#\s+(.*)$/);
|
||||||
}
|
if (h1Match || h2Match || h3Match) {
|
||||||
const h2Match = rawContent.match(/^##\s+(.*)$/);
|
pushCurrentItem();
|
||||||
if (h2Match) {
|
afterHeading = true;
|
||||||
return { type: 'h2' as BlockType, content: h2Match[1], indent };
|
const type: BlockType = h1Match ? 'h1' : h2Match ? 'h2' : 'h3';
|
||||||
}
|
const content = (h1Match || h2Match || h3Match)![1];
|
||||||
const h1Match = rawContent.match(/^#\s+(.*)$/);
|
parsed.push({ type, content, indent: lineIndent });
|
||||||
if (h1Match) {
|
continue;
|
||||||
return { type: 'h1' as BlockType, content: h1Match[1], indent };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bullets: - or * or •, or numbered lists: 1. or 1)
|
// Dividers: ---, ***, ___
|
||||||
const bulletMatch = rawContent.match(/^[-*•]\s+(.*)$/);
|
if (/^(?:---|---|\*\*\*|___)\s*$/.test(strippedLine.trim())) {
|
||||||
if (bulletMatch) {
|
pushCurrentItem();
|
||||||
return { type: 'text' as BlockType, content: bulletMatch[1], indent };
|
afterHeading = false;
|
||||||
}
|
parsed.push({ type: 'divider', content: '---', indent: lineIndent });
|
||||||
const numMatch = rawContent.match(/^\d+[\.\)]\s+(.*)$/);
|
continue;
|
||||||
if (numMatch) {
|
|
||||||
return { type: 'text' as BlockType, content: numMatch[1], indent };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
// Todo items: - [ ], - [x], * [ ], * [x]
|
||||||
type: 'text' as BlockType,
|
const todoMatch = strippedLine.match(/^[-*]\s*\[([ xX])\]\s*(.*)$/);
|
||||||
content: rawContent,
|
if (todoMatch) {
|
||||||
indent,
|
pushCurrentItem();
|
||||||
};
|
afterHeading = false;
|
||||||
});
|
parsed.push({
|
||||||
|
type: 'todo',
|
||||||
|
checked: todoMatch[1].toLowerCase() === 'x',
|
||||||
|
content: todoMatch[2],
|
||||||
|
indent: lineIndent,
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Explicit bullet or numbered list: - item, * item, • item, 1. item, 1) item
|
||||||
|
const bulletMatch = strippedLine.match(/^[-*•]\s+(.*)$/);
|
||||||
|
const numMatch = strippedLine.match(/^\d+[\.\)]\s+(.*)$/);
|
||||||
|
if (bulletMatch || numMatch) {
|
||||||
|
pushCurrentItem();
|
||||||
|
afterHeading = false;
|
||||||
|
parsed.push({
|
||||||
|
type: 'text',
|
||||||
|
content: (bulletMatch ? bulletMatch[1] : numMatch![1]),
|
||||||
|
indent: lineIndent,
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regular text line
|
||||||
|
if (afterHeading) {
|
||||||
|
// This starts the single bullet point after the heading
|
||||||
|
afterHeading = false;
|
||||||
|
pushCurrentItem();
|
||||||
|
currentItem = {
|
||||||
|
type: 'text',
|
||||||
|
content: strippedLine,
|
||||||
|
indent: lineIndent,
|
||||||
|
};
|
||||||
|
} else if (currentItem) {
|
||||||
|
if (currentItem.content.endsWith('\n')) {
|
||||||
|
currentItem.content += strippedLine;
|
||||||
|
} else {
|
||||||
|
currentItem.content += '\n' + strippedLine;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
currentItem = {
|
||||||
|
type: 'text',
|
||||||
|
content: strippedLine,
|
||||||
|
indent: lineIndent,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pushCurrentItem();
|
||||||
|
|
||||||
|
if (parsed.length === 0) return;
|
||||||
|
|
||||||
const line0 = parsed[0];
|
const line0 = parsed[0];
|
||||||
const baseIndent = line0.indent;
|
const baseIndent = line0.indent;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import React from 'react';
|
|||||||
* - `code`
|
* - `code`
|
||||||
* - [label](url)
|
* - [label](url)
|
||||||
*/
|
*/
|
||||||
export function renderInlineMarkdown(text: string): React.ReactNode[] {
|
function renderInlineTokens(text: string): React.ReactNode[] {
|
||||||
if (!text) return [];
|
if (!text) return [];
|
||||||
|
|
||||||
const regex = /(`[^`]+`|\*\*[^*]+\*\*|__[^_]+__|~~[^~]+~~|\*[^*]+\*|_[^_]+_|\[([^\]]+)\]\(([^)]+)\))/g;
|
const regex = /(`[^`]+`|\*\*[^*]+\*\*|__[^_]+__|~~[^~]+~~|\*[^*]+\*|_[^_]+_|\[([^\]]+)\]\(([^)]+)\))/g;
|
||||||
@@ -83,6 +83,55 @@ export function renderInlineMarkdown(text: string): React.ReactNode[] {
|
|||||||
return elements;
|
return elements;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses inline markdown and fenced code blocks:
|
||||||
|
* - ```lang\ncode\n```
|
||||||
|
* - **bold** or __bold__
|
||||||
|
* - *italic* or _italic_
|
||||||
|
* - ~~strikethrough~~
|
||||||
|
* - `code`
|
||||||
|
* - [label](url)
|
||||||
|
*/
|
||||||
|
export function renderInlineMarkdown(text: string): React.ReactNode[] {
|
||||||
|
if (!text) return [];
|
||||||
|
|
||||||
|
if (text.includes('```')) {
|
||||||
|
const codeBlockRegex = /(```[a-zA-Z0-9_-]*\r?\n[\s\S]*?```|```[\s\S]*?```)/g;
|
||||||
|
const parts = text.split(codeBlockRegex);
|
||||||
|
const nodes: React.ReactNode[] = [];
|
||||||
|
|
||||||
|
parts.forEach((part, partIdx) => {
|
||||||
|
if (part.startsWith('```') && part.endsWith('```')) {
|
||||||
|
const lines = part.split(/\r?\n/);
|
||||||
|
const firstLine = lines[0].slice(3).trim();
|
||||||
|
const codeLines = lines.slice(1, lines.length - 1);
|
||||||
|
const code = codeLines.join('\n');
|
||||||
|
nodes.push(
|
||||||
|
<div
|
||||||
|
key={`fenced-${partIdx}`}
|
||||||
|
className="p-2.5 my-2 rounded bg-code text-accent font-mono text-xs overflow-x-auto border border-border/60 select-text"
|
||||||
|
>
|
||||||
|
{firstLine && (
|
||||||
|
<div className="text-[10px] text-faint uppercase font-sans pb-1 select-none">
|
||||||
|
{firstLine}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<pre className="whitespace-pre overflow-x-auto font-mono text-status-ok leading-relaxed">
|
||||||
|
<code>{code}</code>
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (part) {
|
||||||
|
nodes.push(...renderInlineTokens(part));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
return renderInlineTokens(text);
|
||||||
|
}
|
||||||
|
|
||||||
export function MarkdownViewer({ content, className = '' }: { content: string; className?: string }) {
|
export function MarkdownViewer({ content, className = '' }: { content: string; className?: string }) {
|
||||||
if (!content) return null;
|
if (!content) return null;
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user