diff --git a/src/app/Store.ts b/src/app/Store.ts
index d826acac..aa39637f 100644
--- a/src/app/Store.ts
+++ b/src/app/Store.ts
@@ -4,6 +4,7 @@ import type { Project } from './contexts/index.js'
import { DRAFT_PROJECT } from './contexts/index.js'
import type { VersionId } from './services/index.js'
import { DEFAULT_VERSION, VersionIds } from './services/index.js'
+import { safeJsonParse } from './Utils.js'
export namespace Store {
export const ID_LANGUAGE = 'language'
@@ -65,7 +66,7 @@ export namespace Store {
export function getProjects(): Project[] {
const projects = localStorage.getItem(ID_PROJECTS)
if (projects) {
- return JSON.parse(projects) as Project[]
+ return safeJsonParse(projects) ?? []
}
return [DRAFT_PROJECT]
}
@@ -73,13 +74,13 @@ export namespace Store {
export function getPreviewPanelOpen(): boolean | undefined {
const open = localStorage.getItem(ID_PREVIEW_PANEL_OPEN)
if (open === null) return undefined
- return JSON.parse(open)
+ return safeJsonParse(open)
}
export function getProjectPanelOpen(): boolean | undefined {
const open = localStorage.getItem(ID_PROJECT_PANEL_OPEN)
if (open === null) return undefined
- return JSON.parse(open)
+ return safeJsonParse(open)
}
export function getOpenProject() {
@@ -99,7 +100,8 @@ export namespace Store {
}
export function getGeneratorHistory(): string[] {
- return JSON.parse(localStorage.getItem(ID_GENERATOR_HISTORY) ?? '[]')
+ const value = localStorage.getItem(ID_GENERATOR_HISTORY) ?? '[]'
+ return safeJsonParse(value) ?? []
}
export function setLanguage(language: string | undefined) {
@@ -173,7 +175,8 @@ export namespace Store {
}
export function getWhatsNewSeen(): { id: string, time: string }[] {
- return JSON.parse(localStorage.getItem(ID_WHATS_NEW_SEEN) ?? '[]')
+ const value = localStorage.getItem(ID_WHATS_NEW_SEEN) ?? '[]'
+ return safeJsonParse(value) ?? []
}
export function seeWhatsNew(ids: string[]) {
diff --git a/src/app/Utils.ts b/src/app/Utils.ts
index 29757e85..fc3daefa 100644
--- a/src/app/Utils.ts
+++ b/src/app/Utils.ts
@@ -623,3 +623,11 @@ export function makeDescriptionId(prefix: string, id: Identifier | undefined) {
}
return `${prefix}.${id.namespace}.${id.path.replaceAll('/', '.')}`
}
+
+export function safeJsonParse(text: string): any {
+ try {
+ return JSON.parse(text)
+ } catch (e) {
+ return undefined
+ }
+}
diff --git a/src/app/components/generator/FileCreation.tsx b/src/app/components/generator/FileCreation.tsx
index bd349105..4afd93cf 100644
--- a/src/app/components/generator/FileCreation.tsx
+++ b/src/app/components/generator/FileCreation.tsx
@@ -2,6 +2,7 @@ import type { DocAndNode } from '@spyglassmc/core'
import { useState } from 'preact/hooks'
import { Analytics } from '../../Analytics.js'
import { useLocale, useProject } from '../../contexts/index.js'
+import { safeJsonParse } from '../../Utils.js'
import { Btn } from '../Btn.js'
import { TextInput } from '../forms/index.js'
import { Modal } from '../Modal.js'
@@ -29,8 +30,11 @@ export function FileCreation({ docAndNode, id, method, onClose }: Props) {
return
}
Analytics.saveProjectFile(id, projects.length, project.files.length, method as any)
- const data = JSON.parse(docAndNode.doc.getText())
- updateFile(id, undefined, { type: id, id: fileId, data })
+ const text = docAndNode.doc.getText()
+ const data = safeJsonParse(text)
+ if (data !== undefined) {
+ updateFile(id, undefined, { type: id, id: fileId, data })
+ }
onClose()
}
diff --git a/src/app/components/generator/PreviewPanel.tsx b/src/app/components/generator/PreviewPanel.tsx
index 9077f8f3..90196e28 100644
--- a/src/app/components/generator/PreviewPanel.tsx
+++ b/src/app/components/generator/PreviewPanel.tsx
@@ -2,6 +2,7 @@ import type { DocAndNode } from '@spyglassmc/core'
import { useDocAndNode } from '../../contexts/Spyglass.jsx'
import { useVersion } from '../../contexts/Version.jsx'
import { checkVersion } from '../../services/index.js'
+import { safeJsonParse } from '../../Utils.js'
import { BiomeSourcePreview, BlockStatePreview, DecoratorPreview, DensityFunctionPreview, LootTablePreview, ModelPreview, NoisePreview, NoiseSettingsPreview, RecipePreview, StructureSetPreview } from '../previews/index.js'
export const HasPreview = ['loot_table', 'recipe', 'dimension', 'worldgen/density_function', 'worldgen/noise', 'worldgen/noise_settings', 'worldgen/configured_feature', 'worldgen/placed_feature', 'worldgen/structure_set', 'block_definition', 'docAndNode']
@@ -27,7 +28,7 @@ export function PreviewPanel({ docAndNode: original, id, shown }: PreviewPanelPr
return
}
- if (id === 'dimension' && JSON.parse(docAndNode.doc.getText()).generator?.type?.endsWith('noise')) {
+ if (id === 'dimension' && safeJsonParse(docAndNode.doc.getText())?.generator?.type?.endsWith('noise')) {
return
}
diff --git a/src/app/components/generator/SchemaGenerator.tsx b/src/app/components/generator/SchemaGenerator.tsx
index 0a7a42fc..0d87c44a 100644
--- a/src/app/components/generator/SchemaGenerator.tsx
+++ b/src/app/components/generator/SchemaGenerator.tsx
@@ -9,7 +9,7 @@ import { AsyncCancel, useActiveTimeout, useAsync, useSearchParam } from '../../h
import type { VersionId } from '../../services/index.js'
import { checkVersion, fetchPreset, fetchRegistries, getSnippet, shareSnippet } from '../../services/index.js'
import { Store } from '../../Store.js'
-import { cleanUrl, genPath } from '../../Utils.js'
+import { cleanUrl, genPath, safeJsonParse } from '../../Utils.js'
import { Ad, Btn, BtnMenu, ErrorPanel, FileCreation, FileRenaming, Footer, HasPreview, Octicon, PreviewPanel, ProjectCreation, ProjectDeletion, ProjectPanel, SearchList, SourcePanel, TextInput, Tree, VersionSwitcher } from '../index.js'
export const SHARE_KEY = 'share'
@@ -102,8 +102,10 @@ export function SchemaGenerator({ gen, allowedVersions }: Props) {
setSharedSnippetId(undefined, true)
}
if (file) {
- const data = JSON.parse(doc.getText())
- updateFile(gen.id, file.id, { id: file.id, data })
+ const data = safeJsonParse(doc.getText())
+ if (data !== undefined) {
+ updateFile(gen.id, file.id, { id: file.id, data })
+ }
}
ignoreChange.current = false
setError(null)
@@ -211,7 +213,7 @@ export function SchemaGenerator({ gen, allowedVersions }: Props) {
setShareShown(true)
} else if (doc) {
setShareLoading(true)
- shareSnippet(gen.id, version, JSON.parse(doc.getText()), previewShown)
+ shareSnippet(gen.id, version, doc.getText(), previewShown)
.then(({ id, length, compressed, rate }) => {
Analytics.createSnippet(gen.id, id, version, length, compressed, rate)
const url = `${location.origin}/${gen.url}/?${SHARE_KEY}=${id}`
diff --git a/src/app/components/previews/BiomeSourcePreview.tsx b/src/app/components/previews/BiomeSourcePreview.tsx
index f03e06ea..6d38092b 100644
--- a/src/app/components/previews/BiomeSourcePreview.tsx
+++ b/src/app/components/previews/BiomeSourcePreview.tsx
@@ -5,7 +5,7 @@ import { getProjectData, useLocale, useProject, useStore, useVersion } from '../
import { useAsync } from '../../hooks/index.js'
import { checkVersion } from '../../services/Versions.js'
import { Store } from '../../Store.js'
-import { iterateWorld2D, randomSeed, stringToColor } from '../../Utils.js'
+import { iterateWorld2D, randomSeed, safeJsonParse, stringToColor } from '../../Utils.js'
import { Btn, BtnMenu, NumberInput } from '../index.js'
import type { ColormapType } from './Colormap.js'
import { getColormap } from './Colormap.js'
@@ -32,7 +32,7 @@ export const BiomeSourcePreview = ({ docAndNode, shown }: PreviewProps) => {
const [focused2, setFocused2] = useState([])
const text = docAndNode.doc.getText()
- const data = JSON.parse(text)
+ const data = safeJsonParse(text) ?? {}
const type: string = data?.generator?.biome_source?.type?.replace(/^minecraft:/, '') ?? ''
const hasRandomness = type === 'multi_noise' || type === 'the_end'
diff --git a/src/app/components/previews/BlockStatePreview.tsx b/src/app/components/previews/BlockStatePreview.tsx
index d10e92ff..45fe000a 100644
--- a/src/app/components/previews/BlockStatePreview.tsx
+++ b/src/app/components/previews/BlockStatePreview.tsx
@@ -5,6 +5,7 @@ import { useVersion } from '../../contexts/index.js'
import { useAsync } from '../../hooks/useAsync.js'
import { AsyncCancel } from '../../hooks/useAsyncFn.js'
import { getResources, ResourceWrapper } from '../../services/Resources.js'
+import { safeJsonParse } from '../../Utils.js'
import type { PreviewProps } from './index.js'
import { InteractiveCanvas3D } from './InteractiveCanvas3D.jsx'
@@ -18,7 +19,7 @@ export const BlockStatePreview = ({ docAndNode, shown }: PreviewProps) => {
const { value: resources } = useAsync(async () => {
if (!shown) return AsyncCancel
const resources = await getResources(version)
- const definition = BlockDefinition.fromJson(JSON.parse(text))
+ const definition = BlockDefinition.fromJson(safeJsonParse(text) ?? {})
const wrapper = new ResourceWrapper(resources, {
getBlockDefinition(id) {
if (id.equals(PREVIEW_ID)) return definition
diff --git a/src/app/components/previews/DecoratorPreview.tsx b/src/app/components/previews/DecoratorPreview.tsx
index 5ac9c333..f8a2d63d 100644
--- a/src/app/components/previews/DecoratorPreview.tsx
+++ b/src/app/components/previews/DecoratorPreview.tsx
@@ -2,7 +2,7 @@ import { BlockPos, ChunkPos, LegacyRandom, PerlinNoise } from 'deepslate'
import type { mat3 } from 'gl-matrix'
import { useCallback, useMemo, useRef, useState } from 'preact/hooks'
import { useLocale, useVersion } from '../../contexts/index.js'
-import { computeIfAbsent, iterateWorld2D, randomSeed } from '../../Utils.js'
+import { computeIfAbsent, iterateWorld2D, randomSeed, safeJsonParse } from '../../Utils.js'
import { Btn } from '../index.js'
import type { PlacedFeature, PlacementContext } from './Decorator.js'
import { decorateChunk } from './Decorator.js'
@@ -50,7 +50,7 @@ export const DecoratorPreview = ({ docAndNode, shown }: PreviewProps) => {
}, [])
const onDraw = useCallback(function onDraw(transform: mat3) {
if (!ctx.current || !imageData.current || !shown) return
- const data = JSON.parse(text)
+ const data = safeJsonParse(text) ?? {}
iterateWorld2D(imageData.current, transform, (x, y) => {
const pos = ChunkPos.create(Math.floor(x / 16), Math.floor(-y / 16))
const features = computeIfAbsent(chunkFeatures, `${pos[0]} ${pos[1]}`, () => decorateChunk(pos, data, context))
diff --git a/src/app/components/previews/DensityFunctionPreview.tsx b/src/app/components/previews/DensityFunctionPreview.tsx
index 3f470a0b..d6bcac04 100644
--- a/src/app/components/previews/DensityFunctionPreview.tsx
+++ b/src/app/components/previews/DensityFunctionPreview.tsx
@@ -6,7 +6,7 @@ import { getProjectData, useLocale, useProject, useVersion } from '../../context
import { useAsync } from '../../hooks/useAsync.js'
import { useLocalStorage } from '../../hooks/useLocalStorage.js'
import { Store } from '../../Store.js'
-import { iterateWorld2D, randomSeed } from '../../Utils.js'
+import { iterateWorld2D, randomSeed, safeJsonParse } from '../../Utils.js'
import { Btn, BtnMenu, NumberInput } from '../index.js'
import type { ColormapType } from './Colormap.js'
import { getColormap } from './Colormap.js'
@@ -33,7 +33,7 @@ export const DensityFunctionPreview = ({ docAndNode, shown }: PreviewProps) => {
const { value: df } = useAsync(async () => {
await DEEPSLATE.loadVersion(version, getProjectData(project))
- const df = DEEPSLATE.loadDensityFunction(JSON.parse(text), minY, height, seed)
+ const df = DEEPSLATE.loadDensityFunction(safeJsonParse(text) ?? {}, minY, height, seed)
return df
}, [version, project, minY, height, seed, text])
diff --git a/src/app/components/previews/LootTablePreview.tsx b/src/app/components/previews/LootTablePreview.tsx
index 43eff887..0d0d575d 100644
--- a/src/app/components/previews/LootTablePreview.tsx
+++ b/src/app/components/previews/LootTablePreview.tsx
@@ -3,7 +3,7 @@ import { useMemo, useRef, useState } from 'preact/hooks'
import { useLocale, useVersion } from '../../contexts/index.js'
import { useAsync } from '../../hooks/useAsync.js'
import { checkVersion, fetchAllPresets, fetchItemComponents } from '../../services/index.js'
-import { clamp, jsonToNbt, randomSeed } from '../../Utils.js'
+import { clamp, jsonToNbt, randomSeed, safeJsonParse } from '../../Utils.js'
import { Btn, BtnMenu, NumberInput } from '../index.js'
import { ItemDisplay } from '../ItemDisplay.jsx'
import { ItemDisplay1204 } from '../ItemDisplay1204.jsx'
@@ -40,10 +40,7 @@ export const LootTablePreview = ({ docAndNode }: PreviewProps) => {
return []
}
const [itemTags, lootTables, itemComponents, enchantments, enchantmentTags] = dependencies
- let table = {}
- try {
- table = JSON.parse(text)
- } catch (e) {}
+ const table = safeJsonParse(text) ?? {}
if (use1204) {
return generateLootTable1204(table, {
version, seed, luck, daytime, weather,
diff --git a/src/app/components/previews/ModelPreview.tsx b/src/app/components/previews/ModelPreview.tsx
index 1b958c73..3f9ca064 100644
--- a/src/app/components/previews/ModelPreview.tsx
+++ b/src/app/components/previews/ModelPreview.tsx
@@ -5,6 +5,7 @@ import { useVersion } from '../../contexts/index.js'
import { useAsync } from '../../hooks/useAsync.js'
import { AsyncCancel } from '../../hooks/useAsyncFn.js'
import { getResources, ResourceWrapper } from '../../services/Resources.js'
+import { safeJsonParse } from '../../Utils.js'
import type { PreviewProps } from './index.js'
import { InteractiveCanvas3D } from './InteractiveCanvas3D.jsx'
@@ -19,7 +20,7 @@ export const ModelPreview = ({ docAndNode, shown }: PreviewProps) => {
const { value: resources } = useAsync(async () => {
if (!shown) return AsyncCancel
const resources = await getResources(version)
- const blockModel = BlockModel.fromJson(JSON.parse(text))
+ const blockModel = BlockModel.fromJson(safeJsonParse(text) ?? {})
blockModel.flatten(resources)
const wrapper = new ResourceWrapper(resources, {
getBlockDefinition(id) {
diff --git a/src/app/components/previews/NoisePreview.tsx b/src/app/components/previews/NoisePreview.tsx
index a0c40322..cefc07f9 100644
--- a/src/app/components/previews/NoisePreview.tsx
+++ b/src/app/components/previews/NoisePreview.tsx
@@ -3,7 +3,7 @@ import type { mat3 } from 'gl-matrix'
import { useCallback, useMemo, useRef, useState } from 'preact/hooks'
import { useLocale } from '../../contexts/index.js'
import { Store } from '../../Store.js'
-import { iterateWorld2D, randomSeed } from '../../Utils.js'
+import { iterateWorld2D, randomSeed, safeJsonParse } from '../../Utils.js'
import { Btn } from '../index.js'
import type { ColormapType } from './Colormap.js'
import { getColormap } from './Colormap.js'
@@ -19,7 +19,7 @@ export const NoisePreview = ({ docAndNode, shown }: PreviewProps) => {
const noise = useMemo(() => {
const random = XoroshiroRandom.create(seed)
- const params = NoiseParameters.fromJson(JSON.parse(text))
+ const params = NoiseParameters.fromJson(safeJsonParse(text) ?? {})
return new NormalNoise(random, params)
}, [text, seed])
diff --git a/src/app/components/previews/NoiseSettingsPreview.tsx b/src/app/components/previews/NoiseSettingsPreview.tsx
index a1d4a289..ae6d5ff3 100644
--- a/src/app/components/previews/NoiseSettingsPreview.tsx
+++ b/src/app/components/previews/NoiseSettingsPreview.tsx
@@ -6,7 +6,7 @@ import { getProjectData, useLocale, useProject, useVersion } from '../../context
import { useAsync } from '../../hooks/index.js'
import { fetchRegistries } from '../../services/index.js'
import { Store } from '../../Store.js'
-import { iterateWorld2D, randomSeed } from '../../Utils.js'
+import { iterateWorld2D, randomSeed, safeJsonParse } from '../../Utils.js'
import { Btn, BtnInput, BtnMenu, ErrorPanel } from '../index.js'
import type { ColormapType } from './Colormap.js'
import { getColormap } from './Colormap.js'
@@ -26,7 +26,7 @@ export const NoiseSettingsPreview = ({ docAndNode, shown }: PreviewProps) => {
const text = docAndNode.doc.getText()
const { value, error } = useAsync(async () => {
- const data = JSON.parse(text)
+ const data = safeJsonParse(text) ?? {}
await DEEPSLATE.loadVersion(version, getProjectData(project))
const biomeSource = { type: 'fixed', biome }
await DEEPSLATE.loadChunkGenerator(data, biomeSource, seed)
diff --git a/src/app/components/previews/RecipePreview.tsx b/src/app/components/previews/RecipePreview.tsx
index 350b0f3a..b4c19b2e 100644
--- a/src/app/components/previews/RecipePreview.tsx
+++ b/src/app/components/previews/RecipePreview.tsx
@@ -4,7 +4,7 @@ import { useLocale, useVersion } from '../../contexts/index.js'
import { useAsync } from '../../hooks/useAsync.js'
import type { VersionId } from '../../services/index.js'
import { checkVersion, fetchAllPresets } from '../../services/index.js'
-import { jsonToNbt } from '../../Utils.js'
+import { jsonToNbt, safeJsonParse } from '../../Utils.js'
import { Btn, BtnMenu } from '../index.js'
import { ItemDisplay } from '../ItemDisplay.jsx'
import type { PreviewProps } from './index.js'
@@ -30,13 +30,13 @@ export const RecipePreview = ({ docAndNode }: PreviewProps) => {
}, [])
const text = docAndNode.doc.getText()
- const recipe = JSON.parse(text)
+ const recipe = safeJsonParse(text) ?? {}
const items = useMemo