Spaces:
Sleeping
Sleeping
File size: 7,139 Bytes
797a378 4167172 797a378 4167172 797a378 4167172 797a378 4167172 797a378 4167172 797a378 4167172 797a378 4167172 797a378 4167172 797a378 4167172 797a378 4167172 797a378 4167172 797a378 4167172 797a378 4167172 797a378 4167172 797a378 4167172 797a378 4167172 797a378 4167172 797a378 4167172 797a378 4167172 797a378 4167172 797a378 4167172 797a378 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 | 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');
|