Skip to content

Commit 7cce705

Browse files
committed
Add scenario sanitization utilities and docs
1 parent 87592e5 commit 7cce705

5 files changed

Lines changed: 271 additions & 26 deletions

File tree

README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
- [動作確認手順](#動作確認手順)
1414
- [Quick Start(デモゲームを弄ってみよう)](#quick-startデモゲームを弄ってみよう)
1515
- [開発コマンド](#開発コマンド)
16+
- [シナリオ検証API](#シナリオ検証api)
1617
- [現在の状況](#現在の状況)
1718
- [ロードマップ](#ロードマップ実装予定)
1819
- [できること](#アルファ版01x-02xでできること)
@@ -185,6 +186,35 @@ npm run play
185186
- `npm run docs:build` - ドキュメントをビルド
186187
- `npm run docs:preview` - ビルドしたドキュメントをプレビュー
187188

189+
## シナリオ検証API
190+
191+
WebTaleKit には、シナリオ配列を検証しつつ、HTML風の文字列を非破壊でサニタイズできる公開APIがあります。
192+
193+
- `validateScenarioObjects` - 検証結果とサニタイズ済みシナリオを返します
194+
- `formatValidationOutput` - エラーと警告を表示用文字列へ整形します
195+
- `createScenarioValidationError` - 検証結果から Error を生成します
196+
- `assertScenarioValidation` - エラーがある場合に例外を送出します
197+
- `reportScenarioValidation` - logger 経由で警告とエラーを出力します
198+
199+
これらのAPIは、エンジン実行時に自動で強制適用されません。シナリオの読み込み時、エディタ連携時、独自ビルド処理時などに、利用者が必要に応じて呼び出す想定です。
200+
201+
```ts
202+
import {
203+
assertScenarioValidation,
204+
reportScenarioValidation,
205+
validateScenarioObjects,
206+
} from './src/utils/validateScenario'
207+
208+
const result = validateScenarioObjects(scenarioObjects, commandList)
209+
210+
await reportScenarioValidation(result, 'Scene import')
211+
assertScenarioValidation(result, 'Scene import')
212+
213+
const safeScenario = result.sanitizedScenario
214+
```
215+
216+
`sanitizedScenario` は元の入力配列を破壊せずに返されます。`sanitized``true` の場合は、HTML風の文字列がエスケープされています。
217+
188218
## 現在の状況
189219

190220
webTaleKitは、現在アルファ版です。

README_EN.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
- [Testing Instructions](#testing-instructions)
1414
- [Quick Start](#quick-start)
1515
- [Development Commands](#development-commands)
16+
- [Scenario Validation API](#scenario-validation-api)
1617
- [Current Status](#current-status)
1718
- [Roadmap](#roadmap)
1819
- [Features](#features-available-in-alpha-01x-02x)
@@ -185,6 +186,35 @@ Choices are important elements that let players control game progression. You ca
185186
- `npm run docs:build` - Build documentation
186187
- `npm run docs:preview` - Preview built documentation
187188
189+
## Scenario Validation API
190+
191+
webTaleKit provides a public API for validating scenario arrays and non-destructively sanitizing HTML-like string content.
192+
193+
- `validateScenarioObjects` returns validation results and a sanitized scenario
194+
- `formatValidationOutput` converts errors and warnings into display-friendly strings
195+
- `createScenarioValidationError` builds an `Error` from validation results
196+
- `assertScenarioValidation` throws when validation errors exist
197+
- `reportScenarioValidation` sends warnings and errors through the logger
198+
199+
These APIs are not automatically enforced by the engine at runtime. They are intended to be called explicitly by the user from scene import flows, editor integrations, custom build steps, or server-side tooling.
200+
201+
```ts
202+
import {
203+
assertScenarioValidation,
204+
reportScenarioValidation,
205+
validateScenarioObjects,
206+
} from './src/utils/validateScenario'
207+
208+
const result = validateScenarioObjects(scenarioObjects, commandList)
209+
210+
await reportScenarioValidation(result, 'Scene import')
211+
assertScenarioValidation(result, 'Scene import')
212+
213+
const safeScenario = result.sanitizedScenario
214+
```
215+
216+
`sanitizedScenario` is returned without mutating the original input array. When `sanitized` is `true`, HTML-like text has been escaped.
217+
188218
## Current Status
189219
190220
webTaleKit is currently in alpha.

documents/manual.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,5 +105,30 @@
105105

106106
4. `dist`ディレクトリの内容をWebサーバーにデプロイすることで、ゲームを公開できます。
107107

108+
## シナリオ検証とサニタイズ
109+
110+
WebTaleKit では、シナリオ配列に対して検証とサニタイズを行う公開APIを利用できます。
111+
112+
- `validateScenarioObjects``valid``errors``warnings` に加えて `sanitizedScenario` を返します
113+
- `sanitizedScenario` は非破壊で生成されるため、元のシナリオ配列は変更されません
114+
- `reportScenarioValidation` を使うと、既存 logger に合わせた警告・エラー出力ができます
115+
- `assertScenarioValidation` を使うと、エラーがある場合に呼び出し側で停止できます
116+
117+
この機能は公開APIであり、エンジン本体が自動で適用するわけではありません。ツール、エディタ、ビルド処理、サーバー側変換など、必要な場所で明示的に呼び出してください。
118+
119+
```ts
120+
import {
121+
reportScenarioValidation,
122+
validateScenarioObjects,
123+
} from '../src/utils/validateScenario'
124+
125+
const result = validateScenarioObjects(scenarioObjects, commandList)
126+
await reportScenarioValidation(result, 'Manual validation')
127+
128+
if (result.valid) {
129+
const scenarioForRuntime = result.sanitizedScenario
130+
}
131+
```
132+
108133
以上がWebTaleKitを使ってビジュアルノベルゲームを作成するための基本的な流れです。
109134
タグの詳細については、[WebTaleKit仕様](documents/spec.md)を参照してください。

src/utils/validateScenario.test.ts

Lines changed: 70 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import { validateScenarioObjects } from '../utils/validateScenario'
1+
import * as logger from '../utils/logger'
2+
import {
3+
assertScenarioValidation,
4+
createScenarioValidationError,
5+
formatValidationOutput,
6+
reportScenarioValidation,
7+
validateScenarioObjects,
8+
} from '../utils/validateScenario'
29

310
const mockCommandList: Record<string, Function> = {
411
text: () => {},
@@ -29,6 +36,8 @@ describe('validateScenarioObjects', () => {
2936
expect(result.valid).toBe(true)
3037
expect(result.errors).toHaveLength(0)
3138
expect(result.warnings).toHaveLength(0)
39+
expect(result.sanitizedScenario).toEqual(scenario)
40+
expect(result.sanitized).toBe(false)
3241
})
3342

3443
test('空配列で valid: false + error が返ること', () => {
@@ -80,9 +89,26 @@ describe('validateScenarioObjects', () => {
8089

8190
test('HTMLタグを含むテキストがエスケープされること', () => {
8291
const scenario = [{ type: 'text', content: ['<script>alert("xss")</script>'] }]
83-
validateScenarioObjects(scenario, mockCommandList)
84-
expect(scenario[0].content[0]).not.toContain('<script>')
85-
expect(scenario[0].content[0]).toContain('&lt;script&gt;')
92+
const result = validateScenarioObjects(scenario, mockCommandList)
93+
expect(scenario[0].content[0]).toContain('<script>')
94+
expect(result.sanitizedScenario[0].content[0]).not.toContain('<script>')
95+
expect(result.sanitizedScenario[0].content[0]).toContain('&lt;script&gt;')
96+
expect(result.sanitized).toBe(true)
97+
})
98+
99+
test('ネストしたオブジェクトも非破壊でサニタイズされること', () => {
100+
const scenario = [
101+
{
102+
type: 'choice',
103+
content: [{ type: 'item', label: '<b>危険</b>', content: ['<i>text</i>'] }],
104+
},
105+
]
106+
107+
const result = validateScenarioObjects(scenario, mockCommandList)
108+
109+
expect(scenario[0].content[0].label).toBe('<b>危険</b>')
110+
expect(result.sanitizedScenario[0].content[0].label).toBe('&lt;b&gt;危険&lt;/b&gt;')
111+
expect(result.sanitizedScenario[0].content[0].content[0]).toBe('&lt;i&gt;text&lt;/i&gt;')
86112
})
87113

88114
test('index が正常範囲内の jump で warning が出ないこと', () => {
@@ -94,4 +120,44 @@ describe('validateScenarioObjects', () => {
94120
const result = validateScenarioObjects(scenario, mockCommandList)
95121
expect(result.warnings.filter((w) => w.type === 'jump')).toHaveLength(0)
96122
})
123+
124+
test('formatValidationOutput が warnings と errors を整形すること', () => {
125+
const result = validateScenarioObjects([{ type: 'say', content: ['hello'] }], mockCommandList)
126+
const formatted = formatValidationOutput(result)
127+
128+
expect(formatted.errors).toHaveLength(0)
129+
expect(formatted.warnings[0]).toContain('[index:0]')
130+
expect(formatted.warnings[0]).toContain('<say>')
131+
})
132+
133+
test('createScenarioValidationError が error をまとめた Error を返すこと', () => {
134+
const result = validateScenarioObjects([{ type: 'choice', content: [] }], mockCommandList)
135+
const error = createScenarioValidationError(result, 'Custom context')
136+
137+
expect(error).toBeInstanceOf(Error)
138+
expect(error?.message).toContain('Custom context')
139+
expect(error?.message).toContain('choice コマンドに item が含まれていません')
140+
})
141+
142+
test('assertScenarioValidation が invalid 時に throw すること', () => {
143+
const result = validateScenarioObjects([{ type: 'choice', content: [] }], mockCommandList)
144+
expect(() => assertScenarioValidation(result)).toThrow(/Scenario validation failed/)
145+
})
146+
147+
test('reportScenarioValidation が warning と error を logger に流すこと', async () => {
148+
const outputLogSpy = jest.spyOn(logger, 'outputLog').mockResolvedValue()
149+
const logErrorSpy = jest.spyOn(logger, 'logError').mockResolvedValue()
150+
const result = validateScenarioObjects([
151+
{ type: 'say', content: ['hello'] },
152+
{ type: 'choice', content: [] },
153+
], mockCommandList)
154+
155+
await reportScenarioValidation(result, 'Validation test')
156+
157+
expect(outputLogSpy).toHaveBeenCalled()
158+
expect(logErrorSpy).toHaveBeenCalled()
159+
160+
outputLogSpy.mockRestore()
161+
logErrorSpy.mockRestore()
162+
})
97163
})

0 commit comments

Comments
 (0)