feat: initial commit

Fork from https://github.com/zkqiang/wechat-mdeditor
This commit is contained in:
yanglbme 2019-11-01 17:16:40 +08:00
parent 8514c5957e
commit ae774c3ac7
17 changed files with 1531 additions and 0 deletions

207
assets/css/app.css Normal file
View File

@ -0,0 +1,207 @@
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
input, button, textarea {
font-family: inherit;
}
h1, h2, h3, h4, h5, h6 {
font-weight: normal;
}
em {
font-style: normal !important;
}
html, body {
height: 100%;
font-family: 'PingFang SC', BlinkMacSystemFont, Roboto, 'Helvetica Neue', sans-serif;
}
.copy-button {
text-decoration: none;
color: #ff3502
}
.el-message__icon {
display: none
}
.container {
height: 100%;
display: flex;
flex-direction: column;
}
.top {
height: 60px;
padding: 10px 20px;
display: flex;
align-items: center;
}
.top {
margin-right: 20px;
}
.web-title {
margin: 0 15px 0 5px;
}
.web-icon {
width: auto;
height: 1.5rem;
vertical-align: middle;
}
#editor {
height: 100%;
display: block;
border: none;
width: 100%;
padding: 10px;
}
section {
height: 100%;
}
.main-body {
display: flex;
flex-direction: column;
padding-top: 0;
padding-bottom: 10px;
}
.ctrl {
flex-basis: 60px;
flex-grow: 1;
flex-shrink: 1;
display: flex;
align-items: center;
}
.preview-wrapper {
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
padding: 0;
align-items: center;
justify-content: center;
display: flex;
/* height: 100%; */
overflow: scroll;
}
.main-section {
display: flex;
height: 100%;
}
.hint {
opacity: 0.6;
margin: 20px 0;
}
.preview {
margin: 0 -20px;
width: 375px;
padding: 20px;
font-size: 14px;
outline: none;
box-shadow: 0 0 60px rgba(0, 0, 0, 0.1);
}
.preview table {
margin-bottom: 10px;
border-collapse: collapse;
display: table;
width: 100% !important;
}
/*.preview ul, .preview ol {*/
/* padding-left: 40px !important;*/
/*}*/
.select-item-left {
float: left;
}
.select-item-right {
float: right;
color: #8492a6;
font-size: 13px;
}
.CodeMirror {
height: 100%;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
font-size: 14px;
padding: 20px;
width: 100%;
font-family: 'PingFang SC', BlinkMacSystemFont, Roboto, 'Helvetica Neue', sans-serif;
}
/* ele ui */
.el-form-item {
margin-bottom: 0 !important;
}
/*wechat code block*/
.rich_media_content .code-snippet *, .rich_media_content .code-snippet__fix * {
max-width: 1000% !important;
}
.code-snippet__fix {
word-wrap: break-word !important;
ont-size: 14px;
margin: 10px 0;
color: #333;
position: relative;
background-color: rgba(0, 0, 0, 0.03);
border: 1px solid #f0f0f0;
border-radius: 2px;
display: flex;
line-height: 26px;
}
.code-snippet__fix .code-snippet__line-index {
counter-reset: line;
flex-shrink: 0;
height: 100%;
padding: 1em;
list-style-type: none;
}
.code-snippet__fix .code-snippet__line-index li {
list-style-type: none;
text-align: right;
}
.code-snippet__fix .code-snippet__line-index li::before {
min-width: 1.5em;
text-align: right;
left: -2.5em;
counter-increment: line;
content: counter(line);
display: inline;
color: rgba(0, 0, 0, 0.15);
}
.code-snippet__fix pre {
overflow-x: auto;
padding: 1em 1em 1em 1em;
white-space: normal;
flex: 1;
-webkit-overflow-scrolling: touch;
}
.code-snippet__fix code {
text-align: left;
font-size: 14px;
white-space: pre;
display: flex;
position: relative;
font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
}

43
assets/css/loading.css Normal file
View File

@ -0,0 +1,43 @@
.loading {
text-align: center;
position: fixed;
width: 100%;
height: 100%;
overflow: hidden;
z-index: 99999;
background-color: #f2f2f2;
}
.loading-wrapper {
position: fixed;
top: 50%;
left: 50%;
-webkit-transform: translateX(-50%) translateY(-50%);
-moz-transform: translateX(-50%) translateY(-50%);
-ms-transform: translateX(-50%) translateY(-50%);
transform: translateX(-50%) translateY(-50%);
}
.loading-text {
line-height: 1.4;
font-size: 1.2rem;
font-weight: bold;
margin-bottom: 1rem;
}
.loading-anim {
width: 35px;
height: 35px;
border: 5px solid rgba(189, 189, 189, 0.25);
border-left-color: rgba(3, 155, 229, 1);
border-top-color: rgba(3, 155, 229, 1);
border-radius: 50%;
display: inline-block;
animation: rotate 600ms infinite linear;
}
@keyframes rotate {
to {
transform: rotate(1turn)
}
}

108
assets/default-content.md Normal file
View File

@ -0,0 +1,108 @@
# 公众号 Markdown 编辑器
### 简介
这款编辑器可以将 Markdown 转换成微信公众号编辑器的样式,只需将 MD 文档复制到左侧栏,再在右侧栏顶部"点击复制",右侧预览内容就可被复制到公众号后台。
这让你在公众号创作时,把更多的时间专注于文章本身,而不是繁琐地调整文章样式。
### 功能
- 支持序号列表和圆点列表,解决了样式会被重置的问题
- 外链会自动转换为参考文献索引,并且附在文章末尾
- 支持多种字体和样式
- 支持日语注音假名、汉语拼音样式
- 支持不同于微信的代码配色方案
- 支持编辑内容自动保存、预览同步滚动等常见功能
### 关于 Markdown
1. Markdown 是一种轻量级标记语言,能将文本换成有效的 XHTML(或者HTML) 文档
2. Markdown 强大之处,在于可以用一套格式,在所有支持 Markdown 的编辑器中转换成发布样式,做到最大化兼容,不需要担心复制到不同编辑器中样式被破坏
3. 正如你右侧看到的这样Markdown 被转换成了微信支持的样式,同样你可以在一字不改的情况下,在 Github 等平台上转换类似的样式
4. 学习 Markdown 的语法,可以查看 [Markdown 语法入门手册](https://www.w3cschool.cn/markdownyfsm/markdownyfsm-odm6256r.html)
## 更多样式
### 注音符号
[注音符号 W3C 定义](http://www.w3.org/TR/ruby/)。
支持日语注音假名、汉语拼音。
用法有以下几种:
* 世界{せかい}
* 小夜時雨{さ・よ・しぐれ}
* 食べる{たべる}
* 丧心病狂{gàn・de・piào・liang}
### 图片
接下来是一张图片。你可以用自己图床,也可以上传到微信媒体库再把图片 URL
粘贴回来,或者编辑好以后,在公众号里插入图片。
![这里可以写图片描述](https://static.zkqiang.cn/images/20191019181145.JPG-slim)
如果使用图床链接的话,有可能复制后图片不能被上传,需要手动在微信重新上传替换。
### 代码块
代码高亮使用了 Github 配色方案,后续会加入更多配色。
**注意:由于微信编辑器限制,复制后若在微信编辑器中点击代码块,会被微信自动重置后它的配色,只能重新再复制**
```cpp
#include <stdio.h>
const int MAX = 10;
int cache[MAX] = {0};
int fib(int x) {
if (x == 1) return 1;
if (x == 0) return 0;
if (cache[x] == 0) {
int ret = fib(x - 1) + fib(x - 2);
cache[x] = ret;
}
return cache[x];
}
int main() {
int i;
printf("fibonacci series:\n");
for (i = 0; i < MAX; ++i) {
printf("%d ", fib(i));
}
return 0;
}
```
### 内联代码
inline code `{code: 0}`
### 表格
表格无法使用自定义样式,暂时没找到解决途径
| Header 1 | Header 2 |
| --- | --- |
| Key 1 | Value 1 |
| Key 2 | Value 2 |
| Key 3 | Value 3 |
### 超链接
如果是公众号文章的超链接,是可以点击打开的,但其他链接都无法点击,所以这里使用类似于文献的底部引用。
例如:
[这是一篇公众号文章](https://mp.weixin.qq.com/s/ahpV7Poj5wHmtUP6vqy3gg)
[这是我的博客地址](http://zkqiang.cn)
[通过引号设置引用名](http://prod.zkqiang.cn/wxeditor "这是自定义的引用名")
[本项目是 Fork 自 Lyric 原项目后的二次开发,感谢他的贡献!](https://github.com/lyricat/wechat-format "原项目代码库")

BIN
assets/images/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

163
assets/scripts/editor.js Normal file
View File

@ -0,0 +1,163 @@
let app = new Vue({
el: '#app',
data: function () {
let d = {
aboutOutput: '',
output: '',
source: '',
editorThemes: [
{ label: 'base16-light', value: 'base16-light' },
{ label: 'duotone-light', value: 'duotone-light' },
{ label: 'monokai', value: 'monokai' }
],
editor: null,
builtinFonts: [
{
label: '无衬线',
value: "-apple-system-font,BlinkMacSystemFont, Helvetica Neue, PingFang SC, Hiragino Sans GB , Microsoft YaHei UI , Microsoft YaHei ,Arial,sans-serif"
},
{
label: '衬线',
value: "Optima-Regular, Optima, PingFangSC-light, PingFangTC-light, 'PingFang SC', Cambria, Cochin, Georgia, Times, 'Times New Roman', serif"
}
],
sizeOption: [
{ label: '14px', value: '14px', desc: '稍小' },
{ label: '15px', value: '15px', desc: '默认' },
{ label: '16px', value: '16px', desc: '稍大' },
{ label: '17px', value: '17px', desc: '很大' },
],
themeOption: [
{ label: 'default', value: 'default', author: '张凯强' },
{ label: 'lyric', value: 'lyric', author: 'Lyric' },
{ label: 'lupeng', value: 'lupeng', author: '鲁鹏' }
],
styleThemes: {
default: defaultTheme,
lyric: lyricTheme,
lupeng: lupengTheme
},
aboutDialogVisible: false
};
d.currentEditorTheme = d.editorThemes[0].value;
d.currentFont = d.builtinFonts[0].value;
d.currentSize = d.sizeOption[1].value;
d.currentTheme = d.themeOption[0].value;
return d;
},
mounted() {
let self = this;
this.editor = CodeMirror.fromTextArea(
document.getElementById('editor'),
{
lineNumbers: false,
lineWrapping: true,
styleActiveLine: true,
theme: this.currentEditorTheme,
mode: 'text/x-markdown',
}
);
this.editor.on("change", function (cm, change) {
self.refresh();
self.saveEditorContent();
});
this.wxRenderer = new WxRenderer({
theme: this.styleThemes.default,
fonts: this.currentFont,
size: this.currentSize
});
// 如果有编辑内容被保存则读取,否则加载默认文档
if (localStorage.getItem("__editor_content")) {
this.editor.setValue(localStorage.getItem("__editor_content"));
} else {
axios({
method: 'get',
url: './assets/default-content.md',
}).then(function (resp) {
self.editor.setValue(resp.data)
})
}
},
methods: {
renderWeChat: function (source) {
let output = marked(source, { renderer: this.wxRenderer.getRenderer() });
if (this.wxRenderer.hasFootnotes()) {
// 去除第一行的 margin-top
output = output.replace(/(style=".*?)"/, '$1;margin-top: 0"');
// 引用注脚
output += this.wxRenderer.buildFootnotes();
// 附加的一些 style
output += this.wxRenderer.buildAddition();
}
return output
},
editorThemeChanged: function (editorTheme) {
this.editor.setOption('theme', editorTheme)
},
fontChanged: function (fonts) {
this.wxRenderer.setOptions({
fonts: fonts
});
this.refresh()
},
sizeChanged: function (size) {
this.wxRenderer.setOptions({
size: size
});
this.refresh()
},
themeChanged: function (themeName) {
let themeObject = this.styleThemes[themeName];
this.wxRenderer.setOptions({
theme: themeObject
});
this.refresh()
},
// 刷新右侧预览
refresh: function () {
this.output = this.renderWeChat(this.editor.getValue(0))
},
// 将左侧编辑器内容保存到 LocalStorage
saveEditorContent: function () {
let content = this.editor.getValue(0);
if (content){
localStorage.setItem("__editor_content", content);
} else {
localStorage.removeItem("__editor_content");
}
},
copy: function () {
let clipboardDiv = document.getElementById('output');
clipboardDiv.focus();
window.getSelection().removeAllRanges();
let range = document.createRange();
range.setStartBefore(clipboardDiv.firstChild);
range.setEndAfter(clipboardDiv.lastChild);
window.getSelection().addRange(range);
try {
if (document.execCommand('copy')) {
this.$message({
message: '已复制到剪贴板', type: 'success'
})
} else {
this.$message({
message: '未能复制到剪贴板,请全选后右键复制', type: 'warning'
})
}
} catch (err) {
this.$message({
message: '未能复制到剪贴板,请全选后右键复制', type: 'warning'
})
}
},
openWindow: function (url) {
window.open(url);
}
},
updated: function () {
this.$nextTick(function () {
prettyPrint()
})
}
});

View File

@ -0,0 +1,4 @@
// 加载完成隐藏 loading 界面
window.onload = () => {
$('#loading').hide();
};

View File

@ -0,0 +1,200 @@
let WxRenderer = function (opts) {
this.opts = opts;
let ENV_USE_REFERENCES = true;
let ENV_STRETCH_IMAGE = true;
let footnotes = [];
let footnoteIndex = 0;
let styleMapping = null;
let CODE_FONT_FAMILY = "Menlo, Operator Mono, Consolas, Monaco, monospace";
let merge = function (base, extend) {
return Object.assign({}, base, extend)
};
this.buildTheme = function (themeTpl) {
let mapping = {};
let base = merge(themeTpl.BASE, {
'font-family': this.opts.fonts,
'font-size': this.opts.size
});
let base_block = merge(base, {});
for (let ele in themeTpl.inline) {
if (themeTpl.inline.hasOwnProperty(ele)) {
let style = themeTpl.inline[ele];
if (ele === 'codespan') {
style['font-family'] = CODE_FONT_FAMILY;
style['white-space'] = 'normal';
}
mapping[ele] = merge(base, style)
}
}
for (let ele in themeTpl.block) {
if (themeTpl.block.hasOwnProperty(ele)) {
let style = themeTpl.block[ele];
if (ele === 'code') {
style['font-family'] = CODE_FONT_FAMILY
}
mapping[ele] = merge(base_block, style)
}
}
return mapping
};
let getStyles = function (tokenName, addition) {
let arr = [];
let dict = styleMapping[tokenName];
if (!dict) return '';
for (const key in dict) {
arr.push(key + ':' + dict[key])
}
return `style="${ arr.join(';') + (addition || '') }"`
};
let addFootnote = function (title, link) {
footnoteIndex += 1;
footnotes.push([footnoteIndex, title, link]);
return footnoteIndex
};
this.buildFootnotes = function () {
let footnoteArray = footnotes.map(function (x) {
if (x[1] === x[2]) {
return `<code style="font-size: 90%; opacity: 0.6;">[${ x[0] }]</code>: <i>${ x[1] }</i><br/>`
}
return `<code style="font-size: 90%; opacity: 0.6;">[${ x[0] }]</code> ${ x[1] }: <i>${ x[2] }</i><br/>`
});
return `<h3 ${ getStyles('h3') }>References</h3><p ${ getStyles('footnotes') }>${ footnoteArray.join('\n') }</p>`
};
this.buildAddition = function () {
return '<style>.preview-wrapper pre::before{' +
'font-family:"SourceSansPro","HelveticaNeue",Arial,sans-serif;' +
'position:absolute;' +
'top:0;' +
'right:0;' +
'color:#ccc;' +
'text-align:right;' +
'font-size:0.8em;' +
'padding:5px10px0;' +
'line-height:15px;' +
'height:15px;' +
'font-weight:600;' +
'}</style>'
};
this.setOptions = function (newOpts) {
this.opts = merge(this.opts, newOpts)
};
this.hasFootnotes = function () {
return footnotes.length !== 0
};
this.getRenderer = function () {
footnotes = [];
footnoteIndex = 0;
styleMapping = this.buildTheme(this.opts.theme);
let renderer = new marked.Renderer();
FuriganaMD.register(renderer);
renderer.heading = function (text, level) {
switch (level) {
case 1:
return `<h1 ${ getStyles('h1') }>${ text }</h1>`;
case 2:
return `<h2 ${ getStyles('h2') }>${ text }</h2>`;
case 3:
return `<h3 ${ getStyles('h3') }>${ text }</h3>`;
default:
return `<h4 ${ getStyles('h4') }>${ text }</h4>`;
}
};
renderer.paragraph = function (text) {
return `<p ${ getStyles('p') }>${ text }</p>`
};
renderer.blockquote = function (text) {
text = text.replace(/<p.*?>/, `<p ${ getStyles('blockquote_p') }>`);
return `<blockquote ${ getStyles('blockquote') }>${ text }</blockquote>`
};
renderer.code = function (text, infoString) {
text = text.replace(/</g, "&lt;");
text = text.replace(/>/g, "&gt;");
let lines = text.split('\n');
let codeLines = [];
let numbers = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
codeLines.push(`<code><span class="code-snippet_outer">${ (line || '<br>') }</span></code>`);
numbers.push('<li></li>')
}
let lang = infoString || '';
return `<section class="code-snippet__fix code-snippet__js">`
+ `<ul class="code-snippet__line-index code-snippet__js">${ numbers.join('') }</ul>`
+ `<pre class="code-snippet__js prettyprint" data-lang="${ lang }">`
+ codeLines.join('')
+ `</pre></section>`
};
renderer.codespan = function (text, infoString) {
return `<code ${ getStyles('codespan') }>${ text }</code>`
};
renderer.listitem = function (text) {
return `<span ${ getStyles('listitem') }><span style="margin-right: 10px;"><%s/></span>${ text }</span>`;
};
renderer.list = function (text, ordered, start) {
text = text.replace(/<\/*p.*?>/g, '');
let segments = text.split(`<%s/>`);
if (!ordered) {
text = segments.join('•');
return `<p ${ getStyles('ul') }>${ text }</p>`;
}
text = segments[0];
for (let i = 1; i < segments.length; i++) {
text = text + i + '.' + segments[i];
}
return `<p ${ getStyles('ol') }>${ text }</p>`;
};
renderer.image = function (href, title, text) {
let subText = '';
if (text) {
subText = `<figcaption ${ getStyles('figcaption') }>${ text }</figcaption>`
}
let figureStyles = getStyles('figure');
let imgStyles = getStyles(ENV_STRETCH_IMAGE ? 'image' : 'image_org');
return `<figure ${ figureStyles }><img ${ imgStyles } src="${ href }" title="${ title }" alt="${ text }"/>${ subText }</figure>`
};
renderer.link = function (href, title, text) {
if (href.indexOf('https://mp.weixin.qq.com') === 0) {
return `<a href="${ href }" title="${ (title || text) }" ${ getStyles('wx_link') }>${ text }</a>`;
} else if (href === text) {
return text;
} else {
if (ENV_USE_REFERENCES) {
let ref = addFootnote(title || text, href);
return `<span ${ getStyles('link') }>${ text }<sup>[${ ref }]</sup></span>`;
} else {
return `<a href="${ href }" title="${ (title || text) }" ${ getStyles('link') }>${ text }</a>`;
}
}
};
renderer.strong = function (text) {
return `<strong ${ getStyles('strong') }>${ text }</strong>`;
};
renderer.em = function (text) {
return `<p ${ getStyles('p', ';font-style: italic;')}>${ text }</p>`
};
renderer.table = function (header, body) {
return `<table class="preview-table"><thead ${ getStyles('thead') }>${ header }</thead><tbody>${ body }</tbody></table>`;
};
renderer.tablecell = function (text, flags) {
return `<td ${ getStyles('td') }>${ text }</td>`;
};
renderer.hr = function () {
return `<hr style="border-style: solid;border-width: 1px 0 0;border-color: rgba(0,0,0,0.1);-webkit-transform-origin: 0 0;-webkit-transform: scale(1, 0.5);transform-origin: 0 0;transform: scale(1, 0.5);">`;
};
return renderer
}
};

View File

@ -0,0 +1,150 @@
let defaultTheme = {
BASE: {
'text-align': 'left',
'color': '#3f3f3f',
'line-height': '1.75',
},
BASE_BLOCK: {
'margin': '1em 8px'
},
// block element
block: {
h1: {
'font-size': '1.2em',
'text-align': 'center',
'font-weight': 'bold',
'display': 'table',
'margin': '2em auto 1em auto',
'padding': '0 1em',
'border-bottom': '1px solid rgb(248,57,41)'
},
h2: {
'font-size': '1.2em',
'text-align': 'center',
'font-weight': 'bold',
'display': 'table',
'margin': '4em auto 2em auto',
'padding': '0 1em',
'border-bottom': '1px solid rgb(248,57,41)'
},
h3: {
'font-weight': 'bold',
'font-size': '1.1em',
'margin': '2em 8px 0.75em 0',
'padding-bottom': '.1em',
// 'border-bottom': '1px solid #eaecef',
'padding-left': '8px',
'border-left': '4px solid rgb(248,57,41)'
},
h4: {
'font-weight': 'bold',
'font-size': '1em',
'margin': '2em 8px 0.5em 8px',
},
p: {
'margin': '1.5em 8px',
'letter-spacing': '0.1em'
},
blockquote: {
'font-style': 'normal',
'border-left': 'none',
'padding': '1em',
'border-radius': '4px',
'color': '#FEEEED',
'background': 'rgba(27,31,35,.05)',
'margin': '2em 8px'
},
blockquote_p: {
'letter-spacing': '0.1em',
'color': 'rgb(80, 80, 80)',
'font-family': 'PingFangSC-light, PingFangTC-light, Open Sans, Helvetica Neue, sans-serif',
'font-size': '1em',
'display': 'inline',
},
code: {
'font-size': '80%',
'overflow': 'auto',
'color': '#333',
'background': 'rgb(247, 247, 247)',
'border-radius': '2px',
'padding': '10px',
'line-height': '1.5',
'border': '1px solid rgb(236,236,236)',
'margin': '20px 0',
},
image: {
'border-radius': '4px',
'display': 'block',
'margin': '0.5em auto',
'width': '100%'
},
image_org: {
'border-radius': '4px',
'display': 'block'
},
ol: {
'margin-left': '0',
'padding-left': '1em'
},
ul: {
'margin-left': '0',
'padding-left': '1em',
'list-style': 'circle'
},
footnotes: {
'margin': '0.5em 8px',
'font-size': '80%'
},
figure: {
'margin': '1.5em 8px',
}
},
inline: {
// inline element
listitem: {
'text-indent': '-1em',
'display': 'block',
'margin': '0.5em 8px'
},
codespan: {
'font-size': '90%',
'color': '#d14',
'background': 'rgba(27,31,35,.05)',
'padding': '3px 5px',
'border-radius': '4px',
},
link: {
'color': '#009926'
},
wx_link: {
'color': '#0080ff',
'text-decoration': 'none',
'border-bottom': '1px solid #d1e9ff'
},
strong: {
'color': '#ff5f2e',
'font-weight': 'bold',
},
table: {
'border-collapse': 'collapse',
'text-align': 'center',
'margin': '1em 8px'
},
thead: {
'background': 'rgba(0, 0, 0, 0.05)'
},
td: {
'font-size': '80%',
'border': '1px solid #dfdfdf',
'padding': '0.25em 0.5em'
},
footnote: {
'font-size': '12px'
},
figcaption: {
'text-align': 'center',
'color': '#888',
'font-size': '0.8em'
}
}
};

View File

@ -0,0 +1,125 @@
let lupengTheme = {
BASE: {
'text-align': 'left',
'color': '#595959',
'line-height': '1.55em',
'letter-spacing': '0.06em'
},
BASE_BLOCK: {
'margin': '20px 10px'
},
block: {
h1: {
'font-size': '140%',
'text-align': 'center',
'font-weight': 'normal',
'margin': '80px 10px 40px 10px'
},
h2: {
'font-size': '140%',
'text-align': 'center',
'font-weight': 'normal',
'margin': '80px 10px 40px 10px'
},
h3: {
'font-weight': 'bold',
'font-size': '120%',
'margin': '40px 10px 20px 10px'
},
h4: {
'font-weight': 'bold',
'font-size': '100%',
'margin': '20px 10px 10px 10px'
},
p: {
'margin': '10px 10px',
'line-height': '1.6'
},
blockquote: {
'color': '#9a9a9a',
'padding-left': '10px',
// 'padding-top': '0.05px',
'background-color': '#fefefe',
'line-height': '1.6',
'border-left': '3px solid #dbdbdb',
'font-size': '15px',
'margin': '1em 0'
},
code: {
'font-size': '80%',
'overflow': 'auto',
'color': '#333',
'background': 'rgb(247, 247, 247)',
'border-radius': '2px',
'padding': '10px',
'line-height': '1.3',
'border': '1px solid rgb(236,236,236)',
'margin': '20px 0',
},
image: {
'border-radius': '4px',
'display': 'block',
'margin': '20px auto',
'width': '100%',
},
image_org: {
'border-radius': '4px',
'display': 'block',
},
ol: {
'margin-left': '0',
'padding-left': '20px'
},
ul: {
'margin-left': '0',
'padding-left': '20px',
'list-style': 'circle',
},
footnotes: {
'margin': '10px 10px',
'font-size': '14px'
}
},
inline: {
// inline element
listitem: {
'text-indent': '-20px',
'display': 'block',
'margin': '10px 10px',
},
codespan: {
'font-size': '0.8em',
'color': '#d14',
'background': '#fefefe',
'padding': '3px 5px 0px',
'margin': '0px 2px',
'border': '1px solid #ddd',
'border-radius': '3px',
},
link: {
'color': '#ff3502'
},
wx_link: {
'color': '#576b95',
'text-decoration': 'none'
},
strong: {
'font-weight': 'bold',
},
table: {
'border-collapse': 'collapse',
'margin': '20px 0',
},
thead: {
'background': 'rgba(0,0,0,0.05)',
},
td: {
'font-size': '80%',
'border': '1px solid #dfdfdf',
'padding': '4px 8px',
},
footnote: {
'font-size': '12px',
}
}
};

View File

@ -0,0 +1,116 @@
let lyricTheme = {
BASE: {
'text-align': 'left',
'color': '#3f3f3f',
'line-height': '1.5'
},
BASE_BLOCK: {
'margin': '20px 10px'
},
// block element
block: {
h1: {
'font-size': '140%',
'text-align': 'center',
'font-weight': 'normal',
'margin': '80px 10px 40px 10px'
},
h2: {
'font-size': '140%',
'text-align': 'center',
'font-weight': 'normal',
'margin': '80px 10px 40px 10px'
},
h3: {
'font-weight': 'bold',
'font-size': '120%',
'margin': '40px 10px 20px 10px'
},
h4: {
'font-weight': 'bold',
'font-size': '100%',
'margin': '20px 10px 10px 10px'
},
p: {
'margin': '10px 10px',
'line-height': '1.6'
},
blockquote: {
'color': 'rgb(91, 91, 91)',
'padding': '1px 0 1px 10px',
'background': 'rgba(158, 158, 158, 0.1)',
'border-left': '3px solid rgb(158,158,158)',
},
code: {
'font-size': '80%',
'overflow': 'auto',
'color': '#333',
'background': 'rgb(247, 247, 247)',
'border-radius': '2px',
'padding': '10px',
'line-height': '1.3',
'border': '1px solid rgb(236,236,236)',
'margin': '20px 0',
},
image: {
'border-radius': '4px',
'display': 'block',
'margin': '20px auto',
'width': '100%',
},
image_org: {
'border-radius': '4px',
'display': 'block',
},
ol: {
'margin-left': '0',
'padding-left': '20px'
},
ul: {
'margin-left': '0',
'padding-left': '20px',
'list-style': 'circle',
},
footnotes: {
'margin': '10px 10px',
'font-size': '14px'
}
},
inline: {
// inline element
listitem: {
'text-indent': '-20px',
'display': 'block',
'margin': '10px 10px',
},
codespan: {
'font-size': '90%',
// 'font-family': FONT_FAMILY_MONO,
'color': '#ff3502',
'background': '#f8f5ec',
'padding': '3px 5px',
'border-radius': '2px',
},
link: {
'color': '#ff3502'
},
strong: {
'color': '#ff3502'
},
table: {
'border-collapse': 'collapse',
'margin': '20px 0',
},
thead: {
'background': 'rgba(0,0,0,0.05)',
},
td: {
'font-size': '80%',
'border': '1px solid #dfdfdf',
'padding': '4px 8px',
},
footnote: {
'font-size': '12px',
}
}
};

139
index.html Normal file
View File

@ -0,0 +1,139 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>微信公众号 Markdown 编辑器</title>
<link rel="shortcut icon" href="assets/images/favicon.png">
<link rel="apple-touch-icon-precomposed" href="assets/images/favicon.png">
<link rel="stylesheet" href="assets/css/loading.css">
<link rel="stylesheet" href="https://cdn.staticfile.org/element-ui/2.11.1/theme-chalk/index.css">
<link rel="stylesheet" href="https://cdn.staticfile.org/codemirror/5.48.4/codemirror.min.css">
<link rel="stylesheet" href="https://cdn.staticfile.org/codemirror/5.48.4/theme/base16-light.min.css">
<link rel="stylesheet" href="https://cdn.staticfile.org/codemirror/5.48.4/theme/duotone-light.min.css">
<link rel="stylesheet" href="https://cdn.staticfile.org/codemirror/5.48.4/theme/monokai.min.css">
<link rel="stylesheet" href="libs/prettify/color-themes/github-v2.min.css">
<link rel="stylesheet" href="assets/css/app.css">
</head>
<body>
<!--loading 界面-->
<div class="loading" id="loading">
<div class="loading-wrapper">
<div class="loading-text">Loading...</div>
<div class="loading-anim"></div>
</div>
</div>
<!--应用主体-->
<div id="app" class="container">
<el-container>
<el-header class="top">
<div><img src="assets/images/favicon.png" class="web-icon" alt="icon"> <span
class="web-title">公众号 Markdown 编辑器 </span></div>
<el-form size="mini" class="ctrl" :inline=true>
<el-form-item label="Editor Themes">
<el-select v-model="currentEditorTheme" size="mini" placeholder="选择字体" @change="editorThemeChanged">
<el-option v-for="editorTheme in editorThemes" :key="editorTheme.value" :label="editorTheme.label"
:value="editorTheme.value">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="Fonts">
<el-select v-model="currentFont" size="mini" placeholder="选择字体" @change="fontChanged">
<el-option v-for="font in builtinFonts" :style="{fontFamily: font.value}"
:key="font.value"
:label="font.label"
:value="font.value">
<span class="select-item-left">{{ font.label }}</span>
<span class="select-item-right">Abc</span>
</el-option>
</el-select>
</el-form-item>
<el-form-item label="Font Size">
<el-select v-model="currentSize" size="mini" placeholder="选择段落字体大小" @change="sizeChanged">
<el-option v-for="size in sizeOption"
:key="size.value"
:label="size.label"
:value="size.value">
<span class="select-item-left">{{ size.label }}</span>
<span class="select-item-right">{{ size.desc }}</span>
</el-option>
</el-select>
</el-form-item>
<el-form-item label="Themes">
<el-select v-model="currentTheme" size="mini" placeholder="选择主题样式" @change="themeChanged">
<el-option v-for="theme in themeOption" :key="theme.value" :label="theme.label" :value="theme.value">
<span class="select-item-left">{{ theme.label }}</span>
<span class="select-item-right">{{ theme.author }}</span>
</el-option>
</el-select>
</el-form-item>
</el-form>
<el-button class="about" @click="aboutDialogVisible = true">关于</el-button>
</el-header>
<el-main class="main-body">
<el-row :gutter="10" class="main-section">
<el-col :span="12">
<textarea
id="editor"
type="textarea"
placeholder="Your markdown here."
v-model="source">
</textarea>
</el-col>
<el-col :span="12" class="preview-wrapper" id="preview">
<section>
<div class="hint">全选复制或<a href="#" @click="copy" class="copy-button">点此复制</a>,然后在公众号编辑器粘贴</div>
<div class="preview" contenteditable="true">
<div id="output" v-html="output">
</div>
</div>
</section>
</el-col>
</el-row>
</el-main>
</el-container>
<el-dialog title="关于" :visible.sync="aboutDialogVisible" width="30%" center>
<div>
<p>一款可以将 Markdown 转换为微信公众号文章的在线编辑器,</p>
<p>这让你在公众号创作时,摆脱繁琐地排版样式,</p>
<p>可以把更多的时间专注于文章本身。</p>
<p>除了常规 Markdown 格式化,还增加了外链引用、注音样式等。</p>
</div>
<div style="text-align: center;">
<img src="https://static.zkqiang.cn/images/20191019181436.JPG-slim" style="max-width: 300px">
</div>
<span slot="footer" class="dialog-footer">
<el-button type="primary"
@click="openWindow('https://github.com/zkqiang/wechat-format')">查看 GitHub 仓库</el-button>
</span>
</el-dialog>
</div>
<script src="https://cdn.staticfile.org/vue/2.6.10/vue.min.js"></script>
<script src="https://cdn.staticfile.org/axios/0.19.0-beta.1/axios.min.js"></script>
<script src="https://cdn.staticfile.org/marked/0.7.0/marked.min.js"></script>
<script src="https://cdn.staticfile.org/codemirror/5.48.4/codemirror.min.js"></script>
<script src="https://cdn.staticfile.org/codemirror/5.48.4/mode/markdown/markdown.min.js"></script>
<script src="https://cdn.staticfile.org/prettify/r298/prettify.min.js"></script>
<script src="https://cdn.staticfile.org/element-ui/2.11.1/index.js"></script>
<script src="https://cdn.staticfile.org/jquery/3.4.1/jquery.min.js"></script>
<script src="libs/sync-scroll.js"></script>
<script src="libs/FuriganaMD.js"></script>
<script src="assets/scripts/themes/default.js"></script>
<script src="assets/scripts/themes/lyric.js"></script>
<script src="assets/scripts/themes/lupeng.js"></script>
<script src="assets/scripts/renderers/wx-renderer.js"></script>
<script src="assets/scripts/editor.js"></script>
<script src="assets/scripts/loading.js"></script>
</body>
</html>

241
libs/FuriganaMD.js Normal file
View File

@ -0,0 +1,241 @@
// 注音功能来自于
// https://github.com/amclees/furigana-markdown
// 详见上述文档
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global.FuriganaMD = factory());
}(this, (function () {
'use strict';
// This function escapes special characters for use in a regex constructor.
function escapeForRegex(string) {
return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
}
function emptyStringFilter(block) {
return block !== '';
}
const kanjiRange = '\\u4e00-\\u9faf';
const kanjiBlockRegex = new RegExp(`[${kanjiRange}]+`, 'g');
const nonKanjiBlockRegex = new RegExp(`[^${kanjiRange}]+`, 'g');
const kanaWithAnnotations = '\\u3041-\\u3095\\u3099-\\u309c\\u3081-\\u30fa\\u30fc';
const furiganaSeperators = '..。・';
const seperatorRegex = new RegExp(`[${furiganaSeperators}]`, 'g');
const singleKanjiRegex = new RegExp(`^[${kanjiRange}]$`);
function isKanji(character) {
return character.match(singleKanjiRegex);
}
const innerRegexString = '(?:[^\\u0000-\\u007F]|\\w)+';
let regexList = [];
let previousFuriganaForms = '';
function updateRegexList(furiganaForms) {
previousFuriganaForms = furiganaForms;
let formArray = furiganaForms.split('|');
if (formArray.length === 0) {
formArray = ['[]:^:()'];
}
regexList = formArray.map(form => {
let furiganaComponents = form.split(':');
if (furiganaComponents.length !== 3) {
furiganaComponents = ['[]', '^', '()'];
}
const mainBrackets = furiganaComponents[0];
const seperator = furiganaComponents[1];
const furiganaBrackets = furiganaComponents[2];
return new RegExp(
escapeForRegex(mainBrackets[0]) +
'(' + innerRegexString + ')' +
escapeForRegex(mainBrackets[1]) +
escapeForRegex(seperator) +
escapeForRegex(furiganaBrackets[0]) +
'(' + innerRegexString + ')' +
escapeForRegex(furiganaBrackets[1]),
'g'
);
});
}
let autoRegexList = [];
let previousAutoBracketSets = '';
function updateAutoRegexList(autoBracketSets) {
previousAutoBracketSets = autoBracketSets;
autoRegexList = autoBracketSets.split('|').map(brackets => {
/*
Sample built regex:
/(^|[^\u4e00-\u9faf]|)([\u4e00-\u9faf]+)([\u3041-\u3095\u3099-\u309c\u3081-\u30fa\u30fc]*)【((?:[^【】\u4e00-\u9faf]|w)+)】/g
*/
return new RegExp(
`(^|[^${kanjiRange}]|)` +
`([${kanjiRange}]+)` +
`([${kanaWithAnnotations}]*)` +
escapeForRegex(brackets[0]) +
`((?:[^${escapeForRegex(brackets)}\\u0000-\\u007F]|\\w|[${furiganaSeperators}])+)` +
escapeForRegex(brackets[1]),
'g'
);
});
}
let replacementTemplate = '';
let replacementBrackets = '';
function updateReplacementTemplate(furiganaFallbackBrackets) {
if (furiganaFallbackBrackets.length !== 2) {
furiganaFallbackBrackets = '【】';
}
replacementBrackets = furiganaFallbackBrackets;
replacementTemplate = `<ruby>$1<rp>${furiganaFallbackBrackets[0]}</rp><rt style="line-height:1;font-size:10px;">$2</rt><rp>${furiganaFallbackBrackets[1]}</rp></ruby>`;
}
updateReplacementTemplate('【】');
function addFurigana(text, options) {
if (options.furiganaForms !== previousFuriganaForms) {
updateRegexList(options.furiganaForms);
}
if (options.furiganaFallbackBrackets !== replacementBrackets) {
updateReplacementTemplate(options.furiganaFallbackBrackets);
}
regexList.forEach(regex => {
text = text.replace(regex, (match, wordText, furiganaText, offset, mainText) => {
if (match.indexOf('\\') === -1 && mainText[offset - 1] !== '\\') {
if ((!options.furiganaPatternMatching) || wordText.search(kanjiBlockRegex) === -1 || wordText[0].search(kanjiBlockRegex) === -1) {
return replacementTemplate.replace('$1', wordText).replace('$2', furiganaText);
} else {
let originalFuriganaText = (' ' + furiganaText).slice(1);
let nonKanji = wordText.split(kanjiBlockRegex).filter(emptyStringFilter);
let kanji = wordText.split(nonKanjiBlockRegex).filter(emptyStringFilter);
let replacementText = '';
let lastUsedKanjiIndex = 0;
if (nonKanji.length === 0) {
return replacementTemplate.replace('$1', wordText).replace('$2', furiganaText);
}
nonKanji.forEach((currentNonKanji, index) => {
if (furiganaText === undefined) {
if (index < kanji.length) {
replacementText += kanji[index];
}
replacementText += currentNonKanji;
return;
}
let splitFurigana = furiganaText.split(new RegExp(escapeForRegex(currentNonKanji) + '(.*)')).filter(emptyStringFilter);
lastUsedKanjiIndex = index;
replacementText += replacementTemplate.replace('$1', kanji[index]).replace('$2', splitFurigana[0]);
replacementText += currentNonKanji;
furiganaText = splitFurigana[1];
});
if (furiganaText !== undefined && lastUsedKanjiIndex + 1 < kanji.length) {
replacementText += replacementTemplate.replace('$1', kanji[lastUsedKanjiIndex + 1]).replace('$2', furiganaText);
} else if (furiganaText !== undefined) {
return replacementTemplate.replace('$1', wordText).replace('$2', originalFuriganaText);
} else if (lastUsedKanjiIndex + 1 < kanji.length) {
replacementText += kanji[lastUsedKanjiIndex + 1];
}
return replacementText;
}
} else {
return match;
}
});
});
if (!options.furiganaStrictMode) {
if (options.furiganaAutoBracketSets !== previousAutoBracketSets) {
updateAutoRegexList(options.furiganaAutoBracketSets);
}
autoRegexList.forEach(regex => {
text = text.replace(regex, (match, preWordTerminator, wordKanji, wordKanaSuffix, furiganaText, offset, mainText) => {
if (match.indexOf('\\') === -1) {
if (options.furiganaPatternMatching) {
let rubies = [];
let furigana = furiganaText;
let stem = (' ' + wordKanaSuffix).slice(1);
for (let i = furiganaText.length - 1; i >= 0; i--) {
if (wordKanaSuffix.length === 0) {
furigana = furiganaText.substring(0, i + 1);
break;
}
if (furiganaText[i] !== wordKanaSuffix.slice(-1)) {
furigana = furiganaText.substring(0, i + 1);
break;
}
wordKanaSuffix = wordKanaSuffix.slice(0, -1);
}
if (furiganaSeperators.split('').reduce(
(noSeperator, seperator) => {
return noSeperator && (furigana.indexOf(seperator) === -1);
},
true
)) {
rubies = [replacementTemplate.replace('$1', wordKanji).replace('$2', furigana)];
} else {
let kanaParts = furigana.split(seperatorRegex);
let kanji = wordKanji.split('');
if (kanaParts.length === 0 || kanaParts.length > kanji.length) {
rubies = [replacementTemplate.replace('$1', wordKanji).replace('$2', furigana)];
} else {
for (let i = 0; i < kanaParts.length - 1; i++) {
if (kanji.length === 0) {
break;
}
rubies.push(replacementTemplate.replace('$1', kanji.shift()).replace('$2', kanaParts[i]));
}
let lastKanaPart = kanaParts.pop();
rubies.push(replacementTemplate.replace('$1', kanji.join('')).replace('$2', lastKanaPart));
}
}
return preWordTerminator + rubies.join('') + stem;
} else {
return preWordTerminator + replacementTemplate.replace('$1', wordKanji).replace('$2', furiganaText) + wordKanaSuffix;
}
} else {
return match;
}
});
});
}
return text;
}
function handleEscapedSpecialBrackets(text) {
// By default 【 and 】 cannot be escaped in markdown, this will remove backslashes from in front of them to give that effect.
return text.replace(/\\([【】])/g, '$1');
}
let FuriganaMD = {};
FuriganaMD.register = function (renderer) {
renderer.text = function (text) {
let options = {
furigana: true,
furiganaForms: "()::{}",
furiganaFallbackBrackets: "{}",
furiganaStrictMode: false,
furiganaAutoBracketSets: "{}",
furiganaPatternMatching: true,
};
// console.log('override text render',text);
// console.log('after add',addFurigana(text, options));
return handleEscapedSpecialBrackets(addFurigana(text, options));
};
};
return FuriganaMD;
})));

View File

@ -0,0 +1,2 @@
/*! Color themes for Google Code Prettify | MIT License | github.com/jmblog/color-themes-for-google-code-prettify */
.prettyprint{font-family:Menlo,Bitstream Vera Sans Mono,DejaVu Sans Mono,Monaco,Consolas,monospace;border:0!important}.pln{color:#333}ol.linenums{margin-top:0;margin-bottom:0;color:#ccc}li.L0,li.L1,li.L2,li.L3,li.L4,li.L5,li.L6,li.L7,li.L8,li.L9{padding-left:1em;background-color:#fafbfc;list-style-type:decimal}@media screen{.str{color:#183691}.kwd{color:#a71d5d}.com{color:#969896}.typ{color:#0086b3}.lit{color:#0086b3}.pun{color:#333}.opn{color:#333}.clo{color:#333}.tag{color:navy}.atn{color:#795da3}.atv{color:#183691}.dec{color:#333}.var{color:teal}.fun{color:#900}}

View File

@ -0,0 +1,2 @@
/*! Color themes for Google Code Prettify | MIT License | github.com/jmblog/color-themes-for-google-code-prettify */
.prettyprint{background:#2d2d2d!important;font-family:Menlo,Bitstream Vera Sans Mono,DejaVu Sans Mono,Monaco,Consolas,monospace;border:0!important}.pln{color:#ccc}ol.linenums{margin-top:0;margin-bottom:0;color:#999}li.L0,li.L1,li.L2,li.L3,li.L4,li.L5,li.L6,li.L7,li.L8,li.L9{padding-left:1em;background-color:#2d2d2d;list-style-type:decimal}@media screen{.str{color:#9c9}.kwd{color:#c9c}.com{color:#999}.typ{color:#69c}.lit{color:#f99157}.pun{color:#ccc}.opn{color:#ccc}.clo{color:#ccc}.tag{color:#f2777a}.atn{color:#f99157}.atv{color:#6cc}.dec{color:#f99157}.var{color:#f2777a}.fun{color:#69c}}

View File

@ -0,0 +1,2 @@
/*! Color themes for Google Code Prettify | MIT License | github.com/jmblog/color-themes-for-google-code-prettify */
.prettyprint{background:#1d1f21!important;font-family:Menlo,Bitstream Vera Sans Mono,DejaVu Sans Mono,Monaco,Consolas,monospace;border:0!important}.pln{color:#c5c8c6}ol.linenums{margin-top:0;margin-bottom:0;color:#969896}li.L0,li.L1,li.L2,li.L3,li.L4,li.L5,li.L6,li.L7,li.L8,li.L9{padding-left:1em;background-color:#1d1f21;list-style-type:decimal}@media screen{.str{color:#b5bd68}.kwd{color:#b294bb}.com{color:#969896}.typ{color:#81a2be}.lit{color:#de935f}.pun{color:#c5c8c6}.opn{color:#c5c8c6}.clo{color:#c5c8c6}.tag{color:#c66}.atn{color:#de935f}.atv{color:#8abeb7}.dec{color:#de935f}.var{color:#c66}.fun{color:#81a2be}}

View File

@ -0,0 +1,2 @@
/*! Color themes for Google Code Prettify | MIT License | github.com/jmblog/color-themes-for-google-code-prettify */
.prettyprint{background:#f6f8fa!important;font-family:Menlo,Bitstream Vera Sans Mono,DejaVu Sans Mono,Monaco,Consolas,monospace;border:0!important}.pln{color:#4d4d4c}ol.linenums{margin-top:0;margin-bottom:0;color:#8e908c}li.L0,li.L1,li.L2,li.L3,li.L4,li.L5,li.L6,li.L7,li.L8,li.L9{padding-left:1em;background-color:#f6f8fa;list-style-type:decimal}@media screen{.str{color:#718c00}.kwd{color:#8959a8}.com{color:#8e908c}.typ{color:#4271ae}.lit{color:#f5871f}.pun{color:#4d4d4c}.opn{color:#4d4d4c}.clo{color:#4d4d4c}.tag{color:#c82829}.atn{color:#f5871f}.atv{color:#3e999f}.dec{color:#f5871f}.var{color:#c82829}.fun{color:#4271ae}}

27
libs/sync-scroll.js Normal file
View File

@ -0,0 +1,27 @@
// 左右栏同步滚动
$(document).ready(function () {
let timeout;
$('div.CodeMirror-scroll, #preview').on("scroll", function callback() {
clearTimeout(timeout);
let source = $(this),
target = $(source.is("#preview") ? 'div.CodeMirror-scroll' : '#preview');
target.off("scroll");
let source0 = source[0];
let target0 = target[0];
let percentage = source0.scrollTop / (source0.scrollHeight - source0.offsetHeight);
let height = percentage * (target0.scrollHeight - target0.offsetHeight);
target0.scrollTo(0, height);
timeout = setTimeout(function () {
target.on("scroll", callback);
}, 100);
});
});