├── .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 | Chrome Web Store 5 | GitHub 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 | ![Untitled1](https://user-images.githubusercontent.com/10095631/64230905-b12e1080-cf20-11e9-9db9-970e64518aed.gif) 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 | 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 | 21 | 22 | 78 | 79 | 110 | -------------------------------------------------------------------------------- /src/popup/router/pages/Options.vue: -------------------------------------------------------------------------------- 1 | 98 | 99 | 194 | 195 | 263 | -------------------------------------------------------------------------------- /src/popup/router/pages/Service.vue: -------------------------------------------------------------------------------- 1 | 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 | --------------------------------------------------------------------------------