Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions .github/release-drafter.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name-template: 'v$RESOLVED_VERSION'
tag-template: 'v$RESOLVED_VERSION'
change-template: '- $TITLE (#$NUMBER) @$AUTHOR'
template: |
## Changes

$CHANGES

version-resolver:
major:
labels:
- 'breaking'
minor:
labels:
- 'feature'
- 'enhancement'
patch:
labels:
- 'bug'
- 'fix'
- 'dependencies'
default: patch

categories:
- title: 'Breaking Changes'
labels:
- 'breaking'
- title: 'Features'
labels:
- 'feature'
- 'enhancement'
- title: 'Bug Fixes'
labels:
- 'bug'
- 'fix'
- title: 'Maintenance'
labels:
- 'chore'
- 'dependencies'
- 'documentation'

exclude-labels:
- 'skip-changelog'

autolabeler:
- label: 'documentation'
files:
- '*.md'
- 'docs/**'
- label: 'tests'
files:
- 'tests/**'
- label: 'feature'
files:
- 'src/**'

sort-by: 'merged_at'
sort-direction: 'descending'
4 changes: 2 additions & 2 deletions .github/workflows/qa.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
php: [ '8.2', '8.3', '8.4' ]
php: [ '8.2', '8.3', '8.4', '8.5' ]

steps:
- name: Checkout
Expand Down Expand Up @@ -76,4 +76,4 @@ jobs:
token: ${{ secrets.CODECOV_TOKEN }}
files: build/coverage/junit.xml
report_type: test_results
fail_ci_if_error: false
fail_ci_if_error: false
29 changes: 29 additions & 0 deletions .github/workflows/release-drafter.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Release Drafter

on:
push:
branches:
- main
pull_request_target:
types:
- opened
- reopened
- synchronize
- labeled
- unlabeled

permissions:
contents: write
pull-requests: write

jobs:
update_release_draft:
runs-on: ubuntu-latest

steps:
- name: Draft next release
uses: release-drafter/release-drafter@v6
with:
config-name: release-drafter.yml
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
19 changes: 19 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: Release

on:
push:
tags:
- 'v*'

permissions:
contents: write

jobs:
publish:
runs-on: ubuntu-latest

steps:
- name: Create GitHub release
uses: softprops/action-gh-release@v2
with:
generate_release_notes: true
14 changes: 13 additions & 1 deletion README.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions composer.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 0 additions & 6 deletions phpunit.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

45 changes: 45 additions & 0 deletions roadmap.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

62 changes: 62 additions & 0 deletions src/Rules/Anchor/LinkHrefValidityRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

declare(strict_types=1);

namespace TwigA11y\Rules\Anchor;

use TwigA11y\Rules\AbstractA11yRule;
use TwigCsFixer\Token\Token;
use TwigCsFixer\Token\Tokens;

final class LinkHrefValidityRule extends AbstractA11yRule
{
public function evaluate(Tokens $tokens, int $tokenIndex, callable $emit): void
{
if ($this->shouldSkipByTokenIndex($tokenIndex)) {
return;
}

$token = $tokens->get($tokenIndex);
if (!$token->isMatching(Token::TEXT_TYPE)) {
return;
}

$value = $token->getValue();
if (!str_contains($value, '<a')) {
return;
}

$tag = $this->collectUntil($tokenIndex, $tokens, '>', 100);

if (!preg_match('/<a\b/i', $tag)) {
return;
}

if (preg_match('/\brole\s*=\s*["\']button["\']/i', $tag)) {
return;
}

if ($this->containsTwigExpressions($tag)) {
return;
}

if (!preg_match('/\bhref\s*=\s*(?:"([^"]*)"|\'([^\']*)\')/i', $tag, $hrefMatch)) {
$emit('Anchor elements should include a valid href attribute.', $token, 'LinkHref.MissingHref');

return;
}

$href = $this->firstMatch($hrefMatch, 1, 2);
$normalized = strtolower(trim($href));

if ('' === $normalized) {
$emit('Anchor elements should not use an empty href attribute.', $token, 'LinkHref.EmptyHref');

return;
}

if ('#' === $normalized || 'javascript:void(0)' === $normalized || 'javascript:void(0);' === $normalized) {
$emit('Anchor elements should use a real destination href instead of placeholder links.', $token, 'LinkHref.PlaceholderHref');
}
}
}
81 changes: 81 additions & 0 deletions src/Rules/Aria/AriaControlsIdExistsRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php

declare(strict_types=1);

namespace TwigA11y\Rules\Aria;

use TwigA11y\Rules\AbstractA11yRule;
use TwigCsFixer\Token\Token;
use TwigCsFixer\Token\Tokens;

final class AriaControlsIdExistsRule extends AbstractA11yRule
{
public function evaluate(Tokens $tokens, int $tokenIndex, callable $emit): void
{
if ($this->shouldSkipByTokenIndex($tokenIndex)) {
return;
}

$full = $this->getFullContent($tokens);

if (!str_contains($full, 'aria-controls')) {
return;
}

$idCount = preg_match_all('/\bid\s*=\s*(?:"|\')([^"\']+)(?:"|\')/i', $full, $idMatches);
$ids = [];
if ($idCount > 0) {
$ids = array_flip($idMatches[1]);
}

if (!preg_match_all('/\baria-controls\s*=\s*(?:"([^"]+)"|\'([^\']+)\')/i', $full, $refs, PREG_OFFSET_CAPTURE)) {
return;
}

foreach ($refs[0] as $index => $match) {
$attr = $match[0];
$offset = $match[1];
$value = $refs[1][$index][0] ?: $refs[2][$index][0];
$pieces = preg_split('/\s+/', trim($value));

if (false === $pieces) {
$pieces = [];
}

foreach ($pieces as $refId) {
if ('' === $refId) {
continue;
}

if (isset($ids[$refId])) {
continue;
}

$line = 1 + substr_count(substr($full, 0, $offset), "\n");
$fakeToken = $this->fakeTokenForLine($tokens, $line, $attr);

$emit(sprintf('Referenced id "%s" in aria-controls does not exist in template.', $refId), $fakeToken, 'AriaControls.MissingId');

return;
}
}
}

protected function evaluateOncePerFile(): bool
{
return true;
}

private function fakeTokenForLine(Tokens $tokens, int $line, string $value): Token
{
$token = $tokens->get(0);

return new Token(
$token->getType(),
$line,
1,
$token->getFilename(),
$value
);
}
}
Loading