Skip to content

Instantly share code, notes, and snippets.

@ivawzh
Created September 13, 2020 14:35
Show Gist options
  • Save ivawzh/44cc105072b287d2793e19a1b77ac6e5 to your computer and use it in GitHub Desktop.
Save ivawzh/44cc105072b287d2793e19a1b77ac6e5 to your computer and use it in GitHub Desktop.
TypeScript utils
//@ts-nocheck
// copied from https://github.com/zspecza/common-tags/blob/master/src/stripIndents/stripIndents.js
const tagTransformersSymbol = 'COMMON_TAGS_TAG_TRANSFORMERS_SYMBOL';
function isTag(fn) {
return typeof fn === 'function' && fn[tagTransformersSymbol];
}
function cleanTransformers(transformers) {
return flat(transformers).reduce(
(transformers, transformer) =>
isTag(transformer)
? [...transformers, ...transformer[tagTransformersSymbol]]
: [...transformers, transformer],
[],
);
}
/**
* An intermediary template tag that receives a template tag and passes the result of calling the template with the received
* template tag to our own template tag.
* @param {Function} nextTag - The received template tag
* @param {Array<String>} template - The template to process
* @param {...*} ...substitutions - `substitutions` is an array of all substitutions in the template
* @return {*} - The final processed value
*/
function getInterimTag(originalTag, extraTag) {
return function tag(...args) {
return originalTag(['', ''], extraTag(...args));
};
}
function getTagCallInfo(transformers) {
return {
transformers,
context: transformers.map((transformer) =>
transformer.getInitialContext ? transformer.getInitialContext() : {},
),
};
}
/**
* Iterate through each transformer, calling the transformer's specified hook.
* @param {Array<Function>} transformers - The transformer functions
* @param {String} hookName - The name of the hook
* @param {String} initialString - The input string
* @return {String} - The final results of applying each transformer
*/
function applyHook0({ transformers, context }, hookName, initialString) {
return transformers.reduce(
(result, transformer, index) =>
transformer[hookName]
? transformer[hookName](result, context[index])
: result,
initialString,
);
}
/**
* Iterate through each transformer, calling the transformer's specified hook.
* @param {Array<Function>} transformers - The transformer functions
* @param {String} hookName - The name of the hook
* @param {String} initialString - The input string
* @param {*} arg1 - An additional argument passed to the hook
* @return {String} - The final results of applying each transformer
*/
function applyHook1({ transformers, context }, hookName, initialString, arg1) {
return transformers.reduce(
(result, transformer, index) =>
transformer[hookName]
? transformer[hookName](result, arg1, context[index])
: result,
initialString,
);
}
/**
* Consumes a pipeline of composable transformer plugins and produces a template tag.
* @param {...Object} [...rawTransformers] - An array or arguments list of transformers
* @return {Function} - A template tag
*/
export default function createTag(...rawTransformers) {
const transformers = cleanTransformers(rawTransformers);
function tag(strings, ...expressions) {
if (typeof strings === 'function') {
// if the first argument passed is a function, assume it is a template tag and return
// an intermediary tag that processes the template using the aforementioned tag, passing the
// result to our tag
return getInterimTag(tag, strings);
}
if (!Array.isArray(strings)) {
return tag([strings]);
}
const tagCallInfo = getTagCallInfo(transformers);
// if the first argument is an array, return a transformed end result of processing the template with our tag
const processedTemplate = strings
.map((string) => applyHook0(tagCallInfo, 'onString', string))
.reduce((result, string, index) =>
''.concat(
result,
applyHook1(
tagCallInfo,
'onSubstitution',
expressions[index - 1],
result,
),
string,
),
);
return applyHook0(tagCallInfo, 'onEndResult', processedTemplate);
}
tag[tagTransformersSymbol] = transformers;
return tag;
}
function flat(array) {
return [].concat(...array);
}
const stripIndentTransformerSupportedTypes = ['initial', 'all'];
/**
* strips indentation from a template literal
* @param {String} type = 'initial' - whether to remove all indentation or just leading indentation. can be 'all' or 'initial'
* @return {Object} - a TemplateTag transformer
*/
const stripIndentTransformer = (type = 'initial') => {
if (!stripIndentTransformerSupportedTypes.includes(type)) {
throw new Error(`Type not supported: ${type}`);
}
return {
onEndResult(endResult) {
if (type === 'all') {
// remove all indentation from each line
return endResult.replace(/^[^\S\n]+/gm, '');
}
// remove the shortest leading indentation from each line
const match = endResult.match(/^[^\S\n]*(?=\S)/gm);
const indent = match && Math.min(...match.map((el) => el.length));
if (indent) {
const regexp = new RegExp(`^.{${indent}}`, 'gm');
return endResult.replace(regexp, '');
}
return endResult;
},
};
};
const trimResultTransformerSupportedSides = ['', 'start', 'left', 'end', 'right', 'smart'];
/**
* TemplateTag transformer that trims whitespace on the end result of a tagged template
* @param {String} side = '' - The side of the string to trim. Can be 'start' or 'end' (alternatively 'left' or 'right')
* @return {Object} - a TemplateTag transformer
*/
const trimResultTransformer = (side = '') => {
if (!trimResultTransformerSupportedSides.includes(side)) {
throw new Error(`Side not supported: ${side}`);
}
return {
onEndResult(endResult) {
switch (side) {
case '':
return endResult.trim();
case 'start':
case 'left':
return endResult.replace(/^\s*/, '');
case 'end':
case 'right':
return endResult.replace(/\s*$/, '');
case 'smart':
return endResult.replace(/[^\S\n]+$/gm, '').replace(/^\n/, '');
}
},
};
};
export const stripIndents = createTag(
stripIndentTransformer('all'),
trimResultTransformer('smart'),
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment