2022-01-21 08:28:41 +00:00
|
|
|
|
'use strict'
|
|
|
|
|
|
2022-06-08 10:36:39 +00:00
|
|
|
|
let config
|
|
|
|
|
let tokenId = 0
|
2022-01-21 08:28:41 +00:00
|
|
|
|
|
2022-06-08 10:36:39 +00:00
|
|
|
|
const CHAR_COMMENT_REPLACEMENT = '\uFFFD' // <20>
|
|
|
|
|
const CHAR_TOKEN_REPLACEMENT = '\u00A4' // ¤
|
|
|
|
|
const CHAR_TOKEN_START = '\u00AB' // «
|
|
|
|
|
const CHAR_TOKEN_END = '\u00BB' // »
|
2022-01-21 08:28:41 +00:00
|
|
|
|
|
2022-06-08 10:36:39 +00:00
|
|
|
|
const REGEX_COMMENT_REPLACEMENT = new RegExp(CHAR_COMMENT_REPLACEMENT, 'ig')
|
|
|
|
|
const REGEX_TOKEN_REPLACEMENT = new RegExp(CHAR_TOKEN_REPLACEMENT, 'ig')
|
2022-01-21 08:28:41 +00:00
|
|
|
|
|
2022-06-08 10:36:39 +00:00
|
|
|
|
const PATTERN_NUMBER = '\\-?(\\d*?\\.\\d+|\\d+)'
|
|
|
|
|
const PATTERN_NUMBER_WITH_CALC = '(calc' + CHAR_TOKEN_REPLACEMENT + ')|(' + PATTERN_NUMBER + ')(?!d\\()'
|
|
|
|
|
const PATTERN_TOKEN = CHAR_TOKEN_START + '\\d+:\\d+' + CHAR_TOKEN_END // «offset:index»
|
|
|
|
|
const PATTERN_TOKEN_WITH_NAME = '\\w*?' + CHAR_TOKEN_START + '\\d+:\\d+' + CHAR_TOKEN_END // «offset:index»
|
2022-01-21 08:28:41 +00:00
|
|
|
|
|
2022-06-08 10:36:39 +00:00
|
|
|
|
const REGEX_COMMENT = /\/\*[^]*?\*\//igm // non-greedy
|
|
|
|
|
const REGEX_DIRECTIVE = /\/\*\s*(?:!)?\s*rtl:[^]*?\*\//img
|
|
|
|
|
const REGEX_ESCAPE = /[.*+?^${}()|[\]\\]/g
|
|
|
|
|
const REGEX_FUNCTION = /\([^()]+\)/i
|
|
|
|
|
const REGEX_HEX_COLOR = /#[a-f0-9]{3,6}/ig
|
|
|
|
|
const REGEX_CALC = /calc/
|
|
|
|
|
const REGEX_TOKENS = new RegExp(PATTERN_TOKEN, 'ig')
|
|
|
|
|
const REGEX_TOKENS_WITH_NAME = new RegExp(PATTERN_TOKEN_WITH_NAME, 'ig')
|
|
|
|
|
|
|
|
|
|
const REGEX_COMPLEMENT = new RegExp(PATTERN_NUMBER_WITH_CALC, 'i')
|
|
|
|
|
const REGEX_NEGATE_ALL = new RegExp(PATTERN_NUMBER_WITH_CALC, 'ig')
|
|
|
|
|
const REGEX_NEGATE_ONE = new RegExp(PATTERN_NUMBER_WITH_CALC, 'i')
|
|
|
|
|
|
|
|
|
|
const DEFAULT_STRING_MAP_OPTIONS = { scope: '*', ignoreCase: true }
|
2022-01-21 08:28:41 +00:00
|
|
|
|
|
|
|
|
|
function compare (what, to, ignoreCase) {
|
2022-06-08 10:36:39 +00:00
|
|
|
|
return ignoreCase
|
|
|
|
|
? what.toLowerCase() === to.toLowerCase()
|
|
|
|
|
: what === to
|
2022-01-21 08:28:41 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function escapeRegExp (string) {
|
|
|
|
|
return string.replace(REGEX_ESCAPE, '\\$&')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
module.exports = {
|
2022-06-08 10:36:39 +00:00
|
|
|
|
extend (dest, src) {
|
2022-01-21 08:28:41 +00:00
|
|
|
|
if (typeof dest === 'undefined' || typeof dest !== 'object') {
|
|
|
|
|
dest = {}
|
|
|
|
|
}
|
2022-06-08 10:36:39 +00:00
|
|
|
|
|
|
|
|
|
for (const prop in src) {
|
|
|
|
|
if (!Object.prototype.hasOwnProperty.call(dest, prop)) {
|
2022-01-21 08:28:41 +00:00
|
|
|
|
dest[prop] = src[prop]
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-06-08 10:36:39 +00:00
|
|
|
|
|
2022-01-21 08:28:41 +00:00
|
|
|
|
return dest
|
|
|
|
|
},
|
2022-06-08 10:36:39 +00:00
|
|
|
|
swap (value, a, b, options) {
|
|
|
|
|
let expr = `${escapeRegExp(a)}|${escapeRegExp(b)}`
|
2022-01-21 08:28:41 +00:00
|
|
|
|
options = options || DEFAULT_STRING_MAP_OPTIONS
|
2022-06-08 10:36:39 +00:00
|
|
|
|
const greedy = Object.prototype.hasOwnProperty.call(options, 'greedy') ? options.greedy : config.greedy
|
|
|
|
|
if (!greedy) expr = `\\b(${expr})\\b`
|
|
|
|
|
|
|
|
|
|
const flags = options.ignoreCase ? 'img' : 'mg'
|
|
|
|
|
return value.replace(new RegExp(expr, flags), (m) => compare(m, a, options.ignoreCase) ? b : a)
|
2022-01-21 08:28:41 +00:00
|
|
|
|
},
|
2022-06-08 10:36:39 +00:00
|
|
|
|
swapLeftRight (value) {
|
2022-01-21 08:28:41 +00:00
|
|
|
|
return this.swap(value, 'left', 'right')
|
|
|
|
|
},
|
2022-06-08 10:36:39 +00:00
|
|
|
|
swapLtrRtl (value) {
|
2022-01-21 08:28:41 +00:00
|
|
|
|
return this.swap(value, 'ltr', 'rtl')
|
|
|
|
|
},
|
2022-06-08 10:36:39 +00:00
|
|
|
|
applyStringMap (value, isUrl) {
|
|
|
|
|
let result = value
|
|
|
|
|
for (const map of config.stringMap) {
|
|
|
|
|
const options = this.extend(map.options, DEFAULT_STRING_MAP_OPTIONS)
|
2022-01-21 08:28:41 +00:00
|
|
|
|
if (options.scope === '*' || (isUrl && options.scope === 'url') || (!isUrl && options.scope === 'selector')) {
|
|
|
|
|
if (Array.isArray(map.search) && Array.isArray(map.replace)) {
|
2022-06-08 10:36:39 +00:00
|
|
|
|
for (let mapIndex = 0; mapIndex < map.search.length; mapIndex++) {
|
2022-01-21 08:28:41 +00:00
|
|
|
|
result = this.swap(result, map.search[mapIndex], map.replace[mapIndex % map.search.length], options)
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
result = this.swap(result, map.search, map.replace, options)
|
|
|
|
|
}
|
2022-06-08 10:36:39 +00:00
|
|
|
|
|
2022-01-21 08:28:41 +00:00
|
|
|
|
if (map.exclusive === true) {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-06-08 10:36:39 +00:00
|
|
|
|
|
2022-01-21 08:28:41 +00:00
|
|
|
|
return result
|
|
|
|
|
},
|
2022-06-08 10:36:39 +00:00
|
|
|
|
negate (value) {
|
|
|
|
|
const state = this.saveTokens(value)
|
|
|
|
|
state.value = state.value.replace(REGEX_NEGATE_ONE, (num) => {
|
|
|
|
|
return REGEX_TOKEN_REPLACEMENT.test(num)
|
|
|
|
|
? num.replace(REGEX_TOKEN_REPLACEMENT, (m) => '(-1*' + m + ')')
|
|
|
|
|
: Number.parseFloat(num) * -1
|
2022-01-21 08:28:41 +00:00
|
|
|
|
})
|
|
|
|
|
return this.restoreTokens(state)
|
|
|
|
|
},
|
2022-06-08 10:36:39 +00:00
|
|
|
|
negateAll (value) {
|
|
|
|
|
const state = this.saveTokens(value)
|
|
|
|
|
state.value = state.value.replace(REGEX_NEGATE_ALL, (num) => {
|
|
|
|
|
return REGEX_TOKEN_REPLACEMENT.test(num)
|
|
|
|
|
? num.replace(REGEX_TOKEN_REPLACEMENT, (m) => '(-1*' + m + ')')
|
|
|
|
|
: Number.parseFloat(num) * -1
|
2022-01-21 08:28:41 +00:00
|
|
|
|
})
|
|
|
|
|
return this.restoreTokens(state)
|
|
|
|
|
},
|
2022-06-08 10:36:39 +00:00
|
|
|
|
complement (value) {
|
|
|
|
|
const state = this.saveTokens(value)
|
|
|
|
|
state.value = state.value.replace(REGEX_COMPLEMENT, (num) => {
|
|
|
|
|
return REGEX_TOKEN_REPLACEMENT.test(num)
|
|
|
|
|
? num.replace(REGEX_TOKEN_REPLACEMENT, (m) => '(100% - ' + m + ')')
|
|
|
|
|
: 100 - Number.parseFloat(num)
|
2022-01-21 08:28:41 +00:00
|
|
|
|
})
|
|
|
|
|
return this.restoreTokens(state)
|
|
|
|
|
},
|
2022-06-08 10:36:39 +00:00
|
|
|
|
flipLength (value) {
|
|
|
|
|
return config.useCalc ? `calc(100% - ${value})` : value
|
2022-01-21 08:28:41 +00:00
|
|
|
|
},
|
2022-06-08 10:36:39 +00:00
|
|
|
|
save (what, who, replacement, restorer, exclude) {
|
|
|
|
|
const state = {
|
2022-01-21 08:28:41 +00:00
|
|
|
|
value: who,
|
|
|
|
|
store: [],
|
2022-06-08 10:36:39 +00:00
|
|
|
|
replacement,
|
|
|
|
|
restorer
|
2022-01-21 08:28:41 +00:00
|
|
|
|
}
|
2022-06-08 10:36:39 +00:00
|
|
|
|
state.value = state.value.replace(what, (c) => {
|
2022-01-21 08:28:41 +00:00
|
|
|
|
if (exclude && c.match(exclude)) {
|
|
|
|
|
return c
|
|
|
|
|
}
|
2022-06-08 10:36:39 +00:00
|
|
|
|
|
|
|
|
|
state.store.push(c)
|
|
|
|
|
return state.replacement
|
2022-01-21 08:28:41 +00:00
|
|
|
|
})
|
|
|
|
|
return state
|
|
|
|
|
},
|
2022-06-08 10:36:39 +00:00
|
|
|
|
restore (state) {
|
|
|
|
|
let index = 0
|
|
|
|
|
const result = state.value.replace(state.restorer, () => {
|
2022-01-21 08:28:41 +00:00
|
|
|
|
return state.store[index++]
|
|
|
|
|
})
|
|
|
|
|
state.store.length = 0
|
|
|
|
|
return result
|
|
|
|
|
},
|
2022-06-08 10:36:39 +00:00
|
|
|
|
saveComments (value) {
|
2022-01-21 08:28:41 +00:00
|
|
|
|
return this.save(REGEX_COMMENT, value, CHAR_COMMENT_REPLACEMENT, REGEX_COMMENT_REPLACEMENT)
|
|
|
|
|
},
|
2022-06-08 10:36:39 +00:00
|
|
|
|
restoreComments (state) {
|
2022-01-21 08:28:41 +00:00
|
|
|
|
return this.restore(state)
|
|
|
|
|
},
|
2022-06-08 10:36:39 +00:00
|
|
|
|
saveTokens (value, excludeCalc) {
|
2022-01-21 08:28:41 +00:00
|
|
|
|
return excludeCalc === true
|
|
|
|
|
? this.save(REGEX_TOKENS_WITH_NAME, value, CHAR_TOKEN_REPLACEMENT, REGEX_TOKEN_REPLACEMENT, REGEX_CALC)
|
|
|
|
|
: this.save(REGEX_TOKENS, value, CHAR_TOKEN_REPLACEMENT, REGEX_TOKEN_REPLACEMENT)
|
|
|
|
|
},
|
2022-06-08 10:36:39 +00:00
|
|
|
|
restoreTokens (state) {
|
2022-01-21 08:28:41 +00:00
|
|
|
|
return this.restore(state)
|
|
|
|
|
},
|
2022-06-08 10:36:39 +00:00
|
|
|
|
guard (what, who, indexed) {
|
|
|
|
|
const state = {
|
2022-01-21 08:28:41 +00:00
|
|
|
|
value: who,
|
|
|
|
|
store: [],
|
2022-06-08 10:36:39 +00:00
|
|
|
|
offset: tokenId++,
|
|
|
|
|
token: CHAR_TOKEN_START + tokenId,
|
2022-01-21 08:28:41 +00:00
|
|
|
|
indexed: indexed === true
|
|
|
|
|
}
|
|
|
|
|
if (state.indexed === true) {
|
|
|
|
|
while (what.test(state.value)) {
|
2022-06-08 10:36:39 +00:00
|
|
|
|
state.value = state.value.replace(what, (m) => {
|
|
|
|
|
state.store.push(m)
|
|
|
|
|
return `${state.token}:${state.store.length}${CHAR_TOKEN_END}`
|
|
|
|
|
})
|
2022-01-21 08:28:41 +00:00
|
|
|
|
}
|
|
|
|
|
} else {
|
2022-06-08 10:36:39 +00:00
|
|
|
|
state.value = state.value.replace(what, (m) => {
|
|
|
|
|
state.store.push(m)
|
|
|
|
|
return state.token + CHAR_TOKEN_END
|
|
|
|
|
})
|
2022-01-21 08:28:41 +00:00
|
|
|
|
}
|
2022-06-08 10:36:39 +00:00
|
|
|
|
|
2022-01-21 08:28:41 +00:00
|
|
|
|
return state
|
|
|
|
|
},
|
2022-06-08 10:36:39 +00:00
|
|
|
|
unguard (state, callback) {
|
2022-01-21 08:28:41 +00:00
|
|
|
|
if (state.indexed === true) {
|
2022-06-08 10:36:39 +00:00
|
|
|
|
const detokenizer = new RegExp('(\\w*?)' + state.token + ':(\\d+)' + CHAR_TOKEN_END, 'i')
|
2022-01-21 08:28:41 +00:00
|
|
|
|
while (detokenizer.test(state.value)) {
|
2022-06-08 10:36:39 +00:00
|
|
|
|
state.value = state.value.replace(detokenizer, (match, name, index) => {
|
|
|
|
|
const value = state.store[index - 1]
|
|
|
|
|
return typeof callback === 'function'
|
|
|
|
|
? name + callback(value, name)
|
|
|
|
|
: name + value
|
2022-01-21 08:28:41 +00:00
|
|
|
|
})
|
|
|
|
|
}
|
2022-06-08 10:36:39 +00:00
|
|
|
|
|
2022-01-21 08:28:41 +00:00
|
|
|
|
return state.value
|
|
|
|
|
}
|
2022-06-08 10:36:39 +00:00
|
|
|
|
|
|
|
|
|
return state.value.replace(new RegExp('(\\w*?)' + state.token + CHAR_TOKEN_END, 'i'), (match, name) => {
|
|
|
|
|
const value = state.store.shift()
|
|
|
|
|
return typeof callback === 'function'
|
|
|
|
|
? name + callback(value, name)
|
|
|
|
|
: name + value
|
|
|
|
|
})
|
2022-01-21 08:28:41 +00:00
|
|
|
|
},
|
2022-06-08 10:36:39 +00:00
|
|
|
|
guardHexColors (value) {
|
2022-01-21 08:28:41 +00:00
|
|
|
|
return this.guard(REGEX_HEX_COLOR, value, true)
|
|
|
|
|
},
|
2022-06-08 10:36:39 +00:00
|
|
|
|
unguardHexColors (state, callback) {
|
2022-01-21 08:28:41 +00:00
|
|
|
|
return this.unguard(state, callback)
|
|
|
|
|
},
|
2022-06-08 10:36:39 +00:00
|
|
|
|
guardFunctions (value) {
|
2022-01-21 08:28:41 +00:00
|
|
|
|
return this.guard(REGEX_FUNCTION, value, true)
|
|
|
|
|
},
|
2022-06-08 10:36:39 +00:00
|
|
|
|
unguardFunctions (state, callback) {
|
2022-01-21 08:28:41 +00:00
|
|
|
|
return this.unguard(state, callback)
|
|
|
|
|
},
|
2022-06-08 10:36:39 +00:00
|
|
|
|
trimDirective (value) {
|
2022-01-21 08:28:41 +00:00
|
|
|
|
return value.replace(REGEX_DIRECTIVE, '')
|
|
|
|
|
},
|
|
|
|
|
regexCache: {},
|
2022-06-08 10:36:39 +00:00
|
|
|
|
regexDirective (name) {
|
2022-01-21 08:28:41 +00:00
|
|
|
|
// /(?:\/\*(?:!)?rtl:ignore(?::)?)([^]*?)(?:\*\/)/img
|
|
|
|
|
this.regexCache[name] = this.regexCache[name] || new RegExp('(?:\\/\\*\\s*(?:!)?\\s*rtl:' + (name ? escapeRegExp(name) + '(?::)?' : '') + ')([^]*?)(?:\\*\\/)', 'img')
|
|
|
|
|
return this.regexCache[name]
|
|
|
|
|
},
|
2022-06-08 10:36:39 +00:00
|
|
|
|
regex (what, options) {
|
2022-01-21 08:28:41 +00:00
|
|
|
|
what = what || []
|
2022-06-08 10:36:39 +00:00
|
|
|
|
let expression = ''
|
|
|
|
|
|
|
|
|
|
for (const exp of what) {
|
|
|
|
|
switch (exp) {
|
2022-01-21 08:28:41 +00:00
|
|
|
|
case 'percent':
|
2022-06-08 10:36:39 +00:00
|
|
|
|
expression += `|(${PATTERN_NUMBER}%)`
|
2022-01-21 08:28:41 +00:00
|
|
|
|
break
|
|
|
|
|
case 'length':
|
2022-06-08 10:36:39 +00:00
|
|
|
|
expression += `|(${PATTERN_NUMBER})(?:ex|ch|r?em|vh|vw|vmin|vmax|px|mm|cm|in|pt|pc)?`
|
2022-01-21 08:28:41 +00:00
|
|
|
|
break
|
|
|
|
|
case 'number':
|
2022-06-08 10:36:39 +00:00
|
|
|
|
expression += `|(${PATTERN_NUMBER})`
|
2022-01-21 08:28:41 +00:00
|
|
|
|
break
|
|
|
|
|
case 'position':
|
|
|
|
|
expression += '|(left|center|right|top|bottom)'
|
|
|
|
|
break
|
|
|
|
|
case 'calc':
|
2022-06-08 10:36:39 +00:00
|
|
|
|
expression += `|(calc${PATTERN_TOKEN})`
|
2022-01-21 08:28:41 +00:00
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-06-08 10:36:39 +00:00
|
|
|
|
|
2022-01-21 08:28:41 +00:00
|
|
|
|
return new RegExp(expression.slice(1), options)
|
|
|
|
|
},
|
2022-06-08 10:36:39 +00:00
|
|
|
|
isLastOfType (node) {
|
|
|
|
|
let isLast = true
|
|
|
|
|
let next = node.next()
|
|
|
|
|
|
2022-01-21 08:28:41 +00:00
|
|
|
|
while (next) {
|
2022-06-08 10:36:39 +00:00
|
|
|
|
if (next.type === node.type) {
|
2022-01-21 08:28:41 +00:00
|
|
|
|
isLast = false
|
|
|
|
|
break
|
|
|
|
|
}
|
2022-06-08 10:36:39 +00:00
|
|
|
|
|
2022-01-21 08:28:41 +00:00
|
|
|
|
next = next.next()
|
|
|
|
|
}
|
2022-06-08 10:36:39 +00:00
|
|
|
|
|
2022-01-21 08:28:41 +00:00
|
|
|
|
return isLast
|
|
|
|
|
},
|
|
|
|
|
/**
|
|
|
|
|
* Simple breakable each: returning false in the callback will break the loop
|
|
|
|
|
* returns false if the loop was broken, otherwise true
|
|
|
|
|
*/
|
2022-06-08 10:36:39 +00:00
|
|
|
|
each (array, callback) {
|
|
|
|
|
for (const element of array) {
|
|
|
|
|
if (callback(element) === false) {
|
2022-01-21 08:28:41 +00:00
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-06-08 10:36:39 +00:00
|
|
|
|
|
2022-01-21 08:28:41 +00:00
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
module.exports.configure = function (configuration) {
|
|
|
|
|
config = configuration
|
|
|
|
|
return this
|
|
|
|
|
}
|