This repository has been archived on 2023-08-30. You can view files and clone it, but cannot push or open issues/pull-requests.
bytedo
/
less
Archived
1
0
Fork 0
master
yutent 2023-06-17 22:31:03 +08:00
commit 4b4e92f3e9
103 changed files with 14656 additions and 0 deletions

17
.gitignore vendored Normal file
View File

@ -0,0 +1,17 @@
.vscode
node_modules/
dist
benchmark
bin
*.sublime-project
*.sublime-workspace
package-lock.json
._*
.Spotlight-V100
.Trashes
.DS_Store
.AppleDouble
.LSOverride

406
Gruntfile.js Normal file
View File

@ -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'])
}

13
README.md Normal file
View File

@ -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
```

14
build/banner.js Normal file
View File

@ -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}
*/
`

88
build/rollup.js Normal file
View File

@ -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()

1
index.js Normal file
View File

@ -0,0 +1 @@
module.exports = require('./lib/less-node').default;

130
package.json Normal file
View File

@ -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"
}
}

View File

@ -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
}
}

View File

@ -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

7
src/less-node/fs.js Normal file
View File

@ -0,0 +1,7 @@
let fs
try {
fs = require('graceful-fs')
} catch (e) {
fs = require('fs')
}
export default fs

View File

@ -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)
}

25
src/less-node/index.js Normal file
View File

@ -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

View File

@ -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]
}
}

View File

@ -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

View File

@ -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

12
src/less/constants.js Normal file
View File

@ -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
}

183
src/less/contexts.js Normal file
View File

@ -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 ?

150
src/less/data/colors.js Normal file
View File

@ -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'
}

4
src/less/data/index.js Normal file
View File

@ -0,0 +1,4 @@
import colors from './colors'
import unitConversions from './unit-conversions'
export default { colors, unitConversions }

View File

@ -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
}
}

View File

@ -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: ''
}
}

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -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 }
}

View File

@ -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 }

View File

@ -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

445
src/less/functions/color.js Normal file
View File

@ -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

View File

@ -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
)
}
}
}

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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
}

181
src/less/functions/list.js Normal file
View File

@ -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)
}
}

View File

@ -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

View File

@ -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

View File

@ -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
}
}

View File

@ -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)
}
}

116
src/less/functions/svg.js Normal file
View File

@ -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
)
}
}
}

View File

@ -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)
}
}

235
src/less/import-manager.js Normal file
View File

@ -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
}

97
src/less/index.js Normal file
View File

@ -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
}

172
src/less/less-error.js Normal file
View File

@ -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

34
src/less/logger.js Normal file
View File

@ -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: []
}

80
src/less/parse-tree.js Normal file
View File

@ -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
}

95
src/less/parse.js Normal file
View File

@ -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
}

172
src/less/parser/chunker.js Normal file
View File

@ -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
}

View File

@ -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
}

2740
src/less/parser/parser.js Normal file

File diff suppressed because it is too large Load Diff

180
src/less/plugin-manager.js Normal file
View File

@ -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

43
src/less/render.js Normal file
View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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

View File

@ -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

177
src/less/tree/atrule.js Normal file
View File

@ -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

View File

@ -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

118
src/less/tree/call.js Normal file
View File

@ -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

272
src/less/tree/color.js Normal file
View File

@ -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

View File

@ -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

28
src/less/tree/comment.js Normal file
View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

185
src/less/tree/dimension.js Normal file
View File

@ -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

86
src/less/tree/element.js Normal file
View File

@ -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

View File

@ -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

88
src/less/tree/extend.js Normal file
View File

@ -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

208
src/less/tree/import.js Normal file
View File

@ -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

79
src/less/tree/index.js Normal file
View File

@ -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
}
}

View File

@ -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

View File

@ -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

21
src/less/tree/keyword.js Normal file
View File

@ -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

185
src/less/tree/media.js Normal file
View File

@ -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

259
src/less/tree/mixin-call.js Normal file
View File

@ -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

View File

@ -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

View File

@ -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

25
src/less/tree/negative.js Normal file
View File

@ -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

193
src/less/tree/node.js Normal file
View File

@ -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

View File

@ -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

21
src/less/tree/paren.js Normal file
View File

@ -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

85
src/less/tree/property.js Normal file
View File

@ -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

79
src/less/tree/quoted.js Normal file
View File

@ -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

965
src/less/tree/ruleset.js Normal file
View File

@ -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

184
src/less/tree/selector.js Normal file
View File

@ -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

View File

@ -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

149
src/less/tree/unit.js Normal file
View File

@ -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

67
src/less/tree/url.js Normal file
View File

@ -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

46
src/less/tree/value.js Normal file
View File

@ -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

View File

@ -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

76
src/less/tree/variable.js Normal file
View File

@ -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

126
src/less/utils.js Normal file
View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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