diff --git a/assets/css/app.css b/assets/css/app.css new file mode 100644 index 0000000..7769cbc --- /dev/null +++ b/assets/css/app.css @@ -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; +} diff --git a/assets/css/loading.css b/assets/css/loading.css new file mode 100644 index 0000000..9533f45 --- /dev/null +++ b/assets/css/loading.css @@ -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) + } +} diff --git a/assets/default-content.md b/assets/default-content.md new file mode 100644 index 0000000..b610d2d --- /dev/null +++ b/assets/default-content.md @@ -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 + +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 "原项目代码库") diff --git a/assets/images/favicon.png b/assets/images/favicon.png new file mode 100644 index 0000000..11f6d64 Binary files /dev/null and b/assets/images/favicon.png differ diff --git a/assets/scripts/editor.js b/assets/scripts/editor.js new file mode 100644 index 0000000..2d8bd25 --- /dev/null +++ b/assets/scripts/editor.js @@ -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() + }) + } +}); diff --git a/assets/scripts/loading.js b/assets/scripts/loading.js new file mode 100644 index 0000000..78f321f --- /dev/null +++ b/assets/scripts/loading.js @@ -0,0 +1,4 @@ +// 加载完成隐藏 loading 界面 +window.onload = () => { + $('#loading').hide(); +}; diff --git a/assets/scripts/renderers/wx-renderer.js b/assets/scripts/renderers/wx-renderer.js new file mode 100644 index 0000000..5c550d7 --- /dev/null +++ b/assets/scripts/renderers/wx-renderer.js @@ -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 `[${ x[0] }]: ${ x[1] }
` + } + return `[${ x[0] }] ${ x[1] }: ${ x[2] }
` + }); + return `

References

${ footnoteArray.join('\n') }

` + }; + + this.buildAddition = function () { + return '' + }; + + 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 `

${ text }

`; + case 2: + return `

${ text }

`; + case 3: + return `

${ text }

`; + default: + return `

${ text }

`; + } + }; + renderer.paragraph = function (text) { + return `

${ text }

` + }; + renderer.blockquote = function (text) { + text = text.replace(//, `

`); + return `

${ text }
` + }; + renderer.code = function (text, infoString) { + text = text.replace(//g, ">"); + + let lines = text.split('\n'); + let codeLines = []; + let numbers = []; + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + codeLines.push(`${ (line || '
') }
`); + numbers.push('
  • ') + } + let lang = infoString || ''; + return `
    ` + + `
      ${ numbers.join('') }
    ` + + `
    `
    +        + codeLines.join('')
    +        + `
    ` + }; + renderer.codespan = function (text, infoString) { + return `${ text }` + }; + renderer.listitem = function (text) { + return `<%s/>${ text }`; + }; + renderer.list = function (text, ordered, start) { + text = text.replace(/<\/*p.*?>/g, ''); + let segments = text.split(`<%s/>`); + if (!ordered) { + text = segments.join('•'); + return `

    ${ text }

    `; + } + text = segments[0]; + for (let i = 1; i < segments.length; i++) { + text = text + i + '.' + segments[i]; + } + return `

    ${ text }

    `; + }; + renderer.image = function (href, title, text) { + let subText = ''; + if (text) { + subText = `
    ${ text }
    ` + } + let figureStyles = getStyles('figure'); + let imgStyles = getStyles(ENV_STRETCH_IMAGE ? 'image' : 'image_org'); + return `
    ${ text }${ subText }
    ` + }; + renderer.link = function (href, title, text) { + if (href.indexOf('https://mp.weixin.qq.com') === 0) { + return `${ text }`; + } else if (href === text) { + return text; + } else { + if (ENV_USE_REFERENCES) { + let ref = addFootnote(title || text, href); + return `${ text }[${ ref }]`; + } else { + return `${ text }`; + } + } + }; + renderer.strong = function (text) { + return `${ text }`; + }; + renderer.em = function (text) { + return `

    ${ text }

    ` + }; + renderer.table = function (header, body) { + return `${ header }${ body }
    `; + }; + renderer.tablecell = function (text, flags) { + return `${ text }`; + }; + renderer.hr = function () { + return `
    `; + }; + return renderer + } +}; diff --git a/assets/scripts/themes/default.js b/assets/scripts/themes/default.js new file mode 100644 index 0000000..df9696d --- /dev/null +++ b/assets/scripts/themes/default.js @@ -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' + } + } +}; diff --git a/assets/scripts/themes/lupeng.js b/assets/scripts/themes/lupeng.js new file mode 100644 index 0000000..12b65b6 --- /dev/null +++ b/assets/scripts/themes/lupeng.js @@ -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', + } + } +}; diff --git a/assets/scripts/themes/lyric.js b/assets/scripts/themes/lyric.js new file mode 100644 index 0000000..fa72434 --- /dev/null +++ b/assets/scripts/themes/lyric.js @@ -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', + } + } +}; diff --git a/index.html b/index.html new file mode 100644 index 0000000..1828a3e --- /dev/null +++ b/index.html @@ -0,0 +1,139 @@ + + + + + + + 微信公众号 Markdown 编辑器 + + + + + + + + + + + + + + + + + +
    +
    +
    Loading...
    +
    +
    +
    + + +
    + + +
    icon 公众号 Markdown 编辑器
    + + + + + + + + + + + {{ font.label }} + Abc + + + + + + + {{ size.label }} + {{ size.desc }} + + + + + + + {{ theme.label }} + {{ theme.author }} + + + + + 关于 +
    + + + + + + +
    +
    全选复制或点此复制,然后在公众号编辑器粘贴
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + +
    +

    一款可以将 Markdown 转换为微信公众号文章的在线编辑器,

    +

    这让你在公众号创作时,摆脱繁琐地排版样式,

    +

    可以把更多的时间专注于文章本身。

    +

    除了常规 Markdown 格式化,还增加了外链引用、注音样式等。

    +
    +
    + +
    + + 查看 GitHub 仓库 + +
    +
    + + + + + + + + + + + + + + + + + + + + + + diff --git a/libs/FuriganaMD.js b/libs/FuriganaMD.js new file mode 100644 index 0000000..c0dafd9 --- /dev/null +++ b/libs/FuriganaMD.js @@ -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 = `$1${furiganaFallbackBrackets[0]}$2${furiganaFallbackBrackets[1]}`; + } + + 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; + +}))); diff --git a/libs/prettify/color-themes/github-v2.min.css b/libs/prettify/color-themes/github-v2.min.css new file mode 100644 index 0000000..2415a70 --- /dev/null +++ b/libs/prettify/color-themes/github-v2.min.css @@ -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}} \ No newline at end of file diff --git a/libs/prettify/color-themes/tomorrow-night-eighties.min.css b/libs/prettify/color-themes/tomorrow-night-eighties.min.css new file mode 100644 index 0000000..796fec5 --- /dev/null +++ b/libs/prettify/color-themes/tomorrow-night-eighties.min.css @@ -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}} \ No newline at end of file diff --git a/libs/prettify/color-themes/tomorrow-night.min.css b/libs/prettify/color-themes/tomorrow-night.min.css new file mode 100644 index 0000000..70fdab7 --- /dev/null +++ b/libs/prettify/color-themes/tomorrow-night.min.css @@ -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}} \ No newline at end of file diff --git a/libs/prettify/color-themes/tomorrow.min.css b/libs/prettify/color-themes/tomorrow.min.css new file mode 100644 index 0000000..84f4019 --- /dev/null +++ b/libs/prettify/color-themes/tomorrow.min.css @@ -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}} \ No newline at end of file diff --git a/libs/sync-scroll.js b/libs/sync-scroll.js new file mode 100644 index 0000000..d1b4b40 --- /dev/null +++ b/libs/sync-scroll.js @@ -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); + }); + +});