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,211 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_requirement"
4
+
5
+ module SvgConform
6
+ module Requirements
7
+ # Validates that elements don't have attributes from disallowed namespaces
8
+ # or only have attributes from allowed namespaces (whitelist mode)
9
+ class NamespaceAttributesRequirement < BaseRequirement
10
+ attribute :type, :string, default: -> { "NamespaceAttributesRequirement" }
11
+ attribute :disallowed_namespaces, :string, collection: true, default: -> {
12
+ []
13
+ }
14
+ attribute :allowed_namespaces, :string, collection: true, default: -> {
15
+ []
16
+ }
17
+ attribute :exempt_elements, :string, collection: true, default: -> { [] }
18
+
19
+ yaml do
20
+ map "id", to: :id
21
+ map "description", to: :description
22
+ map "type", to: :type
23
+ map "disallowed_namespaces", to: :disallowed_namespaces
24
+ map "allowed_namespaces", to: :allowed_namespaces
25
+ map "exempt_elements", to: :exempt_elements
26
+ end
27
+
28
+ def check(node, context)
29
+ return unless element?(node)
30
+
31
+ # Skip validation for exempt elements (e.g., RDF metadata elements)
32
+ return if exempt_elements.include?(node.name)
33
+
34
+ # Try to get attributes using different methods depending on what's available
35
+ if node.respond_to?(:attribute_nodes)
36
+ # Use attribute_nodes if available (Nokogiri style)
37
+ check_attribute_nodes(node, context)
38
+ elsif node.respond_to?(:attributes)
39
+ # Fallback to attributes method (Moxml style)
40
+ check_attributes_hash(node, context)
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def check_attribute_nodes(node, context)
47
+ node.attribute_nodes.each do |attr|
48
+ # Check if attribute has a namespace
49
+ namespace_uri = attr.namespace&.href
50
+ next unless namespace_uri
51
+
52
+ # Determine if this namespace is invalid based on configuration
53
+ invalid_namespace = if allowed_namespaces.empty?
54
+ # Blacklist mode: disallowed namespaces are forbidden
55
+ disallowed_namespaces.include?(namespace_uri)
56
+ else
57
+ # Whitelist mode: only allowed namespaces are permitted
58
+ !allowed_namespaces.include?(namespace_uri)
59
+ end
60
+
61
+ next unless invalid_namespace
62
+
63
+ # Get the full attribute name with prefix if available
64
+ attr_name = if attr.respond_to?(:namespace) && attr.namespace&.prefix
65
+ "#{attr.namespace.prefix}:#{attr.name}"
66
+ else
67
+ attr.name
68
+ end
69
+
70
+ context.add_error(
71
+ requirement_id: id,
72
+ message: "Element '#{node.name}' does not allow attributes with namespace '#{namespace_uri}'",
73
+ node: node,
74
+ severity: :error,
75
+ data: { attribute: attr_name, namespace: namespace_uri },
76
+ )
77
+ end
78
+ end
79
+
80
+ def check_attributes_hash(node, context)
81
+ return unless node.respond_to?(:attributes)
82
+
83
+ attributes = node.attributes
84
+
85
+ # Handle both Hash and Array cases
86
+ if attributes.respond_to?(:each_key)
87
+ # Hash case
88
+ attributes.each_key do |name|
89
+ check_attribute_name(name, node, context)
90
+ end
91
+ elsif attributes.respond_to?(:each)
92
+ # Array case - iterate over attribute objects
93
+ attributes.each do |attr|
94
+ check_moxml_attribute(attr, node, context)
95
+ end
96
+ end
97
+ end
98
+
99
+ def check_moxml_attribute(attr, node, context)
100
+ # For Moxml attributes, check if they have namespace information
101
+ if attr.respond_to?(:namespace) && attr.namespace
102
+ namespace_uri = if attr.namespace.respond_to?(:href)
103
+ attr.namespace.href
104
+ elsif attr.namespace.respond_to?(:uri)
105
+ attr.namespace.uri
106
+ else
107
+ attr.namespace.to_s
108
+ end
109
+
110
+ # Skip if no namespace URI
111
+ return unless namespace_uri && !namespace_uri.empty?
112
+
113
+ # Determine if this namespace is invalid based on configuration
114
+ invalid_namespace = if allowed_namespaces.empty?
115
+ # Blacklist mode: disallowed namespaces are forbidden
116
+ disallowed_namespaces.include?(namespace_uri)
117
+ else
118
+ # Whitelist mode: only allowed namespaces are permitted
119
+ !allowed_namespaces.include?(namespace_uri)
120
+ end
121
+
122
+ return unless invalid_namespace
123
+
124
+ # Get the full attribute name with prefix if available
125
+ attr_name = if attr.namespace.respond_to?(:prefix) && attr.namespace.prefix
126
+ "#{attr.namespace.prefix}:#{attr.name}"
127
+ else
128
+ attr.name
129
+ end
130
+
131
+ context.add_error(
132
+ requirement_id: id,
133
+ message: "Element '#{node.name}' does not allow attributes with namespace '#{namespace_uri}'",
134
+ node: node,
135
+ severity: :error,
136
+ data: { attribute: attr_name, namespace: namespace_uri },
137
+ )
138
+ else
139
+ # Fallback to name-based checking for attributes without namespace objects
140
+ name = attr.respond_to?(:name) ? attr.name : attr.to_s
141
+ check_attribute_name(name, node, context)
142
+ end
143
+ end
144
+
145
+ def check_attribute_name(name, node, context)
146
+ # Convert name to string if it's not already
147
+ name_str = name.to_s
148
+
149
+ # Check if this is a namespaced attribute by looking for colon in name
150
+ return unless name_str.include?(":")
151
+
152
+ prefix, = name_str.split(":", 2)
153
+
154
+ # Find the namespace URI for this prefix
155
+ namespace_uri = find_namespace_uri(node, prefix)
156
+
157
+ return unless namespace_uri
158
+
159
+ # Determine if this namespace is invalid based on configuration
160
+ invalid_namespace = if allowed_namespaces.empty?
161
+ # Blacklist mode: disallowed namespaces are forbidden
162
+ disallowed_namespaces.include?(namespace_uri)
163
+ else
164
+ # Whitelist mode: only allowed namespaces are permitted
165
+ !allowed_namespaces.include?(namespace_uri)
166
+ end
167
+
168
+ return unless invalid_namespace
169
+
170
+ context.add_error(
171
+ requirement_id: id,
172
+ message: "Element '#{node.name}' does not allow attributes with namespace '#{namespace_uri}'",
173
+ node: node,
174
+ severity: :error,
175
+ data: { attribute: name, namespace: namespace_uri },
176
+ )
177
+ end
178
+
179
+ def find_namespace_uri(node, prefix)
180
+ # Check current node's namespace definitions
181
+ current = node
182
+ while current.respond_to?(:parent)
183
+ if current.respond_to?(:namespace_definitions)
184
+ ns_def = current.namespace_definitions.find do |ns|
185
+ ns.prefix == prefix
186
+ end
187
+ if ns_def
188
+ return ns_def.uri if ns_def.respond_to?(:uri)
189
+ return ns_def.href if ns_def.respond_to?(:href)
190
+
191
+ return ns_def.to_s
192
+ end
193
+ end
194
+
195
+ # Check if it's defined as an attribute (xmlns:prefix)
196
+ xmlns_value = get_attribute(current, "xmlns:#{prefix}")
197
+ return xmlns_value if xmlns_value
198
+
199
+ # Move to parent, but break if parent is nil or doesn't respond to parent
200
+ begin
201
+ current = current.parent
202
+ rescue StandardError
203
+ break
204
+ end
205
+ end
206
+
207
+ nil
208
+ end
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,294 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_requirement"
4
+
5
+ module SvgConform
6
+ module Requirements
7
+ # Validates that SVG documents have proper namespace declarations
8
+ class NamespaceRequirement < BaseRequirement
9
+ attribute :type, :string, default: -> { "NamespaceRequirement" }
10
+ attribute :allowed_namespaces, :string, collection: true, default: -> {
11
+ ["http://www.w3.org/2000/svg"]
12
+ }
13
+ attribute :disallowed_namespaces, :string, collection: true, default: -> {
14
+ []
15
+ }
16
+ attribute :required_namespace, :string, default: "http://www.w3.org/2000/svg"
17
+ attribute :allow_rdf_metadata, :boolean, default: false
18
+
19
+ # RDF-related namespaces commonly found in SVG metadata
20
+ RDF_NAMESPACES = [
21
+ "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
22
+ "http://creativecommons.org/ns#",
23
+ "http://purl.org/dc/elements/1.1/",
24
+ "http://purl.org/dc/dcmitype/",
25
+ "http://www.w3.org/2000/01/rdf-schema#",
26
+ ].freeze
27
+
28
+ yaml do
29
+ map "id", to: :id
30
+ map "description", to: :description
31
+ map "type", to: :type
32
+ map "allowed_namespaces", to: :allowed_namespaces
33
+ map "disallowed_namespaces", to: :disallowed_namespaces
34
+ map "required_namespace", to: :required_namespace
35
+ map "allow_rdf_metadata", to: :allow_rdf_metadata
36
+ end
37
+
38
+ def validate_document(document, context)
39
+ root = document.root
40
+ return unless root
41
+
42
+ # Check if root element is svg
43
+ unless root.name == "svg"
44
+ context.add_error(
45
+ requirement: self,
46
+ node: root,
47
+ message: "Root element must be 'svg'",
48
+ data: { element_name: root.name },
49
+ )
50
+ return
51
+ end
52
+
53
+ # Check for SVG namespace - Moxml handles namespace differently
54
+ svg_namespace = nil
55
+
56
+ # Try to get namespace from Moxml's namespace method
57
+ if root.respond_to?(:namespace) && root.namespace
58
+ namespace_str = root.namespace.to_s
59
+ # Extract the URI from the namespace string like 'xmlns="http://www.w3.org/2000/svg"'
60
+ svg_namespace = ::Regexp.last_match(1) if namespace_str =~ /xmlns="([^"]+)"/
61
+ end
62
+
63
+ # Fallback to checking xmlns attribute
64
+ svg_namespace ||= get_attribute(root, "xmlns")
65
+
66
+ # Default namespace (empty string) should be treated as SVG namespace
67
+ svg_namespace = "" if svg_namespace.nil?
68
+
69
+ # Check against allowed namespaces if configured
70
+ if allowed_namespaces && !allowed_namespaces.empty? && !allowed_namespaces.include?(svg_namespace)
71
+ context.add_error(
72
+ requirement: self,
73
+ node: root,
74
+ message: "Namespace '#{svg_namespace}' is not allowed. Only #{allowed_namespaces.join(', ')} are permitted",
75
+ data: {
76
+ current_namespace: svg_namespace,
77
+ allowed_namespaces: allowed_namespaces,
78
+ },
79
+ )
80
+ return
81
+ end
82
+
83
+ # Check against disallowed namespaces if configured (legacy support)
84
+ if disallowed_namespaces && !disallowed_namespaces.empty? && disallowed_namespaces.include?(svg_namespace)
85
+ context.add_error(
86
+ requirement: self,
87
+ node: root,
88
+ message: "Namespace '#{svg_namespace}' is not allowed",
89
+ data: {
90
+ current_namespace: svg_namespace,
91
+ disallowed_namespaces: disallowed_namespaces,
92
+ },
93
+ )
94
+ return
95
+ end
96
+
97
+ # Default behavior: require SVG namespace
98
+ unless allowed_namespaces.empty? && disallowed_namespaces.empty?
99
+ # Now check all elements in the document for namespace violations
100
+ check_all_elements(document, context)
101
+ return
102
+ end
103
+
104
+ return if svg_namespace == "http://www.w3.org/2000/svg"
105
+
106
+ context.add_error(
107
+ requirement: self,
108
+ node: root,
109
+ message: "SVG namespace declaration missing or incorrect",
110
+ data: {
111
+ current_namespace: svg_namespace,
112
+ expected_namespace: "http://www.w3.org/2000/svg",
113
+ },
114
+ )
115
+
116
+ # Also check all elements for namespace violations
117
+ check_all_elements(document, context)
118
+ end
119
+
120
+ def check(node, context)
121
+ return unless element?(node)
122
+
123
+ # Debug: Check all elements
124
+ puts "DEBUG: Checking element: #{node.name}" if node.name.include?(":")
125
+
126
+ # Check if this element has a namespace
127
+ element_namespace = nil
128
+
129
+ # Try to get namespace from the element
130
+ if node.respond_to?(:namespace) && node.namespace
131
+ namespace_str = node.namespace.to_s
132
+ puts "DEBUG: namespace_str = #{namespace_str}" if node.name.include?(":")
133
+ # Extract the URI from the namespace string
134
+ element_namespace = ::Regexp.last_match(1) if namespace_str =~ /xmlns[^=]*="([^"]+)"/
135
+ end
136
+
137
+ # If no namespace found, check if element has a prefix (indicating it's namespaced)
138
+ if element_namespace.nil? && node.name.include?(":")
139
+ prefix = node.name.split(":").first
140
+ puts "DEBUG: Found prefixed element #{node.name}, prefix = #{prefix}"
141
+ element_namespace = find_namespace_uri_for_prefix(node, prefix)
142
+ puts "DEBUG: Found namespace URI = #{element_namespace}"
143
+ end
144
+
145
+ # Skip if no namespace (default SVG namespace)
146
+ if element_namespace.nil? || element_namespace.empty?
147
+ puts "DEBUG: Skipping #{node.name} - no namespace found" if node.name.include?(":")
148
+ return
149
+ end
150
+
151
+ puts "DEBUG: Element #{node.name} has namespace #{element_namespace}"
152
+
153
+ # Check against allowed namespaces if configured
154
+ # If allow_rdf_metadata is enabled, also allow RDF namespaces
155
+ effective_allowed_namespaces = allowed_namespaces
156
+ if allow_rdf_metadata
157
+ effective_allowed_namespaces = allowed_namespaces + RDF_NAMESPACES
158
+ end
159
+
160
+ if effective_allowed_namespaces && !effective_allowed_namespaces.empty? && !effective_allowed_namespaces.include?(element_namespace)
161
+ puts "DEBUG: Adding error for disallowed namespace #{element_namespace}"
162
+ context.add_error(
163
+ requirement_id: id,
164
+ message: "The namespace #{element_namespace} is not permitted for svg elements.",
165
+ node: node,
166
+ severity: :error,
167
+ data: {
168
+ element_name: node.name,
169
+ namespace: element_namespace,
170
+ allowed_namespaces: effective_allowed_namespaces,
171
+ },
172
+ )
173
+ return
174
+ end
175
+
176
+ # Check against disallowed namespaces if configured
177
+ return unless disallowed_namespaces && !disallowed_namespaces.empty? && disallowed_namespaces.include?(element_namespace)
178
+
179
+ puts "DEBUG: Adding error for explicitly disallowed namespace #{element_namespace}"
180
+ context.add_error(
181
+ requirement_id: id,
182
+ message: "The namespace #{element_namespace} is not permitted for svg elements.",
183
+ node: node,
184
+ severity: :error,
185
+ data: {
186
+ element_name: node.name,
187
+ namespace: element_namespace,
188
+ disallowed_namespaces: disallowed_namespaces,
189
+ },
190
+ )
191
+ end
192
+
193
+ private
194
+
195
+ def check_all_elements(document, context)
196
+ # Recursively check all elements in the document
197
+ traverse_elements(document.root, context)
198
+ end
199
+
200
+ def traverse_elements(node, context)
201
+ return unless node
202
+
203
+ # Check this element
204
+ check_element_namespace(node, context) if element?(node)
205
+
206
+ # Recursively check children
207
+ if node.respond_to?(:children)
208
+ node.children.each { |child| traverse_elements(child, context) }
209
+ elsif node.respond_to?(:elements)
210
+ node.elements.each { |child| traverse_elements(child, context) }
211
+ end
212
+ end
213
+
214
+ def check_element_namespace(node, context)
215
+ return unless element?(node)
216
+
217
+ # Check if this element has a namespace
218
+ element_namespace = nil
219
+
220
+ # Try to get namespace from the element
221
+ if node.respond_to?(:namespace) && node.namespace
222
+ namespace_str = node.namespace.to_s
223
+ # Extract the URI from the namespace string
224
+ element_namespace = ::Regexp.last_match(1) if namespace_str =~ /xmlns[^=]*="([^"]+)"/
225
+ end
226
+
227
+ # If no namespace found, check if element has a prefix (indicating it's namespaced)
228
+ if element_namespace.nil? && node.name.include?(":")
229
+ prefix = node.name.split(":").first
230
+ element_namespace = find_namespace_uri_for_prefix(node, prefix)
231
+ end
232
+
233
+ # Skip if no namespace (default SVG namespace)
234
+ return if element_namespace.nil? || element_namespace.empty?
235
+
236
+ # Check against allowed namespaces if configured
237
+ # If allow_rdf_metadata is enabled, also allow RDF namespaces
238
+ effective_allowed_namespaces = allowed_namespaces
239
+ if allow_rdf_metadata
240
+ effective_allowed_namespaces = allowed_namespaces + RDF_NAMESPACES
241
+ end
242
+
243
+ if effective_allowed_namespaces && !effective_allowed_namespaces.empty? && !effective_allowed_namespaces.include?(element_namespace)
244
+ context.add_error(
245
+ requirement_id: id,
246
+ message: "The namespace #{element_namespace} is not permitted for svg elements.",
247
+ node: node,
248
+ severity: :error,
249
+ data: {
250
+ element_name: node.name,
251
+ namespace: element_namespace,
252
+ allowed_namespaces: effective_allowed_namespaces,
253
+ },
254
+ )
255
+ return
256
+ end
257
+
258
+ # Check against disallowed namespaces if configured
259
+ return unless disallowed_namespaces && !disallowed_namespaces.empty? && disallowed_namespaces.include?(element_namespace)
260
+
261
+ context.add_error(
262
+ requirement_id: id,
263
+ message: "The namespace #{element_namespace} is not permitted for svg elements.",
264
+ node: node,
265
+ severity: :error,
266
+ data: {
267
+ element_name: node.name,
268
+ namespace: element_namespace,
269
+ disallowed_namespaces: disallowed_namespaces,
270
+ },
271
+ )
272
+ end
273
+
274
+ def find_namespace_uri_for_prefix(node, prefix)
275
+ # Check current node and ancestors for namespace declarations
276
+ current = node
277
+ while current
278
+ # Check for xmlns:prefix attribute
279
+ xmlns_attr = "xmlns:#{prefix}"
280
+ return current.attributes[xmlns_attr] if current.respond_to?(:attributes) && current.attributes[xmlns_attr]
281
+
282
+ # Check using get_attribute method
283
+ namespace_uri = get_attribute(current, xmlns_attr)
284
+ return namespace_uri if namespace_uri
285
+
286
+ # Move to parent
287
+ current = current.respond_to?(:parent) ? current.parent : nil
288
+ end
289
+
290
+ nil
291
+ end
292
+ end
293
+ end
294
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_requirement"
4
+
5
+ module SvgConform
6
+ module Requirements
7
+ # Validates that no external CSS references are present
8
+ class NoExternalCssRequirement < BaseRequirement
9
+ attribute :type, :string, default: -> { "NoExternalCssRequirement" }
10
+ attribute :check_style_elements, :boolean, default: true
11
+ attribute :check_style_attributes, :boolean, default: true
12
+ attribute :check_link_elements, :boolean, default: true
13
+ attribute :allowed_protocols, :string, collection: true
14
+
15
+ yaml do
16
+ map "id", to: :id
17
+ map "description", to: :description
18
+ map "type", to: :type
19
+ map "check_style_elements", to: :check_style_elements
20
+ map "check_style_attributes", to: :check_style_attributes
21
+ map "check_link_elements", to: :check_link_elements
22
+ map "allowed_protocols", to: :allowed_protocols
23
+ end
24
+
25
+ def check(node, context)
26
+ return unless element?(node)
27
+
28
+ case node.name
29
+ when "style"
30
+ check_style_element(node, context) if check_style_elements
31
+ when "link"
32
+ check_link_element(node, context) if check_link_elements
33
+ else
34
+ check_style_attribute(node, context) if check_style_attributes
35
+ end
36
+ end
37
+
38
+ def should_check_node?(node, context = nil)
39
+ return false unless element?(node)
40
+ return false if context&.node_structurally_invalid?(node)
41
+
42
+ node.name == "style" ||
43
+ node.name == "link" ||
44
+ has_style_attribute?(node)
45
+ end
46
+
47
+ private
48
+
49
+ def check_style_element(node, context)
50
+ # Check for @import rules in style elements
51
+ content = node.text || ""
52
+
53
+ if content =~ /@import\s+url\s*\(\s*['"]?([^'")\s]+)['"]?\s*\)/i
54
+ url = ::Regexp.last_match(1)
55
+ unless allowed_url?(url)
56
+ context.add_error(
57
+ requirement_id: id,
58
+ message: "External CSS import not allowed: #{url}",
59
+ node: node,
60
+ severity: :error,
61
+ )
62
+ end
63
+ end
64
+
65
+ return unless content =~ /@import\s+['"]([^'"]+)['"]/i
66
+
67
+ url = ::Regexp.last_match(1)
68
+ return if allowed_url?(url)
69
+
70
+ context.add_error(
71
+ requirement_id: id,
72
+ message: "External CSS import not allowed: #{url}",
73
+ node: node,
74
+ severity: :error,
75
+ )
76
+ end
77
+
78
+ def check_link_element(node, context)
79
+ rel = get_attribute(node, "rel")
80
+ href = get_attribute(node, "href")
81
+
82
+ return unless rel&.downcase == "stylesheet" && href
83
+
84
+ return if allowed_url?(href)
85
+
86
+ context.add_error(
87
+ requirement_id: id,
88
+ message: "External CSS link not allowed: #{href}",
89
+ node: node,
90
+ severity: :error,
91
+ )
92
+ end
93
+
94
+ def check_style_attribute(node, context)
95
+ style_value = get_attribute(node, "style")
96
+ return unless style_value
97
+
98
+ # Check for url() references in style attributes
99
+ return unless style_value =~ /url\s*\(\s*['"]?([^'")\s]+)['"]?\s*\)/i
100
+
101
+ url = ::Regexp.last_match(1)
102
+ return if allowed_url?(url)
103
+
104
+ context.add_error(
105
+ requirement_id: id,
106
+ message: "External URL reference in style attribute not allowed: #{url}",
107
+ node: node,
108
+ severity: :error,
109
+ )
110
+ end
111
+
112
+ def has_style_attribute?(node)
113
+ !get_attribute(node, "style").nil?
114
+ end
115
+
116
+ def allowed_url?(url)
117
+ return true if url.nil? || url.empty?
118
+
119
+ # Data URLs are typically allowed
120
+ return true if url.start_with?("data:")
121
+
122
+ # Fragment identifiers (internal references) are allowed
123
+ return true if url.start_with?("#")
124
+
125
+ # Check against allowed protocols
126
+ return false if allowed_protocols.nil? || allowed_protocols.empty?
127
+
128
+ allowed_protocols.any? { |protocol| url.start_with?("#{protocol}:") }
129
+ end
130
+ end
131
+ end
132
+ end