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,367 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_requirement"
4
+ require_relative "element_requirement_config"
5
+
6
+ module SvgConform
7
+ module Requirements
8
+ # Validates that only allowed SVG elements and their attributes are used
9
+ class AllowedElementsRequirement < BaseRequirement
10
+ attribute :type, :string, default: -> { "AllowedElementsRequirement" }
11
+ attribute :element_configs, ElementRequirementConfig, collection: true, default: -> {
12
+ []
13
+ }
14
+ attribute :disallowed_elements, :string, collection: true, default: -> {
15
+ []
16
+ }
17
+ attribute :check_attributes, :boolean, default: false
18
+ attribute :check_invalid_attributes, :boolean, default: false
19
+ attribute :check_parent_child, :boolean, default: false
20
+ attribute :parent_child_rules, :string, default: -> { {} }
21
+ attribute :skip_foreign_namespaces, :boolean, default: false
22
+ attribute :allowed_namespaces, :string, collection: true, default: -> {
23
+ []
24
+ }
25
+ attribute :allow_rdf_metadata, :boolean, default: false
26
+
27
+ # RDF-related namespaces (same as in NamespaceRequirement for consistency)
28
+ RDF_NAMESPACES = [
29
+ "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
30
+ "http://creativecommons.org/ns#",
31
+ "http://purl.org/dc/elements/1.1/",
32
+ "http://purl.org/dc/dcmitype/",
33
+ "http://www.w3.org/2000/01/rdf-schema#",
34
+ ].freeze
35
+
36
+ yaml do
37
+ map "id", to: :id
38
+ map "description", to: :description
39
+ map "type", to: :type
40
+ map "element_configs", to: :element_configs
41
+ map "disallowed_elements", to: :disallowed_elements
42
+ map "check_attributes", to: :check_attributes
43
+ map "check_invalid_attributes", to: :check_invalid_attributes
44
+ map "check_parent_child", to: :check_parent_child
45
+ map "skip_foreign_namespaces", to: :skip_foreign_namespaces
46
+ map "allowed_namespaces", to: :allowed_namespaces
47
+ map "allow_rdf_metadata", to: :allow_rdf_metadata
48
+ end
49
+
50
+ def check(node, context)
51
+ return unless element?(node)
52
+
53
+ # Skip foreign namespace elements if configured (let NamespaceRequirement handle them)
54
+ if skip_foreign_namespaces && foreign_namespace?(node)
55
+ return
56
+ end
57
+
58
+ element_name = node.name
59
+
60
+ # Check if element is explicitly disallowed
61
+ if disallowed_element?(element_name)
62
+ context.add_error(
63
+ requirement_id: id,
64
+ message: "Element '#{element_name}' is not allowed in this profile",
65
+ node: node,
66
+ severity: :error,
67
+ data: { element: element_name },
68
+ )
69
+ return
70
+ end
71
+
72
+ # Check parent-child relationships
73
+ if check_parent_child && node.parent && element?(node.parent)
74
+ parent_name = node.parent.name
75
+ if invalid_parent_child?(parent_name, element_name)
76
+ context.add_error(
77
+ requirement_id: id,
78
+ message: "The element '#{element_name}' is not allowed as a child of '#{parent_name}'",
79
+ node: node,
80
+ severity: :error,
81
+ data: { element: element_name, parent: parent_name },
82
+ )
83
+ # Mark node AND descendants as structurally invalid
84
+ # svgcheck does not validate attributes of forbidden children - just reports one error
85
+ context.mark_node_structurally_invalid(node)
86
+ return
87
+ end
88
+ end
89
+
90
+ # Check if element is in allowed list
91
+ if element_configs&.any?
92
+ allowed_elements = element_configs.map(&:tag)
93
+ unless allowed_elements.include?(element_name)
94
+ context.add_error(
95
+ requirement_id: id,
96
+ message: "Element '#{element_name}' is not allowed in this profile",
97
+ node: node,
98
+ severity: :error,
99
+ data: { element: element_name },
100
+ )
101
+ # Mark as structurally invalid so children aren't validated
102
+ # (matches svgcheck behavior: invalid element removed with all children)
103
+ context.mark_node_structurally_invalid(node)
104
+ return
105
+ end
106
+ end
107
+
108
+ # Collect all potential attribute errors, then apply priority rules
109
+ potential_errors = collect_attribute_errors(node)
110
+ prioritized_errors = prioritize_errors(potential_errors)
111
+
112
+ # Add the prioritized errors to the context
113
+ prioritized_errors.each do |error|
114
+ context.add_error(
115
+ requirement_id: id,
116
+ message: error[:message],
117
+ node: node,
118
+ severity: :error,
119
+ )
120
+ end
121
+ end
122
+
123
+ private
124
+
125
+ def disallowed_element?(element_name)
126
+ disallowed_elements&.include?(element_name) || false
127
+ end
128
+
129
+ def invalid_parent_child?(parent_name, child_name)
130
+ return false unless element_configs&.any?
131
+
132
+ # Find the configuration for the parent element
133
+ parent_config = element_configs.find do |config|
134
+ config.tag == parent_name
135
+ end
136
+ return false unless parent_config
137
+
138
+ # If allowed_children is defined and not empty, use it
139
+ if parent_config.allowed_children&.any?
140
+ # Child must be in the allowed list
141
+ return !parent_config.allowed_children.include?(child_name)
142
+ end
143
+
144
+ # No restrictions defined for this parent
145
+ false
146
+ end
147
+
148
+ def collect_attribute_errors(node)
149
+ errors = []
150
+ node.name
151
+
152
+ # Always collect global disallowed attributes first (highest priority)
153
+ errors.concat(collect_global_disallowed_errors(node))
154
+
155
+ # Collect element-specific attribute errors if enabled
156
+ errors.concat(collect_element_attribute_errors(node)) if check_attributes
157
+
158
+ errors
159
+ end
160
+
161
+ def collect_element_attribute_errors(node)
162
+ errors = []
163
+ element_name = node.name
164
+
165
+ return errors unless element_configs&.any?
166
+
167
+ element_config = element_configs.find do |config|
168
+ config.tag == element_name
169
+ end
170
+
171
+ return errors unless element_config&.attr
172
+
173
+ allowed_attrs = []
174
+ disallowed_attrs = []
175
+
176
+ # Parse attributes, separating allowed from disallowed (prefixed with !)
177
+ element_config.attr.each do |attribute|
178
+ if attribute.start_with?("!")
179
+ disallowed_attrs << attribute[1..].downcase
180
+ else
181
+ allowed_attrs << attribute.downcase
182
+ end
183
+ end
184
+
185
+ # Add common attributes that are allowed on all elements
186
+ common_attrs = %w[id class style xmlns]
187
+ allowed_attrs = (allowed_attrs + common_attrs).uniq
188
+
189
+ # Add global properties that svgcheck allows on any element (from word_properties.py)
190
+ global_properties = %w[
191
+ about base baseprofile d break class content cx cy datatype height href
192
+ label lang pathlength points preserveaspectratio property r rel resource
193
+ rev role rotate rx ry space snapshottime transform typeof version width
194
+ viewbox x x1 x2 y y1 y2 stroke stroke-width stroke-linecap stroke-linejoin
195
+ stroke-miterlimit stroke-dasharray stroke-dashoffset stroke-opacity
196
+ vector-effect viewport-fill display viewport-fill-opacity visibility
197
+ image-rendering color-rendering shape-rendering text-rendering
198
+ buffered-rendering solid-opacity solid-color color stop-color stop-opacity
199
+ line-increment text-align display-align font-size font-family font-weight
200
+ font-style font-variant direction unicode-bidi text-anchor fill fill-rule
201
+ fill-opacity requiredfeatures requiredformats requiredextensions
202
+ requiredfonts systemlanguage
203
+ ]
204
+ allowed_attrs = (allowed_attrs + global_properties).uniq
205
+
206
+ node.attributes.each do |attr|
207
+ attr_name = attr.name.downcase
208
+ next if attr_name.start_with?("xmlns:")
209
+ next if attr_name.start_with?("xml:")
210
+
211
+ # Skip namespaced attributes - they should be handled by NamespaceAttributesRequirement
212
+ next if attr.namespace
213
+
214
+ # Check if matches data-* pattern (wildcard pattern)
215
+ next if attr_name.start_with?("data-")
216
+
217
+ # Check if explicitly disallowed
218
+ if disallowed_attrs.include?(attr_name)
219
+ errors << {
220
+ type: :explicitly_disallowed,
221
+ attribute: attr_name,
222
+ message: "Attribute '#{attr_name}' is explicitly disallowed on element '#{element_name}'",
223
+ }
224
+ next
225
+ end
226
+
227
+ # Check if not in allowed list
228
+ next if allowed_attrs.include?(attr_name)
229
+
230
+ errors << {
231
+ type: :not_allowed,
232
+ attribute: attr_name,
233
+ message: "Attribute '#{attr_name}' is not allowed on element '#{element_name}'",
234
+ }
235
+ end
236
+
237
+ errors
238
+ end
239
+
240
+ def collect_global_disallowed_errors(node)
241
+ errors = []
242
+
243
+ # Check for globally disallowed attributes (using * tag)
244
+ return errors unless element_configs&.any?
245
+
246
+ global_config = element_configs.find { |config| config.tag == "*" }
247
+ return errors unless global_config&.attr
248
+
249
+ global_disallowed = []
250
+ global_config.attr.each do |attribute|
251
+ global_disallowed << attribute[1..].downcase if attribute.start_with?("!")
252
+ end
253
+
254
+ return errors if global_disallowed.empty?
255
+
256
+ node.attributes.each do |attr|
257
+ attr_name = attr.name.downcase
258
+ next unless global_disallowed.include?(attr_name)
259
+
260
+ errors << {
261
+ type: :globally_disallowed,
262
+ attribute: attr_name,
263
+ message: "Attribute '#{attr_name}' is globally disallowed in this profile",
264
+ }
265
+ end
266
+
267
+ errors
268
+ end
269
+
270
+ def prioritize_errors(errors)
271
+ # Group errors by attribute name
272
+ errors_by_attr = errors.group_by { |error| error[:attribute] }
273
+
274
+ prioritized = []
275
+
276
+ errors_by_attr.each_value do |attr_errors|
277
+ # Priority order: globally_disallowed > explicitly_disallowed > not_allowed
278
+ prioritized << if attr_errors.any? do |e|
279
+ e[:type] == :globally_disallowed
280
+ end
281
+ attr_errors.find do |e|
282
+ e[:type] == :globally_disallowed
283
+ end
284
+ elsif attr_errors.any? do |e|
285
+ e[:type] == :explicitly_disallowed
286
+ end
287
+ attr_errors.find do |e|
288
+ e[:type] == :explicitly_disallowed
289
+ end
290
+ else
291
+ attr_errors.find { |e| e[:type] == :not_allowed }
292
+ end
293
+ end
294
+
295
+ prioritized
296
+ end
297
+
298
+ def should_check_node?(node, context = nil)
299
+ return false unless element?(node)
300
+ return false if context&.node_structurally_invalid?(node)
301
+
302
+ true
303
+ end
304
+
305
+ def foreign_namespace?(node)
306
+ return false unless skip_foreign_namespaces
307
+
308
+ # Check if element has a namespace
309
+ element_namespace = get_element_namespace(node)
310
+
311
+ # No namespace or empty namespace means SVG namespace (default)
312
+ return false if element_namespace.nil? || element_namespace.empty?
313
+
314
+ # Check if namespace is in allowed list
315
+ # If allow_rdf_metadata is enabled, also allow RDF namespaces
316
+ effective_allowed_namespaces = allowed_namespaces
317
+ if allow_rdf_metadata
318
+ effective_allowed_namespaces = allowed_namespaces + RDF_NAMESPACES
319
+ end
320
+
321
+ return false if effective_allowed_namespaces.empty?
322
+
323
+ !effective_allowed_namespaces.include?(element_namespace)
324
+ end
325
+
326
+ def get_element_namespace(node)
327
+ # Try to get namespace from the element
328
+ if node.respond_to?(:namespace) && node.namespace
329
+ namespace_str = node.namespace.to_s
330
+ # Extract the URI from the namespace string
331
+ if namespace_str =~ /xmlns[^=]*="([^"]+)"/
332
+ return ::Regexp.last_match(1)
333
+ end
334
+ end
335
+
336
+ # If no namespace found, check if element has a prefix (indicating it's namespaced)
337
+ if node.name.include?(":")
338
+ prefix = node.name.split(":").first
339
+ return find_namespace_uri_for_prefix(node, prefix)
340
+ end
341
+
342
+ nil
343
+ end
344
+
345
+ def find_namespace_uri_for_prefix(node, prefix)
346
+ # Check current node and ancestors for namespace declarations
347
+ current = node
348
+ while current
349
+ # Check for xmlns:prefix attribute
350
+ xmlns_attr = "xmlns:#{prefix}"
351
+ if current.respond_to?(:attributes) && current.attributes[xmlns_attr]
352
+ return current.attributes[xmlns_attr]
353
+ end
354
+
355
+ # Check using get_attribute method
356
+ namespace_uri = get_attribute(current, xmlns_attr)
357
+ return namespace_uri if namespace_uri
358
+
359
+ # Move to parent
360
+ current = current.respond_to?(:parent) ? current.parent : nil
361
+ end
362
+
363
+ nil
364
+ end
365
+ end
366
+ end
367
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SvgConform
4
+ module Requirements
5
+ # Base class for all validation requirements
6
+ class BaseRequirement < Lutaml::Model::Serializable
7
+ attribute :id, :string
8
+ attribute :description, :string
9
+ attribute :type, :string, polymorphic_class: true, default: -> {
10
+ self.class.name.split("::").last
11
+ }
12
+
13
+ yaml do
14
+ map "id", to: :id
15
+ map "description", to: :description
16
+ map "type", to: :type
17
+ end
18
+
19
+ # Main validation method - must be implemented by subclasses
20
+ def check(node, context)
21
+ raise NotImplementedError, "Subclasses must implement #check"
22
+ end
23
+
24
+ # Validate the entire document (called once per requirement)
25
+ def validate_document(document, context)
26
+ document.traverse do |node|
27
+ check(node, context) if should_check_node?(node, context)
28
+ end
29
+ end
30
+
31
+ # Determine if this requirement should check a specific node
32
+ def should_check_node?(node, context = nil)
33
+ return false unless node.respond_to?(:name) && node.respond_to?(:attributes)
34
+
35
+ # Skip structurally invalid nodes (and their children are automatically skipped by marking the parent)
36
+ return false if context&.node_structurally_invalid?(node)
37
+
38
+ true
39
+ end
40
+
41
+ # Helper method to check if a node is an element
42
+ def element?(node)
43
+ node.respond_to?(:name) && !node.name.nil?
44
+ end
45
+
46
+ # Helper method to check if a node is text
47
+ def text?(node)
48
+ node.respond_to?(:text?) && node.text?
49
+ end
50
+
51
+ # Helper method to get attribute value
52
+ def get_attribute(node, name)
53
+ return nil unless node.respond_to?(:attribute)
54
+
55
+ attr = node.attribute(name)
56
+ attr&.value
57
+ end
58
+
59
+ # Helper method to set attribute value
60
+ def set_attribute(node, name, value)
61
+ return false unless node.respond_to?(:set_attribute)
62
+
63
+ node.set_attribute(name, value)
64
+ true
65
+ end
66
+
67
+ # Helper method to remove attribute
68
+ def remove_attribute(node, name)
69
+ return false unless node.respond_to?(:remove_attribute)
70
+
71
+ node.remove_attribute(name)
72
+ true
73
+ end
74
+
75
+ # Helper method to check if attribute exists
76
+ def has_attribute?(node, name)
77
+ return false unless node.respond_to?(:attribute)
78
+
79
+ !node.attribute(name).nil?
80
+ end
81
+
82
+ # Helper method to get all attributes
83
+ def get_attributes(node)
84
+ return {} unless node.respond_to?(:attributes)
85
+
86
+ attrs = node.attributes || []
87
+ # Convert array of Moxml::Attribute objects to hash
88
+ attrs.each_with_object({}) do |attr, hash|
89
+ hash[attr.name] = attr.value
90
+ end
91
+ end
92
+
93
+ def to_s
94
+ "#{@id}: #{@description}"
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_requirement"
4
+ require_relative "../css_color"
5
+
6
+ module SvgConform
7
+ module Requirements
8
+ # Validates color restrictions (e.g., black and white only for IETF profile)
9
+ class ColorRestrictionsRequirement < BaseRequirement
10
+ attribute :type, :string, default: -> { "ColorRestrictionsRequirement" }
11
+ attribute :mode, :string, default: "black_white_only"
12
+ attribute :allowed_colors, :string, collection: true, default: lambda {
13
+ ["black", "white", "#000000", "#ffffff", "none", "inherit", "currentcolor"]
14
+ }
15
+ attribute :black_and_white_threshold, :integer, default: nil
16
+
17
+ yaml do
18
+ map "id", to: :id
19
+ map "description", to: :description
20
+ map "type", to: :type
21
+ map "mode", to: :mode
22
+ map "allowed_colors", to: :allowed_colors
23
+ map "black_and_white_threshold", to: :black_and_white_threshold
24
+ end
25
+
26
+ def check(node, context)
27
+ return unless element?(node)
28
+
29
+ # Skip attribute validation for structurally invalid nodes (e.g., wrong parent-child)
30
+ return if context.node_structurally_invalid?(node)
31
+
32
+ # Check color-related attributes
33
+ color_attributes = %w[fill stroke color stop-color flood-color
34
+ lighting-color]
35
+
36
+ color_attributes.each do |attr_name|
37
+ value = get_attribute(node, attr_name)
38
+ next if value.nil? || value.empty?
39
+
40
+ next if valid_color?(value)
41
+
42
+ context.add_error(
43
+ requirement_id: id,
44
+ message: "Color '#{value}' in attribute '#{attr_name}' is not allowed in this profile",
45
+ node: node,
46
+ severity: :error,
47
+ data: {
48
+ attribute: attr_name,
49
+ value: value,
50
+ element: node.name,
51
+ },
52
+ )
53
+ end
54
+
55
+ # Check style attribute for color properties
56
+ style_value = get_attribute(node, "style")
57
+ return unless style_value
58
+
59
+ styles = parse_style(style_value)
60
+ color_properties = %w[fill stroke color stop-color flood-color
61
+ lighting-color]
62
+
63
+ color_properties.each do |prop|
64
+ value = styles[prop]
65
+ next if value.nil? || value.empty?
66
+
67
+ next if valid_color?(value)
68
+
69
+ context.add_error(
70
+ requirement_id: id,
71
+ message: "Color '#{value}' in style property '#{prop}' is not allowed in this profile",
72
+ node: node,
73
+ severity: :error,
74
+ data: {
75
+ attribute: prop,
76
+ value: value,
77
+ element: node.name,
78
+ },
79
+ )
80
+ end
81
+ end
82
+
83
+ private
84
+
85
+ def valid_color?(color)
86
+ # First check if threshold-based validation is enabled
87
+ if black_and_white_threshold
88
+ return valid_color_for_threshold?(color)
89
+ end
90
+
91
+ # Fall back to standard allowed colors list validation
92
+ CssColor.allowed_in_list?(color, allowed_colors)
93
+ end
94
+
95
+ def valid_color_for_threshold?(color)
96
+ # Handle special keywords that are always valid
97
+ return true if %w[none inherit
98
+ currentcolor].include?(color.strip.downcase)
99
+
100
+ # In threshold mode, be strict about exact string matching
101
+ # Only allow the exact formats that svgcheck accepts
102
+ allowed_colors.include?(color.strip)
103
+ end
104
+
105
+ def parse_style(style_string)
106
+ return {} if style_string.nil? || style_string.empty?
107
+
108
+ properties = {}
109
+ declarations = style_string.split(";").map(&:strip)
110
+
111
+ declarations.each do |declaration|
112
+ next if declaration.empty?
113
+
114
+ parts = declaration.split(":", 2)
115
+ next unless parts.length == 2
116
+
117
+ property = parts[0].strip
118
+ value = parts[1].strip
119
+ properties[property] = value
120
+ end
121
+
122
+ properties
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+
5
+ module SvgConform
6
+ module Requirements
7
+ # Represents an allowed SVG element configuration with its allowed attributes
8
+ class ElementRequirementConfig < Lutaml::Model::Serializable
9
+ attribute :tag, :string
10
+ attribute :attr, :string, collection: true, default: -> { [] }
11
+ attribute :allowed_children, :string, collection: true, default: -> { [] }
12
+
13
+ yaml do
14
+ map "tag", to: :tag
15
+ map "attributes", to: :attr
16
+ map "allowed_children", to: :allowed_children
17
+ end
18
+
19
+ # Check if an attribute is allowed for this element
20
+ def attribute_allowed?(attr_name)
21
+ return false unless attr
22
+
23
+ attr_name = attr_name.downcase
24
+
25
+ # Common attributes that are allowed on all elements
26
+ common_attrs = %w[id class style xmlns]
27
+ return true if common_attrs.include?(attr_name)
28
+
29
+ # Skip xmlns: and xml: prefixed attributes
30
+ return true if attr_name.start_with?("xmlns:", "xml:")
31
+
32
+ # Check if explicitly disallowed (prefixed with !)
33
+ return false if attr.any? do |attribute|
34
+ attribute.start_with?("!") && attribute[1..].downcase == attr_name
35
+ end
36
+
37
+ # Check if in allowed list
38
+ attr.any? do |attribute|
39
+ !attribute.start_with?("!") && attribute.downcase == attr_name
40
+ end
41
+ end
42
+
43
+ # Get list of explicitly disallowed attributes (those prefixed with !)
44
+ def disallowed_attributes
45
+ return [] unless attr
46
+
47
+ attr.filter_map do |attribute|
48
+ attribute[1..].downcase if attribute.start_with?("!")
49
+ end
50
+ end
51
+
52
+ # Get list of allowed attributes (those not prefixed with !)
53
+ def allowed_attributes
54
+ return [] unless attr
55
+
56
+ allowed = attr.filter_map do |attribute|
57
+ attribute.downcase unless attribute.start_with?("!")
58
+ end
59
+
60
+ # Add common attributes
61
+ common_attrs = %w[id class style xmlns]
62
+ (allowed + common_attrs).uniq
63
+ end
64
+
65
+ # Check if this is a global config (applies to all elements)
66
+ def global_config?
67
+ tag == "*"
68
+ end
69
+
70
+ def to_s
71
+ "ElementRequirementConfig(#{tag}: #{attr&.join(', ') || 'none'})"
72
+ end
73
+ end
74
+ end
75
+ end