dcc 0.2.0 → 0.2.1
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/.github/workflows/ci.yml +19 -0
- data/.github/workflows/release.yml +28 -0
- data/.gitignore +4 -0
- data/.rubocop_todo.yml +560 -10
- data/CHANGELOG.adoc +34 -1
- data/CONTRIBUTING.adoc +8 -1
- data/Gemfile +5 -7
- data/README.adoc +162 -7
- data/dcc.gemspec +8 -2
- data/docs/schema-versions.adoc +20 -7
- data/examples/build_minimal_dcc.rb +0 -0
- data/examples/convert_all_formats.rb +2 -1
- data/examples/parse_and_inspect.rb +0 -0
- data/examples/quantity_math_demo.rb +7 -7
- data/examples/validate_full.rb +1 -1
- data/lib/dcc/base/calibration_laboratory.rb +2 -1
- data/lib/dcc/base/condition.rb +5 -2
- data/lib/dcc/base/core_data.rb +8 -3
- data/lib/dcc/base/formula.rb +3 -3
- data/lib/dcc/base/identification.rb +6 -3
- data/lib/dcc/base/mathml.rb +33 -0
- data/lib/dcc/base/quantity.rb +2 -1
- data/lib/dcc/base/resp_person.rb +4 -2
- data/lib/dcc/base/text.rb +1 -1
- data/lib/dcc/base.rb +2 -1
- data/lib/dcc/builder/dsl.rb +1 -1
- data/lib/dcc/builder/session.rb +16 -14
- data/lib/dcc/builder.rb +3 -3
- data/lib/dcc/cli/cli.rb +36 -18
- data/lib/dcc/cli/formatters.rb +35 -7
- data/lib/dcc/cli.rb +1 -1
- data/lib/dcc/context_configuration.rb +2 -1
- data/lib/dcc/context_options.rb +4 -2
- data/lib/dcc/convert/csv.rb +27 -9
- data/lib/dcc/convert/html.rb +26 -110
- data/lib/dcc/convert/json.rb +10 -7
- data/lib/dcc/convert/liquid_filters.rb +58 -0
- data/lib/dcc/convert/result.rb +1 -1
- data/lib/dcc/convert/templates/report.liquid +250 -0
- data/lib/dcc/convert/yaml.rb +1 -1
- data/lib/dcc/convert.rb +1 -0
- data/lib/dcc/diff/change.rb +1 -1
- data/lib/dcc/diff/result.rb +8 -4
- data/lib/dcc/diff.rb +43 -11
- data/lib/dcc/error.rb +7 -0
- data/lib/dcc/extract/file.rb +3 -4
- data/lib/dcc/extract/ring.rb +1 -1
- data/lib/dcc/extract.rb +1 -1
- data/lib/dcc/i18n/text_lookup.rb +8 -4
- data/lib/dcc/i18n.rb +1 -1
- data/lib/dcc/inspect/report.rb +51 -14
- data/lib/dcc/inspect.rb +1 -1
- data/lib/dcc/migrate/route.rb +44 -0
- data/lib/dcc/migrate/v2_to_v3.rb +445 -0
- data/lib/dcc/migrate.rb +130 -26
- data/lib/dcc/plugin/base.rb +64 -0
- data/lib/dcc/plugin/registry.rb +7 -2
- data/lib/dcc/plugin.rb +6 -2
- data/lib/dcc/quantity_format/formatter.rb +16 -10
- data/lib/dcc/quantity_format.rb +1 -1
- data/lib/dcc/quantity_math/quantity.rb +1 -1
- data/lib/dcc/quantity_math/real.rb +6 -6
- data/lib/dcc/quantity_math.rb +1 -1
- data/lib/dcc/schema/version.rb +2 -2
- data/lib/dcc/server/app.html +282 -0
- data/lib/dcc/server/app.rb +171 -87
- data/lib/dcc/server/demo_cert.xml +244 -0
- data/lib/dcc/server/storage/entry.rb +1 -1
- data/lib/dcc/server/storage/memory.rb +3 -2
- data/lib/dcc/server/storage.rb +1 -1
- data/lib/dcc/server.rb +1 -1
- data/lib/dcc/si/base/complex.rb +1 -1
- data/lib/dcc/si/base/complex_list_xml_list.rb +1 -1
- data/lib/dcc/si/base/constant.rb +1 -1
- data/lib/dcc/si/base/coverage_interval.rb +36 -0
- data/lib/dcc/si/base/coverage_interval_mu.rb +1 -1
- data/lib/dcc/si/base/expanded_mu.rb +1 -1
- data/lib/dcc/si/base/expanded_unc.rb +34 -0
- data/lib/dcc/si/base/expanded_unc_xml_list.rb +3 -2
- data/lib/dcc/si/base/hybrid.rb +3 -2
- data/lib/dcc/si/base/measurement_uncertainty_univariate.rb +1 -1
- data/lib/dcc/si/base/real.rb +13 -3
- data/lib/dcc/si/base/real_list_xml_list.rb +5 -3
- data/lib/dcc/si/base/si_list.rb +3 -2
- data/lib/dcc/si/base/standard_mu.rb +1 -1
- data/lib/dcc/si/base.rb +5 -2
- data/lib/dcc/si/v1/coverage_interval.rb +8 -0
- data/lib/dcc/si/v1/expanded_unc.rb +1 -16
- data/lib/dcc/si/v1/measurement_uncertainty_univariate.rb +2 -1
- data/lib/dcc/si/v1.rb +5 -2
- data/lib/dcc/si/v2/coverage_interval.rb +8 -0
- data/lib/dcc/si/v2/coverage_interval_mu_xml_list.rb +4 -2
- data/lib/dcc/si/v2/expanded_mu_xml_list.rb +2 -1
- data/lib/dcc/si/v2/expanded_unc.rb +8 -0
- data/lib/dcc/si/v2/measurement_uncertainty_univariate.rb +2 -1
- data/lib/dcc/si/v2.rb +8 -3
- data/lib/dcc/signature/result.rb +2 -2
- data/lib/dcc/signature/signer.rb +31 -18
- data/lib/dcc/signature/verifier.rb +23 -21
- data/lib/dcc/signature.rb +1 -1
- data/lib/dcc/transform/result.rb +1 -1
- data/lib/dcc/transform/xslt.rb +22 -12
- data/lib/dcc/transform.rb +5 -4
- data/lib/dcc/type/base64_binary.rb +5 -3
- data/lib/dcc/type/decimal_xml_list.rb +99 -13
- data/lib/dcc/type/iso_country_code.rb +3 -2
- data/lib/dcc/type/iso_language_code.rb +3 -2
- data/lib/dcc/type/schema_version.rb +3 -2
- data/lib/dcc/type/si_unit.rb +3 -2
- data/lib/dcc/type_guards.rb +3 -3
- data/lib/dcc/v2/calibration_laboratory.rb +2 -1
- data/lib/dcc/v2/condition.rb +22 -1
- data/lib/dcc/v2/configuration.rb +1 -1
- data/lib/dcc/v2/digital_calibration_certificate.rb +2 -1
- data/lib/dcc/v2/identification.rb +14 -1
- data/lib/dcc/v2/mathml.rb +8 -0
- data/lib/dcc/v2/measurement_meta_data_list.rb +2 -1
- data/lib/dcc/v2/quantity.rb +4 -2
- data/lib/dcc/v2.rb +4 -2
- data/lib/dcc/v3/calibration_laboratory.rb +2 -1
- data/lib/dcc/v3/configuration.rb +1 -1
- data/lib/dcc/v3/digital_calibration_certificate.rb +2 -1
- data/lib/dcc/v3/mathml.rb +8 -0
- data/lib/dcc/v3/measurement_meta_data_list.rb +2 -1
- data/lib/dcc/v3/quantity.rb +4 -2
- data/lib/dcc/v3.rb +12 -2
- data/lib/dcc/validate/business_rules.rb +23 -9
- data/lib/dcc/validate/issue.rb +2 -1
- data/lib/dcc/validate/result.rb +6 -4
- data/lib/dcc/validate/schematron/profile.rb +73 -18
- data/lib/dcc/validate/schematron/rule.rb +13 -3
- data/lib/dcc/validate/schematron/rules/administrative_data_completeness.rb +11 -4
- data/lib/dcc/validate/schematron/rules/base.rb +1 -2
- data/lib/dcc/validate/schematron/rules/date_range_check.rb +11 -5
- data/lib/dcc/validate/schematron/rules/id_refid_linking.rb +0 -3
- data/lib/dcc/validate/schematron/rules/influence_conditions_placement.rb +22 -12
- data/lib/dcc/validate/schematron/rules/iso_code_validation.rb +31 -14
- data/lib/dcc/validate/schematron/rules/language_code_dedup.rb +12 -5
- data/lib/dcc/validate/schematron/rules/non_si_declaration.rb +7 -4
- data/lib/dcc/validate/schematron/rules/release_format_check.rb +7 -4
- data/lib/dcc/validate/schematron/rules/schema_version_check.rb +13 -9
- data/lib/dcc/validate/schematron/rules/uncertainty_consistency.rb +39 -10
- data/lib/dcc/validate/schematron/rules/unit_format_check.rb +2 -3
- data/lib/dcc/validate/schematron/rules/used_methods_placement.rb +15 -10
- data/lib/dcc/validate/schematron/rules/used_software_placement.rb +15 -10
- data/lib/dcc/validate/schematron/rules/xml_list_spacing.rb +4 -3
- data/lib/dcc/validate/schematron/rules.rb +28 -14
- data/lib/dcc/validate/schematron.rb +1 -1
- data/lib/dcc/validate/xsd.rb +9 -3
- data/lib/dcc/validate.rb +1 -1
- data/lib/dcc/version.rb +1 -1
- data/lib/dcc/versioned_parser.rb +1 -1
- data/lib/dcc.rb +55 -7
- data/sample_dcc.xml +140 -0
- data/web/index.html +12 -0
- data/web/package-lock.json +1599 -0
- data/web/package.json +22 -0
- data/web/src/App.vue +349 -0
- data/web/src/lib/builder.ts +171 -0
- data/web/src/lib/dcc.ts +338 -0
- data/web/src/lib/units.json +5581 -0
- data/web/src/main.ts +5 -0
- data/web/src/style.css +27 -0
- data/web/tsconfig.json +15 -0
- data/web/vite.config.ts +14 -0
- metadata +125 -9
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dcc
|
|
4
|
+
module Plugin
|
|
5
|
+
# Included by a plugin class so it can declare what it adds to Dcc.
|
|
6
|
+
#
|
|
7
|
+
# @example
|
|
8
|
+
# class MyPlugin
|
|
9
|
+
# include Dcc::Plugin::Base
|
|
10
|
+
# register_validator MyRule
|
|
11
|
+
# end
|
|
12
|
+
module Base
|
|
13
|
+
# @param base [Class] the including class.
|
|
14
|
+
def self.included(base)
|
|
15
|
+
base.extend(ClassMethods)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# `Profile#call` does `rule_class.new.check_on(dcc)`, so a validator
|
|
19
|
+
# has to be a class. An instance responds to `#check_on` and so looks
|
|
20
|
+
# right, but would fail deep inside validation with a message naming
|
|
21
|
+
# neither the plugin nor the author's line.
|
|
22
|
+
#
|
|
23
|
+
# @param entry [Object] the candidate rule.
|
|
24
|
+
# @raise [ArgumentError] unless entry is a class defining #check_on.
|
|
25
|
+
# @return [Class] the entry.
|
|
26
|
+
def self.rule_class!(entry)
|
|
27
|
+
return entry if rule_class?(entry)
|
|
28
|
+
|
|
29
|
+
raise ::ArgumentError,
|
|
30
|
+
"expected a rule class responding to #check_on, " \
|
|
31
|
+
"got #{describe(entry)}"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @param entry [Object]
|
|
35
|
+
# @return [Boolean]
|
|
36
|
+
def self.rule_class?(entry)
|
|
37
|
+
entry.is_a?(::Class) && entry.method_defined?(:check_on)
|
|
38
|
+
end
|
|
39
|
+
private_class_method :rule_class?
|
|
40
|
+
|
|
41
|
+
# @param entry [Object]
|
|
42
|
+
# @return [String]
|
|
43
|
+
def self.describe(entry)
|
|
44
|
+
return entry.inspect if entry.is_a?(::Class)
|
|
45
|
+
|
|
46
|
+
"an instance of #{entry.class}"
|
|
47
|
+
end
|
|
48
|
+
private_class_method :describe
|
|
49
|
+
|
|
50
|
+
# Declaration helpers available on the including class.
|
|
51
|
+
module ClassMethods
|
|
52
|
+
# Add a Schematron rule to the active validation profile.
|
|
53
|
+
#
|
|
54
|
+
# @param rule_class [Class] a rule class defining `#check_on(dcc)`.
|
|
55
|
+
# @raise [ArgumentError] unless rule_class is such a class.
|
|
56
|
+
# @return [Class] the rule class.
|
|
57
|
+
def register_validator(rule_class)
|
|
58
|
+
checked = ::Dcc::Plugin::Base.rule_class!(rule_class)
|
|
59
|
+
::Dcc::Plugin.register(:validators, checked)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
data/lib/dcc/plugin/registry.rb
CHANGED
|
@@ -9,12 +9,17 @@ module Dcc
|
|
|
9
9
|
@mutex = ::Mutex.new
|
|
10
10
|
|
|
11
11
|
class << self
|
|
12
|
+
# Idempotent per (category, entry): registering the same entry in
|
|
13
|
+
# the same category twice is a no-op, so a doubly-loaded plugin does
|
|
14
|
+
# not run its validators twice.
|
|
15
|
+
#
|
|
12
16
|
# @param category [Symbol]
|
|
13
17
|
# @param entry [Class, Object]
|
|
14
18
|
# @return [Class, Object]
|
|
15
19
|
def register(category, entry)
|
|
16
20
|
@mutex.synchronize do
|
|
17
|
-
@categories[category.to_sym]
|
|
21
|
+
entries = @categories[category.to_sym]
|
|
22
|
+
entries << entry unless entries.include?(entry)
|
|
18
23
|
entry
|
|
19
24
|
end
|
|
20
25
|
end
|
|
@@ -42,4 +47,4 @@ module Dcc
|
|
|
42
47
|
end
|
|
43
48
|
end
|
|
44
49
|
end
|
|
45
|
-
end
|
|
50
|
+
end
|
data/lib/dcc/plugin.rb
CHANGED
|
@@ -5,13 +5,17 @@
|
|
|
5
5
|
# without modifying the core codebase (Open/Closed Principle).
|
|
6
6
|
#
|
|
7
7
|
# @example Register a custom validator
|
|
8
|
-
#
|
|
8
|
+
# class MyPlugin
|
|
9
|
+
# include Dcc::Plugin::Base
|
|
10
|
+
# register_validator MyCustomRule
|
|
11
|
+
# end
|
|
9
12
|
#
|
|
10
13
|
# @example List all registered validators
|
|
11
14
|
# Dcc::Plugin.all(:validators)
|
|
12
15
|
module Dcc
|
|
13
16
|
module Plugin
|
|
14
17
|
autoload :Registry, "dcc/plugin/registry"
|
|
18
|
+
autoload :Base, "dcc/plugin/base"
|
|
15
19
|
|
|
16
20
|
class << self
|
|
17
21
|
# @param category [Symbol] e.g. :validators, :converters, :cli_commands.
|
|
@@ -39,4 +43,4 @@ module Dcc
|
|
|
39
43
|
end
|
|
40
44
|
end
|
|
41
45
|
end
|
|
42
|
-
end
|
|
46
|
+
end
|
|
@@ -45,25 +45,24 @@ module Dcc
|
|
|
45
45
|
private
|
|
46
46
|
|
|
47
47
|
def format_value
|
|
48
|
-
# Two decimals by default
|
|
49
|
-
|
|
48
|
+
# Two decimals by default, tightened to the uncertainty's precision
|
|
49
|
+
# when it has any. Neither a nil nor a NaN uncertainty has one.
|
|
50
|
+
return @value.round(2).to_s("F") if uncertainty.nil? || uncertainty.nan?
|
|
50
51
|
|
|
51
|
-
# Decimal places = 1 - floor(log10(uncertainty))
|
|
52
|
-
decimal_places = [0, 1 - Integer(Math.log10(uncertainty.to_f).floor)].max
|
|
53
52
|
@value.round(decimal_places).to_s("F")
|
|
54
53
|
end
|
|
55
54
|
|
|
56
55
|
def format_uncertainty
|
|
57
56
|
return "" if uncertainty.nil?
|
|
57
|
+
return "NaN" if uncertainty.nan?
|
|
58
58
|
|
|
59
|
-
decimal_places = [0, 1 - Integer(Math.log10(uncertainty.to_f).floor)].max
|
|
60
59
|
uncertainty.round(decimal_places).to_s("F")
|
|
61
60
|
end
|
|
62
61
|
|
|
63
62
|
def align_value_and_uncertainty
|
|
64
63
|
return [@value.to_s("F"), uncertainty.to_s("F")] if uncertainty.nil?
|
|
64
|
+
return [format_value, "NaN"] if uncertainty.nan?
|
|
65
65
|
|
|
66
|
-
decimal_places = [0, 1 - Integer(Math.log10(uncertainty.to_f).floor)].max
|
|
67
66
|
v_str = @value.round(decimal_places).to_s("F")
|
|
68
67
|
u_str = uncertainty.round(decimal_places).to_s("F")
|
|
69
68
|
# Short form: digits of uncertainty after the last common digit
|
|
@@ -71,6 +70,13 @@ module Dcc
|
|
|
71
70
|
[v_str, last_digits]
|
|
72
71
|
end
|
|
73
72
|
|
|
73
|
+
# Decimal places = 1 - floor(log10(uncertainty)). D-SI writes `NaN` for a
|
|
74
|
+
# not-measured entry (SI_Format.xsd:1195) and `Math.log10` cannot answer
|
|
75
|
+
# for it, so every caller excludes nil and NaN first.
|
|
76
|
+
def decimal_places
|
|
77
|
+
[0, 1 - Integer(Math.log10(uncertainty.to_f).floor)].max
|
|
78
|
+
end
|
|
79
|
+
|
|
74
80
|
def unit_suffix
|
|
75
81
|
return "" if unit.nil? || unit.empty?
|
|
76
82
|
|
|
@@ -81,10 +87,10 @@ module Dcc
|
|
|
81
87
|
unit
|
|
82
88
|
.to_s
|
|
83
89
|
.gsub(/\\([a-z]+)/, '\\1') # \kelvin → kelvin
|
|
84
|
-
.gsub(
|
|
85
|
-
.gsub(
|
|
86
|
-
.gsub(
|
|
90
|
+
.gsub("\\per", "/")
|
|
91
|
+
.gsub("\\cubic", "^3")
|
|
92
|
+
.gsub("\\square", "^2")
|
|
87
93
|
end
|
|
88
94
|
end
|
|
89
95
|
end
|
|
90
|
-
end
|
|
96
|
+
end
|
data/lib/dcc/quantity_format.rb
CHANGED
|
@@ -52,7 +52,7 @@ module Dcc
|
|
|
52
52
|
# @param exponent [Integer]
|
|
53
53
|
# @return [Real]
|
|
54
54
|
def **(exponent)
|
|
55
|
-
new_value = value
|
|
55
|
+
new_value = value**exponent
|
|
56
56
|
Real.new(
|
|
57
57
|
value: new_value,
|
|
58
58
|
unit: unit && "^#{exponent}",
|
|
@@ -75,7 +75,7 @@ module Dcc
|
|
|
75
75
|
return other.uncertainty if uncertainty.nil?
|
|
76
76
|
return uncertainty if other.uncertainty.nil?
|
|
77
77
|
|
|
78
|
-
sum_of_squares = (uncertainty
|
|
78
|
+
sum_of_squares = (uncertainty**2) + (other.uncertainty**2)
|
|
79
79
|
BigDecimal(Math.sqrt(sum_of_squares.to_f).to_s)
|
|
80
80
|
end
|
|
81
81
|
|
|
@@ -86,15 +86,15 @@ module Dcc
|
|
|
86
86
|
|
|
87
87
|
a_rel = uncertainty / value
|
|
88
88
|
b_rel = other.uncertainty / other.value
|
|
89
|
-
sum_of_squares = (a_rel
|
|
89
|
+
sum_of_squares = (a_rel**2) + (b_rel**2)
|
|
90
90
|
BigDecimal(Math.sqrt(sum_of_squares.to_f).to_s) * new_value.abs
|
|
91
91
|
end
|
|
92
92
|
|
|
93
93
|
# Power rule: σ_y = |n * x^(n-1)| * σ_x
|
|
94
|
-
def power_uncertainty(exponent,
|
|
94
|
+
def power_uncertainty(exponent, _new_value)
|
|
95
95
|
return nil unless uncertain?
|
|
96
96
|
|
|
97
|
-
derivative = exponent * (value
|
|
97
|
+
derivative = exponent * (value**(exponent - 1))
|
|
98
98
|
(derivative.abs * uncertainty)
|
|
99
99
|
end
|
|
100
100
|
|
|
@@ -113,4 +113,4 @@ module Dcc
|
|
|
113
113
|
end
|
|
114
114
|
end
|
|
115
115
|
end
|
|
116
|
-
end
|
|
116
|
+
end
|
data/lib/dcc/quantity_math.rb
CHANGED
data/lib/dcc/schema/version.rb
CHANGED
|
@@ -30,7 +30,7 @@ module Dcc
|
|
|
30
30
|
def normalize(v)
|
|
31
31
|
return nil if v.nil?
|
|
32
32
|
|
|
33
|
-
s = v.to_s.sub(/\Av/, "").gsub(
|
|
33
|
+
s = v.to_s.sub(/\Av/, "").gsub("_", ".")
|
|
34
34
|
s.empty? ? nil : s
|
|
35
35
|
end
|
|
36
36
|
|
|
@@ -56,7 +56,7 @@ module Dcc
|
|
|
56
56
|
# from the XML body.
|
|
57
57
|
def resolve_dcc(version, xml: nil)
|
|
58
58
|
v = normalize(version)
|
|
59
|
-
return detect_from_xml(xml) if v.nil? || v == "auto"
|
|
59
|
+
return detect_from_xml(xml) || DCC_LATEST if v.nil? || v == "auto"
|
|
60
60
|
return DCC_LATEST if v == "latest"
|
|
61
61
|
|
|
62
62
|
unless dcc?(v)
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
|
6
|
+
<title>DCC Validator</title>
|
|
7
|
+
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
|
8
|
+
<script src="https://unpkg.com/@tailwindcss/browser@4"></script>
|
|
9
|
+
<style>
|
|
10
|
+
[v-cloak] { display: none; }
|
|
11
|
+
@keyframes slideUp { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
|
|
12
|
+
.slide-up { animation: slideUp 0.4s ease-out both; }
|
|
13
|
+
.slide-up:nth-child(2) { animation-delay: .06s; }
|
|
14
|
+
.slide-up:nth-child(3) { animation-delay: .12s; }
|
|
15
|
+
.slide-up:nth-child(4) { animation-delay: .18s; }
|
|
16
|
+
.slide-up:nth-child(5) { animation-delay: .24s; }
|
|
17
|
+
.pulse-dot { animation: pd 2s ease-in-out infinite; }
|
|
18
|
+
@keyframes pd { 0%,100% { opacity:1; } 50% { opacity:.3; } }
|
|
19
|
+
</style>
|
|
20
|
+
</head>
|
|
21
|
+
<body class="bg-slate-950 text-slate-200 font-sans antialiased min-h-screen" style="font-family: 'Inter', system-ui, sans-serif;">
|
|
22
|
+
<div id="app" v-cloak>
|
|
23
|
+
|
|
24
|
+
<!-- Header bar -->
|
|
25
|
+
<div class="border-b border-slate-800 bg-slate-900/50 backdrop-blur sticky top-0 z-50">
|
|
26
|
+
<div class="max-w-5xl mx-auto px-6 py-3 flex items-center gap-4">
|
|
27
|
+
<div class="flex items-center gap-2">
|
|
28
|
+
<div class="w-8 h-8 rounded-lg bg-cyan-500/10 border border-cyan-500/30 flex items-center justify-center">
|
|
29
|
+
<span class="text-cyan-400 text-lg font-bold">D</span>
|
|
30
|
+
</div>
|
|
31
|
+
<span class="font-semibold text-slate-100">DCC Validator</span>
|
|
32
|
+
<span class="text-xs text-slate-500 hidden sm:inline">v{{ version }}</span>
|
|
33
|
+
</div>
|
|
34
|
+
<div class="flex-1"></div>
|
|
35
|
+
<button @click="loadDemo" class="px-3 py-1.5 text-xs font-medium rounded-lg border border-slate-700 hover:border-cyan-500/50 hover:text-cyan-400 transition-colors">
|
|
36
|
+
Load Demo Certificate
|
|
37
|
+
</button>
|
|
38
|
+
<label class="cursor-pointer px-3 py-1.5 text-xs font-medium rounded-lg bg-cyan-500/10 border border-cyan-500/30 text-cyan-400 hover:bg-cyan-500/20 transition-colors">
|
|
39
|
+
Upload DCC XML
|
|
40
|
+
<input type="file" accept=".xml,.dcc" class="hidden" @change="onUpload($event)">
|
|
41
|
+
</label>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<!-- Drop zone (when no cert loaded) -->
|
|
46
|
+
<div v-if="!cert && !loading" class="max-w-2xl mx-auto px-6 pt-24">
|
|
47
|
+
<div
|
|
48
|
+
@dragover.prevent="dragActive = true"
|
|
49
|
+
@dragleave.prevent="dragActive = false"
|
|
50
|
+
@drop.prevent="onDrop($event)"
|
|
51
|
+
:class="['border-2 border-dashed rounded-2xl p-16 text-center transition-all', dragActive ? 'border-cyan-500 bg-cyan-500/5' : 'border-slate-800']"
|
|
52
|
+
>
|
|
53
|
+
<div class="text-5xl mb-4 opacity-30">📋</div>
|
|
54
|
+
<p class="text-slate-400 mb-1">Drag a DCC XML file here</p>
|
|
55
|
+
<p class="text-slate-600 text-sm">or click upload above</p>
|
|
56
|
+
<button @click="loadDemo" class="mt-6 px-4 py-2 text-sm font-medium rounded-lg bg-cyan-500/10 border border-cyan-500/30 text-cyan-400 hover:bg-cyan-500/20 transition-colors">
|
|
57
|
+
Try a demo certificate →
|
|
58
|
+
</button>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<!-- Loading -->
|
|
63
|
+
<div v-if="loading" class="max-w-2xl mx-auto px-6 pt-32 text-center">
|
|
64
|
+
<div class="inline-block w-8 h-8 border-2 border-cyan-500/30 border-t-cyan-500 rounded-full animate-spin mb-4"></div>
|
|
65
|
+
<p class="text-slate-400 text-sm">Validating certificate...</p>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<!-- Certificate view -->
|
|
69
|
+
<div v-if="cert && !loading" class="max-w-5xl mx-auto px-6 py-8 space-y-4">
|
|
70
|
+
|
|
71
|
+
<!-- Validity banner -->
|
|
72
|
+
<div :class="['rounded-2xl p-5 border slide-up flex items-center gap-4',
|
|
73
|
+
validation.ok ? 'bg-green-500/5 border-green-500/20' : 'bg-red-500/5 border-red-500/20']">
|
|
74
|
+
<div :class="['w-12 h-12 rounded-xl flex items-center justify-center text-2xl',
|
|
75
|
+
validation.ok ? 'bg-green-500/10' : 'bg-red-500/10']">
|
|
76
|
+
{{ validation.ok ? '✓' : '✕' }}
|
|
77
|
+
</div>
|
|
78
|
+
<div class="flex-1">
|
|
79
|
+
<div :class="['font-semibold text-lg', validation.ok ? 'text-green-400' : 'text-red-400']">
|
|
80
|
+
{{ validation.ok ? 'Certificate Valid' : 'Validation Failed' }}
|
|
81
|
+
</div>
|
|
82
|
+
<div class="text-sm text-slate-500">
|
|
83
|
+
{{ validation.errors.length }} error{{ validation.errors.length === 1 ? '' : 's' }},
|
|
84
|
+
{{ validation.warnings.length }} warning{{ validation.warnings.length === 1 ? '' : 's' }}
|
|
85
|
+
<span v-if="cert.schemaVersion"> · Schema v{{ cert.schemaVersion }}</span>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
<div class="flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-medium"
|
|
89
|
+
:class="validation.ok ? 'bg-green-500/10 text-green-400 border border-green-500/20' : 'bg-red-500/10 text-red-400 border border-red-500/20'">
|
|
90
|
+
<span class="w-1.5 h-1.5 rounded-full pulse-dot" :class="validation.ok ? 'bg-green-400' : 'bg-red-400'"></span>
|
|
91
|
+
{{ validation.ok ? 'VALID' : 'INVALID' }}
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<!-- Errors list -->
|
|
96
|
+
<div v-if="validation.errors.length" class="slide-up rounded-xl border border-red-500/15 bg-red-500/[0.03] overflow-hidden">
|
|
97
|
+
<div class="px-5 py-3 border-b border-red-500/10 text-xs font-semibold uppercase tracking-wide text-red-400/80">Validation Errors</div>
|
|
98
|
+
<div class="divide-y divide-red-500/5">
|
|
99
|
+
<div v-for="e in validation.errors" :key="e.line" class="px-5 py-3 flex gap-3">
|
|
100
|
+
<span class="text-red-500/50 text-xs font-mono pt-0.5">{{ e.line ? 'L'+e.line : '—' }}</span>
|
|
101
|
+
<span class="text-sm text-slate-300">{{ e.message }}</span>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
<!-- Certificate identity -->
|
|
107
|
+
<div class="slide-up rounded-2xl border border-slate-800 bg-slate-900 overflow-hidden">
|
|
108
|
+
<div class="bg-gradient-to-br from-slate-900 to-slate-800/50 p-6 relative overflow-hidden">
|
|
109
|
+
<div class="absolute top-0 right-0 w-64 h-64 bg-cyan-500/5 rounded-full blur-3xl"></div>
|
|
110
|
+
<div class="relative flex justify-between items-start gap-4">
|
|
111
|
+
<div>
|
|
112
|
+
<div class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-[10px] font-semibold uppercase tracking-wider border border-cyan-500/30 text-cyan-400 bg-cyan-500/5 mb-3">
|
|
113
|
+
<span class="w-1 h-1 rounded-full bg-cyan-400 pulse-dot"></span>
|
|
114
|
+
Digital Calibration Certificate
|
|
115
|
+
</div>
|
|
116
|
+
<h1 class="text-2xl font-bold text-slate-100 mb-1">{{ cert.itemName || 'Calibration Certificate' }}</h1>
|
|
117
|
+
<p class="text-slate-400">{{ cert.labName || '' }}</p>
|
|
118
|
+
<p class="font-mono text-xs text-slate-600 mt-1.5">ID: {{ cert.uniqueId || '—' }}</p>
|
|
119
|
+
</div>
|
|
120
|
+
<div class="text-right border-l border-slate-700 pl-6">
|
|
121
|
+
<div class="text-[10px] uppercase tracking-wider text-slate-500">Schema</div>
|
|
122
|
+
<div class="font-mono text-green-400 font-medium">v{{ cert.schemaVersion }}</div>
|
|
123
|
+
<div class="mt-2 text-[10px] uppercase tracking-wider text-slate-500">Country</div>
|
|
124
|
+
<div class="font-mono text-lg">{{ cert.country || '—' }}</div>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
<!-- Core data -->
|
|
131
|
+
<div v-if="cert.coreData" class="slide-up rounded-xl border border-slate-800 bg-slate-900 overflow-hidden">
|
|
132
|
+
<div class="px-5 py-3 border-b border-slate-800 flex items-center gap-2">
|
|
133
|
+
<span class="text-slate-500">⚙</span>
|
|
134
|
+
<span class="text-xs font-semibold uppercase tracking-wide text-slate-500">Core Data</span>
|
|
135
|
+
</div>
|
|
136
|
+
<div class="p-5 grid grid-cols-2 sm:grid-cols-3 gap-px bg-slate-800/50">
|
|
137
|
+
<div class="bg-slate-900 p-3" v-for="f in cert.coreData">
|
|
138
|
+
<div class="text-[10px] uppercase tracking-wider text-slate-600 mb-0.5">{{ f.label }}</div>
|
|
139
|
+
<div class="font-mono text-sm text-slate-200">{{ f.value }}</div>
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
<!-- Measurement results -->
|
|
145
|
+
<div v-if="cert.measurements && cert.measurements.length" class="slide-up rounded-xl border border-slate-800 bg-slate-900 overflow-hidden">
|
|
146
|
+
<div class="px-5 py-3 border-b border-slate-800 flex items-center gap-2">
|
|
147
|
+
<span class="text-slate-500">⚡</span>
|
|
148
|
+
<span class="text-xs font-semibold uppercase tracking-wide text-slate-500">Measurement Results</span>
|
|
149
|
+
</div>
|
|
150
|
+
<div class="p-5 space-y-3">
|
|
151
|
+
<div v-for="mr in cert.measurements" :key="mr.name" class="rounded-lg border border-slate-800 bg-slate-800/40 p-4">
|
|
152
|
+
<div class="font-medium text-slate-200 mb-3">{{ mr.name }}</div>
|
|
153
|
+
<div v-if="mr.results && mr.results.length" class="space-y-2">
|
|
154
|
+
<div v-for="r in mr.results" :key="r.name" class="flex items-center justify-between py-2 border-l-2 border-cyan-500/30 pl-3 bg-cyan-500/[0.03] rounded-r-lg">
|
|
155
|
+
<span class="text-sm text-slate-400">{{ r.name }}</span>
|
|
156
|
+
<span v-if="r.values" class="font-mono text-sm">
|
|
157
|
+
<span v-for="(v, i) in r.values" :key="i" class="ml-2">
|
|
158
|
+
<span class="text-green-400">{{ v.value }}</span>
|
|
159
|
+
<span class="text-slate-600"> {{ v.unit }}</span>
|
|
160
|
+
<span v-if="v.uncertainty" class="text-amber-400/60 text-xs"> ±{{ v.uncertainty }}</span>
|
|
161
|
+
</span>
|
|
162
|
+
</span>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
|
|
169
|
+
<!-- Lab / Signer -->
|
|
170
|
+
<div class="slide-up grid sm:grid-cols-2 gap-4">
|
|
171
|
+
<div v-if="cert.lab" class="rounded-xl border border-slate-800 bg-slate-900 overflow-hidden">
|
|
172
|
+
<div class="px-5 py-3 border-b border-slate-800 text-xs font-semibold uppercase tracking-wide text-slate-500">Calibration Laboratory</div>
|
|
173
|
+
<div class="p-5">
|
|
174
|
+
<div class="font-medium text-slate-200 mb-1">{{ cert.lab.name }}</div>
|
|
175
|
+
<div v-if="cert.lab.email" class="font-mono text-xs text-slate-500">{{ cert.lab.email }}</div>
|
|
176
|
+
<div v-if="cert.lab.address" class="text-sm text-slate-400 mt-2">{{ cert.lab.address }}</div>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
<div v-if="cert.signers && cert.signers.length" class="rounded-xl border border-slate-800 bg-slate-900 overflow-hidden">
|
|
180
|
+
<div class="px-5 py-3 border-b border-slate-800 text-xs font-semibold uppercase tracking-wide text-slate-500">Responsible Persons</div>
|
|
181
|
+
<div class="p-5 space-y-2">
|
|
182
|
+
<div v-for="s in cert.signers" :key="s.name" class="flex items-center gap-3 rounded-lg border border-slate-800 bg-slate-800/40 px-3 py-2">
|
|
183
|
+
<div class="w-8 h-8 rounded-full bg-cyan-500 text-slate-900 flex items-center justify-center text-xs font-bold">{{ initials(s.name) }}</div>
|
|
184
|
+
<div class="flex-1">
|
|
185
|
+
<div class="text-sm font-medium text-slate-200">{{ s.name }}</div>
|
|
186
|
+
<div v-if="s.email" class="font-mono text-xs text-slate-500">{{ s.email }}</div>
|
|
187
|
+
</div>
|
|
188
|
+
<span v-if="s.main" class="px-2 py-0.5 rounded-full text-[10px] font-semibold uppercase tracking-wider text-green-400 border border-green-500/20 bg-green-500/10">Main</span>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
<!-- XML source -->
|
|
195
|
+
<div class="slide-up rounded-xl border border-slate-800 bg-slate-900 overflow-hidden">
|
|
196
|
+
<button @click="showXml = !showXml" class="w-full px-5 py-3 flex items-center justify-between text-xs font-semibold uppercase tracking-wide text-slate-500 hover:bg-slate-800/50 transition-colors">
|
|
197
|
+
<span>XML Source</span>
|
|
198
|
+
<span>{{ showXml ? '▲' : '▼' }}</span>
|
|
199
|
+
</button>
|
|
200
|
+
<div v-if="showXml" class="p-5 overflow-auto max-h-96">
|
|
201
|
+
<pre class="text-xs text-slate-400 font-mono whitespace-pre-wrap">{{ rawXml }}</pre>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
|
|
208
|
+
<script>
|
|
209
|
+
(function() {
|
|
210
|
+
const { createApp, ref, computed } = Vue;
|
|
211
|
+
|
|
212
|
+
createApp({
|
|
213
|
+
setup() {
|
|
214
|
+
const cert = ref(null);
|
|
215
|
+
const loading = ref(false);
|
|
216
|
+
const dragActive = ref(false);
|
|
217
|
+
const showXml = ref(false);
|
|
218
|
+
const rawXml = ref('');
|
|
219
|
+
const validation = ref({ ok: true, errors: [], warnings: [] });
|
|
220
|
+
const version = ref('0.2.0');
|
|
221
|
+
|
|
222
|
+
async function validate(xml) {
|
|
223
|
+
loading.value = true;
|
|
224
|
+
try {
|
|
225
|
+
const res = await fetch('/validate', {
|
|
226
|
+
method: 'POST',
|
|
227
|
+
headers: { 'Content-Type': 'application/xml' },
|
|
228
|
+
body: xml,
|
|
229
|
+
});
|
|
230
|
+
const data = await res.json();
|
|
231
|
+
cert.value = data.cert;
|
|
232
|
+
validation.value = data.validation;
|
|
233
|
+
rawXml.value = xml;
|
|
234
|
+
} catch (e) {
|
|
235
|
+
validation.value = { ok: false, errors: [{ line: null, message: 'Server error: ' + e.message }], warnings: [] };
|
|
236
|
+
}
|
|
237
|
+
loading.value = false;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function loadDemo() {
|
|
241
|
+
loading.value = true;
|
|
242
|
+
try {
|
|
243
|
+
const res = await fetch('/demo.xml');
|
|
244
|
+
const xml = await res.text();
|
|
245
|
+
await validate(xml);
|
|
246
|
+
} catch (e) {
|
|
247
|
+
loading.value = false;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function onUpload(e) {
|
|
252
|
+
const file = e.target.files[0];
|
|
253
|
+
if (!file) return;
|
|
254
|
+
const reader = new FileReader();
|
|
255
|
+
reader.onload = (ev) => validate(ev.target.result);
|
|
256
|
+
reader.readAsText(file);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function onDrop(e) {
|
|
260
|
+
dragActive.value = false;
|
|
261
|
+
const file = e.dataTransfer.files[0];
|
|
262
|
+
if (!file) return;
|
|
263
|
+
const reader = new FileReader();
|
|
264
|
+
reader.onload = (ev) => validate(ev.target.result);
|
|
265
|
+
reader.readAsText(file);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function initials(name) {
|
|
269
|
+
if (!name) return '?';
|
|
270
|
+
const parts = name.trim().split(/\s+/);
|
|
271
|
+
if (parts.length === 1) return parts[0][0].toUpperCase();
|
|
272
|
+
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return { cert, loading, dragActive, showXml, rawXml, validation, version,
|
|
276
|
+
loadDemo, onUpload, onDrop, initials };
|
|
277
|
+
}
|
|
278
|
+
}).mount('#app');
|
|
279
|
+
})();
|
|
280
|
+
</script>
|
|
281
|
+
</body>
|
|
282
|
+
</html>
|