PHPackages                             yellow-twins/fluid-lens - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. yellow-twins/fluid-lens

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

yellow-twins/fluid-lens
=======================

Static analyzer for Fluid templates: finds duplicated markup that should become Partials, plus accessibility and best-practice sniffs.

v0.8.0(1mo ago)13168↓46.8%GPL-2.0-or-laterPHPPHP ^8.1CI failing

Since Jul 3Pushed 1mo agoCompare

[ Source](https://github.com/yellow-twins/fluid-lens)[ Packagist](https://packagist.org/packages/yellow-twins/fluid-lens)[ RSS](/packages/yellow-twins-fluid-lens/feed)WikiDiscussions main Synced 1w ago

READMEChangelogDependencies (7)Versions (21)Used By (0)

fluid-lens
==========

[](#fluid-lens)

[![Packagist Version](https://camo.githubusercontent.com/92e8d11965d7ee09113a37fcef1907d24c76224d615d6bb98a641863ec589432/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f79656c6c6f772d7477696e732f666c7569642d6c656e73)](https://packagist.org/packages/yellow-twins/fluid-lens)[![QA](https://github.com/yellow-twins/fluid-lens/actions/workflows/qa.yml/badge.svg)](https://github.com/yellow-twins/fluid-lens/actions/workflows/qa.yml)[![PHP Version](https://camo.githubusercontent.com/6a609c7e3aae7a2233807e512671bc9e129a28081ab416d1c01b33d8bee48cde/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f7068702d762f79656c6c6f772d7477696e732f666c7569642d6c656e73)](https://packagist.org/packages/yellow-twins/fluid-lens)[![License](https://camo.githubusercontent.com/c7740e77b4e906d413de23b7669afd93af6b07a532cb5651f5970fefa427772e/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f79656c6c6f772d7477696e732f666c7569642d6c656e73)](LICENSE)

> Static analysis for Fluid templates — find markup that should be a Partial, and catch accessibility &amp; best-practice problems, straight from the command line.

`fluid-lens` reads your Fluid templates and *x-rays* them: it looks for duplicated markup structures that should be extracted into reusable Partials, and (from a later milestone) flags accessibility and best-practice violations — the kind of review you would otherwise do by hand, one template at a time.

It runs **standalone** (no TYPO3 instance required) as a Composer tool, and ships an optional TYPO3 command wrapper.

Status
------

[](#status)

**Usable today.** fluid-lens finds exact duplicated structures and near-duplicates across your templates, and lints them for accessibility (WCAG) and best-practice problems — with inline suppression and a baseline for adopting it on an existing project. See the roadmap for what's next.

Why
---

[](#why)

Fluid is built around Partials so that markup — images, accordions, tiles, list views, navigations — lives in *one* place and is reused everywhere. In practice most projects re-implement the same structures inline across many templates. `fluid-lens` makes that duplication visible and measurable, like PHPStan or PHP\_CodeSniffer do for PHP.

Requirements
------------

[](#requirements)

- PHP 8.1+
- ext-dom, ext-json

Installation
------------

[](#installation)

```
composer require --dev yellow-twins/fluid-lens
```

Or, for development inside this repository, the package lives in `packages/fluid-lens/` and is installed on its own:

```
cd packages/fluid-lens
composer install
```

Usage
-----

[](#usage)

### Analyse for duplicated structures

[](#analyse-for-duplicated-structures)

Scan a file or a whole directory for markup that repeats and should become a Partial:

```
vendor/bin/fluid-lens analyze path/to/Templates/
```

Tune the sensitivity, or emit JSON for CI and tooling:

```
vendor/bin/fluid-lens analyze path/to/Templates/ --min-elements=5 --min-occurrences=3
vendor/bin/fluid-lens analyze path/to/Templates/ --json
```

The command exits non-zero when duplicates are found, so it can gate a pipeline.

### Find near-duplicate structures

[](#find-near-duplicate-structures)

Find blocks that are *almost* identical — differing by a node or an attribute — and could share one Partial with the differences passed as arguments:

```
vendor/bin/fluid-lens similar path/to/Templates/
vendor/bin/fluid-lens similar path/to/Templates/ --threshold=0.85 --json
```

Similarity is measured with pq-gram distance, a fast approximation of tree edit distance. Structures are clustered by transitive similarity above the threshold.

### Check project-wide consistency

[](#check-project-wide-consistency)

Over time projects drift: the same slider gets copied around, and different libraries or icon sets creep in. The `consistency` command runs a set of project-wide checks and fails when competing implementations are mixed:

```
vendor/bin/fluid-lens consistency packages/
vendor/bin/fluid-lens consistency packages/ --only=sliders     # just one check
vendor/bin/fluid-lens consistency --list-checks
```

Built-in checks (selectable with `--only` / `--exclude`, wildcards allowed):

CheckDetects`sliders`Slider/carousel libraries (Swiper, Slick, Glide, Splide, Owl, Flickity, Keen, Tiny Slider)`icons`Icon sets (Font Awesome, Bootstrap Icons, Material, Ionicons, Feather, Remix, Boxicons)`css`CSS frameworks (Bootstrap, Tailwind, Bulma, Foundation) via distinctive signatures`js-framework`JS interaction frameworks (Alpine, Vue, htmx, Stimulus, Turbo) via their attributes`lightbox`Lightbox/gallery libraries (Fancybox, GLightbox, Magnific Popup, Lightgallery, PhotoSwipe)`animation`Animation libraries (Animate.css, WOW.js, AOS)`lazyload`Lazy-loading strategy (native `loading` vs lazysizes/lozad/vanilla-lazyload)`maps`Map libraries (Leaflet, Mapbox GL, OpenLayers, Google Maps)`video-player`Video players (Plyr, Video.js, MediaElement.js, JW Player, Flowplayer)`grid`Grid/masonry libraries (Isotope, Masonry, Packery, Muuri)`tooltip`Tooltip libraries (Tippy.js, Foundation, hint.css, microtip)`cookie-consent`Cookie-consent solutions (Cookiebot, OneTrust, Osano, Klaro, Borlabs)`namespace-style`Fluid namespace declared on `` vs inline `{namespace ...}``render-style`Partial render as `` tag vs inline `{f:render(...)}``translate-style`Translation as `` tag vs inline `{f:translate(...)}``image-approach``` vs `` vs raw `` with a dynamic `src``link-approach`Fluid link ViewHelper vs raw `` with a dynamic `href`The last five are Fluid-specific: they compare *how* the project writes Fluid (tag vs inline syntax, ViewHelper vs raw markup), not which CSS library it uses. `image-approach`/`link-approach` only count a raw ``/`` when its `src`/`href` is a Fluid expression — a static asset or external link is not a competing approach.

Duplicated *markup* (e.g. the same slider copied into five templates) is caught by `analyze` and `similar` — extract it into one shared Partial.

### Check accessibility (WCAG) and best practices

[](#check-accessibility-wcag-and-best-practices)

Scan templates for accessibility and best-practice problems in seconds, instead of opening every page in a browser:

```
vendor/bin/fluid-lens lint path/to/Templates/
vendor/bin/fluid-lens lint path/to/Templates/ --json
```

By default the command exits non-zero on any error or warning (notices are advisory). Adjust the gate with `--fail-on=error|warning|notice|never` — for example fail only on errors while you adopt it:

```
vendor/bin/fluid-lens lint packages/ --fail-on=error
```

What it checks statically:

RuleSeverityWCAG`wcag.img-alt` — image without an `alt` attributeerror1.1.1 (A)`wcag.link-name` — link with no discernible text (icon-only)error2.4.4 (A)`wcag.button-name` — button with no discernible text (icon-only)error4.1.2 (A)`wcag.duplicate-id` — duplicate `id` in one documenterror4.1.1 (A)`wcag.form-label` — control with no way to be labelledwarning4.1.2 (A)`wcag.label-for` — `` with no matching id (skips Fluid forms)notice1.3.1 (A)`wcag.html-lang` — `` without `lang`warning3.1.1 (A)`wcag.positive-tabindex` — `tabindex` greater than 0warning2.4.3 (A)`wcag.table-header` — data table without ``warning1.3.1 (A)`wcag.empty-heading` — heading with no textwarning1.3.1 (A)`wcag.meta-viewport` — viewport meta tag that blocks zoomwarning1.4.4 (AA)`wcag.aria-role` — unknown WAI-ARIA `role` valuewarning4.1.2 (A)`wcag.aria-attr` — unknown `aria-*` attribute (typo)warning4.1.2 (A)`wcag.aria-hidden-focusable` — `aria-hidden` on a focusable elementwarning4.1.2 (A)`wcag.aria-expanded-role` — `aria-expanded` on a non-interactive element (broken accordion)warning4.1.2 (A)`wcag.tab-selected` — `role="tab"` without `aria-selected`warning4.1.2 (A)`wcag.tablist-tab` — `role="tablist"` without any `role="tab"`warning1.3.1 (A)`wcag.iframe-title` — `` without a `title`warning4.1.2 (A)`wcag.media-autoplay` — audio/unmuted video that autoplays soundwarning1.4.2 (A)`wcag.heading-order` — heading levels skippedwarning1.3.1 (A)`wcag.input-image-alt` — `` without alterror1.1.1 (A)`wcag.alt-filename` — alt text that is just a file namewarning1.1.1 (A)`wcag.nested-interactive` — a control nested inside anotherwarning4.1.2 (A)`wcag.fieldset-legend` — `` without a ``warning1.3.1 (A)`wcag.list-structure` — ``/`` with a non-`` childwarning1.3.1 (A)`wcag.th-empty` — empty `` header cellwarning1.3.1 (A)`wcag.role-required-attr` — ARIA role missing its required statewarning4.1.2 (A)`wcag.video-captions` — `` without a captions trackwarning1.2.2 (A)`wcag.marquee-blink` — ``/`` moving contentwarning2.2.2 (A)`wcag.link-generic-text` — non-descriptive link text ("read more")notice2.4.4 (A)`wcag.lang-valid` — invalid `lang` attribute valuewarning3.1.1 (A)`wcag.label-empty` — `` with no textwarning3.3.2 (A)`wcag.aria-boolean` — boolean ARIA attribute with an invalid valuewarning4.1.2 (A)`wcag.dir-valid` — invalid `dir` attribute valuewarning1.3.2 (A)`wcag.meta-refresh` — timed ``warning2.2.1 (A)`wcag.summary-details` — `` outside a ``warning1.3.1 (A)`wcag.scope-value` — invalid table-cell `scope` valuewarning1.3.1 (A)`wcag.abbr-title` — `` without a `title`notice3.1.4 (**AAA**)`wcag.autocomplete-token` — invalid `autocomplete` autofill tokenwarning1.3.5 (AA)`wcag.lang-xml-mismatch` — `lang` and `xml:lang` disagreewarning3.1.2 (AA)`wcag.accesskey-duplicate` — duplicate `accesskey` in one documentwarning2.1.1 (A)`wcag.target-blank-purpose` — `target="_blank"` without announcing the new tabnotice3.2.5 (**AAA**)`wcag.aria-controls-target` — `aria-controls` with no matching id in the templatenotice4.1.2 (A)`wcag.aria-ref-target` — `aria-labelledby`/`aria-describedby` with no matching idnotice1.3.1 (A)`wcag.nav-label` — multiple navigation landmarks without labelsnotice1.3.1 (A)`markup.picture-img` — `` without an `` fallbackwarning—`markup.source-srcset` — `` in `` without `srcset`warning—`style.inline` — inline `style` attributenotice—`partial.inline-svg` — inline `` to extract into an Icon partialnotice—`image.prefer-fluid` — raw `` instead of ``notice—`link.target-blank-rel` — `target="_blank"` without `rel="noopener"`notice—Pick which rules run with `--only` / `--exclude`, or see them all (grouped) with `--list-rules`. A trailing `*` matches a prefix:

```
vendor/bin/fluid-lens lint --list-rules
vendor/bin/fluid-lens lint path/ --only=wcag.*                 # accessibility only
vendor/bin/fluid-lens lint path/ --exclude=style.*,partial.*   # skip advisory notices
vendor/bin/fluid-lens lint path/ --only=wcag.img-alt,wcag.button-name
```

**Honest by design:** criteria that genuinely need a rendered page — colour contrast, runtime focus order, reflow — are *not* silently passed. The report states plainly that they must be verified with a runtime tool (axe, Lighthouse). A static pass is a fast first line of defence, not a replacement for those.

### Suppress a block inline

[](#suppress-a-block-inline)

Mark a block that should intentionally stay inline with a comment on the line before it — it is then excluded from every analysis:

```
{# @fluidlint-ignore this one really is a one-off #}
…
```

### Adopt on an existing project with a baseline

[](#adopt-on-an-existing-project-with-a-baseline)

Record all current duplication once, then only see *new* duplication from then on:

```
# Freeze what exists today
vendor/bin/fluid-lens analyze path/to/Templates/ --generate-baseline

# Later runs report only duplication that is new or has grown
vendor/bin/fluid-lens analyze path/to/Templates/ --baseline=fluid-lens-baseline.json
```

`lint` has the same baseline (by rule, file and message, ignoring line numbers) — freeze today's accessibility debt and only fail on new findings:

```
vendor/bin/fluid-lens lint path/to/Templates/ --generate-baseline
vendor/bin/fluid-lens lint path/to/Templates/ --baseline=fluid-lens-lint-baseline.json
```

`consistency` has one too — accept today's mix and only fail on new drift. It records the set of variants each check currently sees, then fails when a variant appears that the baseline does not list (a third library joining an already-mixed check, or a previously consistent check becoming mixed):

```
vendor/bin/fluid-lens consistency path/to/Templates/ --generate-baseline
vendor/bin/fluid-lens consistency path/to/Templates/ --baseline=fluid-lens-consistency-baseline.json
```

### Parse a single template

[](#parse-a-single-template)

Dump the structural tree the analyzer sees — handy for understanding a finding:

```
vendor/bin/fluid-lens parse path/to/Template.html
vendor/bin/fluid-lens parse path/to/Template.html --json
```

### Running through Composer

[](#running-through-composer)

You don't have to call the binary directly. In a project that requires fluid-lens, add a Composer script referencing the command by name — Composer puts `vendor/bin` on the path for scripts:

```
{
    "scripts": {
        "lint:fluid": "fluid-lens analyze packages/",
        "lint:fluid-similar": "fluid-lens similar packages/"
    }
}
```

```
composer lint:fluid
composer exec fluid-lens -- analyze packages/   # ad-hoc, without a script
```

This repository itself ships `composer analyze`, `composer similar` and `composer lint` shortcuts (see `composer.json`), e.g. `composer analyze -- path/`.

Configuration
-------------

[](#configuration)

Instead of repeating options on every run, drop a `fluid-lens.php` in your project root (auto-discovered, or point at it with `--config`). Command-line options always win over the file, which in turn wins over the built-in defaults.

```
