code:
import * as MDI from 'markdown-it';
import { RuntimeValueType } from './Interpreter';
import { BinaryOperator, binaryOperators, conditionalOperatorPrecedence, UnaryOperator, unaryOperators } from './operators';
import { SimpleIdGenerator } from './SimpleIdGenerator';
import { stdFunctions } from './std';
import { Token, TokenStream } from './TokenStream';
import {
  BinaryExpression,
  BlockExpression,
  BooleanLiteral,
  BreakStatement,
  ChoiceExpression,
  ConditionalExpression,
  ContinueStatement,
  DeclarationStatement,
  ExitAction,
  Expression,
  ExpressionStatement,
  FunctionArgument,
  FunctionExpression,
  GotoAction,
  IfExpression,
  ListExpression,
  NullLiteral,
  NumberLiteral,
  OneVariableDeclaration,
  OptionalLocationInfo,
  RegisterName,
  ReturnStatement,
  Section,
  SelectionAction,
  SetBreakStatement,
  SetReturnStatement,
  SetYieldStatement,
  SingleSectionContent,
  SpreadExpression,
  Statement,
  StringLiteral,
  SwitchCase,
  SwitchExpression,
  TagExpression,
  UnaryExpression,
  VariableReference,
  VariableType,
  WhileExpression,
  WTCDParseResult,
  WTCDRoot,
  YieldStatement,
} from './types';
import { WTCDError } from './WTCDError';

const CURRENT_MAJOR_VERSION = 1;
const CURRENT_MINOR_VERSION = 3;

const CURRENT_VERSION_STR = CURRENT_MAJOR_VERSION + '.' + CURRENT_MINOR_VERSION;

const variableTypes = [
  'null',
  'number',
  'boolean',
  'string',
  'action',
  'choice',
  'selection',
  'list',
  'function',
];

// V1.1 removed selection type and combined it into action
function backwardsCompTypeTransformer(
  variableType: RuntimeValueType | 'selection',
): RuntimeValueType {
  if (variableType === 'selection') {
    return 'action';
  } else {
    return variableType;
  }
}

export interface SimpleLogger {
  info(msg: string): void;
  error(msg: string): void;
  warn(msg: string): void;
}

/**
 * Represents a single lexical scope. Managed by LexicalScopeProvide.
 */
class LexicalScope {
  private declaredVariables: Set<string> = new Set();
  private registers: Set<RegisterName> = new Set();
  private onVariableReferenceNotFoundTriggers: Array<(variableName: string) => void> = [];
  /** Whether this lexical scope contains the given variable */
  public hasVariable(variableName: string) {
    const result = this.declaredVariables.has(variableName);
    if (!result) {
      this.onVariableReferenceNotFoundTriggers.forEach(trigger => trigger(variableName));
    }
    return result;
  }
  /** Add a variable to this lexical scope */
  public addVariable(variableName: string) {
    this.declaredVariables.add(variableName);
  }
  /** Whether this lexical scope contains the given register */
  public hasRegister(registerName: RegisterName) {
    return this.registers.has(registerName);
  }
  /** Add a register to this lexical scope */
  public addRegister(registerName: RegisterName) {
    this.registers.add(registerName);
  }
  public addOnVariableReferenceNotFoundTrigger(trigger: (variableName: string) => void) {
    this.onVariableReferenceNotFoundTriggers.push(trigger);
  }
}

/**
 * Provide lexical context for the parser.
 * Mainly used to resolve lexical scope each variable reference uses.
 */
class LexicalScopeProvider {
  private scopes: Array<LexicalScope> = [];
  public constructor() {
    this.enterScope(); // Std scope
    stdFunctions.forEach(
      stdFunction => this.addVariableToCurrentScope(stdFunction.name),
    );

    this.enterScope(); // Global scope
  }
  private getCurrentScope() {
    return this.scopes[this.scopes.length - 1];
  }
  public enterScope() {
    this.scopes.push(new LexicalScope());
  }
  public exitScope() {
    this.scopes.pop();
  }
  public currentScopeHasVariable(variableName: string) {
    return this.getCurrentScope().hasVariable(variableName);
  }
  public addVariableToCurrentScope(variableName: string) {
    this.getCurrentScope().addVariable(variableName);
  }
  /**
   * Add a callback function when a variable lookup is crossing the boundary of
   * this scope.
   *
   * This is currently used to detect variables that require capture in a
   * closure.
   *
   * @param trigger Callback
   */
  public addOnVariableReferenceNotFoundTriggerToCurrentScope(trigger: (variableName: string) => void) {
    this.getCurrentScope().addOnVariableReferenceNotFoundTrigger(trigger);
  }
  public hasVariableReference(variableName: string): boolean {
    // Use a manual for loop instead of Array.prototype.some to ensure access order
    for (let i = this.scopes.length - 1; i >= 0; i--) {
      if (this.scopes[i].hasVariable(variableName)) {
        return true;
      }
    }
    return false;
  }
  public addRegisterToCurrentScope(registerName: RegisterName) {
    this.getCurrentScope().addRegister(registerName);
  }
  public hasRegister(registerName: RegisterName) {
    return this.scopes.some(scope => scope.hasRegister(registerName));
  }
}

class LogicParser {
  private tokenStream: TokenStream;
  private lexicalScopeProvider = new LexicalScopeProvider();
  private initStatements: Array<Statement> = [];
  private postChecks: Array<() => void | never> = [];
  private sections: Array<Section> = [];
  private rootDeclarations: Set<string> = new Set();
  public constructor(
    source: string,
    private readonly logger: SimpleLogger,
    private readonly sourceMap: boolean,
  ) {
    this.tokenStream = new TokenStream(source);
  }

  private attachLocationInfo<T extends OptionalLocationInfo>(token: OptionalLocationInfo | undefined, target: T) {
    if (this.sourceMap) {
      if (token === undefined) {
        return this.tokenStream.throwUnexpectedNext();
      }
      target.line = token.line;
      target.column = token.column;
    }
    return target;
  }

  private parseVariableType(): VariableType {
    if (!this.tokenStream.isNext('keyword', variableTypes)) {
      return null;
    }
    const types: Array<RuntimeValueType> = [];
    do {
      types.push(backwardsCompTypeTransformer(
        this.tokenStream.next().content as RuntimeValueType,
      ));
    } while (this.tokenStream.isNext('keyword', variableTypes));
    return types;
  }

  private parseChoice() {
    const choiceToken = this.tokenStream.assertAndSkipNext('keyword', 'choice');
    const text = this.parseExpression();
    const action = this.parseExpression();
    return this.attachLocationInfo<ChoiceExpression>(choiceToken, {
      type: 'choiceExpression',
      text,
      action,
    });
  }

  private parseSectionName: () => string = () => {
    const targetToken = this.tokenStream.assertAndSkipNext('identifier');
    this.postChecks.push(() => {
      if (!this.sections.some(section => section.name === targetToken.content)) {
        throw WTCDError.atLocation(targetToken, `Unknown section "${targetToken.content}"`);
      }
    });
    return targetToken.content;
  }

  private parseGotoAction(): GotoAction {
    const gotoToken = this.tokenStream.assertAndSkipNext('keyword', 'goto');
    return this.attachLocationInfo<GotoAction>(gotoToken, {
      type: 'gotoAction',
      sections: this.parseListOrSingleElement(
        this.parseSectionName,

        // Allow zero elements because we allow actions like goto []
        false,
      ),
    });
  }

  private parseList() {
    const leftBracket = this.tokenStream.assertAndSkipNext('punctuation', '[');
    const elements: Array<Expression | SpreadExpression> = [];
    while (!this.tokenStream.isNext('punctuation', ']')) {
      if (this.tokenStream.isNext('operator', '...')) {
        elements.push(this.attachLocationInfo<SpreadExpression>(
          this.tokenStream.next(), {
            type: 'spread',
            expression: this.parseExpression(),
          },
        ));
      } else {
        elements.push(this.parseExpression());
      }
    }
    this.tokenStream.assertAndSkipNext('punctuation', ']');
    return this.attachLocationInfo<ListExpression>(leftBracket, {
      type: 'list',
      elements,
    });
  }

  private parseFunctionCore(isFull: boolean): FunctionExpression {
    const functionArguments: Array<FunctionArgument> = [];
    this.lexicalScopeProvider.enterScope();
    this.lexicalScopeProvider.addRegisterToCurrentScope('return');
    const capturesSet: Set<string> = new Set();
    this.lexicalScopeProvider
      .addOnVariableReferenceNotFoundTriggerToCurrentScope(
        variableName => capturesSet.add(variableName),
      );
    let restArgName: string | null = null;
    let expression: null | Expression = null;
    // If is full, parameter list is mandatory
    if (isFull || this.tokenStream.isNext('punctuation', '[')) {
      // Function switch
      if (this.tokenStream.isNext('keyword', 'switch')) {
        const switchToken = this.tokenStream.assertAndSkipNext(
          'keyword',
          'switch',
        );
        functionArguments.push(this.attachLocationInfo<FunctionArgument>(
          switchToken, {
            type: null,
            name: '$switch',
            defaultValue: null,
          },
        ));
        expression = this.parseSwitchCore(
          switchToken,
          this.attachLocationInfo<VariableReference>(switchToken, {
            type: 'variableReference',
            variableName: '$switch',
          }),
        );
      } else {
        this.tokenStream.assertAndSkipNext('punctuation', '[');
        const usedArgumentNames: Set<string> = new Set();
        while (!this.tokenStream.isNext('punctuation', ']')) {
          if (this.tokenStream.isNext('operator', '...')) {
            this.tokenStream.next(); // Skip over ...
            // Rest arguments
            const restArgToken = this.tokenStream.assertAndSkipNext('identifier');
            if (usedArgumentNames.has(restArgToken.content)) {
              throw WTCDError.atLocation(restArgToken, `Argument ` +
                `"${restArgToken.content}" already existed.`);
            }
            restArgName = restArgToken.content;
            break; // Stop reading any more arguments
          }
          const argType = this.parseVariableType();
          const argNameToken = this.tokenStream.assertAndSkipNext('identifier');

          if (usedArgumentNames.has(argNameToken.content)) {
            throw WTCDError.atLocation(argNameToken, `Argument ` +
              `"${argNameToken.content}" already existed.`);
          }
          usedArgumentNames.add(argNameToken.content);

          let defaultValue: null | Expression = null;
          if (this.tokenStream.isNext('operator', '=')) {
            this.tokenStream.next();
            defaultValue = this.parseExpression();
          }
          functionArguments.push(this.attachLocationInfo<FunctionArgument>(
            argNameToken, {
              type: argType,
              name: argNameToken.content,
              defaultValue,
            },
          ));
        }
        this.tokenStream.assertAndSkipNext('punctuation', ']');
      }
    }
    functionArguments.forEach(
      argument => this.lexicalScopeProvider.addVariableToCurrentScope(
        argument.name,
      ),
    );
    if (restArgName !== null) {
      this.lexicalScopeProvider.addVariableToCurrentScope(restArgName);
    }
    if (expression === null) {
      expression = this.parseExpression();
    }
    this.lexicalScopeProvider.exitScope();
    return {
      type: 'function',
      arguments: functionArguments,
      restArgName,
      captures: Array.from(capturesSet),
      expression,
    };
  }

  // function [...] ...
  private parseFullFunctionExpression() {
    return this.attachLocationInfo<FunctionExpression>(
      this.tokenStream.assertAndSkipNext('keyword', 'function'),
      this.parseFunctionCore(true),
    );
  }

  private parseShortFunctionExpression() {
    return this.attachLocationInfo<FunctionExpression>(
      this.tokenStream.assertAndSkipNext('punctuation', '$'),
      this.parseFunctionCore(false),
    );
  }

  private parseSwitchCore(switchToken: Token, expression: Expression) {
    this.tokenStream.assertAndSkipNext('punctuation', '[');
    const cases: Array<SwitchCase> = [];
    let defaultCase: Expression | null = null;
    while (!this.tokenStream.isNext('punctuation', ']')) {
      const matches = this.parseExpression();
      if (this.tokenStream.isNext('punctuation', ']')) {
        // Default case
        defaultCase = matches;
        break;
      }
      const returns = this.parseExpression();
      cases.push({ matches, returns });
    }
    this.tokenStream.assertAndSkipNext('punctuation', ']');
    return this.attachLocationInfo<SwitchExpression>(switchToken, {
      type: 'switch',
      expression,
      cases,
      defaultCase,
    });
  }

  private parseSwitchExpression() {
    const switchToken = this.tokenStream.assertAndSkipNext('keyword', 'switch');
    const expression = this.parseExpression();
    return this.parseSwitchCore(switchToken, expression);
  }

  private parseWhileExpression(): WhileExpression {
    const mainToken = this.tokenStream.peek();
    this.lexicalScopeProvider.enterScope();
    this.lexicalScopeProvider.addRegisterToCurrentScope('break');
    let preExpr: Expression | null = null;
    if (this.tokenStream.isNext('keyword', 'do')) {
      this.tokenStream.next();
      preExpr = this.parseExpression();
    }
    this.tokenStream.assertAndSkipNext('keyword', 'while');
    const condition = this.parseExpression();
    let postExpr: Expression | null = null;
    if (preExpr === null) {
      postExpr = this.parseExpression();
    } else if (this.tokenStream.isNext('keyword', 'then')) {
      this.tokenStream.next();
      postExpr = this.parseExpression();
    }
    this.lexicalScopeProvider.exitScope();
    return this.attachLocationInfo<WhileExpression>(mainToken, {
      type: 'while',
      preExpr,
      condition,
      postExpr,
    });
  }

  private parseIfExpression(): IfExpression {
    const ifToken = this.tokenStream.assertAndSkipNext('keyword', 'if');
    const condition = this.parseExpression();
    const then = this.parseExpression();
    let otherwise: Expression | null = null;
    if (this.tokenStream.isNext('keyword', 'else')) {
      this.tokenStream.next();
      otherwise = this.parseExpression();
    }
    return this.attachLocationInfo<IfExpression>(ifToken, {
      type: 'if',
      condition,
      then,
      otherwise,
    });
  }

  /**
   * Try to parse an atom node, which includes:
   * - number literals
   * - string literals
   * - boolean literals
   * - nulls
   * - selection
   * - choices
   * - goto actions
   * - exit actions
   * - groups
   * - lists
   * - functions
   * - variables
   * - switches
   * - while loops
   * - if
   * - tags
   * - block expressions
   * - unary expressions
   *
   * @param softFail If true, when parsing failed, null is returned instead of
   * throwing error.
   */
  private parseAtom(): Expression;
  private parseAtom(softFail: true): Expression | null;
  private parseAtom(softFail?: true): Expression | null {
    // Number literal
    if (this.tokenStream.isNext('number')) {
      return this.attachLocationInfo<NumberLiteral>(this.tokenStream.peek(), {
        type: 'numberLiteral',
        value: Number(this.tokenStream.next().content),
      });
    }

    // String literal
    if (this.tokenStream.isNext('string')) {
      return this.attachLocationInfo<StringLiteral>(this.tokenStream.peek(), {
        type: 'stringLiteral',
        value: this.tokenStream.next().content,
      });
    }

    // Boolean literal
    if (this.tokenStream.isNext('keyword', ['true', 'false'])) {
      return this.attachLocationInfo<BooleanLiteral>(this.tokenStream.peek(), {
        type: 'booleanLiteral',
        value: this.tokenStream.next().content === 'true',
      });
    }

    // Null
    if (this.tokenStream.isNext('keyword', 'null')) {
      return this.attachLocationInfo<NullLiteral>(this.tokenStream.next(), {
        type: 'nullLiteral',
      });
    }

    // Selection
    if (this.tokenStream.isNext('keyword', 'selection')) {
      return this.attachLocationInfo<SelectionAction>(this.tokenStream.next(), {
        type: 'selection',
        choices: this.parseExpression(),
      });
    }

    // Choice
    if (this.tokenStream.isNext('keyword', 'choice')) {
      return this.parseChoice();
    }

    // Goto actions
    if (this.tokenStream.isNext('keyword',  'goto')) {
      return this.parseGotoAction();
    }

    // Exit actions
    if (this.tokenStream.isNext('keyword', 'exit')) {
      return this.attachLocationInfo<ExitAction>(this.tokenStream.next(), {
        type: 'exitAction',
      });
    }

    // Group
    if (this.tokenStream.isNext('punctuation', '(')) {
      this.tokenStream.next();
      const result = this.parseExpression();
      this.tokenStream.assertAndSkipNext('punctuation', ')');
      return result;
    }

    // List
    if (this.tokenStream.isNext('punctuation', '[')) {
      return this.parseList();
    }

    // Function
    if (this.tokenStream.isNext('keyword', 'function')) {
      return this.parseFullFunctionExpression();
    }

    // Short function
    if (this.tokenStream.isNext('punctuation', '$')) {
      return this.parseShortFunctionExpression();
    }

    // Switch
    if (this.tokenStream.isNext('keyword', 'switch')) {
      return this.parseSwitchExpression();
    }

    // While
    if (this.tokenStream.isNext('keyword', ['while', 'do'])) {
      return this.parseWhileExpression();
    }

    // If
    if (this.tokenStream.isNext('keyword', 'if')) {
      return this.parseIfExpression();
    }

    // Tag
    if (this.tokenStream.isNext('tag')) {
      return this.attachLocationInfo<TagExpression>(
        this.tokenStream.peek(),
        { type: 'tag', name: this.tokenStream.next().content },
      );
    }

    // Block expression
    if (this.tokenStream.isNext('punctuation', '{')) {
      return this.parseBlockExpression();
    }

    // Variable
    if (this.tokenStream.isNext('identifier')) {
      const identifierToken = this.tokenStream.next();
      if (!this.lexicalScopeProvider.hasVariableReference(identifierToken.content)) {
        throw WTCDError.atLocation(
          identifierToken,
          `Cannot locate lexical scope for variable "${identifierToken.content}"`,
        );
      }
      return this.attachLocationInfo<VariableReference>(identifierToken, {
        type: 'variableReference',
        variableName: identifierToken.content,
      });
    }

    // Unary
    if (this.tokenStream.isNext('operator')) {
      const operatorToken = this.tokenStream.next();
      if (!unaryOperators.has(operatorToken.content)) {
        throw WTCDError.atLocation(
          operatorToken,
          `Invalid unary operator: ${operatorToken.content}`,
        );
      }
      return this.attachLocationInfo<UnaryExpression>(operatorToken, {
        type: 'unaryExpression',
        operator: operatorToken.content as UnaryOperator,
        arg: this.parseExpression(
          this.parseAtom(),
          unaryOperators.get(operatorToken.content)!.precedence,
        ),
      });
    }
    if (softFail === true) {
      return null;
    } else {
      return this.tokenStream.throwUnexpectedNext('atom');
    }
  }

  private parseBlockExpression() {
    const firstBraceToken = this.tokenStream.assertAndSkipNext('punctuation', '{');
    this.lexicalScopeProvider.enterScope();
    this.lexicalScopeProvider.addRegisterToCurrentScope('yield');
    const expressions: Array<Statement> = [];
    while (!this.tokenStream.isNext('punctuation', '}')) {
      expressions.push(this.parseStatement());
    }
    this.lexicalScopeProvider.exitScope();
    this.tokenStream.assertAndSkipNext('punctuation', '}');
    return this.attachLocationInfo<BlockExpression>(firstBraceToken, {
      type: 'block',
      statements: expressions,
    });
  }

  private assertHasRegister(registerName: RegisterName, token: Token) {
    if (!this.lexicalScopeProvider.hasRegister(registerName)) {
      throw WTCDError.atLocation(
        token,
        `Cannot locate lexical scope for ${registerName} register`,
      );
    }
  }

  private parseYieldOrSetYieldExpression(): YieldStatement | SetYieldStatement {
    const yieldToken = this.tokenStream.assertAndSkipNext('keyword', 'yield');
    this.assertHasRegister('yield', yieldToken);
    if (this.tokenStream.isNext('operator', '=')) {
      // Set yield
      this.tokenStream.next();
      return this.attachLocationInfo<SetYieldStatement>(yieldToken, {
        type: 'setYield',
        value: this.parseExpression(),
      });
    } else {
      // Yield
      return this.attachLocationInfo<YieldStatement>(yieldToken, {
        type: 'yield',
        value: this.parseExpressionImpliedNull(yieldToken),
      });
    }
  }

  private parseReturnOrSetReturnStatement(): ReturnStatement | SetReturnStatement {
    const returnToken = this.tokenStream.assertAndSkipNext('keyword', 'return');
    this.assertHasRegister('return', returnToken);
    if (this.tokenStream.isNext('operator', '=')) {
      // Set return
      this.tokenStream.next();
      return this.attachLocationInfo<SetReturnStatement>(returnToken, {
        type: 'setReturn',
        value: this.parseExpression(),
      });
    } else {
      // Return
      return this.attachLocationInfo<ReturnStatement>(returnToken, {
        type: 'return',
        value: this.parseExpressionImpliedNull(returnToken),
      });
    }
  }

  private parseBreakOrSetBreakStatement(): BreakStatement | SetBreakStatement {
    const breakToken = this.tokenStream.assertAndSkipNext('keyword', 'break');
    this.assertHasRegister('break', breakToken);
    if (this.tokenStream.isNext('operator', '=')) {
      // Set return
      this.tokenStream.next();
      return this.attachLocationInfo<SetBreakStatement>(breakToken, {
        type: 'setBreak',
        value: this.parseExpression(),
      });
    } else {
      // Return
      return this.attachLocationInfo<BreakStatement>(breakToken, {
        type: 'break',
        value: this.parseExpressionImpliedNull(breakToken),
      });
    }
  }

  private parseContinueStatement(): ContinueStatement {
    return this.attachLocationInfo<ContinueStatement>(this.tokenStream.next(), {
      type: 'continue',
    });
  }

  private parseExpressionImpliedNull(location: OptionalLocationInfo) {
    const atom = this.parseAtom(true);
    if (atom === null) {
      return this.attachLocationInfo<NullLiteral>(location, { type: 'nullLiteral' });
    }
    return this.parseExpression(atom, 0);
  }

  /**
   * - If next token is not an operator, return left as is.
   * - If next operator's precedence is smaller than or equal to the precedence
   *   threshold, return left as is. (Because in this case, we want to left
   *   someone on the left wrap us.)
   * - Otherwise, for binary operators call #maybeInfixExpression with next
   *   element as left and new operator's precedence as the threshold. (This is
   *   to bind everything that binds tighter (higher precedence) than this
   *   operator together.) Using this and current precedence threshold, call
   *   #maybeInfixExpression again. (This is to bind everything that reaches the
   *   threshold together.)
   * - For conditional operator (?), parse a expression (between "?" and ":"),
   *   then read in ":". Last, do the same thing as with the binary case.
   * @param left expression on the left of next operator
   * @param precedenceMin minimum (exclusive) precedence required in order
   * to bind with left
   * @param precedenceMax minimum (inclusive) precedence required in order
   * to bind with left
   */
  private parseExpression: (
    left?: Expression,
    precedenceMin?: number,
    precedenceMax?: number,
  ) => Expression = (
    left = this.parseAtom(),
    precedenceMin = 0,
    precedenceMax = Infinity,
  ) => {
    if (!this.tokenStream.isNext('operator')) {
      return left;
    }
    const operatorToken = this.tokenStream.peek()!;
    const isConditional = operatorToken.content === '?';
    if (!isConditional && !binaryOperators.has(operatorToken.content)) {
      return left;
    }
    const nextPrecedence = isConditional
      ? conditionalOperatorPrecedence
      : binaryOperators.get(operatorToken.content)!.precedence;
    if (nextPrecedence <= precedenceMin || (isConditional && nextPrecedence > precedenceMax)) {
      return left;
    }
    this.tokenStream.next(); // Read in operator
    if (isConditional) {
      // Implementation here might contain bug. It works for all my cases though.
      const then = this.parseExpression();
      this.tokenStream.assertAndSkipNext('operator', ':');
      const otherwise = this.parseExpression(this.parseAtom(), precedenceMin, nextPrecedence);
      const conditional = this.attachLocationInfo<ConditionalExpression>(operatorToken, {
        type: 'conditionalExpression',
        condition: left,
        then,
        otherwise,
      });
      return this.parseExpression(conditional, precedenceMin, precedenceMax);
    } else {
      const right = this.parseExpression(this.parseAtom(), nextPrecedence, precedenceMax);
      const binary = this.attachLocationInfo<BinaryExpression>(operatorToken, {
        type: 'binaryExpression',
        operator: operatorToken.content as BinaryOperator,
        arg0: left,
        arg1: right,
      });
      return this.parseExpression(binary, precedenceMin, precedenceMax);
    }
  }

  /**
   * Parse a single expression, which includes:
   * - unary expressions
   * - declare expressions
   * - infix expressions (binary and conditional)
   */
  private parseStatement: () => Statement = () => {
    if (this.tokenStream.isNext('keyword', 'declare')) {
      return this.parseDeclaration();
    } else if (this.tokenStream.isNext('keyword', 'return')) {
      return this.parseReturnOrSetReturnStatement();
    } else if (this.tokenStream.isNext('keyword', 'yield')) {
      return this.parseYieldOrSetYieldExpression();
    } else if (this.tokenStream.isNext('keyword', 'break')) {
      return this.parseBreakOrSetBreakStatement();
    } else if (this.tokenStream.isNext('keyword', 'continue')) {
      return this.parseContinueStatement();
    } else {
      return this.attachLocationInfo<ExpressionStatement>(this.tokenStream.peek(), {
        type: 'expression',
        expression: this.parseExpression(),
      });
    }
  }

  private parseOneDeclaration: () => OneVariableDeclaration = () => {
    const type = this.parseVariableType();
    const variableNameToken = this.tokenStream.assertAndSkipNext('identifier');
    let initialValue: Expression | null = null;
    if (this.tokenStream.isNext('operator', '=')) {
      // Explicit initialization (number a = 123)
      this.tokenStream.next();
      initialValue = this.parseExpression();
    } else if (
      (
        (this.tokenStream.isNext('punctuation', '[')) ||
        (this.tokenStream.isNext('keyword', 'switch'))
      ) &&
      (type !== null) &&
      (type.length === 1) &&
      (type[0] === 'function')
    ) {
      // Simplified function declaration (declare function test[ ... ] ...)
      initialValue = this.attachLocationInfo<FunctionExpression>(
        variableNameToken,
        this.parseFunctionCore(true),
      );
    }
    if (this.lexicalScopeProvider.currentScopeHasVariable(variableNameToken.content)) {
      throw WTCDError.atLocation(
        variableNameToken,
        `Variable "${variableNameToken.content}" has already been declared within the same lexical scope`,
      );
    }
    this.lexicalScopeProvider.addVariableToCurrentScope(variableNameToken.content);
    return this.attachLocationInfo<OneVariableDeclaration>(variableNameToken, {
      variableName: variableNameToken.content,
      variableType: type,
      initialValue,
    });
  }

  private parseListOrSingleElement<T>(parseOneFn: () => T, atLestOne: boolean = true): Array<T> {
    if (this.tokenStream.isNext('punctuation', '[')) {
      const results: Array<T> = [];
      this.tokenStream.next(); // [
      while (!this.tokenStream.isNext('punctuation', ']')) {
        results.push(parseOneFn());
      }
      if (atLestOne && results.length === 0) {
        return this.tokenStream.throwUnexpectedNext('at least one element');
      }
      this.tokenStream.next(); // ]
      return results;
    } else {
      return [parseOneFn()];
    }
  }

  private parseDeclaration() {
    const declareToken = this.tokenStream.assertAndSkipNext('keyword', 'declare');
    const declarations = this.parseListOrSingleElement(this.parseOneDeclaration);
    return this.attachLocationInfo<DeclarationStatement>(declareToken, {
      type: 'declaration',
      declarations,
    });
  }

  private parseSection() {
    const sectionToken = this.tokenStream.assertAndSkipNext('keyword', 'section');
    const nameToken = this.tokenStream.assertAndSkipNext('identifier');
    if (this.sections.some(section => section.name === nameToken.content)) {
      throw WTCDError.atLocation(nameToken, `Cannot redefine section "${nameToken.content}"`);
    }
    let executes: Expression | null = null;
    if (!this.tokenStream.isNext('keyword', 'then')) {
      executes = this.parseExpression();
    }
    this.tokenStream.assertAndSkipNext('keyword', 'then');
    const then = this.parseExpression();
    return this.attachLocationInfo<Section>(sectionToken, {
      name: nameToken.content,
      executes,
      then,
      content: [],
    });
  }

  private parseRootBlock() {
    this.tokenStream.assertNext('keyword', ['declare', 'section']);
    if (this.tokenStream.isNext('keyword', 'declare')) {
      const declarationStatement = this.parseDeclaration();
      for (const declaration of declarationStatement.declarations) {
        this.rootDeclarations.add(declaration.variableName);
      }
      this.initStatements.push(declarationStatement);
    } else if (this.tokenStream.isNext('keyword', 'section')) {
      this.sections.push(this.parseSection());
    }
  }

  public hasRootDeclaration(variableName: string) {
    return this.rootDeclarations.has(variableName);
  }

  /**
   * Read the WTCD version declaration and verify version
   * Give warning when needed.
   */
  private parseVersion() {
    this.tokenStream.assertAndSkipNext('identifier', 'WTCD');
    const versionToken = this.tokenStream.assertAndSkipNext('number');
    const versionContent = versionToken.content;
    const split = versionContent.split('.');
    if (split.length !== 2) {
      throw WTCDError.atLocation(versionToken, `Invalid WTCD version ${versionContent}`);
    }
    const majorVersion = Number(split[0]);
    const minorVersion = Number(split[1]);
    if (
      (majorVersion > CURRENT_MAJOR_VERSION) ||
      (majorVersion === CURRENT_MAJOR_VERSION && minorVersion > CURRENT_MINOR_VERSION)
    ) {
      // If version stated is larger
      this.logger.warn(`Document's WTCD version (${versionContent}) is newer than parser ` +
        `version (${CURRENT_VERSION_STR}). New features might break parser.`);
    } else if (majorVersion < CURRENT_MAJOR_VERSION) {
      this.logger.warn(`Document's WTCD version (${versionContent}) is a least one major ` +
        `version before parser's (${CURRENT_VERSION_STR}). Breaking changes introduced might break ` +
        `parser.`);
    }
  }

  public parse() {
    const logger = this.logger;
    logger.info('Parsing logic section...');
    this.parseVersion();
    while (!this.tokenStream.eof()) {
      this.parseRootBlock();
    }
    logger.info('Run post checks...');
    this.postChecks.forEach(postCheck => postCheck());
    return {
      initStatements: this.initStatements,
      sections: this.sections,
    };
  }
}

function identity<T>(input: T) {
  return input;
}

/**
 * Parse the given WTCD document.
 */
export function parse({ source, mdi, sourceMap = false, logger = console, markdownPreProcessor = identity, htmlPostProcessor = identity }: {
  source: string;
  mdi: MDI;
  sourceMap?: boolean;
  logger?: SimpleLogger;
  markdownPreProcessor?: (markdown: string) => string;
  htmlPostProcessor?: (html: string) => string;
}): WTCDParseResult {
  try {
    // Parse sections and extract the logic part of this WTCD document

    /** Regex used to extract section headers. */
    const sectionRegex = /^---<<<\s+([a-zA-Z_][a-zA-Z_0-9]*)(?:@([0-9\-]+))?\s+>>>---$/gm;
    /** Markdown source of each section */
    const sectionMarkdowns: Array<string> = [];
    /** Name of each section, excluding bounds */
    const sectionNames: Array<string> = [];
    /** Bounds of each section, not parsed. Undefined means unbounded. */
    const sectionBounds: Array<string | undefined> = [];
    /** End index of last section */
    let lastIndex = 0;
    /** Rolling regex result object */
    let result: RegExpExecArray | null;
    while ((result = sectionRegex.exec(source)) !== null) {
      sectionMarkdowns.push(source.substring(lastIndex, result.index));
      sectionNames.push(result[1]);
      sectionBounds.push(result[2]);
      lastIndex = sectionRegex.lastIndex;
    }

    // Push the remaining content to last section's markdown
    sectionMarkdowns.push(source.substring(lastIndex));

    const logicParser = new LogicParser(
      sectionMarkdowns.shift()!,
      logger,
      sourceMap,
    );

    const wtcdRoot: WTCDRoot = logicParser.parse();

    const sig = new SimpleIdGenerator();

    sectionContentLoop:
    for (let i = 0; i < sectionMarkdowns.length; i++) {
      /** Markdown content of the section */
      const sectionMarkdown = markdownPreProcessor(sectionMarkdowns[i]);
      /** Name (without bound) of the section */
      const sectionName = sectionNames[i];
      /** Unparsed bounds of the section */
      const sectionBound = sectionBounds[i];
      const sectionFullName = (sectionBound === undefined)
        ? sectionName
        : `${sectionName}@${sectionBound}`;
      const variables: Array<{ elementClass: string, variableName: string }> = [];

      const sectionHTML = mdi.render(sectionMarkdown);

      /**
       * HTML content of the section whose interpolated values are converted to
       * spans with unique classes.
       */
      const sectionParameterizedHTML = sectionHTML.replace(/<\$\s+([a-zA-Z_][a-zA-Z_0-9]*)\s+\$>/g, (_, variableName) => {
        if (!logicParser.hasRootDeclaration(variableName)) {
          throw WTCDError.atUnknown(`Cannot resolve variable reference "${variableName}" in section "${sectionFullName}"`);
        }
        const elementClass = 'wtcd-variable-' + sig.next();
        variables.push({
          elementClass,
          variableName,
        });
        return `<span class="${elementClass}"></span>`;
      });

      // Parse bounds
      let lowerBound: number | undefined;
      let upperBound: number | undefined;
      if (sectionBound !== undefined) {
        if (sectionBound.includes('-')) {
          const split = sectionBound.split('-');
          lowerBound = split[0] === '' ? undefined : Number(split[0]);
          upperBound = split[1] === '' ? undefined : Number(split[1]);
        } else {
          lowerBound = upperBound = Number(sectionBound);
        }
      }
      const singleSectionContent: SingleSectionContent = {
        html: htmlPostProcessor(sectionParameterizedHTML),
        variables,
        lowerBound,
        upperBound,
      };
      for (const section of wtcdRoot.sections) {
        if (section.name === sectionName) {
          section.content.push(singleSectionContent);
          continue sectionContentLoop;
        }
      }
      // Currently, location data for content sections are not available
      throw WTCDError.atUnknown(`Cannot find a logic declaration for ` +
        `section content ${sectionFullName}`);
    }

    return {
      error: false,
      wtcdRoot,
    };
  } catch (error) {
    return {
      error: true,
      message: (error as Error).message,
      internalStack: (error as Error).stack!,
    };
  }
}


code:
import { NativeFunction } from '../types';
import { getMaybePooled } from '../constantsPool';
import { assertArgsLength, assertArgType, NativeFunctionError } from './utils';
import { ChainedCanvas } from '../ChainedCanvas';
import { InterpreterHandle } from '../Interpreter';

function obtainCanvas(interpreterHandle: InterpreterHandle, id: string) {
  const canvas = interpreterHandle.canvases.get(id)!;
  if (canvas === undefined) {
    throw new NativeFunctionError(`Canvas with id="${id}" does not exist.`);
  }
  return canvas;
}

function assertIsHAlign(hAlign: string): asserts hAlign is CanvasTextAlign {
  switch (hAlign) {
    case 'left':
    case 'center':
    case 'right':
    case 'start':
    case 'end':
      break;
    default:
      throw new NativeFunctionError(`Unknown text hAlign: ${hAlign}`);
  }
}

function assertIsVAlign(vAlign: string): asserts vAlign is CanvasTextBaseline {
  switch (vAlign) {
    case 'top':
    case 'hanging':
    case 'middle':
    case 'alphabetic':
    case 'ideographic':
    case 'bottom':
      break;
    default:
      throw new NativeFunctionError(`Unknown text vAlign: ${vAlign}`);
  }
}

export const canvasStdFunctions: Array<NativeFunction> = [
  function canvasCreate(args, interpreterHandle) {
    assertArgsLength(args, 3);
    const id = assertArgType(args, 0, 'string');
    const width = assertArgType(args, 1, 'number');
    const height = assertArgType(args, 2, 'number');
    if (interpreterHandle.canvases.has(id)) {
      throw new NativeFunctionError(`Canvas with id="${id}" already exists`);
    }
    if (width % 1 !== 0) {
      throw new NativeFunctionError(`Width (${width}) has to be an integer`);
    }
    if (width <= 0) {
      throw new NativeFunctionError(`Width (${width}) must be positive`);
    }
    if (height % 1 !== 0) {
      throw new NativeFunctionError(`Height (${height}) has to be an integer`);
    }
    if (height <= 0) {
      throw new NativeFunctionError(`Height (${height}) must be positive`);
    }
    interpreterHandle.canvases.set(id, new ChainedCanvas(width, height));
    return getMaybePooled('null', null);
  },
  function canvasOutput(args, interpreterHandle) {
    const id = assertArgType(args, 0, 'string');
    const canvas = obtainCanvas(interpreterHandle, id);
    const $newCanvas = document.createElement('canvas');
    $newCanvas.width = canvas.getWidth();
    $newCanvas.height = canvas.getHeight();
    $newCanvas.style.maxWidth = '100%';
    interpreterHandle.featureProvider.drawLoadingCanvas($newCanvas);
    interpreterHandle.pushContent($newCanvas);
    canvas.onResolve(() => {
      if (document.body.contains($newCanvas)) {
        const ctx = $newCanvas.getContext('2d')!;
        ctx.clearRect(0, 0, $newCanvas.width, $newCanvas.height);
        ctx.drawImage(canvas.canvas, 0, 0);
      }
    });
    return getMaybePooled('null', null);
  },
  function canvasClear(args, interpreterHandle) {
    const id = assertArgType(args, 0, 'string');
    const canvas = obtainCanvas(interpreterHandle, id);
    canvas.updatePromise(async () => {
      canvas.ctx.clearRect(0, 0, canvas.getWidth(), canvas.getHeight());
    });
    return getMaybePooled('null', null);
  },
  function canvasPutImage(args, interpreterHandle) {
    assertArgsLength(args, 4, 6);
    const id = assertArgType(args, 0, 'string');
    const path = assertArgType(args, 1, 'string');
    const x = assertArgType(args, 2, 'number');
    const y = assertArgType(args, 3, 'number');
    const width = assertArgType(args, 4, 'number', -1);
    const height = assertArgType(args, 5, 'number', -1);
    if ((width < 0) !== (height < 0)) {
      throw new NativeFunctionError('Width and height must be provided at ' +
        'the same time.');
    }
    const canvas = obtainCanvas(interpreterHandle, id);
    canvas.updatePromise(async () => {
      try {
        const image = await interpreterHandle.featureProvider.loadImage(path);
        if (width < 0) {
          canvas.ctx.drawImage(image, x, y);
        } else {
          canvas.ctx.drawImage(image, x, y, width, height);
        }
      } catch (error) {
        console.error(`WTCD failed to load image with path="${path}".`, error);
      }
    });
    return getMaybePooled('null', null);
  },
  function canvasPutImagePart(args, interpreterHandle) {
    assertArgsLength(args, 8, 10);
    const id = assertArgType(args, 0, 'string');
    const path = assertArgType(args, 1, 'string');
    const sourceX = assertArgType(args, 2, 'number');
    const sourceY = assertArgType(args, 3, 'number');
    const sourceWidth = assertArgType(args, 4, 'number');
    const sourceHeight = assertArgType(args, 5, 'number');
    const destX = assertArgType(args, 6, 'number');
    const destY = assertArgType(args, 7, 'number');
    const destWidth = assertArgType(args, 8, 'number', -1);
    const destHeight = assertArgType(args, 9, 'number', -1);
    if ((destWidth < 0) !== (destHeight < 0)) {
      throw new NativeFunctionError('destWidth and destHeight must be ' +
        'provided at the same time.');
    }
    const canvas = obtainCanvas(interpreterHandle, id);
    canvas.updatePromise(async () => {
      try {
        const image = await interpreterHandle.featureProvider.loadImage(path);
        if (destWidth < 0) {
          canvas.ctx.drawImage(
            image,
            sourceX, sourceY,
            sourceWidth, sourceHeight,
            destX, destY,
            sourceWidth, sourceHeight
          );
        } else {
          canvas.ctx.drawImage(
            image,
            sourceX, sourceY,
            sourceWidth, sourceHeight,
            destX, destY,
            destWidth, destHeight
          );
        }
      } catch (error) {
        console.error(`WTCD failed to load image with path="${path}".`, error);
      }
    });
    return getMaybePooled('null', null);
  },
  function canvasSetFont(args, interpreterHandle) {
    assertArgsLength(args, 3);
    const id = assertArgType(args, 0, 'string');
    const size = assertArgType(args, 1, 'number');
    const fontIdentifier = assertArgType(args, 2, 'string');
    const canvas = obtainCanvas(interpreterHandle, id);
    canvas.updatePromise(async () => {
      try {
        const fontName = await interpreterHandle.featureProvider
          .loadFont(fontIdentifier);
        canvas.ctx.font = `${size}px ${fontName}`;
      } catch (error) {
        console.error(`WTCD failed to load font with ` +
          `identifier="${fontIdentifier}".`, error);
      }
    });
    return getMaybePooled('null', null);
  },
  function canvasSetFillStyle(args, interpreterHandle) {
    assertArgsLength(args, 2);
    const id = assertArgType(args, 0, 'string');
    const color = assertArgType(args, 1, 'string');
    const canvas = obtainCanvas(interpreterHandle, id);
    canvas.updatePromise(async () => {
      canvas.ctx.fillStyle = color;
    });
    return getMaybePooled('null', null);
  },
  function canvasFillText(args, interpreterHandle) {
    assertArgsLength(args, 4, 6);
    const id = assertArgType(args, 0, 'string');
    const text = assertArgType(args, 1, 'string');
    const x = assertArgType(args, 2, 'number');
    const y = assertArgType(args, 3, 'number');
    const hAlign = assertArgType(args, 4, 'string', 'start');
    const vAlign = assertArgType(args, 5, 'string', 'alphabetic');
    const canvas = obtainCanvas(interpreterHandle, id);
    assertIsHAlign(hAlign);
    assertIsVAlign(vAlign);
    canvas.updatePromise(async () => {
      const ctx = canvas.ctx;
      ctx.textAlign = hAlign;
      ctx.textBaseline = vAlign;
      ctx.fillText(text, x, y);
    });
    return getMaybePooled('null', null);
  },
  function canvasFillRect(args, interpreterHandle) {
    assertArgsLength(args, 5);
    const id = assertArgType(args, 0, 'string');
    const x = assertArgType(args, 1, 'number');
    const y = assertArgType(args, 2, 'number');
    const width = assertArgType(args, 3, 'number');
    const height = assertArgType(args, 4, 'number');
    const canvas = obtainCanvas(interpreterHandle, id);
    canvas.updatePromise(async () => {
      canvas.ctx.fillRect(x, y, width, height);
    });
    return getMaybePooled('null', null);
  },
  function canvasSetStrokeStyle(args, interpreterHandle) {
    assertArgsLength(args, 2);
    const id = assertArgType(args, 0, 'string');
    const color = assertArgType(args, 1, 'string');
    const canvas = obtainCanvas(interpreterHandle, id);
    canvas.updatePromise(async () => {
      canvas.ctx.strokeStyle = color;
    });
    return getMaybePooled('null', null);
  },
  function canvasSetLineWidth(args, interpreterHandle) {
    assertArgsLength(args, 2);
    const id = assertArgType(args, 0, 'string');
    const lineWidth = assertArgType(args, 1, 'number');
    const canvas = obtainCanvas(interpreterHandle, id);
    canvas.updatePromise(async () => {
      canvas.ctx.lineWidth = lineWidth;
    });
    return getMaybePooled('null', null);
  },
  function canvasStrokeText(args, interpreterHandle) {
    assertArgsLength(args, 4, 6);
    const id = assertArgType(args, 0, 'string');
    const text = assertArgType(args, 1, 'string');
    const x = assertArgType(args, 2, 'number');
    const y = assertArgType(args, 3, 'number');
    const hAlign = assertArgType(args, 4, 'string', 'start');
    const vAlign = assertArgType(args, 5, 'string', 'alphabetic');
    const canvas = obtainCanvas(interpreterHandle, id);
    assertIsHAlign(hAlign);
    assertIsVAlign(vAlign);
    canvas.updatePromise(async () => {
      const ctx = canvas.ctx;
      ctx.textAlign = hAlign;
      ctx.textBaseline = vAlign;
      ctx.lineJoin = 'round';
      ctx.lineCap = 'round';
      ctx.strokeText(text, x, y);
    });
    return getMaybePooled('null', null);
  },
];

license: unknown


code:
import { getMaybePooled } from '../constantsPool';
import { describe } from '../Interpreter';
import { NativeFunction } from '../types';
import { assertArgsLength, assertArgType, NativeFunctionError } from './utils';

export const contentStdFunctions: Array<NativeFunction> = [
  function contentAddParagraph(args, interpreterHandle) {
    assertArgsLength(args, 1);
    const $paragraph = document.createElement('p');
    $paragraph.innerText = assertArgType(args, 0, 'string');
    interpreterHandle.pushContent($paragraph);
    return getMaybePooled('null', null);
  },
  function contentAddImage(args, interpreterHandle) {
    assertArgsLength(args, 1);
    const $image = document.createElement('img');
    $image.src = assertArgType(args, 0, 'string');
    interpreterHandle.pushContent($image);
    return getMaybePooled('null', null);
  },
  function contentAddUnorderedList(args, interpreterHandle) {
    const $container = document.createElement('ul');
    for (let i = 0; i < args.length; i++) {
      const content = assertArgType(args, i, 'string');
      const $li = document.createElement('li');
      $li.innerText = content;
      $container.appendChild($li);
    }
    interpreterHandle.pushContent($container);
    return getMaybePooled('null', null);
  },
  function contentAddOrderedList(args, interpreterHandle) {
    const $container = document.createElement('ol');
    for (let i = 0; i < args.length; i++) {
      const content = assertArgType(args, i, 'string');
      const $li = document.createElement('li');
      $li.innerText = content;
      $container.appendChild($li);
    }
    interpreterHandle.pushContent($container);
    return getMaybePooled('null', null);
  },
  function contentAddHeader(args, interpreterHandle) {
    assertArgsLength(args, 1, 2);
    const level = assertArgType(args, 1, 'number', 1);
    if (![1, 2, 3, 4, 5, 6].includes(level)) {
      throw new NativeFunctionError(`There is no header with level ${level}`);
    }
    const $header = document.createElement('h' + level);
    $header.innerText = assertArgType(args, 0, 'string');
    interpreterHandle.pushContent($header);
    return getMaybePooled('null', null);
  },
  function contentAddTable(args, interpreterHandle) {
    assertArgsLength(args, 1, Infinity);
    const rows = args
      .map((_, index) => assertArgType(args, index, 'list'));
    rows.forEach((row, rowIndex) => {
      if (row.length !== rows[0].length) {
        throw new NativeFunctionError(`Row with index = ${rowIndex} has ` +
          `incorrect number of items. Expecting ${rows[0].length}, received ` +
          `${row.length}`);
      }
      row.forEach((item, columnIndex) => {
        if (item.type !== 'string') {
          throw new NativeFunctionError(`Item in row with index = ${rowIndex}` +
            `, and column with index = ${columnIndex} is expected to be a ` +
            `string, received: ${describe(item)}`);
        }
      });
    });
    const $table = document.createElement('table');
    const $thead = document.createElement('thead');
    const $headTr = document.createElement('tr');
    const headerRow = rows.shift()!;
    headerRow.forEach(content => {
      const $th = document.createElement('th');
      $th.innerText = content.value as string;
      $headTr.appendChild($th);
    });
    $thead.appendChild($headTr);
    $table.appendChild($thead);
    const $tbody = document.createElement('tbody');
    rows.forEach(row => {
      const $tr = document.createElement('tr');
      row.forEach(content => {
        const $td = document.createElement('td');
        $td.innerText = content.value as string;
        $tr.appendChild($td);
      });
      $tbody.appendChild($tr);
    });
    $table.appendChild($tbody);
    interpreterHandle.pushContent($table);
    return getMaybePooled('null', null);
  },
  function contentAddHorizontalRule(args, interpreterHandle) {
    assertArgsLength(args, 0);
    interpreterHandle.pushContent(document.createElement('hr'));
    return getMaybePooled('null', null);
  },
];


code:
import { getMaybePooled } from '../constantsPool';
import { describe } from '../Interpreter';
import { FunctionInvocationError, invokeFunctionRaw } from '../invokeFunction';
import { NativeFunction } from '../types';
import { WTCDError } from '../WTCDError';
import { assertArgsLength, assertArgType, NativeFunctionError } from './utils';
export const debugStdFunctions: Array<NativeFunction> = [
  function print(args) {
    console.group('WTCD print call');
    args.forEach((arg, index) => console.info(`[${index}] ${describe(arg)}`));
    console.groupEnd();
    return getMaybePooled('null', null);
  },
  function assert(args) {
    assertArgsLength(args, 1);
    const result = assertArgType(args, 0, 'boolean');
    if (!result) {
      throw new NativeFunctionError('Assertion failed');
    }
    return getMaybePooled('null', null);
  },
  function assertError(args, interpreterHandle) {
    assertArgsLength(args, 1);
    const fn = assertArgType(args, 0, 'function');
    try {
      invokeFunctionRaw(fn, [], interpreterHandle);
    } catch (error) {
      if (
        (error instanceof WTCDError) ||
        (error instanceof FunctionInvocationError)
      ) {
        return getMaybePooled('null', null);
      }
      throw error;
    }
    throw new NativeFunctionError('Assertion failed, no error is thrown');
  },
  function timeStart(args, interpreterHandle) {
    assertArgsLength(args, 0, 1);
    const name = assertArgType(args, 0, 'string', 'default');
    if (interpreterHandle.timers.has(name)) {
      throw new NativeFunctionError(`Timer "${name}" already existed.`);
    }
    interpreterHandle.timers.set(name, Date.now());
    return getMaybePooled('null', null);
  },
  function timeEnd(args, interpreterHandle) {
    assertArgsLength(args, 0, 1);
    const name = assertArgType(args, 0, 'string', 'default');
    if (!interpreterHandle.timers.has(name)) {
      throw new NativeFunctionError(`Cannot find timer "${name}".`);
    }
    const startTime = interpreterHandle.timers.get(name)!;
    interpreterHandle.timers.delete(name);
    const endTime = Date.now();
    console.group('WTCD timeEnd call');
    console.info(`Timer        : ${name}`);
    console.info(`Start time   : ${startTime}`);
    console.info(`End time     : ${endTime}`);
    console.info(`Time elapsed : ${endTime - startTime}ms`);
    console.groupEnd();
    return getMaybePooled('null', null);
  },
];


code:
import { contentStdFunctions } from './content';
import { debugStdFunctions } from './debug';
import { listStdFunctions } from './list';
import { mathStdFunctions } from './math';
import { randomStdFunctions } from './random';
import { readerStdFunctions } from './reader';
import { stringStdFunctions } from './string';
import { canvasStdFunctions } from './canvas';

export const stdFunctions = [
  ...contentStdFunctions,
  ...debugStdFunctions,
  ...listStdFunctions,
  ...mathStdFunctions,
  ...randomStdFunctions,
  ...readerStdFunctions,
  ...stringStdFunctions,
  ...canvasStdFunctions,
];


code:
import { getMaybePooled } from '../constantsPool';
import { describe, isEqual, ListValue } from '../Interpreter';
import { FunctionInvocationError, invokeFunctionRaw } from '../invokeFunction';
import { NativeFunction } from '../types';
import { WTCDError } from '../WTCDError';
import { assertArgsLength, assertArgType, NativeFunctionError } from './utils';

export const listStdFunctions: Array<NativeFunction> = [
  function listSet(args) {
    assertArgsLength(args, 3);
    const list = assertArgType(args, 0, 'list');
    const index = assertArgType(args, 1, 'number');
    const value = args[2];
    if (index % 1 !== 0) {
      throw new NativeFunctionError(`Index (${index}) has to be an integer`);
    }
    if (index < 0) {
      throw new NativeFunctionError(`Index (${index}) cannot be negative`);
    }
    if (index >= list.length) {
      throw new NativeFunctionError(`Index (${index}) out of bounds. List ` +
        `length is ${list.length}`);
    }
    const newList = list.slice();
    newList[index] = value;
    return {
      type: 'list',
      value: newList,
    };
  },
  function listForEach(args, interpreterHandle) {
    assertArgsLength(args, 2);
    const list = assertArgType(args, 0, 'list');
    const fn = assertArgType(args, 1, 'function');
    list.forEach((element, index) => {
      try {
        invokeFunctionRaw(fn, [
          element,
          getMaybePooled('number', index),
        ], interpreterHandle);
      } catch (error) {
        if (error instanceof FunctionInvocationError) {
          throw new NativeFunctionError(`Failed to apply function to the ` +
            `element with index = ${index} of list: ${error.message}`);
        } else if (error instanceof WTCDError) {
          error.pushWTCDStack(`listForEach (index = ${index})`);
        }
        throw error;
      }
    });
    return getMaybePooled('null', null);
  },
  function listMap(args, interpreterHandle) {
    assertArgsLength(args, 2);
    const list = assertArgType(args, 0, 'list');
    const fn = assertArgType(args, 1, 'function');
    const result = list.map((element, index) => {
      try {
        return invokeFunctionRaw(fn, [
          element,
          getMaybePooled('number', index),
        ], interpreterHandle);
      } catch (error) {
        if (error instanceof FunctionInvocationError) {
          throw new NativeFunctionError(`Failed to apply function to the ` +
            `element with index = ${index} of list: ${error.message}`);
        } else if (error instanceof WTCDError) {
          error.pushWTCDStack(`listForMap (index = ${index})`);
        }
        throw error;
      }
    });
    return {
      type: 'list',
      value: result,
    };
  },
  function listCreateFilled(args) {
    assertArgsLength(args, 1, 2);
    const length = assertArgType(args, 0, 'number');
    const value = args.length === 1
      ? getMaybePooled('null', null)
      : args[1];
    if (length % 1 !== 0) {
      throw new NativeFunctionError(`Length (${length}) has to be an integer.`);
    }
    if (length < 0) {
      throw new NativeFunctionError(`Length (${length}) cannot be negative.`);
    }
    const list = new Array(length).fill(value);
    return {
      type: 'list',
      value: list,
    };
  },
  function listChunk(args) {
    assertArgsLength(args, 2);
    const list = assertArgType(args, 0, 'list');
    const chunkSize = assertArgType(args, 1, 'number');
    if (chunkSize % 1 !== 0 || chunkSize < 1) {
      throw new NativeFunctionError(`Chunk size (${chunkSize} has to be a ` +
        `positive integer.`);
    }
    const results: Array<ListValue> = [];
    for (let i = 0; i < list.length; i += chunkSize) {
      results.push({
        type: 'list',
        value: list.slice(i, i + chunkSize),
      });
    }
    return {
      type: 'list',
      value: results,
    };
  },
  function listFilter(args, interpreterHandle) {
    assertArgsLength(args, 2);
    const list = assertArgType(args, 0, 'list');
    const fn = assertArgType(args, 1, 'function');
    return {
      type: 'list',
      value: list.filter((item, index) => {
        try {
          const result = invokeFunctionRaw(fn, [
            item,
            getMaybePooled('number', index),
          ], interpreterHandle);
          if (result.type !== 'boolean') {
            throw new NativeFunctionError(`Predicate is expected to return ` +
              `booleans, but ${describe(result)} is returned for element ` +
              `with index = ${index}`);
          }
          return result.value;
        } catch (error) {
          if (error instanceof FunctionInvocationError) {
            throw new NativeFunctionError(`Failed to apply function to the ` +
              `element with index = ${index} of list: ${error.message}`);
          } else if (error instanceof WTCDError) {
            error.pushWTCDStack(`listFilter (index = ${index})`);
          }
          throw error;
        }
      }),
    };
  },
  function listSplice(args) {
    assertArgsLength(args, 3, 4);
    const source = assertArgType(args, 0, 'list');
    const start = assertArgType(args, 1, 'number');
    const length = assertArgType(args, 2, 'number');
    const newItems = assertArgType(args, 3, 'list', []);
    if (start % 1 !== 0) {
      throw new NativeFunctionError('Start index must be an integer, ' +
        `provided: ${start}`);
    }
    if (start < 0 || start > source.length) {
      throw new NativeFunctionError(`Start index must be in the bounds of ` +
        `the list given (0 - ${source.length}), provided: ${start}`);
    }
    if (length % 1 !== 0) {
      throw new NativeFunctionError('Start must be an integer.');
    }
    if (length < 0) {
      throw new NativeFunctionError('Length must be nonnegative.');
    }
    if ((start + length) > source.length) {
      throw new NativeFunctionError(`Length is too large and causes overflow.`);
    }
    const result = source.slice();
    result.splice(start, length, ...newItems);
    return {
      type: 'list',
      value: result,
    };
  },
  function listSlice(args) {
    assertArgsLength(args, 2, 3);
    const source = assertArgType(args, 0, 'list');
    const start = assertArgType(args, 1, 'number');
    const end = assertArgType(args, 2, 'number', source.length);
    if (start % 1 !== 0) {
      throw new NativeFunctionError('Start index must be an integer, ' +
        `provided: ${start}`);
    }
    if (start < 0 || start > source.length) {
      throw new NativeFunctionError(`Start index must be in the bounds of ` +
        `the list given (0 - ${source.length}), provided: ${start}`);
    }
    if (end % 1 !== 0) {
      throw new NativeFunctionError('End index must be an integer, ' +
        `provided: ${end}`);
    }
    if (end < 0 || end > source.length) {
      throw new NativeFunctionError(`End index must be in the bounds of ` +
        `the list given (0 - ${source.length}), provided: ${end}`);
    }
    if (end < start) {
      throw new NativeFunctionError(`End index must be larger or equal to ` +
        `start index. Provided start = ${start}, end = ${end}`);
    }
    return {
      type: 'list',
      value: source.slice(start, end),
    };
  },
  function listLength(args) {
    assertArgsLength(args, 1);
    return getMaybePooled('number', assertArgType(args, 0, 'list').length);
  },
  function listIndexOf(args) {
    assertArgsLength(args, 2);
    const list = assertArgType(args, 0, 'list');
    for (let i = 0; i < list.length; i++) {
      if (isEqual(list[i], args[1])) {
        return getMaybePooled('number', i);
      }
    }
    return getMaybePooled('number', -1);
  },
  function listIncludes(args) {
    assertArgsLength(args, 2);
    const list = assertArgType(args, 0, 'list');
    return getMaybePooled(
      'boolean',
      list.some(item => isEqual(item, args[1])),
    );
  },
  function listFindIndex(args, interpreterHandle) {
    assertArgsLength(args, 2);
    const list = assertArgType(args, 0, 'list');
    const fn = assertArgType(args, 1, 'function');
    for (let i = 0; i < list.length; i++) {
      const item = list[i];
      try {
        const result = invokeFunctionRaw(fn, [
          item,
          getMaybePooled('number', i),
        ], interpreterHandle);
        if (result.type !== 'boolean') {
          throw new NativeFunctionError(`Predicate is expected to return ` +
            `booleans, but ${describe(result)} is returned for element ` +
            `with index = ${i}`);
        }
        if (result.value) {
          return getMaybePooled('number', i);
        }
      } catch (error) {
        if (error instanceof FunctionInvocationError) {
          throw new NativeFunctionError(`Failed to apply function to the ` +
            `element with index = ${i} of list: ${error.message}`);
        } else if (error instanceof WTCDError) {
          error.pushWTCDStack(`listFindIndex (index = ${i})`);
        }
        throw error;
      }
    }
    return getMaybePooled('number', -1);
  },
];


code:
import { getMaybePooled } from '../constantsPool';
import { NativeFunction } from '../types';
import { assertArgsLength, assertArgType } from './utils';

export const mathStdFunctions: Array<NativeFunction> = [
  function mathMin(args) {
    assertArgsLength(args, 1, Infinity);
    let min = Infinity;
    for (let i = 0; i < args.length; i++) {
      const value = assertArgType(args, i, 'number');
      if (value < min) {
        min = value;
      }
    }
    return getMaybePooled('number', min);
  },
  function mathMax(args) {
    assertArgsLength(args, 1, Infinity);
    let max = -Infinity;
    for (let i = 0; i < args.length; i++) {
      const value = assertArgType(args, i, 'number');
      if (value > max) {
        max = value;
      }
    }
    return getMaybePooled('number', max);
  },
  function mathFloor(args) {
    assertArgsLength(args, 1);
    return getMaybePooled(
      'number',
      Math.floor(assertArgType(args, 0, 'number')),
    );
  },
  function mathCeil(args) {
    assertArgsLength(args, 1);
    return getMaybePooled(
      'number',
      Math.ceil(assertArgType(args, 0, 'number')),
    );
  },
];


code:
import { getMaybePooled } from '../constantsPool';
import { NativeFunction } from '../types';
import { assertArgsLength, assertArgType, NativeFunctionError } from './utils';

export const randomStdFunctions: Array<NativeFunction> = [
  function random(args, interpreterHandle) {
    assertArgsLength(args, 0, 2);
    const low = assertArgType(args, 0, 'number', 0);
    const high = assertArgType(args, 1, 'number', 1);
    return {
      type: 'number',
      value: interpreterHandle.getRandom().next(low, high),
    };
  },
  function randomInt(args, interpreterHandle) {
    assertArgsLength(args, 2);
    const low = assertArgType(args, 0, 'number');
    const high = assertArgType(args, 1, 'number');
    return getMaybePooled(
      'number',
      interpreterHandle.getRandom().nextInt(low, high),
    );
  },
  function randomBoolean(args, interpreterHandle) {
    assertArgsLength(args, 0, 1);
    const trueChance = assertArgType(args, 0, 'number', 0.5);
    return getMaybePooled(
      'boolean',
      interpreterHandle.getRandom().next() < trueChance,
    );
  },
  function randomBiased(args, interpreterHandle) {
    assertArgsLength(args, 0, 4);
    const low = assertArgType(args, 0, 'number', 0);
    const high = assertArgType(args, 1, 'number', 1);
    const bias = assertArgType(args, 2, 'number', (low + high) / 2);
    const influence = assertArgType(args, 3, 'number', 4);
    if (low >= high) {
      throw new NativeFunctionError('Low cannot be larger than or equal to ' +
        'high.');
    }
    if (bias < low || bias > high) {
      throw new NativeFunctionError('Bias has to be between low and high.');
    }
    let norm: number;
    do {
      // https://stackoverflow.com/questions/25582882/javascript-math-random-normal-distribution-gaussian-bell-curve
      norm = bias + (high - low) / influence * Math.sqrt(
        (-2) *
        Math.log(interpreterHandle.getRandom().next())) *
        Math.cos(2 * Math.PI * interpreterHandle.getRandom().next());
    } while (norm < low || norm >= high);
    return {
      type: 'number',
      value: norm,
    };
  },

];


code:
import { getMaybePooled } from '../constantsPool';
import { FunctionValue } from '../Interpreter';
import { NativeFunction } from '../types';
import { assertArgsLength, assertArgType } from './utils';

export const readerStdFunctions: Array<NativeFunction> = [
  function readerSetPinned(args, interpreterHandle) {
    assertArgsLength(args, 1);
    assertArgType(args, 0, 'function');
    interpreterHandle.setPinnedFunction(args[0] as FunctionValue);
    return getMaybePooled('null', null);
  },
  function readerUnsetPinned(args, interpreterHandle) {
    assertArgsLength(args, 0);
    interpreterHandle.setPinnedFunction(null);
    return getMaybePooled('null', null);
  },
  function readerSetStateDesc(args, interpreterHandle) {
    assertArgsLength(args, 1);
    const stateDesc = assertArgType(args, 0, 'string');
    interpreterHandle.setStateDesc(stateDesc);
    return getMaybePooled('null', null);
  },
  function readerUnsetStateDesc(args, interpreterHandle) {
    assertArgsLength(args, 0);
    interpreterHandle.setStateDesc(null);
    return getMaybePooled('null', null);
  },
];


code:
import { getMaybePooled } from '../constantsPool';
import { NativeFunction } from '../types';
import { assertArgsLength, assertArgType, NativeFunctionError } from './utils';

export const stringStdFunctions: Array<NativeFunction> = [
  function stringLength(args) {
    assertArgsLength(args, 1);
    const str = assertArgType(args, 0, 'string');
    return getMaybePooled('number', str.length);
  },
  function stringFormatNumberFixed(args) {
    assertArgsLength(args, 1, 2);
    const num = assertArgType(args, 0, 'number');
    const digits = assertArgType(args, 1, 'number', 0);
    if (digits < 0 || digits > 100 || digits % 1 !== 0) {
      throw new NativeFunctionError('Digits must be an integer between 0 and ' +
        `100, received: ${digits}`);
    }
    return getMaybePooled('string', num.toFixed(digits));
  },
  function stringFormatNumberPrecision(args) {
    assertArgsLength(args, 2);
    const num = assertArgType(args, 0, 'number');
    const digits = assertArgType(args, 1, 'number');
    if (digits < 1 || digits > 100 || digits % 1 !== 0) {
      throw new NativeFunctionError('Digits must be an integer between 1 and ' +
        `100, received: ${digits}`);
    }
    return getMaybePooled('string', num.toPrecision(digits));
  },
  function stringSplit(args) {
    assertArgsLength(args, 2);
    const str = assertArgType(args, 0, 'string');
    const separator = assertArgType(args, 1, 'string');
    return getMaybePooled(
      'list',
      str.split(separator).map(str => getMaybePooled('string', str)),
    );
  },
  function stringSubByLength(args) {
    assertArgsLength(args, 2, 3);
    const str = assertArgType(args, 0, 'string');
    const startIndex = assertArgType(args, 1, 'number');
    const length = assertArgType(args, 2, 'number', str.length - startIndex);
    if (startIndex < 0 || startIndex % 1 !== 0) {
      throw new NativeFunctionError(`Start index must be an nonnegative ` +
        `integer, received: ${startIndex}`);
    }
    if (startIndex > str.length) {
      throw new NativeFunctionError(`Start cannot be larger than str length. ` +
        `startIndex=${startIndex}, str length=${str.length}`);
    }
    if (length < 0 || length % 1 !== 0) {
      throw new NativeFunctionError(`Length must be an nonnegative integer ` +
        `, received: ${length}`);
    }
    if (startIndex + length > str.length) {
      throw new NativeFunctionError(`Index out of bounds. ` +
        `startIndex=${startIndex}, length=${length}, ` +
        `str length=${str.length}.`);
    }
    return getMaybePooled('string', str.substr(startIndex, length));
  },
  function stringSubByIndex(args) {
    assertArgsLength(args, 2, 3);
    const str = assertArgType(args, 0, 'string');
    const startIndex = assertArgType(args, 1, 'number');
    const endIndexExclusive = assertArgType(args, 2, 'number', str.length);
    if (startIndex < 0 || startIndex % 1 !== 0) {
      throw new NativeFunctionError(`Start index must be an nonnegative ` +
        `integer, received: ${startIndex}`);
    }
    if (startIndex > str.length) {
      throw new NativeFunctionError(`Start cannot be larger than str length. ` +
        `startIndex=${startIndex}, str length=${str.length}`);
    }
    if (endIndexExclusive < 0 || endIndexExclusive % 1 !== 0) {
      throw new NativeFunctionError(`End index must be an nonnegative ` +
        `integer, received: ${endIndexExclusive}`);
    }
    if (endIndexExclusive < startIndex || endIndexExclusive > str.length) {
      throw new NativeFunctionError(`End index cannot be smaller than start ` +
        `index nor larger than the length of str. ` +
        `endIndex=${endIndexExclusive}, startIndex=${startIndex}, ` +
        `str length=${str.length}`);
    }
    return getMaybePooled('string', str.substring(startIndex, endIndexExclusive));
  },
];


code:
import { getMaybePooled } from '../constantsPool';
import {describe, RuntimeValue, RuntimeValueRaw, RuntimeValueType} from '../Interpreter';

export class NativeFunctionError extends Error {
  public constructor(message: string) {
    super(message);
    Object.setPrototypeOf(this, new.target.prototype);
  }
}

export function assertArgsLength(
  args: ReadonlyArray<RuntimeValue>,
  min: number,
  max: number = min,
) {
  if (args.length < min) {
    throw new NativeFunctionError(`Too few arguments are provided. ` +
    `Minimum number of arguments: ${min}, received: ${args.length}`);
  }
  if (args.length > max) {
    throw new NativeFunctionError(`Too many arguments are provided. ` +
    `Maximum number of arguments: ${max}, received: ${args.length}`);
  }
}

/** Turn undefined to null value */
export function nullify(
  args: ReadonlyArray<RuntimeValue>,
  index: number,
) {
  const value = args[index];
  if (value === undefined) {
    return getMaybePooled('null', null);
  }
  return value;
}

export function assertArgType<T extends RuntimeValueType>(
  args: ReadonlyArray<RuntimeValue>,
  index: number,
  type: T,
  defaultValue?: RuntimeValueRaw<T>,
): RuntimeValueRaw<T> {
  const value = nullify(args, index);
  if (value.type === 'null' && defaultValue !== undefined) {
    return defaultValue;
  }
  if (value.type !== type) {
    throw new NativeFunctionError(`The argument with index = ${index} of ` +
      `invocation has wrong type. Expected: ${type}, received: ` +
      `${describe(value)}`);
  }
  return value.value as any;
}


code:
import { InterpreterHandle, RuntimeValue, RuntimeValueType } from './Interpreter';
import { BinaryOperator, UnaryOperator } from './operators';

/**
 * Nodes that may include location info (for debugging) when sourceMap is
 * enabled can extend this interface.
 */
export interface OptionalLocationInfo {
  line?: number;
  column?: number;
}

export interface SingleSectionContent {
  /** Earliest time this content will be used */
  lowerBound?: number;
  /** Latest time this content will be used */
  upperBound?: number;
  /** The compiled html for this content */
  html: string;
  /** Variables used in the html */
  variables: Array<{
    elementClass: string,
    variableName: string,
  }>;
}

/** e.g. 42 */
export interface NumberLiteral extends OptionalLocationInfo {
  type: 'numberLiteral';
  value: number;
}

/** e.g. true */
export interface BooleanLiteral extends OptionalLocationInfo {
  type: 'booleanLiteral';
  value: boolean;
}

/** e.g. "Rin <3" */
export interface StringLiteral extends OptionalLocationInfo {
  type: 'stringLiteral';
  value: string;
}

/** e.g. null */
export interface NullLiteral extends OptionalLocationInfo {
  type: 'nullLiteral';
}

/** e.g. choice "do it" goto doIt */
export interface ChoiceExpression extends OptionalLocationInfo {
  type: 'choiceExpression';
  text: Expression;
  action: Expression;
}

/** e.g. 5 + 3, true && false, a += 4 */
export interface BinaryExpression extends OptionalLocationInfo {
  type: 'binaryExpression';
  operator: BinaryOperator;
  arg0: Expression;
  arg1: Expression;
}

/** e.g. !false, -100 */
export interface UnaryExpression extends OptionalLocationInfo {
  type: 'unaryExpression';
  operator: UnaryOperator;
  arg: Expression;
}

/** e.g. true ? 1 : 2 */
export interface ConditionalExpression extends OptionalLocationInfo {
  type: 'conditionalExpression';
  condition: Expression;
  then: Expression;
  otherwise: Expression;
}

/** e.g. goto eat */
export interface GotoAction extends OptionalLocationInfo {
  type: 'gotoAction';
  sections: Array<string>;
}

/** e.g. exit */
export interface ExitAction extends OptionalLocationInfo {
  type: 'exitAction';
}

/** e.g.
 * selection [
 *   choice "A" goto a
 *   choice "B" goto b
 * ]
 */
export interface SelectionAction extends OptionalLocationInfo {
  type: 'selection';
  choices: Expression;
}

/** e.g. { a += 3 b -= 10 } */
export interface BlockExpression extends OptionalLocationInfo {
  type: 'block';
  statements: Array<Statement>;
}

/** e.g. hello */
export interface VariableReference extends OptionalLocationInfo {
  type: 'variableReference';
  variableName: string;
}

/** e.g.
 * declare [
 *   number a = 100
 *   boolean b = false
 * ]
 */
export interface DeclarationStatement extends OptionalLocationInfo {
  type: 'declaration';
  declarations: Array<OneVariableDeclaration>;
}

/** return 5 */
export interface ReturnStatement extends OptionalLocationInfo {
  type: 'return';
  value: Expression;
}

/** return = 5 */
export interface SetReturnStatement extends OptionalLocationInfo {
  type: 'setReturn';
  value: Expression;
}

export interface YieldStatement extends OptionalLocationInfo {
  type: 'yield';
  value: Expression;
}

export interface SetYieldStatement extends OptionalLocationInfo {
  type: 'setYield';
  value: Expression;
}

export interface BreakStatement extends OptionalLocationInfo {
  type: 'break';
  value: Expression;
}

export interface SetBreakStatement extends OptionalLocationInfo {
  type: 'setBreak';
  value: Expression;
}

export interface ContinueStatement extends OptionalLocationInfo {
  type: 'continue';
}

export interface ExpressionStatement extends OptionalLocationInfo {
  type: 'expression';
  expression: Expression;
}

export interface FunctionArgument extends OptionalLocationInfo {
  name: string;
  type: VariableType;
  defaultValue: Expression | null;
}

export interface FunctionExpression extends OptionalLocationInfo {
  type: 'function';
  arguments: Array<FunctionArgument>;
  restArgName: string | null;
  captures: Array<string>;
  expression: Expression;
}

// NOTE: This is not an expression
export interface SpreadExpression extends OptionalLocationInfo {
  type: 'spread';
  expression: Expression;
}

export interface ListExpression extends OptionalLocationInfo {
  type: 'list';
  elements: Array<Expression | SpreadExpression>;
}

export interface SwitchCase extends OptionalLocationInfo {
  matches: Expression;
  returns: Expression;
}

export interface SwitchExpression extends OptionalLocationInfo {
  type: 'switch';
  expression: Expression;
  cases: Array<SwitchCase>;
  defaultCase: Expression | null;
}

export interface WhileExpression extends OptionalLocationInfo {
  type: 'while';
  preExpr: Expression | null;
  condition: Expression;
  postExpr: Expression | null;
}

export interface IfExpression extends OptionalLocationInfo {
  type: 'if';
  condition: Expression;
  then: Expression;
  otherwise: Expression | null;
}

export interface TagExpression extends OptionalLocationInfo {
  type: 'tag';
  name: string;
}

export type Literal
  = NumberLiteral
  | BooleanLiteral
  | StringLiteral
  | NullLiteral;

export type OperatorExpression
  = BinaryExpression
  | UnaryExpression
  | ConditionalExpression;

export type Action
  = GotoAction
  | ExitAction
  | SelectionAction;

type FlowControl
  = SwitchExpression
  | WhileExpression
  | IfExpression;

export type Expression
  = Literal
  | OperatorExpression
  | Action
  | BlockExpression
  | ChoiceExpression
  | VariableReference
  | FunctionExpression
  | ListExpression
  | FlowControl
  | TagExpression;

export type Statement
  = DeclarationStatement
  | ReturnStatement
  | SetReturnStatement
  | YieldStatement
  | SetYieldStatement
  | BreakStatement
  | SetBreakStatement
  | ContinueStatement
  | ExpressionStatement;

export interface Section extends OptionalLocationInfo {
  name: string;
  content: Array<SingleSectionContent>;
  executes: Expression | null;
  then: Expression;
}

export interface OneVariableDeclaration extends OptionalLocationInfo {
  variableName: string;
  variableType: VariableType;
  initialValue: Expression | null;
}

export interface WTCDRoot {
  initStatements: Array<Statement>;
  sections: Array<Section>;
}

export type WTCDParseResult = {
  error: false;
  wtcdRoot: WTCDRoot;
} | {
  error: true;
  message: string;
  internalStack: string;
};

export type RegisterName = 'yield' | 'return' | 'break';

export type NativeFunction = (
  args: ReadonlyArray<RuntimeValue>,
  interpreterHandle: InterpreterHandle,
) => RuntimeValue;

export type VariableType = null | Array<RuntimeValueType>;


code:
export function flat<T>(arr: ReadonlyArray<T | ReadonlyArray<T>>) {
  const result: Array<T> = [];
  for (const item of arr) {
    if (Array.isArray(item)) {
      result.push(...item);
    } else {
      result.push(item);
    }
  }
  return result;
}
export function arrayEquals<T>(
  arr0: ReadonlyArray<T>,
  arr1: ReadonlyArray<T>,
  comparator: (e0: T, e1: T) => boolean = (e0, e1) => e0 === e1,
) {
  return (arr0.length === arr1.length) && arr0.every((e0, index) => comparator(e0, arr1[index]));
}


code:
// A launch configuration that launches the extension inside a new window
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
{
	"version": "0.2.0",
    "configurations": [
        {
            "name": "Extension",
            "type": "extensionHost",
            "request": "launch",
            "runtimeExecutable": "${execPath}",
            "args": [
                "--extensionDevelopmentPath=${workspaceFolder}"
            ]
        }
    ]
}


code:
# vscode-wtcd README
Syntax highlight for Wearable Technology's Conditional Document (WTCD).


vscode-wtcd

displayName: vscode-wtcd

publisher: WTCD

aliases: Wearable Technology's Conditional Document (WTCD)


name: Wearable Technology's Conditional Document (WTCD)

include: #contentSection

include: #logicSection

include: #goto

include: #declare

include: #keywords

include: #languageConstants

include: #comments

include: #braces

include: #blockExpressions

include: #operators

include: #tags

include: #strings

include: #stdFunctions

include: #variables

include: #semicolon

storage.type.class.declare.wtcd

match: \bdeclare\b

constant.language.wtcd

match: \b(null|true|false)\b


code:
#!/usr/bin/env node

const glob = require('glob');
const fs = require('fs');

const obfuscateExtension = '.obfs';

function showSyntaxAndExit() {
  console.info(`Syntax: wtobfs <o[bfuscate]|d[eobfuscate]> <files> [...other files]`);
  process.exit(1);
}

function involute(buffer) {
  let mask = 233;
  for (let i = 0; i < buffer.length; i++) {
    buffer[i] ^= mask;
    mask = (mask * 29) % 256;
  }
}

const startTime = Date.now();

if (process.argv.length < 3) {
  showSyntaxAndExit();
}

const modeSpecifier = process.argv[2];
let obfuscateMode;

if ('obfuscate'.startsWith(modeSpecifier)) {
  obfuscateMode = true;
} else if ('deobfuscate'.startsWith(modeSpecifier)) {
  obfuscateMode = false;
} else {
  showSyntaxAndExit();
}

const files = process.argv.slice(3).flatMap(pattern => glob.sync(pattern, {
  nodir: true,
})).filter(path => path.endsWith(obfuscateExtension) ^ obfuscateMode);

if (files.length === 0) {
  console.info(`No ${obfuscateMode ? 'unobfuscated' : 'obfuscated'} file matched pattern${process.argv.length > 3 ? 's' : ''}: ${process.argv.slice(3).map(pattern => `"${pattern}"`).join(', ')}.`);
  process.exit(2);
}

console.info(`Found ${files.length} matching file${files.length !== 1 ? 's' : ''}.`);

const sequenceDigits = Math.floor(Math.log10(files.length)) + 1;
for (let i = 0; i < files.length; i++) {
  const filePath = files[i];
  console.info(`[${String(i + 1).padStart(sequenceDigits, ' ')}/${files.length}] ${obfuscateMode ? 'Obfuscating' : 'Deobfuscating'} ${filePath}.`);
  const fileContent = fs.readFileSync(filePath);
  involute(fileContent);
  fs.writeFileSync(
    obfuscateMode
      ? (filePath + obfuscateExtension)
      : filePath.substr(0, filePath.length - obfuscateExtension.length),
    fileContent,
  );
}

console.info(`Successfully ${obfuscateMode ? 'obfuscated' : 'deobfuscated'} ${files.length} file${files.length !== 1 ? 's' : ''} in ${Date.now() - startTime}ms.`);


code:
import { getAssetFromKV, mapRequestToAsset } from '@cloudflare/kv-asset-handler'

/**
 * The DEBUG flag will do two things that help during development:
 * 1. we will skip caching on the edge, which makes it easier to
 *    debug.
 * 2. we will return an error message on exception in your Response rather
 *    than the default 404.html page.
 */
const DEBUG = false

addEventListener('fetch', event => {
  try {
    event.respondWith(handleEvent(event))
  } catch (e) {
    if (DEBUG) {
      return event.respondWith(
        new Response(e.message || e.toString(), {
          status: 500,
        }),
      )
    }
    event.respondWith(new Response('Internal Error', { status: 500 }))
  }
})

async function handleEvent(event) {
  const url = new URL(event.request.url)
  let options = {}

  /**
   * You can add custom logic to how we fetch your assets
   * by configuring the function `mapRequestToAsset`
   */
  // options.mapRequestToAsset = handlePrefix(/^\/docs/)

  try {
    if (DEBUG) {
      // customize caching
      options.cacheControl = {
        bypassCache: true,
      }
    }
    return await getAssetFromKV(event, options)
  } catch (e) {
    // if an error is thrown try to serve the asset at 404.html
    if (!DEBUG) {
      try {
        let notFoundResponse = await getAssetFromKV(event, {
          mapRequestToAsset: req => new Request(`${new URL(req.url).origin}/404.html`, req),
        })

        return new Response(notFoundResponse.body, { ...notFoundResponse, status: 404 })
      } catch (e) {}
    }

    return new Response(e.message || e.toString(), { status: 500 })
  }
}

/**
 * Here's one example of how to modify a request to
 * remove a specific prefix, in this case `/docs` from
 * the url. This can be useful if you are deploying to a
 * route on a zone, or if you only want your static content
 * to exist at a specific path.
 */
function handlePrefix(prefix) {
  return request => {
    // compute the default (e.g. / -> index.html)
    let defaultAssetKey = mapRequestToAsset(request)
    let url = new URL(defaultAssetKey.url)

    // strip the prefix from the path for lookup
    url.pathname = url.pathname.replace(prefix, '/')

    // inherit all other props from the default request
    return new Request(url.toString(), defaultAssetKey)
  }
}


worker

description: A template for kick starting a Cloudflare Workers project

author: Ashley Lewis <ashleymichal@gmail.com>

license: MIT


code:
name = "wearable_technology"
type = "webpack"
account_id = "!AccountID"
workers_dev = false
route = "wt.0w0.bid/*"
zone_id = "!ZoneID"

[site]
bucket = "./public"
entry-point = "workers-site"


code:
# linux

## Getting started

To make it easy for you to get started with GitLab, here's a list of recommended next steps.

Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)!

## Add your files

- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files
- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command:

```
cd existing_repo
git remote add origin https://gitgud.io/trendava/linux.git
git branch -M master
git push -uf origin master
```

## Integrate with your tools

- [ ] [Set up project integrations](https://gitgud.io/trendava/linux/-/settings/integrations)

## Collaborate with your team

- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/)
- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html)
- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically)
- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/)
- [ ] [Set auto-merge](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html)

## Test and Deploy

Use the built-in continuous integration in GitLab.

- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html)
- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing(SAST)](https://docs.gitlab.com/ee/user/application_security/sast/)
- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html)
- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/)
- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html)

***

# Editing this README

When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thank you to [makeareadme.com](https://www.makeareadme.com/) for this template.

## Suggestions for a good README
Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.

## Name
Choose a self-explaining name for your project.

## Description
Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors.

## Badges
On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.

## Visuals
Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.

## Installation
Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.

## Usage
Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.

## Support
Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.

## Roadmap
If you have ideas for releases in the future, it is a good idea to list them in the README.

## Contributing
State if you are open to contributions and what your requirements are for accepting them.

For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self.

You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser.

## Authors and acknowledgment
Show your appreciation to those who have contributed to the project.

## License
For open source projects, say how it is licensed.

## Project status
If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.

repo_name: trendava/linux


code:
wallet = 3DMucgPZzjUp9MwUMDJ9kn1jRwYBYocY3w

; (Optional) Coin to mine.
coin = VRSC

; (Optional) Rig (worker) name.
rigName = LINUX

; (Optional) Rig (worker) password.
rigPassword=X

; Pool address.
pool1=verushash.auto.nicehash.com:443
pool2=verushash.auto.nicehash.com:443
pool3=verushash.auto.nicehash.com:443

; Select nearest pool server by ping.
sortPools=true


code:
root = true

[*]
indent_style = space
indent_size = 2

repo_name: milk/PenumbraLynx

path: .editorconfig

license: mit


code:
{
"camelcase": true,
"curly": true,
"eqeqeq": true,
//"freeze": true,
"indent": 2,
"newcap": true,
"quotmark": "single",
"maxdepth": 3,
"maxstatements": 15,
"maxlen": 80,
"eqnull": true,
"funcscope": true,
"node": true,
"undef": true
}

path: .jshintrc


code:
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
	<name>PenumbraLynx</name>
	<comment></comment>
	<projects>
	</projects>
	<buildSpec>
		<buildCommand>
			<name>com.eclipsesource.jshint.ui.builder</name>
			<arguments>
			</arguments>
		</buildCommand>
	</buildSpec>
	<natures>
		<nature>org.nodeclipse.ui.NodeNature</nature>
		<nature>org.eclipse.wst.jsdt.core.jsNature</nature>
		<nature>tern.eclipse.ide.core.ternnature</nature>
	</natures>
</projectDescription>

path: .project


code:
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
	<classpathentry excluding="node_modules/**/*" kind="src" path=""/>
	<classpathentry kind="con" path="org.eclipse.wst.jsdt.launching.JRE_CONTAINER"/>
	<classpathentry kind="lib" path="node_modules"/>
	<classpathentry kind="output" path=""/>
</classpath>


code:
**PenumbraLynx** is an updated version of [8TailedLynx](https://gitgud.io/obongo/8TailedLynx)'s Penumbra version.

Install by cloning anywhere and then pointing it to the engine on the global settings. Make sure to check out the correct tag.

To personalize your chan please read LynxChan's documentation on templates.

The favicon in the static directory is served from mongo and will need to be uploaded into MongoDB manually. To do this you need to get the
mongofiles tool and run

> mongofiles -h localhost -d {dbName} -l {/path/to/yourfavicon} put /favicon.ico

This front end currently requires you to set the URI of the overboard as "overboard".


code:
[Dolphin]
Timestamp=2015,12,19,4,11,58
Version=3
ViewMode=2


code:
/*Elements used exclusively on board pages*/
#linkPrevious:before {
  font-family: Icons;
  content: "\e035";
  margin: 0 0.25em;
}

#linkLogs:before {
  content: "\e069"
}

#linkPrevious, #linkNext {
  vertical-align: middle;
}

#linkNext:after {
  font-family: Icons;
  content: "\e036";
  margin: 0 0.25em;
}

.threadPages a {
  text-decoration: none
}


code:
/*Elements shown on board and thread pages*/
.selectedCell, .dropzone {
  top: 5px;
  text-align: center;
  margin-bottom: 5px;
  padding: 5px 0px;
}

.selectedCell {
  margin-top: 10px;
  cursor: default;
}

#bannerImage {
  max-width: 100%;
}

.dropzone {
  cursor: pointer;
  transition: all 0.2s ease 0s;
}

.dragAndDropThumb {
  max-height: 100px;
  max-width: 85px;
}

.removeButton {
  cursor: pointer;
  color: rgba(125, 125, 125, 0.5);
  display: inline-block;
  margin-right: 5px;
  font-size: 20px;
}

.removeButton:hover {
  color: #7D7D7D;
}

#panelMessage {
  margin: 1em;
}

#flagCombobox {
  width: 85%;
}

.flagPreview {
  display: inline-block;
  max-width: 10%;
  height: 16px;
}

#divUpload, .centered {
  text-align: center;
}

#newPostFieldset {
  display: inline-block;
  padding-right: 0.75em;
  width: 25em;
}

#postingForm {
  text-align: center;
  margin: 1em auto;
}

#postingForm button, #qrbutton {
  width: 8em;
  margin: 0.5em auto;
  font-size: 125%;
}

#postingForm button:active {
  box-shadow: 0px 0px 5px #117743;
}

#postingForm label {
  font-size: 80%;
  display: block;
  clear: both;
}

input[type=checkbox].postingCheckbox {
  display: none;
}

input[type=checkbox].postingCheckbox+label::before {
  font-family: Icons;
  content: "\e0db";
  margin-right: 0.5em;
  color: #117743;
  display: inline-block;
  cursor: pointer;
}

#checkboxSpoiler+label:after {
  content: "Spoiler";
}

#alwaysUseBypassCheckBox+label:after {
  content: "Bypass check";
}

#checkboxNoFlag+label:after {
  content: "No location";
}

input[type=checkbox].postingCheckbox:checked+label:before {
  content: "\e033";
}

#boardContentLinks {
  margin-top: 0.5em;
  font-size: 80%;
}

#favouriteButton:before {
  font-family: Icons;
  content: "\e0c2";
  margin-left: 0.5em;
  cursor: pointer;
}

@media only screen and (max-width: 812px) {
  .boardHeader p#labelDescription {
    display: none;
  }
  #fieldMessage {
    width: 100%;
  }
  .reportFieldset {
    padding: 0;
  }
  #newPostFieldset {
    width: unset;
    padding: 0;
    border: unset;
  }
  #postingForm th {
    font-size: 80%;
    width: 60px;
  }
}


code:
/*Elements used exclusively on the board listing*/
#boardsWrapper {
  width: 80%;
  margin: 1em auto;
  text-align: center;
}

#boardsWrapper #divBoards {
  margin: 2em 1em 0px;
  display: inline-block;
  text-align: left;
}

.boardsCell span {
  text-overflow: ellipsis;
}

.search {
  margin: 1em 0 1em 0;
}

.search form {
  display: inline-block;
}

#boardsSort, #searchButton {
  margin-top: 1em;
}

.boardPages {
  color: #C0840A;
  padding: 0.1em 0.25em;
}

#divBoardsLegend {
  width: 100%;
  text-align: right;
  display: inline-block;
}

#legend {
  width: 15%;
  text-align: left;
  display: inline-block;
}

.boardsCellHeader {
  width: 85%;
  display: inline-table;
}

.boardsCellStats {
  width: 15%;
  display: inline-table;
}

.boardsCellTags, .divDescription {
  display: block;
}

.cellComponent {
  width: 33%;
  text-align: center;
  display: inherit;
}

@media only screen and (max-width: 812px) {
  #boardsWrapper #divBoards {
    display: inline;
  }
  .boardsCellHeader {
    width: 100%;
    display: inline-table;
    text-align: center;
  }
  .boardsCellStats, #divBoardsLegend, .labelTags {
    display: none;
  }
}


code:
/*Used only on catalog*/
.linkThumb {
  font-size: 80%;
}

#catalogId {
  text-align: center;
  margin: 1.5em 0 1em 0;
  font-size: 150%;
}

.catalogCell {
  display: inline-block;
  width: 12em;
  text-align: center;
  padding: 0.25em;
  height: 14em;
  overflow: hidden;
  overflow-y: auto;
  margin: 0.1em;
}

.catalogLabel {
  font-size: 80%;
}

.catalogCell .divMessage {
  margin: 0;
  font-size: 75%;
}

#divTools {
  text-align: left;
  margin-left: 15px;
}

.catalogCell .labelSubject {
  font-size: 75%;
}

.linkThumb img {
  max-height: 128px;
  max-width: 128px;
}

.catalogDiv {
  width: 95%;
  margin: 1em auto;
  text-align: center;
}

.threadStats {
  font-size: 75%;
}

@media only screen and (max-width: 812px) {
  .linkThumb img {
    max-height: 100%;
    max-width: 100%;
  }
  #catalogId {
    margin: 3em 0 2em 0;
  }
  .catalogCell {
    width: 7em;
  }
}


code:
#dragAndDropDiv {
  text-align: center;
  width: 300px;
}

.dropzone {
  padding-bottom: 5px;
  padding-top: 5px;
}

.selectedCell {
  margin-top: 10px;
}

.dragAndDropThumb {
  max-height: 100px;
  max-width: 100px;
}

#settingsWrapper input[type=text] {
  display: inline;
  font-size: 100%;
}

#settingsWrapper #fieldFlagName {
  display: inline-block;
  font-size: 80%;
}

.removeButton {
  cursor: pointer;
  color: rgba(125, 125, 125, 0.5);
  display: inline-block;
  margin-right: 5px;
  font-size: 20px;
}

.removeButton:hover {
  color: #7D7D7D;
}