Skip to content
7 changes: 4 additions & 3 deletions src/app/components/BtnMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,16 @@ interface BtnMenuProps extends JSX.HTMLAttributes<HTMLDivElement> {
relative?: boolean,
tooltip?: string,
tooltipLoc?: 'se' | 'sw' | 'nw',
menuDir?: 'left' | 'right'
children: ComponentChildren,
}
export function BtnMenu(props: BtnMenuProps) {
const { icon, label, relative, tooltip, tooltipLoc, children } = props
const { icon, label, relative, tooltip, tooltipLoc, menuDir, children } = props
const [active, setActive] = useFocus()

return <div {...props} class={`btn-menu${relative === false ? ' no-relative' : ''} ${props.class}`}>
return <div {...props} class={`btn-menu${relative === false ? ' no-relative' : ''} ${props.class}`} >
<Btn {...{icon, label, tooltip, tooltipLoc}} onClick={() => setActive()} />
{active && <div class="btn-group">
{active && <div class="btn-group" style={menuDir === 'right' ? 'left:0;right:unset' : 'right:0;left:unset'}>
{children}
</div>}
</div>
Expand Down
1 change: 1 addition & 0 deletions src/app/components/Octicon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,5 @@ export const Octicon = {
upload: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8.53 1.22a.75.75 0 00-1.06 0L3.72 4.97a.75.75 0 001.06 1.06l2.47-2.47v6.69a.75.75 0 001.5 0V3.56l2.47 2.47a.75.75 0 101.06-1.06L8.53 1.22zM3.75 13a.75.75 0 000 1.5h8.5a.75.75 0 000-1.5h-8.5z"></path></svg>,
x: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M3.72 3.72a.75.75 0 011.06 0L8 6.94l3.22-3.22a.75.75 0 111.06 1.06L9.06 8l3.22 3.22a.75.75 0 11-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 01-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 010-1.06z"></path></svg>,
x_circle: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M3.404 12.596a6.5 6.5 0 119.192-9.192 6.5 6.5 0 01-9.192 9.192zM2.344 2.343a8 8 0 1011.313 11.314A8 8 0 002.343 2.343zM6.03 4.97a.75.75 0 00-1.06 1.06L6.94 8 4.97 9.97a.75.75 0 101.06 1.06L8 9.06l1.97 1.97a.75.75 0 101.06-1.06L9.06 8l1.97-1.97a.75.75 0 10-1.06-1.06L8 6.94 6.03 4.97z"></path></svg>,
wrap_inside: <svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><rect x="4" y="4" width="8" height="8" rx="1" stroke="currentColor" fill="none" stroke-width="1"/><path d="M3 6 C3 4.5 4.2 3.5 5.7 3.5H10 M10 3.5 L8.8 2.3 M10 3.5 L8.8 4.7" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"/><path d="M13 10 C13 11.5 11.8 12.5 10.3 12.5H6 M6 12.5 L7.2 13.7 M6 12.5 L7.2 11.3" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"/></svg>
}
4 changes: 2 additions & 2 deletions src/app/components/generator/JsonFileView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,11 @@ export function JsonFileView({ docAndNode, node }: JsonFileViewProps) {
}
const rootType = getRootType(resourceType)
const type = simplifyType(rootType, ctx)
return type
return {type, rootType}
}, [resourceType, ctx])

return <div class="file-view node-root" data-category={getCategory(resourceType)}>
{(ctx && mcdocType) && <McdocRoot type={mcdocType} node={node} ctx={ctx} />}
{(ctx && mcdocType) && <McdocRoot type={mcdocType.type} node={node} ctx={ctx} rootType={mcdocType.rootType} />}
</div>
}

Expand Down
119 changes: 117 additions & 2 deletions src/app/components/generator/McdocHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import * as core from '@spyglassmc/core'
import type { JsonNode, JsonPairNode } from '@spyglassmc/json'
import { JsonArrayNode, JsonObjectNode, JsonStringNode } from '@spyglassmc/json'
import { JsonStringOptions } from '@spyglassmc/json/lib/parser/string.js'
import type { Attributes, AttributeValue, ListType, McdocType, NumericType, PrimitiveArrayType, TupleType, UnionType } from '@spyglassmc/mcdoc'
import type { Attributes, AttributeValue, DispatcherType, ListType, McdocType, NumericType, PrimitiveArrayType, ReferenceType, TupleType, UnionType } from '@spyglassmc/mcdoc'
import { NumericRange, RangeKind } from '@spyglassmc/mcdoc'
import { TypeDefSymbolData } from '@spyglassmc/mcdoc/lib/binder/index.js'
import type { McdocCheckerContext, SimplifiedMcdocType, SimplifiedMcdocTypeNoUnion, SimplifyValueNode } from '@spyglassmc/mcdoc/lib/runtime/checker/index.js'
import { simplify } from '@spyglassmc/mcdoc/lib/runtime/checker/index.js'
import config from '../../Config.js'
Expand Down Expand Up @@ -42,7 +43,7 @@ export function getRootDefault(id: string, ctx: core.CheckerContext) {
return getDefault(type, core.Range.create(0), ctx)
}

export function getDefault(type: SimplifiedMcdocType, range: core.Range, ctx: core.CheckerContext): JsonNode {
export function getDefault(type: McdocType, range: core.Range, ctx: core.CheckerContext): JsonNode {
if (type.kind === 'string') {
return JsonStringNode.mock(range)
}
Expand Down Expand Up @@ -391,6 +392,7 @@ export function isSelectRegistry(registry: string) {

const defaultCollapsedTypes = new Set([
'::java::data::worldgen::surface_rule::SurfaceRule',
'::java::data::worldgen::density_function::DensityFunctionRef'
])

export function isDefaultCollapsedType(type: McdocType) {
Expand All @@ -399,6 +401,10 @@ export function isDefaultCollapsedType(type: McdocType) {
}
return false
}
export function canCollapse(node:JsonNode | undefined)
{
return node !== undefined && (node.type === 'json:array' || node.type == 'json:object');
}

interface SimplifyNodeContext {
key?: JsonStringNode
Expand Down Expand Up @@ -472,6 +478,115 @@ function inferType(node: JsonNode): Exclude<McdocType, UnionType> {
}
}

export type RecursiveSlot = {
identifier: string
dispatcher: DispatcherType
fieldKey: string
fieldCount: number
}
export function collectRecursiveDefinitions(ctx:core.CheckerContext, type: McdocType){
let recursiveSlots:RecursiveSlot[] = []
if(type.kind === 'reference')
{
let finalRefType = type;
while(true)
{
const newTypeDef = (ctx.symbols.query(ctx.doc, 'mcdoc', finalRefType.path!).symbol?.data as TypeDefSymbolData | undefined)?.typeDef
if(newTypeDef?.kind === 'reference')
finalRefType = newTypeDef
else break;
}
addRecursiveDefinitions(ctx, finalRefType, finalRefType, recursiveSlots);
}
else if(type.kind === 'dispatcher')
{
for(const index of type.parallelIndices)
{
if(index.kind === 'static')
{
const querySymbol = ctx.symbols.query(ctx.doc, 'mcdoc/dispatcher', type.registry, index.value).symbol
const targetType = (querySymbol?.data as TypeDefSymbolData | undefined)?.typeDef
if(targetType?.kind === 'reference')
{
let finalRefType = targetType;
while(true)
{
const newTypeDef = (ctx.symbols.query(ctx.doc, 'mcdoc', finalRefType.path!).symbol?.data as TypeDefSymbolData | undefined)?.typeDef
if(newTypeDef?.kind === 'reference')
finalRefType = newTypeDef
else break;
}
addRecursiveDefinitions(ctx, finalRefType, finalRefType, recursiveSlots);
}
}
}
}

return recursiveSlots
}
function addRecursiveDefinitions(ctx:core.CheckerContext, targetType: ReferenceType, referencedType:McdocType | undefined, recursiveSlots:RecursiveSlot[]) {
if(referencedType == undefined) return;
if(referencedType.kind === 'union') {
for(const member of referencedType.members){
addRecursiveDefinitions(ctx, targetType, member, recursiveSlots)
}
} else if(referencedType.kind === 'reference' && referencedType.path != undefined) {

if(referencedType.path == undefined) return;
const docValue = ctx.symbols.query(ctx.doc, 'mcdoc', referencedType.path).symbol;
if(!docValue?.data) return;
addRecursiveDefinitions(ctx, targetType, (docValue.data as TypeDefSymbolData | undefined)?.typeDef , recursiveSlots);
} else if(referencedType.kind === 'dispatcher')
{
for(const index of referencedType.parallelIndices)
{
if(index.kind === 'static')
{
const docValue = ctx.symbols.query(ctx.doc, 'mcdoc/dispatcher', referencedType.registry, index.value).symbol
if(docValue?.data)
addRecursiveDefinitions(ctx, targetType, (docValue.data as TypeDefSymbolData).typeDef, recursiveSlots);
}
}
}
else if(referencedType.kind === 'struct') {
for(const field of referencedType.fields) {
if(field.kind === 'spread')
{
if(field.type.kind === 'dispatcher'){
const symbols = ctx.symbols.global['mcdoc/dispatcher']?.[field.type.registry]
for(const key in symbols?.members)
{
const item = symbols.members[key] as core.Symbol
if(item == undefined || item.data == undefined) continue
const itemData = item.data as TypeDefSymbolData;
if(itemData.typeDef.kind != 'reference' || itemData.typeDef.path == undefined) continue
const simplified = simplifyType(itemData.typeDef, ctx);
if(simplified.kind != 'struct') continue;

for(const spreadField of simplified.fields){
if(spreadField.key.kind !== 'literal') continue;
let checkType: McdocType | undefined = spreadField.type;
while(checkType !== undefined && checkType.kind === 'reference' && checkType.path)
{
if(checkType.path === targetType.path)
{
recursiveSlots.push({identifier: 'minecraft:' + key, dispatcher: field.type, fieldKey: spreadField.key.value.value.toString(), fieldCount: simplified.fields.length})
break;
}
else{
const querySymbol:core.Symbol | undefined = ctx.symbols.query(ctx.doc, 'mcdoc', checkType.path).symbol
if(!querySymbol?.data) break;
checkType = (querySymbol.data as TypeDefSymbolData | undefined)?.typeDef
}
}
}
}
}
}
}
}
}

export function quickEqualTypes(a: SimplifiedMcdocTypeNoUnion, b: SimplifiedMcdocTypeNoUnion): boolean {
if (a === b) {
return true
Expand Down
86 changes: 83 additions & 3 deletions src/app/components/generator/McdocRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@ 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 { BtnMenu } from '../BtnMenu.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'
import { canCollapse, collectRecursiveDefinitions, formatIdentifier, getCategory, getChange, getDefault, getItemType, isDefaultCollapsedType, isFixedList, isInlineTuple, isListOrArray, isNumericType, isSelectRegistry, quickEqualTypes, RecursiveSlot, simplifyType } from './McdocHelpers.js'

export interface McdocContext extends core.CheckerContext {
makeEdit: MakeEdit
Expand All @@ -38,7 +39,10 @@ interface Props<Type extends SimplifiedMcdocType = SimplifiedMcdocType> {
node: JsonNode | undefined
ctx: McdocContext
}
export function McdocRoot({ type, node, ctx } : Props) {
interface McdocRootProps extends Props{
rootType: McdocType
}
export function McdocRoot({ type, rootType, node, ctx } : McdocRootProps) {
const { locale } = useLocale()

if (type.kind === 'struct' && type.fields.length > 0 && JsonObjectNode.is(node)) {
Expand All @@ -50,6 +54,7 @@ export function McdocRoot({ type, node, ctx } : Props) {
<Errors type={type} node={node} ctx={ctx} />
<Key label={locale('root')} />
<Head type={type} node={node} ctx={ctx} />
{node != undefined && <RecursiveContextMenu type={rootType} node={node} ctx={ctx}/>}
</div>
<Body type={type} node={node} ctx={ctx} />
</>
Expand Down Expand Up @@ -590,7 +595,7 @@ function StaticField({ pair, index, field, fieldKey, staticFields, isToggled, ex

const child = pair?.value
const childType = simplifyType(field.type, ctx, { key: pair?.key, parent: node })
const canToggle = isDefaultCollapsedType(field.type)
const canToggle = isDefaultCollapsedType(field.type) && canCollapse(child)
const isCollapsed = canToggle && isToggled !== true

const makeFieldEdit = useCallback<MakeEdit>((edit) => {
Expand Down Expand Up @@ -660,11 +665,86 @@ function StaticField({ pair, index, field, fieldKey, staticFields, isToggled, ex
)}
<Key label={fieldKey} doc={field.desc} />
{!isCollapsed && <Head type={childType} node={child} optional={field.optional} ctx={fieldCtx} />}
{child != undefined && <RecursiveContextMenu type={field.type} node={child} ctx={fieldCtx}/>}
</div>
{!isCollapsed && <Body type={childType} node={child} optional={field.optional} ctx={fieldCtx} />}
</div>
}
interface RecursiveContextMenuProps{
type: McdocType
node: JsonNode
ctx: McdocContext
}
function RecursiveContextMenu({type, node, ctx} : RecursiveContextMenuProps){
const wrapTargets = useMemo(() => {
return collectRecursiveDefinitions(ctx, type)
}, [type, ctx.doc, ctx.symbols]);

const onclick = useCallback((wrappable: RecursiveSlot) => {
ctx.makeEdit(range => {
//const keyNode = JsonStringNode.mock(core.Range.create(0))
//keyNode.value = 'type'
const objectNode = JsonObjectNode.mock(range)
for(const index of wrappable.dispatcher.parallelIndices)
{
let valueNode = JsonStringNode.mock(core.Range.create(0));
valueNode.value = wrappable.identifier

if(index.kind == 'static')
{
let keyNode = JsonStringNode.mock(core.Range.create(0));
keyNode.value = index.value

objectNode.children.push({
type: 'pair',
range: core.Range.create(0),
key: keyNode,
value: valueNode,
});
} else if(index.kind == 'dynamic')
{
for(const accessor of index.accessor)
{
let keyNode = JsonStringNode.mock(core.Range.create(0));
keyNode.value = accessor.toString()

objectNode.children.push({
type: 'pair',
range: core.Range.create(0),
key: keyNode,
value: valueNode,
});
}
}
}


let childKeyNode = JsonStringNode.mock(core.Range.create(0));
childKeyNode.value = wrappable.fieldKey
const newPair: core.PairNode<JsonStringNode, JsonNode> = {
type: 'pair',
range: core.Range.create(0),
key: childKeyNode,
value: node,
}
objectNode.children.push(newPair)

return objectNode
})
}, [node, ctx]);
if(wrapTargets.length == 0) return <></>;
return <BtnMenu class='recursive-wrapper' tooltipLoc='se' menuDir='right' icon='wrap_inside'>
{wrapTargets.map(v => {
let label = formatIdentifier(v.identifier)
if(v.fieldCount > 1)
{
label += '/' + formatIdentifier(v.fieldKey)
}
return <Btn label={label} onClick={() => onclick(v)}/>
}
)}
</BtnMenu>
}
interface DynamicKeyProps {
keyType: SimplifiedMcdocType
valueType: McdocType
Expand Down
3 changes: 3 additions & 0 deletions src/styles/nodes.css
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,9 @@
padding-left: 9px;
background-color: var(--node-background-input);
}
.node-header > .btn-menu > .btn{
border-radius: 0;
}

.node-header > input[type="color"] {
padding: 0 2px;
Expand Down