Merge branch 'm_create_vue'

This commit is contained in:
JimQing 2020-05-02 18:50:16 +08:00
commit 8a8406059a
88 changed files with 11380 additions and 2297 deletions

2
.browserslistrc Normal file
View File

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

5
.editorconfig Normal file
View File

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

2
.eslintignore Normal file
View File

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

30
.eslintrc.js Normal file
View File

@ -0,0 +1,30 @@
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
}
}
]
}

15
.gitignore vendored
View File

@ -27,3 +27,18 @@ 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?

View File

@ -1,6 +1,6 @@
<p align="center">
<a href="https://github.com/doocs/md">
<img src="./assets/images/logo-2.png">
<img src="https://imgkr.cn-bj.ufileos.com/f3accc83-b854-4e99-afb5-8a6465e1d84f.png" alt="">
</a>
</p>
<h1 align="center">微信 Markdown 编辑器</h1>

View File

@ -1,348 +0,0 @@
let app = new Vue({
el: '#app',
data: function () {
let d = {
aboutOutput: '',
output: '',
source: '',
editor: null,
cssEditor: null,
builtinFonts: [
{ label: '无衬线', value: "-apple-system-font,BlinkMacSystemFont, Helvetica Neue, PingFang SC, Hiragino Sans GB , Microsoft YaHei UI , Microsoft YaHei ,Arial,sans-serif" },
{ label: '衬线', value: "Optima-Regular, Optima, PingFangSC-light, PingFangTC-light, 'PingFang SC', Cambria, Cochin, Georgia, Times, 'Times New Roman', serif" }
],
sizeOption: [
{ label: '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: '热情活泼' }
],
showBox: true,
aboutDialogVisible: false,
dialogFormVisible: false,
form: {
rows: 1,
cols: 1
}
};
d.currentFont = d.builtinFonts[0].value;
d.currentSize = d.sizeOption[2].value;
d.currentColor = d.colorOption[1].value;
d.status = '1';
return d;
},
mounted() {
this.currentFont = localStorage.getItem('fonts') || this.builtinFonts[0].value;
this.currentColor = localStorage.getItem('color') || this.colorOption[1].value;
this.currentSize = localStorage.getItem('size') || this.sizeOption[2].value;
this.status = localStorage.getItem('status') === 'true';
this.showBox = false
this.editor = CodeMirror.fromTextArea(
document.getElementById('editor'),
{
mode: 'text/x-markdown',
theme: 'xq-light',
lineNumbers: false,
lineWrapping: true,
styleActiveLine: true,
autoCloseBrackets: true
}
);
this.cssEditor = CodeMirror.fromTextArea(
document.getElementById('cssEditor'), {
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 });
}
},
}
);
// 自动提示
this.cssEditor.on('keyup', (cm, e) => {
if ((e.keyCode >= 65 && e.keyCode <= 90) || e.keyCode === 189) {
cm.showHint(e);
}
});
this.editor.on('change', (cm, e) => {
this.refresh();
this.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();
if (!(this.checkType(pasteFile) && this.checkImageSize(pasteFile))) {
return;
}
let data = new FormData();
data.append('file', pasteFile);
axios.post('https://imgkr.com/api/files/upload', data, {
headers: {
'Content-Type': 'multipart/form-data'
}
}).then(resp => {
this.uploaded(resp.data)
}).catch(err => { })
}
}
});
this.cssEditor.on('update', (instance) => {
this.cssChanged();
this.saveEditorContent(this.cssEditor, '__css_content');
});
this.wxRenderer = new WxRenderer({
theme: setColor(this.currentColor),
fonts: this.currentFont,
size: this.currentSize,
status: this.status
});
// 如果有编辑器内容被保存则读取,否则加载默认内容
this.loadLocalStorage(this.editor, '__editor_content', DEFAULT_CONTENT);
this.loadLocalStorage(this.cssEditor, '__css_content', DEFAULT_CSS_CONTENT);
},
methods: {
renderWeChat(source) {
let output = marked(source, { renderer: this.wxRenderer.getRenderer(this.status) });
// 去除第一行的 margin-top
output = output.replace(/(style=".*?)"/, '$1;margin-top: 0"');
if (this.status) {
// 引用脚注
output += this.wxRenderer.buildFootnotes();
// 附加的一些 style
output += this.wxRenderer.buildAddition();
}
return output;
},
editorThemeChanged(editorTheme) {
this.editor.setOption('theme', editorTheme);
},
fontChanged(fonts) {
this.wxRenderer.setOptions({
fonts: fonts
});
this.currentFont = fonts;
localStorage.setItem('fonts', fonts);
this.refresh();
},
sizeChanged(size) {
this.wxRenderer.setOptions({
size: size
});
let theme = setFontSize(size.replace('px', ''));
theme = setColorWithCustomTemplate(theme, this.currentColor);
this.wxRenderer.setOptions({
theme: theme
});
this.currentSize = size;
localStorage.setItem('size', size);
this.refresh();
},
colorChanged(color) {
let theme = setFontSize(this.currentSize.replace('px', ''));
theme = setColorWithCustomTemplate(theme, color);
this.wxRenderer.setOptions({
theme: theme
});
this.currentColor = color;
localStorage.setItem('color', color);
this.refresh();
},
cssChanged() {
let json = css2json(this.cssEditor.getValue(0));
let theme = setFontSize(this.currentSize.replace('px', ''));
theme = customCssWithTemplate(json, this.currentColor, theme);
this.wxRenderer.setOptions({
theme: theme
});
this.refresh();
},
// 图片上传前的处理
beforeUpload(file) {
return this.checkType(file) && this.checkImageSize(file);
},
// 检查文件类型
checkType(file) {
if (!/\.(gif|jpg|jpeg|png|GIF|JPG|PNG)$/.test(file.name)) {
this.$message({
showClose: true,
message: '请上传 JPG/PNG/GIF 格式的图片',
type: 'error'
});
return false;
}
return true;
},
// 检查图片大小
checkImageSize(file) {
if (file.size > 5 * 1024 * 1024) {
this.$message({
showClose: true,
message: '由于公众号限制,图片大小不能超过 5.0M',
type: 'error'
});
return false;
}
return true;
},
// 图片上传结束
uploaded(response, file, fileList) {
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.refresh();
} else {
// 上传失败
this.$message({
showClose: true,
message: response.message,
type: 'error'
});
}
},
// 刷新右侧预览
refresh() {
this.output = this.renderWeChat(this.editor.getValue(0));
},
// 重置页面
reset() {
this.$confirm('此操作将丢失本地缓存的文本和自定义样式,是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
confirmButtonClass: 'el-button--success',
cancelButtonClass: 'el-button--success is-plain',
type: 'warning',
center: true
}).then(() => {
localStorage.clear()
this.editor.setValue(DEFAULT_CONTENT);
this.cssEditor.setValue(DEFAULT_CSS_CONTENT);
this.editor.focus();
this.status = '1';
this.fontChanged(this.builtinFonts[0].value);
this.colorChanged(this.colorOption[1].value);
this.sizeChanged(this.sizeOption[2].value);
this.cssChanged()
}).catch(() => {
this.editor.focus();
});
},
// 插入表格
insertTable() {
const cursor = this.editor.getCursor();
const rows = parseInt(this.form.rows);
const cols = parseInt(this.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.dialogFormVisible = false
this.refresh();
},
statusChanged() {
localStorage.setItem('status', this.status);
this.refresh();
},
// 将编辑器内容保存到 LocalStorage
saveEditorContent(editor, name) {
const content = editor.getValue(0);
if (content) {
localStorage.setItem(name, content);
} else {
localStorage.removeItem(name);
}
},
loadLocalStorage(editor, name, content) {
if (localStorage.getItem(name)) {
editor.setValue(localStorage.getItem(name));
} else {
editor.setValue(content);
}
},
// 下载编辑器内容到本地
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);
},
// 自定义CSS样式
async customStyle() {
this.showBox = !this.showBox;
let flag = await localStorage.getItem('__css_content')
if (!flag) {
this.cssEditor.setValue(DEFAULT_CSS_CONTENT);
}
},
// 复制渲染后的内容到剪贴板
copy() {
let clipboardDiv = document.getElementById('output');
clipboardDiv.focus();
window.getSelection().removeAllRanges();
let range = document.createRange();
range.setStartBefore(clipboardDiv.firstChild);
range.setEndAfter(clipboardDiv.lastChild);
window.getSelection().addRange(range);
document.execCommand('copy')
// 输出提示
this.$notify({
showClose: true,
message: '已复制渲染后的文章到剪贴板,可直接到公众号后台粘贴',
offset: 80,
duration: 1600,
type: 'success'
});
}
},
updated() {
this.$nextTick(() => {
prettyPrint();
})
}
});

5
babel.config.js Normal file
View File

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

View File

@ -1,224 +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="assets/images/favicon.png">
<link rel="apple-touch-icon-precomposed" href="assets/images/favicon.png">
<link rel="stylesheet" href="assets/css/loading.css">
<link rel="stylesheet" href="libs/css/index.css">
<link rel="stylesheet" href="libs/css/xq-light.min.css">
<link rel="stylesheet" href="libs/css/code-themes/github-v2.min.css">
<!-- codemirror -->
<link rel="stylesheet" href="libs/css/codemirror.min.css">
<link rel="stylesheet" href="libs/css/show-hint.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">
<!-- 默认CSS/JS -->
<script src="assets/scripts/themes/default-theme-css.js"></script>
<script src="assets/scripts/default-content.js"></script>
</head>
<body>
<!--loading 界面-->
<div class="loading" id="loading">
<div class="loading-wrapper">
<div class="loading-text">Loading...</div>
<div class="loading-anim"></div>
</div>
</div>
<!--应用主体-->
<div id="app" class="container">
<el-container>
<el-header class="top">
<!-- 图片上传 -->
<el-upload 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" :on-success="uploaded">
<el-tooltip class="item" effect="dark" content="上传图片" placement="bottom-start">
<i class="el-icon-upload" size="medium">&nbsp;</i>
</el-tooltip>
</el-upload>
<!-- 下载文本文档 -->
<el-tooltip class="item" effect="dark" content="下载编辑框Markdown文档" placement="bottom-start">
<i class="el-icon-download" size="medium" @click="downloadEditorContent">&nbsp;</i>
</el-tooltip>
<!-- 页面重置 -->
<el-tooltip class="item" effect="dark" content="重置页面" placement="bottom-start">
<i class="el-icon-refresh" size="medium" @click="reset">&nbsp;</i>
</el-tooltip>
<!-- 插入表格 -->
<el-tooltip class="item" effect="dark" content="插入表格" placement="bottom-start">
<i class="el-icon-s-grid" size="medium" @click="dialogFormVisible = true">&nbsp;</i>
</el-tooltip>
<el-form size="mini" class="ctrl" :inline=true>
<el-form-item>
<el-select v-model="currentFont" size="mini" placeholder="选择字体" clearable @change="fontChanged">
<el-option v-for="font in builtinFonts" :style="{fontFamily: font.value}" :key="font.value"
:label="font.label" :value="font.value">
<span class="select-item-left">{{ font.label }}</span>
<span class="select-item-right">Abc</span>
</el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-select v-model="currentSize" size="mini" placeholder="选择段落字号" clearable @change="sizeChanged">
<el-option v-for="size in sizeOption" :key="size.value" :label="size.label" :value="size.value">
<span class="select-item-left">{{ size.label }}</span>
<span class="select-item-right">{{ size.desc }}</span>
</el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-select v-model="currentColor" size="mini" placeholder="选择颜色" clearable @change="colorChanged">
<el-option v-for="color in 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="currentColor" size="mini" show-alpha @change="colorChanged"></el-color-picker>
</el-tooltip>
&nbsp;&nbsp;
<el-tooltip content="微信外链自动转为文末引用" placement="top">
<el-switch v-model="status" 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="aboutDialogVisible = true">关于</el-button>
</el-header>
<el-main class="main-body">
<el-row :gutter="10" class="main-section">
<el-col :span="12">
<textarea id="editor" type="textarea" placeholder="Your markdown text here." v-model="source">
</textarea>
</el-col>
<el-col :span="12" class="preview-wrapper" id="preview">
<section>
<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>
<el-dialog title="关于" :visible.sync="aboutDialogVisible" 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">
<a href="https://github.com/doocs/md" target="_blank">
<el-button type="success" plain>GitHub 仓库</el-button>
</a>
<a href="https://gitee.com/doocs/md" target="_blank">
<el-button type="success" plain>Gitee 仓库</el-button>
</a>
</span>
</el-dialog>
<el-dialog title="插入表格" :visible.sync="dialogFormVisible">
<el-form :model="form">
<el-form-item label="行数(表头不计入行数)">
<el-input v-model="form.rows"></el-input>
</el-form-item>
<el-form-item label="列数">
<el-input v-model="form.cols"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="success" plain @click="dialogFormVisible = false">取 消</el-button>
<el-button type="success" @click="insertTable">确 定</el-button>
</div>
</el-dialog>
</div>
<script src="libs/scripts/vue.min.js"></script>
<script src="libs/scripts/axios.min.js"></script>
<script src="libs/scripts/marked.min.js"></script>
<!-- codemirror -->
<script src="libs/scripts/codemirror/codemirror.min.js"></script>
<script src="libs/scripts/codemirror/css.js"></script>
<script src="libs/scripts/codemirror/matchbrackets.js"></script>
<script src="libs/scripts/codemirror/active-line.js"></script>
<script src="libs/scripts/codemirror/show-hint.js"></script>
<script src="libs/scripts/codemirror/css-hint.js"></script>
<script src="libs/scripts/codemirror/format.js"></script>
<script src="libs/scripts/markdown.min.js"></script>
<script src="libs/scripts/prettify.min.js"></script>
<script src="libs/scripts/index.js"></script>
<script src="libs/scripts/jquery.min.js"></script>
<script src="libs/scripts/closebrackets.js"></script>
<script src="assets/scripts/sync-scroll.js"></script>
<script src="assets/scripts/themes/default-theme.js"></script>
<script src="assets/scripts/renderers/wx-renderer.js"></script>
<script src="assets/scripts/util.js"></script>
<script src="assets/scripts/editor.js"></script>
<script>
$('#loading').hide();
window.console
&& window.console.log
&& (console.log("Think big, train fast, learn deep. See https://github.com/yanglbme"))
</script>
</body>
</html>

3
jest.config.js Normal file
View File

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

File diff suppressed because one or more lines are too long

View File

@ -1,72 +0,0 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: https://codemirror.net/LICENSE
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../../lib/codemirror"));
else if (typeof define == "function" && define.amd) // AMD
define(["../../lib/codemirror"], mod);
else // Plain browser env
mod(CodeMirror);
})(function(CodeMirror) {
"use strict";
var WRAP_CLASS = "CodeMirror-activeline";
var BACK_CLASS = "CodeMirror-activeline-background";
var GUTT_CLASS = "CodeMirror-activeline-gutter";
CodeMirror.defineOption("styleActiveLine", false, function(cm, val, old) {
var prev = old == CodeMirror.Init ? false : old;
if (val == prev) return
if (prev) {
cm.off("beforeSelectionChange", selectionChange);
clearActiveLines(cm);
delete cm.state.activeLines;
}
if (val) {
cm.state.activeLines = [];
updateActiveLines(cm, cm.listSelections());
cm.on("beforeSelectionChange", selectionChange);
}
});
function clearActiveLines(cm) {
for (var i = 0; i < cm.state.activeLines.length; i++) {
cm.removeLineClass(cm.state.activeLines[i], "wrap", WRAP_CLASS);
cm.removeLineClass(cm.state.activeLines[i], "background", BACK_CLASS);
cm.removeLineClass(cm.state.activeLines[i], "gutter", GUTT_CLASS);
}
}
function sameArray(a, b) {
if (a.length != b.length) return false;
for (var i = 0; i < a.length; i++)
if (a[i] != b[i]) return false;
return true;
}
function updateActiveLines(cm, ranges) {
var active = [];
for (var i = 0; i < ranges.length; i++) {
var range = ranges[i];
var option = cm.getOption("styleActiveLine");
if (typeof option == "object" && option.nonEmpty ? range.anchor.line != range.head.line : !range.empty())
continue
var line = cm.getLineHandleVisualStart(range.head.line);
if (active[active.length - 1] != line) active.push(line);
}
if (sameArray(cm.state.activeLines, active)) return;
cm.operation(function() {
clearActiveLines(cm);
for (var i = 0; i < active.length; i++) {
cm.addLineClass(active[i], "wrap", WRAP_CLASS);
cm.addLineClass(active[i], "background", BACK_CLASS);
cm.addLineClass(active[i], "gutter", GUTT_CLASS);
}
cm.state.activeLines = active;
});
}
function selectionChange(cm, sel) {
updateActiveLines(cm, sel.ranges);
}
});

File diff suppressed because one or more lines are too long

View File

@ -1,60 +0,0 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: https://codemirror.net/LICENSE
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../../lib/codemirror"), require("../../mode/css/css"));
else if (typeof define == "function" && define.amd) // AMD
define(["../../lib/codemirror", "../../mode/css/css"], mod);
else // Plain browser env
mod(CodeMirror);
})(function(CodeMirror) {
"use strict";
var pseudoClasses = {link: 1, visited: 1, active: 1, hover: 1, focus: 1,
"first-letter": 1, "first-line": 1, "first-child": 1,
before: 1, after: 1, lang: 1};
CodeMirror.registerHelper("hint", "css", function(cm) {
var cur = cm.getCursor(), token = cm.getTokenAt(cur);
var inner = CodeMirror.innerMode(cm.getMode(), token.state);
if (inner.mode.name != "css") return;
if (token.type == "keyword" && "!important".indexOf(token.string) == 0)
return {list: ["!important"], from: CodeMirror.Pos(cur.line, token.start),
to: CodeMirror.Pos(cur.line, token.end)};
var start = token.start, end = cur.ch, word = token.string.slice(0, end - start);
if (/[^\w$_-]/.test(word)) {
word = ""; start = end = cur.ch;
}
var spec = CodeMirror.resolveMode("text/css");
var result = [];
function add(keywords) {
for (var name in keywords)
if (!word || name.lastIndexOf(word, 0) == 0)
result.push(name);
}
var st = inner.state.state;
if (st == "pseudo" || token.type == "variable-3") {
add(pseudoClasses);
} else if (st == "block" || st == "maybeprop") {
add(spec.propertyKeywords);
} else if (st == "prop" || st == "parens" || st == "at" || st == "params") {
add(spec.valueKeywords);
add(spec.colorKeywords);
} else if (st == "media" || st == "media_parens") {
add(spec.mediaTypes);
add(spec.mediaFeatures);
}
if (result.length) return {
list: result,
from: CodeMirror.Pos(cur.line, start),
to: CodeMirror.Pos(cur.line, end)
};
});
});

View File

@ -1,831 +0,0 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: https://codemirror.net/LICENSE
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../../lib/codemirror"));
else if (typeof define == "function" && define.amd) // AMD
define(["../../lib/codemirror"], mod);
else // Plain browser env
mod(CodeMirror);
})(function(CodeMirror) {
"use strict";
CodeMirror.defineMode("css", function(config, parserConfig) {
var inline = parserConfig.inline
if (!parserConfig.propertyKeywords) parserConfig = CodeMirror.resolveMode("text/css");
var indentUnit = config.indentUnit,
tokenHooks = parserConfig.tokenHooks,
documentTypes = parserConfig.documentTypes || {},
mediaTypes = parserConfig.mediaTypes || {},
mediaFeatures = parserConfig.mediaFeatures || {},
mediaValueKeywords = parserConfig.mediaValueKeywords || {},
propertyKeywords = parserConfig.propertyKeywords || {},
nonStandardPropertyKeywords = parserConfig.nonStandardPropertyKeywords || {},
fontProperties = parserConfig.fontProperties || {},
counterDescriptors = parserConfig.counterDescriptors || {},
colorKeywords = parserConfig.colorKeywords || {},
valueKeywords = parserConfig.valueKeywords || {},
allowNested = parserConfig.allowNested,
lineComment = parserConfig.lineComment,
supportsAtComponent = parserConfig.supportsAtComponent === true;
var type, override;
function ret(style, tp) { type = tp; return style; }
// Tokenizers
function tokenBase(stream, state) {
var ch = stream.next();
if (tokenHooks[ch]) {
var result = tokenHooks[ch](stream, state);
if (result !== false) return result;
}
if (ch == "@") {
stream.eatWhile(/[\w\\\-]/);
return ret("def", stream.current());
} else if (ch == "=" || (ch == "~" || ch == "|") && stream.eat("=")) {
return ret(null, "compare");
} else if (ch == "\"" || ch == "'") {
state.tokenize = tokenString(ch);
return state.tokenize(stream, state);
} else if (ch == "#") {
stream.eatWhile(/[\w\\\-]/);
return ret("atom", "hash");
} else if (ch == "!") {
stream.match(/^\s*\w*/);
return ret("keyword", "important");
} else if (/\d/.test(ch) || ch == "." && stream.eat(/\d/)) {
stream.eatWhile(/[\w.%]/);
return ret("number", "unit");
} else if (ch === "-") {
if (/[\d.]/.test(stream.peek())) {
stream.eatWhile(/[\w.%]/);
return ret("number", "unit");
} else if (stream.match(/^-[\w\\\-]*/)) {
stream.eatWhile(/[\w\\\-]/);
if (stream.match(/^\s*:/, false))
return ret("variable-2", "variable-definition");
return ret("variable-2", "variable");
} else if (stream.match(/^\w+-/)) {
return ret("meta", "meta");
}
} else if (/[,+>*\/]/.test(ch)) {
return ret(null, "select-op");
} else if (ch == "." && stream.match(/^-?[_a-z][_a-z0-9-]*/i)) {
return ret("qualifier", "qualifier");
} else if (/[:;{}\[\]\(\)]/.test(ch)) {
return ret(null, ch);
} else if (stream.match(/[\w-.]+(?=\()/)) {
if (/^(url(-prefix)?|domain|regexp)$/.test(stream.current().toLowerCase())) {
state.tokenize = tokenParenthesized;
}
return ret("variable callee", "variable");
} else if (/[\w\\\-]/.test(ch)) {
stream.eatWhile(/[\w\\\-]/);
return ret("property", "word");
} else {
return ret(null, null);
}
}
function tokenString(quote) {
return function(stream, state) {
var escaped = false, ch;
while ((ch = stream.next()) != null) {
if (ch == quote && !escaped) {
if (quote == ")") stream.backUp(1);
break;
}
escaped = !escaped && ch == "\\";
}
if (ch == quote || !escaped && quote != ")") state.tokenize = null;
return ret("string", "string");
};
}
function tokenParenthesized(stream, state) {
stream.next(); // Must be '('
if (!stream.match(/\s*[\"\')]/, false))
state.tokenize = tokenString(")");
else
state.tokenize = null;
return ret(null, "(");
}
// Context management
function Context(type, indent, prev) {
this.type = type;
this.indent = indent;
this.prev = prev;
}
function pushContext(state, stream, type, indent) {
state.context = new Context(type, stream.indentation() + (indent === false ? 0 : indentUnit), state.context);
return type;
}
function popContext(state) {
if (state.context.prev)
state.context = state.context.prev;
return state.context.type;
}
function pass(type, stream, state) {
return states[state.context.type](type, stream, state);
}
function popAndPass(type, stream, state, n) {
for (var i = n || 1; i > 0; i--)
state.context = state.context.prev;
return pass(type, stream, state);
}
// Parser
function wordAsValue(stream) {
var word = stream.current().toLowerCase();
if (valueKeywords.hasOwnProperty(word))
override = "atom";
else if (colorKeywords.hasOwnProperty(word))
override = "keyword";
else
override = "variable";
}
var states = {};
states.top = function(type, stream, state) {
if (type == "{") {
return pushContext(state, stream, "block");
} else if (type == "}" && state.context.prev) {
return popContext(state);
} else if (supportsAtComponent && /@component/i.test(type)) {
return pushContext(state, stream, "atComponentBlock");
} else if (/^@(-moz-)?document$/i.test(type)) {
return pushContext(state, stream, "documentTypes");
} else if (/^@(media|supports|(-moz-)?document|import)$/i.test(type)) {
return pushContext(state, stream, "atBlock");
} else if (/^@(font-face|counter-style)/i.test(type)) {
state.stateArg = type;
return "restricted_atBlock_before";
} else if (/^@(-(moz|ms|o|webkit)-)?keyframes$/i.test(type)) {
return "keyframes";
} else if (type && type.charAt(0) == "@") {
return pushContext(state, stream, "at");
} else if (type == "hash") {
override = "builtin";
} else if (type == "word") {
override = "tag";
} else if (type == "variable-definition") {
return "maybeprop";
} else if (type == "interpolation") {
return pushContext(state, stream, "interpolation");
} else if (type == ":") {
return "pseudo";
} else if (allowNested && type == "(") {
return pushContext(state, stream, "parens");
}
return state.context.type;
};
states.block = function(type, stream, state) {
if (type == "word") {
var word = stream.current().toLowerCase();
if (propertyKeywords.hasOwnProperty(word)) {
override = "property";
return "maybeprop";
} else if (nonStandardPropertyKeywords.hasOwnProperty(word)) {
override = "string-2";
return "maybeprop";
} else if (allowNested) {
override = stream.match(/^\s*:(?:\s|$)/, false) ? "property" : "tag";
return "block";
} else {
override += " error";
return "maybeprop";
}
} else if (type == "meta") {
return "block";
} else if (!allowNested && (type == "hash" || type == "qualifier")) {
override = "error";
return "block";
} else {
return states.top(type, stream, state);
}
};
states.maybeprop = function(type, stream, state) {
if (type == ":") return pushContext(state, stream, "prop");
return pass(type, stream, state);
};
states.prop = function(type, stream, state) {
if (type == ";") return popContext(state);
if (type == "{" && allowNested) return pushContext(state, stream, "propBlock");
if (type == "}" || type == "{") return popAndPass(type, stream, state);
if (type == "(") return pushContext(state, stream, "parens");
if (type == "hash" && !/^#([0-9a-fA-f]{3,4}|[0-9a-fA-f]{6}|[0-9a-fA-f]{8})$/.test(stream.current())) {
override += " error";
} else if (type == "word") {
wordAsValue(stream);
} else if (type == "interpolation") {
return pushContext(state, stream, "interpolation");
}
return "prop";
};
states.propBlock = function(type, _stream, state) {
if (type == "}") return popContext(state);
if (type == "word") { override = "property"; return "maybeprop"; }
return state.context.type;
};
states.parens = function(type, stream, state) {
if (type == "{" || type == "}") return popAndPass(type, stream, state);
if (type == ")") return popContext(state);
if (type == "(") return pushContext(state, stream, "parens");
if (type == "interpolation") return pushContext(state, stream, "interpolation");
if (type == "word") wordAsValue(stream);
return "parens";
};
states.pseudo = function(type, stream, state) {
if (type == "meta") return "pseudo";
if (type == "word") {
override = "variable-3";
return state.context.type;
}
return pass(type, stream, state);
};
states.documentTypes = function(type, stream, state) {
if (type == "word" && documentTypes.hasOwnProperty(stream.current())) {
override = "tag";
return state.context.type;
} else {
return states.atBlock(type, stream, state);
}
};
states.atBlock = function(type, stream, state) {
if (type == "(") return pushContext(state, stream, "atBlock_parens");
if (type == "}" || type == ";") return popAndPass(type, stream, state);
if (type == "{") return popContext(state) && pushContext(state, stream, allowNested ? "block" : "top");
if (type == "interpolation") return pushContext(state, stream, "interpolation");
if (type == "word") {
var word = stream.current().toLowerCase();
if (word == "only" || word == "not" || word == "and" || word == "or")
override = "keyword";
else if (mediaTypes.hasOwnProperty(word))
override = "attribute";
else if (mediaFeatures.hasOwnProperty(word))
override = "property";
else if (mediaValueKeywords.hasOwnProperty(word))
override = "keyword";
else if (propertyKeywords.hasOwnProperty(word))
override = "property";
else if (nonStandardPropertyKeywords.hasOwnProperty(word))
override = "string-2";
else if (valueKeywords.hasOwnProperty(word))
override = "atom";
else if (colorKeywords.hasOwnProperty(word))
override = "keyword";
else
override = "error";
}
return state.context.type;
};
states.atComponentBlock = function(type, stream, state) {
if (type == "}")
return popAndPass(type, stream, state);
if (type == "{")
return popContext(state) && pushContext(state, stream, allowNested ? "block" : "top", false);
if (type == "word")
override = "error";
return state.context.type;
};
states.atBlock_parens = function(type, stream, state) {
if (type == ")") return popContext(state);
if (type == "{" || type == "}") return popAndPass(type, stream, state, 2);
return states.atBlock(type, stream, state);
};
states.restricted_atBlock_before = function(type, stream, state) {
if (type == "{")
return pushContext(state, stream, "restricted_atBlock");
if (type == "word" && state.stateArg == "@counter-style") {
override = "variable";
return "restricted_atBlock_before";
}
return pass(type, stream, state);
};
states.restricted_atBlock = function(type, stream, state) {
if (type == "}") {
state.stateArg = null;
return popContext(state);
}
if (type == "word") {
if ((state.stateArg == "@font-face" && !fontProperties.hasOwnProperty(stream.current().toLowerCase())) ||
(state.stateArg == "@counter-style" && !counterDescriptors.hasOwnProperty(stream.current().toLowerCase())))
override = "error";
else
override = "property";
return "maybeprop";
}
return "restricted_atBlock";
};
states.keyframes = function(type, stream, state) {
if (type == "word") { override = "variable"; return "keyframes"; }
if (type == "{") return pushContext(state, stream, "top");
return pass(type, stream, state);
};
states.at = function(type, stream, state) {
if (type == ";") return popContext(state);
if (type == "{" || type == "}") return popAndPass(type, stream, state);
if (type == "word") override = "tag";
else if (type == "hash") override = "builtin";
return "at";
};
states.interpolation = function(type, stream, state) {
if (type == "}") return popContext(state);
if (type == "{" || type == ";") return popAndPass(type, stream, state);
if (type == "word") override = "variable";
else if (type != "variable" && type != "(" && type != ")") override = "error";
return "interpolation";
};
return {
startState: function(base) {
return {tokenize: null,
state: inline ? "block" : "top",
stateArg: null,
context: new Context(inline ? "block" : "top", base || 0, null)};
},
token: function(stream, state) {
if (!state.tokenize && stream.eatSpace()) return null;
var style = (state.tokenize || tokenBase)(stream, state);
if (style && typeof style == "object") {
type = style[1];
style = style[0];
}
override = style;
if (type != "comment")
state.state = states[state.state](type, stream, state);
return override;
},
indent: function(state, textAfter) {
var cx = state.context, ch = textAfter && textAfter.charAt(0);
var indent = cx.indent;
if (cx.type == "prop" && (ch == "}" || ch == ")")) cx = cx.prev;
if (cx.prev) {
if (ch == "}" && (cx.type == "block" || cx.type == "top" ||
cx.type == "interpolation" || cx.type == "restricted_atBlock")) {
// Resume indentation from parent context.
cx = cx.prev;
indent = cx.indent;
} else if (ch == ")" && (cx.type == "parens" || cx.type == "atBlock_parens") ||
ch == "{" && (cx.type == "at" || cx.type == "atBlock")) {
// Dedent relative to current context.
indent = Math.max(0, cx.indent - indentUnit);
}
}
return indent;
},
electricChars: "}",
blockCommentStart: "/*",
blockCommentEnd: "*/",
blockCommentContinue: " * ",
lineComment: lineComment,
fold: "brace"
};
});
function keySet(array) {
var keys = {};
for (var i = 0; i < array.length; ++i) {
keys[array[i].toLowerCase()] = true;
}
return keys;
}
var documentTypes_ = [
"domain", "regexp", "url", "url-prefix"
], documentTypes = keySet(documentTypes_);
var mediaTypes_ = [
"all", "aural", "braille", "handheld", "print", "projection", "screen",
"tty", "tv", "embossed"
], mediaTypes = keySet(mediaTypes_);
var mediaFeatures_ = [
"width", "min-width", "max-width", "height", "min-height", "max-height",
"device-width", "min-device-width", "max-device-width", "device-height",
"min-device-height", "max-device-height", "aspect-ratio",
"min-aspect-ratio", "max-aspect-ratio", "device-aspect-ratio",
"min-device-aspect-ratio", "max-device-aspect-ratio", "color", "min-color",
"max-color", "color-index", "min-color-index", "max-color-index",
"monochrome", "min-monochrome", "max-monochrome", "resolution",
"min-resolution", "max-resolution", "scan", "grid", "orientation",
"device-pixel-ratio", "min-device-pixel-ratio", "max-device-pixel-ratio",
"pointer", "any-pointer", "hover", "any-hover"
], mediaFeatures = keySet(mediaFeatures_);
var mediaValueKeywords_ = [
"landscape", "portrait", "none", "coarse", "fine", "on-demand", "hover",
"interlace", "progressive"
], mediaValueKeywords = keySet(mediaValueKeywords_);
var propertyKeywords_ = [
"align-content", "align-items", "align-self", "alignment-adjust",
"alignment-baseline", "anchor-point", "animation", "animation-delay",
"animation-direction", "animation-duration", "animation-fill-mode",
"animation-iteration-count", "animation-name", "animation-play-state",
"animation-timing-function", "appearance", "azimuth", "backface-visibility",
"background", "background-attachment", "background-blend-mode", "background-clip",
"background-color", "background-image", "background-origin", "background-position",
"background-repeat", "background-size", "baseline-shift", "binding",
"bleed", "bookmark-label", "bookmark-level", "bookmark-state",
"bookmark-target", "border", "border-bottom", "border-bottom-color",
"border-bottom-left-radius", "border-bottom-right-radius",
"border-bottom-style", "border-bottom-width", "border-collapse",
"border-color", "border-image", "border-image-outset",
"border-image-repeat", "border-image-slice", "border-image-source",
"border-image-width", "border-left", "border-left-color",
"border-left-style", "border-left-width", "border-radius", "border-right",
"border-right-color", "border-right-style", "border-right-width",
"border-spacing", "border-style", "border-top", "border-top-color",
"border-top-left-radius", "border-top-right-radius", "border-top-style",
"border-top-width", "border-width", "bottom", "box-decoration-break",
"box-shadow", "box-sizing", "break-after", "break-before", "break-inside",
"caption-side", "caret-color", "clear", "clip", "color", "color-profile", "column-count",
"column-fill", "column-gap", "column-rule", "column-rule-color",
"column-rule-style", "column-rule-width", "column-span", "column-width",
"columns", "content", "counter-increment", "counter-reset", "crop", "cue",
"cue-after", "cue-before", "cursor", "direction", "display",
"dominant-baseline", "drop-initial-after-adjust",
"drop-initial-after-align", "drop-initial-before-adjust",
"drop-initial-before-align", "drop-initial-size", "drop-initial-value",
"elevation", "empty-cells", "fit", "fit-position", "flex", "flex-basis",
"flex-direction", "flex-flow", "flex-grow", "flex-shrink", "flex-wrap",
"float", "float-offset", "flow-from", "flow-into", "font", "font-feature-settings",
"font-family", "font-kerning", "font-language-override", "font-size", "font-size-adjust",
"font-stretch", "font-style", "font-synthesis", "font-variant",
"font-variant-alternates", "font-variant-caps", "font-variant-east-asian",
"font-variant-ligatures", "font-variant-numeric", "font-variant-position",
"font-weight", "grid", "grid-area", "grid-auto-columns", "grid-auto-flow",
"grid-auto-rows", "grid-column", "grid-column-end", "grid-column-gap",
"grid-column-start", "grid-gap", "grid-row", "grid-row-end", "grid-row-gap",
"grid-row-start", "grid-template", "grid-template-areas", "grid-template-columns",
"grid-template-rows", "hanging-punctuation", "height", "hyphens",
"icon", "image-orientation", "image-rendering", "image-resolution",
"inline-box-align", "justify-content", "justify-items", "justify-self", "left", "letter-spacing",
"line-break", "line-height", "line-stacking", "line-stacking-ruby",
"line-stacking-shift", "line-stacking-strategy", "list-style",
"list-style-image", "list-style-position", "list-style-type", "margin",
"margin-bottom", "margin-left", "margin-right", "margin-top",
"marks", "marquee-direction", "marquee-loop",
"marquee-play-count", "marquee-speed", "marquee-style", "max-height",
"max-width", "min-height", "min-width", "mix-blend-mode", "move-to", "nav-down", "nav-index",
"nav-left", "nav-right", "nav-up", "object-fit", "object-position",
"opacity", "order", "orphans", "outline",
"outline-color", "outline-offset", "outline-style", "outline-width",
"overflow", "overflow-style", "overflow-wrap", "overflow-x", "overflow-y",
"padding", "padding-bottom", "padding-left", "padding-right", "padding-top",
"page", "page-break-after", "page-break-before", "page-break-inside",
"page-policy", "pause", "pause-after", "pause-before", "perspective",
"perspective-origin", "pitch", "pitch-range", "place-content", "place-items", "place-self", "play-during", "position",
"presentation-level", "punctuation-trim", "quotes", "region-break-after",
"region-break-before", "region-break-inside", "region-fragment",
"rendering-intent", "resize", "rest", "rest-after", "rest-before", "richness",
"right", "rotation", "rotation-point", "ruby-align", "ruby-overhang",
"ruby-position", "ruby-span", "shape-image-threshold", "shape-inside", "shape-margin",
"shape-outside", "size", "speak", "speak-as", "speak-header",
"speak-numeral", "speak-punctuation", "speech-rate", "stress", "string-set",
"tab-size", "table-layout", "target", "target-name", "target-new",
"target-position", "text-align", "text-align-last", "text-decoration",
"text-decoration-color", "text-decoration-line", "text-decoration-skip",
"text-decoration-style", "text-emphasis", "text-emphasis-color",
"text-emphasis-position", "text-emphasis-style", "text-height",
"text-indent", "text-justify", "text-outline", "text-overflow", "text-shadow",
"text-size-adjust", "text-space-collapse", "text-transform", "text-underline-position",
"text-wrap", "top", "transform", "transform-origin", "transform-style",
"transition", "transition-delay", "transition-duration",
"transition-property", "transition-timing-function", "unicode-bidi",
"user-select", "vertical-align", "visibility", "voice-balance", "voice-duration",
"voice-family", "voice-pitch", "voice-range", "voice-rate", "voice-stress",
"voice-volume", "volume", "white-space", "widows", "width", "will-change", "word-break",
"word-spacing", "word-wrap", "z-index",
// SVG-specific
"clip-path", "clip-rule", "mask", "enable-background", "filter", "flood-color",
"flood-opacity", "lighting-color", "stop-color", "stop-opacity", "pointer-events",
"color-interpolation", "color-interpolation-filters",
"color-rendering", "fill", "fill-opacity", "fill-rule", "image-rendering",
"marker", "marker-end", "marker-mid", "marker-start", "shape-rendering", "stroke",
"stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-linejoin",
"stroke-miterlimit", "stroke-opacity", "stroke-width", "text-rendering",
"baseline-shift", "dominant-baseline", "glyph-orientation-horizontal",
"glyph-orientation-vertical", "text-anchor", "writing-mode"
], propertyKeywords = keySet(propertyKeywords_);
var nonStandardPropertyKeywords_ = [
"scrollbar-arrow-color", "scrollbar-base-color", "scrollbar-dark-shadow-color",
"scrollbar-face-color", "scrollbar-highlight-color", "scrollbar-shadow-color",
"scrollbar-3d-light-color", "scrollbar-track-color", "shape-inside",
"searchfield-cancel-button", "searchfield-decoration", "searchfield-results-button",
"searchfield-results-decoration", "zoom"
], nonStandardPropertyKeywords = keySet(nonStandardPropertyKeywords_);
var fontProperties_ = [
"font-family", "src", "unicode-range", "font-variant", "font-feature-settings",
"font-stretch", "font-weight", "font-style"
], fontProperties = keySet(fontProperties_);
var counterDescriptors_ = [
"additive-symbols", "fallback", "negative", "pad", "prefix", "range",
"speak-as", "suffix", "symbols", "system"
], counterDescriptors = keySet(counterDescriptors_);
var colorKeywords_ = [
"aliceblue", "antiquewhite", "aqua", "aquamarine", "azure", "beige",
"bisque", "black", "blanchedalmond", "blue", "blueviolet", "brown",
"burlywood", "cadetblue", "chartreuse", "chocolate", "coral", "cornflowerblue",
"cornsilk", "crimson", "cyan", "darkblue", "darkcyan", "darkgoldenrod",
"darkgray", "darkgreen", "darkkhaki", "darkmagenta", "darkolivegreen",
"darkorange", "darkorchid", "darkred", "darksalmon", "darkseagreen",
"darkslateblue", "darkslategray", "darkturquoise", "darkviolet",
"deeppink", "deepskyblue", "dimgray", "dodgerblue", "firebrick",
"floralwhite", "forestgreen", "fuchsia", "gainsboro", "ghostwhite",
"gold", "goldenrod", "gray", "grey", "green", "greenyellow", "honeydew",
"hotpink", "indianred", "indigo", "ivory", "khaki", "lavender",
"lavenderblush", "lawngreen", "lemonchiffon", "lightblue", "lightcoral",
"lightcyan", "lightgoldenrodyellow", "lightgray", "lightgreen", "lightpink",
"lightsalmon", "lightseagreen", "lightskyblue", "lightslategray",
"lightsteelblue", "lightyellow", "lime", "limegreen", "linen", "magenta",
"maroon", "mediumaquamarine", "mediumblue", "mediumorchid", "mediumpurple",
"mediumseagreen", "mediumslateblue", "mediumspringgreen", "mediumturquoise",
"mediumvioletred", "midnightblue", "mintcream", "mistyrose", "moccasin",
"navajowhite", "navy", "oldlace", "olive", "olivedrab", "orange", "orangered",
"orchid", "palegoldenrod", "palegreen", "paleturquoise", "palevioletred",
"papayawhip", "peachpuff", "peru", "pink", "plum", "powderblue",
"purple", "rebeccapurple", "red", "rosybrown", "royalblue", "saddlebrown",
"salmon", "sandybrown", "seagreen", "seashell", "sienna", "silver", "skyblue",
"slateblue", "slategray", "snow", "springgreen", "steelblue", "tan",
"teal", "thistle", "tomato", "turquoise", "violet", "wheat", "white",
"whitesmoke", "yellow", "yellowgreen"
], colorKeywords = keySet(colorKeywords_);
var valueKeywords_ = [
"above", "absolute", "activeborder", "additive", "activecaption", "afar",
"after-white-space", "ahead", "alias", "all", "all-scroll", "alphabetic", "alternate",
"always", "amharic", "amharic-abegede", "antialiased", "appworkspace",
"arabic-indic", "armenian", "asterisks", "attr", "auto", "auto-flow", "avoid", "avoid-column", "avoid-page",
"avoid-region", "background", "backwards", "baseline", "below", "bidi-override", "binary",
"bengali", "blink", "block", "block-axis", "bold", "bolder", "border", "border-box",
"both", "bottom", "break", "break-all", "break-word", "bullets", "button", "button-bevel",
"buttonface", "buttonhighlight", "buttonshadow", "buttontext", "calc", "cambodian",
"capitalize", "caps-lock-indicator", "caption", "captiontext", "caret",
"cell", "center", "checkbox", "circle", "cjk-decimal", "cjk-earthly-branch",
"cjk-heavenly-stem", "cjk-ideographic", "clear", "clip", "close-quote",
"col-resize", "collapse", "color", "color-burn", "color-dodge", "column", "column-reverse",
"compact", "condensed", "contain", "content", "contents",
"content-box", "context-menu", "continuous", "copy", "counter", "counters", "cover", "crop",
"cross", "crosshair", "currentcolor", "cursive", "cyclic", "darken", "dashed", "decimal",
"decimal-leading-zero", "default", "default-button", "dense", "destination-atop",
"destination-in", "destination-out", "destination-over", "devanagari", "difference",
"disc", "discard", "disclosure-closed", "disclosure-open", "document",
"dot-dash", "dot-dot-dash",
"dotted", "double", "down", "e-resize", "ease", "ease-in", "ease-in-out", "ease-out",
"element", "ellipse", "ellipsis", "embed", "end", "ethiopic", "ethiopic-abegede",
"ethiopic-abegede-am-et", "ethiopic-abegede-gez", "ethiopic-abegede-ti-er",
"ethiopic-abegede-ti-et", "ethiopic-halehame-aa-er",
"ethiopic-halehame-aa-et", "ethiopic-halehame-am-et",
"ethiopic-halehame-gez", "ethiopic-halehame-om-et",
"ethiopic-halehame-sid-et", "ethiopic-halehame-so-et",
"ethiopic-halehame-ti-er", "ethiopic-halehame-ti-et", "ethiopic-halehame-tig",
"ethiopic-numeric", "ew-resize", "exclusion", "expanded", "extends", "extra-condensed",
"extra-expanded", "fantasy", "fast", "fill", "fixed", "flat", "flex", "flex-end", "flex-start", "footnotes",
"forwards", "from", "geometricPrecision", "georgian", "graytext", "grid", "groove",
"gujarati", "gurmukhi", "hand", "hangul", "hangul-consonant", "hard-light", "hebrew",
"help", "hidden", "hide", "higher", "highlight", "highlighttext",
"hiragana", "hiragana-iroha", "horizontal", "hsl", "hsla", "hue", "icon", "ignore",
"inactiveborder", "inactivecaption", "inactivecaptiontext", "infinite",
"infobackground", "infotext", "inherit", "initial", "inline", "inline-axis",
"inline-block", "inline-flex", "inline-grid", "inline-table", "inset", "inside", "intrinsic", "invert",
"italic", "japanese-formal", "japanese-informal", "justify", "kannada",
"katakana", "katakana-iroha", "keep-all", "khmer",
"korean-hangul-formal", "korean-hanja-formal", "korean-hanja-informal",
"landscape", "lao", "large", "larger", "left", "level", "lighter", "lighten",
"line-through", "linear", "linear-gradient", "lines", "list-item", "listbox", "listitem",
"local", "logical", "loud", "lower", "lower-alpha", "lower-armenian",
"lower-greek", "lower-hexadecimal", "lower-latin", "lower-norwegian",
"lower-roman", "lowercase", "ltr", "luminosity", "malayalam", "match", "matrix", "matrix3d",
"media-controls-background", "media-current-time-display",
"media-fullscreen-button", "media-mute-button", "media-play-button",
"media-return-to-realtime-button", "media-rewind-button",
"media-seek-back-button", "media-seek-forward-button", "media-slider",
"media-sliderthumb", "media-time-remaining-display", "media-volume-slider",
"media-volume-slider-container", "media-volume-sliderthumb", "medium",
"menu", "menulist", "menulist-button", "menulist-text",
"menulist-textfield", "menutext", "message-box", "middle", "min-intrinsic",
"mix", "mongolian", "monospace", "move", "multiple", "multiply", "myanmar", "n-resize",
"narrower", "ne-resize", "nesw-resize", "no-close-quote", "no-drop",
"no-open-quote", "no-repeat", "none", "normal", "not-allowed", "nowrap",
"ns-resize", "numbers", "numeric", "nw-resize", "nwse-resize", "oblique", "octal", "opacity", "open-quote",
"optimizeLegibility", "optimizeSpeed", "oriya", "oromo", "outset",
"outside", "outside-shape", "overlay", "overline", "padding", "padding-box",
"painted", "page", "paused", "persian", "perspective", "plus-darker", "plus-lighter",
"pointer", "polygon", "portrait", "pre", "pre-line", "pre-wrap", "preserve-3d",
"progress", "push-button", "radial-gradient", "radio", "read-only",
"read-write", "read-write-plaintext-only", "rectangle", "region",
"relative", "repeat", "repeating-linear-gradient",
"repeating-radial-gradient", "repeat-x", "repeat-y", "reset", "reverse",
"rgb", "rgba", "ridge", "right", "rotate", "rotate3d", "rotateX", "rotateY",
"rotateZ", "round", "row", "row-resize", "row-reverse", "rtl", "run-in", "running",
"s-resize", "sans-serif", "saturation", "scale", "scale3d", "scaleX", "scaleY", "scaleZ", "screen",
"scroll", "scrollbar", "scroll-position", "se-resize", "searchfield",
"searchfield-cancel-button", "searchfield-decoration",
"searchfield-results-button", "searchfield-results-decoration", "self-start", "self-end",
"semi-condensed", "semi-expanded", "separate", "serif", "show", "sidama",
"simp-chinese-formal", "simp-chinese-informal", "single",
"skew", "skewX", "skewY", "skip-white-space", "slide", "slider-horizontal",
"slider-vertical", "sliderthumb-horizontal", "sliderthumb-vertical", "slow",
"small", "small-caps", "small-caption", "smaller", "soft-light", "solid", "somali",
"source-atop", "source-in", "source-out", "source-over", "space", "space-around", "space-between", "space-evenly", "spell-out", "square",
"square-button", "start", "static", "status-bar", "stretch", "stroke", "sub",
"subpixel-antialiased", "super", "sw-resize", "symbolic", "symbols", "system-ui", "table",
"table-caption", "table-cell", "table-column", "table-column-group",
"table-footer-group", "table-header-group", "table-row", "table-row-group",
"tamil",
"telugu", "text", "text-bottom", "text-top", "textarea", "textfield", "thai",
"thick", "thin", "threeddarkshadow", "threedface", "threedhighlight",
"threedlightshadow", "threedshadow", "tibetan", "tigre", "tigrinya-er",
"tigrinya-er-abegede", "tigrinya-et", "tigrinya-et-abegede", "to", "top",
"trad-chinese-formal", "trad-chinese-informal", "transform",
"translate", "translate3d", "translateX", "translateY", "translateZ",
"transparent", "ultra-condensed", "ultra-expanded", "underline", "unset", "up",
"upper-alpha", "upper-armenian", "upper-greek", "upper-hexadecimal",
"upper-latin", "upper-norwegian", "upper-roman", "uppercase", "urdu", "url",
"var", "vertical", "vertical-text", "visible", "visibleFill", "visiblePainted",
"visibleStroke", "visual", "w-resize", "wait", "wave", "wider",
"window", "windowframe", "windowtext", "words", "wrap", "wrap-reverse", "x-large", "x-small", "xor",
"xx-large", "xx-small"
], valueKeywords = keySet(valueKeywords_);
var allWords = documentTypes_.concat(mediaTypes_).concat(mediaFeatures_).concat(mediaValueKeywords_)
.concat(propertyKeywords_).concat(nonStandardPropertyKeywords_).concat(colorKeywords_)
.concat(valueKeywords_);
CodeMirror.registerHelper("hintWords", "css", allWords);
function tokenCComment(stream, state) {
var maybeEnd = false, ch;
while ((ch = stream.next()) != null) {
if (maybeEnd && ch == "/") {
state.tokenize = null;
break;
}
maybeEnd = (ch == "*");
}
return ["comment", "comment"];
}
CodeMirror.defineMIME("text/css", {
documentTypes: documentTypes,
mediaTypes: mediaTypes,
mediaFeatures: mediaFeatures,
mediaValueKeywords: mediaValueKeywords,
propertyKeywords: propertyKeywords,
nonStandardPropertyKeywords: nonStandardPropertyKeywords,
fontProperties: fontProperties,
counterDescriptors: counterDescriptors,
colorKeywords: colorKeywords,
valueKeywords: valueKeywords,
tokenHooks: {
"/": function(stream, state) {
if (!stream.eat("*")) return false;
state.tokenize = tokenCComment;
return tokenCComment(stream, state);
}
},
name: "css"
});
CodeMirror.defineMIME("text/x-scss", {
mediaTypes: mediaTypes,
mediaFeatures: mediaFeatures,
mediaValueKeywords: mediaValueKeywords,
propertyKeywords: propertyKeywords,
nonStandardPropertyKeywords: nonStandardPropertyKeywords,
colorKeywords: colorKeywords,
valueKeywords: valueKeywords,
fontProperties: fontProperties,
allowNested: true,
lineComment: "//",
tokenHooks: {
"/": function(stream, state) {
if (stream.eat("/")) {
stream.skipToEnd();
return ["comment", "comment"];
} else if (stream.eat("*")) {
state.tokenize = tokenCComment;
return tokenCComment(stream, state);
} else {
return ["operator", "operator"];
}
},
":": function(stream) {
if (stream.match(/\s*\{/, false))
return [null, null]
return false;
},
"$": function(stream) {
stream.match(/^[\w-]+/);
if (stream.match(/^\s*:/, false))
return ["variable-2", "variable-definition"];
return ["variable-2", "variable"];
},
"#": function(stream) {
if (!stream.eat("{")) return false;
return [null, "interpolation"];
}
},
name: "css",
helperType: "scss"
});
CodeMirror.defineMIME("text/x-less", {
mediaTypes: mediaTypes,
mediaFeatures: mediaFeatures,
mediaValueKeywords: mediaValueKeywords,
propertyKeywords: propertyKeywords,
nonStandardPropertyKeywords: nonStandardPropertyKeywords,
colorKeywords: colorKeywords,
valueKeywords: valueKeywords,
fontProperties: fontProperties,
allowNested: true,
lineComment: "//",
tokenHooks: {
"/": function(stream, state) {
if (stream.eat("/")) {
stream.skipToEnd();
return ["comment", "comment"];
} else if (stream.eat("*")) {
state.tokenize = tokenCComment;
return tokenCComment(stream, state);
} else {
return ["operator", "operator"];
}
},
"@": function(stream) {
if (stream.eat("{")) return [null, "interpolation"];
if (stream.match(/^(charset|document|font-face|import|(-(moz|ms|o|webkit)-)?keyframes|media|namespace|page|supports)\b/i, false)) return false;
stream.eatWhile(/[\w\\\-]/);
if (stream.match(/^\s*:/, false))
return ["variable-2", "variable-definition"];
return ["variable-2", "variable"];
},
"&": function() {
return ["atom", "atom"];
}
},
name: "css",
helperType: "less"
});
CodeMirror.defineMIME("text/x-gss", {
documentTypes: documentTypes,
mediaTypes: mediaTypes,
mediaFeatures: mediaFeatures,
propertyKeywords: propertyKeywords,
nonStandardPropertyKeywords: nonStandardPropertyKeywords,
fontProperties: fontProperties,
counterDescriptors: counterDescriptors,
colorKeywords: colorKeywords,
valueKeywords: valueKeywords,
supportsAtComponent: true,
tokenHooks: {
"/": function(stream, state) {
if (!stream.eat("*")) return false;
state.tokenize = tokenCComment;
return tokenCComment(stream, state);
}
},
name: "css",
helperType: "gss"
});
});

View File

@ -1,86 +0,0 @@
(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, 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(), text = cm.getRange(from, to).split("\n");
var state = CodeMirror.copyState(outer, cm.getTokenAt(from).state);
var tabSize = cm.getOption("tabSize");
var out = "", lines = 0, 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), cur = stream.current();
stream.start = stream.pos;
if (!atSol || /\S/.test(cur)) {
out += cur;
atSol = false;
}
if (!atSol && inner.mode.newlineAfterToken &&
inner.mode.newlineAfterToken(style, cur, stream.string.slice(stream.pos) || text[i+1] || "", inner.state))
newline();
}
if (!stream.pos && outer.blankLine) outer.blankLine(state);
if (!atSol) newline();
}
cm.operation(function () {
cm.replaceRange(out, from, to);
for (var cur = from.line + 1, end = from.line + lines; cur <= end; ++cur)
cm.indentLine(cur, "smart");
cm.setSelection(from, cm.getCursor(false));
});
});
})();

View File

@ -1,150 +0,0 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: https://codemirror.net/LICENSE
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../../lib/codemirror"));
else if (typeof define == "function" && define.amd) // AMD
define(["../../lib/codemirror"], mod);
else // Plain browser env
mod(CodeMirror);
})(function(CodeMirror) {
var ie_lt8 = /MSIE \d/.test(navigator.userAgent) &&
(document.documentMode == null || document.documentMode < 8);
var Pos = CodeMirror.Pos;
var matching = {"(": ")>", ")": "(<", "[": "]>", "]": "[<", "{": "}>", "}": "{<", "<": ">>", ">": "<<"};
function bracketRegex(config) {
return config && config.bracketRegex || /[(){}[\]]/
}
function findMatchingBracket(cm, where, config) {
var line = cm.getLineHandle(where.line), pos = where.ch - 1;
var afterCursor = config && config.afterCursor
if (afterCursor == null)
afterCursor = /(^| )cm-fat-cursor($| )/.test(cm.getWrapperElement().className)
var re = bracketRegex(config)
// A cursor is defined as between two characters, but in in vim command mode
// (i.e. not insert mode), the cursor is visually represented as a
// highlighted box on top of the 2nd character. Otherwise, we allow matches
// from before or after the cursor.
var match = (!afterCursor && pos >= 0 && re.test(line.text.charAt(pos)) && matching[line.text.charAt(pos)]) ||
re.test(line.text.charAt(pos + 1)) && matching[line.text.charAt(++pos)];
if (!match) return null;
var dir = match.charAt(1) == ">" ? 1 : -1;
if (config && config.strict && (dir > 0) != (pos == where.ch)) return null;
var style = cm.getTokenTypeAt(Pos(where.line, pos + 1));
var found = scanForBracket(cm, Pos(where.line, pos + (dir > 0 ? 1 : 0)), dir, style || null, config);
if (found == null) return null;
return {from: Pos(where.line, pos), to: found && found.pos,
match: found && found.ch == match.charAt(0), forward: dir > 0};
}
// bracketRegex is used to specify which type of bracket to scan
// should be a regexp, e.g. /[[\]]/
//
// Note: If "where" is on an open bracket, then this bracket is ignored.
//
// Returns false when no bracket was found, null when it reached
// maxScanLines and gave up
function scanForBracket(cm, where, dir, style, config) {
var maxScanLen = (config && config.maxScanLineLength) || 10000;
var maxScanLines = (config && config.maxScanLines) || 1000;
var stack = [];
var re = bracketRegex(config)
var lineEnd = dir > 0 ? Math.min(where.line + maxScanLines, cm.lastLine() + 1)
: Math.max(cm.firstLine() - 1, where.line - maxScanLines);
for (var lineNo = where.line; lineNo != lineEnd; lineNo += dir) {
var line = cm.getLine(lineNo);
if (!line) continue;
var pos = dir > 0 ? 0 : line.length - 1, end = dir > 0 ? line.length : -1;
if (line.length > maxScanLen) continue;
if (lineNo == where.line) pos = where.ch - (dir < 0 ? 1 : 0);
for (; pos != end; pos += dir) {
var ch = line.charAt(pos);
if (re.test(ch) && (style === undefined || cm.getTokenTypeAt(Pos(lineNo, pos + 1)) == style)) {
var match = matching[ch];
if (match && (match.charAt(1) == ">") == (dir > 0)) stack.push(ch);
else if (!stack.length) return {pos: Pos(lineNo, pos), ch: ch};
else stack.pop();
}
}
}
return lineNo - dir == (dir > 0 ? cm.lastLine() : cm.firstLine()) ? false : null;
}
function matchBrackets(cm, autoclear, config) {
// Disable brace matching in long lines, since it'll cause hugely slow updates
var maxHighlightLen = cm.state.matchBrackets.maxHighlightLineLength || 1000;
var marks = [], ranges = cm.listSelections();
for (var i = 0; i < ranges.length; i++) {
var match = ranges[i].empty() && findMatchingBracket(cm, ranges[i].head, config);
if (match && cm.getLine(match.from.line).length <= maxHighlightLen) {
var style = match.match ? "CodeMirror-matchingbracket" : "CodeMirror-nonmatchingbracket";
marks.push(cm.markText(match.from, Pos(match.from.line, match.from.ch + 1), {className: style}));
if (match.to && cm.getLine(match.to.line).length <= maxHighlightLen)
marks.push(cm.markText(match.to, Pos(match.to.line, match.to.ch + 1), {className: style}));
}
}
if (marks.length) {
// Kludge to work around the IE bug from issue #1193, where text
// input stops going to the textare whever this fires.
if (ie_lt8 && cm.state.focused) cm.focus();
var clear = function() {
cm.operation(function() {
for (var i = 0; i < marks.length; i++) marks[i].clear();
});
};
if (autoclear) setTimeout(clear, 800);
else return clear;
}
}
function doMatchBrackets(cm) {
cm.operation(function() {
if (cm.state.matchBrackets.currentlyHighlighted) {
cm.state.matchBrackets.currentlyHighlighted();
cm.state.matchBrackets.currentlyHighlighted = null;
}
cm.state.matchBrackets.currentlyHighlighted = matchBrackets(cm, false, cm.state.matchBrackets);
});
}
CodeMirror.defineOption("matchBrackets", false, function(cm, val, old) {
if (old && old != CodeMirror.Init) {
cm.off("cursorActivity", doMatchBrackets);
if (cm.state.matchBrackets && cm.state.matchBrackets.currentlyHighlighted) {
cm.state.matchBrackets.currentlyHighlighted();
cm.state.matchBrackets.currentlyHighlighted = null;
}
}
if (val) {
cm.state.matchBrackets = typeof val == "object" ? val : {};
cm.on("cursorActivity", doMatchBrackets);
}
});
CodeMirror.defineExtension("matchBrackets", function() {matchBrackets(this, true);});
CodeMirror.defineExtension("findMatchingBracket", function(pos, config, oldConfig){
// Backwards-compatibility kludge
if (oldConfig || typeof config == "boolean") {
if (!oldConfig) {
config = config ? {strict: true} : null
} else {
oldConfig.strict = config
config = oldConfig
}
}
return findMatchingBracket(this, pos, config)
});
CodeMirror.defineExtension("scanForBracket", function(pos, dir, style, config){
return scanForBracket(this, pos, dir, style, config);
});
});

View File

@ -1,460 +0,0 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: https://codemirror.net/LICENSE
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../../lib/codemirror"));
else if (typeof define == "function" && define.amd) // AMD
define(["../../lib/codemirror"], mod);
else // Plain browser env
mod(CodeMirror);
})(function(CodeMirror) {
"use strict";
var HINT_ELEMENT_CLASS = "CodeMirror-hint";
var ACTIVE_HINT_ELEMENT_CLASS = "CodeMirror-hint-active";
// This is the old interface, kept around for now to stay
// backwards-compatible.
CodeMirror.showHint = function(cm, getHints, options) {
if (!getHints) return cm.showHint(options);
if (options && options.async) getHints.async = true;
var newOpts = {hint: getHints};
if (options) for (var prop in options) newOpts[prop] = options[prop];
return cm.showHint(newOpts);
};
CodeMirror.defineExtension("showHint", function(options) {
options = parseOptions(this, this.getCursor("start"), options);
var selections = this.listSelections()
if (selections.length > 1) return;
// By default, don't allow completion when something is selected.
// A hint function can have a `supportsSelection` property to
// indicate that it can handle selections.
if (this.somethingSelected()) {
if (!options.hint.supportsSelection) return;
// Don't try with cross-line selections
for (var i = 0; i < selections.length; i++)
if (selections[i].head.line != selections[i].anchor.line) return;
}
if (this.state.completionActive) this.state.completionActive.close();
var completion = this.state.completionActive = new Completion(this, options);
if (!completion.options.hint) return;
CodeMirror.signal(this, "startCompletion", this);
completion.update(true);
});
CodeMirror.defineExtension("closeHint", function() {
if (this.state.completionActive) this.state.completionActive.close()
})
function Completion(cm, options) {
this.cm = cm;
this.options = options;
this.widget = null;
this.debounce = 0;
this.tick = 0;
this.startPos = this.cm.getCursor("start");
this.startLen = this.cm.getLine(this.startPos.line).length - this.cm.getSelection().length;
var self = this;
cm.on("cursorActivity", this.activityFunc = function() { self.cursorActivity(); });
}
var requestAnimationFrame = window.requestAnimationFrame || function(fn) {
return setTimeout(fn, 1000/60);
};
var cancelAnimationFrame = window.cancelAnimationFrame || clearTimeout;
Completion.prototype = {
close: function() {
if (!this.active()) return;
this.cm.state.completionActive = null;
this.tick = null;
this.cm.off("cursorActivity", this.activityFunc);
if (this.widget && this.data) CodeMirror.signal(this.data, "close");
if (this.widget) this.widget.close();
CodeMirror.signal(this.cm, "endCompletion", this.cm);
},
active: function() {
return this.cm.state.completionActive == this;
},
pick: function(data, i) {
var completion = data.list[i];
if (completion.hint) completion.hint(this.cm, data, completion);
else this.cm.replaceRange(getText(completion), completion.from || data.from,
completion.to || data.to, "complete");
CodeMirror.signal(data, "pick", completion);
this.close();
},
cursorActivity: function() {
if (this.debounce) {
cancelAnimationFrame(this.debounce);
this.debounce = 0;
}
var pos = this.cm.getCursor(), line = this.cm.getLine(pos.line);
if (pos.line != this.startPos.line || line.length - pos.ch != this.startLen - this.startPos.ch ||
pos.ch < this.startPos.ch || this.cm.somethingSelected() ||
(!pos.ch || this.options.closeCharacters.test(line.charAt(pos.ch - 1)))) {
this.close();
} else {
var self = this;
this.debounce = requestAnimationFrame(function() {self.update();});
if (this.widget) this.widget.disable();
}
},
update: function(first) {
if (this.tick == null) return
var self = this, myTick = ++this.tick
fetchHints(this.options.hint, this.cm, this.options, function(data) {
if (self.tick == myTick) self.finishUpdate(data, first)
})
},
finishUpdate: function(data, first) {
if (this.data) CodeMirror.signal(this.data, "update");
var picked = (this.widget && this.widget.picked) || (first && this.options.completeSingle);
if (this.widget) this.widget.close();
this.data = data;
if (data && data.list.length) {
if (picked && data.list.length == 1) {
this.pick(data, 0);
} else {
this.widget = new Widget(this, data);
CodeMirror.signal(data, "shown");
}
}
}
};
function parseOptions(cm, pos, options) {
var editor = cm.options.hintOptions;
var out = {};
for (var prop in defaultOptions) out[prop] = defaultOptions[prop];
if (editor) for (var prop in editor)
if (editor[prop] !== undefined) out[prop] = editor[prop];
if (options) for (var prop in options)
if (options[prop] !== undefined) out[prop] = options[prop];
if (out.hint.resolve) out.hint = out.hint.resolve(cm, pos)
return out;
}
function getText(completion) {
if (typeof completion == "string") return completion;
else return completion.text;
}
function buildKeyMap(completion, handle) {
var baseMap = {
Up: function() {handle.moveFocus(-1);},
Down: function() {handle.moveFocus(1);},
PageUp: function() {handle.moveFocus(-handle.menuSize() + 1, true);},
PageDown: function() {handle.moveFocus(handle.menuSize() - 1, true);},
Home: function() {handle.setFocus(0);},
End: function() {handle.setFocus(handle.length - 1);},
Enter: handle.pick,
Tab: handle.pick,
Esc: handle.close
};
var mac = /Mac/.test(navigator.platform);
if (mac) {
baseMap["Ctrl-P"] = function() {handle.moveFocus(-1);};
baseMap["Ctrl-N"] = function() {handle.moveFocus(1);};
}
var custom = completion.options.customKeys;
var ourMap = custom ? {} : baseMap;
function addBinding(key, val) {
var bound;
if (typeof val != "string")
bound = function(cm) { return val(cm, handle); };
// This mechanism is deprecated
else if (baseMap.hasOwnProperty(val))
bound = baseMap[val];
else
bound = val;
ourMap[key] = bound;
}
if (custom)
for (var key in custom) if (custom.hasOwnProperty(key))
addBinding(key, custom[key]);
var extra = completion.options.extraKeys;
if (extra)
for (var key in extra) if (extra.hasOwnProperty(key))
addBinding(key, extra[key]);
return ourMap;
}
function getHintElement(hintsElement, el) {
while (el && el != hintsElement) {
if (el.nodeName.toUpperCase() === "LI" && el.parentNode == hintsElement) return el;
el = el.parentNode;
}
}
function Widget(completion, data) {
this.completion = completion;
this.data = data;
this.picked = false;
var widget = this, cm = completion.cm;
var ownerDocument = cm.getInputField().ownerDocument;
var parentWindow = ownerDocument.defaultView || ownerDocument.parentWindow;
var hints = this.hints = ownerDocument.createElement("ul");
var theme = completion.cm.options.theme;
hints.className = "CodeMirror-hints " + theme;
this.selectedHint = data.selectedHint || 0;
var completions = data.list;
for (var i = 0; i < completions.length; ++i) {
var elt = hints.appendChild(ownerDocument.createElement("li")), cur = completions[i];
var className = HINT_ELEMENT_CLASS + (i != this.selectedHint ? "" : " " + ACTIVE_HINT_ELEMENT_CLASS);
if (cur.className != null) className = cur.className + " " + className;
elt.className = className;
if (cur.render) cur.render(elt, data, cur);
else elt.appendChild(ownerDocument.createTextNode(cur.displayText || getText(cur)));
elt.hintId = i;
}
var container = completion.options.container || ownerDocument.body;
var pos = cm.cursorCoords(completion.options.alignWithWord ? data.from : null);
var left = pos.left, top = pos.bottom, below = true;
var offsetLeft = 0, offsetTop = 0;
if (container !== ownerDocument.body) {
// We offset the cursor position because left and top are relative to the offsetParent's top left corner.
var isContainerPositioned = ['absolute', 'relative', 'fixed'].indexOf(parentWindow.getComputedStyle(container).position) !== -1;
var offsetParent = isContainerPositioned ? container : container.offsetParent;
var offsetParentPosition = offsetParent.getBoundingClientRect();
var bodyPosition = ownerDocument.body.getBoundingClientRect();
offsetLeft = (offsetParentPosition.left - bodyPosition.left - offsetParent.scrollLeft);
offsetTop = (offsetParentPosition.top - bodyPosition.top - offsetParent.scrollTop);
}
hints.style.left = (left - offsetLeft) + "px";
hints.style.top = (top - offsetTop) + "px";
// If we're at the edge of the screen, then we want the menu to appear on the left of the cursor.
var winW = parentWindow.innerWidth || Math.max(ownerDocument.body.offsetWidth, ownerDocument.documentElement.offsetWidth);
var winH = parentWindow.innerHeight || Math.max(ownerDocument.body.offsetHeight, ownerDocument.documentElement.offsetHeight);
container.appendChild(hints);
var box = hints.getBoundingClientRect(), overlapY = box.bottom - winH;
var scrolls = hints.scrollHeight > hints.clientHeight + 1
var startScroll = cm.getScrollInfo();
if (overlapY > 0) {
var height = box.bottom - box.top, curTop = pos.top - (pos.bottom - box.top);
if (curTop - height > 0) { // Fits above cursor
hints.style.top = (top = pos.top - height - offsetTop) + "px";
below = false;
} else if (height > winH) {
hints.style.height = (winH - 5) + "px";
hints.style.top = (top = pos.bottom - box.top - offsetTop) + "px";
var cursor = cm.getCursor();
if (data.from.ch != cursor.ch) {
pos = cm.cursorCoords(cursor);
hints.style.left = (left = pos.left - offsetLeft) + "px";
box = hints.getBoundingClientRect();
}
}
}
var overlapX = box.right - winW;
if (overlapX > 0) {
if (box.right - box.left > winW) {
hints.style.width = (winW - 5) + "px";
overlapX -= (box.right - box.left) - winW;
}
hints.style.left = (left = pos.left - overlapX - offsetLeft) + "px";
}
if (scrolls) for (var node = hints.firstChild; node; node = node.nextSibling)
node.style.paddingRight = cm.display.nativeBarWidth + "px"
cm.addKeyMap(this.keyMap = buildKeyMap(completion, {
moveFocus: function(n, avoidWrap) { widget.changeActive(widget.selectedHint + n, avoidWrap); },
setFocus: function(n) { widget.changeActive(n); },
menuSize: function() { return widget.screenAmount(); },
length: completions.length,
close: function() { completion.close(); },
pick: function() { widget.pick(); },
data: data
}));
if (completion.options.closeOnUnfocus) {
var closingOnBlur;
cm.on("blur", this.onBlur = function() { closingOnBlur = setTimeout(function() { completion.close(); }, 100); });
cm.on("focus", this.onFocus = function() { clearTimeout(closingOnBlur); });
}
cm.on("scroll", this.onScroll = function() {
var curScroll = cm.getScrollInfo(), editor = cm.getWrapperElement().getBoundingClientRect();
var newTop = top + startScroll.top - curScroll.top;
var point = newTop - (parentWindow.pageYOffset || (ownerDocument.documentElement || ownerDocument.body).scrollTop);
if (!below) point += hints.offsetHeight;
if (point <= editor.top || point >= editor.bottom) return completion.close();
hints.style.top = newTop + "px";
hints.style.left = (left + startScroll.left - curScroll.left) + "px";
});
CodeMirror.on(hints, "dblclick", function(e) {
var t = getHintElement(hints, e.target || e.srcElement);
if (t && t.hintId != null) {widget.changeActive(t.hintId); widget.pick();}
});
CodeMirror.on(hints, "click", function(e) {
var t = getHintElement(hints, e.target || e.srcElement);
if (t && t.hintId != null) {
widget.changeActive(t.hintId);
if (completion.options.completeOnSingleClick) widget.pick();
}
});
CodeMirror.on(hints, "mousedown", function() {
setTimeout(function(){cm.focus();}, 20);
});
CodeMirror.signal(data, "select", completions[this.selectedHint], hints.childNodes[this.selectedHint]);
return true;
}
Widget.prototype = {
close: function() {
if (this.completion.widget != this) return;
this.completion.widget = null;
this.hints.parentNode.removeChild(this.hints);
this.completion.cm.removeKeyMap(this.keyMap);
var cm = this.completion.cm;
if (this.completion.options.closeOnUnfocus) {
cm.off("blur", this.onBlur);
cm.off("focus", this.onFocus);
}
cm.off("scroll", this.onScroll);
},
disable: function() {
this.completion.cm.removeKeyMap(this.keyMap);
var widget = this;
this.keyMap = {Enter: function() { widget.picked = true; }};
this.completion.cm.addKeyMap(this.keyMap);
},
pick: function() {
this.completion.pick(this.data, this.selectedHint);
},
changeActive: function(i, avoidWrap) {
if (i >= this.data.list.length)
i = avoidWrap ? this.data.list.length - 1 : 0;
else if (i < 0)
i = avoidWrap ? 0 : this.data.list.length - 1;
if (this.selectedHint == i) return;
var node = this.hints.childNodes[this.selectedHint];
if (node) node.className = node.className.replace(" " + ACTIVE_HINT_ELEMENT_CLASS, "");
node = this.hints.childNodes[this.selectedHint = i];
node.className += " " + ACTIVE_HINT_ELEMENT_CLASS;
if (node.offsetTop < this.hints.scrollTop)
this.hints.scrollTop = node.offsetTop - 3;
else if (node.offsetTop + node.offsetHeight > this.hints.scrollTop + this.hints.clientHeight)
this.hints.scrollTop = node.offsetTop + node.offsetHeight - this.hints.clientHeight + 3;
CodeMirror.signal(this.data, "select", this.data.list[this.selectedHint], node);
},
screenAmount: function() {
return Math.floor(this.hints.clientHeight / this.hints.firstChild.offsetHeight) || 1;
}
};
function applicableHelpers(cm, helpers) {
if (!cm.somethingSelected()) return helpers
var result = []
for (var i = 0; i < helpers.length; i++)
if (helpers[i].supportsSelection) result.push(helpers[i])
return result
}
function fetchHints(hint, cm, options, callback) {
if (hint.async) {
hint(cm, callback, options)
} else {
var result = hint(cm, options)
if (result && result.then) result.then(callback)
else callback(result)
}
}
function resolveAutoHints(cm, pos) {
var helpers = cm.getHelpers(pos, "hint"), words
if (helpers.length) {
var resolved = function(cm, callback, options) {
var app = applicableHelpers(cm, helpers);
function run(i) {
if (i == app.length) return callback(null)
fetchHints(app[i], cm, options, function(result) {
if (result && result.list.length > 0) callback(result)
else run(i + 1)
})
}
run(0)
}
resolved.async = true
resolved.supportsSelection = true
return resolved
} else if (words = cm.getHelper(cm.getCursor(), "hintWords")) {
return function(cm) { return CodeMirror.hint.fromList(cm, {words: words}) }
} else if (CodeMirror.hint.anyword) {
return function(cm, options) { return CodeMirror.hint.anyword(cm, options) }
} else {
return function() {}
}
}
CodeMirror.registerHelper("hint", "auto", {
resolve: resolveAutoHints
});
CodeMirror.registerHelper("hint", "fromList", function(cm, options) {
var cur = cm.getCursor(), token = cm.getTokenAt(cur)
var term, from = CodeMirror.Pos(cur.line, token.start), to = cur
if (token.start < cur.ch && /\w/.test(token.string.charAt(cur.ch - token.start - 1))) {
term = token.string.substr(0, cur.ch - token.start)
} else {
term = ""
from = cur
}
var found = [];
for (var i = 0; i < options.words.length; i++) {
var word = options.words[i];
if (word.slice(0, term.length) == term)
found.push(word);
}
if (found.length) return {list: found, from: from, to: to};
});
CodeMirror.commands.autocomplete = CodeMirror.showHint;
var defaultOptions = {
hint: CodeMirror.hint.auto,
completeSingle: true,
alignWithWord: true,
closeCharacters: /[\s()\[\]{};:>,]/,
closeOnUnfocus: true,
completeOnSingleClick: true,
container: null,
customKeys: null,
extraKeys: null
};
CodeMirror.defineOption("hintOptions", null);
});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

40
package.json Normal file
View File

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

View File

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

Before

Width:  |  Height:  |  Size: 2.5 MiB

After

Width:  |  Height:  |  Size: 2.5 MiB

View File

Before

Width:  |  Height:  |  Size: 852 KiB

After

Width:  |  Height:  |  Size: 852 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

Before

Width:  |  Height:  |  Size: 2.7 MiB

After

Width:  |  Height:  |  Size: 2.7 MiB

View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

Before

Width:  |  Height:  |  Size: 190 KiB

After

Width:  |  Height:  |  Size: 190 KiB

View File

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 67 KiB

View File

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 57 KiB

View File

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 51 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 65 KiB

View File

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 70 KiB

View File

@ -0,0 +1,177 @@
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'
}
}
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

55
public/index.html Normal file
View File

@ -0,0 +1,55 @@
<!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>

37
src/App.vue Normal file
View File

@ -0,0 +1,37 @@
<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>

31
src/api/fetch.js Normal file
View File

@ -0,0 +1,31 @@
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;

15
src/api/file.js Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,71 @@
<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>

41
src/element/index.js Normal file
View File

@ -0,0 +1,41 @@
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 Normal file
View File

@ -0,0 +1,20 @@
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')

29
src/router/index.js Normal file
View File

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

View File

@ -1,14 +1,7 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: https://codemirror.net/LICENSE
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../../lib/codemirror"));
else if (typeof define == "function" && define.amd) // AMD
define(["../../lib/codemirror"], mod);
else // Plain browser env
mod(CodeMirror);
})(function(CodeMirror) {
import CodeMirror from "codemirror/lib/codemirror";
(function(CodeMirror) {
var defaults = {
pairs: "()[]{}''\"\"",
closeBefore: ")]}'\":;>",
@ -24,7 +17,7 @@
cm.state.closeBrackets = null;
}
if (val) {
ensureBound(getOption(val, "pairs"))
ensureBound(getOption(val, "pairs"));
cm.state.closeBrackets = val;
cm.addKeyMap(keyMap);
}
@ -39,14 +32,17 @@
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)
var ch = chars.charAt(i),
key = "'" + ch + "'";
if (!keyMap[key]) keyMap[key] = handler(ch);
}
}
ensureBound(defaults.pairs + "`")
ensureBound(defaults.pairs + "`");
function handler(ch) {
return function(cm) { return handleChar(cm, ch); };
return function(cm) {
return handleChar(cm, ch);
};
}
function getConfig(cm) {
@ -69,7 +65,12 @@
}
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");
cm.replaceRange(
"",
Pos(cur.line, cur.ch - 1),
Pos(cur.line, cur.ch + 1),
"+delete"
);
}
}
@ -99,8 +100,10 @@
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))};
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) {
@ -121,26 +124,46 @@
var type;
for (var i = 0; i < ranges.length; i++) {
var range = ranges[i], cur = range.head, curType;
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)
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;
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";
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)) {
} else if (
opening &&
(next.length === 0 || /\s/.test(next) || closeBefore.indexOf(next) > -1)
) {
curType = "both";
} else {
return CodeMirror.Pass;
@ -155,12 +178,10 @@
if (type == "skip") {
cm.execCommand("goCharRight");
} else if (type == "skipThree") {
for (var i = 0; i < 3; i++)
cm.execCommand("goCharRight");
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;
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++)
@ -178,14 +199,16 @@
}
function charsAround(cm, pos) {
var str = cm.getRange(Pos(pos.line, pos.ch - 1),
Pos(pos.line, pos.ch + 1));
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 &&
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);

60
src/scripts/config.js Normal file
View File

@ -0,0 +1,60 @@
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
}
};

29
src/scripts/converter.js Normal file
View File

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

View File

@ -0,0 +1,100 @@
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

84
src/scripts/format.js Normal file
View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
let default_theme = {
export default {
BASE: {
'text-align': 'left',
'color': '#3f3f3f',

195
src/scripts/util.js Normal file
View File

@ -0,0 +1,195 @@
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;
}

145
src/store/index.js Normal file
View File

@ -0,0 +1,145 @@
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: {}
})

5
src/views/About.vue Normal file
View File

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

14
src/views/Home.vue Normal file
View File

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

View File

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

9324
yarn.lock Normal file

File diff suppressed because it is too large Load Diff