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,226 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_remediation"
|
|
4
|
+
|
|
5
|
+
module SvgConform
|
|
6
|
+
module Remediations
|
|
7
|
+
# Remediation action for color-related issues that matches svgcheck behavior
|
|
8
|
+
class ColorRemediation < BaseRemediation
|
|
9
|
+
attribute :type, :string, default: -> { "ColorRemediation" }
|
|
10
|
+
|
|
11
|
+
DEFAULT_COLOR_THRESHOLD = 764 # Default threshold if not specified in requirement
|
|
12
|
+
COLOR_DEFAULT = "black"
|
|
13
|
+
|
|
14
|
+
COLOR_MAP = {
|
|
15
|
+
"rgb(0,0,0)" => "black",
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
def apply(document, context)
|
|
19
|
+
# Store the context for use in other methods
|
|
20
|
+
@context = context
|
|
21
|
+
@failed_requirements = context[:failed_requirements] if context.is_a?(Hash)
|
|
22
|
+
|
|
23
|
+
changes = []
|
|
24
|
+
|
|
25
|
+
# Find all elements with color attributes
|
|
26
|
+
document.traverse do |node|
|
|
27
|
+
next unless node.respond_to?(:name) && node.name
|
|
28
|
+
|
|
29
|
+
changes.concat(fix_color_attributes(node))
|
|
30
|
+
changes.concat(fix_style_colors(node))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
changes
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def fix_color_attributes(node)
|
|
39
|
+
changes = []
|
|
40
|
+
color_attributes = %w[fill stroke color stop-color flood-color
|
|
41
|
+
lighting-color]
|
|
42
|
+
|
|
43
|
+
color_attributes.each do |attr_name|
|
|
44
|
+
value = get_attribute(node, attr_name)
|
|
45
|
+
next if value.nil? || value.empty?
|
|
46
|
+
|
|
47
|
+
new_color = convert_color_value(value)
|
|
48
|
+
next unless new_color != value
|
|
49
|
+
|
|
50
|
+
set_attribute(node, attr_name, new_color)
|
|
51
|
+
changes << log_change(
|
|
52
|
+
:attribute_modified,
|
|
53
|
+
"The attribute '#{attr_name}' does not allow the value '#{value}', replaced with '#{new_color}'",
|
|
54
|
+
node,
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
changes
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def fix_style_colors(node)
|
|
62
|
+
changes = []
|
|
63
|
+
style_value = get_attribute(node, "style")
|
|
64
|
+
return changes unless style_value
|
|
65
|
+
|
|
66
|
+
styles = parse_style(style_value)
|
|
67
|
+
color_properties = %w[fill stroke color stop-color flood-color
|
|
68
|
+
lighting-color]
|
|
69
|
+
style_changed = false
|
|
70
|
+
|
|
71
|
+
color_properties.each do |prop|
|
|
72
|
+
value = styles[prop]
|
|
73
|
+
next if value.nil? || value.empty?
|
|
74
|
+
|
|
75
|
+
new_color = convert_color_value(value)
|
|
76
|
+
if new_color != value
|
|
77
|
+
styles[prop] = new_color
|
|
78
|
+
style_changed = true
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
if style_changed
|
|
83
|
+
new_style = build_style(styles)
|
|
84
|
+
set_attribute(node, "style", new_style)
|
|
85
|
+
changes << log_change(
|
|
86
|
+
:attribute_modified,
|
|
87
|
+
"Updated style colors",
|
|
88
|
+
node,
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
changes
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def convert_color_value(color_value)
|
|
96
|
+
v = color_value.strip
|
|
97
|
+
|
|
98
|
+
# Check if it's already a valid color according to profile
|
|
99
|
+
return color_value if valid_color?(v)
|
|
100
|
+
|
|
101
|
+
# Handle grey/gray conversion - check if they're allowed, otherwise convert to default
|
|
102
|
+
if %w[grey gray GREY GRAY].include?(v)
|
|
103
|
+
return v if valid_color?(v) # If grey is allowed in profile, keep it
|
|
104
|
+
|
|
105
|
+
return COLOR_DEFAULT # Otherwise convert to black to match svgcheck
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Check color map first
|
|
109
|
+
return COLOR_MAP[v] if COLOR_MAP.key?(v)
|
|
110
|
+
|
|
111
|
+
# Handle different color formats
|
|
112
|
+
if v.start_with?("rgb(") && v.end_with?(")")
|
|
113
|
+
convert_rgb_color(v)
|
|
114
|
+
elsif v.start_with?("#")
|
|
115
|
+
convert_hex_color(v)
|
|
116
|
+
else
|
|
117
|
+
# Unknown color, convert to default
|
|
118
|
+
COLOR_DEFAULT
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def valid_color?(color)
|
|
123
|
+
# Get allowed colors from the color restrictions requirement in failed requirements
|
|
124
|
+
color_requirement = find_color_requirement
|
|
125
|
+
if color_requirement.respond_to?(:allowed_colors)
|
|
126
|
+
allowed_colors = color_requirement.allowed_colors
|
|
127
|
+
# Check for exact case-sensitive matches first
|
|
128
|
+
return true if allowed_colors.include?(color)
|
|
129
|
+
|
|
130
|
+
# Check for case-insensitive hex color matches
|
|
131
|
+
if color.match?(/^#[0-9a-fA-F]{6}$/)
|
|
132
|
+
hex_allowed = allowed_colors.select do |c|
|
|
133
|
+
c.match?(/^#[0-9a-fA-F]{6}$/)
|
|
134
|
+
end
|
|
135
|
+
return hex_allowed.any? do |allowed|
|
|
136
|
+
allowed.downcase == color.downcase
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
return false
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Fallback to default validation
|
|
144
|
+
%w[black white #ffffff #000000 none inherit
|
|
145
|
+
currentcolor].include?(color.downcase)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def find_color_requirement
|
|
149
|
+
return nil unless @failed_requirements
|
|
150
|
+
|
|
151
|
+
@failed_requirements.find do |failure|
|
|
152
|
+
requirement_id = failure.requirement_id || failure.rule&.id
|
|
153
|
+
requirement_id == "color_restrictions"
|
|
154
|
+
end&.rule
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def convert_rgb_color(rgb_string)
|
|
158
|
+
# Extract RGB values from rgb(r,g,b) or rgb(r%,g%,b%)
|
|
159
|
+
match = rgb_string.match(/rgb\(([^)]+)\)/)
|
|
160
|
+
return COLOR_DEFAULT unless match
|
|
161
|
+
|
|
162
|
+
values = match[1].split(",").map(&:strip)
|
|
163
|
+
return COLOR_DEFAULT unless values.length == 3
|
|
164
|
+
|
|
165
|
+
shade = if values.first.include?("%")
|
|
166
|
+
# Percentage values
|
|
167
|
+
values.sum { |v| v.gsub("%", "").to_f * 255 / 100 }
|
|
168
|
+
else
|
|
169
|
+
# Integer values
|
|
170
|
+
values.sum(&:to_i)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
shade > color_threshold ? "white" : COLOR_DEFAULT
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def convert_hex_color(hex_string)
|
|
177
|
+
if hex_string.length == 7 # #rrggbb
|
|
178
|
+
r = hex_string[1..2].to_i(16)
|
|
179
|
+
g = hex_string[3..4].to_i(16)
|
|
180
|
+
b = hex_string[5..6].to_i(16)
|
|
181
|
+
shade = r + g + b
|
|
182
|
+
elsif hex_string.length == 4 # #rgb
|
|
183
|
+
r = hex_string[1].to_i(16) * 17 # Convert single digit to double
|
|
184
|
+
g = hex_string[2].to_i(16) * 17
|
|
185
|
+
b = hex_string[3].to_i(16) * 17
|
|
186
|
+
shade = r + g + b
|
|
187
|
+
else
|
|
188
|
+
return COLOR_DEFAULT
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
shade > color_threshold ? "white" : COLOR_DEFAULT
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def color_threshold
|
|
195
|
+
# Get threshold from the color requirement configuration, fall back to default
|
|
196
|
+
color_requirement = find_color_requirement
|
|
197
|
+
if color_requirement.respond_to?(:black_and_white_threshold) && color_requirement.black_and_white_threshold
|
|
198
|
+
color_requirement.black_and_white_threshold
|
|
199
|
+
else
|
|
200
|
+
DEFAULT_COLOR_THRESHOLD
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def parse_style(style_value)
|
|
205
|
+
return {} if style_value.nil? || style_value.empty?
|
|
206
|
+
|
|
207
|
+
styles = {}
|
|
208
|
+
style_value.split(";").each do |declaration|
|
|
209
|
+
next if declaration.strip.empty?
|
|
210
|
+
|
|
211
|
+
parts = declaration.split(":", 2)
|
|
212
|
+
next unless parts.size == 2
|
|
213
|
+
|
|
214
|
+
property = parts[0].strip
|
|
215
|
+
value = parts[1].strip
|
|
216
|
+
styles[property] = value unless property.empty? || value.empty?
|
|
217
|
+
end
|
|
218
|
+
styles
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def build_style(styles)
|
|
222
|
+
styles.map { |prop, value| "#{prop}:#{value}" }.join(";")
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_remediation"
|
|
4
|
+
|
|
5
|
+
module SvgConform
|
|
6
|
+
module Remediations
|
|
7
|
+
# Remediation for embedding external fonts
|
|
8
|
+
# NOTE: This is a placeholder - actual font embedding would require
|
|
9
|
+
# fetching external fonts and converting them to data URIs
|
|
10
|
+
class FontEmbeddingRemediation < BaseRemediation
|
|
11
|
+
attribute :type, :string, default: -> { "FontEmbeddingRemediation" }
|
|
12
|
+
|
|
13
|
+
yaml do
|
|
14
|
+
map "id", to: :id
|
|
15
|
+
map "description", to: :description
|
|
16
|
+
map "type", to: :type
|
|
17
|
+
map "targets", to: :targets
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def apply(document, _context)
|
|
21
|
+
changes = []
|
|
22
|
+
|
|
23
|
+
# Find all style elements with @font-face rules
|
|
24
|
+
document.xpath("//style").each do |style_node|
|
|
25
|
+
content = style_node.text || ""
|
|
26
|
+
next unless content.include?("@font-face")
|
|
27
|
+
|
|
28
|
+
modified_content = embed_fonts_in_css(content, changes)
|
|
29
|
+
if modified_content != content
|
|
30
|
+
style_node.content = modified_content
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Find font-face elements with external references
|
|
35
|
+
document.xpath("//font-face-src/font-face-uri").each do |uri_node|
|
|
36
|
+
href = get_attribute(uri_node,
|
|
37
|
+
"xlink:href") || get_attribute(uri_node, "href")
|
|
38
|
+
next unless href && !href.start_with?("data:", "#")
|
|
39
|
+
|
|
40
|
+
# Convert external font to data URI
|
|
41
|
+
embedded_uri = fetch_and_embed_font(href)
|
|
42
|
+
if embedded_uri
|
|
43
|
+
set_attribute(uri_node, "xlink:href", embedded_uri)
|
|
44
|
+
changes << {
|
|
45
|
+
type: "font_embedded",
|
|
46
|
+
element: "font-face-uri",
|
|
47
|
+
message: "Embedded external font: #{href}",
|
|
48
|
+
}
|
|
49
|
+
else
|
|
50
|
+
changes << {
|
|
51
|
+
type: "font_embedding_failed",
|
|
52
|
+
element: "font-face-uri",
|
|
53
|
+
message: "Failed to embed font: #{href}",
|
|
54
|
+
}
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
changes
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def can_remediate?
|
|
62
|
+
true
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def embed_fonts_in_css(css_content, changes)
|
|
68
|
+
# Match @font-face blocks and replace external URLs with data URIs
|
|
69
|
+
css_content.gsub(/@font-face\s*\{([^}]+)\}/m) do |_font_face_block|
|
|
70
|
+
font_face_content = ::Regexp.last_match(1)
|
|
71
|
+
|
|
72
|
+
# Find src: url(...) declarations
|
|
73
|
+
modified_content = font_face_content.gsub(/src\s*:\s*url\s*\(\s*['"]?([^'")\s]+)['"]?\s*\)/i) do |url_match|
|
|
74
|
+
url = ::Regexp.last_match(1)
|
|
75
|
+
|
|
76
|
+
# Skip if already embedded or internal reference
|
|
77
|
+
next url_match if url.start_with?("data:", "#")
|
|
78
|
+
|
|
79
|
+
# Fetch and embed the font
|
|
80
|
+
embedded_uri = fetch_and_embed_font(url)
|
|
81
|
+
if embedded_uri
|
|
82
|
+
changes << {
|
|
83
|
+
type: "font_embedded",
|
|
84
|
+
element: "style",
|
|
85
|
+
message: "Embedded external font: #{url}",
|
|
86
|
+
}
|
|
87
|
+
"src: url(#{embedded_uri})"
|
|
88
|
+
else
|
|
89
|
+
changes << {
|
|
90
|
+
type: "font_embedding_failed",
|
|
91
|
+
element: "style",
|
|
92
|
+
message: "Failed to embed font: #{url}",
|
|
93
|
+
}
|
|
94
|
+
url_match
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
"@font-face {#{modified_content}}"
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def fetch_and_embed_font(url)
|
|
103
|
+
require "net/http"
|
|
104
|
+
require "uri"
|
|
105
|
+
require "base64"
|
|
106
|
+
|
|
107
|
+
# Parse URL
|
|
108
|
+
uri = URI.parse(url)
|
|
109
|
+
|
|
110
|
+
# Fetch the font
|
|
111
|
+
response = Net::HTTP.get_response(uri)
|
|
112
|
+
return nil unless response.is_a?(Net::HTTPSuccess)
|
|
113
|
+
|
|
114
|
+
# Get content type or infer from URL
|
|
115
|
+
content_type = response["content-type"] || infer_font_mime_type(url)
|
|
116
|
+
|
|
117
|
+
# Encode as base64
|
|
118
|
+
base64_data = Base64.strict_encode64(response.body)
|
|
119
|
+
|
|
120
|
+
# Return data URI
|
|
121
|
+
"data:#{content_type};base64,#{base64_data}"
|
|
122
|
+
rescue StandardError => e
|
|
123
|
+
warn "Failed to fetch font #{url}: #{e.message}"
|
|
124
|
+
nil
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def infer_font_mime_type(url)
|
|
128
|
+
case url.downcase
|
|
129
|
+
when /\.woff2$/
|
|
130
|
+
"font/woff2"
|
|
131
|
+
when /\.woff$/
|
|
132
|
+
"font/woff"
|
|
133
|
+
when /\.ttf$/
|
|
134
|
+
"font/ttf"
|
|
135
|
+
when /\.otf$/
|
|
136
|
+
"font/otf"
|
|
137
|
+
when /\.eot$/
|
|
138
|
+
"application/vnd.ms-fontobject"
|
|
139
|
+
else
|
|
140
|
+
"font/woff2" # Default to woff2
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_remediation"
|
|
4
|
+
|
|
5
|
+
module SvgConform
|
|
6
|
+
module Remediations
|
|
7
|
+
# Remediation action for font-related issues that matches svgcheck behavior
|
|
8
|
+
class FontRemediation < BaseRemediation
|
|
9
|
+
attribute :type, :string, default: -> { "FontRemediation" }
|
|
10
|
+
attribute :default_family, :string, default: -> { "sans-serif" }
|
|
11
|
+
attribute :mapping, :hash, default: -> { {} }
|
|
12
|
+
|
|
13
|
+
def apply(document, _context)
|
|
14
|
+
changes = []
|
|
15
|
+
# Find all elements with font-family attributes
|
|
16
|
+
document.traverse do |node|
|
|
17
|
+
next unless node.respond_to?(:name) && node.name
|
|
18
|
+
|
|
19
|
+
changes.concat(fix_font_family_attribute(node, default_family,
|
|
20
|
+
mapping))
|
|
21
|
+
changes.concat(fix_style_font_family(node, default_family, mapping))
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
changes
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def fix_font_family_attribute(node, default_family, mapping)
|
|
30
|
+
changes = []
|
|
31
|
+
font_family = get_attribute(node, "font-family")
|
|
32
|
+
return changes unless font_family
|
|
33
|
+
|
|
34
|
+
new_family = convert_font_family(font_family, default_family, mapping)
|
|
35
|
+
if new_family != font_family
|
|
36
|
+
set_attribute(node, "font-family", new_family)
|
|
37
|
+
changes << log_change(
|
|
38
|
+
:attribute_modified,
|
|
39
|
+
"Changed font-family from '#{font_family}' to '#{new_family}'",
|
|
40
|
+
node,
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
changes
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def fix_style_font_family(node, default_family, mapping)
|
|
48
|
+
changes = []
|
|
49
|
+
style_value = get_attribute(node, "style")
|
|
50
|
+
return changes unless style_value
|
|
51
|
+
|
|
52
|
+
styles = parse_style(style_value)
|
|
53
|
+
font_family_style = styles["font-family"]
|
|
54
|
+
return changes unless font_family_style
|
|
55
|
+
|
|
56
|
+
new_family = convert_font_family(font_family_style, default_family,
|
|
57
|
+
mapping)
|
|
58
|
+
if new_family != font_family_style
|
|
59
|
+
styles["font-family"] = new_family
|
|
60
|
+
new_style = build_style(styles)
|
|
61
|
+
set_attribute(node, "style", new_style)
|
|
62
|
+
changes << log_change(
|
|
63
|
+
:attribute_modified,
|
|
64
|
+
"Updated font-family in style from '#{font_family_style}' to '#{new_family}'",
|
|
65
|
+
node,
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
changes
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def convert_font_family(font_family, default_family, mapping)
|
|
73
|
+
# Parse font family list (can be comma-separated)
|
|
74
|
+
families = font_family.split(",").map(&:strip)
|
|
75
|
+
|
|
76
|
+
# Check if any family is valid according to svgcheck's allowed values
|
|
77
|
+
valid_families = %w[serif sans-serif monospace inherit]
|
|
78
|
+
|
|
79
|
+
converted_families = families.map do |family|
|
|
80
|
+
# Remove quotes if present
|
|
81
|
+
clean_family = family.gsub(/['"]/, "").strip
|
|
82
|
+
|
|
83
|
+
# Check mapping first
|
|
84
|
+
mapped = mapping[clean_family]
|
|
85
|
+
return mapped if mapped
|
|
86
|
+
|
|
87
|
+
# Check if it's already a valid family according to svgcheck
|
|
88
|
+
if valid_families.include?(clean_family.downcase)
|
|
89
|
+
clean_family.downcase
|
|
90
|
+
else
|
|
91
|
+
# svgcheck replaces any invalid font family with 'sans-serif'
|
|
92
|
+
default_family
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Return the first converted family or default
|
|
97
|
+
converted_families.first || default_family
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def parse_style(style_value)
|
|
101
|
+
return {} if style_value.nil? || style_value.empty?
|
|
102
|
+
|
|
103
|
+
styles = {}
|
|
104
|
+
style_value.split(";").each do |declaration|
|
|
105
|
+
next if declaration.strip.empty?
|
|
106
|
+
|
|
107
|
+
parts = declaration.split(":", 2)
|
|
108
|
+
next unless parts.size == 2
|
|
109
|
+
|
|
110
|
+
property = parts[0].strip
|
|
111
|
+
value = parts[1].strip
|
|
112
|
+
styles[property] = value unless property.empty? || value.empty?
|
|
113
|
+
end
|
|
114
|
+
styles
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def build_style(styles)
|
|
118
|
+
styles.map { |prop, value| "#{prop}:#{value}" }.join(";")
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_remediation"
|
|
4
|
+
|
|
5
|
+
module SvgConform
|
|
6
|
+
module Remediations
|
|
7
|
+
# Remediation for embedding external images
|
|
8
|
+
class ImageEmbeddingRemediation < BaseRemediation
|
|
9
|
+
attribute :type, :string, default: -> { "ImageEmbeddingRemediation" }
|
|
10
|
+
|
|
11
|
+
yaml do
|
|
12
|
+
map "id", to: :id
|
|
13
|
+
map "description", to: :description
|
|
14
|
+
map "type", to: :type
|
|
15
|
+
map "targets", to: :targets
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def apply(document, _context)
|
|
19
|
+
changes = []
|
|
20
|
+
|
|
21
|
+
# Find all image elements with external references
|
|
22
|
+
document.xpath("//image").each do |image_node|
|
|
23
|
+
href = get_attribute(image_node,
|
|
24
|
+
"href") || get_attribute(image_node,
|
|
25
|
+
"xlink:href")
|
|
26
|
+
next unless href && !href.start_with?("data:", "#")
|
|
27
|
+
|
|
28
|
+
# Convert external image to data URI
|
|
29
|
+
embedded_uri = fetch_and_embed_image(href)
|
|
30
|
+
if embedded_uri
|
|
31
|
+
# Update both href variants for compatibility
|
|
32
|
+
set_attribute(image_node, "href", embedded_uri)
|
|
33
|
+
if get_attribute(
|
|
34
|
+
image_node, "xlink:href"
|
|
35
|
+
)
|
|
36
|
+
set_attribute(image_node, "xlink:href",
|
|
37
|
+
embedded_uri)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
changes << {
|
|
41
|
+
type: "image_embedded",
|
|
42
|
+
element: "image",
|
|
43
|
+
message: "Embedded external image: #{href}",
|
|
44
|
+
}
|
|
45
|
+
else
|
|
46
|
+
changes << {
|
|
47
|
+
type: "image_embedding_failed",
|
|
48
|
+
element: "image",
|
|
49
|
+
message: "Failed to embed image: #{href}",
|
|
50
|
+
}
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Find style attributes with background images
|
|
55
|
+
document.traverse do |node|
|
|
56
|
+
next unless element?(node)
|
|
57
|
+
|
|
58
|
+
style_value = get_attribute(node, "style")
|
|
59
|
+
next unless style_value&.include?("url(")
|
|
60
|
+
|
|
61
|
+
modified_style = embed_images_in_style(style_value, changes)
|
|
62
|
+
if modified_style != style_value
|
|
63
|
+
set_attribute(node, "style", modified_style)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
changes
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def can_remediate?
|
|
71
|
+
true
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def embed_images_in_style(style_value, changes)
|
|
77
|
+
# SECURITY: Prevent ReDoS by limiting input length and using bounded quantifiers
|
|
78
|
+
# GitHub CodeQL: Regular expression with excessive backtracking
|
|
79
|
+
return style_value if style_value.length > 10_000
|
|
80
|
+
|
|
81
|
+
# Replace url() references with data URIs
|
|
82
|
+
# Use bounded quantifier {1,2000} to prevent exponential backtracking
|
|
83
|
+
style_value.gsub(/url\s*\(\s*['"]?([^'")\s]{1,2000})['"]?\s*\)/i) do |url_match|
|
|
84
|
+
url = ::Regexp.last_match(1)
|
|
85
|
+
|
|
86
|
+
# Skip if already embedded or internal reference
|
|
87
|
+
next url_match if url.start_with?("data:", "#")
|
|
88
|
+
|
|
89
|
+
# Fetch and embed the image
|
|
90
|
+
embedded_uri = fetch_and_embed_image(url)
|
|
91
|
+
if embedded_uri
|
|
92
|
+
changes << {
|
|
93
|
+
type: "image_embedded",
|
|
94
|
+
element: "style",
|
|
95
|
+
message: "Embedded external image: #{url}",
|
|
96
|
+
}
|
|
97
|
+
"url(#{embedded_uri})"
|
|
98
|
+
else
|
|
99
|
+
changes << {
|
|
100
|
+
type: "image_embedding_failed",
|
|
101
|
+
element: "style",
|
|
102
|
+
message: "Failed to embed image: #{url}",
|
|
103
|
+
}
|
|
104
|
+
url_match
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def fetch_and_embed_image(url)
|
|
110
|
+
require "net/http"
|
|
111
|
+
require "uri"
|
|
112
|
+
require "base64"
|
|
113
|
+
|
|
114
|
+
# Parse URL
|
|
115
|
+
uri = URI.parse(url)
|
|
116
|
+
|
|
117
|
+
# Fetch the image
|
|
118
|
+
response = Net::HTTP.get_response(uri)
|
|
119
|
+
return nil unless response.is_a?(Net::HTTPSuccess)
|
|
120
|
+
|
|
121
|
+
# Get content type or infer from URL
|
|
122
|
+
content_type = response["content-type"] || infer_image_mime_type(url)
|
|
123
|
+
|
|
124
|
+
# Encode as base64
|
|
125
|
+
base64_data = Base64.strict_encode64(response.body)
|
|
126
|
+
|
|
127
|
+
# Return data URI
|
|
128
|
+
"data:#{content_type};base64,#{base64_data}"
|
|
129
|
+
rescue StandardError => e
|
|
130
|
+
warn "Failed to fetch image #{url}: #{e.message}"
|
|
131
|
+
nil
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def infer_image_mime_type(url)
|
|
135
|
+
case url.downcase
|
|
136
|
+
when /\.png$/
|
|
137
|
+
"image/png"
|
|
138
|
+
when /\.jpe?g$/
|
|
139
|
+
"image/jpeg"
|
|
140
|
+
when /\.gif$/
|
|
141
|
+
"image/gif"
|
|
142
|
+
when /\.svg$/
|
|
143
|
+
"image/svg+xml"
|
|
144
|
+
when /\.webp$/
|
|
145
|
+
"image/webp"
|
|
146
|
+
when /\.bmp$/
|
|
147
|
+
"image/bmp"
|
|
148
|
+
else
|
|
149
|
+
"image/png" # Default to PNG
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|