Skip to content

Commit 2231c72

Browse files
authored
Merge pull request #167 from WeihanLi/json-records-content-type
feat: json-records content type support
2 parents ac44f8e + 1adda66 commit 2231c72

File tree

13 files changed

+966
-100
lines changed

13 files changed

+966
-100
lines changed

package-lock.json

Lines changed: 23 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@
9898
"har-validator": "^5.1.3",
9999
"http-encoding": "^2.0.1",
100100
"js-beautify": "^1.8.8",
101+
"jsonc-parser": "^3.3.1",
101102
"jsonwebtoken": "^8.4.0",
102103
"localforage": "^1.7.3",
103104
"lodash": "^4.17.21",

src/components/editor/monaco.ts

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import type * as MonacoTypes from 'monaco-editor';
22
import type { default as _MonacoEditor, MonacoEditorProps } from 'react-monaco-editor';
3+
import { observable, runInAction } from 'mobx';
34

45
import { defineMonacoThemes } from '../../styles';
56

67
import { delay } from '../../util/promise';
78
import { asError } from '../../util/error';
8-
import { observable, runInAction } from 'mobx';
9-
import { setupXMLValidation } from './xml-validation';
9+
10+
import { ContentValidator, validateJsonRecords, validateXml } from '../../model/events/content-validation';
1011

1112
export type {
1213
MonacoTypes,
@@ -75,7 +76,12 @@ async function loadMonacoEditor(retries = 5): Promise<void> {
7576
},
7677
});
7778

78-
setupXMLValidation(monaco);
79+
monaco.languages.register({
80+
id: 'json-records'
81+
});
82+
83+
addValidator(monaco, 'xml', validateXml);
84+
addValidator(monaco, 'json-records', validateJsonRecords);
7985

8086
MonacoEditor = rmeModule.default;
8187
} catch (err) {
@@ -89,6 +95,43 @@ async function loadMonacoEditor(retries = 5): Promise<void> {
8995
}
9096
}
9197

98+
function addValidator(monaco: typeof MonacoTypes, modeId: string, validator: ContentValidator) {
99+
function validate(model: MonacoTypes.editor.ITextModel) {
100+
const text = model.getValue();
101+
const markers = validator(text, model);
102+
monaco.editor.setModelMarkers(model, modeId, markers);
103+
}
104+
105+
const contentChangeListeners = new Map<MonacoTypes.editor.ITextModel, MonacoTypes.IDisposable>();
106+
107+
function manageContentChangeListener(model: MonacoTypes.editor.ITextModel) {
108+
const isActiveMode = model.getModeId() === modeId;
109+
const listener = contentChangeListeners.get(model);
110+
111+
if (isActiveMode && !listener) {
112+
contentChangeListeners.set(model, model.onDidChangeContent(() =>
113+
validate(model)
114+
));
115+
validate(model);
116+
} else if (!isActiveMode && listener) {
117+
listener.dispose();
118+
contentChangeListeners.delete(model);
119+
monaco.editor.setModelMarkers(model, modeId, []);
120+
}
121+
}
122+
123+
monaco.editor.onWillDisposeModel(model => {
124+
contentChangeListeners.delete(model);
125+
});
126+
monaco.editor.onDidChangeModelLanguage(({ model }) => {
127+
manageContentChangeListener(model);
128+
});
129+
monaco.editor.onDidCreateModel(model => {
130+
manageContentChangeListener(model);
131+
});
132+
133+
}
134+
92135
export function reloadMonacoEditor() {
93136
return monacoLoadingPromise = loadMonacoEditor(0);
94137
}

src/components/editor/xml-validation.ts

Lines changed: 0 additions & 58 deletions
This file was deleted.

src/model/events/body-formatting.ts

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type { WorkerFormatterKey } from '../../services/ui-worker-formatters';
99
import { formatBufferAsync } from '../../services/ui-worker-api';
1010
import { ReadOnlyParams } from '../../components/common/editable-params';
1111
import { ImageViewer } from '../../components/editor/image-viewer';
12+
import { formatJson } from '../../util/json';
1213

1314
export interface EditorFormatter {
1415
language: string;
@@ -106,27 +107,29 @@ export const Formatters: { [key in ViewableContentType]: Formatter } = {
106107
render: (input: Buffer, headers?: Headers) => {
107108
if (input.byteLength < 10_000) {
108109
const inputAsString = bufferToString(input);
109-
110-
try {
111-
// For short-ish inputs, we return synchronously - conveniently this avoids
112-
// showing the loading spinner that churns the layout in short content cases.
113-
return JSON.stringify(
114-
JSON.parse(inputAsString),
115-
null,
116-
2
117-
);
118-
// ^ Same logic as in UI-worker-formatter
119-
} catch (e) {
120-
// Fallback to showing the raw un-formatted JSON:
121-
return inputAsString;
122-
}
110+
return formatJson(inputAsString, { formatRecords: false });
123111
} else {
124112
return observablePromise(
125113
formatBufferAsync(input, 'json', headers)
126114
);
127115
}
128116
}
129117
},
118+
'json-records': {
119+
language: 'json-records',
120+
cacheKey: Symbol('json-records'),
121+
isEditApplicable: false,
122+
render: (input: Buffer, headers?: Headers) => {
123+
if (input.byteLength < 10_000) {
124+
const inputAsString = bufferToString(input);
125+
return formatJson(inputAsString, { formatRecords: true });
126+
} else {
127+
return observablePromise(
128+
formatBufferAsync(input, 'json-records', headers)
129+
);
130+
}
131+
}
132+
},
130133
javascript: {
131134
language: 'javascript',
132135
cacheKey: Symbol('javascript'),

src/model/events/content-types.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
isProbablyGrpcProto,
88
isValidGrpcProto,
99
} from '../../util/protobuf';
10+
import { isProbablyJson, isProbablyJsonRecords } from '../../util/json';
1011

1112
// Simplify a mime type as much as we can, without throwing any errors
1213
export const getBaseContentType = (mimeType: string | undefined) => {
@@ -51,7 +52,9 @@ export type ViewableContentType =
5152
| 'yaml'
5253
| 'image'
5354
| 'protobuf'
54-
| 'grpc-proto';
55+
| 'grpc-proto'
56+
| 'json-records'
57+
;
5558

5659
export const EditableContentTypes = [
5760
'text',
@@ -119,6 +122,14 @@ const mimeTypeToContentTypeMap: { [mimeType: string]: ViewableContentType } = {
119122
'application/grpc-proto': 'grpc-proto',
120123
'application/grpc-protobuf': 'grpc-proto',
121124

125+
// Nobody can quite agree on the names for the various sequence-of-JSON formats:
126+
'application/jsonlines': 'json-records',
127+
'application/json-lines': 'json-records',
128+
'application/x-jsonlines': 'json-records',
129+
'application/jsonl': 'json-records',
130+
'application/x-ndjson': 'json-records',
131+
'application/json-seq': 'json-records',
132+
122133
'application/octet-stream': 'raw'
123134
} as const;
124135

@@ -141,6 +152,7 @@ export function getEditableContentType(mimeType: string | undefined): EditableCo
141152

142153
export function getContentEditorName(contentType: ViewableContentType): string {
143154
return contentType === 'raw' ? 'Hex'
155+
: contentType === 'json-records' ? 'JSON Records'
144156
: contentType === 'json' ? 'JSON'
145157
: contentType === 'css' ? 'CSS'
146158
: contentType === 'url-encoded' ? 'URL-Encoded'
@@ -186,16 +198,18 @@ export function getCompatibleTypes(
186198
body = body.decodedData;
187199
}
188200

189-
// Examine the first char of the body, assuming it's ascii
190-
const firstChar = body && body.subarray(0, 1).toString('ascii');
201+
// Allow optionally formatting non-JSON-records as JSON-records, if it looks like it might be
202+
if (!types.has('json-records') && isProbablyJsonRecords(body)) {
203+
types.add('json-records');
204+
}
191205

192-
// Allow optionally formatting non-JSON as JSON, if it looks like it might be
193-
if (firstChar === '{' || firstChar === '[') {
206+
if (!types.has('json-records') && isProbablyJson(body)) {
207+
// Allow optionally formatting non-JSON as JSON, if it's anything remotely close
194208
types.add('json');
195209
}
196210

197211
// Allow optionally formatting non-XML as XML, if it looks like it might be
198-
if (firstChar === '<') {
212+
if (body?.subarray(0, 1).toString('ascii') === '<') {
199213
types.add('xml');
200214
}
201215

0 commit comments

Comments
 (0)