Repository: zkqiang/wechat-mdeditor Branch: master Commit: 7c9f5de5f498 Files: 14 Total size: 44.2 KB Directory structure: gitextract_hh_tb0ei/ ├── .gitignore ├── README.md └── src/ ├── assets/ │ ├── css/ │ │ ├── app.css │ │ └── loading.css │ ├── default-content.md │ └── scripts/ │ ├── editor.js │ ├── loading.js │ ├── renderers/ │ │ └── wx-renderer.js │ └── themes/ │ ├── default.js │ ├── lupeng.js │ └── lyric.js ├── index.html └── libs/ ├── FuriganaMD.js └── sync-scroll.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Created by .ignore support plugin (hsz.mobi) ### Java template # Compiled class file *.class # Log file *.log # BlueJ files *.ctxt # Mobile Tools for Java (J2ME) .mtj.tmp/ # Package Files # *.jar *.war *.nar *.ear *.zip *.tar.gz *.rar # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* ### JetBrains template .idea ### Android template # Built application files *.apk *.ap_ *.aab # Files for the ART/Dalvik VM *.dex # Java class files *.class # Generated files bin/ gen/ out/ release/ # Gradle files .gradle/ build/ # Local configuration file (sdk path, etc) local.properties # Proguard folder generated by Eclipse proguard/ # Log Files *.log # Android Studio Navigation editor temp files .navigation/ # Android Studio captures folder captures/ # IntelliJ *.iml .idea/workspace.xml .idea/tasks.xml .idea/gradle.xml .idea/assetWizardSettings.xml .idea/dictionaries .idea/libraries # Android Studio 3 in .gitignore file. .idea/caches .idea/modules.xml # Comment next line if keeping position of elements in Navigation Editor is relevant for you .idea/navEditor.xml # Keystore files # Uncomment the following lines if you do not want to check your keystore files in. #*.jks #*.keystore # External native build folder generated in Android Studio 2.2 and later .externalNativeBuild # Google Services (e.g. APIs or Firebase) # google-services.json # Freeline freeline.py freeline/ freeline_project_description.json # fastlane fastlane/report.xml fastlane/Preview.html fastlane/screenshots fastlane/test_output fastlane/readme.md # Version control vcs.xml # lint lint/intermediates/ lint/generated/ lint/outputs/ lint/tmp/ # lint/reports/ ### macOS template # General .DS_Store .AppleDouble .LSOverride # Icon must end with two \r Icon # Thumbnails ._* # Files that might appear in the root of a volume .DocumentRevisions-V100 .fseventsd .Spotlight-V100 .TemporaryItems .Trashes .VolumeIcon.icns .com.apple.timemachine.donotpresent # Directories potentially created on remote AFP share .AppleDB .AppleDesktop Network Trash Folder Temporary Items .apdisk .idea/markdown-navigator/ .idea/misc.xml ================================================ FILE: README.md ================================================ # 微信公众号 Markdown 编辑器 这款编辑器可以将 Markdown 转换成微信公众号编辑器的样式,可以直接复制到公众号后台。 这让你在公众号创作时,把更多的时间专注于文章本身,而不是繁琐地调整文章样式。 [在线使用](http://prod.zkqiang.cn/wxeditor) ## 功能 - 支持序号列表和圆点列表,解决了样式会被重置的问题 - 外链会自动转换为参考文献索引,并且附在文章末尾 - 支持多种字体和样式 - 支持日语注音假名、汉语拼音样式 - 支持不同于微信的代码配色方案 ## 关于 本仓库 Fork 自 [yricat/wechat-format](https://github.com/lyricat/wechat-format),并根据自用需求进行修改开发。 感谢他的创意和贡献! ================================================ FILE: src/assets/css/app.css ================================================ * { 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; } ================================================ FILE: src/assets/css/loading.css ================================================ .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) } } ================================================ FILE: src/assets/default-content.md ================================================ # 公众号 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 "原项目代码库") ================================================ FILE: src/assets/scripts/editor.js ================================================ 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() }) } }); ================================================ FILE: src/assets/scripts/loading.js ================================================ // 加载完成隐藏 loading 界面 window.onload = () => { $('#loading').hide(); }; ================================================ FILE: src/assets/scripts/renderers/wx-renderer.js ================================================ 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 } }; ================================================ FILE: src/assets/scripts/themes/default.js ================================================ 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' } } }; ================================================ FILE: src/assets/scripts/themes/lupeng.js ================================================ 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', } } }; ================================================ FILE: src/assets/scripts/themes/lyric.js ================================================ 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', } } }; ================================================ FILE: src/index.html ================================================ 微信公众号 Markdown 编辑器
    Loading...
    icon 公众号 Markdown 编辑器
    {{ font.label }} Abc {{ size.label }} {{ size.desc }} {{ theme.label }} {{ theme.author }} 关于
    全选复制或点此复制,然后在公众号编辑器粘贴

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

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

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

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

    查看 GitHub 仓库
    ================================================ FILE: src/libs/FuriganaMD.js ================================================ // 注音功能来自于 // 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; }))); ================================================ FILE: src/libs/sync-scroll.js ================================================ // 左右栏同步滚动 $(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); }); });