duqing2026 commited on
Commit
797a378
·
0 Parent(s):

Initial commit of WeChat Markdown Editor

Browse files
Files changed (7) hide show
  1. Dockerfile +19 -0
  2. README.md +39 -0
  3. app.py +10 -0
  4. requirements.txt +2 -0
  5. static/css/style.css +155 -0
  6. static/js/app.js +152 -0
  7. templates/index.html +69 -0
Dockerfile ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.9-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY requirements.txt .
6
+ RUN pip install --no-cache-dir -r requirements.txt
7
+
8
+ COPY . .
9
+
10
+ # Set up a new user named "user" with user ID 1000
11
+ RUN useradd -m -u 1000 user
12
+
13
+ # Switch to the "user" user
14
+ USER user
15
+
16
+ ENV HOME=/home/user \
17
+ PATH=/home/user/.local/bin:$PATH
18
+
19
+ CMD ["gunicorn", "-b", "0.0.0.0:7860", "app:app"]
README.md ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: WeChat Markdown Editor
3
+ emoji: 📝
4
+ colorFrom: green
5
+ colorTo: blue
6
+ sdk: docker
7
+ pinned: false
8
+ ---
9
+
10
+ # 微信公众号 Markdown 编辑器 (WeChat Markdown Editor)
11
+
12
+ 这是一个专为微信公众号写作设计的 Markdown 编辑器。它可以将 Markdown 实时转换为适合微信公众号粘贴的 HTML 格式,支持多种配色主题和代码高亮。
13
+
14
+ ## 功能特点
15
+
16
+ - **实时预览**:左侧写作,右侧实时预览效果。
17
+ - **一键复制**:点击按钮即可复制带有内联样式的 HTML,直接粘贴到微信公众号后台。
18
+ - **多种主题**:内置多种配色方案(默认、极客黑、清新绿等)。
19
+ - **代码高亮**:完美支持代码块高亮,解决公众号代码排版难题。
20
+ - **链接转脚注**:自动将 Markdown 中的链接转换为文末脚注(因为公众号不支持外链)。
21
+ - **本地优先**:纯前端渲染,保护隐私,无数据上传。
22
+
23
+ ## 技术栈
24
+
25
+ - Backend: Flask
26
+ - Frontend: Vue 3, Tailwind CSS
27
+ - Markdown Engine: markdown-it
28
+ - Syntax Highlighting: highlight.js
29
+
30
+ ## 如何使用
31
+
32
+ 1. 在左侧输入 Markdown 文本。
33
+ 2. 在右侧预览效果。
34
+ 3. 点击右上角的“复制到公众号”按钮。
35
+ 4. 在微信公众号后台编辑器中粘贴 (Ctrl+V / Cmd+V)。
36
+
37
+ ## 作者
38
+
39
+ Designed by DuQing.
app.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, render_template
2
+
3
+ app = Flask(__name__)
4
+
5
+ @app.route('/')
6
+ def index():
7
+ return render_template('index.html')
8
+
9
+ if __name__ == '__main__':
10
+ app.run(host='0.0.0.0', port=7860, debug=True)
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ Flask==3.0.0
2
+ gunicorn==21.2.0
static/css/style.css ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --primary-color: #07c160;
3
+ --text-color: #333;
4
+ --bg-color: #fff;
5
+ --border-color: #eee;
6
+ --code-bg: #f6f8fa;
7
+ --quote-border: #07c160;
8
+ --quote-bg: #f9f9f9;
9
+ }
10
+
11
+ /* Theme: Geek Black */
12
+ [data-theme="geek"] {
13
+ --primary-color: #333;
14
+ --text-color: #2c3e50;
15
+ --code-bg: #282c34;
16
+ --quote-border: #000;
17
+ }
18
+
19
+ /* Theme: Pastel */
20
+ [data-theme="pastel"] {
21
+ --primary-color: #ff9a9e;
22
+ --text-color: #555;
23
+ --code-bg: #fff0f5;
24
+ --quote-border: #fad0c4;
25
+ }
26
+
27
+ body {
28
+ margin: 0;
29
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
30
+ height: 100vh;
31
+ display: flex;
32
+ flex-direction: column;
33
+ background-color: #f3f4f6;
34
+ }
35
+
36
+ /* WeChat Preview Styles - This is what gets copied */
37
+ #preview-content {
38
+ font-size: 16px;
39
+ line-height: 1.75;
40
+ color: var(--text-color);
41
+ word-break: break-all;
42
+ text-align: justify;
43
+ }
44
+
45
+ #preview-content h1,
46
+ #preview-content h2,
47
+ #preview-content h3 {
48
+ margin-top: 1.5em;
49
+ margin-bottom: 1em;
50
+ font-weight: bold;
51
+ color: var(--primary-color);
52
+ }
53
+
54
+ #preview-content h1 {
55
+ font-size: 1.4em;
56
+ border-bottom: 2px solid var(--primary-color);
57
+ padding-bottom: 0.3em;
58
+ }
59
+
60
+ #preview-content h2 {
61
+ font-size: 1.2em;
62
+ display: inline-block;
63
+ border-bottom: 2px solid var(--primary-color);
64
+ padding-bottom: 5px;
65
+ }
66
+
67
+ #preview-content h3 {
68
+ font-size: 1.1em;
69
+ padding-left: 10px;
70
+ border-left: 4px solid var(--primary-color);
71
+ }
72
+
73
+ #preview-content p {
74
+ margin-bottom: 1.5em;
75
+ text-align: justify;
76
+ }
77
+
78
+ #preview-content ul,
79
+ #preview-content ol {
80
+ margin-bottom: 1.5em;
81
+ padding-left: 20px;
82
+ }
83
+
84
+ #preview-content li {
85
+ margin-bottom: 0.5em;
86
+ }
87
+
88
+ #preview-content blockquote {
89
+ margin: 1.5em 0;
90
+ padding: 1em;
91
+ background-color: var(--quote-bg);
92
+ border-left: 4px solid var(--quote-border);
93
+ color: #666;
94
+ font-size: 0.95em;
95
+ border-radius: 4px;
96
+ }
97
+
98
+ #preview-content code {
99
+ font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
100
+ font-size: 0.9em;
101
+ padding: 2px 4px;
102
+ background-color: rgba(27, 31, 35, 0.05);
103
+ color: #d63384;
104
+ border-radius: 4px;
105
+ }
106
+
107
+ #preview-content pre {
108
+ background-color: #282c34; /* Dark theme for code blocks mostly looks better */
109
+ padding: 1em;
110
+ border-radius: 8px;
111
+ overflow-x: auto;
112
+ margin: 1.5em 0;
113
+ font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
114
+ -webkit-overflow-scrolling: touch;
115
+ }
116
+
117
+ #preview-content pre code {
118
+ background-color: transparent;
119
+ color: #abb2bf;
120
+ padding: 0;
121
+ font-size: 14px;
122
+ border-radius: 0;
123
+ }
124
+
125
+ /* Footnotes */
126
+ .footnote-ref {
127
+ color: var(--primary-color);
128
+ text-decoration: none;
129
+ font-size: 0.8em;
130
+ vertical-align: super;
131
+ }
132
+
133
+ .footnotes-sep {
134
+ border-top: 1px solid #ddd;
135
+ margin-top: 2em;
136
+ margin-bottom: 1em;
137
+ }
138
+
139
+ .footnotes-list {
140
+ font-size: 0.9em;
141
+ color: #666;
142
+ }
143
+
144
+ /* Scrollbar */
145
+ ::-webkit-scrollbar {
146
+ width: 8px;
147
+ height: 8px;
148
+ }
149
+ ::-webkit-scrollbar-thumb {
150
+ background: #ccc;
151
+ border-radius: 4px;
152
+ }
153
+ ::-webkit-scrollbar-track {
154
+ background: transparent;
155
+ }
static/js/app.js ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const { createApp, ref, computed, onMounted, watch } = Vue;
2
+
3
+ createApp({
4
+ setup() {
5
+ const markdown = ref(`# 欢迎使用微信 Markdown 编辑器
6
+
7
+ 这是一个专为微信公众号设计的 **Markdown** 编辑器。
8
+
9
+ ## 主要功能
10
+
11
+ 1. **实时预览**:左侧输入,右侧实时预览。
12
+ 2. **代码高亮**:
13
+ \`\`\`python
14
+ def hello():
15
+ print("Hello, WeChat!")
16
+ \`\`\`
17
+ 3. **一键复制**:点击右上角按钮,直接粘贴到公众号后台。
18
+ 4. **多主题**:支持极客黑、粉彩等主题。
19
+
20
+ ## 关于链接
21
+
22
+ 微信公众号不支持外部链接,本编辑器会自动将链接转换为脚注。
23
+ 例如:[GitHub](https://github.com) 和 [Google](https://google.com)。
24
+
25
+ > 引用样式也很漂亮哦。
26
+
27
+ ---
28
+
29
+ 开始你的创作吧!
30
+ `);
31
+
32
+ const currentTheme = ref('default');
33
+ const copied = ref(false);
34
+ const editorRef = ref(null);
35
+ const previewRef = ref(null);
36
+ let isScrolling = false;
37
+
38
+ // Markdown-it setup
39
+ const md = window.markdownit({
40
+ html: true,
41
+ breaks: true,
42
+ linkify: true,
43
+ typographer: true,
44
+ highlight: function (str, lang) {
45
+ if (lang && hljs.getLanguage(lang)) {
46
+ try {
47
+ return hljs.highlight(str, { language: lang, ignoreIllegals: true }).value;
48
+ } catch (__) {}
49
+ }
50
+ return ''; // use external default escaping
51
+ }
52
+ });
53
+
54
+ // Custom Link Renderer for Footnotes
55
+ const defaultRender = md.renderer.rules.link_open || function(tokens, idx, options, env, self) {
56
+ return self.renderToken(tokens, idx, options);
57
+ };
58
+
59
+ // We need a way to store footnotes per render.
60
+ // Since markdown-it is sync, we can reset a global list before render.
61
+ let footnotes = [];
62
+
63
+ md.renderer.rules.link_open = function (tokens, idx, options, env, self) {
64
+ const href = tokens[idx].attrGet('href');
65
+ // Ignore internal links or empty
66
+ if (!href || href.startsWith('#')) {
67
+ return defaultRender(tokens, idx, options, env, self);
68
+ }
69
+
70
+ footnotes.push(href);
71
+ const n = footnotes.length;
72
+
73
+ // Render the link text but add a superscript
74
+ return `<span class="link-text">`;
75
+ };
76
+
77
+ md.renderer.rules.link_close = function (tokens, idx, options, env, self) {
78
+ const n = footnotes.length;
79
+ return `<sup>[${n}]</sup></span>`;
80
+ };
81
+
82
+ const htmlContent = computed(() => {
83
+ footnotes = []; // Reset footnotes
84
+ let rendered = md.render(markdown.value);
85
+
86
+ // Append footnotes if any
87
+ if (footnotes.length > 0) {
88
+ rendered += `<div class="footnotes-sep"></div><div class="footnotes-list">`;
89
+ rendered += `<h3>引用链接</h3><ol>`;
90
+ footnotes.forEach((url, index) => {
91
+ rendered += `<li>${url}</li>`;
92
+ });
93
+ rendered += `</ol></div>`;
94
+ }
95
+ return rendered;
96
+ });
97
+
98
+ const updateTheme = () => {
99
+ document.body.setAttribute('data-theme', currentTheme.value);
100
+ };
101
+
102
+ const copyToWeChat = () => {
103
+ const range = document.createRange();
104
+ range.selectNode(document.getElementById('preview-content'));
105
+ window.getSelection().removeAllRanges();
106
+ window.getSelection().addRange(range);
107
+ document.execCommand('copy');
108
+ window.getSelection().removeAllRanges();
109
+
110
+ copied.value = true;
111
+ setTimeout(() => {
112
+ copied.value = false;
113
+ }, 2000);
114
+ };
115
+
116
+ const handleScroll = (source) => {
117
+ if (isScrolling) return;
118
+ isScrolling = true;
119
+
120
+ const editor = editorRef.value;
121
+ const preview = previewRef.value;
122
+
123
+ if (source === 'editor') {
124
+ const percent = editor.scrollTop / (editor.scrollHeight - editor.clientHeight);
125
+ preview.scrollTop = percent * (preview.scrollHeight - preview.clientHeight);
126
+ } else {
127
+ const percent = preview.scrollTop / (preview.scrollHeight - preview.clientHeight);
128
+ editor.scrollTop = percent * (editor.scrollHeight - editor.clientHeight);
129
+ }
130
+
131
+ setTimeout(() => {
132
+ isScrolling = false;
133
+ }, 50);
134
+ };
135
+
136
+ onMounted(() => {
137
+ updateTheme();
138
+ });
139
+
140
+ return {
141
+ markdown,
142
+ htmlContent,
143
+ currentTheme,
144
+ copied,
145
+ editorRef,
146
+ previewRef,
147
+ updateTheme,
148
+ copyToWeChat,
149
+ handleScroll
150
+ };
151
+ }
152
+ }).mount('#app');
templates/index.html ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>微信公众号 Markdown 编辑器</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
9
+ <script src="https://cdn.jsdelivr.net/npm/markdown-it@13.0.1/dist/markdown-it.min.js"></script>
10
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/atom-one-dark.min.css">
11
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script>
12
+ <link rel="stylesheet" href="/static/css/style.css">
13
+ </head>
14
+ <body class="bg-gray-100 h-screen flex flex-col overflow-hidden">
15
+ <div id="app" class="h-full flex flex-col">
16
+ <!-- Header -->
17
+ <header class="bg-white border-b border-gray-200 px-6 py-3 flex justify-between items-center shadow-sm z-10">
18
+ <div class="flex items-center gap-3">
19
+ <div class="text-2xl">📝</div>
20
+ <h1 class="font-bold text-gray-800 text-lg">WeChat Markdown Editor</h1>
21
+ </div>
22
+
23
+ <div class="flex items-center gap-4">
24
+ <select v-model="currentTheme" @change="updateTheme" class="border border-gray-300 rounded px-3 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-green-500">
25
+ <option value="default">默认主题 (WeChat Green)</option>
26
+ <option value="geek">极客黑 (Geek Black)</option>
27
+ <option value="pastel">粉彩 (Pastel)</option>
28
+ </select>
29
+
30
+ <button @click="copyToWeChat" class="bg-green-600 hover:bg-green-700 text-white px-4 py-1.5 rounded text-sm font-medium transition-colors flex items-center gap-2">
31
+ <span>📋</span> 复制到公众号
32
+ </button>
33
+ </div>
34
+ </header>
35
+
36
+ <!-- Main Content -->
37
+ <main class="flex-1 flex overflow-hidden">
38
+ <!-- Editor -->
39
+ <div class="w-1/2 h-full flex flex-col border-r border-gray-200 bg-gray-50">
40
+ <div class="bg-gray-100 px-4 py-2 text-xs text-gray-500 font-mono border-b border-gray-200">MARKDOWN</div>
41
+ <textarea
42
+ v-model="markdown"
43
+ class="flex-1 w-full p-6 resize-none focus:outline-none font-mono text-sm leading-relaxed bg-gray-50 text-gray-800"
44
+ placeholder="在此输入 Markdown 内容..."
45
+ @scroll="handleScroll('editor')"
46
+ ref="editorRef"
47
+ ></textarea>
48
+ </div>
49
+
50
+ <!-- Preview -->
51
+ <div class="w-1/2 h-full flex flex-col bg-white">
52
+ <div class="bg-gray-100 px-4 py-2 text-xs text-gray-500 font-mono border-b border-gray-200 flex justify-between">
53
+ <span>PREVIEW</span>
54
+ <span v-if="copied" class="text-green-600 font-bold animate-pulse">已复制!</span>
55
+ </div>
56
+ <div
57
+ id="preview-content"
58
+ class="flex-1 w-full p-8 overflow-y-auto"
59
+ v-html="htmlContent"
60
+ @scroll="handleScroll('preview')"
61
+ ref="previewRef"
62
+ ></div>
63
+ </div>
64
+ </main>
65
+ </div>
66
+
67
+ <script src="/static/js/app.js"></script>
68
+ </body>
69
+ </html>