This commit is contained in:
JimQing 2020-05-02 19:36:53 +08:00
parent e6101b70df
commit 90ba7e0578
75 changed files with 0 additions and 12960 deletions

View File

@ -1,2 +0,0 @@
> 1%
last 2 versions

View File

@ -1,5 +0,0 @@
[*.{js,jsx,ts,tsx,vue}]
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
insert_final_newline = true

View File

@ -1,2 +0,0 @@
*.js
*.vue

View File

@ -1,30 +0,0 @@
module.exports = {
root: true,
env: {
node: true
},
'extends': [
'plugin:vue/essential',
'@vue/standard'
],
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'camelcase': 'off',
'eqeqeq': 'off'
},
parserOptions: {
parser: 'babel-eslint'
},
overrides: [
{
files: [
'**/__tests__/*.{j,t}s?(x)',
'**/tests/unit/**/*.spec.{j,t}s?(x)'
],
env: {
jest: true
}
}
]
}

View File

@ -1,38 +0,0 @@
name: Sync
on:
push:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [8.x, 10.x, 12.x]
steps:
- uses: actions/checkout@v1
- name: Use Node.js 10
uses: actions/setup-node@v1
with:
node-version: 10
- name: npm install, build
uses: yanglbme/gitee-pages-action@master
env:
BRANCH: master # 存放产物的分支名称
FOLDER: dist # 存放build后产物的目录
BUILD_SCRIPT: npm install && npm run build # 执行的命令
- name: Sync to Gitee
uses: wearerequired/git-mirror-action@master
env:
SSH_PRIVATE_KEY: ${{ secrets.GITEE_RSA_PRIVATE_KEY }}
with:
source-repo: "git@github.com:doocs/md.git"
destination-repo: "git@gitee.com:Doocs/md.git"
- name: Build Gitee Pages
uses: yanglbme/gitee-pages-action@master
with:
gitee-username: yanglbme
gitee-password: ${{ secrets.GITEE_PASSWORD }}
gitee-repo: doocs/md

44
.gitignore vendored
View File

@ -1,44 +0,0 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
.vscode
npm-debug.log*
yarn-debug.log*
yarn-error.log*
dist
package-lock.json
lib
node_modules
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

13
LICENSE
View File

@ -1,13 +0,0 @@
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
Copyright (C) 2012 Romain Lespinasse <romain.lespinasse@gmail.com>
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.

131
README.md
View File

@ -1,131 +0,0 @@
<p align="center">
<a href="https://github.com/doocs/md">
<img src="https://imgkr.cn-bj.ufileos.com/f3accc83-b854-4e99-afb5-8a6465e1d84f.png" alt="">
</a>
</p>
<h1 align="center">微信 Markdown 编辑器</h1>
<div align="center">
[![github](https://badgen.net/badge/⭐/GitHub/cyan)](https://github.com/doocs/md) [![gitee](https://badgen.net/badge/⭐/Gitee/cyan)](https://gitee.com/doocs/md) [![PRs Welcome](https://badgen.net/badge/PRs/welcome/green)](../../pulls) [![users](https://badgen.net/badge/who's/using/green)](../../issues) [![license](https://badgen.net/github/license/doocs/md)](./LICENSE) [![release](https://img.shields.io/github/v/release/doocs/md.svg)](../../releases)
</div>
## 项目介绍
> 本项目基于 [wechat-format](https://github.com/lyricat/wechat-format) 进行二次开发,感谢 [lyricat](https://github.com/lyricat) 的创意和贡献!
Markdown 文档自动即时渲染为微信图文,让你不再为微信文章排版而发愁!只要你会基本的 Markdown 语法,就能做出一篇样式简洁而又美观大方的微信图文。
## 在线编辑器地址
- Netlify: https://mdmd.netlify.app
- Gitee Pageshttps://doocs.gitee.io/md
- GitHub Pageshttps://doocs.github.io/md
注:推荐使用 Chrome 浏览器,效果最佳。另外,对于国内(中国)的朋友,访问 [Gitee Pages](https://doocs.gitee.io/md) 速度会相对快一些。
## 为何二次开发
现有的开源微信 Markdown 编辑器,样式繁杂,也不符合我个人的审美需求。在我使用它们进行文章排版的时候,经常还要自己做一些改动,费时费力,因此动手做了二次开发。
欢迎各位朋友随时提交 PR让这款微信 Markdown 编辑器变得更好!如果你有新的想法,也欢迎在 Issues 区反馈。
注:目前在非 master 分支上对项目进行重构,更多新特性,敬请期待!
- [Vue 分支](https://github.com/doocs/md/tree/m_create_vue)
- [React 分支](https://github.com/doocs/md/tree/chore-webpack)
## 功能特性
- [x] 支持 Markdown 所有基础语法
- [x] 支持单独进行字体、字号设置
- [x] 支持色盘取色,并一键替换颜色
- [x] 支持链接微信图文,外链自动转为文末索引
- [x] 支持一键复制并粘贴到公众号后台
- [x] 支持多图上传并将 URL 插入编辑器光标定位处
- [x] 支持一键下载 Markdown 文档到本地
- [x] 支持自定义 CSS 样式并实时渲染
- [x] 支持一键恢复至默认内容及样式
- [x] 支持打开或关闭引用链接的选项
<!--
![select-and-change-color-theme](./assets/images/doocs-md-select-and-change-color-theme.gif)
![copy-and-paste](./assets/images/doocs-md-copy-and-paste.gif)
![custom](./assets/images/doocs-md-custom-css.gif)
![doocs-md-upload-image](./assets/images/doocs-md-upload-image.gif)
-->
![select-and-change-color-theme](https://imgkr.cn-bj.ufileos.com/32c05c23-6309-491f-bd0d-f22a62c944b4.gif)
![copy-and-paste](https://imgkr.cn-bj.ufileos.com/31f16c2f-480c-4ea3-bb89-89b6e14d18e5.gif)
![custom](https://imgkr.cn-bj.ufileos.com/bbf0a0b6-b817-4626-bf79-4e18df318681.gif)
![doocs-md-upload-image](https://imgkr.cn-bj.ufileos.com/97db3cd6-bddc-4eff-8635-472631b0a642.gif)
## 谁在使用
<table>
<tr>
<td align="center" style="width: 80px;">
<a href="https://mp.weixin.qq.com/s/RNKDCK2KoyeuMeEs6GUrow">
<img src="https://imgkr.cn-bj.ufileos.com/29fbfc6e-b1f2-4995-982f-74f993256626.png" style="width: 50px;"><br>
<sub>Doocs开源社区</sub>
</a>
</td>
<td align="center" style="width: 80px;">
<a href="https://mp.weixin.qq.com/s/FpGIX9viQR6Z9iSCEPH86g">
<img src="https://imgkr.cn-bj.ufileos.com/2631fe1d-0521-4f51-abb9-5250f4dda268.jpg" style="width: 50px;"><br>
<sub>掘墓人的小铲子</sub>
</a>
</td>
<td align="center" style="width: 80px;">
<a href="https://mp.weixin.qq.com/s/yB3ZH3jmcF_LbzuKmnR9BQ">
<img src="https://imgkr.cn-bj.ufileos.com/4b4b10a4-1146-4056-8799-9f8c1a3e5e9e.png" style="width: 50px;"><br>
<sub>全网重点</sub>
</a>
</td>
<td align="center" style="width: 80px;">
<a href="https://mp.weixin.qq.com/s/oc5Z2t9ykbu_Dezjnw5mfQ">
<img src="https://imgkr.cn-bj.ufileos.com/1ddb47f6-4943-4aae-ad24-c75c22c758bf.png" style="width: 50px;"><br>
<sub>爱码士的内心独白</sub>
</a>
</td>
<td align="center" style="width: 80px;">
<a href="https://mp.weixin.qq.com/s/SFde8OsZ8FzNGMHwpmDtrg">
<img src="https://imgkr.cn-bj.ufileos.com/830333b7-74b2-4dbc-9384-b5cac63b1d17.jpg" style="width: 50px;"><br>
<sub>乐玩nodejs npm工具库</sub>
</a>
</td>
<td align="center" style="width: 80px;">
<a href="https://mp.weixin.qq.com/s/7UG24ZugfI5ZnhUpo8vfvQ">
<img src="https://imgkr.cn-bj.ufileos.com/95e553de-fd8f-4374-8a98-14809122e80e.jpg" style="width: 50px;"><br>
<sub>简静慢</sub>
</a>
</td>
</tr>
</table>
注:如果你使用了本 Markdown 编辑器进行文章排版,并且希望在本项目 README 中展示你的公众号,请到 [#5](https://github.com/doocs/md/issues/5) 留言。
## 示例文章
- [全网首发GPU 驱动自升级原理详解](https://mp.weixin.qq.com/s/7UG24ZugfI5ZnhUpo8vfvQ)
- [Quick Start - 天下武功,唯快不破!效率工具,老少皆宜](https://mp.weixin.qq.com/s/SFde8OsZ8FzNGMHwpmDtrg)
- [死磕JavaScript系列之原来你是对象(一)](https://mp.weixin.qq.com/s/oc5Z2t9ykbu_Dezjnw5mfQ)
- [一文多发神器--ArtiPub&OpenWrite](https://mp.weixin.qq.com/s/FpGIX9viQR6Z9iSCEPH86g)
- [免费且好用的图床,就你了,「图壳」!](https://mp.weixin.qq.com/s/0HhgHLo_tTRFZcC-CVjDbw)
- [GitHub 项目持续本地化,交给它来做,准没错!](https://mp.weixin.qq.com/s/KO4xHr4EI0YfjF0hiT3pbw)
- [阿里又一个 20k+ stars 开源项目诞生,恭喜 fastjson](https://mp.weixin.qq.com/s/RNKDCK2KoyeuMeEs6GUrow)
- [全球最大的成人网站 Pornhub 2019 年度报告新鲜出炉!](https://mp.weixin.qq.com/s/LY5kOzof1h3I0bw7tCkV1Q)
- [刷掉 90 % 候选人的海量数据面试题(附题解+方法总结)](https://mp.weixin.qq.com/s/rjGqxUvrEqJNlo09GrT1Dw)
- [GitHub 标星 11.5k 的一款开源工具,助你轻松查看 Git 历史](https://mp.weixin.qq.com/s/PK-ikENqF13Lmqy2pcMhYQ)
## 项目维护者
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<a href="https://opencollective.com/doocs-md/contributors.svg?width=890&button=true"><img src="https://opencollective.com/doocs-md/contributors.svg?width=890&button=false" /></a>
<!-- ALL-CONTRIBUTORS-LIST:END -->
## 项目许可证
[本项目没有任何限制Just Do What The F*ck You Want。](LICENSE)

View File

@ -1,5 +0,0 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

View File

@ -1,3 +0,0 @@
module.exports = {
preset: '@vue/cli-plugin-unit-jest'
}

View File

@ -1,41 +0,0 @@
{
"name": "vue-md",
"version": "0.1.0",
"private": true,
"homepage": "https://doocs.gitee.io/md",
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"test:unit": "vue-cli-service test:unit"
},
"dependencies": {
"axios": "^0.19.1",
"codemirror": "^5.50.2",
"core-js": "^3.4.4",
"element-ui": "^2.13.0",
"jquery": "^3.4.1",
"juice": "^6.0.0",
"markdown": "^0.5.0",
"marked": "^0.8.0",
"prettify": "^0.1.7",
"vue": "^2.6.10",
"vue-router": "^3.1.3",
"vuex": "^3.1.2"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^4.1.0",
"@vue/cli-plugin-eslint": "^4.1.0",
"@vue/cli-plugin-unit-jest": "^4.1.0",
"@vue/cli-service": "^4.1.0",
"@vue/eslint-config-standard": "^4.0.0",
"@vue/test-utils": "1.0.0-beta.29",
"babel-eslint": "^10.0.3",
"eslint": "^5.16.0",
"eslint-plugin-vue": "^5.0.0",
"less-loader": "^6.0.0",
"node-sass": "^4.12.0",
"sass-loader": "^8.0.0",
"vue-template-compiler": "^2.6.10"
}
}

View File

@ -1,233 +0,0 @@
* {
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;
}
.el-message__icon {
display: none
}
.container {
height: 100%;
display: flex;
flex-direction: column;
}
.top {
height: 60px;
padding: 10px 20px;
display: flex;
align-items: center;
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;
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;
box-sizing: border-box;
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 table tr:nth-child(even){
background: rgb(250, 250, 250);
}
*/
.select-item-left {
float: left;
}
.select-item-right {
float: right;
color: #8492a6;
font-size: 13px;
}
.CodeMirror {
height: 100% !important;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
font-size: 14px;
padding: 20px;
width: 100% !important;
font-family: 'PingFang SC', BlinkMacSystemFont, Roboto, 'Helvetica Neue', sans-serif;
}
/* ele ui */
.el-form-item {
margin-bottom: 0 !important;
}
.el-tooltip {
cursor: pointer;
}
/*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;
font-size: 14px;
margin: 10px 8px;
color: #333;
position: relative;
background-color: rgba(0, 0, 0, 0.03);
border: 1px solid #f0f0f0;
border-radius: 2px;
display: flex;
line-height: 24px;
}
.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;
}
::-webkit-scrollbar {
width: 6px;
height: 6px;
background-color: #FFF;
}
::-webkit-scrollbar-track {
border-radius: 6px;
background-color: rgba(200, 200, 200, 0.3);
}
::-webkit-scrollbar-thumb {
border-radius: 6px;
background-color: rgba(144, 146, 152, 0.5);
transition: background-color .3s;
}
::-webkit-scrollbar-thumb:hover {
background-color: rgba(144, 146, 152, 0.5);
}
.CodeMirror-vscrollbar:focus {
outline: none;
}
.CodeMirror-scroll, .preview-wrapper {
overflow: unset!important;
overflow-y: scroll!important;
}

View File

@ -1,43 +0,0 @@
.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(66, 185, 131, 0.9);
border-top-color: rgba(66, 185, 131, 0.9);
border-radius: 50%;
display: inline-block;
animation: rotate 600ms infinite linear;
}
@keyframes rotate {
to {
transform: rotate(1turn)
}
}

View File

@ -1,104 +0,0 @@
/*
Name: Base16 Default Light
Author: Chris Kempson (http://chriskempson.com)
CodeMirror template by Jan T. Sott (https://github.com/idleberg/base16-codemirror)
Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16)
*/
.cm-s-style-mirror.CodeMirror {
background: #f5f5f5;
color: #444;
font-size: 16px;
padding: 20px;
line-height: 25px;
}
.cm-s-style-mirror div.CodeMirror-selected {
background: #e0e0e0;
}
.cm-s-style-mirror .CodeMirror-line::selection,
.cm-s-style-mirror .CodeMirror-line > span::selection,
.cm-s-style-mirror .CodeMirror-line > span > span::selection {
background: #e0e0e0;
}
.cm-s-style-mirror .CodeMirror-line::-moz-selection,
.cm-s-style-mirror .CodeMirror-line > span::-moz-selection,
.cm-s-style-mirror .CodeMirror-line > span > span::-moz-selection {
background: #e0e0e0;
}
.cm-s-style-mirror .CodeMirror-gutters {
background: #f5f5f5;
border-right: 0px;
}
.cm-s-style-mirror .CodeMirror-guttermarker {
color: #ac4142;
}
.cm-s-style-mirror .CodeMirror-guttermarker-subtle {
color: #b0b0b0;
}
.cm-s-style-mirror .CodeMirror-linenumber {
color: #b0b0b0;
}
.cm-s-style-mirror .CodeMirror-cursor {
border-left: 1px solid #505050;
}
.cm-s-style-mirror span.cm-comment {
color:green;
}
.cm-s-style-mirror span.cm-atom {
color: #aa759f;
}
.cm-s-style-mirror span.cm-number {
color: #aa759f;
}
.cm-s-style-mirror span.cm-property,
.cm-s-style-mirror span.cm-attribute {
color: #90a959;
}
.cm-s-style-mirror span.cm-keyword {
color: #023a52;
}
.cm-s-style-mirror span.cm-string {
color: #e46918;
}
.cm-s-style-mirror span.cm-variable {
color: #90a959;
}
.cm-s-style-mirror span.cm-variable-2 {
color: #00695f;
}
.cm-s-style-mirror span.cm-variable-3 {
color: #2e6e8a;
}
.cm-s-style-mirror span.cm-def {
color: #d28445;
}
.cm-s-style-mirror span.cm-bracket {
color: #202020;
}
.cm-s-style-mirror span.cm-tag {
color:#000;
}
.cm-s-style-mirror span.cm-link {
color: #b26a00;
}
.cm-s-style-mirror span.cm-error {
/* background: #ac4142;
color: #f5f5f5; */
text-decoration: underline;
text-decoration-style: wavy;
text-decoration-color: #df8d8e;
}
.cm-s-style-mirror .CodeMirror-activeline-background {
background: #dddcdc;
}
.cm-s-style-mirror .CodeMirror-matchingbracket {
color: rgb(32,32,32) !important;
background-color: rgba(0,0,0,0.1) !important;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 852 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

View File

@ -1,99 +0,0 @@
const DEFAULT_CONTENT =
`# 示例文章Google 搜索的即时自动补全功能究竟是如何“工作”的?
> Google 搜索**自动补全功能**的强大相信不少朋友都能感受到它帮助我们更快地补全我们所要输入的搜索关键字那么它怎么知道我们要输入什么内容它又是如何工作的在这篇文章里我们一起来看看
## 使用自动补全
Google 搜索的自动补全功能可以在 Google 搜索应用的大多数位置使用包括 [Google](https://www.google.com/) 主页、适用于 IOS 和 Android 的 Google 应用,我们只需要在 Google 搜索框上开始键入关键字,就可以看到联想词了。
![](https://imgkr.cn-bj.ufileos.com/17ed83bf-e028-4db2-9503-5a3b4e64deee.gif)
在上图示例中我们可以看到输入关键字 \`juej\`Google 搜索会联想到“掘金”、“掘金小册”、“绝句”等等,好处就是,我们无须输入完整的关键字即可轻松完成针对这些 topics 的搜索。
谷歌搜索的自动补全功能对于使用移动设备的用户来说特别有用用户可以轻松在难以键入的小屏幕上完成搜索当然对于移动设备用户和台式机用户而言这都节省了大量的时间根据 Google 官方报告自动补全功能可以减少大约 25% 的打字累积起来预计每天可以节省 200 多年的打字时间是的每天
> 注意本文所提到的**联想词****预测**是同一个意思
## 基于预测而非建议
Google 官方将自动补全功能称之为预测而不是建议为什么呢其实是有充分理由的自动补全功能是为了**帮助用户完成他们打算进行的搜索**而不是建议用户要执行什么搜索
那么Google 是如何确定这些预测其实Google 会根据趋势搜索 [trends](https://trends.google.com/trends/?geo=US) 给到我们这些“预测”。简单来说,哪个热门、哪个搜索频率高,就更可能推给我们。当然,这也与我们当前所处的位置以及我们的搜索历史相关。
另外这些预测也会随着我们键入的关键字的变更而更改例如当我们把键入的关键字从 \`juej\` 更改为 \`juex\` 时,与“掘金”相关的预测会“消失”,同时,与“觉醒”、“决心”相关联的词会出现。
![](https://imgkr.cn-bj.ufileos.com/5b17dc99-606d-42c1-9f86-e09e88aaa822.gif)
## 为什么看不到某些联想词
如果我们在输入某个关键字时看不到联想词那么表明 Google 的算法可能检测到
- 这个关键字不是热门字词
- 搜索的字词太新了我们可能需要等待几天或几周才能看到联想词
- 这是一个侮辱性或敏感字词这个搜索字词违反了 Google 的相关政策更加详细的情况可以了解 [Google 搜索自动补全政策](https://support.google.com/websearch/answer/7368877)。
## 为什么会看到某些不当的联想词
Google 拥有专门设计的系统可以自动捕获不适当的预测结果而不显示出来然而Google 每天需要处理数十亿次搜索这意味着 Google 每天会显示数十亿甚至上百亿条预测再好的系统也可能存在缺陷不正确的预测也可能随时会出现
我们作为 Google 搜索的用户如果认定某条预测违反了相关的搜索自动补全政策可以进行举报反馈点击右下角**举报不当的联想查询**并勾选相关选项即可
![](https://imgkr.cn-bj.ufileos.com/6ca8185d-12c6-4550-bb4e-e49cfbf56db7.gif)
## 如何实现自动补全算法
目前Google 官方似乎并没有公开搜索自动补全的算法实现但是业界在这方面已经有了不少研究
一个好的自动补全器必须是快速的并且在用户键入下一个字符后立即更新联想词列表**自动补全器的核心是一个函数它接受输入的前缀并搜索以给定前缀开头的词汇或语句列表**通常来说只需要返回少量的数目即可
接下来我们先从一个简单且低效的实现开始并在此基础上逐步构建更高效的方法
### 词汇表实现
一个**简单粗暴的实现方式**顺序查找词汇表依次检查每个词汇看它是否以给定的前缀开头
但是此方法需要将前缀与每个词汇进行匹配检查若词汇量较少这种方式可能勉强行得通但是如果词汇量规模较大效率就太低了
一个**更好的实现方式是**让词汇按字典顺序排序借助二分搜索算法可以快速搜索有序词汇表中的前缀由于二分搜索的每一步都会将搜索的范围减半因此总的搜索时间与词汇表中单词数量的对数成正比即时间复杂度是 \`O(log N)\`。二分搜索的性能很好,但有没有更好的实现呢?当然有,往下看。
### 前缀树实现
通常来说许多词汇都以相同的前缀开头比如 \`need\`\`nested\` 都以 \`ne\` 开头,\`seed\`\`speed\` 都以 \`s\` 开头。要是为每个单词分别存储公共前缀似乎很浪费。
![](https://imgkr.cn-bj.ufileos.com/7cc3cf37-040a-420e-8ef9-d05e92c82cfd.png)
前缀树是一种利用公共前缀来加速补全速度的数据结构前缀树在节点树中排列一组单词单词沿着从根节点到叶子节点的路径存储树的层次对应于前缀的字母位置
前缀的补全是顺着前缀定义的路径来查找的例如在上图的前缀树中前缀 \`ne\` 对应于从子节点取左边缘 \`N\` 和唯一边缘 \`E\` 的路径。然后可以通过继续遍历从 \`E\` 节点可以达到的所有叶节点来生成补全列表。在图中,\`ne\` 的补全可以是两个分支:\`-ed\`\`-sted\`。如果在数中找不到由前缀定义的路径,则说明词汇表中不包含以该前缀开头的单词。
### 有限状态自动机(DFA)实现
前缀树可以有效处理公共前缀但是对于其他共享词部分仍会分别存储在每个分支中比如后缀 \`ed\`\`ing\`\`tion\` 在英文单词中特别常见。在上一个例子中,\`e\`\`d\` 分别存放在了每一个分支上。
有没有一种方法可以更加节省存储空间呢有的那就是 DFA
<center>
<img src="https://imgkr.cn-bj.ufileos.com/02bc143e-e1a7-4b3c-bd5d-8d6d39139f0a.png" style="width: 50%;"></center>
在上面的例子中单词 \`need\`\`nested\`\`seed\`\`speed\` 仅由 9 个节点组成,而上一张图中的前缀树包含了 17 个节点。
可以看出最小化前缀树 DFA 可以在很大程度上减少数据结构的大小即使词汇量很大最小化 DFA 通常也适合在内存中存储避免昂贵的磁盘访问是实现快速自动补全的关键
### 一些扩展
上面介绍了如何利用合理的数据结构实现基本的自动补全功能这些数据结构可以通过多种方式进行扩展从而改善用户体验
通常满足特定前缀的词汇可能很多而用户界面上能够显示的却不多我们更希望能显示最常搜索或者最有价值的词汇这通常可以通过为词汇表中的每个单词增加一个代表单词值的**权重** \`weight\`,并且按照权重高低来排序自动补全列表。
- 对于排序后的词汇表来说在词汇表每个元素上增加 \`weight\` 属性并不难;
- 对于前缀树来说 \`weight\` 存储在叶子节点中,也是很简单的一个实现;
- 对于 \`DFA\` 来说,则较为复杂。因为一个叶子节点可以通过多条路径到达。一种解决方案是将权重关联到路径而不是叶子节点。
目前有不少开源库都提供了这个功能比如主流的搜索引擎框架 [Elasticsearch](https://www.elastic.co/products/elasticsearch)、[Solr](https://lucene.apache.org/solr/) 等,基于此,我们可以实现高效而强大的自动补全功能。
#### 推荐阅读
- [阿里又一个 20k+ stars 开源项目诞生恭喜 fastjson](https://mp.weixin.qq.com/s/RNKDCK2KoyeuMeEs6GUrow)
- [刷掉 90% 候选人的互联网大厂海量数据面试题附题解 + 方法总结](https://mp.weixin.qq.com/s/rjGqxUvrEqJNlo09GrT1Dw)
- [好用期待已久的文本块功能究竟如何在 Java 13 中发挥作用](https://mp.weixin.qq.com/s/kalGv5T8AZGxTnLHr2wDsA)
- [2019 GitHub 开源贡献排行榜新鲜出炉微软谷歌领头阿里跻身前 12](https://mp.weixin.qq.com/s/_q812aGD1b9QvZ2WFI0Qgw)
---
欢迎关注我的公众号**Doocs开源社区**原创技术文章第一时间推送
<center>
<img src="https://imgkr.cn-bj.ufileos.com/1092dc45-e817-4bb0-82b0-2b2b4826ccf2.gif" style="width: 100px;">
</center>
`

View File

@ -1,190 +0,0 @@
let WxRenderer = function (opts) {
this.opts = opts;
let ENV_STRETCH_IMAGE = true;
let footnotes = [];
let footnoteIndex = 0;
let styleMapping = null;
const CODE_FONT_FAMILY = "Menlo, Operator Mono, Consolas, Monaco, monospace";
let merge = (base, extend) => Object.assign({}, base, extend);
this.buildTheme = 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 = (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 = (title, link) => {
footnotes.push([++footnoteIndex, title, link]);
return footnoteIndex;
};
this.buildFootnotes = () => {
let footnoteArray = footnotes.map(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 `<h4 ${getStyles('h4')}>引用链接</h4><p ${getStyles('footnotes')}>${footnoteArray.join('\n')}</p>`;
};
this.buildAddition = () => {
return `
<style>
.preview-wrapper pre::before {
font-family: "SourceSansPro", "HelveticaNeue", Arial, sans-serif;
position: absolute;
top: 0;
right: 0;
color: #ccc;
text-align: center;
font-size: 0.8em;
padding: 5px 10px 0;
line-height: 15px;
height: 15px;
font-weight: 600;
}
</style>
`;
}
this.setOptions = newOpts => {
this.opts = merge(this.opts, newOpts);
};
this.hasFootnotes = () => footnotes.length !== 0;
this.getRenderer = (status) => {
footnotes = [];
footnoteIndex = 0;
styleMapping = this.buildTheme(this.opts.theme);
let renderer = new marked.Renderer();
renderer.heading = (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 = text => `<p ${getStyles('p')}>${text}</p>`;
renderer.blockquote = text => {
text = text.replace(/<p.*?>/, `<p ${getStyles('blockquote_p')}>`);
return `<blockquote ${getStyles('blockquote')}>${text}</blockquote>`;
};
renderer.code = (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 class="prettyprint"><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" data-lang="${lang}">
${codeLines.join('')}
</pre>
</section>
`;
};
renderer.codespan = (text, infoString) => `<code ${getStyles('codespan')}>${text}</code>`;
renderer.listitem = text => `<span ${getStyles('listitem')}><span style="margin-right: 10px;"><%s/></span>${text}</span>`;
renderer.list = (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 = (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 = (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 (status) {
let ref = addFootnote(title || text, href);
return `<span ${getStyles('link')}>${text}<sup>[${ref}]</sup></span>`;
} else {
return text;
}
}
};
renderer.strong = text => `<strong ${getStyles('strong')}>${text}</strong>`;
renderer.em = text => `<p ${getStyles('p', ';font-style: italic;')}>${text}</p>`;
renderer.table = (header, body) => `<section style="padding:0 8px;"><table class="preview-table"><thead ${getStyles('thead')}>${header}</thead><tbody>${body}</tbody></table></section>`;
// renderer.tablerow = (text) => `<tr style="">${text}</tr>`;
renderer.tablecell = (text, flags) => `<td ${getStyles('td')}>${text}</td>`;
renderer.hr = () => `<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

@ -1,41 +0,0 @@
const DEFAULT_CSS_CONTENT =
`/*
按Ctrl+F可格式化
*/
/* 一级标题样式 */
h1 {
}
/* 二级标题样式 */
h2 {
}
/* 三级标题样式 */
h3 {
}
/* 四级标题样式 */
h4 {
}
/* 图片样式 */
image {
}
/* 引用样式 */
blockquote {
}
/* 引用段落样式 */
blockquote_p {
}
/* 段落样式 */
p {
}
/* 行内代码样式 */
codespan {
}
/* 粗体样式 */
strong {
}
/* 链接样式 */
link {
}
/* 微信链接样式 */
wx_link {
}
`

View File

@ -1,177 +0,0 @@
export const default_theme = {
BASE: {
'text-align': 'left',
'color': '#3f3f3f',
'line-height': '1.75'
},
BASE_BLOCK: {
'margin': '1em 8px'
},
block: {
// 一级标题样式
h1: {
'font-size': '1.2em',
'text-align': 'center',
'font-weight': 'bold',
'display': 'table',
'margin': '2em auto 1em',
'padding': '0 1em',
'border-bottom': '2px solid rgba(0, 152, 116, 0.9)'
},
// 二级标题样式
h2: {
'font-size': '1.2em',
'text-align': 'center',
'font-weight': 'bold',
'display': 'table',
'margin': '4em auto 2em',
'padding': '0 0.2em',
'background': 'rgba(0, 152, 116, 0.9)',
'color': '#fff'
},
// 三级标题样式
h3: {
'font-weight': 'bold',
'font-size': '1.1em',
'margin': '2em 8px 0.75em 0',
'line-height': '1.2',
'padding-left': '8px',
'border-left': '3px solid rgba(0, 152, 116, 0.9)'
},
// 四级标题样式
h4: {
'font-weight': 'bold',
'font-size': '1em',
'margin': '2em 8px 0.5em',
'color': 'rgba(66, 185, 131, 0.9)'
},
// 段落样式
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.1em auto 0.5em',
'width': '100% !important'
},
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: {
listitem: {
'text-indent': '-1em',
'display': 'block',
'margin': '0.2em 8px'
},
codespan: {
'font-size': '90%',
'color': '#d14',
'background': 'rgba(27,31,35,.05)',
'padding': '3px 5px',
'border-radius': '4px'
},
link: {
'color': '#576b95'
},
wx_link: {
'color': '#576b95',
'text-decoration': 'none'
},
// 字体加粗样式
strong: {
'color': 'rgba(15, 76, 129, 0.9)',
'font-weight': 'bold'
},
table: {
'border-collapse': 'collapse',
'text-align': 'center',
'margin': '1em 8px'
},
thead: {
'background': 'rgba(0, 0, 0, 0.05)',
'font-weight': 'bold'
},
td: {
'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

@ -1,143 +0,0 @@
// 设置自定义颜色
function setColorWithTemplate(template) {
return function (color) {
let custom_theme = JSON.parse(JSON.stringify(template));
custom_theme.block.h1['border-bottom'] = `2px solid ${color}`;
custom_theme.block.h2['background'] = color;
custom_theme.block.h3['border-left'] = `3px solid ${color}`;
custom_theme.block.h4['color'] = color;
custom_theme.inline.strong['color'] = color;
return custom_theme;
};
}
let setColorWithCustomTemplate = function setColorWithCustomTemplate(template, color) {
let custom_theme = JSON.parse(JSON.stringify(template));
custom_theme.block.h1['border-bottom'] = `2px solid ${color}`;
custom_theme.block.h2['background'] = color;
custom_theme.block.h3['border-left'] = `3px solid ${color}`;
custom_theme.block.h4['color'] = color;
custom_theme.inline.strong['color'] = color;
return custom_theme;
}
// 设置自定义字体大小
function setFontSizeWithTemplate(template) {
return function (fontSize) {
let custom_theme = JSON.parse(JSON.stringify(template));
custom_theme.block.h1['font-size'] = `${fontSize * 1.14}px`;
custom_theme.block.h2['font-size'] = `${fontSize * 1.1}px`;
custom_theme.block.h3['font-size'] = `${fontSize}px`;
custom_theme.block.h4['font-size'] = `${fontSize}px`;
return custom_theme;
}
}
let setColor = setColorWithTemplate(default_theme);
let setFontSize = setFontSizeWithTemplate(default_theme);
function customCssWithTemplate(jsonString, color, theme) {
let custom_theme = JSON.parse(JSON.stringify(theme));
// block
custom_theme.block.h1['border-bottom'] = `2px solid ${color}`;
custom_theme.block.h2['background'] = color;
custom_theme.block.h3['border-left'] = `3px solid ${color}`;
custom_theme.block.h4['color'] = color;
custom_theme.inline.strong['color'] = color;
custom_theme.block.h1 = Object.assign(custom_theme.block.h1, jsonString.h1);
custom_theme.block.h2 = Object.assign(custom_theme.block.h2, jsonString.h2);
custom_theme.block.h3 = Object.assign(custom_theme.block.h3, jsonString.h3);
custom_theme.block.h4 = Object.assign(custom_theme.block.h4, jsonString.h4);
custom_theme.block.p = Object.assign(custom_theme.block.p, jsonString.p);
custom_theme.block.blockquote = Object.assign(custom_theme.block.blockquote, jsonString.blockquote);
custom_theme.block.blockquote_p = Object.assign(custom_theme.block.blockquote_p, jsonString.blockquote_p);
custom_theme.block.image = Object.assign(custom_theme.block.image, jsonString.image);
// inline
custom_theme.inline.strong = Object.assign(custom_theme.inline.strong, jsonString.strong);
custom_theme.inline.codespan = Object.assign(custom_theme.inline.codespan, jsonString.codespan);
custom_theme.inline.link = Object.assign(custom_theme.inline.link, jsonString.link);
custom_theme.inline.wx_link = Object.assign(custom_theme.inline.wx_link, jsonString.wx_link);
return custom_theme;
}
/**
* 将CSS形式的字符串转换为JSON
*
* @param {css字符串} css
*/
function css2json(css) {
// 移除CSS所有注释
while ((open = css.indexOf("/*")) !== -1 &&
(close = css.indexOf("*/")) !== -1) {
css = css.substring(0, open) + css.substring(close + 2);
}
// 初始化返回值
let json = {};
while (css.length > 0 && (css.indexOf('{') !== -1) && (css.indexOf('}') !== -1)) {
// 存储第一个左/右花括号的下标
const lbracket = css.indexOf('{');
const rbracket = css.indexOf('}');
// 第一步将声明转换为Object
// `font: 'Times New Roman' 1em; color: #ff0000; margin-top: 1em;`
// ==>
// `{"font": "'Times New Roman' 1em", "color": "#ff0000", "margin-top": "1em"}`
// 辅助方法将array转为object
function toObject(array) {
let ret = {};
array.forEach(e => {
const index = e.indexOf(':');
const property = e.substring(0, index).trim();
const value = e.substring(index + 1).trim();
ret[property] = value;
});
return ret;
}
// 切割声明块并移除空白符,然后放入数组中
let declarations = css.substring(lbracket + 1, rbracket)
.split(";")
.map(e => e.trim())
.filter(e => e.length > 0); // 移除所有""空值
// 转为Object对象
declarations = toObject(declarations);
// 第二步:选择器处理,每个选择器会与它对应的声明相关联,如:
// `h1, p#bar {color: red}`
// ==>
// {"h1": {color: red}, "p#bar": {color: red}}
let selectors = css.substring(0, lbracket)
// 以,切割,并移除空格:`"h1, p#bar, span.foo"` => ["h1", "p#bar", "span.foo"]
.split(",")
.map(selector => selector.trim());
// 迭代赋值
selectors.forEach(selector => {
// 若不存在,则先初始化
if (!json[selector]) json[selector] = {};
// 赋值到JSON
Object.keys(declarations).forEach(key => {
json[selector][key] = declarations[key];
});
});
// 继续下个声明块
css = css.slice(rbracket + 1).trim();
}
// 返回JSON形式的结果串
return json;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -1,55 +0,0 @@
<!DOCTYPE html>
<!--
_.._ ,------------.
,' `. ( 你终于发现我啦 )
/ __) __` \ `-,----------'
( (`-`(-') ) _.-'
/) \ = / (
/' |--' . \
( ,---| `-.)__`
)( `-.,--' _`-.
'/,' ( Uu",
(_ , `/,-' )
`.__, : `-'/ /`--'
| `--' |
` `-._ /
\ (
/\ . \.
/ |` \ ,-\
/ \| .) / \
( ,'|\ ,' :
| \,`.`--"/ }
`,' \ |,' /
/ "-._ `-/ |
"-. "-.,'| ;
/ _/["---'""]
: / |"- '
' | /
` |
-->
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="keywords" content="md,markdown,markdown-editor,wechat,official-account,yanglbme,doocs">
<meta name="description" content="Wechat Markdown Editor | 一款高度简洁的微信 Markdown 编辑器">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<title>微信 Markdown 编辑器</title>
<link rel="shortcut icon" href="https://imgkr.cn-bj.ufileos.com/f3accc83-b854-4e99-afb5-8a6465e1d84f.png">
<link rel="apple-touch-icon-precomposed" href="https://imgkr.cn-bj.ufileos.com/f3accc83-b854-4e99-afb5-8a6465e1d84f.png">
<link rel="stylesheet" href="assets/css/loading.css">
<link rel="stylesheet" href="libs/css/index.css">
<link rel="stylesheet" href="libs/css/code-themes/github-v2.min.css">
<link rel="stylesheet" href="libs/css/style-mirror.css">
<link rel="stylesheet" href="libs/css/animate.css">
<link rel="stylesheet" href="assets/css/app.css">
</head>
<body>
<div id="app" >
</div>
</body>
</html>

File diff suppressed because one or more lines are too long

View File

@ -1,2 +0,0 @@
/*! 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

@ -1,2 +0,0 @@
/*! 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

@ -1,2 +0,0 @@
/*! 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

@ -1,2 +0,0 @@
/*! 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}}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,36 +0,0 @@
.CodeMirror-hints {
position: absolute;
z-index: 10;
overflow: hidden;
list-style: none;
margin: 0;
padding: 2px;
-webkit-box-shadow: 2px 3px 5px rgba(0,0,0,.2);
-moz-box-shadow: 2px 3px 5px rgba(0,0,0,.2);
box-shadow: 2px 3px 5px rgba(0,0,0,.2);
border-radius: 3px;
border: 1px solid silver;
background: white;
font-size: 90%;
font-family: monospace;
max-height: 20em;
overflow-y: auto;
}
.CodeMirror-hint {
margin: 0;
padding: 0 4px;
border-radius: 2px;
white-space: pre;
color: black;
cursor: pointer;
}
li.CodeMirror-hint-active {
background: #08f;
color: white;
}

View File

@ -1,104 +0,0 @@
/*
Name: Base16 Default Light
Author: Chris Kempson (http://chriskempson.com)
CodeMirror template by Jan T. Sott (https://github.com/idleberg/base16-codemirror)
Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16)
*/
.cm-s-style-mirror.CodeMirror {
background: #f5f5f5;
color: #444;
font-size: 16px;
padding: 20px;
line-height: 25px;
}
.cm-s-style-mirror div.CodeMirror-selected {
background: #e0e0e0;
}
.cm-s-style-mirror .CodeMirror-line::selection,
.cm-s-style-mirror .CodeMirror-line > span::selection,
.cm-s-style-mirror .CodeMirror-line > span > span::selection {
background: #e0e0e0;
}
.cm-s-style-mirror .CodeMirror-line::-moz-selection,
.cm-s-style-mirror .CodeMirror-line > span::-moz-selection,
.cm-s-style-mirror .CodeMirror-line > span > span::-moz-selection {
background: #e0e0e0;
}
.cm-s-style-mirror .CodeMirror-gutters {
background: #f5f5f5;
border-right: 0px;
}
.cm-s-style-mirror .CodeMirror-guttermarker {
color: #ac4142;
}
.cm-s-style-mirror .CodeMirror-guttermarker-subtle {
color: #b0b0b0;
}
.cm-s-style-mirror .CodeMirror-linenumber {
color: #b0b0b0;
}
.cm-s-style-mirror .CodeMirror-cursor {
border-left: 1px solid #505050;
}
.cm-s-style-mirror span.cm-comment {
color:green;
}
.cm-s-style-mirror span.cm-atom {
color: #aa759f;
}
.cm-s-style-mirror span.cm-number {
color: #aa759f;
}
.cm-s-style-mirror span.cm-property,
.cm-s-style-mirror span.cm-attribute {
color: #90a959;
}
.cm-s-style-mirror span.cm-keyword {
color: #023a52;
}
.cm-s-style-mirror span.cm-string {
color: #e46918;
}
.cm-s-style-mirror span.cm-variable {
color: #90a959;
}
.cm-s-style-mirror span.cm-variable-2 {
color: #00695f;
}
.cm-s-style-mirror span.cm-variable-3 {
color: #2e6e8a;
}
.cm-s-style-mirror span.cm-def {
color: #d28445;
}
.cm-s-style-mirror span.cm-bracket {
color: #202020;
}
.cm-s-style-mirror span.cm-tag {
color:#000;
}
.cm-s-style-mirror span.cm-link {
color: #b26a00;
}
.cm-s-style-mirror span.cm-error {
/* background: #ac4142;
color: #f5f5f5; */
text-decoration: underline;
text-decoration-style: wavy;
text-decoration-color: #df8d8e;
}
.cm-s-style-mirror .CodeMirror-activeline-background {
background: #dddcdc;
}
.cm-s-style-mirror .CodeMirror-matchingbracket {
color: rgb(32,32,32) !important;
background-color: rgba(0,0,0,0.1) !important;
}

View File

@ -1,2 +0,0 @@
.cm-s-xq-light span.cm-keyword{line-height:1em;font-weight:700;color:#5a5cad}.cm-s-xq-light span.cm-atom{color:#6c8cd5}.cm-s-xq-light span.cm-number{color:#164}.cm-s-xq-light span.cm-def{text-decoration:underline}.cm-s-xq-light span.cm-variable{color:#000}.cm-s-xq-light span.cm-variable-2{color:#000}.cm-s-xq-light span.cm-type,.cm-s-xq-light span.cm-variable-3{color:#000}.cm-s-xq-light span.cm-comment{color:#0080ff;font-style:italic}.cm-s-xq-light span.cm-string{color:red}.cm-s-xq-light span.cm-meta{color:#ff0}.cm-s-xq-light span.cm-qualifier{color:grey}.cm-s-xq-light span.cm-builtin{color:#7ea656}.cm-s-xq-light span.cm-bracket{color:#cc7}.cm-s-xq-light span.cm-tag{color:#3f7f7f}.cm-s-xq-light span.cm-attribute{color:#7f007f}.cm-s-xq-light span.cm-error{color:red}.cm-s-xq-light .CodeMirror-activeline-background{background:#e8f2ff}.cm-s-xq-light .CodeMirror-matchingbracket{outline:1px solid grey;color:#000!important;background:#ff0}
/*# sourceMappingURL=xq-light.min.css.map */

View File

@ -1,37 +0,0 @@
<template>
<loading v-if="loading" />
<codemirror-editor v-else />
</template>
<script>
import Loading from './components/Loading'
import CodemirrorEditor from './components/CodemirrorEditor'
import prettyPrint from 'prettify'
export default {
name: 'App',
components: {
Loading,
CodemirrorEditor
},
data() {
return {
loading: true
}
},
mounted() {
setTimeout(() => {
this.loading = false
}, 200)
window.console &&
window.console.log &&
(console.log("Think big, train fast, learn deep. See https://github.com/yanglbme"))
setTimeout(() => {
// document.body.addEventListener('load', prettyPrint())
}, 2000)
}
}
</script>
<style lang="scss" scoped>
</style>

View File

@ -1,31 +0,0 @@
import axios from 'axios';
// 创建axios实例
const service = axios.create({
baseURL: '',
timeout: 10 * 1000 // 请求超时时间
});
service.interceptors.request.use(
config => {
if (/^(post)|(put)|(delete)$/i.test(config.method)) {
if (config.data && config.data.upload) {
config.headers['Content-Type'] = 'multipart/form-data';
}
}
return config;
}, error => {
Promise.reject(error);
}
);
service.interceptors.response.use(res => {
if (res.data.success) {
return res.data;
} else {
console.log(res);
}
return Promise.reject(res.data);
}, error => Promise.reject(error));
export default service;

View File

@ -1,15 +0,0 @@
import fetch from './fetch';
function fileUpload(data) {
return fetch({
url: 'https://imgkr.com/api/files/upload',
method: 'post',
data: data
})
}
export default {
fileUpload
};

View File

@ -1,233 +0,0 @@
<template>
<div id="app" class="container">
<el-container>
<el-header class="top editor__header">
<editor-header
@uploaded="uploaded"
@cssChanged="cssChanged"
@showBox="showBox = !showBox"
@showAboutDialog="aboutDialogVisible = true"
@showDialogForm="dialogFormVisible = true"
/>
</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 text here." v-model="source">
</textarea>
</el-col>
<el-col :span="12" class="preview-wrapper" id="preview">
<section id="output-wrapper">
<div class="preview" contenteditable="true">
<section id="output" v-html="output">
</section>
</div>
</section>
</el-col>
<transition name="custom-classes-transition" enter-active-class="animated bounceInRight">
<el-col id="cssBox" :span="12" v-show="showBox">
<textarea id="cssEditor" type="textarea" placeholder="Your custom css here.">
</textarea>
</el-col>
</transition>
</el-row>
</el-main>
</el-container>
<about-dialog :aboutDialogVisible="aboutDialogVisible"
@close="aboutDialogVisible = false" />
<insert-form-dialog :dialogFormVisible="dialogFormVisible"
@close="dialogFormVisible = false" />
</div>
</template>
<script>
import CodeMirror from 'codemirror/lib/codemirror'
import 'codemirror/mode/css/css'
import 'codemirror/mode/markdown/markdown'
import 'codemirror/addon/edit/matchbrackets'
import 'codemirror/addon/selection/active-line'
import 'codemirror/addon/hint/show-hint.js'
import 'codemirror/addon/hint/css-hint.js'
import '../scripts/format.js'
import fileApi from '../api/file';
import editorHeader from './codeMirror/header';
import aboutDialog from './codeMirror/aboutDialog';
import insertFormDialog from './codeMirror/insertForm';
import {
setFontSize,
css2json,
customCssWithTemplate,
saveEditorContent,
isImageIllegal
} from '../scripts/util'
require('codemirror/mode/javascript/javascript')
import '../scripts/closebrackets'
import $ from 'jquery'
import config from '../scripts/config'
import {mapState, mapMutations} from 'vuex';
export default {
data() {
return {
config: config,
showBox: false,
aboutDialogVisible: false,
dialogFormVisible: false,
timeout: null,
source: ''
}
},
components: {
editorHeader, aboutDialog, insertFormDialog
},
computed: {
...mapState({
wxRenderer: state=> state.wxRenderer,
output: state=> state.output,
editor: state=> state.editor,
cssEditor: state=> state.cssEditor,
currentSize: state=> state.currentSize,
currentColor: state=> state.currentColor,
html: state=> state.html
})
},
created() {
this.initEditorState()
this.$nextTick(() => {
this.initEditor()
this.initCssEditor()
this.editorRefresh()
})
},
methods: {
initEditor() {
this.initEditorEntity();
this.editor.on('change', (cm, e) => {
this.editorRefresh()
saveEditorContent(this.editor, '__editor_content')
});
//
this.editor.on('paste', (cm, e) => {
if (!(e.clipboardData && e.clipboardData.items)) {
return
}
for (let i = 0, len = e.clipboardData.items.length; i < len; ++i) {
let item = e.clipboardData.items[i]
if (item.kind === 'file') {
const pasteFile = item.getAsFile()
const checkImageResult = isImageIllegal(pasteFile);
if (checkImageResult) {
this.$message({
showClose: true,
message: checkImageResult,
type: 'error'
});
return;
}
let data = new FormData()
data.append('file', pasteFile)
fileApi.fileUpload(data).then(res => {
this.uploaded(res)
}).catch(err => {
console.log(err.message)
})
}
}
});
},
initCssEditor() {
this.initCssEditorEntity();
//
this.cssEditor.on('keyup', (cm, e) => {
if ((e.keyCode >= 65 && e.keyCode <= 90) || e.keyCode === 189) {
cm.showHint(e)
}
});
this.cssEditor.on('update', (instance) => {
this.cssChanged()
saveEditorContent(this.cssEditor, '__css_content')
})
},
cssChanged() {
let json = css2json(this.cssEditor.getValue(0))
let theme = setFontSize(this.currentSize.replace('px', ''))
theme = customCssWithTemplate(json, this.currentColor, theme)
this.setWxRendererOptions({
theme: theme
});
this.editorRefresh()
},
//
uploaded(response, file, fileList) {
if (response) {
if (response.success) {
//
const cursor = this.editor.getCursor()
const imageUrl = response.data
const markdownImage = `![](${imageUrl})`
// Markdown URL
this.editor.replaceSelection(`\n${markdownImage}\n`, cursor)
this.$message({
showClose: true,
message: '图片插入成功',
type: 'success'
})
this.editorRefresh()
} else {
//
this.$message({
showClose: true,
message: response.message,
type: 'error'
})
}
} else {
this.$message({
showClose: true,
message: '上传图片未知异常',
type: 'error'
})
}
},
//
leftAndRightScroll() {
$('div.CodeMirror-scroll, #preview').on('scroll', function callback() {
clearTimeout(this.timeout)
let source = $(this)
let 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)
this.timeout = setTimeout(() => {
target.on('scroll', callback)
}, 100)
})
},
...mapMutations(['initEditorState', 'initEditorEntity', 'setWxRendererOptions',
'editorRefresh', 'initCssEditorEntity'])
},
mounted() {
this.leftAndRightScroll()
}
}
</script>
<style lang="less" scoped>
.main-body {
padding-top: 0;
}
</style>

View File

@ -1,18 +0,0 @@
<template>
<div class="loading" id="loading">
<div class="loading-wrapper">
<div class="loading-text">Loading...</div>
<div class="loading-anim"></div>
</div>
</div>
</template>
<script>
export default {
}
</script>
<style scoped>
</style>

View File

@ -1,34 +0,0 @@
<template>
<el-dialog title="关于" :visible="aboutDialogVisible" @close="$emit('close')" width="30%" center>
<div style="text-align: center;">
<h3>一款高度简洁的微信 Markdown 编辑器</h3>
</div>
<div style="text-align: center;margin-top:10px;">
<p>扫码关注我的公众号原创技术文章第一时间推送</p>
<img src="assets/images/qrcode-for-doocs.jpg" style="width: 40%; display: block; margin: 20px auto 10px;">
</div>
<span slot="footer" class="dialog-footer">
<el-button type="success" @click="onRedirect('https://github.com/doocs/md')" plain>GitHub 仓库</el-button>
<el-button type="success" @click="onRedirect('https://gitee.com/doocs/md')" plain>Gitee 仓库</el-button>
</span>
</el-dialog>
</template>
<script>
export default {
props: {
aboutDialogVisible: {
type: Boolean,
default: false
}
},
methods: {
onRedirect(url) {
window.open(url)
}
}
}
</script>
<style lang="less" scoped>
</style>

View File

@ -1,254 +0,0 @@
<template>
<el-container class="top">
<!-- 图片上传 -->
<el-upload class="header__item" action="https://imgkr.com/api/files/upload"
:headers="{'Content-Type': 'multipart/form-data'}"
:show-file-list="false" :multiple="true" accept=".jpg,.jpeg,.png,.gif" name="file"
:before-upload="beforeUpload">
<el-tooltip effect="dark" content="上传图片" placement="bottom-start">
<i class="el-icon-upload" size="medium"></i>
</el-tooltip>
</el-upload>
<!-- 下载文本文档 -->
<el-tooltip class="header__item" effect="dark" content="下载编辑框Markdown文档" placement="bottom-start">
<i class="el-icon-download" size="medium" @click="downloadEditorContent"></i>
</el-tooltip>
<!-- 页面重置 -->
<el-tooltip class="header__item" effect="dark" content="重置页面" placement="bottom-start">
<i class="el-icon-refresh" size="medium" @click="reset"></i>
</el-tooltip>
<!-- 插入表格 -->
<el-tooltip class="header__item header__item_last" effect="dark" content="插入表格" placement="bottom-start">
<i class="el-icon-s-grid" size="medium" @click="$emit('showDialogForm')"></i>
</el-tooltip>
<el-form size="mini" class="ctrl" :inline=true>
<el-form-item>
<el-select v-model="selectFont" size="mini" placeholder="选择字体" clearable @change="fontChanged">
<el-option v-for="font in config.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>
<el-select v-model="selectSize" size="mini" placeholder="选择段落字号" clearable @change="sizeChanged">
<el-option v-for="size in config.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>
<el-select v-model="selectColor" size="mini" placeholder="选择颜色" clearable @change="colorChanged">
<el-option v-for="color in config.colorOption" :key="color.value" :label="color.label" :value="color.value">
<span class="select-item-left">{{ color.label }}</span>
<span class="select-item-right">{{ color.hex }}</span>
</el-option>
</el-select>
</el-form-item>
<el-tooltip content="自定义颜色" placement="top">
<el-color-picker v-model="selectColor" size="mini" show-alpha @change="colorChanged"></el-color-picker>
</el-tooltip>
<el-tooltip content="微信外链自动转为文末引用" placement="top">
<el-switch class="header__switch" v-model="citeStatus" active-color="#67c23a" inactive-color="#dcdfe6" @change="statusChanged">
</el-switch>
</el-tooltip>
</el-form>
<el-tooltip class="item" effect="dark" content="自定义CSS样式" placement="left">
<el-button type="success" plain size="medium" icon="el-icon-setting" @click="customStyle"></el-button>
</el-tooltip>
<el-button type="success" plain size="medium" @click="copy">复制</el-button>
<el-button type="success" plain size="medium" class="about" @click="$emit('showAboutDialog')">关于</el-button>
</el-container>
</template>
<script>
import {
setColorWithCustomTemplate,
setFontSize,
isImageIllegal
} from '../../scripts/util'
import fileApi from '../../api/file';
import {
solveWeChatImage,
solveHtml
} from '../../scripts/converter'
import config from '../../scripts/config'
import DEFAULT_CSS_CONTENT from '../../scripts/themes/default-theme-css'
import {mapState, mapMutations} from 'vuex'
export default {
name: 'editor-header',
data() {
return {
config: config,
citeStatus: false,
selectFont: '',
selectSize: '',
selectColor: ''
};
},
computed: {
...mapState({
output: state=> state.output,
editor: state=> state.editor,
cssEditor: state=> state.cssEditor,
currentFont: state=> state.currentFont,
currentSize: state=> state.currentSize,
currentColor: state=> state.currentColor
})
},
methods: {
fontChanged(fonts) {
this.setWxRendererOptions({
fonts: fonts
})
this.setCurrentFont(fonts);
this.editorRefresh()
},
sizeChanged(size) {
let theme = setFontSize(size.replace('px', ''))
theme = setColorWithCustomTemplate(theme, this.currentColor)
this.setWxRendererOptions({
size: size,
theme: theme
})
this.setCurrentSize(size);
this.editorRefresh()
},
colorChanged(color) {
let theme = setFontSize(this.currentSize.replace('px', ''))
theme = setColorWithCustomTemplate(theme, color)
this.setWxRendererOptions({
theme: theme
})
this.setCurrentColor(color);
this.editorRefresh()
},
statusChanged(val) {
this.setCiteStatus(val)
this.editorRefresh()
},
//
beforeUpload(file) {
const checkImageResult = isImageIllegal(file);
if (checkImageResult) {
this.$message({
showClose: true,
message: checkImageResult,
type: 'error'
});
return false;
}
let fd = new FormData();
fd.append('file', file);
fileApi.fileUpload(fd).then(res => {
this.$emit('uploaded', res)
}).catch(err => {
console.log(err.message)
})
return false;
},
//
copy() {
let clipboardDiv = document.getElementById('output')
solveWeChatImage()
this.setHtml(solveHtml())
clipboardDiv.focus()
window.getSelection().removeAllRanges()
let range = document.createRange()
range.setStartBefore(clipboardDiv.firstChild)
range.setEndAfter(clipboardDiv.lastChild)
window.getSelection().addRange(range)
document.execCommand('copy')
//
this.$notify({
showClose: true,
message: '已复制渲染后的文章到剪贴板,可直接到公众号后台粘贴',
offset: 80,
duration: 1600,
type: 'success'
})
clipboardDiv.innerHTML = this.output; //
},
// CSS
async customStyle () {
this.$emit('showBox');
this.$nextTick(() => {
if(!this.cssEditor) {
this.cssEditor.refresh()
}
})
setTimeout(() => {
this.cssEditor.refresh()
},50)
let flag = await localStorage.getItem('__css_content')
if (!flag) {
this.setCssEditorValue(DEFAULT_CSS_CONTENT)
}
},
//
reset() {
this.$confirm('此操作将丢失本地缓存的文本和自定义样式,是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
confirmButtonClass: 'el-button--success',
cancelButtonClass: 'el-button--success is-plain',
type: 'warning',
center: true
}).then(() => {
localStorage.clear()
this.clearEditorToDefault();
this.editor.focus()
this.citeStatus = false;
this.statusChanged(false);
this.fontChanged(this.config.builtinFonts[0].value)
this.colorChanged(this.config.colorOption[1].value)
this.sizeChanged(this.config.sizeOption[2].value)
this.$emit('cssChanged')
}).catch(() => {
this.editor.focus()
})
},
//
downloadEditorContent () {
let downLink = document.createElement('a')
downLink.download = 'content.md'
downLink.style.display = 'none'
let blob = new Blob([this.editor.getValue(0)])
downLink.href = URL.createObjectURL(blob)
document.body.appendChild(downLink)
downLink.click()
document.body.removeChild(downLink)
},
...mapMutations(['editorRefresh', 'clearEditorToDefault','setCurrentColor', 'setCiteStatus',
'setHtml', 'setCurrentFont', 'setCurrentSize', 'setCssEditorValue', 'setWxRendererOptions'])
},
mounted() {
this.selectFont = this.currentFont
this.selectSize = this.currentSize
this.selectColor = this.currentColor
}
}
</script>
<style lang="less" scoped>
.editor__header {
width: 100%;
}
.header__item {
margin: 0 3px;
}
.header__item_last {
margin-right: 8px;
}
.header__switch {
margin-left: 8px;
}
</style>

View File

@ -1,71 +0,0 @@
<template>
<el-dialog title="插入表格" :visible="dialogFormVisible" @close="$emit('close')">
<el-form :model="config.form">
<el-form-item label="行数(表头不计入行数)">
<el-input v-model="config.form.rows"></el-input>
</el-form-item>
<el-form-item label="列数">
<el-input v-model="config.form.cols"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="success" plain @click="$emit('close')"> </el-button>
<el-button type="success" @click="insertTable"> </el-button>
</div>
</el-dialog>
</template>
<script>
import config from '../../scripts/config'
import {mapState, mapMutations} from 'vuex';
export default {
props: {
dialogFormVisible: {
type: Boolean,
default: false
}
},
data() {
return {
config: config
}
},
computed: {
...mapState({
editor: state=> state.editor
})
},
methods: {
//
insertTable() {
const cursor = this.editor.getCursor()
const rows = parseInt(this.config.form.rows)
const cols = parseInt(this.config.form.cols)
if (isNaN(rows) || isNaN(cols) || rows < 1 || cols < 1) {
this.$message({
showClose: true,
message: '输入的行/列数无效,请重新输入',
type: 'error'
})
return
}
let table = ''
for (let i = 0; i < rows + 2; ++i) {
for (let j = 0; j < cols + 1; ++j) {
table += (j === 0 ? '|' : (i !== 1 ? ' |' : ' --- |'))
}
table += '\n'
}
this.editor.replaceSelection(`\n${table}\n`, cursor)
this.$emit('close')
this.editorRefresh()
},
...mapMutations(['editorRefresh'])
}
}
</script>
<style lang="less" scoped>
</style>

View File

@ -1,41 +0,0 @@
import Vue from 'vue'
import {
Container,
Header,
Upload,
Tooltip,
Form,
FormItem,
Select,
Option,
ColorPicker,
Switch,
Button,
Main,
Col,
Row,
Dialog,
Loading,
Message
} from 'element-ui'
Vue.use(Container)
Vue.use(Header)
Vue.use(Upload)
Vue.use(Tooltip)
Vue.use(Form)
Vue.use(FormItem)
Vue.use(Select)
Vue.use(Option)
Vue.use(ColorPicker)
Vue.use(Switch)
Vue.use(Button)
Vue.use(Main)
Vue.use(Col)
Vue.use(Row)
Vue.use(Dialog)
Vue.use(Loading)
Vue.component(Message.name, Message)
Vue.prototype.$loading = Loading.service
Vue.prototype.$message = Message

View File

@ -1,20 +0,0 @@
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
import './element'
import 'codemirror/lib/codemirror.css';
import "codemirror/theme/ambiance.css";
import "codemirror/addon/hint/show-hint.css";
import "codemirror/theme/xq-light.css";
Vue.use(ElementUI)
Vue.config.productionTip = false
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')

View File

@ -1,29 +0,0 @@
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'home',
component: Home
},
{
path: '/about',
name: 'about',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
}
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
export default router

View File

@ -1,214 +0,0 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: https://codemirror.net/LICENSE
import CodeMirror from "codemirror/lib/codemirror";
(function(CodeMirror) {
var defaults = {
pairs: "()[]{}''\"\"",
closeBefore: ")]}'\":;>",
triples: "",
explode: "[]{}"
};
var Pos = CodeMirror.Pos;
CodeMirror.defineOption("autoCloseBrackets", false, function(cm, val, old) {
if (old && old != CodeMirror.Init) {
cm.removeKeyMap(keyMap);
cm.state.closeBrackets = null;
}
if (val) {
ensureBound(getOption(val, "pairs"));
cm.state.closeBrackets = val;
cm.addKeyMap(keyMap);
}
});
function getOption(conf, name) {
if (name == "pairs" && typeof conf == "string") return conf;
if (typeof conf == "object" && conf[name] != null) return conf[name];
return defaults[name];
}
var keyMap = { Backspace: handleBackspace, Enter: handleEnter };
function ensureBound(chars) {
for (var i = 0; i < chars.length; i++) {
var ch = chars.charAt(i),
key = "'" + ch + "'";
if (!keyMap[key]) keyMap[key] = handler(ch);
}
}
ensureBound(defaults.pairs + "`");
function handler(ch) {
return function(cm) {
return handleChar(cm, ch);
};
}
function getConfig(cm) {
var deflt = cm.state.closeBrackets;
if (!deflt || deflt.override) return deflt;
var mode = cm.getModeAt(cm.getCursor());
return mode.closeBrackets || deflt;
}
function handleBackspace(cm) {
var conf = getConfig(cm);
if (!conf || cm.getOption("disableInput")) return CodeMirror.Pass;
var pairs = getOption(conf, "pairs");
var ranges = cm.listSelections();
for (var i = 0; i < ranges.length; i++) {
if (!ranges[i].empty()) return CodeMirror.Pass;
var around = charsAround(cm, ranges[i].head);
if (!around || pairs.indexOf(around) % 2 != 0) return CodeMirror.Pass;
}
for (var i = ranges.length - 1; i >= 0; i--) {
var cur = ranges[i].head;
cm.replaceRange(
"",
Pos(cur.line, cur.ch - 1),
Pos(cur.line, cur.ch + 1),
"+delete"
);
}
}
function handleEnter(cm) {
var conf = getConfig(cm);
var explode = conf && getOption(conf, "explode");
if (!explode || cm.getOption("disableInput")) return CodeMirror.Pass;
var ranges = cm.listSelections();
for (var i = 0; i < ranges.length; i++) {
if (!ranges[i].empty()) return CodeMirror.Pass;
var around = charsAround(cm, ranges[i].head);
if (!around || explode.indexOf(around) % 2 != 0) return CodeMirror.Pass;
}
cm.operation(function() {
var linesep = cm.lineSeparator() || "\n";
cm.replaceSelection(linesep + linesep, null);
cm.execCommand("goCharLeft");
ranges = cm.listSelections();
for (var i = 0; i < ranges.length; i++) {
var line = ranges[i].head.line;
cm.indentLine(line, null, true);
cm.indentLine(line + 1, null, true);
}
});
}
function contractSelection(sel) {
var inverted = CodeMirror.cmpPos(sel.anchor, sel.head) > 0;
return {
anchor: new Pos(sel.anchor.line, sel.anchor.ch + (inverted ? -1 : 1)),
head: new Pos(sel.head.line, sel.head.ch + (inverted ? 1 : -1))
};
}
function handleChar(cm, ch) {
var conf = getConfig(cm);
if (!conf || cm.getOption("disableInput")) return CodeMirror.Pass;
var pairs = getOption(conf, "pairs");
var pos = pairs.indexOf(ch);
if (pos == -1) return CodeMirror.Pass;
var closeBefore = getOption(conf, "closeBefore");
var triples = getOption(conf, "triples");
var identical = pairs.charAt(pos + 1) == ch;
var ranges = cm.listSelections();
var opening = pos % 2 == 0;
var type;
for (var i = 0; i < ranges.length; i++) {
var range = ranges[i],
cur = range.head,
curType;
var next = cm.getRange(cur, Pos(cur.line, cur.ch + 1));
if (opening && !range.empty()) {
curType = "surround";
} else if ((identical || !opening) && next == ch) {
if (identical && stringStartsAfter(cm, cur)) curType = "both";
else if (
triples.indexOf(ch) >= 0 &&
cm.getRange(cur, Pos(cur.line, cur.ch + 3)) == ch + ch + ch
)
curType = "skipThree";
else curType = "skip";
} else if (
identical &&
cur.ch > 1 &&
triples.indexOf(ch) >= 0 &&
cm.getRange(Pos(cur.line, cur.ch - 2), cur) == ch + ch
) {
if (
cur.ch > 2 &&
/\bstring/.test(cm.getTokenTypeAt(Pos(cur.line, cur.ch - 2)))
)
return CodeMirror.Pass;
curType = "addFour";
} else if (identical) {
var prev =
cur.ch == 0 ? " " : cm.getRange(Pos(cur.line, cur.ch - 1), cur);
if (
!CodeMirror.isWordChar(next) &&
prev != ch &&
!CodeMirror.isWordChar(prev)
)
curType = "both";
else return CodeMirror.Pass;
} else if (
opening &&
(next.length === 0 || /\s/.test(next) || closeBefore.indexOf(next) > -1)
) {
curType = "both";
} else {
return CodeMirror.Pass;
}
if (!type) type = curType;
else if (type != curType) return CodeMirror.Pass;
}
var left = pos % 2 ? pairs.charAt(pos - 1) : ch;
var right = pos % 2 ? ch : pairs.charAt(pos + 1);
cm.operation(function() {
if (type == "skip") {
cm.execCommand("goCharRight");
} else if (type == "skipThree") {
for (var i = 0; i < 3; i++) cm.execCommand("goCharRight");
} else if (type == "surround") {
var sels = cm.getSelections();
for (var i = 0; i < sels.length; i++) sels[i] = left + sels[i] + right;
cm.replaceSelections(sels, "around");
sels = cm.listSelections().slice();
for (var i = 0; i < sels.length; i++)
sels[i] = contractSelection(sels[i]);
cm.setSelections(sels);
} else if (type == "both") {
cm.replaceSelection(left + right, null);
cm.triggerElectric(left + right);
cm.execCommand("goCharLeft");
} else if (type == "addFour") {
cm.replaceSelection(left + left + left + left, "before");
cm.execCommand("goCharRight");
}
});
}
function charsAround(cm, pos) {
var str = cm.getRange(Pos(pos.line, pos.ch - 1), Pos(pos.line, pos.ch + 1));
return str.length == 2 ? str : null;
}
function stringStartsAfter(cm, pos) {
var token = cm.getTokenAt(Pos(pos.line, pos.ch + 1));
return (
/\bstring/.test(token.type) &&
token.start == pos.ch &&
(pos.ch == 0 || !/\bstring/.test(cm.getTokenTypeAt(pos)))
);
}
})(CodeMirror);

View File

@ -1,60 +0,0 @@
export default {
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: '12px',
value: '12px',
desc: '更小'
},
{
label: '13px',
value: '13px',
desc: '稍小'
},
{
label: '14px',
value: '14px',
desc: '推荐'
},
{
label: '15px',
value: '15px',
desc: '稍大'
},
{
label: '16px',
value: '16px',
desc: '更大'
}
],
colorOption: [
{
label: '经典蓝',
value: 'rgba(15, 76, 129, 1)',
hex: '最新流行'
},
{
label: '翡翠绿',
value: 'rgba(0, 152, 116, 1)',
hex: '优雅清新'
},
{
label: '活力橘',
value: 'rgba(250, 81, 81, 1)',
hex: '热情活泼'
}
],
form: {
rows: 1,
cols: 1
}
};

View File

@ -1,29 +0,0 @@
import juice from 'juice'
export function solveWeChatImage() {
const clipboardDiv = document.getElementById('output');
const images = clipboardDiv.getElementsByTagName("img");
for (let i = 0; i < images.length; i++) {
const image = images[i];
const width = image.getAttribute("width");
const height = image.getAttribute("height");
image.removeAttribute("width");
image.removeAttribute("height");
image.style.width = width;
image.style.height = height;
}
}
export function solveHtml() {
const element = document.getElementById("output-wrapper");
let html = element.innerHTML;
let res = "";
res = juice.inlineContent(
html,
{
inlinePseudoElements: true,
preserveImportant: true
}
);
// console.log(res);
return res;
}

View File

@ -1,100 +0,0 @@
const DEFAULT_CONTENT =
`# 示例文章Google 搜索的即时自动补全功能究竟是如何“工作”的?
> Google 搜索**自动补全功能**的强大相信不少朋友都能感受到它帮助我们更快地补全我们所要输入的搜索关键字那么它怎么知道我们要输入什么内容它又是如何工作的在这篇文章里我们一起来看看
## 使用自动补全
Google 搜索的自动补全功能可以在 Google 搜索应用的大多数位置使用包括 [Google](https://www.google.com/) 主页、适用于 IOS 和 Android 的 Google 应用,我们只需要在 Google 搜索框上开始键入关键字,就可以看到联想词了。
![](https://imgkr.cn-bj.ufileos.com/17ed83bf-e028-4db2-9503-5a3b4e64deee.gif)
在上图示例中我们可以看到输入关键字 \`juej\`Google 搜索会联想到“掘金”、“掘金小册”、“绝句”等等,好处就是,我们无须输入完整的关键字即可轻松完成针对这些 topics 的搜索。
谷歌搜索的自动补全功能对于使用移动设备的用户来说特别有用用户可以轻松在难以键入的小屏幕上完成搜索当然对于移动设备用户和台式机用户而言这都节省了大量的时间根据 Google 官方报告自动补全功能可以减少大约 25% 的打字累积起来预计每天可以节省 200 多年的打字时间是的每天
> 注意本文所提到的**联想词****预测**是同一个意思
## 基于预测而非建议
Google 官方将自动补全功能称之为预测而不是建议为什么呢其实是有充分理由的自动补全功能是为了**帮助用户完成他们打算进行的搜索**而不是建议用户要执行什么搜索
那么Google 是如何确定这些预测其实Google 会根据趋势搜索 [trends](https://trends.google.com/trends/?geo=US) 给到我们这些“预测”。简单来说,哪个热门、哪个搜索频率高,就更可能推给我们。当然,这也与我们当前所处的位置以及我们的搜索历史相关。
另外这些预测也会随着我们键入的关键字的变更而更改例如当我们把键入的关键字从 \`juej\` 更改为 \`juex\` 时,与“掘金”相关的预测会“消失”,同时,与“觉醒”、“决心”相关联的词会出现。
![](https://imgkr.cn-bj.ufileos.com/5b17dc99-606d-42c1-9f86-e09e88aaa822.gif)
## 为什么看不到某些联想词
如果我们在输入某个关键字时看不到联想词那么表明 Google 的算法可能检测到
- 这个关键字不是热门字词
- 搜索的字词太新了我们可能需要等待几天或几周才能看到联想词
- 这是一个侮辱性或敏感字词这个搜索字词违反了 Google 的相关政策更加详细的情况可以了解 [Google 搜索自动补全政策](https://support.google.com/websearch/answer/7368877)。
## 为什么会看到某些不当的联想词
Google 拥有专门设计的系统可以自动捕获不适当的预测结果而不显示出来然而Google 每天需要处理数十亿次搜索这意味着 Google 每天会显示数十亿甚至上百亿条预测再好的系统也可能存在缺陷不正确的预测也可能随时会出现
我们作为 Google 搜索的用户如果认定某条预测违反了相关的搜索自动补全政策可以进行举报反馈点击右下角**举报不当的联想查询**并勾选相关选项即可
![](https://imgkr.cn-bj.ufileos.com/6ca8185d-12c6-4550-bb4e-e49cfbf56db7.gif)
## 如何实现自动补全算法
目前Google 官方似乎并没有公开搜索自动补全的算法实现但是业界在这方面已经有了不少研究
一个好的自动补全器必须是快速的并且在用户键入下一个字符后立即更新联想词列表**自动补全器的核心是一个函数它接受输入的前缀并搜索以给定前缀开头的词汇或语句列表**通常来说只需要返回少量的数目即可
接下来我们先从一个简单且低效的实现开始并在此基础上逐步构建更高效的方法
### 词汇表实现
一个**简单粗暴的实现方式**顺序查找词汇表依次检查每个词汇看它是否以给定的前缀开头
但是此方法需要将前缀与每个词汇进行匹配检查若词汇量较少这种方式可能勉强行得通但是如果词汇量规模较大效率就太低了
一个**更好的实现方式是**让词汇按字典顺序排序借助二分搜索算法可以快速搜索有序词汇表中的前缀由于二分搜索的每一步都会将搜索的范围减半因此总的搜索时间与词汇表中单词数量的对数成正比即时间复杂度是 \`O(log N)\`。二分搜索的性能很好,但有没有更好的实现呢?当然有,往下看。
### 前缀树实现
通常来说许多词汇都以相同的前缀开头比如 \`need\`\`nested\` 都以 \`ne\` 开头,\`seed\`\`speed\` 都以 \`s\` 开头。要是为每个单词分别存储公共前缀似乎很浪费。
![](https://imgkr.cn-bj.ufileos.com/7cc3cf37-040a-420e-8ef9-d05e92c82cfd.png)
前缀树是一种利用公共前缀来加速补全速度的数据结构前缀树在节点树中排列一组单词单词沿着从根节点到叶子节点的路径存储树的层次对应于前缀的字母位置
前缀的补全是顺着前缀定义的路径来查找的例如在上图的前缀树中前缀 \`ne\` 对应于从子节点取左边缘 \`N\` 和唯一边缘 \`E\` 的路径。然后可以通过继续遍历从 \`E\` 节点可以达到的所有叶节点来生成补全列表。在图中,\`ne\` 的补全可以是两个分支:\`-ed\`\`-sted\`。如果在数中找不到由前缀定义的路径,则说明词汇表中不包含以该前缀开头的单词。
### 有限状态自动机(DFA)实现
前缀树可以有效处理公共前缀但是对于其他共享词部分仍会分别存储在每个分支中比如后缀 \`ed\`\`ing\`\`tion\` 在英文单词中特别常见。在上一个例子中,\`e\`\`d\` 分别存放在了每一个分支上。
有没有一种方法可以更加节省存储空间呢有的那就是 DFA
<center>
<img src="https://imgkr.cn-bj.ufileos.com/02bc143e-e1a7-4b3c-bd5d-8d6d39139f0a.png" style="width: 50%;"></center>
在上面的例子中单词 \`need\`\`nested\`\`seed\`\`speed\` 仅由 9 个节点组成,而上一张图中的前缀树包含了 17 个节点。
可以看出最小化前缀树 DFA 可以在很大程度上减少数据结构的大小即使词汇量很大最小化 DFA 通常也适合在内存中存储避免昂贵的磁盘访问是实现快速自动补全的关键
### 一些扩展
上面介绍了如何利用合理的数据结构实现基本的自动补全功能这些数据结构可以通过多种方式进行扩展从而改善用户体验
通常满足特定前缀的词汇可能很多而用户界面上能够显示的却不多我们更希望能显示最常搜索或者最有价值的词汇这通常可以通过为词汇表中的每个单词增加一个代表单词值的**权重** \`weight\`,并且按照权重高低来排序自动补全列表。
- 对于排序后的词汇表来说在词汇表每个元素上增加 \`weight\` 属性并不难;
- 对于前缀树来说 \`weight\` 存储在叶子节点中,也是很简单的一个实现;
- 对于 \`DFA\` 来说,则较为复杂。因为一个叶子节点可以通过多条路径到达。一种解决方案是将权重关联到路径而不是叶子节点。
目前有不少开源库都提供了这个功能比如主流的搜索引擎框架 [Elasticsearch](https://www.elastic.co/products/elasticsearch)、[Solr](https://lucene.apache.org/solr/) 等,基于此,我们可以实现高效而强大的自动补全功能。
#### 推荐阅读
- [阿里又一个 20k+ stars 开源项目诞生恭喜 fastjson](https://mp.weixin.qq.com/s/RNKDCK2KoyeuMeEs6GUrow)
- [刷掉 90% 候选人的互联网大厂海量数据面试题附题解 + 方法总结](https://mp.weixin.qq.com/s/rjGqxUvrEqJNlo09GrT1Dw)
- [好用期待已久的文本块功能究竟如何在 Java 13 中发挥作用](https://mp.weixin.qq.com/s/kalGv5T8AZGxTnLHr2wDsA)
- [2019 GitHub 开源贡献排行榜新鲜出炉微软谷歌领头阿里跻身前 12](https://mp.weixin.qq.com/s/_q812aGD1b9QvZ2WFI0Qgw)
---
欢迎关注我的公众号**Doocs开源社区**原创技术文章第一时间推送
<center>
<img src="https://imgkr.cn-bj.ufileos.com/1092dc45-e817-4bb0-82b0-2b2b4826ccf2.gif" style="width: 100px;">
</center>
`
export default DEFAULT_CONTENT

View File

@ -1,84 +0,0 @@
import CodeMirror from "codemirror/lib/codemirror";
(function () {
CodeMirror.extendMode('css', {
commentStart: '/*',
commentEnd: '*/',
newlineAfterToken: function (type, content) {
return /^[;{}]$/.test(content)
}
})
// Comment/uncomment the specified range
CodeMirror.defineExtension('commentRange', function (isComment, from, to) {
var cm = this; var curMode = CodeMirror.innerMode(cm.getMode(), cm.getTokenAt(from).state).mode
cm.operation(function () {
if (isComment) { // Comment range
cm.replaceRange(curMode.commentEnd, to)
cm.replaceRange(curMode.commentStart, from)
if (from.line == to.line && from.ch == to.ch) // An empty comment inserted - put cursor inside
{ cm.setCursor(from.line, from.ch + curMode.commentStart.length) }
} else { // Uncomment range
var selText = cm.getRange(from, to)
var startIndex = selText.indexOf(curMode.commentStart)
var endIndex = selText.lastIndexOf(curMode.commentEnd)
if (startIndex > -1 && endIndex > -1 && endIndex > startIndex) {
// Take string till comment start
selText = selText.substr(0, startIndex) +
// From comment start till comment end
selText.substring(startIndex + curMode.commentStart.length, endIndex) +
// From comment end till string end
selText.substr(endIndex + curMode.commentEnd.length)
}
cm.replaceRange(selText, from, to)
}
})
})
// Applies automatic mode-aware indentation to the specified range
CodeMirror.defineExtension('autoIndentRange', function (from, to) {
var cmInstance = this
this.operation(function () {
for (var i = from.line; i <= to.line; i++) {
cmInstance.indentLine(i, 'smart')
}
})
})
// Applies automatic formatting to the specified range
CodeMirror.defineExtension('autoFormatRange', function (from, to) {
var cm = this
var outer = cm.getMode(); var text = cm.getRange(from, to).split('\n')
var state = CodeMirror.copyState(outer, cm.getTokenAt(from).state)
var tabSize = cm.getOption('tabSize')
var out = ''; var lines = 0; var atSol = from.ch == 0
function newline () {
out += '\n'
atSol = true
++lines
}
for (var i = 0; i < text.length; ++i) {
var stream = new CodeMirror.StringStream(text[i], tabSize)
while (!stream.eol()) {
var inner = CodeMirror.innerMode(outer, state)
var style = outer.token(stream, state); var cur = stream.current()
stream.start = stream.pos
if (!atSol || /\S/.test(cur)) {
out += cur
atSol = false
}
if (!atSol && inner.mode.newlineAfterToken &&
inner.mode.newlineAfterToken(style, cur, stream.string.slice(stream.pos) || text[i + 1] || '', inner.state)) { newline() }
}
if (!stream.pos && outer.blankLine) outer.blankLine(state)
if (!atSol) newline()
}
cm.operation(function () {
cm.replaceRange(out, from, to)
for (var cur = from.line + 1, end = from.line + lines; cur <= end; ++cur) { cm.indentLine(cur, 'smart') }
cm.setSelection(from, cm.getCursor(false))
})
})
})()

View File

@ -1,192 +0,0 @@
import marked from 'marked'
const WxRenderer = function (opts) {
this.opts = opts
let ENV_STRETCH_IMAGE = true
let footnotes = []
let footnoteIndex = 0
let styleMapping = null
const CODE_FONT_FAMILY = 'Menlo, Operator Mono, Consolas, Monaco, monospace'
let merge = (base, extend) => Object.assign({}, base, extend)
this.buildTheme = 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 = (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 = (title, link) => {
footnotes.push([++footnoteIndex, title, link])
return footnoteIndex
}
this.buildFootnotes = () => {
let footnoteArray = footnotes.map(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 `<h4 ${getStyles('h4')}>引用链接</h4><p ${getStyles('footnotes')}>${footnoteArray.join('\n')}</p>`
}
this.buildAddition = () => {
return `
<style>
.preview-wrapper pre::before {
font-family: "SourceSansPro", "HelveticaNeue", Arial, sans-serif;
position: absolute;
top: 0;
right: 0;
color: #ccc;
text-align: center;
font-size: 0.8em;
padding: 5px 10px 0;
line-height: 15px;
height: 15px;
font-weight: 600;
}
</style>
`
}
this.setOptions = newOpts => {
this.opts = merge(this.opts, newOpts)
}
this.hasFootnotes = () => footnotes.length !== 0
this.getRenderer = (status) => {
footnotes = []
footnoteIndex = 0
styleMapping = this.buildTheme(this.opts.theme)
let renderer = new marked.Renderer()
renderer.heading = (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 = text => `<p ${getStyles('p')}>${text}</p>`
renderer.blockquote = text => {
text = text.replace(/<p.*?>/, `<p ${getStyles('blockquote_p')}>`)
return `<blockquote ${getStyles('blockquote')}>${text}</blockquote>`
}
renderer.code = (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 class="prettyprint"><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" data-lang="${lang}">
${codeLines.join('')}
</pre>
</section>
`
}
renderer.codespan = (text, infoString) => `<code ${getStyles('codespan')}>${text}</code>`
renderer.listitem = text => `<span ${getStyles('listitem')}><span style="margin-right: 10px;"><%s/></span>${text}</span>`
renderer.list = (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 = (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 = (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 (status) {
let ref = addFootnote(title || text, href)
return `<span ${getStyles('link')}>${text}<sup>[${ref}]</sup></span>`
} else {
return text
}
}
}
renderer.strong = text => `<strong ${getStyles('strong')}>${text}</strong>`
renderer.em = text => `<p ${getStyles('p', ';font-style: italic;')}>${text}</p>`
renderer.table = (header, body) => `<section style="padding:0 8px;"><table class="preview-table"><thead ${getStyles('thead')}>${header}</thead><tbody>${body}</tbody></table></section>`
// renderer.tablerow = (text) => `<tr style="">${text}</tr>`;
renderer.tablecell = (text, flags) => `<td ${getStyles('td')}>${text}</td>`
renderer.hr = () => `<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
}
}
export default WxRenderer

View File

@ -1,23 +0,0 @@
// 左右栏同步滚动
$(document).ready(() => {
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(() => {
target.on("scroll", callback);
}, 100);
});
});

View File

@ -1,42 +0,0 @@
const DEFAULT_CSS_CONTENT =
`/*
按Ctrl+F可格式化
*/
/* 一级标题样式 */
h1 {
}
/* 二级标题样式 */
h2 {
}
/* 三级标题样式 */
h3 {
}
/* 四级标题样式 */
h4 {
}
/* 图片样式 */
image {
}
/* 引用样式 */
blockquote {
}
/* 引用段落样式 */
blockquote_p {
}
/* 段落样式 */
p {
}
/* 行内代码样式 */
codespan {
}
/* 粗体样式 */
strong {
}
/* 链接样式 */
link {
}
/* 微信链接样式 */
wx_link {
}
`
export default DEFAULT_CSS_CONTENT

View File

@ -1,177 +0,0 @@
export default {
BASE: {
'text-align': 'left',
'color': '#3f3f3f',
'line-height': '1.75',
},
BASE_BLOCK: {
'margin': '1em 8px'
},
block: {
// 一级标题样式
h1: {
'font-size': '1.2em',
'text-align': 'center',
'font-weight': 'bold',
'display': 'table',
'margin': '2em auto 1em',
'padding': '0 1em',
'border-bottom': '2px solid rgba(0, 152, 116, 0.9)'
},
// 二级标题样式
h2: {
'font-size': '1.2em',
'text-align': 'center',
'font-weight': 'bold',
'display': 'table',
'margin': '4em auto 2em',
'padding': '0 0.2em',
'background': 'rgba(0, 152, 116, 0.9)',
'color': '#fff'
},
// 三级标题样式
h3: {
'font-weight': 'bold',
'font-size': '1.1em',
'margin': '2em 8px 0.75em 0',
'line-height': '1.2',
'padding-left': '8px',
'border-left': '3px solid rgba(0, 152, 116, 0.9)'
},
// 四级标题样式
h4: {
'font-weight': 'bold',
'font-size': '1em',
'margin': '2em 8px 0.5em',
'color': 'rgba(66, 185, 131, 0.9)'
},
// 段落样式
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.1em auto 0.5em',
'width': '100% !important',
},
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: {
listitem: {
'text-indent': '-1em',
'display': 'block',
'margin': '0.2em 8px'
},
codespan: {
'font-size': '90%',
'color': '#d14',
'background': 'rgba(27,31,35,.05)',
'padding': '3px 5px',
'border-radius': '4px',
},
link: {
'color': '#576b95'
},
wx_link: {
'color': '#576b95',
'text-decoration': 'none',
},
// 字体加粗样式
strong: {
'color': 'rgba(15, 76, 129, 0.9)',
'font-weight': 'bold',
},
table: {
'border-collapse': 'collapse',
'text-align': 'center',
'margin': '1em 8px'
},
thead: {
'background': 'rgba(0, 0, 0, 0.05)',
'font-weight': 'bold'
},
td: {
'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

@ -1,195 +0,0 @@
import default_theme from "./themes/default-theme";
// 设置自定义颜色
export function setColorWithTemplate (template) {
return function (color) {
let custom_theme = JSON.parse(JSON.stringify(template))
custom_theme.block.h1['border-bottom'] = `2px solid ${color}`
custom_theme.block.h2['background'] = color
custom_theme.block.h3['border-left'] = `3px solid ${color}`
custom_theme.block.h4['color'] = color
custom_theme.inline.strong['color'] = color
return custom_theme
}
}
export const setColorWithCustomTemplate = function setColorWithCustomTemplate (
template,
color
) {
let custom_theme = JSON.parse(JSON.stringify(template))
custom_theme.block.h1['border-bottom'] = `2px solid ${color}`
custom_theme.block.h2['background'] = color
custom_theme.block.h3['border-left'] = `3px solid ${color}`
custom_theme.block.h4['color'] = color
custom_theme.inline.strong['color'] = color
return custom_theme
}
// 设置自定义字体大小
export function setFontSizeWithTemplate (template) {
return function (fontSize) {
let custom_theme = JSON.parse(JSON.stringify(template))
custom_theme.block.h1['font-size'] = `${fontSize * 1.14}px`
custom_theme.block.h2['font-size'] = `${fontSize * 1.1}px`
custom_theme.block.h3['font-size'] = `${fontSize}px`
custom_theme.block.h4['font-size'] = `${fontSize}px`
return custom_theme
}
}
export const setColor = setColorWithTemplate(default_theme)
export const setFontSize = setFontSizeWithTemplate(default_theme)
export function customCssWithTemplate (jsonString, color, theme) {
let custom_theme = JSON.parse(JSON.stringify(theme))
// block
custom_theme.block.h1['border-bottom'] = `2px solid ${color}`
custom_theme.block.h2['background'] = color
custom_theme.block.h3['border-left'] = `3px solid ${color}`
custom_theme.block.h4['color'] = color
custom_theme.inline.strong['color'] = color
custom_theme.block.h1 = Object.assign(custom_theme.block.h1, jsonString.h1)
custom_theme.block.h2 = Object.assign(custom_theme.block.h2, jsonString.h2)
custom_theme.block.h3 = Object.assign(custom_theme.block.h3, jsonString.h3)
custom_theme.block.h4 = Object.assign(custom_theme.block.h4, jsonString.h4)
custom_theme.block.p = Object.assign(custom_theme.block.p, jsonString.p)
custom_theme.block.blockquote = Object.assign(
custom_theme.block.blockquote,
jsonString.blockquote
)
custom_theme.block.blockquote_p = Object.assign(
custom_theme.block.blockquote_p,
jsonString.blockquote_p
)
custom_theme.block.image = Object.assign(
custom_theme.block.image,
jsonString.image
)
// inline
custom_theme.inline.strong = Object.assign(
custom_theme.inline.strong,
jsonString.strong
)
custom_theme.inline.codespan = Object.assign(
custom_theme.inline.codespan,
jsonString.codespan
)
custom_theme.inline.link = Object.assign(
custom_theme.inline.link,
jsonString.link
)
custom_theme.inline.wx_link = Object.assign(
custom_theme.inline.wx_link,
jsonString.wx_link
)
return custom_theme
}
/**
* 将CSS形式的字符串转换为JSON
*
* @param {css字符串} css
*/
export function css2json (css) {
// 移除CSS所有注释
while (
(open = css.indexOf('/*')) !== -1 &&
(close = css.indexOf('*/')) !== -1
) {
css = css.substring(0, open) + css.substring(close + 2)
}
// 初始化返回值
let json = {}
while (css.length > 0 && css.indexOf('{') !== -1 && css.indexOf('}') !== -1) {
// 存储第一个左/右花括号的下标
const lbracket = css.indexOf('{')
const rbracket = css.indexOf('}')
// 第一步将声明转换为Object
// `font: 'Times New Roman' 1em; color: #ff0000; margin-top: 1em;`
// ==>
// `{"font": "'Times New Roman' 1em", "color": "#ff0000", "margin-top": "1em"}`
// 辅助方法将array转为object
function toObject (array) {
let ret = {}
array.forEach(e => {
const index = e.indexOf(':')
const property = e.substring(0, index).trim()
const value = e.substring(index + 1).trim()
ret[property] = value
})
return ret
}
// 切割声明块并移除空白符,然后放入数组中
let declarations = css
.substring(lbracket + 1, rbracket)
.split(';')
.map(e => e.trim())
.filter(e => e.length > 0) // 移除所有""空值
// 转为Object对象
declarations = toObject(declarations)
// 第二步:选择器处理,每个选择器会与它对应的声明相关联,如:
// `h1, p#bar {color: red}`
// ==>
// {"h1": {color: red}, "p#bar": {color: red}}
let selectors = css
.substring(0, lbracket)
// 以,切割,并移除空格:`"h1, p#bar, span.foo"` => ["h1", "p#bar", "span.foo"]
.split(',')
.map(selector => selector.trim())
// 迭代赋值
selectors.forEach(selector => {
// 若不存在,则先初始化
if (!json[selector]) json[selector] = {}
// 赋值到JSON
Object.keys(declarations).forEach(key => {
json[selector][key] = declarations[key]
})
})
// 继续下个声明块
css = css.slice(rbracket + 1).trim()
}
// 返回JSON形式的结果串
return json
}
/**
* 将编辑器内容保存到 LocalStorage
* @param {*} editor
* @param {*} name
*/
export function saveEditorContent(editor, name) {
const content = editor.getValue(0)
if (content) {
localStorage.setItem(name, content)
} else {
localStorage.removeItem(name)
}
}
export function isImageIllegal(file) {
if (!/\.(gif|jpg|jpeg|png|GIF|JPG|PNG)$/.test(file.name)) {
return '请上传 JPG/PNG/GIF 格式的图片';
}
if (file.size > 5 * 1024 * 1024) {
return '由于公众号限制,图片大小不能超过 5.0M';
}
return false;
}

View File

@ -1,145 +0,0 @@
import Vue from 'vue'
import Vuex from 'vuex'
import config from '../scripts/config';
import WxRenderer from '../scripts/renderers/wx-renderer'
import marked from 'marked'
import CodeMirror from 'codemirror/lib/codemirror'
import DEFAULT_CONTENT from '../scripts/default-content'
import DEFAULT_CSS_CONTENT from '../scripts/themes/default-theme-css'
import {
setColor
} from '../scripts/util'
Vue.use(Vuex)
const state = {
wxRenderer: null,
output: '',
editor: null,
cssEditor: null,
html: '',
currentFont: '',
currentSize: '',
currentColor: '',
citeStatus: 0
};
const mutations = {
setHtml(state, data) {
state.html = data;
},
setEditorValue(state, data) {
state.editor.setValue(data)
},
setCssEditorValue(state, data) {
state.cssEditor.setValue(data)
},
setWxRendererOptions(state, data) {
state.wxRenderer.setOptions(data);
},
setCiteStatus(state, data) {
state.citeStatus = data;
localStorage.setItem('citeStatus', data)
},
setCurrentFont(state, data) {
state.currentFont = data;
localStorage.setItem('fonts', data)
},
setCurrentSize(state, data) {
state.currentSize = data;
localStorage.setItem('size', data)
},
setCurrentColor(state, data) {
state.currentColor = data;
localStorage.setItem('color', data)
},
initEditorState(state) {
state.currentFont = localStorage.getItem('fonts') || config.builtinFonts[0].value
state.currentColor = localStorage.getItem('color') || config.colorOption[1].value
state.currentSize = localStorage.getItem('size') || config.sizeOption[2].value
state.citeStatus = localStorage.getItem('citeStatus') === 'true'
state.wxRenderer = new WxRenderer({
theme: setColor(state.currentColor),
fonts: state.currentFont,
size: state.currentSize,
status: state.citeStatus
})
},
initEditorEntity(state) {
state.editor = CodeMirror.fromTextArea(
document.getElementById('editor'),
{
value: '',
mode: 'text/x-markdown',
theme: 'xq-light',
lineNumbers: false,
lineWrapping: true,
styleActiveLine: true,
autoCloseBrackets: true
}
)
// 如果有编辑器内容被保存则读取,否则加载默认内容
if (localStorage.getItem('__editor_content')) {
state.editor.setValue(localStorage.getItem('__editor_content'))
} else {
state.editor.setValue(DEFAULT_CONTENT)
}
},
initCssEditorEntity(state) {
state.cssEditor = CodeMirror.fromTextArea(
document.getElementById('cssEditor'), {
value: '',
mode: 'css',
theme: 'style-mirror',
lineNumbers: false,
lineWrapping: true,
matchBrackets: true,
autofocus: true,
extraKeys: {
'Ctrl-F': function autoFormat(editor) {
const totalLines = editor.lineCount()
editor.autoFormatRange({
line: 0,
ch: 0
}, {
line: totalLines
})
}
}
}
)
// 如果有编辑器内容被保存则读取,否则加载默认内容
if (localStorage.getItem('__css_content')) {
state.cssEditor.setValue(localStorage.getItem('__css_content'))
} else {
state.cssEditor.setValue(DEFAULT_CSS_CONTENT)
}
},
editorRefresh(state) {
let output = marked(state.editor.getValue(0), {
renderer: state.wxRenderer.getRenderer(state.citeStatus)
})
// 去除第一行的 margin-top
output = output.replace(/(style=".*?)"/, '$1;margin-top: 0"')
if (state.citeStatus) {
// 引用脚注
output += state.wxRenderer.buildFootnotes()
// 附加的一些 style
output += state.wxRenderer.buildAddition()
}
state.output = output
},
clearEditorToDefault(state) {
state.editor.setValue(DEFAULT_CONTENT)
state.cssEditor.setValue(DEFAULT_CSS_CONTENT)
}
}
export default new Vuex.Store({
state,
mutations,
actions: {},
modules: {}
})

View File

@ -1,5 +0,0 @@
<template>
<div class="about">
<h1>This is an about page</h1>
</div>
</template>

View File

@ -1,14 +0,0 @@
<template>
<div class="home">
</div>
</template>
<script>
// @ is an alias to /src
export default {
name: 'home',
components: {
}
}
</script>

View File

@ -1,6 +0,0 @@
import { shallowMount } from '@vue/test-utils'
import HelloWorld from '@/components/HelloWorld.vue'
describe('HelloWorld.vue', () => {
})

View File

@ -1,4 +0,0 @@
module.exports = {
outputDir: 'dist',
publicPath: '/md/'
}

9324
yarn.lock

File diff suppressed because it is too large Load Diff