metanorma-plugin-glossarist 0.3.0 → 0.3.2

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.
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Metanorma
4
+ module Plugin
5
+ module Glossarist
6
+ class ConceptFilter
7
+ COLLECTION_FILTERS = %w[lang domain group sort_by].freeze
8
+ SORT_LAST = ["￿"].freeze
9
+
10
+ def initialize(filters)
11
+ @filters = filters || {}
12
+ end
13
+
14
+ def apply(collection)
15
+ result = collection
16
+ result = filter_by_lang(result) if @filters.key?("lang")
17
+ if @filters.key?("domain") || @filters.key?("group")
18
+ result = filter_by_domain(result)
19
+ end
20
+ result = filter_by_field(result) if field_filter?
21
+ result = sort(result) if @filters.key?("sort_by")
22
+ result
23
+ end
24
+
25
+ private
26
+
27
+ def field_filter?
28
+ (@filters.keys - COLLECTION_FILTERS).any?
29
+ end
30
+
31
+ def field_filter_key
32
+ (@filters.keys - COLLECTION_FILTERS).first
33
+ end
34
+
35
+ def filter_by_lang(collection)
36
+ lang = @filters["lang"]
37
+ collection.reject { |c| c.localization(lang).nil? }
38
+ end
39
+
40
+ def filter_by_domain(collection)
41
+ domain = @filters["domain"] || @filters["group"]
42
+ collection.select do |c|
43
+ c.data.domains&.any? { |d| d.concept_id == domain }
44
+ end
45
+ end
46
+
47
+ def sort(collection)
48
+ field = @filters["sort_by"]
49
+ return collection unless field
50
+
51
+ case field
52
+ when "term", "default_designation"
53
+ collection.sort_by { |c| c.default_designation.to_s.downcase }
54
+ else
55
+ parts = parse_path(field)
56
+ collection.sort_by { |c| sort_key(c, parts) }
57
+ end
58
+ end
59
+
60
+ def sort_key(concept, parts)
61
+ hash = ConceptSerializer.new(concept).to_h
62
+ value = dig_path(hash, parts)
63
+ value.nil? ? SORT_LAST : natural_sort_key(value.to_s)
64
+ end
65
+
66
+ def natural_sort_key(str)
67
+ str.scan(/(\d+)|(\D+)/)
68
+ .map { |num, txt| num ? num.to_i : txt.downcase }
69
+ end
70
+
71
+ def filter_by_field(collection)
72
+ path = field_filter_key
73
+ value = @filters[path]
74
+
75
+ start_with = path.include?(".start_with(")
76
+ path, match_value = extract_start_with(path, value, start_with)
77
+
78
+ parts = parse_path(path)
79
+ collection.select do |concept|
80
+ hash = ConceptSerializer.new(concept).to_h
81
+ actual = dig_path(hash, parts)
82
+ if start_with
83
+ actual&.start_with?(match_value)
84
+ else
85
+ actual == value
86
+ end
87
+ end
88
+ end
89
+
90
+ def extract_start_with(path, value, start_with)
91
+ return [path, value] unless start_with
92
+
93
+ match = path.match(/^([^.]+(?:\.[^.]+)*)\.start_with\(([^)]+)\)$/)
94
+ return [path, value] unless match
95
+
96
+ [match[1], match[2]]
97
+ end
98
+
99
+ def parse_path(path)
100
+ path.split(".").flat_map do |segment|
101
+ if segment.include?("[")
102
+ parse_indexed_segment(segment)
103
+ else
104
+ [segment]
105
+ end
106
+ end
107
+ end
108
+
109
+ def parse_indexed_segment(segment)
110
+ field, index_part = segment.split("[", 2)
111
+ index = index_part&.delete("]'\"")
112
+ if index.match?(/\A\d+\z/)
113
+ [field, index.to_i]
114
+ else
115
+ [field, index]
116
+ end
117
+ end
118
+
119
+ def dig_path(hash, parts)
120
+ parts.reduce(hash) do |current, key|
121
+ case current
122
+ when Hash
123
+ current[key] || current[key.to_s]
124
+ when Array
125
+ key.is_a?(Integer) ? current[key] : nil
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Metanorma
4
+ module Plugin
5
+ module Glossarist
6
+ class ConceptRenderer
7
+ TERM_TYPES = %w[preferred admitted deprecated].freeze
8
+
9
+ def initialize(concept, depth:, anchor_prefix: nil)
10
+ @concept = concept
11
+ @depth = depth
12
+ @anchor_prefix = anchor_prefix
13
+ end
14
+
15
+ def render
16
+ sections = [concept_header]
17
+ sections << alt_terms_section
18
+ sections << definition_section
19
+ sections << examples_section
20
+ sections << notes_section
21
+ sections << sources_section
22
+ sections.compact.join("\n\n")
23
+ end
24
+
25
+ private
26
+
27
+ def eng_l10n
28
+ @eng_l10n ||= @concept.localization("eng")
29
+ end
30
+
31
+ def concept_header
32
+ "[[#{anchor_id}]]\n#{heading_line}"
33
+ end
34
+
35
+ def anchor_id
36
+ id = "#{@anchor_prefix}#{@concept.data.id}"
37
+ id.match?(/\A\d/) ? id : Metanorma::Utils.to_ncname(id.gsub(":", "_"))
38
+ end
39
+
40
+ def heading_line
41
+ "#{'=' * (@depth + 1)} #{term_designation}"
42
+ end
43
+
44
+ def term_designation
45
+ eng_l10n.terms.first&.designation.to_s
46
+ end
47
+
48
+ def alt_terms_section
49
+ terms = eng_l10n.terms[1..].map do |term|
50
+ type = TERM_TYPES.include?(term.normative_status) ? term.normative_status : "alt"
51
+ "#{type}:[#{term.designation}]"
52
+ end
53
+ terms.empty? ? nil : terms.join("\n")
54
+ end
55
+
56
+ def definition_section
57
+ content = eng_l10n.definition.first&.content
58
+ return nil unless content
59
+
60
+ Sanitize.references(content.to_s)
61
+ end
62
+
63
+ def examples_section
64
+ examples = eng_l10n.examples.map do |example|
65
+ "[example]\n#{Sanitize.references(example.content.to_s)}"
66
+ end
67
+ examples.empty? ? nil : examples.join("\n")
68
+ end
69
+
70
+ def notes_section
71
+ notes = eng_l10n.notes.map do |note|
72
+ "[NOTE]\n====\n#{Sanitize.references(note.content.to_s)}\n===="
73
+ end
74
+ notes.empty? ? nil : notes.join("\n")
75
+ end
76
+
77
+ def sources_section
78
+ sources = eng_l10n.sources.filter_map do |source|
79
+ next if source.origin&.text.nil? || source.origin.text.empty?
80
+ next unless source.origin.locality&.type == "clause"
81
+
82
+ ref = source.origin.text.gsub(%r{[ /:]}, "_")
83
+ clause = source.origin.locality.reference_from
84
+ "[.source]\n<<#{ref},#{clause}>>"
85
+ end
86
+ sources.empty? ? nil : sources.join("\n")
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Metanorma
4
+ module Plugin
5
+ module Glossarist
6
+ class ConceptSerializer
7
+ def initialize(concept)
8
+ @concept = concept
9
+ end
10
+
11
+ def to_h
12
+ data = @concept.data.to_hash
13
+ data["localizations"] = localizations_hash unless @concept.localizations.empty?
14
+ { "data" => data.compact }
15
+ end
16
+
17
+ private
18
+
19
+ def localizations_hash
20
+ @concept.localizations.to_h do |l10n|
21
+ [l10n.language_code, l10n.to_hash]
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end