├── .browserslistrc
├── .eslintrc.js
├── .gitignore
├── LICENSE
├── README.md
├── babel.config.js
├── media
└── jumbotron.jpg
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
├── _locales
│ ├── en
│ │ └── messages.json
│ └── zh_CN
│ │ └── messages.json
└── icons
│ ├── 128.png
│ ├── 16.png
│ ├── 32.png
│ ├── 48.png
│ └── 724.png
├── src
├── const.js
├── content_scripts
│ └── index.js
├── manifest.json
├── popup
│ ├── App.vue
│ ├── popup.html
│ ├── popup.js
│ ├── router
│ │ ├── index.js
│ │ ├── pages
│ │ │ ├── Index.vue
│ │ │ ├── Options.vue
│ │ │ ├── Service.vue
│ │ │ └── logo.png
│ │ └── routes.js
│ └── utils.js
└── store
│ ├── index.js
│ ├── mutation-types.js
│ └── mutations.js
└── vue.config.js
/.browserslistrc:
--------------------------------------------------------------------------------
1 | > 1%
2 | last 2 versions
3 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: {
4 | node: true,
5 | webextensions: true
6 | },
7 | extends: ["plugin:vue/essential", "@vue/prettier"],
8 | rules: {
9 | "no-console": process.env.NODE_ENV === "production" ? "error" : "off",
10 | "no-debugger": process.env.NODE_ENV === "production" ? "error" : "off"
11 | },
12 | parserOptions: {
13 | parser: "babel-eslint"
14 | }
15 | };
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 |
5 | # local env files
6 | .env.local
7 | .env.*.local
8 |
9 | # Log files
10 | npm-debug.log*
11 | yarn-debug.log*
12 | yarn-error.log*
13 |
14 | # Editor directories and files
15 | .idea
16 | .vscode
17 | *.suo
18 | *.ntvs*
19 | *.njsproj
20 | *.sln
21 | *.sw?
22 |
23 | # Vue Browser Extension Output
24 | *.pem
25 | *.pub
26 | *.zip
27 | /dist-zip
28 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 杨奕
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # doctor-jones-extension
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | 链接
16 |
17 | Chrome 应用商店
18 | ·
19 | 文档
20 |
21 |
22 | 相关项目
23 |
24 | doctor-jones
25 | ·
26 | doctor-jones-loader
27 | ·
28 | More to be developed...
29 |
30 |
31 | ## 琼斯医生
32 |
33 | 💊 一个能够美化网页中文排版(包括中英文混排)的扩展
34 |
35 | ## 特性
36 |
37 | - 遵循 [w3c/clreq](https://github.com/w3c/clreq) 和其他中文排版最佳实践,格式化你正在浏览的页面
38 | - 所有格式化功能均独立可配置,你可以按照自己的喜好打开/关闭
39 | - 支持在页面打开后自动格式化,同时支持配置自动格式化的黑名单
40 |
41 | ## 截图
42 |
43 | 
44 |
45 | ## 下载
46 |
47 | - [Chrome 应用商店](https://chrome.google.com/webstore/detail/lggmpimhpmplkengmfmfecohbdbooiem)
48 | - [手动下载](https://github.com/Leopoldthecoder/doctor-jones-extension/releases)
49 |
50 | ## 相关介绍
51 | - [Doctor Jones 上篇:中文排版格式化插件](https://zhuanlan.zhihu.com/p/79147438)
52 | - [Doctor Jones 下篇:npm 包与 webpack loader](https://zhuanlan.zhihu.com/p/80346888)
53 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: ["@vue/app"]
3 | };
4 |
--------------------------------------------------------------------------------
/media/jumbotron.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Leopoldthecoder/doctor-jones-extension/12c4007a2d0094059877970cefdd299eeaa862ca/media/jumbotron.jpg
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "doctor-jones-extension",
3 | "version": "1.2.1",
4 | "private": true,
5 | "scripts": {
6 | "serve": "vue-cli-service build --mode development --watch",
7 | "build": "vue-cli-service build",
8 | "lint": "vue-cli-service lint"
9 | },
10 | "dependencies": {
11 | "core-js": "^2.6.5",
12 | "debounce": "^1.2.0",
13 | "doctor-jones": "^1.0.2",
14 | "material-design-icons-iconfont": "^5.0.1",
15 | "parse-domain": "^7.0.1",
16 | "vue": "^2.6.10",
17 | "vue-router": "^3.0.1",
18 | "vuetify": "^1.5.16",
19 | "vuex": "^3.0.1"
20 | },
21 | "devDependencies": {
22 | "@vue/cli-plugin-babel": "^3.1.1",
23 | "@vue/cli-plugin-eslint": "^5.0.8",
24 | "@vue/cli-service": "^5.0.8",
25 | "@vue/eslint-config-prettier": "^4.0.1",
26 | "babel-eslint": "^10.0.1",
27 | "eslint": "^5.16.0",
28 | "eslint-plugin-vue": "^5.0.0",
29 | "stylus": "^0.54.5",
30 | "stylus-loader": "^3.0.2",
31 | "vue-cli-plugin-browser-extension": "^0.25.2",
32 | "vue-template-compiler": "^2.6.10"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | autoprefixer: {}
4 | }
5 | };
6 |
--------------------------------------------------------------------------------
/public/_locales/en/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "extName": {
3 | "message": "Doctor Jones"
4 | },
5 | "format": {
6 | "message": "Format this page"
7 | },
8 | "revoke": {
9 | "message": "Revoke"
10 | },
11 | "service": {
12 | "message": "Format a paragraph"
13 | },
14 | "options": {
15 | "message": "Options"
16 | },
17 | "issues": {
18 | "message": "Questions/Suggestions"
19 | },
20 | "generalOptions": {
21 | "message": "General Options"
22 | },
23 | "formatOptions": {
24 | "message": "Format Options"
25 | },
26 | "autoFormat": {
27 | "message": "Auto format after page loads"
28 | },
29 | "blacklist": {
30 | "message": "Auto format black list"
31 | },
32 | "blacklistPlaceholder": {
33 | "message": "One URL each line in the format of www.youku.com or youku.com"
34 | },
35 | "spacing": {
36 | "message": "Add a halfwidth space between Chinese character and alphabet/number"
37 | },
38 | "spaceBetweenFullwidthPunctuationAndAlphabets": {
39 | "message": "Allow unnecessary halfwidth space between fullwidth punctuation and alphabet/number"
40 | },
41 | "successiveExclamationMarks": {
42 | "message": "Allow successive exclamation marks"
43 | },
44 | "ellipsisTolerance": {
45 | "message": "Allowed ellipses"
46 | },
47 | "replaceWithCornerQuotes": {
48 | "message": "Replace the following mark with corner quotes"
49 | },
50 | "halfwidthParenthesisAroundNumbers": {
51 | "message": "Replace fullwidth brackets around numbers with halfwidth ones"
52 | },
53 | "ellipsisNone": {
54 | "message": "None"
55 | },
56 | "ellipsisDots": {
57 | "message": "..."
58 | },
59 | "ellipsisAll": {
60 | "message": "All"
61 | },
62 | "quotationNone": {
63 | "message": "Nothing"
64 | },
65 | "quotationDouble": {
66 | "message": "Double quotes"
67 | },
68 | "quotationSingle": {
69 | "message": "Single quotes"
70 | },
71 | "serviceInputLabel": {
72 | "message": "Input"
73 | },
74 | "serviceOutputLabel": {
75 | "message": "Output"
76 | },
77 | "serviceInputPlaceholder": {
78 | "message": "Enter some text"
79 | },
80 | "serviceOutputPlaceholder": {
81 | "message": "Formatted output"
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/public/_locales/zh_CN/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "extName": {
3 | "message": "琼斯医生"
4 | },
5 | "format": {
6 | "message": "格式化当前页面"
7 | },
8 | "revoke": {
9 | "message": "还原当前页面"
10 | },
11 | "service": {
12 | "message": "格式化自定义段落"
13 | },
14 | "options": {
15 | "message": "选项"
16 | },
17 | "issues": {
18 | "message": "问题/建议"
19 | },
20 | "generalOptions": {
21 | "message": "通用选项"
22 | },
23 | "formatOptions": {
24 | "message": "格式化选项"
25 | },
26 | "autoFormat": {
27 | "message": "页面打开后自动格式化"
28 | },
29 | "blacklist": {
30 | "message": "自动格式化黑名单"
31 | },
32 | "blacklistPlaceholder": {
33 | "message": "每行一个网址,格式为 www.youku.com 或 youku.com"
34 | },
35 | "spacing": {
36 | "message": "在中文和字母数字之间添加空格"
37 | },
38 | "spaceBetweenFullwidthPunctuationAndAlphabets": {
39 | "message": "允许在全角符号与字母数字之间存在空格"
40 | },
41 | "successiveExclamationMarks": {
42 | "message": "允许连续的感叹号"
43 | },
44 | "ellipsisTolerance": {
45 | "message": "允许的省略号"
46 | },
47 | "replaceWithCornerQuotes": {
48 | "message": "使用直角引号替换的弯引号"
49 | },
50 | "halfwidthParenthesisAroundNumbers": {
51 | "message": "在数字周围使用半角括号"
52 | },
53 | "ellipsisNone": {
54 | "message": "无"
55 | },
56 | "ellipsisDots": {
57 | "message": "..."
58 | },
59 | "ellipsisAll": {
60 | "message": "所有"
61 | },
62 | "quotationNone": {
63 | "message": "不替换"
64 | },
65 | "quotationDouble": {
66 | "message": "双引号"
67 | },
68 | "quotationSingle": {
69 | "message": "单引号"
70 | },
71 | "serviceInputLabel": {
72 | "message": "输入"
73 | },
74 | "serviceOutputLabel": {
75 | "message": "输出"
76 | },
77 | "serviceInputPlaceholder": {
78 | "message": "输入一段任意文本"
79 | },
80 | "serviceOutputPlaceholder": {
81 | "message": "格式化后的输出"
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/public/icons/128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Leopoldthecoder/doctor-jones-extension/12c4007a2d0094059877970cefdd299eeaa862ca/public/icons/128.png
--------------------------------------------------------------------------------
/public/icons/16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Leopoldthecoder/doctor-jones-extension/12c4007a2d0094059877970cefdd299eeaa862ca/public/icons/16.png
--------------------------------------------------------------------------------
/public/icons/32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Leopoldthecoder/doctor-jones-extension/12c4007a2d0094059877970cefdd299eeaa862ca/public/icons/32.png
--------------------------------------------------------------------------------
/public/icons/48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Leopoldthecoder/doctor-jones-extension/12c4007a2d0094059877970cefdd299eeaa862ca/public/icons/48.png
--------------------------------------------------------------------------------
/public/icons/724.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Leopoldthecoder/doctor-jones-extension/12c4007a2d0094059877970cefdd299eeaa862ca/public/icons/724.png
--------------------------------------------------------------------------------
/src/const.js:
--------------------------------------------------------------------------------
1 | export const messageType = {
2 | format: "format",
3 | revoke: "revoke",
4 | startAutoFormat: "startAutoFormat",
5 | stopAutoFormat: "stopAutoFormat"
6 | };
7 |
--------------------------------------------------------------------------------
/src/content_scripts/index.js:
--------------------------------------------------------------------------------
1 | import dj from "doctor-jones";
2 | import { messageType } from "../const";
3 | import { debounce } from "debounce";
4 | import { isInBlacklist, convertReversedQuotes } from "../popup/utils";
5 |
6 | let storedOptions = {};
7 |
8 | let textNodes = [];
9 | let touched = false;
10 | let manualRevoked = false;
11 | let blacklist = [];
12 |
13 | const walk = nodes => {
14 | nodes.forEach(node => {
15 | if (
16 | ["SCRIPT", "STYLE", "META", "LINK", "HEAD", "SVG", "PATH"].includes(
17 | (node.tagName || "").toUpperCase()
18 | )
19 | ) {
20 | return;
21 | }
22 | if (node.getAttribute && node.getAttribute("contenteditable")) {
23 | const activeElement = document.activeElement;
24 | if (activeElement && node.contains(activeElement)) {
25 | return;
26 | }
27 | }
28 | if (node.hasChildNodes()) {
29 | walk(node.childNodes);
30 | } else if (node.nodeType === 1 || node.nodeType === 3) {
31 | textNodes.push(node);
32 | }
33 | });
34 | };
35 |
36 | const format = options => {
37 | touched = true;
38 | textNodes = [];
39 | walk([document.documentElement]);
40 | textNodes.forEach(node => {
41 | if (node.hasChildNodes()) return;
42 | let formattedText;
43 | switch (node.nodeType) {
44 | case 1:
45 | node._innerText = node._innerText || node.innerText;
46 | formattedText = dj(convertReversedQuotes(node.innerText), options);
47 | if (node.innerText !== formattedText) {
48 | node.innerText = formattedText;
49 | }
50 | break;
51 | case 3:
52 | node._nodeValue = node._nodeValue || node.nodeValue;
53 | formattedText = dj(convertReversedQuotes(node.nodeValue), options);
54 | if (node.nodeValue !== formattedText) {
55 | node.nodeValue = formattedText;
56 | }
57 | break;
58 | default:
59 | break;
60 | }
61 | });
62 | };
63 |
64 | let observer;
65 |
66 | const autoFormat = () => {
67 | if (isInBlacklist(blacklist)) return;
68 | const debouncedFormat = debounce(format, 100, true);
69 |
70 | const mutationHandler = mutationList => {
71 | if (manualRevoked) return;
72 | mutationList.forEach(() => {
73 | debouncedFormat(storedOptions);
74 | });
75 | };
76 |
77 | const observerOptions = {
78 | childList: true,
79 | subtree: true
80 | };
81 |
82 | observer = new MutationObserver(mutationHandler);
83 | observer.observe(document.body, observerOptions);
84 | debouncedFormat(storedOptions);
85 | };
86 |
87 | chrome.storage.sync.get(["FORMAT_OPTIONS"], result => {
88 | if (result && result["FORMAT_OPTIONS"]) {
89 | storedOptions = result["FORMAT_OPTIONS"];
90 | if (storedOptions.blacklist) {
91 | blacklist = storedOptions.blacklist
92 | .split("\n")
93 | .map(s => s.trim())
94 | .filter(Boolean);
95 | }
96 | if (storedOptions.autoFormat) {
97 | autoFormat();
98 | }
99 | }
100 | });
101 |
102 | const revoke = () => {
103 | if (!touched) return;
104 | touched = false;
105 | textNodes.forEach(node => {
106 | if (node.hasChildNodes()) return;
107 | switch (node.nodeType) {
108 | case 1:
109 | node.innerText = node._innerText || node.innerText;
110 | break;
111 | case 3:
112 | node.nodeValue = node._nodeValue || node.nodeValue;
113 | break;
114 | default:
115 | break;
116 | }
117 | });
118 | };
119 |
120 | chrome.runtime.onMessage.addListener((request, sender) => {
121 | if (chrome.runtime.id !== sender.id) {
122 | return;
123 | }
124 |
125 | switch (request.type) {
126 | case messageType.format:
127 | format(request.options);
128 | break;
129 |
130 | case messageType.revoke:
131 | revoke();
132 | manualRevoked = true;
133 | break;
134 |
135 | case messageType.startAutoFormat:
136 | autoFormat();
137 | break;
138 |
139 | case messageType.stopAutoFormat:
140 | if (observer) {
141 | observer.disconnect();
142 | }
143 | revoke();
144 | break;
145 |
146 | default:
147 | break;
148 | }
149 | });
150 |
--------------------------------------------------------------------------------
/src/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 2,
3 | "name": "__MSG_extName__",
4 | "homepage_url": "https://leopoldthecoder.github.io/doctor-jones/",
5 | "description": "中文排版格式化工具",
6 | "default_locale": "zh_CN",
7 | "icons": {
8 | "16": "icons/16.png",
9 | "48": "icons/48.png",
10 | "128": "icons/128.png"
11 | },
12 | "permissions": [
13 | "activeTab",
14 | "storage"
15 | ],
16 | "content_scripts":[{
17 | "matches":[
18 | ""
19 | ],
20 | "js":["content.js"]
21 | }],
22 | "browser_action": {
23 | "default_popup": "popup/popup.html",
24 | "default_title": "__MSG_extName__",
25 | "default_icon": {
26 | "16": "icons/16.png",
27 | "32": "icons/32.png",
28 | "48": "icons/48.png",
29 | "128": "icons/128.png",
30 | "724": "icons/724.png"
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/popup/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
15 |
16 |
29 |
--------------------------------------------------------------------------------
/src/popup/popup.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | <%= htmlWebpackPlugin.options.title %>
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/popup/popup.js:
--------------------------------------------------------------------------------
1 | import Vue from "vue";
2 | import Vuetify from "vuetify";
3 | import "vuetify/dist/vuetify.min.css";
4 | import "material-design-icons-iconfont/dist/material-design-icons.css";
5 | import App from "./App";
6 | import store from "../store";
7 | import router from "./router";
8 |
9 | Vue.use(Vuetify, {
10 | theme: {
11 | primary: "#12d3cf",
12 | secondary: "#fcf9ec",
13 | accent: "#67eaca"
14 | }
15 | });
16 |
17 | /* eslint-disable no-new */
18 | new Vue({
19 | el: "#app",
20 | store,
21 | router,
22 | render: h => h(App)
23 | });
24 |
--------------------------------------------------------------------------------
/src/popup/router/index.js:
--------------------------------------------------------------------------------
1 | import Vue from "vue";
2 | import VueRouter from "vue-router";
3 | import routes from "./routes";
4 |
5 | Vue.use(VueRouter);
6 |
7 | export default new VueRouter({
8 | routes
9 | });
10 |
--------------------------------------------------------------------------------
/src/popup/router/pages/Index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |

4 |
{{
5 | formatText
6 | }}
7 |
{{
8 | revokeText
9 | }}
10 |
{{ serviceText }}
11 |
{{ optionText }}
12 |
19 |
20 |
21 |
22 |
78 |
79 |
110 |
--------------------------------------------------------------------------------
/src/popup/router/pages/Options.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | arrow_back
5 | {{ titleText }}
6 |
7 |
8 |
9 | {{ generalOptionsText }}
10 | {
15 | onChange(v, 'autoFormat');
16 | onAutoFormatChange(v);
17 | }
18 | "
19 | >
20 |
21 | {{ generalLabelText.blacklist }}
22 | {
29 | onChange(v, 'blacklist', false);
30 | }
31 | "
32 | >
33 |
34 |
35 |
36 | {{ formatOptionsText }}
37 | {
42 | onChange(v, 'spacing');
43 | }
44 | "
45 | >
46 | {
51 | onChange(v, 'spaceBetweenFullwidthPunctuationAndAlphabets');
52 | }
53 | "
54 | >
55 | {
61 | onChange(v, 'successiveExclamationMarks');
62 | }
63 | "
64 | >
65 | {
70 | onChange(v, 'halfwidthParenthesisAroundNumbers');
71 | }
72 | "
73 | >
74 | {
80 | onChange(v, 'ellipsisTolerance');
81 | }
82 | "
83 | >
84 | {
90 | onChange(v, 'replaceWithCornerQuotes');
91 | }
92 | "
93 | >
94 |
95 |
96 |
97 |
98 |
99 |
194 |
195 |
263 |
--------------------------------------------------------------------------------
/src/popup/router/pages/Service.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | arrow_back
5 | {{ titleText }}
6 |
7 |
8 |
9 |
15 |
16 |
23 |
24 |
25 |
26 |
27 |
73 |
74 |
113 |
--------------------------------------------------------------------------------
/src/popup/router/pages/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Leopoldthecoder/doctor-jones-extension/12c4007a2d0094059877970cefdd299eeaa862ca/src/popup/router/pages/logo.png
--------------------------------------------------------------------------------
/src/popup/router/routes.js:
--------------------------------------------------------------------------------
1 | import PageIndex from "./pages/Index";
2 | import PageOptions from "./pages/Options";
3 | import PageService from "./pages/Service";
4 |
5 | export default [
6 | {
7 | path: "/",
8 | component: PageIndex
9 | },
10 | {
11 | path: "/options",
12 | component: PageOptions
13 | },
14 | {
15 | path: "/service",
16 | component: PageService
17 | }
18 | ];
19 |
--------------------------------------------------------------------------------
/src/popup/utils.js:
--------------------------------------------------------------------------------
1 | import parseDomain from "parse-domain";
2 |
3 | let activeTab;
4 |
5 | export const getActiveTab = () => {
6 | if (!activeTab) {
7 | activeTab = new Promise(resolve => {
8 | chrome.tabs.query({ active: true, currentWindow: true }, tabs => {
9 | resolve(tabs[0]);
10 | });
11 | });
12 | }
13 | return activeTab;
14 | };
15 |
16 | export const isInBlacklist = blacklist => {
17 | return blacklist.some(item => {
18 | const itemDomain = parseDomain(item) || {};
19 | const currentDomain = parseDomain(location.href) || {};
20 | return (
21 | itemDomain.tld === currentDomain.tld &&
22 | itemDomain.domain === currentDomain.domain &&
23 | (!itemDomain.subdomain ||
24 | itemDomain.subdomain === currentDomain.subdomain)
25 | );
26 | });
27 | };
28 |
29 | export const convertReversedQuotes = s => {
30 | let counter = {
31 | single: 0,
32 | double: 0
33 | };
34 | const len = s.length;
35 | let ans = "";
36 | for (let i = 0; i < len; i++) {
37 | const char = s[i];
38 | if (char === "‘" || char === "’") {
39 | ans += counter.single % 2 === 0 ? "‘" : "’";
40 | counter.single++;
41 | } else if (char === "“" || char === "”") {
42 | ans += counter.double % 2 === 0 ? "“" : "”";
43 | counter.double++;
44 | } else {
45 | ans += s[i];
46 | }
47 | }
48 | return ans;
49 | };
50 |
--------------------------------------------------------------------------------
/src/store/index.js:
--------------------------------------------------------------------------------
1 | import Vue from "vue";
2 | import Vuex from "vuex";
3 | import mutations from "./mutations";
4 |
5 | Vue.use(Vuex);
6 |
7 | export default new Vuex.Store({
8 | state: {
9 | options: {
10 | // 通用选项
11 | autoFormat: false,
12 | blacklist: "",
13 |
14 | // 格式化选项
15 | spacing: true,
16 | spaceBetweenFullwidthPunctuationAndAlphabets: false,
17 | successiveExclamationMarks: false,
18 | ellipsisTolerance: "none",
19 | replaceWithCornerQuotes: "double",
20 | halfwidthParenthesisAroundNumbers: true
21 | }
22 | },
23 | mutations
24 | });
25 |
--------------------------------------------------------------------------------
/src/store/mutation-types.js:
--------------------------------------------------------------------------------
1 | export const UPDATE_OPTIONS = "UPDATE_OPTIONS";
2 | export const RESET_OPTIONS = "RESET_OPTIONS";
3 |
--------------------------------------------------------------------------------
/src/store/mutations.js:
--------------------------------------------------------------------------------
1 | import * as types from "./mutation-types";
2 |
3 | export default {
4 | [types.RESET_OPTIONS](state, payload) {
5 | state.options = payload;
6 | },
7 |
8 | [types.UPDATE_OPTIONS](state, payload) {
9 | state.options[payload.key] = payload.value;
10 | }
11 | };
12 |
--------------------------------------------------------------------------------
/vue.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | css: {
3 | extract: false
4 | },
5 | pages: {
6 | "popup/popup": {
7 | entry: "src/popup/popup.js",
8 | title: "Popup"
9 | }
10 | },
11 | pluginOptions: {
12 | browserExtension: {
13 | registry: undefined,
14 | components: {
15 | background: false,
16 | contentScripts: true,
17 | popup: true
18 | },
19 | api: "chrome",
20 | usePolyfill: false,
21 | autoImportPolyfill: false,
22 | componentOptions: {
23 | contentScripts: {
24 | entries: {
25 | content: "src/content_scripts/index.js"
26 | }
27 | }
28 | }
29 | }
30 | }
31 | };
32 |
--------------------------------------------------------------------------------