a11y_agent 0.0.5.pre.alpha.3 → 0.0.11

Sign up to get free protection for your applications and to get access to all the features.
@@ -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>
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module A11yAgent
4
- VERSION = '0.0.5-alpha.3'
4
+ VERSION = '0.0.11'
5
5
  end
@@ -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
@@ -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", "--without", "development"
58
- bin.install libexec/"bin/a11y_agent"
59
- bin.env_script_all_files(libexec/"bin", GEM_HOME: ENV.fetch("GEM_HOME"))
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}/a11y_agent", "--help"
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
- "@eslint/js": "^9.8.0",
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
  }