This commit is contained in:
Your Name
2026-08-11 17:41:36 +08:00
parent cfe4c82c90
commit 03fe4ddf9d
18771 changed files with 3617239 additions and 0 deletions
+78
View File
@@ -0,0 +1,78 @@
<p align="center"><img src="http://lesscss.org/public/img/less_logo.png" width="264" height="117" alt="Less.js logo"></p>
<p align="center">
<a href="https://github.com/less/less.js/actions?query=branch%3Amaster"><img src="https://github.com/less/less.js/actions/workflows/ci.yml/badge.svg?branch=master" alt="Github Actions CI"/></a>
<a href="https://www.npmtrends.com/less"><img src="https://img.shields.io/npm/dm/less.svg?sanitize=true" alt="Downloads"></a>
<a href="https://www.npmjs.com/package/less"><img src="https://img.shields.io/npm/v/less.svg?sanitize=true" alt="npm version" /></a>
</p>
# Less.js
> The dynamic stylesheet language. [lesscss.org](http://lesscss.org)
Less extends CSS with variables, mixins, functions, nesting, and more — then compiles to standard CSS. Write cleaner stylesheets with less code.
```less
@primary: #4a90d9;
.button {
color: @primary;
&:hover {
color: darken(@primary, 10%);
}
}
```
## Install
```sh
npm install less
```
## Usage
### Node.js
```js
import less from 'less';
const output = await less.render('.class { width: (1 + 1) }');
console.log(output.css);
```
### Command Line
```sh
npx lessc styles.less styles.css
```
### Browser
```html
<link rel="stylesheet/less" type="text/css" href="styles.less" />
<script src="https://cdn.jsdelivr.net/npm/less"></script>
```
## Why Less?
- **Variables** — define reusable values once
- **Mixins** — reuse groups of declarations across rulesets
- **Nesting** — mirror HTML structure in your stylesheets
- **Functions** — transform colors, manipulate strings, do math
- **Imports** — split stylesheets into manageable pieces
- **Extend** — reduce output size by combining selectors
## Documentation
Full documentation, usage guides, and configuration options at **[lesscss.org](http://lesscss.org)**.
## Contributing
Less.js is open source. [Report bugs](https://github.com/less/less.js/issues), submit pull requests, or help improve the [documentation](https://github.com/less/less-docs).
See [CONTRIBUTING.md](https://github.com/less/less.js/blob/master/CONTRIBUTING.md) for development setup.
## License
Copyright (c) 2009-2025 [Alexis Sellier](http://cloudhead.io) & The Core Less Team
Licensed under the [Apache License](https://github.com/less/less.js/blob/master/LICENSE).
+14351
View File
File diff suppressed because it is too large Load Diff
+14929
View File
File diff suppressed because it is too large Load Diff
+14929
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
View File
@@ -0,0 +1,2 @@
// CJS entry — requires the pre-built CJS bundle
module.exports = require('./dist/less-node.cjs');
+53
View File
@@ -0,0 +1,53 @@
import {addDataAttr} from './utils.js';
import browser from './browser.js';
/**
* @param {Window} window
* @param {Record<string, *>} options
*/
export default (window, options) => {
// use options from the current script tag data attribues
addDataAttr(options, browser.currentScript(window));
if (options.isFileProtocol === undefined) {
options.isFileProtocol = /^(file|(chrome|safari)(-extension)?|resource|qrc|app):/.test(window.location.protocol);
}
// Load styles asynchronously (default: false)
//
// This is set to `false` by default, so that the body
// doesn't start loading before the stylesheets are parsed.
// Setting this to `true` can result in flickering.
//
options.async = options.async || false;
options.fileAsync = options.fileAsync || false;
// Interval between watch polls
options.poll = options.poll || (options.isFileProtocol ? 1000 : 1500);
options.env = options.env || (window.location.hostname == '127.0.0.1' ||
window.location.hostname == '0.0.0.0' ||
window.location.hostname == 'localhost' ||
(window.location.port &&
window.location.port.length > 0) ||
options.isFileProtocol ? 'development'
: 'production');
const dumpLineNumbers = /!dumpLineNumbers:(comments|mediaquery|all)/.exec(window.location.hash);
if (dumpLineNumbers) {
options.dumpLineNumbers = dumpLineNumbers[1];
}
if (options.useFileCache === undefined) {
options.useFileCache = true;
}
if (options.onReady === undefined) {
options.onReady = true;
}
if (options.relativeUrls) {
options.rewriteUrls = 'all';
}
};
+67
View File
@@ -0,0 +1,67 @@
/**
* Kicks off less and compiles any stylesheets
* used in the browser distributed version of less
* to kick-start less using the browser api
*/
import defaultOptions from '../less/default-options.js';
import addDefaultOptions from './add-default-options.js';
import root from './index.js';
const options = defaultOptions();
if (window.less) {
for (const key in window.less) {
if (Object.prototype.hasOwnProperty.call(window.less, key)) {
options[key] = window.less[key];
}
}
}
addDefaultOptions(window, options);
options.plugins = options.plugins || [];
if (window.LESS_PLUGINS) {
options.plugins = options.plugins.concat(window.LESS_PLUGINS);
}
const less = root(window, options);
export default less;
window.less = less;
let css;
let head;
let style;
// Always restore page visibility
function resolveOrReject(data) {
if (data.filename) {
console.warn(data);
}
if (!options.async) {
head.removeChild(style);
}
}
if (options.onReady) {
if (/!watch/.test(window.location.hash)) {
less.watch();
}
// Simulate synchronous stylesheet loading by hiding page rendering
if (!options.async) {
css = 'body { display: none !important }';
head = document.head || document.getElementsByTagName('head')[0];
style = document.createElement('style');
style.type = 'text/css';
if (style.styleSheet) {
style.styleSheet.cssText = css;
} else {
style.appendChild(document.createTextNode(css));
}
head.appendChild(style);
}
less.registerStylesheetsImmediately();
less.pageLoadFinished = less.refresh(less.env === 'development').then(resolveOrReject, resolveOrReject);
}
+65
View File
@@ -0,0 +1,65 @@
import * as utils from './utils.js';
export default {
createCSS: function (document, styles, sheet) {
// Strip the query-string
const href = sheet.href || '';
// If there is no title set, use the filename, minus the extension
const id = `less:${sheet.title || utils.extractId(href)}`;
// If this has already been inserted into the DOM, we may need to replace it
const oldStyleNode = document.getElementById(id);
let keepOldStyleNode = false;
// Create a new stylesheet node for insertion or (if necessary) replacement
const styleNode = document.createElement('style');
styleNode.setAttribute('type', 'text/css');
if (sheet.media) {
styleNode.setAttribute('media', sheet.media);
}
styleNode.id = id;
if (!styleNode.styleSheet) {
styleNode.appendChild(document.createTextNode(styles));
// If new contents match contents of oldStyleNode, don't replace oldStyleNode
keepOldStyleNode = (oldStyleNode !== null && oldStyleNode.childNodes.length > 0 && styleNode.childNodes.length > 0 &&
oldStyleNode.firstChild.nodeValue === styleNode.firstChild.nodeValue);
}
const head = document.getElementsByTagName('head')[0];
// If there is no oldStyleNode, just append; otherwise, only append if we need
// to replace oldStyleNode with an updated stylesheet
if (oldStyleNode === null || keepOldStyleNode === false) {
const nextEl = sheet && sheet.nextSibling || null;
if (nextEl) {
nextEl.parentNode.insertBefore(styleNode, nextEl);
} else {
head.appendChild(styleNode);
}
}
if (oldStyleNode && keepOldStyleNode === false) {
oldStyleNode.parentNode.removeChild(oldStyleNode);
}
// For IE.
// This needs to happen *after* the style element is added to the DOM, otherwise IE 7 and 8 may crash.
// See http://social.msdn.microsoft.com/Forums/en-US/7e081b65-878a-4c22-8e68-c10d39c2ed32/internet-explorer-crashes-appending-style-element-to-head
if (styleNode.styleSheet) {
try {
styleNode.styleSheet.cssText = styles;
} catch (e) {
throw new Error('Couldn\'t reassign styleSheet.cssText.');
}
}
},
currentScript: function(window) {
const document = window.document;
return document.currentScript || (() => {
const scripts = document.getElementsByTagName('script');
return scripts[scripts.length - 1];
})();
}
};
+43
View File
@@ -0,0 +1,43 @@
// Cache system is a bit outdated and could do with work
export default (window, options, logger) => {
let cache = null;
if (options.env !== 'development') {
try {
cache = (typeof window.localStorage === 'undefined') ? null : window.localStorage;
} catch (_) {}
}
return {
setCSS: function(path, lastModified, modifyVars, styles) {
if (cache) {
logger.info(`saving ${path} to cache.`);
try {
cache.setItem(path, styles);
cache.setItem(`${path}:timestamp`, lastModified);
if (modifyVars) {
cache.setItem(`${path}:vars`, JSON.stringify(modifyVars));
}
} catch (e) {
// TODO - could do with adding more robust error handling
logger.error(`failed to save "${path}" to local storage for caching.`);
}
}
},
getCSS: function(path, webInfo, modifyVars) {
const css = cache && cache.getItem(path);
const timestamp = cache && cache.getItem(`${path}:timestamp`);
let vars = cache && cache.getItem(`${path}:vars`);
modifyVars = modifyVars || {};
vars = vars || '{}'; // if not set, treat as the JSON representation of an empty object
if (timestamp && webInfo.lastModified &&
(new Date(webInfo.lastModified).valueOf() ===
new Date(timestamp).valueOf()) &&
JSON.stringify(modifyVars) === vars) {
// Use local copy
return css;
}
}
};
};
+170
View File
@@ -0,0 +1,170 @@
import * as utils from './utils.js';
import browser from './browser.js';
export default (window, less, options) => {
function errorHTML(e, rootHref) {
const id = `less-error-message:${utils.extractId(rootHref || '')}`;
const template = '<li><label>{line}</label><pre class="{class}">{content}</pre></li>';
const elem = window.document.createElement('div');
let timer;
let content;
const errors = [];
const filename = e.filename || rootHref;
const filenameNoPath = filename.match(/([^/]+(\?.*)?)$/)[1];
elem.id = id;
elem.className = 'less-error-message';
content = `<h3>${e.type || 'Syntax'}Error: ${e.message || 'There is an error in your .less file'}` +
`</h3><p>in <a href="${filename}">${filenameNoPath}</a> `;
const errorline = (e, i, classname) => {
if (e.extract[i] !== undefined) {
errors.push(template.replace(/\{line\}/, (parseInt(e.line, 10) || 0) + (i - 1))
.replace(/\{class\}/, classname)
.replace(/\{content\}/, e.extract[i]));
}
};
if (e.line) {
errorline(e, 0, '');
errorline(e, 1, 'line');
errorline(e, 2, '');
content += `on line ${e.line}, column ${e.column + 1}:</p><ul>${errors.join('')}</ul>`;
}
if (e.stack && (e.extract || options.logLevel >= 4)) {
content += `<br/>Stack Trace</br />${e.stack.split('\n').slice(1).join('<br/>')}`;
}
elem.innerHTML = content;
// CSS for error messages
browser.createCSS(window.document, [
'.less-error-message ul, .less-error-message li {',
'list-style-type: none;',
'margin-right: 15px;',
'padding: 4px 0;',
'margin: 0;',
'}',
'.less-error-message label {',
'font-size: 12px;',
'margin-right: 15px;',
'padding: 4px 0;',
'color: #cc7777;',
'}',
'.less-error-message pre {',
'color: #dd6666;',
'padding: 4px 0;',
'margin: 0;',
'display: inline-block;',
'}',
'.less-error-message pre.line {',
'color: #ff0000;',
'}',
'.less-error-message h3 {',
'font-size: 20px;',
'font-weight: bold;',
'padding: 15px 0 5px 0;',
'margin: 0;',
'}',
'.less-error-message a {',
'color: #10a',
'}',
'.less-error-message .error {',
'color: red;',
'font-weight: bold;',
'padding-bottom: 2px;',
'border-bottom: 1px dashed red;',
'}'
].join('\n'), { title: 'error-message' });
elem.style.cssText = [
'font-family: Arial, sans-serif',
'border: 1px solid #e00',
'background-color: #eee',
'border-radius: 5px',
'-webkit-border-radius: 5px',
'-moz-border-radius: 5px',
'color: #e00',
'padding: 15px',
'margin-bottom: 15px'
].join(';');
if (options.env === 'development') {
timer = setInterval(() => {
const document = window.document;
const body = document.body;
if (body) {
if (document.getElementById(id)) {
body.replaceChild(elem, document.getElementById(id));
} else {
body.insertBefore(elem, body.firstChild);
}
clearInterval(timer);
}
}, 10);
}
}
function removeErrorHTML(path) {
const node = window.document.getElementById(`less-error-message:${utils.extractId(path)}`);
if (node) {
node.parentNode.removeChild(node);
}
}
function removeErrorConsole() {
// no action
}
function removeError(path) {
if (!options.errorReporting || options.errorReporting === 'html') {
removeErrorHTML(path);
} else if (options.errorReporting === 'console') {
removeErrorConsole(path);
} else if (typeof options.errorReporting === 'function') {
options.errorReporting('remove', path);
}
}
function errorConsole(e, rootHref) {
const template = '{line} {content}';
const filename = e.filename || rootHref;
const errors = [];
let content = `${e.type || 'Syntax'}Error: ${e.message || 'There is an error in your .less file'} in ${filename}`;
const errorline = (e, i, classname) => {
if (e.extract[i] !== undefined) {
errors.push(template.replace(/\{line\}/, (parseInt(e.line, 10) || 0) + (i - 1))
.replace(/\{class\}/, classname)
.replace(/\{content\}/, e.extract[i]));
}
};
if (e.line) {
errorline(e, 0, '');
errorline(e, 1, 'line');
errorline(e, 2, '');
content += ` on line ${e.line}, column ${e.column + 1}:\n${errors.join('\n')}`;
}
if (e.stack && (e.extract || options.logLevel >= 4)) {
content += `\nStack Trace\n${e.stack}`;
}
less.logger.error(content);
}
function error(e, rootHref) {
if (!options.errorReporting || options.errorReporting === 'html') {
errorHTML(e, rootHref);
} else if (options.errorReporting === 'console') {
errorConsole(e, rootHref);
} else if (typeof options.errorReporting === 'function') {
options.errorReporting('add', e, rootHref);
}
}
return {
add: error,
remove: removeError
};
};
+112
View File
@@ -0,0 +1,112 @@
import AbstractFileManager from '../less/environment/abstract-file-manager.js';
let options;
let logger;
let fileCache = {};
// TODOS - move log somewhere. pathDiff and doing something similar in node. use pathDiff in the other browser file for the initial load
const FileManager = function() {}
FileManager.prototype = Object.assign(new AbstractFileManager(), {
alwaysMakePathsAbsolute() {
return true;
},
join(basePath, laterPath) {
if (!basePath) {
return laterPath;
}
return this.extractUrlParts(laterPath, basePath).path;
},
doXHR(url, type, callback, errback) {
const xhr = new XMLHttpRequest();
const async = options.isFileProtocol ? options.fileAsync : true;
if (typeof xhr.overrideMimeType === 'function') {
xhr.overrideMimeType('text/css');
}
logger.debug(`XHR: Getting '${url}'`);
xhr.open('GET', url, async);
xhr.setRequestHeader('Accept', type || 'text/x-less, text/css; q=0.9, */*; q=0.5');
xhr.send(null);
function handleResponse(xhr, callback, errback) {
if (xhr.status >= 200 && xhr.status < 300) {
callback(xhr.responseText,
xhr.getResponseHeader('Last-Modified'));
} else if (typeof errback === 'function') {
errback(xhr.status, url);
}
}
if (options.isFileProtocol && !options.fileAsync) {
if (xhr.status === 0 || (xhr.status >= 200 && xhr.status < 300)) {
callback(xhr.responseText);
} else {
errback(xhr.status, url);
}
} else if (async) {
xhr.onreadystatechange = () => {
if (xhr.readyState == 4) {
handleResponse(xhr, callback, errback);
}
};
} else {
handleResponse(xhr, callback, errback);
}
},
supports() {
return true;
},
clearFileCache() {
fileCache = {};
},
loadFile(filename, currentDirectory, options) {
// TODO: Add prefix support like less-node?
// What about multiple paths?
if (currentDirectory && !this.isPathAbsolute(filename)) {
filename = currentDirectory + filename;
}
filename = options.ext ? this.tryAppendExtension(filename, options.ext) : filename;
options = options || {};
// sheet may be set to the stylesheet for the initial load or a collection of properties including
// some context variables for imports
const hrefParts = this.extractUrlParts(filename, window.location.href);
const href = hrefParts.url;
const self = this;
return new Promise((resolve, reject) => {
if (options.useFileCache && fileCache[href]) {
try {
const lessText = fileCache[href];
return resolve({ contents: lessText, filename: href, webInfo: { lastModified: new Date() }});
} catch (e) {
return reject({ filename: href, message: `Error loading file ${href} error was ${e.message}` });
}
}
self.doXHR(href, options.mime, function doXHRCallback(data, lastModified) {
// per file cache
fileCache[href] = data;
// Use remote copy (re-parse)
resolve({ contents: data, filename: href, webInfo: { lastModified }});
}, function doXHRError(status, url) {
reject({ type: 'File', message: `'${url}' wasn't found (${status})`, href });
});
});
}
});
export default (opts, log) => {
options = opts;
logger = log;
return FileManager;
}
+28
View File
@@ -0,0 +1,28 @@
import functionRegistry from './../less/functions/function-registry.js';
export default () => {
function imageSize() {
throw {
type: 'Runtime',
message: 'Image size functions are not supported in browser version of less'
};
}
const imageFunctions = {
'image-size': function(filePathNode) {
imageSize(this, filePathNode);
return -1;
},
'image-width': function(filePathNode) {
imageSize(this, filePathNode);
return -1;
},
'image-height': function(filePathNode) {
imageSize(this, filePathNode);
return -1;
}
};
functionRegistry.addMultiple(imageFunctions);
};
+284
View File
@@ -0,0 +1,284 @@
//
// index.js
// Should expose the additional browser functions on to the less object
//
import {addDataAttr} from './utils.js';
import lessRoot from '../less/index.js';
import browser from './browser.js';
import FM from './file-manager.js';
import PluginLoader from './plugin-loader.js';
import LogListener from './log-listener.js';
import ErrorReporting from './error-reporting.js';
import Cache from './cache.js';
import ImageSize from './image-size.js';
import pkg from '../../package.json';
/**
* @param {Window} window
* @param {Object} options
*/
export default (window, options) => {
const document = window.document;
const less = lessRoot(undefined, undefined, pkg.version);
less.options = options;
const environment = less.environment;
const FileManager = FM(options, less.logger);
const fileManager = new FileManager();
environment.addFileManager(fileManager);
less.FileManager = FileManager;
less.PluginLoader = PluginLoader;
LogListener(less, options);
const errors = ErrorReporting(window, less, options);
const cache = less.cache = options.cache || Cache(window, options, less.logger);
ImageSize(less.environment);
// Setup user functions - Deprecate?
if (options.functions) {
less.functions.functionRegistry.addMultiple(options.functions);
}
const typePattern = /^text\/(x-)?less$/;
function clone(obj) {
const cloned = {};
for (const prop in obj) {
if (Object.prototype.hasOwnProperty.call(obj, prop)) {
cloned[prop] = obj[prop];
}
}
return cloned;
}
function loadStyles(modifyVars) {
const styles = document.getElementsByTagName('style');
for (let style of styles) {
if (style.type.match(typePattern)) {
const instanceOptions = {
...clone(options),
modifyVars,
filename: document.location.href.replace(/#.*$/, '')
}
const lessText = style.innerHTML || '';
/* jshint loopfunc:true */
less.render(lessText, instanceOptions, (err, result) => {
if (err) {
errors.add(err, 'inline');
} else {
style.type = 'text/css';
if (style.styleSheet) {
style.styleSheet.cssText = result.css;
} else {
style.innerHTML = result.css;
}
}
});
}
}
}
function loadStyleSheet(sheet, callback, reload, remaining, modifyVars) {
const instanceOptions = clone(options);
addDataAttr(instanceOptions, sheet);
instanceOptions.mime = sheet.type;
if (modifyVars) {
instanceOptions.modifyVars = modifyVars;
}
function loadInitialFileCallback(loadedFile) {
const data = loadedFile.contents;
const path = loadedFile.filename;
const webInfo = loadedFile.webInfo;
const newFileInfo = {
currentDirectory: fileManager.getPath(path),
filename: path,
rootFilename: path,
rewriteUrls: instanceOptions.rewriteUrls
};
newFileInfo.entryPath = newFileInfo.currentDirectory;
newFileInfo.rootpath = instanceOptions.rootpath || newFileInfo.currentDirectory;
if (webInfo) {
webInfo.remaining = remaining;
const css = cache.getCSS(path, webInfo, instanceOptions.modifyVars);
if (!reload && css) {
webInfo.local = true;
callback(null, css, data, sheet, webInfo, path);
return;
}
}
// TODO add tests around how this behaves when reloading
errors.remove(path);
instanceOptions.rootFileInfo = newFileInfo;
less.render(data, instanceOptions, (e, result) => {
if (e) {
e.href = path;
callback(e);
} else {
cache.setCSS(sheet.href, webInfo.lastModified, instanceOptions.modifyVars, result.css);
callback(null, result.css, data, sheet, webInfo, path);
}
});
}
fileManager.loadFile(sheet.href, null, instanceOptions, environment)
.then(loadedFile => {
loadInitialFileCallback(loadedFile);
}).catch(err => {
console.log(err);
callback(err);
});
}
function loadStyleSheets(callback, reload, modifyVars) {
for (let i = 0; i < less.sheets.length; i++) {
loadStyleSheet(less.sheets[i], callback, reload, less.sheets.length - (i + 1), modifyVars);
}
}
function initRunningMode() {
if (less.env === 'development') {
less.watchTimer = setInterval(() => {
if (less.watchMode) {
fileManager.clearFileCache();
/**
* @todo remove when this is typed with JSDoc
*/
// eslint-disable-next-line no-unused-vars
loadStyleSheets((e, css, _, sheet, webInfo) => {
if (e) {
errors.add(e, e.href || sheet.href);
} else if (css) {
browser.createCSS(window.document, css, sheet);
}
});
}
}, options.poll);
}
}
//
// Watch mode
//
less.watch = function () {
if (!less.watchMode ) {
less.env = 'development';
initRunningMode();
}
this.watchMode = true;
return true;
};
less.unwatch = function () {clearInterval(less.watchTimer); this.watchMode = false; return false; };
//
// Synchronously get all <link> tags with the 'rel' attribute set to
// "stylesheet/less".
//
less.registerStylesheetsImmediately = () => {
const links = document.getElementsByTagName('link');
less.sheets = [];
for (let i = 0; i < links.length; i++) {
if (links[i].rel === 'stylesheet/less' || (links[i].rel.match(/stylesheet/) &&
(links[i].type.match(typePattern)))) {
less.sheets.push(links[i]);
}
}
};
//
// Asynchronously get all <link> tags with the 'rel' attribute set to
// "stylesheet/less", returning a Promise.
//
less.registerStylesheets = () => new Promise((resolve) => {
less.registerStylesheetsImmediately();
resolve();
});
//
// With this function, it's possible to alter variables and re-render
// CSS without reloading less-files
//
less.modifyVars = record => less.refresh(true, record, false);
less.refresh = (reload, modifyVars, clearFileCache) => {
if ((reload || clearFileCache) && clearFileCache !== false) {
fileManager.clearFileCache();
}
return new Promise((resolve, reject) => {
let startTime;
let endTime;
let totalMilliseconds;
let remainingSheets;
startTime = endTime = new Date();
// Set counter for remaining unprocessed sheets
remainingSheets = less.sheets.length;
if (remainingSheets === 0) {
endTime = new Date();
totalMilliseconds = endTime - startTime;
less.logger.info('Less has finished and no sheets were loaded.');
resolve({
startTime,
endTime,
totalMilliseconds,
sheets: less.sheets.length
});
} else {
// Relies on less.sheets array, callback seems to be guaranteed to be called for every element of the array
loadStyleSheets((e, css, _, sheet, webInfo) => {
if (e) {
errors.add(e, e.href || sheet.href);
reject(e);
return;
}
if (webInfo.local) {
less.logger.info(`Loading ${sheet.href} from cache.`);
} else {
less.logger.info(`Rendered ${sheet.href} successfully.`);
}
browser.createCSS(window.document, css, sheet);
less.logger.info(`CSS for ${sheet.href} generated in ${new Date() - endTime}ms`);
// Count completed sheet
remainingSheets--;
// Check if the last remaining sheet was processed and then call the promise
if (remainingSheets === 0) {
totalMilliseconds = new Date() - startTime;
less.logger.info(`Less has finished. CSS generated in ${totalMilliseconds}ms`);
resolve({
startTime,
endTime,
totalMilliseconds,
sheets: less.sheets.length
});
}
endTime = new Date();
}, reload, modifyVars);
}
loadStyles(modifyVars);
});
};
less.refreshStyles = loadStyles;
return less;
};
+42
View File
@@ -0,0 +1,42 @@
export default (less, options) => {
const logLevel_debug = 4;
const logLevel_info = 3;
const logLevel_warn = 2;
const logLevel_error = 1;
// The amount of logging in the javascript console.
// 3 - Debug, information and errors
// 2 - Information and errors
// 1 - Errors
// 0 - None
// Defaults to 2
options.logLevel = typeof options.logLevel !== 'undefined' ? options.logLevel : (options.env === 'development' ? logLevel_info : logLevel_error);
if (!options.loggers) {
options.loggers = [{
debug: function(msg) {
if (options.logLevel >= logLevel_debug) {
console.log(msg);
}
},
info: function(msg) {
if (options.logLevel >= logLevel_info) {
console.log(msg);
}
},
warn: function(msg) {
if (options.logLevel >= logLevel_warn) {
console.warn(msg);
}
},
error: function(msg) {
if (options.logLevel >= logLevel_error) {
console.error(msg);
}
}
}];
}
for (let i = 0; i < options.loggers.length; i++) {
less.logger.addListener(options.loggers[i]);
}
};
+24
View File
@@ -0,0 +1,24 @@
/**
* @todo Add tests for browser `@plugin`
*/
import AbstractPluginLoader from '../less/environment/abstract-plugin-loader.js';
/**
* Browser Plugin Loader
*/
const PluginLoader = function(less) {
this.less = less;
// Should we shim this.require for browser? Probably not?
};
PluginLoader.prototype = Object.assign(new AbstractPluginLoader(), {
loadPlugin(filename, basePath, context, environment, fileManager) {
return new Promise((fulfill, reject) => {
fileManager.loadFile(filename, basePath, context, environment)
.then(fulfill).catch(reject);
});
}
});
export default PluginLoader;
+30
View File
@@ -0,0 +1,30 @@
/** @param {string} href */
export function extractId(href) {
return href.replace(/^[a-z-]+:\/+?[^/]+/, '') // Remove protocol & domain
.replace(/[?&]livereload=\w+/, '') // Remove LiveReload cachebuster
.replace(/^\//, '') // Remove root /
.replace(/\.[a-zA-Z]+$/, '') // Remove simple extension
.replace(/[^.\w-]+/g, '-') // Replace illegal characters
.replace(/\./g, ':'); // Replace dots with colons(for valid id)
}
/**
* @param {Record<string, *>} options
* @param {HTMLElement | null} tag
*/
export function addDataAttr(options, tag) {
if (!tag) {return;} // in case of tag is null or undefined
for (const opt in tag.dataset) {
if (Object.prototype.hasOwnProperty.call(tag.dataset, opt)) {
if (opt === 'env' || opt === 'dumpLineNumbers' || opt === 'rootpath' || opt === 'errorReporting') {
options[opt] = tag.dataset[opt];
} else {
try {
options[opt] = JSON.parse(tag.dataset[opt]);
}
catch (_) {}
}
}
}
}
+43
View File
@@ -0,0 +1,43 @@
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
class SourceMapGeneratorFallback {
addMapping(){}
setSourceContent(){}
toJSON(){
return null;
}
}
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) {
try {
const mimeModule = require('mime');
return mimeModule ? mimeModule.lookup(filename) : 'application/octet-stream';
} catch (e) {
return 'application/octet-stream';
}
},
charsetLookup: function (mime) {
try {
const mimeModule = require('mime');
return mimeModule ? mimeModule.charsets.lookup(mime) : undefined;
} catch (e) {
return undefined;
}
},
getSourceMapGenerator: function getSourceMapGenerator() {
try {
const sourceMapModule = require('source-map');
return sourceMapModule ? sourceMapModule.SourceMapGenerator : SourceMapGeneratorFallback;
} catch (e) {
return SourceMapGeneratorFallback;
}
}
};
+156
View File
@@ -0,0 +1,156 @@
import path from 'path';
import { createRequire } from 'module';
import fs from './fs.js';
import AbstractFileManager from '../less/environment/abstract-file-manager.js';
const require = createRequire(import.meta.url);
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]) {
if (paths[i].startsWith('#')) {
// Handling paths starting with '#'
fullFilename = paths[i].substr(1) + fullFilename;
}else{
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;
+14
View File
@@ -0,0 +1,14 @@
/** @typedef {import('fs')} FS */
import nodeFs from 'fs';
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
/** @type {FS} */
let fs;
try {
fs = require('graceful-fs');
} catch (e) {
fs = nodeFs;
}
export default fs;
+78
View File
@@ -0,0 +1,78 @@
import { readFileSync } from 'fs';
import { createRequire } from 'module';
import Dimension from '../less/tree/dimension.js';
import Expression from '../less/tree/expression.js';
import functionRegistry from './../less/functions/function-registry.js';
const require = createRequire(import.meta.url);
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;
}
let probe;
try {
probe = require('probe-image-size/sync');
} catch (_) {
return { width: 0, height: 0 };
}
const size = probe(readFileSync(fileSync.filename));
if (!size) {
throw {
type: 'File',
message: `Unrecognised image format for '${filePath}'`
};
}
return {
width: size.width,
height: size.height
};
}
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);
};
+31
View File
@@ -0,0 +1,31 @@
import { createRequire } from 'module';
import environment from './environment.js';
import FileManager from './file-manager.js';
import UrlFileManager from './url-file-manager.js';
import createFromEnvironment from '../less/index.js';
import lesscHelper from './lessc-helper.js';
import PluginLoader from './plugin-loader.js';
import fs from './fs.js';
import defaultOptions from '../less/default-options.js';
import imageSize from './image-size.js';
const require = createRequire(import.meta.url);
const { version } = require('../../package.json');
const less = createFromEnvironment(environment, [new FileManager(), new UrlFileManager()], version);
// allow people to create less with their own environment
less.createFromEnvironment = createFromEnvironment;
less.lesscHelper = lesscHelper;
less.PluginLoader = PluginLoader;
less.fs = fs;
less.FileManager = FileManager;
less.UrlFileManager = UrlFileManager;
// Set up options
less.options = defaultOptions();
// provide image-size functionality
imageSize(less.environment);
export default less;
+95
View File
@@ -0,0 +1,95 @@
// 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(' --quiet Suppresses output of warnings.');
console.log(' --strict-imports (DEPRECATED) Ignores .less imports inside selector blocks. Has confusing behavior.');
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(' --quiet-deprecations Suppress deprecation warnings only (keeps other warnings).');
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 (DEPRECATED) Outputs filename and line numbers.');
console.log(' TYPE can be either \'comments\', \'mediaquery\', or \'all\'.');
console.log(' The entire dumpLineNumbers option is deprecated.');
console.log(' Use sourcemaps (--source-map) instead.');
console.log(' All modes will be removed in a future version.');
console.log(' Note: \'mediaquery\' and \'all\' modes generate @media -sass-debug-info');
console.log(' which had short-lived usage and is no longer recommended.');
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/>');
}
};
export const { stylize, printUsage } = lessc_helper;
export default lessc_helper;
+62
View File
@@ -0,0 +1,62 @@
import path from 'path';
import { createRequire } from 'module';
import AbstractPluginLoader from '../less/environment/abstract-plugin-loader.js';
const require = createRequire(import.meta.url);
/**
* Node Plugin Loader
*/
const PluginLoader = function(less) {
this.less = less;
this.require = prefix => {
prefix = path.dirname(prefix);
return id => {
const str = id.slice(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;
+60
View File
@@ -0,0 +1,60 @@
/* eslint-disable no-unused-vars */
/**
* @todo - remove top eslint rule when FileManagers have JSDoc type
* and are TS-type-checked
*/
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const isUrlRe = /^(?:https?:)?\/\//i;
import url from 'url';
let request;
import AbstractFileManager from '../less/environment/abstract-file-manager.js';
import logger from '../less/logger.js';
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;
+13
View File
@@ -0,0 +1,13 @@
export const Math = {
ALWAYS: 0,
PARENS_DIVISION: 1,
PARENS: 2
// removed - STRICT_LEGACY: 3
};
export const RewriteUrls = {
OFF: 0,
LOCAL: 1,
ALL: 2
};
+165
View File
@@ -0,0 +1,165 @@
const contexts = {};
export default contexts;
import * as Constants from './constants.js';
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 - @deprecated The dumpLineNumbers option is deprecated. Use sourcemaps instead. All modes ('comments', 'mediaquery', 'all') will be removed in a future version.
'compress', // option - whether to compress
'syncImport', // option - whether to import synchronously
'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
'quiet', // option - whether to log warnings
'quietDeprecations', // option - whether to suppress deprecation warnings only
];
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
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
View File
@@ -0,0 +1,4 @@
import colors from './colors.js';
import unitConversions from './unit-conversions.js';
export default { colors, unitConversions };
+21
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
}
};
+89
View File
@@ -0,0 +1,89 @@
// 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,
/**
* @deprecated This option has confusing behavior and may be removed in a future version.
*
* When true, prevents @import statements for .less files from being evaluated inside
* selector blocks (rulesets). The imports are silently ignored and not output.
*
* Behavior:
* - @import at root level: Always processed
* - @import inside @-rules (@media, @supports, etc.): Processed (these are not selector blocks)
* - @import inside selector blocks (.class, #id, etc.): NOT processed (silently ignored)
*
* When false (default): All @import statements are processed regardless of context.
*
* Note: Despite the name "strict", this option does NOT throw an error when imports
* are used in selector blocks - it silently ignores them. This is confusing
* behavior that may catch users off guard.
*
* Note: Only affects .less file imports. CSS imports (url(...) or .css files) are
* always output as CSS @import statements regardless of this setting.
*
* @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: ''
}
}
+82
View File
@@ -0,0 +1,82 @@
/**
* Deprecation registry for Less.js
*
* Each deprecation has a unique ID and description.
* Repetition limiting caps warnings at 5 per deprecation type per parse.
* Use --quiet-deprecations to suppress all deprecation warnings.
*/
const deprecations = {
'mixin-call-no-parens': {
description: 'Calling a mixin without parentheses is deprecated.'
},
'mixin-call-whitespace': {
description: 'Whitespace between a mixin name and parentheses for a mixin call is deprecated.'
},
'dot-slash-operator': {
description: 'The ./ operator is deprecated.'
},
'variable-in-unknown-value': {
description: '@variable in custom property values is treated as literal text.'
},
'variable-in-at-rule-prelude': {
description: 'A bare @variable in an at-rule prelude (e.g. @media @foo) is deprecated. Use @{variable} interpolation instead.'
},
'numeric-variable-name': {
description: 'Variable names beginning with a number are deprecated and will be removed in Less 5.x.'
},
'dash-only-variable-name': {
description: 'The dash-only variable names @- and @{-} are deprecated and will be removed in Less 5.x.'
},
'dash-only-mixin-name': {
description: 'The dash-only mixin names .-() and #-() are deprecated and will be removed in Less 5.x.'
},
'dynamic-charset': {
description: 'Dynamic @charset interpolation is deprecated and will be removed in Less 5.x.'
},
'property-in-unknown-value': {
description: '$property in custom property values is treated as literal text.'
},
'js-eval': {
description: 'Inline JavaScript evaluation (backtick expressions) is deprecated and will be removed in Less 5.x.'
},
'at-plugin': {
description: 'The @plugin directive is deprecated and will be replaced in Less 5.x.'
},
'dump-line-numbers': {
description: 'The dumpLineNumbers option is deprecated and will be removed in Less 5.x.'
},
'math-always': {
description: '--math=always is deprecated and will be removed in Less 5.x.'
}
};
const MAX_REPETITIONS = 5;
class DeprecationHandler {
constructor() {
/** @type {Record<string, number>} */
this._counts = {};
}
/** @param {string} deprecationId */
shouldWarn(deprecationId) {
if (!deprecationId) { return true; }
const count = (this._counts[deprecationId] || 0) + 1;
this._counts[deprecationId] = count;
return count <= MAX_REPETITIONS;
}
/** @param {{ warn: (msg: string) => void }} logger */
summarize(logger) {
for (const id of Object.keys(this._counts)) {
const omitted = this._counts[id] - MAX_REPETITIONS;
if (omitted > 0) {
logger.warn(`${omitted} repetitive "${id}" deprecation warning(s) omitted.`);
}
}
}
}
export { deprecations, DeprecationHandler, MAX_REPETITIONS };
export default { deprecations, DeprecationHandler };
@@ -0,0 +1,140 @@
class AbstractFileManager {
getPath(filename) {
let j = filename.lastIndexOf('?');
if (j > 0) {
filename = filename.slice(0, j);
}
j = filename.lastIndexOf('/');
if (j < 0) {
j = filename.lastIndexOf('\\');
}
if (j < 0) {
return '';
}
return filename.slice(0, j + 1);
}
tryAppendExtension(path, ext) {
return /(\.[a-z]*$)|([?;].*)$/.test(path) ? path : path + ext;
}
tryAppendLessExtension(path) {
return this.tryAppendExtension(path, '.less');
}
supportsSync() {
return false;
}
alwaysMakePathsAbsolute() {
return false;
}
isPathAbsolute(filename) {
return (/^(?:[a-z-]+:|\/|\\|#)/i).test(filename);
}
// TODO: pull out / replace?
join(basePath, laterPath) {
if (!basePath) {
return laterPath;
}
return basePath + laterPath;
}
pathDiff(url, baseUrl) {
// diff between two paths to create a relative path
const urlParts = this.extractUrlParts(url);
const baseUrlParts = this.extractUrlParts(baseUrl);
let i;
let max;
let urlDirectories;
let baseUrlDirectories;
let diff = '';
if (urlParts.hostPart !== baseUrlParts.hostPart) {
return '';
}
max = Math.max(baseUrlParts.directories.length, urlParts.directories.length);
for (i = 0; i < max; i++) {
if (baseUrlParts.directories[i] !== urlParts.directories[i]) { break; }
}
baseUrlDirectories = baseUrlParts.directories.slice(i);
urlDirectories = urlParts.directories.slice(i);
for (i = 0; i < baseUrlDirectories.length - 1; i++) {
diff += '../';
}
for (i = 0; i < urlDirectories.length - 1; i++) {
diff += `${urlDirectories[i]}/`;
}
return diff;
}
/**
* Helper function, not part of API.
* This should be replaceable by newer Node / Browser APIs
*
* @param {string} url
* @param {string} baseUrl
*/
extractUrlParts(url, baseUrl) {
// urlParts[1] = protocol://hostname/ OR /
// urlParts[2] = / if path relative to host base
// urlParts[3] = directories
// urlParts[4] = filename
// urlParts[5] = parameters
const urlPartsRegex = /^((?:[a-z-]+:)?\/{2}(?:[^/?#]*\/)|([/\\]))?((?:[^/\\?#]*[/\\])*)([^/\\?#]*)([#?].*)?$/i;
const urlParts = url.match(urlPartsRegex);
const returner = {};
let rawDirectories = [];
const directories = [];
let i;
let baseUrlParts;
if (!urlParts) {
throw new Error(`Could not parse sheet href - '${url}'`);
}
// Stylesheets in IE don't always return the full path
if (baseUrl && (!urlParts[1] || urlParts[2])) {
baseUrlParts = baseUrl.match(urlPartsRegex);
if (!baseUrlParts) {
throw new Error(`Could not parse page url - '${baseUrl}'`);
}
urlParts[1] = urlParts[1] || baseUrlParts[1] || '';
if (!urlParts[2]) {
urlParts[3] = baseUrlParts[3] + urlParts[3];
}
}
if (urlParts[3]) {
rawDirectories = urlParts[3].replace(/\\/g, '/').split('/');
// collapse '..' and skip '.'
for (i = 0; i < rawDirectories.length; i++) {
if (rawDirectories[i] === '..') {
directories.pop();
}
else if (rawDirectories[i] !== '.') {
directories.push(rawDirectories[i]);
}
}
}
returner.hostPart = urlParts[1];
returner.directories = directories;
returner.rawPath = (urlParts[1] || '') + rawDirectories.join('/');
returner.path = (urlParts[1] || '') + directories.join('/');
returner.filename = urlParts[4];
returner.fileUrl = returner.path + (urlParts[4] || '');
returner.url = returner.fileUrl + (urlParts[5] || '');
return returner;
}
}
export default AbstractFileManager;
@@ -0,0 +1,185 @@
import functionRegistry from '../functions/function-registry.js';
import LessError from '../less-error.js';
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;
+21
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
}
+59
View File
@@ -0,0 +1,59 @@
/**
* @todo Document why this abstraction exists, and the relationship between
* environment, file managers, and plugin manager
*/
import logger from '../logger.js';
class Environment {
constructor(externalEnvironment, fileManagers) {
this.fileManagers = fileManagers || [];
externalEnvironment = externalEnvironment || {};
const optionalFunctions = ['encodeBase64', 'mimeLookup', 'charsetLookup', 'getSourceMapGenerator'];
const requiredFunctions = [];
const functions = requiredFunctions.concat(optionalFunctions);
for (let i = 0; i < functions.length; i++) {
const propName = functions[i];
const environmentFunc = externalEnvironment[propName];
if (environmentFunc) {
this[propName] = environmentFunc.bind(externalEnvironment);
} else if (i < requiredFunctions.length) {
this.warn(`missing required function in environment - ${propName}`);
}
}
}
getFileManager(filename, currentDirectory, options, environment, isSync) {
if (!filename) {
logger.warn('getFileManager called with no filename.. Please report this issue. continuing.');
}
if (currentDirectory === undefined) {
logger.warn('getFileManager called with null directory.. Please report this issue. continuing.');
}
let fileManagers = this.fileManagers;
if (options.pluginManager) {
fileManagers = [].concat(fileManagers).concat(options.pluginManager.getFileManagers());
}
for (let i = fileManagers.length - 1; i >= 0 ; i--) {
const fileManager = fileManagers[i];
if (fileManager[isSync ? 'supportsSync' : 'supports'](filename, currentDirectory, options, environment)) {
return fileManager;
}
}
return null;
}
addFileManager(fileManager) {
this.fileManagers.push(fileManager);
}
clearFileManagers() {
this.fileManagers = [];
}
}
export default Environment;
@@ -0,0 +1,77 @@
import type { Environment } from './environment-api'
export interface FileManager {
/**
* Given the full path to a file, return the path component
* Provided by AbstractFileManager
*/
getPath(filename: string): string
/**
* Append a .less extension if appropriate. Only called if less thinks one could be added.
* Provided by AbstractFileManager
*/
tryAppendLessExtension(filename: string): string
/**
* Whether the rootpath should be converted to be absolute.
* The browser ovverides this to return true because urls must be absolute.
* Provided by AbstractFileManager (returns false)
*/
alwaysMakePathsAbsolute(): boolean
/**
* Returns whether a path is absolute
* Provided by AbstractFileManager
*/
isPathAbsolute(path: string): boolean
/**
* joins together 2 paths
* Provided by AbstractFileManager
*/
join(basePath: string, laterPath: string): string
/**
* Returns the difference between 2 paths
* E.g. url = a/ baseUrl = a/b/ returns ../
* url = a/b/ baseUrl = a/ returns b/
* Provided by AbstractFileManager
*/
pathDiff(url: string, baseUrl: string): string
/**
* Returns whether this file manager supports this file for syncronous file retrieval
* If true is returned, loadFileSync will then be called with the file.
* Provided by AbstractFileManager (returns false)
*
* @todo - Narrow Options type
*/
supportsSync(
filename: string,
currentDirectory: string,
options: Record<string, any>,
environment: Environment
): boolean
/**
* If file manager supports async file retrieval for this file type
*/
supports(
filename: string,
currentDirectory: string,
options: Record<string, any>,
environment: Environment
): boolean
/**
* Loads a file asynchronously.
*/
loadFile(
filename: string,
currentDirectory: string,
options: Record<string, any>,
environment: Environment
): Promise<{ filename: string, contents: string }>
/**
* Loads a file synchronously. Expects an immediate return with an object
*/
loadFileSync(
filename: string,
currentDirectory: string,
options: Record<string, any>,
environment: Environment
): { error?: unknown, filename: string, contents: string }
}
+29
View File
@@ -0,0 +1,29 @@
import Anonymous from '../tree/anonymous.js';
import Keyword from '../tree/keyword.js';
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 };
+85
View File
@@ -0,0 +1,85 @@
import Color from '../tree/color.js';
// 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;
+452
View File
@@ -0,0 +1,452 @@
import Dimension from '../tree/dimension.js';
import Color from '../tree/color.js';
import Quoted from '../tree/quoted.js';
import Anonymous from '../tree/anonymous.js';
import Expression from '../tree/expression.js';
import Operation from '../tree/operation.js';
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;
+74
View File
@@ -0,0 +1,74 @@
import Quoted from '../tree/quoted.js';
import URL from '../tree/url.js';
import * as utils from '../utils.js';
import logger from '../logger.js';
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);
}};
};
+26
View File
@@ -0,0 +1,26 @@
import Keyword from '../tree/keyword.js';
import * as utils from '../utils.js';
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;
+55
View File
@@ -0,0 +1,55 @@
import Expression from '../tree/expression.js';
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;
+36
View File
@@ -0,0 +1,36 @@
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 );
+35
View File
@@ -0,0 +1,35 @@
import functionRegistry from './function-registry.js';
import functionCaller from './function-caller.js';
import boolean from './boolean.js';
import defaultFunc from './default.js';
import color from './color.js';
import colorBlending from './color-blending.js';
import dataUri from './data-uri.js';
import list from './list.js';
import math from './math.js';
import number from './number.js';
import string from './string.js';
import svg from './svg.js';
import types from './types.js';
import style from './style.js';
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);
functionRegistry.addMultiple(style);
return functions;
};
+158
View File
@@ -0,0 +1,158 @@
import Comment from '../tree/comment.js';
import Node from '../tree/node.js';
import Dimension from '../tree/dimension.js';
import Declaration from '../tree/declaration.js';
import Expression from '../tree/expression.js';
import Ruleset from '../tree/ruleset.js';
import Selector from '../tree/selector.js';
import Element from '../tree/element.js';
import Quote from '../tree/quoted.js';
import Value from '../tree/value.js';
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);
}
};
+22
View File
@@ -0,0 +1,22 @@
import Dimension from '../tree/dimension.js';
const MathHelper = (fn, unit, cssEvaluable, n) => {
if (!(n instanceof Dimension)) {
// A runtime CSS value such as var()/env() stays an unevaluated call and
// cannot be resolved to a number at compile time. Only functions with a
// CSS equivalent may be left for the browser; the rest (percentage, ceil,
// floor, round) have no CSS form and keep erroring on such input.
if (cssEvaluable && n && n.type === 'Call') {
return undefined;
}
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;
+30
View File
@@ -0,0 +1,30 @@
import mathHelper from './math-helper.js';
const mathFunctions = {
// name: [unit, has a CSS equivalent that the browser can evaluate]
ceil: [null, false],
floor: [null, false],
sqrt: [null, true],
abs: [null, true],
tan: ['', true],
sin: ['', true],
cos: ['', true],
atan: ['rad', true],
asin: ['rad', true],
acos: ['rad', true]
};
for (const f in mathFunctions) {
// eslint-disable-next-line no-prototype-builtins
if (mathFunctions.hasOwnProperty(f)) {
const [unit, cssEvaluable] = mathFunctions[f];
mathFunctions[f] = mathHelper.bind(null, Math[f], unit, cssEvaluable);
}
}
mathFunctions.round = (n, f) => {
const fraction = typeof f === 'undefined' ? 0 : f.value;
return mathHelper(num => num.toFixed(fraction), null, false, n);
};
export default mathFunctions;
+95
View File
@@ -0,0 +1,95 @@
import Dimension from '../tree/dimension.js';
import Anonymous from '../tree/anonymous.js';
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;
} else {
throw { type: 'Argument', message: 'incompatible types' };
}
}
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, '%', false, n);
return result;
}
};
+36
View File
@@ -0,0 +1,36 @@
import Quoted from '../tree/quoted.js';
import Anonymous from '../tree/anonymous.js';
import JavaScript from '../tree/javascript.js';
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);
}
};
+29
View File
@@ -0,0 +1,29 @@
import Variable from '../tree/variable.js';
import Anonymous from '../tree/anonymous.js';
/** @param {*[]} args */
const styleExpression = function (args) {
args = Array.prototype.slice.call(args);
if (args.length === 0) {
throw { type: 'Argument', message: 'one or more arguments required' };
}
const entityList = [new Variable(args[0].value, this.index, this.currentFileInfo).eval(this.context)];
const result = entityList.map(a => { return a.toCSS(this.context); }).join(this.context.compress ? ',' : ', ');
return new Anonymous(`style(${result})`);
};
export default {
/** @param {...*} args */
style: function(...args) {
try {
return styleExpression.call(this, args);
} catch (e) {
// When style() is used as a CSS function (e.g. @container style(--responsive: true)),
// arguments won't be valid Less variables. Return undefined to let the
// parser fall through and treat it as plain CSS.
}
},
};
+87
View File
@@ -0,0 +1,87 @@
import Dimension from '../tree/dimension.js';
import Color from '../tree/color.js';
import Expression from '../tree/expression.js';
import Quoted from '../tree/quoted.js';
import URL from '../tree/url.js';
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);
}};
};
+70
View File
@@ -0,0 +1,70 @@
import Keyword from '../tree/keyword.js';
import DetachedRuleset from '../tree/detached-ruleset.js';
import Dimension from '../tree/dimension.js';
import Color from '../tree/color.js';
import Quoted from '../tree/quoted.js';
import Anonymous from '../tree/anonymous.js';
import URL from '../tree/url.js';
import Operation from '../tree/operation.js';
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);
}
};
+183
View File
@@ -0,0 +1,183 @@
import contexts from './contexts.js';
import Parser from './parser/parser.js';
import LessError from './less-error.js';
import * as utils from './utils.js';
import logger from './logger.js';
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;
}
+95
View File
@@ -0,0 +1,95 @@
import Environment from './environment/environment.js';
import data from './data/index.js';
import tree from './tree/index.js';
import AbstractFileManager from './environment/abstract-file-manager.js';
import AbstractPluginLoader from './environment/abstract-plugin-loader.js';
import visitors from './visitors/index.js';
import Parser from './parser/parser.js';
import functions from './functions/index.js';
import contexts from './contexts.js';
import LessError from './less-error.js';
import transformTree from './transform-tree.js';
import * as utils from './utils.js';
import PluginManager from './plugin-manager.js';
import logger from './logger.js';
import SourceMapOutput from './source-map-output.js';
import SourceMapBuilder from './source-map-builder.js';
import ParseTree from './parse-tree.js';
import ImportManager from './import-manager.js';
import Parse from './parse.js';
import Render from './render.js';
import parseVersion from 'parse-node-version';
export default function(environment, fileManagers, version = '0.0.0') {
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(...args) {
return new t(...args);
};
};
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;
}
+166
View File
@@ -0,0 +1,166 @@
import * as utils from './utils.js';
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;
// Set type early so it's always available, even if fileContentMap is missing
this.type = e.type || 'Syntax';
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.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 || {};
const isWarning = (this.type ?? '').toLowerCase().includes('warning');
const type = isWarning ? this.type : `${this.type}Error`;
const color = isWarning ? 'yellow' : 'red';
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 (!isWarning && 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].slice(this.column, this.column + 1), 'bold') +
extract[1].slice(this.column + 1), 'red'), 'inverse');
}
error.push(errorTxt);
}
if (!isWarning && typeof extract[2] === 'string') {
error.push(stylize(`${this.line + 1} ${extract[2]}`, 'grey'));
}
error = `${error.join('\n') + stylize('', 'reset')}\n`;
}
message += stylize(`${type}: ${this.message}`, color);
if (this.filename) {
message += stylize(' in ', color) + 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 ', color) + (this.filename || '')}/n`;
message += `${stylize(this.callLine, 'grey')} ${this.callExtract}/n`;
}
return message;
};
export default LessError;
+34
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: []
};
+124
View File
@@ -0,0 +1,124 @@
import LessError from './less-error.js';
import transformTree from './transform-tree.js';
import logger from './logger.js';
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,
// @deprecated The dumpLineNumbers option is deprecated. Use sourcemaps instead. All modes will be removed in a future version.
dumpLineNumbers: options.dumpLineNumbers,
strictUnits: Boolean(options.strictUnits),
numPrecision: 8};
if (options.sourceMap) {
// Normalize sourceMap option: if it's just true, convert to object
if (options.sourceMap === true) {
options.sourceMap = {};
}
const sourceMapOpts = options.sourceMap;
// Set sourceMapInputFilename if not set and filename is available
if (!sourceMapOpts.sourceMapInputFilename && options.filename) {
sourceMapOpts.sourceMapInputFilename = options.filename;
}
// Default sourceMapBasepath to the input file's directory if not set
// This matches the behavior documented and implemented in bin/lessc
if (sourceMapOpts.sourceMapBasepath === undefined && options.filename) {
// Get directory from filename using string manipulation (works cross-platform)
const lastSlash = Math.max(options.filename.lastIndexOf('/'), options.filename.lastIndexOf('\\'));
if (lastSlash >= 0) {
sourceMapOpts.sourceMapBasepath = options.filename.substring(0, lastSlash);
} else {
// No directory separator found, use current directory
sourceMapOpts.sourceMapBasepath = '.';
}
}
// Handle sourceMapFullFilename (CLI-specific: --source-map=filename)
// This is converted to sourceMapFilename and sourceMapOutputFilename
if (sourceMapOpts.sourceMapFullFilename && !sourceMapOpts.sourceMapFileInline) {
// This case is handled by lessc before calling render
// We just need to ensure sourceMapFilename is set if sourceMapFullFilename is provided
if (!sourceMapOpts.sourceMapFilename && !sourceMapOpts.sourceMapURL) {
// Extract just the basename for the sourceMappingURL comment
const mapBase = sourceMapOpts.sourceMapFullFilename.split(/[/\\]/).pop();
sourceMapOpts.sourceMapFilename = mapBase;
}
} else if (!sourceMapOpts.sourceMapFilename && !sourceMapOpts.sourceMapURL) {
// If sourceMapFilename is not set and sourceMapURL is not set,
// derive it from the output filename (if available) or input filename
if (sourceMapOpts.sourceMapOutputFilename) {
// Use output filename + .map
sourceMapOpts.sourceMapFilename = sourceMapOpts.sourceMapOutputFilename + '.map';
} else if (options.filename) {
// Fallback to input filename + .css.map (basename only)
const inputBasename = options.filename.split(/[/\\]/).pop().replace(/\.[^/.]+$/, '');
sourceMapOpts.sourceMapFilename = inputBasename + '.css.map';
}
}
// Default sourceMapOutputFilename if not set
if (!sourceMapOpts.sourceMapOutputFilename) {
if (options.filename) {
const inputBasename = options.filename.split(/[/\\]/).pop().replace(/\.[^/.]+$/, '');
sourceMapOpts.sourceMapOutputFilename = inputBasename + '.css';
} else {
sourceMapOpts.sourceMapOutputFilename = 'output.css';
}
}
sourceMapBuilder = new SourceMapBuilder(sourceMapOpts);
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;
}
+87
View File
@@ -0,0 +1,87 @@
import contexts from './contexts.js';
import Parser from './parser/parser.js';
import PluginManager from './plugin-manager.js';
import LessError from './less-error.js';
import * as utils from './utils.js';
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;
}
+400
View File
@@ -0,0 +1,400 @@
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.slice(comment.index, parserInput.i);
parserInput.commentStore.push(comment);
continue;
} else if (nextChar === '*') {
const nextStarSlash = inp.indexOf('*/', parserInput.i + 2);
if (nextStarSlash >= 0) {
comment = {
index: parserInput.i,
text: inp.slice(parserInput.i, nextStarSlash + 2),
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.$peekChar = tok => {
if (input.charAt(parserInput.i) !== tok) {
return null;
}
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.slice(currentPosition, 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)
*
* @param {string|RegExp} tok - stop token
* @param {boolean} [detectBareVar] - when set, also record the position of the
* first bare `@variable` reference (not `@{interpolation}`) that appears at
* PAREN depth 0 — i.e. a structural reference, not a declaration value inside
* `(...)`. Reuses this single pass (which already skips strings/comments) so
* callers don't re-scan the text. Exposed as `.bareVarIndex` on the returned
* group array (or null). `[...]`/`{...}` do NOT shield a reference — only
* `(...)` (a declaration-value group) does.
*/
parserInput.$parseUntil = (tok, detectBareVar) => {
let quote = '';
let returnVal = null;
let inComment = false;
let blockDepth = 0;
let parenDepth = 0;
let bareVarIndex = null;
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.slice(lastPos, i);
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;
}
if (detectBareVar && bareVarIndex === null && nextChar === '@' && parenDepth === 0) {
// A bare `@ident` (not `@{interpolation}`) outside any `(...)` —
// a structural reference. Strings/comments are already skipped above.
const after = input.charAt(i + 1);
if (after && /[-\w]/.test(after)) { bareVarIndex = i; }
}
switch (nextChar) {
case '\\':
i++;
nextChar = input.charAt(i);
parseGroups.push(input.slice(lastPos, i + 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.slice(lastPos, i), 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++;
parenDepth++;
break;
case '[':
blockStack.push(']');
blockDepth++;
break;
case '}':
case ')':
case ']': {
const expected = blockStack.pop();
if (nextChar === expected) {
blockDepth--;
if (nextChar === ')' && parenDepth > 0) { parenDepth--; }
} 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);
if (Array.isArray(returnVal)) { returnVal.bareVarIndex = bareVarIndex; }
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) => {
input = str;
parserInput.i = j = currentPos = furthest = 0;
chunks = [str];
current = chunks[0];
skipWhitespace(0);
};
parserInput.end = () => {
let message;
const isFinished = parserInput.i >= input.length;
if (parserInput.i < furthest) {
message = furthestPossibleErrorMessage;
parserInput.i = furthest;
}
return {
isFinished,
furthest: parserInput.i,
furthestPossibleErrorMessage: message,
furthestReachedEnd: parserInput.i >= input.length - 1,
furthestChar: input[parserInput.i]
};
};
return parserInput;
};
File diff suppressed because it is too large Load Diff
+168
View File
@@ -0,0 +1,168 @@
/**
* 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;
+41
View File
@@ -0,0 +1,41 @@
import * as utils from './utils.js';
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;
}
+82
View File
@@ -0,0 +1,82 @@
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;
}
+151
View File
@@ -0,0 +1,151 @@
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 ? options.outputFilename.replace(/\\/g, '/') : 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;
}
+102
View File
@@ -0,0 +1,102 @@
import contexts from './contexts.js';
import visitor from './visitors/index.js';
import tree from './tree/index.js';
/**
* @param {import('./tree/node.js').default} root
* @param {{ variables?: Record<string, *>, compress?: boolean, pluginManager?: *, frames?: *[] }} options
* @returns {import('./tree/node.js').default}
*/
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;
}
+57
View File
@@ -0,0 +1,57 @@
// @ts-check
/** @import { EvalContext, CSSOutput, FileInfo, VisibilityInfo } from './node.js' */
import Node from './node.js';
class Anonymous extends Node {
get type() { return 'Anonymous'; }
/**
* @param {string | null} value
* @param {number} [index]
* @param {FileInfo} [currentFileInfo]
* @param {boolean} [mapLines]
* @param {boolean} [rulesetLike]
* @param {VisibilityInfo} [visibilityInfo]
*/
constructor(value, index, currentFileInfo, mapLines, rulesetLike, visibilityInfo) {
super();
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);
}
/** @returns {Anonymous} */
eval() {
return new Anonymous(/** @type {string | null} */ (this.value), this._index, this._fileInfo, this.mapLines, this.rulesetLike, this.visibilityInfo());
}
/**
* @param {Node} other
* @returns {number | undefined}
*/
compare(other) {
return other.toCSS && this.toCSS(/** @type {EvalContext} */ ({})) === other.toCSS(/** @type {EvalContext} */ ({})) ? 0 : undefined;
}
/** @returns {boolean} */
isRulesetLike() {
return this.rulesetLike;
}
/**
* @param {EvalContext} context
* @param {CSSOutput} output
*/
genCSS(context, output) {
this.nodeVisible = Boolean(this.value);
if (this.nodeVisible) {
output.add(/** @type {string} */ (this.value), this._fileInfo, this._index, this.mapLines);
}
}
}
export default Anonymous;
+48
View File
@@ -0,0 +1,48 @@
// @ts-check
/** @import { EvalContext, CSSOutput, TreeVisitor } from './node.js' */
import Node from './node.js';
class Assignment extends Node {
get type() { return 'Assignment'; }
/**
* @param {string} key
* @param {Node} val
*/
constructor(key, val) {
super();
this.key = key;
this.value = val;
}
/** @param {TreeVisitor} visitor */
accept(visitor) {
this.value = visitor.visit(/** @type {Node} */ (this.value));
}
/**
* @param {EvalContext} context
* @returns {Assignment}
*/
eval(context) {
if (/** @type {Node} */ (this.value).eval) {
return new Assignment(this.key, /** @type {Node} */ (this.value).eval(context));
}
return this;
}
/**
* @param {EvalContext} context
* @param {CSSOutput} output
*/
genCSS(context, output) {
output.add(`${this.key}=`);
if (/** @type {Node} */ (this.value).genCSS) {
/** @type {Node} */ (this.value).genCSS(context, output);
} else {
output.add(/** @type {string} */ (/** @type {unknown} */ (this.value)));
}
}
}
export default Assignment;
+8
View File
@@ -0,0 +1,8 @@
// @ts-check
export const MediaSyntaxOptions = {
queryInParens: true
};
export const ContainerSyntaxOptions = {
queryInParens: true
};
+340
View File
@@ -0,0 +1,340 @@
// @ts-check
/** @import { EvalContext, CSSOutput, TreeVisitor, FileInfo, VisibilityInfo } from './node.js' */
/** @import { FunctionRegistry } from './nested-at-rule.js' */
import Node from './node.js';
import Selector from './selector.js';
import Ruleset from './ruleset.js';
import Anonymous from './anonymous.js';
import NestableAtRulePrototype from './nested-at-rule.js';
import mergeRules from './merge-rules.js';
/**
* @typedef {Node & {
* rules?: Node[],
* selectors?: Selector[],
* root?: boolean,
* allowImports?: boolean,
* functionRegistry?: FunctionRegistry,
* merge?: boolean,
* debugInfo?: { lineNumber: number, fileName: string },
* elements?: import('./element.js').default[]
* }} RulesetLikeNode
*/
class AtRule extends Node {
get type() { return 'AtRule'; }
/**
* @param {string} [name]
* @param {Node | string} [value]
* @param {Node[] | Ruleset} [rules]
* @param {number} [index]
* @param {FileInfo} [currentFileInfo]
* @param {{ lineNumber: number, fileName: string }} [debugInfo]
* @param {boolean} [isRooted]
* @param {VisibilityInfo} [visibilityInfo]
*/
constructor(
name,
value,
rules,
index,
currentFileInfo,
debugInfo,
isRooted,
visibilityInfo
) {
super();
let i;
var selectors = (new Selector([], null, null, index, currentFileInfo)).createEmptySelectors();
/** @type {string | undefined} */
this.name = name;
this.value = (value instanceof Node) ? value : (value ? new Anonymous(value) : value);
/** @type {boolean | undefined} */
this.simpleBlock = undefined;
/** @type {RulesetLikeNode[] | undefined} */
this.declarations = undefined;
/** @type {RulesetLikeNode[] | undefined} */
this.rules = undefined;
if (rules) {
if (Array.isArray(rules)) {
const allDeclarations = this.declarationsBlock(rules);
let allRulesetDeclarations = true;
rules.forEach(rule => {
if (rule.type === 'Ruleset' && /** @type {RulesetLikeNode} */ (rule).rules) allRulesetDeclarations = allRulesetDeclarations && this.declarationsBlock(/** @type {Node[]} */ (/** @type {RulesetLikeNode} */ (rule).rules), true);
});
if (allDeclarations && !isRooted) {
this.simpleBlock = true;
this.declarations = rules;
} else if (allRulesetDeclarations && rules.length === 1 && !isRooted && !value) {
this.simpleBlock = true;
this.declarations = /** @type {RulesetLikeNode} */ (rules[0]).rules ? /** @type {RulesetLikeNode} */ (rules[0]).rules : rules;
} else {
this.rules = rules;
}
} else {
const allDeclarations = this.declarationsBlock(/** @type {Node[]} */ (rules.rules));
if (allDeclarations && !isRooted && !value) {
this.simpleBlock = true;
this.declarations = rules.rules;
} else {
this.rules = [rules];
/** @type {RulesetLikeNode} */ (this.rules[0]).selectors = (new Selector([], null, null, index, currentFileInfo)).createEmptySelectors();
}
}
if (!this.simpleBlock) {
for (i = 0; i < this.rules.length; i++) {
/** @type {RulesetLikeNode} */ (this.rules[i]).allowImports = true;
}
}
if (this.declarations) {
this.setParent(this.declarations, /** @type {Node} */ (/** @type {unknown} */ (this)));
}
if (this.rules) {
this.setParent(this.rules, /** @type {Node} */ (/** @type {unknown} */ (this)));
}
}
this._index = index;
this._fileInfo = currentFileInfo;
/** @type {{ lineNumber: number, fileName: string } | undefined} */
this.debugInfo = debugInfo;
/** @type {boolean} */
this.isRooted = isRooted || false;
this.copyVisibilityInfo(visibilityInfo);
this.allowRoot = true;
}
/**
* @param {Node[]} rules
* @param {boolean} [mergeable]
* @returns {boolean}
*/
declarationsBlock(rules, mergeable = false) {
if (!mergeable) {
return rules.filter(function (/** @type {Node & { merge?: boolean }} */ node) { return (node.type === 'Declaration' || node.type === 'Comment') && !node.merge}).length === rules.length;
} else {
return rules.filter(function (/** @type {Node} */ node) { return (node.type === 'Declaration' || node.type === 'Comment'); }).length === rules.length;
}
}
/**
* @param {Node[]} rules
* @returns {boolean}
*/
keywordList(rules) {
if (!Array.isArray(rules)) {
return false;
} else {
return rules.filter(function (/** @type {Node} */ node) { return (node.type === 'Keyword' || node.type === 'Comment'); }).length === rules.length;
}
}
/** @param {TreeVisitor} visitor */
accept(visitor) {
const value = this.value, rules = this.rules, declarations = this.declarations;
if (rules) {
this.rules = visitor.visitArray(rules);
} else if (declarations) {
this.declarations = visitor.visitArray(declarations);
}
if (value) {
this.value = visitor.visit(/** @type {Node} */ (value));
}
}
/** @override @returns {boolean} */
isRulesetLike() {
return /** @type {boolean} */ (/** @type {unknown} */ (this.rules || !this.isCharset()));
}
isCharset() {
return '@charset' === this.name;
}
/**
* @param {EvalContext} context
* @param {CSSOutput} output
*/
genCSS(context, output) {
const value = this.value, rules = this.rules || this.declarations;
output.add(/** @type {string} */ (this.name), this.fileInfo(), this.getIndex());
if (value) {
output.add(' ');
/** @type {Node} */ (value).genCSS(context, output);
}
if (this.simpleBlock) {
this.outputRuleset(context, output, /** @type {Node[]} */ (this.declarations));
} else if (rules) {
this.outputRuleset(context, output, rules);
} else {
output.add(';');
}
}
/**
* @param {EvalContext} context
* @returns {Node}
*/
eval(context) {
let mediaPathBackup, mediaBlocksBackup, value = this.value, rules = this.rules || this.declarations;
// 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 = /** @type {Node} */ (value).eval(context);
}
if (rules) {
rules = this.evalRoot(context, rules);
}
if (Array.isArray(rules) && /** @type {RulesetLikeNode} */ (rules[0]).rules && Array.isArray(/** @type {RulesetLikeNode} */ (rules[0]).rules) && /** @type {Node[]} */ (/** @type {RulesetLikeNode} */ (rules[0]).rules).length) {
const allMergeableDeclarations = this.declarationsBlock(/** @type {Node[]} */ (/** @type {RulesetLikeNode} */ (rules[0]).rules), true);
if (allMergeableDeclarations && !this.isRooted && !value) {
mergeRules(/** @type {Node[]} */ (/** @type {RulesetLikeNode} */ (rules[0]).rules));
rules = /** @type {RulesetLikeNode[]} */ (/** @type {RulesetLikeNode} */ (rules[0]).rules);
rules.forEach(/** @param {RulesetLikeNode} rule */ rule => { rule.merge = false; });
}
}
if (this.simpleBlock && rules) {
/** @type {RulesetLikeNode} */ (rules[0]).functionRegistry = /** @type {RulesetLikeNode} */ (context.frames[0]).functionRegistry.inherit();
rules = rules.map(function (/** @type {Node} */ rule) { return rule.eval(context); });
}
// restore media bubbling information
context.mediaPath = mediaPathBackup;
context.mediaBlocks = mediaBlocksBackup;
return /** @type {Node} */ (/** @type {unknown} */ (new AtRule(this.name, value, rules, this.getIndex(), this.fileInfo(), this.debugInfo, this.isRooted, this.visibilityInfo())));
}
/**
* @param {EvalContext} context
* @param {Node[]} rules
* @returns {Node[]}
*/
evalRoot(context, rules) {
let ampersandCount = 0;
let noAmpersandCount = 0;
let noAmpersands = true;
if (!this.simpleBlock) {
rules = [rules[0].eval(context)];
}
/** @type {Selector[]} */
let precedingSelectors = [];
if (context.frames.length > 0) {
for (let index = 0; index < context.frames.length; index++) {
const frame = /** @type {RulesetLikeNode} */ (context.frames[index]);
if (
frame.type === 'Ruleset' &&
frame.rules &&
frame.rules.length > 0
) {
if (frame && !frame.root && frame.selectors && frame.selectors.length > 0) {
precedingSelectors = precedingSelectors.concat(frame.selectors);
}
}
if (precedingSelectors.length > 0) {
const allAmpersandElements = precedingSelectors.every(
sel => sel.elements && sel.elements.length > 0 && sel.elements.every(
/** @param {import('./element.js').default} el */
el => el.value === '&'
)
);
if (allAmpersandElements) {
noAmpersands = false;
noAmpersandCount++;
} else {
ampersandCount++;
}
}
}
}
const mixedAmpersands = ampersandCount > 0 && noAmpersandCount > 0 && !noAmpersands;
if (
(this.isRooted && ampersandCount > 0 && noAmpersandCount === 0 && noAmpersands)
|| !mixedAmpersands
) {
/** @type {RulesetLikeNode} */ (rules[0]).root = true;
}
return rules;
}
/** @param {string} name */
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]);
}
}
/**
* @param {EvalContext} context
* @param {CSSOutput} output
* @param {Node[]} rules
*/
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--;
}
}
// Apply shared methods from NestableAtRulePrototype that AtRule doesn't override
const { evalFunction, evalTop, evalNested, permute, bubbleSelectors } = NestableAtRulePrototype;
Object.assign(AtRule.prototype, { evalFunction, evalTop, evalNested, permute, bubbleSelectors });
export default AtRule;
+63
View File
@@ -0,0 +1,63 @@
// @ts-check
/** @import { EvalContext, CSSOutput } from './node.js' */
import Node from './node.js';
class Attribute extends Node {
get type() { return 'Attribute'; }
/**
* @param {string | Node} key
* @param {string} op
* @param {string | Node} value
* @param {string} cif
*/
constructor(key, op, value, cif) {
super();
this.key = key;
this.op = op;
this.value = value;
this.cif = cif;
}
/**
* @param {EvalContext} context
* @returns {Attribute}
*/
eval(context) {
return new Attribute(
/** @type {Node} */ (this.key).eval ? /** @type {Node} */ (this.key).eval(context) : /** @type {string} */ (this.key),
this.op,
(this.value && /** @type {Node} */ (this.value).eval) ? /** @type {Node} */ (this.value).eval(context) : this.value,
this.cif
);
}
/**
* @param {EvalContext} context
* @param {CSSOutput} output
*/
genCSS(context, output) {
output.add(this.toCSS(context));
}
/**
* @param {EvalContext} context
* @returns {string}
*/
toCSS(context) {
let value = /** @type {Node} */ (this.key).toCSS ? /** @type {Node} */ (this.key).toCSS(context) : /** @type {string} */ (this.key);
if (this.op) {
value += this.op;
value += (/** @type {Node} */ (this.value).toCSS ? /** @type {Node} */ (this.value).toCSS(context) : /** @type {string} */ (this.value));
}
if (this.cif) {
value = value + ' ' + this.cif;
}
return `[${value}]`;
}
}
export default Attribute;
+132
View File
@@ -0,0 +1,132 @@
// @ts-check
/** @import { EvalContext, CSSOutput, TreeVisitor, FileInfo } from './node.js' */
import Node from './node.js';
import Anonymous from './anonymous.js';
import FunctionCaller from '../functions/function-caller.js';
//
// A function call node.
//
class Call extends Node {
get type() { return 'Call'; }
/**
* @param {string} name
* @param {Node[]} args
* @param {number} index
* @param {FileInfo} currentFileInfo
*/
constructor(name, args, index, currentFileInfo) {
super();
this.name = name;
this.args = args;
this.calc = name === 'calc';
this._index = index;
this._fileInfo = currentFileInfo;
}
/** @param {TreeVisitor} visitor */
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.
//
/**
* @param {EvalContext} context
* @returns {Node}
*/
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;
};
/** @type {Node | string | boolean | null | undefined} */
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 (/** @type {Record<string, unknown>} */ (e).hasOwnProperty('line') && /** @type {Record<string, unknown>} */ (e).hasOwnProperty('column')) {
throw e;
}
throw {
type: /** @type {Record<string, string>} */ (e).type || 'Runtime',
message: `Error evaluating function \`${this.name}\`${/** @type {Error} */ (e).message ? `: ${/** @type {Error} */ (e).message}` : ''}`,
index: this.getIndex(),
filename: this.fileInfo().filename,
line: /** @type {Record<string, number>} */ (e).lineNumber,
column: /** @type {Record<string, number>} */ (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());
}
/**
* @param {EvalContext} context
* @param {CSSOutput} output
*/
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;
+285
View File
@@ -0,0 +1,285 @@
// @ts-check
import Node from './node.js';
import colors from '../data/colors.js';
/** @import { EvalContext, CSSOutput } from './node.js' */
//
// RGB Colors - #ff0014, #eee
//
class Color extends Node {
get type() { return 'Color'; }
/**
* @param {number[] | string} rgb
* @param {number} [a]
* @param {string} [originalForm]
*/
constructor(rgb, a, originalForm) {
super();
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)) {
/** @type {number[]} */
this.rgb = rgb;
} else if (rgb.length >= 6) {
/** @type {number[]} */
this.rgb = [];
/** @type {RegExpMatchArray} */ (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 {
/** @type {number[]} */
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;
}
});
}
/** @type {number} */
if (typeof this.alpha === 'undefined') {
this.alpha = (typeof a === 'number') ? a : 1;
}
if (typeof originalForm !== 'undefined') {
this.value = originalForm;
}
}
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;
}
/**
* @param {EvalContext} context
* @param {CSSOutput} output
*/
genCSS(context, output) {
output.add(this.toCSS(context));
}
/**
* @param {EvalContext} context
* @param {boolean} [doNotCompress]
* @returns {string}
*/
toCSS(context, doNotCompress) {
const compress = context && context.compress && !doNotCompress;
let color;
let alpha;
/** @type {string | undefined} */
let colorFunction;
/** @type {(string | number)[]} */
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 (/** @type {string} */ (this.value).indexOf('rgb') === 0) {
if (alpha < 1) {
colorFunction = 'rgba';
}
} else if (/** @type {string} */ (this.value).indexOf('hsl') === 0) {
if (alpha < 1) {
colorFunction = 'hsla';
} else {
colorFunction = 'hsl';
}
} else {
return /** @type {string} */ (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.
//
/**
* @param {EvalContext} context
* @param {string} op
* @param {Color} other
*/
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);
/** @type {number} */
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;
}
/** @type {number} */ (h) /= 6;
}
return { h: /** @type {number} */ (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);
/** @type {number} */
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;
}
/** @type {number} */ (h) /= 6;
}
return { h: /** @type {number} */ (h) * 360, s, v, a };
}
toARGB() {
return toHex([this.alpha * 255].concat(this.rgb));
}
/**
* @param {Node & { rgb?: number[], alpha?: number }} x
* @returns {0 | undefined}
*/
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;
}
/** @param {string} keyword */
static fromKeyword(keyword) {
/** @type {Color | undefined} */
let c;
const key = keyword.toLowerCase();
// eslint-disable-next-line no-prototype-builtins
if (colors.hasOwnProperty(key)) {
c = new Color(/** @type {string} */ (colors[/** @type {keyof typeof colors} */ (key)]).slice(1));
}
else if (key === 'transparent') {
c = new Color([0, 0, 0], 0);
}
if (c) {
c.value = keyword;
return c;
}
}
}
/**
* @param {number} v
* @param {number} max
*/
function clamp(v, max) {
return Math.min(Math.max(v, 0), max);
}
/** @param {number[]} v */
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;
+38
View File
@@ -0,0 +1,38 @@
// @ts-check
import Node from './node.js';
/** @import { EvalContext, CSSOutput } from './node.js' */
/** @type {Record<string, boolean>} */
const _noSpaceCombinators = {
'': true,
' ': true,
'|': true
};
class Combinator extends Node {
get type() { return 'Combinator'; }
/** @param {string} value */
constructor(value) {
super();
if (value === ' ') {
this.value = ' ';
this.emptyOrWhitespace = true;
} else {
this.value = value ? value.trim() : '';
this.emptyOrWhitespace = this.value === '';
}
}
/**
* @param {EvalContext} context
* @param {CSSOutput} output
*/
genCSS(context, output) {
const spaceOrEmpty = (context.compress || _noSpaceCombinators[/** @type {string} */ (this.value)]) ? '' : ' ';
output.add(spaceOrEmpty + this.value + spaceOrEmpty);
}
}
export default Combinator;
+48
View File
@@ -0,0 +1,48 @@
// @ts-check
/** @import { EvalContext, CSSOutput, FileInfo } from './node.js' */
/** @import { DebugInfoContext } from './debug-info.js' */
import Node from './node.js';
import getDebugInfo from './debug-info.js';
class Comment extends Node {
get type() { return 'Comment'; }
/**
* @param {string} value
* @param {boolean} isLineComment
* @param {number} index
* @param {FileInfo} currentFileInfo
*/
constructor(value, isLineComment, index, currentFileInfo) {
super();
this.value = value;
this.isLineComment = isLineComment;
this._index = index;
this._fileInfo = currentFileInfo;
this.allowRoot = true;
/** @type {{ lineNumber: number, fileName: string } | undefined} */
this.debugInfo = undefined;
}
/**
* @param {EvalContext} context
* @param {CSSOutput} output
*/
genCSS(context, output) {
if (this.debugInfo) {
output.add(getDebugInfo(context, /** @type {DebugInfoContext} */ (this)), this.fileInfo(), this.getIndex());
}
output.add(/** @type {string} */ (this.value));
}
/**
* @param {EvalContext} context
* @returns {boolean}
*/
isSilent(context) {
const isCompressed = context.compress && /** @type {string} */ (this.value)[2] !== '!';
return this.isLineComment || isCompressed;
}
}
export default Comment;
+65
View File
@@ -0,0 +1,65 @@
// @ts-check
/** @import { EvalContext, TreeVisitor } from './node.js' */
import Node from './node.js';
class Condition extends Node {
get type() { return 'Condition'; }
/**
* @param {string} op
* @param {Node} l
* @param {Node} r
* @param {number} i
* @param {boolean} negate
*/
constructor(op, l, r, i, negate) {
super();
this.op = op.trim();
this.lvalue = l;
this.rvalue = r;
this._index = i;
this.negate = negate;
}
/** @param {TreeVisitor} visitor */
accept(visitor) {
this.lvalue = visitor.visit(this.lvalue);
this.rvalue = visitor.visit(this.rvalue);
}
/**
* @param {EvalContext} context
* @returns {boolean}
* @suppress {checkTypes}
*/
// @ts-ignore - Condition.eval returns boolean, not Node (used as guard condition)
eval(context) {
const a = this.lvalue.eval(context);
const b = this.rvalue.eval(context);
/** @type {boolean} */
let result;
switch (this.op) {
case 'and': result = Boolean(a && b); break;
case 'or': result = Boolean(a || b); break;
default:
switch (Node.compare(a, b)) {
case -1:
result = this.op === '<' || this.op === '=<' || this.op === '<=';
break;
case 0:
result = this.op === '=' || this.op === '>=' || this.op === '=<' || this.op === '<=';
break;
case 1:
result = this.op === '>' || this.op === '>=';
break;
default:
result = false;
}
}
return this.negate ? !result : result;
}
}
export default Condition;
+104
View File
@@ -0,0 +1,104 @@
// @ts-check
/** @import { EvalContext, CSSOutput, FileInfo, VisibilityInfo } from './node.js' */
/** @import { FunctionRegistry, NestableAtRuleThis } from './nested-at-rule.js' */
import Node from './node.js';
import Ruleset from './ruleset.js';
import Value from './value.js';
import Selector from './selector.js';
import AtRule from './atrule.js';
import NestableAtRulePrototype from './nested-at-rule.js';
/**
* @typedef {Ruleset & {
* allowImports?: boolean,
* debugInfo?: { lineNumber: number, fileName: string },
* functionRegistry?: FunctionRegistry
* }} RulesetWithExtras
*/
class Container extends AtRule {
get type() { return 'Container'; }
/**
* @param {Node[] | null} value
* @param {Node[]} features
* @param {number} index
* @param {FileInfo} currentFileInfo
* @param {VisibilityInfo} visibilityInfo
*/
constructor(value, features, index, currentFileInfo, visibilityInfo) {
super();
this._index = index;
this._fileInfo = currentFileInfo;
const selectors = (new Selector([], null, null, this._index, this._fileInfo)).createEmptySelectors();
/** @type {Value} */
this.features = new Value(features);
/** @type {RulesetWithExtras[]} */
this.rules = [new Ruleset(selectors, value)];
this.rules[0].allowImports = true;
this.copyVisibilityInfo(visibilityInfo);
this.allowRoot = true;
this.setParent(selectors, /** @type {Node} */ (/** @type {unknown} */ (this)));
this.setParent(this.features, /** @type {Node} */ (/** @type {unknown} */ (this)));
this.setParent(this.rules, /** @type {Node} */ (/** @type {unknown} */ (this)));
/** @type {boolean | undefined} */
this._evaluated = undefined;
}
/**
* @param {EvalContext} context
* @param {CSSOutput} output
*/
genCSS(context, output) {
output.add('@container ', this._fileInfo, this._index);
this.features.genCSS(context, output);
this.outputRuleset(context, output, this.rules);
}
/**
* @param {EvalContext} context
* @returns {Node}
*/
eval(context) {
if (this._evaluated) {
return /** @type {Node} */ (/** @type {unknown} */ (this));
}
if (!context.mediaBlocks) {
context.mediaBlocks = [];
context.mediaPath = [];
}
const media = new Container(null, [], this._index, this._fileInfo, this.visibilityInfo());
media._evaluated = true;
if (this.debugInfo) {
this.rules[0].debugInfo = this.debugInfo;
media.debugInfo = this.debugInfo;
}
media.features = /** @type {Value} */ (this.features.eval(context));
context.mediaPath.push(/** @type {Node} */ (/** @type {unknown} */ (media)));
context.mediaBlocks.push(/** @type {Node} */ (/** @type {unknown} */ (media)));
const fr = /** @type {RulesetWithExtras} */ (context.frames[0]).functionRegistry;
if (fr) {
this.rules[0].functionRegistry = fr.inherit();
}
context.frames.unshift(this.rules[0]);
media.rules = [/** @type {RulesetWithExtras} */ (this.rules[0].eval(context))];
context.frames.shift();
context.mediaPath.pop();
return context.mediaPath.length === 0 ? /** @type {NestableAtRuleThis} */ (/** @type {unknown} */ (media)).evalTop(context) :
/** @type {NestableAtRuleThis} */ (/** @type {unknown} */ (media)).evalNested(context);
}
}
// Apply NestableAtRulePrototype methods (accept, isRulesetLike override AtRule's versions)
Object.assign(Container.prototype, NestableAtRulePrototype);
export default Container;
+80
View File
@@ -0,0 +1,80 @@
// @ts-check
/**
* @typedef {object} DebugInfoData
* @property {number} lineNumber
* @property {string} fileName
*/
/**
* @typedef {object} DebugInfoContext
* @property {DebugInfoData} debugInfo
*/
/**
* @deprecated The dumpLineNumbers option is deprecated. Use sourcemaps instead.
* This will be removed in a future version.
*
* @param {DebugInfoContext} ctx - Context object with debugInfo
* @returns {string} Debug info as CSS comment
*/
function asComment(ctx) {
return `/* line ${ctx.debugInfo.lineNumber}, ${ctx.debugInfo.fileName} */\n`;
}
/**
* @deprecated The dumpLineNumbers option is deprecated. Use sourcemaps instead.
* This function generates Sass-compatible debug info using @media -sass-debug-info syntax.
* This format had short-lived usage and is no longer recommended.
* This will be removed in a future version.
*
* @param {DebugInfoContext} ctx - Context object with debugInfo
* @returns {string} Sass-compatible debug info as @media query
*/
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`;
}
/**
* Generates debug information (line numbers) for CSS output.
*
* @param {{ dumpLineNumbers?: string, compress?: boolean }} context - Context object with dumpLineNumbers option
* @param {DebugInfoContext} ctx - Context object with debugInfo
* @param {string} [lineSeparator] - Separator between comment and media query (for 'all' mode)
* @returns {string} Debug info string
*
* @deprecated The dumpLineNumbers option is deprecated. Use sourcemaps instead.
* All modes ('comments', 'mediaquery', 'all') are deprecated and will be removed in a future version.
* The 'mediaquery' and 'all' modes generate Sass-compatible @media -sass-debug-info output
* which had short-lived usage and is no longer recommended.
*/
function debugInfo(context, ctx, lineSeparator) {
let result = '';
// Some nodes never receive a debugInfo during parsing (e.g. the implicit
// ruleset of a nested @supports/@document), so there is no line to emit.
if (context.dumpLineNumbers && !context.compress && ctx.debugInfo) {
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;
+139
View File
@@ -0,0 +1,139 @@
// @ts-check
/** @import { EvalContext, CSSOutput, FileInfo } from './node.js' */
import Node from './node.js';
import Value from './value.js';
import Keyword from './keyword.js';
import Anonymous from './anonymous.js';
import * as Constants from '../constants.js';
const MATH = Constants.Math;
/**
* @param {EvalContext} context
* @param {Node[]} name
* @returns {string}
*/
function evalName(context, name) {
let value = '';
let i;
const n = name.length;
/** @type {CSSOutput} */
const output = {add: function (s) {value += s;}, isEmpty: function() { return value === ''; }};
for (i = 0; i < n; i++) {
name[i].eval(context).genCSS(context, output);
}
return value;
}
class Declaration extends Node {
get type() { return 'Declaration'; }
/**
* @param {string | Node[]} name
* @param {Node | string | null} value
* @param {string} [important]
* @param {string} [merge]
* @param {number} [index]
* @param {FileInfo} [currentFileInfo]
* @param {boolean} [inline]
* @param {boolean} [variable]
*/
constructor(name, value, important, merge, index, currentFileInfo, inline, variable) {
super();
this.name = name;
this.value = (value instanceof Node) ? value : new Value([value ? new Anonymous(value) : null]);
this.important = important ? ` ${important.trim()}` : '';
/** @type {string | undefined} */
this.merge = merge;
this._index = index;
this._fileInfo = currentFileInfo;
/** @type {boolean} */
this.inline = inline || false;
/** @type {boolean} */
this.variable = (variable !== undefined) ? variable
: (typeof name === 'string' && name.charAt(0) === '@');
/** @type {boolean} */
this.allowRoot = true;
this.setParent(this.value, this);
}
/**
* @param {EvalContext} context
* @param {CSSOutput} output
*/
genCSS(context, output) {
output.add(/** @type {string} */ (this.name) + (context.compress ? ':' : ': '), this.fileInfo(), this.getIndex());
try {
/** @type {Node} */ (this.value).genCSS(context, output);
}
catch (e) {
const err = /** @type {{ index?: number, filename?: string }} */ (e);
err.index = this._index;
err.filename = this._fileInfo && this._fileInfo.filename;
throw e;
}
output.add(this.important + ((this.inline || (context.lastRule && context.compress)) ? '' : ';'), this._fileInfo, this._index);
}
/** @param {EvalContext} context */
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 = (/** @type {Node[]} */ (name).length === 1) && (/** @type {Node[]} */ (name)[0] instanceof Keyword) ?
/** @type {string} */ (/** @type {Node[]} */ (name)[0].value) : evalName(context, /** @type {Node[]} */ (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 = /** @type {Node} */ (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 && importantResult.important) {
important = importantResult.important;
}
return new Declaration(/** @type {string} */ (name),
evaldValue,
important,
this.merge,
this.getIndex(), this.fileInfo(), this.inline,
variable);
}
catch (e) {
const err = /** @type {{ index?: number, filename?: string }} */ (e);
if (typeof err.index !== 'number') {
err.index = this.getIndex();
err.filename = this.fileInfo().filename;
}
throw e;
}
finally {
if (mathBypass) {
context.math = prevMath;
}
}
}
makeImportant() {
return new Declaration(this.name,
/** @type {Node} */ (this.value),
'!important',
this.merge,
this.getIndex(), this.fileInfo(), this.inline);
}
}
export default Declaration;
+45
View File
@@ -0,0 +1,45 @@
// @ts-check
/** @import { EvalContext, TreeVisitor } from './node.js' */
import Node from './node.js';
import contexts from '../contexts.js';
import * as utils from '../utils.js';
class DetachedRuleset extends Node {
get type() { return 'DetachedRuleset'; }
/**
* @param {Node} ruleset
* @param {Node[]} [frames]
*/
constructor(ruleset, frames) {
super();
this.ruleset = ruleset;
this.frames = frames;
this.evalFirst = true;
this.setParent(this.ruleset, this);
}
/** @param {TreeVisitor} visitor */
accept(visitor) {
this.ruleset = visitor.visit(this.ruleset);
}
/**
* @param {EvalContext} context
* @returns {DetachedRuleset}
*/
eval(context) {
const frames = this.frames || utils.copyArray(context.frames);
return new DetachedRuleset(this.ruleset, frames);
}
/**
* @param {EvalContext} context
* @returns {Node}
*/
callEval(context) {
return this.ruleset.eval(this.frames ? new contexts.Eval(context, this.frames.concat(context.frames)) : context);
}
}
export default DetachedRuleset;
+215
View File
@@ -0,0 +1,215 @@
// @ts-check
/* eslint-disable no-prototype-builtins */
import Node from './node.js';
import unitConversions from '../data/unit-conversions.js';
import Unit from './unit.js';
import Color from './color.js';
/** @import { EvalContext, CSSOutput } from './node.js' */
//
// A number with a unit
//
class Dimension extends Node {
get type() { return 'Dimension'; }
/**
* @param {number | string} value
* @param {Unit | string} [unit]
*/
constructor(value, unit) {
super();
/** @type {number} */
this.value = parseFloat(/** @type {string} */ (value));
if (isNaN(this.value)) {
throw new Error('Dimension is not a number.');
}
/** @type {Unit} */
this.unit = (unit && unit instanceof Unit) ? unit :
new Unit(unit ? [/** @type {string} */ (unit)] : undefined);
this.setParent(this.unit, this);
}
/**
* @param {import('./node.js').TreeVisitor} visitor
*/
accept(visitor) {
this.unit = /** @type {Unit} */ (visitor.visit(this.unit));
}
// remove when Nodes have JSDoc types
// eslint-disable-next-line no-unused-vars
/** @param {EvalContext} context */
eval(context) {
return this;
}
toColor() {
const v = /** @type {number} */ (this.value);
return new Color([v, v, v]);
}
/**
* @param {EvalContext} context
* @param {CSSOutput} output
*/
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, /** @type {number} */ (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).slice(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`.
/**
* @param {EvalContext} context
* @param {string} op
* @param {Dimension} other
*/
operate(context, op, other) {
/* jshint noempty:false */
let value = this._operate(context, op, /** @type {number} */ (this.value), /** @type {number} */ (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, /** @type {number} */ (this.value), /** @type {number} */ (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(/** @type {number} */ (value), unit);
}
/**
* @param {Node} other
* @returns {number | undefined}
*/
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(/** @type {number} */ (a.value), /** @type {number} */ (b.value));
}
unify() {
return this.convertTo({ length: 'px', duration: 's', angle: 'rad' });
}
/**
* @param {string | { [groupName: string]: string }} conversions
* @returns {Dimension}
*/
convertTo(conversions) {
let value = /** @type {number} */ (this.value);
const unit = this.unit.clone();
let i;
/** @type {string} */
let groupName;
/** @type {{ [unitName: string]: number }} */
let group;
/** @type {string} */
let targetUnit;
/** @type {{ [groupName: string]: string }} */
let derivedConversions = {};
/** @type {(atomicUnit: string, denominator: boolean) => string} */
let applyUnit;
if (typeof conversions === 'string') {
for (i in unitConversions) {
if (unitConversions[/** @type {keyof typeof 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 = /** @type {{ [unitName: string]: number }} */ (unitConversions[/** @type {keyof typeof unitConversions} */ (groupName)]);
unit.map(applyUnit);
}
}
unit.cancel();
return new Dimension(value, unit);
}
}
export default Dimension;
+93
View File
@@ -0,0 +1,93 @@
// @ts-check
/** @import { EvalContext, CSSOutput, TreeVisitor, FileInfo, VisibilityInfo } from './node.js' */
import Node from './node.js';
import Paren from './paren.js';
import Combinator from './combinator.js';
class Element extends Node {
get type() { return 'Element'; }
/**
* @param {Combinator | string} combinator
* @param {string | Node} value
* @param {boolean} [isVariable]
* @param {number} [index]
* @param {FileInfo} [currentFileInfo]
* @param {VisibilityInfo} [visibilityInfo]
*/
constructor(combinator, value, isVariable, index, currentFileInfo, visibilityInfo) {
super();
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 = '';
}
/** @type {boolean | undefined} */
this.isVariable = isVariable;
this._index = index;
this._fileInfo = currentFileInfo;
this.copyVisibilityInfo(visibilityInfo);
this.setParent(this.combinator, this);
}
/** @param {TreeVisitor} visitor */
accept(visitor) {
const value = this.value;
this.combinator = /** @type {Combinator} */ (visitor.visit(this.combinator));
if (typeof value === 'object') {
this.value = visitor.visit(/** @type {Node} */ (value));
}
}
/** @param {EvalContext} context */
eval(context) {
return new Element(this.combinator,
/** @type {Node} */ (this.value).eval ? /** @type {Node} */ (this.value).eval(context) : /** @type {string} */ (this.value),
this.isVariable,
this.getIndex(),
this.fileInfo(), this.visibilityInfo());
}
clone() {
return new Element(this.combinator,
/** @type {string | Node} */ (this.value),
this.isVariable,
this.getIndex(),
this.fileInfo(), this.visibilityInfo());
}
/**
* @param {EvalContext} context
* @param {CSSOutput} output
*/
genCSS(context, output) {
output.add(this.toCSS(context), this.fileInfo(), this.getIndex());
}
/** @param {EvalContext} [context] */
toCSS(context) {
/** @type {EvalContext & { firstSelector?: boolean }} */
const ctx = context || {};
let value = this.value;
const firstSelector = ctx.firstSelector;
if (value instanceof Paren) {
// selector in parens should not be affected by outer selector
// flags (breaks only interpolated selectors - see #1973)
ctx.firstSelector = true;
}
value = /** @type {Node} */ (value).toCSS ? /** @type {Node} */ (value).toCSS(ctx) : /** @type {string} */ (value);
ctx.firstSelector = firstSelector;
if (value === '' && this.combinator.value.charAt(0) === '&') {
return '';
} else {
return this.combinator.toCSS(ctx) + value;
}
}
}
export default Element;
+100
View File
@@ -0,0 +1,100 @@
// @ts-check
/** @import { EvalContext, CSSOutput, TreeVisitor } from './node.js' */
import Node from './node.js';
import Paren from './paren.js';
import Comment from './comment.js';
import Dimension from './dimension.js';
import Anonymous from './anonymous.js';
class Expression extends Node {
get type() { return 'Expression'; }
/**
* @param {Node[]} value
* @param {boolean} [noSpacing]
*/
constructor(value, noSpacing) {
super();
this.value = value;
/** @type {boolean | undefined} */
this.noSpacing = noSpacing;
/** @type {boolean | undefined} */
this.parens = undefined;
/** @type {boolean | undefined} */
this.parensInOp = undefined;
if (!value) {
throw new Error('Expression requires an array parameter');
}
}
/** @param {TreeVisitor} visitor */
accept(visitor) {
this.value = visitor.visitArray(/** @type {Node[]} */ (this.value));
}
/** @param {EvalContext} context */
eval(context) {
const noSpacing = this.noSpacing;
/** @type {Node | Expression} */
let returnValue;
const mathOn = context.isMathOn();
const inParenthesis = this.parens;
let doubleParen = false;
if (inParenthesis) {
context.inParenthesis();
}
const value = /** @type {Node[]} */ (this.value);
if (value.length > 1) {
returnValue = new Expression(value.map(function (e) {
if (!e.eval) {
return e;
}
return e.eval(context);
}), this.noSpacing);
} else if (value.length === 1) {
const first = /** @type {Expression} */ (value[0]);
if (first.parens && !first.parensInOp && !context.inCalc) {
doubleParen = true;
}
returnValue = 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);
}
/** @type {Expression} */ (returnValue).noSpacing =
/** @type {Expression} */ (returnValue).noSpacing || noSpacing;
return returnValue;
}
/**
* @param {EvalContext} context
* @param {CSSOutput} output
*/
genCSS(context, output) {
const value = /** @type {Node[]} */ (this.value);
for (let i = 0; i < value.length; i++) {
value[i].genCSS(context, output);
if (!this.noSpacing && i + 1 < value.length) {
if (!(value[i + 1] instanceof Anonymous) ||
value[i + 1] instanceof Anonymous && /** @type {string} */ (value[i + 1].value) !== ',') {
output.add(' ');
}
}
}
}
throwAwayComments() {
this.value = /** @type {Node[]} */ (this.value).filter(function(v) {
return !(v instanceof Comment);
});
}
}
export default Expression;
+88
View File
@@ -0,0 +1,88 @@
// @ts-check
/** @import { EvalContext, TreeVisitor, FileInfo, VisibilityInfo } from './node.js' */
import Node from './node.js';
import Selector from './selector.js';
class Extend extends Node {
get type() { return 'Extend'; }
/**
* @param {Selector} selector
* @param {string} option
* @param {number} index
* @param {FileInfo} currentFileInfo
* @param {VisibilityInfo} [visibilityInfo]
*/
constructor(selector, option, index, currentFileInfo, visibilityInfo) {
super();
this.selector = selector;
this.option = option;
this.object_id = Extend.next_id++;
/** @type {number[]} */
this.parent_ids = [this.object_id];
this._index = index;
this._fileInfo = currentFileInfo;
this.copyVisibilityInfo(visibilityInfo);
/** @type {boolean} */
this.allowRoot = true;
/** @type {boolean} */
this.allowBefore = false;
/** @type {boolean} */
this.allowAfter = false;
switch (option) {
case '!all':
case 'all':
this.allowBefore = true;
this.allowAfter = true;
break;
default:
this.allowBefore = false;
this.allowAfter = false;
break;
}
this.setParent(this.selector, this);
}
/** @param {TreeVisitor} visitor */
accept(visitor) {
this.selector = /** @type {Selector} */ (visitor.visit(this.selector));
}
/** @param {EvalContext} context */
eval(context) {
return new Extend(/** @type {Selector} */ (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
/** @param {EvalContext} [context] */
clone(context) {
return new Extend(this.selector, this.option, this.getIndex(), this.fileInfo(), this.visibilityInfo());
}
// it concatenates (joins) all selectors in selector array
/** @param {Selector[]} selectors */
findSelfSelectors(selectors) {
/** @type {import('./element.js').default[]} */
let selfElements = [];
let 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);
}
/** @type {Selector[]} */
this.selfSelectors = [new Selector(selfElements)];
this.selfSelectors[0].copyVisibilityInfo(this.visibilityInfo());
}
}
Extend.next_id = 0;
export default Extend;
+299
View File
@@ -0,0 +1,299 @@
// @ts-check
/** @import { EvalContext, CSSOutput, TreeVisitor, FileInfo, VisibilityInfo } from './node.js' */
import Node from './node.js';
import Media from './media.js';
import URL from './url.js';
import Quoted from './quoted.js';
import Ruleset from './ruleset.js';
import Anonymous from './anonymous.js';
import Expression from './expression.js';
import * as utils from '../utils.js';
import LessError from '../less-error.js';
/**
* @typedef {object} ImportOptions
* @property {boolean} [less]
* @property {boolean} [inline]
* @property {boolean} [isPlugin]
* @property {boolean} [reference]
*/
//
// 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.
//
class Import extends Node {
get type() { return 'Import'; }
/**
* @param {Node} path
* @param {Node | null} features
* @param {ImportOptions} options
* @param {number} index
* @param {FileInfo} [currentFileInfo]
* @param {VisibilityInfo} [visibilityInfo]
*/
constructor(path, features, options, index, currentFileInfo, visibilityInfo) {
super();
/** @type {ImportOptions} */
this.options = options;
this._index = index;
this._fileInfo = currentFileInfo;
this.path = path;
/** @type {Node | null} */
this.features = features;
/** @type {boolean} */
this.allowRoot = true;
/** @type {boolean | undefined} */
this.css = undefined;
/** @type {boolean | undefined} */
this.layerCss = undefined;
/** @type {(Ruleset & { imports?: object, filename?: string, functions?: object, functionRegistry?: { addMultiple: (fns: object) => void } }) | undefined} */
this.root = undefined;
/** @type {string | undefined} */
this.importedFilename = undefined;
/** @type {boolean | (() => boolean) | undefined} */
this.skip = undefined;
/** @type {{ message: string, index: number, filename: string } | undefined} */
this.error = undefined;
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);
if (this.features) {
this.setParent(this.features, /** @type {Node} */ (this));
}
this.setParent(this.path, /** @type {Node} */ (this));
}
/** @param {TreeVisitor} visitor */
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 = /** @type {Ruleset} */ (visitor.visit(this.root));
}
}
/**
* @param {EvalContext} context
* @param {CSSOutput} output
*/
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(';');
}
}
/** @returns {string | undefined} */
getPath() {
return (this.path instanceof URL) ?
/** @type {string} */ (/** @type {Node} */ (this.path.value).value) :
/** @type {string | undefined} */ (this.path.value);
}
/** @returns {boolean | RegExpMatchArray | null} */
isVariableImport() {
let path = this.path;
if (path instanceof URL) {
path = /** @type {Node} */ (path.value);
}
if (path instanceof Quoted) {
return path.containsVariables();
}
return true;
}
/** @param {EvalContext} context */
evalForImport(context) {
let path = this.path;
if (path instanceof URL) {
path = /** @type {Node} */ (path.value);
}
return new Import(path.eval(context), this.features, this.options, this._index || 0, this._fileInfo, this.visibilityInfo());
}
/** @param {EvalContext} context */
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 = /** @type {string} */ (path.value);
if (fileInfo &&
pathValue &&
context.pathRequiresRewrite(pathValue)) {
path.value = context.rewritePath(pathValue, fileInfo.rootpath);
} else {
path.value = context.normalizePath(/** @type {string} */ (path.value));
}
}
return path;
}
/** @param {EvalContext} context */
// @ts-ignore - Import.eval returns Node | Node[] | Import (wider than Node.eval's Node return)
eval(context) {
const result = this.doEval(context);
if (this.options.reference || this.blocksVisibility()) {
if (Array.isArray(result)) {
result.forEach(function (node) {
node.addVisibilityBlock();
});
} else {
/** @type {Node} */ (result).addVisibilityBlock();
}
}
return result;
}
/** @param {EvalContext} context */
doEval(context) {
/** @type {Ruleset | undefined} */
let ruleset;
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) {
const err = /** @type {{ message: string }} */ (e);
err.message = 'Plugin error during evaluation';
throw new LessError(
/** @type {{ message: string, index?: number, filename?: string }} */ (e),
/** @type {{ imports: object }} */ (/** @type {unknown} */ (this.root)).imports,
/** @type {{ filename: string }} */ (/** @type {unknown} */ (this.root)).filename
);
}
}
const frame0 = /** @type {Ruleset & { functionRegistry?: { addMultiple: (fns: object) => void } }} */ (context.frames[0]);
const registry = frame0 && frame0.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.features) {
let featureValue = /** @type {Node[]} */ (this.features.value);
if (Array.isArray(featureValue) && featureValue.length >= 1) {
const expr = featureValue[0];
if (expr.type === 'Expression' && Array.isArray(expr.value) && /** @type {Node[]} */ (expr.value).length >= 2) {
featureValue = /** @type {Node[]} */ (expr.value);
const isLayer = featureValue[0].type === 'Keyword' && featureValue[0].value === 'layer'
&& featureValue[1].type === 'Paren';
if (isLayer) {
this.css = false;
}
}
}
}
if (this.options.inline) {
const contents = new Anonymous(
/** @type {string} */ (/** @type {unknown} */ (this.root)),
0,
{
filename: this.importedFilename,
reference: this.path._fileInfo && this.path._fileInfo.reference
},
true,
true
);
return this.features ? new Media([contents], /** @type {Node[]} */ (this.features.value)) : [contents];
} else if (this.css || this.layerCss) {
const newImport = new Import(this.evalPath(context), features, this.options, this._index || 0);
if (this.layerCss) {
newImport.css = this.layerCss;
newImport.path._fileInfo = this._fileInfo;
}
if (!newImport.css && this.error) {
throw this.error;
}
return newImport;
} else if (this.root) {
if (this.features) {
let featureValue = /** @type {Node[]} */ (this.features.value);
if (Array.isArray(featureValue) && featureValue.length === 1) {
const expr = featureValue[0];
if (expr.type === 'Expression' && Array.isArray(expr.value) && /** @type {Node[]} */ (expr.value).length >= 2) {
featureValue = /** @type {Node[]} */ (expr.value);
const isLayer = featureValue[0].type === 'Keyword' && featureValue[0].value === 'layer'
&& featureValue[1].type === 'Paren';
if (isLayer) {
this.layerCss = true;
featureValue[0] = new Expression(/** @type {Node[]} */ (featureValue.slice(0, 2)));
featureValue.splice(1, 1);
/** @type {Expression} */ (featureValue[0]).noSpacing = true;
return this;
}
}
}
}
ruleset = new Ruleset(null, utils.copyArray(this.root.rules));
ruleset.evalImports(context);
return this.features ? new Media(ruleset.rules, /** @type {Node[]} */ (this.features.value)) : ruleset.rules;
} else {
if (this.features) {
let featureValue = /** @type {Node[]} */ (this.features.value);
if (Array.isArray(featureValue) && featureValue.length >= 1) {
featureValue = /** @type {Node[]} */ (featureValue[0].value);
if (Array.isArray(featureValue) && featureValue.length >= 2) {
const isLayer = featureValue[0].type === 'Keyword' && featureValue[0].value === 'layer'
&& featureValue[1].type === 'Paren';
if (isLayer) {
this.css = true;
featureValue[0] = new Expression(/** @type {Node[]} */ (featureValue.slice(0, 2)));
featureValue.splice(1, 1);
/** @type {Expression} */ (featureValue[0]).noSpacing = true;
return this;
}
}
}
}
return [];
}
}
}
export default Import;
+56
View File
@@ -0,0 +1,56 @@
// @ts-check
import Node from './node.js';
import Color from './color.js';
import AtRule from './atrule.js';
import DetachedRuleset from './detached-ruleset.js';
import Operation from './operation.js';
import Dimension from './dimension.js';
import Unit from './unit.js';
import Keyword from './keyword.js';
import Variable from './variable.js';
import Property from './property.js';
import Ruleset from './ruleset.js';
import Element from './element.js';
import Attribute from './attribute.js';
import Combinator from './combinator.js';
import Selector from './selector.js';
import Quoted from './quoted.js';
import Expression from './expression.js';
import Declaration from './declaration.js';
import Call from './call.js';
import URL from './url.js';
import Import from './import.js';
import Comment from './comment.js';
import Anonymous from './anonymous.js';
import Value from './value.js';
import JavaScript from './javascript.js';
import Assignment from './assignment.js';
import Condition from './condition.js';
import QueryInParens from './query-in-parens.js';
import Paren from './paren.js';
import Media from './media.js';
import Container from './container.js';
import UnicodeDescriptor from './unicode-descriptor.js';
import Negative from './negative.js';
import Extend from './extend.js';
import VariableCall from './variable-call.js';
import NamespaceValue from './namespace-value.js';
// mixins
import MixinCall from './mixin-call.js';
import MixinDefinition from './mixin-definition.js';
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, Container, QueryInParens,
UnicodeDescriptor, Negative, Extend, VariableCall,
NamespaceValue,
mixin: {
Call: MixinCall,
Definition: MixinDefinition
}
};
+45
View File
@@ -0,0 +1,45 @@
// @ts-check
/** @import { EvalContext, FileInfo } from './node.js' */
import JsEvalNode from './js-eval-node.js';
import Dimension from './dimension.js';
import Quoted from './quoted.js';
import Anonymous from './anonymous.js';
class JavaScript extends JsEvalNode {
get type() { return 'JavaScript'; }
/**
* @param {string} string
* @param {boolean} escaped
* @param {number} index
* @param {FileInfo} currentFileInfo
*/
constructor(string, escaped, index, currentFileInfo) {
super();
this.escaped = escaped;
this.expression = string;
this._index = index;
this._fileInfo = currentFileInfo;
}
/**
* @param {EvalContext} context
* @returns {Dimension | Quoted | Anonymous}
*/
eval(context) {
const result = this.evaluateJavaScript(this.expression, context);
const type = typeof result;
if (type === 'number' && !isNaN(/** @type {number} */ (result))) {
return new Dimension(/** @type {number} */ (result));
} else if (type === 'string') {
return new Quoted(`"${result}"`, /** @type {string} */ (result), this.escaped, this._index);
} else if (Array.isArray(result)) {
return new Anonymous(/** @type {string[]} */ (result).join(', '));
} else {
return new Anonymous(/** @type {string} */ (result));
}
}
}
export default JavaScript;
+74
View File
@@ -0,0 +1,74 @@
// @ts-check
/** @import { EvalContext } from './node.js' */
import Node from './node.js';
import Variable from './variable.js';
class JsEvalNode extends Node {
/**
* @param {string} expression
* @param {EvalContext} context
* @returns {string | number | boolean}
*/
evaluateJavaScript(expression, context) {
let result;
const that = this;
/** @type {Record<string, { value: Node, toJS: () => string }>} */
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));
});
/** @type {Function} */
let expressionFunc;
try {
expressionFunc = new Function(`return (${expression})`);
} catch (e) {
throw { message: `JavaScript evaluation error: ${/** @type {Error} */ (e).message} from \`${expression}\`` ,
filename: this.fileInfo().filename,
index: this.getIndex() };
}
const variables = /** @type {Node & { variables: () => Record<string, { value: Node }> }} */ (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(context);
}
};
}
}
try {
result = expressionFunc.call(evalContext);
} catch (e) {
throw { message: `JavaScript evaluation error: '${/** @type {Error} */ (e).name}: ${/** @type {Error} */ (e).message.replace(/["]/g, '\'')}'` ,
filename: this.fileInfo().filename,
index: this.getIndex() };
}
return result;
}
/**
* @param {Node} obj
* @returns {string}
*/
jsify(obj) {
if (Array.isArray(obj.value) && (obj.value.length > 1)) {
return `[${obj.value.map(function (v) { return v.toCSS(/** @type {EvalContext} */ (undefined)); }).join(', ')}]`;
} else {
return obj.toCSS(/** @type {EvalContext} */ (undefined));
}
}
}
export default JsEvalNode;
+28
View File
@@ -0,0 +1,28 @@
// @ts-check
import Node from './node.js';
/** @import { EvalContext, CSSOutput } from './node.js' */
class Keyword extends Node {
get type() { return 'Keyword'; }
/** @param {string} value */
constructor(value) {
super();
this.value = value;
}
/**
* @param {EvalContext} context
* @param {CSSOutput} output
*/
genCSS(context, output) {
if (this.value === '%') { throw { type: 'Syntax', message: 'Invalid % without number' }; }
output.add(/** @type {string} */ (this.value));
}
}
Keyword.True = new Keyword('true');
Keyword.False = new Keyword('false');
export default Keyword;
+90
View File
@@ -0,0 +1,90 @@
// @ts-check
/** @import { EvalContext, CSSOutput, FileInfo, VisibilityInfo } from './node.js' */
/** @import { NestableAtRuleThis } from './nested-at-rule.js' */
/** @import { RulesetLikeNode } from './atrule.js' */
import Node from './node.js';
import Ruleset from './ruleset.js';
import Value from './value.js';
import Selector from './selector.js';
import AtRule from './atrule.js';
import NestableAtRulePrototype from './nested-at-rule.js';
class Media extends AtRule {
get type() { return 'Media'; }
/**
* @param {Node[] | null} value
* @param {Node[]} features
* @param {number} [index]
* @param {FileInfo} [currentFileInfo]
* @param {VisibilityInfo} [visibilityInfo]
*/
constructor(value, features, index, currentFileInfo, visibilityInfo) {
super();
this._index = index;
this._fileInfo = currentFileInfo;
const selectors = (new Selector([], null, null, this._index, this._fileInfo)).createEmptySelectors();
/** @type {Value} */
this.features = new Value(features);
/** @type {RulesetLikeNode[]} */
this.rules = [new Ruleset(selectors, value)];
/** @type {RulesetLikeNode} */ (this.rules[0]).allowImports = true;
this.copyVisibilityInfo(visibilityInfo);
this.allowRoot = true;
this.setParent(selectors, /** @type {Node} */ (/** @type {unknown} */ (this)));
this.setParent(this.features, /** @type {Node} */ (/** @type {unknown} */ (this)));
this.setParent(this.rules, /** @type {Node} */ (/** @type {unknown} */ (this)));
}
/**
* @param {EvalContext} context
* @param {CSSOutput} output
*/
genCSS(context, output) {
output.add('@media ', this._fileInfo, this._index);
this.features.genCSS(context, output);
this.outputRuleset(context, output, this.rules);
}
/**
* @param {EvalContext} context
* @returns {Node}
*/
eval(context) {
if (!context.mediaBlocks) {
context.mediaBlocks = [];
context.mediaPath = [];
}
const media = new Media(null, [], this._index, this._fileInfo, this.visibilityInfo());
if (this.debugInfo) {
/** @type {RulesetLikeNode} */ (this.rules[0]).debugInfo = this.debugInfo;
media.debugInfo = this.debugInfo;
}
media.features = /** @type {Value} */ (this.features.eval(context));
context.mediaPath.push(/** @type {Node} */ (/** @type {unknown} */ (media)));
context.mediaBlocks.push(/** @type {Node} */ (/** @type {unknown} */ (media)));
const fr = /** @type {RulesetLikeNode} */ (context.frames[0]).functionRegistry;
if (fr) {
/** @type {RulesetLikeNode} */ (this.rules[0]).functionRegistry = fr.inherit();
}
context.frames.unshift(this.rules[0]);
media.rules = [/** @type {RulesetLikeNode} */ (this.rules[0].eval(context))];
context.frames.shift();
context.mediaPath.pop();
return context.mediaPath.length === 0 ? /** @type {NestableAtRuleThis} */ (/** @type {unknown} */ (media)).evalTop(context) :
/** @type {NestableAtRuleThis} */ (/** @type {unknown} */ (media)).evalNested(context);
}
}
// Apply NestableAtRulePrototype methods (accept, isRulesetLike override AtRule's versions)
Object.assign(Media.prototype, NestableAtRulePrototype);
export default Media;
+47
View File
@@ -0,0 +1,47 @@
// @ts-check
import Expression from './expression.js';
import Value from './value.js';
import Node from './node.js';
/**
* Merges declarations with merge flags (+ or ,) into combined values.
* Used by both the ToCSSVisitor and AtRule eval.
* @param {Node[]} rules
*/
export default function mergeRules(rules) {
if (!rules) {
return;
}
/** @type {Record<string, Array<Node & { merge: string, name: string, value: Node, important: string }>>} */
const groups = {};
/** @type {Array<Array<Node & { merge: string, name: string, value: Node, important: string }>>} */
const groupsArr = [];
for (let i = 0; i < rules.length; i++) {
const rule = /** @type {Node & { merge: string, name: string }} */ (rules[i]);
if (rule.merge) {
const key = rule.name;
groups[key] ? rules.splice(i--, 1) :
groupsArr.push(groups[key] = []);
groups[key].push(/** @type {Node & { merge: string, name: string, value: Node, important: string }} */ (rule));
}
}
groupsArr.forEach(group => {
if (group.length > 0) {
const result = group[0];
/** @type {Node[]} */
let space = [];
const comma = [new Expression(space)];
group.forEach(rule => {
if ((rule.merge === '+') && (space.length > 0)) {
comma.push(new Expression(space = []));
}
space.push(rule.value);
result.important = result.important || rule.important;
});
result.value = new Value(comma);
}
});
}
+283
View File
@@ -0,0 +1,283 @@
// @ts-check
/** @import { EvalContext, TreeVisitor, FileInfo } from './node.js' */
/** @import { FunctionRegistry } from './nested-at-rule.js' */
import Node from './node.js';
import Selector from './selector.js';
import MixinDefinition from './mixin-definition.js';
import defaultFunc from '../functions/default.js';
/**
* @typedef {{ name?: string, value: Node, expand?: boolean }} MixinArg
*/
/**
* @typedef {Node & {
* rules?: Node[],
* selectors?: Selector[],
* originalRuleset?: Node,
* matchArgs: (args: MixinArg[] | null, context: EvalContext) => boolean,
* matchCondition?: (args: MixinArg[] | null, context: EvalContext) => boolean,
* find: (selector: Selector, self?: Node | null, filter?: (rule: Node) => boolean) => Array<{ rule: Node & { rules?: Node[], originalRuleset?: Node, matchArgs: Function, matchCondition?: Function, evalCall?: Function }, path: Node[] }>,
* functionRegistry?: FunctionRegistry
* }} MixinSearchFrame
*/
class MixinCall extends Node {
get type() { return 'MixinCall'; }
/**
* @param {import('./element.js').default[]} elements
* @param {MixinArg[]} [args]
* @param {number} [index]
* @param {FileInfo} [currentFileInfo]
* @param {string} [important]
*/
constructor(elements, args, index, currentFileInfo, important) {
super();
/** @type {Selector} */
this.selector = new Selector(elements);
/** @type {MixinArg[]} */
this.arguments = args || [];
this._index = index;
this._fileInfo = currentFileInfo;
/** @type {string | undefined} */
this.important = important;
this.allowRoot = true;
this.setParent(this.selector, /** @type {Node} */ (/** @type {unknown} */ (this)));
}
/** @param {TreeVisitor} visitor */
accept(visitor) {
if (this.selector) {
this.selector = /** @type {Selector} */ (visitor.visit(this.selector));
}
if (this.arguments.length) {
this.arguments = /** @type {MixinArg[]} */ (/** @type {unknown} */ (visitor.visitArray(/** @type {Node[]} */ (/** @type {unknown} */ (this.arguments)))));
}
}
/**
* @param {EvalContext} context
* @returns {Node}
*/
eval(context) {
/** @type {{ rule: Node & { rules?: Node[], originalRuleset?: Node, matchArgs: Function, matchCondition?: Function, evalCall?: Function }, path: Node[] }[] | undefined} */
let mixins;
/** @type {Node & { rules?: Node[], originalRuleset?: Node, matchArgs: Function, matchCondition?: Function, evalCall?: Function }} */
let mixin;
/** @type {Node[]} */
let mixinPath;
/** @type {MixinArg[]} */
const args = [];
/** @type {MixinArg} */
let arg;
/** @type {Node} */
let argValue;
/** @type {Node[]} */
const rules = [];
let match = false;
/** @type {number} */
let i;
/** @type {number} */
let m;
/** @type {number} */
let f;
/** @type {boolean} */
let isRecursive;
/** @type {boolean | undefined} */
let isOneFound;
/** @type {{ mixin: Node & { rules?: Node[], originalRuleset?: Node, matchArgs: Function, matchCondition?: Function, evalCall?: Function }, group: number }[]} */
const candidates = [];
/** @type {{ mixin: Node & { rules?: Node[], originalRuleset?: Node, matchArgs: Function, matchCondition?: Function, evalCall?: Function }, group: number } | number} */
let candidate;
/** @type {boolean[]} */
const conditionResult = [];
/** @type {number | undefined} */
let defaultResult;
const defFalseEitherCase = -1;
const defNone = 0;
const defTrue = 1;
const defFalse = 2;
/** @type {number[]} */
let count;
/** @type {Node | undefined} */
let originalRuleset;
/** @type {((rule: MixinSearchFrame) => boolean) | undefined} */
let noArgumentsFilter;
this.selector = /** @type {Selector} */ (this.selector.eval(context));
/**
* @param {Node & { matchCondition?: Function }} mixin
* @param {Node[]} mixinPath
*/
function calcDefGroup(mixin, mixinPath) {
/** @type {number} */
let f;
/** @type {number} */
let p;
/** @type {Node & { matchCondition?: Function }} */
let 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)) {
const expandedValues = /** @type {Node[]} */ (argValue.value);
for (m = 0; m < expandedValues.length; m++) {
args.push({value: expandedValues[m]});
}
} else if (argValue.type === 'Expression' && Array.isArray(argValue.value) && argValue.value.length === 0) {
// an unset variadic holds no captured args; omit it so the callee
// falls through to its own parameter defaults instead of receiving empty
} else {
args.push({name: arg.name, value: argValue});
}
}
noArgumentsFilter = function(/** @type {MixinSearchFrame} */ rule) {return rule.matchArgs(null, context);};
for (i = 0; i < context.frames.length; i++) {
if ((mixins = /** @type {MixinSearchFrame} */ (context.frames[i]).find(this.selector, null, /** @type {(rule: Node) => boolean} */ (/** @type {unknown} */ (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 === (/** @type {Node & { originalRuleset?: Node }} */ (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 (/** @type {{ mixin: Node, group: number }} */ (candidate).group !== defFalseEitherCase) {
candidates.push(/** @type {{ mixin: Node & { rules?: Node[], originalRuleset?: Node, matchArgs: Function, matchCondition?: Function, evalCall?: Function }, group: number }} */ (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 = /** @type {Node & { originalRuleset?: Node }} */ (mixin).originalRuleset || mixin;
mixin = new MixinDefinition('', [], mixin.rules, null, false, null, originalRuleset.visibilityInfo());
/** @type {Node & { originalRuleset?: Node }} */ (mixin).originalRuleset = originalRuleset;
}
const newRules = /** @type {MixinDefinition} */ (mixin).evalCall(context, args, this.important).rules;
this._setVisibilityToReplacement(newRules);
Array.prototype.push.apply(rules, newRules);
} catch (e) {
throw { .../** @type {object} */ (e), index: this.getIndex(), filename: this.fileInfo().filename };
}
}
}
if (match) {
return /** @type {Node} */ (/** @type {unknown} */ (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(/** @type {EvalContext} */ ({})).trim()} is undefined`,
index: this.getIndex(), filename: this.fileInfo().filename };
}
}
/** @param {Node[]} replacement */
_setVisibilityToReplacement(replacement) {
/** @type {number} */
let i;
/** @type {Node} */
let rule;
if (this.blocksVisibility()) {
for (i = 0; i < replacement.length; i++) {
rule = replacement[i];
rule.addVisibilityBlock();
}
}
}
/** @param {MixinArg[]} args */
format(args) {
return `${this.selector.toCSS(/** @type {EvalContext} */ ({})).trim()}(${args ? args.map(function (/** @type {MixinArg} */ a) {
let argValue = '';
if (a.name) {
argValue += `${a.name}:`;
}
if (a.value.toCSS) {
argValue += a.value.toCSS(/** @type {EvalContext} */ ({}));
} else {
argValue += '???';
}
return argValue;
}).join(', ') : ''})`;
}
}
export default MixinCall;
+319
View File
@@ -0,0 +1,319 @@
// @ts-check
/** @import { EvalContext, TreeVisitor, VisibilityInfo } from './node.js' */
/** @import { FunctionRegistry } from './nested-at-rule.js' */
/** @import { MixinArg } from './mixin-call.js' */
import Node from './node.js';
import Selector from './selector.js';
import Element from './element.js';
import Ruleset from './ruleset.js';
import Declaration from './declaration.js';
import DetachedRuleset from './detached-ruleset.js';
import Expression from './expression.js';
import contexts from '../contexts.js';
import * as utils from '../utils.js';
/**
* @typedef {object} MixinParam
* @property {string} [name]
* @property {Node} [value]
* @property {boolean} [variadic]
*/
/**
* @typedef {Ruleset & {
* functionRegistry?: FunctionRegistry,
* originalRuleset?: Node
* }} RulesetWithRegistry
*/
class Definition extends Ruleset {
get type() { return 'MixinDefinition'; }
/**
* @param {string | undefined} name
* @param {MixinParam[]} params
* @param {Node[]} rules
* @param {Node | null} [condition]
* @param {boolean} [variadic]
* @param {Node[] | null} [frames]
* @param {VisibilityInfo} [visibilityInfo]
*/
constructor(name, params, rules, condition, variadic, frames, visibilityInfo) {
super(null, null);
/** @type {string} */
this.name = name || 'anonymous mixin';
this.selectors = [new Selector([new Element(null, name, false, this._index, this._fileInfo)])];
/** @type {MixinParam[]} */
this.params = params;
/** @type {Node | null | undefined} */
this.condition = condition;
/** @type {boolean | undefined} */
this.variadic = variadic;
/** @type {number} */
this.arity = params.length;
this.rules = rules;
this._lookups = {};
/** @type {string[]} */
const optionalParameters = [];
/** @type {number} */
this.required = params.reduce(function (/** @type {number} */ count, /** @type {MixinParam} */ p) {
if (!p.name || (p.name && !p.value)) {
return count + 1;
}
else {
optionalParameters.push(p.name);
return count;
}
}, 0);
/** @type {string[]} */
this.optionalParameters = optionalParameters;
/** @type {Node[] | null | undefined} */
this.frames = frames;
this.copyVisibilityInfo(visibilityInfo);
this.allowRoot = true;
/** @type {boolean} */
this.evalFirst = true;
}
/** @param {TreeVisitor} visitor */
accept(visitor) {
if (this.params && this.params.length) {
this.params = /** @type {MixinParam[]} */ (/** @type {unknown} */ (visitor.visitArray(/** @type {Node[]} */ (/** @type {unknown} */ (this.params)))));
}
this.rules = visitor.visitArray(this.rules);
if (this.condition) {
this.condition = visitor.visit(this.condition);
}
}
/**
* @param {EvalContext} context
* @param {EvalContext} mixinEnv
* @param {MixinArg[] | null} args
* @param {Node[]} evaldArguments
* @returns {Ruleset}
*/
evalParams(context, mixinEnv, args, evaldArguments) {
/* jshint boss:true */
const frame = new Ruleset(null, null);
/** @type {Node[] | undefined} */
let varargs;
/** @type {MixinArg | undefined} */
let arg;
const params = /** @type {MixinParam[]} */ (utils.copyArray(this.params));
/** @type {number} */
let i;
/** @type {number} */
let j;
/** @type {Node | undefined} */
let val;
/** @type {string | undefined} */
let name;
/** @type {boolean} */
let isNamedFound;
/** @type {number} */
let argIndex;
let argsLength = 0;
if (mixinEnv.frames && mixinEnv.frames[0] && /** @type {RulesetWithRegistry} */ (mixinEnv.frames[0]).functionRegistry) {
/** @type {RulesetWithRegistry} */ (frame).functionRegistry = /** @type {FunctionRegistry} */ (/** @type {RulesetWithRegistry} */ (mixinEnv.frames[0]).functionRegistry).inherit();
}
mixinEnv = new contexts.Eval(mixinEnv, /** @type {Node[]} */ ([frame]).concat(/** @type {Node[]} */ (mixinEnv.frames)));
if (args) {
args = /** @type {MixinArg[]} */ (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(/** @type {MixinArg[]} */ (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 = /** @type {Node} */ (/** @type {unknown} */ (new DetachedRuleset(new Ruleset(null, /** @type {Node[]} */ (val)))));
}
else {
val = val.eval(context);
}
} else if (params[i].value) {
val = /** @type {Node} */ (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;
}
/** @returns {Ruleset} */
makeImportant() {
const rules = !this.rules ? this.rules : this.rules.map(function (/** @type {Node & { makeImportant?: (important?: boolean) => Node }} */ 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 /** @type {Ruleset} */ (/** @type {unknown} */ (result));
}
/**
* @param {EvalContext} context
* @returns {Definition}
*/
eval(context) {
return new Definition(this.name, this.params, this.rules, this.condition, this.variadic, this.frames || utils.copyArray(context.frames));
}
/**
* @param {EvalContext} context
* @param {MixinArg[]} args
* @param {string | undefined} important
* @returns {Ruleset}
*/
evalCall(context, args, important) {
/** @type {Node[]} */
const _arguments = [];
const mixinFrames = this.frames ? /** @type {Node[]} */ (this.frames).concat(/** @type {Node[]} */ (context.frames)) : /** @type {Node[]} */ (context.frames);
const frame = this.evalParams(context, new contexts.Eval(context, mixinFrames), args, _arguments);
/** @type {Node[]} */
let rules;
/** @type {Ruleset} */
let ruleset;
frame.prependRule(new Declaration('@arguments', new Expression(_arguments).eval(context)));
rules = utils.copyArray(this.rules);
ruleset = new Ruleset(null, rules);
/** @type {RulesetWithRegistry} */ (ruleset).originalRuleset = this;
ruleset = /** @type {Ruleset} */ (ruleset.eval(new contexts.Eval(context, /** @type {Node[]} */ ([this, frame]).concat(mixinFrames))));
if (important) {
ruleset = /** @type {Ruleset} */ (ruleset.makeImportant());
}
return ruleset;
}
/**
* @param {MixinArg[] | null} args
* @param {EvalContext} context
* @returns {boolean}
*/
matchCondition(args, context) {
if (this.condition && !this.condition.eval(
new contexts.Eval(context,
/** @type {Node[]} */ ([this.evalParams(context, /* the parameter variables */
new contexts.Eval(context, this.frames ? /** @type {Node[]} */ (this.frames).concat(/** @type {Node[]} */ (context.frames)) : context.frames), args, [])])
.concat(/** @type {Node[]} */ (this.frames || [])) // the parent namespace/mixin frames
.concat(/** @type {Node[]} */ (context.frames))))) { // the current environment frames
return false;
}
return true;
}
/**
* @param {MixinArg[] | null} args
* @param {EvalContext} [context]
* @returns {boolean}
*/
matchArgs(args, context) {
const allArgsCnt = (args && args.length) || 0;
const evaldArguments = new Array(this.params.length);
const positionalArgs = [];
let positionalIndex = 0;
for (let i = 0; i < allArgsCnt; i++) {
const arg = /** @type {MixinArg[]} */ (args)[i];
if (!arg.name) {
positionalArgs.push(arg);
continue;
}
const paramIndex = this.params.findIndex((param, index) => param.name === arg.name && !evaldArguments[index]);
if (paramIndex < 0) {
return false;
}
evaldArguments[paramIndex] = arg;
}
for (let i = 0; i < this.params.length; i++) {
if (evaldArguments[i]) {
continue;
}
if (this.params[i].variadic) {
positionalIndex = positionalArgs.length;
continue;
}
if (positionalIndex < positionalArgs.length) {
evaldArguments[i] = positionalArgs[positionalIndex++];
} else if (!this.params[i].name || !this.params[i].value) {
return false;
}
}
if (positionalIndex < positionalArgs.length) {
return false;
}
// check patterns
for (let i = 0; i < this.arity; i++) {
if (!this.params[i].name && !this.params[i].variadic) {
if (evaldArguments[i].value.eval(context).toCSS(/** @type {EvalContext} */ ({})) != /** @type {Node} */ (this.params[i].value).eval(context).toCSS(/** @type {EvalContext} */ ({}))) {
return false;
}
}
}
return true;
}
}
export default Definition;
+95
View File
@@ -0,0 +1,95 @@
// @ts-check
/** @import { EvalContext, FileInfo } from './node.js' */
import Node from './node.js';
import Variable from './variable.js';
import Ruleset from './ruleset.js';
import Selector from './selector.js';
class NamespaceValue extends Node {
get type() { return 'NamespaceValue'; }
/**
* @param {Node} ruleCall
* @param {string[]} lookups
* @param {number} index
* @param {FileInfo} fileInfo
*/
constructor(ruleCall, lookups, index, fileInfo) {
super();
this.value = ruleCall;
this.lookups = lookups;
this._index = index;
this._fileInfo = fileInfo;
}
/**
* @param {EvalContext} context
* @returns {Node}
*/
eval(context) {
let i, name;
/** @type {Ruleset | Node | Node[]} */
let rules = this.value.eval(context);
for (i = 0; i < this.lookups.length; i++) {
name = this.lookups[i];
if (Array.isArray(rules)) {
rules = new Ruleset([new Selector()], rules);
}
const rs = /** @type {Ruleset} */ (rules);
if (name === '') {
rules = rs.lastDeclaration();
}
else if (name.charAt(0) === '@') {
if (name.charAt(1) === '@') {
name = `@${new Variable(name.slice(1)).eval(context).value}`;
}
if (rs.variables) {
rules = rs.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.slice(1)).eval(context).value}`;
}
else {
name = name.charAt(0) === '$' ? name : `$${name}`;
}
if (rs.properties) {
rules = rs.property(name);
}
if (!rules) {
throw { type: 'Name',
message: `property "${name.slice(1)}" not found`,
filename: this.fileInfo().filename,
index: this.getIndex() };
}
const rulesArr = /** @type {Node[]} */ (rules);
rules = rulesArr[rulesArr.length - 1];
}
const current = /** @type {Node} */ (rules);
if (current.value) {
rules = /** @type {Node} */ (current.eval(context).value);
}
const currentNode = /** @type {Node & { ruleset?: Node }} */ (rules);
if (currentNode.ruleset) {
rules = currentNode.ruleset.eval(context);
}
}
return /** @type {Node} */ (rules);
}
}
export default NamespaceValue;
+37
View File
@@ -0,0 +1,37 @@
// @ts-check
/** @import { EvalContext, CSSOutput } from './node.js' */
import Node from './node.js';
import Operation from './operation.js';
import Dimension from './dimension.js';
class Negative extends Node {
get type() { return 'Negative'; }
/** @param {Node} node */
constructor(node) {
super();
this.value = node;
}
/**
* @param {EvalContext} context
* @param {CSSOutput} output
*/
genCSS(context, output) {
output.add('-');
/** @type {Node} */ (this.value).genCSS(context, output);
}
/**
* @param {EvalContext} context
* @returns {Node}
*/
eval(context) {
if (context.isMathOn('*')) {
return (new Operation('*', [new Dimension(-1), /** @type {Node} */ (this.value)], false)).eval(context);
}
return new Negative(/** @type {Node} */ (this.value).eval(context));
}
}
export default Negative;
+198
View File
@@ -0,0 +1,198 @@
// @ts-check
/** @import { EvalContext, CSSOutput, TreeVisitor, FileInfo, VisibilityInfo } from './node.js' */
import Ruleset from './ruleset.js';
import Value from './value.js';
import Selector from './selector.js';
import Anonymous from './anonymous.js';
import Expression from './expression.js';
import * as utils from '../utils.js';
import Node from './node.js';
/**
* @typedef {object} FunctionRegistry
* @property {(name: string, func: Function) => void} add
* @property {(functions: Object) => void} addMultiple
* @property {(name: string) => Function} get
* @property {() => Object} getLocalFunctions
* @property {() => FunctionRegistry} inherit
* @property {(base: FunctionRegistry) => FunctionRegistry} create
*/
/**
* @typedef {Node & {
* features: Value,
* rules: Ruleset[],
* type: string,
* functionRegistry?: FunctionRegistry,
* multiMedia?: boolean,
* debugInfo?: { lineNumber: number, fileName: string },
* allowRoot?: boolean,
* _evaluated?: boolean,
* evalFunction: () => void,
* evalTop: (context: EvalContext) => Node | Ruleset,
* evalNested: (context: EvalContext) => Node | Ruleset,
* permute: (arr: Node[][]) => Node[][],
* bubbleSelectors: (selectors: Selector[] | undefined) => void,
* outputRuleset: (context: EvalContext, output: CSSOutput, rules: Node[]) => void
* }} NestableAtRuleThis
*/
const NestableAtRulePrototype = {
isRulesetLike() {
return true;
},
/** @param {TreeVisitor} visitor */
accept(visitor) {
/** @type {NestableAtRuleThis} */
const self = /** @type {NestableAtRuleThis} */ (/** @type {unknown} */ (this));
if (self.features) {
self.features = /** @type {Value} */ (visitor.visit(self.features));
}
if (self.rules) {
self.rules = /** @type {Ruleset[]} */ (visitor.visitArray(self.rules));
}
},
evalFunction: function () {
/** @type {NestableAtRuleThis} */
const self = /** @type {NestableAtRuleThis} */ (/** @type {unknown} */ (this));
if (!self.features || !Array.isArray(self.features.value) || self.features.value.length < 1) {
return;
}
const exprValues = /** @type {Node[]} */ (self.features.value);
/** @type {Node | undefined} */
let expr;
/** @type {Node | undefined} */
let paren;
for (let index = 0; index < exprValues.length; ++index) {
expr = exprValues[index];
if ((expr.type === 'Keyword' || expr.type === 'Variable')
&& index + 1 < exprValues.length
&& (/** @type {Node & { noSpacing?: boolean }} */ (expr).noSpacing || /** @type {Node & { noSpacing?: boolean }} */ (expr).noSpacing == null)) {
paren = exprValues[index + 1];
if (paren.type === 'Paren' && /** @type {Node & { noSpacing?: boolean }} */ (paren).noSpacing) {
exprValues[index]= new Expression([expr, paren]);
exprValues.splice(index + 1, 1);
/** @type {Node & { noSpacing?: boolean }} */ (exprValues[index]).noSpacing = true;
}
}
}
},
/** @param {EvalContext} context */
evalTop(context) {
/** @type {NestableAtRuleThis} */
const self = /** @type {NestableAtRuleThis} */ (/** @type {unknown} */ (this));
self.evalFunction();
/** @type {Node | Ruleset} */
let result = self;
// Render all dependent Media blocks.
if (context.mediaBlocks.length > 1) {
const selectors = (new Selector([], null, null, self.getIndex(), self.fileInfo())).createEmptySelectors();
result = new Ruleset(selectors, context.mediaBlocks);
/** @type {Ruleset & { multiMedia?: boolean }} */ (result).multiMedia = true;
result.copyVisibilityInfo(self.visibilityInfo());
self.setParent(result, self);
}
delete context.mediaBlocks;
delete context.mediaPath;
return result;
},
/** @param {EvalContext} context */
evalNested(context) {
/** @type {NestableAtRuleThis} */
const self = /** @type {NestableAtRuleThis} */ (/** @type {unknown} */ (this));
self.evalFunction();
let i;
/** @type {Node | Node[]} */
let value;
const path = context.mediaPath.concat([self]);
// Extract the media-query conditions separated with `,` (OR).
for (i = 0; i < path.length; i++) {
if (path[i].type !== self.type) {
const blockIndex = context.mediaBlocks.indexOf(self);
if (blockIndex > -1) {
context.mediaBlocks.splice(blockIndex, 1);
}
return self;
}
value = /** @type {NestableAtRuleThis} */ (path[i]).features instanceof Value ?
/** @type {Node[]} */ (/** @type {NestableAtRuleThis} */ (path[i]).features.value) : /** @type {NestableAtRuleThis} */ (path[i]).features;
path[i] = /** @type {Node} */ (/** @type {unknown} */ (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
self.features = new Value(self.permute(/** @type {Node[][]} */ (/** @type {unknown} */ (path))).map(
/** @param {Node | Node[]} path */
path => {
path = /** @type {Node[]} */ (path).map(
/** @param {Node & { toCSS?: Function }} fragment */
fragment => fragment.toCSS ? fragment : new Anonymous(/** @type {string} */ (/** @type {unknown} */ (fragment))));
for (i = /** @type {Node[]} */ (path).length - 1; i > 0; i--) {
/** @type {Node[]} */ (path).splice(i, 0, new Anonymous('and'));
}
return new Expression(/** @type {Node[]} */ (path));
}));
self.setParent(self.features, self);
// Fake a tree-node that doesn't output anything.
return new Ruleset([], []);
},
/**
* @param {Node[][]} arr
* @returns {Node[][]}
*/
permute(arr) {
if (arr.length === 0) {
return [];
} else if (arr.length === 1) {
return /** @type {Node[][]} */ (/** @type {unknown} */ (arr[0]));
} else {
/** @type {Node[][]} */
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;
}
},
/** @param {Selector[] | undefined} selectors */
bubbleSelectors(selectors) {
/** @type {NestableAtRuleThis} */
const self = /** @type {NestableAtRuleThis} */ (/** @type {unknown} */ (this));
if (!selectors) {
return;
}
self.rules = [new Ruleset(utils.copyArray(selectors), [self.rules[0]])];
self.setParent(self.rules, self);
}
};
export default NestableAtRulePrototype;
+310
View File
@@ -0,0 +1,310 @@
// @ts-check
/**
* @typedef {object} FileInfo
* @property {string} [filename]
* @property {string} [rootpath]
* @property {string} [currentDirectory]
* @property {string} [rootFilename]
* @property {string} [entryPath]
* @property {boolean} [reference]
*/
/**
* @typedef {object} VisibilityInfo
* @property {number} [visibilityBlocks]
* @property {boolean} [nodeVisible]
*/
/**
* @typedef {object} CSSOutput
* @property {(chunk: string, fileInfo?: FileInfo, index?: number, mapLines?: boolean) => void} add
* @property {() => boolean} isEmpty
*/
/**
* @typedef {object} EvalContext
* @property {number} [numPrecision]
* @property {(op?: string) => boolean} [isMathOn]
* @property {number} [math]
* @property {Node[]} [frames]
* @property {Array<{important?: string}>} [importantScope]
* @property {string[]} [paths]
* @property {boolean} [compress]
* @property {boolean} [strictUnits]
* @property {boolean} [sourceMap]
* @property {boolean} [importMultiple]
* @property {string} [urlArgs]
* @property {boolean} [javascriptEnabled]
* @property {object} [pluginManager]
* @property {number} [rewriteUrls]
* @property {boolean} [inCalc]
* @property {boolean} [mathOn]
* @property {boolean[]} [calcStack]
* @property {boolean[]} [parensStack]
* @property {Node[]} [mediaBlocks]
* @property {Node[]} [mediaPath]
* @property {() => void} [inParenthesis]
* @property {() => void} [outOfParenthesis]
* @property {() => void} [enterCalc]
* @property {() => void} [exitCalc]
* @property {(path: string) => boolean} [pathRequiresRewrite]
* @property {(path: string, rootpath?: string) => string} [rewritePath]
* @property {(path: string) => string} [normalizePath]
* @property {number} [tabLevel]
* @property {boolean} [lastRule]
*/
/**
* @typedef {object} TreeVisitor
* @property {(node: Node) => Node} visit
* @property {(nodes: Node[], nonReplacing?: boolean) => Node[]} visitArray
*/
/**
* 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 {
get type() { return ''; }
constructor() {
/** @type {Node | null} */
this.parent = null;
/** @type {number | undefined} */
this.visibilityBlocks = undefined;
/** @type {boolean | undefined} */
this.nodeVisible = undefined;
/** @type {Node | null} */
this.rootNode = null;
/** @type {Node | null} */
this.parsed = null;
/** @type {Node | Node[] | string | number | undefined} */
this.value = undefined;
/** @type {number | undefined} */
this._index = undefined;
/** @type {FileInfo | undefined} */
this._fileInfo = undefined;
}
get currentFileInfo() {
return this.fileInfo();
}
get index() {
return this.getIndex();
}
/**
* @param {Node | Node[]} nodes
* @param {Node} parent
*/
setParent(nodes, parent) {
/** @param {Node} node */
function set(node) {
if (node && node instanceof Node) {
node.parent = parent;
}
}
if (Array.isArray(nodes)) {
nodes.forEach(set);
}
else {
set(nodes);
}
}
/** @returns {number} */
getIndex() {
return this._index || (this.parent && this.parent.getIndex()) || 0;
}
/** @returns {FileInfo} */
fileInfo() {
return this._fileInfo || (this.parent && this.parent.fileInfo()) || {};
}
/** @returns {boolean} */
isRulesetLike() { return false; }
/**
* @param {EvalContext} context
* @returns {string}
*/
toCSS(context) {
/** @type {string[]} */
const strs = [];
this.genCSS(context, {
add: function(chunk, fileInfo, index) {
strs.push(chunk);
},
isEmpty: function () {
return strs.length === 0;
}
});
return strs.join('');
}
/**
* @param {EvalContext} context
* @param {CSSOutput} output
*/
genCSS(context, output) {
output.add(/** @type {string} */ (this.value));
}
/**
* @param {TreeVisitor} visitor
*/
accept(visitor) {
this.value = visitor.visit(/** @type {Node} */ (this.value));
}
/**
* @param {EvalContext} [context]
* @returns {Node}
*/
eval(context) { return this; }
/**
* @param {EvalContext} context
* @param {string} op
* @param {number} a
* @param {number} b
* @returns {number | undefined}
*/
_operate(context, op, a, b) {
switch (op) {
case '+': return a + b;
case '-': return a - b;
case '*': return a * b;
case '/': return a / b;
}
}
/**
* @param {EvalContext} context
* @param {number} value
* @returns {number}
*/
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;
}
/**
* @param {Node & { compare?: (other: Node) => number | undefined }} a
* @param {Node & { compare?: (other: Node) => number | undefined }} b
* @returns {number | undefined}
*/
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;
}
let aVal = a.value;
let bVal = b.value;
if (!Array.isArray(aVal)) {
return aVal === bVal ? 0 : undefined;
}
if (!Array.isArray(bVal)) {
return undefined;
}
if (aVal.length !== bVal.length) {
return undefined;
}
for (let i = 0; i < aVal.length; i++) {
if (Node.compare(aVal[i], bVal[i]) !== 0) {
return undefined;
}
}
return 0;
}
/**
* @param {number | string} a
* @param {number | string} b
* @returns {number | undefined}
*/
static numericCompare(a, b) {
return a < b ? -1
: a === b ? 0
: a > b ? 1 : undefined;
}
/** @returns {boolean} */
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;
}
ensureVisibility() {
this.nodeVisible = true;
}
ensureInvisibility() {
this.nodeVisible = false;
}
/** @returns {boolean | undefined} */
isVisible() {
return this.nodeVisible;
}
/** @returns {VisibilityInfo} */
visibilityInfo() {
return {
visibilityBlocks: this.visibilityBlocks,
nodeVisible: this.nodeVisible
};
}
/** @param {VisibilityInfo} info */
copyVisibilityInfo(info) {
if (!info) {
return;
}
this.visibilityBlocks = info.visibilityBlocks;
this.nodeVisible = info.nodeVisible;
}
}
/**
* Set by the parser at runtime on Node.prototype.
* @type {{ context: EvalContext, importManager: object, imports: object } | undefined}
*/
Node.prototype.parse = undefined;
export default Node;
+81
View File
@@ -0,0 +1,81 @@
// @ts-check
/** @import { EvalContext, CSSOutput, TreeVisitor } from './node.js' */
import Node from './node.js';
import Color from './color.js';
import Dimension from './dimension.js';
import * as Constants from '../constants.js';
const MATH = Constants.Math;
class Operation extends Node {
get type() { return 'Operation'; }
/**
* @param {string} op
* @param {Node[]} operands
* @param {boolean} isSpaced
*/
constructor(op, operands, isSpaced) {
super();
this.op = op.trim();
this.operands = operands;
this.isSpaced = isSpaced;
}
/** @param {TreeVisitor} visitor */
accept(visitor) {
this.operands = visitor.visitArray(this.operands);
}
/**
* @param {EvalContext} context
* @returns {Node}
*/
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 = /** @type {Dimension} */ (a).toColor();
}
if (b instanceof Dimension && a instanceof Color) {
b = /** @type {Dimension} */ (b).toColor();
}
if (!/** @type {Dimension | Color} */ (a).operate || !/** @type {Dimension | Color} */ (b).operate) {
if (
(a instanceof Operation || b instanceof Operation)
&& /** @type {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' };
}
if (a instanceof Dimension) {
return a.operate(context, op, /** @type {Dimension} */ (b));
}
return /** @type {Color} */ (a).operate(context, op, /** @type {Color} */ (b));
} else {
return new Operation(this.op, [a, b], this.isSpaced);
}
}
/**
* @param {EvalContext} context
* @param {CSSOutput} output
*/
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;
+41
View File
@@ -0,0 +1,41 @@
// @ts-check
/** @import { EvalContext, CSSOutput } from './node.js' */
import Node from './node.js';
class Paren extends Node {
get type() { return 'Paren'; }
/** @param {Node} node */
constructor(node) {
super();
this.value = node;
/** @type {boolean | undefined} */
this.noSpacing = undefined;
}
/**
* @param {EvalContext} context
* @param {CSSOutput} output
*/
genCSS(context, output) {
output.add('(');
/** @type {Node} */ (this.value).genCSS(context, output);
output.add(')');
}
/**
* @param {EvalContext} context
* @returns {Paren}
*/
eval(context) {
const paren = new Paren(/** @type {Node} */ (this.value).eval(context));
if (this.noSpacing) {
paren.noSpacing = true;
}
return paren;
}
}
export default Paren;

Some files were not shown because too many files have changed in this diff Show More