mailcatcher-ng 1.4.6 → 1.5.2

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.
@@ -821,11 +821,187 @@ class MailCatcher {
821
821
 
822
822
  $("#message .views .download a").attr("href", `messages/${id}.eml`);
823
823
 
824
+ this.loadAccessibilityScore(id);
824
825
  this.loadMessageBody();
825
826
  });
826
827
  }
827
828
  }
828
829
 
830
+ loadAccessibilityScore(id) {
831
+ $.getJSON(`messages/${id}/accessibility.json`, (data) => {
832
+ // Update accessibility score
833
+ $("#accessibilityScore").text(data.score);
834
+
835
+ // Update breakdown metrics
836
+ if (data.breakdown) {
837
+ $("#imagesWithAlt").text(data.breakdown.images_with_alt + "%");
838
+ $("#semanticHtml").text(data.breakdown.semantic_html + "%");
839
+ $("#linksWithText").text(data.breakdown.links_with_text + "%");
840
+ }
841
+
842
+ // Initialize Tippy tooltips with detailed findings
843
+ this.initAccessibilityTooltips(data);
844
+ }).fail(() => {
845
+ // Reset scores if API call fails (e.g., for plain text emails)
846
+ $("#accessibilityScore").text("-");
847
+ $("#imagesWithAlt").text("-");
848
+ $("#semanticHtml").text("-");
849
+ $("#linksWithText").text("-");
850
+ });
851
+ }
852
+
853
+ initAccessibilityTooltips(data) {
854
+ // Main accessibility score tooltip
855
+ tippy("#accessibilityScoreBtn", {
856
+ content: this.buildAccessibilityTooltip(data),
857
+ allowHTML: true,
858
+ interactive: true,
859
+ theme: 'light',
860
+ placement: 'bottom',
861
+ maxWidth: 400,
862
+ sticky: true
863
+ });
864
+
865
+ // Images with alt text tooltip
866
+ if (data.findings && data.findings.images) {
867
+ tippy("#imagesWithAltBtn", {
868
+ content: this.buildImagesTooltiip(data.findings.images),
869
+ allowHTML: true,
870
+ interactive: true,
871
+ theme: 'light',
872
+ placement: 'bottom',
873
+ maxWidth: 400,
874
+ sticky: true
875
+ });
876
+ }
877
+
878
+ // Semantic HTML tooltip
879
+ if (data.findings && data.findings.semantic) {
880
+ tippy("#semanticHtmlBtn", {
881
+ content: this.buildSemanticTooltip(data.findings.semantic),
882
+ allowHTML: true,
883
+ interactive: true,
884
+ theme: 'light',
885
+ placement: 'bottom',
886
+ maxWidth: 400,
887
+ sticky: true
888
+ });
889
+ }
890
+
891
+ // Links with text tooltip
892
+ if (data.findings && data.findings.links) {
893
+ tippy("#linksWithTextBtn", {
894
+ content: this.buildLinksTooltip(data.findings.links),
895
+ allowHTML: true,
896
+ interactive: true,
897
+ theme: 'light',
898
+ placement: 'bottom',
899
+ maxWidth: 400,
900
+ sticky: true
901
+ });
902
+ }
903
+ }
904
+
905
+ buildAccessibilityTooltip(data) {
906
+ let html = `<div class="accessibility-tooltip">
907
+ <h4>Overall Accessibility Score: ${data.score}/100</h4>`;
908
+
909
+ if (data.recommendations && data.recommendations.length) {
910
+ html += `<div class="tooltip-section">
911
+ <h5>Recommendations:</h5>
912
+ <ul>`;
913
+ data.recommendations.forEach(rec => {
914
+ html += `<li>${rec}</li>`;
915
+ });
916
+ html += `</ul></div>`;
917
+ }
918
+
919
+ html += `</div>`;
920
+ return html;
921
+ }
922
+
923
+ buildImagesTooltiip(findings) {
924
+ let html = `<div class="accessibility-tooltip">
925
+ <h4>Image Accessibility</h4>
926
+ <div class="tooltip-stats">
927
+ <div><strong>Total images:</strong> ${findings.total}</div>
928
+ <div><strong>With alt text:</strong> ${findings.with_alt}</div>
929
+ <div><strong>Missing alt text:</strong> ${findings.total - findings.with_alt}</div>
930
+ </div>`;
931
+
932
+ if (findings.without_alt && findings.without_alt.length > 0) {
933
+ html += `<div class="tooltip-section">
934
+ <h5>Images without alt text:</h5>
935
+ <ul class="tooltip-issues">`;
936
+ findings.without_alt.slice(0, 5).forEach(img => {
937
+ html += `<li>${img.alt_missing ? '❌ Missing alt attribute' : '⚠️ Empty alt text'}: ${img.src || '(no src)'}</li>`;
938
+ });
939
+ if (findings.without_alt.length > 5) {
940
+ html += `<li>... and ${findings.without_alt.length - 5} more</li>`;
941
+ }
942
+ html += `</ul></div>`;
943
+ }
944
+
945
+ html += `</div>`;
946
+ return html;
947
+ }
948
+
949
+ buildSemanticTooltip(findings) {
950
+ let html = `<div class="accessibility-tooltip">
951
+ <h4>Semantic HTML</h4>`;
952
+
953
+ if (findings.has_semantic_tags) {
954
+ html += `<div class="tooltip-stats">
955
+ <div>✅ Semantic tags found</div>`;
956
+ if (findings.found_tags && findings.found_tags.length) {
957
+ html += `<div><strong>Tags:</strong> ${findings.found_tags.join(', ')}</div>`;
958
+ }
959
+ html += `</div>`;
960
+ } else {
961
+ html += `<div class="tooltip-stats">
962
+ <div>⚠️ No semantic HTML tags found</div>
963
+ <div style="margin-top: 8px; font-size: 12px;">Consider using semantic tags like:</div>
964
+ <ul class="tooltip-tips">
965
+ <li>&lt;header&gt; - Page header</li>
966
+ <li>&lt;nav&gt; - Navigation area</li>
967
+ <li>&lt;main&gt; - Main content</li>
968
+ <li>&lt;article&gt; - Article content</li>
969
+ <li>&lt;section&gt; - Content section</li>
970
+ <li>&lt;footer&gt; - Page footer</li>
971
+ </ul>
972
+ </div>`;
973
+ }
974
+
975
+ html += `</div>`;
976
+ return html;
977
+ }
978
+
979
+ buildLinksTooltip(findings) {
980
+ let html = `<div class="accessibility-tooltip">
981
+ <h4>Link Accessibility</h4>
982
+ <div class="tooltip-stats">
983
+ <div><strong>Total links:</strong> ${findings.total}</div>
984
+ <div><strong>With descriptive text:</strong> ${findings.with_text}</div>
985
+ <div><strong>Missing text/aria-label:</strong> ${findings.total - findings.with_text}</div>
986
+ </div>`;
987
+
988
+ if (findings.without_text && findings.without_text.length > 0) {
989
+ html += `<div class="tooltip-section">
990
+ <h5>Links without descriptive text:</h5>
991
+ <ul class="tooltip-issues">`;
992
+ findings.without_text.slice(0, 5).forEach(link => {
993
+ html += `<li>${link.text_empty ? '❌ No text' : '⚠️ No aria-label'}: ${link.href || '(no href)'}</li>`;
994
+ });
995
+ if (findings.without_text.length > 5) {
996
+ html += `<li>... and ${findings.without_text.length - 5} more</li>`;
997
+ }
998
+ html += `</ul></div>`;
999
+ }
1000
+
1001
+ html += `</div>`;
1002
+ return html;
1003
+ }
1004
+
829
1005
  loadMessageBody(id, format) {
830
1006
  id = id || this.selectedMessage();
831
1007
  format = format || $("#message .views .tab.format.selected").attr("data-message-format");
data/views/index.erb CHANGED
@@ -191,6 +191,35 @@
191
191
  </a></li>
192
192
  </ul>
193
193
  </nav>
194
+ <div class="accessibility-ratings">
195
+ <div class="accessibility-score">
196
+ <div class="score-label">Accessibility</div>
197
+ <button class="accessibility-score-btn" id="accessibilityScoreBtn" title="Click for accessibility details">
198
+ <div class="score-value" id="accessibilityScore">-</div>
199
+ </button>
200
+ <div class="score-unit">/ 100</div>
201
+ </div>
202
+ <div class="accessibility-breakdown">
203
+ <div class="breakdown-item">
204
+ <span class="breakdown-label">Images with Alt Text</span>
205
+ <button class="accessibility-metric-btn" id="imagesWithAltBtn" title="Click for image accessibility details">
206
+ <span class="breakdown-value" id="imagesWithAlt">-</span>
207
+ </button>
208
+ </div>
209
+ <div class="breakdown-item">
210
+ <span class="breakdown-label">Semantic HTML</span>
211
+ <button class="accessibility-metric-btn" id="semanticHtmlBtn" title="Click for semantic HTML details">
212
+ <span class="breakdown-value" id="semanticHtml">-</span>
213
+ </button>
214
+ </div>
215
+ <div class="breakdown-item">
216
+ <span class="breakdown-label">Link Text</span>
217
+ <button class="accessibility-metric-btn" id="linksWithTextBtn" title="Click for link accessibility details">
218
+ <span class="breakdown-value" id="linksWithText">-</span>
219
+ </button>
220
+ </div>
221
+ </div>
222
+ </div>
194
223
  </div>
195
224
  <div class="metadata">
196
225
  <div class="metadata-column">
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mailcatcher-ng
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.6
4
+ version: 1.5.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stephane Paquet
@@ -65,6 +65,20 @@ dependencies:
65
65
  - - "~>"
66
66
  - !ruby/object:Gem::Version
67
67
  version: 0.5.1
68
+ - !ruby/object:Gem::Dependency
69
+ name: nokogiri
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '1.18'
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '1.18'
68
82
  - !ruby/object:Gem::Dependency
69
83
  name: ostruct
70
84
  requirement: !ruby/object:Gem::Requirement
@@ -325,6 +339,9 @@ files:
325
339
  - bin/mailcatcher
326
340
  - lib/mail_catcher.rb
327
341
  - lib/mail_catcher/bus.rb
342
+ - lib/mail_catcher/integrations.rb
343
+ - lib/mail_catcher/integrations/mcp_server.rb
344
+ - lib/mail_catcher/integrations/mcp_tools.rb
328
345
  - lib/mail_catcher/mail.rb
329
346
  - lib/mail_catcher/smtp.rb
330
347
  - lib/mail_catcher/version.rb