UNPKG

@kstory/core

Version:

Core parser and lexer for KStory interactive fiction language

1,622 lines (1,617 loc) 60.2 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); var __esm = (fn, res) => function __init() { return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res; }; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/token.ts var TokenTypes; var init_token = __esm({ "src/token.ts"() { "use strict"; TokenTypes = { EOF: "EOF", INDENT: "INDENT", DEDENT: "DEDENT", NEWLINE: "NEWLINE", COMMENT: "COMMENT", // # comment COMMENT_CONTENT: "COMMENT_CONTENT", // comment content REPLICA_BEGIN: "REPLICA_BEGIN", // " REPLICA_END: "REPLICA_END", COMMENT_MULTILINE_BEGIN: "COMMENT_MULTILINE_BEGIN", // /* COMMENT_MULTILINE_END: "COMMENT_MULTILINE_END", // */ GOTO: "GOTO", // => | -> TAG: "TAG", // @tag TAG_VALUE: "TAG_VALUE", // @tag value SECTION: "SECTION", // == IDENTIFIER: "IDENTIFIER", // == name | @name CHOICE: "CHOICE", // + CHOICE_TAG: "CHOICE_TAG", // @@tag CHOICE_TEXT: "CHOICE_TEXT", // choice text content CHOICE_TEXT_BOUND: "CHOICE_TEXT_BOUND", // ``` // values BOOLEAN: "BOOLEAN", // false / true INT: "INT", // 12 FLOAT: "FLOAT", // 12.2 STRING: "STRING", // Text ERROR: "ERROR", // for unknown tokens // function calls CALL: "CALL", // @call:functionName() CALL_ARGUMENT: "CALL_ARGUMENT" // function parameter }; } }); // src/tokenFactory.ts function commentToken(value) { return { type: TokenTypes.COMMENT, value }; } function commentContentToken(value) { return { type: TokenTypes.COMMENT_CONTENT, value }; } function multiCommentBeginToken() { return { type: TokenTypes.COMMENT_MULTILINE_BEGIN, value: "/*" }; } function multiCommentEndToken() { return { type: TokenTypes.COMMENT_MULTILINE_END, value: "*/" }; } function newLineToken() { return { type: TokenTypes.NEWLINE, value: "\n" }; } function indentToken() { return { type: TokenTypes.INDENT }; } function dedentToken() { return { type: TokenTypes.DEDENT }; } function eofToken() { return { type: TokenTypes.EOF }; } function replicaBeginToken() { return { type: TokenTypes.REPLICA_BEGIN, value: '" ' }; } function replicaEndToken() { return { type: TokenTypes.REPLICA_END }; } function stringToken(value) { return { type: TokenTypes.STRING, value }; } function choiceToken(val) { return { type: TokenTypes.CHOICE, value: val }; } function choiceTextBoundToken(silent = false) { return { type: TokenTypes.CHOICE_TEXT_BOUND, value: silent ? "" : "```" }; } function choiceTextToken(value) { return { type: TokenTypes.CHOICE_TEXT, value }; } function choiceTagToken(val) { return { type: TokenTypes.CHOICE_TAG, value: val }; } function tagToken(val) { return { type: TokenTypes.TAG, value: val }; } function tagValueToken(val) { return { type: TokenTypes.TAG_VALUE, value: val }; } function sectionToken() { return { type: TokenTypes.SECTION, value: "== " }; } function identifierToken(val) { return { type: TokenTypes.IDENTIFIER, value: val }; } function gotoToken(val) { return { type: TokenTypes.GOTO, value: val }; } function callToken(functionName) { return { type: TokenTypes.CALL, value: functionName }; } function callArgumentToken(argument) { return { type: TokenTypes.CALL_ARGUMENT, value: argument }; } var init_tokenFactory = __esm({ "src/tokenFactory.ts"() { "use strict"; init_token(); __name(commentToken, "commentToken"); __name(commentContentToken, "commentContentToken"); __name(multiCommentBeginToken, "multiCommentBeginToken"); __name(multiCommentEndToken, "multiCommentEndToken"); __name(newLineToken, "newLineToken"); __name(indentToken, "indentToken"); __name(dedentToken, "dedentToken"); __name(eofToken, "eofToken"); __name(replicaBeginToken, "replicaBeginToken"); __name(replicaEndToken, "replicaEndToken"); __name(stringToken, "stringToken"); __name(choiceToken, "choiceToken"); __name(choiceTextBoundToken, "choiceTextBoundToken"); __name(choiceTextToken, "choiceTextToken"); __name(choiceTagToken, "choiceTagToken"); __name(tagToken, "tagToken"); __name(tagValueToken, "tagValueToken"); __name(sectionToken, "sectionToken"); __name(identifierToken, "identifierToken"); __name(gotoToken, "gotoToken"); __name(callToken, "callToken"); __name(callArgumentToken, "callArgumentToken"); } }); // src/lexer.ts var lexer_exports = {}; __export(lexer_exports, { Lexer: () => Lexer }); var INDENT_WIDTH, CALL_PREFIX_LENGTH, MULTI_COMMENT_START_LENGTH, MULTI_COMMENT_END_LENGTH, CHOICE_PREFIX_LENGTH, REPLICA_PREFIX_LENGTH, SECTION_PREFIX_LENGTH, GOTO_PREFIX_LENGTH, CHOICE_TEXT_BOUND_LENGTH, INLINE_CALL_PREFIX_LENGTH, Lexer; var init_lexer = __esm({ "src/lexer.ts"() { "use strict"; init_token(); init_tokenFactory(); INDENT_WIDTH = 2; CALL_PREFIX_LENGTH = 6; MULTI_COMMENT_START_LENGTH = 2; MULTI_COMMENT_END_LENGTH = 2; CHOICE_PREFIX_LENGTH = 2; REPLICA_PREFIX_LENGTH = 2; SECTION_PREFIX_LENGTH = 3; GOTO_PREFIX_LENGTH = 3; CHOICE_TEXT_BOUND_LENGTH = 3; INLINE_CALL_PREFIX_LENGTH = 6; Lexer = class { constructor(source) { this.source = source; this.position = 0; this.tokens = []; this.curChar = void 0; this.curLine = 1; this.curColumn = 1; this.prevIndent = 0; this.isInComment = false; this.isInChoiceText = false; this.isExtendingStr = false; this.lastStringTokenIndex = 0; // Start position of the current STRING buffer (for precise text token spans) this.stringStartLine = 1; this.stringStartColumn = 1; this.curChar = this.source[0]; } static { __name(this, "Lexer"); } getTokens() { return this.tokens; } step(times = 1) { for (let i = 0; i < times; i++) { const prevChar = this.curChar; this.position++; this.curChar = this.source[this.position]; if (prevChar === "\n") { this.curLine++; this.curColumn = 1; } else { this.curColumn++; } } } process() { let iterations = 0; const maxIterations = 1e5; while (this.curChar && iterations < maxIterations) { this.handleCurrentToken(); iterations++; } if (iterations >= maxIterations) { console.error( "Error: Lexer exceeded max iterations, possible infinite loop detected" ); console.error( `Last position: ${this.position}, line: ${this.curLine}, column: ${this.curColumn}, char: '${this.curChar}'` ); throw new Error( `Lexer exceeded max iterations at position ${this.position}, line ${this.curLine}, column ${this.curColumn}` ); } this.push(eofToken()); } // Push a token stamped with the current cursor position (both start and end) push(token) { token.line = this.curLine; token.column = this.curColumn; token.endLine = this.curLine; token.endColumn = this.curColumn; this.tokens.push(token); } // Push a token with an explicit start position; end is the current cursor pushAt(token, line, column) { token.line = line; token.column = column; token.endLine = this.curLine; token.endColumn = this.curColumn; this.tokens.push(token); } // Insert a token at an index, stamping with the current cursor position insertTokenAt(index, token) { token.line = this.curLine; token.column = this.curColumn; token.endLine = this.curLine; token.endColumn = this.curColumn; this.tokens.splice(index, 0, token); } handleCurrentToken() { this.handleNewLine(); this.handleIndent(); this.handleComment(); if (this.isInComment) return; this.handleNonCommentTokens(); this.handleErrorIfNeeded(); } handleNonCommentTokens() { if (!this.isExtendingStr && !this.isInChoiceText) { this.handleStructuralTokens(); } this.handleChoiceText(); this.handleString(); } handleStructuralTokens() { this.handleCall(); this.handleChoiceTag(); this.handleTag(); this.handleSection(); this.handleGoto(); this.handleChoice(); } handleErrorIfNeeded() { if (!this.isInComment && !this.isExtendingStr && !this.isInChoiceText) { this.handleError(); } } handleGoto() { if (!this.isGoto()) { return; } const startLine = this.curLine; const startColumn = this.curColumn; const content = `${this.curChar}> `; this.step(GOTO_PREFIX_LENGTH); this.pushAt(gotoToken(content), startLine, startColumn); this.handleIdentifier(); } handleIdentifier() { const startLine = this.curLine; const startColumn = this.curColumn; const content = this.readLineUntilComment(); this.pushAt(identifierToken(content), startLine, startColumn); } handleError() { let content = ""; let iterations = 0; const maxIterations = 1e3; while (!this.getTokenType() && iterations < maxIterations) { if (this.curChar === void 0) { break; } content += this.curChar; this.step(); iterations++; } if (iterations >= maxIterations) { console.warn("Warning: handleError() exceeded max iterations"); } if (content.trim().length > 0) { this.push({ type: TokenTypes.ERROR, value: content }); } } handleChoice() { if (!this.isChoice()) { return; } const startLine = this.curLine; const startColumn = this.curColumn; if (this.peek(1) === " ") { this.step(CHOICE_PREFIX_LENGTH); this.pushAt(choiceToken("+ "), startLine, startColumn); } else { this.step(1); this.pushAt(choiceToken("+"), startLine, startColumn); } this.handleChoiceTextInline(); } handleChoiceTextInline() { const content = this.readLineUntilComment(); if (content.length > 0) { this.push(choiceTextBoundToken(true)); this.push(choiceTextToken(content)); this.push(choiceTextBoundToken(true)); } } handleSection() { if (!this.isSection()) { return; } const startLine = this.curLine; const startColumn = this.curColumn; this.step(SECTION_PREFIX_LENGTH); this.pushAt(sectionToken(), startLine, startColumn); this.handleSectionName(); } handleSectionName() { let content = ""; if (this.isWhitespace() || this.isNewLine()) { return; } while (this.curChar) { content += this.curChar; if (this.isWhitespace(1) || this.isNewLine(1)) { break; } this.step(); } this.step(); this.push(identifierToken(content)); } handleChoiceTag() { if (!this.isChoiceTag()) { return; } const startLine = this.curLine; const startColumn = this.curColumn; this.step(); const tagName = `@${this.getTagName()}`; const value = this.getTagValue(); this.pushAt(choiceTagToken(tagName), startLine, startColumn); if (value) { this.pushAt(tagValueToken(value), startLine, startColumn); } } handleChoiceText() { if (this.isChoiceTextBound() && !this.isInChoiceText) { this.isInChoiceText = true; this.push(choiceTextBoundToken()); this.step(3); this.handleChoiceTextExtend(); } else if (this.isInChoiceText) { this.handleChoiceTextExtend(); } } handleChoiceTextExtend(isInlined = false) { let content = ""; let iterations = 0; const maxIterations = 1e4; while (this.curChar && iterations < maxIterations) { if (this.isChoiceTextEnding()) { this.finalizeChoiceText(content, isInlined); return; } content = this.processChoiceTextContent(content); iterations++; } if (iterations >= maxIterations) { console.warn("Warning: handleChoiceTextExtend() exceeded max iterations"); } this.finalizeChoiceText(content, isInlined); } isChoiceTextEnding() { return this.isChoiceTextBound(); } processChoiceTextContent(content) { if (this.isNewLine()) { return this.handleChoiceTextNewline(content); } if (this.isComment()) { return this.handleChoiceTextComment(content); } if (this.isMultiComment()) { return this.handleChoiceTextMultiComment(content); } content += this.curChar; this.step(); return content; } handleChoiceTextNewline(content) { if (content.length > 0) { this.push(choiceTextToken(content)); } this.push(newLineToken()); this.step(); return ""; } handleChoiceTextComment(content) { if (content.length > 0) { this.push(choiceTextToken(content)); } this.handleCommentContent(); return ""; } handleChoiceTextMultiComment(content) { if (content.length > 0) { this.push(choiceTextToken(content)); } this.isInComment = true; this.step(2); this.push(multiCommentBeginToken()); this.handleMultiCommentExtend(); return ""; } finalizeChoiceText(content, isInlined) { if (content.length > 0) { this.push(choiceTextToken(content)); } this.isInChoiceText = false; this.step(CHOICE_TEXT_BOUND_LENGTH); this.push(choiceTextBoundToken(isInlined)); } handleTag() { if (!this.isTag()) { return; } const startLine = this.curLine; const startColumn = this.curColumn; const tagName = this.getTagName(); const value = this.getTagValue(); this.pushAt(tagToken(tagName), startLine, startColumn); if (value) { this.pushAt(tagValueToken(value), startLine, startColumn); } } handleCall() { if (!this.isCall()) { return; } const startLine = this.curLine; const startColumn = this.curColumn; this.step(CALL_PREFIX_LENGTH); let functionName = ""; while (this.curChar && this.curChar !== "(" && this.curChar !== " " && !this.isNewLine()) { functionName += this.curChar; this.step(); } if (this.curChar === "(" && this.isEscapedChar()) { functionName += this.curChar; this.step(); while (this.curChar && this.curChar !== "(" && this.curChar !== " " && !this.isNewLine()) { functionName += this.curChar; this.step(); } } this.pushAt(callToken(functionName), startLine, startColumn); if (this.curChar === "(") { this.step(); this.handleCallArguments(); this.step(); } } handleCallArguments() { let depth = 1; let currentArg = ""; let inQuotes = false; let argStartLine = this.curLine; let argStartColumn = this.curColumn; let iterations = 0; const maxIterations = 1e4; while (this.curChar && depth > 0 && iterations < maxIterations) { if (this.curChar === '"' && !this.isEscapedChar()) { inQuotes = !inQuotes; } if (!inQuotes) { if (this.curChar === "(" && !this.isEscapedChar()) { depth++; } else if (this.curChar === ")" && !this.isEscapedChar()) { depth--; if (depth === 0) { break; } } else if (this.curChar === "," && depth === 1) { if (currentArg.trim().length > 0) { this.pushAt( callArgumentToken(currentArg.trim()), argStartLine, argStartColumn ); } currentArg = ""; this.step(); argStartLine = this.curLine; argStartColumn = this.curColumn; iterations++; continue; } } currentArg += this.curChar; this.step(); iterations++; } if (iterations >= maxIterations) { console.warn("Warning: handleCallArguments() exceeded max iterations"); } if (currentArg.trim().length > 0) { this.pushAt( callArgumentToken(currentArg.trim()), argStartLine, argStartColumn ); } } getTagName() { let tagName = ""; let iterations = 0; const maxIterations = 1e3; while (this.curChar && iterations < maxIterations) { tagName += this.curChar; if (this.isWhitespace(1) || this.isNewLine(1)) { break; } this.step(); iterations++; } if (iterations >= maxIterations) { console.warn("Warning: getTagName() exceeded max iterations"); } this.step(); return tagName; } getTagValue() { let value = ""; let iterations = 0; const maxIterations = 1e3; if (this.getTokenType(0)) { return value; } while (this.curChar && iterations < maxIterations) { if (this.getTokenType(1) || this.isEOF(1)) { break; } this.step(); value += this.curChar; iterations++; } if (iterations >= maxIterations) { console.warn("Warning: getTagValue() exceeded max iterations"); } this.step(); return value; } handleString() { if (this.isReplicaBegin()) { if (this.isExtendingStr) { this.handleEndReplica(); } this.handleNewReplica(); } else if (this.isExtendingStr) { this.handleExtendReplica(); } else { this.handleInlineCall(); } } handleInlineCall() { if (!this.isInlineCall()) { return ""; } let result = "{call:"; this.step(INLINE_CALL_PREFIX_LENGTH); let iterations = 0; const maxIterations = 1e3; while (this.curChar && this.curChar !== "(" && this.curChar !== "}" && iterations < maxIterations) { result += this.curChar; this.step(); iterations++; } if (iterations >= maxIterations) { console.warn( "Warning: handleInlineCall() function name reading exceeded max iterations" ); } if (this.curChar === "(") { result += this.curChar; this.step(); let depth = 1; let parenIterations = 0; const maxParenIterations = 1e4; while (this.curChar && depth > 0 && parenIterations < maxParenIterations) { result += this.curChar; if (this.curChar === "(" && !this.isEscapedChar()) { depth++; } else if (this.curChar === ")" && !this.isEscapedChar()) { depth--; } this.step(); parenIterations++; } if (parenIterations >= maxParenIterations) { console.warn( "Warning: handleInlineCall() parentheses processing exceeded max iterations" ); } } if (this.curChar === "}") { result += "}"; this.step(); } return result; } isInlineCall(offset = 0) { return this.isNotEscapingChar("{", offset) && this.peek(offset + 1) === "c" && this.peek(offset + 2) === "a" && this.peek(offset + 3) === "l" && this.peek(offset + 4) === "l" && this.peek(offset + 5) === ":"; } handleNewReplica() { const startLine = this.curLine; const startColumn = this.curColumn; this.step(REPLICA_PREFIX_LENGTH); this.isExtendingStr = true; this.pushAt(replicaBeginToken(), startLine, startColumn); this.handleExtendReplica(); } handleExtendReplica() { let content = ""; if (this.shouldEndReplica()) { this.handleEndReplica(); return; } content = this.handleInlineCallInReplica(content); if (this.getTokenType(0)) { return; } content = this.processReplicaContent(content); this.finalizeReplicaContent(content); } shouldEndReplica() { return this.isReplicaBegin() || this.isTag() || this.isChoiceTag() || this.isGoto() || this.isChoice() || this.isSection() || this.isEOF(); } handleInlineCallInReplica(content) { if (this.isInlineCall()) { this.addStringTokenIfNotEmpty(content); return this.handleInlineCall(); } return content; } addStringTokenIfNotEmpty(content) { if (content.trim().length > 0) { this.pushAt( stringToken(content), this.stringStartLine, this.stringStartColumn ); this.lastStringTokenIndex = this.tokens.length - 1; } } processReplicaContent(content) { let isReplicaEnding = false; let iterations = 0; const maxIterations = 1e4; while (this.curChar && iterations < maxIterations) { if (this.isInlineCall()) { this.addStringTokenIfNotEmpty(content); content = this.handleInlineCall(); iterations++; continue; } if (content.length === 0) { this.stringStartLine = this.curLine; this.stringStartColumn = this.curColumn; } content += this.curChar; if (isReplicaEnding) { isReplicaEnding = true; break; } if (this.getTokenType(1)) { break; } this.step(); iterations++; } if (iterations >= maxIterations) { console.warn("Warning: processReplicaContent() exceeded max iterations"); } this.step(); return content; } isReplicaEnding() { return this.isTag(1) || this.isChoiceTag(1) || this.isReplicaBegin(1) || this.isEOF(1); } finalizeReplicaContent(content) { if (content.trim().length > 0) { this.pushAt( stringToken(content), this.stringStartLine, this.stringStartColumn ); this.lastStringTokenIndex = this.tokens.length - 1; } } handleEndReplica() { this.isExtendingStr = false; this.insertTokenAt(this.lastStringTokenIndex + 1, replicaEndToken()); } handleIndent() { if (this.isInComment) { return; } if (!this.isFirstOnLine()) { return; } let spaces = 0; let iterations = 0; const maxIterations = 1e3; while (this.curChar && this.isWhitespace() && iterations < maxIterations) { if (this.isNewLine()) { break; } if (this.curChar === " ") { spaces++; } if (this.curChar === " ") { spaces += 2; } this.step(); iterations++; } if (iterations >= maxIterations) { console.warn("Warning: handleIndent() exceeded max iterations"); } let indentDiff = Math.floor((spaces - this.prevIndent) / INDENT_WIDTH); if (indentDiff !== 0 && this.isExtendingStr) { this.handleEndReplica(); } while (indentDiff !== 0) { if (indentDiff > 0) { this.push(indentToken()); indentDiff--; } else { this.push(dedentToken()); indentDiff++; } } this.prevIndent = spaces; } handleComment() { if (this.isInComment) { this.handleMultiCommentExtend(); return; } if (this.isMultiComment()) { this.isInComment = true; this.step(MULTI_COMMENT_START_LENGTH); this.push(multiCommentBeginToken()); this.handleMultiCommentExtend(); return; } if (this.isComment()) { this.handleCommentContent(); } } handleCommentContent() { let content = "#"; this.step(); while (this.curChar && !this.isEndOfLine()) { content += this.curChar; this.step(); } if (content.length > 1) { this.push(commentToken(content)); } } handleMultiCommentExtend() { let content = ""; let iterations = 0; const maxIterations = 1e4; if (this.isMultiCommentEnd()) { this.isInComment = false; this.push(multiCommentEndToken()); this.step(MULTI_COMMENT_END_LENGTH); return; } if (this.isNewLine()) { return; } while (this.curChar && iterations < maxIterations) { content += this.curChar; if (this.isEndOfLine(1) || this.isMultiCommentEnd(1)) { break; } this.step(); iterations++; } if (iterations >= maxIterations) { console.warn( "Warning: handleMultiCommentExtend() exceeded max iterations" ); } this.push(commentContentToken(content)); if (this.isMultiCommentEnd(1)) { this.isInComment = false; this.push(multiCommentEndToken()); this.step(2); } this.step(); } handleNewLine() { if (this.curChar === "\n") { this.push(newLineToken()); this.step(); } } getTokenType(offset = 0) { const tokens = [ { type: TokenTypes.NEWLINE, fn: this.isNewLine }, { type: TokenTypes.CHOICE_TAG, fn: this.isChoiceTag }, { type: TokenTypes.TAG, fn: this.isTag }, { type: TokenTypes.COMMENT, fn: this.isComment }, { type: TokenTypes.COMMENT_MULTILINE_BEGIN, fn: this.isMultiComment }, { type: TokenTypes.GOTO, fn: this.isGoto }, { type: TokenTypes.SECTION, fn: this.isSection }, { type: TokenTypes.CHOICE, fn: this.isChoice }, { type: TokenTypes.STRING, fn: this.isReplicaBegin }, { type: TokenTypes.CHOICE_TEXT_BOUND, fn: this.isChoiceTextBound }, { type: TokenTypes.CALL, fn: this.isCall } ]; return tokens.find((info) => { return info.fn.call(this, offset); })?.type; } isGoto(offset = 0) { if (!this.isNotEscapingChar("=", offset) && !this.isNotEscapingChar("-", offset)) { return false; } return this.peek(offset + 1) === ">" && this.peek(offset + 2) === " "; } isSection(offset = 0) { return this.isNotEscapingChar("=", offset) && this.peek(offset + 1) === "=" && this.peek(offset + 2) === " "; } isChoice(offset = 0) { if (!this.isFirstOnLine(offset)) { return false; } return this.isNotEscapingChar("+", offset); } isChoiceTextBound(offset = 0) { return this.isNotEscapingChar("`", offset) && this.peek(offset + 1) === "`" && this.peek(offset + 2) === "`"; } isReplicaBegin(offset = 0) { return this.isNotEscapingChar('"', offset) && this.peek(offset + 1) === " "; } isTag(offset = 0) { return this.isNotEscapingChar("@", offset); } isChoiceTag(offset = 0) { return this.isNotEscapingChar("@", offset) && this.peek(1) === "@"; } isComment(offset = 0) { return this.peek(offset) === "#" && this.peek(offset - 1) !== "\\"; } isMultiComment(offset = 0) { return this.isNotEscapingChar("/", offset) && this.peek(offset + 1) === "*"; } isMultiCommentEnd(offset = 0) { return this.isNotEscapingChar("*", offset) && this.peek(offset + 1) === "/"; } isNewLine(offset = 0) { return this.peek(offset) === "\n"; } isEOF(offset = 0) { return this.peek(offset) === void 0; } isFirstOnLine(offset = 0) { let prevPos = offset - 1; while (prevPos >= 0) { if (this.isEOF(prevPos) || this.isNewLine(prevPos)) { return true; } if (!this.isWhitespace(prevPos)) { return false; } prevPos--; } return true; } readLine() { let content = ""; while (this.curChar && !this.isEndOfLine()) { content += this.curChar; this.step(); } return content; } readLineUntilComment() { let content = ""; let iterations = 0; const maxIterations = 1e3; while (this.curChar && !this.isEndOfLine() && iterations < maxIterations) { if (this.isComment() || this.isMultiComment()) { break; } content += this.curChar; this.step(); iterations++; } if (iterations >= maxIterations) { console.warn("Warning: readLineUntilComment() exceeded max iterations"); } return content; } isNotEscapingChar(char, offset = 0) { return this.peek(offset) === char && this.peek(offset - 1) !== "\\"; } isEndOfLine(offset = 0) { return this.isNewLine(offset) || this.isEOF(offset); } isWhitespace(offset = 0) { const char = this.peek(offset); return char === " " || char === " "; } isEscapedChar(offset = 0) { return this.peek(offset - 1) === "\\"; } peek(pos = 0) { return this.source[this.position + pos]; } isCall(offset = 0) { return this.isNotEscapingChar("@", offset) && this.peek(offset + 1) === "c" && this.peek(offset + 2) === "a" && this.peek(offset + 3) === "l" && this.peek(offset + 4) === "l" && this.peek(offset + 5) === ":"; } }; } }); // src/index.ts var index_exports = {}; __export(index_exports, { Lexer: () => Lexer, ParseError: () => ParseError, callArgumentToken: () => callArgumentToken, callToken: () => callToken, choiceTagToken: () => choiceTagToken, choiceTextBoundToken: () => choiceTextBoundToken, choiceTextToken: () => choiceTextToken, choiceToken: () => choiceToken, commentContentToken: () => commentContentToken, commentToken: () => commentToken, dedentToken: () => dedentToken, eofToken: () => eofToken, gotoToken: () => gotoToken, identifierToken: () => identifierToken, indentToken: () => indentToken, multiCommentBeginToken: () => multiCommentBeginToken, multiCommentEndToken: () => multiCommentEndToken, newLineToken: () => newLineToken, parseAll: () => parseAll, parseFromSource: () => parseFromSource, parseProgramFromTokens: () => parseProgramFromTokens, printToken: () => printToken, replicaBeginToken: () => replicaBeginToken, replicaEndToken: () => replicaEndToken, sectionToken: () => sectionToken, stringToken: () => stringToken, tagToken: () => tagToken, tagValueToken: () => tagValueToken, validateProgram: () => validateProgram, validateTokens: () => validateTokens }); module.exports = __toCommonJS(index_exports); init_lexer(); // src/parser.ts init_token(); var ParseError = class extends Error { static { __name(this, "ParseError"); } constructor(message) { super(message); this.name = "ParseError"; } }; var lastParserIssues = []; function getParserIssues() { return lastParserIssues; } __name(getParserIssues, "getParserIssues"); function addIssue(sink, issue) { if (sink) { sink.push(issue); return; } lastParserIssues.push(issue); } __name(addIssue, "addIssue"); function parseProgramFromTokens(tokens, issues) { const program = buildAstFromTokens(tokens, issues); return { program, issues: issues ?? getParserIssues() }; } __name(parseProgramFromTokens, "parseProgramFromTokens"); function parseFromSource(source) { const { Lexer: Lexer2 } = (init_lexer(), __toCommonJS(lexer_exports)); const lexer = new Lexer2(source); lexer.process(); const tokens = lexer.getTokens(); return parseProgramFromTokens(tokens); } __name(parseFromSource, "parseFromSource"); function parseAll(source) { const { Lexer: Lexer2 } = (init_lexer(), __toCommonJS(lexer_exports)); const lexer = new Lexer2(source); lexer.process(); const tokens = lexer.getTokens(); const issues = []; const program = buildAstFromTokens(tokens, issues); return { tokens, program, issues }; } __name(parseAll, "parseAll"); var SECTION_HEADER_TOKEN_COUNT = 2; var GOTO_TOKENS_CONSUMED = 2; var buildAstFromTokens = /* @__PURE__ */ __name((allTokens, issues) => { lastParserIssues = []; const sections = []; const sectionIndices = findSectionIndices(allTokens); if (sectionIndices.length === 0) { const body = parseSimpleStatements(allTokens, { collectIssues: true, issues }); sections.push({ name: "main", body, position: getTokenPosition(allTokens[0]), endPosition: getTokenEndPosition(allTokens[allTokens.length - 1]) }); return { sections }; } const prelude = allTokens.slice(0, sectionIndices[0]); const hasPreludeContent = prelude.some( (t) => t.type !== TokenTypes.NEWLINE && t.type !== TokenTypes.COMMENT && t.type !== TokenTypes.COMMENT_CONTENT ); if (hasPreludeContent) { const body = parseSimpleStatements(prelude, { collectIssues: true, issues }); const preludeStartTok = prelude[0] ?? allTokens[0]; const preludeEndTok = allTokens[Math.max(0, sectionIndices[0] - 1)]; sections.push({ name: "main", body, position: getTokenPosition(preludeStartTok), endPosition: getTokenEndPosition(preludeEndTok) }); } for (const idx of sectionIndices) { const sectionTok = allTokens[idx]; const nextIdx = sectionIndices.find((v) => v > idx) ?? allTokens.length; const nameTok = allTokens[idx + 1]; if (!nameTok || nameTok.type !== TokenTypes.IDENTIFIER) { addIssue(issues, { kind: "Error", message: "Expected section name (IDENTIFIER) after SECTION token", position: getTokenPosition(sectionTok), endPosition: getTokenEndPosition(sectionTok) }); continue; } const name = String(nameTok.value ?? ""); const sectionWindow = allTokens.slice( idx + SECTION_HEADER_TOKEN_COUNT, nextIdx ); const body = parseSimpleStatements(sectionWindow, { collectIssues: true, issues }); sections.push({ name, body, position: getTokenPosition(sectionTok), endPosition: getTokenEndPosition(allTokens[nextIdx - 1]) }); } return { sections }; }, "buildAstFromTokens"); var parseSimpleStatements = /* @__PURE__ */ __name((tokens, options = { collectIssues: true }) => { const result = []; let currentIndex = 0; let iterations = 0; const maxIterations = tokens.length * 2; let pendingTags = []; let pendingChoiceTags = []; while (currentIndex < tokens.length && iterations < maxIterations) { const currentToken = tokens[currentIndex]; if (currentToken.type === TokenTypes.TAG) { const { tags, nextStartIndex } = collectLeadingTags( tokens.slice(currentIndex) ); pendingTags.push(...tags); currentIndex += nextStartIndex; if (nextStartIndex === 0) { currentIndex++; console.warn( "Warning: collectLeadingTags returned nextStartIndex=0, forcing increment" ); } iterations++; continue; } if (currentToken.type === TokenTypes.CHOICE_TAG) { const { tags, nextStartIndex } = collectLeadingChoiceTags( tokens.slice(currentIndex) ); pendingChoiceTags.push(...tags); currentIndex += nextStartIndex; if (nextStartIndex === 0) { currentIndex++; console.warn( "Warning: collectLeadingChoiceTags returned nextStartIndex=0, forcing increment" ); } iterations++; continue; } if (currentToken.type === TokenTypes.NEWLINE || currentToken.type === TokenTypes.COMMENT || currentToken.type === TokenTypes.COMMENT_CONTENT) { currentIndex++; iterations++; continue; } if (currentToken.type === TokenTypes.CHOICE) { const { node, nextIndex } = parseChoiceAt( tokens, currentIndex, pendingTags, pendingChoiceTags, options.issues ); result.push(node); pendingTags = []; pendingChoiceTags = []; currentIndex = nextIndex; iterations++; continue; } if (currentToken.type === TokenTypes.GOTO) { const nextToken = tokens[currentIndex + 1]; if (!nextToken || nextToken.type !== TokenTypes.IDENTIFIER || !nextToken.value) { if (options.collectIssues) addIssue(options.issues, { kind: "Error", message: "Goto must be followed by IDENTIFIER", position: getTokenPosition(currentToken), endPosition: getTokenEndPosition(currentToken) }); currentIndex++; iterations++; continue; } result.push({ kind: "Goto", target: String(nextToken.value ?? ""), tags: pendingTags.length ? pendingTags : void 0, position: getTokenPosition(currentToken), endPosition: getTokenEndPosition(nextToken) }); pendingTags = []; currentIndex += GOTO_TOKENS_CONSUMED; iterations++; continue; } if (currentToken.type === TokenTypes.CALL) { const functionName = String(currentToken.value ?? ""); const callArguments = []; let argumentIndex = currentIndex + 1; while (argumentIndex < tokens.length && tokens[argumentIndex].type === TokenTypes.CALL_ARGUMENT) { callArguments.push(String(tokens[argumentIndex].value ?? "")); argumentIndex++; } if (callArguments.length === 0 && tokens[argumentIndex]?.type === TokenTypes.NEWLINE) { if (options.collectIssues) addIssue(options.issues, { kind: "Warning", message: `Empty or malformed call: ${functionName}()`, position: getTokenPosition(currentToken), endPosition: getTokenEndPosition( tokens[Math.max(currentIndex, argumentIndex)] ) }); } result.push({ kind: "Call", name: functionName, args: callArguments, tags: pendingTags.length ? pendingTags : void 0, position: getTokenPosition(currentToken), endPosition: getTokenEndPosition( tokens[Math.max(currentIndex, argumentIndex - 1)] ) }); pendingTags = []; currentIndex = argumentIndex; iterations++; continue; } if (currentToken.type === TokenTypes.REPLICA_BEGIN) { let scanIndex = currentIndex + 1; const startPos = getTokenPosition(currentToken); const stringTokens = []; while (scanIndex < tokens.length && tokens[scanIndex].type !== TokenTypes.REPLICA_END) { const t = tokens[scanIndex]; if (t.type === TokenTypes.STRING) { stringTokens.push(t); } scanIndex++; } const fullText = stringTokens.map((t) => String(t.value ?? "")).join(""); const pieces = buildTextPieces(stringTokens); const segments = splitTextIntoSegmentsWithPositions( fullText, pieces, options.issues ); const endTok = tokens[Math.min(scanIndex, tokens.length - 1)]; const replicaNode = { kind: "Replica", text: segments.filter((s) => s.kind === "Text").map((s) => s.text).join(""), segments: segments.length > 0 ? segments : void 0, tags: pendingTags.length ? pendingTags : void 0, position: startPos, endPosition: getTokenEndPosition(endTok) }; result.push(replicaNode); pendingTags = []; currentIndex = tokens[scanIndex]?.type === TokenTypes.REPLICA_END ? scanIndex + 1 : scanIndex; iterations++; continue; } currentIndex++; iterations++; } if (iterations >= maxIterations) { console.warn("Warning: parseSimpleStatements() exceeded max iterations"); } return result; }, "parseSimpleStatements"); var collectLeadingTags = /* @__PURE__ */ __name((tokens) => { const collected = []; let index = 0; while (index < tokens.length) { const t = tokens[index]; if (t.type !== TokenTypes.TAG) break; const name = String(t.value ?? ""); const maybeValue = tokens[index + 1]; if (maybeValue && maybeValue.type === TokenTypes.TAG_VALUE) { collected.push({ name, value: String(maybeValue.value ?? ""), position: getTokenPosition(t), endPosition: getTokenEndPosition(maybeValue) }); index += 2; continue; } collected.push({ name, position: getTokenPosition(t), endPosition: getTokenEndPosition(t) }); index += 1; } return { tags: collected, nextStartIndex: index }; }, "collectLeadingTags"); var collectLeadingChoiceTags = /* @__PURE__ */ __name((tokens) => { const collected = []; let index = 0; while (index < tokens.length) { const t = tokens[index]; if (t.type !== TokenTypes.CHOICE_TAG) break; const name = String(t.value ?? ""); const maybeValue = tokens[index + 1]; if (maybeValue && maybeValue.type === TokenTypes.TAG_VALUE) { collected.push({ name, value: String(maybeValue.value ?? ""), position: getTokenPosition(t), endPosition: getTokenEndPosition(maybeValue) }); index += 2; continue; } collected.push({ name, position: getTokenPosition(t), endPosition: getTokenEndPosition(t) }); index += 1; } return { tags: collected, nextStartIndex: index }; }, "collectLeadingChoiceTags"); function findSectionIndices(tokens) { const indices = []; for (let tokenIndex = 0; tokenIndex < tokens.length; tokenIndex++) { if (tokens[tokenIndex]?.type === TokenTypes.SECTION) { indices.push(tokenIndex); } } return indices; } __name(findSectionIndices, "findSectionIndices"); function getTokenPosition(token) { if (!token || typeof token.line !== "number" || typeof token.column !== "number") return void 0; return { line: token.line, column: token.column }; } __name(getTokenPosition, "getTokenPosition"); function getTokenEndPosition(token) { if (!token || typeof token.endLine !== "number" || typeof token.endColumn !== "number") return void 0; return { line: token.endLine, column: token.endColumn }; } __name(getTokenEndPosition, "getTokenEndPosition"); function buildTextPieces(stringTokens) { const pieces = []; let offset = 0; for (const t of stringTokens) { const text = String(t.value ?? ""); const start = offset; const end = offset + text.length; pieces.push({ start, end, startPos: getTokenPosition(t), endPos: getTokenEndPosition(t) }); offset = end; } return pieces; } __name(buildTextPieces, "buildTextPieces"); function mapSpanToPositions(start, end, pieces) { let startPos; let endPos; for (const p of pieces) { if (startPos === void 0 && start >= p.start && start <= p.end) { startPos = p.startPos; } if (endPos === void 0 && end >= p.start && end <= p.end) { endPos = p.endPos; } if (startPos && endPos) break; } return { start: startPos, end: endPos }; } __name(mapSpanToPositions, "mapSpanToPositions"); function splitTextIntoSegmentsWithPositions(text, pieces, issues) { const out = []; let globalOffset = 0; while (globalOffset < text.length) { const idx = findNextInlineStart(text, globalOffset); if (idx === -1) { const span2 = mapSpanToPositions(globalOffset, text.length, pieces); if (text.length > globalOffset) { out.push({ kind: "Text", text: text.slice(globalOffset), position: span2.start, endPosition: span2.end }); } break; } if (idx > globalOffset) { const span2 = mapSpanToPositions(globalOffset, idx, pieces); out.push({ kind: "Text", text: text.slice(globalOffset, idx), position: span2.start, endPosition: span2.end }); } let i = idx + "{call:".length; let depth = 0; let inQuote = null; while (i < text.length) { const ch = text[i]; const prev = text[i - 1]; if ((ch === '"' || ch === "'") && prev !== "\\") { inQuote = inQuote ? inQuote === ch ? null : inQuote : ch; } else if (!inQuote) { if (ch === "(" && prev !== "\\") depth++; else if (ch === ")" && prev !== "\\") depth--; else if (ch === "}" && prev !== "\\" && depth <= 0) { i++; break; } } i++; } if (i > text.length) { const span2 = mapSpanToPositions(idx, text.length, pieces); addIssue(issues, { kind: "Warning", message: "Unterminated inline call", position: span2.start, endPosition: span2.end }); out.push({ kind: "Text", text: text.slice(idx), position: span2.start, endPosition: span2.end }); break; } const inlineSrc = text.slice(idx, i); const parsed = parseInlineCallFromText(inlineSrc); const span = mapSpanToPositions(idx, i, pieces); parsed.position = span.start; parsed.endPosition = span.end; out.push(parsed); globalOffset = i; } return out; } __name(splitTextIntoSegmentsWithPositions, "splitTextIntoSegmentsWithPositions"); function findNextInlineStart(text, fromIndex) { for (let i = fromIndex; i <= text.length - 6; i++) { if (text[i] !== "{") continue; let bs = 0; for (let j = i - 1; j >= 0 && text[j] === "\\"; j--) bs++; if (bs % 2 === 1) continue; if (text[i + 1] === "c" && text[i + 2] === "a" && text[i + 3] === "l" && text[i + 4] === "l" && text[i + 5] === ":") { return i; } } return -1; } __name(findNextInlineStart, "findNextInlineStart"); function parseInlineCallFromText(src) { let inner = src.slice( "{call:".length, src.endsWith("}") ? src.length - 1 : src.length ); inner = inner.trim(); let name = ""; const args = []; const parenIndex = inner.indexOf("("); if (parenIndex === -1) { name = inner; } else { name = inner.slice(0, parenIndex); const argStr = inner.slice(parenIndex + 1, inner.lastIndexOf(")")); let i = 0; let buf = ""; let inQuote = null; let depth = 0; while (i < argStr.length) { const ch = argStr[i]; const prev = argStr[i - 1]; if ((ch === '"' || ch === "'") && prev !== "\\") { inQuote = inQuote ? inQuote === ch ? null : inQuote : ch; buf += ch; i++; continue; } if (!inQuote) { if (ch === "(" && prev !== "\\") { depth++; buf += ch; i++; continue; } if (ch === ")" && prev !== "\\") { depth--; buf += ch; i++; continue; } if (ch === "," && prev !== "\\" && depth === 0) { if (buf.trim().length > 0) args.push(buf.trim()); buf = ""; i++; continue; } } buf += ch; i++; } if (buf.trim().length > 0) args.push(buf.trim()); } return { kind: "InlineCall", name, args }; } __name(parseInlineCallFromText, "parseInlineCallFromText"); function parseChoiceAt(tokens, startIndex, pendingTags, pendingChoiceTags, issues) { let index = startIndex + 1; let text; let ric