Spaces:
Sleeping
Sleeping
| const { createApp, ref, computed, onMounted, watch } = Vue; | |
| createApp({ | |
| setup() { | |
| const markdown = ref(`# 欢迎使用微信 Markdown 编辑器 | |
| 这是一个专为微信公众号设计的 **Markdown** 编辑器。 | |
| ## 新增功能 ✨ | |
| - **一键格式化**:自动在中英文之间添加空格(盘古之白)。 | |
| - **多主题支持**:新增 **Notion** 风格。 | |
| - **图片美化**:支持圆角和阴影。 | |
| - **字数统计**:实时显示字数和阅读时间。 | |
| ## 代码示例 | |
| \`\`\`python | |
| def hello_world(): | |
| print("Hello, WeChat!") | |
| \`\`\` | |
| ## 引用链接 | |
| 微信公众号不支持外部链接,本编辑器会自动将链接转换为脚注。 | |
| 例如:[GitHub](https://github.com) 和 [Google](https://google.com)。 | |
| > 极致的排版体验。 | |
| `); | |
| const currentTheme = ref('default'); | |
| const imgStyle = ref('none'); // none, rounded, shadow, both | |
| const copied = ref(false); | |
| const editorRef = ref(null); | |
| const previewRef = ref(null); | |
| let isScrolling = false; | |
| // Statistics | |
| const wordCount = computed(() => { | |
| // Simple logic: remove markdown symbols and count | |
| const text = markdown.value.replace(/[#*>\`\-]/g, '').trim(); | |
| return text.length; | |
| }); | |
| const readTime = computed(() => { | |
| return Math.ceil(wordCount.value / 400); // 400 chars per minute | |
| }); | |
| // Pangu (Auto Space) Logic | |
| const formatContent = () => { | |
| let text = markdown.value; | |
| // Add space between CJK and English/Number | |
| text = text.replace(/([\u4e00-\u9fa5])([a-zA-Z0-9])/g, '$1 $2'); | |
| text = text.replace(/([a-zA-Z0-9])([\u4e00-\u9fa5])/g, '$1 $2'); | |
| markdown.value = text; | |
| }; | |
| // Markdown-it setup | |
| const md = window.markdownit({ | |
| html: true, | |
| breaks: true, | |
| linkify: true, | |
| typographer: true, | |
| highlight: function (str, lang) { | |
| if (lang && hljs.getLanguage(lang)) { | |
| try { | |
| return hljs.highlight(str, { language: lang, ignoreIllegals: true }).value; | |
| } catch (__) {} | |
| } | |
| return ''; | |
| } | |
| }); | |
| // Footnotes logic | |
| const defaultRender = md.renderer.rules.link_open || function(tokens, idx, options, env, self) { | |
| return self.renderToken(tokens, idx, options); | |
| }; | |
| let footnotes = []; | |
| md.renderer.rules.link_open = function (tokens, idx, options, env, self) { | |
| const href = tokens[idx].attrGet('href'); | |
| if (!href || href.startsWith('#')) { | |
| return defaultRender(tokens, idx, options, env, self); | |
| } | |
| footnotes.push(href); | |
| return `<span class="link-text">`; | |
| }; | |
| md.renderer.rules.link_close = function (tokens, idx, options, env, self) { | |
| const n = footnotes.length; | |
| return `<sup>[${n}]</sup></span>`; | |
| }; | |
| const htmlContent = computed(() => { | |
| footnotes = []; | |
| let rendered = md.render(markdown.value); | |
| if (footnotes.length > 0) { | |
| rendered += `<div class="footnotes-sep"></div><div class="footnotes-list">`; | |
| rendered += `<h3>引用链接</h3><ol>`; | |
| footnotes.forEach((url) => { | |
| rendered += `<li>${url}</li>`; | |
| }); | |
| rendered += `</ol></div>`; | |
| } | |
| return rendered; | |
| }); | |
| const updateTheme = () => { | |
| document.body.setAttribute('data-theme', currentTheme.value); | |
| }; | |
| const updateImgStyle = () => { | |
| document.body.setAttribute('data-img-style', imgStyle.value); | |
| }; | |
| const copyToWeChat = () => { | |
| const range = document.createRange(); | |
| range.selectNode(document.getElementById('preview-content')); | |
| window.getSelection().removeAllRanges(); | |
| window.getSelection().addRange(range); | |
| document.execCommand('copy'); | |
| window.getSelection().removeAllRanges(); | |
| copied.value = true; | |
| setTimeout(() => { | |
| copied.value = false; | |
| }, 2000); | |
| }; | |
| // Insert Markdown syntax at cursor | |
| const insertSyntax = (type) => { | |
| const textarea = editorRef.value; | |
| const start = textarea.selectionStart; | |
| const end = textarea.selectionEnd; | |
| const text = markdown.value; | |
| const selected = text.substring(start, end); | |
| let before = ''; | |
| let after = ''; | |
| switch(type) { | |
| case 'bold': before = '**'; after = '**'; break; | |
| case 'italic': before = '*'; after = '*'; break; | |
| case 'code': before = '`'; after = '`'; break; | |
| case 'quote': before = '> '; after = ''; break; | |
| case 'link': before = '['; after = '](url)'; break; | |
| case 'image': before = ''; break; | |
| case 'h2': before = '## '; after = ''; break; | |
| case 'h3': before = '### '; after = ''; break; | |
| case 'hr': before = '\n---\n'; after = ''; break; | |
| } | |
| const newText = text.substring(0, start) + before + selected + after + text.substring(end); | |
| markdown.value = newText; | |
| // Restore focus (approximate) | |
| setTimeout(() => { | |
| textarea.focus(); | |
| textarea.setSelectionRange(start + before.length, end + before.length); | |
| }, 0); | |
| }; | |
| const handleScroll = (source) => { | |
| if (isScrolling) return; | |
| isScrolling = true; | |
| const editor = editorRef.value; | |
| const preview = previewRef.value; | |
| if (source === 'editor') { | |
| const percent = editor.scrollTop / (editor.scrollHeight - editor.clientHeight); | |
| preview.scrollTop = percent * (preview.scrollHeight - preview.clientHeight); | |
| } else { | |
| const percent = preview.scrollTop / (preview.scrollHeight - preview.clientHeight); | |
| editor.scrollTop = percent * (editor.scrollHeight - editor.clientHeight); | |
| } | |
| setTimeout(() => { | |
| isScrolling = false; | |
| }, 50); | |
| }; | |
| onMounted(() => { | |
| updateTheme(); | |
| updateImgStyle(); | |
| }); | |
| watch(currentTheme, updateTheme); | |
| watch(imgStyle, updateImgStyle); | |
| return { | |
| markdown, | |
| htmlContent, | |
| currentTheme, | |
| imgStyle, | |
| copied, | |
| wordCount, | |
| readTime, | |
| editorRef, | |
| previewRef, | |
| formatContent, | |
| copyToWeChat, | |
| insertSyntax, | |
| handleScroll | |
| }; | |
| } | |
| }).mount('#app'); | |