@xh/hoist 79.0.0-SNAPSHOT.1765828486265 → 79.0.0-SNAPSHOT.1765831120891

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,15 +4,27 @@
4
4
  *
5
5
  * Copyright © 2025 Extremely Heavy Industries Inc.
6
6
  */
7
- import {hoistCmp} from '@xh/hoist/core';
7
+ import {hoistCmp, PlainObject} from '@xh/hoist/core';
8
8
  import '@xh/hoist/desktop/register';
9
9
  import {fmtJson} from '@xh/hoist/format';
10
- import * as codemirror from 'codemirror';
11
- import 'codemirror/mode/javascript/javascript';
10
+ import Ajv, {Options, SchemaObject, ValidateFunction} from 'ajv';
12
11
  import {codeInput, CodeInputProps} from './CodeInput';
13
- import {jsonlint} from './impl/jsonlint';
12
+ import {jsonlint} from './impl/jsonlint.js';
14
13
 
15
- export type JsonInputProps = CodeInputProps;
14
+ export interface JsonInputProps extends CodeInputProps {
15
+ /**
16
+ * JSON Schema object used to validate the input JSON. Accepts any valid JSON Schema keywords
17
+ * supported by AJV, such as `type`, `properties`, `required`, and `additionalProperties`.
18
+ * @see https://ajv.js.org/json-schema.html
19
+ */
20
+ jsonSchema?: SchemaObject;
21
+
22
+ /**
23
+ * Configuration object with any properties supported by the AJV API.
24
+ * @see {@link https://ajv.js.org/options.html}
25
+ */
26
+ ajvProps?: Options;
27
+ }
16
28
 
17
29
  /**
18
30
  * Code-editor style input for editing and validating JSON, powered by CodeMirror.
@@ -21,11 +33,13 @@ export const [JsonInput, jsonInput] = hoistCmp.withFactory<JsonInputProps>({
21
33
  displayName: 'JsonInput',
22
34
  className: 'xh-json-input',
23
35
  render(props, ref) {
36
+ const {jsonSchema, ajvProps, ...rest} = props;
37
+
24
38
  return codeInput({
25
- linter: linter,
39
+ linter: jsonLinterWrapper(jsonSchema, ajvProps),
26
40
  formatter: fmtJson,
27
- mode: 'application/json',
28
- ...props,
41
+ language: 'json',
42
+ ...rest,
29
43
  ref
30
44
  });
31
45
  }
@@ -33,24 +47,114 @@ export const [JsonInput, jsonInput] = hoistCmp.withFactory<JsonInputProps>({
33
47
  (JsonInput as any).hasLayoutSupport = true;
34
48
 
35
49
  //----------------------
36
- // Implementation
37
- //-----------------------
38
- function linter(text: string) {
39
- const errors = [];
40
- if (!text) return errors;
41
-
42
- jsonlint.parseError = function (str, hash) {
43
- const loc = hash.loc;
44
- errors.push({
45
- from: codemirror.Pos(loc.first_line - 1, loc.first_column),
46
- to: codemirror.Pos(loc.last_line - 1, loc.last_column),
47
- message: str
50
+ // JSON Linter helper
51
+ //----------------------
52
+ function jsonLinterWrapper(jsonSchema?: PlainObject, ajvProps?: Options) {
53
+ // No schema → only use JSONLint
54
+ if (!jsonSchema) return jsonLintOnly;
55
+
56
+ const ajv = new Ajv({...ajvProps}),
57
+ validate = ajv.compile(jsonSchema);
58
+
59
+ return (text: string) => {
60
+ const annotations: any[] = [];
61
+
62
+ if (!text.trim()) return annotations;
63
+
64
+ runJsonLint(text, annotations);
65
+ if (annotations.length) return annotations;
66
+
67
+ runAjvValidation(text, validate, annotations);
68
+
69
+ return annotations;
70
+ };
71
+ }
72
+
73
+ /** Run JSONLint and append errors to annotations */
74
+ function runJsonLint(text: string, annotations: any[]) {
75
+ jsonlint.parseError = (message, hash) => {
76
+ const {first_line, first_column, last_line, last_column} = hash.loc;
77
+ annotations.push({
78
+ from: indexFromLineCol(text, first_line, first_column),
79
+ to: indexFromLineCol(text, last_line, last_column),
80
+ message,
81
+ severity: 'error'
48
82
  });
49
83
  };
50
84
 
51
85
  try {
52
86
  jsonlint.parse(text);
53
- } catch (ignored) {}
87
+ } catch {
88
+ // intentionally ignored: parseError handles reporting
89
+ }
90
+ }
91
+
92
+ /** Run AJV schema validation and append errors to annotations */
93
+ function runAjvValidation(text: string, validate: ValidateFunction, annotations: any[]) {
94
+ let data: any;
95
+ try {
96
+ data = JSON.parse(text);
97
+ } catch {
98
+ return;
99
+ }
100
+
101
+ const valid = validate(data);
102
+ if (valid || !validate.errors) return;
103
+
104
+ validate.errors.forEach(err => {
105
+ const {from, to} = getErrorPosition(err, text),
106
+ message = formatAjvMessage(err);
107
+
108
+ annotations.push({from, to, message, severity: 'error'});
109
+ });
110
+ }
111
+
112
+ /** Determine text positions for AJV error highlighting */
113
+ function getErrorPosition(err: any, text: string): {from: number; to: number} {
114
+ let from = 0,
115
+ to = 0,
116
+ key: string;
117
+
118
+ if (err.keyword === 'additionalProperties' && err.params?.additionalProperty) {
119
+ key = err.params.additionalProperty;
120
+ } else {
121
+ const parts = (err.instancePath || '').split('/').filter(Boolean);
122
+ key = parts[parts.length - 1];
123
+ }
124
+
125
+ if (key) {
126
+ const idx = text.indexOf(`"${key}"`);
127
+ if (idx >= 0) {
128
+ from = idx;
129
+ to = idx + key.length + 2;
130
+ }
131
+ }
132
+
133
+ return {from, to};
134
+ }
135
+
136
+ /** Format AJV error messages nicely */
137
+ function formatAjvMessage(err: any): string {
138
+ const path = err.instancePath || '(root)';
139
+ if (err.keyword === 'additionalProperties' && err.params?.additionalProperty) {
140
+ return `Unexpected property "${err.params.additionalProperty}"`;
141
+ }
142
+ return `${path} ${err.message}`;
143
+ }
144
+
145
+ /** JSONLint-only linter (used when no jsonSchema prop) */
146
+ function jsonLintOnly(text: string) {
147
+ const annotations: any[] = [];
148
+ if (!text) return annotations;
149
+
150
+ runJsonLint(text, annotations);
151
+ return annotations;
152
+ }
54
153
 
55
- return errors;
154
+ /** Convert line/col to string index */
155
+ function indexFromLineCol(text: string, line: number, col: number): number {
156
+ const lines = text.split('\n');
157
+ let idx = 0;
158
+ for (let i = 0; i < line - 1; i++) idx += lines[i].length + 1;
159
+ return idx + col;
56
160
  }
@@ -0,0 +1,163 @@
1
+ import {EditorView} from '@codemirror/view';
2
+ import {Extension} from '@codemirror/state';
3
+ import {HighlightStyle, syntaxHighlighting} from '@codemirror/language';
4
+ import {tags as t} from '@lezer/highlight';
5
+
6
+ // Using https://github.com/one-dark/vscode-one-dark-theme/ as reference for the colors
7
+
8
+ const chalky = '#e5c07b',
9
+ coral = '#e06c75',
10
+ cyan = '#56b6c2',
11
+ invalid = '#ffffff',
12
+ ivory = '#abb2bf',
13
+ stone = '#7d8799', // Brightened compared to original to increase contrast
14
+ malibu = '#61afef',
15
+ sage = '#98c379',
16
+ whiskey = '#d19a66',
17
+ violet = '#c678dd',
18
+ darkBackground = '#21252b',
19
+ highlightBackground = '#2c313a',
20
+ background = '#282c34',
21
+ tooltipBackground = '#353a42',
22
+ selection = '#3E4451',
23
+ cursor = '#528bff';
24
+
25
+ /// The colors used in the theme, as CSS color strings.
26
+ export const color = {
27
+ chalky,
28
+ coral,
29
+ cyan,
30
+ invalid,
31
+ ivory,
32
+ stone,
33
+ malibu,
34
+ sage,
35
+ whiskey,
36
+ violet,
37
+ darkBackground,
38
+ highlightBackground,
39
+ background,
40
+ tooltipBackground,
41
+ selection,
42
+ cursor
43
+ };
44
+
45
+ /// The editor theme styles for One Dark.
46
+ export const oneDarkTheme = EditorView.theme(
47
+ {
48
+ '&': {
49
+ color: ivory,
50
+ backgroundColor: background
51
+ },
52
+
53
+ '.cm-content': {
54
+ caretColor: cursor
55
+ },
56
+
57
+ '.cm-cursor, .cm-dropCursor': {borderLeftColor: cursor},
58
+ '&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection':
59
+ {backgroundColor: selection},
60
+
61
+ '.cm-panels': {backgroundColor: darkBackground, color: ivory},
62
+ '.cm-panels.cm-panels-top': {borderBottom: '2px solid black'},
63
+ '.cm-panels.cm-panels-bottom': {borderTop: '2px solid black'},
64
+
65
+ '.cm-searchMatch': {
66
+ backgroundColor: '#72a1ff59',
67
+ outline: '1px solid #457dff'
68
+ },
69
+ '.cm-searchMatch.cm-searchMatch-selected': {
70
+ backgroundColor: '#6199ff2f'
71
+ },
72
+
73
+ '.cm-activeLine': {backgroundColor: '#6699ff0b'},
74
+ '.cm-selectionMatch': {backgroundColor: '#aafe661a'},
75
+
76
+ '&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
77
+ backgroundColor: '#bad0f847'
78
+ },
79
+
80
+ '.cm-gutters': {
81
+ backgroundColor: background,
82
+ color: stone,
83
+ border: 'none'
84
+ },
85
+
86
+ '.cm-activeLineGutter': {
87
+ backgroundColor: highlightBackground
88
+ },
89
+
90
+ '.cm-foldPlaceholder': {
91
+ backgroundColor: 'transparent',
92
+ border: 'none',
93
+ color: '#ddd'
94
+ },
95
+
96
+ '.cm-tooltip': {
97
+ border: 'none',
98
+ backgroundColor: tooltipBackground
99
+ },
100
+ '.cm-tooltip .cm-tooltip-arrow:before': {
101
+ borderTopColor: 'transparent',
102
+ borderBottomColor: 'transparent'
103
+ },
104
+ '.cm-tooltip .cm-tooltip-arrow:after': {
105
+ borderTopColor: tooltipBackground,
106
+ borderBottomColor: tooltipBackground
107
+ },
108
+ '.cm-tooltip-autocomplete': {
109
+ '& > ul > li[aria-selected]': {
110
+ backgroundColor: highlightBackground,
111
+ color: ivory
112
+ }
113
+ }
114
+ },
115
+ {dark: true}
116
+ );
117
+
118
+ /// The highlighting style for code in the One Dark theme.
119
+ export const oneDarkHighlightStyle = HighlightStyle.define([
120
+ {tag: t.keyword, color: violet},
121
+ {tag: [t.name, t.deleted, t.character, t.propertyName, t.macroName], color: coral},
122
+ {tag: [t.function(t.variableName), t.labelName], color: malibu},
123
+ {tag: [t.color, t.constant(t.name), t.standard(t.name)], color: whiskey},
124
+ {tag: [t.definition(t.name), t.separator], color: ivory},
125
+ {
126
+ tag: [
127
+ t.typeName,
128
+ t.className,
129
+ t.number,
130
+ t.changed,
131
+ t.annotation,
132
+ t.modifier,
133
+ t.self,
134
+ t.namespace
135
+ ],
136
+ color: chalky
137
+ },
138
+ {
139
+ tag: [
140
+ t.operator,
141
+ t.operatorKeyword,
142
+ t.url,
143
+ t.escape,
144
+ t.regexp,
145
+ t.link,
146
+ t.special(t.string)
147
+ ],
148
+ color: cyan
149
+ },
150
+ {tag: [t.meta, t.comment], color: stone},
151
+ {tag: t.strong, fontWeight: 'bold'},
152
+ {tag: t.emphasis, fontStyle: 'italic'},
153
+ {tag: t.strikethrough, textDecoration: 'line-through'},
154
+ {tag: t.link, color: stone, textDecoration: 'underline'},
155
+ {tag: t.heading, fontWeight: 'bold', color: coral},
156
+ {tag: [t.atom, t.bool, t.special(t.variableName)], color: whiskey},
157
+ {tag: [t.processingInstruction, t.string, t.inserted], color: sage},
158
+ {tag: t.invalid, color: invalid}
159
+ ]);
160
+
161
+ /// Extension to enable the One Dark theme (both the editor theme and
162
+ /// the highlight style).
163
+ export const oneDark: Extension = [oneDarkTheme, syntaxHighlighting(oneDarkHighlightStyle)];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xh/hoist",
3
- "version": "79.0.0-SNAPSHOT.1765828486265",
3
+ "version": "79.0.0-SNAPSHOT.1765831120891",
4
4
  "description": "Hoist add-on for building and deploying React Applications.",
5
5
  "repository": "github:xh/hoist-react",
6
6
  "homepage": "https://xh.io",
@@ -33,6 +33,12 @@
33
33
  "@blueprintjs/core": "^5.10.5",
34
34
  "@blueprintjs/datetime": "^5.3.7",
35
35
  "@blueprintjs/datetime2": "^2.3.7",
36
+ "@codemirror/commands": "6.9.0",
37
+ "@codemirror/lang-javascript": "^6.2.4",
38
+ "@codemirror/language": "6.11.3",
39
+ "@codemirror/lint": "6.9.0",
40
+ "@codemirror/search": "6.5.11",
41
+ "@codemirror/view": "6.38.6",
36
42
  "@fortawesome/fontawesome-pro": "^6.6.0",
37
43
  "@fortawesome/fontawesome-svg-core": "^6.6.0",
38
44
  "@fortawesome/pro-light-svg-icons": "^6.6.0",
@@ -43,9 +49,10 @@
43
49
  "@onsenui/fastclick": "~1.1.1",
44
50
  "@popperjs/core": "~2.11.0",
45
51
  "@seznam/compose-react-refs": "~1.0.5",
52
+ "ajv": "~8.17.1",
46
53
  "classnames": "~2.5.1",
47
54
  "clipboard-copy": "~4.0.1",
48
- "codemirror": "~5.65.0",
55
+ "codemirror": "~6.0.2",
49
56
  "core-js": "3.x",
50
57
  "debounce-promise": "~3.1.0",
51
58
  "dompurify": "~3.3.0",