Standardize settings file naming and relocate documentation files Fix code quality violations from rsx:check Reorganize user_management directory into logical subdirectories Move Quill Bundle to core and align with Tom Select pattern Simplify Site Settings page to focus on core site information Complete Phase 5: Multi-tenant authentication with login flow and site selection Add route query parameter rule and synchronize filename validation logic Fix critical bug in UpdateNpmCommand causing missing JavaScript stubs Implement filename convention rule and resolve VS Code auto-rename conflict Implement js-sanitizer RPC server to eliminate 900+ Node.js process spawns Implement RPC server architecture for JavaScript parsing WIP: Add RPC server infrastructure for JS parsing (partial implementation) Update jqhtml terminology from destroy to stop, fix datagrid DOM preservation Add JQHTML-CLASS-01 rule and fix redundant class names Improve code quality rules and resolve violations Remove legacy fatal error format in favor of unified 'fatal' error type Filter internal keys from window.rsxapp output Update button styling and comprehensive form/modal documentation Add conditional fly-in animation for modals Fix non-deterministic bundle compilation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
449 lines
10 KiB
JavaScript
449 lines
10 KiB
JavaScript
let range = require('normalize-range')
|
|
let parser = require('postcss-value-parser')
|
|
|
|
let OldValue = require('../old-value')
|
|
let utils = require('../utils')
|
|
let Value = require('../value')
|
|
|
|
let IS_DIRECTION = /top|left|right|bottom/gi
|
|
|
|
class Gradient extends Value {
|
|
/**
|
|
* Do not add non-webkit prefixes for list-style and object
|
|
*/
|
|
add(decl, prefix) {
|
|
let p = decl.prop
|
|
if (p.includes('mask')) {
|
|
if (prefix === '-webkit-' || prefix === '-webkit- old') {
|
|
return super.add(decl, prefix)
|
|
}
|
|
} else if (
|
|
p === 'list-style' ||
|
|
p === 'list-style-image' ||
|
|
p === 'content'
|
|
) {
|
|
if (prefix === '-webkit-' || prefix === '-webkit- old') {
|
|
return super.add(decl, prefix)
|
|
}
|
|
} else {
|
|
return super.add(decl, prefix)
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
/**
|
|
* Get div token from exists parameters
|
|
*/
|
|
cloneDiv(params) {
|
|
for (let i of params) {
|
|
if (i.type === 'div' && i.value === ',') {
|
|
return i
|
|
}
|
|
}
|
|
return { after: ' ', type: 'div', value: ',' }
|
|
}
|
|
|
|
/**
|
|
* Change colors syntax to old webkit
|
|
*/
|
|
colorStops(params) {
|
|
let result = []
|
|
for (let i = 0; i < params.length; i++) {
|
|
let pos
|
|
let param = params[i]
|
|
let item
|
|
if (i === 0) {
|
|
continue
|
|
}
|
|
|
|
let color = parser.stringify(param[0])
|
|
if (param[1] && param[1].type === 'word') {
|
|
pos = param[1].value
|
|
} else if (param[2] && param[2].type === 'word') {
|
|
pos = param[2].value
|
|
}
|
|
|
|
let stop
|
|
if (i === 1 && (!pos || pos === '0%')) {
|
|
stop = `from(${color})`
|
|
} else if (i === params.length - 1 && (!pos || pos === '100%')) {
|
|
stop = `to(${color})`
|
|
} else if (pos) {
|
|
stop = `color-stop(${pos}, ${color})`
|
|
} else {
|
|
stop = `color-stop(${color})`
|
|
}
|
|
|
|
let div = param[param.length - 1]
|
|
params[i] = [{ type: 'word', value: stop }]
|
|
if (div.type === 'div' && div.value === ',') {
|
|
item = params[i].push(div)
|
|
}
|
|
result.push(item)
|
|
}
|
|
return result
|
|
}
|
|
|
|
/**
|
|
* Change new direction to old
|
|
*/
|
|
convertDirection(params) {
|
|
if (params.length > 0) {
|
|
if (params[0].value === 'to') {
|
|
this.fixDirection(params)
|
|
} else if (params[0].value.includes('deg')) {
|
|
this.fixAngle(params)
|
|
} else if (this.isRadial(params)) {
|
|
this.fixRadial(params)
|
|
}
|
|
}
|
|
return params
|
|
}
|
|
|
|
/**
|
|
* Add 90 degrees
|
|
*/
|
|
fixAngle(params) {
|
|
let first = params[0].value
|
|
first = parseFloat(first)
|
|
first = Math.abs(450 - first) % 360
|
|
first = this.roundFloat(first, 3)
|
|
params[0].value = `${first}deg`
|
|
}
|
|
|
|
/**
|
|
* Replace `to top left` to `bottom right`
|
|
*/
|
|
fixDirection(params) {
|
|
params.splice(0, 2)
|
|
|
|
for (let param of params) {
|
|
if (param.type === 'div') {
|
|
break
|
|
}
|
|
if (param.type === 'word') {
|
|
param.value = this.revertDirection(param.value)
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fix radial direction syntax
|
|
*/
|
|
fixRadial(params) {
|
|
let first = []
|
|
let second = []
|
|
let a, b, c, i, next
|
|
|
|
for (i = 0; i < params.length - 2; i++) {
|
|
a = params[i]
|
|
b = params[i + 1]
|
|
c = params[i + 2]
|
|
if (a.type === 'space' && b.value === 'at' && c.type === 'space') {
|
|
next = i + 3
|
|
break
|
|
} else {
|
|
first.push(a)
|
|
}
|
|
}
|
|
|
|
let div
|
|
for (i = next; i < params.length; i++) {
|
|
if (params[i].type === 'div') {
|
|
div = params[i]
|
|
break
|
|
} else {
|
|
second.push(params[i])
|
|
}
|
|
}
|
|
|
|
params.splice(0, i, ...second, div, ...first)
|
|
}
|
|
|
|
/**
|
|
* Look for at word
|
|
*/
|
|
isRadial(params) {
|
|
let state = 'before'
|
|
for (let param of params) {
|
|
if (state === 'before' && param.type === 'space') {
|
|
state = 'at'
|
|
} else if (state === 'at' && param.value === 'at') {
|
|
state = 'after'
|
|
} else if (state === 'after' && param.type === 'space') {
|
|
return true
|
|
} else if (param.type === 'div') {
|
|
break
|
|
} else {
|
|
state = 'before'
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
/**
|
|
* Replace old direction to new
|
|
*/
|
|
newDirection(params) {
|
|
if (params[0].value === 'to') {
|
|
return params
|
|
}
|
|
IS_DIRECTION.lastIndex = 0 // reset search index of global regexp
|
|
if (!IS_DIRECTION.test(params[0].value)) {
|
|
return params
|
|
}
|
|
|
|
params.unshift(
|
|
{
|
|
type: 'word',
|
|
value: 'to'
|
|
},
|
|
{
|
|
type: 'space',
|
|
value: ' '
|
|
}
|
|
)
|
|
|
|
for (let i = 2; i < params.length; i++) {
|
|
if (params[i].type === 'div') {
|
|
break
|
|
}
|
|
if (params[i].type === 'word') {
|
|
params[i].value = this.revertDirection(params[i].value)
|
|
}
|
|
}
|
|
|
|
return params
|
|
}
|
|
|
|
/**
|
|
* Normalize angle
|
|
*/
|
|
normalize(nodes, gradientName) {
|
|
if (!nodes[0]) return nodes
|
|
|
|
if (/-?\d+(.\d+)?grad/.test(nodes[0].value)) {
|
|
nodes[0].value = this.normalizeUnit(nodes[0].value, 400)
|
|
} else if (/-?\d+(.\d+)?rad/.test(nodes[0].value)) {
|
|
nodes[0].value = this.normalizeUnit(nodes[0].value, 2 * Math.PI)
|
|
} else if (/-?\d+(.\d+)?turn/.test(nodes[0].value)) {
|
|
nodes[0].value = this.normalizeUnit(nodes[0].value, 1)
|
|
} else if (nodes[0].value.includes('deg')) {
|
|
let num = parseFloat(nodes[0].value)
|
|
num = range.wrap(0, 360, num)
|
|
nodes[0].value = `${num}deg`
|
|
}
|
|
|
|
if (
|
|
gradientName === 'linear-gradient' ||
|
|
gradientName === 'repeating-linear-gradient'
|
|
) {
|
|
let direction = nodes[0].value
|
|
|
|
// Unitless zero for `<angle>` values are allowed in CSS gradients and transforms.
|
|
// Spec: https://github.com/w3c/csswg-drafts/commit/602789171429b2231223ab1e5acf8f7f11652eb3
|
|
if (direction === '0deg' || direction === '0') {
|
|
nodes = this.replaceFirst(nodes, 'to', ' ', 'top')
|
|
} else if (direction === '90deg') {
|
|
nodes = this.replaceFirst(nodes, 'to', ' ', 'right')
|
|
} else if (direction === '180deg') {
|
|
nodes = this.replaceFirst(nodes, 'to', ' ', 'bottom') // default value
|
|
} else if (direction === '270deg') {
|
|
nodes = this.replaceFirst(nodes, 'to', ' ', 'left')
|
|
}
|
|
}
|
|
|
|
return nodes
|
|
}
|
|
|
|
/**
|
|
* Convert angle unit to deg
|
|
*/
|
|
normalizeUnit(str, full) {
|
|
let num = parseFloat(str)
|
|
let deg = (num / full) * 360
|
|
return `${deg}deg`
|
|
}
|
|
|
|
/**
|
|
* Remove old WebKit gradient too
|
|
*/
|
|
old(prefix) {
|
|
if (prefix === '-webkit-') {
|
|
let type
|
|
if (this.name === 'linear-gradient') {
|
|
type = 'linear'
|
|
} else if (this.name === 'repeating-linear-gradient') {
|
|
type = 'repeating-linear'
|
|
} else if (this.name === 'repeating-radial-gradient') {
|
|
type = 'repeating-radial'
|
|
} else {
|
|
type = 'radial'
|
|
}
|
|
let string = '-gradient'
|
|
let regexp = utils.regexp(
|
|
`-webkit-(${type}-gradient|gradient\\(\\s*${type})`,
|
|
false
|
|
)
|
|
|
|
return new OldValue(this.name, prefix + this.name, string, regexp)
|
|
} else {
|
|
return super.old(prefix)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Change direction syntax to old webkit
|
|
*/
|
|
oldDirection(params) {
|
|
let div = this.cloneDiv(params[0])
|
|
|
|
if (params[0][0].value !== 'to') {
|
|
return params.unshift([
|
|
{ type: 'word', value: Gradient.oldDirections.bottom },
|
|
div
|
|
])
|
|
} else {
|
|
let words = []
|
|
for (let node of params[0].slice(2)) {
|
|
if (node.type === 'word') {
|
|
words.push(node.value.toLowerCase())
|
|
}
|
|
}
|
|
|
|
words = words.join(' ')
|
|
let old = Gradient.oldDirections[words] || words
|
|
|
|
params[0] = [{ type: 'word', value: old }, div]
|
|
return params[0]
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convert to old webkit syntax
|
|
*/
|
|
oldWebkit(node) {
|
|
let { nodes } = node
|
|
let string = parser.stringify(node.nodes)
|
|
|
|
if (this.name !== 'linear-gradient') {
|
|
return false
|
|
}
|
|
if (nodes[0] && nodes[0].value.includes('deg')) {
|
|
return false
|
|
}
|
|
if (
|
|
string.includes('px') ||
|
|
string.includes('-corner') ||
|
|
string.includes('-side')
|
|
) {
|
|
return false
|
|
}
|
|
|
|
let params = [[]]
|
|
for (let i of nodes) {
|
|
params[params.length - 1].push(i)
|
|
if (i.type === 'div' && i.value === ',') {
|
|
params.push([])
|
|
}
|
|
}
|
|
|
|
this.oldDirection(params)
|
|
this.colorStops(params)
|
|
|
|
node.nodes = []
|
|
for (let param of params) {
|
|
node.nodes = node.nodes.concat(param)
|
|
}
|
|
|
|
node.nodes.unshift(
|
|
{ type: 'word', value: 'linear' },
|
|
this.cloneDiv(node.nodes)
|
|
)
|
|
node.value = '-webkit-gradient'
|
|
|
|
return true
|
|
}
|
|
|
|
/**
|
|
* Change degrees for webkit prefix
|
|
*/
|
|
replace(string, prefix) {
|
|
let ast = parser(string)
|
|
for (let node of ast.nodes) {
|
|
let gradientName = this.name // gradient name
|
|
if (node.type === 'function' && node.value === gradientName) {
|
|
node.nodes = this.newDirection(node.nodes)
|
|
node.nodes = this.normalize(node.nodes, gradientName)
|
|
if (prefix === '-webkit- old') {
|
|
let changes = this.oldWebkit(node)
|
|
if (!changes) {
|
|
return false
|
|
}
|
|
} else {
|
|
node.nodes = this.convertDirection(node.nodes)
|
|
node.value = prefix + node.value
|
|
}
|
|
}
|
|
}
|
|
return ast.toString()
|
|
}
|
|
|
|
/**
|
|
* Replace first token
|
|
*/
|
|
replaceFirst(params, ...words) {
|
|
let prefix = words.map(i => {
|
|
if (i === ' ') {
|
|
return { type: 'space', value: i }
|
|
}
|
|
return { type: 'word', value: i }
|
|
})
|
|
return prefix.concat(params.slice(1))
|
|
}
|
|
|
|
revertDirection(word) {
|
|
return Gradient.directions[word.toLowerCase()] || word
|
|
}
|
|
|
|
/**
|
|
* Round float and save digits under dot
|
|
*/
|
|
roundFloat(float, digits) {
|
|
return parseFloat(float.toFixed(digits))
|
|
}
|
|
}
|
|
|
|
Gradient.names = [
|
|
'linear-gradient',
|
|
'repeating-linear-gradient',
|
|
'radial-gradient',
|
|
'repeating-radial-gradient'
|
|
]
|
|
|
|
Gradient.directions = {
|
|
bottom: 'top',
|
|
left: 'right',
|
|
right: 'left',
|
|
top: 'bottom' // default value
|
|
}
|
|
|
|
// Direction to replace
|
|
Gradient.oldDirections = {
|
|
'bottom': 'left top, left bottom',
|
|
'bottom left': 'right top, left bottom',
|
|
'bottom right': 'left top, right bottom',
|
|
'left': 'right top, left top',
|
|
|
|
'left bottom': 'right top, left bottom',
|
|
'left top': 'right bottom, left top',
|
|
'right': 'left top, right top',
|
|
'right bottom': 'left top, right bottom',
|
|
'right top': 'left bottom, right top',
|
|
'top': 'left bottom, left top',
|
|
'top left': 'right bottom, left top',
|
|
'top right': 'left bottom, right top'
|
|
}
|
|
|
|
module.exports = Gradient
|