JSON UI
This commit is contained in:
parent
1b4b07c1fa
commit
a5baf0d33a
4 changed files with 586 additions and 1 deletions
|
|
@ -1,7 +1,8 @@
|
||||||
import { createSignal, Switch, Match } from 'solid-js';
|
import { createSignal, Switch, Match } from 'solid-js';
|
||||||
import { App as LispApp } from './languages/lisp/App';
|
import { App as LispApp } from './languages/lisp/App';
|
||||||
|
import { App as JsonApp } from './languages/json/App';
|
||||||
|
|
||||||
type LanguageId = "lisp";
|
type LanguageId = "lisp" | "json";
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
const [language, setLanguage] = createSignal<LanguageId>("lisp");
|
const [language, setLanguage] = createSignal<LanguageId>("lisp");
|
||||||
|
|
@ -16,6 +17,7 @@ export function App() {
|
||||||
onChange={(event) => setLanguage(event.currentTarget.value as LanguageId)}
|
onChange={(event) => setLanguage(event.currentTarget.value as LanguageId)}
|
||||||
>
|
>
|
||||||
<option value="lisp">Lisp</option>
|
<option value="lisp">Lisp</option>
|
||||||
|
<option value="json">JSON</option>
|
||||||
</select>
|
</select>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|
@ -23,6 +25,9 @@ export function App() {
|
||||||
<Match when={language() === "lisp"}>
|
<Match when={language() === "lisp"}>
|
||||||
<LispApp />
|
<LispApp />
|
||||||
</Match>
|
</Match>
|
||||||
|
<Match when={language() === "json"}>
|
||||||
|
<JsonApp />
|
||||||
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,116 @@
|
||||||
|
import { createMemo, createSignal } from 'solid-js';
|
||||||
|
import { sourceText } from 'source-region';
|
||||||
|
import type { CodePointSpan, SourceRegion, SourceText } from 'source-region';
|
||||||
|
import {
|
||||||
|
parseDocument,
|
||||||
|
programOf,
|
||||||
|
} from '../../../languages/json';
|
||||||
|
import type {
|
||||||
|
ConcreteSyntaxResult,
|
||||||
|
ParseError,
|
||||||
|
PartialConcreteSyntax,
|
||||||
|
} from '../../../languages/json';
|
||||||
|
import { hoverAnnotation } from '../../annotations';
|
||||||
|
import { spanLabel } from '../../format';
|
||||||
|
import { PaneHeader, PaneSplitter } from '../../Pane';
|
||||||
|
import { SourceGrid } from '../../SourceGrid';
|
||||||
|
import type { HoverTarget } from '../../types';
|
||||||
|
import { clamp } from '../../utils';
|
||||||
|
import { StructureTree } from './StructurePane';
|
||||||
|
|
||||||
|
type ParsedDocument = {
|
||||||
|
source: SourceText;
|
||||||
|
region: SourceRegion;
|
||||||
|
syntax: ConcreteSyntaxResult;
|
||||||
|
program: PartialConcreteSyntax;
|
||||||
|
errors: ParseError[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const SAMPLE_INPUT = `{
|
||||||
|
"name": "Ada",
|
||||||
|
"scores": [1, 2, 3],
|
||||||
|
"active": true,
|
||||||
|
"nested": { "ok": null }
|
||||||
|
}
|
||||||
|
|
||||||
|
[1 2, 3]
|
||||||
|
{"x" 1, "y": 2}
|
||||||
|
{"bad": "escape \\x"}
|
||||||
|
01 - 1. 1e+ 123abc
|
||||||
|
@@@ {"recovered": true}`;
|
||||||
|
|
||||||
|
export function App() {
|
||||||
|
const [input, setInput] = createSignal(SAMPLE_INPUT);
|
||||||
|
const [hovered, setHovered] = createSignal<HoverTarget | undefined>();
|
||||||
|
const [leftWidth, setLeftWidth] = createSignal(420);
|
||||||
|
const [middleWidth, setMiddleWidth] = createSignal(480);
|
||||||
|
|
||||||
|
const parsed = createMemo<ParsedDocument>(() => {
|
||||||
|
const source = sourceText(input());
|
||||||
|
const region = source.fullRegion();
|
||||||
|
const result = parseDocument(region);
|
||||||
|
return { source, region, syntax: result.syntax, program: programOf(result.syntax), errors: result.errors };
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main
|
||||||
|
class="app-shell"
|
||||||
|
style={{
|
||||||
|
"--left-width": `${leftWidth()}px`,
|
||||||
|
"--middle-width": `${middleWidth()}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<section class="pane input-pane">
|
||||||
|
<PaneHeader title="Source" detail={`${input().length} UTF-16 units`} />
|
||||||
|
<textarea
|
||||||
|
class="source-input"
|
||||||
|
spellcheck={false}
|
||||||
|
value={input()}
|
||||||
|
onInput={(event) => {
|
||||||
|
setInput(event.currentTarget.value);
|
||||||
|
setHovered(undefined);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<PaneSplitter
|
||||||
|
label="Resize source and structure panes"
|
||||||
|
onDrag={(delta) => {
|
||||||
|
setLeftWidth((width) => clamp(width + delta, 280, 760));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<section class="pane structure-pane">
|
||||||
|
<PaneHeader
|
||||||
|
title="Structure"
|
||||||
|
detail={`${parsed().syntax.tag}, ${parsed().program.expressions.length} values, ${parsed().errors.length} errors`}
|
||||||
|
/>
|
||||||
|
<StructureTree
|
||||||
|
program={parsed().program}
|
||||||
|
isValid={parsed().syntax.tag === "valid"}
|
||||||
|
errorCount={parsed().errors.length}
|
||||||
|
onHover={setHovered}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<PaneSplitter
|
||||||
|
label="Resize structure and source grid panes"
|
||||||
|
onDrag={(delta) => {
|
||||||
|
setMiddleWidth((width) => clamp(width + delta, 260, 820));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<section class="pane source-pane">
|
||||||
|
<PaneHeader
|
||||||
|
title="Source Grid"
|
||||||
|
detail={hovered() ? spanLabel(hovered()!.span) : "nothing hovered"}
|
||||||
|
/>
|
||||||
|
<SourceGrid
|
||||||
|
source={parsed().source}
|
||||||
|
region={parsed().region}
|
||||||
|
annotations={hovered() ? [hoverAnnotation(hovered()!)] : []}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,407 @@
|
||||||
|
import { For, Show } from 'solid-js';
|
||||||
|
import type {
|
||||||
|
ArrayItem,
|
||||||
|
ConcreteError,
|
||||||
|
ConcreteErrorNode,
|
||||||
|
ConcreteInfo,
|
||||||
|
JsonArray,
|
||||||
|
JsonObject,
|
||||||
|
JsonValue,
|
||||||
|
MemberItem,
|
||||||
|
PartialConcreteSyntax,
|
||||||
|
PartialJsonValue,
|
||||||
|
StringLiteral,
|
||||||
|
} from '../../../languages/json';
|
||||||
|
import { JsonValue as JsonValueOps } from '../../../languages/json';
|
||||||
|
import { HoverBlock } from '../../HoverBlock';
|
||||||
|
import { SpanChip } from '../../SpanChip';
|
||||||
|
import { spanLabel } from '../../format';
|
||||||
|
import type { HoverTarget } from '../../types';
|
||||||
|
import { errorDetail, errorTitle } from './format';
|
||||||
|
|
||||||
|
type PartialObject = JsonObject<ConcreteInfo, ConcreteError>;
|
||||||
|
type PartialArray = JsonArray<ConcreteInfo, ConcreteError>;
|
||||||
|
type PartialMemberItem = MemberItem<ConcreteInfo, ConcreteError>;
|
||||||
|
type PartialArrayItem = ArrayItem<ConcreteInfo, ConcreteError>;
|
||||||
|
type PartialString = StringLiteral<ConcreteInfo, ConcreteError>;
|
||||||
|
|
||||||
|
export function StructureTree(props: {
|
||||||
|
program: PartialConcreteSyntax;
|
||||||
|
isValid: boolean;
|
||||||
|
errorCount: number;
|
||||||
|
onHover: (target: HoverTarget | undefined) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div class="structure-tree">
|
||||||
|
<HoverBlock
|
||||||
|
class="syntax-node program-node"
|
||||||
|
label="program"
|
||||||
|
span={props.program.span}
|
||||||
|
onHover={props.onHover}
|
||||||
|
>
|
||||||
|
<div class="node-header">
|
||||||
|
<span class={props.isValid ? "status-dot status-valid" : "status-dot status-invalid"} />
|
||||||
|
<span class="node-kind">program</span>
|
||||||
|
<span class="item-meta">
|
||||||
|
{props.isValid ? "valid" : "invalid"} · {props.program.expressions.length} values · {props.errorCount} errors
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show when={props.program.error}>
|
||||||
|
{(error) => <ConcreteErrorView error={error()} label="program error" onHover={props.onHover} />}
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<div class="list-children">
|
||||||
|
<For each={props.program.expressions}>
|
||||||
|
{(value, index) => (
|
||||||
|
<ValueView
|
||||||
|
value={value}
|
||||||
|
label={`value ${index() + 1}`}
|
||||||
|
onHover={props.onHover}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</HoverBlock>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ValueView(props: {
|
||||||
|
value: PartialJsonValue;
|
||||||
|
label: string;
|
||||||
|
onHover: (target: HoverTarget | undefined) => void;
|
||||||
|
}) {
|
||||||
|
switch (props.value.tag) {
|
||||||
|
case "object":
|
||||||
|
return <ObjectView object={props.value} label={props.label} onHover={props.onHover} />;
|
||||||
|
case "array":
|
||||||
|
return <ArrayView array={props.value} label={props.label} onHover={props.onHover} />;
|
||||||
|
case "string":
|
||||||
|
case "number":
|
||||||
|
case "null":
|
||||||
|
case "true":
|
||||||
|
case "false":
|
||||||
|
return <ScalarView value={props.value} label={props.label} onHover={props.onHover} />;
|
||||||
|
case "error-string":
|
||||||
|
case "error-number":
|
||||||
|
case "error-expression":
|
||||||
|
return <ErrorValueView value={props.value} label={props.label} onHover={props.onHover} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ObjectView(props: {
|
||||||
|
object: PartialObject;
|
||||||
|
label: string;
|
||||||
|
onHover: (target: HoverTarget | undefined) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<HoverBlock
|
||||||
|
class={props.object.error ? "syntax-node list-node syntax-error-node" : "syntax-node list-node"}
|
||||||
|
label={`${props.label}: object`}
|
||||||
|
span={props.object.span}
|
||||||
|
onHover={props.onHover}
|
||||||
|
>
|
||||||
|
<div class="node-header">
|
||||||
|
<Show when={props.object.error}>
|
||||||
|
<span class="status-dot status-invalid" />
|
||||||
|
</Show>
|
||||||
|
<span class="node-kind">object</span>
|
||||||
|
<span class="item-meta">{props.object.members.length} members · {delimiterLabel(props.object)}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="delimiter-row">
|
||||||
|
<SpanChip label={props.object.open.tag} span={props.object.open.span} onHover={props.onHover} />
|
||||||
|
<Show when={props.object.close}>
|
||||||
|
{(close) => <SpanChip label={close().tag} span={close().span} onHover={props.onHover} />}
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show when={props.object.error}>
|
||||||
|
{(error) => <ConcreteErrorView error={error()} label="object error" onHover={props.onHover} />}
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<div class="list-children">
|
||||||
|
<For each={props.object.members}>
|
||||||
|
{(member, index) => (
|
||||||
|
<MemberItemView
|
||||||
|
item={member}
|
||||||
|
label={`${props.label}.member ${index() + 1}`}
|
||||||
|
onHover={props.onHover}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</HoverBlock>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MemberItemView(props: {
|
||||||
|
item: PartialMemberItem;
|
||||||
|
label: string;
|
||||||
|
onHover: (target: HoverTarget | undefined) => void;
|
||||||
|
}) {
|
||||||
|
if (props.item.tag === "error-object-separator") {
|
||||||
|
return (
|
||||||
|
<HoverBlock
|
||||||
|
class="syntax-node syntax-error-node"
|
||||||
|
label={`${props.label}: object separator`}
|
||||||
|
span={props.item.span}
|
||||||
|
onHover={props.onHover}
|
||||||
|
>
|
||||||
|
<div class="node-header">
|
||||||
|
<span class="status-dot status-invalid" />
|
||||||
|
<span class="node-kind">error-object-separator</span>
|
||||||
|
<span class="item-meta">{spanLabel(props.item.span)}</span>
|
||||||
|
</div>
|
||||||
|
<ConcreteErrorView error={props.item.error} label="object separator error" onHover={props.onHover} />
|
||||||
|
</HoverBlock>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HoverBlock
|
||||||
|
class={props.item.error ? "syntax-node syntax-error-node" : "syntax-node"}
|
||||||
|
label={`${props.label}: member`}
|
||||||
|
span={props.item.span}
|
||||||
|
onHover={props.onHover}
|
||||||
|
>
|
||||||
|
<div class="node-header">
|
||||||
|
<Show when={props.item.error}>
|
||||||
|
<span class="status-dot status-invalid" />
|
||||||
|
</Show>
|
||||||
|
<span class="node-kind">member</span>
|
||||||
|
<span class="item-meta">{spanLabel(props.item.span)}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="delimiter-row">
|
||||||
|
<Show when={props.item.colon}>
|
||||||
|
{(colon) => <SpanChip label="colon" span={colon().span} onHover={props.onHover} />}
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show when={props.item.error}>
|
||||||
|
{(error) => <ConcreteErrorView error={error()} label="member error" onHover={props.onHover} />}
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<div class="list-children">
|
||||||
|
<StringView string={props.item.key} label={`${props.label}.key`} onHover={props.onHover} />
|
||||||
|
<ValueView value={props.item.value} label={`${props.label}.value`} onHover={props.onHover} />
|
||||||
|
</div>
|
||||||
|
</HoverBlock>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ArrayView(props: {
|
||||||
|
array: PartialArray;
|
||||||
|
label: string;
|
||||||
|
onHover: (target: HoverTarget | undefined) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<HoverBlock
|
||||||
|
class={props.array.error ? "syntax-node list-node syntax-error-node" : "syntax-node list-node"}
|
||||||
|
label={`${props.label}: array`}
|
||||||
|
span={props.array.span}
|
||||||
|
onHover={props.onHover}
|
||||||
|
>
|
||||||
|
<div class="node-header">
|
||||||
|
<Show when={props.array.error}>
|
||||||
|
<span class="status-dot status-invalid" />
|
||||||
|
</Show>
|
||||||
|
<span class="node-kind">array</span>
|
||||||
|
<span class="item-meta">{props.array.items.length} items · {delimiterLabel(props.array)}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="delimiter-row">
|
||||||
|
<SpanChip label={props.array.open.tag} span={props.array.open.span} onHover={props.onHover} />
|
||||||
|
<Show when={props.array.close}>
|
||||||
|
{(close) => <SpanChip label={close().tag} span={close().span} onHover={props.onHover} />}
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show when={props.array.error}>
|
||||||
|
{(error) => <ConcreteErrorView error={error()} label="array error" onHover={props.onHover} />}
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<div class="list-children">
|
||||||
|
<For each={props.array.items}>
|
||||||
|
{(item, index) => (
|
||||||
|
<ArrayItemView
|
||||||
|
item={item}
|
||||||
|
label={`${props.label}.${index() + 1}`}
|
||||||
|
onHover={props.onHover}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</HoverBlock>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ArrayItemView(props: {
|
||||||
|
item: PartialArrayItem;
|
||||||
|
label: string;
|
||||||
|
onHover: (target: HoverTarget | undefined) => void;
|
||||||
|
}) {
|
||||||
|
if (props.item.tag === "error-array-separator") {
|
||||||
|
return (
|
||||||
|
<HoverBlock
|
||||||
|
class="syntax-node syntax-error-node"
|
||||||
|
label={`${props.label}: array separator`}
|
||||||
|
span={props.item.span}
|
||||||
|
onHover={props.onHover}
|
||||||
|
>
|
||||||
|
<div class="node-header">
|
||||||
|
<span class="status-dot status-invalid" />
|
||||||
|
<span class="node-kind">error-array-separator</span>
|
||||||
|
<span class="item-meta">{spanLabel(props.item.span)}</span>
|
||||||
|
</div>
|
||||||
|
<ConcreteErrorView error={props.item.error} label="array separator error" onHover={props.onHover} />
|
||||||
|
</HoverBlock>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <ValueView value={props.item} label={props.label} onHover={props.onHover} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ScalarView(props: {
|
||||||
|
value: Exclude<PartialJsonValue, PartialObject | PartialArray | { tag: "error-expression"; error: ConcreteError } & ConcreteInfo>;
|
||||||
|
label: string;
|
||||||
|
onHover: (target: HoverTarget | undefined) => void;
|
||||||
|
}) {
|
||||||
|
if (props.value.tag === "string") {
|
||||||
|
return <StringView string={props.value} label={props.label} onHover={props.onHover} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HoverBlock
|
||||||
|
class={props.value.error ? "syntax-node literal-node syntax-error-node" : "syntax-node literal-node"}
|
||||||
|
label={`${props.label}: ${props.value.tag}`}
|
||||||
|
span={props.value.span}
|
||||||
|
onHover={props.onHover}
|
||||||
|
>
|
||||||
|
<div class="node-header">
|
||||||
|
<Show when={props.value.error}>
|
||||||
|
<span class="status-dot status-invalid" />
|
||||||
|
</Show>
|
||||||
|
<span class="node-kind">{props.value.tag}</span>
|
||||||
|
<span class="node-value">{JsonValueOps.show(props.value as JsonValue<ConcreteInfo, ConcreteError>)}</span>
|
||||||
|
</div>
|
||||||
|
<Show when={props.value.error}>
|
||||||
|
{(error) => <ConcreteErrorView error={error()} label={`${props.label} error`} onHover={props.onHover} />}
|
||||||
|
</Show>
|
||||||
|
</HoverBlock>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function StringView(props: {
|
||||||
|
string: PartialString;
|
||||||
|
label: string;
|
||||||
|
onHover: (target: HoverTarget | undefined) => void;
|
||||||
|
}) {
|
||||||
|
if (props.string.tag === "error-string") {
|
||||||
|
return (
|
||||||
|
<HoverBlock
|
||||||
|
class="syntax-node syntax-error-node"
|
||||||
|
label={`${props.label}: error-string`}
|
||||||
|
span={props.string.span}
|
||||||
|
onHover={props.onHover}
|
||||||
|
>
|
||||||
|
<div class="node-header">
|
||||||
|
<span class="status-dot status-invalid" />
|
||||||
|
<span class="node-kind">error-string</span>
|
||||||
|
<span class="item-meta">{spanLabel(props.string.span)}</span>
|
||||||
|
</div>
|
||||||
|
<ConcreteErrorView error={props.string.error} label="string error" onHover={props.onHover} />
|
||||||
|
</HoverBlock>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HoverBlock
|
||||||
|
class={props.string.error ? "syntax-node literal-node syntax-error-node" : "syntax-node literal-node"}
|
||||||
|
label={`${props.label}: string`}
|
||||||
|
span={props.string.span}
|
||||||
|
onHover={props.onHover}
|
||||||
|
>
|
||||||
|
<div class="node-header">
|
||||||
|
<Show when={props.string.error}>
|
||||||
|
<span class="status-dot status-invalid" />
|
||||||
|
</Show>
|
||||||
|
<span class="node-kind">string</span>
|
||||||
|
<span class="node-value">{JSON.stringify(props.string.value)}</span>
|
||||||
|
</div>
|
||||||
|
<Show when={props.string.error}>
|
||||||
|
{(error) => <ConcreteErrorView error={error()} label="string error" onHover={props.onHover} />}
|
||||||
|
</Show>
|
||||||
|
</HoverBlock>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ErrorValueView(props: {
|
||||||
|
value:
|
||||||
|
| Extract<PartialJsonValue, { tag: "error-expression" }>
|
||||||
|
| Extract<PartialJsonValue, { tag: "error-number" }>
|
||||||
|
| Extract<PartialJsonValue, { tag: "error-string" }>;
|
||||||
|
label: string;
|
||||||
|
onHover: (target: HoverTarget | undefined) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<HoverBlock
|
||||||
|
class="syntax-node syntax-error-node"
|
||||||
|
label={`${props.label}: ${props.value.tag}`}
|
||||||
|
span={props.value.span}
|
||||||
|
onHover={props.onHover}
|
||||||
|
>
|
||||||
|
<div class="node-header">
|
||||||
|
<span class="status-dot status-invalid" />
|
||||||
|
<span class="node-kind">{props.value.tag}</span>
|
||||||
|
<span class="item-meta">{spanLabel(props.value.span)}</span>
|
||||||
|
</div>
|
||||||
|
<ConcreteErrorView error={props.value.error} label={props.value.tag} onHover={props.onHover} />
|
||||||
|
</HoverBlock>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConcreteErrorView(props: {
|
||||||
|
error: ConcreteError;
|
||||||
|
label: string;
|
||||||
|
onHover: (target: HoverTarget | undefined) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div class="concrete-error-list">
|
||||||
|
<For each={props.error}>
|
||||||
|
{(node, index) => (
|
||||||
|
<ConcreteErrorNodeView
|
||||||
|
node={node}
|
||||||
|
label={`${props.label} ${index() + 1}`}
|
||||||
|
onHover={props.onHover}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConcreteErrorNodeView(props: {
|
||||||
|
node: ConcreteErrorNode;
|
||||||
|
label: string;
|
||||||
|
onHover: (target: HoverTarget | undefined) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div class="concrete-error">
|
||||||
|
<div class="error-title">{errorTitle(props.node.error)}</div>
|
||||||
|
<div class="item-meta">{errorDetail(props.node.error)}</div>
|
||||||
|
<div class="span-chip-row">
|
||||||
|
<SpanChip label="focus" span={props.node.span} onHover={props.onHover} />
|
||||||
|
<Show when={props.node.panickedOver}>
|
||||||
|
{(span) => <SpanChip label="panicked over" span={span()} onHover={props.onHover} />}
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function delimiterLabel(value: PartialObject | PartialArray): string {
|
||||||
|
return value.close ? `${value.open.tag} / ${value.close.tag}` : `${value.open.tag} / missing close`;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
import type { FoundSyntax, ParseError } from '../../../languages/json';
|
||||||
|
import { spanLabel } from '../../format';
|
||||||
|
|
||||||
|
export function errorTitle(error: ParseError): string {
|
||||||
|
switch (error.tag) {
|
||||||
|
case "expected-value":
|
||||||
|
return "Expected value";
|
||||||
|
case "expected-member-key":
|
||||||
|
return "Expected member key";
|
||||||
|
case "expected-colon":
|
||||||
|
return "Expected colon";
|
||||||
|
case "expected-array-separator":
|
||||||
|
return "Expected array separator";
|
||||||
|
case "expected-object-separator":
|
||||||
|
return "Expected object separator";
|
||||||
|
case "expected-close-delimiter":
|
||||||
|
return "Expected closing delimiter";
|
||||||
|
case "unexpected-close-delimiter":
|
||||||
|
return "Unexpected closing delimiter";
|
||||||
|
case "invalid-string":
|
||||||
|
return "Invalid string";
|
||||||
|
case "invalid-number":
|
||||||
|
return "Invalid number";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function errorDetail(error: ParseError): string {
|
||||||
|
switch (error.tag) {
|
||||||
|
case "expected-value":
|
||||||
|
return `found ${foundLabel(error.found)}`;
|
||||||
|
case "expected-member-key":
|
||||||
|
return `found ${foundLabel(error.found)}`;
|
||||||
|
case "expected-colon":
|
||||||
|
return `found ${foundLabel(error.found)}`;
|
||||||
|
case "expected-array-separator":
|
||||||
|
return `found ${foundLabel(error.found)}`;
|
||||||
|
case "expected-object-separator":
|
||||||
|
return `found ${foundLabel(error.found)}`;
|
||||||
|
case "expected-close-delimiter":
|
||||||
|
return `expected ${error.expected}, opened at ${spanLabel(error.open)}, found ${foundLabel(error.found)}`;
|
||||||
|
case "unexpected-close-delimiter":
|
||||||
|
return `${error.delimiter} ${spanLabel(error.span)}`;
|
||||||
|
case "invalid-string":
|
||||||
|
return error.reason;
|
||||||
|
case "invalid-number":
|
||||||
|
return `${error.reason}: ${error.text}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function foundLabel(found: FoundSyntax): string {
|
||||||
|
switch (found.tag) {
|
||||||
|
case "eof":
|
||||||
|
return `EOF ${spanLabel(found.span)}`;
|
||||||
|
case "code-point":
|
||||||
|
return `${String.fromCodePoint(found.value)} ${spanLabel(found.span)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue