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.
- checksums.yaml +4 -4
- data/lib/odin/export.rb +1 -1
- data/lib/odin/forms/accessibility.rb +95 -0
- data/lib/odin/forms/css.rb +42 -0
- data/lib/odin/forms/parser.rb +719 -0
- data/lib/odin/forms/renderer.rb +534 -0
- data/lib/odin/forms/types.rb +102 -0
- data/lib/odin/forms/units.rb +41 -0
- data/lib/odin/forms.rb +55 -0
- data/lib/odin/parsing/parser.rb +25 -1
- data/lib/odin/parsing/tokenizer.rb +38 -20
- data/lib/odin/parsing/value_parser.rb +65 -7
- data/lib/odin/resolver/import_resolver.rb +40 -12
- data/lib/odin/resolver/type_registry.rb +54 -0
- data/lib/odin/transform/format_exporters.rb +88 -48
- data/lib/odin/transform/source_parsers.rb +2 -2
- data/lib/odin/transform/transform_engine.rb +1157 -155
- data/lib/odin/transform/transform_parser.rb +333 -16
- data/lib/odin/transform/transform_types.rb +23 -7
- data/lib/odin/transform/verb_context.rb +19 -1
- data/lib/odin/transform/verbs/aggregation_verbs.rb +2 -1
- data/lib/odin/transform/verbs/collection_verbs.rb +110 -55
- data/lib/odin/transform/verbs/datetime_verbs.rb +66 -12
- data/lib/odin/transform/verbs/financial_verbs.rb +47 -22
- data/lib/odin/transform/verbs/numeric_verbs.rb +5 -7
- data/lib/odin/transform/verbs/object_verbs.rb +13 -15
- data/lib/odin/types/errors.rb +9 -1
- data/lib/odin/types/schema.rb +20 -3
- data/lib/odin/utils/format_utils.rb +31 -15
- data/lib/odin/validation/format_validators.rb +7 -9
- data/lib/odin/validation/invariant_evaluator.rb +410 -0
- data/lib/odin/validation/schema_definition_validator.rb +357 -0
- data/lib/odin/validation/schema_parser.rb +234 -21
- data/lib/odin/validation/validator.rb +281 -123
- data/lib/odin/version.rb +1 -1
- data/lib/odin.rb +98 -4
- metadata +12 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 73fe064a2bdc58380f03167913ebea10dce6c722a8b0b2b30703134b28f4ce2e
|
|
4
|
+
data.tar.gz: c60ca22db57be40b617169266dbd97c0e3e181674eaf1e5ec57796fc24729635
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|