diff --git a/src/components/editor/components/blocks/code/Code.tsx b/src/components/editor/components/blocks/code/Code.tsx index 27d60088d..adafa0cba 100644 --- a/src/components/editor/components/blocks/code/Code.tsx +++ b/src/components/editor/components/blocks/code/Code.tsx @@ -14,6 +14,7 @@ export const CodeBlock = forwardRef ({ node, children, ...attributes }, ref) => { const { language, handleChangeLanguage } = useCodeBlock(node); const [showToolbar, setShowToolbar] = useState(false); + const isMermaid = language === 'mermaid'; const editor = useSlateStatic(); const readOnly = useReadOnly() || editor.isElementReadOnly(node as unknown as Element); @@ -52,10 +53,18 @@ export const CodeBlock = forwardRef
-            {children}
-            {language === 'mermaid' && (
+            
+              {children}
+            
+            {isMermaid && (
               
                 
               
diff --git a/src/components/editor/parsers/__tests__/block-converters.test.ts b/src/components/editor/parsers/__tests__/block-converters.test.ts
index 268641da3..16c5236c3 100644
--- a/src/components/editor/parsers/__tests__/block-converters.test.ts
+++ b/src/components/editor/parsers/__tests__/block-converters.test.ts
@@ -79,6 +79,7 @@ describe('block-converters', () => {
 
       const block = parseParagraph(node);
 
+      if (Array.isArray(block)) throw new Error('Expected one paragraph block');
       expect(block.type).toBe(BlockType.Paragraph);
       expect(block.text).toBe('Simple paragraph text');
       expect(block.formats).toEqual([]);
@@ -109,6 +110,7 @@ describe('block-converters', () => {
 
       const block = parseParagraph(node);
 
+      if (Array.isArray(block)) throw new Error('Expected one paragraph block');
       expect(block.text).toBe('Text with bold and italic');
       expect(block.formats).toHaveLength(2);
     });
@@ -131,6 +133,7 @@ describe('block-converters', () => {
 
       const block = parseParagraph(node);
 
+      if (Array.isArray(block)) throw new Error('Expected one paragraph block');
       expect(block.text).toBe('Visit our site');
       expect(block.formats).toHaveLength(1);
       expect(block.formats[0]).toMatchObject({
@@ -138,6 +141,101 @@ describe('block-converters', () => {
         data: { href: 'https://example.com' },
       });
     });
+
+    it('should split br-separated clipboard lines into readable paragraphs', () => {
+      const node: HastElement = {
+        type: 'element',
+        tagName: 'div',
+        properties: {},
+        children: [
+          { type: 'text', value: 'Overview' },
+          { type: 'element', tagName: 'br', properties: {}, children: [] },
+          { type: 'text', value: 'Desktop checklist' },
+          { type: 'element', tagName: 'br', properties: {}, children: [] },
+          { type: 'text', value: ' Mobile checklist' },
+        ],
+      };
+
+      const blocks = parseParagraph(node);
+
+      expect(blocks).toEqual([
+        expect.objectContaining({ type: BlockType.Paragraph, text: 'Overview' }),
+        expect.objectContaining({ type: BlockType.Paragraph, text: 'Desktop checklist' }),
+        expect.objectContaining({ type: BlockType.Paragraph, text: ' Mobile checklist' }),
+      ]);
+    });
+
+    it('should split nested div clipboard lines into readable paragraphs', () => {
+      const node: HastElement = {
+        type: 'element',
+        tagName: 'div',
+        properties: {},
+        children: [
+          {
+            type: 'element',
+            tagName: 'div',
+            properties: {},
+            children: [{ type: 'text', value: 'Summary' }],
+          },
+          {
+            type: 'element',
+            tagName: 'div',
+            properties: {},
+            children: [{ type: 'text', value: 'Owner: Alex' }],
+          },
+          {
+            type: 'element',
+            tagName: 'div',
+            properties: {},
+            children: [{ type: 'text', value: 'Status: ready' }],
+          },
+        ],
+      };
+
+      const blocks = parseParagraph(node);
+
+      expect(blocks).toEqual([
+        expect.objectContaining({ type: BlockType.Paragraph, text: 'Summary' }),
+        expect.objectContaining({ type: BlockType.Paragraph, text: 'Owner: Alex' }),
+        expect.objectContaining({ type: BlockType.Paragraph, text: 'Status: ready' }),
+      ]);
+    });
+
+    it('should preserve inline formats when splitting clipboard lines', () => {
+      const node: HastElement = {
+        type: 'element',
+        tagName: 'div',
+        properties: {},
+        children: [
+          {
+            type: 'element',
+            tagName: 'strong',
+            properties: {},
+            children: [{ type: 'text', value: 'Important' }],
+          },
+          { type: 'element', tagName: 'br', properties: {}, children: [] },
+          {
+            type: 'element',
+            tagName: 'em',
+            properties: {},
+            children: [{ type: 'text', value: 'Follow up' }],
+          },
+        ],
+      };
+
+      const blocks = parseParagraph(node);
+
+      expect(blocks).toEqual([
+        expect.objectContaining({
+          text: 'Important',
+          formats: [{ start: 0, end: 9, type: 'bold', data: undefined }],
+        }),
+        expect.objectContaining({
+          text: 'Follow up',
+          formats: [{ start: 0, end: 9, type: 'italic', data: undefined }],
+        }),
+      ]);
+    });
   });
 
   describe('parseCodeBlock', () => {
diff --git a/src/components/editor/parsers/__tests__/paste-fragment-detectors.test.ts b/src/components/editor/parsers/__tests__/paste-fragment-detectors.test.ts
new file mode 100644
index 000000000..fc024fe79
--- /dev/null
+++ b/src/components/editor/parsers/__tests__/paste-fragment-detectors.test.ts
@@ -0,0 +1,271 @@
+import { BlockType } from '@/application/types';
+
+import { parsePlainTextFragments } from '../paste-fragment-detectors';
+
+describe('paste-fragment-detectors', () => {
+  describe('parsePlainTextFragments', () => {
+    const supportedMermaidStarts = [
+      ['sequenceDiagram', 'sequenceDiagram\n    A->>B: Hello'],
+      ['sequenceDiagram-v2', 'sequenceDiagram-v2\n    A->>B: Hello'],
+      ['flowchart TD', 'flowchart TD\n    A --> B'],
+      ['flowchart LR', 'flowchart LR\n    A --> B'],
+      ['graph TD', 'graph TD\n    A --> B'],
+      ['classDiagram', 'classDiagram\n    Animal <|-- Duck'],
+      ['classDiagram-v2', 'classDiagram-v2\n    Animal <|-- Duck'],
+      ['stateDiagram-v2', 'stateDiagram-v2\n    [*] --> Still'],
+      ['erDiagram', 'erDiagram\n    CUSTOMER ||--o{ ORDER : places'],
+      ['journey', 'journey\n    title My working day'],
+      ['gantt', 'gantt\n    title Project timeline'],
+      ['pie', 'pie\n    title Pets'],
+      ['mindmap', 'mindmap\n    root((mindmap))'],
+      ['timeline', 'timeline\n    title History'],
+      ['gitGraph', 'gitGraph\n    commit'],
+      ['quadrantChart', 'quadrantChart\n    title Reach and engagement'],
+      ['requirementDiagram', 'requirementDiagram\n    requirement test_req'],
+      ['C4Context', 'C4Context\n    title System context'],
+      ['C4Container', 'C4Container\n    title Container diagram'],
+      ['C4Component', 'C4Component\n    title Component diagram'],
+      ['C4Dynamic', 'C4Dynamic\n    title Dynamic diagram'],
+      ['C4Deployment', 'C4Deployment\n    title Deployment diagram'],
+      ['packet-beta', 'packet-beta\n    0-15: "Source Port"'],
+      ['block-beta', 'block-beta\n    columns 3'],
+      ['architecture-beta', 'architecture-beta\n    group api(cloud)[API]'],
+      ['xychart-beta', 'xychart-beta\n    title "Sales Revenue"'],
+      ['sankey-beta', 'sankey-beta\n    source,target,value'],
+    ] as const;
+
+    it('returns null when no special fragment is detected', () => {
+      const blocks = parsePlainTextFragments('First pasted line\nSecond pasted line');
+
+      expect(blocks).toBeNull();
+    });
+
+    it.each(supportedMermaidStarts)('detects supported Mermaid start: %s', (_name, diagram) => {
+      const blocks = parsePlainTextFragments(diagram);
+
+      expect(blocks).toHaveLength(1);
+      expect(blocks?.[0]).toMatchObject({
+        type: BlockType.CodeBlock,
+        data: { language: 'mermaid' },
+      });
+      expect(blocks?.[0].text).toBe(diagram);
+    });
+
+    it('detects an unfenced Mermaid sequence diagram', () => {
+      const blocks = parsePlainTextFragments(`
+sequenceDiagram
+    participant Client as Browser client
+    participant Server as Application server
+    Client->>Server: send request
+      `.trim());
+
+      expect(blocks).toHaveLength(1);
+      expect(blocks?.[0]).toMatchObject({
+        type: BlockType.CodeBlock,
+        data: { language: 'mermaid' },
+      });
+      expect(blocks?.[0].text).toContain('sequenceDiagram');
+      expect(blocks?.[0].text).toContain('Client->>Server');
+    });
+
+    it('keeps one simulated sequence diagram across blank lines inside the diagram', () => {
+      const blocks = parsePlainTextFragments(`
+1. Simulated Async Handoff
+
+sequenceDiagram
+    participant Client as Browser client
+    participant Server as Application server
+    participant Worker as Background worker
+
+    Client->>Server: submit request
+    Server->>Worker: assign task
+
+    Worker-->>Server: report completion
+    Note over Server,Worker: status is shown in dashboard
+
+After diagram
+      `.trim());
+
+      expect(blocks?.map((block) => block.type)).toEqual([
+        BlockType.NumberedListBlock,
+        BlockType.CodeBlock,
+        BlockType.Paragraph,
+      ]);
+      expect(blocks?.[1].data).toEqual({ language: 'mermaid' });
+      expect(blocks?.[1].text).toContain('Client->>Server');
+      expect(blocks?.[1].text).toContain('Worker-->>Server');
+      expect(blocks?.[1].text).toContain('Note over Server,Worker');
+      expect(blocks?.[2].text).toBe('After diagram');
+    });
+
+    it('splits mixed prose and Mermaid fragments into extensible parsed blocks', () => {
+      const blocks = parsePlainTextFragments(`
+sequenceDiagram
+    participant Client as Browser client
+    participant Server as Application server
+    Client->>Server: send request
+
+Context: this paragraph should stay outside the diagram.
+
+Next shape:
+
+flowchart TD
+    A[Start] --> B{Continue?}
+    B -- No --> C[Stop]
+      `.trim());
+
+      expect(blocks?.map((block) => block.type)).toEqual([
+        BlockType.CodeBlock,
+        BlockType.Paragraph,
+        BlockType.Paragraph,
+        BlockType.CodeBlock,
+      ]);
+      expect(blocks?.[0].data).toEqual({ language: 'mermaid' });
+      expect(blocks?.[1].text).toBe('Context: this paragraph should stay outside the diagram.');
+      expect(blocks?.[2].text).toBe('Next shape:');
+      expect(blocks?.[3].data).toEqual({ language: 'mermaid' });
+      expect(blocks?.[3].text).toContain('flowchart TD');
+    });
+
+    it('supports prose before and after an unfenced Mermaid fragment', () => {
+      const blocks = parsePlainTextFragments(`
+Before diagram
+
+flowchart TD
+    A[Start] --> B{Continue?}
+
+After diagram
+      `.trim());
+
+      expect(blocks?.map((block) => block.type)).toEqual([
+        BlockType.Paragraph,
+        BlockType.CodeBlock,
+        BlockType.Paragraph,
+      ]);
+      expect(blocks?.[0].text).toBe('Before diagram');
+      expect(blocks?.[1].data).toEqual({ language: 'mermaid' });
+      expect(blocks?.[2].text).toBe('After diagram');
+    });
+
+    it('keeps Markdown prose chunks when a Mermaid fragment is present', () => {
+      const blocks = parsePlainTextFragments(`
+## Simulated Flow
+
+flowchart TD
+    A --> B
+
+- first follow-up item
+- second follow-up item
+      `.trim());
+
+      expect(blocks?.map((block) => block.type)).toEqual([
+        BlockType.HeadingBlock,
+        BlockType.CodeBlock,
+        BlockType.BulletedListBlock,
+        BlockType.BulletedListBlock,
+      ]);
+      expect(blocks?.[0].text).toBe('Simulated Flow');
+      expect(blocks?.[1].data).toEqual({ language: 'mermaid' });
+      expect(blocks?.[2].text).toBe('first follow-up item');
+      expect(blocks?.[3].text).toBe('second follow-up item');
+    });
+
+    it('keeps TSV chunks when a Mermaid fragment is present', () => {
+      const blocks = parsePlainTextFragments(
+        'Name\tStatus\nExample\tReady\n\nsequenceDiagram\n    Client->>Server: send request'
+      );
+
+      expect(blocks?.map((block) => block.type)).toEqual([
+        BlockType.SimpleTableBlock,
+        BlockType.CodeBlock,
+      ]);
+      expect(blocks?.[1].data).toEqual({ language: 'mermaid' });
+    });
+
+    it('keeps fenced code chunks when an unfenced Mermaid fragment is present', () => {
+      const blocks = parsePlainTextFragments(`
+\`\`\`mermaid
+sequenceDiagram
+    Client->>Server: send request
+\`\`\`
+
+flowchart TD
+    A --> B
+      `.trim());
+
+      expect(blocks?.map((block) => block.type)).toEqual([
+        BlockType.CodeBlock,
+        BlockType.CodeBlock,
+      ]);
+      expect(blocks?.[0].data).toEqual({ language: 'mermaid' });
+      expect(blocks?.[0].text).toContain('sequenceDiagram');
+      expect(blocks?.[1].data).toEqual({ language: 'mermaid' });
+      expect(blocks?.[1].text).toContain('flowchart TD');
+    });
+
+    it('does not treat a lone Mermaid keyword as a diagram', () => {
+      const blocks = parsePlainTextFragments('sequenceDiagram');
+
+      expect(blocks).toBeNull();
+    });
+
+    it('does not treat prose mentioning a Mermaid keyword as a diagram', () => {
+      const blocks = parsePlainTextFragments(
+        'The flowchart TD direction is useful when writing Mermaid diagrams.'
+      );
+
+      expect(blocks).toBeNull();
+    });
+
+    it('dedents Mermaid fragments copied with shared indentation', () => {
+      const blocks = parsePlainTextFragments(`
+        flowchart TD
+          A[Start] --> B{Continue?}
+          B --> C[Done]
+      `);
+
+      expect(blocks).toHaveLength(1);
+      expect(blocks?.[0].text.startsWith('flowchart TD')).toBe(true);
+      expect(blocks?.[0].text).toContain('  A[Start]');
+    });
+
+    it('supports Mermaid init directives before the diagram start', () => {
+      const blocks = parsePlainTextFragments(`
+%%{init: {"theme": "base"}}%%
+flowchart LR
+    A --> B
+      `.trim());
+
+      expect(blocks).toHaveLength(1);
+      expect(blocks?.[0].data).toEqual({ language: 'mermaid' });
+      expect(blocks?.[0].text).toContain('%%{init');
+      expect(blocks?.[0].text).toContain('flowchart LR');
+    });
+
+    it('supports Mermaid comments before the diagram start', () => {
+      const blocks = parsePlainTextFragments(`
+%% Paste source: synthetic fixture
+flowchart LR
+    A --> B
+      `.trim());
+
+      expect(blocks).toHaveLength(1);
+      expect(blocks?.[0].data).toEqual({ language: 'mermaid' });
+      expect(blocks?.[0].text).toContain('%% Paste source');
+      expect(blocks?.[0].text).toContain('flowchart LR');
+    });
+
+    it('splits fragments with Windows line endings', () => {
+      const blocks = parsePlainTextFragments(
+        'flowchart TD\r\n    A --> B\r\n\r\nAfter diagram'
+      );
+
+      expect(blocks?.map((block) => block.type)).toEqual([
+        BlockType.CodeBlock,
+        BlockType.Paragraph,
+      ]);
+      expect(blocks?.[0].text).toContain('A --> B');
+      expect(blocks?.[1].text).toBe('After diagram');
+    });
+  });
+});
diff --git a/src/components/editor/parsers/block-converters.ts b/src/components/editor/parsers/block-converters.ts
index a5c99e623..52d12b69c 100644
--- a/src/components/editor/parsers/block-converters.ts
+++ b/src/components/editor/parsers/block-converters.ts
@@ -7,6 +7,234 @@ import { ParsedBlock } from './types';
 
 import type { Element as HastElement } from 'hast';
 
+type ActiveInlineFormat = Pick;
+
+type ParagraphSegment = {
+  text: string;
+  formats: ParsedBlock['formats'];
+};
+
+const PARAGRAPH_BOUNDARY_TAGS = new Set([
+  'address',
+  'article',
+  'aside',
+  'blockquote',
+  'dd',
+  'details',
+  'dialog',
+  'div',
+  'dl',
+  'dt',
+  'fieldset',
+  'figcaption',
+  'figure',
+  'footer',
+  'form',
+  'h1',
+  'h2',
+  'h3',
+  'h4',
+  'h5',
+  'h6',
+  'header',
+  'li',
+  'main',
+  'nav',
+  'ol',
+  'p',
+  'pre',
+  'section',
+  'table',
+  'ul',
+]);
+
+function parseInlineStyle(style: string): Record {
+  const styles: Record = {};
+
+  style.split(';').forEach((part) => {
+    const [key, value] = part.split(':');
+
+    if (key && value) {
+      styles[key.trim().toLowerCase()] = value.trim().toLowerCase();
+    }
+  });
+
+  return styles;
+}
+
+function addActiveFormat(formats: ActiveInlineFormat[], format: ActiveInlineFormat) {
+  const exists = formats.some((item) => {
+    return item.type === format.type && JSON.stringify(item.data ?? {}) === JSON.stringify(format.data ?? {});
+  });
+
+  if (!exists) {
+    formats.push(format);
+  }
+}
+
+function getElementFormats(node: HastElement, activeFormats: ActiveInlineFormat[]): ActiveInlineFormat[] {
+  const formats = [...activeFormats];
+  const style = node.properties?.style as string | undefined;
+
+  if (style) {
+    const styles = parseInlineStyle(style);
+    const weight = styles['font-weight'];
+
+    if (weight && (weight === 'bold' || weight === 'bolder' || parseInt(weight) >= 700)) {
+      addActiveFormat(formats, { type: 'bold' });
+    }
+
+    if (styles['font-style'] === 'italic' || styles['font-style'] === 'oblique') {
+      addActiveFormat(formats, { type: 'italic' });
+    }
+
+    const decoration = styles['text-decoration'];
+
+    if (decoration?.includes('underline')) {
+      addActiveFormat(formats, { type: 'underline' });
+    }
+
+    if (decoration?.includes('line-through')) {
+      addActiveFormat(formats, { type: 'strikethrough' });
+    }
+
+    if (styles.color) {
+      addActiveFormat(formats, { type: 'color', data: { color: styles.color } });
+    }
+
+    if (styles['background-color']) {
+      addActiveFormat(formats, { type: 'bgColor', data: { bgColor: styles['background-color'] } });
+    }
+  }
+
+  switch (node.tagName) {
+    case 'strong':
+    case 'b':
+      addActiveFormat(formats, { type: 'bold' });
+      break;
+    case 'em':
+    case 'i':
+      addActiveFormat(formats, { type: 'italic' });
+      break;
+    case 'u':
+      addActiveFormat(formats, { type: 'underline' });
+      break;
+    case 's':
+    case 'strike':
+    case 'del':
+      addActiveFormat(formats, { type: 'strikethrough' });
+      break;
+    case 'code':
+      addActiveFormat(formats, { type: 'code' });
+      break;
+    case 'a': {
+      const href = node.properties?.href as string | undefined;
+
+      if (href) {
+        addActiveFormat(formats, { type: 'link', data: { href } });
+      }
+
+      break;
+    }
+  }
+
+  return formats;
+}
+
+function removeInvisiblePasteMarkers(text: string): string {
+  return text.replace(/\uFEFF/g, '');
+}
+
+function extractParagraphSegments(node: HastElement): ParagraphSegment[] {
+  const segments: ParagraphSegment[] = [];
+  let current: ParagraphSegment = { text: '', formats: [] };
+
+  const pushSegment = () => {
+    if (current.text.trim().length > 0) {
+      segments.push(current);
+    }
+
+    current = { text: '', formats: [] };
+  };
+
+  const appendText = (value: string, activeFormats: ActiveInlineFormat[]) => {
+    const text = removeInvisiblePasteMarkers(value);
+
+    if (text.length === 0) return;
+
+    const start = current.text.length;
+    const end = start + text.length;
+
+    current.text += text;
+
+    activeFormats.forEach((format) => {
+      current.formats.push({
+        start,
+        end,
+        type: format.type,
+        data: format.data,
+      });
+    });
+  };
+
+  const walkNode = (child: HastElement | { type: string; value?: string }, activeFormats: ActiveInlineFormat[], isRoot = false) => {
+    if (child.type === 'text') {
+      appendText(child.value ?? '', activeFormats);
+      return;
+    }
+
+    if (child.type !== 'element') return;
+
+    const element = child as HastElement;
+
+    if (element.tagName === 'br') {
+      pushSegment();
+      return;
+    }
+
+    const isNestedBoundary = !isRoot && PARAGRAPH_BOUNDARY_TAGS.has(element.tagName);
+
+    if (isNestedBoundary) {
+      pushSegment();
+    }
+
+    const nextFormats = getElementFormats(element, activeFormats);
+
+    element.children.forEach((nestedChild) => {
+      walkNode(nestedChild as HastElement | { type: string; value?: string }, nextFormats);
+    });
+
+    if (isNestedBoundary) {
+      pushSegment();
+    }
+  };
+
+  walkNode(node, [], true);
+  pushSegment();
+
+  return segments;
+}
+
+function buildParagraphBlock(text: string, formats: ParsedBlock['formats']): ParsedBlock {
+  if (text === '---') {
+    return {
+      type: BlockType.DividerBlock,
+      data: {},
+      text: '',
+      formats: [],
+      children: [],
+    };
+  }
+
+  return {
+    type: BlockType.Paragraph,
+    data: {},
+    text,
+    formats,
+    children: [],
+  };
+}
+
 /**
  * Checks if a HAST element represents a heading
  */
@@ -67,27 +295,20 @@ export function parseHeading(node: HastElement): ParsedBlock {
 /**
  * Converts a paragraph element to ParsedBlock
  */
-export function parseParagraph(node: HastElement): ParsedBlock {
-  const text = extractTextFromHAST(node);
+export function parseParagraph(node: HastElement): ParsedBlock | ParsedBlock[] {
+  const segments = extractParagraphSegments(node);
 
-  // Check for special markdown-like patterns
-  if (text === '---') {
-    return {
-      type: BlockType.DividerBlock,
-      data: {},
-      text: '',
-      formats: [],
-      children: [],
-    };
+  if (segments.length > 1) {
+    return segments.map((segment) => buildParagraphBlock(segment.text, segment.formats));
   }
 
-  return {
-    type: BlockType.Paragraph,
-    data: {},
-    text,
-    formats: extractInlineFormatsFromHAST(node),
-    children: [],
-  };
+  if (segments.length === 1) {
+    return buildParagraphBlock(segments[0].text, segments[0].formats);
+  }
+
+  const text = removeInvisiblePasteMarkers(extractTextFromHAST(node));
+
+  return buildParagraphBlock(text, extractInlineFormatsFromHAST(node));
 }
 
 /**
diff --git a/src/components/editor/parsers/paste-fragment-detectors.ts b/src/components/editor/parsers/paste-fragment-detectors.ts
new file mode 100644
index 000000000..fdaf9cea9
--- /dev/null
+++ b/src/components/editor/parsers/paste-fragment-detectors.ts
@@ -0,0 +1,247 @@
+import { BlockType, CodeBlockData } from '@/application/types';
+import { detectMarkdown, detectTSV } from '@/components/editor/utils/markdown-detector';
+
+import { parseMarkdown } from './markdown-parser';
+import { parseTSVTable } from './table-parser';
+import { ParsedBlock } from './types';
+
+type PasteChunk = {
+  text: string;
+};
+
+type PasteFragmentMatch = {
+  blocks: ParsedBlock[];
+  consumedChunks: number;
+};
+
+type PasteFragmentDetector = {
+  id: string;
+  parse: (chunks: PasteChunk[], startIndex: number) => PasteFragmentMatch | null;
+};
+
+const MERMAID_LANGUAGE = 'mermaid';
+
+const MERMAID_START_PATTERNS = [
+  /^sequenceDiagram(?:-v2)?\b/i,
+  /^flowchart\s+(?:TB|TD|BT|RL|LR)\b/i,
+  /^graph\s+(?:TB|TD|BT|RL|LR)\b/i,
+  /^classDiagram(?:-v2)?\b/i,
+  /^stateDiagram(?:-v2)?\b/i,
+  /^erDiagram\b/i,
+  /^journey\b/i,
+  /^gantt\b/i,
+  /^pie\b/i,
+  /^mindmap\b/i,
+  /^timeline\b/i,
+  /^gitGraph\b/i,
+  /^quadrantChart\b/i,
+  /^requirementDiagram\b/i,
+  /^C4(?:Context|Container|Component|Dynamic|Deployment)\b/,
+  /^(?:packet|block|architecture|xychart|sankey)-beta\b/i,
+];
+
+const fragmentDetectors: PasteFragmentDetector[] = [
+  {
+    id: MERMAID_LANGUAGE,
+    parse: parseMermaidFragment,
+  },
+];
+
+export function parsePlainTextFragments(text: string): ParsedBlock[] | null {
+  const chunks = splitPasteChunks(text);
+
+  if (!chunks.some((_, index) => parseFragmentChunk(chunks, index))) {
+    return null;
+  }
+
+  const blocks: ParsedBlock[] = [];
+  let detectedFragment = false;
+
+  for (let index = 0; index < chunks.length;) {
+    const parsedFragment = parseFragmentChunk(chunks, index);
+
+    if (parsedFragment) {
+      detectedFragment = true;
+      blocks.push(...parsedFragment.blocks);
+      index += parsedFragment.consumedChunks;
+      continue;
+    }
+
+    const chunk = chunks[index];
+
+    blocks.push(...parsePlainTextChunk(chunk.text));
+    index += 1;
+  }
+
+  return detectedFragment && blocks.length > 0 ? blocks : null;
+}
+
+function parseFragmentChunk(chunks: PasteChunk[], startIndex: number): PasteFragmentMatch | null {
+  for (const detector of fragmentDetectors) {
+    const match = detector.parse(chunks, startIndex);
+
+    if (match) return match;
+  }
+
+  return null;
+}
+
+function parseMermaidFragment(chunks: PasteChunk[], startIndex: number): PasteFragmentMatch | null {
+  const firstChunk = chunks[startIndex];
+  const firstDiagram = normalizeFragmentText(firstChunk.text);
+  const firstLines = getNonEmptyLines(firstDiagram);
+
+  if (firstLines.length === 0) return null;
+
+  const firstDiagramLine = getFirstDiagramLine(firstLines);
+
+  if (!firstDiagramLine || !isMermaidStartLine(firstDiagramLine)) return null;
+
+  const diagramChunks = [firstChunk.text];
+  let consumedChunks = 1;
+
+  for (let index = startIndex + 1; index < chunks.length; index += 1) {
+    const nextChunk = chunks[index];
+
+    if (!isMermaidContinuationChunk(nextChunk.text)) break;
+
+    diagramChunks.push(nextChunk.text);
+    consumedChunks += 1;
+  }
+
+  const diagram = normalizeFragmentText(diagramChunks.join('\n\n'));
+  const lines = getNonEmptyLines(diagram);
+
+  if (lines.length === 0) return null;
+
+  const isMultiLineDiagram = lines.length > 1;
+  const isInlineDiagram = /;/.test(firstDiagramLine);
+
+  if (!isMultiLineDiagram && !isInlineDiagram) return null;
+
+  return {
+    consumedChunks,
+    blocks: [
+      {
+        type: BlockType.CodeBlock,
+        data: { language: MERMAID_LANGUAGE } as CodeBlockData,
+        text: diagram,
+        formats: [],
+        children: [],
+      },
+    ],
+  };
+}
+
+function getFirstDiagramLine(lines: string[]): string | null {
+  const first = lines.find((line) => !isMermaidPreambleLine(line.trim()))?.trim();
+
+  return first ?? null;
+}
+
+function isMermaidStartLine(line: string): boolean {
+  return MERMAID_START_PATTERNS.some((pattern) => pattern.test(line.trim()));
+}
+
+function isMermaidPreambleLine(line: string): boolean {
+  return /^%%(?:\{[\s\S]*\}%%|.*)$/.test(line);
+}
+
+function isMermaidContinuationChunk(text: string): boolean {
+  const lines = getNonEmptyLines(normalizeFragmentText(text));
+
+  if (lines.length === 0) return false;
+
+  return lines.every((line) => isMermaidContinuationLine(line.trim()));
+}
+
+function isMermaidContinuationLine(line: string): boolean {
+  if (isMermaidPreambleLine(line)) return true;
+
+  return [
+    /^participant\s+\S+(?:\s+as\s+.+)?$/i,
+    /^actor\s+\S+(?:\s+as\s+.+)?$/i,
+    /^create\s+(?:participant|actor)\s+\S+(?:\s+as\s+.+)?$/i,
+    /^destroy\s+\S+$/i,
+    /^autonumber(?:\s+.*)?$/i,
+    /^box(?:\s+.*)?$/i,
+    /^end$/i,
+    /^(?:activate|deactivate)\s+\S+$/i,
+    /^(?:loop|alt|else|opt|par|and|critical|option|break|rect)(?:\s+.*)?$/i,
+    /^Note\s+(?:left of|right of|over)\s+[^:]+:.+$/i,
+    /^[\w.$()[\]{}<>"'\-/ ]+\s*(?:-+|=+|x-?|o-?)(?:>>|>|\))[\w.$()[\]{}<>"'\-/ ]*(?::.*)?$/i,
+    /^(?:subgraph|direction|classDef|class|style|linkStyle|click|accTitle|accDescr)(?:\s+.*)?$/i,
+    /^[\w.-]+(?:\[[^\]]+\]|\([^)]+\)|\{[^}]+\}|>[^]]+\])(?:\s*:::\w+)?$/i,
+    /^[\w.$()[\]{}<>"'\-/ ]+\s*(?:-{2,}|={2,}|-\.|\.->|~~~|o--|x--).+$/i,
+  ].some((pattern) => pattern.test(line));
+}
+
+function parsePlainTextChunk(text: string): ParsedBlock[] {
+  const normalizedText = text.trim();
+
+  if (!normalizedText) return [];
+
+  if (detectMarkdown(normalizedText)) {
+    const markdownBlocks = parseMarkdown(normalizedText);
+
+    if (markdownBlocks.length > 0) return markdownBlocks;
+  }
+
+  if (detectTSV(normalizedText)) {
+    const table = parseTSVTable(normalizedText);
+
+    if (table) return [table];
+  }
+
+  return normalizedText
+    .split(/\r\n|\r|\n/)
+    .map((line) => line.trim())
+    .filter(Boolean)
+    .map((line) => ({
+      type: BlockType.Paragraph,
+      data: {},
+      text: line,
+      formats: [],
+      children: [],
+    }));
+}
+
+function splitPasteChunks(text: string): PasteChunk[] {
+  return normalizeLineEndings(text)
+    .split(/\n[ \t]*\n+/)
+    .map((chunk) => ({ text: chunk }))
+    .filter((chunk) => chunk.text.trim().length > 0);
+}
+
+function normalizeLineEndings(text: string): string {
+  return text.replace(/\r\n?/g, '\n');
+}
+
+function normalizeFragmentText(text: string): string {
+  return dedent(trimOuterBlankLines(text));
+}
+
+function trimOuterBlankLines(text: string): string {
+  return text
+    .replace(/^(?:[ \t]*\r?\n)+/, '')
+    .replace(/(?:\r?\n[ \t]*)+$/, '');
+}
+
+function dedent(text: string): string {
+  const lines = text.split(/\r\n|\r|\n/);
+  const indents = lines
+    .filter((line) => line.trim().length > 0)
+    .map((line) => line.match(/^[ \t]*/)?.[0].length ?? 0);
+
+  if (indents.length === 0) return text;
+
+  const minIndent = Math.min(...indents);
+
+  if (minIndent === 0) return text;
+
+  return lines.map((line) => line.slice(minIndent)).join('\n');
+}
+
+function getNonEmptyLines(text: string): string[] {
+  return text.split(/\r\n|\r|\n/).filter((line) => line.trim().length > 0);
+}
diff --git a/src/components/editor/plugins/withPasted.ts b/src/components/editor/plugins/withPasted.ts
index 2bd455985..33aa5c319 100644
--- a/src/components/editor/plugins/withPasted.ts
+++ b/src/components/editor/plugins/withPasted.ts
@@ -10,6 +10,7 @@ import { assertDocExists, getBlock, getChildrenArray, getText } from '@/applicat
 import { BlockType, LinkPreviewBlockData, MentionType, VideoBlockData, VideoType, YjsEditorKey } from '@/application/types';
 import { parseHTML } from '@/components/editor/parsers/html-parser';
 import { parseMarkdown } from '@/components/editor/parsers/markdown-parser';
+import { parsePlainTextFragments } from '@/components/editor/parsers/paste-fragment-detectors';
 import { parseTSVTable } from '@/components/editor/parsers/table-parser';
 import { ParsedBlock } from '@/components/editor/parsers/types';
 import { detectMarkdown, detectTSV } from '@/components/editor/utils/markdown-detector';
@@ -314,6 +315,12 @@ function handlePlainTextPaste(editor: ReactEditor, text: string): boolean {
     return false;
   }
 
+  const fragmentBlocks = parsePlainTextFragments(text);
+
+  if (fragmentBlocks) {
+    return insertParsedBlocks(editor, fragmentBlocks);
+  }
+
   // Multi-line text: Check if it's Markdown
   if (detectMarkdown(text)) {
     return handleMarkdownPaste(editor, text);
@@ -424,7 +431,8 @@ function handleURLPaste(editor: ReactEditor, url: string): boolean {
  */
 function handleMultiLinePlainText(editor: ReactEditor, lines: string[]): boolean {
   const blocks = lines
-    .filter(Boolean)
+    .map((line) => line.replace(/\uFEFF/g, ''))
+    .filter((line) => line.trim().length > 0)
     .map((line) => ({
       type: BlockType.Paragraph,
       data: {},
diff --git a/src/components/editor/shortcut.hooks.ts b/src/components/editor/shortcut.hooks.ts
index 48c184f71..05d09f71c 100644
--- a/src/components/editor/shortcut.hooks.ts
+++ b/src/components/editor/shortcut.hooks.ts
@@ -224,11 +224,14 @@ export function useShortcuts(editor: ReactEditor) {
          * Special case for select all in code block: Only select all text in code block
          */
         case createHotkey(HOT_KEY_NAME.SELECT_ALL)(e):
+          event.preventDefault();
+
           if (node && node[0].type === BlockType.CodeBlock) {
-            event.preventDefault();
             editor.select(node[1]);
+            break;
           }
 
+          editor.select(Editor.range(editor, []));
           break;
         /**
          * Indent block: Tab
diff --git a/src/components/view-meta/TitleEditable.tsx b/src/components/view-meta/TitleEditable.tsx
index 2a96571c7..3dcc34b6f 100644
--- a/src/components/view-meta/TitleEditable.tsx
+++ b/src/components/view-meta/TitleEditable.tsx
@@ -2,6 +2,7 @@ import { debounce } from 'lodash-es';
 import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
 import { useTranslation } from 'react-i18next';
 
+import { createHotkey, HOT_KEY_NAME } from '@/utils/hotkeys';
 import { Log } from '@/utils/log';
 
 /**
@@ -61,6 +62,15 @@ const setCursorPosition = (element: HTMLDivElement, position: number) => {
   selection?.addRange(range);
 };
 
+const selectContentEditableText = (element: HTMLDivElement) => {
+  const range = document.createRange();
+  const selection = window.getSelection();
+
+  range.selectNodeContents(element);
+  selection?.removeAllRanges();
+  selection?.addRange(range);
+};
+
 function TitleEditable({
   viewId,
   name,
@@ -277,6 +287,13 @@ function TitleEditable({
     
     lastInputTimeRef.current = Date.now();
 
+    if (createHotkey(HOT_KEY_NAME.SELECT_ALL)(e.nativeEvent)) {
+      e.preventDefault();
+      e.stopPropagation();
+      selectContentEditableText(contentRef.current);
+      return;
+    }
+
     if (e.key === 'Enter' || e.key === 'Escape') {
       e.preventDefault();
       
@@ -356,4 +373,4 @@ function TitleEditable({
   );
 }
 
-export default memo(TitleEditable);
\ No newline at end of file
+export default memo(TitleEditable);
diff --git a/src/components/view-meta/__tests__/TitleEditable.test.tsx b/src/components/view-meta/__tests__/TitleEditable.test.tsx
new file mode 100644
index 000000000..05aaf6b94
--- /dev/null
+++ b/src/components/view-meta/__tests__/TitleEditable.test.tsx
@@ -0,0 +1,39 @@
+import { fireEvent, render, screen } from '@testing-library/react';
+
+import TitleEditable from '../TitleEditable';
+
+jest.mock('react-i18next', () => ({
+  useTranslation: () => ({
+    t: (key: string) => key,
+  }),
+}));
+
+describe('TitleEditable', () => {
+  it('keeps select-all inside the title instead of bubbling to page selection', () => {
+    const parentKeyDown = jest.fn();
+
+    render(
+      
+ +
+ ); + + const title = screen.getByTestId('page-title-input'); + + fireEvent.keyDown(title, { + key: 'a', + code: 'KeyA', + keyCode: 65, + which: 65, + ctrlKey: true, + }); + + expect(parentKeyDown).not.toHaveBeenCalled(); + expect(window.getSelection()?.toString()).toBe('Synthetic title'); + }); +});