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.
- checksums.yaml +7 -0
- data/.github/workflows/rake.yml +15 -0
- data/.github/workflows/release.yml +23 -0
- data/.github/workflows/svgcheck-compatibility.yml +135 -0
- data/.gitignore +12 -0
- data/.rspec +3 -0
- data/.rubocop.yml +13 -0
- data/.rubocop_todo.yml +183 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/Gemfile +14 -0
- data/README.adoc +1384 -0
- data/Rakefile +15 -0
- data/config/profiles/base.yml +34 -0
- data/config/profiles/lucid_fix.yml +37 -0
- data/config/profiles/metanorma.yml +212 -0
- data/config/profiles/no_external_css.yml +20 -0
- data/config/profiles/svg_1_2_rfc.yml +284 -0
- data/config/profiles/svg_1_2_rfc_with_rdf.yml +145 -0
- data/config/svgcheck_mapping.yml +180 -0
- data/docs/profiles.adoc +547 -0
- data/docs/rdf_metadata_support.adoc +212 -0
- data/docs/remediation.adoc +732 -0
- data/docs/requirements.adoc +709 -0
- data/examples/demo.rb +116 -0
- data/examples/requirements_demo.rb +240 -0
- data/exe/svg_conform +7 -0
- data/lib/svg_conform/batch_report.rb +70 -0
- data/lib/svg_conform/cli.rb +107 -0
- data/lib/svg_conform/commands/check.rb +390 -0
- data/lib/svg_conform/commands/profiles.rb +118 -0
- data/lib/svg_conform/commands/svgcheck.rb +90 -0
- data/lib/svg_conform/commands/svgcheck_compare.rb +92 -0
- data/lib/svg_conform/commands/svgcheck_compatibility.rb +43 -0
- data/lib/svg_conform/commands/svgcheck_generate.rb +262 -0
- data/lib/svg_conform/compatibility/analysis_context.rb +68 -0
- data/lib/svg_conform/compatibility/comparison_result.rb +147 -0
- data/lib/svg_conform/compatibility/compatibility_analyzer.rb +85 -0
- data/lib/svg_conform/compatibility/file_processor.rb +109 -0
- data/lib/svg_conform/compatibility/pattern_discovery.rb +319 -0
- data/lib/svg_conform/compatibility/report_formatter.rb +359 -0
- data/lib/svg_conform/compatibility/svg_analysis_engine.rb +316 -0
- data/lib/svg_conform/compatibility/validity_analysis.rb +252 -0
- data/lib/svg_conform/compatibility/xml_analysis_engine.rb +198 -0
- data/lib/svg_conform/compatibility_analyzer.rb +285 -0
- data/lib/svg_conform/conformance_report.rb +267 -0
- data/lib/svg_conform/constants.rb +199 -0
- data/lib/svg_conform/css_color.rb +262 -0
- data/lib/svg_conform/document.rb +203 -0
- data/lib/svg_conform/external_checkers/svgcheck/compatibility_engine.rb +166 -0
- data/lib/svg_conform/external_checkers/svgcheck/output_generator.rb +101 -0
- data/lib/svg_conform/external_checkers/svgcheck/parser.rb +200 -0
- data/lib/svg_conform/external_checkers/svgcheck/report_comparator.rb +175 -0
- data/lib/svg_conform/external_checkers/svgcheck/report_generator.rb +82 -0
- data/lib/svg_conform/external_checkers/svgcheck/validation_pipeline.rb +249 -0
- data/lib/svg_conform/external_checkers/svgcheck.rb +56 -0
- data/lib/svg_conform/external_checkers.rb +34 -0
- data/lib/svg_conform/fixer.rb +56 -0
- data/lib/svg_conform/profile.rb +164 -0
- data/lib/svg_conform/profiles.rb +60 -0
- data/lib/svg_conform/remediation_engine.rb +92 -0
- data/lib/svg_conform/remediation_result.rb +36 -0
- data/lib/svg_conform/remediation_runner.rb +225 -0
- data/lib/svg_conform/remediations/base_remediation.rb +165 -0
- data/lib/svg_conform/remediations/color_remediation.rb +226 -0
- data/lib/svg_conform/remediations/font_embedding_remediation.rb +145 -0
- data/lib/svg_conform/remediations/font_remediation.rb +122 -0
- data/lib/svg_conform/remediations/image_embedding_remediation.rb +154 -0
- data/lib/svg_conform/remediations/invalid_id_references_remediation.rb +129 -0
- data/lib/svg_conform/remediations/namespace_attribute_remediation.rb +244 -0
- data/lib/svg_conform/remediations/namespace_remediation.rb +151 -0
- data/lib/svg_conform/remediations/no_external_css_remediation.rb +192 -0
- data/lib/svg_conform/remediations/style_promotion_remediation.rb +93 -0
- data/lib/svg_conform/remediations/viewbox_remediation.rb +127 -0
- data/lib/svg_conform/remediations.rb +40 -0
- data/lib/svg_conform/report_comparator.rb +772 -0
- data/lib/svg_conform/requirements/allowed_elements_requirement.rb +367 -0
- data/lib/svg_conform/requirements/base_requirement.rb +98 -0
- data/lib/svg_conform/requirements/color_restrictions_requirement.rb +126 -0
- data/lib/svg_conform/requirements/element_requirement_config.rb +75 -0
- data/lib/svg_conform/requirements/font_family_requirement.rb +133 -0
- data/lib/svg_conform/requirements/forbidden_content_requirement.rb +60 -0
- data/lib/svg_conform/requirements/id_reference_requirement.rb +133 -0
- data/lib/svg_conform/requirements/invalid_id_references_requirement.rb +132 -0
- data/lib/svg_conform/requirements/link_validation_requirement.rb +55 -0
- data/lib/svg_conform/requirements/namespace_attributes_requirement.rb +211 -0
- data/lib/svg_conform/requirements/namespace_requirement.rb +294 -0
- data/lib/svg_conform/requirements/no_external_css_requirement.rb +132 -0
- data/lib/svg_conform/requirements/no_external_fonts_requirement.rb +121 -0
- data/lib/svg_conform/requirements/no_external_images_requirement.rb +91 -0
- data/lib/svg_conform/requirements/style_promotion_requirement.rb +72 -0
- data/lib/svg_conform/requirements/style_requirement.rb +226 -0
- data/lib/svg_conform/requirements/viewbox_required_requirement.rb +96 -0
- data/lib/svg_conform/requirements.rb +49 -0
- data/lib/svg_conform/semantic_comparator.rb +829 -0
- data/lib/svg_conform/validation_context.rb +408 -0
- data/lib/svg_conform/validation_result.rb +146 -0
- data/lib/svg_conform/validator.rb +91 -0
- data/lib/svg_conform/version.rb +5 -0
- data/lib/svg_conform.rb +68 -0
- data/lib/tasks/fixtures.rake +321 -0
- data/lib/tasks/svgcheck.rake +111 -0
- data/reference-docs/SVG-1.2-RFC.rnc.txt +1676 -0
- data/reference-docs/Scalable Vector Graphics (SVG) 1.1 (Second Edition).html +40764 -0
- data/reference-docs/Scalable Vector Graphics (SVG) Tiny 1.2 Specification.html +44591 -0
- data/reference-docs/rfc7996.txt +2971 -0
- data/sig/svg_conform.rbs +4 -0
- data/spec/fixtures/allowed_elements/inputs/basic_violations.svg +21 -0
- data/spec/fixtures/allowed_elements/repair/basic_violations.svg +18 -0
- data/spec/fixtures/color_restrictions/inputs/basic_violations.svg +23 -0
- data/spec/fixtures/color_restrictions/repair/basic_violations.svg +23 -0
- data/spec/fixtures/comprehensive/inputs/multiple_violations.svg +21 -0
- data/spec/fixtures/comprehensive/repair/multiple_violations.svg +16 -0
- data/spec/fixtures/font_family/inputs/basic_violations.svg +17 -0
- data/spec/fixtures/font_family/repair/basic_violations.svg +17 -0
- data/spec/fixtures/forbidden_content/inputs/basic_violations.svg +27 -0
- data/spec/fixtures/forbidden_content/repair/basic_violations.svg +19 -0
- data/spec/fixtures/id_reference/inputs/basic_violations.svg +17 -0
- data/spec/fixtures/id_reference/repair/basic_violations.svg +15 -0
- data/spec/fixtures/link_validation/inputs/basic_violations.svg +20 -0
- data/spec/fixtures/link_validation/repair/basic_violations.svg +16 -0
- data/spec/fixtures/lucid/inputs/simple.svg +67 -0
- data/spec/fixtures/lucid/repair/simple.svg +63 -0
- data/spec/fixtures/namespace/inputs/basic_violations.svg +20 -0
- data/spec/fixtures/namespace/repair/basic_violations.svg +17 -0
- data/spec/fixtures/namespace_attributes/inputs/basic_violations.svg +16 -0
- data/spec/fixtures/namespace_attributes/repair/basic_violations.svg +15 -0
- data/spec/fixtures/no_external_css/inputs/basic_violations.svg +20 -0
- data/spec/fixtures/no_external_css/repair/basic_violations.svg +18 -0
- data/spec/fixtures/style/inputs/basic_violations.svg +22 -0
- data/spec/fixtures/style/repair/basic_violations.svg +22 -0
- data/spec/fixtures/style_promotion/inputs/basic_test.svg +15 -0
- data/spec/fixtures/style_promotion/repair/basic_test.svg +15 -0
- data/spec/fixtures/svg_1_2_rfc/inputs/allowed_elements_violations.svg +18 -0
- data/spec/fixtures/svg_1_2_rfc/inputs/color_restrictions_violations.svg +23 -0
- data/spec/fixtures/svgcheck/check/DrawBerry-sample-2.svg.code +1 -0
- data/spec/fixtures/svgcheck/check/DrawBerry-sample-2.svg.err +0 -0
- data/spec/fixtures/svgcheck/check/DrawBerry-sample-2.svg.out +23 -0
- data/spec/fixtures/svgcheck/check/IETF-test.svg.code +1 -0
- data/spec/fixtures/svgcheck/check/IETF-test.svg.err +0 -0
- data/spec/fixtures/svgcheck/check/IETF-test.svg.out +20 -0
- data/spec/fixtures/svgcheck/check/circle.svg.code +1 -0
- data/spec/fixtures/svgcheck/check/circle.svg.err +0 -0
- data/spec/fixtures/svgcheck/check/circle.svg.out +2 -0
- data/spec/fixtures/svgcheck/check/colors.svg.code +1 -0
- data/spec/fixtures/svgcheck/check/colors.svg.err +0 -0
- data/spec/fixtures/svgcheck/check/colors.svg.out +13 -0
- data/spec/fixtures/svgcheck/check/dia-sample-svg.svg.code +1 -0
- data/spec/fixtures/svgcheck/check/dia-sample-svg.svg.err +0 -0
- data/spec/fixtures/svgcheck/check/dia-sample-svg.svg.out +76 -0
- data/spec/fixtures/svgcheck/check/example-dot.svg.code +1 -0
- data/spec/fixtures/svgcheck/check/example-dot.svg.err +0 -0
- data/spec/fixtures/svgcheck/check/example-dot.svg.out +11 -0
- data/spec/fixtures/svgcheck/check/full-tiny.svg.code +1 -0
- data/spec/fixtures/svgcheck/check/full-tiny.svg.err +0 -0
- data/spec/fixtures/svgcheck/check/full-tiny.svg.out +5835 -0
- data/spec/fixtures/svgcheck/check/good.svg.code +1 -0
- data/spec/fixtures/svgcheck/check/good.svg.err +0 -0
- data/spec/fixtures/svgcheck/check/good.svg.out +1 -0
- data/spec/fixtures/svgcheck/check/httpbis-proxy20-fig6.svg.code +1 -0
- data/spec/fixtures/svgcheck/check/httpbis-proxy20-fig6.svg.err +0 -0
- data/spec/fixtures/svgcheck/check/httpbis-proxy20-fig6.svg.out +5 -0
- data/spec/fixtures/svgcheck/check/malformed.svg.code +1 -0
- data/spec/fixtures/svgcheck/check/malformed.svg.err +0 -0
- data/spec/fixtures/svgcheck/check/malformed.svg.out +8 -0
- data/spec/fixtures/svgcheck/check/rfc-svg.svg.code +1 -0
- data/spec/fixtures/svgcheck/check/rfc-svg.svg.err +0 -0
- data/spec/fixtures/svgcheck/check/rfc-svg.svg.out +1 -0
- data/spec/fixtures/svgcheck/check/rfc.xml.code +1 -0
- data/spec/fixtures/svgcheck/check/rfc.xml.err +0 -0
- data/spec/fixtures/svgcheck/check/rfc.xml.out +2 -0
- data/spec/fixtures/svgcheck/check/rgb.svg.code +1 -0
- data/spec/fixtures/svgcheck/check/rgb.svg.err +0 -0
- data/spec/fixtures/svgcheck/check/rgb.svg.out +9 -0
- data/spec/fixtures/svgcheck/check/svg-wordle.svg.code +1 -0
- data/spec/fixtures/svgcheck/check/svg-wordle.svg.err +0 -0
- data/spec/fixtures/svgcheck/check/svg-wordle.svg.out +508 -0
- data/spec/fixtures/svgcheck/check/threshold.svg.code +1 -0
- data/spec/fixtures/svgcheck/check/threshold.svg.err +0 -0
- data/spec/fixtures/svgcheck/check/threshold.svg.out +20 -0
- data/spec/fixtures/svgcheck/check/utf8.svg.code +1 -0
- data/spec/fixtures/svgcheck/check/utf8.svg.err +0 -0
- data/spec/fixtures/svgcheck/check/utf8.svg.out +162 -0
- data/spec/fixtures/svgcheck/check/viewBox-both.svg.code +1 -0
- data/spec/fixtures/svgcheck/check/viewBox-both.svg.err +0 -0
- data/spec/fixtures/svgcheck/check/viewBox-both.svg.out +4 -0
- data/spec/fixtures/svgcheck/check/viewBox-height.svg.code +1 -0
- data/spec/fixtures/svgcheck/check/viewBox-height.svg.err +0 -0
- data/spec/fixtures/svgcheck/check/viewBox-height.svg.out +3 -0
- data/spec/fixtures/svgcheck/check/viewBox-none.svg.code +1 -0
- data/spec/fixtures/svgcheck/check/viewBox-none.svg.err +0 -0
- data/spec/fixtures/svgcheck/check/viewBox-none.svg.out +3 -0
- data/spec/fixtures/svgcheck/check/viewBox-width.svg.code +1 -0
- data/spec/fixtures/svgcheck/check/viewBox-width.svg.err +0 -0
- data/spec/fixtures/svgcheck/check/viewBox-width.svg.out +3 -0
- data/spec/fixtures/svgcheck/inputs/DrawBerry-sample-2.svg +28 -0
- data/spec/fixtures/svgcheck/inputs/IETF-test.svg +28 -0
- data/spec/fixtures/svgcheck/inputs/circle.svg +3 -0
- data/spec/fixtures/svgcheck/inputs/colors.svg +18 -0
- data/spec/fixtures/svgcheck/inputs/dia-sample-svg.svg +47 -0
- data/spec/fixtures/svgcheck/inputs/example-dot.svg +75 -0
- data/spec/fixtures/svgcheck/inputs/full-tiny.svg +16194 -0
- data/spec/fixtures/svgcheck/inputs/good.svg +19 -0
- data/spec/fixtures/svgcheck/inputs/httpbis-proxy20-fig6.svg +2 -0
- data/spec/fixtures/svgcheck/inputs/malformed.svg +11 -0
- data/spec/fixtures/svgcheck/inputs/rfc-svg.svg +1028 -0
- data/spec/fixtures/svgcheck/inputs/rfc.xml +37 -0
- data/spec/fixtures/svgcheck/inputs/rgb.svg +9 -0
- data/spec/fixtures/svgcheck/inputs/svg-wordle.svg +330 -0
- data/spec/fixtures/svgcheck/inputs/threshold.svg +26 -0
- data/spec/fixtures/svgcheck/inputs/utf8.svg +448 -0
- data/spec/fixtures/svgcheck/inputs/viewBox-both.svg +3 -0
- data/spec/fixtures/svgcheck/inputs/viewBox-height.svg +3 -0
- data/spec/fixtures/svgcheck/inputs/viewBox-none.svg +3 -0
- data/spec/fixtures/svgcheck/inputs/viewBox-width.svg +3 -0
- data/spec/fixtures/svgcheck/repair/DrawBerry-sample-2.svg.code +1 -0
- data/spec/fixtures/svgcheck/repair/DrawBerry-sample-2.svg.err +0 -0
- data/spec/fixtures/svgcheck/repair/DrawBerry-sample-2.svg.file +0 -0
- data/spec/fixtures/svgcheck/repair/DrawBerry-sample-2.svg.out +23 -0
- data/spec/fixtures/svgcheck/repair/IETF-test.svg.code +1 -0
- data/spec/fixtures/svgcheck/repair/IETF-test.svg.err +0 -0
- data/spec/fixtures/svgcheck/repair/IETF-test.svg.file +29 -0
- data/spec/fixtures/svgcheck/repair/IETF-test.svg.out +20 -0
- data/spec/fixtures/svgcheck/repair/circle.svg.code +1 -0
- data/spec/fixtures/svgcheck/repair/circle.svg.err +0 -0
- data/spec/fixtures/svgcheck/repair/circle.svg.file +4 -0
- data/spec/fixtures/svgcheck/repair/circle.svg.out +2 -0
- data/spec/fixtures/svgcheck/repair/colors.svg.code +1 -0
- data/spec/fixtures/svgcheck/repair/colors.svg.err +0 -0
- data/spec/fixtures/svgcheck/repair/colors.svg.file +19 -0
- data/spec/fixtures/svgcheck/repair/colors.svg.out +13 -0
- data/spec/fixtures/svgcheck/repair/dia-sample-svg.svg.code +1 -0
- data/spec/fixtures/svgcheck/repair/dia-sample-svg.svg.err +0 -0
- data/spec/fixtures/svgcheck/repair/dia-sample-svg.svg.file +47 -0
- data/spec/fixtures/svgcheck/repair/dia-sample-svg.svg.out +76 -0
- data/spec/fixtures/svgcheck/repair/example-dot.svg.code +1 -0
- data/spec/fixtures/svgcheck/repair/example-dot.svg.err +0 -0
- data/spec/fixtures/svgcheck/repair/example-dot.svg.file +69 -0
- data/spec/fixtures/svgcheck/repair/example-dot.svg.out +11 -0
- data/spec/fixtures/svgcheck/repair/good.svg.code +1 -0
- data/spec/fixtures/svgcheck/repair/good.svg.err +0 -0
- data/spec/fixtures/svgcheck/repair/good.svg.file +0 -0
- data/spec/fixtures/svgcheck/repair/good.svg.out +1 -0
- data/spec/fixtures/svgcheck/repair/httpbis-proxy20-fig6.svg.code +1 -0
- data/spec/fixtures/svgcheck/repair/httpbis-proxy20-fig6.svg.err +0 -0
- data/spec/fixtures/svgcheck/repair/httpbis-proxy20-fig6.svg.file +73 -0
- data/spec/fixtures/svgcheck/repair/httpbis-proxy20-fig6.svg.out +5 -0
- data/spec/fixtures/svgcheck/repair/malformed.svg.code +1 -0
- data/spec/fixtures/svgcheck/repair/malformed.svg.err +0 -0
- data/spec/fixtures/svgcheck/repair/malformed.svg.file +9 -0
- data/spec/fixtures/svgcheck/repair/malformed.svg.out +8 -0
- data/spec/fixtures/svgcheck/repair/rfc-svg.svg.code +1 -0
- data/spec/fixtures/svgcheck/repair/rfc-svg.svg.err +0 -0
- data/spec/fixtures/svgcheck/repair/rfc-svg.svg.file +0 -0
- data/spec/fixtures/svgcheck/repair/rfc-svg.svg.out +1 -0
- data/spec/fixtures/svgcheck/repair/rfc.xml.code +1 -0
- data/spec/fixtures/svgcheck/repair/rfc.xml.err +0 -0
- data/spec/fixtures/svgcheck/repair/rfc.xml.file +37 -0
- data/spec/fixtures/svgcheck/repair/rfc.xml.out +2 -0
- data/spec/fixtures/svgcheck/repair/rgb.svg.code +1 -0
- data/spec/fixtures/svgcheck/repair/rgb.svg.err +0 -0
- data/spec/fixtures/svgcheck/repair/rgb.svg.file +10 -0
- data/spec/fixtures/svgcheck/repair/rgb.svg.out +9 -0
- data/spec/fixtures/svgcheck/repair/svg-wordle.svg.code +1 -0
- data/spec/fixtures/svgcheck/repair/svg-wordle.svg.err +0 -0
- data/spec/fixtures/svgcheck/repair/svg-wordle.svg.file +112 -0
- data/spec/fixtures/svgcheck/repair/svg-wordle.svg.out +508 -0
- data/spec/fixtures/svgcheck/repair/threshold.svg.code +1 -0
- data/spec/fixtures/svgcheck/repair/threshold.svg.err +0 -0
- data/spec/fixtures/svgcheck/repair/threshold.svg.file +27 -0
- data/spec/fixtures/svgcheck/repair/threshold.svg.out +20 -0
- data/spec/fixtures/svgcheck/repair/utf8.svg.code +1 -0
- data/spec/fixtures/svgcheck/repair/utf8.svg.err +0 -0
- data/spec/fixtures/svgcheck/repair/utf8.svg.file +381 -0
- data/spec/fixtures/svgcheck/repair/utf8.svg.out +162 -0
- data/spec/fixtures/svgcheck/repair/viewBox-both.svg.code +1 -0
- data/spec/fixtures/svgcheck/repair/viewBox-both.svg.err +0 -0
- data/spec/fixtures/svgcheck/repair/viewBox-both.svg.file +4 -0
- data/spec/fixtures/svgcheck/repair/viewBox-both.svg.out +4 -0
- data/spec/fixtures/svgcheck/repair/viewBox-height.svg.code +1 -0
- data/spec/fixtures/svgcheck/repair/viewBox-height.svg.err +0 -0
- data/spec/fixtures/svgcheck/repair/viewBox-height.svg.file +4 -0
- data/spec/fixtures/svgcheck/repair/viewBox-height.svg.out +3 -0
- data/spec/fixtures/svgcheck/repair/viewBox-none.svg.code +1 -0
- data/spec/fixtures/svgcheck/repair/viewBox-none.svg.err +0 -0
- data/spec/fixtures/svgcheck/repair/viewBox-none.svg.file +4 -0
- data/spec/fixtures/svgcheck/repair/viewBox-none.svg.out +3 -0
- data/spec/fixtures/svgcheck/repair/viewBox-width.svg.code +1 -0
- data/spec/fixtures/svgcheck/repair/viewBox-width.svg.err +0 -0
- data/spec/fixtures/svgcheck/repair/viewBox-width.svg.file +4 -0
- data/spec/fixtures/svgcheck/repair/viewBox-width.svg.out +3 -0
- data/spec/fixtures/viewbox_required/inputs/missing_viewbox.svg +10 -0
- data/spec/fixtures/viewbox_required/repair/missing_viewbox.svg +10 -0
- data/spec/spec_helper.rb +16 -0
- data/spec/svg_conform/batch_report_spec.rb +99 -0
- data/spec/svg_conform/commands/check_command_spec.rb +90 -0
- data/spec/svg_conform/commands/profiles_command_spec.rb +20 -0
- data/spec/svg_conform/commands/svgcheck_compare_command_spec.rb +13 -0
- data/spec/svg_conform/commands/svgcheck_compatibility_command_spec.rb +13 -0
- data/spec/svg_conform/commands/svgcheck_generate_command_spec.rb +14 -0
- data/spec/svg_conform/profiles/base_profile_spec.rb +42 -0
- data/spec/svg_conform/profiles/lucid_fix_profile_spec.rb +46 -0
- data/spec/svg_conform/profiles/lucid_profile_spec.rb +84 -0
- data/spec/svg_conform/profiles/metanorma_profile_spec.rb +62 -0
- data/spec/svg_conform/profiles/no_external_css_profile_spec.rb +66 -0
- data/spec/svg_conform/profiles/svg_1_2_rfc_profile_spec.rb +200 -0
- data/spec/svg_conform/profiles/svg_1_2_rfc_with_rdf_profile_spec.rb +81 -0
- data/spec/svg_conform/remediations/color_remediation_spec.rb +95 -0
- data/spec/svg_conform/remediations/font_embedding_remediation_spec.rb +20 -0
- data/spec/svg_conform/remediations/font_remediation_spec.rb +95 -0
- data/spec/svg_conform/remediations/image_embedding_remediation_spec.rb +20 -0
- data/spec/svg_conform/remediations/invalid_id_references_remediation_spec.rb +97 -0
- data/spec/svg_conform/remediations/namespace_attribute_remediation_spec.rb +97 -0
- data/spec/svg_conform/remediations/namespace_remediation_spec.rb +95 -0
- data/spec/svg_conform/remediations/no_external_css_remediation_spec.rb +97 -0
- data/spec/svg_conform/remediations/style_promotion_remediation_spec.rb +97 -0
- data/spec/svg_conform/remediations/viewbox_remediation_spec.rb +95 -0
- data/spec/svg_conform/requirements/allowed_elements_requirement_spec.rb +118 -0
- data/spec/svg_conform/requirements/color_restrictions_requirement_spec.rb +168 -0
- data/spec/svg_conform/requirements/font_family_requirement_spec.rb +188 -0
- data/spec/svg_conform/requirements/forbidden_content_requirement_spec.rb +195 -0
- data/spec/svg_conform/requirements/id_reference_requirement_spec.rb +78 -0
- data/spec/svg_conform/requirements/invalid_id_references_requirement_spec.rb +78 -0
- data/spec/svg_conform/requirements/link_validation_requirement_spec.rb +78 -0
- data/spec/svg_conform/requirements/namespace_attributes_requirement_spec.rb +86 -0
- data/spec/svg_conform/requirements/namespace_requirement_spec.rb +184 -0
- data/spec/svg_conform/requirements/no_external_css_requirement_spec.rb +78 -0
- data/spec/svg_conform/requirements/no_external_fonts_requirement_spec.rb +20 -0
- data/spec/svg_conform/requirements/no_external_images_requirement_spec.rb +20 -0
- data/spec/svg_conform/requirements/style_promotion_requirement_spec.rb +78 -0
- data/spec/svg_conform/requirements/style_requirement_spec.rb +76 -0
- data/spec/svg_conform/requirements/viewbox_required_requirement_spec.rb +165 -0
- data/spec/svg_conform_spec.rb +32 -0
- data/spec/svgcheck_compatibility_spec.rb +355 -0
- data/svg_conform.gemspec +35 -0
- metadata +436 -0
|
@@ -0,0 +1,829 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "nokogiri"
|
|
4
|
+
|
|
5
|
+
module SvgConform
|
|
6
|
+
# Semantic comparison engine for comparing validation results and remediated content
|
|
7
|
+
class SemanticComparator
|
|
8
|
+
# Compare validation results semantically
|
|
9
|
+
def self.compare_validation_results(svg_conform_report, svgcheck_report)
|
|
10
|
+
# Extract semantic features from both reports
|
|
11
|
+
svg_conform_features = extract_validation_features(svg_conform_report)
|
|
12
|
+
svgcheck_features = extract_validation_features(svgcheck_report)
|
|
13
|
+
|
|
14
|
+
# Detect repair mode (both tools report valid=true)
|
|
15
|
+
repair_mode = svg_conform_features[:valid] && svgcheck_features[:valid]
|
|
16
|
+
|
|
17
|
+
# Detect mixed mode (one valid, one invalid)
|
|
18
|
+
mixed_mode = svg_conform_features[:valid] != svgcheck_features[:valid]
|
|
19
|
+
|
|
20
|
+
# Compare features semantically
|
|
21
|
+
comparison = {
|
|
22
|
+
overall_validity: compare_validity(svg_conform_features[:valid],
|
|
23
|
+
svgcheck_features[:valid]),
|
|
24
|
+
requirement_coverage: compare_requirement_coverage(svg_conform_features[:requirements],
|
|
25
|
+
svgcheck_features[:requirements]),
|
|
26
|
+
semantic_issues: compare_semantic_issues(svg_conform_features[:issues], svgcheck_features[:issues],
|
|
27
|
+
repair_mode, mixed_mode),
|
|
28
|
+
detailed_mapping: create_detailed_mapping(svg_conform_features[:issues], svgcheck_features[:issues],
|
|
29
|
+
repair_mode, mixed_mode),
|
|
30
|
+
repair_mode: repair_mode,
|
|
31
|
+
mixed_mode: mixed_mode,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# Add validity_match for compatibility
|
|
35
|
+
comparison[:validity_match] = comparison[:overall_validity][:match]
|
|
36
|
+
|
|
37
|
+
# Calculate overall compatibility score
|
|
38
|
+
comparison[:compatibility_score] =
|
|
39
|
+
calculate_compatibility_score(comparison)
|
|
40
|
+
comparison
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Compare remediated SVG content semantically
|
|
44
|
+
def self.compare_remediated_content(svg_conform_content, svgcheck_content)
|
|
45
|
+
# Parse both SVG documents
|
|
46
|
+
svg_conform_doc = parse_svg_safely(svg_conform_content)
|
|
47
|
+
svgcheck_doc = parse_svg_safely(svgcheck_content)
|
|
48
|
+
|
|
49
|
+
return { error: "Failed to parse SVG content" } if svg_conform_doc.nil? || svgcheck_doc.nil?
|
|
50
|
+
|
|
51
|
+
# Extract semantic features from both documents
|
|
52
|
+
svg_conform_features = extract_svg_features(svg_conform_doc)
|
|
53
|
+
svgcheck_features = extract_svg_features(svgcheck_doc)
|
|
54
|
+
|
|
55
|
+
# Compare features
|
|
56
|
+
{
|
|
57
|
+
structure_match: compare_structure(svg_conform_features[:structure],
|
|
58
|
+
svgcheck_features[:structure]),
|
|
59
|
+
attributes_match: compare_attributes(svg_conform_features[:attributes],
|
|
60
|
+
svgcheck_features[:attributes]),
|
|
61
|
+
content_match: compare_content(svg_conform_features[:content],
|
|
62
|
+
svgcheck_features[:content]),
|
|
63
|
+
namespace_match: compare_namespaces(svg_conform_features[:namespaces],
|
|
64
|
+
svgcheck_features[:namespaces]),
|
|
65
|
+
style_match: compare_styles(svg_conform_features[:styles],
|
|
66
|
+
svgcheck_features[:styles]),
|
|
67
|
+
semantic_equivalence: calculate_semantic_equivalence(
|
|
68
|
+
svg_conform_features, svgcheck_features
|
|
69
|
+
),
|
|
70
|
+
}
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Extract validation features for semantic comparison
|
|
74
|
+
def self.extract_validation_features(report)
|
|
75
|
+
# Extract ALL issues (errors, warnings, infos) for semantic comparison
|
|
76
|
+
issues = []
|
|
77
|
+
|
|
78
|
+
issues.concat(report.errors.issues) if report.respond_to?(:errors) && report.errors.respond_to?(:issues)
|
|
79
|
+
|
|
80
|
+
# For svgcheck reports, also include warnings and infos if available
|
|
81
|
+
issues.concat(report.warnings.issues) if report.respond_to?(:warnings) && report.warnings.respond_to?(:issues)
|
|
82
|
+
|
|
83
|
+
# If the report has a direct issues method, use that
|
|
84
|
+
issues = report.issues if issues.empty? && report.respond_to?(:issues)
|
|
85
|
+
|
|
86
|
+
{
|
|
87
|
+
valid: report.respond_to?(:valid) ? report.valid : false,
|
|
88
|
+
requirements: extract_requirement_types(issues),
|
|
89
|
+
issues: group_issues_semantically(issues),
|
|
90
|
+
}
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Extract requirement types from issues
|
|
94
|
+
def self.extract_requirement_types(issues)
|
|
95
|
+
requirement_counts = {}
|
|
96
|
+
|
|
97
|
+
issues.each do |issue|
|
|
98
|
+
req_id = issue.respond_to?(:requirement_id) ? issue.requirement_id : "unknown"
|
|
99
|
+
requirement_counts[req_id] = (requirement_counts[req_id] || 0) + 1
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
requirement_counts
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Group issues by semantic meaning
|
|
106
|
+
def self.group_issues_semantically(issues)
|
|
107
|
+
semantic_groups = {}
|
|
108
|
+
|
|
109
|
+
issues.each do |issue|
|
|
110
|
+
semantic_key = extract_semantic_key(issue)
|
|
111
|
+
semantic_groups[semantic_key] ||= []
|
|
112
|
+
semantic_groups[semantic_key] << issue
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
semantic_groups.transform_values(&:length)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Extract semantic key from an issue
|
|
119
|
+
def self.extract_semantic_key(issue)
|
|
120
|
+
# Handle different issue types
|
|
121
|
+
message = if issue.respond_to?(:message)
|
|
122
|
+
issue.message
|
|
123
|
+
elsif issue.is_a?(Hash)
|
|
124
|
+
issue[:message] || issue["message"]
|
|
125
|
+
else
|
|
126
|
+
issue.to_s
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# SECURITY: Prevent ReDoS attacks by limiting message length
|
|
130
|
+
# GitHub CodeQL: Regular expressions with excessive backtracking can cause DoS
|
|
131
|
+
# This affects svgcheck comparison commands (development use only)
|
|
132
|
+
# Note: Ruby 3.2+ has built-in regex caching that prevents ReDoS
|
|
133
|
+
if message.length > 1000
|
|
134
|
+
# Truncate very long messages to prevent exponential backtracking
|
|
135
|
+
message = "#{message[0, 997]}..."
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Map ONLY the exact 28 svgcheck.py log patterns to semantic keys
|
|
139
|
+
# SECURITY: Regex patterns use quantifier limits to prevent ReDoS
|
|
140
|
+
case message
|
|
141
|
+
# Error patterns from checksvg.py (4 patterns)
|
|
142
|
+
when /\AMalformed field '([^']{1,200})' in style attribute found\. Field removed\.\z/
|
|
143
|
+
"malformed_style_field:#{normalize_value(::Regexp.last_match(1))}"
|
|
144
|
+
when /\AMalformed field '(\[[^\]]{1,200}\])' in style attribute found\. Field removed\.\z/
|
|
145
|
+
"malformed_style_field:#{normalize_value(::Regexp.last_match(1))}"
|
|
146
|
+
when /\AMalformed style declaration '([^']{1,200})' found\. Declaration removed\.\z/
|
|
147
|
+
"malformed_style_field:#{normalize_value(::Regexp.last_match(1))}"
|
|
148
|
+
when /\AStyle property '([^']{1,100})' promoted to attribute\z/
|
|
149
|
+
"style_promotion:#{::Regexp.last_match(1)}"
|
|
150
|
+
when /\AStyle property '([^']{1,100})' removed\z/
|
|
151
|
+
"style_property_removed:#{::Regexp.last_match(1)}"
|
|
152
|
+
when /\AError when calculating SVG size: (.{1,500})\z/
|
|
153
|
+
"informative:svg_size_calculation_error"
|
|
154
|
+
when /\AFile does not conform to SVG requirements\z/
|
|
155
|
+
"informative:file_nonconformant"
|
|
156
|
+
|
|
157
|
+
# Warning patterns from checksvg.py (10 patterns)
|
|
158
|
+
when /\AElement '([^']{1,100})' in namespace '([^']{1,200})' is not allowed\z/
|
|
159
|
+
"invalid_element_namespace:#{::Regexp.last_match(1)}:#{normalize_namespace(::Regexp.last_match(2))}"
|
|
160
|
+
when /\AElement '([^']{1,100})' not allowed\z/
|
|
161
|
+
"invalid_element:#{::Regexp.last_match(1)}"
|
|
162
|
+
when /\AElement '([^']{1,100})' does not allow attributes with namespace '([^']{1,200})'\z/
|
|
163
|
+
"namespace_violation:#{::Regexp.last_match(1)}:#{normalize_namespace(::Regexp.last_match(2))}"
|
|
164
|
+
when /\AThe element '([^']{1,100})' does not allow the attribute '([^']{1,100})', attribute to be removed\.\z/
|
|
165
|
+
"invalid_attribute:#{::Regexp.last_match(1)}:#{::Regexp.last_match(2)}"
|
|
166
|
+
when /\AThe attribute '([^']{1,100})' does not allow the value '([^']{1,200})', replaced with '([^']{1,200})'\z/
|
|
167
|
+
# Normalize color values for semantic equivalence
|
|
168
|
+
attribute = ::Regexp.last_match(1)
|
|
169
|
+
value = ::Regexp.last_match(2)
|
|
170
|
+
|
|
171
|
+
# For color attributes, use normalized color values
|
|
172
|
+
if %w[fill stroke].include?(attribute)
|
|
173
|
+
"invalid_attribute_value:#{attribute}:#{normalize_color_value(value)}"
|
|
174
|
+
else
|
|
175
|
+
"invalid_attribute_value:#{attribute}:#{normalize_value(value)}"
|
|
176
|
+
end
|
|
177
|
+
when /\AThe attribute '([^']{1,100})' does not allow the value '([^']{1,200})', attribute to be removed\z/
|
|
178
|
+
"invalid_attribute_value:#{::Regexp.last_match(1)}:#{normalize_value(::Regexp.last_match(2))}"
|
|
179
|
+
when /\AThe attribute viewBox is required on the root svg element\z/
|
|
180
|
+
"viewbox_required"
|
|
181
|
+
when /\ATrying to put in the attribute with value '([^']{1,200})'\z/
|
|
182
|
+
"informative:viewbox_auto_added"
|
|
183
|
+
when /\AThe namespace ([^\s]{1,200}) is not permitted for svg elements\.\z/
|
|
184
|
+
"namespace_violation:element:#{normalize_namespace(::Regexp.last_match(1))}"
|
|
185
|
+
when /\AThe element '([^']{1,100})' is not allowed as a child of '([^']{1,100})'\z/
|
|
186
|
+
"invalid_child:#{::Regexp.last_match(1)}:#{::Regexp.last_match(2)}"
|
|
187
|
+
when /\AMalformed namespace\. Should have errored during parsing\z/
|
|
188
|
+
"informative:malformed_namespace"
|
|
189
|
+
when /\A--no-xinclude option is deprecated and has no effect\.\z/
|
|
190
|
+
"informative:deprecated_option"
|
|
191
|
+
|
|
192
|
+
# Note patterns from checksvg.py (13 patterns)
|
|
193
|
+
when /\Amodify_style check '([^']{1,100})' in '([^']{1,100})'\z/
|
|
194
|
+
"informative:modify_style_check"
|
|
195
|
+
when /\A modify_style - p=([^\s]{1,100}) v=(.{1,200})\z/
|
|
196
|
+
"informative:modify_style_processing"
|
|
197
|
+
when /\Avalue_ok look for (.{1,200}) in (.{1,200})\z/
|
|
198
|
+
"informative:value_validation"
|
|
199
|
+
when /\A legal value list (.{1,500})\z/
|
|
200
|
+
"informative:legal_values"
|
|
201
|
+
when /\A --- skip to end -- (.{1,200})\z/
|
|
202
|
+
"informative:validation_skip"
|
|
203
|
+
when /\AColor or grayscale heuristic applied to: '([^']{1,100})' yields shade: '([^']{1,100})'\z/
|
|
204
|
+
"informative:color_heuristic"
|
|
205
|
+
when /\A[^\s]{1,50} tag = (.{1,200})\z/
|
|
206
|
+
"informative:element_processing"
|
|
207
|
+
when /\A[^\s]{1,50} element [^:]{1,50}: (.{1,200})\z/
|
|
208
|
+
"informative:element_attributes"
|
|
209
|
+
when /\A[^\s]{1,50} attr ([^\s]{1,100}) = ([^\s]{1,200}) \(ns = ([^)]{0,200})\)\z/
|
|
210
|
+
"informative:attribute_processing"
|
|
211
|
+
when /\A[^\s]{1,50}child, tag = (.{1,200})\z/
|
|
212
|
+
"informative:child_processing"
|
|
213
|
+
when /\AChecking svg element at line (\d{1,10}) in file (.{1,500})\z/
|
|
214
|
+
"informative:svg_element_check"
|
|
215
|
+
|
|
216
|
+
# SvgConform-specific patterns that map to svgcheck semantic equivalents
|
|
217
|
+
when /\AColor '([^']{1,100})' in attribute '([^']{1,100})' is not allowed in this profile\z/
|
|
218
|
+
# Map SvgConform color restriction to svgcheck invalid_attribute_value pattern
|
|
219
|
+
# Normalize color values to handle different formats (WHITE -> white, etc.)
|
|
220
|
+
"invalid_attribute_value:#{::Regexp.last_match(2)}:#{normalize_color_value(::Regexp.last_match(1))}"
|
|
221
|
+
when /\AColor '([^']{1,100})' in style property '([^']{1,100})' is not allowed in this profile\z/
|
|
222
|
+
# Map SvgConform style property color restriction to svgcheck invalid_attribute_value pattern
|
|
223
|
+
# Svgcheck promotes style properties to attributes, so we map accordingly
|
|
224
|
+
"invalid_attribute_value:#{::Regexp.last_match(2)}:#{normalize_color_value(::Regexp.last_match(1))}"
|
|
225
|
+
when /\AFont family '([^']{1,200})' is not allowed in this profile\z/
|
|
226
|
+
# Map SvgConform font family restriction to svgcheck invalid_attribute_value pattern
|
|
227
|
+
"invalid_attribute_value:font-family:#{normalize_value(::Regexp.last_match(1))}"
|
|
228
|
+
when /\AFont family '([^']{1,200})' in style is not allowed in this profile\z/
|
|
229
|
+
# Map SvgConform style font family restriction to svgcheck invalid_attribute_value pattern
|
|
230
|
+
font_family = normalize_value(::Regexp.last_match(1))
|
|
231
|
+
# Embedded font restrictions are profile differences
|
|
232
|
+
if font_family.include?("embedded")
|
|
233
|
+
"informative:profile_stricter_embedded_fonts:#{font_family}"
|
|
234
|
+
else
|
|
235
|
+
"invalid_attribute_value:font-family:#{font_family}"
|
|
236
|
+
end
|
|
237
|
+
when /\AFont family '([^']{1,200})' is not allowed in this profile\z/
|
|
238
|
+
# Map SvgConform font family restriction to svgcheck invalid_attribute_value pattern
|
|
239
|
+
font_family = normalize_value(::Regexp.last_match(1))
|
|
240
|
+
# Embedded font restrictions are profile differences
|
|
241
|
+
if font_family.include?("embedded")
|
|
242
|
+
"informative:profile_stricter_embedded_fonts:#{font_family}"
|
|
243
|
+
else
|
|
244
|
+
"invalid_attribute_value:font-family:#{font_family}"
|
|
245
|
+
end
|
|
246
|
+
when /\Asvg root element must have a viewbox attribute\z/i
|
|
247
|
+
# Map SvgConform viewBox requirement to svgcheck pattern (case insensitive)
|
|
248
|
+
"viewbox_required"
|
|
249
|
+
when /\AElement '([^']{1,100})' is not allowed in this profile\z/
|
|
250
|
+
# Map SvgConform element restriction to svgcheck invalid_child pattern
|
|
251
|
+
element = ::Regexp.last_match(1)
|
|
252
|
+
# Font-related and clipPath elements are profile differences, not validation errors
|
|
253
|
+
if %w[font glyph font-face missing-glyph clipPath].include?(element)
|
|
254
|
+
"informative:profile_stricter_elements:#{element}"
|
|
255
|
+
else
|
|
256
|
+
"invalid_child:#{element}:svg"
|
|
257
|
+
end
|
|
258
|
+
when /\AThe element '([^']{1,100})' is not allowed as a child of '([^']{1,100})'\z/
|
|
259
|
+
# Map SvgConform child element restriction to svgcheck pattern
|
|
260
|
+
element = ::Regexp.last_match(1)
|
|
261
|
+
parent = ::Regexp.last_match(2)
|
|
262
|
+
# Font-related and clipPath elements as children are profile differences
|
|
263
|
+
if %w[font glyph font-face missing-glyph clipPath].include?(element)
|
|
264
|
+
"informative:profile_stricter_elements:#{element}:#{parent}"
|
|
265
|
+
else
|
|
266
|
+
"invalid_child:#{element}:#{parent}"
|
|
267
|
+
end
|
|
268
|
+
when /\AAttribute '([^']{1,100})' is not allowed on element '([^']{1,100})'\z/
|
|
269
|
+
# Map SvgConform attribute restriction to svgcheck pattern
|
|
270
|
+
"invalid_attribute:#{::Regexp.last_match(2)}:#{::Regexp.last_match(1)}"
|
|
271
|
+
when /\AThe namespace ([^\s]{1,200}) is not permitted for svg elements\.?\z/
|
|
272
|
+
# Map SvgConform namespace restriction to svgcheck pattern
|
|
273
|
+
"namespace_violation:element:#{normalize_namespace(::Regexp.last_match(1))}"
|
|
274
|
+
when /\AviewBox attribute must contain four numeric values/i
|
|
275
|
+
# Map SvgConform viewBox format validation (more strict than svgcheck)
|
|
276
|
+
"viewbox_format_error"
|
|
277
|
+
|
|
278
|
+
# Everything else maps to "other" for non-svgcheck messages
|
|
279
|
+
else
|
|
280
|
+
"other:#{normalize_message(message)}"
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# Compare validity semantically
|
|
285
|
+
def self.compare_validity(svg_conform_valid, svgcheck_valid)
|
|
286
|
+
{
|
|
287
|
+
svg_conform: svg_conform_valid,
|
|
288
|
+
svgcheck: svgcheck_valid,
|
|
289
|
+
match: svg_conform_valid == svgcheck_valid,
|
|
290
|
+
semantic_match: true, # Validity is always semantic
|
|
291
|
+
}
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Compare requirement coverage
|
|
295
|
+
def self.compare_requirement_coverage(svg_conform_reqs, svgcheck_reqs)
|
|
296
|
+
all_requirements = (svg_conform_reqs.keys + svgcheck_reqs.keys).uniq
|
|
297
|
+
|
|
298
|
+
coverage = {}
|
|
299
|
+
all_requirements.each do |req|
|
|
300
|
+
svg_count = svg_conform_reqs[req] || 0
|
|
301
|
+
svgcheck_count = svgcheck_reqs[req] || 0
|
|
302
|
+
|
|
303
|
+
coverage[req] = {
|
|
304
|
+
svg_conform: svg_count,
|
|
305
|
+
svgcheck: svgcheck_count,
|
|
306
|
+
exact_match: svg_count == svgcheck_count,
|
|
307
|
+
coverage_ratio: if svgcheck_count.positive?
|
|
308
|
+
svg_count.to_f / svgcheck_count
|
|
309
|
+
else
|
|
310
|
+
(svg_count.positive? ? Float::INFINITY : 1.0)
|
|
311
|
+
end,
|
|
312
|
+
}
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
coverage
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# Compare semantic issues
|
|
319
|
+
def self.compare_semantic_issues(svg_conform_issues, svgcheck_issues,
|
|
320
|
+
repair_mode = false, _mixed_mode = false)
|
|
321
|
+
all_semantic_keys = (svg_conform_issues.keys + svgcheck_issues.keys).uniq
|
|
322
|
+
|
|
323
|
+
comparison = {}
|
|
324
|
+
all_semantic_keys.each do |key|
|
|
325
|
+
svg_count = svg_conform_issues[key] || 0
|
|
326
|
+
svgcheck_count = svgcheck_issues[key] || 0
|
|
327
|
+
|
|
328
|
+
# In repair mode, treat informational messages differently
|
|
329
|
+
# Check if this is a repair notification by looking at the semantic key pattern
|
|
330
|
+
is_repair_notification = repair_mode && (
|
|
331
|
+
key.start_with?("style_promotion:", "informative:") ||
|
|
332
|
+
key.include?("replaced with") ||
|
|
333
|
+
key.start_with?("invalid_attribute_value:") # These are repair notifications in repair mode
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
comparison[key] = if is_repair_notification
|
|
337
|
+
# Repair notifications are informational, not validation errors
|
|
338
|
+
{
|
|
339
|
+
svg_conform: svg_count,
|
|
340
|
+
svgcheck: svgcheck_count,
|
|
341
|
+
semantic_match: true, # Always consider repair notifications as matching
|
|
342
|
+
coverage: true,
|
|
343
|
+
repair_notification: true,
|
|
344
|
+
}
|
|
345
|
+
else
|
|
346
|
+
{
|
|
347
|
+
svg_conform: svg_count,
|
|
348
|
+
svgcheck: svgcheck_count,
|
|
349
|
+
semantic_match: svg_count == svgcheck_count,
|
|
350
|
+
coverage: svgcheck_count.positive? ? (svg_count >= svgcheck_count) : svg_count.zero?,
|
|
351
|
+
repair_notification: false,
|
|
352
|
+
}
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
comparison
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
# Create detailed mapping between issues
|
|
360
|
+
def self.create_detailed_mapping(svg_conform_issues, svgcheck_issues,
|
|
361
|
+
repair_mode = false, _mixed_mode = false)
|
|
362
|
+
# In repair mode, separate validation issues from repair notifications
|
|
363
|
+
if repair_mode
|
|
364
|
+
svg_conform_validation = svg_conform_issues.reject do |k, _|
|
|
365
|
+
k.start_with?("style_promotion:", "informative:",
|
|
366
|
+
"invalid_attribute_value:")
|
|
367
|
+
end
|
|
368
|
+
svgcheck_validation = svgcheck_issues.reject do |k, _|
|
|
369
|
+
k.start_with?("style_promotion:",
|
|
370
|
+
"informative:") || k.include?("replaced with") || k.start_with?("invalid_attribute_value:")
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
svg_conform_notifications = svg_conform_issues.select do |k, _|
|
|
374
|
+
k.start_with?("style_promotion:", "informative:",
|
|
375
|
+
"invalid_attribute_value:")
|
|
376
|
+
end
|
|
377
|
+
svgcheck_notifications = svgcheck_issues.select do |k, _|
|
|
378
|
+
k.start_with?("style_promotion:",
|
|
379
|
+
"informative:") || k.include?("replaced with") || k.start_with?("invalid_attribute_value:")
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
{
|
|
383
|
+
svg_conform_only: svg_conform_validation.keys - svgcheck_validation.keys,
|
|
384
|
+
svgcheck_only: svgcheck_validation.keys - svg_conform_validation.keys,
|
|
385
|
+
common: svg_conform_validation.keys & svgcheck_validation.keys,
|
|
386
|
+
total_svg_conform: svg_conform_validation.values.sum,
|
|
387
|
+
total_svgcheck: svgcheck_validation.values.sum,
|
|
388
|
+
repair_notifications: {
|
|
389
|
+
svg_conform: svg_conform_notifications.values.sum,
|
|
390
|
+
svgcheck: svgcheck_notifications.values.sum,
|
|
391
|
+
},
|
|
392
|
+
}
|
|
393
|
+
else
|
|
394
|
+
{
|
|
395
|
+
svg_conform_only: svg_conform_issues.keys - svgcheck_issues.keys,
|
|
396
|
+
svgcheck_only: svgcheck_issues.keys - svg_conform_issues.keys,
|
|
397
|
+
common: svg_conform_issues.keys & svgcheck_issues.keys,
|
|
398
|
+
total_svg_conform: svg_conform_issues.values.sum,
|
|
399
|
+
total_svgcheck: svgcheck_issues.values.sum,
|
|
400
|
+
}
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
# Calculate compatibility score
|
|
405
|
+
def self.calculate_compatibility_score(comparison)
|
|
406
|
+
# Handle mixed mode scenarios differently
|
|
407
|
+
return calculate_mixed_mode_compatibility_score(comparison) if comparison[:mixed_mode]
|
|
408
|
+
|
|
409
|
+
validity_score = comparison[:overall_validity][:match] ? 1.0 : 0.0
|
|
410
|
+
|
|
411
|
+
# Separate validation issues from informational issues
|
|
412
|
+
validation_issues = {}
|
|
413
|
+
informational_issues = {}
|
|
414
|
+
|
|
415
|
+
comparison[:semantic_issues].each do |key, issue|
|
|
416
|
+
if key.start_with?("style_promotion:", "informative:")
|
|
417
|
+
informational_issues[key] = issue
|
|
418
|
+
else
|
|
419
|
+
validation_issues[key] = issue
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
# Calculate semantic issues score for validation issues only
|
|
424
|
+
if validation_issues.empty?
|
|
425
|
+
semantic_score = 1.0
|
|
426
|
+
else
|
|
427
|
+
semantic_scores = validation_issues.values.map do |issue|
|
|
428
|
+
issue[:semantic_match] ? 1.0 : 0.0
|
|
429
|
+
end
|
|
430
|
+
semantic_score = semantic_scores.sum / semantic_scores.length
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
# Calculate coverage score excluding informational issues
|
|
434
|
+
total_svg_conform = comparison[:detailed_mapping][:total_svg_conform]
|
|
435
|
+
total_svgcheck = comparison[:detailed_mapping][:total_svgcheck]
|
|
436
|
+
|
|
437
|
+
# Subtract informational issues from both totals
|
|
438
|
+
informational_svg_conform = informational_issues.values.sum do |issue|
|
|
439
|
+
issue[:svg_conform]
|
|
440
|
+
end
|
|
441
|
+
informational_svgcheck = informational_issues.values.sum do |issue|
|
|
442
|
+
issue[:svgcheck]
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
validation_svg_conform_total = total_svg_conform - informational_svg_conform
|
|
446
|
+
validation_svgcheck_total = total_svgcheck - informational_svgcheck
|
|
447
|
+
|
|
448
|
+
# Coverage score: how well do we cover the validation issues?
|
|
449
|
+
# If SvgConform finds more issues than svgcheck, that's not a penalty
|
|
450
|
+
coverage_score = if validation_svgcheck_total.positive?
|
|
451
|
+
# Standard case: measure how much of svgcheck we cover
|
|
452
|
+
[
|
|
453
|
+
validation_svg_conform_total.to_f / validation_svgcheck_total, 1.0
|
|
454
|
+
].min
|
|
455
|
+
elsif validation_svg_conform_total.positive?
|
|
456
|
+
# SvgConform finds issues but svgcheck doesn't - this is fine (more strict)
|
|
457
|
+
1.0
|
|
458
|
+
else
|
|
459
|
+
# Both find no validation issues - perfect
|
|
460
|
+
1.0
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
# Prioritize semantic matching over requirement categorization
|
|
464
|
+
# Validity: 20%, Semantic Issues: 60%, Coverage: 20%
|
|
465
|
+
(validity_score * 0.2 + semantic_score * 0.6 + coverage_score * 0.2).round(3)
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
# Calculate compatibility score for mixed mode scenarios
|
|
469
|
+
def self.calculate_mixed_mode_compatibility_score(comparison)
|
|
470
|
+
# In mixed mode, one tool succeeded and one failed
|
|
471
|
+
# Focus on complementary validation rather than exact overlap
|
|
472
|
+
|
|
473
|
+
# Separate validation issues from informational issues
|
|
474
|
+
validation_issues = {}
|
|
475
|
+
informational_issues = {}
|
|
476
|
+
|
|
477
|
+
comparison[:semantic_issues].each do |key, issue|
|
|
478
|
+
if key.start_with?("style_promotion:", "informative:")
|
|
479
|
+
informational_issues[key] = issue
|
|
480
|
+
else
|
|
481
|
+
validation_issues[key] = issue
|
|
482
|
+
end
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
# Calculate semantic overlap score (exact matches)
|
|
486
|
+
if validation_issues.empty?
|
|
487
|
+
semantic_overlap_score = 1.0
|
|
488
|
+
else
|
|
489
|
+
semantic_matches = validation_issues.values.count do |issue|
|
|
490
|
+
issue[:semantic_match]
|
|
491
|
+
end
|
|
492
|
+
total_unique_issues = validation_issues.size
|
|
493
|
+
semantic_overlap_score = semantic_matches.to_f / total_unique_issues
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
# Calculate complementary validation score with improved logic
|
|
497
|
+
# Give significant credit when tools detect different but valid issues
|
|
498
|
+
total_svg_conform = comparison[:detailed_mapping][:total_svg_conform]
|
|
499
|
+
total_svgcheck = comparison[:detailed_mapping][:total_svgcheck]
|
|
500
|
+
|
|
501
|
+
# Subtract informational issues from both totals
|
|
502
|
+
informational_svg_conform = informational_issues.values.sum do |issue|
|
|
503
|
+
issue[:svg_conform]
|
|
504
|
+
end
|
|
505
|
+
informational_svgcheck = informational_issues.values.sum do |issue|
|
|
506
|
+
issue[:svgcheck]
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
validation_svg_conform_total = total_svg_conform - informational_svg_conform
|
|
510
|
+
validation_svgcheck_total = total_svgcheck - informational_svgcheck
|
|
511
|
+
|
|
512
|
+
# Count common issues (both tools detected)
|
|
513
|
+
common_issues = comparison[:detailed_mapping][:common].size
|
|
514
|
+
|
|
515
|
+
# Enhanced complementary validation scoring
|
|
516
|
+
# Give high credit when both tools find issues, even if different types
|
|
517
|
+
complementary_score = if validation_svg_conform_total.positive? && validation_svgcheck_total.positive?
|
|
518
|
+
# Both tools found validation issues - excellent complementary coverage
|
|
519
|
+
# Base score of 0.8, with bonus for common issues
|
|
520
|
+
base_score = 0.8
|
|
521
|
+
common_bonus = common_issues.positive? ? 0.15 : 0.0
|
|
522
|
+
[base_score + common_bonus, 1.0].min
|
|
523
|
+
elsif validation_svg_conform_total.positive? || validation_svgcheck_total.positive?
|
|
524
|
+
# One tool found issues - partial coverage
|
|
525
|
+
0.7
|
|
526
|
+
else
|
|
527
|
+
# Neither found validation issues - perfect (but unlikely in mixed mode)
|
|
528
|
+
1.0
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
# Mixed mode remediation success bonus - enhanced
|
|
532
|
+
# In mixed mode, successful remediation by either tool is valuable
|
|
533
|
+
remediation_success_bonus = if comparison[:overall_validity][:svg_conform] || comparison[:overall_validity][:svgcheck]
|
|
534
|
+
# One tool successfully remediated the file
|
|
535
|
+
# Higher bonus if both tools found issues but one succeeded
|
|
536
|
+
if validation_svg_conform_total.positive? && validation_svgcheck_total.positive?
|
|
537
|
+
0.5 # Both found issues, one succeeded - excellent
|
|
538
|
+
else
|
|
539
|
+
0.4 # Standard remediation success
|
|
540
|
+
end
|
|
541
|
+
else
|
|
542
|
+
# Neither tool achieved validity (shouldn't happen in mixed mode)
|
|
543
|
+
0.0
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
# Enhanced mixed mode scoring with higher weight on complementary validation
|
|
547
|
+
# Semantic Overlap: 25%, Complementary Validation: 50%, Remediation Success: 25%
|
|
548
|
+
# This better rewards comprehensive validation coverage
|
|
549
|
+
(semantic_overlap_score * 0.25 + complementary_score * 0.5 + remediation_success_bonus * 0.25).round(3)
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
# Parse SVG safely
|
|
553
|
+
def self.parse_svg_safely(content)
|
|
554
|
+
return nil if content.nil? || content.strip.empty?
|
|
555
|
+
|
|
556
|
+
Nokogiri::XML(content, &:strict)
|
|
557
|
+
rescue Nokogiri::XML::SyntaxError
|
|
558
|
+
nil
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
# Extract SVG features for semantic comparison
|
|
562
|
+
def self.extract_svg_features(doc)
|
|
563
|
+
{
|
|
564
|
+
structure: extract_structure(doc),
|
|
565
|
+
attributes: extract_attributes(doc),
|
|
566
|
+
content: extract_content(doc),
|
|
567
|
+
namespaces: extract_namespaces(doc),
|
|
568
|
+
styles: extract_styles(doc),
|
|
569
|
+
}
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
# Extract document structure
|
|
573
|
+
def self.extract_structure(doc)
|
|
574
|
+
elements = doc.xpath("//*").map(&:name).uniq.sort
|
|
575
|
+
hierarchy = extract_hierarchy(doc.root) if doc.root
|
|
576
|
+
|
|
577
|
+
{
|
|
578
|
+
elements: elements,
|
|
579
|
+
hierarchy: hierarchy,
|
|
580
|
+
element_count: doc.xpath("//*").length,
|
|
581
|
+
}
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
# Extract element hierarchy
|
|
585
|
+
def self.extract_hierarchy(element, depth = 0)
|
|
586
|
+
return nil if depth > 10 # Prevent infinite recursion
|
|
587
|
+
|
|
588
|
+
{
|
|
589
|
+
name: element.name,
|
|
590
|
+
children: element.children.select(&:element?).map do |child|
|
|
591
|
+
extract_hierarchy(child, depth + 1)
|
|
592
|
+
end,
|
|
593
|
+
}
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
# Extract attributes
|
|
597
|
+
def self.extract_attributes(doc)
|
|
598
|
+
attributes = {}
|
|
599
|
+
|
|
600
|
+
doc.xpath("//*").each do |element|
|
|
601
|
+
element.attributes.each do |name, attr|
|
|
602
|
+
key = "#{element.name}@#{name}"
|
|
603
|
+
attributes[key] ||= []
|
|
604
|
+
attributes[key] << normalize_attribute_value(attr.value)
|
|
605
|
+
end
|
|
606
|
+
end
|
|
607
|
+
|
|
608
|
+
attributes.transform_values(&:uniq)
|
|
609
|
+
end
|
|
610
|
+
|
|
611
|
+
# Extract text content
|
|
612
|
+
def self.extract_content(doc)
|
|
613
|
+
{
|
|
614
|
+
text_nodes: doc.xpath("//text()").map(&:content).reject(&:empty?),
|
|
615
|
+
cdata_sections: doc.xpath("//comment()").map(&:content),
|
|
616
|
+
}
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
# Extract namespaces
|
|
620
|
+
def self.extract_namespaces(doc)
|
|
621
|
+
namespaces = {}
|
|
622
|
+
|
|
623
|
+
doc.xpath("//*").each do |element|
|
|
624
|
+
element.namespace_definitions.each do |ns|
|
|
625
|
+
namespaces[ns.prefix || "default"] = ns.href
|
|
626
|
+
end
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
namespaces
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
# Extract style information
|
|
633
|
+
def self.extract_styles(doc)
|
|
634
|
+
styles = {}
|
|
635
|
+
|
|
636
|
+
doc.xpath("//*[@style]").each do |element|
|
|
637
|
+
style_attr = element["style"]
|
|
638
|
+
parsed_styles = parse_style_attribute(style_attr)
|
|
639
|
+
styles[element.name] ||= []
|
|
640
|
+
styles[element.name] << parsed_styles
|
|
641
|
+
end
|
|
642
|
+
|
|
643
|
+
styles
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
# Parse style attribute
|
|
647
|
+
def self.parse_style_attribute(style_str)
|
|
648
|
+
return {} if style_str.nil? || style_str.strip.empty?
|
|
649
|
+
|
|
650
|
+
styles = {}
|
|
651
|
+
style_str.split(";").each do |declaration|
|
|
652
|
+
next if declaration.strip.empty?
|
|
653
|
+
|
|
654
|
+
property, value = declaration.split(":", 2)
|
|
655
|
+
next unless property && value
|
|
656
|
+
|
|
657
|
+
styles[property.strip] = normalize_style_value(value.strip)
|
|
658
|
+
end
|
|
659
|
+
|
|
660
|
+
styles
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
# Compare structure
|
|
664
|
+
def self.compare_structure(struct1, struct2)
|
|
665
|
+
{
|
|
666
|
+
elements_match: struct1[:elements] == struct2[:elements],
|
|
667
|
+
hierarchy_match: compare_hierarchy(struct1[:hierarchy],
|
|
668
|
+
struct2[:hierarchy]),
|
|
669
|
+
element_count_match: struct1[:element_count] == struct2[:element_count],
|
|
670
|
+
}
|
|
671
|
+
end
|
|
672
|
+
|
|
673
|
+
# Compare hierarchy
|
|
674
|
+
def self.compare_hierarchy(hier1, hier2)
|
|
675
|
+
return true if hier1.nil? && hier2.nil?
|
|
676
|
+
return false if hier1.nil? || hier2.nil?
|
|
677
|
+
|
|
678
|
+
return false unless hier1[:name] == hier2[:name]
|
|
679
|
+
return false unless hier1[:children].length == hier2[:children].length
|
|
680
|
+
|
|
681
|
+
hier1[:children].zip(hier2[:children]).all? do |child1, child2|
|
|
682
|
+
compare_hierarchy(child1, child2)
|
|
683
|
+
end
|
|
684
|
+
end
|
|
685
|
+
|
|
686
|
+
# Compare attributes
|
|
687
|
+
def self.compare_attributes(attrs1, attrs2)
|
|
688
|
+
all_keys = (attrs1.keys + attrs2.keys).uniq
|
|
689
|
+
|
|
690
|
+
matches = all_keys.map do |key|
|
|
691
|
+
values1 = attrs1[key] || []
|
|
692
|
+
values2 = attrs2[key] || []
|
|
693
|
+
values1.sort == values2.sort
|
|
694
|
+
end
|
|
695
|
+
|
|
696
|
+
{
|
|
697
|
+
exact_match: matches.all?,
|
|
698
|
+
partial_match_ratio: matches.count(true).to_f / matches.length,
|
|
699
|
+
differing_attributes: all_keys.select.with_index { |_, i| !matches[i] },
|
|
700
|
+
}
|
|
701
|
+
end
|
|
702
|
+
|
|
703
|
+
# Compare content
|
|
704
|
+
def self.compare_content(content1, content2)
|
|
705
|
+
{
|
|
706
|
+
text_match: content1[:text_nodes].sort == content2[:text_nodes].sort,
|
|
707
|
+
cdata_match: content1[:cdata_sections].sort == content2[:cdata_sections].sort,
|
|
708
|
+
}
|
|
709
|
+
end
|
|
710
|
+
|
|
711
|
+
# Compare namespaces
|
|
712
|
+
def self.compare_namespaces(ns1, ns2)
|
|
713
|
+
{
|
|
714
|
+
exact_match: ns1 == ns2,
|
|
715
|
+
common_namespaces: ns1.keys & ns2.keys,
|
|
716
|
+
svg_conform_only: ns1.keys - ns2.keys,
|
|
717
|
+
svgcheck_only: ns2.keys - ns1.keys,
|
|
718
|
+
}
|
|
719
|
+
end
|
|
720
|
+
|
|
721
|
+
# Compare styles
|
|
722
|
+
def self.compare_styles(styles1, styles2)
|
|
723
|
+
all_elements = (styles1.keys + styles2.keys).uniq
|
|
724
|
+
|
|
725
|
+
element_comparisons = {}
|
|
726
|
+
all_elements.each do |element|
|
|
727
|
+
elem_styles1 = styles1[element] || []
|
|
728
|
+
elem_styles2 = styles2[element] || []
|
|
729
|
+
|
|
730
|
+
element_comparisons[element] = {
|
|
731
|
+
count_match: elem_styles1.length == elem_styles2.length,
|
|
732
|
+
semantic_match: compare_style_semantics(elem_styles1, elem_styles2),
|
|
733
|
+
}
|
|
734
|
+
end
|
|
735
|
+
|
|
736
|
+
element_comparisons
|
|
737
|
+
end
|
|
738
|
+
|
|
739
|
+
# Compare style semantics
|
|
740
|
+
def self.compare_style_semantics(styles1, styles2)
|
|
741
|
+
# Flatten and normalize all styles
|
|
742
|
+
flat1 = styles1.flat_map(&:to_a).to_h
|
|
743
|
+
flat2 = styles2.flat_map(&:to_a).to_h
|
|
744
|
+
|
|
745
|
+
# Compare normalized styles
|
|
746
|
+
flat1 == flat2
|
|
747
|
+
end
|
|
748
|
+
|
|
749
|
+
# Calculate semantic equivalence
|
|
750
|
+
def self.calculate_semantic_equivalence(features1, features2)
|
|
751
|
+
structure_score = features1[:structure][:elements] == features2[:structure][:elements] ? 1.0 : 0.0
|
|
752
|
+
namespace_score = features1[:namespaces] == features2[:namespaces] ? 1.0 : 0.0
|
|
753
|
+
content_score = features1[:content] == features2[:content] ? 1.0 : 0.0
|
|
754
|
+
|
|
755
|
+
# Weighted average
|
|
756
|
+
(structure_score * 0.4 + namespace_score * 0.3 + content_score * 0.3).round(3)
|
|
757
|
+
end
|
|
758
|
+
|
|
759
|
+
# Utility methods for normalization
|
|
760
|
+
def self.normalize_namespace(namespace)
|
|
761
|
+
case namespace
|
|
762
|
+
when /inkscape/i
|
|
763
|
+
"inkscape"
|
|
764
|
+
when /sodipodi/i
|
|
765
|
+
"sodipodi"
|
|
766
|
+
when /w3\.org.*svg/i
|
|
767
|
+
"svg"
|
|
768
|
+
else
|
|
769
|
+
namespace
|
|
770
|
+
end
|
|
771
|
+
end
|
|
772
|
+
|
|
773
|
+
def self.normalize_value(value)
|
|
774
|
+
# Normalize common values for semantic comparison
|
|
775
|
+
normalized = value.downcase.strip
|
|
776
|
+
|
|
777
|
+
# Handle array-like values from svgcheck (e.g., "['malformed']" -> "malformed")
|
|
778
|
+
normalized = ::Regexp.last_match(1) if normalized =~ /\A\['([^']{1,200})'\]\z/
|
|
779
|
+
|
|
780
|
+
normalized
|
|
781
|
+
end
|
|
782
|
+
|
|
783
|
+
def self.normalize_color_value(color)
|
|
784
|
+
# Normalize color values to handle different formats
|
|
785
|
+
normalized = color.downcase.strip
|
|
786
|
+
|
|
787
|
+
# Map common color equivalents
|
|
788
|
+
case normalized
|
|
789
|
+
when "white", "#fff", "#ffffff", "rgb(255,255,255)", "rgb(255, 255, 255)", "rgb(100%,100%,100%)", "rgb(100%, 100%, 100%)"
|
|
790
|
+
"white"
|
|
791
|
+
when "black", "#000", "#000000", "rgb(0,0,0)", "rgb(0, 0, 0)", "rgb(0%,0%,0%)", "rgb(0%, 0%, 0%)"
|
|
792
|
+
"black"
|
|
793
|
+
when "red", "#f00", "#ff0000", "rgb(255,0,0)", "rgb(255, 0, 0)", "rgb(100%,0%,0%)", "rgb(100%, 0%, 0%)"
|
|
794
|
+
"red"
|
|
795
|
+
when "green", "#0f0", "#00ff00", "rgb(0,255,0)", "rgb(0, 255, 0)", "rgb(0%,100%,0%)", "rgb(0%, 100%, 0%)"
|
|
796
|
+
"green"
|
|
797
|
+
when "blue", "#00f", "#0000ff", "rgb(0,0,255)", "rgb(0, 0, 255)", "rgb(0%,0%,100%)", "rgb(0%, 0%, 100%)"
|
|
798
|
+
"blue"
|
|
799
|
+
when "grey", "gray"
|
|
800
|
+
"grey"
|
|
801
|
+
else
|
|
802
|
+
# For hex colors, normalize to lowercase without spaces
|
|
803
|
+
if /\A#[0-9a-f]{3,8}\z/i.match?(normalized)
|
|
804
|
+
normalized.downcase
|
|
805
|
+
elsif /\Argb\s*\(/i.match?(normalized)
|
|
806
|
+
# Normalize RGB format by removing spaces
|
|
807
|
+
normalized.gsub(/\s+/, "")
|
|
808
|
+
else
|
|
809
|
+
normalized
|
|
810
|
+
end
|
|
811
|
+
end
|
|
812
|
+
end
|
|
813
|
+
|
|
814
|
+
def self.normalize_message(message)
|
|
815
|
+
# Extract key semantic components from message
|
|
816
|
+
message.downcase.gsub(/['":]/, "").strip
|
|
817
|
+
end
|
|
818
|
+
|
|
819
|
+
def self.normalize_attribute_value(value)
|
|
820
|
+
# Normalize attribute values for comparison
|
|
821
|
+
value.strip.downcase
|
|
822
|
+
end
|
|
823
|
+
|
|
824
|
+
def self.normalize_style_value(value)
|
|
825
|
+
# Normalize style values for comparison
|
|
826
|
+
value.strip.downcase
|
|
827
|
+
end
|
|
828
|
+
end
|
|
829
|
+
end
|