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.
@@ -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
  }