suma 0.2.5 → 0.2.6

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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/rake.yml +3 -0
  3. data/.github/workflows/release.yml +5 -1
  4. data/.rubocop_todo.yml +78 -26
  5. data/CLAUDE.md +76 -0
  6. data/Gemfile +3 -1
  7. data/README.adoc +131 -0
  8. data/lib/suma/cli/build.rb +2 -3
  9. data/lib/suma/cli/check_svg_quality.rb +178 -0
  10. data/lib/suma/cli/compare.rb +7 -158
  11. data/lib/suma/cli/export.rb +1 -7
  12. data/lib/suma/cli/extract_terms.rb +7 -648
  13. data/lib/suma/cli/generate_schemas.rb +9 -123
  14. data/lib/suma/cli/validate_links.rb +15 -290
  15. data/lib/suma/cli.rb +39 -0
  16. data/lib/suma/collection_manifest.rb +3 -4
  17. data/lib/suma/express_schema.rb +43 -30
  18. data/lib/suma/jsdai/figure_xml.rb +12 -9
  19. data/lib/suma/jsdai.rb +0 -6
  20. data/lib/suma/link_validator.rb +203 -0
  21. data/lib/suma/processor.rb +75 -101
  22. data/lib/suma/schema_attachment.rb +2 -29
  23. data/lib/suma/schema_collection.rb +1 -32
  24. data/lib/suma/schema_comparer.rb +116 -0
  25. data/lib/suma/schema_document.rb +0 -14
  26. data/lib/suma/schema_exporter.rb +16 -28
  27. data/lib/suma/schema_index.rb +53 -0
  28. data/lib/suma/schema_manifest_generator.rb +105 -0
  29. data/lib/suma/svg_quality/batch_report.rb +80 -0
  30. data/lib/suma/svg_quality/formatters/json_formatter.rb +30 -0
  31. data/lib/suma/svg_quality/formatters/terminal_formatter.rb +168 -0
  32. data/lib/suma/svg_quality/formatters/yaml_formatter.rb +32 -0
  33. data/lib/suma/svg_quality/report.rb +52 -0
  34. data/lib/suma/svg_quality.rb +28 -0
  35. data/lib/suma/term_extractor.rb +393 -0
  36. data/lib/suma/utils.rb +10 -2
  37. data/lib/suma/version.rb +1 -1
  38. data/lib/suma.rb +3 -2
  39. data/suma.gemspec +3 -2
  40. metadata +33 -7
  41. data/lib/suma/export_standalone_schema.rb +0 -14
@@ -2,27 +2,11 @@
2
2
 
3
3
  require "thor"
4
4
  require_relative "../thor_ext"
5
- require "fileutils"
6
- require "expressir"
7
- require "securerandom"
8
- require "glossarist"
5
+ require_relative "../term_extractor"
9
6
 
10
7
  module Suma
11
8
  module Cli
12
- # ExtractTerms command using Expressir to extract terms into the
13
- # Glossarist v2 format
14
9
  class ExtractTerms < Thor
15
- # Matches patterns like "A thing is a type of {{entity}}." or
16
- # "An object is a type of a {{entity}}"
17
- REDUNDANT_NOTE_REGEX =
18
- %r{
19
- ^An? # Starts with "A" or "An"
20
- \s.*?\sis\sa\stype\sof # Text followed by "is a type of"
21
- (\sa|\san)? # Optional " a" or " an"
22
- \s\{\{[^\}]*\}\} # Text in double curly braces
23
- \s*?\.?$ # Optional whitespace and period at the end
24
- }x
25
-
26
10
  desc "extract_terms SCHEMA_MANIFEST_FILE GLOSSARIST_OUTPUT_PATH",
27
11
  "Extract terms from SCHEMA_MANIFEST_FILE into " \
28
12
  "Glossarist v2 format"
@@ -30,641 +14,16 @@ module Suma
30
14
  desc: "Language code for the Glossarist"
31
15
 
32
16
  def extract_terms(schema_manifest_file, output_path)
33
- language_code = options[:language_code]
34
- schema_manifest_file = File.expand_path(schema_manifest_file)
35
-
36
- unless File.exist?(schema_manifest_file)
17
+ unless File.exist?(File.expand_path(schema_manifest_file))
37
18
  raise Errno::ENOENT, "Specified SCHEMA_MANIFEST_FILE " \
38
19
  "`#{schema_manifest_file}` not found."
39
20
  end
40
21
 
41
- run(schema_manifest_file, output_path, language_code)
42
- end
43
-
44
- private
45
-
46
- def run(schema_manifest_file, output_path, language_code = "eng")
47
- get_exp_files(schema_manifest_file).map do |exp_file|
48
- extract(exp_file, output_path, language_code)
49
- end
50
- end
51
-
52
- def get_exp_files(schema_manifest_file)
53
- config = Expressir::SchemaManifest.from_file(schema_manifest_file)
54
- paths = config.schemas.map(&:path)
55
-
56
- if paths.empty?
57
- raise Errno::ENOENT, "No EXPRESS files found in " \
58
- "`#{schema_manifest_file}`."
59
- end
60
-
61
- paths
62
- end
63
-
64
- def extract(exp_file, output_path, language_code)
65
- exp_path_rel = Pathname.new(exp_file).relative_path_from(Pathname.getwd)
66
- puts "Building terms: #{exp_path_rel}"
67
-
68
- repo = Expressir::Express::Parser.from_file(exp_file)
69
- schema = get_default_schema(repo)
70
-
71
- unless schema.file
72
- raise Error.new("Schema must have an associated file")
73
- end
74
-
75
- collection = build_managed_concept_collection(
76
- schema, language_code
77
- )
78
-
79
- output_data(collection, output_path)
80
- end
81
-
82
- def output_data(collection, output_path)
83
- unless File.exist?(output_path)
84
- FileUtils.mkdir_p(File.expand_path(output_path))
85
- end
86
-
87
- puts "Saving collection to files in: #{output_path}"
88
- collection.save_to_files(File.expand_path(output_path))
89
-
90
- collection
91
- end
92
-
93
- def build_managed_concept_collection(schema, language_code)
94
- Glossarist::ManagedConceptCollection.new.tap do |collection|
95
- # Extract schema-level citation data once to reuse across all entities
96
- source_ref = get_source_ref(schema)
97
-
98
- # Create one concept per entity
99
- schema.entities.each do |entity|
100
- localized_concept = build_localized_concept(
101
- schema: schema,
102
- entity: entity,
103
- language_code: language_code,
104
- source_ref: source_ref,
105
- )
106
- localized_concept_id = get_localized_concept_identifier(
107
- schema, entity, language_code
108
- )
109
- localized_concept.uuid = localized_concept_id
110
-
111
- managed_data = Glossarist::ManagedConceptData.new.tap do |data|
112
- data.id = get_entity_identifier(schema, entity)
113
-
114
- # TODO: Why do we need both localizations and localized_concepts??
115
- data.localizations[language_code] = localized_concept
116
- # uuid is automatically set from the serialization of the object
117
- data.localized_concepts = {
118
- language_code => localized_concept_id,
119
- }
120
- end
121
-
122
- managed_concept = Glossarist::ManagedConcept.new.tap do |concept|
123
- # uuid is automatically set from the serialization of the object
124
- concept.id = get_entity_identifier(schema, entity)
125
- concept.uuid = concept.id
126
- concept.data = managed_data
127
- end
128
-
129
- collection.store(managed_concept)
130
- end
131
- end
132
- end
133
-
134
- def build_localized_concept(schema:, entity:, language_code:, source_ref:)
135
- schema_domain = get_domain(schema)
136
-
137
- localized_concept_data = Glossarist::ConceptData.new.tap do |data|
138
- data.terms = get_entity_terms(entity)
139
- data.definition = get_entity_definitions(entity, schema)
140
- data.language_code = language_code
141
- data.domain = schema_domain
142
- data.sources = [source_ref] if source_ref
143
-
144
- # Only assign optional fields if they have content
145
- notes = get_entity_notes(entity, schema_domain, data.definition)
146
- data.notes = notes if notes && !notes.empty?
147
-
148
- # examples = get_entity_examples(entity, schema_domain)
149
- # data.examples = examples if examples && !examples.empty?
150
- data.examples = []
151
- end
152
-
153
- Glossarist::LocalizedConcept.new.tap do |concept|
154
- concept.data = localized_concept_data
155
- end
156
- end
157
-
158
- # We only deal with 1 schema
159
- def get_default_schema(repo)
160
- repo.schemas.first
161
- end
162
-
163
- def find_remark_value(schema, remark_id)
164
- schema.remark_items.find { |s| s.id == remark_id }&.remarks&.first
165
- end
166
-
167
- def get_entity_identifier(schema, entity)
168
- "#{schema.id}.#{entity.id}"
169
- end
170
-
171
- def get_localized_concept_identifier(schema, entity, lang)
172
- "#{schema.id}.#{entity.id}-#{lang}"
173
- end
174
-
175
- def get_source_ref(schema)
176
- origin = Glossarist::Citation.new.tap do |citation|
177
- citation.ref = "ISO 10303"
178
- custom_locality = build_custom_locality(schema)
179
-
180
- unless custom_locality.empty?
181
- citation.custom_locality = custom_locality
182
- end
183
- end
184
-
185
- Glossarist::ConceptSource.new(type: "authoritative", origin: origin)
186
- end
187
-
188
- # SCHEMA action_schema
189
- # '{iso standard 10303 part(41) version(9) object(1) action-schema(1)}';
190
- def build_custom_locality(schema)
191
- [].tap do |localities|
192
- # Add schema name
193
- localities << Glossarist::CustomLocality.new(
194
- name: "schema",
195
- value: schema.id,
196
- )
197
-
198
- # Add version if available
199
- version_item = schema.version.items.detect { |i| i.name == "version" }
200
- if version_item
201
- localities << Glossarist::CustomLocality.new(
202
- name: "version",
203
- value: version_item.value,
204
- )
205
- end
206
- end
207
- end
208
-
209
- # TODO: What if this was a "bom"?
210
- def get_domain(schema)
211
- prefix = if mim?(schema.id) || arm?(schema.id)
212
- "application module"
213
- else
214
- "resource"
215
- end
216
-
217
- "#{prefix}: #{schema.id}"
218
- end
219
-
220
- def arm?(schema_id)
221
- schema_id.end_with?("_arm")
222
- end
223
-
224
- def mim?(schema_id)
225
- schema_id.end_with?("_mim")
226
- end
227
-
228
- def get_terms(schema)
229
- schema_title = get_title(schema)
230
- if schema_title
231
- [
232
- Glossarist::Designation::Base.new(
233
- designation: schema_title,
234
- type: "expression",
235
- normative_status: "preferred",
236
- ),
237
- ]
238
- end
239
- end
240
-
241
- def get_entity_terms(entity)
242
- # For now, use the entity ID as the term
243
- # This could be enhanced to look for entity-specific title remark items
244
- [
245
- Glossarist::Designation::Base.new(
246
- designation: entity.id,
247
- type: "expression",
248
- normative_status: "preferred",
249
- ),
250
- ]
251
- end
252
-
253
- def get_entity_definitions(entity, schema)
254
- schema_type = extract_file_type(schema.file)
255
- schema_domain = get_domain(schema)
256
-
257
- definition = generate_entity_definition(entity, schema_domain,
258
- schema_type)
259
- [Glossarist::DetailedDefinition.new(content: definition)]
260
- end
261
-
262
- def get_entity_notes(entity, schema_domain, definitions)
263
- puts "Extracting notes for entity: #{entity.id}"
264
- notes = []
265
-
266
- notes = add_entity_notes(entity, schema_domain, notes)
267
- # notes = add_other_notes(entity, schema_domain, notes)
268
- notes = only_keep_first_sentence(notes)
269
- notes = remove_see_content(notes)
270
- notes = remove_redundant_note(notes)
271
- notes = remove_invalid_references(notes)
272
- compare_with_definitions(notes, definitions)
273
- end
274
-
275
- def add_entity_notes(entity, schema_domain, notes)
276
- # Add trimmed definition from entity description as first note
277
- if entity.remarks && !entity.remarks.empty?
278
- trimmed_def = trim_definition(entity.remarks)
279
- if trimmed_def && !trimmed_def.empty?
280
- notes << Glossarist::DetailedDefinition.new(
281
- content: convert_express_xref(trimmed_def, schema_domain),
282
- )
283
- end
284
- end
285
-
286
- notes.compact
287
- end
288
-
289
- def add_other_notes(entity, schema_domain, notes)
290
- # Add other notes from entity remarks
291
- other_notes = [
292
- entity.remark_items&.select do |ri|
293
- ri.id == "__note"
294
- end&.map(&:remarks),
295
- ].flatten.compact
296
-
297
- other_notes.each do |note|
298
- notes << Glossarist::DetailedDefinition.new(
299
- content: convert_express_xref(note, schema_domain),
300
- )
301
- end
302
-
303
- notes
304
- end
305
-
306
- # https://github.com/metanorma/iso-10303/issues/621
307
- # 1. First sentence in first paragraph of the entity description
308
- # (in EXPRESS remark) becomes NOTE 1 in ISO 10303-2 of the entity.
309
- def only_keep_first_sentence(notes)
310
- notes.each do |note|
311
- # Skip truncation only for content that starts with a paragraph ending in ":"
312
- # followed by a list (complete list structures that should be preserved)
313
- if note&.content && should_preserve_complete_structure?(note.content)
314
- # For complete list structures, keep the content as-is
315
- next
316
- end
317
-
318
- # Split by period and take the first sentence for all other content
319
- # Avoid splitting by pattern like "abc.def"
320
- if note&.content
321
- new_content = note.content
322
- .split(".\n").first.strip
323
- .split(". ").first.strip
324
- note.content = if new_content.end_with?(".")
325
- new_content
326
- else
327
- "#{new_content}."
328
- end
329
- end
330
- end
331
- end
332
-
333
- def should_preserve_complete_structure?(content)
334
- return false if content.nil? || content.empty?
335
-
336
- # Check if content starts with a single introductory sentence ending in ":"
337
- # followed by a list. This indicates a complete list structure that should be preserved.
338
- lines = content.split("\n")
339
- first_paragraph = lines.first&.strip
340
-
341
- # Look for pattern: Single sentence ending with ":" (introductory pattern)
342
- if first_paragraph&.end_with?(":") && lines.length > 1
343
- # Check if the first paragraph contains multiple sentences (periods before the colon)
344
- # If it does, this is NOT an introductory paragraph - extract first sentence only
345
- if first_paragraph.count(".").positive?
346
- return false
347
- end
348
-
349
- # Check if there's a list after the colon
350
- remaining_content = lines[1..].join("\n")
351
- return starts_with_list?(remaining_content.strip)
352
- end
353
-
354
- false
355
- end
356
-
357
- # https://github.com/metanorma/iso-10303/issues/621
358
- # 2. If this first sentence matches the 7-word magic sentence
359
- # (2-3 forms of that), it is discarded so there will not be a NOTE 1.
360
- def compare_with_definitions(notes, definitions)
361
- if notes&.first&.content == definitions&.first&.content
362
- # Discarding first note as it matches the definition
363
- return []
364
- end
365
-
366
- notes
367
- end
368
-
369
- # https://github.com/metanorma/iso-10303/issues/621
370
- # 3. No reference to any types or attribute or figures allowed in first
371
- # sentence. Entity references “{{…}}” are allowed.
372
- def remove_invalid_references(notes)
373
- notes.reject do |note|
374
- note.content.include?("image::") ||
375
- note.content.match?(/<<(.*?){1,999}>>/)
376
- end
377
- end
378
-
379
- # https://github.com/metanorma/iso-10303/issues/621
380
- # 4. Entity notes and examples in EXPRESS remarks are NOT represented in
381
- # part 2.
382
- def remove_redundant_note(notes)
383
- notes.reject do |note|
384
- note.content.match?(REDUNDANT_NOTE_REGEX) &&
385
- !note.content.include?("\n")
386
- end
387
- end
388
-
389
- # https://github.com/metanorma/iso-10303/issues/621
390
- # 5. If the sentence contains “\s+(see …)”, the contents including the
391
- # parentheses are removed.
392
- def remove_see_content(notes)
393
- notes.each do |note|
394
- note.content = note.content.gsub(/\s+\(see(.*?){1,999}\)/, "")
395
- end
396
- end
397
-
398
- def get_entity_examples(entity, schema_domain)
399
- examples = entity.remark_items&.select do |ri|
400
- ri.id == "__example"
401
- end&.map(&:remarks)&.flatten&.compact || []
402
-
403
- examples.map do |example|
404
- Glossarist::DetailedDefinition.new(
405
- content: convert_express_xref(example, schema_domain),
406
- )
407
- end
408
- end
409
-
410
- def extract_file_type(filename)
411
- match = filename.match(/(arm|mim|bom)_annotated\.exp$/)
412
- return "resource" unless match
413
-
414
- {
415
- "arm" => "module_arm",
416
- "mim" => "module_mim",
417
- "bom" => "business_object_model",
418
- }[match.captures[0]] || "resource"
419
- end
420
-
421
- def get_schema_type(schema)
422
- return "mim" if mim?(schema.id)
423
- return "arm" if arm?(schema.id)
424
- return "bom" if bom?(schema.id)
425
-
426
- "resource"
427
- end
428
-
429
- def bom?(schema_id)
430
- schema_id.end_with?("_bom")
431
- end
432
-
433
- def contains_list?(content)
434
- return false if content.nil? || content.empty?
435
-
436
- # Check if content contains list markers
437
- content.match?(/^\s*[\*\-\+]\s+/m) || content.match?(/^\s*\d+\.\s+/m)
438
- end
439
-
440
- def starts_with_list?(content)
441
- return false if content.nil? || content.empty?
442
-
443
- # Check if content starts with list markers
444
- content.match?(/^\s*[\*\-\+]\s+/) || content.match?(/^\s*\d+\.\s+/)
445
- end
446
-
447
- def is_list_continuation?(content)
448
- return false if content.nil? || content.empty?
449
-
450
- # Check for AsciiDoc list continuation patterns
451
- content.match?(/^\+\s*$/) ||
452
- content.match?(/^--\s*$/) ||
453
- content.match?(/^\s{2,}/) || # Indented content (continuation)
454
- content.start_with?("which", "where", "that") # Logical continuation
455
- end
456
-
457
- def extract_complete_list(paragraphs, start_index)
458
- return paragraphs[start_index] if start_index >= paragraphs.length
459
-
460
- combined = paragraphs[start_index].dup
461
- current_index = start_index + 1
462
-
463
- # Check if the first paragraph already contains an opening continuation block
464
- in_continuation_block = combined.include?("--") && !combined.match?(/--.*--/m)
465
-
466
- # Continue collecting paragraphs while we're in a list context
467
- while current_index < paragraphs.length
468
- next_para = paragraphs[current_index]
469
-
470
- # Check if we're entering or exiting a continuation block
471
- if next_para.match?(/^--\s*$/) || next_para.end_with?("--")
472
- in_continuation_block = !in_continuation_block
473
- combined += "\n\n#{next_para}"
474
- current_index += 1
475
- next
476
- end
477
-
478
- # If we're in a continuation block, include all content until we hit the closing --
479
- if in_continuation_block
480
- combined += "\n\n#{next_para}"
481
- current_index += 1
482
- next
483
- end
484
-
485
- # Check if this is a list item or list continuation
486
- if starts_with_list?(next_para) || is_list_continuation?(next_para)
487
- combined += "\n\n#{next_para}"
488
- current_index += 1
489
-
490
- # Check if this paragraph contains an opening continuation block
491
- if next_para.include?("--") && !next_para.match?(/--.*--/m)
492
- in_continuation_block = true
493
- end
494
- else
495
- # This paragraph is not part of the list structure
496
- break
497
- end
498
- end
499
-
500
- combined
501
- end
502
-
503
- def ends_list_structure?(current_para, next_para)
504
- return true if next_para.nil?
505
-
506
- # List ends if:
507
- # 1. Current paragraph doesn't end with continuation markers
508
- # 2. Next paragraph starts a new section (not list or continuation)
509
- !current_para.match?(/\+\s*$/) &&
510
- !starts_with_list?(next_para) &&
511
- !is_list_continuation?(next_para)
512
- end
513
-
514
- def apply_first_sentence_logic(paragraph)
515
- # Apply the original first-sentence extraction logic
516
- # Split by period and take the first sentence
517
- # Avoid splitting by pattern like "abc.def"
518
- new_content = paragraph
519
- .split(".\n").first.strip
520
- .split(". ").first.strip
521
-
522
- if new_content.end_with?(".")
523
- new_content
524
- else
525
- "#{new_content}."
526
- end
527
- end
528
-
529
- # rubocop:disable Metrics/MethodLength
530
- def combine_paragraphs(full_paragraph, next_paragraph)
531
- # Check if we're dealing with a list structure
532
- if contains_list?(full_paragraph) || starts_with_list?(next_paragraph)
533
- return combine_list_content(full_paragraph, next_paragraph)
534
- end
535
-
536
- # For regular paragraphs, apply the original first-sentence logic
537
- # If full_paragraph already contains a period, extract that.
538
- if m = full_paragraph.match(/\A(?<inner_first>[^\n]*?\.)\s/)
539
- if m[:inner_first]
540
- return m[:inner_first]
541
- else
542
- return full_paragraph
543
- end
544
- end
545
-
546
- # If full_paragraph ends with a period, this is the last.
547
- if /\.\s*\Z/.match?(full_paragraph)
548
- return full_paragraph
549
- end
550
-
551
- # If next_paragraph is a continuation of a paragraph
552
- if next_paragraph&.start_with?("which", "where", "that")
553
- return "#{full_paragraph}\n\n#{next_paragraph}"
554
- end
555
-
556
- full_paragraph
557
- end
558
-
559
- def combine_list_content(full_paragraph, next_paragraph)
560
- combined = full_paragraph.dup
561
-
562
- # If we have a next paragraph, add it
563
- unless next_paragraph.nil? || next_paragraph.empty?
564
- combined += "\n\n#{next_paragraph}"
565
- end
566
-
567
- combined
568
- end
569
-
570
- def trim_definition(definition)
571
- return nil if definition.nil? || definition.empty?
572
-
573
- # Handle case where definition is an array
574
- definition_str = if definition.is_a?(Array)
575
- definition.join("\n\n")
576
- else
577
- definition.to_s
578
- end
579
-
580
- return nil if definition_str.empty?
581
-
582
- paragraphs = definition_str.split("\n\n")
583
- first_paragraph = paragraphs.first
584
-
585
- # If we only have one paragraph, apply the original logic
586
- if paragraphs.length == 1
587
- combined = apply_first_sentence_logic(first_paragraph)
588
- elsif first_paragraph.end_with?(":") && paragraphs.length > 1 && starts_with_list?(paragraphs[1])
589
- # Case 1: First paragraph ends with ":" and leads into a list
590
- # Extract the complete list structure (this is an introductory paragraph)
591
- complete_list = extract_complete_list(paragraphs, 1)
592
- combined = "#{first_paragraph}\n\n#{complete_list}"
593
- else
594
- # Case 2: For all other cases (including sentences followed by lists)
595
- # Extract only the first sentence from the first paragraph
596
- combined = apply_first_sentence_logic(first_paragraph)
597
- end
598
-
599
- # Remove comments until end of line
600
- combined = "#{combined}\n"
601
- combined.gsub!(/\n\/\/.*?\n/, "\n")
602
- combined.strip!
603
-
604
- express_reference_to_mention(combined)
605
- end
606
- # rubocop:enable Metrics/MethodLength
607
-
608
- # Replace `<<express:{schema}.{entity}>>` with {{entity}}
609
- # and `<<express:{schema}.{entity},{render}>>` with {{entity,render}}
610
- def express_reference_to_mention(description)
611
- # TODO: Use Expressir to check whether the "entity" is really an
612
- # EXPRESS ENTITY. If not, skip the mention.
613
- description
614
- .gsub(/<<express:([^,]+)>>/) do |_match|
615
- "{{#{Regexp.last_match[1].split('.').last}}}"
616
- end.gsub(/<<express:([^,]+),([^>]+)>>/) do |_match|
617
- "{{#{Regexp.last_match[1].split('.').last}," \
618
- "#{Regexp.last_match[2]}}}"
619
- end
620
- end
621
-
622
- def entity_name_to_text(entity_id)
623
- entity_id.downcase.gsub("_", " ")
624
- end
625
-
626
- # rubocop:disable Layout/LineLength
627
- def generate_entity_definition(entity, _domain, schema_type)
628
- return "" if entity.nil?
629
-
630
- # See: metanorma/iso-10303-2#90
631
- entity_type = case schema_type
632
- when "module_arm"
633
- "{{application object}}"
634
- when "module_mim"
635
- "{{entity data type}}"
636
- when "resource", "business_object_model"
637
- "{{entity data type}}"
638
- else
639
- raise Error.new("[suma] encountered unsupported schema_type")
640
- end
641
-
642
- if entity.subtype_of.empty?
643
- "#{entity_type} " \
644
- "that represents the " \
645
- "#{entity_name_to_text(entity.id)} {{entity}}"
646
- else
647
- entity_subtypes = entity.subtype_of.map do |e|
648
- "{{#{e.id}}}"
649
- end
650
-
651
- "#{entity_type} that is a type of " \
652
- "#{entity_subtypes.join(' and ')} " \
653
- "that represents the " \
654
- "#{entity_name_to_text(entity.id)} {{entity}}"
655
- end
656
- end
657
- # rubocop:enable Layout/LineLength
658
-
659
- def convert_express_xref(content, schema_domain)
660
- content.gsub(/<<express:(.*),(.*)>>/) do
661
- "{{<#{schema_domain}>" \
662
- "#{Regexp.last_match(1).split('.').last},#{Regexp.last_match(2)}}}"
663
- end
664
- end
665
-
666
- def id_from_designation(designation)
667
- designation.gsub(" ", "_").gsub("/", "_").gsub(":", "_")
22
+ TermExtractor.new(
23
+ schema_manifest_file,
24
+ output_path,
25
+ language_code: options[:language_code],
26
+ ).call
668
27
  end
669
28
  end
670
29
  end