Skip to content

Instantly share code, notes, and snippets.

@fuweichin
Last active July 1, 2022 21:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save fuweichin/3b8d7d757f7a08ebeda6e0f0ae5a7dda to your computer and use it in GitHub Desktop.
Save fuweichin/3b8d7d757f7a08ebeda6e0f0ae5a7dda to your computer and use it in GitHub Desktop.
given a template literal and a scope object, return resolved string
<!-- 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>
// 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]);
});
}
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment