Json edit

JSON editor dialog (intended as a library for userscripts)

目前为 2022-03-07 提交的版本。查看 最新版本

此脚本不应直接安装,它是一个供其他脚本使用的外部库。如果您需要使用该库,请在脚本元属性加入:// @require https://update.cn-greasyfork.org/scripts/441035/1025094/Json%20edit.js

  1. // ==UserScript==
  2. // @name Json edit
  3. // @namespace https://agregen.gitlab.io/
  4. // @version 0.0.1
  5. // @description JSON editor dialog (intended as a library for userscripts)
  6. // @author agreg
  7. // @license MIT
  8. // @match http://localhost:*
  9. // @require https://cdnjs.cloudflare.com/ajax/libs/prism/1.27.0/prism.min.js
  10. // @require https://cdnjs.cloudflare.com/ajax/libs/prism/1.27.0/components/prism-json.min.js
  11. // @require https://cdnjs.cloudflare.com/ajax/libs/prism/1.27.0/plugins/match-braces/prism-match-braces.min.js
  12. // ==/UserScript==
  13.  
  14. var Prism, $jsonEdit = (() => {
  15. const [TAB, NEWLINE, AMPERSAND, LESS_THAN, NONBREAKING_SPACE] = ['\t', '\n', "&", "<", " "];
  16. let {isArray} = Array, isColl = x => x && (typeof x === 'object'); // ignoring non-JSON types
  17. let spaces = n => Array.from({length: n}, _ => " ").join("");
  18. let compactItems = (items, {width, indent}) => items.slice(1).reduce((ss, s) => {
  19. (ss[0].length + 2 + s.length > width ? ss.unshift(s) : (ss[0] += ", " + s));
  20. return ss;
  21. }, [items[0]]).reverse().join(`,\n${indent}`);
  22.  
  23. let pformat = (x, {indent="", width=80, sparse=false, compact=false}={}) => {
  24. if (!isColl(x))
  25. return JSON.stringify(x);
  26. let [bLeft, bRight] = (isArray(x) ? ["[", "]"] : ["{", "}"]);
  27. let _indent = isArray(x) ? (k => spaces(sparse ? 0 : 1)) : (k => spaces(sparse ? 2 : 3+k.length));
  28. let kvs = Object.entries(x).map(([k, v]) => [JSON.stringify(!isArray(x) ? k : Number(k)), v]);
  29. let items = kvs.map(([k, v]) => [k, pformat(v, {width, sparse, compact, indent: indent+_indent(k)})])
  30. .map(([k, v]) => (isArray(x) ? v : `${k}: ${v}`));
  31. let len = indent.length + items.map(s => 2+s.length).reduce((a, b) => a+b, 0);
  32. let flat = !items.some(s => s.includes(NEWLINE));
  33. let _wrap = (s, wrap=s.includes(NEWLINE) && !(isArray(x) && !flat)) => !wrap ? s : `\n${indent} ${s}\n${indent}`;
  34. let _compact = (sep, {_sparse=sparse, _indent=indent+" ", _width=width-_indent.length+1}={}) =>
  35. (_sparse ? _wrap(_compact(sep, {_sparse: false, _width: _width-2, _indent: (flat ? _indent+" " : indent)})) :
  36. !compact || !flat ? items.join((sparse && !flat) || (flat && (len <= width)) ? ", " : sep) :
  37. compactItems(items, {width: _width, indent: _indent}));
  38. return bLeft + (flat && (items.length < 2) ? items[0] || "" :
  39. !sparse ? _compact(`,\n ${indent}`) :
  40. isArray(x) ? _compact(`,\n ${indent}`) :
  41. _wrap(items.join(`,\n ${indent}`), true)) + bRight;
  42. };
  43.  
  44. class ParseError extends Error {
  45. constructor(msg, {position, ...options}) {super(msg, options); this.position = Number(position)}
  46. }
  47.  
  48. let _humanize = s => JSON.stringify( `${s||""}`.replace(/^[a-z]/, c => c.toUpperCase()) ).slice(1, -1);
  49. let parseJson = s => {
  50. try {
  51. return JSON.parse(s);
  52. } catch (e) {
  53. let match, _msg = s => _humanize( `${s||""}`.replaceAll(TAB, "\tab").replaceAll(NEWLINE, "\newline") ); // [sic]
  54. if (match = e.message.match(/^([^]*) in JSON at position ([0-9]+)$|^(Unexpected end of JSON input)$/)) {
  55. let [_, msg, pos, altMsg] = match;
  56. return new ParseError(_msg(msg||altMsg), {cause: e, position: pos||s.length});
  57. } else if (match = e.message.match(/^JSON\.parse: ([^]*) at line ([0-9]+) column ([0-9]+) of the JSON data$/)) {
  58. let [_, msg, row, col] = match;
  59. return new ParseError(_msg(msg), {cause: e, position: Number(col) + (row == 1 ? -1 : s.split('\n').slice(0, row-1).join('\n').length)});
  60. } else {
  61. console.warn(e);
  62. return new ParseError(_msg(e.message), {cause: e, position: 0});
  63. }
  64. }
  65. };
  66.  
  67. let captureTab = editor => event => {
  68. if (event.key == "Tab") {
  69. event.preventDefault();
  70. let before = editor.value.slice(0, editor.selectionStart);
  71. let after = editor.value.slice(editor.selectionEnd, editor.value.length);
  72. let pos = editor.selectionEnd + 1;
  73. editor.value = before + TAB + after;
  74. editor.selectionStart = editor.selectionEnd = pos;
  75. update(editor.value);
  76. }
  77. }
  78.  
  79. let $e = (tag, attrs, ...children) => {
  80. let e = Object.assign(document.createElement(tag), attrs);
  81. children.forEach(child => e.append(typeof child != 'string' ? child : document.createTextNode(child)));
  82. return e;
  83. };
  84.  
  85. let theme = selector => `${selector} pre {color:white; background:black}
  86. ${selector} .token.string {color:slateblue}
  87. ${selector} .token.property {color:orange}
  88. ${selector} .token.number {color:green}
  89. ${selector} .token:is(.boolean, .null) {color:deeppink}
  90. ${selector} .token.operator {color:yellowgreen}
  91. ${selector} .token.punctuation {color:grey}
  92. ${selector} .token:is(.brace-level-2, .brace-level-6, .brace-level-10) {color:#388}
  93. ${selector} .token:is(.brace-level-3, .brace-level-7, .brace-level-11) {color:#838}
  94. ${selector} .token:is(.brace-level-4, .brace-level-8, .brace-level-12) {color:#883}`;
  95.  
  96. let createEditorModal = (id, {maxWidth='90%'}={}) => {
  97. const ID = '#'+id;
  98. let style = $e('style', {},
  99. `${ID} {position:fixed; height:calc(90vh - 2em); top:5vh; width:calc(90vw - 2em);
  100. max-width:${maxWidth}; left:0; right:0; margin:0 auto; z-index:1000}
  101. ${ID}, ${ID} .window {display:flex; flex-direction:column}
  102. ${ID} .title {padding:0 1em; background:lightgrey; font-weight:bold; font-size:larger}
  103. ${ID} .window {position: relative; padding:1em; padding-top:0; background:grey}
  104. ${ID} :is(.title, .window > *) {flex:0} ${ID} :is(.window, .editor) {flex-grow:1}
  105. ${ID} .editor {position:relative; height:calc(100% - 6em)}
  106. ${ID} .editor > * {position:absolute; top:0; left:0; width:calc(100% - 6px); height:100%; overflow:auto; margin:0}
  107. ${ID} .editor :is(textarea, pre) {font-family:monospace; font-size:15pt; line-height:20pt;
  108. border-radius:5px; white-space:pre; hyphens:none}
  109. ${ID}.text .editor :is(textarea, pre) {white-space:pre-wrap; word-wrap:break-word}
  110. ${ID} .editor textarea {resize:none; z-index:2; background:transparent; color:transparent; caret-color:white}
  111. ${ID} .editor pre {z-index:1; margin:0; overflow:auto; padding:3px}
  112. @-moz-document url-prefix() {${ID} .editor pre {padding:4px}}
  113.  
  114. ${theme(ID)}
  115.  
  116. ${ID} :is(.toolbar, .buttons) {margin-top:1em; display:flex; justify-content:space-evenly}
  117. ${ID} .toolbar input[type=number] {width:4em; background:white}
  118. ${ID} .error {color:yellow; font-weight:bold; font-family:monospace}`);
  119. setTimeout(() => document.head.append(style));
  120.  
  121. let modal, title, error, editor, overlay, content, toolbar, sparse, compact, width, redraw, cancel, ok;
  122. modal = $e('div', {id, className: 'modal', style: "display:none", mode: 'json'},
  123. title = $e('div', {className: 'title'}, ""),
  124. $e('div', {className: 'window'},
  125. $e('div', {className: 'error', innerHTML: "&ZeroWidthSpace;"}, error = $e('span')),
  126. $e('div', {className: 'editor'},
  127. editor = $e('textarea'),
  128. overlay = $e('pre', {}, content = $e('code', {className: "highlighting language-json match-braces"}))),
  129. toolbar = $e('div', {className: 'toolbar'},
  130. $e('label', {title: "Don't inline dicts"},
  131. sparse = $e('input', {type: 'checkbox', className: 'sparse'}),
  132. " Sparse"),
  133. $e('label', {title: "Compact long lists"},
  134. compact = $e('input', {type: 'checkbox', className: 'compact'}),
  135. " Compact"),
  136. $e('label', {title: "Width limit (0 = unlimited)"},
  137. "Width ",
  138. width = $e('input', {type: 'number', className: 'width', value: 100, min: 0})),
  139. redraw = $e('button', {className: 'redraw'}, "Check / Reformat")),
  140. $e('div', {className: 'buttons'},
  141. cancel = $e('button', {className: 'cancel'}, "Cancel"),
  142. ok = $e('button', {className: 'ok'}, "OK"))));
  143.  
  144. let _isValid, _width = Number(width.value)||Infinity;
  145.  
  146. let render = (e, text, mode=modal.mode) => {
  147. e.innerHTML = text.replace(/&/g, AMPERSAND).replace(/</g, LESS_THAN); // can't use innerText here
  148. (mode === 'json') && Prism && Prism.highlightElement(e);
  149. };
  150.  
  151. let update = text => render(content, text + (text.slice(-1) != NEWLINE ? "" : " "));
  152. let syncScroll = () => {[overlay.scrollTop, overlay.scrollLeft] = [editor.scrollTop, editor.scrollLeft]};
  153.  
  154. let detectWidth = () => {
  155. let style = "font-family:monospace; font-size:15pt; line-height:20pt; position:fixed; top:0; left:0";
  156. let e = $e('span', {style, innerHTML: NONBREAKING_SPACE});
  157. modal.append(e);
  158. width.value = _width = Math.floor(editor.clientWidth / e.clientWidth);
  159. e.remove();
  160. };
  161.  
  162. let reformat = (s = editor.value, o = parseJson(s)) => {
  163. if (o instanceof Error) {
  164. error.innerText = o.message || "Syntax error";
  165. console.warn(o, {position: o.position});
  166. editor.focus();
  167. editor.selectionStart = editor.selectionEnd = o.position||0;
  168. } else {
  169. error.innerText = "";
  170. editor.value = pformat(o, {sparse: sparse.checked, compact: compact.checked, width: _width});
  171. editor.selectionStart = editor.selectionEnd = 0;
  172. update(editor.value);
  173. syncScroll();
  174. }
  175. };
  176.  
  177. let _visible = () => modal.style.display !== 'none';
  178. let toggle = (visible=!_visible()) => {modal.style.display = (visible ? '' : 'none')};
  179. let _resolve, [editJson, editText] = ['json', 'text'].map(mode => (value=editor.value, options={}) => new Promise(resolve => {
  180. [modal.mode, _resolve, _isValid] = [mode, resolve, options.validator];
  181. toolbar.style.display = (mode == 'json' ? '' : 'none');
  182. modal.classList[mode == 'json' ? 'remove' : 'add']('text');
  183. [error.innerText, editor.value, title.innerText] = ["", value, options.title||`Enter ${mode}`];
  184. render(content, value);
  185. toggle(true);
  186. detectWidth();
  187. editor.focus();
  188. }));
  189. let editAsJson = (value, options={}) => (setTimeout(reformat), editJson(JSON.stringify(value), options));
  190. let resolve = (value=editor.value) => (toggle(false), _resolve && _resolve(value));
  191.  
  192. document.addEventListener('keydown', ({key}) => (key == 'Escape') && toggle(false));
  193. cancel.onclick = () => toggle();
  194. ok.onclick = () => {
  195. if (modal.mode != 'json') resolve(); else {
  196. let s = editor.value, o = parseJson(s);
  197. if (!(o instanceof Error))
  198. resolve(o)
  199. else {
  200. try {if (_isValid(s)) return resolve()} catch (e) {}
  201. reformat(s, o)
  202. }
  203. }
  204. };
  205.  
  206. editor.oninput = () => {update(editor.value); syncScroll()};
  207. editor.onscroll = syncScroll;
  208. editor.onkeydown = captureTab(editor);
  209. redraw.onclick = sparse.onchange = compact.onchange = () => reformat();
  210. width.onchange = () => {
  211. if (!width.value || !Number.isInteger( Number(width.value) ))
  212. alert(`Invalid width value: ${width.value}`);
  213. else {
  214. _width = Number(width.value)||Infinity;
  215. reformat();
  216. }
  217. };
  218.  
  219. return Object.assign(modal, {toggle, editText, editJson, editAsJson, render});
  220. };
  221.  
  222. return {pformat, ParseError, parseJson, theme, createEditorModal};
  223. })();