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,199 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SvgConform
4
+ module Constants
5
+ # SVG elements allowed in SVG 1.2 RFC (based on RFC 7996 and svgcheck)
6
+ SVG_ELEMENTS = {
7
+ "svg" => %w[version baseProfile width viewBox preserveAspectRatio snapshotTime
8
+ height id role break color-rendering fill-rule],
9
+ "desc" => %w[id role shape-rendering text-rendering buffered-rendering
10
+ visibility],
11
+ "title" => %w[id role shape-rendering text-rendering buffered-rendering
12
+ visibility],
13
+ "path" => %w[d pathLength stroke-miterlimit id role fill style
14
+ transform font-size fill-rule],
15
+ "rect" => %w[x y width height rx ry stroke-miterlimit
16
+ id role fill style transform fill-rule],
17
+ "circle" => %w[cx cy r id role fill style transform fill-rule],
18
+ "line" => %w[x1 y1 x2 y2 id role fill transform fill-rule],
19
+ "ellipse" => %w[cx cy rx ry id role fill style transform fill-rule],
20
+ "polyline" => %w[points id role fill transform fill-rule],
21
+ "polygon" => %w[points id role fill style transform fill-rule],
22
+ "solidColor" => %w[id role fill fill-rule],
23
+ "textArea" => %w[x y width height auto id role fill transform fill-rule],
24
+ "text" => %w[x y rotate id role fill style transform font-size fill-rule],
25
+ "g" => %w[label class id role fill style transform fill-rule visibility],
26
+ "defs" => %w[id role fill fill-rule],
27
+ "use" => %w[x y href id role fill transform fill-rule],
28
+ "a" => %w[id role fill transform fill-rule target],
29
+ "tspan" => %w[x y id role fill fill-rule],
30
+ "tbreak" => %w[id role],
31
+ }.freeze
32
+
33
+ # SVG properties with their allowed values
34
+ SVG_PROPERTIES = {
35
+ "about" => [],
36
+ "base" => [],
37
+ "baseProfile" => [],
38
+ "d" => [],
39
+ "break" => [],
40
+ "class" => [],
41
+ "content" => [],
42
+ "cx" => ["<number>"],
43
+ "cy" => ["<number>"],
44
+ "datatype" => [],
45
+ "height" => ["<number>"],
46
+ "href" => [],
47
+ "id" => [],
48
+ "label" => [],
49
+ "lang" => [],
50
+ "pathLength" => [],
51
+ "points" => [],
52
+ "preserveAspectRatio" => [],
53
+ "property" => [],
54
+ "r" => ["<number>"],
55
+ "rel" => [],
56
+ "resource" => [],
57
+ "rev" => [],
58
+ "role" => [],
59
+ "rotate" => [],
60
+ "rx" => ["<number>"],
61
+ "ry" => ["<number>"],
62
+ "space" => [],
63
+ "snapshotTime" => [],
64
+ "transform" => [],
65
+ "typeof" => [],
66
+ "version" => [],
67
+ "width" => ["<number>"],
68
+ "viewBox" => ["<number>"],
69
+ "x" => ["<number>"],
70
+ "x1" => ["<number>"],
71
+ "x2" => ["<number>"],
72
+ "y" => ["<number>"],
73
+ "y1" => ["<number>"],
74
+ "y2" => ["<number>"],
75
+
76
+ "stroke" => ["none", "<paint>"],
77
+ "stroke-width" => [],
78
+ "stroke-linecap" => %w[butt round square inherit],
79
+ "stroke-linejoin" => %w[miter round bevel inherit],
80
+ "stroke-miterlimit" => [],
81
+ "stroke-dasharray" => [],
82
+ "stroke-dashoffset" => [],
83
+ "stroke-opacity" => [],
84
+ "vector-effect" => %w[non-scaling-stroke none inherit],
85
+ "viewport-fill" => ["none", "currentColor", "inherit", "<color>"],
86
+
87
+ "display" => %w[inline block list-item run-in compact table inline-table
88
+ table-row-group table-header-group table-footer-group
89
+ table-row table-column-group table-column table-cell
90
+ table-caption none inherit],
91
+ "viewport-fill-opacity" => [],
92
+ "visibility" => %w[visible hidden collapse inherit],
93
+ "image-rendering" => %w[auto optimizeSpeed optimizeQuality inherit],
94
+ "color-rendering" => %w[auto optimizeSpeed optimizeQuality inherit],
95
+ "shape-rendering" => %w[auto optimizeSpeed crispEdges geometricPrecision
96
+ inherit],
97
+ "text-rendering" => %w[auto optimizeSpeed optimizeLegibility
98
+ geometricPrecision inherit],
99
+ "buffered-rendering" => %w[auto dynamic static inherit],
100
+
101
+ "solid-opacity" => [],
102
+ "solid-color" => ["currentColor", "inherit", "<color>"],
103
+ "color" => ["currentColor", "inherit", "<color>"],
104
+
105
+ "stop-color" => ["currentColor", "inherit", "<color>"],
106
+ "stop-opacity" => [],
107
+
108
+ "line-increment" => [],
109
+ "text-align" => %w[start end center inherit],
110
+ "display-align" => %w[auto before center after inherit],
111
+
112
+ "font-size" => [],
113
+ "font-family" => %w[serif sans-serif monospace inherit],
114
+ "font-weight" => %w[normal bold bolder lighter inherit 100 200 300 400
115
+ 500 600 700 800 900],
116
+ "font-style" => %w[normal italic oblique inherit],
117
+ "font-variant" => %w[normal small-caps inherit],
118
+ "direction" => %w[ltr rtl inherit],
119
+ "unicode-bidi" => %w[normal embed bidi-override inherit],
120
+ "text-anchor" => %w[start middle end inherit],
121
+ "fill" => ["none", "inherit", "<color>"],
122
+ "fill-rule" => %w[nonzero evenodd inherit],
123
+ "fill-opacity" => [],
124
+
125
+ "requiredFeatures" => [],
126
+ "requiredFormats" => [],
127
+ "requiredExtensions" => [],
128
+ "requiredFonts" => [],
129
+ "systemLanguage" => [],
130
+ }.freeze
131
+
132
+ # Basic types for validation
133
+ BASIC_TYPES = {
134
+ "<color>" => %w[black #ffffff #FFFFFF white #000000],
135
+ "<paint>" => ["none", "currentColor", "inherit", "<color>"],
136
+ "<integer>" => ["+"],
137
+ "<number>" => ["+"],
138
+ }.freeze
139
+
140
+ # Default color for replacements
141
+ COLOR_DEFAULT = "black"
142
+
143
+ # Color threshold for grayscale conversion (from Python code)
144
+ COLOR_THRESHOLD = 764
145
+
146
+ # Style properties that can be promoted to attributes
147
+ STYLE_PROPERTIES = %w[
148
+ font-family font-weight font-style font-variant direction unicode-bidi
149
+ text-anchor fill fill-rule stroke stroke-width font-size fill-opacity
150
+ stroke-linecap stroke-opacity stroke-linejoin
151
+ ].freeze
152
+
153
+ # Elements allowed within other elements
154
+ SVG_CHILD_ELEMENTS = %w[
155
+ title path rect circle line ellipse polyline polygon solidColor
156
+ textArea text g defs use a tspan desc
157
+ ].freeze
158
+
159
+ TEXT_CHILD_ELEMENTS = %w[desc title tspan text a].freeze
160
+
161
+ ELEMENT_CHILDREN = {
162
+ "svg" => SVG_CHILD_ELEMENTS,
163
+ "desc" => ["text"],
164
+ "title" => ["text"],
165
+ "path" => %w[title desc],
166
+ "rect" => %w[title desc],
167
+ "circle" => %w[title desc],
168
+ "line" => %w[title desc],
169
+ "ellipse" => %w[title desc],
170
+ "polyline" => %w[title desc],
171
+ "polygon" => %w[title desc],
172
+ "solidColor" => %w[title desc],
173
+ "textArea" => TEXT_CHILD_ELEMENTS,
174
+ "text" => TEXT_CHILD_ELEMENTS,
175
+ "g" => SVG_CHILD_ELEMENTS,
176
+ "defs" => SVG_CHILD_ELEMENTS,
177
+ "use" => %w[title desc],
178
+ "a" => SVG_CHILD_ELEMENTS,
179
+ "tspan" => TEXT_CHILD_ELEMENTS + ["tbreak"],
180
+ }.freeze
181
+
182
+ # Allowed SVG namespaces
183
+ SVG_NAMESPACES = [
184
+ "http://www.w3.org/2000/svg",
185
+ ].freeze
186
+
187
+ # Allowed XML namespaces
188
+ XMLNS_NAMESPACES = [
189
+ "http://www.w3.org/2000/svg",
190
+ "http://www.w3.org/1999/xlink",
191
+ "http://www.w3.org/XML/1998/namespace",
192
+ ].freeze
193
+
194
+ # Color mapping for common color names to RFC-compliant colors
195
+ COLOR_MAP = {
196
+ "rgb(0,0,0)" => "black",
197
+ }.freeze
198
+ end
199
+ end
@@ -0,0 +1,262 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SvgConform
4
+ # Handles CSS color equivalence, normalization, and conversion
5
+ class CssColor
6
+ # Named colors mapping to their hex equivalents
7
+ NAMED_COLORS = {
8
+ "black" => "#000000",
9
+ "white" => "#ffffff",
10
+ "red" => "#ff0000",
11
+ "green" => "#008000",
12
+ "blue" => "#0000ff",
13
+ "yellow" => "#ffff00",
14
+ "cyan" => "#00ffff",
15
+ "magenta" => "#ff00ff",
16
+ "silver" => "#c0c0c0",
17
+ "gray" => "#808080",
18
+ "grey" => "#808080",
19
+ "maroon" => "#800000",
20
+ "olive" => "#808000",
21
+ "lime" => "#00ff00",
22
+ "aqua" => "#00ffff",
23
+ "teal" => "#008080",
24
+ "navy" => "#000080",
25
+ "fuchsia" => "#ff00ff",
26
+ "purple" => "#800080",
27
+ }.freeze
28
+
29
+ # Reverse mapping for canonical named color lookup
30
+ HEX_TO_NAMED = NAMED_COLORS.invert.freeze
31
+
32
+ # CSS color keywords that have special meaning
33
+ SPECIAL_KEYWORDS = %w[inherit currentcolor transparent none].freeze
34
+
35
+ class << self
36
+ # Normalize a color to its canonical hex representation
37
+ def normalize(color)
38
+ return nil if color.nil? || color.empty?
39
+
40
+ color = color.strip.downcase
41
+
42
+ # Handle special keywords
43
+ return color if SPECIAL_KEYWORDS.include?(color)
44
+
45
+ # Handle named colors
46
+ return NAMED_COLORS[color] if NAMED_COLORS.key?(color)
47
+
48
+ # Handle hex colors
49
+ if color.match?(/^#[0-9a-f]{3}$/)
50
+ # Expand short hex: #fff → #ffffff
51
+ return expand_short_hex(color)
52
+ elsif color.match?(/^#[0-9a-f]{6}$/)
53
+ # Already normalized 6-digit hex
54
+ return color
55
+ end
56
+
57
+ # Handle RGB functions with integers
58
+ rgb_match = color.match(/^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/)
59
+ if rgb_match
60
+ r, g, b = rgb_match[1..3].map(&:to_i)
61
+ return rgb_to_hex(r, g, b)
62
+ end
63
+
64
+ # Handle RGB functions with percentages
65
+ rgb_percent_match = color.match(/^rgb\s*\(\s*(\d+(?:\.\d+)?)%\s*,\s*(\d+(?:\.\d+)?)%\s*,\s*(\d+(?:\.\d+)?)%\s*\)$/)
66
+ if rgb_percent_match
67
+ r = (rgb_percent_match[1].to_f * 255 / 100).round
68
+ g = (rgb_percent_match[2].to_f * 255 / 100).round
69
+ b = (rgb_percent_match[3].to_f * 255 / 100).round
70
+ return rgb_to_hex(r, g, b)
71
+ end
72
+
73
+ # Handle mixed RGB functions (percentage and absolute values)
74
+ rgb_mixed_match = color.match(/^rgb\s*\(\s*(\d+(?:\.\d+)?%?)\s*,\s*(\d+(?:\.\d+)?%?)\s*,\s*(\d+(?:\.\d+)?%?)\s*\)$/)
75
+ if rgb_mixed_match
76
+ r = parse_rgb_value(rgb_mixed_match[1])
77
+ g = parse_rgb_value(rgb_mixed_match[2])
78
+ b = parse_rgb_value(rgb_mixed_match[3])
79
+ return rgb_to_hex(r, g, b)
80
+ end
81
+
82
+ # Handle RGBA functions (ignore alpha for SVG purposes)
83
+ rgba_match = color.match(/^rgba\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*[\d.]+\s*\)$/)
84
+ if rgba_match
85
+ r, g, b = rgba_match[1..3].map(&:to_i)
86
+ return rgb_to_hex(r, g, b)
87
+ end
88
+
89
+ # Return original if unrecognized format
90
+ color
91
+ end
92
+
93
+ # Check if two colors are equivalent
94
+ def equivalent?(color1, color2)
95
+ return false if color1.nil? || color2.nil?
96
+ return true if color1 == color2
97
+
98
+ norm1 = normalize(color1)
99
+ norm2 = normalize(color2)
100
+
101
+ return false if norm1.nil? || norm2.nil?
102
+
103
+ norm1 == norm2
104
+ end
105
+
106
+ # Convert color to its canonical form (prefer named colors when available)
107
+ def to_canonical(color)
108
+ normalized = normalize(color)
109
+ return color if normalized.nil?
110
+
111
+ # Return special keywords as-is
112
+ return normalized if SPECIAL_KEYWORDS.include?(normalized)
113
+
114
+ # Convert hex to named color if available
115
+ named = HEX_TO_NAMED[normalized]
116
+ return named if named
117
+
118
+ # Return normalized hex
119
+ normalized
120
+ end
121
+
122
+ # Expand short hex colors: #fff → #ffffff
123
+ def expand_short_hex(hex)
124
+ return hex unless hex.match?(/^#[0-9a-f]{3}$/)
125
+
126
+ chars = hex[1..3].chars
127
+ "##{chars.map { |c| c * 2 }.join}"
128
+ end
129
+
130
+ # Convert RGB values to hex
131
+ def rgb_to_hex(red, green, blue)
132
+ # Clamp values to 0-255 range
133
+ r = [[red.to_i, 0].max, 255].min
134
+ g = [[green.to_i, 0].max, 255].min
135
+ b = [[blue.to_i, 0].max, 255].min
136
+
137
+ format("#%02x%02x%02x", r, g, b)
138
+ end
139
+
140
+ # Check if a color is valid according to SVG specifications
141
+ def valid_css_color?(color)
142
+ return false if color.nil? || color.empty?
143
+
144
+ normalized = normalize(color)
145
+ return false if normalized.nil?
146
+
147
+ # Valid if it normalizes to something we recognize
148
+ SPECIAL_KEYWORDS.include?(normalized) ||
149
+ NAMED_COLORS.key?(color.strip.downcase) ||
150
+ normalized.match?(/^#[0-9a-f]{6}$/)
151
+ end
152
+
153
+ # Get all equivalent forms of a color
154
+ def equivalent_forms(color)
155
+ normalized = normalize(color)
156
+ return [color] if normalized.nil?
157
+
158
+ forms = [normalized]
159
+
160
+ # Add named form if available
161
+ named = HEX_TO_NAMED[normalized]
162
+ forms << named if named
163
+
164
+ # Add short hex form if applicable
165
+ if normalized.match?(/^#([0-9a-f])\1([0-9a-f])\2([0-9a-f])\3$/)
166
+ short_hex = "##{$1}#{$2}#{$3}"
167
+ forms << short_hex
168
+ end
169
+
170
+ # Add uppercase variants for hex
171
+ if normalized.match?(/^#[0-9a-f]{6}$/)
172
+ forms << normalized.upcase
173
+ end
174
+
175
+ forms.uniq
176
+ end
177
+
178
+ # Check if a color is in a list of allowed colors (with equivalence)
179
+ def allowed_in_list?(color, allowed_colors)
180
+ return false if color.nil? || allowed_colors.nil? || allowed_colors.empty?
181
+
182
+ normalized = normalize(color)
183
+ return false if normalized.nil?
184
+
185
+ allowed_colors.any? { |allowed| equivalent?(color, allowed) }
186
+ end
187
+
188
+ # Calculate RGB sum for threshold-based validation
189
+ def rgb_sum(color)
190
+ return nil if color.nil? || color.empty?
191
+
192
+ color = color.strip.downcase
193
+
194
+ # Handle hex colors
195
+ if color.match?(/^#[0-9a-f]{3}$/)
196
+ # Expand short hex and calculate
197
+ expanded = expand_short_hex(color)
198
+ r = expanded[1..2].to_i(16)
199
+ g = expanded[3..4].to_i(16)
200
+ b = expanded[5..6].to_i(16)
201
+ return r + g + b
202
+ elsif color.match?(/^#[0-9a-f]{6}$/)
203
+ r = color[1..2].to_i(16)
204
+ g = color[3..4].to_i(16)
205
+ b = color[5..6].to_i(16)
206
+ return r + g + b
207
+ end
208
+
209
+ # Handle RGB functions with integers
210
+ rgb_match = color.match(/^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/)
211
+ if rgb_match
212
+ r, g, b = rgb_match[1..3].map(&:to_i)
213
+ return r + g + b
214
+ end
215
+
216
+ # Handle RGB functions with percentages
217
+ rgb_percent_match = color.match(/^rgb\s*\(\s*(\d+(?:\.\d+)?)%\s*,\s*(\d+(?:\.\d+)?)%\s*,\s*(\d+(?:\.\d+)?)%\s*\)$/)
218
+ if rgb_percent_match
219
+ r = (rgb_percent_match[1].to_f * 255 / 100).round
220
+ g = (rgb_percent_match[2].to_f * 255 / 100).round
221
+ b = (rgb_percent_match[3].to_f * 255 / 100).round
222
+ return r + g + b
223
+ end
224
+
225
+ # Handle mixed RGB functions
226
+ rgb_mixed_match = color.match(/^rgb\s*\(\s*(\d+(?:\.\d+)?%?)\s*,\s*(\d+(?:\.\d+)?%?)\s*,\s*(\d+(?:\.\d+)?%?)\s*\)$/)
227
+ if rgb_mixed_match
228
+ r = parse_rgb_value(rgb_mixed_match[1])
229
+ g = parse_rgb_value(rgb_mixed_match[2])
230
+ b = parse_rgb_value(rgb_mixed_match[3])
231
+ return r + g + b
232
+ end
233
+
234
+ # Handle named colors
235
+ if NAMED_COLORS.key?(color)
236
+ hex = NAMED_COLORS[color]
237
+ r = hex[1..2].to_i(16)
238
+ g = hex[3..4].to_i(16)
239
+ b = hex[5..6].to_i(16)
240
+ return r + g + b
241
+ end
242
+
243
+ nil
244
+ end
245
+
246
+ private
247
+
248
+ # Parse individual RGB value (percentage or absolute)
249
+ def parse_rgb_value(value_str)
250
+ value_str = value_str.strip
251
+ if value_str.end_with?("%")
252
+ # Percentage value
253
+ percent = value_str.chomp("%").to_f
254
+ (percent * 255 / 100).round
255
+ else
256
+ # Absolute value
257
+ value_str.to_i
258
+ end
259
+ end
260
+ end
261
+ end
262
+ end
@@ -0,0 +1,203 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "moxml"
4
+
5
+ module SvgConform
6
+ # Wrapper around Moxml document for SVG validation
7
+ class Document
8
+ attr_reader :file_path, :moxml_document
9
+
10
+ def initialize(content_or_path)
11
+ if File.exist?(content_or_path.to_s)
12
+ @file_path = content_or_path
13
+ @content = File.read(@file_path)
14
+ else
15
+ @file_path = nil
16
+ @content = content_or_path
17
+ end
18
+
19
+ parse_document
20
+ end
21
+
22
+ def self.from_file(file_path)
23
+ new(file_path)
24
+ end
25
+
26
+ def self.from_content(content)
27
+ new(content)
28
+ end
29
+
30
+ def root
31
+ @moxml_document.root
32
+ end
33
+
34
+ def elements
35
+ @moxml_document.elements
36
+ end
37
+
38
+ def xpath(path, namespaces = {})
39
+ @moxml_document.xpath(path, namespaces)
40
+ end
41
+
42
+ def traverse(&)
43
+ traverse_node(root, &) if root
44
+ end
45
+
46
+ def svg_elements
47
+ # Handle both default namespace and prefixed namespace
48
+ if has_svg_namespace_prefix?
49
+ xpath("//svg:*", { "svg" => "http://www.w3.org/2000/svg" }).select { |node| node.respond_to?(:name) }
50
+ else
51
+ xpath("//*[namespace-uri()='http://www.w3.org/2000/svg']").select { |node| node.respond_to?(:name) }
52
+ end
53
+ end
54
+
55
+ def to_xml
56
+ @moxml_document.to_xml
57
+ end
58
+
59
+ def dup
60
+ Document.from_content(to_xml)
61
+ end
62
+
63
+ def valid_xml?
64
+ !@moxml_document.nil?
65
+ rescue StandardError
66
+ false
67
+ end
68
+
69
+ def namespace_uri
70
+ root&.namespace&.uri
71
+ end
72
+
73
+ def svg_namespace?
74
+ namespace_uri == "http://www.w3.org/2000/svg"
75
+ end
76
+
77
+ def has_svg_namespace_prefix?
78
+ @content.include?("xmlns:svg=") || @content.include?("svg:")
79
+ end
80
+
81
+ def has_viewbox?
82
+ root&.attribute("viewBox")
83
+ end
84
+
85
+ def viewbox
86
+ root&.attribute("viewBox")&.value
87
+ end
88
+
89
+ def width
90
+ root&.attribute("width")&.value
91
+ end
92
+
93
+ def height
94
+ root&.attribute("height")&.value
95
+ end
96
+
97
+ def version
98
+ root&.attribute("version")&.value
99
+ end
100
+
101
+ # Find all elements with a specific name
102
+ def find_elements(name)
103
+ if has_svg_namespace_prefix?
104
+ xpath("//svg:#{name}", { "svg" => "http://www.w3.org/2000/svg" })
105
+ else
106
+ xpath("//#{name}[namespace-uri()='http://www.w3.org/2000/svg']")
107
+ end
108
+ end
109
+
110
+ # Find all elements with style attributes
111
+ def elements_with_style
112
+ xpath("//*[@style]")
113
+ end
114
+
115
+ # Find all elements with specific attributes
116
+ def elements_with_attribute(attr_name)
117
+ xpath("//*[@#{attr_name}]")
118
+ end
119
+
120
+ # Get all unique element names in the document
121
+ def element_names
122
+ svg_elements.map(&:name).uniq.sort
123
+ end
124
+
125
+ # Get all unique attribute names in the document
126
+ def attribute_names
127
+ attributes = []
128
+ traverse do |node|
129
+ attributes.concat(node.attributes.keys) if node.respond_to?(:attributes)
130
+ end
131
+ attributes.uniq.sort
132
+ end
133
+
134
+ # Check if document contains external references
135
+ def has_external_references?
136
+ # Check for external stylesheets
137
+ link_elements = if has_svg_namespace_prefix?
138
+ xpath('//svg:link[@rel="stylesheet"]',
139
+ { "svg" => "http://www.w3.org/2000/svg" })
140
+ else
141
+ xpath('//link[@rel="stylesheet"][namespace-uri()="http://www.w3.org/2000/svg"]')
142
+ end
143
+ return true if link_elements.any?
144
+
145
+ # Check for @import in style elements
146
+ style_elements = if has_svg_namespace_prefix?
147
+ xpath("//svg:style",
148
+ { "svg" => "http://www.w3.org/2000/svg" })
149
+ else
150
+ xpath("//style[namespace-uri()='http://www.w3.org/2000/svg']")
151
+ end
152
+
153
+ style_elements.each do |style|
154
+ content = style.text
155
+ return true if content&.include?("@import")
156
+ end
157
+
158
+ # Check for external references in style attributes
159
+ elements_with_style.each do |element|
160
+ style_value = element.attribute("style")&.value
161
+ return true if style_value&.include?("url(")
162
+ end
163
+
164
+ false
165
+ end
166
+
167
+ private
168
+
169
+ def parse_document
170
+ begin
171
+ # Create a Moxml context and parse the document
172
+ context = Moxml.new
173
+ @moxml_document = context.parse(@content)
174
+ rescue StandardError => e
175
+ raise ParseError, "Failed to parse SVG document: #{e.message}"
176
+ end
177
+
178
+ raise ParseError, "Document could not be parsed" unless @moxml_document
179
+
180
+ raise ParseError, "Document has no root element" unless root
181
+
182
+ # Check if root element is SVG (handle both namespaced and non-namespaced)
183
+ root_name = root.name
184
+ root_namespace = root.namespace&.uri
185
+
186
+ # Accept if element name is "svg" regardless of namespace
187
+ # or if it's in the SVG namespace
188
+ return if root_name == "svg" || root_namespace == "http://www.w3.org/2000/svg"
189
+
190
+ raise ParseError, "Root element must be 'svg', found '#{root_name}'"
191
+ end
192
+
193
+ def traverse_node(node, &block)
194
+ yield node if block
195
+
196
+ return unless node.respond_to?(:children)
197
+
198
+ node.children.each do |child|
199
+ traverse_node(child, &block)
200
+ end
201
+ end
202
+ end
203
+ end