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