diff options
Diffstat (limited to 'node_modules/tailwindcss/src')
114 files changed, 15676 insertions, 0 deletions
diff --git a/node_modules/tailwindcss/src/cli-peer-dependencies.js b/node_modules/tailwindcss/src/cli-peer-dependencies.js new file mode 100644 index 0000000..6b9f986 --- /dev/null +++ b/node_modules/tailwindcss/src/cli-peer-dependencies.js @@ -0,0 +1,15 @@ +export function lazyPostcss() { + return require('postcss') +} + +export function lazyPostcssImport() { + return require('postcss-import') +} + +export function lazyAutoprefixer() { + return require('autoprefixer') +} + +export function lazyCssnano() { + return require('cssnano') +} diff --git a/node_modules/tailwindcss/src/cli.js b/node_modules/tailwindcss/src/cli.js new file mode 100644 index 0000000..4b73acc --- /dev/null +++ b/node_modules/tailwindcss/src/cli.js @@ -0,0 +1,7 @@ +#!/usr/bin/env node + +if (__OXIDE__) { + module.exports = require('./oxide/cli') +} else { + module.exports = require('./cli/index') +} diff --git a/node_modules/tailwindcss/src/cli/build/deps.js b/node_modules/tailwindcss/src/cli/build/deps.js new file mode 100644 index 0000000..9435b92 --- /dev/null +++ b/node_modules/tailwindcss/src/cli/build/deps.js @@ -0,0 +1,56 @@ +// @ts-check + +import { + // @ts-ignore + lazyPostcss, + + // @ts-ignore + lazyPostcssImport, + + // @ts-ignore + lazyCssnano, + + // @ts-ignore + lazyAutoprefixer, +} from '../../../peers/index.js' + +/** + * @returns {import('postcss')} + */ +export function loadPostcss() { + // Try to load a local `postcss` version first + try { + return require('postcss') + } catch {} + + return lazyPostcss() +} + +export function loadPostcssImport() { + // Try to load a local `postcss-import` version first + try { + return require('postcss-import') + } catch {} + + return lazyPostcssImport() +} + +export function loadCssNano() { + let options = { preset: ['default', { cssDeclarationSorter: false }] } + + // Try to load a local `cssnano` version first + try { + return require('cssnano') + } catch {} + + return lazyCssnano()(options) +} + +export function loadAutoprefixer() { + // Try to load a local `autoprefixer` version first + try { + return require('autoprefixer') + } catch {} + + return lazyAutoprefixer() +} diff --git a/node_modules/tailwindcss/src/cli/build/index.js b/node_modules/tailwindcss/src/cli/build/index.js new file mode 100644 index 0000000..62c020e --- /dev/null +++ b/node_modules/tailwindcss/src/cli/build/index.js @@ -0,0 +1,49 @@ +// @ts-check + +import fs from 'fs' +import path from 'path' +import { resolveDefaultConfigPath } from '../../util/resolveConfigPath.js' +import { createProcessor } from './plugin.js' + +export async function build(args) { + let input = args['--input'] + let shouldWatch = args['--watch'] + + // TODO: Deprecate this in future versions + if (!input && args['_'][1]) { + console.error('[deprecation] Running tailwindcss without -i, please provide an input file.') + input = args['--input'] = args['_'][1] + } + + if (input && input !== '-' && !fs.existsSync((input = path.resolve(input)))) { + console.error(`Specified input file ${args['--input']} does not exist.`) + process.exit(9) + } + + if (args['--config'] && !fs.existsSync((args['--config'] = path.resolve(args['--config'])))) { + console.error(`Specified config file ${args['--config']} does not exist.`) + process.exit(9) + } + + // TODO: Reference the @config path here if exists + let configPath = args['--config'] ? args['--config'] : resolveDefaultConfigPath() + + let processor = await createProcessor(args, configPath) + + if (shouldWatch) { + // Abort the watcher if stdin is closed to avoid zombie processes + // You can disable this behavior with --watch=always + if (args['--watch'] !== 'always') { + process.stdin.on('end', () => process.exit(0)) + } + + process.stdin.resume() + + await processor.watch() + } else { + await processor.build().catch((e) => { + console.error(e) + process.exit(1) + }) + } +} diff --git a/node_modules/tailwindcss/src/cli/build/plugin.js b/node_modules/tailwindcss/src/cli/build/plugin.js new file mode 100644 index 0000000..6af590d --- /dev/null +++ b/node_modules/tailwindcss/src/cli/build/plugin.js @@ -0,0 +1,444 @@ +// @ts-check + +import path from 'path' +import fs from 'fs' +import postcssrc from 'postcss-load-config' +import { lilconfig } from 'lilconfig' +import loadPlugins from 'postcss-load-config/src/plugins' // Little bit scary, looking at private/internal API +import loadOptions from 'postcss-load-config/src/options' // Little bit scary, looking at private/internal API + +import tailwind from '../../processTailwindFeatures' +import { loadAutoprefixer, loadCssNano, loadPostcss, loadPostcssImport } from './deps' +import { formatNodes, drainStdin, outputFile } from './utils' +import { env } from '../../lib/sharedState' +import resolveConfig from '../../../resolveConfig.js' +import { parseCandidateFiles } from '../../lib/content.js' +import { createWatcher } from './watching.js' +import fastGlob from 'fast-glob' +import { findAtConfigPath } from '../../lib/findAtConfigPath.js' +import log from '../../util/log' +import { loadConfig } from '../../lib/load-config' +import getModuleDependencies from '../../lib/getModuleDependencies' + +/** + * + * @param {string} [customPostCssPath ] + * @returns + */ +async function loadPostCssPlugins(customPostCssPath) { + let config = customPostCssPath + ? await (async () => { + let file = path.resolve(customPostCssPath) + + // Implementation, see: https://unpkg.com/browse/postcss-load-config@3.1.0/src/index.js + // @ts-ignore + let { config = {} } = await lilconfig('postcss').load(file) + if (typeof config === 'function') { + config = config() + } else { + config = Object.assign({}, config) + } + + if (!config.plugins) { + config.plugins = [] + } + + return { + file, + plugins: loadPlugins(config, file), + options: loadOptions(config, file), + } + })() + : await postcssrc() + + let configPlugins = config.plugins + + let configPluginTailwindIdx = configPlugins.findIndex((plugin) => { + if (typeof plugin === 'function' && plugin.name === 'tailwindcss') { + return true + } + + if (typeof plugin === 'object' && plugin !== null && plugin.postcssPlugin === 'tailwindcss') { + return true + } + + return false + }) + + let beforePlugins = + configPluginTailwindIdx === -1 ? [] : configPlugins.slice(0, configPluginTailwindIdx) + let afterPlugins = + configPluginTailwindIdx === -1 + ? configPlugins + : configPlugins.slice(configPluginTailwindIdx + 1) + + return [beforePlugins, afterPlugins, config.options] +} + +function loadBuiltinPostcssPlugins() { + let postcss = loadPostcss() + let IMPORT_COMMENT = '__TAILWIND_RESTORE_IMPORT__: ' + return [ + [ + (root) => { + root.walkAtRules('import', (rule) => { + if (rule.params.slice(1).startsWith('tailwindcss/')) { + rule.after(postcss.comment({ text: IMPORT_COMMENT + rule.params })) + rule.remove() + } + }) + }, + loadPostcssImport(), + (root) => { + root.walkComments((rule) => { + if (rule.text.startsWith(IMPORT_COMMENT)) { + rule.after( + postcss.atRule({ + name: 'import', + params: rule.text.replace(IMPORT_COMMENT, ''), + }) + ) + rule.remove() + } + }) + }, + ], + [], + {}, + ] +} + +let state = { + /** @type {any} */ + context: null, + + /** @type {ReturnType<typeof createWatcher> | null} */ + watcher: null, + + /** @type {{content: string, extension: string}[]} */ + changedContent: [], + + /** @type {ReturnType<typeof load> | null} */ + configBag: null, + + contextDependencies: new Set(), + + /** @type {import('../../lib/content.js').ContentPath[]} */ + contentPaths: [], + + refreshContentPaths() { + this.contentPaths = parseCandidateFiles(this.context, this.context?.tailwindConfig) + }, + + get config() { + return this.context.tailwindConfig + }, + + get contentPatterns() { + return { + all: this.contentPaths.map((contentPath) => contentPath.pattern), + dynamic: this.contentPaths + .filter((contentPath) => contentPath.glob !== undefined) + .map((contentPath) => contentPath.pattern), + } + }, + + loadConfig(configPath, content) { + if (this.watcher && configPath) { + this.refreshConfigDependencies() + } + + let config = loadConfig(configPath) + let dependencies = getModuleDependencies(configPath) + this.configBag = { + config, + dependencies, + dispose() { + for (let file of dependencies) { + delete require.cache[require.resolve(file)] + } + }, + } + + // @ts-ignore + this.configBag.config = resolveConfig(this.configBag.config, { content: { files: [] } }) + + // Override content files if `--content` has been passed explicitly + if (content?.length > 0) { + this.configBag.config.content.files = content + } + + return this.configBag.config + }, + + refreshConfigDependencies() { + env.DEBUG && console.time('Module dependencies') + this.configBag?.dispose() + env.DEBUG && console.timeEnd('Module dependencies') + }, + + readContentPaths() { + let content = [] + + // Resolve globs from the content config + // TODO: When we make the postcss plugin async-capable this can become async + let files = fastGlob.sync(this.contentPatterns.all) + + for (let file of files) { + if (__OXIDE__) { + content.push({ + file, + extension: path.extname(file).slice(1), + }) + } else { + content.push({ + content: fs.readFileSync(path.resolve(file), 'utf8'), + extension: path.extname(file).slice(1), + }) + } + } + + // Resolve raw content in the tailwind config + let rawContent = this.config.content.files.filter((file) => { + return file !== null && typeof file === 'object' + }) + + for (let { raw: htmlContent, extension = 'html' } of rawContent) { + content.push({ content: htmlContent, extension }) + } + + return content + }, + + getContext({ createContext, cliConfigPath, root, result, content }) { + if (this.context) { + this.context.changedContent = this.changedContent.splice(0) + + return this.context + } + + env.DEBUG && console.time('Searching for config') + let configPath = findAtConfigPath(root, result) ?? cliConfigPath + env.DEBUG && console.timeEnd('Searching for config') + + env.DEBUG && console.time('Loading config') + let config = this.loadConfig(configPath, content) + env.DEBUG && console.timeEnd('Loading config') + + env.DEBUG && console.time('Creating context') + this.context = createContext(config, []) + Object.assign(this.context, { + userConfigPath: configPath, + }) + env.DEBUG && console.timeEnd('Creating context') + + env.DEBUG && console.time('Resolving content paths') + this.refreshContentPaths() + env.DEBUG && console.timeEnd('Resolving content paths') + + if (this.watcher) { + env.DEBUG && console.time('Watch new files') + this.watcher.refreshWatchedFiles() + env.DEBUG && console.timeEnd('Watch new files') + } + + for (let file of this.readContentPaths()) { + this.context.changedContent.push(file) + } + + return this.context + }, +} + +export async function createProcessor(args, cliConfigPath) { + let postcss = loadPostcss() + + let input = args['--input'] + let output = args['--output'] + let includePostCss = args['--postcss'] + let customPostCssPath = typeof args['--postcss'] === 'string' ? args['--postcss'] : undefined + + let [beforePlugins, afterPlugins, postcssOptions] = includePostCss + ? await loadPostCssPlugins(customPostCssPath) + : loadBuiltinPostcssPlugins() + + if (args['--purge']) { + log.warn('purge-flag-deprecated', [ + 'The `--purge` flag has been deprecated.', + 'Please use `--content` instead.', + ]) + + if (!args['--content']) { + args['--content'] = args['--purge'] + } + } + + let content = args['--content']?.split(/(?<!{[^}]+),/) ?? [] + + let tailwindPlugin = () => { + return { + postcssPlugin: 'tailwindcss', + async Once(root, { result }) { + env.DEBUG && console.time('Compiling CSS') + await tailwind(({ createContext }) => { + console.error() + console.error('Rebuilding...') + + return () => { + return state.getContext({ + createContext, + cliConfigPath, + root, + result, + content, + }) + } + })(root, result) + env.DEBUG && console.timeEnd('Compiling CSS') + }, + } + } + + tailwindPlugin.postcss = true + + let plugins = [ + ...beforePlugins, + tailwindPlugin, + !args['--minify'] && formatNodes, + ...afterPlugins, + !args['--no-autoprefixer'] && loadAutoprefixer(), + args['--minify'] && loadCssNano(), + ].filter(Boolean) + + /** @type {import('postcss').Processor} */ + // @ts-ignore + let processor = postcss(plugins) + + async function readInput() { + // Piping in data, let's drain the stdin + if (input === '-') { + return drainStdin() + } + + // Input file has been provided + if (input) { + return fs.promises.readFile(path.resolve(input), 'utf8') + } + + // No input file provided, fallback to default atrules + return '@tailwind base; @tailwind components; @tailwind utilities' + } + + async function build() { + let start = process.hrtime.bigint() + + return readInput() + .then((css) => processor.process(css, { ...postcssOptions, from: input, to: output })) + .then((result) => { + if (!state.watcher) { + return result + } + + env.DEBUG && console.time('Recording PostCSS dependencies') + for (let message of result.messages) { + if (message.type === 'dependency') { + state.contextDependencies.add(message.file) + } + } + env.DEBUG && console.timeEnd('Recording PostCSS dependencies') + + // TODO: This needs to be in a different spot + env.DEBUG && console.time('Watch new files') + state.watcher.refreshWatchedFiles() + env.DEBUG && console.timeEnd('Watch new files') + + return result + }) + .then((result) => { + if (!output) { + process.stdout.write(result.css) + return + } + + return Promise.all([ + outputFile(result.opts.to, result.css), + result.map && outputFile(result.opts.to + '.map', result.map.toString()), + ]) + }) + .then(() => { + let end = process.hrtime.bigint() + console.error() + console.error('Done in', (end - start) / BigInt(1e6) + 'ms.') + }) + .then( + () => {}, + (err) => { + // TODO: If an initial build fails we can't easily pick up any PostCSS dependencies + // that were collected before the error occurred + // The result is not stored on the error so we have to store it externally + // and pull the messages off of it here somehow + + // This results in a less than ideal DX because the watcher will not pick up + // changes to imported CSS if one of them caused an error during the initial build + // If you fix it and then save the main CSS file so there's no error + // The watcher will start watching the imported CSS files and will be + // resilient to future errors. + + if (state.watcher) { + console.error(err) + } else { + return Promise.reject(err) + } + } + ) + } + + /** + * @param {{file: string, content(): Promise<string>, extension: string}[]} changes + */ + async function parseChanges(changes) { + return Promise.all( + changes.map(async (change) => ({ + content: await change.content(), + extension: change.extension, + })) + ) + } + + if (input !== undefined && input !== '-') { + state.contextDependencies.add(path.resolve(input)) + } + + return { + build, + watch: async () => { + state.watcher = createWatcher(args, { + state, + + /** + * @param {{file: string, content(): Promise<string>, extension: string}[]} changes + */ + async rebuild(changes) { + let needsNewContext = changes.some((change) => { + return ( + state.configBag?.dependencies.has(change.file) || + state.contextDependencies.has(change.file) + ) + }) + + if (needsNewContext) { + state.context = null + } else { + for (let change of await parseChanges(changes)) { + state.changedContent.push(change) + } + } + + return build() + }, + }) + + await build() + }, + } +} diff --git a/node_modules/tailwindcss/src/cli/build/utils.js b/node_modules/tailwindcss/src/cli/build/utils.js new file mode 100644 index 0000000..3462a97 --- /dev/null +++ b/node_modules/tailwindcss/src/cli/build/utils.js @@ -0,0 +1,76 @@ +// @ts-check + +import fs from 'fs' +import path from 'path' + +export function indentRecursive(node, indent = 0) { + node.each && + node.each((child, i) => { + if (!child.raws.before || !child.raws.before.trim() || child.raws.before.includes('\n')) { + child.raws.before = `\n${node.type !== 'rule' && i > 0 ? '\n' : ''}${' '.repeat(indent)}` + } + child.raws.after = `\n${' '.repeat(indent)}` + indentRecursive(child, indent + 1) + }) +} + +export function formatNodes(root) { + indentRecursive(root) + if (root.first) { + root.first.raws.before = '' + } +} + +/** + * When rapidly saving files atomically a couple of situations can happen: + * - The file is missing since the external program has deleted it by the time we've gotten around to reading it from the earlier save. + * - The file is being written to by the external program by the time we're going to read it and is thus treated as busy because a lock is held. + * + * To work around this we retry reading the file a handful of times with a delay between each attempt + * + * @param {string} path + * @param {number} tries + * @returns {Promise<string | undefined>} + * @throws {Error} If the file is still missing or busy after the specified number of tries + */ +export async function readFileWithRetries(path, tries = 5) { + for (let n = 0; n <= tries; n++) { + try { + return await fs.promises.readFile(path, 'utf8') + } catch (err) { + if (n !== tries) { + if (err.code === 'ENOENT' || err.code === 'EBUSY') { + await new Promise((resolve) => setTimeout(resolve, 10)) + + continue + } + } + + throw err + } + } +} + +export function drainStdin() { + return new Promise((resolve, reject) => { + let result = '' + process.stdin.on('data', (chunk) => { + result += chunk + }) + process.stdin.on('end', () => resolve(result)) + process.stdin.on('error', (err) => reject(err)) + }) +} + +export async function outputFile(file, newContents) { + try { + let currentContents = await fs.promises.readFile(file, 'utf8') + if (currentContents === newContents) { + return // Skip writing the file + } + } catch {} + + // Write the file + await fs.promises.mkdir(path.dirname(file), { recursive: true }) + await fs.promises.writeFile(file, newContents, 'utf8') +} diff --git a/node_modules/tailwindcss/src/cli/build/watching.js b/node_modules/tailwindcss/src/cli/build/watching.js new file mode 100644 index 0000000..b778872 --- /dev/null +++ b/node_modules/tailwindcss/src/cli/build/watching.js @@ -0,0 +1,229 @@ +// @ts-check + +import chokidar from 'chokidar' +import fs from 'fs' +import micromatch from 'micromatch' +import normalizePath from 'normalize-path' +import path from 'path' + +import { readFileWithRetries } from './utils.js' + +/** + * The core idea of this watcher is: + * 1. Whenever a file is added, changed, or renamed we queue a rebuild + * 2. Perform as few rebuilds as possible by batching them together + * 3. Coalesce events that happen in quick succession to avoid unnecessary rebuilds + * 4. Ensure another rebuild happens _if_ changed while a rebuild is in progress + */ + +/** + * + * @param {*} args + * @param {{ state, rebuild(changedFiles: any[]): Promise<any> }} param1 + * @returns {{ + * fswatcher: import('chokidar').FSWatcher, + * refreshWatchedFiles(): void, + * }} + */ +export function createWatcher(args, { state, rebuild }) { + let shouldPoll = args['--poll'] + let shouldCoalesceWriteEvents = shouldPoll || process.platform === 'win32' + + // Polling interval in milliseconds + // Used only when polling or coalescing add/change events on Windows + let pollInterval = 10 + + let watcher = chokidar.watch([], { + // Force checking for atomic writes in all situations + // This causes chokidar to wait up to 100ms for a file to re-added after it's been unlinked + // This only works when watching directories though + atomic: true, + + usePolling: shouldPoll, + interval: shouldPoll ? pollInterval : undefined, + ignoreInitial: true, + awaitWriteFinish: shouldCoalesceWriteEvents + ? { + stabilityThreshold: 50, + pollInterval: pollInterval, + } + : false, + }) + + // A queue of rebuilds, file reads, etc… to run + let chain = Promise.resolve() + + /** + * A list of files that have been changed since the last rebuild + * + * @type {{file: string, content: () => Promise<string>, extension: string}[]} + */ + let changedContent = [] + + /** + * A list of files for which a rebuild has already been queued. + * This is used to prevent duplicate rebuilds when multiple events are fired for the same file. + * The rebuilt file is cleared from this list when it's associated rebuild has _started_ + * This is because if the file is changed during a rebuild it won't trigger a new rebuild which it should + **/ + let pendingRebuilds = new Set() + + let _timer + let _reject + + /** + * Rebuilds the changed files and resolves when the rebuild is + * complete regardless of whether it was successful or not + */ + async function rebuildAndContinue() { + let changes = changedContent.splice(0) + + // There are no changes to rebuild so we can just do nothing + if (changes.length === 0) { + return Promise.resolve() + } + + // Clear all pending rebuilds for the about-to-be-built files + changes.forEach((change) => pendingRebuilds.delete(change.file)) + + // Resolve the promise even when the rebuild fails + return rebuild(changes).then( + () => {}, + (e) => { + console.error(e.toString()) + } + ) + } + + /** + * + * @param {*} file + * @param {(() => Promise<string>) | null} content + * @param {boolean} skipPendingCheck + * @returns {Promise<void>} + */ + function recordChangedFile(file, content = null, skipPendingCheck = false) { + file = path.resolve(file) + + // Applications like Vim/Neovim fire both rename and change events in succession for atomic writes + // In that case rebuild has already been queued by rename, so can be skipped in change + if (pendingRebuilds.has(file) && !skipPendingCheck) { + return Promise.resolve() + } + + // Mark that a rebuild of this file is going to happen + // It MUST happen synchronously before the rebuild is queued for this to be effective + pendingRebuilds.add(file) + + changedContent.push({ + file, + content: content ?? (() => fs.promises.readFile(file, 'utf8')), + extension: path.extname(file).slice(1), + }) + + if (_timer) { + clearTimeout(_timer) + _reject() + } + + // If a rebuild is already in progress we don't want to start another one until the 10ms timer has expired + chain = chain.then( + () => + new Promise((resolve, reject) => { + _timer = setTimeout(resolve, 10) + _reject = reject + }) + ) + + // Resolves once this file has been rebuilt (or the rebuild for this file has failed) + // This queues as many rebuilds as there are changed files + // But those rebuilds happen after some delay + // And will immediately resolve if there are no changes + chain = chain.then(rebuildAndContinue, rebuildAndContinue) + + return chain + } + + watcher.on('change', (file) => recordChangedFile(file)) + watcher.on('add', (file) => recordChangedFile(file)) + + // Restore watching any files that are "removed" + // This can happen when a file is pseudo-atomically replaced (a copy is created, overwritten, the old one is unlinked, and the new one is renamed) + // TODO: An an optimization we should allow removal when the config changes + watcher.on('unlink', (file) => { + file = normalizePath(file) + + // Only re-add the file if it's not covered by a dynamic pattern + if (!micromatch.some([file], state.contentPatterns.dynamic)) { + watcher.add(file) + } + }) + + // Some applications such as Visual Studio (but not VS Code) + // will only fire a rename event for atomic writes and not a change event + // This is very likely a chokidar bug but it's one we need to work around + // We treat this as a change event and rebuild the CSS + watcher.on('raw', (evt, filePath, meta) => { + if (evt !== 'rename' || filePath === null) { + return + } + + let watchedPath = meta.watchedPath + + // Watched path might be the file itself + // Or the directory it is in + filePath = watchedPath.endsWith(filePath) ? watchedPath : path.join(watchedPath, filePath) + + // Skip this event since the files it is for does not match any of the registered content globs + if (!micromatch.some([filePath], state.contentPatterns.all)) { + return + } + + // Skip since we've already queued a rebuild for this file that hasn't happened yet + if (pendingRebuilds.has(filePath)) { + return + } + + // We'll go ahead and add the file to the pending rebuilds list here + // It'll be removed when the rebuild starts unless the read fails + // which will be taken care of as well + pendingRebuilds.add(filePath) + + async function enqueue() { + try { + // We need to read the file as early as possible outside of the chain + // because it may be gone by the time we get to it. doing the read + // immediately increases the chance that the file is still there + let content = await readFileWithRetries(path.resolve(filePath)) + + if (content === undefined) { + return + } + + // This will push the rebuild onto the chain + // We MUST skip the rebuild check here otherwise the rebuild will never happen on Linux + // This is because the order of events and timing is different on Linux + // @ts-ignore: TypeScript isn't picking up that content is a string here + await recordChangedFile(filePath, () => content, true) + } catch { + // If reading the file fails, it's was probably a deleted temporary file + // So we can ignore it and no rebuild is needed + } + } + + enqueue().then(() => { + // If the file read fails we still need to make sure the file isn't stuck in the pending rebuilds list + pendingRebuilds.delete(filePath) + }) + }) + + return { + fswatcher: watcher, + + refreshWatchedFiles() { + watcher.add(Array.from(state.contextDependencies)) + watcher.add(Array.from(state.configBag.dependencies)) + watcher.add(state.contentPatterns.all) + }, + } +} diff --git a/node_modules/tailwindcss/src/cli/help/index.js b/node_modules/tailwindcss/src/cli/help/index.js new file mode 100644 index 0000000..ea4137a --- /dev/null +++ b/node_modules/tailwindcss/src/cli/help/index.js @@ -0,0 +1,70 @@ +// @ts-check +import packageJson from '../../../package.json' + +export function help({ message, usage, commands, options }) { + let indent = 2 + + // Render header + console.log() + console.log(`${packageJson.name} v${packageJson.version}`) + + // Render message + if (message) { + console.log() + for (let msg of message.split('\n')) { + console.log(msg) + } + } + + // Render usage + if (usage && usage.length > 0) { + console.log() + console.log('Usage:') + for (let example of usage) { + console.log(' '.repeat(indent), example) + } + } + + // Render commands + if (commands && commands.length > 0) { + console.log() + console.log('Commands:') + for (let command of commands) { + console.log(' '.repeat(indent), command) + } + } + + // Render options + if (options) { + let groupedOptions = {} + for (let [key, value] of Object.entries(options)) { + if (typeof value === 'object') { + groupedOptions[key] = { ...value, flags: [key] } + } else { + groupedOptions[value].flags.push(key) + } + } + + console.log() + console.log('Options:') + for (let { flags, description, deprecated } of Object.values(groupedOptions)) { + if (deprecated) continue + + if (flags.length === 1) { + console.log( + ' '.repeat(indent + 4 /* 4 = "-i, ".length */), + flags.slice().reverse().join(', ').padEnd(20, ' '), + description + ) + } else { + console.log( + ' '.repeat(indent), + flags.slice().reverse().join(', ').padEnd(24, ' '), + description + ) + } + } + } + + console.log() +} diff --git a/node_modules/tailwindcss/src/cli/index.js b/node_modules/tailwindcss/src/cli/index.js new file mode 100644 index 0000000..fc1497f --- /dev/null +++ b/node_modules/tailwindcss/src/cli/index.js @@ -0,0 +1,216 @@ +#!/usr/bin/env node + +import path from 'path' +import arg from 'arg' +import fs from 'fs' + +import { build } from './build' +import { help } from './help' +import { init } from './init' + +function oneOf(...options) { + return Object.assign( + (value = true) => { + for (let option of options) { + let parsed = option(value) + if (parsed === value) { + return parsed + } + } + + throw new Error('...') + }, + { manualParsing: true } + ) +} + +let commands = { + init: { + run: init, + args: { + '--esm': { type: Boolean, description: `Initialize configuration file as ESM` }, + '--ts': { type: Boolean, description: `Initialize configuration file as TypeScript` }, + '--postcss': { type: Boolean, description: `Initialize a \`postcss.config.js\` file` }, + '--full': { + type: Boolean, + description: `Include the default values for all options in the generated configuration file`, + }, + '-f': '--full', + '-p': '--postcss', + }, + }, + build: { + run: build, + args: { + '--input': { type: String, description: 'Input file' }, + '--output': { type: String, description: 'Output file' }, + '--watch': { + type: oneOf(String, Boolean), + description: 'Watch for changes and rebuild as needed', + }, + '--poll': { + type: Boolean, + description: 'Use polling instead of filesystem events when watching', + }, + '--content': { + type: String, + description: 'Content paths to use for removing unused classes', + }, + '--purge': { + type: String, + deprecated: true, + }, + '--postcss': { + type: oneOf(String, Boolean), + description: 'Load custom PostCSS configuration', + }, + '--minify': { type: Boolean, description: 'Minify the output' }, + '--config': { + type: String, + description: 'Path to a custom config file', + }, + '--no-autoprefixer': { + type: Boolean, + description: 'Disable autoprefixer', + }, + '-c': '--config', + '-i': '--input', + '-o': '--output', + '-m': '--minify', + '-w': '--watch', + '-p': '--poll', + }, + }, +} + +let sharedFlags = { + '--help': { type: Boolean, description: 'Display usage information' }, + '-h': '--help', +} + +if ( + process.stdout.isTTY /* Detect redirecting output to a file */ && + (process.argv[2] === undefined || + process.argv.slice(2).every((flag) => sharedFlags[flag] !== undefined)) +) { + help({ + usage: [ + 'tailwindcss [--input input.css] [--output output.css] [--watch] [options...]', + 'tailwindcss init [--full] [--postcss] [options...]', + ], + commands: Object.keys(commands) + .filter((command) => command !== 'build') + .map((command) => `${command} [options]`), + options: { ...commands.build.args, ...sharedFlags }, + }) + process.exit(0) +} + +let command = ((arg = '') => (arg.startsWith('-') ? undefined : arg))(process.argv[2]) || 'build' + +if (commands[command] === undefined) { + if (fs.existsSync(path.resolve(command))) { + // TODO: Deprecate this in future versions + // Check if non-existing command, might be a file. + command = 'build' + } else { + help({ + message: `Invalid command: ${command}`, + usage: ['tailwindcss <command> [options]'], + commands: Object.keys(commands) + .filter((command) => command !== 'build') + .map((command) => `${command} [options]`), + options: sharedFlags, + }) + process.exit(1) + } +} + +// Execute command +let { args: flags, run } = commands[command] +let args = (() => { + try { + let result = arg( + Object.fromEntries( + Object.entries({ ...flags, ...sharedFlags }) + .filter(([_key, value]) => !value?.type?.manualParsing) + .map(([key, value]) => [key, typeof value === 'object' ? value.type : value]) + ), + { permissive: true } + ) + + // Manual parsing of flags to allow for special flags like oneOf(Boolean, String) + for (let i = result['_'].length - 1; i >= 0; --i) { + let flag = result['_'][i] + if (!flag.startsWith('-')) continue + + let [flagName, flagValue] = flag.split('=') + let handler = flags[flagName] + + // Resolve flagName & handler + while (typeof handler === 'string') { + flagName = handler + handler = flags[handler] + } + + if (!handler) continue + + let args = [] + let offset = i + 1 + + // --flag value syntax was used so we need to pull `value` from `args` + if (flagValue === undefined) { + // Parse args for current flag + while (result['_'][offset] && !result['_'][offset].startsWith('-')) { + args.push(result['_'][offset++]) + } + + // Cleanup manually parsed flags + args + result['_'].splice(i, 1 + args.length) + + // No args were provided, use default value defined in handler + // One arg was provided, use that directly + // Multiple args were provided so pass them all in an array + flagValue = args.length === 0 ? undefined : args.length === 1 ? args[0] : args + } else { + // Remove the whole flag from the args array + result['_'].splice(i, 1) + } + + // Set the resolved value in the `result` object + result[flagName] = handler.type(flagValue, flagName) + } + + // Ensure that the `command` is always the first argument in the `args`. + // This is important so that we don't have to check if a default command + // (build) was used or not from within each plugin. + // + // E.g.: tailwindcss input.css -> _: ['build', 'input.css'] + // E.g.: tailwindcss build input.css -> _: ['build', 'input.css'] + if (result['_'][0] !== command) { + result['_'].unshift(command) + } + + return result + } catch (err) { + if (err.code === 'ARG_UNKNOWN_OPTION') { + help({ + message: err.message, + usage: ['tailwindcss <command> [options]'], + options: sharedFlags, + }) + process.exit(1) + } + throw err + } +})() + +if (args['--help']) { + help({ + options: { ...flags, ...sharedFlags }, + usage: [`tailwindcss ${command} [options]`], + }) + process.exit(0) +} + +run(args) diff --git a/node_modules/tailwindcss/src/cli/init/index.js b/node_modules/tailwindcss/src/cli/init/index.js new file mode 100644 index 0000000..6bd7e41 --- /dev/null +++ b/node_modules/tailwindcss/src/cli/init/index.js @@ -0,0 +1,79 @@ +// @ts-check + +import fs from 'fs' +import path from 'path' + +function isESM() { + const pkgPath = path.resolve('./package.json') + + try { + let pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')) + return pkg.type && pkg.type === 'module' + } catch (err) { + return false + } +} + +export function init(args) { + let messages = [] + + let isProjectESM = args['--ts'] || args['--esm'] || isESM() + let syntax = args['--ts'] ? 'ts' : isProjectESM ? 'js' : 'cjs' + let extension = args['--ts'] ? 'ts' : 'js' + + let tailwindConfigLocation = path.resolve(args['_'][1] ?? `./tailwind.config.${extension}`) + + if (fs.existsSync(tailwindConfigLocation)) { + messages.push(`${path.basename(tailwindConfigLocation)} already exists.`) + } else { + let stubContentsFile = fs.readFileSync( + args['--full'] + ? path.resolve(__dirname, '../../../stubs/config.full.js') + : path.resolve(__dirname, '../../../stubs/config.simple.js'), + 'utf8' + ) + + let stubFile = fs.readFileSync( + path.resolve(__dirname, `../../../stubs/tailwind.config.${syntax}`), + 'utf8' + ) + + // Change colors import + stubContentsFile = stubContentsFile.replace('../colors', 'tailwindcss/colors') + + // Replace contents of {ts,js,cjs} file with the stub {simple,full}. + stubFile = + stubFile + .replace('__CONFIG__', stubContentsFile.replace('module.exports =', '').trim()) + .trim() + '\n\n' + + fs.writeFileSync(tailwindConfigLocation, stubFile, 'utf8') + + messages.push(`Created Tailwind CSS config file: ${path.basename(tailwindConfigLocation)}`) + } + + if (args['--postcss']) { + let postcssConfigLocation = path.resolve('./postcss.config.js') + if (fs.existsSync(postcssConfigLocation)) { + messages.push(`${path.basename(postcssConfigLocation)} already exists.`) + } else { + let stubFile = fs.readFileSync( + isProjectESM + ? path.resolve(__dirname, '../../../stubs/postcss.config.js') + : path.resolve(__dirname, '../../../stubs/postcss.config.cjs'), + 'utf8' + ) + + fs.writeFileSync(postcssConfigLocation, stubFile, 'utf8') + + messages.push(`Created PostCSS config file: ${path.basename(postcssConfigLocation)}`) + } + } + + if (messages.length > 0) { + console.log() + for (let message of messages) { + console.log(message) + } + } +} diff --git a/node_modules/tailwindcss/src/corePluginList.js b/node_modules/tailwindcss/src/corePluginList.js new file mode 100644 index 0000000..f172ceb --- /dev/null +++ b/node_modules/tailwindcss/src/corePluginList.js @@ -0,0 +1 @@ +export default ["preflight","container","accessibility","pointerEvents","visibility","position","inset","isolation","zIndex","order","gridColumn","gridColumnStart","gridColumnEnd","gridRow","gridRowStart","gridRowEnd","float","clear","margin","boxSizing","lineClamp","display","aspectRatio","height","maxHeight","minHeight","width","minWidth","maxWidth","flex","flexShrink","flexGrow","flexBasis","tableLayout","captionSide","borderCollapse","borderSpacing","transformOrigin","translate","rotate","skew","scale","transform","animation","cursor","touchAction","userSelect","resize","scrollSnapType","scrollSnapAlign","scrollSnapStop","scrollMargin","scrollPadding","listStylePosition","listStyleType","listStyleImage","appearance","columns","breakBefore","breakInside","breakAfter","gridAutoColumns","gridAutoFlow","gridAutoRows","gridTemplateColumns","gridTemplateRows","flexDirection","flexWrap","placeContent","placeItems","alignContent","alignItems","justifyContent","justifyItems","gap","space","divideWidth","divideStyle","divideColor","divideOpacity","placeSelf","alignSelf","justifySelf","overflow","overscrollBehavior","scrollBehavior","textOverflow","hyphens","whitespace","wordBreak","borderRadius","borderWidth","borderStyle","borderColor","borderOpacity","backgroundColor","backgroundOpacity","backgroundImage","gradientColorStops","boxDecorationBreak","backgroundSize","backgroundAttachment","backgroundClip","backgroundPosition","backgroundRepeat","backgroundOrigin","fill","stroke","strokeWidth","objectFit","objectPosition","padding","textAlign","textIndent","verticalAlign","fontFamily","fontSize","fontWeight","textTransform","fontStyle","fontVariantNumeric","lineHeight","letterSpacing","textColor","textOpacity","textDecoration","textDecorationColor","textDecorationStyle","textDecorationThickness","textUnderlineOffset","fontSmoothing","placeholderColor","placeholderOpacity","caretColor","accentColor","opacity","backgroundBlendMode","mixBlendMode","boxShadow","boxShadowColor","outlineStyle","outlineWidth","outlineOffset","outlineColor","ringWidth","ringColor","ringOpacity","ringOffsetWidth","ringOffsetColor","blur","brightness","contrast","dropShadow","grayscale","hueRotate","invert","saturate","sepia","filter","backdropBlur","backdropBrightness","backdropContrast","backdropGrayscale","backdropHueRotate","backdropInvert","backdropOpacity","backdropSaturate","backdropSepia","backdropFilter","transitionProperty","transitionDelay","transitionDuration","transitionTimingFunction","willChange","content"]
\ No newline at end of file diff --git a/node_modules/tailwindcss/src/corePlugins.js b/node_modules/tailwindcss/src/corePlugins.js new file mode 100644 index 0000000..5db1fdb --- /dev/null +++ b/node_modules/tailwindcss/src/corePlugins.js @@ -0,0 +1,2855 @@ +import fs from 'fs' +import * as path from 'path' +import postcss from 'postcss' +import createUtilityPlugin from './util/createUtilityPlugin' +import buildMediaQuery from './util/buildMediaQuery' +import escapeClassName from './util/escapeClassName' +import parseAnimationValue from './util/parseAnimationValue' +import flattenColorPalette from './util/flattenColorPalette' +import withAlphaVariable, { withAlphaValue } from './util/withAlphaVariable' +import toColorValue from './util/toColorValue' +import isPlainObject from './util/isPlainObject' +import transformThemeValue from './util/transformThemeValue' +import { version as tailwindVersion } from '../package.json' +import log from './util/log' +import { + normalizeScreens, + isScreenSortable, + compareScreens, + toScreen, +} from './util/normalizeScreens' +import { formatBoxShadowValue, parseBoxShadowValue } from './util/parseBoxShadowValue' +import { removeAlphaVariables } from './util/removeAlphaVariables' +import { flagEnabled } from './featureFlags' +import { normalize } from './util/dataTypes' +import { INTERNAL_FEATURES } from './lib/setupContextUtils' + +export let variantPlugins = { + pseudoElementVariants: ({ addVariant }) => { + addVariant('first-letter', '&::first-letter') + addVariant('first-line', '&::first-line') + + addVariant('marker', [ + ({ container }) => { + removeAlphaVariables(container, ['--tw-text-opacity']) + + return '& *::marker' + }, + ({ container }) => { + removeAlphaVariables(container, ['--tw-text-opacity']) + + return '&::marker' + }, + ]) + + addVariant('selection', ['& *::selection', '&::selection']) + + addVariant('file', '&::file-selector-button') + + addVariant('placeholder', '&::placeholder') + + addVariant('backdrop', '&::backdrop') + + addVariant('before', ({ container }) => { + container.walkRules((rule) => { + let foundContent = false + rule.walkDecls('content', () => { + foundContent = true + }) + + if (!foundContent) { + rule.prepend(postcss.decl({ prop: 'content', value: 'var(--tw-content)' })) + } + }) + + return '&::before' + }) + + addVariant('after', ({ container }) => { + container.walkRules((rule) => { + let foundContent = false + rule.walkDecls('content', () => { + foundContent = true + }) + + if (!foundContent) { + rule.prepend(postcss.decl({ prop: 'content', value: 'var(--tw-content)' })) + } + }) + + return '&::after' + }) + }, + + pseudoClassVariants: ({ addVariant, matchVariant, config, prefix }) => { + let pseudoVariants = [ + // Positional + ['first', '&:first-child'], + ['last', '&:last-child'], + ['only', '&:only-child'], + ['odd', '&:nth-child(odd)'], + ['even', '&:nth-child(even)'], + 'first-of-type', + 'last-of-type', + 'only-of-type', + + // State + [ + 'visited', + ({ container }) => { + removeAlphaVariables(container, [ + '--tw-text-opacity', + '--tw-border-opacity', + '--tw-bg-opacity', + ]) + + return '&:visited' + }, + ], + 'target', + ['open', '&[open]'], + + // Forms + 'default', + 'checked', + 'indeterminate', + 'placeholder-shown', + 'autofill', + 'optional', + 'required', + 'valid', + 'invalid', + 'in-range', + 'out-of-range', + 'read-only', + + // Content + 'empty', + + // Interactive + 'focus-within', + [ + 'hover', + !flagEnabled(config(), 'hoverOnlyWhenSupported') + ? '&:hover' + : '@media (hover: hover) and (pointer: fine) { &:hover }', + ], + 'focus', + 'focus-visible', + 'active', + 'enabled', + 'disabled', + ].map((variant) => (Array.isArray(variant) ? variant : [variant, `&:${variant}`])) + + for (let [variantName, state] of pseudoVariants) { + addVariant(variantName, (ctx) => { + let result = typeof state === 'function' ? state(ctx) : state + + return result + }) + } + + let variants = { + group: (_, { modifier }) => + modifier + ? [`:merge(${prefix('.group')}\\/${escapeClassName(modifier)})`, ' &'] + : [`:merge(${prefix('.group')})`, ' &'], + peer: (_, { modifier }) => + modifier + ? [`:merge(${prefix('.peer')}\\/${escapeClassName(modifier)})`, ' ~ &'] + : [`:merge(${prefix('.peer')})`, ' ~ &'], + } + + for (let [name, fn] of Object.entries(variants)) { + matchVariant( + name, + (value = '', extra) => { + let result = normalize(typeof value === 'function' ? value(extra) : value) + if (!result.includes('&')) result = '&' + result + + let [a, b] = fn('', extra) + + let start = null + let end = null + let quotes = 0 + + for (let i = 0; i < result.length; ++i) { + let c = result[i] + if (c === '&') { + start = i + } else if (c === "'" || c === '"') { + quotes += 1 + } else if (start !== null && c === ' ' && !quotes) { + end = i + } + } + + if (start !== null && end === null) { + end = result.length + } + + // Basically this but can handle quotes: + // result.replace(/&(\S+)?/g, (_, pseudo = '') => a + pseudo + b) + + return result.slice(0, start) + a + result.slice(start + 1, end) + b + result.slice(end) + }, + { + values: Object.fromEntries(pseudoVariants), + [INTERNAL_FEATURES]: { + respectPrefix: false, + }, + } + ) + } + }, + + directionVariants: ({ addVariant }) => { + addVariant('ltr', ':is([dir="ltr"] &)') + addVariant('rtl', ':is([dir="rtl"] &)') + }, + + reducedMotionVariants: ({ addVariant }) => { + addVariant('motion-safe', '@media (prefers-reduced-motion: no-preference)') + addVariant('motion-reduce', '@media (prefers-reduced-motion: reduce)') + }, + + darkVariants: ({ config, addVariant }) => { + let [mode, className = '.dark'] = [].concat(config('darkMode', 'media')) + + if (mode === false) { + mode = 'media' + log.warn('darkmode-false', [ + 'The `darkMode` option in your Tailwind CSS configuration is set to `false`, which now behaves the same as `media`.', + 'Change `darkMode` to `media` or remove it entirely.', + 'https://tailwindcss.com/docs/upgrade-guide#remove-dark-mode-configuration', + ]) + } + + if (mode === 'class') { + addVariant('dark', `:is(${className} &)`) + } else if (mode === 'media') { + addVariant('dark', '@media (prefers-color-scheme: dark)') + } + }, + + printVariant: ({ addVariant }) => { + addVariant('print', '@media print') + }, + + screenVariants: ({ theme, addVariant, matchVariant }) => { + let rawScreens = theme('screens') ?? {} + let areSimpleScreens = Object.values(rawScreens).every((v) => typeof v === 'string') + let screens = normalizeScreens(theme('screens')) + + /** @type {Set<string>} */ + let unitCache = new Set([]) + + /** @param {string} value */ + function units(value) { + return value.match(/(\D+)$/)?.[1] ?? '(none)' + } + + /** @param {string} value */ + function recordUnits(value) { + if (value !== undefined) { + unitCache.add(units(value)) + } + } + + /** @param {string} value */ + function canUseUnits(value) { + recordUnits(value) + + // If the cache was empty it'll become 1 because we've just added the current unit + // If the cache was not empty and the units are the same the size doesn't change + // Otherwise, if the units are different from what is already known the size will always be > 1 + return unitCache.size === 1 + } + + for (const screen of screens) { + for (const value of screen.values) { + recordUnits(value.min) + recordUnits(value.max) + } + } + + let screensUseConsistentUnits = unitCache.size <= 1 + + /** + * @typedef {import('./util/normalizeScreens').Screen} Screen + */ + + /** + * @param {'min' | 'max'} type + * @returns {Record<string, Screen>} + */ + function buildScreenValues(type) { + return Object.fromEntries( + screens + .filter((screen) => isScreenSortable(screen).result) + .map((screen) => { + let { min, max } = screen.values[0] + + if (type === 'min' && min !== undefined) { + return screen + } else if (type === 'min' && max !== undefined) { + return { ...screen, not: !screen.not } + } else if (type === 'max' && max !== undefined) { + return screen + } else if (type === 'max' && min !== undefined) { + return { ...screen, not: !screen.not } + } + }) + .map((screen) => [screen.name, screen]) + ) + } + + /** + * @param {'min' | 'max'} type + * @returns {(a: { value: string | Screen }, z: { value: string | Screen }) => number} + */ + function buildSort(type) { + return (a, z) => compareScreens(type, a.value, z.value) + } + + let maxSort = buildSort('max') + let minSort = buildSort('min') + + /** @param {'min'|'max'} type */ + function buildScreenVariant(type) { + return (value) => { + if (!areSimpleScreens) { + log.warn('complex-screen-config', [ + 'The `min-*` and `max-*` variants are not supported with a `screens` configuration containing objects.', + ]) + + return [] + } else if (!screensUseConsistentUnits) { + log.warn('mixed-screen-units', [ + 'The `min-*` and `max-*` variants are not supported with a `screens` configuration containing mixed units.', + ]) + + return [] + } else if (typeof value === 'string' && !canUseUnits(value)) { + log.warn('minmax-have-mixed-units', [ + 'The `min-*` and `max-*` variants are not supported with a `screens` configuration containing mixed units.', + ]) + + return [] + } + + return [`@media ${buildMediaQuery(toScreen(value, type))}`] + } + } + + matchVariant('max', buildScreenVariant('max'), { + sort: maxSort, + values: areSimpleScreens ? buildScreenValues('max') : {}, + }) + + // screens and min-* are sorted together when they can be + let id = 'min-screens' + for (let screen of screens) { + addVariant(screen.name, `@media ${buildMediaQuery(screen)}`, { + id, + sort: areSimpleScreens && screensUseConsistentUnits ? minSort : undefined, + value: screen, + }) + } + + matchVariant('min', buildScreenVariant('min'), { + id, + sort: minSort, + }) + }, + + supportsVariants: ({ matchVariant, theme }) => { + matchVariant( + 'supports', + (value = '') => { + let check = normalize(value) + let isRaw = /^\w*\s*\(/.test(check) + + // Chrome has a bug where `(condtion1)or(condition2)` is not valid + // But `(condition1) or (condition2)` is supported. + check = isRaw ? check.replace(/\b(and|or|not)\b/g, ' $1 ') : check + + if (isRaw) { + return `@supports ${check}` + } + + if (!check.includes(':')) { + check = `${check}: var(--tw)` + } + + if (!(check.startsWith('(') && check.endsWith(')'))) { + check = `(${check})` + } + + return `@supports ${check}` + }, + { values: theme('supports') ?? {} } + ) + }, + + ariaVariants: ({ matchVariant, theme }) => { + matchVariant('aria', (value) => `&[aria-${normalize(value)}]`, { values: theme('aria') ?? {} }) + matchVariant( + 'group-aria', + (value, { modifier }) => + modifier + ? `:merge(.group\\/${modifier})[aria-${normalize(value)}] &` + : `:merge(.group)[aria-${normalize(value)}] &`, + { values: theme('aria') ?? {} } + ) + matchVariant( + 'peer-aria', + (value, { modifier }) => + modifier + ? `:merge(.peer\\/${modifier})[aria-${normalize(value)}] ~ &` + : `:merge(.peer)[aria-${normalize(value)}] ~ &`, + { values: theme('aria') ?? {} } + ) + }, + + dataVariants: ({ matchVariant, theme }) => { + matchVariant('data', (value) => `&[data-${normalize(value)}]`, { values: theme('data') ?? {} }) + matchVariant( + 'group-data', + (value, { modifier }) => + modifier + ? `:merge(.group\\/${modifier})[data-${normalize(value)}] &` + : `:merge(.group)[data-${normalize(value)}] &`, + { values: theme('data') ?? {} } + ) + matchVariant( + 'peer-data', + (value, { modifier }) => + modifier + ? `:merge(.peer\\/${modifier})[data-${normalize(value)}] ~ &` + : `:merge(.peer)[data-${normalize(value)}] ~ &`, + { values: theme('data') ?? {} } + ) + }, + + orientationVariants: ({ addVariant }) => { + addVariant('portrait', '@media (orientation: portrait)') + addVariant('landscape', '@media (orientation: landscape)') + }, + + prefersContrastVariants: ({ addVariant }) => { + addVariant('contrast-more', '@media (prefers-contrast: more)') + addVariant('contrast-less', '@media (prefers-contrast: less)') + }, +} + +let cssTransformValue = [ + 'translate(var(--tw-translate-x), var(--tw-translate-y))', + 'rotate(var(--tw-rotate))', + 'skewX(var(--tw-skew-x))', + 'skewY(var(--tw-skew-y))', + 'scaleX(var(--tw-scale-x))', + 'scaleY(var(--tw-scale-y))', +].join(' ') + +let cssFilterValue = [ + 'var(--tw-blur)', + 'var(--tw-brightness)', + 'var(--tw-contrast)', + 'var(--tw-grayscale)', + 'var(--tw-hue-rotate)', + 'var(--tw-invert)', + 'var(--tw-saturate)', + 'var(--tw-sepia)', + 'var(--tw-drop-shadow)', +].join(' ') + +let cssBackdropFilterValue = [ + 'var(--tw-backdrop-blur)', + 'var(--tw-backdrop-brightness)', + 'var(--tw-backdrop-contrast)', + 'var(--tw-backdrop-grayscale)', + 'var(--tw-backdrop-hue-rotate)', + 'var(--tw-backdrop-invert)', + 'var(--tw-backdrop-opacity)', + 'var(--tw-backdrop-saturate)', + 'var(--tw-backdrop-sepia)', +].join(' ') + +export let corePlugins = { + preflight: ({ addBase }) => { + let preflightStyles = postcss.parse( + fs.readFileSync(path.join(__dirname, './css/preflight.css'), 'utf8') + ) + + addBase([ + postcss.comment({ + text: `! tailwindcss v${tailwindVersion} | MIT License | https://tailwindcss.com`, + }), + ...preflightStyles.nodes, + ]) + }, + + container: (() => { + function extractMinWidths(breakpoints = []) { + return breakpoints + .flatMap((breakpoint) => breakpoint.values.map((breakpoint) => breakpoint.min)) + .filter((v) => v !== undefined) + } + + function mapMinWidthsToPadding(minWidths, screens, paddings) { + if (typeof paddings === 'undefined') { + return [] + } + + if (!(typeof paddings === 'object' && paddings !== null)) { + return [ + { + screen: 'DEFAULT', + minWidth: 0, + padding: paddings, + }, + ] + } + + let mapping = [] + + if (paddings.DEFAULT) { + mapping.push({ + screen: 'DEFAULT', + minWidth: 0, + padding: paddings.DEFAULT, + }) + } + + for (let minWidth of minWidths) { + for (let screen of screens) { + for (let { min } of screen.values) { + if (min === minWidth) { + mapping.push({ minWidth, padding: paddings[screen.name] }) + } + } + } + } + + return mapping + } + + return function ({ addComponents, theme }) { + let screens = normalizeScreens(theme('container.screens', theme('screens'))) + let minWidths = extractMinWidths(screens) + let paddings = mapMinWidthsToPadding(minWidths, screens, theme('container.padding')) + + let generatePaddingFor = (minWidth) => { + let paddingConfig = paddings.find((padding) => padding.minWidth === minWidth) + + if (!paddingConfig) { + return {} + } + + return { + paddingRight: paddingConfig.padding, + paddingLeft: paddingConfig.padding, + } + } + + let atRules = Array.from( + new Set(minWidths.slice().sort((a, z) => parseInt(a) - parseInt(z))) + ).map((minWidth) => ({ + [`@media (min-width: ${minWidth})`]: { + '.container': { + 'max-width': minWidth, + ...generatePaddingFor(minWidth), + }, + }, + })) + + addComponents([ + { + '.container': Object.assign( + { width: '100%' }, + theme('container.center', false) ? { marginRight: 'auto', marginLeft: 'auto' } : {}, + generatePaddingFor(0) + ), + }, + ...atRules, + ]) + } + })(), + + accessibility: ({ addUtilities }) => { + addUtilities({ + '.sr-only': { + position: 'absolute', + width: '1px', + height: '1px', + padding: '0', + margin: '-1px', + overflow: 'hidden', + clip: 'rect(0, 0, 0, 0)', + whiteSpace: 'nowrap', + borderWidth: '0', + }, + '.not-sr-only': { + position: 'static', + width: 'auto', + height: 'auto', + padding: '0', + margin: '0', + overflow: 'visible', + clip: 'auto', + whiteSpace: 'normal', + }, + }) + }, + + pointerEvents: ({ addUtilities }) => { + addUtilities({ + '.pointer-events-none': { 'pointer-events': 'none' }, + '.pointer-events-auto': { 'pointer-events': 'auto' }, + }) + }, + + visibility: ({ addUtilities }) => { + addUtilities({ + '.visible': { visibility: 'visible' }, + '.invisible': { visibility: 'hidden' }, + '.collapse': { visibility: 'collapse' }, + }) + }, + + position: ({ addUtilities }) => { + addUtilities({ + '.static': { position: 'static' }, + '.fixed': { position: 'fixed' }, + '.absolute': { position: 'absolute' }, + '.relative': { position: 'relative' }, + '.sticky': { position: 'sticky' }, + }) + }, + + inset: createUtilityPlugin( + 'inset', + [ + ['inset', ['inset']], + [ + ['inset-x', ['left', 'right']], + ['inset-y', ['top', 'bottom']], + ], + [ + ['start', ['inset-inline-start']], + ['end', ['inset-inline-end']], + ['top', ['top']], + ['right', ['right']], + ['bottom', ['bottom']], + ['left', ['left']], + ], + ], + { supportsNegativeValues: true } + ), + + isolation: ({ addUtilities }) => { + addUtilities({ + '.isolate': { isolation: 'isolate' }, + '.isolation-auto': { isolation: 'auto' }, + }) + }, + + zIndex: createUtilityPlugin('zIndex', [['z', ['zIndex']]], { supportsNegativeValues: true }), + order: createUtilityPlugin('order', undefined, { supportsNegativeValues: true }), + gridColumn: createUtilityPlugin('gridColumn', [['col', ['gridColumn']]]), + gridColumnStart: createUtilityPlugin('gridColumnStart', [['col-start', ['gridColumnStart']]]), + gridColumnEnd: createUtilityPlugin('gridColumnEnd', [['col-end', ['gridColumnEnd']]]), + gridRow: createUtilityPlugin('gridRow', [['row', ['gridRow']]]), + gridRowStart: createUtilityPlugin('gridRowStart', [['row-start', ['gridRowStart']]]), + gridRowEnd: createUtilityPlugin('gridRowEnd', [['row-end', ['gridRowEnd']]]), + + float: ({ addUtilities }) => { + addUtilities({ + '.float-right': { float: 'right' }, + '.float-left': { float: 'left' }, + '.float-none': { float: 'none' }, + }) + }, + + clear: ({ addUtilities }) => { + addUtilities({ + '.clear-left': { clear: 'left' }, + '.clear-right': { clear: 'right' }, + '.clear-both': { clear: 'both' }, + '.clear-none': { clear: 'none' }, + }) + }, + + margin: createUtilityPlugin( + 'margin', + [ + ['m', ['margin']], + [ + ['mx', ['margin-left', 'margin-right']], + ['my', ['margin-top', 'margin-bottom']], + ], + [ + ['ms', ['margin-inline-start']], + ['me', ['margin-inline-end']], + ['mt', ['margin-top']], + ['mr', ['margin-right']], + ['mb', ['margin-bottom']], + ['ml', ['margin-left']], + ], + ], + { supportsNegativeValues: true } + ), + + boxSizing: ({ addUtilities }) => { + addUtilities({ + '.box-border': { 'box-sizing': 'border-box' }, + '.box-content': { 'box-sizing': 'content-box' }, + }) + }, + + lineClamp: ({ matchUtilities, addUtilities, theme }) => { + matchUtilities( + { + 'line-clamp': (value) => ({ + overflow: 'hidden', + display: '-webkit-box', + '-webkit-box-orient': 'vertical', + '-webkit-line-clamp': `${value}`, + }), + }, + { values: theme('lineClamp') } + ) + + addUtilities({ + '.line-clamp-none': { + overflow: 'visible', + display: 'block', + '-webkit-box-orient': 'horizontal', + '-webkit-line-clamp': 'none', + }, + }) + }, + + display: ({ addUtilities }) => { + addUtilities({ + '.block': { display: 'block' }, + '.inline-block': { display: 'inline-block' }, + '.inline': { display: 'inline' }, + '.flex': { display: 'flex' }, + '.inline-flex': { display: 'inline-flex' }, + '.table': { display: 'table' }, + '.inline-table': { display: 'inline-table' }, + '.table-caption': { display: 'table-caption' }, + '.table-cell': { display: 'table-cell' }, + '.table-column': { display: 'table-column' }, + '.table-column-group': { display: 'table-column-group' }, + '.table-footer-group': { display: 'table-footer-group' }, + '.table-header-group': { display: 'table-header-group' }, + '.table-row-group': { display: 'table-row-group' }, + '.table-row': { display: 'table-row' }, + '.flow-root': { display: 'flow-root' }, + '.grid': { display: 'grid' }, + '.inline-grid': { display: 'inline-grid' }, + '.contents': { display: 'contents' }, + '.list-item': { display: 'list-item' }, + '.hidden': { display: 'none' }, + }) + }, + + aspectRatio: createUtilityPlugin('aspectRatio', [['aspect', ['aspect-ratio']]]), + + height: createUtilityPlugin('height', [['h', ['height']]]), + maxHeight: createUtilityPlugin('maxHeight', [['max-h', ['maxHeight']]]), + minHeight: createUtilityPlugin('minHeight', [['min-h', ['minHeight']]]), + + width: createUtilityPlugin('width', [['w', ['width']]]), + minWidth: createUtilityPlugin('minWidth', [['min-w', ['minWidth']]]), + maxWidth: createUtilityPlugin('maxWidth', [['max-w', ['maxWidth']]]), + + flex: createUtilityPlugin('flex'), + flexShrink: createUtilityPlugin('flexShrink', [ + ['flex-shrink', ['flex-shrink']], // Deprecated + ['shrink', ['flex-shrink']], + ]), + flexGrow: createUtilityPlugin('flexGrow', [ + ['flex-grow', ['flex-grow']], // Deprecated + ['grow', ['flex-grow']], + ]), + flexBasis: createUtilityPlugin('flexBasis', [['basis', ['flex-basis']]]), + + tableLayout: ({ addUtilities }) => { + addUtilities({ + '.table-auto': { 'table-layout': 'auto' }, + '.table-fixed': { 'table-layout': 'fixed' }, + }) + }, + + captionSide: ({ addUtilities }) => { + addUtilities({ + '.caption-top': { 'caption-side': 'top' }, + '.caption-bottom': { 'caption-side': 'bottom' }, + }) + }, + + borderCollapse: ({ addUtilities }) => { + addUtilities({ + '.border-collapse': { 'border-collapse': 'collapse' }, + '.border-separate': { 'border-collapse': 'separate' }, + }) + }, + + borderSpacing: ({ addDefaults, matchUtilities, theme }) => { + addDefaults('border-spacing', { + '--tw-border-spacing-x': 0, + '--tw-border-spacing-y': 0, + }) + + matchUtilities( + { + 'border-spacing': (value) => { + return { + '--tw-border-spacing-x': value, + '--tw-border-spacing-y': value, + '@defaults border-spacing': {}, + 'border-spacing': 'var(--tw-border-spacing-x) var(--tw-border-spacing-y)', + } + }, + 'border-spacing-x': (value) => { + return { + '--tw-border-spacing-x': value, + '@defaults border-spacing': {}, + 'border-spacing': 'var(--tw-border-spacing-x) var(--tw-border-spacing-y)', + } + }, + 'border-spacing-y': (value) => { + return { + '--tw-border-spacing-y': value, + '@defaults border-spacing': {}, + 'border-spacing': 'var(--tw-border-spacing-x) var(--tw-border-spacing-y)', + } + }, + }, + { values: theme('borderSpacing') } + ) + }, + + transformOrigin: createUtilityPlugin('transformOrigin', [['origin', ['transformOrigin']]]), + translate: createUtilityPlugin( + 'translate', + [ + [ + [ + 'translate-x', + [['@defaults transform', {}], '--tw-translate-x', ['transform', cssTransformValue]], + ], + [ + 'translate-y', + [['@defaults transform', {}], '--tw-translate-y', ['transform', cssTransformValue]], + ], + ], + ], + { supportsNegativeValues: true } + ), + rotate: createUtilityPlugin( + 'rotate', + [['rotate', [['@defaults transform', {}], '--tw-rotate', ['transform', cssTransformValue]]]], + { supportsNegativeValues: true } + ), + skew: createUtilityPlugin( + 'skew', + [ + [ + ['skew-x', [['@defaults transform', {}], '--tw-skew-x', ['transform', cssTransformValue]]], + ['skew-y', [['@defaults transform', {}], '--tw-skew-y', ['transform', cssTransformValue]]], + ], + ], + { supportsNegativeValues: true } + ), + scale: createUtilityPlugin( + 'scale', + [ + [ + 'scale', + [ + ['@defaults transform', {}], + '--tw-scale-x', + '--tw-scale-y', + ['transform', cssTransformValue], + ], + ], + [ + [ + 'scale-x', + [['@defaults transform', {}], '--tw-scale-x', ['transform', cssTransformValue]], + ], + [ + 'scale-y', + [['@defaults transform', {}], '--tw-scale-y', ['transform', cssTransformValue]], + ], + ], + ], + { supportsNegativeValues: true } + ), + + transform: ({ addDefaults, addUtilities }) => { + addDefaults('transform', { + '--tw-translate-x': '0', + '--tw-translate-y': '0', + '--tw-rotate': '0', + '--tw-skew-x': '0', + '--tw-skew-y': '0', + '--tw-scale-x': '1', + '--tw-scale-y': '1', + }) + + addUtilities({ + '.transform': { '@defaults transform': {}, transform: cssTransformValue }, + '.transform-cpu': { + transform: cssTransformValue, + }, + '.transform-gpu': { + transform: cssTransformValue.replace( + 'translate(var(--tw-translate-x), var(--tw-translate-y))', + 'translate3d(var(--tw-translate-x), var(--tw-translate-y), 0)' + ), + }, + '.transform-none': { transform: 'none' }, + }) + }, + + animation: ({ matchUtilities, theme, config }) => { + let prefixName = (name) => escapeClassName(config('prefix') + name) + let keyframes = Object.fromEntries( + Object.entries(theme('keyframes') ?? {}).map(([key, value]) => { + return [key, { [`@keyframes ${prefixName(key)}`]: value }] + }) + ) + + matchUtilities( + { + animate: (value) => { + let animations = parseAnimationValue(value) + + return [ + ...animations.flatMap((animation) => keyframes[animation.name]), + { + animation: animations + .map(({ name, value }) => { + if (name === undefined || keyframes[name] === undefined) { + return value + } + return value.replace(name, prefixName(name)) + }) + .join(', '), + }, + ] + }, + }, + { values: theme('animation') } + ) + }, + + cursor: createUtilityPlugin('cursor'), + + touchAction: ({ addDefaults, addUtilities }) => { + addDefaults('touch-action', { + '--tw-pan-x': ' ', + '--tw-pan-y': ' ', + '--tw-pinch-zoom': ' ', + }) + + let cssTouchActionValue = 'var(--tw-pan-x) var(--tw-pan-y) var(--tw-pinch-zoom)' + + addUtilities({ + '.touch-auto': { 'touch-action': 'auto' }, + '.touch-none': { 'touch-action': 'none' }, + '.touch-pan-x': { + '@defaults touch-action': {}, + '--tw-pan-x': 'pan-x', + 'touch-action': cssTouchActionValue, + }, + '.touch-pan-left': { + '@defaults touch-action': {}, + '--tw-pan-x': 'pan-left', + 'touch-action': cssTouchActionValue, + }, + '.touch-pan-right': { + '@defaults touch-action': {}, + '--tw-pan-x': 'pan-right', + 'touch-action': cssTouchActionValue, + }, + '.touch-pan-y': { + '@defaults touch-action': {}, + '--tw-pan-y': 'pan-y', + 'touch-action': cssTouchActionValue, + }, + '.touch-pan-up': { + '@defaults touch-action': {}, + '--tw-pan-y': 'pan-up', + 'touch-action': cssTouchActionValue, + }, + '.touch-pan-down': { + '@defaults touch-action': {}, + '--tw-pan-y': 'pan-down', + 'touch-action': cssTouchActionValue, + }, + '.touch-pinch-zoom': { + '@defaults touch-action': {}, + '--tw-pinch-zoom': 'pinch-zoom', + 'touch-action': cssTouchActionValue, + }, + '.touch-manipulation': { 'touch-action': 'manipulation' }, + }) + }, + + userSelect: ({ addUtilities }) => { + addUtilities({ + '.select-none': { 'user-select': 'none' }, + '.select-text': { 'user-select': 'text' }, + '.select-all': { 'user-select': 'all' }, + '.select-auto': { 'user-select': 'auto' }, + }) + }, + + resize: ({ addUtilities }) => { + addUtilities({ + '.resize-none': { resize: 'none' }, + '.resize-y': { resize: 'vertical' }, + '.resize-x': { resize: 'horizontal' }, + '.resize': { resize: 'both' }, + }) + }, + + scrollSnapType: ({ addDefaults, addUtilities }) => { + addDefaults('scroll-snap-type', { + '--tw-scroll-snap-strictness': 'proximity', + }) + + addUtilities({ + '.snap-none': { 'scroll-snap-type': 'none' }, + '.snap-x': { + '@defaults scroll-snap-type': {}, + 'scroll-snap-type': 'x var(--tw-scroll-snap-strictness)', + }, + '.snap-y': { + '@defaults scroll-snap-type': {}, + 'scroll-snap-type': 'y var(--tw-scroll-snap-strictness)', + }, + '.snap-both': { + '@defaults scroll-snap-type': {}, + 'scroll-snap-type': 'both var(--tw-scroll-snap-strictness)', + }, + '.snap-mandatory': { '--tw-scroll-snap-strictness': 'mandatory' }, + '.snap-proximity': { '--tw-scroll-snap-strictness': 'proximity' }, + }) + }, + + scrollSnapAlign: ({ addUtilities }) => { + addUtilities({ + '.snap-start': { 'scroll-snap-align': 'start' }, + '.snap-end': { 'scroll-snap-align': 'end' }, + '.snap-center': { 'scroll-snap-align': 'center' }, + '.snap-align-none': { 'scroll-snap-align': 'none' }, + }) + }, + + scrollSnapStop: ({ addUtilities }) => { + addUtilities({ + '.snap-normal': { 'scroll-snap-stop': 'normal' }, + '.snap-always': { 'scroll-snap-stop': 'always' }, + }) + }, + + scrollMargin: createUtilityPlugin( + 'scrollMargin', + [ + ['scroll-m', ['scroll-margin']], + [ + ['scroll-mx', ['scroll-margin-left', 'scroll-margin-right']], + ['scroll-my', ['scroll-margin-top', 'scroll-margin-bottom']], + ], + [ + ['scroll-ms', ['scroll-margin-inline-start']], + ['scroll-me', ['scroll-margin-inline-end']], + ['scroll-mt', ['scroll-margin-top']], + ['scroll-mr', ['scroll-margin-right']], + ['scroll-mb', ['scroll-margin-bottom']], + ['scroll-ml', ['scroll-margin-left']], + ], + ], + { supportsNegativeValues: true } + ), + + scrollPadding: createUtilityPlugin('scrollPadding', [ + ['scroll-p', ['scroll-padding']], + [ + ['scroll-px', ['scroll-padding-left', 'scroll-padding-right']], + ['scroll-py', ['scroll-padding-top', 'scroll-padding-bottom']], + ], + [ + ['scroll-ps', ['scroll-padding-inline-start']], + ['scroll-pe', ['scroll-padding-inline-end']], + ['scroll-pt', ['scroll-padding-top']], + ['scroll-pr', ['scroll-padding-right']], + ['scroll-pb', ['scroll-padding-bottom']], + ['scroll-pl', ['scroll-padding-left']], + ], + ]), + + listStylePosition: ({ addUtilities }) => { + addUtilities({ + '.list-inside': { 'list-style-position': 'inside' }, + '.list-outside': { 'list-style-position': 'outside' }, + }) + }, + listStyleType: createUtilityPlugin('listStyleType', [['list', ['listStyleType']]]), + listStyleImage: createUtilityPlugin('listStyleImage', [['list-image', ['listStyleImage']]]), + + appearance: ({ addUtilities }) => { + addUtilities({ + '.appearance-none': { appearance: 'none' }, + }) + }, + + columns: createUtilityPlugin('columns', [['columns', ['columns']]]), + + breakBefore: ({ addUtilities }) => { + addUtilities({ + '.break-before-auto': { 'break-before': 'auto' }, + '.break-before-avoid': { 'break-before': 'avoid' }, + '.break-before-all': { 'break-before': 'all' }, + '.break-before-avoid-page': { 'break-before': 'avoid-page' }, + '.break-before-page': { 'break-before': 'page' }, + '.break-before-left': { 'break-before': 'left' }, + '.break-before-right': { 'break-before': 'right' }, + '.break-before-column': { 'break-before': 'column' }, + }) + }, + + breakInside: ({ addUtilities }) => { + addUtilities({ + '.break-inside-auto': { 'break-inside': 'auto' }, + '.break-inside-avoid': { 'break-inside': 'avoid' }, + '.break-inside-avoid-page': { 'break-inside': 'avoid-page' }, + '.break-inside-avoid-column': { 'break-inside': 'avoid-column' }, + }) + }, + + breakAfter: ({ addUtilities }) => { + addUtilities({ + '.break-after-auto': { 'break-after': 'auto' }, + '.break-after-avoid': { 'break-after': 'avoid' }, + '.break-after-all': { 'break-after': 'all' }, + '.break-after-avoid-page': { 'break-after': 'avoid-page' }, + '.break-after-page': { 'break-after': 'page' }, + '.break-after-left': { 'break-after': 'left' }, + '.break-after-right': { 'break-after': 'right' }, + '.break-after-column': { 'break-after': 'column' }, + }) + }, + + gridAutoColumns: createUtilityPlugin('gridAutoColumns', [['auto-cols', ['gridAutoColumns']]]), + + gridAutoFlow: ({ addUtilities }) => { + addUtilities({ + '.grid-flow-row': { gridAutoFlow: 'row' }, + '.grid-flow-col': { gridAutoFlow: 'column' }, + '.grid-flow-dense': { gridAutoFlow: 'dense' }, + '.grid-flow-row-dense': { gridAutoFlow: 'row dense' }, + '.grid-flow-col-dense': { gridAutoFlow: 'column dense' }, + }) + }, + + gridAutoRows: createUtilityPlugin('gridAutoRows', [['auto-rows', ['gridAutoRows']]]), + gridTemplateColumns: createUtilityPlugin('gridTemplateColumns', [ + ['grid-cols', ['gridTemplateColumns']], + ]), + gridTemplateRows: createUtilityPlugin('gridTemplateRows', [['grid-rows', ['gridTemplateRows']]]), + + flexDirection: ({ addUtilities }) => { + addUtilities({ + '.flex-row': { 'flex-direction': 'row' }, + '.flex-row-reverse': { 'flex-direction': 'row-reverse' }, + '.flex-col': { 'flex-direction': 'column' }, + '.flex-col-reverse': { 'flex-direction': 'column-reverse' }, + }) + }, + + flexWrap: ({ addUtilities }) => { + addUtilities({ + '.flex-wrap': { 'flex-wrap': 'wrap' }, + '.flex-wrap-reverse': { 'flex-wrap': 'wrap-reverse' }, + '.flex-nowrap': { 'flex-wrap': 'nowrap' }, + }) + }, + + placeContent: ({ addUtilities }) => { + addUtilities({ + '.place-content-center': { 'place-content': 'center' }, + '.place-content-start': { 'place-content': 'start' }, + '.place-content-end': { 'place-content': 'end' }, + '.place-content-between': { 'place-content': 'space-between' }, + '.place-content-around': { 'place-content': 'space-around' }, + '.place-content-evenly': { 'place-content': 'space-evenly' }, + '.place-content-baseline': { 'place-content': 'baseline' }, + '.place-content-stretch': { 'place-content': 'stretch' }, + }) + }, + + placeItems: ({ addUtilities }) => { + addUtilities({ + '.place-items-start': { 'place-items': 'start' }, + '.place-items-end': { 'place-items': 'end' }, + '.place-items-center': { 'place-items': 'center' }, + '.place-items-baseline': { 'place-items': 'baseline' }, + '.place-items-stretch': { 'place-items': 'stretch' }, + }) + }, + + alignContent: ({ addUtilities }) => { + addUtilities({ + '.content-normal': { 'align-content': 'normal' }, + '.content-center': { 'align-content': 'center' }, + '.content-start': { 'align-content': 'flex-start' }, + '.content-end': { 'align-content': 'flex-end' }, + '.content-between': { 'align-content': 'space-between' }, + '.content-around': { 'align-content': 'space-around' }, + '.content-evenly': { 'align-content': 'space-evenly' }, + '.content-baseline': { 'align-content': 'baseline' }, + '.content-stretch': { 'align-content': 'stretch' }, + }) + }, + + alignItems: ({ addUtilities }) => { + addUtilities({ + '.items-start': { 'align-items': 'flex-start' }, + '.items-end': { 'align-items': 'flex-end' }, + '.items-center': { 'align-items': 'center' }, + '.items-baseline': { 'align-items': 'baseline' }, + '.items-stretch': { 'align-items': 'stretch' }, + }) + }, + + justifyContent: ({ addUtilities }) => { + addUtilities({ + '.justify-normal': { 'justify-content': 'normal' }, + '.justify-start': { 'justify-content': 'flex-start' }, + '.justify-end': { 'justify-content': 'flex-end' }, + '.justify-center': { 'justify-content': 'center' }, + '.justify-between': { 'justify-content': 'space-between' }, + '.justify-around': { 'justify-content': 'space-around' }, + '.justify-evenly': { 'justify-content': 'space-evenly' }, + '.justify-stretch': { 'justify-content': 'stretch' }, + }) + }, + + justifyItems: ({ addUtilities }) => { + addUtilities({ + '.justify-items-start': { 'justify-items': 'start' }, + '.justify-items-end': { 'justify-items': 'end' }, + '.justify-items-center': { 'justify-items': 'center' }, + '.justify-items-stretch': { 'justify-items': 'stretch' }, + }) + }, + + gap: createUtilityPlugin('gap', [ + ['gap', ['gap']], + [ + ['gap-x', ['columnGap']], + ['gap-y', ['rowGap']], + ], + ]), + + space: ({ matchUtilities, addUtilities, theme }) => { + matchUtilities( + { + 'space-x': (value) => { + value = value === '0' ? '0px' : value + + if (__OXIDE__) { + return { + '& > :not([hidden]) ~ :not([hidden])': { + '--tw-space-x-reverse': '0', + 'margin-inline-end': `calc(${value} * var(--tw-space-x-reverse))`, + 'margin-inline-start': `calc(${value} * calc(1 - var(--tw-space-x-reverse)))`, + }, + } + } + + return { + '& > :not([hidden]) ~ :not([hidden])': { + '--tw-space-x-reverse': '0', + 'margin-right': `calc(${value} * var(--tw-space-x-reverse))`, + 'margin-left': `calc(${value} * calc(1 - var(--tw-space-x-reverse)))`, + }, + } + }, + 'space-y': (value) => { + value = value === '0' ? '0px' : value + + return { + '& > :not([hidden]) ~ :not([hidden])': { + '--tw-space-y-reverse': '0', + 'margin-top': `calc(${value} * calc(1 - var(--tw-space-y-reverse)))`, + 'margin-bottom': `calc(${value} * var(--tw-space-y-reverse))`, + }, + } + }, + }, + { values: theme('space'), supportsNegativeValues: true } + ) + + addUtilities({ + '.space-y-reverse > :not([hidden]) ~ :not([hidden])': { '--tw-space-y-reverse': '1' }, + '.space-x-reverse > :not([hidden]) ~ :not([hidden])': { '--tw-space-x-reverse': '1' }, + }) + }, + + divideWidth: ({ matchUtilities, addUtilities, theme }) => { + matchUtilities( + { + 'divide-x': (value) => { + value = value === '0' ? '0px' : value + + if (__OXIDE__) { + return { + '& > :not([hidden]) ~ :not([hidden])': { + '@defaults border-width': {}, + '--tw-divide-x-reverse': '0', + 'border-inline-end-width': `calc(${value} * var(--tw-divide-x-reverse))`, + 'border-inline-start-width': `calc(${value} * calc(1 - var(--tw-divide-x-reverse)))`, + }, + } + } + + return { + '& > :not([hidden]) ~ :not([hidden])': { + '@defaults border-width': {}, + '--tw-divide-x-reverse': '0', + 'border-right-width': `calc(${value} * var(--tw-divide-x-reverse))`, + 'border-left-width': `calc(${value} * calc(1 - var(--tw-divide-x-reverse)))`, + }, + } + }, + 'divide-y': (value) => { + value = value === '0' ? '0px' : value + + return { + '& > :not([hidden]) ~ :not([hidden])': { + '@defaults border-width': {}, + '--tw-divide-y-reverse': '0', + 'border-top-width': `calc(${value} * calc(1 - var(--tw-divide-y-reverse)))`, + 'border-bottom-width': `calc(${value} * var(--tw-divide-y-reverse))`, + }, + } + }, + }, + { values: theme('divideWidth'), type: ['line-width', 'length', 'any'] } + ) + + addUtilities({ + '.divide-y-reverse > :not([hidden]) ~ :not([hidden])': { + '@defaults border-width': {}, + '--tw-divide-y-reverse': '1', + }, + '.divide-x-reverse > :not([hidden]) ~ :not([hidden])': { + '@defaults border-width': {}, + '--tw-divide-x-reverse': '1', + }, + }) + }, + + divideStyle: ({ addUtilities }) => { + addUtilities({ + '.divide-solid > :not([hidden]) ~ :not([hidden])': { 'border-style': 'solid' }, + '.divide-dashed > :not([hidden]) ~ :not([hidden])': { 'border-style': 'dashed' }, + '.divide-dotted > :not([hidden]) ~ :not([hidden])': { 'border-style': 'dotted' }, + '.divide-double > :not([hidden]) ~ :not([hidden])': { 'border-style': 'double' }, + '.divide-none > :not([hidden]) ~ :not([hidden])': { 'border-style': 'none' }, + }) + }, + + divideColor: ({ matchUtilities, theme, corePlugins }) => { + matchUtilities( + { + divide: (value) => { + if (!corePlugins('divideOpacity')) { + return { + ['& > :not([hidden]) ~ :not([hidden])']: { + 'border-color': toColorValue(value), + }, + } + } + + return { + ['& > :not([hidden]) ~ :not([hidden])']: withAlphaVariable({ + color: value, + property: 'border-color', + variable: '--tw-divide-opacity', + }), + } + }, + }, + { + values: (({ DEFAULT: _, ...colors }) => colors)(flattenColorPalette(theme('divideColor'))), + type: ['color', 'any'], + } + ) + }, + + divideOpacity: ({ matchUtilities, theme }) => { + matchUtilities( + { + 'divide-opacity': (value) => { + return { [`& > :not([hidden]) ~ :not([hidden])`]: { '--tw-divide-opacity': value } } + }, + }, + { values: theme('divideOpacity') } + ) + }, + + placeSelf: ({ addUtilities }) => { + addUtilities({ + '.place-self-auto': { 'place-self': 'auto' }, + '.place-self-start': { 'place-self': 'start' }, + '.place-self-end': { 'place-self': 'end' }, + '.place-self-center': { 'place-self': 'center' }, + '.place-self-stretch': { 'place-self': 'stretch' }, + }) + }, + + alignSelf: ({ addUtilities }) => { + addUtilities({ + '.self-auto': { 'align-self': 'auto' }, + '.self-start': { 'align-self': 'flex-start' }, + '.self-end': { 'align-self': 'flex-end' }, + '.self-center': { 'align-self': 'center' }, + '.self-stretch': { 'align-self': 'stretch' }, + '.self-baseline': { 'align-self': 'baseline' }, + }) + }, + + justifySelf: ({ addUtilities }) => { + addUtilities({ + '.justify-self-auto': { 'justify-self': 'auto' }, + '.justify-self-start': { 'justify-self': 'start' }, + '.justify-self-end': { 'justify-self': 'end' }, + '.justify-self-center': { 'justify-self': 'center' }, + '.justify-self-stretch': { 'justify-self': 'stretch' }, + }) + }, + + overflow: ({ addUtilities }) => { + addUtilities({ + '.overflow-auto': { overflow: 'auto' }, + '.overflow-hidden': { overflow: 'hidden' }, + '.overflow-clip': { overflow: 'clip' }, + '.overflow-visible': { overflow: 'visible' }, + '.overflow-scroll': { overflow: 'scroll' }, + '.overflow-x-auto': { 'overflow-x': 'auto' }, + '.overflow-y-auto': { 'overflow-y': 'auto' }, + '.overflow-x-hidden': { 'overflow-x': 'hidden' }, + '.overflow-y-hidden': { 'overflow-y': 'hidden' }, + '.overflow-x-clip': { 'overflow-x': 'clip' }, + '.overflow-y-clip': { 'overflow-y': 'clip' }, + '.overflow-x-visible': { 'overflow-x': 'visible' }, + '.overflow-y-visible': { 'overflow-y': 'visible' }, + '.overflow-x-scroll': { 'overflow-x': 'scroll' }, + '.overflow-y-scroll': { 'overflow-y': 'scroll' }, + }) + }, + + overscrollBehavior: ({ addUtilities }) => { + addUtilities({ + '.overscroll-auto': { 'overscroll-behavior': 'auto' }, + '.overscroll-contain': { 'overscroll-behavior': 'contain' }, + '.overscroll-none': { 'overscroll-behavior': 'none' }, + '.overscroll-y-auto': { 'overscroll-behavior-y': 'auto' }, + '.overscroll-y-contain': { 'overscroll-behavior-y': 'contain' }, + '.overscroll-y-none': { 'overscroll-behavior-y': 'none' }, + '.overscroll-x-auto': { 'overscroll-behavior-x': 'auto' }, + '.overscroll-x-contain': { 'overscroll-behavior-x': 'contain' }, + '.overscroll-x-none': { 'overscroll-behavior-x': 'none' }, + }) + }, + + scrollBehavior: ({ addUtilities }) => { + addUtilities({ + '.scroll-auto': { 'scroll-behavior': 'auto' }, + '.scroll-smooth': { 'scroll-behavior': 'smooth' }, + }) + }, + + textOverflow: ({ addUtilities }) => { + addUtilities({ + '.truncate': { overflow: 'hidden', 'text-overflow': 'ellipsis', 'white-space': 'nowrap' }, + '.overflow-ellipsis': { 'text-overflow': 'ellipsis' }, // Deprecated + '.text-ellipsis': { 'text-overflow': 'ellipsis' }, + '.text-clip': { 'text-overflow': 'clip' }, + }) + }, + + hyphens: ({ addUtilities }) => { + addUtilities({ + '.hyphens-none': { hyphens: 'none' }, + '.hyphens-manual': { hyphens: 'manual' }, + '.hyphens-auto': { hyphens: 'auto' }, + }) + }, + + whitespace: ({ addUtilities }) => { + addUtilities({ + '.whitespace-normal': { 'white-space': 'normal' }, + '.whitespace-nowrap': { 'white-space': 'nowrap' }, + '.whitespace-pre': { 'white-space': 'pre' }, + '.whitespace-pre-line': { 'white-space': 'pre-line' }, + '.whitespace-pre-wrap': { 'white-space': 'pre-wrap' }, + '.whitespace-break-spaces': { 'white-space': 'break-spaces' }, + }) + }, + + wordBreak: ({ addUtilities }) => { + addUtilities({ + '.break-normal': { 'overflow-wrap': 'normal', 'word-break': 'normal' }, + '.break-words': { 'overflow-wrap': 'break-word' }, + '.break-all': { 'word-break': 'break-all' }, + '.break-keep': { 'word-break': 'keep-all' }, + }) + }, + + borderRadius: createUtilityPlugin('borderRadius', [ + ['rounded', ['border-radius']], + [ + ['rounded-s', ['border-start-start-radius', 'border-end-start-radius']], + ['rounded-e', ['border-start-end-radius', 'border-end-end-radius']], + ['rounded-t', ['border-top-left-radius', 'border-top-right-radius']], + ['rounded-r', ['border-top-right-radius', 'border-bottom-right-radius']], + ['rounded-b', ['border-bottom-right-radius', 'border-bottom-left-radius']], + ['rounded-l', ['border-top-left-radius', 'border-bottom-left-radius']], + ], + [ + ['rounded-ss', ['border-start-start-radius']], + ['rounded-se', ['border-start-end-radius']], + ['rounded-ee', ['border-end-end-radius']], + ['rounded-es', ['border-end-start-radius']], + ['rounded-tl', ['border-top-left-radius']], + ['rounded-tr', ['border-top-right-radius']], + ['rounded-br', ['border-bottom-right-radius']], + ['rounded-bl', ['border-bottom-left-radius']], + ], + ]), + + borderWidth: createUtilityPlugin( + 'borderWidth', + [ + ['border', [['@defaults border-width', {}], 'border-width']], + [ + ['border-x', [['@defaults border-width', {}], 'border-left-width', 'border-right-width']], + ['border-y', [['@defaults border-width', {}], 'border-top-width', 'border-bottom-width']], + ], + [ + ['border-s', [['@defaults border-width', {}], 'border-inline-start-width']], + ['border-e', [['@defaults border-width', {}], 'border-inline-end-width']], + ['border-t', [['@defaults border-width', {}], 'border-top-width']], + ['border-r', [['@defaults border-width', {}], 'border-right-width']], + ['border-b', [['@defaults border-width', {}], 'border-bottom-width']], + ['border-l', [['@defaults border-width', {}], 'border-left-width']], + ], + ], + { type: ['line-width', 'length'] } + ), + + borderStyle: ({ addUtilities }) => { + addUtilities({ + '.border-solid': { 'border-style': 'solid' }, + '.border-dashed': { 'border-style': 'dashed' }, + '.border-dotted': { 'border-style': 'dotted' }, + '.border-double': { 'border-style': 'double' }, + '.border-hidden': { 'border-style': 'hidden' }, + '.border-none': { 'border-style': 'none' }, + }) + }, + + borderColor: ({ matchUtilities, theme, corePlugins }) => { + matchUtilities( + { + border: (value) => { + if (!corePlugins('borderOpacity')) { + return { + 'border-color': toColorValue(value), + } + } + + return withAlphaVariable({ + color: value, + property: 'border-color', + variable: '--tw-border-opacity', + }) + }, + }, + { + values: (({ DEFAULT: _, ...colors }) => colors)(flattenColorPalette(theme('borderColor'))), + type: ['color', 'any'], + } + ) + + matchUtilities( + { + 'border-x': (value) => { + if (!corePlugins('borderOpacity')) { + return { + 'border-left-color': toColorValue(value), + 'border-right-color': toColorValue(value), + } + } + + return withAlphaVariable({ + color: value, + property: ['border-left-color', 'border-right-color'], + variable: '--tw-border-opacity', + }) + }, + 'border-y': (value) => { + if (!corePlugins('borderOpacity')) { + return { + 'border-top-color': toColorValue(value), + 'border-bottom-color': toColorValue(value), + } + } + + return withAlphaVariable({ + color: value, + property: ['border-top-color', 'border-bottom-color'], + variable: '--tw-border-opacity', + }) + }, + }, + { + values: (({ DEFAULT: _, ...colors }) => colors)(flattenColorPalette(theme('borderColor'))), + type: ['color', 'any'], + } + ) + + matchUtilities( + { + 'border-s': (value) => { + if (!corePlugins('borderOpacity')) { + return { + 'border-inline-start-color': toColorValue(value), + } + } + + return withAlphaVariable({ + color: value, + property: 'border-inline-start-color', + variable: '--tw-border-opacity', + }) + }, + 'border-e': (value) => { + if (!corePlugins('borderOpacity')) { + return { + 'border-inline-end-color': toColorValue(value), + } + } + + return withAlphaVariable({ + color: value, + property: 'border-inline-end-color', + variable: '--tw-border-opacity', + }) + }, + 'border-t': (value) => { + if (!corePlugins('borderOpacity')) { + return { + 'border-top-color': toColorValue(value), + } + } + + return withAlphaVariable({ + color: value, + property: 'border-top-color', + variable: '--tw-border-opacity', + }) + }, + 'border-r': (value) => { + if (!corePlugins('borderOpacity')) { + return { + 'border-right-color': toColorValue(value), + } + } + + return withAlphaVariable({ + color: value, + property: 'border-right-color', + variable: '--tw-border-opacity', + }) + }, + 'border-b': (value) => { + if (!corePlugins('borderOpacity')) { + return { + 'border-bottom-color': toColorValue(value), + } + } + + return withAlphaVariable({ + color: value, + property: 'border-bottom-color', + variable: '--tw-border-opacity', + }) + }, + 'border-l': (value) => { + if (!corePlugins('borderOpacity')) { + return { + 'border-left-color': toColorValue(value), + } + } + + return withAlphaVariable({ + color: value, + property: 'border-left-color', + variable: '--tw-border-opacity', + }) + }, + }, + { + values: (({ DEFAULT: _, ...colors }) => colors)(flattenColorPalette(theme('borderColor'))), + type: ['color', 'any'], + } + ) + }, + + borderOpacity: createUtilityPlugin('borderOpacity', [ + ['border-opacity', ['--tw-border-opacity']], + ]), + + backgroundColor: ({ matchUtilities, theme, corePlugins }) => { + matchUtilities( + { + bg: (value) => { + if (!corePlugins('backgroundOpacity')) { + return { + 'background-color': toColorValue(value), + } + } + + return withAlphaVariable({ + color: value, + property: 'background-color', + variable: '--tw-bg-opacity', + }) + }, + }, + { values: flattenColorPalette(theme('backgroundColor')), type: ['color', 'any'] } + ) + }, + + backgroundOpacity: createUtilityPlugin('backgroundOpacity', [ + ['bg-opacity', ['--tw-bg-opacity']], + ]), + backgroundImage: createUtilityPlugin('backgroundImage', [['bg', ['background-image']]], { + type: ['lookup', 'image', 'url'], + }), + gradientColorStops: (() => { + function transparentTo(value) { + return withAlphaValue(value, 0, 'rgb(255 255 255 / 0)') + } + + return function ({ matchUtilities, theme, addDefaults }) { + addDefaults('gradient-color-stops', { + '--tw-gradient-from-position': ' ', + '--tw-gradient-via-position': ' ', + '--tw-gradient-to-position': ' ', + }) + + let options = { + values: flattenColorPalette(theme('gradientColorStops')), + type: ['color', 'any'], + } + + let positionOptions = { + values: theme('gradientColorStopPositions'), + type: ['length', 'percentage'], + } + + matchUtilities( + { + from: (value) => { + let transparentToValue = transparentTo(value) + + return { + '@defaults gradient-color-stops': {}, + '--tw-gradient-from': `${toColorValue(value)} var(--tw-gradient-from-position)`, + '--tw-gradient-to': `${transparentToValue} var(--tw-gradient-to-position)`, + '--tw-gradient-stops': `var(--tw-gradient-from), var(--tw-gradient-to)`, + } + }, + }, + options + ) + + matchUtilities( + { + from: (value) => { + return { + '--tw-gradient-from-position': value, + } + }, + }, + positionOptions + ) + + matchUtilities( + { + via: (value) => { + let transparentToValue = transparentTo(value) + + return { + '@defaults gradient-color-stops': {}, + '--tw-gradient-to': `${transparentToValue} var(--tw-gradient-to-position)`, + '--tw-gradient-stops': `var(--tw-gradient-from), ${toColorValue( + value + )} var(--tw-gradient-via-position), var(--tw-gradient-to)`, + } + }, + }, + options + ) + + matchUtilities( + { + via: (value) => { + return { + '--tw-gradient-via-position': value, + } + }, + }, + positionOptions + ) + + matchUtilities( + { + to: (value) => ({ + '@defaults gradient-color-stops': {}, + '--tw-gradient-to': `${toColorValue(value)} var(--tw-gradient-to-position)`, + }), + }, + options + ) + + matchUtilities( + { + to: (value) => { + return { + '--tw-gradient-to-position': value, + } + }, + }, + positionOptions + ) + } + })(), + + boxDecorationBreak: ({ addUtilities }) => { + addUtilities({ + '.decoration-slice': { 'box-decoration-break': 'slice' }, // Deprecated + '.decoration-clone': { 'box-decoration-break': 'clone' }, // Deprecated + '.box-decoration-slice': { 'box-decoration-break': 'slice' }, + '.box-decoration-clone': { 'box-decoration-break': 'clone' }, + }) + }, + + backgroundSize: createUtilityPlugin('backgroundSize', [['bg', ['background-size']]], { + type: ['lookup', 'length', 'percentage', 'size'], + }), + + backgroundAttachment: ({ addUtilities }) => { + addUtilities({ + '.bg-fixed': { 'background-attachment': 'fixed' }, + '.bg-local': { 'background-attachment': 'local' }, + '.bg-scroll': { 'background-attachment': 'scroll' }, + }) + }, + + backgroundClip: ({ addUtilities }) => { + addUtilities({ + '.bg-clip-border': { 'background-clip': 'border-box' }, + '.bg-clip-padding': { 'background-clip': 'padding-box' }, + '.bg-clip-content': { 'background-clip': 'content-box' }, + '.bg-clip-text': { 'background-clip': 'text' }, + }) + }, + + backgroundPosition: createUtilityPlugin('backgroundPosition', [['bg', ['background-position']]], { + type: ['lookup', ['position', { preferOnConflict: true }]], + }), + + backgroundRepeat: ({ addUtilities }) => { + addUtilities({ + '.bg-repeat': { 'background-repeat': 'repeat' }, + '.bg-no-repeat': { 'background-repeat': 'no-repeat' }, + '.bg-repeat-x': { 'background-repeat': 'repeat-x' }, + '.bg-repeat-y': { 'background-repeat': 'repeat-y' }, + '.bg-repeat-round': { 'background-repeat': 'round' }, + '.bg-repeat-space': { 'background-repeat': 'space' }, + }) + }, + + backgroundOrigin: ({ addUtilities }) => { + addUtilities({ + '.bg-origin-border': { 'background-origin': 'border-box' }, + '.bg-origin-padding': { 'background-origin': 'padding-box' }, + '.bg-origin-content': { 'background-origin': 'content-box' }, + }) + }, + + fill: ({ matchUtilities, theme }) => { + matchUtilities( + { + fill: (value) => { + return { fill: toColorValue(value) } + }, + }, + { values: flattenColorPalette(theme('fill')), type: ['color', 'any'] } + ) + }, + + stroke: ({ matchUtilities, theme }) => { + matchUtilities( + { + stroke: (value) => { + return { stroke: toColorValue(value) } + }, + }, + { values: flattenColorPalette(theme('stroke')), type: ['color', 'url', 'any'] } + ) + }, + + strokeWidth: createUtilityPlugin('strokeWidth', [['stroke', ['stroke-width']]], { + type: ['length', 'number', 'percentage'], + }), + + objectFit: ({ addUtilities }) => { + addUtilities({ + '.object-contain': { 'object-fit': 'contain' }, + '.object-cover': { 'object-fit': 'cover' }, + '.object-fill': { 'object-fit': 'fill' }, + '.object-none': { 'object-fit': 'none' }, + '.object-scale-down': { 'object-fit': 'scale-down' }, + }) + }, + objectPosition: createUtilityPlugin('objectPosition', [['object', ['object-position']]]), + + padding: createUtilityPlugin('padding', [ + ['p', ['padding']], + [ + ['px', ['padding-left', 'padding-right']], + ['py', ['padding-top', 'padding-bottom']], + ], + [ + ['ps', ['padding-inline-start']], + ['pe', ['padding-inline-end']], + ['pt', ['padding-top']], + ['pr', ['padding-right']], + ['pb', ['padding-bottom']], + ['pl', ['padding-left']], + ], + ]), + + textAlign: ({ addUtilities }) => { + addUtilities({ + '.text-left': { 'text-align': 'left' }, + '.text-center': { 'text-align': 'center' }, + '.text-right': { 'text-align': 'right' }, + '.text-justify': { 'text-align': 'justify' }, + '.text-start': { 'text-align': 'start' }, + '.text-end': { 'text-align': 'end' }, + }) + }, + + textIndent: createUtilityPlugin('textIndent', [['indent', ['text-indent']]], { + supportsNegativeValues: true, + }), + + verticalAlign: ({ addUtilities, matchUtilities }) => { + addUtilities({ + '.align-baseline': { 'vertical-align': 'baseline' }, + '.align-top': { 'vertical-align': 'top' }, + '.align-middle': { 'vertical-align': 'middle' }, + '.align-bottom': { 'vertical-align': 'bottom' }, + '.align-text-top': { 'vertical-align': 'text-top' }, + '.align-text-bottom': { 'vertical-align': 'text-bottom' }, + '.align-sub': { 'vertical-align': 'sub' }, + '.align-super': { 'vertical-align': 'super' }, + }) + + matchUtilities({ align: (value) => ({ 'vertical-align': value }) }) + }, + + fontFamily: ({ matchUtilities, theme }) => { + matchUtilities( + { + font: (value) => { + let [families, options = {}] = + Array.isArray(value) && isPlainObject(value[1]) ? value : [value] + let { fontFeatureSettings, fontVariationSettings } = options + + return { + 'font-family': Array.isArray(families) ? families.join(', ') : families, + ...(fontFeatureSettings === undefined + ? {} + : { 'font-feature-settings': fontFeatureSettings }), + ...(fontVariationSettings === undefined + ? {} + : { 'font-variation-settings': fontVariationSettings }), + } + }, + }, + { + values: theme('fontFamily'), + type: ['lookup', 'generic-name', 'family-name'], + } + ) + }, + + fontSize: ({ matchUtilities, theme }) => { + matchUtilities( + { + text: (value, { modifier }) => { + let [fontSize, options] = Array.isArray(value) ? value : [value] + + if (modifier) { + return { + 'font-size': fontSize, + 'line-height': modifier, + } + } + + let { lineHeight, letterSpacing, fontWeight } = isPlainObject(options) + ? options + : { lineHeight: options } + + return { + 'font-size': fontSize, + ...(lineHeight === undefined ? {} : { 'line-height': lineHeight }), + ...(letterSpacing === undefined ? {} : { 'letter-spacing': letterSpacing }), + ...(fontWeight === undefined ? {} : { 'font-weight': fontWeight }), + } + }, + }, + { + values: theme('fontSize'), + modifiers: theme('lineHeight'), + type: ['absolute-size', 'relative-size', 'length', 'percentage'], + } + ) + }, + + fontWeight: createUtilityPlugin('fontWeight', [['font', ['fontWeight']]], { + type: ['lookup', 'number', 'any'], + }), + + textTransform: ({ addUtilities }) => { + addUtilities({ + '.uppercase': { 'text-transform': 'uppercase' }, + '.lowercase': { 'text-transform': 'lowercase' }, + '.capitalize': { 'text-transform': 'capitalize' }, + '.normal-case': { 'text-transform': 'none' }, + }) + }, + + fontStyle: ({ addUtilities }) => { + addUtilities({ + '.italic': { 'font-style': 'italic' }, + '.not-italic': { 'font-style': 'normal' }, + }) + }, + + fontVariantNumeric: ({ addDefaults, addUtilities }) => { + let cssFontVariantNumericValue = + 'var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) var(--tw-numeric-spacing) var(--tw-numeric-fraction)' + + addDefaults('font-variant-numeric', { + '--tw-ordinal': ' ', + '--tw-slashed-zero': ' ', + '--tw-numeric-figure': ' ', + '--tw-numeric-spacing': ' ', + '--tw-numeric-fraction': ' ', + }) + + addUtilities({ + '.normal-nums': { 'font-variant-numeric': 'normal' }, + '.ordinal': { + '@defaults font-variant-numeric': {}, + '--tw-ordinal': 'ordinal', + 'font-variant-numeric': cssFontVariantNumericValue, + }, + '.slashed-zero': { + '@defaults font-variant-numeric': {}, + '--tw-slashed-zero': 'slashed-zero', + 'font-variant-numeric': cssFontVariantNumericValue, + }, + '.lining-nums': { + '@defaults font-variant-numeric': {}, + '--tw-numeric-figure': 'lining-nums', + 'font-variant-numeric': cssFontVariantNumericValue, + }, + '.oldstyle-nums': { + '@defaults font-variant-numeric': {}, + '--tw-numeric-figure': 'oldstyle-nums', + 'font-variant-numeric': cssFontVariantNumericValue, + }, + '.proportional-nums': { + '@defaults font-variant-numeric': {}, + '--tw-numeric-spacing': 'proportional-nums', + 'font-variant-numeric': cssFontVariantNumericValue, + }, + '.tabular-nums': { + '@defaults font-variant-numeric': {}, + '--tw-numeric-spacing': 'tabular-nums', + 'font-variant-numeric': cssFontVariantNumericValue, + }, + '.diagonal-fractions': { + '@defaults font-variant-numeric': {}, + '--tw-numeric-fraction': 'diagonal-fractions', + 'font-variant-numeric': cssFontVariantNumericValue, + }, + '.stacked-fractions': { + '@defaults font-variant-numeric': {}, + '--tw-numeric-fraction': 'stacked-fractions', + 'font-variant-numeric': cssFontVariantNumericValue, + }, + }) + }, + + lineHeight: createUtilityPlugin('lineHeight', [['leading', ['lineHeight']]]), + letterSpacing: createUtilityPlugin('letterSpacing', [['tracking', ['letterSpacing']]], { + supportsNegativeValues: true, + }), + + textColor: ({ matchUtilities, theme, corePlugins }) => { + matchUtilities( + { + text: (value) => { + if (!corePlugins('textOpacity')) { + return { color: toColorValue(value) } + } + + return withAlphaVariable({ + color: value, + property: 'color', + variable: '--tw-text-opacity', + }) + }, + }, + { values: flattenColorPalette(theme('textColor')), type: ['color', 'any'] } + ) + }, + + textOpacity: createUtilityPlugin('textOpacity', [['text-opacity', ['--tw-text-opacity']]]), + + textDecoration: ({ addUtilities }) => { + addUtilities({ + '.underline': { 'text-decoration-line': 'underline' }, + '.overline': { 'text-decoration-line': 'overline' }, + '.line-through': { 'text-decoration-line': 'line-through' }, + '.no-underline': { 'text-decoration-line': 'none' }, + }) + }, + + textDecorationColor: ({ matchUtilities, theme }) => { + matchUtilities( + { + decoration: (value) => { + return { 'text-decoration-color': toColorValue(value) } + }, + }, + { values: flattenColorPalette(theme('textDecorationColor')), type: ['color', 'any'] } + ) + }, + + textDecorationStyle: ({ addUtilities }) => { + addUtilities({ + '.decoration-solid': { 'text-decoration-style': 'solid' }, + '.decoration-double': { 'text-decoration-style': 'double' }, + '.decoration-dotted': { 'text-decoration-style': 'dotted' }, + '.decoration-dashed': { 'text-decoration-style': 'dashed' }, + '.decoration-wavy': { 'text-decoration-style': 'wavy' }, + }) + }, + + textDecorationThickness: createUtilityPlugin( + 'textDecorationThickness', + [['decoration', ['text-decoration-thickness']]], + { type: ['length', 'percentage'] } + ), + + textUnderlineOffset: createUtilityPlugin( + 'textUnderlineOffset', + [['underline-offset', ['text-underline-offset']]], + { type: ['length', 'percentage', 'any'] } + ), + + fontSmoothing: ({ addUtilities }) => { + addUtilities({ + '.antialiased': { + '-webkit-font-smoothing': 'antialiased', + '-moz-osx-font-smoothing': 'grayscale', + }, + '.subpixel-antialiased': { + '-webkit-font-smoothing': 'auto', + '-moz-osx-font-smoothing': 'auto', + }, + }) + }, + + placeholderColor: ({ matchUtilities, theme, corePlugins }) => { + matchUtilities( + { + placeholder: (value) => { + if (!corePlugins('placeholderOpacity')) { + return { + '&::placeholder': { + color: toColorValue(value), + }, + } + } + + return { + '&::placeholder': withAlphaVariable({ + color: value, + property: 'color', + variable: '--tw-placeholder-opacity', + }), + } + }, + }, + { values: flattenColorPalette(theme('placeholderColor')), type: ['color', 'any'] } + ) + }, + + placeholderOpacity: ({ matchUtilities, theme }) => { + matchUtilities( + { + 'placeholder-opacity': (value) => { + return { ['&::placeholder']: { '--tw-placeholder-opacity': value } } + }, + }, + { values: theme('placeholderOpacity') } + ) + }, + + caretColor: ({ matchUtilities, theme }) => { + matchUtilities( + { + caret: (value) => { + return { 'caret-color': toColorValue(value) } + }, + }, + { values: flattenColorPalette(theme('caretColor')), type: ['color', 'any'] } + ) + }, + + accentColor: ({ matchUtilities, theme }) => { + matchUtilities( + { + accent: (value) => { + return { 'accent-color': toColorValue(value) } + }, + }, + { values: flattenColorPalette(theme('accentColor')), type: ['color', 'any'] } + ) + }, + + opacity: createUtilityPlugin('opacity', [['opacity', ['opacity']]]), + + backgroundBlendMode: ({ addUtilities }) => { + addUtilities({ + '.bg-blend-normal': { 'background-blend-mode': 'normal' }, + '.bg-blend-multiply': { 'background-blend-mode': 'multiply' }, + '.bg-blend-screen': { 'background-blend-mode': 'screen' }, + '.bg-blend-overlay': { 'background-blend-mode': 'overlay' }, + '.bg-blend-darken': { 'background-blend-mode': 'darken' }, + '.bg-blend-lighten': { 'background-blend-mode': 'lighten' }, + '.bg-blend-color-dodge': { 'background-blend-mode': 'color-dodge' }, + '.bg-blend-color-burn': { 'background-blend-mode': 'color-burn' }, + '.bg-blend-hard-light': { 'background-blend-mode': 'hard-light' }, + '.bg-blend-soft-light': { 'background-blend-mode': 'soft-light' }, + '.bg-blend-difference': { 'background-blend-mode': 'difference' }, + '.bg-blend-exclusion': { 'background-blend-mode': 'exclusion' }, + '.bg-blend-hue': { 'background-blend-mode': 'hue' }, + '.bg-blend-saturation': { 'background-blend-mode': 'saturation' }, + '.bg-blend-color': { 'background-blend-mode': 'color' }, + '.bg-blend-luminosity': { 'background-blend-mode': 'luminosity' }, + }) + }, + + mixBlendMode: ({ addUtilities }) => { + addUtilities({ + '.mix-blend-normal': { 'mix-blend-mode': 'normal' }, + '.mix-blend-multiply': { 'mix-blend-mode': 'multiply' }, + '.mix-blend-screen': { 'mix-blend-mode': 'screen' }, + '.mix-blend-overlay': { 'mix-blend-mode': 'overlay' }, + '.mix-blend-darken': { 'mix-blend-mode': 'darken' }, + '.mix-blend-lighten': { 'mix-blend-mode': 'lighten' }, + '.mix-blend-color-dodge': { 'mix-blend-mode': 'color-dodge' }, + '.mix-blend-color-burn': { 'mix-blend-mode': 'color-burn' }, + '.mix-blend-hard-light': { 'mix-blend-mode': 'hard-light' }, + '.mix-blend-soft-light': { 'mix-blend-mode': 'soft-light' }, + '.mix-blend-difference': { 'mix-blend-mode': 'difference' }, + '.mix-blend-exclusion': { 'mix-blend-mode': 'exclusion' }, + '.mix-blend-hue': { 'mix-blend-mode': 'hue' }, + '.mix-blend-saturation': { 'mix-blend-mode': 'saturation' }, + '.mix-blend-color': { 'mix-blend-mode': 'color' }, + '.mix-blend-luminosity': { 'mix-blend-mode': 'luminosity' }, + '.mix-blend-plus-lighter': { 'mix-blend-mode': 'plus-lighter' }, + }) + }, + + boxShadow: (() => { + let transformValue = transformThemeValue('boxShadow') + let defaultBoxShadow = [ + `var(--tw-ring-offset-shadow, 0 0 #0000)`, + `var(--tw-ring-shadow, 0 0 #0000)`, + `var(--tw-shadow)`, + ].join(', ') + + return function ({ matchUtilities, addDefaults, theme }) { + addDefaults(' box-shadow', { + '--tw-ring-offset-shadow': '0 0 #0000', + '--tw-ring-shadow': '0 0 #0000', + '--tw-shadow': '0 0 #0000', + '--tw-shadow-colored': '0 0 #0000', + }) + + matchUtilities( + { + shadow: (value) => { + value = transformValue(value) + + let ast = parseBoxShadowValue(value) + for (let shadow of ast) { + // Don't override color if the whole shadow is a variable + if (!shadow.valid) { + continue + } + + shadow.color = 'var(--tw-shadow-color)' + } + + return { + '@defaults box-shadow': {}, + '--tw-shadow': value === 'none' ? '0 0 #0000' : value, + '--tw-shadow-colored': value === 'none' ? '0 0 #0000' : formatBoxShadowValue(ast), + 'box-shadow': defaultBoxShadow, + } + }, + }, + { values: theme('boxShadow'), type: ['shadow'] } + ) + } + })(), + + boxShadowColor: ({ matchUtilities, theme }) => { + matchUtilities( + { + shadow: (value) => { + return { + '--tw-shadow-color': toColorValue(value), + '--tw-shadow': 'var(--tw-shadow-colored)', + } + }, + }, + { values: flattenColorPalette(theme('boxShadowColor')), type: ['color', 'any'] } + ) + }, + + outlineStyle: ({ addUtilities }) => { + addUtilities({ + '.outline-none': { + outline: '2px solid transparent', + 'outline-offset': '2px', + }, + '.outline': { 'outline-style': 'solid' }, + '.outline-dashed': { 'outline-style': 'dashed' }, + '.outline-dotted': { 'outline-style': 'dotted' }, + '.outline-double': { 'outline-style': 'double' }, + }) + }, + + outlineWidth: createUtilityPlugin('outlineWidth', [['outline', ['outline-width']]], { + type: ['length', 'number', 'percentage'], + }), + + outlineOffset: createUtilityPlugin('outlineOffset', [['outline-offset', ['outline-offset']]], { + type: ['length', 'number', 'percentage', 'any'], + supportsNegativeValues: true, + }), + + outlineColor: ({ matchUtilities, theme }) => { + matchUtilities( + { + outline: (value) => { + return { 'outline-color': toColorValue(value) } + }, + }, + { values: flattenColorPalette(theme('outlineColor')), type: ['color', 'any'] } + ) + }, + + ringWidth: ({ matchUtilities, addDefaults, addUtilities, theme, config }) => { + let ringColorDefault = (() => { + if (flagEnabled(config(), 'respectDefaultRingColorOpacity')) { + return theme('ringColor.DEFAULT') + } + + let ringOpacityDefault = theme('ringOpacity.DEFAULT', '0.5') + + if (!theme('ringColor')?.DEFAULT) { + return `rgb(147 197 253 / ${ringOpacityDefault})` + } + + return withAlphaValue( + theme('ringColor')?.DEFAULT, + ringOpacityDefault, + `rgb(147 197 253 / ${ringOpacityDefault})` + ) + })() + + addDefaults('ring-width', { + '--tw-ring-inset': ' ', + '--tw-ring-offset-width': theme('ringOffsetWidth.DEFAULT', '0px'), + '--tw-ring-offset-color': theme('ringOffsetColor.DEFAULT', '#fff'), + '--tw-ring-color': ringColorDefault, + '--tw-ring-offset-shadow': '0 0 #0000', + '--tw-ring-shadow': '0 0 #0000', + '--tw-shadow': '0 0 #0000', + '--tw-shadow-colored': '0 0 #0000', + }) + + matchUtilities( + { + ring: (value) => { + return { + '@defaults ring-width': {}, + '--tw-ring-offset-shadow': `var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)`, + '--tw-ring-shadow': `var(--tw-ring-inset) 0 0 0 calc(${value} + var(--tw-ring-offset-width)) var(--tw-ring-color)`, + 'box-shadow': [ + `var(--tw-ring-offset-shadow)`, + `var(--tw-ring-shadow)`, + `var(--tw-shadow, 0 0 #0000)`, + ].join(', '), + } + }, + }, + { values: theme('ringWidth'), type: 'length' } + ) + + addUtilities({ + '.ring-inset': { '@defaults ring-width': {}, '--tw-ring-inset': 'inset' }, + }) + }, + + ringColor: ({ matchUtilities, theme, corePlugins }) => { + matchUtilities( + { + ring: (value) => { + if (!corePlugins('ringOpacity')) { + return { + '--tw-ring-color': toColorValue(value), + } + } + + return withAlphaVariable({ + color: value, + property: '--tw-ring-color', + variable: '--tw-ring-opacity', + }) + }, + }, + { + values: Object.fromEntries( + Object.entries(flattenColorPalette(theme('ringColor'))).filter( + ([modifier]) => modifier !== 'DEFAULT' + ) + ), + type: ['color', 'any'], + } + ) + }, + + ringOpacity: (helpers) => { + let { config } = helpers + + return createUtilityPlugin('ringOpacity', [['ring-opacity', ['--tw-ring-opacity']]], { + filterDefault: !flagEnabled(config(), 'respectDefaultRingColorOpacity'), + })(helpers) + }, + ringOffsetWidth: createUtilityPlugin( + 'ringOffsetWidth', + [['ring-offset', ['--tw-ring-offset-width']]], + { type: 'length' } + ), + + ringOffsetColor: ({ matchUtilities, theme }) => { + matchUtilities( + { + 'ring-offset': (value) => { + return { + '--tw-ring-offset-color': toColorValue(value), + } + }, + }, + { values: flattenColorPalette(theme('ringOffsetColor')), type: ['color', 'any'] } + ) + }, + + blur: ({ matchUtilities, theme }) => { + matchUtilities( + { + blur: (value) => { + return { + '--tw-blur': `blur(${value})`, + '@defaults filter': {}, + filter: cssFilterValue, + } + }, + }, + { values: theme('blur') } + ) + }, + + brightness: ({ matchUtilities, theme }) => { + matchUtilities( + { + brightness: (value) => { + return { + '--tw-brightness': `brightness(${value})`, + '@defaults filter': {}, + filter: cssFilterValue, + } + }, + }, + { values: theme('brightness') } + ) + }, + + contrast: ({ matchUtilities, theme }) => { + matchUtilities( + { + contrast: (value) => { + return { + '--tw-contrast': `contrast(${value})`, + '@defaults filter': {}, + filter: cssFilterValue, + } + }, + }, + { values: theme('contrast') } + ) + }, + + dropShadow: ({ matchUtilities, theme }) => { + matchUtilities( + { + 'drop-shadow': (value) => { + return { + '--tw-drop-shadow': Array.isArray(value) + ? value.map((v) => `drop-shadow(${v})`).join(' ') + : `drop-shadow(${value})`, + '@defaults filter': {}, + filter: cssFilterValue, + } + }, + }, + { values: theme('dropShadow') } + ) + }, + + grayscale: ({ matchUtilities, theme }) => { + matchUtilities( + { + grayscale: (value) => { + return { + '--tw-grayscale': `grayscale(${value})`, + '@defaults filter': {}, + filter: cssFilterValue, + } + }, + }, + { values: theme('grayscale') } + ) + }, + + hueRotate: ({ matchUtilities, theme }) => { + matchUtilities( + { + 'hue-rotate': (value) => { + return { + '--tw-hue-rotate': `hue-rotate(${value})`, + '@defaults filter': {}, + filter: cssFilterValue, + } + }, + }, + { values: theme('hueRotate'), supportsNegativeValues: true } + ) + }, + + invert: ({ matchUtilities, theme }) => { + matchUtilities( + { + invert: (value) => { + return { + '--tw-invert': `invert(${value})`, + '@defaults filter': {}, + filter: cssFilterValue, + } + }, + }, + { values: theme('invert') } + ) + }, + + saturate: ({ matchUtilities, theme }) => { + matchUtilities( + { + saturate: (value) => { + return { + '--tw-saturate': `saturate(${value})`, + '@defaults filter': {}, + filter: cssFilterValue, + } + }, + }, + { values: theme('saturate') } + ) + }, + + sepia: ({ matchUtilities, theme }) => { + matchUtilities( + { + sepia: (value) => { + return { + '--tw-sepia': `sepia(${value})`, + '@defaults filter': {}, + filter: cssFilterValue, + } + }, + }, + { values: theme('sepia') } + ) + }, + + filter: ({ addDefaults, addUtilities }) => { + addDefaults('filter', { + '--tw-blur': ' ', + '--tw-brightness': ' ', + '--tw-contrast': ' ', + '--tw-grayscale': ' ', + '--tw-hue-rotate': ' ', + '--tw-invert': ' ', + '--tw-saturate': ' ', + '--tw-sepia': ' ', + '--tw-drop-shadow': ' ', + }) + addUtilities({ + '.filter': { '@defaults filter': {}, filter: cssFilterValue }, + '.filter-none': { filter: 'none' }, + }) + }, + + backdropBlur: ({ matchUtilities, theme }) => { + matchUtilities( + { + 'backdrop-blur': (value) => { + return { + '--tw-backdrop-blur': `blur(${value})`, + '@defaults backdrop-filter': {}, + 'backdrop-filter': cssBackdropFilterValue, + } + }, + }, + { values: theme('backdropBlur') } + ) + }, + + backdropBrightness: ({ matchUtilities, theme }) => { + matchUtilities( + { + 'backdrop-brightness': (value) => { + return { + '--tw-backdrop-brightness': `brightness(${value})`, + '@defaults backdrop-filter': {}, + 'backdrop-filter': cssBackdropFilterValue, + } + }, + }, + { values: theme('backdropBrightness') } + ) + }, + + backdropContrast: ({ matchUtilities, theme }) => { + matchUtilities( + { + 'backdrop-contrast': (value) => { + return { + '--tw-backdrop-contrast': `contrast(${value})`, + '@defaults backdrop-filter': {}, + 'backdrop-filter': cssBackdropFilterValue, + } + }, + }, + { values: theme('backdropContrast') } + ) + }, + + backdropGrayscale: ({ matchUtilities, theme }) => { + matchUtilities( + { + 'backdrop-grayscale': (value) => { + return { + '--tw-backdrop-grayscale': `grayscale(${value})`, + '@defaults backdrop-filter': {}, + 'backdrop-filter': cssBackdropFilterValue, + } + }, + }, + { values: theme('backdropGrayscale') } + ) + }, + + backdropHueRotate: ({ matchUtilities, theme }) => { + matchUtilities( + { + 'backdrop-hue-rotate': (value) => { + return { + '--tw-backdrop-hue-rotate': `hue-rotate(${value})`, + '@defaults backdrop-filter': {}, + 'backdrop-filter': cssBackdropFilterValue, + } + }, + }, + { values: theme('backdropHueRotate'), supportsNegativeValues: true } + ) + }, + + backdropInvert: ({ matchUtilities, theme }) => { + matchUtilities( + { + 'backdrop-invert': (value) => { + return { + '--tw-backdrop-invert': `invert(${value})`, + '@defaults backdrop-filter': {}, + 'backdrop-filter': cssBackdropFilterValue, + } + }, + }, + { values: theme('backdropInvert') } + ) + }, + + backdropOpacity: ({ matchUtilities, theme }) => { + matchUtilities( + { + 'backdrop-opacity': (value) => { + return { + '--tw-backdrop-opacity': `opacity(${value})`, + '@defaults backdrop-filter': {}, + 'backdrop-filter': cssBackdropFilterValue, + } + }, + }, + { values: theme('backdropOpacity') } + ) + }, + + backdropSaturate: ({ matchUtilities, theme }) => { + matchUtilities( + { + 'backdrop-saturate': (value) => { + return { + '--tw-backdrop-saturate': `saturate(${value})`, + '@defaults backdrop-filter': {}, + 'backdrop-filter': cssBackdropFilterValue, + } + }, + }, + { values: theme('backdropSaturate') } + ) + }, + + backdropSepia: ({ matchUtilities, theme }) => { + matchUtilities( + { + 'backdrop-sepia': (value) => { + return { + '--tw-backdrop-sepia': `sepia(${value})`, + '@defaults backdrop-filter': {}, + 'backdrop-filter': cssBackdropFilterValue, + } + }, + }, + { values: theme('backdropSepia') } + ) + }, + + backdropFilter: ({ addDefaults, addUtilities }) => { + addDefaults('backdrop-filter', { + '--tw-backdrop-blur': ' ', + '--tw-backdrop-brightness': ' ', + '--tw-backdrop-contrast': ' ', + '--tw-backdrop-grayscale': ' ', + '--tw-backdrop-hue-rotate': ' ', + '--tw-backdrop-invert': ' ', + '--tw-backdrop-opacity': ' ', + '--tw-backdrop-saturate': ' ', + '--tw-backdrop-sepia': ' ', + }) + addUtilities({ + '.backdrop-filter': { + '@defaults backdrop-filter': {}, + 'backdrop-filter': cssBackdropFilterValue, + }, + '.backdrop-filter-none': { 'backdrop-filter': 'none' }, + }) + }, + + transitionProperty: ({ matchUtilities, theme }) => { + let defaultTimingFunction = theme('transitionTimingFunction.DEFAULT') + let defaultDuration = theme('transitionDuration.DEFAULT') + + matchUtilities( + { + transition: (value) => { + return { + 'transition-property': value, + ...(value === 'none' + ? {} + : { + 'transition-timing-function': defaultTimingFunction, + 'transition-duration': defaultDuration, + }), + } + }, + }, + { values: theme('transitionProperty') } + ) + }, + + transitionDelay: createUtilityPlugin('transitionDelay', [['delay', ['transitionDelay']]]), + transitionDuration: createUtilityPlugin( + 'transitionDuration', + [['duration', ['transitionDuration']]], + { filterDefault: true } + ), + transitionTimingFunction: createUtilityPlugin( + 'transitionTimingFunction', + [['ease', ['transitionTimingFunction']]], + { filterDefault: true } + ), + willChange: createUtilityPlugin('willChange', [['will-change', ['will-change']]]), + content: createUtilityPlugin('content', [ + ['content', ['--tw-content', ['content', 'var(--tw-content)']]], + ]), +} diff --git a/node_modules/tailwindcss/src/css/LICENSE b/node_modules/tailwindcss/src/css/LICENSE new file mode 100644 index 0000000..a1fb039 --- /dev/null +++ b/node_modules/tailwindcss/src/css/LICENSE @@ -0,0 +1,25 @@ +MIT License + +Copyright (c) Nicolas Gallagher +Copyright (c) Jonathan Neal +Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com) +Copyright (c) Adam Wathan +Copyright (c) Jonathan Reinink + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/node_modules/tailwindcss/src/css/preflight.css b/node_modules/tailwindcss/src/css/preflight.css new file mode 100644 index 0000000..e5e52cd --- /dev/null +++ b/node_modules/tailwindcss/src/css/preflight.css @@ -0,0 +1,378 @@ +/* +1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) +2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) +*/ + +*, +::before, +::after { + box-sizing: border-box; /* 1 */ + border-width: 0; /* 2 */ + border-style: solid; /* 2 */ + border-color: theme('borderColor.DEFAULT', currentColor); /* 2 */ +} + +::before, +::after { + --tw-content: ''; +} + +/* +1. Use a consistent sensible line-height in all browsers. +2. Prevent adjustments of font size after orientation changes in iOS. +3. Use a more readable tab size. +4. Use the user's configured `sans` font-family by default. +5. Use the user's configured `sans` font-feature-settings by default. +6. Use the user's configured `sans` font-variation-settings by default. +*/ + +html { + line-height: 1.5; /* 1 */ + -webkit-text-size-adjust: 100%; /* 2 */ + -moz-tab-size: 4; /* 3 */ + tab-size: 4; /* 3 */ + font-family: theme('fontFamily.sans', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"); /* 4 */ + font-feature-settings: theme('fontFamily.sans[1].fontFeatureSettings', normal); /* 5 */ + font-variation-settings: theme('fontFamily.sans[1].fontVariationSettings', normal); /* 6 */ +} + +/* +1. Remove the margin in all browsers. +2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. +*/ + +body { + margin: 0; /* 1 */ + line-height: inherit; /* 2 */ +} + +/* +1. Add the correct height in Firefox. +2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) +3. Ensure horizontal rules are visible by default. +*/ + +hr { + height: 0; /* 1 */ + color: inherit; /* 2 */ + border-top-width: 1px; /* 3 */ +} + +/* +Add the correct text decoration in Chrome, Edge, and Safari. +*/ + +abbr:where([title]) { + text-decoration: underline dotted; +} + +/* +Remove the default font size and weight for headings. +*/ + +h1, +h2, +h3, +h4, +h5, +h6 { + font-size: inherit; + font-weight: inherit; +} + +/* +Reset links to optimize for opt-in styling instead of opt-out. +*/ + +a { + color: inherit; + text-decoration: inherit; +} + +/* +Add the correct font weight in Edge and Safari. +*/ + +b, +strong { + font-weight: bolder; +} + +/* +1. Use the user's configured `mono` font family by default. +2. Correct the odd `em` font sizing in all browsers. +*/ + +code, +kbd, +samp, +pre { + font-family: theme('fontFamily.mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace); /* 1 */ + font-size: 1em; /* 2 */ +} + +/* +Add the correct font size in all browsers. +*/ + +small { + font-size: 80%; +} + +/* +Prevent `sub` and `sup` elements from affecting the line height in all browsers. +*/ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* +1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) +2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) +3. Remove gaps between table borders by default. +*/ + +table { + text-indent: 0; /* 1 */ + border-color: inherit; /* 2 */ + border-collapse: collapse; /* 3 */ +} + +/* +1. Change the font styles in all browsers. +2. Remove the margin in Firefox and Safari. +3. Remove default padding in all browsers. +*/ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; /* 1 */ + font-feature-settings: inherit; /* 1 */ + font-variation-settings: inherit; /* 1 */ + font-size: 100%; /* 1 */ + font-weight: inherit; /* 1 */ + line-height: inherit; /* 1 */ + color: inherit; /* 1 */ + margin: 0; /* 2 */ + padding: 0; /* 3 */ +} + +/* +Remove the inheritance of text transform in Edge and Firefox. +*/ + +button, +select { + text-transform: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Remove default button styles. +*/ + +button, +[type='button'], +[type='reset'], +[type='submit'] { + -webkit-appearance: button; /* 1 */ + background-color: transparent; /* 2 */ + background-image: none; /* 2 */ +} + +/* +Use the modern Firefox focus style for all focusable elements. +*/ + +:-moz-focusring { + outline: auto; +} + +/* +Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) +*/ + +:-moz-ui-invalid { + box-shadow: none; +} + +/* +Add the correct vertical alignment in Chrome and Firefox. +*/ + +progress { + vertical-align: baseline; +} + +/* +Correct the cursor style of increment and decrement buttons in Safari. +*/ + +::-webkit-inner-spin-button, +::-webkit-outer-spin-button { + height: auto; +} + +/* +1. Correct the odd appearance in Chrome and Safari. +2. Correct the outline style in Safari. +*/ + +[type='search'] { + -webkit-appearance: textfield; /* 1 */ + outline-offset: -2px; /* 2 */ +} + +/* +Remove the inner padding in Chrome and Safari on macOS. +*/ + +::-webkit-search-decoration { + -webkit-appearance: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Change font properties to `inherit` in Safari. +*/ + +::-webkit-file-upload-button { + -webkit-appearance: button; /* 1 */ + font: inherit; /* 2 */ +} + +/* +Add the correct display in Chrome and Safari. +*/ + +summary { + display: list-item; +} + +/* +Removes the default spacing and border for appropriate elements. +*/ + +blockquote, +dl, +dd, +h1, +h2, +h3, +h4, +h5, +h6, +hr, +figure, +p, +pre { + margin: 0; +} + +fieldset { + margin: 0; + padding: 0; +} + +legend { + padding: 0; +} + +ol, +ul, +menu { + list-style: none; + margin: 0; + padding: 0; +} + +/* +Reset default styling for dialogs. +*/ +dialog { + padding: 0; +} + +/* +Prevent resizing textareas horizontally by default. +*/ + +textarea { + resize: vertical; +} + +/* +1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) +2. Set the default placeholder color to the user's configured gray 400 color. +*/ + +input::placeholder, +textarea::placeholder { + opacity: 1; /* 1 */ + color: theme('colors.gray.400', #9ca3af); /* 2 */ +} + +/* +Set the default cursor for buttons. +*/ + +button, +[role="button"] { + cursor: pointer; +} + +/* +Make sure disabled buttons don't get the pointer cursor. +*/ +:disabled { + cursor: default; +} + +/* +1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) +2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) + This can trigger a poorly considered lint error in some tools but is included by design. +*/ + +img, +svg, +video, +canvas, +audio, +iframe, +embed, +object { + display: block; /* 1 */ + vertical-align: middle; /* 2 */ +} + +/* +Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) +*/ + +img, +video { + max-width: 100%; + height: auto; +} + +/* Make elements with the HTML hidden attribute stay hidden by default */ +[hidden] { + display: none; +} diff --git a/node_modules/tailwindcss/src/featureFlags.js b/node_modules/tailwindcss/src/featureFlags.js new file mode 100644 index 0000000..76dd373 --- /dev/null +++ b/node_modules/tailwindcss/src/featureFlags.js @@ -0,0 +1,69 @@ +import colors from 'picocolors' +import log from './util/log' + +let defaults = { + optimizeUniversalDefaults: false, + generalizedModifiers: true, + get disableColorOpacityUtilitiesByDefault() { + return __OXIDE__ + }, + get relativeContentPathsByDefault() { + return __OXIDE__ + }, +} + +let featureFlags = { + future: [ + 'hoverOnlyWhenSupported', + 'respectDefaultRingColorOpacity', + 'disableColorOpacityUtilitiesByDefault', + 'relativeContentPathsByDefault', + ], + experimental: [ + 'optimizeUniversalDefaults', + 'generalizedModifiers', + ], +} + +export function flagEnabled(config, flag) { + if (featureFlags.future.includes(flag)) { + return config.future === 'all' || (config?.future?.[flag] ?? defaults[flag] ?? false) + } + + if (featureFlags.experimental.includes(flag)) { + return ( + config.experimental === 'all' || (config?.experimental?.[flag] ?? defaults[flag] ?? false) + ) + } + + return false +} + +function experimentalFlagsEnabled(config) { + if (config.experimental === 'all') { + return featureFlags.experimental + } + + return Object.keys(config?.experimental ?? {}).filter( + (flag) => featureFlags.experimental.includes(flag) && config.experimental[flag] + ) +} + +export function issueFlagNotices(config) { + if (process.env.JEST_WORKER_ID !== undefined) { + return + } + + if (experimentalFlagsEnabled(config).length > 0) { + let changes = experimentalFlagsEnabled(config) + .map((s) => colors.yellow(s)) + .join(', ') + + log.warn('experimental-flags-enabled', [ + `You have enabled experimental features: ${changes}`, + 'Experimental features in Tailwind CSS are not covered by semver, may introduce breaking changes, and can change at any time.', + ]) + } +} + +export default featureFlags diff --git a/node_modules/tailwindcss/src/index.js b/node_modules/tailwindcss/src/index.js new file mode 100644 index 0000000..d0b329b --- /dev/null +++ b/node_modules/tailwindcss/src/index.js @@ -0,0 +1 @@ +module.exports = require('./plugin') diff --git a/node_modules/tailwindcss/src/lib/cacheInvalidation.js b/node_modules/tailwindcss/src/lib/cacheInvalidation.js new file mode 100644 index 0000000..fa13702 --- /dev/null +++ b/node_modules/tailwindcss/src/lib/cacheInvalidation.js @@ -0,0 +1,52 @@ +import crypto from 'crypto' +import * as sharedState from './sharedState' + +/** + * Calculate the hash of a string. + * + * This doesn't need to be cryptographically secure or + * anything like that since it's used only to detect + * when the CSS changes to invalidate the context. + * + * This is wrapped in a try/catch because it's really dependent + * on how Node itself is build and the environment and OpenSSL + * version / build that is installed on the user's machine. + * + * Based on the environment this can just outright fail. + * + * See https://github.com/nodejs/node/issues/40455 + * + * @param {string} str + */ +function getHash(str) { + try { + return crypto.createHash('md5').update(str, 'utf-8').digest('binary') + } catch (err) { + return '' + } +} + +/** + * Determine if the CSS tree is different from the + * previous version for the given `sourcePath`. + * + * @param {string} sourcePath + * @param {import('postcss').Node} root + */ +export function hasContentChanged(sourcePath, root) { + let css = root.toString() + + // We only care about files with @tailwind directives + // Other files use an existing context + if (!css.includes('@tailwind')) { + return false + } + + let existingHash = sharedState.sourceHashMap.get(sourcePath) + let rootHash = getHash(css) + let didChange = existingHash !== rootHash + + sharedState.sourceHashMap.set(sourcePath, rootHash) + + return didChange +} diff --git a/node_modules/tailwindcss/src/lib/collapseAdjacentRules.js b/node_modules/tailwindcss/src/lib/collapseAdjacentRules.js new file mode 100644 index 0000000..119f592 --- /dev/null +++ b/node_modules/tailwindcss/src/lib/collapseAdjacentRules.js @@ -0,0 +1,58 @@ +let comparisonMap = { + atrule: ['name', 'params'], + rule: ['selector'], +} +let types = new Set(Object.keys(comparisonMap)) + +export default function collapseAdjacentRules() { + function collapseRulesIn(root) { + let currentRule = null + root.each((node) => { + if (!types.has(node.type)) { + currentRule = null + return + } + + if (currentRule === null) { + currentRule = node + return + } + + let properties = comparisonMap[node.type] + + if (node.type === 'atrule' && node.name === 'font-face') { + currentRule = node + } else if ( + properties.every( + (property) => + (node[property] ?? '').replace(/\s+/g, ' ') === + (currentRule[property] ?? '').replace(/\s+/g, ' ') + ) + ) { + // An AtRule may not have children (for example if we encounter duplicate @import url(…) rules) + if (node.nodes) { + currentRule.append(node.nodes) + } + + node.remove() + } else { + currentRule = node + } + }) + + // After we've collapsed adjacent rules & at-rules, we need to collapse + // adjacent rules & at-rules that are children of at-rules. + // We do not care about nesting rules because Tailwind CSS + // explicitly does not handle rule nesting on its own as + // the user is expected to use a nesting plugin + root.each((node) => { + if (node.type === 'atrule') { + collapseRulesIn(node) + } + }) + } + + return (root) => { + collapseRulesIn(root) + } +} diff --git a/node_modules/tailwindcss/src/lib/collapseDuplicateDeclarations.js b/node_modules/tailwindcss/src/lib/collapseDuplicateDeclarations.js new file mode 100644 index 0000000..d310d58 --- /dev/null +++ b/node_modules/tailwindcss/src/lib/collapseDuplicateDeclarations.js @@ -0,0 +1,93 @@ +export default function collapseDuplicateDeclarations() { + return (root) => { + root.walkRules((node) => { + let seen = new Map() + let droppable = new Set([]) + let byProperty = new Map() + + node.walkDecls((decl) => { + // This could happen if we have nested selectors. In that case the + // parent will loop over all its declarations but also the declarations + // of nested rules. With this we ensure that we are shallowly checking + // declarations. + if (decl.parent !== node) { + return + } + + if (seen.has(decl.prop)) { + // Exact same value as what we have seen so far + if (seen.get(decl.prop).value === decl.value) { + // Keep the last one, drop the one we've seen so far + droppable.add(seen.get(decl.prop)) + // Override the existing one with the new value. This is necessary + // so that if we happen to have more than one declaration with the + // same value, that we keep removing the previous one. Otherwise we + // will only remove the *first* one. + seen.set(decl.prop, decl) + return + } + + // Not the same value, so we need to check if we can merge it so + // let's collect it first. + if (!byProperty.has(decl.prop)) { + byProperty.set(decl.prop, new Set()) + } + + byProperty.get(decl.prop).add(seen.get(decl.prop)) + byProperty.get(decl.prop).add(decl) + } + + seen.set(decl.prop, decl) + }) + + // Drop all the duplicate declarations with the exact same value we've + // already seen so far. + for (let decl of droppable) { + decl.remove() + } + + // Analyze the declarations based on its unit, drop all the declarations + // with the same unit but the last one in the list. + for (let declarations of byProperty.values()) { + let byUnit = new Map() + + for (let decl of declarations) { + let unit = resolveUnit(decl.value) + if (unit === null) { + // We don't have a unit, so should never try and collapse this + // value. This is because we can't know how to do it in a correct + // way (e.g.: overrides for older browsers) + continue + } + + if (!byUnit.has(unit)) { + byUnit.set(unit, new Set()) + } + + byUnit.get(unit).add(decl) + } + + for (let declarations of byUnit.values()) { + // Get all but the last one + let removableDeclarations = Array.from(declarations).slice(0, -1) + + for (let decl of removableDeclarations) { + decl.remove() + } + } + } + }) + } +} + +let UNITLESS_NUMBER = Symbol('unitless-number') + +function resolveUnit(input) { + let result = /^-?\d*.?\d+([\w%]+)?$/g.exec(input) + + if (result) { + return result[1] ?? UNITLESS_NUMBER + } + + return null +} diff --git a/node_modules/tailwindcss/src/lib/content.js b/node_modules/tailwindcss/src/lib/content.js new file mode 100644 index 0000000..e814efe --- /dev/null +++ b/node_modules/tailwindcss/src/lib/content.js @@ -0,0 +1,208 @@ +// @ts-check + +import fs from 'fs' +import path from 'path' +import isGlob from 'is-glob' +import fastGlob from 'fast-glob' +import normalizePath from 'normalize-path' +import { parseGlob } from '../util/parseGlob' +import { env } from './sharedState' + +/** @typedef {import('../../types/config.js').RawFile} RawFile */ +/** @typedef {import('../../types/config.js').FilePath} FilePath */ + +/** + * @typedef {object} ContentPath + * @property {string} original + * @property {string} base + * @property {string | null} glob + * @property {boolean} ignore + * @property {string} pattern + */ + +/** + * Turn a list of content paths (absolute or not; glob or not) into a list of + * absolute file paths that exist on the filesystem + * + * If there are symlinks in the path then multiple paths will be returned + * one for the symlink and one for the actual file + * + * @param {*} context + * @param {import('tailwindcss').Config} tailwindConfig + * @returns {ContentPath[]} + */ +export function parseCandidateFiles(context, tailwindConfig) { + let files = tailwindConfig.content.files + + // Normalize the file globs + files = files.filter((filePath) => typeof filePath === 'string') + files = files.map(normalizePath) + + // Split into included and excluded globs + let tasks = fastGlob.generateTasks(files) + + /** @type {ContentPath[]} */ + let included = [] + + /** @type {ContentPath[]} */ + let excluded = [] + + for (const task of tasks) { + included.push(...task.positive.map((filePath) => parseFilePath(filePath, false))) + excluded.push(...task.negative.map((filePath) => parseFilePath(filePath, true))) + } + + let paths = [...included, ...excluded] + + // Resolve paths relative to the config file or cwd + paths = resolveRelativePaths(context, paths) + + // Resolve symlinks if possible + paths = paths.flatMap(resolvePathSymlinks) + + // Update cached patterns + paths = paths.map(resolveGlobPattern) + + return paths +} + +/** + * + * @param {string} filePath + * @param {boolean} ignore + * @returns {ContentPath} + */ +function parseFilePath(filePath, ignore) { + let contentPath = { + original: filePath, + base: filePath, + ignore, + pattern: filePath, + glob: null, + } + + if (isGlob(filePath)) { + Object.assign(contentPath, parseGlob(filePath)) + } + + return contentPath +} + +/** + * + * @param {ContentPath} contentPath + * @returns {ContentPath} + */ +function resolveGlobPattern(contentPath) { + // This is required for Windows support to properly pick up Glob paths. + // Afaik, this technically shouldn't be needed but there's probably + // some internal, direct path matching with a normalized path in + // a package which can't handle mixed directory separators + let base = normalizePath(contentPath.base) + + // If the user's file path contains any special characters (like parens) for instance fast-glob + // is like "OOOH SHINY" and treats them as such. So we have to escape the base path to fix this + base = fastGlob.escapePath(base) + + contentPath.pattern = contentPath.glob ? `${base}/${contentPath.glob}` : base + contentPath.pattern = contentPath.ignore ? `!${contentPath.pattern}` : contentPath.pattern + + return contentPath +} + +/** + * Resolve each path relative to the config file (when possible) if the experimental flag is enabled + * Otherwise, resolve relative to the current working directory + * + * @param {any} context + * @param {ContentPath[]} contentPaths + * @returns {ContentPath[]} + */ +function resolveRelativePaths(context, contentPaths) { + let resolveFrom = [] + + // Resolve base paths relative to the config file (when possible) if the experimental flag is enabled + if (context.userConfigPath && context.tailwindConfig.content.relative) { + resolveFrom = [path.dirname(context.userConfigPath)] + } + + return contentPaths.map((contentPath) => { + contentPath.base = path.resolve(...resolveFrom, contentPath.base) + + return contentPath + }) +} + +/** + * Resolve the symlink for the base directory / file in each path + * These are added as additional dependencies to watch for changes because + * some tools (like webpack) will only watch the actual file or directory + * but not the symlink itself even in projects that use monorepos. + * + * @param {ContentPath} contentPath + * @returns {ContentPath[]} + */ +function resolvePathSymlinks(contentPath) { + let paths = [contentPath] + + try { + let resolvedPath = fs.realpathSync(contentPath.base) + if (resolvedPath !== contentPath.base) { + paths.push({ + ...contentPath, + base: resolvedPath, + }) + } + } catch { + // TODO: log this? + } + + return paths +} + +/** + * @param {any} context + * @param {ContentPath[]} candidateFiles + * @param {Map<string, number>} fileModifiedMap + * @returns {[{ content: string, extension: string }[], Map<string, number>]} + */ +export function resolvedChangedContent(context, candidateFiles, fileModifiedMap) { + let changedContent = context.tailwindConfig.content.files + .filter((item) => typeof item.raw === 'string') + .map(({ raw, extension = 'html' }) => ({ content: raw, extension })) + + let [changedFiles, mTimesToCommit] = resolveChangedFiles(candidateFiles, fileModifiedMap) + + for (let changedFile of changedFiles) { + let extension = path.extname(changedFile).slice(1) + changedContent.push({ file: changedFile, extension }) + } + + return [changedContent, mTimesToCommit] +} + +/** + * + * @param {ContentPath[]} candidateFiles + * @param {Map<string, number>} fileModifiedMap + * @returns {[Set<string>, Map<string, number>]} + */ +function resolveChangedFiles(candidateFiles, fileModifiedMap) { + let paths = candidateFiles.map((contentPath) => contentPath.pattern) + let mTimesToCommit = new Map() + + let changedFiles = new Set() + env.DEBUG && console.time('Finding changed files') + let files = fastGlob.sync(paths, { absolute: true }) + for (let file of files) { + let prevModified = fileModifiedMap.get(file) || -Infinity + let modified = fs.statSync(file).mtimeMs + + if (modified > prevModified) { + changedFiles.add(file) + mTimesToCommit.set(file, modified) + } + } + env.DEBUG && console.timeEnd('Finding changed files') + return [changedFiles, mTimesToCommit] +} diff --git a/node_modules/tailwindcss/src/lib/defaultExtractor.js b/node_modules/tailwindcss/src/lib/defaultExtractor.js new file mode 100644 index 0000000..ae546e9 --- /dev/null +++ b/node_modules/tailwindcss/src/lib/defaultExtractor.js @@ -0,0 +1,216 @@ +import { flagEnabled } from '../featureFlags' +import * as regex from './regex' + +export function defaultExtractor(context) { + let patterns = Array.from(buildRegExps(context)) + + /** + * @param {string} content + */ + return (content) => { + /** @type {(string|string)[]} */ + let results = [] + + for (let pattern of patterns) { + for (let result of content.match(pattern) ?? []) { + results.push(clipAtBalancedParens(result)) + } + } + + return results + } +} + +function* buildRegExps(context) { + let separator = context.tailwindConfig.separator + let prefix = + context.tailwindConfig.prefix !== '' + ? regex.optional(regex.pattern([/-?/, regex.escape(context.tailwindConfig.prefix)])) + : '' + + let utility = regex.any([ + // Arbitrary properties (without square brackets) + /\[[^\s:'"`]+:[^\s\[\]]+\]/, + + // Arbitrary properties with balanced square brackets + // This is a targeted fix to continue to allow theme() + // with square brackets to work in arbitrary properties + // while fixing a problem with the regex matching too much + /\[[^\s:'"`\]]+:[^\s]+?\[[^\s]+\][^\s]+?\]/, + + // Utilities + regex.pattern([ + // Utility Name / Group Name + /-?(?:\w+)/, + + // Normal/Arbitrary values + regex.optional( + regex.any([ + regex.pattern([ + // Arbitrary values + /-(?:\w+-)*\[[^\s:]+\]/, + + // Not immediately followed by an `{[(` + /(?![{([]])/, + + // optionally followed by an opacity modifier + /(?:\/[^\s'"`\\><$]*)?/, + ]), + + regex.pattern([ + // Arbitrary values + /-(?:\w+-)*\[[^\s]+\]/, + + // Not immediately followed by an `{[(` + /(?![{([]])/, + + // optionally followed by an opacity modifier + /(?:\/[^\s'"`\\$]*)?/, + ]), + + // Normal values w/o quotes — may include an opacity modifier + /[-\/][^\s'"`\\$={><]*/, + ]) + ), + ]), + ]) + + let variantPatterns = [ + // Without quotes + regex.any([ + // This is here to provide special support for the `@` variant + regex.pattern([/@\[[^\s"'`]+\](\/[^\s"'`]+)?/, separator]), + + // With variant modifier (e.g.: group-[..]/modifier) + regex.pattern([/([^\s"'`\[\\]+-)?\[[^\s"'`]+\]\/\w+/, separator]), + + regex.pattern([/([^\s"'`\[\\]+-)?\[[^\s"'`]+\]/, separator]), + regex.pattern([/[^\s"'`\[\\]+/, separator]), + ]), + + // With quotes allowed + regex.any([ + // With variant modifier (e.g.: group-[..]/modifier) + regex.pattern([/([^\s"'`\[\\]+-)?\[[^\s`]+\]\/\w+/, separator]), + + regex.pattern([/([^\s"'`\[\\]+-)?\[[^\s`]+\]/, separator]), + regex.pattern([/[^\s`\[\\]+/, separator]), + ]), + ] + + for (const variantPattern of variantPatterns) { + yield regex.pattern([ + // Variants + '((?=((', + variantPattern, + ')+))\\2)?', + + // Important (optional) + /!?/, + + prefix, + + utility, + ]) + } + + // 5. Inner matches + yield /[^<>"'`\s.(){}[\]#=%$]*[^<>"'`\s.(){}[\]#=%:$]/g +} + +// We want to capture any "special" characters +// AND the characters immediately following them (if there is one) +let SPECIALS = /([\[\]'"`])([^\[\]'"`])?/g +let ALLOWED_CLASS_CHARACTERS = /[^"'`\s<>\]]+/ + +/** + * Clips a string ensuring that parentheses, quotes, etc… are balanced + * Used for arbitrary values only + * + * We will go past the end of the balanced parens until we find a non-class character + * + * Depth matching behavior: + * w-[calc(100%-theme('spacing[some_key][1.5]'))]'] + * ┬ ┬ ┬┬ ┬ ┬┬ ┬┬┬┬┬┬┬ + * 1 2 3 4 34 3 210 END + * ╰────┴──────────┴────────┴────────┴┴───┴─┴┴┴ + * + * @param {string} input + */ +function clipAtBalancedParens(input) { + // We are care about this for arbitrary values + if (!input.includes('-[')) { + return input + } + + let depth = 0 + let openStringTypes = [] + + // Find all parens, brackets, quotes, etc + // Stop when we end at a balanced pair + // This is naive and will treat mismatched parens as balanced + // This shouldn't be a problem in practice though + let matches = input.matchAll(SPECIALS) + + // We can't use lookbehind assertions because we have to support Safari + // So, instead, we've emulated it using capture groups and we'll re-work the matches to accommodate + matches = Array.from(matches).flatMap((match) => { + const [, ...groups] = match + + return groups.map((group, idx) => + Object.assign([], match, { + index: match.index + idx, + 0: group, + }) + ) + }) + + for (let match of matches) { + let char = match[0] + let inStringType = openStringTypes[openStringTypes.length - 1] + + if (char === inStringType) { + openStringTypes.pop() + } else if (char === "'" || char === '"' || char === '`') { + openStringTypes.push(char) + } + + if (inStringType) { + continue + } else if (char === '[') { + depth++ + continue + } else if (char === ']') { + depth-- + continue + } + + // We've gone one character past the point where we should stop + // This means that there was an extra closing `]` + // We'll clip to just before it + if (depth < 0) { + return input.substring(0, match.index - 1) + } + + // We've finished balancing the brackets but there still may be characters that can be included + // For example in the class `text-[#336699]/[.35]` + // The depth goes to `0` at the closing `]` but goes up again at the `[` + + // If we're at zero and encounter a non-class character then we clip the class there + if (depth === 0 && !ALLOWED_CLASS_CHARACTERS.test(char)) { + return input.substring(0, match.index) + } + } + + return input +} + +// Regular utilities +// {{modifier}:}*{namespace}{-{suffix}}*{/{opacityModifier}}? + +// Arbitrary values +// {{modifier}:}*{namespace}-[{arbitraryValue}]{/{opacityModifier}}? +// arbitraryValue: no whitespace, balanced quotes unless within quotes, balanced brackets unless within quotes + +// Arbitrary properties +// {{modifier}:}*[{validCssPropertyName}:{arbitraryValue}] diff --git a/node_modules/tailwindcss/src/lib/detectNesting.js b/node_modules/tailwindcss/src/lib/detectNesting.js new file mode 100644 index 0000000..03252e2 --- /dev/null +++ b/node_modules/tailwindcss/src/lib/detectNesting.js @@ -0,0 +1,47 @@ +function isRoot(node) { + return node.type === 'root' +} + +function isAtLayer(node) { + return node.type === 'atrule' && node.name === 'layer' +} + +export default function (_context) { + return (root, result) => { + let found = false + + root.walkAtRules('tailwind', (node) => { + if (found) return false + + if (node.parent && !(isRoot(node.parent) || isAtLayer(node.parent))) { + found = true + node.warn( + result, + [ + 'Nested @tailwind rules were detected, but are not supported.', + "Consider using a prefix to scope Tailwind's classes: https://tailwindcss.com/docs/configuration#prefix", + 'Alternatively, use the important selector strategy: https://tailwindcss.com/docs/configuration#selector-strategy', + ].join('\n') + ) + return false + } + }) + + root.walkRules((rule) => { + if (found) return false + + rule.walkRules((nestedRule) => { + found = true + nestedRule.warn( + result, + [ + 'Nested CSS was detected, but CSS nesting has not been configured correctly.', + 'Please enable a CSS nesting plugin *before* Tailwind in your configuration.', + 'See how here: https://tailwindcss.com/docs/using-with-preprocessors#nesting', + ].join('\n') + ) + return false + }) + }) + } +} diff --git a/node_modules/tailwindcss/src/lib/evaluateTailwindFunctions.js b/node_modules/tailwindcss/src/lib/evaluateTailwindFunctions.js new file mode 100644 index 0000000..ff73f46 --- /dev/null +++ b/node_modules/tailwindcss/src/lib/evaluateTailwindFunctions.js @@ -0,0 +1,272 @@ +import dlv from 'dlv' +import didYouMean from 'didyoumean' +import transformThemeValue from '../util/transformThemeValue' +import parseValue from '../value-parser/index' +import { normalizeScreens } from '../util/normalizeScreens' +import buildMediaQuery from '../util/buildMediaQuery' +import { toPath } from '../util/toPath' +import { withAlphaValue } from '../util/withAlphaVariable' +import { parseColorFormat } from '../util/pluginUtils' +import log from '../util/log' + +function isObject(input) { + return typeof input === 'object' && input !== null +} + +function findClosestExistingPath(theme, path) { + let parts = toPath(path) + do { + parts.pop() + + if (dlv(theme, parts) !== undefined) break + } while (parts.length) + + return parts.length ? parts : undefined +} + +function pathToString(path) { + if (typeof path === 'string') return path + return path.reduce((acc, cur, i) => { + if (cur.includes('.')) return `${acc}[${cur}]` + return i === 0 ? cur : `${acc}.${cur}` + }, '') +} + +function list(items) { + return items.map((key) => `'${key}'`).join(', ') +} + +function listKeys(obj) { + return list(Object.keys(obj)) +} + +function validatePath(config, path, defaultValue, themeOpts = {}) { + const pathString = Array.isArray(path) ? pathToString(path) : path.replace(/^['"]+|['"]+$/g, '') + const pathSegments = Array.isArray(path) ? path : toPath(pathString) + const value = dlv(config.theme, pathSegments, defaultValue) + + if (value === undefined) { + let error = `'${pathString}' does not exist in your theme config.` + const parentSegments = pathSegments.slice(0, -1) + const parentValue = dlv(config.theme, parentSegments) + + if (isObject(parentValue)) { + const validKeys = Object.keys(parentValue).filter( + (key) => validatePath(config, [...parentSegments, key]).isValid + ) + const suggestion = didYouMean(pathSegments[pathSegments.length - 1], validKeys) + if (suggestion) { + error += ` Did you mean '${pathToString([...parentSegments, suggestion])}'?` + } else if (validKeys.length > 0) { + error += ` '${pathToString(parentSegments)}' has the following valid keys: ${list( + validKeys + )}` + } + } else { + const closestPath = findClosestExistingPath(config.theme, pathString) + if (closestPath) { + const closestValue = dlv(config.theme, closestPath) + if (isObject(closestValue)) { + error += ` '${pathToString(closestPath)}' has the following keys: ${listKeys( + closestValue + )}` + } else { + error += ` '${pathToString(closestPath)}' is not an object.` + } + } else { + error += ` Your theme has the following top-level keys: ${listKeys(config.theme)}` + } + } + + return { + isValid: false, + error, + } + } + + if ( + !( + typeof value === 'string' || + typeof value === 'number' || + typeof value === 'function' || + value instanceof String || + value instanceof Number || + Array.isArray(value) + ) + ) { + let error = `'${pathString}' was found but does not resolve to a string.` + + if (isObject(value)) { + let validKeys = Object.keys(value).filter( + (key) => validatePath(config, [...pathSegments, key]).isValid + ) + if (validKeys.length) { + error += ` Did you mean something like '${pathToString([...pathSegments, validKeys[0]])}'?` + } + } + + return { + isValid: false, + error, + } + } + + const [themeSection] = pathSegments + + return { + isValid: true, + value: transformThemeValue(themeSection)(value, themeOpts), + } +} + +function extractArgs(node, vNodes, functions) { + vNodes = vNodes.map((vNode) => resolveVNode(node, vNode, functions)) + + let args = [''] + + for (let vNode of vNodes) { + if (vNode.type === 'div' && vNode.value === ',') { + args.push('') + } else { + args[args.length - 1] += parseValue.stringify(vNode) + } + } + + return args +} + +function resolveVNode(node, vNode, functions) { + if (vNode.type === 'function' && functions[vNode.value] !== undefined) { + let args = extractArgs(node, vNode.nodes, functions) + vNode.type = 'word' + vNode.value = functions[vNode.value](node, ...args) + } + + return vNode +} + +function resolveFunctions(node, input, functions) { + let hasAnyFn = Object.keys(functions).some((fn) => input.includes(`${fn}(`)) + if (!hasAnyFn) return input + + return parseValue(input) + .walk((vNode) => { + resolveVNode(node, vNode, functions) + }) + .toString() +} + +let nodeTypePropertyMap = { + atrule: 'params', + decl: 'value', +} + +/** + * @param {string} path + * @returns {Iterable<[path: string, alpha: string|undefined]>} + */ +function* toPaths(path) { + // Strip quotes from beginning and end of string + // This allows the alpha value to be present inside of quotes + path = path.replace(/^['"]+|['"]+$/g, '') + + let matches = path.match(/^([^\s]+)(?![^\[]*\])(?:\s*\/\s*([^\/\s]+))$/) + let alpha = undefined + + yield [path, undefined] + + if (matches) { + path = matches[1] + alpha = matches[2] + + yield [path, alpha] + } +} + +/** + * + * @param {any} config + * @param {string} path + * @param {any} defaultValue + */ +function resolvePath(config, path, defaultValue) { + const results = Array.from(toPaths(path)).map(([path, alpha]) => { + return Object.assign(validatePath(config, path, defaultValue, { opacityValue: alpha }), { + resolvedPath: path, + alpha, + }) + }) + + return results.find((result) => result.isValid) ?? results[0] +} + +export default function (context) { + let config = context.tailwindConfig + + let functions = { + theme: (node, path, ...defaultValue) => { + let { isValid, value, error, alpha } = resolvePath( + config, + path, + defaultValue.length ? defaultValue : undefined + ) + + if (!isValid) { + let parentNode = node.parent + let candidate = parentNode?.raws.tailwind?.candidate + + if (parentNode && candidate !== undefined) { + // Remove this utility from any caches + context.markInvalidUtilityNode(parentNode) + + // Remove the CSS node from the markup + parentNode.remove() + + // Show a warning + log.warn('invalid-theme-key-in-class', [ + `The utility \`${candidate}\` contains an invalid theme value and was not generated.`, + ]) + + return + } + + throw node.error(error) + } + + let maybeColor = parseColorFormat(value) + let isColorFunction = maybeColor !== undefined && typeof maybeColor === 'function' + + if (alpha !== undefined || isColorFunction) { + if (alpha === undefined) { + alpha = 1.0 + } + + value = withAlphaValue(maybeColor, alpha, maybeColor) + } + + return value + }, + screen: (node, screen) => { + screen = screen.replace(/^['"]+/g, '').replace(/['"]+$/g, '') + let screens = normalizeScreens(config.theme.screens) + let screenDefinition = screens.find(({ name }) => name === screen) + + if (!screenDefinition) { + throw node.error(`The '${screen}' screen does not exist in your theme.`) + } + + return buildMediaQuery(screenDefinition) + }, + } + return (root) => { + root.walk((node) => { + let property = nodeTypePropertyMap[node.type] + + if (property === undefined) { + return + } + + node[property] = resolveFunctions(node, node[property], functions) + }) + } +} diff --git a/node_modules/tailwindcss/src/lib/expandApplyAtRules.js b/node_modules/tailwindcss/src/lib/expandApplyAtRules.js new file mode 100644 index 0000000..ed48dbc --- /dev/null +++ b/node_modules/tailwindcss/src/lib/expandApplyAtRules.js @@ -0,0 +1,620 @@ +import postcss from 'postcss' +import parser from 'postcss-selector-parser' + +import { resolveMatches } from './generateRules' +import escapeClassName from '../util/escapeClassName' +import { applyImportantSelector } from '../util/applyImportantSelector' +import { movePseudos } from '../util/pseudoElements' + +/** @typedef {Map<string, [any, import('postcss').Rule[]]>} ApplyCache */ + +function extractClasses(node) { + /** @type {Map<string, Set<string>>} */ + let groups = new Map() + + let container = postcss.root({ nodes: [node.clone()] }) + + container.walkRules((rule) => { + parser((selectors) => { + selectors.walkClasses((classSelector) => { + let parentSelector = classSelector.parent.toString() + + let classes = groups.get(parentSelector) + if (!classes) { + groups.set(parentSelector, (classes = new Set())) + } + + classes.add(classSelector.value) + }) + }).processSync(rule.selector) + }) + + let normalizedGroups = Array.from(groups.values(), (classes) => Array.from(classes)) + let classes = normalizedGroups.flat() + + return Object.assign(classes, { groups: normalizedGroups }) +} + +let selectorExtractor = parser() + +/** + * @param {string} ruleSelectors + */ +function extractSelectors(ruleSelectors) { + return selectorExtractor.astSync(ruleSelectors) +} + +function extractBaseCandidates(candidates, separator) { + let baseClasses = new Set() + + for (let candidate of candidates) { + baseClasses.add(candidate.split(separator).pop()) + } + + return Array.from(baseClasses) +} + +function prefix(context, selector) { + let prefix = context.tailwindConfig.prefix + return typeof prefix === 'function' ? prefix(selector) : prefix + selector +} + +function* pathToRoot(node) { + yield node + while (node.parent) { + yield node.parent + node = node.parent + } +} + +/** + * Only clone the node itself and not its children + * + * @param {*} node + * @param {*} overrides + * @returns + */ +function shallowClone(node, overrides = {}) { + let children = node.nodes + node.nodes = [] + + let tmp = node.clone(overrides) + + node.nodes = children + + return tmp +} + +/** + * Clone just the nodes all the way to the top that are required to represent + * this singular rule in the tree. + * + * For example, if we have CSS like this: + * ```css + * @media (min-width: 768px) { + * @supports (display: grid) { + * .foo { + * display: grid; + * grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + * } + * } + * + * @supports (backdrop-filter: blur(1px)) { + * .bar { + * backdrop-filter: blur(1px); + * } + * } + * + * .baz { + * color: orange; + * } + * } + * ``` + * + * And we're cloning `.bar` it'll return a cloned version of what's required for just that single node: + * + * ```css + * @media (min-width: 768px) { + * @supports (backdrop-filter: blur(1px)) { + * .bar { + * backdrop-filter: blur(1px); + * } + * } + * } + * ``` + * + * @param {import('postcss').Node} node + */ +function nestedClone(node) { + for (let parent of pathToRoot(node)) { + if (node === parent) { + continue + } + + if (parent.type === 'root') { + break + } + + node = shallowClone(parent, { + nodes: [node], + }) + } + + return node +} + +/** + * @param {import('postcss').Root} root + */ +function buildLocalApplyCache(root, context) { + /** @type {ApplyCache} */ + let cache = new Map() + + root.walkRules((rule) => { + // Ignore rules generated by Tailwind + for (let node of pathToRoot(rule)) { + if (node.raws.tailwind?.layer !== undefined) { + return + } + } + + // Clone what's required to represent this singular rule in the tree + let container = nestedClone(rule) + let sort = context.offsets.create('user') + + for (let className of extractClasses(rule)) { + let list = cache.get(className) || [] + cache.set(className, list) + + list.push([ + { + layer: 'user', + sort, + important: false, + }, + container, + ]) + } + }) + + return cache +} + +/** + * @returns {ApplyCache} + */ +function buildApplyCache(applyCandidates, context) { + for (let candidate of applyCandidates) { + if (context.notClassCache.has(candidate) || context.applyClassCache.has(candidate)) { + continue + } + + if (context.classCache.has(candidate)) { + context.applyClassCache.set( + candidate, + context.classCache.get(candidate).map(([meta, rule]) => [meta, rule.clone()]) + ) + continue + } + + let matches = Array.from(resolveMatches(candidate, context)) + + if (matches.length === 0) { + context.notClassCache.add(candidate) + continue + } + + context.applyClassCache.set(candidate, matches) + } + + return context.applyClassCache +} + +/** + * Build a cache only when it's first used + * + * @param {() => ApplyCache} buildCacheFn + * @returns {ApplyCache} + */ +function lazyCache(buildCacheFn) { + let cache = null + + return { + get: (name) => { + cache = cache || buildCacheFn() + + return cache.get(name) + }, + has: (name) => { + cache = cache || buildCacheFn() + + return cache.has(name) + }, + } +} + +/** + * Take a series of multiple caches and merge + * them so they act like one large cache + * + * @param {ApplyCache[]} caches + * @returns {ApplyCache} + */ +function combineCaches(caches) { + return { + get: (name) => caches.flatMap((cache) => cache.get(name) || []), + has: (name) => caches.some((cache) => cache.has(name)), + } +} + +function extractApplyCandidates(params) { + let candidates = params.split(/[\s\t\n]+/g) + + if (candidates[candidates.length - 1] === '!important') { + return [candidates.slice(0, -1), true] + } + + return [candidates, false] +} + +function processApply(root, context, localCache) { + let applyCandidates = new Set() + + // Collect all @apply rules and candidates + let applies = [] + root.walkAtRules('apply', (rule) => { + let [candidates] = extractApplyCandidates(rule.params) + + for (let util of candidates) { + applyCandidates.add(util) + } + + applies.push(rule) + }) + + // Start the @apply process if we have rules with @apply in them + if (applies.length === 0) { + return + } + + // Fill up some caches! + let applyClassCache = combineCaches([localCache, buildApplyCache(applyCandidates, context)]) + + /** + * When we have an apply like this: + * + * .abc { + * @apply hover:font-bold; + * } + * + * What we essentially will do is resolve to this: + * + * .abc { + * @apply .hover\:font-bold:hover { + * font-weight: 500; + * } + * } + * + * Notice that the to-be-applied class is `.hover\:font-bold:hover` and that the utility candidate was `hover:font-bold`. + * What happens in this function is that we prepend a `.` and escape the candidate. + * This will result in `.hover\:font-bold` + * Which means that we can replace `.hover\:font-bold` with `.abc` in `.hover\:font-bold:hover` resulting in `.abc:hover` + * + * @param {string} selector + * @param {string} utilitySelectors + * @param {string} candidate + */ + function replaceSelector(selector, utilitySelectors, candidate) { + let selectorList = extractSelectors(selector) + let utilitySelectorsList = extractSelectors(utilitySelectors) + let candidateList = extractSelectors(`.${escapeClassName(candidate)}`) + let candidateClass = candidateList.nodes[0].nodes[0] + + selectorList.each((sel) => { + /** @type {Set<import('postcss-selector-parser').Selector>} */ + let replaced = new Set() + + utilitySelectorsList.each((utilitySelector) => { + let hasReplaced = false + utilitySelector = utilitySelector.clone() + + utilitySelector.walkClasses((node) => { + if (node.value !== candidateClass.value) { + return + } + + // Don't replace multiple instances of the same class + // This is theoretically correct but only partially + // We'd need to generate every possible permutation of the replacement + // For example with `.foo + .foo { … }` and `section { @apply foo; }` + // We'd need to generate all of these: + // - `.foo + .foo` + // - `.foo + section` + // - `section + .foo` + // - `section + section` + if (hasReplaced) { + return + } + + // Since you can only `@apply` class names this is sufficient + // We want to replace the matched class name with the selector the user is using + // Ex: Replace `.text-blue-500` with `.foo.bar:is(.something-cool)` + node.replaceWith(...sel.nodes.map((node) => node.clone())) + + // Record that we did something and we want to use this new selector + replaced.add(utilitySelector) + + hasReplaced = true + }) + }) + + // Sort tag names before class names (but only sort each group (separated by a combinator) + // separately and not in total) + // This happens when replacing `.bar` in `.foo.bar` with a tag like `section` + for (let sel of replaced) { + let groups = [[]] + for (let node of sel.nodes) { + if (node.type === 'combinator') { + groups.push(node) + groups.push([]) + } else { + let last = groups[groups.length - 1] + last.push(node) + } + } + + sel.nodes = [] + + for (let group of groups) { + if (Array.isArray(group)) { + group.sort((a, b) => { + if (a.type === 'tag' && b.type === 'class') { + return -1 + } else if (a.type === 'class' && b.type === 'tag') { + return 1 + } else if (a.type === 'class' && b.type === 'pseudo' && b.value.startsWith('::')) { + return -1 + } else if (a.type === 'pseudo' && a.value.startsWith('::') && b.type === 'class') { + return 1 + } + + return 0 + }) + } + + sel.nodes = sel.nodes.concat(group) + } + } + + sel.replaceWith(...replaced) + }) + + return selectorList.toString() + } + + let perParentApplies = new Map() + + // Collect all apply candidates and their rules + for (let apply of applies) { + let [candidates] = perParentApplies.get(apply.parent) || [[], apply.source] + + perParentApplies.set(apply.parent, [candidates, apply.source]) + + let [applyCandidates, important] = extractApplyCandidates(apply.params) + + if (apply.parent.type === 'atrule') { + if (apply.parent.name === 'screen') { + let screenType = apply.parent.params + + throw apply.error( + `@apply is not supported within nested at-rules like @screen. We suggest you write this as @apply ${applyCandidates + .map((c) => `${screenType}:${c}`) + .join(' ')} instead.` + ) + } + + throw apply.error( + `@apply is not supported within nested at-rules like @${apply.parent.name}. You can fix this by un-nesting @${apply.parent.name}.` + ) + } + + for (let applyCandidate of applyCandidates) { + if ([prefix(context, 'group'), prefix(context, 'peer')].includes(applyCandidate)) { + // TODO: Link to specific documentation page with error code. + throw apply.error(`@apply should not be used with the '${applyCandidate}' utility`) + } + + if (!applyClassCache.has(applyCandidate)) { + throw apply.error( + `The \`${applyCandidate}\` class does not exist. If \`${applyCandidate}\` is a custom class, make sure it is defined within a \`@layer\` directive.` + ) + } + + let rules = applyClassCache.get(applyCandidate) + + candidates.push([applyCandidate, important, rules]) + } + } + + for (let [parent, [candidates, atApplySource]] of perParentApplies) { + let siblings = [] + + for (let [applyCandidate, important, rules] of candidates) { + let potentialApplyCandidates = [ + applyCandidate, + ...extractBaseCandidates([applyCandidate], context.tailwindConfig.separator), + ] + + for (let [meta, node] of rules) { + let parentClasses = extractClasses(parent) + let nodeClasses = extractClasses(node) + + // When we encounter a rule like `.dark .a, .b { … }` we only want to be left with `[.dark, .a]` if the base applyCandidate is `.a` or with `[.b]` if the base applyCandidate is `.b` + // So we've split them into groups + nodeClasses = nodeClasses.groups + .filter((classList) => + classList.some((className) => potentialApplyCandidates.includes(className)) + ) + .flat() + + // Add base utility classes from the @apply node to the list of + // classes to check whether it intersects and therefore results in a + // circular dependency or not. + // + // E.g.: + // .foo { + // @apply hover:a; // This applies "a" but with a modifier + // } + // + // We only have to do that with base classes of the `node`, not of the `parent` + // E.g.: + // .hover\:foo { + // @apply bar; + // } + // .bar { + // @apply foo; + // } + // + // This should not result in a circular dependency because we are + // just applying `.foo` and the rule above is `.hover\:foo` which is + // unrelated. However, if we were to apply `hover:foo` then we _did_ + // have to include this one. + nodeClasses = nodeClasses.concat( + extractBaseCandidates(nodeClasses, context.tailwindConfig.separator) + ) + + let intersects = parentClasses.some((selector) => nodeClasses.includes(selector)) + if (intersects) { + throw node.error( + `You cannot \`@apply\` the \`${applyCandidate}\` utility here because it creates a circular dependency.` + ) + } + + let root = postcss.root({ nodes: [node.clone()] }) + + // Make sure every node in the entire tree points back at the @apply rule that generated it + root.walk((node) => { + node.source = atApplySource + }) + + let canRewriteSelector = + node.type !== 'atrule' || (node.type === 'atrule' && node.name !== 'keyframes') + + if (canRewriteSelector) { + root.walkRules((rule) => { + // Let's imagine you have the following structure: + // + // .foo { + // @apply bar; + // } + // + // @supports (a: b) { + // .bar { + // color: blue + // } + // + // .something-unrelated {} + // } + // + // In this case we want to apply `.bar` but it happens to be in + // an atrule node. We clone that node instead of the nested one + // because we still want that @supports rule to be there once we + // applied everything. + // + // However it happens to be that the `.something-unrelated` is + // also in that same shared @supports atrule. This is not good, + // and this should not be there. The good part is that this is + // a clone already and it can be safely removed. The question is + // how do we know we can remove it. Basically what we can do is + // match it against the applyCandidate that you want to apply. If + // it doesn't match the we can safely delete it. + // + // If we didn't do this, then the `replaceSelector` function + // would have replaced this with something that didn't exist and + // therefore it removed the selector altogether. In this specific + // case it would result in `{}` instead of `.something-unrelated {}` + if (!extractClasses(rule).some((candidate) => candidate === applyCandidate)) { + rule.remove() + return + } + + // Strip the important selector from the parent selector if at the beginning + let importantSelector = + typeof context.tailwindConfig.important === 'string' + ? context.tailwindConfig.important + : null + + // We only want to move the "important" selector if this is a Tailwind-generated utility + // We do *not* want to do this for user CSS that happens to be structured the same + let isGenerated = parent.raws.tailwind !== undefined + + let parentSelector = + isGenerated && importantSelector && parent.selector.indexOf(importantSelector) === 0 + ? parent.selector.slice(importantSelector.length) + : parent.selector + + // If the selector becomes empty after replacing the important selector + // This means that it's the same as the parent selector and we don't want to replace it + // Otherwise we'll crash + if (parentSelector === '') { + parentSelector = parent.selector + } + + rule.selector = replaceSelector(parentSelector, rule.selector, applyCandidate) + + // And then re-add it if it was removed + if (importantSelector && parentSelector !== parent.selector) { + rule.selector = applyImportantSelector(rule.selector, importantSelector) + } + + rule.walkDecls((d) => { + d.important = meta.important || important + }) + + // Move pseudo elements to the end of the selector (if necessary) + let selector = parser().astSync(rule.selector) + selector.each((sel) => movePseudos(sel)) + rule.selector = selector.toString() + }) + } + + // It could be that the node we were inserted was removed because the class didn't match + // If that was the *only* rule in the parent, then we have nothing add so we skip it + if (!root.nodes[0]) { + continue + } + + // Insert it + siblings.push([meta.sort, root.nodes[0]]) + } + } + + // Inject the rules, sorted, correctly + let nodes = context.offsets.sort(siblings).map((s) => s[1]) + + // `parent` refers to the node at `.abc` in: .abc { @apply mt-2 } + parent.after(nodes) + } + + for (let apply of applies) { + // If there are left-over declarations, just remove the @apply + if (apply.parent.nodes.length > 1) { + apply.remove() + } else { + // The node is empty, drop the full node + apply.parent.remove() + } + } + + // Do it again, in case we have other `@apply` rules + processApply(root, context, localCache) +} + +export default function expandApplyAtRules(context) { + return (root) => { + // Build a cache of the user's CSS so we can use it to resolve classes used by @apply + let localCache = lazyCache(() => buildLocalApplyCache(root, context)) + + processApply(root, context, localCache) + } +} diff --git a/node_modules/tailwindcss/src/lib/expandTailwindAtRules.js b/node_modules/tailwindcss/src/lib/expandTailwindAtRules.js new file mode 100644 index 0000000..2933d6f --- /dev/null +++ b/node_modules/tailwindcss/src/lib/expandTailwindAtRules.js @@ -0,0 +1,297 @@ +import fs from 'fs' +import LRU from '@alloc/quick-lru' +import * as sharedState from './sharedState' +import { generateRules } from './generateRules' +import log from '../util/log' +import cloneNodes from '../util/cloneNodes' +import { defaultExtractor } from './defaultExtractor' + +let env = sharedState.env + +const builtInExtractors = { + DEFAULT: defaultExtractor, +} + +const builtInTransformers = { + DEFAULT: (content) => content, + svelte: (content) => content.replace(/(?:^|\s)class:/g, ' '), +} + +function getExtractor(context, fileExtension) { + let extractors = context.tailwindConfig.content.extract + + return ( + extractors[fileExtension] || + extractors.DEFAULT || + builtInExtractors[fileExtension] || + builtInExtractors.DEFAULT(context) + ) +} + +function getTransformer(tailwindConfig, fileExtension) { + let transformers = tailwindConfig.content.transform + + return ( + transformers[fileExtension] || + transformers.DEFAULT || + builtInTransformers[fileExtension] || + builtInTransformers.DEFAULT + ) +} + +let extractorCache = new WeakMap() + +// Scans template contents for possible classes. This is a hot path on initial build but +// not too important for subsequent builds. The faster the better though — if we can speed +// up these regexes by 50% that could cut initial build time by like 20%. +function getClassCandidates(content, extractor, candidates, seen) { + if (!extractorCache.has(extractor)) { + extractorCache.set(extractor, new LRU({ maxSize: 25000 })) + } + + for (let line of content.split('\n')) { + line = line.trim() + + if (seen.has(line)) { + continue + } + seen.add(line) + + if (extractorCache.get(extractor).has(line)) { + for (let match of extractorCache.get(extractor).get(line)) { + candidates.add(match) + } + } else { + let extractorMatches = extractor(line).filter((s) => s !== '!*') + let lineMatchesSet = new Set(extractorMatches) + + for (let match of lineMatchesSet) { + candidates.add(match) + } + + extractorCache.get(extractor).set(line, lineMatchesSet) + } + } +} + +/** + * + * @param {[import('./offsets.js').RuleOffset, import('postcss').Node][]} rules + * @param {*} context + */ +function buildStylesheet(rules, context) { + let sortedRules = context.offsets.sort(rules) + + let returnValue = { + base: new Set(), + defaults: new Set(), + components: new Set(), + utilities: new Set(), + variants: new Set(), + } + + for (let [sort, rule] of sortedRules) { + returnValue[sort.layer].add(rule) + } + + return returnValue +} + +export default function expandTailwindAtRules(context) { + return async (root) => { + let layerNodes = { + base: null, + components: null, + utilities: null, + variants: null, + } + + root.walkAtRules((rule) => { + // Make sure this file contains Tailwind directives. If not, we can save + // a lot of work and bail early. Also we don't have to register our touch + // file as a dependency since the output of this CSS does not depend on + // the source of any templates. Think Vue <style> blocks for example. + if (rule.name === 'tailwind') { + if (Object.keys(layerNodes).includes(rule.params)) { + layerNodes[rule.params] = rule + } + } + }) + + if (Object.values(layerNodes).every((n) => n === null)) { + return root + } + + // --- + + // Find potential rules in changed files + let candidates = new Set([...(context.candidates ?? []), sharedState.NOT_ON_DEMAND]) + let seen = new Set() + + env.DEBUG && console.time('Reading changed files') + + if (__OXIDE__) { + // TODO: Pass through or implement `extractor` + for (let candidate of require('@tailwindcss/oxide').parseCandidateStringsFromFiles( + context.changedContent + // Object.assign({}, builtInTransformers, context.tailwindConfig.content.transform) + )) { + candidates.add(candidate) + } + + // for (let { file, content, extension } of context.changedContent) { + // let transformer = getTransformer(context.tailwindConfig, extension) + // let extractor = getExtractor(context, extension) + // getClassCandidatesOxide(file, transformer(content), extractor, candidates, seen) + // } + } else { + /** @type {[item: {file?: string, content?: string}, meta: {transformer: any, extractor: any}][]} */ + let regexParserContent = [] + + for (let item of context.changedContent) { + let transformer = getTransformer(context.tailwindConfig, item.extension) + let extractor = getExtractor(context, item.extension) + regexParserContent.push([item, { transformer, extractor }]) + } + + const BATCH_SIZE = 500 + + for (let i = 0; i < regexParserContent.length; i += BATCH_SIZE) { + let batch = regexParserContent.slice(i, i + BATCH_SIZE) + await Promise.all( + batch.map(async ([{ file, content }, { transformer, extractor }]) => { + content = file ? await fs.promises.readFile(file, 'utf8') : content + getClassCandidates(transformer(content), extractor, candidates, seen) + }) + ) + } + } + + env.DEBUG && console.timeEnd('Reading changed files') + + // --- + + // Generate the actual CSS + let classCacheCount = context.classCache.size + + env.DEBUG && console.time('Generate rules') + env.DEBUG && console.time('Sorting candidates') + let sortedCandidates = __OXIDE__ + ? candidates + : new Set( + [...candidates].sort((a, z) => { + if (a === z) return 0 + if (a < z) return -1 + return 1 + }) + ) + env.DEBUG && console.timeEnd('Sorting candidates') + generateRules(sortedCandidates, context) + env.DEBUG && console.timeEnd('Generate rules') + + // We only ever add to the classCache, so if it didn't grow, there is nothing new. + env.DEBUG && console.time('Build stylesheet') + if (context.stylesheetCache === null || context.classCache.size !== classCacheCount) { + context.stylesheetCache = buildStylesheet([...context.ruleCache], context) + } + env.DEBUG && console.timeEnd('Build stylesheet') + + let { + defaults: defaultNodes, + base: baseNodes, + components: componentNodes, + utilities: utilityNodes, + variants: screenNodes, + } = context.stylesheetCache + + // --- + + // Replace any Tailwind directives with generated CSS + + if (layerNodes.base) { + layerNodes.base.before( + cloneNodes([...baseNodes, ...defaultNodes], layerNodes.base.source, { + layer: 'base', + }) + ) + layerNodes.base.remove() + } + + if (layerNodes.components) { + layerNodes.components.before( + cloneNodes([...componentNodes], layerNodes.components.source, { + layer: 'components', + }) + ) + layerNodes.components.remove() + } + + if (layerNodes.utilities) { + layerNodes.utilities.before( + cloneNodes([...utilityNodes], layerNodes.utilities.source, { + layer: 'utilities', + }) + ) + layerNodes.utilities.remove() + } + + // We do post-filtering to not alter the emitted order of the variants + const variantNodes = Array.from(screenNodes).filter((node) => { + const parentLayer = node.raws.tailwind?.parentLayer + + if (parentLayer === 'components') { + return layerNodes.components !== null + } + + if (parentLayer === 'utilities') { + return layerNodes.utilities !== null + } + + return true + }) + + if (layerNodes.variants) { + layerNodes.variants.before( + cloneNodes(variantNodes, layerNodes.variants.source, { + layer: 'variants', + }) + ) + layerNodes.variants.remove() + } else if (variantNodes.length > 0) { + root.append( + cloneNodes(variantNodes, root.source, { + layer: 'variants', + }) + ) + } + + // If we've got a utility layer and no utilities are generated there's likely something wrong + const hasUtilityVariants = variantNodes.some( + (node) => node.raws.tailwind?.parentLayer === 'utilities' + ) + + if (layerNodes.utilities && utilityNodes.size === 0 && !hasUtilityVariants) { + log.warn('content-problems', [ + 'No utility classes were detected in your source files. If this is unexpected, double-check the `content` option in your Tailwind CSS configuration.', + 'https://tailwindcss.com/docs/content-configuration', + ]) + } + + // --- + + if (env.DEBUG) { + console.log('Potential classes: ', candidates.size) + console.log('Active contexts: ', sharedState.contextSourcesMap.size) + } + + // Clear the cache for the changed files + context.changedContent = [] + + // Cleanup any leftover @layer atrules + root.walkAtRules('layer', (rule) => { + if (Object.keys(layerNodes).includes(rule.params)) { + rule.remove() + } + }) + } +} diff --git a/node_modules/tailwindcss/src/lib/findAtConfigPath.js b/node_modules/tailwindcss/src/lib/findAtConfigPath.js new file mode 100644 index 0000000..ac0adab --- /dev/null +++ b/node_modules/tailwindcss/src/lib/findAtConfigPath.js @@ -0,0 +1,48 @@ +import fs from 'fs' +import path from 'path' + +/** + * Find the @config at-rule in the given CSS AST and return the relative path to the config file + * + * @param {import('postcss').Root} root + * @param {import('postcss').Result} result + */ +export function findAtConfigPath(root, result) { + let configPath = null + let relativeTo = null + + root.walkAtRules('config', (rule) => { + relativeTo = rule.source?.input.file ?? result.opts.from ?? null + + if (relativeTo === null) { + throw rule.error( + 'The `@config` directive cannot be used without setting `from` in your PostCSS config.' + ) + } + + if (configPath) { + throw rule.error('Only one `@config` directive is allowed per file.') + } + + let matches = rule.params.match(/(['"])(.*?)\1/) + if (!matches) { + throw rule.error('A path is required when using the `@config` directive.') + } + + let inputPath = matches[2] + if (path.isAbsolute(inputPath)) { + throw rule.error('The `@config` directive cannot be used with an absolute path.') + } + + configPath = path.resolve(path.dirname(relativeTo), inputPath) + if (!fs.existsSync(configPath)) { + throw rule.error( + `The config file at "${inputPath}" does not exist. Make sure the path is correct and the file exists.` + ) + } + + rule.remove() + }) + + return configPath ? configPath : null +} diff --git a/node_modules/tailwindcss/src/lib/generateRules.js b/node_modules/tailwindcss/src/lib/generateRules.js new file mode 100644 index 0000000..69b6827 --- /dev/null +++ b/node_modules/tailwindcss/src/lib/generateRules.js @@ -0,0 +1,936 @@ +import postcss from 'postcss' +import selectorParser from 'postcss-selector-parser' +import parseObjectStyles from '../util/parseObjectStyles' +import isPlainObject from '../util/isPlainObject' +import prefixSelector from '../util/prefixSelector' +import { updateAllClasses, getMatchingTypes } from '../util/pluginUtils' +import log from '../util/log' +import * as sharedState from './sharedState' +import { + formatVariantSelector, + finalizeSelector, + eliminateIrrelevantSelectors, +} from '../util/formatVariantSelector' +import { asClass } from '../util/nameClass' +import { normalize } from '../util/dataTypes' +import { isValidVariantFormatString, parseVariant, INTERNAL_FEATURES } from './setupContextUtils' +import isValidArbitraryValue from '../util/isSyntacticallyValidPropertyValue' +import { splitAtTopLevelOnly } from '../util/splitAtTopLevelOnly.js' +import { flagEnabled } from '../featureFlags' +import { applyImportantSelector } from '../util/applyImportantSelector' + +let classNameParser = selectorParser((selectors) => { + return selectors.first.filter(({ type }) => type === 'class').pop().value +}) + +export function getClassNameFromSelector(selector) { + return classNameParser.transformSync(selector) +} + +// Generate match permutations for a class candidate, like: +// ['ring-offset-blue', '100'] +// ['ring-offset', 'blue-100'] +// ['ring', 'offset-blue-100'] +// Example with dynamic classes: +// ['grid-cols', '[[linename],1fr,auto]'] +// ['grid', 'cols-[[linename],1fr,auto]'] +function* candidatePermutations(candidate) { + let lastIndex = Infinity + + while (lastIndex >= 0) { + let dashIdx + let wasSlash = false + + if (lastIndex === Infinity && candidate.endsWith(']')) { + let bracketIdx = candidate.indexOf('[') + + // If character before `[` isn't a dash or a slash, this isn't a dynamic class + // eg. string[] + if (candidate[bracketIdx - 1] === '-') { + dashIdx = bracketIdx - 1 + } else if (candidate[bracketIdx - 1] === '/') { + dashIdx = bracketIdx - 1 + wasSlash = true + } else { + dashIdx = -1 + } + } else if (lastIndex === Infinity && candidate.includes('/')) { + dashIdx = candidate.lastIndexOf('/') + wasSlash = true + } else { + dashIdx = candidate.lastIndexOf('-', lastIndex) + } + + if (dashIdx < 0) { + break + } + + let prefix = candidate.slice(0, dashIdx) + let modifier = candidate.slice(wasSlash ? dashIdx : dashIdx + 1) + + lastIndex = dashIdx - 1 + + // TODO: This feels a bit hacky + if (prefix === '' || modifier === '/') { + continue + } + + yield [prefix, modifier] + } +} + +function applyPrefix(matches, context) { + if (matches.length === 0 || context.tailwindConfig.prefix === '') { + return matches + } + + for (let match of matches) { + let [meta] = match + if (meta.options.respectPrefix) { + let container = postcss.root({ nodes: [match[1].clone()] }) + let classCandidate = match[1].raws.tailwind.classCandidate + + container.walkRules((r) => { + // If this is a negative utility with a dash *before* the prefix we + // have to ensure that the generated selector matches the candidate + + // Not doing this will cause `-tw-top-1` to generate the class `.tw--top-1` + // The disconnect between candidate <-> class can cause @apply to hard crash. + let shouldPrependNegative = classCandidate.startsWith('-') + + r.selector = prefixSelector( + context.tailwindConfig.prefix, + r.selector, + shouldPrependNegative + ) + }) + + match[1] = container.nodes[0] + } + } + + return matches +} + +function applyImportant(matches, classCandidate) { + if (matches.length === 0) { + return matches + } + + let result = [] + + for (let [meta, rule] of matches) { + let container = postcss.root({ nodes: [rule.clone()] }) + + container.walkRules((r) => { + let ast = selectorParser().astSync(r.selector) + + // Remove extraneous selectors that do not include the base candidate + ast.each((sel) => eliminateIrrelevantSelectors(sel, classCandidate)) + + // Update all instances of the base candidate to include the important marker + updateAllClasses(ast, (className) => + className === classCandidate ? `!${className}` : className + ) + + r.selector = ast.toString() + + r.walkDecls((d) => (d.important = true)) + }) + + result.push([{ ...meta, important: true }, container.nodes[0]]) + } + + return result +} + +// Takes a list of rule tuples and applies a variant like `hover`, sm`, +// whatever to it. We used to do some extra caching here to avoid generating +// a variant of the same rule more than once, but this was never hit because +// we cache at the entire selector level further up the tree. +// +// Technically you can get a cache hit if you have `hover:focus:text-center` +// and `focus:hover:text-center` in the same project, but it doesn't feel +// worth the complexity for that case. + +function applyVariant(variant, matches, context) { + if (matches.length === 0) { + return matches + } + + /** @type {{modifier: string | null, value: string | null}} */ + let args = { modifier: null, value: sharedState.NONE } + + // Retrieve "modifier" + { + let [baseVariant, ...modifiers] = splitAtTopLevelOnly(variant, '/') + + // This is a hack to support variants with `/` in them, like `ar-1/10/20:text-red-500` + // In this case 1/10 is a value but /20 is a modifier + if (modifiers.length > 1) { + baseVariant = baseVariant + '/' + modifiers.slice(0, -1).join('/') + modifiers = modifiers.slice(-1) + } + + if (modifiers.length && !context.variantMap.has(variant)) { + variant = baseVariant + args.modifier = modifiers[0] + + if (!flagEnabled(context.tailwindConfig, 'generalizedModifiers')) { + return [] + } + } + } + + // Retrieve "arbitrary value" + if (variant.endsWith(']') && !variant.startsWith('[')) { + // We either have: + // @[200px] + // group-[:hover] + // + // But we don't want: + // @-[200px] (`-` is incorrect) + // group[:hover] (`-` is missing) + let match = /(.)(-?)\[(.*)\]/g.exec(variant) + if (match) { + let [, char, separator, value] = match + // @-[200px] case + if (char === '@' && separator === '-') return [] + // group[:hover] case + if (char !== '@' && separator === '') return [] + + variant = variant.replace(`${separator}[${value}]`, '') + args.value = value + } + } + + // Register arbitrary variants + if (isArbitraryValue(variant) && !context.variantMap.has(variant)) { + let sort = context.offsets.recordVariant(variant) + + let selector = normalize(variant.slice(1, -1)) + let selectors = splitAtTopLevelOnly(selector, ',') + + // We do not support multiple selectors for arbitrary variants + if (selectors.length > 1) { + return [] + } + + if (!selectors.every(isValidVariantFormatString)) { + return [] + } + + let records = selectors.map((sel, idx) => [ + context.offsets.applyParallelOffset(sort, idx), + parseVariant(sel.trim()), + ]) + + context.variantMap.set(variant, records) + } + + if (context.variantMap.has(variant)) { + let isArbitraryVariant = isArbitraryValue(variant) + let internalFeatures = context.variantOptions.get(variant)?.[INTERNAL_FEATURES] ?? {} + let variantFunctionTuples = context.variantMap.get(variant).slice() + let result = [] + + let respectPrefix = (() => { + if (isArbitraryVariant) return false + if (internalFeatures.respectPrefix === false) return false + return true + })() + + for (let [meta, rule] of matches) { + // Don't generate variants for user css + if (meta.layer === 'user') { + continue + } + + let container = postcss.root({ nodes: [rule.clone()] }) + + for (let [variantSort, variantFunction, containerFromArray] of variantFunctionTuples) { + let clone = (containerFromArray ?? container).clone() + let collectedFormats = [] + + function prepareBackup() { + // Already prepared, chicken out + if (clone.raws.neededBackup) { + return + } + clone.raws.neededBackup = true + clone.walkRules((rule) => (rule.raws.originalSelector = rule.selector)) + } + + function modifySelectors(modifierFunction) { + prepareBackup() + clone.each((rule) => { + if (rule.type !== 'rule') { + return + } + + rule.selectors = rule.selectors.map((selector) => { + return modifierFunction({ + get className() { + return getClassNameFromSelector(selector) + }, + selector, + }) + }) + }) + + return clone + } + + let ruleWithVariant = variantFunction({ + // Public API + get container() { + prepareBackup() + return clone + }, + separator: context.tailwindConfig.separator, + modifySelectors, + + // Private API for now + wrap(wrapper) { + let nodes = clone.nodes + clone.removeAll() + wrapper.append(nodes) + clone.append(wrapper) + }, + format(selectorFormat) { + collectedFormats.push({ + format: selectorFormat, + respectPrefix, + }) + }, + args, + }) + + // It can happen that a list of format strings is returned from within the function. In that + // case, we have to process them as well. We can use the existing `variantSort`. + if (Array.isArray(ruleWithVariant)) { + for (let [idx, variantFunction] of ruleWithVariant.entries()) { + // This is a little bit scary since we are pushing to an array of items that we are + // currently looping over. However, you can also think of it like a processing queue + // where you keep handling jobs until everything is done and each job can queue more + // jobs if needed. + variantFunctionTuples.push([ + context.offsets.applyParallelOffset(variantSort, idx), + variantFunction, + + // If the clone has been modified we have to pass that back + // though so each rule can use the modified container + clone.clone(), + ]) + } + continue + } + + if (typeof ruleWithVariant === 'string') { + collectedFormats.push({ + format: ruleWithVariant, + respectPrefix, + }) + } + + if (ruleWithVariant === null) { + continue + } + + // We had to backup selectors, therefore we assume that somebody touched + // `container` or `modifySelectors`. Let's see if they did, so that we + // can restore the selectors, and collect the format strings. + if (clone.raws.neededBackup) { + delete clone.raws.neededBackup + clone.walkRules((rule) => { + let before = rule.raws.originalSelector + if (!before) return + delete rule.raws.originalSelector + if (before === rule.selector) return // No mutation happened + + let modified = rule.selector + + // Rebuild the base selector, this is what plugin authors would do + // as well. E.g.: `${variant}${separator}${className}`. + // However, plugin authors probably also prepend or append certain + // classes, pseudos, ids, ... + let rebuiltBase = selectorParser((selectors) => { + selectors.walkClasses((classNode) => { + classNode.value = `${variant}${context.tailwindConfig.separator}${classNode.value}` + }) + }).processSync(before) + + // Now that we know the original selector, the new selector, and + // the rebuild part in between, we can replace the part that plugin + // authors need to rebuild with `&`, and eventually store it in the + // collectedFormats. Similar to what `format('...')` would do. + // + // E.g.: + // variant: foo + // selector: .markdown > p + // modified (by plugin): .foo .foo\\:markdown > p + // rebuiltBase (internal): .foo\\:markdown > p + // format: .foo & + collectedFormats.push({ + format: modified.replace(rebuiltBase, '&'), + respectPrefix, + }) + rule.selector = before + }) + } + + // This tracks the originating layer for the variant + // For example: + // .sm:underline {} is a variant of something in the utilities layer + // .sm:container {} is a variant of the container component + clone.nodes[0].raws.tailwind = { ...clone.nodes[0].raws.tailwind, parentLayer: meta.layer } + + let withOffset = [ + { + ...meta, + sort: context.offsets.applyVariantOffset( + meta.sort, + variantSort, + Object.assign(args, context.variantOptions.get(variant)) + ), + collectedFormats: (meta.collectedFormats ?? []).concat(collectedFormats), + }, + clone.nodes[0], + ] + result.push(withOffset) + } + } + + return result + } + + return [] +} + +function parseRules(rule, cache, options = {}) { + // PostCSS node + if (!isPlainObject(rule) && !Array.isArray(rule)) { + return [[rule], options] + } + + // Tuple + if (Array.isArray(rule)) { + return parseRules(rule[0], cache, rule[1]) + } + + // Simple object + if (!cache.has(rule)) { + cache.set(rule, parseObjectStyles(rule)) + } + + return [cache.get(rule), options] +} + +const IS_VALID_PROPERTY_NAME = /^[a-z_-]/ + +function isValidPropName(name) { + return IS_VALID_PROPERTY_NAME.test(name) +} + +/** + * @param {string} declaration + * @returns {boolean} + */ +function looksLikeUri(declaration) { + // Quick bailout for obvious non-urls + // This doesn't support schemes that don't use a leading // but that's unlikely to be a problem + if (!declaration.includes('://')) { + return false + } + + try { + const url = new URL(declaration) + return url.scheme !== '' && url.host !== '' + } catch (err) { + // Definitely not a valid url + return false + } +} + +function isParsableNode(node) { + let isParsable = true + + node.walkDecls((decl) => { + if (!isParsableCssValue(decl.prop, decl.value)) { + isParsable = false + return false + } + }) + + return isParsable +} + +function isParsableCssValue(property, value) { + // We don't want to to treat [https://example.com] as a custom property + // Even though, according to the CSS grammar, it's a totally valid CSS declaration + // So we short-circuit here by checking if the custom property looks like a url + if (looksLikeUri(`${property}:${value}`)) { + return false + } + + try { + postcss.parse(`a{${property}:${value}}`).toResult() + return true + } catch (err) { + return false + } +} + +function extractArbitraryProperty(classCandidate, context) { + let [, property, value] = classCandidate.match(/^\[([a-zA-Z0-9-_]+):(\S+)\]$/) ?? [] + + if (value === undefined) { + return null + } + + if (!isValidPropName(property)) { + return null + } + + if (!isValidArbitraryValue(value)) { + return null + } + + let normalized = normalize(value, { property }) + + if (!isParsableCssValue(property, normalized)) { + return null + } + + let sort = context.offsets.arbitraryProperty() + + return [ + [ + { sort, layer: 'utilities' }, + () => ({ + [asClass(classCandidate)]: { + [property]: normalized, + }, + }), + ], + ] +} + +function* resolveMatchedPlugins(classCandidate, context) { + if (context.candidateRuleMap.has(classCandidate)) { + yield [context.candidateRuleMap.get(classCandidate), 'DEFAULT'] + } + + yield* (function* (arbitraryPropertyRule) { + if (arbitraryPropertyRule !== null) { + yield [arbitraryPropertyRule, 'DEFAULT'] + } + })(extractArbitraryProperty(classCandidate, context)) + + let candidatePrefix = classCandidate + let negative = false + + const twConfigPrefix = context.tailwindConfig.prefix + + const twConfigPrefixLen = twConfigPrefix.length + + const hasMatchingPrefix = + candidatePrefix.startsWith(twConfigPrefix) || candidatePrefix.startsWith(`-${twConfigPrefix}`) + + if (candidatePrefix[twConfigPrefixLen] === '-' && hasMatchingPrefix) { + negative = true + candidatePrefix = twConfigPrefix + candidatePrefix.slice(twConfigPrefixLen + 1) + } + + if (negative && context.candidateRuleMap.has(candidatePrefix)) { + yield [context.candidateRuleMap.get(candidatePrefix), '-DEFAULT'] + } + + for (let [prefix, modifier] of candidatePermutations(candidatePrefix)) { + if (context.candidateRuleMap.has(prefix)) { + yield [context.candidateRuleMap.get(prefix), negative ? `-${modifier}` : modifier] + } + } +} + +function splitWithSeparator(input, separator) { + if (input === sharedState.NOT_ON_DEMAND) { + return [sharedState.NOT_ON_DEMAND] + } + + return splitAtTopLevelOnly(input, separator) +} + +function* recordCandidates(matches, classCandidate) { + for (const match of matches) { + match[1].raws.tailwind = { + ...match[1].raws.tailwind, + classCandidate, + preserveSource: match[0].options?.preserveSource ?? false, + } + + yield match + } +} + +function* resolveMatches(candidate, context) { + let separator = context.tailwindConfig.separator + let [classCandidate, ...variants] = splitWithSeparator(candidate, separator).reverse() + let important = false + + if (classCandidate.startsWith('!')) { + important = true + classCandidate = classCandidate.slice(1) + } + + // TODO: Reintroduce this in ways that doesn't break on false positives + // function sortAgainst(toSort, against) { + // return toSort.slice().sort((a, z) => { + // return bigSign(against.get(a)[0] - against.get(z)[0]) + // }) + // } + // let sorted = sortAgainst(variants, context.variantMap) + // if (sorted.toString() !== variants.toString()) { + // let corrected = sorted.reverse().concat(classCandidate).join(':') + // throw new Error(`Class ${candidate} should be written as ${corrected}`) + // } + + for (let matchedPlugins of resolveMatchedPlugins(classCandidate, context)) { + let matches = [] + let typesByMatches = new Map() + + let [plugins, modifier] = matchedPlugins + let isOnlyPlugin = plugins.length === 1 + + for (let [sort, plugin] of plugins) { + let matchesPerPlugin = [] + + if (typeof plugin === 'function') { + for (let ruleSet of [].concat(plugin(modifier, { isOnlyPlugin }))) { + let [rules, options] = parseRules(ruleSet, context.postCssNodeCache) + for (let rule of rules) { + matchesPerPlugin.push([{ ...sort, options: { ...sort.options, ...options } }, rule]) + } + } + } + // Only process static plugins on exact matches + else if (modifier === 'DEFAULT' || modifier === '-DEFAULT') { + let ruleSet = plugin + let [rules, options] = parseRules(ruleSet, context.postCssNodeCache) + for (let rule of rules) { + matchesPerPlugin.push([{ ...sort, options: { ...sort.options, ...options } }, rule]) + } + } + + if (matchesPerPlugin.length > 0) { + let matchingTypes = Array.from( + getMatchingTypes( + sort.options?.types ?? [], + modifier, + sort.options ?? {}, + context.tailwindConfig + ) + ).map(([_, type]) => type) + + if (matchingTypes.length > 0) { + typesByMatches.set(matchesPerPlugin, matchingTypes) + } + + matches.push(matchesPerPlugin) + } + } + + if (isArbitraryValue(modifier)) { + if (matches.length > 1) { + // Partition plugins in 2 categories so that we can start searching in the plugins that + // don't have `any` as a type first. + let [withAny, withoutAny] = matches.reduce( + (group, plugin) => { + let hasAnyType = plugin.some(([{ options }]) => + options.types.some(({ type }) => type === 'any') + ) + + if (hasAnyType) { + group[0].push(plugin) + } else { + group[1].push(plugin) + } + return group + }, + [[], []] + ) + + function findFallback(matches) { + // If only a single plugin matches, let's take that one + if (matches.length === 1) { + return matches[0] + } + + // Otherwise, find the plugin that creates a valid rule given the arbitrary value, and + // also has the correct type which preferOnConflicts the plugin in case of clashes. + return matches.find((rules) => { + let matchingTypes = typesByMatches.get(rules) + return rules.some(([{ options }, rule]) => { + if (!isParsableNode(rule)) { + return false + } + + return options.types.some( + ({ type, preferOnConflict }) => matchingTypes.includes(type) && preferOnConflict + ) + }) + }) + } + + // Try to find a fallback plugin, because we already know that multiple plugins matched for + // the given arbitrary value. + let fallback = findFallback(withoutAny) ?? findFallback(withAny) + if (fallback) { + matches = [fallback] + } + + // We couldn't find a fallback plugin which means that there are now multiple plugins that + // generated css for the current candidate. This means that the result is ambiguous and this + // should not happen. We won't generate anything right now, so let's report this to the user + // by logging some options about what they can do. + else { + let typesPerPlugin = matches.map( + (match) => new Set([...(typesByMatches.get(match) ?? [])]) + ) + + // Remove duplicates, so that we can detect proper unique types for each plugin. + for (let pluginTypes of typesPerPlugin) { + for (let type of pluginTypes) { + let removeFromOwnGroup = false + + for (let otherGroup of typesPerPlugin) { + if (pluginTypes === otherGroup) continue + + if (otherGroup.has(type)) { + otherGroup.delete(type) + removeFromOwnGroup = true + } + } + + if (removeFromOwnGroup) pluginTypes.delete(type) + } + } + + let messages = [] + + for (let [idx, group] of typesPerPlugin.entries()) { + for (let type of group) { + let rules = matches[idx] + .map(([, rule]) => rule) + .flat() + .map((rule) => + rule + .toString() + .split('\n') + .slice(1, -1) // Remove selector and closing '}' + .map((line) => line.trim()) + .map((x) => ` ${x}`) // Re-indent + .join('\n') + ) + .join('\n\n') + + messages.push( + ` Use \`${candidate.replace('[', `[${type}:`)}\` for \`${rules.trim()}\`` + ) + break + } + } + + log.warn([ + `The class \`${candidate}\` is ambiguous and matches multiple utilities.`, + ...messages, + `If this is content and not a class, replace it with \`${candidate + .replace('[', '[') + .replace(']', ']')}\` to silence this warning.`, + ]) + continue + } + } + + matches = matches.map((list) => list.filter((match) => isParsableNode(match[1]))) + } + + matches = matches.flat() + matches = Array.from(recordCandidates(matches, classCandidate)) + matches = applyPrefix(matches, context) + + if (important) { + matches = applyImportant(matches, classCandidate) + } + + for (let variant of variants) { + matches = applyVariant(variant, matches, context) + } + + for (let match of matches) { + match[1].raws.tailwind = { ...match[1].raws.tailwind, candidate } + + // Apply final format selector + match = applyFinalFormat(match, { context, candidate }) + + // Skip rules with invalid selectors + // This will cause the candidate to be added to the "not class" + // cache skipping it entirely for future builds + if (match === null) { + continue + } + + yield match + } + } +} + +function applyFinalFormat(match, { context, candidate }) { + if (!match[0].collectedFormats) { + return match + } + + let isValid = true + let finalFormat + + try { + finalFormat = formatVariantSelector(match[0].collectedFormats, { + context, + candidate, + }) + } catch { + // The format selector we produced is invalid + // This could be because: + // - A bug exists + // - A plugin introduced an invalid variant selector (ex: `addVariant('foo', '&;foo')`) + // - The user used an invalid arbitrary variant (ex: `[&;foo]:underline`) + // Either way the build will fail because of this + // We would rather that the build pass "silently" given that this could + // happen because of picking up invalid things when scanning content + // So we'll throw out the candidate instead + + return null + } + + let container = postcss.root({ nodes: [match[1].clone()] }) + + container.walkRules((rule) => { + if (inKeyframes(rule)) { + return + } + + try { + let selector = finalizeSelector(rule.selector, finalFormat, { + candidate, + context, + }) + + // Finalize Selector determined that this candidate is irrelevant + // TODO: This elimination should happen earlier so this never happens + if (selector === null) { + rule.remove() + return + } + + rule.selector = selector + } catch { + // If this selector is invalid we also want to skip it + // But it's likely that being invalid here means there's a bug in a plugin rather than too loosely matching content + isValid = false + return false + } + }) + + if (!isValid) { + return null + } + + match[1] = container.nodes[0] + + return match +} + +function inKeyframes(rule) { + return rule.parent && rule.parent.type === 'atrule' && rule.parent.name === 'keyframes' +} + +function getImportantStrategy(important) { + if (important === true) { + return (rule) => { + if (inKeyframes(rule)) { + return + } + + rule.walkDecls((d) => { + if (d.parent.type === 'rule' && !inKeyframes(d.parent)) { + d.important = true + } + }) + } + } + + if (typeof important === 'string') { + return (rule) => { + if (inKeyframes(rule)) { + return + } + + rule.selectors = rule.selectors.map((selector) => { + return applyImportantSelector(selector, important) + }) + } + } +} + +function generateRules(candidates, context, isSorting = false) { + let allRules = [] + let strategy = getImportantStrategy(context.tailwindConfig.important) + + for (let candidate of candidates) { + if (context.notClassCache.has(candidate)) { + continue + } + + if (context.candidateRuleCache.has(candidate)) { + allRules = allRules.concat(Array.from(context.candidateRuleCache.get(candidate))) + continue + } + + let matches = Array.from(resolveMatches(candidate, context)) + + if (matches.length === 0) { + context.notClassCache.add(candidate) + continue + } + + context.classCache.set(candidate, matches) + + let rules = context.candidateRuleCache.get(candidate) ?? new Set() + context.candidateRuleCache.set(candidate, rules) + + for (const match of matches) { + let [{ sort, options }, rule] = match + + if (options.respectImportant && strategy) { + let container = postcss.root({ nodes: [rule.clone()] }) + container.walkRules(strategy) + rule = container.nodes[0] + } + + // Note: We have to clone rules during sorting + // so we eliminate some shared mutable state + let newEntry = [sort, isSorting ? rule.clone() : rule] + rules.add(newEntry) + context.ruleCache.add(newEntry) + allRules.push(newEntry) + } + } + + return allRules +} + +function isArbitraryValue(input) { + return input.startsWith('[') && input.endsWith(']') +} + +export { resolveMatches, generateRules } diff --git a/node_modules/tailwindcss/src/lib/getModuleDependencies.js b/node_modules/tailwindcss/src/lib/getModuleDependencies.js new file mode 100644 index 0000000..e6a38a8 --- /dev/null +++ b/node_modules/tailwindcss/src/lib/getModuleDependencies.js @@ -0,0 +1,79 @@ +import fs from 'fs' +import path from 'path' + +let jsExtensions = ['.js', '.cjs', '.mjs'] + +// Given the current file `a.ts`, we want to make sure that when importing `b` that we resolve +// `b.ts` before `b.js` +// +// E.g.: +// +// a.ts +// b // .ts +// c // .ts +// a.js +// b // .js or .ts + +let jsResolutionOrder = ['', '.js', '.cjs', '.mjs', '.ts', '.cts', '.mts', '.jsx', '.tsx'] +let tsResolutionOrder = ['', '.ts', '.cts', '.mts', '.tsx', '.js', '.cjs', '.mjs', '.jsx'] + +function resolveWithExtension(file, extensions) { + // Try to find `./a.ts`, `./a.ts`, ... from `./a` + for (let ext of extensions) { + let full = `${file}${ext}` + if (fs.existsSync(full) && fs.statSync(full).isFile()) { + return full + } + } + + // Try to find `./a/index.js` from `./a` + for (let ext of extensions) { + let full = `${file}/index${ext}` + if (fs.existsSync(full)) { + return full + } + } + + return null +} + +function* _getModuleDependencies(filename, base, seen, ext = path.extname(filename)) { + // Try to find the file + let absoluteFile = resolveWithExtension( + path.resolve(base, filename), + jsExtensions.includes(ext) ? jsResolutionOrder : tsResolutionOrder + ) + if (absoluteFile === null) return // File doesn't exist + + // Prevent infinite loops when there are circular dependencies + if (seen.has(absoluteFile)) return // Already seen + seen.add(absoluteFile) + + // Mark the file as a dependency + yield absoluteFile + + // Resolve new base for new imports/requires + base = path.dirname(absoluteFile) + ext = path.extname(absoluteFile) + + let contents = fs.readFileSync(absoluteFile, 'utf-8') + + // Find imports/requires + for (let match of [ + ...contents.matchAll(/import[\s\S]*?['"](.{3,}?)['"]/gi), + ...contents.matchAll(/import[\s\S]*from[\s\S]*?['"](.{3,}?)['"]/gi), + ...contents.matchAll(/require\(['"`](.+)['"`]\)/gi), + ]) { + // Bail out if it's not a relative file + if (!match[1].startsWith('.')) continue + + yield* _getModuleDependencies(match[1], base, seen, ext) + } +} + +export default function getModuleDependencies(absoluteFilePath) { + if (absoluteFilePath === null) return new Set() + return new Set( + _getModuleDependencies(absoluteFilePath, path.dirname(absoluteFilePath), new Set()) + ) +} diff --git a/node_modules/tailwindcss/src/lib/load-config.ts b/node_modules/tailwindcss/src/lib/load-config.ts new file mode 100644 index 0000000..645e8e1 --- /dev/null +++ b/node_modules/tailwindcss/src/lib/load-config.ts @@ -0,0 +1,31 @@ +import jitiFactory from 'jiti' +import { transform } from 'sucrase' + +import { Config } from '../../types/config' + +let jiti: ReturnType<typeof jitiFactory> | null = null +function lazyJiti() { + return ( + jiti ?? + (jiti = jitiFactory(__filename, { + interopDefault: true, + transform: (opts) => { + return transform(opts.source, { + transforms: ['typescript', 'imports'], + }) + }, + })) + ) +} + +export function loadConfig(path: string): Config { + let config = (function () { + try { + return path ? require(path) : {} + } catch { + return lazyJiti()(path) + } + })() + + return config.default ?? config +} diff --git a/node_modules/tailwindcss/src/lib/normalizeTailwindDirectives.js b/node_modules/tailwindcss/src/lib/normalizeTailwindDirectives.js new file mode 100644 index 0000000..3349a7e --- /dev/null +++ b/node_modules/tailwindcss/src/lib/normalizeTailwindDirectives.js @@ -0,0 +1,84 @@ +import log from '../util/log' + +export default function normalizeTailwindDirectives(root) { + let tailwindDirectives = new Set() + let layerDirectives = new Set() + let applyDirectives = new Set() + + root.walkAtRules((atRule) => { + if (atRule.name === 'apply') { + applyDirectives.add(atRule) + } + + if (atRule.name === 'import') { + if (atRule.params === '"tailwindcss/base"' || atRule.params === "'tailwindcss/base'") { + atRule.name = 'tailwind' + atRule.params = 'base' + } else if ( + atRule.params === '"tailwindcss/components"' || + atRule.params === "'tailwindcss/components'" + ) { + atRule.name = 'tailwind' + atRule.params = 'components' + } else if ( + atRule.params === '"tailwindcss/utilities"' || + atRule.params === "'tailwindcss/utilities'" + ) { + atRule.name = 'tailwind' + atRule.params = 'utilities' + } else if ( + atRule.params === '"tailwindcss/screens"' || + atRule.params === "'tailwindcss/screens'" || + atRule.params === '"tailwindcss/variants"' || + atRule.params === "'tailwindcss/variants'" + ) { + atRule.name = 'tailwind' + atRule.params = 'variants' + } + } + + if (atRule.name === 'tailwind') { + if (atRule.params === 'screens') { + atRule.params = 'variants' + } + tailwindDirectives.add(atRule.params) + } + + if (['layer', 'responsive', 'variants'].includes(atRule.name)) { + if (['responsive', 'variants'].includes(atRule.name)) { + log.warn(`${atRule.name}-at-rule-deprecated`, [ + `The \`@${atRule.name}\` directive has been deprecated in Tailwind CSS v3.0.`, + `Use \`@layer utilities\` or \`@layer components\` instead.`, + 'https://tailwindcss.com/docs/upgrade-guide#replace-variants-with-layer', + ]) + } + layerDirectives.add(atRule) + } + }) + + if ( + !tailwindDirectives.has('base') || + !tailwindDirectives.has('components') || + !tailwindDirectives.has('utilities') + ) { + for (let rule of layerDirectives) { + if (rule.name === 'layer' && ['base', 'components', 'utilities'].includes(rule.params)) { + if (!tailwindDirectives.has(rule.params)) { + throw rule.error( + `\`@layer ${rule.params}\` is used but no matching \`@tailwind ${rule.params}\` directive is present.` + ) + } + } else if (rule.name === 'responsive') { + if (!tailwindDirectives.has('utilities')) { + throw rule.error('`@responsive` is used but `@tailwind utilities` is missing.') + } + } else if (rule.name === 'variants') { + if (!tailwindDirectives.has('utilities')) { + throw rule.error('`@variants` is used but `@tailwind utilities` is missing.') + } + } + } + } + + return { tailwindDirectives, applyDirectives } +} diff --git a/node_modules/tailwindcss/src/lib/offsets.js b/node_modules/tailwindcss/src/lib/offsets.js new file mode 100644 index 0000000..a43ebe4 --- /dev/null +++ b/node_modules/tailwindcss/src/lib/offsets.js @@ -0,0 +1,373 @@ +// @ts-check + +import bigSign from '../util/bigSign' +import { remapBitfield } from './remap-bitfield.js' + +/** + * @typedef {'base' | 'defaults' | 'components' | 'utilities' | 'variants' | 'user'} Layer + */ + +/** + * @typedef {object} VariantOption + * @property {number} id An unique identifier to identify `matchVariant` + * @property {function | undefined} sort The sort function + * @property {string|null} value The value we want to compare + * @property {string|null} modifier The modifier that was used (if any) + * @property {bigint} variant The variant bitmask + */ + +/** + * @typedef {object} RuleOffset + * @property {Layer} layer The layer that this rule belongs to + * @property {Layer} parentLayer The layer that this rule originally belonged to. Only different from layer if this is a variant. + * @property {bigint} arbitrary 0n if false, 1n if true + * @property {bigint} variants Dynamic size. 1 bit per registered variant. 0n means no variants + * @property {bigint} parallelIndex Rule index for the parallel variant. 0 if not applicable. + * @property {bigint} index Index of the rule / utility in it's given *parent* layer. Monotonically increasing. + * @property {VariantOption[]} options Some information on how we can sort arbitrary variants + */ + +export class Offsets { + constructor() { + /** + * Offsets for the next rule in a given layer + * + * @type {Record<Layer, bigint>} + */ + this.offsets = { + defaults: 0n, + base: 0n, + components: 0n, + utilities: 0n, + variants: 0n, + user: 0n, + } + + /** + * Positions for a given layer + * + * @type {Record<Layer, bigint>} + */ + this.layerPositions = { + defaults: 0n, + base: 1n, + components: 2n, + utilities: 3n, + + // There isn't technically a "user" layer, but we need to give it a position + // Because it's used for ordering user-css from @apply + user: 4n, + + variants: 5n, + } + + /** + * The total number of functions currently registered across all variants (including arbitrary variants) + * + * @type {bigint} + */ + this.reservedVariantBits = 0n + + /** + * Positions for a given variant + * + * @type {Map<string, bigint>} + */ + this.variantOffsets = new Map() + } + + /** + * @param {Layer} layer + * @returns {RuleOffset} + */ + create(layer) { + return { + layer, + parentLayer: layer, + arbitrary: 0n, + variants: 0n, + parallelIndex: 0n, + index: this.offsets[layer]++, + options: [], + } + } + + /** + * @returns {RuleOffset} + */ + arbitraryProperty() { + return { + ...this.create('utilities'), + arbitrary: 1n, + } + } + + /** + * Get the offset for a variant + * + * @param {string} variant + * @param {number} index + * @returns {RuleOffset} + */ + forVariant(variant, index = 0) { + let offset = this.variantOffsets.get(variant) + if (offset === undefined) { + throw new Error(`Cannot find offset for unknown variant ${variant}`) + } + + return { + ...this.create('variants'), + variants: offset << BigInt(index), + } + } + + /** + * @param {RuleOffset} rule + * @param {RuleOffset} variant + * @param {VariantOption} options + * @returns {RuleOffset} + */ + applyVariantOffset(rule, variant, options) { + options.variant = variant.variants + + return { + ...rule, + layer: 'variants', + parentLayer: rule.layer === 'variants' ? rule.parentLayer : rule.layer, + variants: rule.variants | variant.variants, + options: options.sort ? [].concat(options, rule.options) : rule.options, + + // TODO: Technically this is wrong. We should be handling parallel index on a per variant basis. + // We'll take the max of all the parallel indexes for now. + // @ts-ignore + parallelIndex: max([rule.parallelIndex, variant.parallelIndex]), + } + } + + /** + * @param {RuleOffset} offset + * @param {number} parallelIndex + * @returns {RuleOffset} + */ + applyParallelOffset(offset, parallelIndex) { + return { + ...offset, + parallelIndex: BigInt(parallelIndex), + } + } + + /** + * Each variant gets 1 bit per function / rule registered. + * This is because multiple variants can be applied to a single rule and we need to know which ones are present and which ones are not. + * Additionally, every unique group of variants is grouped together in the stylesheet. + * + * This grouping is order-independent. For instance, we do not differentiate between `hover:focus` and `focus:hover`. + * + * @param {string[]} variants + * @param {(name: string) => number} getLength + */ + recordVariants(variants, getLength) { + for (let variant of variants) { + this.recordVariant(variant, getLength(variant)) + } + } + + /** + * The same as `recordVariants` but for a single arbitrary variant at runtime. + * @param {string} variant + * @param {number} fnCount + * + * @returns {RuleOffset} The highest offset for this variant + */ + recordVariant(variant, fnCount = 1) { + this.variantOffsets.set(variant, 1n << this.reservedVariantBits) + + // Ensure space is reserved for each "function" in the parallel variant + // by offsetting the next variant by the number of parallel variants + // in the one we just added. + + // Single functions that return parallel variants are NOT handled separately here + // They're offset by 1 (or the number of functions) as usual + // And each rule returned is tracked separately since the functions are evaluated lazily. + // @see `RuleOffset.parallelIndex` + this.reservedVariantBits += BigInt(fnCount) + + return { + ...this.create('variants'), + variants: this.variantOffsets.get(variant), + } + } + + /** + * @param {RuleOffset} a + * @param {RuleOffset} b + * @returns {bigint} + */ + compare(a, b) { + // Sort layers together + if (a.layer !== b.layer) { + return this.layerPositions[a.layer] - this.layerPositions[b.layer] + } + + // When sorting the `variants` layer, we need to sort based on the parent layer as well within + // this variants layer. + if (a.parentLayer !== b.parentLayer) { + return this.layerPositions[a.parentLayer] - this.layerPositions[b.parentLayer] + } + + // Sort based on the sorting function + for (let aOptions of a.options) { + for (let bOptions of b.options) { + if (aOptions.id !== bOptions.id) continue + if (!aOptions.sort || !bOptions.sort) continue + + let maxFnVariant = max([aOptions.variant, bOptions.variant]) ?? 0n + + // Create a mask of 0s from bits 1..N where N represents the mask of the Nth bit + let mask = ~(maxFnVariant | (maxFnVariant - 1n)) + let aVariantsAfterFn = a.variants & mask + let bVariantsAfterFn = b.variants & mask + + // If the variants the same, we _can_ sort them + if (aVariantsAfterFn !== bVariantsAfterFn) { + continue + } + + let result = aOptions.sort( + { + value: aOptions.value, + modifier: aOptions.modifier, + }, + { + value: bOptions.value, + modifier: bOptions.modifier, + } + ) + if (result !== 0) return result + } + } + + // Sort variants in the order they were registered + if (a.variants !== b.variants) { + return a.variants - b.variants + } + + // Make sure each rule returned by a parallel variant is sorted in ascending order + if (a.parallelIndex !== b.parallelIndex) { + return a.parallelIndex - b.parallelIndex + } + + // Always sort arbitrary properties after other utilities + if (a.arbitrary !== b.arbitrary) { + return a.arbitrary - b.arbitrary + } + + // Sort utilities, components, etc… in the order they were registered + return a.index - b.index + } + + /** + * Arbitrary variants are recorded in the order they're encountered. + * This means that the order is not stable between environments and sets of content files. + * + * In order to make the order stable, we need to remap the arbitrary variant offsets to + * be in alphabetical order starting from the offset of the first arbitrary variant. + */ + recalculateVariantOffsets() { + // Sort the variants by their name + let variants = Array.from(this.variantOffsets.entries()) + .filter(([v]) => v.startsWith('[')) + .sort(([a], [z]) => fastCompare(a, z)) + + // Sort the list of offsets + // This is not necessarily a discrete range of numbers which is why + // we're using sort instead of creating a range from min/max + let newOffsets = variants.map(([, offset]) => offset).sort((a, z) => bigSign(a - z)) + + // Create a map from the old offsets to the new offsets in the new sort order + /** @type {[bigint, bigint][]} */ + let mapping = variants.map(([, oldOffset], i) => [oldOffset, newOffsets[i]]) + + // Remove any variants that will not move letting us skip + // remapping if everything happens to be in order + return mapping.filter(([a, z]) => a !== z) + } + + /** + * @template T + * @param {[RuleOffset, T][]} list + * @returns {[RuleOffset, T][]} + */ + remapArbitraryVariantOffsets(list) { + let mapping = this.recalculateVariantOffsets() + + // No arbitrary variants? Nothing to do. + // Everyhing already in order? Nothing to do. + if (mapping.length === 0) { + return list + } + + // Remap every variant offset in the list + return list.map((item) => { + let [offset, rule] = item + + offset = { + ...offset, + variants: remapBitfield(offset.variants, mapping), + } + + return [offset, rule] + }) + } + + /** + * @template T + * @param {[RuleOffset, T][]} list + * @returns {[RuleOffset, T][]} + */ + sort(list) { + list = this.remapArbitraryVariantOffsets(list) + + return list.sort(([a], [b]) => bigSign(this.compare(a, b))) + } +} + +/** + * + * @param {bigint[]} nums + * @returns {bigint|null} + */ +function max(nums) { + let max = null + + for (const num of nums) { + max = max ?? num + max = max > num ? max : num + } + + return max +} + +/** + * A fast ASCII order string comparison function. + * + * Using `.sort()` without a custom compare function is faster + * But you can only use that if you're sorting an array of + * only strings. If you're sorting strings inside objects + * or arrays, you need must use a custom compare function. + * + * @param {string} a + * @param {string} b + */ +function fastCompare(a, b) { + let aLen = a.length + let bLen = b.length + let minLen = aLen < bLen ? aLen : bLen + + for (let i = 0; i < minLen; i++) { + let cmp = a.charCodeAt(i) - b.charCodeAt(i) + if (cmp !== 0) return cmp + } + + return aLen - bLen +} diff --git a/node_modules/tailwindcss/src/lib/partitionApplyAtRules.js b/node_modules/tailwindcss/src/lib/partitionApplyAtRules.js new file mode 100644 index 0000000..34813c6 --- /dev/null +++ b/node_modules/tailwindcss/src/lib/partitionApplyAtRules.js @@ -0,0 +1,52 @@ +function partitionRules(root) { + if (!root.walkAtRules) return + + let applyParents = new Set() + + root.walkAtRules('apply', (rule) => { + applyParents.add(rule.parent) + }) + + if (applyParents.size === 0) { + return + } + + for (let rule of applyParents) { + let nodeGroups = [] + let lastGroup = [] + + for (let node of rule.nodes) { + if (node.type === 'atrule' && node.name === 'apply') { + if (lastGroup.length > 0) { + nodeGroups.push(lastGroup) + lastGroup = [] + } + nodeGroups.push([node]) + } else { + lastGroup.push(node) + } + } + + if (lastGroup.length > 0) { + nodeGroups.push(lastGroup) + } + + if (nodeGroups.length === 1) { + continue + } + + for (let group of [...nodeGroups].reverse()) { + let clone = rule.clone({ nodes: [] }) + clone.append(group) + rule.after(clone) + } + + rule.remove() + } +} + +export default function expandApplyAtRules() { + return (root) => { + partitionRules(root) + } +} diff --git a/node_modules/tailwindcss/src/lib/regex.js b/node_modules/tailwindcss/src/lib/regex.js new file mode 100644 index 0000000..5db7657 --- /dev/null +++ b/node_modules/tailwindcss/src/lib/regex.js @@ -0,0 +1,74 @@ +const REGEX_SPECIAL = /[\\^$.*+?()[\]{}|]/g +const REGEX_HAS_SPECIAL = RegExp(REGEX_SPECIAL.source) + +/** + * @param {string|RegExp|Array<string|RegExp>} source + */ +function toSource(source) { + source = Array.isArray(source) ? source : [source] + + source = source.map((item) => (item instanceof RegExp ? item.source : item)) + + return source.join('') +} + +/** + * @param {string|RegExp|Array<string|RegExp>} source + */ +export function pattern(source) { + return new RegExp(toSource(source), 'g') +} + +/** + * @param {string|RegExp|Array<string|RegExp>} source + */ +export function withoutCapturing(source) { + return new RegExp(`(?:${toSource(source)})`, 'g') +} + +/** + * @param {Array<string|RegExp>} sources + */ +export function any(sources) { + return `(?:${sources.map(toSource).join('|')})` +} + +/** + * @param {string|RegExp} source + */ +export function optional(source) { + return `(?:${toSource(source)})?` +} + +/** + * @param {string|RegExp|Array<string|RegExp>} source + */ +export function zeroOrMore(source) { + return `(?:${toSource(source)})*` +} + +/** + * Generate a RegExp that matches balanced brackets for a given depth + * We have to specify a depth because JS doesn't support recursive groups using ?R + * + * Based on https://stackoverflow.com/questions/17759004/how-to-match-string-within-parentheses-nested-in-java/17759264#17759264 + * + * @param {string|RegExp|Array<string|RegExp>} source + */ +export function nestedBrackets(open, close, depth = 1) { + return withoutCapturing([ + escape(open), + /[^\s]*/, + depth === 1 + ? `[^${escape(open)}${escape(close)}\s]*` + : any([`[^${escape(open)}${escape(close)}\s]*`, nestedBrackets(open, close, depth - 1)]), + /[^\s]*/, + escape(close), + ]) +} + +export function escape(string) { + return string && REGEX_HAS_SPECIAL.test(string) + ? string.replace(REGEX_SPECIAL, '\\$&') + : string || '' +} diff --git a/node_modules/tailwindcss/src/lib/remap-bitfield.js b/node_modules/tailwindcss/src/lib/remap-bitfield.js new file mode 100644 index 0000000..3ddaf20 --- /dev/null +++ b/node_modules/tailwindcss/src/lib/remap-bitfield.js @@ -0,0 +1,82 @@ +// @ts-check + +/** + * We must remap all the old bits to new bits for each set variant + * Only arbitrary variants are considered as those are the only + * ones that need to be re-sorted at this time + * + * An iterated process that removes and sets individual bits simultaneously + * will not work because we may have a new bit that is also a later old bit + * This means that we would be removing a previously set bit which we don't + * want to do + * + * For example (assume `bN` = `1<<N`) + * Given the "total" mapping `[[b1, b3], [b2, b4], [b3, b1], [b4, b2]]` + * The mapping is "total" because: + * 1. Every input and output is accounted for + * 2. All combinations are unique + * 3. No one input maps to multiple outputs and vice versa + * And, given an offset with all bits set: + * V = b1 | b2 | b3 | b4 + * + * Let's explore the issue with removing and setting bits simultaneously: + * V & ~b1 | b3 = b2 | b3 | b4 + * V & ~b2 | b4 = b3 | b4 + * V & ~b3 | b1 = b1 | b4 + * V & ~b4 | b2 = b1 | b2 + * + * As you can see, we end up with the wrong result. + * This is because we're removing a bit that was previously set. + * And, thus the final result is missing b3 and b4. + * + * Now, let's explore the issue with removing the bits first: + * V & ~b1 = b2 | b3 | b4 + * V & ~b2 = b3 | b4 + * V & ~b3 = b4 + * V & ~b4 = 0 + * + * And then setting the bits: + * V | b3 = b3 + * V | b4 = b3 | b4 + * V | b1 = b1 | b3 | b4 + * V | b2 = b1 | b2 | b3 | b4 + * + * We get the correct result because we're not removing any bits that were + * previously set thus properly remapping the bits to the new order + * + * To collect this into a single operation that can be done simultaneously + * we must first create a mask for the old bits that are set and a mask for + * the new bits that are set. Then we can remove the old bits and set the new + * bits simultaneously in a "single" operation like so: + * OldMask = b1 | b2 | b3 | b4 + * NewMask = b3 | b4 | b1 | b2 + * + * So this: + * V & ~oldMask | newMask + * + * Expands to this: + * V & ~b1 & ~b2 & ~b3 & ~b4 | b3 | b4 | b1 | b2 + * + * Which becomes this: + * b1 | b2 | b3 | b4 + * + * Which is the correct result! + * + * @param {bigint} num + * @param {[bigint, bigint][]} mapping + */ +export function remapBitfield(num, mapping) { + // Create masks for the old and new bits that are set + let oldMask = 0n + let newMask = 0n + for (let [oldBit, newBit] of mapping) { + if (num & oldBit) { + oldMask = oldMask | oldBit + newMask = newMask | newBit + } + } + + // Remove all old bits + // Set all new bits + return (num & ~oldMask) | newMask +} diff --git a/node_modules/tailwindcss/src/lib/resolveDefaultsAtRules.js b/node_modules/tailwindcss/src/lib/resolveDefaultsAtRules.js new file mode 100644 index 0000000..389ea4b --- /dev/null +++ b/node_modules/tailwindcss/src/lib/resolveDefaultsAtRules.js @@ -0,0 +1,163 @@ +import postcss from 'postcss' +import selectorParser from 'postcss-selector-parser' +import { flagEnabled } from '../featureFlags' + +let getNode = { + id(node) { + return selectorParser.attribute({ + attribute: 'id', + operator: '=', + value: node.value, + quoteMark: '"', + }) + }, +} + +function minimumImpactSelector(nodes) { + let rest = nodes + .filter((node) => { + // Keep non-pseudo nodes + if (node.type !== 'pseudo') return true + + // Keep pseudo nodes that have subnodes + // E.g.: `:not()` contains subnodes inside the parentheses + if (node.nodes.length > 0) return true + + // Keep pseudo `elements` + // This implicitly means that we ignore pseudo `classes` + return ( + node.value.startsWith('::') || + [':before', ':after', ':first-line', ':first-letter'].includes(node.value) + ) + }) + .reverse() + + let searchFor = new Set(['tag', 'class', 'id', 'attribute']) + + let splitPointIdx = rest.findIndex((n) => searchFor.has(n.type)) + if (splitPointIdx === -1) return rest.reverse().join('').trim() + + let node = rest[splitPointIdx] + let bestNode = getNode[node.type] ? getNode[node.type](node) : node + + rest = rest.slice(0, splitPointIdx) + + let combinatorIdx = rest.findIndex((n) => n.type === 'combinator' && n.value === '>') + if (combinatorIdx !== -1) { + rest.splice(0, combinatorIdx) + rest.unshift(selectorParser.universal()) + } + + return [bestNode, ...rest.reverse()].join('').trim() +} + +export let elementSelectorParser = selectorParser((selectors) => { + return selectors.map((s) => { + let nodes = s.split((n) => n.type === 'combinator' && n.value === ' ').pop() + return minimumImpactSelector(nodes) + }) +}) + +let cache = new Map() + +function extractElementSelector(selector) { + if (!cache.has(selector)) { + cache.set(selector, elementSelectorParser.transformSync(selector)) + } + + return cache.get(selector) +} + +export default function resolveDefaultsAtRules({ tailwindConfig }) { + return (root) => { + let variableNodeMap = new Map() + + /** @type {Set<import('postcss').AtRule>} */ + let universals = new Set() + + root.walkAtRules('defaults', (rule) => { + if (rule.nodes && rule.nodes.length > 0) { + universals.add(rule) + return + } + + let variable = rule.params + if (!variableNodeMap.has(variable)) { + variableNodeMap.set(variable, new Set()) + } + + variableNodeMap.get(variable).add(rule.parent) + + rule.remove() + }) + + if (flagEnabled(tailwindConfig, 'optimizeUniversalDefaults')) { + for (let universal of universals) { + /** @type {Map<string, Set<string>>} */ + let selectorGroups = new Map() + + let rules = variableNodeMap.get(universal.params) ?? [] + + for (let rule of rules) { + for (let selector of extractElementSelector(rule.selector)) { + // If selector contains a vendor prefix after a pseudo element or class, + // we consider them separately because merging the declarations into + // a single rule will cause browsers that do not understand the + // vendor prefix to throw out the whole rule + let selectorGroupName = + selector.includes(':-') || selector.includes('::-') ? selector : '__DEFAULT__' + + let selectors = selectorGroups.get(selectorGroupName) ?? new Set() + selectorGroups.set(selectorGroupName, selectors) + + selectors.add(selector) + } + } + + if (flagEnabled(tailwindConfig, 'optimizeUniversalDefaults')) { + if (selectorGroups.size === 0) { + universal.remove() + continue + } + + for (let [, selectors] of selectorGroups) { + let universalRule = postcss.rule({ + source: universal.source, + }) + + universalRule.selectors = [...selectors] + + universalRule.append(universal.nodes.map((node) => node.clone())) + universal.before(universalRule) + } + } + + universal.remove() + } + } else if (universals.size) { + let universalRule = postcss.rule({ + selectors: ['*', '::before', '::after'], + }) + + for (let universal of universals) { + universalRule.append(universal.nodes) + + if (!universalRule.parent) { + universal.before(universalRule) + } + + if (!universalRule.source) { + universalRule.source = universal.source + } + + universal.remove() + } + + let backdropRule = universalRule.clone({ + selectors: ['::backdrop'], + }) + + universalRule.after(backdropRule) + } + } +} diff --git a/node_modules/tailwindcss/src/lib/setupContextUtils.js b/node_modules/tailwindcss/src/lib/setupContextUtils.js new file mode 100644 index 0000000..59c261d --- /dev/null +++ b/node_modules/tailwindcss/src/lib/setupContextUtils.js @@ -0,0 +1,1342 @@ +import fs from 'fs' +import url from 'url' +import postcss from 'postcss' +import dlv from 'dlv' +import selectorParser from 'postcss-selector-parser' + +import transformThemeValue from '../util/transformThemeValue' +import parseObjectStyles from '../util/parseObjectStyles' +import prefixSelector from '../util/prefixSelector' +import isPlainObject from '../util/isPlainObject' +import escapeClassName from '../util/escapeClassName' +import nameClass, { formatClass } from '../util/nameClass' +import { coerceValue } from '../util/pluginUtils' +import { variantPlugins, corePlugins } from '../corePlugins' +import * as sharedState from './sharedState' +import { env } from './sharedState' +import { toPath } from '../util/toPath' +import log from '../util/log' +import negateValue from '../util/negateValue' +import isSyntacticallyValidPropertyValue from '../util/isSyntacticallyValidPropertyValue' +import { generateRules, getClassNameFromSelector } from './generateRules' +import { hasContentChanged } from './cacheInvalidation.js' +import { Offsets } from './offsets.js' +import { flagEnabled } from '../featureFlags.js' +import { finalizeSelector, formatVariantSelector } from '../util/formatVariantSelector' + +export const INTERNAL_FEATURES = Symbol() + +const VARIANT_TYPES = { + AddVariant: Symbol.for('ADD_VARIANT'), + MatchVariant: Symbol.for('MATCH_VARIANT'), +} + +const VARIANT_INFO = { + Base: 1 << 0, + Dynamic: 1 << 1, +} + +function prefix(context, selector) { + let prefix = context.tailwindConfig.prefix + return typeof prefix === 'function' ? prefix(selector) : prefix + selector +} + +function normalizeOptionTypes({ type = 'any', ...options }) { + let types = [].concat(type) + + return { + ...options, + types: types.map((type) => { + if (Array.isArray(type)) { + return { type: type[0], ...type[1] } + } + return { type, preferOnConflict: false } + }), + } +} + +function parseVariantFormatString(input) { + /** @type {string[]} */ + let parts = [] + + // When parsing whitespace around special characters are insignificant + // However, _inside_ of a variant they could be + // Because the selector could look like this + // @media { &[data-name="foo bar"] } + // This is why we do not skip whitespace + + let current = '' + let depth = 0 + + for (let idx = 0; idx < input.length; idx++) { + let char = input[idx] + + if (char === '\\') { + // Escaped characters are not special + current += '\\' + input[++idx] + } else if (char === '{') { + // Nested rule: start + ++depth + parts.push(current.trim()) + current = '' + } else if (char === '}') { + // Nested rule: end + if (--depth < 0) { + throw new Error(`Your { and } are unbalanced.`) + } + + parts.push(current.trim()) + current = '' + } else { + // Normal character + current += char + } + } + + if (current.length > 0) { + parts.push(current.trim()) + } + + parts = parts.filter((part) => part !== '') + + return parts +} + +function insertInto(list, value, { before = [] } = {}) { + before = [].concat(before) + + if (before.length <= 0) { + list.push(value) + return + } + + let idx = list.length - 1 + for (let other of before) { + let iidx = list.indexOf(other) + if (iidx === -1) continue + idx = Math.min(idx, iidx) + } + + list.splice(idx, 0, value) +} + +function parseStyles(styles) { + if (!Array.isArray(styles)) { + return parseStyles([styles]) + } + + return styles.flatMap((style) => { + let isNode = !Array.isArray(style) && !isPlainObject(style) + return isNode ? style : parseObjectStyles(style) + }) +} + +function getClasses(selector, mutate) { + let parser = selectorParser((selectors) => { + let allClasses = [] + + if (mutate) { + mutate(selectors) + } + + selectors.walkClasses((classNode) => { + allClasses.push(classNode.value) + }) + + return allClasses + }) + return parser.transformSync(selector) +} + +/** + * Ignore everything inside a :not(...). This allows you to write code like + * `div:not(.foo)`. If `.foo` is never found in your code, then we used to + * not generated it. But now we will ignore everything inside a `:not`, so + * that it still gets generated. + * + * @param {selectorParser.Root} selectors + */ +function ignoreNot(selectors) { + selectors.walkPseudos((pseudo) => { + if (pseudo.value === ':not') { + pseudo.remove() + } + }) +} + +function extractCandidates(node, state = { containsNonOnDemandable: false }, depth = 0) { + let classes = [] + let selectors = [] + + if (node.type === 'rule') { + // Handle normal rules + selectors.push(...node.selectors) + } else if (node.type === 'atrule') { + // Handle at-rules (which contains nested rules) + node.walkRules((rule) => selectors.push(...rule.selectors)) + } + + for (let selector of selectors) { + let classCandidates = getClasses(selector, ignoreNot) + + // At least one of the selectors contains non-"on-demandable" candidates. + if (classCandidates.length === 0) { + state.containsNonOnDemandable = true + } + + for (let classCandidate of classCandidates) { + classes.push(classCandidate) + } + } + + if (depth === 0) { + return [state.containsNonOnDemandable || classes.length === 0, classes] + } + + return classes +} + +function withIdentifiers(styles) { + return parseStyles(styles).flatMap((node) => { + let nodeMap = new Map() + let [containsNonOnDemandableSelectors, candidates] = extractCandidates(node) + + // If this isn't "on-demandable", assign it a universal candidate to always include it. + if (containsNonOnDemandableSelectors) { + candidates.unshift(sharedState.NOT_ON_DEMAND) + } + + // However, it could be that it also contains "on-demandable" candidates. + // E.g.: `span, .foo {}`, in that case it should still be possible to use + // `@apply foo` for example. + return candidates.map((c) => { + if (!nodeMap.has(node)) { + nodeMap.set(node, node) + } + return [c, nodeMap.get(node)] + }) + }) +} + +export function isValidVariantFormatString(format) { + return format.startsWith('@') || format.includes('&') +} + +export function parseVariant(variant) { + variant = variant + .replace(/\n+/g, '') + .replace(/\s{1,}/g, ' ') + .trim() + + let fns = parseVariantFormatString(variant) + .map((str) => { + if (!str.startsWith('@')) { + return ({ format }) => format(str) + } + + let [, name, params] = /@(\S*)( .+|[({].*)?/g.exec(str) + return ({ wrap }) => wrap(postcss.atRule({ name, params: params?.trim() ?? '' })) + }) + .reverse() + + return (api) => { + for (let fn of fns) { + fn(api) + } + } +} + +/** + * + * @param {any} tailwindConfig + * @param {any} context + * @param {object} param2 + * @param {Offsets} param2.offsets + */ +function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offsets, classList }) { + function getConfigValue(path, defaultValue) { + return path ? dlv(tailwindConfig, path, defaultValue) : tailwindConfig + } + + function applyConfiguredPrefix(selector) { + return prefixSelector(tailwindConfig.prefix, selector) + } + + function prefixIdentifier(identifier, options) { + if (identifier === sharedState.NOT_ON_DEMAND) { + return sharedState.NOT_ON_DEMAND + } + + if (!options.respectPrefix) { + return identifier + } + + return context.tailwindConfig.prefix + identifier + } + + function resolveThemeValue(path, defaultValue, opts = {}) { + let parts = toPath(path) + let value = getConfigValue(['theme', ...parts], defaultValue) + return transformThemeValue(parts[0])(value, opts) + } + + let variantIdentifier = 0 + let api = { + postcss, + prefix: applyConfiguredPrefix, + e: escapeClassName, + config: getConfigValue, + theme: resolveThemeValue, + corePlugins: (path) => { + if (Array.isArray(tailwindConfig.corePlugins)) { + return tailwindConfig.corePlugins.includes(path) + } + + return getConfigValue(['corePlugins', path], true) + }, + variants: () => { + // Preserved for backwards compatibility but not used in v3.0+ + return [] + }, + addBase(base) { + for (let [identifier, rule] of withIdentifiers(base)) { + let prefixedIdentifier = prefixIdentifier(identifier, {}) + let offset = offsets.create('base') + + if (!context.candidateRuleMap.has(prefixedIdentifier)) { + context.candidateRuleMap.set(prefixedIdentifier, []) + } + + context.candidateRuleMap + .get(prefixedIdentifier) + .push([{ sort: offset, layer: 'base' }, rule]) + } + }, + /** + * @param {string} group + * @param {Record<string, string | string[]>} declarations + */ + addDefaults(group, declarations) { + const groups = { + [`@defaults ${group}`]: declarations, + } + + for (let [identifier, rule] of withIdentifiers(groups)) { + let prefixedIdentifier = prefixIdentifier(identifier, {}) + + if (!context.candidateRuleMap.has(prefixedIdentifier)) { + context.candidateRuleMap.set(prefixedIdentifier, []) + } + + context.candidateRuleMap + .get(prefixedIdentifier) + .push([{ sort: offsets.create('defaults'), layer: 'defaults' }, rule]) + } + }, + addComponents(components, options) { + let defaultOptions = { + preserveSource: false, + respectPrefix: true, + respectImportant: false, + } + + options = Object.assign({}, defaultOptions, Array.isArray(options) ? {} : options) + + for (let [identifier, rule] of withIdentifiers(components)) { + let prefixedIdentifier = prefixIdentifier(identifier, options) + + classList.add(prefixedIdentifier) + + if (!context.candidateRuleMap.has(prefixedIdentifier)) { + context.candidateRuleMap.set(prefixedIdentifier, []) + } + + context.candidateRuleMap + .get(prefixedIdentifier) + .push([{ sort: offsets.create('components'), layer: 'components', options }, rule]) + } + }, + addUtilities(utilities, options) { + let defaultOptions = { + preserveSource: false, + respectPrefix: true, + respectImportant: true, + } + + options = Object.assign({}, defaultOptions, Array.isArray(options) ? {} : options) + + for (let [identifier, rule] of withIdentifiers(utilities)) { + let prefixedIdentifier = prefixIdentifier(identifier, options) + + classList.add(prefixedIdentifier) + + if (!context.candidateRuleMap.has(prefixedIdentifier)) { + context.candidateRuleMap.set(prefixedIdentifier, []) + } + + context.candidateRuleMap + .get(prefixedIdentifier) + .push([{ sort: offsets.create('utilities'), layer: 'utilities', options }, rule]) + } + }, + matchUtilities: function (utilities, options) { + let defaultOptions = { + respectPrefix: true, + respectImportant: true, + modifiers: false, + } + + options = normalizeOptionTypes({ ...defaultOptions, ...options }) + + let offset = offsets.create('utilities') + + for (let identifier in utilities) { + let prefixedIdentifier = prefixIdentifier(identifier, options) + let rule = utilities[identifier] + + classList.add([prefixedIdentifier, options]) + + function wrapped(modifier, { isOnlyPlugin }) { + let [value, coercedType, utilityModifier] = coerceValue( + options.types, + modifier, + options, + tailwindConfig + ) + + if (value === undefined) { + return [] + } + + if (!options.types.some(({ type }) => type === coercedType)) { + if (isOnlyPlugin) { + log.warn([ + `Unnecessary typehint \`${coercedType}\` in \`${identifier}-${modifier}\`.`, + `You can safely update it to \`${identifier}-${modifier.replace( + coercedType + ':', + '' + )}\`.`, + ]) + } else { + return [] + } + } + + if (!isSyntacticallyValidPropertyValue(value)) { + return [] + } + + let extras = { + get modifier() { + if (!options.modifiers) { + log.warn(`modifier-used-without-options-for-${identifier}`, [ + 'Your plugin must set `modifiers: true` in its options to support modifiers.', + ]) + } + + return utilityModifier + }, + } + + let modifiersEnabled = flagEnabled(tailwindConfig, 'generalizedModifiers') + + let ruleSets = [] + .concat(modifiersEnabled ? rule(value, extras) : rule(value)) + .filter(Boolean) + .map((declaration) => ({ + [nameClass(identifier, modifier)]: declaration, + })) + + return ruleSets + } + + let withOffsets = [{ sort: offset, layer: 'utilities', options }, wrapped] + + if (!context.candidateRuleMap.has(prefixedIdentifier)) { + context.candidateRuleMap.set(prefixedIdentifier, []) + } + + context.candidateRuleMap.get(prefixedIdentifier).push(withOffsets) + } + }, + matchComponents: function (components, options) { + let defaultOptions = { + respectPrefix: true, + respectImportant: false, + modifiers: false, + } + + options = normalizeOptionTypes({ ...defaultOptions, ...options }) + + let offset = offsets.create('components') + + for (let identifier in components) { + let prefixedIdentifier = prefixIdentifier(identifier, options) + let rule = components[identifier] + + classList.add([prefixedIdentifier, options]) + + function wrapped(modifier, { isOnlyPlugin }) { + let [value, coercedType, utilityModifier] = coerceValue( + options.types, + modifier, + options, + tailwindConfig + ) + + if (value === undefined) { + return [] + } + + if (!options.types.some(({ type }) => type === coercedType)) { + if (isOnlyPlugin) { + log.warn([ + `Unnecessary typehint \`${coercedType}\` in \`${identifier}-${modifier}\`.`, + `You can safely update it to \`${identifier}-${modifier.replace( + coercedType + ':', + '' + )}\`.`, + ]) + } else { + return [] + } + } + + if (!isSyntacticallyValidPropertyValue(value)) { + return [] + } + + let extras = { + get modifier() { + if (!options.modifiers) { + log.warn(`modifier-used-without-options-for-${identifier}`, [ + 'Your plugin must set `modifiers: true` in its options to support modifiers.', + ]) + } + + return utilityModifier + }, + } + + let modifiersEnabled = flagEnabled(tailwindConfig, 'generalizedModifiers') + + let ruleSets = [] + .concat(modifiersEnabled ? rule(value, extras) : rule(value)) + .filter(Boolean) + .map((declaration) => ({ + [nameClass(identifier, modifier)]: declaration, + })) + + return ruleSets + } + + let withOffsets = [{ sort: offset, layer: 'components', options }, wrapped] + + if (!context.candidateRuleMap.has(prefixedIdentifier)) { + context.candidateRuleMap.set(prefixedIdentifier, []) + } + + context.candidateRuleMap.get(prefixedIdentifier).push(withOffsets) + } + }, + addVariant(variantName, variantFunctions, options = {}) { + variantFunctions = [].concat(variantFunctions).map((variantFunction) => { + if (typeof variantFunction !== 'string') { + // Safelist public API functions + return (api = {}) => { + let { args, modifySelectors, container, separator, wrap, format } = api + let result = variantFunction( + Object.assign( + { modifySelectors, container, separator }, + options.type === VARIANT_TYPES.MatchVariant && { args, wrap, format } + ) + ) + + if (typeof result === 'string' && !isValidVariantFormatString(result)) { + throw new Error( + `Your custom variant \`${variantName}\` has an invalid format string. Make sure it's an at-rule or contains a \`&\` placeholder.` + ) + } + + if (Array.isArray(result)) { + return result + .filter((variant) => typeof variant === 'string') + .map((variant) => parseVariant(variant)) + } + + // result may be undefined with legacy variants that use APIs like `modifySelectors` + // result may also be a postcss node if someone was returning the result from `modifySelectors` + return result && typeof result === 'string' && parseVariant(result)(api) + } + } + + if (!isValidVariantFormatString(variantFunction)) { + throw new Error( + `Your custom variant \`${variantName}\` has an invalid format string. Make sure it's an at-rule or contains a \`&\` placeholder.` + ) + } + + return parseVariant(variantFunction) + }) + + insertInto(variantList, variantName, options) + variantMap.set(variantName, variantFunctions) + context.variantOptions.set(variantName, options) + }, + matchVariant(variant, variantFn, options) { + // A unique identifier that "groups" these variants together. + // This is for internal use only which is why it is not present in the types + let id = options?.id ?? ++variantIdentifier + let isSpecial = variant === '@' + + let modifiersEnabled = flagEnabled(tailwindConfig, 'generalizedModifiers') + + for (let [key, value] of Object.entries(options?.values ?? {})) { + if (key === 'DEFAULT') continue + + api.addVariant( + isSpecial ? `${variant}${key}` : `${variant}-${key}`, + ({ args, container }) => { + return variantFn( + value, + modifiersEnabled ? { modifier: args?.modifier, container } : { container } + ) + }, + + { + ...options, + value, + id, + type: VARIANT_TYPES.MatchVariant, + variantInfo: VARIANT_INFO.Base, + } + ) + } + + let hasDefault = 'DEFAULT' in (options?.values ?? {}) + + api.addVariant( + variant, + ({ args, container }) => { + if (args?.value === sharedState.NONE && !hasDefault) { + return null + } + + return variantFn( + args?.value === sharedState.NONE + ? options.values.DEFAULT + : // Falling back to args if it is a string, otherwise '' for older intellisense + // (JetBrains) plugins. + args?.value ?? (typeof args === 'string' ? args : ''), + modifiersEnabled ? { modifier: args?.modifier, container } : { container } + ) + }, + { + ...options, + id, + type: VARIANT_TYPES.MatchVariant, + variantInfo: VARIANT_INFO.Dynamic, + } + ) + }, + } + + return api +} + +let fileModifiedMapCache = new WeakMap() +export function getFileModifiedMap(context) { + if (!fileModifiedMapCache.has(context)) { + fileModifiedMapCache.set(context, new Map()) + } + return fileModifiedMapCache.get(context) +} + +function trackModified(files, fileModifiedMap) { + let changed = false + let mtimesToCommit = new Map() + + for (let file of files) { + if (!file) continue + + let parsed = url.parse(file) + let pathname = parsed.hash ? parsed.href.replace(parsed.hash, '') : parsed.href + pathname = parsed.search ? pathname.replace(parsed.search, '') : pathname + let newModified = fs.statSync(decodeURIComponent(pathname), { throwIfNoEntry: false })?.mtimeMs + if (!newModified) { + // It could happen that a file is passed in that doesn't exist. E.g.: + // postcss-cli will provide you a fake path when reading from stdin. This + // path then looks like /path-to-your-project/stdin In that case we just + // want to ignore it and don't track changes at all. + continue + } + + if (!fileModifiedMap.has(file) || newModified > fileModifiedMap.get(file)) { + changed = true + } + + mtimesToCommit.set(file, newModified) + } + + return [changed, mtimesToCommit] +} + +function extractVariantAtRules(node) { + node.walkAtRules((atRule) => { + if (['responsive', 'variants'].includes(atRule.name)) { + extractVariantAtRules(atRule) + atRule.before(atRule.nodes) + atRule.remove() + } + }) +} + +function collectLayerPlugins(root) { + let layerPlugins = [] + + root.each((node) => { + if (node.type === 'atrule' && ['responsive', 'variants'].includes(node.name)) { + node.name = 'layer' + node.params = 'utilities' + } + }) + + // Walk @layer rules and treat them like plugins + root.walkAtRules('layer', (layerRule) => { + extractVariantAtRules(layerRule) + + if (layerRule.params === 'base') { + for (let node of layerRule.nodes) { + layerPlugins.push(function ({ addBase }) { + addBase(node, { respectPrefix: false }) + }) + } + layerRule.remove() + } else if (layerRule.params === 'components') { + for (let node of layerRule.nodes) { + layerPlugins.push(function ({ addComponents }) { + addComponents(node, { respectPrefix: false, preserveSource: true }) + }) + } + layerRule.remove() + } else if (layerRule.params === 'utilities') { + for (let node of layerRule.nodes) { + layerPlugins.push(function ({ addUtilities }) { + addUtilities(node, { respectPrefix: false, preserveSource: true }) + }) + } + layerRule.remove() + } + }) + + return layerPlugins +} + +function resolvePlugins(context, root) { + let corePluginList = Object.entries({ ...variantPlugins, ...corePlugins }) + .map(([name, plugin]) => { + if (!context.tailwindConfig.corePlugins.includes(name)) { + return null + } + + return plugin + }) + .filter(Boolean) + + let userPlugins = context.tailwindConfig.plugins.map((plugin) => { + if (plugin.__isOptionsFunction) { + plugin = plugin() + } + + return typeof plugin === 'function' ? plugin : plugin.handler + }) + + let layerPlugins = collectLayerPlugins(root) + + // TODO: This is a workaround for backwards compatibility, since custom variants + // were historically sorted before screen/stackable variants. + let beforeVariants = [ + variantPlugins['pseudoElementVariants'], + variantPlugins['pseudoClassVariants'], + variantPlugins['ariaVariants'], + variantPlugins['dataVariants'], + ] + let afterVariants = [ + variantPlugins['supportsVariants'], + variantPlugins['directionVariants'], + variantPlugins['reducedMotionVariants'], + variantPlugins['prefersContrastVariants'], + variantPlugins['darkVariants'], + variantPlugins['printVariant'], + variantPlugins['screenVariants'], + variantPlugins['orientationVariants'], + ] + + return [...corePluginList, ...beforeVariants, ...userPlugins, ...afterVariants, ...layerPlugins] +} + +function registerPlugins(plugins, context) { + let variantList = [] + let variantMap = new Map() + context.variantMap = variantMap + + let offsets = new Offsets() + context.offsets = offsets + + let classList = new Set() + + let pluginApi = buildPluginApi(context.tailwindConfig, context, { + variantList, + variantMap, + offsets, + classList, + }) + + for (let plugin of plugins) { + if (Array.isArray(plugin)) { + for (let pluginItem of plugin) { + pluginItem(pluginApi) + } + } else { + plugin?.(pluginApi) + } + } + + // Make sure to record bit masks for every variant + offsets.recordVariants(variantList, (variant) => variantMap.get(variant).length) + + // Build variantMap + for (let [variantName, variantFunctions] of variantMap.entries()) { + context.variantMap.set( + variantName, + variantFunctions.map((variantFunction, idx) => [ + offsets.forVariant(variantName, idx), + variantFunction, + ]) + ) + } + + let safelist = (context.tailwindConfig.safelist ?? []).filter(Boolean) + if (safelist.length > 0) { + let checks = [] + + for (let value of safelist) { + if (typeof value === 'string') { + context.changedContent.push({ content: value, extension: 'html' }) + continue + } + + if (value instanceof RegExp) { + log.warn('root-regex', [ + 'Regular expressions in `safelist` work differently in Tailwind CSS v3.0.', + 'Update your `safelist` configuration to eliminate this warning.', + 'https://tailwindcss.com/docs/content-configuration#safelisting-classes', + ]) + continue + } + + checks.push(value) + } + + if (checks.length > 0) { + let patternMatchingCount = new Map() + let prefixLength = context.tailwindConfig.prefix.length + let checkImportantUtils = checks.some((check) => check.pattern.source.includes('!')) + + for (let util of classList) { + let utils = Array.isArray(util) + ? (() => { + let [utilName, options] = util + let values = Object.keys(options?.values ?? {}) + let classes = values.map((value) => formatClass(utilName, value)) + + if (options?.supportsNegativeValues) { + // This is the normal negated version + // e.g. `-inset-1` or `-tw-inset-1` + classes = [...classes, ...classes.map((cls) => '-' + cls)] + + // This is the negated version *after* the prefix + // e.g. `tw--inset-1` + // The prefix is already attached to util name + // So we add the negative after the prefix + classes = [ + ...classes, + ...classes.map( + (cls) => cls.slice(0, prefixLength) + '-' + cls.slice(prefixLength) + ), + ] + } + + if (options.types.some(({ type }) => type === 'color')) { + classes = [ + ...classes, + ...classes.flatMap((cls) => + Object.keys(context.tailwindConfig.theme.opacity).map( + (opacity) => `${cls}/${opacity}` + ) + ), + ] + } + + if (checkImportantUtils && options?.respectImportant) { + classes = [...classes, ...classes.map((cls) => '!' + cls)] + } + + return classes + })() + : [util] + + for (let util of utils) { + for (let { pattern, variants = [] } of checks) { + // RegExp with the /g flag are stateful, so let's reset the last + // index pointer to reset the state. + pattern.lastIndex = 0 + + if (!patternMatchingCount.has(pattern)) { + patternMatchingCount.set(pattern, 0) + } + + if (!pattern.test(util)) continue + + patternMatchingCount.set(pattern, patternMatchingCount.get(pattern) + 1) + + context.changedContent.push({ content: util, extension: 'html' }) + for (let variant of variants) { + context.changedContent.push({ + content: variant + context.tailwindConfig.separator + util, + extension: 'html', + }) + } + } + } + } + + for (let [regex, count] of patternMatchingCount.entries()) { + if (count !== 0) continue + + log.warn([ + `The safelist pattern \`${regex}\` doesn't match any Tailwind CSS classes.`, + 'Fix this pattern or remove it from your `safelist` configuration.', + 'https://tailwindcss.com/docs/content-configuration#safelisting-classes', + ]) + } + } + } + + let darkClassName = [].concat(context.tailwindConfig.darkMode ?? 'media')[1] ?? 'dark' + + // A list of utilities that are used by certain Tailwind CSS utilities but + // that don't exist on their own. This will result in them "not existing" and + // sorting could be weird since you still require them in order to make the + // host utilities work properly. (Thanks Biology) + let parasiteUtilities = [ + prefix(context, darkClassName), + prefix(context, 'group'), + prefix(context, 'peer'), + ] + context.getClassOrder = function getClassOrder(classes) { + // Sort classes so they're ordered in a deterministic manner + let sorted = [...classes].sort((a, z) => { + if (a === z) return 0 + if (a < z) return -1 + return 1 + }) + + // Non-util classes won't be generated, so we default them to null + let sortedClassNames = new Map(sorted.map((className) => [className, null])) + + // Sort all classes in order + // Non-tailwind classes won't be generated and will be left as `null` + let rules = generateRules(new Set(sorted), context, true) + rules = context.offsets.sort(rules) + + let idx = BigInt(parasiteUtilities.length) + + for (const [, rule] of rules) { + let candidate = rule.raws.tailwind.candidate + + // When multiple rules match a candidate + // always take the position of the first one + sortedClassNames.set(candidate, sortedClassNames.get(candidate) ?? idx++) + } + + return classes.map((className) => { + let order = sortedClassNames.get(className) ?? null + let parasiteIndex = parasiteUtilities.indexOf(className) + + if (order === null && parasiteIndex !== -1) { + // This will make sure that it is at the very beginning of the + // `components` layer which technically means 'before any + // components'. + order = BigInt(parasiteIndex) + } + + return [className, order] + }) + } + + // Generate a list of strings for autocompletion purposes, e.g. + // ['uppercase', 'lowercase', ...] + context.getClassList = function getClassList(options = {}) { + let output = [] + + for (let util of classList) { + if (Array.isArray(util)) { + let [utilName, utilOptions] = util + let negativeClasses = [] + + let modifiers = Object.keys(utilOptions?.modifiers ?? {}) + + if (utilOptions?.types?.some(({ type }) => type === 'color')) { + modifiers.push(...Object.keys(context.tailwindConfig.theme.opacity ?? {})) + } + + let metadata = { modifiers } + let includeMetadata = options.includeMetadata && modifiers.length > 0 + + for (let [key, value] of Object.entries(utilOptions?.values ?? {})) { + // Ignore undefined and null values + if (value == null) { + continue + } + + let cls = formatClass(utilName, key) + output.push(includeMetadata ? [cls, metadata] : cls) + + if (utilOptions?.supportsNegativeValues && negateValue(value)) { + let cls = formatClass(utilName, `-${key}`) + negativeClasses.push(includeMetadata ? [cls, metadata] : cls) + } + } + + output.push(...negativeClasses) + } else { + output.push(util) + } + } + + return output + } + + // Generate a list of available variants with meta information of the type of variant. + context.getVariants = function getVariants() { + let result = [] + for (let [name, options] of context.variantOptions.entries()) { + if (options.variantInfo === VARIANT_INFO.Base) continue + + result.push({ + name, + isArbitrary: options.type === Symbol.for('MATCH_VARIANT'), + values: Object.keys(options.values ?? {}), + hasDash: name !== '@', + selectors({ modifier, value } = {}) { + let candidate = '__TAILWIND_PLACEHOLDER__' + + let rule = postcss.rule({ selector: `.${candidate}` }) + let container = postcss.root({ nodes: [rule.clone()] }) + + let before = container.toString() + + let fns = (context.variantMap.get(name) ?? []).flatMap(([_, fn]) => fn) + let formatStrings = [] + for (let fn of fns) { + let localFormatStrings = [] + + let api = { + args: { modifier, value: options.values?.[value] ?? value }, + separator: context.tailwindConfig.separator, + modifySelectors(modifierFunction) { + // Run the modifierFunction over each rule + container.each((rule) => { + if (rule.type !== 'rule') { + return + } + + rule.selectors = rule.selectors.map((selector) => { + return modifierFunction({ + get className() { + return getClassNameFromSelector(selector) + }, + selector, + }) + }) + }) + + return container + }, + format(str) { + localFormatStrings.push(str) + }, + wrap(wrapper) { + localFormatStrings.push(`@${wrapper.name} ${wrapper.params} { & }`) + }, + container, + } + + let ruleWithVariant = fn(api) + if (localFormatStrings.length > 0) { + formatStrings.push(localFormatStrings) + } + + if (Array.isArray(ruleWithVariant)) { + for (let variantFunction of ruleWithVariant) { + localFormatStrings = [] + variantFunction(api) + formatStrings.push(localFormatStrings) + } + } + } + + // Reverse engineer the result of the `container` + let manualFormatStrings = [] + let after = container.toString() + + if (before !== after) { + // Figure out all selectors + container.walkRules((rule) => { + let modified = rule.selector + + // Rebuild the base selector, this is what plugin authors would do + // as well. E.g.: `${variant}${separator}${className}`. + // However, plugin authors probably also prepend or append certain + // classes, pseudos, ids, ... + let rebuiltBase = selectorParser((selectors) => { + selectors.walkClasses((classNode) => { + classNode.value = `${name}${context.tailwindConfig.separator}${classNode.value}` + }) + }).processSync(modified) + + // Now that we know the original selector, the new selector, and + // the rebuild part in between, we can replace the part that plugin + // authors need to rebuild with `&`, and eventually store it in the + // collectedFormats. Similar to what `format('...')` would do. + // + // E.g.: + // variant: foo + // selector: .markdown > p + // modified (by plugin): .foo .foo\\:markdown > p + // rebuiltBase (internal): .foo\\:markdown > p + // format: .foo & + manualFormatStrings.push(modified.replace(rebuiltBase, '&').replace(candidate, '&')) + }) + + // Figure out all atrules + container.walkAtRules((atrule) => { + manualFormatStrings.push(`@${atrule.name} (${atrule.params}) { & }`) + }) + } + + let isArbitraryVariant = !(value in (options.values ?? {})) + let internalFeatures = options[INTERNAL_FEATURES] ?? {} + + let respectPrefix = (() => { + if (isArbitraryVariant) return false + if (internalFeatures.respectPrefix === false) return false + return true + })() + + formatStrings = formatStrings.map((format) => + format.map((str) => ({ + format: str, + respectPrefix, + })) + ) + + manualFormatStrings = manualFormatStrings.map((format) => ({ + format, + respectPrefix, + })) + + let opts = { + candidate, + context, + } + + let result = formatStrings.map((formats) => + finalizeSelector(`.${candidate}`, formatVariantSelector(formats, opts), opts) + .replace(`.${candidate}`, '&') + .replace('{ & }', '') + .trim() + ) + + if (manualFormatStrings.length > 0) { + result.push( + formatVariantSelector(manualFormatStrings, opts) + .toString() + .replace(`.${candidate}`, '&') + ) + } + + return result + }, + }) + } + + return result + } +} + +/** + * Mark as class as retroactively invalid + * + * + * @param {string} candidate + */ +function markInvalidUtilityCandidate(context, candidate) { + if (!context.classCache.has(candidate)) { + return + } + + // Mark this as not being a real utility + context.notClassCache.add(candidate) + + // Remove it from any candidate-specific caches + context.classCache.delete(candidate) + context.applyClassCache.delete(candidate) + context.candidateRuleMap.delete(candidate) + context.candidateRuleCache.delete(candidate) + + // Ensure the stylesheet gets rebuilt + context.stylesheetCache = null +} + +/** + * Mark as class as retroactively invalid + * + * @param {import('postcss').Node} node + */ +function markInvalidUtilityNode(context, node) { + let candidate = node.raws.tailwind.candidate + + if (!candidate) { + return + } + + for (const entry of context.ruleCache) { + if (entry[1].raws.tailwind.candidate === candidate) { + context.ruleCache.delete(entry) + // context.postCssNodeCache.delete(node) + } + } + + markInvalidUtilityCandidate(context, candidate) +} + +export function createContext(tailwindConfig, changedContent = [], root = postcss.root()) { + let context = { + disposables: [], + ruleCache: new Set(), + candidateRuleCache: new Map(), + classCache: new Map(), + applyClassCache: new Map(), + // Seed the not class cache with the blocklist (which is only strings) + notClassCache: new Set(tailwindConfig.blocklist ?? []), + postCssNodeCache: new Map(), + candidateRuleMap: new Map(), + tailwindConfig, + changedContent: changedContent, + variantMap: new Map(), + stylesheetCache: null, + variantOptions: new Map(), + + markInvalidUtilityCandidate: (candidate) => markInvalidUtilityCandidate(context, candidate), + markInvalidUtilityNode: (node) => markInvalidUtilityNode(context, node), + } + + let resolvedPlugins = resolvePlugins(context, root) + registerPlugins(resolvedPlugins, context) + + return context +} + +let contextMap = sharedState.contextMap +let configContextMap = sharedState.configContextMap +let contextSourcesMap = sharedState.contextSourcesMap + +export function getContext( + root, + result, + tailwindConfig, + userConfigPath, + tailwindConfigHash, + contextDependencies +) { + let sourcePath = result.opts.from + let isConfigFile = userConfigPath !== null + + env.DEBUG && console.log('Source path:', sourcePath) + + let existingContext + + if (isConfigFile && contextMap.has(sourcePath)) { + existingContext = contextMap.get(sourcePath) + } else if (configContextMap.has(tailwindConfigHash)) { + let context = configContextMap.get(tailwindConfigHash) + contextSourcesMap.get(context).add(sourcePath) + contextMap.set(sourcePath, context) + + existingContext = context + } + + let cssDidChange = hasContentChanged(sourcePath, root) + + // If there's already a context in the cache and we don't need to + // reset the context, return the cached context. + if (existingContext) { + let [contextDependenciesChanged, mtimesToCommit] = trackModified( + [...contextDependencies], + getFileModifiedMap(existingContext) + ) + if (!contextDependenciesChanged && !cssDidChange) { + return [existingContext, false, mtimesToCommit] + } + } + + // If this source is in the context map, get the old context. + // Remove this source from the context sources for the old context, + // and clean up that context if no one else is using it. This can be + // called by many processes in rapid succession, so we check for presence + // first because the first process to run this code will wipe it out first. + if (contextMap.has(sourcePath)) { + let oldContext = contextMap.get(sourcePath) + if (contextSourcesMap.has(oldContext)) { + contextSourcesMap.get(oldContext).delete(sourcePath) + if (contextSourcesMap.get(oldContext).size === 0) { + contextSourcesMap.delete(oldContext) + for (let [tailwindConfigHash, context] of configContextMap) { + if (context === oldContext) { + configContextMap.delete(tailwindConfigHash) + } + } + for (let disposable of oldContext.disposables.splice(0)) { + disposable(oldContext) + } + } + } + } + + env.DEBUG && console.log('Setting up new context...') + + let context = createContext(tailwindConfig, [], root) + + Object.assign(context, { + userConfigPath, + }) + + let [, mtimesToCommit] = trackModified([...contextDependencies], getFileModifiedMap(context)) + + // --- + + // Update all context tracking state + + configContextMap.set(tailwindConfigHash, context) + contextMap.set(sourcePath, context) + + if (!contextSourcesMap.has(context)) { + contextSourcesMap.set(context, new Set()) + } + + contextSourcesMap.get(context).add(sourcePath) + + return [context, true, mtimesToCommit] +} diff --git a/node_modules/tailwindcss/src/lib/setupTrackingContext.js b/node_modules/tailwindcss/src/lib/setupTrackingContext.js new file mode 100644 index 0000000..70e7cb6 --- /dev/null +++ b/node_modules/tailwindcss/src/lib/setupTrackingContext.js @@ -0,0 +1,169 @@ +// @ts-check + +import fs from 'fs' +import LRU from '@alloc/quick-lru' + +import hash from '../util/hashConfig' +import resolveConfig from '../public/resolve-config' +import resolveConfigPath from '../util/resolveConfigPath' +import { getContext, getFileModifiedMap } from './setupContextUtils' +import parseDependency from '../util/parseDependency' +import { validateConfig } from '../util/validateConfig.js' +import { parseCandidateFiles, resolvedChangedContent } from './content.js' +import { loadConfig } from '../lib/load-config' +import getModuleDependencies from './getModuleDependencies' + +let configPathCache = new LRU({ maxSize: 100 }) + +let candidateFilesCache = new WeakMap() + +function getCandidateFiles(context, tailwindConfig) { + if (candidateFilesCache.has(context)) { + return candidateFilesCache.get(context) + } + + let candidateFiles = parseCandidateFiles(context, tailwindConfig) + + return candidateFilesCache.set(context, candidateFiles).get(context) +} + +// Get the config object based on a path +function getTailwindConfig(configOrPath) { + let userConfigPath = resolveConfigPath(configOrPath) + + if (userConfigPath !== null) { + let [prevConfig, prevConfigHash, prevDeps, prevModified] = + configPathCache.get(userConfigPath) || [] + + let newDeps = getModuleDependencies(userConfigPath) + + let modified = false + let newModified = new Map() + for (let file of newDeps) { + let time = fs.statSync(file).mtimeMs + newModified.set(file, time) + if (!prevModified || !prevModified.has(file) || time > prevModified.get(file)) { + modified = true + } + } + + // It hasn't changed (based on timestamps) + if (!modified) { + return [prevConfig, userConfigPath, prevConfigHash, prevDeps] + } + + // It has changed (based on timestamps), or first run + for (let file of newDeps) { + delete require.cache[file] + } + let newConfig = validateConfig(resolveConfig(loadConfig(userConfigPath))) + let newHash = hash(newConfig) + configPathCache.set(userConfigPath, [newConfig, newHash, newDeps, newModified]) + return [newConfig, userConfigPath, newHash, newDeps] + } + + // It's a plain object, not a path + let newConfig = resolveConfig(configOrPath?.config ?? configOrPath ?? {}) + + newConfig = validateConfig(newConfig) + + return [newConfig, null, hash(newConfig), []] +} + +// DISABLE_TOUCH = TRUE + +// Retrieve an existing context from cache if possible (since contexts are unique per +// source path), or set up a new one (including setting up watchers and registering +// plugins) then return it +export default function setupTrackingContext(configOrPath) { + return ({ tailwindDirectives, registerDependency }) => { + return (root, result) => { + let [tailwindConfig, userConfigPath, tailwindConfigHash, configDependencies] = + getTailwindConfig(configOrPath) + + let contextDependencies = new Set(configDependencies) + + // If there are no @tailwind or @apply rules, we don't consider this CSS + // file or its dependencies to be dependencies of the context. Can reuse + // the context even if they change. We may want to think about `@layer` + // being part of this trigger too, but it's tough because it's impossible + // for a layer in one file to end up in the actual @tailwind rule in + // another file since independent sources are effectively isolated. + if (tailwindDirectives.size > 0) { + // Add current css file as a context dependencies. + contextDependencies.add(result.opts.from) + + // Add all css @import dependencies as context dependencies. + for (let message of result.messages) { + if (message.type === 'dependency') { + contextDependencies.add(message.file) + } + } + } + + let [context, , mTimesToCommit] = getContext( + root, + result, + tailwindConfig, + userConfigPath, + tailwindConfigHash, + contextDependencies + ) + + let fileModifiedMap = getFileModifiedMap(context) + + let candidateFiles = getCandidateFiles(context, tailwindConfig) + + // If there are no @tailwind or @apply rules, we don't consider this CSS file or it's + // dependencies to be dependencies of the context. Can reuse the context even if they change. + // We may want to think about `@layer` being part of this trigger too, but it's tough + // because it's impossible for a layer in one file to end up in the actual @tailwind rule + // in another file since independent sources are effectively isolated. + if (tailwindDirectives.size > 0) { + // Add template paths as postcss dependencies. + for (let contentPath of candidateFiles) { + for (let dependency of parseDependency(contentPath)) { + registerDependency(dependency) + } + } + + let [changedContent, contentMTimesToCommit] = resolvedChangedContent( + context, + candidateFiles, + fileModifiedMap + ) + + for (let content of changedContent) { + context.changedContent.push(content) + } + + // Add the mtimes of the content files to the commit list + // We can overwrite the existing values because unconditionally + // This is because: + // 1. Most of the files here won't be in the map yet + // 2. If they are that means it's a context dependency + // and we're reading this after the context. This means + // that the mtime we just read is strictly >= the context + // mtime. Unless the user / os is doing something weird + // in which the mtime would be going backwards. If that + // happens there's already going to be problems. + for (let [path, mtime] of contentMTimesToCommit.entries()) { + mTimesToCommit.set(path, mtime) + } + } + + for (let file of configDependencies) { + registerDependency({ type: 'dependency', file }) + } + + // "commit" the new modified time for all context deps + // We do this here because we want content tracking to + // read the "old" mtime even when it's a context dependency. + for (let [path, mtime] of mTimesToCommit.entries()) { + fileModifiedMap.set(path, mtime) + } + + return context + } + } +} diff --git a/node_modules/tailwindcss/src/lib/sharedState.js b/node_modules/tailwindcss/src/lib/sharedState.js new file mode 100644 index 0000000..97bdf09 --- /dev/null +++ b/node_modules/tailwindcss/src/lib/sharedState.js @@ -0,0 +1,61 @@ +import pkg from '../../package.json' + +export const env = + typeof process !== 'undefined' + ? { + NODE_ENV: process.env.NODE_ENV, + DEBUG: resolveDebug(process.env.DEBUG), + ENGINE: pkg.tailwindcss.engine, + } + : { + NODE_ENV: 'production', + DEBUG: false, + ENGINE: pkg.tailwindcss.engine, + } + +export const contextMap = new Map() +export const configContextMap = new Map() +export const contextSourcesMap = new Map() +export const sourceHashMap = new Map() +export const NOT_ON_DEMAND = new String('*') + +export const NONE = Symbol('__NONE__') + +export function resolveDebug(debug) { + if (debug === undefined) { + return false + } + + // Environment variables are strings, so convert to boolean + if (debug === 'true' || debug === '1') { + return true + } + + if (debug === 'false' || debug === '0') { + return false + } + + // Keep the debug convention into account: + // DEBUG=* -> This enables all debug modes + // DEBUG=projectA,projectB,projectC -> This enables debug for projectA, projectB and projectC + // DEBUG=projectA:* -> This enables all debug modes for projectA (if you have sub-types) + // DEBUG=projectA,-projectB -> This enables debug for projectA and explicitly disables it for projectB + + if (debug === '*') { + return true + } + + let debuggers = debug.split(',').map((d) => d.split(':')[0]) + + // Ignoring tailwindcss + if (debuggers.includes('-tailwindcss')) { + return false + } + + // Including tailwindcss + if (debuggers.includes('tailwindcss')) { + return true + } + + return false +} diff --git a/node_modules/tailwindcss/src/lib/substituteScreenAtRules.js b/node_modules/tailwindcss/src/lib/substituteScreenAtRules.js new file mode 100644 index 0000000..5a45cff --- /dev/null +++ b/node_modules/tailwindcss/src/lib/substituteScreenAtRules.js @@ -0,0 +1,19 @@ +import { normalizeScreens } from '../util/normalizeScreens' +import buildMediaQuery from '../util/buildMediaQuery' + +export default function ({ tailwindConfig: { theme } }) { + return function (css) { + css.walkAtRules('screen', (atRule) => { + let screen = atRule.params + let screens = normalizeScreens(theme.screens) + let screenDefinition = screens.find(({ name }) => name === screen) + + if (!screenDefinition) { + throw atRule.error(`No \`${screen}\` screen found.`) + } + + atRule.name = 'media' + atRule.params = buildMediaQuery(screenDefinition) + }) + } +} diff --git a/node_modules/tailwindcss/src/oxide/cli.ts b/node_modules/tailwindcss/src/oxide/cli.ts new file mode 100644 index 0000000..17552ec --- /dev/null +++ b/node_modules/tailwindcss/src/oxide/cli.ts @@ -0,0 +1 @@ +import './cli/index' diff --git a/node_modules/tailwindcss/src/oxide/cli/build/deps.ts b/node_modules/tailwindcss/src/oxide/cli/build/deps.ts new file mode 100644 index 0000000..2b4355b --- /dev/null +++ b/node_modules/tailwindcss/src/oxide/cli/build/deps.ts @@ -0,0 +1,91 @@ +import packageJson from '../../../../package.json' +import browserslist from 'browserslist' +import { Result } from 'postcss' + +import { + // @ts-ignore + lazyPostcss, + + // @ts-ignore + lazyPostcssImport, + + // @ts-ignore + lazyCssnano, + + // @ts-ignore +} from '../../../../peers/index' + +export function lazyLightningCss() { + // TODO: Make this lazy/bundled + return require('lightningcss') +} + +let lightningCss + +function loadLightningCss() { + if (lightningCss) { + return lightningCss + } + + // Try to load a local version first + try { + return (lightningCss = require('lightningcss')) + } catch {} + + return (lightningCss = lazyLightningCss()) +} + +export async function lightningcss(shouldMinify: boolean, result: Result) { + let css = loadLightningCss() + + try { + let transformed = css.transform({ + filename: result.opts.from || 'input.css', + code: Buffer.from(result.css, 'utf-8'), + minify: shouldMinify, + sourceMap: !!result.map, + inputSourceMap: result.map ? result.map.toString() : undefined, + targets: css.browserslistToTargets(browserslist(packageJson.browserslist)), + drafts: { + nesting: true, + }, + }) + + return Object.assign(result, { + css: transformed.code.toString('utf8'), + map: result.map + ? Object.assign(result.map, { + toString() { + return transformed.map.toString() + }, + }) + : result.map, + }) + } catch (err) { + console.error('Unable to use Lightning CSS. Using raw version instead.') + console.error(err) + + return result + } +} + +/** + * @returns {import('postcss')} + */ +export function loadPostcss() { + // Try to load a local `postcss` version first + try { + return require('postcss') + } catch {} + + return lazyPostcss() +} + +export function loadPostcssImport() { + // Try to load a local `postcss-import` version first + try { + return require('postcss-import') + } catch {} + + return lazyPostcssImport() +} diff --git a/node_modules/tailwindcss/src/oxide/cli/build/index.ts b/node_modules/tailwindcss/src/oxide/cli/build/index.ts new file mode 100644 index 0000000..ba7c874 --- /dev/null +++ b/node_modules/tailwindcss/src/oxide/cli/build/index.ts @@ -0,0 +1,47 @@ +import fs from 'fs' +import path from 'path' +import { resolveDefaultConfigPath } from '../../../util/resolveConfigPath' +import { createProcessor } from './plugin' + +export async function build(args) { + let input = args['--input'] + let shouldWatch = args['--watch'] + + // TODO: Deprecate this in future versions + if (!input && args['_'][1]) { + console.error('[deprecation] Running tailwindcss without -i, please provide an input file.') + input = args['--input'] = args['_'][1] + } + + if (input && input !== '-' && !fs.existsSync((input = path.resolve(input)))) { + console.error(`Specified input file ${args['--input']} does not exist.`) + process.exit(9) + } + + if (args['--config'] && !fs.existsSync((args['--config'] = path.resolve(args['--config'])))) { + console.error(`Specified config file ${args['--config']} does not exist.`) + process.exit(9) + } + + // TODO: Reference the @config path here if exists + let configPath = args['--config'] ? args['--config'] : resolveDefaultConfigPath() + + let processor = await createProcessor(args, configPath) + + if (shouldWatch) { + // Abort the watcher if stdin is closed to avoid zombie processes + // You can disable this behavior with --watch=always + if (args['--watch'] !== 'always') { + process.stdin.on('end', () => process.exit(0)) + } + + process.stdin.resume() + + await processor.watch() + } else { + await processor.build().catch((e) => { + console.error(e) + process.exit(1) + }) + } +} diff --git a/node_modules/tailwindcss/src/oxide/cli/build/plugin.ts b/node_modules/tailwindcss/src/oxide/cli/build/plugin.ts new file mode 100644 index 0000000..b878b91 --- /dev/null +++ b/node_modules/tailwindcss/src/oxide/cli/build/plugin.ts @@ -0,0 +1,442 @@ +import path from 'path' +import fs from 'fs' +import postcssrc from 'postcss-load-config' +import { lilconfig } from 'lilconfig' +import loadPlugins from 'postcss-load-config/src/plugins' // Little bit scary, looking at private/internal API +import loadOptions from 'postcss-load-config/src/options' // Little bit scary, looking at private/internal API + +import tailwind from '../../../processTailwindFeatures' +import { loadPostcss, loadPostcssImport, lightningcss } from './deps' +import { formatNodes, drainStdin, outputFile } from './utils' +import { env } from '../../../lib/sharedState' +import resolveConfig from '../../../../resolveConfig' +import { parseCandidateFiles } from '../../../lib/content' +import { createWatcher } from './watching' +import fastGlob from 'fast-glob' +import { findAtConfigPath } from '../../../lib/findAtConfigPath' +import log from '../../../util/log' +import { loadConfig } from '../../../lib/load-config' +import getModuleDependencies from '../../../lib/getModuleDependencies' +import type { Config } from '../../../../types' + +/** + * + * @param {string} [customPostCssPath ] + * @returns + */ +async function loadPostCssPlugins(customPostCssPath) { + let config = customPostCssPath + ? await (async () => { + let file = path.resolve(customPostCssPath) + + // Implementation, see: https://unpkg.com/browse/postcss-load-config@3.1.0/src/index.js + // @ts-ignore + let { config = {} } = await lilconfig('postcss').load(file) + if (typeof config === 'function') { + config = config() + } else { + config = Object.assign({}, config) + } + + if (!config.plugins) { + config.plugins = [] + } + + return { + file, + plugins: loadPlugins(config, file), + options: loadOptions(config, file), + } + })() + : await postcssrc() + + let configPlugins = config.plugins + + let configPluginTailwindIdx = configPlugins.findIndex((plugin) => { + if (typeof plugin === 'function' && plugin.name === 'tailwindcss') { + return true + } + + if (typeof plugin === 'object' && plugin !== null && plugin.postcssPlugin === 'tailwindcss') { + return true + } + + return false + }) + + let beforePlugins = + configPluginTailwindIdx === -1 ? [] : configPlugins.slice(0, configPluginTailwindIdx) + let afterPlugins = + configPluginTailwindIdx === -1 + ? configPlugins + : configPlugins.slice(configPluginTailwindIdx + 1) + + return [beforePlugins, afterPlugins, config.options] +} + +function loadBuiltinPostcssPlugins() { + let postcss = loadPostcss() + let IMPORT_COMMENT = '__TAILWIND_RESTORE_IMPORT__: ' + return [ + [ + (root) => { + root.walkAtRules('import', (rule) => { + if (rule.params.slice(1).startsWith('tailwindcss/')) { + rule.after(postcss.comment({ text: IMPORT_COMMENT + rule.params })) + rule.remove() + } + }) + }, + loadPostcssImport(), + (root) => { + root.walkComments((rule) => { + if (rule.text.startsWith(IMPORT_COMMENT)) { + rule.after( + postcss.atRule({ + name: 'import', + params: rule.text.replace(IMPORT_COMMENT, ''), + }) + ) + rule.remove() + } + }) + }, + ], + [], + {}, + ] +} + +let state = { + /** @type {any} */ + context: null, + + /** @type {ReturnType<typeof createWatcher> | null} */ + watcher: null, + + /** @type {{content: string, extension: string}[]} */ + changedContent: [], + + /** @type {{config: Config, dependencies: Set<string>, dispose: Function } | null} */ + configBag: null, + + contextDependencies: new Set(), + + /** @type {import('../../lib/content.js').ContentPath[]} */ + contentPaths: [], + + refreshContentPaths() { + this.contentPaths = parseCandidateFiles(this.context, this.context?.tailwindConfig) + }, + + get config() { + return this.context.tailwindConfig + }, + + get contentPatterns() { + return { + all: this.contentPaths.map((contentPath) => contentPath.pattern), + dynamic: this.contentPaths + .filter((contentPath) => contentPath.glob !== undefined) + .map((contentPath) => contentPath.pattern), + } + }, + + loadConfig(configPath, content) { + if (this.watcher && configPath) { + this.refreshConfigDependencies() + } + + let config = loadConfig(configPath) + let dependencies = getModuleDependencies(configPath) + this.configBag = { + config, + dependencies, + dispose() { + for (let file of dependencies) { + delete require.cache[require.resolve(file)] + } + }, + } + + // @ts-ignore + this.configBag.config = resolveConfig(this.configBag.config, { content: { files: [] } }) + + // Override content files if `--content` has been passed explicitly + if (content?.length > 0) { + this.configBag.config.content.files = content + } + + return this.configBag.config + }, + + refreshConfigDependencies(configPath) { + env.DEBUG && console.time('Module dependencies') + this.configBag?.dispose() + env.DEBUG && console.timeEnd('Module dependencies') + }, + + readContentPaths() { + let content = [] + + // Resolve globs from the content config + // TODO: When we make the postcss plugin async-capable this can become async + let files = fastGlob.sync(this.contentPatterns.all) + + for (let file of files) { + if (__OXIDE__) { + content.push({ + file, + extension: path.extname(file).slice(1), + }) + } else { + content.push({ + content: fs.readFileSync(path.resolve(file), 'utf8'), + extension: path.extname(file).slice(1), + }) + } + } + + // Resolve raw content in the tailwind config + let rawContent = this.config.content.files.filter((file) => { + return file !== null && typeof file === 'object' + }) + + for (let { raw: htmlContent, extension = 'html' } of rawContent) { + content.push({ content: htmlContent, extension }) + } + + return content + }, + + getContext({ createContext, cliConfigPath, root, result, content }) { + if (this.context) { + this.context.changedContent = this.changedContent.splice(0) + + return this.context + } + + env.DEBUG && console.time('Searching for config') + let configPath = findAtConfigPath(root, result) ?? cliConfigPath + env.DEBUG && console.timeEnd('Searching for config') + + env.DEBUG && console.time('Loading config') + let config = this.loadConfig(configPath, content) + env.DEBUG && console.timeEnd('Loading config') + + env.DEBUG && console.time('Creating context') + this.context = createContext(config, []) + Object.assign(this.context, { + userConfigPath: configPath, + }) + env.DEBUG && console.timeEnd('Creating context') + + env.DEBUG && console.time('Resolving content paths') + this.refreshContentPaths() + env.DEBUG && console.timeEnd('Resolving content paths') + + if (this.watcher) { + env.DEBUG && console.time('Watch new files') + this.watcher.refreshWatchedFiles() + env.DEBUG && console.timeEnd('Watch new files') + } + + for (let file of this.readContentPaths()) { + this.context.changedContent.push(file) + } + + return this.context + }, +} + +export async function createProcessor(args, cliConfigPath) { + let postcss = loadPostcss() + + let input = args['--input'] + let output = args['--output'] + let includePostCss = args['--postcss'] + let customPostCssPath = typeof args['--postcss'] === 'string' ? args['--postcss'] : undefined + + let [beforePlugins, afterPlugins, postcssOptions] = includePostCss + ? await loadPostCssPlugins(customPostCssPath) + : loadBuiltinPostcssPlugins() + + if (args['--purge']) { + log.warn('purge-flag-deprecated', [ + 'The `--purge` flag has been deprecated.', + 'Please use `--content` instead.', + ]) + + if (!args['--content']) { + args['--content'] = args['--purge'] + } + } + + let content = args['--content']?.split(/(?<!{[^}]+),/) ?? [] + + let tailwindPlugin = () => { + return { + postcssPlugin: 'tailwindcss', + async Once(root, { result }) { + env.DEBUG && console.time('Compiling CSS') + await tailwind(({ createContext }) => { + console.error() + console.error('Rebuilding...') + + return () => { + return state.getContext({ + createContext, + cliConfigPath, + root, + result, + content, + }) + } + })(root, result) + env.DEBUG && console.timeEnd('Compiling CSS') + }, + } + } + + tailwindPlugin.postcss = true + + let plugins = [ + ...beforePlugins, + tailwindPlugin, + !args['--minify'] && formatNodes, + ...afterPlugins, + ].filter(Boolean) + + /** @type {import('postcss').Processor} */ + // @ts-ignore + let processor = postcss(plugins) + + async function readInput() { + // Piping in data, let's drain the stdin + if (input === '-') { + return drainStdin() + } + + // Input file has been provided + if (input) { + return fs.promises.readFile(path.resolve(input), 'utf8') + } + + // No input file provided, fallback to default atrules + return '@tailwind base; @tailwind components; @tailwind utilities' + } + + async function build() { + let start = process.hrtime.bigint() + + return readInput() + .then((css) => processor.process(css, { ...postcssOptions, from: input, to: output })) + .then((result) => lightningcss(!!args['--minify'], result)) + .then((result) => { + if (!state.watcher) { + return result + } + + env.DEBUG && console.time('Recording PostCSS dependencies') + for (let message of result.messages) { + if (message.type === 'dependency') { + state.contextDependencies.add(message.file) + } + } + env.DEBUG && console.timeEnd('Recording PostCSS dependencies') + + // TODO: This needs to be in a different spot + env.DEBUG && console.time('Watch new files') + state.watcher.refreshWatchedFiles() + env.DEBUG && console.timeEnd('Watch new files') + + return result + }) + .then((result) => { + if (!output) { + process.stdout.write(result.css) + return + } + + return Promise.all([ + outputFile(result.opts.to, result.css), + result.map && outputFile(result.opts.to + '.map', result.map.toString()), + ]) + }) + .then(() => { + let end = process.hrtime.bigint() + console.error() + console.error('Done in', (end - start) / BigInt(1e6) + 'ms.') + }) + .then( + () => {}, + (err) => { + // TODO: If an initial build fails we can't easily pick up any PostCSS dependencies + // that were collected before the error occurred + // The result is not stored on the error so we have to store it externally + // and pull the messages off of it here somehow + + // This results in a less than ideal DX because the watcher will not pick up + // changes to imported CSS if one of them caused an error during the initial build + // If you fix it and then save the main CSS file so there's no error + // The watcher will start watching the imported CSS files and will be + // resilient to future errors. + + if (state.watcher) { + console.error(err) + } else { + return Promise.reject(err) + } + } + ) + } + + /** + * @param {{file: string, content(): Promise<string>, extension: string}[]} changes + */ + async function parseChanges(changes) { + return Promise.all( + changes.map(async (change) => ({ + content: await change.content(), + extension: change.extension, + })) + ) + } + + if (input !== undefined && input !== '-') { + state.contextDependencies.add(path.resolve(input)) + } + + return { + build, + watch: async () => { + state.watcher = createWatcher(args, { + state, + + /** + * @param {{file: string, content(): Promise<string>, extension: string}[]} changes + */ + async rebuild(changes) { + let needsNewContext = changes.some((change) => { + return ( + state.configBag?.dependencies.has(change.file) || + state.contextDependencies.has(change.file) + ) + }) + + if (needsNewContext) { + state.context = null + } else { + for (let change of await parseChanges(changes)) { + state.changedContent.push(change) + } + } + + return build() + }, + }) + + await build() + }, + } +} diff --git a/node_modules/tailwindcss/src/oxide/cli/build/utils.ts b/node_modules/tailwindcss/src/oxide/cli/build/utils.ts new file mode 100644 index 0000000..001857a --- /dev/null +++ b/node_modules/tailwindcss/src/oxide/cli/build/utils.ts @@ -0,0 +1,74 @@ +import fs from 'fs' +import path from 'path' + +export function indentRecursive(node, indent = 0) { + node.each && + node.each((child, i) => { + if (!child.raws.before || !child.raws.before.trim() || child.raws.before.includes('\n')) { + child.raws.before = `\n${node.type !== 'rule' && i > 0 ? '\n' : ''}${' '.repeat(indent)}` + } + child.raws.after = `\n${' '.repeat(indent)}` + indentRecursive(child, indent + 1) + }) +} + +export function formatNodes(root) { + indentRecursive(root) + if (root.first) { + root.first.raws.before = '' + } +} + +/** + * When rapidly saving files atomically a couple of situations can happen: + * - The file is missing since the external program has deleted it by the time we've gotten around to reading it from the earlier save. + * - The file is being written to by the external program by the time we're going to read it and is thus treated as busy because a lock is held. + * + * To work around this we retry reading the file a handful of times with a delay between each attempt + * + * @param {string} path + * @param {number} tries + * @returns {Promise<string | undefined>} + * @throws {Error} If the file is still missing or busy after the specified number of tries + */ +export async function readFileWithRetries(path, tries = 5) { + for (let n = 0; n <= tries; n++) { + try { + return await fs.promises.readFile(path, 'utf8') + } catch (err) { + if (n !== tries) { + if (err.code === 'ENOENT' || err.code === 'EBUSY') { + await new Promise((resolve) => setTimeout(resolve, 10)) + + continue + } + } + + throw err + } + } +} + +export function drainStdin() { + return new Promise((resolve, reject) => { + let result = '' + process.stdin.on('data', (chunk) => { + result += chunk + }) + process.stdin.on('end', () => resolve(result)) + process.stdin.on('error', (err) => reject(err)) + }) +} + +export async function outputFile(file, newContents) { + try { + let currentContents = await fs.promises.readFile(file, 'utf8') + if (currentContents === newContents) { + return // Skip writing the file + } + } catch {} + + // Write the file + await fs.promises.mkdir(path.dirname(file), { recursive: true }) + await fs.promises.writeFile(file, newContents, 'utf8') +} diff --git a/node_modules/tailwindcss/src/oxide/cli/build/watching.ts b/node_modules/tailwindcss/src/oxide/cli/build/watching.ts new file mode 100644 index 0000000..9d5680a --- /dev/null +++ b/node_modules/tailwindcss/src/oxide/cli/build/watching.ts @@ -0,0 +1,225 @@ +import chokidar from 'chokidar' +import fs from 'fs' +import micromatch from 'micromatch' +import normalizePath from 'normalize-path' +import path from 'path' + +import { readFileWithRetries } from './utils' + +/** + * The core idea of this watcher is: + * 1. Whenever a file is added, changed, or renamed we queue a rebuild + * 2. Perform as few rebuilds as possible by batching them together + * 3. Coalesce events that happen in quick succession to avoid unnecessary rebuilds + * 4. Ensure another rebuild happens _if_ changed while a rebuild is in progress + */ + +/** + * + * @param {*} args + * @param {{ state, rebuild(changedFiles: any[]): Promise<any> }} param1 + * @returns {{ + * fswatcher: import('chokidar').FSWatcher, + * refreshWatchedFiles(): void, + * }} + */ +export function createWatcher(args, { state, rebuild }) { + let shouldPoll = args['--poll'] + let shouldCoalesceWriteEvents = shouldPoll || process.platform === 'win32' + + // Polling interval in milliseconds + // Used only when polling or coalescing add/change events on Windows + let pollInterval = 10 + + let watcher = chokidar.watch([], { + // Force checking for atomic writes in all situations + // This causes chokidar to wait up to 100ms for a file to re-added after it's been unlinked + // This only works when watching directories though + atomic: true, + + usePolling: shouldPoll, + interval: shouldPoll ? pollInterval : undefined, + ignoreInitial: true, + awaitWriteFinish: shouldCoalesceWriteEvents + ? { + stabilityThreshold: 50, + pollInterval: pollInterval, + } + : false, + }) + + // A queue of rebuilds, file reads, etc… to run + let chain = Promise.resolve() + + /** + * A list of files that have been changed since the last rebuild + * + * @type {{file: string, content: () => Promise<string>, extension: string}[]} + */ + let changedContent = [] + + /** + * A list of files for which a rebuild has already been queued. + * This is used to prevent duplicate rebuilds when multiple events are fired for the same file. + * The rebuilt file is cleared from this list when it's associated rebuild has _started_ + * This is because if the file is changed during a rebuild it won't trigger a new rebuild which it should + **/ + let pendingRebuilds = new Set() + + let _timer + let _reject + + /** + * Rebuilds the changed files and resolves when the rebuild is + * complete regardless of whether it was successful or not + */ + async function rebuildAndContinue() { + let changes = changedContent.splice(0) + + // There are no changes to rebuild so we can just do nothing + if (changes.length === 0) { + return Promise.resolve() + } + + // Clear all pending rebuilds for the about-to-be-built files + changes.forEach((change) => pendingRebuilds.delete(change.file)) + + // Resolve the promise even when the rebuild fails + return rebuild(changes).then( + () => {}, + () => {} + ) + } + + /** + * + * @param {*} file + * @param {(() => Promise<string>) | null} content + * @param {boolean} skipPendingCheck + * @returns {Promise<void>} + */ + function recordChangedFile(file, content = null, skipPendingCheck = false) { + file = path.resolve(file) + + // Applications like Vim/Neovim fire both rename and change events in succession for atomic writes + // In that case rebuild has already been queued by rename, so can be skipped in change + if (pendingRebuilds.has(file) && !skipPendingCheck) { + return Promise.resolve() + } + + // Mark that a rebuild of this file is going to happen + // It MUST happen synchronously before the rebuild is queued for this to be effective + pendingRebuilds.add(file) + + changedContent.push({ + file, + content: content ?? (() => fs.promises.readFile(file, 'utf8')), + extension: path.extname(file).slice(1), + }) + + if (_timer) { + clearTimeout(_timer) + _reject() + } + + // If a rebuild is already in progress we don't want to start another one until the 10ms timer has expired + chain = chain.then( + () => + new Promise((resolve, reject) => { + _timer = setTimeout(resolve, 10) + _reject = reject + }) + ) + + // Resolves once this file has been rebuilt (or the rebuild for this file has failed) + // This queues as many rebuilds as there are changed files + // But those rebuilds happen after some delay + // And will immediately resolve if there are no changes + chain = chain.then(rebuildAndContinue, rebuildAndContinue) + + return chain + } + + watcher.on('change', (file) => recordChangedFile(file)) + watcher.on('add', (file) => recordChangedFile(file)) + + // Restore watching any files that are "removed" + // This can happen when a file is pseudo-atomically replaced (a copy is created, overwritten, the old one is unlinked, and the new one is renamed) + // TODO: An an optimization we should allow removal when the config changes + watcher.on('unlink', (file) => { + file = normalizePath(file) + + // Only re-add the file if it's not covered by a dynamic pattern + if (!micromatch.some([file], state.contentPatterns.dynamic)) { + watcher.add(file) + } + }) + + // Some applications such as Visual Studio (but not VS Code) + // will only fire a rename event for atomic writes and not a change event + // This is very likely a chokidar bug but it's one we need to work around + // We treat this as a change event and rebuild the CSS + watcher.on('raw', (evt, filePath, meta) => { + if (evt !== 'rename') { + return + } + + let watchedPath = meta.watchedPath + + // Watched path might be the file itself + // Or the directory it is in + filePath = watchedPath.endsWith(filePath) ? watchedPath : path.join(watchedPath, filePath) + + // Skip this event since the files it is for does not match any of the registered content globs + if (!micromatch.some([filePath], state.contentPatterns.all)) { + return + } + + // Skip since we've already queued a rebuild for this file that hasn't happened yet + if (pendingRebuilds.has(filePath)) { + return + } + + // We'll go ahead and add the file to the pending rebuilds list here + // It'll be removed when the rebuild starts unless the read fails + // which will be taken care of as well + pendingRebuilds.add(filePath) + + async function enqueue() { + try { + // We need to read the file as early as possible outside of the chain + // because it may be gone by the time we get to it. doing the read + // immediately increases the chance that the file is still there + let content = await readFileWithRetries(path.resolve(filePath)) + + if (content === undefined) { + return + } + + // This will push the rebuild onto the chain + // We MUST skip the rebuild check here otherwise the rebuild will never happen on Linux + // This is because the order of events and timing is different on Linux + // @ts-ignore: TypeScript isn't picking up that content is a string here + await recordChangedFile(filePath, () => content, true) + } catch { + // If reading the file fails, it's was probably a deleted temporary file + // So we can ignore it and no rebuild is needed + } + } + + enqueue().then(() => { + // If the file read fails we still need to make sure the file isn't stuck in the pending rebuilds list + pendingRebuilds.delete(filePath) + }) + }) + + return { + fswatcher: watcher, + + refreshWatchedFiles() { + watcher.add(Array.from(state.contextDependencies)) + watcher.add(Array.from(state.configBag.dependencies)) + watcher.add(state.contentPatterns.all) + }, + } +} diff --git a/node_modules/tailwindcss/src/oxide/cli/help/index.ts b/node_modules/tailwindcss/src/oxide/cli/help/index.ts new file mode 100644 index 0000000..c36042a --- /dev/null +++ b/node_modules/tailwindcss/src/oxide/cli/help/index.ts @@ -0,0 +1,69 @@ +import packageJson from '../../../../package.json' + +export function help({ message, usage, commands, options }) { + let indent = 2 + + // Render header + console.log() + console.log(`${packageJson.name} v${packageJson.version}`) + + // Render message + if (message) { + console.log() + for (let msg of message.split('\n')) { + console.log(msg) + } + } + + // Render usage + if (usage && usage.length > 0) { + console.log() + console.log('Usage:') + for (let example of usage) { + console.log(' '.repeat(indent), example) + } + } + + // Render commands + if (commands && commands.length > 0) { + console.log() + console.log('Commands:') + for (let command of commands) { + console.log(' '.repeat(indent), command) + } + } + + // Render options + if (options) { + let groupedOptions = {} + for (let [key, value] of Object.entries(options)) { + if (typeof value === 'object') { + groupedOptions[key] = { ...value, flags: [key] } + } else { + groupedOptions[value].flags.push(key) + } + } + + console.log() + console.log('Options:') + for (let { flags, description, deprecated } of Object.values(groupedOptions)) { + if (deprecated) continue + + if (flags.length === 1) { + console.log( + ' '.repeat(indent + 4 /* 4 = "-i, ".length */), + flags.slice().reverse().join(', ').padEnd(20, ' '), + description + ) + } else { + console.log( + ' '.repeat(indent), + flags.slice().reverse().join(', ').padEnd(24, ' '), + description + ) + } + } + } + + console.log() +} diff --git a/node_modules/tailwindcss/src/oxide/cli/index.ts b/node_modules/tailwindcss/src/oxide/cli/index.ts new file mode 100644 index 0000000..96b284d --- /dev/null +++ b/node_modules/tailwindcss/src/oxide/cli/index.ts @@ -0,0 +1,204 @@ +#!/usr/bin/env node + +import path from 'path' +import arg from 'arg' +import fs from 'fs' + +import { build } from './build' +import { help } from './help' +import { init } from './init' + +// --- + +function oneOf(...options) { + return Object.assign( + (value = true) => { + for (let option of options) { + let parsed = option(value) + if (parsed === value) { + return parsed + } + } + + throw new Error('...') + }, + { manualParsing: true } + ) +} + +let commands = { + init: { + run: init, + args: { + '--esm': { type: Boolean, description: `Initialize configuration file as ESM` }, + '--ts': { type: Boolean, description: `Initialize configuration file as TypeScript` }, + '--full': { + type: Boolean, + description: `Include the default values for all options in the generated configuration file`, + }, + '-f': '--full', + }, + }, + build: { + run: build, + args: { + '--input': { type: String, description: 'Input file' }, + '--output': { type: String, description: 'Output file' }, + '--watch': { + type: oneOf(String, Boolean), + description: 'Watch for changes and rebuild as needed', + }, + '--poll': { + type: Boolean, + description: 'Use polling instead of filesystem events when watching', + }, + '--content': { + type: String, + description: 'Content paths to use for removing unused classes', + }, + '--minify': { type: Boolean, description: 'Minify the output' }, + '--config': { + type: String, + description: 'Path to a custom config file', + }, + '-c': '--config', + '-i': '--input', + '-o': '--output', + '-m': '--minify', + '-w': '--watch', + '-p': '--poll', + }, + }, +} + +let sharedFlags = { + '--help': { type: Boolean, description: 'Display usage information' }, + '-h': '--help', +} + +if ( + process.stdout.isTTY /* Detect redirecting output to a file */ && + (process.argv[2] === undefined || + process.argv.slice(2).every((flag) => sharedFlags[flag] !== undefined)) +) { + help({ + usage: [ + 'tailwindcss [--input input.css] [--output output.css] [--watch] [options...]', + 'tailwindcss init [--full] [options...]', + ], + commands: Object.keys(commands) + .filter((command) => command !== 'build') + .map((command) => `${command} [options]`), + options: { ...commands.build.args, ...sharedFlags }, + }) + process.exit(0) +} + +let command = ((arg = '') => (arg.startsWith('-') ? undefined : arg))(process.argv[2]) || 'build' + +if (commands[command] === undefined) { + if (fs.existsSync(path.resolve(command))) { + // TODO: Deprecate this in future versions + // Check if non-existing command, might be a file. + command = 'build' + } else { + help({ + message: `Invalid command: ${command}`, + usage: ['tailwindcss <command> [options]'], + commands: Object.keys(commands) + .filter((command) => command !== 'build') + .map((command) => `${command} [options]`), + options: sharedFlags, + }) + process.exit(1) + } +} + +// Execute command +let { args: flags, run } = commands[command] +let args = (() => { + try { + let result = arg( + Object.fromEntries( + Object.entries({ ...flags, ...sharedFlags }) + .filter(([_key, value]) => !value?.type?.manualParsing) + .map(([key, value]) => [key, typeof value === 'object' ? value.type : value]) + ), + { permissive: true } + ) + + // Manual parsing of flags to allow for special flags like oneOf(Boolean, String) + for (let i = result['_'].length - 1; i >= 0; --i) { + let flag = result['_'][i] + if (!flag.startsWith('-')) continue + + let [flagName, flagValue] = flag.split('=') + let handler = flags[flagName] + + // Resolve flagName & handler + while (typeof handler === 'string') { + flagName = handler + handler = flags[handler] + } + + if (!handler) continue + + let args = [] + let offset = i + 1 + + // --flag value syntax was used so we need to pull `value` from `args` + if (flagValue === undefined) { + // Parse args for current flag + while (result['_'][offset] && !result['_'][offset].startsWith('-')) { + args.push(result['_'][offset++]) + } + + // Cleanup manually parsed flags + args + result['_'].splice(i, 1 + args.length) + + // No args were provided, use default value defined in handler + // One arg was provided, use that directly + // Multiple args were provided so pass them all in an array + flagValue = args.length === 0 ? undefined : args.length === 1 ? args[0] : args + } else { + // Remove the whole flag from the args array + result['_'].splice(i, 1) + } + + // Set the resolved value in the `result` object + result[flagName] = handler.type(flagValue, flagName) + } + + // Ensure that the `command` is always the first argument in the `args`. + // This is important so that we don't have to check if a default command + // (build) was used or not from within each plugin. + // + // E.g.: tailwindcss input.css -> _: ['build', 'input.css'] + // E.g.: tailwindcss build input.css -> _: ['build', 'input.css'] + if (result['_'][0] !== command) { + result['_'].unshift(command) + } + + return result + } catch (err) { + if (err.code === 'ARG_UNKNOWN_OPTION') { + help({ + message: err.message, + usage: ['tailwindcss <command> [options]'], + options: sharedFlags, + }) + process.exit(1) + } + throw err + } +})() + +if (args['--help']) { + help({ + options: { ...flags, ...sharedFlags }, + usage: [`tailwindcss ${command} [options]`], + }) + process.exit(0) +} + +run(args) diff --git a/node_modules/tailwindcss/src/oxide/cli/init/index.ts b/node_modules/tailwindcss/src/oxide/cli/init/index.ts new file mode 100644 index 0000000..abc93cd --- /dev/null +++ b/node_modules/tailwindcss/src/oxide/cli/init/index.ts @@ -0,0 +1,59 @@ +import fs from 'fs' +import path from 'path' + +function isESM() { + const pkgPath = path.resolve('./package.json') + + try { + let pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')) + return pkg.type && pkg.type === 'module' + } catch (err) { + return false + } +} + +export function init(args) { + let messages: string[] = [] + + let isProjectESM = args['--ts'] || args['--esm'] || isESM() + let syntax = args['--ts'] ? 'ts' : isProjectESM ? 'js' : 'cjs' + let extension = args['--ts'] ? 'ts' : 'js' + + let tailwindConfigLocation = path.resolve(args['_'][1] ?? `./tailwind.config.${extension}`) + + if (fs.existsSync(tailwindConfigLocation)) { + messages.push(`${path.basename(tailwindConfigLocation)} already exists.`) + } else { + let stubContentsFile = fs.readFileSync( + args['--full'] + ? path.resolve(__dirname, '../../../../stubs/config.full.js') + : path.resolve(__dirname, '../../../../stubs/config.simple.js'), + 'utf8' + ) + + let stubFile = fs.readFileSync( + path.resolve(__dirname, `../../../../stubs/tailwind.config.${syntax}`), + 'utf8' + ) + + // Change colors import + stubContentsFile = stubContentsFile.replace('../colors', 'tailwindcss/colors') + + // Replace contents of {ts,js,cjs} file with the stub {simple,full}. + stubFile = + stubFile + .replace('__CONFIG__', stubContentsFile.replace('module.exports =', '').trim()) + .trim() + '\n\n' + + fs.writeFileSync(tailwindConfigLocation, stubFile, 'utf8') + + messages.push(`Created Tailwind CSS config file: ${path.basename(tailwindConfigLocation)}`) + } + + if (messages.length > 0) { + console.log() + for (let message of messages) { + console.log(message) + } + } +} diff --git a/node_modules/tailwindcss/src/oxide/postcss-plugin.ts b/node_modules/tailwindcss/src/oxide/postcss-plugin.ts new file mode 100644 index 0000000..1be3203 --- /dev/null +++ b/node_modules/tailwindcss/src/oxide/postcss-plugin.ts @@ -0,0 +1 @@ +module.exports = require('../plugin.js') diff --git a/node_modules/tailwindcss/src/plugin.js b/node_modules/tailwindcss/src/plugin.js new file mode 100644 index 0000000..bbb8cc1 --- /dev/null +++ b/node_modules/tailwindcss/src/plugin.js @@ -0,0 +1,107 @@ +import setupTrackingContext from './lib/setupTrackingContext' +import processTailwindFeatures from './processTailwindFeatures' +import { env } from './lib/sharedState' +import { findAtConfigPath } from './lib/findAtConfigPath' + +module.exports = function tailwindcss(configOrPath) { + return { + postcssPlugin: 'tailwindcss', + plugins: [ + env.DEBUG && + function (root) { + console.log('\n') + console.time('JIT TOTAL') + return root + }, + async function (root, result) { + // Use the path for the `@config` directive if it exists, otherwise use the + // path for the file being processed + configOrPath = findAtConfigPath(root, result) ?? configOrPath + + let context = setupTrackingContext(configOrPath) + + if (root.type === 'document') { + let roots = root.nodes.filter((node) => node.type === 'root') + + for (const root of roots) { + if (root.type === 'root') { + await processTailwindFeatures(context)(root, result) + } + } + + return + } + + await processTailwindFeatures(context)(root, result) + }, + __OXIDE__ && + function lightningCssPlugin(_root, result) { + let postcss = require('postcss') + let lightningcss = require('lightningcss') + let browserslist = require('browserslist') + + try { + let transformed = lightningcss.transform({ + filename: result.opts.from, + code: Buffer.from(result.root.toString()), + minify: false, + sourceMap: !!result.map, + inputSourceMap: result.map ? result.map.toString() : undefined, + targets: + typeof process !== 'undefined' && process.env.JEST_WORKER_ID + ? { chrome: 106 << 16 } + : lightningcss.browserslistToTargets( + browserslist(require('../package.json').browserslist) + ), + + drafts: { + nesting: true, + customMedia: true, + }, + }) + + result.map = Object.assign(result.map ?? {}, { + toJSON() { + return transformed.map.toJSON() + }, + toString() { + return transformed.map.toString() + }, + }) + + result.root = postcss.parse(transformed.code.toString('utf8')) + } catch (err) { + if (typeof process !== 'undefined' && process.env.JEST_WORKER_ID) { + let lines = err.source.split('\n') + err = new Error( + [ + 'Error formatting using Lightning CSS:', + '', + ...[ + '```css', + ...lines.slice(Math.max(err.loc.line - 3, 0), err.loc.line), + ' '.repeat(err.loc.column - 1) + '^-- ' + err.toString(), + ...lines.slice(err.loc.line, err.loc.line + 2), + '```', + ], + ].join('\n') + ) + } + + if (Error.captureStackTrace) { + Error.captureStackTrace(err, lightningCssPlugin) + } + throw err + } + }, + env.DEBUG && + function (root) { + console.timeEnd('JIT TOTAL') + console.log('\n') + return root + }, + ].filter(Boolean), + } +} + +module.exports.postcss = true diff --git a/node_modules/tailwindcss/src/postcss-plugins/nesting/README.md b/node_modules/tailwindcss/src/postcss-plugins/nesting/README.md new file mode 100644 index 0000000..49cdbb5 --- /dev/null +++ b/node_modules/tailwindcss/src/postcss-plugins/nesting/README.md @@ -0,0 +1,42 @@ +# tailwindcss/nesting + +This is a PostCSS plugin that wraps [postcss-nested](https://github.com/postcss/postcss-nested) or [postcss-nesting](https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-nesting) and acts as a compatibility layer to make sure your nesting plugin of choice properly understands Tailwind's custom syntax like `@apply` and `@screen`. + +Add it to your PostCSS configuration, somewhere before Tailwind itself: + +```js +// postcss.config.js +module.exports = { + plugins: [ + require('postcss-import'), + require('tailwindcss/nesting'), + require('tailwindcss'), + require('autoprefixer'), + ] +} +``` + +By default, it uses the [postcss-nested](https://github.com/postcss/postcss-nested) plugin under the hood, which uses a Sass-like syntax and is the plugin that powers nesting support in the [Tailwind CSS plugin API](https://tailwindcss.com/docs/plugins#css-in-js-syntax). + +If you'd rather use [postcss-nesting](https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-nesting) (which is based on the work-in-progress [CSS Nesting](https://drafts.csswg.org/css-nesting-1/) specification), first install the plugin alongside: + +```shell +npm install postcss-nesting +``` + +Then pass the plugin itself as an argument to `tailwindcss/nesting` in your PostCSS configuration: + +```js +// postcss.config.js +module.exports = { + plugins: [ + require('postcss-import'), + require('tailwindcss/nesting')(require('postcss-nesting')), + require('tailwindcss'), + require('autoprefixer'), + ] +} +``` + +This can also be helpful if for whatever reason you need to use a very specific version of `postcss-nested` and want to override the version we bundle with `tailwindcss/nesting` itself. + diff --git a/node_modules/tailwindcss/src/postcss-plugins/nesting/index.js b/node_modules/tailwindcss/src/postcss-plugins/nesting/index.js new file mode 100644 index 0000000..9fbcddf --- /dev/null +++ b/node_modules/tailwindcss/src/postcss-plugins/nesting/index.js @@ -0,0 +1,13 @@ +import { nesting } from './plugin' + +export default Object.assign( + function (opts) { + return { + postcssPlugin: 'tailwindcss/nesting', + Once(root, { result }) { + return nesting(opts)(root, result) + }, + } + }, + { postcss: true } +) diff --git a/node_modules/tailwindcss/src/postcss-plugins/nesting/plugin.js b/node_modules/tailwindcss/src/postcss-plugins/nesting/plugin.js new file mode 100644 index 0000000..a13cfd4 --- /dev/null +++ b/node_modules/tailwindcss/src/postcss-plugins/nesting/plugin.js @@ -0,0 +1,80 @@ +import postcss from 'postcss' +import postcssNested from 'postcss-nested' + +export function nesting(opts = postcssNested) { + return (root, result) => { + root.walkAtRules('screen', (rule) => { + rule.name = 'media' + rule.params = `screen(${rule.params})` + }) + + root.walkAtRules('apply', (rule) => { + rule.before(postcss.decl({ prop: '__apply', value: rule.params, source: rule.source })) + rule.remove() + }) + + let plugin = (() => { + if ( + typeof opts === 'function' || + (typeof opts === 'object' && opts?.hasOwnProperty?.('postcssPlugin')) + ) { + return opts + } + + if (typeof opts === 'string') { + return require(opts) + } + + if (Object.keys(opts).length <= 0) { + return postcssNested + } + + throw new Error('tailwindcss/nesting should be loaded with a nesting plugin.') + })() + + postcss([plugin]).process(root, result.opts).sync() + + root.walkDecls('__apply', (decl) => { + decl.before(postcss.atRule({ name: 'apply', params: decl.value, source: decl.source })) + decl.remove() + }) + + /** + * Use a private PostCSS API to remove the "clean" flag from the entire AST. + * This is done because running process() on the AST will set the "clean" + * flag on all nodes, which we don't want. + * + * This causes downstream plugins using the visitor API to be skipped. + * + * This is guarded because the PostCSS API is not public + * and may change in future versions of PostCSS. + * + * See https://github.com/postcss/postcss/issues/1712 for more details + * + * @param {import('postcss').Node} node + */ + function markDirty(node) { + if (!('markDirty' in node)) { + return + } + + // Traverse the tree down to the leaf nodes + if (node.nodes) { + node.nodes.forEach((n) => markDirty(n)) + } + + // If it's a leaf node mark it as dirty + // We do this here because marking a node as dirty + // will walk up the tree and mark all parents as dirty + // resulting in a lot of unnecessary work if we did this + // for every single node + if (!node.nodes) { + node.markDirty() + } + } + + markDirty(root) + + return root + } +} diff --git a/node_modules/tailwindcss/src/processTailwindFeatures.js b/node_modules/tailwindcss/src/processTailwindFeatures.js new file mode 100644 index 0000000..fa363b0 --- /dev/null +++ b/node_modules/tailwindcss/src/processTailwindFeatures.js @@ -0,0 +1,59 @@ +import normalizeTailwindDirectives from './lib/normalizeTailwindDirectives' +import expandTailwindAtRules from './lib/expandTailwindAtRules' +import expandApplyAtRules from './lib/expandApplyAtRules' +import evaluateTailwindFunctions from './lib/evaluateTailwindFunctions' +import substituteScreenAtRules from './lib/substituteScreenAtRules' +import resolveDefaultsAtRules from './lib/resolveDefaultsAtRules' +import collapseAdjacentRules from './lib/collapseAdjacentRules' +import collapseDuplicateDeclarations from './lib/collapseDuplicateDeclarations' +import partitionApplyAtRules from './lib/partitionApplyAtRules' +import detectNesting from './lib/detectNesting' +import { createContext } from './lib/setupContextUtils' +import { issueFlagNotices } from './featureFlags' + +export default function processTailwindFeatures(setupContext) { + return async function (root, result) { + let { tailwindDirectives, applyDirectives } = normalizeTailwindDirectives(root) + + detectNesting()(root, result) + + // Partition apply rules that are found in the css + // itself. + partitionApplyAtRules()(root, result) + + let context = setupContext({ + tailwindDirectives, + applyDirectives, + registerDependency(dependency) { + result.messages.push({ + plugin: 'tailwindcss', + parent: result.opts.from, + ...dependency, + }) + }, + createContext(tailwindConfig, changedContent) { + return createContext(tailwindConfig, changedContent, root) + }, + })(root, result) + + if (context.tailwindConfig.separator === '-') { + throw new Error( + "The '-' character cannot be used as a custom separator in JIT mode due to parsing ambiguity. Please use another character like '_' instead." + ) + } + + issueFlagNotices(context.tailwindConfig) + + await expandTailwindAtRules(context)(root, result) + + // Partition apply rules that are generated by + // addComponents, addUtilities and so on. + partitionApplyAtRules()(root, result) + expandApplyAtRules(context)(root, result) + evaluateTailwindFunctions(context)(root, result) + substituteScreenAtRules(context)(root, result) + resolveDefaultsAtRules(context)(root, result) + collapseAdjacentRules(context)(root, result) + collapseDuplicateDeclarations(context)(root, result) + } +} diff --git a/node_modules/tailwindcss/src/public/colors.js b/node_modules/tailwindcss/src/public/colors.js new file mode 100644 index 0000000..0764bfa --- /dev/null +++ b/node_modules/tailwindcss/src/public/colors.js @@ -0,0 +1,322 @@ +import log from '../util/log' + +function warn({ version, from, to }) { + log.warn(`${from}-color-renamed`, [ + `As of Tailwind CSS ${version}, \`${from}\` has been renamed to \`${to}\`.`, + 'Update your configuration file to silence this warning.', + ]) +} + +export default { + inherit: 'inherit', + current: 'currentColor', + transparent: 'transparent', + black: '#000', + white: '#fff', + slate: { + 50: '#f8fafc', + 100: '#f1f5f9', + 200: '#e2e8f0', + 300: '#cbd5e1', + 400: '#94a3b8', + 500: '#64748b', + 600: '#475569', + 700: '#334155', + 800: '#1e293b', + 900: '#0f172a', + 950: '#020617', + }, + gray: { + 50: '#f9fafb', + 100: '#f3f4f6', + 200: '#e5e7eb', + 300: '#d1d5db', + 400: '#9ca3af', + 500: '#6b7280', + 600: '#4b5563', + 700: '#374151', + 800: '#1f2937', + 900: '#111827', + 950: '#030712', + }, + zinc: { + 50: '#fafafa', + 100: '#f4f4f5', + 200: '#e4e4e7', + 300: '#d4d4d8', + 400: '#a1a1aa', + 500: '#71717a', + 600: '#52525b', + 700: '#3f3f46', + 800: '#27272a', + 900: '#18181b', + 950: '#09090b', + }, + neutral: { + 50: '#fafafa', + 100: '#f5f5f5', + 200: '#e5e5e5', + 300: '#d4d4d4', + 400: '#a3a3a3', + 500: '#737373', + 600: '#525252', + 700: '#404040', + 800: '#262626', + 900: '#171717', + 950: '#0a0a0a', + }, + stone: { + 50: '#fafaf9', + 100: '#f5f5f4', + 200: '#e7e5e4', + 300: '#d6d3d1', + 400: '#a8a29e', + 500: '#78716c', + 600: '#57534e', + 700: '#44403c', + 800: '#292524', + 900: '#1c1917', + 950: '#0c0a09', + }, + red: { + 50: '#fef2f2', + 100: '#fee2e2', + 200: '#fecaca', + 300: '#fca5a5', + 400: '#f87171', + 500: '#ef4444', + 600: '#dc2626', + 700: '#b91c1c', + 800: '#991b1b', + 900: '#7f1d1d', + 950: '#450a0a', + }, + orange: { + 50: '#fff7ed', + 100: '#ffedd5', + 200: '#fed7aa', + 300: '#fdba74', + 400: '#fb923c', + 500: '#f97316', + 600: '#ea580c', + 700: '#c2410c', + 800: '#9a3412', + 900: '#7c2d12', + 950: '#431407', + }, + amber: { + 50: '#fffbeb', + 100: '#fef3c7', + 200: '#fde68a', + 300: '#fcd34d', + 400: '#fbbf24', + 500: '#f59e0b', + 600: '#d97706', + 700: '#b45309', + 800: '#92400e', + 900: '#78350f', + 950: '#451a03', + }, + yellow: { + 50: '#fefce8', + 100: '#fef9c3', + 200: '#fef08a', + 300: '#fde047', + 400: '#facc15', + 500: '#eab308', + 600: '#ca8a04', + 700: '#a16207', + 800: '#854d0e', + 900: '#713f12', + 950: '#422006', + }, + lime: { + 50: '#f7fee7', + 100: '#ecfccb', + 200: '#d9f99d', + 300: '#bef264', + 400: '#a3e635', + 500: '#84cc16', + 600: '#65a30d', + 700: '#4d7c0f', + 800: '#3f6212', + 900: '#365314', + 950: '#1a2e05', + }, + green: { + 50: '#f0fdf4', + 100: '#dcfce7', + 200: '#bbf7d0', + 300: '#86efac', + 400: '#4ade80', + 500: '#22c55e', + 600: '#16a34a', + 700: '#15803d', + 800: '#166534', + 900: '#14532d', + 950: '#052e16', + }, + emerald: { + 50: '#ecfdf5', + 100: '#d1fae5', + 200: '#a7f3d0', + 300: '#6ee7b7', + 400: '#34d399', + 500: '#10b981', + 600: '#059669', + 700: '#047857', + 800: '#065f46', + 900: '#064e3b', + 950: '#022c22', + }, + teal: { + 50: '#f0fdfa', + 100: '#ccfbf1', + 200: '#99f6e4', + 300: '#5eead4', + 400: '#2dd4bf', + 500: '#14b8a6', + 600: '#0d9488', + 700: '#0f766e', + 800: '#115e59', + 900: '#134e4a', + 950: '#042f2e', + }, + cyan: { + 50: '#ecfeff', + 100: '#cffafe', + 200: '#a5f3fc', + 300: '#67e8f9', + 400: '#22d3ee', + 500: '#06b6d4', + 600: '#0891b2', + 700: '#0e7490', + 800: '#155e75', + 900: '#164e63', + 950: '#083344', + }, + sky: { + 50: '#f0f9ff', + 100: '#e0f2fe', + 200: '#bae6fd', + 300: '#7dd3fc', + 400: '#38bdf8', + 500: '#0ea5e9', + 600: '#0284c7', + 700: '#0369a1', + 800: '#075985', + 900: '#0c4a6e', + 950: '#082f49', + }, + blue: { + 50: '#eff6ff', + 100: '#dbeafe', + 200: '#bfdbfe', + 300: '#93c5fd', + 400: '#60a5fa', + 500: '#3b82f6', + 600: '#2563eb', + 700: '#1d4ed8', + 800: '#1e40af', + 900: '#1e3a8a', + 950: '#172554', + }, + indigo: { + 50: '#eef2ff', + 100: '#e0e7ff', + 200: '#c7d2fe', + 300: '#a5b4fc', + 400: '#818cf8', + 500: '#6366f1', + 600: '#4f46e5', + 700: '#4338ca', + 800: '#3730a3', + 900: '#312e81', + 950: '#1e1b4b', + }, + violet: { + 50: '#f5f3ff', + 100: '#ede9fe', + 200: '#ddd6fe', + 300: '#c4b5fd', + 400: '#a78bfa', + 500: '#8b5cf6', + 600: '#7c3aed', + 700: '#6d28d9', + 800: '#5b21b6', + 900: '#4c1d95', + 950: '#2e1065', + }, + purple: { + 50: '#faf5ff', + 100: '#f3e8ff', + 200: '#e9d5ff', + 300: '#d8b4fe', + 400: '#c084fc', + 500: '#a855f7', + 600: '#9333ea', + 700: '#7e22ce', + 800: '#6b21a8', + 900: '#581c87', + 950: '#3b0764', + }, + fuchsia: { + 50: '#fdf4ff', + 100: '#fae8ff', + 200: '#f5d0fe', + 300: '#f0abfc', + 400: '#e879f9', + 500: '#d946ef', + 600: '#c026d3', + 700: '#a21caf', + 800: '#86198f', + 900: '#701a75', + 950: '#4a044e', + }, + pink: { + 50: '#fdf2f8', + 100: '#fce7f3', + 200: '#fbcfe8', + 300: '#f9a8d4', + 400: '#f472b6', + 500: '#ec4899', + 600: '#db2777', + 700: '#be185d', + 800: '#9d174d', + 900: '#831843', + 950: '#500724', + }, + rose: { + 50: '#fff1f2', + 100: '#ffe4e6', + 200: '#fecdd3', + 300: '#fda4af', + 400: '#fb7185', + 500: '#f43f5e', + 600: '#e11d48', + 700: '#be123c', + 800: '#9f1239', + 900: '#881337', + 950: '#4c0519', + }, + get lightBlue() { + warn({ version: 'v2.2', from: 'lightBlue', to: 'sky' }) + return this.sky + }, + get warmGray() { + warn({ version: 'v3.0', from: 'warmGray', to: 'stone' }) + return this.stone + }, + get trueGray() { + warn({ version: 'v3.0', from: 'trueGray', to: 'neutral' }) + return this.neutral + }, + get coolGray() { + warn({ version: 'v3.0', from: 'coolGray', to: 'gray' }) + return this.gray + }, + get blueGray() { + warn({ version: 'v3.0', from: 'blueGray', to: 'slate' }) + return this.slate + }, +} diff --git a/node_modules/tailwindcss/src/public/create-plugin.js b/node_modules/tailwindcss/src/public/create-plugin.js new file mode 100644 index 0000000..4124fc3 --- /dev/null +++ b/node_modules/tailwindcss/src/public/create-plugin.js @@ -0,0 +1,2 @@ +import createPlugin from '../util/createPlugin' +export default createPlugin diff --git a/node_modules/tailwindcss/src/public/default-config.js b/node_modules/tailwindcss/src/public/default-config.js new file mode 100644 index 0000000..4c7e907 --- /dev/null +++ b/node_modules/tailwindcss/src/public/default-config.js @@ -0,0 +1,4 @@ +import { cloneDeep } from '../util/cloneDeep' +import defaultConfig from '../../stubs/config.full' + +export default cloneDeep(defaultConfig) diff --git a/node_modules/tailwindcss/src/public/default-theme.js b/node_modules/tailwindcss/src/public/default-theme.js new file mode 100644 index 0000000..582edc3 --- /dev/null +++ b/node_modules/tailwindcss/src/public/default-theme.js @@ -0,0 +1,4 @@ +import { cloneDeep } from '../util/cloneDeep' +import defaultFullConfig from '../../stubs/config.full' + +export default cloneDeep(defaultFullConfig.theme) diff --git a/node_modules/tailwindcss/src/public/load-config.js b/node_modules/tailwindcss/src/public/load-config.js new file mode 100644 index 0000000..d5d3712 --- /dev/null +++ b/node_modules/tailwindcss/src/public/load-config.js @@ -0,0 +1,2 @@ +import { loadConfig } from '../lib/load-config' +export default loadConfig diff --git a/node_modules/tailwindcss/src/public/resolve-config.js b/node_modules/tailwindcss/src/public/resolve-config.js new file mode 100644 index 0000000..891d90a --- /dev/null +++ b/node_modules/tailwindcss/src/public/resolve-config.js @@ -0,0 +1,7 @@ +import resolveConfigObjects from '../util/resolveConfig' +import getAllConfigs from '../util/getAllConfigs' + +export default function resolveConfig(...configs) { + let [, ...defaultConfigs] = getAllConfigs(configs[0]) + return resolveConfigObjects([...configs, ...defaultConfigs]) +} diff --git a/node_modules/tailwindcss/src/util/applyImportantSelector.js b/node_modules/tailwindcss/src/util/applyImportantSelector.js new file mode 100644 index 0000000..ff9ec4f --- /dev/null +++ b/node_modules/tailwindcss/src/util/applyImportantSelector.js @@ -0,0 +1,27 @@ +import parser from 'postcss-selector-parser' +import { movePseudos } from './pseudoElements' + +export function applyImportantSelector(selector, important) { + let sel = parser().astSync(selector) + + sel.each((sel) => { + // Wrap with :is if it's not already wrapped + let isWrapped = + sel.nodes[0].type === 'pseudo' && + sel.nodes[0].value === ':is' && + sel.nodes.every((node) => node.type !== 'combinator') + + if (!isWrapped) { + sel.nodes = [ + parser.pseudo({ + value: ':is', + nodes: [sel.clone()], + }), + ] + } + + movePseudos(sel) + }) + + return `${important} ${sel.toString()}` +} diff --git a/node_modules/tailwindcss/src/util/bigSign.js b/node_modules/tailwindcss/src/util/bigSign.js new file mode 100644 index 0000000..8514aef --- /dev/null +++ b/node_modules/tailwindcss/src/util/bigSign.js @@ -0,0 +1,3 @@ +export default function bigSign(bigIntValue) { + return (bigIntValue > 0n) - (bigIntValue < 0n) +} diff --git a/node_modules/tailwindcss/src/util/buildMediaQuery.js b/node_modules/tailwindcss/src/util/buildMediaQuery.js new file mode 100644 index 0000000..8489dd4 --- /dev/null +++ b/node_modules/tailwindcss/src/util/buildMediaQuery.js @@ -0,0 +1,22 @@ +export default function buildMediaQuery(screens) { + screens = Array.isArray(screens) ? screens : [screens] + + return screens + .map((screen) => { + let values = screen.values.map((screen) => { + if (screen.raw !== undefined) { + return screen.raw + } + + return [ + screen.min && `(min-width: ${screen.min})`, + screen.max && `(max-width: ${screen.max})`, + ] + .filter(Boolean) + .join(' and ') + }) + + return screen.not ? `not all and ${values}` : values + }) + .join(', ') +} diff --git a/node_modules/tailwindcss/src/util/cloneDeep.js b/node_modules/tailwindcss/src/util/cloneDeep.js new file mode 100644 index 0000000..47a1217 --- /dev/null +++ b/node_modules/tailwindcss/src/util/cloneDeep.js @@ -0,0 +1,11 @@ +export function cloneDeep(value) { + if (Array.isArray(value)) { + return value.map((child) => cloneDeep(child)) + } + + if (typeof value === 'object' && value !== null) { + return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, cloneDeep(v)])) + } + + return value +} diff --git a/node_modules/tailwindcss/src/util/cloneNodes.js b/node_modules/tailwindcss/src/util/cloneNodes.js new file mode 100644 index 0000000..299dd63 --- /dev/null +++ b/node_modules/tailwindcss/src/util/cloneNodes.js @@ -0,0 +1,28 @@ +export default function cloneNodes(nodes, source = undefined, raws = undefined) { + return nodes.map((node) => { + let cloned = node.clone() + + // We always want override the source map + // except when explicitly told not to + let shouldOverwriteSource = node.raws.tailwind?.preserveSource !== true || !cloned.source + + if (source !== undefined && shouldOverwriteSource) { + cloned.source = source + + if ('walk' in cloned) { + cloned.walk((child) => { + child.source = source + }) + } + } + + if (raws !== undefined) { + cloned.raws.tailwind = { + ...cloned.raws.tailwind, + ...raws, + } + } + + return cloned + }) +} diff --git a/node_modules/tailwindcss/src/util/color.js b/node_modules/tailwindcss/src/util/color.js new file mode 100644 index 0000000..733ca99 --- /dev/null +++ b/node_modules/tailwindcss/src/util/color.js @@ -0,0 +1,88 @@ +import namedColors from './colorNames' + +let HEX = /^#([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})?$/i +let SHORT_HEX = /^#([a-f\d])([a-f\d])([a-f\d])([a-f\d])?$/i +let VALUE = /(?:\d+|\d*\.\d+)%?/ +let SEP = /(?:\s*,\s*|\s+)/ +let ALPHA_SEP = /\s*[,/]\s*/ +let CUSTOM_PROPERTY = /var\(--(?:[^ )]*?)(?:,(?:[^ )]*?|var\(--[^ )]*?\)))?\)/ + +let RGB = new RegExp( + `^(rgba?)\\(\\s*(${VALUE.source}|${CUSTOM_PROPERTY.source})(?:${SEP.source}(${VALUE.source}|${CUSTOM_PROPERTY.source}))?(?:${SEP.source}(${VALUE.source}|${CUSTOM_PROPERTY.source}))?(?:${ALPHA_SEP.source}(${VALUE.source}|${CUSTOM_PROPERTY.source}))?\\s*\\)$` +) +let HSL = new RegExp( + `^(hsla?)\\(\\s*((?:${VALUE.source})(?:deg|rad|grad|turn)?|${CUSTOM_PROPERTY.source})(?:${SEP.source}(${VALUE.source}|${CUSTOM_PROPERTY.source}))?(?:${SEP.source}(${VALUE.source}|${CUSTOM_PROPERTY.source}))?(?:${ALPHA_SEP.source}(${VALUE.source}|${CUSTOM_PROPERTY.source}))?\\s*\\)$` +) + +// In "loose" mode the color may contain fewer than 3 parts, as long as at least +// one of the parts is variable. +export function parseColor(value, { loose = false } = {}) { + if (typeof value !== 'string') { + return null + } + + value = value.trim() + if (value === 'transparent') { + return { mode: 'rgb', color: ['0', '0', '0'], alpha: '0' } + } + + if (value in namedColors) { + return { mode: 'rgb', color: namedColors[value].map((v) => v.toString()) } + } + + let hex = value + .replace(SHORT_HEX, (_, r, g, b, a) => ['#', r, r, g, g, b, b, a ? a + a : ''].join('')) + .match(HEX) + + if (hex !== null) { + return { + mode: 'rgb', + color: [parseInt(hex[1], 16), parseInt(hex[2], 16), parseInt(hex[3], 16)].map((v) => + v.toString() + ), + alpha: hex[4] ? (parseInt(hex[4], 16) / 255).toString() : undefined, + } + } + + let match = value.match(RGB) ?? value.match(HSL) + + if (match === null) { + return null + } + + let color = [match[2], match[3], match[4]].filter(Boolean).map((v) => v.toString()) + + // rgba(var(--my-color), 0.1) + // hsla(var(--my-color), 0.1) + if (color.length === 2 && color[0].startsWith('var(')) { + return { + mode: match[1], + color: [color[0]], + alpha: color[1], + } + } + + if (!loose && color.length !== 3) { + return null + } + + if (color.length < 3 && !color.some((part) => /^var\(.*?\)$/.test(part))) { + return null + } + + return { + mode: match[1], + color, + alpha: match[5]?.toString?.(), + } +} + +export function formatColor({ mode, color, alpha }) { + let hasAlpha = alpha !== undefined + + if (mode === 'rgba' || mode === 'hsla') { + return `${mode}(${color.join(', ')}${hasAlpha ? `, ${alpha}` : ''})` + } + + return `${mode}(${color.join(' ')}${hasAlpha ? ` / ${alpha}` : ''})` +} diff --git a/node_modules/tailwindcss/src/util/colorNames.js b/node_modules/tailwindcss/src/util/colorNames.js new file mode 100644 index 0000000..a056cce --- /dev/null +++ b/node_modules/tailwindcss/src/util/colorNames.js @@ -0,0 +1,150 @@ +export default { + aliceblue: [240, 248, 255], + antiquewhite: [250, 235, 215], + aqua: [0, 255, 255], + aquamarine: [127, 255, 212], + azure: [240, 255, 255], + beige: [245, 245, 220], + bisque: [255, 228, 196], + black: [0, 0, 0], + blanchedalmond: [255, 235, 205], + blue: [0, 0, 255], + blueviolet: [138, 43, 226], + brown: [165, 42, 42], + burlywood: [222, 184, 135], + cadetblue: [95, 158, 160], + chartreuse: [127, 255, 0], + chocolate: [210, 105, 30], + coral: [255, 127, 80], + cornflowerblue: [100, 149, 237], + cornsilk: [255, 248, 220], + crimson: [220, 20, 60], + cyan: [0, 255, 255], + darkblue: [0, 0, 139], + darkcyan: [0, 139, 139], + darkgoldenrod: [184, 134, 11], + darkgray: [169, 169, 169], + darkgreen: [0, 100, 0], + darkgrey: [169, 169, 169], + darkkhaki: [189, 183, 107], + darkmagenta: [139, 0, 139], + darkolivegreen: [85, 107, 47], + darkorange: [255, 140, 0], + darkorchid: [153, 50, 204], + darkred: [139, 0, 0], + darksalmon: [233, 150, 122], + darkseagreen: [143, 188, 143], + darkslateblue: [72, 61, 139], + darkslategray: [47, 79, 79], + darkslategrey: [47, 79, 79], + darkturquoise: [0, 206, 209], + darkviolet: [148, 0, 211], + deeppink: [255, 20, 147], + deepskyblue: [0, 191, 255], + dimgray: [105, 105, 105], + dimgrey: [105, 105, 105], + dodgerblue: [30, 144, 255], + firebrick: [178, 34, 34], + floralwhite: [255, 250, 240], + forestgreen: [34, 139, 34], + fuchsia: [255, 0, 255], + gainsboro: [220, 220, 220], + ghostwhite: [248, 248, 255], + gold: [255, 215, 0], + goldenrod: [218, 165, 32], + gray: [128, 128, 128], + green: [0, 128, 0], + greenyellow: [173, 255, 47], + grey: [128, 128, 128], + honeydew: [240, 255, 240], + hotpink: [255, 105, 180], + indianred: [205, 92, 92], + indigo: [75, 0, 130], + ivory: [255, 255, 240], + khaki: [240, 230, 140], + lavender: [230, 230, 250], + lavenderblush: [255, 240, 245], + lawngreen: [124, 252, 0], + lemonchiffon: [255, 250, 205], + lightblue: [173, 216, 230], + lightcoral: [240, 128, 128], + lightcyan: [224, 255, 255], + lightgoldenrodyellow: [250, 250, 210], + lightgray: [211, 211, 211], + lightgreen: [144, 238, 144], + lightgrey: [211, 211, 211], + lightpink: [255, 182, 193], + lightsalmon: [255, 160, 122], + lightseagreen: [32, 178, 170], + lightskyblue: [135, 206, 250], + lightslategray: [119, 136, 153], + lightslategrey: [119, 136, 153], + lightsteelblue: [176, 196, 222], + lightyellow: [255, 255, 224], + lime: [0, 255, 0], + limegreen: [50, 205, 50], + linen: [250, 240, 230], + magenta: [255, 0, 255], + maroon: [128, 0, 0], + mediumaquamarine: [102, 205, 170], + mediumblue: [0, 0, 205], + mediumorchid: [186, 85, 211], + mediumpurple: [147, 112, 219], + mediumseagreen: [60, 179, 113], + mediumslateblue: [123, 104, 238], + mediumspringgreen: [0, 250, 154], + mediumturquoise: [72, 209, 204], + mediumvioletred: [199, 21, 133], + midnightblue: [25, 25, 112], + mintcream: [245, 255, 250], + mistyrose: [255, 228, 225], + moccasin: [255, 228, 181], + navajowhite: [255, 222, 173], + navy: [0, 0, 128], + oldlace: [253, 245, 230], + olive: [128, 128, 0], + olivedrab: [107, 142, 35], + orange: [255, 165, 0], + orangered: [255, 69, 0], + orchid: [218, 112, 214], + palegoldenrod: [238, 232, 170], + palegreen: [152, 251, 152], + paleturquoise: [175, 238, 238], + palevioletred: [219, 112, 147], + papayawhip: [255, 239, 213], + peachpuff: [255, 218, 185], + peru: [205, 133, 63], + pink: [255, 192, 203], + plum: [221, 160, 221], + powderblue: [176, 224, 230], + purple: [128, 0, 128], + rebeccapurple: [102, 51, 153], + red: [255, 0, 0], + rosybrown: [188, 143, 143], + royalblue: [65, 105, 225], + saddlebrown: [139, 69, 19], + salmon: [250, 128, 114], + sandybrown: [244, 164, 96], + seagreen: [46, 139, 87], + seashell: [255, 245, 238], + sienna: [160, 82, 45], + silver: [192, 192, 192], + skyblue: [135, 206, 235], + slateblue: [106, 90, 205], + slategray: [112, 128, 144], + slategrey: [112, 128, 144], + snow: [255, 250, 250], + springgreen: [0, 255, 127], + steelblue: [70, 130, 180], + tan: [210, 180, 140], + teal: [0, 128, 128], + thistle: [216, 191, 216], + tomato: [255, 99, 71], + turquoise: [64, 224, 208], + violet: [238, 130, 238], + wheat: [245, 222, 179], + white: [255, 255, 255], + whitesmoke: [245, 245, 245], + yellow: [255, 255, 0], + yellowgreen: [154, 205, 50], +} diff --git a/node_modules/tailwindcss/src/util/configurePlugins.js b/node_modules/tailwindcss/src/util/configurePlugins.js new file mode 100644 index 0000000..4220eae --- /dev/null +++ b/node_modules/tailwindcss/src/util/configurePlugins.js @@ -0,0 +1,23 @@ +export default function (pluginConfig, plugins) { + if (pluginConfig === undefined) { + return plugins + } + + const pluginNames = Array.isArray(pluginConfig) + ? pluginConfig + : [ + ...new Set( + plugins + .filter((pluginName) => { + return pluginConfig !== false && pluginConfig[pluginName] !== false + }) + .concat( + Object.keys(pluginConfig).filter((pluginName) => { + return pluginConfig[pluginName] !== false + }) + ) + ), + ] + + return pluginNames +} diff --git a/node_modules/tailwindcss/src/util/createPlugin.js b/node_modules/tailwindcss/src/util/createPlugin.js new file mode 100644 index 0000000..c2c7f5f --- /dev/null +++ b/node_modules/tailwindcss/src/util/createPlugin.js @@ -0,0 +1,27 @@ +function createPlugin(plugin, config) { + return { + handler: plugin, + config, + } +} + +createPlugin.withOptions = function (pluginFunction, configFunction = () => ({})) { + const optionsFunction = function (options) { + return { + __options: options, + handler: pluginFunction(options), + config: configFunction(options), + } + } + + optionsFunction.__isOptionsFunction = true + + // Expose plugin dependencies so that `object-hash` returns a different + // value if anything here changes, to ensure a rebuild is triggered. + optionsFunction.__pluginFunction = pluginFunction + optionsFunction.__configFunction = configFunction + + return optionsFunction +} + +export default createPlugin diff --git a/node_modules/tailwindcss/src/util/createUtilityPlugin.js b/node_modules/tailwindcss/src/util/createUtilityPlugin.js new file mode 100644 index 0000000..32c2c8a --- /dev/null +++ b/node_modules/tailwindcss/src/util/createUtilityPlugin.js @@ -0,0 +1,37 @@ +import transformThemeValue from './transformThemeValue' + +export default function createUtilityPlugin( + themeKey, + utilityVariations = [[themeKey, [themeKey]]], + { filterDefault = false, ...options } = {} +) { + let transformValue = transformThemeValue(themeKey) + return function ({ matchUtilities, theme }) { + for (let utilityVariation of utilityVariations) { + let group = Array.isArray(utilityVariation[0]) ? utilityVariation : [utilityVariation] + + matchUtilities( + group.reduce((obj, [classPrefix, properties]) => { + return Object.assign(obj, { + [classPrefix]: (value) => { + return properties.reduce((obj, name) => { + if (Array.isArray(name)) { + return Object.assign(obj, { [name[0]]: name[1] }) + } + return Object.assign(obj, { [name]: transformValue(value) }) + }, {}) + }, + }) + }, {}), + { + ...options, + values: filterDefault + ? Object.fromEntries( + Object.entries(theme(themeKey) ?? {}).filter(([modifier]) => modifier !== 'DEFAULT') + ) + : theme(themeKey), + } + ) + } + } +} diff --git a/node_modules/tailwindcss/src/util/dataTypes.js b/node_modules/tailwindcss/src/util/dataTypes.js new file mode 100644 index 0000000..005c37a --- /dev/null +++ b/node_modules/tailwindcss/src/util/dataTypes.js @@ -0,0 +1,394 @@ +import { parseColor } from './color' +import { parseBoxShadowValue } from './parseBoxShadowValue' +import { splitAtTopLevelOnly } from './splitAtTopLevelOnly' + +let cssFunctions = ['min', 'max', 'clamp', 'calc'] + +// Ref: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Types + +function isCSSFunction(value) { + return cssFunctions.some((fn) => new RegExp(`^${fn}\\(.*\\)`).test(value)) +} + +// These properties accept a `<dashed-ident>` as one of the values. This means that you can use them +// as: `timeline-scope: --tl;` +// +// Without the `var(--tl)`, in these cases we don't want to normalize the value, and you should add +// the `var()` yourself. +// +// More info: +// - https://drafts.csswg.org/scroll-animations/#propdef-timeline-scope +// - https://developer.mozilla.org/en-US/docs/Web/CSS/timeline-scope#dashed-ident +// +const AUTO_VAR_INJECTION_EXCEPTIONS = new Set([ + // Concrete properties + 'scroll-timeline-name', + 'timeline-scope', + 'view-timeline-name', + 'font-palette', + + // Shorthand properties + 'scroll-timeline', + 'animation-timeline', + 'view-timeline', +]) + +// This is not a data type, but rather a function that can normalize the +// correct values. +export function normalize(value, context = null, isRoot = true) { + let isVarException = context && AUTO_VAR_INJECTION_EXCEPTIONS.has(context.property) + if (value.startsWith('--') && !isVarException) { + return `var(${value})` + } + + // Keep raw strings if it starts with `url(` + if (value.includes('url(')) { + return value + .split(/(url\(.*?\))/g) + .filter(Boolean) + .map((part) => { + if (/^url\(.*?\)$/.test(part)) { + return part + } + + return normalize(part, context, false) + }) + .join('') + } + + // Convert `_` to ` `, except for escaped underscores `\_` + value = value + .replace( + /([^\\])_+/g, + (fullMatch, characterBefore) => characterBefore + ' '.repeat(fullMatch.length - 1) + ) + .replace(/^_/g, ' ') + .replace(/\\_/g, '_') + + // Remove leftover whitespace + if (isRoot) { + value = value.trim() + } + + value = normalizeMathOperatorSpacing(value) + + return value +} + +/** + * Add spaces around operators inside math functions + * like calc() that do not follow an operator or '('. + * + * @param {string} value + * @returns {string} + */ +function normalizeMathOperatorSpacing(value) { + let preventFormattingInFunctions = ['theme'] + let preventFormattingKeywords = [ + 'min-content', + 'max-content', + 'fit-content', + + // Env + 'safe-area-inset-top', + 'safe-area-inset-right', + 'safe-area-inset-bottom', + 'safe-area-inset-left', + + 'titlebar-area-x', + 'titlebar-area-y', + 'titlebar-area-width', + 'titlebar-area-height', + + 'keyboard-inset-top', + 'keyboard-inset-right', + 'keyboard-inset-bottom', + 'keyboard-inset-left', + 'keyboard-inset-width', + 'keyboard-inset-height', + ] + + return value.replace(/(calc|min|max|clamp)\(.+\)/g, (match) => { + let result = '' + + function lastChar() { + let char = result.trimEnd() + return char[char.length - 1] + } + + for (let i = 0; i < match.length; i++) { + function peek(word) { + return word.split('').every((char, j) => match[i + j] === char) + } + + function consumeUntil(chars) { + let minIndex = Infinity + for (let char of chars) { + let index = match.indexOf(char, i) + if (index !== -1 && index < minIndex) { + minIndex = index + } + } + + let result = match.slice(i, minIndex) + i += result.length - 1 + return result + } + + let char = match[i] + + // Handle `var(--variable)` + if (peek('var')) { + // When we consume until `)`, then we are dealing with this scenario: + // `var(--example)` + // + // When we consume until `,`, then we are dealing with this scenario: + // `var(--example, 1rem)` + // + // In this case we do want to "format", the default value as well + result += consumeUntil([')', ',']) + } + + // Skip formatting of known keywords + else if (preventFormattingKeywords.some((keyword) => peek(keyword))) { + let keyword = preventFormattingKeywords.find((keyword) => peek(keyword)) + result += keyword + i += keyword.length - 1 + } + + // Skip formatting inside known functions + else if (preventFormattingInFunctions.some((fn) => peek(fn))) { + result += consumeUntil([')']) + } + + // Handle operators + else if ( + ['+', '-', '*', '/'].includes(char) && + !['(', '+', '-', '*', '/'].includes(lastChar()) + ) { + result += ` ${char} ` + } else { + result += char + } + } + + // Simplify multiple spaces + return result.replace(/\s+/g, ' ') + }) +} + +export function url(value) { + return value.startsWith('url(') +} + +export function number(value) { + return !isNaN(Number(value)) || isCSSFunction(value) +} + +export function percentage(value) { + return (value.endsWith('%') && number(value.slice(0, -1))) || isCSSFunction(value) +} + +// Please refer to MDN when updating this list: +// https://developer.mozilla.org/en-US/docs/Learn/CSS/Building_blocks/Values_and_units +// https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Container_Queries#container_query_length_units +let lengthUnits = [ + 'cm', + 'mm', + 'Q', + 'in', + 'pc', + 'pt', + 'px', + 'em', + 'ex', + 'ch', + 'rem', + 'lh', + 'rlh', + 'vw', + 'vh', + 'vmin', + 'vmax', + 'vb', + 'vi', + 'svw', + 'svh', + 'lvw', + 'lvh', + 'dvw', + 'dvh', + 'cqw', + 'cqh', + 'cqi', + 'cqb', + 'cqmin', + 'cqmax', +] +let lengthUnitsPattern = `(?:${lengthUnits.join('|')})` +export function length(value) { + return ( + value === '0' || + new RegExp(`^[+-]?[0-9]*\.?[0-9]+(?:[eE][+-]?[0-9]+)?${lengthUnitsPattern}$`).test(value) || + isCSSFunction(value) + ) +} + +let lineWidths = new Set(['thin', 'medium', 'thick']) +export function lineWidth(value) { + return lineWidths.has(value) +} + +export function shadow(value) { + let parsedShadows = parseBoxShadowValue(normalize(value)) + + for (let parsedShadow of parsedShadows) { + if (!parsedShadow.valid) { + return false + } + } + + return true +} + +export function color(value) { + let colors = 0 + + let result = splitAtTopLevelOnly(value, '_').every((part) => { + part = normalize(part) + + if (part.startsWith('var(')) return true + if (parseColor(part, { loose: true }) !== null) return colors++, true + + return false + }) + + if (!result) return false + return colors > 0 +} + +export function image(value) { + let images = 0 + let result = splitAtTopLevelOnly(value, ',').every((part) => { + part = normalize(part) + + if (part.startsWith('var(')) return true + if ( + url(part) || + gradient(part) || + ['element(', 'image(', 'cross-fade(', 'image-set('].some((fn) => part.startsWith(fn)) + ) { + images++ + return true + } + + return false + }) + + if (!result) return false + return images > 0 +} + +let gradientTypes = new Set([ + 'conic-gradient', + 'linear-gradient', + 'radial-gradient', + 'repeating-conic-gradient', + 'repeating-linear-gradient', + 'repeating-radial-gradient', +]) +export function gradient(value) { + value = normalize(value) + + for (let type of gradientTypes) { + if (value.startsWith(`${type}(`)) { + return true + } + } + return false +} + +let validPositions = new Set(['center', 'top', 'right', 'bottom', 'left']) +export function position(value) { + let positions = 0 + let result = splitAtTopLevelOnly(value, '_').every((part) => { + part = normalize(part) + + if (part.startsWith('var(')) return true + if (validPositions.has(part) || length(part) || percentage(part)) { + positions++ + return true + } + + return false + }) + + if (!result) return false + return positions > 0 +} + +export function familyName(value) { + let fonts = 0 + let result = splitAtTopLevelOnly(value, ',').every((part) => { + part = normalize(part) + + if (part.startsWith('var(')) return true + + // If it contains spaces, then it should be quoted + if (part.includes(' ')) { + if (!/(['"])([^"']+)\1/g.test(part)) { + return false + } + } + + // If it starts with a number, it's invalid + if (/^\d/g.test(part)) { + return false + } + + fonts++ + + return true + }) + + if (!result) return false + return fonts > 0 +} + +let genericNames = new Set([ + 'serif', + 'sans-serif', + 'monospace', + 'cursive', + 'fantasy', + 'system-ui', + 'ui-serif', + 'ui-sans-serif', + 'ui-monospace', + 'ui-rounded', + 'math', + 'emoji', + 'fangsong', +]) +export function genericName(value) { + return genericNames.has(value) +} + +let absoluteSizes = new Set([ + 'xx-small', + 'x-small', + 'small', + 'medium', + 'large', + 'x-large', + 'x-large', + 'xxx-large', +]) +export function absoluteSize(value) { + return absoluteSizes.has(value) +} + +let relativeSizes = new Set(['larger', 'smaller']) +export function relativeSize(value) { + return relativeSizes.has(value) +} diff --git a/node_modules/tailwindcss/src/util/defaults.js b/node_modules/tailwindcss/src/util/defaults.js new file mode 100644 index 0000000..1d4aa7b --- /dev/null +++ b/node_modules/tailwindcss/src/util/defaults.js @@ -0,0 +1,17 @@ +export function defaults(target, ...sources) { + for (let source of sources) { + for (let k in source) { + if (!target?.hasOwnProperty?.(k)) { + target[k] = source[k] + } + } + + for (let k of Object.getOwnPropertySymbols(source)) { + if (!target?.hasOwnProperty?.(k)) { + target[k] = source[k] + } + } + } + + return target +} diff --git a/node_modules/tailwindcss/src/util/escapeClassName.js b/node_modules/tailwindcss/src/util/escapeClassName.js new file mode 100644 index 0000000..cb5924a --- /dev/null +++ b/node_modules/tailwindcss/src/util/escapeClassName.js @@ -0,0 +1,8 @@ +import parser from 'postcss-selector-parser' +import escapeCommas from './escapeCommas' + +export default function escapeClassName(className) { + let node = parser.className() + node.value = className + return escapeCommas(node?.raws?.value ?? node.value) +} diff --git a/node_modules/tailwindcss/src/util/escapeCommas.js b/node_modules/tailwindcss/src/util/escapeCommas.js new file mode 100644 index 0000000..e7f1c73 --- /dev/null +++ b/node_modules/tailwindcss/src/util/escapeCommas.js @@ -0,0 +1,3 @@ +export default function escapeCommas(className) { + return className.replace(/\\,/g, '\\2c ') +} diff --git a/node_modules/tailwindcss/src/util/flattenColorPalette.js b/node_modules/tailwindcss/src/util/flattenColorPalette.js new file mode 100644 index 0000000..c1259a7 --- /dev/null +++ b/node_modules/tailwindcss/src/util/flattenColorPalette.js @@ -0,0 +1,13 @@ +const flattenColorPalette = (colors) => + Object.assign( + {}, + ...Object.entries(colors ?? {}).flatMap(([color, values]) => + typeof values == 'object' + ? Object.entries(flattenColorPalette(values)).map(([number, hex]) => ({ + [color + (number === 'DEFAULT' ? '' : `-${number}`)]: hex, + })) + : [{ [`${color}`]: values }] + ) + ) + +export default flattenColorPalette diff --git a/node_modules/tailwindcss/src/util/formatVariantSelector.js b/node_modules/tailwindcss/src/util/formatVariantSelector.js new file mode 100644 index 0000000..6ba6f2c --- /dev/null +++ b/node_modules/tailwindcss/src/util/formatVariantSelector.js @@ -0,0 +1,324 @@ +import selectorParser from 'postcss-selector-parser' +import unescape from 'postcss-selector-parser/dist/util/unesc' +import escapeClassName from '../util/escapeClassName' +import prefixSelector from '../util/prefixSelector' +import { movePseudos } from './pseudoElements' +import { splitAtTopLevelOnly } from './splitAtTopLevelOnly' + +/** @typedef {import('postcss-selector-parser').Root} Root */ +/** @typedef {import('postcss-selector-parser').Selector} Selector */ +/** @typedef {import('postcss-selector-parser').Pseudo} Pseudo */ +/** @typedef {import('postcss-selector-parser').Node} Node */ + +/** @typedef {{format: string, respectPrefix: boolean}[]} RawFormats */ +/** @typedef {import('postcss-selector-parser').Root} ParsedFormats */ +/** @typedef {RawFormats | ParsedFormats} AcceptedFormats */ + +let MERGE = ':merge' + +/** + * @param {RawFormats} formats + * @param {{context: any, candidate: string, base: string | null}} options + * @returns {ParsedFormats | null} + */ +export function formatVariantSelector(formats, { context, candidate }) { + let prefix = context?.tailwindConfig.prefix ?? '' + + // Parse the format selector into an AST + let parsedFormats = formats.map((format) => { + let ast = selectorParser().astSync(format.format) + + return { + ...format, + ast: format.respectPrefix ? prefixSelector(prefix, ast) : ast, + } + }) + + // We start with the candidate selector + let formatAst = selectorParser.root({ + nodes: [ + selectorParser.selector({ + nodes: [selectorParser.className({ value: escapeClassName(candidate) })], + }), + ], + }) + + // And iteratively merge each format selector into the candidate selector + for (let { ast } of parsedFormats) { + // 1. Handle :merge() special pseudo-class + ;[formatAst, ast] = handleMergePseudo(formatAst, ast) + + // 2. Merge the format selector into the current selector AST + ast.walkNesting((nesting) => nesting.replaceWith(...formatAst.nodes[0].nodes)) + + // 3. Keep going! + formatAst = ast + } + + return formatAst +} + +/** + * Given any node in a selector this gets the "simple" selector it's a part of + * A simple selector is just a list of nodes without any combinators + * Technically :is(), :not(), :has(), etc… can have combinators but those are nested + * inside the relevant node and won't be picked up so they're fine to ignore + * + * @param {Node} node + * @returns {Node[]} + **/ +function simpleSelectorForNode(node) { + /** @type {Node[]} */ + let nodes = [] + + // Walk backwards until we hit a combinator node (or the start) + while (node.prev() && node.prev().type !== 'combinator') { + node = node.prev() + } + + // Now record all non-combinator nodes until we hit one (or the end) + while (node && node.type !== 'combinator') { + nodes.push(node) + node = node.next() + } + + return nodes +} + +/** + * Resorts the nodes in a selector to ensure they're in the correct order + * Tags go before classes, and pseudo classes go after classes + * + * @param {Selector} sel + * @returns {Selector} + **/ +function resortSelector(sel) { + sel.sort((a, b) => { + if (a.type === 'tag' && b.type === 'class') { + return -1 + } else if (a.type === 'class' && b.type === 'tag') { + return 1 + } else if (a.type === 'class' && b.type === 'pseudo' && b.value.startsWith('::')) { + return -1 + } else if (a.type === 'pseudo' && a.value.startsWith('::') && b.type === 'class') { + return 1 + } + + return sel.index(a) - sel.index(b) + }) + + return sel +} + +/** + * Remove extraneous selectors that do not include the base class/candidate + * + * Example: + * Given the utility `.a, .b { color: red}` + * Given the candidate `sm:b` + * + * The final selector should be `.sm\:b` and not `.a, .sm\:b` + * + * @param {Selector} ast + * @param {string} base + */ +export function eliminateIrrelevantSelectors(sel, base) { + let hasClassesMatchingCandidate = false + + sel.walk((child) => { + if (child.type === 'class' && child.value === base) { + hasClassesMatchingCandidate = true + return false // Stop walking + } + }) + + if (!hasClassesMatchingCandidate) { + sel.remove() + } + + // We do NOT recursively eliminate sub selectors that don't have the base class + // as this is NOT a safe operation. For example, if we have: + // `.space-x-2 > :not([hidden]) ~ :not([hidden])` + // We cannot remove the [hidden] from the :not() because it would change the + // meaning of the selector. + + // TODO: Can we do this for :matches, :is, and :where? +} + +/** + * @param {string} current + * @param {AcceptedFormats} formats + * @param {{context: any, candidate: string, base: string | null}} options + * @returns {string} + */ +export function finalizeSelector(current, formats, { context, candidate, base }) { + let separator = context?.tailwindConfig?.separator ?? ':' + + // Split by the separator, but ignore the separator inside square brackets: + // + // E.g.: dark:lg:hover:[paint-order:markers] + // ┬ ┬ ┬ ┬ + // │ │ │ ╰── We will not split here + // ╰──┴─────┴─────────────── We will split here + // + base = base ?? splitAtTopLevelOnly(candidate, separator).pop() + + // Parse the selector into an AST + let selector = selectorParser().astSync(current) + + // Normalize escaped classes, e.g.: + // + // The idea would be to replace the escaped `base` in the selector with the + // `format`. However, in css you can escape the same selector in a few + // different ways. This would result in different strings and therefore we + // can't replace it properly. + // + // base: bg-[rgb(255,0,0)] + // base in selector: bg-\\[rgb\\(255\\,0\\,0\\)\\] + // escaped base: bg-\\[rgb\\(255\\2c 0\\2c 0\\)\\] + // + selector.walkClasses((node) => { + if (node.raws && node.value.includes(base)) { + node.raws.value = escapeClassName(unescape(node.raws.value)) + } + }) + + // Remove extraneous selectors that do not include the base candidate + selector.each((sel) => eliminateIrrelevantSelectors(sel, base)) + + // If ffter eliminating irrelevant selectors, we end up with nothing + // Then the whole "rule" this is associated with does not need to exist + // We use `null` as a marker value for that case + if (selector.length === 0) { + return null + } + + // If there are no formats that means there were no variants added to the candidate + // so we can just return the selector as-is + let formatAst = Array.isArray(formats) + ? formatVariantSelector(formats, { context, candidate }) + : formats + + if (formatAst === null) { + return selector.toString() + } + + let simpleStart = selectorParser.comment({ value: '/*__simple__*/' }) + let simpleEnd = selectorParser.comment({ value: '/*__simple__*/' }) + + // We can safely replace the escaped base now, since the `base` section is + // now in a normalized escaped value. + selector.walkClasses((node) => { + if (node.value !== base) { + return + } + + let parent = node.parent + let formatNodes = formatAst.nodes[0].nodes + + // Perf optimization: if the parent is a single class we can just replace it and be done + if (parent.nodes.length === 1) { + node.replaceWith(...formatNodes) + return + } + + let simpleSelector = simpleSelectorForNode(node) + parent.insertBefore(simpleSelector[0], simpleStart) + parent.insertAfter(simpleSelector[simpleSelector.length - 1], simpleEnd) + + for (let child of formatNodes) { + parent.insertBefore(simpleSelector[0], child.clone()) + } + + node.remove() + + // Re-sort the simple selector to ensure it's in the correct order + simpleSelector = simpleSelectorForNode(simpleStart) + let firstNode = parent.index(simpleStart) + + parent.nodes.splice( + firstNode, + simpleSelector.length, + ...resortSelector(selectorParser.selector({ nodes: simpleSelector })).nodes + ) + + simpleStart.remove() + simpleEnd.remove() + }) + + // Remove unnecessary pseudo selectors that we used as placeholders + selector.walkPseudos((p) => { + if (p.value === MERGE) { + p.replaceWith(p.nodes) + } + }) + + // Move pseudo elements to the end of the selector (if necessary) + selector.each((sel) => movePseudos(sel)) + + return selector.toString() +} + +/** + * + * @param {Selector} selector + * @param {Selector} format + */ +export function handleMergePseudo(selector, format) { + /** @type {{pseudo: Pseudo, value: string}[]} */ + let merges = [] + + // Find all :merge() pseudo-classes in `selector` + selector.walkPseudos((pseudo) => { + if (pseudo.value === MERGE) { + merges.push({ + pseudo, + value: pseudo.nodes[0].toString(), + }) + } + }) + + // Find all :merge() "attachments" in `format` and attach them to the matching selector in `selector` + format.walkPseudos((pseudo) => { + if (pseudo.value !== MERGE) { + return + } + + let value = pseudo.nodes[0].toString() + + // Does `selector` contain a :merge() pseudo-class with the same value? + let existing = merges.find((merge) => merge.value === value) + + // Nope so there's nothing to do + if (!existing) { + return + } + + // Everything after `:merge()` up to the next combinator is what is attached to the merged selector + let attachments = [] + let next = pseudo.next() + while (next && next.type !== 'combinator') { + attachments.push(next) + next = next.next() + } + + let combinator = next + + existing.pseudo.parent.insertAfter( + existing.pseudo, + selectorParser.selector({ nodes: attachments.map((node) => node.clone()) }) + ) + + pseudo.remove() + attachments.forEach((node) => node.remove()) + + // What about this case: + // :merge(.group):focus > & + // :merge(.group):hover & + if (combinator && combinator.type === 'combinator') { + combinator.remove() + } + }) + + return [selector, format] +} diff --git a/node_modules/tailwindcss/src/util/getAllConfigs.js b/node_modules/tailwindcss/src/util/getAllConfigs.js new file mode 100644 index 0000000..ce3665b --- /dev/null +++ b/node_modules/tailwindcss/src/util/getAllConfigs.js @@ -0,0 +1,38 @@ +import defaultFullConfig from '../../stubs/config.full.js' +import { flagEnabled } from '../featureFlags' + +export default function getAllConfigs(config) { + const configs = (config?.presets ?? [defaultFullConfig]) + .slice() + .reverse() + .flatMap((preset) => getAllConfigs(preset instanceof Function ? preset() : preset)) + + const features = { + // Add experimental configs here... + respectDefaultRingColorOpacity: { + theme: { + ringColor: ({ theme }) => ({ + DEFAULT: '#3b82f67f', + ...theme('colors'), + }), + }, + }, + + disableColorOpacityUtilitiesByDefault: { + corePlugins: { + backgroundOpacity: false, + borderOpacity: false, + divideOpacity: false, + placeholderOpacity: false, + ringOpacity: false, + textOpacity: false, + }, + }, + } + + const experimentals = Object.keys(features) + .filter((feature) => flagEnabled(config, feature)) + .map((feature) => features[feature]) + + return [config, ...experimentals, ...configs] +} diff --git a/node_modules/tailwindcss/src/util/hashConfig.js b/node_modules/tailwindcss/src/util/hashConfig.js new file mode 100644 index 0000000..543e020 --- /dev/null +++ b/node_modules/tailwindcss/src/util/hashConfig.js @@ -0,0 +1,5 @@ +import hash from 'object-hash' + +export default function hashConfig(config) { + return hash(config, { ignoreUnknown: true }) +} diff --git a/node_modules/tailwindcss/src/util/isKeyframeRule.js b/node_modules/tailwindcss/src/util/isKeyframeRule.js new file mode 100644 index 0000000..a745e97 --- /dev/null +++ b/node_modules/tailwindcss/src/util/isKeyframeRule.js @@ -0,0 +1,3 @@ +export default function isKeyframeRule(rule) { + return rule.parent && rule.parent.type === 'atrule' && /keyframes$/.test(rule.parent.name) +} diff --git a/node_modules/tailwindcss/src/util/isPlainObject.js b/node_modules/tailwindcss/src/util/isPlainObject.js new file mode 100644 index 0000000..6bd031a --- /dev/null +++ b/node_modules/tailwindcss/src/util/isPlainObject.js @@ -0,0 +1,8 @@ +export default function isPlainObject(value) { + if (Object.prototype.toString.call(value) !== '[object Object]') { + return false + } + + const prototype = Object.getPrototypeOf(value) + return prototype === null || Object.getPrototypeOf(prototype) === null +} diff --git a/node_modules/tailwindcss/src/util/isSyntacticallyValidPropertyValue.js b/node_modules/tailwindcss/src/util/isSyntacticallyValidPropertyValue.js new file mode 100644 index 0000000..ff00074 --- /dev/null +++ b/node_modules/tailwindcss/src/util/isSyntacticallyValidPropertyValue.js @@ -0,0 +1,61 @@ +let matchingBrackets = new Map([ + ['{', '}'], + ['[', ']'], + ['(', ')'], +]) +let inverseMatchingBrackets = new Map( + Array.from(matchingBrackets.entries()).map(([k, v]) => [v, k]) +) + +let quotes = new Set(['"', "'", '`']) + +// Arbitrary values must contain balanced brackets (), [] and {}. Escaped +// values don't count, and brackets inside quotes also don't count. +// +// E.g.: w-[this-is]w-[weird-and-invalid] +// E.g.: w-[this-is\\]w-\\[weird-but-valid] +// E.g.: content-['this-is-also-valid]-weirdly-enough'] +export default function isSyntacticallyValidPropertyValue(value) { + let stack = [] + let inQuotes = false + + for (let i = 0; i < value.length; i++) { + let char = value[i] + + if (char === ':' && !inQuotes && stack.length === 0) { + return false + } + + // Non-escaped quotes allow us to "allow" anything in between + if (quotes.has(char) && value[i - 1] !== '\\') { + inQuotes = !inQuotes + } + + if (inQuotes) continue + if (value[i - 1] === '\\') continue // Escaped + + if (matchingBrackets.has(char)) { + stack.push(char) + } else if (inverseMatchingBrackets.has(char)) { + let inverse = inverseMatchingBrackets.get(char) + + // Nothing to pop from, therefore it is unbalanced + if (stack.length <= 0) { + return false + } + + // Popped value must match the inverse value, otherwise it is unbalanced + if (stack.pop() !== inverse) { + return false + } + } + } + + // If there is still something on the stack, it is also unbalanced + if (stack.length > 0) { + return false + } + + // All good, totally balanced! + return true +} diff --git a/node_modules/tailwindcss/src/util/log.js b/node_modules/tailwindcss/src/util/log.js new file mode 100644 index 0000000..0df5c87 --- /dev/null +++ b/node_modules/tailwindcss/src/util/log.js @@ -0,0 +1,29 @@ +import colors from 'picocolors' + +let alreadyShown = new Set() + +function log(type, messages, key) { + if (typeof process !== 'undefined' && process.env.JEST_WORKER_ID) return + + if (key && alreadyShown.has(key)) return + if (key) alreadyShown.add(key) + + console.warn('') + messages.forEach((message) => console.warn(type, '-', message)) +} + +export function dim(input) { + return colors.dim(input) +} + +export default { + info(key, messages) { + log(colors.bold(colors.cyan('info')), ...(Array.isArray(key) ? [key] : [messages, key])) + }, + warn(key, messages) { + log(colors.bold(colors.yellow('warn')), ...(Array.isArray(key) ? [key] : [messages, key])) + }, + risk(key, messages) { + log(colors.bold(colors.magenta('risk')), ...(Array.isArray(key) ? [key] : [messages, key])) + }, +} diff --git a/node_modules/tailwindcss/src/util/nameClass.js b/node_modules/tailwindcss/src/util/nameClass.js new file mode 100644 index 0000000..cb37dba --- /dev/null +++ b/node_modules/tailwindcss/src/util/nameClass.js @@ -0,0 +1,30 @@ +import escapeClassName from './escapeClassName' +import escapeCommas from './escapeCommas' + +export function asClass(name) { + return escapeCommas(`.${escapeClassName(name)}`) +} + +export default function nameClass(classPrefix, key) { + return asClass(formatClass(classPrefix, key)) +} + +export function formatClass(classPrefix, key) { + if (key === 'DEFAULT') { + return classPrefix + } + + if (key === '-' || key === '-DEFAULT') { + return `-${classPrefix}` + } + + if (key.startsWith('-')) { + return `-${classPrefix}${key}` + } + + if (key.startsWith('/')) { + return `${classPrefix}${key}` + } + + return `${classPrefix}-${key}` +} diff --git a/node_modules/tailwindcss/src/util/negateValue.js b/node_modules/tailwindcss/src/util/negateValue.js new file mode 100644 index 0000000..b1cdf78 --- /dev/null +++ b/node_modules/tailwindcss/src/util/negateValue.js @@ -0,0 +1,24 @@ +export default function negateValue(value) { + value = `${value}` + + if (value === '0') { + return '0' + } + + // Flip sign of numbers + if (/^[+-]?(\d+|\d*\.\d+)(e[+-]?\d+)?(%|\w+)?$/.test(value)) { + return value.replace(/^[+-]?/, (sign) => (sign === '-' ? '' : '-')) + } + + // What functions we support negating numeric values for + // var() isn't inherently a numeric function but we support it anyway + // The trigonometric functions are omitted because you'll need to use calc(…) with them _anyway_ + // to produce generally useful results and that will be covered already + let numericFunctions = ['var', 'calc', 'min', 'max', 'clamp'] + + for (const fn of numericFunctions) { + if (value.includes(`${fn}(`)) { + return `calc(${value} * -1)` + } + } +} diff --git a/node_modules/tailwindcss/src/util/normalizeConfig.js b/node_modules/tailwindcss/src/util/normalizeConfig.js new file mode 100644 index 0000000..7e1a592 --- /dev/null +++ b/node_modules/tailwindcss/src/util/normalizeConfig.js @@ -0,0 +1,301 @@ +import { flagEnabled } from '../featureFlags' +import log, { dim } from './log' + +export function normalizeConfig(config) { + // Quick structure validation + /** + * type FilePath = string + * type RawFile = { raw: string, extension?: string } + * type ExtractorFn = (content: string) => Array<string> + * type TransformerFn = (content: string) => string + * + * type Content = + * | Array<FilePath | RawFile> + * | { + * files: Array<FilePath | RawFile>, + * extract?: ExtractorFn | { [extension: string]: ExtractorFn } + * transform?: TransformerFn | { [extension: string]: TransformerFn } + * } + */ + let valid = (() => { + // `config.purge` should not exist anymore + if (config.purge) { + return false + } + + // `config.content` should exist + if (!config.content) { + return false + } + + // `config.content` should be an object or an array + if ( + !Array.isArray(config.content) && + !(typeof config.content === 'object' && config.content !== null) + ) { + return false + } + + // When `config.content` is an array, it should consist of FilePaths or RawFiles + if (Array.isArray(config.content)) { + return config.content.every((path) => { + // `path` can be a string + if (typeof path === 'string') return true + + // `path` can be an object { raw: string, extension?: string } + // `raw` must be a string + if (typeof path?.raw !== 'string') return false + + // `extension` (if provided) should also be a string + if (path?.extension && typeof path?.extension !== 'string') { + return false + } + + return true + }) + } + + // When `config.content` is an object + if (typeof config.content === 'object' && config.content !== null) { + // Only `files`, `relative`, `extract`, and `transform` can exist in `config.content` + if ( + Object.keys(config.content).some( + (key) => !['files', 'relative', 'extract', 'transform'].includes(key) + ) + ) { + return false + } + + // `config.content.files` should exist of FilePaths or RawFiles + if (Array.isArray(config.content.files)) { + if ( + !config.content.files.every((path) => { + // `path` can be a string + if (typeof path === 'string') return true + + // `path` can be an object { raw: string, extension?: string } + // `raw` must be a string + if (typeof path?.raw !== 'string') return false + + // `extension` (if provided) should also be a string + if (path?.extension && typeof path?.extension !== 'string') { + return false + } + + return true + }) + ) { + return false + } + + // `config.content.extract` is optional, and can be a Function or a Record<String, Function> + if (typeof config.content.extract === 'object') { + for (let value of Object.values(config.content.extract)) { + if (typeof value !== 'function') { + return false + } + } + } else if ( + !(config.content.extract === undefined || typeof config.content.extract === 'function') + ) { + return false + } + + // `config.content.transform` is optional, and can be a Function or a Record<String, Function> + if (typeof config.content.transform === 'object') { + for (let value of Object.values(config.content.transform)) { + if (typeof value !== 'function') { + return false + } + } + } else if ( + !( + config.content.transform === undefined || typeof config.content.transform === 'function' + ) + ) { + return false + } + + // `config.content.relative` is optional and can be a boolean + if ( + typeof config.content.relative !== 'boolean' && + typeof config.content.relative !== 'undefined' + ) { + return false + } + } + + return true + } + + return false + })() + + if (!valid) { + log.warn('purge-deprecation', [ + 'The `purge`/`content` options have changed in Tailwind CSS v3.0.', + 'Update your configuration file to eliminate this warning.', + 'https://tailwindcss.com/docs/upgrade-guide#configure-content-sources', + ]) + } + + // Normalize the `safelist` + config.safelist = (() => { + let { content, purge, safelist } = config + + if (Array.isArray(safelist)) return safelist + if (Array.isArray(content?.safelist)) return content.safelist + if (Array.isArray(purge?.safelist)) return purge.safelist + if (Array.isArray(purge?.options?.safelist)) return purge.options.safelist + + return [] + })() + + // Normalize the `blocklist` + config.blocklist = (() => { + let { blocklist } = config + + if (Array.isArray(blocklist)) { + if (blocklist.every((item) => typeof item === 'string')) { + return blocklist + } + + log.warn('blocklist-invalid', [ + 'The `blocklist` option must be an array of strings.', + 'https://tailwindcss.com/docs/content-configuration#discarding-classes', + ]) + } + + return [] + })() + + // Normalize prefix option + if (typeof config.prefix === 'function') { + log.warn('prefix-function', [ + 'As of Tailwind CSS v3.0, `prefix` cannot be a function.', + 'Update `prefix` in your configuration to be a string to eliminate this warning.', + 'https://tailwindcss.com/docs/upgrade-guide#prefix-cannot-be-a-function', + ]) + config.prefix = '' + } else { + config.prefix = config.prefix ?? '' + } + + // Normalize the `content` + config.content = { + relative: (() => { + let { content } = config + + if (content?.relative) { + return content.relative + } + + return flagEnabled(config, 'relativeContentPathsByDefault') + })(), + + files: (() => { + let { content, purge } = config + + if (Array.isArray(purge)) return purge + if (Array.isArray(purge?.content)) return purge.content + if (Array.isArray(content)) return content + if (Array.isArray(content?.content)) return content.content + if (Array.isArray(content?.files)) return content.files + + return [] + })(), + + extract: (() => { + let extract = (() => { + if (config.purge?.extract) return config.purge.extract + if (config.content?.extract) return config.content.extract + + if (config.purge?.extract?.DEFAULT) return config.purge.extract.DEFAULT + if (config.content?.extract?.DEFAULT) return config.content.extract.DEFAULT + + if (config.purge?.options?.extractors) return config.purge.options.extractors + if (config.content?.options?.extractors) return config.content.options.extractors + + return {} + })() + + let extractors = {} + + let defaultExtractor = (() => { + if (config.purge?.options?.defaultExtractor) { + return config.purge.options.defaultExtractor + } + + if (config.content?.options?.defaultExtractor) { + return config.content.options.defaultExtractor + } + + return undefined + })() + + if (defaultExtractor !== undefined) { + extractors.DEFAULT = defaultExtractor + } + + // Functions + if (typeof extract === 'function') { + extractors.DEFAULT = extract + } + + // Arrays + else if (Array.isArray(extract)) { + for (let { extensions, extractor } of extract ?? []) { + for (let extension of extensions) { + extractors[extension] = extractor + } + } + } + + // Objects + else if (typeof extract === 'object' && extract !== null) { + Object.assign(extractors, extract) + } + + return extractors + })(), + + transform: (() => { + let transform = (() => { + if (config.purge?.transform) return config.purge.transform + if (config.content?.transform) return config.content.transform + + if (config.purge?.transform?.DEFAULT) return config.purge.transform.DEFAULT + if (config.content?.transform?.DEFAULT) return config.content.transform.DEFAULT + + return {} + })() + + let transformers = {} + + if (typeof transform === 'function') { + transformers.DEFAULT = transform + } + + if (typeof transform === 'object' && transform !== null) { + Object.assign(transformers, transform) + } + + return transformers + })(), + } + + // Validate globs to prevent bogus globs. + // E.g.: `./src/*.{html}` is invalid, the `{html}` should just be `html` + for (let file of config.content.files) { + if (typeof file === 'string' && /{([^,]*?)}/g.test(file)) { + log.warn('invalid-glob-braces', [ + `The glob pattern ${dim(file)} in your Tailwind CSS configuration is invalid.`, + `Update it to ${dim(file.replace(/{([^,]*?)}/g, '$1'))} to silence this warning.`, + // TODO: Add https://tw.wtf/invalid-glob-braces + ]) + break + } + } + + return config +} diff --git a/node_modules/tailwindcss/src/util/normalizeScreens.js b/node_modules/tailwindcss/src/util/normalizeScreens.js new file mode 100644 index 0000000..559f7cc --- /dev/null +++ b/node_modules/tailwindcss/src/util/normalizeScreens.js @@ -0,0 +1,140 @@ +/** + * @typedef {object} ScreenValue + * @property {number|undefined} min + * @property {number|undefined} max + * @property {string|undefined} raw + */ + +/** + * @typedef {object} Screen + * @property {string} name + * @property {boolean} not + * @property {ScreenValue[]} values + */ + +/** + * A function that normalizes the various forms that the screens object can be + * provided in. + * + * Input(s): + * - ['100px', '200px'] // Raw strings + * - { sm: '100px', md: '200px' } // Object with string values + * - { sm: { min: '100px' }, md: { max: '100px' } } // Object with object values + * - { sm: [{ min: '100px' }, { max: '200px' }] } // Object with object array (multiple values) + * + * Output(s): + * - [{ name: 'sm', values: [{ min: '100px', max: '200px' }] }] // List of objects, that contains multiple values + * + * @returns {Screen[]} + */ +export function normalizeScreens(screens, root = true) { + if (Array.isArray(screens)) { + return screens.map((screen) => { + if (root && Array.isArray(screen)) { + throw new Error('The tuple syntax is not supported for `screens`.') + } + + if (typeof screen === 'string') { + return { name: screen.toString(), not: false, values: [{ min: screen, max: undefined }] } + } + + let [name, options] = screen + name = name.toString() + + if (typeof options === 'string') { + return { name, not: false, values: [{ min: options, max: undefined }] } + } + + if (Array.isArray(options)) { + return { name, not: false, values: options.map((option) => resolveValue(option)) } + } + + return { name, not: false, values: [resolveValue(options)] } + }) + } + + return normalizeScreens(Object.entries(screens ?? {}), false) +} + +/** + * @param {Screen} screen + * @returns {{result: false, reason: string} | {result: true, reason: null}} + */ +export function isScreenSortable(screen) { + if (screen.values.length !== 1) { + return { result: false, reason: 'multiple-values' } + } else if (screen.values[0].raw !== undefined) { + return { result: false, reason: 'raw-values' } + } else if (screen.values[0].min !== undefined && screen.values[0].max !== undefined) { + return { result: false, reason: 'min-and-max' } + } + + return { result: true, reason: null } +} + +/** + * @param {'min' | 'max'} type + * @param {Screen | 'string'} a + * @param {Screen | 'string'} z + * @returns {number} + */ +export function compareScreens(type, a, z) { + let aScreen = toScreen(a, type) + let zScreen = toScreen(z, type) + + let aSorting = isScreenSortable(aScreen) + let bSorting = isScreenSortable(zScreen) + + // These cases should never happen and indicate a bug in Tailwind CSS itself + if (aSorting.reason === 'multiple-values' || bSorting.reason === 'multiple-values') { + throw new Error( + 'Attempted to sort a screen with multiple values. This should never happen. Please open a bug report.' + ) + } else if (aSorting.reason === 'raw-values' || bSorting.reason === 'raw-values') { + throw new Error( + 'Attempted to sort a screen with raw values. This should never happen. Please open a bug report.' + ) + } else if (aSorting.reason === 'min-and-max' || bSorting.reason === 'min-and-max') { + throw new Error( + 'Attempted to sort a screen with both min and max values. This should never happen. Please open a bug report.' + ) + } + + // Let the sorting begin + let { min: aMin, max: aMax } = aScreen.values[0] + let { min: zMin, max: zMax } = zScreen.values[0] + + // Negating screens flip their behavior. Basically `not min-width` is `max-width` + if (a.not) [aMin, aMax] = [aMax, aMin] + if (z.not) [zMin, zMax] = [zMax, zMin] + + aMin = aMin === undefined ? aMin : parseFloat(aMin) + aMax = aMax === undefined ? aMax : parseFloat(aMax) + zMin = zMin === undefined ? zMin : parseFloat(zMin) + zMax = zMax === undefined ? zMax : parseFloat(zMax) + + let [aValue, zValue] = type === 'min' ? [aMin, zMin] : [zMax, aMax] + + return aValue - zValue +} + +/** + * + * @param {PartialScreen> | string} value + * @param {'min' | 'max'} type + * @returns {Screen} + */ +export function toScreen(value, type) { + if (typeof value === 'object') { + return value + } + + return { + name: 'arbitrary-screen', + values: [{ [type]: value }], + } +} + +function resolveValue({ 'min-width': _minWidth, min = _minWidth, max, raw } = {}) { + return { min, max, raw } +} diff --git a/node_modules/tailwindcss/src/util/parseAnimationValue.js b/node_modules/tailwindcss/src/util/parseAnimationValue.js new file mode 100644 index 0000000..990e7aa --- /dev/null +++ b/node_modules/tailwindcss/src/util/parseAnimationValue.js @@ -0,0 +1,68 @@ +const DIRECTIONS = new Set(['normal', 'reverse', 'alternate', 'alternate-reverse']) +const PLAY_STATES = new Set(['running', 'paused']) +const FILL_MODES = new Set(['none', 'forwards', 'backwards', 'both']) +const ITERATION_COUNTS = new Set(['infinite']) +const TIMINGS = new Set([ + 'linear', + 'ease', + 'ease-in', + 'ease-out', + 'ease-in-out', + 'step-start', + 'step-end', +]) +const TIMING_FNS = ['cubic-bezier', 'steps'] + +const COMMA = /\,(?![^(]*\))/g // Comma separator that is not located between brackets. E.g.: `cubiz-bezier(a, b, c)` these don't count. +const SPACE = /\ +(?![^(]*\))/g // Similar to the one above, but with spaces instead. +const TIME = /^(-?[\d.]+m?s)$/ +const DIGIT = /^(\d+)$/ + +export default function parseAnimationValue(input) { + let animations = input.split(COMMA) + return animations.map((animation) => { + let value = animation.trim() + let result = { value } + let parts = value.split(SPACE) + let seen = new Set() + + for (let part of parts) { + if (!seen.has('DIRECTIONS') && DIRECTIONS.has(part)) { + result.direction = part + seen.add('DIRECTIONS') + } else if (!seen.has('PLAY_STATES') && PLAY_STATES.has(part)) { + result.playState = part + seen.add('PLAY_STATES') + } else if (!seen.has('FILL_MODES') && FILL_MODES.has(part)) { + result.fillMode = part + seen.add('FILL_MODES') + } else if ( + !seen.has('ITERATION_COUNTS') && + (ITERATION_COUNTS.has(part) || DIGIT.test(part)) + ) { + result.iterationCount = part + seen.add('ITERATION_COUNTS') + } else if (!seen.has('TIMING_FUNCTION') && TIMINGS.has(part)) { + result.timingFunction = part + seen.add('TIMING_FUNCTION') + } else if (!seen.has('TIMING_FUNCTION') && TIMING_FNS.some((f) => part.startsWith(`${f}(`))) { + result.timingFunction = part + seen.add('TIMING_FUNCTION') + } else if (!seen.has('DURATION') && TIME.test(part)) { + result.duration = part + seen.add('DURATION') + } else if (!seen.has('DELAY') && TIME.test(part)) { + result.delay = part + seen.add('DELAY') + } else if (!seen.has('NAME')) { + result.name = part + seen.add('NAME') + } else { + if (!result.unknown) result.unknown = [] + result.unknown.push(part) + } + } + + return result + }) +} diff --git a/node_modules/tailwindcss/src/util/parseBoxShadowValue.js b/node_modules/tailwindcss/src/util/parseBoxShadowValue.js new file mode 100644 index 0000000..4be3efa --- /dev/null +++ b/node_modules/tailwindcss/src/util/parseBoxShadowValue.js @@ -0,0 +1,72 @@ +import { splitAtTopLevelOnly } from './splitAtTopLevelOnly' + +let KEYWORDS = new Set(['inset', 'inherit', 'initial', 'revert', 'unset']) +let SPACE = /\ +(?![^(]*\))/g // Similar to the one above, but with spaces instead. +let LENGTH = /^-?(\d+|\.\d+)(.*?)$/g + +export function parseBoxShadowValue(input) { + let shadows = splitAtTopLevelOnly(input, ',') + return shadows.map((shadow) => { + let value = shadow.trim() + let result = { raw: value } + let parts = value.split(SPACE) + let seen = new Set() + + for (let part of parts) { + // Reset index, since the regex is stateful. + LENGTH.lastIndex = 0 + + // Keyword + if (!seen.has('KEYWORD') && KEYWORDS.has(part)) { + result.keyword = part + seen.add('KEYWORD') + } + + // Length value + else if (LENGTH.test(part)) { + if (!seen.has('X')) { + result.x = part + seen.add('X') + } else if (!seen.has('Y')) { + result.y = part + seen.add('Y') + } else if (!seen.has('BLUR')) { + result.blur = part + seen.add('BLUR') + } else if (!seen.has('SPREAD')) { + result.spread = part + seen.add('SPREAD') + } + } + + // Color or unknown + else { + if (!result.color) { + result.color = part + } else { + if (!result.unknown) result.unknown = [] + result.unknown.push(part) + } + } + } + + // Check if valid + result.valid = result.x !== undefined && result.y !== undefined + + return result + }) +} + +export function formatBoxShadowValue(shadows) { + return shadows + .map((shadow) => { + if (!shadow.valid) { + return shadow.raw + } + + return [shadow.keyword, shadow.x, shadow.y, shadow.blur, shadow.spread, shadow.color] + .filter(Boolean) + .join(' ') + }) + .join(', ') +} diff --git a/node_modules/tailwindcss/src/util/parseDependency.js b/node_modules/tailwindcss/src/util/parseDependency.js new file mode 100644 index 0000000..f26eb1a --- /dev/null +++ b/node_modules/tailwindcss/src/util/parseDependency.js @@ -0,0 +1,44 @@ +// @ts-check + +/** + * @typedef {{type: 'dependency', file: string} | {type: 'dir-dependency', dir: string, glob: string}} Dependency + */ + +/** + * + * @param {import('../lib/content.js').ContentPath} contentPath + * @returns {Dependency[]} + */ +export default function parseDependency(contentPath) { + if (contentPath.ignore) { + return [] + } + + if (!contentPath.glob) { + return [ + { + type: 'dependency', + file: contentPath.base, + }, + ] + } + + if (process.env.ROLLUP_WATCH === 'true') { + // rollup-plugin-postcss does not support dir-dependency messages + // but directories can be watched in the same way as files + return [ + { + type: 'dependency', + file: contentPath.base, + }, + ] + } + + return [ + { + type: 'dir-dependency', + dir: contentPath.base, + glob: contentPath.glob, + }, + ] +} diff --git a/node_modules/tailwindcss/src/util/parseGlob.js b/node_modules/tailwindcss/src/util/parseGlob.js new file mode 100644 index 0000000..5c03f41 --- /dev/null +++ b/node_modules/tailwindcss/src/util/parseGlob.js @@ -0,0 +1,24 @@ +import globParent from 'glob-parent' + +// Based on `glob-base` +// https://github.com/micromatch/glob-base/blob/master/index.js +export function parseGlob(pattern) { + let glob = pattern + let base = globParent(pattern) + + if (base !== '.') { + glob = pattern.substr(base.length) + if (glob.charAt(0) === '/') { + glob = glob.substr(1) + } + } + + if (glob.substr(0, 2) === './') { + glob = glob.substr(2) + } + if (glob.charAt(0) === '/') { + glob = glob.substr(1) + } + + return { base, glob } +} diff --git a/node_modules/tailwindcss/src/util/parseObjectStyles.js b/node_modules/tailwindcss/src/util/parseObjectStyles.js new file mode 100644 index 0000000..cb54787 --- /dev/null +++ b/node_modules/tailwindcss/src/util/parseObjectStyles.js @@ -0,0 +1,19 @@ +import postcss from 'postcss' +import postcssNested from 'postcss-nested' +import postcssJs from 'postcss-js' + +export default function parseObjectStyles(styles) { + if (!Array.isArray(styles)) { + return parseObjectStyles([styles]) + } + + return styles.flatMap((style) => { + return postcss([ + postcssNested({ + bubble: ['screen'], + }), + ]).process(style, { + parser: postcssJs, + }).root.nodes + }) +} diff --git a/node_modules/tailwindcss/src/util/pluginUtils.js b/node_modules/tailwindcss/src/util/pluginUtils.js new file mode 100644 index 0000000..bef9fc1 --- /dev/null +++ b/node_modules/tailwindcss/src/util/pluginUtils.js @@ -0,0 +1,291 @@ +import escapeCommas from './escapeCommas' +import { withAlphaValue } from './withAlphaVariable' +import { + normalize, + length, + number, + percentage, + url, + color as validateColor, + genericName, + familyName, + image, + absoluteSize, + relativeSize, + position, + lineWidth, + shadow, +} from './dataTypes' +import negateValue from './negateValue' +import { backgroundSize } from './validateFormalSyntax' +import { flagEnabled } from '../featureFlags.js' + +/** + * @param {import('postcss-selector-parser').Container} selectors + * @param {(className: string) => string} updateClass + * @returns {string} + */ +export function updateAllClasses(selectors, updateClass) { + selectors.walkClasses((sel) => { + sel.value = updateClass(sel.value) + + if (sel.raws && sel.raws.value) { + sel.raws.value = escapeCommas(sel.raws.value) + } + }) +} + +function resolveArbitraryValue(modifier, validate) { + if (!isArbitraryValue(modifier)) { + return undefined + } + + let value = modifier.slice(1, -1) + + if (!validate(value)) { + return undefined + } + + return normalize(value) +} + +function asNegativeValue(modifier, lookup = {}, validate) { + let positiveValue = lookup[modifier] + + if (positiveValue !== undefined) { + return negateValue(positiveValue) + } + + if (isArbitraryValue(modifier)) { + let resolved = resolveArbitraryValue(modifier, validate) + + if (resolved === undefined) { + return undefined + } + + return negateValue(resolved) + } +} + +export function asValue(modifier, options = {}, { validate = () => true } = {}) { + let value = options.values?.[modifier] + + if (value !== undefined) { + return value + } + + if (options.supportsNegativeValues && modifier.startsWith('-')) { + return asNegativeValue(modifier.slice(1), options.values, validate) + } + + return resolveArbitraryValue(modifier, validate) +} + +function isArbitraryValue(input) { + return input.startsWith('[') && input.endsWith(']') +} + +function splitUtilityModifier(modifier) { + let slashIdx = modifier.lastIndexOf('/') + + if (slashIdx === -1 || slashIdx === modifier.length - 1) { + return [modifier, undefined] + } + + let arbitrary = isArbitraryValue(modifier) + + // The modifier could be of the form `[foo]/[bar]` + // We want to handle this case properly + // without affecting `[foo/bar]` + if (arbitrary && !modifier.includes(']/[')) { + return [modifier, undefined] + } + + return [modifier.slice(0, slashIdx), modifier.slice(slashIdx + 1)] +} + +export function parseColorFormat(value) { + if (typeof value === 'string' && value.includes('<alpha-value>')) { + let oldValue = value + + return ({ opacityValue = 1 }) => oldValue.replace('<alpha-value>', opacityValue) + } + + return value +} + +function unwrapArbitraryModifier(modifier) { + return normalize(modifier.slice(1, -1)) +} + +export function asColor(modifier, options = {}, { tailwindConfig = {} } = {}) { + if (options.values?.[modifier] !== undefined) { + return parseColorFormat(options.values?.[modifier]) + } + + // TODO: Hoist this up to getMatchingTypes or something + // We do this here because we need the alpha value (if any) + let [color, alpha] = splitUtilityModifier(modifier) + + if (alpha !== undefined) { + let normalizedColor = + options.values?.[color] ?? (isArbitraryValue(color) ? color.slice(1, -1) : undefined) + + if (normalizedColor === undefined) { + return undefined + } + + normalizedColor = parseColorFormat(normalizedColor) + + if (isArbitraryValue(alpha)) { + return withAlphaValue(normalizedColor, unwrapArbitraryModifier(alpha)) + } + + if (tailwindConfig.theme?.opacity?.[alpha] === undefined) { + return undefined + } + + return withAlphaValue(normalizedColor, tailwindConfig.theme.opacity[alpha]) + } + + return asValue(modifier, options, { validate: validateColor }) +} + +export function asLookupValue(modifier, options = {}) { + return options.values?.[modifier] +} + +function guess(validate) { + return (modifier, options) => { + return asValue(modifier, options, { validate }) + } +} + +export let typeMap = { + any: asValue, + color: asColor, + url: guess(url), + image: guess(image), + length: guess(length), + percentage: guess(percentage), + position: guess(position), + lookup: asLookupValue, + 'generic-name': guess(genericName), + 'family-name': guess(familyName), + number: guess(number), + 'line-width': guess(lineWidth), + 'absolute-size': guess(absoluteSize), + 'relative-size': guess(relativeSize), + shadow: guess(shadow), + size: guess(backgroundSize), +} + +let supportedTypes = Object.keys(typeMap) + +function splitAtFirst(input, delim) { + let idx = input.indexOf(delim) + if (idx === -1) return [undefined, input] + return [input.slice(0, idx), input.slice(idx + 1)] +} + +export function coerceValue(types, modifier, options, tailwindConfig) { + if (options.values && modifier in options.values) { + for (let { type } of types ?? []) { + let result = typeMap[type](modifier, options, { + tailwindConfig, + }) + + if (result === undefined) { + continue + } + + return [result, type, null] + } + } + + if (isArbitraryValue(modifier)) { + let arbitraryValue = modifier.slice(1, -1) + let [explicitType, value] = splitAtFirst(arbitraryValue, ':') + + // It could be that this resolves to `url(https` which is not a valid + // identifier. We currently only support "simple" words with dashes or + // underscores. E.g.: family-name + if (!/^[\w-_]+$/g.test(explicitType)) { + value = arbitraryValue + } + + // + else if (explicitType !== undefined && !supportedTypes.includes(explicitType)) { + return [] + } + + if (value.length > 0 && supportedTypes.includes(explicitType)) { + return [asValue(`[${value}]`, options), explicitType, null] + } + } + + let matches = getMatchingTypes(types, modifier, options, tailwindConfig) + + // Find first matching type + for (let match of matches) { + return match + } + + return [] +} + +/** + * + * @param {{type: string}[]} types + * @param {string} rawModifier + * @param {any} options + * @param {any} tailwindConfig + * @returns {Iterator<[value: string, type: string, modifier: string | null]>} + */ +export function* getMatchingTypes(types, rawModifier, options, tailwindConfig) { + let modifiersEnabled = flagEnabled(tailwindConfig, 'generalizedModifiers') + + let [modifier, utilityModifier] = splitUtilityModifier(rawModifier) + + let canUseUtilityModifier = + modifiersEnabled && + options.modifiers != null && + (options.modifiers === 'any' || + (typeof options.modifiers === 'object' && + ((utilityModifier && isArbitraryValue(utilityModifier)) || + utilityModifier in options.modifiers))) + + if (!canUseUtilityModifier) { + modifier = rawModifier + utilityModifier = undefined + } + + if (utilityModifier !== undefined && modifier === '') { + modifier = 'DEFAULT' + } + + // Check the full value first + // TODO: Move to asValue… somehow + if (utilityModifier !== undefined) { + if (typeof options.modifiers === 'object') { + let configValue = options.modifiers?.[utilityModifier] ?? null + if (configValue !== null) { + utilityModifier = configValue + } else if (isArbitraryValue(utilityModifier)) { + utilityModifier = unwrapArbitraryModifier(utilityModifier) + } + } + } + + for (let { type } of types ?? []) { + let result = typeMap[type](modifier, options, { + tailwindConfig, + }) + + if (result === undefined) { + continue + } + + yield [result, type, utilityModifier ?? null] + } +} diff --git a/node_modules/tailwindcss/src/util/prefixSelector.js b/node_modules/tailwindcss/src/util/prefixSelector.js new file mode 100644 index 0000000..93cbeb9 --- /dev/null +++ b/node_modules/tailwindcss/src/util/prefixSelector.js @@ -0,0 +1,33 @@ +import parser from 'postcss-selector-parser' + +/** + * @template {string | import('postcss-selector-parser').Root} T + * + * Prefix all classes in the selector with the given prefix + * + * It can take either a string or a selector AST and will return the same type + * + * @param {string} prefix + * @param {T} selector + * @param {boolean} prependNegative + * @returns {T} + */ +export default function (prefix, selector, prependNegative = false) { + if (prefix === '') { + return selector + } + + /** @type {import('postcss-selector-parser').Root} */ + let ast = typeof selector === 'string' ? parser().astSync(selector) : selector + + ast.walkClasses((classSelector) => { + let baseClass = classSelector.value + let shouldPlaceNegativeBeforePrefix = prependNegative && baseClass.startsWith('-') + + classSelector.value = shouldPlaceNegativeBeforePrefix + ? `-${prefix}${baseClass.slice(1)}` + : `${prefix}${baseClass}` + }) + + return typeof selector === 'string' ? ast.toString() : ast +} diff --git a/node_modules/tailwindcss/src/util/pseudoElements.js b/node_modules/tailwindcss/src/util/pseudoElements.js new file mode 100644 index 0000000..5795cdd --- /dev/null +++ b/node_modules/tailwindcss/src/util/pseudoElements.js @@ -0,0 +1,167 @@ +/** @typedef {import('postcss-selector-parser').Root} Root */ +/** @typedef {import('postcss-selector-parser').Selector} Selector */ +/** @typedef {import('postcss-selector-parser').Pseudo} Pseudo */ +/** @typedef {import('postcss-selector-parser').Node} Node */ + +// There are some pseudo-elements that may or may not be: + +// **Actionable** +// Zero or more user-action pseudo-classes may be attached to the pseudo-element itself +// structural-pseudo-classes are NOT allowed but we don't make +// The spec is not clear on whether this is allowed or not — but in practice it is. + +// **Terminal** +// It MUST be placed at the end of a selector +// +// This is the required in the spec. However, some pseudo elements are not "terminal" because +// they represent a "boundary piercing" that is compiled out by a build step. + +// **Jumpable** +// Any terminal element may "jump" over combinators when moving to the end of the selector +// +// This is a backwards-compat quirk of pseudo element variants from earlier versions of Tailwind CSS. + +/** @typedef {'terminal' | 'actionable' | 'jumpable'} PseudoProperty */ + +/** @type {Record<string, PseudoProperty[]>} */ +let elementProperties = { + // Pseudo elements from the spec + '::after': ['terminal', 'jumpable'], + '::backdrop': ['terminal', 'jumpable'], + '::before': ['terminal', 'jumpable'], + '::cue': ['terminal'], + '::cue-region': ['terminal'], + '::first-letter': ['terminal', 'jumpable'], + '::first-line': ['terminal', 'jumpable'], + '::grammar-error': ['terminal'], + '::marker': ['terminal', 'jumpable'], + '::part': ['terminal', 'actionable'], + '::placeholder': ['terminal', 'jumpable'], + '::selection': ['terminal', 'jumpable'], + '::slotted': ['terminal'], + '::spelling-error': ['terminal'], + '::target-text': ['terminal'], + + // Pseudo elements from the spec with special rules + '::file-selector-button': ['terminal', 'actionable'], + + // Library-specific pseudo elements used by component libraries + // These are Shadow DOM-like + '::deep': ['actionable'], + '::v-deep': ['actionable'], + '::ng-deep': ['actionable'], + + // Note: As a rule, double colons (::) should be used instead of a single colon + // (:). This distinguishes pseudo-classes from pseudo-elements. However, since + // this distinction was not present in older versions of the W3C spec, most + // browsers support both syntaxes for the original pseudo-elements. + ':after': ['terminal', 'jumpable'], + ':before': ['terminal', 'jumpable'], + ':first-letter': ['terminal', 'jumpable'], + ':first-line': ['terminal', 'jumpable'], + + // The default value is used when the pseudo-element is not recognized + // Because it's not recognized, we don't know if it's terminal or not + // So we assume it can be moved AND can have user-action pseudo classes attached to it + __default__: ['terminal', 'actionable'], +} + +/** + * @param {Selector} sel + * @returns {Selector} + */ +export function movePseudos(sel) { + let [pseudos] = movablePseudos(sel) + + // Remove all pseudo elements from their respective selectors + pseudos.forEach(([sel, pseudo]) => sel.removeChild(pseudo)) + + // Re-add them to the end of the selector in the correct order. + // This moves terminal pseudo elements to the end of the + // selector otherwise the selector will not be valid. + // + // Examples: + // - `before:hover:text-center` would result in `.before\:hover\:text-center:hover::before` + // - `hover:before:text-center` would result in `.hover\:before\:text-center:hover::before` + // + // The selector `::before:hover` does not work but we + // can make it work for you by flipping the order. + sel.nodes.push(...pseudos.map(([, pseudo]) => pseudo)) + + return sel +} + +/** @typedef {[sel: Selector, pseudo: Pseudo, attachedTo: Pseudo | null]} MovablePseudo */ +/** @typedef {[pseudos: MovablePseudo[], lastSeenElement: Pseudo | null]} MovablePseudosResult */ + +/** + * @param {Selector} sel + * @returns {MovablePseudosResult} + */ +function movablePseudos(sel) { + /** @type {MovablePseudo[]} */ + let buffer = [] + + /** @type {Pseudo | null} */ + let lastSeenElement = null + + for (let node of sel.nodes) { + if (node.type === 'combinator') { + buffer = buffer.filter(([, node]) => propertiesForPseudo(node).includes('jumpable')) + lastSeenElement = null + } else if (node.type === 'pseudo') { + if (isMovablePseudoElement(node)) { + lastSeenElement = node + buffer.push([sel, node, null]) + } else if (lastSeenElement && isAttachablePseudoClass(node, lastSeenElement)) { + buffer.push([sel, node, lastSeenElement]) + } else { + lastSeenElement = null + } + + for (let sub of node.nodes ?? []) { + let [movable, lastSeenElementInSub] = movablePseudos(sub) + lastSeenElement = lastSeenElementInSub || lastSeenElement + buffer.push(...movable) + } + } + } + + return [buffer, lastSeenElement] +} + +/** + * @param {Node} node + * @returns {boolean} + */ +function isPseudoElement(node) { + return node.value.startsWith('::') || elementProperties[node.value] !== undefined +} + +/** + * @param {Node} node + * @returns {boolean} + */ +function isMovablePseudoElement(node) { + return isPseudoElement(node) && propertiesForPseudo(node).includes('terminal') +} + +/** + * @param {Node} node + * @param {Pseudo} pseudo + * @returns {boolean} + */ +function isAttachablePseudoClass(node, pseudo) { + if (node.type !== 'pseudo') return false + if (isPseudoElement(node)) return false + + return propertiesForPseudo(pseudo).includes('actionable') +} + +/** + * @param {Pseudo} pseudo + * @returns {PseudoProperty[]} + */ +function propertiesForPseudo(pseudo) { + return elementProperties[pseudo.value] ?? elementProperties.__default__ +} diff --git a/node_modules/tailwindcss/src/util/removeAlphaVariables.js b/node_modules/tailwindcss/src/util/removeAlphaVariables.js new file mode 100644 index 0000000..76655be --- /dev/null +++ b/node_modules/tailwindcss/src/util/removeAlphaVariables.js @@ -0,0 +1,24 @@ +/** + * This function removes any uses of CSS variables used as an alpha channel + * + * This is required for selectors like `:visited` which do not allow + * changes in opacity or external control using CSS variables. + * + * @param {import('postcss').Container} container + * @param {string[]} toRemove + */ +export function removeAlphaVariables(container, toRemove) { + container.walkDecls((decl) => { + if (toRemove.includes(decl.prop)) { + decl.remove() + + return + } + + for (let varName of toRemove) { + if (decl.value.includes(`/ var(${varName})`)) { + decl.value = decl.value.replace(`/ var(${varName})`, '') + } + } + }) +} diff --git a/node_modules/tailwindcss/src/util/resolveConfig.js b/node_modules/tailwindcss/src/util/resolveConfig.js new file mode 100644 index 0000000..cfeda6e --- /dev/null +++ b/node_modules/tailwindcss/src/util/resolveConfig.js @@ -0,0 +1,277 @@ +import negateValue from './negateValue' +import corePluginList from '../corePluginList' +import configurePlugins from './configurePlugins' +import colors from '../public/colors' +import { defaults } from './defaults' +import { toPath } from './toPath' +import { normalizeConfig } from './normalizeConfig' +import isPlainObject from './isPlainObject' +import { cloneDeep } from './cloneDeep' +import { parseColorFormat } from './pluginUtils' +import { withAlphaValue } from './withAlphaVariable' +import toColorValue from './toColorValue' + +function isFunction(input) { + return typeof input === 'function' +} + +function mergeWith(target, ...sources) { + let customizer = sources.pop() + + for (let source of sources) { + for (let k in source) { + let merged = customizer(target[k], source[k]) + + if (merged === undefined) { + if (isPlainObject(target[k]) && isPlainObject(source[k])) { + target[k] = mergeWith({}, target[k], source[k], customizer) + } else { + target[k] = source[k] + } + } else { + target[k] = merged + } + } + } + + return target +} + +const configUtils = { + colors, + negative(scale) { + // TODO: Log that this function isn't really needed anymore? + return Object.keys(scale) + .filter((key) => scale[key] !== '0') + .reduce((negativeScale, key) => { + let negativeValue = negateValue(scale[key]) + + if (negativeValue !== undefined) { + negativeScale[`-${key}`] = negativeValue + } + + return negativeScale + }, {}) + }, + breakpoints(screens) { + return Object.keys(screens) + .filter((key) => typeof screens[key] === 'string') + .reduce( + (breakpoints, key) => ({ + ...breakpoints, + [`screen-${key}`]: screens[key], + }), + {} + ) + }, +} + +function value(valueToResolve, ...args) { + return isFunction(valueToResolve) ? valueToResolve(...args) : valueToResolve +} + +function collectExtends(items) { + return items.reduce((merged, { extend }) => { + return mergeWith(merged, extend, (mergedValue, extendValue) => { + if (mergedValue === undefined) { + return [extendValue] + } + + if (Array.isArray(mergedValue)) { + return [extendValue, ...mergedValue] + } + + return [extendValue, mergedValue] + }) + }, {}) +} + +function mergeThemes(themes) { + return { + ...themes.reduce((merged, theme) => defaults(merged, theme), {}), + + // In order to resolve n config objects, we combine all of their `extend` properties + // into arrays instead of objects so they aren't overridden. + extend: collectExtends(themes), + } +} + +function mergeExtensionCustomizer(merged, value) { + // When we have an array of objects, we do want to merge it + if (Array.isArray(merged) && isPlainObject(merged[0])) { + return merged.concat(value) + } + + // When the incoming value is an array, and the existing config is an object, prepend the existing object + if (Array.isArray(value) && isPlainObject(value[0]) && isPlainObject(merged)) { + return [merged, ...value] + } + + // Override arrays (for example for font-families, box-shadows, ...) + if (Array.isArray(value)) { + return value + } + + // Execute default behaviour + return undefined +} + +function mergeExtensions({ extend, ...theme }) { + return mergeWith(theme, extend, (themeValue, extensions) => { + // The `extend` property is an array, so we need to check if it contains any functions + if (!isFunction(themeValue) && !extensions.some(isFunction)) { + return mergeWith({}, themeValue, ...extensions, mergeExtensionCustomizer) + } + + return (resolveThemePath, utils) => + mergeWith( + {}, + ...[themeValue, ...extensions].map((e) => value(e, resolveThemePath, utils)), + mergeExtensionCustomizer + ) + }) +} + +/** + * + * @param {string} key + * @return {Iterable<string[] & {alpha: string | undefined}>} + */ +function* toPaths(key) { + let path = toPath(key) + + if (path.length === 0) { + return + } + + yield path + + if (Array.isArray(key)) { + return + } + + let pattern = /^(.*?)\s*\/\s*([^/]+)$/ + let matches = key.match(pattern) + + if (matches !== null) { + let [, prefix, alpha] = matches + + let newPath = toPath(prefix) + newPath.alpha = alpha + + yield newPath + } +} + +function resolveFunctionKeys(object) { + // theme('colors.red.500 / 0.5') -> ['colors', 'red', '500 / 0', '5] + + const resolvePath = (key, defaultValue) => { + for (const path of toPaths(key)) { + let index = 0 + let val = object + + while (val !== undefined && val !== null && index < path.length) { + val = val[path[index++]] + + let shouldResolveAsFn = + isFunction(val) && (path.alpha === undefined || index <= path.length - 1) + + val = shouldResolveAsFn ? val(resolvePath, configUtils) : val + } + + if (val !== undefined) { + if (path.alpha !== undefined) { + let normalized = parseColorFormat(val) + + return withAlphaValue(normalized, path.alpha, toColorValue(normalized)) + } + + if (isPlainObject(val)) { + return cloneDeep(val) + } + + return val + } + } + + return defaultValue + } + + Object.assign(resolvePath, { + theme: resolvePath, + ...configUtils, + }) + + return Object.keys(object).reduce((resolved, key) => { + resolved[key] = isFunction(object[key]) ? object[key](resolvePath, configUtils) : object[key] + + return resolved + }, {}) +} + +function extractPluginConfigs(configs) { + let allConfigs = [] + + configs.forEach((config) => { + allConfigs = [...allConfigs, config] + + const plugins = config?.plugins ?? [] + + if (plugins.length === 0) { + return + } + + plugins.forEach((plugin) => { + if (plugin.__isOptionsFunction) { + plugin = plugin() + } + allConfigs = [...allConfigs, ...extractPluginConfigs([plugin?.config ?? {}])] + }) + }) + + return allConfigs +} + +function resolveCorePlugins(corePluginConfigs) { + const result = [...corePluginConfigs].reduceRight((resolved, corePluginConfig) => { + if (isFunction(corePluginConfig)) { + return corePluginConfig({ corePlugins: resolved }) + } + return configurePlugins(corePluginConfig, resolved) + }, corePluginList) + + return result +} + +function resolvePluginLists(pluginLists) { + const result = [...pluginLists].reduceRight((resolved, pluginList) => { + return [...resolved, ...pluginList] + }, []) + + return result +} + +export default function resolveConfig(configs) { + let allConfigs = [ + ...extractPluginConfigs(configs), + { + prefix: '', + important: false, + separator: ':', + }, + ] + + return normalizeConfig( + defaults( + { + theme: resolveFunctionKeys( + mergeExtensions(mergeThemes(allConfigs.map((t) => t?.theme ?? {}))) + ), + corePlugins: resolveCorePlugins(allConfigs.map((c) => c.corePlugins)), + plugins: resolvePluginLists(configs.map((c) => c?.plugins ?? [])), + }, + ...allConfigs + ) + ) +} diff --git a/node_modules/tailwindcss/src/util/resolveConfigPath.js b/node_modules/tailwindcss/src/util/resolveConfigPath.js new file mode 100644 index 0000000..2b50789 --- /dev/null +++ b/node_modules/tailwindcss/src/util/resolveConfigPath.js @@ -0,0 +1,66 @@ +import fs from 'fs' +import path from 'path' + +const defaultConfigFiles = [ + './tailwind.config.js', + './tailwind.config.cjs', + './tailwind.config.mjs', + './tailwind.config.ts', +] + +function isObject(value) { + return typeof value === 'object' && value !== null +} + +function isEmpty(obj) { + return Object.keys(obj).length === 0 +} + +function isString(value) { + return typeof value === 'string' || value instanceof String +} + +export default function resolveConfigPath(pathOrConfig) { + // require('tailwindcss')({ theme: ..., variants: ... }) + if (isObject(pathOrConfig) && pathOrConfig.config === undefined && !isEmpty(pathOrConfig)) { + return null + } + + // require('tailwindcss')({ config: 'custom-config.js' }) + if ( + isObject(pathOrConfig) && + pathOrConfig.config !== undefined && + isString(pathOrConfig.config) + ) { + return path.resolve(pathOrConfig.config) + } + + // require('tailwindcss')({ config: { theme: ..., variants: ... } }) + if ( + isObject(pathOrConfig) && + pathOrConfig.config !== undefined && + isObject(pathOrConfig.config) + ) { + return null + } + + // require('tailwindcss')('custom-config.js') + if (isString(pathOrConfig)) { + return path.resolve(pathOrConfig) + } + + // require('tailwindcss') + return resolveDefaultConfigPath() +} + +export function resolveDefaultConfigPath() { + for (const configFile of defaultConfigFiles) { + try { + const configPath = path.resolve(configFile) + fs.accessSync(configPath) + return configPath + } catch (err) {} + } + + return null +} diff --git a/node_modules/tailwindcss/src/util/responsive.js b/node_modules/tailwindcss/src/util/responsive.js new file mode 100644 index 0000000..29bf9e9 --- /dev/null +++ b/node_modules/tailwindcss/src/util/responsive.js @@ -0,0 +1,10 @@ +import postcss from 'postcss' +import cloneNodes from './cloneNodes' + +export default function responsive(rules) { + return postcss + .atRule({ + name: 'responsive', + }) + .append(cloneNodes(Array.isArray(rules) ? rules : [rules])) +} diff --git a/node_modules/tailwindcss/src/util/splitAtTopLevelOnly.js b/node_modules/tailwindcss/src/util/splitAtTopLevelOnly.js new file mode 100644 index 0000000..a749c79 --- /dev/null +++ b/node_modules/tailwindcss/src/util/splitAtTopLevelOnly.js @@ -0,0 +1,52 @@ +/** + * This splits a string on a top-level character. + * + * Regex doesn't support recursion (at least not the JS-flavored version). + * So we have to use a tiny state machine to keep track of paren placement. + * + * Expected behavior using commas: + * var(--a, 0 0 1px rgb(0, 0, 0)), 0 0 1px rgb(0, 0, 0) + * ─┬─ ┬ ┬ ┬ + * x x x ╰──────── Split because top-level + * ╰──────────────┴──┴───────────── Ignored b/c inside >= 1 levels of parens + * + * @param {string} input + * @param {string} separator + */ +export function splitAtTopLevelOnly(input, separator) { + let stack = [] + let parts = [] + let lastPos = 0 + let isEscaped = false + + for (let idx = 0; idx < input.length; idx++) { + let char = input[idx] + + if (stack.length === 0 && char === separator[0] && !isEscaped) { + if (separator.length === 1 || input.slice(idx, idx + separator.length) === separator) { + parts.push(input.slice(lastPos, idx)) + lastPos = idx + separator.length + } + } + + if (isEscaped) { + isEscaped = false + } else if (char === '\\') { + isEscaped = true + } + + if (char === '(' || char === '[' || char === '{') { + stack.push(char) + } else if ( + (char === ')' && stack[stack.length - 1] === '(') || + (char === ']' && stack[stack.length - 1] === '[') || + (char === '}' && stack[stack.length - 1] === '{') + ) { + stack.pop() + } + } + + parts.push(input.slice(lastPos)) + + return parts +} diff --git a/node_modules/tailwindcss/src/util/tap.js b/node_modules/tailwindcss/src/util/tap.js new file mode 100644 index 0000000..0590e4b --- /dev/null +++ b/node_modules/tailwindcss/src/util/tap.js @@ -0,0 +1,4 @@ +export function tap(value, mutator) { + mutator(value) + return value +} diff --git a/node_modules/tailwindcss/src/util/toColorValue.js b/node_modules/tailwindcss/src/util/toColorValue.js new file mode 100644 index 0000000..288d907 --- /dev/null +++ b/node_modules/tailwindcss/src/util/toColorValue.js @@ -0,0 +1,3 @@ +export default function toColorValue(maybeFunction) { + return typeof maybeFunction === 'function' ? maybeFunction({}) : maybeFunction +} diff --git a/node_modules/tailwindcss/src/util/toPath.js b/node_modules/tailwindcss/src/util/toPath.js new file mode 100644 index 0000000..6dce924 --- /dev/null +++ b/node_modules/tailwindcss/src/util/toPath.js @@ -0,0 +1,26 @@ +/** + * Parse a path string into an array of path segments. + * + * Square bracket notation `a[b]` may be used to "escape" dots that would otherwise be interpreted as path separators. + * + * Example: + * a -> ['a'] + * a.b.c -> ['a', 'b', 'c'] + * a[b].c -> ['a', 'b', 'c'] + * a[b.c].e.f -> ['a', 'b.c', 'e', 'f'] + * a[b][c][d] -> ['a', 'b', 'c', 'd'] + * + * @param {string|string[]} path + **/ +export function toPath(path) { + if (Array.isArray(path)) return path + + let openBrackets = path.split('[').length - 1 + let closedBrackets = path.split(']').length - 1 + + if (openBrackets !== closedBrackets) { + throw new Error(`Path is invalid. Has unbalanced brackets: ${path}`) + } + + return path.split(/\.(?![^\[]*\])|[\[\]]/g).filter(Boolean) +} diff --git a/node_modules/tailwindcss/src/util/transformThemeValue.js b/node_modules/tailwindcss/src/util/transformThemeValue.js new file mode 100644 index 0000000..2469612 --- /dev/null +++ b/node_modules/tailwindcss/src/util/transformThemeValue.js @@ -0,0 +1,62 @@ +import postcss from 'postcss' +import isPlainObject from './isPlainObject' + +export default function transformThemeValue(themeSection) { + if (['fontSize', 'outline'].includes(themeSection)) { + return (value) => { + if (typeof value === 'function') value = value({}) + if (Array.isArray(value)) value = value[0] + + return value + } + } + + if (themeSection === 'fontFamily') { + return (value) => { + if (typeof value === 'function') value = value({}) + let families = Array.isArray(value) && isPlainObject(value[1]) ? value[0] : value + return Array.isArray(families) ? families.join(', ') : families + } + } + + if ( + [ + 'boxShadow', + 'transitionProperty', + 'transitionDuration', + 'transitionDelay', + 'transitionTimingFunction', + 'backgroundImage', + 'backgroundSize', + 'backgroundColor', + 'cursor', + 'animation', + ].includes(themeSection) + ) { + return (value) => { + if (typeof value === 'function') value = value({}) + if (Array.isArray(value)) value = value.join(', ') + + return value + } + } + + // For backwards compatibility reasons, before we switched to underscores + // instead of commas for arbitrary values. + if (['gridTemplateColumns', 'gridTemplateRows', 'objectPosition'].includes(themeSection)) { + return (value) => { + if (typeof value === 'function') value = value({}) + if (typeof value === 'string') value = postcss.list.comma(value).join(' ') + + return value + } + } + + return (value, opts = {}) => { + if (typeof value === 'function') { + value = value(opts) + } + + return value + } +} diff --git a/node_modules/tailwindcss/src/util/validateConfig.js b/node_modules/tailwindcss/src/util/validateConfig.js new file mode 100644 index 0000000..8c22e44 --- /dev/null +++ b/node_modules/tailwindcss/src/util/validateConfig.js @@ -0,0 +1,26 @@ +import log from './log' + +export function validateConfig(config) { + if (config.content.files.length === 0) { + log.warn('content-problems', [ + 'The `content` option in your Tailwind CSS configuration is missing or empty.', + 'Configure your content sources or your generated CSS will be missing styles.', + 'https://tailwindcss.com/docs/content-configuration', + ]) + } + + // Warn if the line-clamp plugin is installed + try { + let plugin = require('@tailwindcss/line-clamp') + if (config.plugins.includes(plugin)) { + log.warn('line-clamp-in-core', [ + 'As of Tailwind CSS v3.3, the `@tailwindcss/line-clamp` plugin is now included by default.', + 'Remove it from the `plugins` array in your configuration to eliminate this warning.', + ]) + + config.plugins = config.plugins.filter((p) => p !== plugin) + } + } catch {} + + return config +} diff --git a/node_modules/tailwindcss/src/util/validateFormalSyntax.js b/node_modules/tailwindcss/src/util/validateFormalSyntax.js new file mode 100644 index 0000000..d3dafea --- /dev/null +++ b/node_modules/tailwindcss/src/util/validateFormalSyntax.js @@ -0,0 +1,34 @@ +import { length, percentage } from './dataTypes' +import { splitAtTopLevelOnly } from './splitAtTopLevelOnly' + +/** + * + * https://developer.mozilla.org/en-US/docs/Web/CSS/background-size#formal_syntax + * + * background-size = + * <bg-size># + * + * <bg-size> = + * [ <length-percentage [0,∞]> | auto ]{1,2} | + * cover | + * contain + * + * <length-percentage> = + * <length> | + * <percentage> + * + * @param {string} value + */ +export function backgroundSize(value) { + let keywordValues = ['cover', 'contain'] + // the <length-percentage> type will probably be a css function + // so we have to use `splitAtTopLevelOnly` + return splitAtTopLevelOnly(value, ',').every((part) => { + let sizes = splitAtTopLevelOnly(part, '_').filter(Boolean) + if (sizes.length === 1 && keywordValues.includes(sizes[0])) return true + + if (sizes.length !== 1 && sizes.length !== 2) return false + + return sizes.every((size) => length(size) || percentage(size) || size === 'auto') + }) +} diff --git a/node_modules/tailwindcss/src/util/withAlphaVariable.js b/node_modules/tailwindcss/src/util/withAlphaVariable.js new file mode 100644 index 0000000..15aedb7 --- /dev/null +++ b/node_modules/tailwindcss/src/util/withAlphaVariable.js @@ -0,0 +1,49 @@ +import { parseColor, formatColor } from './color' + +export function withAlphaValue(color, alphaValue, defaultValue) { + if (typeof color === 'function') { + return color({ opacityValue: alphaValue }) + } + + let parsed = parseColor(color, { loose: true }) + + if (parsed === null) { + return defaultValue + } + + return formatColor({ ...parsed, alpha: alphaValue }) +} + +export default function withAlphaVariable({ color, property, variable }) { + let properties = [].concat(property) + if (typeof color === 'function') { + return { + [variable]: '1', + ...Object.fromEntries( + properties.map((p) => { + return [p, color({ opacityVariable: variable, opacityValue: `var(${variable})` })] + }) + ), + } + } + + const parsed = parseColor(color) + + if (parsed === null) { + return Object.fromEntries(properties.map((p) => [p, color])) + } + + if (parsed.alpha !== undefined) { + // Has an alpha value, return color as-is + return Object.fromEntries(properties.map((p) => [p, color])) + } + + return { + [variable]: '1', + ...Object.fromEntries( + properties.map((p) => { + return [p, formatColor({ ...parsed, alpha: `var(${variable})` })] + }) + ), + } +} diff --git a/node_modules/tailwindcss/src/value-parser/LICENSE b/node_modules/tailwindcss/src/value-parser/LICENSE new file mode 100644 index 0000000..6dcaefc --- /dev/null +++ b/node_modules/tailwindcss/src/value-parser/LICENSE @@ -0,0 +1,22 @@ +Copyright (c) Bogdan Chadkin <trysound@yandex.ru> + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. diff --git a/node_modules/tailwindcss/src/value-parser/README.md b/node_modules/tailwindcss/src/value-parser/README.md new file mode 100644 index 0000000..ea9e202 --- /dev/null +++ b/node_modules/tailwindcss/src/value-parser/README.md @@ -0,0 +1,3 @@ +# postcss-value-parser (forked + inlined) + +This is a customized version of of [PostCSS Value Parser](https://github.com/TrySound/postcss-value-parser) to fix some bugs around parsing CSS functions. diff --git a/node_modules/tailwindcss/src/value-parser/index.d.ts b/node_modules/tailwindcss/src/value-parser/index.d.ts new file mode 100644 index 0000000..0c0c4b9 --- /dev/null +++ b/node_modules/tailwindcss/src/value-parser/index.d.ts @@ -0,0 +1,177 @@ +declare namespace postcssValueParser { + interface BaseNode { + /** + * The offset, inclusive, inside the CSS value at which the node starts. + */ + sourceIndex: number + + /** + * The offset, exclusive, inside the CSS value at which the node ends. + */ + sourceEndIndex: number + + /** + * The node's characteristic value + */ + value: string + } + + interface ClosableNode { + /** + * Whether the parsed CSS value ended before the node was properly closed + */ + unclosed?: true + } + + interface AdjacentAwareNode { + /** + * The token at the start of the node + */ + before: string + + /** + * The token at the end of the node + */ + after: string + } + + interface CommentNode extends BaseNode, ClosableNode { + type: 'comment' + } + + interface DivNode extends BaseNode, AdjacentAwareNode { + type: 'div' + } + + interface FunctionNode extends BaseNode, ClosableNode, AdjacentAwareNode { + type: 'function' + + /** + * Nodes inside the function + */ + nodes: Node[] + } + + interface SpaceNode extends BaseNode { + type: 'space' + } + + interface StringNode extends BaseNode, ClosableNode { + type: 'string' + + /** + * The quote type delimiting the string + */ + quote: '"' | "'" + } + + interface UnicodeRangeNode extends BaseNode { + type: 'unicode-range' + } + + interface WordNode extends BaseNode { + type: 'word' + } + + /** + * Any node parsed from a CSS value + */ + type Node = + | CommentNode + | DivNode + | FunctionNode + | SpaceNode + | StringNode + | UnicodeRangeNode + | WordNode + + interface CustomStringifierCallback { + /** + * @param node The node to stringify + * @returns The serialized CSS representation of the node + */ + (nodes: Node): string | undefined + } + + interface WalkCallback { + /** + * @param node The currently visited node + * @param index The index of the node in the series of parsed nodes + * @param nodes The series of parsed nodes + * @returns Returning `false` will prevent traversal of descendant nodes (only applies if `bubble` was set to `true` in the `walk()` call) + */ + (node: Node, index: number, nodes: Node[]): void | boolean + } + + /** + * A CSS dimension, decomposed into its numeric and unit parts + */ + interface Dimension { + number: string + unit: string + } + + /** + * A wrapper around a parsed CSS value that allows for inspecting and walking nodes + */ + interface ParsedValue { + /** + * The series of parsed nodes + */ + nodes: Node[] + + /** + * Walk all parsed nodes, applying a callback + * + * @param callback A visitor callback that will be executed for each node + * @param bubble When set to `true`, walking will be done inside-out instead of outside-in + */ + walk(callback: WalkCallback, bubble?: boolean): this + } + + interface ValueParser { + /** + * Decompose a CSS dimension into its numeric and unit part + * + * @param value The dimension to decompose + * @returns An object representing `number` and `unit` part of the dimension or `false` if the decomposing fails + */ + unit(value: string): Dimension | false + + /** + * Serialize a series of nodes into a CSS value + * + * @param nodes The nodes to stringify + * @param custom A custom stringifier callback + * @returns The generated CSS value + */ + stringify(nodes: Node | Node[], custom?: CustomStringifierCallback): string + + /** + * Walk a series of nodes, applying a callback + * + * @param nodes The nodes to walk + * @param callback A visitor callback that will be executed for each node + * @param bubble When set to `true`, walking will be done inside-out instead of outside-in + */ + walk(nodes: Node[], callback: WalkCallback, bubble?: boolean): void + + /** + * Parse a CSS value into a series of nodes to operate on + * + * @param value The value to parse + */ + new (value: string): ParsedValue + + /** + * Parse a CSS value into a series of nodes to operate on + * + * @param value The value to parse + */ + (value: string): ParsedValue + } +} + +declare const postcssValueParser: postcssValueParser.ValueParser + +export = postcssValueParser diff --git a/node_modules/tailwindcss/src/value-parser/index.js b/node_modules/tailwindcss/src/value-parser/index.js new file mode 100644 index 0000000..5587ccf --- /dev/null +++ b/node_modules/tailwindcss/src/value-parser/index.js @@ -0,0 +1,28 @@ +var parse = require('./parse') +var walk = require('./walk') +var stringify = require('./stringify') + +function ValueParser(value) { + if (this instanceof ValueParser) { + this.nodes = parse(value) + return this + } + return new ValueParser(value) +} + +ValueParser.prototype.toString = function () { + return Array.isArray(this.nodes) ? stringify(this.nodes) : '' +} + +ValueParser.prototype.walk = function (cb, bubble) { + walk(this.nodes, cb, bubble) + return this +} + +ValueParser.unit = require('./unit') + +ValueParser.walk = walk + +ValueParser.stringify = stringify + +module.exports = ValueParser diff --git a/node_modules/tailwindcss/src/value-parser/parse.js b/node_modules/tailwindcss/src/value-parser/parse.js new file mode 100644 index 0000000..4455996 --- /dev/null +++ b/node_modules/tailwindcss/src/value-parser/parse.js @@ -0,0 +1,303 @@ +var openParentheses = '('.charCodeAt(0) +var closeParentheses = ')'.charCodeAt(0) +var singleQuote = "'".charCodeAt(0) +var doubleQuote = '"'.charCodeAt(0) +var backslash = '\\'.charCodeAt(0) +var slash = '/'.charCodeAt(0) +var comma = ','.charCodeAt(0) +var colon = ':'.charCodeAt(0) +var star = '*'.charCodeAt(0) +var uLower = 'u'.charCodeAt(0) +var uUpper = 'U'.charCodeAt(0) +var plus = '+'.charCodeAt(0) +var isUnicodeRange = /^[a-f0-9?-]+$/i + +module.exports = function (input) { + var tokens = [] + var value = input + + var next, quote, prev, token, escape, escapePos, whitespacePos, parenthesesOpenPos + var pos = 0 + var code = value.charCodeAt(pos) + var max = value.length + var stack = [{ nodes: tokens }] + var balanced = 0 + var parent + + var name = '' + var before = '' + var after = '' + + while (pos < max) { + // Whitespaces + if (code <= 32) { + next = pos + do { + next += 1 + code = value.charCodeAt(next) + } while (code <= 32) + token = value.slice(pos, next) + + prev = tokens[tokens.length - 1] + if (code === closeParentheses && balanced) { + after = token + } else if (prev && prev.type === 'div') { + prev.after = token + prev.sourceEndIndex += token.length + } else if ( + code === comma || + code === colon || + (code === slash && + value.charCodeAt(next + 1) !== star && + (!parent || (parent && parent.type === 'function' && false))) + ) { + before = token + } else { + tokens.push({ + type: 'space', + sourceIndex: pos, + sourceEndIndex: next, + value: token, + }) + } + + pos = next + + // Quotes + } else if (code === singleQuote || code === doubleQuote) { + next = pos + quote = code === singleQuote ? "'" : '"' + token = { + type: 'string', + sourceIndex: pos, + quote: quote, + } + do { + escape = false + next = value.indexOf(quote, next + 1) + if (~next) { + escapePos = next + while (value.charCodeAt(escapePos - 1) === backslash) { + escapePos -= 1 + escape = !escape + } + } else { + value += quote + next = value.length - 1 + token.unclosed = true + } + } while (escape) + token.value = value.slice(pos + 1, next) + token.sourceEndIndex = token.unclosed ? next : next + 1 + tokens.push(token) + pos = next + 1 + code = value.charCodeAt(pos) + + // Comments + } else if (code === slash && value.charCodeAt(pos + 1) === star) { + next = value.indexOf('*/', pos) + + token = { + type: 'comment', + sourceIndex: pos, + sourceEndIndex: next + 2, + } + + if (next === -1) { + token.unclosed = true + next = value.length + token.sourceEndIndex = next + } + + token.value = value.slice(pos + 2, next) + tokens.push(token) + + pos = next + 2 + code = value.charCodeAt(pos) + + // Operation within calc + } else if ((code === slash || code === star) && parent && parent.type === 'function' && true) { + token = value[pos] + tokens.push({ + type: 'word', + sourceIndex: pos - before.length, + sourceEndIndex: pos + token.length, + value: token, + }) + pos += 1 + code = value.charCodeAt(pos) + + // Dividers + } else if (code === slash || code === comma || code === colon) { + token = value[pos] + + tokens.push({ + type: 'div', + sourceIndex: pos - before.length, + sourceEndIndex: pos + token.length, + value: token, + before: before, + after: '', + }) + before = '' + + pos += 1 + code = value.charCodeAt(pos) + + // Open parentheses + } else if (openParentheses === code) { + // Whitespaces after open parentheses + next = pos + do { + next += 1 + code = value.charCodeAt(next) + } while (code <= 32) + parenthesesOpenPos = pos + token = { + type: 'function', + sourceIndex: pos - name.length, + value: name, + before: value.slice(parenthesesOpenPos + 1, next), + } + pos = next + + if (name === 'url' && code !== singleQuote && code !== doubleQuote) { + next -= 1 + do { + escape = false + next = value.indexOf(')', next + 1) + if (~next) { + escapePos = next + while (value.charCodeAt(escapePos - 1) === backslash) { + escapePos -= 1 + escape = !escape + } + } else { + value += ')' + next = value.length - 1 + token.unclosed = true + } + } while (escape) + // Whitespaces before closed + whitespacePos = next + do { + whitespacePos -= 1 + code = value.charCodeAt(whitespacePos) + } while (code <= 32) + if (parenthesesOpenPos < whitespacePos) { + if (pos !== whitespacePos + 1) { + token.nodes = [ + { + type: 'word', + sourceIndex: pos, + sourceEndIndex: whitespacePos + 1, + value: value.slice(pos, whitespacePos + 1), + }, + ] + } else { + token.nodes = [] + } + if (token.unclosed && whitespacePos + 1 !== next) { + token.after = '' + token.nodes.push({ + type: 'space', + sourceIndex: whitespacePos + 1, + sourceEndIndex: next, + value: value.slice(whitespacePos + 1, next), + }) + } else { + token.after = value.slice(whitespacePos + 1, next) + token.sourceEndIndex = next + } + } else { + token.after = '' + token.nodes = [] + } + pos = next + 1 + token.sourceEndIndex = token.unclosed ? next : pos + code = value.charCodeAt(pos) + tokens.push(token) + } else { + balanced += 1 + token.after = '' + token.sourceEndIndex = pos + 1 + tokens.push(token) + stack.push(token) + tokens = token.nodes = [] + parent = token + } + name = '' + + // Close parentheses + } else if (closeParentheses === code && balanced) { + pos += 1 + code = value.charCodeAt(pos) + + parent.after = after + parent.sourceEndIndex += after.length + after = '' + balanced -= 1 + stack[stack.length - 1].sourceEndIndex = pos + stack.pop() + parent = stack[balanced] + tokens = parent.nodes + + // Words + } else { + next = pos + do { + if (code === backslash) { + next += 1 + } + next += 1 + code = value.charCodeAt(next) + } while ( + next < max && + !( + code <= 32 || + code === singleQuote || + code === doubleQuote || + code === comma || + code === colon || + code === slash || + code === openParentheses || + (code === star && parent && parent.type === 'function' && true) || + (code === slash && parent.type === 'function' && true) || + (code === closeParentheses && balanced) + ) + ) + token = value.slice(pos, next) + + if (openParentheses === code) { + name = token + } else if ( + (uLower === token.charCodeAt(0) || uUpper === token.charCodeAt(0)) && + plus === token.charCodeAt(1) && + isUnicodeRange.test(token.slice(2)) + ) { + tokens.push({ + type: 'unicode-range', + sourceIndex: pos, + sourceEndIndex: next, + value: token, + }) + } else { + tokens.push({ + type: 'word', + sourceIndex: pos, + sourceEndIndex: next, + value: token, + }) + } + + pos = next + } + } + + for (pos = stack.length - 1; pos; pos -= 1) { + stack[pos].unclosed = true + stack[pos].sourceEndIndex = value.length + } + + return stack[0].nodes +} diff --git a/node_modules/tailwindcss/src/value-parser/stringify.js b/node_modules/tailwindcss/src/value-parser/stringify.js new file mode 100644 index 0000000..c958906 --- /dev/null +++ b/node_modules/tailwindcss/src/value-parser/stringify.js @@ -0,0 +1,41 @@ +function stringifyNode(node, custom) { + var type = node.type + var value = node.value + var buf + var customResult + + if (custom && (customResult = custom(node)) !== undefined) { + return customResult + } else if (type === 'word' || type === 'space') { + return value + } else if (type === 'string') { + buf = node.quote || '' + return buf + value + (node.unclosed ? '' : buf) + } else if (type === 'comment') { + return '/*' + value + (node.unclosed ? '' : '*/') + } else if (type === 'div') { + return (node.before || '') + value + (node.after || '') + } else if (Array.isArray(node.nodes)) { + buf = stringify(node.nodes, custom) + if (type !== 'function') { + return buf + } + return value + '(' + (node.before || '') + buf + (node.after || '') + (node.unclosed ? '' : ')') + } + return value +} + +function stringify(nodes, custom) { + var result, i + + if (Array.isArray(nodes)) { + result = '' + for (i = nodes.length - 1; ~i; i -= 1) { + result = stringifyNode(nodes[i], custom) + result + } + return result + } + return stringifyNode(nodes, custom) +} + +module.exports = stringify diff --git a/node_modules/tailwindcss/src/value-parser/unit.js b/node_modules/tailwindcss/src/value-parser/unit.js new file mode 100644 index 0000000..42d6cd3 --- /dev/null +++ b/node_modules/tailwindcss/src/value-parser/unit.js @@ -0,0 +1,118 @@ +var minus = '-'.charCodeAt(0) +var plus = '+'.charCodeAt(0) +var dot = '.'.charCodeAt(0) +var exp = 'e'.charCodeAt(0) +var EXP = 'E'.charCodeAt(0) + +// Check if three code points would start a number +// https://www.w3.org/TR/css-syntax-3/#starts-with-a-number +function likeNumber(value) { + var code = value.charCodeAt(0) + var nextCode + + if (code === plus || code === minus) { + nextCode = value.charCodeAt(1) + + if (nextCode >= 48 && nextCode <= 57) { + return true + } + + var nextNextCode = value.charCodeAt(2) + + if (nextCode === dot && nextNextCode >= 48 && nextNextCode <= 57) { + return true + } + + return false + } + + if (code === dot) { + nextCode = value.charCodeAt(1) + + if (nextCode >= 48 && nextCode <= 57) { + return true + } + + return false + } + + if (code >= 48 && code <= 57) { + return true + } + + return false +} + +// Consume a number +// https://www.w3.org/TR/css-syntax-3/#consume-number +module.exports = function (value) { + var pos = 0 + var length = value.length + var code + var nextCode + var nextNextCode + + if (length === 0 || !likeNumber(value)) { + return false + } + + code = value.charCodeAt(pos) + + if (code === plus || code === minus) { + pos++ + } + + while (pos < length) { + code = value.charCodeAt(pos) + + if (code < 48 || code > 57) { + break + } + + pos += 1 + } + + code = value.charCodeAt(pos) + nextCode = value.charCodeAt(pos + 1) + + if (code === dot && nextCode >= 48 && nextCode <= 57) { + pos += 2 + + while (pos < length) { + code = value.charCodeAt(pos) + + if (code < 48 || code > 57) { + break + } + + pos += 1 + } + } + + code = value.charCodeAt(pos) + nextCode = value.charCodeAt(pos + 1) + nextNextCode = value.charCodeAt(pos + 2) + + if ( + (code === exp || code === EXP) && + ((nextCode >= 48 && nextCode <= 57) || + ((nextCode === plus || nextCode === minus) && nextNextCode >= 48 && nextNextCode <= 57)) + ) { + pos += nextCode === plus || nextCode === minus ? 3 : 2 + + while (pos < length) { + code = value.charCodeAt(pos) + + if (code < 48 || code > 57) { + break + } + + pos += 1 + } + } + + return { + number: value.slice(0, pos), + unit: value.slice(pos), + } +} diff --git a/node_modules/tailwindcss/src/value-parser/walk.js b/node_modules/tailwindcss/src/value-parser/walk.js new file mode 100644 index 0000000..dd20a43 --- /dev/null +++ b/node_modules/tailwindcss/src/value-parser/walk.js @@ -0,0 +1,18 @@ +module.exports = function walk(nodes, cb, bubble) { + var i, max, node, result + + for (i = 0, max = nodes.length; i < max; i += 1) { + node = nodes[i] + if (!bubble) { + result = cb(node, i, nodes) + } + + if (result !== false && node.type === 'function' && Array.isArray(node.nodes)) { + walk(node.nodes, cb, bubble) + } + + if (bubble) { + cb(node, i, nodes) + } + } +} |