@treeui/vue 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. package/dist/components/TCard.cjs.map +1 -1
  2. package/dist/components/TCard.js.map +1 -1
  3. package/dist/components/TCard.vue_vue_type_script_setup_true_lang.cjs +1 -1
  4. package/dist/components/TCard.vue_vue_type_script_setup_true_lang.cjs.map +1 -1
  5. package/dist/components/TCard.vue_vue_type_script_setup_true_lang.js +2 -2
  6. package/dist/components/TCard.vue_vue_type_script_setup_true_lang.js.map +1 -1
  7. package/dist/components/TDropdown.cjs.map +1 -1
  8. package/dist/components/TDropdown.js.map +1 -1
  9. package/dist/components/TDropdown.vue.d.ts +2 -2
  10. package/dist/components/TDropdown.vue.d.ts.map +1 -1
  11. package/dist/components/TDropdown.vue_vue_type_script_setup_true_lang.cjs +1 -1
  12. package/dist/components/TDropdown.vue_vue_type_script_setup_true_lang.cjs.map +1 -1
  13. package/dist/components/TDropdown.vue_vue_type_script_setup_true_lang.js +35 -32
  14. package/dist/components/TDropdown.vue_vue_type_script_setup_true_lang.js.map +1 -1
  15. package/dist/components/TMarkdownEditor.cjs.map +1 -1
  16. package/dist/components/TMarkdownEditor.js.map +1 -1
  17. package/dist/components/TMarkdownEditor.vue.d.ts +2 -0
  18. package/dist/components/TMarkdownEditor.vue.d.ts.map +1 -1
  19. package/dist/components/TMarkdownEditor.vue_vue_type_script_setup_true_lang.cjs +9 -9
  20. package/dist/components/TMarkdownEditor.vue_vue_type_script_setup_true_lang.cjs.map +1 -1
  21. package/dist/components/TMarkdownEditor.vue_vue_type_script_setup_true_lang.js +117 -96
  22. package/dist/components/TMarkdownEditor.vue_vue_type_script_setup_true_lang.js.map +1 -1
  23. package/dist/components/index.d.ts +9 -0
  24. package/dist/components/index.d.ts.map +1 -1
  25. package/dist/style.css +1 -1
  26. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"TMarkdownEditor.cjs","names":[],"sources":["../../src/components/TMarkdownEditor.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, ref, useAttrs, nextTick } from 'vue';\nimport type { TSize } from '../types/contracts';\n\ndefineOptions({\n inheritAttrs: false,\n});\n\nconst props = withDefaults(\n defineProps<{\n modelValue?: string;\n size?: TSize;\n disabled?: boolean;\n placeholder?: string;\n uploadImage?: (file: File) => Promise<string>;\n }>(),\n {\n modelValue: '',\n size: 'md',\n disabled: false,\n placeholder: 'Write your markdown here...',\n uploadImage: undefined,\n },\n);\n\nconst emit = defineEmits<{\n 'update:modelValue': [value: string];\n 'image-upload': [file: File, url: string];\n 'image-upload-error': [file: File, error: Error];\n}>();\n\ndefineSlots<{\n toolbar?: () => unknown;\n}>();\n\nconst attrs = useAttrs();\nconst textareaRef = ref<HTMLTextAreaElement | null>(null);\nconst isUploading = ref(false);\n\nconst rootClasses = computed(() => [\n 't-md-editor',\n `t-md-editor--${props.size}`,\n {\n 'is-disabled': props.disabled,\n 'is-uploading': isUploading.value,\n },\n attrs.class,\n]);\n\nconst rootStyle = computed(() => attrs.style);\n\nconst editorAttrs = computed(() => {\n const { class: _class, style: _style, ...rest } = attrs;\n return rest;\n});\n\nconst stringValue = computed(() => `${props.modelValue ?? ''}`);\n\nconst onInput = (event: Event) => {\n emit('update:modelValue', (event.target as HTMLTextAreaElement).value);\n};\n\n/* ──────── Toolbar Actions ──────── */\n\nconst insertAtCursor = (before: string, after = '') => {\n const el = textareaRef.value;\n if (!el) return;\n\n const start = el.selectionStart;\n const end = el.selectionEnd;\n const text = stringValue.value;\n const selected = text.slice(start, end);\n\n const newText = text.slice(0, start) + before + selected + after + text.slice(end);\n emit('update:modelValue', newText);\n\n nextTick(() => {\n el.focus();\n const cursorPos = selected\n ? start + before.length + selected.length + after.length\n : start + before.length;\n el.setSelectionRange(cursorPos, cursorPos);\n });\n};\n\nconst insertHeading = (level: number) => {\n const prefix = '#'.repeat(level) + ' ';\n const el = textareaRef.value;\n if (!el) return;\n\n const start = el.selectionStart;\n const text = stringValue.value;\n\n // Find start of current line\n const lineStart = text.lastIndexOf('\\n', start - 1) + 1;\n const lineEnd = text.indexOf('\\n', start);\n const actualEnd = lineEnd === -1 ? text.length : lineEnd;\n const line = text.slice(lineStart, actualEnd);\n\n // Strip existing heading markers\n const stripped = line.replace(/^#{1,6}\\s*/, '');\n const newLine = prefix + stripped;\n\n const newText = text.slice(0, lineStart) + newLine + text.slice(actualEnd);\n emit('update:modelValue', newText);\n\n nextTick(() => {\n el.focus();\n el.setSelectionRange(lineStart + newLine.length, lineStart + newLine.length);\n });\n};\n\nconst insertBold = () => insertAtCursor('**', '**');\nconst insertItalic = () => insertAtCursor('_', '_');\nconst insertCode = () => insertAtCursor('`', '`');\nconst insertCodeBlock = () => insertAtCursor('\\n```\\n', '\\n```\\n');\nconst insertLink = () => insertAtCursor('[', '](url)');\nconst insertList = () => insertAtCursor('\\n- ');\nconst insertOrderedList = () => insertAtCursor('\\n1. ');\nconst insertQuote = () => insertAtCursor('\\n> ');\nconst insertHr = () => insertAtCursor('\\n---\\n');\n\n/* ──────── Markdown → HTML rendering ──────── */\n\nconst renderedHtml = computed(() => {\n return markdownToHtml(stringValue.value);\n});\n\nfunction escapeHtml(text: string): string {\n return text\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#039;');\n}\n\nfunction markdownToHtml(md: string): string {\n if (!md) return '';\n\n const lines = md.split('\\n');\n const html: string[] = [];\n let inCodeBlock = false;\n let inList = false;\n let inOrderedList = false;\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i];\n\n // Code blocks\n if (line.trim().startsWith('```')) {\n if (inCodeBlock) {\n html.push('</code></pre>');\n inCodeBlock = false;\n } else {\n inCodeBlock = true;\n html.push('<pre class=\"t-md-editor__code-block\"><code>');\n }\n continue;\n }\n\n if (inCodeBlock) {\n html.push(escapeHtml(line));\n html.push('\\n');\n continue;\n }\n\n // Close open lists if needed\n const isUnorderedListItem = /^[-*+]\\s/.test(line.trim());\n const isOrderedListItem = /^\\d+\\.\\s/.test(line.trim());\n\n if (inList && !isUnorderedListItem) {\n html.push('</ul>');\n inList = false;\n }\n if (inOrderedList && !isOrderedListItem) {\n html.push('</ol>');\n inOrderedList = false;\n }\n\n // Horizontal rule\n if (/^(-{3,}|_{3,}|\\*{3,})$/.test(line.trim())) {\n html.push('<hr class=\"t-md-editor__hr\" />');\n continue;\n }\n\n // Headings\n const headingMatch = line.match(/^(#{1,6})\\s+(.+)$/);\n if (headingMatch) {\n const level = headingMatch[1].length;\n html.push(`<h${level} class=\"t-md-editor__heading t-md-editor__heading--${level}\">${inlineMarkdown(escapeHtml(headingMatch[2]))}</h${level}>`);\n continue;\n }\n\n // Blockquote\n if (line.trim().startsWith('> ')) {\n html.push(`<blockquote class=\"t-md-editor__blockquote\">${inlineMarkdown(escapeHtml(line.trim().slice(2)))}</blockquote>`);\n continue;\n }\n\n // Unordered list\n if (isUnorderedListItem) {\n if (!inList) {\n html.push('<ul class=\"t-md-editor__list\">');\n inList = true;\n }\n html.push(`<li>${inlineMarkdown(escapeHtml(line.trim().slice(2)))}</li>`);\n continue;\n }\n\n // Ordered list\n if (isOrderedListItem) {\n if (!inOrderedList) {\n html.push('<ol class=\"t-md-editor__list t-md-editor__list--ordered\">');\n inOrderedList = true;\n }\n const content = line.trim().replace(/^\\d+\\.\\s/, '');\n html.push(`<li>${inlineMarkdown(escapeHtml(content))}</li>`);\n continue;\n }\n\n // Empty line\n if (line.trim() === '') {\n html.push('<br />');\n continue;\n }\n\n // Paragraph\n html.push(`<p class=\"t-md-editor__paragraph\">${inlineMarkdown(escapeHtml(line))}</p>`);\n }\n\n // Close any open blocks\n if (inCodeBlock) html.push('</code></pre>');\n if (inList) html.push('</ul>');\n if (inOrderedList) html.push('</ol>');\n\n return html.join('\\n');\n}\n\nfunction inlineMarkdown(text: string): string {\n // Images: ![alt](url)\n text = text.replace(/!\\[([^\\]]*)\\]\\(([^)]+)\\)/g, '<img src=\"$2\" alt=\"$1\" class=\"t-md-editor__image\" loading=\"lazy\" />');\n // Links: [text](url)\n text = text.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, '<a href=\"$2\" target=\"_blank\" rel=\"noopener noreferrer\">$1</a>');\n // Bold: **text**\n text = text.replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>');\n // Italic: _text_ or *text*\n text = text.replace(/_(.+?)_/g, '<em>$1</em>');\n text = text.replace(/(?<!\\*)\\*(?!\\*)(.+?)(?<!\\*)\\*(?!\\*)/g, '<em>$1</em>');\n // Inline code: `text`\n text = text.replace(/`(.+?)`/g, '<code class=\"t-md-editor__inline-code\">$1</code>');\n // Strikethrough: ~~text~~\n text = text.replace(/~~(.+?)~~/g, '<del>$1</del>');\n return text;\n}\n\n/* ──────── Image paste / drop handling ──────── */\n\nconst handlePaste = async (event: ClipboardEvent) => {\n if (props.disabled) return;\n\n const items = event.clipboardData?.items;\n if (!items) return;\n\n for (const item of items) {\n if (item.type.startsWith('image/')) {\n event.preventDefault();\n const file = item.getAsFile();\n if (file) await processImageUpload(file);\n return;\n }\n }\n};\n\nconst handleDrop = async (event: DragEvent) => {\n if (props.disabled) return;\n\n const files = event.dataTransfer?.files;\n if (!files) return;\n\n for (const file of files) {\n if (file.type.startsWith('image/')) {\n event.preventDefault();\n await processImageUpload(file);\n }\n }\n};\n\nconst handleDragOver = (event: DragEvent) => {\n if (props.disabled) return;\n event.preventDefault();\n};\n\nconst processImageUpload = async (file: File) => {\n if (!props.uploadImage) {\n // Fallback: insert a placeholder with the file name\n insertAtCursor(`![${file.name}]()`);\n return;\n }\n\n // Insert uploading placeholder\n const placeholder = `![Uploading ${file.name}...]()`;\n insertAtCursor(placeholder);\n isUploading.value = true;\n\n try {\n const url = props.uploadImage(file);\n const resolvedUrl = url instanceof Promise ? await url : url;\n\n // Replace placeholder with actual URL\n const currentValue = stringValue.value;\n const newValue = currentValue.replace(placeholder, `![${file.name}](${resolvedUrl})`);\n emit('update:modelValue', newValue);\n emit('image-upload', file, resolvedUrl);\n } catch (err) {\n // Remove placeholder on error\n const currentValue = stringValue.value;\n const newValue = currentValue.replace(placeholder, '');\n emit('update:modelValue', newValue);\n emit('image-upload-error', file, err instanceof Error ? err : new Error(String(err)));\n } finally {\n isUploading.value = false;\n }\n};\n\n/* ──────── Keyboard shortcuts ──────── */\n\nconst handleKeydown = (event: KeyboardEvent) => {\n if (props.disabled) return;\n\n const mod = event.metaKey || event.ctrlKey;\n if (!mod) return;\n\n switch (event.key) {\n case 'b':\n event.preventDefault();\n insertBold();\n break;\n case 'i':\n event.preventDefault();\n insertItalic();\n break;\n case 'k':\n event.preventDefault();\n insertLink();\n break;\n case 'e':\n event.preventDefault();\n insertCode();\n break;\n }\n};\n\n</script>\n\n<template>\n <div\n v-bind=\"editorAttrs\"\n :class=\"rootClasses\"\n :style=\"rootStyle\"\n >\n <!-- Toolbar -->\n <div\n class=\"t-md-editor__toolbar\"\n role=\"toolbar\"\n aria-label=\"Markdown formatting\"\n >\n <div class=\"t-md-editor__toolbar-group\">\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Heading 1 (H1)\"\n :disabled=\"disabled\"\n @click=\"insertHeading(1)\"\n >\n H1\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Heading 2 (H2)\"\n :disabled=\"disabled\"\n @click=\"insertHeading(2)\"\n >\n H2\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Heading 3 (H3)\"\n :disabled=\"disabled\"\n @click=\"insertHeading(3)\"\n >\n H3\n </button>\n </div>\n\n <div\n class=\"t-md-editor__toolbar-divider\"\n role=\"separator\"\n />\n\n <div class=\"t-md-editor__toolbar-group\">\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Bold (Ctrl+B)\"\n :disabled=\"disabled\"\n @click=\"insertBold\"\n >\n <strong>B</strong>\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Italic (Ctrl+I)\"\n :disabled=\"disabled\"\n @click=\"insertItalic\"\n >\n <em>I</em>\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Strikethrough\"\n :disabled=\"disabled\"\n @click=\"insertAtCursor('~~', '~~')\"\n >\n <del>S</del>\n </button>\n </div>\n\n <div\n class=\"t-md-editor__toolbar-divider\"\n role=\"separator\"\n />\n\n <div class=\"t-md-editor__toolbar-group\">\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Inline code (Ctrl+E)\"\n :disabled=\"disabled\"\n @click=\"insertCode\"\n >\n &lt;/&gt;\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Code block\"\n :disabled=\"disabled\"\n @click=\"insertCodeBlock\"\n >\n { }\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Link (Ctrl+K)\"\n :disabled=\"disabled\"\n @click=\"insertLink\"\n >\n 🔗\n </button>\n </div>\n\n <div\n class=\"t-md-editor__toolbar-divider\"\n role=\"separator\"\n />\n\n <div class=\"t-md-editor__toolbar-group\">\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Unordered list\"\n :disabled=\"disabled\"\n @click=\"insertList\"\n >\n •&nbsp;List\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Ordered list\"\n :disabled=\"disabled\"\n @click=\"insertOrderedList\"\n >\n 1.&nbsp;List\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Blockquote\"\n :disabled=\"disabled\"\n @click=\"insertQuote\"\n >\n &gt;\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Horizontal rule\"\n :disabled=\"disabled\"\n @click=\"insertHr\"\n >\n ―\n </button>\n </div>\n\n <slot name=\"toolbar\" />\n </div>\n\n <!-- Editor body -->\n <div class=\"t-md-editor__body\">\n <!-- Write pane -->\n <div\n class=\"t-md-editor__pane t-md-editor__pane--write\"\n >\n <textarea\n ref=\"textareaRef\"\n class=\"t-md-editor__textarea\"\n :value=\"stringValue\"\n :placeholder=\"placeholder\"\n :disabled=\"disabled\"\n :aria-label=\"placeholder\"\n @input=\"onInput\"\n @paste=\"handlePaste\"\n @drop=\"handleDrop\"\n @dragover=\"handleDragOver\"\n @keydown=\"handleKeydown\"\n />\n </div>\n\n <!-- Preview pane -->\n <div\n class=\"t-md-editor__pane t-md-editor__pane--preview\"\n aria-label=\"Markdown preview\"\n >\n <!-- eslint-disable vue/no-v-html -->\n <div\n class=\"t-md-editor__preview\"\n v-html=\"renderedHtml\"\n />\n <!-- eslint-enable vue/no-v-html -->\n </div>\n </div>\n\n <!-- Upload indicator -->\n <div\n v-if=\"isUploading\"\n class=\"t-md-editor__upload-bar\"\n role=\"status\"\n aria-live=\"polite\"\n >\n Uploading image…\n </div>\n </div>\n</template>\n"],"mappings":""}
1
+ {"version":3,"file":"TMarkdownEditor.cjs","names":[],"sources":["../../src/components/TMarkdownEditor.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, ref, useAttrs, nextTick } from 'vue';\nimport type { TSize } from '../types/contracts';\n\ndefineOptions({\n inheritAttrs: false,\n});\n\nconst props = withDefaults(\n defineProps<{\n modelValue?: string;\n size?: TSize;\n disabled?: boolean;\n placeholder?: string;\n uploadImage?: (file: File) => Promise<string>;\n sanitize?: (html: string) => string;\n }>(),\n {\n modelValue: '',\n size: 'md',\n disabled: false,\n placeholder: 'Write your markdown here...',\n uploadImage: undefined,\n sanitize: undefined,\n },\n);\n\nconst emit = defineEmits<{\n 'update:modelValue': [value: string];\n 'image-upload': [file: File, url: string];\n 'image-upload-error': [file: File, error: Error];\n}>();\n\ndefineSlots<{\n toolbar?: () => unknown;\n}>();\n\nconst attrs = useAttrs();\nconst textareaRef = ref<HTMLTextAreaElement | null>(null);\nconst isUploading = ref(false);\n\nconst rootClasses = computed(() => [\n 't-md-editor',\n `t-md-editor--${props.size}`,\n {\n 'is-disabled': props.disabled,\n 'is-uploading': isUploading.value,\n },\n attrs.class,\n]);\n\nconst rootStyle = computed(() => attrs.style);\n\nconst editorAttrs = computed(() => {\n const { class: _class, style: _style, ...rest } = attrs;\n return rest;\n});\n\nconst stringValue = computed(() => `${props.modelValue ?? ''}`);\n\nconst onInput = (event: Event) => {\n emit('update:modelValue', (event.target as HTMLTextAreaElement).value);\n};\n\n/* ──────── Toolbar Actions ──────── */\n\nconst insertAtCursor = (before: string, after = '') => {\n const el = textareaRef.value;\n if (!el) return;\n\n const start = el.selectionStart;\n const end = el.selectionEnd;\n const text = stringValue.value;\n const selected = text.slice(start, end);\n\n const newText = text.slice(0, start) + before + selected + after + text.slice(end);\n emit('update:modelValue', newText);\n\n nextTick(() => {\n el.focus();\n const cursorPos = selected\n ? start + before.length + selected.length + after.length\n : start + before.length;\n el.setSelectionRange(cursorPos, cursorPos);\n });\n};\n\nconst insertHeading = (level: number) => {\n const prefix = '#'.repeat(level) + ' ';\n const el = textareaRef.value;\n if (!el) return;\n\n const start = el.selectionStart;\n const text = stringValue.value;\n\n // Find start of current line\n const lineStart = text.lastIndexOf('\\n', start - 1) + 1;\n const lineEnd = text.indexOf('\\n', start);\n const actualEnd = lineEnd === -1 ? text.length : lineEnd;\n const line = text.slice(lineStart, actualEnd);\n\n // Strip existing heading markers\n const stripped = line.replace(/^#{1,6}\\s*/, '');\n const newLine = prefix + stripped;\n\n const newText = text.slice(0, lineStart) + newLine + text.slice(actualEnd);\n emit('update:modelValue', newText);\n\n nextTick(() => {\n el.focus();\n el.setSelectionRange(lineStart + newLine.length, lineStart + newLine.length);\n });\n};\n\nconst insertBold = () => insertAtCursor('**', '**');\nconst insertItalic = () => insertAtCursor('_', '_');\nconst insertCode = () => insertAtCursor('`', '`');\nconst insertCodeBlock = () => insertAtCursor('\\n```\\n', '\\n```\\n');\nconst insertLink = () => insertAtCursor('[', '](url)');\nconst insertList = () => insertAtCursor('\\n- ');\nconst insertOrderedList = () => insertAtCursor('\\n1. ');\nconst insertQuote = () => insertAtCursor('\\n> ');\nconst insertHr = () => insertAtCursor('\\n---\\n');\n\n/* ──────── Markdown → HTML rendering ──────── */\n\nconst renderedHtml = computed(() => {\n const html = markdownToHtml(stringValue.value);\n // Escape hatch: the built-in rendering escapes HTML and allowlists URL\n // schemes, but consumers with a stricter policy can pass their own\n // sanitizer (DOMPurify or similar) instead of forking the component.\n return props.sanitize ? props.sanitize(html) : html;\n});\n\nfunction escapeHtml(text: string): string {\n return text\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#039;');\n}\n\nfunction markdownToHtml(md: string): string {\n if (!md) return '';\n\n const lines = md.split('\\n');\n const html: string[] = [];\n let inCodeBlock = false;\n let inList = false;\n let inOrderedList = false;\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i];\n\n // Code blocks\n if (line.trim().startsWith('```')) {\n if (inCodeBlock) {\n html.push('</code></pre>');\n inCodeBlock = false;\n } else {\n inCodeBlock = true;\n html.push('<pre class=\"t-md-editor__code-block\"><code>');\n }\n continue;\n }\n\n if (inCodeBlock) {\n html.push(escapeHtml(line));\n html.push('\\n');\n continue;\n }\n\n // Close open lists if needed\n const isUnorderedListItem = /^[-*+]\\s/.test(line.trim());\n const isOrderedListItem = /^\\d+\\.\\s/.test(line.trim());\n\n if (inList && !isUnorderedListItem) {\n html.push('</ul>');\n inList = false;\n }\n if (inOrderedList && !isOrderedListItem) {\n html.push('</ol>');\n inOrderedList = false;\n }\n\n // Horizontal rule\n if (/^(-{3,}|_{3,}|\\*{3,})$/.test(line.trim())) {\n html.push('<hr class=\"t-md-editor__hr\" />');\n continue;\n }\n\n // Headings\n const headingMatch = line.match(/^(#{1,6})\\s+(.+)$/);\n if (headingMatch) {\n const level = headingMatch[1].length;\n html.push(`<h${level} class=\"t-md-editor__heading t-md-editor__heading--${level}\">${inlineMarkdown(escapeHtml(headingMatch[2]))}</h${level}>`);\n continue;\n }\n\n // Blockquote\n if (line.trim().startsWith('> ')) {\n html.push(`<blockquote class=\"t-md-editor__blockquote\">${inlineMarkdown(escapeHtml(line.trim().slice(2)))}</blockquote>`);\n continue;\n }\n\n // Unordered list\n if (isUnorderedListItem) {\n if (!inList) {\n html.push('<ul class=\"t-md-editor__list\">');\n inList = true;\n }\n html.push(`<li>${inlineMarkdown(escapeHtml(line.trim().slice(2)))}</li>`);\n continue;\n }\n\n // Ordered list\n if (isOrderedListItem) {\n if (!inOrderedList) {\n html.push('<ol class=\"t-md-editor__list t-md-editor__list--ordered\">');\n inOrderedList = true;\n }\n const content = line.trim().replace(/^\\d+\\.\\s/, '');\n html.push(`<li>${inlineMarkdown(escapeHtml(content))}</li>`);\n continue;\n }\n\n // Empty line\n if (line.trim() === '') {\n html.push('<br />');\n continue;\n }\n\n // Paragraph\n html.push(`<p class=\"t-md-editor__paragraph\">${inlineMarkdown(escapeHtml(line))}</p>`);\n }\n\n // Close any open blocks\n if (inCodeBlock) html.push('</code></pre>');\n if (inList) html.push('</ul>');\n if (inOrderedList) html.push('</ol>');\n\n return html.join('\\n');\n}\n\n/* ──────── URL safety ──────── */\n\n// A URL carries a scheme only when a colon follows a valid scheme name.\n// Anything else (`/path`, `./path`, `#anchor`, `?q=1`) is relative and cannot\n// introduce a new scheme, so it is safe. `//host/path` is protocol-relative:\n// it keeps the page's protocol but points at another host, so it can navigate\n// off-origin. That is allowed — it is a link, not script — and `rel=\"noopener\n// noreferrer\"` on the anchor covers the tab-nabbing side of it.\nconst HAS_SCHEME = /^[a-z][a-z0-9+.-]*:/;\n// Navigable schemes. `javascript:`, `vbscript:`, `data:` and friends are\n// rejected — everything reaching inlineMarkdown is already HTML-escaped, so\n// the URL scheme is the only remaining way to get script into the preview.\nconst SAFE_LINK_SCHEME = /^(https?|mailto|tel):/;\nconst SAFE_IMAGE_SCHEME = /^https?:/;\n// Inline raster images are useful for pasted screenshots. `image/svg+xml` is\n// deliberately excluded: SVG is a document format that can carry script and\n// external references, not just pixels.\nconst SAFE_IMAGE_DATA_URL = /^data:image\\/(png|jpe?g|gif|webp|avif|bmp|x-icon|vnd\\.microsoft\\.icon)[;,]/;\n\nconst fromCodePoint = (code: number) =>\n Number.isFinite(code) && code >= 0 && code <= 0x10ffff ? String.fromCodePoint(code) : '';\n\n// Reduce a URL to the form a browser would act on before testing its scheme:\n// decode character references, drop control characters and whitespace, and\n// lowercase. This defeats `JaVaScRiPt:`, ` javascript:`, `java\\tscript:`,\n// `java\\x00script:` and `&#106;avascript:` style evasion.\nfunction normalizeUrl(url: string): string {\n return url\n .replace(/&quot;/g, '\"')\n .replace(/&#039;/g, \"'\")\n .replace(/&lt;/g, '<')\n .replace(/&gt;/g, '>')\n .replace(/&amp;/g, '&')\n .replace(/&#x([0-9a-f]+);?/gi, (_match, hex: string) => fromCodePoint(parseInt(hex, 16)))\n .replace(/&#(\\d+);?/g, (_match, dec: string) => fromCodePoint(parseInt(dec, 10)))\n // eslint-disable-next-line no-control-regex -- matching control characters is the point\n .replace(/[\\x00-\\x20\\x7f]/g, '')\n .toLowerCase();\n}\n\nfunction isSafeLinkUrl(url: string): boolean {\n const normalized = normalizeUrl(url);\n return !HAS_SCHEME.test(normalized) || SAFE_LINK_SCHEME.test(normalized);\n}\n\nfunction isSafeImageUrl(url: string): boolean {\n const normalized = normalizeUrl(url);\n if (!HAS_SCHEME.test(normalized)) return true;\n return SAFE_IMAGE_SCHEME.test(normalized) || SAFE_IMAGE_DATA_URL.test(normalized);\n}\n\n// Placeholder delimiter for generated markup. NUL is stripped from the input\n// before any token is created, so a placeholder can never collide with user\n// text, and it survives the emphasis passes because none of them treat it as\n// a delimiter.\nconst TOKEN = '\\x00';\n// eslint-disable-next-line no-control-regex -- matching control characters is the point\nconst TOKEN_PATTERN = /\\x00(\\d+)\\x00/g;\n\nfunction inlineMarkdown(text: string): string {\n const tokens: string[] = [];\n // Park generated markup outside the emphasis passes below. Without this,\n // `_(.+?)_` eats the underscore in the generated target=\"_blank\" and any\n // underscore inside a URL.\n const token = (html: string) => {\n tokens.push(html);\n return `${TOKEN}${tokens.length - 1}${TOKEN}`;\n };\n const inert = (source: string) => token(`<span class=\"t-md-editor__blocked-link\">${source}</span>`);\n\n // eslint-disable-next-line no-control-regex -- matching control characters is the point\n text = text.replace(/\\x00/g, '');\n\n // Images: ![alt](url) — alt is a plain-text attribute, so the whole tag is parked.\n text = text.replace(/!\\[([^\\]]*)\\]\\(([^)]+)\\)/g, (match, alt: string, url: string) =>\n isSafeImageUrl(url)\n ? token(`<img src=\"${url}\" alt=\"${alt}\" class=\"t-md-editor__image\" loading=\"lazy\" />`)\n : inert(match),\n );\n // Links: [text](url) — only the tags are parked so the label still formats.\n text = text.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, (match, label: string, url: string) =>\n isSafeLinkUrl(url)\n ? `${token(`<a href=\"${url}\" target=\"_blank\" rel=\"noopener noreferrer\">`)}${label}${token('</a>')}`\n : inert(match),\n );\n // Bold: **text**\n text = text.replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>');\n // Italic: _text_ or *text*\n text = text.replace(/_(.+?)_/g, '<em>$1</em>');\n text = text.replace(/(?<!\\*)\\*(?!\\*)(.+?)(?<!\\*)\\*(?!\\*)/g, '<em>$1</em>');\n // Inline code: `text`\n text = text.replace(/`(.+?)`/g, '<code class=\"t-md-editor__inline-code\">$1</code>');\n // Strikethrough: ~~text~~\n text = text.replace(/~~(.+?)~~/g, '<del>$1</del>');\n\n return text.replace(TOKEN_PATTERN, (_match, index: string) => tokens[Number(index)]);\n}\n\n/* ──────── Image paste / drop handling ──────── */\n\nconst handlePaste = async (event: ClipboardEvent) => {\n if (props.disabled) return;\n\n const items = event.clipboardData?.items;\n if (!items) return;\n\n for (const item of items) {\n if (item.type.startsWith('image/')) {\n event.preventDefault();\n const file = item.getAsFile();\n if (file) await processImageUpload(file);\n return;\n }\n }\n};\n\nconst handleDrop = async (event: DragEvent) => {\n if (props.disabled) return;\n\n const files = event.dataTransfer?.files;\n if (!files) return;\n\n for (const file of files) {\n if (file.type.startsWith('image/')) {\n event.preventDefault();\n await processImageUpload(file);\n }\n }\n};\n\nconst handleDragOver = (event: DragEvent) => {\n if (props.disabled) return;\n event.preventDefault();\n};\n\nconst processImageUpload = async (file: File) => {\n if (!props.uploadImage) {\n // Fallback: insert a placeholder with the file name\n insertAtCursor(`![${file.name}]()`);\n return;\n }\n\n // Insert uploading placeholder\n const placeholder = `![Uploading ${file.name}...]()`;\n insertAtCursor(placeholder);\n isUploading.value = true;\n\n try {\n const url = props.uploadImage(file);\n const resolvedUrl = url instanceof Promise ? await url : url;\n\n // Replace placeholder with actual URL\n const currentValue = stringValue.value;\n const newValue = currentValue.replace(placeholder, `![${file.name}](${resolvedUrl})`);\n emit('update:modelValue', newValue);\n emit('image-upload', file, resolvedUrl);\n } catch (err) {\n // Remove placeholder on error\n const currentValue = stringValue.value;\n const newValue = currentValue.replace(placeholder, '');\n emit('update:modelValue', newValue);\n emit('image-upload-error', file, err instanceof Error ? err : new Error(String(err)));\n } finally {\n isUploading.value = false;\n }\n};\n\n/* ──────── Keyboard shortcuts ──────── */\n\nconst handleKeydown = (event: KeyboardEvent) => {\n if (props.disabled) return;\n\n const mod = event.metaKey || event.ctrlKey;\n if (!mod) return;\n\n switch (event.key) {\n case 'b':\n event.preventDefault();\n insertBold();\n break;\n case 'i':\n event.preventDefault();\n insertItalic();\n break;\n case 'k':\n event.preventDefault();\n insertLink();\n break;\n case 'e':\n event.preventDefault();\n insertCode();\n break;\n }\n};\n\n</script>\n\n<template>\n <div\n v-bind=\"editorAttrs\"\n :class=\"rootClasses\"\n :style=\"rootStyle\"\n >\n <!-- Toolbar -->\n <div\n class=\"t-md-editor__toolbar\"\n role=\"toolbar\"\n aria-label=\"Markdown formatting\"\n >\n <div class=\"t-md-editor__toolbar-group\">\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Heading 1 (H1)\"\n :disabled=\"disabled\"\n @click=\"insertHeading(1)\"\n >\n H1\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Heading 2 (H2)\"\n :disabled=\"disabled\"\n @click=\"insertHeading(2)\"\n >\n H2\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Heading 3 (H3)\"\n :disabled=\"disabled\"\n @click=\"insertHeading(3)\"\n >\n H3\n </button>\n </div>\n\n <div\n class=\"t-md-editor__toolbar-divider\"\n role=\"separator\"\n />\n\n <div class=\"t-md-editor__toolbar-group\">\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Bold (Ctrl+B)\"\n :disabled=\"disabled\"\n @click=\"insertBold\"\n >\n <strong>B</strong>\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Italic (Ctrl+I)\"\n :disabled=\"disabled\"\n @click=\"insertItalic\"\n >\n <em>I</em>\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Strikethrough\"\n :disabled=\"disabled\"\n @click=\"insertAtCursor('~~', '~~')\"\n >\n <del>S</del>\n </button>\n </div>\n\n <div\n class=\"t-md-editor__toolbar-divider\"\n role=\"separator\"\n />\n\n <div class=\"t-md-editor__toolbar-group\">\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Inline code (Ctrl+E)\"\n :disabled=\"disabled\"\n @click=\"insertCode\"\n >\n &lt;/&gt;\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Code block\"\n :disabled=\"disabled\"\n @click=\"insertCodeBlock\"\n >\n { }\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Link (Ctrl+K)\"\n :disabled=\"disabled\"\n @click=\"insertLink\"\n >\n 🔗\n </button>\n </div>\n\n <div\n class=\"t-md-editor__toolbar-divider\"\n role=\"separator\"\n />\n\n <div class=\"t-md-editor__toolbar-group\">\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Unordered list\"\n :disabled=\"disabled\"\n @click=\"insertList\"\n >\n •&nbsp;List\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Ordered list\"\n :disabled=\"disabled\"\n @click=\"insertOrderedList\"\n >\n 1.&nbsp;List\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Blockquote\"\n :disabled=\"disabled\"\n @click=\"insertQuote\"\n >\n &gt;\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Horizontal rule\"\n :disabled=\"disabled\"\n @click=\"insertHr\"\n >\n ―\n </button>\n </div>\n\n <slot name=\"toolbar\" />\n </div>\n\n <!-- Editor body -->\n <div class=\"t-md-editor__body\">\n <!-- Write pane -->\n <div\n class=\"t-md-editor__pane t-md-editor__pane--write\"\n >\n <textarea\n ref=\"textareaRef\"\n class=\"t-md-editor__textarea\"\n :value=\"stringValue\"\n :placeholder=\"placeholder\"\n :disabled=\"disabled\"\n :aria-label=\"placeholder\"\n @input=\"onInput\"\n @paste=\"handlePaste\"\n @drop=\"handleDrop\"\n @dragover=\"handleDragOver\"\n @keydown=\"handleKeydown\"\n />\n </div>\n\n <!-- Preview pane -->\n <div\n class=\"t-md-editor__pane t-md-editor__pane--preview\"\n aria-label=\"Markdown preview\"\n >\n <!-- eslint-disable vue/no-v-html -->\n <div\n class=\"t-md-editor__preview\"\n v-html=\"renderedHtml\"\n />\n <!-- eslint-enable vue/no-v-html -->\n </div>\n </div>\n\n <!-- Upload indicator -->\n <div\n v-if=\"isUploading\"\n class=\"t-md-editor__upload-bar\"\n role=\"status\"\n aria-live=\"polite\"\n >\n Uploading image…\n </div>\n </div>\n</template>\n"],"mappings":""}
@@ -1 +1 @@
1
- {"version":3,"file":"TMarkdownEditor.js","names":[],"sources":["../../src/components/TMarkdownEditor.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, ref, useAttrs, nextTick } from 'vue';\nimport type { TSize } from '../types/contracts';\n\ndefineOptions({\n inheritAttrs: false,\n});\n\nconst props = withDefaults(\n defineProps<{\n modelValue?: string;\n size?: TSize;\n disabled?: boolean;\n placeholder?: string;\n uploadImage?: (file: File) => Promise<string>;\n }>(),\n {\n modelValue: '',\n size: 'md',\n disabled: false,\n placeholder: 'Write your markdown here...',\n uploadImage: undefined,\n },\n);\n\nconst emit = defineEmits<{\n 'update:modelValue': [value: string];\n 'image-upload': [file: File, url: string];\n 'image-upload-error': [file: File, error: Error];\n}>();\n\ndefineSlots<{\n toolbar?: () => unknown;\n}>();\n\nconst attrs = useAttrs();\nconst textareaRef = ref<HTMLTextAreaElement | null>(null);\nconst isUploading = ref(false);\n\nconst rootClasses = computed(() => [\n 't-md-editor',\n `t-md-editor--${props.size}`,\n {\n 'is-disabled': props.disabled,\n 'is-uploading': isUploading.value,\n },\n attrs.class,\n]);\n\nconst rootStyle = computed(() => attrs.style);\n\nconst editorAttrs = computed(() => {\n const { class: _class, style: _style, ...rest } = attrs;\n return rest;\n});\n\nconst stringValue = computed(() => `${props.modelValue ?? ''}`);\n\nconst onInput = (event: Event) => {\n emit('update:modelValue', (event.target as HTMLTextAreaElement).value);\n};\n\n/* ──────── Toolbar Actions ──────── */\n\nconst insertAtCursor = (before: string, after = '') => {\n const el = textareaRef.value;\n if (!el) return;\n\n const start = el.selectionStart;\n const end = el.selectionEnd;\n const text = stringValue.value;\n const selected = text.slice(start, end);\n\n const newText = text.slice(0, start) + before + selected + after + text.slice(end);\n emit('update:modelValue', newText);\n\n nextTick(() => {\n el.focus();\n const cursorPos = selected\n ? start + before.length + selected.length + after.length\n : start + before.length;\n el.setSelectionRange(cursorPos, cursorPos);\n });\n};\n\nconst insertHeading = (level: number) => {\n const prefix = '#'.repeat(level) + ' ';\n const el = textareaRef.value;\n if (!el) return;\n\n const start = el.selectionStart;\n const text = stringValue.value;\n\n // Find start of current line\n const lineStart = text.lastIndexOf('\\n', start - 1) + 1;\n const lineEnd = text.indexOf('\\n', start);\n const actualEnd = lineEnd === -1 ? text.length : lineEnd;\n const line = text.slice(lineStart, actualEnd);\n\n // Strip existing heading markers\n const stripped = line.replace(/^#{1,6}\\s*/, '');\n const newLine = prefix + stripped;\n\n const newText = text.slice(0, lineStart) + newLine + text.slice(actualEnd);\n emit('update:modelValue', newText);\n\n nextTick(() => {\n el.focus();\n el.setSelectionRange(lineStart + newLine.length, lineStart + newLine.length);\n });\n};\n\nconst insertBold = () => insertAtCursor('**', '**');\nconst insertItalic = () => insertAtCursor('_', '_');\nconst insertCode = () => insertAtCursor('`', '`');\nconst insertCodeBlock = () => insertAtCursor('\\n```\\n', '\\n```\\n');\nconst insertLink = () => insertAtCursor('[', '](url)');\nconst insertList = () => insertAtCursor('\\n- ');\nconst insertOrderedList = () => insertAtCursor('\\n1. ');\nconst insertQuote = () => insertAtCursor('\\n> ');\nconst insertHr = () => insertAtCursor('\\n---\\n');\n\n/* ──────── Markdown → HTML rendering ──────── */\n\nconst renderedHtml = computed(() => {\n return markdownToHtml(stringValue.value);\n});\n\nfunction escapeHtml(text: string): string {\n return text\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#039;');\n}\n\nfunction markdownToHtml(md: string): string {\n if (!md) return '';\n\n const lines = md.split('\\n');\n const html: string[] = [];\n let inCodeBlock = false;\n let inList = false;\n let inOrderedList = false;\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i];\n\n // Code blocks\n if (line.trim().startsWith('```')) {\n if (inCodeBlock) {\n html.push('</code></pre>');\n inCodeBlock = false;\n } else {\n inCodeBlock = true;\n html.push('<pre class=\"t-md-editor__code-block\"><code>');\n }\n continue;\n }\n\n if (inCodeBlock) {\n html.push(escapeHtml(line));\n html.push('\\n');\n continue;\n }\n\n // Close open lists if needed\n const isUnorderedListItem = /^[-*+]\\s/.test(line.trim());\n const isOrderedListItem = /^\\d+\\.\\s/.test(line.trim());\n\n if (inList && !isUnorderedListItem) {\n html.push('</ul>');\n inList = false;\n }\n if (inOrderedList && !isOrderedListItem) {\n html.push('</ol>');\n inOrderedList = false;\n }\n\n // Horizontal rule\n if (/^(-{3,}|_{3,}|\\*{3,})$/.test(line.trim())) {\n html.push('<hr class=\"t-md-editor__hr\" />');\n continue;\n }\n\n // Headings\n const headingMatch = line.match(/^(#{1,6})\\s+(.+)$/);\n if (headingMatch) {\n const level = headingMatch[1].length;\n html.push(`<h${level} class=\"t-md-editor__heading t-md-editor__heading--${level}\">${inlineMarkdown(escapeHtml(headingMatch[2]))}</h${level}>`);\n continue;\n }\n\n // Blockquote\n if (line.trim().startsWith('> ')) {\n html.push(`<blockquote class=\"t-md-editor__blockquote\">${inlineMarkdown(escapeHtml(line.trim().slice(2)))}</blockquote>`);\n continue;\n }\n\n // Unordered list\n if (isUnorderedListItem) {\n if (!inList) {\n html.push('<ul class=\"t-md-editor__list\">');\n inList = true;\n }\n html.push(`<li>${inlineMarkdown(escapeHtml(line.trim().slice(2)))}</li>`);\n continue;\n }\n\n // Ordered list\n if (isOrderedListItem) {\n if (!inOrderedList) {\n html.push('<ol class=\"t-md-editor__list t-md-editor__list--ordered\">');\n inOrderedList = true;\n }\n const content = line.trim().replace(/^\\d+\\.\\s/, '');\n html.push(`<li>${inlineMarkdown(escapeHtml(content))}</li>`);\n continue;\n }\n\n // Empty line\n if (line.trim() === '') {\n html.push('<br />');\n continue;\n }\n\n // Paragraph\n html.push(`<p class=\"t-md-editor__paragraph\">${inlineMarkdown(escapeHtml(line))}</p>`);\n }\n\n // Close any open blocks\n if (inCodeBlock) html.push('</code></pre>');\n if (inList) html.push('</ul>');\n if (inOrderedList) html.push('</ol>');\n\n return html.join('\\n');\n}\n\nfunction inlineMarkdown(text: string): string {\n // Images: ![alt](url)\n text = text.replace(/!\\[([^\\]]*)\\]\\(([^)]+)\\)/g, '<img src=\"$2\" alt=\"$1\" class=\"t-md-editor__image\" loading=\"lazy\" />');\n // Links: [text](url)\n text = text.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, '<a href=\"$2\" target=\"_blank\" rel=\"noopener noreferrer\">$1</a>');\n // Bold: **text**\n text = text.replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>');\n // Italic: _text_ or *text*\n text = text.replace(/_(.+?)_/g, '<em>$1</em>');\n text = text.replace(/(?<!\\*)\\*(?!\\*)(.+?)(?<!\\*)\\*(?!\\*)/g, '<em>$1</em>');\n // Inline code: `text`\n text = text.replace(/`(.+?)`/g, '<code class=\"t-md-editor__inline-code\">$1</code>');\n // Strikethrough: ~~text~~\n text = text.replace(/~~(.+?)~~/g, '<del>$1</del>');\n return text;\n}\n\n/* ──────── Image paste / drop handling ──────── */\n\nconst handlePaste = async (event: ClipboardEvent) => {\n if (props.disabled) return;\n\n const items = event.clipboardData?.items;\n if (!items) return;\n\n for (const item of items) {\n if (item.type.startsWith('image/')) {\n event.preventDefault();\n const file = item.getAsFile();\n if (file) await processImageUpload(file);\n return;\n }\n }\n};\n\nconst handleDrop = async (event: DragEvent) => {\n if (props.disabled) return;\n\n const files = event.dataTransfer?.files;\n if (!files) return;\n\n for (const file of files) {\n if (file.type.startsWith('image/')) {\n event.preventDefault();\n await processImageUpload(file);\n }\n }\n};\n\nconst handleDragOver = (event: DragEvent) => {\n if (props.disabled) return;\n event.preventDefault();\n};\n\nconst processImageUpload = async (file: File) => {\n if (!props.uploadImage) {\n // Fallback: insert a placeholder with the file name\n insertAtCursor(`![${file.name}]()`);\n return;\n }\n\n // Insert uploading placeholder\n const placeholder = `![Uploading ${file.name}...]()`;\n insertAtCursor(placeholder);\n isUploading.value = true;\n\n try {\n const url = props.uploadImage(file);\n const resolvedUrl = url instanceof Promise ? await url : url;\n\n // Replace placeholder with actual URL\n const currentValue = stringValue.value;\n const newValue = currentValue.replace(placeholder, `![${file.name}](${resolvedUrl})`);\n emit('update:modelValue', newValue);\n emit('image-upload', file, resolvedUrl);\n } catch (err) {\n // Remove placeholder on error\n const currentValue = stringValue.value;\n const newValue = currentValue.replace(placeholder, '');\n emit('update:modelValue', newValue);\n emit('image-upload-error', file, err instanceof Error ? err : new Error(String(err)));\n } finally {\n isUploading.value = false;\n }\n};\n\n/* ──────── Keyboard shortcuts ──────── */\n\nconst handleKeydown = (event: KeyboardEvent) => {\n if (props.disabled) return;\n\n const mod = event.metaKey || event.ctrlKey;\n if (!mod) return;\n\n switch (event.key) {\n case 'b':\n event.preventDefault();\n insertBold();\n break;\n case 'i':\n event.preventDefault();\n insertItalic();\n break;\n case 'k':\n event.preventDefault();\n insertLink();\n break;\n case 'e':\n event.preventDefault();\n insertCode();\n break;\n }\n};\n\n</script>\n\n<template>\n <div\n v-bind=\"editorAttrs\"\n :class=\"rootClasses\"\n :style=\"rootStyle\"\n >\n <!-- Toolbar -->\n <div\n class=\"t-md-editor__toolbar\"\n role=\"toolbar\"\n aria-label=\"Markdown formatting\"\n >\n <div class=\"t-md-editor__toolbar-group\">\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Heading 1 (H1)\"\n :disabled=\"disabled\"\n @click=\"insertHeading(1)\"\n >\n H1\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Heading 2 (H2)\"\n :disabled=\"disabled\"\n @click=\"insertHeading(2)\"\n >\n H2\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Heading 3 (H3)\"\n :disabled=\"disabled\"\n @click=\"insertHeading(3)\"\n >\n H3\n </button>\n </div>\n\n <div\n class=\"t-md-editor__toolbar-divider\"\n role=\"separator\"\n />\n\n <div class=\"t-md-editor__toolbar-group\">\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Bold (Ctrl+B)\"\n :disabled=\"disabled\"\n @click=\"insertBold\"\n >\n <strong>B</strong>\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Italic (Ctrl+I)\"\n :disabled=\"disabled\"\n @click=\"insertItalic\"\n >\n <em>I</em>\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Strikethrough\"\n :disabled=\"disabled\"\n @click=\"insertAtCursor('~~', '~~')\"\n >\n <del>S</del>\n </button>\n </div>\n\n <div\n class=\"t-md-editor__toolbar-divider\"\n role=\"separator\"\n />\n\n <div class=\"t-md-editor__toolbar-group\">\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Inline code (Ctrl+E)\"\n :disabled=\"disabled\"\n @click=\"insertCode\"\n >\n &lt;/&gt;\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Code block\"\n :disabled=\"disabled\"\n @click=\"insertCodeBlock\"\n >\n { }\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Link (Ctrl+K)\"\n :disabled=\"disabled\"\n @click=\"insertLink\"\n >\n 🔗\n </button>\n </div>\n\n <div\n class=\"t-md-editor__toolbar-divider\"\n role=\"separator\"\n />\n\n <div class=\"t-md-editor__toolbar-group\">\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Unordered list\"\n :disabled=\"disabled\"\n @click=\"insertList\"\n >\n •&nbsp;List\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Ordered list\"\n :disabled=\"disabled\"\n @click=\"insertOrderedList\"\n >\n 1.&nbsp;List\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Blockquote\"\n :disabled=\"disabled\"\n @click=\"insertQuote\"\n >\n &gt;\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Horizontal rule\"\n :disabled=\"disabled\"\n @click=\"insertHr\"\n >\n ―\n </button>\n </div>\n\n <slot name=\"toolbar\" />\n </div>\n\n <!-- Editor body -->\n <div class=\"t-md-editor__body\">\n <!-- Write pane -->\n <div\n class=\"t-md-editor__pane t-md-editor__pane--write\"\n >\n <textarea\n ref=\"textareaRef\"\n class=\"t-md-editor__textarea\"\n :value=\"stringValue\"\n :placeholder=\"placeholder\"\n :disabled=\"disabled\"\n :aria-label=\"placeholder\"\n @input=\"onInput\"\n @paste=\"handlePaste\"\n @drop=\"handleDrop\"\n @dragover=\"handleDragOver\"\n @keydown=\"handleKeydown\"\n />\n </div>\n\n <!-- Preview pane -->\n <div\n class=\"t-md-editor__pane t-md-editor__pane--preview\"\n aria-label=\"Markdown preview\"\n >\n <!-- eslint-disable vue/no-v-html -->\n <div\n class=\"t-md-editor__preview\"\n v-html=\"renderedHtml\"\n />\n <!-- eslint-enable vue/no-v-html -->\n </div>\n </div>\n\n <!-- Upload indicator -->\n <div\n v-if=\"isUploading\"\n class=\"t-md-editor__upload-bar\"\n role=\"status\"\n aria-live=\"polite\"\n >\n Uploading image…\n </div>\n </div>\n</template>\n"],"mappings":""}
1
+ {"version":3,"file":"TMarkdownEditor.js","names":[],"sources":["../../src/components/TMarkdownEditor.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, ref, useAttrs, nextTick } from 'vue';\nimport type { TSize } from '../types/contracts';\n\ndefineOptions({\n inheritAttrs: false,\n});\n\nconst props = withDefaults(\n defineProps<{\n modelValue?: string;\n size?: TSize;\n disabled?: boolean;\n placeholder?: string;\n uploadImage?: (file: File) => Promise<string>;\n sanitize?: (html: string) => string;\n }>(),\n {\n modelValue: '',\n size: 'md',\n disabled: false,\n placeholder: 'Write your markdown here...',\n uploadImage: undefined,\n sanitize: undefined,\n },\n);\n\nconst emit = defineEmits<{\n 'update:modelValue': [value: string];\n 'image-upload': [file: File, url: string];\n 'image-upload-error': [file: File, error: Error];\n}>();\n\ndefineSlots<{\n toolbar?: () => unknown;\n}>();\n\nconst attrs = useAttrs();\nconst textareaRef = ref<HTMLTextAreaElement | null>(null);\nconst isUploading = ref(false);\n\nconst rootClasses = computed(() => [\n 't-md-editor',\n `t-md-editor--${props.size}`,\n {\n 'is-disabled': props.disabled,\n 'is-uploading': isUploading.value,\n },\n attrs.class,\n]);\n\nconst rootStyle = computed(() => attrs.style);\n\nconst editorAttrs = computed(() => {\n const { class: _class, style: _style, ...rest } = attrs;\n return rest;\n});\n\nconst stringValue = computed(() => `${props.modelValue ?? ''}`);\n\nconst onInput = (event: Event) => {\n emit('update:modelValue', (event.target as HTMLTextAreaElement).value);\n};\n\n/* ──────── Toolbar Actions ──────── */\n\nconst insertAtCursor = (before: string, after = '') => {\n const el = textareaRef.value;\n if (!el) return;\n\n const start = el.selectionStart;\n const end = el.selectionEnd;\n const text = stringValue.value;\n const selected = text.slice(start, end);\n\n const newText = text.slice(0, start) + before + selected + after + text.slice(end);\n emit('update:modelValue', newText);\n\n nextTick(() => {\n el.focus();\n const cursorPos = selected\n ? start + before.length + selected.length + after.length\n : start + before.length;\n el.setSelectionRange(cursorPos, cursorPos);\n });\n};\n\nconst insertHeading = (level: number) => {\n const prefix = '#'.repeat(level) + ' ';\n const el = textareaRef.value;\n if (!el) return;\n\n const start = el.selectionStart;\n const text = stringValue.value;\n\n // Find start of current line\n const lineStart = text.lastIndexOf('\\n', start - 1) + 1;\n const lineEnd = text.indexOf('\\n', start);\n const actualEnd = lineEnd === -1 ? text.length : lineEnd;\n const line = text.slice(lineStart, actualEnd);\n\n // Strip existing heading markers\n const stripped = line.replace(/^#{1,6}\\s*/, '');\n const newLine = prefix + stripped;\n\n const newText = text.slice(0, lineStart) + newLine + text.slice(actualEnd);\n emit('update:modelValue', newText);\n\n nextTick(() => {\n el.focus();\n el.setSelectionRange(lineStart + newLine.length, lineStart + newLine.length);\n });\n};\n\nconst insertBold = () => insertAtCursor('**', '**');\nconst insertItalic = () => insertAtCursor('_', '_');\nconst insertCode = () => insertAtCursor('`', '`');\nconst insertCodeBlock = () => insertAtCursor('\\n```\\n', '\\n```\\n');\nconst insertLink = () => insertAtCursor('[', '](url)');\nconst insertList = () => insertAtCursor('\\n- ');\nconst insertOrderedList = () => insertAtCursor('\\n1. ');\nconst insertQuote = () => insertAtCursor('\\n> ');\nconst insertHr = () => insertAtCursor('\\n---\\n');\n\n/* ──────── Markdown → HTML rendering ──────── */\n\nconst renderedHtml = computed(() => {\n const html = markdownToHtml(stringValue.value);\n // Escape hatch: the built-in rendering escapes HTML and allowlists URL\n // schemes, but consumers with a stricter policy can pass their own\n // sanitizer (DOMPurify or similar) instead of forking the component.\n return props.sanitize ? props.sanitize(html) : html;\n});\n\nfunction escapeHtml(text: string): string {\n return text\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#039;');\n}\n\nfunction markdownToHtml(md: string): string {\n if (!md) return '';\n\n const lines = md.split('\\n');\n const html: string[] = [];\n let inCodeBlock = false;\n let inList = false;\n let inOrderedList = false;\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i];\n\n // Code blocks\n if (line.trim().startsWith('```')) {\n if (inCodeBlock) {\n html.push('</code></pre>');\n inCodeBlock = false;\n } else {\n inCodeBlock = true;\n html.push('<pre class=\"t-md-editor__code-block\"><code>');\n }\n continue;\n }\n\n if (inCodeBlock) {\n html.push(escapeHtml(line));\n html.push('\\n');\n continue;\n }\n\n // Close open lists if needed\n const isUnorderedListItem = /^[-*+]\\s/.test(line.trim());\n const isOrderedListItem = /^\\d+\\.\\s/.test(line.trim());\n\n if (inList && !isUnorderedListItem) {\n html.push('</ul>');\n inList = false;\n }\n if (inOrderedList && !isOrderedListItem) {\n html.push('</ol>');\n inOrderedList = false;\n }\n\n // Horizontal rule\n if (/^(-{3,}|_{3,}|\\*{3,})$/.test(line.trim())) {\n html.push('<hr class=\"t-md-editor__hr\" />');\n continue;\n }\n\n // Headings\n const headingMatch = line.match(/^(#{1,6})\\s+(.+)$/);\n if (headingMatch) {\n const level = headingMatch[1].length;\n html.push(`<h${level} class=\"t-md-editor__heading t-md-editor__heading--${level}\">${inlineMarkdown(escapeHtml(headingMatch[2]))}</h${level}>`);\n continue;\n }\n\n // Blockquote\n if (line.trim().startsWith('> ')) {\n html.push(`<blockquote class=\"t-md-editor__blockquote\">${inlineMarkdown(escapeHtml(line.trim().slice(2)))}</blockquote>`);\n continue;\n }\n\n // Unordered list\n if (isUnorderedListItem) {\n if (!inList) {\n html.push('<ul class=\"t-md-editor__list\">');\n inList = true;\n }\n html.push(`<li>${inlineMarkdown(escapeHtml(line.trim().slice(2)))}</li>`);\n continue;\n }\n\n // Ordered list\n if (isOrderedListItem) {\n if (!inOrderedList) {\n html.push('<ol class=\"t-md-editor__list t-md-editor__list--ordered\">');\n inOrderedList = true;\n }\n const content = line.trim().replace(/^\\d+\\.\\s/, '');\n html.push(`<li>${inlineMarkdown(escapeHtml(content))}</li>`);\n continue;\n }\n\n // Empty line\n if (line.trim() === '') {\n html.push('<br />');\n continue;\n }\n\n // Paragraph\n html.push(`<p class=\"t-md-editor__paragraph\">${inlineMarkdown(escapeHtml(line))}</p>`);\n }\n\n // Close any open blocks\n if (inCodeBlock) html.push('</code></pre>');\n if (inList) html.push('</ul>');\n if (inOrderedList) html.push('</ol>');\n\n return html.join('\\n');\n}\n\n/* ──────── URL safety ──────── */\n\n// A URL carries a scheme only when a colon follows a valid scheme name.\n// Anything else (`/path`, `./path`, `#anchor`, `?q=1`) is relative and cannot\n// introduce a new scheme, so it is safe. `//host/path` is protocol-relative:\n// it keeps the page's protocol but points at another host, so it can navigate\n// off-origin. That is allowed — it is a link, not script — and `rel=\"noopener\n// noreferrer\"` on the anchor covers the tab-nabbing side of it.\nconst HAS_SCHEME = /^[a-z][a-z0-9+.-]*:/;\n// Navigable schemes. `javascript:`, `vbscript:`, `data:` and friends are\n// rejected — everything reaching inlineMarkdown is already HTML-escaped, so\n// the URL scheme is the only remaining way to get script into the preview.\nconst SAFE_LINK_SCHEME = /^(https?|mailto|tel):/;\nconst SAFE_IMAGE_SCHEME = /^https?:/;\n// Inline raster images are useful for pasted screenshots. `image/svg+xml` is\n// deliberately excluded: SVG is a document format that can carry script and\n// external references, not just pixels.\nconst SAFE_IMAGE_DATA_URL = /^data:image\\/(png|jpe?g|gif|webp|avif|bmp|x-icon|vnd\\.microsoft\\.icon)[;,]/;\n\nconst fromCodePoint = (code: number) =>\n Number.isFinite(code) && code >= 0 && code <= 0x10ffff ? String.fromCodePoint(code) : '';\n\n// Reduce a URL to the form a browser would act on before testing its scheme:\n// decode character references, drop control characters and whitespace, and\n// lowercase. This defeats `JaVaScRiPt:`, ` javascript:`, `java\\tscript:`,\n// `java\\x00script:` and `&#106;avascript:` style evasion.\nfunction normalizeUrl(url: string): string {\n return url\n .replace(/&quot;/g, '\"')\n .replace(/&#039;/g, \"'\")\n .replace(/&lt;/g, '<')\n .replace(/&gt;/g, '>')\n .replace(/&amp;/g, '&')\n .replace(/&#x([0-9a-f]+);?/gi, (_match, hex: string) => fromCodePoint(parseInt(hex, 16)))\n .replace(/&#(\\d+);?/g, (_match, dec: string) => fromCodePoint(parseInt(dec, 10)))\n // eslint-disable-next-line no-control-regex -- matching control characters is the point\n .replace(/[\\x00-\\x20\\x7f]/g, '')\n .toLowerCase();\n}\n\nfunction isSafeLinkUrl(url: string): boolean {\n const normalized = normalizeUrl(url);\n return !HAS_SCHEME.test(normalized) || SAFE_LINK_SCHEME.test(normalized);\n}\n\nfunction isSafeImageUrl(url: string): boolean {\n const normalized = normalizeUrl(url);\n if (!HAS_SCHEME.test(normalized)) return true;\n return SAFE_IMAGE_SCHEME.test(normalized) || SAFE_IMAGE_DATA_URL.test(normalized);\n}\n\n// Placeholder delimiter for generated markup. NUL is stripped from the input\n// before any token is created, so a placeholder can never collide with user\n// text, and it survives the emphasis passes because none of them treat it as\n// a delimiter.\nconst TOKEN = '\\x00';\n// eslint-disable-next-line no-control-regex -- matching control characters is the point\nconst TOKEN_PATTERN = /\\x00(\\d+)\\x00/g;\n\nfunction inlineMarkdown(text: string): string {\n const tokens: string[] = [];\n // Park generated markup outside the emphasis passes below. Without this,\n // `_(.+?)_` eats the underscore in the generated target=\"_blank\" and any\n // underscore inside a URL.\n const token = (html: string) => {\n tokens.push(html);\n return `${TOKEN}${tokens.length - 1}${TOKEN}`;\n };\n const inert = (source: string) => token(`<span class=\"t-md-editor__blocked-link\">${source}</span>`);\n\n // eslint-disable-next-line no-control-regex -- matching control characters is the point\n text = text.replace(/\\x00/g, '');\n\n // Images: ![alt](url) — alt is a plain-text attribute, so the whole tag is parked.\n text = text.replace(/!\\[([^\\]]*)\\]\\(([^)]+)\\)/g, (match, alt: string, url: string) =>\n isSafeImageUrl(url)\n ? token(`<img src=\"${url}\" alt=\"${alt}\" class=\"t-md-editor__image\" loading=\"lazy\" />`)\n : inert(match),\n );\n // Links: [text](url) — only the tags are parked so the label still formats.\n text = text.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, (match, label: string, url: string) =>\n isSafeLinkUrl(url)\n ? `${token(`<a href=\"${url}\" target=\"_blank\" rel=\"noopener noreferrer\">`)}${label}${token('</a>')}`\n : inert(match),\n );\n // Bold: **text**\n text = text.replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>');\n // Italic: _text_ or *text*\n text = text.replace(/_(.+?)_/g, '<em>$1</em>');\n text = text.replace(/(?<!\\*)\\*(?!\\*)(.+?)(?<!\\*)\\*(?!\\*)/g, '<em>$1</em>');\n // Inline code: `text`\n text = text.replace(/`(.+?)`/g, '<code class=\"t-md-editor__inline-code\">$1</code>');\n // Strikethrough: ~~text~~\n text = text.replace(/~~(.+?)~~/g, '<del>$1</del>');\n\n return text.replace(TOKEN_PATTERN, (_match, index: string) => tokens[Number(index)]);\n}\n\n/* ──────── Image paste / drop handling ──────── */\n\nconst handlePaste = async (event: ClipboardEvent) => {\n if (props.disabled) return;\n\n const items = event.clipboardData?.items;\n if (!items) return;\n\n for (const item of items) {\n if (item.type.startsWith('image/')) {\n event.preventDefault();\n const file = item.getAsFile();\n if (file) await processImageUpload(file);\n return;\n }\n }\n};\n\nconst handleDrop = async (event: DragEvent) => {\n if (props.disabled) return;\n\n const files = event.dataTransfer?.files;\n if (!files) return;\n\n for (const file of files) {\n if (file.type.startsWith('image/')) {\n event.preventDefault();\n await processImageUpload(file);\n }\n }\n};\n\nconst handleDragOver = (event: DragEvent) => {\n if (props.disabled) return;\n event.preventDefault();\n};\n\nconst processImageUpload = async (file: File) => {\n if (!props.uploadImage) {\n // Fallback: insert a placeholder with the file name\n insertAtCursor(`![${file.name}]()`);\n return;\n }\n\n // Insert uploading placeholder\n const placeholder = `![Uploading ${file.name}...]()`;\n insertAtCursor(placeholder);\n isUploading.value = true;\n\n try {\n const url = props.uploadImage(file);\n const resolvedUrl = url instanceof Promise ? await url : url;\n\n // Replace placeholder with actual URL\n const currentValue = stringValue.value;\n const newValue = currentValue.replace(placeholder, `![${file.name}](${resolvedUrl})`);\n emit('update:modelValue', newValue);\n emit('image-upload', file, resolvedUrl);\n } catch (err) {\n // Remove placeholder on error\n const currentValue = stringValue.value;\n const newValue = currentValue.replace(placeholder, '');\n emit('update:modelValue', newValue);\n emit('image-upload-error', file, err instanceof Error ? err : new Error(String(err)));\n } finally {\n isUploading.value = false;\n }\n};\n\n/* ──────── Keyboard shortcuts ──────── */\n\nconst handleKeydown = (event: KeyboardEvent) => {\n if (props.disabled) return;\n\n const mod = event.metaKey || event.ctrlKey;\n if (!mod) return;\n\n switch (event.key) {\n case 'b':\n event.preventDefault();\n insertBold();\n break;\n case 'i':\n event.preventDefault();\n insertItalic();\n break;\n case 'k':\n event.preventDefault();\n insertLink();\n break;\n case 'e':\n event.preventDefault();\n insertCode();\n break;\n }\n};\n\n</script>\n\n<template>\n <div\n v-bind=\"editorAttrs\"\n :class=\"rootClasses\"\n :style=\"rootStyle\"\n >\n <!-- Toolbar -->\n <div\n class=\"t-md-editor__toolbar\"\n role=\"toolbar\"\n aria-label=\"Markdown formatting\"\n >\n <div class=\"t-md-editor__toolbar-group\">\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Heading 1 (H1)\"\n :disabled=\"disabled\"\n @click=\"insertHeading(1)\"\n >\n H1\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Heading 2 (H2)\"\n :disabled=\"disabled\"\n @click=\"insertHeading(2)\"\n >\n H2\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Heading 3 (H3)\"\n :disabled=\"disabled\"\n @click=\"insertHeading(3)\"\n >\n H3\n </button>\n </div>\n\n <div\n class=\"t-md-editor__toolbar-divider\"\n role=\"separator\"\n />\n\n <div class=\"t-md-editor__toolbar-group\">\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Bold (Ctrl+B)\"\n :disabled=\"disabled\"\n @click=\"insertBold\"\n >\n <strong>B</strong>\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Italic (Ctrl+I)\"\n :disabled=\"disabled\"\n @click=\"insertItalic\"\n >\n <em>I</em>\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Strikethrough\"\n :disabled=\"disabled\"\n @click=\"insertAtCursor('~~', '~~')\"\n >\n <del>S</del>\n </button>\n </div>\n\n <div\n class=\"t-md-editor__toolbar-divider\"\n role=\"separator\"\n />\n\n <div class=\"t-md-editor__toolbar-group\">\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Inline code (Ctrl+E)\"\n :disabled=\"disabled\"\n @click=\"insertCode\"\n >\n &lt;/&gt;\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Code block\"\n :disabled=\"disabled\"\n @click=\"insertCodeBlock\"\n >\n { }\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Link (Ctrl+K)\"\n :disabled=\"disabled\"\n @click=\"insertLink\"\n >\n 🔗\n </button>\n </div>\n\n <div\n class=\"t-md-editor__toolbar-divider\"\n role=\"separator\"\n />\n\n <div class=\"t-md-editor__toolbar-group\">\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Unordered list\"\n :disabled=\"disabled\"\n @click=\"insertList\"\n >\n •&nbsp;List\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Ordered list\"\n :disabled=\"disabled\"\n @click=\"insertOrderedList\"\n >\n 1.&nbsp;List\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Blockquote\"\n :disabled=\"disabled\"\n @click=\"insertQuote\"\n >\n &gt;\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Horizontal rule\"\n :disabled=\"disabled\"\n @click=\"insertHr\"\n >\n ―\n </button>\n </div>\n\n <slot name=\"toolbar\" />\n </div>\n\n <!-- Editor body -->\n <div class=\"t-md-editor__body\">\n <!-- Write pane -->\n <div\n class=\"t-md-editor__pane t-md-editor__pane--write\"\n >\n <textarea\n ref=\"textareaRef\"\n class=\"t-md-editor__textarea\"\n :value=\"stringValue\"\n :placeholder=\"placeholder\"\n :disabled=\"disabled\"\n :aria-label=\"placeholder\"\n @input=\"onInput\"\n @paste=\"handlePaste\"\n @drop=\"handleDrop\"\n @dragover=\"handleDragOver\"\n @keydown=\"handleKeydown\"\n />\n </div>\n\n <!-- Preview pane -->\n <div\n class=\"t-md-editor__pane t-md-editor__pane--preview\"\n aria-label=\"Markdown preview\"\n >\n <!-- eslint-disable vue/no-v-html -->\n <div\n class=\"t-md-editor__preview\"\n v-html=\"renderedHtml\"\n />\n <!-- eslint-enable vue/no-v-html -->\n </div>\n </div>\n\n <!-- Upload indicator -->\n <div\n v-if=\"isUploading\"\n class=\"t-md-editor__upload-bar\"\n role=\"status\"\n aria-live=\"polite\"\n >\n Uploading image…\n </div>\n </div>\n</template>\n"],"mappings":""}
@@ -5,6 +5,7 @@ type __VLS_Props = {
5
5
  disabled?: boolean;
6
6
  placeholder?: string;
7
7
  uploadImage?: (file: File) => Promise<string>;
8
+ sanitize?: (html: string) => string;
8
9
  };
9
10
  declare function __VLS_template(): {
10
11
  attrs: Partial<{}>;
@@ -33,6 +34,7 @@ declare const __VLS_component: import('vue').DefineComponent<__VLS_Props, {}, {}
33
34
  size: TSize;
34
35
  placeholder: string;
35
36
  uploadImage: (file: File) => Promise<string>;
37
+ sanitize: (html: string) => string;
36
38
  }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {
37
39
  textareaRef: HTMLTextAreaElement;
38
40
  }, any>;
@@ -1 +1 @@
1
- {"version":3,"file":"TMarkdownEditor.vue.d.ts","sourceRoot":"","sources":["../../src/components/TMarkdownEditor.vue"],"names":[],"mappings":"AAmjBA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAMhD,KAAK,WAAW,GAAG;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,KAAK,CAAC;IACb,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CAC/C,CAAC;AA+VJ,iBAAS,cAAc;WAkMT,OAAO,IAA6B;;kBA7gBtC,MAAM,OAAO;;kBAAb,MAAM,OAAO;;;;;;EAkhBxB;AAiCD,KAAK,oBAAoB,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AAC9D,QAAA,MAAM,eAAe;;;;;;;;;gBA7kBJ,MAAM;cAER,OAAO;UADX,KAAK;iBAEE,MAAM;iBACN,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,MAAM,CAAC;;;OAklB/C,CAAC;wBACkB,uBAAuB,CAAC,OAAO,eAAe,EAAE,oBAAoB,CAAC,OAAO,CAAC,CAAC;AAAnG,wBAAoG;AAapG,KAAK,uBAAuB,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,GAAG;IACxC,QAAO;QACN,MAAM,EAAE,CAAC,CAAC;KAEV,CAAA;CACD,CAAC"}
1
+ {"version":3,"file":"TMarkdownEditor.vue.d.ts","sourceRoot":"","sources":["../../src/components/TMarkdownEditor.vue"],"names":[],"mappings":"AA0oBA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAMhD,KAAK,WAAW,GAAG;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,KAAK,CAAC;IACb,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAC9C,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;CACrC,CAAC;AAsbJ,iBAAS,cAAc;WAkMT,OAAO,IAA6B;;kBAnmBtC,MAAM,OAAO;;kBAAb,MAAM,OAAO;;;;;;EAwmBxB;AAiCD,KAAK,oBAAoB,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AAC9D,QAAA,MAAM,eAAe;;;;;;;;;gBArqBJ,MAAM;cAER,OAAO;UADX,KAAK;iBAEE,MAAM;iBACN,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,MAAM,CAAC;cAClC,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM;;;OAyqBrC,CAAC;wBACkB,uBAAuB,CAAC,OAAO,eAAe,EAAE,oBAAoB,CAAC,OAAO,CAAC,CAAC;AAAnG,wBAAoG;AAapG,KAAK,uBAAuB,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,GAAG;IACxC,QAAO;QACN,MAAM,EAAE,CAAC,CAAC;KAEV,CAAA;CACD,CAAC"}
@@ -1,12 +1,12 @@
1
- let e=require("vue");var t={class:`t-md-editor__toolbar`,role:`toolbar`,"aria-label":`Markdown formatting`},n={class:`t-md-editor__toolbar-group`},r=[`disabled`],i=[`disabled`],a=[`disabled`],o={class:`t-md-editor__toolbar-group`},s=[`disabled`],c=[`disabled`],l=[`disabled`],u={class:`t-md-editor__toolbar-group`},d=[`disabled`],f=[`disabled`],p=[`disabled`],m={class:`t-md-editor__toolbar-group`},h=[`disabled`],g=[`disabled`],_=[`disabled`],ee=[`disabled`],v={class:`t-md-editor__body`},y={class:`t-md-editor__pane t-md-editor__pane--write`},b=[`value`,`placeholder`,`disabled`,`aria-label`],x={class:`t-md-editor__pane t-md-editor__pane--preview`,"aria-label":`Markdown preview`},S=[`innerHTML`],C={key:0,class:`t-md-editor__upload-bar`,role:`status`,"aria-live":`polite`},w=(0,e.defineComponent)({inheritAttrs:!1,__name:`TMarkdownEditor`,props:{modelValue:{default:``},size:{default:`md`},disabled:{type:Boolean,default:!1},placeholder:{default:`Write your markdown here...`},uploadImage:{type:Function,default:void 0}},emits:[`update:modelValue`,`image-upload`,`image-upload-error`],setup(w,{emit:T}){let E=w,D=T,O=(0,e.useAttrs)(),k=(0,e.ref)(null),A=(0,e.ref)(!1),j=(0,e.computed)(()=>[`t-md-editor`,`t-md-editor--${E.size}`,{"is-disabled":E.disabled,"is-uploading":A.value},O.class]),M=(0,e.computed)(()=>O.style),N=(0,e.computed)(()=>{let{class:e,style:t,...n}=O;return n}),P=(0,e.computed)(()=>`${E.modelValue??``}`),F=e=>{D(`update:modelValue`,e.target.value)},I=(t,n=``)=>{let r=k.value;if(!r)return;let i=r.selectionStart,a=r.selectionEnd,o=P.value,s=o.slice(i,a),c=o.slice(0,i)+t+s+n+o.slice(a);D(`update:modelValue`,c),(0,e.nextTick)(()=>{r.focus();let e=s?i+t.length+s.length+n.length:i+t.length;r.setSelectionRange(e,e)})},L=t=>{let n=`#`.repeat(t)+` `,r=k.value;if(!r)return;let i=r.selectionStart,a=P.value,o=a.lastIndexOf(`
1
+ let e=require("vue");var t={class:`t-md-editor__toolbar`,role:`toolbar`,"aria-label":`Markdown formatting`},n={class:`t-md-editor__toolbar-group`},r=[`disabled`],i=[`disabled`],a=[`disabled`],o={class:`t-md-editor__toolbar-group`},s=[`disabled`],c=[`disabled`],l=[`disabled`],u={class:`t-md-editor__toolbar-group`},d=[`disabled`],f=[`disabled`],p=[`disabled`],ee={class:`t-md-editor__toolbar-group`},m=[`disabled`],h=[`disabled`],g=[`disabled`],te=[`disabled`],_={class:`t-md-editor__body`},v={class:`t-md-editor__pane t-md-editor__pane--write`},y=[`value`,`placeholder`,`disabled`,`aria-label`],b={class:`t-md-editor__pane t-md-editor__pane--preview`,"aria-label":`Markdown preview`},x=[`innerHTML`],ne={key:0,class:`t-md-editor__upload-bar`,role:`status`,"aria-live":`polite`},S=`\0`,C=(0,e.defineComponent)({inheritAttrs:!1,__name:`TMarkdownEditor`,props:{modelValue:{default:``},size:{default:`md`},disabled:{type:Boolean,default:!1},placeholder:{default:`Write your markdown here...`},uploadImage:{type:Function,default:void 0},sanitize:{type:Function,default:void 0}},emits:[`update:modelValue`,`image-upload`,`image-upload-error`],setup(C,{emit:w}){let T=C,E=w,D=(0,e.useAttrs)(),O=(0,e.ref)(null),k=(0,e.ref)(!1),re=(0,e.computed)(()=>[`t-md-editor`,`t-md-editor--${T.size}`,{"is-disabled":T.disabled,"is-uploading":k.value},D.class]),A=(0,e.computed)(()=>D.style),j=(0,e.computed)(()=>{let{class:e,style:t,...n}=D;return n}),M=(0,e.computed)(()=>`${T.modelValue??``}`),N=e=>{E(`update:modelValue`,e.target.value)},P=(t,n=``)=>{let r=O.value;if(!r)return;let i=r.selectionStart,a=r.selectionEnd,o=M.value,s=o.slice(i,a),c=o.slice(0,i)+t+s+n+o.slice(a);E(`update:modelValue`,c),(0,e.nextTick)(()=>{r.focus();let e=s?i+t.length+s.length+n.length:i+t.length;r.setSelectionRange(e,e)})},F=t=>{let n=`#`.repeat(t)+` `,r=O.value;if(!r)return;let i=r.selectionStart,a=M.value,o=a.lastIndexOf(`
2
2
  `,i-1)+1,s=a.indexOf(`
3
- `,i),c=s===-1?a.length:s,l=n+a.slice(o,c).replace(/^#{1,6}\s*/,``),u=a.slice(0,o)+l+a.slice(c);D(`update:modelValue`,u),(0,e.nextTick)(()=>{r.focus(),r.setSelectionRange(o+l.length,o+l.length)})},R=()=>I(`**`,`**`),z=()=>I(`_`,`_`),B=()=>I("`","`"),V=()=>I("\n```\n","\n```\n"),H=()=>I(`[`,`](url)`),U=()=>I(`
4
- - `),W=()=>I(`
5
- 1. `),G=()=>I(`
6
- > `),K=()=>I(`
3
+ `,i),c=s===-1?a.length:s,l=n+a.slice(o,c).replace(/^#{1,6}\s*/,``),u=a.slice(0,o)+l+a.slice(c);E(`update:modelValue`,u),(0,e.nextTick)(()=>{r.focus(),r.setSelectionRange(o+l.length,o+l.length)})},I=()=>P(`**`,`**`),L=()=>P(`_`,`_`),R=()=>P("`","`"),z=()=>P("\n```\n","\n```\n"),B=()=>P(`[`,`](url)`),V=()=>P(`
4
+ - `),H=()=>P(`
5
+ 1. `),U=()=>P(`
6
+ > `),W=()=>P(`
7
7
  ---
8
- `),q=(0,e.computed)(()=>Y(P.value));function J(e){return e.replace(/&/g,`&amp;`).replace(/</g,`&lt;`).replace(/>/g,`&gt;`).replace(/"/g,`&quot;`).replace(/'/g,`&#039;`)}function Y(e){if(!e)return``;let t=e.split(`
9
- `),n=[],r=!1,i=!1,a=!1;for(let e=0;e<t.length;e++){let o=t[e];if(o.trim().startsWith("```")){r?(n.push(`</code></pre>`),r=!1):(r=!0,n.push(`<pre class="t-md-editor__code-block"><code>`));continue}if(r){n.push(J(o)),n.push(`
10
- `);continue}let s=/^[-*+]\s/.test(o.trim()),c=/^\d+\.\s/.test(o.trim());if(i&&!s&&(n.push(`</ul>`),i=!1),a&&!c&&(n.push(`</ol>`),a=!1),/^(-{3,}|_{3,}|\*{3,})$/.test(o.trim())){n.push(`<hr class="t-md-editor__hr" />`);continue}let l=o.match(/^(#{1,6})\s+(.+)$/);if(l){let e=l[1].length;n.push(`<h${e} class="t-md-editor__heading t-md-editor__heading--${e}">${X(J(l[2]))}</h${e}>`);continue}if(o.trim().startsWith(`> `)){n.push(`<blockquote class="t-md-editor__blockquote">${X(J(o.trim().slice(2)))}</blockquote>`);continue}if(s){i||=(n.push(`<ul class="t-md-editor__list">`),!0),n.push(`<li>${X(J(o.trim().slice(2)))}</li>`);continue}if(c){a||=(n.push(`<ol class="t-md-editor__list t-md-editor__list--ordered">`),!0);let e=o.trim().replace(/^\d+\.\s/,``);n.push(`<li>${X(J(e))}</li>`);continue}if(o.trim()===``){n.push(`<br />`);continue}n.push(`<p class="t-md-editor__paragraph">${X(J(o))}</p>`)}return r&&n.push(`</code></pre>`),i&&n.push(`</ul>`),a&&n.push(`</ol>`),n.join(`
11
- `)}function X(e){return e=e.replace(/!\[([^\]]*)\]\(([^)]+)\)/g,`<img src="$2" alt="$1" class="t-md-editor__image" loading="lazy" />`),e=e.replace(/\[([^\]]+)\]\(([^)]+)\)/g,`<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>`),e=e.replace(/\*\*(.+?)\*\*/g,`<strong>$1</strong>`),e=e.replace(/_(.+?)_/g,`<em>$1</em>`),e=e.replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g,`<em>$1</em>`),e=e.replace(/`(.+?)`/g,`<code class="t-md-editor__inline-code">$1</code>`),e=e.replace(/~~(.+?)~~/g,`<del>$1</del>`),e}let Z=async e=>{if(E.disabled)return;let t=e.clipboardData?.items;if(t){for(let n of t)if(n.type.startsWith(`image/`)){e.preventDefault();let t=n.getAsFile();t&&await $(t);return}}},Q=async e=>{if(E.disabled)return;let t=e.dataTransfer?.files;if(t)for(let n of t)n.type.startsWith(`image/`)&&(e.preventDefault(),await $(n))},te=e=>{E.disabled||e.preventDefault()},$=async e=>{if(!E.uploadImage){I(`![${e.name}]()`);return}let t=`![Uploading ${e.name}...]()`;I(t),A.value=!0;try{let n=E.uploadImage(e),r=n instanceof Promise?await n:n,i=P.value.replace(t,`![${e.name}](${r})`);D(`update:modelValue`,i),D(`image-upload`,e,r)}catch(n){let r=P.value.replace(t,``);D(`update:modelValue`,r),D(`image-upload-error`,e,n instanceof Error?n:Error(String(n)))}finally{A.value=!1}},ne=e=>{if(!E.disabled&&(e.metaKey||e.ctrlKey))switch(e.key){case`b`:e.preventDefault(),R();break;case`i`:e.preventDefault(),z();break;case`k`:e.preventDefault(),H();break;case`e`:e.preventDefault(),B();break}};return(T,E)=>((0,e.openBlock)(),(0,e.createElementBlock)(`div`,(0,e.mergeProps)(N.value,{class:j.value,style:M.value}),[(0,e.createElementVNode)(`div`,t,[(0,e.createElementVNode)(`div`,n,[(0,e.createElementVNode)(`button`,{type:`button`,class:`t-md-editor__toolbar-btn`,title:`Heading 1 (H1)`,disabled:w.disabled,onClick:E[0]||=e=>L(1)},` H1 `,8,r),(0,e.createElementVNode)(`button`,{type:`button`,class:`t-md-editor__toolbar-btn`,title:`Heading 2 (H2)`,disabled:w.disabled,onClick:E[1]||=e=>L(2)},` H2 `,8,i),(0,e.createElementVNode)(`button`,{type:`button`,class:`t-md-editor__toolbar-btn`,title:`Heading 3 (H3)`,disabled:w.disabled,onClick:E[2]||=e=>L(3)},` H3 `,8,a)]),E[7]||=(0,e.createElementVNode)(`div`,{class:`t-md-editor__toolbar-divider`,role:`separator`},null,-1),(0,e.createElementVNode)(`div`,o,[(0,e.createElementVNode)(`button`,{type:`button`,class:`t-md-editor__toolbar-btn`,title:`Bold (Ctrl+B)`,disabled:w.disabled,onClick:R},[...E[4]||=[(0,e.createElementVNode)(`strong`,null,`B`,-1)]],8,s),(0,e.createElementVNode)(`button`,{type:`button`,class:`t-md-editor__toolbar-btn`,title:`Italic (Ctrl+I)`,disabled:w.disabled,onClick:z},[...E[5]||=[(0,e.createElementVNode)(`em`,null,`I`,-1)]],8,c),(0,e.createElementVNode)(`button`,{type:`button`,class:`t-md-editor__toolbar-btn`,title:`Strikethrough`,disabled:w.disabled,onClick:E[3]||=e=>I(`~~`,`~~`)},[...E[6]||=[(0,e.createElementVNode)(`del`,null,`S`,-1)]],8,l)]),E[8]||=(0,e.createElementVNode)(`div`,{class:`t-md-editor__toolbar-divider`,role:`separator`},null,-1),(0,e.createElementVNode)(`div`,u,[(0,e.createElementVNode)(`button`,{type:`button`,class:`t-md-editor__toolbar-btn`,title:`Inline code (Ctrl+E)`,disabled:w.disabled,onClick:B},` </> `,8,d),(0,e.createElementVNode)(`button`,{type:`button`,class:`t-md-editor__toolbar-btn`,title:`Code block`,disabled:w.disabled,onClick:V},` { } `,8,f),(0,e.createElementVNode)(`button`,{type:`button`,class:`t-md-editor__toolbar-btn`,title:`Link (Ctrl+K)`,disabled:w.disabled,onClick:H},` 🔗 `,8,p)]),E[9]||=(0,e.createElementVNode)(`div`,{class:`t-md-editor__toolbar-divider`,role:`separator`},null,-1),(0,e.createElementVNode)(`div`,m,[(0,e.createElementVNode)(`button`,{type:`button`,class:`t-md-editor__toolbar-btn`,title:`Unordered list`,disabled:w.disabled,onClick:U},` •\xA0List `,8,h),(0,e.createElementVNode)(`button`,{type:`button`,class:`t-md-editor__toolbar-btn`,title:`Ordered list`,disabled:w.disabled,onClick:W},` 1.\xA0List `,8,g),(0,e.createElementVNode)(`button`,{type:`button`,class:`t-md-editor__toolbar-btn`,title:`Blockquote`,disabled:w.disabled,onClick:G},` > `,8,_),(0,e.createElementVNode)(`button`,{type:`button`,class:`t-md-editor__toolbar-btn`,title:`Horizontal rule`,disabled:w.disabled,onClick:K},` ― `,8,ee)]),(0,e.renderSlot)(T.$slots,`toolbar`)]),(0,e.createElementVNode)(`div`,v,[(0,e.createElementVNode)(`div`,y,[(0,e.createElementVNode)(`textarea`,{ref_key:`textareaRef`,ref:k,class:`t-md-editor__textarea`,value:P.value,placeholder:w.placeholder,disabled:w.disabled,"aria-label":w.placeholder,onInput:F,onPaste:Z,onDrop:Q,onDragover:te,onKeydown:ne},null,40,b)]),(0,e.createElementVNode)(`div`,x,[(0,e.createElementVNode)(`div`,{class:`t-md-editor__preview`,innerHTML:q.value},null,8,S)])]),A.value?((0,e.openBlock)(),(0,e.createElementBlock)(`div`,C,` Uploading image… `)):(0,e.createCommentVNode)(``,!0)],16))}});exports.default=w;
8
+ `),ie=(0,e.computed)(()=>{let e=K(M.value);return T.sanitize?T.sanitize(e):e});function G(e){return e.replace(/&/g,`&amp;`).replace(/</g,`&lt;`).replace(/>/g,`&gt;`).replace(/"/g,`&quot;`).replace(/'/g,`&#039;`)}function K(e){if(!e)return``;let t=e.split(`
9
+ `),n=[],r=!1,i=!1,a=!1;for(let e=0;e<t.length;e++){let o=t[e];if(o.trim().startsWith("```")){r?(n.push(`</code></pre>`),r=!1):(r=!0,n.push(`<pre class="t-md-editor__code-block"><code>`));continue}if(r){n.push(G(o)),n.push(`
10
+ `);continue}let s=/^[-*+]\s/.test(o.trim()),c=/^\d+\.\s/.test(o.trim());if(i&&!s&&(n.push(`</ul>`),i=!1),a&&!c&&(n.push(`</ol>`),a=!1),/^(-{3,}|_{3,}|\*{3,})$/.test(o.trim())){n.push(`<hr class="t-md-editor__hr" />`);continue}let l=o.match(/^(#{1,6})\s+(.+)$/);if(l){let e=l[1].length;n.push(`<h${e} class="t-md-editor__heading t-md-editor__heading--${e}">${Q(G(l[2]))}</h${e}>`);continue}if(o.trim().startsWith(`> `)){n.push(`<blockquote class="t-md-editor__blockquote">${Q(G(o.trim().slice(2)))}</blockquote>`);continue}if(s){i||=(n.push(`<ul class="t-md-editor__list">`),!0),n.push(`<li>${Q(G(o.trim().slice(2)))}</li>`);continue}if(c){a||=(n.push(`<ol class="t-md-editor__list t-md-editor__list--ordered">`),!0);let e=o.trim().replace(/^\d+\.\s/,``);n.push(`<li>${Q(G(e))}</li>`);continue}if(o.trim()===``){n.push(`<br />`);continue}n.push(`<p class="t-md-editor__paragraph">${Q(G(o))}</p>`)}return r&&n.push(`</code></pre>`),i&&n.push(`</ul>`),a&&n.push(`</ol>`),n.join(`
11
+ `)}let q=/^[a-z][a-z0-9+.-]*:/,J=/^(https?|mailto|tel):/,Y=/^https?:/,ae=/^data:image\/(png|jpe?g|gif|webp|avif|bmp|x-icon|vnd\.microsoft\.icon)[;,]/,X=e=>Number.isFinite(e)&&e>=0&&e<=1114111?String.fromCodePoint(e):``;function Z(e){return e.replace(/&quot;/g,`"`).replace(/&#039;/g,`'`).replace(/&lt;/g,`<`).replace(/&gt;/g,`>`).replace(/&amp;/g,`&`).replace(/&#x([0-9a-f]+);?/gi,(e,t)=>X(parseInt(t,16))).replace(/&#(\d+);?/g,(e,t)=>X(parseInt(t,10))).replace(/[\x00-\x20\x7f]/g,``).toLowerCase()}function oe(e){let t=Z(e);return!q.test(t)||J.test(t)}function se(e){let t=Z(e);return!q.test(t)||Y.test(t)||ae.test(t)}let ce=/\x00(\d+)\x00/g;function Q(e){let t=[],n=e=>(t.push(e),`${S}${t.length-1}${S}`),r=e=>n(`<span class="t-md-editor__blocked-link">${e}</span>`);return e=e.replace(/\x00/g,``),e=e.replace(/!\[([^\]]*)\]\(([^)]+)\)/g,(e,t,i)=>se(i)?n(`<img src="${i}" alt="${t}" class="t-md-editor__image" loading="lazy" />`):r(e)),e=e.replace(/\[([^\]]+)\]\(([^)]+)\)/g,(e,t,i)=>oe(i)?`${n(`<a href="${i}" target="_blank" rel="noopener noreferrer">`)}${t}${n(`</a>`)}`:r(e)),e=e.replace(/\*\*(.+?)\*\*/g,`<strong>$1</strong>`),e=e.replace(/_(.+?)_/g,`<em>$1</em>`),e=e.replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g,`<em>$1</em>`),e=e.replace(/`(.+?)`/g,`<code class="t-md-editor__inline-code">$1</code>`),e=e.replace(/~~(.+?)~~/g,`<del>$1</del>`),e.replace(ce,(e,n)=>t[Number(n)])}let le=async e=>{if(T.disabled)return;let t=e.clipboardData?.items;if(t){for(let n of t)if(n.type.startsWith(`image/`)){e.preventDefault();let t=n.getAsFile();t&&await $(t);return}}},ue=async e=>{if(T.disabled)return;let t=e.dataTransfer?.files;if(t)for(let n of t)n.type.startsWith(`image/`)&&(e.preventDefault(),await $(n))},de=e=>{T.disabled||e.preventDefault()},$=async e=>{if(!T.uploadImage){P(`![${e.name}]()`);return}let t=`![Uploading ${e.name}...]()`;P(t),k.value=!0;try{let n=T.uploadImage(e),r=n instanceof Promise?await n:n,i=M.value.replace(t,`![${e.name}](${r})`);E(`update:modelValue`,i),E(`image-upload`,e,r)}catch(n){let r=M.value.replace(t,``);E(`update:modelValue`,r),E(`image-upload-error`,e,n instanceof Error?n:Error(String(n)))}finally{k.value=!1}},fe=e=>{if(!T.disabled&&(e.metaKey||e.ctrlKey))switch(e.key){case`b`:e.preventDefault(),I();break;case`i`:e.preventDefault(),L();break;case`k`:e.preventDefault(),B();break;case`e`:e.preventDefault(),R();break}};return(S,w)=>((0,e.openBlock)(),(0,e.createElementBlock)(`div`,(0,e.mergeProps)(j.value,{class:re.value,style:A.value}),[(0,e.createElementVNode)(`div`,t,[(0,e.createElementVNode)(`div`,n,[(0,e.createElementVNode)(`button`,{type:`button`,class:`t-md-editor__toolbar-btn`,title:`Heading 1 (H1)`,disabled:C.disabled,onClick:w[0]||=e=>F(1)},` H1 `,8,r),(0,e.createElementVNode)(`button`,{type:`button`,class:`t-md-editor__toolbar-btn`,title:`Heading 2 (H2)`,disabled:C.disabled,onClick:w[1]||=e=>F(2)},` H2 `,8,i),(0,e.createElementVNode)(`button`,{type:`button`,class:`t-md-editor__toolbar-btn`,title:`Heading 3 (H3)`,disabled:C.disabled,onClick:w[2]||=e=>F(3)},` H3 `,8,a)]),w[7]||=(0,e.createElementVNode)(`div`,{class:`t-md-editor__toolbar-divider`,role:`separator`},null,-1),(0,e.createElementVNode)(`div`,o,[(0,e.createElementVNode)(`button`,{type:`button`,class:`t-md-editor__toolbar-btn`,title:`Bold (Ctrl+B)`,disabled:C.disabled,onClick:I},[...w[4]||=[(0,e.createElementVNode)(`strong`,null,`B`,-1)]],8,s),(0,e.createElementVNode)(`button`,{type:`button`,class:`t-md-editor__toolbar-btn`,title:`Italic (Ctrl+I)`,disabled:C.disabled,onClick:L},[...w[5]||=[(0,e.createElementVNode)(`em`,null,`I`,-1)]],8,c),(0,e.createElementVNode)(`button`,{type:`button`,class:`t-md-editor__toolbar-btn`,title:`Strikethrough`,disabled:C.disabled,onClick:w[3]||=e=>P(`~~`,`~~`)},[...w[6]||=[(0,e.createElementVNode)(`del`,null,`S`,-1)]],8,l)]),w[8]||=(0,e.createElementVNode)(`div`,{class:`t-md-editor__toolbar-divider`,role:`separator`},null,-1),(0,e.createElementVNode)(`div`,u,[(0,e.createElementVNode)(`button`,{type:`button`,class:`t-md-editor__toolbar-btn`,title:`Inline code (Ctrl+E)`,disabled:C.disabled,onClick:R},` </> `,8,d),(0,e.createElementVNode)(`button`,{type:`button`,class:`t-md-editor__toolbar-btn`,title:`Code block`,disabled:C.disabled,onClick:z},` { } `,8,f),(0,e.createElementVNode)(`button`,{type:`button`,class:`t-md-editor__toolbar-btn`,title:`Link (Ctrl+K)`,disabled:C.disabled,onClick:B},` 🔗 `,8,p)]),w[9]||=(0,e.createElementVNode)(`div`,{class:`t-md-editor__toolbar-divider`,role:`separator`},null,-1),(0,e.createElementVNode)(`div`,ee,[(0,e.createElementVNode)(`button`,{type:`button`,class:`t-md-editor__toolbar-btn`,title:`Unordered list`,disabled:C.disabled,onClick:V},` •\xA0List `,8,m),(0,e.createElementVNode)(`button`,{type:`button`,class:`t-md-editor__toolbar-btn`,title:`Ordered list`,disabled:C.disabled,onClick:H},` 1.\xA0List `,8,h),(0,e.createElementVNode)(`button`,{type:`button`,class:`t-md-editor__toolbar-btn`,title:`Blockquote`,disabled:C.disabled,onClick:U},` > `,8,g),(0,e.createElementVNode)(`button`,{type:`button`,class:`t-md-editor__toolbar-btn`,title:`Horizontal rule`,disabled:C.disabled,onClick:W},` ― `,8,te)]),(0,e.renderSlot)(S.$slots,`toolbar`)]),(0,e.createElementVNode)(`div`,_,[(0,e.createElementVNode)(`div`,v,[(0,e.createElementVNode)(`textarea`,{ref_key:`textareaRef`,ref:O,class:`t-md-editor__textarea`,value:M.value,placeholder:C.placeholder,disabled:C.disabled,"aria-label":C.placeholder,onInput:N,onPaste:le,onDrop:ue,onDragover:de,onKeydown:fe},null,40,y)]),(0,e.createElementVNode)(`div`,b,[(0,e.createElementVNode)(`div`,{class:`t-md-editor__preview`,innerHTML:ie.value},null,8,x)])]),k.value?((0,e.openBlock)(),(0,e.createElementBlock)(`div`,ne,` Uploading image… `)):(0,e.createCommentVNode)(``,!0)],16))}});exports.default=C;
12
12
  //# sourceMappingURL=TMarkdownEditor.vue_vue_type_script_setup_true_lang.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"TMarkdownEditor.vue_vue_type_script_setup_true_lang.cjs","names":[],"sources":["../../src/components/TMarkdownEditor.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, ref, useAttrs, nextTick } from 'vue';\nimport type { TSize } from '../types/contracts';\n\ndefineOptions({\n inheritAttrs: false,\n});\n\nconst props = withDefaults(\n defineProps<{\n modelValue?: string;\n size?: TSize;\n disabled?: boolean;\n placeholder?: string;\n uploadImage?: (file: File) => Promise<string>;\n }>(),\n {\n modelValue: '',\n size: 'md',\n disabled: false,\n placeholder: 'Write your markdown here...',\n uploadImage: undefined,\n },\n);\n\nconst emit = defineEmits<{\n 'update:modelValue': [value: string];\n 'image-upload': [file: File, url: string];\n 'image-upload-error': [file: File, error: Error];\n}>();\n\ndefineSlots<{\n toolbar?: () => unknown;\n}>();\n\nconst attrs = useAttrs();\nconst textareaRef = ref<HTMLTextAreaElement | null>(null);\nconst isUploading = ref(false);\n\nconst rootClasses = computed(() => [\n 't-md-editor',\n `t-md-editor--${props.size}`,\n {\n 'is-disabled': props.disabled,\n 'is-uploading': isUploading.value,\n },\n attrs.class,\n]);\n\nconst rootStyle = computed(() => attrs.style);\n\nconst editorAttrs = computed(() => {\n const { class: _class, style: _style, ...rest } = attrs;\n return rest;\n});\n\nconst stringValue = computed(() => `${props.modelValue ?? ''}`);\n\nconst onInput = (event: Event) => {\n emit('update:modelValue', (event.target as HTMLTextAreaElement).value);\n};\n\n/* ──────── Toolbar Actions ──────── */\n\nconst insertAtCursor = (before: string, after = '') => {\n const el = textareaRef.value;\n if (!el) return;\n\n const start = el.selectionStart;\n const end = el.selectionEnd;\n const text = stringValue.value;\n const selected = text.slice(start, end);\n\n const newText = text.slice(0, start) + before + selected + after + text.slice(end);\n emit('update:modelValue', newText);\n\n nextTick(() => {\n el.focus();\n const cursorPos = selected\n ? start + before.length + selected.length + after.length\n : start + before.length;\n el.setSelectionRange(cursorPos, cursorPos);\n });\n};\n\nconst insertHeading = (level: number) => {\n const prefix = '#'.repeat(level) + ' ';\n const el = textareaRef.value;\n if (!el) return;\n\n const start = el.selectionStart;\n const text = stringValue.value;\n\n // Find start of current line\n const lineStart = text.lastIndexOf('\\n', start - 1) + 1;\n const lineEnd = text.indexOf('\\n', start);\n const actualEnd = lineEnd === -1 ? text.length : lineEnd;\n const line = text.slice(lineStart, actualEnd);\n\n // Strip existing heading markers\n const stripped = line.replace(/^#{1,6}\\s*/, '');\n const newLine = prefix + stripped;\n\n const newText = text.slice(0, lineStart) + newLine + text.slice(actualEnd);\n emit('update:modelValue', newText);\n\n nextTick(() => {\n el.focus();\n el.setSelectionRange(lineStart + newLine.length, lineStart + newLine.length);\n });\n};\n\nconst insertBold = () => insertAtCursor('**', '**');\nconst insertItalic = () => insertAtCursor('_', '_');\nconst insertCode = () => insertAtCursor('`', '`');\nconst insertCodeBlock = () => insertAtCursor('\\n```\\n', '\\n```\\n');\nconst insertLink = () => insertAtCursor('[', '](url)');\nconst insertList = () => insertAtCursor('\\n- ');\nconst insertOrderedList = () => insertAtCursor('\\n1. ');\nconst insertQuote = () => insertAtCursor('\\n> ');\nconst insertHr = () => insertAtCursor('\\n---\\n');\n\n/* ──────── Markdown → HTML rendering ──────── */\n\nconst renderedHtml = computed(() => {\n return markdownToHtml(stringValue.value);\n});\n\nfunction escapeHtml(text: string): string {\n return text\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#039;');\n}\n\nfunction markdownToHtml(md: string): string {\n if (!md) return '';\n\n const lines = md.split('\\n');\n const html: string[] = [];\n let inCodeBlock = false;\n let inList = false;\n let inOrderedList = false;\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i];\n\n // Code blocks\n if (line.trim().startsWith('```')) {\n if (inCodeBlock) {\n html.push('</code></pre>');\n inCodeBlock = false;\n } else {\n inCodeBlock = true;\n html.push('<pre class=\"t-md-editor__code-block\"><code>');\n }\n continue;\n }\n\n if (inCodeBlock) {\n html.push(escapeHtml(line));\n html.push('\\n');\n continue;\n }\n\n // Close open lists if needed\n const isUnorderedListItem = /^[-*+]\\s/.test(line.trim());\n const isOrderedListItem = /^\\d+\\.\\s/.test(line.trim());\n\n if (inList && !isUnorderedListItem) {\n html.push('</ul>');\n inList = false;\n }\n if (inOrderedList && !isOrderedListItem) {\n html.push('</ol>');\n inOrderedList = false;\n }\n\n // Horizontal rule\n if (/^(-{3,}|_{3,}|\\*{3,})$/.test(line.trim())) {\n html.push('<hr class=\"t-md-editor__hr\" />');\n continue;\n }\n\n // Headings\n const headingMatch = line.match(/^(#{1,6})\\s+(.+)$/);\n if (headingMatch) {\n const level = headingMatch[1].length;\n html.push(`<h${level} class=\"t-md-editor__heading t-md-editor__heading--${level}\">${inlineMarkdown(escapeHtml(headingMatch[2]))}</h${level}>`);\n continue;\n }\n\n // Blockquote\n if (line.trim().startsWith('> ')) {\n html.push(`<blockquote class=\"t-md-editor__blockquote\">${inlineMarkdown(escapeHtml(line.trim().slice(2)))}</blockquote>`);\n continue;\n }\n\n // Unordered list\n if (isUnorderedListItem) {\n if (!inList) {\n html.push('<ul class=\"t-md-editor__list\">');\n inList = true;\n }\n html.push(`<li>${inlineMarkdown(escapeHtml(line.trim().slice(2)))}</li>`);\n continue;\n }\n\n // Ordered list\n if (isOrderedListItem) {\n if (!inOrderedList) {\n html.push('<ol class=\"t-md-editor__list t-md-editor__list--ordered\">');\n inOrderedList = true;\n }\n const content = line.trim().replace(/^\\d+\\.\\s/, '');\n html.push(`<li>${inlineMarkdown(escapeHtml(content))}</li>`);\n continue;\n }\n\n // Empty line\n if (line.trim() === '') {\n html.push('<br />');\n continue;\n }\n\n // Paragraph\n html.push(`<p class=\"t-md-editor__paragraph\">${inlineMarkdown(escapeHtml(line))}</p>`);\n }\n\n // Close any open blocks\n if (inCodeBlock) html.push('</code></pre>');\n if (inList) html.push('</ul>');\n if (inOrderedList) html.push('</ol>');\n\n return html.join('\\n');\n}\n\nfunction inlineMarkdown(text: string): string {\n // Images: ![alt](url)\n text = text.replace(/!\\[([^\\]]*)\\]\\(([^)]+)\\)/g, '<img src=\"$2\" alt=\"$1\" class=\"t-md-editor__image\" loading=\"lazy\" />');\n // Links: [text](url)\n text = text.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, '<a href=\"$2\" target=\"_blank\" rel=\"noopener noreferrer\">$1</a>');\n // Bold: **text**\n text = text.replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>');\n // Italic: _text_ or *text*\n text = text.replace(/_(.+?)_/g, '<em>$1</em>');\n text = text.replace(/(?<!\\*)\\*(?!\\*)(.+?)(?<!\\*)\\*(?!\\*)/g, '<em>$1</em>');\n // Inline code: `text`\n text = text.replace(/`(.+?)`/g, '<code class=\"t-md-editor__inline-code\">$1</code>');\n // Strikethrough: ~~text~~\n text = text.replace(/~~(.+?)~~/g, '<del>$1</del>');\n return text;\n}\n\n/* ──────── Image paste / drop handling ──────── */\n\nconst handlePaste = async (event: ClipboardEvent) => {\n if (props.disabled) return;\n\n const items = event.clipboardData?.items;\n if (!items) return;\n\n for (const item of items) {\n if (item.type.startsWith('image/')) {\n event.preventDefault();\n const file = item.getAsFile();\n if (file) await processImageUpload(file);\n return;\n }\n }\n};\n\nconst handleDrop = async (event: DragEvent) => {\n if (props.disabled) return;\n\n const files = event.dataTransfer?.files;\n if (!files) return;\n\n for (const file of files) {\n if (file.type.startsWith('image/')) {\n event.preventDefault();\n await processImageUpload(file);\n }\n }\n};\n\nconst handleDragOver = (event: DragEvent) => {\n if (props.disabled) return;\n event.preventDefault();\n};\n\nconst processImageUpload = async (file: File) => {\n if (!props.uploadImage) {\n // Fallback: insert a placeholder with the file name\n insertAtCursor(`![${file.name}]()`);\n return;\n }\n\n // Insert uploading placeholder\n const placeholder = `![Uploading ${file.name}...]()`;\n insertAtCursor(placeholder);\n isUploading.value = true;\n\n try {\n const url = props.uploadImage(file);\n const resolvedUrl = url instanceof Promise ? await url : url;\n\n // Replace placeholder with actual URL\n const currentValue = stringValue.value;\n const newValue = currentValue.replace(placeholder, `![${file.name}](${resolvedUrl})`);\n emit('update:modelValue', newValue);\n emit('image-upload', file, resolvedUrl);\n } catch (err) {\n // Remove placeholder on error\n const currentValue = stringValue.value;\n const newValue = currentValue.replace(placeholder, '');\n emit('update:modelValue', newValue);\n emit('image-upload-error', file, err instanceof Error ? err : new Error(String(err)));\n } finally {\n isUploading.value = false;\n }\n};\n\n/* ──────── Keyboard shortcuts ──────── */\n\nconst handleKeydown = (event: KeyboardEvent) => {\n if (props.disabled) return;\n\n const mod = event.metaKey || event.ctrlKey;\n if (!mod) return;\n\n switch (event.key) {\n case 'b':\n event.preventDefault();\n insertBold();\n break;\n case 'i':\n event.preventDefault();\n insertItalic();\n break;\n case 'k':\n event.preventDefault();\n insertLink();\n break;\n case 'e':\n event.preventDefault();\n insertCode();\n break;\n }\n};\n\n</script>\n\n<template>\n <div\n v-bind=\"editorAttrs\"\n :class=\"rootClasses\"\n :style=\"rootStyle\"\n >\n <!-- Toolbar -->\n <div\n class=\"t-md-editor__toolbar\"\n role=\"toolbar\"\n aria-label=\"Markdown formatting\"\n >\n <div class=\"t-md-editor__toolbar-group\">\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Heading 1 (H1)\"\n :disabled=\"disabled\"\n @click=\"insertHeading(1)\"\n >\n H1\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Heading 2 (H2)\"\n :disabled=\"disabled\"\n @click=\"insertHeading(2)\"\n >\n H2\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Heading 3 (H3)\"\n :disabled=\"disabled\"\n @click=\"insertHeading(3)\"\n >\n H3\n </button>\n </div>\n\n <div\n class=\"t-md-editor__toolbar-divider\"\n role=\"separator\"\n />\n\n <div class=\"t-md-editor__toolbar-group\">\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Bold (Ctrl+B)\"\n :disabled=\"disabled\"\n @click=\"insertBold\"\n >\n <strong>B</strong>\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Italic (Ctrl+I)\"\n :disabled=\"disabled\"\n @click=\"insertItalic\"\n >\n <em>I</em>\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Strikethrough\"\n :disabled=\"disabled\"\n @click=\"insertAtCursor('~~', '~~')\"\n >\n <del>S</del>\n </button>\n </div>\n\n <div\n class=\"t-md-editor__toolbar-divider\"\n role=\"separator\"\n />\n\n <div class=\"t-md-editor__toolbar-group\">\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Inline code (Ctrl+E)\"\n :disabled=\"disabled\"\n @click=\"insertCode\"\n >\n &lt;/&gt;\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Code block\"\n :disabled=\"disabled\"\n @click=\"insertCodeBlock\"\n >\n { }\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Link (Ctrl+K)\"\n :disabled=\"disabled\"\n @click=\"insertLink\"\n >\n 🔗\n </button>\n </div>\n\n <div\n class=\"t-md-editor__toolbar-divider\"\n role=\"separator\"\n />\n\n <div class=\"t-md-editor__toolbar-group\">\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Unordered list\"\n :disabled=\"disabled\"\n @click=\"insertList\"\n >\n •&nbsp;List\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Ordered list\"\n :disabled=\"disabled\"\n @click=\"insertOrderedList\"\n >\n 1.&nbsp;List\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Blockquote\"\n :disabled=\"disabled\"\n @click=\"insertQuote\"\n >\n &gt;\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Horizontal rule\"\n :disabled=\"disabled\"\n @click=\"insertHr\"\n >\n ―\n </button>\n </div>\n\n <slot name=\"toolbar\" />\n </div>\n\n <!-- Editor body -->\n <div class=\"t-md-editor__body\">\n <!-- Write pane -->\n <div\n class=\"t-md-editor__pane t-md-editor__pane--write\"\n >\n <textarea\n ref=\"textareaRef\"\n class=\"t-md-editor__textarea\"\n :value=\"stringValue\"\n :placeholder=\"placeholder\"\n :disabled=\"disabled\"\n :aria-label=\"placeholder\"\n @input=\"onInput\"\n @paste=\"handlePaste\"\n @drop=\"handleDrop\"\n @dragover=\"handleDragOver\"\n @keydown=\"handleKeydown\"\n />\n </div>\n\n <!-- Preview pane -->\n <div\n class=\"t-md-editor__pane t-md-editor__pane--preview\"\n aria-label=\"Markdown preview\"\n >\n <!-- eslint-disable vue/no-v-html -->\n <div\n class=\"t-md-editor__preview\"\n v-html=\"renderedHtml\"\n />\n <!-- eslint-enable vue/no-v-html -->\n </div>\n </div>\n\n <!-- Upload indicator -->\n <div\n v-if=\"isUploading\"\n class=\"t-md-editor__upload-bar\"\n role=\"status\"\n aria-live=\"polite\"\n >\n Uploading image…\n </div>\n </div>\n</template>\n"],"mappings":"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"}
1
+ {"version":3,"file":"TMarkdownEditor.vue_vue_type_script_setup_true_lang.cjs","names":[],"sources":["../../src/components/TMarkdownEditor.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, ref, useAttrs, nextTick } from 'vue';\nimport type { TSize } from '../types/contracts';\n\ndefineOptions({\n inheritAttrs: false,\n});\n\nconst props = withDefaults(\n defineProps<{\n modelValue?: string;\n size?: TSize;\n disabled?: boolean;\n placeholder?: string;\n uploadImage?: (file: File) => Promise<string>;\n sanitize?: (html: string) => string;\n }>(),\n {\n modelValue: '',\n size: 'md',\n disabled: false,\n placeholder: 'Write your markdown here...',\n uploadImage: undefined,\n sanitize: undefined,\n },\n);\n\nconst emit = defineEmits<{\n 'update:modelValue': [value: string];\n 'image-upload': [file: File, url: string];\n 'image-upload-error': [file: File, error: Error];\n}>();\n\ndefineSlots<{\n toolbar?: () => unknown;\n}>();\n\nconst attrs = useAttrs();\nconst textareaRef = ref<HTMLTextAreaElement | null>(null);\nconst isUploading = ref(false);\n\nconst rootClasses = computed(() => [\n 't-md-editor',\n `t-md-editor--${props.size}`,\n {\n 'is-disabled': props.disabled,\n 'is-uploading': isUploading.value,\n },\n attrs.class,\n]);\n\nconst rootStyle = computed(() => attrs.style);\n\nconst editorAttrs = computed(() => {\n const { class: _class, style: _style, ...rest } = attrs;\n return rest;\n});\n\nconst stringValue = computed(() => `${props.modelValue ?? ''}`);\n\nconst onInput = (event: Event) => {\n emit('update:modelValue', (event.target as HTMLTextAreaElement).value);\n};\n\n/* ──────── Toolbar Actions ──────── */\n\nconst insertAtCursor = (before: string, after = '') => {\n const el = textareaRef.value;\n if (!el) return;\n\n const start = el.selectionStart;\n const end = el.selectionEnd;\n const text = stringValue.value;\n const selected = text.slice(start, end);\n\n const newText = text.slice(0, start) + before + selected + after + text.slice(end);\n emit('update:modelValue', newText);\n\n nextTick(() => {\n el.focus();\n const cursorPos = selected\n ? start + before.length + selected.length + after.length\n : start + before.length;\n el.setSelectionRange(cursorPos, cursorPos);\n });\n};\n\nconst insertHeading = (level: number) => {\n const prefix = '#'.repeat(level) + ' ';\n const el = textareaRef.value;\n if (!el) return;\n\n const start = el.selectionStart;\n const text = stringValue.value;\n\n // Find start of current line\n const lineStart = text.lastIndexOf('\\n', start - 1) + 1;\n const lineEnd = text.indexOf('\\n', start);\n const actualEnd = lineEnd === -1 ? text.length : lineEnd;\n const line = text.slice(lineStart, actualEnd);\n\n // Strip existing heading markers\n const stripped = line.replace(/^#{1,6}\\s*/, '');\n const newLine = prefix + stripped;\n\n const newText = text.slice(0, lineStart) + newLine + text.slice(actualEnd);\n emit('update:modelValue', newText);\n\n nextTick(() => {\n el.focus();\n el.setSelectionRange(lineStart + newLine.length, lineStart + newLine.length);\n });\n};\n\nconst insertBold = () => insertAtCursor('**', '**');\nconst insertItalic = () => insertAtCursor('_', '_');\nconst insertCode = () => insertAtCursor('`', '`');\nconst insertCodeBlock = () => insertAtCursor('\\n```\\n', '\\n```\\n');\nconst insertLink = () => insertAtCursor('[', '](url)');\nconst insertList = () => insertAtCursor('\\n- ');\nconst insertOrderedList = () => insertAtCursor('\\n1. ');\nconst insertQuote = () => insertAtCursor('\\n> ');\nconst insertHr = () => insertAtCursor('\\n---\\n');\n\n/* ──────── Markdown → HTML rendering ──────── */\n\nconst renderedHtml = computed(() => {\n const html = markdownToHtml(stringValue.value);\n // Escape hatch: the built-in rendering escapes HTML and allowlists URL\n // schemes, but consumers with a stricter policy can pass their own\n // sanitizer (DOMPurify or similar) instead of forking the component.\n return props.sanitize ? props.sanitize(html) : html;\n});\n\nfunction escapeHtml(text: string): string {\n return text\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#039;');\n}\n\nfunction markdownToHtml(md: string): string {\n if (!md) return '';\n\n const lines = md.split('\\n');\n const html: string[] = [];\n let inCodeBlock = false;\n let inList = false;\n let inOrderedList = false;\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i];\n\n // Code blocks\n if (line.trim().startsWith('```')) {\n if (inCodeBlock) {\n html.push('</code></pre>');\n inCodeBlock = false;\n } else {\n inCodeBlock = true;\n html.push('<pre class=\"t-md-editor__code-block\"><code>');\n }\n continue;\n }\n\n if (inCodeBlock) {\n html.push(escapeHtml(line));\n html.push('\\n');\n continue;\n }\n\n // Close open lists if needed\n const isUnorderedListItem = /^[-*+]\\s/.test(line.trim());\n const isOrderedListItem = /^\\d+\\.\\s/.test(line.trim());\n\n if (inList && !isUnorderedListItem) {\n html.push('</ul>');\n inList = false;\n }\n if (inOrderedList && !isOrderedListItem) {\n html.push('</ol>');\n inOrderedList = false;\n }\n\n // Horizontal rule\n if (/^(-{3,}|_{3,}|\\*{3,})$/.test(line.trim())) {\n html.push('<hr class=\"t-md-editor__hr\" />');\n continue;\n }\n\n // Headings\n const headingMatch = line.match(/^(#{1,6})\\s+(.+)$/);\n if (headingMatch) {\n const level = headingMatch[1].length;\n html.push(`<h${level} class=\"t-md-editor__heading t-md-editor__heading--${level}\">${inlineMarkdown(escapeHtml(headingMatch[2]))}</h${level}>`);\n continue;\n }\n\n // Blockquote\n if (line.trim().startsWith('> ')) {\n html.push(`<blockquote class=\"t-md-editor__blockquote\">${inlineMarkdown(escapeHtml(line.trim().slice(2)))}</blockquote>`);\n continue;\n }\n\n // Unordered list\n if (isUnorderedListItem) {\n if (!inList) {\n html.push('<ul class=\"t-md-editor__list\">');\n inList = true;\n }\n html.push(`<li>${inlineMarkdown(escapeHtml(line.trim().slice(2)))}</li>`);\n continue;\n }\n\n // Ordered list\n if (isOrderedListItem) {\n if (!inOrderedList) {\n html.push('<ol class=\"t-md-editor__list t-md-editor__list--ordered\">');\n inOrderedList = true;\n }\n const content = line.trim().replace(/^\\d+\\.\\s/, '');\n html.push(`<li>${inlineMarkdown(escapeHtml(content))}</li>`);\n continue;\n }\n\n // Empty line\n if (line.trim() === '') {\n html.push('<br />');\n continue;\n }\n\n // Paragraph\n html.push(`<p class=\"t-md-editor__paragraph\">${inlineMarkdown(escapeHtml(line))}</p>`);\n }\n\n // Close any open blocks\n if (inCodeBlock) html.push('</code></pre>');\n if (inList) html.push('</ul>');\n if (inOrderedList) html.push('</ol>');\n\n return html.join('\\n');\n}\n\n/* ──────── URL safety ──────── */\n\n// A URL carries a scheme only when a colon follows a valid scheme name.\n// Anything else (`/path`, `./path`, `#anchor`, `?q=1`) is relative and cannot\n// introduce a new scheme, so it is safe. `//host/path` is protocol-relative:\n// it keeps the page's protocol but points at another host, so it can navigate\n// off-origin. That is allowed — it is a link, not script — and `rel=\"noopener\n// noreferrer\"` on the anchor covers the tab-nabbing side of it.\nconst HAS_SCHEME = /^[a-z][a-z0-9+.-]*:/;\n// Navigable schemes. `javascript:`, `vbscript:`, `data:` and friends are\n// rejected — everything reaching inlineMarkdown is already HTML-escaped, so\n// the URL scheme is the only remaining way to get script into the preview.\nconst SAFE_LINK_SCHEME = /^(https?|mailto|tel):/;\nconst SAFE_IMAGE_SCHEME = /^https?:/;\n// Inline raster images are useful for pasted screenshots. `image/svg+xml` is\n// deliberately excluded: SVG is a document format that can carry script and\n// external references, not just pixels.\nconst SAFE_IMAGE_DATA_URL = /^data:image\\/(png|jpe?g|gif|webp|avif|bmp|x-icon|vnd\\.microsoft\\.icon)[;,]/;\n\nconst fromCodePoint = (code: number) =>\n Number.isFinite(code) && code >= 0 && code <= 0x10ffff ? String.fromCodePoint(code) : '';\n\n// Reduce a URL to the form a browser would act on before testing its scheme:\n// decode character references, drop control characters and whitespace, and\n// lowercase. This defeats `JaVaScRiPt:`, ` javascript:`, `java\\tscript:`,\n// `java\\x00script:` and `&#106;avascript:` style evasion.\nfunction normalizeUrl(url: string): string {\n return url\n .replace(/&quot;/g, '\"')\n .replace(/&#039;/g, \"'\")\n .replace(/&lt;/g, '<')\n .replace(/&gt;/g, '>')\n .replace(/&amp;/g, '&')\n .replace(/&#x([0-9a-f]+);?/gi, (_match, hex: string) => fromCodePoint(parseInt(hex, 16)))\n .replace(/&#(\\d+);?/g, (_match, dec: string) => fromCodePoint(parseInt(dec, 10)))\n // eslint-disable-next-line no-control-regex -- matching control characters is the point\n .replace(/[\\x00-\\x20\\x7f]/g, '')\n .toLowerCase();\n}\n\nfunction isSafeLinkUrl(url: string): boolean {\n const normalized = normalizeUrl(url);\n return !HAS_SCHEME.test(normalized) || SAFE_LINK_SCHEME.test(normalized);\n}\n\nfunction isSafeImageUrl(url: string): boolean {\n const normalized = normalizeUrl(url);\n if (!HAS_SCHEME.test(normalized)) return true;\n return SAFE_IMAGE_SCHEME.test(normalized) || SAFE_IMAGE_DATA_URL.test(normalized);\n}\n\n// Placeholder delimiter for generated markup. NUL is stripped from the input\n// before any token is created, so a placeholder can never collide with user\n// text, and it survives the emphasis passes because none of them treat it as\n// a delimiter.\nconst TOKEN = '\\x00';\n// eslint-disable-next-line no-control-regex -- matching control characters is the point\nconst TOKEN_PATTERN = /\\x00(\\d+)\\x00/g;\n\nfunction inlineMarkdown(text: string): string {\n const tokens: string[] = [];\n // Park generated markup outside the emphasis passes below. Without this,\n // `_(.+?)_` eats the underscore in the generated target=\"_blank\" and any\n // underscore inside a URL.\n const token = (html: string) => {\n tokens.push(html);\n return `${TOKEN}${tokens.length - 1}${TOKEN}`;\n };\n const inert = (source: string) => token(`<span class=\"t-md-editor__blocked-link\">${source}</span>`);\n\n // eslint-disable-next-line no-control-regex -- matching control characters is the point\n text = text.replace(/\\x00/g, '');\n\n // Images: ![alt](url) — alt is a plain-text attribute, so the whole tag is parked.\n text = text.replace(/!\\[([^\\]]*)\\]\\(([^)]+)\\)/g, (match, alt: string, url: string) =>\n isSafeImageUrl(url)\n ? token(`<img src=\"${url}\" alt=\"${alt}\" class=\"t-md-editor__image\" loading=\"lazy\" />`)\n : inert(match),\n );\n // Links: [text](url) — only the tags are parked so the label still formats.\n text = text.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, (match, label: string, url: string) =>\n isSafeLinkUrl(url)\n ? `${token(`<a href=\"${url}\" target=\"_blank\" rel=\"noopener noreferrer\">`)}${label}${token('</a>')}`\n : inert(match),\n );\n // Bold: **text**\n text = text.replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>');\n // Italic: _text_ or *text*\n text = text.replace(/_(.+?)_/g, '<em>$1</em>');\n text = text.replace(/(?<!\\*)\\*(?!\\*)(.+?)(?<!\\*)\\*(?!\\*)/g, '<em>$1</em>');\n // Inline code: `text`\n text = text.replace(/`(.+?)`/g, '<code class=\"t-md-editor__inline-code\">$1</code>');\n // Strikethrough: ~~text~~\n text = text.replace(/~~(.+?)~~/g, '<del>$1</del>');\n\n return text.replace(TOKEN_PATTERN, (_match, index: string) => tokens[Number(index)]);\n}\n\n/* ──────── Image paste / drop handling ──────── */\n\nconst handlePaste = async (event: ClipboardEvent) => {\n if (props.disabled) return;\n\n const items = event.clipboardData?.items;\n if (!items) return;\n\n for (const item of items) {\n if (item.type.startsWith('image/')) {\n event.preventDefault();\n const file = item.getAsFile();\n if (file) await processImageUpload(file);\n return;\n }\n }\n};\n\nconst handleDrop = async (event: DragEvent) => {\n if (props.disabled) return;\n\n const files = event.dataTransfer?.files;\n if (!files) return;\n\n for (const file of files) {\n if (file.type.startsWith('image/')) {\n event.preventDefault();\n await processImageUpload(file);\n }\n }\n};\n\nconst handleDragOver = (event: DragEvent) => {\n if (props.disabled) return;\n event.preventDefault();\n};\n\nconst processImageUpload = async (file: File) => {\n if (!props.uploadImage) {\n // Fallback: insert a placeholder with the file name\n insertAtCursor(`![${file.name}]()`);\n return;\n }\n\n // Insert uploading placeholder\n const placeholder = `![Uploading ${file.name}...]()`;\n insertAtCursor(placeholder);\n isUploading.value = true;\n\n try {\n const url = props.uploadImage(file);\n const resolvedUrl = url instanceof Promise ? await url : url;\n\n // Replace placeholder with actual URL\n const currentValue = stringValue.value;\n const newValue = currentValue.replace(placeholder, `![${file.name}](${resolvedUrl})`);\n emit('update:modelValue', newValue);\n emit('image-upload', file, resolvedUrl);\n } catch (err) {\n // Remove placeholder on error\n const currentValue = stringValue.value;\n const newValue = currentValue.replace(placeholder, '');\n emit('update:modelValue', newValue);\n emit('image-upload-error', file, err instanceof Error ? err : new Error(String(err)));\n } finally {\n isUploading.value = false;\n }\n};\n\n/* ──────── Keyboard shortcuts ──────── */\n\nconst handleKeydown = (event: KeyboardEvent) => {\n if (props.disabled) return;\n\n const mod = event.metaKey || event.ctrlKey;\n if (!mod) return;\n\n switch (event.key) {\n case 'b':\n event.preventDefault();\n insertBold();\n break;\n case 'i':\n event.preventDefault();\n insertItalic();\n break;\n case 'k':\n event.preventDefault();\n insertLink();\n break;\n case 'e':\n event.preventDefault();\n insertCode();\n break;\n }\n};\n\n</script>\n\n<template>\n <div\n v-bind=\"editorAttrs\"\n :class=\"rootClasses\"\n :style=\"rootStyle\"\n >\n <!-- Toolbar -->\n <div\n class=\"t-md-editor__toolbar\"\n role=\"toolbar\"\n aria-label=\"Markdown formatting\"\n >\n <div class=\"t-md-editor__toolbar-group\">\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Heading 1 (H1)\"\n :disabled=\"disabled\"\n @click=\"insertHeading(1)\"\n >\n H1\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Heading 2 (H2)\"\n :disabled=\"disabled\"\n @click=\"insertHeading(2)\"\n >\n H2\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Heading 3 (H3)\"\n :disabled=\"disabled\"\n @click=\"insertHeading(3)\"\n >\n H3\n </button>\n </div>\n\n <div\n class=\"t-md-editor__toolbar-divider\"\n role=\"separator\"\n />\n\n <div class=\"t-md-editor__toolbar-group\">\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Bold (Ctrl+B)\"\n :disabled=\"disabled\"\n @click=\"insertBold\"\n >\n <strong>B</strong>\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Italic (Ctrl+I)\"\n :disabled=\"disabled\"\n @click=\"insertItalic\"\n >\n <em>I</em>\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Strikethrough\"\n :disabled=\"disabled\"\n @click=\"insertAtCursor('~~', '~~')\"\n >\n <del>S</del>\n </button>\n </div>\n\n <div\n class=\"t-md-editor__toolbar-divider\"\n role=\"separator\"\n />\n\n <div class=\"t-md-editor__toolbar-group\">\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Inline code (Ctrl+E)\"\n :disabled=\"disabled\"\n @click=\"insertCode\"\n >\n &lt;/&gt;\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Code block\"\n :disabled=\"disabled\"\n @click=\"insertCodeBlock\"\n >\n { }\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Link (Ctrl+K)\"\n :disabled=\"disabled\"\n @click=\"insertLink\"\n >\n 🔗\n </button>\n </div>\n\n <div\n class=\"t-md-editor__toolbar-divider\"\n role=\"separator\"\n />\n\n <div class=\"t-md-editor__toolbar-group\">\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Unordered list\"\n :disabled=\"disabled\"\n @click=\"insertList\"\n >\n •&nbsp;List\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Ordered list\"\n :disabled=\"disabled\"\n @click=\"insertOrderedList\"\n >\n 1.&nbsp;List\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Blockquote\"\n :disabled=\"disabled\"\n @click=\"insertQuote\"\n >\n &gt;\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Horizontal rule\"\n :disabled=\"disabled\"\n @click=\"insertHr\"\n >\n ―\n </button>\n </div>\n\n <slot name=\"toolbar\" />\n </div>\n\n <!-- Editor body -->\n <div class=\"t-md-editor__body\">\n <!-- Write pane -->\n <div\n class=\"t-md-editor__pane t-md-editor__pane--write\"\n >\n <textarea\n ref=\"textareaRef\"\n class=\"t-md-editor__textarea\"\n :value=\"stringValue\"\n :placeholder=\"placeholder\"\n :disabled=\"disabled\"\n :aria-label=\"placeholder\"\n @input=\"onInput\"\n @paste=\"handlePaste\"\n @drop=\"handleDrop\"\n @dragover=\"handleDragOver\"\n @keydown=\"handleKeydown\"\n />\n </div>\n\n <!-- Preview pane -->\n <div\n class=\"t-md-editor__pane t-md-editor__pane--preview\"\n aria-label=\"Markdown preview\"\n >\n <!-- eslint-disable vue/no-v-html -->\n <div\n class=\"t-md-editor__preview\"\n v-html=\"renderedHtml\"\n />\n <!-- eslint-enable vue/no-v-html -->\n </div>\n </div>\n\n <!-- Upload indicator -->\n <div\n v-if=\"isUploading\"\n class=\"t-md-editor__upload-bar\"\n role=\"status\"\n aria-live=\"polite\"\n >\n Uploading image…\n </div>\n </div>\n</template>\n"],"mappings":"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"}