| // META: global=window,worker,jsshell |
| 'use strict'; |
| |
| class LipFuzzTransformer { |
| constructor(substitutions) { |
| this.substitutions = substitutions; |
| this.partialChunk = ''; |
| this.lastIndex = undefined; |
| } |
| |
| transform(chunk, controller) { |
| chunk = this.partialChunk + chunk; |
| this.partialChunk = ''; |
| // lastIndex is the index of the first character after the last substitution. |
| this.lastIndex = 0; |
| chunk = chunk.replace(/\{\{([a-zA-Z0-9_-]+)\}\}/g, this.replaceTag.bind(this)); |
| // Regular expression for an incomplete template at the end of a string. |
| const partialAtEndRegexp = /\{(\{([a-zA-Z0-9_-]+(\})?)?)?$/g; |
| // Avoid looking at any characters that have already been substituted. |
| partialAtEndRegexp.lastIndex = this.lastIndex; |
| this.lastIndex = undefined; |
| const match = partialAtEndRegexp.exec(chunk); |
| if (match) { |
| this.partialChunk = chunk.substring(match.index); |
| chunk = chunk.substring(0, match.index); |
| } |
| controller.enqueue(chunk); |
| } |
| |
| flush(controller) { |
| if (this.partialChunk.length > 0) { |
| controller.enqueue(this.partialChunk); |
| } |
| } |
| |
| replaceTag(match, p1, offset) { |
| let replacement = this.substitutions[p1]; |
| if (replacement === undefined) { |
| replacement = ''; |
| } |
| this.lastIndex = offset + replacement.length; |
| return replacement; |
| } |
| } |
| |
| const substitutions = { |
| in1: 'out1', |
| in2: 'out2', |
| quine: '{{quine}}', |
| bogusPartial: '{{incompleteResult}' |
| }; |
| |
| const cases = [ |
| { |
| input: [''], |
| output: [''] |
| }, |
| { |
| input: [], |
| output: [] |
| }, |
| { |
| input: ['{{in1}}'], |
| output: ['out1'] |
| }, |
| { |
| input: ['z{{in1}}'], |
| output: ['zout1'] |
| }, |
| { |
| input: ['{{in1}}q'], |
| output: ['out1q'] |
| }, |
| { |
| input: ['{{in1}}{{in1}'], |
| output: ['out1', '{{in1}'] |
| }, |
| { |
| input: ['{{in1}}{{in1}', '}'], |
| output: ['out1', 'out1'] |
| }, |
| { |
| input: ['{{in1', '}}'], |
| output: ['', 'out1'] |
| }, |
| { |
| input: ['{{', 'in1}}'], |
| output: ['', 'out1'] |
| }, |
| { |
| input: ['{', '{in1}}'], |
| output: ['', 'out1'] |
| }, |
| { |
| input: ['{{', 'in1}'], |
| output: ['', '', '{{in1}'] |
| }, |
| { |
| input: ['{'], |
| output: ['', '{'] |
| }, |
| { |
| input: ['{', ''], |
| output: ['', '', '{'] |
| }, |
| { |
| input: ['{', '{', 'i', 'n', '1', '}', '}'], |
| output: ['', '', '', '', '', '', 'out1'] |
| }, |
| { |
| input: ['{{in1}}{{in2}}{{in1}}'], |
| output: ['out1out2out1'] |
| }, |
| { |
| input: ['{{wrong}}'], |
| output: [''] |
| }, |
| { |
| input: ['{{wron', 'g}}'], |
| output: ['', ''] |
| }, |
| { |
| input: ['{{quine}}'], |
| output: ['{{quine}}'] |
| }, |
| { |
| input: ['{{bogusPartial}}'], |
| output: ['{{incompleteResult}'] |
| }, |
| { |
| input: ['{{bogusPartial}}}'], |
| output: ['{{incompleteResult}}'] |
| } |
| ]; |
| |
| for (const testCase of cases) { |
| const inputChunks = testCase.input; |
| const outputChunks = testCase.output; |
| promise_test(() => { |
| const lft = new TransformStream(new LipFuzzTransformer(substitutions)); |
| const writer = lft.writable.getWriter(); |
| const promises = []; |
| for (const inputChunk of inputChunks) { |
| promises.push(writer.write(inputChunk)); |
| } |
| promises.push(writer.close()); |
| const reader = lft.readable.getReader(); |
| let readerChain = Promise.resolve(); |
| for (const outputChunk of outputChunks) { |
| readerChain = readerChain.then(() => { |
| return reader.read().then(({ value, done }) => { |
| assert_false(done, `done should be false when reading ${outputChunk}`); |
| assert_equals(value, outputChunk, `value should match outputChunk`); |
| }); |
| }); |
| } |
| readerChain = readerChain.then(() => { |
| return reader.read().then(({ done }) => assert_true(done, `done should be true`)); |
| }); |
| promises.push(readerChain); |
| return Promise.all(promises); |
| }, `testing "${inputChunks}" (length ${inputChunks.length})`); |
| } |