glossarist 2.6.2 → 2.6.4

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,253 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tmpdir"
4
+ require_relative "import_result"
5
+
6
+ module Glossarist
7
+ module Sts
8
+ class Importer
9
+ STRATEGIES = %i[skip replace merge].freeze
10
+
11
+ attr_reader :duplicate_strategy
12
+
13
+ def initialize(duplicate_strategy: :skip)
14
+ unless STRATEGIES.include?(duplicate_strategy)
15
+ raise ArgumentError,
16
+ "duplicate_strategy must be one of #{STRATEGIES.join(', ')}, got #{duplicate_strategy}"
17
+ end
18
+
19
+ @duplicate_strategy = duplicate_strategy
20
+ @mapper = TermMapper.new
21
+ end
22
+
23
+ def import_new(xml_files, output:, shortname: nil, version: nil, **opts)
24
+ raw_concepts = extract_all_concepts(xml_files)
25
+ concepts, conflicts, skipped = dedup_concepts(raw_concepts)
26
+
27
+ if output.end_with?(".gcr")
28
+ unless shortname
29
+ raise ArgumentError,
30
+ "--shortname is required for GCR output"
31
+ end
32
+ unless version
33
+ raise ArgumentError,
34
+ "--version is required for GCR output"
35
+ end
36
+
37
+ create_gcr(concepts, output, shortname: shortname, version: version,
38
+ **opts)
39
+ else
40
+ save_dataset(concepts, output)
41
+ end
42
+
43
+ ImportResult.new(
44
+ concepts: concepts,
45
+ conflicts: conflicts,
46
+ source_files: xml_files,
47
+ skipped_count: skipped,
48
+ )
49
+ end
50
+
51
+ def import_into_existing(xml_files, dataset_path)
52
+ existing = load_existing(dataset_path)
53
+ new_concepts = extract_all_concepts(xml_files)
54
+ index = build_concept_index(existing)
55
+
56
+ result_state = apply_with_dedup(new_concepts, existing, index)
57
+
58
+ save_to_path(existing, dataset_path)
59
+
60
+ ImportResult.new(
61
+ concepts: existing.managed_concepts,
62
+ conflicts: result_state.conflicts,
63
+ source_files: xml_files,
64
+ skipped_count: result_state.skipped,
65
+ )
66
+ end
67
+
68
+ DedupState = Struct.new(:conflicts, :skipped, keyword_init: true)
69
+
70
+ private
71
+
72
+ def apply_with_dedup(new_concepts, existing, index)
73
+ state = DedupState.new(conflicts: [], skipped: 0)
74
+
75
+ new_concepts.each do |mc|
76
+ key = concept_key(mc)
77
+ existing_mc = index[key]
78
+
79
+ if existing_mc.nil?
80
+ existing.store(mc)
81
+ index[key] = mc
82
+ else
83
+ state.conflicts << DuplicateConflict.new(
84
+ new_concept: mc, existing_concept: existing_mc, key: key,
85
+ )
86
+ handle_duplicate(existing, existing_mc, mc, index, key, state)
87
+ end
88
+ end
89
+
90
+ state
91
+ end
92
+
93
+ def handle_duplicate(existing, old_mc, new_mc, index, key, state)
94
+ case duplicate_strategy
95
+ when :skip
96
+ state.skipped += 1
97
+ when :replace
98
+ replace_in_collection(existing, old_mc, new_mc)
99
+ index[key] = new_mc
100
+ when :merge
101
+ merge_concept(old_mc, new_mc)
102
+ end
103
+ end
104
+
105
+ def extract_all_concepts(xml_files)
106
+ xml_files.flat_map do |path|
107
+ extractor = TermExtractor.new(path)
108
+ terms = extractor.extract
109
+ terms.map { |t| @mapper.map(t) }
110
+ end
111
+ end
112
+
113
+ def dedup_concepts(concepts) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
114
+ seen = {}
115
+ conflicts = []
116
+ skipped = 0
117
+ unique = []
118
+
119
+ concepts.each do |mc|
120
+ key = concept_key(mc)
121
+ if key.first.empty? || seen[key].nil?
122
+ unique << mc
123
+ seen[key] = mc unless key.first.empty?
124
+ else
125
+ conflicts << DuplicateConflict.new(
126
+ new_concept: mc, existing_concept: seen[key], key: key,
127
+ )
128
+ skipped += apply_dedup_to_unique(unique, seen, mc, key)
129
+ end
130
+ end
131
+
132
+ [unique, conflicts, skipped]
133
+ end
134
+
135
+ def apply_dedup_to_unique(unique, seen, new_mc, key)
136
+ case duplicate_strategy
137
+ when :skip
138
+ 1
139
+ when :replace
140
+ unique.delete(seen[key])
141
+ unique << new_mc
142
+ seen[key] = new_mc
143
+ 0
144
+ when :merge
145
+ merge_concept(seen[key], new_mc)
146
+ 0
147
+ end
148
+ end
149
+
150
+ def concept_key(managed_concept)
151
+ designation = managed_concept.default_designation.to_s.downcase.strip
152
+ domain = begin
153
+ l10n = managed_concept.default_lang
154
+ l10n&.data&.domain.to_s.downcase.strip
155
+ end
156
+ [designation, domain]
157
+ end
158
+
159
+ def build_concept_index(collection)
160
+ index = {}
161
+ collection.each do |mc|
162
+ key = concept_key(mc)
163
+ index[key] = mc unless key.first.empty?
164
+ end
165
+ index
166
+ end
167
+
168
+ def merge_concept(existing_mc, new_mc)
169
+ new_mc.localizations.each do |l10n|
170
+ lang = l10n.language_code
171
+ if existing_mc.localization(lang).nil?
172
+ existing_mc.add_localization(l10n)
173
+ end
174
+ end
175
+ end
176
+
177
+ def replace_in_collection(collection, old_mc, new_mc)
178
+ collection.managed_concepts.delete(old_mc)
179
+ collection.store(new_mc)
180
+ end
181
+
182
+ def load_existing(path)
183
+ collection = ManagedConceptCollection.new
184
+ if path.end_with?(".gcr")
185
+ package = GcrPackage.load(path)
186
+ package.concepts.each { |mc| collection.store(mc) }
187
+ else
188
+ concepts = ConceptCollector.collect(path)
189
+ concepts.each { |mc| collection.store(mc) }
190
+ end
191
+ collection
192
+ end
193
+
194
+ def save_to_path(collection, path)
195
+ if path.end_with?(".gcr")
196
+ tmpdir = build_temp_dataset(collection.managed_concepts)
197
+ begin
198
+ GC.start
199
+ tmp_gcr = "#{path}.tmp.#{Process.pid}"
200
+ GcrPackage.create_from_directory(
201
+ tmpdir,
202
+ output: tmp_gcr,
203
+ shortname: File.basename(path, ".gcr"),
204
+ version: "1.0.0",
205
+ )
206
+ FileUtils.rm_f(path)
207
+ FileUtils.mv(tmp_gcr, path)
208
+ ensure
209
+ FileUtils.rm_rf(tmpdir)
210
+ FileUtils.rm_f(tmp_gcr)
211
+ end
212
+ else
213
+ save_dataset(collection.managed_concepts, path)
214
+ end
215
+ end
216
+
217
+ def save_dataset(concepts, dir)
218
+ concepts_dir = File.join(dir, "concepts")
219
+ FileUtils.mkdir_p(concepts_dir)
220
+ collection = ManagedConceptCollection.new
221
+ concepts.each { |mc| collection.store(mc) }
222
+ collection.save_grouped_concepts_to_files(concepts_dir)
223
+ end
224
+
225
+ def create_gcr(concepts, output, shortname:, version:, **opts)
226
+ tmpdir = build_temp_dataset(concepts)
227
+ begin
228
+ GcrPackage.create_from_directory(
229
+ tmpdir,
230
+ output: output,
231
+ shortname: shortname,
232
+ version: version,
233
+ **opts,
234
+ )
235
+ ensure
236
+ FileUtils.rm_rf(tmpdir)
237
+ end
238
+ end
239
+
240
+ def build_temp_dataset(concepts)
241
+ tmpdir = Dir.mktmpdir("glossarist-sts-import")
242
+ concepts_dir = File.join(tmpdir, "concepts")
243
+ FileUtils.mkdir_p(concepts_dir)
244
+
245
+ collection = ManagedConceptCollection.new
246
+ concepts.each { |mc| collection.store(mc) }
247
+ collection.save_grouped_concepts_to_files(concepts_dir)
248
+
249
+ tmpdir
250
+ end
251
+ end
252
+ end
253
+ end
@@ -0,0 +1,186 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Glossarist
4
+ module Sts
5
+ class TermExtractor
6
+ def initialize(xml_path)
7
+ raw = File.read(xml_path)
8
+ @standard = ::Sts::IsoSts::Standard.from_xml(raw)
9
+ @source_ref = extract_source_ref
10
+ end
11
+
12
+ def extract
13
+ term_secs = collect_term_secs
14
+ term_secs.filter_map do |ts|
15
+ next unless ts.term_entry
16
+
17
+ build_extracted_term(ts)
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def collect_term_secs
24
+ secs = []
25
+ walk_sections(@standard.body, secs) if @standard.body
26
+ secs
27
+ end
28
+
29
+ def walk_sections(container, collected)
30
+ collect_term_secs_from(container, collected)
31
+ walk_child_secs(container, collected)
32
+ end
33
+
34
+ def collect_term_secs_from(container, collected)
35
+ secs = container.term_sec
36
+ secs&.each do |ts|
37
+ collected << ts
38
+ walk_sections(ts, collected) if ts.term_sec&.any?
39
+ end
40
+ end
41
+
42
+ def walk_child_secs(container, collected)
43
+ secs = container_child_secs(container)
44
+ secs&.each { |s| walk_sections(s, collected) }
45
+ end
46
+
47
+ def container_child_secs(container)
48
+ case container
49
+ when ::Sts::IsoSts::Body, ::Sts::IsoSts::Sec
50
+ container.sec
51
+ end
52
+ end
53
+
54
+ def build_extracted_term(term_sec)
55
+ entry = term_sec.term_entry
56
+ label_text = extract_label(term_sec)
57
+
58
+ lang_sets = entry.lang_set.filter_map do |ls|
59
+ build_lang_set(ls)
60
+ end
61
+
62
+ Sts::ExtractedTerm.new(
63
+ id: entry.id,
64
+ label: label_text,
65
+ source_ref: @source_ref,
66
+ lang_sets: lang_sets,
67
+ )
68
+ end
69
+
70
+ def extract_label(term_sec)
71
+ label = term_sec.label
72
+ return nil unless label
73
+
74
+ label.content&.join.to_s.strip
75
+ end
76
+
77
+ def build_lang_set(lang_set) # rubocop:disable Metrics/AbcSize
78
+ lang_code = Sts.convert_language_code(lang_set.lang.to_s)
79
+
80
+ Sts::ExtractedLangSet.new(
81
+ language_code: lang_code,
82
+ definition_text: extract_definition_text(lang_set),
83
+ note_texts: extract_note_texts(lang_set),
84
+ example_texts: extract_example_texts(lang_set),
85
+ source_texts: extract_source_texts(lang_set),
86
+ domain: extract_subject_field(lang_set),
87
+ designations: lang_set.tig.filter_map do |tig|
88
+ build_designation(tig)
89
+ end,
90
+ )
91
+ end
92
+
93
+ def extract_definition_text(lang_set)
94
+ definitions = lang_set.definition
95
+ return "" unless definitions&.any?
96
+
97
+ definitions.first.value&.join.to_s.strip
98
+ end
99
+
100
+ def extract_note_texts(lang_set)
101
+ lang_set.note.filter_map do |n|
102
+ text = n.value&.join.to_s.strip
103
+ text unless text.empty?
104
+ end
105
+ end
106
+
107
+ def extract_example_texts(lang_set)
108
+ lang_set.example.filter_map do |e|
109
+ text = e.value&.join.to_s.strip
110
+ text unless text.empty?
111
+ end
112
+ end
113
+
114
+ def extract_source_texts(lang_set)
115
+ lang_set.source.filter_map do |s|
116
+ text = s.value&.join.to_s.strip
117
+ text unless text.empty?
118
+ end
119
+ end
120
+
121
+ def extract_subject_field(lang_set)
122
+ fields = lang_set.subject_field
123
+ return nil unless fields&.any?
124
+
125
+ text = fields.first.value&.join.to_s.strip
126
+ text unless text.empty?
127
+ end
128
+
129
+ def build_designation(tig)
130
+ Sts::ExtractedDesignation.new(
131
+ term: resolve_term_text(tig),
132
+ type: map_term_type(tig),
133
+ normative_status: map_normative_status(tig),
134
+ part_of_speech: tig.pos&.value,
135
+ abbreviation_type: map_abbreviation_type(tig),
136
+ )
137
+ end
138
+
139
+ def resolve_term_text(tig)
140
+ tig.term&.value&.join.to_s.strip
141
+ end
142
+
143
+ def map_term_type(tig)
144
+ raw = tig.term_type&.value.to_s
145
+ mapped = TERM_TYPE_MAP[raw]
146
+ mapped.nil? || raw.empty? ? "expression" : mapped
147
+ end
148
+
149
+ def map_abbreviation_type(tig)
150
+ raw = tig.term_type&.value.to_s
151
+ return nil unless TERM_TYPE_MAP[raw] == "abbreviation"
152
+
153
+ raw == "acronym" ? "acronym" : "truncation"
154
+ end
155
+
156
+ def map_normative_status(tig)
157
+ NORMATIVE_STATUS_MAP[tig.normative_authorization&.value.to_s]
158
+ end
159
+
160
+ def extract_source_ref # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
161
+ front = @standard.front
162
+ return nil unless front
163
+
164
+ meta = front.iso_meta || front.std_meta
165
+ return nil unless meta
166
+
167
+ refs = meta.std_ref
168
+ return nil unless refs&.any?
169
+
170
+ best_ref = refs.find { |r| r.type == "dated" } ||
171
+ refs.find { |r| r.type == "undated" } ||
172
+ refs.first
173
+
174
+ extract_ref_text(best_ref)
175
+ end
176
+
177
+ def extract_ref_text(ref)
178
+ if ref.value.is_a?(String)
179
+ ref.value.to_s.strip
180
+ else
181
+ ref.content&.join.to_s.strip
182
+ end
183
+ end
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Glossarist
4
+ module Sts
5
+ class TermMapper
6
+ def map(extracted_term)
7
+ concept_id = extracted_term.label || extracted_term.id
8
+
9
+ mc = Glossarist::ManagedConcept.new(data: { id: concept_id })
10
+
11
+ extracted_term.lang_sets.each do |ls|
12
+ mc.add_localization(build_localized_concept(ls,
13
+ extracted_term.source_ref))
14
+ end
15
+
16
+ mc
17
+ end
18
+
19
+ private
20
+
21
+ def build_localized_concept(lang_set, source_ref)
22
+ terms = lang_set.designations.map { |d| build_designation(d) }
23
+
24
+ Glossarist::LocalizedConcept.of_yaml(
25
+ "data" => {
26
+ "language_code" => lang_set.language_code,
27
+ "terms" => terms,
28
+ "definition" => build_definitions(lang_set.definition_text),
29
+ "notes" => build_detailed_definitions(lang_set.note_texts),
30
+ "examples" => build_detailed_definitions(lang_set.example_texts),
31
+ "sources" => build_sources(lang_set.source_texts, source_ref),
32
+ "domain" => lang_set.domain,
33
+ "entry_status" => "valid",
34
+ },
35
+ )
36
+ end
37
+
38
+ def build_definitions(text)
39
+ return [] unless text && !text.empty?
40
+
41
+ [{ "content" => text }]
42
+ end
43
+
44
+ def build_detailed_definitions(texts)
45
+ texts.filter_map do |text|
46
+ next if text.empty?
47
+
48
+ { "content" => text }
49
+ end
50
+ end
51
+
52
+ def build_designation(ext_desig)
53
+ case ext_desig.type
54
+ when "abbreviation"
55
+ build_abbreviation_designation(ext_desig)
56
+ when "symbol"
57
+ build_symbol_designation(ext_desig)
58
+ else
59
+ build_expression_designation(ext_desig)
60
+ end
61
+ end
62
+
63
+ def build_expression_designation(ext_desig)
64
+ hash = {
65
+ "type" => "expression",
66
+ "designation" => ext_desig.term,
67
+ "normative_status" => ext_desig.normative_status,
68
+ }.compact
69
+
70
+ if ext_desig.part_of_speech
71
+ hash["grammar_info"] =
72
+ [{ "part_of_speech" => ext_desig.part_of_speech }]
73
+ end
74
+
75
+ hash
76
+ end
77
+
78
+ def build_abbreviation_designation(ext_desig)
79
+ {
80
+ "type" => "abbreviation",
81
+ "designation" => ext_desig.term,
82
+ "normative_status" => ext_desig.normative_status,
83
+ "abbreviation_type" => ext_desig.abbreviation_type,
84
+ }.compact
85
+ end
86
+
87
+ def build_symbol_designation(ext_desig)
88
+ {
89
+ "type" => "symbol",
90
+ "designation" => ext_desig.term,
91
+ "normative_status" => ext_desig.normative_status,
92
+ }.compact
93
+ end
94
+
95
+ def build_sources(source_texts, source_ref)
96
+ sources = []
97
+ if source_ref
98
+ sources << {
99
+ "status" => "identical",
100
+ "type" => "authoritative",
101
+ "origin" => { "text" => source_ref },
102
+ }
103
+ end
104
+
105
+ source_texts.each do |text|
106
+ next if text.empty?
107
+
108
+ sources << {
109
+ "type" => "authoritative",
110
+ "origin" => { "text" => text },
111
+ }
112
+ end
113
+
114
+ sources
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sts"
4
+
5
+ module Glossarist
6
+ module Sts
7
+ autoload :ExtractedDesignation, "#{__dir__}/sts/extracted_designation"
8
+ autoload :ExtractedLangSet, "#{__dir__}/sts/extracted_lang_set"
9
+ autoload :ExtractedTerm, "#{__dir__}/sts/extracted_term"
10
+ autoload :ImportResult, "#{__dir__}/sts/import_result"
11
+ autoload :Importer, "#{__dir__}/sts/importer"
12
+ autoload :TermExtractor, "#{__dir__}/sts/term_extractor"
13
+ autoload :TermMapper, "#{__dir__}/sts/term_mapper"
14
+
15
+ ISO_639_1_TO_639_2 = {
16
+ "aa" => "aar", "ab" => "abk", "af" => "afr", "ak" => "aka",
17
+ "am" => "amh", "an" => "arg", "ar" => "ara", "as" => "asm",
18
+ "av" => "ava", "ay" => "aym", "az" => "aze", "ba" => "bak",
19
+ "be" => "bel", "bg" => "bul", "bh" => "bih", "bi" => "bis",
20
+ "bm" => "bam", "bn" => "ben", "bo" => "bod", "br" => "bre",
21
+ "bs" => "bos", "ca" => "cat", "ce" => "che", "ch" => "cha",
22
+ "co" => "cos", "cr" => "cre", "cs" => "ces", "cu" => "chu",
23
+ "cv" => "chv", "cy" => "cym", "da" => "dan", "de" => "deu",
24
+ "dv" => "div", "dz" => "dzo", "ee" => "ewe", "el" => "ell",
25
+ "en" => "eng", "eo" => "epo", "es" => "spa", "et" => "est",
26
+ "eu" => "eus", "fa" => "fas", "ff" => "ful", "fi" => "fin",
27
+ "fj" => "fij", "fo" => "fao", "fr" => "fra", "fy" => "fry",
28
+ "ga" => "gle", "gd" => "gla", "gl" => "glg", "gn" => "grn",
29
+ "gu" => "guj", "gv" => "glv", "ha" => "hau", "he" => "heb",
30
+ "hi" => "hin", "ho" => "hmo", "hr" => "hrv", "ht" => "hat",
31
+ "hu" => "hun", "hy" => "hye", "hz" => "her", "ia" => "ina",
32
+ "id" => "ind", "ie" => "ile", "ig" => "ibo", "ii" => "iii",
33
+ "ik" => "ipk", "io" => "ido", "is" => "isl", "it" => "ita",
34
+ "iu" => "iku", "ja" => "jpn", "jv" => "jav", "ka" => "kat",
35
+ "kg" => "kon", "ki" => "kik", "kj" => "kua", "kk" => "kaz",
36
+ "kl" => "kal", "km" => "khm", "kn" => "kan", "ko" => "kor",
37
+ "kr" => "kau", "ks" => "kas", "ku" => "kur", "kv" => "kom",
38
+ "kw" => "cor", "ky" => "kir", "la" => "lat", "lb" => "ltz",
39
+ "lg" => "lug", "li" => "lim", "ln" => "lin", "lo" => "lao",
40
+ "lt" => "lit", "lu" => "lub", "lv" => "lav", "mg" => "mlg",
41
+ "mh" => "mah", "mi" => "mri", "mk" => "mkd", "ml" => "mal",
42
+ "mn" => "mon", "mr" => "mar", "ms" => "msa", "mt" => "mlt",
43
+ "my" => "mya", "na" => "nau", "nb" => "nob", "nd" => "nde",
44
+ "ne" => "nep", "ng" => "ndo", "nl" => "nld", "nn" => "nno",
45
+ "no" => "nor", "nr" => "nbl", "nv" => "nav", "ny" => "nya",
46
+ "oc" => "oci", "oj" => "oji", "om" => "orm", "or" => "ori",
47
+ "os" => "oss", "pa" => "pan", "pi" => "pli", "pl" => "pol",
48
+ "ps" => "pus", "pt" => "por", "qu" => "que", "rm" => "roh",
49
+ "rn" => "run", "ro" => "ron", "ru" => "rus", "rw" => "kin",
50
+ "sa" => "san", "sc" => "srd", "sd" => "snd", "se" => "sme",
51
+ "sg" => "sag", "si" => "sin", "sk" => "slk", "sl" => "slv",
52
+ "sm" => "smo", "sn" => "sna", "so" => "som", "sq" => "sqi",
53
+ "sr" => "srp", "ss" => "ssw", "st" => "sot", "su" => "sun",
54
+ "sv" => "swe", "sw" => "swa", "ta" => "tam", "te" => "tel",
55
+ "tg" => "tgk", "th" => "tha", "ti" => "tir", "tk" => "tuk",
56
+ "tl" => "tgl", "tn" => "tsn", "to" => "ton", "tr" => "tur",
57
+ "ts" => "tso", "tt" => "tat", "tw" => "twi", "ty" => "tah",
58
+ "ug" => "uig", "uk" => "ukr", "ur" => "urd", "uz" => "uzb",
59
+ "ve" => "ven", "vi" => "vie", "vo" => "vol", "wa" => "wln",
60
+ "wo" => "wol", "xh" => "xho", "yi" => "yid", "yo" => "yor",
61
+ "za" => "zha", "zh" => "zho", "zu" => "zul"
62
+ }.freeze
63
+
64
+ TERM_TYPE_MAP = {
65
+ "acronym" => "abbreviation",
66
+ "abbreviation" => "abbreviation",
67
+ "fullForm" => "expression",
68
+ "symbol" => "symbol",
69
+ "variant" => "expression",
70
+ "equation" => "expression",
71
+ "formula" => "expression",
72
+ }.freeze
73
+
74
+ NORMATIVE_STATUS_MAP = {
75
+ "preferredTerm" => "preferred",
76
+ "admittedTerm" => "admitted",
77
+ "deprecatedTerm" => "deprecated",
78
+ }.freeze
79
+
80
+ def self.convert_language_code(code)
81
+ return code if code.nil?
82
+ return code if code.length == 3
83
+
84
+ ISO_639_1_TO_639_2[code] || code
85
+ end
86
+ end
87
+ end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../rdf"
4
-
5
3
  module Glossarist
6
4
  module Transforms
7
5
  class ConceptToSkosTransform
@@ -4,5 +4,5 @@
4
4
  #
5
5
 
6
6
  module Glossarist
7
- VERSION = "2.6.2"
7
+ VERSION = "2.6.4"
8
8
  end