Skip to content

Instantly share code, notes, and snippets.

@fuweichin
Last active July 1, 2022 21:02
Show Gist options
  • Select an option

  • Save fuweichin/3b8d7d757f7a08ebeda6e0f0ae5a7dda to your computer and use it in GitHub Desktop.

Select an option

Save fuweichin/3b8d7d757f7a08ebeda6e0f0ae5a7dda to your computer and use it in GitHub Desktop.

Revisions

  1. fuweichin revised this gist Jul 1, 2022. 2 changed files with 128 additions and 101 deletions.
    94 changes: 59 additions & 35 deletions index.html
    Original file line number Diff line number Diff line change
    @@ -1,35 +1,59 @@
    <!-- to resolve synchronously -->
    <script src="resolve-template.js"></script>

    <!-- to resolve asynchronously -->
    <script>
    let worker = new Worker('resolve-template.js');
    function resolveTempalteAsync(source, scope) {
    return new Promise((resolve, reject) => {
    worker.onmessage = (e) => {
    let {result, error} = e.data;
    if (result) {
    resolve(result);
    } else if (error) {
    reject(error);
    } else {
    reject(new Error('Invalid message from worker'));
    }
    };
    worker.onerror = (e) => {
    reject(new Error('Worker loading error'));
    };
    worker.postMessage([source, scope]);
    });
    }
    </script>

    <!-- usage -->
    <script>
    /* global resolveTemplate, resolveTempalteAsync */
    document.addEventListener('DOMContentLoaded', async () => {
    let message1 = resolveTemplate('`Hello ${name}!`', {name: 'World'});
    let message2 = await resolveTempalteAsync('`Hello ${name}!`', {name: 'World'});
    console.log(message1, message2);
    });
    </script>
    <!-- to resolve synchronously -->
    <script src="resolve-template.js"></script>

    <!-- to resolve asynchronously -->
    <script>
    let worker = new Worker('resolve-template.js');
    let idGen = (function* () {
    for (let i = 0; true; i = (i + 1) >> 0) {
    yield i;
    }
    })();
    function resolveTemplateAsync(source, scope) {
    return new Promise((resolve, reject) => {
    let req = {
    // id: crypto.randomUUID(),
    id: idGen.next().value,
    action: 'resolveTemplate',
    data: [source, scope]
    };
    worker.onmessage = (e) => {
    let res = e.data;
    if (res.id !== req.id) {
    return;
    }
    if (res.ok) {
    resolve(res.result);
    } else {
    reject(res.error);
    }
    };
    worker.onerror = (e) => {
    reject(new Error('Worker loading error'));
    };
    worker.postMessage(req);
    });
    }
    </script>

    <!-- usage -->
    <script>
    /* global resolveTemplate, resolveTemplateAsync */
    document.addEventListener('DOMContentLoaded', async () => {
    console.time('resolveTemplate sync');
    let message1 = resolveTemplate('`Hello ${name}!`', {name: 'World'});
    console.timeEnd('resolveTemplate sync');
    console.log(message1);
    try {
    console.time('resolveTemplate async');
    let message2 = await resolveTemplateAsync('`Hello ${name}!`', {name: 'World'});
    console.timeEnd('resolveTemplate async');
    console.time('resolveTemplate async');
    message2 = await resolveTemplateAsync('`Hello ${name}!`', {name: 'World'});
    console.timeEnd('resolveTemplate async');
    console.log(message2);
    } catch (e) {
    console.error(e);
    }
    });
    </script>
    135 changes: 69 additions & 66 deletions resolve-template.js
    Original file line number Diff line number Diff line change
    @@ -1,66 +1,69 @@
    // subset of identifiers, see https://gist.github.com/mathiasbynens/6334847#file-ecmascript-6-js
    let es6Identifier = /^[A-Za-z$_][A-Za-z0-9$_]*$/;
    // for lsit of reserved words, see https://262.ecma-international.org/12.0/#sec-keywords-and-reserved-words
    let reservedWords = new Set(
    ['await', 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'default', 'delete', 'do', 'else', 'enum', 'export', 'extends', 'false', 'finally', 'for', 'function', 'if', 'import', 'in', 'instanceof', 'new', 'null', 'return', 'super', 'switch', 'this', 'throw', 'true', 'try', 'typeof', 'var', 'void', 'while', 'with', 'yield']
    .concat(['enum', 'implements', 'interface', 'package', 'private', 'protected', 'public', 'arguments', 'eval'], ['undefined'])
    );
    /**
    * @param {string} source - template literal quoted with grave accent (`)
    * @param {object} scope - if a reserved word / non-identifier (e.g. "default", "user-name") is used as property name,
    * then use "this", e.g. ${this.default} / ${this['user-name']}, to access the scope object
    * @returns {string}
    * @example resolveTemplate('`Hello ${name}!`', {name:'World'})
    */
    function resolveTemplate(source, scope) {
    if (!(source.length >= 2 && source.charAt(0) === '`' && source.charAt(source.length - 1) === '`')) {
    throw new TypeError('Invalid template source');
    }
    let identifiers = Object.getOwnPropertyNames(scope).filter((word) => {
    return es6Identifier.test(word) && !reservedWords.has(word);
    });
    let fn = new Function(`"use strict";${identifiers.length > 0 ? 'let {' + identifiers.join(',') + '}=this;' : ''}return ${source};`);
    return fn.call(scope);
    }

    /* ====== when loaded as worker script ====== */
    if (self.importScripts) {
    //
    self.addEventListener('message', (e) => {
    let result = resolveTemplate(...e.data);
    self.postMessage({result});
    });
    self.addEventListener('error', function(e) {
    let obj;
    let error = e.error;
    if (Object.prototype.toString.call(error) === '[object Error]') {
    obj = {name: error.name, message: error.message, stack: error.stack};
    } else {
    obj = {name: 'Error', message: e.message, stack: 'at ' + e.filename + ':' + e.lineno + ':' + e.colno};
    }
    self.postMessage({error: obj});
    });
    }

    /* ====== to use worker script in main thread ====== */
    /*
    let worker = new Worker('resolve-template.js');
    function resolveTempalteAsync(source, scope) {
    return new Promise((resolve, reject) => {
    worker.onmessage = (e) => {
    let {result, error} = e.data;
    if (result) {
    resolve(result);
    } else if (error) {
    reject(error);
    } else {
    reject(new Error('Invalid message from worker'));
    }
    };
    worker.onerror = (e) => {
    reject(new Error('Worker loading error'));
    };
    worker.postMessage([source, scope]);
    });
    }
    */
    // subset of identifiers, see https://gist.github.com/mathiasbynens/6334847#file-ecmascript-6-js
    let es6Identifier = /^[A-Za-z$_][A-Za-z0-9$_]*$/;
    // for lsit of reserved words, see https://262.ecma-international.org/12.0/#sec-keywords-and-reserved-words
    let reservedWords = new Set(
    ['await', 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'default', 'delete', 'do', 'else', 'enum', 'export', 'extends', 'false', 'finally', 'for', 'function', 'if', 'import', 'in', 'instanceof', 'new', 'null', 'return', 'super', 'switch', 'this', 'throw', 'true', 'try', 'typeof', 'var', 'void', 'while', 'with', 'yield']
    .concat(['enum', 'implements', 'interface', 'package', 'private', 'protected', 'public', 'arguments', 'eval'], ['undefined'])
    );
    /**
    * @param {string} source - template literal quoted with grave accent (`)
    * @param {object} scope - if a reserved word / non-identifier (e.g. "default", "user-name") is used as property name,
    * then use "this", e.g. ${this.default} / ${this['user-name']}, to access the scope object
    * @returns {string}
    * @example resolveTemplate('`Hello ${name}!`', {name:'World'})
    */
    function resolveTemplate(source, scope) {
    if (!(source.length >= 2 && source.charAt(0) === '`' && source.charAt(source.length - 1) === '`')) {
    throw new TypeError('Invalid template source');
    }
    let identifiers = Object.getOwnPropertyNames(scope).filter((word) => {
    return es6Identifier.test(word) && !reservedWords.has(word);
    });
    let fn = new Function(`"use strict";${identifiers.length > 0 ? 'let {' + identifiers.join(',') + '}=this;' : ''}return ${source};`);
    return fn.call(scope);
    }

    /* ====== when loaded as worker script ====== */
    if (self.importScripts) {
    self.addEventListener('message', (e) => {
    let req = e.data;
    switch (req.action) {
    case 'resolveTemplate': {
    try {
    let result = resolveTemplate(...req.data);
    self.postMessage({id: req.id, ok: true, result});
    } catch (e) {
    self.postMessage({id: req.id, ok: false, error: e});
    }
    break;
    }
    default: {
    self.postMessage({id: req.id, ok: false, error: new Error('No such action: ' + req.action)});
    break;
    }
    }
    });
    }

    /* ====== to use worker script in main thread ====== */
    /*
    let worker = new Worker('resolve-template.js');
    function resolveTempalteAsync(source, scope) {
    return new Promise((resolve, reject) => {
    worker.onmessage = (e) => {
    let {result, error} = e.data;
    if (result) {
    resolve(result);
    } else if (error) {
    reject(error);
    } else {
    reject(new Error('Invalid message from worker'));
    }
    };
    worker.onerror = (e) => {
    reject(new Error('Worker loading error'));
    };
    worker.postMessage([source, scope]);
    });
    }
    */
  2. fuweichin revised this gist Jun 12, 2022. No changes.
  3. fuweichin renamed this gist Jun 12, 2022. 1 changed file with 0 additions and 0 deletions.
    File renamed without changes.
  4. fuweichin created this gist Jun 12, 2022.
    35 changes: 35 additions & 0 deletions resolve-template.html
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,35 @@
    <!-- to resolve synchronously -->
    <script src="resolve-template.js"></script>

    <!-- to resolve asynchronously -->
    <script>
    let worker = new Worker('resolve-template.js');
    function resolveTempalteAsync(source, scope) {
    return new Promise((resolve, reject) => {
    worker.onmessage = (e) => {
    let {result, error} = e.data;
    if (result) {
    resolve(result);
    } else if (error) {
    reject(error);
    } else {
    reject(new Error('Invalid message from worker'));
    }
    };
    worker.onerror = (e) => {
    reject(new Error('Worker loading error'));
    };
    worker.postMessage([source, scope]);
    });
    }
    </script>

    <!-- usage -->
    <script>
    /* global resolveTemplate, resolveTempalteAsync */
    document.addEventListener('DOMContentLoaded', async () => {
    let message1 = resolveTemplate('`Hello ${name}!`', {name: 'World'});
    let message2 = await resolveTempalteAsync('`Hello ${name}!`', {name: 'World'});
    console.log(message1, message2);
    });
    </script>
    66 changes: 66 additions & 0 deletions resolve-template.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,66 @@
    // subset of identifiers, see https://gist.github.com/mathiasbynens/6334847#file-ecmascript-6-js
    let es6Identifier = /^[A-Za-z$_][A-Za-z0-9$_]*$/;
    // for lsit of reserved words, see https://262.ecma-international.org/12.0/#sec-keywords-and-reserved-words
    let reservedWords = new Set(
    ['await', 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'default', 'delete', 'do', 'else', 'enum', 'export', 'extends', 'false', 'finally', 'for', 'function', 'if', 'import', 'in', 'instanceof', 'new', 'null', 'return', 'super', 'switch', 'this', 'throw', 'true', 'try', 'typeof', 'var', 'void', 'while', 'with', 'yield']
    .concat(['enum', 'implements', 'interface', 'package', 'private', 'protected', 'public', 'arguments', 'eval'], ['undefined'])
    );
    /**
    * @param {string} source - template literal quoted with grave accent (`)
    * @param {object} scope - if a reserved word / non-identifier (e.g. "default", "user-name") is used as property name,
    * then use "this", e.g. ${this.default} / ${this['user-name']}, to access the scope object
    * @returns {string}
    * @example resolveTemplate('`Hello ${name}!`', {name:'World'})
    */
    function resolveTemplate(source, scope) {
    if (!(source.length >= 2 && source.charAt(0) === '`' && source.charAt(source.length - 1) === '`')) {
    throw new TypeError('Invalid template source');
    }
    let identifiers = Object.getOwnPropertyNames(scope).filter((word) => {
    return es6Identifier.test(word) && !reservedWords.has(word);
    });
    let fn = new Function(`"use strict";${identifiers.length > 0 ? 'let {' + identifiers.join(',') + '}=this;' : ''}return ${source};`);
    return fn.call(scope);
    }

    /* ====== when loaded as worker script ====== */
    if (self.importScripts) {
    //
    self.addEventListener('message', (e) => {
    let result = resolveTemplate(...e.data);
    self.postMessage({result});
    });
    self.addEventListener('error', function(e) {
    let obj;
    let error = e.error;
    if (Object.prototype.toString.call(error) === '[object Error]') {
    obj = {name: error.name, message: error.message, stack: error.stack};
    } else {
    obj = {name: 'Error', message: e.message, stack: 'at ' + e.filename + ':' + e.lineno + ':' + e.colno};
    }
    self.postMessage({error: obj});
    });
    }

    /* ====== to use worker script in main thread ====== */
    /*
    let worker = new Worker('resolve-template.js');
    function resolveTempalteAsync(source, scope) {
    return new Promise((resolve, reject) => {
    worker.onmessage = (e) => {
    let {result, error} = e.data;
    if (result) {
    resolve(result);
    } else if (error) {
    reject(error);
    } else {
    reject(new Error('Invalid message from worker'));
    }
    };
    worker.onerror = (e) => {
    reject(new Error('Worker loading error'));
    };
    worker.postMessage([source, scope]);
    });
    }
    */