init
commit
4b4e92f3e9
|
@ -0,0 +1,17 @@
|
||||||
|
.vscode
|
||||||
|
node_modules/
|
||||||
|
dist
|
||||||
|
benchmark
|
||||||
|
bin
|
||||||
|
|
||||||
|
*.sublime-project
|
||||||
|
*.sublime-workspace
|
||||||
|
package-lock.json
|
||||||
|
|
||||||
|
._*
|
||||||
|
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
.DS_Store
|
||||||
|
.AppleDouble
|
||||||
|
.LSOverride
|
|
@ -0,0 +1,406 @@
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
var resolve = require('resolve')
|
||||||
|
var path = require('path')
|
||||||
|
|
||||||
|
var testFolder = path.relative(
|
||||||
|
process.cwd(),
|
||||||
|
path.dirname(resolve.sync('@less/test-data'))
|
||||||
|
)
|
||||||
|
var lessFolder = path.join(testFolder, 'less')
|
||||||
|
|
||||||
|
module.exports = function (grunt) {
|
||||||
|
grunt.option('stack', true)
|
||||||
|
|
||||||
|
// Report the elapsed execution time of tasks.
|
||||||
|
require('time-grunt')(grunt)
|
||||||
|
|
||||||
|
var git = require('git-rev')
|
||||||
|
|
||||||
|
// Sauce Labs browser
|
||||||
|
var browsers = [
|
||||||
|
// Desktop browsers
|
||||||
|
{
|
||||||
|
browserName: 'chrome',
|
||||||
|
version: 'latest',
|
||||||
|
platform: 'Windows 7'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
browserName: 'firefox',
|
||||||
|
version: 'latest',
|
||||||
|
platform: 'Linux'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
browserName: 'safari',
|
||||||
|
version: '9',
|
||||||
|
platform: 'OS X 10.11'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
browserName: 'internet explorer',
|
||||||
|
version: '8',
|
||||||
|
platform: 'Windows XP'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
browserName: 'internet explorer',
|
||||||
|
version: '11',
|
||||||
|
platform: 'Windows 8.1'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
browserName: 'edge',
|
||||||
|
version: '13',
|
||||||
|
platform: 'Windows 10'
|
||||||
|
},
|
||||||
|
// Mobile browsers
|
||||||
|
{
|
||||||
|
browserName: 'ipad',
|
||||||
|
deviceName: 'iPad Air Simulator',
|
||||||
|
deviceOrientation: 'portrait',
|
||||||
|
version: '8.4',
|
||||||
|
platform: 'OS X 10.9'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
browserName: 'iphone',
|
||||||
|
deviceName: 'iPhone 5 Simulator',
|
||||||
|
deviceOrientation: 'portrait',
|
||||||
|
version: '9.3',
|
||||||
|
platform: 'OS X 10.11'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
browserName: 'android',
|
||||||
|
deviceName: 'Google Nexus 7 HD Emulator',
|
||||||
|
deviceOrientation: 'portrait',
|
||||||
|
version: '4.4',
|
||||||
|
platform: 'Linux'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
var sauceJobs = {}
|
||||||
|
|
||||||
|
var browserTests = [
|
||||||
|
'filemanager-plugin',
|
||||||
|
'visitor-plugin',
|
||||||
|
'global-vars',
|
||||||
|
'modify-vars',
|
||||||
|
'production',
|
||||||
|
'rootpath-relative',
|
||||||
|
'rootpath-rewrite-urls',
|
||||||
|
'rootpath',
|
||||||
|
'relative-urls',
|
||||||
|
'rewrite-urls',
|
||||||
|
'browser',
|
||||||
|
'no-js-errors',
|
||||||
|
'legacy'
|
||||||
|
]
|
||||||
|
|
||||||
|
function makeJob(testName) {
|
||||||
|
sauceJobs[testName] = {
|
||||||
|
options: {
|
||||||
|
urls:
|
||||||
|
testName === 'all'
|
||||||
|
? browserTests.map(function (name) {
|
||||||
|
return (
|
||||||
|
'http://localhost:8081/tmp/browser/test-runner-' +
|
||||||
|
name +
|
||||||
|
'.html'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
: [
|
||||||
|
'http://localhost:8081/tmp/browser/test-runner-' +
|
||||||
|
testName +
|
||||||
|
'.html'
|
||||||
|
],
|
||||||
|
testname: testName === 'all' ? 'Unit Tests for Less.js' : testName,
|
||||||
|
browsers: browsers,
|
||||||
|
public: 'public',
|
||||||
|
recordVideo: false,
|
||||||
|
videoUploadOnPass: false,
|
||||||
|
recordScreenshots: process.env.TRAVIS_BRANCH !== 'master',
|
||||||
|
build:
|
||||||
|
process.env.TRAVIS_BRANCH === 'master'
|
||||||
|
? process.env.TRAVIS_JOB_ID
|
||||||
|
: undefined,
|
||||||
|
tags: [
|
||||||
|
process.env.TRAVIS_BUILD_NUMBER,
|
||||||
|
process.env.TRAVIS_PULL_REQUEST,
|
||||||
|
process.env.TRAVIS_BRANCH
|
||||||
|
],
|
||||||
|
statusCheckAttempts: -1,
|
||||||
|
sauceConfig: {
|
||||||
|
'idle-timeout': 100
|
||||||
|
},
|
||||||
|
throttled: 5,
|
||||||
|
onTestComplete: function (result, callback) {
|
||||||
|
// Called after a unit test is done, per page, per browser
|
||||||
|
// 'result' param is the object returned by the test framework's reporter
|
||||||
|
// 'callback' is a Node.js style callback function. You must invoke it after you
|
||||||
|
// finish your work.
|
||||||
|
// Pass a non-null value as the callback's first parameter if you want to throw an
|
||||||
|
// exception. If your function is synchronous you can also throw exceptions
|
||||||
|
// directly.
|
||||||
|
// Passing true or false as the callback's second parameter passes or fails the
|
||||||
|
// test. Passing undefined does not alter the test result. Please note that this
|
||||||
|
// only affects the grunt task's result. You have to explicitly update the Sauce
|
||||||
|
// Labs job's status via its REST API, if you want so.
|
||||||
|
|
||||||
|
// This should be the encrypted value in Travis
|
||||||
|
var user = process.env.SAUCE_USERNAME
|
||||||
|
var pass = process.env.SAUCE_ACCESS_KEY
|
||||||
|
|
||||||
|
git.short(function (hash) {
|
||||||
|
require('phin')(
|
||||||
|
{
|
||||||
|
method: 'PUT',
|
||||||
|
url: [
|
||||||
|
'https://saucelabs.com/rest/v1',
|
||||||
|
user,
|
||||||
|
'jobs',
|
||||||
|
result.job_id
|
||||||
|
].join('/'),
|
||||||
|
auth: { user: user, pass: pass },
|
||||||
|
data: {
|
||||||
|
passed: result.passed,
|
||||||
|
build: 'build-' + hash
|
||||||
|
}
|
||||||
|
},
|
||||||
|
function (error, response) {
|
||||||
|
if (error) {
|
||||||
|
console.log(error)
|
||||||
|
callback(error)
|
||||||
|
} else if (response.statusCode !== 200) {
|
||||||
|
console.log(response)
|
||||||
|
callback(new Error('Unexpected response status'))
|
||||||
|
} else {
|
||||||
|
callback(null, result.passed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make the SauceLabs jobs
|
||||||
|
;['all'].concat(browserTests).map(makeJob)
|
||||||
|
|
||||||
|
var path = require('path')
|
||||||
|
|
||||||
|
// Handle async / await in Rollup build for tests
|
||||||
|
const tsNodeRuntime = path.resolve(
|
||||||
|
path.join('node_modules', '.bin', 'ts-node')
|
||||||
|
)
|
||||||
|
const crossEnv = path.resolve(path.join('node_modules', '.bin', 'cross-env'))
|
||||||
|
|
||||||
|
// Project configuration.
|
||||||
|
grunt.initConfig({
|
||||||
|
shell: {
|
||||||
|
options: {
|
||||||
|
stdout: true,
|
||||||
|
failOnError: true,
|
||||||
|
execOptions: {
|
||||||
|
maxBuffer: Infinity
|
||||||
|
}
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
command: [
|
||||||
|
/** Browser runtime */
|
||||||
|
'node build/rollup.js --dist',
|
||||||
|
/** Copy to repo root */
|
||||||
|
'npm run copy:root',
|
||||||
|
/** Node.js runtime */
|
||||||
|
'npm run build'
|
||||||
|
].join(' && ')
|
||||||
|
},
|
||||||
|
testbuild: {
|
||||||
|
command: [
|
||||||
|
'npm run build',
|
||||||
|
'node build/rollup.js --browser --out=./tmp/browser/less.min.js'
|
||||||
|
].join(' && ')
|
||||||
|
},
|
||||||
|
testcjs: {
|
||||||
|
command: 'npm run build'
|
||||||
|
},
|
||||||
|
testbrowser: {
|
||||||
|
command:
|
||||||
|
'node build/rollup.js --browser --out=./tmp/browser/less.min.js'
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
command: [
|
||||||
|
// https://github.com/TypeStrong/ts-node/issues/693#issuecomment-848907036
|
||||||
|
crossEnv + ' TS_NODE_SCOPE=true',
|
||||||
|
tsNodeRuntime + ' test/test-es6.ts',
|
||||||
|
'node test/index.js'
|
||||||
|
].join(' && ')
|
||||||
|
},
|
||||||
|
generatebrowser: {
|
||||||
|
command: 'node test/browser/generator/generate.js'
|
||||||
|
},
|
||||||
|
runbrowser: {
|
||||||
|
command: 'node test/browser/generator/runner.js'
|
||||||
|
},
|
||||||
|
benchmark: {
|
||||||
|
command: 'node benchmark/index.js'
|
||||||
|
},
|
||||||
|
opts: {
|
||||||
|
// test running with all current options (using `opts` since `options` means something already)
|
||||||
|
command: [
|
||||||
|
// @TODO: make this more thorough
|
||||||
|
// CURRENT OPTIONS
|
||||||
|
`node bin/lessc --ie-compat ${lessFolder}/_main/lazy-eval.less tmp/lazy-eval.css`,
|
||||||
|
// --math
|
||||||
|
`node bin/lessc --math=always ${lessFolder}/_main/lazy-eval.less tmp/lazy-eval.css`,
|
||||||
|
`node bin/lessc --math=parens-division ${lessFolder}/_main/lazy-eval.less tmp/lazy-eval.css`,
|
||||||
|
`node bin/lessc --math=parens ${lessFolder}/_main/lazy-eval.less tmp/lazy-eval.css`,
|
||||||
|
`node bin/lessc --math=strict ${lessFolder}/_main/lazy-eval.less tmp/lazy-eval.css`,
|
||||||
|
`node bin/lessc --math=strict-legacy ${lessFolder}/_main/lazy-eval.less tmp/lazy-eval.css`,
|
||||||
|
|
||||||
|
// DEPRECATED OPTIONS
|
||||||
|
// --strict-math
|
||||||
|
`node bin/lessc --strict-math=on ${lessFolder}/_main/lazy-eval.less tmp/lazy-eval.css`
|
||||||
|
].join(' && ')
|
||||||
|
},
|
||||||
|
plugin: {
|
||||||
|
command: [
|
||||||
|
`node bin/lessc --clean-css="--s1 --advanced" ${lessFolder}/_main/lazy-eval.less tmp/lazy-eval.css`,
|
||||||
|
'cd lib',
|
||||||
|
`node ../bin/lessc --clean-css="--s1 --advanced" ../${lessFolder}/_main/lazy-eval.less ../tmp/lazy-eval.css`,
|
||||||
|
`node ../bin/lessc --source-map=lazy-eval.css.map --autoprefix ../${lessFolder}/_main/lazy-eval.less ../tmp/lazy-eval.css`,
|
||||||
|
'cd ..',
|
||||||
|
// Test multiple plugins
|
||||||
|
`node bin/lessc --plugin=clean-css="--s1 --advanced" --plugin=autoprefix="ie 11,Edge >= 13,Chrome >= 47,Firefox >= 45,iOS >= 9.2,Safari >= 9" ${lessFolder}/_main/lazy-eval.less tmp/lazy-eval.css`
|
||||||
|
].join(' && ')
|
||||||
|
},
|
||||||
|
'sourcemap-test': {
|
||||||
|
// quoted value doesn't seem to get picked up by time-grunt, or isn't output, at least; maybe just "sourcemap" is fine?
|
||||||
|
command: [
|
||||||
|
`node bin/lessc --source-map=test/sourcemaps/maps/import-map.map ${lessFolder}/_main/import.less test/sourcemaps/import.css`,
|
||||||
|
`node bin/lessc --source-map ${lessFolder}/sourcemaps/basic.less test/sourcemaps/basic.css`
|
||||||
|
].join(' && ')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
eslint: {
|
||||||
|
target: [
|
||||||
|
'test/**/*.js',
|
||||||
|
'src/less*/**/*.js',
|
||||||
|
'!test/less/errors/plugin/plugin-error.js'
|
||||||
|
],
|
||||||
|
options: {
|
||||||
|
configFile: '.eslintrc.js',
|
||||||
|
fix: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
connect: {
|
||||||
|
server: {
|
||||||
|
options: {
|
||||||
|
port: 8081
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
'saucelabs-mocha': sauceJobs,
|
||||||
|
|
||||||
|
// Clean the version of less built for the tests
|
||||||
|
clean: {
|
||||||
|
test: ['test/browser/less.js', 'tmp', 'test/less-bom'],
|
||||||
|
'sourcemap-test': ['test/sourcemaps/*.css', 'test/sourcemaps/*.map'],
|
||||||
|
sauce_log: ['sc_*.log']
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Load these plugins to provide the necessary tasks
|
||||||
|
grunt.loadNpmTasks('grunt-saucelabs')
|
||||||
|
|
||||||
|
require('jit-grunt')(grunt)
|
||||||
|
|
||||||
|
// by default, run tests
|
||||||
|
grunt.registerTask('default', ['test'])
|
||||||
|
|
||||||
|
// Release
|
||||||
|
grunt.registerTask('dist', ['shell:build'])
|
||||||
|
|
||||||
|
// Create the browser version of less.js
|
||||||
|
grunt.registerTask('browsertest-lessjs', ['shell:testbrowser'])
|
||||||
|
|
||||||
|
// Run all browser tests
|
||||||
|
grunt.registerTask('browsertest', [
|
||||||
|
'browsertest-lessjs',
|
||||||
|
'connect',
|
||||||
|
'shell:runbrowser'
|
||||||
|
])
|
||||||
|
|
||||||
|
// setup a web server to run the browser tests in a browser rather than phantom
|
||||||
|
grunt.registerTask('browsertest-server', [
|
||||||
|
'browsertest-lessjs',
|
||||||
|
'shell:generatebrowser',
|
||||||
|
'connect::keepalive'
|
||||||
|
])
|
||||||
|
|
||||||
|
var previous_force_state = grunt.option('force')
|
||||||
|
|
||||||
|
grunt.registerTask('force', function (set) {
|
||||||
|
if (set === 'on') {
|
||||||
|
grunt.option('force', true)
|
||||||
|
} else if (set === 'off') {
|
||||||
|
grunt.option('force', false)
|
||||||
|
} else if (set === 'restore') {
|
||||||
|
grunt.option('force', previous_force_state)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
grunt.registerTask('sauce', [
|
||||||
|
'browsertest-lessjs',
|
||||||
|
'shell:generatebrowser',
|
||||||
|
'connect',
|
||||||
|
'sauce-after-setup'
|
||||||
|
])
|
||||||
|
|
||||||
|
grunt.registerTask('sauce-after-setup', [
|
||||||
|
'saucelabs-mocha:all',
|
||||||
|
'clean:sauce_log'
|
||||||
|
])
|
||||||
|
|
||||||
|
var testTasks = [
|
||||||
|
'clean',
|
||||||
|
'eslint',
|
||||||
|
'shell:testbuild',
|
||||||
|
'shell:test',
|
||||||
|
'shell:opts',
|
||||||
|
'shell:plugin',
|
||||||
|
'connect',
|
||||||
|
'shell:runbrowser'
|
||||||
|
]
|
||||||
|
|
||||||
|
if (
|
||||||
|
isNaN(Number(process.env.TRAVIS_PULL_REQUEST, 10)) &&
|
||||||
|
process.env.TRAVIS_BRANCH === 'master'
|
||||||
|
) {
|
||||||
|
testTasks.push('force:on')
|
||||||
|
testTasks.push('sauce-after-setup')
|
||||||
|
testTasks.push('force:off')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run all tests
|
||||||
|
grunt.registerTask('test', testTasks)
|
||||||
|
|
||||||
|
// Run shell option tests (includes deprecated options)
|
||||||
|
grunt.registerTask('shell-options', ['shell:opts'])
|
||||||
|
|
||||||
|
// Run shell plugin test
|
||||||
|
grunt.registerTask('shell-plugin', ['shell:plugin'])
|
||||||
|
|
||||||
|
// Quickly build and run Node tests
|
||||||
|
grunt.registerTask('quicktest', ['shell:testcjs', 'shell:test'])
|
||||||
|
|
||||||
|
// generate a good test environment for testing sourcemaps
|
||||||
|
grunt.registerTask('sourcemap-test', [
|
||||||
|
'clean:sourcemap-test',
|
||||||
|
'shell:build:lessc',
|
||||||
|
'shell:sourcemap-test',
|
||||||
|
'connect::keepalive'
|
||||||
|
])
|
||||||
|
|
||||||
|
// Run benchmark
|
||||||
|
grunt.registerTask('benchmark', ['shell:testcjs', 'shell:benchmark'])
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
# [Less.js](http://lesscss.org)
|
||||||
|
|
||||||
|
> The **dynamic** stylesheet language. [http://lesscss.org](http://lesscss.org).
|
||||||
|
|
||||||
|
This is the JavaScript, official, stable version of Less.
|
||||||
|
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
Add Less.js to your project:
|
||||||
|
```sh
|
||||||
|
npm install less
|
||||||
|
```
|
|
@ -0,0 +1,14 @@
|
||||||
|
const pkg = require('./../package.json')
|
||||||
|
|
||||||
|
module.exports = `/**
|
||||||
|
* Less - ${pkg.description} v${pkg.version}
|
||||||
|
* http://lesscss.org
|
||||||
|
*
|
||||||
|
* Copyright (c) 2009-${new Date().getFullYear()}, ${pkg.author.name} <${
|
||||||
|
pkg.author.email
|
||||||
|
}>
|
||||||
|
* Licensed under the ${pkg.license} License.
|
||||||
|
*
|
||||||
|
* @license ${pkg.license}
|
||||||
|
*/
|
||||||
|
`
|
|
@ -0,0 +1,88 @@
|
||||||
|
const rollup = require('rollup')
|
||||||
|
const typescript = require('rollup-plugin-typescript2')
|
||||||
|
const commonjs = require('@rollup/plugin-commonjs')
|
||||||
|
const json = require('@rollup/plugin-json')
|
||||||
|
const resolve = require('@rollup/plugin-node-resolve').nodeResolve
|
||||||
|
const terser = require('rollup-plugin-terser').terser
|
||||||
|
const banner = require('./banner')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
const rootPath = path.join(__dirname, '..')
|
||||||
|
|
||||||
|
const args = require('minimist')(process.argv.slice(2))
|
||||||
|
|
||||||
|
let outDir = args.dist ? './dist' : './tmp'
|
||||||
|
|
||||||
|
async function buildBrowser() {
|
||||||
|
let bundle = await rollup.rollup({
|
||||||
|
input: './src/less-browser/bootstrap.js',
|
||||||
|
output: [
|
||||||
|
{
|
||||||
|
file: 'less.js',
|
||||||
|
format: 'umd'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
file: 'less.min.js',
|
||||||
|
format: 'umd'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
plugins: [
|
||||||
|
resolve(),
|
||||||
|
commonjs(),
|
||||||
|
json(),
|
||||||
|
typescript({
|
||||||
|
verbosity: 2,
|
||||||
|
tsconfigDefaults: {
|
||||||
|
compilerOptions: {
|
||||||
|
allowJs: true,
|
||||||
|
sourceMap: true,
|
||||||
|
target: 'ES5'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
include: ['*.ts', '**/*.ts', '*.js', '**/*.js'],
|
||||||
|
exclude: ['node_modules'] // only transpile our source code
|
||||||
|
}),
|
||||||
|
terser({
|
||||||
|
compress: true,
|
||||||
|
include: [/^.+\.min\.js$/],
|
||||||
|
output: {
|
||||||
|
comments: function (node, comment) {
|
||||||
|
if (comment.type == 'comment2') {
|
||||||
|
// preserve banner
|
||||||
|
return /@license/i.test(comment.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!args.out || args.out.indexOf('less.js') > -1) {
|
||||||
|
const file = args.out || `${outDir}/less.js`
|
||||||
|
console.log(`Writing ${file}...`)
|
||||||
|
await bundle.write({
|
||||||
|
file: path.join(rootPath, file),
|
||||||
|
format: 'umd',
|
||||||
|
name: 'less',
|
||||||
|
banner
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!args.out || args.out.indexOf('less.min.js') > -1) {
|
||||||
|
const file = args.out || `${outDir}/less.min.js`
|
||||||
|
console.log(`Writing ${file}...`)
|
||||||
|
await bundle.write({
|
||||||
|
file: path.join(rootPath, file),
|
||||||
|
format: 'umd',
|
||||||
|
name: 'less',
|
||||||
|
sourcemap: true,
|
||||||
|
banner
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function build() {
|
||||||
|
await buildBrowser()
|
||||||
|
}
|
||||||
|
|
||||||
|
build()
|
|
@ -0,0 +1,130 @@
|
||||||
|
{
|
||||||
|
"name": "less",
|
||||||
|
"version": "4.1.3",
|
||||||
|
"description": "Leaner CSS",
|
||||||
|
"homepage": "http://lesscss.org",
|
||||||
|
"author": {
|
||||||
|
"name": "Alexis Sellier",
|
||||||
|
"email": "self@cloudhead.net"
|
||||||
|
},
|
||||||
|
"contributors": [
|
||||||
|
"The Core Less Team"
|
||||||
|
],
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/less/less.js.git"
|
||||||
|
},
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"lessc": "./bin/lessc"
|
||||||
|
},
|
||||||
|
"main": "index",
|
||||||
|
"module": "./lib/less-node/index",
|
||||||
|
"directories": {
|
||||||
|
"test": "./test"
|
||||||
|
},
|
||||||
|
"browser": "./dist/less.js",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "grunt test",
|
||||||
|
"grunt": "grunt",
|
||||||
|
"lint": "eslint '**/*.{ts,js}'",
|
||||||
|
"lint:fix": "eslint '**/*.{ts,js}' --fix",
|
||||||
|
"build": "npm-run-all clean compile",
|
||||||
|
"clean": "shx rm -rf ./lib tsconfig.tsbuildinfo",
|
||||||
|
"compile": "tsc -p tsconfig.build.json",
|
||||||
|
"copy:root": "shx cp -rf ./dist ../../",
|
||||||
|
"dev": "tsc -p tsconfig.build.json -w",
|
||||||
|
"prepublishOnly": "grunt dist"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"errno": "^0.1.1",
|
||||||
|
"graceful-fs": "^4.1.2",
|
||||||
|
"image-size": "~0.5.0",
|
||||||
|
"make-dir": "^2.1.0",
|
||||||
|
"mime": "^1.4.1",
|
||||||
|
"needle": "^3.1.0",
|
||||||
|
"source-map": "~0.6.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@less/test-data": "^4.1.0",
|
||||||
|
"@less/test-import-module": "^4.0.0",
|
||||||
|
"@rollup/plugin-commonjs": "^17.0.0",
|
||||||
|
"@rollup/plugin-json": "^4.1.0",
|
||||||
|
"@rollup/plugin-node-resolve": "^11.0.0",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^4.28.0",
|
||||||
|
"@typescript-eslint/parser": "^4.28.0",
|
||||||
|
"benny": "^3.6.12",
|
||||||
|
"bootstrap-less-port": "0.3.0",
|
||||||
|
"chai": "^4.2.0",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"diff": "^3.2.0",
|
||||||
|
"eslint": "^7.29.0",
|
||||||
|
"fs-extra": "^8.1.0",
|
||||||
|
"git-rev": "^0.2.1",
|
||||||
|
"globby": "^10.0.1",
|
||||||
|
"grunt": "^1.0.4",
|
||||||
|
"grunt-cli": "^1.3.2",
|
||||||
|
"grunt-contrib-clean": "^1.0.0",
|
||||||
|
"grunt-contrib-connect": "^1.0.2",
|
||||||
|
"grunt-eslint": "^23.0.0",
|
||||||
|
"grunt-saucelabs": "^9.0.1",
|
||||||
|
"grunt-shell": "^1.3.0",
|
||||||
|
"html-template-tag": "^3.2.0",
|
||||||
|
"jit-grunt": "^0.10.0",
|
||||||
|
"less-plugin-autoprefix": "^1.5.1",
|
||||||
|
"less-plugin-clean-css": "^1.5.1",
|
||||||
|
"minimist": "^1.2.0",
|
||||||
|
"mocha": "^6.2.1",
|
||||||
|
"mocha-headless-chrome": "^4.0.0",
|
||||||
|
"mocha-teamcity-reporter": "^3.0.0",
|
||||||
|
"nock": "^11.8.2",
|
||||||
|
"npm-run-all": "^4.1.5",
|
||||||
|
"performance-now": "^0.2.0",
|
||||||
|
"phin": "^2.2.3",
|
||||||
|
"promise": "^7.1.1",
|
||||||
|
"read-glob": "^3.0.0",
|
||||||
|
"resolve": "^1.17.0",
|
||||||
|
"rollup": "^2.52.2",
|
||||||
|
"rollup-plugin-terser": "^5.1.1",
|
||||||
|
"rollup-plugin-typescript2": "^0.29.0",
|
||||||
|
"semver": "^6.3.0",
|
||||||
|
"shx": "^0.3.2",
|
||||||
|
"time-grunt": "^1.3.0",
|
||||||
|
"ts-node": "^9.1.1",
|
||||||
|
"typescript": "^4.3.4",
|
||||||
|
"uikit": "2.27.4"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"compile less",
|
||||||
|
"css nesting",
|
||||||
|
"css variable",
|
||||||
|
"css",
|
||||||
|
"gradients css",
|
||||||
|
"gradients css3",
|
||||||
|
"less compiler",
|
||||||
|
"less css",
|
||||||
|
"less mixins",
|
||||||
|
"less",
|
||||||
|
"less.js",
|
||||||
|
"lesscss",
|
||||||
|
"mixins",
|
||||||
|
"nested css",
|
||||||
|
"parser",
|
||||||
|
"preprocessor",
|
||||||
|
"bootstrap css",
|
||||||
|
"bootstrap less",
|
||||||
|
"style",
|
||||||
|
"styles",
|
||||||
|
"stylesheet",
|
||||||
|
"variables in css",
|
||||||
|
"css less"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"copy-anything": "^2.0.1",
|
||||||
|
"parse-node-version": "^1.0.1",
|
||||||
|
"tslib": "^2.3.0"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
export default {
|
||||||
|
encodeBase64: function encodeBase64(str) {
|
||||||
|
// Avoid Buffer constructor on newer versions of Node.js.
|
||||||
|
const buffer = Buffer.from ? Buffer.from(str) : new Buffer(str)
|
||||||
|
return buffer.toString('base64')
|
||||||
|
},
|
||||||
|
mimeLookup: function (filename) {
|
||||||
|
return require('mime').lookup(filename)
|
||||||
|
},
|
||||||
|
charsetLookup: function (mime) {
|
||||||
|
return require('mime').charsets.lookup(mime)
|
||||||
|
},
|
||||||
|
getSourceMapGenerator: function getSourceMapGenerator() {
|
||||||
|
return require('source-map').SourceMapGenerator
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,153 @@
|
||||||
|
import path from 'path'
|
||||||
|
import fs from './fs'
|
||||||
|
import AbstractFileManager from '../less/environment/abstract-file-manager.js'
|
||||||
|
|
||||||
|
const FileManager = function () {}
|
||||||
|
FileManager.prototype = Object.assign(new AbstractFileManager(), {
|
||||||
|
supports() {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
|
||||||
|
supportsSync() {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
|
||||||
|
loadFile(filename, currentDirectory, options, environment, callback) {
|
||||||
|
let fullFilename
|
||||||
|
const isAbsoluteFilename = this.isPathAbsolute(filename)
|
||||||
|
const filenamesTried = []
|
||||||
|
const self = this
|
||||||
|
const prefix = filename.slice(0, 1)
|
||||||
|
const explicit = prefix === '.' || prefix === '/'
|
||||||
|
let result = null
|
||||||
|
let isNodeModule = false
|
||||||
|
const npmPrefix = 'npm://'
|
||||||
|
|
||||||
|
options = options || {}
|
||||||
|
|
||||||
|
const paths = isAbsoluteFilename ? [''] : [currentDirectory]
|
||||||
|
|
||||||
|
if (options.paths) {
|
||||||
|
paths.push.apply(paths, options.paths)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAbsoluteFilename && paths.indexOf('.') === -1) {
|
||||||
|
paths.push('.')
|
||||||
|
}
|
||||||
|
|
||||||
|
const prefixes = options.prefixes || ['']
|
||||||
|
const fileParts = this.extractUrlParts(filename)
|
||||||
|
|
||||||
|
if (options.syncImport) {
|
||||||
|
getFileData(returnData, returnData)
|
||||||
|
if (callback) {
|
||||||
|
callback(result.error, result)
|
||||||
|
} else {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// promise is guaranteed to be asyncronous
|
||||||
|
// which helps as it allows the file handle
|
||||||
|
// to be closed before it continues with the next file
|
||||||
|
return new Promise(getFileData)
|
||||||
|
}
|
||||||
|
|
||||||
|
function returnData(data) {
|
||||||
|
if (!data.filename) {
|
||||||
|
result = { error: data }
|
||||||
|
} else {
|
||||||
|
result = data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFileData(fulfill, reject) {
|
||||||
|
;(function tryPathIndex(i) {
|
||||||
|
function tryWithExtension() {
|
||||||
|
const extFilename = options.ext
|
||||||
|
? self.tryAppendExtension(fullFilename, options.ext)
|
||||||
|
: fullFilename
|
||||||
|
|
||||||
|
if (extFilename !== fullFilename && !explicit && paths[i] === '.') {
|
||||||
|
try {
|
||||||
|
fullFilename = require.resolve(extFilename)
|
||||||
|
isNodeModule = true
|
||||||
|
} catch (e) {
|
||||||
|
filenamesTried.push(npmPrefix + extFilename)
|
||||||
|
fullFilename = extFilename
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fullFilename = extFilename
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (i < paths.length) {
|
||||||
|
;(function tryPrefix(j) {
|
||||||
|
if (j < prefixes.length) {
|
||||||
|
isNodeModule = false
|
||||||
|
fullFilename =
|
||||||
|
fileParts.rawPath + prefixes[j] + fileParts.filename
|
||||||
|
|
||||||
|
if (paths[i]) {
|
||||||
|
fullFilename = path.join(paths[i], fullFilename)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!explicit && paths[i] === '.') {
|
||||||
|
try {
|
||||||
|
fullFilename = require.resolve(fullFilename)
|
||||||
|
isNodeModule = true
|
||||||
|
} catch (e) {
|
||||||
|
filenamesTried.push(npmPrefix + fullFilename)
|
||||||
|
tryWithExtension()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tryWithExtension()
|
||||||
|
}
|
||||||
|
|
||||||
|
const readFileArgs = [fullFilename]
|
||||||
|
if (!options.rawBuffer) {
|
||||||
|
readFileArgs.push('utf-8')
|
||||||
|
}
|
||||||
|
if (options.syncImport) {
|
||||||
|
try {
|
||||||
|
const data = fs.readFileSync.apply(this, readFileArgs)
|
||||||
|
fulfill({ contents: data, filename: fullFilename })
|
||||||
|
} catch (e) {
|
||||||
|
filenamesTried.push(
|
||||||
|
isNodeModule ? npmPrefix + fullFilename : fullFilename
|
||||||
|
)
|
||||||
|
return tryPrefix(j + 1)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
readFileArgs.push(function (e, data) {
|
||||||
|
if (e) {
|
||||||
|
filenamesTried.push(
|
||||||
|
isNodeModule ? npmPrefix + fullFilename : fullFilename
|
||||||
|
)
|
||||||
|
return tryPrefix(j + 1)
|
||||||
|
}
|
||||||
|
fulfill({ contents: data, filename: fullFilename })
|
||||||
|
})
|
||||||
|
fs.readFile.apply(this, readFileArgs)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tryPathIndex(i + 1)
|
||||||
|
}
|
||||||
|
})(0)
|
||||||
|
} else {
|
||||||
|
reject({
|
||||||
|
type: 'File',
|
||||||
|
message: `'${filename}' wasn't found. Tried - ${filenamesTried.join(
|
||||||
|
','
|
||||||
|
)}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})(0)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
loadFileSync(filename, currentDirectory, options, environment) {
|
||||||
|
options.syncImport = true
|
||||||
|
return this.loadFile(filename, currentDirectory, options, environment)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default FileManager
|
|
@ -0,0 +1,7 @@
|
||||||
|
let fs
|
||||||
|
try {
|
||||||
|
fs = require('graceful-fs')
|
||||||
|
} catch (e) {
|
||||||
|
fs = require('fs')
|
||||||
|
}
|
||||||
|
export default fs
|
|
@ -0,0 +1,67 @@
|
||||||
|
import Dimension from '../less/tree/dimension'
|
||||||
|
import Expression from '../less/tree/expression'
|
||||||
|
import functionRegistry from './../less/functions/function-registry'
|
||||||
|
|
||||||
|
export default environment => {
|
||||||
|
function imageSize(functionContext, filePathNode) {
|
||||||
|
let filePath = filePathNode.value
|
||||||
|
const currentFileInfo = functionContext.currentFileInfo
|
||||||
|
const currentDirectory = currentFileInfo.rewriteUrls
|
||||||
|
? currentFileInfo.currentDirectory
|
||||||
|
: currentFileInfo.entryPath
|
||||||
|
|
||||||
|
const fragmentStart = filePath.indexOf('#')
|
||||||
|
if (fragmentStart !== -1) {
|
||||||
|
filePath = filePath.slice(0, fragmentStart)
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileManager = environment.getFileManager(
|
||||||
|
filePath,
|
||||||
|
currentDirectory,
|
||||||
|
functionContext.context,
|
||||||
|
environment,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!fileManager) {
|
||||||
|
throw {
|
||||||
|
type: 'File',
|
||||||
|
message: `Can not set up FileManager for ${filePathNode}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileSync = fileManager.loadFileSync(
|
||||||
|
filePath,
|
||||||
|
currentDirectory,
|
||||||
|
functionContext.context,
|
||||||
|
environment
|
||||||
|
)
|
||||||
|
|
||||||
|
if (fileSync.error) {
|
||||||
|
throw fileSync.error
|
||||||
|
}
|
||||||
|
|
||||||
|
const sizeOf = require('image-size')
|
||||||
|
return sizeOf(fileSync.filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageFunctions = {
|
||||||
|
'image-size': function (filePathNode) {
|
||||||
|
const size = imageSize(this, filePathNode)
|
||||||
|
return new Expression([
|
||||||
|
new Dimension(size.width, 'px'),
|
||||||
|
new Dimension(size.height, 'px')
|
||||||
|
])
|
||||||
|
},
|
||||||
|
'image-width': function (filePathNode) {
|
||||||
|
const size = imageSize(this, filePathNode)
|
||||||
|
return new Dimension(size.width, 'px')
|
||||||
|
},
|
||||||
|
'image-height': function (filePathNode) {
|
||||||
|
const size = imageSize(this, filePathNode)
|
||||||
|
return new Dimension(size.height, 'px')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
functionRegistry.addMultiple(imageFunctions)
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
import environment from './environment'
|
||||||
|
import FileManager from './file-manager'
|
||||||
|
import UrlFileManager from './url-file-manager'
|
||||||
|
import createFromEnvironment from '../less'
|
||||||
|
const less = createFromEnvironment(environment, [
|
||||||
|
new FileManager(),
|
||||||
|
new UrlFileManager()
|
||||||
|
])
|
||||||
|
import lesscHelper from './lessc-helper'
|
||||||
|
|
||||||
|
// allow people to create less with their own environment
|
||||||
|
less.createFromEnvironment = createFromEnvironment
|
||||||
|
less.lesscHelper = lesscHelper
|
||||||
|
less.PluginLoader = require('./plugin-loader').default
|
||||||
|
less.fs = require('./fs').default
|
||||||
|
less.FileManager = FileManager
|
||||||
|
less.UrlFileManager = UrlFileManager
|
||||||
|
|
||||||
|
// Set up options
|
||||||
|
less.options = require('../less/default-options').default()
|
||||||
|
|
||||||
|
// provide image-size functionality
|
||||||
|
require('./image-size').default(less.environment)
|
||||||
|
|
||||||
|
export default less
|
|
@ -0,0 +1,179 @@
|
||||||
|
// lessc_helper.js
|
||||||
|
//
|
||||||
|
// helper functions for lessc
|
||||||
|
const lessc_helper = {
|
||||||
|
// Stylize a string
|
||||||
|
stylize: function (str, style) {
|
||||||
|
const styles = {
|
||||||
|
reset: [0, 0],
|
||||||
|
bold: [1, 22],
|
||||||
|
inverse: [7, 27],
|
||||||
|
underline: [4, 24],
|
||||||
|
yellow: [33, 39],
|
||||||
|
green: [32, 39],
|
||||||
|
red: [31, 39],
|
||||||
|
grey: [90, 39]
|
||||||
|
}
|
||||||
|
return `\x1b[${styles[style][0]}m${str}\x1b[${styles[style][1]}m`
|
||||||
|
},
|
||||||
|
|
||||||
|
// Print command line options
|
||||||
|
printUsage: function () {
|
||||||
|
console.log(
|
||||||
|
'usage: lessc [option option=parameter ...] <source> [destination]'
|
||||||
|
)
|
||||||
|
console.log('')
|
||||||
|
console.log(
|
||||||
|
"If source is set to `-' (dash or hyphen-minus), input is read from stdin."
|
||||||
|
)
|
||||||
|
console.log('')
|
||||||
|
console.log('options:')
|
||||||
|
console.log(
|
||||||
|
' -h, --help Prints help (this message) and exit.'
|
||||||
|
)
|
||||||
|
console.log(
|
||||||
|
" --include-path=PATHS Sets include paths. Separated by `:'. `;' also supported on windows."
|
||||||
|
)
|
||||||
|
console.log(
|
||||||
|
' -M, --depends Outputs a makefile import dependency list to stdout.'
|
||||||
|
)
|
||||||
|
console.log(' --no-color Disables colorized output.')
|
||||||
|
console.log(
|
||||||
|
' --ie-compat Enables IE8 compatibility checks.'
|
||||||
|
)
|
||||||
|
console.log(
|
||||||
|
' --js Enables inline JavaScript in less files'
|
||||||
|
)
|
||||||
|
console.log(' -l, --lint Syntax check only (lint).')
|
||||||
|
console.log(
|
||||||
|
' -s, --silent Suppresses output of error messages.'
|
||||||
|
)
|
||||||
|
console.log(' --strict-imports Forces evaluation of imports.')
|
||||||
|
console.log(
|
||||||
|
' --insecure Allows imports from insecure https hosts.'
|
||||||
|
)
|
||||||
|
console.log(
|
||||||
|
' -v, --version Prints version number and exit.'
|
||||||
|
)
|
||||||
|
console.log(' --verbose Be verbose.')
|
||||||
|
console.log(
|
||||||
|
' --source-map[=FILENAME] Outputs a v3 sourcemap to the filename (or output filename.map).'
|
||||||
|
)
|
||||||
|
console.log(
|
||||||
|
' --source-map-rootpath=X Adds this path onto the sourcemap filename and less file paths.'
|
||||||
|
)
|
||||||
|
console.log(
|
||||||
|
' --source-map-basepath=X Sets sourcemap base path, defaults to current working directory.'
|
||||||
|
)
|
||||||
|
console.log(
|
||||||
|
' --source-map-include-source Puts the less files into the map instead of referencing them.'
|
||||||
|
)
|
||||||
|
console.log(
|
||||||
|
' --source-map-inline Puts the map (and any less files) as a base64 data uri into the output css file.'
|
||||||
|
)
|
||||||
|
console.log(
|
||||||
|
' --source-map-url=URL Sets a custom URL to map file, for sourceMappingURL comment'
|
||||||
|
)
|
||||||
|
console.log(' in generated CSS file.')
|
||||||
|
console.log(
|
||||||
|
' --source-map-no-annotation Excludes the sourceMappingURL comment from the output css file.'
|
||||||
|
)
|
||||||
|
console.log(
|
||||||
|
' -rp, --rootpath=URL Sets rootpath for url rewriting in relative imports and urls'
|
||||||
|
)
|
||||||
|
console.log(
|
||||||
|
' Works with or without the relative-urls option.'
|
||||||
|
)
|
||||||
|
console.log(
|
||||||
|
' -ru=, --rewrite-urls= Rewrites URLs to make them relative to the base less file.'
|
||||||
|
)
|
||||||
|
console.log(
|
||||||
|
" all|local|off 'all' rewrites all URLs, 'local' just those starting with a '.'"
|
||||||
|
)
|
||||||
|
console.log('')
|
||||||
|
console.log(' -m=, --math=')
|
||||||
|
console.log(
|
||||||
|
' always Less will eagerly perform math operations always.'
|
||||||
|
)
|
||||||
|
console.log(
|
||||||
|
' parens-division Math performed except for division (/) operator'
|
||||||
|
)
|
||||||
|
console.log(
|
||||||
|
' parens | strict Math only performed inside parentheses'
|
||||||
|
)
|
||||||
|
console.log(
|
||||||
|
' strict-legacy Parens required in very strict terms (legacy --strict-math)'
|
||||||
|
)
|
||||||
|
console.log('')
|
||||||
|
console.log(
|
||||||
|
' -su=on|off Allows mixed units, e.g. 1px+1em or 1px*1px which have units'
|
||||||
|
)
|
||||||
|
console.log(' --strict-units=on|off that cannot be represented.')
|
||||||
|
console.log(
|
||||||
|
" --global-var='VAR=VALUE' Defines a variable that can be referenced by the file."
|
||||||
|
)
|
||||||
|
console.log(
|
||||||
|
" --modify-var='VAR=VALUE' Modifies a variable already declared in the file."
|
||||||
|
)
|
||||||
|
console.log(
|
||||||
|
" --url-args='QUERYSTRING' Adds params into url tokens (e.g. 42, cb=42 or 'a=1&b=2')"
|
||||||
|
)
|
||||||
|
console.log(
|
||||||
|
' --plugin=PLUGIN=OPTIONS Loads a plugin. You can also omit the --plugin= if the plugin begins'
|
||||||
|
)
|
||||||
|
console.log(
|
||||||
|
' less-plugin. E.g. the clean css plugin is called less-plugin-clean-css'
|
||||||
|
)
|
||||||
|
console.log(
|
||||||
|
' once installed (npm install less-plugin-clean-css), use either with'
|
||||||
|
)
|
||||||
|
console.log(
|
||||||
|
' --plugin=less-plugin-clean-css or just --clean-css'
|
||||||
|
)
|
||||||
|
console.log(
|
||||||
|
' specify options afterwards e.g. --plugin=less-plugin-clean-css="advanced"'
|
||||||
|
)
|
||||||
|
console.log(' or --clean-css="advanced"')
|
||||||
|
console.log(' --disable-plugin-rule Disallow @plugin statements')
|
||||||
|
console.log('')
|
||||||
|
console.log('-------------------------- Deprecated ----------------')
|
||||||
|
console.log(
|
||||||
|
' -sm=on|off Legacy parens-only math. Use --math'
|
||||||
|
)
|
||||||
|
console.log(' --strict-math=on|off ')
|
||||||
|
console.log('')
|
||||||
|
console.log(' --line-numbers=TYPE Outputs filename and line numbers.')
|
||||||
|
console.log(
|
||||||
|
" TYPE can be either 'comments', which will output"
|
||||||
|
)
|
||||||
|
console.log(
|
||||||
|
" the debug info within comments, 'mediaquery'"
|
||||||
|
)
|
||||||
|
console.log(
|
||||||
|
' that will output the information within a fake'
|
||||||
|
)
|
||||||
|
console.log(
|
||||||
|
' media query which is compatible with the SASS'
|
||||||
|
)
|
||||||
|
console.log(
|
||||||
|
" format, and 'all' which will do both."
|
||||||
|
)
|
||||||
|
console.log(
|
||||||
|
' -x, --compress Compresses output by removing some whitespaces.'
|
||||||
|
)
|
||||||
|
console.log(
|
||||||
|
' We recommend you use a dedicated minifer like less-plugin-clean-css'
|
||||||
|
)
|
||||||
|
console.log('')
|
||||||
|
console.log('Report bugs to: http://github.com/less/less.js/issues')
|
||||||
|
console.log('Home page: <http://lesscss.org/>')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exports helper functions
|
||||||
|
// eslint-disable-next-line no-prototype-builtins
|
||||||
|
for (const h in lessc_helper) {
|
||||||
|
if (lessc_helper.hasOwnProperty(h)) {
|
||||||
|
exports[h] = lessc_helper[h]
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
import path from 'path'
|
||||||
|
import AbstractPluginLoader from '../less/environment/abstract-plugin-loader.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Node Plugin Loader
|
||||||
|
*/
|
||||||
|
const PluginLoader = function (less) {
|
||||||
|
this.less = less
|
||||||
|
this.require = prefix => {
|
||||||
|
prefix = path.dirname(prefix)
|
||||||
|
return id => {
|
||||||
|
const str = id.substr(0, 2)
|
||||||
|
if (str === '..' || str === './') {
|
||||||
|
return require(path.join(prefix, id))
|
||||||
|
} else {
|
||||||
|
return require(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PluginLoader.prototype = Object.assign(new AbstractPluginLoader(), {
|
||||||
|
loadPlugin(filename, basePath, context, environment, fileManager) {
|
||||||
|
const prefix = filename.slice(0, 1)
|
||||||
|
const explicit =
|
||||||
|
prefix === '.' ||
|
||||||
|
prefix === '/' ||
|
||||||
|
filename.slice(-3).toLowerCase() === '.js'
|
||||||
|
if (!explicit) {
|
||||||
|
context.prefixes = ['less-plugin-', '']
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.syncImport) {
|
||||||
|
return fileManager.loadFileSync(filename, basePath, context, environment)
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((fulfill, reject) => {
|
||||||
|
fileManager
|
||||||
|
.loadFile(filename, basePath, context, environment)
|
||||||
|
.then(data => {
|
||||||
|
try {
|
||||||
|
fulfill(data)
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
reject(e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
reject(err)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
loadPluginSync(filename, basePath, context, environment, fileManager) {
|
||||||
|
context.syncImport = true
|
||||||
|
return this.loadPlugin(
|
||||||
|
filename,
|
||||||
|
basePath,
|
||||||
|
context,
|
||||||
|
environment,
|
||||||
|
fileManager
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default PluginLoader
|
|
@ -0,0 +1,73 @@
|
||||||
|
/* eslint-disable no-unused-vars */
|
||||||
|
/**
|
||||||
|
* @todo - remove top eslint rule when FileManagers have JSDoc type
|
||||||
|
* and are TS-type-checked
|
||||||
|
*/
|
||||||
|
const isUrlRe = /^(?:https?:)?\/\//i
|
||||||
|
import url from 'url'
|
||||||
|
let request
|
||||||
|
import AbstractFileManager from '../less/environment/abstract-file-manager.js'
|
||||||
|
import logger from '../less/logger'
|
||||||
|
|
||||||
|
const UrlFileManager = function () {}
|
||||||
|
UrlFileManager.prototype = Object.assign(new AbstractFileManager(), {
|
||||||
|
supports(filename, currentDirectory, options, environment) {
|
||||||
|
return isUrlRe.test(filename) || isUrlRe.test(currentDirectory)
|
||||||
|
},
|
||||||
|
|
||||||
|
loadFile(filename, currentDirectory, options, environment) {
|
||||||
|
return new Promise((fulfill, reject) => {
|
||||||
|
if (request === undefined) {
|
||||||
|
try {
|
||||||
|
request = require('needle')
|
||||||
|
} catch (e) {
|
||||||
|
request = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!request) {
|
||||||
|
reject({
|
||||||
|
type: 'File',
|
||||||
|
message:
|
||||||
|
"optional dependency 'needle' required to import over http(s)\n"
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let urlStr = isUrlRe.test(filename)
|
||||||
|
? filename
|
||||||
|
: url.resolve(currentDirectory, filename)
|
||||||
|
|
||||||
|
/** native-request currently has a bug */
|
||||||
|
const hackUrlStr = urlStr.indexOf('?') === -1 ? urlStr + '?' : urlStr
|
||||||
|
|
||||||
|
request.get(hackUrlStr, { follow_max: 5 }, (err, resp, body) => {
|
||||||
|
if (err || (resp && resp.statusCode >= 400)) {
|
||||||
|
const message =
|
||||||
|
resp && resp.statusCode === 404
|
||||||
|
? `resource '${urlStr}' was not found\n`
|
||||||
|
: `resource '${urlStr}' gave this Error:\n ${
|
||||||
|
err || resp.statusMessage || resp.statusCode
|
||||||
|
}\n`
|
||||||
|
reject({ type: 'File', message })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (resp.statusCode >= 300) {
|
||||||
|
reject({
|
||||||
|
type: 'File',
|
||||||
|
message: `resource '${urlStr}' caused too many redirects`
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
body = body.toString('utf8')
|
||||||
|
if (!body) {
|
||||||
|
logger.warn(
|
||||||
|
`Warning: Empty body (HTTP ${resp.statusCode}) returned by "${urlStr}"`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
fulfill({ contents: body || '', filename: urlStr })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default UrlFileManager
|
|
@ -0,0 +1,12 @@
|
||||||
|
export const Math = {
|
||||||
|
ALWAYS: 0,
|
||||||
|
PARENS_DIVISION: 1,
|
||||||
|
PARENS: 2
|
||||||
|
// removed - STRICT_LEGACY: 3
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RewriteUrls = {
|
||||||
|
OFF: 0,
|
||||||
|
LOCAL: 1,
|
||||||
|
ALL: 2
|
||||||
|
}
|
|
@ -0,0 +1,183 @@
|
||||||
|
const contexts = {}
|
||||||
|
export default contexts
|
||||||
|
import * as Constants from './constants'
|
||||||
|
|
||||||
|
const copyFromOriginal = function copyFromOriginal(
|
||||||
|
original,
|
||||||
|
destination,
|
||||||
|
propertiesToCopy
|
||||||
|
) {
|
||||||
|
if (!original) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < propertiesToCopy.length; i++) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(original, propertiesToCopy[i])) {
|
||||||
|
destination[propertiesToCopy[i]] = original[propertiesToCopy[i]]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
parse is used whilst parsing
|
||||||
|
*/
|
||||||
|
const parseCopyProperties = [
|
||||||
|
// options
|
||||||
|
'paths', // option - unmodified - paths to search for imports on
|
||||||
|
'rewriteUrls', // option - whether to adjust URL's to be relative
|
||||||
|
'rootpath', // option - rootpath to append to URL's
|
||||||
|
'strictImports', // option -
|
||||||
|
'insecure', // option - whether to allow imports from insecure ssl hosts
|
||||||
|
'dumpLineNumbers', // option - whether to dump line numbers
|
||||||
|
'compress', // option - whether to compress
|
||||||
|
'syncImport', // option - whether to import synchronously
|
||||||
|
'chunkInput', // option - whether to chunk input. more performant but causes parse issues.
|
||||||
|
'mime', // browser only - mime type for sheet import
|
||||||
|
'useFileCache', // browser only - whether to use the per file session cache
|
||||||
|
// context
|
||||||
|
'processImports', // option & context - whether to process imports. if false then imports will not be imported.
|
||||||
|
// Used by the import manager to stop multiple import visitors being created.
|
||||||
|
'pluginManager' // Used as the plugin manager for the session
|
||||||
|
]
|
||||||
|
|
||||||
|
contexts.Parse = function (options) {
|
||||||
|
copyFromOriginal(options, this, parseCopyProperties)
|
||||||
|
|
||||||
|
if (typeof this.paths === 'string') {
|
||||||
|
this.paths = [this.paths]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const evalCopyProperties = [
|
||||||
|
'paths', // additional include paths
|
||||||
|
'compress', // whether to compress
|
||||||
|
'math', // whether math has to be within parenthesis
|
||||||
|
'strictUnits', // whether units need to evaluate correctly
|
||||||
|
'sourceMap', // whether to output a source map
|
||||||
|
'importMultiple', // whether we are currently importing multiple copies
|
||||||
|
'urlArgs', // whether to add args into url tokens
|
||||||
|
'javascriptEnabled', // option - whether Inline JavaScript is enabled. if undefined, defaults to false
|
||||||
|
'pluginManager', // Used as the plugin manager for the session
|
||||||
|
'importantScope', // used to bubble up !important statements
|
||||||
|
'rewriteUrls' // option - whether to adjust URL's to be relative
|
||||||
|
]
|
||||||
|
|
||||||
|
contexts.Eval = function (options, frames) {
|
||||||
|
copyFromOriginal(options, this, evalCopyProperties)
|
||||||
|
|
||||||
|
if (typeof this.paths === 'string') {
|
||||||
|
this.paths = [this.paths]
|
||||||
|
}
|
||||||
|
|
||||||
|
this.frames = frames || []
|
||||||
|
this.importantScope = this.importantScope || []
|
||||||
|
}
|
||||||
|
|
||||||
|
contexts.Eval.prototype.enterCalc = function () {
|
||||||
|
if (!this.calcStack) {
|
||||||
|
this.calcStack = []
|
||||||
|
}
|
||||||
|
this.calcStack.push(true)
|
||||||
|
this.inCalc = true
|
||||||
|
}
|
||||||
|
|
||||||
|
contexts.Eval.prototype.exitCalc = function () {
|
||||||
|
this.calcStack.pop()
|
||||||
|
if (!this.calcStack.length) {
|
||||||
|
this.inCalc = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
contexts.Eval.prototype.inParenthesis = function () {
|
||||||
|
if (!this.parensStack) {
|
||||||
|
this.parensStack = []
|
||||||
|
}
|
||||||
|
this.parensStack.push(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
contexts.Eval.prototype.outOfParenthesis = function () {
|
||||||
|
this.parensStack.pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
contexts.Eval.prototype.inCalc = false
|
||||||
|
contexts.Eval.prototype.mathOn = true
|
||||||
|
contexts.Eval.prototype.isMathOn = function (op) {
|
||||||
|
if (!this.mathOn) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
op === '/' &&
|
||||||
|
this.math !== Constants.Math.ALWAYS &&
|
||||||
|
(!this.parensStack || !this.parensStack.length)
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (this.math > Constants.Math.PARENS_DIVISION) {
|
||||||
|
return this.parensStack && this.parensStack.length
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
contexts.Eval.prototype.pathRequiresRewrite = function (path) {
|
||||||
|
const isRelative =
|
||||||
|
this.rewriteUrls === Constants.RewriteUrls.LOCAL
|
||||||
|
? isPathLocalRelative
|
||||||
|
: isPathRelative
|
||||||
|
|
||||||
|
return isRelative(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
contexts.Eval.prototype.rewritePath = function (path, rootpath) {
|
||||||
|
let newPath
|
||||||
|
|
||||||
|
rootpath = rootpath || ''
|
||||||
|
newPath = this.normalizePath(rootpath + path)
|
||||||
|
|
||||||
|
// If a path was explicit relative and the rootpath was not an absolute path
|
||||||
|
// we must ensure that the new path is also explicit relative.
|
||||||
|
if (
|
||||||
|
isPathLocalRelative(path) &&
|
||||||
|
isPathRelative(rootpath) &&
|
||||||
|
isPathLocalRelative(newPath) === false
|
||||||
|
) {
|
||||||
|
newPath = `./${newPath}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return newPath
|
||||||
|
}
|
||||||
|
|
||||||
|
contexts.Eval.prototype.normalizePath = function (path) {
|
||||||
|
const segments = path.split('/').reverse()
|
||||||
|
let segment
|
||||||
|
|
||||||
|
path = []
|
||||||
|
while (segments.length !== 0) {
|
||||||
|
segment = segments.pop()
|
||||||
|
switch (segment) {
|
||||||
|
case '.':
|
||||||
|
break
|
||||||
|
case '..':
|
||||||
|
if (path.length === 0 || path[path.length - 1] === '..') {
|
||||||
|
path.push(segment)
|
||||||
|
} else {
|
||||||
|
path.pop()
|
||||||
|
}
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
path.push(segment)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return path.join('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPathRelative(path) {
|
||||||
|
return !/^(?:[a-z-]+:|\/|#)/i.test(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPathLocalRelative(path) {
|
||||||
|
return path.charAt(0) === '.'
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo - do the same for the toCSS ?
|
|
@ -0,0 +1,150 @@
|
||||||
|
export default {
|
||||||
|
aliceblue: '#f0f8ff',
|
||||||
|
antiquewhite: '#faebd7',
|
||||||
|
aqua: '#00ffff',
|
||||||
|
aquamarine: '#7fffd4',
|
||||||
|
azure: '#f0ffff',
|
||||||
|
beige: '#f5f5dc',
|
||||||
|
bisque: '#ffe4c4',
|
||||||
|
black: '#000000',
|
||||||
|
blanchedalmond: '#ffebcd',
|
||||||
|
blue: '#0000ff',
|
||||||
|
blueviolet: '#8a2be2',
|
||||||
|
brown: '#a52a2a',
|
||||||
|
burlywood: '#deb887',
|
||||||
|
cadetblue: '#5f9ea0',
|
||||||
|
chartreuse: '#7fff00',
|
||||||
|
chocolate: '#d2691e',
|
||||||
|
coral: '#ff7f50',
|
||||||
|
cornflowerblue: '#6495ed',
|
||||||
|
cornsilk: '#fff8dc',
|
||||||
|
crimson: '#dc143c',
|
||||||
|
cyan: '#00ffff',
|
||||||
|
darkblue: '#00008b',
|
||||||
|
darkcyan: '#008b8b',
|
||||||
|
darkgoldenrod: '#b8860b',
|
||||||
|
darkgray: '#a9a9a9',
|
||||||
|
darkgrey: '#a9a9a9',
|
||||||
|
darkgreen: '#006400',
|
||||||
|
darkkhaki: '#bdb76b',
|
||||||
|
darkmagenta: '#8b008b',
|
||||||
|
darkolivegreen: '#556b2f',
|
||||||
|
darkorange: '#ff8c00',
|
||||||
|
darkorchid: '#9932cc',
|
||||||
|
darkred: '#8b0000',
|
||||||
|
darksalmon: '#e9967a',
|
||||||
|
darkseagreen: '#8fbc8f',
|
||||||
|
darkslateblue: '#483d8b',
|
||||||
|
darkslategray: '#2f4f4f',
|
||||||
|
darkslategrey: '#2f4f4f',
|
||||||
|
darkturquoise: '#00ced1',
|
||||||
|
darkviolet: '#9400d3',
|
||||||
|
deeppink: '#ff1493',
|
||||||
|
deepskyblue: '#00bfff',
|
||||||
|
dimgray: '#696969',
|
||||||
|
dimgrey: '#696969',
|
||||||
|
dodgerblue: '#1e90ff',
|
||||||
|
firebrick: '#b22222',
|
||||||
|
floralwhite: '#fffaf0',
|
||||||
|
forestgreen: '#228b22',
|
||||||
|
fuchsia: '#ff00ff',
|
||||||
|
gainsboro: '#dcdcdc',
|
||||||
|
ghostwhite: '#f8f8ff',
|
||||||
|
gold: '#ffd700',
|
||||||
|
goldenrod: '#daa520',
|
||||||
|
gray: '#808080',
|
||||||
|
grey: '#808080',
|
||||||
|
green: '#008000',
|
||||||
|
greenyellow: '#adff2f',
|
||||||
|
honeydew: '#f0fff0',
|
||||||
|
hotpink: '#ff69b4',
|
||||||
|
indianred: '#cd5c5c',
|
||||||
|
indigo: '#4b0082',
|
||||||
|
ivory: '#fffff0',
|
||||||
|
khaki: '#f0e68c',
|
||||||
|
lavender: '#e6e6fa',
|
||||||
|
lavenderblush: '#fff0f5',
|
||||||
|
lawngreen: '#7cfc00',
|
||||||
|
lemonchiffon: '#fffacd',
|
||||||
|
lightblue: '#add8e6',
|
||||||
|
lightcoral: '#f08080',
|
||||||
|
lightcyan: '#e0ffff',
|
||||||
|
lightgoldenrodyellow: '#fafad2',
|
||||||
|
lightgray: '#d3d3d3',
|
||||||
|
lightgrey: '#d3d3d3',
|
||||||
|
lightgreen: '#90ee90',
|
||||||
|
lightpink: '#ffb6c1',
|
||||||
|
lightsalmon: '#ffa07a',
|
||||||
|
lightseagreen: '#20b2aa',
|
||||||
|
lightskyblue: '#87cefa',
|
||||||
|
lightslategray: '#778899',
|
||||||
|
lightslategrey: '#778899',
|
||||||
|
lightsteelblue: '#b0c4de',
|
||||||
|
lightyellow: '#ffffe0',
|
||||||
|
lime: '#00ff00',
|
||||||
|
limegreen: '#32cd32',
|
||||||
|
linen: '#faf0e6',
|
||||||
|
magenta: '#ff00ff',
|
||||||
|
maroon: '#800000',
|
||||||
|
mediumaquamarine: '#66cdaa',
|
||||||
|
mediumblue: '#0000cd',
|
||||||
|
mediumorchid: '#ba55d3',
|
||||||
|
mediumpurple: '#9370d8',
|
||||||
|
mediumseagreen: '#3cb371',
|
||||||
|
mediumslateblue: '#7b68ee',
|
||||||
|
mediumspringgreen: '#00fa9a',
|
||||||
|
mediumturquoise: '#48d1cc',
|
||||||
|
mediumvioletred: '#c71585',
|
||||||
|
midnightblue: '#191970',
|
||||||
|
mintcream: '#f5fffa',
|
||||||
|
mistyrose: '#ffe4e1',
|
||||||
|
moccasin: '#ffe4b5',
|
||||||
|
navajowhite: '#ffdead',
|
||||||
|
navy: '#000080',
|
||||||
|
oldlace: '#fdf5e6',
|
||||||
|
olive: '#808000',
|
||||||
|
olivedrab: '#6b8e23',
|
||||||
|
orange: '#ffa500',
|
||||||
|
orangered: '#ff4500',
|
||||||
|
orchid: '#da70d6',
|
||||||
|
palegoldenrod: '#eee8aa',
|
||||||
|
palegreen: '#98fb98',
|
||||||
|
paleturquoise: '#afeeee',
|
||||||
|
palevioletred: '#d87093',
|
||||||
|
papayawhip: '#ffefd5',
|
||||||
|
peachpuff: '#ffdab9',
|
||||||
|
peru: '#cd853f',
|
||||||
|
pink: '#ffc0cb',
|
||||||
|
plum: '#dda0dd',
|
||||||
|
powderblue: '#b0e0e6',
|
||||||
|
purple: '#800080',
|
||||||
|
rebeccapurple: '#663399',
|
||||||
|
red: '#ff0000',
|
||||||
|
rosybrown: '#bc8f8f',
|
||||||
|
royalblue: '#4169e1',
|
||||||
|
saddlebrown: '#8b4513',
|
||||||
|
salmon: '#fa8072',
|
||||||
|
sandybrown: '#f4a460',
|
||||||
|
seagreen: '#2e8b57',
|
||||||
|
seashell: '#fff5ee',
|
||||||
|
sienna: '#a0522d',
|
||||||
|
silver: '#c0c0c0',
|
||||||
|
skyblue: '#87ceeb',
|
||||||
|
slateblue: '#6a5acd',
|
||||||
|
slategray: '#708090',
|
||||||
|
slategrey: '#708090',
|
||||||
|
snow: '#fffafa',
|
||||||
|
springgreen: '#00ff7f',
|
||||||
|
steelblue: '#4682b4',
|
||||||
|
tan: '#d2b48c',
|
||||||
|
teal: '#008080',
|
||||||
|
thistle: '#d8bfd8',
|
||||||
|
tomato: '#ff6347',
|
||||||
|
turquoise: '#40e0d0',
|
||||||
|
violet: '#ee82ee',
|
||||||
|
wheat: '#f5deb3',
|
||||||
|
white: '#ffffff',
|
||||||
|
whitesmoke: '#f5f5f5',
|
||||||
|
yellow: '#ffff00',
|
||||||
|
yellowgreen: '#9acd32'
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
import colors from './colors'
|
||||||
|
import unitConversions from './unit-conversions'
|
||||||
|
|
||||||
|
export default { colors, unitConversions }
|
|
@ -0,0 +1,21 @@
|
||||||
|
export default {
|
||||||
|
length: {
|
||||||
|
m: 1,
|
||||||
|
cm: 0.01,
|
||||||
|
mm: 0.001,
|
||||||
|
in: 0.0254,
|
||||||
|
px: 0.0254 / 96,
|
||||||
|
pt: 0.0254 / 72,
|
||||||
|
pc: (0.0254 / 72) * 12
|
||||||
|
},
|
||||||
|
duration: {
|
||||||
|
s: 1,
|
||||||
|
ms: 0.001
|
||||||
|
},
|
||||||
|
angle: {
|
||||||
|
rad: 1 / (2 * Math.PI),
|
||||||
|
deg: 1 / 360,
|
||||||
|
grad: 1 / 400,
|
||||||
|
turn: 1
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
// Export a new default each time
|
||||||
|
export default function () {
|
||||||
|
return {
|
||||||
|
/* Inline Javascript - @plugin still allowed */
|
||||||
|
javascriptEnabled: false,
|
||||||
|
|
||||||
|
/* Outputs a makefile import dependency list to stdout. */
|
||||||
|
depends: false,
|
||||||
|
|
||||||
|
/* (DEPRECATED) Compress using less built-in compression.
|
||||||
|
* This does an okay job but does not utilise all the tricks of
|
||||||
|
* dedicated css compression. */
|
||||||
|
compress: false,
|
||||||
|
|
||||||
|
/* Runs the less parser and just reports errors without any output. */
|
||||||
|
lint: false,
|
||||||
|
|
||||||
|
/* Sets available include paths.
|
||||||
|
* If the file in an @import rule does not exist at that exact location,
|
||||||
|
* less will look for it at the location(s) passed to this option.
|
||||||
|
* You might use this for instance to specify a path to a library which
|
||||||
|
* you want to be referenced simply and relatively in the less files. */
|
||||||
|
paths: [],
|
||||||
|
|
||||||
|
/* color output in the terminal */
|
||||||
|
color: true,
|
||||||
|
|
||||||
|
/* The strictImports controls whether the compiler will allow an @import inside of either
|
||||||
|
* @media blocks or (a later addition) other selector blocks.
|
||||||
|
* See: https://github.com/less/less.js/issues/656 */
|
||||||
|
strictImports: false,
|
||||||
|
|
||||||
|
/* Allow Imports from Insecure HTTPS Hosts */
|
||||||
|
insecure: false,
|
||||||
|
|
||||||
|
/* Allows you to add a path to every generated import and url in your css.
|
||||||
|
* This does not affect less import statements that are processed, just ones
|
||||||
|
* that are left in the output css. */
|
||||||
|
rootpath: '',
|
||||||
|
|
||||||
|
/* By default URLs are kept as-is, so if you import a file in a sub-directory
|
||||||
|
* that references an image, exactly the same URL will be output in the css.
|
||||||
|
* This option allows you to re-write URL's in imported files so that the
|
||||||
|
* URL is always relative to the base imported file */
|
||||||
|
rewriteUrls: false,
|
||||||
|
|
||||||
|
/* How to process math
|
||||||
|
* 0 always - eagerly try to solve all operations
|
||||||
|
* 1 parens-division - require parens for division "/"
|
||||||
|
* 2 parens | strict - require parens for all operations
|
||||||
|
* 3 strict-legacy - legacy strict behavior (super-strict)
|
||||||
|
*/
|
||||||
|
math: 1,
|
||||||
|
|
||||||
|
/* Without this option, less attempts to guess at the output unit when it does maths. */
|
||||||
|
strictUnits: false,
|
||||||
|
|
||||||
|
/* Effectively the declaration is put at the top of your base Less file,
|
||||||
|
* meaning it can be used but it also can be overridden if this variable
|
||||||
|
* is defined in the file. */
|
||||||
|
globalVars: null,
|
||||||
|
|
||||||
|
/* As opposed to the global variable option, this puts the declaration at the
|
||||||
|
* end of your base file, meaning it will override anything defined in your Less file. */
|
||||||
|
modifyVars: null,
|
||||||
|
|
||||||
|
/* This option allows you to specify a argument to go on to every URL. */
|
||||||
|
urlArgs: ''
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,140 @@
|
||||||
|
class AbstractFileManager {
|
||||||
|
getPath(filename) {
|
||||||
|
let j = filename.lastIndexOf('?')
|
||||||
|
if (j > 0) {
|
||||||
|
filename = filename.slice(0, j)
|
||||||
|
}
|
||||||
|
j = filename.lastIndexOf('/')
|
||||||
|
if (j < 0) {
|
||||||
|
j = filename.lastIndexOf('\\')
|
||||||
|
}
|
||||||
|
if (j < 0) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
return filename.slice(0, j + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
tryAppendExtension(path, ext) {
|
||||||
|
return /(\.[a-z]*$)|([?;].*)$/.test(path) ? path : path + ext
|
||||||
|
}
|
||||||
|
|
||||||
|
tryAppendLessExtension(path) {
|
||||||
|
return this.tryAppendExtension(path, '.less')
|
||||||
|
}
|
||||||
|
|
||||||
|
supportsSync() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
alwaysMakePathsAbsolute() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
isPathAbsolute(filename) {
|
||||||
|
return /^(?:[a-z-]+:|\/|\\|#)/i.test(filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: pull out / replace?
|
||||||
|
join(basePath, laterPath) {
|
||||||
|
if (!basePath) {
|
||||||
|
return laterPath
|
||||||
|
}
|
||||||
|
return basePath + laterPath
|
||||||
|
}
|
||||||
|
|
||||||
|
pathDiff(url, baseUrl) {
|
||||||
|
// diff between two paths to create a relative path
|
||||||
|
|
||||||
|
const urlParts = this.extractUrlParts(url)
|
||||||
|
|
||||||
|
const baseUrlParts = this.extractUrlParts(baseUrl)
|
||||||
|
let i
|
||||||
|
let max
|
||||||
|
let urlDirectories
|
||||||
|
let baseUrlDirectories
|
||||||
|
let diff = ''
|
||||||
|
if (urlParts.hostPart !== baseUrlParts.hostPart) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
max = Math.max(baseUrlParts.directories.length, urlParts.directories.length)
|
||||||
|
for (i = 0; i < max; i++) {
|
||||||
|
if (baseUrlParts.directories[i] !== urlParts.directories[i]) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
baseUrlDirectories = baseUrlParts.directories.slice(i)
|
||||||
|
urlDirectories = urlParts.directories.slice(i)
|
||||||
|
for (i = 0; i < baseUrlDirectories.length - 1; i++) {
|
||||||
|
diff += '../'
|
||||||
|
}
|
||||||
|
for (i = 0; i < urlDirectories.length - 1; i++) {
|
||||||
|
diff += `${urlDirectories[i]}/`
|
||||||
|
}
|
||||||
|
return diff
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function, not part of API.
|
||||||
|
* This should be replaceable by newer Node / Browser APIs
|
||||||
|
*
|
||||||
|
* @param {string} url
|
||||||
|
* @param {string} baseUrl
|
||||||
|
*/
|
||||||
|
extractUrlParts(url, baseUrl) {
|
||||||
|
// urlParts[1] = protocol://hostname/ OR /
|
||||||
|
// urlParts[2] = / if path relative to host base
|
||||||
|
// urlParts[3] = directories
|
||||||
|
// urlParts[4] = filename
|
||||||
|
// urlParts[5] = parameters
|
||||||
|
|
||||||
|
const urlPartsRegex =
|
||||||
|
/^((?:[a-z-]+:)?\/{2}(?:[^/?#]*\/)|([/\\]))?((?:[^/\\?#]*[/\\])*)([^/\\?#]*)([#?].*)?$/i
|
||||||
|
|
||||||
|
const urlParts = url.match(urlPartsRegex)
|
||||||
|
const returner = {}
|
||||||
|
let rawDirectories = []
|
||||||
|
const directories = []
|
||||||
|
let i
|
||||||
|
let baseUrlParts
|
||||||
|
|
||||||
|
if (!urlParts) {
|
||||||
|
throw new Error(`Could not parse sheet href - '${url}'`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stylesheets in IE don't always return the full path
|
||||||
|
if (baseUrl && (!urlParts[1] || urlParts[2])) {
|
||||||
|
baseUrlParts = baseUrl.match(urlPartsRegex)
|
||||||
|
if (!baseUrlParts) {
|
||||||
|
throw new Error(`Could not parse page url - '${baseUrl}'`)
|
||||||
|
}
|
||||||
|
urlParts[1] = urlParts[1] || baseUrlParts[1] || ''
|
||||||
|
if (!urlParts[2]) {
|
||||||
|
urlParts[3] = baseUrlParts[3] + urlParts[3]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (urlParts[3]) {
|
||||||
|
rawDirectories = urlParts[3].replace(/\\/g, '/').split('/')
|
||||||
|
|
||||||
|
// collapse '..' and skip '.'
|
||||||
|
for (i = 0; i < rawDirectories.length; i++) {
|
||||||
|
if (rawDirectories[i] === '..') {
|
||||||
|
directories.pop()
|
||||||
|
} else if (rawDirectories[i] !== '.') {
|
||||||
|
directories.push(rawDirectories[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
returner.hostPart = urlParts[1]
|
||||||
|
returner.directories = directories
|
||||||
|
returner.rawPath = (urlParts[1] || '') + rawDirectories.join('/')
|
||||||
|
returner.path = (urlParts[1] || '') + directories.join('/')
|
||||||
|
returner.filename = urlParts[4]
|
||||||
|
returner.fileUrl = returner.path + (urlParts[4] || '')
|
||||||
|
returner.url = returner.fileUrl + (urlParts[5] || '')
|
||||||
|
return returner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AbstractFileManager
|
|
@ -0,0 +1,215 @@
|
||||||
|
import functionRegistry from '../functions/function-registry'
|
||||||
|
import LessError from '../less-error'
|
||||||
|
|
||||||
|
class AbstractPluginLoader {
|
||||||
|
constructor() {
|
||||||
|
// Implemented by Node.js plugin loader
|
||||||
|
this.require = function () {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
evalPlugin(contents, context, imports, pluginOptions, fileInfo) {
|
||||||
|
let loader,
|
||||||
|
registry,
|
||||||
|
pluginObj,
|
||||||
|
localModule,
|
||||||
|
pluginManager,
|
||||||
|
filename,
|
||||||
|
result
|
||||||
|
|
||||||
|
pluginManager = context.pluginManager
|
||||||
|
|
||||||
|
if (fileInfo) {
|
||||||
|
if (typeof fileInfo === 'string') {
|
||||||
|
filename = fileInfo
|
||||||
|
} else {
|
||||||
|
filename = fileInfo.filename
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const shortname = new this.less.FileManager().extractUrlParts(
|
||||||
|
filename
|
||||||
|
).filename
|
||||||
|
|
||||||
|
if (filename) {
|
||||||
|
pluginObj = pluginManager.get(filename)
|
||||||
|
|
||||||
|
if (pluginObj) {
|
||||||
|
result = this.trySetOptions(
|
||||||
|
pluginObj,
|
||||||
|
filename,
|
||||||
|
shortname,
|
||||||
|
pluginOptions
|
||||||
|
)
|
||||||
|
if (result) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (pluginObj.use) {
|
||||||
|
pluginObj.use.call(this.context, pluginObj)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
e.message = e.message || 'Error during @plugin call'
|
||||||
|
return new LessError(e, imports, filename)
|
||||||
|
}
|
||||||
|
return pluginObj
|
||||||
|
}
|
||||||
|
}
|
||||||
|
localModule = {
|
||||||
|
exports: {},
|
||||||
|
pluginManager,
|
||||||
|
fileInfo
|
||||||
|
}
|
||||||
|
registry = functionRegistry.create()
|
||||||
|
|
||||||
|
const registerPlugin = function (obj) {
|
||||||
|
pluginObj = obj
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
loader = new Function(
|
||||||
|
'module',
|
||||||
|
'require',
|
||||||
|
'registerPlugin',
|
||||||
|
'functions',
|
||||||
|
'tree',
|
||||||
|
'less',
|
||||||
|
'fileInfo',
|
||||||
|
contents
|
||||||
|
)
|
||||||
|
loader(
|
||||||
|
localModule,
|
||||||
|
this.require(filename),
|
||||||
|
registerPlugin,
|
||||||
|
registry,
|
||||||
|
this.less.tree,
|
||||||
|
this.less,
|
||||||
|
fileInfo
|
||||||
|
)
|
||||||
|
} catch (e) {
|
||||||
|
return new LessError(e, imports, filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pluginObj) {
|
||||||
|
pluginObj = localModule.exports
|
||||||
|
}
|
||||||
|
pluginObj = this.validatePlugin(pluginObj, filename, shortname)
|
||||||
|
|
||||||
|
if (pluginObj instanceof LessError) {
|
||||||
|
return pluginObj
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pluginObj) {
|
||||||
|
pluginObj.imports = imports
|
||||||
|
pluginObj.filename = filename
|
||||||
|
|
||||||
|
// For < 3.x (or unspecified minVersion) - setOptions() before install()
|
||||||
|
if (
|
||||||
|
!pluginObj.minVersion ||
|
||||||
|
this.compareVersion('3.0.0', pluginObj.minVersion) < 0
|
||||||
|
) {
|
||||||
|
result = this.trySetOptions(
|
||||||
|
pluginObj,
|
||||||
|
filename,
|
||||||
|
shortname,
|
||||||
|
pluginOptions
|
||||||
|
)
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run on first load
|
||||||
|
pluginManager.addPlugin(pluginObj, fileInfo.filename, registry)
|
||||||
|
pluginObj.functions = registry.getLocalFunctions()
|
||||||
|
|
||||||
|
// Need to call setOptions again because the pluginObj might have functions
|
||||||
|
result = this.trySetOptions(pluginObj, filename, shortname, pluginOptions)
|
||||||
|
if (result) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run every @plugin call
|
||||||
|
try {
|
||||||
|
if (pluginObj.use) {
|
||||||
|
pluginObj.use.call(this.context, pluginObj)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
e.message = e.message || 'Error during @plugin call'
|
||||||
|
return new LessError(e, imports, filename)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return new LessError({ message: 'Not a valid plugin' }, imports, filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pluginObj
|
||||||
|
}
|
||||||
|
|
||||||
|
trySetOptions(plugin, filename, name, options) {
|
||||||
|
if (options && !plugin.setOptions) {
|
||||||
|
return new LessError({
|
||||||
|
message: `Options have been provided but the plugin ${name} does not support any options.`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
plugin.setOptions && plugin.setOptions(options)
|
||||||
|
} catch (e) {
|
||||||
|
return new LessError(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
validatePlugin(plugin, filename, name) {
|
||||||
|
if (plugin) {
|
||||||
|
// support plugins being a function
|
||||||
|
// so that the plugin can be more usable programmatically
|
||||||
|
if (typeof plugin === 'function') {
|
||||||
|
plugin = new plugin()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plugin.minVersion) {
|
||||||
|
if (this.compareVersion(plugin.minVersion, this.less.version) < 0) {
|
||||||
|
return new LessError({
|
||||||
|
message: `Plugin ${name} requires version ${this.versionToString(
|
||||||
|
plugin.minVersion
|
||||||
|
)}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return plugin
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
compareVersion(aVersion, bVersion) {
|
||||||
|
if (typeof aVersion === 'string') {
|
||||||
|
aVersion = aVersion.match(/^(\d+)\.?(\d+)?\.?(\d+)?/)
|
||||||
|
aVersion.shift()
|
||||||
|
}
|
||||||
|
for (let i = 0; i < aVersion.length; i++) {
|
||||||
|
if (aVersion[i] !== bVersion[i]) {
|
||||||
|
return parseInt(aVersion[i]) > parseInt(bVersion[i]) ? -1 : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
versionToString(version) {
|
||||||
|
let versionString = ''
|
||||||
|
for (let i = 0; i < version.length; i++) {
|
||||||
|
versionString += (versionString ? '.' : '') + version[i]
|
||||||
|
}
|
||||||
|
return versionString
|
||||||
|
}
|
||||||
|
|
||||||
|
printUsage(plugins) {
|
||||||
|
for (let i = 0; i < plugins.length; i++) {
|
||||||
|
const plugin = plugins[i]
|
||||||
|
if (plugin.printUsage) {
|
||||||
|
plugin.printUsage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AbstractPluginLoader
|
|
@ -0,0 +1,21 @@
|
||||||
|
export interface Environment {
|
||||||
|
/**
|
||||||
|
* Converts a string to a base 64 string
|
||||||
|
*/
|
||||||
|
encodeBase64(str: string): string
|
||||||
|
/**
|
||||||
|
* Lookup the mime-type of a filename
|
||||||
|
*/
|
||||||
|
mimeLookup(filename: string): string
|
||||||
|
/**
|
||||||
|
* Look up the charset of a mime type
|
||||||
|
* @param mime
|
||||||
|
*/
|
||||||
|
charsetLookup(mime: string): string
|
||||||
|
/**
|
||||||
|
* Gets a source map generator
|
||||||
|
*
|
||||||
|
* @todo - Figure out precise type
|
||||||
|
*/
|
||||||
|
getSourceMapGenerator(): any
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
/**
|
||||||
|
* @todo Document why this abstraction exists, and the relationship between
|
||||||
|
* environment, file managers, and plugin manager
|
||||||
|
*/
|
||||||
|
|
||||||
|
import logger from '../logger'
|
||||||
|
|
||||||
|
class Environment {
|
||||||
|
constructor(externalEnvironment, fileManagers) {
|
||||||
|
this.fileManagers = fileManagers || []
|
||||||
|
externalEnvironment = externalEnvironment || {}
|
||||||
|
|
||||||
|
const optionalFunctions = [
|
||||||
|
'encodeBase64',
|
||||||
|
'mimeLookup',
|
||||||
|
'charsetLookup',
|
||||||
|
'getSourceMapGenerator'
|
||||||
|
]
|
||||||
|
const requiredFunctions = []
|
||||||
|
const functions = requiredFunctions.concat(optionalFunctions)
|
||||||
|
|
||||||
|
for (let i = 0; i < functions.length; i++) {
|
||||||
|
const propName = functions[i]
|
||||||
|
const environmentFunc = externalEnvironment[propName]
|
||||||
|
if (environmentFunc) {
|
||||||
|
this[propName] = environmentFunc.bind(externalEnvironment)
|
||||||
|
} else if (i < requiredFunctions.length) {
|
||||||
|
this.warn(`missing required function in environment - ${propName}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getFileManager(filename, currentDirectory, options, environment, isSync) {
|
||||||
|
if (!filename) {
|
||||||
|
logger.warn(
|
||||||
|
'getFileManager called with no filename.. Please report this issue. continuing.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (currentDirectory === undefined) {
|
||||||
|
logger.warn(
|
||||||
|
'getFileManager called with null directory.. Please report this issue. continuing.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let fileManagers = this.fileManagers
|
||||||
|
if (options.pluginManager) {
|
||||||
|
fileManagers = []
|
||||||
|
.concat(fileManagers)
|
||||||
|
.concat(options.pluginManager.getFileManagers())
|
||||||
|
}
|
||||||
|
for (let i = fileManagers.length - 1; i >= 0; i--) {
|
||||||
|
const fileManager = fileManagers[i]
|
||||||
|
if (
|
||||||
|
fileManager[isSync ? 'supportsSync' : 'supports'](
|
||||||
|
filename,
|
||||||
|
currentDirectory,
|
||||||
|
options,
|
||||||
|
environment
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return fileManager
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
addFileManager(fileManager) {
|
||||||
|
this.fileManagers.push(fileManager)
|
||||||
|
}
|
||||||
|
|
||||||
|
clearFileManagers() {
|
||||||
|
this.fileManagers = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Environment
|
|
@ -0,0 +1,77 @@
|
||||||
|
import type { Environment } from './environment-api'
|
||||||
|
|
||||||
|
export interface FileManager {
|
||||||
|
/**
|
||||||
|
* Given the full path to a file, return the path component
|
||||||
|
* Provided by AbstractFileManager
|
||||||
|
*/
|
||||||
|
getPath(filename: string): string
|
||||||
|
/**
|
||||||
|
* Append a .less extension if appropriate. Only called if less thinks one could be added.
|
||||||
|
* Provided by AbstractFileManager
|
||||||
|
*/
|
||||||
|
tryAppendLessExtension(filename: string): string
|
||||||
|
/**
|
||||||
|
* Whether the rootpath should be converted to be absolute.
|
||||||
|
* The browser ovverides this to return true because urls must be absolute.
|
||||||
|
* Provided by AbstractFileManager (returns false)
|
||||||
|
*/
|
||||||
|
alwaysMakePathsAbsolute(): boolean
|
||||||
|
/**
|
||||||
|
* Returns whether a path is absolute
|
||||||
|
* Provided by AbstractFileManager
|
||||||
|
*/
|
||||||
|
isPathAbsolute(path: string): boolean
|
||||||
|
/**
|
||||||
|
* joins together 2 paths
|
||||||
|
* Provided by AbstractFileManager
|
||||||
|
*/
|
||||||
|
join(basePath: string, laterPath: string): string
|
||||||
|
/**
|
||||||
|
* Returns the difference between 2 paths
|
||||||
|
* E.g. url = a/ baseUrl = a/b/ returns ../
|
||||||
|
* url = a/b/ baseUrl = a/ returns b/
|
||||||
|
* Provided by AbstractFileManager
|
||||||
|
*/
|
||||||
|
pathDiff(url: string, baseUrl: string): string
|
||||||
|
/**
|
||||||
|
* Returns whether this file manager supports this file for syncronous file retrieval
|
||||||
|
* If true is returned, loadFileSync will then be called with the file.
|
||||||
|
* Provided by AbstractFileManager (returns false)
|
||||||
|
*
|
||||||
|
* @todo - Narrow Options type
|
||||||
|
*/
|
||||||
|
supportsSync(
|
||||||
|
filename: string,
|
||||||
|
currentDirectory: string,
|
||||||
|
options: Record<string, any>,
|
||||||
|
environment: Environment
|
||||||
|
): boolean
|
||||||
|
/**
|
||||||
|
* If file manager supports async file retrieval for this file type
|
||||||
|
*/
|
||||||
|
supports(
|
||||||
|
filename: string,
|
||||||
|
currentDirectory: string,
|
||||||
|
options: Record<string, any>,
|
||||||
|
environment: Environment
|
||||||
|
): boolean
|
||||||
|
/**
|
||||||
|
* Loads a file asynchronously.
|
||||||
|
*/
|
||||||
|
loadFile(
|
||||||
|
filename: string,
|
||||||
|
currentDirectory: string,
|
||||||
|
options: Record<string, any>,
|
||||||
|
environment: Environment
|
||||||
|
): Promise<{ filename: string, contents: string }>
|
||||||
|
/**
|
||||||
|
* Loads a file synchronously. Expects an immediate return with an object
|
||||||
|
*/
|
||||||
|
loadFileSync(
|
||||||
|
filename: string,
|
||||||
|
currentDirectory: string,
|
||||||
|
options: Record<string, any>,
|
||||||
|
environment: Environment
|
||||||
|
): { error?: unknown, filename: string, contents: string }
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
import Anonymous from '../tree/anonymous'
|
||||||
|
import Keyword from '../tree/keyword'
|
||||||
|
|
||||||
|
function boolean(condition) {
|
||||||
|
return condition ? Keyword.True : Keyword.False
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Functions with evalArgs set to false are sent context
|
||||||
|
* as the first argument.
|
||||||
|
*/
|
||||||
|
function If(context, condition, trueValue, falseValue) {
|
||||||
|
return condition.eval(context)
|
||||||
|
? trueValue.eval(context)
|
||||||
|
: falseValue
|
||||||
|
? falseValue.eval(context)
|
||||||
|
: new Anonymous()
|
||||||
|
}
|
||||||
|
If.evalArgs = false
|
||||||
|
|
||||||
|
function isdefined(context, variable) {
|
||||||
|
try {
|
||||||
|
variable.eval(context)
|
||||||
|
return Keyword.True
|
||||||
|
} catch (e) {
|
||||||
|
return Keyword.False
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isdefined.evalArgs = false
|
||||||
|
|
||||||
|
export default { isdefined, boolean, if: If }
|
|
@ -0,0 +1,83 @@
|
||||||
|
import Color from '../tree/color'
|
||||||
|
|
||||||
|
// Color Blending
|
||||||
|
// ref: http://www.w3.org/TR/compositing-1
|
||||||
|
|
||||||
|
function colorBlend(mode, color1, color2) {
|
||||||
|
const ab = color1.alpha // result
|
||||||
|
|
||||||
|
let // backdrop
|
||||||
|
cb
|
||||||
|
|
||||||
|
const as = color2.alpha
|
||||||
|
|
||||||
|
let // source
|
||||||
|
cs
|
||||||
|
|
||||||
|
let ar
|
||||||
|
let cr
|
||||||
|
const r = []
|
||||||
|
|
||||||
|
ar = as + ab * (1 - as)
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
cb = color1.rgb[i] / 255
|
||||||
|
cs = color2.rgb[i] / 255
|
||||||
|
cr = mode(cb, cs)
|
||||||
|
if (ar) {
|
||||||
|
cr = (as * cs + ab * (cb - as * (cb + cs - cr))) / ar
|
||||||
|
}
|
||||||
|
r[i] = cr * 255
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Color(r, ar)
|
||||||
|
}
|
||||||
|
|
||||||
|
const colorBlendModeFunctions = {
|
||||||
|
multiply: function (cb, cs) {
|
||||||
|
return cb * cs
|
||||||
|
},
|
||||||
|
screen: function (cb, cs) {
|
||||||
|
return cb + cs - cb * cs
|
||||||
|
},
|
||||||
|
overlay: function (cb, cs) {
|
||||||
|
cb *= 2
|
||||||
|
return cb <= 1
|
||||||
|
? colorBlendModeFunctions.multiply(cb, cs)
|
||||||
|
: colorBlendModeFunctions.screen(cb - 1, cs)
|
||||||
|
},
|
||||||
|
softlight: function (cb, cs) {
|
||||||
|
let d = 1
|
||||||
|
let e = cb
|
||||||
|
if (cs > 0.5) {
|
||||||
|
e = 1
|
||||||
|
d = cb > 0.25 ? Math.sqrt(cb) : ((16 * cb - 12) * cb + 4) * cb
|
||||||
|
}
|
||||||
|
return cb - (1 - 2 * cs) * e * (d - cb)
|
||||||
|
},
|
||||||
|
hardlight: function (cb, cs) {
|
||||||
|
return colorBlendModeFunctions.overlay(cs, cb)
|
||||||
|
},
|
||||||
|
difference: function (cb, cs) {
|
||||||
|
return Math.abs(cb - cs)
|
||||||
|
},
|
||||||
|
exclusion: function (cb, cs) {
|
||||||
|
return cb + cs - 2 * cb * cs
|
||||||
|
},
|
||||||
|
|
||||||
|
// non-w3c functions:
|
||||||
|
average: function (cb, cs) {
|
||||||
|
return (cb + cs) / 2
|
||||||
|
},
|
||||||
|
negation: function (cb, cs) {
|
||||||
|
return 1 - Math.abs(cb + cs - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const f in colorBlendModeFunctions) {
|
||||||
|
// eslint-disable-next-line no-prototype-builtins
|
||||||
|
if (colorBlendModeFunctions.hasOwnProperty(f)) {
|
||||||
|
colorBlend[f] = colorBlend.bind(null, colorBlendModeFunctions[f])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default colorBlend
|
|
@ -0,0 +1,445 @@
|
||||||
|
import Dimension from '../tree/dimension'
|
||||||
|
import Color from '../tree/color'
|
||||||
|
import Quoted from '../tree/quoted'
|
||||||
|
import Anonymous from '../tree/anonymous'
|
||||||
|
import Expression from '../tree/expression'
|
||||||
|
import Operation from '../tree/operation'
|
||||||
|
let colorFunctions
|
||||||
|
|
||||||
|
function clamp(val) {
|
||||||
|
return Math.min(1, Math.max(0, val))
|
||||||
|
}
|
||||||
|
function hsla(origColor, hsl) {
|
||||||
|
const color = colorFunctions.hsla(hsl.h, hsl.s, hsl.l, hsl.a)
|
||||||
|
if (color) {
|
||||||
|
if (origColor.value && /^(rgb|hsl)/.test(origColor.value)) {
|
||||||
|
color.value = origColor.value
|
||||||
|
} else {
|
||||||
|
color.value = 'rgb'
|
||||||
|
}
|
||||||
|
return color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function toHSL(color) {
|
||||||
|
if (color.toHSL) {
|
||||||
|
return color.toHSL()
|
||||||
|
} else {
|
||||||
|
throw new Error('Argument cannot be evaluated to a color')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toHSV(color) {
|
||||||
|
if (color.toHSV) {
|
||||||
|
return color.toHSV()
|
||||||
|
} else {
|
||||||
|
throw new Error('Argument cannot be evaluated to a color')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function number(n) {
|
||||||
|
if (n instanceof Dimension) {
|
||||||
|
return parseFloat(n.unit.is('%') ? n.value / 100 : n.value)
|
||||||
|
} else if (typeof n === 'number') {
|
||||||
|
return n
|
||||||
|
} else {
|
||||||
|
throw {
|
||||||
|
type: 'Argument',
|
||||||
|
message: 'color functions take numbers as parameters'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function scaled(n, size) {
|
||||||
|
if (n instanceof Dimension && n.unit.is('%')) {
|
||||||
|
return parseFloat((n.value * size) / 100)
|
||||||
|
} else {
|
||||||
|
return number(n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
colorFunctions = {
|
||||||
|
rgb: function (r, g, b) {
|
||||||
|
let a = 1
|
||||||
|
/**
|
||||||
|
* Comma-less syntax
|
||||||
|
* e.g. rgb(0 128 255 / 50%)
|
||||||
|
*/
|
||||||
|
if (r instanceof Expression) {
|
||||||
|
const val = r.value
|
||||||
|
r = val[0]
|
||||||
|
g = val[1]
|
||||||
|
b = val[2]
|
||||||
|
/**
|
||||||
|
* @todo - should this be normalized in
|
||||||
|
* function caller? Or parsed differently?
|
||||||
|
*/
|
||||||
|
if (b instanceof Operation) {
|
||||||
|
const op = b
|
||||||
|
b = op.operands[0]
|
||||||
|
a = op.operands[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const color = colorFunctions.rgba(r, g, b, a)
|
||||||
|
if (color) {
|
||||||
|
color.value = 'rgb'
|
||||||
|
return color
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rgba: function (r, g, b, a) {
|
||||||
|
try {
|
||||||
|
if (r instanceof Color) {
|
||||||
|
if (g) {
|
||||||
|
a = number(g)
|
||||||
|
} else {
|
||||||
|
a = r.alpha
|
||||||
|
}
|
||||||
|
return new Color(r.rgb, a, 'rgba')
|
||||||
|
}
|
||||||
|
const rgb = [r, g, b].map(c => scaled(c, 255))
|
||||||
|
a = number(a)
|
||||||
|
return new Color(rgb, a, 'rgba')
|
||||||
|
} catch (e) {}
|
||||||
|
},
|
||||||
|
hsl: function (h, s, l) {
|
||||||
|
let a = 1
|
||||||
|
if (h instanceof Expression) {
|
||||||
|
const val = h.value
|
||||||
|
h = val[0]
|
||||||
|
s = val[1]
|
||||||
|
l = val[2]
|
||||||
|
|
||||||
|
if (l instanceof Operation) {
|
||||||
|
const op = l
|
||||||
|
l = op.operands[0]
|
||||||
|
a = op.operands[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const color = colorFunctions.hsla(h, s, l, a)
|
||||||
|
if (color) {
|
||||||
|
color.value = 'hsl'
|
||||||
|
return color
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hsla: function (h, s, l, a) {
|
||||||
|
let m1
|
||||||
|
let m2
|
||||||
|
|
||||||
|
function hue(h) {
|
||||||
|
h = h < 0 ? h + 1 : h > 1 ? h - 1 : h
|
||||||
|
if (h * 6 < 1) {
|
||||||
|
return m1 + (m2 - m1) * h * 6
|
||||||
|
} else if (h * 2 < 1) {
|
||||||
|
return m2
|
||||||
|
} else if (h * 3 < 2) {
|
||||||
|
return m1 + (m2 - m1) * (2 / 3 - h) * 6
|
||||||
|
} else {
|
||||||
|
return m1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (h instanceof Color) {
|
||||||
|
if (s) {
|
||||||
|
a = number(s)
|
||||||
|
} else {
|
||||||
|
a = h.alpha
|
||||||
|
}
|
||||||
|
return new Color(h.rgb, a, 'hsla')
|
||||||
|
}
|
||||||
|
|
||||||
|
h = (number(h) % 360) / 360
|
||||||
|
s = clamp(number(s))
|
||||||
|
l = clamp(number(l))
|
||||||
|
a = clamp(number(a))
|
||||||
|
|
||||||
|
m2 = l <= 0.5 ? l * (s + 1) : l + s - l * s
|
||||||
|
m1 = l * 2 - m2
|
||||||
|
|
||||||
|
const rgb = [hue(h + 1 / 3) * 255, hue(h) * 255, hue(h - 1 / 3) * 255]
|
||||||
|
a = number(a)
|
||||||
|
return new Color(rgb, a, 'hsla')
|
||||||
|
} catch (e) {}
|
||||||
|
},
|
||||||
|
|
||||||
|
hsv: function (h, s, v) {
|
||||||
|
return colorFunctions.hsva(h, s, v, 1.0)
|
||||||
|
},
|
||||||
|
|
||||||
|
hsva: function (h, s, v, a) {
|
||||||
|
h = ((number(h) % 360) / 360) * 360
|
||||||
|
s = number(s)
|
||||||
|
v = number(v)
|
||||||
|
a = number(a)
|
||||||
|
|
||||||
|
let i
|
||||||
|
let f
|
||||||
|
i = Math.floor((h / 60) % 6)
|
||||||
|
f = h / 60 - i
|
||||||
|
|
||||||
|
const vs = [v, v * (1 - s), v * (1 - f * s), v * (1 - (1 - f) * s)]
|
||||||
|
const perm = [
|
||||||
|
[0, 3, 1],
|
||||||
|
[2, 0, 1],
|
||||||
|
[1, 0, 3],
|
||||||
|
[1, 2, 0],
|
||||||
|
[3, 1, 0],
|
||||||
|
[0, 1, 2]
|
||||||
|
]
|
||||||
|
|
||||||
|
return colorFunctions.rgba(
|
||||||
|
vs[perm[i][0]] * 255,
|
||||||
|
vs[perm[i][1]] * 255,
|
||||||
|
vs[perm[i][2]] * 255,
|
||||||
|
a
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
hue: function (color) {
|
||||||
|
return new Dimension(toHSL(color).h)
|
||||||
|
},
|
||||||
|
saturation: function (color) {
|
||||||
|
return new Dimension(toHSL(color).s * 100, '%')
|
||||||
|
},
|
||||||
|
lightness: function (color) {
|
||||||
|
return new Dimension(toHSL(color).l * 100, '%')
|
||||||
|
},
|
||||||
|
hsvhue: function (color) {
|
||||||
|
return new Dimension(toHSV(color).h)
|
||||||
|
},
|
||||||
|
hsvsaturation: function (color) {
|
||||||
|
return new Dimension(toHSV(color).s * 100, '%')
|
||||||
|
},
|
||||||
|
hsvvalue: function (color) {
|
||||||
|
return new Dimension(toHSV(color).v * 100, '%')
|
||||||
|
},
|
||||||
|
red: function (color) {
|
||||||
|
return new Dimension(color.rgb[0])
|
||||||
|
},
|
||||||
|
green: function (color) {
|
||||||
|
return new Dimension(color.rgb[1])
|
||||||
|
},
|
||||||
|
blue: function (color) {
|
||||||
|
return new Dimension(color.rgb[2])
|
||||||
|
},
|
||||||
|
alpha: function (color) {
|
||||||
|
return new Dimension(toHSL(color).a)
|
||||||
|
},
|
||||||
|
luma: function (color) {
|
||||||
|
return new Dimension(color.luma() * color.alpha * 100, '%')
|
||||||
|
},
|
||||||
|
luminance: function (color) {
|
||||||
|
const luminance =
|
||||||
|
(0.2126 * color.rgb[0]) / 255 +
|
||||||
|
(0.7152 * color.rgb[1]) / 255 +
|
||||||
|
(0.0722 * color.rgb[2]) / 255
|
||||||
|
|
||||||
|
return new Dimension(luminance * color.alpha * 100, '%')
|
||||||
|
},
|
||||||
|
saturate: function (color, amount, method) {
|
||||||
|
// filter: saturate(3.2);
|
||||||
|
// should be kept as is, so check for color
|
||||||
|
if (!color.rgb) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const hsl = toHSL(color)
|
||||||
|
|
||||||
|
if (typeof method !== 'undefined' && method.value === 'relative') {
|
||||||
|
hsl.s += (hsl.s * amount.value) / 100
|
||||||
|
} else {
|
||||||
|
hsl.s += amount.value / 100
|
||||||
|
}
|
||||||
|
hsl.s = clamp(hsl.s)
|
||||||
|
return hsla(color, hsl)
|
||||||
|
},
|
||||||
|
desaturate: function (color, amount, method) {
|
||||||
|
const hsl = toHSL(color)
|
||||||
|
|
||||||
|
if (typeof method !== 'undefined' && method.value === 'relative') {
|
||||||
|
hsl.s -= (hsl.s * amount.value) / 100
|
||||||
|
} else {
|
||||||
|
hsl.s -= amount.value / 100
|
||||||
|
}
|
||||||
|
hsl.s = clamp(hsl.s)
|
||||||
|
return hsla(color, hsl)
|
||||||
|
},
|
||||||
|
lighten: function (color, amount, method) {
|
||||||
|
const hsl = toHSL(color)
|
||||||
|
|
||||||
|
if (typeof method !== 'undefined' && method.value === 'relative') {
|
||||||
|
hsl.l += (hsl.l * amount.value) / 100
|
||||||
|
} else {
|
||||||
|
hsl.l += amount.value / 100
|
||||||
|
}
|
||||||
|
hsl.l = clamp(hsl.l)
|
||||||
|
return hsla(color, hsl)
|
||||||
|
},
|
||||||
|
darken: function (color, amount, method) {
|
||||||
|
const hsl = toHSL(color)
|
||||||
|
|
||||||
|
if (typeof method !== 'undefined' && method.value === 'relative') {
|
||||||
|
hsl.l -= (hsl.l * amount.value) / 100
|
||||||
|
} else {
|
||||||
|
hsl.l -= amount.value / 100
|
||||||
|
}
|
||||||
|
hsl.l = clamp(hsl.l)
|
||||||
|
return hsla(color, hsl)
|
||||||
|
},
|
||||||
|
fadein: function (color, amount, method) {
|
||||||
|
const hsl = toHSL(color)
|
||||||
|
|
||||||
|
if (typeof method !== 'undefined' && method.value === 'relative') {
|
||||||
|
hsl.a += (hsl.a * amount.value) / 100
|
||||||
|
} else {
|
||||||
|
hsl.a += amount.value / 100
|
||||||
|
}
|
||||||
|
hsl.a = clamp(hsl.a)
|
||||||
|
return hsla(color, hsl)
|
||||||
|
},
|
||||||
|
fadeout: function (color, amount, method) {
|
||||||
|
const hsl = toHSL(color)
|
||||||
|
|
||||||
|
if (typeof method !== 'undefined' && method.value === 'relative') {
|
||||||
|
hsl.a -= (hsl.a * amount.value) / 100
|
||||||
|
} else {
|
||||||
|
hsl.a -= amount.value / 100
|
||||||
|
}
|
||||||
|
hsl.a = clamp(hsl.a)
|
||||||
|
return hsla(color, hsl)
|
||||||
|
},
|
||||||
|
fade: function (color, amount) {
|
||||||
|
const hsl = toHSL(color)
|
||||||
|
|
||||||
|
hsl.a = amount.value / 100
|
||||||
|
hsl.a = clamp(hsl.a)
|
||||||
|
return hsla(color, hsl)
|
||||||
|
},
|
||||||
|
spin: function (color, amount) {
|
||||||
|
const hsl = toHSL(color)
|
||||||
|
const hue = (hsl.h + amount.value) % 360
|
||||||
|
|
||||||
|
hsl.h = hue < 0 ? 360 + hue : hue
|
||||||
|
|
||||||
|
return hsla(color, hsl)
|
||||||
|
},
|
||||||
|
//
|
||||||
|
// Copyright (c) 2006-2009 Hampton Catlin, Natalie Weizenbaum, and Chris Eppstein
|
||||||
|
// http://sass-lang.com
|
||||||
|
//
|
||||||
|
mix: function (color1, color2, weight) {
|
||||||
|
if (!weight) {
|
||||||
|
weight = new Dimension(50)
|
||||||
|
}
|
||||||
|
const p = weight.value / 100.0
|
||||||
|
const w = p * 2 - 1
|
||||||
|
const a = toHSL(color1).a - toHSL(color2).a
|
||||||
|
|
||||||
|
const w1 = ((w * a == -1 ? w : (w + a) / (1 + w * a)) + 1) / 2.0
|
||||||
|
const w2 = 1 - w1
|
||||||
|
|
||||||
|
const rgb = [
|
||||||
|
color1.rgb[0] * w1 + color2.rgb[0] * w2,
|
||||||
|
color1.rgb[1] * w1 + color2.rgb[1] * w2,
|
||||||
|
color1.rgb[2] * w1 + color2.rgb[2] * w2
|
||||||
|
]
|
||||||
|
|
||||||
|
const alpha = color1.alpha * p + color2.alpha * (1 - p)
|
||||||
|
|
||||||
|
return new Color(rgb, alpha)
|
||||||
|
},
|
||||||
|
greyscale: function (color) {
|
||||||
|
return colorFunctions.desaturate(color, new Dimension(100))
|
||||||
|
},
|
||||||
|
contrast: function (color, dark, light, threshold) {
|
||||||
|
// filter: contrast(3.2);
|
||||||
|
// should be kept as is, so check for color
|
||||||
|
if (!color.rgb) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (typeof light === 'undefined') {
|
||||||
|
light = colorFunctions.rgba(255, 255, 255, 1.0)
|
||||||
|
}
|
||||||
|
if (typeof dark === 'undefined') {
|
||||||
|
dark = colorFunctions.rgba(0, 0, 0, 1.0)
|
||||||
|
}
|
||||||
|
// Figure out which is actually light and dark:
|
||||||
|
if (dark.luma() > light.luma()) {
|
||||||
|
const t = light
|
||||||
|
light = dark
|
||||||
|
dark = t
|
||||||
|
}
|
||||||
|
if (typeof threshold === 'undefined') {
|
||||||
|
threshold = 0.43
|
||||||
|
} else {
|
||||||
|
threshold = number(threshold)
|
||||||
|
}
|
||||||
|
if (color.luma() < threshold) {
|
||||||
|
return light
|
||||||
|
} else {
|
||||||
|
return dark
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Changes made in 2.7.0 - Reverted in 3.0.0
|
||||||
|
// contrast: function (color, color1, color2, threshold) {
|
||||||
|
// // Return which of `color1` and `color2` has the greatest contrast with `color`
|
||||||
|
// // according to the standard WCAG contrast ratio calculation.
|
||||||
|
// // http://www.w3.org/TR/WCAG20/#contrast-ratiodef
|
||||||
|
// // The threshold param is no longer used, in line with SASS.
|
||||||
|
// // filter: contrast(3.2);
|
||||||
|
// // should be kept as is, so check for color
|
||||||
|
// if (!color.rgb) {
|
||||||
|
// return null;
|
||||||
|
// }
|
||||||
|
// if (typeof color1 === 'undefined') {
|
||||||
|
// color1 = colorFunctions.rgba(0, 0, 0, 1.0);
|
||||||
|
// }
|
||||||
|
// if (typeof color2 === 'undefined') {
|
||||||
|
// color2 = colorFunctions.rgba(255, 255, 255, 1.0);
|
||||||
|
// }
|
||||||
|
// var contrast1, contrast2;
|
||||||
|
// var luma = color.luma();
|
||||||
|
// var luma1 = color1.luma();
|
||||||
|
// var luma2 = color2.luma();
|
||||||
|
// // Calculate contrast ratios for each color
|
||||||
|
// if (luma > luma1) {
|
||||||
|
// contrast1 = (luma + 0.05) / (luma1 + 0.05);
|
||||||
|
// } else {
|
||||||
|
// contrast1 = (luma1 + 0.05) / (luma + 0.05);
|
||||||
|
// }
|
||||||
|
// if (luma > luma2) {
|
||||||
|
// contrast2 = (luma + 0.05) / (luma2 + 0.05);
|
||||||
|
// } else {
|
||||||
|
// contrast2 = (luma2 + 0.05) / (luma + 0.05);
|
||||||
|
// }
|
||||||
|
// if (contrast1 > contrast2) {
|
||||||
|
// return color1;
|
||||||
|
// } else {
|
||||||
|
// return color2;
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
argb: function (color) {
|
||||||
|
return new Anonymous(color.toARGB())
|
||||||
|
},
|
||||||
|
color: function (c) {
|
||||||
|
if (
|
||||||
|
c instanceof Quoted &&
|
||||||
|
/^#([A-Fa-f0-9]{8}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{3,4})$/i.test(c.value)
|
||||||
|
) {
|
||||||
|
const val = c.value.slice(1)
|
||||||
|
return new Color(val, undefined, `#${val}`)
|
||||||
|
}
|
||||||
|
if (c instanceof Color || (c = Color.fromKeyword(c.value))) {
|
||||||
|
c.value = undefined
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
throw {
|
||||||
|
type: 'Argument',
|
||||||
|
message: 'argument must be a color keyword or 3|4|6|8 digit hex e.g. #FFF'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tint: function (color, amount) {
|
||||||
|
return colorFunctions.mix(colorFunctions.rgb(255, 255, 255), color, amount)
|
||||||
|
},
|
||||||
|
shade: function (color, amount) {
|
||||||
|
return colorFunctions.mix(colorFunctions.rgb(0, 0, 0), color, amount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default colorFunctions
|
|
@ -0,0 +1,95 @@
|
||||||
|
import Quoted from '../tree/quoted'
|
||||||
|
import URL from '../tree/url'
|
||||||
|
import * as utils from '../utils'
|
||||||
|
import logger from '../logger'
|
||||||
|
|
||||||
|
export default environment => {
|
||||||
|
const fallback = (functionThis, node) =>
|
||||||
|
new URL(node, functionThis.index, functionThis.currentFileInfo).eval(
|
||||||
|
functionThis.context
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'data-uri': function (mimetypeNode, filePathNode) {
|
||||||
|
if (!filePathNode) {
|
||||||
|
filePathNode = mimetypeNode
|
||||||
|
mimetypeNode = null
|
||||||
|
}
|
||||||
|
|
||||||
|
let mimetype = mimetypeNode && mimetypeNode.value
|
||||||
|
let filePath = filePathNode.value
|
||||||
|
const currentFileInfo = this.currentFileInfo
|
||||||
|
const currentDirectory = currentFileInfo.rewriteUrls
|
||||||
|
? currentFileInfo.currentDirectory
|
||||||
|
: currentFileInfo.entryPath
|
||||||
|
|
||||||
|
const fragmentStart = filePath.indexOf('#')
|
||||||
|
let fragment = ''
|
||||||
|
if (fragmentStart !== -1) {
|
||||||
|
fragment = filePath.slice(fragmentStart)
|
||||||
|
filePath = filePath.slice(0, fragmentStart)
|
||||||
|
}
|
||||||
|
const context = utils.clone(this.context)
|
||||||
|
context.rawBuffer = true
|
||||||
|
|
||||||
|
const fileManager = environment.getFileManager(
|
||||||
|
filePath,
|
||||||
|
currentDirectory,
|
||||||
|
context,
|
||||||
|
environment,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!fileManager) {
|
||||||
|
return fallback(this, filePathNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
let useBase64 = false
|
||||||
|
|
||||||
|
// detect the mimetype if not given
|
||||||
|
if (!mimetypeNode) {
|
||||||
|
mimetype = environment.mimeLookup(filePath)
|
||||||
|
|
||||||
|
if (mimetype === 'image/svg+xml') {
|
||||||
|
useBase64 = false
|
||||||
|
} else {
|
||||||
|
// use base 64 unless it's an ASCII or UTF-8 format
|
||||||
|
const charset = environment.charsetLookup(mimetype)
|
||||||
|
useBase64 = ['US-ASCII', 'UTF-8'].indexOf(charset) < 0
|
||||||
|
}
|
||||||
|
if (useBase64) {
|
||||||
|
mimetype += ';base64'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
useBase64 = /;base64$/.test(mimetype)
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileSync = fileManager.loadFileSync(
|
||||||
|
filePath,
|
||||||
|
currentDirectory,
|
||||||
|
context,
|
||||||
|
environment
|
||||||
|
)
|
||||||
|
if (!fileSync.contents) {
|
||||||
|
logger.warn(
|
||||||
|
`Skipped data-uri embedding of ${filePath} because file not found`
|
||||||
|
)
|
||||||
|
return fallback(this, filePathNode || mimetypeNode)
|
||||||
|
}
|
||||||
|
let buf = fileSync.contents
|
||||||
|
if (useBase64 && !environment.encodeBase64) {
|
||||||
|
return fallback(this, filePathNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf = useBase64 ? environment.encodeBase64(buf) : encodeURIComponent(buf)
|
||||||
|
|
||||||
|
const uri = `data:${mimetype},${buf}${fragment}`
|
||||||
|
|
||||||
|
return new URL(
|
||||||
|
new Quoted(`"${uri}"`, uri, false, this.index, this.currentFileInfo),
|
||||||
|
this.index,
|
||||||
|
this.currentFileInfo
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
import Keyword from '../tree/keyword'
|
||||||
|
import * as utils from '../utils'
|
||||||
|
|
||||||
|
const defaultFunc = {
|
||||||
|
eval: function () {
|
||||||
|
const v = this.value_
|
||||||
|
const e = this.error_
|
||||||
|
if (e) {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
if (!utils.isNullOrUndefined(v)) {
|
||||||
|
return v ? Keyword.True : Keyword.False
|
||||||
|
}
|
||||||
|
},
|
||||||
|
value: function (v) {
|
||||||
|
this.value_ = v
|
||||||
|
},
|
||||||
|
error: function (e) {
|
||||||
|
this.error_ = e
|
||||||
|
},
|
||||||
|
reset: function () {
|
||||||
|
this.value_ = this.error_ = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defaultFunc
|
|
@ -0,0 +1,53 @@
|
||||||
|
import Expression from '../tree/expression'
|
||||||
|
|
||||||
|
class functionCaller {
|
||||||
|
constructor(name, context, index, currentFileInfo) {
|
||||||
|
this.name = name.toLowerCase()
|
||||||
|
this.index = index
|
||||||
|
this.context = context
|
||||||
|
this.currentFileInfo = currentFileInfo
|
||||||
|
|
||||||
|
this.func = context.frames[0].functionRegistry.get(this.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
isValid() {
|
||||||
|
return Boolean(this.func)
|
||||||
|
}
|
||||||
|
|
||||||
|
call(args) {
|
||||||
|
if (!Array.isArray(args)) {
|
||||||
|
args = [args]
|
||||||
|
}
|
||||||
|
const evalArgs = this.func.evalArgs
|
||||||
|
if (evalArgs !== false) {
|
||||||
|
args = args.map(a => a.eval(this.context))
|
||||||
|
}
|
||||||
|
const commentFilter = item => !(item.type === 'Comment')
|
||||||
|
|
||||||
|
// This code is terrible and should be replaced as per this issue...
|
||||||
|
// https://github.com/less/less.js/issues/2477
|
||||||
|
args = args.filter(commentFilter).map(item => {
|
||||||
|
if (item.type === 'Expression') {
|
||||||
|
const subNodes = item.value.filter(commentFilter)
|
||||||
|
if (subNodes.length === 1) {
|
||||||
|
// https://github.com/less/less.js/issues/3616
|
||||||
|
if (item.parens && subNodes[0].op === '/') {
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
return subNodes[0]
|
||||||
|
} else {
|
||||||
|
return new Expression(subNodes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
})
|
||||||
|
|
||||||
|
if (evalArgs === false) {
|
||||||
|
return this.func(this.context, ...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.func(...args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default functionCaller
|
|
@ -0,0 +1,35 @@
|
||||||
|
function makeRegistry(base) {
|
||||||
|
return {
|
||||||
|
_data: {},
|
||||||
|
add: function (name, func) {
|
||||||
|
// precautionary case conversion, as later querying of
|
||||||
|
// the registry by function-caller uses lower case as well.
|
||||||
|
name = name.toLowerCase()
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-prototype-builtins
|
||||||
|
if (this._data.hasOwnProperty(name)) {
|
||||||
|
// TODO warn
|
||||||
|
}
|
||||||
|
this._data[name] = func
|
||||||
|
},
|
||||||
|
addMultiple: function (functions) {
|
||||||
|
Object.keys(functions).forEach(name => {
|
||||||
|
this.add(name, functions[name])
|
||||||
|
})
|
||||||
|
},
|
||||||
|
get: function (name) {
|
||||||
|
return this._data[name] || (base && base.get(name))
|
||||||
|
},
|
||||||
|
getLocalFunctions: function () {
|
||||||
|
return this._data
|
||||||
|
},
|
||||||
|
inherit: function () {
|
||||||
|
return makeRegistry(this)
|
||||||
|
},
|
||||||
|
create: function (base) {
|
||||||
|
return makeRegistry(base)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default makeRegistry(null)
|
|
@ -0,0 +1,33 @@
|
||||||
|
import functionRegistry from './function-registry'
|
||||||
|
import functionCaller from './function-caller'
|
||||||
|
|
||||||
|
import boolean from './boolean'
|
||||||
|
import defaultFunc from './default'
|
||||||
|
import color from './color'
|
||||||
|
import colorBlending from './color-blending'
|
||||||
|
import dataUri from './data-uri'
|
||||||
|
import list from './list'
|
||||||
|
import math from './math'
|
||||||
|
import number from './number'
|
||||||
|
import string from './string'
|
||||||
|
import svg from './svg'
|
||||||
|
import types from './types'
|
||||||
|
|
||||||
|
export default environment => {
|
||||||
|
const functions = { functionRegistry, functionCaller }
|
||||||
|
|
||||||
|
// register functions
|
||||||
|
functionRegistry.addMultiple(boolean)
|
||||||
|
functionRegistry.add('default', defaultFunc.eval.bind(defaultFunc))
|
||||||
|
functionRegistry.addMultiple(color)
|
||||||
|
functionRegistry.addMultiple(colorBlending)
|
||||||
|
functionRegistry.addMultiple(dataUri(environment))
|
||||||
|
functionRegistry.addMultiple(list)
|
||||||
|
functionRegistry.addMultiple(math)
|
||||||
|
functionRegistry.addMultiple(number)
|
||||||
|
functionRegistry.addMultiple(string)
|
||||||
|
functionRegistry.addMultiple(svg(environment))
|
||||||
|
functionRegistry.addMultiple(types)
|
||||||
|
|
||||||
|
return functions
|
||||||
|
}
|
|
@ -0,0 +1,181 @@
|
||||||
|
import Comment from '../tree/comment'
|
||||||
|
import Node from '../tree/node'
|
||||||
|
import Dimension from '../tree/dimension'
|
||||||
|
import Declaration from '../tree/declaration'
|
||||||
|
import Expression from '../tree/expression'
|
||||||
|
import Ruleset from '../tree/ruleset'
|
||||||
|
import Selector from '../tree/selector'
|
||||||
|
import Element from '../tree/element'
|
||||||
|
import Quote from '../tree/quoted'
|
||||||
|
import Value from '../tree/value'
|
||||||
|
|
||||||
|
const getItemsFromNode = node => {
|
||||||
|
// handle non-array values as an array of length 1
|
||||||
|
// return 'undefined' if index is invalid
|
||||||
|
const items = Array.isArray(node.value) ? node.value : Array(node)
|
||||||
|
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
_SELF: function (n) {
|
||||||
|
return n
|
||||||
|
},
|
||||||
|
'~': function (...expr) {
|
||||||
|
if (expr.length === 1) {
|
||||||
|
return expr[0]
|
||||||
|
}
|
||||||
|
return new Value(expr)
|
||||||
|
},
|
||||||
|
extract: function (values, index) {
|
||||||
|
// (1-based index)
|
||||||
|
index = index.value - 1
|
||||||
|
|
||||||
|
return getItemsFromNode(values)[index]
|
||||||
|
},
|
||||||
|
length: function (values) {
|
||||||
|
return new Dimension(getItemsFromNode(values).length)
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Creates a Less list of incremental values.
|
||||||
|
* Modeled after Lodash's range function, also exists natively in PHP
|
||||||
|
*
|
||||||
|
* @param {Dimension} [start=1]
|
||||||
|
* @param {Dimension} end - e.g. 10 or 10px - unit is added to output
|
||||||
|
* @param {Dimension} [step=1]
|
||||||
|
*/
|
||||||
|
range: function (start, end, step) {
|
||||||
|
let from
|
||||||
|
let to
|
||||||
|
let stepValue = 1
|
||||||
|
const list = []
|
||||||
|
if (end) {
|
||||||
|
to = end
|
||||||
|
from = start.value
|
||||||
|
if (step) {
|
||||||
|
stepValue = step.value
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
from = 1
|
||||||
|
to = start
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = from; i <= to.value; i += stepValue) {
|
||||||
|
list.push(new Dimension(i, to.unit))
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Expression(list)
|
||||||
|
},
|
||||||
|
each: function (list, rs) {
|
||||||
|
const rules = []
|
||||||
|
let newRules
|
||||||
|
let iterator
|
||||||
|
|
||||||
|
const tryEval = val => {
|
||||||
|
if (val instanceof Node) {
|
||||||
|
return val.eval(this.context)
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
if (list.value && !(list instanceof Quote)) {
|
||||||
|
if (Array.isArray(list.value)) {
|
||||||
|
iterator = list.value.map(tryEval)
|
||||||
|
} else {
|
||||||
|
iterator = [tryEval(list.value)]
|
||||||
|
}
|
||||||
|
} else if (list.ruleset) {
|
||||||
|
iterator = tryEval(list.ruleset).rules
|
||||||
|
} else if (list.rules) {
|
||||||
|
iterator = list.rules.map(tryEval)
|
||||||
|
} else if (Array.isArray(list)) {
|
||||||
|
iterator = list.map(tryEval)
|
||||||
|
} else {
|
||||||
|
iterator = [tryEval(list)]
|
||||||
|
}
|
||||||
|
|
||||||
|
let valueName = '@value'
|
||||||
|
let keyName = '@key'
|
||||||
|
let indexName = '@index'
|
||||||
|
|
||||||
|
if (rs.params) {
|
||||||
|
valueName = rs.params[0] && rs.params[0].name
|
||||||
|
keyName = rs.params[1] && rs.params[1].name
|
||||||
|
indexName = rs.params[2] && rs.params[2].name
|
||||||
|
rs = rs.rules
|
||||||
|
} else {
|
||||||
|
rs = rs.ruleset
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < iterator.length; i++) {
|
||||||
|
let key
|
||||||
|
let value
|
||||||
|
const item = iterator[i]
|
||||||
|
if (item instanceof Declaration) {
|
||||||
|
key = typeof item.name === 'string' ? item.name : item.name[0].value
|
||||||
|
value = item.value
|
||||||
|
} else {
|
||||||
|
key = new Dimension(i + 1)
|
||||||
|
value = item
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item instanceof Comment) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
newRules = rs.rules.slice(0)
|
||||||
|
if (valueName) {
|
||||||
|
newRules.push(
|
||||||
|
new Declaration(
|
||||||
|
valueName,
|
||||||
|
value,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
this.index,
|
||||||
|
this.currentFileInfo
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (indexName) {
|
||||||
|
newRules.push(
|
||||||
|
new Declaration(
|
||||||
|
indexName,
|
||||||
|
new Dimension(i + 1),
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
this.index,
|
||||||
|
this.currentFileInfo
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (keyName) {
|
||||||
|
newRules.push(
|
||||||
|
new Declaration(
|
||||||
|
keyName,
|
||||||
|
key,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
this.index,
|
||||||
|
this.currentFileInfo
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
rules.push(
|
||||||
|
new Ruleset(
|
||||||
|
[new Selector([new Element('', '&')])],
|
||||||
|
newRules,
|
||||||
|
rs.strictImports,
|
||||||
|
rs.visibilityInfo()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Ruleset(
|
||||||
|
[new Selector([new Element('', '&')])],
|
||||||
|
rules,
|
||||||
|
rs.strictImports,
|
||||||
|
rs.visibilityInfo()
|
||||||
|
).eval(this.context)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
import Dimension from '../tree/dimension'
|
||||||
|
|
||||||
|
const MathHelper = (fn, unit, n) => {
|
||||||
|
if (!(n instanceof Dimension)) {
|
||||||
|
throw { type: 'Argument', message: 'argument must be a number' }
|
||||||
|
}
|
||||||
|
if (unit === null) {
|
||||||
|
unit = n.unit
|
||||||
|
} else {
|
||||||
|
n = n.unify()
|
||||||
|
}
|
||||||
|
return new Dimension(fn(parseFloat(n.value)), unit)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MathHelper
|
|
@ -0,0 +1,29 @@
|
||||||
|
import mathHelper from './math-helper.js'
|
||||||
|
|
||||||
|
const mathFunctions = {
|
||||||
|
// name, unit
|
||||||
|
ceil: null,
|
||||||
|
floor: null,
|
||||||
|
sqrt: null,
|
||||||
|
abs: null,
|
||||||
|
tan: '',
|
||||||
|
sin: '',
|
||||||
|
cos: '',
|
||||||
|
atan: 'rad',
|
||||||
|
asin: 'rad',
|
||||||
|
acos: 'rad'
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const f in mathFunctions) {
|
||||||
|
// eslint-disable-next-line no-prototype-builtins
|
||||||
|
if (mathFunctions.hasOwnProperty(f)) {
|
||||||
|
mathFunctions[f] = mathHelper.bind(null, Math[f], mathFunctions[f])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mathFunctions.round = (n, f) => {
|
||||||
|
const fraction = typeof f === 'undefined' ? 0 : f.value
|
||||||
|
return mathHelper(num => num.toFixed(fraction), null, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default mathFunctions
|
|
@ -0,0 +1,122 @@
|
||||||
|
import Dimension from '../tree/dimension'
|
||||||
|
import Anonymous from '../tree/anonymous'
|
||||||
|
import mathHelper from './math-helper.js'
|
||||||
|
|
||||||
|
const minMax = function (isMin, args) {
|
||||||
|
args = Array.prototype.slice.call(args)
|
||||||
|
switch (args.length) {
|
||||||
|
case 0:
|
||||||
|
throw { type: 'Argument', message: 'one or more arguments required' }
|
||||||
|
}
|
||||||
|
let i // key is the unit.toString() for unified Dimension values,
|
||||||
|
let j
|
||||||
|
let current
|
||||||
|
let currentUnified
|
||||||
|
let referenceUnified
|
||||||
|
let unit
|
||||||
|
let unitStatic
|
||||||
|
let unitClone
|
||||||
|
|
||||||
|
const // elems only contains original argument values.
|
||||||
|
order = []
|
||||||
|
|
||||||
|
const values = {}
|
||||||
|
// value is the index into the order array.
|
||||||
|
for (i = 0; i < args.length; i++) {
|
||||||
|
current = args[i]
|
||||||
|
if (!(current instanceof Dimension)) {
|
||||||
|
if (Array.isArray(args[i].value)) {
|
||||||
|
Array.prototype.push.apply(
|
||||||
|
args,
|
||||||
|
Array.prototype.slice.call(args[i].value)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
currentUnified =
|
||||||
|
current.unit.toString() === '' && unitClone !== undefined
|
||||||
|
? new Dimension(current.value, unitClone).unify()
|
||||||
|
: current.unify()
|
||||||
|
unit =
|
||||||
|
currentUnified.unit.toString() === '' && unitStatic !== undefined
|
||||||
|
? unitStatic
|
||||||
|
: currentUnified.unit.toString()
|
||||||
|
unitStatic =
|
||||||
|
(unit !== '' && unitStatic === undefined) ||
|
||||||
|
(unit !== '' && order[0].unify().unit.toString() === '')
|
||||||
|
? unit
|
||||||
|
: unitStatic
|
||||||
|
unitClone =
|
||||||
|
unit !== '' && unitClone === undefined
|
||||||
|
? current.unit.toString()
|
||||||
|
: unitClone
|
||||||
|
j =
|
||||||
|
values[''] !== undefined && unit !== '' && unit === unitStatic
|
||||||
|
? values['']
|
||||||
|
: values[unit]
|
||||||
|
if (j === undefined) {
|
||||||
|
if (unitStatic !== undefined && unit !== unitStatic) {
|
||||||
|
throw { type: 'Argument', message: 'incompatible types' }
|
||||||
|
}
|
||||||
|
values[unit] = order.length
|
||||||
|
order.push(current)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
referenceUnified =
|
||||||
|
order[j].unit.toString() === '' && unitClone !== undefined
|
||||||
|
? new Dimension(order[j].value, unitClone).unify()
|
||||||
|
: order[j].unify()
|
||||||
|
if (
|
||||||
|
(isMin && currentUnified.value < referenceUnified.value) ||
|
||||||
|
(!isMin && currentUnified.value > referenceUnified.value)
|
||||||
|
) {
|
||||||
|
order[j] = current
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (order.length == 1) {
|
||||||
|
return order[0]
|
||||||
|
}
|
||||||
|
args = order
|
||||||
|
.map(a => {
|
||||||
|
return a.toCSS(this.context)
|
||||||
|
})
|
||||||
|
.join(this.context.compress ? ',' : ', ')
|
||||||
|
return new Anonymous(`${isMin ? 'min' : 'max'}(${args})`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
min: function (...args) {
|
||||||
|
try {
|
||||||
|
return minMax.call(this, true, args)
|
||||||
|
} catch (e) {}
|
||||||
|
},
|
||||||
|
max: function (...args) {
|
||||||
|
try {
|
||||||
|
return minMax.call(this, false, args)
|
||||||
|
} catch (e) {}
|
||||||
|
},
|
||||||
|
convert: function (val, unit) {
|
||||||
|
return val.convertTo(unit.value)
|
||||||
|
},
|
||||||
|
pi: function () {
|
||||||
|
return new Dimension(Math.PI)
|
||||||
|
},
|
||||||
|
mod: function (a, b) {
|
||||||
|
return new Dimension(a.value % b.value, a.unit)
|
||||||
|
},
|
||||||
|
pow: function (x, y) {
|
||||||
|
if (typeof x === 'number' && typeof y === 'number') {
|
||||||
|
x = new Dimension(x)
|
||||||
|
y = new Dimension(y)
|
||||||
|
} else if (!(x instanceof Dimension) || !(y instanceof Dimension)) {
|
||||||
|
throw { type: 'Argument', message: 'arguments must be numbers' }
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Dimension(Math.pow(x.value, y.value), x.unit)
|
||||||
|
},
|
||||||
|
percentage: function (n) {
|
||||||
|
const result = mathHelper(num => num * 100, '%', n)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
import Quoted from '../tree/quoted'
|
||||||
|
import Anonymous from '../tree/anonymous'
|
||||||
|
import JavaScript from '../tree/javascript'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
e: function (str) {
|
||||||
|
return new Quoted(
|
||||||
|
'"',
|
||||||
|
str instanceof JavaScript ? str.evaluated : str.value,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
},
|
||||||
|
escape: function (str) {
|
||||||
|
return new Anonymous(
|
||||||
|
encodeURI(str.value)
|
||||||
|
.replace(/=/g, '%3D')
|
||||||
|
.replace(/:/g, '%3A')
|
||||||
|
.replace(/#/g, '%23')
|
||||||
|
.replace(/;/g, '%3B')
|
||||||
|
.replace(/\(/g, '%28')
|
||||||
|
.replace(/\)/g, '%29')
|
||||||
|
)
|
||||||
|
},
|
||||||
|
replace: function (string, pattern, replacement, flags) {
|
||||||
|
let result = string.value
|
||||||
|
replacement =
|
||||||
|
replacement.type === 'Quoted' ? replacement.value : replacement.toCSS()
|
||||||
|
result = result.replace(
|
||||||
|
new RegExp(pattern.value, flags ? flags.value : ''),
|
||||||
|
replacement
|
||||||
|
)
|
||||||
|
return new Quoted(string.quote || '', result, string.escaped)
|
||||||
|
},
|
||||||
|
'%': function (string /* arg, arg, ... */) {
|
||||||
|
const args = Array.prototype.slice.call(arguments, 1)
|
||||||
|
let result = string.value
|
||||||
|
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
/* jshint loopfunc:true */
|
||||||
|
result = result.replace(/%[sda]/i, token => {
|
||||||
|
const value =
|
||||||
|
args[i].type === 'Quoted' && token.match(/s/i)
|
||||||
|
? args[i].value
|
||||||
|
: args[i].toCSS()
|
||||||
|
return token.match(/[A-Z]$/) ? encodeURIComponent(value) : value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
result = result.replace(/%%/g, '%')
|
||||||
|
return new Quoted(string.quote || '', result, string.escaped)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,116 @@
|
||||||
|
import Dimension from '../tree/dimension'
|
||||||
|
import Color from '../tree/color'
|
||||||
|
import Expression from '../tree/expression'
|
||||||
|
import Quoted from '../tree/quoted'
|
||||||
|
import URL from '../tree/url'
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
return {
|
||||||
|
'svg-gradient': function (direction) {
|
||||||
|
let stops
|
||||||
|
let gradientDirectionSvg
|
||||||
|
let gradientType = 'linear'
|
||||||
|
let rectangleDimension = 'x="0" y="0" width="1" height="1"'
|
||||||
|
const renderEnv = { compress: false }
|
||||||
|
let returner
|
||||||
|
const directionValue = direction.toCSS(renderEnv)
|
||||||
|
let i
|
||||||
|
let color
|
||||||
|
let position
|
||||||
|
let positionValue
|
||||||
|
let alpha
|
||||||
|
|
||||||
|
function throwArgumentDescriptor() {
|
||||||
|
throw {
|
||||||
|
type: 'Argument',
|
||||||
|
message:
|
||||||
|
'svg-gradient expects direction, start_color [start_position], [color position,]...,' +
|
||||||
|
' end_color [end_position] or direction, color list'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arguments.length == 2) {
|
||||||
|
if (arguments[1].value.length < 2) {
|
||||||
|
throwArgumentDescriptor()
|
||||||
|
}
|
||||||
|
stops = arguments[1].value
|
||||||
|
} else if (arguments.length < 3) {
|
||||||
|
throwArgumentDescriptor()
|
||||||
|
} else {
|
||||||
|
stops = Array.prototype.slice.call(arguments, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (directionValue) {
|
||||||
|
case 'to bottom':
|
||||||
|
gradientDirectionSvg = 'x1="0%" y1="0%" x2="0%" y2="100%"'
|
||||||
|
break
|
||||||
|
case 'to right':
|
||||||
|
gradientDirectionSvg = 'x1="0%" y1="0%" x2="100%" y2="0%"'
|
||||||
|
break
|
||||||
|
case 'to bottom right':
|
||||||
|
gradientDirectionSvg = 'x1="0%" y1="0%" x2="100%" y2="100%"'
|
||||||
|
break
|
||||||
|
case 'to top right':
|
||||||
|
gradientDirectionSvg = 'x1="0%" y1="100%" x2="100%" y2="0%"'
|
||||||
|
break
|
||||||
|
case 'ellipse':
|
||||||
|
case 'ellipse at center':
|
||||||
|
gradientType = 'radial'
|
||||||
|
gradientDirectionSvg = 'cx="50%" cy="50%" r="75%"'
|
||||||
|
rectangleDimension = 'x="-50" y="-50" width="101" height="101"'
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
throw {
|
||||||
|
type: 'Argument',
|
||||||
|
message:
|
||||||
|
"svg-gradient direction must be 'to bottom', 'to right'," +
|
||||||
|
" 'to bottom right', 'to top right' or 'ellipse at center'"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
returner = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1 1"><${gradientType}Gradient id="g" ${gradientDirectionSvg}>`
|
||||||
|
|
||||||
|
for (i = 0; i < stops.length; i += 1) {
|
||||||
|
if (stops[i] instanceof Expression) {
|
||||||
|
color = stops[i].value[0]
|
||||||
|
position = stops[i].value[1]
|
||||||
|
} else {
|
||||||
|
color = stops[i]
|
||||||
|
position = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!(color instanceof Color) ||
|
||||||
|
(!((i === 0 || i + 1 === stops.length) && position === undefined) &&
|
||||||
|
!(position instanceof Dimension))
|
||||||
|
) {
|
||||||
|
throwArgumentDescriptor()
|
||||||
|
}
|
||||||
|
positionValue = position
|
||||||
|
? position.toCSS(renderEnv)
|
||||||
|
: i === 0
|
||||||
|
? '0%'
|
||||||
|
: '100%'
|
||||||
|
alpha = color.alpha
|
||||||
|
returner += `<stop offset="${positionValue}" stop-color="${color.toRGB()}"${
|
||||||
|
alpha < 1 ? ` stop-opacity="${alpha}"` : ''
|
||||||
|
}/>`
|
||||||
|
}
|
||||||
|
returner += `</${gradientType}Gradient><rect ${rectangleDimension} fill="url(#g)" /></svg>`
|
||||||
|
|
||||||
|
returner = encodeURIComponent(returner)
|
||||||
|
|
||||||
|
returner = `data:image/svg+xml,${returner}`
|
||||||
|
return new URL(
|
||||||
|
new Quoted(
|
||||||
|
`'${returner}'`,
|
||||||
|
returner,
|
||||||
|
false,
|
||||||
|
this.index,
|
||||||
|
this.currentFileInfo
|
||||||
|
),
|
||||||
|
this.index,
|
||||||
|
this.currentFileInfo
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
import Keyword from '../tree/keyword'
|
||||||
|
import DetachedRuleset from '../tree/detached-ruleset'
|
||||||
|
import Dimension from '../tree/dimension'
|
||||||
|
import Color from '../tree/color'
|
||||||
|
import Quoted from '../tree/quoted'
|
||||||
|
import Anonymous from '../tree/anonymous'
|
||||||
|
import URL from '../tree/url'
|
||||||
|
import Operation from '../tree/operation'
|
||||||
|
|
||||||
|
const isa = (n, Type) => (n instanceof Type ? Keyword.True : Keyword.False)
|
||||||
|
const isunit = (n, unit) => {
|
||||||
|
if (unit === undefined) {
|
||||||
|
throw {
|
||||||
|
type: 'Argument',
|
||||||
|
message: 'missing the required second argument to isunit.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unit = typeof unit.value === 'string' ? unit.value : unit
|
||||||
|
if (typeof unit !== 'string') {
|
||||||
|
throw {
|
||||||
|
type: 'Argument',
|
||||||
|
message: 'Second argument to isunit should be a unit or a string.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n instanceof Dimension && n.unit.is(unit)
|
||||||
|
? Keyword.True
|
||||||
|
: Keyword.False
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
isruleset: function (n) {
|
||||||
|
return isa(n, DetachedRuleset)
|
||||||
|
},
|
||||||
|
iscolor: function (n) {
|
||||||
|
return isa(n, Color)
|
||||||
|
},
|
||||||
|
isnumber: function (n) {
|
||||||
|
return isa(n, Dimension)
|
||||||
|
},
|
||||||
|
isstring: function (n) {
|
||||||
|
return isa(n, Quoted)
|
||||||
|
},
|
||||||
|
iskeyword: function (n) {
|
||||||
|
return isa(n, Keyword)
|
||||||
|
},
|
||||||
|
isurl: function (n) {
|
||||||
|
return isa(n, URL)
|
||||||
|
},
|
||||||
|
ispixel: function (n) {
|
||||||
|
return isunit(n, 'px')
|
||||||
|
},
|
||||||
|
ispercentage: function (n) {
|
||||||
|
return isunit(n, '%')
|
||||||
|
},
|
||||||
|
isem: function (n) {
|
||||||
|
return isunit(n, 'em')
|
||||||
|
},
|
||||||
|
isunit,
|
||||||
|
unit: function (val, unit) {
|
||||||
|
if (!(val instanceof Dimension)) {
|
||||||
|
throw {
|
||||||
|
type: 'Argument',
|
||||||
|
message: `the first argument to unit must be a number${
|
||||||
|
val instanceof Operation ? '. Have you forgotten parenthesis?' : ''
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (unit) {
|
||||||
|
if (unit instanceof Keyword) {
|
||||||
|
unit = unit.value
|
||||||
|
} else {
|
||||||
|
unit = unit.toCSS()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
unit = ''
|
||||||
|
}
|
||||||
|
return new Dimension(val.value, unit)
|
||||||
|
},
|
||||||
|
'get-unit': function (n) {
|
||||||
|
return new Anonymous(n.unit)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,235 @@
|
||||||
|
import contexts from './contexts'
|
||||||
|
import Parser from './parser/parser'
|
||||||
|
import LessError from './less-error'
|
||||||
|
import * as utils from './utils'
|
||||||
|
import logger from './logger'
|
||||||
|
|
||||||
|
export default function (environment) {
|
||||||
|
// FileInfo = {
|
||||||
|
// 'rewriteUrls' - option - whether to adjust URL's to be relative
|
||||||
|
// 'filename' - full resolved filename of current file
|
||||||
|
// 'rootpath' - path to append to normal URLs for this node
|
||||||
|
// 'currentDirectory' - path to the current file, absolute
|
||||||
|
// 'rootFilename' - filename of the base file
|
||||||
|
// 'entryPath' - absolute path to the entry file
|
||||||
|
// 'reference' - whether the file should not be output and only output parts that are referenced
|
||||||
|
|
||||||
|
class ImportManager {
|
||||||
|
constructor(less, context, rootFileInfo) {
|
||||||
|
this.less = less
|
||||||
|
this.rootFilename = rootFileInfo.filename
|
||||||
|
this.paths = context.paths || [] // Search paths, when importing
|
||||||
|
this.contents = {} // map - filename to contents of all the files
|
||||||
|
this.contentsIgnoredChars = {} // map - filename to lines at the beginning of each file to ignore
|
||||||
|
this.mime = context.mime
|
||||||
|
this.error = null
|
||||||
|
this.context = context
|
||||||
|
// Deprecated? Unused outside of here, could be useful.
|
||||||
|
this.queue = [] // Files which haven't been imported yet
|
||||||
|
this.files = {} // Holds the imported parse trees.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an import to be imported
|
||||||
|
* @param path - the raw path
|
||||||
|
* @param tryAppendExtension - whether to try appending a file extension (.less or .js if the path has no extension)
|
||||||
|
* @param currentFileInfo - the current file info (used for instance to work out relative paths)
|
||||||
|
* @param importOptions - import options
|
||||||
|
* @param callback - callback for when it is imported
|
||||||
|
*/
|
||||||
|
push(path, tryAppendExtension, currentFileInfo, importOptions, callback) {
|
||||||
|
const importManager = this,
|
||||||
|
pluginLoader = this.context.pluginManager.Loader
|
||||||
|
|
||||||
|
this.queue.push(path)
|
||||||
|
|
||||||
|
const fileParsedFunc = function (e, root, fullPath) {
|
||||||
|
importManager.queue.splice(importManager.queue.indexOf(path), 1) // Remove the path from the queue
|
||||||
|
|
||||||
|
const importedEqualsRoot = fullPath === importManager.rootFilename
|
||||||
|
if (importOptions.optional && e) {
|
||||||
|
callback(null, { rules: [] }, false, null)
|
||||||
|
logger.info(
|
||||||
|
`The file ${fullPath} was skipped because it was not found and the import was marked optional.`
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// Inline imports aren't cached here.
|
||||||
|
// If we start to cache them, please make sure they won't conflict with non-inline imports of the
|
||||||
|
// same name as they used to do before this comment and the condition below have been added.
|
||||||
|
if (!importManager.files[fullPath] && !importOptions.inline) {
|
||||||
|
importManager.files[fullPath] = { root, options: importOptions }
|
||||||
|
}
|
||||||
|
if (e && !importManager.error) {
|
||||||
|
importManager.error = e
|
||||||
|
}
|
||||||
|
callback(e, root, importedEqualsRoot, fullPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newFileInfo = {
|
||||||
|
rewriteUrls: this.context.rewriteUrls,
|
||||||
|
entryPath: currentFileInfo.entryPath,
|
||||||
|
rootpath: currentFileInfo.rootpath,
|
||||||
|
rootFilename: currentFileInfo.rootFilename
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileManager = environment.getFileManager(
|
||||||
|
path,
|
||||||
|
currentFileInfo.currentDirectory,
|
||||||
|
this.context,
|
||||||
|
environment
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!fileManager) {
|
||||||
|
fileParsedFunc({ message: `Could not find a file-manager for ${path}` })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadFileCallback = function (loadedFile) {
|
||||||
|
let plugin
|
||||||
|
const resolvedFilename = loadedFile.filename
|
||||||
|
const contents = loadedFile.contents.replace(/^\uFEFF/, '')
|
||||||
|
|
||||||
|
// Pass on an updated rootpath if path of imported file is relative and file
|
||||||
|
// is in a (sub|sup) directory
|
||||||
|
//
|
||||||
|
// Examples:
|
||||||
|
// - If path of imported file is 'module/nav/nav.less' and rootpath is 'less/',
|
||||||
|
// then rootpath should become 'less/module/nav/'
|
||||||
|
// - If path of imported file is '../mixins.less' and rootpath is 'less/',
|
||||||
|
// then rootpath should become 'less/../'
|
||||||
|
newFileInfo.currentDirectory = fileManager.getPath(resolvedFilename)
|
||||||
|
if (newFileInfo.rewriteUrls) {
|
||||||
|
newFileInfo.rootpath = fileManager.join(
|
||||||
|
importManager.context.rootpath || '',
|
||||||
|
fileManager.pathDiff(
|
||||||
|
newFileInfo.currentDirectory,
|
||||||
|
newFileInfo.entryPath
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
!fileManager.isPathAbsolute(newFileInfo.rootpath) &&
|
||||||
|
fileManager.alwaysMakePathsAbsolute()
|
||||||
|
) {
|
||||||
|
newFileInfo.rootpath = fileManager.join(
|
||||||
|
newFileInfo.entryPath,
|
||||||
|
newFileInfo.rootpath
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newFileInfo.filename = resolvedFilename
|
||||||
|
|
||||||
|
const newEnv = new contexts.Parse(importManager.context)
|
||||||
|
|
||||||
|
newEnv.processImports = false
|
||||||
|
importManager.contents[resolvedFilename] = contents
|
||||||
|
|
||||||
|
if (currentFileInfo.reference || importOptions.reference) {
|
||||||
|
newFileInfo.reference = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (importOptions.isPlugin) {
|
||||||
|
plugin = pluginLoader.evalPlugin(
|
||||||
|
contents,
|
||||||
|
newEnv,
|
||||||
|
importManager,
|
||||||
|
importOptions.pluginArgs,
|
||||||
|
newFileInfo
|
||||||
|
)
|
||||||
|
if (plugin instanceof LessError) {
|
||||||
|
fileParsedFunc(plugin, null, resolvedFilename)
|
||||||
|
} else {
|
||||||
|
fileParsedFunc(null, plugin, resolvedFilename)
|
||||||
|
}
|
||||||
|
} else if (importOptions.inline) {
|
||||||
|
fileParsedFunc(null, contents, resolvedFilename)
|
||||||
|
} else {
|
||||||
|
// import (multiple) parse trees apparently get altered and can't be cached.
|
||||||
|
// TODO: investigate why this is
|
||||||
|
if (
|
||||||
|
importManager.files[resolvedFilename] &&
|
||||||
|
!importManager.files[resolvedFilename].options.multiple &&
|
||||||
|
!importOptions.multiple
|
||||||
|
) {
|
||||||
|
fileParsedFunc(
|
||||||
|
null,
|
||||||
|
importManager.files[resolvedFilename].root,
|
||||||
|
resolvedFilename
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
new Parser(newEnv, importManager, newFileInfo).parse(
|
||||||
|
contents,
|
||||||
|
function (e, root) {
|
||||||
|
fileParsedFunc(e, root, resolvedFilename)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let loadedFile
|
||||||
|
let promise
|
||||||
|
const context = utils.clone(this.context)
|
||||||
|
|
||||||
|
if (tryAppendExtension) {
|
||||||
|
context.ext = importOptions.isPlugin ? '.js' : '.less'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (importOptions.isPlugin) {
|
||||||
|
context.mime = 'application/javascript'
|
||||||
|
|
||||||
|
if (context.syncImport) {
|
||||||
|
loadedFile = pluginLoader.loadPluginSync(
|
||||||
|
path,
|
||||||
|
currentFileInfo.currentDirectory,
|
||||||
|
context,
|
||||||
|
environment,
|
||||||
|
fileManager
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
promise = pluginLoader.loadPlugin(
|
||||||
|
path,
|
||||||
|
currentFileInfo.currentDirectory,
|
||||||
|
context,
|
||||||
|
environment,
|
||||||
|
fileManager
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (context.syncImport) {
|
||||||
|
loadedFile = fileManager.loadFileSync(
|
||||||
|
path,
|
||||||
|
currentFileInfo.currentDirectory,
|
||||||
|
context,
|
||||||
|
environment
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
promise = fileManager.loadFile(
|
||||||
|
path,
|
||||||
|
currentFileInfo.currentDirectory,
|
||||||
|
context,
|
||||||
|
environment,
|
||||||
|
(err, loadedFile) => {
|
||||||
|
if (err) {
|
||||||
|
fileParsedFunc(err)
|
||||||
|
} else {
|
||||||
|
loadFileCallback(loadedFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (loadedFile) {
|
||||||
|
if (!loadedFile.filename) {
|
||||||
|
fileParsedFunc(loadedFile)
|
||||||
|
} else {
|
||||||
|
loadFileCallback(loadedFile)
|
||||||
|
}
|
||||||
|
} else if (promise) {
|
||||||
|
promise.then(loadFileCallback, fileParsedFunc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ImportManager
|
||||||
|
}
|
|
@ -0,0 +1,97 @@
|
||||||
|
import Environment from './environment/environment'
|
||||||
|
import data from './data'
|
||||||
|
import tree from './tree'
|
||||||
|
import AbstractFileManager from './environment/abstract-file-manager'
|
||||||
|
import AbstractPluginLoader from './environment/abstract-plugin-loader'
|
||||||
|
import visitors from './visitors'
|
||||||
|
import Parser from './parser/parser'
|
||||||
|
import functions from './functions'
|
||||||
|
import contexts from './contexts'
|
||||||
|
import LessError from './less-error'
|
||||||
|
import transformTree from './transform-tree'
|
||||||
|
import * as utils from './utils'
|
||||||
|
import PluginManager from './plugin-manager'
|
||||||
|
import logger from './logger'
|
||||||
|
import SourceMapOutput from './source-map-output'
|
||||||
|
import SourceMapBuilder from './source-map-builder'
|
||||||
|
import ParseTree from './parse-tree'
|
||||||
|
import ImportManager from './import-manager'
|
||||||
|
import Parse from './parse'
|
||||||
|
import Render from './render'
|
||||||
|
import { version } from '../../package.json'
|
||||||
|
import parseVersion from 'parse-node-version'
|
||||||
|
|
||||||
|
export default function (environment, fileManagers) {
|
||||||
|
let sourceMapOutput, sourceMapBuilder, parseTree, importManager
|
||||||
|
|
||||||
|
environment = new Environment(environment, fileManagers)
|
||||||
|
sourceMapOutput = SourceMapOutput(environment)
|
||||||
|
sourceMapBuilder = SourceMapBuilder(sourceMapOutput, environment)
|
||||||
|
parseTree = ParseTree(sourceMapBuilder)
|
||||||
|
importManager = ImportManager(environment)
|
||||||
|
|
||||||
|
const render = Render(environment, parseTree, importManager)
|
||||||
|
const parse = Parse(environment, parseTree, importManager)
|
||||||
|
|
||||||
|
const v = parseVersion(`v${version}`)
|
||||||
|
const initial = {
|
||||||
|
version: [v.major, v.minor, v.patch],
|
||||||
|
data,
|
||||||
|
tree,
|
||||||
|
Environment,
|
||||||
|
AbstractFileManager,
|
||||||
|
AbstractPluginLoader,
|
||||||
|
environment,
|
||||||
|
visitors,
|
||||||
|
Parser,
|
||||||
|
functions: functions(environment),
|
||||||
|
contexts,
|
||||||
|
SourceMapOutput: sourceMapOutput,
|
||||||
|
SourceMapBuilder: sourceMapBuilder,
|
||||||
|
ParseTree: parseTree,
|
||||||
|
ImportManager: importManager,
|
||||||
|
render,
|
||||||
|
parse,
|
||||||
|
LessError,
|
||||||
|
transformTree,
|
||||||
|
utils,
|
||||||
|
PluginManager,
|
||||||
|
logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a public API
|
||||||
|
|
||||||
|
const ctor = function (t) {
|
||||||
|
return function () {
|
||||||
|
const obj = Object.create(t.prototype)
|
||||||
|
t.apply(obj, Array.prototype.slice.call(arguments, 0))
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let t
|
||||||
|
const api = Object.create(initial)
|
||||||
|
for (const n in initial.tree) {
|
||||||
|
/* eslint guard-for-in: 0 */
|
||||||
|
t = initial.tree[n]
|
||||||
|
if (typeof t === 'function') {
|
||||||
|
api[n.toLowerCase()] = ctor(t)
|
||||||
|
} else {
|
||||||
|
api[n] = Object.create(null)
|
||||||
|
for (const o in t) {
|
||||||
|
/* eslint guard-for-in: 0 */
|
||||||
|
api[n][o.toLowerCase()] = ctor(t[o])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Some of the functions assume a `this` context of the API object,
|
||||||
|
* which causes it to fail when wrapped for ES6 imports.
|
||||||
|
*
|
||||||
|
* An assumed `this` should be removed in the future.
|
||||||
|
*/
|
||||||
|
initial.parse = initial.parse.bind(api)
|
||||||
|
initial.render = initial.render.bind(api)
|
||||||
|
|
||||||
|
return api
|
||||||
|
}
|
|
@ -0,0 +1,172 @@
|
||||||
|
import * as utils from './utils'
|
||||||
|
|
||||||
|
const anonymousFunc = /(<anonymous>|Function):(\d+):(\d+)/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a centralized class of any error that could be thrown internally (mostly by the parser).
|
||||||
|
* Besides standard .message it keeps some additional data like a path to the file where the error
|
||||||
|
* occurred along with line and column numbers.
|
||||||
|
*
|
||||||
|
* @class
|
||||||
|
* @extends Error
|
||||||
|
* @type {module.LessError}
|
||||||
|
*
|
||||||
|
* @prop {string} type
|
||||||
|
* @prop {string} filename
|
||||||
|
* @prop {number} index
|
||||||
|
* @prop {number} line
|
||||||
|
* @prop {number} column
|
||||||
|
* @prop {number} callLine
|
||||||
|
* @prop {number} callExtract
|
||||||
|
* @prop {string[]} extract
|
||||||
|
*
|
||||||
|
* @param {Object} e - An error object to wrap around or just a descriptive object
|
||||||
|
* @param {Object} fileContentMap - An object with file contents in 'contents' property (like importManager) @todo - move to fileManager?
|
||||||
|
* @param {string} [currentFilename]
|
||||||
|
*/
|
||||||
|
const LessError = function (e, fileContentMap, currentFilename) {
|
||||||
|
Error.call(this)
|
||||||
|
|
||||||
|
const filename = e.filename || currentFilename
|
||||||
|
|
||||||
|
this.message = e.message
|
||||||
|
this.stack = e.stack
|
||||||
|
|
||||||
|
if (fileContentMap && filename) {
|
||||||
|
const input = fileContentMap.contents[filename]
|
||||||
|
const loc = utils.getLocation(e.index, input)
|
||||||
|
var line = loc.line
|
||||||
|
const col = loc.column
|
||||||
|
const callLine = e.call && utils.getLocation(e.call, input).line
|
||||||
|
const lines = input ? input.split('\n') : ''
|
||||||
|
|
||||||
|
this.type = e.type || 'Syntax'
|
||||||
|
this.filename = filename
|
||||||
|
this.index = e.index
|
||||||
|
this.line = typeof line === 'number' ? line + 1 : null
|
||||||
|
this.column = col
|
||||||
|
|
||||||
|
if (!this.line && this.stack) {
|
||||||
|
const found = this.stack.match(anonymousFunc)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We have to figure out how this environment stringifies anonymous functions
|
||||||
|
* so we can correctly map plugin errors.
|
||||||
|
*
|
||||||
|
* Note, in Node 8, the output of anonymous funcs varied based on parameters
|
||||||
|
* being present or not, so we inject dummy params.
|
||||||
|
*/
|
||||||
|
const func = new Function('a', 'throw new Error()')
|
||||||
|
let lineAdjust = 0
|
||||||
|
try {
|
||||||
|
func()
|
||||||
|
} catch (e) {
|
||||||
|
const match = e.stack.match(anonymousFunc)
|
||||||
|
lineAdjust = 1 - parseInt(match[2])
|
||||||
|
}
|
||||||
|
|
||||||
|
if (found) {
|
||||||
|
if (found[2]) {
|
||||||
|
this.line = parseInt(found[2]) + lineAdjust
|
||||||
|
}
|
||||||
|
if (found[3]) {
|
||||||
|
this.column = parseInt(found[3])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.callLine = callLine + 1
|
||||||
|
this.callExtract = lines[callLine]
|
||||||
|
|
||||||
|
this.extract = [
|
||||||
|
lines[this.line - 2],
|
||||||
|
lines[this.line - 1],
|
||||||
|
lines[this.line]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof Object.create === 'undefined') {
|
||||||
|
const F = function () {}
|
||||||
|
F.prototype = Error.prototype
|
||||||
|
LessError.prototype = new F()
|
||||||
|
} else {
|
||||||
|
LessError.prototype = Object.create(Error.prototype)
|
||||||
|
}
|
||||||
|
|
||||||
|
LessError.prototype.constructor = LessError
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An overridden version of the default Object.prototype.toString
|
||||||
|
* which uses additional information to create a helpful message.
|
||||||
|
*
|
||||||
|
* @param {Object} options
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
LessError.prototype.toString = function (options) {
|
||||||
|
options = options || {}
|
||||||
|
|
||||||
|
let message = ''
|
||||||
|
const extract = this.extract || []
|
||||||
|
let error = []
|
||||||
|
let stylize = function (str) {
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
if (options.stylize) {
|
||||||
|
const type = typeof options.stylize
|
||||||
|
if (type !== 'function') {
|
||||||
|
throw Error(`options.stylize should be a function, got a ${type}!`)
|
||||||
|
}
|
||||||
|
stylize = options.stylize
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.line !== null) {
|
||||||
|
if (typeof extract[0] === 'string') {
|
||||||
|
error.push(stylize(`${this.line - 1} ${extract[0]}`, 'grey'))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof extract[1] === 'string') {
|
||||||
|
let errorTxt = `${this.line} `
|
||||||
|
if (extract[1]) {
|
||||||
|
errorTxt +=
|
||||||
|
extract[1].slice(0, this.column) +
|
||||||
|
stylize(
|
||||||
|
stylize(
|
||||||
|
stylize(extract[1].substr(this.column, 1), 'bold') +
|
||||||
|
extract[1].slice(this.column + 1),
|
||||||
|
'red'
|
||||||
|
),
|
||||||
|
'inverse'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
error.push(errorTxt)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof extract[2] === 'string') {
|
||||||
|
error.push(stylize(`${this.line + 1} ${extract[2]}`, 'grey'))
|
||||||
|
}
|
||||||
|
error = `${error.join('\n') + stylize('', 'reset')}\n`
|
||||||
|
}
|
||||||
|
|
||||||
|
message += stylize(`${this.type}Error: ${this.message}`, 'red')
|
||||||
|
if (this.filename) {
|
||||||
|
message += stylize(' in ', 'red') + this.filename
|
||||||
|
}
|
||||||
|
if (this.line) {
|
||||||
|
message += stylize(
|
||||||
|
` on line ${this.line}, column ${this.column + 1}:`,
|
||||||
|
'grey'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
message += `\n${error}`
|
||||||
|
|
||||||
|
if (this.callLine) {
|
||||||
|
message += `${stylize('from ', 'red') + (this.filename || '')}/n`
|
||||||
|
message += `${stylize(this.callLine, 'grey')} ${this.callExtract}/n`
|
||||||
|
}
|
||||||
|
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LessError
|
|
@ -0,0 +1,34 @@
|
||||||
|
export default {
|
||||||
|
error: function (msg) {
|
||||||
|
this._fireEvent('error', msg)
|
||||||
|
},
|
||||||
|
warn: function (msg) {
|
||||||
|
this._fireEvent('warn', msg)
|
||||||
|
},
|
||||||
|
info: function (msg) {
|
||||||
|
this._fireEvent('info', msg)
|
||||||
|
},
|
||||||
|
debug: function (msg) {
|
||||||
|
this._fireEvent('debug', msg)
|
||||||
|
},
|
||||||
|
addListener: function (listener) {
|
||||||
|
this._listeners.push(listener)
|
||||||
|
},
|
||||||
|
removeListener: function (listener) {
|
||||||
|
for (let i = 0; i < this._listeners.length; i++) {
|
||||||
|
if (this._listeners[i] === listener) {
|
||||||
|
this._listeners.splice(i, 1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_fireEvent: function (type, msg) {
|
||||||
|
for (let i = 0; i < this._listeners.length; i++) {
|
||||||
|
const logFunction = this._listeners[i][type]
|
||||||
|
if (logFunction) {
|
||||||
|
logFunction(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_listeners: []
|
||||||
|
}
|
|
@ -0,0 +1,80 @@
|
||||||
|
import LessError from './less-error'
|
||||||
|
import transformTree from './transform-tree'
|
||||||
|
import logger from './logger'
|
||||||
|
|
||||||
|
export default function (SourceMapBuilder) {
|
||||||
|
class ParseTree {
|
||||||
|
constructor(root, imports) {
|
||||||
|
this.root = root
|
||||||
|
this.imports = imports
|
||||||
|
}
|
||||||
|
|
||||||
|
toCSS(options) {
|
||||||
|
let evaldRoot
|
||||||
|
const result = {}
|
||||||
|
let sourceMapBuilder
|
||||||
|
try {
|
||||||
|
evaldRoot = transformTree(this.root, options)
|
||||||
|
} catch (e) {
|
||||||
|
throw new LessError(e, this.imports)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const compress = Boolean(options.compress)
|
||||||
|
if (compress) {
|
||||||
|
logger.warn(
|
||||||
|
'The compress option has been deprecated. ' +
|
||||||
|
'We recommend you use a dedicated css minifier, for instance see less-plugin-clean-css.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const toCSSOptions = {
|
||||||
|
compress,
|
||||||
|
dumpLineNumbers: options.dumpLineNumbers,
|
||||||
|
strictUnits: Boolean(options.strictUnits),
|
||||||
|
numPrecision: 8
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.sourceMap) {
|
||||||
|
sourceMapBuilder = new SourceMapBuilder(options.sourceMap)
|
||||||
|
result.css = sourceMapBuilder.toCSS(
|
||||||
|
evaldRoot,
|
||||||
|
toCSSOptions,
|
||||||
|
this.imports
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
result.css = evaldRoot.toCSS(toCSSOptions)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
throw new LessError(e, this.imports)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.pluginManager) {
|
||||||
|
const postProcessors = options.pluginManager.getPostProcessors()
|
||||||
|
for (let i = 0; i < postProcessors.length; i++) {
|
||||||
|
result.css = postProcessors[i].process(result.css, {
|
||||||
|
sourceMap: sourceMapBuilder,
|
||||||
|
options,
|
||||||
|
imports: this.imports
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (options.sourceMap) {
|
||||||
|
result.map = sourceMapBuilder.getExternalSourceMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
result.imports = []
|
||||||
|
for (const file in this.imports.files) {
|
||||||
|
if (
|
||||||
|
Object.prototype.hasOwnProperty.call(this.imports.files, file) &&
|
||||||
|
file !== this.imports.rootFilename
|
||||||
|
) {
|
||||||
|
result.imports.push(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ParseTree
|
||||||
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
import contexts from './contexts'
|
||||||
|
import Parser from './parser/parser'
|
||||||
|
import PluginManager from './plugin-manager'
|
||||||
|
import LessError from './less-error'
|
||||||
|
import * as utils from './utils'
|
||||||
|
|
||||||
|
export default function (environment, ParseTree, ImportManager) {
|
||||||
|
const parse = function (input, options, callback) {
|
||||||
|
if (typeof options === 'function') {
|
||||||
|
callback = options
|
||||||
|
options = utils.copyOptions(this.options, {})
|
||||||
|
} else {
|
||||||
|
options = utils.copyOptions(this.options, options || {})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!callback) {
|
||||||
|
const self = this
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
parse.call(self, input, options, function (err, output) {
|
||||||
|
if (err) {
|
||||||
|
reject(err)
|
||||||
|
} else {
|
||||||
|
resolve(output)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
let context
|
||||||
|
let rootFileInfo
|
||||||
|
const pluginManager = new PluginManager(this, !options.reUsePluginManager)
|
||||||
|
|
||||||
|
options.pluginManager = pluginManager
|
||||||
|
|
||||||
|
context = new contexts.Parse(options)
|
||||||
|
|
||||||
|
if (options.rootFileInfo) {
|
||||||
|
rootFileInfo = options.rootFileInfo
|
||||||
|
} else {
|
||||||
|
const filename = options.filename || 'input'
|
||||||
|
const entryPath = filename.replace(/[^/\\]*$/, '')
|
||||||
|
rootFileInfo = {
|
||||||
|
filename,
|
||||||
|
rewriteUrls: context.rewriteUrls,
|
||||||
|
rootpath: context.rootpath || '',
|
||||||
|
currentDirectory: entryPath,
|
||||||
|
entryPath,
|
||||||
|
rootFilename: filename
|
||||||
|
}
|
||||||
|
// add in a missing trailing slash
|
||||||
|
if (rootFileInfo.rootpath && rootFileInfo.rootpath.slice(-1) !== '/') {
|
||||||
|
rootFileInfo.rootpath += '/'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const imports = new ImportManager(this, context, rootFileInfo)
|
||||||
|
this.importManager = imports
|
||||||
|
|
||||||
|
// TODO: allow the plugins to be just a list of paths or names
|
||||||
|
// Do an async plugin queue like lessc
|
||||||
|
|
||||||
|
if (options.plugins) {
|
||||||
|
options.plugins.forEach(function (plugin) {
|
||||||
|
let evalResult, contents
|
||||||
|
if (plugin.fileContent) {
|
||||||
|
contents = plugin.fileContent.replace(/^\uFEFF/, '')
|
||||||
|
evalResult = pluginManager.Loader.evalPlugin(
|
||||||
|
contents,
|
||||||
|
context,
|
||||||
|
imports,
|
||||||
|
plugin.options,
|
||||||
|
plugin.filename
|
||||||
|
)
|
||||||
|
if (evalResult instanceof LessError) {
|
||||||
|
return callback(evalResult)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pluginManager.addPlugin(plugin)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
new Parser(context, imports, rootFileInfo).parse(
|
||||||
|
input,
|
||||||
|
function (e, root) {
|
||||||
|
if (e) {
|
||||||
|
return callback(e)
|
||||||
|
}
|
||||||
|
callback(null, root, imports, options)
|
||||||
|
},
|
||||||
|
options
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return parse
|
||||||
|
}
|
|
@ -0,0 +1,172 @@
|
||||||
|
// Split the input into chunks.
|
||||||
|
export default function (input, fail) {
|
||||||
|
const len = input.length
|
||||||
|
let level = 0
|
||||||
|
let parenLevel = 0
|
||||||
|
let lastOpening
|
||||||
|
let lastOpeningParen
|
||||||
|
let lastMultiComment
|
||||||
|
let lastMultiCommentEndBrace
|
||||||
|
const chunks = []
|
||||||
|
let emitFrom = 0
|
||||||
|
let chunkerCurrentIndex
|
||||||
|
let currentChunkStartIndex
|
||||||
|
let cc
|
||||||
|
let cc2
|
||||||
|
let matched
|
||||||
|
|
||||||
|
function emitChunk(force) {
|
||||||
|
const len = chunkerCurrentIndex - emitFrom
|
||||||
|
if ((len < 512 && !force) || !len) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
chunks.push(input.slice(emitFrom, chunkerCurrentIndex + 1))
|
||||||
|
emitFrom = chunkerCurrentIndex + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
for (
|
||||||
|
chunkerCurrentIndex = 0;
|
||||||
|
chunkerCurrentIndex < len;
|
||||||
|
chunkerCurrentIndex++
|
||||||
|
) {
|
||||||
|
cc = input.charCodeAt(chunkerCurrentIndex)
|
||||||
|
if ((cc >= 97 && cc <= 122) || cc < 34) {
|
||||||
|
// a-z or whitespace
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (cc) {
|
||||||
|
case 40: // (
|
||||||
|
parenLevel++
|
||||||
|
lastOpeningParen = chunkerCurrentIndex
|
||||||
|
continue
|
||||||
|
case 41: // )
|
||||||
|
if (--parenLevel < 0) {
|
||||||
|
return fail('missing opening `(`', chunkerCurrentIndex)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
case 59: // ;
|
||||||
|
if (!parenLevel) {
|
||||||
|
emitChunk()
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
case 123: // {
|
||||||
|
level++
|
||||||
|
lastOpening = chunkerCurrentIndex
|
||||||
|
continue
|
||||||
|
case 125: // }
|
||||||
|
if (--level < 0) {
|
||||||
|
return fail('missing opening `{`', chunkerCurrentIndex)
|
||||||
|
}
|
||||||
|
if (!level && !parenLevel) {
|
||||||
|
emitChunk()
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
case 92: // \
|
||||||
|
if (chunkerCurrentIndex < len - 1) {
|
||||||
|
chunkerCurrentIndex++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return fail('unescaped `\\`', chunkerCurrentIndex)
|
||||||
|
case 34:
|
||||||
|
case 39:
|
||||||
|
case 96: // ", ' and `
|
||||||
|
matched = 0
|
||||||
|
currentChunkStartIndex = chunkerCurrentIndex
|
||||||
|
for (
|
||||||
|
chunkerCurrentIndex = chunkerCurrentIndex + 1;
|
||||||
|
chunkerCurrentIndex < len;
|
||||||
|
chunkerCurrentIndex++
|
||||||
|
) {
|
||||||
|
cc2 = input.charCodeAt(chunkerCurrentIndex)
|
||||||
|
if (cc2 > 96) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (cc2 == cc) {
|
||||||
|
matched = 1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (cc2 == 92) {
|
||||||
|
// \
|
||||||
|
if (chunkerCurrentIndex == len - 1) {
|
||||||
|
return fail('unescaped `\\`', chunkerCurrentIndex)
|
||||||
|
}
|
||||||
|
chunkerCurrentIndex++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (matched) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return fail(
|
||||||
|
`unmatched \`${String.fromCharCode(cc)}\``,
|
||||||
|
currentChunkStartIndex
|
||||||
|
)
|
||||||
|
case 47: // /, check for comment
|
||||||
|
if (parenLevel || chunkerCurrentIndex == len - 1) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cc2 = input.charCodeAt(chunkerCurrentIndex + 1)
|
||||||
|
if (cc2 == 47) {
|
||||||
|
// //, find lnfeed
|
||||||
|
for (
|
||||||
|
chunkerCurrentIndex = chunkerCurrentIndex + 2;
|
||||||
|
chunkerCurrentIndex < len;
|
||||||
|
chunkerCurrentIndex++
|
||||||
|
) {
|
||||||
|
cc2 = input.charCodeAt(chunkerCurrentIndex)
|
||||||
|
if (cc2 <= 13 && (cc2 == 10 || cc2 == 13)) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (cc2 == 42) {
|
||||||
|
// /*, find */
|
||||||
|
lastMultiComment = currentChunkStartIndex = chunkerCurrentIndex
|
||||||
|
for (
|
||||||
|
chunkerCurrentIndex = chunkerCurrentIndex + 2;
|
||||||
|
chunkerCurrentIndex < len - 1;
|
||||||
|
chunkerCurrentIndex++
|
||||||
|
) {
|
||||||
|
cc2 = input.charCodeAt(chunkerCurrentIndex)
|
||||||
|
if (cc2 == 125) {
|
||||||
|
lastMultiCommentEndBrace = chunkerCurrentIndex
|
||||||
|
}
|
||||||
|
if (cc2 != 42) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (input.charCodeAt(chunkerCurrentIndex + 1) == 47) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (chunkerCurrentIndex == len - 1) {
|
||||||
|
return fail('missing closing `*/`', currentChunkStartIndex)
|
||||||
|
}
|
||||||
|
chunkerCurrentIndex++
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
case 42: // *, check for unmatched */
|
||||||
|
if (
|
||||||
|
chunkerCurrentIndex < len - 1 &&
|
||||||
|
input.charCodeAt(chunkerCurrentIndex + 1) == 47
|
||||||
|
) {
|
||||||
|
return fail('unmatched `/*`', chunkerCurrentIndex)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (level !== 0) {
|
||||||
|
if (
|
||||||
|
lastMultiComment > lastOpening &&
|
||||||
|
lastMultiCommentEndBrace > lastMultiComment
|
||||||
|
) {
|
||||||
|
return fail('missing closing `}` or `*/`', lastOpening)
|
||||||
|
} else {
|
||||||
|
return fail('missing closing `}`', lastOpening)
|
||||||
|
}
|
||||||
|
} else if (parenLevel !== 0) {
|
||||||
|
return fail('missing closing `)`', lastOpeningParen)
|
||||||
|
}
|
||||||
|
|
||||||
|
emitChunk(true)
|
||||||
|
return chunks
|
||||||
|
}
|
|
@ -0,0 +1,412 @@
|
||||||
|
import chunker from './chunker'
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
let // Less input string
|
||||||
|
input
|
||||||
|
|
||||||
|
let // current chunk
|
||||||
|
j
|
||||||
|
|
||||||
|
const // holds state for backtracking
|
||||||
|
saveStack = []
|
||||||
|
|
||||||
|
let // furthest index the parser has gone to
|
||||||
|
furthest
|
||||||
|
|
||||||
|
let // if this is furthest we got to, this is the probably cause
|
||||||
|
furthestPossibleErrorMessage
|
||||||
|
|
||||||
|
let // chunkified input
|
||||||
|
chunks
|
||||||
|
|
||||||
|
let // current chunk
|
||||||
|
current
|
||||||
|
|
||||||
|
let // index of current chunk, in `input`
|
||||||
|
currentPos
|
||||||
|
|
||||||
|
const parserInput = {}
|
||||||
|
const CHARCODE_SPACE = 32
|
||||||
|
const CHARCODE_TAB = 9
|
||||||
|
const CHARCODE_LF = 10
|
||||||
|
const CHARCODE_CR = 13
|
||||||
|
const CHARCODE_PLUS = 43
|
||||||
|
const CHARCODE_COMMA = 44
|
||||||
|
const CHARCODE_FORWARD_SLASH = 47
|
||||||
|
const CHARCODE_9 = 57
|
||||||
|
|
||||||
|
function skipWhitespace(length) {
|
||||||
|
const oldi = parserInput.i
|
||||||
|
const oldj = j
|
||||||
|
const curr = parserInput.i - currentPos
|
||||||
|
const endIndex = parserInput.i + current.length - curr
|
||||||
|
const mem = (parserInput.i += length)
|
||||||
|
const inp = input
|
||||||
|
let c
|
||||||
|
let nextChar
|
||||||
|
let comment
|
||||||
|
|
||||||
|
for (; parserInput.i < endIndex; parserInput.i++) {
|
||||||
|
c = inp.charCodeAt(parserInput.i)
|
||||||
|
|
||||||
|
if (parserInput.autoCommentAbsorb && c === CHARCODE_FORWARD_SLASH) {
|
||||||
|
nextChar = inp.charAt(parserInput.i + 1)
|
||||||
|
if (nextChar === '/') {
|
||||||
|
comment = { index: parserInput.i, isLineComment: true }
|
||||||
|
let nextNewLine = inp.indexOf('\n', parserInput.i + 2)
|
||||||
|
if (nextNewLine < 0) {
|
||||||
|
nextNewLine = endIndex
|
||||||
|
}
|
||||||
|
parserInput.i = nextNewLine
|
||||||
|
comment.text = inp.substr(
|
||||||
|
comment.index,
|
||||||
|
parserInput.i - comment.index
|
||||||
|
)
|
||||||
|
parserInput.commentStore.push(comment)
|
||||||
|
continue
|
||||||
|
} else if (nextChar === '*') {
|
||||||
|
const nextStarSlash = inp.indexOf('*/', parserInput.i + 2)
|
||||||
|
if (nextStarSlash >= 0) {
|
||||||
|
comment = {
|
||||||
|
index: parserInput.i,
|
||||||
|
text: inp.substr(
|
||||||
|
parserInput.i,
|
||||||
|
nextStarSlash + 2 - parserInput.i
|
||||||
|
),
|
||||||
|
isLineComment: false
|
||||||
|
}
|
||||||
|
parserInput.i += comment.text.length - 1
|
||||||
|
parserInput.commentStore.push(comment)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
c !== CHARCODE_SPACE &&
|
||||||
|
c !== CHARCODE_LF &&
|
||||||
|
c !== CHARCODE_TAB &&
|
||||||
|
c !== CHARCODE_CR
|
||||||
|
) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
current = current.slice(length + parserInput.i - mem + curr)
|
||||||
|
currentPos = parserInput.i
|
||||||
|
|
||||||
|
if (!current.length) {
|
||||||
|
if (j < chunks.length - 1) {
|
||||||
|
current = chunks[++j]
|
||||||
|
skipWhitespace(0) // skip space at the beginning of a chunk
|
||||||
|
return true // things changed
|
||||||
|
}
|
||||||
|
parserInput.finished = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return oldi !== parserInput.i || oldj !== j
|
||||||
|
}
|
||||||
|
|
||||||
|
parserInput.save = () => {
|
||||||
|
currentPos = parserInput.i
|
||||||
|
saveStack.push({ current, i: parserInput.i, j })
|
||||||
|
}
|
||||||
|
parserInput.restore = possibleErrorMessage => {
|
||||||
|
if (
|
||||||
|
parserInput.i > furthest ||
|
||||||
|
(parserInput.i === furthest &&
|
||||||
|
possibleErrorMessage &&
|
||||||
|
!furthestPossibleErrorMessage)
|
||||||
|
) {
|
||||||
|
furthest = parserInput.i
|
||||||
|
furthestPossibleErrorMessage = possibleErrorMessage
|
||||||
|
}
|
||||||
|
const state = saveStack.pop()
|
||||||
|
current = state.current
|
||||||
|
currentPos = parserInput.i = state.i
|
||||||
|
j = state.j
|
||||||
|
}
|
||||||
|
parserInput.forget = () => {
|
||||||
|
saveStack.pop()
|
||||||
|
}
|
||||||
|
parserInput.isWhitespace = offset => {
|
||||||
|
const pos = parserInput.i + (offset || 0)
|
||||||
|
const code = input.charCodeAt(pos)
|
||||||
|
return (
|
||||||
|
code === CHARCODE_SPACE ||
|
||||||
|
code === CHARCODE_CR ||
|
||||||
|
code === CHARCODE_TAB ||
|
||||||
|
code === CHARCODE_LF
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Specialization of $(tok)
|
||||||
|
parserInput.$re = tok => {
|
||||||
|
if (parserInput.i > currentPos) {
|
||||||
|
current = current.slice(parserInput.i - currentPos)
|
||||||
|
currentPos = parserInput.i
|
||||||
|
}
|
||||||
|
|
||||||
|
const m = tok.exec(current)
|
||||||
|
if (!m) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
skipWhitespace(m[0].length)
|
||||||
|
if (typeof m === 'string') {
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.length === 1 ? m[0] : m
|
||||||
|
}
|
||||||
|
|
||||||
|
parserInput.$char = tok => {
|
||||||
|
if (input.charAt(parserInput.i) !== tok) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
skipWhitespace(1)
|
||||||
|
return tok
|
||||||
|
}
|
||||||
|
|
||||||
|
parserInput.$str = tok => {
|
||||||
|
const tokLength = tok.length
|
||||||
|
|
||||||
|
// https://jsperf.com/string-startswith/21
|
||||||
|
for (let i = 0; i < tokLength; i++) {
|
||||||
|
if (input.charAt(parserInput.i + i) !== tok.charAt(i)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
skipWhitespace(tokLength)
|
||||||
|
return tok
|
||||||
|
}
|
||||||
|
|
||||||
|
parserInput.$quoted = loc => {
|
||||||
|
const pos = loc || parserInput.i
|
||||||
|
const startChar = input.charAt(pos)
|
||||||
|
|
||||||
|
if (startChar !== "'" && startChar !== '"') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const length = input.length
|
||||||
|
const currentPosition = pos
|
||||||
|
|
||||||
|
for (let i = 1; i + currentPosition < length; i++) {
|
||||||
|
const nextChar = input.charAt(i + currentPosition)
|
||||||
|
switch (nextChar) {
|
||||||
|
case '\\':
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
case '\r':
|
||||||
|
case '\n':
|
||||||
|
break
|
||||||
|
case startChar: {
|
||||||
|
const str = input.substr(currentPosition, i + 1)
|
||||||
|
if (!loc && loc !== 0) {
|
||||||
|
skipWhitespace(i + 1)
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
return [startChar, str]
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Permissive parsing. Ignores everything except matching {} [] () and quotes
|
||||||
|
* until matching token (outside of blocks)
|
||||||
|
*/
|
||||||
|
parserInput.$parseUntil = tok => {
|
||||||
|
let quote = ''
|
||||||
|
let returnVal = null
|
||||||
|
let inComment = false
|
||||||
|
let blockDepth = 0
|
||||||
|
const blockStack = []
|
||||||
|
const parseGroups = []
|
||||||
|
const length = input.length
|
||||||
|
const startPos = parserInput.i
|
||||||
|
let lastPos = parserInput.i
|
||||||
|
let i = parserInput.i
|
||||||
|
let loop = true
|
||||||
|
let testChar
|
||||||
|
|
||||||
|
if (typeof tok === 'string') {
|
||||||
|
testChar = char => char === tok
|
||||||
|
} else {
|
||||||
|
testChar = char => tok.test(char)
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
let nextChar = input.charAt(i)
|
||||||
|
if (blockDepth === 0 && testChar(nextChar)) {
|
||||||
|
returnVal = input.substr(lastPos, i - lastPos)
|
||||||
|
if (returnVal) {
|
||||||
|
parseGroups.push(returnVal)
|
||||||
|
} else {
|
||||||
|
parseGroups.push(' ')
|
||||||
|
}
|
||||||
|
returnVal = parseGroups
|
||||||
|
skipWhitespace(i - startPos)
|
||||||
|
loop = false
|
||||||
|
} else {
|
||||||
|
if (inComment) {
|
||||||
|
if (nextChar === '*' && input.charAt(i + 1) === '/') {
|
||||||
|
i++
|
||||||
|
blockDepth--
|
||||||
|
inComment = false
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch (nextChar) {
|
||||||
|
case '\\':
|
||||||
|
i++
|
||||||
|
nextChar = input.charAt(i)
|
||||||
|
parseGroups.push(input.substr(lastPos, i - lastPos + 1))
|
||||||
|
lastPos = i + 1
|
||||||
|
break
|
||||||
|
case '/':
|
||||||
|
if (input.charAt(i + 1) === '*') {
|
||||||
|
i++
|
||||||
|
inComment = true
|
||||||
|
blockDepth++
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case "'":
|
||||||
|
case '"':
|
||||||
|
quote = parserInput.$quoted(i)
|
||||||
|
if (quote) {
|
||||||
|
parseGroups.push(input.substr(lastPos, i - lastPos), quote)
|
||||||
|
i += quote[1].length - 1
|
||||||
|
lastPos = i + 1
|
||||||
|
} else {
|
||||||
|
skipWhitespace(i - startPos)
|
||||||
|
returnVal = nextChar
|
||||||
|
loop = false
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case '{':
|
||||||
|
blockStack.push('}')
|
||||||
|
blockDepth++
|
||||||
|
break
|
||||||
|
case '(':
|
||||||
|
blockStack.push(')')
|
||||||
|
blockDepth++
|
||||||
|
break
|
||||||
|
case '[':
|
||||||
|
blockStack.push(']')
|
||||||
|
blockDepth++
|
||||||
|
break
|
||||||
|
case '}':
|
||||||
|
case ')':
|
||||||
|
case ']': {
|
||||||
|
const expected = blockStack.pop()
|
||||||
|
if (nextChar === expected) {
|
||||||
|
blockDepth--
|
||||||
|
} else {
|
||||||
|
// move the parser to the error and return expected
|
||||||
|
skipWhitespace(i - startPos)
|
||||||
|
returnVal = expected
|
||||||
|
loop = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
if (i > length) {
|
||||||
|
loop = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} while (loop)
|
||||||
|
|
||||||
|
return returnVal ? returnVal : null
|
||||||
|
}
|
||||||
|
|
||||||
|
parserInput.autoCommentAbsorb = true
|
||||||
|
parserInput.commentStore = []
|
||||||
|
parserInput.finished = false
|
||||||
|
|
||||||
|
// Same as $(), but don't change the state of the parser,
|
||||||
|
// just return the match.
|
||||||
|
parserInput.peek = tok => {
|
||||||
|
if (typeof tok === 'string') {
|
||||||
|
// https://jsperf.com/string-startswith/21
|
||||||
|
for (let i = 0; i < tok.length; i++) {
|
||||||
|
if (input.charAt(parserInput.i + i) !== tok.charAt(i)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return tok.test(current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Specialization of peek()
|
||||||
|
// TODO remove or change some currentChar calls to peekChar
|
||||||
|
parserInput.peekChar = tok => input.charAt(parserInput.i) === tok
|
||||||
|
|
||||||
|
parserInput.currentChar = () => input.charAt(parserInput.i)
|
||||||
|
|
||||||
|
parserInput.prevChar = () => input.charAt(parserInput.i - 1)
|
||||||
|
|
||||||
|
parserInput.getInput = () => input
|
||||||
|
|
||||||
|
parserInput.peekNotNumeric = () => {
|
||||||
|
const c = input.charCodeAt(parserInput.i)
|
||||||
|
// Is the first char of the dimension 0-9, '.', '+' or '-'
|
||||||
|
return (
|
||||||
|
c > CHARCODE_9 ||
|
||||||
|
c < CHARCODE_PLUS ||
|
||||||
|
c === CHARCODE_FORWARD_SLASH ||
|
||||||
|
c === CHARCODE_COMMA
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
parserInput.start = (str, chunkInput, failFunction) => {
|
||||||
|
input = str
|
||||||
|
parserInput.i = j = currentPos = furthest = 0
|
||||||
|
|
||||||
|
// chunking apparently makes things quicker (but my tests indicate
|
||||||
|
// it might actually make things slower in node at least)
|
||||||
|
// and it is a non-perfect parse - it can't recognise
|
||||||
|
// unquoted urls, meaning it can't distinguish comments
|
||||||
|
// meaning comments with quotes or {}() in them get 'counted'
|
||||||
|
// and then lead to parse errors.
|
||||||
|
// In addition if the chunking chunks in the wrong place we might
|
||||||
|
// not be able to parse a parser statement in one go
|
||||||
|
// this is officially deprecated but can be switched on via an option
|
||||||
|
// in the case it causes too much performance issues.
|
||||||
|
if (chunkInput) {
|
||||||
|
chunks = chunker(str, failFunction)
|
||||||
|
} else {
|
||||||
|
chunks = [str]
|
||||||
|
}
|
||||||
|
|
||||||
|
current = chunks[0]
|
||||||
|
|
||||||
|
skipWhitespace(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
parserInput.end = () => {
|
||||||
|
let message
|
||||||
|
const isFinished = parserInput.i >= input.length
|
||||||
|
|
||||||
|
if (parserInput.i < furthest) {
|
||||||
|
message = furthestPossibleErrorMessage
|
||||||
|
parserInput.i = furthest
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
isFinished,
|
||||||
|
furthest: parserInput.i,
|
||||||
|
furthestPossibleErrorMessage: message,
|
||||||
|
furthestReachedEnd: parserInput.i >= input.length - 1,
|
||||||
|
furthestChar: input[parserInput.i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parserInput
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,180 @@
|
||||||
|
/**
|
||||||
|
* Plugin Manager
|
||||||
|
*/
|
||||||
|
class PluginManager {
|
||||||
|
constructor(less) {
|
||||||
|
this.less = less
|
||||||
|
this.visitors = []
|
||||||
|
this.preProcessors = []
|
||||||
|
this.postProcessors = []
|
||||||
|
this.installedPlugins = []
|
||||||
|
this.fileManagers = []
|
||||||
|
this.iterator = -1
|
||||||
|
this.pluginCache = {}
|
||||||
|
this.Loader = new less.PluginLoader(less)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds all the plugins in the array
|
||||||
|
* @param {Array} plugins
|
||||||
|
*/
|
||||||
|
addPlugins(plugins) {
|
||||||
|
if (plugins) {
|
||||||
|
for (let i = 0; i < plugins.length; i++) {
|
||||||
|
this.addPlugin(plugins[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param plugin
|
||||||
|
* @param {String} filename
|
||||||
|
*/
|
||||||
|
addPlugin(plugin, filename, functionRegistry) {
|
||||||
|
this.installedPlugins.push(plugin)
|
||||||
|
if (filename) {
|
||||||
|
this.pluginCache[filename] = plugin
|
||||||
|
}
|
||||||
|
if (plugin.install) {
|
||||||
|
plugin.install(
|
||||||
|
this.less,
|
||||||
|
this,
|
||||||
|
functionRegistry || this.less.functions.functionRegistry
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param filename
|
||||||
|
*/
|
||||||
|
get(filename) {
|
||||||
|
return this.pluginCache[filename]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a visitor. The visitor object has options on itself to determine
|
||||||
|
* when it should run.
|
||||||
|
* @param visitor
|
||||||
|
*/
|
||||||
|
addVisitor(visitor) {
|
||||||
|
this.visitors.push(visitor)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a pre processor object
|
||||||
|
* @param {object} preProcessor
|
||||||
|
* @param {number} priority - guidelines 1 = before import, 1000 = import, 2000 = after import
|
||||||
|
*/
|
||||||
|
addPreProcessor(preProcessor, priority) {
|
||||||
|
let indexToInsertAt
|
||||||
|
for (
|
||||||
|
indexToInsertAt = 0;
|
||||||
|
indexToInsertAt < this.preProcessors.length;
|
||||||
|
indexToInsertAt++
|
||||||
|
) {
|
||||||
|
if (this.preProcessors[indexToInsertAt].priority >= priority) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.preProcessors.splice(indexToInsertAt, 0, { preProcessor, priority })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a post processor object
|
||||||
|
* @param {object} postProcessor
|
||||||
|
* @param {number} priority - guidelines 1 = before compression, 1000 = compression, 2000 = after compression
|
||||||
|
*/
|
||||||
|
addPostProcessor(postProcessor, priority) {
|
||||||
|
let indexToInsertAt
|
||||||
|
for (
|
||||||
|
indexToInsertAt = 0;
|
||||||
|
indexToInsertAt < this.postProcessors.length;
|
||||||
|
indexToInsertAt++
|
||||||
|
) {
|
||||||
|
if (this.postProcessors[indexToInsertAt].priority >= priority) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.postProcessors.splice(indexToInsertAt, 0, { postProcessor, priority })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param manager
|
||||||
|
*/
|
||||||
|
addFileManager(manager) {
|
||||||
|
this.fileManagers.push(manager)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @returns {Array}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
getPreProcessors() {
|
||||||
|
const preProcessors = []
|
||||||
|
for (let i = 0; i < this.preProcessors.length; i++) {
|
||||||
|
preProcessors.push(this.preProcessors[i].preProcessor)
|
||||||
|
}
|
||||||
|
return preProcessors
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @returns {Array}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
getPostProcessors() {
|
||||||
|
const postProcessors = []
|
||||||
|
for (let i = 0; i < this.postProcessors.length; i++) {
|
||||||
|
postProcessors.push(this.postProcessors[i].postProcessor)
|
||||||
|
}
|
||||||
|
return postProcessors
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @returns {Array}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
getVisitors() {
|
||||||
|
return this.visitors
|
||||||
|
}
|
||||||
|
|
||||||
|
visitor() {
|
||||||
|
const self = this
|
||||||
|
return {
|
||||||
|
first: function () {
|
||||||
|
self.iterator = -1
|
||||||
|
return self.visitors[self.iterator]
|
||||||
|
},
|
||||||
|
get: function () {
|
||||||
|
self.iterator += 1
|
||||||
|
return self.visitors[self.iterator]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @returns {Array}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
getFileManagers() {
|
||||||
|
return this.fileManagers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let pm
|
||||||
|
|
||||||
|
const PluginManagerFactory = function (less, newFactory) {
|
||||||
|
if (newFactory || !pm) {
|
||||||
|
pm = new PluginManager(less)
|
||||||
|
}
|
||||||
|
return pm
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
export default PluginManagerFactory
|
|
@ -0,0 +1,43 @@
|
||||||
|
import * as utils from './utils'
|
||||||
|
|
||||||
|
export default function (environment, ParseTree) {
|
||||||
|
const render = function (input, options, callback) {
|
||||||
|
if (typeof options === 'function') {
|
||||||
|
callback = options
|
||||||
|
options = utils.copyOptions(this.options, {})
|
||||||
|
} else {
|
||||||
|
options = utils.copyOptions(this.options, options || {})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!callback) {
|
||||||
|
const self = this
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
render.call(self, input, options, function (err, output) {
|
||||||
|
if (err) {
|
||||||
|
reject(err)
|
||||||
|
} else {
|
||||||
|
resolve(output)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.parse(input, options, function (err, root, imports, options) {
|
||||||
|
if (err) {
|
||||||
|
return callback(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
let result
|
||||||
|
try {
|
||||||
|
const parseTree = new ParseTree(root, imports)
|
||||||
|
result = parseTree.toCSS(options)
|
||||||
|
} catch (err) {
|
||||||
|
return callback(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(null, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return render
|
||||||
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
export default function (SourceMapOutput, environment) {
|
||||||
|
class SourceMapBuilder {
|
||||||
|
constructor(options) {
|
||||||
|
this.options = options
|
||||||
|
}
|
||||||
|
|
||||||
|
toCSS(rootNode, options, imports) {
|
||||||
|
const sourceMapOutput = new SourceMapOutput({
|
||||||
|
contentsIgnoredCharsMap: imports.contentsIgnoredChars,
|
||||||
|
rootNode,
|
||||||
|
contentsMap: imports.contents,
|
||||||
|
sourceMapFilename: this.options.sourceMapFilename,
|
||||||
|
sourceMapURL: this.options.sourceMapURL,
|
||||||
|
outputFilename: this.options.sourceMapOutputFilename,
|
||||||
|
sourceMapBasepath: this.options.sourceMapBasepath,
|
||||||
|
sourceMapRootpath: this.options.sourceMapRootpath,
|
||||||
|
outputSourceFiles: this.options.outputSourceFiles,
|
||||||
|
sourceMapGenerator: this.options.sourceMapGenerator,
|
||||||
|
sourceMapFileInline: this.options.sourceMapFileInline,
|
||||||
|
disableSourcemapAnnotation: this.options.disableSourcemapAnnotation
|
||||||
|
})
|
||||||
|
|
||||||
|
const css = sourceMapOutput.toCSS(options)
|
||||||
|
this.sourceMap = sourceMapOutput.sourceMap
|
||||||
|
this.sourceMapURL = sourceMapOutput.sourceMapURL
|
||||||
|
if (this.options.sourceMapInputFilename) {
|
||||||
|
this.sourceMapInputFilename = sourceMapOutput.normalizeFilename(
|
||||||
|
this.options.sourceMapInputFilename
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
this.options.sourceMapBasepath !== undefined &&
|
||||||
|
this.sourceMapURL !== undefined
|
||||||
|
) {
|
||||||
|
this.sourceMapURL = sourceMapOutput.removeBasepath(this.sourceMapURL)
|
||||||
|
}
|
||||||
|
return css + this.getCSSAppendage()
|
||||||
|
}
|
||||||
|
|
||||||
|
getCSSAppendage() {
|
||||||
|
let sourceMapURL = this.sourceMapURL
|
||||||
|
if (this.options.sourceMapFileInline) {
|
||||||
|
if (this.sourceMap === undefined) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
sourceMapURL = `data:application/json;base64,${environment.encodeBase64(
|
||||||
|
this.sourceMap
|
||||||
|
)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.options.disableSourcemapAnnotation) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sourceMapURL) {
|
||||||
|
return `/*# sourceMappingURL=${sourceMapURL} */`
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
getExternalSourceMap() {
|
||||||
|
return this.sourceMap
|
||||||
|
}
|
||||||
|
|
||||||
|
setExternalSourceMap(sourceMap) {
|
||||||
|
this.sourceMap = sourceMap
|
||||||
|
}
|
||||||
|
|
||||||
|
isInline() {
|
||||||
|
return this.options.sourceMapFileInline
|
||||||
|
}
|
||||||
|
|
||||||
|
getSourceMapURL() {
|
||||||
|
return this.sourceMapURL
|
||||||
|
}
|
||||||
|
|
||||||
|
getOutputFilename() {
|
||||||
|
return this.options.sourceMapOutputFilename
|
||||||
|
}
|
||||||
|
|
||||||
|
getInputFilename() {
|
||||||
|
return this.sourceMapInputFilename
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return SourceMapBuilder
|
||||||
|
}
|
|
@ -0,0 +1,181 @@
|
||||||
|
export default function (environment) {
|
||||||
|
class SourceMapOutput {
|
||||||
|
constructor(options) {
|
||||||
|
this._css = []
|
||||||
|
this._rootNode = options.rootNode
|
||||||
|
this._contentsMap = options.contentsMap
|
||||||
|
this._contentsIgnoredCharsMap = options.contentsIgnoredCharsMap
|
||||||
|
if (options.sourceMapFilename) {
|
||||||
|
this._sourceMapFilename = options.sourceMapFilename.replace(/\\/g, '/')
|
||||||
|
}
|
||||||
|
this._outputFilename = options.outputFilename
|
||||||
|
this.sourceMapURL = options.sourceMapURL
|
||||||
|
if (options.sourceMapBasepath) {
|
||||||
|
this._sourceMapBasepath = options.sourceMapBasepath.replace(/\\/g, '/')
|
||||||
|
}
|
||||||
|
if (options.sourceMapRootpath) {
|
||||||
|
this._sourceMapRootpath = options.sourceMapRootpath.replace(/\\/g, '/')
|
||||||
|
if (
|
||||||
|
this._sourceMapRootpath.charAt(this._sourceMapRootpath.length - 1) !==
|
||||||
|
'/'
|
||||||
|
) {
|
||||||
|
this._sourceMapRootpath += '/'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this._sourceMapRootpath = ''
|
||||||
|
}
|
||||||
|
this._outputSourceFiles = options.outputSourceFiles
|
||||||
|
this._sourceMapGeneratorConstructor = environment.getSourceMapGenerator()
|
||||||
|
|
||||||
|
this._lineNumber = 0
|
||||||
|
this._column = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
removeBasepath(path) {
|
||||||
|
if (
|
||||||
|
this._sourceMapBasepath &&
|
||||||
|
path.indexOf(this._sourceMapBasepath) === 0
|
||||||
|
) {
|
||||||
|
path = path.substring(this._sourceMapBasepath.length)
|
||||||
|
if (path.charAt(0) === '\\' || path.charAt(0) === '/') {
|
||||||
|
path = path.substring(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizeFilename(filename) {
|
||||||
|
filename = filename.replace(/\\/g, '/')
|
||||||
|
filename = this.removeBasepath(filename)
|
||||||
|
return (this._sourceMapRootpath || '') + filename
|
||||||
|
}
|
||||||
|
|
||||||
|
add(chunk, fileInfo, index, mapLines) {
|
||||||
|
// ignore adding empty strings
|
||||||
|
if (!chunk) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let lines, sourceLines, columns, sourceColumns, i
|
||||||
|
|
||||||
|
if (fileInfo && fileInfo.filename) {
|
||||||
|
let inputSource = this._contentsMap[fileInfo.filename]
|
||||||
|
|
||||||
|
// remove vars/banner added to the top of the file
|
||||||
|
if (this._contentsIgnoredCharsMap[fileInfo.filename]) {
|
||||||
|
// adjust the index
|
||||||
|
index -= this._contentsIgnoredCharsMap[fileInfo.filename]
|
||||||
|
if (index < 0) {
|
||||||
|
index = 0
|
||||||
|
}
|
||||||
|
// adjust the source
|
||||||
|
inputSource = inputSource.slice(
|
||||||
|
this._contentsIgnoredCharsMap[fileInfo.filename]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ignore empty content, or failsafe
|
||||||
|
* if contents map is incorrect
|
||||||
|
*/
|
||||||
|
if (inputSource === undefined) {
|
||||||
|
this._css.push(chunk)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
inputSource = inputSource.substring(0, index)
|
||||||
|
sourceLines = inputSource.split('\n')
|
||||||
|
sourceColumns = sourceLines[sourceLines.length - 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
lines = chunk.split('\n')
|
||||||
|
columns = lines[lines.length - 1]
|
||||||
|
|
||||||
|
if (fileInfo && fileInfo.filename) {
|
||||||
|
if (!mapLines) {
|
||||||
|
this._sourceMapGenerator.addMapping({
|
||||||
|
generated: { line: this._lineNumber + 1, column: this._column },
|
||||||
|
original: {
|
||||||
|
line: sourceLines.length,
|
||||||
|
column: sourceColumns.length
|
||||||
|
},
|
||||||
|
source: this.normalizeFilename(fileInfo.filename)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
for (i = 0; i < lines.length; i++) {
|
||||||
|
this._sourceMapGenerator.addMapping({
|
||||||
|
generated: {
|
||||||
|
line: this._lineNumber + i + 1,
|
||||||
|
column: i === 0 ? this._column : 0
|
||||||
|
},
|
||||||
|
original: {
|
||||||
|
line: sourceLines.length + i,
|
||||||
|
column: i === 0 ? sourceColumns.length : 0
|
||||||
|
},
|
||||||
|
source: this.normalizeFilename(fileInfo.filename)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lines.length === 1) {
|
||||||
|
this._column += columns.length
|
||||||
|
} else {
|
||||||
|
this._lineNumber += lines.length - 1
|
||||||
|
this._column = columns.length
|
||||||
|
}
|
||||||
|
|
||||||
|
this._css.push(chunk)
|
||||||
|
}
|
||||||
|
|
||||||
|
isEmpty() {
|
||||||
|
return this._css.length === 0
|
||||||
|
}
|
||||||
|
|
||||||
|
toCSS(context) {
|
||||||
|
this._sourceMapGenerator = new this._sourceMapGeneratorConstructor({
|
||||||
|
file: this._outputFilename,
|
||||||
|
sourceRoot: null
|
||||||
|
})
|
||||||
|
|
||||||
|
if (this._outputSourceFiles) {
|
||||||
|
for (const filename in this._contentsMap) {
|
||||||
|
// eslint-disable-next-line no-prototype-builtins
|
||||||
|
if (this._contentsMap.hasOwnProperty(filename)) {
|
||||||
|
let source = this._contentsMap[filename]
|
||||||
|
if (this._contentsIgnoredCharsMap[filename]) {
|
||||||
|
source = source.slice(this._contentsIgnoredCharsMap[filename])
|
||||||
|
}
|
||||||
|
this._sourceMapGenerator.setSourceContent(
|
||||||
|
this.normalizeFilename(filename),
|
||||||
|
source
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._rootNode.genCSS(context, this)
|
||||||
|
|
||||||
|
if (this._css.length > 0) {
|
||||||
|
let sourceMapURL
|
||||||
|
const sourceMapContent = JSON.stringify(
|
||||||
|
this._sourceMapGenerator.toJSON()
|
||||||
|
)
|
||||||
|
|
||||||
|
if (this.sourceMapURL) {
|
||||||
|
sourceMapURL = this.sourceMapURL
|
||||||
|
} else if (this._sourceMapFilename) {
|
||||||
|
sourceMapURL = this._sourceMapFilename
|
||||||
|
}
|
||||||
|
this.sourceMapURL = sourceMapURL
|
||||||
|
|
||||||
|
this.sourceMap = sourceMapContent
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._css.join('')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return SourceMapOutput
|
||||||
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
import contexts from './contexts'
|
||||||
|
import visitor from './visitors'
|
||||||
|
import tree from './tree'
|
||||||
|
|
||||||
|
export default function (root, options) {
|
||||||
|
options = options || {}
|
||||||
|
let evaldRoot
|
||||||
|
let variables = options.variables
|
||||||
|
const evalEnv = new contexts.Eval(options)
|
||||||
|
|
||||||
|
//
|
||||||
|
// Allows setting variables with a hash, so:
|
||||||
|
//
|
||||||
|
// `{ color: new tree.Color('#f01') }` will become:
|
||||||
|
//
|
||||||
|
// new tree.Declaration('@color',
|
||||||
|
// new tree.Value([
|
||||||
|
// new tree.Expression([
|
||||||
|
// new tree.Color('#f01')
|
||||||
|
// ])
|
||||||
|
// ])
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
if (typeof variables === 'object' && !Array.isArray(variables)) {
|
||||||
|
variables = Object.keys(variables).map(function (k) {
|
||||||
|
let value = variables[k]
|
||||||
|
|
||||||
|
if (!(value instanceof tree.Value)) {
|
||||||
|
if (!(value instanceof tree.Expression)) {
|
||||||
|
value = new tree.Expression([value])
|
||||||
|
}
|
||||||
|
value = new tree.Value([value])
|
||||||
|
}
|
||||||
|
return new tree.Declaration(`@${k}`, value, false, null, 0)
|
||||||
|
})
|
||||||
|
evalEnv.frames = [new tree.Ruleset(null, variables)]
|
||||||
|
}
|
||||||
|
|
||||||
|
const visitors = [
|
||||||
|
new visitor.JoinSelectorVisitor(),
|
||||||
|
new visitor.MarkVisibleSelectorsVisitor(true),
|
||||||
|
new visitor.ExtendVisitor(),
|
||||||
|
new visitor.ToCSSVisitor({ compress: Boolean(options.compress) })
|
||||||
|
]
|
||||||
|
|
||||||
|
const preEvalVisitors = []
|
||||||
|
let v
|
||||||
|
let visitorIterator
|
||||||
|
|
||||||
|
/**
|
||||||
|
* first() / get() allows visitors to be added while visiting
|
||||||
|
*
|
||||||
|
* @todo Add scoping for visitors just like functions for @plugin; right now they're global
|
||||||
|
*/
|
||||||
|
if (options.pluginManager) {
|
||||||
|
visitorIterator = options.pluginManager.visitor()
|
||||||
|
for (let i = 0; i < 2; i++) {
|
||||||
|
visitorIterator.first()
|
||||||
|
while ((v = visitorIterator.get())) {
|
||||||
|
if (v.isPreEvalVisitor) {
|
||||||
|
if (i === 0 || preEvalVisitors.indexOf(v) === -1) {
|
||||||
|
preEvalVisitors.push(v)
|
||||||
|
v.run(root)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (i === 0 || visitors.indexOf(v) === -1) {
|
||||||
|
if (v.isPreVisitor) {
|
||||||
|
visitors.unshift(v)
|
||||||
|
} else {
|
||||||
|
visitors.push(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
evaldRoot = root.eval(evalEnv)
|
||||||
|
|
||||||
|
for (let i = 0; i < visitors.length; i++) {
|
||||||
|
visitors[i].run(evaldRoot)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run any remaining visitors added after eval pass
|
||||||
|
if (options.pluginManager) {
|
||||||
|
visitorIterator.first()
|
||||||
|
while ((v = visitorIterator.get())) {
|
||||||
|
if (visitors.indexOf(v) === -1 && preEvalVisitors.indexOf(v) === -1) {
|
||||||
|
v.run(evaldRoot)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return evaldRoot
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
import Node from './node'
|
||||||
|
|
||||||
|
const Anonymous = function (
|
||||||
|
value,
|
||||||
|
index,
|
||||||
|
currentFileInfo,
|
||||||
|
mapLines,
|
||||||
|
rulesetLike,
|
||||||
|
visibilityInfo
|
||||||
|
) {
|
||||||
|
this.value = value
|
||||||
|
this._index = index
|
||||||
|
this._fileInfo = currentFileInfo
|
||||||
|
this.mapLines = mapLines
|
||||||
|
this.rulesetLike = typeof rulesetLike === 'undefined' ? false : rulesetLike
|
||||||
|
this.allowRoot = true
|
||||||
|
this.copyVisibilityInfo(visibilityInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
Anonymous.prototype = Object.assign(new Node(), {
|
||||||
|
type: 'Anonymous',
|
||||||
|
eval() {
|
||||||
|
return new Anonymous(
|
||||||
|
this.value,
|
||||||
|
this._index,
|
||||||
|
this._fileInfo,
|
||||||
|
this.mapLines,
|
||||||
|
this.rulesetLike,
|
||||||
|
this.visibilityInfo()
|
||||||
|
)
|
||||||
|
},
|
||||||
|
compare(other) {
|
||||||
|
return other.toCSS && this.toCSS() === other.toCSS() ? 0 : undefined
|
||||||
|
},
|
||||||
|
isRulesetLike() {
|
||||||
|
return this.rulesetLike
|
||||||
|
},
|
||||||
|
genCSS(context, output) {
|
||||||
|
this.nodeVisible = Boolean(this.value)
|
||||||
|
if (this.nodeVisible) {
|
||||||
|
output.add(this.value, this._fileInfo, this._index, this.mapLines)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default Anonymous
|
|
@ -0,0 +1,32 @@
|
||||||
|
import Node from './node'
|
||||||
|
|
||||||
|
const Assignment = function (key, val) {
|
||||||
|
this.key = key
|
||||||
|
this.value = val
|
||||||
|
}
|
||||||
|
|
||||||
|
Assignment.prototype = Object.assign(new Node(), {
|
||||||
|
type: 'Assignment',
|
||||||
|
|
||||||
|
accept(visitor) {
|
||||||
|
this.value = visitor.visit(this.value)
|
||||||
|
},
|
||||||
|
|
||||||
|
eval(context) {
|
||||||
|
if (this.value.eval) {
|
||||||
|
return new Assignment(this.key, this.value.eval(context))
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
},
|
||||||
|
|
||||||
|
genCSS(context, output) {
|
||||||
|
output.add(`${this.key}=`)
|
||||||
|
if (this.value.genCSS) {
|
||||||
|
this.value.genCSS(context, output)
|
||||||
|
} else {
|
||||||
|
output.add(this.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default Assignment
|
|
@ -0,0 +1,177 @@
|
||||||
|
import Node from './node'
|
||||||
|
import Selector from './selector'
|
||||||
|
import Ruleset from './ruleset'
|
||||||
|
import Anonymous from './anonymous'
|
||||||
|
|
||||||
|
const AtRule = function (
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
rules,
|
||||||
|
index,
|
||||||
|
currentFileInfo,
|
||||||
|
debugInfo,
|
||||||
|
isRooted,
|
||||||
|
visibilityInfo
|
||||||
|
) {
|
||||||
|
let i
|
||||||
|
|
||||||
|
this.name = name
|
||||||
|
this.value =
|
||||||
|
value instanceof Node ? value : value ? new Anonymous(value) : value
|
||||||
|
if (rules) {
|
||||||
|
if (Array.isArray(rules)) {
|
||||||
|
this.rules = rules
|
||||||
|
} else {
|
||||||
|
this.rules = [rules]
|
||||||
|
this.rules[0].selectors = new Selector(
|
||||||
|
[],
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
index,
|
||||||
|
currentFileInfo
|
||||||
|
).createEmptySelectors()
|
||||||
|
}
|
||||||
|
for (i = 0; i < this.rules.length; i++) {
|
||||||
|
this.rules[i].allowImports = true
|
||||||
|
}
|
||||||
|
this.setParent(this.rules, this)
|
||||||
|
}
|
||||||
|
this._index = index
|
||||||
|
this._fileInfo = currentFileInfo
|
||||||
|
this.debugInfo = debugInfo
|
||||||
|
this.isRooted = isRooted || false
|
||||||
|
this.copyVisibilityInfo(visibilityInfo)
|
||||||
|
this.allowRoot = true
|
||||||
|
}
|
||||||
|
|
||||||
|
AtRule.prototype = Object.assign(new Node(), {
|
||||||
|
type: 'AtRule',
|
||||||
|
accept(visitor) {
|
||||||
|
const value = this.value,
|
||||||
|
rules = this.rules
|
||||||
|
if (rules) {
|
||||||
|
this.rules = visitor.visitArray(rules)
|
||||||
|
}
|
||||||
|
if (value) {
|
||||||
|
this.value = visitor.visit(value)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
isRulesetLike() {
|
||||||
|
return this.rules || !this.isCharset()
|
||||||
|
},
|
||||||
|
|
||||||
|
isCharset() {
|
||||||
|
return '@charset' === this.name
|
||||||
|
},
|
||||||
|
|
||||||
|
genCSS(context, output) {
|
||||||
|
const value = this.value,
|
||||||
|
rules = this.rules
|
||||||
|
output.add(this.name, this.fileInfo(), this.getIndex())
|
||||||
|
if (value) {
|
||||||
|
output.add(' ')
|
||||||
|
value.genCSS(context, output)
|
||||||
|
}
|
||||||
|
if (rules) {
|
||||||
|
this.outputRuleset(context, output, rules)
|
||||||
|
} else {
|
||||||
|
output.add(';')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
eval(context) {
|
||||||
|
let mediaPathBackup,
|
||||||
|
mediaBlocksBackup,
|
||||||
|
value = this.value,
|
||||||
|
rules = this.rules
|
||||||
|
|
||||||
|
// media stored inside other atrule should not bubble over it
|
||||||
|
// backpup media bubbling information
|
||||||
|
mediaPathBackup = context.mediaPath
|
||||||
|
mediaBlocksBackup = context.mediaBlocks
|
||||||
|
// deleted media bubbling information
|
||||||
|
context.mediaPath = []
|
||||||
|
context.mediaBlocks = []
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
value = value.eval(context)
|
||||||
|
}
|
||||||
|
if (rules) {
|
||||||
|
// assuming that there is only one rule at this point - that is how parser constructs the rule
|
||||||
|
rules = [rules[0].eval(context)]
|
||||||
|
rules[0].root = true
|
||||||
|
}
|
||||||
|
// restore media bubbling information
|
||||||
|
context.mediaPath = mediaPathBackup
|
||||||
|
context.mediaBlocks = mediaBlocksBackup
|
||||||
|
|
||||||
|
return new AtRule(
|
||||||
|
this.name,
|
||||||
|
value,
|
||||||
|
rules,
|
||||||
|
this.getIndex(),
|
||||||
|
this.fileInfo(),
|
||||||
|
this.debugInfo,
|
||||||
|
this.isRooted,
|
||||||
|
this.visibilityInfo()
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
variable(name) {
|
||||||
|
if (this.rules) {
|
||||||
|
// assuming that there is only one rule at this point - that is how parser constructs the rule
|
||||||
|
return Ruleset.prototype.variable.call(this.rules[0], name)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
find() {
|
||||||
|
if (this.rules) {
|
||||||
|
// assuming that there is only one rule at this point - that is how parser constructs the rule
|
||||||
|
return Ruleset.prototype.find.apply(this.rules[0], arguments)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
rulesets() {
|
||||||
|
if (this.rules) {
|
||||||
|
// assuming that there is only one rule at this point - that is how parser constructs the rule
|
||||||
|
return Ruleset.prototype.rulesets.apply(this.rules[0])
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
outputRuleset(context, output, rules) {
|
||||||
|
const ruleCnt = rules.length
|
||||||
|
let i
|
||||||
|
context.tabLevel = (context.tabLevel | 0) + 1
|
||||||
|
|
||||||
|
// Compressed
|
||||||
|
if (context.compress) {
|
||||||
|
output.add('{')
|
||||||
|
for (i = 0; i < ruleCnt; i++) {
|
||||||
|
rules[i].genCSS(context, output)
|
||||||
|
}
|
||||||
|
output.add('}')
|
||||||
|
context.tabLevel--
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-compressed
|
||||||
|
const tabSetStr = `\n${Array(context.tabLevel).join(' ')}`,
|
||||||
|
tabRuleStr = `${tabSetStr} `
|
||||||
|
if (!ruleCnt) {
|
||||||
|
output.add(` {${tabSetStr}}`)
|
||||||
|
} else {
|
||||||
|
output.add(` {${tabRuleStr}`)
|
||||||
|
rules[0].genCSS(context, output)
|
||||||
|
for (i = 1; i < ruleCnt; i++) {
|
||||||
|
output.add(tabRuleStr)
|
||||||
|
rules[i].genCSS(context, output)
|
||||||
|
}
|
||||||
|
output.add(`${tabSetStr}}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
context.tabLevel--
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default AtRule
|
|
@ -0,0 +1,42 @@
|
||||||
|
import Node from './node'
|
||||||
|
|
||||||
|
const Attribute = function (key, op, value, cif) {
|
||||||
|
this.key = key
|
||||||
|
this.op = op
|
||||||
|
this.value = value
|
||||||
|
this.cif = cif
|
||||||
|
}
|
||||||
|
|
||||||
|
Attribute.prototype = Object.assign(new Node(), {
|
||||||
|
type: 'Attribute',
|
||||||
|
|
||||||
|
eval(context) {
|
||||||
|
return new Attribute(
|
||||||
|
this.key.eval ? this.key.eval(context) : this.key,
|
||||||
|
this.op,
|
||||||
|
this.value && this.value.eval ? this.value.eval(context) : this.value,
|
||||||
|
this.cif
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
genCSS(context, output) {
|
||||||
|
output.add(this.toCSS(context))
|
||||||
|
},
|
||||||
|
|
||||||
|
toCSS(context) {
|
||||||
|
let value = this.key.toCSS ? this.key.toCSS(context) : this.key
|
||||||
|
|
||||||
|
if (this.op) {
|
||||||
|
value += this.op
|
||||||
|
value += this.value.toCSS ? this.value.toCSS(context) : this.value
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.cif) {
|
||||||
|
value = value + ' ' + this.cif
|
||||||
|
}
|
||||||
|
|
||||||
|
return `[${value}]`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default Attribute
|
|
@ -0,0 +1,118 @@
|
||||||
|
import Node from './node'
|
||||||
|
import Anonymous from './anonymous'
|
||||||
|
import FunctionCaller from '../functions/function-caller'
|
||||||
|
|
||||||
|
//
|
||||||
|
// A function call node.
|
||||||
|
//
|
||||||
|
const Call = function (name, args, index, currentFileInfo) {
|
||||||
|
this.name = name
|
||||||
|
this.args = args
|
||||||
|
this.calc = name === 'calc'
|
||||||
|
this._index = index
|
||||||
|
this._fileInfo = currentFileInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
Call.prototype = Object.assign(new Node(), {
|
||||||
|
type: 'Call',
|
||||||
|
|
||||||
|
accept(visitor) {
|
||||||
|
if (this.args) {
|
||||||
|
this.args = visitor.visitArray(this.args)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
//
|
||||||
|
// When evaluating a function call,
|
||||||
|
// we either find the function in the functionRegistry,
|
||||||
|
// in which case we call it, passing the evaluated arguments,
|
||||||
|
// if this returns null or we cannot find the function, we
|
||||||
|
// simply print it out as it appeared originally [2].
|
||||||
|
//
|
||||||
|
// The reason why we evaluate the arguments, is in the case where
|
||||||
|
// we try to pass a variable to a function, like: `saturate(@color)`.
|
||||||
|
// The function should receive the value, not the variable.
|
||||||
|
//
|
||||||
|
eval(context) {
|
||||||
|
/**
|
||||||
|
* Turn off math for calc(), and switch back on for evaluating nested functions
|
||||||
|
*/
|
||||||
|
const currentMathContext = context.mathOn
|
||||||
|
context.mathOn = !this.calc
|
||||||
|
if (this.calc || context.inCalc) {
|
||||||
|
context.enterCalc()
|
||||||
|
}
|
||||||
|
|
||||||
|
const exitCalc = () => {
|
||||||
|
if (this.calc || context.inCalc) {
|
||||||
|
context.exitCalc()
|
||||||
|
}
|
||||||
|
context.mathOn = currentMathContext
|
||||||
|
}
|
||||||
|
|
||||||
|
let result
|
||||||
|
const funcCaller = new FunctionCaller(
|
||||||
|
this.name,
|
||||||
|
context,
|
||||||
|
this.getIndex(),
|
||||||
|
this.fileInfo()
|
||||||
|
)
|
||||||
|
|
||||||
|
if (funcCaller.isValid()) {
|
||||||
|
try {
|
||||||
|
result = funcCaller.call(this.args)
|
||||||
|
exitCalc()
|
||||||
|
} catch (e) {
|
||||||
|
// eslint-disable-next-line no-prototype-builtins
|
||||||
|
if (e.hasOwnProperty('line') && e.hasOwnProperty('column')) {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
throw {
|
||||||
|
type: e.type || 'Runtime',
|
||||||
|
message: `Error evaluating function \`${this.name}\`${
|
||||||
|
e.message ? `: ${e.message}` : ''
|
||||||
|
}`,
|
||||||
|
index: this.getIndex(),
|
||||||
|
filename: this.fileInfo().filename,
|
||||||
|
line: e.lineNumber,
|
||||||
|
column: e.columnNumber
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result !== null && result !== undefined) {
|
||||||
|
// Results that that are not nodes are cast as Anonymous nodes
|
||||||
|
// Falsy values or booleans are returned as empty nodes
|
||||||
|
if (!(result instanceof Node)) {
|
||||||
|
if (!result || result === true) {
|
||||||
|
result = new Anonymous(null)
|
||||||
|
} else {
|
||||||
|
result = new Anonymous(result.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result._index = this._index
|
||||||
|
result._fileInfo = this._fileInfo
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = this.args.map(a => a.eval(context))
|
||||||
|
exitCalc()
|
||||||
|
|
||||||
|
return new Call(this.name, args, this.getIndex(), this.fileInfo())
|
||||||
|
},
|
||||||
|
|
||||||
|
genCSS(context, output) {
|
||||||
|
output.add(`${this.name}(`, this.fileInfo(), this.getIndex())
|
||||||
|
|
||||||
|
for (let i = 0; i < this.args.length; i++) {
|
||||||
|
this.args[i].genCSS(context, output)
|
||||||
|
if (i + 1 < this.args.length) {
|
||||||
|
output.add(', ')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output.add(')')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default Call
|
|
@ -0,0 +1,272 @@
|
||||||
|
import Node from './node'
|
||||||
|
import colors from '../data/colors'
|
||||||
|
|
||||||
|
//
|
||||||
|
// RGB Colors - #ff0014, #eee
|
||||||
|
//
|
||||||
|
const Color = function (rgb, a, originalForm) {
|
||||||
|
const self = this
|
||||||
|
//
|
||||||
|
// The end goal here, is to parse the arguments
|
||||||
|
// into an integer triplet, such as `128, 255, 0`
|
||||||
|
//
|
||||||
|
// This facilitates operations and conversions.
|
||||||
|
//
|
||||||
|
if (Array.isArray(rgb)) {
|
||||||
|
this.rgb = rgb
|
||||||
|
} else if (rgb.length >= 6) {
|
||||||
|
this.rgb = []
|
||||||
|
rgb.match(/.{2}/g).map(function (c, i) {
|
||||||
|
if (i < 3) {
|
||||||
|
self.rgb.push(parseInt(c, 16))
|
||||||
|
} else {
|
||||||
|
self.alpha = parseInt(c, 16) / 255
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.rgb = []
|
||||||
|
rgb.split('').map(function (c, i) {
|
||||||
|
if (i < 3) {
|
||||||
|
self.rgb.push(parseInt(c + c, 16))
|
||||||
|
} else {
|
||||||
|
self.alpha = parseInt(c + c, 16) / 255
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.alpha = this.alpha || (typeof a === 'number' ? a : 1)
|
||||||
|
if (typeof originalForm !== 'undefined') {
|
||||||
|
this.value = originalForm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Color.prototype = Object.assign(new Node(), {
|
||||||
|
type: 'Color',
|
||||||
|
|
||||||
|
luma() {
|
||||||
|
let r = this.rgb[0] / 255,
|
||||||
|
g = this.rgb[1] / 255,
|
||||||
|
b = this.rgb[2] / 255
|
||||||
|
|
||||||
|
r = r <= 0.03928 ? r / 12.92 : Math.pow((r + 0.055) / 1.055, 2.4)
|
||||||
|
g = g <= 0.03928 ? g / 12.92 : Math.pow((g + 0.055) / 1.055, 2.4)
|
||||||
|
b = b <= 0.03928 ? b / 12.92 : Math.pow((b + 0.055) / 1.055, 2.4)
|
||||||
|
|
||||||
|
return 0.2126 * r + 0.7152 * g + 0.0722 * b
|
||||||
|
},
|
||||||
|
|
||||||
|
genCSS(context, output) {
|
||||||
|
output.add(this.toCSS(context))
|
||||||
|
},
|
||||||
|
|
||||||
|
toCSS(context, doNotCompress) {
|
||||||
|
const compress = context && context.compress && !doNotCompress
|
||||||
|
let color
|
||||||
|
let alpha
|
||||||
|
let colorFunction
|
||||||
|
let args = []
|
||||||
|
|
||||||
|
// `value` is set if this color was originally
|
||||||
|
// converted from a named color string so we need
|
||||||
|
// to respect this and try to output named color too.
|
||||||
|
alpha = this.fround(context, this.alpha)
|
||||||
|
|
||||||
|
if (this.value) {
|
||||||
|
if (this.value.indexOf('rgb') === 0) {
|
||||||
|
if (alpha < 1) {
|
||||||
|
colorFunction = 'rgba'
|
||||||
|
}
|
||||||
|
} else if (this.value.indexOf('hsl') === 0) {
|
||||||
|
if (alpha < 1) {
|
||||||
|
colorFunction = 'hsla'
|
||||||
|
} else {
|
||||||
|
colorFunction = 'hsl'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return this.value
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (alpha < 1) {
|
||||||
|
colorFunction = 'rgba'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (colorFunction) {
|
||||||
|
case 'rgba':
|
||||||
|
args = this.rgb
|
||||||
|
.map(function (c) {
|
||||||
|
return clamp(Math.round(c), 255)
|
||||||
|
})
|
||||||
|
.concat(clamp(alpha, 1))
|
||||||
|
break
|
||||||
|
case 'hsla':
|
||||||
|
args.push(clamp(alpha, 1))
|
||||||
|
// eslint-disable-next-line no-fallthrough
|
||||||
|
case 'hsl':
|
||||||
|
color = this.toHSL()
|
||||||
|
args = [
|
||||||
|
this.fround(context, color.h),
|
||||||
|
`${this.fround(context, color.s * 100)}%`,
|
||||||
|
`${this.fround(context, color.l * 100)}%`
|
||||||
|
].concat(args)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (colorFunction) {
|
||||||
|
// Values are capped between `0` and `255`, rounded and zero-padded.
|
||||||
|
return `${colorFunction}(${args.join(`,${compress ? '' : ' '}`)})`
|
||||||
|
}
|
||||||
|
|
||||||
|
color = this.toRGB()
|
||||||
|
|
||||||
|
if (compress) {
|
||||||
|
const splitcolor = color.split('')
|
||||||
|
|
||||||
|
// Convert color to short format
|
||||||
|
if (
|
||||||
|
splitcolor[1] === splitcolor[2] &&
|
||||||
|
splitcolor[3] === splitcolor[4] &&
|
||||||
|
splitcolor[5] === splitcolor[6]
|
||||||
|
) {
|
||||||
|
color = `#${splitcolor[1]}${splitcolor[3]}${splitcolor[5]}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return color
|
||||||
|
},
|
||||||
|
|
||||||
|
//
|
||||||
|
// Operations have to be done per-channel, if not,
|
||||||
|
// channels will spill onto each other. Once we have
|
||||||
|
// our result, in the form of an integer triplet,
|
||||||
|
// we create a new Color node to hold the result.
|
||||||
|
//
|
||||||
|
operate(context, op, other) {
|
||||||
|
const rgb = new Array(3)
|
||||||
|
const alpha = this.alpha * (1 - other.alpha) + other.alpha
|
||||||
|
for (let c = 0; c < 3; c++) {
|
||||||
|
rgb[c] = this._operate(context, op, this.rgb[c], other.rgb[c])
|
||||||
|
}
|
||||||
|
return new Color(rgb, alpha)
|
||||||
|
},
|
||||||
|
|
||||||
|
toRGB() {
|
||||||
|
return toHex(this.rgb)
|
||||||
|
},
|
||||||
|
|
||||||
|
toHSL() {
|
||||||
|
const r = this.rgb[0] / 255,
|
||||||
|
g = this.rgb[1] / 255,
|
||||||
|
b = this.rgb[2] / 255,
|
||||||
|
a = this.alpha
|
||||||
|
|
||||||
|
const max = Math.max(r, g, b),
|
||||||
|
min = Math.min(r, g, b)
|
||||||
|
let h
|
||||||
|
let s
|
||||||
|
const l = (max + min) / 2
|
||||||
|
const d = max - min
|
||||||
|
|
||||||
|
if (max === min) {
|
||||||
|
h = s = 0
|
||||||
|
} else {
|
||||||
|
s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
|
||||||
|
|
||||||
|
switch (max) {
|
||||||
|
case r:
|
||||||
|
h = (g - b) / d + (g < b ? 6 : 0)
|
||||||
|
break
|
||||||
|
case g:
|
||||||
|
h = (b - r) / d + 2
|
||||||
|
break
|
||||||
|
case b:
|
||||||
|
h = (r - g) / d + 4
|
||||||
|
break
|
||||||
|
}
|
||||||
|
h /= 6
|
||||||
|
}
|
||||||
|
return { h: h * 360, s, l, a }
|
||||||
|
},
|
||||||
|
|
||||||
|
// Adapted from http://mjijackson.com/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript
|
||||||
|
toHSV() {
|
||||||
|
const r = this.rgb[0] / 255,
|
||||||
|
g = this.rgb[1] / 255,
|
||||||
|
b = this.rgb[2] / 255,
|
||||||
|
a = this.alpha
|
||||||
|
|
||||||
|
const max = Math.max(r, g, b),
|
||||||
|
min = Math.min(r, g, b)
|
||||||
|
let h
|
||||||
|
let s
|
||||||
|
const v = max
|
||||||
|
|
||||||
|
const d = max - min
|
||||||
|
if (max === 0) {
|
||||||
|
s = 0
|
||||||
|
} else {
|
||||||
|
s = d / max
|
||||||
|
}
|
||||||
|
|
||||||
|
if (max === min) {
|
||||||
|
h = 0
|
||||||
|
} else {
|
||||||
|
switch (max) {
|
||||||
|
case r:
|
||||||
|
h = (g - b) / d + (g < b ? 6 : 0)
|
||||||
|
break
|
||||||
|
case g:
|
||||||
|
h = (b - r) / d + 2
|
||||||
|
break
|
||||||
|
case b:
|
||||||
|
h = (r - g) / d + 4
|
||||||
|
break
|
||||||
|
}
|
||||||
|
h /= 6
|
||||||
|
}
|
||||||
|
return { h: h * 360, s, v, a }
|
||||||
|
},
|
||||||
|
|
||||||
|
toARGB() {
|
||||||
|
return toHex([this.alpha * 255].concat(this.rgb))
|
||||||
|
},
|
||||||
|
|
||||||
|
compare(x) {
|
||||||
|
return x.rgb &&
|
||||||
|
x.rgb[0] === this.rgb[0] &&
|
||||||
|
x.rgb[1] === this.rgb[1] &&
|
||||||
|
x.rgb[2] === this.rgb[2] &&
|
||||||
|
x.alpha === this.alpha
|
||||||
|
? 0
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
Color.fromKeyword = function (keyword) {
|
||||||
|
let c
|
||||||
|
const key = keyword.toLowerCase()
|
||||||
|
// eslint-disable-next-line no-prototype-builtins
|
||||||
|
if (colors.hasOwnProperty(key)) {
|
||||||
|
c = new Color(colors[key].slice(1))
|
||||||
|
} else if (key === 'transparent') {
|
||||||
|
c = new Color([0, 0, 0], 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c) {
|
||||||
|
c.value = keyword
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clamp(v, max) {
|
||||||
|
return Math.min(Math.max(v, 0), max)
|
||||||
|
}
|
||||||
|
|
||||||
|
function toHex(v) {
|
||||||
|
return `#${v
|
||||||
|
.map(function (c) {
|
||||||
|
c = clamp(Math.round(c), 255)
|
||||||
|
return (c < 16 ? '0' : '') + c.toString(16)
|
||||||
|
})
|
||||||
|
.join('')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Color
|
|
@ -0,0 +1,28 @@
|
||||||
|
import Node from './node'
|
||||||
|
const _noSpaceCombinators = {
|
||||||
|
'': true,
|
||||||
|
' ': true,
|
||||||
|
'|': true
|
||||||
|
}
|
||||||
|
|
||||||
|
const Combinator = function (value) {
|
||||||
|
if (value === ' ') {
|
||||||
|
this.value = ' '
|
||||||
|
this.emptyOrWhitespace = true
|
||||||
|
} else {
|
||||||
|
this.value = value ? value.trim() : ''
|
||||||
|
this.emptyOrWhitespace = this.value === ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Combinator.prototype = Object.assign(new Node(), {
|
||||||
|
type: 'Combinator',
|
||||||
|
|
||||||
|
genCSS(context, output) {
|
||||||
|
const spaceOrEmpty =
|
||||||
|
context.compress || _noSpaceCombinators[this.value] ? '' : ' '
|
||||||
|
output.add(spaceOrEmpty + this.value + spaceOrEmpty)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default Combinator
|
|
@ -0,0 +1,28 @@
|
||||||
|
import Node from './node'
|
||||||
|
import getDebugInfo from './debug-info'
|
||||||
|
|
||||||
|
const Comment = function (value, isLineComment, index, currentFileInfo) {
|
||||||
|
this.value = value
|
||||||
|
this.isLineComment = isLineComment
|
||||||
|
this._index = index
|
||||||
|
this._fileInfo = currentFileInfo
|
||||||
|
this.allowRoot = true
|
||||||
|
}
|
||||||
|
|
||||||
|
Comment.prototype = Object.assign(new Node(), {
|
||||||
|
type: 'Comment',
|
||||||
|
|
||||||
|
genCSS(context, output) {
|
||||||
|
if (this.debugInfo) {
|
||||||
|
output.add(getDebugInfo(context, this), this.fileInfo(), this.getIndex())
|
||||||
|
}
|
||||||
|
output.add(this.value)
|
||||||
|
},
|
||||||
|
|
||||||
|
isSilent(context) {
|
||||||
|
const isCompressed = context.compress && this.value[2] !== '!'
|
||||||
|
return this.isLineComment || isCompressed
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default Comment
|
|
@ -0,0 +1,44 @@
|
||||||
|
import Node from './node'
|
||||||
|
|
||||||
|
const Condition = function (op, l, r, i, negate) {
|
||||||
|
this.op = op.trim()
|
||||||
|
this.lvalue = l
|
||||||
|
this.rvalue = r
|
||||||
|
this._index = i
|
||||||
|
this.negate = negate
|
||||||
|
}
|
||||||
|
|
||||||
|
Condition.prototype = Object.assign(new Node(), {
|
||||||
|
type: 'Condition',
|
||||||
|
|
||||||
|
accept(visitor) {
|
||||||
|
this.lvalue = visitor.visit(this.lvalue)
|
||||||
|
this.rvalue = visitor.visit(this.rvalue)
|
||||||
|
},
|
||||||
|
|
||||||
|
eval(context) {
|
||||||
|
const result = (function (op, a, b) {
|
||||||
|
switch (op) {
|
||||||
|
case 'and':
|
||||||
|
return a && b
|
||||||
|
case 'or':
|
||||||
|
return a || b
|
||||||
|
default:
|
||||||
|
switch (Node.compare(a, b)) {
|
||||||
|
case -1:
|
||||||
|
return op === '<' || op === '=<' || op === '<='
|
||||||
|
case 0:
|
||||||
|
return op === '=' || op === '>=' || op === '=<' || op === '<='
|
||||||
|
case 1:
|
||||||
|
return op === '>' || op === '>='
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})(this.op, this.lvalue.eval(context), this.rvalue.eval(context))
|
||||||
|
|
||||||
|
return this.negate ? !result : result
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default Condition
|
|
@ -0,0 +1,39 @@
|
||||||
|
function asComment(ctx) {
|
||||||
|
return `/* line ${ctx.debugInfo.lineNumber}, ${ctx.debugInfo.fileName} */\n`
|
||||||
|
}
|
||||||
|
|
||||||
|
function asMediaQuery(ctx) {
|
||||||
|
let filenameWithProtocol = ctx.debugInfo.fileName
|
||||||
|
if (!/^[a-z]+:\/\//i.test(filenameWithProtocol)) {
|
||||||
|
filenameWithProtocol = `file://${filenameWithProtocol}`
|
||||||
|
}
|
||||||
|
return `@media -sass-debug-info{filename{font-family:${filenameWithProtocol.replace(
|
||||||
|
/([.:/\\])/g,
|
||||||
|
function (a) {
|
||||||
|
if (a == '\\') {
|
||||||
|
a = '/'
|
||||||
|
}
|
||||||
|
return `\\${a}`
|
||||||
|
}
|
||||||
|
)}}line{font-family:\\00003${ctx.debugInfo.lineNumber}}}\n`
|
||||||
|
}
|
||||||
|
|
||||||
|
function debugInfo(context, ctx, lineSeparator) {
|
||||||
|
let result = ''
|
||||||
|
if (context.dumpLineNumbers && !context.compress) {
|
||||||
|
switch (context.dumpLineNumbers) {
|
||||||
|
case 'comments':
|
||||||
|
result = asComment(ctx)
|
||||||
|
break
|
||||||
|
case 'mediaquery':
|
||||||
|
result = asMediaQuery(ctx)
|
||||||
|
break
|
||||||
|
case 'all':
|
||||||
|
result = asComment(ctx) + (lineSeparator || '') + asMediaQuery(ctx)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export default debugInfo
|
|
@ -0,0 +1,148 @@
|
||||||
|
import Node from './node'
|
||||||
|
import Value from './value'
|
||||||
|
import Keyword from './keyword'
|
||||||
|
import Anonymous from './anonymous'
|
||||||
|
import * as Constants from '../constants'
|
||||||
|
const MATH = Constants.Math
|
||||||
|
|
||||||
|
function evalName(context, name) {
|
||||||
|
let value = ''
|
||||||
|
let i
|
||||||
|
const n = name.length
|
||||||
|
const output = {
|
||||||
|
add: function (s) {
|
||||||
|
value += s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (i = 0; i < n; i++) {
|
||||||
|
name[i].eval(context).genCSS(context, output)
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
const Declaration = function (
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
important,
|
||||||
|
merge,
|
||||||
|
index,
|
||||||
|
currentFileInfo,
|
||||||
|
inline,
|
||||||
|
variable
|
||||||
|
) {
|
||||||
|
this.name = name
|
||||||
|
this.value =
|
||||||
|
value instanceof Node
|
||||||
|
? value
|
||||||
|
: new Value([value ? new Anonymous(value) : null])
|
||||||
|
this.important = important ? ` ${important.trim()}` : ''
|
||||||
|
this.merge = merge
|
||||||
|
this._index = index
|
||||||
|
this._fileInfo = currentFileInfo
|
||||||
|
this.inline = inline || false
|
||||||
|
this.variable =
|
||||||
|
variable !== undefined ? variable : name.charAt && name.charAt(0) === '@'
|
||||||
|
this.allowRoot = true
|
||||||
|
this.setParent(this.value, this)
|
||||||
|
}
|
||||||
|
|
||||||
|
Declaration.prototype = Object.assign(new Node(), {
|
||||||
|
type: 'Declaration',
|
||||||
|
|
||||||
|
genCSS(context, output) {
|
||||||
|
output.add(
|
||||||
|
this.name + (context.compress ? ':' : ': '),
|
||||||
|
this.fileInfo(),
|
||||||
|
this.getIndex()
|
||||||
|
)
|
||||||
|
try {
|
||||||
|
this.value.genCSS(context, output)
|
||||||
|
} catch (e) {
|
||||||
|
e.index = this._index
|
||||||
|
e.filename = this._fileInfo.filename
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
output.add(
|
||||||
|
this.important +
|
||||||
|
(this.inline || (context.lastRule && context.compress) ? '' : ';'),
|
||||||
|
this._fileInfo,
|
||||||
|
this._index
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
eval(context) {
|
||||||
|
let mathBypass = false,
|
||||||
|
prevMath,
|
||||||
|
name = this.name,
|
||||||
|
evaldValue,
|
||||||
|
variable = this.variable
|
||||||
|
if (typeof name !== 'string') {
|
||||||
|
// expand 'primitive' name directly to get
|
||||||
|
// things faster (~10% for benchmark.less):
|
||||||
|
name =
|
||||||
|
name.length === 1 && name[0] instanceof Keyword
|
||||||
|
? name[0].value
|
||||||
|
: evalName(context, name)
|
||||||
|
variable = false // never treat expanded interpolation as new variable name
|
||||||
|
}
|
||||||
|
|
||||||
|
// @todo remove when parens-division is default
|
||||||
|
if (name === 'font' && context.math === MATH.ALWAYS) {
|
||||||
|
mathBypass = true
|
||||||
|
prevMath = context.math
|
||||||
|
context.math = MATH.PARENS_DIVISION
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
context.importantScope.push({})
|
||||||
|
evaldValue = this.value.eval(context)
|
||||||
|
|
||||||
|
if (!this.variable && evaldValue.type === 'DetachedRuleset') {
|
||||||
|
throw {
|
||||||
|
message: 'Rulesets cannot be evaluated on a property.',
|
||||||
|
index: this.getIndex(),
|
||||||
|
filename: this.fileInfo().filename
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let important = this.important
|
||||||
|
const importantResult = context.importantScope.pop()
|
||||||
|
if (!important && importantResult.important) {
|
||||||
|
important = importantResult.important
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Declaration(
|
||||||
|
name,
|
||||||
|
evaldValue,
|
||||||
|
important,
|
||||||
|
this.merge,
|
||||||
|
this.getIndex(),
|
||||||
|
this.fileInfo(),
|
||||||
|
this.inline,
|
||||||
|
variable
|
||||||
|
)
|
||||||
|
} catch (e) {
|
||||||
|
if (typeof e.index !== 'number') {
|
||||||
|
e.index = this.getIndex()
|
||||||
|
e.filename = this.fileInfo().filename
|
||||||
|
}
|
||||||
|
throw e
|
||||||
|
} finally {
|
||||||
|
if (mathBypass) {
|
||||||
|
context.math = prevMath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
makeImportant() {
|
||||||
|
return new Declaration(
|
||||||
|
this.name,
|
||||||
|
this.value,
|
||||||
|
'!important',
|
||||||
|
this.merge,
|
||||||
|
this.getIndex(),
|
||||||
|
this.fileInfo(),
|
||||||
|
this.inline
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default Declaration
|
|
@ -0,0 +1,33 @@
|
||||||
|
import Node from './node'
|
||||||
|
import contexts from '../contexts'
|
||||||
|
import * as utils from '../utils'
|
||||||
|
|
||||||
|
const DetachedRuleset = function (ruleset, frames) {
|
||||||
|
this.ruleset = ruleset
|
||||||
|
this.frames = frames
|
||||||
|
this.setParent(this.ruleset, this)
|
||||||
|
}
|
||||||
|
|
||||||
|
DetachedRuleset.prototype = Object.assign(new Node(), {
|
||||||
|
type: 'DetachedRuleset',
|
||||||
|
evalFirst: true,
|
||||||
|
|
||||||
|
accept(visitor) {
|
||||||
|
this.ruleset = visitor.visit(this.ruleset)
|
||||||
|
},
|
||||||
|
|
||||||
|
eval(context) {
|
||||||
|
const frames = this.frames || utils.copyArray(context.frames)
|
||||||
|
return new DetachedRuleset(this.ruleset, frames)
|
||||||
|
},
|
||||||
|
|
||||||
|
callEval(context) {
|
||||||
|
return this.ruleset.eval(
|
||||||
|
this.frames
|
||||||
|
? new contexts.Eval(context, this.frames.concat(context.frames))
|
||||||
|
: context
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default DetachedRuleset
|
|
@ -0,0 +1,185 @@
|
||||||
|
/* eslint-disable no-prototype-builtins */
|
||||||
|
import Node from './node'
|
||||||
|
import unitConversions from '../data/unit-conversions'
|
||||||
|
import Unit from './unit'
|
||||||
|
import Color from './color'
|
||||||
|
|
||||||
|
//
|
||||||
|
// A number with a unit
|
||||||
|
//
|
||||||
|
const Dimension = function (value, unit) {
|
||||||
|
this.value = parseFloat(value)
|
||||||
|
if (isNaN(this.value)) {
|
||||||
|
throw new Error('Dimension is not a number.')
|
||||||
|
}
|
||||||
|
this.unit =
|
||||||
|
unit && unit instanceof Unit ? unit : new Unit(unit ? [unit] : undefined)
|
||||||
|
this.setParent(this.unit, this)
|
||||||
|
}
|
||||||
|
|
||||||
|
Dimension.prototype = Object.assign(new Node(), {
|
||||||
|
type: 'Dimension',
|
||||||
|
|
||||||
|
accept(visitor) {
|
||||||
|
this.unit = visitor.visit(this.unit)
|
||||||
|
},
|
||||||
|
|
||||||
|
// remove when Nodes have JSDoc types
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
eval(context) {
|
||||||
|
return this
|
||||||
|
},
|
||||||
|
|
||||||
|
toColor() {
|
||||||
|
return new Color([this.value, this.value, this.value])
|
||||||
|
},
|
||||||
|
|
||||||
|
genCSS(context, output) {
|
||||||
|
if (context && context.strictUnits && !this.unit.isSingular()) {
|
||||||
|
throw new Error(
|
||||||
|
`Multiple units in dimension. Correct the units or use the unit function. Bad unit: ${this.unit.toString()}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = this.fround(context, this.value)
|
||||||
|
let strValue = String(value)
|
||||||
|
|
||||||
|
if (value !== 0 && value < 0.000001 && value > -0.000001) {
|
||||||
|
// would be output 1e-6 etc.
|
||||||
|
strValue = value.toFixed(20).replace(/0+$/, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context && context.compress) {
|
||||||
|
// Zero values doesn't need a unit
|
||||||
|
if (value === 0 && this.unit.isLength()) {
|
||||||
|
output.add(strValue)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Float values doesn't need a leading zero
|
||||||
|
if (value > 0 && value < 1) {
|
||||||
|
strValue = strValue.substr(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output.add(strValue)
|
||||||
|
this.unit.genCSS(context, output)
|
||||||
|
},
|
||||||
|
|
||||||
|
// In an operation between two Dimensions,
|
||||||
|
// we default to the first Dimension's unit,
|
||||||
|
// so `1px + 2` will yield `3px`.
|
||||||
|
operate(context, op, other) {
|
||||||
|
/* jshint noempty:false */
|
||||||
|
let value = this._operate(context, op, this.value, other.value)
|
||||||
|
let unit = this.unit.clone()
|
||||||
|
|
||||||
|
if (op === '+' || op === '-') {
|
||||||
|
if (unit.numerator.length === 0 && unit.denominator.length === 0) {
|
||||||
|
unit = other.unit.clone()
|
||||||
|
if (this.unit.backupUnit) {
|
||||||
|
unit.backupUnit = this.unit.backupUnit
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
other.unit.numerator.length === 0 &&
|
||||||
|
unit.denominator.length === 0
|
||||||
|
) {
|
||||||
|
// do nothing
|
||||||
|
} else {
|
||||||
|
other = other.convertTo(this.unit.usedUnits())
|
||||||
|
|
||||||
|
if (context.strictUnits && other.unit.toString() !== unit.toString()) {
|
||||||
|
throw new Error(
|
||||||
|
'Incompatible units. Change the units or use the unit function. ' +
|
||||||
|
`Bad units: '${unit.toString()}' and '${other.unit.toString()}'.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
value = this._operate(context, op, this.value, other.value)
|
||||||
|
}
|
||||||
|
} else if (op === '*') {
|
||||||
|
unit.numerator = unit.numerator.concat(other.unit.numerator).sort()
|
||||||
|
unit.denominator = unit.denominator.concat(other.unit.denominator).sort()
|
||||||
|
unit.cancel()
|
||||||
|
} else if (op === '/') {
|
||||||
|
unit.numerator = unit.numerator.concat(other.unit.denominator).sort()
|
||||||
|
unit.denominator = unit.denominator.concat(other.unit.numerator).sort()
|
||||||
|
unit.cancel()
|
||||||
|
}
|
||||||
|
return new Dimension(value, unit)
|
||||||
|
},
|
||||||
|
|
||||||
|
compare(other) {
|
||||||
|
let a, b
|
||||||
|
|
||||||
|
if (!(other instanceof Dimension)) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.unit.isEmpty() || other.unit.isEmpty()) {
|
||||||
|
a = this
|
||||||
|
b = other
|
||||||
|
} else {
|
||||||
|
a = this.unify()
|
||||||
|
b = other.unify()
|
||||||
|
if (a.unit.compare(b.unit) !== 0) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Node.numericCompare(a.value, b.value)
|
||||||
|
},
|
||||||
|
|
||||||
|
unify() {
|
||||||
|
return this.convertTo({ length: 'px', duration: 's', angle: 'rad' })
|
||||||
|
},
|
||||||
|
|
||||||
|
convertTo(conversions) {
|
||||||
|
let value = this.value
|
||||||
|
const unit = this.unit.clone()
|
||||||
|
let i
|
||||||
|
let groupName
|
||||||
|
let group
|
||||||
|
let targetUnit
|
||||||
|
let derivedConversions = {}
|
||||||
|
let applyUnit
|
||||||
|
|
||||||
|
if (typeof conversions === 'string') {
|
||||||
|
for (i in unitConversions) {
|
||||||
|
if (unitConversions[i].hasOwnProperty(conversions)) {
|
||||||
|
derivedConversions = {}
|
||||||
|
derivedConversions[i] = conversions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
conversions = derivedConversions
|
||||||
|
}
|
||||||
|
applyUnit = function (atomicUnit, denominator) {
|
||||||
|
if (group.hasOwnProperty(atomicUnit)) {
|
||||||
|
if (denominator) {
|
||||||
|
value = value / (group[atomicUnit] / group[targetUnit])
|
||||||
|
} else {
|
||||||
|
value = value * (group[atomicUnit] / group[targetUnit])
|
||||||
|
}
|
||||||
|
|
||||||
|
return targetUnit
|
||||||
|
}
|
||||||
|
|
||||||
|
return atomicUnit
|
||||||
|
}
|
||||||
|
|
||||||
|
for (groupName in conversions) {
|
||||||
|
if (conversions.hasOwnProperty(groupName)) {
|
||||||
|
targetUnit = conversions[groupName]
|
||||||
|
group = unitConversions[groupName]
|
||||||
|
|
||||||
|
unit.map(applyUnit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unit.cancel()
|
||||||
|
|
||||||
|
return new Dimension(value, unit)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default Dimension
|
|
@ -0,0 +1,86 @@
|
||||||
|
import Node from './node'
|
||||||
|
import Paren from './paren'
|
||||||
|
import Combinator from './combinator'
|
||||||
|
|
||||||
|
const Element = function (
|
||||||
|
combinator,
|
||||||
|
value,
|
||||||
|
isVariable,
|
||||||
|
index,
|
||||||
|
currentFileInfo,
|
||||||
|
visibilityInfo
|
||||||
|
) {
|
||||||
|
this.combinator =
|
||||||
|
combinator instanceof Combinator ? combinator : new Combinator(combinator)
|
||||||
|
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
this.value = value.trim()
|
||||||
|
} else if (value) {
|
||||||
|
this.value = value
|
||||||
|
} else {
|
||||||
|
this.value = ''
|
||||||
|
}
|
||||||
|
this.isVariable = isVariable
|
||||||
|
this._index = index
|
||||||
|
this._fileInfo = currentFileInfo
|
||||||
|
this.copyVisibilityInfo(visibilityInfo)
|
||||||
|
this.setParent(this.combinator, this)
|
||||||
|
}
|
||||||
|
|
||||||
|
Element.prototype = Object.assign(new Node(), {
|
||||||
|
type: 'Element',
|
||||||
|
|
||||||
|
accept(visitor) {
|
||||||
|
const value = this.value
|
||||||
|
this.combinator = visitor.visit(this.combinator)
|
||||||
|
if (typeof value === 'object') {
|
||||||
|
this.value = visitor.visit(value)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
eval(context) {
|
||||||
|
return new Element(
|
||||||
|
this.combinator,
|
||||||
|
this.value.eval ? this.value.eval(context) : this.value,
|
||||||
|
this.isVariable,
|
||||||
|
this.getIndex(),
|
||||||
|
this.fileInfo(),
|
||||||
|
this.visibilityInfo()
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
clone() {
|
||||||
|
return new Element(
|
||||||
|
this.combinator,
|
||||||
|
this.value,
|
||||||
|
this.isVariable,
|
||||||
|
this.getIndex(),
|
||||||
|
this.fileInfo(),
|
||||||
|
this.visibilityInfo()
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
genCSS(context, output) {
|
||||||
|
output.add(this.toCSS(context), this.fileInfo(), this.getIndex())
|
||||||
|
},
|
||||||
|
|
||||||
|
toCSS(context) {
|
||||||
|
context = context || {}
|
||||||
|
let value = this.value
|
||||||
|
const firstSelector = context.firstSelector
|
||||||
|
if (value instanceof Paren) {
|
||||||
|
// selector in parens should not be affected by outer selector
|
||||||
|
// flags (breaks only interpolated selectors - see #1973)
|
||||||
|
context.firstSelector = true
|
||||||
|
}
|
||||||
|
value = value.toCSS ? value.toCSS(context) : value
|
||||||
|
context.firstSelector = firstSelector
|
||||||
|
if (value === '' && this.combinator.value.charAt(0) === '&') {
|
||||||
|
return ''
|
||||||
|
} else {
|
||||||
|
return this.combinator.toCSS(context) + value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default Element
|
|
@ -0,0 +1,83 @@
|
||||||
|
import Node from './node'
|
||||||
|
import Paren from './paren'
|
||||||
|
import Comment from './comment'
|
||||||
|
import Dimension from './dimension'
|
||||||
|
|
||||||
|
const Expression = function (value, noSpacing) {
|
||||||
|
this.value = value
|
||||||
|
this.noSpacing = noSpacing
|
||||||
|
if (!value) {
|
||||||
|
throw new Error('Expression requires an array parameter')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Expression.prototype = Object.assign(new Node(), {
|
||||||
|
type: 'Expression',
|
||||||
|
|
||||||
|
accept(visitor) {
|
||||||
|
this.value = visitor.visitArray(this.value)
|
||||||
|
},
|
||||||
|
|
||||||
|
eval(context) {
|
||||||
|
let returnValue
|
||||||
|
const mathOn = context.isMathOn()
|
||||||
|
const inParenthesis = this.parens
|
||||||
|
|
||||||
|
let doubleParen = false
|
||||||
|
if (inParenthesis) {
|
||||||
|
context.inParenthesis()
|
||||||
|
}
|
||||||
|
if (this.value.length > 1) {
|
||||||
|
returnValue = new Expression(
|
||||||
|
this.value.map(function (e) {
|
||||||
|
if (!e.eval) {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
return e.eval(context)
|
||||||
|
}),
|
||||||
|
this.noSpacing
|
||||||
|
)
|
||||||
|
} else if (this.value.length === 1) {
|
||||||
|
if (
|
||||||
|
this.value[0].parens &&
|
||||||
|
!this.value[0].parensInOp &&
|
||||||
|
!context.inCalc
|
||||||
|
) {
|
||||||
|
doubleParen = true
|
||||||
|
}
|
||||||
|
returnValue = this.value[0].eval(context)
|
||||||
|
} else {
|
||||||
|
returnValue = this
|
||||||
|
}
|
||||||
|
if (inParenthesis) {
|
||||||
|
context.outOfParenthesis()
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
this.parens &&
|
||||||
|
this.parensInOp &&
|
||||||
|
!mathOn &&
|
||||||
|
!doubleParen &&
|
||||||
|
!(returnValue instanceof Dimension)
|
||||||
|
) {
|
||||||
|
returnValue = new Paren(returnValue)
|
||||||
|
}
|
||||||
|
return returnValue
|
||||||
|
},
|
||||||
|
|
||||||
|
genCSS(context, output) {
|
||||||
|
for (let i = 0; i < this.value.length; i++) {
|
||||||
|
this.value[i].genCSS(context, output)
|
||||||
|
if (!this.noSpacing && i + 1 < this.value.length) {
|
||||||
|
output.add(' ')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
throwAwayComments() {
|
||||||
|
this.value = this.value.filter(function (v) {
|
||||||
|
return !(v instanceof Comment)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default Expression
|
|
@ -0,0 +1,88 @@
|
||||||
|
import Node from './node'
|
||||||
|
import Selector from './selector'
|
||||||
|
|
||||||
|
const Extend = function (
|
||||||
|
selector,
|
||||||
|
option,
|
||||||
|
index,
|
||||||
|
currentFileInfo,
|
||||||
|
visibilityInfo
|
||||||
|
) {
|
||||||
|
this.selector = selector
|
||||||
|
this.option = option
|
||||||
|
this.object_id = Extend.next_id++
|
||||||
|
this.parent_ids = [this.object_id]
|
||||||
|
this._index = index
|
||||||
|
this._fileInfo = currentFileInfo
|
||||||
|
this.copyVisibilityInfo(visibilityInfo)
|
||||||
|
this.allowRoot = true
|
||||||
|
|
||||||
|
switch (option) {
|
||||||
|
case 'all':
|
||||||
|
this.allowBefore = true
|
||||||
|
this.allowAfter = true
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
this.allowBefore = false
|
||||||
|
this.allowAfter = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
this.setParent(this.selector, this)
|
||||||
|
}
|
||||||
|
|
||||||
|
Extend.prototype = Object.assign(new Node(), {
|
||||||
|
type: 'Extend',
|
||||||
|
|
||||||
|
accept(visitor) {
|
||||||
|
this.selector = visitor.visit(this.selector)
|
||||||
|
},
|
||||||
|
|
||||||
|
eval(context) {
|
||||||
|
return new Extend(
|
||||||
|
this.selector.eval(context),
|
||||||
|
this.option,
|
||||||
|
this.getIndex(),
|
||||||
|
this.fileInfo(),
|
||||||
|
this.visibilityInfo()
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
// remove when Nodes have JSDoc types
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
clone(context) {
|
||||||
|
return new Extend(
|
||||||
|
this.selector,
|
||||||
|
this.option,
|
||||||
|
this.getIndex(),
|
||||||
|
this.fileInfo(),
|
||||||
|
this.visibilityInfo()
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
// it concatenates (joins) all selectors in selector array
|
||||||
|
findSelfSelectors(selectors) {
|
||||||
|
let selfElements = [],
|
||||||
|
i,
|
||||||
|
selectorElements
|
||||||
|
|
||||||
|
for (i = 0; i < selectors.length; i++) {
|
||||||
|
selectorElements = selectors[i].elements
|
||||||
|
// duplicate the logic in genCSS function inside the selector node.
|
||||||
|
// future TODO - move both logics into the selector joiner visitor
|
||||||
|
if (
|
||||||
|
i > 0 &&
|
||||||
|
selectorElements.length &&
|
||||||
|
selectorElements[0].combinator.value === ''
|
||||||
|
) {
|
||||||
|
selectorElements[0].combinator.value = ' '
|
||||||
|
}
|
||||||
|
selfElements = selfElements.concat(selectors[i].elements)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.selfSelectors = [new Selector(selfElements)]
|
||||||
|
this.selfSelectors[0].copyVisibilityInfo(this.visibilityInfo())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
Extend.next_id = 0
|
||||||
|
export default Extend
|
|
@ -0,0 +1,208 @@
|
||||||
|
import Node from './node'
|
||||||
|
import Media from './media'
|
||||||
|
import URL from './url'
|
||||||
|
import Quoted from './quoted'
|
||||||
|
import Ruleset from './ruleset'
|
||||||
|
import Anonymous from './anonymous'
|
||||||
|
import * as utils from '../utils'
|
||||||
|
import LessError from '../less-error'
|
||||||
|
|
||||||
|
//
|
||||||
|
// CSS @import node
|
||||||
|
//
|
||||||
|
// The general strategy here is that we don't want to wait
|
||||||
|
// for the parsing to be completed, before we start importing
|
||||||
|
// the file. That's because in the context of a browser,
|
||||||
|
// most of the time will be spent waiting for the server to respond.
|
||||||
|
//
|
||||||
|
// On creation, we push the import path to our import queue, though
|
||||||
|
// `import,push`, we also pass it a callback, which it'll call once
|
||||||
|
// the file has been fetched, and parsed.
|
||||||
|
//
|
||||||
|
const Import = function (
|
||||||
|
path,
|
||||||
|
features,
|
||||||
|
options,
|
||||||
|
index,
|
||||||
|
currentFileInfo,
|
||||||
|
visibilityInfo
|
||||||
|
) {
|
||||||
|
this.options = options
|
||||||
|
this._index = index
|
||||||
|
this._fileInfo = currentFileInfo
|
||||||
|
this.path = path
|
||||||
|
this.features = features
|
||||||
|
this.allowRoot = true
|
||||||
|
|
||||||
|
if (this.options.less !== undefined || this.options.inline) {
|
||||||
|
this.css = !this.options.less || this.options.inline
|
||||||
|
} else {
|
||||||
|
const pathValue = this.getPath()
|
||||||
|
if (pathValue && /[#.&?]css([?;].*)?$/.test(pathValue)) {
|
||||||
|
this.css = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.copyVisibilityInfo(visibilityInfo)
|
||||||
|
this.setParent(this.features, this)
|
||||||
|
this.setParent(this.path, this)
|
||||||
|
}
|
||||||
|
|
||||||
|
Import.prototype = Object.assign(new Node(), {
|
||||||
|
type: 'Import',
|
||||||
|
|
||||||
|
accept(visitor) {
|
||||||
|
if (this.features) {
|
||||||
|
this.features = visitor.visit(this.features)
|
||||||
|
}
|
||||||
|
this.path = visitor.visit(this.path)
|
||||||
|
if (!this.options.isPlugin && !this.options.inline && this.root) {
|
||||||
|
this.root = visitor.visit(this.root)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
genCSS(context, output) {
|
||||||
|
if (this.css && this.path._fileInfo.reference === undefined) {
|
||||||
|
output.add('@import ', this._fileInfo, this._index)
|
||||||
|
this.path.genCSS(context, output)
|
||||||
|
if (this.features) {
|
||||||
|
output.add(' ')
|
||||||
|
this.features.genCSS(context, output)
|
||||||
|
}
|
||||||
|
output.add(';')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getPath() {
|
||||||
|
return this.path instanceof URL ? this.path.value.value : this.path.value
|
||||||
|
},
|
||||||
|
|
||||||
|
isVariableImport() {
|
||||||
|
let path = this.path
|
||||||
|
if (path instanceof URL) {
|
||||||
|
path = path.value
|
||||||
|
}
|
||||||
|
if (path instanceof Quoted) {
|
||||||
|
return path.containsVariables()
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
|
||||||
|
evalForImport(context) {
|
||||||
|
let path = this.path
|
||||||
|
|
||||||
|
if (path instanceof URL) {
|
||||||
|
path = path.value
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Import(
|
||||||
|
path.eval(context),
|
||||||
|
this.features,
|
||||||
|
this.options,
|
||||||
|
this._index,
|
||||||
|
this._fileInfo,
|
||||||
|
this.visibilityInfo()
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
evalPath(context) {
|
||||||
|
const path = this.path.eval(context)
|
||||||
|
const fileInfo = this._fileInfo
|
||||||
|
|
||||||
|
if (!(path instanceof URL)) {
|
||||||
|
// Add the rootpath if the URL requires a rewrite
|
||||||
|
const pathValue = path.value
|
||||||
|
if (fileInfo && pathValue && context.pathRequiresRewrite(pathValue)) {
|
||||||
|
path.value = context.rewritePath(pathValue, fileInfo.rootpath)
|
||||||
|
} else {
|
||||||
|
path.value = context.normalizePath(path.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return path
|
||||||
|
},
|
||||||
|
|
||||||
|
eval(context) {
|
||||||
|
const result = this.doEval(context)
|
||||||
|
if (this.options.reference || this.blocksVisibility()) {
|
||||||
|
if (result.length || result.length === 0) {
|
||||||
|
result.forEach(function (node) {
|
||||||
|
node.addVisibilityBlock()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
result.addVisibilityBlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
|
||||||
|
doEval(context) {
|
||||||
|
let ruleset
|
||||||
|
let registry
|
||||||
|
const features = this.features && this.features.eval(context)
|
||||||
|
|
||||||
|
if (this.options.isPlugin) {
|
||||||
|
if (this.root && this.root.eval) {
|
||||||
|
try {
|
||||||
|
this.root.eval(context)
|
||||||
|
} catch (e) {
|
||||||
|
e.message = 'Plugin error during evaluation'
|
||||||
|
throw new LessError(e, this.root.imports, this.root.filename)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
registry = context.frames[0] && context.frames[0].functionRegistry
|
||||||
|
if (registry && this.root && this.root.functions) {
|
||||||
|
registry.addMultiple(this.root.functions)
|
||||||
|
}
|
||||||
|
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.skip) {
|
||||||
|
if (typeof this.skip === 'function') {
|
||||||
|
this.skip = this.skip()
|
||||||
|
}
|
||||||
|
if (this.skip) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.options.inline) {
|
||||||
|
const contents = new Anonymous(
|
||||||
|
this.root,
|
||||||
|
0,
|
||||||
|
{
|
||||||
|
filename: this.importedFilename,
|
||||||
|
reference: this.path._fileInfo && this.path._fileInfo.reference
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
|
||||||
|
return this.features
|
||||||
|
? new Media([contents], this.features.value)
|
||||||
|
: [contents]
|
||||||
|
} else if (this.css) {
|
||||||
|
const newImport = new Import(
|
||||||
|
this.evalPath(context),
|
||||||
|
features,
|
||||||
|
this.options,
|
||||||
|
this._index
|
||||||
|
)
|
||||||
|
if (!newImport.css && this.error) {
|
||||||
|
throw this.error
|
||||||
|
}
|
||||||
|
return newImport
|
||||||
|
} else if (this.root) {
|
||||||
|
ruleset = new Ruleset(null, utils.copyArray(this.root.rules))
|
||||||
|
ruleset.evalImports(context)
|
||||||
|
|
||||||
|
return this.features
|
||||||
|
? new Media(ruleset.rules, this.features.value)
|
||||||
|
: ruleset.rules
|
||||||
|
} else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default Import
|
|
@ -0,0 +1,79 @@
|
||||||
|
import Node from './node'
|
||||||
|
import Color from './color'
|
||||||
|
import AtRule from './atrule'
|
||||||
|
import DetachedRuleset from './detached-ruleset'
|
||||||
|
import Operation from './operation'
|
||||||
|
import Dimension from './dimension'
|
||||||
|
import Unit from './unit'
|
||||||
|
import Keyword from './keyword'
|
||||||
|
import Variable from './variable'
|
||||||
|
import Property from './property'
|
||||||
|
import Ruleset from './ruleset'
|
||||||
|
import Element from './element'
|
||||||
|
import Attribute from './attribute'
|
||||||
|
import Combinator from './combinator'
|
||||||
|
import Selector from './selector'
|
||||||
|
import Quoted from './quoted'
|
||||||
|
import Expression from './expression'
|
||||||
|
import Declaration from './declaration'
|
||||||
|
import Call from './call'
|
||||||
|
import URL from './url'
|
||||||
|
import Import from './import'
|
||||||
|
import Comment from './comment'
|
||||||
|
import Anonymous from './anonymous'
|
||||||
|
import Value from './value'
|
||||||
|
import JavaScript from './javascript'
|
||||||
|
import Assignment from './assignment'
|
||||||
|
import Condition from './condition'
|
||||||
|
import Paren from './paren'
|
||||||
|
import Media from './media'
|
||||||
|
import UnicodeDescriptor from './unicode-descriptor'
|
||||||
|
import Negative from './negative'
|
||||||
|
import Extend from './extend'
|
||||||
|
import VariableCall from './variable-call'
|
||||||
|
import NamespaceValue from './namespace-value'
|
||||||
|
|
||||||
|
// mixins
|
||||||
|
import MixinCall from './mixin-call'
|
||||||
|
import MixinDefinition from './mixin-definition'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
Node,
|
||||||
|
Color,
|
||||||
|
AtRule,
|
||||||
|
DetachedRuleset,
|
||||||
|
Operation,
|
||||||
|
Dimension,
|
||||||
|
Unit,
|
||||||
|
Keyword,
|
||||||
|
Variable,
|
||||||
|
Property,
|
||||||
|
Ruleset,
|
||||||
|
Element,
|
||||||
|
Attribute,
|
||||||
|
Combinator,
|
||||||
|
Selector,
|
||||||
|
Quoted,
|
||||||
|
Expression,
|
||||||
|
Declaration,
|
||||||
|
Call,
|
||||||
|
URL,
|
||||||
|
Import,
|
||||||
|
Comment,
|
||||||
|
Anonymous,
|
||||||
|
Value,
|
||||||
|
JavaScript,
|
||||||
|
Assignment,
|
||||||
|
Condition,
|
||||||
|
Paren,
|
||||||
|
Media,
|
||||||
|
UnicodeDescriptor,
|
||||||
|
Negative,
|
||||||
|
Extend,
|
||||||
|
VariableCall,
|
||||||
|
NamespaceValue,
|
||||||
|
mixin: {
|
||||||
|
Call: MixinCall,
|
||||||
|
Definition: MixinDefinition
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
import JsEvalNode from './js-eval-node'
|
||||||
|
import Dimension from './dimension'
|
||||||
|
import Quoted from './quoted'
|
||||||
|
import Anonymous from './anonymous'
|
||||||
|
|
||||||
|
const JavaScript = function (string, escaped, index, currentFileInfo) {
|
||||||
|
this.escaped = escaped
|
||||||
|
this.expression = string
|
||||||
|
this._index = index
|
||||||
|
this._fileInfo = currentFileInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
JavaScript.prototype = Object.assign(new JsEvalNode(), {
|
||||||
|
type: 'JavaScript',
|
||||||
|
|
||||||
|
eval(context) {
|
||||||
|
const result = this.evaluateJavaScript(this.expression, context)
|
||||||
|
const type = typeof result
|
||||||
|
|
||||||
|
if (type === 'number' && !isNaN(result)) {
|
||||||
|
return new Dimension(result)
|
||||||
|
} else if (type === 'string') {
|
||||||
|
return new Quoted(`"${result}"`, result, this.escaped, this._index)
|
||||||
|
} else if (Array.isArray(result)) {
|
||||||
|
return new Anonymous(result.join(', '))
|
||||||
|
} else {
|
||||||
|
return new Anonymous(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default JavaScript
|
|
@ -0,0 +1,77 @@
|
||||||
|
import Node from './node'
|
||||||
|
import Variable from './variable'
|
||||||
|
|
||||||
|
const JsEvalNode = function () {}
|
||||||
|
|
||||||
|
JsEvalNode.prototype = Object.assign(new Node(), {
|
||||||
|
evaluateJavaScript(expression, context) {
|
||||||
|
let result
|
||||||
|
const that = this
|
||||||
|
const evalContext = {}
|
||||||
|
|
||||||
|
if (!context.javascriptEnabled) {
|
||||||
|
throw {
|
||||||
|
message: 'Inline JavaScript is not enabled. Is it set in your options?',
|
||||||
|
filename: this.fileInfo().filename,
|
||||||
|
index: this.getIndex()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expression = expression.replace(/@\{([\w-]+)\}/g, function (_, name) {
|
||||||
|
return that.jsify(
|
||||||
|
new Variable(`@${name}`, that.getIndex(), that.fileInfo()).eval(context)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
expression = new Function(`return (${expression})`)
|
||||||
|
} catch (e) {
|
||||||
|
throw {
|
||||||
|
message: `JavaScript evaluation error: ${e.message} from \`${expression}\``,
|
||||||
|
filename: this.fileInfo().filename,
|
||||||
|
index: this.getIndex()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const variables = context.frames[0].variables()
|
||||||
|
for (const k in variables) {
|
||||||
|
// eslint-disable-next-line no-prototype-builtins
|
||||||
|
if (variables.hasOwnProperty(k)) {
|
||||||
|
evalContext[k.slice(1)] = {
|
||||||
|
value: variables[k].value,
|
||||||
|
toJS: function () {
|
||||||
|
return this.value.eval(context).toCSS()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
result = expression.call(evalContext)
|
||||||
|
} catch (e) {
|
||||||
|
throw {
|
||||||
|
message: `JavaScript evaluation error: '${e.name}: ${e.message.replace(
|
||||||
|
/["]/g,
|
||||||
|
"'"
|
||||||
|
)}'`,
|
||||||
|
filename: this.fileInfo().filename,
|
||||||
|
index: this.getIndex()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
|
||||||
|
jsify(obj) {
|
||||||
|
if (Array.isArray(obj.value) && obj.value.length > 1) {
|
||||||
|
return `[${obj.value
|
||||||
|
.map(function (v) {
|
||||||
|
return v.toCSS()
|
||||||
|
})
|
||||||
|
.join(', ')}]`
|
||||||
|
} else {
|
||||||
|
return obj.toCSS()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default JsEvalNode
|
|
@ -0,0 +1,21 @@
|
||||||
|
import Node from './node'
|
||||||
|
|
||||||
|
const Keyword = function (value) {
|
||||||
|
this.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
Keyword.prototype = Object.assign(new Node(), {
|
||||||
|
type: 'Keyword',
|
||||||
|
|
||||||
|
genCSS(context, output) {
|
||||||
|
if (this.value === '%') {
|
||||||
|
throw { type: 'Syntax', message: 'Invalid % without number' }
|
||||||
|
}
|
||||||
|
output.add(this.value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
Keyword.True = new Keyword('true')
|
||||||
|
Keyword.False = new Keyword('false')
|
||||||
|
|
||||||
|
export default Keyword
|
|
@ -0,0 +1,185 @@
|
||||||
|
import Ruleset from './ruleset'
|
||||||
|
import Value from './value'
|
||||||
|
import Selector from './selector'
|
||||||
|
import Anonymous from './anonymous'
|
||||||
|
import Expression from './expression'
|
||||||
|
import AtRule from './atrule'
|
||||||
|
import * as utils from '../utils'
|
||||||
|
|
||||||
|
const Media = function (
|
||||||
|
value,
|
||||||
|
features,
|
||||||
|
index,
|
||||||
|
currentFileInfo,
|
||||||
|
visibilityInfo
|
||||||
|
) {
|
||||||
|
this._index = index
|
||||||
|
this._fileInfo = currentFileInfo
|
||||||
|
|
||||||
|
const selectors = new Selector(
|
||||||
|
[],
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
this._index,
|
||||||
|
this._fileInfo
|
||||||
|
).createEmptySelectors()
|
||||||
|
|
||||||
|
this.features = new Value(features)
|
||||||
|
this.rules = [new Ruleset(selectors, value)]
|
||||||
|
this.rules[0].allowImports = true
|
||||||
|
this.copyVisibilityInfo(visibilityInfo)
|
||||||
|
this.allowRoot = true
|
||||||
|
this.setParent(selectors, this)
|
||||||
|
this.setParent(this.features, this)
|
||||||
|
this.setParent(this.rules, this)
|
||||||
|
}
|
||||||
|
|
||||||
|
Media.prototype = Object.assign(new AtRule(), {
|
||||||
|
type: 'Media',
|
||||||
|
|
||||||
|
isRulesetLike() {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
|
||||||
|
accept(visitor) {
|
||||||
|
if (this.features) {
|
||||||
|
this.features = visitor.visit(this.features)
|
||||||
|
}
|
||||||
|
if (this.rules) {
|
||||||
|
this.rules = visitor.visitArray(this.rules)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
genCSS(context, output) {
|
||||||
|
output.add('@media ', this._fileInfo, this._index)
|
||||||
|
this.features.genCSS(context, output)
|
||||||
|
this.outputRuleset(context, output, this.rules)
|
||||||
|
},
|
||||||
|
|
||||||
|
eval(context) {
|
||||||
|
if (!context.mediaBlocks) {
|
||||||
|
context.mediaBlocks = []
|
||||||
|
context.mediaPath = []
|
||||||
|
}
|
||||||
|
|
||||||
|
const media = new Media(
|
||||||
|
null,
|
||||||
|
[],
|
||||||
|
this._index,
|
||||||
|
this._fileInfo,
|
||||||
|
this.visibilityInfo()
|
||||||
|
)
|
||||||
|
if (this.debugInfo) {
|
||||||
|
this.rules[0].debugInfo = this.debugInfo
|
||||||
|
media.debugInfo = this.debugInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
media.features = this.features.eval(context)
|
||||||
|
|
||||||
|
context.mediaPath.push(media)
|
||||||
|
context.mediaBlocks.push(media)
|
||||||
|
|
||||||
|
this.rules[0].functionRegistry =
|
||||||
|
context.frames[0].functionRegistry.inherit()
|
||||||
|
context.frames.unshift(this.rules[0])
|
||||||
|
media.rules = [this.rules[0].eval(context)]
|
||||||
|
context.frames.shift()
|
||||||
|
|
||||||
|
context.mediaPath.pop()
|
||||||
|
|
||||||
|
return context.mediaPath.length === 0
|
||||||
|
? media.evalTop(context)
|
||||||
|
: media.evalNested(context)
|
||||||
|
},
|
||||||
|
|
||||||
|
evalTop(context) {
|
||||||
|
let result = this
|
||||||
|
|
||||||
|
// Render all dependent Media blocks.
|
||||||
|
if (context.mediaBlocks.length > 1) {
|
||||||
|
const selectors = new Selector(
|
||||||
|
[],
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
this.getIndex(),
|
||||||
|
this.fileInfo()
|
||||||
|
).createEmptySelectors()
|
||||||
|
result = new Ruleset(selectors, context.mediaBlocks)
|
||||||
|
result.multiMedia = true
|
||||||
|
result.copyVisibilityInfo(this.visibilityInfo())
|
||||||
|
this.setParent(result, this)
|
||||||
|
}
|
||||||
|
|
||||||
|
delete context.mediaBlocks
|
||||||
|
delete context.mediaPath
|
||||||
|
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
|
||||||
|
evalNested(context) {
|
||||||
|
let i
|
||||||
|
let value
|
||||||
|
const path = context.mediaPath.concat([this])
|
||||||
|
|
||||||
|
// Extract the media-query conditions separated with `,` (OR).
|
||||||
|
for (i = 0; i < path.length; i++) {
|
||||||
|
value =
|
||||||
|
path[i].features instanceof Value
|
||||||
|
? path[i].features.value
|
||||||
|
: path[i].features
|
||||||
|
path[i] = Array.isArray(value) ? value : [value]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trace all permutations to generate the resulting media-query.
|
||||||
|
//
|
||||||
|
// (a, b and c) with nested (d, e) ->
|
||||||
|
// a and d
|
||||||
|
// a and e
|
||||||
|
// b and c and d
|
||||||
|
// b and c and e
|
||||||
|
this.features = new Value(
|
||||||
|
this.permute(path).map(path => {
|
||||||
|
path = path.map(fragment =>
|
||||||
|
fragment.toCSS ? fragment : new Anonymous(fragment)
|
||||||
|
)
|
||||||
|
|
||||||
|
for (i = path.length - 1; i > 0; i--) {
|
||||||
|
path.splice(i, 0, new Anonymous('and'))
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Expression(path)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
this.setParent(this.features, this)
|
||||||
|
|
||||||
|
// Fake a tree-node that doesn't output anything.
|
||||||
|
return new Ruleset([], [])
|
||||||
|
},
|
||||||
|
|
||||||
|
permute(arr) {
|
||||||
|
if (arr.length === 0) {
|
||||||
|
return []
|
||||||
|
} else if (arr.length === 1) {
|
||||||
|
return arr[0]
|
||||||
|
} else {
|
||||||
|
const result = []
|
||||||
|
const rest = this.permute(arr.slice(1))
|
||||||
|
for (let i = 0; i < rest.length; i++) {
|
||||||
|
for (let j = 0; j < arr[0].length; j++) {
|
||||||
|
result.push([arr[0][j]].concat(rest[i]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
bubbleSelectors(selectors) {
|
||||||
|
if (!selectors) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.rules = [new Ruleset(utils.copyArray(selectors), [this.rules[0]])]
|
||||||
|
this.setParent(this.rules, this)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default Media
|
|
@ -0,0 +1,259 @@
|
||||||
|
import Node from './node'
|
||||||
|
import Selector from './selector'
|
||||||
|
import MixinDefinition from './mixin-definition'
|
||||||
|
import defaultFunc from '../functions/default'
|
||||||
|
|
||||||
|
const MixinCall = function (elements, args, index, currentFileInfo, important) {
|
||||||
|
this.selector = new Selector(elements)
|
||||||
|
this.arguments = args || []
|
||||||
|
this._index = index
|
||||||
|
this._fileInfo = currentFileInfo
|
||||||
|
this.important = important
|
||||||
|
this.allowRoot = true
|
||||||
|
this.setParent(this.selector, this)
|
||||||
|
}
|
||||||
|
|
||||||
|
MixinCall.prototype = Object.assign(new Node(), {
|
||||||
|
type: 'MixinCall',
|
||||||
|
|
||||||
|
accept(visitor) {
|
||||||
|
if (this.selector) {
|
||||||
|
this.selector = visitor.visit(this.selector)
|
||||||
|
}
|
||||||
|
if (this.arguments.length) {
|
||||||
|
this.arguments = visitor.visitArray(this.arguments)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
eval(context) {
|
||||||
|
let mixins
|
||||||
|
let mixin
|
||||||
|
let mixinPath
|
||||||
|
const args = []
|
||||||
|
let arg
|
||||||
|
let argValue
|
||||||
|
const rules = []
|
||||||
|
let match = false
|
||||||
|
let i
|
||||||
|
let m
|
||||||
|
let f
|
||||||
|
let isRecursive
|
||||||
|
let isOneFound
|
||||||
|
const candidates = []
|
||||||
|
let candidate
|
||||||
|
const conditionResult = []
|
||||||
|
let defaultResult
|
||||||
|
const defFalseEitherCase = -1
|
||||||
|
const defNone = 0
|
||||||
|
const defTrue = 1
|
||||||
|
const defFalse = 2
|
||||||
|
let count
|
||||||
|
let originalRuleset
|
||||||
|
let noArgumentsFilter
|
||||||
|
|
||||||
|
this.selector = this.selector.eval(context)
|
||||||
|
|
||||||
|
function calcDefGroup(mixin, mixinPath) {
|
||||||
|
let f, p, namespace
|
||||||
|
|
||||||
|
for (f = 0; f < 2; f++) {
|
||||||
|
conditionResult[f] = true
|
||||||
|
defaultFunc.value(f)
|
||||||
|
for (p = 0; p < mixinPath.length && conditionResult[f]; p++) {
|
||||||
|
namespace = mixinPath[p]
|
||||||
|
if (namespace.matchCondition) {
|
||||||
|
conditionResult[f] =
|
||||||
|
conditionResult[f] && namespace.matchCondition(null, context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mixin.matchCondition) {
|
||||||
|
conditionResult[f] =
|
||||||
|
conditionResult[f] && mixin.matchCondition(args, context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (conditionResult[0] || conditionResult[1]) {
|
||||||
|
if (conditionResult[0] != conditionResult[1]) {
|
||||||
|
return conditionResult[1] ? defTrue : defFalse
|
||||||
|
}
|
||||||
|
|
||||||
|
return defNone
|
||||||
|
}
|
||||||
|
return defFalseEitherCase
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i = 0; i < this.arguments.length; i++) {
|
||||||
|
arg = this.arguments[i]
|
||||||
|
argValue = arg.value.eval(context)
|
||||||
|
if (arg.expand && Array.isArray(argValue.value)) {
|
||||||
|
argValue = argValue.value
|
||||||
|
for (m = 0; m < argValue.length; m++) {
|
||||||
|
args.push({ value: argValue[m] })
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
args.push({ name: arg.name, value: argValue })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
noArgumentsFilter = function (rule) {
|
||||||
|
return rule.matchArgs(null, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i = 0; i < context.frames.length; i++) {
|
||||||
|
if (
|
||||||
|
(mixins = context.frames[i].find(
|
||||||
|
this.selector,
|
||||||
|
null,
|
||||||
|
noArgumentsFilter
|
||||||
|
)).length > 0
|
||||||
|
) {
|
||||||
|
isOneFound = true
|
||||||
|
|
||||||
|
// To make `default()` function independent of definition order we have two "subpasses" here.
|
||||||
|
// At first we evaluate each guard *twice* (with `default() == true` and `default() == false`),
|
||||||
|
// and build candidate list with corresponding flags. Then, when we know all possible matches,
|
||||||
|
// we make a final decision.
|
||||||
|
|
||||||
|
for (m = 0; m < mixins.length; m++) {
|
||||||
|
mixin = mixins[m].rule
|
||||||
|
mixinPath = mixins[m].path
|
||||||
|
isRecursive = false
|
||||||
|
for (f = 0; f < context.frames.length; f++) {
|
||||||
|
if (
|
||||||
|
!(mixin instanceof MixinDefinition) &&
|
||||||
|
mixin === (context.frames[f].originalRuleset || context.frames[f])
|
||||||
|
) {
|
||||||
|
isRecursive = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isRecursive) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mixin.matchArgs(args, context)) {
|
||||||
|
candidate = { mixin, group: calcDefGroup(mixin, mixinPath) }
|
||||||
|
|
||||||
|
if (candidate.group !== defFalseEitherCase) {
|
||||||
|
candidates.push(candidate)
|
||||||
|
}
|
||||||
|
|
||||||
|
match = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultFunc.reset()
|
||||||
|
|
||||||
|
count = [0, 0, 0]
|
||||||
|
for (m = 0; m < candidates.length; m++) {
|
||||||
|
count[candidates[m].group]++
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count[defNone] > 0) {
|
||||||
|
defaultResult = defFalse
|
||||||
|
} else {
|
||||||
|
defaultResult = defTrue
|
||||||
|
if (count[defTrue] + count[defFalse] > 1) {
|
||||||
|
throw {
|
||||||
|
type: 'Runtime',
|
||||||
|
message: `Ambiguous use of \`default()\` found when matching for \`${this.format(
|
||||||
|
args
|
||||||
|
)}\``,
|
||||||
|
index: this.getIndex(),
|
||||||
|
filename: this.fileInfo().filename
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (m = 0; m < candidates.length; m++) {
|
||||||
|
candidate = candidates[m].group
|
||||||
|
if (candidate === defNone || candidate === defaultResult) {
|
||||||
|
try {
|
||||||
|
mixin = candidates[m].mixin
|
||||||
|
if (!(mixin instanceof MixinDefinition)) {
|
||||||
|
originalRuleset = mixin.originalRuleset || mixin
|
||||||
|
mixin = new MixinDefinition(
|
||||||
|
'',
|
||||||
|
[],
|
||||||
|
mixin.rules,
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
originalRuleset.visibilityInfo()
|
||||||
|
)
|
||||||
|
mixin.originalRuleset = originalRuleset
|
||||||
|
}
|
||||||
|
const newRules = mixin.evalCall(
|
||||||
|
context,
|
||||||
|
args,
|
||||||
|
this.important
|
||||||
|
).rules
|
||||||
|
this._setVisibilityToReplacement(newRules)
|
||||||
|
Array.prototype.push.apply(rules, newRules)
|
||||||
|
} catch (e) {
|
||||||
|
throw {
|
||||||
|
message: e.message,
|
||||||
|
index: this.getIndex(),
|
||||||
|
filename: this.fileInfo().filename,
|
||||||
|
stack: e.stack
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
return rules
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isOneFound) {
|
||||||
|
throw {
|
||||||
|
type: 'Runtime',
|
||||||
|
message: `No matching definition was found for \`${this.format(
|
||||||
|
args
|
||||||
|
)}\``,
|
||||||
|
index: this.getIndex(),
|
||||||
|
filename: this.fileInfo().filename
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw {
|
||||||
|
type: 'Name',
|
||||||
|
message: `${this.selector.toCSS().trim()} is undefined`,
|
||||||
|
index: this.getIndex(),
|
||||||
|
filename: this.fileInfo().filename
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_setVisibilityToReplacement(replacement) {
|
||||||
|
let i, rule
|
||||||
|
if (this.blocksVisibility()) {
|
||||||
|
for (i = 0; i < replacement.length; i++) {
|
||||||
|
rule = replacement[i]
|
||||||
|
rule.addVisibilityBlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
format(args) {
|
||||||
|
return `${this.selector.toCSS().trim()}(${
|
||||||
|
args
|
||||||
|
? args
|
||||||
|
.map(function (a) {
|
||||||
|
let argValue = ''
|
||||||
|
if (a.name) {
|
||||||
|
argValue += `${a.name}:`
|
||||||
|
}
|
||||||
|
if (a.value.toCSS) {
|
||||||
|
argValue += a.value.toCSS()
|
||||||
|
} else {
|
||||||
|
argValue += '???'
|
||||||
|
}
|
||||||
|
return argValue
|
||||||
|
})
|
||||||
|
.join(', ')
|
||||||
|
: ''
|
||||||
|
})`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default MixinCall
|
|
@ -0,0 +1,300 @@
|
||||||
|
import Selector from './selector'
|
||||||
|
import Element from './element'
|
||||||
|
import Ruleset from './ruleset'
|
||||||
|
import Declaration from './declaration'
|
||||||
|
import DetachedRuleset from './detached-ruleset'
|
||||||
|
import Expression from './expression'
|
||||||
|
import contexts from '../contexts'
|
||||||
|
import * as utils from '../utils'
|
||||||
|
|
||||||
|
const Definition = function (
|
||||||
|
name,
|
||||||
|
params,
|
||||||
|
rules,
|
||||||
|
condition,
|
||||||
|
variadic,
|
||||||
|
frames,
|
||||||
|
visibilityInfo
|
||||||
|
) {
|
||||||
|
this.name = name || 'anonymous mixin'
|
||||||
|
this.selectors = [
|
||||||
|
new Selector([new Element(null, name, false, this._index, this._fileInfo)])
|
||||||
|
]
|
||||||
|
this.params = params
|
||||||
|
this.condition = condition
|
||||||
|
this.variadic = variadic
|
||||||
|
this.arity = params.length
|
||||||
|
this.rules = rules
|
||||||
|
this._lookups = {}
|
||||||
|
const optionalParameters = []
|
||||||
|
this.required = params.reduce(function (count, p) {
|
||||||
|
if (!p.name || (p.name && !p.value)) {
|
||||||
|
return count + 1
|
||||||
|
} else {
|
||||||
|
optionalParameters.push(p.name)
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
}, 0)
|
||||||
|
this.optionalParameters = optionalParameters
|
||||||
|
this.frames = frames
|
||||||
|
this.copyVisibilityInfo(visibilityInfo)
|
||||||
|
this.allowRoot = true
|
||||||
|
}
|
||||||
|
|
||||||
|
Definition.prototype = Object.assign(new Ruleset(), {
|
||||||
|
type: 'MixinDefinition',
|
||||||
|
evalFirst: true,
|
||||||
|
|
||||||
|
accept(visitor) {
|
||||||
|
if (this.params && this.params.length) {
|
||||||
|
this.params = visitor.visitArray(this.params)
|
||||||
|
}
|
||||||
|
this.rules = visitor.visitArray(this.rules)
|
||||||
|
if (this.condition) {
|
||||||
|
this.condition = visitor.visit(this.condition)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
evalParams(context, mixinEnv, args, evaldArguments) {
|
||||||
|
/* jshint boss:true */
|
||||||
|
const frame = new Ruleset(null, null)
|
||||||
|
|
||||||
|
let varargs
|
||||||
|
let arg
|
||||||
|
const params = utils.copyArray(this.params)
|
||||||
|
let i
|
||||||
|
let j
|
||||||
|
let val
|
||||||
|
let name
|
||||||
|
let isNamedFound
|
||||||
|
let argIndex
|
||||||
|
let argsLength = 0
|
||||||
|
|
||||||
|
if (
|
||||||
|
mixinEnv.frames &&
|
||||||
|
mixinEnv.frames[0] &&
|
||||||
|
mixinEnv.frames[0].functionRegistry
|
||||||
|
) {
|
||||||
|
frame.functionRegistry = mixinEnv.frames[0].functionRegistry.inherit()
|
||||||
|
}
|
||||||
|
mixinEnv = new contexts.Eval(mixinEnv, [frame].concat(mixinEnv.frames))
|
||||||
|
|
||||||
|
if (args) {
|
||||||
|
args = utils.copyArray(args)
|
||||||
|
argsLength = args.length
|
||||||
|
|
||||||
|
for (i = 0; i < argsLength; i++) {
|
||||||
|
arg = args[i]
|
||||||
|
if ((name = arg && arg.name)) {
|
||||||
|
isNamedFound = false
|
||||||
|
for (j = 0; j < params.length; j++) {
|
||||||
|
if (!evaldArguments[j] && name === params[j].name) {
|
||||||
|
evaldArguments[j] = arg.value.eval(context)
|
||||||
|
frame.prependRule(new Declaration(name, arg.value.eval(context)))
|
||||||
|
isNamedFound = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isNamedFound) {
|
||||||
|
args.splice(i, 1)
|
||||||
|
i--
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
throw {
|
||||||
|
type: 'Runtime',
|
||||||
|
message: `Named argument for ${this.name} ${args[i].name} not found`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
argIndex = 0
|
||||||
|
for (i = 0; i < params.length; i++) {
|
||||||
|
if (evaldArguments[i]) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
arg = args && args[argIndex]
|
||||||
|
|
||||||
|
if ((name = params[i].name)) {
|
||||||
|
if (params[i].variadic) {
|
||||||
|
varargs = []
|
||||||
|
for (j = argIndex; j < argsLength; j++) {
|
||||||
|
varargs.push(args[j].value.eval(context))
|
||||||
|
}
|
||||||
|
frame.prependRule(
|
||||||
|
new Declaration(name, new Expression(varargs).eval(context))
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val = arg && arg.value
|
||||||
|
if (val) {
|
||||||
|
// This was a mixin call, pass in a detached ruleset of it's eval'd rules
|
||||||
|
if (Array.isArray(val)) {
|
||||||
|
val = new DetachedRuleset(new Ruleset('', val))
|
||||||
|
} else {
|
||||||
|
val = val.eval(context)
|
||||||
|
}
|
||||||
|
} else if (params[i].value) {
|
||||||
|
val = params[i].value.eval(mixinEnv)
|
||||||
|
frame.resetCache()
|
||||||
|
} else {
|
||||||
|
throw {
|
||||||
|
type: 'Runtime',
|
||||||
|
message: `wrong number of arguments for ${this.name} (${argsLength} for ${this.arity})`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
frame.prependRule(new Declaration(name, val))
|
||||||
|
evaldArguments[i] = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params[i].variadic && args) {
|
||||||
|
for (j = argIndex; j < argsLength; j++) {
|
||||||
|
evaldArguments[j] = args[j].value.eval(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
argIndex++
|
||||||
|
}
|
||||||
|
|
||||||
|
return frame
|
||||||
|
},
|
||||||
|
|
||||||
|
makeImportant() {
|
||||||
|
const rules = !this.rules
|
||||||
|
? this.rules
|
||||||
|
: this.rules.map(function (r) {
|
||||||
|
if (r.makeImportant) {
|
||||||
|
return r.makeImportant(true)
|
||||||
|
} else {
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const result = new Definition(
|
||||||
|
this.name,
|
||||||
|
this.params,
|
||||||
|
rules,
|
||||||
|
this.condition,
|
||||||
|
this.variadic,
|
||||||
|
this.frames
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
|
||||||
|
eval(context) {
|
||||||
|
return new Definition(
|
||||||
|
this.name,
|
||||||
|
this.params,
|
||||||
|
this.rules,
|
||||||
|
this.condition,
|
||||||
|
this.variadic,
|
||||||
|
this.frames || utils.copyArray(context.frames)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
evalCall(context, args, important) {
|
||||||
|
const _arguments = []
|
||||||
|
const mixinFrames = this.frames
|
||||||
|
? this.frames.concat(context.frames)
|
||||||
|
: context.frames
|
||||||
|
const frame = this.evalParams(
|
||||||
|
context,
|
||||||
|
new contexts.Eval(context, mixinFrames),
|
||||||
|
args,
|
||||||
|
_arguments
|
||||||
|
)
|
||||||
|
let rules
|
||||||
|
let ruleset
|
||||||
|
|
||||||
|
frame.prependRule(
|
||||||
|
new Declaration('@arguments', new Expression(_arguments).eval(context))
|
||||||
|
)
|
||||||
|
|
||||||
|
rules = utils.copyArray(this.rules)
|
||||||
|
|
||||||
|
ruleset = new Ruleset(null, rules)
|
||||||
|
ruleset.originalRuleset = this
|
||||||
|
ruleset = ruleset.eval(
|
||||||
|
new contexts.Eval(context, [this, frame].concat(mixinFrames))
|
||||||
|
)
|
||||||
|
if (important) {
|
||||||
|
ruleset = ruleset.makeImportant()
|
||||||
|
}
|
||||||
|
return ruleset
|
||||||
|
},
|
||||||
|
|
||||||
|
matchCondition(args, context) {
|
||||||
|
if (
|
||||||
|
this.condition &&
|
||||||
|
!this.condition.eval(
|
||||||
|
new contexts.Eval(
|
||||||
|
context,
|
||||||
|
[
|
||||||
|
this.evalParams(
|
||||||
|
context /* the parameter variables */,
|
||||||
|
new contexts.Eval(
|
||||||
|
context,
|
||||||
|
this.frames
|
||||||
|
? this.frames.concat(context.frames)
|
||||||
|
: context.frames
|
||||||
|
),
|
||||||
|
args,
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
.concat(this.frames || []) // the parent namespace/mixin frames
|
||||||
|
.concat(context.frames)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// the current environment frames
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
|
||||||
|
matchArgs(args, context) {
|
||||||
|
const allArgsCnt = (args && args.length) || 0
|
||||||
|
let len
|
||||||
|
const optionalParameters = this.optionalParameters
|
||||||
|
const requiredArgsCnt = !args
|
||||||
|
? 0
|
||||||
|
: args.reduce(function (count, p) {
|
||||||
|
if (optionalParameters.indexOf(p.name) < 0) {
|
||||||
|
return count + 1
|
||||||
|
} else {
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
}, 0)
|
||||||
|
|
||||||
|
if (!this.variadic) {
|
||||||
|
if (requiredArgsCnt < this.required) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (allArgsCnt > this.params.length) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (requiredArgsCnt < this.required - 1) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check patterns
|
||||||
|
len = Math.min(requiredArgsCnt, this.arity)
|
||||||
|
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
if (!this.params[i].name && !this.params[i].variadic) {
|
||||||
|
if (
|
||||||
|
args[i].value.eval(context).toCSS() !=
|
||||||
|
this.params[i].value.eval(context).toCSS()
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default Definition
|
|
@ -0,0 +1,85 @@
|
||||||
|
import Node from './node'
|
||||||
|
import Variable from './variable'
|
||||||
|
import Ruleset from './ruleset'
|
||||||
|
import Selector from './selector'
|
||||||
|
|
||||||
|
const NamespaceValue = function (ruleCall, lookups, index, fileInfo) {
|
||||||
|
this.value = ruleCall
|
||||||
|
this.lookups = lookups
|
||||||
|
this._index = index
|
||||||
|
this._fileInfo = fileInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
NamespaceValue.prototype = Object.assign(new Node(), {
|
||||||
|
type: 'NamespaceValue',
|
||||||
|
|
||||||
|
eval(context) {
|
||||||
|
let i,
|
||||||
|
name,
|
||||||
|
rules = this.value.eval(context)
|
||||||
|
|
||||||
|
for (i = 0; i < this.lookups.length; i++) {
|
||||||
|
name = this.lookups[i]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Eval'd DRs return rulesets.
|
||||||
|
* Eval'd mixins return rules, so let's make a ruleset if we need it.
|
||||||
|
* We need to do this because of late parsing of values
|
||||||
|
*/
|
||||||
|
if (Array.isArray(rules)) {
|
||||||
|
rules = new Ruleset([new Selector()], rules)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === '') {
|
||||||
|
rules = rules.lastDeclaration()
|
||||||
|
} else if (name.charAt(0) === '@') {
|
||||||
|
if (name.charAt(1) === '@') {
|
||||||
|
name = `@${new Variable(name.substr(1)).eval(context).value}`
|
||||||
|
}
|
||||||
|
if (rules.variables) {
|
||||||
|
rules = rules.variable(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rules) {
|
||||||
|
throw {
|
||||||
|
type: 'Name',
|
||||||
|
message: `variable ${name} not found`,
|
||||||
|
filename: this.fileInfo().filename,
|
||||||
|
index: this.getIndex()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (name.substring(0, 2) === '$@') {
|
||||||
|
name = `$${new Variable(name.substr(1)).eval(context).value}`
|
||||||
|
} else {
|
||||||
|
name = name.charAt(0) === '$' ? name : `$${name}`
|
||||||
|
}
|
||||||
|
if (rules.properties) {
|
||||||
|
rules = rules.property(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rules) {
|
||||||
|
throw {
|
||||||
|
type: 'Name',
|
||||||
|
message: `property "${name.substr(1)}" not found`,
|
||||||
|
filename: this.fileInfo().filename,
|
||||||
|
index: this.getIndex()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Properties are an array of values, since a ruleset can have multiple props.
|
||||||
|
// We pick the last one (the "cascaded" value)
|
||||||
|
rules = rules[rules.length - 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rules.value) {
|
||||||
|
rules = rules.eval(context).value
|
||||||
|
}
|
||||||
|
if (rules.ruleset) {
|
||||||
|
rules = rules.ruleset.eval(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rules
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default NamespaceValue
|
|
@ -0,0 +1,25 @@
|
||||||
|
import Node from './node'
|
||||||
|
import Operation from './operation'
|
||||||
|
import Dimension from './dimension'
|
||||||
|
|
||||||
|
const Negative = function (node) {
|
||||||
|
this.value = node
|
||||||
|
}
|
||||||
|
|
||||||
|
Negative.prototype = Object.assign(new Node(), {
|
||||||
|
type: 'Negative',
|
||||||
|
|
||||||
|
genCSS(context, output) {
|
||||||
|
output.add('-')
|
||||||
|
this.value.genCSS(context, output)
|
||||||
|
},
|
||||||
|
|
||||||
|
eval(context) {
|
||||||
|
if (context.isMathOn()) {
|
||||||
|
return new Operation('*', [new Dimension(-1), this.value]).eval(context)
|
||||||
|
}
|
||||||
|
return new Negative(this.value.eval(context))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default Negative
|
|
@ -0,0 +1,193 @@
|
||||||
|
/**
|
||||||
|
* The reason why Node is a class and other nodes simply do not extend
|
||||||
|
* from Node (since we're transpiling) is due to this issue:
|
||||||
|
*
|
||||||
|
* @see https://github.com/less/less.js/issues/3434
|
||||||
|
*/
|
||||||
|
class Node {
|
||||||
|
constructor() {
|
||||||
|
this.parent = null
|
||||||
|
this.visibilityBlocks = undefined
|
||||||
|
this.nodeVisible = undefined
|
||||||
|
this.rootNode = null
|
||||||
|
this.parsed = null
|
||||||
|
}
|
||||||
|
|
||||||
|
get currentFileInfo() {
|
||||||
|
return this.fileInfo()
|
||||||
|
}
|
||||||
|
|
||||||
|
get index() {
|
||||||
|
return this.getIndex()
|
||||||
|
}
|
||||||
|
|
||||||
|
setParent(nodes, parent) {
|
||||||
|
function set(node) {
|
||||||
|
if (node && node instanceof Node) {
|
||||||
|
node.parent = parent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Array.isArray(nodes)) {
|
||||||
|
nodes.forEach(set)
|
||||||
|
} else {
|
||||||
|
set(nodes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getIndex() {
|
||||||
|
return this._index || (this.parent && this.parent.getIndex()) || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fileInfo() {
|
||||||
|
return this._fileInfo || (this.parent && this.parent.fileInfo()) || {}
|
||||||
|
}
|
||||||
|
|
||||||
|
isRulesetLike() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
toCSS(context) {
|
||||||
|
const strs = []
|
||||||
|
this.genCSS(context, {
|
||||||
|
// remove when genCSS has JSDoc types
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
add: function (chunk, fileInfo, index) {
|
||||||
|
strs.push(chunk)
|
||||||
|
},
|
||||||
|
isEmpty: function () {
|
||||||
|
return strs.length === 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return strs.join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
genCSS(context, output) {
|
||||||
|
output.add(this.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
accept(visitor) {
|
||||||
|
this.value = visitor.visit(this.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
eval() {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
_operate(context, op, a, b) {
|
||||||
|
switch (op) {
|
||||||
|
case '+':
|
||||||
|
return a + b
|
||||||
|
case '-':
|
||||||
|
return a - b
|
||||||
|
case '*':
|
||||||
|
return a * b
|
||||||
|
case '/':
|
||||||
|
return a / b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fround(context, value) {
|
||||||
|
const precision = context && context.numPrecision
|
||||||
|
// add "epsilon" to ensure numbers like 1.000000005 (represented as 1.000000004999...) are properly rounded:
|
||||||
|
return precision ? Number((value + 2e-16).toFixed(precision)) : value
|
||||||
|
}
|
||||||
|
|
||||||
|
static compare(a, b) {
|
||||||
|
/* returns:
|
||||||
|
-1: a < b
|
||||||
|
0: a = b
|
||||||
|
1: a > b
|
||||||
|
and *any* other value for a != b (e.g. undefined, NaN, -2 etc.) */
|
||||||
|
|
||||||
|
if (
|
||||||
|
a.compare &&
|
||||||
|
// for "symmetric results" force toCSS-based comparison
|
||||||
|
// of Quoted or Anonymous if either value is one of those
|
||||||
|
!(b.type === 'Quoted' || b.type === 'Anonymous')
|
||||||
|
) {
|
||||||
|
return a.compare(b)
|
||||||
|
} else if (b.compare) {
|
||||||
|
return -b.compare(a)
|
||||||
|
} else if (a.type !== b.type) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
a = a.value
|
||||||
|
b = b.value
|
||||||
|
if (!Array.isArray(a)) {
|
||||||
|
return a === b ? 0 : undefined
|
||||||
|
}
|
||||||
|
if (a.length !== b.length) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
for (let i = 0; i < a.length; i++) {
|
||||||
|
if (Node.compare(a[i], b[i]) !== 0) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
static numericCompare(a, b) {
|
||||||
|
return a < b ? -1 : a === b ? 0 : a > b ? 1 : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns true if this node represents root of ast imported by reference
|
||||||
|
blocksVisibility() {
|
||||||
|
if (this.visibilityBlocks === undefined) {
|
||||||
|
this.visibilityBlocks = 0
|
||||||
|
}
|
||||||
|
return this.visibilityBlocks !== 0
|
||||||
|
}
|
||||||
|
|
||||||
|
addVisibilityBlock() {
|
||||||
|
if (this.visibilityBlocks === undefined) {
|
||||||
|
this.visibilityBlocks = 0
|
||||||
|
}
|
||||||
|
this.visibilityBlocks = this.visibilityBlocks + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
removeVisibilityBlock() {
|
||||||
|
if (this.visibilityBlocks === undefined) {
|
||||||
|
this.visibilityBlocks = 0
|
||||||
|
}
|
||||||
|
this.visibilityBlocks = this.visibilityBlocks - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Turns on node visibility - if called node will be shown in output regardless
|
||||||
|
// of whether it comes from import by reference or not
|
||||||
|
ensureVisibility() {
|
||||||
|
this.nodeVisible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Turns off node visibility - if called node will NOT be shown in output regardless
|
||||||
|
// of whether it comes from import by reference or not
|
||||||
|
ensureInvisibility() {
|
||||||
|
this.nodeVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// return values:
|
||||||
|
// false - the node must not be visible
|
||||||
|
// true - the node must be visible
|
||||||
|
// undefined or null - the node has the same visibility as its parent
|
||||||
|
isVisible() {
|
||||||
|
return this.nodeVisible
|
||||||
|
}
|
||||||
|
|
||||||
|
visibilityInfo() {
|
||||||
|
return {
|
||||||
|
visibilityBlocks: this.visibilityBlocks,
|
||||||
|
nodeVisible: this.nodeVisible
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
copyVisibilityInfo(info) {
|
||||||
|
if (!info) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.visibilityBlocks = info.visibilityBlocks
|
||||||
|
this.nodeVisible = info.nodeVisible
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Node
|
|
@ -0,0 +1,63 @@
|
||||||
|
import Node from './node'
|
||||||
|
import Color from './color'
|
||||||
|
import Dimension from './dimension'
|
||||||
|
import * as Constants from '../constants'
|
||||||
|
const MATH = Constants.Math
|
||||||
|
|
||||||
|
const Operation = function (op, operands, isSpaced) {
|
||||||
|
this.op = op.trim()
|
||||||
|
this.operands = operands
|
||||||
|
this.isSpaced = isSpaced
|
||||||
|
}
|
||||||
|
|
||||||
|
Operation.prototype = Object.assign(new Node(), {
|
||||||
|
type: 'Operation',
|
||||||
|
|
||||||
|
accept(visitor) {
|
||||||
|
this.operands = visitor.visitArray(this.operands)
|
||||||
|
},
|
||||||
|
|
||||||
|
eval(context) {
|
||||||
|
let a = this.operands[0].eval(context),
|
||||||
|
b = this.operands[1].eval(context),
|
||||||
|
op
|
||||||
|
|
||||||
|
if (context.isMathOn(this.op)) {
|
||||||
|
op = this.op === './' ? '/' : this.op
|
||||||
|
if (a instanceof Dimension && b instanceof Color) {
|
||||||
|
a = a.toColor()
|
||||||
|
}
|
||||||
|
if (b instanceof Dimension && a instanceof Color) {
|
||||||
|
b = b.toColor()
|
||||||
|
}
|
||||||
|
if (!a.operate || !b.operate) {
|
||||||
|
if (
|
||||||
|
(a instanceof Operation || b instanceof Operation) &&
|
||||||
|
a.op === '/' &&
|
||||||
|
context.math === MATH.PARENS_DIVISION
|
||||||
|
) {
|
||||||
|
return new Operation(this.op, [a, b], this.isSpaced)
|
||||||
|
}
|
||||||
|
throw { type: 'Operation', message: 'Operation on an invalid type' }
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.operate(context, op, b)
|
||||||
|
} else {
|
||||||
|
return new Operation(this.op, [a, b], this.isSpaced)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
genCSS(context, output) {
|
||||||
|
this.operands[0].genCSS(context, output)
|
||||||
|
if (this.isSpaced) {
|
||||||
|
output.add(' ')
|
||||||
|
}
|
||||||
|
output.add(this.op)
|
||||||
|
if (this.isSpaced) {
|
||||||
|
output.add(' ')
|
||||||
|
}
|
||||||
|
this.operands[1].genCSS(context, output)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default Operation
|
|
@ -0,0 +1,21 @@
|
||||||
|
import Node from './node'
|
||||||
|
|
||||||
|
const Paren = function (node) {
|
||||||
|
this.value = node
|
||||||
|
}
|
||||||
|
|
||||||
|
Paren.prototype = Object.assign(new Node(), {
|
||||||
|
type: 'Paren',
|
||||||
|
|
||||||
|
genCSS(context, output) {
|
||||||
|
output.add('(')
|
||||||
|
this.value.genCSS(context, output)
|
||||||
|
output.add(')')
|
||||||
|
},
|
||||||
|
|
||||||
|
eval(context) {
|
||||||
|
return new Paren(this.value.eval(context))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default Paren
|
|
@ -0,0 +1,85 @@
|
||||||
|
import Node from './node'
|
||||||
|
import Declaration from './declaration'
|
||||||
|
|
||||||
|
const Property = function (name, index, currentFileInfo) {
|
||||||
|
this.name = name
|
||||||
|
this._index = index
|
||||||
|
this._fileInfo = currentFileInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
Property.prototype = Object.assign(new Node(), {
|
||||||
|
type: 'Property',
|
||||||
|
|
||||||
|
eval(context) {
|
||||||
|
let property
|
||||||
|
const name = this.name
|
||||||
|
// TODO: shorten this reference
|
||||||
|
const mergeRules =
|
||||||
|
context.pluginManager.less.visitors.ToCSSVisitor.prototype._mergeRules
|
||||||
|
|
||||||
|
if (this.evaluating) {
|
||||||
|
throw {
|
||||||
|
type: 'Name',
|
||||||
|
message: `Recursive property reference for ${name}`,
|
||||||
|
filename: this.fileInfo().filename,
|
||||||
|
index: this.getIndex()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.evaluating = true
|
||||||
|
|
||||||
|
property = this.find(context.frames, function (frame) {
|
||||||
|
let v
|
||||||
|
const vArr = frame.property(name)
|
||||||
|
if (vArr) {
|
||||||
|
for (let i = 0; i < vArr.length; i++) {
|
||||||
|
v = vArr[i]
|
||||||
|
|
||||||
|
vArr[i] = new Declaration(
|
||||||
|
v.name,
|
||||||
|
v.value,
|
||||||
|
v.important,
|
||||||
|
v.merge,
|
||||||
|
v.index,
|
||||||
|
v.currentFileInfo,
|
||||||
|
v.inline,
|
||||||
|
v.variable
|
||||||
|
)
|
||||||
|
}
|
||||||
|
mergeRules(vArr)
|
||||||
|
|
||||||
|
v = vArr[vArr.length - 1]
|
||||||
|
if (v.important) {
|
||||||
|
const importantScope =
|
||||||
|
context.importantScope[context.importantScope.length - 1]
|
||||||
|
importantScope.important = v.important
|
||||||
|
}
|
||||||
|
v = v.value.eval(context)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (property) {
|
||||||
|
this.evaluating = false
|
||||||
|
return property
|
||||||
|
} else {
|
||||||
|
throw {
|
||||||
|
type: 'Name',
|
||||||
|
message: `Property '${name}' is undefined`,
|
||||||
|
filename: this.currentFileInfo.filename,
|
||||||
|
index: this.index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
find(obj, fun) {
|
||||||
|
for (let i = 0, r; i < obj.length; i++) {
|
||||||
|
r = fun.call(obj, obj[i])
|
||||||
|
if (r) {
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default Property
|
|
@ -0,0 +1,79 @@
|
||||||
|
import Node from './node'
|
||||||
|
import Variable from './variable'
|
||||||
|
import Property from './property'
|
||||||
|
|
||||||
|
const Quoted = function (str, content, escaped, index, currentFileInfo) {
|
||||||
|
this.escaped = escaped === undefined ? true : escaped
|
||||||
|
this.value = content || ''
|
||||||
|
this.quote = str.charAt(0)
|
||||||
|
this._index = index
|
||||||
|
this._fileInfo = currentFileInfo
|
||||||
|
this.variableRegex = /@\{([\w-]+)\}/g
|
||||||
|
this.propRegex = /\$\{([\w-]+)\}/g
|
||||||
|
this.allowRoot = escaped
|
||||||
|
}
|
||||||
|
|
||||||
|
Quoted.prototype = Object.assign(new Node(), {
|
||||||
|
type: 'Quoted',
|
||||||
|
|
||||||
|
genCSS(context, output) {
|
||||||
|
if (!this.escaped) {
|
||||||
|
output.add(this.quote, this.fileInfo(), this.getIndex())
|
||||||
|
}
|
||||||
|
output.add(this.value)
|
||||||
|
if (!this.escaped) {
|
||||||
|
output.add(this.quote)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
containsVariables() {
|
||||||
|
return this.value.match(this.variableRegex)
|
||||||
|
},
|
||||||
|
|
||||||
|
eval(context) {
|
||||||
|
const that = this
|
||||||
|
let value = this.value
|
||||||
|
const variableReplacement = function (_, name) {
|
||||||
|
const v = new Variable(`@${name}`, that.getIndex(), that.fileInfo()).eval(
|
||||||
|
context,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
return v instanceof Quoted ? v.value : v.toCSS()
|
||||||
|
}
|
||||||
|
const propertyReplacement = function (_, name) {
|
||||||
|
const v = new Property(`$${name}`, that.getIndex(), that.fileInfo()).eval(
|
||||||
|
context,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
return v instanceof Quoted ? v.value : v.toCSS()
|
||||||
|
}
|
||||||
|
function iterativeReplace(value, regexp, replacementFnc) {
|
||||||
|
let evaluatedValue = value
|
||||||
|
do {
|
||||||
|
value = evaluatedValue.toString()
|
||||||
|
evaluatedValue = value.replace(regexp, replacementFnc)
|
||||||
|
} while (value !== evaluatedValue)
|
||||||
|
return evaluatedValue
|
||||||
|
}
|
||||||
|
value = iterativeReplace(value, this.variableRegex, variableReplacement)
|
||||||
|
value = iterativeReplace(value, this.propRegex, propertyReplacement)
|
||||||
|
return new Quoted(
|
||||||
|
this.quote + value + this.quote,
|
||||||
|
value,
|
||||||
|
this.escaped,
|
||||||
|
this.getIndex(),
|
||||||
|
this.fileInfo()
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
compare(other) {
|
||||||
|
// when comparing quoted strings allow the quote to differ
|
||||||
|
if (other.type === 'Quoted' && !this.escaped && !other.escaped) {
|
||||||
|
return Node.numericCompare(this.value, other.value)
|
||||||
|
} else {
|
||||||
|
return other.toCSS && this.toCSS() === other.toCSS() ? 0 : undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default Quoted
|
|
@ -0,0 +1,965 @@
|
||||||
|
import Node from './node'
|
||||||
|
import Declaration from './declaration'
|
||||||
|
import Keyword from './keyword'
|
||||||
|
import Comment from './comment'
|
||||||
|
import Paren from './paren'
|
||||||
|
import Selector from './selector'
|
||||||
|
import Element from './element'
|
||||||
|
import Anonymous from './anonymous'
|
||||||
|
import contexts from '../contexts'
|
||||||
|
import globalFunctionRegistry from '../functions/function-registry'
|
||||||
|
import defaultFunc from '../functions/default'
|
||||||
|
import getDebugInfo from './debug-info'
|
||||||
|
import * as utils from '../utils'
|
||||||
|
import Parser from '../parser/parser'
|
||||||
|
|
||||||
|
const Ruleset = function (selectors, rules, strictImports, visibilityInfo) {
|
||||||
|
this.selectors = selectors
|
||||||
|
this.rules = rules
|
||||||
|
this._lookups = {}
|
||||||
|
this._variables = null
|
||||||
|
this._properties = null
|
||||||
|
this.strictImports = strictImports
|
||||||
|
this.copyVisibilityInfo(visibilityInfo)
|
||||||
|
this.allowRoot = true
|
||||||
|
|
||||||
|
this.setParent(this.selectors, this)
|
||||||
|
this.setParent(this.rules, this)
|
||||||
|
}
|
||||||
|
|
||||||
|
Ruleset.prototype = Object.assign(new Node(), {
|
||||||
|
type: 'Ruleset',
|
||||||
|
isRuleset: true,
|
||||||
|
|
||||||
|
isRulesetLike() {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
|
||||||
|
accept(visitor) {
|
||||||
|
if (this.paths) {
|
||||||
|
this.paths = visitor.visitArray(this.paths, true)
|
||||||
|
} else if (this.selectors) {
|
||||||
|
this.selectors = visitor.visitArray(this.selectors)
|
||||||
|
}
|
||||||
|
if (this.rules && this.rules.length) {
|
||||||
|
this.rules = visitor.visitArray(this.rules)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
eval(context) {
|
||||||
|
let selectors
|
||||||
|
let selCnt
|
||||||
|
let selector
|
||||||
|
let i
|
||||||
|
let hasVariable
|
||||||
|
let hasOnePassingSelector = false
|
||||||
|
|
||||||
|
if (this.selectors && (selCnt = this.selectors.length)) {
|
||||||
|
selectors = new Array(selCnt)
|
||||||
|
defaultFunc.error({
|
||||||
|
type: 'Syntax',
|
||||||
|
message: 'it is currently only allowed in parametric mixin guards,'
|
||||||
|
})
|
||||||
|
|
||||||
|
for (i = 0; i < selCnt; i++) {
|
||||||
|
selector = this.selectors[i].eval(context)
|
||||||
|
for (let j = 0; j < selector.elements.length; j++) {
|
||||||
|
if (selector.elements[j].isVariable) {
|
||||||
|
hasVariable = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
selectors[i] = selector
|
||||||
|
if (selector.evaldCondition) {
|
||||||
|
hasOnePassingSelector = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasVariable) {
|
||||||
|
const toParseSelectors = new Array(selCnt)
|
||||||
|
for (i = 0; i < selCnt; i++) {
|
||||||
|
selector = selectors[i]
|
||||||
|
toParseSelectors[i] = selector.toCSS(context)
|
||||||
|
}
|
||||||
|
const startingIndex = selectors[0].getIndex()
|
||||||
|
const selectorFileInfo = selectors[0].fileInfo()
|
||||||
|
new Parser(
|
||||||
|
context,
|
||||||
|
this.parse.importManager,
|
||||||
|
selectorFileInfo,
|
||||||
|
startingIndex
|
||||||
|
).parseNode(
|
||||||
|
toParseSelectors.join(','),
|
||||||
|
['selectors'],
|
||||||
|
function (err, result) {
|
||||||
|
if (result) {
|
||||||
|
selectors = utils.flattenArray(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultFunc.reset()
|
||||||
|
} else {
|
||||||
|
hasOnePassingSelector = true
|
||||||
|
}
|
||||||
|
|
||||||
|
let rules = this.rules ? utils.copyArray(this.rules) : null
|
||||||
|
const ruleset = new Ruleset(
|
||||||
|
selectors,
|
||||||
|
rules,
|
||||||
|
this.strictImports,
|
||||||
|
this.visibilityInfo()
|
||||||
|
)
|
||||||
|
let rule
|
||||||
|
let subRule
|
||||||
|
|
||||||
|
ruleset.originalRuleset = this
|
||||||
|
ruleset.root = this.root
|
||||||
|
ruleset.firstRoot = this.firstRoot
|
||||||
|
ruleset.allowImports = this.allowImports
|
||||||
|
|
||||||
|
if (this.debugInfo) {
|
||||||
|
ruleset.debugInfo = this.debugInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasOnePassingSelector) {
|
||||||
|
rules.length = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// inherit a function registry from the frames stack when possible;
|
||||||
|
// otherwise from the global registry
|
||||||
|
ruleset.functionRegistry = (function (frames) {
|
||||||
|
let i = 0
|
||||||
|
const n = frames.length
|
||||||
|
let found
|
||||||
|
for (; i !== n; ++i) {
|
||||||
|
found = frames[i].functionRegistry
|
||||||
|
if (found) {
|
||||||
|
return found
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return globalFunctionRegistry
|
||||||
|
})(context.frames).inherit()
|
||||||
|
|
||||||
|
// push the current ruleset to the frames stack
|
||||||
|
const ctxFrames = context.frames
|
||||||
|
ctxFrames.unshift(ruleset)
|
||||||
|
|
||||||
|
// currrent selectors
|
||||||
|
let ctxSelectors = context.selectors
|
||||||
|
if (!ctxSelectors) {
|
||||||
|
context.selectors = ctxSelectors = []
|
||||||
|
}
|
||||||
|
ctxSelectors.unshift(this.selectors)
|
||||||
|
|
||||||
|
// Evaluate imports
|
||||||
|
if (ruleset.root || ruleset.allowImports || !ruleset.strictImports) {
|
||||||
|
ruleset.evalImports(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the frames around mixin definitions,
|
||||||
|
// so they can be evaluated like closures when the time comes.
|
||||||
|
const rsRules = ruleset.rules
|
||||||
|
for (i = 0; (rule = rsRules[i]); i++) {
|
||||||
|
if (rule.evalFirst) {
|
||||||
|
rsRules[i] = rule.eval(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mediaBlockCount =
|
||||||
|
(context.mediaBlocks && context.mediaBlocks.length) || 0
|
||||||
|
|
||||||
|
// Evaluate mixin calls.
|
||||||
|
for (i = 0; (rule = rsRules[i]); i++) {
|
||||||
|
if (rule.type === 'MixinCall') {
|
||||||
|
/* jshint loopfunc:true */
|
||||||
|
rules = rule.eval(context).filter(function (r) {
|
||||||
|
if (r instanceof Declaration && r.variable) {
|
||||||
|
// do not pollute the scope if the variable is
|
||||||
|
// already there. consider returning false here
|
||||||
|
// but we need a way to "return" variable from mixins
|
||||||
|
return !ruleset.variable(r.name)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
rsRules.splice.apply(rsRules, [i, 1].concat(rules))
|
||||||
|
i += rules.length - 1
|
||||||
|
ruleset.resetCache()
|
||||||
|
} else if (rule.type === 'VariableCall') {
|
||||||
|
/* jshint loopfunc:true */
|
||||||
|
rules = rule.eval(context).rules.filter(function (r) {
|
||||||
|
if (r instanceof Declaration && r.variable) {
|
||||||
|
// do not pollute the scope at all
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
rsRules.splice.apply(rsRules, [i, 1].concat(rules))
|
||||||
|
i += rules.length - 1
|
||||||
|
ruleset.resetCache()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Evaluate everything else
|
||||||
|
for (i = 0; (rule = rsRules[i]); i++) {
|
||||||
|
if (!rule.evalFirst) {
|
||||||
|
rsRules[i] = rule = rule.eval ? rule.eval(context) : rule
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Evaluate everything else
|
||||||
|
for (i = 0; (rule = rsRules[i]); i++) {
|
||||||
|
// for rulesets, check if it is a css guard and can be removed
|
||||||
|
if (
|
||||||
|
rule instanceof Ruleset &&
|
||||||
|
rule.selectors &&
|
||||||
|
rule.selectors.length === 1
|
||||||
|
) {
|
||||||
|
// check if it can be folded in (e.g. & where)
|
||||||
|
if (rule.selectors[0] && rule.selectors[0].isJustParentSelector()) {
|
||||||
|
rsRules.splice(i--, 1)
|
||||||
|
|
||||||
|
for (let j = 0; (subRule = rule.rules[j]); j++) {
|
||||||
|
if (subRule instanceof Node) {
|
||||||
|
subRule.copyVisibilityInfo(rule.visibilityInfo())
|
||||||
|
if (!(subRule instanceof Declaration) || !subRule.variable) {
|
||||||
|
rsRules.splice(++i, 0, subRule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pop the stack
|
||||||
|
ctxFrames.shift()
|
||||||
|
ctxSelectors.shift()
|
||||||
|
|
||||||
|
if (context.mediaBlocks) {
|
||||||
|
for (i = mediaBlockCount; i < context.mediaBlocks.length; i++) {
|
||||||
|
context.mediaBlocks[i].bubbleSelectors(selectors)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ruleset
|
||||||
|
},
|
||||||
|
|
||||||
|
evalImports(context) {
|
||||||
|
const rules = this.rules
|
||||||
|
let i
|
||||||
|
let importRules
|
||||||
|
if (!rules) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i = 0; i < rules.length; i++) {
|
||||||
|
if (rules[i].type === 'Import') {
|
||||||
|
importRules = rules[i].eval(context)
|
||||||
|
if (importRules && (importRules.length || importRules.length === 0)) {
|
||||||
|
rules.splice.apply(rules, [i, 1].concat(importRules))
|
||||||
|
i += importRules.length - 1
|
||||||
|
} else {
|
||||||
|
rules.splice(i, 1, importRules)
|
||||||
|
}
|
||||||
|
this.resetCache()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
makeImportant() {
|
||||||
|
const result = new Ruleset(
|
||||||
|
this.selectors,
|
||||||
|
this.rules.map(function (r) {
|
||||||
|
if (r.makeImportant) {
|
||||||
|
return r.makeImportant()
|
||||||
|
} else {
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
this.strictImports,
|
||||||
|
this.visibilityInfo()
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
|
||||||
|
matchArgs(args) {
|
||||||
|
return !args || args.length === 0
|
||||||
|
},
|
||||||
|
|
||||||
|
// lets you call a css selector with a guard
|
||||||
|
matchCondition(args, context) {
|
||||||
|
const lastSelector = this.selectors[this.selectors.length - 1]
|
||||||
|
if (!lastSelector.evaldCondition) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
lastSelector.condition &&
|
||||||
|
!lastSelector.condition.eval(new contexts.Eval(context, context.frames))
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
|
||||||
|
resetCache() {
|
||||||
|
this._rulesets = null
|
||||||
|
this._variables = null
|
||||||
|
this._properties = null
|
||||||
|
this._lookups = {}
|
||||||
|
},
|
||||||
|
|
||||||
|
variables() {
|
||||||
|
if (!this._variables) {
|
||||||
|
this._variables = !this.rules
|
||||||
|
? {}
|
||||||
|
: this.rules.reduce(function (hash, r) {
|
||||||
|
if (r instanceof Declaration && r.variable === true) {
|
||||||
|
hash[r.name] = r
|
||||||
|
}
|
||||||
|
// when evaluating variables in an import statement, imports have not been eval'd
|
||||||
|
// so we need to go inside import statements.
|
||||||
|
// guard against root being a string (in the case of inlined less)
|
||||||
|
if (r.type === 'Import' && r.root && r.root.variables) {
|
||||||
|
const vars = r.root.variables()
|
||||||
|
for (const name in vars) {
|
||||||
|
// eslint-disable-next-line no-prototype-builtins
|
||||||
|
if (vars.hasOwnProperty(name)) {
|
||||||
|
hash[name] = r.root.variable(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hash
|
||||||
|
}, {})
|
||||||
|
}
|
||||||
|
return this._variables
|
||||||
|
},
|
||||||
|
|
||||||
|
properties() {
|
||||||
|
if (!this._properties) {
|
||||||
|
this._properties = !this.rules
|
||||||
|
? {}
|
||||||
|
: this.rules.reduce(function (hash, r) {
|
||||||
|
if (r instanceof Declaration && r.variable !== true) {
|
||||||
|
const name =
|
||||||
|
r.name.length === 1 && r.name[0] instanceof Keyword
|
||||||
|
? r.name[0].value
|
||||||
|
: r.name
|
||||||
|
// Properties don't overwrite as they can merge
|
||||||
|
if (!hash[`$${name}`]) {
|
||||||
|
hash[`$${name}`] = [r]
|
||||||
|
} else {
|
||||||
|
hash[`$${name}`].push(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hash
|
||||||
|
}, {})
|
||||||
|
}
|
||||||
|
return this._properties
|
||||||
|
},
|
||||||
|
|
||||||
|
variable(name) {
|
||||||
|
const decl = this.variables()[name]
|
||||||
|
if (decl) {
|
||||||
|
return this.parseValue(decl)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
property(name) {
|
||||||
|
const decl = this.properties()[name]
|
||||||
|
if (decl) {
|
||||||
|
return this.parseValue(decl)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
lastDeclaration() {
|
||||||
|
for (let i = this.rules.length; i > 0; i--) {
|
||||||
|
const decl = this.rules[i - 1]
|
||||||
|
if (decl instanceof Declaration) {
|
||||||
|
return this.parseValue(decl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
parseValue(toParse) {
|
||||||
|
const self = this
|
||||||
|
function transformDeclaration(decl) {
|
||||||
|
if (decl.value instanceof Anonymous && !decl.parsed) {
|
||||||
|
if (typeof decl.value.value === 'string') {
|
||||||
|
new Parser(
|
||||||
|
this.parse.context,
|
||||||
|
this.parse.importManager,
|
||||||
|
decl.fileInfo(),
|
||||||
|
decl.value.getIndex()
|
||||||
|
).parseNode(
|
||||||
|
decl.value.value,
|
||||||
|
['value', 'important'],
|
||||||
|
function (err, result) {
|
||||||
|
if (err) {
|
||||||
|
decl.parsed = true
|
||||||
|
}
|
||||||
|
if (result) {
|
||||||
|
decl.value = result[0]
|
||||||
|
decl.important = result[1] || ''
|
||||||
|
decl.parsed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
decl.parsed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return decl
|
||||||
|
} else {
|
||||||
|
return decl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!Array.isArray(toParse)) {
|
||||||
|
return transformDeclaration.call(self, toParse)
|
||||||
|
} else {
|
||||||
|
const nodes = []
|
||||||
|
toParse.forEach(function (n) {
|
||||||
|
nodes.push(transformDeclaration.call(self, n))
|
||||||
|
})
|
||||||
|
return nodes
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
rulesets() {
|
||||||
|
if (!this.rules) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const filtRules = []
|
||||||
|
const rules = this.rules
|
||||||
|
let i
|
||||||
|
let rule
|
||||||
|
|
||||||
|
for (i = 0; (rule = rules[i]); i++) {
|
||||||
|
if (rule.isRuleset) {
|
||||||
|
filtRules.push(rule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtRules
|
||||||
|
},
|
||||||
|
|
||||||
|
prependRule(rule) {
|
||||||
|
const rules = this.rules
|
||||||
|
if (rules) {
|
||||||
|
rules.unshift(rule)
|
||||||
|
} else {
|
||||||
|
this.rules = [rule]
|
||||||
|
}
|
||||||
|
this.setParent(rule, this)
|
||||||
|
},
|
||||||
|
|
||||||
|
find(selector, self, filter) {
|
||||||
|
self = self || this
|
||||||
|
const rules = []
|
||||||
|
let match
|
||||||
|
let foundMixins
|
||||||
|
const key = selector.toCSS()
|
||||||
|
|
||||||
|
if (key in this._lookups) {
|
||||||
|
return this._lookups[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
this.rulesets().forEach(function (rule) {
|
||||||
|
if (rule !== self) {
|
||||||
|
for (let j = 0; j < rule.selectors.length; j++) {
|
||||||
|
match = selector.match(rule.selectors[j])
|
||||||
|
if (match) {
|
||||||
|
if (selector.elements.length > match) {
|
||||||
|
if (!filter || filter(rule)) {
|
||||||
|
foundMixins = rule.find(
|
||||||
|
new Selector(selector.elements.slice(match)),
|
||||||
|
self,
|
||||||
|
filter
|
||||||
|
)
|
||||||
|
for (let i = 0; i < foundMixins.length; ++i) {
|
||||||
|
foundMixins[i].path.push(rule)
|
||||||
|
}
|
||||||
|
Array.prototype.push.apply(rules, foundMixins)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rules.push({ rule, path: [] })
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this._lookups[key] = rules
|
||||||
|
return rules
|
||||||
|
},
|
||||||
|
|
||||||
|
genCSS(context, output) {
|
||||||
|
let i
|
||||||
|
let j
|
||||||
|
const charsetRuleNodes = []
|
||||||
|
let ruleNodes = []
|
||||||
|
|
||||||
|
let // Line number debugging
|
||||||
|
debugInfo
|
||||||
|
|
||||||
|
let rule
|
||||||
|
let path
|
||||||
|
|
||||||
|
context.tabLevel = context.tabLevel || 0
|
||||||
|
|
||||||
|
if (!this.root) {
|
||||||
|
context.tabLevel++
|
||||||
|
}
|
||||||
|
|
||||||
|
const tabRuleStr = context.compress
|
||||||
|
? ''
|
||||||
|
: Array(context.tabLevel + 1).join(' ')
|
||||||
|
const tabSetStr = context.compress ? '' : Array(context.tabLevel).join(' ')
|
||||||
|
let sep
|
||||||
|
|
||||||
|
let charsetNodeIndex = 0
|
||||||
|
let importNodeIndex = 0
|
||||||
|
for (i = 0; (rule = this.rules[i]); i++) {
|
||||||
|
if (rule instanceof Comment) {
|
||||||
|
if (importNodeIndex === i) {
|
||||||
|
importNodeIndex++
|
||||||
|
}
|
||||||
|
ruleNodes.push(rule)
|
||||||
|
} else if (rule.isCharset && rule.isCharset()) {
|
||||||
|
ruleNodes.splice(charsetNodeIndex, 0, rule)
|
||||||
|
charsetNodeIndex++
|
||||||
|
importNodeIndex++
|
||||||
|
} else if (rule.type === 'Import') {
|
||||||
|
ruleNodes.splice(importNodeIndex, 0, rule)
|
||||||
|
importNodeIndex++
|
||||||
|
} else {
|
||||||
|
ruleNodes.push(rule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ruleNodes = charsetRuleNodes.concat(ruleNodes)
|
||||||
|
|
||||||
|
// If this is the root node, we don't render
|
||||||
|
// a selector, or {}.
|
||||||
|
if (!this.root) {
|
||||||
|
debugInfo = getDebugInfo(context, this, tabSetStr)
|
||||||
|
|
||||||
|
if (debugInfo) {
|
||||||
|
output.add(debugInfo)
|
||||||
|
output.add(tabSetStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
const paths = this.paths
|
||||||
|
const pathCnt = paths.length
|
||||||
|
let pathSubCnt
|
||||||
|
|
||||||
|
sep = context.compress ? ',' : `,\n${tabSetStr}`
|
||||||
|
|
||||||
|
for (i = 0; i < pathCnt; i++) {
|
||||||
|
path = paths[i]
|
||||||
|
if (!(pathSubCnt = path.length)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (i > 0) {
|
||||||
|
output.add(sep)
|
||||||
|
}
|
||||||
|
|
||||||
|
context.firstSelector = true
|
||||||
|
path[0].genCSS(context, output)
|
||||||
|
|
||||||
|
context.firstSelector = false
|
||||||
|
for (j = 1; j < pathSubCnt; j++) {
|
||||||
|
path[j].genCSS(context, output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output.add((context.compress ? '{' : ' {\n') + tabRuleStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compile rules and rulesets
|
||||||
|
for (i = 0; (rule = ruleNodes[i]); i++) {
|
||||||
|
if (i + 1 === ruleNodes.length) {
|
||||||
|
context.lastRule = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentLastRule = context.lastRule
|
||||||
|
if (rule.isRulesetLike(rule)) {
|
||||||
|
context.lastRule = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rule.genCSS) {
|
||||||
|
rule.genCSS(context, output)
|
||||||
|
} else if (rule.value) {
|
||||||
|
output.add(rule.value.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
context.lastRule = currentLastRule
|
||||||
|
|
||||||
|
if (!context.lastRule && rule.isVisible()) {
|
||||||
|
output.add(context.compress ? '' : `\n${tabRuleStr}`)
|
||||||
|
} else {
|
||||||
|
context.lastRule = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.root) {
|
||||||
|
output.add(context.compress ? '}' : `\n${tabSetStr}}`)
|
||||||
|
context.tabLevel--
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!output.isEmpty() && !context.compress && this.firstRoot) {
|
||||||
|
output.add('\n')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
joinSelectors(paths, context, selectors) {
|
||||||
|
for (let s = 0; s < selectors.length; s++) {
|
||||||
|
this.joinSelector(paths, context, selectors[s])
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
joinSelector(paths, context, selector) {
|
||||||
|
function createParenthesis(elementsToPak, originalElement) {
|
||||||
|
let replacementParen, j
|
||||||
|
if (elementsToPak.length === 0) {
|
||||||
|
replacementParen = new Paren(elementsToPak[0])
|
||||||
|
} else {
|
||||||
|
const insideParent = new Array(elementsToPak.length)
|
||||||
|
for (j = 0; j < elementsToPak.length; j++) {
|
||||||
|
insideParent[j] = new Element(
|
||||||
|
null,
|
||||||
|
elementsToPak[j],
|
||||||
|
originalElement.isVariable,
|
||||||
|
originalElement._index,
|
||||||
|
originalElement._fileInfo
|
||||||
|
)
|
||||||
|
}
|
||||||
|
replacementParen = new Paren(new Selector(insideParent))
|
||||||
|
}
|
||||||
|
return replacementParen
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSelector(containedElement, originalElement) {
|
||||||
|
let element, selector
|
||||||
|
element = new Element(
|
||||||
|
null,
|
||||||
|
containedElement,
|
||||||
|
originalElement.isVariable,
|
||||||
|
originalElement._index,
|
||||||
|
originalElement._fileInfo
|
||||||
|
)
|
||||||
|
selector = new Selector([element])
|
||||||
|
return selector
|
||||||
|
}
|
||||||
|
|
||||||
|
// joins selector path from `beginningPath` with selector path in `addPath`
|
||||||
|
// `replacedElement` contains element that is being replaced by `addPath`
|
||||||
|
// returns concatenated path
|
||||||
|
function addReplacementIntoPath(
|
||||||
|
beginningPath,
|
||||||
|
addPath,
|
||||||
|
replacedElement,
|
||||||
|
originalSelector
|
||||||
|
) {
|
||||||
|
let newSelectorPath, lastSelector, newJoinedSelector
|
||||||
|
// our new selector path
|
||||||
|
newSelectorPath = []
|
||||||
|
|
||||||
|
// construct the joined selector - if & is the first thing this will be empty,
|
||||||
|
// if not newJoinedSelector will be the last set of elements in the selector
|
||||||
|
if (beginningPath.length > 0) {
|
||||||
|
newSelectorPath = utils.copyArray(beginningPath)
|
||||||
|
lastSelector = newSelectorPath.pop()
|
||||||
|
newJoinedSelector = originalSelector.createDerived(
|
||||||
|
utils.copyArray(lastSelector.elements)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
newJoinedSelector = originalSelector.createDerived([])
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addPath.length > 0) {
|
||||||
|
// /deep/ is a CSS4 selector - (removed, so should deprecate)
|
||||||
|
// that is valid without anything in front of it
|
||||||
|
// so if the & does not have a combinator that is "" or " " then
|
||||||
|
// and there is a combinator on the parent, then grab that.
|
||||||
|
// this also allows + a { & .b { .a & { ... though not sure why you would want to do that
|
||||||
|
let combinator = replacedElement.combinator
|
||||||
|
|
||||||
|
const parentEl = addPath[0].elements[0]
|
||||||
|
if (
|
||||||
|
combinator.emptyOrWhitespace &&
|
||||||
|
!parentEl.combinator.emptyOrWhitespace
|
||||||
|
) {
|
||||||
|
combinator = parentEl.combinator
|
||||||
|
}
|
||||||
|
// join the elements so far with the first part of the parent
|
||||||
|
newJoinedSelector.elements.push(
|
||||||
|
new Element(
|
||||||
|
combinator,
|
||||||
|
parentEl.value,
|
||||||
|
replacedElement.isVariable,
|
||||||
|
replacedElement._index,
|
||||||
|
replacedElement._fileInfo
|
||||||
|
)
|
||||||
|
)
|
||||||
|
newJoinedSelector.elements = newJoinedSelector.elements.concat(
|
||||||
|
addPath[0].elements.slice(1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// now add the joined selector - but only if it is not empty
|
||||||
|
if (newJoinedSelector.elements.length !== 0) {
|
||||||
|
newSelectorPath.push(newJoinedSelector)
|
||||||
|
}
|
||||||
|
|
||||||
|
// put together the parent selectors after the join (e.g. the rest of the parent)
|
||||||
|
if (addPath.length > 1) {
|
||||||
|
let restOfPath = addPath.slice(1)
|
||||||
|
restOfPath = restOfPath.map(function (selector) {
|
||||||
|
return selector.createDerived(selector.elements, [])
|
||||||
|
})
|
||||||
|
newSelectorPath = newSelectorPath.concat(restOfPath)
|
||||||
|
}
|
||||||
|
return newSelectorPath
|
||||||
|
}
|
||||||
|
|
||||||
|
// joins selector path from `beginningPath` with every selector path in `addPaths` array
|
||||||
|
// `replacedElement` contains element that is being replaced by `addPath`
|
||||||
|
// returns array with all concatenated paths
|
||||||
|
function addAllReplacementsIntoPath(
|
||||||
|
beginningPath,
|
||||||
|
addPaths,
|
||||||
|
replacedElement,
|
||||||
|
originalSelector,
|
||||||
|
result
|
||||||
|
) {
|
||||||
|
let j
|
||||||
|
for (j = 0; j < beginningPath.length; j++) {
|
||||||
|
const newSelectorPath = addReplacementIntoPath(
|
||||||
|
beginningPath[j],
|
||||||
|
addPaths,
|
||||||
|
replacedElement,
|
||||||
|
originalSelector
|
||||||
|
)
|
||||||
|
result.push(newSelectorPath)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeElementsOnToSelectors(elements, selectors) {
|
||||||
|
let i, sel
|
||||||
|
|
||||||
|
if (elements.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (selectors.length === 0) {
|
||||||
|
selectors.push([new Selector(elements)])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i = 0; (sel = selectors[i]); i++) {
|
||||||
|
// if the previous thing in sel is a parent this needs to join on to it
|
||||||
|
if (sel.length > 0) {
|
||||||
|
sel[sel.length - 1] = sel[sel.length - 1].createDerived(
|
||||||
|
sel[sel.length - 1].elements.concat(elements)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
sel.push(new Selector(elements))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// replace all parent selectors inside `inSelector` by content of `context` array
|
||||||
|
// resulting selectors are returned inside `paths` array
|
||||||
|
// returns true if `inSelector` contained at least one parent selector
|
||||||
|
function replaceParentSelector(paths, context, inSelector) {
|
||||||
|
// The paths are [[Selector]]
|
||||||
|
// The first list is a list of comma separated selectors
|
||||||
|
// The inner list is a list of inheritance separated selectors
|
||||||
|
// e.g.
|
||||||
|
// .a, .b {
|
||||||
|
// .c {
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// == [[.a] [.c]] [[.b] [.c]]
|
||||||
|
//
|
||||||
|
let i,
|
||||||
|
j,
|
||||||
|
k,
|
||||||
|
currentElements,
|
||||||
|
newSelectors,
|
||||||
|
selectorsMultiplied,
|
||||||
|
sel,
|
||||||
|
el,
|
||||||
|
hadParentSelector = false,
|
||||||
|
length,
|
||||||
|
lastSelector
|
||||||
|
function findNestedSelector(element) {
|
||||||
|
let maybeSelector
|
||||||
|
if (!(element.value instanceof Paren)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
maybeSelector = element.value.value
|
||||||
|
if (!(maybeSelector instanceof Selector)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return maybeSelector
|
||||||
|
}
|
||||||
|
|
||||||
|
// the elements from the current selector so far
|
||||||
|
currentElements = []
|
||||||
|
// the current list of new selectors to add to the path.
|
||||||
|
// We will build it up. We initiate it with one empty selector as we "multiply" the new selectors
|
||||||
|
// by the parents
|
||||||
|
newSelectors = [[]]
|
||||||
|
|
||||||
|
for (i = 0; (el = inSelector.elements[i]); i++) {
|
||||||
|
// non parent reference elements just get added
|
||||||
|
if (el.value !== '&') {
|
||||||
|
const nestedSelector = findNestedSelector(el)
|
||||||
|
if (nestedSelector !== null) {
|
||||||
|
// merge the current list of non parent selector elements
|
||||||
|
// on to the current list of selectors to add
|
||||||
|
mergeElementsOnToSelectors(currentElements, newSelectors)
|
||||||
|
|
||||||
|
const nestedPaths = []
|
||||||
|
let replaced
|
||||||
|
const replacedNewSelectors = []
|
||||||
|
replaced = replaceParentSelector(
|
||||||
|
nestedPaths,
|
||||||
|
context,
|
||||||
|
nestedSelector
|
||||||
|
)
|
||||||
|
hadParentSelector = hadParentSelector || replaced
|
||||||
|
// the nestedPaths array should have only one member - replaceParentSelector does not multiply selectors
|
||||||
|
for (k = 0; k < nestedPaths.length; k++) {
|
||||||
|
const replacementSelector = createSelector(
|
||||||
|
createParenthesis(nestedPaths[k], el),
|
||||||
|
el
|
||||||
|
)
|
||||||
|
addAllReplacementsIntoPath(
|
||||||
|
newSelectors,
|
||||||
|
[replacementSelector],
|
||||||
|
el,
|
||||||
|
inSelector,
|
||||||
|
replacedNewSelectors
|
||||||
|
)
|
||||||
|
}
|
||||||
|
newSelectors = replacedNewSelectors
|
||||||
|
currentElements = []
|
||||||
|
} else {
|
||||||
|
currentElements.push(el)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
hadParentSelector = true
|
||||||
|
// the new list of selectors to add
|
||||||
|
selectorsMultiplied = []
|
||||||
|
|
||||||
|
// merge the current list of non parent selector elements
|
||||||
|
// on to the current list of selectors to add
|
||||||
|
mergeElementsOnToSelectors(currentElements, newSelectors)
|
||||||
|
|
||||||
|
// loop through our current selectors
|
||||||
|
for (j = 0; j < newSelectors.length; j++) {
|
||||||
|
sel = newSelectors[j]
|
||||||
|
// if we don't have any parent paths, the & might be in a mixin so that it can be used
|
||||||
|
// whether there are parents or not
|
||||||
|
if (context.length === 0) {
|
||||||
|
// the combinator used on el should now be applied to the next element instead so that
|
||||||
|
// it is not lost
|
||||||
|
if (sel.length > 0) {
|
||||||
|
sel[0].elements.push(
|
||||||
|
new Element(
|
||||||
|
el.combinator,
|
||||||
|
'',
|
||||||
|
el.isVariable,
|
||||||
|
el._index,
|
||||||
|
el._fileInfo
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
selectorsMultiplied.push(sel)
|
||||||
|
} else {
|
||||||
|
// and the parent selectors
|
||||||
|
for (k = 0; k < context.length; k++) {
|
||||||
|
// We need to put the current selectors
|
||||||
|
// then join the last selector's elements on to the parents selectors
|
||||||
|
const newSelectorPath = addReplacementIntoPath(
|
||||||
|
sel,
|
||||||
|
context[k],
|
||||||
|
el,
|
||||||
|
inSelector
|
||||||
|
)
|
||||||
|
// add that to our new set of selectors
|
||||||
|
selectorsMultiplied.push(newSelectorPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// our new selectors has been multiplied, so reset the state
|
||||||
|
newSelectors = selectorsMultiplied
|
||||||
|
currentElements = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we have any elements left over (e.g. .a& .b == .b)
|
||||||
|
// add them on to all the current selectors
|
||||||
|
mergeElementsOnToSelectors(currentElements, newSelectors)
|
||||||
|
|
||||||
|
for (i = 0; i < newSelectors.length; i++) {
|
||||||
|
length = newSelectors[i].length
|
||||||
|
if (length > 0) {
|
||||||
|
paths.push(newSelectors[i])
|
||||||
|
lastSelector = newSelectors[i][length - 1]
|
||||||
|
newSelectors[i][length - 1] = lastSelector.createDerived(
|
||||||
|
lastSelector.elements,
|
||||||
|
inSelector.extendList
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hadParentSelector
|
||||||
|
}
|
||||||
|
|
||||||
|
function deriveSelector(visibilityInfo, deriveFrom) {
|
||||||
|
const newSelector = deriveFrom.createDerived(
|
||||||
|
deriveFrom.elements,
|
||||||
|
deriveFrom.extendList,
|
||||||
|
deriveFrom.evaldCondition
|
||||||
|
)
|
||||||
|
newSelector.copyVisibilityInfo(visibilityInfo)
|
||||||
|
return newSelector
|
||||||
|
}
|
||||||
|
|
||||||
|
// joinSelector code follows
|
||||||
|
let i, newPaths, hadParentSelector
|
||||||
|
|
||||||
|
newPaths = []
|
||||||
|
hadParentSelector = replaceParentSelector(newPaths, context, selector)
|
||||||
|
|
||||||
|
if (!hadParentSelector) {
|
||||||
|
if (context.length > 0) {
|
||||||
|
newPaths = []
|
||||||
|
for (i = 0; i < context.length; i++) {
|
||||||
|
const concatenated = context[i].map(
|
||||||
|
deriveSelector.bind(this, selector.visibilityInfo())
|
||||||
|
)
|
||||||
|
|
||||||
|
concatenated.push(selector)
|
||||||
|
newPaths.push(concatenated)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newPaths = [[selector]]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i = 0; i < newPaths.length; i++) {
|
||||||
|
paths.push(newPaths[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default Ruleset
|
|
@ -0,0 +1,184 @@
|
||||||
|
import Node from './node'
|
||||||
|
import Element from './element'
|
||||||
|
import LessError from '../less-error'
|
||||||
|
import * as utils from '../utils'
|
||||||
|
import Parser from '../parser/parser'
|
||||||
|
|
||||||
|
const Selector = function (
|
||||||
|
elements,
|
||||||
|
extendList,
|
||||||
|
condition,
|
||||||
|
index,
|
||||||
|
currentFileInfo,
|
||||||
|
visibilityInfo
|
||||||
|
) {
|
||||||
|
this.extendList = extendList
|
||||||
|
this.condition = condition
|
||||||
|
this.evaldCondition = !condition
|
||||||
|
this._index = index
|
||||||
|
this._fileInfo = currentFileInfo
|
||||||
|
this.elements = this.getElements(elements)
|
||||||
|
this.mixinElements_ = undefined
|
||||||
|
this.copyVisibilityInfo(visibilityInfo)
|
||||||
|
this.setParent(this.elements, this)
|
||||||
|
}
|
||||||
|
|
||||||
|
Selector.prototype = Object.assign(new Node(), {
|
||||||
|
type: 'Selector',
|
||||||
|
|
||||||
|
accept(visitor) {
|
||||||
|
if (this.elements) {
|
||||||
|
this.elements = visitor.visitArray(this.elements)
|
||||||
|
}
|
||||||
|
if (this.extendList) {
|
||||||
|
this.extendList = visitor.visitArray(this.extendList)
|
||||||
|
}
|
||||||
|
if (this.condition) {
|
||||||
|
this.condition = visitor.visit(this.condition)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
createDerived(elements, extendList, evaldCondition) {
|
||||||
|
elements = this.getElements(elements)
|
||||||
|
const newSelector = new Selector(
|
||||||
|
elements,
|
||||||
|
extendList || this.extendList,
|
||||||
|
null,
|
||||||
|
this.getIndex(),
|
||||||
|
this.fileInfo(),
|
||||||
|
this.visibilityInfo()
|
||||||
|
)
|
||||||
|
newSelector.evaldCondition = !utils.isNullOrUndefined(evaldCondition)
|
||||||
|
? evaldCondition
|
||||||
|
: this.evaldCondition
|
||||||
|
newSelector.mediaEmpty = this.mediaEmpty
|
||||||
|
return newSelector
|
||||||
|
},
|
||||||
|
|
||||||
|
getElements(els) {
|
||||||
|
if (!els) {
|
||||||
|
return [new Element('', '&', false, this._index, this._fileInfo)]
|
||||||
|
}
|
||||||
|
if (typeof els === 'string') {
|
||||||
|
new Parser(
|
||||||
|
this.parse.context,
|
||||||
|
this.parse.importManager,
|
||||||
|
this._fileInfo,
|
||||||
|
this._index
|
||||||
|
).parseNode(els, ['selector'], function (err, result) {
|
||||||
|
if (err) {
|
||||||
|
throw new LessError(
|
||||||
|
{
|
||||||
|
index: err.index,
|
||||||
|
message: err.message
|
||||||
|
},
|
||||||
|
this.parse.imports,
|
||||||
|
this._fileInfo.filename
|
||||||
|
)
|
||||||
|
}
|
||||||
|
els = result[0].elements
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return els
|
||||||
|
},
|
||||||
|
|
||||||
|
createEmptySelectors() {
|
||||||
|
const el = new Element('', '&', false, this._index, this._fileInfo),
|
||||||
|
sels = [new Selector([el], null, null, this._index, this._fileInfo)]
|
||||||
|
sels[0].mediaEmpty = true
|
||||||
|
return sels
|
||||||
|
},
|
||||||
|
|
||||||
|
match(other) {
|
||||||
|
const elements = this.elements
|
||||||
|
const len = elements.length
|
||||||
|
let olen
|
||||||
|
let i
|
||||||
|
|
||||||
|
other = other.mixinElements()
|
||||||
|
olen = other.length
|
||||||
|
if (olen === 0 || len < olen) {
|
||||||
|
return 0
|
||||||
|
} else {
|
||||||
|
for (i = 0; i < olen; i++) {
|
||||||
|
if (elements[i].value !== other[i]) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return olen // return number of matched elements
|
||||||
|
},
|
||||||
|
|
||||||
|
mixinElements() {
|
||||||
|
if (this.mixinElements_) {
|
||||||
|
return this.mixinElements_
|
||||||
|
}
|
||||||
|
|
||||||
|
let elements = this.elements
|
||||||
|
.map(function (v) {
|
||||||
|
return v.combinator.value + (v.value.value || v.value)
|
||||||
|
})
|
||||||
|
.join('')
|
||||||
|
.match(/[,&#*.\w-]([\w-]|(\\.))*/g)
|
||||||
|
|
||||||
|
if (elements) {
|
||||||
|
if (elements[0] === '&') {
|
||||||
|
elements.shift()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
elements = []
|
||||||
|
}
|
||||||
|
|
||||||
|
return (this.mixinElements_ = elements)
|
||||||
|
},
|
||||||
|
|
||||||
|
isJustParentSelector() {
|
||||||
|
return (
|
||||||
|
!this.mediaEmpty &&
|
||||||
|
this.elements.length === 1 &&
|
||||||
|
this.elements[0].value === '&' &&
|
||||||
|
(this.elements[0].combinator.value === ' ' ||
|
||||||
|
this.elements[0].combinator.value === '')
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
eval(context) {
|
||||||
|
const evaldCondition = this.condition && this.condition.eval(context)
|
||||||
|
let elements = this.elements
|
||||||
|
let extendList = this.extendList
|
||||||
|
|
||||||
|
elements =
|
||||||
|
elements &&
|
||||||
|
elements.map(function (e) {
|
||||||
|
return e.eval(context)
|
||||||
|
})
|
||||||
|
extendList =
|
||||||
|
extendList &&
|
||||||
|
extendList.map(function (extend) {
|
||||||
|
return extend.eval(context)
|
||||||
|
})
|
||||||
|
|
||||||
|
return this.createDerived(elements, extendList, evaldCondition)
|
||||||
|
},
|
||||||
|
|
||||||
|
genCSS(context, output) {
|
||||||
|
let i, element
|
||||||
|
if (
|
||||||
|
(!context || !context.firstSelector) &&
|
||||||
|
this.elements[0].combinator.value === ''
|
||||||
|
) {
|
||||||
|
output.add(' ', this.fileInfo(), this.getIndex())
|
||||||
|
}
|
||||||
|
for (i = 0; i < this.elements.length; i++) {
|
||||||
|
element = this.elements[i]
|
||||||
|
element.genCSS(context, output)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getIsOutput() {
|
||||||
|
return this.evaldCondition
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default Selector
|
|
@ -0,0 +1,11 @@
|
||||||
|
import Node from './node'
|
||||||
|
|
||||||
|
const UnicodeDescriptor = function (value) {
|
||||||
|
this.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
UnicodeDescriptor.prototype = Object.assign(new Node(), {
|
||||||
|
type: 'UnicodeDescriptor'
|
||||||
|
})
|
||||||
|
|
||||||
|
export default UnicodeDescriptor
|
|
@ -0,0 +1,149 @@
|
||||||
|
import Node from './node'
|
||||||
|
import unitConversions from '../data/unit-conversions'
|
||||||
|
import * as utils from '../utils'
|
||||||
|
|
||||||
|
const Unit = function (numerator, denominator, backupUnit) {
|
||||||
|
this.numerator = numerator ? utils.copyArray(numerator).sort() : []
|
||||||
|
this.denominator = denominator ? utils.copyArray(denominator).sort() : []
|
||||||
|
if (backupUnit) {
|
||||||
|
this.backupUnit = backupUnit
|
||||||
|
} else if (numerator && numerator.length) {
|
||||||
|
this.backupUnit = numerator[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Unit.prototype = Object.assign(new Node(), {
|
||||||
|
type: 'Unit',
|
||||||
|
|
||||||
|
clone() {
|
||||||
|
return new Unit(
|
||||||
|
utils.copyArray(this.numerator),
|
||||||
|
utils.copyArray(this.denominator),
|
||||||
|
this.backupUnit
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
genCSS(context, output) {
|
||||||
|
// Dimension checks the unit is singular and throws an error if in strict math mode.
|
||||||
|
const strictUnits = context && context.strictUnits
|
||||||
|
if (this.numerator.length === 1) {
|
||||||
|
output.add(this.numerator[0]) // the ideal situation
|
||||||
|
} else if (!strictUnits && this.backupUnit) {
|
||||||
|
output.add(this.backupUnit)
|
||||||
|
} else if (!strictUnits && this.denominator.length) {
|
||||||
|
output.add(this.denominator[0])
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
toString() {
|
||||||
|
let i,
|
||||||
|
returnStr = this.numerator.join('*')
|
||||||
|
for (i = 0; i < this.denominator.length; i++) {
|
||||||
|
returnStr += `/${this.denominator[i]}`
|
||||||
|
}
|
||||||
|
return returnStr
|
||||||
|
},
|
||||||
|
|
||||||
|
compare(other) {
|
||||||
|
return this.is(other.toString()) ? 0 : undefined
|
||||||
|
},
|
||||||
|
|
||||||
|
is(unitString) {
|
||||||
|
return this.toString().toUpperCase() === unitString.toUpperCase()
|
||||||
|
},
|
||||||
|
|
||||||
|
isLength() {
|
||||||
|
return RegExp(
|
||||||
|
'^(px|em|ex|ch|rem|in|cm|mm|pc|pt|ex|vw|vh|vmin|vmax)$',
|
||||||
|
'gi'
|
||||||
|
).test(this.toCSS())
|
||||||
|
},
|
||||||
|
|
||||||
|
isEmpty() {
|
||||||
|
return this.numerator.length === 0 && this.denominator.length === 0
|
||||||
|
},
|
||||||
|
|
||||||
|
isSingular() {
|
||||||
|
return this.numerator.length <= 1 && this.denominator.length === 0
|
||||||
|
},
|
||||||
|
|
||||||
|
map(callback) {
|
||||||
|
let i
|
||||||
|
|
||||||
|
for (i = 0; i < this.numerator.length; i++) {
|
||||||
|
this.numerator[i] = callback(this.numerator[i], false)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i = 0; i < this.denominator.length; i++) {
|
||||||
|
this.denominator[i] = callback(this.denominator[i], true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
usedUnits() {
|
||||||
|
let group
|
||||||
|
const result = {}
|
||||||
|
let mapUnit
|
||||||
|
let groupName
|
||||||
|
|
||||||
|
mapUnit = function (atomicUnit) {
|
||||||
|
// eslint-disable-next-line no-prototype-builtins
|
||||||
|
if (group.hasOwnProperty(atomicUnit) && !result[groupName]) {
|
||||||
|
result[groupName] = atomicUnit
|
||||||
|
}
|
||||||
|
|
||||||
|
return atomicUnit
|
||||||
|
}
|
||||||
|
|
||||||
|
for (groupName in unitConversions) {
|
||||||
|
// eslint-disable-next-line no-prototype-builtins
|
||||||
|
if (unitConversions.hasOwnProperty(groupName)) {
|
||||||
|
group = unitConversions[groupName]
|
||||||
|
|
||||||
|
this.map(mapUnit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
|
||||||
|
cancel() {
|
||||||
|
const counter = {}
|
||||||
|
let atomicUnit
|
||||||
|
let i
|
||||||
|
|
||||||
|
for (i = 0; i < this.numerator.length; i++) {
|
||||||
|
atomicUnit = this.numerator[i]
|
||||||
|
counter[atomicUnit] = (counter[atomicUnit] || 0) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i = 0; i < this.denominator.length; i++) {
|
||||||
|
atomicUnit = this.denominator[i]
|
||||||
|
counter[atomicUnit] = (counter[atomicUnit] || 0) - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
this.numerator = []
|
||||||
|
this.denominator = []
|
||||||
|
|
||||||
|
for (atomicUnit in counter) {
|
||||||
|
// eslint-disable-next-line no-prototype-builtins
|
||||||
|
if (counter.hasOwnProperty(atomicUnit)) {
|
||||||
|
const count = counter[atomicUnit]
|
||||||
|
|
||||||
|
if (count > 0) {
|
||||||
|
for (i = 0; i < count; i++) {
|
||||||
|
this.numerator.push(atomicUnit)
|
||||||
|
}
|
||||||
|
} else if (count < 0) {
|
||||||
|
for (i = 0; i < -count; i++) {
|
||||||
|
this.denominator.push(atomicUnit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.numerator.sort()
|
||||||
|
this.denominator.sort()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default Unit
|
|
@ -0,0 +1,67 @@
|
||||||
|
import Node from './node'
|
||||||
|
|
||||||
|
function escapePath(path) {
|
||||||
|
return path.replace(/[()'"\s]/g, function (match) {
|
||||||
|
return `\\${match}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const URL = function (val, index, currentFileInfo, isEvald) {
|
||||||
|
this.value = val
|
||||||
|
this._index = index
|
||||||
|
this._fileInfo = currentFileInfo
|
||||||
|
this.isEvald = isEvald
|
||||||
|
}
|
||||||
|
|
||||||
|
URL.prototype = Object.assign(new Node(), {
|
||||||
|
type: 'Url',
|
||||||
|
|
||||||
|
accept(visitor) {
|
||||||
|
this.value = visitor.visit(this.value)
|
||||||
|
},
|
||||||
|
|
||||||
|
genCSS(context, output) {
|
||||||
|
output.add('url(')
|
||||||
|
this.value.genCSS(context, output)
|
||||||
|
output.add(')')
|
||||||
|
},
|
||||||
|
|
||||||
|
eval(context) {
|
||||||
|
const val = this.value.eval(context)
|
||||||
|
let rootpath
|
||||||
|
|
||||||
|
if (!this.isEvald) {
|
||||||
|
// Add the rootpath if the URL requires a rewrite
|
||||||
|
rootpath = this.fileInfo() && this.fileInfo().rootpath
|
||||||
|
if (
|
||||||
|
typeof rootpath === 'string' &&
|
||||||
|
typeof val.value === 'string' &&
|
||||||
|
context.pathRequiresRewrite(val.value)
|
||||||
|
) {
|
||||||
|
if (!val.quote) {
|
||||||
|
rootpath = escapePath(rootpath)
|
||||||
|
}
|
||||||
|
val.value = context.rewritePath(val.value, rootpath)
|
||||||
|
} else {
|
||||||
|
val.value = context.normalizePath(val.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add url args if enabled
|
||||||
|
if (context.urlArgs) {
|
||||||
|
if (!val.value.match(/^\s*data:/)) {
|
||||||
|
const delimiter = val.value.indexOf('?') === -1 ? '?' : '&'
|
||||||
|
const urlArgs = delimiter + context.urlArgs
|
||||||
|
if (val.value.indexOf('#') !== -1) {
|
||||||
|
val.value = val.value.replace('#', `${urlArgs}#`)
|
||||||
|
} else {
|
||||||
|
val.value += urlArgs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new URL(val, this.getIndex(), this.fileInfo(), true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default URL
|
|
@ -0,0 +1,46 @@
|
||||||
|
import Node from './node'
|
||||||
|
|
||||||
|
const Value = function (value) {
|
||||||
|
if (!value) {
|
||||||
|
throw new Error('Value requires an array argument')
|
||||||
|
}
|
||||||
|
if (!Array.isArray(value)) {
|
||||||
|
this.value = [value]
|
||||||
|
} else {
|
||||||
|
this.value = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Value.prototype = Object.assign(new Node(), {
|
||||||
|
type: 'Value',
|
||||||
|
|
||||||
|
accept(visitor) {
|
||||||
|
if (this.value) {
|
||||||
|
this.value = visitor.visitArray(this.value)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
eval(context) {
|
||||||
|
if (this.value.length === 1) {
|
||||||
|
return this.value[0].eval(context)
|
||||||
|
} else {
|
||||||
|
return new Value(
|
||||||
|
this.value.map(function (v) {
|
||||||
|
return v.eval(context)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
genCSS(context, output) {
|
||||||
|
let i
|
||||||
|
for (i = 0; i < this.value.length; i++) {
|
||||||
|
this.value[i].genCSS(context, output)
|
||||||
|
if (i + 1 < this.value.length) {
|
||||||
|
output.add(context && context.compress ? ',' : ', ')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default Value
|
|
@ -0,0 +1,48 @@
|
||||||
|
import Node from './node'
|
||||||
|
import Variable from './variable'
|
||||||
|
import Ruleset from './ruleset'
|
||||||
|
import DetachedRuleset from './detached-ruleset'
|
||||||
|
import LessError from '../less-error'
|
||||||
|
|
||||||
|
const VariableCall = function (variable, index, currentFileInfo) {
|
||||||
|
this.variable = variable
|
||||||
|
this._index = index
|
||||||
|
this._fileInfo = currentFileInfo
|
||||||
|
this.allowRoot = true
|
||||||
|
}
|
||||||
|
|
||||||
|
VariableCall.prototype = Object.assign(new Node(), {
|
||||||
|
type: 'VariableCall',
|
||||||
|
|
||||||
|
eval(context) {
|
||||||
|
let rules
|
||||||
|
let detachedRuleset = new Variable(
|
||||||
|
this.variable,
|
||||||
|
this.getIndex(),
|
||||||
|
this.fileInfo()
|
||||||
|
).eval(context)
|
||||||
|
const error = new LessError({
|
||||||
|
message: `Could not evaluate variable call ${this.variable}`
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!detachedRuleset.ruleset) {
|
||||||
|
if (detachedRuleset.rules) {
|
||||||
|
rules = detachedRuleset
|
||||||
|
} else if (Array.isArray(detachedRuleset)) {
|
||||||
|
rules = new Ruleset('', detachedRuleset)
|
||||||
|
} else if (Array.isArray(detachedRuleset.value)) {
|
||||||
|
rules = new Ruleset('', detachedRuleset.value)
|
||||||
|
} else {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
detachedRuleset = new DetachedRuleset(rules)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (detachedRuleset.ruleset) {
|
||||||
|
return detachedRuleset.callEval(context)
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default VariableCall
|
|
@ -0,0 +1,76 @@
|
||||||
|
import Node from './node'
|
||||||
|
import Call from './call'
|
||||||
|
|
||||||
|
const Variable = function (name, index, currentFileInfo) {
|
||||||
|
this.name = name
|
||||||
|
this._index = index
|
||||||
|
this._fileInfo = currentFileInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
Variable.prototype = Object.assign(new Node(), {
|
||||||
|
type: 'Variable',
|
||||||
|
|
||||||
|
eval(context) {
|
||||||
|
let variable,
|
||||||
|
name = this.name
|
||||||
|
|
||||||
|
if (name.indexOf('@@') === 0) {
|
||||||
|
name = `@${
|
||||||
|
new Variable(name.slice(1), this.getIndex(), this.fileInfo()).eval(
|
||||||
|
context
|
||||||
|
).value
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.evaluating) {
|
||||||
|
throw {
|
||||||
|
type: 'Name',
|
||||||
|
message: `Recursive variable definition for ${name}`,
|
||||||
|
filename: this.fileInfo().filename,
|
||||||
|
index: this.getIndex()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.evaluating = true
|
||||||
|
|
||||||
|
variable = this.find(context.frames, function (frame) {
|
||||||
|
const v = frame.variable(name)
|
||||||
|
if (v) {
|
||||||
|
if (v.important) {
|
||||||
|
const importantScope =
|
||||||
|
context.importantScope[context.importantScope.length - 1]
|
||||||
|
importantScope.important = v.important
|
||||||
|
}
|
||||||
|
// If in calc, wrap vars in a function call to cascade evaluate args first
|
||||||
|
if (context.inCalc) {
|
||||||
|
return new Call('_SELF', [v.value]).eval(context)
|
||||||
|
} else {
|
||||||
|
return v.value.eval(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (variable) {
|
||||||
|
this.evaluating = false
|
||||||
|
return variable
|
||||||
|
} else {
|
||||||
|
throw {
|
||||||
|
type: 'Name',
|
||||||
|
message: `variable ${name} is undefined`,
|
||||||
|
filename: this.fileInfo().filename,
|
||||||
|
index: this.getIndex()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
find(obj, fun) {
|
||||||
|
for (let i = 0, r; i < obj.length; i++) {
|
||||||
|
r = fun.call(obj, obj[i])
|
||||||
|
if (r) {
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default Variable
|
|
@ -0,0 +1,126 @@
|
||||||
|
/* jshint proto: true */
|
||||||
|
import * as Constants from './constants'
|
||||||
|
import { copy } from 'copy-anything'
|
||||||
|
|
||||||
|
export function getLocation(index, inputStream) {
|
||||||
|
let n = index + 1
|
||||||
|
let line = null
|
||||||
|
let column = -1
|
||||||
|
|
||||||
|
while (--n >= 0 && inputStream.charAt(n) !== '\n') {
|
||||||
|
column++
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof index === 'number') {
|
||||||
|
line = (inputStream.slice(0, index).match(/\n/g) || '').length
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
line,
|
||||||
|
column
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function copyArray(arr) {
|
||||||
|
let i
|
||||||
|
const length = arr.length
|
||||||
|
const copy = new Array(length)
|
||||||
|
|
||||||
|
for (i = 0; i < length; i++) {
|
||||||
|
copy[i] = arr[i]
|
||||||
|
}
|
||||||
|
return copy
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clone(obj) {
|
||||||
|
const cloned = {}
|
||||||
|
for (const prop in obj) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(obj, prop)) {
|
||||||
|
cloned[prop] = obj[prop]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cloned
|
||||||
|
}
|
||||||
|
|
||||||
|
export function defaults(obj1, obj2) {
|
||||||
|
let newObj = obj2 || {}
|
||||||
|
if (!obj2._defaults) {
|
||||||
|
newObj = {}
|
||||||
|
const defaults = copy(obj1)
|
||||||
|
newObj._defaults = defaults
|
||||||
|
const cloned = obj2 ? copy(obj2) : {}
|
||||||
|
Object.assign(newObj, defaults, cloned)
|
||||||
|
}
|
||||||
|
return newObj
|
||||||
|
}
|
||||||
|
|
||||||
|
export function copyOptions(obj1, obj2) {
|
||||||
|
if (obj2 && obj2._defaults) {
|
||||||
|
return obj2
|
||||||
|
}
|
||||||
|
const opts = defaults(obj1, obj2)
|
||||||
|
if (opts.strictMath) {
|
||||||
|
opts.math = Constants.Math.PARENS
|
||||||
|
}
|
||||||
|
// Back compat with changed relativeUrls option
|
||||||
|
if (opts.relativeUrls) {
|
||||||
|
opts.rewriteUrls = Constants.RewriteUrls.ALL
|
||||||
|
}
|
||||||
|
if (typeof opts.math === 'string') {
|
||||||
|
switch (opts.math.toLowerCase()) {
|
||||||
|
case 'always':
|
||||||
|
opts.math = Constants.Math.ALWAYS
|
||||||
|
break
|
||||||
|
case 'parens-division':
|
||||||
|
opts.math = Constants.Math.PARENS_DIVISION
|
||||||
|
break
|
||||||
|
case 'strict':
|
||||||
|
case 'parens':
|
||||||
|
opts.math = Constants.Math.PARENS
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
opts.math = Constants.Math.PARENS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (typeof opts.rewriteUrls === 'string') {
|
||||||
|
switch (opts.rewriteUrls.toLowerCase()) {
|
||||||
|
case 'off':
|
||||||
|
opts.rewriteUrls = Constants.RewriteUrls.OFF
|
||||||
|
break
|
||||||
|
case 'local':
|
||||||
|
opts.rewriteUrls = Constants.RewriteUrls.LOCAL
|
||||||
|
break
|
||||||
|
case 'all':
|
||||||
|
opts.rewriteUrls = Constants.RewriteUrls.ALL
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return opts
|
||||||
|
}
|
||||||
|
|
||||||
|
export function merge(obj1, obj2) {
|
||||||
|
for (const prop in obj2) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(obj2, prop)) {
|
||||||
|
obj1[prop] = obj2[prop]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return obj1
|
||||||
|
}
|
||||||
|
|
||||||
|
export function flattenArray(arr, result = []) {
|
||||||
|
for (let i = 0, length = arr.length; i < length; i++) {
|
||||||
|
const value = arr[i]
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
flattenArray(value, result)
|
||||||
|
} else {
|
||||||
|
if (value !== undefined) {
|
||||||
|
result.push(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isNullOrUndefined(val) {
|
||||||
|
return val === null || val === undefined
|
||||||
|
}
|
|
@ -0,0 +1,627 @@
|
||||||
|
/* eslint-disable no-unused-vars */
|
||||||
|
/**
|
||||||
|
* @todo - Remove unused when JSDoc types are added for visitor methods
|
||||||
|
*/
|
||||||
|
import tree from '../tree'
|
||||||
|
import Visitor from './visitor'
|
||||||
|
import logger from '../logger'
|
||||||
|
import * as utils from '../utils'
|
||||||
|
|
||||||
|
/* jshint loopfunc:true */
|
||||||
|
|
||||||
|
class ExtendFinderVisitor {
|
||||||
|
constructor() {
|
||||||
|
this._visitor = new Visitor(this)
|
||||||
|
this.contexts = []
|
||||||
|
this.allExtendsStack = [[]]
|
||||||
|
}
|
||||||
|
|
||||||
|
run(root) {
|
||||||
|
root = this._visitor.visit(root)
|
||||||
|
root.allExtends = this.allExtendsStack[0]
|
||||||
|
return root
|
||||||
|
}
|
||||||
|
|
||||||
|
visitDeclaration(declNode, visitArgs) {
|
||||||
|
visitArgs.visitDeeper = false
|
||||||
|
}
|
||||||
|
|
||||||
|
visitMixinDefinition(mixinDefinitionNode, visitArgs) {
|
||||||
|
visitArgs.visitDeeper = false
|
||||||
|
}
|
||||||
|
|
||||||
|
visitRuleset(rulesetNode, visitArgs) {
|
||||||
|
if (rulesetNode.root) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let i
|
||||||
|
let j
|
||||||
|
let extend
|
||||||
|
const allSelectorsExtendList = []
|
||||||
|
let extendList
|
||||||
|
|
||||||
|
// get &:extend(.a); rules which apply to all selectors in this ruleset
|
||||||
|
const rules = rulesetNode.rules,
|
||||||
|
ruleCnt = rules ? rules.length : 0
|
||||||
|
for (i = 0; i < ruleCnt; i++) {
|
||||||
|
if (rulesetNode.rules[i] instanceof tree.Extend) {
|
||||||
|
allSelectorsExtendList.push(rules[i])
|
||||||
|
rulesetNode.extendOnEveryPath = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// now find every selector and apply the extends that apply to all extends
|
||||||
|
// and the ones which apply to an individual extend
|
||||||
|
const paths = rulesetNode.paths
|
||||||
|
for (i = 0; i < paths.length; i++) {
|
||||||
|
const selectorPath = paths[i],
|
||||||
|
selector = selectorPath[selectorPath.length - 1],
|
||||||
|
selExtendList = selector.extendList
|
||||||
|
|
||||||
|
extendList = selExtendList
|
||||||
|
? utils.copyArray(selExtendList).concat(allSelectorsExtendList)
|
||||||
|
: allSelectorsExtendList
|
||||||
|
|
||||||
|
if (extendList) {
|
||||||
|
extendList = extendList.map(function (allSelectorsExtend) {
|
||||||
|
return allSelectorsExtend.clone()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for (j = 0; j < extendList.length; j++) {
|
||||||
|
this.foundExtends = true
|
||||||
|
extend = extendList[j]
|
||||||
|
extend.findSelfSelectors(selectorPath)
|
||||||
|
extend.ruleset = rulesetNode
|
||||||
|
if (j === 0) {
|
||||||
|
extend.firstExtendOnThisSelectorPath = true
|
||||||
|
}
|
||||||
|
this.allExtendsStack[this.allExtendsStack.length - 1].push(extend)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.contexts.push(rulesetNode.selectors)
|
||||||
|
}
|
||||||
|
|
||||||
|
visitRulesetOut(rulesetNode) {
|
||||||
|
if (!rulesetNode.root) {
|
||||||
|
this.contexts.length = this.contexts.length - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
visitMedia(mediaNode, visitArgs) {
|
||||||
|
mediaNode.allExtends = []
|
||||||
|
this.allExtendsStack.push(mediaNode.allExtends)
|
||||||
|
}
|
||||||
|
|
||||||
|
visitMediaOut(mediaNode) {
|
||||||
|
this.allExtendsStack.length = this.allExtendsStack.length - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
visitAtRule(atRuleNode, visitArgs) {
|
||||||
|
atRuleNode.allExtends = []
|
||||||
|
this.allExtendsStack.push(atRuleNode.allExtends)
|
||||||
|
}
|
||||||
|
|
||||||
|
visitAtRuleOut(atRuleNode) {
|
||||||
|
this.allExtendsStack.length = this.allExtendsStack.length - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ProcessExtendsVisitor {
|
||||||
|
constructor() {
|
||||||
|
this._visitor = new Visitor(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
run(root) {
|
||||||
|
const extendFinder = new ExtendFinderVisitor()
|
||||||
|
this.extendIndices = {}
|
||||||
|
extendFinder.run(root)
|
||||||
|
if (!extendFinder.foundExtends) {
|
||||||
|
return root
|
||||||
|
}
|
||||||
|
root.allExtends = root.allExtends.concat(
|
||||||
|
this.doExtendChaining(root.allExtends, root.allExtends)
|
||||||
|
)
|
||||||
|
this.allExtendsStack = [root.allExtends]
|
||||||
|
const newRoot = this._visitor.visit(root)
|
||||||
|
this.checkExtendsForNonMatched(root.allExtends)
|
||||||
|
return newRoot
|
||||||
|
}
|
||||||
|
|
||||||
|
checkExtendsForNonMatched(extendList) {
|
||||||
|
const indices = this.extendIndices
|
||||||
|
extendList
|
||||||
|
.filter(function (extend) {
|
||||||
|
return !extend.hasFoundMatches && extend.parent_ids.length == 1
|
||||||
|
})
|
||||||
|
.forEach(function (extend) {
|
||||||
|
let selector = '_unknown_'
|
||||||
|
try {
|
||||||
|
selector = extend.selector.toCSS({})
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
if (!indices[`${extend.index} ${selector}`]) {
|
||||||
|
indices[`${extend.index} ${selector}`] = true
|
||||||
|
logger.warn(`extend '${selector}' has no matches`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
doExtendChaining(extendsList, extendsListTarget, iterationCount) {
|
||||||
|
//
|
||||||
|
// chaining is different from normal extension.. if we extend an extend then we are not just copying, altering
|
||||||
|
// and pasting the selector we would do normally, but we are also adding an extend with the same target selector
|
||||||
|
// this means this new extend can then go and alter other extends
|
||||||
|
//
|
||||||
|
// this method deals with all the chaining work - without it, extend is flat and doesn't work on other extend selectors
|
||||||
|
// this is also the most expensive.. and a match on one selector can cause an extension of a selector we had already
|
||||||
|
// processed if we look at each selector at a time, as is done in visitRuleset
|
||||||
|
|
||||||
|
let extendIndex
|
||||||
|
|
||||||
|
let targetExtendIndex
|
||||||
|
let matches
|
||||||
|
const extendsToAdd = []
|
||||||
|
let newSelector
|
||||||
|
const extendVisitor = this
|
||||||
|
let selectorPath
|
||||||
|
let extend
|
||||||
|
let targetExtend
|
||||||
|
let newExtend
|
||||||
|
|
||||||
|
iterationCount = iterationCount || 0
|
||||||
|
|
||||||
|
// loop through comparing every extend with every target extend.
|
||||||
|
// a target extend is the one on the ruleset we are looking at copy/edit/pasting in place
|
||||||
|
// e.g. .a:extend(.b) {} and .b:extend(.c) {} then the first extend extends the second one
|
||||||
|
// and the second is the target.
|
||||||
|
// the separation into two lists allows us to process a subset of chains with a bigger set, as is the
|
||||||
|
// case when processing media queries
|
||||||
|
for (extendIndex = 0; extendIndex < extendsList.length; extendIndex++) {
|
||||||
|
for (
|
||||||
|
targetExtendIndex = 0;
|
||||||
|
targetExtendIndex < extendsListTarget.length;
|
||||||
|
targetExtendIndex++
|
||||||
|
) {
|
||||||
|
extend = extendsList[extendIndex]
|
||||||
|
targetExtend = extendsListTarget[targetExtendIndex]
|
||||||
|
|
||||||
|
// look for circular references
|
||||||
|
if (extend.parent_ids.indexOf(targetExtend.object_id) >= 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// find a match in the target extends self selector (the bit before :extend)
|
||||||
|
selectorPath = [targetExtend.selfSelectors[0]]
|
||||||
|
matches = extendVisitor.findMatch(extend, selectorPath)
|
||||||
|
|
||||||
|
if (matches.length) {
|
||||||
|
extend.hasFoundMatches = true
|
||||||
|
|
||||||
|
// we found a match, so for each self selector..
|
||||||
|
extend.selfSelectors.forEach(function (selfSelector) {
|
||||||
|
const info = targetExtend.visibilityInfo()
|
||||||
|
|
||||||
|
// process the extend as usual
|
||||||
|
newSelector = extendVisitor.extendSelector(
|
||||||
|
matches,
|
||||||
|
selectorPath,
|
||||||
|
selfSelector,
|
||||||
|
extend.isVisible()
|
||||||
|
)
|
||||||
|
|
||||||
|
// but now we create a new extend from it
|
||||||
|
newExtend = new tree.Extend(
|
||||||
|
targetExtend.selector,
|
||||||
|
targetExtend.option,
|
||||||
|
0,
|
||||||
|
targetExtend.fileInfo(),
|
||||||
|
info
|
||||||
|
)
|
||||||
|
newExtend.selfSelectors = newSelector
|
||||||
|
|
||||||
|
// add the extend onto the list of extends for that selector
|
||||||
|
newSelector[newSelector.length - 1].extendList = [newExtend]
|
||||||
|
|
||||||
|
// record that we need to add it.
|
||||||
|
extendsToAdd.push(newExtend)
|
||||||
|
newExtend.ruleset = targetExtend.ruleset
|
||||||
|
|
||||||
|
// remember its parents for circular references
|
||||||
|
newExtend.parent_ids = newExtend.parent_ids.concat(
|
||||||
|
targetExtend.parent_ids,
|
||||||
|
extend.parent_ids
|
||||||
|
)
|
||||||
|
|
||||||
|
// only process the selector once.. if we have :extend(.a,.b) then multiple
|
||||||
|
// extends will look at the same selector path, so when extending
|
||||||
|
// we know that any others will be duplicates in terms of what is added to the css
|
||||||
|
if (targetExtend.firstExtendOnThisSelectorPath) {
|
||||||
|
newExtend.firstExtendOnThisSelectorPath = true
|
||||||
|
targetExtend.ruleset.paths.push(newSelector)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extendsToAdd.length) {
|
||||||
|
// try to detect circular references to stop a stack overflow.
|
||||||
|
// may no longer be needed.
|
||||||
|
this.extendChainCount++
|
||||||
|
if (iterationCount > 100) {
|
||||||
|
let selectorOne = '{unable to calculate}'
|
||||||
|
let selectorTwo = '{unable to calculate}'
|
||||||
|
try {
|
||||||
|
selectorOne = extendsToAdd[0].selfSelectors[0].toCSS()
|
||||||
|
selectorTwo = extendsToAdd[0].selector.toCSS()
|
||||||
|
} catch (e) {}
|
||||||
|
throw {
|
||||||
|
message: `extend circular reference detected. One of the circular extends is currently:${selectorOne}:extend(${selectorTwo})`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// now process the new extends on the existing rules so that we can handle a extending b extending c extending
|
||||||
|
// d extending e...
|
||||||
|
return extendsToAdd.concat(
|
||||||
|
extendVisitor.doExtendChaining(
|
||||||
|
extendsToAdd,
|
||||||
|
extendsListTarget,
|
||||||
|
iterationCount + 1
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return extendsToAdd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
visitDeclaration(ruleNode, visitArgs) {
|
||||||
|
visitArgs.visitDeeper = false
|
||||||
|
}
|
||||||
|
|
||||||
|
visitMixinDefinition(mixinDefinitionNode, visitArgs) {
|
||||||
|
visitArgs.visitDeeper = false
|
||||||
|
}
|
||||||
|
|
||||||
|
visitSelector(selectorNode, visitArgs) {
|
||||||
|
visitArgs.visitDeeper = false
|
||||||
|
}
|
||||||
|
|
||||||
|
visitRuleset(rulesetNode, visitArgs) {
|
||||||
|
if (rulesetNode.root) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let matches
|
||||||
|
let pathIndex
|
||||||
|
let extendIndex
|
||||||
|
const allExtends = this.allExtendsStack[this.allExtendsStack.length - 1]
|
||||||
|
const selectorsToAdd = []
|
||||||
|
const extendVisitor = this
|
||||||
|
let selectorPath
|
||||||
|
|
||||||
|
// look at each selector path in the ruleset, find any extend matches and then copy, find and replace
|
||||||
|
|
||||||
|
for (extendIndex = 0; extendIndex < allExtends.length; extendIndex++) {
|
||||||
|
for (pathIndex = 0; pathIndex < rulesetNode.paths.length; pathIndex++) {
|
||||||
|
selectorPath = rulesetNode.paths[pathIndex]
|
||||||
|
|
||||||
|
// extending extends happens initially, before the main pass
|
||||||
|
if (rulesetNode.extendOnEveryPath) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const extendList = selectorPath[selectorPath.length - 1].extendList
|
||||||
|
if (extendList && extendList.length) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
matches = this.findMatch(allExtends[extendIndex], selectorPath)
|
||||||
|
|
||||||
|
if (matches.length) {
|
||||||
|
allExtends[extendIndex].hasFoundMatches = true
|
||||||
|
|
||||||
|
allExtends[extendIndex].selfSelectors.forEach(function (
|
||||||
|
selfSelector
|
||||||
|
) {
|
||||||
|
let extendedSelectors
|
||||||
|
extendedSelectors = extendVisitor.extendSelector(
|
||||||
|
matches,
|
||||||
|
selectorPath,
|
||||||
|
selfSelector,
|
||||||
|
allExtends[extendIndex].isVisible()
|
||||||
|
)
|
||||||
|
selectorsToAdd.push(extendedSelectors)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rulesetNode.paths = rulesetNode.paths.concat(selectorsToAdd)
|
||||||
|
}
|
||||||
|
|
||||||
|
findMatch(extend, haystackSelectorPath) {
|
||||||
|
//
|
||||||
|
// look through the haystack selector path to try and find the needle - extend.selector
|
||||||
|
// returns an array of selector matches that can then be replaced
|
||||||
|
//
|
||||||
|
let haystackSelectorIndex
|
||||||
|
|
||||||
|
let hackstackSelector
|
||||||
|
let hackstackElementIndex
|
||||||
|
let haystackElement
|
||||||
|
let targetCombinator
|
||||||
|
let i
|
||||||
|
const extendVisitor = this
|
||||||
|
const needleElements = extend.selector.elements
|
||||||
|
const potentialMatches = []
|
||||||
|
let potentialMatch
|
||||||
|
const matches = []
|
||||||
|
|
||||||
|
// loop through the haystack elements
|
||||||
|
for (
|
||||||
|
haystackSelectorIndex = 0;
|
||||||
|
haystackSelectorIndex < haystackSelectorPath.length;
|
||||||
|
haystackSelectorIndex++
|
||||||
|
) {
|
||||||
|
hackstackSelector = haystackSelectorPath[haystackSelectorIndex]
|
||||||
|
|
||||||
|
for (
|
||||||
|
hackstackElementIndex = 0;
|
||||||
|
hackstackElementIndex < hackstackSelector.elements.length;
|
||||||
|
hackstackElementIndex++
|
||||||
|
) {
|
||||||
|
haystackElement = hackstackSelector.elements[hackstackElementIndex]
|
||||||
|
|
||||||
|
// if we allow elements before our match we can add a potential match every time. otherwise only at the first element.
|
||||||
|
if (
|
||||||
|
extend.allowBefore ||
|
||||||
|
(haystackSelectorIndex === 0 && hackstackElementIndex === 0)
|
||||||
|
) {
|
||||||
|
potentialMatches.push({
|
||||||
|
pathIndex: haystackSelectorIndex,
|
||||||
|
index: hackstackElementIndex,
|
||||||
|
matched: 0,
|
||||||
|
initialCombinator: haystackElement.combinator
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i = 0; i < potentialMatches.length; i++) {
|
||||||
|
potentialMatch = potentialMatches[i]
|
||||||
|
|
||||||
|
// selectors add " " onto the first element. When we use & it joins the selectors together, but if we don't
|
||||||
|
// then each selector in haystackSelectorPath has a space before it added in the toCSS phase. so we need to
|
||||||
|
// work out what the resulting combinator will be
|
||||||
|
targetCombinator = haystackElement.combinator.value
|
||||||
|
if (targetCombinator === '' && hackstackElementIndex === 0) {
|
||||||
|
targetCombinator = ' '
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we don't match, null our match to indicate failure
|
||||||
|
if (
|
||||||
|
!extendVisitor.isElementValuesEqual(
|
||||||
|
needleElements[potentialMatch.matched].value,
|
||||||
|
haystackElement.value
|
||||||
|
) ||
|
||||||
|
(potentialMatch.matched > 0 &&
|
||||||
|
needleElements[potentialMatch.matched].combinator.value !==
|
||||||
|
targetCombinator)
|
||||||
|
) {
|
||||||
|
potentialMatch = null
|
||||||
|
} else {
|
||||||
|
potentialMatch.matched++
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we are still valid and have finished, test whether we have elements after and whether these are allowed
|
||||||
|
if (potentialMatch) {
|
||||||
|
potentialMatch.finished =
|
||||||
|
potentialMatch.matched === needleElements.length
|
||||||
|
if (
|
||||||
|
potentialMatch.finished &&
|
||||||
|
!extend.allowAfter &&
|
||||||
|
(hackstackElementIndex + 1 < hackstackSelector.elements.length ||
|
||||||
|
haystackSelectorIndex + 1 < haystackSelectorPath.length)
|
||||||
|
) {
|
||||||
|
potentialMatch = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// if null we remove, if not, we are still valid, so either push as a valid match or continue
|
||||||
|
if (potentialMatch) {
|
||||||
|
if (potentialMatch.finished) {
|
||||||
|
potentialMatch.length = needleElements.length
|
||||||
|
potentialMatch.endPathIndex = haystackSelectorIndex
|
||||||
|
potentialMatch.endPathElementIndex = hackstackElementIndex + 1 // index after end of match
|
||||||
|
potentialMatches.length = 0 // we don't allow matches to overlap, so start matching again
|
||||||
|
matches.push(potentialMatch)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
potentialMatches.splice(i, 1)
|
||||||
|
i--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return matches
|
||||||
|
}
|
||||||
|
|
||||||
|
isElementValuesEqual(elementValue1, elementValue2) {
|
||||||
|
if (
|
||||||
|
typeof elementValue1 === 'string' ||
|
||||||
|
typeof elementValue2 === 'string'
|
||||||
|
) {
|
||||||
|
return elementValue1 === elementValue2
|
||||||
|
}
|
||||||
|
if (elementValue1 instanceof tree.Attribute) {
|
||||||
|
if (
|
||||||
|
elementValue1.op !== elementValue2.op ||
|
||||||
|
elementValue1.key !== elementValue2.key
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (!elementValue1.value || !elementValue2.value) {
|
||||||
|
if (elementValue1.value || elementValue2.value) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
elementValue1 = elementValue1.value.value || elementValue1.value
|
||||||
|
elementValue2 = elementValue2.value.value || elementValue2.value
|
||||||
|
return elementValue1 === elementValue2
|
||||||
|
}
|
||||||
|
elementValue1 = elementValue1.value
|
||||||
|
elementValue2 = elementValue2.value
|
||||||
|
if (elementValue1 instanceof tree.Selector) {
|
||||||
|
if (
|
||||||
|
!(elementValue2 instanceof tree.Selector) ||
|
||||||
|
elementValue1.elements.length !== elementValue2.elements.length
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for (let i = 0; i < elementValue1.elements.length; i++) {
|
||||||
|
if (
|
||||||
|
elementValue1.elements[i].combinator.value !==
|
||||||
|
elementValue2.elements[i].combinator.value
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
i !== 0 ||
|
||||||
|
(elementValue1.elements[i].combinator.value || ' ') !==
|
||||||
|
(elementValue2.elements[i].combinator.value || ' ')
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!this.isElementValuesEqual(
|
||||||
|
elementValue1.elements[i].value,
|
||||||
|
elementValue2.elements[i].value
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
extendSelector(matches, selectorPath, replacementSelector, isVisible) {
|
||||||
|
// for a set of matches, replace each match with the replacement selector
|
||||||
|
|
||||||
|
let currentSelectorPathIndex = 0,
|
||||||
|
currentSelectorPathElementIndex = 0,
|
||||||
|
path = [],
|
||||||
|
matchIndex,
|
||||||
|
selector,
|
||||||
|
firstElement,
|
||||||
|
match,
|
||||||
|
newElements
|
||||||
|
|
||||||
|
for (matchIndex = 0; matchIndex < matches.length; matchIndex++) {
|
||||||
|
match = matches[matchIndex]
|
||||||
|
selector = selectorPath[match.pathIndex]
|
||||||
|
firstElement = new tree.Element(
|
||||||
|
match.initialCombinator,
|
||||||
|
replacementSelector.elements[0].value,
|
||||||
|
replacementSelector.elements[0].isVariable,
|
||||||
|
replacementSelector.elements[0].getIndex(),
|
||||||
|
replacementSelector.elements[0].fileInfo()
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
match.pathIndex > currentSelectorPathIndex &&
|
||||||
|
currentSelectorPathElementIndex > 0
|
||||||
|
) {
|
||||||
|
path[path.length - 1].elements = path[path.length - 1].elements.concat(
|
||||||
|
selectorPath[currentSelectorPathIndex].elements.slice(
|
||||||
|
currentSelectorPathElementIndex
|
||||||
|
)
|
||||||
|
)
|
||||||
|
currentSelectorPathElementIndex = 0
|
||||||
|
currentSelectorPathIndex++
|
||||||
|
}
|
||||||
|
|
||||||
|
newElements = selector.elements
|
||||||
|
.slice(currentSelectorPathElementIndex, match.index)
|
||||||
|
.concat([firstElement])
|
||||||
|
.concat(replacementSelector.elements.slice(1))
|
||||||
|
|
||||||
|
if (currentSelectorPathIndex === match.pathIndex && matchIndex > 0) {
|
||||||
|
path[path.length - 1].elements =
|
||||||
|
path[path.length - 1].elements.concat(newElements)
|
||||||
|
} else {
|
||||||
|
path = path.concat(
|
||||||
|
selectorPath.slice(currentSelectorPathIndex, match.pathIndex)
|
||||||
|
)
|
||||||
|
|
||||||
|
path.push(new tree.Selector(newElements))
|
||||||
|
}
|
||||||
|
currentSelectorPathIndex = match.endPathIndex
|
||||||
|
currentSelectorPathElementIndex = match.endPathElementIndex
|
||||||
|
if (
|
||||||
|
currentSelectorPathElementIndex >=
|
||||||
|
selectorPath[currentSelectorPathIndex].elements.length
|
||||||
|
) {
|
||||||
|
currentSelectorPathElementIndex = 0
|
||||||
|
currentSelectorPathIndex++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
currentSelectorPathIndex < selectorPath.length &&
|
||||||
|
currentSelectorPathElementIndex > 0
|
||||||
|
) {
|
||||||
|
path[path.length - 1].elements = path[path.length - 1].elements.concat(
|
||||||
|
selectorPath[currentSelectorPathIndex].elements.slice(
|
||||||
|
currentSelectorPathElementIndex
|
||||||
|
)
|
||||||
|
)
|
||||||
|
currentSelectorPathIndex++
|
||||||
|
}
|
||||||
|
|
||||||
|
path = path.concat(
|
||||||
|
selectorPath.slice(currentSelectorPathIndex, selectorPath.length)
|
||||||
|
)
|
||||||
|
path = path.map(function (currentValue) {
|
||||||
|
// we can re-use elements here, because the visibility property matters only for selectors
|
||||||
|
const derived = currentValue.createDerived(currentValue.elements)
|
||||||
|
if (isVisible) {
|
||||||
|
derived.ensureVisibility()
|
||||||
|
} else {
|
||||||
|
derived.ensureInvisibility()
|
||||||
|
}
|
||||||
|
return derived
|
||||||
|
})
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
visitMedia(mediaNode, visitArgs) {
|
||||||
|
let newAllExtends = mediaNode.allExtends.concat(
|
||||||
|
this.allExtendsStack[this.allExtendsStack.length - 1]
|
||||||
|
)
|
||||||
|
newAllExtends = newAllExtends.concat(
|
||||||
|
this.doExtendChaining(newAllExtends, mediaNode.allExtends)
|
||||||
|
)
|
||||||
|
this.allExtendsStack.push(newAllExtends)
|
||||||
|
}
|
||||||
|
|
||||||
|
visitMediaOut(mediaNode) {
|
||||||
|
const lastIndex = this.allExtendsStack.length - 1
|
||||||
|
this.allExtendsStack.length = lastIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
visitAtRule(atRuleNode, visitArgs) {
|
||||||
|
let newAllExtends = atRuleNode.allExtends.concat(
|
||||||
|
this.allExtendsStack[this.allExtendsStack.length - 1]
|
||||||
|
)
|
||||||
|
newAllExtends = newAllExtends.concat(
|
||||||
|
this.doExtendChaining(newAllExtends, atRuleNode.allExtends)
|
||||||
|
)
|
||||||
|
this.allExtendsStack.push(newAllExtends)
|
||||||
|
}
|
||||||
|
|
||||||
|
visitAtRuleOut(atRuleNode) {
|
||||||
|
const lastIndex = this.allExtendsStack.length - 1
|
||||||
|
this.allExtendsStack.length = lastIndex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProcessExtendsVisitor
|
|
@ -0,0 +1,56 @@
|
||||||
|
class ImportSequencer {
|
||||||
|
constructor(onSequencerEmpty) {
|
||||||
|
this.imports = []
|
||||||
|
this.variableImports = []
|
||||||
|
this._onSequencerEmpty = onSequencerEmpty
|
||||||
|
this._currentDepth = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
addImport(callback) {
|
||||||
|
const importSequencer = this,
|
||||||
|
importItem = {
|
||||||
|
callback,
|
||||||
|
args: null,
|
||||||
|
isReady: false
|
||||||
|
}
|
||||||
|
this.imports.push(importItem)
|
||||||
|
return function () {
|
||||||
|
importItem.args = Array.prototype.slice.call(arguments, 0)
|
||||||
|
importItem.isReady = true
|
||||||
|
importSequencer.tryRun()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addVariableImport(callback) {
|
||||||
|
this.variableImports.push(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
tryRun() {
|
||||||
|
this._currentDepth++
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
while (this.imports.length > 0) {
|
||||||
|
const importItem = this.imports[0]
|
||||||
|
if (!importItem.isReady) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.imports = this.imports.slice(1)
|
||||||
|
importItem.callback.apply(null, importItem.args)
|
||||||
|
}
|
||||||
|
if (this.variableImports.length === 0) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
const variableImport = this.variableImports[0]
|
||||||
|
this.variableImports = this.variableImports.slice(1)
|
||||||
|
variableImport()
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this._currentDepth--
|
||||||
|
}
|
||||||
|
if (this._currentDepth === 0 && this._onSequencerEmpty) {
|
||||||
|
this._onSequencerEmpty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ImportSequencer
|
|
@ -0,0 +1,216 @@
|
||||||
|
/* eslint-disable no-unused-vars */
|
||||||
|
/**
|
||||||
|
* @todo - Remove unused when JSDoc types are added for visitor methods
|
||||||
|
*/
|
||||||
|
import contexts from '../contexts'
|
||||||
|
import Visitor from './visitor'
|
||||||
|
import ImportSequencer from './import-sequencer'
|
||||||
|
import * as utils from '../utils'
|
||||||
|
|
||||||
|
const ImportVisitor = function (importer, finish) {
|
||||||
|
this._visitor = new Visitor(this)
|
||||||
|
this._importer = importer
|
||||||
|
this._finish = finish
|
||||||
|
this.context = new contexts.Eval()
|
||||||
|
this.importCount = 0
|
||||||
|
this.onceFileDetectionMap = {}
|
||||||
|
this.recursionDetector = {}
|
||||||
|
this._sequencer = new ImportSequencer(this._onSequencerEmpty.bind(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
ImportVisitor.prototype = {
|
||||||
|
isReplacing: false,
|
||||||
|
run: function (root) {
|
||||||
|
try {
|
||||||
|
// process the contents
|
||||||
|
this._visitor.visit(root)
|
||||||
|
} catch (e) {
|
||||||
|
this.error = e
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isFinished = true
|
||||||
|
this._sequencer.tryRun()
|
||||||
|
},
|
||||||
|
_onSequencerEmpty: function () {
|
||||||
|
if (!this.isFinished) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this._finish(this.error)
|
||||||
|
},
|
||||||
|
visitImport: function (importNode, visitArgs) {
|
||||||
|
const inlineCSS = importNode.options.inline
|
||||||
|
|
||||||
|
if (!importNode.css || inlineCSS) {
|
||||||
|
const context = new contexts.Eval(
|
||||||
|
this.context,
|
||||||
|
utils.copyArray(this.context.frames)
|
||||||
|
)
|
||||||
|
const importParent = context.frames[0]
|
||||||
|
|
||||||
|
this.importCount++
|
||||||
|
if (importNode.isVariableImport()) {
|
||||||
|
this._sequencer.addVariableImport(
|
||||||
|
this.processImportNode.bind(this, importNode, context, importParent)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
this.processImportNode(importNode, context, importParent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
visitArgs.visitDeeper = false
|
||||||
|
},
|
||||||
|
processImportNode: function (importNode, context, importParent) {
|
||||||
|
let evaldImportNode
|
||||||
|
const inlineCSS = importNode.options.inline
|
||||||
|
|
||||||
|
try {
|
||||||
|
evaldImportNode = importNode.evalForImport(context)
|
||||||
|
} catch (e) {
|
||||||
|
if (!e.filename) {
|
||||||
|
e.index = importNode.getIndex()
|
||||||
|
e.filename = importNode.fileInfo().filename
|
||||||
|
}
|
||||||
|
// attempt to eval properly and treat as css
|
||||||
|
importNode.css = true
|
||||||
|
// if that fails, this error will be thrown
|
||||||
|
importNode.error = e
|
||||||
|
}
|
||||||
|
|
||||||
|
if (evaldImportNode && (!evaldImportNode.css || inlineCSS)) {
|
||||||
|
if (evaldImportNode.options.multiple) {
|
||||||
|
context.importMultiple = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// try appending if we haven't determined if it is css or not
|
||||||
|
const tryAppendLessExtension = evaldImportNode.css === undefined
|
||||||
|
|
||||||
|
for (let i = 0; i < importParent.rules.length; i++) {
|
||||||
|
if (importParent.rules[i] === importNode) {
|
||||||
|
importParent.rules[i] = evaldImportNode
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onImported = this.onImported.bind(this, evaldImportNode, context),
|
||||||
|
sequencedOnImported = this._sequencer.addImport(onImported)
|
||||||
|
|
||||||
|
this._importer.push(
|
||||||
|
evaldImportNode.getPath(),
|
||||||
|
tryAppendLessExtension,
|
||||||
|
evaldImportNode.fileInfo(),
|
||||||
|
evaldImportNode.options,
|
||||||
|
sequencedOnImported
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
this.importCount--
|
||||||
|
if (this.isFinished) {
|
||||||
|
this._sequencer.tryRun()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onImported: function (
|
||||||
|
importNode,
|
||||||
|
context,
|
||||||
|
e,
|
||||||
|
root,
|
||||||
|
importedAtRoot,
|
||||||
|
fullPath
|
||||||
|
) {
|
||||||
|
if (e) {
|
||||||
|
if (!e.filename) {
|
||||||
|
e.index = importNode.getIndex()
|
||||||
|
e.filename = importNode.fileInfo().filename
|
||||||
|
}
|
||||||
|
this.error = e
|
||||||
|
}
|
||||||
|
|
||||||
|
const importVisitor = this,
|
||||||
|
inlineCSS = importNode.options.inline,
|
||||||
|
isPlugin = importNode.options.isPlugin,
|
||||||
|
isOptional = importNode.options.optional,
|
||||||
|
duplicateImport =
|
||||||
|
importedAtRoot || fullPath in importVisitor.recursionDetector
|
||||||
|
|
||||||
|
if (!context.importMultiple) {
|
||||||
|
if (duplicateImport) {
|
||||||
|
importNode.skip = true
|
||||||
|
} else {
|
||||||
|
importNode.skip = function () {
|
||||||
|
if (fullPath in importVisitor.onceFileDetectionMap) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
importVisitor.onceFileDetectionMap[fullPath] = true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fullPath && isOptional) {
|
||||||
|
importNode.skip = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (root) {
|
||||||
|
importNode.root = root
|
||||||
|
importNode.importedFilename = fullPath
|
||||||
|
|
||||||
|
if (
|
||||||
|
!inlineCSS &&
|
||||||
|
!isPlugin &&
|
||||||
|
(context.importMultiple || !duplicateImport)
|
||||||
|
) {
|
||||||
|
importVisitor.recursionDetector[fullPath] = true
|
||||||
|
|
||||||
|
const oldContext = this.context
|
||||||
|
this.context = context
|
||||||
|
try {
|
||||||
|
this._visitor.visit(root)
|
||||||
|
} catch (e) {
|
||||||
|
this.error = e
|
||||||
|
}
|
||||||
|
this.context = oldContext
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
importVisitor.importCount--
|
||||||
|
|
||||||
|
if (importVisitor.isFinished) {
|
||||||
|
importVisitor._sequencer.tryRun()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
visitDeclaration: function (declNode, visitArgs) {
|
||||||
|
if (declNode.value.type === 'DetachedRuleset') {
|
||||||
|
this.context.frames.unshift(declNode)
|
||||||
|
} else {
|
||||||
|
visitArgs.visitDeeper = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
visitDeclarationOut: function (declNode) {
|
||||||
|
if (declNode.value.type === 'DetachedRuleset') {
|
||||||
|
this.context.frames.shift()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
visitAtRule: function (atRuleNode, visitArgs) {
|
||||||
|
this.context.frames.unshift(atRuleNode)
|
||||||
|
},
|
||||||
|
visitAtRuleOut: function (atRuleNode) {
|
||||||
|
this.context.frames.shift()
|
||||||
|
},
|
||||||
|
visitMixinDefinition: function (mixinDefinitionNode, visitArgs) {
|
||||||
|
this.context.frames.unshift(mixinDefinitionNode)
|
||||||
|
},
|
||||||
|
visitMixinDefinitionOut: function (mixinDefinitionNode) {
|
||||||
|
this.context.frames.shift()
|
||||||
|
},
|
||||||
|
visitRuleset: function (rulesetNode, visitArgs) {
|
||||||
|
this.context.frames.unshift(rulesetNode)
|
||||||
|
},
|
||||||
|
visitRulesetOut: function (rulesetNode) {
|
||||||
|
this.context.frames.shift()
|
||||||
|
},
|
||||||
|
visitMedia: function (mediaNode, visitArgs) {
|
||||||
|
this.context.frames.unshift(mediaNode.rules[0])
|
||||||
|
},
|
||||||
|
visitMediaOut: function (mediaNode) {
|
||||||
|
this.context.frames.shift()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default ImportVisitor
|
|
@ -0,0 +1,15 @@
|
||||||
|
import Visitor from './visitor'
|
||||||
|
import ImportVisitor from './import-visitor'
|
||||||
|
import MarkVisibleSelectorsVisitor from './set-tree-visibility-visitor'
|
||||||
|
import ExtendVisitor from './extend-visitor'
|
||||||
|
import JoinSelectorVisitor from './join-selector-visitor'
|
||||||
|
import ToCSSVisitor from './to-css-visitor'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
Visitor,
|
||||||
|
ImportVisitor,
|
||||||
|
MarkVisibleSelectorsVisitor,
|
||||||
|
ExtendVisitor,
|
||||||
|
JoinSelectorVisitor,
|
||||||
|
ToCSSVisitor
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
/* eslint-disable no-unused-vars */
|
||||||
|
/**
|
||||||
|
* @todo - Remove unused when JSDoc types are added for visitor methods
|
||||||
|
*/
|
||||||
|
import Visitor from './visitor';
|
||||||
|
|
||||||
|
class JoinSelectorVisitor {
|
||||||
|
constructor() {
|
||||||
|
this.contexts = [[]];
|
||||||
|
this._visitor = new Visitor(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
run(root) {
|
||||||
|
return this._visitor.visit(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
visitDeclaration(declNode, visitArgs) {
|
||||||
|
visitArgs.visitDeeper = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
visitMixinDefinition(mixinDefinitionNode, visitArgs) {
|
||||||
|
visitArgs.visitDeeper = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
visitRuleset(rulesetNode, visitArgs) {
|
||||||
|
const context = this.contexts[this.contexts.length - 1];
|
||||||
|
const paths = [];
|
||||||
|
let selectors;
|
||||||
|
|
||||||
|
this.contexts.push(paths);
|
||||||
|
|
||||||
|
if (!rulesetNode.root) {
|
||||||
|
selectors = rulesetNode.selectors;
|
||||||
|
if (selectors) {
|
||||||
|
selectors = selectors.filter(function(selector) { return selector.getIsOutput(); });
|
||||||
|
rulesetNode.selectors = selectors.length ? selectors : (selectors = null);
|
||||||
|
if (selectors) { rulesetNode.joinSelectors(paths, context, selectors); }
|
||||||
|
}
|
||||||
|
if (!selectors) { rulesetNode.rules = null; }
|
||||||
|
rulesetNode.paths = paths;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
visitRulesetOut(rulesetNode) {
|
||||||
|
this.contexts.length = this.contexts.length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
visitMedia(mediaNode, visitArgs) {
|
||||||
|
const context = this.contexts[this.contexts.length - 1];
|
||||||
|
mediaNode.rules[0].root = (context.length === 0 || context[0].multiMedia);
|
||||||
|
}
|
||||||
|
|
||||||
|
visitAtRule(atRuleNode, visitArgs) {
|
||||||
|
const context = this.contexts[this.contexts.length - 1];
|
||||||
|
if (atRuleNode.rules && atRuleNode.rules.length) {
|
||||||
|
atRuleNode.rules[0].root = (atRuleNode.isRooted || context.length === 0 || null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default JoinSelectorVisitor;
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue