odin-foundation 1.0.4 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/lib/odin/export.rb +1 -1
  3. data/lib/odin/forms/accessibility.rb +95 -0
  4. data/lib/odin/forms/css.rb +42 -0
  5. data/lib/odin/forms/parser.rb +719 -0
  6. data/lib/odin/forms/renderer.rb +534 -0
  7. data/lib/odin/forms/types.rb +102 -0
  8. data/lib/odin/forms/units.rb +41 -0
  9. data/lib/odin/forms.rb +55 -0
  10. data/lib/odin/parsing/parser.rb +25 -1
  11. data/lib/odin/parsing/tokenizer.rb +38 -20
  12. data/lib/odin/parsing/value_parser.rb +65 -7
  13. data/lib/odin/resolver/import_resolver.rb +40 -12
  14. data/lib/odin/resolver/type_registry.rb +54 -0
  15. data/lib/odin/transform/format_exporters.rb +88 -48
  16. data/lib/odin/transform/source_parsers.rb +2 -2
  17. data/lib/odin/transform/transform_engine.rb +1157 -155
  18. data/lib/odin/transform/transform_parser.rb +333 -16
  19. data/lib/odin/transform/transform_types.rb +23 -7
  20. data/lib/odin/transform/verb_context.rb +19 -1
  21. data/lib/odin/transform/verbs/aggregation_verbs.rb +2 -1
  22. data/lib/odin/transform/verbs/collection_verbs.rb +110 -55
  23. data/lib/odin/transform/verbs/datetime_verbs.rb +66 -12
  24. data/lib/odin/transform/verbs/financial_verbs.rb +47 -22
  25. data/lib/odin/transform/verbs/numeric_verbs.rb +5 -7
  26. data/lib/odin/transform/verbs/object_verbs.rb +13 -15
  27. data/lib/odin/types/errors.rb +9 -1
  28. data/lib/odin/types/schema.rb +20 -3
  29. data/lib/odin/utils/format_utils.rb +31 -15
  30. data/lib/odin/validation/format_validators.rb +7 -9
  31. data/lib/odin/validation/invariant_evaluator.rb +410 -0
  32. data/lib/odin/validation/schema_definition_validator.rb +357 -0
  33. data/lib/odin/validation/schema_parser.rb +234 -21
  34. data/lib/odin/validation/validator.rb +281 -123
  35. data/lib/odin/version.rb +1 -1
  36. data/lib/odin.rb +98 -4
  37. metadata +12 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e48cf0a0983a951cff7ce73bdc7855030d887ed1d42b2c6ad2d3fd7fe8a2183b
4
- data.tar.gz: 85ee5230297ef6ae6a481c5b0fb56e7aa4df7aac789c12d06e617abfe821f985
3
+ metadata.gz: 73fe064a2bdc58380f03167913ebea10dce6c722a8b0b2b30703134b28f4ce2e
4
+ data.tar.gz: c60ca22db57be40b617169266dbd97c0e3e181674eaf1e5ec57796fc24729635
5
5
  SHA512:
6
- metadata.gz: 9fb30a61553eb90ff2729e1c310b55a64ea349f58d46205c326d473783e3f12f793c79708e5d4fcf4041131fcf2d4ea77839c485507187ba398d63dbcde5b98a
7
- data.tar.gz: 2c93ac612d779ad45d9fb749fcc6e01c21ee090f96d57f5c670a42bd87a9e8186a92867af6b132936f24620056ee2cd92d4bc7d84206dcbe12864b484bd68b76
6
+ metadata.gz: 3495932fb340947ca0bc35a381557d5b1bf55e18d50150b9117ae073965faeb2dac8f23eedbcc0fdf8058013288b819aa41da3cf1436418819bc941847018064
7
+ data.tar.gz: 2d01979d2e392f6ed40b84a2626b2056d9fda8b281f1cfe6978604f7ae0359b7882db6c2465555cc272337560b010fe1d018f2fcf773647ab0da126a296e0652
data/lib/odin/export.rb CHANGED
@@ -17,7 +17,7 @@ module Odin
17
17
 
18
18
  # Convert OdinDocument to XML string
19
19
  def self.to_xml(doc, root: "root", preserve_types: false, preserve_modifiers: false)
20
- # When preserving modifiers, always preserve types too (matches Java behavior)
20
+ # When preserving modifiers, always preserve types too
21
21
  preserve_types = true if preserve_modifiers
22
22
 
23
23
  xml = +%{<?xml version="1.0" encoding="UTF-8"?>\n}
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Odin
4
+ module Forms
5
+ # Pure helpers for generating accessible markup and computing WCAG contrast.
6
+ module Accessibility
7
+ module_function
8
+
9
+ # Unique, stable HTML element id for a field.
10
+ def generate_field_id(element_name, page_index)
11
+ "odin-field-#{page_index}-#{element_name}"
12
+ end
13
+
14
+ # A <label> associated with the given input id.
15
+ def field_label_html(label, input_id)
16
+ %(<label for="#{input_id}" class="odin-form-label">#{label}</label>)
17
+ end
18
+
19
+ # ARIA and id attributes for a field element.
20
+ def field_aria_attrs(element, page_index)
21
+ attrs = {
22
+ "id" => generate_field_id(element.name, page_index),
23
+ "aria-label" => element[:"aria-label"] || element[:label],
24
+ }
25
+ attrs["aria-required"] = "true" if element[:required]
26
+ attrs
27
+ end
28
+
29
+ # Wrap content in a <fieldset>/<legend> for grouped controls.
30
+ def field_group_html(_group_name, legend, content)
31
+ %(<fieldset class="odin-form-fieldset">) +
32
+ %(<legend class="odin-form-legend">#{legend}</legend>) +
33
+ content +
34
+ "</fieldset>"
35
+ end
36
+
37
+ # Skip-navigation link targeting the form content anchor.
38
+ def skip_link_html(form_title)
39
+ %(<a class="odin-form-sr-only odin-form-skip" href="#odin-form-content">) +
40
+ "Skip to #{form_title}" +
41
+ "</a>"
42
+ end
43
+
44
+ # Visually-hidden text that remains announced to screen readers.
45
+ def sr_only_html(text)
46
+ %(<span class="odin-form-sr-only">#{text}</span>)
47
+ end
48
+
49
+ # Field elements only, sorted by reading order (top-to-bottom, left-to-right).
50
+ def tab_order_sort(elements)
51
+ elements.select(&:field?).sort do |a, b|
52
+ ay = a[:y] || 0
53
+ by = b[:y] || 0
54
+ if ay != by
55
+ ay <=> by
56
+ else
57
+ (a[:x] || 0) <=> (b[:x] || 0)
58
+ end
59
+ end
60
+ end
61
+
62
+ # ── WCAG contrast ──────────────────────────────────────────────────────
63
+
64
+ def contrast_ratio(fg, bg)
65
+ l1 = relative_luminance(fg)
66
+ l2 = relative_luminance(bg)
67
+ lighter = [l1, l2].max
68
+ darker = [l1, l2].min
69
+ (lighter + 0.05) / (darker + 0.05)
70
+ end
71
+
72
+ def meets_contrast_aa(fg, bg, font_size)
73
+ ratio = contrast_ratio(fg, bg)
74
+ font_size >= 18 ? ratio >= 3.0 : ratio >= 4.5
75
+ end
76
+
77
+ def linearize(channel)
78
+ srgb = channel / 255.0
79
+ srgb <= 0.04045 ? srgb / 12.92 : (((srgb + 0.055) / 1.055)**2.4)
80
+ end
81
+
82
+ def parse_hex(hex)
83
+ clean = hex.start_with?("#") ? hex[1..] : hex
84
+ raise ArgumentError, %(Invalid hex colour: "#{hex}") unless clean.match?(/\A[0-9a-fA-F]{6}\z/)
85
+
86
+ [clean[0, 2].to_i(16), clean[2, 2].to_i(16), clean[4, 2].to_i(16)]
87
+ end
88
+
89
+ def relative_luminance(hex)
90
+ r, g, b = parse_hex(hex)
91
+ (0.2126 * linearize(r)) + (0.7152 * linearize(g)) + (0.0722 * linearize(b))
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Odin
4
+ module Forms
5
+ # Scoped CSS generation for the form HTML renderer.
6
+ module Css
7
+ module_function
8
+
9
+ # Base stylesheet scoped under .odin-form.
10
+ def generate_form_css
11
+ [
12
+ ".odin-form { position: relative; font-family: Helvetica, Arial, sans-serif; }",
13
+ ".odin-form-page { position: relative; background: white; overflow: hidden; box-sizing: border-box; margin: 0 auto; }",
14
+ ".odin-form-element { position: absolute; box-sizing: border-box; }",
15
+ ".odin-form-label { display: block; font-size: 8pt; color: #666; margin-bottom: 1px; }",
16
+ ".odin-form-input { width: 100%; height: 100%; box-sizing: border-box; border: 1px solid #999; padding: 2px 4px; font-family: inherit; font-size: inherit; background: transparent; }",
17
+ ".odin-form-input:focus { outline: 2px solid #34A3F5; border-color: #34A3F5; }",
18
+ ".odin-form-checkbox, .odin-form-radio { width: auto; height: auto; }",
19
+ ".odin-form-select { width: 100%; height: 100%; }",
20
+ ".odin-form-signature { border: none; border-bottom: 1px solid #000; background: transparent; }",
21
+ ".odin-form-fieldset { border: none; padding: 0; margin: 0; position: absolute; }",
22
+ ".odin-form-legend { font-size: 8pt; color: #666; }",
23
+ ".odin-form-sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }",
24
+ ".odin-form-skip:focus { position: static; width: auto; height: auto; clip: auto; overflow: visible; margin: 0; padding: 4px 8px; }",
25
+ ].join("\n")
26
+ end
27
+
28
+ # @media print block optimising the form for printing.
29
+ def generate_print_css
30
+ [
31
+ "@media print {",
32
+ " .odin-form-page { page-break-after: always; margin: 0; box-shadow: none; }",
33
+ " .odin-form-page:last-child { page-break-after: auto; }",
34
+ " .odin-form-input { border: none; border-bottom: 1px solid #000; background: transparent; }",
35
+ " .odin-form-skip { display: none; }",
36
+ " .odin-form-sr-only { display: none; }",
37
+ "}",
38
+ ].join("\n")
39
+ end
40
+ end
41
+ end
42
+ end