svg_conform 0.1.0

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.
Files changed (335) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/rake.yml +15 -0
  3. data/.github/workflows/release.yml +23 -0
  4. data/.github/workflows/svgcheck-compatibility.yml +135 -0
  5. data/.gitignore +12 -0
  6. data/.rspec +3 -0
  7. data/.rubocop.yml +13 -0
  8. data/.rubocop_todo.yml +183 -0
  9. data/CODE_OF_CONDUCT.md +132 -0
  10. data/Gemfile +14 -0
  11. data/README.adoc +1384 -0
  12. data/Rakefile +15 -0
  13. data/config/profiles/base.yml +34 -0
  14. data/config/profiles/lucid_fix.yml +37 -0
  15. data/config/profiles/metanorma.yml +212 -0
  16. data/config/profiles/no_external_css.yml +20 -0
  17. data/config/profiles/svg_1_2_rfc.yml +284 -0
  18. data/config/profiles/svg_1_2_rfc_with_rdf.yml +145 -0
  19. data/config/svgcheck_mapping.yml +180 -0
  20. data/docs/profiles.adoc +547 -0
  21. data/docs/rdf_metadata_support.adoc +212 -0
  22. data/docs/remediation.adoc +732 -0
  23. data/docs/requirements.adoc +709 -0
  24. data/examples/demo.rb +116 -0
  25. data/examples/requirements_demo.rb +240 -0
  26. data/exe/svg_conform +7 -0
  27. data/lib/svg_conform/batch_report.rb +70 -0
  28. data/lib/svg_conform/cli.rb +107 -0
  29. data/lib/svg_conform/commands/check.rb +390 -0
  30. data/lib/svg_conform/commands/profiles.rb +118 -0
  31. data/lib/svg_conform/commands/svgcheck.rb +90 -0
  32. data/lib/svg_conform/commands/svgcheck_compare.rb +92 -0
  33. data/lib/svg_conform/commands/svgcheck_compatibility.rb +43 -0
  34. data/lib/svg_conform/commands/svgcheck_generate.rb +262 -0
  35. data/lib/svg_conform/compatibility/analysis_context.rb +68 -0
  36. data/lib/svg_conform/compatibility/comparison_result.rb +147 -0
  37. data/lib/svg_conform/compatibility/compatibility_analyzer.rb +85 -0
  38. data/lib/svg_conform/compatibility/file_processor.rb +109 -0
  39. data/lib/svg_conform/compatibility/pattern_discovery.rb +319 -0
  40. data/lib/svg_conform/compatibility/report_formatter.rb +359 -0
  41. data/lib/svg_conform/compatibility/svg_analysis_engine.rb +316 -0
  42. data/lib/svg_conform/compatibility/validity_analysis.rb +252 -0
  43. data/lib/svg_conform/compatibility/xml_analysis_engine.rb +198 -0
  44. data/lib/svg_conform/compatibility_analyzer.rb +285 -0
  45. data/lib/svg_conform/conformance_report.rb +267 -0
  46. data/lib/svg_conform/constants.rb +199 -0
  47. data/lib/svg_conform/css_color.rb +262 -0
  48. data/lib/svg_conform/document.rb +203 -0
  49. data/lib/svg_conform/external_checkers/svgcheck/compatibility_engine.rb +166 -0
  50. data/lib/svg_conform/external_checkers/svgcheck/output_generator.rb +101 -0
  51. data/lib/svg_conform/external_checkers/svgcheck/parser.rb +200 -0
  52. data/lib/svg_conform/external_checkers/svgcheck/report_comparator.rb +175 -0
  53. data/lib/svg_conform/external_checkers/svgcheck/report_generator.rb +82 -0
  54. data/lib/svg_conform/external_checkers/svgcheck/validation_pipeline.rb +249 -0
  55. data/lib/svg_conform/external_checkers/svgcheck.rb +56 -0
  56. data/lib/svg_conform/external_checkers.rb +34 -0
  57. data/lib/svg_conform/fixer.rb +56 -0
  58. data/lib/svg_conform/profile.rb +164 -0
  59. data/lib/svg_conform/profiles.rb +60 -0
  60. data/lib/svg_conform/remediation_engine.rb +92 -0
  61. data/lib/svg_conform/remediation_result.rb +36 -0
  62. data/lib/svg_conform/remediation_runner.rb +225 -0
  63. data/lib/svg_conform/remediations/base_remediation.rb +165 -0
  64. data/lib/svg_conform/remediations/color_remediation.rb +226 -0
  65. data/lib/svg_conform/remediations/font_embedding_remediation.rb +145 -0
  66. data/lib/svg_conform/remediations/font_remediation.rb +122 -0
  67. data/lib/svg_conform/remediations/image_embedding_remediation.rb +154 -0
  68. data/lib/svg_conform/remediations/invalid_id_references_remediation.rb +129 -0
  69. data/lib/svg_conform/remediations/namespace_attribute_remediation.rb +244 -0
  70. data/lib/svg_conform/remediations/namespace_remediation.rb +151 -0
  71. data/lib/svg_conform/remediations/no_external_css_remediation.rb +192 -0
  72. data/lib/svg_conform/remediations/style_promotion_remediation.rb +93 -0
  73. data/lib/svg_conform/remediations/viewbox_remediation.rb +127 -0
  74. data/lib/svg_conform/remediations.rb +40 -0
  75. data/lib/svg_conform/report_comparator.rb +772 -0
  76. data/lib/svg_conform/requirements/allowed_elements_requirement.rb +367 -0
  77. data/lib/svg_conform/requirements/base_requirement.rb +98 -0
  78. data/lib/svg_conform/requirements/color_restrictions_requirement.rb +126 -0
  79. data/lib/svg_conform/requirements/element_requirement_config.rb +75 -0
  80. data/lib/svg_conform/requirements/font_family_requirement.rb +133 -0
  81. data/lib/svg_conform/requirements/forbidden_content_requirement.rb +60 -0
  82. data/lib/svg_conform/requirements/id_reference_requirement.rb +133 -0
  83. data/lib/svg_conform/requirements/invalid_id_references_requirement.rb +132 -0
  84. data/lib/svg_conform/requirements/link_validation_requirement.rb +55 -0
  85. data/lib/svg_conform/requirements/namespace_attributes_requirement.rb +211 -0
  86. data/lib/svg_conform/requirements/namespace_requirement.rb +294 -0
  87. data/lib/svg_conform/requirements/no_external_css_requirement.rb +132 -0
  88. data/lib/svg_conform/requirements/no_external_fonts_requirement.rb +121 -0
  89. data/lib/svg_conform/requirements/no_external_images_requirement.rb +91 -0
  90. data/lib/svg_conform/requirements/style_promotion_requirement.rb +72 -0
  91. data/lib/svg_conform/requirements/style_requirement.rb +226 -0
  92. data/lib/svg_conform/requirements/viewbox_required_requirement.rb +96 -0
  93. data/lib/svg_conform/requirements.rb +49 -0
  94. data/lib/svg_conform/semantic_comparator.rb +829 -0
  95. data/lib/svg_conform/validation_context.rb +408 -0
  96. data/lib/svg_conform/validation_result.rb +146 -0
  97. data/lib/svg_conform/validator.rb +91 -0
  98. data/lib/svg_conform/version.rb +5 -0
  99. data/lib/svg_conform.rb +68 -0
  100. data/lib/tasks/fixtures.rake +321 -0
  101. data/lib/tasks/svgcheck.rake +111 -0
  102. data/reference-docs/SVG-1.2-RFC.rnc.txt +1676 -0
  103. data/reference-docs/Scalable Vector Graphics (SVG) 1.1 (Second Edition).html +40764 -0
  104. data/reference-docs/Scalable Vector Graphics (SVG) Tiny 1.2 Specification.html +44591 -0
  105. data/reference-docs/rfc7996.txt +2971 -0
  106. data/sig/svg_conform.rbs +4 -0
  107. data/spec/fixtures/allowed_elements/inputs/basic_violations.svg +21 -0
  108. data/spec/fixtures/allowed_elements/repair/basic_violations.svg +18 -0
  109. data/spec/fixtures/color_restrictions/inputs/basic_violations.svg +23 -0
  110. data/spec/fixtures/color_restrictions/repair/basic_violations.svg +23 -0
  111. data/spec/fixtures/comprehensive/inputs/multiple_violations.svg +21 -0
  112. data/spec/fixtures/comprehensive/repair/multiple_violations.svg +16 -0
  113. data/spec/fixtures/font_family/inputs/basic_violations.svg +17 -0
  114. data/spec/fixtures/font_family/repair/basic_violations.svg +17 -0
  115. data/spec/fixtures/forbidden_content/inputs/basic_violations.svg +27 -0
  116. data/spec/fixtures/forbidden_content/repair/basic_violations.svg +19 -0
  117. data/spec/fixtures/id_reference/inputs/basic_violations.svg +17 -0
  118. data/spec/fixtures/id_reference/repair/basic_violations.svg +15 -0
  119. data/spec/fixtures/link_validation/inputs/basic_violations.svg +20 -0
  120. data/spec/fixtures/link_validation/repair/basic_violations.svg +16 -0
  121. data/spec/fixtures/lucid/inputs/simple.svg +67 -0
  122. data/spec/fixtures/lucid/repair/simple.svg +63 -0
  123. data/spec/fixtures/namespace/inputs/basic_violations.svg +20 -0
  124. data/spec/fixtures/namespace/repair/basic_violations.svg +17 -0
  125. data/spec/fixtures/namespace_attributes/inputs/basic_violations.svg +16 -0
  126. data/spec/fixtures/namespace_attributes/repair/basic_violations.svg +15 -0
  127. data/spec/fixtures/no_external_css/inputs/basic_violations.svg +20 -0
  128. data/spec/fixtures/no_external_css/repair/basic_violations.svg +18 -0
  129. data/spec/fixtures/style/inputs/basic_violations.svg +22 -0
  130. data/spec/fixtures/style/repair/basic_violations.svg +22 -0
  131. data/spec/fixtures/style_promotion/inputs/basic_test.svg +15 -0
  132. data/spec/fixtures/style_promotion/repair/basic_test.svg +15 -0
  133. data/spec/fixtures/svg_1_2_rfc/inputs/allowed_elements_violations.svg +18 -0
  134. data/spec/fixtures/svg_1_2_rfc/inputs/color_restrictions_violations.svg +23 -0
  135. data/spec/fixtures/svgcheck/check/DrawBerry-sample-2.svg.code +1 -0
  136. data/spec/fixtures/svgcheck/check/DrawBerry-sample-2.svg.err +0 -0
  137. data/spec/fixtures/svgcheck/check/DrawBerry-sample-2.svg.out +23 -0
  138. data/spec/fixtures/svgcheck/check/IETF-test.svg.code +1 -0
  139. data/spec/fixtures/svgcheck/check/IETF-test.svg.err +0 -0
  140. data/spec/fixtures/svgcheck/check/IETF-test.svg.out +20 -0
  141. data/spec/fixtures/svgcheck/check/circle.svg.code +1 -0
  142. data/spec/fixtures/svgcheck/check/circle.svg.err +0 -0
  143. data/spec/fixtures/svgcheck/check/circle.svg.out +2 -0
  144. data/spec/fixtures/svgcheck/check/colors.svg.code +1 -0
  145. data/spec/fixtures/svgcheck/check/colors.svg.err +0 -0
  146. data/spec/fixtures/svgcheck/check/colors.svg.out +13 -0
  147. data/spec/fixtures/svgcheck/check/dia-sample-svg.svg.code +1 -0
  148. data/spec/fixtures/svgcheck/check/dia-sample-svg.svg.err +0 -0
  149. data/spec/fixtures/svgcheck/check/dia-sample-svg.svg.out +76 -0
  150. data/spec/fixtures/svgcheck/check/example-dot.svg.code +1 -0
  151. data/spec/fixtures/svgcheck/check/example-dot.svg.err +0 -0
  152. data/spec/fixtures/svgcheck/check/example-dot.svg.out +11 -0
  153. data/spec/fixtures/svgcheck/check/full-tiny.svg.code +1 -0
  154. data/spec/fixtures/svgcheck/check/full-tiny.svg.err +0 -0
  155. data/spec/fixtures/svgcheck/check/full-tiny.svg.out +5835 -0
  156. data/spec/fixtures/svgcheck/check/good.svg.code +1 -0
  157. data/spec/fixtures/svgcheck/check/good.svg.err +0 -0
  158. data/spec/fixtures/svgcheck/check/good.svg.out +1 -0
  159. data/spec/fixtures/svgcheck/check/httpbis-proxy20-fig6.svg.code +1 -0
  160. data/spec/fixtures/svgcheck/check/httpbis-proxy20-fig6.svg.err +0 -0
  161. data/spec/fixtures/svgcheck/check/httpbis-proxy20-fig6.svg.out +5 -0
  162. data/spec/fixtures/svgcheck/check/malformed.svg.code +1 -0
  163. data/spec/fixtures/svgcheck/check/malformed.svg.err +0 -0
  164. data/spec/fixtures/svgcheck/check/malformed.svg.out +8 -0
  165. data/spec/fixtures/svgcheck/check/rfc-svg.svg.code +1 -0
  166. data/spec/fixtures/svgcheck/check/rfc-svg.svg.err +0 -0
  167. data/spec/fixtures/svgcheck/check/rfc-svg.svg.out +1 -0
  168. data/spec/fixtures/svgcheck/check/rfc.xml.code +1 -0
  169. data/spec/fixtures/svgcheck/check/rfc.xml.err +0 -0
  170. data/spec/fixtures/svgcheck/check/rfc.xml.out +2 -0
  171. data/spec/fixtures/svgcheck/check/rgb.svg.code +1 -0
  172. data/spec/fixtures/svgcheck/check/rgb.svg.err +0 -0
  173. data/spec/fixtures/svgcheck/check/rgb.svg.out +9 -0
  174. data/spec/fixtures/svgcheck/check/svg-wordle.svg.code +1 -0
  175. data/spec/fixtures/svgcheck/check/svg-wordle.svg.err +0 -0
  176. data/spec/fixtures/svgcheck/check/svg-wordle.svg.out +508 -0
  177. data/spec/fixtures/svgcheck/check/threshold.svg.code +1 -0
  178. data/spec/fixtures/svgcheck/check/threshold.svg.err +0 -0
  179. data/spec/fixtures/svgcheck/check/threshold.svg.out +20 -0
  180. data/spec/fixtures/svgcheck/check/utf8.svg.code +1 -0
  181. data/spec/fixtures/svgcheck/check/utf8.svg.err +0 -0
  182. data/spec/fixtures/svgcheck/check/utf8.svg.out +162 -0
  183. data/spec/fixtures/svgcheck/check/viewBox-both.svg.code +1 -0
  184. data/spec/fixtures/svgcheck/check/viewBox-both.svg.err +0 -0
  185. data/spec/fixtures/svgcheck/check/viewBox-both.svg.out +4 -0
  186. data/spec/fixtures/svgcheck/check/viewBox-height.svg.code +1 -0
  187. data/spec/fixtures/svgcheck/check/viewBox-height.svg.err +0 -0
  188. data/spec/fixtures/svgcheck/check/viewBox-height.svg.out +3 -0
  189. data/spec/fixtures/svgcheck/check/viewBox-none.svg.code +1 -0
  190. data/spec/fixtures/svgcheck/check/viewBox-none.svg.err +0 -0
  191. data/spec/fixtures/svgcheck/check/viewBox-none.svg.out +3 -0
  192. data/spec/fixtures/svgcheck/check/viewBox-width.svg.code +1 -0
  193. data/spec/fixtures/svgcheck/check/viewBox-width.svg.err +0 -0
  194. data/spec/fixtures/svgcheck/check/viewBox-width.svg.out +3 -0
  195. data/spec/fixtures/svgcheck/inputs/DrawBerry-sample-2.svg +28 -0
  196. data/spec/fixtures/svgcheck/inputs/IETF-test.svg +28 -0
  197. data/spec/fixtures/svgcheck/inputs/circle.svg +3 -0
  198. data/spec/fixtures/svgcheck/inputs/colors.svg +18 -0
  199. data/spec/fixtures/svgcheck/inputs/dia-sample-svg.svg +47 -0
  200. data/spec/fixtures/svgcheck/inputs/example-dot.svg +75 -0
  201. data/spec/fixtures/svgcheck/inputs/full-tiny.svg +16194 -0
  202. data/spec/fixtures/svgcheck/inputs/good.svg +19 -0
  203. data/spec/fixtures/svgcheck/inputs/httpbis-proxy20-fig6.svg +2 -0
  204. data/spec/fixtures/svgcheck/inputs/malformed.svg +11 -0
  205. data/spec/fixtures/svgcheck/inputs/rfc-svg.svg +1028 -0
  206. data/spec/fixtures/svgcheck/inputs/rfc.xml +37 -0
  207. data/spec/fixtures/svgcheck/inputs/rgb.svg +9 -0
  208. data/spec/fixtures/svgcheck/inputs/svg-wordle.svg +330 -0
  209. data/spec/fixtures/svgcheck/inputs/threshold.svg +26 -0
  210. data/spec/fixtures/svgcheck/inputs/utf8.svg +448 -0
  211. data/spec/fixtures/svgcheck/inputs/viewBox-both.svg +3 -0
  212. data/spec/fixtures/svgcheck/inputs/viewBox-height.svg +3 -0
  213. data/spec/fixtures/svgcheck/inputs/viewBox-none.svg +3 -0
  214. data/spec/fixtures/svgcheck/inputs/viewBox-width.svg +3 -0
  215. data/spec/fixtures/svgcheck/repair/DrawBerry-sample-2.svg.code +1 -0
  216. data/spec/fixtures/svgcheck/repair/DrawBerry-sample-2.svg.err +0 -0
  217. data/spec/fixtures/svgcheck/repair/DrawBerry-sample-2.svg.file +0 -0
  218. data/spec/fixtures/svgcheck/repair/DrawBerry-sample-2.svg.out +23 -0
  219. data/spec/fixtures/svgcheck/repair/IETF-test.svg.code +1 -0
  220. data/spec/fixtures/svgcheck/repair/IETF-test.svg.err +0 -0
  221. data/spec/fixtures/svgcheck/repair/IETF-test.svg.file +29 -0
  222. data/spec/fixtures/svgcheck/repair/IETF-test.svg.out +20 -0
  223. data/spec/fixtures/svgcheck/repair/circle.svg.code +1 -0
  224. data/spec/fixtures/svgcheck/repair/circle.svg.err +0 -0
  225. data/spec/fixtures/svgcheck/repair/circle.svg.file +4 -0
  226. data/spec/fixtures/svgcheck/repair/circle.svg.out +2 -0
  227. data/spec/fixtures/svgcheck/repair/colors.svg.code +1 -0
  228. data/spec/fixtures/svgcheck/repair/colors.svg.err +0 -0
  229. data/spec/fixtures/svgcheck/repair/colors.svg.file +19 -0
  230. data/spec/fixtures/svgcheck/repair/colors.svg.out +13 -0
  231. data/spec/fixtures/svgcheck/repair/dia-sample-svg.svg.code +1 -0
  232. data/spec/fixtures/svgcheck/repair/dia-sample-svg.svg.err +0 -0
  233. data/spec/fixtures/svgcheck/repair/dia-sample-svg.svg.file +47 -0
  234. data/spec/fixtures/svgcheck/repair/dia-sample-svg.svg.out +76 -0
  235. data/spec/fixtures/svgcheck/repair/example-dot.svg.code +1 -0
  236. data/spec/fixtures/svgcheck/repair/example-dot.svg.err +0 -0
  237. data/spec/fixtures/svgcheck/repair/example-dot.svg.file +69 -0
  238. data/spec/fixtures/svgcheck/repair/example-dot.svg.out +11 -0
  239. data/spec/fixtures/svgcheck/repair/good.svg.code +1 -0
  240. data/spec/fixtures/svgcheck/repair/good.svg.err +0 -0
  241. data/spec/fixtures/svgcheck/repair/good.svg.file +0 -0
  242. data/spec/fixtures/svgcheck/repair/good.svg.out +1 -0
  243. data/spec/fixtures/svgcheck/repair/httpbis-proxy20-fig6.svg.code +1 -0
  244. data/spec/fixtures/svgcheck/repair/httpbis-proxy20-fig6.svg.err +0 -0
  245. data/spec/fixtures/svgcheck/repair/httpbis-proxy20-fig6.svg.file +73 -0
  246. data/spec/fixtures/svgcheck/repair/httpbis-proxy20-fig6.svg.out +5 -0
  247. data/spec/fixtures/svgcheck/repair/malformed.svg.code +1 -0
  248. data/spec/fixtures/svgcheck/repair/malformed.svg.err +0 -0
  249. data/spec/fixtures/svgcheck/repair/malformed.svg.file +9 -0
  250. data/spec/fixtures/svgcheck/repair/malformed.svg.out +8 -0
  251. data/spec/fixtures/svgcheck/repair/rfc-svg.svg.code +1 -0
  252. data/spec/fixtures/svgcheck/repair/rfc-svg.svg.err +0 -0
  253. data/spec/fixtures/svgcheck/repair/rfc-svg.svg.file +0 -0
  254. data/spec/fixtures/svgcheck/repair/rfc-svg.svg.out +1 -0
  255. data/spec/fixtures/svgcheck/repair/rfc.xml.code +1 -0
  256. data/spec/fixtures/svgcheck/repair/rfc.xml.err +0 -0
  257. data/spec/fixtures/svgcheck/repair/rfc.xml.file +37 -0
  258. data/spec/fixtures/svgcheck/repair/rfc.xml.out +2 -0
  259. data/spec/fixtures/svgcheck/repair/rgb.svg.code +1 -0
  260. data/spec/fixtures/svgcheck/repair/rgb.svg.err +0 -0
  261. data/spec/fixtures/svgcheck/repair/rgb.svg.file +10 -0
  262. data/spec/fixtures/svgcheck/repair/rgb.svg.out +9 -0
  263. data/spec/fixtures/svgcheck/repair/svg-wordle.svg.code +1 -0
  264. data/spec/fixtures/svgcheck/repair/svg-wordle.svg.err +0 -0
  265. data/spec/fixtures/svgcheck/repair/svg-wordle.svg.file +112 -0
  266. data/spec/fixtures/svgcheck/repair/svg-wordle.svg.out +508 -0
  267. data/spec/fixtures/svgcheck/repair/threshold.svg.code +1 -0
  268. data/spec/fixtures/svgcheck/repair/threshold.svg.err +0 -0
  269. data/spec/fixtures/svgcheck/repair/threshold.svg.file +27 -0
  270. data/spec/fixtures/svgcheck/repair/threshold.svg.out +20 -0
  271. data/spec/fixtures/svgcheck/repair/utf8.svg.code +1 -0
  272. data/spec/fixtures/svgcheck/repair/utf8.svg.err +0 -0
  273. data/spec/fixtures/svgcheck/repair/utf8.svg.file +381 -0
  274. data/spec/fixtures/svgcheck/repair/utf8.svg.out +162 -0
  275. data/spec/fixtures/svgcheck/repair/viewBox-both.svg.code +1 -0
  276. data/spec/fixtures/svgcheck/repair/viewBox-both.svg.err +0 -0
  277. data/spec/fixtures/svgcheck/repair/viewBox-both.svg.file +4 -0
  278. data/spec/fixtures/svgcheck/repair/viewBox-both.svg.out +4 -0
  279. data/spec/fixtures/svgcheck/repair/viewBox-height.svg.code +1 -0
  280. data/spec/fixtures/svgcheck/repair/viewBox-height.svg.err +0 -0
  281. data/spec/fixtures/svgcheck/repair/viewBox-height.svg.file +4 -0
  282. data/spec/fixtures/svgcheck/repair/viewBox-height.svg.out +3 -0
  283. data/spec/fixtures/svgcheck/repair/viewBox-none.svg.code +1 -0
  284. data/spec/fixtures/svgcheck/repair/viewBox-none.svg.err +0 -0
  285. data/spec/fixtures/svgcheck/repair/viewBox-none.svg.file +4 -0
  286. data/spec/fixtures/svgcheck/repair/viewBox-none.svg.out +3 -0
  287. data/spec/fixtures/svgcheck/repair/viewBox-width.svg.code +1 -0
  288. data/spec/fixtures/svgcheck/repair/viewBox-width.svg.err +0 -0
  289. data/spec/fixtures/svgcheck/repair/viewBox-width.svg.file +4 -0
  290. data/spec/fixtures/svgcheck/repair/viewBox-width.svg.out +3 -0
  291. data/spec/fixtures/viewbox_required/inputs/missing_viewbox.svg +10 -0
  292. data/spec/fixtures/viewbox_required/repair/missing_viewbox.svg +10 -0
  293. data/spec/spec_helper.rb +16 -0
  294. data/spec/svg_conform/batch_report_spec.rb +99 -0
  295. data/spec/svg_conform/commands/check_command_spec.rb +90 -0
  296. data/spec/svg_conform/commands/profiles_command_spec.rb +20 -0
  297. data/spec/svg_conform/commands/svgcheck_compare_command_spec.rb +13 -0
  298. data/spec/svg_conform/commands/svgcheck_compatibility_command_spec.rb +13 -0
  299. data/spec/svg_conform/commands/svgcheck_generate_command_spec.rb +14 -0
  300. data/spec/svg_conform/profiles/base_profile_spec.rb +42 -0
  301. data/spec/svg_conform/profiles/lucid_fix_profile_spec.rb +46 -0
  302. data/spec/svg_conform/profiles/lucid_profile_spec.rb +84 -0
  303. data/spec/svg_conform/profiles/metanorma_profile_spec.rb +62 -0
  304. data/spec/svg_conform/profiles/no_external_css_profile_spec.rb +66 -0
  305. data/spec/svg_conform/profiles/svg_1_2_rfc_profile_spec.rb +200 -0
  306. data/spec/svg_conform/profiles/svg_1_2_rfc_with_rdf_profile_spec.rb +81 -0
  307. data/spec/svg_conform/remediations/color_remediation_spec.rb +95 -0
  308. data/spec/svg_conform/remediations/font_embedding_remediation_spec.rb +20 -0
  309. data/spec/svg_conform/remediations/font_remediation_spec.rb +95 -0
  310. data/spec/svg_conform/remediations/image_embedding_remediation_spec.rb +20 -0
  311. data/spec/svg_conform/remediations/invalid_id_references_remediation_spec.rb +97 -0
  312. data/spec/svg_conform/remediations/namespace_attribute_remediation_spec.rb +97 -0
  313. data/spec/svg_conform/remediations/namespace_remediation_spec.rb +95 -0
  314. data/spec/svg_conform/remediations/no_external_css_remediation_spec.rb +97 -0
  315. data/spec/svg_conform/remediations/style_promotion_remediation_spec.rb +97 -0
  316. data/spec/svg_conform/remediations/viewbox_remediation_spec.rb +95 -0
  317. data/spec/svg_conform/requirements/allowed_elements_requirement_spec.rb +118 -0
  318. data/spec/svg_conform/requirements/color_restrictions_requirement_spec.rb +168 -0
  319. data/spec/svg_conform/requirements/font_family_requirement_spec.rb +188 -0
  320. data/spec/svg_conform/requirements/forbidden_content_requirement_spec.rb +195 -0
  321. data/spec/svg_conform/requirements/id_reference_requirement_spec.rb +78 -0
  322. data/spec/svg_conform/requirements/invalid_id_references_requirement_spec.rb +78 -0
  323. data/spec/svg_conform/requirements/link_validation_requirement_spec.rb +78 -0
  324. data/spec/svg_conform/requirements/namespace_attributes_requirement_spec.rb +86 -0
  325. data/spec/svg_conform/requirements/namespace_requirement_spec.rb +184 -0
  326. data/spec/svg_conform/requirements/no_external_css_requirement_spec.rb +78 -0
  327. data/spec/svg_conform/requirements/no_external_fonts_requirement_spec.rb +20 -0
  328. data/spec/svg_conform/requirements/no_external_images_requirement_spec.rb +20 -0
  329. data/spec/svg_conform/requirements/style_promotion_requirement_spec.rb +78 -0
  330. data/spec/svg_conform/requirements/style_requirement_spec.rb +76 -0
  331. data/spec/svg_conform/requirements/viewbox_required_requirement_spec.rb +165 -0
  332. data/spec/svg_conform_spec.rb +32 -0
  333. data/spec/svgcheck_compatibility_spec.rb +355 -0
  334. data/svg_conform.gemspec +35 -0
  335. metadata +436 -0
@@ -0,0 +1,359 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "paint"
4
+
5
+ module SvgConform
6
+ module Compatibility
7
+ # Handles formatting and display of compatibility analysis results
8
+ class ReportFormatter
9
+ def initialize(context)
10
+ @context = context
11
+ end
12
+
13
+ def display_single_file_result(result)
14
+ if @context.semantic_analysis?
15
+ display_semantic_single_result(result)
16
+ else
17
+ display_basic_single_result(result)
18
+ end
19
+ end
20
+
21
+ def display_batch_results(results)
22
+ if @context.semantic_analysis?
23
+ display_semantic_batch_results(results)
24
+ else
25
+ display_basic_batch_results(results)
26
+ end
27
+ end
28
+
29
+ def write_output_file(results)
30
+ return unless @context.file_output?
31
+
32
+ content = generate_output_content(results)
33
+ File.write(@context.output_file, content)
34
+ puts "📄 Results written to: #{@context.output_file}"
35
+ end
36
+
37
+ private
38
+
39
+ def display_semantic_single_result(result)
40
+ display_file_header(result.filename)
41
+ display_semantic_validation_section(result)
42
+ display_semantic_repair_section(result) if @context.repair_mode?
43
+ display_xml_equivalence_section(result) if result.xml_equivalence
44
+ end
45
+
46
+ def display_basic_single_result(result)
47
+ display_file_header(result.filename)
48
+ display_basic_validation_section(result)
49
+ display_basic_repair_section(result) if @context.repair_mode?
50
+ end
51
+
52
+ def display_semantic_batch_results(results)
53
+ display_batch_header(results.length)
54
+ display_semantic_summary_table(results)
55
+ display_semantic_batch_statistics(results)
56
+ end
57
+
58
+ def display_basic_batch_results(results)
59
+ display_batch_header(results.length)
60
+ display_basic_summary_table(results)
61
+ display_basic_batch_statistics(results)
62
+ end
63
+
64
+ def display_file_header(filename)
65
+ mode_icon = @context.repair_mode? ? "🔧" : "📋"
66
+ mode_text = @context.repair_mode? ? "Repair Analysis" : "Analysis"
67
+ analysis_type = @context.semantic_analysis? ? "Semantic" : "Basic"
68
+
69
+ puts Paint["#{mode_icon} #{analysis_type} #{mode_text}: #{filename}",
70
+ :cyan, :bold]
71
+ puts "=" * 60
72
+ end
73
+
74
+ def display_batch_header(file_count)
75
+ mode_icon = @context.repair_mode? ? "🔧" : "📋"
76
+ mode_text = @context.repair_mode? ? "Repair Analysis" : "Analysis"
77
+ analysis_type = @context.semantic_analysis? ? "Semantic" : "Basic"
78
+
79
+ puts Paint["#{mode_icon} #{analysis_type} Batch #{mode_text} (#{file_count} files)",
80
+ :cyan, :bold]
81
+ puts "=" * 80
82
+ end
83
+
84
+ def display_semantic_validation_section(result)
85
+ return unless result.validation_comparison
86
+
87
+ puts Paint["📊 Validation Comparison:", :blue, :bold]
88
+
89
+ score = result.compatibility_score
90
+ score_color = if score >= 90
91
+ :green
92
+ else
93
+ score >= 70 ? :yellow : :red
94
+ end
95
+ puts " Compatibility Score: #{Paint[format('%.1f%%', score),
96
+ score_color, :bold]}"
97
+
98
+ validity_status = result.validity_match? ? "✅ Match" : "❌ Mismatch"
99
+ puts " Validity Match: #{validity_status}"
100
+ puts " SvgConform Valid: #{result.svg_conform_valid? ? '✅' : '❌'}"
101
+ puts " Svgcheck Valid: #{result.svgcheck_valid? ? '✅' : '❌'}"
102
+ puts
103
+ end
104
+
105
+ def display_semantic_repair_section(result)
106
+ return unless result.validation_comparison
107
+
108
+ puts Paint["🔧 Repair Results:", :blue, :bold]
109
+
110
+ if result.successful_remediation?
111
+ puts " Remediation: #{Paint['✅ Successful', :green, :bold]}"
112
+ else
113
+ puts " Remediation: #{Paint['❌ Failed', :red, :bold]}"
114
+ end
115
+
116
+ if result.content_comparison
117
+ equivalence = result.content_equivalence_score
118
+ equiv_color = if equivalence >= 90
119
+ :green
120
+ else
121
+ equivalence >= 70 ? :yellow : :red
122
+ end
123
+ puts " Content Equivalence: #{Paint[format('%.1f%%', equivalence),
124
+ equiv_color, :bold]}"
125
+ end
126
+ puts
127
+ end
128
+
129
+ def display_xml_equivalence_section(result)
130
+ puts Paint["🔍 XML Equivalence:", :blue, :bold]
131
+
132
+ if result.xml_error
133
+ puts " Status: #{Paint['❌ Error', :red, :bold]}"
134
+ puts " Error: #{result.xml_error}"
135
+ elsif result.xml_equivalent?
136
+ puts " Status: #{Paint['✅ Equivalent', :green, :bold]}"
137
+ else
138
+ puts " Status: #{Paint['❌ Different', :red, :bold]}"
139
+ puts " Differences: #{result.xml_differences.length}"
140
+ end
141
+ puts
142
+ end
143
+
144
+ def display_basic_validation_section(result)
145
+ puts Paint["📊 Validation Results:", :blue, :bold]
146
+ puts " SvgConform Valid: #{result.svg_conform_valid? ? '✅' : '❌'}"
147
+ puts " Svgcheck Valid: #{result.svgcheck_valid? ? '✅' : '❌'}"
148
+ puts " Validity Match: #{result.validity_match? ? '✅' : '❌'}"
149
+ puts
150
+ end
151
+
152
+ def display_basic_repair_section(result)
153
+ puts Paint["🔧 Repair Results:", :blue, :bold]
154
+
155
+ if result.successful_remediation?
156
+ puts " Remediation: #{Paint['✅ Successful', :green, :bold]}"
157
+ puts " Issues Fixed: #{result.issues_fixed}"
158
+ puts " Remediations Applied: #{result.remediations_applied}"
159
+ else
160
+ puts " Remediation: #{Paint['❌ Failed', :red, :bold]}"
161
+ end
162
+ puts
163
+ end
164
+
165
+ def display_semantic_summary_table(results)
166
+ puts Paint["📊 Summary Table:", :blue, :bold]
167
+ puts
168
+
169
+ header = format("%-25s %12s %8s %12s %12s",
170
+ "File", "Compatibility", "Valid", "SvgConform", "Svgcheck")
171
+ puts Paint[header, :white, :bold]
172
+ puts "-" * 70
173
+
174
+ results.each do |result|
175
+ score = format("%.1f%%", result.compatibility_score)
176
+ score_color = if result.compatibility_score >= 90
177
+ :green
178
+ else
179
+ result.compatibility_score >= 70 ? :yellow : :red
180
+ end
181
+
182
+ validity = result.validity_match? ? "✅" : "❌"
183
+ svg_conform = result.svg_conform_valid? ? "✅" : "❌"
184
+ svgcheck = result.svgcheck_valid? ? "✅" : "❌"
185
+
186
+ row = format("%-25s %12s %8s %12s %12s",
187
+ result.filename.truncate(23),
188
+ Paint[score, score_color],
189
+ validity,
190
+ svg_conform,
191
+ svgcheck)
192
+ puts row
193
+ end
194
+ puts
195
+ end
196
+
197
+ def display_basic_summary_table(results)
198
+ puts Paint["📊 Summary Table:", :blue, :bold]
199
+ puts
200
+
201
+ header = format("%-25s %8s %12s %12s %8s",
202
+ "File", "Valid", "SvgConform", "Svgcheck", "Errors")
203
+ puts Paint[header, :white, :bold]
204
+ puts "-" * 66
205
+
206
+ results.each do |result|
207
+ validity = result.validity_match? ? "✅" : "❌"
208
+ svg_conform = result.svg_conform_valid? ? "✅" : "❌"
209
+ svgcheck = result.svgcheck_valid? ? "✅" : "❌"
210
+ errors = result.error_count
211
+
212
+ row = format("%-25s %8s %12s %12s %8d",
213
+ result.filename.truncate(23),
214
+ validity,
215
+ svg_conform,
216
+ svgcheck,
217
+ errors)
218
+ puts row
219
+ end
220
+ puts
221
+ end
222
+
223
+ def display_semantic_batch_statistics(results)
224
+ puts Paint["📈 Statistics:", :blue, :bold]
225
+
226
+ avg_compatibility = results.sum(&:compatibility_score) / results.length
227
+ validity_matches = results.count(&:validity_match?)
228
+ successful_remediations = results.count(&:successful_remediation?) if @context.repair_mode?
229
+
230
+ score_color = if avg_compatibility >= 90
231
+ :green
232
+ else
233
+ avg_compatibility >= 70 ? :yellow : :red
234
+ end
235
+ puts " Average Compatibility: #{Paint[format('%.1f%%', avg_compatibility),
236
+ score_color, :bold]}"
237
+ puts " Validity Matches: #{validity_matches}/#{results.length} (#{format('%.1f%%',
238
+ validity_matches * 100.0 / results.length)})"
239
+
240
+ if @context.repair_mode?
241
+ puts " Successful Remediations: #{successful_remediations}/#{results.length} (#{format('%.1f%%',
242
+ successful_remediations * 100.0 / results.length)})"
243
+ end
244
+ puts
245
+ end
246
+
247
+ def display_basic_batch_statistics(results)
248
+ puts Paint["📈 Statistics:", :blue, :bold]
249
+
250
+ validity_matches = results.count(&:validity_match?)
251
+ successful_remediations = results.count(&:successful_remediation?) if @context.repair_mode?
252
+ total_errors = results.sum(&:error_count)
253
+
254
+ puts " Validity Matches: #{validity_matches}/#{results.length} (#{format('%.1f%%',
255
+ validity_matches * 100.0 / results.length)})"
256
+ puts " Total Errors: #{total_errors}"
257
+
258
+ if @context.repair_mode?
259
+ puts " Successful Remediations: #{successful_remediations}/#{results.length} (#{format('%.1f%%',
260
+ successful_remediations * 100.0 / results.length)})"
261
+ end
262
+ puts
263
+ end
264
+
265
+ def generate_output_content(results)
266
+ content = []
267
+ content << "# Compatibility Analysis Results"
268
+ content << ""
269
+ content << "Analysis Type: #{@context.semantic_analysis? ? 'Semantic' : 'Basic'}"
270
+ content << "Mode: #{@context.mode.to_s.capitalize}"
271
+ content << "Profile: #{@context.profile}"
272
+ content << "Generated: #{Time.now}"
273
+ content << ""
274
+
275
+ if results.is_a?(Array)
276
+ generate_batch_output_content(content, results)
277
+ else
278
+ generate_single_output_content(content, results)
279
+ end
280
+
281
+ content.join("\n")
282
+ end
283
+
284
+ def generate_batch_output_content(content, results)
285
+ content << "## Summary"
286
+ content << ""
287
+
288
+ if @context.semantic_analysis?
289
+ avg_compatibility = results.sum(&:compatibility_score) / results.length
290
+ content << "Average Compatibility: #{format('%.1f%%',
291
+ avg_compatibility)}"
292
+ end
293
+
294
+ validity_matches = results.count(&:validity_match?)
295
+ content << "Validity Matches: #{validity_matches}/#{results.length}"
296
+
297
+ if @context.repair_mode?
298
+ successful_remediations = results.count(&:successful_remediation?)
299
+ content << "Successful Remediations: #{successful_remediations}/#{results.length}"
300
+ end
301
+
302
+ content << ""
303
+ content << "## Individual Results"
304
+ content << ""
305
+
306
+ results.each do |result|
307
+ content << "### #{result.filename}"
308
+ content << ""
309
+ add_result_details(content, result)
310
+ content << ""
311
+ end
312
+ end
313
+
314
+ def generate_single_output_content(content, result)
315
+ content << "## #{result.filename}"
316
+ content << ""
317
+ add_result_details(content, result)
318
+ end
319
+
320
+ def add_result_details(content, result)
321
+ if @context.semantic_analysis?
322
+ content << "Compatibility Score: #{format('%.1f%%',
323
+ result.compatibility_score)}"
324
+ end
325
+
326
+ content << "Validity Match: #{result.validity_match?}"
327
+ content << "SvgConform Valid: #{result.svg_conform_valid?}"
328
+ content << "Svgcheck Valid: #{result.svgcheck_valid?}"
329
+
330
+ return unless @context.repair_mode?
331
+
332
+ content << "Successful Remediation: #{result.successful_remediation?}"
333
+
334
+ if result.content_comparison
335
+ content << "Content Equivalence: #{format('%.1f%%',
336
+ result.content_equivalence_score)}"
337
+ end
338
+
339
+ return unless result.xml_equivalence
340
+
341
+ content << "XML Equivalent: #{result.xml_equivalent?}"
342
+ return unless result.xml_error
343
+
344
+ content << "XML Error: #{result.xml_error}"
345
+ end
346
+ end
347
+ end
348
+ end
349
+
350
+ # String extension for truncation
351
+ class String
352
+ def truncate(length)
353
+ if self.length > length
354
+ "#{self[0..length - 4]}..."
355
+ else
356
+ self
357
+ end
358
+ end
359
+ end
@@ -0,0 +1,316 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../semantic_comparator"
4
+ require_relative "../remediation_runner"
5
+
6
+ module SvgConform
7
+ module Compatibility
8
+ # Handles SVG-specific analysis operations
9
+ class SvgAnalysisEngine
10
+ def initialize(context, file_processor)
11
+ @context = context
12
+ @file_processor = file_processor
13
+ end
14
+
15
+ def analyze_file(filename, svgcheck_report)
16
+ if @context.check_mode?
17
+ analyze_validation(filename, svgcheck_report)
18
+ else
19
+ analyze_repair(filename, svgcheck_report)
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def analyze_validation(filename, svgcheck_report)
26
+ input_file = @file_processor.input_file_path(filename)
27
+ svg_conform_result = run_validation(input_file)
28
+ svg_conform_report = create_conformance_report(filename,
29
+ svg_conform_result)
30
+
31
+ if @context.semantic_analysis?
32
+ build_semantic_validation_result(filename, svg_conform_report,
33
+ svgcheck_report)
34
+ else
35
+ build_basic_validation_result(filename, svg_conform_report,
36
+ svgcheck_report)
37
+ end
38
+ end
39
+
40
+ def analyze_repair(filename, svgcheck_report)
41
+ input_file = @file_processor.input_file_path(filename)
42
+ svg_conform_result = run_remediation(input_file, filename)
43
+
44
+ if @context.semantic_analysis?
45
+ build_semantic_repair_result(filename, svg_conform_result,
46
+ svgcheck_report)
47
+ else
48
+ build_basic_repair_result(filename, svg_conform_result,
49
+ svgcheck_report)
50
+ end
51
+ end
52
+
53
+ def run_validation(input_file)
54
+ validator = SvgConform::Validator.new
55
+ validator.validate_file(input_file, profile: @context.profile)
56
+ end
57
+
58
+ def run_remediation(input_file, filename)
59
+ remediation_runner = SvgConform::RemediationRunner.new(profile: @context.profile)
60
+ svg_content = File.read(input_file)
61
+ remediation_runner.run_remediation(svg_content, filename: filename)
62
+ end
63
+
64
+ def create_conformance_report(filename, svg_conform_result)
65
+ SvgConform::ConformanceReport.from_svg_conform_result(
66
+ filename,
67
+ svg_conform_result,
68
+ profile: @context.profile,
69
+ use_svgcheck_mapping: true,
70
+ )
71
+ end
72
+
73
+ def build_semantic_validation_result(filename, svg_conform_report,
74
+ svgcheck_report)
75
+ validation_comparison = SvgConform::SemanticComparator.compare_validation_results(
76
+ svg_conform_report,
77
+ svgcheck_report,
78
+ )
79
+
80
+ ComparisonResult.new(
81
+ filename: filename,
82
+ type: :semantic_validation,
83
+ validation_comparison: validation_comparison,
84
+ )
85
+ end
86
+
87
+ def build_basic_validation_result(filename, svg_conform_report,
88
+ svgcheck_report)
89
+ ComparisonResult.new(
90
+ filename: filename,
91
+ type: :basic_validation,
92
+ svg_conform_result: svg_conform_report,
93
+ svgcheck_result: svgcheck_report,
94
+ )
95
+ end
96
+
97
+ def build_semantic_repair_result(filename, svg_conform_result,
98
+ svgcheck_report)
99
+ svg_conform_report = svg_conform_result.generate_conformance_report
100
+ validation_comparison = SvgConform::SemanticComparator.compare_validation_results(
101
+ svg_conform_report,
102
+ svgcheck_report,
103
+ )
104
+
105
+ content_comparison = build_content_comparison(svg_conform_result,
106
+ svgcheck_report)
107
+ xml_equivalence = build_xml_equivalence(filename, svg_conform_result)
108
+
109
+ ComparisonResult.new(
110
+ filename: filename,
111
+ type: :semantic_repair,
112
+ validation_comparison: validation_comparison,
113
+ content_comparison: content_comparison,
114
+ xml_equivalence: xml_equivalence,
115
+ )
116
+ end
117
+
118
+ def build_basic_repair_result(filename, svg_conform_result,
119
+ svgcheck_report)
120
+ ComparisonResult.new(
121
+ filename: filename,
122
+ type: :basic_repair,
123
+ svg_conform_result: svg_conform_result,
124
+ svgcheck_result: svgcheck_report,
125
+ )
126
+ end
127
+
128
+ def build_content_comparison(svg_conform_result, svgcheck_report)
129
+ return nil unless svgcheck_report.respond_to?(:remediated_content) &&
130
+ svg_conform_result.remediated_content
131
+
132
+ SvgConform::SemanticComparator.compare_remediated_content(
133
+ svg_conform_result.remediated_content,
134
+ svgcheck_report.remediated_content,
135
+ )
136
+ end
137
+
138
+ def build_xml_equivalence(filename, svg_conform_result)
139
+ return nil unless svg_conform_result.remediated_content
140
+
141
+ unless @file_processor.svgcheck_repaired_file_exists?(filename)
142
+ return {
143
+ error: "Svgcheck repaired file not found: #{@file_processor.svgcheck_repaired_file_path(filename)}",
144
+ xml_equivalent: false,
145
+ }
146
+ end
147
+
148
+ begin
149
+ svgcheck_content = @file_processor.read_svgcheck_repaired_content(filename)
150
+ compare_xml_content(svg_conform_result.remediated_content,
151
+ svgcheck_content)
152
+ rescue StandardError => e
153
+ {
154
+ error: "Error comparing XML files: #{e.message}",
155
+ xml_equivalent: false,
156
+ }
157
+ end
158
+ end
159
+
160
+ def compare_xml_content(svg_conform_content, svgcheck_content)
161
+ require "equivalent-xml"
162
+
163
+ xml_equivalent = EquivalentXml.equivalent?(svg_conform_content, svgcheck_content, {
164
+ element_order: false,
165
+ normalize_whitespace: true,
166
+ })
167
+
168
+ if xml_equivalent
169
+ {
170
+ xml_equivalent: true,
171
+ differences: [],
172
+ }
173
+ else
174
+ differences = analyze_xml_differences(svg_conform_content,
175
+ svgcheck_content)
176
+ {
177
+ xml_equivalent: false,
178
+ differences: differences,
179
+ }
180
+ end
181
+ end
182
+
183
+ def analyze_xml_differences(svg_conform_content, svgcheck_content)
184
+ differences = []
185
+
186
+ begin
187
+ require "moxml"
188
+ moxml = Moxml.new
189
+
190
+ svg_conform_doc = moxml.parse(svg_conform_content)
191
+ svgcheck_doc = moxml.parse(svgcheck_content)
192
+
193
+ svg_conform_root = svg_conform_doc.root
194
+ svgcheck_root = svgcheck_doc.root
195
+
196
+ # Compare element names
197
+ if svg_conform_root.name != svgcheck_root.name
198
+ differences << {
199
+ type: :root_element_name,
200
+ svg_conform: svg_conform_root.name,
201
+ svgcheck: svgcheck_root.name,
202
+ }
203
+ end
204
+
205
+ # Compare attributes and children recursively
206
+ differences.concat(compare_element_attributes(svg_conform_root,
207
+ svgcheck_root))
208
+ differences.concat(compare_child_elements(svg_conform_root,
209
+ svgcheck_root))
210
+ rescue StandardError => e
211
+ differences << {
212
+ type: :parsing_error,
213
+ error: e.message,
214
+ }
215
+ end
216
+
217
+ differences
218
+ end
219
+
220
+ def compare_element_attributes(elem1, elem2)
221
+ differences = []
222
+ attrs1 = normalize_attributes(elem1.attributes)
223
+ attrs2 = normalize_attributes(elem2.attributes)
224
+ all_attr_names = (attrs1.keys + attrs2.keys).uniq
225
+
226
+ all_attr_names.each do |attr_name|
227
+ val1 = attrs1[attr_name]
228
+ val2 = attrs2[attr_name]
229
+
230
+ next unless val1 != val2
231
+
232
+ differences << {
233
+ type: :attribute_difference,
234
+ element: elem1.name,
235
+ attribute: attr_name,
236
+ svg_conform: val1,
237
+ svgcheck: val2,
238
+ }
239
+ end
240
+
241
+ differences
242
+ end
243
+
244
+ def normalize_attributes(attributes)
245
+ return {} if attributes.nil?
246
+
247
+ case attributes
248
+ when Hash
249
+ attributes
250
+ when Array
251
+ attributes.each_with_object({}) do |attr, hash|
252
+ if attr.respond_to?(:name) && attr.respond_to?(:value)
253
+ hash[attr.name] = attr.value
254
+ elsif attr.is_a?(Array) && attr.length == 2
255
+ hash[attr[0]] = attr[1]
256
+ end
257
+ end
258
+ else
259
+ {}
260
+ end
261
+ end
262
+
263
+ def compare_child_elements(elem1, elem2)
264
+ differences = []
265
+ children1 = elem1.children.select { |child| child.respond_to?(:name) }
266
+ children2 = elem2.children.select { |child| child.respond_to?(:name) }
267
+
268
+ if children1.length != children2.length
269
+ differences << {
270
+ type: :child_count_difference,
271
+ element: elem1.name,
272
+ svg_conform_count: children1.length,
273
+ svgcheck_count: children2.length,
274
+ }
275
+ end
276
+
277
+ max_children = [children1.length, children2.length].max
278
+ (0...max_children).each do |i|
279
+ child1 = children1[i]
280
+ child2 = children2[i]
281
+
282
+ if child1 && child2
283
+ if child1.name == child2.name
284
+ differences.concat(compare_element_attributes(child1, child2))
285
+ differences.concat(compare_child_elements(child1, child2))
286
+ else
287
+ differences << {
288
+ type: :child_element_name,
289
+ parent: elem1.name,
290
+ position: i,
291
+ svg_conform: child1.name,
292
+ svgcheck: child2.name,
293
+ }
294
+ end
295
+ elsif child1
296
+ differences << {
297
+ type: :extra_child_svg_conform,
298
+ parent: elem1.name,
299
+ position: i,
300
+ element: child1.name,
301
+ }
302
+ elsif child2
303
+ differences << {
304
+ type: :extra_child_svgcheck,
305
+ parent: elem1.name,
306
+ position: i,
307
+ element: child2.name,
308
+ }
309
+ end
310
+ end
311
+
312
+ differences
313
+ end
314
+ end
315
+ end
316
+ end