diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 42645aa492d5..63f79fa100e6 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -50,7 +50,7 @@ If you open a pull request for a new feature, we're likely to close it not becau
## Coding standards
-Our code formatting rules are defined in the `"prettier"` section of [package.json](https://github.com/tailwindcss/tailwindcss/blob/main/package.json). You can check your code against these standards by running:
+Our code formatting rules are defined in the `"prettier"` section of [package.json](https://github.com/tailwindlabs/tailwindcss/blob/main/package.json). You can check your code against these standards by running:
```sh
pnpm run lint
@@ -76,7 +76,7 @@ To run the integration tests, use:
pnpm build && pnpm test:integrations
```
-Additionally, some features require testing in browsers (i.e to ensure CSS variable resolution works as expected). These can be run via:
+Additionally, some features require testing in browsers (i.e. to ensure CSS variable resolution works as expected). These can be run via:
```sh
pnpm build && pnpm test:ui
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index ee9d3f9abcc0..aa0793aa39f8 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -8,7 +8,7 @@ It's never a fun experience to have your pull request declined after investing a
For more info, check out the contributing guide:
-https://github.com/tailwindcss/tailwindcss/blob/main/.github/CONTRIBUTING.md
+https://github.com/tailwindlabs/tailwindcss/blob/main/.github/CONTRIBUTING.md
-->
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index d8c099873b64..fca477c08bec 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -11,6 +11,10 @@ permissions:
env:
NODE_VERSION: 24
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
jobs:
tests:
strategy:
@@ -106,9 +110,13 @@ jobs:
- name: Run Playwright tests
run: npm run test:ui
+ notify:
+ if: ${{ always() && github.ref == 'refs/heads/main' && needs.tests.result == 'failure' }}
+ needs: tests
+ runs-on: ubuntu-latest
+ steps:
- name: Notify Discord
- if: failure() && github.ref == 'refs/heads/main'
uses: discord-actions/message@v2
with:
webhookUrl: ${{ secrets.DISCORD_WEBHOOK_URL }}
- message: 'The [most recent build](<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}>) on the `main` branch has failed.'
+ message: 'The [most recent ${{ github.workflow }} workflow](<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}>) on the `main` branch has failed.'
diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml
index e36f5b3abc47..3b2cf8f1b80a 100644
--- a/.github/workflows/integration-tests.yml
+++ b/.github/workflows/integration-tests.yml
@@ -11,6 +11,10 @@ permissions:
env:
NODE_VERSION: 24
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
jobs:
tests:
strategy:
@@ -109,9 +113,13 @@ jobs:
env:
GITHUB_WORKSPACE: ${{ github.workspace }}
+ notify:
+ if: ${{ always() && github.ref == 'refs/heads/main' && needs.tests.result == 'failure' }}
+ needs: tests
+ runs-on: ubuntu-latest
+ steps:
- name: Notify Discord
- if: failure() && github.ref == 'refs/heads/main'
uses: discord-actions/message@v2
with:
webhookUrl: ${{ secrets.DISCORD_WEBHOOK_URL }}
- message: 'The [most recent build](<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}>) on the `main` branch has failed.'
+ message: 'The [most recent ${{ github.workflow }} workflow](<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}>) on the `main` branch has failed.'
diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml
index e506ab187b7a..d5ad798be455 100644
--- a/.github/workflows/prepare-release.yml
+++ b/.github/workflows/prepare-release.yml
@@ -14,6 +14,10 @@ env:
permissions:
contents: read
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
jobs:
build:
strategy:
@@ -262,7 +266,7 @@ jobs:
run: pnpm --filter=!./playgrounds/* install
- name: Download artifacts
- uses: actions/download-artifact@v6
+ uses: actions/download-artifact@v7
with:
path: ${{ env.OXIDE_LOCATION }}
diff --git a/.github/workflows/release-insiders.yml b/.github/workflows/release-insiders.yml
index f7792244f520..4852cce7b7a9 100644
--- a/.github/workflows/release-insiders.yml
+++ b/.github/workflows/release-insiders.yml
@@ -13,6 +13,10 @@ env:
OXIDE_LOCATION: ./crates/node
RELEASE_CHANNEL: insiders
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
jobs:
build:
strategy:
@@ -259,7 +263,7 @@ jobs:
run: pnpm --filter=!./playgrounds/* install
- name: Download artifacts
- uses: actions/download-artifact@v6
+ uses: actions/download-artifact@v7
with:
path: ${{ env.OXIDE_LOCATION }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 3b0aeab20049..a872900ff9e2 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -254,7 +254,7 @@ jobs:
run: pnpm --filter=!./playgrounds/* install
- name: Download artifacts
- uses: actions/download-artifact@v6
+ uses: actions/download-artifact@v7
with:
path: ${{ env.OXIDE_LOCATION }}
diff --git a/.prettierignore b/.prettierignore
index 3de4530ddcf2..4f50b932fbca 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -4,5 +4,6 @@ pnpm-lock.yaml
target/
crates/node/index.d.ts
crates/node/index.js
+crates/ignore/
.next
.fingerprint
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b66ebaea00dd..dd549036ed0f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,18 +7,51 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+### Added
+
+- _Experimental_: Add `@container-size` utility ([#18901](https://github.com/tailwindlabs/tailwindcss/pull/18901))
+
+### Fixed
+
+- Allow trailing dash in functional utility names for backwards compatibility ([#19696](https://github.com/tailwindlabs/tailwindcss/pull/19696))
+- Fix missing extracted classes in mdx files containing `.` ([#19711](https://github.com/tailwindlabs/tailwindcss/pull/19711))
+
+## [4.2.0] - 2026-02-18
+
+### Added
+
+- Add mauve, olive, mist, and taupe color palettes to the default theme ([#19627](https://github.com/tailwindlabs/tailwindcss/pull/19627))
+- Add `@tailwindcss/webpack` package to run Tailwind CSS as a webpack plugin ([#19610](https://github.com/tailwindlabs/tailwindcss/pull/19610))
+- Add `pbs-*` and `pbe-*` utilities for `padding-block-start` and `padding-block-end` ([#19601](https://github.com/tailwindlabs/tailwindcss/pull/19601))
+- Add `mbs-*` and `mbe-*` utilities for `margin-block-start` and `margin-block-end` ([#19601](https://github.com/tailwindlabs/tailwindcss/pull/19601))
+- Add `scroll-pbs-*` and `scroll-pbe-*` utilities for `scroll-padding-block-start` and `scroll-padding-block-end` ([#19601](https://github.com/tailwindlabs/tailwindcss/pull/19601))
+- Add `scroll-mbs-*` and `scroll-mbe-*` utilities for `scroll-margin-block-start` and `scroll-margin-block-end` ([#19601](https://github.com/tailwindlabs/tailwindcss/pull/19601))
+- Add `border-bs-*` and `border-be-*` utilities for `border-block-start` and `border-block-end` ([#19601](https://github.com/tailwindlabs/tailwindcss/pull/19601))
+- Add `inline-*`, `min-inline-*`, `max-inline-*` utilities for `inline-size`, `min-inline-size`, and `max-inline-size` ([#19612](https://github.com/tailwindlabs/tailwindcss/pull/19612))
+- Add `block-*`, `min-block-*`, `max-block-*` utilities for `block-size`, `min-block-size`, and `max-block-size` ([#19612](https://github.com/tailwindlabs/tailwindcss/pull/19612))
+- Add `inset-s-*`, `inset-e-*`, `inset-bs-*`, `inset-be-*` utilities for `inset-inline-start`, `inset-inline-end`, `inset-block-start`, and `inset-block-end` ([#19613](https://github.com/tailwindlabs/tailwindcss/pull/19613))
+- Add `font-features-*` utility for `font-feature-settings` ([#19623](https://github.com/tailwindlabs/tailwindcss/pull/19615))
+
### Fixed
-- Do not wrap `color-mix` in a `@supports` rule if one already exists ([#19450](https://github.com/tailwindlabs/tailwindcss/pull/19450))
+- Prevent double `@supports` wrapper for `color-mix` values ([#19450](https://github.com/tailwindlabs/tailwindcss/pull/19450))
- Allow whitespace around `@source inline()` argument ([#19461](https://github.com/tailwindlabs/tailwindcss/pull/19461))
-- CLI: Emit comment when source maps are saved to files ([#19447](https://github.com/tailwindlabs/tailwindcss/pull/19447))
-- Detect utilities when containing capital letters followed by numbers ([#19465](https://github.com/tailwindlabs/tailwindcss/pull/19465))
+- Emit comment when source maps are saved to files when using `@tailwindcss/cli` ([#19447](https://github.com/tailwindlabs/tailwindcss/pull/19447))
+- Detect utilities containing capital letters followed by numbers ([#19465](https://github.com/tailwindlabs/tailwindcss/pull/19465))
- Fix class extraction for Rails' strict locals ([#19525](https://github.com/tailwindlabs/tailwindcss/pull/19525))
- Align `@utility` name validation with Oxide scanner rules ([#19524](https://github.com/tailwindlabs/tailwindcss/pull/19524))
+- Fix infinite loop when using `@variant` inside `@custom-variant` ([#19633](https://github.com/tailwindlabs/tailwindcss/pull/19633))
+- Allow multiples of `.25` in `aspect-*` fractions (e.g. `aspect-8.5/11`) ([#19688](https://github.com/tailwindlabs/tailwindcss/pull/19688))
+- Ensure changes to external files listed via `@source` trigger a full page reload when using `@tailwindcss/vite` ([#19670](https://github.com/tailwindlabs/tailwindcss/pull/19670))
+- Improve performance of Oxide scanner in bigger projects by reducing file system walks ([#19632](https://github.com/tailwindlabs/tailwindcss/pull/19632))
+- Ensure import aliases in Astro v5 work without crashing when using `@tailwindcss/vite` ([#19677](https://github.com/tailwindlabs/tailwindcss/issues/19677))
+- Allow escape characters in `@utility` names to improve support with formatters such as Biome ([#19626](https://github.com/tailwindlabs/tailwindcss/pull/19626))
+- Fix incorrect canonicalization results when canonicalizing multiple times ([#19675](https://github.com/tailwindlabs/tailwindcss/pull/19675))
+- Add `.jj` to default ignored content directories ([#19687](https://github.com/tailwindlabs/tailwindcss/pull/19687))
-### Added
+### Deprecated
-- _Experimental_: Add `@container-size` utility ([#18901](https://github.com/tailwindlabs/tailwindcss/pull/18901))
+- Deprecate `start-*` and `end-*` utilities in favor of `inset-s-*` and `inset-e-*` utilities ([#19613](https://github.com/tailwindlabs/tailwindcss/pull/19613))
## [4.1.18] - 2025-12-11
@@ -3913,7 +3946,8 @@ No release notes
- Everything!
-[unreleased]: https://github.com/tailwindlabs/tailwindcss/compare/v4.1.18...HEAD
+[unreleased]: https://github.com/tailwindlabs/tailwindcss/compare/v4.2.0...HEAD
+[4.2.0]: https://github.com/tailwindlabs/tailwindcss/compare/v4.1.18...v4.2.0
[4.1.18]: https://github.com/tailwindlabs/tailwindcss/compare/v4.1.17...v4.1.18
[3.4.19]: https://github.com/tailwindlabs/tailwindcss/compare/v3.4.18...v3.4.19
[4.1.17]: https://github.com/tailwindlabs/tailwindcss/compare/v4.1.16...v4.1.17
diff --git a/README.md b/README.md
index 7d21bd88385a..5f532607d00a 100644
--- a/README.md
+++ b/README.md
@@ -13,10 +13,10 @@
-
+
-
-
+
+
---
@@ -29,8 +29,8 @@ For full documentation, visit [tailwindcss.com](https://tailwindcss.com).
For help, discussion about best practices, or feature ideas:
-[Discuss Tailwind CSS on GitHub](https://github.com/tailwindcss/tailwindcss/discussions)
+[Discuss Tailwind CSS on GitHub](https://github.com/tailwindlabs/tailwindcss/discussions)
## Contributing
-If you're interested in contributing to Tailwind CSS, please read our [contributing docs](https://github.com/tailwindcss/tailwindcss/blob/next/.github/CONTRIBUTING.md) **before submitting a pull request**.
+If you're interested in contributing to Tailwind CSS, please read our [contributing docs](https://github.com/tailwindlabs/tailwindcss/blob/main/.github/CONTRIBUTING.md) **before submitting a pull request**.
diff --git a/crates/node/npm/android-arm-eabi/package.json b/crates/node/npm/android-arm-eabi/package.json
index 47df39f4ec30..cba6fedce0b2 100644
--- a/crates/node/npm/android-arm-eabi/package.json
+++ b/crates/node/npm/android-arm-eabi/package.json
@@ -1,6 +1,6 @@
{
"name": "@tailwindcss/oxide-android-arm-eabi",
- "version": "4.1.18",
+ "version": "4.2.0",
"repository": {
"type": "git",
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
diff --git a/crates/node/npm/android-arm64/package.json b/crates/node/npm/android-arm64/package.json
index a2d8231abb28..0950b7a03e95 100644
--- a/crates/node/npm/android-arm64/package.json
+++ b/crates/node/npm/android-arm64/package.json
@@ -1,6 +1,6 @@
{
"name": "@tailwindcss/oxide-android-arm64",
- "version": "4.1.18",
+ "version": "4.2.0",
"repository": {
"type": "git",
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
diff --git a/crates/node/npm/darwin-arm64/package.json b/crates/node/npm/darwin-arm64/package.json
index 153b2306fe9c..423333d61dcf 100644
--- a/crates/node/npm/darwin-arm64/package.json
+++ b/crates/node/npm/darwin-arm64/package.json
@@ -1,6 +1,6 @@
{
"name": "@tailwindcss/oxide-darwin-arm64",
- "version": "4.1.18",
+ "version": "4.2.0",
"repository": {
"type": "git",
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
diff --git a/crates/node/npm/darwin-x64/package.json b/crates/node/npm/darwin-x64/package.json
index fd4fba92b312..81007674ee5c 100644
--- a/crates/node/npm/darwin-x64/package.json
+++ b/crates/node/npm/darwin-x64/package.json
@@ -1,6 +1,6 @@
{
"name": "@tailwindcss/oxide-darwin-x64",
- "version": "4.1.18",
+ "version": "4.2.0",
"repository": {
"type": "git",
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
diff --git a/crates/node/npm/freebsd-x64/package.json b/crates/node/npm/freebsd-x64/package.json
index 89f126ba19fd..8c25927edc39 100644
--- a/crates/node/npm/freebsd-x64/package.json
+++ b/crates/node/npm/freebsd-x64/package.json
@@ -1,6 +1,6 @@
{
"name": "@tailwindcss/oxide-freebsd-x64",
- "version": "4.1.18",
+ "version": "4.2.0",
"repository": {
"type": "git",
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
diff --git a/crates/node/npm/linux-arm-gnueabihf/package.json b/crates/node/npm/linux-arm-gnueabihf/package.json
index e0939b03718c..b2a4c0281127 100644
--- a/crates/node/npm/linux-arm-gnueabihf/package.json
+++ b/crates/node/npm/linux-arm-gnueabihf/package.json
@@ -1,6 +1,6 @@
{
"name": "@tailwindcss/oxide-linux-arm-gnueabihf",
- "version": "4.1.18",
+ "version": "4.2.0",
"repository": {
"type": "git",
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
diff --git a/crates/node/npm/linux-arm64-gnu/package.json b/crates/node/npm/linux-arm64-gnu/package.json
index ec4d07081c52..e1855f2894c3 100644
--- a/crates/node/npm/linux-arm64-gnu/package.json
+++ b/crates/node/npm/linux-arm64-gnu/package.json
@@ -1,6 +1,6 @@
{
"name": "@tailwindcss/oxide-linux-arm64-gnu",
- "version": "4.1.18",
+ "version": "4.2.0",
"repository": {
"type": "git",
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
diff --git a/crates/node/npm/linux-arm64-musl/package.json b/crates/node/npm/linux-arm64-musl/package.json
index 1ff8c8917ebe..c6b96ab8ab14 100644
--- a/crates/node/npm/linux-arm64-musl/package.json
+++ b/crates/node/npm/linux-arm64-musl/package.json
@@ -1,6 +1,6 @@
{
"name": "@tailwindcss/oxide-linux-arm64-musl",
- "version": "4.1.18",
+ "version": "4.2.0",
"repository": {
"type": "git",
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
diff --git a/crates/node/npm/linux-x64-gnu/package.json b/crates/node/npm/linux-x64-gnu/package.json
index b27ebb4f169b..2d4fa6fe3c84 100644
--- a/crates/node/npm/linux-x64-gnu/package.json
+++ b/crates/node/npm/linux-x64-gnu/package.json
@@ -1,6 +1,6 @@
{
"name": "@tailwindcss/oxide-linux-x64-gnu",
- "version": "4.1.18",
+ "version": "4.2.0",
"repository": {
"type": "git",
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
diff --git a/crates/node/npm/linux-x64-musl/package.json b/crates/node/npm/linux-x64-musl/package.json
index b38fe0792485..5855b5d6a331 100644
--- a/crates/node/npm/linux-x64-musl/package.json
+++ b/crates/node/npm/linux-x64-musl/package.json
@@ -1,6 +1,6 @@
{
"name": "@tailwindcss/oxide-linux-x64-musl",
- "version": "4.1.18",
+ "version": "4.2.0",
"repository": {
"type": "git",
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
diff --git a/crates/node/npm/wasm32-wasi/package.json b/crates/node/npm/wasm32-wasi/package.json
index ea06f944dd16..0c1defca3a43 100644
--- a/crates/node/npm/wasm32-wasi/package.json
+++ b/crates/node/npm/wasm32-wasi/package.json
@@ -1,6 +1,6 @@
{
"name": "@tailwindcss/oxide-wasm32-wasi",
- "version": "4.1.18",
+ "version": "4.2.0",
"cpu": [
"wasm32"
],
@@ -28,11 +28,11 @@
"browser": "tailwindcss-oxide.wasi-browser.js",
"dependencies": {
"@napi-rs/wasm-runtime": "^1.1.1",
- "@emnapi/core": "^1.7.1",
- "@emnapi/runtime": "^1.7.1",
+ "@emnapi/core": "^1.8.1",
+ "@emnapi/runtime": "^1.8.1",
"@tybys/wasm-util": "^0.10.1",
"@emnapi/wasi-threads": "^1.1.0",
- "tslib": "^2.4.0"
+ "tslib": "^2.8.1"
},
"bundledDependencies": [
"@napi-rs/wasm-runtime",
diff --git a/crates/node/npm/win32-arm64-msvc/package.json b/crates/node/npm/win32-arm64-msvc/package.json
index f1ef4133a570..633a4d12eb9c 100644
--- a/crates/node/npm/win32-arm64-msvc/package.json
+++ b/crates/node/npm/win32-arm64-msvc/package.json
@@ -1,6 +1,6 @@
{
"name": "@tailwindcss/oxide-win32-arm64-msvc",
- "version": "4.1.18",
+ "version": "4.2.0",
"repository": {
"type": "git",
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
diff --git a/crates/node/npm/win32-x64-msvc/package.json b/crates/node/npm/win32-x64-msvc/package.json
index 4049d272da2b..99dca35d18c4 100644
--- a/crates/node/npm/win32-x64-msvc/package.json
+++ b/crates/node/npm/win32-x64-msvc/package.json
@@ -1,6 +1,6 @@
{
"name": "@tailwindcss/oxide-win32-x64-msvc",
- "version": "4.1.18",
+ "version": "4.2.0",
"repository": {
"type": "git",
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
diff --git a/crates/node/package.json b/crates/node/package.json
index 00557993d8f3..6da669ced184 100644
--- a/crates/node/package.json
+++ b/crates/node/package.json
@@ -1,6 +1,6 @@
{
"name": "@tailwindcss/oxide",
- "version": "4.1.18",
+ "version": "4.2.0",
"repository": {
"type": "git",
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
@@ -35,7 +35,7 @@
"devDependencies": {
"@napi-rs/cli": "3.4.1",
"@napi-rs/wasm-runtime": "^1.1.1",
- "emnapi": "1.7.1"
+ "emnapi": "1.8.1"
},
"engines": {
"node": ">= 20"
diff --git a/crates/oxide/src/cursor.rs b/crates/oxide/src/cursor.rs
index 818a0ef1b450..71cdccdf6aeb 100644
--- a/crates/oxide/src/cursor.rs
+++ b/crates/oxide/src/cursor.rs
@@ -1,44 +1,49 @@
use std::{ascii::escape_default, fmt::Display};
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, Copy)]
pub struct Cursor<'a> {
// The input we're scanning
pub input: &'a [u8],
// The location of the cursor in the input
pub pos: usize,
-
- /// Is the cursor at the start of the input
- pub at_start: bool,
-
- /// Is the cursor at the end of the input
- pub at_end: bool,
-
- /// The previously consumed character
- /// If `at_start` is true, this will be NUL
- pub prev: u8,
-
- /// The current character
- pub curr: u8,
-
- /// The upcoming character (if any)
- /// If `at_end` is true, this will be NUL
- pub next: u8,
}
impl<'a> Cursor<'a> {
+ #[inline(always)]
pub fn new(input: &'a [u8]) -> Self {
- let mut cursor = Self {
- input,
- pos: 0,
- at_start: true,
- at_end: false,
- prev: 0x00,
- curr: 0x00,
- next: 0x00,
- };
- cursor.move_to(0);
- cursor
+ Self { input, pos: 0 }
+ }
+
+ /// The current byte at `pos`, or 0x00 if past the end.
+ #[inline(always)]
+ pub fn curr(&self) -> u8 {
+ if self.pos < self.input.len() {
+ unsafe { *self.input.get_unchecked(self.pos) }
+ } else {
+ 0x00
+ }
+ }
+
+ /// The next byte at `pos + 1`, or 0x00 if past the end.
+ #[inline(always)]
+ pub fn next(&self) -> u8 {
+ let next_pos = self.pos + 1;
+ if next_pos < self.input.len() {
+ unsafe { *self.input.get_unchecked(next_pos) }
+ } else {
+ 0x00
+ }
+ }
+
+ /// The previous byte at `pos - 1`, or 0x00 if at the start.
+ #[inline(always)]
+ pub fn prev(&self) -> u8 {
+ if self.pos > 0 {
+ unsafe { *self.input.get_unchecked(self.pos - 1) }
+ } else {
+ 0x00
+ }
}
pub fn advance_by(&mut self, amount: usize) {
@@ -48,38 +53,15 @@ impl<'a> Cursor<'a> {
#[inline(always)]
pub fn advance(&mut self) {
self.pos += 1;
-
- self.prev = self.curr;
- self.curr = self.next;
- self.next = *self
- .input
- .get(self.pos.saturating_add(1))
- .unwrap_or(&0x00u8);
}
#[inline(always)]
pub fn advance_twice(&mut self) {
self.pos += 2;
-
- self.prev = self.next;
- self.curr = *self.input.get(self.pos).unwrap_or(&0x00u8);
- self.next = *self
- .input
- .get(self.pos.saturating_add(1))
- .unwrap_or(&0x00u8);
}
pub fn move_to(&mut self, pos: usize) {
- let len = self.input.len();
- let pos = pos.clamp(0, len);
-
- self.pos = pos;
- self.at_start = pos == 0;
- self.at_end = pos + 1 >= len;
-
- self.prev = *self.input.get(pos.wrapping_sub(1)).unwrap_or(&0x00u8);
- self.curr = *self.input.get(pos).unwrap_or(&0x00u8);
- self.next = *self.input.get(pos.saturating_add(1)).unwrap_or(&0x00u8);
+ self.pos = pos.min(self.input.len());
}
}
@@ -90,9 +72,9 @@ impl Display for Cursor<'_> {
let pos = format!("{: >len_count$}", self.pos, len_count = len.len());
write!(f, "{}/{} ", pos, len)?;
- if self.at_start {
+ if self.pos == 0 {
write!(f, "S ")?;
- } else if self.at_end {
+ } else if self.pos + 1 >= self.input.len() {
write!(f, "E ")?;
} else {
write!(f, "M ")?;
@@ -109,9 +91,9 @@ impl Display for Cursor<'_> {
write!(
f,
"[{} {} {}]",
- to_str(self.prev),
- to_str(self.curr),
- to_str(self.next)
+ to_str(self.prev()),
+ to_str(self.curr()),
+ to_str(self.next())
)
}
}
@@ -125,36 +107,28 @@ mod test {
fn test_cursor() {
let mut cursor = Cursor::new(b"hello world");
assert_eq!(cursor.pos, 0);
- assert!(cursor.at_start);
- assert!(!cursor.at_end);
- assert_eq!(cursor.prev, 0x00);
- assert_eq!(cursor.curr, b'h');
- assert_eq!(cursor.next, b'e');
+ assert_eq!(cursor.prev(), 0x00);
+ assert_eq!(cursor.curr(), b'h');
+ assert_eq!(cursor.next(), b'e');
cursor.advance_by(1);
assert_eq!(cursor.pos, 1);
- assert!(!cursor.at_start);
- assert!(!cursor.at_end);
- assert_eq!(cursor.prev, b'h');
- assert_eq!(cursor.curr, b'e');
- assert_eq!(cursor.next, b'l');
+ assert_eq!(cursor.prev(), b'h');
+ assert_eq!(cursor.curr(), b'e');
+ assert_eq!(cursor.next(), b'l');
// Advancing too far should stop at the end
cursor.advance_by(10);
assert_eq!(cursor.pos, 11);
- assert!(!cursor.at_start);
- assert!(cursor.at_end);
- assert_eq!(cursor.prev, b'd');
- assert_eq!(cursor.curr, 0x00);
- assert_eq!(cursor.next, 0x00);
+ assert_eq!(cursor.prev(), b'd');
+ assert_eq!(cursor.curr(), 0x00);
+ assert_eq!(cursor.next(), 0x00);
// Can't advance past the end
cursor.advance_by(1);
assert_eq!(cursor.pos, 11);
- assert!(!cursor.at_start);
- assert!(cursor.at_end);
- assert_eq!(cursor.prev, b'd');
- assert_eq!(cursor.curr, 0x00);
- assert_eq!(cursor.next, 0x00);
+ assert_eq!(cursor.prev(), b'd');
+ assert_eq!(cursor.curr(), 0x00);
+ assert_eq!(cursor.next(), 0x00);
}
}
diff --git a/crates/oxide/src/extractor/arbitrary_property_machine.rs b/crates/oxide/src/extractor/arbitrary_property_machine.rs
index 8fd13e39405f..a4325333925c 100644
--- a/crates/oxide/src/extractor/arbitrary_property_machine.rs
+++ b/crates/oxide/src/extractor/arbitrary_property_machine.rs
@@ -74,7 +74,7 @@ impl Machine for ArbitraryPropertyMachine {
#[inline]
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
- match cursor.curr.into() {
+ match cursor.curr().into() {
// Start of an arbitrary property
Class::OpenBracket => {
self.start_pos = cursor.pos;
@@ -97,8 +97,8 @@ impl Machine for ArbitraryPropertyMachine {
let len = cursor.input.len();
while cursor.pos < len {
- match cursor.curr.into() {
- Class::Dash => match cursor.next.into() {
+ match cursor.curr().into() {
+ Class::Dash => match cursor.next().into() {
// Start of a CSS variable
//
// E.g.: `[--my-color:red]`
@@ -137,7 +137,7 @@ impl ArbitraryPropertyMachine {
fn parse_property_variable(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
match self.css_variable_machine.next(cursor) {
MachineState::Idle => self.restart(),
- MachineState::Done(_) => match cursor.next.into() {
+ MachineState::Done(_) => match cursor.next().into() {
// End of the CSS variable, must be followed by a `:`
//
// E.g.: `[--my-color:red]`
@@ -165,8 +165,8 @@ impl Machine for ArbitraryPropertyMachine {
let len = cursor.input.len();
let start_of_value_pos = cursor.pos;
while cursor.pos < len {
- match cursor.curr.into() {
- Class::Escape => match cursor.next.into() {
+ match cursor.curr().into() {
+ Class::Escape => match cursor.next().into() {
// An escaped whitespace character is not allowed
//
// E.g.: `[color:var(--my-\ color)]`
@@ -181,7 +181,7 @@ impl Machine for ArbitraryPropertyMachine {
},
Class::OpenParen | Class::OpenBracket | Class::OpenCurly => {
- if !self.bracket_stack.push(cursor.curr) {
+ if !self.bracket_stack.push(cursor.curr()) {
return self.restart();
}
cursor.advance();
@@ -190,7 +190,7 @@ impl Machine for ArbitraryPropertyMachine {
Class::CloseParen | Class::CloseBracket | Class::CloseCurly
if !self.bracket_stack.is_empty() =>
{
- if !self.bracket_stack.pop(cursor.curr) {
+ if !self.bracket_stack.pop(cursor.curr()) {
return self.restart();
}
cursor.advance();
@@ -227,7 +227,7 @@ impl Machine for ArbitraryPropertyMachine {
Class::Slash if start_of_value_pos == cursor.pos => return self.restart(),
// String interpolation-like syntax is not allowed. E.g.: `[${x}]`
- Class::Dollar if matches!(cursor.next.into(), Class::OpenCurly) => {
+ Class::Dollar if matches!(cursor.next().into(), Class::OpenCurly) => {
return self.restart()
}
diff --git a/crates/oxide/src/extractor/arbitrary_value_machine.rs b/crates/oxide/src/extractor/arbitrary_value_machine.rs
index f5a612df13f2..cbef62c42681 100644
--- a/crates/oxide/src/extractor/arbitrary_value_machine.rs
+++ b/crates/oxide/src/extractor/arbitrary_value_machine.rs
@@ -32,7 +32,7 @@ impl Machine for ArbitraryValueMachine {
#[inline]
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
// An arbitrary value must start with an open bracket
- if Class::OpenBracket != cursor.curr.into() {
+ if Class::OpenBracket != cursor.curr().into() {
return MachineState::Idle;
}
@@ -42,8 +42,8 @@ impl Machine for ArbitraryValueMachine {
let len = cursor.input.len();
while cursor.pos < len {
- match cursor.curr.into() {
- Class::Escape => match cursor.next.into() {
+ match cursor.curr().into() {
+ Class::Escape => match cursor.next().into() {
// An escaped whitespace character is not allowed
//
// E.g.: `[color:var(--my-\ color)]`
@@ -61,7 +61,7 @@ impl Machine for ArbitraryValueMachine {
},
Class::OpenParen | Class::OpenBracket | Class::OpenCurly => {
- if !self.bracket_stack.push(cursor.curr) {
+ if !self.bracket_stack.push(cursor.curr()) {
return self.restart();
}
cursor.advance();
@@ -70,7 +70,7 @@ impl Machine for ArbitraryValueMachine {
Class::CloseParen | Class::CloseBracket | Class::CloseCurly
if !self.bracket_stack.is_empty() =>
{
- if !self.bracket_stack.pop(cursor.curr) {
+ if !self.bracket_stack.pop(cursor.curr()) {
return self.restart();
}
cursor.advance();
@@ -96,7 +96,7 @@ impl Machine for ArbitraryValueMachine {
Class::Whitespace => return self.restart(),
// String interpolation-like syntax is not allowed. E.g.: `[${x}]`
- Class::Dollar if matches!(cursor.next.into(), Class::OpenCurly) => {
+ Class::Dollar if matches!(cursor.next().into(), Class::OpenCurly) => {
return self.restart()
}
diff --git a/crates/oxide/src/extractor/arbitrary_variable_machine.rs b/crates/oxide/src/extractor/arbitrary_variable_machine.rs
index 6990e110b087..e8d4755571c7 100644
--- a/crates/oxide/src/extractor/arbitrary_variable_machine.rs
+++ b/crates/oxide/src/extractor/arbitrary_variable_machine.rs
@@ -80,13 +80,13 @@ impl Machine for ArbitraryVariableMachine {
#[inline(always)]
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
- match cursor.curr.into() {
+ match cursor.curr().into() {
// Arbitrary variables start with `(` followed by a CSS variable
//
// E.g.: `(--my-variable)`
// ^^
//
- Class::OpenParen => match cursor.next.into() {
+ Class::OpenParen => match cursor.next().into() {
Class::Dash => {
self.start_pos = cursor.pos;
cursor.advance();
@@ -117,7 +117,7 @@ impl Machine for ArbitraryVariableMachine {
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
match self.css_variable_machine.next(cursor) {
MachineState::Idle => self.restart(),
- MachineState::Done(_) => match cursor.next.into() {
+ MachineState::Done(_) => match cursor.next().into() {
// A CSS variable followed by a `,` means that there is a fallback
//
// E.g.: `(--my-color,red)`
@@ -134,7 +134,7 @@ impl Machine for ArbitraryVariableMachine {
_ => {
cursor.advance();
- match cursor.curr.into() {
+ match cursor.curr().into() {
// End of an arbitrary variable, must be followed by `)`
Class::CloseParen => self.done(self.start_pos, cursor),
@@ -156,7 +156,7 @@ impl Machine for ArbitraryVariableMachine {
let len = cursor.input.len();
while cursor.pos < len {
- match cursor.curr.into() {
+ match cursor.curr().into() {
// Valid data type characters
//
// E.g.: `(length:--my-length)`
@@ -169,7 +169,7 @@ impl Machine for ArbitraryVariableMachine {
//
// E.g.: `(length:--my-length)`
// ^
- Class::Colon => match cursor.next.into() {
+ Class::Colon => match cursor.next().into() {
Class::Dash => {
cursor.advance();
return self.transition::().next(cursor);
@@ -196,8 +196,8 @@ impl Machine for ArbitraryVariableMachine {
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
let len = cursor.input.len();
while cursor.pos < len {
- match cursor.curr.into() {
- Class::Escape => match cursor.next.into() {
+ match cursor.curr().into() {
+ Class::Escape => match cursor.next().into() {
// An escaped whitespace character is not allowed
//
// E.g.: `(--my-\ color)`
@@ -212,7 +212,7 @@ impl Machine for ArbitraryVariableMachine {
},
Class::OpenParen | Class::OpenBracket | Class::OpenCurly => {
- if !self.bracket_stack.push(cursor.curr) {
+ if !self.bracket_stack.push(cursor.curr()) {
return self.restart();
}
cursor.advance();
@@ -221,7 +221,7 @@ impl Machine for ArbitraryVariableMachine {
Class::CloseParen | Class::CloseBracket | Class::CloseCurly
if !self.bracket_stack.is_empty() =>
{
- if !self.bracket_stack.pop(cursor.curr) {
+ if !self.bracket_stack.pop(cursor.curr()) {
return self.restart();
}
cursor.advance();
@@ -253,7 +253,7 @@ impl Machine for ArbitraryVariableMachine {
Class::Whitespace => return self.restart(),
// String interpolation-like syntax is not allowed. E.g.: `[${x}]`
- Class::Dollar if matches!(cursor.next.into(), Class::OpenCurly) => {
+ Class::Dollar if matches!(cursor.next().into(), Class::OpenCurly) => {
return self.restart()
}
diff --git a/crates/oxide/src/extractor/candidate_machine.rs b/crates/oxide/src/extractor/candidate_machine.rs
index c82cc3707993..a755cf02a2b5 100644
--- a/crates/oxide/src/extractor/candidate_machine.rs
+++ b/crates/oxide/src/extractor/candidate_machine.rs
@@ -32,14 +32,14 @@ impl Machine for CandidateMachine {
while cursor.pos < len {
// Skip ahead for known characters that will never be part of a candidate. No need to
// run any sub-machines.
- if cursor.curr.is_ascii_whitespace() {
+ if cursor.curr().is_ascii_whitespace() {
self.reset();
cursor.advance();
continue;
}
// Candidates don't start with these characters, so we can skip ahead.
- if matches!(cursor.curr, b':' | b'"' | b'\'' | b'`') {
+ if matches!(cursor.curr(), b':' | b'"' | b'\'' | b'`') {
self.reset();
cursor.advance();
continue;
@@ -56,7 +56,7 @@ impl Machine for CandidateMachine {
// E.g.: `Some Class`
// ^ ^ Invalid, we can jump ahead to the next boundary
//
- if matches!(cursor.curr, b'<' | b'A'..=b'Z') {
+ if matches!(cursor.curr(), b'<' | b'A'..=b'Z') {
if let Some(offset) = cursor.input[cursor.pos..]
.iter()
.position(|&c| is_valid_before_boundary(&c))
diff --git a/crates/oxide/src/extractor/css_variable_machine.rs b/crates/oxide/src/extractor/css_variable_machine.rs
index 4e19c7111f5d..7beb827ee84b 100644
--- a/crates/oxide/src/extractor/css_variable_machine.rs
+++ b/crates/oxide/src/extractor/css_variable_machine.rs
@@ -20,7 +20,7 @@ impl Machine for CssVariableMachine {
#[inline]
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
// CSS Variables must start with `--`
- if Class::Dash != cursor.curr.into() || Class::Dash != cursor.next.into() {
+ if Class::Dash != cursor.curr().into() || Class::Dash != cursor.next().into() {
return MachineState::Idle;
}
@@ -30,11 +30,11 @@ impl Machine for CssVariableMachine {
cursor.advance_twice();
while cursor.pos < len {
- match cursor.curr.into() {
+ match cursor.curr().into() {
// https://drafts.csswg.org/css-syntax-3/#ident-token-diagram
//
Class::AllowedCharacter | Class::Dash => {
- match cursor.next.into() {
+ match cursor.next().into() {
// Valid character followed by a valid character or an escape character
//
// E.g.: `--my-variable`
@@ -58,7 +58,7 @@ impl Machine for CssVariableMachine {
}
}
- Class::Escape => match cursor.next.into() {
+ Class::Escape => match cursor.next().into() {
// An escaped whitespace character is not allowed
//
// In CSS it is allowed, but in the context of a class it's not because then we
diff --git a/crates/oxide/src/extractor/mod.rs b/crates/oxide/src/extractor/mod.rs
index add0de4acaca..a37d6a0a49b3 100644
--- a/crates/oxide/src/extractor/mod.rs
+++ b/crates/oxide/src/extractor/mod.rs
@@ -86,7 +86,7 @@ impl<'a> Extractor<'a> {
{
let cursor = &mut self.cursor.clone();
while cursor.pos < len {
- if cursor.curr.is_ascii_whitespace() {
+ if cursor.curr().is_ascii_whitespace() {
cursor.advance();
continue;
}
@@ -104,7 +104,7 @@ impl<'a> Extractor<'a> {
let cursor = &mut self.cursor.clone();
while cursor.pos < len {
- if cursor.curr.is_ascii_whitespace() {
+ if cursor.curr().is_ascii_whitespace() {
cursor.advance();
continue;
}
@@ -147,7 +147,7 @@ impl<'a> Extractor<'a> {
let cursor = &mut self.cursor.clone();
while cursor.pos < len {
- if cursor.curr.is_ascii_whitespace() {
+ if cursor.curr().is_ascii_whitespace() {
cursor.advance();
continue;
}
@@ -238,7 +238,7 @@ mod tests {
use std::hint::black_box;
fn pre_process_input(input: &str, extension: &str) -> String {
- let input = crate::scanner::pre_process_input(input.as_bytes(), extension);
+ let input = crate::scanner::pre_process_input(input.as_bytes().to_vec(), extension);
String::from_utf8(input).unwrap()
}
diff --git a/crates/oxide/src/extractor/modifier_machine.rs b/crates/oxide/src/extractor/modifier_machine.rs
index 22668a828749..811bc9ded9e0 100644
--- a/crates/oxide/src/extractor/modifier_machine.rs
+++ b/crates/oxide/src/extractor/modifier_machine.rs
@@ -31,14 +31,14 @@ impl Machine for ModifierMachine {
#[inline]
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
// A modifier must start with a `/`, everything else is not a valid start of a modifier
- if Class::Slash != cursor.curr.into() {
+ if Class::Slash != cursor.curr().into() {
return MachineState::Idle;
}
let start_pos = cursor.pos;
cursor.advance();
- match cursor.curr.into() {
+ match cursor.curr().into() {
// Start of an arbitrary value:
//
// ```
@@ -70,9 +70,9 @@ impl Machine for ModifierMachine {
Class::ValidStart => {
let len = cursor.input.len();
while cursor.pos < len {
- match cursor.curr.into() {
+ match cursor.curr().into() {
Class::ValidStart | Class::ValidInside => {
- match cursor.next.into() {
+ match cursor.next().into() {
// Only valid characters are allowed, if followed by another valid character
Class::ValidStart | Class::ValidInside => cursor.advance(),
diff --git a/crates/oxide/src/extractor/named_utility_machine.rs b/crates/oxide/src/extractor/named_utility_machine.rs
index d218dbeb7a9b..24dc027b2d69 100644
--- a/crates/oxide/src/extractor/named_utility_machine.rs
+++ b/crates/oxide/src/extractor/named_utility_machine.rs
@@ -52,8 +52,8 @@ impl Machine for NamedUtilityMachine {
#[inline]
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
- match cursor.curr.into() {
- Class::AlphaLower => match cursor.next.into() {
+ match cursor.curr().into() {
+ Class::AlphaLower => match cursor.next().into() {
// Valid single character utility in between quotes
//
// E.g.: ``
@@ -90,7 +90,7 @@ impl Machine for NamedUtilityMachine {
//
// E.g.: `-mx-2.5`
// ^^
- Class::Dash => match cursor.next.into() {
+ Class::Dash => match cursor.next().into() {
Class::AlphaLower => {
self.start_pos = cursor.pos;
cursor.advance();
@@ -118,7 +118,7 @@ impl Machine for NamedUtilityMachine {
let len = cursor.input.len();
while cursor.pos < len {
- match cursor.curr.into() {
+ match cursor.curr().into() {
// Followed by a boundary character, we are at the end of the utility.
//
// E.g.: `'flex'`
@@ -132,14 +132,14 @@ impl Machine for NamedUtilityMachine {
// E.g.: `:div="{ flex: true }"` (JavaScript object syntax)
// ^
Class::AlphaLower | Class::AlphaUpper => {
- if is_valid_after_boundary(&cursor.next) || {
+ if is_valid_after_boundary(&cursor.next()) || {
// Or any of these characters
//
// - `:`, because of JS object keys
// - `/`, because of modifiers
// - `!`, because of important
matches!(
- cursor.next.into(),
+ cursor.next().into(),
Class::Colon | Class::Slash | Class::Exclamation
)
} {
@@ -150,7 +150,7 @@ impl Machine for NamedUtilityMachine {
cursor.advance()
}
- Class::Dash => match cursor.next.into() {
+ Class::Dash => match cursor.next().into() {
// Start of an arbitrary value
//
// E.g.: `bg-[#0088cc]`
@@ -196,7 +196,7 @@ impl Machine for NamedUtilityMachine {
_ => return self.restart(),
},
- Class::Underscore => match cursor.next.into() {
+ Class::Underscore => match cursor.next().into() {
// Valid characters _if_ followed by another valid character. These characters are
// only valid inside of the utility but not at the end of the utility.
//
@@ -225,14 +225,14 @@ impl Machine for NamedUtilityMachine {
// ^
// E.g.: `:div="{ flex: true }"` (JavaScript object syntax)
// ^
- _ if is_valid_after_boundary(&cursor.next) || {
+ _ if is_valid_after_boundary(&cursor.next()) || {
// Or any of these characters
//
// - `:`, because of JS object keys
// - `/`, because of modifiers
// - `!`, because of important
matches!(
- cursor.next.into(),
+ cursor.next().into(),
Class::Colon | Class::Slash | Class::Exclamation
)
} =>
@@ -249,11 +249,11 @@ impl Machine for NamedUtilityMachine {
// E.g.: `px-2.5`
// ^^^
Class::Dot => {
- if !matches!(cursor.prev.into(), Class::Number) {
+ if !matches!(cursor.prev().into(), Class::Number) {
return self.restart();
}
- if !matches!(cursor.next.into(), Class::Number) {
+ if !matches!(cursor.next().into(), Class::Number) {
return self.restart();
}
@@ -278,7 +278,7 @@ impl Machine for NamedUtilityMachine {
//
Class::Number => {
if !matches!(
- cursor.prev.into(),
+ cursor.prev().into(),
Class::Dash
| Class::Underscore
| Class::Dot
@@ -290,7 +290,7 @@ impl Machine for NamedUtilityMachine {
}
if !matches!(
- cursor.next.into(),
+ cursor.next().into(),
Class::Dot
| Class::Number
| Class::AlphaLower
@@ -314,7 +314,7 @@ impl Machine for NamedUtilityMachine {
// ^^
// ```
Class::Percent => {
- if !matches!(cursor.prev.into(), Class::Number) {
+ if !matches!(cursor.prev().into(), Class::Number) {
return self.restart();
}
diff --git a/crates/oxide/src/extractor/named_variant_machine.rs b/crates/oxide/src/extractor/named_variant_machine.rs
index b6de8220b4cb..285fe6ab51a6 100644
--- a/crates/oxide/src/extractor/named_variant_machine.rs
+++ b/crates/oxide/src/extractor/named_variant_machine.rs
@@ -81,8 +81,8 @@ impl Machine for NamedVariantMachine {
#[inline(always)]
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
- match cursor.curr.into() {
- Class::AlphaLower | Class::Star => match cursor.next.into() {
+ match cursor.curr().into() {
+ Class::AlphaLower | Class::Star => match cursor.next().into() {
// Valid single character variant, must be followed by a `:`
//
// E.g.: ``
@@ -134,8 +134,8 @@ impl Machine for NamedVariantMachine {
let len = cursor.input.len();
while cursor.pos < len {
- match cursor.curr.into() {
- Class::Dash => match cursor.next.into() {
+ match cursor.curr().into() {
+ Class::Dash => match cursor.next().into() {
// Start of an arbitrary value
//
// E.g.: `data-[state=pending]:`.
@@ -192,7 +192,7 @@ impl Machine for NamedVariantMachine {
};
}
- Class::Underscore => match cursor.next.into() {
+ Class::Underscore => match cursor.next().into() {
// Valid characters _if_ followed by another valid character. These characters are
// only valid inside of the variant but not at the end of the variant.
//
@@ -240,11 +240,11 @@ impl Machine for NamedVariantMachine {
// E.g.: `2.5xl:flex`
// ^^^
Class::Dot => {
- if !matches!(cursor.prev.into(), Class::Number) {
+ if !matches!(cursor.prev().into(), Class::Number) {
return self.restart();
}
- if !matches!(cursor.next.into(), Class::Number) {
+ if !matches!(cursor.next().into(), Class::Number) {
return self.restart();
}
@@ -263,7 +263,7 @@ impl Machine for NamedVariantMachine {
impl NamedVariantMachine {
#[inline(always)]
fn parse_arbitrary_end(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
- match cursor.next.into() {
+ match cursor.next().into() {
Class::Slash => {
cursor.advance();
self.transition::().next(cursor)
@@ -285,7 +285,7 @@ impl Machine for NamedVariantMachine {
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
match self.modifier_machine.next(cursor) {
MachineState::Idle => self.restart(),
- MachineState::Done(_) => match cursor.next.into() {
+ MachineState::Done(_) => match cursor.next().into() {
// Modifier must be followed by a `:`
//
// E.g.: `group-hover/name:`
@@ -308,7 +308,7 @@ impl Machine for NamedVariantMachine {
#[inline(always)]
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
- match cursor.curr.into() {
+ match cursor.curr().into() {
// The end of a variant must be the `:`
//
// E.g.: `hover:`
diff --git a/crates/oxide/src/extractor/pre_processors/clojure.rs b/crates/oxide/src/extractor/pre_processors/clojure.rs
index ecf02b4ff634..3fb4ce601cd6 100644
--- a/crates/oxide/src/extractor/pre_processors/clojure.rs
+++ b/crates/oxide/src/extractor/pre_processors/clojure.rs
@@ -32,14 +32,14 @@ impl PreProcessor for Clojure {
let mut cursor = cursor::Cursor::new(&content);
while cursor.pos < len {
- match cursor.curr {
+ match cursor.curr() {
// Consume strings as-is
b'"' => {
result[cursor.pos] = b' ';
cursor.advance();
while cursor.pos < len {
- match cursor.curr {
+ match cursor.curr() {
// Escaped character, skip ahead to the next character
b'\\' => cursor.advance_twice(),
@@ -57,8 +57,8 @@ impl PreProcessor for Clojure {
// Discard line comments until the end of the line.
// Comments start with `;;`
- b';' if matches!(cursor.next, b';') => {
- while cursor.pos < len && cursor.curr != b'\n' {
+ b';' if matches!(cursor.next(), b';') => {
+ while cursor.pos < len && cursor.curr() != b'\n' {
result[cursor.pos] = b' ';
cursor.advance();
}
@@ -70,7 +70,7 @@ impl PreProcessor for Clojure {
cursor.advance();
while cursor.pos < len {
- match cursor.curr {
+ match cursor.curr() {
// A `.` surrounded by digits is a decimal number, so we don't want to replace it.
//
// E.g.:
@@ -78,8 +78,8 @@ impl PreProcessor for Clojure {
// gap-1.5
// ^
// ```
- b'.' if cursor.prev.is_ascii_digit()
- && cursor.next.is_ascii_digit() =>
+ b'.' if cursor.prev().is_ascii_digit()
+ && cursor.next().is_ascii_digit() =>
{
// Keep the `.` as-is
}
@@ -95,7 +95,7 @@ impl PreProcessor for Clojure {
result[cursor.pos] = b' ';
}
// End of keyword.
- _ if !is_keyword_character(cursor.curr) => {
+ _ if !is_keyword_character(cursor.curr()) => {
result[cursor.pos] = b' ';
break;
}
@@ -110,11 +110,11 @@ impl PreProcessor for Clojure {
// Handle quote with a list, e.g.: `'(…)`
// and with a vector, e.g.: `'[…]`
- b'\'' if matches!(cursor.next, b'[' | b'(') => {
+ b'\'' if matches!(cursor.next(), b'[' | b'(') => {
result[cursor.pos] = b' ';
cursor.advance();
result[cursor.pos] = b' ';
- let end = match cursor.curr {
+ let end = match cursor.curr() {
b'[' => b']',
b'(' => b')',
_ => unreachable!(),
@@ -122,7 +122,7 @@ impl PreProcessor for Clojure {
// Consume until the closing `]`
while cursor.pos < len {
- match cursor.curr {
+ match cursor.curr() {
x if x == end => {
result[cursor.pos] = b' ';
break;
@@ -134,7 +134,7 @@ impl PreProcessor for Clojure {
cursor.advance();
while cursor.pos < len {
- match cursor.curr {
+ match cursor.curr() {
// Escaped character, skip ahead to the next character
b'\\' => cursor.advance_twice(),
@@ -157,14 +157,14 @@ impl PreProcessor for Clojure {
}
// Handle quote with a keyword, e.g.: `'bg-white`
- b'\'' if !cursor.next.is_ascii_whitespace() => {
+ b'\'' if !cursor.next().is_ascii_whitespace() => {
result[cursor.pos] = b' ';
cursor.advance();
while cursor.pos < len {
- match cursor.curr {
+ match cursor.curr() {
// End of keyword.
- _ if !is_keyword_character(cursor.curr) => {
+ _ if !is_keyword_character(cursor.curr()) => {
result[cursor.pos] = b' ';
break;
}
diff --git a/crates/oxide/src/extractor/pre_processors/elixir.rs b/crates/oxide/src/extractor/pre_processors/elixir.rs
index 87b89a2a9dda..7737a6b2d1ae 100644
--- a/crates/oxide/src/extractor/pre_processors/elixir.rs
+++ b/crates/oxide/src/extractor/pre_processors/elixir.rs
@@ -13,13 +13,13 @@ impl PreProcessor for Elixir {
while cursor.pos < content.len() {
// Look for a sigil marker
- if cursor.curr != b'~' {
+ if cursor.curr() != b'~' {
cursor.advance();
continue;
}
// Scan charlists, strings, and wordlists
- if !matches!(cursor.next, b'c' | b'C' | b's' | b'S' | b'w' | b'W') {
+ if !matches!(cursor.next(), b'c' | b'C' | b's' | b'S' | b'w' | b'W') {
cursor.advance();
continue;
}
@@ -27,7 +27,7 @@ impl PreProcessor for Elixir {
cursor.advance_twice();
// Match the opening for a sigil
- if !matches!(cursor.curr, b'(' | b'[' | b'{') {
+ if !matches!(cursor.curr(), b'(' | b'[' | b'{') {
continue;
}
@@ -35,19 +35,19 @@ impl PreProcessor for Elixir {
result[cursor.pos] = b' ';
// Scan until we find a balanced closing one and replace it too
- bracket_stack.push(cursor.curr);
+ bracket_stack.push(cursor.curr());
while cursor.pos < content.len() {
cursor.advance();
- match cursor.curr {
+ match cursor.curr() {
// Escaped character, skip ahead to the next character
b'\\' => cursor.advance_twice(),
b'(' | b'[' | b'{' => {
- bracket_stack.push(cursor.curr);
+ bracket_stack.push(cursor.curr());
}
b')' | b']' | b'}' if !bracket_stack.is_empty() => {
- bracket_stack.pop(cursor.curr);
+ bracket_stack.pop(cursor.curr());
if bracket_stack.is_empty() {
// Replace the closing bracket with a space
diff --git a/crates/oxide/src/extractor/pre_processors/haml.rs b/crates/oxide/src/extractor/pre_processors/haml.rs
index a131618abe66..99b7bd052302 100644
--- a/crates/oxide/src/extractor/pre_processors/haml.rs
+++ b/crates/oxide/src/extractor/pre_processors/haml.rs
@@ -101,7 +101,7 @@ impl PreProcessor for Haml {
let mut last_newline_position = 0;
while cursor.pos < len {
- match cursor.curr {
+ match cursor.curr() {
// Escape the next character
b'\\' => {
cursor.advance_twice();
@@ -119,7 +119,7 @@ impl PreProcessor for Haml {
b'-' if cursor.input[last_newline_position..cursor.pos]
.iter()
.all(u8::is_ascii_whitespace)
- && matches!(cursor.next, b'#') =>
+ && matches!(cursor.next(), b'#') =>
{
// Just consume the comment
let updated_last_newline_position =
@@ -159,7 +159,7 @@ impl PreProcessor for Haml {
// Override the last known newline position
last_newline_position = end;
- let replaced = pre_process_input(ruby_code, "rb");
+ let replaced = pre_process_input(ruby_code.to_vec(), "rb");
result.replace_range(start..end, replaced);
}
@@ -190,7 +190,7 @@ impl PreProcessor for Haml {
// digit.
// E.g.: `bg-red-500.2xl:flex`
// ^^^
- if cursor.prev.is_ascii_digit() && cursor.next.is_ascii_digit() {
+ if cursor.prev().is_ascii_digit() && cursor.next().is_ascii_digit() {
let mut next_cursor = cursor.clone();
next_cursor.advance();
@@ -213,11 +213,11 @@ impl PreProcessor for Haml {
if bracket_stack.is_empty() {
result[cursor.pos] = b' ';
}
- bracket_stack.push(cursor.curr);
+ bracket_stack.push(cursor.curr());
}
b')' | b']' | b'}' if !bracket_stack.is_empty() => {
- bracket_stack.pop(cursor.curr);
+ bracket_stack.pop(cursor.curr());
// Replace closing bracket with a space
if bracket_stack.is_empty() {
@@ -257,7 +257,7 @@ impl Haml {
// :url => { :action => "add", :id => product.id },
// :update => { :success => "cart", :failure => "error" }
// ```
- let evaluation_type = cursor.curr;
+ let evaluation_type = cursor.curr();
let block_indentation_level = cursor
.pos
@@ -267,17 +267,17 @@ impl Haml {
let mut last_newline_position = last_known_newline_position;
// Consume until the end of the line first
- while cursor.pos < len && cursor.curr != b'\n' {
+ while cursor.pos < len && cursor.curr() != b'\n' {
cursor.advance();
}
// Block is already done, aka just a line
- if evaluation_type == b'=' && cursor.prev != b',' {
+ if evaluation_type == b'=' && cursor.prev() != b',' {
return cursor.pos;
}
'outer: while cursor.pos < len {
- match cursor.curr {
+ match cursor.curr() {
// Escape the next character
b'\\' => {
cursor.advance_twice();
@@ -289,7 +289,7 @@ impl Haml {
last_newline_position = cursor.pos;
// We are done with this block
- if evaluation_type == b'=' && cursor.prev != b',' {
+ if evaluation_type == b'=' && cursor.prev() != b',' {
break;
}
@@ -300,11 +300,11 @@ impl Haml {
// Skip whitespace and compute the indentation level
x if x.is_ascii_whitespace() => {
// Find first non-whitespace character
- while cursor.pos < len && cursor.curr.is_ascii_whitespace() {
- if cursor.curr == b'\n' {
+ while cursor.pos < len && cursor.curr().is_ascii_whitespace() {
+ if cursor.curr() == b'\n' {
last_newline_position = cursor.pos;
- if evaluation_type == b'=' && cursor.prev != b',' {
+ if evaluation_type == b'=' && cursor.prev() != b',' {
// We are done with this block
break 'outer;
}
diff --git a/crates/oxide/src/extractor/pre_processors/json.rs b/crates/oxide/src/extractor/pre_processors/json.rs
index 809eb501e679..cd457050c5fc 100644
--- a/crates/oxide/src/extractor/pre_processors/json.rs
+++ b/crates/oxide/src/extractor/pre_processors/json.rs
@@ -11,13 +11,13 @@ impl PreProcessor for Json {
let mut cursor = cursor::Cursor::new(content);
while cursor.pos < len {
- match cursor.curr {
+ match cursor.curr() {
// Consume strings as-is
b'"' => {
cursor.advance();
while cursor.pos < len {
- match cursor.curr {
+ match cursor.curr() {
// Escaped character, skip ahead to the next character
b'\\' => cursor.advance_twice(),
diff --git a/crates/oxide/src/extractor/pre_processors/markdown.rs b/crates/oxide/src/extractor/pre_processors/markdown.rs
index 63dc37d1eb4b..488614ee57bd 100644
--- a/crates/oxide/src/extractor/pre_processors/markdown.rs
+++ b/crates/oxide/src/extractor/pre_processors/markdown.rs
@@ -9,20 +9,27 @@ impl PreProcessor for Markdown {
let len = content.len();
let mut result = content.to_vec();
let mut cursor = cursor::Cursor::new(content);
+ let mut bracket_stack = vec![];
let mut in_directive = false;
while cursor.pos < len {
- match (in_directive, cursor.curr) {
+ match (in_directive, cursor.curr()) {
(false, b'{') => {
result[cursor.pos] = b' ';
in_directive = true;
}
+ (true, b'(' | b'[' | b'{' | b'<') => {
+ bracket_stack.push(cursor.curr());
+ }
+ (true, b')' | b']' | b'}' | b'>') if !bracket_stack.is_empty() => {
+ bracket_stack.pop();
+ }
(true, b'}') => {
result[cursor.pos] = b' ';
in_directive = false;
}
- (true, b'.') => {
+ (true, b'.') if bracket_stack.is_empty() => {
result[cursor.pos] = b' ';
}
_ => {}
@@ -60,4 +67,17 @@ mod tests {
Markdown::test(input, expected);
}
}
+
+ #[test]
+ fn test_nested_classes_keep_the_dots() {
+ for (input, expected) in [
+ (
+ r#"{}"#,
+ r#" "#,
+ ),
+ (r#"{content-['example.js']}"#, r#" content-['example.js'] "#),
+ ] {
+ Markdown::test(input, expected);
+ }
+ }
}
diff --git a/crates/oxide/src/extractor/pre_processors/pug.rs b/crates/oxide/src/extractor/pre_processors/pug.rs
index 7b44fcf5be04..70dfe5683197 100644
--- a/crates/oxide/src/extractor/pre_processors/pug.rs
+++ b/crates/oxide/src/extractor/pre_processors/pug.rs
@@ -15,7 +15,7 @@ impl PreProcessor for Pug {
let mut bracket_stack = BracketStack::default();
while cursor.pos < len {
- match cursor.curr {
+ match cursor.curr() {
// Only replace `.` with a space if it's not surrounded by numbers. E.g.:
//
// ```diff
@@ -43,7 +43,7 @@ impl PreProcessor for Pug {
// digit.
// E.g.: `bg-red-500.2xl:flex`
// ^^^
- if cursor.prev.is_ascii_digit() && cursor.next.is_ascii_digit() {
+ if cursor.prev().is_ascii_digit() && cursor.next().is_ascii_digit() {
let mut next_cursor = cursor.clone();
next_cursor.advance();
@@ -68,17 +68,17 @@ impl PreProcessor for Pug {
//
// However, we also need to make sure that we keep the parens that are part of the
// utility class. E.g.: `bg-(--my-color)`.
- b'(' if bracket_stack.is_empty() && !matches!(cursor.prev, b'-' | b'/') => {
+ b'(' if bracket_stack.is_empty() && !matches!(cursor.prev(), b'-' | b'/') => {
result[cursor.pos] = b' ';
- bracket_stack.push(cursor.curr);
+ bracket_stack.push(cursor.curr());
}
b'(' | b'[' | b'{' => {
- bracket_stack.push(cursor.curr);
+ bracket_stack.push(cursor.curr());
}
b')' | b']' | b'}' if !bracket_stack.is_empty() => {
- bracket_stack.pop(cursor.curr);
+ bracket_stack.pop(cursor.curr());
}
// Consume everything else
diff --git a/crates/oxide/src/extractor/pre_processors/ruby.rs b/crates/oxide/src/extractor/pre_processors/ruby.rs
index 89ac1ee7b1c6..1f2221414ff4 100644
--- a/crates/oxide/src/extractor/pre_processors/ruby.rs
+++ b/crates/oxide/src/extractor/pre_processors/ruby.rs
@@ -68,7 +68,8 @@ impl PreProcessor for Ruby {
}
let body = &content_as_str[body_start..body_end];
- let replaced = pre_process_input(body.as_bytes(), &lang.to_ascii_lowercase());
+ let replaced =
+ pre_process_input(body.as_bytes().to_vec(), &lang.to_ascii_lowercase());
result.replace_range(body_start..body_end, replaced);
break;
@@ -77,12 +78,12 @@ impl PreProcessor for Ruby {
// Ruby extraction
while cursor.pos < len {
- match cursor.curr {
+ match cursor.curr() {
b'"' => {
cursor.advance();
while cursor.pos < len {
- match cursor.curr {
+ match cursor.curr() {
// Escaped character, skip ahead to the next character
b'\\' => cursor.advance_twice(),
@@ -102,7 +103,7 @@ impl PreProcessor for Ruby {
cursor.advance();
while cursor.pos < len {
- match cursor.curr {
+ match cursor.curr() {
// Escaped character, skip ahead to the next character
b'\\' => cursor.advance_twice(),
@@ -123,12 +124,12 @@ impl PreProcessor for Ruby {
// Except for strict locals, these are defined in a `<%# locals: … %>`. Checking if
// the comment is preceded by a `%` should be enough without having to perform more
// parsing logic. Worst case we _do_ scan a few comments.
- b'#' if !matches!(cursor.prev, b'%') => {
+ b'#' if !matches!(cursor.prev(), b'%') => {
result[cursor.pos] = b' ';
cursor.advance();
while cursor.pos < len {
- match cursor.curr {
+ match cursor.curr() {
// End of the comment
b'\n' => break,
@@ -148,7 +149,7 @@ impl PreProcessor for Ruby {
}
// Looking for `%w`, `%W`, or `%p`
- if cursor.curr != b'%' || !matches!(cursor.next, b'w' | b'W' | b'p') {
+ if cursor.curr() != b'%' || !matches!(cursor.next(), b'w' | b'W' | b'p') {
cursor.advance();
continue;
}
@@ -156,7 +157,7 @@ impl PreProcessor for Ruby {
cursor.advance_twice();
// Boundary character
- let boundary = match cursor.curr {
+ let boundary = match cursor.curr() {
b'[' => b']',
b'(' => b')',
b'{' => b'}',
@@ -177,11 +178,11 @@ impl PreProcessor for Ruby {
cursor.advance();
while cursor.pos < len {
- match cursor.curr {
+ match cursor.curr() {
// Skip escaped characters
b'\\' => {
// Use backslash to embed spaces in the strings.
- if cursor.next == b' ' {
+ if cursor.next() == b' ' {
result[cursor.pos] = b' ';
}
@@ -190,19 +191,19 @@ impl PreProcessor for Ruby {
// Start of a nested bracket
b'[' | b'(' | b'{' => {
- bracket_stack.push(cursor.curr);
+ bracket_stack.push(cursor.curr());
}
// End of a nested bracket
b']' | b')' | b'}' if !bracket_stack.is_empty() => {
- if !bracket_stack.pop(cursor.curr) {
+ if !bracket_stack.pop(cursor.curr()) {
// Unbalanced
cursor.advance();
}
}
// End of the pattern, replace the boundary character with a space
- _ if cursor.curr == boundary => {
+ _ if cursor.curr() == boundary => {
if boundary != b'\n' {
result[cursor.pos] = b' ';
}
diff --git a/crates/oxide/src/extractor/pre_processors/rust.rs b/crates/oxide/src/extractor/pre_processors/rust.rs
index 6404fffb5e29..34c9e0c916b7 100644
--- a/crates/oxide/src/extractor/pre_processors/rust.rs
+++ b/crates/oxide/src/extractor/pre_processors/rust.rs
@@ -33,7 +33,7 @@ impl Rust {
let mut bracket_stack = bracket_stack::BracketStack::default();
while cursor.pos < len {
- match cursor.curr {
+ match cursor.curr() {
// Escaped character, skip ahead to the next character
b'\\' => {
cursor.advance_twice();
@@ -46,7 +46,7 @@ impl Rust {
cursor.advance();
while cursor.pos < len {
- match cursor.curr {
+ match cursor.curr() {
// Escaped character, skip ahead to the next character
b'\\' => cursor.advance_twice(),
@@ -89,7 +89,7 @@ impl Rust {
// digit.
// E.g.: `bg-red-500.2xl:flex`
// ^^^
- if cursor.prev.is_ascii_digit() && cursor.next.is_ascii_digit() {
+ if cursor.prev().is_ascii_digit() && cursor.next().is_ascii_digit() {
let mut next_cursor = cursor.clone();
next_cursor.advance();
@@ -103,11 +103,11 @@ impl Rust {
}
b'[' => {
- bracket_stack.push(cursor.curr);
+ bracket_stack.push(cursor.curr());
}
b']' if !bracket_stack.is_empty() => {
- bracket_stack.pop(cursor.curr);
+ bracket_stack.pop(cursor.curr());
}
// Consume everything else
diff --git a/crates/oxide/src/extractor/pre_processors/slim.rs b/crates/oxide/src/extractor/pre_processors/slim.rs
index 6d41e5a09b72..eacfb55eedfd 100644
--- a/crates/oxide/src/extractor/pre_processors/slim.rs
+++ b/crates/oxide/src/extractor/pre_processors/slim.rs
@@ -15,7 +15,7 @@ impl PreProcessor for Slim {
let mut bracket_stack = BracketStack::default();
while cursor.pos < len {
- match cursor.curr {
+ match cursor.curr() {
// Only replace `.` with a space if it's not surrounded by numbers. E.g.:
//
// ```diff
@@ -43,7 +43,7 @@ impl PreProcessor for Slim {
// digit.
// E.g.: `bg-red-500.2xl:flex`
// ^^^
- if cursor.prev.is_ascii_digit() && cursor.next.is_ascii_digit() {
+ if cursor.prev().is_ascii_digit() && cursor.next().is_ascii_digit() {
let mut next_cursor = cursor.clone();
next_cursor.advance();
@@ -65,7 +65,7 @@ impl PreProcessor for Slim {
// class=%w[bg-blue-500 w-10 h-10]
// ]
// ```
- b'%' if matches!(cursor.next, b'w' | b'W')
+ b'%' if matches!(cursor.next(), b'w' | b'W')
&& matches!(cursor.input.get(cursor.pos + 2), Some(b'[' | b'(' | b'{')) =>
{
result[cursor.pos] = b' '; // Replace `%`
@@ -73,7 +73,7 @@ impl PreProcessor for Slim {
result[cursor.pos] = b' '; // Replace `w`
cursor.advance();
result[cursor.pos] = b' '; // Replace `[` or `(` or `{`
- bracket_stack.push(cursor.curr);
+ bracket_stack.push(cursor.curr());
cursor.advance(); // Move past the bracket
continue;
}
@@ -96,10 +96,10 @@ impl PreProcessor for Slim {
// Instead of listing all boundary characters, let's list the characters we know
// will be invalid instead.
b'[' if bracket_stack.is_empty()
- && matches!(cursor.prev, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9') =>
+ && matches!(cursor.prev(), b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9') =>
{
result[cursor.pos] = b' ';
- bracket_stack.push(cursor.curr);
+ bracket_stack.push(cursor.curr());
}
// In Slim the class name shorthand can be followed by a parenthesis. E.g.:
@@ -114,17 +114,17 @@ impl PreProcessor for Slim {
//
// However, we also need to make sure that we keep the parens that are part of the
// utility class. E.g.: `bg-(--my-color)`.
- b'(' if bracket_stack.is_empty() && !matches!(cursor.prev, b'-' | b'/') => {
+ b'(' if bracket_stack.is_empty() && !matches!(cursor.prev(), b'-' | b'/') => {
result[cursor.pos] = b' ';
- bracket_stack.push(cursor.curr);
+ bracket_stack.push(cursor.curr());
}
b'(' | b'[' | b'{' => {
- bracket_stack.push(cursor.curr);
+ bracket_stack.push(cursor.curr());
}
b')' | b']' | b'}' if !bracket_stack.is_empty() => {
- bracket_stack.pop(cursor.curr);
+ bracket_stack.pop(cursor.curr());
}
// Consume everything else
diff --git a/crates/oxide/src/extractor/pre_processors/vue.rs b/crates/oxide/src/extractor/pre_processors/vue.rs
index 119e2a3d2079..1b9bf1668cb4 100644
--- a/crates/oxide/src/extractor/pre_processors/vue.rs
+++ b/crates/oxide/src/extractor/pre_processors/vue.rs
@@ -14,13 +14,12 @@ pub struct Vue;
impl PreProcessor for Vue {
fn process(&self, content: &[u8]) -> Vec {
let mut result = content.to_vec();
-
let content_as_str = std::str::from_utf8(content).unwrap();
for (_, [lang, body]) in TEMPLATE_REGEX
.captures_iter(content_as_str)
.map(|c| c.extract())
{
- let replaced = pre_process_input(body.as_bytes(), lang);
+ let replaced = pre_process_input(body.as_bytes().to_vec(), lang);
result = result.replace(body, replaced);
}
diff --git a/crates/oxide/src/extractor/string_machine.rs b/crates/oxide/src/extractor/string_machine.rs
index 9148a1fbf619..4fad49faae19 100644
--- a/crates/oxide/src/extractor/string_machine.rs
+++ b/crates/oxide/src/extractor/string_machine.rs
@@ -30,20 +30,20 @@ impl Machine for StringMachine {
#[inline]
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
- if Class::Quote != cursor.curr.into() {
+ if Class::Quote != cursor.curr().into() {
return MachineState::Idle;
}
// Start of a string
let len = cursor.input.len();
let start_pos = cursor.pos;
- let end_char = cursor.curr;
+ let end_char = cursor.curr();
cursor.advance();
while cursor.pos < len {
- match cursor.curr.into() {
- Class::Escape => match cursor.next.into() {
+ match cursor.curr().into() {
+ Class::Escape => match cursor.next().into() {
// An escaped whitespace character is not allowed
Class::Whitespace => return MachineState::Idle,
@@ -52,7 +52,7 @@ impl Machine for StringMachine {
},
// End of the string
- Class::Quote if cursor.curr == end_char => return self.done(start_pos, cursor),
+ Class::Quote if cursor.curr() == end_char => return self.done(start_pos, cursor),
// Any kind of whitespace is not allowed
Class::Whitespace => return MachineState::Idle,
diff --git a/crates/oxide/src/extractor/utility_machine.rs b/crates/oxide/src/extractor/utility_machine.rs
index b9b1038cdb3a..c116ec4e4226 100644
--- a/crates/oxide/src/extractor/utility_machine.rs
+++ b/crates/oxide/src/extractor/utility_machine.rs
@@ -27,12 +27,12 @@ impl Machine for UtilityMachine {
#[inline]
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
- match cursor.curr.into() {
+ match cursor.curr().into() {
// LEGACY: Important marker
Class::Exclamation => {
self.legacy_important = true;
- match cursor.next.into() {
+ match cursor.next().into() {
// Start of an arbitrary property
//
// E.g.: `![color:red]`
@@ -78,7 +78,7 @@ impl UtilityMachine {
fn parse_arbitrary_property(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
match self.arbitrary_property_machine.next(cursor) {
MachineState::Idle => self.restart(),
- MachineState::Done(_) => match cursor.next.into() {
+ MachineState::Done(_) => match cursor.next().into() {
// End of arbitrary property, but there is a potential modifier.
//
// E.g.: `[color:#0088cc]/`
@@ -109,7 +109,7 @@ impl UtilityMachine {
fn parse_named_utility(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
match self.named_utility_machine.next(cursor) {
MachineState::Idle => self.restart(),
- MachineState::Done(_) => match cursor.next.into() {
+ MachineState::Done(_) => match cursor.next().into() {
// End of a named utility, but there is a potential modifier.
//
// E.g.: `bg-red-500/`
@@ -140,7 +140,7 @@ impl UtilityMachine {
fn parse_modifier(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
match self.modifier_machine.next(cursor) {
MachineState::Idle => self.restart(),
- MachineState::Done(_) => match cursor.next.into() {
+ MachineState::Done(_) => match cursor.next().into() {
// A modifier followed by a modifier is invalid
Class::Slash => self.restart(),
diff --git a/crates/oxide/src/extractor/variant_machine.rs b/crates/oxide/src/extractor/variant_machine.rs
index 23aa61798643..9ec2adc0922a 100644
--- a/crates/oxide/src/extractor/variant_machine.rs
+++ b/crates/oxide/src/extractor/variant_machine.rs
@@ -16,7 +16,7 @@ impl Machine for VariantMachine {
#[inline]
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
- match cursor.curr.into() {
+ match cursor.curr().into() {
// Start of an arbitrary variant
//
// E.g.: `[&:hover]:`
@@ -48,7 +48,7 @@ impl VariantMachine {
start_pos: usize,
cursor: &mut cursor::Cursor<'_>,
) -> MachineState {
- match cursor.next.into() {
+ match cursor.next().into() {
// End of an arbitrary value, must be followed by a `:`
//
// E.g.: `[&:hover]:`
diff --git a/crates/oxide/src/fast_skip.rs b/crates/oxide/src/fast_skip.rs
index 488e61bd68da..54930548c620 100644
--- a/crates/oxide/src/fast_skip.rs
+++ b/crates/oxide/src/fast_skip.rs
@@ -10,7 +10,7 @@ pub fn fast_skip(cursor: &Cursor) -> Option {
return None;
}
- if !cursor.curr.is_ascii_whitespace() {
+ if !cursor.curr().is_ascii_whitespace() {
return None;
}
diff --git a/crates/oxide/src/scanner/detect_sources.rs b/crates/oxide/src/scanner/detect_sources.rs
index 9dc340fbaf47..c519cd00d788 100644
--- a/crates/oxide/src/scanner/detect_sources.rs
+++ b/crates/oxide/src/scanner/detect_sources.rs
@@ -30,11 +30,9 @@ fn sort_by_dir_and_name(a: &DirEntry, z: &DirEntry) -> Ordering {
pub fn resolve_globs(
base: PathBuf,
- dirs: &[PathBuf],
+ dirs: &FxHashSet,
extensions: &FxHashSet,
) -> Vec {
- let allowed_paths: FxHashSet = FxHashSet::from_iter(dirs.iter().cloned());
-
// A list of known extensions + a list of extensions we found in the project.
let mut found_extensions: FxHashSet =
FxHashSet::from_iter(KNOWN_EXTENSIONS.iter().map(|x| x.to_string()));
@@ -72,7 +70,7 @@ pub fn resolve_globs(
continue;
}
- if !allowed_paths.contains(path) {
+ if !dirs.contains(path) {
let mut path = path;
while let Some(parent) = path.parent() {
if parent == base {
@@ -113,7 +111,7 @@ pub fn resolve_globs(
continue;
}
- if !allowed_paths.contains(path) {
+ if !dirs.contains(path) {
continue;
}
diff --git a/crates/oxide/src/scanner/fixtures/ignored-content-dirs.txt b/crates/oxide/src/scanner/fixtures/ignored-content-dirs.txt
index 0921d2ff8c8e..79f01ecc117a 100644
--- a/crates/oxide/src/scanner/fixtures/ignored-content-dirs.txt
+++ b/crates/oxide/src/scanner/fixtures/ignored-content-dirs.txt
@@ -1,14 +1,15 @@
.git
.hg
-.svn
-node_modules
-.yarn
-.venv
-venv
+.jj
.next
-.turbo
.parcel-cache
-__pycache__
-.svelte-kit
.pnpm-store
+.svelte-kit
+.svn
+.turbo
+.venv
.vercel
+.yarn
+__pycache__
+node_modules
+venv
diff --git a/crates/oxide/src/scanner/init_tracing.rs b/crates/oxide/src/scanner/init_tracing.rs
new file mode 100644
index 000000000000..2a570ff20188
--- /dev/null
+++ b/crates/oxide/src/scanner/init_tracing.rs
@@ -0,0 +1,70 @@
+use std::fs::OpenOptions;
+use std::io::{self, Write};
+use std::path::Path;
+use std::sync::{self, Arc, Mutex};
+use tracing_subscriber::fmt::writer::BoxMakeWriter;
+
+pub static SHOULD_TRACE: sync::LazyLock = sync::LazyLock::new(
+ || matches!(std::env::var("DEBUG"), Ok(value) if value.eq("*") || (value.contains("tailwindcss:oxide") && !value.contains("-tailwindcss:oxide"))),
+);
+
+fn dim(input: &str) -> String {
+ format!("\u{001b}[2m{input}\u{001b}[22m")
+}
+
+fn blue(input: &str) -> String {
+ format!("\u{001b}[34m{input}\u{001b}[39m")
+}
+
+fn highlight(input: &str) -> String {
+ format!("{}{}{}", dim(&blue("`")), blue(input), dim(&blue("`")))
+}
+
+struct MutexWriter(Arc>);
+
+impl Write for MutexWriter {
+ fn write(&mut self, buf: &[u8]) -> io::Result {
+ self.0.lock().unwrap().write(buf)
+ }
+
+ fn flush(&mut self) -> io::Result<()> {
+ self.0.lock().unwrap().flush()
+ }
+}
+
+pub fn init_tracing() {
+ if !*SHOULD_TRACE {
+ return;
+ }
+
+ let file_path = format!("tailwindcss-{}.log", std::process::id());
+ let file = OpenOptions::new()
+ .create(true)
+ .append(true)
+ .open(&file_path)
+ .unwrap_or_else(|_| panic!("Failed to open {file_path}"));
+
+ let file_path = Path::new(&file_path);
+ let absolute_file_path = dunce::canonicalize(file_path)
+ .unwrap_or_else(|_| panic!("Failed to canonicalize {file_path:?}"));
+ eprintln!(
+ "{} Writing debug info to: {}\n",
+ dim("[DEBUG]"),
+ highlight(absolute_file_path.as_path().to_str().unwrap())
+ );
+
+ let file = Arc::new(Mutex::new(file));
+
+ let writer: BoxMakeWriter = BoxMakeWriter::new({
+ let file = file.clone();
+ move || Box::new(MutexWriter(file.clone())) as Box
+ });
+
+ _ = tracing_subscriber::fmt()
+ .with_max_level(tracing::Level::INFO)
+ .with_span_events(tracing_subscriber::fmt::format::FmtSpan::ACTIVE)
+ .with_writer(writer)
+ .with_ansi(false)
+ .compact()
+ .try_init();
+}
diff --git a/crates/oxide/src/scanner/mod.rs b/crates/oxide/src/scanner/mod.rs
index ec6aea642481..6d6cabc2aa80 100644
--- a/crates/oxide/src/scanner/mod.rs
+++ b/crates/oxide/src/scanner/mod.rs
@@ -1,5 +1,6 @@
pub mod auto_source_detection;
pub mod detect_sources;
+pub mod init_tracing;
pub mod sources;
use crate::extractor::{Extracted, Extractor};
@@ -14,15 +15,13 @@ use bstr::ByteSlice;
use fast_glob::glob_match;
use fxhash::{FxHashMap, FxHashSet};
use ignore::{gitignore::GitignoreBuilder, WalkBuilder};
+use init_tracing::{init_tracing, SHOULD_TRACE};
use rayon::prelude::*;
use std::collections::{BTreeMap, BTreeSet};
-use std::fs::OpenOptions;
-use std::io::{self, Write};
use std::path::{Path, PathBuf};
-use std::sync::{self, Arc, Mutex};
+use std::sync::{Arc, Mutex};
use std::time::SystemTime;
use tracing::event;
-use tracing_subscriber::fmt::writer::BoxMakeWriter;
// @source "some/folder"; // This is auto source detection
// @source "some/folder/**/*"; // This is auto source detection
@@ -34,70 +33,6 @@ use tracing_subscriber::fmt::writer::BoxMakeWriter;
//
// @source "do-include-me.bin"; // `.bin` is typically ignored, but now it's explicit so should be included
// @source "git-ignored.html"; // A git ignored file that is listed explicitly, should be scanned
-static SHOULD_TRACE: sync::LazyLock = sync::LazyLock::new(
- || matches!(std::env::var("DEBUG"), Ok(value) if value.eq("*") || (value.contains("tailwindcss:oxide") && !value.contains("-tailwindcss:oxide"))),
-);
-
-fn dim(input: &str) -> String {
- format!("\u{001b}[2m{input}\u{001b}[22m")
-}
-
-fn blue(input: &str) -> String {
- format!("\u{001b}[34m{input}\u{001b}[39m")
-}
-
-fn highlight(input: &str) -> String {
- format!("{}{}{}", dim(&blue("`")), blue(input), dim(&blue("`")))
-}
-
-fn init_tracing() {
- if !*SHOULD_TRACE {
- return;
- }
-
- let file_path = format!("tailwindcss-{}.log", std::process::id());
- let file = OpenOptions::new()
- .create(true)
- .append(true)
- .open(&file_path)
- .unwrap_or_else(|_| panic!("Failed to open {file_path}"));
-
- let file_path = Path::new(&file_path);
- let absolute_file_path = dunce::canonicalize(file_path)
- .unwrap_or_else(|_| panic!("Failed to canonicalize {file_path:?}"));
- eprintln!(
- "{} Writing debug info to: {}\n",
- dim("[DEBUG]"),
- highlight(absolute_file_path.as_path().to_str().unwrap())
- );
-
- let file = Arc::new(Mutex::new(file));
-
- let writer: BoxMakeWriter = BoxMakeWriter::new({
- let file = file.clone();
- move || Box::new(MutexWriter(file.clone())) as Box
- });
-
- _ = tracing_subscriber::fmt()
- .with_max_level(tracing::Level::INFO)
- .with_span_events(tracing_subscriber::fmt::format::FmtSpan::ACTIVE)
- .with_writer(writer)
- .with_ansi(false)
- .compact()
- .try_init();
-}
-
-struct MutexWriter(Arc>);
-
-impl Write for MutexWriter {
- fn write(&mut self, buf: &[u8]) -> io::Result {
- self.0.lock().unwrap().write(buf)
- }
-
- fn flush(&mut self) -> io::Result<()> {
- self.0.lock().unwrap().flush()
- }
-}
#[derive(Debug, Clone)]
pub enum ChangedContent {
@@ -129,26 +64,32 @@ pub struct Scanner {
/// The walker to detect all files that we have to scan
walker: Option,
- /// All changed content that we have to parse
- changed_content: Vec,
-
/// All found extensions
extensions: FxHashSet,
- /// All CSS files we want to scan for CSS variable usage
- css_files: Vec,
-
/// All files that we have to scan
- files: Vec,
+ files: FxHashSet,
/// All directories, sub-directories, etc… we saw during source detection
- dirs: Vec,
+ dirs: FxHashSet,
/// All generated globs, used for setting up watchers
globs: Option>,
/// Track unique set of candidates
candidates: FxHashSet,
+
+ /// Track mtimes for files so re-scans can skip unchanged files.
+ /// Only populated after the first scan completes (to avoid unnecessary
+ /// metadata calls on initial build).
+ mtimes: FxHashMap,
+
+ /// Whether we've completed at least one full scan. When false, we skip
+ /// mtime tracking entirely so the initial build stays fast.
+ has_scanned_once: bool,
+
+ /// Whether sources have been scanned since the last `scan()` call
+ sources_scanned: bool,
}
impl Scanner {
@@ -163,7 +104,6 @@ impl Scanner {
}
let sources = Sources::new(public_source_entries_to_private_source_entries(sources));
-
if *SHOULD_TRACE {
event!(tracing::Level::INFO, "Optimized sources:");
for source in sources.iter() {
@@ -171,29 +111,26 @@ impl Scanner {
}
}
+ let walker = create_walker(&sources);
+
Self {
- sources: sources.clone(),
- walker: create_walker(sources),
+ sources,
+ walker,
..Default::default()
}
}
pub fn scan(&mut self) -> Vec {
- self.scan_sources();
+ self.sources_scanned = false;
- // TODO: performance improvement, bail early if we don't have any changed content
- // if self.changed_content.is_empty() {
- // return vec![];
- // }
+ let (scanned_blobs, css_files) = self.discover_sources();
- let _new_candidates = self.extract_candidates();
+ self.extract_candidates(scanned_blobs, css_files);
- // Make sure we have a sorted list of candidates
- let mut candidates = self.candidates.iter().cloned().collect::<_>>();
- candidates.par_sort_unstable();
-
- // Return all candidates instead of only the new ones
- candidates
+ // Return all candidates sorted
+ let mut result = self.candidates.iter().cloned().collect::<_>>();
+ result.par_sort_unstable();
+ result
}
#[tracing::instrument(skip_all)]
@@ -208,7 +145,7 @@ impl Scanner {
// Raw content can be parsed directly, no need to verify if the file exists and is allowed
// to be scanned.
- self.changed_content.extend(changed_contents);
+ let mut content_to_scan: Vec = changed_contents;
// Fully resolve all files
let changed_files = changed_files
@@ -232,7 +169,7 @@ impl Scanner {
});
// All known files are allowed to be scanned
- self.changed_content.extend(known_files);
+ content_to_scan.extend(known_files);
// Figure out if the new unknown files are allowed to be scanned
if !new_unknown_files.is_empty() {
@@ -252,8 +189,8 @@ impl Scanner {
// When the file is found on disk it means that all the rules pass. We can
// extract the current file and remove it from the list of passed in files.
if file == path {
- self.files.push(path.to_path_buf()); // Track for future use
- self.changed_content.push(changed_file.clone()); // Track for parsing
+ self.files.insert(path.to_path_buf()); // Track for future use
+ content_to_scan.push(changed_file.clone()); // Track for parsing
drop_file_indexes.push(idx);
}
}
@@ -274,18 +211,17 @@ impl Scanner {
}
}
- self.extract_candidates()
+ // Read all content into blobs for extraction
+ let blobs = read_all_files(content_to_scan);
+ self.extract_candidates(blobs, vec![])
}
#[tracing::instrument(skip_all)]
- fn extract_candidates(&mut self) -> Vec {
- let changed_content = self.changed_content.drain(..).collect::<_>>();
-
- // Extract all candidates from the changed content
- let mut new_candidates = parse_all_blobs(read_all_files(changed_content));
+ fn extract_candidates(&mut self, blobs: Vec>, css_files: Vec) -> Vec {
+ // Extract all candidates from the pre-read blobs
+ let mut new_candidates = parse_all_blobs(blobs);
// Extract all CSS variables from the CSS files
- let css_files = self.css_files.drain(..).collect::<_>>();
if !css_files.is_empty() {
let css_variables = extract_css_variables(read_all_files(
css_files
@@ -297,63 +233,23 @@ impl Scanner {
new_candidates.extend(css_variables);
}
- // Only compute the new candidates and ignore the ones we already have. This is for
- // subsequent calls to prevent serializing the entire set of candidates every time.
- let mut new_candidates = new_candidates
- .into_par_iter()
- .filter(|candidate| !self.candidates.contains(candidate))
- .collect::<_>>();
-
- new_candidates.par_sort_unstable();
+ // Only keep candidates we haven't seen before
+ for existing in self.candidates.iter() {
+ new_candidates.remove(existing);
+ }
// Track new candidates for subsequent calls
- self.candidates.par_extend(new_candidates.clone());
-
- new_candidates
- }
-
- #[tracing::instrument(skip_all)]
- fn scan_sources(&mut self) {
- let Some(walker) = &mut self.walker else {
- return;
- };
+ self.candidates.extend(new_candidates.iter().cloned());
- for entry in walker.build().filter_map(Result::ok) {
- let path = entry.into_path();
- let Ok(metadata) = path.metadata() else {
- continue;
- };
- if metadata.is_dir() {
- self.dirs.push(path);
- } else if metadata.is_file() {
- let extension = path
- .extension()
- .and_then(|x| x.to_str())
- .unwrap_or_default(); // In case the file has no extension
+ let mut result: Vec = new_candidates.into_iter().collect();
+ result.par_sort_unstable();
- match extension {
- // Special handing for CSS files, we don't want to extract candidates from
- // these files, but we do want to extract used CSS variables.
- "css" => {
- self.css_files.push(path.clone());
- }
- _ => {
- self.changed_content.push(ChangedContent::File(
- path.to_path_buf(),
- extension.to_owned(),
- ));
- }
- }
-
- self.extensions.insert(extension.to_owned());
- self.files.push(path);
- }
- }
+ result
}
#[tracing::instrument(skip_all)]
pub fn get_files(&mut self) -> Vec {
- self.scan_sources();
+ let _ = self.discover_sources();
self.files
.par_iter()
@@ -367,7 +263,7 @@ impl Scanner {
return globs.clone();
}
- self.scan_sources();
+ let _ = self.discover_sources();
let mut globs = vec![];
for source in self.sources.iter() {
@@ -456,6 +352,99 @@ impl Scanner {
})
.collect()
}
+
+ #[tracing::instrument(skip_all)]
+ fn discover_sources(&mut self) -> (Vec>, Vec) {
+ if self.sources_scanned {
+ return (vec![], vec![]);
+ }
+ self.sources_scanned = true;
+
+ let Some(walker) = &mut self.walker else {
+ return (vec![], vec![]);
+ };
+
+ // Use synchronous walk for the initial build (lower overhead) and parallel
+ // walk for subsequent calls (watch mode) where the overhead is amortised.
+ let all_entries = if self.has_scanned_once {
+ walk_parallel(walker)
+ } else {
+ walk_synchronous(walker)
+ };
+
+ let mut css_files: Vec = vec![];
+ let mut content_paths: Vec<(PathBuf, String)> = Vec::new();
+ let mut seen_files: FxHashSet = FxHashSet::default();
+
+ for (path, is_dir, extension) in all_entries {
+ if is_dir {
+ self.dirs.insert(path);
+ } else {
+ // Deduplicate: parallel walk can visit the same file from multiple threads
+ if !seen_files.insert(path.clone()) {
+ continue;
+ }
+
+ // On re-scans, check mtime to skip unchanged files.
+ // On the first scan we skip this entirely to avoid extra
+ // metadata syscalls.
+ let changed = if self.has_scanned_once {
+ let current_mtime = std::fs::metadata(&path)
+ .ok()
+ .and_then(|m| m.modified().ok());
+
+ match current_mtime {
+ Some(mtime) => {
+ let prev = self.mtimes.insert(path.clone(), mtime);
+ prev.is_none_or(|prev| prev != mtime)
+ }
+ None => true,
+ }
+ } else {
+ true
+ };
+
+ match extension.as_str() {
+ // Special handing for CSS files, we don't want to extract candidates from
+ // these files, but we do want to extract used CSS variables.
+ "css" => {
+ if changed {
+ css_files.push(path.clone());
+ }
+ }
+ _ => {
+ if changed {
+ content_paths.push((path.clone(), extension.clone()));
+ }
+ }
+ }
+
+ self.extensions.insert(extension);
+ self.files.insert(path);
+ }
+ }
+
+ // Read + preprocess all discovered files in parallel
+ let scanned_blobs: Vec> = content_paths
+ .into_par_iter()
+ .filter_map(|(path, ext)| {
+ let content = std::fs::read(&path).ok()?;
+ event!(tracing::Level::INFO, "Reading {:?}", path);
+ let processed = pre_process_input(content, &ext);
+ if processed.is_empty() {
+ None
+ } else {
+ Some(processed)
+ }
+ })
+ .collect();
+
+ if !self.has_scanned_once {
+ self.has_scanned_once = true;
+ }
+
+ (scanned_blobs, css_files)
+ }
}
fn read_changed_content(c: ChangedContent) -> Option> {
@@ -474,26 +463,26 @@ fn read_changed_content(c: ChangedContent) -> Option> {
ChangedContent::Content(contents, extension) => (contents.into_bytes(), extension),
};
- Some(pre_process_input(&content, &extension))
+ Some(pre_process_input(content, &extension))
}
-pub fn pre_process_input(content: &[u8], extension: &str) -> Vec {
+pub fn pre_process_input(content: Vec, extension: &str) -> Vec {
use crate::extractor::pre_processors::*;
match extension {
- "clj" | "cljs" | "cljc" => Clojure.process(content),
- "heex" | "eex" | "ex" | "exs" => Elixir.process(content),
- "cshtml" | "razor" => Razor.process(content),
- "haml" => Haml.process(content),
- "json" => Json.process(content),
- "md" | "mdx" => Markdown.process(content),
- "pug" => Pug.process(content),
- "rb" | "erb" => Ruby.process(content),
- "slim" | "slang" => Slim.process(content),
- "svelte" => Svelte.process(content),
- "rs" => Rust.process(content),
- "vue" => Vue.process(content),
- _ => content.to_vec(),
+ "clj" | "cljs" | "cljc" => Clojure.process(&content),
+ "heex" | "eex" | "ex" | "exs" => Elixir.process(&content),
+ "cshtml" | "razor" => Razor.process(&content),
+ "haml" => Haml.process(&content),
+ "json" => Json.process(&content),
+ "md" | "mdx" => Markdown.process(&content),
+ "pug" => Pug.process(&content),
+ "rb" | "erb" => Ruby.process(&content),
+ "slim" | "slang" => Slim.process(&content),
+ "svelte" => Svelte.process(&content),
+ "rs" => Rust.process(&content),
+ "vue" => Vue.process(&content),
+ _ => content,
}
}
@@ -512,23 +501,23 @@ fn read_all_files(changed_content: Vec) -> Vec> {
}
#[tracing::instrument(skip_all)]
-fn extract_css_variables(blobs: Vec>) -> Vec {
+fn extract_css_variables(blobs: Vec>) -> FxHashSet {
extract(blobs, |mut extractor| {
extractor.extract_variables_from_css()
})
}
#[tracing::instrument(skip_all)]
-fn parse_all_blobs(blobs: Vec>) -> Vec {
+fn parse_all_blobs(blobs: Vec>) -> FxHashSet {
extract(blobs, |mut extractor| extractor.extract())
}
#[tracing::instrument(skip_all)]
-fn extract(blobs: Vec>, handle: H) -> Vec
+fn extract(blobs: Vec>, handle: H) -> FxHashSet
where
H: Fn(Extractor) -> Vec + std::marker::Sync,
{
- let mut result: Vec<_> = blobs
+ blobs
.par_iter()
.flat_map(|blob| blob.par_split(|x| *x == b'\n'))
.filter_map(|blob| {
@@ -554,22 +543,97 @@ where
})
.into_iter()
.map(|s| unsafe { String::from_utf8_unchecked(s.to_vec()) })
- .collect();
+ .collect()
+}
+
+type WalkEntry = (PathBuf, bool, String);
- // SAFETY: Unstable sort is faster and in this scenario it's also safe because we are
- // guaranteed to have unique candidates.
- result.par_sort_unstable();
+/// Walk the file system synchronously. Used for the initial build where the overhead of spawning
+/// parallel walker threads is not worth it.
+#[tracing::instrument(skip_all)]
+fn walk_synchronous(walker: &mut WalkBuilder) -> Vec {
+ let mut entries = Vec::new();
+
+ for entry in walker.build().filter_map(Result::ok) {
+ let is_dir = entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false);
+ let path = entry.into_path();
+
+ if is_dir {
+ entries.push((path, true, String::new()));
+ } else {
+ let ext = path
+ .extension()
+ .and_then(|x| x.to_str())
+ .unwrap_or_default()
+ .to_owned();
+ entries.push((path, false, ext));
+ }
+ }
- result
+ entries
}
-/// Create a walker for the given sources to detect all the files that we have to scan.
+/// Walk the file system in parallel. Used in watch mode where the parallel walker overhead is
+/// amortised across many rebuilds and subsequent calls are much faster.
+#[tracing::instrument(skip_all)]
+fn walk_parallel(walker: &mut WalkBuilder) -> Vec {
+ struct FlushOnDrop {
+ local: Vec,
+ shared: Arc>>,
+ }
+
+ impl Drop for FlushOnDrop {
+ fn drop(&mut self) {
+ if !self.local.is_empty() {
+ self.shared.lock().unwrap().append(&mut self.local);
+ }
+ }
+ }
+
+ let collected: Arc>> = Arc::new(Mutex::new(Vec::new()));
+
+ walker.build_parallel().run(|| {
+ let mut buf = FlushOnDrop {
+ local: Vec::with_capacity(256),
+ shared: collected.clone(),
+ };
+
+ Box::new(move |entry| {
+ let Ok(entry) = entry else {
+ return ignore::WalkState::Continue;
+ };
+
+ let is_dir = entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false);
+ let path = entry.into_path();
+
+ if is_dir {
+ buf.local.push((path, true, String::new()));
+ } else {
+ let ext = path
+ .extension()
+ .and_then(|x| x.to_str())
+ .unwrap_or_default()
+ .to_owned();
+ buf.local.push((path, false, ext));
+ }
+
+ if buf.local.len() >= 256 {
+ buf.shared.lock().unwrap().append(&mut buf.local);
+ }
+
+ ignore::WalkState::Continue
+ })
+ });
+
+ // All threads have finished and flushed their buffers via FlushOnDrop::drop
+ Arc::try_unwrap(collected).unwrap().into_inner().unwrap()
+}
+
+/// Sets up a WalkBuilder with all source roots, gitignore rules, and source pattern matching.
///
-/// The `mtimes` map is used to keep track of the last modified time of each file. This is used to
-/// determine if a file or folder has changed since the last scan and we can skip folders that
-/// haven't changed.
-fn create_walker(sources: Sources) -> Option {
- let mtimes: Arc>> = Default::default();
+/// This is the common setup shared between the full walker (with mtime tracking for re-scans)
+/// and the parallel walker (without mtime tracking for the initial scan).
+fn create_walker(sources: &Sources) -> Option {
let mut other_roots: FxHashSet<&PathBuf> = FxHashSet::default();
let mut first_root: Option<&PathBuf> = None;
let mut ignores: BTreeMap<&PathBuf, BTreeSet> = Default::default();
@@ -717,75 +781,68 @@ fn create_walker(sources: Sources) -> Option {
builder.add_gitignore(ignore);
}
- builder.filter_entry({
- move |entry| {
- let path = entry.path();
-
- // Ensure the entries are matching any of the provided source patterns (this is
- // necessary for manual-patterns that can filter the file extension)
- if path.is_file() {
- let mut matches = false;
- for source in sources.iter() {
- match source {
- SourceEntry::Auto { base } | SourceEntry::External { base } => {
- if path.starts_with(base) {
- matches = true;
- break;
- }
- }
- SourceEntry::Pattern { base, pattern } => {
- let mut pattern = pattern.to_string();
- // Ensure that the pattern is pinned to the base path.
- if !pattern.starts_with("/") {
- pattern = format!("/{pattern}");
- }
-
- // Check if path starts with base, if so, remove the prefix and check the remainder against the pattern
- let remainder = path.strip_prefix(base);
- if remainder.is_ok_and(|remainder| {
- let mut path_str = remainder.to_string_lossy().to_string();
- if !path_str.starts_with("/") {
- path_str = format!("/{path_str}");
- }
- glob_match(pattern, path_str.as_bytes())
- }) {
- matches = true;
- break;
- }
- }
- _ => {}
- }
- }
+ // Pre-compute source matching data to avoid allocations in the hot filter_entry path
+ let auto_bases: Vec = sources
+ .iter()
+ .filter_map(|source| match source {
+ SourceEntry::Auto { base } | SourceEntry::External { base } => Some(base.clone()),
+ _ => None,
+ })
+ .collect();
- if !matches {
- return false;
- }
+ let pattern_sources: Vec<(PathBuf, String)> = sources
+ .iter()
+ .filter_map(|source| match source {
+ SourceEntry::Pattern { base, pattern } => {
+ let normalized = if pattern.starts_with("/") {
+ pattern.to_string()
+ } else {
+ format!("/{pattern}")
+ };
+ Some((base.clone(), normalized))
}
+ _ => None,
+ })
+ .collect();
- let mut mtimes = mtimes.lock().unwrap();
- let current_time = match entry.metadata() {
- Ok(metadata) if metadata.is_file() => {
- if let Ok(time) = metadata.modified() {
- Some(time)
- } else {
- None
- }
- }
- _ => None,
- };
+ // Source pattern matching filter (lock-free, safe for parallel walking)
+ builder.filter_entry(move |entry| {
+ let path = entry.path();
- let previous_time =
- current_time.and_then(|time| mtimes.insert(entry.clone().into_path(), time));
+ // Ensure the entries are matching any of the provided source patterns (this is
+ // necessary for manual-patterns that can filter the file extension)
+ if path.is_file() {
+ let mut matches = false;
- match (current_time, previous_time) {
- (Some(current), Some(prev)) if prev == current => false,
- _ => {
- event!(tracing::Level::INFO, "Discovering {:?}", path);
+ for base in &auto_bases {
+ if path.starts_with(base) {
+ matches = true;
+ break;
+ }
+ }
- true
+ if !matches {
+ for (base, pattern) in &pattern_sources {
+ let remainder = path.strip_prefix(base);
+ if remainder.is_ok_and(|remainder| {
+ let mut path_str = remainder.to_string_lossy().to_string();
+ if !path_str.starts_with("/") {
+ path_str = format!("/{path_str}");
+ }
+ glob_match(pattern, path_str.as_bytes())
+ }) {
+ matches = true;
+ break;
+ }
}
}
+
+ if !matches {
+ return false;
+ }
}
+
+ true
});
Some(builder)
diff --git a/integrations/utils.ts b/integrations/utils.ts
index a2fc0d58af9a..c5d3306c4677 100644
--- a/integrations/utils.ts
+++ b/integrations/utils.ts
@@ -498,6 +498,7 @@ async function overwriteVersionsInPackageJson(content: string): Promise
json.pnpm.overrides['@tailwindcss/cli>tailwindcss'] = resolveVersion(pkg)
json.pnpm.overrides['@tailwindcss/postcss>tailwindcss'] = resolveVersion(pkg)
json.pnpm.overrides['@tailwindcss/vite>tailwindcss'] = resolveVersion(pkg)
+ json.pnpm.overrides['@tailwindcss/webpack>tailwindcss'] = resolveVersion(pkg)
} else {
json.pnpm.overrides[pkg] = resolveVersion(pkg)
}
diff --git a/integrations/vite/astro.test.ts b/integrations/vite/astro.test.ts
index 43406624c316..f828b5071b60 100644
--- a/integrations/vite/astro.test.ts
+++ b/integrations/vite/astro.test.ts
@@ -1,4 +1,4 @@
-import { candidate, fetchStyles, html, js, json, retryAssertion, test, ts } from '../utils'
+import { candidate, css, fetchStyles, html, js, json, retryAssertion, test, ts } from '../utils'
test(
'dev mode',
@@ -129,3 +129,73 @@ test(
await fs.expectFileToContain(files[0][0], [candidate`underline`, candidate`overline`])
},
)
+
+// https://github.com/tailwindlabs/tailwindcss/issues/19677
+test(
+ 'import aliases should work in
+
+
+ Astro
+
+