a11y_agent 0.0.5.pre.alpha.3 → 0.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.ruby-version +1 -0
- data/LICENSE +661 -0
- data/fixtures/react/.eslintrc +13 -0
- data/fixtures/react/.gitignore +3 -0
- data/fixtures/react/package-lock.json +16453 -0
- data/fixtures/react/package.json +42 -0
- data/fixtures/react/public/index.html +19 -0
- data/fixtures/react/readme.md +42 -0
- data/fixtures/react/src/index.js +16 -0
- data/fixtures/react/src/todo/app.css +87 -0
- data/fixtures/react/src/todo/app.jsx +20 -0
- data/fixtures/react/src/todo/components/footer.jsx +46 -0
- data/fixtures/react/src/todo/components/header.jsx +15 -0
- data/fixtures/react/src/todo/components/input.jsx +46 -0
- data/fixtures/react/src/todo/components/item.jsx +55 -0
- data/fixtures/react/src/todo/components/main.jsx +45 -0
- data/fixtures/react/src/todo/constants.js +7 -0
- data/fixtures/react/src/todo/reducer.js +64 -0
- data/fixtures/react/webpack.common.js +43 -0
- data/fixtures/react/webpack.dev.js +18 -0
- data/fixtures/react/webpack.prod.js +35 -0
- data/fixtures/sample.html +93 -0
- data/lib/a11y_agent/version.rb +1 -1
- data/lib/agents/a11y_agent.rb +131 -0
- data/lib/generators/confirmable_fix_generator.rb +56 -0
- data/lib/generators/fix_a11y_generator.rb +35 -0
- data/lib/tasks/release.rake +7 -4
- data/package.json +12 -7
- data/yarn.lock +114 -1810
- metadata +87 -12
- data/eslint.config.mjs +0 -23
- data/lib/a11y_agent.rb +0 -119
- data/lib/fix_a11y_generator.rb +0 -32
@@ -0,0 +1,93 @@
|
|
1
|
+
<html>
|
2
|
+
|
3
|
+
<head>
|
4
|
+
<title>Accessibility Violations</title>
|
5
|
+
</head>
|
6
|
+
|
7
|
+
<body>
|
8
|
+
<div>
|
9
|
+
<p>Here is an emoji not wrapped in a span: 😀</p>
|
10
|
+
|
11
|
+
<img src="image.jpg" alt="image" />
|
12
|
+
|
13
|
+
<a href="https://example.com">Click here</a>
|
14
|
+
|
15
|
+
<a href="#"></a>
|
16
|
+
|
17
|
+
<a href="invalid-url">Invalid Link</a>
|
18
|
+
|
19
|
+
<div></div>
|
20
|
+
|
21
|
+
<div aria-bogus="true"></div>
|
22
|
+
|
23
|
+
<div aria-hidden="false"></div>
|
24
|
+
|
25
|
+
<div role="invalidRole"></div>
|
26
|
+
|
27
|
+
<input type="text" autoComplete="off" />
|
28
|
+
|
29
|
+
<div onClick={()=> alert("Clicked!")}></div>
|
30
|
+
|
31
|
+
<input type="text" />
|
32
|
+
|
33
|
+
<h1></h1>
|
34
|
+
|
35
|
+
<iframe src="some-video.mp4"></iframe>
|
36
|
+
|
37
|
+
<img src="photo.jpg" alt="photo" />
|
38
|
+
|
39
|
+
<div onClick={()=> alert("Clicked!")} tabIndex={-1}></div>
|
40
|
+
|
41
|
+
<label>Username</label>
|
42
|
+
<input type="text" id="username" />
|
43
|
+
|
44
|
+
<label htmlFor="username">Username</label>
|
45
|
+
<input type="text" id="username" />
|
46
|
+
|
47
|
+
<audio controls>
|
48
|
+
<source src="audio.mp3" type="audio/mp3" />
|
49
|
+
</audio>
|
50
|
+
|
51
|
+
<div onMouseOver={()=> {}}></div>
|
52
|
+
|
53
|
+
<div accessKey="s"></div>
|
54
|
+
|
55
|
+
<div aria-hidden="true" tabIndex={0}></div>
|
56
|
+
|
57
|
+
<input type="text" autoFocus />
|
58
|
+
|
59
|
+
<marquee>Scrolling text</marquee>
|
60
|
+
|
61
|
+
<div role="button" onClick={()=> {}}></div>
|
62
|
+
|
63
|
+
<div onClick={()=> {}}></div>
|
64
|
+
|
65
|
+
<div role="link"></div>
|
66
|
+
|
67
|
+
<div tabIndex={1}></div>
|
68
|
+
|
69
|
+
<select onChange={()=> {}}>
|
70
|
+
<option>Option 1</option>
|
71
|
+
<option>Option 2</option>
|
72
|
+
</select>
|
73
|
+
|
74
|
+
<div role="banner"></div>
|
75
|
+
|
76
|
+
<button role="button">Click me</button>
|
77
|
+
|
78
|
+
<table>
|
79
|
+
<tr>
|
80
|
+
<th>Header</th>
|
81
|
+
<th>Another Header</th>
|
82
|
+
</tr>
|
83
|
+
<tr>
|
84
|
+
<td>Data</td>
|
85
|
+
<td>More Data</td>
|
86
|
+
</tr>
|
87
|
+
</table>
|
88
|
+
|
89
|
+
<div tabIndex={5}></div>
|
90
|
+
</div>
|
91
|
+
</body>
|
92
|
+
|
93
|
+
</html>
|
data/lib/a11y_agent/version.rb
CHANGED
@@ -0,0 +1,131 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'axe/core'
|
4
|
+
require 'axe/api/run'
|
5
|
+
require 'dotenv/load'
|
6
|
+
require 'diffy'
|
7
|
+
require 'fileutils'
|
8
|
+
require 'json'
|
9
|
+
require 'open3'
|
10
|
+
require 'rainbow/refinement'
|
11
|
+
require 'selenium-webdriver'
|
12
|
+
require 'sublayer'
|
13
|
+
require 'tty-prompt'
|
14
|
+
require_relative '../generators/confirmable_fix_generator'
|
15
|
+
|
16
|
+
Diffy::Diff.default_format = :color
|
17
|
+
|
18
|
+
# Sublayer.configuration.ai_provider = Sublayer::Providers::OpenAI
|
19
|
+
# Sublayer.configuration.ai_model = 'gpt-4o-mini'
|
20
|
+
|
21
|
+
# Sublayer.configuration.ai_provider = Sublayer::Providers::Gemini
|
22
|
+
# Sublayer.configuration.ai_model = "gemini-1.5-flash-latest"
|
23
|
+
|
24
|
+
Sublayer.configuration.ai_provider = Sublayer::Providers::Claude
|
25
|
+
Sublayer.configuration.ai_model = 'claude-3-5-haiku-latest'
|
26
|
+
|
27
|
+
module Sublayer
|
28
|
+
module Agents
|
29
|
+
class A11yAgent < Base
|
30
|
+
using Rainbow
|
31
|
+
|
32
|
+
def initialize(file:)
|
33
|
+
@accessibility_issues = []
|
34
|
+
@issue_types = []
|
35
|
+
@file = file
|
36
|
+
@file_contents = File.read(@file)
|
37
|
+
@source_code = @file_contents
|
38
|
+
@prompt = TTY::Prompt.new
|
39
|
+
end
|
40
|
+
|
41
|
+
trigger_on_files_changed do
|
42
|
+
['./trigger.txt']
|
43
|
+
end
|
44
|
+
|
45
|
+
check_status do
|
46
|
+
load_issues
|
47
|
+
end
|
48
|
+
|
49
|
+
goal_condition do
|
50
|
+
puts "🤷 No accessibility issues found in #{@file}" if @accessibility_issues.empty?
|
51
|
+
exit 0 if @accessibility_issues.empty?
|
52
|
+
end
|
53
|
+
|
54
|
+
step do
|
55
|
+
@accessibility_issues.each do |issue|
|
56
|
+
puts issue.description
|
57
|
+
fix_issue_and_save(issue:)
|
58
|
+
end
|
59
|
+
|
60
|
+
exit 0
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def load_issues
|
66
|
+
Tempfile.create(['', File.extname(@file)]) do |tempfile|
|
67
|
+
tempfile.write(@file_contents)
|
68
|
+
tempfile.rewind
|
69
|
+
|
70
|
+
command = %(yarn --silent biome lint --reporter=json --only=a11y #{tempfile.path})
|
71
|
+
stdout, _stderr, _status = Open3.capture3(command, stdin_data: @file_contents)
|
72
|
+
|
73
|
+
@accessibility_issues = JSON.parse(stdout).fetch('diagnostics').map do |d|
|
74
|
+
OpenStruct.new(
|
75
|
+
description: d.fetch('description'),
|
76
|
+
location: d.fetch('location').fetch('span'),
|
77
|
+
snippet: d.fetch('location').fetch('sourceCode')[d.fetch('location').fetch('span')[0]..d.fetch('location').fetch('span')[1] - 1],
|
78
|
+
advice: d.fetch('advices').fetch('advices').map do |a|
|
79
|
+
a.fetch('log')[1][0].fetch('content') unless a.fetch('log', nil).nil?
|
80
|
+
end
|
81
|
+
)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
puts "🚨 Found #{@accessibility_issues.length} accessibility issues" unless @accessibility_issues.empty?
|
86
|
+
end
|
87
|
+
|
88
|
+
def fix_issue_and_save(issue:)
|
89
|
+
additional_instructions = nil
|
90
|
+
input = nil
|
91
|
+
|
92
|
+
until %i[fix skip].include?(input)
|
93
|
+
exit 0 if input == :exit
|
94
|
+
puts additional_instructions if additional_instructions
|
95
|
+
|
96
|
+
result = ConfirmableFixGenerator.new(lint_failure: issue, source_code: @source_code, additional_instructions:).generate
|
97
|
+
fixed = result.fixed + "\n"
|
98
|
+
puts Diffy::Diff.new(@source_code, fixed, context: 2).to_s(:color)
|
99
|
+
|
100
|
+
input = @prompt.expand('Apply the fix?', [
|
101
|
+
{ key: 'y', name: 'Apply the fix', value: :fix },
|
102
|
+
{ key: 'n', name: 'Skip the fix', value: :skip },
|
103
|
+
{ key: 'e', name: 'Explain why', value: :explain },
|
104
|
+
{ key: 'r', name: 'Retry with optional instructions', value: :retry },
|
105
|
+
{ key: 'q', name: 'Exit', value: :exit }
|
106
|
+
])
|
107
|
+
|
108
|
+
if input == :retry
|
109
|
+
additional_instructions = @prompt.ask('Provide additional instructions:')
|
110
|
+
next
|
111
|
+
elsif input == :explain
|
112
|
+
puts
|
113
|
+
puts 'Explanation'.bright
|
114
|
+
puts result.description
|
115
|
+
puts
|
116
|
+
|
117
|
+
continue = @prompt.yes? 'Continue with the fix?'
|
118
|
+
next unless continue
|
119
|
+
input = :fix
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
|
124
|
+
return unless input == :fix
|
125
|
+
puts '📝 Saving changes...'
|
126
|
+
File.write(@file, fixed)
|
127
|
+
@source_code = fixed
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dotenv/load'
|
4
|
+
require 'sublayer'
|
5
|
+
|
6
|
+
class ConfirmableFixGenerator < Sublayer::Generators::Base
|
7
|
+
llm_output_adapter type: :named_strings,
|
8
|
+
name: 'lint_fix',
|
9
|
+
description: 'A fix for lint failures',
|
10
|
+
item_name: 'lint_fix',
|
11
|
+
attributes: [
|
12
|
+
{
|
13
|
+
name: 'description',
|
14
|
+
description: 'A brief description of the fix and why it is important'
|
15
|
+
},
|
16
|
+
{
|
17
|
+
name: 'impact',
|
18
|
+
description: 'A brief explanation of how the fix impacts assistive technologies'
|
19
|
+
},
|
20
|
+
{
|
21
|
+
name: 'fixed',
|
22
|
+
description: 'The complete source code with the fix applied'
|
23
|
+
}
|
24
|
+
]
|
25
|
+
|
26
|
+
def initialize(lint_failure:, source_code:, additional_instructions: nil)
|
27
|
+
super()
|
28
|
+
@source_code = source_code
|
29
|
+
@additional_instructions = additional_instructions
|
30
|
+
@failure_line = %(#{lint_failure.description} at span #{lint_failure.location[0]}:#{lint_failure.location[1]})
|
31
|
+
end
|
32
|
+
|
33
|
+
def prompt
|
34
|
+
<<-PROMPT
|
35
|
+
You are an expert at remediating lint errors in source code.
|
36
|
+
Generate a fix for the following lint failure in the provided source code.
|
37
|
+
Only fix one specified lint failure at a time, at the given position.
|
38
|
+
|
39
|
+
Source code:
|
40
|
+
#{@source_code}
|
41
|
+
|
42
|
+
Lint failure:
|
43
|
+
#{@failure_line}
|
44
|
+
|
45
|
+
Additional instructions (if any):
|
46
|
+
#{@additional_instructions}
|
47
|
+
|
48
|
+
For the fix provide:
|
49
|
+
- description: A brief description of the change and why it is important.
|
50
|
+
- fixed: the fixed source code with the issue resolved.
|
51
|
+
- impact: A description of how the fix impacts assistive technologies.
|
52
|
+
|
53
|
+
Provide your response is an object containing the above attributes.
|
54
|
+
PROMPT
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
class FixA11yGenerator < Sublayer::Generators::Base
|
2
|
+
llm_output_adapter type: :single_string,
|
3
|
+
name: 'fix_template_content_based_on_a11y_issue',
|
4
|
+
description: 'Given a web document template and an accessibility issue, generate a new file with the issue fixed.'
|
5
|
+
|
6
|
+
def initialize(contents:, issue:, extension: '', additional_prompt: nil)
|
7
|
+
@extension = extension
|
8
|
+
@contents = contents
|
9
|
+
@issue = issue
|
10
|
+
@additional_prompt = additional_prompt
|
11
|
+
end
|
12
|
+
|
13
|
+
def generate
|
14
|
+
super
|
15
|
+
end
|
16
|
+
|
17
|
+
def prompt
|
18
|
+
<<~PROMPT
|
19
|
+
Given the following #{@extension} template contents and an
|
20
|
+
accessibility issue, generate a new #{@extension} file with the
|
21
|
+
issue fixed, leaving the rest of the contents unchanged.
|
22
|
+
|
23
|
+
#{@extension} code:
|
24
|
+
#{@contents}
|
25
|
+
|
26
|
+
Accessibility issue:
|
27
|
+
#{@issue}
|
28
|
+
|
29
|
+
Additional user instructions (if any):
|
30
|
+
#{@additional_prompt || 'None'}
|
31
|
+
|
32
|
+
Return the contents with the issue fixed.
|
33
|
+
PROMPT
|
34
|
+
end
|
35
|
+
end
|
data/lib/tasks/release.rake
CHANGED
@@ -54,14 +54,17 @@ namespace :release do
|
|
54
54
|
# Extract all files to libexec, which is a common Homebrew practice for third-party tools
|
55
55
|
libexec.install Dir["*"]
|
56
56
|
|
57
|
-
system "bundle", "install", "--
|
58
|
-
|
59
|
-
|
57
|
+
system "bundle", "install", "--gemfile", libexec/"Gemfile"
|
58
|
+
system "gem", "build", libexec/"#{spec.name}.gemspec"
|
59
|
+
system "gem", "install", "--ignore-dependencies", "#{spec.name}-#{version}.gem"
|
60
|
+
|
61
|
+
bin.install libexec/"exe/#{spec.name}"
|
62
|
+
bin.env_script_all_files(libexec/"exe", GEM_HOME: ENV.fetch("GEM_HOME", nil))
|
60
63
|
end
|
61
64
|
|
62
65
|
test do
|
63
66
|
# Simple test to check the version or a help command
|
64
|
-
system "\#{bin}
|
67
|
+
system "\#{bin}/#{spec.name}", "--help"
|
65
68
|
end
|
66
69
|
end
|
67
70
|
RUBY
|
data/package.json
CHANGED
@@ -1,11 +1,16 @@
|
|
1
1
|
{
|
2
|
+
"name": "a11y-agent",
|
3
|
+
"version": "0.0.0",
|
4
|
+
"main": "index.js",
|
5
|
+
"repository": "git@github.com:AccessLint/a11y-agent.git",
|
6
|
+
"author": "Cameron Cundiff <github@ckundo.com>",
|
7
|
+
"license": "AGPL-3.0",
|
8
|
+
"private": true,
|
9
|
+
"dependencies": {
|
10
|
+
"@biomejs/biome": "1.9.4",
|
11
|
+
"eslint-plugin-vuejs-accessibility": "^2.4.1"
|
12
|
+
},
|
2
13
|
"devDependencies": {
|
3
|
-
"
|
4
|
-
"eslint": "^9.8.0",
|
5
|
-
"eslint-plugin-jsx-a11y": "^6.9.0",
|
6
|
-
"eslint-plugin-react": "^7.35.0",
|
7
|
-
"globals": "^15.8.0",
|
8
|
-
"typescript": "^5.5.4",
|
9
|
-
"typescript-eslint": "^7.17.0"
|
14
|
+
"globals": "^15.9.0"
|
10
15
|
}
|
11
16
|
}
|