(.*?)<\\/div>/gs) || [];\r\n const codeContent = lines.map(line => {\r\n // Extract text from each div - content is already escaped\r\n const text = line.replace(/
(.*?)<\\/div>/s, '$1')\r\n .replace(/ /g, ' ');\r\n return text;\r\n }).join('\\n');\r\n\r\n // Extract language from the opening fence\r\n const lang = openFence.slice(3).trim();\r\n const langClass = lang ? ` class=\"language-${lang}\"` : '';\r\n\r\n // Apply code highlighting if available\r\n let highlightedContent = codeContent;\r\n // Use instance highlighter if provided, otherwise fall back to global highlighter\r\n const highlighter = instanceHighlighter || this.codeHighlighter;\r\n if (highlighter) {\r\n try {\r\n // CRITICAL: Decode HTML entities before passing to highlighter\r\n // In the DOM path, textContent automatically decodes entities.\r\n // In the manual path, we need to decode explicitly to avoid double-escaping.\r\n const decodedCode = codeContent\r\n .replace(/"/g, '\"')\r\n .replace(/'/g, \"'\")\r\n .replace(/</g, '<')\r\n .replace(/>/g, '>')\r\n .replace(/&/g, '&'); // Must be last to avoid double-decoding\r\n\r\n const result = highlighter(decodedCode, lang);\r\n\r\n // Check if result is a Promise (async highlighter)\r\n // Note: In Node.js context, we can't easily defer rendering, so we warn\r\n if (result && typeof result.then === 'function') {\r\n console.warn('Async highlighters are not supported in Node.js (non-DOM) context. Use synchronous highlighters for server-side rendering.');\r\n // Fall back to escaped content\r\n } else {\r\n // Synchronous highlighter - verify returned non-empty string\r\n if (result && typeof result === 'string' && result.trim()) {\r\n highlightedContent = result;\r\n }\r\n // else: keep the escaped codeContent as fallback\r\n }\r\n } catch (error) {\r\n console.warn('Code highlighting failed:', error);\r\n // Fall back to original content\r\n }\r\n }\r\n\r\n // Keep fence markers visible as separate divs, with pre/code block between them\r\n let result = `
${openFence}
`;\r\n // Use highlighted content if available, otherwise use escaped content\r\n result += `
${highlightedContent}
`;\r\n result += `
${closeFence}
`;\r\n\r\n return result;\r\n });\r\n\r\n return processed;\r\n }\r\n\r\n /**\r\n * List pattern definitions\r\n */\r\n static LIST_PATTERNS = {\r\n bullet: /^(\\s*)([-*+])\\s+(.*)$/,\r\n numbered: /^(\\s*)(\\d+)\\.\\s+(.*)$/,\r\n checkbox: /^(\\s*)-\\s+\\[([ x])\\]\\s+(.*)$/\r\n };\r\n\r\n /**\r\n * Get list context at cursor position\r\n * @param {string} text - Full text content\r\n * @param {number} cursorPosition - Current cursor position\r\n * @returns {Object} List context information\r\n */\r\n static getListContext(text, cursorPosition) {\r\n // Find the line containing the cursor\r\n const lines = text.split('\\n');\r\n let currentPos = 0;\r\n let lineIndex = 0;\r\n let lineStart = 0;\r\n\r\n for (let i = 0; i < lines.length; i++) {\r\n const lineLength = lines[i].length;\r\n if (currentPos + lineLength >= cursorPosition) {\r\n lineIndex = i;\r\n lineStart = currentPos;\r\n break;\r\n }\r\n currentPos += lineLength + 1; // +1 for newline\r\n }\r\n\r\n const currentLine = lines[lineIndex];\r\n const lineEnd = lineStart + currentLine.length;\r\n\r\n // Check for checkbox first (most specific)\r\n const checkboxMatch = currentLine.match(this.LIST_PATTERNS.checkbox);\r\n if (checkboxMatch) {\r\n return {\r\n inList: true,\r\n listType: 'checkbox',\r\n indent: checkboxMatch[1],\r\n marker: '-',\r\n checked: checkboxMatch[2] === 'x',\r\n content: checkboxMatch[3],\r\n lineStart,\r\n lineEnd,\r\n markerEndPos: lineStart + checkboxMatch[1].length + checkboxMatch[2].length + 5 // indent + \"- [ ] \"\r\n };\r\n }\r\n\r\n // Check for bullet list\r\n const bulletMatch = currentLine.match(this.LIST_PATTERNS.bullet);\r\n if (bulletMatch) {\r\n return {\r\n inList: true,\r\n listType: 'bullet',\r\n indent: bulletMatch[1],\r\n marker: bulletMatch[2],\r\n content: bulletMatch[3],\r\n lineStart,\r\n lineEnd,\r\n markerEndPos: lineStart + bulletMatch[1].length + bulletMatch[2].length + 1 // indent + marker + space\r\n };\r\n }\r\n\r\n // Check for numbered list\r\n const numberedMatch = currentLine.match(this.LIST_PATTERNS.numbered);\r\n if (numberedMatch) {\r\n return {\r\n inList: true,\r\n listType: 'numbered',\r\n indent: numberedMatch[1],\r\n marker: parseInt(numberedMatch[2]),\r\n content: numberedMatch[3],\r\n lineStart,\r\n lineEnd,\r\n markerEndPos: lineStart + numberedMatch[1].length + numberedMatch[2].length + 2 // indent + number + \". \"\r\n };\r\n }\r\n\r\n // Not in a list\r\n return {\r\n inList: false,\r\n listType: null,\r\n indent: '',\r\n marker: null,\r\n content: currentLine,\r\n lineStart,\r\n lineEnd,\r\n markerEndPos: lineStart\r\n };\r\n }\r\n\r\n /**\r\n * Create a new list item based on context\r\n * @param {Object} context - List context from getListContext\r\n * @returns {string} New list item text\r\n */\r\n static createNewListItem(context) {\r\n switch (context.listType) {\r\n case 'bullet':\r\n return `${context.indent}${context.marker} `;\r\n case 'numbered':\r\n return `${context.indent}${context.marker + 1}. `;\r\n case 'checkbox':\r\n return `${context.indent}- [ ] `;\r\n default:\r\n return '';\r\n }\r\n }\r\n\r\n /**\r\n * Renumber all numbered lists in text\r\n * @param {string} text - Text containing numbered lists\r\n * @returns {string} Text with renumbered lists\r\n */\r\n static renumberLists(text) {\r\n const lines = text.split('\\n');\r\n const numbersByIndent = new Map();\r\n let inList = false;\r\n\r\n const result = lines.map(line => {\r\n const match = line.match(this.LIST_PATTERNS.numbered);\r\n\r\n if (match) {\r\n const indent = match[1];\r\n const indentLevel = indent.length;\r\n const content = match[3];\r\n\r\n // If we weren't in a list or indent changed, reset lower levels\r\n if (!inList) {\r\n numbersByIndent.clear();\r\n }\r\n\r\n // Get the next number for this indent level\r\n const currentNumber = (numbersByIndent.get(indentLevel) || 0) + 1;\r\n numbersByIndent.set(indentLevel, currentNumber);\r\n\r\n // Clear deeper indent levels\r\n for (const [level] of numbersByIndent) {\r\n if (level > indentLevel) {\r\n numbersByIndent.delete(level);\r\n }\r\n }\r\n\r\n inList = true;\r\n return `${indent}${currentNumber}. ${content}`;\r\n } else {\r\n // Not a numbered list item\r\n if (line.trim() === '' || !line.match(/^\\s/)) {\r\n // Empty line or non-indented line breaks the list\r\n inList = false;\r\n numbersByIndent.clear();\r\n }\r\n return line;\r\n }\r\n });\r\n\r\n return result.join('\\n');\r\n }\r\n}\r\n", "/**\r\n * Keyboard shortcuts handler for OverType editor\r\n * Delegates to editor.performAction for consistent behavior\r\n */\r\n\r\n/**\r\n * ShortcutsManager - Handles keyboard shortcuts for the editor\r\n */\r\nexport class ShortcutsManager {\r\n constructor(editor) {\r\n this.editor = editor;\r\n }\r\n\r\n /**\r\n * Handle keydown events - called by OverType\r\n * @param {KeyboardEvent} event - The keyboard event\r\n * @returns {boolean} Whether the event was handled\r\n */\r\n handleKeydown(event) {\r\n const isMac = navigator.platform.toLowerCase().includes('mac');\r\n const modKey = isMac ? event.metaKey : event.ctrlKey;\r\n\r\n if (!modKey) return false;\r\n\r\n let actionId = null;\r\n\r\n switch (event.key.toLowerCase()) {\r\n case 'b':\r\n if (!event.shiftKey) actionId = 'toggleBold';\r\n break;\r\n case 'i':\r\n if (!event.shiftKey) actionId = 'toggleItalic';\r\n break;\r\n case 'k':\r\n if (!event.shiftKey) actionId = 'insertLink';\r\n break;\r\n case '7':\r\n if (event.shiftKey) actionId = 'toggleNumberedList';\r\n break;\r\n case '8':\r\n if (event.shiftKey) actionId = 'toggleBulletList';\r\n break;\r\n }\r\n\r\n if (actionId) {\r\n event.preventDefault();\r\n this.editor.performAction(actionId, event);\r\n return true;\r\n }\r\n\r\n return false;\r\n }\r\n\r\n /**\r\n * Cleanup\r\n */\r\n destroy() {\r\n // Nothing to clean up since we don't add our own listener\r\n }\r\n}\r\n", "/**\r\n * Built-in themes for OverType editor\r\n * Each theme provides a complete color palette for the editor\r\n */\r\n\r\n/**\r\n * Solar theme - Light, warm and bright\r\n */\r\nexport const solar = {\r\n name: 'solar',\r\n colors: {\r\n bgPrimary: '#faf0ca', // Lemon Chiffon - main background\r\n bgSecondary: '#ffffff', // White - editor background\r\n text: '#0d3b66', // Yale Blue - main text\r\n textPrimary: '#0d3b66', // Yale Blue - primary text (same as text)\r\n textSecondary: '#5a7a9b', // Muted blue - secondary text\r\n h1: '#f95738', // Tomato - h1 headers\r\n h2: '#ee964b', // Sandy Brown - h2 headers\r\n h3: '#3d8a51', // Forest green - h3 headers\r\n strong: '#ee964b', // Sandy Brown - bold text\r\n em: '#f95738', // Tomato - italic text\r\n del: '#ee964b', // Sandy Brown - deleted text (same as strong)\r\n link: '#0d3b66', // Yale Blue - links\r\n code: '#0d3b66', // Yale Blue - inline code\r\n codeBg: 'rgba(244, 211, 94, 0.4)', // Naples Yellow with transparency\r\n blockquote: '#5a7a9b', // Muted blue - blockquotes\r\n hr: '#5a7a9b', // Muted blue - horizontal rules\r\n syntaxMarker: 'rgba(13, 59, 102, 0.52)', // Yale Blue with transparency\r\n syntax: '#999999', // Gray - syntax highlighting fallback\r\n cursor: '#f95738', // Tomato - cursor\r\n selection: 'rgba(244, 211, 94, 0.4)', // Naples Yellow with transparency\r\n listMarker: '#ee964b', // Sandy Brown - list markers\r\n rawLine: '#5a7a9b', // Muted blue - raw line indicators\r\n border: '#e0e0e0', // Light gray - borders\r\n hoverBg: '#f0f0f0', // Very light gray - hover backgrounds\r\n primary: '#0d3b66', // Yale Blue - primary accent\r\n // Toolbar colors\r\n toolbarBg: '#ffffff', // White - toolbar background\r\n toolbarIcon: '#0d3b66', // Yale Blue - icon color\r\n toolbarHover: '#f5f5f5', // Light gray - hover background\r\n toolbarActive: '#faf0ca', // Lemon Chiffon - active button background\r\n }\r\n};\r\n\r\n/**\r\n * Cave theme - Dark ocean depths\r\n */\r\nexport const cave = {\r\n name: 'cave',\r\n colors: {\r\n bgPrimary: '#141E26', // Deep ocean - main background\r\n bgSecondary: '#1D2D3E', // Darker charcoal - editor background\r\n text: '#c5dde8', // Light blue-gray - main text\r\n textPrimary: '#c5dde8', // Light blue-gray - primary text (same as text)\r\n textSecondary: '#9fcfec', // Brighter blue - secondary text\r\n h1: '#d4a5ff', // Rich lavender - h1 headers\r\n h2: '#f6ae2d', // Hunyadi Yellow - h2 headers\r\n h3: '#9fcfec', // Brighter blue - h3 headers\r\n strong: '#f6ae2d', // Hunyadi Yellow - bold text\r\n em: '#9fcfec', // Brighter blue - italic text\r\n del: '#f6ae2d', // Hunyadi Yellow - deleted text (same as strong)\r\n link: '#9fcfec', // Brighter blue - links\r\n code: '#c5dde8', // Light blue-gray - inline code\r\n codeBg: '#1a232b', // Very dark blue - code background\r\n blockquote: '#9fcfec', // Brighter blue - same as italic\r\n hr: '#c5dde8', // Light blue-gray - horizontal rules\r\n syntaxMarker: 'rgba(159, 207, 236, 0.73)', // Brighter blue semi-transparent\r\n syntax: '#7a8c98', // Muted gray-blue - syntax highlighting fallback\r\n cursor: '#f26419', // Orange Pantone - cursor\r\n selection: 'rgba(51, 101, 138, 0.4)', // Lapis Lazuli with transparency\r\n listMarker: '#f6ae2d', // Hunyadi Yellow - list markers\r\n rawLine: '#9fcfec', // Brighter blue - raw line indicators\r\n border: '#2a3f52', // Dark blue-gray - borders\r\n hoverBg: '#243546', // Slightly lighter charcoal - hover backgrounds\r\n primary: '#9fcfec', // Brighter blue - primary accent\r\n // Toolbar colors for dark theme\r\n toolbarBg: '#1D2D3E', // Darker charcoal - toolbar background\r\n toolbarIcon: '#c5dde8', // Light blue-gray - icon color\r\n toolbarHover: '#243546', // Slightly lighter charcoal - hover background\r\n toolbarActive: '#2a3f52', // Even lighter - active button background\r\n }\r\n};\r\n\r\n/**\r\n * Auto theme - Automatically switches between solar and cave based on system preference\r\n * This is a special marker theme that triggers automatic theme switching\r\n */\r\nexport const auto = {\r\n name: 'auto',\r\n // The auto theme doesn't have its own colors; it uses solar or cave dynamically\r\n colors: solar.colors // Default to solar colors for initial render\r\n};\r\n\r\n/**\r\n * Default themes registry\r\n */\r\nexport const themes = {\r\n solar,\r\n cave,\r\n auto,\r\n // Aliases for backward compatibility\r\n light: solar,\r\n dark: cave\r\n};\r\n\r\n/**\r\n * Get theme by name or return custom theme object\r\n * @param {string|Object} theme - Theme name or custom theme object\r\n * @returns {Object} Theme configuration\r\n */\r\nexport function getTheme(theme) {\r\n if (typeof theme === 'string') {\r\n const themeObj = themes[theme] || themes.solar;\r\n // Preserve the requested theme name (important for 'light' and 'dark' aliases)\r\n return { ...themeObj, name: theme };\r\n }\r\n return theme;\r\n}\r\n\r\n/**\r\n * Resolve auto theme to actual theme based on system color scheme preference\r\n * @param {string} themeName - Theme name to resolve\r\n * @returns {string} Resolved theme name ('solar' or 'cave' if auto, otherwise the original name)\r\n */\r\nexport function resolveAutoTheme(themeName) {\r\n if (themeName !== 'auto') {\r\n return themeName;\r\n }\r\n\r\n // Check for system dark mode preference\r\n const isDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;\r\n return isDarkMode ? 'cave' : 'solar';\r\n}\r\n\r\n/**\r\n * Get the current system color scheme preference\r\n * @returns {string} 'dark' or 'light'\r\n */\r\nexport function getSystemColorScheme() {\r\n const isDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;\r\n return isDarkMode ? 'dark' : 'light';\r\n}\r\n\r\n/**\r\n * Apply theme colors to CSS variables\r\n * @param {Object} colors - Theme colors object\r\n * @returns {string} CSS custom properties string\r\n */\r\nexport function themeToCSSVars(colors) {\r\n const vars = [];\r\n for (const [key, value] of Object.entries(colors)) {\r\n // Convert camelCase to kebab-case\r\n const varName = key.replace(/([A-Z])/g, '-$1').toLowerCase();\r\n vars.push(`--${varName}: ${value};`);\r\n }\r\n return vars.join('\\n');\r\n}\r\n\r\n/**\r\n * Merge custom colors with base theme\r\n * @param {Object} baseTheme - Base theme object\r\n * @param {Object} customColors - Custom color overrides\r\n * @returns {Object} Merged theme object\r\n */\r\nexport function mergeTheme(baseTheme, customColors = {}) {\r\n return {\r\n ...baseTheme,\r\n colors: {\r\n ...baseTheme.colors,\r\n ...customColors\r\n }\r\n };\r\n}", "/**\r\n * CSS styles for OverType editor\r\n * Embedded in JavaScript to ensure single-file distribution\r\n */\r\n\r\nimport { themeToCSSVars } from './themes.js';\r\n\r\n/**\r\n * Generate the complete CSS for the editor\r\n * @param {Object} options - Configuration options\r\n * @returns {string} Complete CSS string\r\n */\r\nexport function generateStyles(options = {}) {\r\n const {\r\n fontSize = '14px',\r\n lineHeight = 1.6,\r\n /* System-first, guaranteed monospaced; avoids Android 'ui-monospace' pitfalls */\r\n fontFamily = '\"SF Mono\", SFMono-Regular, Menlo, Monaco, \"Cascadia Code\", Consolas, \"Roboto Mono\", \"Noto Sans Mono\", \"Droid Sans Mono\", \"Ubuntu Mono\", \"DejaVu Sans Mono\", \"Liberation Mono\", \"Courier New\", Courier, monospace',\r\n padding = '20px',\r\n theme = null,\r\n mobile = {}\r\n } = options;\r\n\r\n // Generate mobile overrides\r\n const mobileStyles = Object.keys(mobile).length > 0 ? `\r\n @media (max-width: 640px) {\r\n .overtype-wrapper .overtype-input,\r\n .overtype-wrapper .overtype-preview {\r\n ${Object.entries(mobile)\r\n .map(([prop, val]) => {\r\n const cssProp = prop.replace(/([A-Z])/g, '-$1').toLowerCase();\r\n return `${cssProp}: ${val} !important;`;\r\n })\r\n .join('\\n ')}\r\n }\r\n }\r\n ` : '';\r\n\r\n // Generate theme variables if provided\r\n const themeVars = theme && theme.colors ? themeToCSSVars(theme.colors) : '';\r\n\r\n return `\r\n /* OverType Editor Styles */\r\n \r\n /* Middle-ground CSS Reset - Prevent parent styles from leaking in */\r\n .overtype-container * {\r\n /* Box model - these commonly leak */\r\n margin: 0 !important;\r\n padding: 0 !important;\r\n border: 0 !important;\r\n \r\n /* Layout - these can break our layout */\r\n /* Don't reset position - it breaks dropdowns */\r\n float: none !important;\r\n clear: none !important;\r\n \r\n /* Typography - only reset decorative aspects */\r\n text-decoration: none !important;\r\n text-transform: none !important;\r\n letter-spacing: normal !important;\r\n \r\n /* Visual effects that can interfere */\r\n box-shadow: none !important;\r\n text-shadow: none !important;\r\n \r\n /* Ensure box-sizing is consistent */\r\n box-sizing: border-box !important;\r\n \r\n /* Keep inheritance for these */\r\n /* font-family, color, line-height, font-size - inherit */\r\n }\r\n \r\n /* Container base styles after reset */\r\n .overtype-container {\r\n display: flex !important;\r\n flex-direction: column !important;\r\n width: 100% !important;\r\n height: 100% !important;\r\n position: relative !important; /* Override reset - needed for absolute children */\r\n overflow: visible !important; /* Allow dropdown to overflow container */\r\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif !important;\r\n text-align: left !important;\r\n ${themeVars ? `\r\n /* Theme Variables */\r\n ${themeVars}` : ''}\r\n }\r\n \r\n /* Force left alignment for all elements in the editor */\r\n .overtype-container .overtype-wrapper * {\r\n text-align: left !important;\r\n }\r\n \r\n /* Auto-resize mode styles */\r\n .overtype-container.overtype-auto-resize {\r\n height: auto !important;\r\n }\r\n\r\n .overtype-container.overtype-auto-resize .overtype-wrapper {\r\n flex: 0 0 auto !important; /* Don't grow/shrink, use explicit height */\r\n height: auto !important;\r\n min-height: 60px !important;\r\n overflow: visible !important;\r\n }\r\n \r\n .overtype-wrapper {\r\n position: relative !important; /* Override reset - needed for absolute children */\r\n width: 100% !important;\r\n flex: 1 1 0 !important; /* Grow to fill remaining space, with flex-basis: 0 */\r\n min-height: 60px !important; /* Minimum usable height */\r\n overflow: hidden !important;\r\n background: var(--bg-secondary, #ffffff) !important;\r\n z-index: 1; /* Below toolbar and dropdown */\r\n }\r\n\r\n /* Critical alignment styles - must be identical for both layers */\r\n .overtype-wrapper .overtype-input,\r\n .overtype-wrapper .overtype-preview {\r\n /* Positioning - must be identical */\r\n position: absolute !important; /* Override reset - required for overlay */\r\n top: 0 !important;\r\n left: 0 !important;\r\n width: 100% !important;\r\n height: 100% !important;\r\n \r\n /* Font properties - any difference breaks alignment */\r\n font-family: ${fontFamily} !important;\r\n font-variant-ligatures: none !important; /* keep metrics stable for code */\r\n font-size: var(--instance-font-size, ${fontSize}) !important;\r\n line-height: var(--instance-line-height, ${lineHeight}) !important;\r\n font-weight: normal !important;\r\n font-style: normal !important;\r\n font-variant: normal !important;\r\n font-stretch: normal !important;\r\n font-kerning: none !important;\r\n font-feature-settings: normal !important;\r\n \r\n /* Box model - must match exactly */\r\n padding: var(--instance-padding, ${padding}) !important;\r\n margin: 0 !important;\r\n border: none !important;\r\n outline: none !important;\r\n box-sizing: border-box !important;\r\n \r\n /* Text layout - critical for character positioning */\r\n white-space: pre-wrap !important;\r\n word-wrap: break-word !important;\r\n word-break: normal !important;\r\n overflow-wrap: break-word !important;\r\n tab-size: 2 !important;\r\n -moz-tab-size: 2 !important;\r\n text-align: left !important;\r\n text-indent: 0 !important;\r\n letter-spacing: normal !important;\r\n word-spacing: normal !important;\r\n \r\n /* Text rendering */\r\n text-transform: none !important;\r\n text-rendering: auto !important;\r\n -webkit-font-smoothing: auto !important;\r\n -webkit-text-size-adjust: 100% !important;\r\n \r\n /* Direction and writing */\r\n direction: ltr !important;\r\n writing-mode: horizontal-tb !important;\r\n unicode-bidi: normal !important;\r\n text-orientation: mixed !important;\r\n \r\n /* Visual effects that could shift perception */\r\n text-shadow: none !important;\r\n filter: none !important;\r\n transform: none !important;\r\n zoom: 1 !important;\r\n \r\n /* Vertical alignment */\r\n vertical-align: baseline !important;\r\n \r\n /* Size constraints */\r\n min-width: 0 !important;\r\n min-height: 0 !important;\r\n max-width: none !important;\r\n max-height: none !important;\r\n \r\n /* Overflow */\r\n overflow-y: auto !important;\r\n overflow-x: auto !important;\r\n /* overscroll-behavior removed to allow scroll-through to parent */\r\n scrollbar-width: auto !important;\r\n scrollbar-gutter: auto !important;\r\n \r\n /* Animation/transition - disabled to prevent movement */\r\n animation: none !important;\r\n transition: none !important;\r\n }\r\n\r\n /* Input layer styles */\r\n .overtype-wrapper .overtype-input {\r\n /* Layer positioning */\r\n z-index: 1 !important;\r\n \r\n /* Text visibility */\r\n color: transparent !important;\r\n caret-color: var(--cursor, #f95738) !important;\r\n background-color: transparent !important;\r\n \r\n /* Textarea-specific */\r\n resize: none !important;\r\n appearance: none !important;\r\n -webkit-appearance: none !important;\r\n -moz-appearance: none !important;\r\n \r\n /* Prevent mobile zoom on focus */\r\n touch-action: manipulation !important;\r\n \r\n /* Disable autofill and spellcheck */\r\n autocomplete: off !important;\r\n autocorrect: off !important;\r\n autocapitalize: off !important;\r\n spellcheck: false !important;\r\n }\r\n\r\n .overtype-wrapper .overtype-input::selection {\r\n background-color: var(--selection, rgba(244, 211, 94, 0.4));\r\n }\r\n\r\n /* Preview layer styles */\r\n .overtype-wrapper .overtype-preview {\r\n /* Layer positioning */\r\n z-index: 0 !important;\r\n pointer-events: none !important;\r\n color: var(--text, #0d3b66) !important;\r\n background-color: transparent !important;\r\n \r\n /* Prevent text selection */\r\n user-select: none !important;\r\n -webkit-user-select: none !important;\r\n -moz-user-select: none !important;\r\n -ms-user-select: none !important;\r\n }\r\n\r\n /* Defensive styles for preview child divs */\r\n .overtype-wrapper .overtype-preview div {\r\n /* Reset any inherited styles */\r\n margin: 0 !important;\r\n padding: 0 !important;\r\n border: none !important;\r\n text-align: left !important;\r\n text-indent: 0 !important;\r\n display: block !important;\r\n position: static !important;\r\n transform: none !important;\r\n min-height: 0 !important;\r\n max-height: none !important;\r\n line-height: inherit !important;\r\n font-size: inherit !important;\r\n font-family: inherit !important;\r\n }\r\n\r\n /* Markdown element styling - NO SIZE CHANGES */\r\n .overtype-wrapper .overtype-preview .header {\r\n font-weight: bold !important;\r\n }\r\n\r\n /* Header colors */\r\n .overtype-wrapper .overtype-preview .h1 { \r\n color: var(--h1, #f95738) !important; \r\n }\r\n .overtype-wrapper .overtype-preview .h2 { \r\n color: var(--h2, #ee964b) !important; \r\n }\r\n .overtype-wrapper .overtype-preview .h3 { \r\n color: var(--h3, #3d8a51) !important; \r\n }\r\n\r\n /* Semantic headers - flatten in edit mode */\r\n .overtype-wrapper .overtype-preview h1,\r\n .overtype-wrapper .overtype-preview h2,\r\n .overtype-wrapper .overtype-preview h3 {\r\n font-size: inherit !important;\r\n font-weight: bold !important;\r\n margin: 0 !important;\r\n padding: 0 !important;\r\n display: inline !important;\r\n line-height: inherit !important;\r\n }\r\n\r\n /* Header colors for semantic headers */\r\n .overtype-wrapper .overtype-preview h1 { \r\n color: var(--h1, #f95738) !important; \r\n }\r\n .overtype-wrapper .overtype-preview h2 { \r\n color: var(--h2, #ee964b) !important; \r\n }\r\n .overtype-wrapper .overtype-preview h3 { \r\n color: var(--h3, #3d8a51) !important; \r\n }\r\n\r\n /* Lists - remove styling in edit mode */\r\n .overtype-wrapper .overtype-preview ul,\r\n .overtype-wrapper .overtype-preview ol {\r\n list-style: none !important;\r\n margin: 0 !important;\r\n padding: 0 !important;\r\n display: block !important; /* Lists need to be block for line breaks */\r\n }\r\n\r\n .overtype-wrapper .overtype-preview li {\r\n display: block !important; /* Each item on its own line */\r\n margin: 0 !important;\r\n padding: 0 !important;\r\n /* Don't set list-style here - let ul/ol control it */\r\n }\r\n\r\n /* Bold text */\r\n .overtype-wrapper .overtype-preview strong {\r\n color: var(--strong, #ee964b) !important;\r\n font-weight: bold !important;\r\n }\r\n\r\n /* Italic text */\r\n .overtype-wrapper .overtype-preview em {\r\n color: var(--em, #f95738) !important;\r\n text-decoration-color: var(--em, #f95738) !important;\r\n text-decoration-thickness: 1px !important;\r\n font-style: italic !important;\r\n }\r\n\r\n /* Strikethrough text */\r\n .overtype-wrapper .overtype-preview del {\r\n color: var(--del, #ee964b) !important;\r\n text-decoration: line-through !important;\r\n text-decoration-color: var(--del, #ee964b) !important;\r\n text-decoration-thickness: 1px !important;\r\n }\r\n\r\n /* Inline code */\r\n .overtype-wrapper .overtype-preview code {\r\n background: var(--code-bg, rgba(244, 211, 94, 0.4)) !important;\r\n color: var(--code, #0d3b66) !important;\r\n padding: 0 !important;\r\n border-radius: 2px !important;\r\n font-family: inherit !important;\r\n font-size: inherit !important;\r\n line-height: inherit !important;\r\n font-weight: normal !important;\r\n }\r\n\r\n /* Code blocks - consolidated pre blocks */\r\n .overtype-wrapper .overtype-preview pre {\r\n padding: 0 !important;\r\n margin: 0 !important;\r\n border-radius: 4px !important;\r\n overflow-x: auto !important;\r\n }\r\n \r\n /* Code block styling in normal mode - yellow background */\r\n .overtype-wrapper .overtype-preview pre.code-block {\r\n background: var(--code-bg, rgba(244, 211, 94, 0.4)) !important;\r\n white-space: break-spaces !important; /* Prevent horizontal scrollbar that breaks alignment */\r\n }\r\n\r\n /* Code inside pre blocks - remove background */\r\n .overtype-wrapper .overtype-preview pre code {\r\n background: transparent !important;\r\n color: var(--code, #0d3b66) !important;\r\n font-family: ${fontFamily} !important; /* Match textarea font exactly for alignment */\r\n }\r\n\r\n /* Blockquotes */\r\n .overtype-wrapper .overtype-preview .blockquote {\r\n color: var(--blockquote, #5a7a9b) !important;\r\n padding: 0 !important;\r\n margin: 0 !important;\r\n border: none !important;\r\n }\r\n\r\n /* Links */\r\n .overtype-wrapper .overtype-preview a {\r\n color: var(--link, #0d3b66) !important;\r\n text-decoration: underline !important;\r\n font-weight: normal !important;\r\n }\r\n\r\n .overtype-wrapper .overtype-preview a:hover {\r\n text-decoration: underline !important;\r\n color: var(--link, #0d3b66) !important;\r\n }\r\n\r\n /* Lists - no list styling */\r\n .overtype-wrapper .overtype-preview ul,\r\n .overtype-wrapper .overtype-preview ol {\r\n list-style: none !important;\r\n margin: 0 !important;\r\n padding: 0 !important;\r\n }\r\n\r\n\r\n /* Horizontal rules */\r\n .overtype-wrapper .overtype-preview hr {\r\n border: none !important;\r\n color: var(--hr, #5a7a9b) !important;\r\n margin: 0 !important;\r\n padding: 0 !important;\r\n }\r\n\r\n .overtype-wrapper .overtype-preview .hr-marker {\r\n color: var(--hr, #5a7a9b) !important;\r\n opacity: 0.6 !important;\r\n }\r\n\r\n /* Code fence markers - with background when not in code block */\r\n .overtype-wrapper .overtype-preview .code-fence {\r\n color: var(--code, #0d3b66) !important;\r\n background: var(--code-bg, rgba(244, 211, 94, 0.4)) !important;\r\n }\r\n \r\n /* Code block lines - background for entire code block */\r\n .overtype-wrapper .overtype-preview .code-block-line {\r\n background: var(--code-bg, rgba(244, 211, 94, 0.4)) !important;\r\n }\r\n \r\n /* Remove background from code fence when inside code block line */\r\n .overtype-wrapper .overtype-preview .code-block-line .code-fence {\r\n background: transparent !important;\r\n }\r\n\r\n /* Raw markdown line */\r\n .overtype-wrapper .overtype-preview .raw-line {\r\n color: var(--raw-line, #5a7a9b) !important;\r\n font-style: normal !important;\r\n font-weight: normal !important;\r\n }\r\n\r\n /* Syntax markers */\r\n .overtype-wrapper .overtype-preview .syntax-marker {\r\n color: var(--syntax-marker, rgba(13, 59, 102, 0.52)) !important;\r\n opacity: 0.7 !important;\r\n }\r\n\r\n /* List markers */\r\n .overtype-wrapper .overtype-preview .list-marker {\r\n color: var(--list-marker, #ee964b) !important;\r\n }\r\n\r\n /* Stats bar */\r\n \r\n /* Stats bar - positioned by flexbox */\r\n .overtype-stats {\r\n height: 40px !important;\r\n padding: 0 20px !important;\r\n background: #f8f9fa !important;\r\n border-top: 1px solid #e0e0e0 !important;\r\n display: flex !important;\r\n justify-content: space-between !important;\r\n align-items: center !important;\r\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important;\r\n font-size: 0.85rem !important;\r\n color: #666 !important;\r\n flex-shrink: 0 !important; /* Don't shrink */\r\n z-index: 10001 !important; /* Above link tooltip */\r\n position: relative !important; /* Enable z-index */\r\n }\r\n \r\n /* Dark theme stats bar */\r\n .overtype-container[data-theme=\"cave\"] .overtype-stats {\r\n background: var(--bg-secondary, #1D2D3E) !important;\r\n border-top: 1px solid rgba(197, 221, 232, 0.1) !important;\r\n color: var(--text, #c5dde8) !important;\r\n }\r\n \r\n .overtype-stats .overtype-stat {\r\n display: flex !important;\r\n align-items: center !important;\r\n gap: 5px !important;\r\n white-space: nowrap !important;\r\n }\r\n \r\n .overtype-stats .live-dot {\r\n width: 8px !important;\r\n height: 8px !important;\r\n background: #4caf50 !important;\r\n border-radius: 50% !important;\r\n animation: overtype-pulse 2s infinite !important;\r\n }\r\n \r\n @keyframes overtype-pulse {\r\n 0%, 100% { opacity: 1; transform: scale(1); }\r\n 50% { opacity: 0.6; transform: scale(1.2); }\r\n }\r\n \r\n\r\n /* Toolbar Styles */\r\n .overtype-toolbar {\r\n display: flex !important;\r\n align-items: center !important;\r\n gap: 4px !important;\r\n padding: 8px !important; /* Override reset */\r\n background: var(--toolbar-bg, var(--bg-primary, #f8f9fa)) !important; /* Override reset */\r\n border-bottom: 1px solid var(--toolbar-border, transparent) !important; /* Override reset */\r\n overflow-x: auto !important; /* Allow horizontal scrolling */\r\n overflow-y: hidden !important; /* Hide vertical overflow */\r\n -webkit-overflow-scrolling: touch !important;\r\n flex-shrink: 0 !important;\r\n height: auto !important;\r\n position: relative !important; /* Override reset */\r\n z-index: 100 !important; /* Ensure toolbar is above wrapper */\r\n scrollbar-width: thin; /* Thin scrollbar on Firefox */\r\n }\r\n \r\n /* Thin scrollbar styling */\r\n .overtype-toolbar::-webkit-scrollbar {\r\n height: 4px;\r\n }\r\n \r\n .overtype-toolbar::-webkit-scrollbar-track {\r\n background: transparent;\r\n }\r\n \r\n .overtype-toolbar::-webkit-scrollbar-thumb {\r\n background: rgba(0, 0, 0, 0.2);\r\n border-radius: 2px;\r\n }\r\n\r\n .overtype-toolbar-button {\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n width: 32px;\r\n height: 32px;\r\n padding: 0;\r\n border: none;\r\n border-radius: 6px;\r\n background: transparent;\r\n color: var(--toolbar-icon, var(--text-secondary, #666));\r\n cursor: pointer;\r\n transition: all 0.2s ease;\r\n flex-shrink: 0;\r\n }\r\n\r\n .overtype-toolbar-button svg {\r\n width: 20px;\r\n height: 20px;\r\n fill: currentColor;\r\n }\r\n\r\n .overtype-toolbar-button:hover {\r\n background: var(--toolbar-hover, var(--bg-secondary, #e9ecef));\r\n color: var(--toolbar-icon, var(--text-primary, #333));\r\n }\r\n\r\n .overtype-toolbar-button:active {\r\n transform: scale(0.95);\r\n }\r\n\r\n .overtype-toolbar-button.active {\r\n background: var(--toolbar-active, var(--primary, #007bff));\r\n color: var(--toolbar-icon, var(--text-primary, #333));\r\n }\r\n\r\n .overtype-toolbar-button:disabled {\r\n opacity: 0.5;\r\n cursor: not-allowed;\r\n }\r\n\r\n .overtype-toolbar-separator {\r\n width: 1px;\r\n height: 24px;\r\n background: var(--border, #e0e0e0);\r\n margin: 0 4px;\r\n flex-shrink: 0;\r\n }\r\n\r\n /* Adjust wrapper when toolbar is present */\r\n /* Mobile toolbar adjustments */\r\n @media (max-width: 640px) {\r\n .overtype-toolbar {\r\n padding: 6px;\r\n gap: 2px;\r\n }\r\n\r\n .overtype-toolbar-button {\r\n width: 36px;\r\n height: 36px;\r\n }\r\n\r\n .overtype-toolbar-separator {\r\n margin: 0 2px;\r\n }\r\n }\r\n \r\n /* Plain mode - hide preview and show textarea text */\r\n .overtype-container[data-mode=\"plain\"] .overtype-preview {\r\n display: none !important;\r\n }\r\n \r\n .overtype-container[data-mode=\"plain\"] .overtype-input {\r\n color: var(--text, #0d3b66) !important;\r\n /* Use system font stack for better plain text readability */\r\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \r\n \"Helvetica Neue\", Arial, sans-serif !important;\r\n }\r\n \r\n /* Ensure textarea remains transparent in overlay mode */\r\n .overtype-container:not([data-mode=\"plain\"]) .overtype-input {\r\n color: transparent !important;\r\n }\r\n\r\n /* Dropdown menu styles */\r\n .overtype-toolbar-button {\r\n position: relative !important; /* Override reset - needed for dropdown */\r\n }\r\n\r\n .overtype-toolbar-button.dropdown-active {\r\n background: var(--toolbar-active, var(--hover-bg, #f0f0f0));\r\n }\r\n\r\n .overtype-dropdown-menu {\r\n position: fixed !important; /* Fixed positioning relative to viewport */\r\n background: var(--bg-secondary, white) !important; /* Override reset */\r\n border: 1px solid var(--border, #e0e0e0) !important; /* Override reset */\r\n border-radius: 6px;\r\n box-shadow: 0 2px 8px rgba(0,0,0,0.1) !important; /* Override reset */\r\n z-index: 10000; /* Very high z-index to ensure visibility */\r\n min-width: 150px;\r\n padding: 4px 0 !important; /* Override reset */\r\n /* Position will be set via JavaScript based on button position */\r\n }\r\n\r\n .overtype-dropdown-item {\r\n display: flex;\r\n align-items: center;\r\n width: 100%;\r\n padding: 8px 12px;\r\n border: none;\r\n background: none;\r\n text-align: left;\r\n cursor: pointer;\r\n font-size: 14px;\r\n color: var(--text, #333);\r\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\r\n }\r\n\r\n .overtype-dropdown-item:hover {\r\n background: var(--hover-bg, #f0f0f0);\r\n }\r\n\r\n .overtype-dropdown-item.active {\r\n font-weight: 600;\r\n }\r\n\r\n .overtype-dropdown-check {\r\n width: 16px;\r\n margin-right: 8px;\r\n color: var(--h1, #007bff);\r\n }\r\n\r\n .overtype-dropdown-icon {\r\n width: 20px;\r\n margin-right: 8px;\r\n text-align: center;\r\n }\r\n\r\n /* Preview mode styles */\r\n .overtype-container[data-mode=\"preview\"] .overtype-input {\r\n display: none !important;\r\n }\r\n\r\n .overtype-container[data-mode=\"preview\"] .overtype-preview {\r\n pointer-events: auto !important;\r\n user-select: text !important;\r\n cursor: text !important;\r\n }\r\n\r\n /* Hide syntax markers in preview mode */\r\n .overtype-container[data-mode=\"preview\"] .syntax-marker {\r\n display: none !important;\r\n }\r\n \r\n /* Hide URL part of links in preview mode - extra specificity */\r\n .overtype-container[data-mode=\"preview\"] .syntax-marker.url-part,\r\n .overtype-container[data-mode=\"preview\"] .url-part {\r\n display: none !important;\r\n }\r\n \r\n /* Hide all syntax markers inside links too */\r\n .overtype-container[data-mode=\"preview\"] a .syntax-marker {\r\n display: none !important;\r\n }\r\n\r\n /* Headers - restore proper sizing in preview mode */\r\n .overtype-container[data-mode=\"preview\"] .overtype-wrapper .overtype-preview h1, \r\n .overtype-container[data-mode=\"preview\"] .overtype-wrapper .overtype-preview h2, \r\n .overtype-container[data-mode=\"preview\"] .overtype-wrapper .overtype-preview h3 {\r\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif !important;\r\n font-weight: 600 !important;\r\n margin: 0 !important;\r\n display: block !important;\r\n color: inherit !important; /* Use parent text color */\r\n line-height: 1 !important; /* Tight line height for headings */\r\n }\r\n \r\n .overtype-container[data-mode=\"preview\"] .overtype-wrapper .overtype-preview h1 { \r\n font-size: 2em !important; \r\n }\r\n \r\n .overtype-container[data-mode=\"preview\"] .overtype-wrapper .overtype-preview h2 { \r\n font-size: 1.5em !important; \r\n }\r\n \r\n .overtype-container[data-mode=\"preview\"] .overtype-wrapper .overtype-preview h3 { \r\n font-size: 1.17em !important; \r\n }\r\n\r\n /* Lists - restore list styling in preview mode */\r\n .overtype-container[data-mode=\"preview\"] .overtype-wrapper .overtype-preview ul {\r\n display: block !important;\r\n list-style: disc !important;\r\n padding-left: 2em !important;\r\n margin: 1em 0 !important;\r\n }\r\n\r\n .overtype-container[data-mode=\"preview\"] .overtype-wrapper .overtype-preview ol {\r\n display: block !important;\r\n list-style: decimal !important;\r\n padding-left: 2em !important;\r\n margin: 1em 0 !important;\r\n }\r\n \r\n .overtype-container[data-mode=\"preview\"] .overtype-wrapper .overtype-preview li {\r\n display: list-item !important;\r\n margin: 0 !important;\r\n padding: 0 !important;\r\n }\r\n\r\n /* Task list checkboxes - only in preview mode */\r\n .overtype-container[data-mode=\"preview\"] .overtype-wrapper .overtype-preview li.task-list {\r\n list-style: none !important;\r\n position: relative !important;\r\n }\r\n\r\n .overtype-container[data-mode=\"preview\"] .overtype-wrapper .overtype-preview li.task-list input[type=\"checkbox\"] {\r\n margin-right: 0.5em !important;\r\n cursor: default !important;\r\n vertical-align: middle !important;\r\n }\r\n\r\n /* Task list in normal mode - keep syntax visible */\r\n .overtype-container:not([data-mode=\"preview\"]) .overtype-wrapper .overtype-preview li.task-list {\r\n list-style: none !important;\r\n }\r\n\r\n .overtype-container:not([data-mode=\"preview\"]) .overtype-wrapper .overtype-preview li.task-list .syntax-marker {\r\n color: var(--syntax, #999999) !important;\r\n font-weight: normal !important;\r\n }\r\n\r\n /* Links - make clickable in preview mode */\r\n .overtype-container[data-mode=\"preview\"] .overtype-wrapper .overtype-preview a {\r\n pointer-events: auto !important;\r\n cursor: pointer !important;\r\n color: var(--link, #0066cc) !important;\r\n text-decoration: underline !important;\r\n }\r\n\r\n /* Code blocks - proper pre/code styling in preview mode */\r\n .overtype-container[data-mode=\"preview\"] .overtype-wrapper .overtype-preview pre.code-block {\r\n background: #2d2d2d !important;\r\n color: #f8f8f2 !important;\r\n padding: 1.2em !important;\r\n border-radius: 3px !important;\r\n overflow-x: auto !important;\r\n margin: 0 !important;\r\n display: block !important;\r\n }\r\n \r\n /* Cave theme code block background in preview mode */\r\n .overtype-container[data-theme=\"cave\"][data-mode=\"preview\"] .overtype-wrapper .overtype-preview pre.code-block {\r\n background: #11171F !important;\r\n }\r\n\r\n .overtype-container[data-mode=\"preview\"] .overtype-wrapper .overtype-preview pre.code-block code {\r\n background: transparent !important;\r\n color: inherit !important;\r\n padding: 0 !important;\r\n font-family: ${fontFamily} !important;\r\n font-size: 0.9em !important;\r\n line-height: 1.4 !important;\r\n }\r\n\r\n /* Hide old code block lines and fences in preview mode */\r\n .overtype-container[data-mode=\"preview\"] .overtype-wrapper .overtype-preview .code-block-line {\r\n display: none !important;\r\n }\r\n\r\n .overtype-container[data-mode=\"preview\"] .overtype-wrapper .overtype-preview .code-fence {\r\n display: none !important;\r\n }\r\n\r\n /* Blockquotes - enhanced styling in preview mode */\r\n .overtype-container[data-mode=\"preview\"] .overtype-wrapper .overtype-preview .blockquote {\r\n display: block !important;\r\n border-left: 4px solid var(--blockquote, #ddd) !important;\r\n padding-left: 1em !important;\r\n margin: 1em 0 !important;\r\n font-style: italic !important;\r\n }\r\n\r\n /* Typography improvements in preview mode */\r\n .overtype-container[data-mode=\"preview\"] .overtype-wrapper .overtype-preview {\r\n font-family: Georgia, 'Times New Roman', serif !important;\r\n font-size: 16px !important;\r\n line-height: 1.8 !important;\r\n color: var(--text, #333) !important; /* Consistent text color */\r\n }\r\n\r\n /* Inline code in preview mode - keep monospace */\r\n .overtype-container[data-mode=\"preview\"] .overtype-wrapper .overtype-preview code {\r\n font-family: ${fontFamily} !important;\r\n font-size: 0.9em !important;\r\n background: rgba(135, 131, 120, 0.15) !important;\r\n padding: 0.2em 0.4em !important;\r\n border-radius: 3px !important;\r\n }\r\n\r\n /* Strong and em elements in preview mode */\r\n .overtype-container[data-mode=\"preview\"] .overtype-wrapper .overtype-preview strong {\r\n font-weight: 700 !important;\r\n color: inherit !important; /* Use parent text color */\r\n }\r\n\r\n .overtype-container[data-mode=\"preview\"] .overtype-wrapper .overtype-preview em {\r\n font-style: italic !important;\r\n color: inherit !important; /* Use parent text color */\r\n }\r\n\r\n /* HR in preview mode */\r\n .overtype-container[data-mode=\"preview\"] .overtype-wrapper .overtype-preview .hr-marker {\r\n display: block !important;\r\n border-top: 2px solid var(--hr, #ddd) !important;\r\n text-indent: -9999px !important;\r\n height: 2px !important;\r\n }\r\n\r\n /* Link Tooltip - Base styles (all browsers) */\r\n .overtype-link-tooltip {\r\n /* Visual styles that work for both positioning methods */\r\n background: #333 !important;\r\n color: white !important;\r\n padding: 6px 10px !important;\r\n border-radius: 16px !important;\r\n font-size: 12px !important;\r\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important;\r\n display: none !important;\r\n z-index: 10000 !important;\r\n cursor: pointer !important;\r\n box-shadow: 0 2px 8px rgba(0,0,0,0.3) !important;\r\n max-width: 300px !important;\r\n white-space: nowrap !important;\r\n overflow: hidden !important;\r\n text-overflow: ellipsis !important;\r\n\r\n /* Base positioning for Floating UI fallback */\r\n position: absolute;\r\n }\r\n\r\n .overtype-link-tooltip.visible {\r\n display: flex !important;\r\n }\r\n\r\n /* CSS Anchor Positioning (modern browsers only) */\r\n @supports (position-anchor: --x) and (position-area: center) {\r\n .overtype-link-tooltip {\r\n /* Only anchor positioning specific properties */\r\n position-anchor: var(--target-anchor, --link-0);\r\n position-area: block-end center;\r\n margin-top: 8px !important;\r\n position-try: most-width block-end inline-end, flip-inline, block-start center;\r\n position-visibility: anchors-visible;\r\n }\r\n }\r\n\r\n ${mobileStyles}\r\n `;\r\n}", "/**\n * Format definitions for markdown syntax\n */\n\nexport const FORMATS = {\n bold: {\n prefix: '**',\n suffix: '**',\n trimFirst: true\n },\n italic: {\n prefix: '_',\n suffix: '_',\n trimFirst: true\n },\n code: {\n prefix: '`',\n suffix: '`',\n blockPrefix: '```',\n blockSuffix: '```'\n },\n link: {\n prefix: '[',\n suffix: '](url)',\n replaceNext: 'url',\n scanFor: 'https?://'\n },\n bulletList: {\n prefix: '- ',\n multiline: true,\n unorderedList: true\n },\n numberedList: {\n prefix: '1. ',\n multiline: true,\n orderedList: true\n },\n quote: {\n prefix: '> ',\n multiline: true,\n surroundWithNewlines: true\n },\n taskList: {\n prefix: '- [ ] ',\n multiline: true,\n surroundWithNewlines: true\n },\n header1: { prefix: '# ' },\n header2: { prefix: '## ' },\n header3: { prefix: '### ' },\n header4: { prefix: '#### ' },\n header5: { prefix: '##### ' },\n header6: { prefix: '###### ' }\n}\n\n/**\n * Default style configuration\n */\nexport function getDefaultStyle() {\n return {\n prefix: '',\n suffix: '',\n blockPrefix: '',\n blockSuffix: '',\n multiline: false,\n replaceNext: '',\n prefixSpace: false,\n scanFor: '',\n surroundWithNewlines: false,\n orderedList: false,\n unorderedList: false,\n trimFirst: false\n }\n}\n\n/**\n * Merge format with defaults\n */\nexport function mergeWithDefaults(format) {\n return { ...getDefaultStyle(), ...format }\n}", "/**\n * Debug utilities for markdown-actions\n * Add console logging to track what's happening\n */\n\n// Debug mode flag - disabled by default\nlet debugMode = false;\n\n/**\n * Enable or disable debug mode\n * @param {boolean} enabled - Whether to enable debug mode\n */\nexport function setDebugMode(enabled) {\n debugMode = enabled;\n}\n\n/**\n * Get current debug mode status\n * @returns {boolean} Whether debug mode is enabled\n */\nexport function getDebugMode() {\n return debugMode;\n}\n\nexport function debugLog(funcName, message, data) {\n // These will be completely removed by esbuild's drop: ['console'] in production\n if (!debugMode) return;\n \n console.group(`\uD83D\uDD0D ${funcName}`);\n console.log(message);\n if (data) {\n console.log('Data:', data);\n }\n console.groupEnd();\n}\n\nexport function debugSelection(textarea, label) {\n // These will be completely removed by esbuild's drop: ['console'] in production\n if (!debugMode) return;\n \n const selected = textarea.value.slice(textarea.selectionStart, textarea.selectionEnd);\n console.group(`\uD83D\uDCCD Selection: ${label}`);\n console.log('Position:', `${textarea.selectionStart}-${textarea.selectionEnd}`);\n console.log('Selected text:', JSON.stringify(selected));\n console.log('Length:', selected.length);\n \n // Show context around selection\n const before = textarea.value.slice(Math.max(0, textarea.selectionStart - 10), textarea.selectionStart);\n const after = textarea.value.slice(textarea.selectionEnd, Math.min(textarea.value.length, textarea.selectionEnd + 10));\n console.log('Context:', JSON.stringify(before) + '[SELECTION]' + JSON.stringify(after));\n console.groupEnd();\n}\n\nexport function debugResult(result) {\n // These will be completely removed by esbuild's drop: ['console'] in production\n if (!debugMode) return;\n \n console.group('\uD83D\uDCDD Result');\n console.log('Text to insert:', JSON.stringify(result.text));\n console.log('New selection:', `${result.selectionStart}-${result.selectionEnd}`);\n console.groupEnd();\n}", "/**\n * Text insertion system with undo/redo support\n * Extracted and adapted from GitHub's markdown-toolbar-element\n */\n\nimport { getDebugMode } from '../debug.js'\n\nlet canInsertText = null\n\n/**\n * Insert text at current position with undo/redo support\n * @param {HTMLTextAreaElement} textarea - Target textarea\n * @param {Object} options - Text and selection options\n * @param {string} options.text - Text to insert\n * @param {number} [options.selectionStart] - New selection start\n * @param {number} [options.selectionEnd] - New selection end\n */\nexport function insertText(textarea, { text, selectionStart, selectionEnd }) {\n const debugMode = getDebugMode();\n \n if (debugMode) {\n console.group('\uD83D\uDD27 insertText');\n console.log('Current selection:', `${textarea.selectionStart}-${textarea.selectionEnd}`);\n console.log('Text to insert:', JSON.stringify(text));\n console.log('New selection to set:', selectionStart, '-', selectionEnd);\n }\n \n // Make sure the textarea is focused\n textarea.focus();\n \n const originalSelectionStart = textarea.selectionStart\n const originalSelectionEnd = textarea.selectionEnd\n const before = textarea.value.slice(0, originalSelectionStart)\n const after = textarea.value.slice(originalSelectionEnd)\n \n if (debugMode) {\n console.log('Before text (last 20):', JSON.stringify(before.slice(-20)));\n console.log('After text (first 20):', JSON.stringify(after.slice(0, 20)));\n console.log('Selected text being replaced:', JSON.stringify(textarea.value.slice(originalSelectionStart, originalSelectionEnd)));\n }\n \n // Store the original value to check if execCommand actually changed it\n const originalValue = textarea.value\n\n // Try execCommand for both insertions and replacements to preserve undo history\n // execCommand('insertText') can handle replacing selected text\n const hasSelection = originalSelectionStart !== originalSelectionEnd\n \n if (canInsertText === null || canInsertText === true) {\n textarea.contentEditable = 'true'\n try {\n canInsertText = document.execCommand('insertText', false, text)\n if (debugMode) console.log('execCommand returned:', canInsertText, 'for text with', text.split('\\n').length, 'lines');\n } catch (error) {\n canInsertText = false\n if (debugMode) console.log('execCommand threw error:', error);\n }\n textarea.contentEditable = 'false'\n }\n\n if (debugMode) {\n console.log('canInsertText before:', canInsertText);\n console.log('execCommand result:', canInsertText);\n }\n \n // Check if execCommand actually worked by comparing the value\n if (canInsertText) {\n const expectedValue = before + text + after\n const actualValue = textarea.value\n \n if (debugMode) {\n console.log('Expected length:', expectedValue.length);\n console.log('Actual length:', actualValue.length);\n }\n \n if (actualValue !== expectedValue) {\n if (debugMode) {\n console.log('execCommand changed the value but not as expected');\n console.log('Expected:', JSON.stringify(expectedValue.slice(0, 100)));\n console.log('Actual:', JSON.stringify(actualValue.slice(0, 100)));\n }\n // Don't set canInsertText to false here - execCommand did work\n // We just need to not double-insert\n }\n }\n\n if (!canInsertText) {\n if (debugMode) console.log('Using manual insertion');\n // Only do manual insertion if execCommand didn't change the value\n if (textarea.value === originalValue) {\n if (debugMode) console.log('Value unchanged, doing manual replacement');\n try {\n document.execCommand('ms-beginUndoUnit')\n } catch (e) {\n // Do nothing.\n }\n textarea.value = before + text + after\n try {\n document.execCommand('ms-endUndoUnit')\n } catch (e) {\n // Do nothing.\n }\n textarea.dispatchEvent(new CustomEvent('input', { bubbles: true, cancelable: true }))\n } else {\n if (debugMode) console.log('Value was changed by execCommand, skipping manual insertion');\n }\n }\n\n if (debugMode) console.log('Setting selection range:', selectionStart, selectionEnd);\n if (selectionStart != null && selectionEnd != null) {\n textarea.setSelectionRange(selectionStart, selectionEnd)\n } else {\n textarea.setSelectionRange(originalSelectionStart, textarea.selectionEnd)\n }\n \n if (debugMode) {\n console.log('Final value length:', textarea.value.length);\n console.groupEnd();\n }\n}\n\n/**\n * Configure undo method\n * @param {'native' | 'manual' | 'auto'} method - Undo method to use\n */\nexport function setUndoMethod(method) {\n switch (method) {\n case 'native':\n canInsertText = true\n break\n case 'manual':\n canInsertText = false\n break\n case 'auto':\n canInsertText = null\n break\n }\n}", "/**\n * Core selection utilities extracted and adapted from GitHub's markdown-toolbar-element\n */\n\n/**\n * Check if string contains multiple lines\n */\nexport function isMultipleLines(string) {\n return string.trim().split('\\n').length > 1\n}\n\n/**\n * Find the start of the word at position i\n */\nexport function wordSelectionStart(text, i) {\n let index = i\n while (text[index] && text[index - 1] != null && !text[index - 1].match(/\\s/)) {\n index--\n }\n return index\n}\n\n/**\n * Find the end of the word at position i\n */\nexport function wordSelectionEnd(text, i, multiline) {\n let index = i\n const breakpoint = multiline ? /\\n/ : /\\s/\n while (text[index] && !text[index].match(breakpoint)) {\n index++\n }\n return index\n}\n\n/**\n * Expand selection to line boundaries\n */\nexport function expandSelectionToLine(textarea) {\n const lines = textarea.value.split('\\n')\n let counter = 0\n for (let index = 0; index < lines.length; index++) {\n const lineLength = lines[index].length + 1\n if (textarea.selectionStart >= counter && textarea.selectionStart < counter + lineLength) {\n textarea.selectionStart = counter\n }\n if (textarea.selectionEnd >= counter && textarea.selectionEnd < counter + lineLength) {\n // For the last line, don't go past the actual text length\n if (index === lines.length - 1) {\n textarea.selectionEnd = Math.min(counter + lines[index].length, textarea.value.length)\n } else {\n textarea.selectionEnd = counter + lineLength - 1\n }\n }\n counter += lineLength\n }\n}\n\n/**\n * Expand selected text with smart boundary detection\n */\nexport function expandSelectedText(textarea, prefixToUse, suffixToUse, multiline = false) {\n if (textarea.selectionStart === textarea.selectionEnd) {\n textarea.selectionStart = wordSelectionStart(textarea.value, textarea.selectionStart)\n textarea.selectionEnd = wordSelectionEnd(textarea.value, textarea.selectionEnd, multiline)\n } else {\n const expandedSelectionStart = textarea.selectionStart - prefixToUse.length\n const expandedSelectionEnd = textarea.selectionEnd + suffixToUse.length\n const beginsWithPrefix = textarea.value.slice(expandedSelectionStart, textarea.selectionStart) === prefixToUse\n const endsWithSuffix = textarea.value.slice(textarea.selectionEnd, expandedSelectionEnd) === suffixToUse\n if (beginsWithPrefix && endsWithSuffix) {\n textarea.selectionStart = expandedSelectionStart\n textarea.selectionEnd = expandedSelectionEnd\n }\n }\n return textarea.value.slice(textarea.selectionStart, textarea.selectionEnd)\n}\n\n/**\n * Calculate newlines needed to surround selected text\n */\nexport function newlinesToSurroundSelectedText(textarea) {\n const beforeSelection = textarea.value.slice(0, textarea.selectionStart)\n const afterSelection = textarea.value.slice(textarea.selectionEnd)\n\n const breaksBefore = beforeSelection.match(/\\n*$/)\n const breaksAfter = afterSelection.match(/^\\n*/)\n const newlinesBeforeSelection = breaksBefore ? breaksBefore[0].length : 0\n const newlinesAfterSelection = breaksAfter ? breaksAfter[0].length : 0\n\n let newlinesToAppend = ''\n let newlinesToPrepend = ''\n\n if (beforeSelection.match(/\\S/) && newlinesBeforeSelection < 2) {\n newlinesToAppend = '\\n'.repeat(2 - newlinesBeforeSelection)\n }\n\n if (afterSelection.match(/\\S/) && newlinesAfterSelection < 2) {\n newlinesToPrepend = '\\n'.repeat(2 - newlinesAfterSelection)\n }\n\n return { newlinesToAppend, newlinesToPrepend }\n}\n\n/**\n * Utility to preserve selection during operations\n */\nexport function preserveSelection(textarea, callback) {\n const start = textarea.selectionStart\n const end = textarea.selectionEnd\n const scrollTop = textarea.scrollTop\n \n callback()\n \n textarea.selectionStart = start\n textarea.selectionEnd = end\n textarea.scrollTop = scrollTop\n}\n\n/**\n * Apply a line-based operation with cursor preservation\n * This function handles expanding to line boundaries and preserving cursor position\n * @param {HTMLTextAreaElement} textarea - The textarea element\n * @param {Function} operation - The operation to perform (receives textarea and returns result)\n * @param {Object} options - Options for the operation\n * @param {string} options.prefix - The prefix being added/removed (for cursor adjustment)\n * @param {Function} options.adjustSelection - Custom selection adjustment function\n * @returns {Object} The result from the operation with adjusted cursor position\n */\nexport function applyLineOperation(textarea, operation, options = {}) {\n // Save original cursor position AND selection before any expansion\n const originalStart = textarea.selectionStart\n const originalEnd = textarea.selectionEnd\n const noInitialSelection = originalStart === originalEnd\n \n // Store the line start position to calculate offset later\n const value = textarea.value\n let lineStart = originalStart\n \n // Find start of the line containing the selection start\n while (lineStart > 0 && value[lineStart - 1] !== '\\n') {\n lineStart--\n }\n \n // Expand selection to line boundaries for the operation\n if (noInitialSelection) {\n // Expand to current line when no selection\n let lineEnd = originalStart\n \n // Find end of current line\n while (lineEnd < value.length && value[lineEnd] !== '\\n') {\n lineEnd++\n }\n \n textarea.selectionStart = lineStart\n textarea.selectionEnd = lineEnd\n } else {\n // For selections, expand to full lines\n expandSelectionToLine(textarea)\n }\n \n // Apply the operation\n const result = operation(textarea)\n \n // Restore original selection/cursor with prefix adjustment\n if (options.adjustSelection) {\n // Use custom selection adjustment logic\n const selectedText = textarea.value.slice(textarea.selectionStart, textarea.selectionEnd)\n const isRemoving = selectedText.startsWith(options.prefix)\n const adjusted = options.adjustSelection(isRemoving, originalStart, originalEnd, lineStart)\n result.selectionStart = adjusted.start\n result.selectionEnd = adjusted.end\n } else if (options.prefix) {\n // Use default prefix-based adjustment\n const selectedText = textarea.value.slice(textarea.selectionStart, textarea.selectionEnd)\n const isRemoving = selectedText.startsWith(options.prefix)\n \n if (noInitialSelection) {\n // No selection - just restore cursor position\n if (isRemoving) {\n // When removing prefix, adjust cursor position\n result.selectionStart = Math.max(originalStart - options.prefix.length, lineStart)\n result.selectionEnd = result.selectionStart\n } else {\n // When adding prefix, adjust cursor position\n result.selectionStart = originalStart + options.prefix.length\n result.selectionEnd = result.selectionStart\n }\n } else {\n // Had a selection - restore it with adjustment\n if (isRemoving) {\n // When removing prefix, shift selection back\n result.selectionStart = Math.max(originalStart - options.prefix.length, lineStart)\n result.selectionEnd = Math.max(originalEnd - options.prefix.length, lineStart)\n } else {\n // When adding prefix, shift selection forward\n result.selectionStart = originalStart + options.prefix.length\n result.selectionEnd = originalEnd + options.prefix.length\n }\n }\n }\n \n return result\n}", "/**\n * Block-level text formatting operations\n * Handles inline formats like bold, italic, code, and links\n */\n\nimport { expandSelectedText, newlinesToSurroundSelectedText, isMultipleLines } from '../core/selection.js'\nimport { insertText } from '../core/insertion.js'\n\n/**\n * Apply block-level styling to selected text\n */\nexport function blockStyle(textarea, style) {\n let newlinesToAppend\n let newlinesToPrepend\n\n const { prefix, suffix, blockPrefix, blockSuffix, replaceNext, prefixSpace, scanFor, surroundWithNewlines, trimFirst } = style\n const originalSelectionStart = textarea.selectionStart\n const originalSelectionEnd = textarea.selectionEnd\n\n let selectedText = textarea.value.slice(textarea.selectionStart, textarea.selectionEnd)\n let prefixToUse = isMultipleLines(selectedText) && blockPrefix && blockPrefix.length > 0 ? `${blockPrefix}\\n` : prefix\n let suffixToUse = isMultipleLines(selectedText) && blockSuffix && blockSuffix.length > 0 ? `\\n${blockSuffix}` : suffix\n\n if (prefixSpace) {\n const beforeSelection = textarea.value[textarea.selectionStart - 1]\n if (textarea.selectionStart !== 0 && beforeSelection != null && !beforeSelection.match(/\\s/)) {\n prefixToUse = ` ${prefixToUse}`\n }\n }\n \n selectedText = expandSelectedText(textarea, prefixToUse, suffixToUse, style.multiline)\n let selectionStart = textarea.selectionStart\n let selectionEnd = textarea.selectionEnd\n const hasReplaceNext = replaceNext && replaceNext.length > 0 && suffixToUse.indexOf(replaceNext) > -1 && selectedText.length > 0\n \n if (surroundWithNewlines) {\n const ref = newlinesToSurroundSelectedText(textarea)\n newlinesToAppend = ref.newlinesToAppend\n newlinesToPrepend = ref.newlinesToPrepend\n prefixToUse = newlinesToAppend + prefix\n suffixToUse += newlinesToPrepend\n }\n\n // Check if we should remove formatting (toggle off)\n if (selectedText.startsWith(prefixToUse) && selectedText.endsWith(suffixToUse)) {\n const replacementText = selectedText.slice(prefixToUse.length, selectedText.length - suffixToUse.length)\n if (originalSelectionStart === originalSelectionEnd) {\n let position = originalSelectionStart - prefixToUse.length\n position = Math.max(position, selectionStart)\n position = Math.min(position, selectionStart + replacementText.length)\n selectionStart = selectionEnd = position\n } else {\n selectionEnd = selectionStart + replacementText.length\n }\n return { text: replacementText, selectionStart, selectionEnd }\n } else if (!hasReplaceNext) {\n // Add formatting\n let replacementText = prefixToUse + selectedText + suffixToUse\n selectionStart = originalSelectionStart + prefixToUse.length\n selectionEnd = originalSelectionEnd + prefixToUse.length\n const whitespaceEdges = selectedText.match(/^\\s*|\\s*$/g)\n if (trimFirst && whitespaceEdges) {\n const leadingWhitespace = whitespaceEdges[0] || ''\n const trailingWhitespace = whitespaceEdges[1] || ''\n replacementText = leadingWhitespace + prefixToUse + selectedText.trim() + suffixToUse + trailingWhitespace\n selectionStart += leadingWhitespace.length\n selectionEnd -= trailingWhitespace.length\n }\n return { text: replacementText, selectionStart, selectionEnd }\n } else if (scanFor && scanFor.length > 0 && selectedText.match(scanFor)) {\n // Handle link/image with URL detection\n suffixToUse = suffixToUse.replace(replaceNext, selectedText)\n const replacementText = prefixToUse + suffixToUse\n selectionStart = selectionEnd = selectionStart + prefixToUse.length\n return { text: replacementText, selectionStart, selectionEnd }\n } else {\n // Handle link/image with placeholder\n const replacementText = prefixToUse + selectedText + suffixToUse\n selectionStart = selectionStart + prefixToUse.length + selectedText.length + suffixToUse.indexOf(replaceNext)\n selectionEnd = selectionStart + replaceNext.length\n return { text: replacementText, selectionStart, selectionEnd }\n }\n}\n\n/**\n * Apply style to selected text in textarea\n */\nexport function styleSelectedText(textarea, style) {\n const text = textarea.value.slice(textarea.selectionStart, textarea.selectionEnd)\n \n let result\n if (style.orderedList || style.unorderedList) {\n // Will be handled by list operations\n return\n } else if (style.multiline && isMultipleLines(text)) {\n result = multilineStyle(textarea, style)\n } else {\n result = blockStyle(textarea, style)\n }\n\n insertText(textarea, result)\n}\n\n/**\n * Apply multiline styling (quotes, task lists, etc)\n * Note: This does NOT expand selection to line - that should be done by the caller if needed\n */\nexport function multilineStyle(textarea, style) {\n const { prefix, suffix, surroundWithNewlines } = style\n let text = textarea.value.slice(textarea.selectionStart, textarea.selectionEnd)\n let selectionStart = textarea.selectionStart\n let selectionEnd = textarea.selectionEnd\n const lines = text.split('\\n')\n \n // Check if we need to undo (all lines have the format)\n const undoStyle = lines.every(line => line.startsWith(prefix) && (!suffix || line.endsWith(suffix)))\n\n if (undoStyle) {\n // Remove the formatting\n text = lines.map(line => {\n let result = line.slice(prefix.length)\n if (suffix) {\n result = result.slice(0, result.length - suffix.length)\n }\n return result\n }).join('\\n')\n selectionEnd = selectionStart + text.length\n } else {\n // Apply the formatting\n text = lines.map(line => prefix + line + (suffix || '')).join('\\n')\n if (surroundWithNewlines) {\n const { newlinesToAppend, newlinesToPrepend } = newlinesToSurroundSelectedText(textarea)\n selectionStart += newlinesToAppend.length\n selectionEnd = selectionStart + text.length\n text = newlinesToAppend + text + newlinesToPrepend\n }\n }\n\n return { text, selectionStart, selectionEnd }\n}", "/**\n * List operations for bullet and numbered lists\n */\n\nimport { expandSelectionToLine, newlinesToSurroundSelectedText, applyLineOperation } from '../core/selection.js'\nimport { insertText } from '../core/insertion.js'\n\n/**\n * Undo ordered list formatting\n */\nfunction undoOrderedListStyle(text) {\n const lines = text.split('\\n')\n const orderedListRegex = /^\\d+\\.\\s+/\n const shouldUndoOrderedList = lines.every(line => orderedListRegex.test(line))\n let result = lines\n if (shouldUndoOrderedList) {\n result = lines.map(line => line.replace(orderedListRegex, ''))\n }\n\n return {\n text: result.join('\\n'),\n processed: shouldUndoOrderedList\n }\n}\n\n/**\n * Undo unordered list formatting\n */\nfunction undoUnorderedListStyle(text) {\n const lines = text.split('\\n')\n const unorderedListPrefix = '- '\n const shouldUndoUnorderedList = lines.every(line => line.startsWith(unorderedListPrefix))\n let result = lines\n if (shouldUndoUnorderedList) {\n result = lines.map(line => line.slice(unorderedListPrefix.length))\n }\n\n return {\n text: result.join('\\n'),\n processed: shouldUndoUnorderedList\n }\n}\n\n/**\n * Make prefix for list item\n */\nfunction makePrefix(index, unorderedList) {\n if (unorderedList) {\n return '- '\n } else {\n return `${index + 1}. `\n }\n}\n\n/**\n * Clear existing list style\n */\nfunction clearExistingListStyle(style, selectedText) {\n let undoResult\n let undoResultOppositeList\n let pristineText\n \n if (style.orderedList) {\n undoResult = undoOrderedListStyle(selectedText)\n undoResultOppositeList = undoUnorderedListStyle(undoResult.text)\n pristineText = undoResultOppositeList.text\n } else {\n undoResult = undoUnorderedListStyle(selectedText)\n undoResultOppositeList = undoOrderedListStyle(undoResult.text)\n pristineText = undoResultOppositeList.text\n }\n \n return [undoResult, undoResultOppositeList, pristineText]\n}\n\n/**\n * Apply list styling to selected text\n */\nexport function listStyle(textarea, style) {\n const noInitialSelection = textarea.selectionStart === textarea.selectionEnd\n let selectionStart = textarea.selectionStart\n let selectionEnd = textarea.selectionEnd\n\n // Select whole line\n expandSelectionToLine(textarea)\n\n const selectedText = textarea.value.slice(textarea.selectionStart, textarea.selectionEnd)\n\n // Clear any existing list formatting\n const [undoResult, undoResultOppositeList, pristineText] = clearExistingListStyle(style, selectedText)\n\n const prefixedLines = pristineText.split('\\n').map((value, index) => {\n return `${makePrefix(index, style.unorderedList)}${value}`\n })\n\n const totalPrefixLength = prefixedLines.reduce((previousValue, _currentValue, currentIndex) => {\n return previousValue + makePrefix(currentIndex, style.unorderedList).length\n }, 0)\n\n const totalPrefixLengthOppositeList = prefixedLines.reduce((previousValue, _currentValue, currentIndex) => {\n return previousValue + makePrefix(currentIndex, !style.unorderedList).length\n }, 0)\n\n // If we're undoing the same list type, just return the pristine text\n if (undoResult.processed) {\n if (noInitialSelection) {\n selectionStart = Math.max(selectionStart - makePrefix(0, style.unorderedList).length, 0)\n selectionEnd = selectionStart\n } else {\n selectionStart = textarea.selectionStart\n selectionEnd = textarea.selectionEnd - totalPrefixLength\n }\n return { text: pristineText, selectionStart, selectionEnd }\n }\n\n // Apply new list formatting\n const { newlinesToAppend, newlinesToPrepend } = newlinesToSurroundSelectedText(textarea)\n const text = newlinesToAppend + prefixedLines.join('\\n') + newlinesToPrepend\n\n if (noInitialSelection) {\n selectionStart = Math.max(selectionStart + makePrefix(0, style.unorderedList).length + newlinesToAppend.length, 0)\n selectionEnd = selectionStart\n } else {\n if (undoResultOppositeList.processed) {\n // Converting from one list type to another\n selectionStart = Math.max(textarea.selectionStart + newlinesToAppend.length, 0)\n selectionEnd = textarea.selectionEnd + newlinesToAppend.length + totalPrefixLength - totalPrefixLengthOppositeList\n } else {\n // Adding list formatting to plain text\n selectionStart = Math.max(textarea.selectionStart + newlinesToAppend.length, 0)\n selectionEnd = textarea.selectionEnd + newlinesToAppend.length + totalPrefixLength\n }\n }\n\n return { text, selectionStart, selectionEnd }\n}\n\n/**\n * Apply list style to textarea\n */\nexport function applyListStyle(textarea, style) {\n // Use applyLineOperation for consistent selection preservation\n const result = applyLineOperation(\n textarea,\n (ta) => listStyle(ta, style),\n {\n // Custom selection adjustment for lists\n adjustSelection: (isRemoving, selStart, selEnd, lineStart) => {\n // Get the current line to check if we're removing\n const currentLine = textarea.value.slice(lineStart, textarea.selectionEnd)\n const orderedListRegex = /^\\d+\\.\\s+/\n const unorderedListRegex = /^- /\n \n // Check if we're removing a list\n const hasOrderedList = orderedListRegex.test(currentLine)\n const hasUnorderedList = unorderedListRegex.test(currentLine)\n const isRemovingCurrent = (style.orderedList && hasOrderedList) || (style.unorderedList && hasUnorderedList)\n \n if (selStart === selEnd) {\n // No selection - cursor position\n if (isRemovingCurrent) {\n // Removing list - adjust cursor back\n const prefixMatch = currentLine.match(style.orderedList ? orderedListRegex : unorderedListRegex)\n const prefixLength = prefixMatch ? prefixMatch[0].length : 0\n return {\n start: Math.max(selStart - prefixLength, lineStart),\n end: Math.max(selStart - prefixLength, lineStart)\n }\n } else if (hasOrderedList || hasUnorderedList) {\n // Converting from one list type to another\n const oldPrefixMatch = currentLine.match(hasOrderedList ? orderedListRegex : unorderedListRegex)\n const oldPrefixLength = oldPrefixMatch ? oldPrefixMatch[0].length : 0\n const newPrefixLength = style.unorderedList ? 2 : 3 // \"- \" or \"1. \"\n const adjustment = newPrefixLength - oldPrefixLength\n return {\n start: selStart + adjustment,\n end: selStart + adjustment\n }\n } else {\n // Adding new list\n const prefixLength = style.unorderedList ? 2 : 3 // \"- \" or \"1. \"\n return {\n start: selStart + prefixLength,\n end: selStart + prefixLength\n }\n }\n } else {\n // Has selection - preserve it\n if (isRemovingCurrent) {\n // Removing current list type\n const prefixMatch = currentLine.match(style.orderedList ? orderedListRegex : unorderedListRegex)\n const prefixLength = prefixMatch ? prefixMatch[0].length : 0\n return {\n start: Math.max(selStart - prefixLength, lineStart),\n end: Math.max(selEnd - prefixLength, lineStart)\n }\n } else if (hasOrderedList || hasUnorderedList) {\n // Converting from one list type to another\n const oldPrefixMatch = currentLine.match(hasOrderedList ? orderedListRegex : unorderedListRegex)\n const oldPrefixLength = oldPrefixMatch ? oldPrefixMatch[0].length : 0\n const newPrefixLength = style.unorderedList ? 2 : 3 // \"- \" or \"1. \"\n const adjustment = newPrefixLength - oldPrefixLength\n return {\n start: selStart + adjustment,\n end: selEnd + adjustment\n }\n } else {\n // Adding new list\n const prefixLength = style.unorderedList ? 2 : 3 // \"- \" or \"1. \"\n return {\n start: selStart + prefixLength,\n end: selEnd + prefixLength\n }\n }\n }\n }\n }\n )\n \n insertText(textarea, result)\n}", "/**\n * Format detection utilities\n */\n\nimport { FORMATS } from './formats.js'\n\n/**\n * Check if text has a specific format applied\n */\nfunction hasFormatApplied(text, format) {\n if (!format.prefix) return false\n \n if (format.suffix) {\n return text.startsWith(format.prefix) && text.endsWith(format.suffix)\n } else {\n return text.startsWith(format.prefix)\n }\n}\n\n/**\n * Get active formats at cursor position\n */\nexport function getActiveFormats(textarea) {\n if (!textarea) return []\n \n const formats = []\n const { selectionStart, selectionEnd, value } = textarea\n \n // Get current line for line-based formats\n const lines = value.split('\\n')\n let lineStart = 0\n let currentLine = ''\n \n for (const line of lines) {\n if (selectionStart >= lineStart && selectionStart <= lineStart + line.length) {\n currentLine = line\n break\n }\n lineStart += line.length + 1\n }\n \n // Check line-based formats\n if (currentLine.startsWith('- ')) {\n if (currentLine.startsWith('- [ ] ') || currentLine.startsWith('- [x] ')) {\n formats.push('task-list')\n } else {\n formats.push('bullet-list')\n }\n }\n \n if (/^\\d+\\.\\s/.test(currentLine)) {\n formats.push('numbered-list')\n }\n \n if (currentLine.startsWith('> ')) {\n formats.push('quote')\n }\n \n if (currentLine.startsWith('# ')) formats.push('header')\n if (currentLine.startsWith('## ')) formats.push('header-2')\n if (currentLine.startsWith('### ')) formats.push('header-3')\n \n // Check inline formats by looking around cursor\n const lookBehind = Math.max(0, selectionStart - 10)\n const lookAhead = Math.min(value.length, selectionEnd + 10)\n const surrounding = value.slice(lookBehind, lookAhead)\n \n // Check for bold\n if (surrounding.includes('**')) {\n const beforeCursor = value.slice(Math.max(0, selectionStart - 100), selectionStart)\n const afterCursor = value.slice(selectionEnd, Math.min(value.length, selectionEnd + 100))\n const lastOpenBold = beforeCursor.lastIndexOf('**')\n const nextCloseBold = afterCursor.indexOf('**')\n if (lastOpenBold !== -1 && nextCloseBold !== -1) {\n formats.push('bold')\n }\n }\n \n // Check for italic\n if (surrounding.includes('_')) {\n const beforeCursor = value.slice(Math.max(0, selectionStart - 100), selectionStart)\n const afterCursor = value.slice(selectionEnd, Math.min(value.length, selectionEnd + 100))\n const lastOpenItalic = beforeCursor.lastIndexOf('_')\n const nextCloseItalic = afterCursor.indexOf('_')\n if (lastOpenItalic !== -1 && nextCloseItalic !== -1) {\n formats.push('italic')\n }\n }\n \n // Check for code\n if (surrounding.includes('`')) {\n const beforeCursor = value.slice(Math.max(0, selectionStart - 100), selectionStart)\n const afterCursor = value.slice(selectionEnd, Math.min(value.length, selectionEnd + 100))\n if (beforeCursor.includes('`') && afterCursor.includes('`')) {\n formats.push('code')\n }\n }\n \n // Check for link\n if (surrounding.includes('[') && surrounding.includes(']')) {\n const beforeCursor = value.slice(Math.max(0, selectionStart - 100), selectionStart)\n const afterCursor = value.slice(selectionEnd, Math.min(value.length, selectionEnd + 100))\n const lastOpenBracket = beforeCursor.lastIndexOf('[')\n const nextCloseBracket = afterCursor.indexOf(']')\n if (lastOpenBracket !== -1 && nextCloseBracket !== -1) {\n const afterBracket = value.slice(selectionEnd + nextCloseBracket + 1, selectionEnd + nextCloseBracket + 10)\n if (afterBracket.startsWith('(')) {\n formats.push('link')\n }\n }\n }\n \n return formats\n}\n\n/**\n * Check if specific format is active at cursor\n */\nexport function hasFormat(textarea, format) {\n const activeFormats = getActiveFormats(textarea)\n return activeFormats.includes(format)\n}\n\n/**\n * Expand selection based on options\n */\nexport function expandSelection(textarea, options = {}) {\n if (!textarea) return\n \n const { toWord, toLine, toFormat } = options\n const { selectionStart, selectionEnd, value } = textarea\n \n if (toLine) {\n // Find line boundaries\n const lines = value.split('\\n')\n let lineStart = 0\n let lineEnd = 0\n let currentPos = 0\n \n for (const line of lines) {\n if (selectionStart >= currentPos && selectionStart <= currentPos + line.length) {\n lineStart = currentPos\n lineEnd = currentPos + line.length\n break\n }\n currentPos += line.length + 1\n }\n \n textarea.selectionStart = lineStart\n textarea.selectionEnd = lineEnd\n } else if (toWord && selectionStart === selectionEnd) {\n // Find word boundaries\n let start = selectionStart\n let end = selectionEnd\n \n // Move start back to word boundary\n while (start > 0 && !/\\s/.test(value[start - 1])) {\n start--\n }\n \n // Move end forward to word boundary\n while (end < value.length && !/\\s/.test(value[end])) {\n end++\n }\n \n textarea.selectionStart = start\n textarea.selectionEnd = end\n }\n}", "/**\n * markdown-actions - Lightweight markdown toolbar functionality\n * Based on GitHub's markdown-toolbar-element\n */\n\nimport { FORMATS, mergeWithDefaults } from './core/formats.js'\nimport { insertText, setUndoMethod } from './core/insertion.js'\nimport { preserveSelection, isMultipleLines, expandSelectionToLine, applyLineOperation } from './core/selection.js'\nimport { blockStyle, multilineStyle } from './operations/block.js'\nimport { applyListStyle } from './operations/list.js'\nimport { getActiveFormats as getActive, hasFormat as has, expandSelection as expand } from './core/detection.js'\nimport { debugLog, debugSelection, debugResult, setDebugMode, getDebugMode } from './debug.js'\n\n/**\n * Toggle bold formatting\n */\nexport function toggleBold(textarea) {\n if (!textarea || textarea.disabled || textarea.readOnly) return\n \n debugLog('toggleBold', 'Starting');\n debugSelection(textarea, 'Before');\n \n const style = mergeWithDefaults(FORMATS.bold)\n const result = blockStyle(textarea, style)\n \n debugResult(result);\n \n insertText(textarea, result)\n \n debugSelection(textarea, 'After');\n}\n\n/**\n * Toggle italic formatting\n */\nexport function toggleItalic(textarea) {\n if (!textarea || textarea.disabled || textarea.readOnly) return\n const style = mergeWithDefaults(FORMATS.italic)\n const result = blockStyle(textarea, style)\n insertText(textarea, result)\n}\n\n/**\n * Toggle code formatting\n */\nexport function toggleCode(textarea) {\n if (!textarea || textarea.disabled || textarea.readOnly) return\n \n // blockStyle already handles both inline and block code correctly\n const style = mergeWithDefaults(FORMATS.code)\n const result = blockStyle(textarea, style)\n insertText(textarea, result)\n}\n\n/**\n * Insert or toggle link formatting\n */\nexport function insertLink(textarea, options = {}) {\n if (!textarea || textarea.disabled || textarea.readOnly) return\n \n const selectedText = textarea.value.slice(textarea.selectionStart, textarea.selectionEnd)\n let style = mergeWithDefaults(FORMATS.link)\n \n // Check if selected text is a URL\n const isURL = selectedText && selectedText.match(/^https?:\\/\\//)\n \n if (isURL && !options.url) {\n // If selected text is a URL, use it as both link text and URL\n style.suffix = `](${selectedText})`\n style.replaceNext = ''\n // Don't change the selected text, it becomes the link text\n } else if (options.url) {\n // Override with custom URL if provided\n style.suffix = `](${options.url})`\n style.replaceNext = ''\n }\n \n // Override with custom text if provided\n if (options.text && !selectedText) {\n // Insert the text and select it\n const pos = textarea.selectionStart\n textarea.value = textarea.value.slice(0, pos) + options.text + textarea.value.slice(pos)\n textarea.selectionStart = pos\n textarea.selectionEnd = pos + options.text.length\n }\n \n const result = blockStyle(textarea, style)\n insertText(textarea, result)\n}\n\n/**\n * Toggle bullet list formatting\n */\nexport function toggleBulletList(textarea) {\n if (!textarea || textarea.disabled || textarea.readOnly) return\n const style = mergeWithDefaults(FORMATS.bulletList)\n applyListStyle(textarea, style)\n}\n\n/**\n * Toggle numbered list formatting\n */\nexport function toggleNumberedList(textarea) {\n if (!textarea || textarea.disabled || textarea.readOnly) return\n const style = mergeWithDefaults(FORMATS.numberedList)\n applyListStyle(textarea, style)\n}\n\n/**\n * Toggle quote formatting\n * Matches GitHub's implementation for quotes\n */\nexport function toggleQuote(textarea) {\n if (!textarea || textarea.disabled || textarea.readOnly) return\n \n debugLog('toggleQuote', 'Starting');\n debugSelection(textarea, 'Initial');\n \n const style = mergeWithDefaults(FORMATS.quote)\n \n // Use the reusable line operation helper\n const result = applyLineOperation(\n textarea,\n (ta) => multilineStyle(ta, style),\n { prefix: style.prefix }\n )\n \n debugResult(result);\n insertText(textarea, result)\n debugSelection(textarea, 'Final');\n}\n\n/**\n * Toggle task list formatting\n * Matches GitHub's implementation for task lists\n */\nexport function toggleTaskList(textarea) {\n if (!textarea || textarea.disabled || textarea.readOnly) return\n \n const style = mergeWithDefaults(FORMATS.taskList)\n \n // Use the reusable line operation helper\n const result = applyLineOperation(\n textarea,\n (ta) => multilineStyle(ta, style),\n { prefix: style.prefix }\n )\n \n insertText(textarea, result)\n}\n\n/**\n * Insert or toggle header with specific level\n */\nexport function insertHeader(textarea, level = 1, toggle = false) {\n if (!textarea || textarea.disabled || textarea.readOnly) return\n if (level < 1 || level > 6) level = 1\n \n debugLog('insertHeader', `============ START ============`);\n debugLog('insertHeader', `Level: ${level}, Toggle: ${toggle}`);\n debugLog('insertHeader', `Initial cursor: ${textarea.selectionStart}-${textarea.selectionEnd}`);\n \n const headerKey = `header${level === 1 ? '1' : level}`\n const style = mergeWithDefaults(FORMATS[headerKey] || FORMATS.header1)\n debugLog('insertHeader', `Style prefix: \"${style.prefix}\"`);\n \n // Save original positions and get line info BEFORE applyLineOperation\n const value = textarea.value\n const originalStart = textarea.selectionStart\n const originalEnd = textarea.selectionEnd\n \n // Find the current line boundaries to check existing header\n let lineStart = originalStart\n while (lineStart > 0 && value[lineStart - 1] !== '\\n') {\n lineStart--\n }\n let lineEnd = originalEnd\n while (lineEnd < value.length && value[lineEnd] !== '\\n') {\n lineEnd++\n }\n \n // Get current line and check for existing header\n const currentLineContent = value.slice(lineStart, lineEnd)\n debugLog('insertHeader', `Current line (before): \"${currentLineContent}\"`);\n \n const existingHeaderMatch = currentLineContent.match(/^(#{1,6})\\s*/)\n const existingLevel = existingHeaderMatch ? existingHeaderMatch[1].length : 0\n const existingPrefixLength = existingHeaderMatch ? existingHeaderMatch[0].length : 0\n \n debugLog('insertHeader', `Existing header check:`);\n debugLog('insertHeader', ` - Match: ${existingHeaderMatch ? `\"${existingHeaderMatch[0]}\"` : 'none'}`);\n debugLog('insertHeader', ` - Existing level: ${existingLevel}`);\n debugLog('insertHeader', ` - Existing prefix length: ${existingPrefixLength}`);\n debugLog('insertHeader', ` - Target level: ${level}`);\n \n // Determine if we're toggling off\n const shouldToggleOff = toggle && existingLevel === level\n debugLog('insertHeader', `Should toggle OFF: ${shouldToggleOff} (toggle=${toggle}, existingLevel=${existingLevel}, level=${level})`);\n \n // Use applyLineOperation for consistent behavior\n const result = applyLineOperation(\n textarea,\n (ta) => {\n const currentLine = ta.value.slice(ta.selectionStart, ta.selectionEnd)\n debugLog('insertHeader', `Line in operation: \"${currentLine}\"`);\n \n // Remove any existing header formatting\n const cleanedLine = currentLine.replace(/^#{1,6}\\s*/, '')\n debugLog('insertHeader', `Cleaned line: \"${cleanedLine}\"`);\n \n let newLine\n \n if (shouldToggleOff) {\n // Toggle off - just use the cleaned line\n debugLog('insertHeader', 'ACTION: Toggling OFF - removing header');\n newLine = cleanedLine\n } else if (existingLevel > 0) {\n // Replace existing header with new one\n debugLog('insertHeader', `ACTION: Replacing H${existingLevel} with H${level}`);\n newLine = style.prefix + cleanedLine\n } else {\n // Add new header\n debugLog('insertHeader', 'ACTION: Adding new header');\n newLine = style.prefix + cleanedLine\n }\n \n debugLog('insertHeader', `New line: \"${newLine}\"`);\n \n return {\n text: newLine,\n selectionStart: ta.selectionStart,\n selectionEnd: ta.selectionEnd\n }\n },\n {\n prefix: style.prefix,\n // Custom selection adjustment for headers\n adjustSelection: (isRemoving, selStart, selEnd, lineStartPos) => {\n debugLog('insertHeader', `Adjusting selection:`);\n debugLog('insertHeader', ` - isRemoving param: ${isRemoving}`);\n debugLog('insertHeader', ` - shouldToggleOff: ${shouldToggleOff}`);\n debugLog('insertHeader', ` - selStart: ${selStart}, selEnd: ${selEnd}`);\n debugLog('insertHeader', ` - lineStartPos: ${lineStartPos}`);\n \n if (shouldToggleOff) {\n // Removing the header entirely\n const adjustment = Math.max(selStart - existingPrefixLength, lineStartPos)\n debugLog('insertHeader', ` - Removing header, adjusting by -${existingPrefixLength}`);\n return {\n start: adjustment,\n end: selStart === selEnd ? adjustment : Math.max(selEnd - existingPrefixLength, lineStartPos)\n }\n } else if (existingPrefixLength > 0) {\n // Replacing existing header with new one\n const prefixDiff = style.prefix.length - existingPrefixLength\n debugLog('insertHeader', ` - Replacing header, adjusting by ${prefixDiff}`);\n return {\n start: selStart + prefixDiff,\n end: selEnd + prefixDiff\n }\n } else {\n // Adding new header\n debugLog('insertHeader', ` - Adding header, adjusting by +${style.prefix.length}`);\n return {\n start: selStart + style.prefix.length,\n end: selEnd + style.prefix.length\n }\n }\n }\n }\n )\n \n debugLog('insertHeader', `Final result: text=\"${result.text}\", cursor=${result.selectionStart}-${result.selectionEnd}`);\n debugLog('insertHeader', `============ END ============`);\n \n insertText(textarea, result)\n}\n\n/**\n * Toggle H1 header\n */\nexport function toggleH1(textarea) {\n insertHeader(textarea, 1, true)\n}\n\n/**\n * Toggle H2 header\n */\nexport function toggleH2(textarea) {\n insertHeader(textarea, 2, true)\n}\n\n/**\n * Toggle H3 header\n */\nexport function toggleH3(textarea) {\n insertHeader(textarea, 3, true)\n}\n\n/**\n * Get active formats at cursor position\n */\nexport function getActiveFormats(textarea) {\n return getActive(textarea)\n}\n\n/**\n * Check if format is active at cursor\n */\nexport function hasFormat(textarea, format) {\n return has(textarea, format)\n}\n\n/**\n * Expand selection based on options\n */\nexport function expandSelection(textarea, options = {}) {\n expand(textarea, options)\n}\n\n/**\n * Apply custom format\n */\nexport function applyCustomFormat(textarea, format) {\n if (!textarea || textarea.disabled || textarea.readOnly) return\n \n const style = mergeWithDefaults(format)\n let result\n \n if (style.multiline) {\n const selectedText = textarea.value.slice(textarea.selectionStart, textarea.selectionEnd)\n if (isMultipleLines(selectedText)) {\n result = multilineStyle(textarea, style)\n } else {\n result = blockStyle(textarea, style)\n }\n } else {\n result = blockStyle(textarea, style)\n }\n \n insertText(textarea, result)\n}\n\n/**\n * Preserve selection during callback\n */\nexport { preserveSelection }\n\n/**\n * Configure undo method\n */\nexport { setUndoMethod }\n\n/**\n * Debug mode control\n */\nexport { setDebugMode, getDebugMode }\n\n/**\n * Default export with all functions\n */\nexport default {\n toggleBold,\n toggleItalic,\n toggleCode,\n insertLink,\n toggleBulletList,\n toggleNumberedList,\n toggleQuote,\n toggleTaskList,\n insertHeader,\n toggleH1,\n toggleH2,\n toggleH3,\n getActiveFormats,\n hasFormat,\n expandSelection,\n applyCustomFormat,\n preserveSelection,\n setUndoMethod,\n setDebugMode,\n getDebugMode\n}", "/**\r\n * Toolbar component for OverType editor\r\n * Provides markdown formatting buttons with support for custom buttons\r\n */\r\n\r\nimport * as markdownActions from 'markdown-actions';\r\n\r\nexport class Toolbar {\r\n constructor(editor, options = {}) {\r\n this.editor = editor;\r\n this.container = null;\r\n this.buttons = {};\r\n\r\n // Get toolbar buttons array\r\n this.toolbarButtons = options.toolbarButtons || [];\r\n }\r\n\r\n /**\r\n * Create and render toolbar\r\n */\r\n create() {\r\n this.container = document.createElement('div');\r\n this.container.className = 'overtype-toolbar';\r\n this.container.setAttribute('role', 'toolbar');\r\n this.container.setAttribute('aria-label', 'Formatting toolbar');\r\n\r\n // Create buttons from toolbarButtons array\r\n this.toolbarButtons.forEach(buttonConfig => {\r\n if (buttonConfig.name === 'separator') {\r\n const separator = this.createSeparator();\r\n this.container.appendChild(separator);\r\n } else {\r\n const button = this.createButton(buttonConfig);\r\n this.buttons[buttonConfig.name] = button;\r\n this.container.appendChild(button);\r\n }\r\n });\r\n\r\n // Insert toolbar before the wrapper (as sibling, not child)\r\n this.editor.container.insertBefore(this.container, this.editor.wrapper);\r\n }\r\n\r\n /**\r\n * Create a toolbar separator\r\n */\r\n createSeparator() {\r\n const separator = document.createElement('div');\r\n separator.className = 'overtype-toolbar-separator';\r\n separator.setAttribute('role', 'separator');\r\n return separator;\r\n }\r\n\r\n /**\r\n * Create a toolbar button\r\n */\r\n createButton(buttonConfig) {\r\n const button = document.createElement('button');\r\n button.className = 'overtype-toolbar-button';\r\n button.type = 'button';\r\n button.setAttribute('data-button', buttonConfig.name);\r\n button.title = buttonConfig.title || '';\r\n button.setAttribute('aria-label', buttonConfig.title || buttonConfig.name);\r\n button.innerHTML = this.sanitizeSVG(buttonConfig.icon || '');\r\n\r\n // Special handling for viewMode dropdown\r\n if (buttonConfig.name === 'viewMode') {\r\n button.classList.add('has-dropdown');\r\n button.dataset.dropdown = 'true';\r\n button.addEventListener('click', (e) => {\r\n e.preventDefault();\r\n this.toggleViewModeDropdown(button);\r\n });\r\n return button;\r\n }\r\n\r\n // Standard button click handler - delegate to performAction\r\n button._clickHandler = (e) => {\r\n e.preventDefault();\r\n const actionId = buttonConfig.actionId || buttonConfig.name;\r\n this.editor.performAction(actionId, e);\r\n };\r\n\r\n button.addEventListener('click', button._clickHandler);\r\n return button;\r\n }\r\n\r\n /**\r\n * Handle button action programmatically\r\n * Accepts either an actionId string or a buttonConfig object (backwards compatible)\r\n * @param {string|Object} actionIdOrConfig - Action identifier string or button config object\r\n * @returns {Promise
} Whether the action was executed\r\n */\r\n async handleAction(actionIdOrConfig) {\r\n // Old style: buttonConfig object with .action function - execute directly\r\n if (actionIdOrConfig && typeof actionIdOrConfig === 'object' && typeof actionIdOrConfig.action === 'function') {\r\n this.editor.textarea.focus();\r\n try {\r\n await actionIdOrConfig.action({\r\n editor: this.editor,\r\n getValue: () => this.editor.getValue(),\r\n setValue: (value) => this.editor.setValue(value),\r\n event: null\r\n });\r\n return true;\r\n } catch (error) {\r\n console.error(`Action \"${actionIdOrConfig.name}\" error:`, error);\r\n this.editor.wrapper.dispatchEvent(new CustomEvent('button-error', {\r\n detail: { buttonName: actionIdOrConfig.name, error }\r\n }));\r\n return false;\r\n }\r\n }\r\n\r\n // New style: string actionId - delegate to performAction\r\n if (typeof actionIdOrConfig === 'string') {\r\n return this.editor.performAction(actionIdOrConfig, null);\r\n }\r\n\r\n return false;\r\n }\r\n\r\n /**\r\n * Sanitize SVG to prevent XSS\r\n */\r\n sanitizeSVG(svg) {\r\n if (typeof svg !== 'string') return '';\r\n\r\n // Remove script tags and on* event handlers\r\n const cleaned = svg\r\n .replace(/