Compare commits
1 commit
main
...
feat/compa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
436d727093 |
437 changed files with 26769 additions and 96128 deletions
76
.github/CONTRIBUTING.md
vendored
76
.github/CONTRIBUTING.md
vendored
|
|
@ -1,48 +1,12 @@
|
|||
# Contributing
|
||||
|
||||
## Requirements
|
||||
|
||||
Before getting started, ensure your system has access to the following tools:
|
||||
|
||||
- [Node.js](https://nodejs.org/)
|
||||
- [Rustup](https://rustup.rs/)
|
||||
- [pnpm](https://pnpm.io/)
|
||||
|
||||
## Getting started
|
||||
|
||||
```sh
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
|
||||
# Install Rust toolchain and WASM targets
|
||||
rustup default stable
|
||||
rustup target add wasm32-wasip1-threads
|
||||
|
||||
# Build the project
|
||||
pnpm build
|
||||
```
|
||||
|
||||
## Development workflow
|
||||
|
||||
During development, you can run tests in watch mode:
|
||||
|
||||
```sh
|
||||
pnpm tdd
|
||||
```
|
||||
|
||||
The `playgrounds` directory contains example projects you can use to test your changes. To start the Vite playground, use:
|
||||
|
||||
```sh
|
||||
pnpm build && pnpm vite
|
||||
```
|
||||
|
||||
## Bug fixes
|
||||
|
||||
If you've found a bug in Tailwind that you'd like to fix, [submit a pull request](https://github.com/tailwindlabs/tailwindcss/pulls) with your changes. Include a helpful description of the problem and how your changes address it, and provide tests so we can verify the fix works as expected.
|
||||
|
||||
## New features
|
||||
|
||||
If there's a new feature you'd like to see added to Tailwind, [share your idea with us](https://github.com/tailwindlabs/tailwindcss/discussions/new?category=ideas) in our discussion forum to get it on our radar as something to consider for a future release before starting work on it.
|
||||
If there's a new feature you'd like to see added to Tailwind, [share your idea with us](https://github.com/tailwindlabs/tailwindcss/discussions/new?category=ideas) in our discussion forum to get it on our radar as something to consider for a future release.
|
||||
|
||||
**Please note that we don't often accept pull requests for new features.** Adding a new feature to Tailwind requires us to think through the entire problem ourselves to make sure we agree with the proposed API, which means the feature needs to be high on our own priority list for us to be able to give it the attention it needs.
|
||||
|
||||
|
|
@ -50,7 +14,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/tailwindlabs/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/tailwindcss/tailwindcss/blob/main/package.json). You can check your code against these standards by running:
|
||||
|
||||
```sh
|
||||
pnpm run lint
|
||||
|
|
@ -64,40 +28,10 @@ pnpm run format
|
|||
|
||||
## Running tests
|
||||
|
||||
You can run the TypeScript and Rust test suites using the following command:
|
||||
You can run the test suite using the following commands:
|
||||
|
||||
```sh
|
||||
pnpm test
|
||||
pnpm build && pnpm test
|
||||
```
|
||||
|
||||
To run the integration tests, use:
|
||||
|
||||
```sh
|
||||
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:
|
||||
|
||||
```sh
|
||||
pnpm build && pnpm test:ui
|
||||
```
|
||||
|
||||
Please ensure that all tests are passing when submitting a pull request. If you're adding new features to Tailwind CSS, always include tests.
|
||||
|
||||
After a successful build, you can also use the npm package tarballs created inside the `dist/` folder to install your build in other local projects.
|
||||
|
||||
## Pull request process
|
||||
|
||||
When submitting a pull request:
|
||||
|
||||
- Ensure the pull request title and description explain the changes you made and why you made them.
|
||||
- Include a test plan section that outlines how you tested your contributions. We do not accept contributions without tests.
|
||||
- Ensure all tests pass. You can add the tag `[ci-all]` in your pull request description to run the test suites across all platforms.
|
||||
|
||||
When a pull request is created, Tailwind CSS maintainers will be notified automatically.
|
||||
|
||||
## Communication
|
||||
|
||||
- **GitHub discussions**: For feature ideas and general questions
|
||||
- **GitHub issues**: For bug reports
|
||||
- **GitHub pull requests**: For code contributions
|
||||
Please ensure that the tests are passing when submitting a pull request. If you're adding new features to Tailwind, please include tests.
|
||||
|
|
|
|||
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
|
|
@ -1 +0,0 @@
|
|||
custom: ['https://tailwindcss.com/sponsor']
|
||||
22
.github/PULL_REQUEST_TEMPLATE.md
vendored
22
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
|
@ -4,26 +4,8 @@
|
|||
|
||||
**Please ask first before starting work on any significant new features.**
|
||||
|
||||
It's never a fun experience to have your pull request declined after investing a lot of time and effort into a new feature. To avoid this from happening, we request that contributors create a discussion to first discuss any significant new features.
|
||||
It's never a fun experience to have your pull request declined after investing a lot of time and effort into a new feature. To avoid this from happening, we request that contributors create an issue to first discuss any significant new features. This includes things like adding new utilities, creating new at-rules, or adding new component examples to the documentation.
|
||||
|
||||
For more info, check out the contributing guide:
|
||||
|
||||
https://github.com/tailwindlabs/tailwindcss/blob/main/.github/CONTRIBUTING.md
|
||||
|
||||
-->
|
||||
|
||||
## Summary
|
||||
|
||||
<!--
|
||||
|
||||
Provide a summary of the issue and the changes you're making. How does your change solve the problem?
|
||||
|
||||
-->
|
||||
|
||||
## Test plan
|
||||
|
||||
<!--
|
||||
|
||||
Explain how you tested your changes. Include the exact commands that you used to verify the change works and include screenshots/screen recordings of the update behavior in the browser if applicable.
|
||||
https://github.com/tailwindcss/tailwindcss/blob/master/.github/CONTRIBUTING.md
|
||||
|
||||
-->
|
||||
|
|
|
|||
62
.github/workflows/ci.yml
vendored
62
.github/workflows/ci.yml
vendored
|
|
@ -8,18 +8,12 @@ on:
|
|||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
NODE_VERSION: 24
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
node-version: [20]
|
||||
runner:
|
||||
- name: Windows
|
||||
os: windows-latest
|
||||
|
|
@ -27,19 +21,17 @@ jobs:
|
|||
- name: Linux
|
||||
os: namespace-profile-default
|
||||
|
||||
# Playwright 1.62+ dropped WebKit support for macOS 14, and hangs
|
||||
# instead of failing when launching WebKit on a macos-14 runner.
|
||||
- name: macOS
|
||||
os: macos-15
|
||||
os: macos-14
|
||||
|
||||
# Exclude windows and macos from being built on feature branches
|
||||
run-all:
|
||||
- ${{ github.ref == 'refs/heads/main' || contains(github.event.pull_request.body, '[ci-all]') || github.event.pull_request.user.login == 'depfu[bot]' }}
|
||||
on-main-branch:
|
||||
- ${{ github.ref == 'refs/heads/main' }}
|
||||
exclude:
|
||||
- run-all: false
|
||||
- on-main-branch: false
|
||||
runner:
|
||||
name: Windows
|
||||
- run-all: false
|
||||
- on-main-branch: false
|
||||
runner:
|
||||
name: macOS
|
||||
|
||||
|
|
@ -49,21 +41,21 @@ jobs:
|
|||
name: ${{ matrix.runner.name }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Use Node.js ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
|
||||
# Cargo already skips downloading dependencies if they already exist
|
||||
- name: Cache cargo
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
|
|
@ -72,25 +64,17 @@ jobs:
|
|||
|
||||
# Cache the `oxide` Rust build
|
||||
- name: Cache oxide build
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
./target/
|
||||
./crates/node/*.node
|
||||
./crates/node/*.wasm
|
||||
./crates/node/index.d.ts
|
||||
./crates/node/index.js
|
||||
./crates/node/browser.js
|
||||
./crates/node/tailwindcss-oxide.wasi-browser.js
|
||||
./crates/node/tailwindcss-oxide.wasi.cjs
|
||||
./crates/node/wasi-worker-browser.mjs
|
||||
./crates/node/wasi-worker.mjs
|
||||
./crates/node/index.d.ts
|
||||
key: ${{ runner.os }}-oxide-${{ hashFiles('./crates/**/*') }}
|
||||
|
||||
- name: Setup WASM target
|
||||
run: rustup target add wasm32-wasip1-threads
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
run: pnpm install
|
||||
|
||||
- name: Build
|
||||
run: pnpm run build
|
||||
|
|
@ -112,13 +96,9 @@ 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
|
||||
uses: discord-actions/message@5c7149c81a83146e5d01f142be1bf87a61831c4d # v2
|
||||
if: failure() && github.ref == 'refs/heads/main'
|
||||
uses: discord-actions/message@v2
|
||||
with:
|
||||
webhookUrl: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
message: 'The [most recent ${{ github.workflow }} workflow](<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}>) on the `main` branch has failed.'
|
||||
message: 'The [most recent build](<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}>) on the `main` branch has failed.'
|
||||
|
|
|
|||
64
.github/workflows/integration-tests.yml
vendored
64
.github/workflows/integration-tests.yml
vendored
|
|
@ -8,18 +8,13 @@ on:
|
|||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
NODE_VERSION: 24
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
node-version: [20]
|
||||
|
||||
runner:
|
||||
- name: Windows
|
||||
os: windows-latest
|
||||
|
|
@ -35,17 +30,16 @@ jobs:
|
|||
- vite
|
||||
- cli
|
||||
- postcss
|
||||
- oxide
|
||||
- webpack
|
||||
- workers
|
||||
|
||||
# Exclude windows and macos from being built on feature branches
|
||||
run-all:
|
||||
- ${{ github.ref == 'refs/heads/main' || contains(github.event.pull_request.body, '[ci-all]') }}
|
||||
on-main-branch:
|
||||
- ${{ github.ref == 'refs/heads/main' }}
|
||||
exclude:
|
||||
- run-all: false
|
||||
- on-main-branch: false
|
||||
runner:
|
||||
name: Windows
|
||||
- run-all: false
|
||||
- on-main-branch: false
|
||||
runner:
|
||||
name: macOS
|
||||
|
||||
|
|
@ -55,25 +49,21 @@ jobs:
|
|||
name: ${{ matrix.runner.name }} / ${{ matrix.integration }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
|
||||
- run: |
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
- name: Use Node.js ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
|
||||
# Cargo already skips downloading dependencies if they already exist
|
||||
- name: Cache cargo
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
|
|
@ -82,23 +72,15 @@ jobs:
|
|||
|
||||
# Cache the `oxide` Rust build
|
||||
- name: Cache oxide build
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
./target/
|
||||
./crates/node/*.node
|
||||
./crates/node/*.wasm
|
||||
./crates/node/index.d.ts
|
||||
./crates/node/index.js
|
||||
./crates/node/browser.js
|
||||
./crates/node/tailwindcss-oxide.wasi-browser.js
|
||||
./crates/node/tailwindcss-oxide.wasi.cjs
|
||||
./crates/node/wasi-worker-browser.mjs
|
||||
./crates/node/wasi-worker.mjs
|
||||
./crates/node/index.d.ts
|
||||
key: ${{ runner.os }}-oxide-${{ hashFiles('./crates/**/*') }}
|
||||
|
||||
- name: Setup WASM target
|
||||
run: rustup target add wasm32-wasip1-threads
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
|
|
@ -113,13 +95,9 @@ 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
|
||||
uses: discord-actions/message@5c7149c81a83146e5d01f142be1bf87a61831c4d # v2
|
||||
if: failure() && github.ref == 'refs/heads/main'
|
||||
uses: discord-actions/message@v2
|
||||
with:
|
||||
webhookUrl: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
message: 'The [most recent ${{ github.workflow }} workflow](<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}>) on the `main` branch has failed.'
|
||||
message: 'The [most recent build](<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}>) on the `main` branch has failed.'
|
||||
|
|
|
|||
222
.github/workflows/prepare-release.yml
vendored
222
.github/workflows/prepare-release.yml
vendored
|
|
@ -2,29 +2,18 @@ name: Prepare Release
|
|||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dry_run:
|
||||
description: Skip creating the draft GitHub release
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
env:
|
||||
APP_NAME: tailwindcss-oxide
|
||||
NODE_VERSION: 24
|
||||
PNPM_VERSION: '11.9.0'
|
||||
NODE_VERSION: 20
|
||||
OXIDE_LOCATION: ./crates/node
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
|
|
@ -54,97 +43,96 @@ jobs:
|
|||
- os: ubuntu-latest
|
||||
target: x86_64-unknown-linux-gnu
|
||||
strip: strip
|
||||
build-flags: --use-napi-cross
|
||||
container:
|
||||
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian
|
||||
- os: ubuntu-latest
|
||||
target: aarch64-unknown-linux-gnu
|
||||
strip: aarch64-linux-gnu-strip
|
||||
build-flags: --use-napi-cross
|
||||
strip: llvm-strip
|
||||
container:
|
||||
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64
|
||||
- os: ubuntu-latest
|
||||
target: armv7-unknown-linux-gnueabihf
|
||||
strip: arm-linux-gnueabihf-strip
|
||||
build-flags: --use-napi-cross
|
||||
strip: llvm-strip
|
||||
container:
|
||||
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-zig
|
||||
- os: ubuntu-latest
|
||||
target: aarch64-unknown-linux-musl
|
||||
strip-zig: true
|
||||
build-flags: -x
|
||||
strip: aarch64-linux-musl-strip
|
||||
download: true
|
||||
container:
|
||||
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine
|
||||
- os: ubuntu-latest
|
||||
target: x86_64-unknown-linux-musl
|
||||
strip: strip
|
||||
build-flags: -x
|
||||
download: true
|
||||
container:
|
||||
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine
|
||||
|
||||
name: Build ${{ matrix.target }} (oxide)
|
||||
name: Build ${{ matrix.target }} (OXIDE)
|
||||
runs-on: ${{ matrix.os }}
|
||||
container: ${{ matrix.container }}
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||
with:
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Use Node.js ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
package-manager-cache: false
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install gcc-arm-linux-gnueabihf
|
||||
if: ${{ matrix.target == 'armv7-unknown-linux-gnueabihf' }}
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf -y
|
||||
|
||||
- name: Install binutils-aarch64-linux-gnu
|
||||
if: ${{ matrix.target == 'aarch64-unknown-linux-gnu' }}
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install binutils-aarch64-linux-gnu -y
|
||||
|
||||
- uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2
|
||||
if: ${{ contains(matrix.target, 'musl') }}
|
||||
# Cargo already skips downloading dependencies if they already exist
|
||||
- name: Cache cargo
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
version: 0.14.1
|
||||
use-cache: false
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
target/
|
||||
key: ${{ runner.os }}-${{ matrix.target }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Install cargo-zigbuild
|
||||
uses: taiki-e/install-action@65851e10cd6c377f11a60e600abc07cb08643468 # v2
|
||||
if: ${{ contains(matrix.target, 'musl') }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
# Cache the `oxide` Rust build
|
||||
- name: Cache oxide build
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
tool: cargo-zigbuild
|
||||
path: |
|
||||
./oxide/target/
|
||||
./crates/node/*.node
|
||||
./crates/node/index.js
|
||||
./crates/node/index.d.ts
|
||||
key: ${{ runner.os }}-${{ matrix.target }}-oxide-${{ hashFiles('./crates/**/*') }}
|
||||
|
||||
- name: Install Node.JS
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Install Rust (Stable)
|
||||
if: ${{ matrix.download }}
|
||||
run: |
|
||||
rustup default stable
|
||||
|
||||
- name: Setup rust target
|
||||
run: rustup target add ${{ matrix.target }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --ignore-scripts --frozen-lockfile --filter=!./playgrounds/*
|
||||
run: pnpm install --ignore-scripts --filter=!./playgrounds/*
|
||||
|
||||
- name: Build release
|
||||
run: pnpm run --filter ${{ env.OXIDE_LOCATION }} build:platform --target=${{ matrix.target }} ${{ matrix.build-flags }}
|
||||
run: pnpm run --filter ${{ env.OXIDE_LOCATION }} build
|
||||
env:
|
||||
RUST_TARGET: ${{ matrix.target }}
|
||||
JEMALLOC_SYS_WITH_LG_PAGE: ${{ matrix.page-size }}
|
||||
|
||||
- name: Strip debug symbols # https://github.com/rust-lang/rust/issues/46034
|
||||
if: ${{ matrix.strip || matrix.strip-zig }}
|
||||
env:
|
||||
STRIP_COMMAND: ${{ matrix.strip }}
|
||||
STRIP_ZIG: ${{ matrix.strip-zig }}
|
||||
run: |
|
||||
if [ "$STRIP_ZIG" = "true" ]; then
|
||||
for file in ${{ env.OXIDE_LOCATION }}/*.node; do
|
||||
zig objcopy --strip-all "$file" "$file.stripped"
|
||||
mv "$file.stripped" "$file"
|
||||
done
|
||||
exit 0
|
||||
fi
|
||||
|
||||
eval "$STRIP_COMMAND ${{ env.OXIDE_LOCATION }}/*.node"
|
||||
if: ${{ matrix.strip }}
|
||||
run: ${{ matrix.strip }} ${{ env.OXIDE_LOCATION }}/*.node
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: bindings-${{ matrix.target }}
|
||||
path: ${{ env.OXIDE_LOCATION }}/*.node
|
||||
|
|
@ -154,11 +142,9 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build FreeBSD
|
||||
uses: cross-platform-actions/action@cdc9ee69ef84a5f2e59c9058335d9c57bcb4ac86 # v0.25.0
|
||||
uses: cross-platform-actions/action@v0.25.0
|
||||
env:
|
||||
DEBUG: napi:*
|
||||
RUSTUP_HOME: /usr/local/rustup
|
||||
|
|
@ -174,23 +160,23 @@ jobs:
|
|||
shell: bash
|
||||
run: |
|
||||
sudo pkg install -y -f curl node libnghttp2 npm
|
||||
sudo npm install -g pnpm@${{ env.PNPM_VERSION }} --unsafe-perm=true
|
||||
sudo npm install -g pnpm@9.6.0 --unsafe-perm=true
|
||||
curl -sSf https://static.rust-lang.org/rustup/archive/1.27.1/x86_64-unknown-freebsd/rustup-init --output rustup-init
|
||||
chmod +x rustup-init
|
||||
./rustup-init -y --profile minimal
|
||||
source "$HOME/.cargo/env"
|
||||
pnpm install --ignore-scripts --frozen-lockfile --filter=!./playgrounds/* || true
|
||||
pnpm install --ignore-scripts --filter=!./playgrounds/* || true
|
||||
echo "~~~~ rustc --version ~~~~"
|
||||
rustc --version
|
||||
echo "~~~~ node -v ~~~~"
|
||||
node -v
|
||||
echo "~~~~ pnpm --version ~~~~"
|
||||
pnpm --version
|
||||
pnpm run --filter ${{ env.OXIDE_LOCATION }} build:platform
|
||||
pnpm run --filter ${{ env.OXIDE_LOCATION }} build
|
||||
strip -x ${{ env.OXIDE_LOCATION }}/*.node
|
||||
ls -la ${{ env.OXIDE_LOCATION }}
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: bindings-x86_64-unknown-freebsd
|
||||
path: ${{ env.OXIDE_LOCATION }}/*.node
|
||||
|
|
@ -201,17 +187,18 @@ jobs:
|
|||
name: Build and release Tailwind CSS
|
||||
|
||||
permissions:
|
||||
contents: write # Required for creating releases
|
||||
contents: write # for softprops/action-gh-release to create GitHub release
|
||||
# https://docs.npmjs.com/generating-provenance-statements#publishing-packages-with-provenance-via-github-actions
|
||||
id-token: write
|
||||
|
||||
needs:
|
||||
- build
|
||||
- build-freebsd
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 20
|
||||
persist-credentials: false
|
||||
|
||||
- run: git fetch --tags -f
|
||||
|
||||
|
|
@ -220,25 +207,43 @@ jobs:
|
|||
run: |
|
||||
echo "TAG_NAME=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV
|
||||
|
||||
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||
with:
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
- uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Use Node.js ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: 'pnpm'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
package-manager-cache: false
|
||||
|
||||
- name: Setup WASM target
|
||||
run: rustup target add wasm32-wasip1-threads
|
||||
# Cargo already skips downloading dependencies if they already exist
|
||||
- name: Cache cargo
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
target/
|
||||
key: ${{ runner.os }}-${{ matrix.target }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
# Cache the `oxide` Rust build
|
||||
- name: Cache oxide build
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
./oxide/target/
|
||||
./crates/node/*.node
|
||||
./crates/node/index.js
|
||||
./crates/node/index.d.ts
|
||||
key: ${{ runner.os }}-${{ matrix.target }}-oxide-${{ hashFiles('./crates/**/*') }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm --filter=!./playgrounds/* install --frozen-lockfile
|
||||
run: pnpm --filter=!./playgrounds/* install
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: ${{ env.OXIDE_LOCATION }}
|
||||
|
||||
|
|
@ -276,32 +281,25 @@ jobs:
|
|||
echo "$RELEASE_NOTES" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
|
||||
- name: Upload standalone artifacts
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
- name: Upload Standalone Artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: tailwindcss-standalone
|
||||
path: packages/@tailwindcss-standalone/dist/
|
||||
|
||||
- name: Upload npm package tarballs
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
with:
|
||||
name: npm-package-tarballs
|
||||
path: dist/*.tgz
|
||||
|
||||
- name: Prepare GitHub Release
|
||||
if: ${{ !inputs.dry_run }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
gh release create "${TAG_NAME}" \
|
||||
--draft \
|
||||
--title "${TAG_NAME}" \
|
||||
--notes "${RELEASE_NOTES}" \
|
||||
packages/@tailwindcss-standalone/dist/sha256sums.txt \
|
||||
packages/@tailwindcss-standalone/dist/tailwindcss-linux-arm64 \
|
||||
packages/@tailwindcss-standalone/dist/tailwindcss-linux-arm64-musl \
|
||||
packages/@tailwindcss-standalone/dist/tailwindcss-linux-x64 \
|
||||
packages/@tailwindcss-standalone/dist/tailwindcss-linux-x64-musl \
|
||||
packages/@tailwindcss-standalone/dist/tailwindcss-macos-arm64 \
|
||||
packages/@tailwindcss-standalone/dist/tailwindcss-macos-x64 \
|
||||
packages/@tailwindcss-standalone/dist/tailwindcss-windows-x64.exe
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
draft: true
|
||||
tag_name: ${{ env.TAG_NAME }}
|
||||
body: |
|
||||
${{ env.RELEASE_NOTES }}
|
||||
files: |
|
||||
packages/@tailwindcss-standalone/dist/sha256sums.txt
|
||||
packages/@tailwindcss-standalone/dist/tailwindcss-linux-arm64
|
||||
packages/@tailwindcss-standalone/dist/tailwindcss-linux-arm64-musl
|
||||
packages/@tailwindcss-standalone/dist/tailwindcss-linux-x64
|
||||
packages/@tailwindcss-standalone/dist/tailwindcss-linux-x64-musl
|
||||
packages/@tailwindcss-standalone/dist/tailwindcss-macos-arm64
|
||||
packages/@tailwindcss-standalone/dist/tailwindcss-macos-x64
|
||||
packages/@tailwindcss-standalone/dist/tailwindcss-windows-x64.exe
|
||||
|
|
|
|||
290
.github/workflows/release-insiders.yml
vendored
Normal file
290
.github/workflows/release-insiders.yml
vendored
Normal file
|
|
@ -0,0 +1,290 @@
|
|||
name: Release Insiders
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
APP_NAME: tailwindcss-oxide
|
||||
NODE_VERSION: 20
|
||||
OXIDE_LOCATION: ./crates/node
|
||||
RELEASE_CHANNEL: insiders
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
# Windows
|
||||
- os: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
- os: windows-latest
|
||||
target: aarch64-pc-windows-msvc
|
||||
# macOS
|
||||
- os: macos-latest
|
||||
target: x86_64-apple-darwin
|
||||
strip: strip -x # Must use -x on macOS. This produces larger results on linux.
|
||||
- os: macos-latest
|
||||
target: aarch64-apple-darwin
|
||||
page-size: 14
|
||||
strip: strip -x # Must use -x on macOS. This produces larger results on linux.
|
||||
# Android
|
||||
- os: ubuntu-latest
|
||||
target: aarch64-linux-android
|
||||
strip: ${ANDROID_NDK_LATEST_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip
|
||||
- os: ubuntu-latest
|
||||
target: armv7-linux-androideabi
|
||||
strip: ${ANDROID_NDK_LATEST_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip
|
||||
# Linux
|
||||
- os: ubuntu-latest
|
||||
target: x86_64-unknown-linux-gnu
|
||||
strip: strip
|
||||
container:
|
||||
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian
|
||||
- os: ubuntu-latest
|
||||
target: aarch64-unknown-linux-gnu
|
||||
strip: llvm-strip
|
||||
container:
|
||||
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64
|
||||
- os: ubuntu-latest
|
||||
target: armv7-unknown-linux-gnueabihf
|
||||
strip: llvm-strip
|
||||
container:
|
||||
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-zig
|
||||
- os: ubuntu-latest
|
||||
target: aarch64-unknown-linux-musl
|
||||
strip: aarch64-linux-musl-strip
|
||||
download: true
|
||||
container:
|
||||
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine
|
||||
- os: ubuntu-latest
|
||||
target: x86_64-unknown-linux-musl
|
||||
strip: strip
|
||||
download: true
|
||||
container:
|
||||
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine
|
||||
|
||||
name: Build ${{ matrix.target }} (OXIDE)
|
||||
runs-on: ${{ matrix.os }}
|
||||
container: ${{ matrix.container }}
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Use Node.js ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: 'pnpm'
|
||||
|
||||
# Cargo already skips downloading dependencies if they already exist
|
||||
- name: Cache cargo
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
target/
|
||||
key: ${{ runner.os }}-${{ matrix.target }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
# Cache the `oxide` Rust build
|
||||
- name: Cache oxide build
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
./oxide/target/
|
||||
./crates/node/*.node
|
||||
./crates/node/index.js
|
||||
./crates/node/index.d.ts
|
||||
key: ${{ runner.os }}-${{ matrix.target }}-oxide-${{ hashFiles('./crates/**/*') }}
|
||||
|
||||
- name: Install Node.JS
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Install Rust (Stable)
|
||||
if: ${{ matrix.download }}
|
||||
run: |
|
||||
rustup default stable
|
||||
|
||||
- name: Setup rust target
|
||||
run: rustup target add ${{ matrix.target }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --ignore-scripts --filter=!./playgrounds/*
|
||||
|
||||
- name: Build release
|
||||
run: pnpm run --filter ${{ env.OXIDE_LOCATION }} build
|
||||
env:
|
||||
RUST_TARGET: ${{ matrix.target }}
|
||||
JEMALLOC_SYS_WITH_LG_PAGE: ${{ matrix.page-size }}
|
||||
|
||||
- name: Strip debug symbols # https://github.com/rust-lang/rust/issues/46034
|
||||
if: ${{ matrix.strip }}
|
||||
run: ${{ matrix.strip }} ${{ env.OXIDE_LOCATION }}/*.node
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: bindings-${{ matrix.target }}
|
||||
path: ${{ env.OXIDE_LOCATION }}/*.node
|
||||
|
||||
build-freebsd:
|
||||
name: Build x86_64-unknown-freebsd (OXIDE)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build FreeBSD
|
||||
uses: cross-platform-actions/action@v0.25.0
|
||||
env:
|
||||
DEBUG: napi:*
|
||||
RUSTUP_HOME: /usr/local/rustup
|
||||
CARGO_HOME: /usr/local/cargo
|
||||
RUSTUP_IO_THREADS: 1
|
||||
RUST_TARGET: x86_64-unknown-freebsd
|
||||
with:
|
||||
operating_system: freebsd
|
||||
version: '14.0'
|
||||
memory: 13G
|
||||
cpu_count: 3
|
||||
environment_variables: 'DEBUG RUSTUP_IO_THREADS'
|
||||
shell: bash
|
||||
run: |
|
||||
sudo pkg install -y -f curl node libnghttp2 npm
|
||||
sudo npm install -g pnpm@9.6.0 --unsafe-perm=true
|
||||
curl -sSf https://static.rust-lang.org/rustup/archive/1.27.1/x86_64-unknown-freebsd/rustup-init --output rustup-init
|
||||
chmod +x rustup-init
|
||||
./rustup-init -y --profile minimal
|
||||
source "$HOME/.cargo/env"
|
||||
echo "~~~~ rustc --version ~~~~"
|
||||
rustc --version
|
||||
echo "~~~~ node -v ~~~~"
|
||||
node -v
|
||||
echo "~~~~ pnpm --version ~~~~"
|
||||
pnpm --version
|
||||
pnpm install --ignore-scripts --filter=!./playgrounds/* || true
|
||||
pnpm run --filter ${{ env.OXIDE_LOCATION }} build
|
||||
strip -x ${{ env.OXIDE_LOCATION }}/*.node
|
||||
ls -la ${{ env.OXIDE_LOCATION }}
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: bindings-x86_64-unknown-freebsd
|
||||
path: ${{ env.OXIDE_LOCATION }}/*.node
|
||||
|
||||
release:
|
||||
runs-on: macos-14
|
||||
timeout-minutes: 15
|
||||
name: Build and release Tailwind CSS insiders
|
||||
|
||||
permissions:
|
||||
contents: write # for softprops/action-gh-release to create GitHub release
|
||||
# https://docs.npmjs.com/generating-provenance-statements#publishing-packages-with-provenance-via-github-actions
|
||||
id-token: write
|
||||
|
||||
needs:
|
||||
- build
|
||||
- build-freebsd
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 20
|
||||
|
||||
- name: Resolve version
|
||||
id: vars
|
||||
run: |
|
||||
echo "SHA_SHORT=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Use Node.js ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: 'pnpm'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
# Cargo already skips downloading dependencies if they already exist
|
||||
- name: Cache cargo
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
target/
|
||||
key: ${{ runner.os }}-${{ matrix.target }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
# Cache the `oxide` Rust build
|
||||
- name: Cache oxide build
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
./oxide/target/
|
||||
./crates/node/*.node
|
||||
./crates/node/index.js
|
||||
./crates/node/index.d.ts
|
||||
key: ${{ runner.os }}-${{ matrix.target }}-oxide-${{ hashFiles('./crates/**/*') }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm --filter=!./playgrounds/* install
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: ${{ env.OXIDE_LOCATION }}
|
||||
|
||||
- name: Move artifacts
|
||||
run: |
|
||||
cd ${{ env.OXIDE_LOCATION }}
|
||||
cp bindings-x86_64-pc-windows-msvc/* ./npm/win32-x64-msvc/
|
||||
cp bindings-aarch64-pc-windows-msvc/* ./npm/win32-arm64-msvc/
|
||||
cp bindings-x86_64-apple-darwin/* ./npm/darwin-x64/
|
||||
cp bindings-aarch64-apple-darwin/* ./npm/darwin-arm64/
|
||||
cp bindings-aarch64-linux-android/* ./npm/android-arm64/
|
||||
cp bindings-armv7-linux-androideabi/* ./npm/android-arm-eabi/
|
||||
cp bindings-aarch64-unknown-linux-gnu/* ./npm/linux-arm64-gnu/
|
||||
cp bindings-aarch64-unknown-linux-musl/* ./npm/linux-arm64-musl/
|
||||
cp bindings-armv7-unknown-linux-gnueabihf/* ./npm/linux-arm-gnueabihf/
|
||||
cp bindings-x86_64-unknown-linux-gnu/* ./npm/linux-x64-gnu/
|
||||
cp bindings-x86_64-unknown-linux-musl/* ./npm/linux-x64-musl/
|
||||
cp bindings-x86_64-unknown-freebsd/* ./npm/freebsd-x64/
|
||||
|
||||
- name: 'Version based on commit: 0.0.0-${{ env.RELEASE_CHANNEL }}.${{ env.SHA_SHORT }}'
|
||||
run: pnpm run version-packages 0.0.0-${{ env.RELEASE_CHANNEL }}.${{ env.SHA_SHORT }}
|
||||
|
||||
- name: Build Tailwind CSS
|
||||
run: pnpm run build
|
||||
|
||||
- name: Run pre-publish optimizations scripts
|
||||
run: node ./scripts/pre-publish-optimizations.mjs
|
||||
|
||||
- name: Lock pre-release versions
|
||||
run: node ./scripts/lock-pre-release-versions.mjs
|
||||
|
||||
- name: Publish
|
||||
run: pnpm --recursive publish --tag ${{ env.RELEASE_CHANNEL }} --no-git-checks
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Trigger Tailwind Play update
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.TAILWIND_PLAY_TOKEN }}
|
||||
script: |
|
||||
await github.rest.actions.createWorkflowDispatch({
|
||||
owner: 'tailwindlabs',
|
||||
repo: 'play.tailwindcss.com',
|
||||
ref: 'master',
|
||||
workflow_id: 'upgrade-tailwindcss.yml'
|
||||
})
|
||||
274
.github/workflows/release.yml
vendored
274
.github/workflows/release.yml
vendored
|
|
@ -1,34 +1,18 @@
|
|||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
channel:
|
||||
description: Release channel to publish
|
||||
required: true
|
||||
default: insiders
|
||||
type: choice
|
||||
options:
|
||||
- insiders
|
||||
- release
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
APP_NAME: tailwindcss-oxide
|
||||
NODE_VERSION: 24
|
||||
PNPM_VERSION: '11.9.0'
|
||||
NODE_VERSION: 20
|
||||
OXIDE_LOCATION: ./crates/node
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
|
|
@ -58,97 +42,96 @@ jobs:
|
|||
- os: ubuntu-latest
|
||||
target: x86_64-unknown-linux-gnu
|
||||
strip: strip
|
||||
build-flags: --use-napi-cross
|
||||
container:
|
||||
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian
|
||||
- os: ubuntu-latest
|
||||
target: aarch64-unknown-linux-gnu
|
||||
strip: aarch64-linux-gnu-strip
|
||||
build-flags: --use-napi-cross
|
||||
strip: llvm-strip
|
||||
container:
|
||||
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64
|
||||
- os: ubuntu-latest
|
||||
target: armv7-unknown-linux-gnueabihf
|
||||
strip: arm-linux-gnueabihf-strip
|
||||
build-flags: --use-napi-cross
|
||||
strip: llvm-strip
|
||||
container:
|
||||
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-zig
|
||||
- os: ubuntu-latest
|
||||
target: aarch64-unknown-linux-musl
|
||||
strip-zig: true
|
||||
build-flags: -x
|
||||
strip: aarch64-linux-musl-strip
|
||||
download: true
|
||||
container:
|
||||
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine
|
||||
- os: ubuntu-latest
|
||||
target: x86_64-unknown-linux-musl
|
||||
strip: strip
|
||||
build-flags: -x
|
||||
download: true
|
||||
container:
|
||||
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine
|
||||
|
||||
name: Build ${{ matrix.target }} (oxide)
|
||||
name: Build ${{ matrix.target }} (OXIDE)
|
||||
runs-on: ${{ matrix.os }}
|
||||
container: ${{ matrix.container }}
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||
with:
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Use Node.js ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
package-manager-cache: false
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install gcc-arm-linux-gnueabihf
|
||||
if: ${{ matrix.target == 'armv7-unknown-linux-gnueabihf' }}
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf -y
|
||||
|
||||
- name: Install binutils-aarch64-linux-gnu
|
||||
if: ${{ matrix.target == 'aarch64-unknown-linux-gnu' }}
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install binutils-aarch64-linux-gnu -y
|
||||
|
||||
- uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2
|
||||
if: ${{ contains(matrix.target, 'musl') }}
|
||||
# Cargo already skips downloading dependencies if they already exist
|
||||
- name: Cache cargo
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
version: 0.14.1
|
||||
use-cache: false
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
target/
|
||||
key: ${{ runner.os }}-${{ matrix.target }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Install cargo-zigbuild
|
||||
uses: taiki-e/install-action@65851e10cd6c377f11a60e600abc07cb08643468 # v2
|
||||
if: ${{ contains(matrix.target, 'musl') }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
# Cache the `oxide` Rust build
|
||||
- name: Cache oxide build
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
tool: cargo-zigbuild
|
||||
path: |
|
||||
./oxide/target/
|
||||
./crates/node/*.node
|
||||
./crates/node/index.js
|
||||
./crates/node/index.d.ts
|
||||
key: ${{ runner.os }}-${{ matrix.target }}-oxide-${{ hashFiles('./crates/**/*') }}
|
||||
|
||||
- name: Install Node.JS
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Install Rust (Stable)
|
||||
if: ${{ matrix.download }}
|
||||
run: |
|
||||
rustup default stable
|
||||
|
||||
- name: Setup rust target
|
||||
run: rustup target add ${{ matrix.target }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --ignore-scripts --frozen-lockfile --filter=!./playgrounds/*
|
||||
run: pnpm install --ignore-scripts --filter=!./playgrounds/*
|
||||
|
||||
- name: Build release
|
||||
run: pnpm run --filter ${{ env.OXIDE_LOCATION }} build:platform --target=${{ matrix.target }} ${{ matrix.build-flags }}
|
||||
run: pnpm run --filter ${{ env.OXIDE_LOCATION }} build
|
||||
env:
|
||||
RUST_TARGET: ${{ matrix.target }}
|
||||
JEMALLOC_SYS_WITH_LG_PAGE: ${{ matrix.page-size }}
|
||||
|
||||
- name: Strip debug symbols # https://github.com/rust-lang/rust/issues/46034
|
||||
if: ${{ matrix.strip || matrix.strip-zig }}
|
||||
env:
|
||||
STRIP_COMMAND: ${{ matrix.strip }}
|
||||
STRIP_ZIG: ${{ matrix.strip-zig }}
|
||||
run: |
|
||||
if [ "$STRIP_ZIG" = "true" ]; then
|
||||
for file in ${{ env.OXIDE_LOCATION }}/*.node; do
|
||||
zig objcopy --strip-all "$file" "$file.stripped"
|
||||
mv "$file.stripped" "$file"
|
||||
done
|
||||
exit 0
|
||||
fi
|
||||
|
||||
eval "$STRIP_COMMAND ${{ env.OXIDE_LOCATION }}/*.node"
|
||||
if: ${{ matrix.strip }}
|
||||
run: ${{ matrix.strip }} ${{ env.OXIDE_LOCATION }}/*.node
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: bindings-${{ matrix.target }}
|
||||
path: ${{ env.OXIDE_LOCATION }}/*.node
|
||||
|
|
@ -158,11 +141,9 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build FreeBSD
|
||||
uses: cross-platform-actions/action@cdc9ee69ef84a5f2e59c9058335d9c57bcb4ac86 # v0.25.0
|
||||
uses: cross-platform-actions/action@v0.25.0
|
||||
env:
|
||||
DEBUG: napi:*
|
||||
RUSTUP_HOME: /usr/local/rustup
|
||||
|
|
@ -178,7 +159,7 @@ jobs:
|
|||
shell: bash
|
||||
run: |
|
||||
sudo pkg install -y -f curl node libnghttp2 npm
|
||||
sudo npm install -g pnpm@${{ env.PNPM_VERSION }} --unsafe-perm=true
|
||||
sudo npm install -g pnpm@9.6.0 --unsafe-perm=true
|
||||
curl -sSf https://static.rust-lang.org/rustup/archive/1.27.1/x86_64-unknown-freebsd/rustup-init --output rustup-init
|
||||
chmod +x rustup-init
|
||||
./rustup-init -y --profile minimal
|
||||
|
|
@ -189,12 +170,12 @@ jobs:
|
|||
node -v
|
||||
echo "~~~~ pnpm --version ~~~~"
|
||||
pnpm --version
|
||||
pnpm install --ignore-scripts --frozen-lockfile --filter=!./playgrounds/* || true
|
||||
pnpm run --filter ${{ env.OXIDE_LOCATION }} build:platform
|
||||
pnpm install --ignore-scripts --filter=!./playgrounds/* || true
|
||||
pnpm run --filter ${{ env.OXIDE_LOCATION }} build
|
||||
strip -x ${{ env.OXIDE_LOCATION }}/*.node
|
||||
ls -la ${{ env.OXIDE_LOCATION }}
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: bindings-x86_64-unknown-freebsd
|
||||
path: ${{ env.OXIDE_LOCATION }}/*.node
|
||||
|
|
@ -202,10 +183,10 @@ jobs:
|
|||
release:
|
||||
runs-on: macos-14
|
||||
timeout-minutes: 15
|
||||
name: Build and publish Tailwind CSS
|
||||
name: Build and release Tailwind CSS
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
contents: write # for softprops/action-gh-release to create GitHub release
|
||||
# https://docs.npmjs.com/generating-provenance-statements#publishing-packages-with-provenance-via-github-actions
|
||||
id-token: write
|
||||
|
||||
|
|
@ -214,51 +195,47 @@ jobs:
|
|||
- build-freebsd
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 20
|
||||
persist-credentials: false
|
||||
|
||||
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||
with:
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
- uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Use Node.js ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: 'pnpm'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
package-manager-cache: false
|
||||
|
||||
# npm trusted publishing validates the caller workflow filename, so all npm publishes live here.
|
||||
# This workflow rebuilds the publish artifacts instead of depending on prepare-release.yml.
|
||||
- name: Resolve release metadata
|
||||
env:
|
||||
INPUT_CHANNEL: ${{ github.event.inputs.channel || '' }}
|
||||
run: |
|
||||
if [[ "${{ github.event_name }}" == "release" || "$INPUT_CHANNEL" == "release" ]]; then
|
||||
release_channel=$(node ./scripts/release-channel.js)
|
||||
# Cargo already skips downloading dependencies if they already exist
|
||||
- name: Cache cargo
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
target/
|
||||
key: ${{ runner.os }}-${{ matrix.target }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
echo "RELEASE_KIND=release" >> $GITHUB_ENV
|
||||
echo "RELEASE_CHANNEL=$release_channel" >> $GITHUB_ENV
|
||||
echo "FEATURES_ENV=stable" >> $GITHUB_ENV
|
||||
else
|
||||
sha_short=$(git rev-parse --short HEAD)
|
||||
|
||||
echo "RELEASE_KIND=insiders" >> $GITHUB_ENV
|
||||
echo "RELEASE_CHANNEL=insiders" >> $GITHUB_ENV
|
||||
echo "SHA_SHORT=$sha_short" >> $GITHUB_ENV
|
||||
echo "INSIDERS_VERSION=0.0.0-insiders.$sha_short" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Setup WASM target
|
||||
run: rustup target add wasm32-wasip1-threads
|
||||
# Cache the `oxide` Rust build
|
||||
- name: Cache oxide build
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
./oxide/target/
|
||||
./crates/node/*.node
|
||||
./crates/node/index.js
|
||||
./crates/node/index.d.ts
|
||||
key: ${{ runner.os }}-${{ matrix.target }}-oxide-${{ hashFiles('./crates/**/*') }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm --filter=!./playgrounds/* install --frozen-lockfile
|
||||
run: pnpm --filter=!./playgrounds/* install
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: ${{ env.OXIDE_LOCATION }}
|
||||
|
||||
|
|
@ -278,19 +255,10 @@ jobs:
|
|||
cp bindings-x86_64-unknown-linux-musl/* ./npm/linux-x64-musl/
|
||||
cp bindings-x86_64-unknown-freebsd/* ./npm/freebsd-x64/
|
||||
|
||||
- name: 'Version based on commit: ${{ env.INSIDERS_VERSION }}'
|
||||
if: env.RELEASE_KIND == 'insiders'
|
||||
run: pnpm run version-packages ${INSIDERS_VERSION}
|
||||
|
||||
- name: Build Tailwind CSS
|
||||
if: env.RELEASE_KIND == 'insiders'
|
||||
run: pnpm run build
|
||||
|
||||
- name: Build Tailwind CSS
|
||||
if: env.RELEASE_KIND == 'release'
|
||||
run: pnpm run build
|
||||
env:
|
||||
FEATURES_ENV: ${{ env.FEATURES_ENV }}
|
||||
FEATURES_ENV: stable
|
||||
|
||||
- name: Run pre-publish optimizations scripts
|
||||
run: node ./scripts/pre-publish-optimizations.mjs
|
||||
|
|
@ -298,61 +266,25 @@ jobs:
|
|||
- name: Lock pre-release versions
|
||||
run: node ./scripts/lock-pre-release-versions.mjs
|
||||
|
||||
- name: Upload npm package tarballs
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
with:
|
||||
name: npm-package-tarballs
|
||||
path: dist/*.tgz
|
||||
- name: Calculate environment variables
|
||||
run: |
|
||||
echo "RELEASE_CHANNEL=$(node ./scripts/release-channel.js)" >> $GITHUB_ENV
|
||||
echo "TAILWINDCSS_VERSION=$(node -e 'console.log(require(`./packages/tailwindcss/package.json`).version);')" >> $GITHUB_ENV
|
||||
|
||||
- name: Publish
|
||||
run: |
|
||||
pnpm --recursive --filter="!@tailwindcss/oxide-wasm32-wasi" publish --tag ${RELEASE_CHANNEL} --no-git-checks
|
||||
# The wasm package needs a special npm config that isn't read when pnpm --recursive is used
|
||||
pushd crates/node/npm/wasm32-wasi; pnpm publish --tag ${RELEASE_CHANNEL} --no-git-checks --config.node-linker=hoisted; popd;
|
||||
run: pnpm --recursive publish --tag ${{ env.RELEASE_CHANNEL }} --no-git-checks
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Trigger Tailwind Play update
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
if: env.RELEASE_CHANNEL == 'latest'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.TAILWIND_PLAY_TOKEN }}
|
||||
script: |
|
||||
await github.rest.actions.createWorkflowDispatch({
|
||||
owner: 'tailwindlabs',
|
||||
repo: 'upgrades',
|
||||
ref: 'main',
|
||||
repo: 'play.tailwindcss.com',
|
||||
ref: 'master',
|
||||
workflow_id: 'upgrade-tailwindcss.yml'
|
||||
})
|
||||
|
||||
notify:
|
||||
if: ${{ always() && (needs.build.result == 'failure' || needs.build-freebsd.result == 'failure' || needs.release.result == 'failure') }}
|
||||
needs:
|
||||
- build
|
||||
- build-freebsd
|
||||
- release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Resolve release label
|
||||
id: release
|
||||
env:
|
||||
INPUT_CHANNEL: ${{ github.event.inputs.channel || '' }}
|
||||
RELEASE_TAG: ${{ github.event.release.tag_name || '' }}
|
||||
run: |
|
||||
if [[ "${{ github.event_name }}" == "release" ]]; then
|
||||
tag_name="${RELEASE_TAG:-${GITHUB_REF_NAME}}"
|
||||
echo "label=release ${tag_name}" >> $GITHUB_OUTPUT
|
||||
elif [[ "$INPUT_CHANNEL" == "release" ]]; then
|
||||
version=$(node -p "require('./packages/tailwindcss/package.json').version")
|
||||
echo "label=release v${version}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
sha_short=$(git rev-parse --short HEAD)
|
||||
echo "label=insiders release 0.0.0-insiders.${sha_short}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Notify Discord
|
||||
uses: discord-actions/message@5c7149c81a83146e5d01f142be1bf87a61831c4d # v2
|
||||
with:
|
||||
webhookUrl: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
message: 'The [most recent ${{ github.workflow }} workflow](<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}>) for `${{ steps.release.outputs.label }}` has failed.'
|
||||
|
|
|
|||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -7,4 +7,4 @@ playwright-report/
|
|||
blob-report/
|
||||
playwright/.cache/
|
||||
target/
|
||||
.debug/
|
||||
.debug
|
||||
|
|
|
|||
1
.npmrc
Normal file
1
.npmrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
auto-install-peers = true
|
||||
|
|
@ -4,6 +4,5 @@ pnpm-lock.yaml
|
|||
target/
|
||||
crates/node/index.d.ts
|
||||
crates/node/index.js
|
||||
crates/ignore/
|
||||
.next
|
||||
.fingerprint
|
||||
|
|
|
|||
616
CHANGELOG.md
616
CHANGELOG.md
|
|
@ -9,580 +9,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
### Added
|
||||
|
||||
- Add `@tailwindcss/turbopack` package to run Tailwind CSS with Next.js ([20367](https://github.com/tailwindlabs/tailwindcss/pull/20367))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Ensure watch mode detects changes to symlinked `@source` files whose real paths aren't otherwise scanned ([#20356](https://github.com/tailwindlabs/tailwindcss/pull/20356))
|
||||
- Ensure custom variants using `@scope` wrap the generated utilities instead of nesting inside them ([#20369](https://github.com/tailwindlabs/tailwindcss/pull/20369))
|
||||
- Fix flattening of `@scope` at-rules ([#20369](https://github.com/tailwindlabs/tailwindcss/pull/20369))
|
||||
- Fix standalone declarations in `@scope`, wrap them in `:where(:scope)` ([#20369](https://github.com/tailwindlabs/tailwindcss/pull/20369))
|
||||
- Always emit a space for empty fallback values in CSS variables (e.g. `var(--tw-blur,)` → `var(--tw-blur, )`) ([#20373](https://github.com/tailwindlabs/tailwindcss/pull/20373))
|
||||
- Canonicalization: convert arbitrary breakpoint and container query variants to named equivalents (e.g. `max-[64rem]` → `max-lg`) ([#20380](https://github.com/tailwindlabs/tailwindcss/pull/20380))
|
||||
- Prevent `@tailwindcss/vite` from crashing on every edit under Vite's experimental `bundledDev` mode ([#20379](https://github.com/tailwindlabs/tailwindcss/pull/20379))
|
||||
- Ensure `@tailwindcss/oxide` falls back to WASM on platforms without native bindings ([#20383](https://github.com/tailwindlabs/tailwindcss/pull/20383))
|
||||
- Detect classes in Ruby percent literals using angle brackets or custom delimiters (e.g. `%w<flex>`, `%w|flex|`), including in Slim and Haml templates ([#20387](https://github.com/tailwindlabs/tailwindcss/pull/20387))
|
||||
- Preserve whitespace in `--default(…)` values in custom functional utilities (e.g. `--default(box alphabetic)` no longer becomes `boxalphabetic`) ([#20392](https://github.com/tailwindlabs/tailwindcss/pull/20392))
|
||||
- Don't scan gitignored directories (e.g. `node_modules` and `.git`) when the project uses a safelist-style `.gitignore` (e.g. `/*` followed by `!/…` negations) ([#20397](https://github.com/tailwindlabs/tailwindcss/pull/20397))
|
||||
- Ensure root `theme('…')` namespace lookups in JavaScript plugins and config files return the full namespace object instead of the value of its `DEFAULT` key ([#20399](https://github.com/tailwindlabs/tailwindcss/pull/20399))
|
||||
- Skip ignored directories entirely when computing watch globs (`scanner.globs`), instead of walking their full contents on every rebuild ([#20408](https://github.com/tailwindlabs/tailwindcss/pull/20408))
|
||||
- Oxide: drop invalid UTF-8 candidates ([#20389](https://github.com/tailwindlabs/tailwindcss/pull/20389))
|
||||
- `@tailwindcss/vite` no longer forces a full page reload for external files (e.g.: `.php` files) ([#20414](https://github.com/tailwindlabs/tailwindcss/issues/20414))
|
||||
- Canonicalization: don't merge utilities that reference different theme variables set to CSS-wide keywords like `unset` ([#20417](https://github.com/tailwindlabs/tailwindcss/pull/20417))
|
||||
- Don't generate utilities when a modifier is used that would otherwise be silently ignored (e.g. `rounded-sm/[5]`, `shadow-sm/foo`, `stroke-2/50`) ([#20419](https://github.com/tailwindlabs/tailwindcss/pull/20419))
|
||||
- Only normalize top-level `and`, `or`, and `not` keywords in `supports-[…]` variants (e.g. `selector(a: not (.foo))` → `selector(a:not(.foo))`) ([#20420](https://github.com/tailwindlabs/tailwindcss/pull/20420))
|
||||
- Don't warn about Angular's `::ng-deep` and `:host-context()` when optimizing CSS ([#20434](https://github.com/tailwindlabs/tailwindcss/pull/20434))
|
||||
- Don't generate CSS for candidates containing an empty additional modifier (e.g. `bg-red-500/50/` and `group-hover/foo//bar:flex`) ([#20466](https://github.com/tailwindlabs/tailwindcss/pull/20466))
|
||||
- Sort `min-*`, `max-*`, and container query variants with decimal values numerically (e.g. `min-[40.25rem]` before `min-[40.5rem]`) ([#20512](https://github.com/tailwindlabs/tailwindcss/pull/20512))
|
||||
- Ensure CSS comments ending with `\*/` are closed correctly instead of swallowing the CSS that follows (e.g. `/* C:\temp\*/`) ([#20508](https://github.com/tailwindlabs/tailwindcss/pull/20508))
|
||||
- Improve style invalidation performance of `group-*` and `peer-*` variants ([#20513](https://github.com/tailwindlabs/tailwindcss/pull/20513))
|
||||
|
||||
## [4.3.3] - 2026-07-16
|
||||
|
||||
### Fixed
|
||||
|
||||
- Support `--watch --poll[=ms]` in `@tailwindcss/cli` when filesystem events are unreliable or unavailable ([#20297](https://github.com/tailwindlabs/tailwindcss/pull/20297))
|
||||
- Canonicalization: match arbitrary hex colors against theme colors case-insensitively (e.g. `bg-[#fff]` and `bg-[#FFF]` → `bg-white`) ([#20298](https://github.com/tailwindlabs/tailwindcss/pull/20298))
|
||||
- Prevent Preflight from overriding Firefox's native `iframe:focus-visible` outline styles ([#20292](https://github.com/tailwindlabs/tailwindcss/pull/20292))
|
||||
- Ensure `theme('colors.foo')` in JS plugins resolves correctly when both `--color-foo` and `--color-foo-bar` exist ([#20299](https://github.com/tailwindlabs/tailwindcss/pull/20299))
|
||||
- Ensure fractional opacity modifiers work with named shadow sizes like `shadow-sm/12.5`, `text-shadow-sm/12.5`, `drop-shadow-sm/12.5`, and `inset-shadow-sm/12.5` ([#20302](https://github.com/tailwindlabs/tailwindcss/pull/20302))
|
||||
- Parse selectors like `[data-foo]div` as two selectors instead of one ([#20303](https://github.com/tailwindlabs/tailwindcss/pull/20303))
|
||||
- Ensure `@tailwindcss/postcss` rebuilds when a preprocessor like Sass changes the input CSS without changing the input file on disk ([#20310](https://github.com/tailwindlabs/tailwindcss/pull/20310))
|
||||
- Ensure CSS nesting is handled even when Lightning CSS isn't run, such as in `@tailwindcss/browser` and Tailwind Play ([#20124](https://github.com/tailwindlabs/tailwindcss/pull/20124))
|
||||
- Prevent achromatic theme colors from shifting hue when mixed in polar color spaces like `oklch` ([#20314](https://github.com/tailwindlabs/tailwindcss/pull/20314))
|
||||
- Ensure `--spacing(0)` is optimized to `0px` instead of `0` so it remains a `<length>` when used in `calc(…)` ([#20319](https://github.com/tailwindlabs/tailwindcss/pull/20319))
|
||||
- Load `@parcel/watcher` only when needed in `@tailwindcss/cli --watch` mode, so one-off builds and `--watch --poll` work when `@parcel/watcher` can't be loaded ([#20325](https://github.com/tailwindlabs/tailwindcss/pull/20325))
|
||||
- Use explicit platform fonts instead of `system-ui` and `ui-sans-serif` so CJK text respects the page's `lang` attribute on Windows ([#20318](https://github.com/tailwindlabs/tailwindcss/pull/20318))
|
||||
- Prevent `@tailwindcss/upgrade` from rewriting ignored files when run from a subdirectory ([#20329](https://github.com/tailwindlabs/tailwindcss/pull/20329))
|
||||
- Ensure earlier `@source` rules pointing to nested files are scanned when later `@source` rules point to files in parent folders ([#20335](https://github.com/tailwindlabs/tailwindcss/pull/20335))
|
||||
- Prevent `@tailwindcss/vite` from triggering full page reloads when scanned files are processed by Vite but haven't been loaded as modules yet ([#20336](https://github.com/tailwindlabs/tailwindcss/pull/20336))
|
||||
|
||||
## [4.3.2] - 2026-06-26
|
||||
|
||||
### Fixed
|
||||
|
||||
- Support bare spacing values for `auto-rows-*` and `auto-cols-*` utilities (e.g. `auto-rows-12` and `auto-cols-16`) ([#20229](https://github.com/tailwindlabs/tailwindcss/pull/20229))
|
||||
- Prevent `@tailwindcss/cli` in `--watch` mode from crashing on Windows when `@source` points to a directory that doesn't exist ([#20242](https://github.com/tailwindlabs/tailwindcss/pull/20242))
|
||||
- Prevent `@tailwindcss/vite` from crashing in Deno v2.8.x when `context.parentURL` is not a valid URL ([#20245](https://github.com/tailwindlabs/tailwindcss/pull/20245))
|
||||
- Ensure `@tailwindcss/cli` in `--watch` mode rebuilds when the input CSS file changes in an ignored directory ([#20246](https://github.com/tailwindlabs/tailwindcss/pull/20246))
|
||||
- Allow `@variant` rules used in `addBase(…)` to use custom variants defined later ([#20247](https://github.com/tailwindlabs/tailwindcss/pull/20247))
|
||||
- Prevent `@tailwindcss/vite` from crashing during HMR when scanned files or directories are deleted ([#20259](https://github.com/tailwindlabs/tailwindcss/pull/20259))
|
||||
- Generate `font-size` instead of `color` declarations for `text-[--spacing(…)]` ([#20260](https://github.com/tailwindlabs/tailwindcss/pull/20260))
|
||||
- Prevent `@source` patterns from scanning unrelated sibling files and folders ([#20263](https://github.com/tailwindlabs/tailwindcss/pull/20263))
|
||||
- Extract class candidates adjacent to Template Toolkit delimiters like `%]…[%` in `.tt`, `.tt2`, and `.tx` files ([#20269](https://github.com/tailwindlabs/tailwindcss/pull/20269))
|
||||
- Extract class candidates from conditional Maud syntax like `p.text-black[condition]` ([#20269](https://github.com/tailwindlabs/tailwindcss/pull/20269))
|
||||
- Prevent `@position-try` rules from triggering unknown at-rule warnings when optimizing CSS ([#20277](https://github.com/tailwindlabs/tailwindcss/pull/20277))
|
||||
- Support class suggestions for named opacity modifiers from `--opacity` theme values ([#20287](https://github.com/tailwindlabs/tailwindcss/pull/20287))
|
||||
- Prevent type errors in `@tailwindcss/postcss` when used with newer PostCSS patch releases ([#20289](https://github.com/tailwindlabs/tailwindcss/pull/20289))
|
||||
|
||||
## [4.3.1] - 2026-06-12
|
||||
|
||||
### Added
|
||||
|
||||
- Add `--silent` option to suppress output in `@tailwindcss/cli` ([#20100](https://github.com/tailwindlabs/tailwindcss/pull/20100))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Remove deprecation warnings by using `Module#registerHooks` instead of `Module#register` on Node 26+ ([#20028](https://github.com/tailwindlabs/tailwindcss/pull/20028))
|
||||
- Canonicalization: don't crash when plugin utilities throw for unsupported values ([#20052](https://github.com/tailwindlabs/tailwindcss/pull/20052))
|
||||
- Allow `@apply` to be used with CSS mixins ([#19427](https://github.com/tailwindlabs/tailwindcss/pull/19427))
|
||||
- Ensure `not-*` correctly negates `@container` queries, including `style(…)` queries ([#20059](https://github.com/tailwindlabs/tailwindcss/pull/20059))
|
||||
- Ensure `drop-shadow-*` color utilities work with custom shadow values containing `calc(…)` ([#20080](https://github.com/tailwindlabs/tailwindcss/pull/20080))
|
||||
- Fix 'Sourcemap is likely to be incorrect' warnings when using `@tailwindcss/vite` ([#20103](https://github.com/tailwindlabs/tailwindcss/pull/20103))
|
||||
- Ensure `@tailwindcss/webpack` can be installed in Rspack projects without requiring `webpack` as a peer dependency ([#20027](https://github.com/tailwindlabs/tailwindcss/pull/20027))
|
||||
- Canonicalization: don't suggest invalid `calc(…)` expressions (e.g. `px-[calc(1rem+0px)]` → `px-[calc(1rem+0)]`) ([#20127](https://github.com/tailwindlabs/tailwindcss/pull/20127))
|
||||
- Canonicalization: avoid suggesting large spacing-scale values for arbitrary lengths (e.g. `left-[99999px]` → `left-[99999px]`, not `left-24999.75`) ([#20130](https://github.com/tailwindlabs/tailwindcss/pull/20130))
|
||||
- Ensure `@tailwindcss/cli` in `--watch` mode recovers when a tracked dependency is deleted and restored ([#20137](https://github.com/tailwindlabs/tailwindcss/pull/20137))
|
||||
- Ensure standalone `@tailwindcss/cli` binaries are ignored when scanning for class candidates ([#20139](https://github.com/tailwindlabs/tailwindcss/pull/20139))
|
||||
- Ensure class candidates are extracted from Twig `addClass(…)` and `removeClass(…)` calls ([#20198](https://github.com/tailwindlabs/tailwindcss/pull/20198))
|
||||
- Don't crash in the Ruby or Vue preprocessors when scanning files containing invalid UTF-8 bytes ([#19588](https://github.com/tailwindlabs/tailwindcss/pull/19588))
|
||||
- Allow `@variant` to be used inside `addBase` ([#19480](https://github.com/tailwindlabs/tailwindcss/pull/19480))
|
||||
- Ensure `@source` globs with symlinks are preserved ([#20203](https://github.com/tailwindlabs/tailwindcss/pull/20203))
|
||||
- Ensure later `@source` rules can re-include files excluded by earlier `@source not` rules ([#20203](https://github.com/tailwindlabs/tailwindcss/pull/20203))
|
||||
- Upgrade: don't migrate empty class rules to invalid `@utility` rules ([#20205](https://github.com/tailwindlabs/tailwindcss/pull/20205))
|
||||
- Ensure transitions between `inset-shadow-none` and other inset shadows work correctly ([#20208](https://github.com/tailwindlabs/tailwindcss/pull/20208))
|
||||
- Ensure explicitly referenced `@source` directories are scanned even when ignored by git ([#20214](https://github.com/tailwindlabs/tailwindcss/pull/20214))
|
||||
- Ensure `@source` globs ending in `**/*` preserve dynamic path segments to avoid scanning too many files ([#20217](https://github.com/tailwindlabs/tailwindcss/pull/20217))
|
||||
- Canonicalization: don't fold `calc(…)` divisions when the result would require high precision (e.g. `w-[calc(100%/3.5)]` → `w-[calc(100%/3.5)]`, not `w-[28.571428571428573%]`) ([#20221](https://github.com/tailwindlabs/tailwindcss/pull/20221))
|
||||
- Serve ESM type declarations to ESM importers of `@tailwindcss/postcss` ([#20228](https://github.com/tailwindlabs/tailwindcss/pull/20228))
|
||||
|
||||
### Changed
|
||||
|
||||
- Generate `0` instead of `calc(var(--spacing) * 0)` for spacing utilities like `m-0` and `left-0` ([#20196](https://github.com/tailwindlabs/tailwindcss/pull/20196))
|
||||
- Generate `var(--spacing)` instead of `calc(var(--spacing) * 1)` for spacing utilities like `m-1` and `left-1` ([#20196](https://github.com/tailwindlabs/tailwindcss/pull/20196))
|
||||
|
||||
## [4.3.0] - 2026-05-08
|
||||
|
||||
### Added
|
||||
|
||||
- Add `@container-size` utility ([#18901](https://github.com/tailwindlabs/tailwindcss/pull/18901))
|
||||
- Add `scrollbar-{auto,thin,none}` utilities for `scrollbar-width`, and `scrollbar-thumb-*` / `scrollbar-track-*` color utilities for `scrollbar-color` ([#19981](https://github.com/tailwindlabs/tailwindcss/pull/19981), [#20019](https://github.com/tailwindlabs/tailwindcss/pull/20019))
|
||||
- Add `scrollbar-gutter-*` utilities ([#20018](https://github.com/tailwindlabs/tailwindcss/pull/20018))
|
||||
- Add `zoom-*` utilities ([#20020](https://github.com/tailwindlabs/tailwindcss/pull/20020))
|
||||
- Add `tab-*` utilities ([#20022](https://github.com/tailwindlabs/tailwindcss/pull/20022))
|
||||
- Allow using `@variant` with stacked variants (e.g. `@variant hover:focus { … }`) ([#19996](https://github.com/tailwindlabs/tailwindcss/pull/19996))
|
||||
- Allow using `@variant` with compound variants (e.g. `@variant hover, focus { … }`) ([#19996](https://github.com/tailwindlabs/tailwindcss/pull/19996))
|
||||
- Support `--default(…)` in `--value(…)` and `--modifier(…)` for functional `@utility` definitions ([#19989](https://github.com/tailwindlabs/tailwindcss/pull/19989))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Ensure `@plugin` resolves package JavaScript entries instead of browser CSS entries when using `@tailwindcss/vite` ([#19949](https://github.com/tailwindlabs/tailwindcss/pull/19949))
|
||||
- Fix relative `@import` and `@plugin` paths resolving from the wrong directory when using `@tailwindcss/vite` ([#19965](https://github.com/tailwindlabs/tailwindcss/pull/19965))
|
||||
- Ensure CSS files containing `@variant` are processed by `@tailwindcss/vite` ([#19966](https://github.com/tailwindlabs/tailwindcss/pull/19966))
|
||||
- Resolve imports relative to `base` when `result.opts.from` is not provided when using `@tailwindcss/postcss` ([#19980](https://github.com/tailwindlabs/tailwindcss/pull/19980))
|
||||
- Canonicalization: preserve significant `_` whitespace in arbitrary values ([#19986](https://github.com/tailwindlabs/tailwindcss/pull/19986))
|
||||
- Canonicalization: add parentheses when removing whitespace from arbitrary values would hurt readability (e.g. `w-[calc(100%---spacing(60))]` → `w-[calc(100%-(--spacing(60)))]`) ([#19986](https://github.com/tailwindlabs/tailwindcss/pull/19986))
|
||||
- Canonicalization: preserve the original unit in arbitrary values instead of normalizing to base units (e.g. `-mt-[20in]` → `mt-[-20in]`, not `mt-[-1920px]`) ([#19988](https://github.com/tailwindlabs/tailwindcss/pull/19988))
|
||||
- Canonicalization: migrate arbitrary `:has()` variants from `[&:has(…)]` to `has-[…]` ([#19991](https://github.com/tailwindlabs/tailwindcss/pull/19991))
|
||||
- Upgrade: don’t migrate inline `style` attributes (e.g. `style="flex-grow: 1"` → `style="flex-grow: 1"`, not `style="grow: 1"`) ([#19918](https://github.com/tailwindlabs/tailwindcss/pull/19918))
|
||||
- Allow multiple `@utility` definitions with the same name but different value types ([#19777](https://github.com/tailwindlabs/tailwindcss/pull/19777))
|
||||
- Export missing `PluginWithConfig` type from `tailwindcss/plugin` to fix errors when inferring plugin config types ([#19707](https://github.com/tailwindlabs/tailwindcss/pull/19707))
|
||||
- Ensure `start` and `end` legacy utilities without values do not generate CSS ([#20003](https://github.com/tailwindlabs/tailwindcss/pull/20003))
|
||||
- Ensure `--value(…)` is required in functional `@utility` definitions ([#20005](https://github.com/tailwindlabs/tailwindcss/pull/20005))
|
||||
- Canonicalization: preserve required whitespace around operators in negated arbitrary values (e.g. `-left-[(var(--a)+var(--b))]`) ([#20011](https://github.com/tailwindlabs/tailwindcss/pull/20011))
|
||||
|
||||
## [4.2.4] - 2026-04-21
|
||||
|
||||
### Fixed
|
||||
|
||||
- Ensure imports in `@import` and `@plugin` still resolve correctly when using Vite aliases in `@tailwindcss/vite` ([#19947](https://github.com/tailwindlabs/tailwindcss/pull/19947))
|
||||
|
||||
## [4.2.3] - 2026-04-20
|
||||
|
||||
### Fixed
|
||||
|
||||
- Canonicalization: improve canonicalization for `tracking-*` utilities by preferring non-negative utilities (e.g. `-tracking-tighter` → `tracking-wider`) ([#19827](https://github.com/tailwindlabs/tailwindcss/pull/19827))
|
||||
- Fix crash due to invalid characters in candidate (exceeding valid unicode code point range) ([#19829](https://github.com/tailwindlabs/tailwindcss/pull/19829))
|
||||
- Ensure query params in imports are considered unique resources when using `@tailwindcss/webpack` ([#19723](https://github.com/tailwindlabs/tailwindcss/pull/19723))
|
||||
- Canonicalization: collapse arbitrary values into shorthand utilities (e.g. `px-[1.2rem] py-[1.2rem]` → `p-[1.2rem]`) ([#19837](https://github.com/tailwindlabs/tailwindcss/pull/19837))
|
||||
- Canonicalization: collapse `border-{t,b}-*` into `border-y-*`, `border-{l,r}-*` into `border-x-*`, and `border-{t,r,b,l}-*` into `border-*` ([#19842](https://github.com/tailwindlabs/tailwindcss/pull/19842))
|
||||
- Canonicalization: collapse `scroll-m{t,b}-*` into `scroll-my-*`, `scroll-m{l,r}-*` into `scroll-mx-*`, and `scroll-m{t,r,b,l}-*` into `scroll-m-*` ([#19842](https://github.com/tailwindlabs/tailwindcss/pull/19842))
|
||||
- Canonicalization: collapse `scroll-p{t,b}-*` into `scroll-py-*`, `scroll-p{l,r}-*` into `scroll-px-*`, and `scroll-p{t,r,b,l}-*` into `scroll-p-*` ([#19842](https://github.com/tailwindlabs/tailwindcss/pull/19842))
|
||||
- Canonicalization: collapse `overflow-{x,y}-*` into `overflow-*` ([#19842](https://github.com/tailwindlabs/tailwindcss/pull/19842))
|
||||
- Canonicalization: collapse `overscroll-{x,y}-*` into `overscroll-*` ([#19842](https://github.com/tailwindlabs/tailwindcss/pull/19842))
|
||||
- Read from `--placeholder-color` instead of `--background-color` for `placeholder-*` utilities ([#19843](https://github.com/tailwindlabs/tailwindcss/pull/19843))
|
||||
- Upgrade: ensure files are not emptied out when killing the upgrade process while it's running ([#19846](https://github.com/tailwindlabs/tailwindcss/pull/19846))
|
||||
- Upgrade: use `config.content` when migrating from Tailwind CSS v3 to Tailwind CSS v4 ([#19846](https://github.com/tailwindlabs/tailwindcss/pull/19846))
|
||||
- Upgrade: never migrate files that are ignored by git ([#19846](https://github.com/tailwindlabs/tailwindcss/pull/19846))
|
||||
- Add `.env` and `.env.*` to default ignored content files ([#19846](https://github.com/tailwindlabs/tailwindcss/pull/19846))
|
||||
- Canonicalization: migrate `overflow-ellipsis` into `text-ellipsis` ([#19849](https://github.com/tailwindlabs/tailwindcss/pull/19849))
|
||||
- Canonicalization: migrate `start-full` → `inset-s-full`, `start-auto` → `inset-s-auto`, `start-px` → `inset-s-px`, and `start-<number>` → `inset-s-<number>` as well as negative versions ([#19849](https://github.com/tailwindlabs/tailwindcss/pull/19849))
|
||||
- Canonicalization: migrate `end-full` → `inset-e-full`, `end-auto` → `inset-e-auto`, `end-px` → `inset-e-px`, and `end-<number>` → `inset-e-<number>` as well as negative versions ([#19849](https://github.com/tailwindlabs/tailwindcss/pull/19849))
|
||||
- Canonicalization: move the `-` sign inside the arbitrary value `-left-[9rem]` → `left-[-9rem]` ([#19858](https://github.com/tailwindlabs/tailwindcss/pull/19858))
|
||||
- Canonicalization: move the `-` sign outside the arbitrary value `ml-[calc(-1*var(--width))]` → `-ml-(--width)` ([#19858](https://github.com/tailwindlabs/tailwindcss/pull/19858))
|
||||
- Improve performance when scanning JSONL / NDJSON files ([#19862](https://github.com/tailwindlabs/tailwindcss/pull/19862))
|
||||
- Support `NODE_PATH` environment variable in standalone CLI ([#19617](https://github.com/tailwindlabs/tailwindcss/pull/19617))
|
||||
|
||||
## [4.2.2] - 2026-03-18
|
||||
|
||||
### Fixed
|
||||
|
||||
- Don't crash when candidates contain prototype properties like `row-constructor` ([#19725](https://github.com/tailwindlabs/tailwindcss/pull/19725))
|
||||
- Canonicalize `calc(var(--spacing)*…)` expressions into `--spacing(…)` ([#19769](https://github.com/tailwindlabs/tailwindcss/pull/19769))
|
||||
- Fix crash in canonicalization step when handling utilities containing `@property` at-rules (e.g. `shadow-sm border`) ([#19727](https://github.com/tailwindlabs/tailwindcss/pull/19727))
|
||||
- Skip full reload for server only modules scanned by client CSS when using `@tailwindcss/vite` ([#19745](https://github.com/tailwindlabs/tailwindcss/pull/19745))
|
||||
- Add support for Vite 8 in `@tailwindcss/vite` ([#19790](https://github.com/tailwindlabs/tailwindcss/pull/19790))
|
||||
- Improve canonicalization for bare values exceeding default spacing scale suggestions (e.g. `w-1234 h-1234` → `size-1234`) ([#19809](https://github.com/tailwindlabs/tailwindcss/pull/19809))
|
||||
- Fix canonicalization resulting in empty list (e.g. `w-5 h-5 size-5` → `''` instead of `size-5`) ([#19812](https://github.com/tailwindlabs/tailwindcss/pull/19812))
|
||||
- Resolve tsconfig paths to allow for `@import '@/path/to/file';` when using `@tailwindcss/vite` ([#19803](https://github.com/tailwindlabs/tailwindcss/pull/19803))
|
||||
|
||||
## [4.2.1] - 2026-02-23
|
||||
|
||||
### Fixed
|
||||
|
||||
- Allow trailing dash in functional utility names for backwards compatibility ([#19696](https://github.com/tailwindlabs/tailwindcss/pull/19696))
|
||||
- Properly detect classes containing `.` characters within curly braces in MDX files ([#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/19623))
|
||||
|
||||
### Fixed
|
||||
|
||||
- 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))
|
||||
- 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))
|
||||
|
||||
### Deprecated
|
||||
|
||||
- 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
|
||||
|
||||
### Fixed
|
||||
|
||||
- Ensure validation of `source(…)` happens relative to the file it is in ([#19274](https://github.com/tailwindlabs/tailwindcss/pull/19274))
|
||||
- Include filename and line numbers in CSS parse errors ([#19282](https://github.com/tailwindlabs/tailwindcss/pull/19282))
|
||||
- Skip comments in Ruby files when checking for class names ([#19243](https://github.com/tailwindlabs/tailwindcss/pull/19243))
|
||||
- Skip over arbitrary property utilities with a top-level `!` in the value ([#19243](https://github.com/tailwindlabs/tailwindcss/pull/19243))
|
||||
- Support environment API in `@tailwindcss/vite` ([#18970](https://github.com/tailwindlabs/tailwindcss/pull/18970))
|
||||
- Preserve case of theme keys from JS configs and plugins ([#19337](https://github.com/tailwindlabs/tailwindcss/pull/19337))
|
||||
- Write source maps correctly on the CLI when using `--watch` ([#19373](https://github.com/tailwindlabs/tailwindcss/pull/19373))
|
||||
- Handle special defaults (like `ringColor.DEFAULT`) in JS configs ([#19348](https://github.com/tailwindlabs/tailwindcss/pull/19348))
|
||||
- Improve backwards compatibility for `content` theme key from JS configs ([#19381](https://github.com/tailwindlabs/tailwindcss/pull/19381))
|
||||
- Upgrade: Handle `future` and `experimental` config keys ([#19344](https://github.com/tailwindlabs/tailwindcss/pull/19344))
|
||||
- Try to canonicalize any arbitrary utility to a bare value ([#19379](https://github.com/tailwindlabs/tailwindcss/pull/19379))
|
||||
- Validate candidates similarly to Oxide ([#19397](https://github.com/tailwindlabs/tailwindcss/pull/19397))
|
||||
- Canonicalization: combine `text-*` and `leading-*` classes ([#19396](https://github.com/tailwindlabs/tailwindcss/pull/19396))
|
||||
- Correctly handle duplicate CLI arguments ([#19416](https://github.com/tailwindlabs/tailwindcss/pull/19416))
|
||||
- Don’t emit color-mix fallback rules inside `@keyframes` ([#19419](https://github.com/tailwindlabs/tailwindcss/pull/19419))
|
||||
- CLI: Don't hang when output is `/dev/stdout` ([#19421](https://github.com/tailwindlabs/tailwindcss/pull/19421))
|
||||
|
||||
## [3.4.19] - 2025-12-10
|
||||
|
||||
### Fixed
|
||||
|
||||
- Don’t break `sibling-*()` functions when used inside `calc(…)` ([#19335](https://github.com/tailwindlabs/tailwindcss/pull/19335))
|
||||
|
||||
## [4.1.17] - 2025-11-06
|
||||
|
||||
### Fixed
|
||||
|
||||
- Substitute `@variant` inside legacy JS APIs ([#19263](https://github.com/tailwindlabs/tailwindcss/pull/19263))
|
||||
- Prevent occasional crash on Windows when loaded into a worker thread ([#19242](https://github.com/tailwindlabs/tailwindcss/pull/19242))
|
||||
|
||||
## [4.1.16] - 2025-10-23
|
||||
|
||||
### Fixed
|
||||
|
||||
- Discard candidates with an empty data type ([#19172](https://github.com/tailwindlabs/tailwindcss/pull/19172))
|
||||
- Fix canonicalization of arbitrary variants with attribute selectors ([#19176](https://github.com/tailwindlabs/tailwindcss/pull/19176))
|
||||
- Fix invalid colors due to nested `&` ([#19184](https://github.com/tailwindlabs/tailwindcss/pull/19184))
|
||||
- Improve canonicalization for `& > :pseudo` and `& :pseudo` arbitrary variants ([#19178](https://github.com/tailwindlabs/tailwindcss/pull/19178))
|
||||
|
||||
## [4.1.15] - 2025-10-20
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix Safari devtools rendering issue due to `color-mix` fallback ([#19069](https://github.com/tailwindlabs/tailwindcss/pull/19069))
|
||||
- Suppress Lightning CSS warnings about `:deep`, `:slotted`, and `:global` ([#19094](https://github.com/tailwindlabs/tailwindcss/pull/19094))
|
||||
- Fix resolving theme keys when starting with the name of another theme key in JS configs and plugins ([#19097](https://github.com/tailwindlabs/tailwindcss/pull/19097))
|
||||
- Allow named groups in combination with `not-*`, `has-*`, and `in-*` ([#19100](https://github.com/tailwindlabs/tailwindcss/pull/19100))
|
||||
- Prevent important utilities from affecting other utilities ([#19110](https://github.com/tailwindlabs/tailwindcss/pull/19110))
|
||||
- Don’t index into strings with the `theme(…)` function ([#19111](https://github.com/tailwindlabs/tailwindcss/pull/19111))
|
||||
- Fix parsing issue when `\t` is used in at-rules ([#19130](https://github.com/tailwindlabs/tailwindcss/pull/19130))
|
||||
- Upgrade: Canonicalize utilities containing `0` values ([#19095](https://github.com/tailwindlabs/tailwindcss/pull/19095))
|
||||
- Upgrade: Migrate deprecated `break-words` to `wrap-break-word` ([#19157](https://github.com/tailwindlabs/tailwindcss/pull/19157))
|
||||
|
||||
### Changed
|
||||
|
||||
- Remove the `postinstall` script from oxide ([#19149])(https://github.com/tailwindlabs/tailwindcss/pull/19149)
|
||||
|
||||
## [4.1.14] - 2025-10-01
|
||||
|
||||
### Fixed
|
||||
|
||||
- Handle `'` syntax in ClojureScript when extracting classes ([#18888](https://github.com/tailwindlabs/tailwindcss/pull/18888))
|
||||
- Handle `@variant` inside `@custom-variant` ([#18885](https://github.com/tailwindlabs/tailwindcss/pull/18885))
|
||||
- Merge suggestions when using `@utility` ([#18900](https://github.com/tailwindlabs/tailwindcss/pull/18900))
|
||||
- Ensure that file system watchers created when using the CLI are always cleaned up ([#18905](https://github.com/tailwindlabs/tailwindcss/pull/18905))
|
||||
- Do not generate `grid-column` utilities when configuring `grid-column-start` or `grid-column-end` ([#18907](https://github.com/tailwindlabs/tailwindcss/pull/18907))
|
||||
- Do not generate `grid-row` utilities when configuring `grid-row-start` or `grid-row-end` ([#18907](https://github.com/tailwindlabs/tailwindcss/pull/18907))
|
||||
- Prevent duplicate CSS when overwriting a static utility with a theme key ([#18056](https://github.com/tailwindlabs/tailwindcss/pull/18056))
|
||||
- Show Lightning CSS warnings (if any) when optimizing/minifying ([#18918](https://github.com/tailwindlabs/tailwindcss/pull/18918))
|
||||
- Use `default` export condition for `@tailwindcss/vite` ([#18948](https://github.com/tailwindlabs/tailwindcss/pull/18948))
|
||||
- Re-throw errors from PostCSS nodes ([#18373](https://github.com/tailwindlabs/tailwindcss/pull/18373))
|
||||
- Detect classes in markdown inline directives ([#18967](https://github.com/tailwindlabs/tailwindcss/pull/18967))
|
||||
- Ensure files with only `@theme` produce no output when built ([#18979](https://github.com/tailwindlabs/tailwindcss/pull/18979))
|
||||
- Support Maud templates when extracting classes ([#18988](https://github.com/tailwindlabs/tailwindcss/pull/18988))
|
||||
- Upgrade: Do not migrate `variant = 'outline'` during upgrades ([#18922](https://github.com/tailwindlabs/tailwindcss/pull/18922))
|
||||
- Upgrade: Show version mismatch (if any) when running upgrade tool ([#19028](https://github.com/tailwindlabs/tailwindcss/pull/19028))
|
||||
- Upgrade: Ensure first class inside `className` is migrated ([#19031](https://github.com/tailwindlabs/tailwindcss/pull/19031))
|
||||
- Upgrade: Migrate classes inside `*ClassName` and `*Class` attributes ([#19031](https://github.com/tailwindlabs/tailwindcss/pull/19031))
|
||||
|
||||
## [3.4.18] - 2025-10-01
|
||||
|
||||
### Fixed
|
||||
|
||||
- Improve support for raw `supports-[…]` queries in arbitrary values ([#13605](https://github.com/tailwindlabs/tailwindcss/pull/13605))
|
||||
- Fix `require.cache` error when loaded through a TypeScript file in Node 22.18+ ([#18665](https://github.com/tailwindlabs/tailwindcss/pull/18665))
|
||||
- Support `import.meta.resolve(…)` in configs for new enough Node.js versions ([#18938](https://github.com/tailwindlabs/tailwindcss/pull/18938))
|
||||
- Allow using newer versions of `postcss-load-config` for better ESM and TypeScript PostCSS config support with the CLI ([#18938](https://github.com/tailwindlabs/tailwindcss/pull/18938))
|
||||
- Remove irrelevant utility rules when matching important classes ([#19030](https://github.com/tailwindlabs/tailwindcss/pull/19030))
|
||||
|
||||
## [4.1.13] - 2025-09-03
|
||||
|
||||
### Changed
|
||||
|
||||
- Drop warning from browser build ([#18731](https://github.com/tailwindlabs/tailwindcss/issues/18731))
|
||||
- Drop exact duplicate declarations when emitting CSS ([#18809](https://github.com/tailwindlabs/tailwindcss/issues/18809))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Don't transition `visibility` when using `transition` ([#18795](https://github.com/tailwindlabs/tailwindcss/pull/18795))
|
||||
- Discard matched variants with unknown named values ([#18799](https://github.com/tailwindlabs/tailwindcss/pull/18799))
|
||||
- Discard matched variants with non-string values ([#18799](https://github.com/tailwindlabs/tailwindcss/pull/18799))
|
||||
- Show suggestions for known `matchVariant` values ([#18798](https://github.com/tailwindlabs/tailwindcss/pull/18798))
|
||||
- Replace deprecated `clip` with `clip-path` in `sr-only` ([#18769](https://github.com/tailwindlabs/tailwindcss/pull/18769))
|
||||
- Hide internal fields from completions in `matchUtilities` ([#18820](https://github.com/tailwindlabs/tailwindcss/pull/18820))
|
||||
- Ignore `.vercel` folders by default (can be overridden by `@source …` rules) ([#18855](https://github.com/tailwindlabs/tailwindcss/pull/18855))
|
||||
- Consider variants starting with `@-` to be invalid (e.g. `@-2xl:flex`) ([#18869](https://github.com/tailwindlabs/tailwindcss/pull/18869))
|
||||
- Do not allow custom variants to start or end with a `-` or `_` ([#18867](https://github.com/tailwindlabs/tailwindcss/pull/18867), [#18872](https://github.com/tailwindlabs/tailwindcss/pull/18872))
|
||||
- Upgrade: Migrate `aria` theme keys to `@custom-variant` ([#18815](https://github.com/tailwindlabs/tailwindcss/pull/18815))
|
||||
- Upgrade: Migrate `data` theme keys to `@custom-variant` ([#18816](https://github.com/tailwindlabs/tailwindcss/pull/18816))
|
||||
- Upgrade: Migrate `supports` theme keys to `@custom-variant` ([#18817](https://github.com/tailwindlabs/tailwindcss/pull/18817))
|
||||
|
||||
## [4.1.12] - 2025-08-13
|
||||
|
||||
### Fixed
|
||||
|
||||
- Don't consider the global important state in `@apply` ([#18404](https://github.com/tailwindlabs/tailwindcss/pull/18404))
|
||||
- Add missing suggestions for `flex-<number>` utilities ([#18642](https://github.com/tailwindlabs/tailwindcss/pull/18642))
|
||||
- Fix trailing `)` from interfering with extraction in Clojure keywords ([#18345](https://github.com/tailwindlabs/tailwindcss/pull/18345))
|
||||
- Detect classes inside Elixir charlist, word list, and string sigils ([#18432](https://github.com/tailwindlabs/tailwindcss/pull/18432))
|
||||
- Track source locations through `@plugin` and `@config` ([#18345](https://github.com/tailwindlabs/tailwindcss/pull/18345))
|
||||
- Allow boolean values of `process.env.DEBUG` in `@tailwindcss/node` ([#18485](https://github.com/tailwindlabs/tailwindcss/pull/18485))
|
||||
- Ignore consecutive semicolons in the CSS parser ([#18532](https://github.com/tailwindlabs/tailwindcss/pull/18532))
|
||||
- Center the dropdown icon added to an input with a paired datalist by default ([#18511](https://github.com/tailwindlabs/tailwindcss/pull/18511))
|
||||
- Extract candidates in Slang templates ([#18565](https://github.com/tailwindlabs/tailwindcss/pull/18565))
|
||||
- Improve error messages when encountering invalid functional utility names ([#18568](https://github.com/tailwindlabs/tailwindcss/pull/18568))
|
||||
- Discard CSS AST objects with `false` or `undefined` properties ([#18571](https://github.com/tailwindlabs/tailwindcss/pull/18571))
|
||||
- Allow users to disable URL rebasing in `@tailwindcss/postcss` via `transformAssetUrls: false` ([#18321](https://github.com/tailwindlabs/tailwindcss/pull/18321))
|
||||
- Fix false-positive migrations in `addEventListener` and JavaScript variable names ([#18718](https://github.com/tailwindlabs/tailwindcss/pull/18718))
|
||||
- Fix Standalone CLI showing default Bun help when run via symlink on Windows ([#18723](https://github.com/tailwindlabs/tailwindcss/pull/18723))
|
||||
- Read from `--border-color-*` theme keys in `divide-*` utilities for backwards compatibility ([#18704](https://github.com/tailwindlabs/tailwindcss/pull/18704/))
|
||||
- Don't scan `.hdr` and `.exr` files for classes by default ([#18734](https://github.com/tailwindlabs/tailwindcss/pull/18734))
|
||||
|
||||
## [4.1.11] - 2025-06-26
|
||||
|
||||
### Fixed
|
||||
|
||||
- Add heuristic to skip candidate migrations inside `emit(…)` ([#18330](https://github.com/tailwindlabs/tailwindcss/pull/18330))
|
||||
- Extract candidates with variants in Clojure/ClojureScript keywords ([#18338](https://github.com/tailwindlabs/tailwindcss/pull/18338))
|
||||
- Document `--watch=always` in the CLI's usage ([#18337](https://github.com/tailwindlabs/tailwindcss/pull/18337))
|
||||
- Add support for Vite 7 to `@tailwindcss/vite` ([#18384](https://github.com/tailwindlabs/tailwindcss/pull/18384))
|
||||
|
||||
## [4.1.10] - 2025-06-11
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix incorrectly generated CSS when using percentages in arbitrary values with calc (e.g. `w-[calc(100%-var(--offset))]`) ([#18289](https://github.com/tailwindlabs/tailwindcss/pull/18289))
|
||||
|
||||
## [4.1.9] - 2025-06-11
|
||||
|
||||
### Fixed
|
||||
|
||||
- Correctly parse custom properties with strings containing semicolons ([#18251](https://github.com/tailwindlabs/tailwindcss/pull/18251))
|
||||
- Upgrade: Migrate arbitrary modifiers without percentage signs to bare values (e.g. `/[0.16]` → `/16`) ([#18184](https://github.com/tailwindlabs/tailwindcss/pull/18184))
|
||||
- Upgrade: Migrate CSS variable shorthands where fallback value contains function call ([#18184](https://github.com/tailwindlabs/tailwindcss/pull/18184))
|
||||
- Upgrade: Migrate negative arbitrary values to negative bare values (e.g. `mb-[-32rem]` → `-mb-128`) ([#18212](https://github.com/tailwindlabs/tailwindcss/pull/18212))
|
||||
- Upgrade: Do not migrate `blur` in `wire:model.blur` ([#18216](https://github.com/tailwindlabs/tailwindcss/pull/18216))
|
||||
- Don't add spaces around CSS dashed idents when formatting math expressions ([#18220](https://github.com/tailwindlabs/tailwindcss/pull/18220))
|
||||
|
||||
## [4.1.8] - 2025-05-27
|
||||
|
||||
### Added
|
||||
|
||||
- Improve error messages when `@apply` fails ([#18059](https://github.com/tailwindlabs/tailwindcss/pull/18059))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Upgrade: Do not migrate declarations that look like candidates in `<style>` blocks ([#18057](https://github.com/tailwindlabs/tailwindcss/pull/18057), [18068](https://github.com/tailwindlabs/tailwindcss/pull/18068))
|
||||
- Upgrade: Don't error when looking for `tailwindcss` in pnpm monorepos ([#18065](https://github.com/tailwindlabs/tailwindcss/pull/18065))
|
||||
- Upgrade: Don't error when updating dependencies in pnpm monorepos ([#18065](https://github.com/tailwindlabs/tailwindcss/pull/18065))
|
||||
- Upgrade: Migrate deprecated `order-none` to `order-0` ([#18126](https://github.com/tailwindlabs/tailwindcss/pull/18126))
|
||||
- Support Leptos `class:` attributes when extracting classes ([#18093](https://github.com/tailwindlabs/tailwindcss/pull/18093))
|
||||
- Fix "Cannot read properties of undefined" crash on malformed arbitrary value ([#18133](https://github.com/tailwindlabs/tailwindcss/pull/18133))
|
||||
- Upgrade: Migrate `-mt-[0px]` to `mt-[0px]` instead of the other way around ([#18154](https://github.com/tailwindlabs/tailwindcss/pull/18154))
|
||||
- Fix Haml pre-processing crash when there is no `\n` at the end of the file ([#18155](https://github.com/tailwindlabs/tailwindcss/pull/18155))
|
||||
- Ignore `.pnpm-store` folders by default (can be overridden by `@source …` rules) ([#18163](https://github.com/tailwindlabs/tailwindcss/pull/18163))
|
||||
- Fix PostCSS crash when calling `toJSON()` ([#18083](https://github.com/tailwindlabs/tailwindcss/pull/18083))
|
||||
|
||||
## [4.1.7] - 2025-05-15
|
||||
|
||||
### Added
|
||||
|
||||
- Upgrade: Migrate bare values to named values ([#18000](https://github.com/tailwindlabs/tailwindcss/pull/18000))
|
||||
- Upgrade: Added cache to improve template migration performance ([#18025](https://github.com/tailwindlabs/tailwindcss/pull/18025))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Allow `_` before numbers during candidate extraction ([#17961](https://github.com/tailwindlabs/tailwindcss/pull/17961))
|
||||
- Prevent duplicate suggestions when using `@theme` and `@utility` together ([#17675](https://github.com/tailwindlabs/tailwindcss/pull/17675))
|
||||
- Ensure that media queries within `::before` and `::after` pseudo selectors create valid CSS rules in production builds ([#17979](https://github.com/tailwindlabs/tailwindcss/pull/17979))
|
||||
- Ensure that the standalone CLI does not leave temporary files behind ([#17981](https://github.com/tailwindlabs/tailwindcss/pull/17981))
|
||||
- Ensure `-rotate-*` utilities properly negate arbitrary values ([#18014](https://github.com/tailwindlabs/tailwindcss/pull/18014))
|
||||
- Ignore custom variants using `:merge(…)` selectors in legacy JS plugins ([#18020](https://github.com/tailwindlabs/tailwindcss/pull/18020))
|
||||
- Ensure classes containing `.` are properly extracted from Clojure files ([#18038](https://github.com/tailwindlabs/tailwindcss/pull/18038))
|
||||
- Upgrade: Fix error when using `@import … source(…)` ([#17963](https://github.com/tailwindlabs/tailwindcss/pull/17963))
|
||||
- Upgrade: Change casing of utilities with named values to kebab-case to match updated theme variables ([#18017](https://github.com/tailwindlabs/tailwindcss/pull/18017))
|
||||
- Upgrade: Don't migrate strings that match utility names in Vue attribute bindings other than `class` ([#18025](https://github.com/tailwindlabs/tailwindcss/pull/18025))
|
||||
|
||||
## [4.1.6] - 2025-05-09
|
||||
|
||||
### Added
|
||||
|
||||
- Upgrade: Automatically convert arbitrary values to named values when possible (e.g. `h-[1lh]` to `h-lh`) ([#17831](https://github.com/tailwindlabs/tailwindcss/pull/17831), [#17854](https://github.com/tailwindlabs/tailwindcss/pull/17854))
|
||||
- Upgrade: Update dependencies in parallel for improved performance ([#17898](https://github.com/tailwindlabs/tailwindcss/pull/17898))
|
||||
- Add detailed logging about `@source` directives, discovered files and scanned files when using `DEBUG=*` ([#17906](https://github.com/tailwindlabs/tailwindcss/pull/17906), [#17952](https://github.com/tailwindlabs/tailwindcss/pull/17952))
|
||||
- Add support for generating source maps in development ([#17775](https://github.com/tailwindlabs/tailwindcss/pull/17775))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Ensure negative arbitrary `scale` values generate negative values ([#17831](https://github.com/tailwindlabs/tailwindcss/pull/17831))
|
||||
- Fix HAML extraction with embedded Ruby ([#17846](https://github.com/tailwindlabs/tailwindcss/pull/17846))
|
||||
- Don't scan files for utilities when using `@reference` ([#17836](https://github.com/tailwindlabs/tailwindcss/pull/17836))
|
||||
- Fix incorrectly replacing `_` with ` ` in arbitrary modifier shorthand `bg-red-500/(--my_opacity)` ([#17889](https://github.com/tailwindlabs/tailwindcss/pull/17889))
|
||||
- Don't scan `.log` files for classes by default ([#17906](https://github.com/tailwindlabs/tailwindcss/pull/17906))
|
||||
- Ensure that custom utilities applying other custom utilities don't swallow nested `@apply` rules ([#17925](https://github.com/tailwindlabs/tailwindcss/pull/17925))
|
||||
- Download platform specific package if `optionalDependencies` are skipped ([#17929](https://github.com/tailwindlabs/tailwindcss/pull/17929))
|
||||
|
||||
## [4.1.5] - 2025-04-30
|
||||
|
||||
### Added
|
||||
|
||||
- Support using `@tailwindcss/upgrade` to upgrade between versions of v4.\* ([#17717](https://github.com/tailwindlabs/tailwindcss/pull/17717))
|
||||
- Add `h-lh` / `min-h-lh` / `max-h-lh` utilities ([#17790](https://github.com/tailwindlabs/tailwindcss/pull/17790))
|
||||
- Transition `display`, `visibility`, `content-visibility`, `overlay`, and `pointer-events` when using `transition` to simplify `@starting-style` usage ([#17812](https://github.com/tailwindlabs/tailwindcss/pull/17812))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Don't scan `.geojson` or `.db` files for classes by default ([#17700](https://github.com/tailwindlabs/tailwindcss/pull/17700), [#17711](https://github.com/tailwindlabs/tailwindcss/pull/17711))
|
||||
- Hide default shadow suggestions when missing default shadow theme keys ([#17743](https://github.com/tailwindlabs/tailwindcss/pull/17743))
|
||||
- Replace `_` with `.` in theme suggestions for `@utility` if surrounded by digits ([#17733](https://github.com/tailwindlabs/tailwindcss/pull/17733))
|
||||
- Skip `color-mix(…)` when opacity is `100%` ([#17815](https://github.com/tailwindlabs/tailwindcss/pull/17815))
|
||||
- PostCSS: Ensure that errors in imported stylesheets are recoverable ([#17754](https://github.com/tailwindlabs/tailwindcss/pull/17754))
|
||||
- Upgrade: Bump all Tailwind CSS related dependencies during upgrade ([#17763](https://github.com/tailwindlabs/tailwindcss/pull/17763))
|
||||
- Upgrade: Don't add `-` to variants starting with `@` ([#17814](https://github.com/tailwindlabs/tailwindcss/pull/17814))
|
||||
- Upgrade: Don't format stylesheets that didn't change when upgrading ([#17824](https://github.com/tailwindlabs/tailwindcss/pull/17824))
|
||||
|
||||
### Changed
|
||||
|
||||
- Ignore `.hg`, `.svn`, `.venv`, `venv`, `.yarn`, `.next`, `.turbo`, `.parcel-cache`, `__pycache__`, and `.svelte-kit` folders by default (can be overridden by `@source …` rules) ([#17892](https://github.com/tailwindlabs/tailwindcss/pull/17892))
|
||||
- `@source` rules that point inside `.hg`, `.svn`, `.venv`, `venv`, `.yarn`, `.next`, `.turbo`, `.parcel-cache`, `__pycache__`, and `.svelte-kit` folders no longer consider your `.gitignore` rules ([#17892](https://github.com/tailwindlabs/tailwindcss/pull/17892))
|
||||
|
||||
## [4.1.4] - 2025-04-14
|
||||
|
||||
### Added
|
||||
|
||||
- Add experimental `@tailwindcss/oxide-wasm32-wasi` target for running Tailwind in browser environments like StackBlitz ([#17558](https://github.com/tailwindlabs/tailwindcss/pull/17558))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Ensure `color-mix(…)` polyfills do not cause used CSS variables to be removed ([#17555](https://github.com/tailwindlabs/tailwindcss/pull/17555))
|
||||
- Ensure `color-mix(…)` polyfills create fallbacks for theme variables that reference other theme variables ([#17562](https://github.com/tailwindlabs/tailwindcss/pull/17562))
|
||||
- Fix brace expansion in declining ranges like `{10..0..5}` and `{0..10..-5}` ([#17591](https://github.com/tailwindlabs/tailwindcss/pull/17591))
|
||||
- Work around a Chrome rendering bug when using the `skew-*` utilities ([#17627](https://github.com/tailwindlabs/tailwindcss/pull/17627))
|
||||
- Ensure container query variant names can contain hyphens ([#17628](https://github.com/tailwindlabs/tailwindcss/pull/17628))
|
||||
- Ensure `shadow-inherit`, `inset-shadow-inherit`, `drop-shadow-inherit`, and `text-shadow-inherit` inherit the shadow color ([#17647](https://github.com/tailwindlabs/tailwindcss/pull/17647))
|
||||
- Ensure compatibility with array tuples used in `fontSize` JS theme keys ([#17630](https://github.com/tailwindlabs/tailwindcss/pull/17630))
|
||||
- Ensure folders with binary file extensions in their names are scanned for utilities ([#17595](https://github.com/tailwindlabs/tailwindcss/pull/17595))
|
||||
- Upgrade: Convert `fontSize` array tuple syntax to CSS theme variables ([#17630](https://github.com/tailwindlabs/tailwindcss/pull/17630))
|
||||
|
||||
## [4.1.3] - 2025-04-04
|
||||
|
||||
### Fixed
|
||||
|
||||
- Show warning when using unsupported bare value data type in `--value(…)` ([#17464](https://github.com/tailwindlabs/tailwindcss/pull/17464))
|
||||
- PostCSS: Ensure changes to the input CSS file don't generate stale output when using Turbopack ([#17554](https://github.com/tailwindlabs/tailwindcss/pull/17554))
|
||||
- Ensure classes are detected in Ruby's `%w` syntax in Slim templates ([#17557](https://github.com/tailwindlabs/tailwindcss/pull/17557))
|
||||
|
||||
## [4.1.2] - 2025-04-03
|
||||
|
||||
### Fixed
|
||||
|
||||
- Don't rely on the presence of `@layer base` to polyfill `@property` ([#17506](https://github.com/tailwindlabs/tailwindcss/pull/17506))
|
||||
- Support setting multiple inset shadows as arbitrary values ([#17523](https://github.com/tailwindlabs/tailwindcss/pull/17523))
|
||||
- Fix `drop-shadow-*` utilities that are defined with multiple shadows ([#17515](https://github.com/tailwindlabs/tailwindcss/pull/17515))
|
||||
- PostCSS: Fix race condition when two changes are queued concurrently ([#17514](https://github.com/tailwindlabs/tailwindcss/pull/17514))
|
||||
- PostCSS: Ensure files containing `@tailwind utilities` are processed ([#17514](https://github.com/tailwindlabs/tailwindcss/pull/17514))
|
||||
- Ensure the `color-mix(…)` polyfill creates fallbacks even when using colors that cannot be statically analyzed ([#17513](https://github.com/tailwindlabs/tailwindcss/pull/17513))
|
||||
- Fix slow incremental builds with `@tailwindcss/vite` and `@tailwindcss/postscss` (especially on Windows) ([#17511](https://github.com/tailwindlabs/tailwindcss/pull/17511))
|
||||
- Vite: Fix missing CSS file in Qwik setups ([#17533](https://github.com/tailwindlabs/tailwindcss/pull/17533))
|
||||
|
||||
## [4.1.1] - 2025-04-02
|
||||
|
||||
### Fixed
|
||||
|
||||
- Disable padding in `@source inline(…)` brace expansion ([#17491](https://github.com/tailwindlabs/tailwindcss/pull/17491))
|
||||
- Inject polyfills after `@import` and body-less `@layer` ([#17493](https://github.com/tailwindlabs/tailwindcss/pull/17493))
|
||||
- Ensure `@tailwindcss/cli` does not contain an import for `jiti` ([#17502](https://github.com/tailwindlabs/tailwindcss/pull/17502))
|
||||
|
||||
## [4.1.0] - 2025-04-01
|
||||
|
||||
### Added
|
||||
|
||||
- Add `details-content` variant ([#15319](https://github.com/tailwindlabs/tailwindcss/pull/15319))
|
||||
- Add `inverted-colors` variant ([#11693](https://github.com/tailwindlabs/tailwindcss/pull/11693))
|
||||
- Add `noscript` variant ([#11929](https://github.com/tailwindlabs/tailwindcss/pull/11929), [#17431](https://github.com/tailwindlabs/tailwindcss/pull/17431))
|
||||
- Add `items-baseline-last` and `self-baseline-last` utilities ([#13888](https://github.com/tailwindlabs/tailwindcss/pull/13888), [#17476](https://github.com/tailwindlabs/tailwindcss/pull/17476))
|
||||
- Add `pointer-none`, `pointer-coarse`, and `pointer-fine` variants ([#16946](https://github.com/tailwindlabs/tailwindcss/pull/16946))
|
||||
- Add `any-pointer-none`, `any-pointer-coarse`, and `any-pointer-fine` variants ([#16941](https://github.com/tailwindlabs/tailwindcss/pull/16941))
|
||||
- Add safe alignment utilities ([#14607](https://github.com/tailwindlabs/tailwindcss/pull/14607))
|
||||
- Add `user-valid` and `user-invalid` variants ([#12370](https://github.com/tailwindlabs/tailwindcss/pull/12370))
|
||||
- Add `wrap-anywhere`, `wrap-break-word`, and `wrap-normal` utilities ([#12128](https://github.com/tailwindlabs/tailwindcss/pull/12128))
|
||||
- Add `@source inline(…)` and `@source not inline(…)` ([#17147](https://github.com/tailwindlabs/tailwindcss/pull/17147))
|
||||
- Add `@source not "…"` ([#17255](https://github.com/tailwindlabs/tailwindcss/pull/17255))
|
||||
- Add `text-shadow-*` utilities ([#17389](https://github.com/tailwindlabs/tailwindcss/pull/17389))
|
||||
- Add `mask-*` utilities ([#17134](https://github.com/tailwindlabs/tailwindcss/pull/17134))
|
||||
- Add `bg-{position,size}-*` utilities for arbitrary values ([#17432](https://github.com/tailwindlabs/tailwindcss/pull/17432))
|
||||
- Add `shadow-*/<alpha>`, `inset-shadow-*/<alpha>`, `drop-shadow-*/<alpha>`, and `text-shadow-*/<alpha>` utilities to control shadow opacity ([#17398](https://github.com/tailwindlabs/tailwindcss/pull/17398), [#17434](https://github.com/tailwindlabs/tailwindcss/pull/17434))
|
||||
- Add `drop-shadow-<color>` utilities ([#17434](https://github.com/tailwindlabs/tailwindcss/pull/17434))
|
||||
- Improve compatibility with older versions of Safari and Firefox ([#17435](https://github.com/tailwindlabs/tailwindcss/pull/17435))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Follow symlinks when resolving `@source` directives ([#17391](https://github.com/tailwindlabs/tailwindcss/pull/17391))
|
||||
- Don't scan ignored files for classes when changing an ignored file triggers a rebuild using `@tailwindcss/cli` ([#17255](https://github.com/tailwindlabs/tailwindcss/pull/17255))
|
||||
- Support negated `content` rules in legacy JavaScript configuration ([#17255](https://github.com/tailwindlabs/tailwindcss/pull/17255))
|
||||
- Interpret syntax like `@("@")md:…` as `@md:…` in Razor files ([#17427](https://github.com/tailwindlabs/tailwindcss/pull/17427))
|
||||
- Disallow top-level braces, top-level semicolons, and unbalanced parentheses and brackets in arbitrary values ([#17361](https://github.com/tailwindlabs/tailwindcss/pull/17361))
|
||||
- Ensure the `--theme(…)` function still resolves to the CSS variables when using legacy JS plugins ([#17458](https://github.com/tailwindlabs/tailwindcss/pull/17458))
|
||||
- Detect used theme variables in CSS module files ([#17433](https://github.com/tailwindlabs/tailwindcss/pull/17433), [#17467](https://github.com/tailwindlabs/tailwindcss/pull/17467))
|
||||
|
||||
### Changed
|
||||
|
||||
- Ignore `node_modules` by default (can be overridden by `@source …` rules) ([#17255](https://github.com/tailwindlabs/tailwindcss/pull/17255))
|
||||
- `@source` rules that include file extensions or point inside `node_modules/` folders no longer consider your `.gitignore` rules ([#17255](https://github.com/tailwindlabs/tailwindcss/pull/17255))
|
||||
- Deprecate `bg-{left,right}-{top,bottom}` in favor of `bg-{top,bottom}-{left,right}` utilities ([#17378](https://github.com/tailwindlabs/tailwindcss/pull/17378))
|
||||
- Deprecate `object-{left,right}-{top,bottom}` in favor of `object-{top,bottom}-{left,right}` utilities ([#17437](https://github.com/tailwindlabs/tailwindcss/pull/17437))
|
||||
|
||||
## [4.0.17] - 2025-03-26
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix an issue causing the CLI to hang when processing Ruby files ([#17383](https://github.com/tailwindlabs/tailwindcss/pull/17383))
|
||||
|
||||
## [4.0.16] - 2025-03-25
|
||||
|
||||
### Added
|
||||
|
||||
- _Experimental_: Add `details-content` variant ([#15319](https://github.com/tailwindlabs/tailwindcss/pull/15319))
|
||||
- _Experimental_: Add `inverted-colors` variant ([#11693](https://github.com/tailwindlabs/tailwindcss/pull/11693))
|
||||
- _Experimental_: Add `scripting`, `scripting-none`, and `scripting-initial` variants ([#11929](https://github.com/tailwindlabs/tailwindcss/pull/11929))
|
||||
- _Experimental_: Add `items-baseline-last` utility ([#12128](https://github.com/tailwindlabs/tailwindcss/pull/12128))
|
||||
- _Experimental_: Add `pointer-none`, `pointer-coarse`, and `pointer-fine` variant ([#16946](https://github.com/tailwindlabs/tailwindcss/pull/16946))
|
||||
- _Experimental_: Add `any-pointer-none`, `any-pointer-coarse`, and `any-pointer-fine` variants ([#16941](https://github.com/tailwindlabs/tailwindcss/pull/16941))
|
||||
- _Experimental_: Add safe alignment utilities ([#14607](https://github.com/tailwindlabs/tailwindcss/pull/14607))
|
||||
- _Experimental_: Add `user-valid` and `user-invalid` variants ([#12370](https://github.com/tailwindlabs/tailwindcss/pull/12370))
|
||||
- _Experimental_: Add `wrap-anywhere`, `wrap-break-word`, and `wrap-normal` utilities ([#12128](https://github.com/tailwindlabs/tailwindcss/pull/12128))
|
||||
- _Experimental_: Add `@source inline(…)` ([#17147](https://github.com/tailwindlabs/tailwindcss/pull/17147))
|
||||
- Add support for literal values in `--value('…')` and `--modifier('…')` ([#17304](https://github.com/tailwindlabs/tailwindcss/pull/17304))
|
||||
- Add suggestions when `--spacing(--value(integer, number))` is used ([#17308](https://github.com/tailwindlabs/tailwindcss/pull/17308))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix class extraction followed by `(` in Pug ([#17320](https://github.com/tailwindlabs/tailwindcss/pull/17320))
|
||||
- Ensure `@keyframes` for theme animations are emitted if they are referenced following a comma ([#17352](https://github.com/tailwindlabs/tailwindcss/pull/17352))
|
||||
- Vite: Ensure that updates to an imported CSS file are properly propagated after updating source files ([#17347](https://github.com/tailwindlabs/tailwindcss/pull/17347))
|
||||
- Ensure `@keyframes` for theme animations are emitted if they are referenced following a comma
|
||||
- Vite: Ensure that updates to an imported CSS file are properly propagated after updating templates ([#17347](https://github.com/tailwindlabs/tailwindcss/pull/17347))
|
||||
- Pre process `Slim` templates embedded in Ruby files ([#17336](https://github.com/tailwindlabs/tailwindcss/pull/17336))
|
||||
- Error when input and output files resolve to the same file when using the CLI ([#17311](https://github.com/tailwindlabs/tailwindcss/pull/17311))
|
||||
- Add missing suggestions when `--spacing(--value(integer, number))` is used ([#17308](https://github.com/tailwindlabs/tailwindcss/pull/17308))
|
||||
- Add `::-webkit-details-marker` pseudo to `marker` variant ([#17362](https://github.com/tailwindlabs/tailwindcss/pull/17362))
|
||||
|
||||
## [4.0.15] - 2025-03-20
|
||||
### [4.0.15] - 2025-03-20
|
||||
|
||||
### Fixed
|
||||
|
||||
|
|
@ -4026,7 +3474,7 @@ No release notes
|
|||
|
||||
- Lists now have no margins by default
|
||||
- `.pin` no longer sets width and height to 100%
|
||||
- SVG `fill` no longer defaults to currentcolor
|
||||
- SVG `fill` no longer defaults to currentColor
|
||||
|
||||
## [0.2.2] - 2017-11-19
|
||||
|
||||
|
|
@ -4123,39 +3571,7 @@ No release notes
|
|||
|
||||
- Everything!
|
||||
|
||||
[unreleased]: https://github.com/tailwindlabs/tailwindcss/compare/v4.3.3...HEAD
|
||||
[4.3.3]: https://github.com/tailwindlabs/tailwindcss/compare/v4.3.2...v4.3.3
|
||||
[4.3.2]: https://github.com/tailwindlabs/tailwindcss/compare/v4.3.1...v4.3.2
|
||||
[4.3.1]: https://github.com/tailwindlabs/tailwindcss/compare/v4.3.0...v4.3.1
|
||||
[4.3.0]: https://github.com/tailwindlabs/tailwindcss/compare/v4.2.4...v4.3.0
|
||||
[4.2.4]: https://github.com/tailwindlabs/tailwindcss/compare/v4.2.3...v4.2.4
|
||||
[4.2.3]: https://github.com/tailwindlabs/tailwindcss/compare/v4.2.2...v4.2.3
|
||||
[4.2.2]: https://github.com/tailwindlabs/tailwindcss/compare/v4.2.1...v4.2.2
|
||||
[4.2.1]: https://github.com/tailwindlabs/tailwindcss/compare/v4.2.0...v4.2.1
|
||||
[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
|
||||
[4.1.16]: https://github.com/tailwindlabs/tailwindcss/compare/v4.1.15...v4.1.16
|
||||
[4.1.15]: https://github.com/tailwindlabs/tailwindcss/compare/v4.1.14...v4.1.15
|
||||
[4.1.14]: https://github.com/tailwindlabs/tailwindcss/compare/v4.1.13...v4.1.14
|
||||
[3.4.18]: https://github.com/tailwindlabs/tailwindcss/compare/v3.4.17...v3.4.18
|
||||
[4.1.13]: https://github.com/tailwindlabs/tailwindcss/compare/v4.1.12...v4.1.13
|
||||
[4.1.12]: https://github.com/tailwindlabs/tailwindcss/compare/v4.1.11...v4.1.12
|
||||
[4.1.11]: https://github.com/tailwindlabs/tailwindcss/compare/v4.1.10...v4.1.11
|
||||
[4.1.10]: https://github.com/tailwindlabs/tailwindcss/compare/v4.1.9...v4.1.10
|
||||
[4.1.9]: https://github.com/tailwindlabs/tailwindcss/compare/v4.1.8...v4.1.9
|
||||
[4.1.8]: https://github.com/tailwindlabs/tailwindcss/compare/v4.1.7...v4.1.8
|
||||
[4.1.7]: https://github.com/tailwindlabs/tailwindcss/compare/v4.1.6...v4.1.7
|
||||
[4.1.6]: https://github.com/tailwindlabs/tailwindcss/compare/v4.1.5...v4.1.6
|
||||
[4.1.5]: https://github.com/tailwindlabs/tailwindcss/compare/v4.1.4...v4.1.5
|
||||
[4.1.4]: https://github.com/tailwindlabs/tailwindcss/compare/v4.1.3...v4.1.4
|
||||
[4.1.3]: https://github.com/tailwindlabs/tailwindcss/compare/v4.1.2...v4.1.3
|
||||
[4.1.2]: https://github.com/tailwindlabs/tailwindcss/compare/v4.1.1...v4.1.2
|
||||
[4.1.1]: https://github.com/tailwindlabs/tailwindcss/compare/v4.1.0...v4.1.1
|
||||
[4.1.0]: https://github.com/tailwindlabs/tailwindcss/compare/v4.0.17...v4.1.0
|
||||
[4.0.17]: https://github.com/tailwindlabs/tailwindcss/compare/v4.0.16...v4.0.17
|
||||
[4.0.16]: https://github.com/tailwindlabs/tailwindcss/compare/v4.0.15...v4.0.16
|
||||
[unreleased]: https://github.com/tailwindlabs/tailwindcss/compare/v4.0.15...HEAD
|
||||
[4.0.15]: https://github.com/tailwindlabs/tailwindcss/compare/v4.0.14...v4.0.15
|
||||
[4.0.14]: https://github.com/tailwindlabs/tailwindcss/compare/v4.0.13...v4.0.14
|
||||
[4.0.13]: https://github.com/tailwindlabs/tailwindcss/compare/v4.0.12...v4.0.13
|
||||
|
|
|
|||
342
Cargo.lock
generated
342
Cargo.lock
generated
|
|
@ -27,6 +27,21 @@ dependencies = [
|
|||
"nom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bit-set"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
|
||||
dependencies = [
|
||||
"bit-vec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bit-vec"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.6.0"
|
||||
|
|
@ -40,7 +55,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"regex-automata 0.4.18",
|
||||
"regex-automata 0.4.8",
|
||||
"serde",
|
||||
]
|
||||
|
||||
|
|
@ -59,31 +74,33 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "console"
|
||||
version = "0.16.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fe5f465a4f6fee88fad41b85d990f84c835335e85b5d9e6e63e0d06d28cba7c"
|
||||
dependencies = [
|
||||
"encode_unicode",
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.11.0"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49"
|
||||
checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
|
||||
dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.5.15"
|
||||
name = "crossbeam"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
|
||||
checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8"
|
||||
dependencies = [
|
||||
"crossbeam-channel",
|
||||
"crossbeam-deque",
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-queue",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.5.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
|
@ -107,6 +124,15 @@ dependencies = [
|
|||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-queue"
|
||||
version = "0.3.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.20"
|
||||
|
|
@ -115,15 +141,13 @@ checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
|
|||
|
||||
[[package]]
|
||||
name = "ctor"
|
||||
version = "1.0.12"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d83cb7e7a873830708d6b02a78cd36a592c6fa14bf267b68725103b85c0d77f"
|
||||
|
||||
[[package]]
|
||||
name = "diff"
|
||||
version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
|
||||
checksum = "990a40740adf249724a6000c0fc4bd574712f50bb17c2d6f6cec837ae2f0ee75"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dunce"
|
||||
|
|
@ -137,12 +161,6 @@ version = "1.8.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
|
||||
|
||||
[[package]]
|
||||
name = "encode_unicode"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.9"
|
||||
|
|
@ -153,6 +171,17 @@ dependencies = [
|
|||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fancy-regex"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298"
|
||||
dependencies = [
|
||||
"bit-set",
|
||||
"regex-automata 0.4.8",
|
||||
"regex-syntax 0.8.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fast-glob"
|
||||
version = "0.4.3"
|
||||
|
|
@ -168,104 +197,16 @@ version = "2.1.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6"
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-executor",
|
||||
"futures-io",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
|
||||
|
||||
[[package]]
|
||||
name = "futures-executor"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-macro",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"memchr",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "globset"
|
||||
version = "0.4.20"
|
||||
version = "0.4.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07c34a9410465b45bd9787443bc7370f37735bad04b0f0cd57ff1a3186c98988"
|
||||
checksum = "15f1ce686646e7f1e19bf7d5533fe443a45dbfb990e00629110797578b42fb19"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"bstr",
|
||||
"log",
|
||||
"regex-automata 0.4.18",
|
||||
"regex-automata 0.4.8",
|
||||
"regex-syntax 0.8.5",
|
||||
]
|
||||
|
||||
|
|
@ -276,7 +217,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"ignore 0.4.23",
|
||||
"ignore",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
|
|
@ -290,41 +231,12 @@ dependencies = [
|
|||
"globset",
|
||||
"log",
|
||||
"memchr",
|
||||
"regex-automata 0.4.18",
|
||||
"regex-automata 0.4.8",
|
||||
"same-file",
|
||||
"walkdir",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ignore"
|
||||
version = "0.4.33"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"crossbeam-channel",
|
||||
"crossbeam-deque",
|
||||
"dunce",
|
||||
"globset",
|
||||
"log",
|
||||
"memchr",
|
||||
"regex-automata 0.4.18",
|
||||
"same-file",
|
||||
"walkdir",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "insta"
|
||||
version = "1.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "86f0f8fee8c926415c58d6ae43a08523a26faccb2323f5e6b644fe7dd4ef6b82"
|
||||
dependencies = [
|
||||
"console",
|
||||
"once_cell",
|
||||
"similar",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.11.0"
|
||||
|
|
@ -348,12 +260,12 @@ checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5"
|
|||
|
||||
[[package]]
|
||||
name = "libloading"
|
||||
version = "0.9.0"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "754ca22de805bb5744484a5b151a9e1a8e837d5dc232c2d7d8c2e3492edc8b60"
|
||||
checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-link",
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -391,33 +303,31 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
|||
|
||||
[[package]]
|
||||
name = "napi"
|
||||
version = "3.11.0"
|
||||
version = "2.16.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de33522036981030a75c231829566bc63414e08101a6f5ff4ac6cef19c8e0941"
|
||||
checksum = "839ae2ee5e62c6348669c50098b187c08115bd3cced658c9c0bf945fca0fec83"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"ctor",
|
||||
"futures",
|
||||
"napi-build",
|
||||
"napi-derive",
|
||||
"napi-sys",
|
||||
"nohash-hasher",
|
||||
"rustc-hash",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "napi-build"
|
||||
version = "2.3.2"
|
||||
version = "2.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c9c366d2c8c60b86fa632df75f745509b52f9128f91a6bad4c796e44abb505e1"
|
||||
checksum = "db836caddef23662b94e16bf1f26c40eceb09d6aee5d5b06a7ac199320b69b19"
|
||||
|
||||
[[package]]
|
||||
name = "napi-derive"
|
||||
version = "3.6.0"
|
||||
version = "2.16.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a49c513341a61a16a10af6efcce46b30d0822ba2d4fb197d24d33dfc199c78d5"
|
||||
checksum = "7cbe2585d8ac223f7d34f13701434b9d5f4eb9c332cccce8dee57ea18ab8ab0c"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"convert_case",
|
||||
"ctor",
|
||||
"napi-derive-backend",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
@ -426,32 +336,28 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "napi-derive-backend"
|
||||
version = "6.1.1"
|
||||
version = "1.0.75"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d60b5d773ad46c698c8cc2cd9fde0b283d39cbb7f71c04bee633c7bdba4423bd"
|
||||
checksum = "1639aaa9eeb76e91c6ae66da8ce3e89e921cd3885e99ec85f4abacae72fc91bf"
|
||||
dependencies = [
|
||||
"convert_case",
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"semver",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "napi-sys"
|
||||
version = "3.3.0"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85fbf1fa9f1babfe396d74bbbf52b3643770243e8f5b0b46715d4caf7f0dfc9a"
|
||||
checksum = "427802e8ec3a734331fec1035594a210ce1ff4dc5bc1950530920ab717964ea3"
|
||||
dependencies = [
|
||||
"libloading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nohash-hasher"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451"
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
|
|
@ -474,9 +380,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
version = "1.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||
|
||||
[[package]]
|
||||
name = "overload"
|
||||
|
|
@ -490,16 +396,6 @@ version = "0.2.9"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
|
||||
|
||||
[[package]]
|
||||
name = "pretty_assertions"
|
||||
version = "1.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d"
|
||||
dependencies = [
|
||||
"diff",
|
||||
"yansi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.86"
|
||||
|
|
@ -511,18 +407,18 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
version = "1.0.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||
checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.12.0"
|
||||
version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
|
||||
checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
|
||||
dependencies = [
|
||||
"either",
|
||||
"rayon-core",
|
||||
|
|
@ -530,9 +426,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "rayon-core"
|
||||
version = "1.13.0"
|
||||
version = "1.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
|
||||
checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
|
||||
dependencies = [
|
||||
"crossbeam-deque",
|
||||
"crossbeam-utils",
|
||||
|
|
@ -546,7 +442,7 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
|
|||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata 0.4.18",
|
||||
"regex-automata 0.4.8",
|
||||
"regex-syntax 0.8.5",
|
||||
]
|
||||
|
||||
|
|
@ -561,9 +457,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.18"
|
||||
version = "0.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
|
||||
checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
|
|
@ -631,18 +527,6 @@ dependencies = [
|
|||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "similar"
|
||||
version = "2.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.10.0"
|
||||
|
|
@ -651,9 +535,9 @@ checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
|
|||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.87"
|
||||
version = "2.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d"
|
||||
checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
@ -678,20 +562,19 @@ dependencies = [
|
|||
"bexpand",
|
||||
"bstr",
|
||||
"classification-macros",
|
||||
"crossbeam",
|
||||
"dunce",
|
||||
"fancy-regex",
|
||||
"fast-glob",
|
||||
"globwalk",
|
||||
"ignore 0.4.33",
|
||||
"insta",
|
||||
"ignore",
|
||||
"log",
|
||||
"pretty_assertions",
|
||||
"rayon",
|
||||
"regex",
|
||||
"rustc-hash",
|
||||
"tempfile",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"unicode-width",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
|
|
@ -791,12 +674,6 @@ version = "1.10.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.0"
|
||||
|
|
@ -844,12 +721,6 @@ version = "0.4.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
|
|
@ -868,15 +739,6 @@ dependencies = [
|
|||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
|
|
@ -940,9 +802,3 @@ name = "windows_x86_64_msvc"
|
|||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "yansi"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
|
||||
|
|
|
|||
16
README.md
16
README.md
|
|
@ -13,10 +13,10 @@
|
|||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/tailwindlabs/tailwindcss/actions"><img src="https://img.shields.io/github/actions/workflow/status/tailwindlabs/tailwindcss/ci.yml?branch=main" alt="Build Status"></a>
|
||||
<a href="https://github.com/tailwindlabs/tailwindcss/actions"><img src="https://img.shields.io/github/actions/workflow/status/tailwindlabs/tailwindcss/ci.yml?branch=next" alt="Build Status"></a>
|
||||
<a href="https://www.npmjs.com/package/tailwindcss"><img src="https://img.shields.io/npm/dt/tailwindcss.svg" alt="Total Downloads"></a>
|
||||
<a href="https://github.com/tailwindlabs/tailwindcss/releases"><img src="https://img.shields.io/npm/v/tailwindcss.svg" alt="Latest Release"></a>
|
||||
<a href="https://github.com/tailwindlabs/tailwindcss/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/tailwindcss.svg" alt="License"></a>
|
||||
<a href="https://github.com/tailwindcss/tailwindcss/releases"><img src="https://img.shields.io/npm/v/tailwindcss.svg" alt="Latest Release"></a>
|
||||
<a href="https://github.com/tailwindcss/tailwindcss/blob/master/LICENSE"><img src="https://img.shields.io/npm/l/tailwindcss.svg" alt="License"></a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
|
@ -27,10 +27,14 @@ For full documentation, visit [tailwindcss.com](https://tailwindcss.com).
|
|||
|
||||
## Community
|
||||
|
||||
For help, discussion about best practices, or feature ideas:
|
||||
For help, discussion about best practices, or any other conversation that would benefit from being searchable:
|
||||
|
||||
[Discuss Tailwind CSS on GitHub](https://github.com/tailwindlabs/tailwindcss/discussions)
|
||||
[Discuss Tailwind CSS on GitHub](https://github.com/tailwindcss/tailwindcss/discussions)
|
||||
|
||||
For chatting with others using the framework:
|
||||
|
||||
[Join the Tailwind CSS Discord Server](https://discord.gg/7NF8GNe)
|
||||
|
||||
## Contributing
|
||||
|
||||
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**.
|
||||
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**.
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
This project is dual-licensed under the Unlicense and MIT licenses.
|
||||
|
||||
You may use this code under the terms of either license.
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
[package]
|
||||
name = "ignore"
|
||||
version = "0.4.33" #:version
|
||||
authors = ["Andrew Gallant <jamslam@gmail.com>"]
|
||||
description = """
|
||||
A fast library for efficiently matching ignore files such as `.gitignore`
|
||||
against file paths.
|
||||
"""
|
||||
documentation = "https://docs.rs/ignore"
|
||||
homepage = "https://github.com/BurntSushi/ripgrep/tree/master/crates/ignore"
|
||||
repository = "https://github.com/BurntSushi/ripgrep/tree/master/crates/ignore"
|
||||
readme = "README.md"
|
||||
keywords = ["glob", "ignore", "gitignore", "pattern", "file"]
|
||||
license = "Unlicense OR MIT"
|
||||
# CHANGED: Use an explicit edition instead of `edition.workspace = true` since this crate is
|
||||
# vendored into the Tailwind CSS workspace.
|
||||
edition = "2024"
|
||||
rust-version = "1.88"
|
||||
|
||||
[lib]
|
||||
name = "ignore"
|
||||
bench = false
|
||||
|
||||
[dependencies]
|
||||
crossbeam-deque = "0.8.3"
|
||||
# CHANGED: Use the published globset crate instead of a path dependency.
|
||||
globset = "0.4.20"
|
||||
log = "0.4.20"
|
||||
memchr = "2.6.3"
|
||||
same-file = "1.0.6"
|
||||
walkdir = "2.4.0"
|
||||
# CHANGED: Added `dunce` to canonicalize paths without UNC prefixes on Windows.
|
||||
dunce = "1.0.5"
|
||||
|
||||
[dependencies.regex-automata]
|
||||
version = "0.4.18"
|
||||
default-features = false
|
||||
features = ["std", "perf", "syntax", "meta", "nfa", "hybrid", "dfa-onepass"]
|
||||
|
||||
[target.'cfg(windows)'.dependencies.winapi-util]
|
||||
version = "0.1.2"
|
||||
|
||||
[dev-dependencies]
|
||||
bstr = { version = "1.6.2", default-features = false, features = ["std"] }
|
||||
crossbeam-channel = "0.5.15"
|
||||
|
||||
[features]
|
||||
# DEPRECATED. It is a no-op. SIMD is done automatically through runtime
|
||||
# dispatch.
|
||||
simd-accel = []
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Andrew Gallant
|
||||
|
||||
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.
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
ignore
|
||||
======
|
||||
The ignore crate provides a fast recursive directory iterator that respects
|
||||
various filters such as globs, file types and `.gitignore` files. This crate
|
||||
also provides lower level direct access to gitignore and file type matchers.
|
||||
|
||||
[](https://github.com/BurntSushi/ripgrep/actions)
|
||||
[](https://crates.io/crates/ignore)
|
||||
|
||||
Dual-licensed under MIT or the [UNLICENSE](https://unlicense.org/).
|
||||
|
||||
### Documentation
|
||||
|
||||
[https://docs.rs/ignore](https://docs.rs/ignore)
|
||||
|
||||
### Usage
|
||||
|
||||
Add this to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
ignore = "0.4"
|
||||
```
|
||||
|
||||
### Example
|
||||
|
||||
This example shows the most basic usage of this crate. This code will
|
||||
recursively traverse the current directory while automatically filtering out
|
||||
files and directories according to ignore globs found in files like
|
||||
`.ignore` and `.gitignore`:
|
||||
|
||||
|
||||
```rust,no_run
|
||||
use ignore::Walk;
|
||||
|
||||
for result in Walk::new("./") {
|
||||
// Each item yielded by the iterator is either a directory entry or an
|
||||
// error, so either print the path or the error.
|
||||
match result {
|
||||
Ok(entry) => println!("{}", entry.path().display()),
|
||||
Err(err) => println!("ERROR: {}", err),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example: advanced
|
||||
|
||||
By default, the recursive directory iterator will ignore hidden files and
|
||||
directories. This can be disabled by building the iterator with `WalkBuilder`:
|
||||
|
||||
```rust,no_run
|
||||
use ignore::WalkBuilder;
|
||||
|
||||
for result in WalkBuilder::new("./").hidden(false).build() {
|
||||
println!("{:?}", result);
|
||||
}
|
||||
```
|
||||
|
||||
See the documentation for `WalkBuilder` for many other options.
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
This is free and unencumbered software released into the public domain.
|
||||
|
||||
Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||
distribute this software, either in source code form or as a compiled
|
||||
binary, for any purpose, commercial or non-commercial, and by any
|
||||
means.
|
||||
|
||||
In jurisdictions that recognize copyright laws, the author or authors
|
||||
of this software dedicate any and all copyright interest in the
|
||||
software to the public domain. We make this dedication for the benefit
|
||||
of the public at large and to the detriment of our heirs and
|
||||
successors. We intend this dedication to be an overt act of
|
||||
relinquishment in perpetuity of all present and future rights to this
|
||||
software under copyright law.
|
||||
|
||||
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 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.
|
||||
|
||||
For more information, please refer to <http://unlicense.org/>
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
use std::{env, io::Write, path::Path};
|
||||
|
||||
use {bstr::ByteVec, ignore::WalkBuilder, walkdir::WalkDir};
|
||||
|
||||
fn main() {
|
||||
let mut path = env::args().nth(1).unwrap();
|
||||
let mut parallel = false;
|
||||
let mut simple = false;
|
||||
let (tx, rx) = crossbeam_channel::bounded::<DirEntry>(100);
|
||||
if path == "parallel" {
|
||||
path = env::args().nth(2).unwrap();
|
||||
parallel = true;
|
||||
} else if path == "walkdir" {
|
||||
path = env::args().nth(2).unwrap();
|
||||
simple = true;
|
||||
}
|
||||
|
||||
let stdout_thread = std::thread::spawn(move || {
|
||||
let mut stdout = std::io::BufWriter::new(std::io::stdout());
|
||||
for dent in rx {
|
||||
stdout.write_all(&Vec::from_path_lossy(dent.path())).unwrap();
|
||||
stdout.write_all(b"\n").unwrap();
|
||||
}
|
||||
});
|
||||
|
||||
if parallel {
|
||||
let walker = WalkBuilder::new(path).threads(6).build_parallel();
|
||||
walker.run(|| {
|
||||
let tx = tx.clone();
|
||||
Box::new(move |result| {
|
||||
use ignore::WalkState::*;
|
||||
|
||||
tx.send(DirEntry::Y(result.unwrap())).unwrap();
|
||||
Continue
|
||||
})
|
||||
});
|
||||
} else if simple {
|
||||
let walker = WalkDir::new(path);
|
||||
for result in walker {
|
||||
tx.send(DirEntry::X(result.unwrap())).unwrap();
|
||||
}
|
||||
} else {
|
||||
let walker = WalkBuilder::new(path).build();
|
||||
for result in walker {
|
||||
tx.send(DirEntry::Y(result.unwrap())).unwrap();
|
||||
}
|
||||
}
|
||||
drop(tx);
|
||||
stdout_thread.join().unwrap();
|
||||
}
|
||||
|
||||
enum DirEntry {
|
||||
X(walkdir::DirEntry),
|
||||
Y(ignore::DirEntry),
|
||||
}
|
||||
|
||||
impl DirEntry {
|
||||
fn path(&self) -> &Path {
|
||||
match *self {
|
||||
DirEntry::X(ref x) => x.path(),
|
||||
DirEntry::Y(ref y) => y.path(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,378 +0,0 @@
|
|||
/// This list represents the default file types that ripgrep ships with. In
|
||||
/// general, any file format is fair game, although it should generally be
|
||||
/// limited to reasonably popular open formats. For other cases, you can add
|
||||
/// types to each invocation of ripgrep with the '--type-add' flag.
|
||||
///
|
||||
/// If you would like to add or improve this list, please file a PR:
|
||||
/// <https://github.com/BurntSushi/ripgrep>.
|
||||
///
|
||||
/// Please try to keep this list sorted lexicographically and wrapped to 79
|
||||
/// columns (inclusive).
|
||||
#[rustfmt::skip]
|
||||
pub(crate) const DEFAULT_TYPES: &[(&[&str], &[&str])] = &[
|
||||
(&["ada"], &["*.adb", "*.ads"]),
|
||||
(&["agda"], &["*.agda", "*.lagda"]),
|
||||
(&["aidl"], &["*.aidl"]),
|
||||
(&["alire"], &["alire.toml"]),
|
||||
(&["amake"], &["*.mk", "*.bp"]),
|
||||
(&["asciidoc"], &["*.adoc", "*.asc", "*.asciidoc"]),
|
||||
(&["asm"], &["*.asm", "*.s", "*.S"]),
|
||||
(&["asp"], &[
|
||||
"*.aspx", "*.aspx.cs", "*.aspx.vb", "*.ascx", "*.ascx.cs",
|
||||
"*.ascx.vb", "*.asp"
|
||||
]),
|
||||
(&["ats"], &["*.ats", "*.dats", "*.sats", "*.hats"]),
|
||||
(&["avro"], &["*.avdl", "*.avpr", "*.avsc"]),
|
||||
(&["awk"], &["*.awk"]),
|
||||
(&["bat", "batch"], &["*.bat"]),
|
||||
(&["bazel"], &[
|
||||
"*.bazel", "*.bzl", "*.BUILD", "*.bazelrc", "BUILD", "MODULE.bazel",
|
||||
"WORKSPACE", "WORKSPACE.bazel", "WORKSPACE.bzlmod",
|
||||
]),
|
||||
(&["bitbake"], &["*.bb", "*.bbappend", "*.bbclass", "*.conf", "*.inc"]),
|
||||
(&["boxlang"], &["*.bx", "*.bxm", "*.bxs"]),
|
||||
(&["brotli"], &["*.br"]),
|
||||
(&["buildstream"], &["*.bst"]),
|
||||
(&["bzip2"], &["*.bz2", "*.tbz2"]),
|
||||
(&["c"], &["*.[chH]", "*.[chH].in", "*.cats"]),
|
||||
(&["cabal"], &["*.cabal"]),
|
||||
(&["candid"], &["*.did"]),
|
||||
(&["carp"], &["*.carp"]),
|
||||
(&["cbor"], &["*.cbor"]),
|
||||
(&["ceylon"], &["*.ceylon"]),
|
||||
(&["cfml"], &["*.cfc", "*.cfm"]),
|
||||
(&["clojure"], &["*.clj", "*.cljc", "*.cljs", "*.cljx"]),
|
||||
(&["cmake"], &["*.cmake", "CMakeLists.txt"]),
|
||||
(&["cmd"], &["*.bat", "*.cmd"]),
|
||||
(&["cml"], &["*.cml"]),
|
||||
(&["coffeescript"], &["*.coffee"]),
|
||||
(&["config"], &["*.cfg", "*.conf", "*.config", "*.ini"]),
|
||||
(&["container"], &["*Containerfile*", "*Dockerfile*"]),
|
||||
(&["coq"], &["*.v"]),
|
||||
(&["cpp"], &[
|
||||
"*.[ChH]", "*.cc", "*.[ch]pp", "*.[ch]xx", "*.hh", "*.inl",
|
||||
"*.[ChH].in", "*.cc.in", "*.[ch]pp.in", "*.[ch]xx.in", "*.hh.in",
|
||||
]),
|
||||
(&["creole"], &["*.creole"]),
|
||||
(&["crystal"], &["Projectfile", "*.cr", "*.ecr", "shard.yml"]),
|
||||
(&["cs"], &["*.cs"]),
|
||||
(&["csharp"], &["*.cs"]),
|
||||
(&["cshtml"], &["*.cshtml"]),
|
||||
(&["csproj"], &["*.csproj"]),
|
||||
(&["css"], &["*.css", "*.scss"]),
|
||||
(&["csv"], &["*.csv"]),
|
||||
(&["cuda"], &["*.cu", "*.cuh"]),
|
||||
(&["cython"], &["*.pyx", "*.pxi", "*.pxd"]),
|
||||
(&["d"], &["*.d"]),
|
||||
(&["dart"], &["*.dart"]),
|
||||
(&["devicetree"], &["*.dts", "*.dtsi", "*.dtso"]),
|
||||
(&["dhall"], &["*.dhall"]),
|
||||
(&["diff"], &["*.patch", "*.diff"]),
|
||||
(&["dita"], &["*.dita", "*.ditamap", "*.ditaval"]),
|
||||
(&["docker"], &["*Dockerfile*"]),
|
||||
(&["dockercompose"], &["docker-compose.yml", "docker-compose.*.yml"]),
|
||||
(&["dts"], &["*.dts", "*.dtsi"]),
|
||||
(&["dvc"], &["Dvcfile", "*.dvc"]),
|
||||
(&["ebuild"], &["*.ebuild", "*.eclass"]),
|
||||
(&["edn"], &["*.edn"]),
|
||||
(&["elisp"], &["*.el"]),
|
||||
(&["elixir"], &["*.ex", "*.eex", "*.exs", "*.heex", "*.leex", "*.livemd"]),
|
||||
(&["elm"], &["*.elm"]),
|
||||
(&["erb"], &["*.erb"]),
|
||||
(&["erlang"], &["*.erl", "*.hrl"]),
|
||||
(&["fennel"], &["*.fnl"]),
|
||||
(&["fidl"], &["*.fidl"]),
|
||||
(&["fish"], &["*.fish"]),
|
||||
(&["flatbuffers"], &["*.fbs"]),
|
||||
(&["fortran"], &[
|
||||
"*.f", "*.F", "*.f77", "*.F77", "*.pfo",
|
||||
"*.f90", "*.F90", "*.f95", "*.F95",
|
||||
]),
|
||||
(&["fsharp"], &["*.fs", "*.fsx", "*.fsi"]),
|
||||
(&["fut"], &["*.fut"]),
|
||||
(&["gap"], &["*.g", "*.gap", "*.gi", "*.gd", "*.tst"]),
|
||||
(&["gdscript"], &["*.gd"]),
|
||||
(&["gleam"], &["*.gleam"]),
|
||||
(&["gn"], &["*.gn", "*.gni"]),
|
||||
(&["go"], &["*.go"]),
|
||||
(&["gprbuild"], &["*.gpr"]),
|
||||
(&["gradle"], &[
|
||||
"*.gradle", "*.gradle.kts", "gradle.properties", "gradle-wrapper.*",
|
||||
"gradlew", "gradlew.bat",
|
||||
]),
|
||||
(&["graphql"], &["*.graphql", "*.graphqls"]),
|
||||
(&["groovy"], &["*.groovy", "*.gradle"]),
|
||||
(&["gzip"], &["*.gz", "*.tgz"]),
|
||||
(&["h"], &["*.h", "*.hh", "*.hpp"]),
|
||||
(&["haml"], &["*.haml"]),
|
||||
(&["hare"], &["*.ha"]),
|
||||
(&["haskell"], &["*.hs", "*.lhs", "*.cpphs", "*.c2hs", "*.hsc"]),
|
||||
(&["hbs"], &["*.hbs"]),
|
||||
(&["hs"], &["*.hs", "*.lhs"]),
|
||||
(&["html"], &["*.htm", "*.html", "*.ejs"]),
|
||||
(&["hurl"], &["*.hurl"]),
|
||||
(&["hy"], &["*.hy"]),
|
||||
(&["idris"], &["*.idr", "*.lidr"]),
|
||||
(&["janet"], &["*.janet"]),
|
||||
(&["java"], &["*.java", "*.jsp", "*.jspx", "*.properties"]),
|
||||
(&["jinja"], &["*.j2", "*.jinja", "*.jinja2"]),
|
||||
(&["jl"], &["*.jl"]),
|
||||
(&["js"], &["*.js", "*.jsx", "*.vue", "*.cjs", "*.mjs"]),
|
||||
(&["json"], &["*.json", "composer.lock", "*.sarif"]),
|
||||
(&["jsonl"], &["*.jsonl"]),
|
||||
(&["julia"], &["*.jl"]),
|
||||
(&["jupyter"], &["*.ipynb", "*.jpynb"]),
|
||||
(&["k"], &["*.k"]),
|
||||
(&["kconfig"], &["Kconfig", "Kconfig.*"]),
|
||||
(&["kotlin"], &["*.kt", "*.kts"]),
|
||||
(&["lean"], &["*.lean"]),
|
||||
(&["less"], &["*.less"]),
|
||||
(&["license"], &[
|
||||
// General
|
||||
"COPYING", "COPYING[.-]*",
|
||||
"COPYRIGHT", "COPYRIGHT[.-]*",
|
||||
"EULA", "EULA[.-]*",
|
||||
"licen[cs]e", "licen[cs]e.*",
|
||||
"LICEN[CS]E", "LICEN[CS]E[.-]*", "*[.-]LICEN[CS]E*",
|
||||
"NOTICE", "NOTICE[.-]*",
|
||||
"PATENTS", "PATENTS[.-]*",
|
||||
"UNLICEN[CS]E", "UNLICEN[CS]E[.-]*",
|
||||
// GPL (gpl.txt, etc.)
|
||||
"agpl[.-]*",
|
||||
"gpl[.-]*",
|
||||
"lgpl[.-]*",
|
||||
// Other license-specific (APACHE-2.0.txt, etc.)
|
||||
"AGPL-*[0-9]*",
|
||||
"APACHE-*[0-9]*",
|
||||
"BSD-*[0-9]*",
|
||||
"CC-BY-*",
|
||||
"GFDL-*[0-9]*",
|
||||
"GNU-*[0-9]*",
|
||||
"GPL-*[0-9]*",
|
||||
"LGPL-*[0-9]*",
|
||||
"MIT-*[0-9]*",
|
||||
"MPL-*[0-9]*",
|
||||
"OFL-*[0-9]*",
|
||||
]),
|
||||
(&["lilypond"], &["*.ly", "*.ily"]),
|
||||
(&["lisp"], &["*.el", "*.jl", "*.lisp", "*.lsp", "*.sc", "*.scm"]),
|
||||
(&["llvm"], &["*.ll"]),
|
||||
(&["lock"], &["*.lock", "package-lock.json"]),
|
||||
(&["log"], &["*.log"]),
|
||||
(&["lua"], &["*.lua"]),
|
||||
(&["lz4"], &["*.lz4"]),
|
||||
(&["lzma"], &["*.lzma"]),
|
||||
(&["m4"], &["*.ac", "*.m4"]),
|
||||
(&["make"], &[
|
||||
"[Gg][Nn][Uu]makefile", "[Mm]akefile",
|
||||
"[Gg][Nn][Uu]makefile.am", "[Mm]akefile.am",
|
||||
"[Gg][Nn][Uu]makefile.in", "[Mm]akefile.in",
|
||||
"Makefile.*",
|
||||
"*.mk", "*.mak"
|
||||
]),
|
||||
(&["mako"], &["*.mako", "*.mao"]),
|
||||
(&["man"], &["*.[0-9lnpx]", "*.[0-9][cEFMmpSx]"]),
|
||||
(&["markdown", "md"], &[
|
||||
"*.markdown",
|
||||
"*.md",
|
||||
"*.mdown",
|
||||
"*.mdwn",
|
||||
"*.mkd",
|
||||
"*.mkdn",
|
||||
"*.mdx",
|
||||
]),
|
||||
(&["matlab"], &["*.m"]),
|
||||
(&["meson"], &["meson.build", "meson_options.txt", "meson.options"]),
|
||||
(&["minified"], &["*.min.html", "*.min.css", "*.min.js"]),
|
||||
(&["mint"], &["*.mint"]),
|
||||
(&["mk"], &["mkfile"]),
|
||||
(&["ml"], &["*.ml"]),
|
||||
(&["mojo"], &["*.mojo"]),
|
||||
(&["motoko"], &["*.mo"]),
|
||||
(&["msbuild"], &[
|
||||
"*.csproj", "*.fsproj", "*.vcxproj", "*.proj", "*.props", "*.targets",
|
||||
"*.sln", "*.slnf"
|
||||
]),
|
||||
(&["nim"], &["*.nim", "*.nimf", "*.nimble", "*.nims"]),
|
||||
(&["nix"], &["*.nix"]),
|
||||
(&["objc"], &["*.h", "*.m"]),
|
||||
(&["objcpp"], &["*.h", "*.mm"]),
|
||||
(&["ocaml"], &["*.ml", "*.mli", "*.mll", "*.mly"]),
|
||||
(&["org"], &["*.org", "*.org_archive"]),
|
||||
(&["pants"], &["BUILD"]),
|
||||
(&["pascal"], &["*.pas", "*.dpr", "*.lpr", "*.pp", "*.inc"]),
|
||||
(&["pdf"], &["*.pdf"]),
|
||||
(&["perl"], &["*.perl", "*.pl", "*.PL", "*.plh", "*.plx", "*.pm", "*.t"]),
|
||||
(&["php"], &[
|
||||
// note that PHP 6 doesn't exist
|
||||
// See: https://wiki.php.net/rfc/php6
|
||||
"*.php", "*.php3", "*.php4", "*.php5", "*.php7", "*.php8",
|
||||
"*.pht", "*.phtml"
|
||||
]),
|
||||
(&["pkgbuild"], &["PKGBUILD"]),
|
||||
(&["po"], &["*.po"]),
|
||||
(&["pod"], &["*.pod"]),
|
||||
(&["postscript"], &["*.eps", "*.ps"]),
|
||||
(&["prolog"], &["*.pl", "*.pro", "*.prolog", "*.P"]),
|
||||
(&["proto", "protobuf"], &["*.proto"]),
|
||||
(&["ps"], &["*.cdxml", "*.ps1", "*.ps1xml", "*.psd1", "*.psm1"]),
|
||||
(&["puppet"], &["*.epp", "*.erb", "*.pp", "*.rb"]),
|
||||
(&["purs"], &["*.purs"]),
|
||||
(&["py", "python"], &["*.py", "*.pyi"]),
|
||||
(&["qmake"], &["*.pro", "*.pri", "*.prf"]),
|
||||
(&["qml"], &["*.qml"]),
|
||||
(&["qrc"], &["*.qrc"]),
|
||||
(&["qui"], &["*.ui"]),
|
||||
(&["r"], &["*.R", "*.r", "*.Rmd", "*.rmd", "*.Rnw", "*.rnw"]),
|
||||
(&["racket"], &["*.rkt"]),
|
||||
(&["raku"], &[
|
||||
"*.raku", "*.rakumod", "*.rakudoc", "*.rakutest",
|
||||
"*.p6", "*.pl6", "*.pm6"
|
||||
]),
|
||||
(&["rdoc"], &["*.rdoc"]),
|
||||
(&["readme"], &["README*", "*README"]),
|
||||
(&["reasonml"], &["*.re", "*.rei"]),
|
||||
(&["red"], &["*.r", "*.red", "*.reds"]),
|
||||
(&["rescript"], &["*.res", "*.resi"]),
|
||||
(&["robot"], &["*.robot"]),
|
||||
(&["rocq"], &["*.v"]),
|
||||
(&["rst"], &["*.rst"]),
|
||||
(&["ruby"], &[
|
||||
// Idiomatic files
|
||||
"config.ru", "Gemfile", ".irbrc", "Rakefile",
|
||||
// Extensions
|
||||
"*.gemspec", "*.rb", "*.rbw", "*.rake"
|
||||
]),
|
||||
(&["rust"], &["*.rs"]),
|
||||
(&["sass"], &["*.sass", "*.scss"]),
|
||||
(&["scala"], &["*.scala", "*.sbt"]),
|
||||
(&["scdoc"], &["*.scd", "*.scdoc"]),
|
||||
(&["seed7"], &["*.sd7", "*.s7i"]),
|
||||
(&["sh"], &[
|
||||
// Portable/misc. init files
|
||||
".env", ".login", ".logout", ".profile", "profile",
|
||||
// bash-specific init files
|
||||
".bash_login", "bash_login",
|
||||
".bash_logout", "bash_logout",
|
||||
".bash_profile", "bash_profile",
|
||||
".bashrc", "bashrc", "*.bashrc",
|
||||
// csh-specific init files
|
||||
".cshrc", "*.cshrc",
|
||||
// ksh-specific init files
|
||||
".kshrc", "*.kshrc",
|
||||
// tcsh-specific init files
|
||||
".tcshrc",
|
||||
// zsh-specific init files
|
||||
".zshenv", "zshenv",
|
||||
".zlogin", "zlogin",
|
||||
".zlogout", "zlogout",
|
||||
".zprofile", "zprofile",
|
||||
".zshrc", "zshrc",
|
||||
// Extensions
|
||||
"*.bash", "*.csh", "*.env", "*.ksh", "*.sh", "*.tcsh", "*.zsh",
|
||||
]),
|
||||
(&["slim"], &["*.skim", "*.slim", "*.slime"]),
|
||||
(&["smarty"], &["*.tpl"]),
|
||||
(&["sml"], &["*.sml", "*.sig"]),
|
||||
(&["solidity"], &["*.sol"]),
|
||||
(&["soy"], &["*.soy"]),
|
||||
(&["spark"], &["*.spark"]),
|
||||
(&["spec"], &["*.spec"]),
|
||||
(&["sql"], &["*.sql", "*.psql"]),
|
||||
(&["ssa"], &["*.ssa"]),
|
||||
(&["stylus"], &["*.styl"]),
|
||||
(&["sv"], &["*.v", "*.vg", "*.sv", "*.svh", "*.h"]),
|
||||
(&["svelte"], &["*.svelte", "*.svelte.ts"]),
|
||||
(&["svg"], &["*.svg"]),
|
||||
(&["swift"], &["*.swift"]),
|
||||
(&["swig"], &["*.def", "*.i"]),
|
||||
(&["systemd"], &[
|
||||
"*.automount", "*.conf", "*.device", "*.link", "*.mount", "*.path",
|
||||
"*.scope", "*.service", "*.slice", "*.socket", "*.swap", "*.target",
|
||||
"*.timer",
|
||||
]),
|
||||
(&["taskpaper"], &["*.taskpaper"]),
|
||||
(&["tcl"], &["*.tcl"]),
|
||||
(&["tex"], &["*.tex", "*.ltx", "*.cls", "*.sty", "*.bib", "*.dtx", "*.ins"]),
|
||||
(&["texinfo"], &["*.texi"]),
|
||||
(&["textile"], &["*.textile"]),
|
||||
(&["tf"], &[
|
||||
"*.tf", "*.tf.json", "*.tfvars", "*.tfvars.json",
|
||||
"*.terraformrc", "terraform.rc", "*.tfrc", "*.terraform.lock.hcl",
|
||||
]),
|
||||
(&["thrift"], &["*.thrift"]),
|
||||
(&["toml"], &["*.toml", "Cargo.lock"]),
|
||||
(&["ts", "typescript"], &["*.ts", "*.tsx", "*.cts", "*.mts"]),
|
||||
(&["twig"], &["*.twig"]),
|
||||
(&["txt"], &["*.txt"]),
|
||||
(&["typoscript"], &["*.typoscript", "*.ts"]),
|
||||
(&["typst"], &["*.typ"]),
|
||||
(&["usd"], &["*.usd", "*.usda", "*.usdc"]),
|
||||
(&["v"], &["*.v", "*.vsh"]),
|
||||
(&["vala"], &["*.vala"]),
|
||||
(&["vb"], &["*.vb"]),
|
||||
(&["vcl"], &["*.vcl"]),
|
||||
(&["verilog"], &["*.v", "*.vh", "*.sv", "*.svh"]),
|
||||
(&["vhdl"], &["*.vhd", "*.vhdl"]),
|
||||
(&["vim"], &[
|
||||
"*.vim", ".vimrc", ".gvimrc", "vimrc", "gvimrc", "_vimrc", "_gvimrc",
|
||||
]),
|
||||
(&["vimscript"], &[
|
||||
"*.vim", ".vimrc", ".gvimrc", "vimrc", "gvimrc", "_vimrc", "_gvimrc",
|
||||
]),
|
||||
(&["vue"], &["*.vue"]),
|
||||
(&["webidl"], &["*.idl", "*.webidl", "*.widl"]),
|
||||
(&["wgsl"], &["*.wgsl"]),
|
||||
(&["wiki"], &["*.mediawiki", "*.wiki"]),
|
||||
(&["xml"], &[
|
||||
"*.xml", "*.xml.dist", "*.dtd", "*.xsl", "*.xslt", "*.xsd", "*.xjb",
|
||||
"*.rng", "*.sch", "*.xhtml",
|
||||
]),
|
||||
(&["xz"], &["*.xz", "*.txz"]),
|
||||
(&["yacc"], &["*.y"]),
|
||||
(&["yaml"], &["*.yaml", "*.yml"]),
|
||||
(&["yang"], &["*.yang"]),
|
||||
(&["z"], &["*.Z"]),
|
||||
(&["zig"], &["*.zig"]),
|
||||
(&["zsh"], &[
|
||||
".zshenv", "zshenv",
|
||||
".zlogin", "zlogin",
|
||||
".zlogout", "zlogout",
|
||||
".zprofile", "zprofile",
|
||||
".zshrc", "zshrc",
|
||||
"*.zsh",
|
||||
]),
|
||||
(&["zstd"], &["*.zst", "*.zstd"]),
|
||||
];
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::DEFAULT_TYPES;
|
||||
|
||||
#[test]
|
||||
fn default_types_are_sorted() {
|
||||
let mut names = DEFAULT_TYPES.iter().map(|(aliases, _)| aliases[0]);
|
||||
let Some(mut previous_name) = names.next() else {
|
||||
return;
|
||||
};
|
||||
for name in names {
|
||||
assert!(
|
||||
name > previous_name,
|
||||
r#""{}" should be sorted before "{}" in `DEFAULT_TYPES`"#,
|
||||
name,
|
||||
previous_name
|
||||
);
|
||||
previous_name = name;
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_types_aliases_are_sorted() {
|
||||
for (aliases, _) in DEFAULT_TYPES.iter() {
|
||||
assert!(
|
||||
aliases.is_sorted(),
|
||||
"this alias list is not sorted: {aliases:?}",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,913 +0,0 @@
|
|||
/*!
|
||||
The gitignore module provides a way to match globs from a gitignore file
|
||||
against file paths.
|
||||
|
||||
Note that this module implements the specification as described in the
|
||||
`gitignore` man page from scratch. That is, this module does *not* shell out to
|
||||
the `git` command line tool.
|
||||
*/
|
||||
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{BufRead, BufReader, Read},
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use {
|
||||
globset::{Candidate, GlobBuilder, GlobSet, GlobSetBuilder},
|
||||
regex_automata::util::pool::Pool,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
Error, Match, PartialErrorBuilder,
|
||||
pathutil::{is_file_name, strip_prefix},
|
||||
};
|
||||
|
||||
/// Glob represents a single glob in a gitignore file.
|
||||
///
|
||||
/// This is used to report information about the highest precedent glob that
|
||||
/// matched in one or more gitignore files.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Glob {
|
||||
/// The file path that this glob was extracted from.
|
||||
from: Option<PathBuf>,
|
||||
/// The original glob string.
|
||||
original: String,
|
||||
/// The actual glob string used to convert to a regex.
|
||||
actual: String,
|
||||
/// Whether this is a whitelisted glob or not.
|
||||
is_whitelist: bool,
|
||||
/// Whether this glob should only match directories or not.
|
||||
is_only_dir: bool,
|
||||
}
|
||||
|
||||
impl Glob {
|
||||
/// Returns the file path that defined this glob.
|
||||
pub fn from(&self) -> Option<&Path> {
|
||||
self.from.as_ref().map(|p| &**p)
|
||||
}
|
||||
|
||||
/// The original glob as it was defined in a gitignore file.
|
||||
pub fn original(&self) -> &str {
|
||||
&self.original
|
||||
}
|
||||
|
||||
/// The actual glob that was compiled to respect gitignore
|
||||
/// semantics.
|
||||
pub fn actual(&self) -> &str {
|
||||
&self.actual
|
||||
}
|
||||
|
||||
/// Whether this was a whitelisted glob or not.
|
||||
pub fn is_whitelist(&self) -> bool {
|
||||
self.is_whitelist
|
||||
}
|
||||
|
||||
/// Whether this glob must match a directory or not.
|
||||
pub fn is_only_dir(&self) -> bool {
|
||||
self.is_only_dir
|
||||
}
|
||||
|
||||
/// Returns true if and only if this glob has a `**/` prefix.
|
||||
fn has_doublestar_prefix(&self) -> bool {
|
||||
self.actual.starts_with("**/") || self.actual == "**"
|
||||
}
|
||||
}
|
||||
|
||||
/// Gitignore is a matcher for the globs in one or more gitignore files
|
||||
/// in the same directory.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Gitignore {
|
||||
set: GlobSet,
|
||||
root: PathBuf,
|
||||
globs: Vec<Glob>,
|
||||
num_ignores: u64,
|
||||
num_whitelists: u64,
|
||||
matches: Option<Arc<Pool<Vec<usize>>>>,
|
||||
// CHANGED: Add a flag to have Gitignore rules that apply only to files.
|
||||
only_on_files: bool,
|
||||
}
|
||||
|
||||
impl Gitignore {
|
||||
/// Creates a new gitignore matcher from the gitignore file path given.
|
||||
///
|
||||
/// If it's desirable to include multiple gitignore files in a single
|
||||
/// matcher, or read gitignore globs from a different source, then
|
||||
/// use `GitignoreBuilder`.
|
||||
///
|
||||
/// This always returns a valid matcher, even if it's empty. In particular,
|
||||
/// a Gitignore file can be partially valid, e.g., when one glob is invalid
|
||||
/// but the rest aren't.
|
||||
///
|
||||
/// Note that I/O errors are ignored. For more granular control over
|
||||
/// errors, use `GitignoreBuilder`.
|
||||
pub fn new<P: AsRef<Path>>(
|
||||
gitignore_path: P,
|
||||
) -> (Gitignore, Option<Error>) {
|
||||
let path = gitignore_path.as_ref();
|
||||
let parent = path.parent().unwrap_or(Path::new("/"));
|
||||
let mut builder = GitignoreBuilder::new(parent);
|
||||
let mut errs = PartialErrorBuilder::default();
|
||||
errs.maybe_push_ignore_io(builder.add(path));
|
||||
match builder.build() {
|
||||
Ok(gi) => (gi, errs.into_error_option()),
|
||||
Err(err) => {
|
||||
errs.push(err);
|
||||
(Gitignore::empty(), errs.into_error_option())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new gitignore matcher from the global ignore file, if one
|
||||
/// exists.
|
||||
///
|
||||
/// The global config file path is specified by git's `core.excludesFile`
|
||||
/// config option.
|
||||
///
|
||||
/// # Behavior
|
||||
///
|
||||
/// This routine does its best to discover any global git exclude files.
|
||||
/// This will try to parse out the `excludesFile` config option in your
|
||||
/// global git configuration, if necessary.
|
||||
///
|
||||
/// The specific things this routine tries (which are subject to change
|
||||
/// based on how git behaves) are:
|
||||
///
|
||||
///
|
||||
///
|
||||
/// Git's config file location is `$HOME/.gitconfig`. If `$HOME/.gitconfig`
|
||||
/// does not exist or does not specify `core.excludesFile`, then
|
||||
/// `$XDG_CONFIG_HOME/git/ignore` is read. If `$XDG_CONFIG_HOME` is not
|
||||
/// set or is empty, then `$HOME/.config/git/ignore` is used instead.
|
||||
pub fn global() -> (Gitignore, Option<Error>) {
|
||||
match std::env::current_dir() {
|
||||
Ok(cwd) => GitignoreBuilder::new(cwd).build_global(),
|
||||
Err(err) => (Gitignore::empty(), Some(err.into())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new empty gitignore matcher that never matches anything.
|
||||
///
|
||||
/// Its path is empty.
|
||||
pub fn empty() -> Gitignore {
|
||||
Gitignore {
|
||||
set: GlobSet::empty(),
|
||||
root: PathBuf::from(""),
|
||||
globs: vec![],
|
||||
num_ignores: 0,
|
||||
num_whitelists: 0,
|
||||
matches: None,
|
||||
// CHANGED: Add a flag to have Gitignore rules that apply only to
|
||||
// files.
|
||||
only_on_files: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the directory containing this gitignore matcher.
|
||||
///
|
||||
/// All matches are done relative to this path.
|
||||
pub fn path(&self) -> &Path {
|
||||
&*self.root
|
||||
}
|
||||
|
||||
/// Returns true if and only if this gitignore has zero globs, and
|
||||
/// therefore never matches any file path.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.set.is_empty()
|
||||
}
|
||||
|
||||
/// Returns the total number of globs, which should be equivalent to
|
||||
/// `num_ignores + num_whitelists`.
|
||||
pub fn len(&self) -> usize {
|
||||
self.set.len()
|
||||
}
|
||||
|
||||
/// Returns the total number of ignore globs.
|
||||
pub fn num_ignores(&self) -> u64 {
|
||||
self.num_ignores
|
||||
}
|
||||
|
||||
/// Returns the total number of whitelisted globs.
|
||||
pub fn num_whitelists(&self) -> u64 {
|
||||
self.num_whitelists
|
||||
}
|
||||
|
||||
/// Returns whether the given path (file or directory) matched a pattern in
|
||||
/// this gitignore matcher.
|
||||
///
|
||||
/// `is_dir` should be true if the path refers to a directory and false
|
||||
/// otherwise.
|
||||
///
|
||||
/// The given path is matched relative to the path given when building
|
||||
/// the matcher. Specifically, before matching `path`, its prefix (as
|
||||
/// determined by a common suffix of the directory containing this
|
||||
/// gitignore) is stripped. If there is no common suffix/prefix overlap,
|
||||
/// then `path` is assumed to be relative to this matcher.
|
||||
pub fn matched<P: AsRef<Path>>(
|
||||
&self,
|
||||
path: P,
|
||||
is_dir: bool,
|
||||
) -> Match<&Glob> {
|
||||
if self.is_empty() {
|
||||
return Match::None;
|
||||
}
|
||||
self.matched_stripped(self.strip(path.as_ref()), is_dir)
|
||||
}
|
||||
|
||||
/// Returns whether the given path (file or directory, and expected to be
|
||||
/// under the root) or any of its parent directories (up to the root)
|
||||
/// matched a pattern in this gitignore matcher.
|
||||
///
|
||||
/// NOTE: This method is more expensive than walking the directory hierarchy
|
||||
/// top-to-bottom and matching the entries. But, is easier to use in cases
|
||||
/// when a list of paths are available without a hierarchy.
|
||||
///
|
||||
/// `is_dir` should be true if the path refers to a directory and false
|
||||
/// otherwise.
|
||||
///
|
||||
/// The given path is matched relative to the path given when building
|
||||
/// the matcher. Specifically, before matching `path`, its prefix (as
|
||||
/// determined by a common suffix of the directory containing this
|
||||
/// gitignore) is stripped. If there is no common suffix/prefix overlap,
|
||||
/// then `path` is assumed to be relative to this matcher.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This method panics if the given file path is not under the root path
|
||||
/// of this matcher.
|
||||
pub fn matched_path_or_any_parents<P: AsRef<Path>>(
|
||||
&self,
|
||||
path: P,
|
||||
is_dir: bool,
|
||||
) -> Match<&Glob> {
|
||||
if self.is_empty() {
|
||||
return Match::None;
|
||||
}
|
||||
let mut path = self.strip(path.as_ref());
|
||||
assert!(!path.has_root(), "path is expected to be under the root");
|
||||
|
||||
match self.matched_stripped(path, is_dir) {
|
||||
Match::None => (), // walk up
|
||||
a_match => return a_match,
|
||||
}
|
||||
while let Some(parent) = path.parent() {
|
||||
match self.matched_stripped(parent, /* is_dir */ true) {
|
||||
Match::None => path = parent, // walk up
|
||||
a_match => return a_match,
|
||||
}
|
||||
}
|
||||
Match::None
|
||||
}
|
||||
|
||||
/// Like matched, but takes a path that has already been stripped.
|
||||
fn matched_stripped<P: AsRef<Path>>(
|
||||
&self,
|
||||
path: P,
|
||||
is_dir: bool,
|
||||
) -> Match<&Glob> {
|
||||
if self.is_empty() {
|
||||
return Match::None;
|
||||
}
|
||||
// CHANGED: Rules marked as only_on_files can not match against
|
||||
// directories.
|
||||
if self.only_on_files && is_dir {
|
||||
return Match::None;
|
||||
}
|
||||
let path = path.as_ref();
|
||||
let mut matches = self.matches.as_ref().unwrap().get();
|
||||
let candidate = Candidate::new(path);
|
||||
self.set.matches_candidate_into(&candidate, &mut *matches);
|
||||
for &i in matches.iter().rev() {
|
||||
let glob = &self.globs[i];
|
||||
if !glob.is_only_dir() || is_dir {
|
||||
return if glob.is_whitelist() {
|
||||
Match::Whitelist(glob)
|
||||
} else {
|
||||
Match::Ignore(glob)
|
||||
};
|
||||
}
|
||||
}
|
||||
Match::None
|
||||
}
|
||||
|
||||
/// Strips the given path such that it's suitable for matching with this
|
||||
/// gitignore matcher.
|
||||
fn strip<'a, P: 'a + AsRef<Path> + ?Sized>(
|
||||
&'a self,
|
||||
path: &'a P,
|
||||
) -> &'a Path {
|
||||
let mut path = path.as_ref();
|
||||
// A leading ./ is completely superfluous. We also strip it from
|
||||
// our gitignore root path, so we need to strip it from our candidate
|
||||
// path too.
|
||||
if let Some(p) = strip_prefix("./", path) {
|
||||
path = p;
|
||||
}
|
||||
// Strip any common prefix between the candidate path and the root
|
||||
// of the gitignore, to make sure we get relative matching right.
|
||||
// BUT, a file name might not have any directory components to it,
|
||||
// in which case, we don't want to accidentally strip any part of the
|
||||
// file name.
|
||||
//
|
||||
// As an additional special case, if the root is just `.`, then we
|
||||
// shouldn't try to strip anything, e.g., when path begins with a `.`.
|
||||
if self.root != Path::new(".") && !is_file_name(path) {
|
||||
if let Some(p) = strip_prefix(&self.root, path) {
|
||||
path = p;
|
||||
// If we're left with a leading slash, get rid of it.
|
||||
if let Some(p) = strip_prefix("/", path) {
|
||||
path = p;
|
||||
}
|
||||
}
|
||||
}
|
||||
path
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a matcher for a single set of globs from a .gitignore file.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GitignoreBuilder {
|
||||
builder: GlobSetBuilder,
|
||||
root: PathBuf,
|
||||
globs: Vec<Glob>,
|
||||
case_insensitive: bool,
|
||||
allow_unclosed_class: bool,
|
||||
// CHANGED: Add a flag to have Gitignore rules that apply only to files.
|
||||
only_on_files: bool,
|
||||
}
|
||||
|
||||
impl GitignoreBuilder {
|
||||
/// Create a new builder for a gitignore file.
|
||||
///
|
||||
/// The path given should be the path at which the globs for this gitignore
|
||||
/// file should be matched. Note that paths are always matched relative
|
||||
/// to the root path given here. Generally, the root path should correspond
|
||||
/// to the *directory* containing a `.gitignore` file.
|
||||
pub fn new<P: AsRef<Path>>(root: P) -> GitignoreBuilder {
|
||||
let root = root.as_ref();
|
||||
GitignoreBuilder {
|
||||
builder: GlobSetBuilder::new(),
|
||||
root: strip_prefix("./", root).unwrap_or(root).to_path_buf(),
|
||||
globs: vec![],
|
||||
case_insensitive: false,
|
||||
allow_unclosed_class: true,
|
||||
// CHANGED: Add a flag to have Gitignore rules that apply only to
|
||||
// files.
|
||||
only_on_files: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a new matcher from the globs added so far.
|
||||
///
|
||||
/// Once a matcher is built, no new globs can be added to it.
|
||||
pub fn build(&self) -> Result<Gitignore, Error> {
|
||||
let nignore = self.globs.iter().filter(|g| !g.is_whitelist()).count();
|
||||
let nwhite = self.globs.iter().filter(|g| g.is_whitelist()).count();
|
||||
let set = self
|
||||
.builder
|
||||
.build()
|
||||
.map_err(|err| Error::Glob { glob: None, err: err.to_string() })?;
|
||||
Ok(Gitignore {
|
||||
set,
|
||||
root: self.root.clone(),
|
||||
globs: self.globs.clone(),
|
||||
num_ignores: nignore as u64,
|
||||
num_whitelists: nwhite as u64,
|
||||
matches: Some(Arc::new(
|
||||
Pool::with_available_parallelism_capacity(|| vec![]),
|
||||
)),
|
||||
// CHANGED: Add a flag to have Gitignore rules that apply only to
|
||||
// files.
|
||||
only_on_files: self.only_on_files,
|
||||
})
|
||||
}
|
||||
|
||||
/// Build a global gitignore matcher using the configuration in this
|
||||
/// builder.
|
||||
///
|
||||
/// This consumes ownership of the builder unlike `build` because it
|
||||
/// must mutate the builder to add the global gitignore globs.
|
||||
///
|
||||
/// Note that this ignores the path given to this builder's constructor
|
||||
/// and instead derives the path automatically from git's global
|
||||
/// configuration.
|
||||
pub fn build_global(mut self) -> (Gitignore, Option<Error>) {
|
||||
match gitconfig_excludes_path() {
|
||||
None => (Gitignore::empty(), None),
|
||||
Some(path) => {
|
||||
if !path.is_file() {
|
||||
(Gitignore::empty(), None)
|
||||
} else {
|
||||
let mut errs = PartialErrorBuilder::default();
|
||||
errs.maybe_push_ignore_io(self.add(path));
|
||||
match self.build() {
|
||||
Ok(gi) => (gi, errs.into_error_option()),
|
||||
Err(err) => {
|
||||
errs.push(err);
|
||||
(Gitignore::empty(), errs.into_error_option())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Add each glob from the file path given.
|
||||
///
|
||||
/// The file given should be formatted as a `gitignore` file.
|
||||
///
|
||||
/// Note that partial errors can be returned. For example, if there was
|
||||
/// a problem adding one glob, an error for that will be returned, but
|
||||
/// all other valid globs will still be added.
|
||||
pub fn add<P: AsRef<Path>>(&mut self, path: P) -> Option<Error> {
|
||||
let path = path.as_ref();
|
||||
let file = match File::open(path) {
|
||||
Err(err) => return Some(Error::Io(err).with_path(path)),
|
||||
Ok(file) => file,
|
||||
};
|
||||
log::debug!("opened gitignore file: {}", path.display());
|
||||
let rdr = BufReader::new(file);
|
||||
let mut errs = PartialErrorBuilder::default();
|
||||
for (i, line) in rdr.lines().enumerate() {
|
||||
let lineno = (i + 1) as u64;
|
||||
let line = match line {
|
||||
Ok(line) => line,
|
||||
Err(err) => {
|
||||
errs.push(Error::Io(err).tagged(path, lineno));
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// Match Git's handling of .gitignore files that begin with the Unicode BOM
|
||||
const UTF8_BOM: &str = "\u{feff}";
|
||||
let line =
|
||||
if i == 0 { line.trim_start_matches(UTF8_BOM) } else { &line };
|
||||
|
||||
if let Err(err) = self.add_line(Some(path.to_path_buf()), &line) {
|
||||
errs.push(err.tagged(path, lineno));
|
||||
}
|
||||
}
|
||||
errs.into_error_option()
|
||||
}
|
||||
|
||||
/// Add each glob line from the string given.
|
||||
///
|
||||
/// If this string came from a particular `gitignore` file, then its path
|
||||
/// should be provided here.
|
||||
///
|
||||
/// The string given should be formatted as a `gitignore` file.
|
||||
#[cfg(test)]
|
||||
fn add_str(
|
||||
&mut self,
|
||||
from: Option<PathBuf>,
|
||||
gitignore: &str,
|
||||
) -> Result<&mut GitignoreBuilder, Error> {
|
||||
for line in gitignore.lines() {
|
||||
self.add_line(from.clone(), line)?;
|
||||
}
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// Add a line from a gitignore file to this builder.
|
||||
///
|
||||
/// If this line came from a particular `gitignore` file, then its path
|
||||
/// should be provided here.
|
||||
///
|
||||
/// If the line could not be parsed as a glob, then an error is returned.
|
||||
pub fn add_line(
|
||||
&mut self,
|
||||
from: Option<PathBuf>,
|
||||
mut line: &str,
|
||||
) -> Result<&mut GitignoreBuilder, Error> {
|
||||
#![allow(deprecated)]
|
||||
|
||||
if line.starts_with("#") {
|
||||
return Ok(self);
|
||||
}
|
||||
if !line.ends_with("\\ ") {
|
||||
line = line.trim_right();
|
||||
}
|
||||
if line.is_empty() {
|
||||
return Ok(self);
|
||||
}
|
||||
let mut glob = Glob {
|
||||
from,
|
||||
original: line.to_string(),
|
||||
actual: String::new(),
|
||||
is_whitelist: false,
|
||||
is_only_dir: false,
|
||||
};
|
||||
let mut is_absolute = false;
|
||||
if line.starts_with("\\!") || line.starts_with("\\#") {
|
||||
line = &line[1..];
|
||||
is_absolute = line.chars().nth(0) == Some('/');
|
||||
} else {
|
||||
if line.starts_with("!") {
|
||||
glob.is_whitelist = true;
|
||||
line = &line[1..];
|
||||
}
|
||||
if line.starts_with("/") {
|
||||
// `man gitignore` says that if a glob starts with a slash,
|
||||
// then the glob can only match the beginning of a path
|
||||
// (relative to the location of gitignore). We achieve this by
|
||||
// simply banning wildcards from matching /.
|
||||
line = &line[1..];
|
||||
is_absolute = true;
|
||||
}
|
||||
}
|
||||
// If it ends with a slash, then this should only match directories,
|
||||
// but the slash should otherwise not be used while globbing.
|
||||
if line.as_bytes().last() == Some(&b'/') {
|
||||
glob.is_only_dir = true;
|
||||
line = &line[..line.len() - 1];
|
||||
// If the slash was escaped, then remove the escape.
|
||||
// See: https://github.com/BurntSushi/ripgrep/issues/2236
|
||||
if line.as_bytes().last() == Some(&b'\\') {
|
||||
line = &line[..line.len() - 1];
|
||||
}
|
||||
}
|
||||
glob.actual = line.to_string();
|
||||
// If there is a literal slash, then this is a glob that must match the
|
||||
// entire path name. Otherwise, we should let it match anywhere, so use
|
||||
// a **/ prefix.
|
||||
if !is_absolute && !line.chars().any(|c| c == '/') {
|
||||
// ... but only if we don't already have a **/ prefix.
|
||||
if !glob.has_doublestar_prefix() {
|
||||
glob.actual = format!("**/{}", glob.actual);
|
||||
}
|
||||
}
|
||||
// If the glob ends with `/**`, then we should only match everything
|
||||
// inside a directory, but not the directory itself. Standard globs
|
||||
// will match the directory. So we add `/*` to force the issue.
|
||||
if glob.actual.ends_with("/**") {
|
||||
glob.actual = format!("{}/*", glob.actual);
|
||||
}
|
||||
let parsed = GlobBuilder::new(&glob.actual)
|
||||
.literal_separator(true)
|
||||
.case_insensitive(self.case_insensitive)
|
||||
.backslash_escape(true)
|
||||
.allow_unclosed_class(self.allow_unclosed_class)
|
||||
.build()
|
||||
.map_err(|err| Error::Glob {
|
||||
glob: Some(glob.original.clone()),
|
||||
err: err.kind().to_string(),
|
||||
})?;
|
||||
self.builder.add(parsed);
|
||||
self.globs.push(glob);
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// Toggle whether the globs should be matched case insensitively or not.
|
||||
///
|
||||
/// When this option is changed, only globs added after the change will be
|
||||
/// affected.
|
||||
///
|
||||
/// This is disabled by default.
|
||||
pub fn case_insensitive(
|
||||
&mut self,
|
||||
yes: bool,
|
||||
) -> Result<&mut GitignoreBuilder, Error> {
|
||||
// TODO: This should not return a `Result`. Fix this in the next semver
|
||||
// release.
|
||||
self.case_insensitive = yes;
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// Toggle whether unclosed character classes are allowed. When allowed,
|
||||
/// a `[` without a matching `]` is treated literally instead of resulting
|
||||
/// in a parse error.
|
||||
///
|
||||
/// For example, if this is set then the glob `[abc` will be treated as the
|
||||
/// literal string `[abc` instead of returning an error.
|
||||
///
|
||||
/// By default, this is true in order to match established `gitignore`
|
||||
/// semantics. Generally speaking, enabling this leads to worse failure
|
||||
/// modes since the glob parser becomes more permissive. You might want to
|
||||
/// enable this when compatibility (e.g., with POSIX glob implementations)
|
||||
/// is more important than good error messages.
|
||||
pub fn allow_unclosed_class(
|
||||
&mut self,
|
||||
yes: bool,
|
||||
) -> &mut GitignoreBuilder {
|
||||
self.allow_unclosed_class = yes;
|
||||
self
|
||||
}
|
||||
|
||||
/// CHANGED: Add a flag to have Gitignore rules that apply only to files.
|
||||
///
|
||||
/// If this is set, then the globs will only be matched against file paths.
|
||||
/// This will ensure that ignore rules like `*.pages` will _only_ ignore
|
||||
/// files ending in `.pages` and not folders ending in `.pages`.
|
||||
pub fn only_on_files(&mut self, yes: bool) -> &mut GitignoreBuilder {
|
||||
self.only_on_files = yes;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the file path of the current environment's global gitignore file.
|
||||
///
|
||||
/// Note that the file path returned may not exist.
|
||||
pub fn gitconfig_excludes_path() -> Option<PathBuf> {
|
||||
// When GIT_CONFIG_GLOBAL is set, it replaces both $HOME/.gitconfig and
|
||||
// $XDG_CONFIG_HOME/git/config (per git 2.32+). Otherwise, git supports
|
||||
// $HOME/.gitconfig and $XDG_CONFIG_HOME/git/config simultaneously, where
|
||||
// $HOME/.gitconfig takes precedent.
|
||||
gitconfig_global_env_contents()
|
||||
.and_then(|x| parse_excludes_file(&x))
|
||||
.or_else(|| {
|
||||
gitconfig_home_contents().and_then(|x| parse_excludes_file(&x))
|
||||
})
|
||||
.or_else(|| {
|
||||
gitconfig_xdg_contents().and_then(|x| parse_excludes_file(&x))
|
||||
})
|
||||
// System-level config has the lowest priority for core.excludesFile.
|
||||
// GIT_CONFIG_SYSTEM overrides the default /etc/gitconfig path.
|
||||
.or_else(|| {
|
||||
gitconfig_system_contents().and_then(|x| parse_excludes_file(&x))
|
||||
})
|
||||
.or_else(excludes_file_default)
|
||||
}
|
||||
|
||||
/// Returns the file contents of git's global config file from the path
|
||||
/// specified by the `GIT_CONFIG_GLOBAL` environment variable.
|
||||
fn gitconfig_global_env_contents() -> Option<Vec<u8>> {
|
||||
let path = std::env::var_os("GIT_CONFIG_GLOBAL").map(PathBuf::from)?;
|
||||
if path.as_os_str().is_empty() {
|
||||
return None;
|
||||
}
|
||||
let mut file = BufReader::new(File::open(path).ok()?);
|
||||
let mut contents = vec![];
|
||||
file.read_to_end(&mut contents).ok().map(|_| contents)
|
||||
}
|
||||
|
||||
/// Returns the file contents of git's system-level config file.
|
||||
///
|
||||
/// Checks `GIT_CONFIG_SYSTEM` first, then falls back to `/etc/gitconfig`.
|
||||
fn gitconfig_system_contents() -> Option<Vec<u8>> {
|
||||
let path = std::env::var_os("GIT_CONFIG_SYSTEM")
|
||||
.map(PathBuf::from)
|
||||
.filter(|x| !x.as_os_str().is_empty())
|
||||
.unwrap_or_else(|| PathBuf::from("/etc/gitconfig"));
|
||||
let mut file = BufReader::new(File::open(path).ok()?);
|
||||
let mut contents = vec![];
|
||||
file.read_to_end(&mut contents).ok().map(|_| contents)
|
||||
}
|
||||
|
||||
/// Returns the file contents of git's global config file, if one exists, in
|
||||
/// the user's home directory.
|
||||
fn gitconfig_home_contents() -> Option<Vec<u8>> {
|
||||
let home = home_dir()?;
|
||||
let mut file = BufReader::new(File::open(home.join(".gitconfig")).ok()?);
|
||||
let mut contents = vec![];
|
||||
file.read_to_end(&mut contents).ok().map(|_| contents)
|
||||
}
|
||||
|
||||
/// Returns the file contents of git's global config file, if one exists, in
|
||||
/// the user's XDG_CONFIG_HOME directory.
|
||||
fn gitconfig_xdg_contents() -> Option<Vec<u8>> {
|
||||
let path = std::env::var_os("XDG_CONFIG_HOME")
|
||||
.map(PathBuf::from)
|
||||
.filter(|x| !x.as_os_str().is_empty())
|
||||
.or_else(|| home_dir().map(|p| p.join(".config")))
|
||||
.map(|x| x.join("git/config"))?;
|
||||
let mut file = BufReader::new(File::open(path).ok()?);
|
||||
let mut contents = vec![];
|
||||
file.read_to_end(&mut contents).ok().map(|_| contents)
|
||||
}
|
||||
|
||||
/// Returns the default file path for a global .gitignore file.
|
||||
///
|
||||
/// Specifically, this respects XDG_CONFIG_HOME.
|
||||
fn excludes_file_default() -> Option<PathBuf> {
|
||||
std::env::var_os("XDG_CONFIG_HOME")
|
||||
.map(PathBuf::from)
|
||||
.filter(|x| !x.as_os_str().is_empty())
|
||||
.or_else(|| home_dir().map(|p| p.join(".config")))
|
||||
.map(|x| x.join("git/ignore"))
|
||||
}
|
||||
|
||||
/// Extract git's `core.excludesfile` config setting from the raw file contents
|
||||
/// given.
|
||||
fn parse_excludes_file(data: &[u8]) -> Option<PathBuf> {
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use regex_automata::{meta::Regex, util::syntax};
|
||||
|
||||
// N.B. This is the lazy approach, and isn't technically correct, but
|
||||
// probably works in more circumstances. I guess we would ideally have
|
||||
// a full INI parser. Yuck.
|
||||
static RE: OnceLock<Regex> = OnceLock::new();
|
||||
let re = RE.get_or_init(|| {
|
||||
Regex::builder()
|
||||
.configure(Regex::config().utf8_empty(false))
|
||||
.syntax(syntax::Config::new().utf8(false))
|
||||
.build(r#"(?im-u)^\s*excludesfile\s*=\s*"?\s*(\S+?)\s*"?\s*$"#)
|
||||
.unwrap()
|
||||
});
|
||||
// We don't care about amortizing allocs here I think. This should only
|
||||
// be called ~once per traversal or so? (Although it's not guaranteed...)
|
||||
let mut caps = re.create_captures();
|
||||
re.captures(data, &mut caps);
|
||||
let span = caps.get_group(1)?;
|
||||
let candidate = &data[span];
|
||||
std::str::from_utf8(candidate).ok().map(|s| PathBuf::from(expand_tilde(s)))
|
||||
}
|
||||
|
||||
/// Expands ~ in file paths to the value of $HOME.
|
||||
fn expand_tilde(path: &str) -> String {
|
||||
let home = match home_dir() {
|
||||
None => return path.to_string(),
|
||||
Some(home) => home.to_string_lossy().into_owned(),
|
||||
};
|
||||
path.replace("~", &home)
|
||||
}
|
||||
|
||||
/// Returns the location of the user's home directory.
|
||||
fn home_dir() -> Option<PathBuf> {
|
||||
// We're fine with using std::env::home_dir for now. Its bugs are, IMO,
|
||||
// pretty minor corner cases.
|
||||
#![allow(deprecated)]
|
||||
std::env::home_dir()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::Path;
|
||||
|
||||
use super::{Gitignore, GitignoreBuilder};
|
||||
|
||||
fn gi_from_str<P: AsRef<Path>>(root: P, s: &str) -> Gitignore {
|
||||
let mut builder = GitignoreBuilder::new(root);
|
||||
builder.add_str(None, s).unwrap();
|
||||
builder.build().unwrap()
|
||||
}
|
||||
|
||||
macro_rules! ignored {
|
||||
($name:ident, $root:expr, $gi:expr, $path:expr) => {
|
||||
ignored!($name, $root, $gi, $path, false);
|
||||
};
|
||||
($name:ident, $root:expr, $gi:expr, $path:expr, $is_dir:expr) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
let gi = gi_from_str($root, $gi);
|
||||
assert!(gi.matched($path, $is_dir).is_ignore());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! not_ignored {
|
||||
($name:ident, $root:expr, $gi:expr, $path:expr) => {
|
||||
not_ignored!($name, $root, $gi, $path, false);
|
||||
};
|
||||
($name:ident, $root:expr, $gi:expr, $path:expr, $is_dir:expr) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
let gi = gi_from_str($root, $gi);
|
||||
assert!(!gi.matched($path, $is_dir).is_ignore());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const ROOT: &'static str = "/home/foobar/rust/rg";
|
||||
|
||||
ignored!(ig1, ROOT, "months", "months");
|
||||
ignored!(ig2, ROOT, "*.lock", "Cargo.lock");
|
||||
ignored!(ig3, ROOT, "*.rs", "src/main.rs");
|
||||
ignored!(ig4, ROOT, "src/*.rs", "src/main.rs");
|
||||
ignored!(ig5, ROOT, "/*.c", "cat-file.c");
|
||||
ignored!(ig6, ROOT, "/src/*.rs", "src/main.rs");
|
||||
ignored!(ig7, ROOT, "!src/main.rs\n*.rs", "src/main.rs");
|
||||
ignored!(ig8, ROOT, "foo/", "foo", true);
|
||||
ignored!(ig9, ROOT, "**/foo", "foo");
|
||||
ignored!(ig10, ROOT, "**/foo", "src/foo");
|
||||
ignored!(ig11, ROOT, "**/foo/**", "src/foo/bar");
|
||||
ignored!(ig12, ROOT, "**/foo/**", "wat/src/foo/bar/baz");
|
||||
ignored!(ig13, ROOT, "**/foo/bar", "foo/bar");
|
||||
ignored!(ig14, ROOT, "**/foo/bar", "src/foo/bar");
|
||||
ignored!(ig15, ROOT, "abc/**", "abc/x");
|
||||
ignored!(ig16, ROOT, "abc/**", "abc/x/y");
|
||||
ignored!(ig17, ROOT, "abc/**", "abc/x/y/z");
|
||||
ignored!(ig18, ROOT, "a/**/b", "a/b");
|
||||
ignored!(ig19, ROOT, "a/**/b", "a/x/b");
|
||||
ignored!(ig20, ROOT, "a/**/b", "a/x/y/b");
|
||||
ignored!(ig21, ROOT, r"\!xy", "!xy");
|
||||
ignored!(ig22, ROOT, r"\#foo", "#foo");
|
||||
ignored!(ig23, ROOT, "foo", "./foo");
|
||||
ignored!(ig24, ROOT, "target", "grep/target");
|
||||
ignored!(ig25, ROOT, "Cargo.lock", "./tabwriter-bin/Cargo.lock");
|
||||
ignored!(ig26, ROOT, "/foo/bar/baz", "./foo/bar/baz");
|
||||
ignored!(ig27, ROOT, "foo/", "xyz/foo", true);
|
||||
ignored!(ig28, "./src", "/llvm/", "./src/llvm", true);
|
||||
ignored!(ig29, ROOT, "node_modules/ ", "node_modules", true);
|
||||
ignored!(ig30, ROOT, "**/", "foo/bar", true);
|
||||
ignored!(ig31, ROOT, "path1/*", "path1/foo");
|
||||
ignored!(ig32, ROOT, ".a/b", ".a/b");
|
||||
ignored!(ig33, "./", ".a/b", ".a/b");
|
||||
ignored!(ig34, ".", ".a/b", ".a/b");
|
||||
ignored!(ig35, "./.", ".a/b", ".a/b");
|
||||
ignored!(ig36, "././", ".a/b", ".a/b");
|
||||
ignored!(ig37, "././.", ".a/b", ".a/b");
|
||||
ignored!(ig38, ROOT, "\\[", "[");
|
||||
ignored!(ig39, ROOT, "\\?", "?");
|
||||
ignored!(ig40, ROOT, "\\*", "*");
|
||||
ignored!(ig41, ROOT, "\\a", "a");
|
||||
ignored!(ig42, ROOT, "s*.rs", "sfoo.rs");
|
||||
ignored!(ig43, ROOT, "**", "foo.rs");
|
||||
ignored!(ig44, ROOT, "**/**/*", "a/foo.rs");
|
||||
|
||||
not_ignored!(ignot1, ROOT, "amonths", "months");
|
||||
not_ignored!(ignot2, ROOT, "monthsa", "months");
|
||||
not_ignored!(ignot3, ROOT, "/src/*.rs", "src/grep/src/main.rs");
|
||||
not_ignored!(ignot4, ROOT, "/*.c", "mozilla-sha1/sha1.c");
|
||||
not_ignored!(ignot5, ROOT, "/src/*.rs", "src/grep/src/main.rs");
|
||||
not_ignored!(ignot6, ROOT, "*.rs\n!src/main.rs", "src/main.rs");
|
||||
not_ignored!(ignot7, ROOT, "foo/", "foo", false);
|
||||
not_ignored!(ignot8, ROOT, "**/foo/**", "wat/src/afoo/bar/baz");
|
||||
not_ignored!(ignot9, ROOT, "**/foo/**", "wat/src/fooa/bar/baz");
|
||||
not_ignored!(ignot10, ROOT, "**/foo/bar", "foo/src/bar");
|
||||
not_ignored!(ignot11, ROOT, "#foo", "#foo");
|
||||
not_ignored!(ignot12, ROOT, "\n\n\n", "foo");
|
||||
not_ignored!(ignot13, ROOT, "foo/**", "foo", true);
|
||||
not_ignored!(
|
||||
ignot14,
|
||||
"./third_party/protobuf",
|
||||
"m4/ltoptions.m4",
|
||||
"./third_party/protobuf/csharp/src/packages/repositories.config"
|
||||
);
|
||||
not_ignored!(ignot15, ROOT, "!/bar", "foo/bar");
|
||||
not_ignored!(ignot16, ROOT, "*\n!**/", "foo", true);
|
||||
not_ignored!(ignot17, ROOT, "src/*.rs", "src/grep/src/main.rs");
|
||||
not_ignored!(ignot18, ROOT, "path1/*", "path2/path1/foo");
|
||||
not_ignored!(ignot19, ROOT, "s*.rs", "src/foo.rs");
|
||||
|
||||
fn bytes(s: &str) -> Vec<u8> {
|
||||
s.to_string().into_bytes()
|
||||
}
|
||||
|
||||
fn path_string<P: AsRef<Path>>(path: P) -> String {
|
||||
path.as_ref().to_str().unwrap().to_string()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_excludes_file1() {
|
||||
let data = bytes("[core]\nexcludesFile = /foo/bar");
|
||||
let got = super::parse_excludes_file(&data).unwrap();
|
||||
assert_eq!(path_string(got), "/foo/bar");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_excludes_file2() {
|
||||
let data = bytes("[core]\nexcludesFile = ~/foo/bar");
|
||||
let got = super::parse_excludes_file(&data).unwrap();
|
||||
assert_eq!(path_string(got), super::expand_tilde("~/foo/bar"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_excludes_file3() {
|
||||
let data = bytes("[core]\nexcludeFile = /foo/bar");
|
||||
assert!(super::parse_excludes_file(&data).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_excludes_file4() {
|
||||
let data = bytes("[core]\nexcludesFile = \"~/foo/bar\"");
|
||||
let got = super::parse_excludes_file(&data);
|
||||
assert_eq!(
|
||||
path_string(got.unwrap()),
|
||||
super::expand_tilde("~/foo/bar")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_excludes_file5() {
|
||||
let data = bytes("[core]\nexcludesFile = \" \"~/foo/bar \" \"");
|
||||
assert!(super::parse_excludes_file(&data).is_none());
|
||||
}
|
||||
|
||||
// See: https://github.com/BurntSushi/ripgrep/issues/106
|
||||
#[test]
|
||||
fn regression_106() {
|
||||
gi_from_str("/", " ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn case_insensitive() {
|
||||
let gi = GitignoreBuilder::new(ROOT)
|
||||
.case_insensitive(true)
|
||||
.unwrap()
|
||||
.add_str(None, "*.html")
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap();
|
||||
assert!(gi.matched("foo.html", false).is_ignore());
|
||||
assert!(gi.matched("foo.HTML", false).is_ignore());
|
||||
assert!(!gi.matched("foo.htm", false).is_ignore());
|
||||
assert!(!gi.matched("foo.HTM", false).is_ignore());
|
||||
}
|
||||
|
||||
ignored!(cs1, ROOT, "*.html", "foo.html");
|
||||
not_ignored!(cs2, ROOT, "*.html", "foo.HTML");
|
||||
not_ignored!(cs3, ROOT, "*.html", "foo.htm");
|
||||
not_ignored!(cs4, ROOT, "*.html", "foo.HTM");
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,549 +0,0 @@
|
|||
/*!
|
||||
The ignore crate provides a fast recursive directory iterator that respects
|
||||
various filters such as globs, file types and `.gitignore` files. The precise
|
||||
matching rules and precedence is explained in the documentation for
|
||||
`WalkBuilder`.
|
||||
|
||||
Secondarily, this crate exposes gitignore and file type matchers for use cases
|
||||
that demand more fine-grained control.
|
||||
|
||||
# Example
|
||||
|
||||
This example shows the most basic usage of this crate. This code will
|
||||
recursively traverse the current directory while automatically filtering out
|
||||
files and directories according to ignore globs found in files like
|
||||
`.ignore` and `.gitignore`:
|
||||
|
||||
|
||||
```rust,no_run
|
||||
use ignore::Walk;
|
||||
|
||||
for result in Walk::new("./") {
|
||||
// Each item yielded by the iterator is either a directory entry or an
|
||||
// error, so either print the path or the error.
|
||||
match result {
|
||||
Ok(entry) => println!("{}", entry.path().display()),
|
||||
Err(err) => println!("ERROR: {}", err),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# Example: advanced
|
||||
|
||||
By default, the recursive directory iterator will ignore hidden files and
|
||||
directories. This can be disabled by building the iterator with `WalkBuilder`:
|
||||
|
||||
```rust,no_run
|
||||
use ignore::WalkBuilder;
|
||||
|
||||
for result in WalkBuilder::new("./").hidden(false).build() {
|
||||
println!("{:?}", result);
|
||||
}
|
||||
```
|
||||
|
||||
See the documentation for `WalkBuilder` for many other options.
|
||||
*/
|
||||
|
||||
#![deny(missing_docs)]
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
pub use crate::incremental::{IncrementalIgnore, IncrementalMatch};
|
||||
pub use crate::walk::{
|
||||
DirEntry, ParallelVisitor, ParallelVisitorBuilder, Walk, WalkBuilder,
|
||||
WalkParallel, WalkState,
|
||||
};
|
||||
|
||||
mod default_types;
|
||||
mod dir;
|
||||
pub mod gitignore;
|
||||
mod incremental;
|
||||
pub mod overrides;
|
||||
mod pathutil;
|
||||
pub mod types;
|
||||
mod walk;
|
||||
|
||||
/// Represents an error that can occur when parsing a gitignore file.
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
/// A collection of "soft" errors. These occur when adding an ignore
|
||||
/// file partially succeeded.
|
||||
Partial(Vec<Error>),
|
||||
/// An error associated with a specific line number.
|
||||
WithLineNumber {
|
||||
/// The line number.
|
||||
line: u64,
|
||||
/// The underlying error.
|
||||
err: Box<Error>,
|
||||
},
|
||||
/// An error associated with a particular file path.
|
||||
WithPath {
|
||||
/// The file path.
|
||||
path: PathBuf,
|
||||
/// The underlying error.
|
||||
err: Box<Error>,
|
||||
},
|
||||
/// An error associated with a particular directory depth when recursively
|
||||
/// walking a directory.
|
||||
WithDepth {
|
||||
/// The directory depth.
|
||||
depth: usize,
|
||||
/// The underlying error.
|
||||
err: Box<Error>,
|
||||
},
|
||||
/// An error that occurs when a file loop is detected when traversing
|
||||
/// symbolic links.
|
||||
Loop {
|
||||
/// The ancestor file path in the loop.
|
||||
ancestor: PathBuf,
|
||||
/// The child file path in the loop.
|
||||
child: PathBuf,
|
||||
},
|
||||
/// An error that occurs when doing I/O, such as reading an ignore file.
|
||||
Io(std::io::Error),
|
||||
/// An error that occurs when trying to parse a glob.
|
||||
Glob {
|
||||
/// The original glob that caused this error. This glob, when
|
||||
/// available, always corresponds to the glob provided by an end user.
|
||||
/// e.g., It is the glob as written in a `.gitignore` file.
|
||||
///
|
||||
/// (This glob may be distinct from the glob that is actually
|
||||
/// compiled, after accounting for `gitignore` semantics.)
|
||||
glob: Option<String>,
|
||||
/// The underlying glob error as a string.
|
||||
err: String,
|
||||
},
|
||||
/// A type selection for a file type that is not defined.
|
||||
UnrecognizedFileType(String),
|
||||
/// A user specified file type definition could not be parsed.
|
||||
InvalidDefinition,
|
||||
}
|
||||
|
||||
impl Clone for Error {
|
||||
fn clone(&self) -> Error {
|
||||
match *self {
|
||||
Error::Partial(ref errs) => Error::Partial(errs.clone()),
|
||||
Error::WithLineNumber { line, ref err } => {
|
||||
Error::WithLineNumber { line, err: err.clone() }
|
||||
}
|
||||
Error::WithPath { ref path, ref err } => {
|
||||
Error::WithPath { path: path.clone(), err: err.clone() }
|
||||
}
|
||||
Error::WithDepth { depth, ref err } => {
|
||||
Error::WithDepth { depth, err: err.clone() }
|
||||
}
|
||||
Error::Loop { ref ancestor, ref child } => Error::Loop {
|
||||
ancestor: ancestor.clone(),
|
||||
child: child.clone(),
|
||||
},
|
||||
Error::Io(ref err) => match err.raw_os_error() {
|
||||
Some(e) => Error::Io(std::io::Error::from_raw_os_error(e)),
|
||||
None => {
|
||||
Error::Io(std::io::Error::new(err.kind(), err.to_string()))
|
||||
}
|
||||
},
|
||||
Error::Glob { ref glob, ref err } => {
|
||||
Error::Glob { glob: glob.clone(), err: err.clone() }
|
||||
}
|
||||
Error::UnrecognizedFileType(ref err) => {
|
||||
Error::UnrecognizedFileType(err.clone())
|
||||
}
|
||||
Error::InvalidDefinition => Error::InvalidDefinition,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error {
|
||||
/// Returns true if this is a partial error.
|
||||
///
|
||||
/// A partial error occurs when only some operations failed while others
|
||||
/// may have succeeded. For example, an ignore file may contain an invalid
|
||||
/// glob among otherwise valid globs.
|
||||
pub fn is_partial(&self) -> bool {
|
||||
match *self {
|
||||
Error::Partial(_) => true,
|
||||
Error::WithLineNumber { ref err, .. } => err.is_partial(),
|
||||
Error::WithPath { ref err, .. } => err.is_partial(),
|
||||
Error::WithDepth { ref err, .. } => err.is_partial(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if this error is exclusively an I/O error.
|
||||
pub fn is_io(&self) -> bool {
|
||||
match *self {
|
||||
Error::Partial(ref errs) => errs.len() == 1 && errs[0].is_io(),
|
||||
Error::WithLineNumber { ref err, .. } => err.is_io(),
|
||||
Error::WithPath { ref err, .. } => err.is_io(),
|
||||
Error::WithDepth { ref err, .. } => err.is_io(),
|
||||
Error::Loop { .. } => false,
|
||||
Error::Io(_) => true,
|
||||
Error::Glob { .. } => false,
|
||||
Error::UnrecognizedFileType(_) => false,
|
||||
Error::InvalidDefinition => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Inspect the original [`std::io::Error`] if there is one.
|
||||
///
|
||||
/// [`None`] is returned if the [`Error`] doesn't correspond to an
|
||||
/// [`std::io::Error`]. This might happen, for example, when the error was
|
||||
/// produced because a cycle was found in the directory tree while
|
||||
/// following symbolic links.
|
||||
///
|
||||
/// This method returns a borrowed value that is bound to the lifetime of the [`Error`]. To
|
||||
/// obtain an owned value, the [`into_io_error`] can be used instead.
|
||||
///
|
||||
/// > This is the original [`std::io::Error`] and is _not_ the same as
|
||||
/// > [`impl From<Error> for std::io::Error`][impl] which contains
|
||||
/// > additional context about the error.
|
||||
///
|
||||
/// [`None`]: https://doc.rust-lang.org/stable/std/option/enum.Option.html#variant.None
|
||||
/// [`std::io::Error`]: https://doc.rust-lang.org/stable/std/io/struct.Error.html
|
||||
/// [`From`]: https://doc.rust-lang.org/stable/std/convert/trait.From.html
|
||||
/// [`Error`]: struct.Error.html
|
||||
/// [`into_io_error`]: struct.Error.html#method.into_io_error
|
||||
/// [impl]: struct.Error.html#impl-From%3CError%3E
|
||||
pub fn io_error(&self) -> Option<&std::io::Error> {
|
||||
match *self {
|
||||
Error::Partial(ref errs) => {
|
||||
if errs.len() == 1 {
|
||||
errs[0].io_error()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Error::WithLineNumber { ref err, .. } => err.io_error(),
|
||||
Error::WithPath { ref err, .. } => err.io_error(),
|
||||
Error::WithDepth { ref err, .. } => err.io_error(),
|
||||
Error::Loop { .. } => None,
|
||||
Error::Io(ref err) => Some(err),
|
||||
Error::Glob { .. } => None,
|
||||
Error::UnrecognizedFileType(_) => None,
|
||||
Error::InvalidDefinition => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Similar to [`io_error`] except consumes self to convert to the original
|
||||
/// [`std::io::Error`] if one exists.
|
||||
///
|
||||
/// [`io_error`]: struct.Error.html#method.io_error
|
||||
/// [`std::io::Error`]: https://doc.rust-lang.org/stable/std/io/struct.Error.html
|
||||
pub fn into_io_error(self) -> Option<std::io::Error> {
|
||||
match self {
|
||||
Error::Partial(mut errs) => {
|
||||
if errs.len() == 1 {
|
||||
errs.remove(0).into_io_error()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Error::WithLineNumber { err, .. } => err.into_io_error(),
|
||||
Error::WithPath { err, .. } => err.into_io_error(),
|
||||
Error::WithDepth { err, .. } => err.into_io_error(),
|
||||
Error::Loop { .. } => None,
|
||||
Error::Io(err) => Some(err),
|
||||
Error::Glob { .. } => None,
|
||||
Error::UnrecognizedFileType(_) => None,
|
||||
Error::InvalidDefinition => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a depth associated with recursively walking a directory (if
|
||||
/// this error was generated from a recursive directory iterator).
|
||||
pub fn depth(&self) -> Option<usize> {
|
||||
match *self {
|
||||
Error::WithPath { ref err, .. } => err.depth(),
|
||||
Error::WithDepth { depth, .. } => Some(depth),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Turn an error into a tagged error with the given file path.
|
||||
fn with_path<P: AsRef<Path>>(self, path: P) -> Error {
|
||||
Error::WithPath {
|
||||
path: path.as_ref().to_path_buf(),
|
||||
err: Box::new(self),
|
||||
}
|
||||
}
|
||||
|
||||
/// Turn an error into a tagged error with the given depth.
|
||||
fn with_depth(self, depth: usize) -> Error {
|
||||
Error::WithDepth { depth, err: Box::new(self) }
|
||||
}
|
||||
|
||||
/// Turn an error into a tagged error with the given file path and line
|
||||
/// number. If path is empty, then it is omitted from the error.
|
||||
fn tagged<P: AsRef<Path>>(self, path: P, lineno: u64) -> Error {
|
||||
let errline =
|
||||
Error::WithLineNumber { line: lineno, err: Box::new(self) };
|
||||
if path.as_ref().as_os_str().is_empty() {
|
||||
return errline;
|
||||
}
|
||||
errline.with_path(path)
|
||||
}
|
||||
|
||||
/// Build an error from a walkdir error.
|
||||
fn from_walkdir(err: walkdir::Error) -> Error {
|
||||
let depth = err.depth();
|
||||
if let (Some(anc), Some(child)) = (err.loop_ancestor(), err.path()) {
|
||||
return Error::WithDepth {
|
||||
depth,
|
||||
err: Box::new(Error::Loop {
|
||||
ancestor: anc.to_path_buf(),
|
||||
child: child.to_path_buf(),
|
||||
}),
|
||||
};
|
||||
}
|
||||
let path = err.path().map(|p| p.to_path_buf());
|
||||
let mut ig_err = Error::WithDepth {
|
||||
depth,
|
||||
err: Box::new(Error::Io(std::io::Error::from(err))),
|
||||
};
|
||||
if let Some(path) = path {
|
||||
ig_err = Error::WithPath { path, err: Box::new(ig_err) };
|
||||
}
|
||||
ig_err
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {
|
||||
#[allow(deprecated)]
|
||||
fn description(&self) -> &str {
|
||||
match *self {
|
||||
Error::Partial(_) => "partial error",
|
||||
Error::WithLineNumber { ref err, .. } => err.description(),
|
||||
Error::WithPath { ref err, .. } => err.description(),
|
||||
Error::WithDepth { ref err, .. } => err.description(),
|
||||
Error::Loop { .. } => "file system loop found",
|
||||
Error::Io(ref err) => err.description(),
|
||||
Error::Glob { ref err, .. } => err,
|
||||
Error::UnrecognizedFileType(_) => "unrecognized file type",
|
||||
Error::InvalidDefinition => "invalid definition",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match *self {
|
||||
Error::Partial(ref errs) => {
|
||||
let msgs: Vec<String> =
|
||||
errs.iter().map(|err| err.to_string()).collect();
|
||||
write!(f, "{}", msgs.join("\n"))
|
||||
}
|
||||
Error::WithLineNumber { line, ref err } => {
|
||||
write!(f, "line {}: {}", line, err)
|
||||
}
|
||||
Error::WithPath { ref path, ref err } => {
|
||||
write!(f, "{}: {}", path.display(), err)
|
||||
}
|
||||
Error::WithDepth { ref err, .. } => err.fmt(f),
|
||||
Error::Loop { ref ancestor, ref child } => write!(
|
||||
f,
|
||||
"File system loop found: \
|
||||
{} points to an ancestor {}",
|
||||
child.display(),
|
||||
ancestor.display()
|
||||
),
|
||||
Error::Io(ref err) => err.fmt(f),
|
||||
Error::Glob { glob: None, ref err } => write!(f, "{}", err),
|
||||
Error::Glob { glob: Some(ref glob), ref err } => {
|
||||
write!(f, "error parsing glob '{}': {}", glob, err)
|
||||
}
|
||||
Error::UnrecognizedFileType(ref ty) => {
|
||||
write!(f, "unrecognized file type: {}", ty)
|
||||
}
|
||||
Error::InvalidDefinition => write!(
|
||||
f,
|
||||
"invalid definition (format is type:glob, e.g., \
|
||||
html:*.html)"
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for Error {
|
||||
fn from(err: std::io::Error) -> Error {
|
||||
Error::Io(err)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct PartialErrorBuilder(Vec<Error>);
|
||||
|
||||
impl PartialErrorBuilder {
|
||||
fn push(&mut self, err: Error) {
|
||||
self.0.push(err);
|
||||
}
|
||||
|
||||
fn push_ignore_io(&mut self, err: Error) {
|
||||
if !err.is_io() {
|
||||
self.push(err);
|
||||
}
|
||||
}
|
||||
|
||||
fn maybe_push(&mut self, err: Option<Error>) {
|
||||
if let Some(err) = err {
|
||||
self.push(err);
|
||||
}
|
||||
}
|
||||
|
||||
fn maybe_push_ignore_io(&mut self, err: Option<Error>) {
|
||||
if let Some(err) = err {
|
||||
self.push_ignore_io(err);
|
||||
}
|
||||
}
|
||||
|
||||
fn into_error_option(mut self) -> Option<Error> {
|
||||
if self.0.is_empty() {
|
||||
None
|
||||
} else if self.0.len() == 1 {
|
||||
Some(self.0.pop().unwrap())
|
||||
} else {
|
||||
Some(Error::Partial(self.0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The result of a glob match.
|
||||
///
|
||||
/// The type parameter `T` typically refers to a type that provides more
|
||||
/// information about a particular match. For example, it might identify
|
||||
/// the specific gitignore file and the specific glob pattern that caused
|
||||
/// the match.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Match<T> {
|
||||
/// The path didn't match any glob.
|
||||
None,
|
||||
/// The highest precedent glob matched indicates the path should be
|
||||
/// ignored.
|
||||
Ignore(T),
|
||||
/// The highest precedent glob matched indicates the path should be
|
||||
/// whitelisted.
|
||||
Whitelist(T),
|
||||
}
|
||||
|
||||
impl<T> Match<T> {
|
||||
/// Returns true if the match result didn't match any globs.
|
||||
pub fn is_none(&self) -> bool {
|
||||
match *self {
|
||||
Match::None => true,
|
||||
Match::Ignore(_) | Match::Whitelist(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the match result implies the path should be ignored.
|
||||
pub fn is_ignore(&self) -> bool {
|
||||
match *self {
|
||||
Match::Ignore(_) => true,
|
||||
Match::None | Match::Whitelist(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the match result implies the path should be
|
||||
/// whitelisted.
|
||||
pub fn is_whitelist(&self) -> bool {
|
||||
match *self {
|
||||
Match::Whitelist(_) => true,
|
||||
Match::None | Match::Ignore(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Inverts the match so that `Ignore` becomes `Whitelist` and
|
||||
/// `Whitelist` becomes `Ignore`. A non-match remains the same.
|
||||
pub fn invert(self) -> Match<T> {
|
||||
match self {
|
||||
Match::None => Match::None,
|
||||
Match::Ignore(t) => Match::Whitelist(t),
|
||||
Match::Whitelist(t) => Match::Ignore(t),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the value inside this match if it exists.
|
||||
pub fn inner(&self) -> Option<&T> {
|
||||
match *self {
|
||||
Match::None => None,
|
||||
Match::Ignore(ref t) => Some(t),
|
||||
Match::Whitelist(ref t) => Some(t),
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply the given function to the value inside this match.
|
||||
///
|
||||
/// If the match has no value, then return the match unchanged.
|
||||
pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> Match<U> {
|
||||
match self {
|
||||
Match::None => Match::None,
|
||||
Match::Ignore(t) => Match::Ignore(f(t)),
|
||||
Match::Whitelist(t) => Match::Whitelist(f(t)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the match if it is not none. Otherwise, return other.
|
||||
pub fn or(self, other: Self) -> Self {
|
||||
if self.is_none() { other } else { self }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{
|
||||
env, fs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
/// A convenient result type alias.
|
||||
pub(crate) type Result<T> =
|
||||
std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;
|
||||
|
||||
macro_rules! err {
|
||||
($($tt:tt)*) => {
|
||||
Box::<dyn std::error::Error + Send + Sync>::from(format!($($tt)*))
|
||||
}
|
||||
}
|
||||
|
||||
/// A simple wrapper for creating a temporary directory that is
|
||||
/// automatically deleted when it's dropped.
|
||||
///
|
||||
/// We use this in lieu of tempfile because tempfile brings in too many
|
||||
/// dependencies.
|
||||
#[derive(Debug)]
|
||||
pub struct TempDir(PathBuf);
|
||||
|
||||
impl Drop for TempDir {
|
||||
fn drop(&mut self) {
|
||||
fs::remove_dir_all(&self.0).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
impl TempDir {
|
||||
/// Create a new empty temporary directory under the system's configured
|
||||
/// temporary directory.
|
||||
pub fn new() -> Result<TempDir> {
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
static TRIES: usize = 100;
|
||||
static COUNTER: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
let tmpdir = env::temp_dir();
|
||||
for _ in 0..TRIES {
|
||||
let count = COUNTER.fetch_add(1, Ordering::Relaxed);
|
||||
let path = tmpdir.join("rust-ignore").join(count.to_string());
|
||||
if path.is_dir() {
|
||||
continue;
|
||||
}
|
||||
fs::create_dir_all(&path).map_err(|e| {
|
||||
err!("failed to create {}: {}", path.display(), e)
|
||||
})?;
|
||||
return Ok(TempDir(path));
|
||||
}
|
||||
Err(err!("failed to create temp dir after {} tries", TRIES))
|
||||
}
|
||||
|
||||
/// Return the underlying path to this temporary directory.
|
||||
pub fn path(&self) -> &Path {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,293 +0,0 @@
|
|||
/*!
|
||||
The overrides module provides a way to specify a set of override globs.
|
||||
|
||||
This provides functionality similar to `--include` or `--exclude` in command
|
||||
line tools.
|
||||
*/
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use crate::{
|
||||
Error, Match,
|
||||
gitignore::{self, Gitignore, GitignoreBuilder},
|
||||
};
|
||||
|
||||
/// Glob represents a single glob in an override matcher.
|
||||
///
|
||||
/// This is used to report information about the highest precedent glob
|
||||
/// that matched.
|
||||
///
|
||||
/// Note that not all matches necessarily correspond to a specific glob. For
|
||||
/// example, if there are one or more whitelist globs and a file path doesn't
|
||||
/// match any glob in the set, then the file path is considered to be ignored.
|
||||
///
|
||||
/// The lifetime `'a` refers to the lifetime of the matcher that produced
|
||||
/// this glob.
|
||||
#[derive(Clone, Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub struct Glob<'a>(GlobInner<'a>);
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[allow(dead_code)]
|
||||
enum GlobInner<'a> {
|
||||
/// No glob matched, but the file path should still be ignored.
|
||||
UnmatchedIgnore,
|
||||
/// A glob matched.
|
||||
Matched(&'a gitignore::Glob),
|
||||
}
|
||||
|
||||
impl<'a> Glob<'a> {
|
||||
fn unmatched() -> Glob<'a> {
|
||||
Glob(GlobInner::UnmatchedIgnore)
|
||||
}
|
||||
}
|
||||
|
||||
/// Manages a set of overrides provided explicitly by the end user.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Override(Gitignore);
|
||||
|
||||
impl Override {
|
||||
/// Returns an empty matcher that never matches any file path.
|
||||
pub fn empty() -> Override {
|
||||
Override(Gitignore::empty())
|
||||
}
|
||||
|
||||
/// Returns the directory of this override set.
|
||||
///
|
||||
/// All matches are done relative to this path.
|
||||
pub fn path(&self) -> &Path {
|
||||
self.0.path()
|
||||
}
|
||||
|
||||
/// Returns true if and only if this matcher is empty.
|
||||
///
|
||||
/// When a matcher is empty, it will never match any file path.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
|
||||
/// Returns the total number of ignore globs.
|
||||
pub fn num_ignores(&self) -> u64 {
|
||||
self.0.num_whitelists()
|
||||
}
|
||||
|
||||
/// Returns the total number of whitelisted globs.
|
||||
pub fn num_whitelists(&self) -> u64 {
|
||||
self.0.num_ignores()
|
||||
}
|
||||
|
||||
/// Returns whether the given file path matched a pattern in this override
|
||||
/// matcher.
|
||||
///
|
||||
/// `is_dir` should be true if the path refers to a directory and false
|
||||
/// otherwise.
|
||||
///
|
||||
/// If there are no overrides, then this always returns `Match::None`.
|
||||
///
|
||||
/// If there is at least one whitelist override and `is_dir` is false, then
|
||||
/// this never returns `Match::None`, since non-matches are interpreted as
|
||||
/// ignored.
|
||||
///
|
||||
/// The given path is matched to the globs relative to the path given
|
||||
/// when building the override matcher. Specifically, before matching
|
||||
/// `path`, its prefix (as determined by a common suffix of the directory
|
||||
/// given) is stripped. If there is no common suffix/prefix overlap, then
|
||||
/// `path` is assumed to reside in the same directory as the root path for
|
||||
/// this set of overrides.
|
||||
pub fn matched<'a, P: AsRef<Path>>(
|
||||
&'a self,
|
||||
path: P,
|
||||
is_dir: bool,
|
||||
) -> Match<Glob<'a>> {
|
||||
if self.is_empty() {
|
||||
return Match::None;
|
||||
}
|
||||
let mat = self.0.matched(path, is_dir).invert();
|
||||
if mat.is_none() && self.num_whitelists() > 0 && !is_dir {
|
||||
return Match::Ignore(Glob::unmatched());
|
||||
}
|
||||
mat.map(move |giglob| Glob(GlobInner::Matched(giglob)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a matcher for a set of glob overrides.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct OverrideBuilder {
|
||||
builder: GitignoreBuilder,
|
||||
}
|
||||
|
||||
impl OverrideBuilder {
|
||||
/// Create a new override builder.
|
||||
///
|
||||
/// Matching is done relative to the directory path provided.
|
||||
pub fn new<P: AsRef<Path>>(path: P) -> OverrideBuilder {
|
||||
let mut builder = GitignoreBuilder::new(path);
|
||||
builder.allow_unclosed_class(false);
|
||||
OverrideBuilder { builder }
|
||||
}
|
||||
|
||||
/// Builds a new override matcher from the globs added so far.
|
||||
///
|
||||
/// Once a matcher is built, no new globs can be added to it.
|
||||
pub fn build(&self) -> Result<Override, Error> {
|
||||
Ok(Override(self.builder.build()?))
|
||||
}
|
||||
|
||||
/// Add a glob to the set of overrides.
|
||||
///
|
||||
/// Globs provided here have precisely the same semantics as a single
|
||||
/// line in a `gitignore` file, where the meaning of `!` is inverted:
|
||||
/// namely, `!` at the beginning of a glob will ignore a file. Without `!`,
|
||||
/// all matches of the glob provided are treated as whitelist matches.
|
||||
pub fn add(&mut self, glob: &str) -> Result<&mut OverrideBuilder, Error> {
|
||||
self.builder.add_line(None, glob)?;
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// Toggle whether the globs should be matched case insensitively or not.
|
||||
///
|
||||
/// When this option is changed, only globs added after the change will be
|
||||
/// affected.
|
||||
///
|
||||
/// This is disabled by default.
|
||||
pub fn case_insensitive(
|
||||
&mut self,
|
||||
yes: bool,
|
||||
) -> Result<&mut OverrideBuilder, Error> {
|
||||
// TODO: This should not return a `Result`. Fix this in the next semver
|
||||
// release.
|
||||
self.builder.case_insensitive(yes)?;
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// Toggle whether unclosed character classes are allowed. When allowed,
|
||||
/// a `[` without a matching `]` is treated literally instead of resulting
|
||||
/// in a parse error.
|
||||
///
|
||||
/// For example, if this is set then the glob `[abc` will be treated as the
|
||||
/// literal string `[abc` instead of returning an error.
|
||||
///
|
||||
/// By default, this is false. Generally speaking, enabling this leads to
|
||||
/// worse failure modes since the glob parser becomes more permissive. You
|
||||
/// might want to enable this when compatibility (e.g., with POSIX glob
|
||||
/// implementations) is more important than good error messages.
|
||||
///
|
||||
/// This default is different from the default for [`Gitignore`]. Namely,
|
||||
/// [`Gitignore`] is intended to match git's behavior as-is. But this
|
||||
/// abstraction for "override" globs does not necessarily conform to any
|
||||
/// other known specification and instead prioritizes better error
|
||||
/// messages.
|
||||
pub fn allow_unclosed_class(&mut self, yes: bool) -> &mut OverrideBuilder {
|
||||
self.builder.allow_unclosed_class(yes);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{Override, OverrideBuilder};
|
||||
|
||||
const ROOT: &'static str = "/home/andrew/foo";
|
||||
|
||||
fn ov(globs: &[&str]) -> Override {
|
||||
let mut builder = OverrideBuilder::new(ROOT);
|
||||
for glob in globs {
|
||||
builder.add(glob).unwrap();
|
||||
}
|
||||
builder.build().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty() {
|
||||
let ov = ov(&[]);
|
||||
assert!(ov.matched("a.foo", false).is_none());
|
||||
assert!(ov.matched("a", false).is_none());
|
||||
assert!(ov.matched("", false).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple() {
|
||||
let ov = ov(&["*.foo", "!*.bar"]);
|
||||
assert!(ov.matched("a.foo", false).is_whitelist());
|
||||
assert!(ov.matched("a.foo", true).is_whitelist());
|
||||
assert!(ov.matched("a.rs", false).is_ignore());
|
||||
assert!(ov.matched("a.rs", true).is_none());
|
||||
assert!(ov.matched("a.bar", false).is_ignore());
|
||||
assert!(ov.matched("a.bar", true).is_ignore());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_ignores() {
|
||||
let ov = ov(&["!*.bar"]);
|
||||
assert!(ov.matched("a.rs", false).is_none());
|
||||
assert!(ov.matched("a.rs", true).is_none());
|
||||
assert!(ov.matched("a.bar", false).is_ignore());
|
||||
assert!(ov.matched("a.bar", true).is_ignore());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn precedence() {
|
||||
let ov = ov(&["*.foo", "!*.bar.foo"]);
|
||||
assert!(ov.matched("a.foo", false).is_whitelist());
|
||||
assert!(ov.matched("a.baz", false).is_ignore());
|
||||
assert!(ov.matched("a.bar.foo", false).is_ignore());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gitignore() {
|
||||
let ov = ov(&["/foo", "bar/*.rs", "baz/**"]);
|
||||
assert!(ov.matched("bar/lib.rs", false).is_whitelist());
|
||||
assert!(ov.matched("bar/wat/lib.rs", false).is_ignore());
|
||||
assert!(ov.matched("wat/bar/lib.rs", false).is_ignore());
|
||||
assert!(ov.matched("foo", false).is_whitelist());
|
||||
assert!(ov.matched("wat/foo", false).is_ignore());
|
||||
assert!(ov.matched("baz", false).is_ignore());
|
||||
assert!(ov.matched("baz/a", false).is_whitelist());
|
||||
assert!(ov.matched("baz/a/b", false).is_whitelist());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allow_directories() {
|
||||
// This tests that directories are NOT ignored when they are unmatched.
|
||||
let ov = ov(&["*.rs"]);
|
||||
assert!(ov.matched("foo.rs", false).is_whitelist());
|
||||
assert!(ov.matched("foo.c", false).is_ignore());
|
||||
assert!(ov.matched("foo", false).is_ignore());
|
||||
assert!(ov.matched("foo", true).is_none());
|
||||
assert!(ov.matched("src/foo.rs", false).is_whitelist());
|
||||
assert!(ov.matched("src/foo.c", false).is_ignore());
|
||||
assert!(ov.matched("src/foo", false).is_ignore());
|
||||
assert!(ov.matched("src/foo", true).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn absolute_path() {
|
||||
let ov = ov(&["!/bar"]);
|
||||
assert!(ov.matched("./foo/bar", false).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn case_insensitive() {
|
||||
let ov = OverrideBuilder::new(ROOT)
|
||||
.case_insensitive(true)
|
||||
.unwrap()
|
||||
.add("*.html")
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap();
|
||||
assert!(ov.matched("foo.html", false).is_whitelist());
|
||||
assert!(ov.matched("foo.HTML", false).is_whitelist());
|
||||
assert!(ov.matched("foo.htm", false).is_ignore());
|
||||
assert!(ov.matched("foo.HTM", false).is_ignore());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_case_sensitive() {
|
||||
let ov =
|
||||
OverrideBuilder::new(ROOT).add("*.html").unwrap().build().unwrap();
|
||||
assert!(ov.matched("foo.html", false).is_whitelist());
|
||||
assert!(ov.matched("foo.HTML", false).is_ignore());
|
||||
assert!(ov.matched("foo.htm", false).is_ignore());
|
||||
assert!(ov.matched("foo.HTM", false).is_ignore());
|
||||
}
|
||||
}
|
||||
|
|
@ -1,171 +0,0 @@
|
|||
use std::{ffi::OsStr, path::Path};
|
||||
|
||||
use crate::walk::DirEntry;
|
||||
|
||||
/// Returns true if and only if this path is considered to be hidden.
|
||||
///
|
||||
/// # Platform behavior
|
||||
///
|
||||
/// ## Windows
|
||||
///
|
||||
/// This returns true if one of the following is true:
|
||||
///
|
||||
/// * The base name of the path starts with a `.`.
|
||||
/// * The file attributes have the `HIDDEN` property set.
|
||||
///
|
||||
/// ## All other platforms
|
||||
///
|
||||
/// This only returns true if the base name of the path starts with a `.`.
|
||||
pub(crate) fn is_hidden_path(dent: &Path) -> bool {
|
||||
#[cfg(not(windows))]
|
||||
fn imp(path: &Path) -> bool {
|
||||
is_hidden_path_only(path)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn imp(path: &Path) -> bool {
|
||||
use std::os::windows::fs::MetadataExt;
|
||||
use winapi_util::file;
|
||||
|
||||
if let Ok(md) = path.metadata() {
|
||||
if file::is_hidden(md.file_attributes() as u64) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
is_hidden_path_only(path)
|
||||
}
|
||||
|
||||
imp(dent)
|
||||
}
|
||||
|
||||
/// Returns true if and only if this directory entry is considered to be
|
||||
/// hidden.
|
||||
///
|
||||
/// # Platform behavior
|
||||
///
|
||||
/// ## Windows
|
||||
///
|
||||
/// This returns true if one of the following is true:
|
||||
///
|
||||
/// * The base name of the path starts with a `.`.
|
||||
/// * The file attributes have the `HIDDEN` property set.
|
||||
///
|
||||
/// ## All other platforms
|
||||
///
|
||||
/// This only returns true if the base name of the path starts with a `.`.
|
||||
pub(crate) fn is_hidden_entry(dent: &DirEntry) -> bool {
|
||||
#[cfg(not(windows))]
|
||||
fn imp(dent: &DirEntry) -> bool {
|
||||
is_hidden_path_only(dent.path())
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn imp(dent: &DirEntry) -> bool {
|
||||
use std::os::windows::fs::MetadataExt;
|
||||
use winapi_util::file;
|
||||
|
||||
// This looks like we're doing an extra stat call, but on Windows, the
|
||||
// directory traverser reuses the metadata retrieved from each directory
|
||||
// entry and stores it on the DirEntry itself. So this is "free."
|
||||
if let Ok(md) = dent.metadata() {
|
||||
if file::is_hidden(md.file_attributes() as u64) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
is_hidden_path_only(dent.path())
|
||||
}
|
||||
|
||||
imp(dent)
|
||||
}
|
||||
|
||||
/// Returns true if and only if this path is considered to be hidden from only
|
||||
/// the path itself.
|
||||
///
|
||||
/// This has the same behavior on all platforms.
|
||||
fn is_hidden_path_only(path: &Path) -> bool {
|
||||
if let Some(name) = file_name(path) {
|
||||
name.as_encoded_bytes().starts_with(b".")
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Strip `prefix` from the `path` and return the remainder.
|
||||
///
|
||||
/// If `path` doesn't have a prefix `prefix`, then return `None`.
|
||||
pub(crate) fn strip_prefix<'a, P: AsRef<Path> + ?Sized>(
|
||||
prefix: &'a P,
|
||||
path: &'a Path,
|
||||
) -> Option<&'a Path> {
|
||||
#[cfg(unix)]
|
||||
fn imp<'a>(prefix: &'a Path, path: &'a Path) -> Option<&'a Path> {
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
|
||||
let prefix = prefix.as_os_str().as_bytes();
|
||||
let path = path.as_os_str().as_bytes();
|
||||
if prefix.len() > path.len() || prefix != &path[0..prefix.len()] {
|
||||
None
|
||||
} else {
|
||||
Some(&Path::new(OsStr::from_bytes(&path[prefix.len()..])))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn imp<'a>(prefix: &'a Path, path: &'a Path) -> Option<&'a Path> {
|
||||
path.strip_prefix(prefix).ok()
|
||||
}
|
||||
|
||||
imp(prefix.as_ref(), path)
|
||||
}
|
||||
|
||||
/// Returns true if this file path is just a file name. i.e., Its parent is
|
||||
/// the empty string.
|
||||
pub(crate) fn is_file_name<P: AsRef<Path>>(path: P) -> bool {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
memchr::memchr(b'/', path.as_ref().as_os_str().as_encoded_bytes())
|
||||
.is_none()
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
path.as_ref()
|
||||
.parent()
|
||||
.map(|p| p.as_os_str().is_empty())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// The final component of the path, if it is a normal file.
|
||||
///
|
||||
/// If the path terminates in `.`, `..`, or consists solely of a root of
|
||||
/// prefix, this will return `None`.
|
||||
pub(crate) fn file_name<'a, P: AsRef<Path> + ?Sized>(
|
||||
path: &'a P,
|
||||
) -> Option<&'a OsStr> {
|
||||
#[cfg(unix)]
|
||||
fn imp(path: &Path) -> Option<&OsStr> {
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
|
||||
use memchr::memrchr;
|
||||
|
||||
let path = path.as_os_str().as_bytes();
|
||||
if path.is_empty() {
|
||||
return None;
|
||||
} else if path.len() == 1 && path[0] == b'.' {
|
||||
return None;
|
||||
} else if path.last() == Some(&b'.') {
|
||||
return None;
|
||||
} else if path.len() >= 2 && &path[path.len() - 2..] == &b".."[..] {
|
||||
return None;
|
||||
}
|
||||
let last_slash = memrchr(b'/', path).map(|i| i + 1).unwrap_or(0);
|
||||
Some(OsStr::from_bytes(&path[last_slash..]))
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn imp(path: &Path) -> Option<&OsStr> {
|
||||
path.file_name()
|
||||
}
|
||||
|
||||
imp(path.as_ref())
|
||||
}
|
||||
|
|
@ -1,588 +0,0 @@
|
|||
/*!
|
||||
The types module provides a way of associating globs on file names to file
|
||||
types.
|
||||
|
||||
This can be used to match specific types of files. For example, among
|
||||
the default file types provided, the Rust file type is defined to be `*.rs`
|
||||
with name `rust`. Similarly, the C file type is defined to be `*.{c,h}` with
|
||||
name `c`.
|
||||
|
||||
Note that the set of default types may change over time.
|
||||
|
||||
# Example
|
||||
|
||||
This shows how to create and use a simple file type matcher using the default
|
||||
file types defined in this crate.
|
||||
|
||||
```
|
||||
use ignore::types::TypesBuilder;
|
||||
|
||||
let mut builder = TypesBuilder::new();
|
||||
builder.add_defaults();
|
||||
builder.select("rust");
|
||||
let matcher = builder.build().unwrap();
|
||||
|
||||
assert!(matcher.matched("foo.rs", false).is_whitelist());
|
||||
assert!(matcher.matched("foo.c", false).is_ignore());
|
||||
```
|
||||
|
||||
# Example: negation
|
||||
|
||||
This is like the previous example, but shows how negating a file type works.
|
||||
That is, this will let us match file paths that *don't* correspond to a
|
||||
particular file type.
|
||||
|
||||
```
|
||||
use ignore::types::TypesBuilder;
|
||||
|
||||
let mut builder = TypesBuilder::new();
|
||||
builder.add_defaults();
|
||||
builder.negate("c");
|
||||
let matcher = builder.build().unwrap();
|
||||
|
||||
assert!(matcher.matched("foo.rs", false).is_none());
|
||||
assert!(matcher.matched("foo.c", false).is_ignore());
|
||||
```
|
||||
|
||||
# Example: custom file type definitions
|
||||
|
||||
This shows how to extend this library default file type definitions with
|
||||
your own.
|
||||
|
||||
```
|
||||
use ignore::types::TypesBuilder;
|
||||
|
||||
let mut builder = TypesBuilder::new();
|
||||
builder.add_defaults();
|
||||
builder.add("foo", "*.foo");
|
||||
// Another way of adding a file type definition.
|
||||
// This is useful when accepting input from an end user.
|
||||
builder.add_def("bar:*.bar");
|
||||
// Note: we only select `foo`, not `bar`.
|
||||
builder.select("foo");
|
||||
let matcher = builder.build().unwrap();
|
||||
|
||||
assert!(matcher.matched("x.foo", false).is_whitelist());
|
||||
// This is ignored because we only selected the `foo` file type.
|
||||
assert!(matcher.matched("x.bar", false).is_ignore());
|
||||
```
|
||||
|
||||
We can also add file type definitions based on other definitions.
|
||||
|
||||
```
|
||||
use ignore::types::TypesBuilder;
|
||||
|
||||
let mut builder = TypesBuilder::new();
|
||||
builder.add_defaults();
|
||||
builder.add("foo", "*.foo");
|
||||
builder.add_def("bar:include:foo,cpp");
|
||||
builder.select("bar");
|
||||
let matcher = builder.build().unwrap();
|
||||
|
||||
assert!(matcher.matched("x.foo", false).is_whitelist());
|
||||
assert!(matcher.matched("y.cpp", false).is_whitelist());
|
||||
```
|
||||
*/
|
||||
|
||||
use std::{collections::HashMap, path::Path, sync::Arc};
|
||||
|
||||
use {
|
||||
globset::{GlobBuilder, GlobSet, GlobSetBuilder},
|
||||
regex_automata::util::pool::Pool,
|
||||
};
|
||||
|
||||
use crate::{Error, Match, default_types::DEFAULT_TYPES, pathutil::file_name};
|
||||
|
||||
/// Glob represents a single glob in a set of file type definitions.
|
||||
///
|
||||
/// There may be more than one glob for a particular file type.
|
||||
///
|
||||
/// This is used to report information about the highest precedent glob
|
||||
/// that matched.
|
||||
///
|
||||
/// Note that not all matches necessarily correspond to a specific glob.
|
||||
/// For example, if there are one or more selections and a file path doesn't
|
||||
/// match any of those selections, then the file path is considered to be
|
||||
/// ignored.
|
||||
///
|
||||
/// The lifetime `'a` refers to the lifetime of the underlying file type
|
||||
/// definition, which corresponds to the lifetime of the file type matcher.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Glob<'a>(GlobInner<'a>);
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum GlobInner<'a> {
|
||||
/// No glob matched, but the file path should still be ignored.
|
||||
UnmatchedIgnore,
|
||||
/// A glob matched.
|
||||
Matched {
|
||||
/// The file type definition which provided the glob.
|
||||
def: &'a FileTypeDef,
|
||||
},
|
||||
}
|
||||
|
||||
impl<'a> Glob<'a> {
|
||||
fn unmatched() -> Glob<'a> {
|
||||
Glob(GlobInner::UnmatchedIgnore)
|
||||
}
|
||||
|
||||
/// Return the file type definition that matched, if one exists. A file type
|
||||
/// definition always exists when a specific definition matches a file
|
||||
/// path.
|
||||
pub fn file_type_def(&self) -> Option<&FileTypeDef> {
|
||||
match self {
|
||||
Glob(GlobInner::UnmatchedIgnore) => None,
|
||||
Glob(GlobInner::Matched { def, .. }) => Some(def),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A single file type definition.
|
||||
///
|
||||
/// File type definitions can be retrieved in aggregate from a file type
|
||||
/// matcher. File type definitions are also reported when its responsible
|
||||
/// for a match.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct FileTypeDef {
|
||||
name: String,
|
||||
globs: Vec<String>,
|
||||
}
|
||||
|
||||
impl FileTypeDef {
|
||||
/// Return the name of this file type.
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
/// Return the globs used to recognize this file type.
|
||||
pub fn globs(&self) -> &[String] {
|
||||
&self.globs
|
||||
}
|
||||
}
|
||||
|
||||
/// Types is a file type matcher.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Types {
|
||||
/// All of the file type definitions, sorted lexicographically by name.
|
||||
defs: Vec<FileTypeDef>,
|
||||
/// All of the selections made by the user.
|
||||
selections: Vec<Selection<FileTypeDef>>,
|
||||
/// Whether there is at least one Selection::Select in our selections.
|
||||
/// When this is true, a Match::None is converted to Match::Ignore.
|
||||
has_selected: bool,
|
||||
/// A mapping from glob index in the set to two indices. The first is an
|
||||
/// index into `selections` and the second is an index into the
|
||||
/// corresponding file type definition's list of globs.
|
||||
glob_to_selection: Vec<(usize, usize)>,
|
||||
/// The set of all glob selections, used for actual matching.
|
||||
set: GlobSet,
|
||||
/// Temporary storage for globs that match.
|
||||
matches: Arc<Pool<Vec<usize>>>,
|
||||
}
|
||||
|
||||
/// Indicates the type of a selection for a particular file type.
|
||||
#[derive(Clone, Debug)]
|
||||
enum Selection<T> {
|
||||
Select(String, T),
|
||||
Negate(String, T),
|
||||
}
|
||||
|
||||
impl<T> Selection<T> {
|
||||
fn is_negated(&self) -> bool {
|
||||
match *self {
|
||||
Selection::Select(..) => false,
|
||||
Selection::Negate(..) => true,
|
||||
}
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
match *self {
|
||||
Selection::Select(ref name, _) => name,
|
||||
Selection::Negate(ref name, _) => name,
|
||||
}
|
||||
}
|
||||
|
||||
fn map<U, F: FnOnce(T) -> U>(self, f: F) -> Selection<U> {
|
||||
match self {
|
||||
Selection::Select(name, inner) => {
|
||||
Selection::Select(name, f(inner))
|
||||
}
|
||||
Selection::Negate(name, inner) => {
|
||||
Selection::Negate(name, f(inner))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn inner(&self) -> &T {
|
||||
match *self {
|
||||
Selection::Select(_, ref inner) => inner,
|
||||
Selection::Negate(_, ref inner) => inner,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Types {
|
||||
/// Creates a new file type matcher that never matches any path and
|
||||
/// contains no file type definitions.
|
||||
pub fn empty() -> Types {
|
||||
Types {
|
||||
defs: vec![],
|
||||
selections: vec![],
|
||||
has_selected: false,
|
||||
glob_to_selection: vec![],
|
||||
set: GlobSetBuilder::new().build().unwrap(),
|
||||
matches: Arc::new(Pool::with_available_parallelism_capacity(
|
||||
|| vec![],
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if and only if this matcher has zero selections.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.selections.is_empty()
|
||||
}
|
||||
|
||||
/// Returns the number of selections used in this matcher.
|
||||
pub fn len(&self) -> usize {
|
||||
self.selections.len()
|
||||
}
|
||||
|
||||
/// Return the set of current file type definitions.
|
||||
///
|
||||
/// Definitions and globs are sorted.
|
||||
pub fn definitions(&self) -> &[FileTypeDef] {
|
||||
&self.defs
|
||||
}
|
||||
|
||||
/// Returns a match for the given path against this file type matcher.
|
||||
///
|
||||
/// The path is considered whitelisted if it matches a selected file type.
|
||||
/// The path is considered ignored if it matches a negated file type.
|
||||
/// If at least one file type is selected and `path` doesn't match, then
|
||||
/// the path is also considered ignored.
|
||||
pub fn matched<'a, P: AsRef<Path>>(
|
||||
&'a self,
|
||||
path: P,
|
||||
is_dir: bool,
|
||||
) -> Match<Glob<'a>> {
|
||||
// File types don't apply to directories, and we can't do anything
|
||||
// if our glob set is empty.
|
||||
if is_dir || self.set.is_empty() {
|
||||
return Match::None;
|
||||
}
|
||||
// We only want to match against the file name, so extract it.
|
||||
// If one doesn't exist, then we can't match it.
|
||||
let name = match file_name(path.as_ref()) {
|
||||
Some(name) => name,
|
||||
None if self.has_selected => {
|
||||
return Match::Ignore(Glob::unmatched());
|
||||
}
|
||||
None => {
|
||||
return Match::None;
|
||||
}
|
||||
};
|
||||
let mut matches = self.matches.get();
|
||||
self.set.matches_into(name, &mut *matches);
|
||||
// The highest precedent match is the last one.
|
||||
if let Some(&i) = matches.last() {
|
||||
let (isel, _) = self.glob_to_selection[i];
|
||||
let sel = &self.selections[isel];
|
||||
let glob = Glob(GlobInner::Matched { def: sel.inner() });
|
||||
return if sel.is_negated() {
|
||||
Match::Ignore(glob)
|
||||
} else {
|
||||
Match::Whitelist(glob)
|
||||
};
|
||||
}
|
||||
if self.has_selected {
|
||||
Match::Ignore(Glob::unmatched())
|
||||
} else {
|
||||
Match::None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// TypesBuilder builds a type matcher from a set of file type definitions and
|
||||
/// a set of file type selections.
|
||||
pub struct TypesBuilder {
|
||||
types: HashMap<String, FileTypeDef>,
|
||||
selections: Vec<Selection<()>>,
|
||||
}
|
||||
|
||||
impl TypesBuilder {
|
||||
/// Create a new builder for a file type matcher.
|
||||
///
|
||||
/// The builder contains *no* type definitions to start with. A set
|
||||
/// of default type definitions can be added with `add_defaults`, and
|
||||
/// additional type definitions can be added with `select` and `negate`.
|
||||
pub fn new() -> TypesBuilder {
|
||||
TypesBuilder { types: HashMap::new(), selections: vec![] }
|
||||
}
|
||||
|
||||
/// Build the current set of file type definitions *and* selections into
|
||||
/// a file type matcher.
|
||||
pub fn build(&self) -> Result<Types, Error> {
|
||||
let defs = self.definitions();
|
||||
let has_selected = self.selections.iter().any(|s| !s.is_negated());
|
||||
|
||||
let mut selections = vec![];
|
||||
let mut glob_to_selection = vec![];
|
||||
let mut build_set = GlobSetBuilder::new();
|
||||
for (isel, selection) in self.selections.iter().enumerate() {
|
||||
let def = match self.types.get(selection.name()) {
|
||||
Some(def) => def.clone(),
|
||||
None => {
|
||||
let name = selection.name().to_string();
|
||||
return Err(Error::UnrecognizedFileType(name));
|
||||
}
|
||||
};
|
||||
for (iglob, glob) in def.globs.iter().enumerate() {
|
||||
build_set.add(
|
||||
GlobBuilder::new(glob)
|
||||
.literal_separator(true)
|
||||
.build()
|
||||
.map_err(|err| Error::Glob {
|
||||
glob: Some(glob.to_string()),
|
||||
err: err.kind().to_string(),
|
||||
})?,
|
||||
);
|
||||
glob_to_selection.push((isel, iglob));
|
||||
}
|
||||
selections.push(selection.clone().map(move |_| def));
|
||||
}
|
||||
let set = build_set
|
||||
.build()
|
||||
.map_err(|err| Error::Glob { glob: None, err: err.to_string() })?;
|
||||
Ok(Types {
|
||||
defs,
|
||||
selections,
|
||||
has_selected,
|
||||
glob_to_selection,
|
||||
set,
|
||||
matches: Arc::new(Pool::with_available_parallelism_capacity(
|
||||
|| vec![],
|
||||
)),
|
||||
})
|
||||
}
|
||||
|
||||
/// Return the set of current file type definitions.
|
||||
///
|
||||
/// Definitions and globs are sorted.
|
||||
pub fn definitions(&self) -> Vec<FileTypeDef> {
|
||||
let mut defs = vec![];
|
||||
for def in self.types.values() {
|
||||
let mut def = def.clone();
|
||||
def.globs.sort();
|
||||
defs.push(def);
|
||||
}
|
||||
defs.sort_by(|def1, def2| def1.name().cmp(def2.name()));
|
||||
defs
|
||||
}
|
||||
|
||||
/// Select the file type given by `name`.
|
||||
///
|
||||
/// If `name` is `all`, then all file types currently defined are selected.
|
||||
pub fn select(&mut self, name: &str) -> &mut TypesBuilder {
|
||||
if name == "all" {
|
||||
for name in self.types.keys() {
|
||||
self.selections.push(Selection::Select(name.to_string(), ()));
|
||||
}
|
||||
} else {
|
||||
self.selections.push(Selection::Select(name.to_string(), ()));
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Ignore the file type given by `name`.
|
||||
///
|
||||
/// If `name` is `all`, then all file types currently defined are negated.
|
||||
pub fn negate(&mut self, name: &str) -> &mut TypesBuilder {
|
||||
if name == "all" {
|
||||
for name in self.types.keys() {
|
||||
self.selections.push(Selection::Negate(name.to_string(), ()));
|
||||
}
|
||||
} else {
|
||||
self.selections.push(Selection::Negate(name.to_string(), ()));
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Clear any file type definitions for the type name given.
|
||||
pub fn clear(&mut self, name: &str) -> &mut TypesBuilder {
|
||||
self.types.remove(name);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a new file type definition. `name` can be arbitrary and `pat`
|
||||
/// should be a glob recognizing file paths belonging to the `name` type.
|
||||
///
|
||||
/// If `name` is `all` or otherwise contains any character that is not a
|
||||
/// Unicode letter or number, then an error is returned.
|
||||
pub fn add(&mut self, name: &str, glob: &str) -> Result<(), Error> {
|
||||
if name == "all" || !name.chars().all(|c| c.is_alphanumeric()) {
|
||||
return Err(Error::InvalidDefinition);
|
||||
}
|
||||
let (key, glob) = (name.to_string(), glob.to_string());
|
||||
self.types
|
||||
.entry(key)
|
||||
.or_insert_with(|| FileTypeDef {
|
||||
name: name.to_string(),
|
||||
globs: vec![],
|
||||
})
|
||||
.globs
|
||||
.push(glob);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add a new file type definition specified in string form. There are two
|
||||
/// valid formats:
|
||||
/// 1. `{name}:{glob}`. This defines a 'root' definition that associates the
|
||||
/// given name with the given glob.
|
||||
/// 2. `{name}:include:{comma-separated list of already defined names}.
|
||||
/// This defines an 'include' definition that associates the given name
|
||||
/// with the definitions of the given existing types.
|
||||
/// Names may not include any characters that are not
|
||||
/// Unicode letters or numbers.
|
||||
pub fn add_def(&mut self, def: &str) -> Result<(), Error> {
|
||||
let parts: Vec<&str> = def.split(':').collect();
|
||||
match parts.len() {
|
||||
2 => {
|
||||
let name = parts[0];
|
||||
let glob = parts[1];
|
||||
if name.is_empty() || glob.is_empty() {
|
||||
return Err(Error::InvalidDefinition);
|
||||
}
|
||||
self.add(name, glob)
|
||||
}
|
||||
3 => {
|
||||
let name = parts[0];
|
||||
let types_string = parts[2];
|
||||
if name.is_empty()
|
||||
|| parts[1] != "include"
|
||||
|| types_string.is_empty()
|
||||
{
|
||||
return Err(Error::InvalidDefinition);
|
||||
}
|
||||
let types = types_string.split(',');
|
||||
// Check ahead of time to ensure that all types specified are
|
||||
// present and fail fast if not.
|
||||
if types.clone().any(|t| !self.types.contains_key(t)) {
|
||||
return Err(Error::InvalidDefinition);
|
||||
}
|
||||
for type_name in types {
|
||||
let globs =
|
||||
self.types.get(type_name).unwrap().globs.clone();
|
||||
for glob in globs {
|
||||
self.add(name, &glob)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(Error::InvalidDefinition),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a set of default file type definitions.
|
||||
pub fn add_defaults(&mut self) -> &mut TypesBuilder {
|
||||
static MSG: &'static str = "adding a default type should never fail";
|
||||
for &(names, exts) in DEFAULT_TYPES {
|
||||
for name in names {
|
||||
for ext in exts {
|
||||
self.add(name, ext).expect(MSG);
|
||||
}
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::TypesBuilder;
|
||||
|
||||
macro_rules! matched {
|
||||
($name:ident, $types:expr, $sel:expr, $selnot:expr,
|
||||
$path:expr) => {
|
||||
matched!($name, $types, $sel, $selnot, $path, true);
|
||||
};
|
||||
(not, $name:ident, $types:expr, $sel:expr, $selnot:expr,
|
||||
$path:expr) => {
|
||||
matched!($name, $types, $sel, $selnot, $path, false);
|
||||
};
|
||||
($name:ident, $types:expr, $sel:expr, $selnot:expr,
|
||||
$path:expr, $matched:expr) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
let mut btypes = TypesBuilder::new();
|
||||
for tydef in $types {
|
||||
btypes.add_def(tydef).unwrap();
|
||||
}
|
||||
for sel in $sel {
|
||||
btypes.select(sel);
|
||||
}
|
||||
for selnot in $selnot {
|
||||
btypes.negate(selnot);
|
||||
}
|
||||
let types = btypes.build().unwrap();
|
||||
let mat = types.matched($path, false);
|
||||
assert_eq!($matched, !mat.is_ignore());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn types() -> Vec<&'static str> {
|
||||
vec![
|
||||
"html:*.html",
|
||||
"html:*.htm",
|
||||
"rust:*.rs",
|
||||
"js:*.js",
|
||||
"py:*.py",
|
||||
"python:*.py",
|
||||
"foo:*.{rs,foo}",
|
||||
"combo:include:html,rust",
|
||||
]
|
||||
}
|
||||
|
||||
matched!(match1, types(), vec!["rust"], vec![], "lib.rs");
|
||||
matched!(match2, types(), vec!["html"], vec![], "index.html");
|
||||
matched!(match3, types(), vec!["html"], vec![], "index.htm");
|
||||
matched!(match4, types(), vec!["html", "rust"], vec![], "main.rs");
|
||||
matched!(match5, types(), vec![], vec![], "index.html");
|
||||
matched!(match6, types(), vec![], vec!["rust"], "index.html");
|
||||
matched!(match7, types(), vec!["foo"], vec!["rust"], "main.foo");
|
||||
matched!(match8, types(), vec!["combo"], vec![], "index.html");
|
||||
matched!(match9, types(), vec!["combo"], vec![], "lib.rs");
|
||||
matched!(match10, types(), vec!["py"], vec![], "main.py");
|
||||
matched!(match11, types(), vec!["python"], vec![], "main.py");
|
||||
|
||||
matched!(not, matchnot1, types(), vec!["rust"], vec![], "index.html");
|
||||
matched!(not, matchnot2, types(), vec![], vec!["rust"], "main.rs");
|
||||
matched!(not, matchnot3, types(), vec!["foo"], vec!["rust"], "main.rs");
|
||||
matched!(not, matchnot4, types(), vec!["rust"], vec!["foo"], "main.rs");
|
||||
matched!(not, matchnot5, types(), vec!["rust"], vec!["foo"], "main.foo");
|
||||
matched!(not, matchnot6, types(), vec!["combo"], vec![], "leftpad.js");
|
||||
matched!(not, matchnot7, types(), vec!["py"], vec![], "index.html");
|
||||
matched!(not, matchnot8, types(), vec!["python"], vec![], "doc.md");
|
||||
|
||||
#[test]
|
||||
fn test_invalid_defs() {
|
||||
let mut btypes = TypesBuilder::new();
|
||||
for tydef in types() {
|
||||
btypes.add_def(tydef).unwrap();
|
||||
}
|
||||
// Preserve the original definitions for later comparison.
|
||||
let original_defs = btypes.definitions();
|
||||
let bad_defs = vec![
|
||||
// Reference to type that does not exist
|
||||
"combo:include:html,qwerty",
|
||||
// Bad format
|
||||
"combo:foobar:html,rust",
|
||||
"",
|
||||
];
|
||||
for def in bad_defs {
|
||||
assert!(btypes.add_def(def).is_err());
|
||||
// Ensure that nothing changed, even if some of the includes were valid.
|
||||
assert_eq!(btypes.definitions(), original_defs);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,216 +0,0 @@
|
|||
# Based on https://github.com/behnam/gitignore-test/blob/master/.gitignore
|
||||
|
||||
### file in root
|
||||
|
||||
# MATCH /file_root_1
|
||||
file_root_00
|
||||
|
||||
# NO_MATCH
|
||||
file_root_01/
|
||||
|
||||
# NO_MATCH
|
||||
file_root_02/*
|
||||
|
||||
# NO_MATCH
|
||||
file_root_03/**
|
||||
|
||||
|
||||
# MATCH /file_root_10
|
||||
/file_root_10
|
||||
|
||||
# NO_MATCH
|
||||
/file_root_11/
|
||||
|
||||
# NO_MATCH
|
||||
/file_root_12/*
|
||||
|
||||
# NO_MATCH
|
||||
/file_root_13/**
|
||||
|
||||
|
||||
# NO_MATCH
|
||||
*/file_root_20
|
||||
|
||||
# NO_MATCH
|
||||
*/file_root_21/
|
||||
|
||||
# NO_MATCH
|
||||
*/file_root_22/*
|
||||
|
||||
# NO_MATCH
|
||||
*/file_root_23/**
|
||||
|
||||
|
||||
# MATCH /file_root_30
|
||||
**/file_root_30
|
||||
|
||||
# NO_MATCH
|
||||
**/file_root_31/
|
||||
|
||||
# NO_MATCH
|
||||
**/file_root_32/*
|
||||
|
||||
# NO_MATCH
|
||||
**/file_root_33/**
|
||||
|
||||
|
||||
### file in sub-dir
|
||||
|
||||
# MATCH /parent_dir/file_deep_1
|
||||
file_deep_00
|
||||
|
||||
# NO_MATCH
|
||||
file_deep_01/
|
||||
|
||||
# NO_MATCH
|
||||
file_deep_02/*
|
||||
|
||||
# NO_MATCH
|
||||
file_deep_03/**
|
||||
|
||||
|
||||
# NO_MATCH
|
||||
/file_deep_10
|
||||
|
||||
# NO_MATCH
|
||||
/file_deep_11/
|
||||
|
||||
# NO_MATCH
|
||||
/file_deep_12/*
|
||||
|
||||
# NO_MATCH
|
||||
/file_deep_13/**
|
||||
|
||||
|
||||
# MATCH /parent_dir/file_deep_20
|
||||
*/file_deep_20
|
||||
|
||||
# NO_MATCH
|
||||
*/file_deep_21/
|
||||
|
||||
# NO_MATCH
|
||||
*/file_deep_22/*
|
||||
|
||||
# NO_MATCH
|
||||
*/file_deep_23/**
|
||||
|
||||
|
||||
# MATCH /parent_dir/file_deep_30
|
||||
**/file_deep_30
|
||||
|
||||
# NO_MATCH
|
||||
**/file_deep_31/
|
||||
|
||||
# NO_MATCH
|
||||
**/file_deep_32/*
|
||||
|
||||
# NO_MATCH
|
||||
**/file_deep_33/**
|
||||
|
||||
|
||||
### dir in root
|
||||
|
||||
# MATCH /dir_root_00
|
||||
dir_root_00
|
||||
|
||||
# MATCH /dir_root_01
|
||||
dir_root_01/
|
||||
|
||||
# MATCH /dir_root_02
|
||||
dir_root_02/*
|
||||
|
||||
# MATCH /dir_root_03
|
||||
dir_root_03/**
|
||||
|
||||
|
||||
# MATCH /dir_root_10
|
||||
/dir_root_10
|
||||
|
||||
# MATCH /dir_root_11
|
||||
/dir_root_11/
|
||||
|
||||
# MATCH /dir_root_12
|
||||
/dir_root_12/*
|
||||
|
||||
# MATCH /dir_root_13
|
||||
/dir_root_13/**
|
||||
|
||||
|
||||
# NO_MATCH
|
||||
*/dir_root_20
|
||||
|
||||
# NO_MATCH
|
||||
*/dir_root_21/
|
||||
|
||||
# NO_MATCH
|
||||
*/dir_root_22/*
|
||||
|
||||
# NO_MATCH
|
||||
*/dir_root_23/**
|
||||
|
||||
|
||||
# MATCH /dir_root_30
|
||||
**/dir_root_30
|
||||
|
||||
# MATCH /dir_root_31
|
||||
**/dir_root_31/
|
||||
|
||||
# MATCH /dir_root_32
|
||||
**/dir_root_32/*
|
||||
|
||||
# MATCH /dir_root_33
|
||||
**/dir_root_33/**
|
||||
|
||||
|
||||
### dir in sub-dir
|
||||
|
||||
# MATCH /parent_dir/dir_deep_00
|
||||
dir_deep_00
|
||||
|
||||
# MATCH /parent_dir/dir_deep_01
|
||||
dir_deep_01/
|
||||
|
||||
# NO_MATCH
|
||||
dir_deep_02/*
|
||||
|
||||
# NO_MATCH
|
||||
dir_deep_03/**
|
||||
|
||||
|
||||
# NO_MATCH
|
||||
/dir_deep_10
|
||||
|
||||
# NO_MATCH
|
||||
/dir_deep_11/
|
||||
|
||||
# NO_MATCH
|
||||
/dir_deep_12/*
|
||||
|
||||
# NO_MATCH
|
||||
/dir_deep_13/**
|
||||
|
||||
|
||||
# MATCH /parent_dir/dir_deep_20
|
||||
*/dir_deep_20
|
||||
|
||||
# MATCH /parent_dir/dir_deep_21
|
||||
*/dir_deep_21/
|
||||
|
||||
# MATCH /parent_dir/dir_deep_22
|
||||
*/dir_deep_22/*
|
||||
|
||||
# MATCH /parent_dir/dir_deep_23
|
||||
*/dir_deep_23/**
|
||||
|
||||
|
||||
# MATCH /parent_dir/dir_deep_30
|
||||
**/dir_deep_30
|
||||
|
||||
# MATCH /parent_dir/dir_deep_31
|
||||
**/dir_deep_31/
|
||||
|
||||
# MATCH /parent_dir/dir_deep_32
|
||||
**/dir_deep_32/*
|
||||
|
||||
# MATCH /parent_dir/dir_deep_33
|
||||
**/dir_deep_33/**
|
||||
|
|
@ -1,318 +0,0 @@
|
|||
use std::path::Path;
|
||||
|
||||
use ignore::gitignore::{Gitignore, GitignoreBuilder};
|
||||
|
||||
const IGNORE_FILE: &'static str =
|
||||
"tests/gitignore_matched_path_or_any_parents_tests.gitignore";
|
||||
|
||||
fn get_gitignore() -> Gitignore {
|
||||
let mut builder = GitignoreBuilder::new("ROOT");
|
||||
let error = builder.add(IGNORE_FILE);
|
||||
assert!(error.is_none(), "failed to open gitignore file");
|
||||
builder.build().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "path is expected to be under the root")]
|
||||
fn test_path_should_be_under_root() {
|
||||
let gitignore = get_gitignore();
|
||||
let path = "/tmp/some_file";
|
||||
gitignore.matched_path_or_any_parents(Path::new(path), false);
|
||||
assert!(false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_files_in_root() {
|
||||
let gitignore = get_gitignore();
|
||||
let m = |path: &str| {
|
||||
gitignore.matched_path_or_any_parents(Path::new(path), false)
|
||||
};
|
||||
|
||||
// 0x
|
||||
assert!(m("ROOT/file_root_00").is_ignore());
|
||||
assert!(m("ROOT/file_root_01").is_none());
|
||||
assert!(m("ROOT/file_root_02").is_none());
|
||||
assert!(m("ROOT/file_root_03").is_none());
|
||||
|
||||
// 1x
|
||||
assert!(m("ROOT/file_root_10").is_ignore());
|
||||
assert!(m("ROOT/file_root_11").is_none());
|
||||
assert!(m("ROOT/file_root_12").is_none());
|
||||
assert!(m("ROOT/file_root_13").is_none());
|
||||
|
||||
// 2x
|
||||
assert!(m("ROOT/file_root_20").is_none());
|
||||
assert!(m("ROOT/file_root_21").is_none());
|
||||
assert!(m("ROOT/file_root_22").is_none());
|
||||
assert!(m("ROOT/file_root_23").is_none());
|
||||
|
||||
// 3x
|
||||
assert!(m("ROOT/file_root_30").is_ignore());
|
||||
assert!(m("ROOT/file_root_31").is_none());
|
||||
assert!(m("ROOT/file_root_32").is_none());
|
||||
assert!(m("ROOT/file_root_33").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_files_in_deep() {
|
||||
let gitignore = get_gitignore();
|
||||
let m = |path: &str| {
|
||||
gitignore.matched_path_or_any_parents(Path::new(path), false)
|
||||
};
|
||||
|
||||
// 0x
|
||||
assert!(m("ROOT/parent_dir/file_deep_00").is_ignore());
|
||||
assert!(m("ROOT/parent_dir/file_deep_01").is_none());
|
||||
assert!(m("ROOT/parent_dir/file_deep_02").is_none());
|
||||
assert!(m("ROOT/parent_dir/file_deep_03").is_none());
|
||||
|
||||
// 1x
|
||||
assert!(m("ROOT/parent_dir/file_deep_10").is_none());
|
||||
assert!(m("ROOT/parent_dir/file_deep_11").is_none());
|
||||
assert!(m("ROOT/parent_dir/file_deep_12").is_none());
|
||||
assert!(m("ROOT/parent_dir/file_deep_13").is_none());
|
||||
|
||||
// 2x
|
||||
assert!(m("ROOT/parent_dir/file_deep_20").is_ignore());
|
||||
assert!(m("ROOT/parent_dir/file_deep_21").is_none());
|
||||
assert!(m("ROOT/parent_dir/file_deep_22").is_none());
|
||||
assert!(m("ROOT/parent_dir/file_deep_23").is_none());
|
||||
|
||||
// 3x
|
||||
assert!(m("ROOT/parent_dir/file_deep_30").is_ignore());
|
||||
assert!(m("ROOT/parent_dir/file_deep_31").is_none());
|
||||
assert!(m("ROOT/parent_dir/file_deep_32").is_none());
|
||||
assert!(m("ROOT/parent_dir/file_deep_33").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dirs_in_root() {
|
||||
let gitignore = get_gitignore();
|
||||
let m = |path: &str, is_dir: bool| {
|
||||
gitignore.matched_path_or_any_parents(Path::new(path), is_dir)
|
||||
};
|
||||
|
||||
// 00
|
||||
assert!(m("ROOT/dir_root_00", true).is_ignore());
|
||||
assert!(m("ROOT/dir_root_00/file", false).is_ignore());
|
||||
assert!(m("ROOT/dir_root_00/child_dir", true).is_ignore());
|
||||
assert!(m("ROOT/dir_root_00/child_dir/file", false).is_ignore());
|
||||
|
||||
// 01
|
||||
assert!(m("ROOT/dir_root_01", true).is_ignore());
|
||||
assert!(m("ROOT/dir_root_01/file", false).is_ignore());
|
||||
assert!(m("ROOT/dir_root_01/child_dir", true).is_ignore());
|
||||
assert!(m("ROOT/dir_root_01/child_dir/file", false).is_ignore());
|
||||
|
||||
// 02
|
||||
assert!(m("ROOT/dir_root_02", true).is_none()); // dir itself doesn't match
|
||||
assert!(m("ROOT/dir_root_02/file", false).is_ignore());
|
||||
assert!(m("ROOT/dir_root_02/child_dir", true).is_ignore());
|
||||
assert!(m("ROOT/dir_root_02/child_dir/file", false).is_ignore());
|
||||
|
||||
// 03
|
||||
assert!(m("ROOT/dir_root_03", true).is_none()); // dir itself doesn't match
|
||||
assert!(m("ROOT/dir_root_03/file", false).is_ignore());
|
||||
assert!(m("ROOT/dir_root_03/child_dir", true).is_ignore());
|
||||
assert!(m("ROOT/dir_root_03/child_dir/file", false).is_ignore());
|
||||
|
||||
// 10
|
||||
assert!(m("ROOT/dir_root_10", true).is_ignore());
|
||||
assert!(m("ROOT/dir_root_10/file", false).is_ignore());
|
||||
assert!(m("ROOT/dir_root_10/child_dir", true).is_ignore());
|
||||
assert!(m("ROOT/dir_root_10/child_dir/file", false).is_ignore());
|
||||
|
||||
// 11
|
||||
assert!(m("ROOT/dir_root_11", true).is_ignore());
|
||||
assert!(m("ROOT/dir_root_11/file", false).is_ignore());
|
||||
assert!(m("ROOT/dir_root_11/child_dir", true).is_ignore());
|
||||
assert!(m("ROOT/dir_root_11/child_dir/file", false).is_ignore());
|
||||
|
||||
// 12
|
||||
assert!(m("ROOT/dir_root_12", true).is_none()); // dir itself doesn't match
|
||||
assert!(m("ROOT/dir_root_12/file", false).is_ignore());
|
||||
assert!(m("ROOT/dir_root_12/child_dir", true).is_ignore());
|
||||
assert!(m("ROOT/dir_root_12/child_dir/file", false).is_ignore());
|
||||
|
||||
// 13
|
||||
assert!(m("ROOT/dir_root_13", true).is_none());
|
||||
assert!(m("ROOT/dir_root_13/file", false).is_ignore());
|
||||
assert!(m("ROOT/dir_root_13/child_dir", true).is_ignore());
|
||||
assert!(m("ROOT/dir_root_13/child_dir/file", false).is_ignore());
|
||||
|
||||
// 20
|
||||
assert!(m("ROOT/dir_root_20", true).is_none());
|
||||
assert!(m("ROOT/dir_root_20/file", false).is_none());
|
||||
assert!(m("ROOT/dir_root_20/child_dir", true).is_none());
|
||||
assert!(m("ROOT/dir_root_20/child_dir/file", false).is_none());
|
||||
|
||||
// 21
|
||||
assert!(m("ROOT/dir_root_21", true).is_none());
|
||||
assert!(m("ROOT/dir_root_21/file", false).is_none());
|
||||
assert!(m("ROOT/dir_root_21/child_dir", true).is_none());
|
||||
assert!(m("ROOT/dir_root_21/child_dir/file", false).is_none());
|
||||
|
||||
// 22
|
||||
assert!(m("ROOT/dir_root_22", true).is_none());
|
||||
assert!(m("ROOT/dir_root_22/file", false).is_none());
|
||||
assert!(m("ROOT/dir_root_22/child_dir", true).is_none());
|
||||
assert!(m("ROOT/dir_root_22/child_dir/file", false).is_none());
|
||||
|
||||
// 23
|
||||
assert!(m("ROOT/dir_root_23", true).is_none());
|
||||
assert!(m("ROOT/dir_root_23/file", false).is_none());
|
||||
assert!(m("ROOT/dir_root_23/child_dir", true).is_none());
|
||||
assert!(m("ROOT/dir_root_23/child_dir/file", false).is_none());
|
||||
|
||||
// 30
|
||||
assert!(m("ROOT/dir_root_30", true).is_ignore());
|
||||
assert!(m("ROOT/dir_root_30/file", false).is_ignore());
|
||||
assert!(m("ROOT/dir_root_30/child_dir", true).is_ignore());
|
||||
assert!(m("ROOT/dir_root_30/child_dir/file", false).is_ignore());
|
||||
|
||||
// 31
|
||||
assert!(m("ROOT/dir_root_31", true).is_ignore());
|
||||
assert!(m("ROOT/dir_root_31/file", false).is_ignore());
|
||||
assert!(m("ROOT/dir_root_31/child_dir", true).is_ignore());
|
||||
assert!(m("ROOT/dir_root_31/child_dir/file", false).is_ignore());
|
||||
|
||||
// 32
|
||||
assert!(m("ROOT/dir_root_32", true).is_none()); // dir itself doesn't match
|
||||
assert!(m("ROOT/dir_root_32/file", false).is_ignore());
|
||||
assert!(m("ROOT/dir_root_32/child_dir", true).is_ignore());
|
||||
assert!(m("ROOT/dir_root_32/child_dir/file", false).is_ignore());
|
||||
|
||||
// 33
|
||||
assert!(m("ROOT/dir_root_33", true).is_none()); // dir itself doesn't match
|
||||
assert!(m("ROOT/dir_root_33/file", false).is_ignore());
|
||||
assert!(m("ROOT/dir_root_33/child_dir", true).is_ignore());
|
||||
assert!(m("ROOT/dir_root_33/child_dir/file", false).is_ignore());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dirs_in_deep() {
|
||||
let gitignore = get_gitignore();
|
||||
let m = |path: &str, is_dir: bool| {
|
||||
gitignore.matched_path_or_any_parents(Path::new(path), is_dir)
|
||||
};
|
||||
|
||||
// 00
|
||||
assert!(m("ROOT/parent_dir/dir_deep_00", true).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_00/file", false).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_00/child_dir", true).is_ignore());
|
||||
assert!(
|
||||
m("ROOT/parent_dir/dir_deep_00/child_dir/file", false).is_ignore()
|
||||
);
|
||||
|
||||
// 01
|
||||
assert!(m("ROOT/parent_dir/dir_deep_01", true).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_01/file", false).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_01/child_dir", true).is_ignore());
|
||||
assert!(
|
||||
m("ROOT/parent_dir/dir_deep_01/child_dir/file", false).is_ignore()
|
||||
);
|
||||
|
||||
// 02
|
||||
assert!(m("ROOT/parent_dir/dir_deep_02", true).is_none());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_02/file", false).is_none());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_02/child_dir", true).is_none());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_02/child_dir/file", false).is_none());
|
||||
|
||||
// 03
|
||||
assert!(m("ROOT/parent_dir/dir_deep_03", true).is_none());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_03/file", false).is_none());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_03/child_dir", true).is_none());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_03/child_dir/file", false).is_none());
|
||||
|
||||
// 10
|
||||
assert!(m("ROOT/parent_dir/dir_deep_10", true).is_none());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_10/file", false).is_none());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_10/child_dir", true).is_none());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_10/child_dir/file", false).is_none());
|
||||
|
||||
// 11
|
||||
assert!(m("ROOT/parent_dir/dir_deep_11", true).is_none());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_11/file", false).is_none());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_11/child_dir", true).is_none());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_11/child_dir/file", false).is_none());
|
||||
|
||||
// 12
|
||||
assert!(m("ROOT/parent_dir/dir_deep_12", true).is_none());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_12/file", false).is_none());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_12/child_dir", true).is_none());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_12/child_dir/file", false).is_none());
|
||||
|
||||
// 13
|
||||
assert!(m("ROOT/parent_dir/dir_deep_13", true).is_none());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_13/file", false).is_none());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_13/child_dir", true).is_none());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_13/child_dir/file", false).is_none());
|
||||
|
||||
// 20
|
||||
assert!(m("ROOT/parent_dir/dir_deep_20", true).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_20/file", false).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_20/child_dir", true).is_ignore());
|
||||
assert!(
|
||||
m("ROOT/parent_dir/dir_deep_20/child_dir/file", false).is_ignore()
|
||||
);
|
||||
|
||||
// 21
|
||||
assert!(m("ROOT/parent_dir/dir_deep_21", true).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_21/file", false).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_21/child_dir", true).is_ignore());
|
||||
assert!(
|
||||
m("ROOT/parent_dir/dir_deep_21/child_dir/file", false).is_ignore()
|
||||
);
|
||||
|
||||
// 22
|
||||
// dir itself doesn't match
|
||||
assert!(m("ROOT/parent_dir/dir_deep_22", true).is_none());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_22/file", false).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_22/child_dir", true).is_ignore());
|
||||
assert!(
|
||||
m("ROOT/parent_dir/dir_deep_22/child_dir/file", false).is_ignore()
|
||||
);
|
||||
|
||||
// 23
|
||||
// dir itself doesn't match
|
||||
assert!(m("ROOT/parent_dir/dir_deep_23", true).is_none());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_23/file", false).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_23/child_dir", true).is_ignore());
|
||||
assert!(
|
||||
m("ROOT/parent_dir/dir_deep_23/child_dir/file", false).is_ignore()
|
||||
);
|
||||
|
||||
// 30
|
||||
assert!(m("ROOT/parent_dir/dir_deep_30", true).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_30/file", false).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_30/child_dir", true).is_ignore());
|
||||
assert!(
|
||||
m("ROOT/parent_dir/dir_deep_30/child_dir/file", false).is_ignore()
|
||||
);
|
||||
|
||||
// 31
|
||||
assert!(m("ROOT/parent_dir/dir_deep_31", true).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_31/file", false).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_31/child_dir", true).is_ignore());
|
||||
assert!(
|
||||
m("ROOT/parent_dir/dir_deep_31/child_dir/file", false).is_ignore()
|
||||
);
|
||||
|
||||
// 32
|
||||
// dir itself doesn't match
|
||||
assert!(m("ROOT/parent_dir/dir_deep_32", true).is_none());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_32/file", false).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_32/child_dir", true).is_ignore());
|
||||
assert!(
|
||||
m("ROOT/parent_dir/dir_deep_32/child_dir/file", false).is_ignore()
|
||||
);
|
||||
|
||||
// 33
|
||||
// dir itself doesn't match
|
||||
assert!(m("ROOT/parent_dir/dir_deep_33", true).is_none());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_33/file", false).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_33/child_dir", true).is_ignore());
|
||||
assert!(
|
||||
m("ROOT/parent_dir/dir_deep_33/child_dir/file", false).is_ignore()
|
||||
);
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
ignore/this/path
|
||||
# This file begins with a BOM (U+FEFF)
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
use ignore::gitignore::GitignoreBuilder;
|
||||
|
||||
const IGNORE_FILE: &'static str = "tests/gitignore_skip_bom.gitignore";
|
||||
|
||||
/// Skip a Byte-Order Mark (BOM) at the beginning of the file, matching Git's
|
||||
/// behavior.
|
||||
///
|
||||
/// Ref: <https://github.com/BurntSushi/ripgrep/issues/2177>
|
||||
#[test]
|
||||
fn gitignore_skip_bom() {
|
||||
let mut builder = GitignoreBuilder::new("ROOT");
|
||||
let error = builder.add(IGNORE_FILE);
|
||||
assert!(error.is_none(), "failed to open gitignore file");
|
||||
let g = builder.build().unwrap();
|
||||
|
||||
assert!(g.matched("ignore/this/path", false).is_ignore());
|
||||
}
|
||||
12
crates/node/.gitignore
vendored
12
crates/node/.gitignore
vendored
|
|
@ -121,7 +121,7 @@ dist
|
|||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two
|
||||
# Icon must end with two
|
||||
Icon
|
||||
|
||||
|
||||
|
|
@ -194,14 +194,8 @@ Cargo.lock
|
|||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
|
||||
# Generated
|
||||
*.node
|
||||
*.wasm
|
||||
|
||||
# Generated
|
||||
index.d.ts
|
||||
index.js
|
||||
browser.js
|
||||
tailwindcss-oxide.wasi-browser.js
|
||||
tailwindcss-oxide.wasi.cjs
|
||||
tailwindcss-oxide.wasi.d.cts
|
||||
wasi-worker-browser.mjs
|
||||
wasi-worker.mjs
|
||||
|
|
|
|||
|
|
@ -8,10 +8,10 @@ crate-type = ["cdylib"]
|
|||
|
||||
[dependencies]
|
||||
# Default enable napi4 feature, see https://nodejs.org/api/n-api.html#node-api-version-matrix
|
||||
napi = { version = "3.11.0", default-features = false, features = ["napi4"] }
|
||||
napi-derive = "3.6.0"
|
||||
napi = { version = "2.16.16", default-features = false, features = ["napi4"] }
|
||||
napi-derive = "2.16.13"
|
||||
tailwindcss-oxide = { path = "../oxide" }
|
||||
rayon = "1.12.0"
|
||||
rayon = "1.10.0"
|
||||
|
||||
[build-dependencies]
|
||||
napi-build = "2.3.2"
|
||||
napi-build = "2.1.4"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide-android-arm-eabi",
|
||||
"version": "4.3.3",
|
||||
"version": "4.0.15",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
@ -22,6 +22,6 @@
|
|||
},
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
"node": ">= 10"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide-android-arm64",
|
||||
"version": "4.3.3",
|
||||
"version": "4.0.15",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
@ -22,6 +22,6 @@
|
|||
},
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
"node": ">= 10"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide-darwin-arm64",
|
||||
"version": "4.3.3",
|
||||
"version": "4.0.15",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
@ -22,6 +22,6 @@
|
|||
},
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
"node": ">= 10"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide-darwin-x64",
|
||||
"version": "4.3.3",
|
||||
"version": "4.0.15",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
@ -22,6 +22,6 @@
|
|||
},
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
"node": ">= 10"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide-freebsd-x64",
|
||||
"version": "4.3.3",
|
||||
"version": "4.0.15",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
@ -22,6 +22,6 @@
|
|||
},
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
"node": ">= 10"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide-linux-arm-gnueabihf",
|
||||
"version": "4.3.3",
|
||||
"version": "4.0.15",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
@ -22,6 +22,6 @@
|
|||
},
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
"node": ">= 10"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide-linux-arm64-gnu",
|
||||
"version": "4.3.3",
|
||||
"version": "4.0.15",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
},
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
"node": ">= 10"
|
||||
},
|
||||
"libc": [
|
||||
"glibc"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide-linux-arm64-musl",
|
||||
"version": "4.3.3",
|
||||
"version": "4.0.15",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
},
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
"node": ">= 10"
|
||||
},
|
||||
"libc": [
|
||||
"musl"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide-linux-x64-gnu",
|
||||
"version": "4.3.3",
|
||||
"version": "4.0.15",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
},
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
"node": ">= 10"
|
||||
},
|
||||
"libc": [
|
||||
"glibc"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide-linux-x64-musl",
|
||||
"version": "4.3.3",
|
||||
"version": "4.0.15",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
},
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
"node": ">= 10"
|
||||
},
|
||||
"libc": [
|
||||
"musl"
|
||||
|
|
|
|||
1
crates/node/npm/wasm32-wasi/.gitignore
vendored
1
crates/node/npm/wasm32-wasi/.gitignore
vendored
|
|
@ -1 +0,0 @@
|
|||
node_modules/
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# `@tailwindcss/oxide-wasm32-wasi`
|
||||
|
||||
This is the **wasm32-wasip1-threads** build of `@tailwindcss/oxide`
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide-wasm32-wasi",
|
||||
"version": "4.3.3",
|
||||
"main": "tailwindcss-oxide.wasi.cjs",
|
||||
"files": [
|
||||
"tailwindcss-oxide.wasm32-wasi.wasm",
|
||||
"tailwindcss-oxide.wasi.cjs",
|
||||
"tailwindcss-oxide.wasi-browser.js",
|
||||
"wasi-worker.mjs",
|
||||
"wasi-worker-browser.mjs"
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.13.0 || >=23.5.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"provenance": true,
|
||||
"access": "public"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
"directory": "crates/node"
|
||||
},
|
||||
"browser": "tailwindcss-oxide.wasi-browser.js",
|
||||
"dependencies": {
|
||||
"@napi-rs/wasm-runtime": "^1.2.2",
|
||||
"@emnapi/core": "^1.11.3",
|
||||
"@emnapi/runtime": "^1.11.3",
|
||||
"@tybys/wasm-util": "^0.10.3",
|
||||
"@emnapi/wasi-threads": "^1.2.3",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"bundledDependencies": [
|
||||
"@napi-rs/wasm-runtime",
|
||||
"@emnapi/core",
|
||||
"@emnapi/runtime",
|
||||
"@tybys/wasm-util",
|
||||
"@emnapi/wasi-threads",
|
||||
"tslib"
|
||||
]
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide-win32-arm64-msvc",
|
||||
"version": "4.3.3",
|
||||
"version": "4.0.15",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
@ -22,6 +22,6 @@
|
|||
},
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
"node": ">= 10"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide-win32-x64-msvc",
|
||||
"version": "4.3.3",
|
||||
"version": "4.0.15",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
@ -22,6 +22,6 @@
|
|||
},
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
"node": ">= 10"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide",
|
||||
"version": "4.3.3",
|
||||
"version": "4.0.15",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
@ -9,38 +9,28 @@
|
|||
"main": "index.js",
|
||||
"types": "index.d.ts",
|
||||
"napi": {
|
||||
"binaryName": "tailwindcss-oxide",
|
||||
"packageName": "@tailwindcss/oxide",
|
||||
"targets": [
|
||||
"armv7-linux-androideabi",
|
||||
"aarch64-linux-android",
|
||||
"aarch64-apple-darwin",
|
||||
"aarch64-unknown-linux-gnu",
|
||||
"aarch64-unknown-linux-musl",
|
||||
"armv7-unknown-linux-gnueabihf",
|
||||
"x86_64-unknown-linux-musl",
|
||||
"x86_64-unknown-freebsd",
|
||||
"i686-pc-windows-msvc",
|
||||
"aarch64-pc-windows-msvc",
|
||||
"wasm32-wasip1-threads"
|
||||
],
|
||||
"wasm": {
|
||||
"initialMemory": 16384,
|
||||
"browser": {
|
||||
"fs": true
|
||||
}
|
||||
"name": "tailwindcss-oxide",
|
||||
"triples": {
|
||||
"additional": [
|
||||
"armv7-linux-androideabi",
|
||||
"aarch64-linux-android",
|
||||
"aarch64-apple-darwin",
|
||||
"aarch64-unknown-linux-gnu",
|
||||
"aarch64-unknown-linux-musl",
|
||||
"armv7-unknown-linux-gnueabihf",
|
||||
"x86_64-unknown-linux-musl",
|
||||
"x86_64-unknown-freebsd",
|
||||
"i686-pc-windows-msvc",
|
||||
"aarch64-pc-windows-msvc"
|
||||
]
|
||||
}
|
||||
},
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@emnapi/core": "1.11.3",
|
||||
"@emnapi/runtime": "1.11.3",
|
||||
"@napi-rs/cli": "3.7.4",
|
||||
"@napi-rs/wasm-runtime": "^1.2.2",
|
||||
"emnapi": "1.11.3"
|
||||
"@napi-rs/cli": "^2.18.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
"node": ">= 10"
|
||||
},
|
||||
"files": [
|
||||
"index.js",
|
||||
|
|
@ -51,13 +41,10 @@
|
|||
"access": "public"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "pnpm run build:platform && pnpm run build:wasm",
|
||||
"build:platform": "napi build --platform --release",
|
||||
"postbuild:platform": "node ./scripts/move-artifacts.mjs",
|
||||
"build:wasm": "napi build --release --target wasm32-wasip1-threads",
|
||||
"postbuild:wasm": "node ./scripts/move-artifacts.mjs",
|
||||
"artifacts": "napi artifacts",
|
||||
"build": "napi build --platform --release --no-const-enum",
|
||||
"dev": "cargo watch --quiet --shell 'npm run build'",
|
||||
"build:debug": "napi build --platform",
|
||||
"build:debug": "napi build --platform --no-const-enum",
|
||||
"version": "napi version"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
|
|
@ -70,8 +57,7 @@
|
|||
"@tailwindcss/oxide-linux-arm64-musl": "workspace:*",
|
||||
"@tailwindcss/oxide-linux-x64-gnu": "workspace:*",
|
||||
"@tailwindcss/oxide-linux-x64-musl": "workspace:*",
|
||||
"@tailwindcss/oxide-wasm32-wasi": "workspace:*",
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": "workspace:*",
|
||||
"@tailwindcss/oxide-win32-x64-msvc": "workspace:*"
|
||||
"@tailwindcss/oxide-win32-x64-msvc": "workspace:*",
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,37 +0,0 @@
|
|||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import url from 'node:url'
|
||||
|
||||
const __dirname = path.dirname(url.fileURLToPath(import.meta.url))
|
||||
let root = path.resolve(__dirname, '..')
|
||||
const tailwindcssOxideRoot = path.join(root)
|
||||
|
||||
// Move napi artifacts into sub packages
|
||||
for (let file of await fs.readdir(tailwindcssOxideRoot)) {
|
||||
if (file.startsWith('tailwindcss-oxide.') && file.endsWith('.node')) {
|
||||
let target = file.split('.')[1]
|
||||
await fs.cp(
|
||||
path.join(tailwindcssOxideRoot, file),
|
||||
path.join(tailwindcssOxideRoot, 'npm', target, file),
|
||||
)
|
||||
console.log(`Moved ${file} to npm/${target}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Move napi wasm artifacts into sub package
|
||||
let wasmArtifacts = {
|
||||
'tailwindcss-oxide.debug.wasm': 'tailwindcss-oxide.wasm32-wasi.debug.wasm',
|
||||
'tailwindcss-oxide.wasm': 'tailwindcss-oxide.wasm32-wasi.wasm',
|
||||
'tailwindcss-oxide.wasi-browser.js': 'tailwindcss-oxide.wasi-browser.js',
|
||||
'tailwindcss-oxide.wasi.cjs': 'tailwindcss-oxide.wasi.cjs',
|
||||
'wasi-worker-browser.mjs': 'wasi-worker-browser.mjs',
|
||||
'wasi-worker.mjs': 'wasi-worker.mjs',
|
||||
}
|
||||
for (let file of await fs.readdir(tailwindcssOxideRoot)) {
|
||||
if (!wasmArtifacts[file]) continue
|
||||
await fs.cp(
|
||||
path.join(tailwindcssOxideRoot, file),
|
||||
path.join(tailwindcssOxideRoot, 'npm', 'wasm32-wasi', wasmArtifacts[file]),
|
||||
)
|
||||
console.log(`Moved ${file} to npm/wasm32-wasi`)
|
||||
}
|
||||
|
|
@ -28,27 +28,20 @@ pub struct GlobEntry {
|
|||
pub pattern: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[napi(object)]
|
||||
pub struct SourceEntry {
|
||||
/// Base path of the glob
|
||||
pub base: String,
|
||||
|
||||
/// Glob pattern
|
||||
pub pattern: String,
|
||||
|
||||
/// Negated flag
|
||||
pub negated: bool,
|
||||
}
|
||||
|
||||
impl From<ChangedContent> for tailwindcss_oxide::ChangedContent {
|
||||
impl From<ChangedContent> for tailwindcss_oxide::ChangedContent<'_> {
|
||||
fn from(changed_content: ChangedContent) -> Self {
|
||||
if let Some(file) = changed_content.file {
|
||||
return tailwindcss_oxide::ChangedContent::File(file.into(), changed_content.extension);
|
||||
return tailwindcss_oxide::ChangedContent::File(
|
||||
file.into(),
|
||||
changed_content.extension.into(),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(contents) = changed_content.content {
|
||||
return tailwindcss_oxide::ChangedContent::Content(contents, changed_content.extension);
|
||||
return tailwindcss_oxide::ChangedContent::Content(
|
||||
contents,
|
||||
changed_content.extension.into(),
|
||||
);
|
||||
}
|
||||
|
||||
unreachable!()
|
||||
|
|
@ -73,23 +66,13 @@ impl From<tailwindcss_oxide::GlobEntry> for GlobEntry {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<SourceEntry> for tailwindcss_oxide::PublicSourceEntry {
|
||||
fn from(source: SourceEntry) -> Self {
|
||||
Self {
|
||||
base: source.base,
|
||||
pattern: source.pattern,
|
||||
negated: source.negated,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[napi(object)]
|
||||
pub struct ScannerOptions {
|
||||
/// Glob sources
|
||||
pub sources: Option<Vec<SourceEntry>>,
|
||||
pub sources: Option<Vec<GlobEntry>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
|
@ -113,10 +96,11 @@ impl Scanner {
|
|||
#[napi(constructor)]
|
||||
pub fn new(opts: ScannerOptions) -> Self {
|
||||
Self {
|
||||
scanner: tailwindcss_oxide::Scanner::new(match opts.sources {
|
||||
Some(sources) => sources.into_iter().map(Into::into).collect(),
|
||||
None => vec![],
|
||||
}),
|
||||
scanner: tailwindcss_oxide::Scanner::new(
|
||||
opts
|
||||
.sources
|
||||
.map(|x| x.into_iter().map(Into::into).collect()),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -165,11 +149,6 @@ impl Scanner {
|
|||
self.scanner.get_files()
|
||||
}
|
||||
|
||||
#[napi(getter)]
|
||||
pub fn scanned_files(&self) -> Vec<String> {
|
||||
self.scanner.get_scanned_files()
|
||||
}
|
||||
|
||||
#[napi(getter)]
|
||||
pub fn globs(&mut self) -> Vec<GlobEntry> {
|
||||
self
|
||||
|
|
@ -179,14 +158,4 @@ impl Scanner {
|
|||
.map(Into::into)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[napi(getter)]
|
||||
pub fn normalized_sources(&mut self) -> Vec<GlobEntry> {
|
||||
self
|
||||
.scanner
|
||||
.get_normalized_sources()
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,18 +9,18 @@ globwalk = "0.9.1"
|
|||
log = "0.4.22"
|
||||
rayon = "1.10.0"
|
||||
fxhash = { package = "rustc-hash", version = "2.1.1" }
|
||||
crossbeam = "0.8.4"
|
||||
tracing = { version = "0.1.40", features = [] }
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||
walkdir = "2.5.0"
|
||||
ignore = "0.4.23"
|
||||
dunce = "1.0.5"
|
||||
bexpand = "1.2.0"
|
||||
fast-glob = "0.4.3"
|
||||
classification-macros = { path = "../classification-macros" }
|
||||
ignore = { path = "../ignore" }
|
||||
regex = "1.11.1"
|
||||
fancy-regex = "0.14.0"
|
||||
|
||||
[dev-dependencies]
|
||||
insta = "1.48.0"
|
||||
tempfile = "3.13.0"
|
||||
pretty_assertions = "1.4.1"
|
||||
unicode-width = "0.2.0"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,49 +1,44 @@
|
|||
use std::{ascii::escape_default, fmt::Display};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[derive(Debug, Clone)]
|
||||
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 {
|
||||
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
|
||||
}
|
||||
let mut cursor = Self {
|
||||
input,
|
||||
pos: 0,
|
||||
at_start: true,
|
||||
at_end: false,
|
||||
prev: 0x00,
|
||||
curr: 0x00,
|
||||
next: 0x00,
|
||||
};
|
||||
cursor.move_to(0);
|
||||
cursor
|
||||
}
|
||||
|
||||
pub fn advance_by(&mut self, amount: usize) {
|
||||
|
|
@ -53,15 +48,38 @@ 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) {
|
||||
self.pos = pos.min(self.input.len());
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -72,9 +90,9 @@ impl Display for Cursor<'_> {
|
|||
let pos = format!("{: >len_count$}", self.pos, len_count = len.len());
|
||||
write!(f, "{}/{} ", pos, len)?;
|
||||
|
||||
if self.pos == 0 {
|
||||
if self.at_start {
|
||||
write!(f, "S ")?;
|
||||
} else if self.pos + 1 >= self.input.len() {
|
||||
} else if self.at_end {
|
||||
write!(f, "E ")?;
|
||||
} else {
|
||||
write!(f, "M ")?;
|
||||
|
|
@ -91,9 +109,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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -101,34 +119,41 @@ impl Display for Cursor<'_> {
|
|||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_cursor() {
|
||||
let mut cursor = Cursor::new(b"hello world");
|
||||
assert_eq!(cursor.pos, 0);
|
||||
assert_eq!(cursor.prev(), 0x00);
|
||||
assert_eq!(cursor.curr(), b'h');
|
||||
assert_eq!(cursor.next(), b'e');
|
||||
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');
|
||||
|
||||
cursor.advance_by(1);
|
||||
assert_eq!(cursor.pos, 1);
|
||||
assert_eq!(cursor.prev(), b'h');
|
||||
assert_eq!(cursor.curr(), b'e');
|
||||
assert_eq!(cursor.next(), b'l');
|
||||
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');
|
||||
|
||||
// Advancing too far should stop at the end
|
||||
cursor.advance_by(10);
|
||||
assert_eq!(cursor.pos, 11);
|
||||
assert_eq!(cursor.prev(), b'd');
|
||||
assert_eq!(cursor.curr(), 0x00);
|
||||
assert_eq!(cursor.next(), 0x00);
|
||||
assert!(!cursor.at_start);
|
||||
assert!(cursor.at_end);
|
||||
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_eq!(cursor.prev(), b'd');
|
||||
assert_eq!(cursor.curr(), 0x00);
|
||||
assert_eq!(cursor.next(), 0x00);
|
||||
assert!(!cursor.at_start);
|
||||
assert!(cursor.at_end);
|
||||
assert_eq!(cursor.prev, b'd');
|
||||
assert_eq!(cursor.curr, 0x00);
|
||||
assert_eq!(cursor.next, 0x00);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ impl Machine for ArbitraryPropertyMachine<IdleState> {
|
|||
|
||||
#[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<ParsingPropertyState> {
|
|||
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<ParsingPropertyState> {
|
|||
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<ParsingValueState> {
|
|||
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<ParsingValueState> {
|
|||
},
|
||||
|
||||
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<ParsingValueState> {
|
|||
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,16 +227,10 @@ impl Machine for ArbitraryPropertyMachine<ParsingValueState> {
|
|||
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()
|
||||
}
|
||||
|
||||
// An `!` at the top-level is invalid. We don't allow things to end with
|
||||
// `!important` either as we have dedicated syntax for this.
|
||||
Class::Exclamation if self.bracket_stack.is_empty() => {
|
||||
return self.restart();
|
||||
}
|
||||
|
||||
// Everything else is valid
|
||||
_ => cursor.advance(),
|
||||
};
|
||||
|
|
@ -299,9 +293,6 @@ enum Class {
|
|||
#[bytes(b'/')]
|
||||
Slash,
|
||||
|
||||
#[bytes(b'!')]
|
||||
Exclamation,
|
||||
|
||||
#[bytes(b' ', b'\t', b'\n', b'\r', b'\x0C')]
|
||||
Whitespace,
|
||||
|
||||
|
|
@ -316,7 +307,6 @@ enum Class {
|
|||
mod tests {
|
||||
use super::{ArbitraryPropertyMachine, IdleState};
|
||||
use crate::extractor::machine::Machine;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
|
|
@ -378,9 +368,6 @@ mod tests {
|
|||
"[background:url(https://example.com?q={[{[([{[[2]]}])]}]})]",
|
||||
vec!["[background:url(https://example.com?q={[{[([{[[2]]}])]}]})]"],
|
||||
),
|
||||
// A property containing `!` at the top-level is invalid
|
||||
("[color:red!]", vec![]),
|
||||
("[color:red!important]", vec![]),
|
||||
] {
|
||||
for wrapper in [
|
||||
// No wrapper
|
||||
|
|
@ -426,7 +413,7 @@ mod tests {
|
|||
let actual = ArbitraryPropertyMachine::<IdleState>::test_extract_all(&input);
|
||||
|
||||
if actual != expected {
|
||||
dbg!(&input);
|
||||
dbg!(&input, &actual, &expected);
|
||||
}
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
@ -149,7 +149,6 @@ enum Class {
|
|||
mod tests {
|
||||
use super::ArbitraryValueMachine;
|
||||
use crate::extractor::machine::Machine;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
|
|
|
|||
|
|
@ -80,13 +80,13 @@ impl Machine for ArbitraryVariableMachine<IdleState> {
|
|||
|
||||
#[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<ParsingState> {
|
|||
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<ParsingState> {
|
|||
_ => {
|
||||
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<ParsingDataTypeState> {
|
|||
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<ParsingDataTypeState> {
|
|||
//
|
||||
// E.g.: `(length:--my-length)`
|
||||
// ^
|
||||
Class::Colon => match cursor.next().into() {
|
||||
Class::Colon => match cursor.next.into() {
|
||||
Class::Dash => {
|
||||
cursor.advance();
|
||||
return self.transition::<ParsingState>().next(cursor);
|
||||
|
|
@ -196,8 +196,8 @@ impl Machine for ArbitraryVariableMachine<ParsingFallbackState> {
|
|||
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<ParsingFallbackState> {
|
|||
},
|
||||
|
||||
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<ParsingFallbackState> {
|
|||
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<ParsingFallbackState> {
|
|||
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()
|
||||
}
|
||||
|
||||
|
|
@ -336,7 +336,6 @@ enum Class {
|
|||
mod tests {
|
||||
use super::ArbitraryVariableMachine;
|
||||
use crate::extractor::{arbitrary_variable_machine::IdleState, machine::Machine};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ impl BracketStack {
|
|||
b'(' => b')',
|
||||
b'[' => b']',
|
||||
b'{' => b'}',
|
||||
b'<' => b'>',
|
||||
_ => std::hint::unreachable_unchecked(),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
@ -181,7 +181,6 @@ impl CandidateMachine {
|
|||
mod tests {
|
||||
use super::CandidateMachine;
|
||||
use crate::extractor::machine::Machine;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
|
|
@ -308,7 +307,7 @@ mod tests {
|
|||
actual.sort();
|
||||
|
||||
if actual != expected {
|
||||
dbg!(&input);
|
||||
dbg!(&input, &expected, &actual);
|
||||
}
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -116,7 +116,6 @@ enum Class {
|
|||
mod tests {
|
||||
use super::CssVariableMachine;
|
||||
use crate::extractor::machine::Machine;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
|
|
@ -204,7 +203,7 @@ mod tests {
|
|||
let actual = CssVariableMachine::test_extract_all(&input);
|
||||
|
||||
if actual != expected {
|
||||
dbg!(&input);
|
||||
dbg!(&input, &actual, &expected);
|
||||
}
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -139,41 +139,6 @@ impl<'a> Extractor<'a> {
|
|||
|
||||
extracted
|
||||
}
|
||||
|
||||
pub fn extract_variables_from_css(&mut self) -> Vec<Extracted<'a>> {
|
||||
let mut extracted = Vec::with_capacity(100);
|
||||
|
||||
let len = self.cursor.input.len();
|
||||
|
||||
let cursor = &mut self.cursor.clone();
|
||||
while cursor.pos < len {
|
||||
if cursor.curr().is_ascii_whitespace() {
|
||||
cursor.advance();
|
||||
continue;
|
||||
}
|
||||
|
||||
if let MachineState::Done(span) = self.css_variable_machine.next(cursor) {
|
||||
// We are only interested in variables that are used, not defined. Therefore we
|
||||
// need to ensure that the variable is prefixed with `var(`.
|
||||
if span.start < 4 {
|
||||
cursor.advance();
|
||||
continue;
|
||||
}
|
||||
|
||||
let slice_before = Span::new(span.start - 4, span.start - 1);
|
||||
if !slice_before.slice(self.cursor.input).starts_with(b"var(") {
|
||||
cursor.advance();
|
||||
continue;
|
||||
}
|
||||
|
||||
extracted.push(Extracted::CssVariable(span.slice(self.cursor.input)));
|
||||
}
|
||||
|
||||
cursor.advance();
|
||||
}
|
||||
|
||||
extracted
|
||||
}
|
||||
}
|
||||
|
||||
// Extract sub-candidates from a given range.
|
||||
|
|
@ -234,11 +199,10 @@ fn drop_covered_spans(mut spans: Vec<Span>) -> Vec<Span> {
|
|||
mod tests {
|
||||
use super::{Extracted, Extractor};
|
||||
use crate::throughput::Throughput;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::hint::black_box;
|
||||
|
||||
fn pre_process_input(input: &str, extension: &str) -> String {
|
||||
let input = crate::scanner::pre_process_input(input.as_bytes().to_vec(), extension);
|
||||
let input = crate::pre_process_input(input.as_bytes(), extension);
|
||||
String::from_utf8(input).unwrap()
|
||||
}
|
||||
|
||||
|
|
@ -280,7 +244,7 @@ mod tests {
|
|||
expected.dedup();
|
||||
|
||||
if actual != expected {
|
||||
dbg!(&input);
|
||||
dbg!(&input, &actual, &expected);
|
||||
}
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
|
@ -308,7 +272,7 @@ mod tests {
|
|||
expected.sort();
|
||||
|
||||
if actual != expected {
|
||||
dbg!(&input);
|
||||
dbg!(&input, &actual, &expected);
|
||||
}
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
|
@ -908,18 +872,6 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/20233
|
||||
#[test]
|
||||
fn test_template_toolkit_syntax() {
|
||||
assert_extract_candidates_contains(
|
||||
&pre_process_input(
|
||||
r#"<div class="[% IF $is_open %]bg-white/40[% ELSE %]bg-white/10[% END %]"></div>"#,
|
||||
"tx",
|
||||
),
|
||||
vec!["bg-white/40", "bg-white/10"],
|
||||
);
|
||||
}
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/17050
|
||||
#[test]
|
||||
fn test_haml_syntax() {
|
||||
|
|
@ -1066,20 +1018,6 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
// See: https://github.com/tailwindlabs/tailwindcss/issues/18092
|
||||
#[test]
|
||||
fn test_leptos_rs_view_class_colon_syntax() {
|
||||
for (input, expected) in [
|
||||
(r#"<div class:px-6=true>"#, vec!["class", "px-6"]),
|
||||
(
|
||||
r#"view! { <div class:px-6=true> }"#,
|
||||
vec!["class", "px-6", "view!"],
|
||||
),
|
||||
] {
|
||||
assert_extract_sorted_candidates(&pre_process_input(input, "svelte"), expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_css_variables() {
|
||||
for (input, expected) in [
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
||||
|
|
@ -121,7 +121,6 @@ enum Class {
|
|||
mod tests {
|
||||
use super::ModifierMachine;
|
||||
use crate::extractor::machine::Machine;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
|
|
|
|||
|
|
@ -52,8 +52,8 @@ impl Machine for NamedUtilityMachine<IdleState> {
|
|||
|
||||
#[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.: `<div class="a"></div>`
|
||||
|
|
@ -90,7 +90,7 @@ impl Machine for NamedUtilityMachine<IdleState> {
|
|||
//
|
||||
// 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<ParsingState> {
|
|||
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<ParsingState> {
|
|||
// 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<ParsingState> {
|
|||
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<ParsingState> {
|
|||
_ => 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<ParsingState> {
|
|||
// ^
|
||||
// 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<ParsingState> {
|
|||
// 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,19 +278,14 @@ impl Machine for NamedUtilityMachine<ParsingState> {
|
|||
//
|
||||
Class::Number => {
|
||||
if !matches!(
|
||||
cursor.prev().into(),
|
||||
Class::Dash
|
||||
| Class::Underscore
|
||||
| Class::Dot
|
||||
| Class::Number
|
||||
| Class::AlphaLower
|
||||
| Class::AlphaUpper
|
||||
cursor.prev.into(),
|
||||
Class::Dash | Class::Dot | Class::Number | Class::AlphaLower
|
||||
) {
|
||||
return self.restart();
|
||||
}
|
||||
|
||||
if !matches!(
|
||||
cursor.next().into(),
|
||||
cursor.next.into(),
|
||||
Class::Dot
|
||||
| Class::Number
|
||||
| Class::AlphaLower
|
||||
|
|
@ -314,7 +309,7 @@ impl Machine for NamedUtilityMachine<ParsingState> {
|
|||
// ^^
|
||||
// ```
|
||||
Class::Percent => {
|
||||
if !matches!(cursor.prev().into(), Class::Number) {
|
||||
if !matches!(cursor.prev.into(), Class::Number) {
|
||||
return self.restart();
|
||||
}
|
||||
|
||||
|
|
@ -391,7 +386,6 @@ enum Class {
|
|||
mod tests {
|
||||
use super::{IdleState, NamedUtilityMachine};
|
||||
use crate::extractor::machine::Machine;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
|
|
@ -420,14 +414,9 @@ mod tests {
|
|||
// With numbers
|
||||
("px-5", vec!["px-5"]),
|
||||
("px-2.5", vec!["px-2.5"]),
|
||||
// Underscores followed by numbers
|
||||
("header_1", vec!["header_1"]),
|
||||
("header_1_2", vec!["header_1_2"]),
|
||||
// With number followed by dash or underscore
|
||||
("text-title1-strong", vec!["text-title1-strong"]),
|
||||
("text-title1_strong", vec!["text-title1_strong"]),
|
||||
// With capital letter followed by number
|
||||
("text-titleV1-strong", vec!["text-titleV1-strong"]),
|
||||
// With trailing % sign
|
||||
("from-15%", vec!["from-15%"]),
|
||||
// Arbitrary value with bracket notation
|
||||
|
|
@ -526,7 +515,7 @@ mod tests {
|
|||
actual.sort();
|
||||
|
||||
if actual != expected {
|
||||
dbg!(&input);
|
||||
dbg!(&input, &expected, &actual);
|
||||
}
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,8 +81,8 @@ impl Machine for NamedVariantMachine<IdleState> {
|
|||
|
||||
#[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.: `<div class="x:flex"></div>`
|
||||
|
|
@ -134,8 +134,8 @@ impl Machine for NamedVariantMachine<ParsingState> {
|
|||
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<ParsingState> {
|
|||
};
|
||||
}
|
||||
|
||||
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<ParsingState> {
|
|||
// 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<ParsingState> {
|
|||
impl NamedVariantMachine<ParsingState> {
|
||||
#[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::<ParsingModifierState>().next(cursor)
|
||||
|
|
@ -285,7 +285,7 @@ impl Machine for NamedVariantMachine<ParsingModifierState> {
|
|||
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<ParsingEndState> {
|
|||
|
||||
#[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:`
|
||||
|
|
@ -367,7 +367,6 @@ enum Class {
|
|||
mod tests {
|
||||
use super::{IdleState, NamedVariantMachine};
|
||||
use crate::extractor::machine::Machine;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
|
|
@ -414,7 +413,7 @@ mod tests {
|
|||
] {
|
||||
let actual = NamedVariantMachine::<IdleState>::test_extract_all(input);
|
||||
if actual != expected {
|
||||
dbg!(&input);
|
||||
dbg!(&input, &actual, &expected);
|
||||
}
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,23 +5,6 @@ use bstr::ByteSlice;
|
|||
#[derive(Debug, Default)]
|
||||
pub struct Clojure;
|
||||
|
||||
/// This is meant to be a rough estimate of a valid ClojureScript keyword
|
||||
///
|
||||
/// This can be approximated by the following regex:
|
||||
/// /::?[a-zA-Z0-9!#$%&*+./:<=>?_|-]+/
|
||||
///
|
||||
/// However, keywords are intended to be detected as utilities. Since the set
|
||||
/// of valid characters in a utility (outside of arbitrary values) is smaller,
|
||||
/// along with the fact that neither `[]` nor `()` are allowed in keywords we
|
||||
/// can simplify this list quite a bit.
|
||||
#[inline]
|
||||
fn is_keyword_character(byte: u8) -> bool {
|
||||
(matches!(
|
||||
byte,
|
||||
b'!' | b'#' | b'%' | b'*' | b'+' | b'-' | b'.' | b'/' | b':' | b'_'
|
||||
) | byte.is_ascii_alphanumeric())
|
||||
}
|
||||
|
||||
impl PreProcessor for Clojure {
|
||||
fn process(&self, content: &[u8]) -> Vec<u8> {
|
||||
let content = content
|
||||
|
|
@ -32,22 +15,18 @@ 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(),
|
||||
|
||||
// End of the string
|
||||
b'"' => {
|
||||
result[cursor.pos] = b' ';
|
||||
break;
|
||||
}
|
||||
b'"' => break,
|
||||
|
||||
// Everything else is valid
|
||||
_ => cursor.advance(),
|
||||
|
|
@ -55,140 +34,20 @@ impl PreProcessor for Clojure {
|
|||
}
|
||||
}
|
||||
|
||||
// Discard line comments until the end of the line.
|
||||
// Consume comments as-is until the end of the line.
|
||||
// Comments start with `;;`
|
||||
b';' if matches!(cursor.next(), b';') => {
|
||||
while cursor.pos < len && cursor.curr() != b'\n' {
|
||||
result[cursor.pos] = b' ';
|
||||
b';' if matches!(cursor.next, b';') => {
|
||||
while cursor.pos < len && cursor.curr != b'\n' {
|
||||
cursor.advance();
|
||||
}
|
||||
}
|
||||
|
||||
// Consume keyword until a terminating character is reached.
|
||||
b':' => {
|
||||
result[cursor.pos] = b' ';
|
||||
cursor.advance();
|
||||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr() {
|
||||
// A `.` surrounded by digits is a decimal number, so we don't want to replace it.
|
||||
//
|
||||
// E.g.:
|
||||
// ```
|
||||
// gap-1.5
|
||||
// ^
|
||||
// ```
|
||||
b'.' if cursor.prev().is_ascii_digit()
|
||||
&& cursor.next().is_ascii_digit() =>
|
||||
{
|
||||
// Keep the `.` as-is
|
||||
}
|
||||
// A `.` not surrounded by digits denotes the start of a new class name in a
|
||||
// dot-delimited keyword.
|
||||
//
|
||||
// E.g.:
|
||||
// ```
|
||||
// flex.gap-1.5
|
||||
// ^
|
||||
// ```
|
||||
b'.' => {
|
||||
result[cursor.pos] = b' ';
|
||||
}
|
||||
// End of keyword.
|
||||
_ if !is_keyword_character(cursor.curr()) => {
|
||||
result[cursor.pos] = b' ';
|
||||
break;
|
||||
}
|
||||
|
||||
// Consume everything else.
|
||||
_ => {}
|
||||
};
|
||||
|
||||
cursor.advance();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle quote with a list, e.g.: `'(…)`
|
||||
// and with a vector, e.g.: `'[…]`
|
||||
b'\'' if matches!(cursor.next(), b'[' | b'(') => {
|
||||
result[cursor.pos] = b' ';
|
||||
cursor.advance();
|
||||
result[cursor.pos] = b' ';
|
||||
let end = match cursor.curr() {
|
||||
b'[' => b']',
|
||||
b'(' => b')',
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
// Consume until the closing `]`
|
||||
while cursor.pos < len {
|
||||
match cursor.curr() {
|
||||
x if x == end => {
|
||||
result[cursor.pos] = b' ';
|
||||
break;
|
||||
}
|
||||
|
||||
// Consume strings as-is
|
||||
b'"' => {
|
||||
result[cursor.pos] = b' ';
|
||||
cursor.advance();
|
||||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr() {
|
||||
// Escaped character, skip ahead to the next character
|
||||
b'\\' => cursor.advance_twice(),
|
||||
|
||||
// End of the string
|
||||
b'"' => {
|
||||
result[cursor.pos] = b' ';
|
||||
break;
|
||||
}
|
||||
|
||||
// Everything else is valid
|
||||
_ => cursor.advance(),
|
||||
};
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
|
||||
cursor.advance();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle quote with a keyword, e.g.: `'bg-white`
|
||||
b'\'' if !cursor.next().is_ascii_whitespace() => {
|
||||
result[cursor.pos] = b' ';
|
||||
cursor.advance();
|
||||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr() {
|
||||
// End of keyword.
|
||||
_ if !is_keyword_character(cursor.curr()) => {
|
||||
result[cursor.pos] = b' ';
|
||||
break;
|
||||
}
|
||||
|
||||
// Consume everything else.
|
||||
_ => {}
|
||||
};
|
||||
|
||||
cursor.advance();
|
||||
}
|
||||
}
|
||||
|
||||
// Aggressively discard everything else, reducing false positives and preventing
|
||||
// characters surrounding keywords from producing false negatives.
|
||||
// E.g.:
|
||||
// ```
|
||||
// (when condition :bg-white)
|
||||
// ^
|
||||
// ```
|
||||
// A ')' is never a valid part of a keyword, but will nonetheless prevent 'bg-white'
|
||||
// from being extracted if not discarded.
|
||||
_ => {
|
||||
b':' | b'.' => {
|
||||
result[cursor.pos] = b' ';
|
||||
}
|
||||
|
||||
// Consume everything else
|
||||
_ => {}
|
||||
};
|
||||
|
||||
cursor.advance();
|
||||
|
|
@ -209,23 +68,19 @@ mod tests {
|
|||
(":div.flex-1.flex-2", " div flex-1 flex-2"),
|
||||
(
|
||||
":.flex-3.flex-4 ;defaults to div",
|
||||
" flex-3 flex-4 ",
|
||||
" flex-3 flex-4 ;defaults to div",
|
||||
),
|
||||
("{:class :flex-5.flex-6", " flex-5 flex-6"),
|
||||
(r#"{:class "flex-7 flex-8"}"#, r#" flex-7 flex-8 "#),
|
||||
("{:class :flex-5.flex-6", "{ flex-5 flex-6"),
|
||||
(r#"{:class "flex-7 flex-8"}"#, r#"{ "flex-7 flex-8"}"#),
|
||||
(
|
||||
r#"{:class ["flex-9" :flex-10]}"#,
|
||||
r#" flex-9 flex-10 "#,
|
||||
r#"{ ["flex-9" flex-10]}"#,
|
||||
),
|
||||
(
|
||||
r#"(dom/div {:class "flex-11 flex-12"})"#,
|
||||
r#" flex-11 flex-12 "#,
|
||||
),
|
||||
("(dom/div :.flex-13.flex-14", " flex-13 flex-14"),
|
||||
(
|
||||
r#"[:div#hello.bg-white.pr-1.5 {:class ["grid grid-cols-[auto,1fr] grid-rows-2"]}]"#,
|
||||
r#" div#hello bg-white pr-1.5 grid grid-cols-[auto,1fr] grid-rows-2 "#,
|
||||
r#"(dom/div { "flex-11 flex-12"})"#,
|
||||
),
|
||||
("(dom/div :.flex-13.flex-14", "(dom/div flex-13 flex-14"),
|
||||
] {
|
||||
Clojure::test(input, expected);
|
||||
}
|
||||
|
|
@ -301,136 +156,4 @@ mod tests {
|
|||
|
||||
Clojure::test_extract_contains(input, vec!["hover:flex", "px-1.5"]);
|
||||
}
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/17760
|
||||
#[test]
|
||||
fn test_extraction_of_classes_with_dots() {
|
||||
let input = r#"
|
||||
($ :div {:class [:flex :gap-1.5 :p-1]} …)
|
||||
"#;
|
||||
|
||||
Clojure::test_extract_contains(input, vec!["flex", "gap-1.5", "p-1"]);
|
||||
}
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/18336
|
||||
#[test]
|
||||
fn test_extraction_of_pseudoclasses_from_keywords() {
|
||||
let input = r#"
|
||||
($ :div {:class [:flex :first:lg:pr-6 :first:2xl:pl-6 :group-hover/2:2xs:pt-6]} …)
|
||||
|
||||
:.hover:bg-white
|
||||
|
||||
[:div#hello.bg-white.pr-1.5]
|
||||
"#;
|
||||
|
||||
Clojure::test_extract_contains(
|
||||
input,
|
||||
vec![
|
||||
"flex",
|
||||
"first:lg:pr-6",
|
||||
"first:2xl:pl-6",
|
||||
"group-hover/2:2xs:pt-6",
|
||||
"hover:bg-white",
|
||||
"bg-white",
|
||||
"pr-1.5",
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/18344
|
||||
#[test]
|
||||
fn test_noninterference_of_parens_on_keywords() {
|
||||
let input = r#"
|
||||
(get props :y-padding :py-5)
|
||||
($ :div {:class [:flex.pr-1.5 (if condition :bg-white :bg-black)]})
|
||||
"#;
|
||||
|
||||
Clojure::test_extract_contains(
|
||||
input,
|
||||
vec!["py-5", "flex", "pr-1.5", "bg-white", "bg-black"],
|
||||
);
|
||||
}
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/18882
|
||||
#[test]
|
||||
fn test_extract_from_symbol_list() {
|
||||
let input = r#"
|
||||
[:div {:class '[z-1 z-2
|
||||
z-3 z-4]}]
|
||||
"#;
|
||||
Clojure::test_extract_contains(input, vec!["z-1", "z-2", "z-3", "z-4"]);
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/pull/18345#issuecomment-3253403847
|
||||
let input = r#"
|
||||
(def hl-class-names '[ring ring-blue-500])
|
||||
|
||||
[:div
|
||||
{:class (cond-> '[input w-full]
|
||||
textarea? (conj 'textarea)
|
||||
(seq errors) (concat '[border-red-500 bg-red-100])
|
||||
highlight? (concat hl-class-names))}]
|
||||
"#;
|
||||
Clojure::test_extract_contains(
|
||||
input,
|
||||
vec![
|
||||
"ring",
|
||||
"ring-blue-500",
|
||||
"input",
|
||||
"w-full",
|
||||
"textarea",
|
||||
"border-red-500",
|
||||
"bg-red-100",
|
||||
],
|
||||
);
|
||||
|
||||
let input = r#"
|
||||
[:div
|
||||
{:class '[h-100 lg:h-200 max-w-32 mx-auto py-60
|
||||
flex flex-col justify-end items-center
|
||||
lg:flex-row lg:justify-between
|
||||
bg-cover bg-center bg-no-repeat rounded-3xl overflow-hidden
|
||||
font-semibold text-gray-900]}]
|
||||
"#;
|
||||
Clojure::test_extract_contains(
|
||||
input,
|
||||
vec![
|
||||
"h-100",
|
||||
"lg:h-200",
|
||||
"max-w-32",
|
||||
"mx-auto",
|
||||
"py-60",
|
||||
"flex",
|
||||
"flex-col",
|
||||
"justify-end",
|
||||
"items-center",
|
||||
"lg:flex-row",
|
||||
"lg:justify-between",
|
||||
"bg-cover",
|
||||
"bg-center",
|
||||
"bg-no-repeat",
|
||||
"rounded-3xl",
|
||||
"overflow-hidden",
|
||||
"font-semibold",
|
||||
"text-gray-900",
|
||||
],
|
||||
);
|
||||
|
||||
// `/` is invalid and requires explicit quoting
|
||||
let input = r#"
|
||||
'[p-32 "text-black/50"]
|
||||
"#;
|
||||
Clojure::test_extract_contains(input, vec!["p-32", "text-black/50"]);
|
||||
|
||||
// `[…]` is invalid and requires explicit quoting
|
||||
let input = r#"
|
||||
(print '[ring ring-blue-500 "bg-[#0088cc]"])
|
||||
"#;
|
||||
Clojure::test_extract_contains(input, vec!["ring", "ring-blue-500", "bg-[#0088cc]"]);
|
||||
|
||||
// `'(…)` looks similar to `[…]` but uses parentheses instead of brackets
|
||||
let input = r#"
|
||||
(print '(ring ring-blue-500 "bg-[#0088cc]"))
|
||||
"#;
|
||||
Clojure::test_extract_contains(input, vec!["ring", "ring-blue-500", "bg-[#0088cc]"]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,154 +0,0 @@
|
|||
use crate::cursor;
|
||||
use crate::extractor::bracket_stack::BracketStack;
|
||||
use crate::extractor::pre_processors::pre_processor::PreProcessor;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Elixir;
|
||||
|
||||
impl PreProcessor for Elixir {
|
||||
fn process(&self, content: &[u8]) -> Vec<u8> {
|
||||
let mut cursor = cursor::Cursor::new(content);
|
||||
let mut result = content.to_vec();
|
||||
let mut bracket_stack = BracketStack::default();
|
||||
|
||||
while cursor.pos < content.len() {
|
||||
// Look for a sigil marker
|
||||
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') {
|
||||
cursor.advance();
|
||||
continue;
|
||||
}
|
||||
|
||||
cursor.advance_twice();
|
||||
|
||||
// Match the opening for a sigil
|
||||
if !matches!(cursor.curr(), b'(' | b'[' | b'{') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Replace the opening bracket with a space
|
||||
result[cursor.pos] = b' ';
|
||||
|
||||
// Scan until we find a balanced closing one and replace it too
|
||||
bracket_stack.push(cursor.curr());
|
||||
|
||||
while cursor.pos < content.len() {
|
||||
cursor.advance();
|
||||
|
||||
match cursor.curr() {
|
||||
// Escaped character, skip ahead to the next character
|
||||
b'\\' => cursor.advance_twice(),
|
||||
b'(' | b'[' | b'{' => {
|
||||
bracket_stack.push(cursor.curr());
|
||||
}
|
||||
b')' | b']' | b'}' if !bracket_stack.is_empty() => {
|
||||
bracket_stack.pop(cursor.curr());
|
||||
|
||||
if bracket_stack.is_empty() {
|
||||
// Replace the closing bracket with a space
|
||||
result[cursor.pos] = b' ';
|
||||
break;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Elixir;
|
||||
use crate::extractor::pre_processors::pre_processor::PreProcessor;
|
||||
|
||||
#[test]
|
||||
fn test_elixir_pre_processor() {
|
||||
for (input, expected) in [
|
||||
// Simple sigils
|
||||
("~W(flex underline)", "~W flex underline "),
|
||||
("~W[flex underline]", "~W flex underline "),
|
||||
("~W{flex underline}", "~W flex underline "),
|
||||
// Sigils with nested brackets
|
||||
(
|
||||
"~W(text-(--my-color) bg-(--my-color))",
|
||||
"~W text-(--my-color) bg-(--my-color) ",
|
||||
),
|
||||
("~W[text-[red] bg-[red]]", "~W text-[red] bg-[red] "),
|
||||
// Word sigils with modifiers
|
||||
("~W(flex underline)a", "~W flex underline a"),
|
||||
("~W(flex underline)c", "~W flex underline c"),
|
||||
("~W(flex underline)s", "~W flex underline s"),
|
||||
// Other sigil types
|
||||
("~w(flex underline)", "~w flex underline "),
|
||||
("~c(flex underline)", "~c flex underline "),
|
||||
("~C(flex underline)", "~C flex underline "),
|
||||
("~s(flex underline)", "~s flex underline "),
|
||||
("~S(flex underline)", "~S flex underline "),
|
||||
] {
|
||||
Elixir::test(input, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_candidates() {
|
||||
let input = r#"
|
||||
~W(c1 c2)
|
||||
~W[c3 c4]
|
||||
~W{c5 c6}
|
||||
~W(text-(--c7) bg-(--c8))
|
||||
~W[text-[c9] bg-[c10]]
|
||||
~W(c13 c14)a
|
||||
~W(c15 c16)c
|
||||
~W(c17 c18)s
|
||||
~w(c19 c20)
|
||||
~c(c21 c22)
|
||||
~C(c23 c24)
|
||||
~s(c25 c26)
|
||||
~S(c27 c28)
|
||||
~W"c29 c30"
|
||||
~W'c31 c32'
|
||||
"#;
|
||||
|
||||
Elixir::test_extract_contains(
|
||||
input,
|
||||
vec![
|
||||
"c1",
|
||||
"c2",
|
||||
"c3",
|
||||
"c4",
|
||||
"c5",
|
||||
"c6",
|
||||
"text-(--c7)",
|
||||
"bg-(--c8)",
|
||||
"c13",
|
||||
"c14",
|
||||
"c15",
|
||||
"c16",
|
||||
"c17",
|
||||
"c18",
|
||||
"c19",
|
||||
"c20",
|
||||
"c21",
|
||||
"c22",
|
||||
"c23",
|
||||
"c24",
|
||||
"c25",
|
||||
"c26",
|
||||
"c27",
|
||||
"c28",
|
||||
"c29",
|
||||
"c30",
|
||||
"c31",
|
||||
"c32",
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -3,8 +3,6 @@ use crate::extractor::bracket_stack::BracketStack;
|
|||
use crate::extractor::machine::{Machine, MachineState};
|
||||
use crate::extractor::pre_processors::pre_processor::PreProcessor;
|
||||
use crate::extractor::variant_machine::VariantMachine;
|
||||
use crate::scanner::pre_process_input;
|
||||
use bstr::ByteVec;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Haml;
|
||||
|
|
@ -16,153 +14,8 @@ impl PreProcessor for Haml {
|
|||
let mut cursor = cursor::Cursor::new(content);
|
||||
let mut bracket_stack = BracketStack::default();
|
||||
|
||||
// Haml Comments: -#
|
||||
// https://haml.info/docs/yardoc/file.REFERENCE.html#ruby-evaluation
|
||||
//
|
||||
// > The hyphen followed immediately by the pound sign signifies a silent comment. Any text
|
||||
// > following this isn’t rendered in the resulting document at all.
|
||||
//
|
||||
// ```haml
|
||||
// %p foo
|
||||
// -# This is a comment
|
||||
// %p bar
|
||||
// ```
|
||||
//
|
||||
// > You can also nest text beneath a silent comment. None of this text will be rendered.
|
||||
//
|
||||
// ```haml
|
||||
// %p foo
|
||||
// -#
|
||||
// This won't be displayed
|
||||
// Nor will this
|
||||
// Nor will this.
|
||||
// %p bar
|
||||
// ```
|
||||
//
|
||||
// Ruby Evaluation
|
||||
// https://haml.info/docs/yardoc/file.REFERENCE.html#ruby-evaluation
|
||||
//
|
||||
// When any of the following characters are the first non-whitespace character on the line,
|
||||
// then the line is treated as Ruby code:
|
||||
//
|
||||
// - Inserting Ruby: =
|
||||
// https://haml.info/docs/yardoc/file.REFERENCE.html#inserting_ruby
|
||||
//
|
||||
// ```haml
|
||||
// %p
|
||||
// = ['hi', 'there', 'reader!'].join " "
|
||||
// = "yo"
|
||||
// ```
|
||||
//
|
||||
// - Running Ruby: -
|
||||
// https://haml.info/docs/yardoc/file.REFERENCE.html#running-ruby--
|
||||
//
|
||||
// ```haml
|
||||
// - foo = "hello"
|
||||
// - foo << " there"
|
||||
// - foo << " you!"
|
||||
// %p= foo
|
||||
// ```
|
||||
//
|
||||
// - Whitespace Preservation: ~
|
||||
// https://haml.info/docs/yardoc/file.REFERENCE.html#tilde
|
||||
//
|
||||
// > ~ works just like =, except that it runs Haml::Helpers.preserve on its input.
|
||||
//
|
||||
// ```haml
|
||||
// ~ "Foo\n<pre>Bar\nBaz</pre>"
|
||||
// ```
|
||||
//
|
||||
// Important note:
|
||||
//
|
||||
// > A line of Ruby code can be stretched over multiple lines as long as each line but the
|
||||
// > last ends with a comma.
|
||||
//
|
||||
// ```haml
|
||||
// - links = {:home => "/",
|
||||
// :docs => "/docs",
|
||||
// :about => "/about"}
|
||||
// ```
|
||||
//
|
||||
// Ruby Blocks:
|
||||
// https://haml.info/docs/yardoc/file.REFERENCE.html#ruby-blocks
|
||||
//
|
||||
// > Ruby blocks, like XHTML tags, don’t need to be explicitly closed in Haml. Rather,
|
||||
// > they’re automatically closed, based on indentation. A block begins whenever the
|
||||
// > indentation is increased after a Ruby evaluation command. It ends when the indentation
|
||||
// > decreases (as long as it’s not an else clause or something similar).
|
||||
//
|
||||
// ```haml
|
||||
// - (42...47).each do |i|
|
||||
// %p= i
|
||||
// %p See, I can count!
|
||||
// ```
|
||||
//
|
||||
let mut last_newline_position = 0;
|
||||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr() {
|
||||
// Escape the next character
|
||||
b'\\' => {
|
||||
cursor.advance_twice();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Track the last newline position
|
||||
b'\n' => {
|
||||
last_newline_position = cursor.pos;
|
||||
cursor.advance();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip HAML comments. `-#`
|
||||
b'-' if cursor.input[last_newline_position..cursor.pos]
|
||||
.iter()
|
||||
.all(u8::is_ascii_whitespace)
|
||||
&& matches!(cursor.next(), b'#') =>
|
||||
{
|
||||
// Just consume the comment
|
||||
let updated_last_newline_position =
|
||||
self.skip_indented_block(&mut cursor, last_newline_position);
|
||||
|
||||
// Override the last known newline position
|
||||
last_newline_position = updated_last_newline_position;
|
||||
}
|
||||
|
||||
// Skip HTML comments. `/`
|
||||
b'/' if cursor.input[last_newline_position..cursor.pos]
|
||||
.iter()
|
||||
.all(u8::is_ascii_whitespace) =>
|
||||
{
|
||||
// Just consume the comment
|
||||
let updated_last_newline_position =
|
||||
self.skip_indented_block(&mut cursor, last_newline_position);
|
||||
|
||||
// Override the last known newline position
|
||||
last_newline_position = updated_last_newline_position;
|
||||
}
|
||||
|
||||
// Ruby evaluation
|
||||
b'-' | b'=' | b'~'
|
||||
if cursor.input[last_newline_position..cursor.pos]
|
||||
.iter()
|
||||
.all(u8::is_ascii_whitespace) =>
|
||||
{
|
||||
let mut start = cursor.pos;
|
||||
let end = self.skip_indented_block(&mut cursor, last_newline_position);
|
||||
|
||||
// Increment start with 1 character to skip the `=` or `-` character
|
||||
start += 1;
|
||||
|
||||
let ruby_code = &cursor.input[start..end];
|
||||
|
||||
// Override the last known newline position
|
||||
last_newline_position = end;
|
||||
|
||||
let replaced = pre_process_input(ruby_code.to_vec(), "rb");
|
||||
result.replace_range(start..end, replaced);
|
||||
}
|
||||
|
||||
match cursor.curr {
|
||||
// Only replace `.` with a space if it's not surrounded by numbers. E.g.:
|
||||
//
|
||||
// ```diff
|
||||
|
|
@ -190,7 +43,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();
|
||||
|
||||
|
|
@ -203,81 +56,6 @@ impl PreProcessor for Haml {
|
|||
}
|
||||
}
|
||||
|
||||
// Handle Ruby syntax with `%w[]` arrays embedded in Haml attribute hashes. E.g.:
|
||||
//
|
||||
// ```haml
|
||||
// %div{class: %w[bg-blue-500 w-10 h-10]}
|
||||
// ```
|
||||
//
|
||||
// A `%` that follows a value is not a percent literal. E.g.: the `50%w` in
|
||||
// `hit rate 50%w.`
|
||||
b'%' if matches!(cursor.next(), b'w' | b'W')
|
||||
&& !cursor.prev().is_ascii_alphanumeric()
|
||||
&& !matches!(cursor.prev(), b'_' | b')' | b']' | b'}') =>
|
||||
{
|
||||
// Boundary characters
|
||||
let (open, close) = match cursor.input.get(cursor.pos + 2) {
|
||||
Some(b'[') => (b'[', b']'),
|
||||
Some(b'(') => (b'(', b')'),
|
||||
Some(b'{') => (b'{', b'}'),
|
||||
Some(b'<') => (b'<', b'>'),
|
||||
|
||||
// Any other ASCII punctuation can be used as a custom delimiter
|
||||
Some(&c) if c.is_ascii_punctuation() => (c, c),
|
||||
|
||||
// Everything else is not a valid delimiter
|
||||
_ => {
|
||||
cursor.advance();
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
result[cursor.pos] = b' '; // Replace `%`
|
||||
cursor.advance();
|
||||
result[cursor.pos] = b' '; // Replace `w`
|
||||
cursor.advance();
|
||||
result[cursor.pos] = b' '; // Replace the opening delimiter
|
||||
cursor.advance();
|
||||
|
||||
// Paired delimiters can be nested as long as they are balanced. E.g.:
|
||||
// `%w[foo[bar]baz]` produces a flat array.
|
||||
let mut depth = 1_usize;
|
||||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr() {
|
||||
// Skip escaped characters, unless the backslash is the delimiter
|
||||
// itself
|
||||
b'\\' if close != b'\\' => {
|
||||
// Use backslash to embed spaces in the strings.
|
||||
if cursor.next() == b' ' {
|
||||
result[cursor.pos] = b' ';
|
||||
}
|
||||
|
||||
cursor.advance();
|
||||
}
|
||||
|
||||
// Start of a nested delimiter pair
|
||||
c if c == open && open != close => depth += 1,
|
||||
|
||||
// Closing delimiter
|
||||
c if c == close => {
|
||||
depth -= 1;
|
||||
|
||||
// End of the literal, replace the closing delimiter with a space
|
||||
if depth == 0 {
|
||||
result[cursor.pos] = b' ';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Everything else is valid content
|
||||
_ => {}
|
||||
}
|
||||
|
||||
cursor.advance();
|
||||
}
|
||||
}
|
||||
|
||||
// Replace following characters with spaces if they are not inside of brackets
|
||||
b'#' | b'=' if bracket_stack.is_empty() => {
|
||||
result[cursor.pos] = b' ';
|
||||
|
|
@ -288,11 +66,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() {
|
||||
|
|
@ -311,117 +89,10 @@ impl PreProcessor for Haml {
|
|||
}
|
||||
}
|
||||
|
||||
impl Haml {
|
||||
fn skip_indented_block(
|
||||
&self,
|
||||
cursor: &mut cursor::Cursor,
|
||||
last_known_newline_position: usize,
|
||||
) -> usize {
|
||||
let len = cursor.input.len();
|
||||
|
||||
// Special case: if the first character of the block is `=`, then newlines are only allowed
|
||||
// _if_ the last character of the previous line is a comma `,`.
|
||||
//
|
||||
// https://haml.info/docs/yardoc/file.REFERENCE.html#inserting_ruby
|
||||
//
|
||||
// > A line of Ruby code can be stretched over multiple lines as long as each line but the
|
||||
// > last ends with a comma. For example:
|
||||
//
|
||||
// ```haml
|
||||
// = link_to_remote "Add to cart",
|
||||
// :url => { :action => "add", :id => product.id },
|
||||
// :update => { :success => "cart", :failure => "error" }
|
||||
// ```
|
||||
let evaluation_type = cursor.curr();
|
||||
|
||||
let block_indentation_level = cursor
|
||||
.pos
|
||||
.saturating_sub(last_known_newline_position)
|
||||
.saturating_sub(1); /* The newline itself */
|
||||
|
||||
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' {
|
||||
cursor.advance();
|
||||
}
|
||||
|
||||
// Block is already done, aka just a line
|
||||
if evaluation_type == b'=' && cursor.prev() != b',' {
|
||||
return cursor.pos;
|
||||
}
|
||||
|
||||
'outer: while cursor.pos < len {
|
||||
match cursor.curr() {
|
||||
// Escape the next character
|
||||
b'\\' => {
|
||||
cursor.advance_twice();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Track the last newline position
|
||||
b'\n' => {
|
||||
last_newline_position = cursor.pos;
|
||||
|
||||
// We are done with this block
|
||||
if evaluation_type == b'=' && cursor.prev() != b',' {
|
||||
break;
|
||||
}
|
||||
|
||||
cursor.advance();
|
||||
continue;
|
||||
}
|
||||
|
||||
// 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' {
|
||||
last_newline_position = cursor.pos;
|
||||
|
||||
if evaluation_type == b'=' && cursor.prev() != b',' {
|
||||
// We are done with this block
|
||||
break 'outer;
|
||||
}
|
||||
}
|
||||
|
||||
cursor.advance();
|
||||
}
|
||||
|
||||
let indentation = cursor
|
||||
.pos
|
||||
.saturating_sub(last_newline_position)
|
||||
.saturating_sub(1); /* The newline itself */
|
||||
if indentation < block_indentation_level {
|
||||
// We are done with this block
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Not whitespace, end of block
|
||||
_ => break,
|
||||
};
|
||||
|
||||
cursor.advance();
|
||||
}
|
||||
|
||||
// We didn't find a newline, we reached the end of the input
|
||||
if last_known_newline_position == last_newline_position {
|
||||
return cursor.pos;
|
||||
}
|
||||
|
||||
// Move the cursor to the last newline position
|
||||
cursor.move_to(last_newline_position);
|
||||
|
||||
last_newline_position
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Haml;
|
||||
use crate::extractor::pre_processors::pre_processor::PreProcessor;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_haml_pre_processor() {
|
||||
|
|
@ -499,97 +170,14 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/20386
|
||||
#[test]
|
||||
fn test_embedded_ruby_percent_w_delimiters() {
|
||||
for (input, expected) in [
|
||||
// %w[…] in an attribute hash
|
||||
(
|
||||
"%div{class: %w[flex px-2.5]}",
|
||||
"%div class: flex px-2.5 ",
|
||||
),
|
||||
// %w<…>
|
||||
(
|
||||
"%div{class: %w<flex px-2.5>}",
|
||||
"%div class: flex px-2.5 ",
|
||||
),
|
||||
// Nested `<…>` does not end the literal
|
||||
(
|
||||
"%div{class: %w<flex <nested> px-2.5>}",
|
||||
"%div class: flex <nested> px-2.5 ",
|
||||
),
|
||||
// Custom delimiters
|
||||
(
|
||||
"%div{class: %w|flex px-2.5|}",
|
||||
"%div class: flex px-2.5 ",
|
||||
),
|
||||
(
|
||||
"%div{class: %W!flex px-2.5!}",
|
||||
"%div class: flex px-2.5 ",
|
||||
),
|
||||
(
|
||||
"%div{class: %w#text-sm leading-6#}",
|
||||
"%div class: text-sm leading-6 ",
|
||||
),
|
||||
(
|
||||
"%div{class: %w=italic tracking-wide=}",
|
||||
"%div class: italic tracking-wide ",
|
||||
),
|
||||
// Nested paired delimiters stay balanced inside the literal
|
||||
(
|
||||
"%div{class: %w[content-['[hello]'] p-4]}",
|
||||
"%div class: content-['[hello]'] p-4 ",
|
||||
),
|
||||
// Escaped spaces embed a space in a single array element
|
||||
(
|
||||
r#"%div{class: %w[foo\ bar baz-1]}"#,
|
||||
r#"%div class: foo bar baz-1 "#,
|
||||
),
|
||||
// A `%` that follows a value is not a percent literal
|
||||
("%p hit rate 50%w.", "%p hit rate 50%w "),
|
||||
] {
|
||||
Haml::test(input, expected);
|
||||
}
|
||||
|
||||
let input = r#"
|
||||
%div{class: %w[bg-blue-500 w-10 h-10]}
|
||||
%div{class: %w<flex px-2.5>}
|
||||
%div{class: %w|underline font-bold|}
|
||||
- classes = %w<mt-4 grid>
|
||||
"#;
|
||||
|
||||
Haml::test_extract_contains(
|
||||
input,
|
||||
vec![
|
||||
"bg-blue-500",
|
||||
"w-10",
|
||||
"h-10",
|
||||
"flex",
|
||||
"px-2.5",
|
||||
"underline",
|
||||
"font-bold",
|
||||
"mt-4",
|
||||
"grid",
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/pull/17051#issuecomment-2711181352
|
||||
#[test]
|
||||
fn test_haml_full_file_17051() {
|
||||
let actual = Haml::extract_annotated(include_bytes!("./test-fixtures/haml/src-17051.haml"));
|
||||
let expected = include_str!("./test-fixtures/haml/dst-17051.haml");
|
||||
fn test_haml_full_file() {
|
||||
let processed = Haml.process(include_bytes!("./test-fixtures/haml/src-1.haml"));
|
||||
let actual = std::str::from_utf8(&processed).unwrap();
|
||||
let expected = include_str!("./test-fixtures/haml/dst-1.haml");
|
||||
|
||||
assert_eq!(actual.replace("\r\n", "\n"), expected.replace("\r\n", "\n"));
|
||||
}
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/17813
|
||||
#[test]
|
||||
fn test_haml_full_file_17813() {
|
||||
let actual = Haml::extract_annotated(include_bytes!("./test-fixtures/haml/src-17813.haml"));
|
||||
let expected = include_str!("./test-fixtures/haml/dst-17813.haml");
|
||||
|
||||
assert_eq!(actual.replace("\r\n", "\n"), expected.replace("\r\n", "\n"));
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -601,16 +189,4 @@ mod tests {
|
|||
"#;
|
||||
Haml::test_extract_contains(input, vec!["flex", "items-center"]);
|
||||
}
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/17379#issuecomment-2910108646
|
||||
#[test]
|
||||
fn test_crash_missing_newline() {
|
||||
// The empty `""` will introduce a newline
|
||||
let good = ["- index = 0", "- index += 1", ""].join("\n");
|
||||
Haml::test_extract_contains(&good, vec!["index"]);
|
||||
|
||||
// This used to crash before the fix
|
||||
let bad = ["- index = 0", "- index += 1"].join("\n");
|
||||
Haml::test_extract_contains(&bad, vec!["index"]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
||||
|
|
|
|||
|
|
@ -1,83 +0,0 @@
|
|||
use crate::cursor;
|
||||
use crate::extractor::pre_processors::pre_processor::PreProcessor;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Markdown;
|
||||
|
||||
impl PreProcessor for Markdown {
|
||||
fn process(&self, content: &[u8]) -> Vec<u8> {
|
||||
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()) {
|
||||
(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'.') if bracket_stack.is_empty() => {
|
||||
result[cursor.pos] = b' ';
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
cursor.advance();
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Markdown;
|
||||
use crate::extractor::pre_processors::pre_processor::PreProcessor;
|
||||
|
||||
#[test]
|
||||
fn test_markdown_pre_processor() {
|
||||
for (input, expected) in [
|
||||
// Convert dots to spaces inside markdown inline directives
|
||||
(
|
||||
":span[Some Text]{.text-gray-500}",
|
||||
":span[Some Text] text-gray-500 ",
|
||||
),
|
||||
(
|
||||
":span[Some Text]{.text-gray-500.bg-red-500}",
|
||||
":span[Some Text] text-gray-500 bg-red-500 ",
|
||||
),
|
||||
(
|
||||
":span[Some Text]{#myId .my-class key=val key2='val 2'}",
|
||||
":span[Some Text] #myId my-class key=val key2='val 2' ",
|
||||
),
|
||||
] {
|
||||
Markdown::test(input, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nested_classes_keep_the_dots() {
|
||||
for (input, expected) in [
|
||||
(
|
||||
r#"{<div class="px-2.5"></div>}"#,
|
||||
r#" <div class="px-2.5"></div> "#,
|
||||
),
|
||||
(r#"{content-['example.js']}"#, r#" content-['example.js'] "#),
|
||||
] {
|
||||
Markdown::test(input, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,31 +1,21 @@
|
|||
pub mod clojure;
|
||||
pub mod elixir;
|
||||
pub mod haml;
|
||||
pub mod json;
|
||||
pub mod markdown;
|
||||
pub mod pre_processor;
|
||||
pub mod pug;
|
||||
pub mod razor;
|
||||
pub mod ruby;
|
||||
pub mod rust;
|
||||
pub mod slim;
|
||||
pub mod svelte;
|
||||
pub mod template_toolkit;
|
||||
pub mod twig;
|
||||
pub mod vue;
|
||||
|
||||
pub use clojure::*;
|
||||
pub use elixir::*;
|
||||
pub use haml::*;
|
||||
pub use json::*;
|
||||
pub use markdown::*;
|
||||
pub use pre_processor::*;
|
||||
pub use pug::*;
|
||||
pub use razor::*;
|
||||
pub use ruby::*;
|
||||
pub use rust::*;
|
||||
pub use slim::*;
|
||||
pub use svelte::*;
|
||||
pub use template_toolkit::*;
|
||||
pub use twig::*;
|
||||
pub use vue::*;
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@ pub trait PreProcessor: Sized + Default {
|
|||
|
||||
#[cfg(test)]
|
||||
fn test(input: &str, expected: &str) {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
let input = input.as_bytes();
|
||||
let expected = expected.as_bytes();
|
||||
|
||||
|
|
@ -17,6 +15,10 @@ pub trait PreProcessor: Sized + Default {
|
|||
let actual = String::from_utf8_lossy(&actual);
|
||||
let expected = String::from_utf8_lossy(expected);
|
||||
|
||||
if actual != expected {
|
||||
dbg!((&input, &actual, &expected));
|
||||
}
|
||||
|
||||
// The input and output should have the exact same length.
|
||||
assert_eq!(input.len(), actual.len());
|
||||
assert_eq!(actual.len(), expected.len());
|
||||
|
|
@ -25,33 +27,7 @@ pub trait PreProcessor: Sized + Default {
|
|||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn test_extract_exact(input: &str, expected: Vec<&str>) {
|
||||
use crate::extractor::{Extracted, Extractor};
|
||||
|
||||
let input = input.as_bytes();
|
||||
|
||||
let processor = Self::default();
|
||||
let transformed = processor.process(input);
|
||||
|
||||
let extracted = Extractor::new(&transformed).extract();
|
||||
|
||||
// Extract all candidates and css variables.
|
||||
let candidates = extracted
|
||||
.iter()
|
||||
.filter_map(|x| match x {
|
||||
Extracted::Candidate(bytes) => std::str::from_utf8(bytes).ok(),
|
||||
Extracted::CssVariable(bytes) => std::str::from_utf8(bytes).ok(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if candidates != expected {
|
||||
dbg!(&candidates, &expected);
|
||||
panic!("Extracted candidates do not match expected candidates");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn test_extract_contains(input: &str, expected: Vec<&str>) {
|
||||
fn test_extract_contains(input: &str, items: Vec<&str>) {
|
||||
use crate::extractor::{Extracted, Extractor};
|
||||
|
||||
let input = input.as_bytes();
|
||||
|
|
@ -72,7 +48,7 @@ pub trait PreProcessor: Sized + Default {
|
|||
|
||||
// Ensure all items are present in the candidates.
|
||||
let mut missing = vec![];
|
||||
for item in &expected {
|
||||
for item in &items {
|
||||
if !candidates.contains(item) {
|
||||
missing.push(item);
|
||||
}
|
||||
|
|
@ -83,104 +59,4 @@ pub trait PreProcessor: Sized + Default {
|
|||
panic!("Missing some items");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn extract_annotated(input: &[u8]) -> String {
|
||||
use crate::extractor::{Extracted, Extractor};
|
||||
use std::collections::BTreeMap;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
let processor = Self::default();
|
||||
let transformed = processor.process(input);
|
||||
|
||||
let extracted = Extractor::new(&transformed).extract();
|
||||
|
||||
// Extract only candidate positions
|
||||
let byte_ranges = extracted
|
||||
.iter()
|
||||
.filter_map(|x| match x {
|
||||
Extracted::Candidate(bytes) => {
|
||||
let start = bytes.as_ptr() as usize - transformed.as_ptr() as usize;
|
||||
let end = start + bytes.len();
|
||||
Some((start, end))
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Convert byte ranges to (line, start_col, end_col)
|
||||
let mut annotations = byte_ranges
|
||||
.into_iter()
|
||||
.map(|(start, end)| {
|
||||
let (line, start_col) = byte_offset_to_line_and_column(input, start);
|
||||
let (_, end_col) = byte_offset_to_line_and_column(input, end);
|
||||
(line, start_col, end_col)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Sort for safe insertion
|
||||
annotations.sort_by(|a, b| b.0.cmp(&a.0).then(b.1.cmp(&a.1)));
|
||||
|
||||
// Convert input to lines
|
||||
let mut lines = std::str::from_utf8(input)
|
||||
.expect("Input must be valid UTF-8")
|
||||
.lines()
|
||||
.map(|line| line.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Group annotations per line
|
||||
let mut grouped = BTreeMap::<usize, Vec<(usize, usize)>>::new();
|
||||
for (line, start_char, end_char) in annotations {
|
||||
grouped
|
||||
.entry(line)
|
||||
.or_default()
|
||||
.push((start_char, end_char));
|
||||
}
|
||||
|
||||
// Inject annotation lines
|
||||
for (line_idx, spans) in grouped.into_iter().rev() {
|
||||
let display_line = &lines[line_idx];
|
||||
let width = UnicodeWidthStr::width(display_line.as_str());
|
||||
let mut annotation = vec![' '; width];
|
||||
|
||||
for (start, end) in spans {
|
||||
for i in start..end.min(annotation.len()) {
|
||||
annotation[i] = '^';
|
||||
}
|
||||
}
|
||||
|
||||
let annotation_line: String = annotation
|
||||
.into_iter()
|
||||
.collect::<String>()
|
||||
.trim_end()
|
||||
.to_owned();
|
||||
lines.insert(line_idx + 1, annotation_line);
|
||||
}
|
||||
|
||||
lines.join("\n").trim_end().to_string() + "\n"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn byte_offset_to_line_and_column(input: &[u8], offset: usize) -> (usize, usize) {
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
let mut line_start = 0;
|
||||
let mut line = 0;
|
||||
|
||||
for (i, &b) in input.iter().enumerate() {
|
||||
if i >= offset {
|
||||
break;
|
||||
}
|
||||
if b == b'\n' {
|
||||
line += 1;
|
||||
line_start = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
let slice = &input[line_start..offset];
|
||||
let column = std::str::from_utf8(slice).expect("Valid UTF-8");
|
||||
let column = UnicodeWidthStr::width(column);
|
||||
|
||||
(line, column)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ pub struct Razor;
|
|||
|
||||
impl PreProcessor for Razor {
|
||||
fn process(&self, content: &[u8]) -> Vec<u8> {
|
||||
content.replace("@@", " @").replace(r#"@("@")"#, " @")
|
||||
content.replace("@@", " @")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -24,19 +24,4 @@ mod tests {
|
|||
Razor::test(input, expected);
|
||||
Razor::test_extract_contains(input, vec!["@sm:text-red-500"]);
|
||||
}
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/17424
|
||||
#[test]
|
||||
fn test_razor_syntax_with() {
|
||||
let (input, expected) = (
|
||||
r#"<p class="@("@")md:bg-red-500 @@md:border-green-500 border-8">With 2 elements</p>"#,
|
||||
r#"<p class=" @md:bg-red-500 @md:border-green-500 border-8">With 2 elements</p>"#,
|
||||
);
|
||||
|
||||
Razor::test(input, expected);
|
||||
Razor::test_extract_contains(
|
||||
input,
|
||||
vec!["@md:bg-red-500", "@md:border-green-500", "border-8"],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,23 +3,13 @@
|
|||
use crate::cursor;
|
||||
use crate::extractor::bracket_stack;
|
||||
use crate::extractor::pre_processors::pre_processor::PreProcessor;
|
||||
use crate::scanner::pre_process_input;
|
||||
use bstr::ByteVec;
|
||||
use regex::{Regex, RegexBuilder};
|
||||
use crate::pre_process_input;
|
||||
use bstr::ByteSlice;
|
||||
use fancy_regex::Regex;
|
||||
use std::sync;
|
||||
|
||||
static TEMPLATE_START_REGEX: sync::LazyLock<Regex> = sync::LazyLock::new(|| {
|
||||
RegexBuilder::new(r#"\s*([a-z0-9_-]+)_template\s*<<[-~]?([A-Z]+)$"#)
|
||||
.multi_line(true)
|
||||
.build()
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
static TEMPLATE_END_REGEX: sync::LazyLock<Regex> = sync::LazyLock::new(|| {
|
||||
RegexBuilder::new(r#"^\s*([A-Z]+)"#)
|
||||
.multi_line(true)
|
||||
.build()
|
||||
.unwrap()
|
||||
static TEMPLATE_REGEX: sync::LazyLock<Regex> = sync::LazyLock::new(|| {
|
||||
Regex::new(r#"\s*(.*?)_template\s*<<[-~]?([A-Z]+?)\n([\s\S]*?)\2"#).unwrap()
|
||||
});
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
|
|
@ -34,135 +24,21 @@ impl PreProcessor for Ruby {
|
|||
|
||||
// Extract embedded template languages
|
||||
// https://viewcomponent.org/guide/templates.html#interpolations
|
||||
// Only process if content is valid UTF-8, otherwise skip HEREDOC extraction
|
||||
// but still perform the byte-level Ruby processing below
|
||||
if let Ok(content_as_str) = std::str::from_utf8(content) {
|
||||
let starts = TEMPLATE_START_REGEX
|
||||
.captures_iter(content_as_str)
|
||||
.collect::<Vec<_>>();
|
||||
let ends = TEMPLATE_END_REGEX
|
||||
.captures_iter(content_as_str)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for start in starts.iter() {
|
||||
// The language for this block
|
||||
let lang = start.get(1).unwrap().as_str();
|
||||
|
||||
// The HEREDOC delimiter
|
||||
let delimiter_start = start.get(2).unwrap().as_str();
|
||||
|
||||
// Where the "body" starts for the HEREDOC block
|
||||
let body_start = start.get(0).unwrap().end();
|
||||
|
||||
// Look through all of the ends to find a matching language
|
||||
for end in ends.iter() {
|
||||
// 1. This must appear after the start
|
||||
let body_end = end.get(0).unwrap().start();
|
||||
if body_end < body_start {
|
||||
continue;
|
||||
}
|
||||
|
||||
// The languages must match otherwise we haven't found the end
|
||||
let delimiter_end = end.get(1).unwrap().as_str();
|
||||
if delimiter_end != delimiter_start {
|
||||
continue;
|
||||
}
|
||||
|
||||
let body = &content_as_str[body_start..body_end];
|
||||
let replaced =
|
||||
pre_process_input(body.as_bytes().to_vec(), &lang.to_ascii_lowercase());
|
||||
|
||||
result.replace_range(body_start..body_end, replaced);
|
||||
break;
|
||||
}
|
||||
}
|
||||
let content_as_str = std::str::from_utf8(content).unwrap();
|
||||
for capture in TEMPLATE_REGEX
|
||||
.captures_iter(content_as_str)
|
||||
.filter_map(Result::ok)
|
||||
{
|
||||
let lang = capture.get(1).unwrap().as_str();
|
||||
let body = capture.get(3).unwrap().as_str();
|
||||
let replaced = pre_process_input(body.as_bytes(), lang);
|
||||
result = result.replace(body, replaced);
|
||||
}
|
||||
|
||||
// Ruby extraction
|
||||
while cursor.pos < len {
|
||||
match cursor.curr() {
|
||||
b'"' => {
|
||||
cursor.advance();
|
||||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr() {
|
||||
// Escaped character, skip ahead to the next character
|
||||
b'\\' => cursor.advance_twice(),
|
||||
|
||||
// End of the string
|
||||
b'"' => break,
|
||||
|
||||
// Everything else is valid
|
||||
_ => cursor.advance(),
|
||||
};
|
||||
}
|
||||
|
||||
cursor.advance();
|
||||
continue;
|
||||
}
|
||||
|
||||
b'\'' => {
|
||||
cursor.advance();
|
||||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr() {
|
||||
// Escaped character, skip ahead to the next character
|
||||
b'\\' => cursor.advance_twice(),
|
||||
|
||||
// End of the string
|
||||
b'\'' => break,
|
||||
|
||||
// Everything else is valid
|
||||
_ => cursor.advance(),
|
||||
};
|
||||
}
|
||||
|
||||
cursor.advance();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Replace comments in Ruby files
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
// We also want to skip interpolation syntax, which look like `#{…}`.
|
||||
b'#' if !matches!(cursor.prev(), b'%') && !matches!(cursor.next(), b'{') => {
|
||||
result[cursor.pos] = b' ';
|
||||
cursor.advance();
|
||||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr() {
|
||||
// End of the comment
|
||||
b'\n' => break,
|
||||
|
||||
// Everything else is part of the comment and replaced
|
||||
_ => {
|
||||
result[cursor.pos] = b' ';
|
||||
cursor.advance();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
cursor.advance();
|
||||
continue;
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Looking for `%w`, `%W`, or `%p`
|
||||
if cursor.curr() != b'%' || !matches!(cursor.next(), b'w' | b'W' | b'p') {
|
||||
cursor.advance();
|
||||
continue;
|
||||
}
|
||||
|
||||
// A `%` that follows a value is a modulo operation, not a percent literal. E.g.: the
|
||||
// `50%w` in `hit rate 50%w.`
|
||||
if cursor.prev().is_ascii_alphanumeric()
|
||||
|| matches!(cursor.prev(), b'_' | b')' | b']' | b'}')
|
||||
{
|
||||
// Looking for `%w` or `%W`
|
||||
if cursor.curr != b'%' && !matches!(cursor.next, b'w' | b'W') {
|
||||
cursor.advance();
|
||||
continue;
|
||||
}
|
||||
|
|
@ -170,17 +46,10 @@ 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'}',
|
||||
b'<' => b'>',
|
||||
b' ' => b'\n',
|
||||
|
||||
// Any other ASCII punctuation can be used as a custom delimiter
|
||||
c if c.is_ascii_punctuation() => c,
|
||||
|
||||
// Everything else is not a valid delimiter
|
||||
_ => {
|
||||
cursor.advance();
|
||||
continue;
|
||||
|
|
@ -196,11 +65,11 @@ impl PreProcessor for Ruby {
|
|||
cursor.advance();
|
||||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr() {
|
||||
// Skip escaped characters, unless the backslash is the delimiter itself
|
||||
b'\\' if boundary != b'\\' => {
|
||||
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' ';
|
||||
}
|
||||
|
||||
|
|
@ -209,36 +78,20 @@ impl PreProcessor for Ruby {
|
|||
|
||||
// Start of a nested bracket
|
||||
b'[' | b'(' | b'{' => {
|
||||
bracket_stack.push(cursor.curr());
|
||||
}
|
||||
|
||||
// Start of a nested `<…>`, which Ruby allows inside a `%w<…>` literal
|
||||
b'<' if boundary == 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()) {
|
||||
// Unbalanced
|
||||
cursor.advance();
|
||||
}
|
||||
}
|
||||
|
||||
// End of a nested `<…>`
|
||||
b'>' if boundary == b'>' && !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 boundary != b'\n' {
|
||||
result[cursor.pos] = b' ';
|
||||
}
|
||||
|
||||
_ if cursor.curr == boundary => {
|
||||
result[cursor.pos] = b' ';
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -280,69 +133,12 @@ mod tests {
|
|||
"%w(flex data-[state=pending]:bg-(--my-color) flex-col)",
|
||||
"%w flex data-[state=pending]:bg-(--my-color) flex-col ",
|
||||
),
|
||||
// %w<…>
|
||||
("%w<flex px-2.5>", "%w flex px-2.5 "),
|
||||
(
|
||||
"%w<flex data-[state=pending]:bg-(--my-color) flex-col>",
|
||||
"%w flex data-[state=pending]:bg-(--my-color) flex-col ",
|
||||
),
|
||||
// Nested `<…>` does not end the literal
|
||||
("%w<flex <nested> px-2.5>", "%w flex <nested> px-2.5 "),
|
||||
// %w|…|, %w:…:, %w!…!
|
||||
("%w|flex px-2.5|", "%w flex px-2.5 "),
|
||||
("%w:flex px-2.5:", "%w flex px-2.5 "),
|
||||
("%w!flex px-2.5!", "%w flex px-2.5 "),
|
||||
(r#"%w\flex px-2.5\"#, r#"%w flex px-2.5 "#),
|
||||
// A `%` that follows a value is a modulo operation, not a percent literal
|
||||
(
|
||||
"hit rate 50%w.\n%w[flex px-2.5]",
|
||||
"hit rate 50%w.\n%w flex px-2.5 ",
|
||||
),
|
||||
|
||||
// %w …\n
|
||||
("%w flex px-2.5\n", "%w flex px-2.5\n"),
|
||||
|
||||
// Use backslash to embed spaces in the strings.
|
||||
(r#"%w[foo\ bar baz\ bat]"#, r#"%w foo bar baz bat "#),
|
||||
(r#"%W[foo\ bar baz\ bat]"#, r#"%W foo bar baz bat "#),
|
||||
|
||||
// The nested delimiters evaluated to a flat array of strings
|
||||
// (not nested array).
|
||||
(r#"%w[foo[bar baz]qux]"#, r#"%w foo[bar baz]qux "#),
|
||||
|
||||
(
|
||||
"# test\n# test\n# {ActiveRecord::Base#save!}[rdoc-ref:Persistence#save!]\n%w[flex px-2.5]",
|
||||
" \n \n \n%w flex px-2.5 "
|
||||
),
|
||||
|
||||
(r#""foo # bar""#, r#""foo # bar""#),
|
||||
(r#"'foo # bar'"#, r#"'foo # bar'"#),
|
||||
(
|
||||
r#"def call = tag.span "Foo", class: %w[rounded-full h-0.75 w-0.75]"#,
|
||||
r#"def call = tag.span "Foo", class: %w rounded-full h-0.75 w-0.75 "#
|
||||
),
|
||||
|
||||
(r#"%w[foo ' bar]"#, r#"%w foo ' bar "#),
|
||||
(r#"%w[foo " bar]"#, r#"%w foo " bar "#),
|
||||
(r#"%W[foo ' bar]"#, r#"%W foo ' bar "#),
|
||||
(r#"%W[foo " bar]"#, r#"%W foo " bar "#),
|
||||
|
||||
(r#"%p foo ' bar "#, r#"%p foo ' bar "#),
|
||||
(r#"%p foo " bar "#, r#"%p foo " bar "#),
|
||||
|
||||
(
|
||||
"%p has a ' quote\n# this should be removed\n%p has a ' quote",
|
||||
"%p has a ' quote\n \n%p has a ' quote"
|
||||
),
|
||||
(
|
||||
"%p has a \" quote\n# this should be removed\n%p has a \" quote",
|
||||
"%p has a \" quote\n \n%p has a \" quote"
|
||||
),
|
||||
|
||||
(
|
||||
"%w#this text is kept# # this text is not",
|
||||
"%w this text is kept ",
|
||||
),
|
||||
] {
|
||||
Ruby::test(input, expected);
|
||||
}
|
||||
|
|
@ -375,30 +171,6 @@ mod tests {
|
|||
"%w(flex data-[state=pending]:bg-(--my-color) flex-col)",
|
||||
vec!["flex", "data-[state=pending]:bg-(--my-color)", "flex-col"],
|
||||
),
|
||||
// %w<…>
|
||||
("%w<flex px-2.5>", vec!["flex", "px-2.5"]),
|
||||
("%w<px-2.5 flex>", vec!["flex", "px-2.5"]),
|
||||
("%w<2xl:flex>", vec!["2xl:flex"]),
|
||||
(
|
||||
"%w<flex data-[state=pending]:bg-(--my-color) flex-col>",
|
||||
vec!["flex", "data-[state=pending]:bg-(--my-color)", "flex-col"],
|
||||
),
|
||||
// Nested `<…>` does not end the literal
|
||||
("%w<flex <nested> px-2.5>", vec!["flex", "px-2.5"]),
|
||||
// %w|…|, %w:…:, %w!…!
|
||||
("%w|flex px-2.5|", vec!["flex", "px-2.5"]),
|
||||
("%w:flex px-2.5:", vec!["flex", "px-2.5"]),
|
||||
("%w!flex px-2.5!", vec!["flex", "px-2.5"]),
|
||||
|
||||
(
|
||||
"# test\n# test\n# {ActiveRecord::Base#save!}[rdoc-ref:Persistence#save!]\n%w[flex px-2.5]",
|
||||
vec!["flex", "px-2.5"],
|
||||
),
|
||||
|
||||
(r#""foo # bar""#, vec!["foo", "bar"]),
|
||||
(r#"'foo # bar'"#, vec!["foo", "bar"]),
|
||||
|
||||
(r#"%w[foo ' bar]"#, vec!["foo", "bar"]),
|
||||
] {
|
||||
Ruby::test_extract_contains(input, expected);
|
||||
}
|
||||
|
|
@ -450,79 +222,4 @@ mod tests {
|
|||
"#;
|
||||
Ruby::test_extract_contains(input, vec!["z-1", "z-2", "z-3"]);
|
||||
}
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/19728
|
||||
#[test]
|
||||
fn test_interpolated_expressions() {
|
||||
let input = r#"
|
||||
def width_class(width = nil)
|
||||
<<~STYLE_CLASS
|
||||
#{width || 'w-100'}
|
||||
STYLE_CLASS
|
||||
end
|
||||
"#;
|
||||
|
||||
Ruby::test_extract_contains(input, vec!["w-100"]);
|
||||
}
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/19239
|
||||
#[test]
|
||||
fn test_skip_comments() {
|
||||
let input = r#"
|
||||
# From activerecord-8.1.1/lib/active_record/errors.rb:147
|
||||
# Rails uses RDoc cross-reference syntax in inline documentation:
|
||||
# {ActiveRecord::Base#save!}[rdoc-ref:Persistence#save!]
|
||||
"#;
|
||||
|
||||
// Nothing should be extracted from comments, so expect an empty array.
|
||||
Ruby::test_extract_exact(input, vec![]);
|
||||
}
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/19481
|
||||
#[test]
|
||||
fn test_strict_locals() {
|
||||
// Strict locals are defined in a `<%# locals: … %>`, but the `#` looks like a comment
|
||||
// which we should not ignore in this case.
|
||||
let input = r#"
|
||||
<%# locals: (css: "text-amber-600") %>
|
||||
<% more_css = "text-sky-500" %>
|
||||
|
||||
<p class="text-green-500">
|
||||
In a partial
|
||||
</p>
|
||||
|
||||
<p class="<%= css %>">
|
||||
In a partial using explicit local variables
|
||||
</p>
|
||||
|
||||
<p class="<%= more_css %>">
|
||||
In a partial using explicit local variables
|
||||
</p>
|
||||
"#;
|
||||
|
||||
Ruby::test_extract_contains(
|
||||
input,
|
||||
vec!["text-amber-600", "text-sky-500", "text-green-500"],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_utf8_does_not_panic() {
|
||||
// Invalid UTF-8 sequence: 0x80 is a continuation byte without a leading byte
|
||||
let invalid_utf8: &[u8] = &[0x80, 0x81, 0x82];
|
||||
|
||||
let processor = Ruby::default();
|
||||
|
||||
// Should not panic, just return the input unchanged
|
||||
let result = processor.process(invalid_utf8);
|
||||
assert_eq!(result, invalid_utf8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_valid_utf8_with_multibyte_chars() {
|
||||
// Test that valid UTF-8 with multi-byte characters (like em-dashes) works
|
||||
let input = "# Comment with em—dash\n%w[flex px-2.5]";
|
||||
|
||||
Ruby::test_extract_contains(input, vec!["flex", "px-2.5"]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,238 +0,0 @@
|
|||
use crate::extractor::bracket_stack;
|
||||
use crate::extractor::cursor;
|
||||
use crate::extractor::machine::Machine;
|
||||
use crate::extractor::pre_processors::pre_processor::PreProcessor;
|
||||
use crate::extractor::variant_machine::VariantMachine;
|
||||
use crate::extractor::MachineState;
|
||||
use bstr::ByteSlice;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Rust;
|
||||
|
||||
impl PreProcessor for Rust {
|
||||
fn process(&self, content: &[u8]) -> Vec<u8> {
|
||||
// Leptos support: https://github.com/tailwindlabs/tailwindcss/pull/18093
|
||||
let replaced_content = content
|
||||
.replace(" class:", " class ")
|
||||
.replace("\tclass:", " class ")
|
||||
.replace("\nclass:", " class ");
|
||||
|
||||
if replaced_content.contains_str(b"html!") {
|
||||
self.process_maud_templates(&replaced_content)
|
||||
} else {
|
||||
replaced_content
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Rust {
|
||||
fn process_maud_templates(&self, replaced_content: &[u8]) -> Vec<u8> {
|
||||
let len = replaced_content.len();
|
||||
let mut result = replaced_content.to_vec();
|
||||
let mut cursor = cursor::Cursor::new(replaced_content);
|
||||
let mut bracket_stack = bracket_stack::BracketStack::default();
|
||||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr() {
|
||||
// Escaped character, skip ahead to the next character
|
||||
b'\\' => {
|
||||
cursor.advance_twice();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Consume strings as-is
|
||||
b'"' => {
|
||||
result[cursor.pos] = b' ';
|
||||
cursor.advance();
|
||||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr() {
|
||||
// Escaped character, skip ahead to the next character
|
||||
b'\\' => cursor.advance_twice(),
|
||||
|
||||
// End of the string
|
||||
b'"' => {
|
||||
result[cursor.pos] = b' ';
|
||||
break;
|
||||
}
|
||||
|
||||
// Everything else is valid
|
||||
_ => cursor.advance(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Only replace `.` with a space if it's not surrounded by numbers. E.g.:
|
||||
//
|
||||
// ```diff
|
||||
// - .flex.items-center
|
||||
// + flex items-center
|
||||
// ```
|
||||
//
|
||||
// But with numbers, it's allowed:
|
||||
//
|
||||
// ```diff
|
||||
// - px-2.5
|
||||
// + px-2.5
|
||||
// ```
|
||||
b'.' => {
|
||||
// Don't replace dots with spaces when inside of any type of brackets, because
|
||||
// this could be part of arbitrary values. E.g.: `bg-[url(https://example.com)]`
|
||||
// ^
|
||||
if !bracket_stack.is_empty() {
|
||||
cursor.advance();
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the dot is surrounded by digits, we want to keep it. E.g.: `px-2.5`
|
||||
// EXCEPT if it's followed by a valid variant that happens to start with a
|
||||
// digit.
|
||||
// E.g.: `bg-red-500.2xl:flex`
|
||||
// ^^^
|
||||
if cursor.prev().is_ascii_digit() && cursor.next().is_ascii_digit() {
|
||||
let mut next_cursor = cursor.clone();
|
||||
next_cursor.advance();
|
||||
|
||||
let mut variant_machine = VariantMachine::default();
|
||||
if let MachineState::Done(_) = variant_machine.next(&mut next_cursor) {
|
||||
result[cursor.pos] = b' ';
|
||||
}
|
||||
} else {
|
||||
result[cursor.pos] = b' ';
|
||||
}
|
||||
}
|
||||
|
||||
b'[' => {
|
||||
bracket_stack.push(cursor.curr());
|
||||
|
||||
// Handle `p.flex[condition]`. If there is a `-` before it, it will likely be an
|
||||
// arbitrary value e.g. `text-[red]`
|
||||
if !matches!(cursor.prev(), b'-') {
|
||||
result[cursor.pos] = b' ';
|
||||
}
|
||||
}
|
||||
|
||||
b']' if !bracket_stack.is_empty() => {
|
||||
bracket_stack.pop(cursor.curr());
|
||||
}
|
||||
|
||||
// Consume everything else
|
||||
_ => {}
|
||||
};
|
||||
|
||||
cursor.advance();
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Rust;
|
||||
use crate::extractor::pre_processors::pre_processor::PreProcessor;
|
||||
|
||||
#[test]
|
||||
fn test_leptos_extraction() {
|
||||
for (input, expected) in [
|
||||
// Spaces
|
||||
(
|
||||
"<div class:flex class:px-2.5={condition()}>",
|
||||
"<div class flex class px-2.5={condition()}>",
|
||||
),
|
||||
// Tabs
|
||||
(
|
||||
"<div\tclass:flex class:px-2.5={condition()}>",
|
||||
"<div class flex class px-2.5={condition()}>",
|
||||
),
|
||||
// Newlines
|
||||
(
|
||||
"<div\nclass:flex class:px-2.5={condition()}>",
|
||||
"<div class flex class px-2.5={condition()}>",
|
||||
),
|
||||
] {
|
||||
Rust::test(input, expected);
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/18984
|
||||
#[test]
|
||||
fn test_maud_template_extraction() {
|
||||
let input = r#"
|
||||
use maud::{html, Markup};
|
||||
|
||||
pub fn main() -> Markup {
|
||||
html! {
|
||||
header.px-8.py-4.text-black {
|
||||
"Hello, world!"
|
||||
}
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
Rust::test_extract_contains(input, vec!["px-8", "py-4", "text-black"]);
|
||||
|
||||
// https://maud.lambda.xyz/elements-attributes.html#classes-and-ids-foo-bar
|
||||
let input = r#"
|
||||
html! {
|
||||
input #cannon .big.scary.bright-red type="button" value="Launch Party Cannon";
|
||||
}
|
||||
"#;
|
||||
Rust::test_extract_contains(input, vec!["big", "scary", "bright-red"]);
|
||||
|
||||
let input = r#"
|
||||
html! {
|
||||
div."bg-[#0088cc]" { "Quotes for backticks" }
|
||||
}
|
||||
"#;
|
||||
Rust::test_extract_contains(input, vec!["bg-[#0088cc]"]);
|
||||
|
||||
let input = r#"
|
||||
html! {
|
||||
#main {
|
||||
"Main content!"
|
||||
.tip { "Storing food in a refrigerator can make it 20% cooler." }
|
||||
}
|
||||
}
|
||||
"#;
|
||||
Rust::test_extract_contains(input, vec!["tip"]);
|
||||
|
||||
let input = r#"
|
||||
html! {
|
||||
div."bg-[url(https://example.com)]" { "Arbitrary values" }
|
||||
}
|
||||
"#;
|
||||
Rust::test_extract_contains(input, vec!["bg-[url(https://example.com)]"]);
|
||||
|
||||
let input = r#"
|
||||
html! {
|
||||
div.px-4.text-black {
|
||||
"Some text, with unbalanced brackets ]["
|
||||
}
|
||||
div.px-8.text-white {
|
||||
"Some more text, with unbalanced brackets ]["
|
||||
}
|
||||
}
|
||||
"#;
|
||||
Rust::test_extract_contains(input, vec!["px-4", "text-black", "px-8", "text-white"]);
|
||||
|
||||
let input = r#"html! { \x.px-4.text-black { } }"#;
|
||||
Rust::test(input, r#"html! { \x px-4 text-black { } }"#);
|
||||
}
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/20233
|
||||
#[test]
|
||||
fn test_maud_template_extraction_with_conditional_classes() {
|
||||
let input = r#"
|
||||
use maud::{html, Markup};
|
||||
|
||||
pub fn main() -> Markup {
|
||||
html! {
|
||||
p.text-black[cuteness > 50] { "Squee!" }
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
Rust::test_extract_contains(input, vec!["text-black"]);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
||||
|
|
@ -56,85 +56,6 @@ impl PreProcessor for Slim {
|
|||
}
|
||||
}
|
||||
|
||||
// Handle Ruby syntax with `%w[]` arrays embedded in Slim directly.
|
||||
//
|
||||
// E.g.:
|
||||
//
|
||||
// ```
|
||||
// div [
|
||||
// class=%w[bg-blue-500 w-10 h-10]
|
||||
// ]
|
||||
// ```
|
||||
//
|
||||
// A `%` that follows a value is not a percent literal. E.g.: the `50%w` in
|
||||
// `hit rate 50%w.`
|
||||
b'%' if matches!(cursor.next(), b'w' | b'W')
|
||||
&& !cursor.prev().is_ascii_alphanumeric()
|
||||
&& !matches!(cursor.prev(), b'_' | b')' | b']' | b'}') =>
|
||||
{
|
||||
// Boundary characters
|
||||
let (open, close) = match cursor.input.get(cursor.pos + 2) {
|
||||
Some(b'[') => (b'[', b']'),
|
||||
Some(b'(') => (b'(', b')'),
|
||||
Some(b'{') => (b'{', b'}'),
|
||||
Some(b'<') => (b'<', b'>'),
|
||||
|
||||
// Any other ASCII punctuation can be used as a custom delimiter
|
||||
Some(&c) if c.is_ascii_punctuation() => (c, c),
|
||||
|
||||
// Everything else is not a valid delimiter
|
||||
_ => {
|
||||
cursor.advance();
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
result[cursor.pos] = b' '; // Replace `%`
|
||||
cursor.advance();
|
||||
result[cursor.pos] = b' '; // Replace `w`
|
||||
cursor.advance();
|
||||
result[cursor.pos] = b' '; // Replace the opening delimiter
|
||||
cursor.advance();
|
||||
|
||||
// Paired delimiters can be nested as long as they are balanced. E.g.:
|
||||
// `%w[foo[bar]baz]` produces a flat array.
|
||||
let mut depth = 1_usize;
|
||||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr() {
|
||||
// Skip escaped characters, unless the backslash is the delimiter
|
||||
// itself
|
||||
b'\\' if close != b'\\' => {
|
||||
// Use backslash to embed spaces in the strings.
|
||||
if cursor.next() == b' ' {
|
||||
result[cursor.pos] = b' ';
|
||||
}
|
||||
|
||||
cursor.advance();
|
||||
}
|
||||
|
||||
// Start of a nested delimiter pair
|
||||
c if c == open && open != close => depth += 1,
|
||||
|
||||
// Closing delimiter
|
||||
c if c == close => {
|
||||
depth -= 1;
|
||||
|
||||
// End of the literal, replace the closing delimiter with a space
|
||||
if depth == 0 {
|
||||
result[cursor.pos] = b' ';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Everything else is valid content
|
||||
_ => {}
|
||||
}
|
||||
|
||||
cursor.advance();
|
||||
}
|
||||
}
|
||||
|
||||
// Any `[` preceded by an alphanumeric value will not be part of a candidate.
|
||||
//
|
||||
// E.g.:
|
||||
|
|
@ -153,10 +74,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.:
|
||||
|
|
@ -171,17 +92,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
|
||||
|
|
@ -360,90 +281,4 @@ mod tests {
|
|||
"#;
|
||||
Slim::test_extract_contains(input, vec!["flex", "items-center"]);
|
||||
}
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/17542
|
||||
#[test]
|
||||
fn test_embedded_ruby_percent_w_extraction() {
|
||||
let input = r#"
|
||||
div[
|
||||
class=%w[bg-blue-500 w-10 h-10]
|
||||
]
|
||||
div[
|
||||
class=%w[w-10 bg-green-500 h-10]
|
||||
]
|
||||
"#;
|
||||
|
||||
let expected = "
|
||||
div \n class= bg-blue-500 w-10 h-10 \n ]
|
||||
div \n class= w-10 bg-green-500 h-10 \n ]
|
||||
";
|
||||
|
||||
Slim::test(input, expected);
|
||||
Slim::test_extract_contains(input, vec!["bg-blue-500", "bg-green-500", "w-10", "h-10"]);
|
||||
}
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/20386
|
||||
#[test]
|
||||
fn test_embedded_ruby_percent_w_delimiters() {
|
||||
for (input, expected) in [
|
||||
// %w<…>, Slim only counts `[({` nesting in attribute values, so the code must be
|
||||
// wrapped in parentheses to contain spaces
|
||||
(
|
||||
"div class=(%w<bg-blue-500 w-10 h-10>)",
|
||||
"div class= bg-blue-500 w-10 h-10 )",
|
||||
),
|
||||
// Nested `<…>` does not end the literal
|
||||
(
|
||||
"div class=(%w<flex <nested> px-2.5>)",
|
||||
"div class= flex <nested> px-2.5 )",
|
||||
),
|
||||
// Custom delimiters
|
||||
("div class=(%w|flex px-2.5|)", "div class= flex px-2.5 )"),
|
||||
("div class=(%W!flex px-2.5!)", "div class= flex px-2.5 )"),
|
||||
(
|
||||
"div class=(%w#text-sm leading-6#)",
|
||||
"div class= text-sm leading-6 )",
|
||||
),
|
||||
(
|
||||
"div class=(%w=italic tracking-wide=)",
|
||||
"div class= italic tracking-wide )",
|
||||
),
|
||||
// Nested paired delimiters stay balanced inside the literal
|
||||
(
|
||||
"div class=(%w[content-['[hello]'] p-4])",
|
||||
"div class= content-['[hello]'] p-4 )",
|
||||
),
|
||||
// Escaped spaces embed a space in a single array element
|
||||
(
|
||||
r#"div class=(%w[foo\ bar baz-1])"#,
|
||||
r#"div class= foo bar baz-1 )"#,
|
||||
),
|
||||
// Ruby control line, which is plain Ruby code
|
||||
("- classes = %w<mt-4 flex>", "- classes = mt-4 flex "),
|
||||
// A `%` that follows a value is not a percent literal
|
||||
("| hit rate 50%w.", "| hit rate 50%w "),
|
||||
] {
|
||||
Slim::test(input, expected);
|
||||
}
|
||||
|
||||
let input = r#"
|
||||
div[
|
||||
class=(%w<bg-blue-500 w-10 h-10>)
|
||||
]
|
||||
- classes = %w|w-10 bg-green-500 h-10|
|
||||
= tag.div class: %W!px-2.5 flex!
|
||||
"#;
|
||||
|
||||
Slim::test_extract_contains(
|
||||
input,
|
||||
vec![
|
||||
"bg-blue-500",
|
||||
"bg-green-500",
|
||||
"w-10",
|
||||
"h-10",
|
||||
"px-2.5",
|
||||
"flex",
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,52 +0,0 @@
|
|||
use crate::cursor;
|
||||
use crate::extractor::pre_processors::pre_processor::PreProcessor;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct TemplateToolkit;
|
||||
|
||||
impl PreProcessor for TemplateToolkit {
|
||||
fn process(&self, content: &[u8]) -> Vec<u8> {
|
||||
let len = content.len();
|
||||
let mut result = content.to_vec();
|
||||
let mut cursor = cursor::Cursor::new(content);
|
||||
|
||||
while cursor.pos < len {
|
||||
match (cursor.curr(), cursor.next()) {
|
||||
(b'[', b'%') => result[cursor.pos] = b' ',
|
||||
(b'%', b']') => result[cursor.pos + 1] = b' ',
|
||||
_ => {}
|
||||
}
|
||||
|
||||
cursor.advance();
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::TemplateToolkit;
|
||||
use crate::extractor::pre_processors::pre_processor::PreProcessor;
|
||||
|
||||
#[test]
|
||||
fn test_template_toolkit_pre_processor() {
|
||||
for (input, expected) in [
|
||||
(
|
||||
"[% IF $is_open %]bg-white/40[% ELSE %]bg-white/10[% END %]",
|
||||
" % IF $is_open % bg-white/40 % ELSE % bg-white/10 % END % ",
|
||||
),
|
||||
("[% WRAPPER %]flex[% END %]", " % WRAPPER % flex % END % "),
|
||||
] {
|
||||
TemplateToolkit::test(input, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extraction_between_template_tags_works() {
|
||||
TemplateToolkit::test_extract_contains(
|
||||
r#"<div class="[% IF $is_open %]bg-white/40[% ELSE %]bg-white/10[% END %]"></div>"#,
|
||||
vec!["bg-white/40", "bg-white/10"],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
/ https://github com/tailwindlabs/tailwindcss/pull/17051 issuecomment-2711181352
|
||||
- star_styles "size- 800px mask mask-star bg-gradient-to-r from-secondary via-cyan-400 to-lime-400"
|
||||
- crazy_text_styles "italic font-black bg-gradient-to-r via-60% to-90% from-orange-500 via-secondary to-primary text-transparent bg-clip-text inline-block py-2"
|
||||
|
||||
relative
|
||||
- Blurred background star
|
||||
absolute left-0 z-0 class: "-top-[400px] -right-[400px]"
|
||||
flex justify-end blur-3xl
|
||||
%div class: star_styles
|
||||
|
||||
relative z-10
|
||||
%h1 mt-8 text-center text-5xl font-black tracking-wide drop-shadow-lg
|
||||
%div
|
||||
Components, Guides, and Paradigms
|
||||
md:inline-flex md:items-center justify-center
|
||||
%span -mr-2
|
||||
for
|
||||
%span relative -rotate-3 hover:-rotate-6 hover:scale-125 transition-transform
|
||||
%span class: crazy_text_styles + " absolute blur-xl"
|
||||
CRAZY
|
||||
%span class: crazy_text_styles + " drop-shadow-[0_0_1px_#fff]"
|
||||
CRAZY
|
||||
%span -ml-3 md:-ml-1
|
||||
\-fast Development
|
||||
%div
|
||||
in Ruby on Rails!
|
||||
|
||||
mt-16 text-center
|
||||
daisy_button "😃 Get Started", css: "btn-primary text-xl",
|
||||
right_icon: "arrow-right", target: "_blank",
|
||||
href: "https://github com/profoundry-us/loco_motion locomotion-components"
|
||||
|
||||
mt-32 lg:flex lg:space-x-8
|
||||
%div class: "md:basis-2/5"
|
||||
%h3 text-4xl font-bold
|
||||
Easy, Flexible Components
|
||||
%p mt-2 text-xl
|
||||
Powered by the fabulous
|
||||
succeed ',' do
|
||||
daisy_link "ViewComponent", "https://viewcomponent.org", target: "_blank"
|
||||
succeed ', and' do
|
||||
daisy_link "DaisyUI", "https://daisyui com/", target: "_blank"
|
||||
daisy_link "TailwindCSS", "https://tailwindcss com/", target: "_blank"
|
||||
libraries, our components are designed to be fast, flexible, and easy to
|
||||
use <i>directy</i> in Ruby on Rails!
|
||||
|
||||
doc_example css: "mt-8 lg:mt-0 lg:basis-3/5 h-44" do
|
||||
flex flex-col sm:flex-row items-center gap-4
|
||||
daisy_button "Accent Button", css: "btn-accent"
|
||||
daisy_tip "Click to Swap" do
|
||||
daisy_swap off: "🌚", on: "🌞", css: "swap-rotate text-4xl"
|
||||
daisy_badge "Large Badge", css: "badge-secondary badge-lg"
|
||||
|
||||
|
||||
mt-32 xl:flex xl:flex-row-reverse xl:items-center
|
||||
text-xl xl:ml-8
|
||||
%h3 text-4xl font-bold
|
||||
Simple, Concise Views
|
||||
|
||||
%p mt-2
|
||||
Utlize
|
||||
daisy_link "HAML", "https://haml.info/", target: "_blank"
|
||||
so your views are simple, concise, and easy to understand
|
||||
|
||||
%p mt-2
|
||||
No more messy ERB files with all of their closing tags and Ruby wrappers
|
||||
HAML feels more natural to write and reduces file sizes, making your
|
||||
views easier to read and maintain
|
||||
|
||||
%p mt-2
|
||||
:markdown
|
||||
**PLUS!** You can utlize filters like _Markdown_, CoffeeScript,
|
||||
Textile, and many more!
|
||||
|
||||
mt-8 xl:mt-0
|
||||
flex flex-col xl:flex xl:flex-row w-full
|
||||
doc_code css: "grow", code_css: "xl:pl-2 xl:pr-0 xl:rounded-r-none", language: "erb" do
|
||||
:escaped
|
||||
<% Ruby %>
|
||||
|
||||
<% 5 times do |i| %>
|
||||
<% if i even? %>
|
||||
<p class "odd">Number <% i %></p>
|
||||
<% else %>
|
||||
<p class "even">Numero <% i %></p>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
doc_code css: "grow mt-8 xl:mt-0", pre_css: "xl:h-full", code_css: "xl:pl-0 xl:pr-2 xl:h-full xl:rounded-l-none" do
|
||||
:escaped
|
||||
- HAML
|
||||
|
||||
- 5 times do |i|
|
||||
- if i even?
|
||||
%p odd Number \ i
|
||||
- else
|
||||
%p even Numero \ i
|
||||
|
||||
|
||||
%h3 mt-32 text-4xl font-bold
|
||||
Build Your <i>OWN</i> Components
|
||||
|
||||
%p mt-2 text-xl
|
||||
Can't find exactly what you need? No problem! Build your own components
|
||||
with ease using our simple, flexible, and powerful DSL
|
||||
|
||||
mt-8 xl:flex xl:space-x-8
|
||||
%div
|
||||
doc_code language: "ruby" do
|
||||
:escaped
|
||||
app/components/application_component rb
|
||||
class ApplicationComponent < LocoMotion::BaseComponent
|
||||
Add your custom / shared component logic here!
|
||||
end
|
||||
|
||||
doc_code language: "haml", css: "mt-8" do
|
||||
:escaped
|
||||
- app/components/character_component html haml
|
||||
part :component do
|
||||
part :head
|
||||
part :body do
|
||||
content
|
||||
part :legs
|
||||
|
||||
%div mt-8 xl:mt-0
|
||||
doc_code language: "ruby" do
|
||||
:escaped
|
||||
app/components/character_component rb
|
||||
class CharacterComponent < ApplicationComponent
|
||||
define_parts :head, :body, :legs
|
||||
|
||||
def before_render
|
||||
set_tag_name :head, :h1
|
||||
add_css :head, "text-3xl font-bold"
|
||||
|
||||
set_tag_name :body, :p
|
||||
add_stimulus_controller :body, "character-body"
|
||||
add_css :body, "text-lg"
|
||||
|
||||
set_tag_name :legs, :footer
|
||||
add_css :legs, "text-sm"
|
||||
end
|
||||
end
|
||||
|
||||
%h3 mt-24 text-center text-4xl font-bold italic
|
||||
More Coming Soon!
|
||||
%p mt-2 text-xl text-center
|
||||
Keen an eye out as we'll be adding more components, guides, and
|
||||
%br max-sm:hidden
|
||||
suggested gems for you to build amazing Rails apps!
|
||||
mt-4 text-center
|
||||
daisy_button "😉 Get Started", css: "btn-primary text-xl",
|
||||
right_icon: "arrow-right", target: "_blank",
|
||||
class: "px-2.5"
|
||||
href: "https://github com/profoundry-us/loco_motion locomotion-components"
|
||||
|
|
@ -1,262 +0,0 @@
|
|||
/ https://github.com/tailwindlabs/tailwindcss/pull/17051#issuecomment-2711181352
|
||||
- star_styles = "size-[800px] mask mask-star bg-gradient-to-r from-secondary via-cyan-400 to-lime-400"
|
||||
^^^^^^^^^^^ ^^^^^^^^^^^^ ^^^^ ^^^^^^^^^ ^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^ ^^^^^^^^^^^^ ^^^^^^^^^^^
|
||||
- crazy_text_styles = "italic font-black bg-gradient-to-r via-60% to-90% from-orange-500 via-secondary to-primary text-transparent bg-clip-text inline-block py-2"
|
||||
^^^^^^^^^^^^^^^^^ ^^^^^^ ^^^^^^^^^^ ^^^^^^^^^^^^^^^^ ^^^^^^^ ^^^^^^ ^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^ ^^^^^^^^^^ ^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^ ^^^^^^^^^^^^ ^^^^
|
||||
|
||||
.relative
|
||||
^^^^^^^^
|
||||
- # Blurred background star
|
||||
.absolute.left-0.z-0{ class: "-top-[400px] -right-[400px]" }
|
||||
^^^^^^^^ ^^^^^^ ^^^ ^^^^^ ^^^^^^^^^^^^ ^^^^^^^^^^^^^^
|
||||
.flex.justify-end.blur-3xl
|
||||
^^^^ ^^^^^^^^^^^ ^^^^^^^^
|
||||
%div{ class: star_styles }
|
||||
^^^^^ ^^^^^^^^^^^
|
||||
|
||||
.relative.z-10
|
||||
^^^^^^^^ ^^^^
|
||||
%h1.mt-8.text-center.text-5xl.font-black.tracking-wide.drop-shadow-lg
|
||||
^^^^ ^^^^^^^^^^^ ^^^^^^^^ ^^^^^^^^^^ ^^^^^^^^^^^^^ ^^^^^^^^^^^^^^
|
||||
%div
|
||||
Components, Guides, and Paradigms
|
||||
^^^
|
||||
.md:inline-flex.md:items-center.justify-center
|
||||
^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^
|
||||
%span.-mr-2
|
||||
^^^^^
|
||||
for
|
||||
^^^
|
||||
%span.relative.-rotate-3.hover:-rotate-6.hover:scale-125.transition-transform
|
||||
^^^^^^^^ ^^^^^^^^^ ^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^
|
||||
%span{ class: crazy_text_styles + " absolute blur-xl" }
|
||||
^^^^^ ^^^^^^^^^^^^^^^^^ ^^^^^^^^ ^^^^^^^
|
||||
CRAZY
|
||||
%span{ class: crazy_text_styles + " drop-shadow-[0_0_1px_#fff]" }
|
||||
^^^^^ ^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
CRAZY
|
||||
%span.-ml-3.md:-ml-1
|
||||
^^^^^ ^^^^^^^^
|
||||
\-fast Development
|
||||
%div
|
||||
in Ruby on Rails!
|
||||
^^ ^^
|
||||
|
||||
.mt-16.text-center
|
||||
^^^^^ ^^^^^^^^^^^
|
||||
= daisy_button "😃 Get Started", css: "btn-primary text-xl",
|
||||
^^^^^^^^^^^^ ^^^ ^^^^^^^^^^^ ^^^^^^^
|
||||
right_icon: "arrow-right", target: "_blank",
|
||||
^^^^^^^^^^ ^^^^^^^^^^^ ^^^^^^
|
||||
href: "https://github.com/profoundry-us/loco_motion#locomotion-components"
|
||||
^^^^ ^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.mt-32.lg:flex.lg:space-x-8
|
||||
^^^^^ ^^^^^^^ ^^^^^^^^^^^^
|
||||
%div{ class: "md:basis-2/5" }
|
||||
^^^^^ ^^^^^^^^^^^^
|
||||
%h3.text-4xl.font-bold
|
||||
^^^^^^^^ ^^^^^^^^^
|
||||
Easy, Flexible Components
|
||||
%p.mt-2.text-xl
|
||||
^^^^ ^^^^^^^
|
||||
Powered by the fabulous
|
||||
^^ ^^^ ^^^^^^^^
|
||||
= succeed ',' do
|
||||
^^^^^^^ ^^
|
||||
= daisy_link("ViewComponent", "https://viewcomponent.org", target: "_blank")
|
||||
^^^ ^^^^^^
|
||||
= succeed ', and' do
|
||||
^^^^^^^ ^^^ ^^
|
||||
= daisy_link "DaisyUI", "https://daisyui.com/", target: "_blank"
|
||||
^^^^^^^^^^ ^^^^^^
|
||||
= daisy_link "TailwindCSS", "https://tailwindcss.com/", target: "_blank"
|
||||
^^^^^^^^^^ ^^^^^^
|
||||
libraries, our components are designed to be fast, flexible, and easy to
|
||||
^^^ ^^^^^^^^^^ ^^^ ^^^^^^^^ ^^ ^^ ^^^ ^^^^ ^^
|
||||
use <i>directly</i> in Ruby on Rails!
|
||||
^^^ ^^^^^^^^ ^^ ^^
|
||||
|
||||
= doc_example(css: "mt-8 lg:mt-0 lg:basis-3/5 h-44") do
|
||||
^^^ ^^^^ ^^^^^^^ ^^^^^^^^^^^^ ^^^^ ^^
|
||||
.flex.flex-col.sm:flex-row.items-center.gap-4
|
||||
^^^^ ^^^^^^^^ ^^^^^^^^^^^ ^^^^^^^^^^^^ ^^^^^
|
||||
= daisy_button "Accent Button", css: "btn-accent"
|
||||
^^^^^^^^^^^^ ^^^ ^^^^^^^^^^
|
||||
= daisy_tip("Click to Swap") do
|
||||
^^ ^^
|
||||
= daisy_swap off: "🌚", on: "🌞", css: "swap-rotate text-4xl"
|
||||
^^^^^^^^^^ ^^^ ^^ ^^^ ^^^^^^^^^^^ ^^^^^^^^
|
||||
= daisy_badge "Large Badge", css: "badge-secondary badge-lg"
|
||||
^^^^^^^^^^^ ^^^ ^^^^^^^^^^^^^^^ ^^^^^^^^
|
||||
|
||||
|
||||
.mt-32.xl:flex.xl:flex-row-reverse.xl:items-center
|
||||
^^^^^ ^^^^^^^ ^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^
|
||||
.text-xl.xl:ml-8
|
||||
^^^^^^^ ^^^^^^^
|
||||
%h3.text-4xl.font-bold
|
||||
^^^^^^^^ ^^^^^^^^^
|
||||
Simple, Concise Views
|
||||
|
||||
%p.mt-2
|
||||
^^^^
|
||||
Utilize
|
||||
= daisy_link("HAML", "https://haml.info/", target: "_blank")
|
||||
^^^^^^
|
||||
so your views are simple, concise, and easy to understand.
|
||||
^^ ^^^^ ^^^^^ ^^^ ^^^ ^^^^ ^^ ^^^^^^^^^^
|
||||
|
||||
%p.mt-2
|
||||
^^^^
|
||||
No more messy ERB files with all of their closing tags and Ruby wrappers.
|
||||
^^^^ ^^^^^ ^^^^^ ^^^^ ^^^ ^^ ^^^^^ ^^^^^^^ ^^^^ ^^^ ^^^^^^^^
|
||||
HAML feels more natural to write and reduces file sizes, making your
|
||||
^^^^^ ^^^^ ^^^^^^^ ^^ ^^^^^ ^^^ ^^^^^^^ ^^^^ ^^^^^^ ^^^^
|
||||
views easier to read and maintain.
|
||||
^^^^^ ^^^^^^ ^^ ^^^^ ^^^ ^^^^^^^^
|
||||
|
||||
%p.mt-2
|
||||
^^^^
|
||||
:markdown
|
||||
**PLUS!** You can utilize filters like _Markdown_, CoffeeScript,
|
||||
^^^ ^^^^^^^ ^^^^^^^ ^^^^
|
||||
Textile, and many more!
|
||||
^^^ ^^^^ ^^^^^
|
||||
|
||||
.mt-8.xl:mt-0
|
||||
^^^^ ^^^^^^^
|
||||
.flex.flex-col.xl:flex.xl:flex-row.w-full
|
||||
^^^^ ^^^^^^^^ ^^^^^^^ ^^^^^^^^^^^ ^^^^^^
|
||||
= doc_code(css: "grow", code_css: "xl:pl-2 xl:pr-0 xl:rounded-r-none", language: "erb") do
|
||||
^^^ ^^^^ ^^^^^^^^ ^^^^^^^ ^^^^^^^ ^^^^^^^^^^^^^^^^^ ^^^^^^^^ ^^^ ^^
|
||||
:escaped
|
||||
<% # Ruby %>
|
||||
|
||||
<% 5.times do |i| %>
|
||||
^^^^^ ^^
|
||||
<% if i.even? %>
|
||||
^^ ^
|
||||
<p class="odd">Number <%= i %></p>
|
||||
^^^^^ ^^^ ^
|
||||
<% else %>
|
||||
^^^^
|
||||
<p class="even">Numero <%= i %></p>
|
||||
^^^^^ ^^^^ ^
|
||||
<% end %>
|
||||
^^^
|
||||
<% end %>
|
||||
^^^
|
||||
|
||||
= doc_code(css: "grow mt-8 xl:mt-0", pre_css: "xl:h-full", code_css: "xl:pl-0 xl:pr-2 xl:h-full xl:rounded-l-none") do
|
||||
^^^ ^^^^ ^^^^ ^^^^^^^ ^^^^^^^ ^^^^^^^^^ ^^^^^^^^ ^^^^^^^ ^^^^^^^ ^^^^^^^^^ ^^^^^^^^^^^^^^^^^ ^^
|
||||
:escaped
|
||||
- # HAML
|
||||
|
||||
- 5.times do |i|
|
||||
^^^^^ ^^
|
||||
- if i.even?
|
||||
^^
|
||||
%p.odd Number \#{i}
|
||||
^^^ ^
|
||||
- else
|
||||
^^^^
|
||||
%p.even Numero \#{i}
|
||||
^^^^ ^
|
||||
|
||||
|
||||
%h3.mt-32.text-4xl.font-bold
|
||||
^^^^^ ^^^^^^^^ ^^^^^^^^^
|
||||
Build Your <i>OWN</i> Components
|
||||
|
||||
%p.mt-2.text-xl
|
||||
^^^^ ^^^^^^^
|
||||
Can't find exactly what you need? No problem! Build your own components
|
||||
^ ^^^^ ^^^^^^^ ^^^^ ^^^ ^^^^^^^^ ^^^^ ^^^ ^^^^^^^^^^
|
||||
with ease using our simple, flexible, and powerful DSL.
|
||||
^^^^ ^^^^ ^^^^^ ^^^ ^^^ ^^^^^^^^
|
||||
|
||||
.mt-8.xl:flex.xl:space-x-8
|
||||
^^^^ ^^^^^^^ ^^^^^^^^^^^^
|
||||
%div
|
||||
= doc_code(language: "ruby") do
|
||||
^^^^^^^^ ^^^^ ^^
|
||||
:escaped
|
||||
# app/components/application_component.rb
|
||||
^^
|
||||
class ApplicationComponent < LocoMotion::BaseComponent
|
||||
^^^^^
|
||||
# Add your custom / shared component logic here!
|
||||
^^^^ ^^^^^^ ^^^^^^ ^^^^^^^^^ ^^^^^ ^^^^^
|
||||
end
|
||||
^^^
|
||||
|
||||
= doc_code(language: "haml", css: "mt-8") do
|
||||
^^^^^^^^ ^^^^ ^^^ ^^^^ ^^
|
||||
:escaped
|
||||
- # app/components/character_component.html.haml
|
||||
= part(:component) do
|
||||
^^
|
||||
= part(:head)
|
||||
= part(:body) do
|
||||
^^
|
||||
= content
|
||||
^^^^^^^
|
||||
= part(:legs)
|
||||
|
||||
%div.mt-8.xl:mt-0
|
||||
^^^^ ^^^^^^^
|
||||
= doc_code(language: "ruby") do
|
||||
^^^^^^^^ ^^^^ ^^
|
||||
:escaped
|
||||
# app/components/character_component.rb
|
||||
^^
|
||||
class CharacterComponent < ApplicationComponent
|
||||
^^^^^
|
||||
define_parts :head, :body, :legs
|
||||
^^^^^^^^^^^^
|
||||
|
||||
def before_render
|
||||
^^^ ^^^^^^^^^^^^^
|
||||
set_tag_name(:head, :h1)
|
||||
^^^^^^^^^^^^
|
||||
add_css(:head, "text-3xl font-bold")
|
||||
^^^^^^^ ^^^^^^^^ ^^^^^^^^^
|
||||
|
||||
set_tag_name(:body, :p)
|
||||
^^^^^^^^^^^^
|
||||
add_stimulus_controller(:body, "character-body")
|
||||
^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^
|
||||
add_css(:body, "text-lg")
|
||||
^^^^^^^ ^^^^^^^
|
||||
|
||||
set_tag_name(:legs, :footer)
|
||||
^^^^^^^^^^^^
|
||||
add_css(:legs, "text-sm")
|
||||
^^^^^^^ ^^^^^^^
|
||||
end
|
||||
^^^
|
||||
end
|
||||
^^^
|
||||
|
||||
%h3.mt-24.text-center.text-4xl.font-bold.italic
|
||||
^^^^^ ^^^^^^^^^^^ ^^^^^^^^ ^^^^^^^^^ ^^^^^^
|
||||
More Coming Soon!
|
||||
%p.mt-2.text-xl.text-center
|
||||
^^^^ ^^^^^^^ ^^^^^^^^^^^
|
||||
Keen an eye out as we'll be adding more components, guides, and
|
||||
^^ ^^^ ^^^ ^^ ^^ ^^ ^^ ^^^^^^ ^^^^ ^^^
|
||||
%br.max-sm:hidden
|
||||
^^^^^^^^^^^^^
|
||||
suggested gems for you to build amazing Rails apps!
|
||||
^^^^^^^^^ ^^^^ ^^^ ^^^ ^^ ^^^^^ ^^^^^^^ ^^^^^
|
||||
.mt-4.text-center
|
||||
^^^^ ^^^^^^^^^^^
|
||||
= daisy_button "😉 Get Started", css: "btn-primary text-xl",
|
||||
^^^^^^^^^^^^ ^^^ ^^^^^^^^^^^ ^^^^^^^
|
||||
right_icon: "arrow-right", target: "_blank",
|
||||
^^^^^^^^^^ ^^^^^^^^^^^ ^^^^^^
|
||||
class: "px-2.5"
|
||||
^^^^^ ^^^^^^
|
||||
href: "https://github.com/profoundry-us/loco_motion#locomotion-components"
|
||||
^^^^ ^^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
:ruby
|
||||
- devices_classes = 'size-5 mr-2px'
|
||||
^^^^^^^^^^^^^^^ ^^^^^^ ^^^^^^
|
||||
- icon_classes = 'w-[12px] h-[12px]'
|
||||
^^^^^^^^^^^^ ^^^^^^^^ ^^^^^^^^
|
||||
!!!
|
||||
%html{ lang: 'en' }
|
||||
^^^^ ^^
|
||||
%head
|
||||
%title Tailwind v4.1.4 + HAML bug
|
||||
^^^^^^ ^^^
|
||||
|
||||
-# This is a comment
|
||||
^^ ^ ^^^^^^^
|
||||
|
||||
A multi-line comment
|
||||
^^^^^^^^^^ ^^^^^^^
|
||||
With more indentation
|
||||
^^^^ ^^^^^^^^^^^
|
||||
|
||||
Which can dedent again just fine
|
||||
^^^ ^^^^^^ ^^^^^ ^^^^ ^^^^
|
||||
|
||||
%body
|
||||
- icon_classes = 'self-center w-[16px] h-[16px]'
|
||||
^^^^^^^^^^^^ ^^^^^^^^^^^ ^^^^^^^^ ^^^^^^^^
|
||||
.flex{ class: icon_classes }
|
||||
^^^^ ^^^^^ ^^^^^^^^^^^^
|
||||
.flex{ class: devices_classes }
|
||||
^^^^ ^^^^^ ^^^^^^^^^^^^^^^
|
||||
|
|
@ -42,7 +42,7 @@
|
|||
= daisy_link "DaisyUI", "https://daisyui.com/", target: "_blank"
|
||||
= daisy_link "TailwindCSS", "https://tailwindcss.com/", target: "_blank"
|
||||
libraries, our components are designed to be fast, flexible, and easy to
|
||||
use <i>directly</i> in Ruby on Rails!
|
||||
use <i>directy</i> in Ruby on Rails!
|
||||
|
||||
= doc_example(css: "mt-8 lg:mt-0 lg:basis-3/5 h-44") do
|
||||
.flex.flex-col.sm:flex-row.items-center.gap-4
|
||||
|
|
@ -58,7 +58,7 @@
|
|||
Simple, Concise Views
|
||||
|
||||
%p.mt-2
|
||||
Utilize
|
||||
Utlize
|
||||
= daisy_link("HAML", "https://haml.info/", target: "_blank")
|
||||
so your views are simple, concise, and easy to understand.
|
||||
|
||||
|
|
@ -69,7 +69,7 @@
|
|||
|
||||
%p.mt-2
|
||||
:markdown
|
||||
**PLUS!** You can utilize filters like _Markdown_, CoffeeScript,
|
||||
**PLUS!** You can utlize filters like _Markdown_, CoffeeScript,
|
||||
Textile, and many more!
|
||||
|
||||
.mt-8.xl:mt-0
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
:ruby
|
||||
- devices_classes = 'size-5 mr-2px'
|
||||
- icon_classes = 'w-[12px] h-[12px]'
|
||||
!!!
|
||||
%html{ lang: 'en' }
|
||||
%head
|
||||
%title Tailwind v4.1.4 + HAML bug
|
||||
|
||||
-# This is a comment
|
||||
|
||||
A multi-line comment
|
||||
With more indentation
|
||||
|
||||
Which can dedent again just fine
|
||||
|
||||
%body
|
||||
- icon_classes = 'self-center w-[16px] h-[16px]'
|
||||
.flex{ class: icon_classes }
|
||||
.flex{ class: devices_classes }
|
||||
|
|
@ -1,179 +0,0 @@
|
|||
use crate::cursor;
|
||||
use crate::extractor::pre_processors::pre_processor::PreProcessor;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Twig;
|
||||
|
||||
impl PreProcessor for Twig {
|
||||
fn process(&self, content: &[u8]) -> Vec<u8> {
|
||||
let len = content.len();
|
||||
let mut result = content.to_vec();
|
||||
let mut cursor = cursor::Cursor::new(content);
|
||||
let mut bracket_stack = vec![];
|
||||
|
||||
const ADD_CLASS: &[u8] = b"addClass";
|
||||
const REMOVE_CLASS: &[u8] = b"removeClass";
|
||||
|
||||
while cursor.pos < len {
|
||||
match (!bracket_stack.is_empty(), cursor.curr()) {
|
||||
// addClass(
|
||||
// ^
|
||||
(false, b'(')
|
||||
if cursor.pos >= ADD_CLASS.len()
|
||||
&& matches!(
|
||||
&content[cursor.pos - ADD_CLASS.len()..cursor.pos],
|
||||
ADD_CLASS
|
||||
) =>
|
||||
{
|
||||
bracket_stack.push(cursor.curr());
|
||||
result[cursor.pos] = b' ';
|
||||
}
|
||||
|
||||
// removeClass(
|
||||
// ^
|
||||
(false, b'(')
|
||||
if cursor.pos >= REMOVE_CLASS.len()
|
||||
&& matches!(
|
||||
&content[cursor.pos - REMOVE_CLASS.len()..cursor.pos],
|
||||
REMOVE_CLASS
|
||||
) =>
|
||||
{
|
||||
bracket_stack.push(cursor.curr());
|
||||
result[cursor.pos] = b' ';
|
||||
}
|
||||
|
||||
(true, b'(') => {
|
||||
bracket_stack.push(cursor.curr());
|
||||
}
|
||||
|
||||
(true, b')') => {
|
||||
bracket_stack.pop();
|
||||
|
||||
if bracket_stack.is_empty() {
|
||||
result[cursor.pos] = b' ';
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
|
||||
cursor.advance();
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Twig;
|
||||
use crate::extractor::pre_processors::pre_processor::PreProcessor;
|
||||
|
||||
#[test]
|
||||
fn test_twig_pre_processor() {
|
||||
for (input, expected) in [
|
||||
(
|
||||
// Ensure we don't crash when we encounter an `(`
|
||||
"(p-(--value))",
|
||||
"(p-(--value))",
|
||||
),
|
||||
(
|
||||
// addClass with single argument
|
||||
"addClass(p-(--value))",
|
||||
"addClass p-(--value) ",
|
||||
),
|
||||
(
|
||||
// addClass with single argument
|
||||
"addClass(m-4 p-8 w-full)",
|
||||
"addClass m-4 p-8 w-full ",
|
||||
),
|
||||
(
|
||||
// removeClass with single argument
|
||||
"removeClass(p-(--value))",
|
||||
"removeClass p-(--value) ",
|
||||
),
|
||||
(
|
||||
// removeClass with single argument
|
||||
"removeClass(m-4 p-8 w-full)",
|
||||
"removeClass m-4 p-8 w-full ",
|
||||
),
|
||||
(
|
||||
// Combined with single arguments
|
||||
"addClass(m-(--value))|removeClass(p-(--value))",
|
||||
"addClass m-(--value) |removeClass p-(--value) ",
|
||||
),
|
||||
] {
|
||||
Twig::test(input, expected);
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/19458
|
||||
#[test]
|
||||
fn test_extraction_in_add_class_and_remove_class_works() {
|
||||
// Inside normal HTML
|
||||
let input = r#"
|
||||
<div data-loading="addClass(opacity-50)">
|
||||
<!-- -->
|
||||
</div>
|
||||
"#;
|
||||
|
||||
let expected = r#"
|
||||
<div data-loading="addClass opacity-50 ">
|
||||
<!-- -->
|
||||
</div>
|
||||
"#;
|
||||
|
||||
Twig::test(input, expected);
|
||||
Twig::test_extract_contains(input, vec!["opacity-50"]);
|
||||
|
||||
// Inside a component
|
||||
let input = r#"
|
||||
<twig:d:Card
|
||||
{{ ...attributes.defaults({
|
||||
as: "a",
|
||||
class: "cursor-pointer bg-base-200 card-sm md:card-md lg:card-lg",
|
||||
role: "button",
|
||||
tabindex: "0",
|
||||
"data-action": "live#$render",
|
||||
"data-loading": "addAttribute(disabled)",
|
||||
"data-poll": "delay(60000)|$render",
|
||||
"data-loading": "addClass(border border-red-500 opacity-75)",
|
||||
})
|
||||
}}
|
||||
>
|
||||
"#;
|
||||
|
||||
let expected = r#"
|
||||
<twig:d:Card
|
||||
{{ ...attributes.defaults({
|
||||
as: "a",
|
||||
class: "cursor-pointer bg-base-200 card-sm md:card-md lg:card-lg",
|
||||
role: "button",
|
||||
tabindex: "0",
|
||||
"data-action": "live#$render",
|
||||
"data-loading": "addAttribute(disabled)",
|
||||
"data-poll": "delay(60000)|$render",
|
||||
"data-loading": "addClass border border-red-500 opacity-75 ",
|
||||
})
|
||||
}}
|
||||
>
|
||||
"#;
|
||||
|
||||
Twig::test(input, expected);
|
||||
Twig::test_extract_contains(
|
||||
input,
|
||||
vec![
|
||||
// class
|
||||
"cursor-pointer",
|
||||
"bg-base-200",
|
||||
"card-sm",
|
||||
"md:card-md",
|
||||
"lg:card-lg",
|
||||
// data-loading
|
||||
"border",
|
||||
"border-red-500",
|
||||
"opacity-75",
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
use crate::extractor::pre_processors::pre_processor::PreProcessor;
|
||||
use crate::scanner::pre_process_input;
|
||||
use crate::pre_process_input;
|
||||
use bstr::ByteSlice;
|
||||
use regex::Regex;
|
||||
use std::sync;
|
||||
|
|
@ -15,15 +15,13 @@ impl PreProcessor for Vue {
|
|||
fn process(&self, content: &[u8]) -> Vec<u8> {
|
||||
let mut result = content.to_vec();
|
||||
|
||||
// Only process template tags if content is valid UTF-8
|
||||
if let Ok(content_as_str) = std::str::from_utf8(content) {
|
||||
for (_, [lang, body]) in TEMPLATE_REGEX
|
||||
.captures_iter(content_as_str)
|
||||
.map(|c| c.extract())
|
||||
{
|
||||
let replaced = pre_process_input(body.as_bytes().to_vec(), lang);
|
||||
result = result.replace(body, replaced);
|
||||
}
|
||||
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);
|
||||
result = result.replace(body, replaced);
|
||||
}
|
||||
|
||||
result
|
||||
|
|
@ -45,16 +43,4 @@ mod tests {
|
|||
|
||||
Vue::test_extract_contains(input, vec!["bg-neutral-900", "text-red-500"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_utf8_does_not_panic() {
|
||||
// Invalid UTF-8 sequence: 0x80 is a continuation byte without a leading byte
|
||||
let invalid_utf8: &[u8] = &[0x80, 0x81, 0x82];
|
||||
|
||||
let processor = Vue::default();
|
||||
|
||||
// Should not panic, just return the input unchanged
|
||||
let result = processor.process(invalid_utf8);
|
||||
assert_eq!(result, invalid_utf8);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -87,7 +87,6 @@ enum Class {
|
|||
mod tests {
|
||||
use super::StringMachine;
|
||||
use crate::extractor::machine::Machine;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
||||
|
|
@ -192,7 +192,6 @@ enum Class {
|
|||
mod tests {
|
||||
use super::UtilityMachine;
|
||||
use crate::extractor::machine::Machine;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
|
|
@ -337,7 +336,7 @@ mod tests {
|
|||
actual.sort();
|
||||
|
||||
if actual != expected {
|
||||
dbg!(&input);
|
||||
dbg!(&input, &expected, &actual);
|
||||
}
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]:`
|
||||
|
|
@ -80,7 +80,6 @@ enum Class {
|
|||
mod tests {
|
||||
use super::VariantMachine;
|
||||
use crate::extractor::machine::Machine;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ pub fn fast_skip(cursor: &Cursor) -> Option<usize> {
|
|||
return None;
|
||||
}
|
||||
|
||||
if !cursor.curr().is_ascii_whitespace() {
|
||||
if !cursor.curr.is_ascii_whitespace() {
|
||||
return None;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,17 +1,11 @@
|
|||
use fast_glob::glob_match;
|
||||
use fxhash::{FxHashMap, FxHashSet};
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tracing::event;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct GlobEntry {
|
||||
/// Base path of the glob
|
||||
pub base: String,
|
||||
use crate::GlobEntry;
|
||||
|
||||
/// Glob pattern
|
||||
pub pattern: String,
|
||||
}
|
||||
|
||||
pub fn hoist_static_glob_parts(entries: &Vec<GlobEntry>, emit_parent_glob: bool) -> Vec<GlobEntry> {
|
||||
pub fn hoist_static_glob_parts(entries: &Vec<GlobEntry>) -> Vec<GlobEntry> {
|
||||
let mut result = vec![];
|
||||
|
||||
for entry in entries {
|
||||
|
|
@ -46,7 +40,7 @@ pub fn hoist_static_glob_parts(entries: &Vec<GlobEntry>, emit_parent_glob: bool)
|
|||
// If the base path is a file, then we want to move the file to the pattern, and point the
|
||||
// directory to the base. This is necessary for file watchers that can only listen to
|
||||
// folders.
|
||||
if emit_parent_glob && pattern.is_empty() && base.is_file() {
|
||||
if pattern.is_empty() && base.is_file() {
|
||||
result.push(GlobEntry {
|
||||
// SAFETY: `parent()` will be available because we verify `base` is a file, thus a
|
||||
// parent folder exists.
|
||||
|
|
@ -89,7 +83,7 @@ pub fn hoist_static_glob_parts(entries: &Vec<GlobEntry>, emit_parent_glob: bool)
|
|||
/// tailwind --pwd ./project/components --content "**/*.js"
|
||||
/// ```
|
||||
pub fn optimize_patterns(entries: &Vec<GlobEntry>) -> Vec<GlobEntry> {
|
||||
let entries = hoist_static_glob_parts(entries, true);
|
||||
let entries = hoist_static_glob_parts(entries);
|
||||
|
||||
// Track all base paths and their patterns. Later we will turn them back into `GlobalEntry`s.
|
||||
let mut pattern_map: FxHashMap<String, FxHashSet<String>> = FxHashMap::default();
|
||||
|
|
@ -138,23 +132,11 @@ pub fn optimize_patterns(entries: &Vec<GlobEntry>) -> Vec<GlobEntry> {
|
|||
// using `*`.
|
||||
//
|
||||
// E.g.:
|
||||
//
|
||||
// Input:
|
||||
// - `../project-b/**/*.html`
|
||||
// - `../project-b/**/*.js`
|
||||
//
|
||||
// Split results in:
|
||||
// - `("../project-b", "**/*.html")`
|
||||
// - `("../project-b", "**/*.js")`
|
||||
//
|
||||
// A static file glob should also be considered as a dynamic part.
|
||||
//
|
||||
// E.g.:
|
||||
//
|
||||
// Input: `../project-b/foo/bar.html`
|
||||
// Split results in: `("../project-b/foo", "bar.html")`
|
||||
//
|
||||
pub fn split_pattern(pattern: &str) -> (Option<String>, Option<String>) {
|
||||
// Original input: `../project-b/**/*.{html,js}`
|
||||
// Expanded input: `../project-b/**/*.html` & `../project-b/**/*.js`
|
||||
// Split on first input: ("../project-b", "**/*.html")
|
||||
// Split on second input: ("../project-b", "**/*.js")
|
||||
fn split_pattern(pattern: &str) -> (Option<String>, Option<String>) {
|
||||
// No dynamic parts, so we can just return the input as-is.
|
||||
if !pattern.contains('*') {
|
||||
return (Some(pattern.to_owned()), None);
|
||||
|
|
@ -186,12 +168,19 @@ pub fn split_pattern(pattern: &str) -> (Option<String>, Option<String>) {
|
|||
(static_part, dynamic_part)
|
||||
}
|
||||
|
||||
pub fn path_matches_globs(path: &Path, globs: &[GlobEntry]) -> bool {
|
||||
let path = path.to_string_lossy();
|
||||
|
||||
globs
|
||||
.iter()
|
||||
.any(|g| glob_match(format!("{}/{}", g.base, g.pattern), path.as_bytes()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::optimize_patterns;
|
||||
use crate::GlobEntry;
|
||||
use bexpand::Expression;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::process::Command;
|
||||
use std::{fs, path};
|
||||
use tempfile::tempdir;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,22 @@
|
|||
use crate::glob::hoist_static_glob_parts;
|
||||
use crate::scanner::allowed_paths::resolve_paths;
|
||||
use crate::scanner::detect_sources::DetectSources;
|
||||
use bexpand::Expression;
|
||||
use bstr::ByteSlice;
|
||||
use extractor::{Extracted, Extractor};
|
||||
use fast_glob::glob_match;
|
||||
use fxhash::{FxHashMap, FxHashSet};
|
||||
use glob::optimize_patterns;
|
||||
use paths::Path;
|
||||
use rayon::prelude::*;
|
||||
use scanner::allowed_paths::read_dir;
|
||||
use std::borrow::Cow;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::sync;
|
||||
use std::time::SystemTime;
|
||||
use tracing::event;
|
||||
|
||||
pub mod cursor;
|
||||
pub mod extractor;
|
||||
pub mod fast_skip;
|
||||
|
|
@ -6,7 +25,554 @@ pub mod paths;
|
|||
pub mod scanner;
|
||||
pub mod throughput;
|
||||
|
||||
pub use glob::GlobEntry;
|
||||
pub use scanner::sources::PublicSourceEntry;
|
||||
pub use scanner::ChangedContent;
|
||||
pub use scanner::Scanner;
|
||||
static SHOULD_TRACE: sync::LazyLock<bool> = sync::LazyLock::new(
|
||||
|| matches!(std::env::var("DEBUG"), Ok(value) if value.eq("*") || (value.contains("tailwindcss:oxide") && !value.contains("-tailwindcss:oxide"))),
|
||||
);
|
||||
|
||||
fn init_tracing() {
|
||||
if !*SHOULD_TRACE {
|
||||
return;
|
||||
}
|
||||
|
||||
_ = tracing_subscriber::fmt()
|
||||
.with_max_level(tracing::Level::INFO)
|
||||
.with_span_events(tracing_subscriber::fmt::format::FmtSpan::ACTIVE)
|
||||
.compact()
|
||||
.try_init();
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ChangedContent<'a> {
|
||||
File(PathBuf, Cow<'a, str>),
|
||||
Content(String, Cow<'a, str>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ScanOptions {
|
||||
/// Base path to start scanning from
|
||||
pub base: Option<String>,
|
||||
/// Glob sources
|
||||
pub sources: Vec<GlobEntry>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ScanResult {
|
||||
pub candidates: Vec<String>,
|
||||
pub files: Vec<String>,
|
||||
pub globs: Vec<GlobEntry>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct GlobEntry {
|
||||
pub base: String,
|
||||
pub pattern: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Scanner {
|
||||
/// Glob sources
|
||||
sources: Option<Vec<GlobEntry>>,
|
||||
|
||||
/// Scanner is ready to scan. We delay the file system traversal for detecting all files until
|
||||
/// we actually need them.
|
||||
ready: bool,
|
||||
|
||||
/// All files that we have to scan
|
||||
files: Vec<PathBuf>,
|
||||
|
||||
/// All directories, sub-directories, etc… we saw during source detection
|
||||
dirs: Vec<PathBuf>,
|
||||
|
||||
/// All generated globs
|
||||
globs: Vec<GlobEntry>,
|
||||
|
||||
/// Track file modification times
|
||||
mtimes: FxHashMap<PathBuf, SystemTime>,
|
||||
|
||||
/// Track unique set of candidates
|
||||
candidates: FxHashSet<String>,
|
||||
}
|
||||
|
||||
impl Scanner {
|
||||
pub fn new(sources: Option<Vec<GlobEntry>>) -> Self {
|
||||
Self {
|
||||
sources,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scan(&mut self) -> Vec<String> {
|
||||
init_tracing();
|
||||
|
||||
self.prepare();
|
||||
self.compute_candidates();
|
||||
|
||||
let mut candidates: Vec<String> = self.candidates.clone().into_par_iter().collect();
|
||||
candidates.par_sort_unstable();
|
||||
|
||||
candidates
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub fn scan_content(&mut self, changed_content: Vec<ChangedContent>) -> Vec<String> {
|
||||
self.prepare();
|
||||
let candidates = parse_all_blobs(read_all_files(changed_content));
|
||||
|
||||
let mut new_candidates = vec![];
|
||||
for candidate in candidates {
|
||||
if self.candidates.contains(&candidate) {
|
||||
continue;
|
||||
}
|
||||
self.candidates.insert(candidate.clone());
|
||||
new_candidates.push(candidate);
|
||||
}
|
||||
|
||||
new_candidates
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub fn get_candidates_with_positions(
|
||||
&mut self,
|
||||
changed_content: ChangedContent,
|
||||
) -> Vec<(String, usize)> {
|
||||
self.prepare();
|
||||
|
||||
let content = read_changed_content(changed_content).unwrap_or_default();
|
||||
let original_content = &content;
|
||||
|
||||
// Workaround for legacy upgrades:
|
||||
//
|
||||
// `-[]` won't parse in the new parser (`[…]` must contain _something_), but we do need it
|
||||
// for people using `group-[]` (which we will later replace with `in-[.group]` instead).
|
||||
let content = content.replace("-[]", "XYZ");
|
||||
let offset = content.as_ptr() as usize;
|
||||
|
||||
let mut extractor = Extractor::new(&content[..]);
|
||||
|
||||
extractor
|
||||
.extract()
|
||||
.into_par_iter()
|
||||
.flat_map(|extracted| match extracted {
|
||||
Extracted::Candidate(s) => {
|
||||
let i = s.as_ptr() as usize - offset;
|
||||
let original = &original_content[i..i + s.len()];
|
||||
if original.contains_str("-[]") {
|
||||
return Some(unsafe {
|
||||
(String::from_utf8_unchecked(original.to_vec()), i)
|
||||
});
|
||||
}
|
||||
|
||||
// SAFETY: When we parsed the candidates, we already guaranteed that the byte
|
||||
// slices are valid, therefore we don't have to re-check here when we want to
|
||||
// convert it back to a string.
|
||||
Some(unsafe { (String::from_utf8_unchecked(s.to_vec()), i) })
|
||||
}
|
||||
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub fn get_files(&mut self) -> Vec<String> {
|
||||
self.prepare();
|
||||
|
||||
self.files
|
||||
.par_iter()
|
||||
.filter_map(|x| Path::from(x.clone()).canonicalize().ok())
|
||||
.map(|x| x.to_string())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub fn get_globs(&mut self) -> Vec<GlobEntry> {
|
||||
self.prepare();
|
||||
|
||||
self.globs.clone()
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
fn compute_candidates(&mut self) {
|
||||
let mut changed_content = vec![];
|
||||
|
||||
let current_mtimes = self
|
||||
.files
|
||||
.par_iter()
|
||||
.map(|path| {
|
||||
fs::metadata(path)
|
||||
.and_then(|m| m.modified())
|
||||
.unwrap_or(SystemTime::now())
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for (idx, path) in self.files.iter().enumerate() {
|
||||
let current_time = current_mtimes[idx];
|
||||
let previous_time = self.mtimes.insert(path.clone(), current_time);
|
||||
|
||||
let should_scan_file = match previous_time {
|
||||
// Time has changed, so we need to re-scan the file
|
||||
Some(prev) if prev != current_time => true,
|
||||
|
||||
// File was in the cache, no need to re-scan
|
||||
Some(_) => false,
|
||||
|
||||
// File didn't exist before, so we need to scan it
|
||||
None => true,
|
||||
};
|
||||
|
||||
if should_scan_file {
|
||||
let extension = path.extension().unwrap_or_default().to_string_lossy();
|
||||
changed_content.push(ChangedContent::File(path.to_path_buf(), extension))
|
||||
}
|
||||
}
|
||||
|
||||
if !changed_content.is_empty() {
|
||||
let candidates = parse_all_blobs(read_all_files(changed_content));
|
||||
self.candidates.par_extend(candidates);
|
||||
}
|
||||
}
|
||||
|
||||
// Ensures that all files/globs are resolved and the scanner is ready to scan
|
||||
// content for candidates.
|
||||
fn prepare(&mut self) {
|
||||
if self.ready {
|
||||
self.check_for_new_files();
|
||||
return;
|
||||
}
|
||||
|
||||
self.scan_sources();
|
||||
|
||||
self.ready = true;
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
fn check_for_new_files(&mut self) {
|
||||
let current_mtimes = self
|
||||
.dirs
|
||||
.par_iter()
|
||||
.map(|path| {
|
||||
fs::metadata(path)
|
||||
.and_then(|m| m.modified())
|
||||
.unwrap_or(SystemTime::now())
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut modified_dirs: Vec<PathBuf> = vec![];
|
||||
|
||||
// Check all directories to see if they were modified
|
||||
for (idx, path) in self.dirs.iter().enumerate() {
|
||||
let current_time = current_mtimes[idx];
|
||||
let previous_time = self.mtimes.insert(path.clone(), current_time);
|
||||
|
||||
let should_scan = match previous_time {
|
||||
// Time has changed, so we need to re-scan the file
|
||||
Some(prev) if prev != current_time => true,
|
||||
|
||||
// File was in the cache, no need to re-scan
|
||||
Some(_) => false,
|
||||
|
||||
// File didn't exist before, so we need to scan it
|
||||
None => true,
|
||||
};
|
||||
|
||||
if should_scan {
|
||||
modified_dirs.push(path.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Scan all modified directories for their immediate files
|
||||
let mut known = FxHashSet::from_iter(self.files.iter().chain(self.dirs.iter()).cloned());
|
||||
|
||||
while !modified_dirs.is_empty() {
|
||||
let new_entries = modified_dirs
|
||||
.iter()
|
||||
.flat_map(|dir| read_dir(dir, Some(1)))
|
||||
.map(|entry| entry.path().to_owned())
|
||||
.filter(|path| !known.contains(path))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
modified_dirs.clear();
|
||||
|
||||
for path in new_entries {
|
||||
if path.is_file() {
|
||||
known.insert(path.clone());
|
||||
self.files.push(path);
|
||||
} else if path.is_dir() {
|
||||
known.insert(path.clone());
|
||||
self.dirs.push(path.clone());
|
||||
|
||||
// Recursively scan the new directory for files
|
||||
modified_dirs.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
fn scan_sources(&mut self) {
|
||||
let Some(sources) = &self.sources else {
|
||||
return;
|
||||
};
|
||||
|
||||
if sources.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Expand glob patterns and create new `GlobEntry` instances for each expanded pattern.
|
||||
let sources = sources
|
||||
.iter()
|
||||
.flat_map(|source| {
|
||||
let expression: Result<Expression, _> = source.pattern[..].try_into();
|
||||
let Ok(expression) = expression else {
|
||||
return vec![source.clone()];
|
||||
};
|
||||
|
||||
expression
|
||||
.into_iter()
|
||||
.filter_map(Result::ok)
|
||||
.map(move |pattern| GlobEntry {
|
||||
base: source.base.clone(),
|
||||
pattern: pattern.into(),
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Partition sources into sources that should be promoted to auto source detection and
|
||||
// sources that should be resolved as globs.
|
||||
let (auto_sources, glob_sources): (Vec<_>, Vec<_>) = sources.iter().partition(|source| {
|
||||
// If a glob ends with `/**/*`, then we just want to register the base path as a new
|
||||
// base. Essentially converting it to use auto source detection.
|
||||
if source.pattern.ends_with("**/*") {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Directories should be promoted to auto source detection
|
||||
if PathBuf::from(&source.base).join(&source.pattern).is_dir() {
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
});
|
||||
|
||||
fn join_paths(a: &str, b: &str) -> PathBuf {
|
||||
let mut tmp = a.to_owned();
|
||||
let b = b.trim_end_matches("**/*").trim_end_matches('/');
|
||||
|
||||
if b.starts_with('/') {
|
||||
return PathBuf::from(b);
|
||||
}
|
||||
|
||||
// On Windows a path like C:/foo.txt is absolute but C:foo.txt is not
|
||||
// (the 2nd is relative to the CWD)
|
||||
if b.chars().nth(1) == Some(':') && b.chars().nth(2) == Some('/') {
|
||||
return PathBuf::from(b);
|
||||
}
|
||||
|
||||
tmp += "/";
|
||||
tmp += b;
|
||||
|
||||
PathBuf::from(&tmp)
|
||||
}
|
||||
|
||||
for path in auto_sources.iter().filter_map(|source| {
|
||||
dunce::canonicalize(join_paths(&source.base, &source.pattern)).ok()
|
||||
}) {
|
||||
// Insert a glob for the base path, so we can see new files/folders in the directory itself.
|
||||
self.globs.push(GlobEntry {
|
||||
base: path.to_string_lossy().into(),
|
||||
pattern: "*".into(),
|
||||
});
|
||||
|
||||
// Detect all files/folders in the directory
|
||||
let detect_sources = DetectSources::new(path);
|
||||
|
||||
let (files, globs, dirs) = detect_sources.detect();
|
||||
self.files.extend(files);
|
||||
self.globs.extend(globs);
|
||||
self.dirs.extend(dirs);
|
||||
}
|
||||
|
||||
// Turn `Vec<&GlobEntry>` in `Vec<GlobEntry>`
|
||||
let glob_sources: Vec<_> = glob_sources.into_iter().cloned().collect();
|
||||
let hoisted = hoist_static_glob_parts(&glob_sources);
|
||||
|
||||
for source in &hoisted {
|
||||
// If the pattern is empty, then the base points to a specific file or folder already
|
||||
// if it doesn't contain any dynamic parts. In that case we can use the base as the
|
||||
// pattern.
|
||||
//
|
||||
// Otherwise we need to combine the base and the pattern, otherwise a pattern that
|
||||
// looks like `*.html`, will never match a path that looks like
|
||||
// `/my-project/project-a/index.html`, because it contains `/`.
|
||||
//
|
||||
// We can't prepend `**/`, because then `/my-project/project-a/nested/index.html` would
|
||||
// match as well.
|
||||
//
|
||||
// Instead we combine the base and the pattern as a single glob pattern.
|
||||
let mut full_pattern = source.base.clone().replace('\\', "/");
|
||||
|
||||
if !source.pattern.is_empty() {
|
||||
full_pattern.push('/');
|
||||
full_pattern.push_str(&source.pattern);
|
||||
}
|
||||
|
||||
let base = PathBuf::from(&source.base);
|
||||
for entry in resolve_paths(&base) {
|
||||
let Some(file_type) = entry.file_type() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if !file_type.is_file() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let file_path = entry.into_path();
|
||||
|
||||
let Some(file_path_str) = file_path.to_str() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let file_path_str = file_path_str.replace('\\', "/");
|
||||
|
||||
if glob_match(&full_pattern, &file_path_str) {
|
||||
self.files.push(file_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.globs.extend(hoisted);
|
||||
|
||||
// Re-optimize the globs to reduce the number of patterns we have to scan.
|
||||
self.globs = optimize_patterns(&self.globs);
|
||||
}
|
||||
}
|
||||
|
||||
fn read_changed_content(c: ChangedContent) -> Option<Vec<u8>> {
|
||||
let (content, extension) = match c {
|
||||
ChangedContent::File(file, extension) => match std::fs::read(&file) {
|
||||
Ok(content) => (content, extension),
|
||||
Err(e) => {
|
||||
event!(tracing::Level::ERROR, "Failed to read file: {:?}", e);
|
||||
return None;
|
||||
}
|
||||
},
|
||||
|
||||
ChangedContent::Content(contents, extension) => (contents.into_bytes(), extension),
|
||||
};
|
||||
|
||||
Some(pre_process_input(&content, &extension))
|
||||
}
|
||||
|
||||
pub fn pre_process_input(content: &[u8], extension: &str) -> Vec<u8> {
|
||||
use crate::extractor::pre_processors::*;
|
||||
|
||||
match extension {
|
||||
"clj" | "cljs" | "cljc" => Clojure.process(content),
|
||||
"cshtml" | "razor" => Razor.process(content),
|
||||
"haml" => Haml.process(content),
|
||||
"json" => Json.process(content),
|
||||
"pug" => Pug.process(content),
|
||||
"rb" | "erb" => Ruby.process(content),
|
||||
"slim" => Slim.process(content),
|
||||
"svelte" => Svelte.process(content),
|
||||
"vue" => Vue.process(content),
|
||||
_ => content.to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
fn read_all_files(changed_content: Vec<ChangedContent>) -> Vec<Vec<u8>> {
|
||||
event!(
|
||||
tracing::Level::INFO,
|
||||
"Reading {:?} file(s)",
|
||||
changed_content.len()
|
||||
);
|
||||
|
||||
changed_content
|
||||
.into_par_iter()
|
||||
.filter_map(read_changed_content)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
fn parse_all_blobs(blobs: Vec<Vec<u8>>) -> Vec<String> {
|
||||
let mut result: Vec<_> = blobs
|
||||
.par_iter()
|
||||
.flat_map(|blob| blob.par_split(|x| *x == b'\n'))
|
||||
.filter_map(|blob| {
|
||||
if blob.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let extracted = crate::extractor::Extractor::new(blob).extract();
|
||||
if extracted.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(FxHashSet::from_iter(extracted.into_iter().map(
|
||||
|x| match x {
|
||||
Extracted::Candidate(bytes) => bytes,
|
||||
Extracted::CssVariable(bytes) => bytes,
|
||||
},
|
||||
)))
|
||||
})
|
||||
.reduce(Default::default, |mut a, b| {
|
||||
a.extend(b);
|
||||
a
|
||||
})
|
||||
.into_iter()
|
||||
.map(|s| unsafe { String::from_utf8_unchecked(s.to_vec()) })
|
||||
.collect();
|
||||
|
||||
// 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();
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::Scanner;
|
||||
|
||||
#[test]
|
||||
fn test_positions() {
|
||||
let mut scanner = Scanner::new(None);
|
||||
|
||||
for (input, expected) in [
|
||||
// Before migrations
|
||||
(
|
||||
r#"<div class="!tw__flex sm:!tw__block tw__bg-gradient-to-t flex tw:[color:red] group-[]:tw__flex"#,
|
||||
vec![
|
||||
("class".to_string(), 5),
|
||||
("!tw__flex".to_string(), 12),
|
||||
("sm:!tw__block".to_string(), 22),
|
||||
("tw__bg-gradient-to-t".to_string(), 36),
|
||||
("flex".to_string(), 57),
|
||||
("tw:[color:red]".to_string(), 62),
|
||||
("group-[]:tw__flex".to_string(), 77),
|
||||
],
|
||||
),
|
||||
// After migrations
|
||||
(
|
||||
r#"<div class="tw:flex! tw:sm:block! tw:bg-linear-to-t flex tw:[color:red] tw:in-[.tw\:group]:flex"></div>"#,
|
||||
vec![
|
||||
("class".to_string(), 5),
|
||||
("tw:flex!".to_string(), 12),
|
||||
("tw:sm:block!".to_string(), 21),
|
||||
("tw:bg-linear-to-t".to_string(), 34),
|
||||
("flex".to_string(), 52),
|
||||
("tw:[color:red]".to_string(), 57),
|
||||
("tw:in-[.tw\\:group]:flex".to_string(), 72),
|
||||
],
|
||||
),
|
||||
] {
|
||||
let candidates = scanner.get_candidates_with_positions(crate::ChangedContent::Content(
|
||||
input.to_string(),
|
||||
"html".into(),
|
||||
));
|
||||
assert_eq!(candidates, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
128
crates/oxide/src/scanner/allowed_paths.rs
Normal file
128
crates/oxide/src/scanner/allowed_paths.rs
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
use ignore::{DirEntry, WalkBuilder};
|
||||
use std::{path::Path, sync};
|
||||
|
||||
static BINARY_EXTENSIONS: sync::LazyLock<Vec<&'static str>> = sync::LazyLock::new(|| {
|
||||
include_str!("fixtures/binary-extensions.txt")
|
||||
.trim()
|
||||
.lines()
|
||||
.collect()
|
||||
});
|
||||
|
||||
static IGNORED_EXTENSIONS: sync::LazyLock<Vec<&'static str>> = sync::LazyLock::new(|| {
|
||||
include_str!("fixtures/ignored-extensions.txt")
|
||||
.trim()
|
||||
.lines()
|
||||
.collect()
|
||||
});
|
||||
|
||||
static IGNORED_FILES: sync::LazyLock<Vec<&'static str>> = sync::LazyLock::new(|| {
|
||||
include_str!("fixtures/ignored-files.txt")
|
||||
.trim()
|
||||
.lines()
|
||||
.collect()
|
||||
});
|
||||
|
||||
static IGNORED_CONTENT_DIRS: sync::LazyLock<Vec<&'static str>> =
|
||||
sync::LazyLock::new(|| vec![".git"]);
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub fn resolve_allowed_paths(root: &Path) -> impl Iterator<Item = DirEntry> {
|
||||
// Read the directory recursively with no depth limit
|
||||
read_dir(root, None)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub fn resolve_paths(root: &Path) -> impl Iterator<Item = DirEntry> {
|
||||
create_walk_builder(root).build().filter_map(Result::ok)
|
||||
}
|
||||
|
||||
pub fn read_dir(root: &Path, depth: Option<usize>) -> impl Iterator<Item = DirEntry> {
|
||||
create_walk_builder(root)
|
||||
.max_depth(depth)
|
||||
.filter_entry(move |entry| match entry.file_type() {
|
||||
Some(file_type) if file_type.is_dir() => match entry.file_name().to_str() {
|
||||
Some(dir) => !IGNORED_CONTENT_DIRS.contains(&dir),
|
||||
None => false,
|
||||
},
|
||||
Some(file_type) if file_type.is_file() || file_type.is_symlink() => {
|
||||
is_allowed_content_path(entry.path())
|
||||
}
|
||||
_ => false,
|
||||
})
|
||||
.build()
|
||||
.filter_map(Result::ok)
|
||||
}
|
||||
|
||||
fn create_walk_builder(root: &Path) -> WalkBuilder {
|
||||
let mut builder = WalkBuilder::new(root);
|
||||
|
||||
// Scan hidden files / directories
|
||||
builder.hidden(false);
|
||||
|
||||
// By default, allow .gitignore files to be used regardless of whether or not
|
||||
// a .git directory is present. This is an optimization for when projects
|
||||
// are first created and may not be in a git repo yet.
|
||||
builder.require_git(false);
|
||||
|
||||
// Don't descend into .git directories inside the root folder
|
||||
// This is necessary when `root` contains the `.git` dir.
|
||||
builder.filter_entry(|entry| entry.file_name() != ".git");
|
||||
|
||||
// If we are in a git repo then require it to ensure that only rules within
|
||||
// the repo are used. For example, we don't want to consider a .gitignore file
|
||||
// in the user's home folder if we're in a git repo.
|
||||
//
|
||||
// The alternative is using a call like `.parents(false)` but that will
|
||||
// prevent looking at parent directories for .gitignore files from within
|
||||
// the repo and that's not what we want.
|
||||
//
|
||||
// For example, in a project with this structure:
|
||||
//
|
||||
// home
|
||||
// .gitignore
|
||||
// my-project
|
||||
// .gitignore
|
||||
// apps
|
||||
// .gitignore
|
||||
// web
|
||||
// {root}
|
||||
//
|
||||
// We do want to consider all .gitignore files listed:
|
||||
// - home/.gitignore
|
||||
// - my-project/.gitignore
|
||||
// - my-project/apps/.gitignore
|
||||
//
|
||||
// However, if a repo is initialized inside my-project then only the following
|
||||
// make sense for consideration:
|
||||
// - my-project/.gitignore
|
||||
// - my-project/apps/.gitignore
|
||||
//
|
||||
// Setting the require_git(true) flag conditionally allows us to do this.
|
||||
for parent in root.ancestors() {
|
||||
if parent.join(".git").exists() {
|
||||
builder.require_git(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
builder
|
||||
}
|
||||
|
||||
pub fn is_allowed_content_path(path: &Path) -> bool {
|
||||
// Skip known ignored files
|
||||
if path
|
||||
.file_name()
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.map(|s| IGNORED_FILES.contains(&s))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Skip known ignored extensions
|
||||
path.extension()
|
||||
.map(|s| s.to_str().unwrap_or_default())
|
||||
.map(|ext| !IGNORED_EXTENSIONS.contains(&ext) && !BINARY_EXTENSIONS.contains(&ext))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
use ignore::gitignore::{Gitignore, GitignoreBuilder};
|
||||
use std::sync;
|
||||
|
||||
/// All the default rules for auto source detection.
|
||||
///
|
||||
/// This includes:
|
||||
///
|
||||
/// - Ignoring common content directories like `.git` and `node_modules`
|
||||
/// - Ignoring file extensions we definitely don't want to include like `.css` and `.scss`
|
||||
/// - Ignoring common binary file extensions like `.png` and `.jpg`
|
||||
/// - Ignoring common files like `yarn.lock` and `package-lock.json`
|
||||
///
|
||||
pub static RULES: sync::LazyLock<Vec<Gitignore>> = sync::LazyLock::new(|| {
|
||||
let mut builder = GitignoreBuilder::new("");
|
||||
|
||||
builder.add_line(None, &IGNORED_CONTENT_DIRS_GLOB).unwrap();
|
||||
builder.add_line(None, &IGNORED_EXTENSIONS_GLOB).unwrap();
|
||||
builder.add_line(None, &IGNORED_FILES_GLOB).unwrap();
|
||||
|
||||
// Ensure these rules do not match on folder names
|
||||
let mut file_only_builder = GitignoreBuilder::new("");
|
||||
file_only_builder
|
||||
.only_on_files(true)
|
||||
.add_line(None, &BINARY_EXTENSIONS_GLOB)
|
||||
.unwrap();
|
||||
|
||||
vec![builder.build().unwrap(), file_only_builder.build().unwrap()]
|
||||
});
|
||||
|
||||
pub static IGNORED_CONTENT_DIRS: sync::LazyLock<Vec<&'static str>> = sync::LazyLock::new(|| {
|
||||
include_str!("fixtures/ignored-content-dirs.txt")
|
||||
.trim()
|
||||
.lines()
|
||||
.collect()
|
||||
});
|
||||
|
||||
static IGNORED_CONTENT_DIRS_GLOB: sync::LazyLock<String> =
|
||||
sync::LazyLock::new(|| format!("{{{}}}/", IGNORED_CONTENT_DIRS.join(",")));
|
||||
|
||||
static IGNORED_EXTENSIONS_GLOB: sync::LazyLock<String> = sync::LazyLock::new(|| {
|
||||
format!(
|
||||
"*.{{{}}}",
|
||||
include_str!("fixtures/ignored-extensions.txt")
|
||||
.trim()
|
||||
.lines()
|
||||
.collect::<Vec<&str>>()
|
||||
.join(",")
|
||||
)
|
||||
});
|
||||
|
||||
pub static BINARY_EXTENSIONS_GLOB: sync::LazyLock<String> = sync::LazyLock::new(|| {
|
||||
format!(
|
||||
"*.{{{}}}",
|
||||
include_str!("fixtures/binary-extensions.txt")
|
||||
.trim()
|
||||
.lines()
|
||||
.collect::<Vec<&str>>()
|
||||
.join(",")
|
||||
)
|
||||
});
|
||||
|
||||
static IGNORED_FILES_GLOB: sync::LazyLock<String> = sync::LazyLock::new(|| {
|
||||
format!(
|
||||
"{{{}}}",
|
||||
include_str!("fixtures/ignored-files.txt")
|
||||
.trim()
|
||||
.lines()
|
||||
.collect::<Vec<&str>>()
|
||||
.join(",")
|
||||
)
|
||||
});
|
||||
|
|
@ -1,12 +1,16 @@
|
|||
use crate::scanner::auto_source_detection::IGNORED_CONTENT_DIRS;
|
||||
use crate::scanner::allowed_paths::{is_allowed_content_path, resolve_allowed_paths};
|
||||
use crate::GlobEntry;
|
||||
use fxhash::FxHashSet;
|
||||
use globwalk::DirEntry;
|
||||
use std::cmp::Ordering;
|
||||
use std::path::PathBuf;
|
||||
use std::sync;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DetectSources {
|
||||
base: PathBuf,
|
||||
}
|
||||
|
||||
static KNOWN_EXTENSIONS: sync::LazyLock<Vec<&'static str>> = sync::LazyLock::new(|| {
|
||||
include_str!("fixtures/template-extensions.txt")
|
||||
.trim()
|
||||
|
|
@ -18,143 +22,200 @@ static KNOWN_EXTENSIONS: sync::LazyLock<Vec<&'static str>> = sync::LazyLock::new
|
|||
.collect()
|
||||
});
|
||||
|
||||
// Sorting to make sure that we always see the directories before the files. Also sorting
|
||||
// alphabetically by default.
|
||||
fn sort_by_dir_and_name(a: &DirEntry, z: &DirEntry) -> Ordering {
|
||||
match (a.file_type().is_dir(), z.file_type().is_dir()) {
|
||||
(true, false) => Ordering::Less,
|
||||
(false, true) => Ordering::Greater,
|
||||
_ => a.file_name().cmp(z.file_name()),
|
||||
impl DetectSources {
|
||||
pub fn new(base: PathBuf) -> Self {
|
||||
Self { base }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_globs(
|
||||
base: PathBuf,
|
||||
dirs: &FxHashSet<PathBuf>,
|
||||
extensions: &FxHashSet<String>,
|
||||
) -> Vec<GlobEntry> {
|
||||
// A list of known extensions + a list of extensions we found in the project.
|
||||
let mut found_extensions: FxHashSet<String> =
|
||||
FxHashSet::from_iter(KNOWN_EXTENSIONS.iter().map(|x| x.to_string()));
|
||||
found_extensions.extend(extensions.iter().cloned());
|
||||
pub fn detect(&self) -> (Vec<PathBuf>, Vec<GlobEntry>, Vec<PathBuf>) {
|
||||
let (files, dirs) = self.resolve_files();
|
||||
let globs = self.resolve_globs(&dirs);
|
||||
|
||||
// A list of directory names where we can't use globs, but we should track each file
|
||||
// individually instead. This is because these directories are often used for both source and
|
||||
// destination files.
|
||||
let forced_static_directories: FxHashSet<PathBuf> =
|
||||
FxHashSet::from_iter(vec![base.join("public")]);
|
||||
(files, globs, dirs)
|
||||
}
|
||||
|
||||
// All directories where we can safely use deeply nested globs to watch all files.
|
||||
// In other comments we refer to these as "deep glob directories" or similar.
|
||||
//
|
||||
// E.g.: `./src/**/*.{html,js}`
|
||||
let mut deep_globable_directories: FxHashSet<PathBuf> = Default::default();
|
||||
fn resolve_files(&self) -> (Vec<PathBuf>, Vec<PathBuf>) {
|
||||
let mut files: Vec<PathBuf> = vec![];
|
||||
let mut dirs: Vec<PathBuf> = vec![];
|
||||
|
||||
// All directories where we can only use shallow globs to watch all direct files but not
|
||||
// folders.
|
||||
// In other comments we refer to these as "shallow glob directories" or similar.
|
||||
//
|
||||
// E.g.: `./src/*/*.{html,js}`
|
||||
let mut shallow_globable_directories: FxHashSet<PathBuf> = Default::default();
|
||||
for entry in resolve_allowed_paths(&self.base) {
|
||||
let Some(file_type) = entry.file_type() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Collect all valid paths from the root. This will already filter out ignored files, unknown
|
||||
// extensions and binary files.
|
||||
let mut it = WalkDir::new(&base)
|
||||
.sort_by(sort_by_dir_and_name)
|
||||
.into_iter();
|
||||
|
||||
// Figure out all the shallow globable directories.
|
||||
while let Some(Ok(entry)) = it.next() {
|
||||
let path = entry.path();
|
||||
if !path.is_dir() {
|
||||
continue;
|
||||
if file_type.is_file() {
|
||||
files.push(entry.into_path());
|
||||
} else if file_type.is_dir() {
|
||||
dirs.push(entry.into_path());
|
||||
}
|
||||
}
|
||||
|
||||
if !dirs.contains(path) {
|
||||
let mut path = path;
|
||||
while let Some(parent) = path.parent() {
|
||||
if parent == base {
|
||||
break;
|
||||
}
|
||||
(files, dirs)
|
||||
}
|
||||
|
||||
shallow_globable_directories.insert(parent.to_path_buf());
|
||||
path = parent
|
||||
fn resolve_globs(&self, dirs: &Vec<PathBuf>) -> Vec<GlobEntry> {
|
||||
let allowed_paths = FxHashSet::from_iter(dirs);
|
||||
|
||||
// A list of directory names where we can't use globs, but we should track each file
|
||||
// individually instead. This is because these directories are often used for both source and
|
||||
// destination files.
|
||||
let mut forced_static_directories = vec![self.base.join("public")];
|
||||
|
||||
// A list of known extensions + a list of extensions we found in the project.
|
||||
let mut found_extensions =
|
||||
FxHashSet::from_iter(KNOWN_EXTENSIONS.iter().map(|x| x.to_string()));
|
||||
|
||||
// All root directories.
|
||||
let mut root_directories = FxHashSet::from_iter(vec![self.base.clone()]);
|
||||
|
||||
// All directories where we can safely use deeply nested globs to watch all files.
|
||||
// In other comments we refer to these as "deep glob directories" or similar.
|
||||
//
|
||||
// E.g.: `./src/**/*.{html,js}`
|
||||
let mut deep_globable_directories: FxHashSet<PathBuf> = FxHashSet::default();
|
||||
|
||||
// All directories where we can only use shallow globs to watch all direct files but not
|
||||
// folders.
|
||||
// In other comments we refer to these as "shallow glob directories" or similar.
|
||||
//
|
||||
// E.g.: `./src/*/*.{html,js}`
|
||||
let mut shallow_globable_directories: FxHashSet<PathBuf> = FxHashSet::default();
|
||||
|
||||
// Collect all valid paths from the root. This will already filter out ignored files, unknown
|
||||
// extensions and binary files.
|
||||
let mut it = WalkDir::new(&self.base)
|
||||
// Sorting to make sure that we always see the directories before the files. Also sorting
|
||||
// alphabetically by default.
|
||||
.sort_by(
|
||||
|a, z| match (a.file_type().is_dir(), z.file_type().is_dir()) {
|
||||
(true, false) => Ordering::Less,
|
||||
(false, true) => Ordering::Greater,
|
||||
_ => a.file_name().cmp(z.file_name()),
|
||||
},
|
||||
)
|
||||
.into_iter();
|
||||
|
||||
// We are only interested in valid entries
|
||||
while let Some(Ok(entry)) = it.next() {
|
||||
// Ignore known directories that we don't want to traverse into.
|
||||
if entry.file_type().is_dir() && entry.file_name() == ".git" {
|
||||
it.skip_current_dir();
|
||||
continue;
|
||||
}
|
||||
|
||||
it.skip_current_dir();
|
||||
if entry.file_type().is_dir() {
|
||||
// If we are in a directory where we know that we can't use any globs, then we have to
|
||||
// track each file individually.
|
||||
if forced_static_directories.contains(&entry.path().to_path_buf()) {
|
||||
forced_static_directories.push(entry.path().to_path_buf());
|
||||
root_directories.insert(entry.path().to_path_buf());
|
||||
continue;
|
||||
}
|
||||
|
||||
// Although normally very unlikely, if running inside a dockerfile
|
||||
// the current directory might be "/" with no parent
|
||||
if let Some(parent) = entry.path().parent() {
|
||||
// If we are in a directory where the parent is a forced static directory, then this
|
||||
// will become a forced static directory as well.
|
||||
if forced_static_directories.contains(&parent.to_path_buf()) {
|
||||
forced_static_directories.push(entry.path().to_path_buf());
|
||||
root_directories.insert(entry.path().to_path_buf());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// If we are in a directory, and the directory is git ignored, then we don't have to
|
||||
// descent into the directory. However, we have to make sure that we mark the _parent_
|
||||
// directory as a shallow glob directory because using deep globs from any of the
|
||||
// parent directories will include this ignored directory which should not be the case.
|
||||
//
|
||||
// Another important part is that if one of the ignored directories is a deep glob
|
||||
// directory, then all of its parents (until the root) should be marked as shallow glob
|
||||
// directories as well.
|
||||
if !allowed_paths.contains(&entry.path().to_path_buf()) {
|
||||
let mut parent = entry.path().parent();
|
||||
while let Some(parent_path) = parent {
|
||||
// If the parent is already marked as a valid deep glob directory, then we have
|
||||
// to mark it as a shallow glob directory instead, because we won't be able to
|
||||
// use deep globs for this directory anymore.
|
||||
if deep_globable_directories.contains(parent_path) {
|
||||
deep_globable_directories.remove(parent_path);
|
||||
shallow_globable_directories.insert(parent_path.to_path_buf());
|
||||
}
|
||||
|
||||
// If we reached the root, then we can stop.
|
||||
if parent_path == self.base {
|
||||
break;
|
||||
}
|
||||
|
||||
// Mark the parent directory as a shallow glob directory and continue with its
|
||||
// parent.
|
||||
shallow_globable_directories.insert(parent_path.to_path_buf());
|
||||
parent = parent_path.parent();
|
||||
}
|
||||
|
||||
it.skip_current_dir();
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we are in a directory that is not git ignored, then we can mark this directory as
|
||||
// a valid deep glob directory. This is only necessary if any of its parents aren't
|
||||
// marked as deep glob directories already.
|
||||
let mut found_deep_glob_parent = false;
|
||||
let mut parent = entry.path().parent();
|
||||
while let Some(parent_path) = parent {
|
||||
// If we reached the root, then we can stop.
|
||||
if parent_path == self.base {
|
||||
break;
|
||||
}
|
||||
|
||||
// If the parent is already marked as a deep glob directory, then we can stop
|
||||
// because this glob will match the current directory already.
|
||||
if deep_globable_directories.contains(parent_path) {
|
||||
found_deep_glob_parent = true;
|
||||
break;
|
||||
}
|
||||
|
||||
parent = parent_path.parent();
|
||||
}
|
||||
|
||||
// If we didn't find a deep glob directory parent, then we can mark this directory as a
|
||||
// deep glob directory (unless it is the root).
|
||||
if !found_deep_glob_parent && entry.path() != self.base {
|
||||
deep_globable_directories.insert(entry.path().to_path_buf());
|
||||
}
|
||||
}
|
||||
|
||||
// Handle allowed content paths
|
||||
if is_allowed_content_path(entry.path())
|
||||
&& allowed_paths.contains(&entry.path().to_path_buf())
|
||||
{
|
||||
let path = entry.path();
|
||||
|
||||
// Collect the extension for future use when building globs.
|
||||
if let Some(extension) = path.extension().and_then(|x| x.to_str()) {
|
||||
found_extensions.insert(extension.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut extension_list = found_extensions.into_iter().collect::<Vec<_>>();
|
||||
|
||||
extension_list.sort();
|
||||
|
||||
let extension_list = extension_list.join(",");
|
||||
|
||||
// Build the globs for all globable directories.
|
||||
let shallow_globs = shallow_globable_directories.iter().map(|path| GlobEntry {
|
||||
base: path.display().to_string(),
|
||||
pattern: format!("*/*.{{{}}}", extension_list),
|
||||
});
|
||||
|
||||
let deep_globs = deep_globable_directories.iter().map(|path| GlobEntry {
|
||||
base: path.display().to_string(),
|
||||
pattern: format!("**/*.{{{}}}", extension_list),
|
||||
});
|
||||
|
||||
shallow_globs.chain(deep_globs).collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
// Figure out all the deep globable directories.
|
||||
let mut it = WalkDir::new(&base)
|
||||
.sort_by(sort_by_dir_and_name)
|
||||
.into_iter();
|
||||
|
||||
while let Some(Ok(entry)) = it.next() {
|
||||
let path = entry.path();
|
||||
if path.is_file() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if path == base {
|
||||
continue;
|
||||
}
|
||||
|
||||
if IGNORED_CONTENT_DIRS
|
||||
.iter()
|
||||
.any(|dir| match path.file_name() {
|
||||
Some(name) => name == *dir,
|
||||
None => false,
|
||||
})
|
||||
{
|
||||
it.skip_current_dir();
|
||||
continue;
|
||||
}
|
||||
|
||||
if !dirs.contains(path) {
|
||||
it.skip_current_dir();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Already marked as a shallow globable directory.
|
||||
if shallow_globable_directories.contains(path) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if forced_static_directories.contains(path) {
|
||||
it.skip_current_dir();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Track deep globable directories.
|
||||
deep_globable_directories.insert(path.to_path_buf());
|
||||
it.skip_current_dir();
|
||||
}
|
||||
|
||||
let mut extension_list = found_extensions.clone().into_iter().collect::<Vec<_>>();
|
||||
|
||||
extension_list.sort();
|
||||
|
||||
let extension_list = extension_list.join(",");
|
||||
|
||||
// Build the globs for all globable directories.
|
||||
let shallow_globs = shallow_globable_directories.iter().map(|path| GlobEntry {
|
||||
base: path.display().to_string(),
|
||||
pattern: format!("*/*.{{{}}}", extension_list),
|
||||
});
|
||||
|
||||
let deep_globs = deep_globable_directories.iter().map(|path| GlobEntry {
|
||||
base: path.display().to_string(),
|
||||
pattern: format!("**/*.{{{}}}", extension_list),
|
||||
});
|
||||
|
||||
shallow_globs
|
||||
.chain(deep_globs)
|
||||
// Insert a glob for the base path, so we can see new files/folders in the directory
|
||||
// itself
|
||||
.chain(vec![GlobEntry {
|
||||
base: base.to_string_lossy().into(),
|
||||
pattern: "*".into(),
|
||||
}])
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@ cpio
|
|||
cr2
|
||||
cur
|
||||
dat
|
||||
db
|
||||
dcm
|
||||
deb
|
||||
dex
|
||||
|
|
@ -66,7 +65,6 @@ eol
|
|||
eot
|
||||
epub
|
||||
exe
|
||||
exr
|
||||
f4v
|
||||
fbs
|
||||
fh
|
||||
|
|
@ -79,7 +77,6 @@ fpx
|
|||
fst
|
||||
fvt
|
||||
g3
|
||||
geojson
|
||||
gh
|
||||
gif
|
||||
graffle
|
||||
|
|
@ -88,7 +85,6 @@ gzip
|
|||
h261
|
||||
h263
|
||||
h264
|
||||
hdr
|
||||
icns
|
||||
ico
|
||||
ief
|
||||
|
|
@ -203,6 +199,8 @@ snap
|
|||
snk
|
||||
so
|
||||
sqlite
|
||||
sqlite
|
||||
sqlite3
|
||||
sqlite3
|
||||
stl
|
||||
storedata
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
.git
|
||||
.hg
|
||||
.jj
|
||||
.next
|
||||
.parcel-cache
|
||||
.pnpm-store
|
||||
.svelte-kit
|
||||
.svn
|
||||
.turbo
|
||||
.venv
|
||||
.vercel
|
||||
.yarn
|
||||
__pycache__
|
||||
node_modules
|
||||
venv
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
css
|
||||
less
|
||||
lock
|
||||
sass
|
||||
scss
|
||||
styl
|
||||
log
|
||||
|
|
|
|||
|
|
@ -1,6 +1,3 @@
|
|||
package-lock.json
|
||||
pnpm-lock.yaml
|
||||
bun.lockb
|
||||
.gitignore
|
||||
.env
|
||||
.env.*
|
||||
|
|
|
|||
|
|
@ -1,102 +0,0 @@
|
|||
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<bool> = 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<Mutex<std::fs::File>>);
|
||||
|
||||
impl Write for MutexWriter {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
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 root = Path::new(".tailwindcss");
|
||||
let logs_dir = root.join("logs");
|
||||
if let Err(err) = std::fs::create_dir_all(&logs_dir) {
|
||||
eprintln!(
|
||||
"{} Failed to create {}, skipping debug logs ({err})",
|
||||
dim("[DEBUG]"),
|
||||
highlight(&logs_dir.display().to_string())
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure everything inside `.tailwindcss/` is ignored by git. The file is only created if it
|
||||
// doesn't exist yet, an existing `.gitignore` is left untouched.
|
||||
if let Ok(mut file) = std::fs::File::create_new(root.join(".gitignore")) {
|
||||
_ = file.write_all(b"*\n");
|
||||
}
|
||||
|
||||
let file_path = logs_dir.join(format!(
|
||||
"scanner-{}-{}.log",
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_millis())
|
||||
.unwrap_or(0),
|
||||
std::process::id()
|
||||
));
|
||||
let file = match OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&file_path)
|
||||
{
|
||||
Ok(file) => file,
|
||||
Err(err) => {
|
||||
eprintln!(
|
||||
"{} Failed to create {}, skipping debug logs ({err})",
|
||||
dim("[DEBUG]"),
|
||||
highlight(&file_path.display().to_string())
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let absolute_file_path = dunce::canonicalize(&file_path).unwrap_or_else(|_| file_path.clone());
|
||||
eprintln!(
|
||||
"{} Writing debug info to: {}\n",
|
||||
dim("[DEBUG]"),
|
||||
highlight(&absolute_file_path.display().to_string())
|
||||
);
|
||||
|
||||
let file = Arc::new(Mutex::new(file));
|
||||
|
||||
let writer: BoxMakeWriter = BoxMakeWriter::new({
|
||||
let file = file.clone();
|
||||
move || Box::new(MutexWriter(file.clone())) as Box<dyn Write + Send>
|
||||
});
|
||||
|
||||
_ = 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();
|
||||
}
|
||||
|
|
@ -1,952 +1,2 @@
|
|||
pub mod auto_source_detection;
|
||||
pub mod allowed_paths;
|
||||
pub mod detect_sources;
|
||||
pub mod init_tracing;
|
||||
pub mod sources;
|
||||
|
||||
use crate::extractor::{Extracted, Extractor};
|
||||
use crate::glob::optimize_patterns;
|
||||
use crate::scanner::detect_sources::resolve_globs;
|
||||
use crate::scanner::sources::{
|
||||
public_source_entries_to_private_source_entries, PublicSourceEntry, SourceEntry, Sources,
|
||||
};
|
||||
use crate::GlobEntry;
|
||||
use auto_source_detection::BINARY_EXTENSIONS_GLOB;
|
||||
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::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::SystemTime;
|
||||
use tracing::event;
|
||||
|
||||
// @source "some/folder"; // This is auto source detection
|
||||
// @source "some/folder/**/*"; // This is auto source detection
|
||||
// @source "some/folder/*.html"; // This is just a glob, but new files matching this should be included
|
||||
// @source "node_modules/my-ui-lib"; // Auto source detection but since node_modules is explicit we allow it
|
||||
// // Maybe could be considered `external(…)` automatically if:
|
||||
// // 1. It's git ignored but listed explicitly
|
||||
// // 2. It exists outside of the current working directory (do we know that?)
|
||||
//
|
||||
// @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
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ChangedContent {
|
||||
File(PathBuf, String),
|
||||
Content(String, String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ScanOptions {
|
||||
/// Base path to start scanning from
|
||||
pub base: Option<String>,
|
||||
|
||||
/// Glob sources
|
||||
pub sources: Vec<GlobEntry>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ScanResult {
|
||||
pub candidates: Vec<String>,
|
||||
pub files: Vec<String>,
|
||||
pub globs: Vec<GlobEntry>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Scanner {
|
||||
/// Content sources
|
||||
sources: Sources,
|
||||
|
||||
/// The walker to detect all files that we have to scan
|
||||
walker: Option<WalkBuilder>,
|
||||
|
||||
/// All found extensions
|
||||
extensions: FxHashSet<String>,
|
||||
|
||||
/// All files that we have to scan
|
||||
files: FxHashSet<PathBuf>,
|
||||
|
||||
/// All directories, sub-directories, etc… we saw during source detection
|
||||
dirs: FxHashSet<PathBuf>,
|
||||
|
||||
/// All generated globs, used for setting up watchers
|
||||
globs: Option<Vec<GlobEntry>>,
|
||||
|
||||
/// Track unique set of candidates
|
||||
candidates: FxHashSet<String>,
|
||||
|
||||
/// Track mtimes for files so incremental scans can skip unchanged files.
|
||||
mtimes: FxHashMap<PathBuf, SystemTime>,
|
||||
|
||||
/// Files that were scanned during the last `scan()` call.
|
||||
scanned_files: Vec<String>,
|
||||
|
||||
/// 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 {
|
||||
pub fn new(sources: Vec<PublicSourceEntry>) -> Self {
|
||||
init_tracing();
|
||||
|
||||
if *SHOULD_TRACE {
|
||||
event!(tracing::Level::INFO, "Provided sources:");
|
||||
for source in &sources {
|
||||
event!(tracing::Level::INFO, "Source: {:?}", source);
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
event!(tracing::Level::INFO, "Source: {:?}", source);
|
||||
}
|
||||
}
|
||||
|
||||
let walker = create_walker(&sources);
|
||||
|
||||
Self {
|
||||
sources,
|
||||
walker,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scan(&mut self) -> Vec<String> {
|
||||
self.sources_scanned = false;
|
||||
|
||||
let (scanned_blobs, css_files, files) = self.discover_sources();
|
||||
|
||||
self.extract_candidates(scanned_blobs, css_files);
|
||||
self.scanned_files = files;
|
||||
|
||||
// Return all candidates sorted
|
||||
let mut result = self.candidates.iter().cloned().collect::<Vec<_>>();
|
||||
result.par_sort_unstable();
|
||||
result
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub fn scan_content(&mut self, changed_content: Vec<ChangedContent>) -> Vec<String> {
|
||||
let (changed_files, changed_contents) =
|
||||
changed_content
|
||||
.into_iter()
|
||||
.partition::<Vec<_>, _>(|x| match x {
|
||||
ChangedContent::File(_, _) => true,
|
||||
ChangedContent::Content(_, _) => false,
|
||||
});
|
||||
|
||||
// Raw content can be parsed directly, no need to verify if the file exists and is allowed
|
||||
// to be scanned.
|
||||
let mut content_to_scan: Vec<ChangedContent> = changed_contents;
|
||||
|
||||
// Fully resolve all files
|
||||
let changed_files = changed_files
|
||||
.into_iter()
|
||||
.filter_map(|changed_content| match changed_content {
|
||||
ChangedContent::File(file, extension) => {
|
||||
let Ok(file) = dunce::canonicalize(file) else {
|
||||
return None;
|
||||
};
|
||||
Some(ChangedContent::File(file, extension))
|
||||
}
|
||||
_ => unreachable!(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let (known_files, mut new_unknown_files) = changed_files
|
||||
.into_iter()
|
||||
.partition::<Vec<_>, _>(|changed_file| match changed_file {
|
||||
ChangedContent::Content(_, _) => unreachable!(),
|
||||
ChangedContent::File(file, _) => self.files.contains(file),
|
||||
});
|
||||
|
||||
// All known files are allowed to be scanned
|
||||
content_to_scan.extend(known_files);
|
||||
|
||||
// Figure out if the new unknown files are allowed to be scanned
|
||||
if !new_unknown_files.is_empty() {
|
||||
if let Some(walk_builder) = &mut self.walker {
|
||||
for entry in walk_builder.build().filter_map(Result::ok) {
|
||||
let path = entry.path();
|
||||
if !path.is_file() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// The walked path can contain symlinks, while the changed files have already
|
||||
// been canonicalized. Lazily canonicalize the walked path so we can compare
|
||||
// the real paths as well.
|
||||
let mut canonical_path: Option<PathBuf> = None;
|
||||
|
||||
let mut drop_file_indexes = vec![];
|
||||
for (idx, changed_file) in new_unknown_files.iter().enumerate().rev() {
|
||||
let ChangedContent::File(file, _) = changed_file else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// 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.
|
||||
let matches = file == path
|
||||
|| (file.file_name() == path.file_name() && {
|
||||
if canonical_path.is_none() {
|
||||
canonical_path = dunce::canonicalize(path).ok();
|
||||
}
|
||||
canonical_path.as_deref() == Some(file.as_path())
|
||||
});
|
||||
|
||||
if matches {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove all files that we found on disk
|
||||
if !drop_file_indexes.is_empty() {
|
||||
drop_file_indexes.into_iter().for_each(|idx| {
|
||||
new_unknown_files.remove(idx);
|
||||
});
|
||||
}
|
||||
|
||||
// We can stop walking the file system if all files we are interested in have
|
||||
// been found.
|
||||
if new_unknown_files.is_empty() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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, blobs: Vec<Vec<u8>>, css_files: Vec<PathBuf>) -> Vec<String> {
|
||||
// Extract all candidates from the pre-read blobs
|
||||
let mut new_candidates = parse_all_blobs(blobs);
|
||||
|
||||
// Extract all CSS variables from the CSS files
|
||||
if !css_files.is_empty() {
|
||||
let css_variables = extract_css_variables(read_all_files(
|
||||
css_files
|
||||
.into_iter()
|
||||
.map(|file| ChangedContent::File(file, "css".into()))
|
||||
.collect(),
|
||||
));
|
||||
|
||||
new_candidates.extend(css_variables);
|
||||
}
|
||||
|
||||
// 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.extend(new_candidates.iter().cloned());
|
||||
|
||||
let mut result: Vec<String> = new_candidates.into_iter().collect();
|
||||
result.par_sort_unstable();
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub fn get_files(&mut self) -> Vec<String> {
|
||||
let _ = self.discover_sources();
|
||||
|
||||
self.files
|
||||
.par_iter()
|
||||
.filter_map(|x| x.clone().into_os_string().into_string().ok())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub fn get_scanned_files(&self) -> Vec<String> {
|
||||
self.scanned_files.clone()
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub fn get_globs(&mut self) -> Vec<GlobEntry> {
|
||||
if let Some(globs) = &self.globs {
|
||||
return globs.clone();
|
||||
}
|
||||
|
||||
let _ = self.discover_sources();
|
||||
|
||||
let mut globs = vec![];
|
||||
for source in self.sources.iter() {
|
||||
match source {
|
||||
SourceEntry::Auto { base } | SourceEntry::External { base } => {
|
||||
globs.extend(resolve_globs(
|
||||
base.to_path_buf(),
|
||||
&self.dirs,
|
||||
&self.extensions,
|
||||
));
|
||||
}
|
||||
SourceEntry::Pattern { base, pattern } => {
|
||||
globs.push(GlobEntry {
|
||||
base: base.to_string_lossy().to_string(),
|
||||
pattern: pattern.to_string(),
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Re-optimize the globs to reduce the number of patterns we have to scan.
|
||||
globs = optimize_patterns(&globs);
|
||||
|
||||
// Track the globs for subsequent calls
|
||||
self.globs = Some(globs.clone());
|
||||
|
||||
globs
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub fn get_normalized_sources(&self) -> Vec<GlobEntry> {
|
||||
self.sources
|
||||
.iter()
|
||||
.filter_map(|source| match source {
|
||||
SourceEntry::Auto { base } | SourceEntry::External { base } => Some(GlobEntry {
|
||||
base: base.to_string_lossy().to_string(),
|
||||
pattern: "**/*".to_string(),
|
||||
}),
|
||||
SourceEntry::Pattern { base, pattern } => Some(GlobEntry {
|
||||
base: base.to_string_lossy().to_string(),
|
||||
pattern: pattern.to_string(),
|
||||
}),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub fn get_candidates_with_positions(
|
||||
&mut self,
|
||||
changed_content: ChangedContent,
|
||||
) -> Vec<(String, usize)> {
|
||||
let content = read_changed_content(changed_content).unwrap_or_default();
|
||||
let original_content = &content;
|
||||
|
||||
// Workaround for legacy upgrades:
|
||||
//
|
||||
// `-[]` won't parse in the new parser (`[…]` must contain _something_), but we do need it
|
||||
// for people using `group-[]` (which we will later replace with `in-[.group]` instead).
|
||||
let content = content.replace("-[]", "XYZ");
|
||||
let offset = content.as_ptr() as usize;
|
||||
|
||||
let mut extractor = Extractor::new(&content[..]);
|
||||
|
||||
extractor
|
||||
.extract()
|
||||
.into_par_iter()
|
||||
.flat_map(|extracted| match extracted {
|
||||
Extracted::Candidate(s) => {
|
||||
let i = s.as_ptr() as usize - offset;
|
||||
let original = &original_content[i..i + s.len()];
|
||||
if original.contains_str("-[]") {
|
||||
return String::from_utf8(original.to_vec())
|
||||
.ok()
|
||||
.map(|candidate| (candidate, i));
|
||||
}
|
||||
|
||||
String::from_utf8(s.to_vec())
|
||||
.ok()
|
||||
.map(|candidate| (candidate, i))
|
||||
}
|
||||
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
fn discover_sources(&mut self) -> (Vec<Vec<u8>>, Vec<PathBuf>, Vec<String>) {
|
||||
if self.sources_scanned {
|
||||
return (vec![], vec![], vec![]);
|
||||
}
|
||||
self.sources_scanned = true;
|
||||
|
||||
let Some(walker) = &mut self.walker else {
|
||||
return (vec![], 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<PathBuf> = vec![];
|
||||
let mut content_paths: Vec<(PathBuf, String)> = vec![];
|
||||
let mut changed_files = vec![];
|
||||
|
||||
// Fresh state
|
||||
self.files.clear();
|
||||
self.dirs.clear();
|
||||
self.extensions.clear();
|
||||
self.globs = None;
|
||||
|
||||
// Cache canonicalized folders in case a file itself is not symlinked, but any of the parent
|
||||
// folders are symlinked.
|
||||
let mut cached_canonical_dirs: FxHashMap<PathBuf, PathBuf> = FxHashMap::default();
|
||||
|
||||
for entry in all_entries {
|
||||
match entry {
|
||||
WalkEntry::Dir(path) => {
|
||||
self.dirs.insert(path);
|
||||
}
|
||||
WalkEntry::File {
|
||||
path,
|
||||
mtime,
|
||||
is_symlink,
|
||||
} => {
|
||||
// Deduplicate: parallel walk can visit the same file from multiple threads
|
||||
if !self.files.insert(path.clone()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Track canonicalized paths in addition to potentially symlinked file paths
|
||||
let canonical = if is_symlink {
|
||||
dunce::canonicalize(&path).ok()
|
||||
} else {
|
||||
path.parent().and_then(|parent| {
|
||||
// Perf: cache the canonicalized parent path such that sibling files don't
|
||||
// have to canonicalize over and over again.
|
||||
let canonical_parent = cached_canonical_dirs
|
||||
.entry(parent.to_path_buf())
|
||||
.or_insert_with(|| {
|
||||
dunce::canonicalize(parent)
|
||||
.unwrap_or_else(|_| parent.to_path_buf())
|
||||
});
|
||||
|
||||
if canonical_parent.as_path() != parent {
|
||||
path.file_name()
|
||||
.map(|file_name| canonical_parent.join(file_name))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
if let Some(canonical) = canonical {
|
||||
if canonical != path {
|
||||
self.files.insert(canonical);
|
||||
}
|
||||
}
|
||||
let extension = path
|
||||
.extension()
|
||||
.and_then(|x| x.to_str())
|
||||
.unwrap_or_default()
|
||||
.to_owned();
|
||||
|
||||
self.extensions.insert(extension.to_owned());
|
||||
|
||||
// On incremental scans, check mtime to skip unchanged files.
|
||||
// On the first scan, track mtimes while still scanning every file.
|
||||
let changed = if self.has_scanned_once {
|
||||
match mtime {
|
||||
Some(mtime) => {
|
||||
let prev = self.mtimes.insert(path.clone(), mtime);
|
||||
prev.is_none_or(|prev| prev != mtime)
|
||||
}
|
||||
None => true,
|
||||
}
|
||||
} else {
|
||||
if let Some(mtime) = mtime {
|
||||
self.mtimes.insert(path.clone(), mtime);
|
||||
}
|
||||
|
||||
true
|
||||
};
|
||||
|
||||
if !changed {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Ok(file) = path.clone().into_os_string().into_string() {
|
||||
changed_files.push(file);
|
||||
}
|
||||
|
||||
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" => css_files.push(path),
|
||||
_ => content_paths.push((path, extension)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure `mtimes` don't include stale files
|
||||
self.mtimes.retain(|path, _| self.files.contains(path));
|
||||
|
||||
// Read + preprocess all discovered files in parallel
|
||||
let scanned_blobs: Vec<Vec<u8>> = 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;
|
||||
}
|
||||
|
||||
changed_files.par_sort_unstable();
|
||||
|
||||
(scanned_blobs, css_files, changed_files)
|
||||
}
|
||||
}
|
||||
|
||||
fn read_changed_content(c: ChangedContent) -> Option<Vec<u8>> {
|
||||
let (content, extension) = match c {
|
||||
ChangedContent::File(file, extension) => match std::fs::read(&file) {
|
||||
Ok(content) => {
|
||||
event!(tracing::Level::INFO, "Reading {:?}", file);
|
||||
(content, extension)
|
||||
}
|
||||
Err(e) => {
|
||||
event!(tracing::Level::ERROR, "Failed to read file: {:?}", e);
|
||||
return None;
|
||||
}
|
||||
},
|
||||
|
||||
ChangedContent::Content(contents, extension) => (contents.into_bytes(), extension),
|
||||
};
|
||||
|
||||
Some(pre_process_input(content, &extension))
|
||||
}
|
||||
|
||||
pub fn pre_process_input(content: Vec<u8>, extension: &str) -> Vec<u8> {
|
||||
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" | "jsonl" | "ndjson" => 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),
|
||||
"tt" | "tt2" | "tx" => TemplateToolkit.process(&content),
|
||||
"twig" => Twig.process(&content),
|
||||
"vue" => Vue.process(&content),
|
||||
_ => content,
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
fn read_all_files(changed_content: Vec<ChangedContent>) -> Vec<Vec<u8>> {
|
||||
event!(
|
||||
tracing::Level::INFO,
|
||||
"Reading {:?} file(s)",
|
||||
changed_content.len()
|
||||
);
|
||||
|
||||
changed_content
|
||||
.into_par_iter()
|
||||
.filter_map(read_changed_content)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
fn extract_css_variables(blobs: Vec<Vec<u8>>) -> FxHashSet<String> {
|
||||
extract(blobs, |mut extractor| {
|
||||
extractor.extract_variables_from_css()
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
fn parse_all_blobs(blobs: Vec<Vec<u8>>) -> FxHashSet<String> {
|
||||
extract(blobs, |mut extractor| extractor.extract())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
fn extract<H>(blobs: Vec<Vec<u8>>, handle: H) -> FxHashSet<String>
|
||||
where
|
||||
H: Fn(Extractor) -> Vec<Extracted> + std::marker::Sync,
|
||||
{
|
||||
blobs
|
||||
.par_iter()
|
||||
.flat_map(|blob| blob.par_split(|x| *x == b'\n'))
|
||||
.filter_map(|blob| {
|
||||
if blob.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let extracted = handle(crate::extractor::Extractor::new(blob));
|
||||
if extracted.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(FxHashSet::from_iter(extracted.into_iter().map(
|
||||
|x| match x {
|
||||
Extracted::Candidate(bytes) => bytes,
|
||||
Extracted::CssVariable(bytes) => bytes,
|
||||
},
|
||||
)))
|
||||
})
|
||||
.reduce(Default::default, |mut a, b| {
|
||||
a.extend(b);
|
||||
a
|
||||
})
|
||||
.into_iter()
|
||||
.filter_map(|s| String::from_utf8(s.to_vec()).ok())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum WalkEntry {
|
||||
Dir(PathBuf),
|
||||
File {
|
||||
path: PathBuf,
|
||||
mtime: Option<SystemTime>,
|
||||
|
||||
/// Whether the path itself is a symlink
|
||||
is_symlink: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<ignore::DirEntry> for WalkEntry {
|
||||
fn from(entry: ignore::DirEntry) -> Self {
|
||||
let is_dir = entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false);
|
||||
let is_symlink = entry.path_is_symlink();
|
||||
let path = entry.into_path();
|
||||
|
||||
if is_dir {
|
||||
WalkEntry::Dir(path)
|
||||
} else {
|
||||
let mtime = path.metadata().ok().and_then(|m| m.modified().ok());
|
||||
WalkEntry::File {
|
||||
path,
|
||||
mtime,
|
||||
is_symlink,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<WalkEntry> {
|
||||
walker
|
||||
.build()
|
||||
.filter_map(Result::ok)
|
||||
.map(WalkEntry::from)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// 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<WalkEntry> {
|
||||
struct FlushOnDrop {
|
||||
local: Vec<WalkEntry>,
|
||||
shared: Arc<Mutex<Vec<WalkEntry>>>,
|
||||
}
|
||||
|
||||
impl Drop for FlushOnDrop {
|
||||
fn drop(&mut self) {
|
||||
if !self.local.is_empty() {
|
||||
self.shared.lock().unwrap().append(&mut self.local);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let collected: Arc<Mutex<Vec<WalkEntry>>> = Arc::new(Mutex::new(vec![]));
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
buf.local.push(WalkEntry::from(entry));
|
||||
|
||||
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.
|
||||
///
|
||||
/// 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<WalkBuilder> {
|
||||
let mut other_roots: FxHashSet<&PathBuf> = FxHashSet::default();
|
||||
let mut first_root: Option<&PathBuf> = None;
|
||||
|
||||
let mut ignores: Vec<(&PathBuf, Vec<String>)> = Default::default();
|
||||
let mut emit = |base, pattern| match ignores.last_mut() {
|
||||
Some((prev_base, patterns)) if *prev_base == base => {
|
||||
patterns.push(pattern);
|
||||
}
|
||||
_ => {
|
||||
ignores.push((base, vec![pattern]));
|
||||
}
|
||||
};
|
||||
|
||||
for source in sources.iter() {
|
||||
match source {
|
||||
SourceEntry::Auto { base } => {
|
||||
if first_root.is_none() {
|
||||
first_root = Some(base);
|
||||
} else {
|
||||
other_roots.insert(base);
|
||||
}
|
||||
}
|
||||
SourceEntry::Pattern { base, pattern } => {
|
||||
let pattern = pattern.to_owned();
|
||||
|
||||
if first_root.is_none() {
|
||||
first_root = Some(base);
|
||||
} else {
|
||||
other_roots.insert(base);
|
||||
}
|
||||
|
||||
if !pattern.contains("**") {
|
||||
// Specific patterns should take precedence even over git-ignored files:
|
||||
emit(base, format!("!{}", pattern));
|
||||
} else {
|
||||
// Assumption: the pattern we receive will already be brace expanded. So
|
||||
// `*.{html,jsx}` will result in two separate patterns: `*.html` and `*.jsx`.
|
||||
if let Some(extension) = Path::new(&pattern).extension() {
|
||||
// Extend auto source detection to include the extension
|
||||
emit(base, format!("!*.{}", extension.to_string_lossy()));
|
||||
}
|
||||
}
|
||||
}
|
||||
SourceEntry::Ignored { base, pattern } => {
|
||||
emit(base, pattern.to_owned());
|
||||
}
|
||||
SourceEntry::External { base } => {
|
||||
if first_root.is_none() {
|
||||
first_root = Some(base);
|
||||
} else {
|
||||
other_roots.insert(base);
|
||||
}
|
||||
|
||||
// External sources should take precedence even over git-ignored files:
|
||||
emit(base, "!/**/*".to_owned());
|
||||
|
||||
// External sources should still disallow binary extensions:
|
||||
emit(base, BINARY_EXTENSIONS_GLOB.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut builder = WalkBuilder::new(first_root?);
|
||||
|
||||
// We have to follow symlinks
|
||||
builder.follow_links(true);
|
||||
|
||||
// Scan hidden files / directories
|
||||
builder.hidden(false);
|
||||
|
||||
// Don't respect global gitignore files
|
||||
builder.git_global(false);
|
||||
|
||||
// By default, allow .gitignore files to be used regardless of whether or not
|
||||
// a .git directory is present. This is an optimization for when projects
|
||||
// are first created and may not be in a git repo yet.
|
||||
builder.require_git(false);
|
||||
|
||||
// If we are in a git repo then require it to ensure that only rules within
|
||||
// the repo are used. For example, we don't want to consider a .gitignore file
|
||||
// in the user's home folder if we're in a git repo.
|
||||
//
|
||||
// The alternative is using a call like `.parents(false)` but that will
|
||||
// prevent looking at parent directories for .gitignore files from within
|
||||
// the repo and that's not what we want.
|
||||
//
|
||||
// For example, in a project with this structure:
|
||||
//
|
||||
// home
|
||||
// .gitignore
|
||||
// my-project
|
||||
// .gitignore
|
||||
// apps
|
||||
// .gitignore
|
||||
// web
|
||||
// {root}
|
||||
//
|
||||
// We do want to consider all .gitignore files listed:
|
||||
// - home/.gitignore
|
||||
// - my-project/.gitignore
|
||||
// - my-project/apps/.gitignore
|
||||
//
|
||||
// However, if a repo is initialized inside my-project then only the following
|
||||
// make sense for consideration:
|
||||
// - my-project/.gitignore
|
||||
// - my-project/apps/.gitignore
|
||||
//
|
||||
// Setting the require_git(true) flag conditionally allows us to do this.
|
||||
for parent in first_root?.ancestors() {
|
||||
if parent.join(".git").exists() {
|
||||
builder.require_git(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for root in other_roots {
|
||||
builder.add(root);
|
||||
}
|
||||
|
||||
// Setup auto source detection rules
|
||||
for ignore in auto_source_detection::RULES.iter() {
|
||||
builder.add_gitignore(ignore.clone());
|
||||
}
|
||||
|
||||
// Setup ignores based on `@source` definitions
|
||||
for (base, patterns) in ignores {
|
||||
let mut ignore_builder = GitignoreBuilder::new(base);
|
||||
for pattern in patterns {
|
||||
ignore_builder.add_line(None, &pattern).unwrap();
|
||||
}
|
||||
let ignore = ignore_builder.build().unwrap();
|
||||
builder.add_gitignore(ignore);
|
||||
}
|
||||
|
||||
// Pre-compute source matching data to avoid allocations in the hot filter_entry path
|
||||
let auto_bases: Vec<PathBuf> = sources
|
||||
.iter()
|
||||
.filter_map(|source| match source {
|
||||
SourceEntry::Auto { base } | SourceEntry::External { base } => Some(base.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let pattern_sources: Vec<(PathBuf, String)> = sources
|
||||
.iter()
|
||||
.filter_map(|source| match source {
|
||||
SourceEntry::Pattern { base, pattern } => Some((base.into(), pattern.into())),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Source pattern matching filter (lock-free, safe for parallel walking)
|
||||
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 base in &auto_bases {
|
||||
if path.starts_with(base) {
|
||||
matches = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{ChangedContent, Scanner};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_positions() {
|
||||
let mut scanner = Scanner::new(vec![]);
|
||||
|
||||
for (input, expected) in [
|
||||
// Before migrations
|
||||
(
|
||||
r#"<div class="!tw__flex sm:!tw__block tw__bg-gradient-to-t flex tw:[color:red] group-[]:tw__flex"#,
|
||||
vec![
|
||||
("class".to_string(), 5),
|
||||
("!tw__flex".to_string(), 12),
|
||||
("sm:!tw__block".to_string(), 22),
|
||||
("tw__bg-gradient-to-t".to_string(), 36),
|
||||
("flex".to_string(), 57),
|
||||
("tw:[color:red]".to_string(), 62),
|
||||
("group-[]:tw__flex".to_string(), 77),
|
||||
],
|
||||
),
|
||||
// After migrations
|
||||
(
|
||||
r#"<div class="tw:flex! tw:sm:block! tw:bg-linear-to-t flex tw:[color:red] tw:in-[.tw\:group]:flex"></div>"#,
|
||||
vec![
|
||||
("class".to_string(), 5),
|
||||
("tw:flex!".to_string(), 12),
|
||||
("tw:sm:block!".to_string(), 21),
|
||||
("tw:bg-linear-to-t".to_string(), 34),
|
||||
("flex".to_string(), 52),
|
||||
("tw:[color:red]".to_string(), 57),
|
||||
("tw:in-[.tw\\:group]:flex".to_string(), 72),
|
||||
],
|
||||
),
|
||||
] {
|
||||
let candidates = scanner.get_candidates_with_positions(ChangedContent::Content(
|
||||
input.to_string(),
|
||||
"html".into(),
|
||||
));
|
||||
assert_eq!(candidates, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue