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,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../compatibility/compatibility_analyzer"
4
+
5
+ module SvgConform
6
+ module Commands
7
+ # Command for comparing SvgConform with svgcheck compatibility
8
+ class SvgcheckCompatibility
9
+ def initialize(options = {})
10
+ @options = options
11
+ end
12
+
13
+ def execute
14
+ validate_options
15
+
16
+ analyzer = SvgConform::Compatibility::CompatibilityAnalyzer.new(
17
+ mode: @options[:mode] || @options["mode"] || "check",
18
+ profile: @options[:profile] || @options["profile"] || "svg_1_2_rfc_compatible",
19
+ semantic: @options[:semantic] || @options["semantic"] || false,
20
+ svgcheck_dir: @options[:svgcheck_dir] || @options["svgcheck_dir"] || "spec/fixtures/svgcheck",
21
+ file: @options[:file] || @options["file"],
22
+ output: @options[:output] || @options["output"],
23
+ )
24
+
25
+ analyzer.run_analysis
26
+ end
27
+
28
+ private
29
+
30
+ def validate_options
31
+ mode = @options[:mode] || @options["mode"] || "check"
32
+ raise "Invalid mode: #{mode}. Must be 'check' or 'repair'" unless %w[
33
+ check repair
34
+ ].include?(mode)
35
+
36
+ file = @options[:file] || @options["file"]
37
+ return unless file && !file.match?(/\.(svg|xml)$/i)
38
+
39
+ raise "File must have .svg or .xml extension: #{file}"
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,262 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "English"
4
+ require "fileutils"
5
+ require "paint"
6
+ require "open3"
7
+
8
+ module SvgConform
9
+ module Commands
10
+ # Generate svgcheck outputs using Open3 for proper process management
11
+ class SvgcheckGenerate
12
+ def initialize(svgcheck_repo_path, options)
13
+ @svgcheck_repo_path = svgcheck_repo_path
14
+ @options = options
15
+ @svgcheck_exec = @options[:svgcheck_exec] || "svgcheck"
16
+ @fixtures_path = @options[:fixtures_path] || "spec/fixtures/svgcheck"
17
+ @test_dir = File.join(@svgcheck_repo_path, "svgcheck", "Tests")
18
+ @svgcheck_dir = File.join(@svgcheck_repo_path, "svgcheck")
19
+ @single_file = @options[:single_file]
20
+ @force = @options[:force] || false
21
+ @verbose = @options[:verbose] || false
22
+ @mode = @options[:mode] || "both" # 'check', 'repair', or 'both'
23
+
24
+ # File renaming map: svgcheck filename => our fixture filename
25
+ @file_rename_map = {
26
+ "full-tiny.xml" => "full-tiny.svg",
27
+ "rfc-svg.xml" => "rfc-svg.svg",
28
+ }
29
+ end
30
+
31
+ def execute
32
+ puts Paint["🔧 Generating svgcheck outputs...", :blue, :bold]
33
+ puts Paint["📋 Mode: #{@mode}", :cyan]
34
+
35
+ # Ensure output directories exist
36
+ ensure_output_directories
37
+
38
+ if @single_file
39
+ generate_single_output(@single_file)
40
+ else
41
+ generate_all_outputs
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def ensure_output_directories
48
+ case @mode
49
+ when "check"
50
+ FileUtils.mkdir_p(File.join(@fixtures_path, "check"))
51
+ when "repair"
52
+ FileUtils.mkdir_p(File.join(@fixtures_path, "repair"))
53
+ when "both"
54
+ FileUtils.mkdir_p(File.join(@fixtures_path, "check"))
55
+ FileUtils.mkdir_p(File.join(@fixtures_path, "repair"))
56
+ end
57
+ end
58
+
59
+ def generate_single_output(filename)
60
+ test_file_path = File.join(@test_dir, filename)
61
+
62
+ unless File.exist?(test_file_path)
63
+ puts Paint["❌ Test file not found: #{test_file_path}", :red]
64
+ return false
65
+ end
66
+
67
+ success = true
68
+
69
+ case @mode
70
+ when "check"
71
+ success = generate_mode_output(filename, "check")
72
+ when "repair"
73
+ success = generate_mode_output(filename, "repair")
74
+ when "both"
75
+ check_success = generate_mode_output(filename, "check")
76
+ repair_success = generate_mode_output(filename, "repair")
77
+ success = check_success && repair_success
78
+ end
79
+
80
+ if success
81
+ puts Paint["✅ Generated outputs for #{filename}", :green]
82
+ else
83
+ puts Paint["❌ Failed to generate outputs for #{filename}", :red]
84
+ end
85
+
86
+ success
87
+ end
88
+
89
+ def generate_mode_output(filename, mode)
90
+ test_file_path = File.join(@test_dir, filename)
91
+
92
+ # Apply file renaming if applicable
93
+ output_filename = @file_rename_map[filename] || filename
94
+ base_name = File.basename(output_filename, ".*")
95
+ extension = File.extname(output_filename)
96
+
97
+ # Create mode-specific output directory and base path
98
+ mode_dir = File.join(@fixtures_path, mode)
99
+ output_base = File.join(mode_dir, "#{base_name}#{extension}")
100
+
101
+ # Check if outputs already exist
102
+ expected_files = if mode == "repair"
103
+ ["#{output_base}.out", "#{output_base}.err",
104
+ "#{output_base}.code", "#{output_base}.file"]
105
+ else
106
+ ["#{output_base}.out", "#{output_base}.err",
107
+ "#{output_base}.code"]
108
+ end
109
+
110
+ if expected_files.any? { |f| File.exist?(f) } && !@force
111
+ puts Paint["⚠️ #{mode.capitalize} outputs already exist for #{filename} (use --force to overwrite)",
112
+ :yellow]
113
+ return true
114
+ end
115
+
116
+ puts Paint["📄 Generating #{mode} outputs for #{filename}...", :cyan]
117
+
118
+ run_svgcheck_mode(test_file_path, output_base, mode)
119
+ end
120
+
121
+ def generate_all_outputs
122
+ test_files = find_test_files
123
+
124
+ if test_files.empty?
125
+ puts Paint["❌ No test files found in #{@test_dir}", :red]
126
+ return
127
+ end
128
+
129
+ puts Paint["📊 Found #{test_files.length} test files", :cyan]
130
+
131
+ success_count = 0
132
+ skip_count = 0
133
+ error_count = 0
134
+
135
+ test_files.each do |filename|
136
+ # Skip problematic files
137
+ if should_skip_file?(filename)
138
+ puts Paint["⏭️ Skipping #{filename} (too large or problematic)",
139
+ :yellow]
140
+ skip_count += 1
141
+ next
142
+ end
143
+
144
+ if generate_single_output(filename)
145
+ success_count += 1
146
+ else
147
+ error_count += 1
148
+ end
149
+ end
150
+
151
+ puts "\n#{Paint['📈 GENERATION SUMMARY:', :blue, :bold]}"
152
+ puts " ✅ Successfully generated: #{Paint[success_count.to_s, :green,
153
+ :bold]}"
154
+ puts " ⏭️ Skipped: #{Paint[skip_count.to_s, :yellow, :bold]}"
155
+ puts " ❌ Failed: #{Paint[error_count.to_s, :red, :bold]}"
156
+ puts " 📁 Output directory: #{Paint[@fixtures_path, :cyan]}"
157
+
158
+ case @mode
159
+ when "check"
160
+ puts " 📂 Check outputs: #{Paint[File.join(@fixtures_path, 'check'),
161
+ :cyan]}"
162
+ when "repair"
163
+ puts " 📂 Repair outputs: #{Paint[File.join(@fixtures_path, 'repair'),
164
+ :cyan]}"
165
+ when "both"
166
+ puts " 📂 Check outputs: #{Paint[File.join(@fixtures_path, 'check'),
167
+ :cyan]}"
168
+ puts " 📂 Repair outputs: #{Paint[File.join(@fixtures_path, 'repair'),
169
+ :cyan]}"
170
+ end
171
+ end
172
+
173
+ def find_test_files
174
+ return [] unless Dir.exist?(@test_dir)
175
+
176
+ # Find all .svg and .xml files
177
+ pattern = File.join(@test_dir, "*.{svg,xml}")
178
+ Dir.glob(pattern).map { |path| File.basename(path) }.sort
179
+ end
180
+
181
+ def should_skip_file?(filename)
182
+ # Skip files that are known to be problematic
183
+ skip_patterns = [
184
+ /^full-tiny\.xml$/, # Too large
185
+ /^cache_saved/, # Cache directory
186
+ ]
187
+
188
+ skip_patterns.any? { |pattern| filename.match?(pattern) }
189
+ end
190
+
191
+ def run_svgcheck_mode(input_file, output_base, mode)
192
+ # Check if python3 is available
193
+ unless command_available?("python3")
194
+ puts Paint["❌ python3 not found. Please install Python 3.", :red]
195
+ return false
196
+ end
197
+
198
+ # Check if svgcheck module is available
199
+ unless Dir.exist?(@svgcheck_dir)
200
+ puts Paint["❌ svgcheck directory not found: #{@svgcheck_dir}", :red]
201
+ return false
202
+ end
203
+
204
+ begin
205
+ # Change to svgcheck directory to run the module
206
+ original_dir = Dir.pwd
207
+ Dir.chdir(@svgcheck_dir)
208
+
209
+ # Build command based on mode
210
+ relative_input = File.join("Tests", File.basename(input_file))
211
+ cmd = if mode == "repair"
212
+ ["python3", "run.py", "--repair", relative_input]
213
+ else
214
+ ["python3", "run.py", relative_input] # check mode - no --repair flag
215
+ end
216
+
217
+ puts " Running #{mode}: #{cmd.join(' ')}" if @verbose
218
+
219
+ stdout, stderr, status = Open3.capture3(*cmd)
220
+
221
+ # Return to original directory
222
+ Dir.chdir(original_dir)
223
+
224
+ # svgcheck writes validation messages to stderr, save as .out (following existing convention)
225
+ File.write("#{output_base}.out", stderr)
226
+
227
+ # svgcheck writes any other errors to stderr, but we'll keep a separate .err for consistency
228
+ File.write("#{output_base}.err", "")
229
+
230
+ # Write exit code to .code file
231
+ File.write("#{output_base}.code", status.exitstatus.to_s)
232
+
233
+ # For repair mode, svgcheck writes remediated SVG content to stdout
234
+ # For check mode, stdout should be empty or minimal
235
+ File.write("#{output_base}.file", stdout) if mode == "repair"
236
+
237
+ puts " #{mode.capitalize} exit status: #{status.exitstatus}" if @verbose
238
+ puts " #{mode.capitalize} stdout lines: #{stdout.lines.count}" if @verbose
239
+ puts " #{mode.capitalize} stderr lines: #{stderr.lines.count}" if @verbose
240
+
241
+ true
242
+ rescue StandardError => e
243
+ puts Paint["❌ Error running svgcheck in #{mode} mode: #{e.message}",
244
+ :red]
245
+ # Make sure we return to original directory even on error
246
+ Dir.chdir(original_dir) if Dir.pwd != original_dir
247
+ false
248
+ end
249
+ end
250
+
251
+ # SECURITY: Prevent command injection by using array form of system()
252
+ # GitHub CodeQL: Uncontrolled command line (CWE-078)
253
+ # This affects svgcheck generation commands (development use only)
254
+ def command_available?(command)
255
+ # Use array form to prevent shell injection
256
+ system("which", command, out: File::NULL, err: File::NULL) ||
257
+ system("where", command, out: File::NULL, err: File::NULL) || # Windows
258
+ File.executable?(command) # Direct path check
259
+ end
260
+ end
261
+ end
262
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SvgConform
4
+ module Compatibility
5
+ # Value object representing the context and configuration for compatibility analysis
6
+ class AnalysisContext
7
+ attr_reader :mode, :profile, :semantic, :svgcheck_dir, :file, :output_file
8
+
9
+ def initialize(options = {})
10
+ @mode = (options[:mode] || "check").to_sym
11
+ @profile = options[:profile]&.to_sym || :svg_1_2_rfc_compatible
12
+ @semantic = options[:semantic] || false
13
+ @svgcheck_dir = options[:svgcheck_dir] || "spec/fixtures/svgcheck"
14
+ @file = options[:file]
15
+ @output_file = options[:output]
16
+ end
17
+
18
+ def single_file_analysis?
19
+ !@file.nil?
20
+ end
21
+
22
+ def batch_analysis?
23
+ @file.nil?
24
+ end
25
+
26
+ def check_mode?
27
+ @mode == :check
28
+ end
29
+
30
+ def repair_mode?
31
+ @mode == :repair
32
+ end
33
+
34
+ def semantic_analysis?
35
+ @semantic
36
+ end
37
+
38
+ def file_output?
39
+ !@output_file.nil?
40
+ end
41
+
42
+ def inputs_dir
43
+ File.join(@svgcheck_dir, "inputs")
44
+ end
45
+
46
+ def outputs_dir
47
+ File.join(@svgcheck_dir, @mode.to_s)
48
+ end
49
+
50
+ def input_file_path
51
+ return nil unless single_file_analysis?
52
+
53
+ File.join(inputs_dir, @file)
54
+ end
55
+
56
+ def to_h
57
+ {
58
+ mode: @mode,
59
+ profile: @profile,
60
+ semantic: @semantic,
61
+ svgcheck_dir: @svgcheck_dir,
62
+ file: @file,
63
+ output_file: @output_file,
64
+ }
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SvgConform
4
+ module Compatibility
5
+ # Value object representing the results of a compatibility comparison
6
+ class ComparisonResult
7
+ attr_reader :filename, :type, :validation_comparison, :content_comparison,
8
+ :xml_equivalence, :svg_conform_result, :svgcheck_result
9
+
10
+ def initialize(filename:, type:, **options)
11
+ @filename = filename
12
+ @type = type
13
+ @validation_comparison = options[:validation_comparison]
14
+ @content_comparison = options[:content_comparison]
15
+ @xml_equivalence = options[:xml_equivalence]
16
+ @svg_conform_result = options[:svg_conform_result]
17
+ @svgcheck_result = options[:svgcheck_result]
18
+ end
19
+
20
+ def semantic_validation?
21
+ @type == :semantic_validation
22
+ end
23
+
24
+ def semantic_repair?
25
+ @type == :semantic_repair
26
+ end
27
+
28
+ def basic_validation?
29
+ @type == :basic_validation
30
+ end
31
+
32
+ def basic_repair?
33
+ @type == :basic_repair
34
+ end
35
+
36
+ def compatibility_score
37
+ return 0.0 unless @validation_comparison
38
+
39
+ raw_score = @validation_comparison[:compatibility_score] || 0.0
40
+ raw_score * 100.0
41
+ end
42
+
43
+ def validity_match?
44
+ return false unless @validation_comparison
45
+
46
+ @validation_comparison.dig(:overall_validity, :match) || false
47
+ end
48
+
49
+ def svg_conform_valid?
50
+ if semantic_validation? || semantic_repair?
51
+ @validation_comparison&.dig(:overall_validity, :svg_conform) || false
52
+ else
53
+ @svg_conform_result&.success? || false
54
+ end
55
+ end
56
+
57
+ def svgcheck_valid?
58
+ if semantic_validation? || semantic_repair?
59
+ @validation_comparison&.dig(:overall_validity, :svgcheck) || false
60
+ else
61
+ @svgcheck_result&.valid || false
62
+ end
63
+ end
64
+
65
+ def content_equivalence_score
66
+ return 0.0 unless @content_comparison
67
+
68
+ raw_score = @content_comparison[:semantic_equivalence] || 0.0
69
+ raw_score * 100.0
70
+ end
71
+
72
+ def xml_equivalent?
73
+ return false unless @xml_equivalence
74
+
75
+ @xml_equivalence[:xml_equivalent] || false
76
+ end
77
+
78
+ def xml_differences
79
+ return [] unless @xml_equivalence
80
+
81
+ @xml_equivalence[:differences] || []
82
+ end
83
+
84
+ def xml_error
85
+ return nil unless @xml_equivalence
86
+
87
+ @xml_equivalence[:error]
88
+ end
89
+
90
+ def successful_remediation?
91
+ if semantic_repair?
92
+ svg_conform_valid?
93
+ elsif basic_repair?
94
+ @svg_conform_result&.success? || false
95
+ else
96
+ false
97
+ end
98
+ end
99
+
100
+ def issues_fixed
101
+ return 0 unless basic_repair?
102
+
103
+ @svg_conform_result&.issues_fixed || 0
104
+ end
105
+
106
+ def remediations_applied
107
+ return 0 unless basic_repair?
108
+
109
+ @svg_conform_result&.remediations_applied || 0
110
+ end
111
+
112
+ def error_count
113
+ if semantic_validation? || semantic_repair?
114
+ # For semantic results, we'd need to extract from validation_comparison
115
+ 0
116
+ else
117
+ # For basic repair mode, count initial validation errors
118
+ svg_conform_errors = if @svg_conform_result&.initial_validation
119
+ @svg_conform_result.initial_validation.failed_requirements.length
120
+ else
121
+ 0
122
+ end
123
+
124
+ svgcheck_errors = @svgcheck_result&.errors&.total_count || 0
125
+
126
+ svg_conform_errors + svgcheck_errors
127
+ end
128
+ end
129
+
130
+ def to_h
131
+ {
132
+ filename: @filename,
133
+ type: @type,
134
+ compatibility_score: compatibility_score,
135
+ validity_match: validity_match?,
136
+ svg_conform_valid: svg_conform_valid?,
137
+ svgcheck_valid: svgcheck_valid?,
138
+ content_equivalence_score: content_equivalence_score,
139
+ xml_equivalent: xml_equivalent?,
140
+ successful_remediation: successful_remediation?,
141
+ issues_fixed: issues_fixed,
142
+ remediations_applied: remediations_applied,
143
+ }
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "analysis_context"
4
+ require_relative "comparison_result"
5
+ require_relative "file_processor"
6
+ require_relative "svg_analysis_engine"
7
+ require_relative "xml_analysis_engine"
8
+ require_relative "report_formatter"
9
+
10
+ module SvgConform
11
+ module Compatibility
12
+ # Main orchestrator for compatibility analysis operations
13
+ class CompatibilityAnalyzer
14
+ def initialize(options = {})
15
+ @context = AnalysisContext.new(options)
16
+ @file_processor = FileProcessor.new(@context)
17
+ @svg_engine = SvgAnalysisEngine.new(@context, @file_processor)
18
+ @xml_engine = XmlAnalysisEngine.new(@context, @file_processor)
19
+ @formatter = ReportFormatter.new(@context)
20
+ end
21
+
22
+ def run_analysis
23
+ @file_processor.validate_environment
24
+
25
+ if @context.single_file_analysis?
26
+ run_single_file_analysis
27
+ else
28
+ run_batch_analysis
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def run_single_file_analysis
35
+ filename = @context.file
36
+ result = analyze_single_file(filename)
37
+
38
+ if result
39
+ @formatter.display_single_file_result(result)
40
+ @formatter.write_output_file(result)
41
+ end
42
+
43
+ result
44
+ end
45
+
46
+ def run_batch_analysis
47
+ files = @file_processor.discover_files
48
+ results = []
49
+
50
+ files.each do |filename|
51
+ result = analyze_single_file(filename)
52
+ results << result if result
53
+ end
54
+
55
+ @formatter.display_batch_results(results)
56
+ @formatter.write_output_file(results)
57
+
58
+ results
59
+ end
60
+
61
+ def analyze_single_file(filename)
62
+ unless @file_processor.file_exists?(filename)
63
+ puts "❌ Input file not found: #{filename}"
64
+ return nil
65
+ end
66
+
67
+ svgcheck_report = @file_processor.parse_svgcheck_outputs(filename)
68
+ unless svgcheck_report
69
+ puts "❌ Svgcheck outputs not found for: #{filename}"
70
+ return nil
71
+ end
72
+
73
+ if @file_processor.xml_file?(filename)
74
+ @xml_engine.analyze_file(filename, svgcheck_report)
75
+ else
76
+ @svg_engine.analyze_file(filename, svgcheck_report)
77
+ end
78
+ rescue StandardError => e
79
+ puts "❌ Error analyzing #{filename}: #{e.message}"
80
+ puts e.backtrace.first(5).join("\n") if ENV["DEBUG"]
81
+ nil
82
+ end
83
+ end
84
+ end
85
+ end