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