浏览代码

feat: prettify JSON value, show parsing errors

tophf 5 年之前
父节点
当前提交
3d42b3dce4
共有 5 个文件被更改,包括 74 次插入22 次删除
  1. 9 8
      src/common/ui/style/style.css
  2. 13 0
      src/common/util.js
  3. 2 3
      src/injected/web/gm-api.js
  4. 0 1
      src/injected/web/gm-values.js
  5. 50 10
      src/options/views/edit/values.vue

+ 9 - 8
src/common/ui/style/style.css

@@ -110,14 +110,6 @@ textarea {
   &:focus {
     border-color: var(--fill-7);
   }
-  &.has-error {
-    // reminder: make sure all colors are readable in light/dark schemes
-    border-color: #8008;
-    background: #f002;
-    &:focus {
-      border-color: #f008;
-    }
-  }
 }
 code {
   padding: 0 .2em;
@@ -347,6 +339,15 @@ body .vl-tooltip {
   box-shadow: 0 0 40px #000;
 }
 
+.has-error {
+  // reminder: make sure all colors are readable in light/dark schemes
+  border-color: #8008;
+  background: #f002;
+  &:focus {
+    border-color: #f008;
+  }
+}
+
 @media (prefers-color-scheme: dark) {
   input[type="radio"],
   input[type="checkbox"] {

+ 13 - 0
src/common/util.js

@@ -202,3 +202,16 @@ export function request(url, options = {}) {
     }, extra);
   }
 }
+
+const SIMPLE_VALUE_TYPE = {
+  string: 's',
+  number: 'n',
+  boolean: 'b',
+};
+
+export function dumpScriptValue(value, jsonDump = JSON.stringify) {
+  if (value !== undefined) {
+    const simple = SIMPLE_VALUE_TYPE[typeof value];
+    return `${simple || 'o'}${simple ? value : jsonDump(value)}`;
+  }
+}

+ 2 - 3
src/injected/web/gm-api.js

@@ -1,4 +1,4 @@
-import { cache2blobUrl, getUniqId, isEmpty } from '#/common';
+import { cache2blobUrl, dumpScriptValue, getUniqId, isEmpty } from '#/common';
 import { downloadBlob } from '#/common/download';
 import {
   defineProperty, objectEntries, objectKeys, objectPick, objectValues,
@@ -43,8 +43,7 @@ export function makeGmApi() {
     },
     GM_setValue(key, val) {
       const { id } = this;
-      const dumped = jsonDump(val);
-      const raw = dumped ? `o${dumped}` : null;
+      const raw = dumpScriptValue(val, jsonDump) || null;
       const values = loadValues(id);
       const oldRaw = values[key];
       values[key] = raw;

+ 0 - 1
src/injected/web/gm-values.js

@@ -10,7 +10,6 @@ export const changeHooks = {};
 
 const dataDecoders = {
   o: jsonLoad,
-  // deprecated
   n: Number,
   b: val => val === 'true',
 };

+ 50 - 10
src/options/views/edit/values.vue

@@ -32,7 +32,10 @@
       <div class="flex mb-1">
         <h4 class="flex-auto" v-text="i18n('labelEditValue')"></h4>
         <div>
-          <button v-text="i18n('editValueSave')" @click="onSave"></button>
+          <button v-text="i18n('editValueSave')" @click="onSave"
+                  :class="{'has-error': current.error}"
+                  :title="current.error"
+                  :disabled="current.error"/>
           <button v-text="i18n('editValueCancel')" @click="onCancel"></button>
         </div>
       </div>
@@ -45,17 +48,29 @@
       <textarea class="flex-auto" v-model="current.value"
                 ref="value"
                 spellcheck="false"
+                @input="onChange"
                 @keydown.esc.exact.stop="onCancel"/>
     </div>
   </div>
 </template>
 
 <script>
-import { sendCmd } from '#/common';
+import { dumpScriptValue, sendCmd } from '#/common';
 import Icon from '#/common/ui/icon';
+import { showMessage } from '../../utils';
 
 const PAGE_SIZE = 25;
 const MAX_LENGTH = 1024;
+const MAX_JSON_DURATION = 10; // ms
+
+const reparseJson = (str) => {
+  try {
+    return JSON.stringify(JSON.parse(str), null, '  ');
+  } catch (e) {
+    // This shouldn't happen but the storage may get corrupted or modified directly
+    return str;
+  }
+};
 
 export default {
   props: ['show', 'script'],
@@ -109,6 +124,7 @@ export default {
       const type = value[0];
       value = value.slice(1);
       if (type === 's') value = JSON.stringify(value);
+      else if (!sliced) value = reparseJson(value);
       if (sliced && value.length > MAX_LENGTH) {
         value = value.slice(0, MAX_LENGTH);
       }
@@ -122,12 +138,12 @@ export default {
         this.page = 1;
       });
     },
-    updateValue({ key, value, isNew }) {
-      const rawValue = value ? `o${value}` : '';
+    updateValue({ key, isNew }) {
+      const rawValue = dumpScriptValue(this.current?.jsonValue) || '';
       const { id } = this.script.props;
       return sendCmd('UpdateValue', { id, key, value: rawValue })
       .then(() => {
-        if (value) {
+        if (rawValue) {
           this.$set(this.values, key, rawValue);
           if (isNew) this.keys.push(key);
         } else {
@@ -159,15 +175,39 @@ export default {
         value: this.getValue(key),
       };
     },
-    onSave() {
-      this.updateValue(this.current)
-      .then(() => {
-        this.current = null;
-      });
+    async onSave() {
+      const { current } = this;
+      if (current.jsonPaused) {
+        current.jsonPaused = false;
+        this.onChange();
+      }
+      if (current.error) {
+        const pos = +current.error.match(/position\s+(\d+)|$/)[1] || 0;
+        this.$refs.value.setSelectionRange(pos, pos + 1);
+        this.$refs.value.focus();
+        showMessage({ text: current.error });
+        return;
+      }
+      await this.updateValue(current);
+      this.current = null;
     },
     onCancel() {
       this.current = null;
     },
+    onChange() {
+      const { current } = this;
+      current.error = null;
+      if (current.jsonPaused) return;
+      const t0 = performance.now();
+      const str = current.value.trim();
+      try {
+        current.jsonValue = str ? JSON.parse(str) : undefined;
+      } catch (e) {
+        current.error = e.message || e;
+        current.jsonValue = undefined;
+      }
+      current.jsonPaused = performance.now() - t0 > MAX_JSON_DURATION;
+    },
   },
   created() {
     let unwatch;