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,199 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SvgConform
|
|
4
|
+
module Constants
|
|
5
|
+
# SVG elements allowed in SVG 1.2 RFC (based on RFC 7996 and svgcheck)
|
|
6
|
+
SVG_ELEMENTS = {
|
|
7
|
+
"svg" => %w[version baseProfile width viewBox preserveAspectRatio snapshotTime
|
|
8
|
+
height id role break color-rendering fill-rule],
|
|
9
|
+
"desc" => %w[id role shape-rendering text-rendering buffered-rendering
|
|
10
|
+
visibility],
|
|
11
|
+
"title" => %w[id role shape-rendering text-rendering buffered-rendering
|
|
12
|
+
visibility],
|
|
13
|
+
"path" => %w[d pathLength stroke-miterlimit id role fill style
|
|
14
|
+
transform font-size fill-rule],
|
|
15
|
+
"rect" => %w[x y width height rx ry stroke-miterlimit
|
|
16
|
+
id role fill style transform fill-rule],
|
|
17
|
+
"circle" => %w[cx cy r id role fill style transform fill-rule],
|
|
18
|
+
"line" => %w[x1 y1 x2 y2 id role fill transform fill-rule],
|
|
19
|
+
"ellipse" => %w[cx cy rx ry id role fill style transform fill-rule],
|
|
20
|
+
"polyline" => %w[points id role fill transform fill-rule],
|
|
21
|
+
"polygon" => %w[points id role fill style transform fill-rule],
|
|
22
|
+
"solidColor" => %w[id role fill fill-rule],
|
|
23
|
+
"textArea" => %w[x y width height auto id role fill transform fill-rule],
|
|
24
|
+
"text" => %w[x y rotate id role fill style transform font-size fill-rule],
|
|
25
|
+
"g" => %w[label class id role fill style transform fill-rule visibility],
|
|
26
|
+
"defs" => %w[id role fill fill-rule],
|
|
27
|
+
"use" => %w[x y href id role fill transform fill-rule],
|
|
28
|
+
"a" => %w[id role fill transform fill-rule target],
|
|
29
|
+
"tspan" => %w[x y id role fill fill-rule],
|
|
30
|
+
"tbreak" => %w[id role],
|
|
31
|
+
}.freeze
|
|
32
|
+
|
|
33
|
+
# SVG properties with their allowed values
|
|
34
|
+
SVG_PROPERTIES = {
|
|
35
|
+
"about" => [],
|
|
36
|
+
"base" => [],
|
|
37
|
+
"baseProfile" => [],
|
|
38
|
+
"d" => [],
|
|
39
|
+
"break" => [],
|
|
40
|
+
"class" => [],
|
|
41
|
+
"content" => [],
|
|
42
|
+
"cx" => ["<number>"],
|
|
43
|
+
"cy" => ["<number>"],
|
|
44
|
+
"datatype" => [],
|
|
45
|
+
"height" => ["<number>"],
|
|
46
|
+
"href" => [],
|
|
47
|
+
"id" => [],
|
|
48
|
+
"label" => [],
|
|
49
|
+
"lang" => [],
|
|
50
|
+
"pathLength" => [],
|
|
51
|
+
"points" => [],
|
|
52
|
+
"preserveAspectRatio" => [],
|
|
53
|
+
"property" => [],
|
|
54
|
+
"r" => ["<number>"],
|
|
55
|
+
"rel" => [],
|
|
56
|
+
"resource" => [],
|
|
57
|
+
"rev" => [],
|
|
58
|
+
"role" => [],
|
|
59
|
+
"rotate" => [],
|
|
60
|
+
"rx" => ["<number>"],
|
|
61
|
+
"ry" => ["<number>"],
|
|
62
|
+
"space" => [],
|
|
63
|
+
"snapshotTime" => [],
|
|
64
|
+
"transform" => [],
|
|
65
|
+
"typeof" => [],
|
|
66
|
+
"version" => [],
|
|
67
|
+
"width" => ["<number>"],
|
|
68
|
+
"viewBox" => ["<number>"],
|
|
69
|
+
"x" => ["<number>"],
|
|
70
|
+
"x1" => ["<number>"],
|
|
71
|
+
"x2" => ["<number>"],
|
|
72
|
+
"y" => ["<number>"],
|
|
73
|
+
"y1" => ["<number>"],
|
|
74
|
+
"y2" => ["<number>"],
|
|
75
|
+
|
|
76
|
+
"stroke" => ["none", "<paint>"],
|
|
77
|
+
"stroke-width" => [],
|
|
78
|
+
"stroke-linecap" => %w[butt round square inherit],
|
|
79
|
+
"stroke-linejoin" => %w[miter round bevel inherit],
|
|
80
|
+
"stroke-miterlimit" => [],
|
|
81
|
+
"stroke-dasharray" => [],
|
|
82
|
+
"stroke-dashoffset" => [],
|
|
83
|
+
"stroke-opacity" => [],
|
|
84
|
+
"vector-effect" => %w[non-scaling-stroke none inherit],
|
|
85
|
+
"viewport-fill" => ["none", "currentColor", "inherit", "<color>"],
|
|
86
|
+
|
|
87
|
+
"display" => %w[inline block list-item run-in compact table inline-table
|
|
88
|
+
table-row-group table-header-group table-footer-group
|
|
89
|
+
table-row table-column-group table-column table-cell
|
|
90
|
+
table-caption none inherit],
|
|
91
|
+
"viewport-fill-opacity" => [],
|
|
92
|
+
"visibility" => %w[visible hidden collapse inherit],
|
|
93
|
+
"image-rendering" => %w[auto optimizeSpeed optimizeQuality inherit],
|
|
94
|
+
"color-rendering" => %w[auto optimizeSpeed optimizeQuality inherit],
|
|
95
|
+
"shape-rendering" => %w[auto optimizeSpeed crispEdges geometricPrecision
|
|
96
|
+
inherit],
|
|
97
|
+
"text-rendering" => %w[auto optimizeSpeed optimizeLegibility
|
|
98
|
+
geometricPrecision inherit],
|
|
99
|
+
"buffered-rendering" => %w[auto dynamic static inherit],
|
|
100
|
+
|
|
101
|
+
"solid-opacity" => [],
|
|
102
|
+
"solid-color" => ["currentColor", "inherit", "<color>"],
|
|
103
|
+
"color" => ["currentColor", "inherit", "<color>"],
|
|
104
|
+
|
|
105
|
+
"stop-color" => ["currentColor", "inherit", "<color>"],
|
|
106
|
+
"stop-opacity" => [],
|
|
107
|
+
|
|
108
|
+
"line-increment" => [],
|
|
109
|
+
"text-align" => %w[start end center inherit],
|
|
110
|
+
"display-align" => %w[auto before center after inherit],
|
|
111
|
+
|
|
112
|
+
"font-size" => [],
|
|
113
|
+
"font-family" => %w[serif sans-serif monospace inherit],
|
|
114
|
+
"font-weight" => %w[normal bold bolder lighter inherit 100 200 300 400
|
|
115
|
+
500 600 700 800 900],
|
|
116
|
+
"font-style" => %w[normal italic oblique inherit],
|
|
117
|
+
"font-variant" => %w[normal small-caps inherit],
|
|
118
|
+
"direction" => %w[ltr rtl inherit],
|
|
119
|
+
"unicode-bidi" => %w[normal embed bidi-override inherit],
|
|
120
|
+
"text-anchor" => %w[start middle end inherit],
|
|
121
|
+
"fill" => ["none", "inherit", "<color>"],
|
|
122
|
+
"fill-rule" => %w[nonzero evenodd inherit],
|
|
123
|
+
"fill-opacity" => [],
|
|
124
|
+
|
|
125
|
+
"requiredFeatures" => [],
|
|
126
|
+
"requiredFormats" => [],
|
|
127
|
+
"requiredExtensions" => [],
|
|
128
|
+
"requiredFonts" => [],
|
|
129
|
+
"systemLanguage" => [],
|
|
130
|
+
}.freeze
|
|
131
|
+
|
|
132
|
+
# Basic types for validation
|
|
133
|
+
BASIC_TYPES = {
|
|
134
|
+
"<color>" => %w[black #ffffff #FFFFFF white #000000],
|
|
135
|
+
"<paint>" => ["none", "currentColor", "inherit", "<color>"],
|
|
136
|
+
"<integer>" => ["+"],
|
|
137
|
+
"<number>" => ["+"],
|
|
138
|
+
}.freeze
|
|
139
|
+
|
|
140
|
+
# Default color for replacements
|
|
141
|
+
COLOR_DEFAULT = "black"
|
|
142
|
+
|
|
143
|
+
# Color threshold for grayscale conversion (from Python code)
|
|
144
|
+
COLOR_THRESHOLD = 764
|
|
145
|
+
|
|
146
|
+
# Style properties that can be promoted to attributes
|
|
147
|
+
STYLE_PROPERTIES = %w[
|
|
148
|
+
font-family font-weight font-style font-variant direction unicode-bidi
|
|
149
|
+
text-anchor fill fill-rule stroke stroke-width font-size fill-opacity
|
|
150
|
+
stroke-linecap stroke-opacity stroke-linejoin
|
|
151
|
+
].freeze
|
|
152
|
+
|
|
153
|
+
# Elements allowed within other elements
|
|
154
|
+
SVG_CHILD_ELEMENTS = %w[
|
|
155
|
+
title path rect circle line ellipse polyline polygon solidColor
|
|
156
|
+
textArea text g defs use a tspan desc
|
|
157
|
+
].freeze
|
|
158
|
+
|
|
159
|
+
TEXT_CHILD_ELEMENTS = %w[desc title tspan text a].freeze
|
|
160
|
+
|
|
161
|
+
ELEMENT_CHILDREN = {
|
|
162
|
+
"svg" => SVG_CHILD_ELEMENTS,
|
|
163
|
+
"desc" => ["text"],
|
|
164
|
+
"title" => ["text"],
|
|
165
|
+
"path" => %w[title desc],
|
|
166
|
+
"rect" => %w[title desc],
|
|
167
|
+
"circle" => %w[title desc],
|
|
168
|
+
"line" => %w[title desc],
|
|
169
|
+
"ellipse" => %w[title desc],
|
|
170
|
+
"polyline" => %w[title desc],
|
|
171
|
+
"polygon" => %w[title desc],
|
|
172
|
+
"solidColor" => %w[title desc],
|
|
173
|
+
"textArea" => TEXT_CHILD_ELEMENTS,
|
|
174
|
+
"text" => TEXT_CHILD_ELEMENTS,
|
|
175
|
+
"g" => SVG_CHILD_ELEMENTS,
|
|
176
|
+
"defs" => SVG_CHILD_ELEMENTS,
|
|
177
|
+
"use" => %w[title desc],
|
|
178
|
+
"a" => SVG_CHILD_ELEMENTS,
|
|
179
|
+
"tspan" => TEXT_CHILD_ELEMENTS + ["tbreak"],
|
|
180
|
+
}.freeze
|
|
181
|
+
|
|
182
|
+
# Allowed SVG namespaces
|
|
183
|
+
SVG_NAMESPACES = [
|
|
184
|
+
"http://www.w3.org/2000/svg",
|
|
185
|
+
].freeze
|
|
186
|
+
|
|
187
|
+
# Allowed XML namespaces
|
|
188
|
+
XMLNS_NAMESPACES = [
|
|
189
|
+
"http://www.w3.org/2000/svg",
|
|
190
|
+
"http://www.w3.org/1999/xlink",
|
|
191
|
+
"http://www.w3.org/XML/1998/namespace",
|
|
192
|
+
].freeze
|
|
193
|
+
|
|
194
|
+
# Color mapping for common color names to RFC-compliant colors
|
|
195
|
+
COLOR_MAP = {
|
|
196
|
+
"rgb(0,0,0)" => "black",
|
|
197
|
+
}.freeze
|
|
198
|
+
end
|
|
199
|
+
end
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SvgConform
|
|
4
|
+
# Handles CSS color equivalence, normalization, and conversion
|
|
5
|
+
class CssColor
|
|
6
|
+
# Named colors mapping to their hex equivalents
|
|
7
|
+
NAMED_COLORS = {
|
|
8
|
+
"black" => "#000000",
|
|
9
|
+
"white" => "#ffffff",
|
|
10
|
+
"red" => "#ff0000",
|
|
11
|
+
"green" => "#008000",
|
|
12
|
+
"blue" => "#0000ff",
|
|
13
|
+
"yellow" => "#ffff00",
|
|
14
|
+
"cyan" => "#00ffff",
|
|
15
|
+
"magenta" => "#ff00ff",
|
|
16
|
+
"silver" => "#c0c0c0",
|
|
17
|
+
"gray" => "#808080",
|
|
18
|
+
"grey" => "#808080",
|
|
19
|
+
"maroon" => "#800000",
|
|
20
|
+
"olive" => "#808000",
|
|
21
|
+
"lime" => "#00ff00",
|
|
22
|
+
"aqua" => "#00ffff",
|
|
23
|
+
"teal" => "#008080",
|
|
24
|
+
"navy" => "#000080",
|
|
25
|
+
"fuchsia" => "#ff00ff",
|
|
26
|
+
"purple" => "#800080",
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
29
|
+
# Reverse mapping for canonical named color lookup
|
|
30
|
+
HEX_TO_NAMED = NAMED_COLORS.invert.freeze
|
|
31
|
+
|
|
32
|
+
# CSS color keywords that have special meaning
|
|
33
|
+
SPECIAL_KEYWORDS = %w[inherit currentcolor transparent none].freeze
|
|
34
|
+
|
|
35
|
+
class << self
|
|
36
|
+
# Normalize a color to its canonical hex representation
|
|
37
|
+
def normalize(color)
|
|
38
|
+
return nil if color.nil? || color.empty?
|
|
39
|
+
|
|
40
|
+
color = color.strip.downcase
|
|
41
|
+
|
|
42
|
+
# Handle special keywords
|
|
43
|
+
return color if SPECIAL_KEYWORDS.include?(color)
|
|
44
|
+
|
|
45
|
+
# Handle named colors
|
|
46
|
+
return NAMED_COLORS[color] if NAMED_COLORS.key?(color)
|
|
47
|
+
|
|
48
|
+
# Handle hex colors
|
|
49
|
+
if color.match?(/^#[0-9a-f]{3}$/)
|
|
50
|
+
# Expand short hex: #fff → #ffffff
|
|
51
|
+
return expand_short_hex(color)
|
|
52
|
+
elsif color.match?(/^#[0-9a-f]{6}$/)
|
|
53
|
+
# Already normalized 6-digit hex
|
|
54
|
+
return color
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Handle RGB functions with integers
|
|
58
|
+
rgb_match = color.match(/^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/)
|
|
59
|
+
if rgb_match
|
|
60
|
+
r, g, b = rgb_match[1..3].map(&:to_i)
|
|
61
|
+
return rgb_to_hex(r, g, b)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Handle RGB functions with percentages
|
|
65
|
+
rgb_percent_match = color.match(/^rgb\s*\(\s*(\d+(?:\.\d+)?)%\s*,\s*(\d+(?:\.\d+)?)%\s*,\s*(\d+(?:\.\d+)?)%\s*\)$/)
|
|
66
|
+
if rgb_percent_match
|
|
67
|
+
r = (rgb_percent_match[1].to_f * 255 / 100).round
|
|
68
|
+
g = (rgb_percent_match[2].to_f * 255 / 100).round
|
|
69
|
+
b = (rgb_percent_match[3].to_f * 255 / 100).round
|
|
70
|
+
return rgb_to_hex(r, g, b)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Handle mixed RGB functions (percentage and absolute values)
|
|
74
|
+
rgb_mixed_match = color.match(/^rgb\s*\(\s*(\d+(?:\.\d+)?%?)\s*,\s*(\d+(?:\.\d+)?%?)\s*,\s*(\d+(?:\.\d+)?%?)\s*\)$/)
|
|
75
|
+
if rgb_mixed_match
|
|
76
|
+
r = parse_rgb_value(rgb_mixed_match[1])
|
|
77
|
+
g = parse_rgb_value(rgb_mixed_match[2])
|
|
78
|
+
b = parse_rgb_value(rgb_mixed_match[3])
|
|
79
|
+
return rgb_to_hex(r, g, b)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Handle RGBA functions (ignore alpha for SVG purposes)
|
|
83
|
+
rgba_match = color.match(/^rgba\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*[\d.]+\s*\)$/)
|
|
84
|
+
if rgba_match
|
|
85
|
+
r, g, b = rgba_match[1..3].map(&:to_i)
|
|
86
|
+
return rgb_to_hex(r, g, b)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Return original if unrecognized format
|
|
90
|
+
color
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Check if two colors are equivalent
|
|
94
|
+
def equivalent?(color1, color2)
|
|
95
|
+
return false if color1.nil? || color2.nil?
|
|
96
|
+
return true if color1 == color2
|
|
97
|
+
|
|
98
|
+
norm1 = normalize(color1)
|
|
99
|
+
norm2 = normalize(color2)
|
|
100
|
+
|
|
101
|
+
return false if norm1.nil? || norm2.nil?
|
|
102
|
+
|
|
103
|
+
norm1 == norm2
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Convert color to its canonical form (prefer named colors when available)
|
|
107
|
+
def to_canonical(color)
|
|
108
|
+
normalized = normalize(color)
|
|
109
|
+
return color if normalized.nil?
|
|
110
|
+
|
|
111
|
+
# Return special keywords as-is
|
|
112
|
+
return normalized if SPECIAL_KEYWORDS.include?(normalized)
|
|
113
|
+
|
|
114
|
+
# Convert hex to named color if available
|
|
115
|
+
named = HEX_TO_NAMED[normalized]
|
|
116
|
+
return named if named
|
|
117
|
+
|
|
118
|
+
# Return normalized hex
|
|
119
|
+
normalized
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Expand short hex colors: #fff → #ffffff
|
|
123
|
+
def expand_short_hex(hex)
|
|
124
|
+
return hex unless hex.match?(/^#[0-9a-f]{3}$/)
|
|
125
|
+
|
|
126
|
+
chars = hex[1..3].chars
|
|
127
|
+
"##{chars.map { |c| c * 2 }.join}"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Convert RGB values to hex
|
|
131
|
+
def rgb_to_hex(red, green, blue)
|
|
132
|
+
# Clamp values to 0-255 range
|
|
133
|
+
r = [[red.to_i, 0].max, 255].min
|
|
134
|
+
g = [[green.to_i, 0].max, 255].min
|
|
135
|
+
b = [[blue.to_i, 0].max, 255].min
|
|
136
|
+
|
|
137
|
+
format("#%02x%02x%02x", r, g, b)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Check if a color is valid according to SVG specifications
|
|
141
|
+
def valid_css_color?(color)
|
|
142
|
+
return false if color.nil? || color.empty?
|
|
143
|
+
|
|
144
|
+
normalized = normalize(color)
|
|
145
|
+
return false if normalized.nil?
|
|
146
|
+
|
|
147
|
+
# Valid if it normalizes to something we recognize
|
|
148
|
+
SPECIAL_KEYWORDS.include?(normalized) ||
|
|
149
|
+
NAMED_COLORS.key?(color.strip.downcase) ||
|
|
150
|
+
normalized.match?(/^#[0-9a-f]{6}$/)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Get all equivalent forms of a color
|
|
154
|
+
def equivalent_forms(color)
|
|
155
|
+
normalized = normalize(color)
|
|
156
|
+
return [color] if normalized.nil?
|
|
157
|
+
|
|
158
|
+
forms = [normalized]
|
|
159
|
+
|
|
160
|
+
# Add named form if available
|
|
161
|
+
named = HEX_TO_NAMED[normalized]
|
|
162
|
+
forms << named if named
|
|
163
|
+
|
|
164
|
+
# Add short hex form if applicable
|
|
165
|
+
if normalized.match?(/^#([0-9a-f])\1([0-9a-f])\2([0-9a-f])\3$/)
|
|
166
|
+
short_hex = "##{$1}#{$2}#{$3}"
|
|
167
|
+
forms << short_hex
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Add uppercase variants for hex
|
|
171
|
+
if normalized.match?(/^#[0-9a-f]{6}$/)
|
|
172
|
+
forms << normalized.upcase
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
forms.uniq
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Check if a color is in a list of allowed colors (with equivalence)
|
|
179
|
+
def allowed_in_list?(color, allowed_colors)
|
|
180
|
+
return false if color.nil? || allowed_colors.nil? || allowed_colors.empty?
|
|
181
|
+
|
|
182
|
+
normalized = normalize(color)
|
|
183
|
+
return false if normalized.nil?
|
|
184
|
+
|
|
185
|
+
allowed_colors.any? { |allowed| equivalent?(color, allowed) }
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Calculate RGB sum for threshold-based validation
|
|
189
|
+
def rgb_sum(color)
|
|
190
|
+
return nil if color.nil? || color.empty?
|
|
191
|
+
|
|
192
|
+
color = color.strip.downcase
|
|
193
|
+
|
|
194
|
+
# Handle hex colors
|
|
195
|
+
if color.match?(/^#[0-9a-f]{3}$/)
|
|
196
|
+
# Expand short hex and calculate
|
|
197
|
+
expanded = expand_short_hex(color)
|
|
198
|
+
r = expanded[1..2].to_i(16)
|
|
199
|
+
g = expanded[3..4].to_i(16)
|
|
200
|
+
b = expanded[5..6].to_i(16)
|
|
201
|
+
return r + g + b
|
|
202
|
+
elsif color.match?(/^#[0-9a-f]{6}$/)
|
|
203
|
+
r = color[1..2].to_i(16)
|
|
204
|
+
g = color[3..4].to_i(16)
|
|
205
|
+
b = color[5..6].to_i(16)
|
|
206
|
+
return r + g + b
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Handle RGB functions with integers
|
|
210
|
+
rgb_match = color.match(/^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/)
|
|
211
|
+
if rgb_match
|
|
212
|
+
r, g, b = rgb_match[1..3].map(&:to_i)
|
|
213
|
+
return r + g + b
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Handle RGB functions with percentages
|
|
217
|
+
rgb_percent_match = color.match(/^rgb\s*\(\s*(\d+(?:\.\d+)?)%\s*,\s*(\d+(?:\.\d+)?)%\s*,\s*(\d+(?:\.\d+)?)%\s*\)$/)
|
|
218
|
+
if rgb_percent_match
|
|
219
|
+
r = (rgb_percent_match[1].to_f * 255 / 100).round
|
|
220
|
+
g = (rgb_percent_match[2].to_f * 255 / 100).round
|
|
221
|
+
b = (rgb_percent_match[3].to_f * 255 / 100).round
|
|
222
|
+
return r + g + b
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Handle mixed RGB functions
|
|
226
|
+
rgb_mixed_match = color.match(/^rgb\s*\(\s*(\d+(?:\.\d+)?%?)\s*,\s*(\d+(?:\.\d+)?%?)\s*,\s*(\d+(?:\.\d+)?%?)\s*\)$/)
|
|
227
|
+
if rgb_mixed_match
|
|
228
|
+
r = parse_rgb_value(rgb_mixed_match[1])
|
|
229
|
+
g = parse_rgb_value(rgb_mixed_match[2])
|
|
230
|
+
b = parse_rgb_value(rgb_mixed_match[3])
|
|
231
|
+
return r + g + b
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Handle named colors
|
|
235
|
+
if NAMED_COLORS.key?(color)
|
|
236
|
+
hex = NAMED_COLORS[color]
|
|
237
|
+
r = hex[1..2].to_i(16)
|
|
238
|
+
g = hex[3..4].to_i(16)
|
|
239
|
+
b = hex[5..6].to_i(16)
|
|
240
|
+
return r + g + b
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
nil
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
private
|
|
247
|
+
|
|
248
|
+
# Parse individual RGB value (percentage or absolute)
|
|
249
|
+
def parse_rgb_value(value_str)
|
|
250
|
+
value_str = value_str.strip
|
|
251
|
+
if value_str.end_with?("%")
|
|
252
|
+
# Percentage value
|
|
253
|
+
percent = value_str.chomp("%").to_f
|
|
254
|
+
(percent * 255 / 100).round
|
|
255
|
+
else
|
|
256
|
+
# Absolute value
|
|
257
|
+
value_str.to_i
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "moxml"
|
|
4
|
+
|
|
5
|
+
module SvgConform
|
|
6
|
+
# Wrapper around Moxml document for SVG validation
|
|
7
|
+
class Document
|
|
8
|
+
attr_reader :file_path, :moxml_document
|
|
9
|
+
|
|
10
|
+
def initialize(content_or_path)
|
|
11
|
+
if File.exist?(content_or_path.to_s)
|
|
12
|
+
@file_path = content_or_path
|
|
13
|
+
@content = File.read(@file_path)
|
|
14
|
+
else
|
|
15
|
+
@file_path = nil
|
|
16
|
+
@content = content_or_path
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
parse_document
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.from_file(file_path)
|
|
23
|
+
new(file_path)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.from_content(content)
|
|
27
|
+
new(content)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def root
|
|
31
|
+
@moxml_document.root
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def elements
|
|
35
|
+
@moxml_document.elements
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def xpath(path, namespaces = {})
|
|
39
|
+
@moxml_document.xpath(path, namespaces)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def traverse(&)
|
|
43
|
+
traverse_node(root, &) if root
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def svg_elements
|
|
47
|
+
# Handle both default namespace and prefixed namespace
|
|
48
|
+
if has_svg_namespace_prefix?
|
|
49
|
+
xpath("//svg:*", { "svg" => "http://www.w3.org/2000/svg" }).select { |node| node.respond_to?(:name) }
|
|
50
|
+
else
|
|
51
|
+
xpath("//*[namespace-uri()='http://www.w3.org/2000/svg']").select { |node| node.respond_to?(:name) }
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def to_xml
|
|
56
|
+
@moxml_document.to_xml
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def dup
|
|
60
|
+
Document.from_content(to_xml)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def valid_xml?
|
|
64
|
+
!@moxml_document.nil?
|
|
65
|
+
rescue StandardError
|
|
66
|
+
false
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def namespace_uri
|
|
70
|
+
root&.namespace&.uri
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def svg_namespace?
|
|
74
|
+
namespace_uri == "http://www.w3.org/2000/svg"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def has_svg_namespace_prefix?
|
|
78
|
+
@content.include?("xmlns:svg=") || @content.include?("svg:")
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def has_viewbox?
|
|
82
|
+
root&.attribute("viewBox")
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def viewbox
|
|
86
|
+
root&.attribute("viewBox")&.value
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def width
|
|
90
|
+
root&.attribute("width")&.value
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def height
|
|
94
|
+
root&.attribute("height")&.value
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def version
|
|
98
|
+
root&.attribute("version")&.value
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Find all elements with a specific name
|
|
102
|
+
def find_elements(name)
|
|
103
|
+
if has_svg_namespace_prefix?
|
|
104
|
+
xpath("//svg:#{name}", { "svg" => "http://www.w3.org/2000/svg" })
|
|
105
|
+
else
|
|
106
|
+
xpath("//#{name}[namespace-uri()='http://www.w3.org/2000/svg']")
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Find all elements with style attributes
|
|
111
|
+
def elements_with_style
|
|
112
|
+
xpath("//*[@style]")
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Find all elements with specific attributes
|
|
116
|
+
def elements_with_attribute(attr_name)
|
|
117
|
+
xpath("//*[@#{attr_name}]")
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Get all unique element names in the document
|
|
121
|
+
def element_names
|
|
122
|
+
svg_elements.map(&:name).uniq.sort
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Get all unique attribute names in the document
|
|
126
|
+
def attribute_names
|
|
127
|
+
attributes = []
|
|
128
|
+
traverse do |node|
|
|
129
|
+
attributes.concat(node.attributes.keys) if node.respond_to?(:attributes)
|
|
130
|
+
end
|
|
131
|
+
attributes.uniq.sort
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Check if document contains external references
|
|
135
|
+
def has_external_references?
|
|
136
|
+
# Check for external stylesheets
|
|
137
|
+
link_elements = if has_svg_namespace_prefix?
|
|
138
|
+
xpath('//svg:link[@rel="stylesheet"]',
|
|
139
|
+
{ "svg" => "http://www.w3.org/2000/svg" })
|
|
140
|
+
else
|
|
141
|
+
xpath('//link[@rel="stylesheet"][namespace-uri()="http://www.w3.org/2000/svg"]')
|
|
142
|
+
end
|
|
143
|
+
return true if link_elements.any?
|
|
144
|
+
|
|
145
|
+
# Check for @import in style elements
|
|
146
|
+
style_elements = if has_svg_namespace_prefix?
|
|
147
|
+
xpath("//svg:style",
|
|
148
|
+
{ "svg" => "http://www.w3.org/2000/svg" })
|
|
149
|
+
else
|
|
150
|
+
xpath("//style[namespace-uri()='http://www.w3.org/2000/svg']")
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
style_elements.each do |style|
|
|
154
|
+
content = style.text
|
|
155
|
+
return true if content&.include?("@import")
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Check for external references in style attributes
|
|
159
|
+
elements_with_style.each do |element|
|
|
160
|
+
style_value = element.attribute("style")&.value
|
|
161
|
+
return true if style_value&.include?("url(")
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
false
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
private
|
|
168
|
+
|
|
169
|
+
def parse_document
|
|
170
|
+
begin
|
|
171
|
+
# Create a Moxml context and parse the document
|
|
172
|
+
context = Moxml.new
|
|
173
|
+
@moxml_document = context.parse(@content)
|
|
174
|
+
rescue StandardError => e
|
|
175
|
+
raise ParseError, "Failed to parse SVG document: #{e.message}"
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
raise ParseError, "Document could not be parsed" unless @moxml_document
|
|
179
|
+
|
|
180
|
+
raise ParseError, "Document has no root element" unless root
|
|
181
|
+
|
|
182
|
+
# Check if root element is SVG (handle both namespaced and non-namespaced)
|
|
183
|
+
root_name = root.name
|
|
184
|
+
root_namespace = root.namespace&.uri
|
|
185
|
+
|
|
186
|
+
# Accept if element name is "svg" regardless of namespace
|
|
187
|
+
# or if it's in the SVG namespace
|
|
188
|
+
return if root_name == "svg" || root_namespace == "http://www.w3.org/2000/svg"
|
|
189
|
+
|
|
190
|
+
raise ParseError, "Root element must be 'svg', found '#{root_name}'"
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def traverse_node(node, &block)
|
|
194
|
+
yield node if block
|
|
195
|
+
|
|
196
|
+
return unless node.respond_to?(:children)
|
|
197
|
+
|
|
198
|
+
node.children.each do |child|
|
|
199
|
+
traverse_node(child, &block)
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|