clean
@ -1,2 +0,0 @@
|
|||||||
> 1%
|
|
||||||
last 2 versions
|
|
@ -1,5 +0,0 @@
|
|||||||
[*.{js,jsx,ts,tsx,vue}]
|
|
||||||
indent_style = space
|
|
||||||
indent_size = 4
|
|
||||||
trim_trailing_whitespace = true
|
|
||||||
insert_final_newline = true
|
|
@ -1,2 +0,0 @@
|
|||||||
*.js
|
|
||||||
*.vue
|
|
30
.eslintrc.js
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
26
.github/workflows/build.yml
vendored
@ -1,26 +0,0 @@
|
|||||||
name: build
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ master ]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout 🛎️
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Install and Build 🔧
|
|
||||||
run: |
|
|
||||||
npm install
|
|
||||||
npm run build
|
|
||||||
- name: deploy 🚀
|
|
||||||
uses: JamesIves/github-pages-deploy-action@master
|
|
||||||
env:
|
|
||||||
ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
|
||||||
BRANCH: gh-pages
|
|
||||||
FOLDER: dist
|
|
||||||
BUILD_SCRIPT: npm install && npm run build
|
|
17
.github/workflows/sync.yml
vendored
@ -1,17 +0,0 @@
|
|||||||
name: Sync
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ master ]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- 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"
|
|
44
.gitignore
vendored
@ -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
@ -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.
|
|
115
README.md
@ -1,115 +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 Pages:https://doocs.gitee.io/md
|
|
||||||
- GitHub Pages:https://doocs.github.io/md
|
|
||||||
|
|
||||||
注:推荐使用 Chrome 浏览器,效果最佳。另外,对于国内(中国)的朋友,访问 [Gitee Pages](https://doocs.gitee.io/md) 速度会相对快一些。
|
|
||||||
|
|
||||||
## 为何二次开发
|
|
||||||
现有的开源微信 Markdown 编辑器,样式繁杂,也不符合我个人的审美需求。在我使用它们进行文章排版的时候,经常还要自己做一些改动,费时费力,因此动手做了二次开发。
|
|
||||||
|
|
||||||
欢迎各位朋友随时提交 PR,让这款微信 Markdown 编辑器变得更好!如果你有新的想法,也欢迎在 Issues 区反馈。
|
|
||||||
|
|
||||||
## 功能特性
|
|
||||||
- [x] 支持 Markdown 所有基础语法
|
|
||||||
- [x] 支持单独进行字体、字号设置
|
|
||||||
- [x] 支持色盘取色,并一键替换颜色
|
|
||||||
- [x] 支持链接微信图文,外链自动转为文末索引
|
|
||||||
- [x] 支持一键复制并粘贴到公众号后台
|
|
||||||
- [x] 支持多图上传并将 URL 插入编辑器光标定位处
|
|
||||||
- [x] 支持一键下载 Markdown 文档到本地
|
|
||||||
- [x] 支持自定义 CSS 样式并实时渲染
|
|
||||||
- [x] 支持一键恢复至默认内容及样式
|
|
||||||
- [x] 支持打开或关闭引用链接的选项
|
|
||||||
|
|
||||||
![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)
|
|
@ -1,5 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
presets: [
|
|
||||||
'@vue/cli-plugin-babel/preset'
|
|
||||||
]
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
preset: '@vue/cli-plugin-unit-jest'
|
|
||||||
}
|
|
41
package.json
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
Before Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 2.5 MiB |
Before Width: | Height: | Size: 852 KiB |
Before Width: | Height: | Size: 1.6 MiB |
Before Width: | Height: | Size: 2.7 MiB |
Before Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 190 KiB |
Before Width: | Height: | Size: 67 KiB |
Before Width: | Height: | Size: 57 KiB |
Before Width: | Height: | Size: 5.7 KiB |
Before Width: | Height: | Size: 6.4 KiB |
Before Width: | Height: | Size: 51 KiB |
Before Width: | Height: | Size: 1.4 MiB |
Before Width: | Height: | Size: 65 KiB |
Before Width: | Height: | Size: 70 KiB |
@ -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>
|
|
||||||
|
|
||||||
`
|
|
@ -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, "<");
|
|
||||||
text = text.replace(/>/g, ">");
|
|
||||||
|
|
||||||
let lines = text.split('\n');
|
|
||||||
let codeLines = [];
|
|
||||||
let numbers = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
|
||||||
const line = lines[i];
|
|
||||||
codeLines.push(`<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;
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,41 +0,0 @@
|
|||||||
const DEFAULT_CSS_CONTENT =
|
|
||||||
`/*
|
|
||||||
按Ctrl+F可格式化
|
|
||||||
*/
|
|
||||||
/* 一级标题样式 */
|
|
||||||
h1 {
|
|
||||||
}
|
|
||||||
/* 二级标题样式 */
|
|
||||||
h2 {
|
|
||||||
}
|
|
||||||
/* 三级标题样式 */
|
|
||||||
h3 {
|
|
||||||
}
|
|
||||||
/* 四级标题样式 */
|
|
||||||
h4 {
|
|
||||||
}
|
|
||||||
/* 图片样式 */
|
|
||||||
image {
|
|
||||||
}
|
|
||||||
/* 引用样式 */
|
|
||||||
blockquote {
|
|
||||||
}
|
|
||||||
/* 引用段落样式 */
|
|
||||||
blockquote_p {
|
|
||||||
}
|
|
||||||
/* 段落样式 */
|
|
||||||
p {
|
|
||||||
}
|
|
||||||
/* 行内代码样式 */
|
|
||||||
codespan {
|
|
||||||
}
|
|
||||||
/* 粗体样式 */
|
|
||||||
strong {
|
|
||||||
}
|
|
||||||
/* 链接样式 */
|
|
||||||
link {
|
|
||||||
}
|
|
||||||
/* 微信链接样式 */
|
|
||||||
wx_link {
|
|
||||||
}
|
|
||||||
`
|
|
@ -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'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
Before Width: | Height: | Size: 4.2 KiB |
@ -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>
|
|
11
public/libs/css/animate.css
vendored
@ -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}}
|
|
@ -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}}
|
|
@ -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}}
|
|
2
public/libs/css/code-themes/tomorrow.min.css
vendored
@ -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}}
|
|
2
public/libs/css/codemirror.min.css
vendored
@ -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;
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
2
public/libs/css/xq-light.min.css
vendored
@ -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 */
|
|
37
src/App.vue
@ -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>
|
|
@ -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;
|
|
@ -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
|
|
||||||
};
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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
|
|
20
src/main.js
@ -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')
|
|
@ -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
|
|
@ -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);
|
|
@ -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
|
|
||||||
}
|
|
||||||
};
|
|
@ -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;
|
|
||||||
}
|
|
@ -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
|
|
@ -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))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})()
|
|
@ -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, '<')
|
|
||||||
text = text.replace(/>/g, '>')
|
|
||||||
|
|
||||||
let lines = text.split('\n')
|
|
||||||
let codeLines = []
|
|
||||||
let numbers = []
|
|
||||||
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
|
||||||
const line = lines[i]
|
|
||||||
codeLines.push(`<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
|
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
@ -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
|
|
@ -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'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
@ -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;
|
|
||||||
}
|
|
@ -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: {}
|
|
||||||
})
|
|
@ -1,5 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="about">
|
|
||||||
<h1>This is an about page</h1>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
@ -1,14 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="home">
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// @ is an alias to /src
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'home',
|
|
||||||
components: {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
@ -1,6 +0,0 @@
|
|||||||
import { shallowMount } from '@vue/test-utils'
|
|
||||||
import HelloWorld from '@/components/HelloWorld.vue'
|
|
||||||
|
|
||||||
describe('HelloWorld.vue', () => {
|
|
||||||
|
|
||||||
})
|
|
@ -1,4 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
outputDir: 'dist',
|
|
||||||
publicPath: '/md/'
|
|
||||||
}
|
|