jscc
Version:
Tiny and powerful preprocessor for conditional comments and replacement of compile-time variables in text files
182 lines (148 loc) • 4.65 kB
text/typescript
/*
Variable replacement inside the code.
*/
import R = require('./regexes')
/** Quote types to escape in strings */
const enum QUOTES {
Single = 1,
Double = 2,
}
/**
* Helper for call `JSON.stringify` in `stringifyObject`.
*
* It outputs a valid number for non-finite numbers and the source string of
* regexes because stringify converts `Infinity` and `-Infinity` to `null`
* and regexes to `{}` (an empty object).
*/
const stringifyFn = (_: string, value: any) => {
if (value && value.constructor.name === 'Number') {
/*
It is necessary to coerce the value to a primitive number because
`Infinity !== new Number(Infinity)`, and the like for `-Infinity`.
*/
if (+value === Infinity) {
return Number.MAX_VALUE
}
if (+value === -Infinity) {
return Number.MIN_VALUE
}
}
return value instanceof RegExp ? value.source : value
}
/**
* Stringify a non-null object `obj` using the following rules:
*
* - NaN -> 'NaN' (a Number or Date with a NaN value)
* - RegExp -> Regex source
* - Date -> Date string in JSON format
* - other -> JSON.stringify(obj)
*
* @param obj Trueish object
* @returns String representation of the object
*/
const stringObject = (obj: object) => {
let str: string
// toISOString throw with NaN dates, toJSON returns `null`
if (obj instanceof Date) {
str = isNaN(+obj) ? 'NaN' : obj.toJSON()
} else if (obj instanceof RegExp) {
str = obj.source
} else if (obj instanceof String) {
str = obj.valueOf()
} else if (obj instanceof Number) {
str = String(obj)
} else {
str = JSON.stringify(obj, stringifyFn)
}
return str
}
/**
* Stringify the given value using this rules:
*
* - undefined -> 'undefined'
* - null -> 'null'
* - NaN -> 'NaN'
* - Infinity -> 'Infinity'
* - RegExp -> JSON.stringify(value.source)
* - objects -> JSON.stringify(value) (Date as ISO string or `null`)
* - primitives -> String(value)
*
* @param value any value, including undefined
*/
const stringValue = (value: any, escapeQuotes: number) => {
// Trap falsy values, including `NaN` and empty strings.
if (!value) {
return String(value)
}
// stringObject accepts `NaN` objects.
if (typeof value === 'object') {
return stringObject(value)
}
// Other non-falsy primitive values.
let str = String(value)
if (escapeQuotes & QUOTES.Single) {
str = str.replace(/(?=')/g, '\\')
}
if (escapeQuotes & QUOTES.Double) {
str = str.replace(/(?=")/g, '\\')
}
return str
}
/**
* Returns the value pointed by match[2] and adjust match[1] to cover
* the length of the replacement.
*
* @param value Source value or object
* @param match Contain the property path in $2
* @throws TypeError if you try to read the prop on a null object.
*/
const getValueInfo = (value: any, match: RegExpExecArray) => {
// Get the property path without the first dot
const propPath = match[2] && match[2].slice(1)
let length = match[1].length
// Try to get the property value only if this is an object
if (propPath) {
const props = propPath.split('.')
let prop = props.shift()
while (prop && typeof value === 'object') {
// the next assignment will raise a TypeError if the current value
// is null, undefined, or a primitive value... it is ok.
value = value[prop]
// include this prop in the replaced length and pick the next
length += prop.length + 1
prop = props.shift()
}
}
return { value, length }
}
/**
* Replaces memvars in a source fragment with its current values.
*
* @param props Jscc properties with the MagicString instance and the values
* @param fragment Fragment of code to replace and add to `magicStr`
* @param start Offset where the replacement starts in magicStr.
*/
const remapVars = function _remapVars (props: JsccProps, fragment: string, start: number) {
// node.js is async, make local copy of the regex
const re = new RegExp(R.VARS_TO_REPL)
let changes = false
let match
// $1: varname including the prefix '$'
// $2: optional property name(s)
// tslint:disable-next-line:no-conditional-assignment
while (match = re.exec(fragment)) {
const vname = match[1].slice(1) // strip the prefix '$'
if (vname in props.values) {
const index = start + match.index
const vinfo = getValueInfo(props.values[vname], match)
props.magicStr.overwrite(
index,
index + vinfo.length,
stringValue(vinfo.value, props.escapeQuotes)
)
changes = true
}
}
return changes
}
export = remapVars