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