Răsfoiți Sursa

json-format content-script完全优化

[email protected] 2 ani în urmă
părinte
comite
7f41d45292

+ 25 - 0
apps/background/background.js

@@ -378,6 +378,31 @@ let BgPageInstance = (function () {
                             autoClose: 2000
                         });
                         break;
+                    case 'request-jsonformat-options':
+                        Awesome.StorageMgr.get(request.params).then(result => {
+                            Object.keys(result).forEach(key => {
+                                if (['MAX_JSON_KEYS_NUMBER', 'JSON_FORMAT_THEME'].includes(key)) {
+                                    result[key] = parseInt(result[key]);
+                                } else {
+                                    result[key] = (result[key] !== 'false');
+                                }
+                            });
+                            callback && callback(result);
+                        });
+                        return true; // 这个返回true是非常重要的!!!要不然callback会拿不到结果
+                    case 'save-jsonformat-options':
+                        Awesome.StorageMgr.set(request.params).then(() => {
+                            callback && callback();
+                        });
+                        return true;
+                    case 'toggle-jsonformat-options':
+                        Awesome.StorageMgr.get('JSON_TOOL_BAR_ALWAYS_SHOW').then(result => {
+                            let show = result !== 'false';
+                            Awesome.StorageMgr.set('JSON_TOOL_BAR_ALWAYS_SHOW',!show).then(() => {
+                                callback && callback(!show);
+                            });
+                        });
+                        return true; // 这个返回true是非常重要的!!!要不然callback会拿不到结果
                     case 'code-beautify':
                         _codeBeautify(request.params);
                         break;

+ 6 - 1
apps/code-beautify/content-script.css

@@ -2,6 +2,7 @@
 
 body {
     font-size: 14px;
+    background: #fff;
 }
 #fehelper_tips {
     position: fixed;
@@ -47,6 +48,7 @@ body {
     border-radius: 3px;
     background-image: linear-gradient(to bottom, #FFFBEA, #FBEDB1);
     vertical-align: middle;
+    color:#000;
 }
 
 #fehelper_tips button:hover {
@@ -131,6 +133,9 @@ body.processing > :not(#fehelper_tips) {
     pointer-events: none;
     cursor: wait;
 }
+pre {
+    padding-top: 0;
+}
 pre>code[class*="language"] {
     overflow: initial;
 }
@@ -150,4 +155,4 @@ pre ol li>span {
 }
 pre ol li:hover {
     background:#f5f5f5;
-}
+}

+ 5 - 20
apps/code-beautify/content-script.js

@@ -21,28 +21,13 @@ window.codebeautifyContentScript = (() => {
 
 
     let highlightWebWorker = () => {
-        let __importScript = (filename) => {
-            let url = filename;
-
-            if (location.protocol === 'chrome-extension:' || typeof chrome !='undefined' && chrome.runtime && chrome.runtime.getURL) {
-                url = chrome.runtime.getURL('code-beautify/' + filename);
-            }
-            fetch(url).then(resp => resp.text()).then(jsText => {
-                if(window.evalCore && window.evalCore.getEvalInstance){
-                    return window.evalCore.getEvalInstance(window)(jsText);
-                }
-                let el = document.createElement('script');
-                el.textContent = jsText;
-                document.head.appendChild(el);
-            });
-        };
-
-        let site = 'chrome-extension://mnaedlmagdcfmejjndjhffalddfofeim';
-        __importScript(site + '/static/vendor/highlight/highlight.js');
+        // TODO ...
+        // __importScript('../static/vendor/highlight/highlight.js');
 
         self.onmessage = (event) => {
-            const result = self.hljs.highlightAuto(event.data);
-            postMessage(result.value);
+            // const result = self.hljs.highlightAuto(event.data);
+            // postMessage(result.value);
+            postMessage(event.data);
         };
     };
 

+ 488 - 91
apps/json-format/content-script.css

@@ -22,13 +22,13 @@
     text-shadow: 1px 1px rgba(255, 255, 255, 0.3)
 }
 
-.xjf-btn-mid,.xjf-btn-right {
+.xjf-btn-mid, .xjf-btn-right {
     margin-left: 0;
     border-top-left-radius: 0;
     border-bottom-left-radius: 0;
 }
 
-.xjf-btn-mid,.xjf-btn-left {
+.xjf-btn-mid, .xjf-btn-left {
     margin-right: 0;
     border-top-right-radius: 0;
     border-bottom-right-radius: 0;
@@ -73,50 +73,43 @@ pre {
     padding: 36px 5px 5px 5px
 }
 
-.kvov {
+#jfContent {
+    margin-bottom: 25px;
+}
+
+/*================json format style start===================*/
+.item {
     display: block;
     padding-left: 20px;
     margin-left: -20px;
     position: relative;
-    padding-top: 2px;
+    padding-top:1px;
+    padding-bottom: 1px;
 }
 
-#jfContent {
-    margin-bottom: 25px;
+.item .kv-list {
+    display: block;
+    padding-left: 24px;
+    border-left: 1px dashed #bbb;
+    margin-left: 2px
+}
+.item .string {
+    word-wrap: break-word
 }
 
-#jfContent .kvov .s a {
-    color: #00b;
+.item .string a {
     text-decoration: underline;
 }
 
-#jfContent .kvov .s a:hover {
+.item .string a:hover {
     color: #b00;
 }
 
-.collapsed {
-    white-space: nowrap
-}
-
-.collapsed > .blockInner {
-    display: none
-}
-
-.collapsed > .ell:after {
-    content: "\2026";
-    font-weight: bold
-}
-
-.collapsed > .ell {
-    margin: 0 4px;
-    color: #888
-}
-
-.collapsed .kvov {
-    display: inline
+.item .brace {
+    font-weight: bold;
 }
 
-.e {
+.item .expand {
     width: 20px;
     height: 18px;
     display: block;
@@ -129,71 +122,64 @@ pre {
     cursor: pointer;
 }
 
-.e:after {
+.item .expand:after {
     content: "\25bc";
 }
 
-.collapsed > .e {
-    -webkit-transform: rotate(-90deg);
-    top: -1px
-}
-
-.e:hover {
+.item .expand:hover {
     opacity: 0.35
 }
 
-.e:active {
+.item .expand:active {
     opacity: 0.5
 }
 
-.collapsed .kvov .e {
-    display: none
-}
-
-.blockInner {
-    display: block;
-    padding-left: 24px;
-    border-left: 1px dotted #bbb;
-    margin-left: 2px
+.item.collapsed {
+    white-space: nowrap
 }
 
-#formattedJson, #jsonpOpener, #jsonpCloser {
-    color: #333;
-    font: 13px/18px monospace
+.item.collapsed > .kv-list {
+    display: none
 }
 
-#formattedJson {
-    color: #444
+.item.collapsed .item .expand {
+    display: none
 }
 
-.b {
+.item.collapsed > .ellipsis:after {
+    content: "\2026";
     font-weight: bold
 }
 
-.s {
-    color: #0B7500;
-    word-wrap: break-word
+.item.collapsed > .ellipsis {
+    margin: 0 4px;
+    color: #888
 }
 
-#jfContent a:link, #jfContent a:visited {
-    text-decoration: none;
-    color: inherit
+.item.collapsed .item {
+    display: inline
 }
 
-#jfContent a:hover, #jfContent a:active {
-    text-decoration: underline;
-    color: #050
+.item.collapsed > .expand {
+    -webkit-transform: rotate(-90deg);
+    top: -1px
+}
+.remove-quote .quote {
+    display: none;
 }
+/*================json format style end===================*/
 
-.bl, .nl, .n {
-    font-weight: bold;
-    color: #1A01CC
+
+#formattedJson, #jsonpOpener, #jsonpCloser {
+    color: #333;
+    font: 14px/18px monospace;
 }
 
-.k {
-    color: black
+#formattedJson {
+    color: #444
 }
 
+
 #formattingMsg {
     position: absolute;
     top: calc(40vh);
@@ -315,6 +301,9 @@ body {
 .x-toolbar .x-sort input {
     margin-right: 10px;
 }
+.x-toolbar span {
+    white-space: normal !important;
+}
 
 .x-toolbar .x-sort input#sort_desc {
     margin-right: 0;
@@ -351,17 +340,23 @@ body {
     font-size: 14px;
     font-weight: bold;
 }
+
 .x-toolbar.t-collapse {
-    position: absolute;
+    position: fixed;
     left: 100%;
     margin-left: -24px;
     width: 10000px;
+    top: -10px;
 }
+
 .x-toolbar.t-collapse .fe-feedback {
     float: left;
     margin-left: -10px;
     margin-right: 20px;
 }
+.x-toolbar.t-collapse .fe-feedback .x-settings{
+    display: none;
+}
 
 .mod-json .format-item button {
     width: 80px;
@@ -373,6 +368,16 @@ body {
     width: auto;
 }
 
+#optionBar {
+    display: inline-block !important;
+    position: static !important;
+}
+
+body>#optionBar ,
+body>#jfContent {
+    display: none !important;
+}
+
 #formatTips {
     color: #888;
     font-size: 14px;
@@ -391,46 +396,64 @@ body {
     color: #a00;
 }
 
-#jfContent .x-hover {
-    outline: 1px solid #cdc;
-    background: #fff;
-}
-
-#jfContent .x-outline {
-    outline: 1px solid #8ac;
-    box-shadow: rgba(100, 100, 100, 0.4) -3px 3px 5px;
-    font-weight: bold;
-    background-color: #fffff8;
-}
-
 #errorMsg {
     margin-top: 10px;
     float: left;
     color: #f00;
 }
 
-#boxOpt {
+#statusBar {
+    position: fixed;
+    bottom: 0;
+    left: 0;
+    right: 0;
+    background: #f5f5f5;
+    border-top:1px solid #f4f4f4;
+    font-size: 12px;
+    font-weight: bold;
+    padding: 2px 10px 2px 2px;
+    z-index: 10
+}
+.hide-status-bar #statusBar {
+    width:0;
+    height:0;
+    opacity: 0;
+}
+#jsonPath {
+    cursor: default;
+}
+
+.boxOpt {
     position: absolute;
-    z-index: 1024;
+    right: 0;
+    top:-1px;
+    background: -webkit-linear-gradient(#fafafa,#f4f4f4 40%,#e5e5e5);
+    color: #888;
+    border:1px solid #ddd;
+    user-select: none;
+    font-weight: normal;
+    z-index: 1000;
 }
 
-#boxOpt a {
+.boxOpt a {
     cursor: pointer;
     margin: 0 5px;
     font-size: 12px;
-    color: #00f;
+    color: #666;
+    text-decoration: none;
 }
-
-#boxOpt a:hover {
-    color: #f00;
+.boxOpt a:hover {
+    color:#000;
+    text-decoration: none;
 }
+
 .fe-feedback {
     font-size: 12px;
     padding-top: 3px;
     color: #888;
     float: right;
     cursor: pointer;
-    font-weight:bold;
+    font-weight: bold;
 }
 
 .fe-feedback a {
@@ -469,11 +492,385 @@ svg:not(:root) {
 }
 
 .fe-feedback .x-settings {
-    float: right;
     cursor: pointer;
-    margin-left: 5px;
+    margin-right: 10px;
 }
 
 .fe-feedback .x-settings:hover {
     color: #c00;
-}
+}
+#fehelper_alertmsg {
+    font-family: "monospace", "微软雅黑";
+}
+
+#aLinkDownload {
+    position: absolute;
+    top:-10000px;
+    left:-10000px;
+}
+
+/*设置面板*/
+.mod-setting-panel {
+    position: absolute;
+    top: 32px;
+    right: 0;
+    z-index:10000;
+    width:300px;
+    background:#f1f1f1;
+    padding:0 20px 20px;
+    border:1px solid #ddd;
+    border-radius: 4px;
+    font-size:12px;
+    color:#454545;
+    box-shadow: 2px 2px #f9f9f9;
+}
+.mod-setting-panel h4 {
+    border-bottom: 1px solid #ccc;
+    padding-bottom: 4px;
+    margin:20px 0 10px;
+}
+.mod-setting-panel input[name="maxlength"] {
+    width:50px;
+    background: #fff;
+    border: 1px solid #eee;
+}
+.mod-setting-panel ul {
+    margin:0;
+    padding:0 10px;
+}
+.mod-setting-panel li {
+    padding:2px 0;
+    list-style: none;
+}
+.mod-setting-panel li:hover {
+    color:#600;
+}
+.mod-setting-panel ol {
+    padding:0 0 0 10px;
+}
+.mod-setting-panel .btns {
+    margin-top:20px;
+}
+.mod-setting-panel input[name="reset"] {
+    margin-left:20px;
+}
+.mod-setting-panel form {
+    margin:0;
+    padding:0;
+}
+.t-collapse .mod-setting-panel {
+    position: fixed;
+    right: 8px;
+}
+
+/*================ 皮肤:theme-default ===================*/
+body.theme-default {
+    background-color: #fff;
+}
+.theme-default .item .key {
+    color: #000;
+}
+
+.theme-default .item .kv-list {
+    border-left: 1px dashed #bbb;
+}
+.theme-default .item .string {
+    color: #0B7500;
+}
+
+.theme-default .item .string a {
+    color: #00b;
+    text-decoration: underline;
+}
+
+.theme-default .item .string a:hover {
+    color: #b00;
+}
+
+.theme-default .item .bool,
+.theme-default .item .null,
+.theme-default .item .number {
+    font-weight: bold;
+    color: #ff6b00
+}
+.theme-default .item .number {
+    color:#1A01CC;
+}
+
+.theme-default .item .item-object:hover > span,
+.theme-default .item .item-array:hover > span,
+.theme-default .item .item-block:hover > span{
+    font-weight: bold;
+    color: #f00;
+}
+
+.theme-default .item .item-line:hover,
+.theme-default .item-line.x-selected{
+    background: #f9f9f9;
+}
+
+.theme-default .item .item-line:hover span ,
+.theme-default .item.x-selected>span {
+    font-weight: bolder;
+    background: #ff5;
+}
+
+
+/*================ 皮肤:theme-simple ===================*/
+.theme-simple .rootItem {
+    white-space: pre !important;
+    color: #000;
+}
+
+
+/*================ 皮肤:theme-light ===================*/
+.theme-light .item .key {
+    color: #4d4d4c;
+}
+
+.theme-light .item .kv-list {
+    border-left: 1px dashed #f1f1f1;
+}
+.theme-light .item .string {
+    color: #718c00;
+}
+
+.theme-light .item .string a {
+    color: #00b;
+    text-decoration: underline;
+}
+
+.theme-light .item .string a:hover {
+    color: #b00;
+}
+.theme-light .item .brace {
+    font-weight: normal;
+}
+.theme-light .item .bool,
+.theme-light .item .null,
+.theme-light .item .number {
+    font-weight: bold;
+    color: #f5871f
+}
+.theme-light .item .number {
+    color:#ff7c00;
+}
+
+.theme-light .item .item-object:hover > span,
+.theme-light .item .item-array:hover > span,
+.theme-light .item .item-block:hover > span{
+
+}
+
+.theme-light .item .item-line:hover,
+.theme-light .item-line.x-selected {
+    background: #f7f3f3;
+}
+
+.theme-light .item .item-line:hover span ,
+.theme-light .item.x-selected>span {
+    font-weight: bolder;
+    background: #f7f3f3;
+}
+.theme-light .item .quote {
+    color:#ccc;
+}
+
+
+/*================ 皮肤:theme-dark ===================*/
+.theme-dark {
+    background:#000;
+}
+.theme-dark .x-toolbar {
+    background: -webkit-linear-gradient(#222,#222,#111);
+    border: 1px solid #333;
+    border-bottom-color: #363636;
+    color:#ddd;
+}
+.theme-dark .x-toolbar .x-a-title {
+    color:#48b;
+}
+.theme-dark .xjf-btn {
+    background:-webkit-linear-gradient(#222,#222,#111);
+    border:1px solid #444;
+    color:#ddd;
+}
+.theme-dark .xjf-btn:hover {
+    background:-webkit-linear-gradient(#333,#333,#222);
+}
+.theme-dark #statusBar {
+    background: #333;
+    color: #ddd;
+    border-top: 1px solid #555;
+}
+.theme-dark .boxOpt {
+    background: -webkit-linear-gradient(#222,#222,#111);
+    border: 1px solid #333;
+}
+.theme-dark .boxOpt a:hover {
+    color:#eee;
+}
+.theme-dark .mod-setting-panel {
+    background: #222;
+    color: #fff;
+    border-color: #444;
+    box-shadow: 1px 1px #111;
+}
+.theme-dark .mod-setting-panel li:hover {
+    color:#ff0;
+}
+.theme-dark .mod-setting-panel input[name="maxlength"] {
+    background: #000;
+    border: 1px solid #555;
+    color: #fff;
+}
+
+.theme-dark .item .key {
+    color: #fff;
+}
+
+.theme-dark .item .kv-list {
+    border-left: 1px dashed #222;
+}
+.theme-dark .item .string {
+    color: #0f0;
+}
+
+.theme-dark .item .string a {
+    color: #369ad6;
+    text-decoration: underline;
+}
+
+.theme-dark .item .string a:hover {
+    color: #b00;
+}
+.theme-dark .item .brace,
+.theme-dark .item .colon,
+.theme-dark .item .comma{
+    color:#eaeaea;
+}
+
+.theme-dark .item .bool,
+.theme-dark .item .null,
+.theme-dark .item .number {
+    font-weight: bold;
+    color: #f00
+}
+.theme-dark .item .item-object:hover > span,
+.theme-dark .item .item-array:hover > span,
+.theme-dark .item .item-block:hover > span{
+    font-weight: bold;
+    color: orange;
+}
+
+.theme-dark .item .item-line:hover,
+.theme-dark .item-line.x-selected {
+    background: #111;
+}
+
+.theme-dark .item .item-line:hover span ,
+.theme-dark .item.x-selected>span {
+    font-weight: bolder;
+    background: #444;
+}
+.theme-dark .item .quote {
+    color:#eaeaea;
+}
+.theme-dark .item .expand {
+    color:#fff;
+}
+
+
+/*================ 皮肤:theme-vscode ===================*/
+.theme-vscode .item .key {
+    color: #f00;
+}
+.theme-vscode .item .quote {
+    color:#eaa;
+}
+.theme-vscode .item .kv-list {
+    border-left: 1px dashed #f1f1f1;
+}
+.theme-vscode .item .string {
+    color: #a31515;
+}
+.theme-vscode .item .string a {
+    color: #00b;
+    text-decoration: underline;
+}
+.theme-vscode .item .string a:hover {
+    color: #b00;
+}
+.theme-vscode .item .brace {
+    font-weight: normal;
+}
+.theme-vscode .item .bool,
+.theme-vscode .item .null,
+.theme-vscode .item .number {
+    font-weight: bold;
+    color: #a31515
+}
+.theme-vscode .item .number {
+    color: blue;
+}
+.theme-vscode .item .item-line:hover,
+.theme-vscode .item-line.x-selected {
+    background: #f7f3f3;
+}
+.theme-vscode .item .item-line:hover span ,
+.theme-vscode .item.x-selected>span {
+    font-weight: bolder;
+    background: #f7f3f3;
+}
+
+
+/*================ 皮肤:theme-github ===================*/
+.theme-github {
+    background: #f8f8f8;
+}
+.theme-github .item .key {
+    color: #333;
+}
+.theme-github .item .quote {
+    color:#aaa;
+}
+.theme-github .item .kv-list {
+    border-left: 1px dashed #e8e8e8;
+}
+.theme-github .item .string {
+    color: #d14;
+}
+.theme-github .item .string a {
+    color: #00b;
+    text-decoration: underline;
+}
+.theme-github .item .string a:hover {
+    color: #b00;
+}
+.theme-github .item .brace {
+    font-weight: normal;
+}
+.theme-github .item .bool,
+.theme-github .item .null,
+.theme-github .item .number {
+    font-weight: bold;
+    color: #008080
+}
+.theme-github .item .number {
+    color: blue;
+}
+.theme-github .item .item-line:hover,
+.theme-github .item-line.x-selected {
+    background: #f7f3f3;
+}
+.theme-github .item .item-line:hover span ,
+.theme-github .item.x-selected>span {
+    font-weight: bolder;
+    background: #f7f3f3;
+}
+
+
+/*================ 皮肤:theme-vegetarian ===================*/
+.theme-vegetarian .item-line.x-selected {
+    background: #f9f9f9;
+}

+ 388 - 165
apps/json-format/content-script.js

@@ -2,6 +2,7 @@
  * Json Page Automatic Format Via FeHelper
  * @author zhaoxianlie
  */
+
 window.JsonAutoFormat = (() => {
 
     // 留100ms时间给静态文件加载,当然,这个代码只是留给未开发过程中用的
@@ -28,43 +29,84 @@ window.JsonAutoFormat = (() => {
     __importScript('json-abc.js');
     __importScript('json-decode.js');
 
-    // **************************************************************
-
     const JSON_SORT_TYPE_KEY = 'json_sort_type_key';
-    const JSON_AUTO_DECODE = 'json_auto_decode';
-    const JSON_TOOL_BAR_ALWAYS_SHOW = 'JSON_TOOL_BAR_ALWAYS_SHOW';
 
-    // 用于记录最原始的json串
-    let originalJsonStr = '';
-    let curSortType = 0;
+    // 本地永久存储的key
+    const STORAGE_KEYS = {
+        // 总是开启JSON自动格式化功能
+        JSON_PAGE_FORMAT: 'JSON_PAGE_FORMAT',
+        // 总是显示顶部工具栏
+        JSON_TOOL_BAR_ALWAYS_SHOW: 'JSON_TOOL_BAR_ALWAYS_SHOW',
+        // 启用底部状态栏
+        STATUS_BAR_ALWAYS_SHOW: 'STATUS_BAR_ALWAYS_SHOW',
+        // 自动进行URL、Unicode解码
+        AUTO_TEXT_DECODE: 'AUTO_TEXT_DECODE',
+        // 修正乱码
+        FIX_ERROR_ENCODING: 'FIX_ERROR_ENCODING',
+        // 启用JSON key排序功能
+        ENABLE_JSON_KEY_SORT: 'ENABLE_JSON_KEY_SORT',
+        // 保留键值双引号
+        KEEP_KEY_VALUE_DBL_QUOTE: 'KEEP_KEY_VALUE_DBL_QUOTE',
+        // 最大json key数量
+        MAX_JSON_KEYS_NUMBER: 'MAX_JSON_KEYS_NUMBER',
+        // 自定义皮肤
+        JSON_FORMAT_THEME: 'JSON_FORMAT_THEME'
+    };
+
+    // 皮肤定义
+    const SKIN_THEME = {
+        '0': 'theme-default',
+        '1': 'theme-simple',
+        '2': 'theme-light',
+        '3': 'theme-dark',
+        '4': 'theme-vscode',
+        '5': 'theme-github',
+        '6': 'theme-vegetarian'
+    };
+
     // JSONP形式下的callback name
     let funcName = null;
     let jsonObj = null;
     let fnTry = null;
     let fnCatch = null;
 
-    let autoDecode = false;
+    // 格式化的配置
+    let formatOptions = {
+        JSON_FORMAT_THEME: 0,
+        sortType: 0,
+        autoDecode: false,
+        originalSource: ''
+    };
+
+    // 获取JSON格式化的配置信息
+    let _getAllOptions = (success) => {
+        chrome.runtime.sendMessage({
+            type: 'fh-dynamic-any-thing',
+            thing:'request-jsonformat-options',
+            params: STORAGE_KEYS
+        }).then(result => success(result));
+    };
 
     let _getHtmlFragment = () => {
         return [
-            '<div class="x-toolbar" style="display:none">' +
-            '    <a href="http://www.baidufe.com/fehelper/feedback.html" target="_blank" class="x-a-title">' +
+            '<div id="jfToolbar" class="x-toolbar" style="display:none">' +
+            '    <a href="https://www.baidufe.com/fehelper/feedback.html" target="_blank" class="x-a-title">' +
             '        <img src="' + chrome.runtime.getURL('static/img/fe-16.png') + '" alt="fehelper"/> FeHelper</a>' +
-            '        <span class="x-b-title"></span>' +
-            '        <span class="x-split">|</span>\n' +
-            '        <input type="checkbox" id="json_endecode"><label for="json_endecode">自动解码</label>' +
-            '        <span class="x-sort">' +
-            '            <span class="x-split">|</span>' +
-            '            <span class="x-stitle">排序:</span>' +
-            '            <label for="sort_null">默认</label><input type="radio" name="jsonsort" id="sort_null" value="0" checked>' +
-            '            <label for="sort_asc">升序</label><input type="radio" name="jsonsort" id="sort_asc" value="1">' +
-            '            <label for="sort_desc">降序</label><input type="radio" name="jsonsort" id="sort_desc" value="-1">' +
-            '        </span>' +
-            '    <span class="x-split">|</span>\n' +
-            '    <button class="xjf-btn" id="jsonGetCorrectCnt">乱码修正</button>' +
+            '    <span class="x-b-title"></span>' +
+            '    <span class="x-sort">' +
+            '        <span class="x-split">|</span>' +
+            '        <span class="x-stitle">排序:</span>' +
+            '        <label for="sort_null">默认</label><input type="radio" name="jsonsort" id="sort_null" value="0" checked>' +
+            '        <label for="sort_asc">升序</label><input type="radio" name="jsonsort" id="sort_asc" value="1">' +
+            '        <label for="sort_desc">降序</label><input type="radio" name="jsonsort" id="sort_desc" value="-1">' +
+            '    </span>' +
+            '    <span class="x-fix-encoding"><span class="x-split">|</span><button class="xjf-btn" id="jsonGetCorrectCnt">乱码修正</button></span>' +
             '    <span id="optionBar"></span>' +
             '    <span class="fe-feedback">' +
-            '        <a id="toggleBtn" title="展开或收起工具栏">隐藏&gt;&gt;</a>' +
+            '       <span class="x-settings"><svg aria-hidden="true" height="16" version="1.1" viewBox="0 0 14 16" width="14">' +
+            '           <path fill-rule="evenodd" d="M14 8.77v-1.6l-1.94-.64-.45-1.09.88-1.84-1.13-1.13-1.81.91-1.09-.45-.69-1.92h-1.6l-.63 1.94-1.11.45-1.84-.88-1.13 1.13.91 1.81-.45 1.09L0 7.23v1.59l1.94.64.45 1.09-.88 1.84 1.13 1.13 1.81-.91 1.09.45.69 1.92h1.59l.63-1.94 1.11-.45 1.84.88 1.13-1.13-.92-1.81.47-1.09L14 8.75v.02zM7 11c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3z"></path>' +
+            '       </svg>高级定制</span>' +
+            '       <a id="toggleBtn" title="展开或收起工具栏">隐藏&gt;&gt;</a>' +
             '    </span>' +
             '</div>',
             '<div id="formattingMsg"><span class="x-loading"></span>格式化中...</div>',
@@ -77,6 +119,276 @@ window.JsonAutoFormat = (() => {
         ].join('')
     };
 
+    let _createSettingPanel = () => {
+        let html = `<div id="jfSettingPanel" class="mod-setting-panel">
+            <h4>基本配置项</h4>
+            <form action="#">
+                <ul>
+                    <li><label><input type="checkbox" name="alwaysOn" value="1">总是开启JSON自动格式化功能</label></li>
+                    <li><label><input type="checkbox" name="alwaysShowToolbar" value="1">总是显示顶部工具栏</label></li>
+                    <li><label><input type="checkbox" name="alwaysShowStatusbar" value="1">启用状态栏(包含复制/下载/删除)</label></li>
+                    <li><label><input type="checkbox" name="autoDecode" value="1">自动进行URL、Unicode解码</label></li>
+                    <li><label><input type="checkbox" name="errorEncoding" value="1">乱码修正(需手动操作,一键修正)</label></li>
+                    <li><label><input type="checkbox" name="enableSort" value="1">启用JSON键名排序功能</label></li>
+                    <li><label><input type="checkbox" name="keepQuote" value="1">格式化后保留键值对的双引号</label></li>
+                    <li><label><input type="text" name="maxlength" value="10000">最大支持的JSON Key数量</label></li>
+               </ul>
+
+               <h4>自定义皮肤</h4>
+               <ul>
+                    <li><label><input type="radio" name="skinId" value="0">默认模式(简约风格)</label></li>
+                    <li><label><input type="radio" name="skinId" value="1">极简模式(纯源码)</label></li>
+                    <li><label><input type="radio" name="skinId" value="2">清爽模式(明亮、跳跃)</label></li>
+                    <li><label><input type="radio" name="skinId" value="3">暗黑模式(安静、忧郁)</label></li>
+                    <li><label><input type="radio" name="skinId" value="4">vscode模式(醒目、专注)</label></li>
+                    <li><label><input type="radio" name="skinId" value="5">github模式(纵享丝滑)</label></li>
+                    <li><label><input type="radio" name="skinId" value="6">素人模式(清心寡欲)</label></li>
+               </ul>
+
+               <div class="btns">
+                    <input type="submit" class="xjf-btn" name="submit" value="确定">
+                    <input type="reset" class="xjf-btn" name="reset" value="取消">
+               </div>
+            </form>
+        </div>`;
+
+        let sPanel = $('#jfSettingPanel');
+        if (!sPanel.length) {
+            sPanel = $(html).appendTo('#jfToolbar');
+            // 表单提交时,保存数据
+            sPanel.find('input[type="submit"]').on('click', function (e) {
+                e.preventDefault();
+                e.stopPropagation();
+
+                let formData = {};
+                formData.JSON_PAGE_FORMAT = sPanel.find('input[name="alwaysOn"]').prop('checked');
+                formData.JSON_TOOL_BAR_ALWAYS_SHOW = sPanel.find('input[name="alwaysShowToolbar"]').prop('checked');
+                formData.STATUS_BAR_ALWAYS_SHOW = sPanel.find('input[name="alwaysShowStatusbar"]').prop('checked');
+                formData.AUTO_TEXT_DECODE = sPanel.find('input[name="autoDecode"]').prop('checked');
+                formData.FIX_ERROR_ENCODING = sPanel.find('input[name="errorEncoding"]').prop('checked');
+                formData.ENABLE_JSON_KEY_SORT = sPanel.find('input[name="enableSort"]').prop('checked');
+                formData.KEEP_KEY_VALUE_DBL_QUOTE = sPanel.find('input[name="keepQuote"]').prop('checked');
+                formData.MAX_JSON_KEYS_NUMBER = sPanel.find('input[name="maxlength"]').val();
+                formData.JSON_FORMAT_THEME = sPanel.find('input[name="skinId"]:checked').val();
+
+                chrome.runtime.sendMessage({
+                    type: 'fh-dynamic-any-thing',
+                    thing: 'save-jsonformat-options',
+                    params: formData
+                }, result => sPanel.hide());
+            });
+
+            sPanel.find('input[name="alwaysShowToolbar"]').on('click', function (e) {
+                $('.fe-feedback #toggleBtn').trigger('click');
+            });
+
+            sPanel.find('input[name="errorEncoding"]').on('click', function (e) {
+                let el = $('#jfToolbar').find('.x-fix-encoding');
+                $(this).prop('checked') ? el.show() : el.hide();
+            });
+
+            sPanel.find('input[name="enableSort"]').on('click', function (e) {
+                let el = $('#jfToolbar').find('.x-sort');
+                $(this).prop('checked') ? el.show() : el.hide();
+            });
+
+            sPanel.find('input[type="reset"]').on('click', (e) => sPanel.hide());
+
+            sPanel.find('input[name="skinId"]').on('click', function (e) {
+                formatOptions.JSON_FORMAT_THEME = this.value;
+                _didFormat();
+            });
+
+            sPanel.find('input[name="alwaysShowStatusbar"]').on('click', function (e) {
+                formatOptions.STATUS_BAR_ALWAYS_SHOW = $(this).prop('checked');
+                let elBody = $('body');
+                if (formatOptions.STATUS_BAR_ALWAYS_SHOW) {
+                    elBody.removeClass('hide-status-bar');
+                } else {
+                    elBody.addClass('hide-status-bar');
+                }
+            });
+
+            sPanel.find('input[name="keepQuote"]').on('click', function (e) {
+                formatOptions.KEEP_KEY_VALUE_DBL_QUOTE = $(this).prop('checked');
+                let elBody = $('body');
+                if (formatOptions.KEEP_KEY_VALUE_DBL_QUOTE) {
+                    elBody.removeClass('remove-quote');
+                } else {
+                    elBody.addClass('remove-quote');
+                }
+            });
+        } else if (sPanel[0].offsetHeight) {
+            return sPanel.hide();
+        } else {
+            sPanel.show();
+        }
+
+        _getAllOptions(result => {
+            result.JSON_PAGE_FORMAT && sPanel.find('input[name="alwaysOn"]').prop('checked', true);
+            result.JSON_TOOL_BAR_ALWAYS_SHOW && sPanel.find('input[name="alwaysShowToolbar"]').prop('checked', true);
+            result.STATUS_BAR_ALWAYS_SHOW && sPanel.find('input[name="alwaysShowStatusbar"]').prop('checked', true);
+            result.AUTO_TEXT_DECODE && sPanel.find('input[name="autoDecode"]').prop('checked', true);
+            result.FIX_ERROR_ENCODING && sPanel.find('input[name="errorEncoding"]').prop('checked', true);
+            result.ENABLE_JSON_KEY_SORT && sPanel.find('input[name="enableSort"]').prop('checked', true);
+            result.KEEP_KEY_VALUE_DBL_QUOTE && sPanel.find('input[name="keepQuote"]').prop('checked', true);
+            sPanel.find('input[name="maxlength"]').attr('value', result.MAX_JSON_KEYS_NUMBER || 10000);
+            sPanel.find(`input[name="skinId"][value="${result.JSON_FORMAT_THEME || 0}"]`).attr('checked', true);
+        });
+    };
+
+
+    // 检测当前页面的CSP,防止出现这种情况:
+    // DOMException: Failed to read the 'localStorage' property from 'Window': The document is sandboxed and lacks the 'allow-same-origin' flag.
+    let _checkContentSecurityPolicy = () => {
+        try {
+            localStorage.getItem(1);
+        } catch (e) {
+            return false;
+        }
+        return true;
+    };
+
+    let _initToolbar = () => {
+
+        let cspSafe = _checkContentSecurityPolicy();
+        if (cspSafe) {
+            // =============================排序:获取上次记录的排序方式
+            if (formatOptions.ENABLE_JSON_KEY_SORT) {
+                formatOptions.sortType = parseInt(localStorage.getItem(JSON_SORT_TYPE_KEY) || 0);
+                // 排序选项初始化
+                $('[name=jsonsort][value=' + formatOptions.sortType + ']').attr('checked', 1);
+            } else {
+                formatOptions.sortType = 0;
+                $('#jfToolbar .x-sort').hide();
+            }
+
+            // =============================事件初始化
+            $('[name=jsonsort]').click(function (e) {
+                let sortType = parseInt(this.value);
+                if (sortType !== formatOptions.sortType) {
+                    formatOptions.sortType = sortType;
+                    _didFormat();
+                }
+                localStorage.setItem(JSON_SORT_TYPE_KEY, sortType);
+            });
+        } else {
+            $('#jfToolbar .x-sort').hide();
+        }
+
+
+        // =============================乱码修正
+        if (!formatOptions.FIX_ERROR_ENCODING) {
+            $('#jfToolbar .x-fix-encoding').hide();
+        }
+
+        // =============================工具栏的显示与隐藏控制
+        let toolBarClassList = document.querySelector('#jfToolbar').classList;
+        let tgBtn = $('.fe-feedback #toggleBtn');
+        if (formatOptions.JSON_TOOL_BAR_ALWAYS_SHOW) {
+            toolBarClassList.remove('t-collapse');
+            tgBtn.html('隐藏&gt;&gt;');
+        } else {
+            toolBarClassList.add('t-collapse');
+            tgBtn.html('&lt;&lt;');
+        }
+        tgBtn.click(function (e) {
+            e.preventDefault();
+            e.stopPropagation();
+
+            chrome.runtime.sendMessage({
+                type: 'fh-dynamic-any-thing',
+                thing: 'toggle-jsonformat-options'
+            }).then(show => {
+                let toolBarClassList = document.querySelector('#jfToolbar').classList;
+                if (show) {
+                    toolBarClassList.remove('t-collapse');
+                    tgBtn.html('隐藏&gt;&gt;');
+                } else {
+                    toolBarClassList.add('t-collapse');
+                    tgBtn.html('&lt;&lt;');
+                }
+                $('#jfToolbar input[name="alwaysShowToolbar"]').prop('checked', show);
+            });
+        });
+
+        $('.fe-feedback .x-settings').click(e => _createSettingPanel());
+        $('#jsonGetCorrectCnt').click(e => _getCorrectContent());
+    };
+
+    let _didFormat = function () {
+        let source = formatOptions.originalSource;
+
+        if (formatOptions.sortType !== 0) {
+            let jsonObj = JsonABC.sortObj(JSON.parse(formatOptions.originalSource), parseInt(formatOptions.sortType), true);
+            source = JSON.stringify(jsonObj);
+        }
+
+        let elBody = $('body');
+
+        let theme = SKIN_THEME[formatOptions.JSON_FORMAT_THEME || 0];
+        Object.values(SKIN_THEME).forEach(th => elBody.removeClass(th));
+        elBody.addClass(theme);
+
+        // 控制引号
+        if (formatOptions.KEEP_KEY_VALUE_DBL_QUOTE) {
+            elBody.removeClass('remove-quote');
+        } else {
+            elBody.addClass('remove-quote');
+        }
+
+        // 控制底部状态栏
+        if (formatOptions.STATUS_BAR_ALWAYS_SHOW) {
+            elBody.removeClass('hide-status-bar');
+        } else {
+            elBody.addClass('hide-status-bar');
+        }
+
+        if (formatOptions.autoDecode) {
+            (async () => {
+                let txt = await JsonEnDecode.urlDecodeByFetch(source);
+                source = JsonEnDecode.uniDecode(txt);
+
+                // 格式化
+                try {
+                    Formatter.format(source, theme);
+                } catch (e) {
+                    Formatter.formatSync(source, theme)
+                }
+                $('#jfToolbar').fadeIn(500);
+            })();
+        } else {
+            // 格式化
+            try {
+                Formatter.format(source, theme);
+            } catch (e) {
+                Formatter.formatSync(source, theme)
+            }
+
+            $('#jfToolbar').fadeIn(500);
+        }
+
+
+        // 如果是JSONP格式的,需要把方法名也显示出来
+        if (funcName != null) {
+            if (fnTry && fnCatch) {
+                $('#jfCallbackName_start').html('<pre style="padding:0">' + fnTry + '</pre>' + funcName + '(');
+                $('#jfCallbackName_end').html(')<br><pre style="padding:0">' + fnCatch + '</pre>');
+            } else {
+                $('#jfCallbackName_start').html(funcName + '(');
+                $('#jfCallbackName_end').html(')');
+            }
+        }
+    };
+
+    let _getCorrectContent = function () {
+        fetch(location.href).then(res => res.text()).then(text => {
+            formatOptions.originalSource = text;
+            _didFormat();
+        });
+    };
+
+
     /**
      * 从页面提取JSON文本
      * @returns {string}
@@ -90,10 +402,19 @@ window.JsonAutoFormat = (() => {
         if (!source) {
             source = (document.body.textContent || '').trim()
         }
+
+        // 如果是js内容,则不进行json格式化
+        let isJs = /\.js$/.test(new URL(location.href).pathname);
+        isJs = isJs && document.contentType === 'application/javascript';
+        if (isJs) {
+            source = '';
+        }
+
         if (!source) {
             return false;
         }
 
+
         // 1、如果body的内容还包含HTML标签,肯定不是合法的json了
         // 2、如果是合法的json,也只可能有一个text节点
         // 3、但是要兼容一下其他插件对页面的破坏情况
@@ -113,10 +434,22 @@ window.JsonAutoFormat = (() => {
                 if (nodes[i].nodeType === Node.ELEMENT_NODE) {
                     let tagName = elm.tagName.toLowerCase();
                     let text = (elm.textContent || '').trim();
-                    // 如果是pre标签,则看内容是不是和source一样,一样则continue
-                    if (!((tagName === 'pre' && text === source)
-                            || ((elm.offsetWidth + elm.offsetHeight === 0 || !text)
-                                && !['script', 'link'].includes(tagName)))) {
+
+                    // 如果包含了script和link标签,需要看标签的src和href属性值,如果不是chrome-extensions注入的,也要跳出
+                    if (['script', 'link'].includes(tagName)) {
+                        let url = elm.getAttribute('src') || elm.getAttribute('href');
+                        if (!!url && !/^chrome\-extension:\/\//.test(url)) {
+                            return false;
+                        }
+                    }
+
+                    // 如果不是pre标签,并且还不是隐藏节点,且内容不为空,也要跳出
+                    else if (tagName !== 'pre' && (elm.offsetWidth + elm.offsetHeight !== 0 && !!text)) {
+                        return false;
+                    }
+
+                    // 如果是pre标签,但当前节点内容与最初body.textContent提取值不一致,都跳出
+                    else if (tagName === 'pre' && text !== source) {
                         return false;
                     }
                 } else {
@@ -151,6 +484,13 @@ window.JsonAutoFormat = (() => {
         return count;
     };
 
+    // 用新的options来覆盖默认options
+    let _extendsOptions = options => {
+        options = options || {};
+        Object.keys(options).forEach(opt => formatOptions[opt] = options[opt]);
+    };
+
+
     /**
      * 执行format操作
      * @private
@@ -162,6 +502,8 @@ window.JsonAutoFormat = (() => {
             return;
         }
 
+        _extendsOptions(options);
+
         // 下面校验给定字符串是否为一个合法的json
         try {
 
@@ -228,163 +570,44 @@ window.JsonAutoFormat = (() => {
             }
 
             // JSON的所有key不能超过预设的值,比如 10000 个,要不然自动格式化会比较卡
-            if (options && options['MAX_JSON_KEYS_NUMBER']) {
+            if (formatOptions['MAX_JSON_KEYS_NUMBER']) {
                 let keysCount = _getAllKeysCount(jsonObj);
-                if (keysCount > options['MAX_JSON_KEYS_NUMBER']) {
-                    let msg = '当前JSON共 <b style="color:red">' + keysCount + '</b> 个Key,大于预设值' + options['MAX_JSON_KEYS_NUMBER'] + ',已取消自动格式化;可到FeHelper设置页调整此配置!';
+                if (keysCount > formatOptions['MAX_JSON_KEYS_NUMBER']) {
+                    let msg = '当前JSON共 <b style="color:red">' + keysCount + '</b> 个Key,大于预设值' + formatOptions['MAX_JSON_KEYS_NUMBER'] + ',已取消自动格式化;可到FeHelper设置页调整此配置!';
                     return toast(msg);
                 }
             }
 
-            let preLength = $('body>pre').hide().length;
             $('body').prepend(_getHtmlFragment());
+            let preLength = $('body>pre').remove().length;
             if (!preLength) {
                 Array.prototype.slice.call(document.body.childNodes).forEach(node => {
                     (node.nodeType === Node.TEXT_NODE) && node.remove();
                 });
             }
 
-            originalJsonStr = source;
-
-            // 获取上次记录的排序方式
-            curSortType = parseInt(localStorage.getItem(JSON_SORT_TYPE_KEY) || 0);
-            _didFormat(curSortType);
-
-            // 排序选项初始化
-            $('[name=jsonsort][value=' + curSortType + ']').attr('checked', 1);
-
-            // 自动解码选项初始化
-            autoDecode = localStorage.getItem(JSON_AUTO_DECODE);
-            if (autoDecode === null) {
-                autoDecode = (options && options['AUTO_TEXT_DECODE']);
-            } else {
-                autoDecode = autoDecode === 'true';
-            }
-            $('#json_endecode').prop('checked', autoDecode);
-
-            _bindEvent();
-        }
-    };
-
-    let _didFormat = function (sortType) {
-        sortType = sortType || 0;
-        let source = originalJsonStr;
-
-        if (sortType !== 0) {
-            let jsonObj = JsonABC.sortObj(JSON.parse(originalJsonStr), parseInt(sortType), true);
-            source = JSON.stringify(jsonObj);
-        }
-
-        if (autoDecode) {
-            (async () => {
-                let txt = await JsonEnDecode.urlDecodeByFetch(source);
-                source = JsonEnDecode.uniDecode(txt);
-
-                // 格式化
-                Formatter.format(source);
-                $('.x-toolbar').fadeIn(500);
-            })();
-        } else {
-            // 格式化
-            Formatter.format(source);
-            $('.x-toolbar').fadeIn(500);
-        }
-
+            formatOptions.originalSource = source;
 
-        // 如果是JSONP格式的,需要把方法名也显示出来
-        if (funcName != null) {
-            if (fnTry && fnCatch) {
-                $('#jfCallbackName_start').html('<pre style="padding:0">' + fnTry + '</pre>' + funcName + '(');
-                $('#jfCallbackName_end').html(')<br><pre style="padding:0">' + fnCatch + '</pre>');
-            } else {
-                $('#jfCallbackName_start').html(funcName + '(');
-                $('#jfCallbackName_end').html(')');
-            }
+            _initToolbar();
+            _didFormat();
         }
     };
 
-    let _getCorrectContent = function () {
-        fetch(location.href).then(res => res.text()).then(text => {
-            originalJsonStr = text;
-            _didFormat(curSortType);
-        });
-    };
-
-    let _bindEvent = function () {
-        $('[name=jsonsort]').click(function (e) {
-            let sortType = parseInt(this.value);
-            if (sortType !== curSortType) {
-                _didFormat(sortType);
-                curSortType = sortType;
-            }
-            localStorage.setItem(JSON_SORT_TYPE_KEY, sortType);
-        });
-
-        $('#json_endecode').click(function (e) {
-            autoDecode = this.checked;
-            localStorage.setItem(JSON_AUTO_DECODE, autoDecode);
-            _didFormat(curSortType);
-        });
-
-        let tgBtn = $('.fe-feedback #toggleBtn').click(function (e) {
-            e.preventDefault();
-            e.stopPropagation();
-
-            chrome.runtime.sendMessage({
-                type: 'fh-dynamic-any-thing'
-            }, (params) => {
-                let show = String(localStorage.getItem(JSON_TOOL_BAR_ALWAYS_SHOW)) !== 'false';
-                localStorage.setItem(JSON_TOOL_BAR_ALWAYS_SHOW, !show);
-
-                let toolBarClassList = document.querySelector('div.x-toolbar').classList;
-                if (!show) {
-                    toolBarClassList.remove('t-collapse');
-                    tgBtn.html('隐藏&gt;&gt;');
-                } else {
-                    toolBarClassList.add('t-collapse');
-                    tgBtn.html('&lt;&lt;');
-                }
-            });
-        });
-
-        chrome.runtime.sendMessage({
-            type: 'fh-dynamic-any-thing'
-        }, params => {
-            let show = String(localStorage.getItem(JSON_TOOL_BAR_ALWAYS_SHOW)) !== 'false';
-            let toolBarClassList = document.querySelector('div.x-toolbar').classList;
-            if (show) {
-                toolBarClassList.remove('t-collapse');
-                tgBtn.html('隐藏&gt;&gt;');
-            } else {
-                toolBarClassList.add('t-collapse');
-                tgBtn.html('&lt;&lt;');
-            }
-        });
-
-        $('#jsonGetCorrectCnt').click(function (e) {
-            _getCorrectContent();
-        });
-
-        $('#capturePage').click(function (e) {
-            chrome.runtime.sendMessage({
-                type: 'capture-visible-page'
-            }, uri => {
-                window.open(uri);
-            });
-            e.preventDefault();
-            e.stopPropagation();
-        });
-    };
-
     return {
-        format: (options) => {
-            setTimeout(() => {
-                _format(options);
-            }, pleaseLetJsLoaded);
-        }
+        format: () => _getAllOptions(result => {
+            if(result.JSON_PAGE_FORMAT) {
+                let intervalId = setInterval(() => {
+                    if(typeof Formatter !== 'undefined') {
+                        clearInterval(intervalId);
+                        _format(result);
+                    }
+                },pleaseLetJsLoaded);
+            }
+        })
     };
 })();
 
+
 if(location.protocol !== 'chorme-extension:') {
-    window.JsonAutoFormat.format({JSON_PAGE_FORMAT: true});
+    window.JsonAutoFormat.format();
 }

+ 453 - 543
apps/json-format/format-lib.js

@@ -1,18 +1,98 @@
 /**
- * FeHelper Json Format Lib
+ * 日期格式化
+ * @param {Object} pattern
+ */
+Date.prototype.format = function (pattern) {
+    let pad = function (source, length) {
+        let pre = "",
+            negative = (source < 0),
+            string = String(Math.abs(source));
+
+        if (string.length < length) {
+            pre = (new Array(length - string.length + 1)).join('0');
+        }
+
+        return (negative ? "-" : "") + pre + string;
+    };
+
+    if ('string' !== typeof pattern) {
+        return this.toString();
+    }
+
+    let replacer = function (patternPart, result) {
+        pattern = pattern.replace(patternPart, result);
+    };
+
+    let year = this.getFullYear(),
+        month = this.getMonth() + 1,
+        date2 = this.getDate(),
+        hours = this.getHours(),
+        minutes = this.getMinutes(),
+        seconds = this.getSeconds(),
+        milliSec = this.getMilliseconds();
+
+    replacer(/yyyy/g, pad(year, 4));
+    replacer(/yy/g, pad(parseInt(year.toString().slice(2), 10), 2));
+    replacer(/MM/g, pad(month, 2));
+    replacer(/M/g, month);
+    replacer(/dd/g, pad(date2, 2));
+    replacer(/d/g, date2);
+
+    replacer(/HH/g, pad(hours, 2));
+    replacer(/H/g, hours);
+    replacer(/hh/g, pad(hours % 12, 2));
+    replacer(/h/g, hours % 12);
+    replacer(/mm/g, pad(minutes, 2));
+    replacer(/m/g, minutes);
+    replacer(/ss/g, pad(seconds, 2));
+    replacer(/s/g, seconds);
+    replacer(/SSS/g, pad(milliSec, 3));
+    replacer(/S/g, milliSec);
+
+    return pattern;
+};
+
+/**
+ * 自动消失的Alert弹窗
+ * @param content
  */
-let JsonFormatEntrance = (function () {
+window.toast = function (content) {
+    window.clearTimeout(window.feHelperAlertMsgTid);
+    let elAlertMsg = document.querySelector("#fehelper_alertmsg");
+    if (!elAlertMsg) {
+        let elWrapper = document.createElement('div');
+        elWrapper.innerHTML = '<div id="fehelper_alertmsg" style="position:fixed;bottom:25px;left:5px;z-index:1000000">' +
+            '<p style="background:#000;display:inline-block;color:#fff;text-align:center;' +
+            'padding:10px 10px;margin:0 auto;font-size:14px;border-radius:4px;">' + content + '</p></div>';
+        elAlertMsg = elWrapper.childNodes[0];
+        document.body.appendChild(elAlertMsg);
+    } else {
+        elAlertMsg.querySelector('p').innerHTML = content;
+        elAlertMsg.style.display = 'block';
+    }
+
+    window.feHelperAlertMsgTid = window.setTimeout(function () {
+        elAlertMsg.style.display = 'none';
+    }, 1000);
+};
+
+
+/**
+ * FeHelper Json Format Lib,入口文件
+ * @example
+ *  Formatter.format(jsonString)
+ */
+window.Formatter = (function () {
 
     "use strict";
 
     let jfContent,
         jfPre,
         jfStyleEl,
-        jfOptEl,
-        jfPathEl,
+        jfStatusBar,
         formattingMsg;
 
-    let lastKvovIdGiven = 0;
+    let lastItemIdGiven = 0;
     let cachedJsonString = '';
 
     let _initElements = function () {
@@ -40,42 +120,12 @@ let JsonFormatEntrance = (function () {
         try {
             jfContent.html('').show();
             jfPre.html('').hide();
-            jfPathEl && jfPathEl.hide();
+            jfStatusBar && jfStatusBar.hide();
             formattingMsg.hide();
         } catch (e) {
         }
     };
 
-    // Add listener to receive response from BG when ready
-    let postMessage = function (msg) {
-
-        switch (msg[0]) {
-            case 'NOT JSON' :
-                jfPre.show();
-                jfContent.html('<span class="x-json-tips">JSON不合法,请检查:</span>');
-                break;
-
-            case 'FORMATTING' :
-                formattingMsg.show();
-                break;
-
-            case 'FORMATTED' :
-                formattingMsg.hide();
-                jfContent.html(msg[1]);
-
-                _buildOptionBar();
-                // 事件绑定
-                _addEvents();
-                // 支持文件下载
-                _downloadSupport(cachedJsonString);
-
-                break;
-
-            default :
-                throw new Error('Message not understood: ' + msg[0]);
-        }
-    };
-
     /**
      * HTML特殊字符格式化
      * @param str
@@ -90,26 +140,6 @@ let JsonFormatEntrance = (function () {
         return str;
     };
 
-
-    /**
-     * 执行代码格式化
-     * @param  {[type]} jsonStr [description]
-     * @return {[type]}
-     */
-    let format = function (jsonStr) {
-        cachedJsonString = JSON.stringify(JSON.parse(jsonStr), null, 4);
-
-        _initElements();
-        jfPre.html(htmlspecialchars(cachedJsonString));
-
-        JsonFormatDealer.postMessage({
-            type: "SENDING TEXT",
-            text: jsonStr,
-            length: jsonStr.length
-        });
-
-    };
-
     /**
      * 直接下载,能解决中文乱码
      * @param content
@@ -181,7 +211,7 @@ let JsonFormatEntrance = (function () {
      */
     let getJsonText = function (el) {
 
-        let txt = el.text().replace(/":\s/gm, '":').replace(/,$/, '').trim();
+        let txt = el.text().replace(/复制\|下载\|删除/gm,'').replace(/":\s/gm, '":').replace(/,$/, '').trim();
         if (!(/^{/.test(txt) && /\}$/.test(txt)) && !(/^\[/.test(txt) && /\]$/.test(txt))) {
             txt = '{' + txt + '}';
         }
@@ -193,15 +223,43 @@ let JsonFormatEntrance = (function () {
         return txt;
     };
 
-    /**
-     * 给某个节点增加操作项
-     * @param el
-     * @private
-     */
-    let _addOptForItem = function (el) {
+    // 添加json路径
+    let _showJsonPath = function (curEl) {
+        let keys = [];
+        do {
+            if (curEl.hasClass('item-block')) {
+                if (!curEl.hasClass('rootItem')) {
+                    keys.unshift('[' + curEl.prevAll('.item').length + ']');
+                } else {
+                    break;
+                }
+            } else {
+                keys.unshift(curEl.find('>.key').text());
+            }
+
+            if (curEl.parent().hasClass('rootItem') || curEl.parent().parent().hasClass('rootItem')) {
+                break;
+            }
+
+            curEl = curEl.parent().parent();
+
+        } while (curEl.length && !curEl.hasClass('rootItem'));
+
+        let path = keys.join('#@#').replace(/#@#\[/g, '[').replace(/#@#/g, '.');
+
+        let jfPath = $('#jsonPath');
+        if (!jfPath.length) {
+            jfPath = $('<span id="jsonPath"/>').prependTo(jfStatusBar);
+        }
+        jfPath.html('当前节点:JSON.' + path);
+    };
+
+    // 给某个节点增加操作项
+    let _addOptForItem = function (el, show) {
 
         // 下载json片段
-        let fnDownload = function (ec) {
+        let fnDownload = function (event) {
+            event.stopPropagation();
 
             let txt = getJsonText(el);
             // 下载片段
@@ -232,36 +290,56 @@ let JsonFormatEntrance = (function () {
         };
 
         // 复制json片段
-        let fnCopy = function (ec) {
+        let fnCopy = function (event) {
+            event.stopPropagation();
             _copyToClipboard(getJsonText(el));
         };
 
         // 删除json片段
-        let fnDel = function (ed) {
+        let fnDel = function (event) {
+            event.stopPropagation();
             if (el.parent().is('#formattedJson')) {
                 toast('如果连最外层的Json也删掉的话,就没啥意义了哦!');
                 return false;
             }
             toast('节点已删除成功!');
             el.remove();
-            jfOptEl.css('top', -1000).hide();
-            jfPathEl && jfPathEl.hide();
+            jfStatusBar && jfStatusBar.hide();
         };
 
+        $('.boxOpt').hide();
+        if (show) {
+            let jfOptEl = el.children('.boxOpt');
+            if (!jfOptEl.length) {
+                jfOptEl = $('<b class="boxOpt">' +
+                    '<a class="opt-copy" title="复制当前选中节点的JSON数据">复制</a>|' +
+                    '<a class="opt-download" target="_blank" title="下载当前选中节点的JSON数据">下载</a>|' +
+                    '<a class="opt-del" title="删除当前选中节点的JSON数据">删除</a></b>').appendTo(el);
+            } else {
+                jfOptEl.show();
+            }
+
+            jfOptEl.find('a.opt-download').unbind('click').bind('click', fnDownload);
+            jfOptEl.find('a.opt-copy').unbind('click').bind('click', fnCopy);
+            jfOptEl.find('a.opt-del').unbind('click').bind('click', fnDel);
+        }
+
+    };
 
-        jfOptEl = $('#boxOpt');
-        if (!jfOptEl.length) {
-            jfOptEl = $('<div id="boxOpt"><a class="opt-download" target="_blank">下载</a>|<a class="opt-copy">复制</a>|<a class="opt-del">删除</a></div>').appendTo(jfContent);
+    // 显示当前节点的Key
+    let _toogleStatusBar = function (curEl, show) {
+        if (!jfStatusBar) {
+            jfStatusBar = $('<div id="statusBar"/>').appendTo('body');
         }
 
-        jfOptEl.find('a.opt-download').unbind('click').bind('click', fnDownload);
-        jfOptEl.find('a.opt-copy').unbind('click').bind('click', fnCopy);
-        jfOptEl.find('a.opt-del').unbind('click').bind('click', fnDel);
+        if (!show) {
+            jfStatusBar.hide();
+            return;
+        } else {
+            jfStatusBar.show();
+        }
 
-        jfOptEl.css({
-            left: el.offset().left + el.width() - 90,
-            top: el.offset().top
-        }).show();
+        _showJsonPath(curEl);
     };
 
 
@@ -274,19 +352,19 @@ let JsonFormatEntrance = (function () {
 
         $.each(elements, function (i) {
             el = $(this);
-            if (el.children('.blockInner').length) {
+            if (el.children('.kv-list').length) {
                 el.addClass('collapsed');
 
                 if (!el.attr('id')) {
-                    el.attr('id', 'kvov' + (++lastKvovIdGiven));
+                    el.attr('id', 'item' + (++lastItemIdGiven));
 
-                    let count = el.children('.blockInner').eq(0).children().length;
+                    let count = el.children('.kv-list').eq(0).children().length;
                     // Generate comment text eg "4 items"
                     let comment = count + (count === 1 ? ' item' : ' items');
                     // Add CSS that targets it
                     jfStyleEl[0].insertAdjacentHTML(
                         'beforeend',
-                        '\n#kvov' + lastKvovIdGiven + '.collapsed:after{color: #aaa; content:" // ' + comment + '"}'
+                        '\n#item' + lastItemIdGiven + '.collapsed:after{color: #aaa; content:" // ' + comment + '"}'
                     );
                 }
 
@@ -303,7 +381,7 @@ let JsonFormatEntrance = (function () {
         let optionBar = $('#optionBar');
         if (optionBar.length) {
             optionBar.html('');
-        }else{
+        } else {
             optionBar = $('<span id="optionBar" />').appendTo(jfContent.parent());
         }
 
@@ -325,8 +403,7 @@ let JsonFormatEntrance = (function () {
                 buttonFormatted.text('格式化');
             }
 
-            jfOptEl && jfOptEl.hide();
-            jfPathEl && jfPathEl.hide();
+            jfStatusBar && jfStatusBar.hide();
         });
 
         buttonCollapseAll.bind('click', function (e) {
@@ -337,60 +414,23 @@ let JsonFormatEntrance = (function () {
 
             if (buttonCollapseAll.text() === '折叠所有') {
                 buttonCollapseAll.text('展开所有');
-                collapse($('.objProp,.arrElem'));
+                collapse($('.item-object,.item-block'));
             } else {
                 buttonCollapseAll.text('折叠所有');
-                $('.objProp,.arrElem').removeClass('collapsed');
+                $('.item-object,.item-block').removeClass('collapsed');
             }
-            jfOptEl && jfOptEl.hide();
-            jfPathEl && jfPathEl.hide();
+            jfStatusBar && jfStatusBar.hide();
         });
 
     };
 
-    // 显示当前节点的Key
-    let _showJsonKey = function (curEl) {
-        let keys = [];
-        do {
-            if (curEl.hasClass('arrElem')) {
-                if (!curEl.hasClass('rootKvov')) {
-                    keys.unshift('[' + curEl.prevAll('.kvov').length + ']');
-                }
-            } else {
-                keys.unshift(curEl.find('>.k').text());
-            }
-
-            if (curEl.parent().hasClass('rootKvov') || curEl.parent().parent().hasClass('rootKvov')) {
-                break;
-            }
-
-            curEl = curEl.parent().parent();
-
-        } while (curEl.length && !curEl.hasClass('rootKvov'));
-
-        let path = keys.join('#@#').replace(/#@#\[/g, '[').replace(/#@#/g, '.');
-        if (!jfPathEl) {
-            jfPathEl = $('<div/>').css({
-                position: 'fixed',
-                bottom: 0,
-                left: 0,
-                background: 'rgb(0, 0, 0,0.6)',
-                color: '#ff0',
-                fontSize: '12px',
-                fontWeight: 'bold',
-                padding: '2px 10px 2px 2px',
-                zIndex: 10
-            }).appendTo('body');
-        }
-        jfPathEl.html('当前路径:' + path).show();
-    };
-
     // 附加操作
     let _addEvents = function () {
 
         // 折叠、展开
-        $('#jfContent span.e').bind('click', function (ev) {
+        $('#jfContent span.expand').bind('click', function (ev) {
             ev.preventDefault();
+            ev.stopPropagation();
 
             let parentEl = $(this).parent();
             parentEl.toggleClass('collapsed');
@@ -401,25 +441,26 @@ let JsonFormatEntrance = (function () {
         });
 
         // 点击选中:高亮
-        $('#jfContent .kvov').bind('click', function (e) {
+        $('#jfContent .item').bind('click', function (e) {
+
+            let el = $(this);
 
-            if ($(this).hasClass('x-outline')) {
-                jfOptEl && jfOptEl.hide();
-                jfPathEl && jfPathEl.hide();
-                $(this).removeClass('x-outline');
+            if (el.hasClass('x-selected')) {
+                _toogleStatusBar(el, false);
+                _addOptForItem(el, false);
+                el.removeClass('x-selected');
                 e.stopPropagation();
                 return true;
             }
 
-            $('.x-outline').removeClass('x-outline');
-            let el = $(this).removeClass('x-hover').addClass('x-outline');
+            $('.x-selected').removeClass('x-selected');
+            el.addClass('x-selected');
 
-            // 增加复制、删除功能
-            _addOptForItem(el);
-            // 显示key
-            _showJsonKey(el);
+            // 显示底部状态栏
+            _toogleStatusBar(el, true);
+            _addOptForItem(el, true);
 
-            if (!$(e.target).is('.kvov .e')) {
+            if (!$(e.target).is('.item .expand')) {
                 e.stopPropagation();
             } else {
                 $(e.target).parent().trigger('click');
@@ -429,25 +470,96 @@ let JsonFormatEntrance = (function () {
             if (typeof window._OnJsonItemClickByFH === 'function') {
                 window._OnJsonItemClickByFH(getJsonText(el));
             }
-        }).bind('mouseover', function (e) {
-            $(this).addClass('x-hover');
-            return false;
-        }).bind('mouseout', function (e) {
-            $(this).removeClass('x-hover');
         });
 
     };
 
+    /**
+     * 执行代码格式化
+     */
+    let format = function (jsonStr, skin) {
+        cachedJsonString = JSON.stringify(JSON.parse(jsonStr), null, 4);
+
+        _initElements();
+        jfPre.html(htmlspecialchars(cachedJsonString));
+
+        // 用webwork的方式来进行格式化,效率更高
+        let worker = new Worker(URL.createObjectURL(new Blob(["(" + JsonFormatWebWorker.toString() + ")()"], {type: 'text/javascript'})));
+        worker.onmessage = function (evt) {
+            let msg = evt.data;
+            switch (msg[0]) {
+                case 'FORMATTING' :
+                    formattingMsg.show();
+                    break;
+
+                case 'FORMATTED' :
+                    formattingMsg.hide();
+                    jfContent.html(msg[1]);
+
+                    _buildOptionBar();
+                    // 事件绑定
+                    _addEvents();
+                    // 支持文件下载
+                    _downloadSupport(cachedJsonString);
+
+                    break;
+            }
+        };
+        worker.postMessage({
+            jsonString: jsonStr,
+            skin: skin
+        });
+    };
+
+    // 同步的方式格式化
+    let formatSync = function (jsonStr, skin) {
+        cachedJsonString = JSON.stringify(JSON.parse(jsonStr), null, 4);
+
+        _initElements();
+        jfPre.html(htmlspecialchars(cachedJsonString));
+
+        let worker = new JsonFormatWebWorker();
+        worker.getFormattedHtml({
+            data: {
+                jsonString: jsonStr,
+                skin: skin
+            },
+            onFormatting: function (msg) {
+                formattingMsg.show();
+            },
+            onFormatted: function (msg) {
+                formattingMsg.hide();
+                jfContent.html(msg[1]);
+
+                _buildOptionBar();
+                // 事件绑定
+                _addEvents();
+                // 支持文件下载
+                _downloadSupport(cachedJsonString);
+            }
+        });
+    };
+
     return {
         format: format,
-        postMessage: postMessage
+        formatSync: formatSync
     }
 })();
 
 
-let JsonFormatDealer = (function () {
+/*============================================== web worker =========================================================*/
 
-    "use strict";
+/**
+ * 用webworker的形式来进行json格式化,在应对大json的时候,效果会非常明显
+ * @constructor
+ */
+var JsonFormatWebWorker = function () {
+
+    // 引入big-json.js解决大数字的问题
+    let __importScript = (filename) => {
+        this.compress && fetch(filename).then(resp => resp.text()).then(jsText => eval(jsText));
+    };
+    __importScript('json-bigint.js');
 
     // Constants
     let
@@ -456,148 +568,145 @@ let JsonFormatDealer = (function () {
         TYPE_OBJECT = 3,
         TYPE_ARRAY = 4,
         TYPE_BOOL = 5,
-        TYPE_NULL = 6
-    ;
-
-    // Utility functions
-    function removeComments(str) {
-        str = ('__' + str + '__').split('');
-        let mode = {
-            singleQuote: false,
-            doubleQuote: false,
-            regex: false,
-            blockComment: false,
-            lineComment: false,
-            condComp: false
+        TYPE_NULL = 6;
+
+    /**
+     * HTML特殊字符格式化
+     * @param str
+     * @returns {*}
+     */
+    let htmlspecialchars = function (str) {
+        str = str.replace(/&/g, '&amp;');
+        str = str.replace(/</g, '&lt;');
+        str = str.replace(/>/g, '&gt;');
+        str = str.replace(/"/g, '&quot;');
+        str = str.replace(/'/g, '&#039;');
+        return str;
+    };
+
+    /**
+     * FH 虚拟DOM
+     * @constructor
+     */
+    let FhVDom = function () {
+
+        this._id = 'fhvd_' + (new Date * 1);
+        this.tag = '';
+        this.innerText = '';
+        this.textContent = '';
+        this.childNodes = [];
+        this.className = '';
+        this.attributes = [];
+        this.classList = [];
+        this.classList.__proto__.add = this.classList.__proto__.push;
+
+        this.createElement = tag => {
+            this.tag = tag;
+            return this;
         };
-        for (let i = 0, l = str.length; i < l; i++) {
-            if (mode.regex) {
-                if (str[i] === '/' && str[i - 1] !== '\\') {
-                    mode.regex = false;
-                }
-                continue;
-            }
-            if (mode.singleQuote) {
-                if (str[i] === "'" && str[i - 1] !== '\\') {
-                    mode.singleQuote = false;
-                }
-                continue;
-            }
-            if (mode.doubleQuote) {
-                if (str[i] === '"' && str[i - 1] !== '\\') {
-                    mode.doubleQuote = false;
-                }
-                continue;
-            }
-            if (mode.blockComment) {
-                if (str[i] === '*' && str[i + 1] === '/') {
-                    str[i + 1] = '';
-                    mode.blockComment = false;
-                }
-                str[i] = '';
-                continue;
-            }
-            if (mode.lineComment) {
-                if (str[i + 1] === '\n' || str[i + 1] === '\r') {
-                    mode.lineComment = false;
-                }
-                str[i] = '';
-                continue;
-            }
-            if (mode.condComp) {
-                if (str[i - 2] === '@' && str[i - 1] === '*' && str[i] === '/') {
-                    mode.condComp = false;
-                }
-                continue;
-            }
-            mode.doubleQuote = str[i] === '"';
-            mode.singleQuote = str[i] === "'";
-            if (str[i] === '/') {
-                if (str[i + 1] === '*' && str[i + 2] === '@') {
-                    mode.condComp = true;
-                    continue;
-                }
-                if (str[i + 1] === '*') {
-                    str[i] = '';
-                    mode.blockComment = true;
-                    continue;
+
+        this.setAttribute = (attr, value) => {
+            this.attributes.push([attr, value]);
+        };
+
+        this.appendChild = child => {
+            this.childNodes.push(child);
+            return this;
+        };
+
+        this.getOuterHTML = () => {
+            let outerHtml = [];
+            if (this.tag) {
+                outerHtml.push(`<${this.tag}`);
+                let clsName = (this.className || '') + ' ' + this.classList.join(' ');
+                clsName.replace(/\s/g, '').length && outerHtml.push(` class="${clsName}"`);
+                this.attributes.length && outerHtml.push(this.attributes.map(attr => ` ${attr[0]}="${attr[1]}"`).join(''));
+                outerHtml.push(`>`);
+                if (('' + this.innerText).length) {
+                    outerHtml.push(this.innerText);
+                } else if (('' + this.textContent).length) {
+                    outerHtml.push(this.textContent);
+                } else {
+                    outerHtml.push(this.childNodes.map(node => node.getOuterHTML()).join(''))
                 }
-                if (str[i + 1] === '/') {
-                    str[i] = '';
-                    mode.lineComment = true;
-                    continue;
+                outerHtml.push(`</${this.tag}>`);
+            } else {
+                if (('' + this.innerText).length) {
+                    outerHtml.push(this.innerText);
+                } else if (('' + this.textContent).length) {
+                    outerHtml.push(this.textContent);
                 }
-                mode.regex = true;
             }
-        }
-        return str.join('').slice(2, -2);
-    }
-
-    // Template elements
-    let templates,
-        baseDiv = document.createElement('div'),
-        baseSpan = document.createElement('span');
+            return outerHtml.join('');
+        };
 
-    function getSpanBoth(innerText, className) {
-        let span = baseSpan.cloneNode(false);
-        span.className = className;
-        span.innerText = innerText;
-        return span;
-    }
+        this.cloneNode = (deep) => {
+            let newDom = FhVDom.getInstance();
+            newDom.tag = this.tag;
+            if (deep || !this.tag) {
+                newDom.innerText = this.innerText;
+                newDom.textContent = this.textContent;
+            } else {
+                newDom.innerText = '';
+                newDom.textContent = '';
+            }
+            newDom.className = this.className;
+            newDom.classList = Array.from(this.classList);
+            newDom.attributes = Array.from(this.attributes);
+            return newDom;
+        };
+    };
 
-    function getSpanText(innerText) {
-        let span = baseSpan.cloneNode(false);
-        span.innerText = innerText;
-        return span;
-    }
+    // 构造器
+    FhVDom.getInstance = () => new FhVDom();
 
-    function getSpanClass(className) {
-        let span = baseSpan.cloneNode(false);
-        span.className = className;
+    function createSpanNode(innerText, className) {
+        let span = FhVDom.getInstance().createElement('span');
+        span.className = className || '';
+        span.innerText = innerText || '';
         return span;
     }
 
-    function getDivClass(className) {
-        let span = baseDiv.cloneNode(false);
-        span.className = className;
-        return span;
+    function createDivNode(className) {
+        let div = FhVDom.getInstance().createElement('div');
+        div.className = className || '';
+        return div;
     }
 
     // Create template nodes
     let templatesObj = {
-        t_kvov: getDivClass('kvov'),
-        t_key: getSpanClass('k'),
-        t_string: getSpanClass('s'),
-        t_number: getSpanClass('n'),
-        t_exp: getSpanClass('e'),
-
-        t_null: getSpanBoth('null', 'nl'),
-        t_true: getSpanBoth('true', 'bl'),
-        t_false: getSpanBoth('false', 'bl'),
-
-        t_oBrace: getSpanBoth('{', 'b'),
-        t_cBrace: getSpanBoth('}', 'b'),
-        t_oBracket: getSpanBoth('[', 'b'),
-        t_cBracket: getSpanBoth(']', 'b'),
-
-        t_ellipsis: getSpanClass('ell'),
-        t_blockInner: getSpanClass('blockInner'),
-
-        t_colonAndSpace: document.createTextNode(':\u00A0'),
-        t_commaText: document.createTextNode(','),
-        t_dblqText: document.createTextNode('"')
+        t_item: createDivNode('item'),
+        t_key: createSpanNode('', 'key'),
+        t_string: createSpanNode('', 'string'),
+        t_number: createSpanNode('', 'number'),
+        t_exp: createSpanNode('', 'expand'),
+
+        t_null: createSpanNode('null', 'null'),
+        t_true: createSpanNode('true', 'bool'),
+        t_false: createSpanNode('false', 'bool'),
+
+        t_oBrace: createSpanNode('{', 'brace'),
+        t_cBrace: createSpanNode('}', 'brace'),
+        t_oBracket: createSpanNode('[', 'brace'),
+        t_cBracket: createSpanNode(']', 'brace'),
+
+        t_ellipsis: createSpanNode('', 'ellipsis'),
+        t_kvList: createDivNode('kv-list'),
+
+        t_colonAndSpace: createSpanNode(':\u00A0', 'colon'),
+        t_commaText: createSpanNode(',', 'comma'),
+        t_dblqText: createSpanNode('"', 'quote')
     };
 
     // Core recursive DOM-building function
-    function getKvovDOM(value, keyName) {
+    function getItemDOM(value, keyName) {
         let type,
-            kvov,
+            item,
             nonZeroSize,
-            templates = templatesObj, // bring into scope for tiny speed boost
+            templates = templatesObj,
             objKey,
             keySpan,
-            valueElement
-        ;
+            valueElement;
 
         // Establish value type
         if (typeof value === 'string')
@@ -613,8 +722,7 @@ let JsonFormatDealer = (function () {
         else
             type = TYPE_OBJECT;
 
-        // Root node for this kvov
-        kvov = templates.t_kvov.cloneNode(false);
+        item = templates.t_item.cloneNode(false);
 
         // Add an 'expander' first (if this is object/array with non-zero size)
         if (type === TYPE_OBJECT || type === TYPE_ARRAY) {
@@ -631,338 +739,140 @@ let JsonFormatDealer = (function () {
                     }
                 }
                 if (nonZeroSize)
-                    kvov.appendChild(templates.t_exp.cloneNode(true));
+                    item.appendChild(templates.t_exp.cloneNode(true));
             }
         }
 
         // If there's a key, add that before the value
         if (keyName !== false) { // NB: "" is a legal keyname in JSON
-            // This kvov must be an object property
-            kvov.classList.add('objProp');
-            // Create a span for the key name
+            item.classList.add(type === TYPE_OBJECT ? 'item-object' : type === TYPE_ARRAY ? 'item-array' : 'item-line');
             keySpan = templates.t_key.cloneNode(false);
             keySpan.textContent = JSON.stringify(keyName).slice(1, -1); // remove quotes
-            // Add it to kvov, with quote marks
-            kvov.appendChild(templates.t_dblqText.cloneNode(false));
-            kvov.appendChild(keySpan);
-            kvov.appendChild(templates.t_dblqText.cloneNode(false));
-            // Also add ":&nbsp;" (colon and non-breaking space)
-            kvov.appendChild(templates.t_colonAndSpace.cloneNode(false));
+            item.appendChild(templates.t_dblqText.cloneNode(true));
+            item.appendChild(keySpan);
+            item.appendChild(templates.t_dblqText.cloneNode(true));
+            item.appendChild(templates.t_colonAndSpace.cloneNode(true));
         }
         else {
-            // This is an array element instead
-            kvov.classList.add('arrElem');
+            item.classList.add('item-block');
         }
 
-        // Generate DOM for this value
-        let blockInner, childKvov;
+        let kvList, childItem;
         switch (type) {
             case TYPE_STRING:
-                // If string is a URL, get a link, otherwise get a span
-                let innerStringEl = baseSpan.cloneNode(false),
+                let innerStringEl = FhVDom.getInstance().createElement('span'),
                     escapedString = JSON.stringify(value);
                 escapedString = escapedString.substring(1, escapedString.length - 1); // remove quotes
-                if (value[0] === 'h' && value.substring(0, 4) === 'http') { // crude but fast - some false positives, but rare, and UX doesn't suffer terribly from them.
-                    let innerStringA = document.createElement('A');
-                    innerStringA.href = value;
-                    innerStringA.innerText = escapedString;
-                    innerStringEl.appendChild(innerStringA);
+                let isLink = false;
+                if (/^[\w]+:\/\//.test(value)) {
+                    try {
+                        let url = new URL(value);
+                        let innerStringA = FhVDom.getInstance().createElement('A');
+                        innerStringA.setAttribute('href', url.href);
+                        innerStringA.setAttribute('target', '_blank');
+                        innerStringA.innerText = htmlspecialchars(escapedString);
+                        innerStringEl.appendChild(innerStringA);
+                        isLink = true;
+                    } catch (e) {
+                    }
                 }
-                else {
-                    innerStringEl.innerText = escapedString;
+
+                if (!isLink) {
+                    innerStringEl.innerText = htmlspecialchars(escapedString);
                 }
                 valueElement = templates.t_string.cloneNode(false);
-                valueElement.appendChild(templates.t_dblqText.cloneNode(false));
+                valueElement.appendChild(templates.t_dblqText.cloneNode(true));
                 valueElement.appendChild(innerStringEl);
-                valueElement.appendChild(templates.t_dblqText.cloneNode(false));
-                kvov.appendChild(valueElement);
+                valueElement.appendChild(templates.t_dblqText.cloneNode(true));
+                item.appendChild(valueElement);
                 break;
 
             case TYPE_NUMBER:
-                // Simply add a number element (span.n)
                 valueElement = templates.t_number.cloneNode(false);
                 valueElement.innerText = value;
-                kvov.appendChild(valueElement);
+                item.appendChild(valueElement);
                 break;
 
             case TYPE_OBJECT:
                 // Add opening brace
-                kvov.appendChild(templates.t_oBrace.cloneNode(true));
-                // If any properties, add a blockInner containing k/v pair(s)
+                item.appendChild(templates.t_oBrace.cloneNode(true));
                 if (nonZeroSize) {
-                    // Add ellipsis (empty, but will be made to do something when kvov is collapsed)
-                    kvov.appendChild(templates.t_ellipsis.cloneNode(false));
-                    // Create blockInner, which indents (don't attach yet)
-                    blockInner = templates.t_blockInner.cloneNode(false);
-                    // For each key/value pair, add as a kvov to blockInner
-                    let count = 0, k, comma;
-                    for (k in value) {
-                        if (value.hasOwnProperty(k)) {
-                            count++;
-                            childKvov = getKvovDOM(value[k], k);
-                            // Add comma
-                            comma = templates.t_commaText.cloneNode();
-                            childKvov.appendChild(comma);
-                            blockInner.appendChild(childKvov);
+                    item.appendChild(templates.t_ellipsis.cloneNode(false));
+                    kvList = templates.t_kvList.cloneNode(false);
+                    let keys = Object.keys(value).filter(k => value.hasOwnProperty(k));
+                    keys.forEach((k, index) => {
+                        childItem = getItemDOM(value[k], k);
+                        if (index < keys.length - 1) {
+                            childItem.appendChild(templates.t_commaText.cloneNode(true));
                         }
-                    }
-                    // Now remove the last comma
-                    childKvov.removeChild(comma);
-                    // Add blockInner
-                    kvov.appendChild(blockInner);
+                        kvList.appendChild(childItem);
+                    });
+                    item.appendChild(kvList);
                 }
 
                 // Add closing brace
-                kvov.appendChild(templates.t_cBrace.cloneNode(true));
+                item.appendChild(templates.t_cBrace.cloneNode(true));
                 break;
 
             case TYPE_ARRAY:
-                // Add opening bracket
-                kvov.appendChild(templates.t_oBracket.cloneNode(true));
-                // If non-zero length array, add blockInner containing inner vals
+                item.appendChild(templates.t_oBracket.cloneNode(true));
                 if (nonZeroSize) {
-                    // Add ellipsis
-                    kvov.appendChild(templates.t_ellipsis.cloneNode(false));
-                    // Create blockInner (which indents) (don't attach yet)
-                    blockInner = templates.t_blockInner.cloneNode(false);
-                    // For each key/value pair, add the markup
+                    item.appendChild(templates.t_ellipsis.cloneNode(false));
+                    kvList = templates.t_kvList.cloneNode(false);
                     for (let i = 0, length = value.length, lastIndex = length - 1; i < length; i++) {
-                        // Make a new kvov, with no key
-                        childKvov = getKvovDOM(value[i], false);
-                        // Add comma if not last one
+                        childItem = getItemDOM(value[i], false);
                         if (i < lastIndex)
-                            childKvov.appendChild(templates.t_commaText.cloneNode());
-                        // Append the child kvov
-                        blockInner.appendChild(childKvov);
+                            childItem.appendChild(templates.t_commaText.cloneNode(true));
+                        kvList.appendChild(childItem);
                     }
-                    // Add blockInner
-                    kvov.appendChild(blockInner);
+                    item.appendChild(kvList);
                 }
                 // Add closing bracket
-                kvov.appendChild(templates.t_cBracket.cloneNode(true));
+                item.appendChild(templates.t_cBracket.cloneNode(true));
                 break;
 
             case TYPE_BOOL:
                 if (value)
-                    kvov.appendChild(templates.t_true.cloneNode(true));
+                    item.appendChild(templates.t_true.cloneNode(true));
                 else
-                    kvov.appendChild(templates.t_false.cloneNode(true));
+                    item.appendChild(templates.t_false.cloneNode(true));
                 break;
 
             case TYPE_NULL:
-                kvov.appendChild(templates.t_null.cloneNode(true));
+                item.appendChild(templates.t_null.cloneNode(true));
                 break;
         }
 
-        return kvov;
-    }
-
-    // Function to convert object to an HTML string
-    function jsonObjToHTML(obj, jsonpFunctionName) {
-
-        // Format object (using recursive kvov builder)
-        let rootKvov = getKvovDOM(obj, false);
-
-        // The whole DOM is now built.
-
-        // Set class on root node to identify it
-        rootKvov.classList.add('rootKvov');
-
-        // Make div#formattedJson and append the root kvov
-        let divFormattedJson = document.createElement('DIV');
-        divFormattedJson.id = 'formattedJson';
-        divFormattedJson.appendChild(rootKvov);
-
-        // Convert it to an HTML string (shame about this step, but necessary for passing it through to the content page)
-        let returnHTML = divFormattedJson.outerHTML;
-
-        // Top and tail with JSONP padding if necessary
-        if (jsonpFunctionName !== null) {
-            returnHTML =
-                '<div id="jsonpOpener">' + jsonpFunctionName + ' ( </div>' +
-                returnHTML +
-                '<div id="jsonpCloser">)</div>';
-        }
-
-        // Return the HTML
-        return returnHTML;
+        return item;
     }
 
     // Listen for requests from content pages wanting to set up a port
-    let postMessage = function (msg) {
-        let jsonpFunctionName = null;
-
-        if (msg.type === 'SENDING TEXT') {
-            // Try to parse as JSON
-            let obj,
-                text = msg.text;
-            try {
-                obj = JSON.parse(text);
-            }
-            catch (e) {
-                // Not JSON; could be JSONP though.
-
-                // Try stripping 'padding' (if any), and try parsing it again
-                text = text.trim();
-                // Find where the first paren is (and exit if none)
-                let indexOfParen;
-                if (!(indexOfParen = text.indexOf('('))) {
-                    JsonFormatEntrance.postMessage(['NOT JSON', 'no opening parenthesis']);
-                    return;
-                }
-
-                // Get the substring up to the first "(", with any comments/whitespace stripped out
-                let firstBit = removeComments(text.substring(0, indexOfParen)).trim();
-                if (!firstBit.match(/^[a-zA-Z_$][\.\[\]'"0-9a-zA-Z_$]*$/)) {
-                    // The 'firstBit' is NOT a valid function identifier.
-                    JsonFormatEntrance.postMessage(['NOT JSON', 'first bit not a valid function name']);
-                    return;
-                }
-
-                // Find last parenthesis (exit if none)
-                let indexOfLastParen;
-                if (!(indexOfLastParen = text.lastIndexOf(')'))) {
-                    JsonFormatEntrance.postMessage(['NOT JSON', 'no closing paren']);
-                    return;
-                }
-
-                // Check that what's after the last parenthesis is just whitespace, comments, and possibly a semicolon (exit if anything else)
-                let lastBit = removeComments(text.substring(indexOfLastParen + 1)).trim();
-                if (lastBit !== "" && lastBit !== ';') {
-                    JsonFormatEntrance.postMessage(['NOT JSON', 'last closing paren followed by invalid characters']);
-                    return;
-                }
-
-                // So, it looks like a valid JS function call, but we don't know whether it's JSON inside the parentheses...
-                // Check if the 'argument' is actually JSON (and record the parsed result)
-                text = text.substring(indexOfParen + 1, indexOfLastParen);
-                try {
-                    obj = JSON.parse(text);
-                }
-                catch (e2) {
-                    // Just some other text that happens to be in a function call.
-                    // Respond as not JSON, and exit
-                    JsonFormatEntrance.postMessage(['NOT JSON', 'looks like a function call, but the parameter is not valid JSON']);
-                    return;
-                }
-
-                jsonpFunctionName = firstBit;
-            }
-
-            // If still running, we now have obj, which is valid JSON.
-
-            // Ensure it's not a number or string (technically valid JSON, but no point prettifying it)
-            if (typeof obj !== 'object' && typeof obj !== 'array') {
-                JsonFormatEntrance.postMessage(['NOT JSON', 'technically JSON but not an object or array']);
-                return;
-            }
-
-            JsonFormatEntrance.postMessage(['FORMATTING']);
-
-            try {
-                // 有的页面设置了 安全策略,连localStorage都不能用,setTimeout开启多线程就更别说了
-                localStorage.getItem('just test : Blocked script execution in xxx?');
-
-                // 在非UI线程中操作:异步。。。
-                setTimeout(function () {
-                    // Do formatting
-                    let html = jsonObjToHTML(obj, jsonpFunctionName);
-
-                    // Post the HTML string to the content script
-                    JsonFormatEntrance.postMessage(['FORMATTED', html]);
-                }, 0);
-            } catch (ex) {
-                // 错误信息类似:Failed to read the 'localStorage' property from 'Window': The document is sandboxed and lacks the 'allow-same-origin' flag.
-                let html = jsonObjToHTML(obj, jsonpFunctionName);
-                JsonFormatEntrance.postMessage(['FORMATTED', html]);
-            }
-
+    self.onmessage = function (event) {
+        self.postMessage(['FORMATTING']);
+        let rootItem;
+        if (event.data.skin && event.data.skin === 'theme-simple') {
+            rootItem = createDivNode('rootItem');
+            rootItem.textContent = JSON.stringify(JSON.parse(event.data.jsonString), null, 4);
+        } else {
+            rootItem = getItemDOM(JSON.parse(event.data.jsonString), false);
+            rootItem.classList.add('rootItem');
         }
+        let formattedHtml = `<div id="formattedJson">${rootItem.getOuterHTML()}</div>`;
+        self.postMessage(['FORMATTED', formattedHtml]);
     };
 
-    return {
-        postMessage: postMessage
-    };
-})();
-
-window.Formatter = {
-    format: JsonFormatEntrance.format
-};
-
-/**
- * 日期格式化
- * @param {Object} pattern
- */
-Date.prototype.format = function (pattern) {
-    let pad = function (source, length) {
-        let pre = "",
-            negative = (source < 0),
-            string = String(Math.abs(source));
-
-        if (string.length < length) {
-            pre = (new Array(length - string.length + 1)).join('0');
+    // 针对不支持webworker的情况,允许直接调用
+    this.getFormattedHtml = function (options) {
+        options.onFormatting && options.onFormatting(['FORMATTING']);
+        let rootItem;
+        if (options.data.skin && options.data.skin === 'theme-simple') {
+            rootItem = createDivNode('rootItem');
+            rootItem.textContent = JSON.stringify(JSON.parse(options.data.jsonString), null, 4);
+        } else {
+            rootItem = getItemDOM(JSON.parse(options.data.jsonString), false);
+            rootItem.classList.add('rootItem');
         }
-
-        return (negative ? "-" : "") + pre + string;
-    };
-
-    if ('string' !== typeof pattern) {
-        return this.toString();
-    }
-
-    let replacer = function (patternPart, result) {
-        pattern = pattern.replace(patternPart, result);
+        let formattedHtml = `<div id="formattedJson">${rootItem.getOuterHTML()}</div>`;
+        options.onFormatted && options.onFormatted(['FORMATTED', formattedHtml]);
     };
-
-    let year = this.getFullYear(),
-        month = this.getMonth() + 1,
-        date2 = this.getDate(),
-        hours = this.getHours(),
-        minutes = this.getMinutes(),
-        seconds = this.getSeconds(),
-        milliSec = this.getMilliseconds();
-
-    replacer(/yyyy/g, pad(year, 4));
-    replacer(/yy/g, pad(parseInt(year.toString().slice(2), 10), 2));
-    replacer(/MM/g, pad(month, 2));
-    replacer(/M/g, month);
-    replacer(/dd/g, pad(date2, 2));
-    replacer(/d/g, date2);
-
-    replacer(/HH/g, pad(hours, 2));
-    replacer(/H/g, hours);
-    replacer(/hh/g, pad(hours % 12, 2));
-    replacer(/h/g, hours % 12);
-    replacer(/mm/g, pad(minutes, 2));
-    replacer(/m/g, minutes);
-    replacer(/ss/g, pad(seconds, 2));
-    replacer(/s/g, seconds);
-    replacer(/SSS/g, pad(milliSec, 3));
-    replacer(/S/g, milliSec);
-
-    return pattern;
-};
-
-/**
- * 自动消失的Alert弹窗
- * @param content
- */
-window.toast = function (content) {
-    window.clearTimeout(window.feHelperAlertMsgTid);
-    let elAlertMsg = document.querySelector("#fehelper_alertmsg");
-    if (!elAlertMsg) {
-        let elWrapper = document.createElement('div');
-        elWrapper.innerHTML = '<div id="fehelper_alertmsg" style="position:fixed;top:5px;right:5px;z-index:1000000">' +
-            '<p style="background:#000;display:inline-block;color:#fff;text-align:center;' +
-            'padding:10px 10px;margin:0 auto;font-size:14px;border-radius:4px;">' + content + '</p></div>';
-        elAlertMsg = elWrapper.childNodes[0];
-        document.body.appendChild(elAlertMsg);
-    } else {
-        elAlertMsg.querySelector('p').innerHTML = content;
-        elAlertMsg.style.display = 'block';
-    }
-
-    window.feHelperAlertMsgTid = window.setTimeout(function () {
-        elAlertMsg.style.display = 'none';
-    }, 3000);
 };

+ 1 - 1
apps/json-format/index.css

@@ -176,4 +176,4 @@ html,body {
 #btnLeftRight:hover,#btnUpDown:hover{-webkit-box-shadow:0px 1px 3px rgba(0,0,0,0.2);
     background:#ebebeb -webkit-linear-gradient(#fefefe, #f8f8f8 40%, #e9e9e9);border-color:#999;color:#222}
 #btnLeftRight.selected, #btnUpDown.selected{-webkit-box-shadow:inset 0px 1px 5px rgba(0,0,0,0.2);
-    background:#ebebeb -webkit-linear-gradient(#e4e4e4, #dfdfdf 40%, #dcdcdc);color:#333}
+    background:#ebebeb -webkit-linear-gradient(#e4e4e4, #dfdfdf 40%, #dcdcdc);color:#333}

+ 5 - 2
apps/json-format/index.html

@@ -7,7 +7,7 @@
 		<script type="text/javascript" src="../static/vendor/evalCore.min.js"></script>
         <script type="text/javascript" src="../static/vendor/vue/vue.js"></script>
     </head>
-    <body>
+    <body class="theme-default">
         <div class="wrapper wp-json" id="pageContainer">
             <div class="panel panel-default" style="margin-bottom: 0px;">
                 <div class="panel-heading">
@@ -75,7 +75,10 @@
         <script src="../static/vendor/codemirror/matchbrackets.js"></script>
         <script src="../static/vendor/codemirror/placeholder.js"></script>
         <script src="json-lint.js"></script>
-        <script src="content-script.js"></script>
+        <script src="json-bigint.js"></script>
+        <script src="format-lib.js"></script>
+        <script src="json-abc.js"></script>
+        <script src="json-decode.js"></script>
         <script src="../static/js/dark-mode.js"></script>
         <script src="index.js" type="module"></script>
     </body>

+ 1 - 1
apps/json-format/index.js

@@ -69,7 +69,7 @@ new Vue({
                     thing: 'request-page-content',
                     tabId: activeTab.id
                 }).then(resp => {
-                    if(!resp && !resp.content) return ;
+                    if(!resp || !resp.content) return ;
                     editor.setValue(resp.content || '');
                     this.format();
                 });