// wysiwyg-lite.jsx — line-first Typora-style Markdown WYSIWYG surface.
// Markdown remains the source of truth; this layer edits rendered-looking lines
// and writes deterministic Markdown back to the document.

const { forwardRef, useImperativeHandle } = React;

function splitSourceLines(source) {
  const src = String(source || '');
  return src === '' ? [''] : src.split('\n');
}

function joinSourceLines(lines) {
  return (lines && lines.length ? lines : ['']).join('\n');
}

function focusTextInput(input, pos) {
  if (!input) return;
  input.focus();
  const text = input.value != null ? input.value : (input.textContent || '');
  const end = text.length;
  const next = pos == null ? end : Math.max(0, Math.min(pos, end));
  if (input.setSelectionRange) {
    try { input.setSelectionRange(next, next); } catch (_) {}
    return;
  }
  setEditableSelection(input, next, next);
}

function autoSizeTextarea(ta) {
  if (!ta) return;
  ta.style.height = 'auto';
  ta.style.height = Math.max(26, ta.scrollHeight + 2) + 'px';
}

function renderInlineMarkdown(markdown) {
  const source = normalizeInlineMathForRender(String(markdown || ''));
  if (!source) return '';
  if (!window.MD) {
    return source.replace(/&/g, '&amp;').replace(/</g, '&lt;');
  }
  const box = document.createElement('div');
  box.innerHTML = window.MD.render(source);
  const first = box.firstElementChild;
  return first ? first.innerHTML : box.innerHTML;
}

function normalizeInlineMathForRender(source) {
  return String(source || '').replace(/(^|[^\\$])\$([^\n$]+?)\$/g, (match, pre, tex, offset, full) => {
    const before = pre.slice(-1);
    const after = full[offset + match.length] || '';
    const needsLeftSpace = before && /[\w\u4e00-\u9fff]/.test(before);
    const needsRightSpace = after && /[\w\u4e00-\u9fff]/.test(after);
    return `${pre}${needsLeftSpace ? ' ' : ''}$${tex}$${needsRightSpace ? ' ' : ''}`;
  });
}

function stripMarkdownFormatting(text) {
  let cleaned = String(text || '');
  let prev = null;
  while (cleaned !== prev) {
    prev = cleaned;
    cleaned = cleaned
      .replace(/!\[([^\]]*)\]\([^)]+\)/g, '$1')
      .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
      .replace(/<span[^>]*>([\s\S]*?)<\/span>/gi, '$1')
      .replace(/<mark[^>]*>([\s\S]*?)<\/mark>/gi, '$1')
      .replace(/<\/?u>/gi, '')
      .replace(/\*\*([^*]+)\*\*/g, '$1')
      .replace(/__([^_]+)__/g, '$1')
      .replace(/\*([^*]+)\*/g, '$1')
      .replace(/_([^_]+)_/g, '$1')
      .replace(/~~([^~]+)~~/g, '$1')
      .replace(/`([^`]+)`/g, '$1')
      .replace(/\$([^$]+)\$/g, '$1')
      .replace(/<[^>]+>/g, '');
  }
  return cleaned;
}

function editableText(node) {
  return String(node?.textContent || '').replace(/\u00a0/g, ' ');
}

function inlineMarkdownFromNode(node) {
  if (!node) return '';
  if (node.nodeType === Node.TEXT_NODE) return String(node.nodeValue || '').replace(/\u00a0/g, ' ');
  if (node.nodeType !== Node.ELEMENT_NODE) return '';
  const tag = node.tagName;
  if (tag === 'BR') return '\n';
  const inner = Array.from(node.childNodes).map(inlineMarkdownFromNode).join('');
  if (tag === 'STRONG' || tag === 'B') return `**${inner}**`;
  if (tag === 'EM' || tag === 'I') return `*${inner}*`;
  if (tag === 'CODE') return `\`${inner}\``;
  if (tag === 'U') return `<u>${inner}</u>`;
  if (tag === 'DEL' || tag === 'S') return `~~${inner}~~`;
  if (tag === 'A') {
    const href = node.getAttribute('href') || '';
    return href ? `[${inner || href}](${href})` : inner;
  }
  if (tag === 'IMG') {
    const src = node.getAttribute('src') || '';
    const alt = node.getAttribute('alt') || 'image';
    return src ? `![${alt}](${src})` : alt;
  }
  if (tag === 'SPAN') {
    const style = node.getAttribute('style') || '';
    const cm = style.match(/color:\s*([^;"]+)/);
    if (cm) return `<span style="color:${cm[1].trim()}">${inner}</span>`;
    return inner;
  }
  if (tag === 'MARK') {
    const style = node.getAttribute('style') || '';
    const bm = style.match(/background:\s*([^;"]+)/);
    if (bm) return `<mark style="background:${bm[1].trim()}">${inner}</mark>`;
    return inner;
  }
  if (tag === 'P' || tag === 'DIV') return inner;
  return inner;
}

function editableSelectionOffsets(root) {
  const sel = window.getSelection();
  if (!root || !sel || sel.rangeCount === 0) return { start: 0, end: 0 };
  const range = sel.getRangeAt(0);
  if (!root.contains(range.startContainer) || !root.contains(range.endContainer)) {
    const len = editableText(root).length;
    return { start: len, end: len };
  }
  const pre = range.cloneRange();
  pre.selectNodeContents(root);
  pre.setEnd(range.startContainer, range.startOffset);
  const start = pre.toString().length;
  return { start, end: start + range.toString().length };
}

function setEditableSelection(root, start, end = start) {
  if (!root) return;
  const range = document.createRange();
  const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
  let node = walker.nextNode();
  let offset = 0;
  let startNode = root;
  let startOffset = 0;
  let endNode = root;
  let endOffset = 0;
  while (node) {
    const nextOffset = offset + node.nodeValue.length;
    if (start >= offset && start <= nextOffset) {
      startNode = node;
      startOffset = start - offset;
    }
    if (end >= offset && end <= nextOffset) {
      endNode = node;
      endOffset = end - offset;
      break;
    }
    offset = nextOffset;
    node = walker.nextNode();
  }
  if (!root.textContent) {
    startNode = root;
    startOffset = 0;
    endNode = root;
    endOffset = 0;
  }
  range.setStart(startNode, startOffset);
  range.setEnd(endNode, endOffset);
  const sel = window.getSelection();
  sel.removeAllRanges();
  sel.addRange(range);
}

function lineView(rawLine) {
  const raw = String(rawLine || '');
  if (/^\s*---+\s*$/.test(raw)) return { kind: 'rule', text: raw.trim(), prefix: '' };
  let m = raw.match(/^(#{1,6})\s*(.*)$/);
  if (m) return { kind: 'heading', level: m[1].length, text: m[2] || '', prefix: `${m[1]} ` };

  m = raw.match(/^(\s*)>\s?(.*)$/);
  if (m) return { kind: 'quote', indent: m[1] || '', text: m[2] || '', prefix: `${m[1]}> ` };

  m = raw.match(/^(\s*)[-*+]\s+\[([ xX])\]\s*(.*)$/);
  if (m) return { kind: 'task', indent: m[1] || '', checked: /x/i.test(m[2]), text: m[3] || '', prefix: `${m[1]}- [${/x/i.test(m[2]) ? 'x' : ' '}] ` };

  m = raw.match(/^(\s*)(\d+)\.\s+(.*)$/);
  if (m) return { kind: 'ordered', indent: m[1] || '', number: Number(m[2]) || 1, text: m[3] || '', prefix: `${m[1]}${m[2]}. ` };

  m = raw.match(/^(\s*)[-*+]\s+(.*)$/);
  if (m) return { kind: 'bullet', indent: m[1] || '', bullet: '-', text: m[2] || '', prefix: `${m[1]}- ` };

  return { kind: raw.trim() ? 'paragraph' : 'empty', text: raw, prefix: '' };
}

function viewToRaw(view, text) {
  const value = String(text || '');
  if (view.kind === 'heading') return `${'#'.repeat(Math.max(1, Math.min(6, view.level || 1)))} ${value}`;
  if (view.kind === 'quote') return `${view.indent || ''}> ${value}`;
  if (view.kind === 'task') return `${view.indent || ''}- [${view.checked ? 'x' : ' '}] ${value}`;
  if (view.kind === 'ordered') return `${view.indent || ''}${view.number || 1}. ${value}`;
  if (view.kind === 'bullet') return `${view.indent || ''}- ${value}`;
  return value;
}

function unescapedMarkerCount(text, marker) {
  const source = String(text || '');
  let count = 0;
  for (let i = 0; i <= source.length - marker.length; i += 1) {
    if (source.slice(i, i + marker.length) === marker && source[i - 1] !== '\\') {
      count += 1;
      i += marker.length - 1;
    }
  }
  return count;
}

function adjustInlineSplit(split) {
  const markers = ['**', '__', '~~', '`', '$', '*', '_'];
  let before = split.before;
  let after = split.after;
  for (const marker of markers) {
    if (!after.startsWith(marker)) continue;
    if (unescapedMarkerCount(before, marker) % 2 !== 1) continue;
    before += marker;
    after = after.slice(marker.length);
    break;
  }
  return { ...split, before, after };
}

function skipHtmlTag(src, i) {
  if (src[i] !== '<') return 0;
  const close = src.indexOf('>', i);
  if (close < 0) return 0;
  const tag = src.slice(i, close + 1);
  if (/^<\/?(?:span|mark|u)\b[^>]*>$/i.test(tag)) return tag.length;
  return 0;
}

function markdownOffsetFromPlain(markdown, plainOffset) {
  const src = String(markdown || '');
  const markers = ['**', '__', '~~', '`', '$', '*', '_'];
  let plain = 0;
  for (let i = 0; i < src.length; i += 1) {
    const htmlLen = skipHtmlTag(src, i);
    if (htmlLen) { i += htmlLen - 1; continue; }
    const marker = markers.find((m) => src.slice(i, i + m.length) === m);
    if (marker) {
      i += marker.length - 1;
      continue;
    }
    if (plain >= plainOffset) return i;
    plain += 1;
  }
  return src.length;
}

function markdownEndOffsetFromPlain(markdown, plainOffset) {
  const src = String(markdown || '');
  const markers = ['**', '__', '~~', '`', '$', '*', '_'];
  let plain = 0;
  for (let i = 0; i < src.length; i += 1) {
    const htmlLen = skipHtmlTag(src, i);
    if (htmlLen) {
      if (plain >= plainOffset) return i;
      i += htmlLen - 1;
      continue;
    }
    const marker = markers.find((m) => src.slice(i, i + m.length) === m);
    if (marker) {
      if (plain >= plainOffset) return i;
      i += marker.length - 1;
      continue;
    }
    if (plain >= plainOffset) return i;
    plain += 1;
  }
  return src.length;
}

function isInlineMarkerAt(source, index, marker) {
  if (index < 0 || source.slice(index, index + marker.length) !== marker) return false;
  if ((marker === '*' || marker === '_') && (source[index - 1] === marker || source[index + marker.length] === marker)) return false;
  return source[index - 1] !== '\\';
}

function findSurroundingInlinePair(source, sourceOffset, marker, rightMarker) {
  const src = String(source || '');
  for (let open = Math.min(sourceOffset, src.length - marker.length); open >= 0; open -= 1) {
    if (!isInlineMarkerAt(src, open, marker)) continue;
    let close = open + marker.length;
    while (close <= src.length - rightMarker.length) {
      const nextClose = src.indexOf(rightMarker, close);
      if (nextClose < 0) break;
      if (isInlineMarkerAt(src, nextClose, rightMarker) && nextClose >= sourceOffset) {
        return { open, close: nextClose };
      }
      close = nextClose + rightMarker.length;
    }
  }
  return null;
}

function wrapInlineRange(source, start, end, marker, rightMarker) {
  const src = String(source || '');
  let open = marker;
  let close = rightMarker;
  if (marker === '$') {
    if (start > 0 && /[\w\u4e00-\u9fff]/.test(src[start - 1])) open = ` ${open}`;
    if (end < src.length && /[\w\u4e00-\u9fff]/.test(src[end])) close = `${close} `;
  }
  return {
    text: src.slice(0, start) + open + src.slice(start, end) + close + src.slice(end),
    plainShift: open.length,
  };
}

function toggleInlineMarkdown(source, plainStart, plainEnd, marker, rightMarker) {
  const src = String(source || '');
  const start = markdownOffsetFromPlain(src, plainStart);
  const end = plainEnd > plainStart ? markdownEndOffsetFromPlain(src, plainEnd) : start;
  const selected = src.slice(start, end);

  if (selected) {
    if (isInlineMarkerAt(src, start - marker.length, marker) && isInlineMarkerAt(src, end, rightMarker)) {
      return {
        text: src.slice(0, start - marker.length) + selected + src.slice(end + rightMarker.length),
        pos: plainEnd,
      };
    }
    const wrapped = wrapInlineRange(src, start, end, marker, rightMarker);
    return { text: wrapped.text, pos: plainEnd };
  }

  const pair = findSurroundingInlinePair(src, Math.max(0, start - 1), marker, rightMarker);
  if (pair) {
    const inner = src.slice(pair.open + marker.length, pair.close);
    return {
      text: src.slice(0, pair.open) + inner + src.slice(pair.close + rightMarker.length),
      pos: plainStart,
    };
  }

  if (src.trim()) {
    const leading = src.match(/^\s*/)[0] || '';
    const trailing = src.match(/\s*$/)[0] || '';
    const coreStart = leading.length;
    const coreEnd = src.length - trailing.length;
    const core = src.slice(coreStart, coreEnd);
    if (core.startsWith(marker) && core.endsWith(rightMarker) && core.length >= marker.length + rightMarker.length) {
      return {
        text: leading + core.slice(marker.length, core.length - rightMarker.length) + trailing,
        pos: Math.min(plainStart, stripMarkdownFormatting(core).length),
      };
    }
    const wrapped = wrapInlineRange(src, coreStart, coreEnd, marker, rightMarker);
    return { text: wrapped.text, pos: plainStart };
  }

  const wrapped = wrapInlineRange(src, start, end, marker, rightMarker);
  return { text: wrapped.text, pos: plainStart + wrapped.plainShift };
}

function renumberOrderedLines(lines) {
  const next = lines.slice();
  const counters = {};
  for (let i = 0; i < next.length; i += 1) {
    const m = String(next[i] || '').match(/^(\s*)(\d+)\.\s+(.*)$/);
    if (!m) {
      if (!/^\s/.test(next[i] || '')) Object.keys(counters).forEach((key) => delete counters[key]);
      continue;
    }
    const indent = m[1] || '';
    const number = counters[indent] == null ? Number(m[2]) || 1 : counters[indent] + 1;
    counters[indent] = number;
    next[i] = `${indent}${number}. ${m[3] || ''}`;
  }
  return next;
}

function normalizeLines(lines) {
  const next = renumberOrderedLines(lines.length ? lines : ['']);
  return next.length ? next : [''];
}

function tableSeparatorLine(line) {
  return /^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?\s*$/.test(String(line || ''));
}

function fenceStartLine(line) {
  return /^\s*```/.test(String(line || ''));
}

function fenceLang(line) {
  return (String(line || '').match(/^\s*```\s*([^\s`]*)/) || [])[1] || '';
}

function fenceSourceFromLines(lines) {
  const body = lines.slice(1, lines[lines.length - 1]?.trim() === '```' ? -1 : lines.length);
  return body.join('\n');
}

function fenceLinesFromSource(source, lang) {
  return [`\`\`\`${lang || ''}`, ...String(source || '').split('\n'), '```'];
}

function tableRowLine(line) {
  return /\|/.test(String(line || ''));
}

function splitTableRow(line) {
  let src = String(line || '').trim();
  if (src.startsWith('|')) src = src.slice(1);
  if (src.endsWith('|')) src = src.slice(0, -1);
  return src.split('|').map((cell) => cell.trim().replace(/\\\|/g, '|'));
}

function parseMarkdownTable(lines) {
  const rows = lines.filter((line, idx) => idx !== 1 || !tableSeparatorLine(line)).map(splitTableRow);
  const width = Math.max(1, ...rows.map((row) => row.length));
  return rows.map((row) => Array.from({ length: width }, (_, i) => row[i] || ''));
}

function tableToMarkdownLines(rows) {
  const safeRows = rows && rows.length ? rows : [['']];
  const width = Math.max(1, ...safeRows.map((row) => row.length));
  const norm = safeRows.map((row) => Array.from({ length: width }, (_, i) => String(row[i] || '').replace(/\|/g, '\\|')));
  const header = norm[0] || Array.from({ length: width }, () => '');
  const body = norm.slice(1);
  const sep = Array.from({ length: width }, () => '---');
  const toRow = (row) => `| ${row.join(' | ')} |`;
  return [toRow(header), toRow(sep), ...body.map(toRow)];
}

function mathSourceFromLines(lines) {
  const body = lines.slice(1, lines[lines.length - 1]?.trim() === '$$' ? -1 : lines.length);
  return body.join('\n');
}

function mathLinesFromSource(source) {
  return ['$$', ...String(source || '').split('\n'), '$$'];
}

function renderKatexDisplay(tex) {
  const source = String(tex || '').trim();
  if (!source) return '<span class="wysiwyg-md-preview-muted">Math preview</span>';
  if (window.katex) {
    try { return window.katex.renderToString(source, { displayMode: true, throwOnError: false }); } catch (_) {}
  }
  return `<code>${source.replace(/&/g, '&amp;').replace(/</g, '&lt;')}</code>`;
}

function buildSegments(lines) {
  const segments = [];
  let i = 0;
  while (i < lines.length) {
    if (String(lines[i] || '').trim() === '$$') {
      let end = i + 1;
      while (end < lines.length && String(lines[end] || '').trim() !== '$$') end += 1;
      if (end < lines.length) {
        segments.push({ type: 'math', start: i, end, lines: lines.slice(i, end + 1) });
        i = end + 1;
        continue;
      }
    }
    if (fenceStartLine(lines[i])) {
      let end = i + 1;
      while (end < lines.length && String(lines[end] || '').trim() !== '```') end += 1;
      if (end < lines.length) {
        segments.push({ type: 'fence', start: i, end, lines: lines.slice(i, end + 1), lang: fenceLang(lines[i]) });
        i = end + 1;
        continue;
      }
    }
    if (i + 1 < lines.length && tableRowLine(lines[i]) && tableSeparatorLine(lines[i + 1])) {
      let end = i + 2;
      while (end < lines.length && tableRowLine(lines[end]) && !/^\s*$/.test(lines[end])) end += 1;
      segments.push({ type: 'table', start: i, end: end - 1, lines: lines.slice(i, end) });
      i = end;
      continue;
    }
    segments.push({ type: 'line', index: i, raw: lines[i] || '' });
    i += 1;
  }
  return segments;
}

function WysiwygLine({ index, raw, active, placeholder, inputRef, onFocus, onChangeText, onKeyDown, onDragOver, onDrop, onMouseUp, onKeyUp }) {
  const view = lineView(raw);
  const taRef = React.useRef(null);

  React.useLayoutEffect(() => {
    const node = taRef.current;
    if (!node || document.activeElement === node) return;
    node.innerHTML = view.kind === 'rule' ? '<hr />' : renderInlineMarkdown(view.text);
  }, [view.text, view.kind, active]);

  const setRef = React.useCallback((node) => {
    taRef.current = node;
    inputRef(index, node);
  }, [index, inputRef]);

  const className = [
    'wysiwyg-md-line',
    `kind-${view.kind}`,
    active ? 'active' : '',
    view.kind === 'heading' ? `level-${view.level}` : '',
  ].filter(Boolean).join(' ');

  return (
    <div className={className} data-line={index}>
      {view.kind === 'bullet' && <span className="wysiwyg-md-line-marker">•</span>}
      {view.kind === 'ordered' && <span className="wysiwyg-md-line-marker ordered-marker">{view.number}.</span>}
      {view.kind === 'task' && (
        <input
          className="wysiwyg-md-task-check"
          type="checkbox"
          checked={view.checked}
          onChange={(e) => onChangeText(index, view.text, { ...view, checked: e.target.checked })}
        />
      )}
      <div className="wysiwyg-md-line-edit">
        <div
          ref={setRef}
          className="wysiwyg-md-line-input"
          contentEditable={view.kind !== 'rule'}
          suppressContentEditableWarning
          role="textbox"
          tabIndex={view.kind === 'rule' ? 0 : undefined}
          spellCheck={false}
          data-placeholder={placeholder}
          onFocus={(e) => onFocus(index, e.currentTarget)}
          onInput={(e) => onChangeText(index, inlineMarkdownFromNode(e.currentTarget), view)}
          onKeyDown={(e) => onKeyDown(e, index, view)}
          onMouseUp={onMouseUp}
          onKeyUp={onKeyUp}
          onDragOver={onDragOver}
          onDrop={onDrop}
        />
      </div>
    </div>
  );
}

function blockLineForAction(type, options = {}) {
  if (type === 'heading') return `${'#'.repeat(Math.max(1, Math.min(6, options.level || 1)))} `;
  if (type === 'bullet') return '- ';
  if (type === 'ordered') return '1. ';
  if (type === 'task') return '- [ ] ';
  if (type === 'quote') return '> ';
  if (type === 'rule') return '---';
  return '';
}

function WysiwygTable({ segment, replaceLines, onFocusTable }) {
  const rows = React.useMemo(() => parseMarkdownTable(segment.lines), [segment.lines]);
  const inputRefs = React.useRef([]);

  React.useEffect(() => {
    inputRefs.current.forEach((input) => input && input.classList.remove('pending'));
  }, [rows]);

  const commitRows = (nextRows) => replaceLines(segment.start, segment.end, tableToMarkdownLines(nextRows));
  const updateCell = (r, c, value) => {
    const next = rows.map((row) => row.slice());
    next[r][c] = value;
    commitRows(next);
  };
  const addRow = () => {
    const width = Math.max(1, rows[0]?.length || 1);
    commitRows([...rows, Array.from({ length: width }, () => '')]);
  };
  const addCol = () => commitRows(rows.map((row) => [...row, '']));

  return (
    <div className="wysiwyg-md-table-editor" data-line={segment.start}>
      <table>
        <tbody>
          {rows.map((row, r) => (
            <tr key={r}>
              {row.map((cell, c) => {
                const Cell = r === 0 ? 'th' : 'td';
                return (
                  <Cell key={c}>
                    <input
                      ref={(node) => { inputRefs.current[(r * row.length) + c] = node; }}
                      value={cell}
                      onFocus={() => onFocusTable(segment.start)}
                      onChange={(e) => updateCell(r, c, e.target.value)}
                      onKeyDown={(e) => {
                        if (e.key === 'Enter') {
                          e.preventDefault();
                          const next = e.currentTarget.closest('td,th')?.nextElementSibling?.querySelector('input');
                          next?.focus();
                        }
                      }}
                    />
                  </Cell>
                );
              })}
            </tr>
          ))}
        </tbody>
      </table>
      <div className="wysiwyg-md-table-actions">
        <button type="button" onMouseDown={(e) => e.preventDefault()} onClick={addRow}>+ row</button>
        <button type="button" onMouseDown={(e) => e.preventDefault()} onClick={addCol}>+ column</button>
      </div>
    </div>
  );
}

function WysiwygMath({ segment, active, replaceLines, onFocusMath }) {
  const source = mathSourceFromLines(segment.lines);
  const inputRef = React.useRef(null);
  const previewHtml = React.useMemo(() => renderKatexDisplay(source), [source]);

  React.useLayoutEffect(() => { autoSizeTextarea(inputRef.current); }, [source]);
  React.useEffect(() => {
    if (!active) return;
    requestAnimationFrame(() => focusTextInput(inputRef.current));
  }, [active]);

  const setSource = (nextSource) => replaceLines(segment.start, segment.end, mathLinesFromSource(nextSource));

  if (!active) {
    return (
      <div
        className="wysiwyg-md-math-preview wysiwyg-md-special"
        data-line={segment.start}
        role="button"
        tabIndex={0}
        onClick={(e) => onFocusMath(segment.start, e.currentTarget)}
        onKeyDown={(e) => {
          if (e.key === 'Enter') {
            e.preventDefault();
            onFocusMath(segment.start, e.currentTarget);
          }
        }}
        dangerouslySetInnerHTML={{ __html: previewHtml }}
      />
    );
  }

  return (
    <div className="wysiwyg-md-special wysiwyg-md-math-editor" data-line={segment.start}>
      <div className="wysiwyg-md-special-label">Math</div>
      <div className="wysiwyg-md-fence-line">$$</div>
      <textarea
        ref={inputRef}
        className="wysiwyg-md-special-input"
        value={source}
        onFocus={(e) => onFocusMath(segment.start, e.currentTarget)}
        onChange={(e) => setSource(e.target.value)}
        spellCheck={false}
      />
      <div className="wysiwyg-md-fence-line">$$</div>
      <div className="wysiwyg-md-live-preview" dangerouslySetInnerHTML={{ __html: previewHtml }} />
    </div>
  );
}

function WysiwygFence({ segment, active, replaceLines, onFocusFence }) {
  const source = fenceSourceFromLines(segment.lines);
  const [lang, setLang] = React.useState(segment.lang || '');
  const inputRef = React.useRef(null);
  const rawMarkdown = segment.lines.join('\n');
  const previewHtml = React.useMemo(() => window.MD ? window.MD.render(rawMarkdown) : `<pre><code>${source}</code></pre>`, [rawMarkdown, source]);

  React.useLayoutEffect(() => { autoSizeTextarea(inputRef.current); }, [source]);
  React.useEffect(() => {
    if (!active) return;
    requestAnimationFrame(() => focusTextInput(inputRef.current));
  }, [active]);

  const setSource = (nextSource, nextLang = lang) => replaceLines(segment.start, segment.end, fenceLinesFromSource(nextSource, nextLang));

  if (!active) {
    return (
      <div
        className="wysiwyg-md-fence-preview"
        data-line={segment.start}
        role="button"
        tabIndex={0}
        onClick={(e) => onFocusFence(segment.start, e.currentTarget)}
        onKeyDown={(e) => {
          if (e.key === 'Enter') {
            e.preventDefault();
            onFocusFence(segment.start, e.currentTarget);
          }
        }}
        dangerouslySetInnerHTML={{ __html: previewHtml }}
      />
    );
  }

  return (
    <div className="wysiwyg-md-special wysiwyg-md-code-editor" data-line={segment.start}>
      <div className="wysiwyg-md-special-label">
        Code
        <input value={lang} onChange={(e) => { setLang(e.target.value); setSource(source, e.target.value); }} placeholder="lang" />
      </div>
      <div className="wysiwyg-md-fence-line">```{lang}</div>
      <textarea
        ref={inputRef}
        className="wysiwyg-md-special-input monospace"
        value={source}
        onFocus={(e) => onFocusFence(segment.start, e.currentTarget)}
        onChange={(e) => setSource(e.target.value)}
        spellCheck={false}
      />
      <div className="wysiwyg-md-fence-line">```</div>
    </div>
  );
}

const WysiwygMarkdownEditor = forwardRef(function WysiwygMarkdownEditor({
  content,
  docId,
  onChange,
  textareaRef,
  scrollRef,
  toolbarVisible,
  selBarVisible,
  lang,
  placeholder,
  onScroll,
  onKeyDown,
  onDragOver,
  onDrop,
  dark,
  onCursorChange,
}, ref) {
  const fallbackRootRef = React.useRef(null);
  const rootRef = scrollRef || fallbackRootRef;
  const lineRefs = React.useRef([]);
  const focusedLineRef = React.useRef(0);
  const [focusedLine, setFocusedLine] = React.useState(0);
  const latestContentRef = React.useRef(content || '');

  React.useEffect(() => { latestContentRef.current = content || ''; }, [content]);

  const lines = React.useMemo(() => splitSourceLines(content), [content]);
  const segments = React.useMemo(() => buildSegments(lines), [lines]);

  const lineStartOffset = React.useCallback((index) => {
    let offset = 0;
    for (let i = 0; i < index && i < lines.length; i += 1) offset += String(lines[i] || '').length + 1;
    return offset;
  }, [lines]);

  const emitCursor = React.useCallback((index, node) => {
    if (!onCursorChange) return;
    const raw = String(lines[index] || '');
    const view = lineView(raw);
    const base = lineStartOffset(index);
    if (view.kind === 'rule') {
      onCursorChange({ start: base, end: base + raw.length });
      return;
    }
    const prefixLen = Math.max(0, raw.length - String(view.text || '').length);
    const sel = node ? editableSelectionOffsets(node) : { start: 0, end: 0 };
    const source = view.text || '';
    const start = base + prefixLen + markdownOffsetFromPlain(source, sel.start);
    const end = base + prefixLen + (sel.end > sel.start ? markdownEndOffsetFromPlain(source, sel.end) : markdownOffsetFromPlain(source, sel.end));
    onCursorChange({ start, end });
  }, [lineStartOffset, lines, onCursorChange]);

  const commitLines = React.useCallback((nextLines) => {
    onChange(joinSourceLines(normalizeLines(nextLines)));
  }, [onChange]);

  const focusLine = React.useCallback((index, pos) => {
    requestAnimationFrame(() => {
      const safe = Math.max(0, Math.min(index, lineRefs.current.length - 1));
      const node = lineRefs.current[safe];
      if (node) focusTextInput(node, pos);
      focusedLineRef.current = safe;
      setFocusedLine(safe);
    });
  }, []);

  const setTextareaNode = React.useCallback((index, node) => {
    lineRefs.current[index] = node;
  }, []);

  const setExternalTextarea = React.useCallback((node) => {
    if (textareaRef) textareaRef.current = node || null;
  }, [textareaRef]);

  const replaceLines = React.useCallback((start, end, replacement) => {
    const next = lines.slice();
    next.splice(start, end - start + 1, ...(replacement && replacement.length ? replacement : ['']));
    commitLines(next);
  }, [lines, commitLines]);

  const insertBlockRows = React.useCallback((rows, focusOffset = 0, focusPos = 0) => {
    const index = focusedLineRef.current || 0;
    const next = lines.slice();
    const currentKind = lineView(next[index] || '').kind;
    if (currentKind === 'empty') {
      next.splice(index, 1, ...rows);
      commitLines(next);
      focusLine(index + focusOffset, focusPos);
      return;
    }
    next.splice(index + 1, 0, ...rows);
    commitLines(next);
    focusLine(index + 1 + focusOffset, focusPos);
  }, [lines, commitLines, focusLine]);

  const setLineRaw = React.useCallback((index, raw) => {
    const next = lines.slice();
    next[index] = raw;
    commitLines(next);
  }, [lines, commitLines]);

  const setLineText = React.useCallback((index, text, viewOverride) => {
    const currentView = viewOverride || lineView(lines[index] || '');
    const raw = viewToRaw(currentView, text);
    if (currentView.kind !== 'rule' && lineView(raw).kind === 'rule') {
      const next = lines.slice();
      next[index] = raw;
      if (index + 1 >= next.length || lineView(next[index + 1] || '').kind !== 'empty') {
        next.splice(index + 1, 0, '');
      }
      commitLines(next);
      focusLine(index + 1, 0);
      return;
    }
    setLineRaw(index, raw);
  }, [lines, setLineRaw, commitLines, focusLine]);

  const splitAtCursor = (ta, index, view) => {
    const currentView = view || lineView(lines[index] || '');
    const sourceText = currentView.text || '';
    const text = ta.value != null ? ta.value : editableText(ta);
    const sel = ta.value != null
      ? { start: ta.selectionStart ?? text.length, end: ta.selectionEnd ?? ta.selectionStart ?? text.length }
      : editableSelectionOffsets(ta);
    if (ta.value == null) {
      const start = markdownOffsetFromPlain(sourceText, sel.start);
      const end = sel.end > sel.start ? markdownEndOffsetFromPlain(sourceText, sel.end) : start;
      return adjustInlineSplit({
        start: sel.start,
        end: sel.end,
        before: sourceText.slice(0, start),
        after: sourceText.slice(end),
      });
    }
    return adjustInlineSplit({ start: sel.start, end: sel.end, before: text.slice(0, sel.start), after: text.slice(sel.end) });
  };

  const insertAfterLine = React.useCallback((index, newRows, focusOffset, focusPos) => {
    const next = lines.slice();
    next.splice(index + 1, 0, ...newRows);
    commitLines(next);
    focusLine(index + 1 + (focusOffset || 0), focusPos == null ? 0 : focusPos);
  }, [lines, commitLines, focusLine]);

  const insertSameFormatLine = React.useCallback((index, view, before, after) => {
    const next = lines.slice();
    const nextView = { ...view };
    if (view.kind === 'ordered') nextView.number = (view.number || 1) + 1;
    next[index] = viewToRaw(view, before || '');
    next.splice(index + 1, 0, viewToRaw(nextView, after || ''));
    commitLines(next);
    focusLine(index + 1, 0);
  }, [lines, commitLines, focusLine]);

  const handleEnter = React.useCallback((e, index, view) => {
    const ta = e.currentTarget;
    const split = splitAtCursor(ta, index, view);
    const next = lines.slice();

    if (e.shiftKey) {
      e.preventDefault();
      if (view.kind === 'quote' || view.kind === 'bullet' || view.kind === 'task' || view.kind === 'ordered') {
        insertSameFormatLine(index, view, split.before, split.after);
      } else {
        next[index] = split.before;
        next.splice(index + 1, 0, split.after);
        commitLines(next);
        focusLine(index + 1, 0);
      }
      return;
    }

    e.preventDefault();

    if (view.kind === 'rule') {
      insertAfterLine(index, [''], 0, 0);
      return;
    }

    if (!editableText(ta).trim() && view.kind !== 'paragraph' && view.kind !== 'empty') {
      next[index] = '';
      commitLines(next);
      focusLine(index, 0);
      return;
    }

    if (view.kind === 'quote') {
      if (!editableText(ta).trim()) {
        next[index] = '';
        commitLines(next);
        focusLine(index, 0);
        return;
      }
      next[index] = viewToRaw(view, split.before);
      next.splice(index + 1, 0, viewToRaw(view, split.after));
      commitLines(next);
      focusLine(index + 1, 0);
      return;
    }

    if (view.kind === 'bullet' || view.kind === 'task' || view.kind === 'ordered') {
      if (!editableText(ta).trim()) {
        next[index] = '';
        commitLines(next);
        focusLine(index, 0);
        return;
      }
      next[index] = viewToRaw(view, split.before);
      const nextView = { ...view };
      if (view.kind === 'ordered') nextView.number = (view.number || 1) + 1;
      next.splice(index + 1, 0, viewToRaw(nextView, split.after));
      commitLines(next);
      focusLine(index + 1, 0);
      return;
    }

    if (view.kind === 'heading') {
      next[index] = viewToRaw(view, split.before);
      next.splice(index + 1, 0, split.after || '');
      commitLines(next);
      focusLine(index + 1, 0);
      return;
    }

    if (!editableText(ta) && lineView(lines[index - 1] || '').kind === 'empty') {
      insertAfterLine(index, [''], 0, 0);
      return;
    }

    next[index] = split.before;
    next.splice(index + 1, 0, split.after);
    commitLines(next);
    focusLine(index + 1, 0);
  }, [lines, commitLines, focusLine, insertAfterLine, insertSameFormatLine]);

  const mergeWithPrevious = React.useCallback((index, view) => {
    if (index <= 0) return;
    const prevView = lineView(lines[index - 1] || '');
    const currentText = view.text || '';
    const next = lines.slice();

    if (!currentText && view.kind !== 'paragraph' && view.kind !== 'empty') {
      next[index] = currentText;
      commitLines(next);
      focusLine(index, 0);
      return;
    }

    if (view.kind === 'empty') {
      next.splice(index, 1);
      commitLines(next);
      focusLine(index - 1, prevView.text.length);
      return;
    }

    const merged = (prevView.text || '') + currentText;
    next[index - 1] = viewToRaw(prevView, merged);
    next.splice(index, 1);
    commitLines(next);
    focusLine(index - 1, prevView.text.length);
  }, [lines, commitLines, focusLine]);

  const mergeWithNext = React.useCallback((index, view) => {
    if (index >= lines.length - 1) return;
    const nextView = lineView(lines[index + 1] || '');
    const next = lines.slice();
    if (nextView.kind === 'empty') {
      next.splice(index + 1, 1);
      commitLines(next);
      focusLine(index, view.text.length);
      return;
    }
    const merged = (view.text || '') + (nextView.text || '');
    next[index] = viewToRaw(view, merged);
    next.splice(index + 1, 1);
    commitLines(next);
    focusLine(index, view.text.length);
  }, [lines, commitLines, focusLine]);

  const handleLineKeyDown = React.useCallback((e, index, view) => {
    if (e.isComposing || e.nativeEvent?.isComposing || e.keyCode === 229) {
      return;
    }
    if (e.key === 'Enter') {
      handleEnter(e, index, view);
      return;
    }
    if (e.key === 'Backspace') {
      if (view.kind === 'rule') {
        e.preventDefault();
        const next = lines.slice();
        next.splice(index, 1);
        commitLines(next);
        focusLine(Math.max(0, index - 1), 0);
        return;
      }
      const ta = e.currentTarget;
      const sel = ta.value != null ? { start: ta.selectionStart, end: ta.selectionEnd } : editableSelectionOffsets(ta);
      if (sel.start === 0 && sel.end === 0) {
        e.preventDefault();
        mergeWithPrevious(index, view);
      }
      return;
    }
    if (e.key === 'Delete') {
      if (view.kind === 'rule') {
        e.preventDefault();
        const next = lines.slice();
        next.splice(index, 1);
        commitLines(next);
        focusLine(Math.min(index, next.length - 1), 0);
        return;
      }
      const ta = e.currentTarget;
      const text = editableText(ta);
      const sel = ta.value != null ? { start: ta.selectionStart, end: ta.selectionEnd } : editableSelectionOffsets(ta);
      if (sel.start === text.length && sel.end === text.length) {
        e.preventDefault();
        mergeWithNext(index, view);
      }
      return;
    }
    if (e.key === 'Tab') {
      e.preventDefault();
      const ta = e.currentTarget;
      const text = editableText(ta);
      const sel = ta.value != null
        ? { start: ta.selectionStart ?? text.length, end: ta.selectionEnd ?? ta.selectionStart ?? text.length }
        : editableSelectionOffsets(ta);
      const s = sel.start;
      const en = sel.end;
      const insert = '  ';
      setLineText(index, text.slice(0, s) + insert + text.slice(en), view);
      focusLine(index, s + insert.length);
      return;
    }
    const currentText = editableText(e.currentTarget);
    const currentSel = e.currentTarget.value != null
      ? { start: e.currentTarget.selectionStart, end: e.currentTarget.selectionEnd }
      : editableSelectionOffsets(e.currentTarget);
    if (e.key === 'ArrowUp' && currentSel.start === 0) {
      const prev = lineRefs.current[index - 1];
      if (prev) {
        e.preventDefault();
        focusLine(index - 1, Math.min(editableText(prev).length, currentSel.end || 0));
      }
      return;
    }
    if (e.key === 'ArrowDown' && currentSel.start === currentText.length) {
      const next = lineRefs.current[index + 1];
      if (next) {
        e.preventDefault();
        focusLine(index + 1, Math.min(editableText(next).length, currentSel.end || 0));
      }
      return;
    }
    if (onKeyDown) onKeyDown(e);
  }, [commitLines, focusLine, handleEnter, lines, mergeWithNext, mergeWithPrevious, onKeyDown]);

  const handleFocus = React.useCallback((index, node) => {
    focusedLineRef.current = index;
    setFocusedLine(index);
    setExternalTextarea(node);
    emitCursor(index, node);
  }, [emitCursor, setExternalTextarea]);

  const handleSelectionHint = React.useCallback(() => {
    const index = focusedLineRef.current;
    const node = lineRefs.current[index];
    setExternalTextarea(node || null);
    emitCursor(index, node);
  }, [emitCursor, setExternalTextarea]);

  const handleHistoryChange = React.useCallback((nextValue) => {
    onChange(String(nextValue || ''));
    requestAnimationFrame(() => focusLine(Math.min(focusedLineRef.current, splitSourceLines(nextValue).length - 1), 0));
  }, [focusLine, onChange]);

  const currentView = lineView(lines[focusedLine] || '');
  const toolbarValue = currentView.text || '';

  const handleToolbarChange = React.useCallback((nextValue) => {
    const index = focusedLineRef.current || 0;
    const node = lineRefs.current[index];
    const raw = String(nextValue || '');
    if (!node) {
      onChange(raw);
      return;
    }
    if (raw.includes('\n')) {
      const next = lines.slice();
      next.splice(index, 1, ...raw.split('\n'));
      commitLines(next);
      focusLine(index, 0);
      return;
    }
    const originalView = lineView(lines[index] || '');
    const typedView = lineView(raw);
    if (typedView.kind !== 'empty' && typedView.kind !== 'paragraph') {
      setLineRaw(index, raw);
    } else {
      setLineText(index, raw, originalView);
    }
    requestAnimationFrame(() => focusTextInput(lineRefs.current[index], raw.length));
  }, [commitLines, focusLine, lines, onChange, setLineRaw, setLineText]);

  const handleInlineAction = React.useCallback((marker, rightMarker = marker) => {
    const index = focusedLineRef.current || 0;
    const node = lineRefs.current[index];
    if (!node) return;
    const view = lineView(lines[index] || '');
    if (view.kind === 'rule') return;
    const text = view.text || '';
    const sel = editableSelectionOffsets(node);
    const result = toggleInlineMarkdown(text, sel.start, sel.end, marker, rightMarker);
    const nextText = result.text;
    const nextPlainPos = result.pos;
    setLineText(index, nextText, view);
    requestAnimationFrame(() => {
      const target = lineRefs.current[index];
      if (target && document.activeElement === target) {
        target.innerHTML = renderInlineMarkdown(nextText);
      }
      focusTextInput(target, nextPlainPos);
    });
  }, [lines, setLineText]);

  const handleApplyColor = React.useCallback((type, color) => {
    const index = focusedLineRef.current || 0;
    const node = lineRefs.current[index];
    if (!node) return;
    const view = lineView(lines[index] || '');
    if (view.kind === 'rule') return;
    const text = view.text || '';
    const sel = editableSelectionOffsets(node);
    if (sel.start === sel.end) return;
    const start = markdownOffsetFromPlain(text, sel.start);
    const end = markdownEndOffsetFromPlain(text, sel.end);
    let inner = text.slice(start, end);
    const re = type === 'text'
      ? /^<span style="color:[^"]+">([\s\S]*)<\/span>$/
      : /^<mark style="background:[^"]+">([\s\S]*)<\/mark>$/;
    const m = inner.match(re);
    if (m) inner = m[1];
    const wrapped = type === 'text'
      ? `<span style="color:${color}">${inner}</span>`
      : `<mark style="background:${color}">${inner}</mark>`;
    const nextText = text.slice(0, start) + wrapped + text.slice(end);
    setLineText(index, nextText, view);
    requestAnimationFrame(() => {
      const target = lineRefs.current[index];
      if (target) target.innerHTML = renderInlineMarkdown(nextText);
      focusTextInput(target, sel.end);
    });
  }, [lines, setLineText]);

  const handleClearFormatting = React.useCallback(() => {
    const index = focusedLineRef.current || 0;
    const view = lineView(lines[index] || '');
    const cleaned = stripMarkdownFormatting(view.text || '');
    const next = lines.slice();
    next[index] = cleaned;
    commitLines(next);
    requestAnimationFrame(() => {
      const node = lineRefs.current[index];
      if (node) node.innerHTML = renderInlineMarkdown(cleaned);
      focusTextInput(node, cleaned.length);
    });
  }, [commitLines, lines]);

  const handleBlockAction = React.useCallback((type, options = {}) => {
    if (type === 'math') {
      insertBlockRows(['$$', '', '$$'], 1, 0);
      return;
    }
    if (type === 'code') {
      insertBlockRows(['```', '', '```'], 1, 0);
      return;
    }
    if (type === 'markdownBlock') {
      const rows = String(options.markdown || '').replace(/^\n+|\n+$/g, '').split('\n');
      insertBlockRows(rows, 0, 0);
      return;
    }
    const line = blockLineForAction(type, options);
    if (!line) return;
    const index = focusedLineRef.current || 0;
    const current = lineView(lines[index] || '');
    const text = current.text || '';
    const next = lines.slice();
    if (type === 'rule') {
      if (text.trim()) {
        insertBlockRows(['---'], 0, 0);
      } else {
        next[index] = '---';
        commitLines(next);
        focusLine(index, 0);
      }
      return;
    }
    if (!text.trim() && current.kind !== 'empty' && current.kind !== 'paragraph') {
      next[index] = '';
      commitLines(next);
      focusLine(index, 0);
      return;
    }
    const nextView = lineView(line);
    if (type === 'heading') nextView.level = options.level || 1;
    next[index] = viewToRaw(nextView, text);
    commitLines(next);
    focusLine(index, text.length);
  }, [commitLines, focusLine, insertBlockRows, lines]);

  const handleInsertToc = React.useCallback((tocMarkdown) => {
    const tocLines = String(tocMarkdown || '').replace(/^\n+|\n+$/g, '').split('\n');
    const index = focusedLineRef.current || 0;
    const next = lines.slice();
    if (lineView(next[index] || '').kind === 'empty') {
      next.splice(index, 1, ...tocLines);
      commitLines(next);
      focusLine(index, 0);
      return;
    }
    next.splice(index + 1, 0, '', ...tocLines, '');
    commitLines(next);
    focusLine(index + 2, 0);
  }, [commitLines, focusLine, lines]);

  const handleInsertMarkdown = React.useCallback((markdown, savedSelection) => {
    const text = String(markdown || '');
    const index = focusedLineRef.current || 0;
    const view = lineView(lines[index] || '');
    if (view.kind === 'rule') {
      insertBlockRows([text], 0, text.length);
      return;
    }
    const source = view.text || '';
    const node = lineRefs.current[index];
    const liveSel = node ? editableSelectionOffsets(node) : null;
    const sel = savedSelection || liveSel || { start: source.length, end: source.length };
    const start = markdownOffsetFromPlain(source, sel.start);
    const end = sel.end > sel.start ? markdownEndOffsetFromPlain(source, sel.end) : start;
    const nextText = source.slice(0, start) + text + source.slice(end);
    setLineText(index, nextText, view);
    focusLine(index, sel.start + stripMarkdownFormatting(text).length);
  }, [focusLine, insertBlockRows, lines, setLineText]);

  useImperativeHandle(ref, () => ({
    jumpToLine(line) {
      focusLine(Math.max(0, Math.min(line || 0, lines.length - 1)), 0);
    },
    jumpToOffset(start, end) {
      const src = latestContentRef.current || '';
      const offset = start || 0;
      let line = 0;
      let lineStart = 0;
      for (let i = 0; i < src.length && i < offset; i += 1) {
        if (src[i] === '\n') { line += 1; lineStart = i + 1; }
      }
      focusLine(line, offset - lineStart);
      requestAnimationFrame(() => {
        const node = lineRefs.current[line];
        if (!node || end == null) return;
        if (node.setSelectionRange) {
          node.setSelectionRange(Math.max(0, offset - lineStart), Math.max(0, end - lineStart));
        } else {
          setEditableSelection(node, Math.max(0, offset - lineStart), Math.max(0, end - lineStart));
        }
      });
    },
    insertMarkdown(markdown) {
      const text = String(markdown || '');
      const index = focusedLineRef.current || 0;
      const node = lineRefs.current[index];
      if (!node) {
        const next = latestContentRef.current ? `${latestContentRef.current}\n\n${text}` : text;
        onChange(next);
        return;
      }
      const view = lineView(lines[index] || '');
      const nodeText = editableText(node);
      const sel = node.value != null
        ? { start: node.selectionStart ?? nodeText.length, end: node.selectionEnd ?? node.selectionStart ?? nodeText.length }
        : editableSelectionOffsets(node);
      const s = sel.start;
      const e = sel.end;
      const nextText = nodeText.slice(0, s) + text + nodeText.slice(e);
      setLineText(index, nextText, view);
      focusLine(index, s + text.length);
    },
    getScrollState() {
      const root = rootRef.current;
      if (!root) return { frac: 0 };
      const max = root.scrollHeight - root.clientHeight;
      return { frac: max > 0 ? root.scrollTop / max : 0 };
    },
  }), [focusLine, lines, onChange, setLineText]);

  React.useEffect(() => () => { if (textareaRef) textareaRef.current = null; }, [docId, textareaRef]);

  React.useEffect(() => {
    const root = rootRef.current;
    if (!root || !window.MD?.renderMermaidIn) return;
    const timer = setTimeout(() => window.MD.renderMermaidIn(root, dark ? 'dark' : 'light'), 40);
    return () => clearTimeout(timer);
  }, [content, dark, rootRef]);

  return (
    <div className="wysiwyg-md-shell">
      <FormatToolbar
        textareaRef={textareaRef}
        scrollRef={rootRef}
        value={toolbarValue}
        onChange={handleToolbarChange}
        toolbarVisible={toolbarVisible}
        selBarVisible={false}
        historyValue={content || ''}
        onHistoryChange={handleHistoryChange}
        historyKey={docId}
        tocValue={content || ''}
        onInsertToc={handleInsertToc}
        onBlockAction={handleBlockAction}
        onInlineAction={handleInlineAction}
        onClearFormatting={handleClearFormatting}
        onApplyColor={handleApplyColor}
        onInsertMarkdown={handleInsertMarkdown}
      />
      <div className="wysiwyg-md-scroll" ref={rootRef} onScroll={onScroll}>
        <div className="wysiwyg-md-page prose">
          {segments.map((segment) => {
            if (segment.type === 'table') {
              return <WysiwygTable key={`table-${segment.start}`} segment={segment} replaceLines={replaceLines} onFocusTable={(line) => { focusedLineRef.current = line; setFocusedLine(line); setExternalTextarea(null); }} />;
            }
            if (segment.type === 'math') {
              return <WysiwygMath key={`math-${segment.start}`} segment={segment} active={focusedLine === segment.start} replaceLines={replaceLines} onFocusMath={(line, node) => { focusedLineRef.current = line; setFocusedLine(line); setExternalTextarea(node?.tagName === 'TEXTAREA' ? node : null); }} />;
            }
            if (segment.type === 'fence') {
              return <WysiwygFence key={`fence-${segment.start}`} segment={segment} active={focusedLine === segment.start} replaceLines={replaceLines} onFocusFence={(line, node) => { focusedLineRef.current = line; setFocusedLine(line); setExternalTextarea(node?.tagName === 'TEXTAREA' ? node : null); }} />;
            }
            return (
              <WysiwygLine
                key={`line-${segment.index}`}
                index={segment.index}
                raw={segment.raw}
                active={focusedLine === segment.index}
                placeholder={!content && segment.index === 0 ? placeholder : ''}
                inputRef={setTextareaNode}
                onFocus={handleFocus}
                onChangeText={setLineText}
                onKeyDown={handleLineKeyDown}
                onMouseUp={handleSelectionHint}
                onKeyUp={handleSelectionHint}
                onDragOver={onDragOver}
                onDrop={onDrop}
              />
            );
          })}
        </div>
      </div>
    </div>
  );
});

window.WysiwygMarkdownEditor = WysiwygMarkdownEditor;
