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);
  },
];

repo_name: de/wearable

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.`);


module: commonjs

outDir: ts-out

rootDir: src

jsx: react

jsxFactory: $e


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:
# App artifacts
/_build
/db
/deps
/*.ez

# Generated on crash by the VM
erl_crash.dump

# Generated on crash by NPM
npm-debug.log

# Static artifacts
/assets/node_modules

# Since we are building assets from assets/,
# we ignore priv/static. You may want to comment
# this depending on your deployment strategy.
/priv/static/

# Files matching config/*.secret.exs pattern contain sensitive
# data and you should not commit them into version control.
#
# Alternatively, you may comment the line below and commit the
# secrets files as long as you replace their contents by environment
# variables.
#/config/*.secret.exs
/config/config.exs
/config/dev.exs
/config/prod.exs
/config/prod.secret.exs
/config/test.exs
/uploads
/uploads-temp

repo_name: Loves/qtfile

path: .gitignore

license: mit


code:
# Qtfile

To start your Phoenix server:

  * Install dependencies with `mix deps.get`
  * Create and migrate your database with `mix ecto.create && mix ecto.migrate`
  * Install Node.js dependencies with `cd assets && npm install`
  * Start Phoenix endpoint with `mix phx.server`

Now you can visit [`localhost:4000`](http://localhost:4000) from your browser.

Ready to run in production? Please [check our deployment guides](http://www.phoenixframework.org/docs/deployment).

## Learn more

  * Official website: http://www.phoenixframework.org/
  * Guides: http://phoenixframework.org/docs/overview
  * Docs: https://hexdocs.pm/phoenix
  * Mailing list: http://groups.google.com/group/phoenix-talk
  * Source: https://github.com/phoenixframework/phoenix


code:
exports.config = {
    // See http://brunch.io/#documentation for docs.
    files: {
        javascripts: {
            // joinTo: "js/app.js"
            joinTo: {
                // "js/uploader.js": "js/uploader.js",
                "js/room-view.js": [
                    "js/uploader.js",
                    "js/room.js",
                    "js/socket.js",
                    "js/file-list.js",
                    "js/upload.js",
                    "js/file.js",
                    "js/file-list-view.js",
                    "js/upload-view.js",
                    "js/file-view.js",
                    "js/presence.js",
                    "js/presence-view.js",
                    "js/room-view.js",
                    "js/async-button-view.js"
                ],
                "js/app.js": /(js\/app\.js|^(?!js))/
            }

            // To use a separate vendor.js bundle, specify two files path
            // http://brunch.io/docs/config#-files-
            // joinTo: {
            //   "js/app.js": /^js/,
            //   "js/vendor.js": /^(?!js)/
            // }
            //
            // To change the order of concatenation of files, explicitly mention here
            // order: {
            //   before: [
            //     "vendor/js/jquery-2.1.1.js",
            //     "vendor/js/bootstrap.min.js"
            //   ]
            // }
        },
        stylesheets: {
            joinTo: "css/app.css"
            /*
              "css/app.css": [
              "css/app.css",
              "css/register.css"
              ]
            */
        },
        templates: {
            joinTo: "js/app.js"
        }
    },

    conventions: {
        // This option sets where we should place non-css and non-js assets in.
        // By default, we set this to "/assets/static". Files in this directory
        // will be copied to `paths.public`, which is "priv/static" by default.
        assets: /^(static)/
    },

    // Phoenix paths configuration
    paths: {
        // Dependencies and current project directories to watch
        watched: ["static", "css", "js", "vendor"],
        // Where to compile files to
        public: "../priv/static"
    },

    // Configure your plugins
    plugins: {
        babel: {
            "presets": [
                ["env", {
                    "useBuiltIns": true
                }]
            ],
            "plugins": [
                ["transform-runtime", {
                    "polyfill": false,
                    "regenerator": true
                }],
                ["transform-object-assign"]
            ]
        }
    },

    modules: {
        autoRequire: {
            "js/app.js": ["js/app"]
        }
    },

    npm: {
        enabled: true,
        whitelist: ["phoenix", "phoenix_html", "vue", "babel-runtime"]
    }
};


code:
/*This file is for your main application css*/
:root {
    --base-text: #C7C7C7; /*Everything from here to...*/
    --blue-text: #3C8FFF;
    --pink-text: #F557FF;
    --green-text: #57FF53;
    --red-text: #FF0000; /*... here is unused*/
    --qtstream: #f1d98d; /*Logo for the site*/
    --dark-background: #202127; /*Background for the whole site*/
    --dark-background2:#1A1B20; /*Used nowhere*/
    --dropzone-bg: #2d2d2d; /*Used nowhere*/
    --lighter-dark-background: #32353e; /*Used for various backgrounds, mainly the header and the login/registration forms*/
    --lighter-dark-background2: #292b32; /*This one effects the even files on the file list*/
    --odd-file: #3e424c; /*Used for the odd file coloring*/
    --del-button:#462b2d; /*Used for the delete button coloring*/
    --l-bg:#54555B; /*Used nowhere*/
    --border-color: #4f5c6d; /*Affects the header's border*/
    --pointless-border: #626262; /*Used nowhere*/
    --dimmer: rgba(255, 255, 255, 0.5); /*Is used when the page is dimmed*/
    --filter-border:#404552; /*This one effects the border of the filter on the file list*/
    --general-text:white; /*Affects the base text of the website*/
    --expire-text: #b1b5c4; /*Affects the file expiration date?*/
    /*These are visited and unvisited links*/
    --new-link: #3961F8;
    --visited-link: #9660FA;
    /*These are for the file list the first two determine the file uploader border and background. The second one effects the color of the text*/
    --uploader-boarder: #58525b;
    --uploader-bg: #273049;
    --file-link: #d2d5e0;
    /*The first one effects the font of the input text on the registration and login page when it's not focused*/
    --input-color: rgb(140,143,148);
    /*These are for the input forms on the login and registration page*/
    --input-underline: #4e588d;
    --input-background: #373743;
    --input-text: #c7c9db;
    /*These affect the upload button*/
    --upload-bg: #2b3246;
    --upload-boarder: #0e1320;
    --upload-text: #e8e8e8;
}

@open-sans {
    font-family: "Open Sans";
    src: url("/font/OpenSans-Regular.ttf")
}

* {
    padding: 0px;
    margin: 0px;
}

a {
    /* color: #4458ED; */
    color: var(--new-link);
}

a:visited {
    color: var(--visited-link);
}

body {
    font-family: Open Sans;
    font-size: 16px;
    color: var(--general-text);
    background: var(--dark-background);
}

.body-smaller {
    font-size: 14px;
}

.content-container {
    text-align: center;
    padding: 20px;
}

.nav-container {
    width: 100%;
    border-color: var(--lighter-dark-background);
    border-bottom: 2px solid var(--border-color);
    background-color: var(--lighter-dark-background);
}

.navigation {
    width: 100%;
    position: relative;
    font-size: 25px;
    line-height: 100px;
}

#room {
    background: var(--dark-background);
    margin-left: 10px;
    margin-right: 10px;
}

.header-icon {
    text-align: center;
    padding: 15px;
    /* background-color: var(--dropzone-bg); */
    background-color: var(--lighter-dark-background);
    /* width: 100%; */
}

#header-image {
    max-width: 40px;
}

.qtstream {
    padding-left: 10px;
    color: var(--qtstream);
    font-weight: bold;
}

.nav-text {
    color: var(--base-text);
    font-weight: normal;
}

.user-profile {
    text-align: center;
    width: 100%;
}

.huge-text {
    font-size: 24px;
    text-align: center;
    padding: 20px;
}

/*
.huge-text a:link {
    color: var(--blue-text);
}

.stream-url-container {
    text-align: center;
    width: 100%;
}

.stream-url-box {
    display: inline-block;
    margin: auto;
    background-color: var(--dropzone-bg);
    padding: 20px;
    border-radius: 5px;
    border: 2px solid #555555;
}

.stream-url {
    color: var(--pink-text);
    font-weight: bold;
}

.live-url {
    color: var(--red-text);
}

.key {
    color: var(--blue-text);
}
*/

hr {
    border-top: 1px solid var(--pointless-border);
    margin-top: 10px;
    margin-bottom: 5px;
}

#file-dropzone {
    background-color: var(--dropzone-bg);
}

#file-list-container {
    box-sizing: border-box;
}

.column, html, body, .container, main {
    display: flex;
    flex-direction: column;
    flex-wrap: nowrap;
    justify-content: flex-start;
    align-items: stretch;
    align-content: stretch;
    max-width: 100%;
    margin: 0;
    padding: 0;
}

.column-wrap {
    flex-wrap: wrap;
}

.row {
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    justify-content: flex-start;
    align-items: baseline;
    align-content: flex-start;
    max-height: 100%;
    margin: 0;
    padding: 0;
}

.row-right {
    justify-content: flex-end;
}

.row-spaced {
    justify-content: space-between;
}

.grow, .wrapped, body, .container, main {
    flex-grow: 1;
}

html {
    height: 100%;
}

.grow-double {
    flex-grow: 2;
}

.shrink {
    flex-shrink: 1;
}

.no-shrink {
    flex-shrink: 0;
}

.no-grow {
    flex-grow: 0;
}

.shrink-double {
    flex-shrink: 2;
}

.scroll-container {
    overflow-y: auto;
}

.scroll-content {
    min-height: 100%;
    width: 100%;
}

.auto-basis {
    flex-basis: auto;
}

.zero-basis {
    flex-basis: 0;
}

.file-container {
    padding-top: 8px;
    padding-bottom: 8px;
    padding-left: 4px;
    padding-right: 3px;
    background-color: var(--lighter-dark-background);
}

.file-link {
    margin-left: 7px;
    text-decoration: none;
    color: var(--file-link);
    font-weight: 500;
    display: block;
}

.file-uploader {
    background: var(--uploader-bg);
    border-style: solid;
    border-color: var(--uploader-boarder);
    border-width: 1px;
    border-radius: 2px;
    padding-left: 2px;
    padding-right: 2px;
    font-size: 11px;
    top: 50%;
    margin-left: 5px;
    display: block;
}

[v-cloak] {
    visibility: hidden;
}

.hidden {
    visibility: hidden;
}

.form-group {
    margin: 10px;
}

#dim-screen {
    position: fixed;
    padding: 0;
    margin: 0;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: var(--dimmer);
    display: flex;
}

#loading-message {
    margin: auto;
    font-size: 36px;
    text-align: center;
}

.register-form {
    background: var(--lighter-dark-background);
    margin-left: 10%;
    margin-right: 10%;
    margin-top: 2%;
    padding-bottom: 15px;
    padding-top: 8px;
    border: 1px solid var(--form-border);
    border-radius: 5px;
}

.login-form {
    background: var(--lighter-dark-background);
    margin-left: 30%;
    margin-right: 30%;
    margin-top: 1%;
    padding-bottom: 15px;
    padding-top: 5px;
    border: 1px solid var(--form-border);
    border-radius: 5px;
    text-align: center;
}

.register-field input, .login-field input, span.info {
    position: relative;
    padding-top: 15px;
    padding-left: 10px;
}

.register-field input, .login-field input {
    background: var(--lighter-dark-background);
    border-style: none;
    border-bottom-style: solid;
    border-color: var(--lighter-dark-background);
    transition: ease-in-out 0.2s;
    width: 40%;
    max-width: 300px;
    color: var(--input-color);
    font-size: 18px;
}

.login-field input {
    width: 90%;
    max-width: 800px;
}

.register-field input:focus, .login-field input:focus {
    border-color: var(--input-underline);
    background: var(--input-background);
    color: var(--input-text);
    transition: ease-in-out 0.2s;
}

.login button {
    margin-left: 30%;
}

.register button {
    margin-left: 10%;
}

.login button, .register button {
    margin-top: 1%;
}

.register-form h1 {
    padding-left: 10px;
}

main .room, main .room-list {
    text-align: center;
}

.room-list  * {
    display: inline-block;
    text-align: center;
}

.room-list {
    padding-top: 5px;
    padding-bottom: 5px;
    font-size: 18px;
}

.room-list > .room-id {
    width: 10%;
    min-width: 100px;
}

.room-list > .room-name {
    width: 50%;
}

.room-list > .room-owner {
    width: 15%;
}

.room * {
    text-align: center;
    display: inline-block;
}

.room > .room-id {
    width: 10%;
    min-width: 100px;
}

.room > .room-name {
    width: 50%;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.room > .room-owner {
    width: 15%;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

h1, .register-field > span, .red, .presence {
    color: #c9c9c9;
}

.presence-container, .presence-container-small {
    max-height: 20vh;
}

.control-bar {
}

.presence-container-medium {
    max-height: 50vh;
}

.presence-container-big {
    max-height: 75vh;
}

.collapse {
    visibility: collapse;
}

.button, .custom-file-upload, button {
    position: relative;
    background: var(--upload-bg);
    border-radius: 3px;
    border: 1px solid var(--upload-border);
    display: block;
    cursor: pointer;
    color: var(--upload-text);
    font-family: Arial;
    font-size: 15px;
    font-weight: bold;
    padding: 11px 23px;
    text-decoration: none;
    transition: 0.1s;
    text-align: center;
}

button:hover, .custom-file-upload:hover, #delete-button:hover {
    filter: brightness(120%);
    transition: 0.1s;
}

.delete.button {
    background: transparent;
    border-style: none;
}

#upload-button, #delete-button, .custom-file-upload {
    vertical-align: middle;
}

#delete-button {
    background: var(--del-button);
}

.small.delete.button, .mark{
    margin: 1px 3px 1px 3px;
    padding: 0;
    vertical-align: middle;
    display: block;
}

.row-centre {
    align-content: center;
}

.file-even {
    background: var(--lighter-dark-background);
}

.file-odd {
    background: var(--odd-file);
}

.file-expiration {
    font-size: 10px;
    color: var(--expire-text);
    margin: auto 6px auto 6px;
    display: block;
}

.nav-text {
    text-decoration: none
}

.register-form h1 {
    padding-left: 10px;
}

.header-icon, .red {
    background: var(--lighter-dark-background);
}

#file-dropzone input {
    border-style: solid;
    border-radius: 3px;
    border-color: var(--filter-border);
}

input[type="file"] {
    display: none;
}

/*#file-dropzone input {
    background: #2b2e36;
    border-top-style: none;
    border-bottom-style: none;
    border-width: 1px;
    border-color: rgb(100,100,100);
    border-left-style: solid;
    border-right-style: solid;
    padding-left: 5px;
    padding-top: 2px;
    padding-bottom: 2px;
}*/