1// META: global=window,worker 2'use strict'; 3 4class LipFuzzTransformer { 5 constructor(substitutions) { 6 this.substitutions = substitutions; 7 this.partialChunk = ''; 8 this.lastIndex = undefined; 9 } 10 11 transform(chunk, controller) { 12 chunk = this.partialChunk + chunk; 13 this.partialChunk = ''; 14 // lastIndex is the index of the first character after the last substitution. 15 this.lastIndex = 0; 16 chunk = chunk.replace(/\{\{([a-zA-Z0-9_-]+)\}\}/g, this.replaceTag.bind(this)); 17 // Regular expression for an incomplete template at the end of a string. 18 const partialAtEndRegexp = /\{(\{([a-zA-Z0-9_-]+(\})?)?)?$/g; 19 // Avoid looking at any characters that have already been substituted. 20 partialAtEndRegexp.lastIndex = this.lastIndex; 21 this.lastIndex = undefined; 22 const match = partialAtEndRegexp.exec(chunk); 23 if (match) { 24 this.partialChunk = chunk.substring(match.index); 25 chunk = chunk.substring(0, match.index); 26 } 27 controller.enqueue(chunk); 28 } 29 30 flush(controller) { 31 if (this.partialChunk.length > 0) { 32 controller.enqueue(this.partialChunk); 33 } 34 } 35 36 replaceTag(match, p1, offset) { 37 let replacement = this.substitutions[p1]; 38 if (replacement === undefined) { 39 replacement = ''; 40 } 41 this.lastIndex = offset + replacement.length; 42 return replacement; 43 } 44} 45 46const substitutions = { 47 in1: 'out1', 48 in2: 'out2', 49 quine: '{{quine}}', 50 bogusPartial: '{{incompleteResult}' 51}; 52 53const cases = [ 54 { 55 input: [''], 56 output: [''] 57 }, 58 { 59 input: [], 60 output: [] 61 }, 62 { 63 input: ['{{in1}}'], 64 output: ['out1'] 65 }, 66 { 67 input: ['z{{in1}}'], 68 output: ['zout1'] 69 }, 70 { 71 input: ['{{in1}}q'], 72 output: ['out1q'] 73 }, 74 { 75 input: ['{{in1}}{{in1}'], 76 output: ['out1', '{{in1}'] 77 }, 78 { 79 input: ['{{in1}}{{in1}', '}'], 80 output: ['out1', 'out1'] 81 }, 82 { 83 input: ['{{in1', '}}'], 84 output: ['', 'out1'] 85 }, 86 { 87 input: ['{{', 'in1}}'], 88 output: ['', 'out1'] 89 }, 90 { 91 input: ['{', '{in1}}'], 92 output: ['', 'out1'] 93 }, 94 { 95 input: ['{{', 'in1}'], 96 output: ['', '', '{{in1}'] 97 }, 98 { 99 input: ['{'], 100 output: ['', '{'] 101 }, 102 { 103 input: ['{', ''], 104 output: ['', '', '{'] 105 }, 106 { 107 input: ['{', '{', 'i', 'n', '1', '}', '}'], 108 output: ['', '', '', '', '', '', 'out1'] 109 }, 110 { 111 input: ['{{in1}}{{in2}}{{in1}}'], 112 output: ['out1out2out1'] 113 }, 114 { 115 input: ['{{wrong}}'], 116 output: [''] 117 }, 118 { 119 input: ['{{wron', 'g}}'], 120 output: ['', ''] 121 }, 122 { 123 input: ['{{quine}}'], 124 output: ['{{quine}}'] 125 }, 126 { 127 input: ['{{bogusPartial}}'], 128 output: ['{{incompleteResult}'] 129 }, 130 { 131 input: ['{{bogusPartial}}}'], 132 output: ['{{incompleteResult}}'] 133 } 134]; 135 136for (const testCase of cases) { 137 const inputChunks = testCase.input; 138 const outputChunks = testCase.output; 139 promise_test(() => { 140 const lft = new TransformStream(new LipFuzzTransformer(substitutions)); 141 const writer = lft.writable.getWriter(); 142 const promises = []; 143 for (const inputChunk of inputChunks) { 144 promises.push(writer.write(inputChunk)); 145 } 146 promises.push(writer.close()); 147 const reader = lft.readable.getReader(); 148 let readerChain = Promise.resolve(); 149 for (const outputChunk of outputChunks) { 150 readerChain = readerChain.then(() => { 151 return reader.read().then(({ value, done }) => { 152 assert_false(done, `done should be false when reading ${outputChunk}`); 153 assert_equals(value, outputChunk, `value should match outputChunk`); 154 }); 155 }); 156 } 157 readerChain = readerChain.then(() => { 158 return reader.read().then(({ done }) => assert_true(done, `done should be true`)); 159 }); 160 promises.push(readerChain); 161 return Promise.all(promises); 162 }, `testing "${inputChunks}" (length ${inputChunks.length})`); 163} 164