tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

preprocessor.ts (4119B)


      1 import { assert } from './util.js';
      2 
      3 // The state of the preprocessor is a stack of States.
      4 type StateStack = { allowsFollowingElse: boolean; state: State }[];
      5 const enum State {
      6  Seeking, // Still looking for a passing condition
      7  Passing, // Currently inside a passing condition (the root is always in this state)
      8  Skipping, // Have already seen a passing condition; now skipping the rest
      9 }
     10 
     11 // The transitions in the state space are the following preprocessor directives:
     12 // - Sibling elif
     13 // - Sibling else
     14 // - Sibling endif
     15 // - Child if
     16 abstract class Directive {
     17  private readonly depth: number;
     18 
     19  constructor(depth: number) {
     20    this.depth = depth;
     21  }
     22 
     23  protected checkDepth(stack: StateStack): void {
     24    assert(
     25      stack.length === this.depth,
     26      `Number of "$"s must match nesting depth, currently ${stack.length} (e.g. $if $$if $$endif $endif)`
     27    );
     28  }
     29 
     30  abstract applyTo(stack: StateStack): void;
     31 }
     32 
     33 class If extends Directive {
     34  private readonly predicate: boolean;
     35 
     36  constructor(depth: number, predicate: boolean) {
     37    super(depth);
     38    this.predicate = predicate;
     39  }
     40 
     41  applyTo(stack: StateStack) {
     42    this.checkDepth(stack);
     43    const parentState = stack[stack.length - 1].state;
     44    stack.push({
     45      allowsFollowingElse: true,
     46      state:
     47        parentState !== State.Passing
     48          ? State.Skipping
     49          : this.predicate
     50          ? State.Passing
     51          : State.Seeking,
     52    });
     53  }
     54 }
     55 
     56 class ElseIf extends If {
     57  override applyTo(stack: StateStack) {
     58    assert(stack.length >= 1);
     59    const { allowsFollowingElse, state: siblingState } = stack.pop()!;
     60    this.checkDepth(stack);
     61    assert(allowsFollowingElse, 'pp.elif after pp.else');
     62    if (siblingState !== State.Seeking) {
     63      stack.push({ allowsFollowingElse: true, state: State.Skipping });
     64    } else {
     65      super.applyTo(stack);
     66    }
     67  }
     68 }
     69 
     70 class Else extends Directive {
     71  applyTo(stack: StateStack) {
     72    assert(stack.length >= 1);
     73    const { allowsFollowingElse, state: siblingState } = stack.pop()!;
     74    this.checkDepth(stack);
     75    assert(allowsFollowingElse, 'pp.else after pp.else');
     76    stack.push({
     77      allowsFollowingElse: false,
     78      state: siblingState === State.Seeking ? State.Passing : State.Skipping,
     79    });
     80  }
     81 }
     82 
     83 class EndIf extends Directive {
     84  applyTo(stack: StateStack) {
     85    stack.pop();
     86    this.checkDepth(stack);
     87  }
     88 }
     89 
     90 /**
     91 * A simple template-based, non-line-based preprocessor implementing if/elif/else/endif.
     92 *
     93 * @example
     94 * ```
     95 *     const shader = pp`
     96 * ${pp._if(expr)}
     97 *   const x: ${type} = ${value};
     98 * ${pp._elif(expr)}
     99 * ${pp.__if(expr)}
    100 * ...
    101 * ${pp.__else}
    102 * ...
    103 * ${pp.__endif}
    104 * ${pp._endif}`;
    105 * ```
    106 *
    107 * @param strings - The array of constant string chunks of the template string.
    108 * @param ...values - The array of interpolated `${}` values within the template string.
    109 */
    110 export function pp(
    111  strings: TemplateStringsArray,
    112  ...values: ReadonlyArray<Directive | string | number>
    113 ): string {
    114  let result = '';
    115  const stateStack: StateStack = [{ allowsFollowingElse: false, state: State.Passing }];
    116 
    117  for (let i = 0; i < values.length; ++i) {
    118    const passing = stateStack[stateStack.length - 1].state === State.Passing;
    119    if (passing) {
    120      result += strings[i];
    121    }
    122 
    123    const value = values[i];
    124    if (value instanceof Directive) {
    125      value.applyTo(stateStack);
    126    } else {
    127      if (passing) {
    128        result += value;
    129      }
    130    }
    131  }
    132  assert(stateStack.length === 1, 'Unterminated preprocessor condition at end of file');
    133  result += strings[values.length];
    134 
    135  return result;
    136 }
    137 pp._if = (predicate: boolean) => new If(1, predicate);
    138 pp._elif = (predicate: boolean) => new ElseIf(1, predicate);
    139 pp._else = new Else(1);
    140 pp._endif = new EndIf(1);
    141 pp.__if = (predicate: boolean) => new If(2, predicate);
    142 pp.__elif = (predicate: boolean) => new ElseIf(2, predicate);
    143 pp.__else = new Else(2);
    144 pp.__endif = new EndIf(2);
    145 pp.___if = (predicate: boolean) => new If(3, predicate);
    146 pp.___elif = (predicate: boolean) => new ElseIf(3, predicate);
    147 pp.___else = new Else(3);
    148 pp.___endif = new EndIf(3);
    149 // Add more if needed.