a11y_agent 0.0.5.pre.alpha.4 → 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.4'
4
+ VERSION = '0.0.11'
5
5
  end
@@ -1,15 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'axe/core'
4
+ require 'axe/api/run'
3
5
  require 'dotenv/load'
4
6
  require 'diffy'
5
7
  require 'fileutils'
6
8
  require 'json'
7
9
  require 'open3'
8
10
  require 'rainbow/refinement'
11
+ require 'selenium-webdriver'
9
12
  require 'sublayer'
10
13
  require 'tty-prompt'
11
- require_relative '../generators/fix_a11y_generator'
12
- require_relative '../generators/hydrate_document_generator'
14
+ require_relative '../generators/confirmable_fix_generator'
13
15
 
14
16
  Diffy::Diff.default_format = :color
15
17
 
@@ -20,14 +22,7 @@ Diffy::Diff.default_format = :color
20
22
  # Sublayer.configuration.ai_model = "gemini-1.5-flash-latest"
21
23
 
22
24
  Sublayer.configuration.ai_provider = Sublayer::Providers::Claude
23
- Sublayer.configuration.ai_model = 'claude-3-haiku-20240307'
24
-
25
- CHOICES = [
26
- { key: 'y', name: 'approve and continue', value: :yes },
27
- { key: 'n', name: 'skip this change', value: :no },
28
- { key: 'r', name: 'retry with optional instructions', value: :retry },
29
- { key: 'q', name: 'quit; stop making changes', value: :quit }
30
- ].freeze
25
+ Sublayer.configuration.ai_model = 'claude-3-5-haiku-latest'
31
26
 
32
27
  module Sublayer
33
28
  module Agents
@@ -39,6 +34,7 @@ module Sublayer
39
34
  @issue_types = []
40
35
  @file = file
41
36
  @file_contents = File.read(@file)
37
+ @source_code = @file_contents
42
38
  @prompt = TTY::Prompt.new
43
39
  end
44
40
 
@@ -47,7 +43,7 @@ module Sublayer
47
43
  end
48
44
 
49
45
  check_status do
50
- load_issues unless run_axe.empty?
46
+ load_issues
51
47
  end
52
48
 
53
49
  goal_condition do
@@ -56,104 +52,79 @@ module Sublayer
56
52
  end
57
53
 
58
54
  step do
59
- @accessibility_issues.each { |issue| fix_issue_and_save(issue:) }
55
+ @accessibility_issues.each do |issue|
56
+ puts issue.description
57
+ fix_issue_and_save(issue:)
58
+ end
59
+
60
60
  exit 0
61
61
  end
62
62
 
63
63
  private
64
64
 
65
- def run_axe(file: @file)
66
- stdout, _stderr, _status = Open3.capture3("ts-node lib/bin/axe.ts #{file}")
67
- JSON.parse(stdout)
68
- end
69
-
70
65
  def load_issues
71
66
  Tempfile.create(['', File.extname(@file)]) do |tempfile|
72
- tempfile.write(hydrated_file)
67
+ tempfile.write(@file_contents)
73
68
  tempfile.rewind
74
69
 
75
- @accessibility_issues = run_axe(file: tempfile.path).map do |issue|
76
- %w[id impact tags helpUrl].each { |key| issue.delete(key) }
77
- issue
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
+ )
78
82
  end
79
83
  end
80
84
 
81
85
  puts "🚨 Found #{@accessibility_issues.length} accessibility issues" unless @accessibility_issues.empty?
82
86
  end
83
87
 
84
- def hydrated_file
85
- puts "Loading fake data into #{@file}"
86
- hydrated = HydrateDocumentGenerator.new(contents: @file_contents, extension: File.extname(@file)).generate
87
- hydrated << "\n" until hydrated.end_with?("\n")
88
-
89
- print_diff(contents: @file_contents, fixed: hydrated, message: '📊 Changes made:')
90
- hydration_approved = @prompt.yes? 'Continue with updates?'
91
- hydrated = @file_contents unless hydration_approved
92
- hydrated
93
- end
94
-
95
88
  def fix_issue_and_save(issue:)
96
- updated_contents = File.read(@file)
97
-
98
- issue['nodes'].each do |node|
99
- user_input = nil
100
- fixed = nil
101
- additional_prompt = nil
102
- summary = node['failureSummary']
103
- node_issue = [summary, issue['help'], node['html']].join("\n\n")
104
-
105
- puts "🔍 #{issue['help']}"
106
- attempt = @prompt.yes? "Attempt to fix these issues in #{@file}?"
107
- next unless attempt
108
-
109
- until %i[yes no].include?(user_input)
110
- puts '🔧 Attempting a fix...'
111
- result = FixA11yGenerator.new(contents: updated_contents, issue: node_issue, extension: File.extname(@file),
112
- additional_prompt:).generate
113
- result << "\n" unless result.end_with?("\n")
114
-
115
- print_chunks(contents: updated_contents, fixed: result)
116
-
117
- user_input = @prompt.expand('Approve changes?', CHOICES)
118
-
119
- case user_input
120
- when :yes
121
- fixed = result
122
- when :no
123
- fixed = updated_contents
124
- when :retry
125
- additional_prompt = @prompt.ask('Additional instructions:')
126
- fixed = nil
127
- when :quit
128
- puts 'Quitting...'
129
- exit 0
130
- end
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
131
120
  end
132
-
133
- puts '📝 Saving changes...'
134
- File.write(@file, fixed) if fixed
135
121
  end
136
- end
137
122
 
138
- def print_diff(contents:, fixed:, message: '')
139
- puts message
140
- puts Diffy::Diff.new(contents, fixed)
141
- end
142
123
 
143
- def print_chunks(contents:, fixed:)
144
- Diffy::Diff.new(contents, fixed).each_chunk do |chunk|
145
- case chunk
146
- when /^\+/
147
- print chunk.to_s.green
148
- when /^-/
149
- print chunk.to_s.red
150
- else
151
- lines = chunk.to_s.split("\n")
152
- puts lines[0..2].join("\n")
153
- puts '...'
154
- puts lines[-3..].join("\n") if lines.length > 5
155
- end
156
- end
124
+ return unless input == :fix
125
+ puts '📝 Saving changes...'
126
+ File.write(@file, fixed)
127
+ @source_code = fixed
157
128
  end
158
129
  end
159
130
  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
@@ -54,8 +54,8 @@ 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
- system "gem", "build", "#{spec.name}.gemspec"
57
+ system "bundle", "install", "--gemfile", libexec/"Gemfile"
58
+ system "gem", "build", libexec/"#{spec.name}.gemspec"
59
59
  system "gem", "install", "--ignore-dependencies", "#{spec.name}-#{version}.gem"
60
60
 
61
61
  bin.install libexec/"exe/#{spec.name}"
@@ -64,7 +64,7 @@ namespace :release do
64
64
 
65
65
  test do
66
66
  # Simple test to check the version or a help command
67
- system "\#{bin}/a11y_agent", "--help"
67
+ system "\#{bin}/#{spec.name}", "--help"
68
68
  end
69
69
  end
70
70
  RUBY
data/package.json CHANGED
@@ -1,16 +1,16 @@
1
1
  {
2
- "devDependencies": {
3
- "@types/jsdom": "^21.1.7",
4
- "eslint": "^8.57.0",
5
- "eslint-plugin-import": "^2.29.1",
6
- "eslint-plugin-jsx-a11y": "^6.9.0",
7
- "globals": "^15.8.0",
8
- "typescript": "^5.5.4",
9
- "typescript-eslint": "^7.17.0"
10
- },
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,
11
9
  "dependencies": {
12
- "axe-core": "^4.10.0",
13
- "canvas": "^2.11.2",
14
- "jsdom": "^24.1.1"
10
+ "@biomejs/biome": "1.9.4",
11
+ "eslint-plugin-vuejs-accessibility": "^2.4.1"
12
+ },
13
+ "devDependencies": {
14
+ "globals": "^15.9.0"
15
15
  }
16
16
  }