feat: update content formatter

This commit is contained in:
yanglbme 2020-05-31 17:30:44 +08:00
parent 265c25b782
commit 7f3db24d05
6 changed files with 16 additions and 469 deletions

View File

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

View File

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

View File

@ -1,177 +0,0 @@
export const default_theme = {
BASE: {
'text-align': 'left',
'color': '#3f3f3f',
'line-height': '1.75'
},
BASE_BLOCK: {
'margin': '1em 8px'
},
block: {
// 一级标题样式
h1: {
'font-size': '1.2em',
'text-align': 'center',
'font-weight': 'bold',
'display': 'table',
'margin': '2em auto 1em',
'padding': '0 1em',
'border-bottom': '2px solid rgba(0, 152, 116, 0.9)'
},
// 二级标题样式
h2: {
'font-size': '1.2em',
'text-align': 'center',
'font-weight': 'bold',
'display': 'table',
'margin': '4em auto 2em',
'padding': '0 0.2em',
'background': 'rgba(0, 152, 116, 0.9)',
'color': '#fff'
},
// 三级标题样式
h3: {
'font-weight': 'bold',
'font-size': '1.1em',
'margin': '2em 8px 0.75em 0',
'line-height': '1.2',
'padding-left': '8px',
'border-left': '3px solid rgba(0, 152, 116, 0.9)'
},
// 四级标题样式
h4: {
'font-weight': 'bold',
'font-size': '1em',
'margin': '2em 8px 0.5em',
'color': 'rgba(66, 185, 131, 0.9)'
},
// 段落样式
p: {
'margin': '1.5em 8px',
'letter-spacing': '0.1em'
},
// 引用样式
blockquote: {
'font-style': 'normal',
'border-left': 'none',
'padding': '1em',
'border-radius': '4px',
'color': '#FEEEED',
'background': 'rgba(27,31,35,.05)',
'margin': '2em 8px'
},
blockquote_p: {
'letter-spacing': '0.1em',
'color': 'rgb(80, 80, 80)',
'font-family': 'PingFangSC-light, PingFangTC-light, Open Sans, Helvetica Neue, sans-serif',
'font-size': '1em',
'display': 'inline'
},
code: {
'font-size': '80%',
'overflow': 'auto',
'color': '#333',
'background': 'rgb(247, 247, 247)',
'border-radius': '2px',
'padding': '10px',
'line-height': '1.5',
'border': '1px solid rgb(236,236,236)',
'margin': '20px 0'
},
image: {
'border-radius': '4px',
'display': 'block',
'margin': '0.1em auto 0.5em',
'width': '100% !important'
},
image_org: {
'border-radius': '4px',
'display': 'block'
},
ol: {
'margin-left': '0',
'padding-left': '1em'
},
ul: {
'margin-left': '0',
'padding-left': '1em',
'list-style': 'circle'
},
footnotes: {
'margin': '0.5em 8px',
'font-size': '80%'
},
figure: {
'margin': '1.5em 8px'
}
},
inline: {
listitem: {
'text-indent': '-1em',
'display': 'block',
'margin': '0.2em 8px'
},
codespan: {
'font-size': '90%',
'color': '#d14',
'background': 'rgba(27,31,35,.05)',
'padding': '3px 5px',
'border-radius': '4px'
},
link: {
'color': '#576b95'
},
wx_link: {
'color': '#576b95',
'text-decoration': 'none'
},
// 字体加粗样式
strong: {
'color': 'rgba(15, 76, 129, 0.9)',
'font-weight': 'bold'
},
table: {
'border-collapse': 'collapse',
'text-align': 'center',
'margin': '1em 8px'
},
thead: {
'background': 'rgba(0, 0, 0, 0.05)',
'font-weight': 'bold'
},
td: {
'border': '1px solid #dfdfdf',
'padding': '0.25em 0.5em'
},
footnote: {
'font-size': '12px'
},
figcaption: {
'text-align': 'center',
'color': '#888',
'font-size': '0.8em'
}
}
}

View File

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

View File

@ -1,4 +1,6 @@
import default_theme from "./themes/default-theme";
import default_theme from './themes/default-theme'
import prettier from 'prettier/standalone'
import prettierMarkdown from 'prettier/parser-markdown'
// 设置自定义颜色
@ -192,4 +194,12 @@ export function isImageIllegal(file) {
return '由于公众号限制,图片大小不能超过 5.0M';
}
return false;
}
export function formatDoc(content) {
const doc = prettier.format(content, {
parser: 'markdown',
plugins: [prettierMarkdown]
})
return doc
}

View File

@ -3,13 +3,12 @@ import Vuex from 'vuex'
import config from '../scripts/config';
import WxRenderer from '../scripts/renderers/wx-renderer'
import marked from 'marked'
import prettier from 'prettier/standalone'
import prettierMarkdown from 'prettier/parser-markdown'
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
setColor,
formatDoc
} from '../scripts/util'
Vue.use(Vuex)
@ -83,10 +82,7 @@ const mutations = {
autoCloseBrackets: true,
extraKeys: {
'Ctrl-F': function autoFormat(editor) {
const doc = prettier.format(editor.getValue(0), {
parser: 'markdown',
plugins: [prettierMarkdown]
})
const doc = formatDoc(editor.getValue(0))
localStorage.setItem('__editor_content', doc)
editor.setValue(doc)
}
@ -97,7 +93,8 @@ const mutations = {
if (localStorage.getItem('__editor_content')) {
state.editor.setValue(localStorage.getItem('__editor_content'))
} else {
state.editor.setValue(DEFAULT_CONTENT)
const doc = formatDoc(DEFAULT_CONTENT)
state.editor.setValue(doc)
}
},
initCssEditorEntity(state) {