import type { Schema, TupleSchema, ArraySchema, ReferenceSchema, StringLiteralSchema, UnionSchema, } from "./types"; import { ParseError } from "./parser"; import { schemaToTypeString } from "./type-utils"; class ValueParser { private input: string; private schemaString: string; private pos: number = 0; constructor(input: string, schemaString: string) { this.input = input; this.schemaString = schemaString; } private peek(): string { return this.input[this.pos] || ""; } private consume(): string { return this.input[this.pos++] || ""; } private skipWhitespace(): void { while (this.pos < this.input.length && /\s/.test(this.input[this.pos])) { this.pos++; } } private consumeStr(str: string): boolean { if (this.input.slice(this.pos, this.pos + str.length) === str) { this.pos += str.length; return true; } return false; } parseValue(schema: Schema): unknown { this.skipWhitespace(); switch (schema.type) { case "string": return this.parseStringValue(); case "number": return this.parseNumberValue(); case "int": return this.parseIntValue(); case "float": return this.parseFloatValue(); case "boolean": return this.parseBooleanValue(); case "stringLiteral": return this.parseStringLiteralValue(schema); case "union": return this.parseUnionValue(schema); case "tuple": return this.parseTupleValue(schema); case "array": return this.parseArrayValue(schema); case "reference": // Reference values are parsed as strings (IDs) initially, resolved later return this.parseReferenceValue(schema); case "reverseReference": // Reverse references are derived fields, not stored in CSV cells // They resolve to null at parse time; actual resolution happens in the loader return null; default: throw new ParseError( `Unknown schema type: ${(schema as { type: string }).type}`, this.pos, this.schemaString, this.input, ); } } private parseStringValue(): string { let result = ""; while (this.pos < this.input.length) { const char = this.peek(); if (char === "\\") { this.consume(); const nextChar = this.consume(); if ( nextChar === ";" || nextChar === "[" || nextChar === "]" || nextChar === "\\" ) { result += nextChar; } else { result += "\\" + nextChar; } } else if (char === ";" || char === "]") { break; } else { result += this.consume(); } } return result.trim(); } private parseNumberValue(): number { let numStr = ""; while (this.pos < this.input.length && /[\d.\-+eE]/.test(this.peek())) { numStr += this.consume(); } const num = parseFloat(numStr); if (isNaN(num)) { throw new ParseError( "Invalid number", this.pos - numStr.length, this.schemaString, this.input, ); } return num; } private parseIntValue(): number { let numStr = ""; while (this.pos < this.input.length && /[\d.\-+eE]/.test(this.peek())) { numStr += this.consume(); } const num = parseFloat(numStr); if (isNaN(num)) { throw new ParseError( "Invalid number", this.pos - numStr.length, this.schemaString, this.input, ); } if (!Number.isInteger(num)) { throw new ParseError( "Expected integer value", this.pos - numStr.length, this.schemaString, this.input, ); } return num; } private parseFloatValue(): number { return this.parseNumberValue(); } private parseBooleanValue(): boolean { if (this.consumeStr("true")) { return true; } if (this.consumeStr("false")) { return false; } throw new ParseError( "Expected true or false", this.pos, this.schemaString, this.input, ); } private parseStringLiteralValue(schema: StringLiteralSchema): string { const quote = this.peek(); // 支持带引号或不带引号的字符串值 if (quote === '"' || quote === "'") { this.consume(); // Consume opening quote let value = ""; while (this.pos < this.input.length) { const char = this.peek(); if (char === "\\") { this.consume(); const nextChar = this.consume(); if ( nextChar === '"' || nextChar === "'" || nextChar === "\\" || nextChar === ";" ) { value += nextChar; } else { value += "\\" + nextChar; } } else if (char === quote) { this.consume(); // Consume closing quote if (value !== schema.value) { throw new ParseError( `Invalid value '"${value}"'. Expected '"${schema.value}"'`, this.pos, this.schemaString, this.input, ); } return value; } else { value += this.consume(); } } throw new ParseError( "Unterminated string literal", this.pos, this.schemaString, this.input, ); } else { // 不带引号的字符串,像普通字符串一样解析 let value = ""; while (this.pos < this.input.length) { const char = this.peek(); if (char === ";" || char === "]" || char === ")") { break; } value += this.consume(); } value = value.trim(); if (value !== schema.value) { throw new ParseError( `Invalid value '${value}'. Expected '${schema.value}'`, this.pos - value.length, this.schemaString, this.input, ); } return value; } } private parseUnionValue(schema: UnionSchema): unknown { const savedPos = this.pos; const errors: Error[] = []; // Try each union member until one succeeds for (let i = 0; i < schema.members.length; i++) { this.pos = savedPos; try { return this.parseValue(schema.members[i]); } catch (e) { errors.push(e as Error); // Continue to next member } } // If all members fail, throw a descriptive error throw new ParseError( `Value does not match any union member. Tried ${schema.members.length} alternatives.`, this.pos, this.schemaString, this.input, ); } private parseTupleValue(schema: TupleSchema): unknown[] { if (!this.consumeStr("[")) { throw new ParseError( "Expected [", this.pos, this.schemaString, this.input, ); } this.skipWhitespace(); if (this.peek() === "]") { this.consume(); return []; } const result: unknown[] = []; for (let i = 0; i < schema.elements.length; i++) { this.skipWhitespace(); const elementSchema = schema.elements[i]; // Try to consume optional name prefix (e.g., "current:") if (elementSchema.name) { this.skipWhitespace(); const savedPos = this.pos; if (this.consumeStr(`${elementSchema.name}:`)) { this.skipWhitespace(); } else { // Name not found, reset position and continue without name this.pos = savedPos; } } result.push(this.parseValue(elementSchema.schema)); this.skipWhitespace(); if (i < schema.elements.length - 1) { if (!this.consumeStr(";")) { throw new ParseError( "Expected ;", this.pos, this.schemaString, this.input, ); } } } this.skipWhitespace(); if (!this.consumeStr("]")) { throw new ParseError( "Expected ]", this.pos, this.schemaString, this.input, ); } return result; } private parseArrayValue(schema: ArraySchema): unknown[] { if (this.pos >= this.input.length || !this.input.trim()) { return []; } if (!this.consumeStr("[")) { throw new ParseError( "Expected [", this.pos, this.schemaString, this.input, ); } this.skipWhitespace(); if (this.peek() === "]") { this.consume(); return []; } const result: unknown[] = []; while (true) { this.skipWhitespace(); result.push(this.parseValue(schema.element)); this.skipWhitespace(); if (!this.consumeStr(";")) { break; } } this.skipWhitespace(); if (!this.consumeStr("]")) { throw new ParseError( "Expected ]", this.pos, this.schemaString, this.input, ); } return result; } private parseReferenceValue( schema: ReferenceSchema, ): string | string[] | null { if (schema.isOptional) { this.skipWhitespace(); if (this.pos >= this.input.length) { return null; } } if (schema.isArray) { // Parse array of IDs: [id1; id2; id3] if (!this.consumeStr("[")) { throw new ParseError( "Expected [", this.pos, this.schemaString, this.input, ); } this.skipWhitespace(); if (this.peek() === "]") { this.consume(); return []; } const ids: string[] = []; while (true) { this.skipWhitespace(); // Parse each ID as a string let id = ""; while ( this.pos < this.input.length && this.peek() !== ";" && this.peek() !== "]" ) { id += this.consume(); } ids.push(id.trim()); this.skipWhitespace(); if (!this.consumeStr(";")) { break; } } this.skipWhitespace(); if (!this.consumeStr("]")) { throw new ParseError( "Expected ]", this.pos, this.schemaString, this.input, ); } return ids; } else { // Parse single ID as string let id = ""; while (this.pos < this.input.length) { const char = this.peek(); if (char === ";" || char === "]" || char === ",") { break; } id += this.consume(); } return id.trim(); } } getPosition(): number { return this.pos; } getInputLength(): number { return this.input.length; } } export function parseValue( schema: Schema, valueString: string, schemaString?: string, ): unknown { const sStr = schemaString || schemaToTypeString(schema); const parser = new ValueParser(valueString.trim(), sStr); const value = parser.parseValue(schema); if (parser.getPosition() < parser.getInputLength()) { throw new ParseError( "Unexpected input after value", parser.getPosition(), sStr, valueString.trim(), ); } return value; }