import * as core from '@spyglassmc/core' import { ColorFormat } from '@spyglassmc/core' import type { JsonPairNode } from '@spyglassmc/json' import * as json from '@spyglassmc/json' import { JsonArrayNode, JsonBooleanNode, JsonNode, JsonNumberNode, JsonObjectNode, JsonStringNode } from '@spyglassmc/json' import { localeQuote } from '@spyglassmc/locales' import type { ListType, LiteralType, McdocType, NumericType, PrimitiveArrayType, StringType, TupleType, UnionType } from '@spyglassmc/mcdoc' import { handleAttributes } from '@spyglassmc/mcdoc/lib/runtime/attribute/index.js' import type { SimplifiedEnum, SimplifiedMcdocType, SimplifiedMcdocTypeNoUnion, SimplifiedStructType, SimplifiedStructTypePairField } from '@spyglassmc/mcdoc/lib/runtime/checker/index.js' import { getValues } from '@spyglassmc/mcdoc/lib/runtime/completer/index.js' import { Identifier as Identifier1204, ItemStack as ItemStack1204 } from 'deepslate-1.20.4/core' import { Identifier, ItemStack } from 'deepslate/core' import DOMPurify from 'dompurify' import { marked } from 'marked' import { useCallback, useEffect, useMemo, useState } from 'preact/hooks' import config from '../../Config.js' import { useLocale } from '../../contexts/Locale.jsx' import { useVersion } from '../../contexts/Version.jsx' import { useFocus } from '../../hooks/useFocus.js' import { checkVersion } from '../../services/Versions.js' import { generateColor, hexId, intToHexRgb, randomInt, randomSeed } from '../../Utils.js' import { Btn } from '../Btn.jsx' import { ItemDisplay } from '../ItemDisplay.jsx' import { ItemDisplay1204 } from '../ItemDisplay1204.jsx' import { Octicon } from '../Octicon.jsx' import { formatIdentifier, getCategory, getChange, getDefault, getItemType, isDefaultCollapsedType, isFixedList, isInlineTuple, isListOrArray, isNumericType, isSelectRegistry, quickEqualTypes, simplifyType } from './McdocHelpers.js' export interface McdocContext extends core.CheckerContext { makeEdit: MakeEdit } type MakeEdit = (edit: (range: core.Range) => JsonNode | undefined) => void interface Props { type: Type optional?: boolean excludeStrings?: string[] node: JsonNode | undefined ctx: McdocContext } export function McdocRoot({ type, node, ctx } : Props) { const { locale } = useLocale() if (type.kind === 'struct' && type.fields.length > 0 && JsonObjectNode.is(node)) { return } return <>
} function Head({ type, optional, excludeStrings, node, ctx }: Props) { if (type.kind === 'string') { return } if (type.kind === 'enum') { return } if (isNumericType(type)) { return } if (type.kind === 'boolean') { return } if (type.kind === 'union') { return } if (type.kind === 'struct') { return } if (isListOrArray(type)) { if (isFixedList(type)) { return getItemType(type)), attributes: type.attributes }} optional={optional} node={node} ctx={ctx} /> } return } if (type.kind === 'tuple') { return } if (type.kind === 'literal') { return } if (type.kind === 'any' || type.kind === 'unsafe') { return } return <> } function Body({ type, optional, node, ctx }: Props) { if (type.kind === 'union') { return } if (type.kind === 'struct') { if (!JsonObjectNode.is(node) || type.fields.length === 0) { return <> } return
} if (isListOrArray(type)) { if (!JsonArrayNode.is(node)) { return <> } if (isFixedList(type)) { const tupleType: TupleType = { kind: 'tuple', items: [...Array(type.lengthRange.min)].map(() => getItemType(type)), attributes: type.attributes } if (isInlineTuple(tupleType)) { return <> } return
} if (node.children?.length === 0) { return <> } return
} if (type.kind === 'tuple') { if (isInlineTuple(type)) { return <> } return
} if (type.kind === 'any' || type.kind === 'unsafe') { return } return <> } const SPECIAL_UNSET = '__unset__' function StringHead({ type, optional, excludeStrings, node, ctx }: Props) { const { locale } = useLocale() const nodeValue = (JsonStringNode.is(node) ? node.value : undefined)?.replaceAll('\n', '\\n') const [value, setValue] = useState(nodeValue) useEffect(() => { setValue(nodeValue) }, [nodeValue]) const idAttribute = type.attributes?.find(a => a.name === 'id')?.value const idRegistry = idAttribute?.kind === 'literal' && idAttribute.value.kind === 'string' ? idAttribute.value.value : idAttribute?.kind === 'tree' && idAttribute.values.registry?.kind === 'literal' && idAttribute.values.registry.value.kind === 'string' ? idAttribute.values.registry.value.value : undefined const idTags = idAttribute?.kind === 'tree' && idAttribute.values.tags?.kind === 'literal' && idAttribute.values.tags.value.kind === 'string' ? idAttribute.values.tags.value.value : undefined const isSelect = idRegistry && isSelectRegistry(idRegistry) const onChangeValue = useCallback((newValue: string) => { newValue = newValue.replaceAll('\\n', '\n') if (nodeValue === newValue) { return } ctx.makeEdit((range) => { if ((newValue.length === 0 && optional) || (isSelect && newValue === SPECIAL_UNSET)) { return undefined } const valueMap = [{ inner: core.Range.create(0), outer: core.Range.create(range.start) }] const source = new core.Source(JSON.stringify(newValue), valueMap) const string = core.string(json.parser.JsonStringOptions)(source, ctx) return { ...string, type: 'json:string', } }) }, [optional, node, ctx, nodeValue, isSelect]) const onCommitValue = useCallback(() => { onChangeValue(value ?? '') }, [value, onChangeValue]) const completions = useMemo(() => { const values = getValues(type, { ...ctx, offset: node?.range.start ?? 0 }) .filter(c => c.kind === 'string' && c.value !== 'THIS') .filter(c => !excludeStrings?.includes(c.value)) values.sort((a, b) => a.value.localeCompare(b.value)) return values }, [type, excludeStrings, node, ctx]) const datalistId = `mcdoc_completions_${hexId()}` const gen = idRegistry ? config.generators.find(gen => gen.id === idRegistry) : undefined const color = type.attributes?.find(a => a.name === 'color')?.value const colorKind = color?.kind === 'literal' && color.value.kind === 'string' ? color.value.value : undefined const onRandomColor = useCallback(() => { const color = generateColor() onChangeValue(intToHexRgb(color)) }, [onChangeValue]) return <> {((idRegistry === 'item' || idRegistry === 'block') && idTags !== 'implicit' && value && !value.startsWith('#')) && } {isSelect ? <> : <> {completions.length > 0 && {completions.map(c => )} } setValue((e.target as HTMLInputElement).value)} onBlur={onCommitValue} onSubmit={onCommitValue} onKeyDown={(e) => {if (e.key === 'Enter') onCommitValue()}} list={completions.length > 0 ? datalistId : undefined} /> {value && gen && {Octicon.link_external} } } {colorKind === 'hex_rgb' && <> onChangeValue((e.target as HTMLInputElement).value)} /> } } function ItemIdPreview({ id }: { id: string }) { const { version } = useVersion() const stack = useMemo(() => { try { if (!checkVersion(version, '1.20.5')) { return new ItemStack1204(Identifier1204.parse(id), 1) } return new ItemStack(Identifier.parse(id), 1) } catch (e) { return undefined } }, [id, version]) return <>{stack && } } function EnumHead({ type, optional, excludeStrings, node, ctx }: Props) { const { locale } = useLocale() const value = JsonStringNode.is(node) ? node.value : (node && JsonNumberNode.is(node)) ? Number(node.value.value) : undefined const onChangeValue = useCallback((newValue: string) => { if (value === newValue) { return } ctx.makeEdit((range) => { if (newValue === SPECIAL_UNSET) { return undefined } if (type.enumKind === 'string') { return { type: 'json:string', range, options: json.parser.JsonStringOptions, value: newValue, valueMap: [{ inner: core.Range.create(0), outer: core.Range.create(range.start) }], } } const number: core.FloatNode = { type: 'float', range, value: parseFloat(newValue), } const result: JsonNumberNode = { type: 'json:number', range, children: [number], value: number, } number.parent = result return result }) }, [type.enumKind, value, ctx]) const color = type.attributes?.find(a => a.name === 'color')?.value const colorKind = color?.kind === 'literal' && color.value.kind === 'string' ? color.value.value : undefined const nodeColor = node?.color ? (Array.isArray(node.color) ? node.color : node.color.value ) : undefined const inputColor = nodeColor ? core.ColorPresentation.fromColorFormat(ColorFormat.HexRGB, nodeColor, core.Range.create(0)).text : undefined return <> {colorKind === 'named' && nodeColor && <> } } function NumericHead({ type, node, ctx }: Props) { const { locale } = useLocale() const nodeValue = node && JsonNumberNode.is(node) ? Number(node.value.value) : undefined const [value, setValue] = useState(nodeValue?.toString()) useEffect(() => { setValue(nodeValue?.toString()) }, [nodeValue]) const onChangeValue = useCallback((value: string | bigint | number) => { const number = typeof value === 'string' ? (value.length === 0 ? undefined : Number(value)) : value if (number !== undefined && Number.isNaN(number)) { return } ctx.makeEdit((range) => { if (number === undefined) { return undefined } const newValue: core.FloatNode | core.LongNode = typeof number === 'bigint' || Number.isInteger(number) ? { type: 'long', range, value: BigInt(number) } : { type: 'float', range, value: Number(number) } const newNode: JsonNumberNode = { type: 'json:number', range, value: newValue, children: [newValue], } newValue.parent = newNode return newNode }) }, [node, ctx]) const onCommitValue = useCallback(() => { onChangeValue(value ?? '') }, [value, onChangeValue]) const color = type.attributes?.find(a => a.name === 'color')?.value const colorKind = color?.kind === 'literal' && color.value.kind === 'string' ? color.value.value : undefined const onChangeColor = useCallback((value: string) => { onChangeValue(parseInt(value.slice(1), 16).toString()) }, [onChangeValue]) const onRandomColor = useCallback(() => { onChangeValue(generateColor().toString()) }, [onChangeValue]) const random = type.attributes?.find(a => a.name === 'random') const onRandom = useCallback(() => { onChangeValue(type.kind === 'long' ? randomSeed() : randomInt()) }, [type, onChangeValue]) return <> setValue((e.target as HTMLInputElement).value)} onBlur={onCommitValue} onSubmit={onCommitValue} onKeyDown={(e) => {if (e.key === 'Enter') onCommitValue()}} /> {colorKind && <> onChangeColor((e.target as HTMLInputElement).value)} /> } {random && <> } } function BooleanHead({ node, ctx }: Props) { const value = node && JsonBooleanNode.is(node) ? node.value : undefined const onSelect = useCallback((newValue: boolean) => { ctx.makeEdit((range) => { if (value === newValue) { return undefined } return { type: 'json:boolean', range, value: newValue, } }) }, [node, ctx, value]) return <> } function UnionHead({ type, optional, node, ctx }: Props>) { const { locale } = useLocale() if (type.members.length === 0) { return <> } const selectedType = selectUnionMember(type, node) const onSelect = useCallback((newValue: string) => { ctx.makeEdit((range) => { if (newValue === SPECIAL_UNSET) { return undefined } const newSelected = type.members[parseInt(newValue)] if (node && selectedType) { return getChange(newSelected, selectedType, node, ctx) } return getDefault(newSelected, range, ctx) }) }, [type, node, ctx, selectedType]) const memberIndex = selectedType ? type.members.findIndex(m => quickEqualTypes(m, selectedType)) : -1 return <> {selectedType && selectedType.kind !== 'literal' && } } function formatUnionMember(type: SimplifiedMcdocTypeNoUnion, others: SimplifiedMcdocTypeNoUnion[]): string { const memberNameAttribute = type.attributes?.find(a => a.name === 'misode_member_name')?.value if (memberNameAttribute?.kind === 'literal' && memberNameAttribute.value.kind === 'string') { return memberNameAttribute.value.value } if (type.kind === 'literal') { return formatIdentifier(type.value.value.toString()) } if (!others.some(o => o.kind === type.kind)) { // No other member is of this kind return formatIdentifier(type.kind === 'struct' ? 'object' : type.kind) } if (type.kind === 'struct') { // Show the first literal key const firstKey = type.fields.find(f => f.key.kind === 'literal')?.key if (firstKey) { return formatUnionMember(firstKey, []) } } return formatIdentifier(type.kind === 'struct' ? 'object' : type.kind) } function UnionBody({ type, optional, node, ctx }: Props>) { const selectedType = selectUnionMember(type, node) if (selectedType === undefined) { return <> } return } function selectUnionMember(union: UnionType, node: JsonNode | undefined) { const selectedType = node?.typeDef if (!selectedType || selectedType.kind === 'any' || selectedType.kind === 'unsafe') { return undefined } if (selectedType.kind === 'union') { // Find the first selected type that is also part of the original definition. // The node technically matches all members of this union, // ideally the editor should show a combination of all members return selectedType.members.find(m1 => union.members.find(m2 => quickEqualTypes(m1, m2))) } return selectedType } function StructHead({ type: outerType, optional, node, ctx }: Props) { const { locale } = useLocale() const type = node?.typeDef?.kind === 'struct' ? node.typeDef : outerType const onRemove = useCallback(() => { ctx.makeEdit(() => { return undefined }) }, [ctx]) const onSetDefault = useCallback(() => { ctx.makeEdit((range) => { return getDefault(type, range, ctx) }) }, [type, ctx]) return <> {optional ? (JsonObjectNode.is(node) ? : ) : (!JsonObjectNode.is(node) ? : <> )} } function StructBody({ type: outerType, node, ctx }: Props) { if (!JsonObjectNode.is(node)) { return <> } const { expand, collapse, isToggled } = useToggles() const type = node.typeDef?.kind === 'struct' ? node.typeDef : outerType // For some reason spyglass can include fields that haven't been filtered out in node.typeDef const fields = type.fields.filter(field => { let keep = true handleAttributes(field.attributes, ctx, (handler, config) => { if (!keep || !handler.filterElement) { return } if (!handler.filterElement(config, ctx)) { keep = false } }) return keep }) const staticFields = fields.filter(field => field.key.kind === 'literal') const dynamicFields = fields.filter(field => field.key.kind !== 'literal') const staticChilds: core.PairNode[] = [] return <> {staticFields.map(field => { const key = (field.key as LiteralType).value.value.toString() const index = node.children.findIndex(p => p.key?.value === key) const pair = index === -1 ? undefined : node.children[index] if (pair) { staticChilds.push(pair) } return })} {dynamicFields.map((field, index) => { if (field.key.kind === 'any' && field.type.kind === 'any') { // Hide dispatch fallback return <> } const keyType = simplifyType(field.key, ctx) return
})} {node.children.map((pair, index) => { const key = pair.key?.value if (staticChilds.includes(pair) || !key) { return <> } if (pair.value && core.Range.length(pair.value.range) === 0) { return <> } // TODO: correctly determine which dynamic field this is a key for // Hack to support component negations, the only place with more than one dynamic field currently const field = dynamicFields[key.startsWith('!') ? 1 : 0] as SimplifiedStructTypePairField | undefined if (!field || (field.key.kind === 'any' && field.type.kind === 'any')) { return } return })} } interface StaticFieldProps extends Props { pair: JsonPairNode | undefined index: number field: SimplifiedStructTypePairField fieldKey: string staticFields: SimplifiedStructTypePairField[] isToggled: boolean | undefined expand: (e: MouseEvent) => void collapse: (e: MouseEvent) => void node: JsonObjectNode } function StaticField({ pair, index, field, fieldKey, staticFields, isToggled, expand, collapse, node, ctx }: StaticFieldProps) { const { locale } = useLocale() const child = pair?.value const childType = simplifyType(field.type, ctx, { key: pair?.key, parent: node }) const canToggle = isDefaultCollapsedType(field.type) const isCollapsed = canToggle && isToggled !== true const makeFieldEdit = useCallback((edit) => { if (pair) { ctx.makeEdit(() => { const newChild = edit(child?.range ?? core.Range.create(pair.range.end)) if (newChild === undefined) { node.children.splice(index, 1) } else { node.children[index] = { type: 'pair', range: pair.range, key: pair.key, value: newChild, } } return node }) } else { const newFieldIndex = staticFields.indexOf(field) const insertIndex = node.children.findIndex(child => { const childKey = child.key?.value if (!childKey) { return false } const otherChildIndex = staticFields.findIndex(f => (f.key as LiteralType).value.value.toString() === childKey) return otherChildIndex > newFieldIndex }) const newChild = edit(core.Range.create(node.range.end)) if (newChild) { ctx.makeEdit(() => { const newPair: JsonPairNode = { type: 'pair', range: newChild.range, key: { type: 'json:string', range: newChild.range, options: json.parser.JsonStringOptions, value: fieldKey, valueMap: [{ inner: core.Range.create(0), outer: newChild.range }], }, value: newChild, } if (insertIndex === -1) { node.children.push(newPair) } else { node.children.splice(insertIndex, 0, newPair) } newPair.parent = node return node }) } } }, [pair, index, node, ctx]) const fieldCtx = useMemo(() => { return { ...ctx, makeEdit: makeFieldEdit } }, [ctx, makeFieldEdit]) return
{!field.optional && child === undefined && } {canToggle && (isCollapsed ? : )} {!isCollapsed && }
{!isCollapsed && }
} interface DynamicKeyProps { keyType: SimplifiedMcdocType valueType: McdocType parent: JsonObjectNode ctx: McdocContext } function DynamicKey({ keyType, valueType, parent, ctx }: DynamicKeyProps) { const { locale } = useLocale() const [key, setKey] = useState() const keyNode = useMemo(() => { if (key === undefined) return undefined const node = JsonStringNode.mock(core.Range.create(0)) node.value = key return node }, [key]) const makeKeyEdit = useCallback((edit) => { const newKeyNode = edit(core.Range.create(0)) if (JsonStringNode.is(newKeyNode)) { setKey(newKeyNode.value) } }, []) const keyCtx = useMemo(() => { return { ...ctx, makeEdit: makeKeyEdit } }, [ctx, makeKeyEdit]) const addKey = useCallback(() => { if (!keyNode) { return } setKey(undefined) ctx.makeEdit((range) => { const valueNode = getDefault(simplifyType(valueType, ctx, { key: keyNode, parent }), range, ctx) const newPair: core.PairNode = { type: 'pair', range: keyNode.range, key: keyNode, value: valueNode, } valueNode.parent = newPair parent.children.push(newPair) newPair.parent = parent return parent }) }, [keyNode, ctx]) const excludeStrings = useMemo(() => { return parent.children.flatMap(pair => pair.key ? [pair.key.value] : []) }, [parent]) return <> } interface DynamicFieldProps extends Props { pair: JsonPairNode index: number field: SimplifiedStructTypePairField fieldKey: string isToggled: boolean | undefined expand: (e: MouseEvent) => void collapse: (e: MouseEvent) => void node: JsonObjectNode } function DynamicField({ pair, index, field, fieldKey, isToggled, expand, collapse, node, ctx }: DynamicFieldProps) { const { locale } = useLocale() const child = pair.value const canToggle = JsonObjectNode.is(child) || JsonArrayNode.is(child) const isCollapsed = canToggle && (isToggled === false || (isToggled === undefined && (node.children.length - node.children.length) > 20)) const childType = simplifyType(field.type, ctx, { key: pair.key, parent: node }) const category = getCategory(field.type) const makeFieldEdit = useCallback((edit) => { ctx.makeEdit(() => { const newChild = edit(child?.range ?? core.Range.create(pair.range.end)) if (newChild === undefined) { node.children.splice(index, 1) } else { node.children[index] = { type: 'pair', range: pair.range, key: pair.key, value: newChild, } } return node }) }, [pair, index, node, ctx]) const fieldCtx = useMemo(() => { return { ...ctx, makeEdit: makeFieldEdit } }, [ctx, makeFieldEdit]) return
{canToggle && (isCollapsed ? : )} {!isCollapsed && }
{!isCollapsed && (childType.kind === 'struct' && category ?
: )}
} interface UnknownFieldProps extends Props { pair: JsonPairNode index: number fieldKey: string node: JsonObjectNode } function UnknownField({ pair, index, fieldKey, node, ctx }: UnknownFieldProps) { const { locale } = useLocale() const removeField = useCallback(() => { ctx.makeEdit(() => { node.children.splice(index, 1) return node }) }, [index, node, ctx]) return
} function ListHead({ type, node, ctx }: Props) { const { locale } = useLocale() const canAdd = (type.lengthRange?.max ?? Infinity) > (node?.children?.length ?? 0) const onAddTop = useCallback(() => { if (canAdd) { ctx.makeEdit((range) => { const itemType = simplifyType(getItemType(type), ctx) const newValue = getDefault(itemType, range, ctx) const newItem: core.ItemNode = { type: 'item', range, children: [newValue], value: newValue, } newValue.parent = newItem if (JsonArrayNode.is(node)) { node.children.unshift(newItem) newItem.parent = node return node } const newArray: JsonArrayNode = { type: 'json:array', range, children: [newItem], } newItem.parent = newArray return newArray }) } }, [type, node, ctx, canAdd]) return } function ListBody({ type: outerType, node, ctx }: Props) { if (!JsonArrayNode.is(node)) { return <> } const { locale } = useLocale() const { expand, collapse, isToggled } = useToggles() const [maxShown, setMaxShown] = useState(50) const type = node.typeDef && isListOrArray(node.typeDef) ? node.typeDef : outerType const itemType = getItemType(type) const category = getCategory(itemType) const childType = simplifyType(itemType, ctx) const canAdd = (type.lengthRange?.max ?? Infinity) > (node?.children?.length ?? 0) const onAddBottom = useCallback(() => { if (canAdd) { ctx.makeEdit((range) => { const itemType = simplifyType(getItemType(type), ctx) const newValue = getDefault(itemType, range, ctx) const newItem: core.ItemNode = { type: 'item', range, children: [newValue], value: newValue, } newValue.parent = newItem if (JsonArrayNode.is(node)) { node.children.push(newItem) newItem.parent = node return node } const newArray: JsonArrayNode = { type: 'json:array', range, children: [newItem], } newItem.parent = newArray return newArray }) } }, [type, node, ctx, canAdd]) return <> {node.children.map((item, index) => { if (index === maxShown) { return
} if (index > maxShown) { return <> } const key = index.toString() return })} {node.children.length > 0 &&
} } interface ListItemProps extends Props { item: core.ItemNode index: number category: string | undefined isToggled: boolean | undefined expand: (e: MouseEvent) => void collapse: (e: MouseEvent) => void node: JsonArrayNode } function ListItem({ item, index, category, type, isToggled, expand, collapse, node, ctx }: ListItemProps) { const { locale } = useLocale() const [active, setActive] = useFocus() const child = item.value const canToggle = JsonObjectNode.is(child) const isCollapsed = canToggle && (isToggled === false || (isToggled === undefined && node.children.length > 20)) const canMoveUp = node.children.length > 1 && index > 0 const canMoveDown = node.children.length > 1 && index < (node.children.length - 1) const onRemove = useCallback(() => { ctx.makeEdit(() => { node.children.splice(index, 1) return node }) }, [ctx, node, index]) const onMoveUp = useCallback(() => { if (node.children.length <= 1 || index <= 0) { return } ctx.makeEdit(() => { const moved = node.children.splice(index, 1) node.children.splice(index - 1, 0, ...moved) return node }) }, [ctx, node, index]) const onMoveDown = useCallback(() => { if (node.children.length <= 1 || index >= node.children.length - 1) { return } ctx.makeEdit(() => { const moved = node.children.splice(index, 1) node.children.splice(index + 1, 0, ...moved) return node }) }, [ctx, node, index]) const onDuplicate = useCallback(() => { ctx.makeEdit(() => { node.children.splice(index + 1, 0, node.children[index]) return node }) }, [ctx, node, index]) const makeItemEdit: MakeEdit = useCallback((edit) => { ctx.makeEdit(() => { const newChild = edit(child?.range ?? item.range) node.children[index] = { type: 'item', range: item.range, value: newChild, } return node }) }, [ctx, child, item, node]) const itemCtx = useMemo(() => { return { ...ctx, makeEdit: makeItemEdit } }, [ctx, makeItemEdit]) const onContextMenu = (evt: MouseEvent) => { evt.preventDefault() setActive() } return
{canToggle && (isCollapsed ? : )} {(canMoveUp || canMoveDown) &&
} {active &&
} {!isCollapsed && }
{!isCollapsed && (type.kind === 'struct' ?
: )}
} function TupleHead({ type, optional, node, ctx }: Props) { const { locale } = useLocale() const isInline = isInlineTuple(type) const onRemove = useCallback(() => { ctx.makeEdit(() => { return undefined }) }, [ctx]) const onSetDefault = useCallback(() => { ctx.makeEdit((range) => { return getDefault(type, range, ctx) }) }, [type, ctx]) return <> {optional ? (JsonArrayNode.is(node) ? : ) : (!JsonArrayNode.is(node) ? : <> )} {isInline && JsonArrayNode.is(node) && type.items.map((itemType, index) => { const item = node?.children?.[index] const child = item?.value const childType = simplifyType(itemType, ctx) return })} } interface TupleHeadItemProps extends Props { child: JsonNode | undefined childType: SimplifiedMcdocType index: number node: JsonArrayNode } function TupleHeadItem({ child, childType, index, node, ctx }: TupleHeadItemProps) { const makeItemEdit = useCallback((edit) => { ctx.makeEdit((range) => { const newChild = edit(child?.range ?? node?.range ?? range) if (newChild === undefined) { return node } node.children[index] = { type: 'item', range: newChild.range, value: newChild, } return node }) }, [index, node, ctx]) const itemCtx = useMemo(() => { return { ...ctx, makeEdit: makeItemEdit } }, [ctx, makeItemEdit]) return } function TupleBody({ type, node, ctx }: Props) { if (!JsonArrayNode.is(node)) { return <> } return <> {type.items.map((itemType, index) => { const item = node?.children?.[index] const child = item?.value const childType = simplifyType(itemType, ctx) return })} } interface TupleBodyItemProps extends Props { child: JsonNode | undefined childType: SimplifiedMcdocType index: number node: JsonArrayNode } function TupleBodyItem({ child, childType, index, node, ctx }: TupleBodyItemProps) { const makeItemEdit = useCallback((edit) => { ctx.makeEdit(() => { const newChild = edit(child?.range ?? node.range) if (newChild === undefined) { return node } node.children[index] = { type: 'item', range: newChild.range, value: newChild, } return node }) }, [index, node, ctx]) const itemCtx = useMemo(() => { return { ...ctx, makeEdit: makeItemEdit } }, [ctx, makeItemEdit]) return
} function LiteralHead({ type, optional, node, ctx }: Props) { return } const ANY_TYPES: SimplifiedMcdocType[] = [ { kind: 'boolean' }, { kind: 'double' }, { kind: 'string' }, { kind: 'list', item: { kind: 'any' } }, { kind: 'struct', fields: [ { kind: 'pair', key: { kind: 'string' }, type: { kind: 'any' } }] }, ] function AnyHead({ optional, node, ctx }: Props) { const { locale } = useLocale() const selectedType = selectAnyType(node) const onSelect = useCallback((newValue: string) => { ctx.makeEdit((range) => { const newSelected = ANY_TYPES.find(t => t.kind === newValue) if (!newSelected) { return undefined } return getDefault(newSelected, range, ctx) }) }, [ctx]) return <> {selectedType && } } function AnyBody({ optional, node, ctx }: Props) { const selectedType = selectAnyType(node) if (!selectedType) { return <> } return } function selectAnyType(node: JsonNode | undefined) { switch (node?.type) { case 'json:boolean': return ANY_TYPES[0] case 'json:number': return ANY_TYPES[1] case 'json:string': return ANY_TYPES[2] case 'json:array': return ANY_TYPES[3] case 'json:object': return ANY_TYPES[4] default: return undefined } } interface KeyProps { label: string | number | boolean doc?: string raw?: boolean } function Key({ label, doc, raw }: KeyProps) { const [shown, setShown] = useFocus() const cleanDoc = useMemo(() => { if (!doc) { return doc } return DOMPurify.sanitize(marked(doc), { FORBID_ATTR: ['style'] }) }, [doc]) return } interface ErrorsProps { type: SimplifiedMcdocType node: core.AstNode | undefined ctx: McdocContext } function Errors({ type, node, ctx }: ErrorsProps) { const errors = useMemo(() => { if (node === undefined) { return [] } const errors = ctx.err.errors // Get all errors inside the current node .filter(e => core.Range.containsRange(node.range, e.range, true)) // Unless they are inside a child node .filter(e => !node.children?.some(c => (c.type === 'item' || c.type === 'pair') && core.Range.containsRange(c.range, e.range, true))) // Filter out "Missing key" errors .filter(e => !(core.Range.length(e.range) === 1 && (type.kind === 'struct' || (type.kind === 'union' && JsonNode.is(node) && (selectUnionMember(type, node) ?? type.members[0]).kind === 'struct')))) // Hide warnings if there are errors return errors.find(e => e.severity === 3) ? errors.filter(e => e.severity === 3) : errors }, [type, node, ctx]) return <> {errors.map(e => )} } interface ErrorIndicatorProps { error: core.LanguageError } function ErrorIndicator({ error }: ErrorIndicatorProps) { const [active, setActive] = useFocus() return
setActive()}> {Octicon.issue_opened} {error.message.replace(/ \(rule: [a-zA-Z]+\)$/, '')}
} function useToggles() { const [toggleState, setToggleState] = useState(new Map()) const [toggleAll, setToggleAll] = useState(undefined) const expand = useCallback((key: string) => (evt: MouseEvent) => { if (evt.ctrlKey) { setToggleState(new Map()) setToggleAll(true) } else { setToggleState(state => new Map(state.set(key, true))) } }, []) const collapse = useCallback((key: string) => (evt: MouseEvent) => { if (evt.ctrlKey) { setToggleState(new Map()) setToggleAll(false) } else { setToggleState(state => new Map(state.set(key, false))) } }, []) const isToggled = useCallback((key: string) => { if (!(toggleState instanceof Map)) return false return toggleState.get(key) ?? toggleAll }, [toggleState, toggleAll]) return { expand, collapse, isToggled } }