scjson 0.3.5 → 0.4.0
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/lib/scjson/version.rb +1 -1
- data/lib/scjson.rb +176 -23
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cf4ac84d8fbd3c14f1f60e0e4b36bb543b9d0c0bbf018d7dcd21c070a2503553
|
|
4
|
+
data.tar.gz: 1ed8ff218919ff5709709c8f880f13cdec73a622a3890ac5e305a296aa7fe806
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1fad325a32b3b769dac3be62beab1e6013d60e1f6b5690aeea076c74f6446547e0caa4474780c5c04694837b120833b48dc236addf53454a84e7377070fae8e0
|
|
7
|
+
data.tar.gz: 01acafaa1b2ee89da044ee309efb93becddb66abea2f3f52bda40e9970807586a42d91498b30edb1b51bceb2908c9d191b6330acf3355d2e60f2d7eca2a0e55e
|
data/lib/scjson/version.rb
CHANGED
data/lib/scjson.rb
CHANGED
|
@@ -21,6 +21,8 @@ require_relative 'scjson/types'
|
|
|
21
21
|
# Canonical SCXML <-> scjson conversion for the Ruby agent.
|
|
22
22
|
module Scjson
|
|
23
23
|
XMLNS = 'http://www.w3.org/2005/07/scxml'.freeze
|
|
24
|
+
XINCLUDE_NS = 'http://www.w3.org/2001/XInclude'.freeze
|
|
25
|
+
XINCLUDE_CLARK_INCLUDE = "{#{XINCLUDE_NS}}include".freeze
|
|
24
26
|
|
|
25
27
|
ATTRIBUTE_MAP = {
|
|
26
28
|
'datamodel' => 'datamodel_attribute',
|
|
@@ -37,6 +39,8 @@ module Scjson
|
|
|
37
39
|
else content donedata initial
|
|
38
40
|
].freeze
|
|
39
41
|
|
|
42
|
+
SOURCE_BODY_TAGS = %w[script data].freeze
|
|
43
|
+
|
|
40
44
|
STRUCTURAL_FIELDS = %w[
|
|
41
45
|
state parallel final history transition invoke finalize datamodel data
|
|
42
46
|
onentry onexit log send cancel raise assign script foreach param if_value
|
|
@@ -58,6 +62,7 @@ module Scjson
|
|
|
58
62
|
raise ArgumentError, 'Document missing <scxml> root element' unless root
|
|
59
63
|
|
|
60
64
|
map = element_to_hash(root)
|
|
65
|
+
attach_root_sibling_comments(doc, root, map)
|
|
61
66
|
collapse_whitespace(map)
|
|
62
67
|
remove_empty(map) if omit_empty
|
|
63
68
|
return JSON.pretty_generate(map)
|
|
@@ -102,6 +107,7 @@ module Scjson
|
|
|
102
107
|
doc.encoding = 'utf-8'
|
|
103
108
|
root = build_element(doc, 'scxml', data)
|
|
104
109
|
doc.root = root
|
|
110
|
+
add_preceding_help_text_comments(doc, root, data)
|
|
105
111
|
return doc.to_xml
|
|
106
112
|
end
|
|
107
113
|
# Fallback: use Python CLI converter when Nokogiri is unavailable.
|
|
@@ -144,6 +150,36 @@ module Scjson
|
|
|
144
150
|
end
|
|
145
151
|
private_class_method :local_name
|
|
146
152
|
|
|
153
|
+
def scxml_element?(node)
|
|
154
|
+
ns = node.namespace&.href
|
|
155
|
+
SCXML_ELEMENTS.include?(local_name(node)) && (ns.nil? || ns.empty? || ns == XMLNS)
|
|
156
|
+
end
|
|
157
|
+
private_class_method :scxml_element?
|
|
158
|
+
|
|
159
|
+
def extension_element?(node)
|
|
160
|
+
ns = node.namespace&.href
|
|
161
|
+
!ns.nil? && !ns.empty? && ns != XMLNS
|
|
162
|
+
end
|
|
163
|
+
private_class_method :extension_element?
|
|
164
|
+
|
|
165
|
+
def comment_node?(node)
|
|
166
|
+
node.respond_to?(:comment?) && node.comment?
|
|
167
|
+
end
|
|
168
|
+
private_class_method :comment_node?
|
|
169
|
+
|
|
170
|
+
def processing_instruction_node?(node)
|
|
171
|
+
node.respond_to?(:processing_instruction?) && node.processing_instruction?
|
|
172
|
+
end
|
|
173
|
+
private_class_method :processing_instruction_node?
|
|
174
|
+
|
|
175
|
+
def clark_name(node)
|
|
176
|
+
ns = node.namespace&.href
|
|
177
|
+
return node.name if ns.nil? || ns.empty?
|
|
178
|
+
|
|
179
|
+
"{#{ns}}#{local_name(node)}"
|
|
180
|
+
end
|
|
181
|
+
private_class_method :clark_name
|
|
182
|
+
|
|
147
183
|
def append_child(hash, key, value)
|
|
148
184
|
if hash.key?(key)
|
|
149
185
|
existing = hash[key]
|
|
@@ -164,8 +200,81 @@ module Scjson
|
|
|
164
200
|
end
|
|
165
201
|
private_class_method :wrap_list
|
|
166
202
|
|
|
203
|
+
def repair_comment_text(raw)
|
|
204
|
+
text = raw.to_s
|
|
205
|
+
return text.strip unless text.include?("\n")
|
|
206
|
+
|
|
207
|
+
lines = text.lines.map { |line| line.chomp("\n") }
|
|
208
|
+
lines.shift while !lines.empty? && lines.first.strip.empty?
|
|
209
|
+
lines.pop while !lines.empty? && lines.last.strip.empty?
|
|
210
|
+
return '' if lines.empty?
|
|
211
|
+
|
|
212
|
+
indents = lines.map do |line|
|
|
213
|
+
next if line.strip.empty?
|
|
214
|
+
|
|
215
|
+
line.length - line.sub(/\A[ \t]+/, '').length
|
|
216
|
+
end.compact
|
|
217
|
+
common = indents.empty? ? 0 : indents.min
|
|
218
|
+
lines = lines.map { |line| line.length >= common ? line[common, line.length] : line } if common.positive?
|
|
219
|
+
lines.join("\n")
|
|
220
|
+
end
|
|
221
|
+
private_class_method :repair_comment_text
|
|
222
|
+
|
|
223
|
+
def emit_safe_comment_text(text)
|
|
224
|
+
safe = text.to_s.gsub('--', '- -')
|
|
225
|
+
safe = "#{safe} " if safe.end_with?('-')
|
|
226
|
+
safe
|
|
227
|
+
end
|
|
228
|
+
private_class_method :emit_safe_comment_text
|
|
229
|
+
|
|
230
|
+
def append_help_text(map, comments, prepend: false)
|
|
231
|
+
repaired = comments.map(&:to_s)
|
|
232
|
+
return if repaired.empty?
|
|
233
|
+
|
|
234
|
+
existing = map['help_text']
|
|
235
|
+
if existing
|
|
236
|
+
existing = wrap_list(existing)
|
|
237
|
+
map['help_text'] = prepend ? repaired + existing : existing + repaired
|
|
238
|
+
else
|
|
239
|
+
map['help_text'] = repaired
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
private_class_method :append_help_text
|
|
243
|
+
|
|
244
|
+
def add_preceding_help_text_comments(doc, element, map)
|
|
245
|
+
return unless map.is_a?(Hash)
|
|
246
|
+
|
|
247
|
+
wrap_list(map['help_text']).each do |text|
|
|
248
|
+
element.add_previous_sibling(Nokogiri::XML::Comment.new(doc, emit_safe_comment_text(text)))
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
private_class_method :add_preceding_help_text_comments
|
|
252
|
+
|
|
253
|
+
def add_child_element(doc, parent, child_name, child_map)
|
|
254
|
+
child = build_element(doc, child_name, child_map)
|
|
255
|
+
if child_map.is_a?(Hash)
|
|
256
|
+
wrap_list(child_map['help_text']).each do |text|
|
|
257
|
+
parent.add_child(Nokogiri::XML::Comment.new(doc, emit_safe_comment_text(text)))
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
parent.add_child(child)
|
|
261
|
+
end
|
|
262
|
+
private_class_method :add_child_element
|
|
263
|
+
|
|
264
|
+
def attach_root_sibling_comments(doc, root, map)
|
|
265
|
+
comments = []
|
|
266
|
+
doc.children.each do |child|
|
|
267
|
+
next if child.equal?(root)
|
|
268
|
+
next unless comment_node?(child)
|
|
269
|
+
|
|
270
|
+
comments << repair_comment_text(child.text || '')
|
|
271
|
+
end
|
|
272
|
+
append_help_text(map, comments) unless comments.empty?
|
|
273
|
+
end
|
|
274
|
+
private_class_method :attach_root_sibling_comments
|
|
275
|
+
|
|
167
276
|
def any_element_to_hash(node)
|
|
168
|
-
result = { 'qname' => node
|
|
277
|
+
result = { 'qname' => clark_name(node) }
|
|
169
278
|
text = node.text
|
|
170
279
|
result['text'] = text.to_s if text
|
|
171
280
|
unless node.attribute_nodes.empty?
|
|
@@ -183,9 +292,11 @@ module Scjson
|
|
|
183
292
|
end
|
|
184
293
|
private_class_method :any_element_to_hash
|
|
185
294
|
|
|
186
|
-
def element_to_hash(node)
|
|
295
|
+
def element_to_hash(node, inside_source_body = false, inside_extension = false)
|
|
187
296
|
map = {}
|
|
188
297
|
local = local_name(node)
|
|
298
|
+
elem_in_source = inside_source_body || SOURCE_BODY_TAGS.include?(local)
|
|
299
|
+
elem_in_extension = inside_extension || (!SCXML_ELEMENTS.include?(local) && local != 'scxml')
|
|
189
300
|
|
|
190
301
|
node.attribute_nodes.each do |attr|
|
|
191
302
|
name = local_name(attr)
|
|
@@ -239,17 +350,32 @@ module Scjson
|
|
|
239
350
|
end
|
|
240
351
|
|
|
241
352
|
text_items = []
|
|
353
|
+
pending_comments = []
|
|
242
354
|
node.children.each do |child|
|
|
243
|
-
if child
|
|
355
|
+
if comment_node?(child)
|
|
356
|
+
pending_comments << repair_comment_text(child.text || '')
|
|
357
|
+
elsif processing_instruction_node?(child)
|
|
358
|
+
next
|
|
359
|
+
elsif child.element?
|
|
244
360
|
child_local = local_name(child)
|
|
245
|
-
if
|
|
361
|
+
if scxml_element?(child)
|
|
246
362
|
key = case child_local
|
|
247
363
|
when 'if' then 'if_value'
|
|
248
364
|
when 'else' then 'else_value'
|
|
249
365
|
when 'raise' then 'raise_value'
|
|
250
366
|
else child_local
|
|
251
367
|
end
|
|
252
|
-
child_map = element_to_hash(child)
|
|
368
|
+
child_map = element_to_hash(child, elem_in_source || local == 'content', elem_in_extension)
|
|
369
|
+
target_eligible = SCXML_ELEMENTS.include?(child_local) && !elem_in_source && !elem_in_extension
|
|
370
|
+
if !pending_comments.empty? && local == 'content' && child_local == 'scxml'
|
|
371
|
+
# Comments inside an inline <content> payload are payload-local,
|
|
372
|
+
# not authoring metadata for the nested machine.
|
|
373
|
+
elsif !pending_comments.empty? && target_eligible
|
|
374
|
+
append_help_text(child_map, pending_comments, prepend: true)
|
|
375
|
+
elsif !pending_comments.empty? && SCXML_ELEMENTS.include?(local) && !elem_in_source && !elem_in_extension
|
|
376
|
+
append_help_text(map, pending_comments)
|
|
377
|
+
end
|
|
378
|
+
pending_comments = []
|
|
253
379
|
target_key = if child_local == 'scxml' && local != 'scxml'
|
|
254
380
|
'content'
|
|
255
381
|
elsif local == 'content' && child_local == 'scxml'
|
|
@@ -263,14 +389,27 @@ module Scjson
|
|
|
263
389
|
append_child(map, target_key, child_map)
|
|
264
390
|
end
|
|
265
391
|
else
|
|
266
|
-
|
|
392
|
+
if !pending_comments.empty? && SCXML_ELEMENTS.include?(local) && !elem_in_source && !elem_in_extension
|
|
393
|
+
append_help_text(map, pending_comments)
|
|
394
|
+
end
|
|
395
|
+
pending_comments = []
|
|
396
|
+
target_key = extension_element?(child) ? 'other_element' : 'content'
|
|
397
|
+
append_child(map, target_key, any_element_to_hash(child))
|
|
267
398
|
end
|
|
268
399
|
elsif child.text?
|
|
269
400
|
value = child.text
|
|
401
|
+
if value && !value.strip.empty? && !pending_comments.empty?
|
|
402
|
+
append_help_text(map, pending_comments) if SCXML_ELEMENTS.include?(local) && !elem_in_source && !elem_in_extension
|
|
403
|
+
pending_comments = []
|
|
404
|
+
end
|
|
270
405
|
text_items << value if value && !value.strip.empty?
|
|
271
406
|
end
|
|
272
407
|
end
|
|
273
408
|
|
|
409
|
+
if !pending_comments.empty? && SCXML_ELEMENTS.include?(local) && !elem_in_source && !elem_in_extension
|
|
410
|
+
append_help_text(map, pending_comments)
|
|
411
|
+
end
|
|
412
|
+
|
|
274
413
|
text_items.each { |text| append_child(map, 'content', text) }
|
|
275
414
|
|
|
276
415
|
if local == 'scxml'
|
|
@@ -378,6 +517,11 @@ module Scjson
|
|
|
378
517
|
raise ArgumentError, 'Expected object for element construction' unless map.is_a?(Hash)
|
|
379
518
|
|
|
380
519
|
element_name = map['qname'] || name
|
|
520
|
+
attrs = map['attributes'].is_a?(Hash) ? map['attributes'].dup : {}
|
|
521
|
+
if element_name == XINCLUDE_CLARK_INCLUDE
|
|
522
|
+
element_name = 'xi:include'
|
|
523
|
+
attrs['xmlns:xi'] ||= XINCLUDE_NS
|
|
524
|
+
end
|
|
381
525
|
element = Nokogiri::XML::Element.new(element_name, doc)
|
|
382
526
|
|
|
383
527
|
if name == 'scxml'
|
|
@@ -390,23 +534,28 @@ module Scjson
|
|
|
390
534
|
element.add_child(Nokogiri::XML::Text.new(map['text'], doc))
|
|
391
535
|
end
|
|
392
536
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
element[attr_name] = attr_value if attr_value
|
|
396
|
-
end
|
|
537
|
+
attrs.each do |attr_name, attr_value|
|
|
538
|
+
element[attr_name] = attr_value if attr_value
|
|
397
539
|
end
|
|
398
540
|
|
|
399
541
|
map.each do |key, value|
|
|
400
|
-
next if %w[qname text attributes].include?(key)
|
|
542
|
+
next if %w[qname text attributes help_text].include?(key)
|
|
401
543
|
|
|
402
544
|
case key
|
|
403
545
|
when 'content'
|
|
404
546
|
handle_content_nodes(doc, element, value, element_name)
|
|
547
|
+
when 'other_element'
|
|
548
|
+
wrap_list(value).each do |child_map|
|
|
549
|
+
next unless child_map.is_a?(Hash)
|
|
550
|
+
|
|
551
|
+
child_name = child_map['qname'] || 'content'
|
|
552
|
+
add_child_element(doc, element, child_name, child_map)
|
|
553
|
+
end
|
|
405
554
|
when 'children'
|
|
406
555
|
wrap_list(value).each do |child_map|
|
|
407
556
|
next unless child_map.is_a?(Hash)
|
|
408
557
|
child_name = child_map['qname'] || 'content'
|
|
409
|
-
|
|
558
|
+
add_child_element(doc, element, child_name, child_map)
|
|
410
559
|
end
|
|
411
560
|
when 'other_attributes'
|
|
412
561
|
next unless value.is_a?(Hash)
|
|
@@ -435,7 +584,7 @@ module Scjson
|
|
|
435
584
|
element['initial'] = joined
|
|
436
585
|
else
|
|
437
586
|
wrap_list(value).each do |child|
|
|
438
|
-
|
|
587
|
+
add_child_element(doc, element, 'initial', child)
|
|
439
588
|
end
|
|
440
589
|
next
|
|
441
590
|
end
|
|
@@ -451,12 +600,12 @@ module Scjson
|
|
|
451
600
|
|
|
452
601
|
if STRUCTURAL_FIELDS.include?(key) || %w[if_value else_value raise_value].include?(key)
|
|
453
602
|
wrap_list(value).each do |child|
|
|
454
|
-
|
|
603
|
+
add_child_element(doc, element, child_name, child)
|
|
455
604
|
end
|
|
456
605
|
elsif value.is_a?(Array) && value.all? { |item| !item.is_a?(Hash) }
|
|
457
606
|
element[key] = join_tokens(value)
|
|
458
607
|
elsif value.is_a?(Hash)
|
|
459
|
-
|
|
608
|
+
add_child_element(doc, element, child_name, value)
|
|
460
609
|
elsif !value.nil?
|
|
461
610
|
element[key] = value.to_s
|
|
462
611
|
end
|
|
@@ -485,26 +634,32 @@ module Scjson
|
|
|
485
634
|
|
|
486
635
|
next unless item.is_a?(Hash)
|
|
487
636
|
|
|
488
|
-
if parent_name == 'send' && item.keys == ['content']
|
|
637
|
+
if parent_name == 'send' && (item.keys - ['help_text']) == ['content']
|
|
638
|
+
wrap_list(item['help_text']).each do |text|
|
|
639
|
+
element.add_child(Nokogiri::XML::Comment.new(doc, emit_safe_comment_text(text)))
|
|
640
|
+
end
|
|
489
641
|
wrap_list(item['content']).each do |inner|
|
|
490
642
|
content_element = Nokogiri::XML::Element.new('content', doc)
|
|
491
643
|
if inner.is_a?(String)
|
|
492
644
|
content_element.add_child(Nokogiri::XML::Text.new(inner, doc))
|
|
493
645
|
elsif inner.is_a?(Hash)
|
|
494
|
-
|
|
646
|
+
add_child_element(doc, content_element, 'content', inner)
|
|
495
647
|
end
|
|
496
648
|
element.add_child(content_element)
|
|
497
649
|
end
|
|
498
650
|
next
|
|
499
651
|
end
|
|
500
652
|
|
|
501
|
-
if parent_name == 'donedata' && item.keys == ['content']
|
|
653
|
+
if parent_name == 'donedata' && (item.keys - ['help_text']) == ['content']
|
|
654
|
+
wrap_list(item['help_text']).each do |text|
|
|
655
|
+
element.add_child(Nokogiri::XML::Comment.new(doc, emit_safe_comment_text(text)))
|
|
656
|
+
end
|
|
502
657
|
content_element = Nokogiri::XML::Element.new('content', doc)
|
|
503
658
|
wrap_list(item['content']).each do |inner|
|
|
504
659
|
if inner.is_a?(String)
|
|
505
660
|
content_element.add_child(Nokogiri::XML::Text.new(inner, doc))
|
|
506
661
|
elsif inner.is_a?(Hash)
|
|
507
|
-
|
|
662
|
+
add_child_element(doc, content_element, 'content', inner)
|
|
508
663
|
end
|
|
509
664
|
end
|
|
510
665
|
element.add_child(content_element)
|
|
@@ -512,8 +667,7 @@ module Scjson
|
|
|
512
667
|
end
|
|
513
668
|
|
|
514
669
|
if item.key?('qname')
|
|
515
|
-
|
|
516
|
-
element.add_child(child)
|
|
670
|
+
add_child_element(doc, element, item['qname'], item)
|
|
517
671
|
next
|
|
518
672
|
end
|
|
519
673
|
|
|
@@ -528,8 +682,7 @@ module Scjson
|
|
|
528
682
|
if parent_name == 'data' && child_name == 'content'
|
|
529
683
|
element.add_child(Nokogiri::XML::Text.new(item['content'].to_s, doc))
|
|
530
684
|
else
|
|
531
|
-
|
|
532
|
-
element.add_child(child_element)
|
|
685
|
+
add_child_element(doc, element, child_name, item)
|
|
533
686
|
end
|
|
534
687
|
end
|
|
535
688
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: scjson
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Softoboros Technology Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-05-26 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: nokogiri
|