@kstory/core
Version:
Core parser and lexer for KStory interactive fiction language
1,622 lines (1,617 loc) • 60.2 kB
JavaScript
"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