@363045841yyt/klinechart-core 0.7.3 → 0.7.5-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +201 -201
- package/README.zh-CN.md +201 -201
- package/dist/engine/renderers/webgl/candleSurface.js +47 -47
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -2
- package/dist/version.js.map +1 -1
- package/package.json +129 -122
- package/src/__tests__/signal.test.ts +124 -124
- package/src/config/chartSettings.ts +66 -66
- package/src/controllers/__tests__/drawing.test.ts +214 -214
- package/src/controllers/__tests__/indicatorSelector.test.ts +481 -481
- package/src/controllers/__tests__/toolbar.test.ts +225 -225
- package/src/controllers/createChartController.ts +665 -665
- package/src/controllers/createDrawingController.ts +96 -96
- package/src/controllers/createIndicatorSelectorController.ts +307 -307
- package/src/controllers/createToolbarController.ts +146 -146
- package/src/controllers/index.ts +19 -19
- package/src/controllers/types.ts +284 -284
- package/src/engine/__tests__/chart.dpr.test.ts +401 -401
- package/src/engine/__tests__/paneRenderer.resize.test.ts +92 -92
- package/src/engine/chart-store.ts +121 -121
- package/src/engine/chart.d.ts +617 -617
- package/src/engine/chart.ts +2815 -2815
- package/src/engine/controller/__tests__/interaction.dpr.test.ts +259 -259
- package/src/engine/controller/interaction.ts +722 -722
- package/src/engine/controller/markerInteraction.ts +130 -130
- package/src/engine/controller/pinchTracker.ts +82 -82
- package/src/engine/controller/tooltipPosition.ts +48 -48
- package/src/engine/draw/__tests__/pixelAlign.spec.ts +176 -176
- package/src/engine/draw/pixelAlign.ts +259 -259
- package/src/engine/drawing/index.ts +655 -655
- package/src/engine/drawing/interaction.ts +842 -842
- package/src/engine/drawing/plugin.ts +343 -343
- package/src/engine/indicators/__tests__/__fixtures__/golden/atr.json +38 -38
- package/src/engine/indicators/__tests__/__fixtures__/golden/dema.json +14 -14
- package/src/engine/indicators/__tests__/__fixtures__/golden/hma.json +14 -14
- package/src/engine/indicators/__tests__/__fixtures__/golden/index.ts +55 -55
- package/src/engine/indicators/__tests__/__fixtures__/golden/kama.json +14 -14
- package/src/engine/indicators/__tests__/__fixtures__/golden/tema.json +14 -14
- package/src/engine/indicators/__tests__/__fixtures__/golden/wma.json +40 -40
- package/src/engine/indicators/__tests__/__fixtures__/synthetic.ts +65 -65
- package/src/engine/indicators/__tests__/_propertyAssertions.ts +76 -76
- package/src/engine/indicators/__tests__/atr.test.ts +153 -153
- package/src/engine/indicators/__tests__/calculators.test.ts +614 -614
- package/src/engine/indicators/__tests__/cmf-mfi.test.ts +100 -100
- package/src/engine/indicators/__tests__/dema.test.ts +73 -73
- package/src/engine/indicators/__tests__/donchian.test.ts +70 -70
- package/src/engine/indicators/__tests__/hma.test.ts +73 -73
- package/src/engine/indicators/__tests__/ichimoku.test.ts +105 -105
- package/src/engine/indicators/__tests__/kama.test.ts +80 -80
- package/src/engine/indicators/__tests__/keltner.test.ts +65 -65
- package/src/engine/indicators/__tests__/pivot-fib.test.ts +110 -110
- package/src/engine/indicators/__tests__/roc.test.ts +68 -68
- package/src/engine/indicators/__tests__/sar.test.ts +86 -86
- package/src/engine/indicators/__tests__/scheduler.test.ts +831 -831
- package/src/engine/indicators/__tests__/soa.test.ts +533 -533
- package/src/engine/indicators/__tests__/structure.test.ts +110 -110
- package/src/engine/indicators/__tests__/supertrend.test.ts +65 -65
- package/src/engine/indicators/__tests__/tema.test.ts +68 -68
- package/src/engine/indicators/__tests__/trix.test.ts +70 -70
- package/src/engine/indicators/__tests__/volatility.test.ts +117 -117
- package/src/engine/indicators/__tests__/volume.test.ts +115 -115
- package/src/engine/indicators/__tests__/volumeProfile.test.ts +74 -74
- package/src/engine/indicators/__tests__/vwap.test.ts +69 -69
- package/src/engine/indicators/__tests__/wma.test.ts +112 -112
- package/src/engine/indicators/__tests__/zones.test.ts +95 -95
- package/src/engine/indicators/atrState.ts +27 -27
- package/src/engine/indicators/bollState.ts +51 -51
- package/src/engine/indicators/calculators.ts +2593 -2593
- package/src/engine/indicators/cciState.ts +25 -25
- package/src/engine/indicators/chaikinVolState.ts +32 -32
- package/src/engine/indicators/cmfState.ts +27 -27
- package/src/engine/indicators/demaState.ts +27 -27
- package/src/engine/indicators/donchianState.ts +43 -43
- package/src/engine/indicators/eneState.ts +43 -43
- package/src/engine/indicators/expmaState.ts +43 -43
- package/src/engine/indicators/fastkState.ts +25 -25
- package/src/engine/indicators/fibState.ts +41 -41
- package/src/engine/indicators/hmaState.ts +27 -27
- package/src/engine/indicators/hvState.ts +28 -28
- package/src/engine/indicators/ichimokuState.ts +70 -70
- package/src/engine/indicators/indicator.worker.ts +169 -169
- package/src/engine/indicators/indicatorDefinitionRegistry.ts +62 -62
- package/src/engine/indicators/indicatorMetadata.ts +110 -110
- package/src/engine/indicators/indicatorRegistry.ts +106 -106
- package/src/engine/indicators/indicatorRuntime.ts +1548 -1548
- package/src/engine/indicators/kamaState.ts +34 -34
- package/src/engine/indicators/keltnerState.ts +49 -49
- package/src/engine/indicators/kstState.ts +42 -42
- package/src/engine/indicators/maState.ts +36 -36
- package/src/engine/indicators/macdState.ts +76 -76
- package/src/engine/indicators/mfiState.ts +27 -27
- package/src/engine/indicators/momState.ts +25 -25
- package/src/engine/indicators/obvState.ts +25 -25
- package/src/engine/indicators/parkinsonState.ts +28 -28
- package/src/engine/indicators/pivotState.ts +51 -51
- package/src/engine/indicators/pvtState.ts +25 -25
- package/src/engine/indicators/rocState.ts +27 -27
- package/src/engine/indicators/rsiState.ts +65 -65
- package/src/engine/indicators/sarState.ts +41 -41
- package/src/engine/indicators/scheduler.ts +1205 -1205
- package/src/engine/indicators/soa.ts +352 -352
- package/src/engine/indicators/stateComposer.ts +1262 -1262
- package/src/engine/indicators/stochState.ts +26 -26
- package/src/engine/indicators/structureState.ts +69 -69
- package/src/engine/indicators/supertrendState.ts +37 -37
- package/src/engine/indicators/temaState.ts +27 -27
- package/src/engine/indicators/trixState.ts +35 -35
- package/src/engine/indicators/vmaState.ts +27 -27
- package/src/engine/indicators/volumeProfileState.ts +63 -63
- package/src/engine/indicators/vwapState.ts +29 -29
- package/src/engine/indicators/wmaState.ts +27 -27
- package/src/engine/indicators/wmsrState.ts +25 -25
- package/src/engine/indicators/workerProtocol.ts +613 -613
- package/src/engine/indicators/zonesState.ts +47 -47
- package/src/engine/layout/pane.ts +161 -161
- package/src/engine/marker/registry.ts +265 -265
- package/src/engine/paneRenderer.ts +169 -169
- package/src/engine/renderers/Indicator/atr.ts +237 -237
- package/src/engine/renderers/Indicator/boll.ts +317 -317
- package/src/engine/renderers/Indicator/cci.ts +275 -275
- package/src/engine/renderers/Indicator/chaikinVol.ts +138 -138
- package/src/engine/renderers/Indicator/cmf.ts +137 -137
- package/src/engine/renderers/Indicator/dema.ts +136 -136
- package/src/engine/renderers/Indicator/donchian.ts +137 -137
- package/src/engine/renderers/Indicator/ene.ts +271 -271
- package/src/engine/renderers/Indicator/expma.ts +197 -197
- package/src/engine/renderers/Indicator/fastk.ts +316 -316
- package/src/engine/renderers/Indicator/fib.ts +141 -141
- package/src/engine/renderers/Indicator/hma.ts +136 -136
- package/src/engine/renderers/Indicator/hv.ts +124 -124
- package/src/engine/renderers/Indicator/ichimoku.ts +181 -181
- package/src/engine/renderers/Indicator/index.ts +241 -241
- package/src/engine/renderers/Indicator/indicatorData.ts +650 -650
- package/src/engine/renderers/Indicator/kama.ts +136 -136
- package/src/engine/renderers/Indicator/keltner.ts +137 -137
- package/src/engine/renderers/Indicator/kst.ts +302 -302
- package/src/engine/renderers/Indicator/ma.ts +200 -200
- package/src/engine/renderers/Indicator/macd.ts +477 -477
- package/src/engine/renderers/Indicator/macdLegend.ts +141 -141
- package/src/engine/renderers/Indicator/mainIndicatorLegend.ts +272 -272
- package/src/engine/renderers/Indicator/mfi.ts +142 -142
- package/src/engine/renderers/Indicator/mom.ts +311 -311
- package/src/engine/renderers/Indicator/obv.ts +123 -123
- package/src/engine/renderers/Indicator/parkinson.ts +124 -124
- package/src/engine/renderers/Indicator/pivot.ts +131 -131
- package/src/engine/renderers/Indicator/pvt.ts +123 -123
- package/src/engine/renderers/Indicator/roc.ts +143 -143
- package/src/engine/renderers/Indicator/rsi.ts +390 -390
- package/src/engine/renderers/Indicator/sar.ts +113 -113
- package/src/engine/renderers/Indicator/scale/atr_scale.ts +19 -19
- package/src/engine/renderers/Indicator/scale/cci_scale.ts +19 -19
- package/src/engine/renderers/Indicator/scale/fastk_scale.ts +19 -19
- package/src/engine/renderers/Indicator/scale/indicator_scale.ts +204 -204
- package/src/engine/renderers/Indicator/scale/kst_scale.ts +19 -19
- package/src/engine/renderers/Indicator/scale/macd_scale.ts +22 -22
- package/src/engine/renderers/Indicator/scale/mom_scale.ts +19 -19
- package/src/engine/renderers/Indicator/scale/rsi_scale.ts +19 -19
- package/src/engine/renderers/Indicator/scale/stoch_scale.ts +19 -19
- package/src/engine/renderers/Indicator/scale/volume_scale.ts +26 -26
- package/src/engine/renderers/Indicator/scale/wmsr_scale.ts +19 -19
- package/src/engine/renderers/Indicator/stoch.ts +359 -359
- package/src/engine/renderers/Indicator/structure.ts +126 -126
- package/src/engine/renderers/Indicator/subPaneConfig.ts +265 -265
- package/src/engine/renderers/Indicator/supertrend.ts +115 -115
- package/src/engine/renderers/Indicator/tema.ts +136 -136
- package/src/engine/renderers/Indicator/trix.ts +158 -158
- package/src/engine/renderers/Indicator/vma.ts +124 -124
- package/src/engine/renderers/Indicator/volumeProfile.ts +125 -125
- package/src/engine/renderers/Indicator/vwap.ts +123 -123
- package/src/engine/renderers/Indicator/wma.ts +136 -136
- package/src/engine/renderers/Indicator/wmsr.ts +328 -328
- package/src/engine/renderers/Indicator/zones.ts +104 -104
- package/src/engine/renderers/__tests__/boll.renderer.test.ts +314 -314
- package/src/engine/renderers/__tests__/ene.renderer.test.ts +305 -305
- package/src/engine/renderers/__tests__/expma.renderer.test.ts +279 -279
- package/src/engine/renderers/__tests__/ma.renderer.test.ts +426 -426
- package/src/engine/renderers/__tests__/mainIndicatorLegend.renderer.test.ts +502 -502
- package/src/engine/renderers/__tests__/yAxis.renderer.test.ts +173 -173
- package/src/engine/renderers/candle.ts +459 -459
- package/src/engine/renderers/crosshair.ts +69 -69
- package/src/engine/renderers/customMarkers.ts +162 -162
- package/src/engine/renderers/extremaMarkers.ts +246 -246
- package/src/engine/renderers/gridLines.ts +90 -90
- package/src/engine/renderers/lastPrice.ts +97 -97
- package/src/engine/renderers/paneTitle.ts +136 -136
- package/src/engine/renderers/subVolume.ts +236 -236
- package/src/engine/renderers/timeAxis.ts +121 -121
- package/src/engine/renderers/webgl/candleSurface.ts +955 -955
- package/src/engine/renderers/webgl/sharedWebGLSurface.ts +146 -146
- package/src/engine/renderers/yAxis.ts +105 -105
- package/src/engine/scale/__tests__/logFormula.spec.ts +148 -148
- package/src/engine/scale/logFormula.ts +130 -130
- package/src/engine/scale/price.ts +39 -39
- package/src/engine/scale/priceScale.ts +264 -264
- package/src/engine/subPaneManager.ts +427 -427
- package/src/engine/theme/colors.ts +642 -642
- package/src/engine/theme/fonts.ts +20 -20
- package/src/engine/utils/klineConfig.ts +49 -49
- package/src/engine/utils/tickCount.ts +11 -11
- package/src/engine/utils/tickPosition.ts +214 -214
- package/src/engine/utils/zoom.ts +83 -83
- package/src/engine/viewport/viewport.ts +67 -67
- package/src/index.ts +3 -3
- package/src/plugin/ConfigManager.ts +93 -93
- package/src/plugin/EventBus.ts +77 -77
- package/src/plugin/HookSystem.ts +106 -106
- package/src/plugin/PluginHost.ts +243 -243
- package/src/plugin/PluginRegistry.ts +92 -92
- package/src/plugin/StateStore.ts +73 -73
- package/src/plugin/index.ts +19 -19
- package/src/plugin/rendererPluginManager.ts +368 -368
- package/src/plugin/stateKeys.ts +8 -8
- package/src/plugin/types.ts +526 -526
- package/src/reactivity/index.ts +2 -2
- package/src/reactivity/signal.ts +119 -119
- package/src/semantic/controller.ts +251 -251
- package/src/semantic/drawShape.ts +260 -260
- package/src/semantic/index.ts +28 -28
- package/src/semantic/schema.json +256 -256
- package/src/semantic/types.ts +251 -251
- package/src/semantic/validator.ts +349 -349
- package/src/types/kLine.ts +13 -13
- package/src/types/price.ts +56 -56
- package/src/types/volumePrice.ts +33 -33
- package/src/utils/dateFormat.ts +208 -208
- package/src/utils/kLineDraw/axis.ts +562 -562
- package/src/utils/priceToY.ts +34 -34
- package/src/utils/volumePrice.ts +202 -202
- package/src/version.ts +1 -1
|
@@ -1,349 +1,349 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 语义化配置校验器
|
|
3
|
-
* 包含 JSON Schema 校验、安全校验、业务逻辑校验
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { SemanticChartConfig, DataConfig, ValidationResult, SecurityResult, MarkerStyle } from './types'
|
|
7
|
-
|
|
8
|
-
// ============ 常量定义 ============
|
|
9
|
-
|
|
10
|
-
/** 禁止的属性键(防止原型污染) */
|
|
11
|
-
const FORBIDDEN_KEYS = ['__proto__', 'constructor', 'prototype']
|
|
12
|
-
|
|
13
|
-
/** 颜色值正则(严格校验) */
|
|
14
|
-
const COLOR_PATTERN =
|
|
15
|
-
/^(#[0-9a-fA-F]{3,8}|rgb\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}\s*\)|rgba\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*[\d.]+\s*\))$/
|
|
16
|
-
|
|
17
|
-
/** 股票代码正则规则
|
|
18
|
-
* 注意:北交所规则持续扩充,以交易所官方公告为准
|
|
19
|
-
* 规则版本日期:2025-01
|
|
20
|
-
*/
|
|
21
|
-
const SYMBOL_PATTERNS = {
|
|
22
|
-
SH: /^(600|601|603|605|688)\d{3}$/, // 上交所
|
|
23
|
-
SZ: /^(000|001|002|003|300|301)\d{3}$/, // 深交所
|
|
24
|
-
BJ: /^(83|87|43|82)\d{4}$/, // 北交所
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** 安全限制 */
|
|
28
|
-
interface SecurityLimits {
|
|
29
|
-
maxJsonSize: number
|
|
30
|
-
maxIndicators: number
|
|
31
|
-
maxCustomMarkers: number
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** 根据 period 计算最大日期范围(天数) */
|
|
35
|
-
function getMaxDateRangeDays(period: DataConfig['period']): number {
|
|
36
|
-
const LIMITS: Record<string, number> = {
|
|
37
|
-
'5min': 30,
|
|
38
|
-
'15min': 60,
|
|
39
|
-
'30min': 90,
|
|
40
|
-
'60min': 180,
|
|
41
|
-
daily: 365 * 3,
|
|
42
|
-
weekly: 365 * 5,
|
|
43
|
-
monthly: 365 * 10,
|
|
44
|
-
}
|
|
45
|
-
return LIMITS[period] ?? 365
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// ============ 单例 Canvas(颜色校验用) ============
|
|
49
|
-
|
|
50
|
-
const _colorCanvas = typeof document !== 'undefined' ? document.createElement('canvas') : null
|
|
51
|
-
const _colorCtx = _colorCanvas?.getContext('2d') ?? null
|
|
52
|
-
|
|
53
|
-
// ============ 校验器类 ============
|
|
54
|
-
|
|
55
|
-
export class SemanticConfigValidator {
|
|
56
|
-
private limits: SecurityLimits
|
|
57
|
-
private _ajv: Promise<any> | null = null
|
|
58
|
-
|
|
59
|
-
constructor() {
|
|
60
|
-
this.limits = {
|
|
61
|
-
maxJsonSize: 64 * 1024, // 64KB
|
|
62
|
-
maxIndicators: 10,
|
|
63
|
-
maxCustomMarkers: 100,
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
private async getAjv(): Promise<any> {
|
|
68
|
-
if (!this._ajv) {
|
|
69
|
-
this._ajv = (async () => {
|
|
70
|
-
const Ajv = (await import('ajv')).default
|
|
71
|
-
const ajv = new Ajv({ allErrors: true, allowUnionTypes: true })
|
|
72
|
-
ajv.addFormat('date', {
|
|
73
|
-
type: 'string',
|
|
74
|
-
validate: (data: string) => /^\d{4}-\d{2}-\d{2}$/.test(data),
|
|
75
|
-
})
|
|
76
|
-
const schemaModule = await import('./schema.json')
|
|
77
|
-
ajv.addSchema(schemaModule.default || schemaModule)
|
|
78
|
-
return ajv
|
|
79
|
-
})()
|
|
80
|
-
}
|
|
81
|
-
return this._ajv
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* 入口校验(在 JSON.parse 之前调用)
|
|
86
|
-
* 检查原始字符串大小
|
|
87
|
-
*/
|
|
88
|
-
validateRawInput(raw: string): ValidationResult {
|
|
89
|
-
const byteLength =
|
|
90
|
-
typeof TextEncoder !== 'undefined'
|
|
91
|
-
? new TextEncoder().encode(raw).byteLength
|
|
92
|
-
: raw.length * 3 // SSR 降级:保守估计
|
|
93
|
-
|
|
94
|
-
if (byteLength > this.limits.maxJsonSize) {
|
|
95
|
-
return { valid: false, errors: ['JSON payload too large (max 64KB)'] }
|
|
96
|
-
}
|
|
97
|
-
return { valid: true }
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* JSON Schema 校验
|
|
102
|
-
*/
|
|
103
|
-
async validate(config: unknown): Promise<ValidationResult> {
|
|
104
|
-
// 1. 类型检查
|
|
105
|
-
if (!config || typeof config !== 'object') {
|
|
106
|
-
return { valid: false, errors: ['Config must be an object'] }
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// 2. 原型污染检查
|
|
110
|
-
const protoCheck = this.checkPrototypePollution(config)
|
|
111
|
-
if (!protoCheck.valid) {
|
|
112
|
-
return protoCheck
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// 3. JSON Schema 校验
|
|
116
|
-
try {
|
|
117
|
-
const ajv = await this.getAjv()
|
|
118
|
-
const valid = ajv.validate('https://kmap.dev/schemas/semantic-chart-config/1.0.0', config)
|
|
119
|
-
if (!valid) {
|
|
120
|
-
const errors = ajv.errors?.map((e: { instancePath: string; message?: string }) => `${e.instancePath} ${e.message}`) || ['Schema validation failed']
|
|
121
|
-
return { valid: false, errors }
|
|
122
|
-
}
|
|
123
|
-
} catch {
|
|
124
|
-
return { valid: false, errors: ['Schema validation unavailable (ajv not loaded)'] }
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return { valid: true }
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* 安全校验(纯同步)
|
|
132
|
-
*/
|
|
133
|
-
securityCheck(config: SemanticChartConfig): SecurityResult {
|
|
134
|
-
const violations: string[] = []
|
|
135
|
-
|
|
136
|
-
// 1. 检查日期范围限制
|
|
137
|
-
const dateCheck = this.checkDateRange(config.data)
|
|
138
|
-
if (!dateCheck.valid) {
|
|
139
|
-
violations.push(...(dateCheck.errors || []))
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// 2. 检查股票代码格式
|
|
143
|
-
const symbolCheck = this.checkSymbol(config.data)
|
|
144
|
-
if (!symbolCheck.valid) {
|
|
145
|
-
violations.push(...(symbolCheck.errors || []))
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// 3. 检查指标数量限制
|
|
149
|
-
if (config.indicators) {
|
|
150
|
-
const mainCount = config.indicators.main?.length || 0
|
|
151
|
-
const subCount = config.indicators.sub?.length || 0
|
|
152
|
-
if (mainCount + subCount > this.limits.maxIndicators) {
|
|
153
|
-
violations.push(`Too many indicators (max ${this.limits.maxIndicators})`)
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// 4. 检查标记数量限制
|
|
158
|
-
const markerCount = config.markers?.customMarkers?.length || 0
|
|
159
|
-
if (markerCount > this.limits.maxCustomMarkers) {
|
|
160
|
-
violations.push(`Too many custom markers (max ${this.limits.maxCustomMarkers})`)
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// 5. 检查颜色值格式
|
|
164
|
-
if (config.markers?.customMarkers) {
|
|
165
|
-
for (const marker of config.markers.customMarkers) {
|
|
166
|
-
if (marker.style) {
|
|
167
|
-
const colorCheck = this.checkMarkerStyle(marker.style, marker.id)
|
|
168
|
-
violations.push(...colorCheck)
|
|
169
|
-
}
|
|
170
|
-
// 检查日期格式
|
|
171
|
-
const dateCheck = this.checkMarkerDate(marker.date, marker.id)
|
|
172
|
-
violations.push(...dateCheck)
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return {
|
|
177
|
-
passed: violations.length === 0,
|
|
178
|
-
violations: violations.length > 0 ? violations : undefined,
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// ============ 私有方法 ============
|
|
183
|
-
|
|
184
|
-
private checkPrototypePollution(obj: unknown, path = ''): ValidationResult {
|
|
185
|
-
if (!obj || typeof obj !== 'object') {
|
|
186
|
-
return { valid: true }
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
const errors: string[] = []
|
|
190
|
-
for (const key of Object.keys(obj as Record<string, unknown>)) {
|
|
191
|
-
if (FORBIDDEN_KEYS.includes(key)) {
|
|
192
|
-
errors.push(`Forbidden key "${key}" found at ${path}`)
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
return errors.length > 0 ? { valid: false, errors } : { valid: true }
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
private checkDateRange(data: DataConfig): ValidationResult {
|
|
200
|
-
const errors: string[] = []
|
|
201
|
-
|
|
202
|
-
const startDate = new Date(data.startDate)
|
|
203
|
-
const endDate = new Date(data.endDate)
|
|
204
|
-
|
|
205
|
-
if (isNaN(startDate.getTime())) {
|
|
206
|
-
errors.push(`Invalid startDate: ${data.startDate}`)
|
|
207
|
-
}
|
|
208
|
-
if (isNaN(endDate.getTime())) {
|
|
209
|
-
errors.push(`Invalid endDate: ${data.endDate}`)
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
if (errors.length > 0) {
|
|
213
|
-
return { valid: false, errors }
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
const maxDays = getMaxDateRangeDays(data.period)
|
|
217
|
-
const diffDays = Math.ceil((endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24))
|
|
218
|
-
|
|
219
|
-
if (diffDays > maxDays) {
|
|
220
|
-
errors.push(`Date range exceeds maximum for period "${data.period}" (max ${maxDays} days, got ${diffDays})`)
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
if (diffDays < 0) {
|
|
224
|
-
errors.push('endDate must be after startDate')
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
return errors.length > 0 ? { valid: false, errors } : { valid: true }
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
private checkSymbol(data: DataConfig): ValidationResult {
|
|
231
|
-
const { symbol, exchange } = data
|
|
232
|
-
|
|
233
|
-
if (exchange) {
|
|
234
|
-
const pattern = SYMBOL_PATTERNS[exchange]
|
|
235
|
-
if (!pattern.test(symbol)) {
|
|
236
|
-
return { valid: false, errors: [`Invalid symbol "${symbol}" for exchange "${exchange}"`] }
|
|
237
|
-
}
|
|
238
|
-
} else {
|
|
239
|
-
// 自动识别
|
|
240
|
-
const valid = Object.values(SYMBOL_PATTERNS).some((p) => p.test(symbol))
|
|
241
|
-
if (!valid) {
|
|
242
|
-
return { valid: false, errors: [`Invalid symbol format: "${symbol}"`] }
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
return { valid: true }
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
private checkMarkerStyle(style: MarkerStyle, markerId: string): string[] {
|
|
250
|
-
const errors: string[] = []
|
|
251
|
-
const colorFields = ['fillColor', 'strokeColor', 'textColor'] as const
|
|
252
|
-
|
|
253
|
-
for (const field of colorFields) {
|
|
254
|
-
const value = style[field]
|
|
255
|
-
if (typeof value === 'string' && !COLOR_PATTERN.test(value)) {
|
|
256
|
-
errors.push(`Invalid ${field} in marker "${markerId}": ${value}`)
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// 检查颜色值安全性(浏览器环境)
|
|
261
|
-
if (_colorCtx) {
|
|
262
|
-
for (const field of colorFields) {
|
|
263
|
-
const value = style[field]
|
|
264
|
-
if (typeof value === 'string') {
|
|
265
|
-
_colorCtx.fillStyle = '#000000'
|
|
266
|
-
_colorCtx.fillStyle = value
|
|
267
|
-
// 浏览器会过滤非法值,如果返回的不是合法颜色说明有问题
|
|
268
|
-
if (_colorCtx.fillStyle === '#000000' && value !== '#000000' && !value.startsWith('#000')) {
|
|
269
|
-
errors.push(`Potentially unsafe ${field} in marker "${markerId}": ${value}`)
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
return errors
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/** 日期格式正则:YYYY-MM-DD 或 YYYY-MM-DD HH:mm */
|
|
279
|
-
private static readonly DATE_PATTERN = /^\d{4}-\d{2}-\d{2}( \d{2}:\d{2})?$/
|
|
280
|
-
|
|
281
|
-
private checkMarkerDate(date: string, markerId: string): string[] {
|
|
282
|
-
const errors: string[] = []
|
|
283
|
-
|
|
284
|
-
if (!SemanticConfigValidator.DATE_PATTERN.test(date)) {
|
|
285
|
-
errors.push(`Invalid date format in marker "${markerId}": ${date} (expected YYYY-MM-DD or YYYY-MM-DD HH:mm)`)
|
|
286
|
-
return errors
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// 验证日期是否有效
|
|
290
|
-
const hasTime = date.includes(' ')
|
|
291
|
-
let parsed: Date
|
|
292
|
-
|
|
293
|
-
if (hasTime) {
|
|
294
|
-
parsed = new Date(date)
|
|
295
|
-
} else {
|
|
296
|
-
// 对于纯日期,解析为 UTC
|
|
297
|
-
const [year, month, day] = date.split('-').map(Number)
|
|
298
|
-
parsed = new Date(Date.UTC(year!, month! - 1, day!))
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
if (isNaN(parsed.getTime())) {
|
|
302
|
-
errors.push(`Invalid date value in marker "${markerId}": ${date}`)
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
return errors
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// ============ 工具函数导出 ============
|
|
310
|
-
|
|
311
|
-
/**
|
|
312
|
-
* 净化参数对象(防止原型污染)
|
|
313
|
-
*/
|
|
314
|
-
export function sanitizeParams(params: Record<string, unknown>): Record<string, unknown> {
|
|
315
|
-
const safe: Record<string, unknown> = Object.create(null)
|
|
316
|
-
for (const [key, value] of Object.entries(params)) {
|
|
317
|
-
if (!FORBIDDEN_KEYS.includes(key)) {
|
|
318
|
-
safe[key] = value
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
return safe
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
/**
|
|
325
|
-
* 净化颜色值(浏览器环境)
|
|
326
|
-
*/
|
|
327
|
-
export function sanitizeColor(input: string): string | null {
|
|
328
|
-
if (!_colorCtx) return null
|
|
329
|
-
_colorCtx.fillStyle = '#000000'
|
|
330
|
-
_colorCtx.fillStyle = input
|
|
331
|
-
return _colorCtx.fillStyle
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
/**
|
|
335
|
-
* 校验颜色值格式
|
|
336
|
-
*/
|
|
337
|
-
export function validateColor(color: string): boolean {
|
|
338
|
-
return COLOR_PATTERN.test(color)
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
/**
|
|
342
|
-
* 校验股票代码
|
|
343
|
-
*/
|
|
344
|
-
export function validateSymbol(symbol: string, exchange?: 'SH' | 'SZ' | 'BJ'): boolean {
|
|
345
|
-
if (exchange) {
|
|
346
|
-
return SYMBOL_PATTERNS[exchange].test(symbol)
|
|
347
|
-
}
|
|
348
|
-
return Object.values(SYMBOL_PATTERNS).some((p) => p.test(symbol))
|
|
349
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* 语义化配置校验器
|
|
3
|
+
* 包含 JSON Schema 校验、安全校验、业务逻辑校验
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { SemanticChartConfig, DataConfig, ValidationResult, SecurityResult, MarkerStyle } from './types'
|
|
7
|
+
|
|
8
|
+
// ============ 常量定义 ============
|
|
9
|
+
|
|
10
|
+
/** 禁止的属性键(防止原型污染) */
|
|
11
|
+
const FORBIDDEN_KEYS = ['__proto__', 'constructor', 'prototype']
|
|
12
|
+
|
|
13
|
+
/** 颜色值正则(严格校验) */
|
|
14
|
+
const COLOR_PATTERN =
|
|
15
|
+
/^(#[0-9a-fA-F]{3,8}|rgb\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}\s*\)|rgba\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*[\d.]+\s*\))$/
|
|
16
|
+
|
|
17
|
+
/** 股票代码正则规则
|
|
18
|
+
* 注意:北交所规则持续扩充,以交易所官方公告为准
|
|
19
|
+
* 规则版本日期:2025-01
|
|
20
|
+
*/
|
|
21
|
+
const SYMBOL_PATTERNS = {
|
|
22
|
+
SH: /^(600|601|603|605|688)\d{3}$/, // 上交所
|
|
23
|
+
SZ: /^(000|001|002|003|300|301)\d{3}$/, // 深交所
|
|
24
|
+
BJ: /^(83|87|43|82)\d{4}$/, // 北交所
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** 安全限制 */
|
|
28
|
+
interface SecurityLimits {
|
|
29
|
+
maxJsonSize: number
|
|
30
|
+
maxIndicators: number
|
|
31
|
+
maxCustomMarkers: number
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** 根据 period 计算最大日期范围(天数) */
|
|
35
|
+
function getMaxDateRangeDays(period: DataConfig['period']): number {
|
|
36
|
+
const LIMITS: Record<string, number> = {
|
|
37
|
+
'5min': 30,
|
|
38
|
+
'15min': 60,
|
|
39
|
+
'30min': 90,
|
|
40
|
+
'60min': 180,
|
|
41
|
+
daily: 365 * 3,
|
|
42
|
+
weekly: 365 * 5,
|
|
43
|
+
monthly: 365 * 10,
|
|
44
|
+
}
|
|
45
|
+
return LIMITS[period] ?? 365
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ============ 单例 Canvas(颜色校验用) ============
|
|
49
|
+
|
|
50
|
+
const _colorCanvas = typeof document !== 'undefined' ? document.createElement('canvas') : null
|
|
51
|
+
const _colorCtx = _colorCanvas?.getContext('2d') ?? null
|
|
52
|
+
|
|
53
|
+
// ============ 校验器类 ============
|
|
54
|
+
|
|
55
|
+
export class SemanticConfigValidator {
|
|
56
|
+
private limits: SecurityLimits
|
|
57
|
+
private _ajv: Promise<any> | null = null
|
|
58
|
+
|
|
59
|
+
constructor() {
|
|
60
|
+
this.limits = {
|
|
61
|
+
maxJsonSize: 64 * 1024, // 64KB
|
|
62
|
+
maxIndicators: 10,
|
|
63
|
+
maxCustomMarkers: 100,
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private async getAjv(): Promise<any> {
|
|
68
|
+
if (!this._ajv) {
|
|
69
|
+
this._ajv = (async () => {
|
|
70
|
+
const Ajv = (await import('ajv')).default
|
|
71
|
+
const ajv = new Ajv({ allErrors: true, allowUnionTypes: true })
|
|
72
|
+
ajv.addFormat('date', {
|
|
73
|
+
type: 'string',
|
|
74
|
+
validate: (data: string) => /^\d{4}-\d{2}-\d{2}$/.test(data),
|
|
75
|
+
})
|
|
76
|
+
const schemaModule = await import('./schema.json')
|
|
77
|
+
ajv.addSchema(schemaModule.default || schemaModule)
|
|
78
|
+
return ajv
|
|
79
|
+
})()
|
|
80
|
+
}
|
|
81
|
+
return this._ajv
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 入口校验(在 JSON.parse 之前调用)
|
|
86
|
+
* 检查原始字符串大小
|
|
87
|
+
*/
|
|
88
|
+
validateRawInput(raw: string): ValidationResult {
|
|
89
|
+
const byteLength =
|
|
90
|
+
typeof TextEncoder !== 'undefined'
|
|
91
|
+
? new TextEncoder().encode(raw).byteLength
|
|
92
|
+
: raw.length * 3 // SSR 降级:保守估计
|
|
93
|
+
|
|
94
|
+
if (byteLength > this.limits.maxJsonSize) {
|
|
95
|
+
return { valid: false, errors: ['JSON payload too large (max 64KB)'] }
|
|
96
|
+
}
|
|
97
|
+
return { valid: true }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* JSON Schema 校验
|
|
102
|
+
*/
|
|
103
|
+
async validate(config: unknown): Promise<ValidationResult> {
|
|
104
|
+
// 1. 类型检查
|
|
105
|
+
if (!config || typeof config !== 'object') {
|
|
106
|
+
return { valid: false, errors: ['Config must be an object'] }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 2. 原型污染检查
|
|
110
|
+
const protoCheck = this.checkPrototypePollution(config)
|
|
111
|
+
if (!protoCheck.valid) {
|
|
112
|
+
return protoCheck
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 3. JSON Schema 校验
|
|
116
|
+
try {
|
|
117
|
+
const ajv = await this.getAjv()
|
|
118
|
+
const valid = ajv.validate('https://kmap.dev/schemas/semantic-chart-config/1.0.0', config)
|
|
119
|
+
if (!valid) {
|
|
120
|
+
const errors = ajv.errors?.map((e: { instancePath: string; message?: string }) => `${e.instancePath} ${e.message}`) || ['Schema validation failed']
|
|
121
|
+
return { valid: false, errors }
|
|
122
|
+
}
|
|
123
|
+
} catch {
|
|
124
|
+
return { valid: false, errors: ['Schema validation unavailable (ajv not loaded)'] }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return { valid: true }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* 安全校验(纯同步)
|
|
132
|
+
*/
|
|
133
|
+
securityCheck(config: SemanticChartConfig): SecurityResult {
|
|
134
|
+
const violations: string[] = []
|
|
135
|
+
|
|
136
|
+
// 1. 检查日期范围限制
|
|
137
|
+
const dateCheck = this.checkDateRange(config.data)
|
|
138
|
+
if (!dateCheck.valid) {
|
|
139
|
+
violations.push(...(dateCheck.errors || []))
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 2. 检查股票代码格式
|
|
143
|
+
const symbolCheck = this.checkSymbol(config.data)
|
|
144
|
+
if (!symbolCheck.valid) {
|
|
145
|
+
violations.push(...(symbolCheck.errors || []))
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 3. 检查指标数量限制
|
|
149
|
+
if (config.indicators) {
|
|
150
|
+
const mainCount = config.indicators.main?.length || 0
|
|
151
|
+
const subCount = config.indicators.sub?.length || 0
|
|
152
|
+
if (mainCount + subCount > this.limits.maxIndicators) {
|
|
153
|
+
violations.push(`Too many indicators (max ${this.limits.maxIndicators})`)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 4. 检查标记数量限制
|
|
158
|
+
const markerCount = config.markers?.customMarkers?.length || 0
|
|
159
|
+
if (markerCount > this.limits.maxCustomMarkers) {
|
|
160
|
+
violations.push(`Too many custom markers (max ${this.limits.maxCustomMarkers})`)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// 5. 检查颜色值格式
|
|
164
|
+
if (config.markers?.customMarkers) {
|
|
165
|
+
for (const marker of config.markers.customMarkers) {
|
|
166
|
+
if (marker.style) {
|
|
167
|
+
const colorCheck = this.checkMarkerStyle(marker.style, marker.id)
|
|
168
|
+
violations.push(...colorCheck)
|
|
169
|
+
}
|
|
170
|
+
// 检查日期格式
|
|
171
|
+
const dateCheck = this.checkMarkerDate(marker.date, marker.id)
|
|
172
|
+
violations.push(...dateCheck)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
passed: violations.length === 0,
|
|
178
|
+
violations: violations.length > 0 ? violations : undefined,
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ============ 私有方法 ============
|
|
183
|
+
|
|
184
|
+
private checkPrototypePollution(obj: unknown, path = ''): ValidationResult {
|
|
185
|
+
if (!obj || typeof obj !== 'object') {
|
|
186
|
+
return { valid: true }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const errors: string[] = []
|
|
190
|
+
for (const key of Object.keys(obj as Record<string, unknown>)) {
|
|
191
|
+
if (FORBIDDEN_KEYS.includes(key)) {
|
|
192
|
+
errors.push(`Forbidden key "${key}" found at ${path}`)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return errors.length > 0 ? { valid: false, errors } : { valid: true }
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private checkDateRange(data: DataConfig): ValidationResult {
|
|
200
|
+
const errors: string[] = []
|
|
201
|
+
|
|
202
|
+
const startDate = new Date(data.startDate)
|
|
203
|
+
const endDate = new Date(data.endDate)
|
|
204
|
+
|
|
205
|
+
if (isNaN(startDate.getTime())) {
|
|
206
|
+
errors.push(`Invalid startDate: ${data.startDate}`)
|
|
207
|
+
}
|
|
208
|
+
if (isNaN(endDate.getTime())) {
|
|
209
|
+
errors.push(`Invalid endDate: ${data.endDate}`)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (errors.length > 0) {
|
|
213
|
+
return { valid: false, errors }
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const maxDays = getMaxDateRangeDays(data.period)
|
|
217
|
+
const diffDays = Math.ceil((endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24))
|
|
218
|
+
|
|
219
|
+
if (diffDays > maxDays) {
|
|
220
|
+
errors.push(`Date range exceeds maximum for period "${data.period}" (max ${maxDays} days, got ${diffDays})`)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (diffDays < 0) {
|
|
224
|
+
errors.push('endDate must be after startDate')
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return errors.length > 0 ? { valid: false, errors } : { valid: true }
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private checkSymbol(data: DataConfig): ValidationResult {
|
|
231
|
+
const { symbol, exchange } = data
|
|
232
|
+
|
|
233
|
+
if (exchange) {
|
|
234
|
+
const pattern = SYMBOL_PATTERNS[exchange]
|
|
235
|
+
if (!pattern.test(symbol)) {
|
|
236
|
+
return { valid: false, errors: [`Invalid symbol "${symbol}" for exchange "${exchange}"`] }
|
|
237
|
+
}
|
|
238
|
+
} else {
|
|
239
|
+
// 自动识别
|
|
240
|
+
const valid = Object.values(SYMBOL_PATTERNS).some((p) => p.test(symbol))
|
|
241
|
+
if (!valid) {
|
|
242
|
+
return { valid: false, errors: [`Invalid symbol format: "${symbol}"`] }
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return { valid: true }
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private checkMarkerStyle(style: MarkerStyle, markerId: string): string[] {
|
|
250
|
+
const errors: string[] = []
|
|
251
|
+
const colorFields = ['fillColor', 'strokeColor', 'textColor'] as const
|
|
252
|
+
|
|
253
|
+
for (const field of colorFields) {
|
|
254
|
+
const value = style[field]
|
|
255
|
+
if (typeof value === 'string' && !COLOR_PATTERN.test(value)) {
|
|
256
|
+
errors.push(`Invalid ${field} in marker "${markerId}": ${value}`)
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// 检查颜色值安全性(浏览器环境)
|
|
261
|
+
if (_colorCtx) {
|
|
262
|
+
for (const field of colorFields) {
|
|
263
|
+
const value = style[field]
|
|
264
|
+
if (typeof value === 'string') {
|
|
265
|
+
_colorCtx.fillStyle = '#000000'
|
|
266
|
+
_colorCtx.fillStyle = value
|
|
267
|
+
// 浏览器会过滤非法值,如果返回的不是合法颜色说明有问题
|
|
268
|
+
if (_colorCtx.fillStyle === '#000000' && value !== '#000000' && !value.startsWith('#000')) {
|
|
269
|
+
errors.push(`Potentially unsafe ${field} in marker "${markerId}": ${value}`)
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return errors
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** 日期格式正则:YYYY-MM-DD 或 YYYY-MM-DD HH:mm */
|
|
279
|
+
private static readonly DATE_PATTERN = /^\d{4}-\d{2}-\d{2}( \d{2}:\d{2})?$/
|
|
280
|
+
|
|
281
|
+
private checkMarkerDate(date: string, markerId: string): string[] {
|
|
282
|
+
const errors: string[] = []
|
|
283
|
+
|
|
284
|
+
if (!SemanticConfigValidator.DATE_PATTERN.test(date)) {
|
|
285
|
+
errors.push(`Invalid date format in marker "${markerId}": ${date} (expected YYYY-MM-DD or YYYY-MM-DD HH:mm)`)
|
|
286
|
+
return errors
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// 验证日期是否有效
|
|
290
|
+
const hasTime = date.includes(' ')
|
|
291
|
+
let parsed: Date
|
|
292
|
+
|
|
293
|
+
if (hasTime) {
|
|
294
|
+
parsed = new Date(date)
|
|
295
|
+
} else {
|
|
296
|
+
// 对于纯日期,解析为 UTC
|
|
297
|
+
const [year, month, day] = date.split('-').map(Number)
|
|
298
|
+
parsed = new Date(Date.UTC(year!, month! - 1, day!))
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (isNaN(parsed.getTime())) {
|
|
302
|
+
errors.push(`Invalid date value in marker "${markerId}": ${date}`)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return errors
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ============ 工具函数导出 ============
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* 净化参数对象(防止原型污染)
|
|
313
|
+
*/
|
|
314
|
+
export function sanitizeParams(params: Record<string, unknown>): Record<string, unknown> {
|
|
315
|
+
const safe: Record<string, unknown> = Object.create(null)
|
|
316
|
+
for (const [key, value] of Object.entries(params)) {
|
|
317
|
+
if (!FORBIDDEN_KEYS.includes(key)) {
|
|
318
|
+
safe[key] = value
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return safe
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* 净化颜色值(浏览器环境)
|
|
326
|
+
*/
|
|
327
|
+
export function sanitizeColor(input: string): string | null {
|
|
328
|
+
if (!_colorCtx) return null
|
|
329
|
+
_colorCtx.fillStyle = '#000000'
|
|
330
|
+
_colorCtx.fillStyle = input
|
|
331
|
+
return _colorCtx.fillStyle
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* 校验颜色值格式
|
|
336
|
+
*/
|
|
337
|
+
export function validateColor(color: string): boolean {
|
|
338
|
+
return COLOR_PATTERN.test(color)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* 校验股票代码
|
|
343
|
+
*/
|
|
344
|
+
export function validateSymbol(symbol: string, exchange?: 'SH' | 'SZ' | 'BJ'): boolean {
|
|
345
|
+
if (exchange) {
|
|
346
|
+
return SYMBOL_PATTERNS[exchange].test(symbol)
|
|
347
|
+
}
|
|
348
|
+
return Object.values(SYMBOL_PATTERNS).some((p) => p.test(symbol))
|
|
349
|
+
}
|