scjson 0.3.3

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e5458d979377d6bb8afc2d6ba6290f228d0b85602ab0c00885fd5f3a0c8f7055
4
+ data.tar.gz: 0a5889b05002e6944b82ae94814aaefa33d008519164486d7d4db5a03f4a8ed3
5
+ SHA512:
6
+ metadata.gz: 762b5feb6ce8d91f42b99fe7358528a02e1c8a462bc41cfcdf6721e17cb8a12be73f1d0ad60185005a312da607fbf49699ec5f272cf3b94cd9a631930609f945
7
+ data.tar.gz: 7e936f6fb3bf500d10e4618d2e19f62af93d124eec40642d162e6c2c939793718f561d6e329bae5c18bc363cf2d1db8f9c9f8bdc2d36e01668a16bf635eedd17
data/bin/scjson ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+ # Agent Name: ruby-cli-runner
3
+ #
4
+ # Part of the scjson project.
5
+ # Developed by Softoboros Technology Inc.
6
+ # Licensed under the BSD 1-Clause License.
7
+
8
+ require_relative '../lib/scjson/cli'
9
+ Scjson.main
data/lib/scjson/cli.rb ADDED
@@ -0,0 +1,219 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Agent Name: ruby-cli
4
+ #
5
+ # Part of the scjson project.
6
+ # Developed by Softoboros Technology Inc.
7
+ # Licensed under the BSD 1-Clause License.
8
+
9
+ require 'pathname'
10
+ require 'optparse'
11
+ require 'fileutils'
12
+ require_relative '../scjson'
13
+
14
+ module Scjson
15
+ ##
16
+ # Display the CLI program header.
17
+ #
18
+ # @return [void]
19
+ def self.splash
20
+ puts "scjson #{VERSION} - SCXML <-> scjson converter"
21
+ end
22
+
23
+ ##
24
+ # Command line interface for scjson conversions.
25
+ #
26
+ # @param [Array<String>] argv Command line arguments provided by the user.
27
+ # @return [void]
28
+ def self.main(argv = ARGV)
29
+ options = { recursive: false, verify: false, keep_empty: false }
30
+ cmd = argv.shift
31
+ if cmd.nil? || %w[-h --help].include?(cmd)
32
+ puts(help_text)
33
+ return
34
+ end
35
+ parser = OptionParser.new do |opts|
36
+ opts.banner = ''
37
+ opts.on('-o', '--output PATH', 'output file or directory') { |v| options[:output] = v }
38
+ opts.on('-r', '--recursive', 'recurse into directories') { options[:recursive] = true }
39
+ opts.on('-v', '--verify', 'verify conversion without writing output') { options[:verify] = true }
40
+ opts.on('--keep-empty', 'keep null or empty items when producing JSON') { options[:keep_empty] = true }
41
+ end
42
+ path = argv.shift
43
+ parser.parse!(argv)
44
+ unless path
45
+ puts(help_text)
46
+ return
47
+ end
48
+ splash
49
+ case cmd
50
+ when 'json'
51
+ handle_json(Pathname.new(path), options)
52
+ when 'xml'
53
+ handle_xml(Pathname.new(path), options)
54
+ when 'validate'
55
+ validate(Pathname.new(path), options[:recursive])
56
+ else
57
+ puts(help_text)
58
+ end
59
+ end
60
+
61
+ ##
62
+ # Render the help text describing CLI usage.
63
+ #
64
+ # @return [String] A one-line summary of the CLI purpose.
65
+ def self.help_text
66
+ 'scjson - SCXML <-> scjson converter and validator'
67
+ end
68
+
69
+ ##
70
+ # Convert SCXML inputs to scjson outputs.
71
+ #
72
+ # Handles both file and directory inputs, preserving relative paths when
73
+ # writing to directories.
74
+ #
75
+ # @param [Pathname] path Source file or directory.
76
+ # @param [Hash] opt Options hash controlling output and recursion behaviour.
77
+ # @return [void]
78
+ def self.handle_json(path, opt)
79
+ if path.directory?
80
+ out_dir = opt[:output] ? Pathname.new(opt[:output]) : path
81
+ pattern = opt[:recursive] ? '**/*.scxml' : '*.scxml'
82
+ Dir.glob(path.join(pattern).to_s).each do |src|
83
+ rel = Pathname.new(src).relative_path_from(path)
84
+ dest = out_dir.join(rel).sub_ext('.scjson')
85
+ convert_scxml_file(src, dest, opt[:verify], opt[:keep_empty])
86
+ end
87
+ else
88
+ dest = if opt[:output]
89
+ p = Pathname.new(opt[:output])
90
+ p.directory? ? p.join(path.basename.sub_ext('.scjson')) : p
91
+ else
92
+ path.sub_ext('.scjson')
93
+ end
94
+ convert_scxml_file(path, dest, opt[:verify], opt[:keep_empty])
95
+ end
96
+ end
97
+
98
+ ##
99
+ # Convert scjson inputs to SCXML outputs.
100
+ #
101
+ # Handles both file and directory inputs.
102
+ #
103
+ # @param [Pathname] path Source file or directory.
104
+ # @param [Hash] opt Options hash controlling output and recursion behaviour.
105
+ # @return [void]
106
+ def self.handle_xml(path, opt)
107
+ if path.directory?
108
+ out_dir = opt[:output] ? Pathname.new(opt[:output]) : path
109
+ pattern = opt[:recursive] ? '**/*.scjson' : '*.scjson'
110
+ Dir.glob(path.join(pattern).to_s).each do |src|
111
+ rel = Pathname.new(src).relative_path_from(path)
112
+ dest = out_dir.join(rel).sub_ext('.scxml')
113
+ convert_scjson_file(src, dest, opt[:verify])
114
+ end
115
+ else
116
+ dest = if opt[:output]
117
+ p = Pathname.new(opt[:output])
118
+ p.directory? ? p.join(path.basename.sub_ext('.scxml')) : p
119
+ else
120
+ path.sub_ext('.scxml')
121
+ end
122
+ convert_scjson_file(path, dest, opt[:verify])
123
+ end
124
+ end
125
+
126
+ ##
127
+ # Convert a single SCXML document to scjson.
128
+ #
129
+ # @param [String, Pathname] src Input SCXML file path.
130
+ # @param [Pathname] dest Target path for scjson output.
131
+ # @param [Boolean] verify When true, only validate round-tripping without writing.
132
+ # @param [Boolean] keep_empty When true, retain empty containers in JSON output.
133
+ # @return [void]
134
+ def self.convert_scxml_file(src, dest, verify, keep_empty)
135
+ xml_str = File.read(src)
136
+ begin
137
+ json_str = Scjson.xml_to_json(xml_str, !keep_empty)
138
+ if verify
139
+ Scjson.json_to_xml(json_str)
140
+ puts "Verified #{src}"
141
+ else
142
+ FileUtils.mkdir_p(dest.dirname)
143
+ File.write(dest, json_str)
144
+ puts "Wrote #{dest}"
145
+ end
146
+ rescue StandardError => e
147
+ warn "Failed to convert #{src}: #{e}"
148
+ end
149
+ end
150
+
151
+ ##
152
+ # Convert a single scjson document to SCXML.
153
+ #
154
+ # @param [String, Pathname] src Input scjson file path.
155
+ # @param [Pathname] dest Target SCXML file path.
156
+ # @param [Boolean] verify When true, only validate round-tripping without writing.
157
+ # @return [void]
158
+ def self.convert_scjson_file(src, dest, verify)
159
+ json_str = File.read(src)
160
+ begin
161
+ xml_str = Scjson.json_to_xml(json_str)
162
+ if verify
163
+ Scjson.xml_to_json(xml_str)
164
+ puts "Verified #{src}"
165
+ else
166
+ FileUtils.mkdir_p(dest.dirname)
167
+ File.write(dest, xml_str)
168
+ puts "Wrote #{dest}"
169
+ end
170
+ rescue StandardError => e
171
+ warn "Failed to convert #{src}: #{e}"
172
+ end
173
+ end
174
+
175
+ ##
176
+ # Validate a file or directory tree of SCXML and scjson documents.
177
+ #
178
+ # @param [Pathname] path File or directory to validate.
179
+ # @param [Boolean] recursive When true, traverse subdirectories.
180
+ # @return [void]
181
+ def self.validate(path, recursive)
182
+ success = true
183
+ if path.directory?
184
+ pattern = recursive ? '**/*' : '*'
185
+ Dir.glob(path.join(pattern).to_s).each do |f|
186
+ next unless File.file?(f)
187
+ next unless f.end_with?('.scxml', '.scjson')
188
+ success &= validate_file(f)
189
+ end
190
+ else
191
+ success &= validate_file(path.to_s)
192
+ end
193
+ exit(1) unless success
194
+ end
195
+
196
+ ##
197
+ # Validate a single SCXML or scjson document by round-tripping.
198
+ #
199
+ # @param [String] src Path to the document to validate.
200
+ # @return [Boolean] True when the document validates successfully.
201
+ def self.validate_file(src)
202
+ begin
203
+ data = File.read(src)
204
+ if src.end_with?('.scxml')
205
+ json = Scjson.xml_to_json(data)
206
+ Scjson.json_to_xml(json)
207
+ elsif src.end_with?('.scjson')
208
+ xml = Scjson.json_to_xml(data)
209
+ Scjson.xml_to_json(xml)
210
+ else
211
+ return true
212
+ end
213
+ true
214
+ rescue StandardError => e
215
+ warn "Validation failed for #{src}: #{e}"
216
+ false
217
+ end
218
+ end
219
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Agent Name: ruby-scjson-version
4
+ #
5
+ # Part of the scjson project.
6
+ # Developed by Softoboros Technology Inc.
7
+ # Licensed under the BSD 1-Clause License.
8
+
9
+ module Scjson
10
+ VERSION = '0.1.4'
11
+ end
data/lib/scjson.rb ADDED
@@ -0,0 +1,477 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Agent Name: ruby-scjson
4
+ #
5
+ # Part of the scjson project.
6
+ # Developed by Softoboros Technology Inc.
7
+ # Licensed under the BSD 1-Clause License.
8
+
9
+ require 'json'
10
+ require 'nokogiri'
11
+
12
+ require_relative 'scjson/version'
13
+
14
+ # Canonical SCXML <-> scjson conversion for the Ruby agent.
15
+ module Scjson
16
+ XMLNS = 'http://www.w3.org/2005/07/scxml'.freeze
17
+
18
+ ATTRIBUTE_MAP = {
19
+ 'datamodel' => 'datamodel_attribute',
20
+ 'initial' => 'initial_attribute',
21
+ 'type' => 'type_value',
22
+ 'raise' => 'raise_value'
23
+ }.freeze
24
+
25
+ COLLAPSE_ATTRS = %w[expr cond event target delay location name src id].freeze
26
+
27
+ SCXML_ELEMENTS = %w[
28
+ scxml state parallel final history transition invoke finalize datamodel data
29
+ onentry onexit log send cancel raise assign script foreach param if elseif
30
+ else content donedata initial
31
+ ].freeze
32
+
33
+ STRUCTURAL_FIELDS = %w[
34
+ state parallel final history transition invoke finalize datamodel data
35
+ onentry onexit log send cancel raise assign script foreach param if_value
36
+ elseif else_value raise_value content donedata initial
37
+ ].freeze
38
+
39
+ module_function
40
+
41
+ ##
42
+ # Convert an SCXML document to its canonical scjson form.
43
+ #
44
+ # @param [String] xml_str SCXML source document.
45
+ # @param [Boolean] omit_empty Remove empty containers when true.
46
+ # @return [String] Canonical scjson output.
47
+ def xml_to_json(xml_str, omit_empty = true)
48
+ doc = Nokogiri::XML(xml_str) { |cfg| cfg.strict.nonet }
49
+ root = locate_root(doc)
50
+ raise ArgumentError, 'Document missing <scxml> root element' unless root
51
+
52
+ map = element_to_hash(root)
53
+ collapse_whitespace(map)
54
+ remove_empty(map) if omit_empty
55
+ JSON.pretty_generate(map)
56
+ end
57
+
58
+ ##
59
+ # Convert a canonical scjson document back to SCXML.
60
+ #
61
+ # @param [String] json_str Canonical scjson input.
62
+ # @return [String] XML document encoded as UTF-8.
63
+ def json_to_xml(json_str)
64
+ data = JSON.parse(json_str)
65
+ remove_empty(data)
66
+ doc = Nokogiri::XML::Document.new
67
+ doc.encoding = 'utf-8'
68
+ root = build_element(doc, 'scxml', data)
69
+ doc.root = root
70
+ doc.to_xml
71
+ end
72
+
73
+ # ----------------------------
74
+ # Conversion helpers
75
+ # ----------------------------
76
+
77
+ def locate_root(doc)
78
+ doc.at_xpath('/*[local-name()="scxml"]')
79
+ end
80
+ private_class_method :locate_root
81
+
82
+ def local_name(node)
83
+ (node.name || '').split(':').last
84
+ end
85
+ private_class_method :local_name
86
+
87
+ def append_child(hash, key, value)
88
+ if hash.key?(key)
89
+ existing = hash[key]
90
+ if existing.is_a?(Array)
91
+ existing << value
92
+ else
93
+ hash[key] = [existing, value]
94
+ end
95
+ else
96
+ hash[key] = [value]
97
+ end
98
+ end
99
+ private_class_method :append_child
100
+
101
+ def wrap_list(value)
102
+ return [] if value.nil?
103
+ value.is_a?(Array) ? value : [value]
104
+ end
105
+ private_class_method :wrap_list
106
+
107
+ def any_element_to_hash(node)
108
+ result = { 'qname' => node.name }
109
+ text = node.text
110
+ result['text'] = text.to_s if text
111
+ unless node.attribute_nodes.empty?
112
+ attrs = {}
113
+ node.attribute_nodes.each do |attr|
114
+ attrs[attr.name] = attr.value
115
+ end
116
+ result['attributes'] = attrs unless attrs.empty?
117
+ end
118
+ unless node.element_children.empty?
119
+ children = node.element_children.map { |child| any_element_to_hash(child) }
120
+ result['children'] = children unless children.empty?
121
+ end
122
+ result
123
+ end
124
+ private_class_method :any_element_to_hash
125
+
126
+ def element_to_hash(node)
127
+ map = {}
128
+ local = local_name(node)
129
+
130
+ node.attribute_nodes.each do |attr|
131
+ name = local_name(attr)
132
+ value = attr.value
133
+ if local == 'transition' && name == 'target'
134
+ map['target'] = value.split(/\s+/)
135
+ elsif name == 'initial'
136
+ tokens = value.split(/\s+/)
137
+ key = local == 'scxml' ? 'initial' : 'initial_attribute'
138
+ map[key] = tokens
139
+ elsif name == 'version'
140
+ number = begin
141
+ Float(value)
142
+ rescue StandardError
143
+ nil
144
+ end
145
+ map['version'] = number ? number : value
146
+ elsif name == 'datamodel'
147
+ map['datamodel_attribute'] = value
148
+ elsif name == 'type'
149
+ map['type_value'] = value
150
+ elsif name == 'raise'
151
+ map['raise_value'] = value
152
+ elsif local == 'send' && name == 'delay'
153
+ map['delay'] = value
154
+ elsif local == 'send' && name == 'event'
155
+ map['event'] = value
156
+ elsif name == 'xmlns'
157
+ next
158
+ else
159
+ map[name] = value
160
+ end
161
+ end
162
+
163
+ if local == 'assign'
164
+ map['type_value'] ||= 'replacechildren'
165
+ end
166
+ if local == 'send'
167
+ map['type_value'] ||= 'scxml'
168
+ map['delay'] ||= '0s'
169
+ end
170
+ if local == 'invoke'
171
+ map['type_value'] ||= 'scxml'
172
+ map['autoforward'] ||= 'false'
173
+ end
174
+ if local == 'assign' && map.key?('id')
175
+ (map['other_attributes'] ||= {})['id'] = map.delete('id')
176
+ end
177
+ if map.key?('intial')
178
+ (map['other_attributes'] ||= {})['intial'] = map.delete('intial')
179
+ end
180
+
181
+ text_items = []
182
+ node.children.each do |child|
183
+ if child.element?
184
+ child_local = local_name(child)
185
+ if SCXML_ELEMENTS.include?(child_local)
186
+ key = case child_local
187
+ when 'if' then 'if_value'
188
+ when 'else' then 'else_value'
189
+ when 'raise' then 'raise_value'
190
+ else child_local
191
+ end
192
+ child_map = element_to_hash(child)
193
+ target_key = if child_local == 'scxml' && local != 'scxml'
194
+ 'content'
195
+ elsif local == 'content' && child_local == 'scxml'
196
+ 'content'
197
+ else
198
+ key
199
+ end
200
+ if %w[initial history].include?(local) && child_local == 'transition'
201
+ map[target_key] = child_map
202
+ else
203
+ append_child(map, target_key, child_map)
204
+ end
205
+ else
206
+ append_child(map, 'content', any_element_to_hash(child))
207
+ end
208
+ elsif child.text?
209
+ value = child.text
210
+ text_items << value if value && !value.strip.empty?
211
+ end
212
+ end
213
+
214
+ text_items.each { |text| append_child(map, 'content', text) }
215
+
216
+ if local == 'scxml'
217
+ map['version'] ||= 1.0
218
+ map['datamodel_attribute'] ||= 'null'
219
+ elsif local == 'donedata'
220
+ content = map['content']
221
+ if content.is_a?(Array) && content.length == 1
222
+ map['content'] = content.first
223
+ end
224
+ end
225
+
226
+ map
227
+ end
228
+ private_class_method :element_to_hash
229
+
230
+ def collapse_whitespace(value)
231
+ case value
232
+ when Array
233
+ value.each { |item| collapse_whitespace(item) }
234
+ when Hash
235
+ value.each do |key, val|
236
+ if (key.end_with?('_attribute') || COLLAPSE_ATTRS.include?(key)) && val.is_a?(String)
237
+ value[key] = val.tr("\n\r\t", ' ')
238
+ else
239
+ collapse_whitespace(val)
240
+ end
241
+ end
242
+ end
243
+ end
244
+ private_class_method :collapse_whitespace
245
+
246
+ PRESERVE_EMPTY_KEYS = %w[expr cond event target id name label text].freeze
247
+
248
+ ALWAYS_KEEP_KEYS = %w[else_value else final onentry].freeze
249
+
250
+ def remove_empty(value, key = nil)
251
+ case value
252
+ when Hash
253
+ value.keys.each do |key|
254
+ remove = remove_empty(value[key], key)
255
+ value.delete(key) if remove
256
+ end
257
+ value.empty? && !ALWAYS_KEEP_KEYS.include?(key)
258
+ when Array
259
+ value.reject! { |item| remove_empty(item, key) }
260
+ value.empty? && !ALWAYS_KEEP_KEYS.include?(key)
261
+ when NilClass
262
+ true
263
+ when String
264
+ if value.empty?
265
+ preserve_empty_string?(key) ? false : true
266
+ else
267
+ false
268
+ end
269
+ else
270
+ false
271
+ end
272
+ end
273
+ private_class_method :remove_empty
274
+
275
+ def preserve_empty_string?(key)
276
+ return false if key.nil?
277
+
278
+ key.end_with?('_attribute') ||
279
+ key.end_with?('_value') ||
280
+ PRESERVE_EMPTY_KEYS.include?(key)
281
+ end
282
+ private_class_method :preserve_empty_string?
283
+
284
+ def join_tokens(value)
285
+ case value
286
+ when Array
287
+ return unless value.all? { |item| item.is_a?(String) || item.is_a?(Numeric) }
288
+ value.map(&:to_s).join(' ')
289
+ when String
290
+ value
291
+ when Numeric
292
+ value.to_s
293
+ else
294
+ nil
295
+ end
296
+ end
297
+ private_class_method :join_tokens
298
+
299
+ def scxml_like?(hash)
300
+ return false unless hash.is_a?(Hash)
301
+
302
+ hash.key?('state') || hash.key?('parallel') || hash.key?('final') ||
303
+ hash.key?('datamodel') || hash.key?('datamodel_attribute') || hash.key?('version')
304
+ end
305
+ private_class_method :scxml_like?
306
+
307
+ def build_element(doc, name, map)
308
+ if map.is_a?(Array) && map.length == 1
309
+ return build_element(doc, name, map.first)
310
+ end
311
+
312
+ if map.is_a?(String)
313
+ element = Nokogiri::XML::Element.new(name, doc)
314
+ element.content = map
315
+ return element
316
+ end
317
+
318
+ raise ArgumentError, 'Expected object for element construction' unless map.is_a?(Hash)
319
+
320
+ element_name = map['qname'] || name
321
+ element = Nokogiri::XML::Element.new(element_name, doc)
322
+
323
+ if name == 'scxml'
324
+ element['xmlns'] ||= XMLNS
325
+ elsif !element_name.include?(':') && !SCXML_ELEMENTS.include?(element_name)
326
+ element['xmlns'] ||= ''
327
+ end
328
+
329
+ if map['text']
330
+ element.add_child(Nokogiri::XML::Text.new(map['text'], doc))
331
+ end
332
+
333
+ if map['attributes'].is_a?(Hash)
334
+ map['attributes'].each do |attr_name, attr_value|
335
+ element[attr_name] = attr_value if attr_value
336
+ end
337
+ end
338
+
339
+ map.each do |key, value|
340
+ next if %w[qname text attributes].include?(key)
341
+
342
+ case key
343
+ when 'content'
344
+ handle_content_nodes(doc, element, value, element_name)
345
+ when 'children'
346
+ wrap_list(value).each do |child_map|
347
+ next unless child_map.is_a?(Hash)
348
+ child_name = child_map['qname'] || 'content'
349
+ element.add_child(build_element(doc, child_name, child_map))
350
+ end
351
+ when 'other_attributes'
352
+ next unless value.is_a?(Hash)
353
+ value.each do |attr_name, attr_value|
354
+ element[attr_name] = join_tokens(attr_value) || attr_value.to_s
355
+ end
356
+ when ->(k) { k.end_with?('_attribute') }
357
+ attr_name = key.sub(/_attribute\z/, '')
358
+ joined = join_tokens(value)
359
+ element[attr_name] = joined if joined
360
+ when 'datamodel_attribute'
361
+ joined = join_tokens(value)
362
+ element['datamodel'] = joined if joined
363
+ when 'type_value'
364
+ joined = join_tokens(value)
365
+ element['type'] = joined if joined
366
+ when 'target'
367
+ joined = join_tokens(value)
368
+ element['target'] = joined if joined
369
+ when 'delay', 'event'
370
+ joined = join_tokens(value)
371
+ element[key] = joined if joined
372
+ when 'initial'
373
+ joined = join_tokens(value)
374
+ if joined
375
+ element['initial'] = joined
376
+ else
377
+ wrap_list(value).each do |child|
378
+ element.add_child(build_element(doc, 'initial', child))
379
+ end
380
+ next
381
+ end
382
+ when 'version'
383
+ element['version'] = value.to_s
384
+ else
385
+ child_name = case key
386
+ when 'if_value' then 'if'
387
+ when 'else_value' then 'else'
388
+ when 'raise_value' then 'raise'
389
+ else key
390
+ end
391
+
392
+ if STRUCTURAL_FIELDS.include?(key) || %w[if_value else_value raise_value].include?(key)
393
+ wrap_list(value).each do |child|
394
+ element.add_child(build_element(doc, child_name, child))
395
+ end
396
+ elsif value.is_a?(Array) && value.all? { |item| !item.is_a?(Hash) }
397
+ element[key] = join_tokens(value)
398
+ elsif value.is_a?(Hash)
399
+ element.add_child(build_element(doc, child_name, value))
400
+ elsif !value.nil?
401
+ element[key] = value.to_s
402
+ end
403
+ end
404
+ end
405
+
406
+ element
407
+ end
408
+ private_class_method :build_element
409
+
410
+ def handle_content_nodes(doc, element, value, parent_name)
411
+ items = wrap_list(value)
412
+ items.each do |item|
413
+ if item.is_a?(String)
414
+ if parent_name == 'script'
415
+ element.add_child(Nokogiri::XML::Text.new(item, doc))
416
+ elsif parent_name == 'data'
417
+ element.add_child(Nokogiri::XML::Text.new(item, doc))
418
+ else
419
+ content_element = Nokogiri::XML::Element.new('content', doc)
420
+ content_element.add_child(Nokogiri::XML::Text.new(item, doc))
421
+ element.add_child(content_element)
422
+ end
423
+ next
424
+ end
425
+
426
+ next unless item.is_a?(Hash)
427
+
428
+ if parent_name == 'send' && item.keys == ['content']
429
+ wrap_list(item['content']).each do |inner|
430
+ content_element = Nokogiri::XML::Element.new('content', doc)
431
+ if inner.is_a?(String)
432
+ content_element.add_child(Nokogiri::XML::Text.new(inner, doc))
433
+ elsif inner.is_a?(Hash)
434
+ content_element.add_child(build_element(doc, 'content', inner))
435
+ end
436
+ element.add_child(content_element)
437
+ end
438
+ next
439
+ end
440
+
441
+ if parent_name == 'donedata' && item.keys == ['content']
442
+ content_element = Nokogiri::XML::Element.new('content', doc)
443
+ wrap_list(item['content']).each do |inner|
444
+ if inner.is_a?(String)
445
+ content_element.add_child(Nokogiri::XML::Text.new(inner, doc))
446
+ elsif inner.is_a?(Hash)
447
+ content_element.add_child(build_element(doc, 'content', inner))
448
+ end
449
+ end
450
+ element.add_child(content_element)
451
+ next
452
+ end
453
+
454
+ if item.key?('qname')
455
+ child = build_element(doc, item['qname'], item)
456
+ element.add_child(child)
457
+ next
458
+ end
459
+
460
+ child_name = if scxml_like?(item)
461
+ 'scxml'
462
+ elsif parent_name == 'script'
463
+ 'content'
464
+ else
465
+ 'content'
466
+ end
467
+
468
+ if parent_name == 'data' && child_name == 'content'
469
+ element.add_child(Nokogiri::XML::Text.new(item['content'].to_s, doc))
470
+ else
471
+ child_element = build_element(doc, child_name, item)
472
+ element.add_child(child_element)
473
+ end
474
+ end
475
+ end
476
+ private_class_method :handle_content_nodes
477
+ end
metadata ADDED
@@ -0,0 +1,62 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: scjson
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.3
5
+ platform: ruby
6
+ authors:
7
+ - Softoboros Technology Inc.
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2025-10-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: nokogiri
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ description:
28
+ email:
29
+ - info@softoboros.com
30
+ executables:
31
+ - scjson
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - bin/scjson
36
+ - lib/scjson.rb
37
+ - lib/scjson/cli.rb
38
+ - lib/scjson/version.rb
39
+ homepage:
40
+ licenses:
41
+ - BSD-1-Clause
42
+ metadata: {}
43
+ post_install_message:
44
+ rdoc_options: []
45
+ require_paths:
46
+ - lib
47
+ required_ruby_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: '0'
52
+ required_rubygems_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: '0'
57
+ requirements: []
58
+ rubygems_version: 3.4.19
59
+ signing_key:
60
+ specification_version: 4
61
+ summary: SCXML <-> scjson converter and validator
62
+ test_files: []