feat(mystery): parseItem — markdown to typed Item

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-09 09:13:51 -05:00
parent cf257c040a
commit e108ca16e0
2 changed files with 74 additions and 3 deletions
+54 -1
View File
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest' import { describe, it, expect } from 'vitest'
import { parseRoom } from './loader' import { parseRoom, parseItem } from './loader'
const FOYER_MD = `--- const FOYER_MD = `---
id: foyer id: foyer
@@ -161,3 +161,56 @@ items: []
expect(() => parseRoom(md, 'rooms/r.md')).toThrow(/exitDRequires is set but exitDLockedText is missing/) expect(() => parseRoom(md, 'rooms/r.md')).toThrow(/exitDRequires is set but exitDLockedText is missing/)
}) })
}) })
// Add to the bottom of loader.test.ts
const LAMP_MD = `---
id: lamp
names: [lamp, oil lamp, torch]
short: an oil lamp
takeable: true
initialState:
lit: false
---
An iron oil lamp with a glass chimney. Currently unlit.
`
describe('parseItem', () => {
it('parses an item with state', () => {
const item = parseItem(LAMP_MD, 'items/lamp.md')
expect(item).toEqual({
id: 'lamp',
names: ['lamp', 'oil lamp', 'torch'],
short: 'an oil lamp',
long: 'An iron oil lamp with a glass chimney. Currently unlit.',
initialState: { lit: false },
takeable: true,
})
})
it('uses empty initialState when omitted', () => {
const md = `---
id: x
names: [x]
short: x
takeable: false
---
The long description.
`
const item = parseItem(md, 'items/x.md')
expect(item.initialState).toEqual({})
})
it('throws when body is empty', () => {
const md = `---
id: x
names: [x]
short: x
takeable: false
---
`
expect(() => parseItem(md, 'items/x.md')).toThrow(/empty long description/i)
})
})
+20 -2
View File
@@ -1,7 +1,7 @@
import matter from 'gray-matter' import matter from 'gray-matter'
import type { Room, RoomDescriptions } from './types' import type { Room, RoomDescriptions, Item } from './types'
import type { Direction } from '../engine/types' import type { Direction } from '../engine/types'
import { roomFrontmatterSchema } from './schema' import { roomFrontmatterSchema, itemFrontmatterSchema } from './schema'
const WIKILINK = /^\[\[([^\]|]+)(?:\|[^\]]*)?\]\]$/ const WIKILINK = /^\[\[([^\]|]+)(?:\|[^\]]*)?\]\]$/
@@ -97,3 +97,21 @@ export function parseRoom(raw: string, sourcePath: string): Room {
if (fm.safe) room.safe = fm.safe if (fm.safe) room.safe = fm.safe
return room return room
} }
export function parseItem(raw: string, sourcePath: string): Item {
const parsed = matter(raw)
const frontmatter = stripWikilink(parsed.data) as Record<string, unknown>
const fm = itemFrontmatterSchema.parse(frontmatter)
const long = parsed.content.trim()
if (long.length === 0) {
throw new Error(`${sourcePath}: empty long description`)
}
return {
id: fm.id,
names: fm.names,
short: fm.short,
long,
initialState: fm.initialState,
takeable: fm.takeable,
}
}