1import { assert } from './util/util.js'; 2 3// The state of the preprocessor is a stack of States. 4type StateStack = { allowsFollowingElse: boolean; state: State }[]; 5const 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 16abstract 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 33class 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 56class ElseIf extends If { 57 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 70class 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 83class 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 * const shader = pp` 95 * ${pp._if(expr)} 96 * const x: ${type} = ${value}; 97 * ${pp._elif(expr)} 98 * ${pp.__if(expr)} 99 * ... 100 * ${pp.__else} 101 * ... 102 * ${pp.__endif} 103 * ${pp._endif}`; 104 * 105 * @param strings - The array of constant string chunks of the template string. 106 * @param ...values - The array of interpolated ${} values within the template string. 107 */ 108export function pp( 109 strings: TemplateStringsArray, 110 ...values: ReadonlyArray<Directive | string | number> 111): string { 112 let result = ''; 113 const stateStack: StateStack = [{ allowsFollowingElse: false, state: State.Passing }]; 114 115 for (let i = 0; i < values.length; ++i) { 116 const passing = stateStack[stateStack.length - 1].state === State.Passing; 117 if (passing) { 118 result += strings[i]; 119 } 120 121 const value = values[i]; 122 if (value instanceof Directive) { 123 value.applyTo(stateStack); 124 } else { 125 if (passing) { 126 result += value; 127 } 128 } 129 } 130 assert(stateStack.length === 1, 'Unterminated preprocessor condition at end of file'); 131 result += strings[values.length]; 132 133 return result; 134} 135pp._if = (predicate: boolean) => new If(1, predicate); 136pp._elif = (predicate: boolean) => new ElseIf(1, predicate); 137pp._else = new Else(1); 138pp._endif = new EndIf(1); 139pp.__if = (predicate: boolean) => new If(2, predicate); 140pp.__elif = (predicate: boolean) => new ElseIf(2, predicate); 141pp.__else = new Else(2); 142pp.__endif = new EndIf(2); 143pp.___if = (predicate: boolean) => new If(3, predicate); 144pp.___elif = (predicate: boolean) => new ElseIf(3, predicate); 145pp.___else = new Else(3); 146pp.___endif = new EndIf(3); 147// Add more if needed. 148