yellow-twins/fluid-lens
Composer 安装命令:
composer require yellow-twins/fluid-lens
包简介
Static analyzer for Fluid templates: finds duplicated markup that should become Partials, plus accessibility and best-practice sniffs.
README 文档
README
Static analysis for Fluid templates — find markup that should be a Partial, and catch accessibility & 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
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
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
- PHP 8.1+
- ext-dom, ext-json
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
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 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 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:
| Rule | Severity | WCAG |
|---|---|---|
wcag.img-alt — image without an alt attribute |
error | 1.1.1 (A) |
wcag.link-name — link with no discernible text (icon-only) |
error | 2.4.4 (A) |
wcag.button-name — button with no discernible text (icon-only) |
error | 4.1.2 (A) |
wcag.duplicate-id — duplicate id in one document |
error | 4.1.1 (A) |
wcag.form-label — control with no way to be labelled |
warning | 4.1.2 (A) |
wcag.html-lang — <html> without lang |
warning | 3.1.1 (A) |
wcag.positive-tabindex — tabindex greater than 0 |
warning | 2.4.3 (A) |
wcag.table-header — data table without <th> |
warning | 1.3.1 (A) |
wcag.empty-heading — heading with no text |
warning | 1.3.1 (A) |
wcag.meta-viewport — viewport meta tag that blocks zoom |
warning | 1.4.4 (AA) |
wcag.aria-role — unknown WAI-ARIA role value |
warning | 4.1.2 (A) |
wcag.aria-attr — unknown aria-* attribute (typo) |
warning | 4.1.2 (A) |
wcag.aria-hidden-focusable — aria-hidden on a focusable element |
warning | 4.1.2 (A) |
wcag.iframe-title — <iframe> without a title |
warning | 4.1.2 (A) |
wcag.media-autoplay — audio/unmuted video that autoplays sound |
warning | 1.4.2 (A) |
wcag.heading-order — heading levels skipped |
warning | 1.3.1 (A) |
markup.picture-img — <picture> without an <img> fallback |
warning | — |
markup.source-srcset — <source> in <picture> without srcset |
warning | — |
style.inline — inline style attribute |
notice | — |
partial.inline-svg — inline <svg> to extract into an Icon partial |
notice | — |
image.prefer-fluid — raw <img> instead of <f:image> |
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
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 #}
<div class="special-case">…</div>
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
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
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
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.
<?php // fluid-lens.php return [ 'paths' => ['packages/'], // scanned when no path is given 'lint' => ['exclude' => ['style.inline']], // or 'only' => [...] 'analyze' => ['minElements' => 3, 'minOccurrences' => 2], 'similar' => ['threshold' => 0.85, 'minElements' => 4], ];
With that in place, fluid-lens lint (no arguments) scans the configured paths with
the configured rules. A ready-to-copy fluid-lens.dist.php ships
with the package.
Command reference
All scanning commands accept --config and, when no path is given, fall back to the
configured paths.
| Command | Purpose | Key options |
|---|---|---|
analyze |
Find exact duplicated structures | --min-elements, --min-occurrences, --baseline, --generate-baseline, --json |
similar |
Find near-duplicate structures | --threshold, --min-elements, --json |
lint |
Check accessibility (WCAG) & best practices | --only, --exclude, --fail-on, --baseline, --generate-baseline, --list-rules, --json, --sarif |
parse |
Dump one template's structural node tree | --json |
Exit codes
0— no findings (or all findings covered by the baseline)1— findings were reported, or the path contained no templates
This lets any command gate a CI pipeline: a clean run passes, new duplication fails.
Using it in TYPO3
The standalone binary works inside any TYPO3 project out of the box, and native
vendor/bin/typo3 fluidlens:* commands are available as an option. See
docs/typo3.md.
How it works
Fluid is not valid XML, and Fluid itself only parses ViewHelpers — the surrounding
HTML is opaque text to it. To reason about structure, fluid-lens parses templates
with a tolerant HTML5 parser and:
- keeps Fluid ViewHelper tags (
<f:image>,<f:render>, …) as ordinary elements, - treats
{expressions}as opaque text, - normalises Fluid's XML-style self-closing tags (
<f:image ... />) so they don't wrongly swallow their following siblings the way an HTML5 parser would.
The result is a small, framework-agnostic node tree (src/Parser/Node.php) that later
stages fingerprint and compare.
Roadmap
Milestone 1 — Parser foundation ✅
Parse a template into a node tree; parse command with text and JSON output.
Milestone 2 — Exact clones ✅ (current)
Canonical structure hash (ignoring class values, text and variables) groups
identical structures across templates; the analyze command reports them with a
structural preview, suppressing groups fully contained in a larger one.
Milestone 3 — Near-duplicates ✅ (current)
pq-gram similarity catches structures that differ only slightly, clustered by a
divergence score; the similar command reports each cluster of variants.
Milestone 4 — Ignores & baseline ✅ (current)
{# @fluidlint-ignore #} inline suppression and a PHPStan-style baseline
(--generate-baseline / --baseline) for adopting the tool on existing projects.
Milestone 5 — TYPO3 command wrapper + docs ✅
Native fluidlens:* TYPO3 commands (Configuration/Commands.php) and full docs.
Sniffs — accessibility & best practices ✅
The lint command ships a WCAG markup module (missing alt, icon-only links,
duplicate ids, unlabelled controls, heading jumps, tables without headers, …) plus
best-practice sniffs, with criteria that need a rendered page reported as
needs runtime check rather than silently passed.
Next — more sniffs: accordions/tiles/navigation component patterns, <picture>
best practice, expanded ARIA validation.
Later — Auto-fix: generate the suggested Partial and the <f:render> replacement.
Continuous integration
Every command exits non-zero on findings, so it gates a pipeline out of the box:
vendor/bin/fluid-lens analyze packages/ --baseline=fluid-lens-baseline.json vendor/bin/fluid-lens lint packages/ --exclude=style.inline,partial.inline-svg
lint --sarif emits SARIF 2.1.0, so accessibility findings show up inline on pull
requests via GitHub code scanning:
- name: Fluid accessibility lint run: vendor/bin/fluid-lens lint packages/ --sarif > fluid-lens.sarif || true - uses: github/codeql-action/upload-sarif@v3 with: sarif_file: fluid-lens.sarif
Quality gates
fluid-lens holds itself to the standard it enforces:
composer qa # phpcs (PSR-12) + PHPStan (level 7) + Psalm (level 6) + PHPUnit
License
GPL-2.0-or-later
统计信息
- 总下载量: 0
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 0
- 点击次数: 1
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: GPL-2.0-or-later
- 更新时间: 2026-07-03