statifier 0.0.1 → 0.1.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.
Files changed (109) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +35 -0
  3. data/{LICENSE.txt → LICENSE} +1 -1
  4. data/README.md +601 -16
  5. data/lib/statifier/accepts.rb +167 -0
  6. data/lib/statifier/basichttp/decode.rb +142 -0
  7. data/lib/statifier/basichttp/encoding.rb +224 -0
  8. data/lib/statifier/basichttp/net_http_transport.rb +157 -0
  9. data/lib/statifier/basichttp/outbound.rb +87 -0
  10. data/lib/statifier/basichttp/processor.rb +170 -0
  11. data/lib/statifier/basichttp/request.rb +94 -0
  12. data/lib/statifier/basichttp/response.rb +34 -0
  13. data/lib/statifier/basichttp.rb +47 -0
  14. data/lib/statifier/compiler/content.rb +131 -0
  15. data/lib/statifier/compiler/error.rb +30 -0
  16. data/lib/statifier/compiler/expressions.rb +64 -0
  17. data/lib/statifier/compiler/numbering.rb +145 -0
  18. data/lib/statifier/compiler/parts.rb +144 -0
  19. data/lib/statifier/compiler/walk.rb +61 -0
  20. data/lib/statifier/compiler.rb +82 -0
  21. data/lib/statifier/datamodel/data_binding.rb +114 -0
  22. data/lib/statifier/datamodel/delay.rb +54 -0
  23. data/lib/statifier/datamodel/location.rb +145 -0
  24. data/lib/statifier/datamodel/program.rb +82 -0
  25. data/lib/statifier/datamodel/reason_value.rb +42 -0
  26. data/lib/statifier/datamodel/reasons.rb +88 -0
  27. data/lib/statifier/datamodel/stores.rb +169 -0
  28. data/lib/statifier/datamodel/system_variables.rb +69 -0
  29. data/lib/statifier/datamodel.rb +120 -0
  30. data/lib/statifier/document/data.rb +36 -0
  31. data/lib/statifier/document/executable.rb +69 -0
  32. data/lib/statifier/document/invoke.rb +17 -0
  33. data/lib/statifier/document/states.rb +40 -0
  34. data/lib/statifier/document.rb +51 -0
  35. data/lib/statifier/driver/calls.rb +183 -0
  36. data/lib/statifier/driver/checks.rb +110 -0
  37. data/lib/statifier/driver/delivery.rb +45 -0
  38. data/lib/statifier/driver/invocations.rb +129 -0
  39. data/lib/statifier/driver/mailbox.rb +120 -0
  40. data/lib/statifier/driver/routing.rb +193 -0
  41. data/lib/statifier/driver/session_id.rb +40 -0
  42. data/lib/statifier/driver/work.rb +196 -0
  43. data/lib/statifier/driver.rb +205 -0
  44. data/lib/statifier/effect.rb +208 -0
  45. data/lib/statifier/event.rb +67 -0
  46. data/lib/statifier/frozen.rb +22 -0
  47. data/lib/statifier/interpreter/blocks.rb +142 -0
  48. data/lib/statifier/interpreter/content.rb +228 -0
  49. data/lib/statifier/interpreter/entering.rb +141 -0
  50. data/lib/statifier/interpreter/exit_entry.rb +174 -0
  51. data/lib/statifier/interpreter/foreach.rb +83 -0
  52. data/lib/statifier/interpreter/invoke/arguments.rb +52 -0
  53. data/lib/statifier/interpreter/invoke/passes.rb +143 -0
  54. data/lib/statifier/interpreter/invoke.rb +145 -0
  55. data/lib/statifier/interpreter/loop.rb +170 -0
  56. data/lib/statifier/interpreter/machine_state.rb +152 -0
  57. data/lib/statifier/interpreter/macrostep.rb +185 -0
  58. data/lib/statifier/interpreter/name_match.rb +43 -0
  59. data/lib/statifier/interpreter/selection.rb +221 -0
  60. data/lib/statifier/interpreter/send.rb +223 -0
  61. data/lib/statifier/interpreter/step.rb +138 -0
  62. data/lib/statifier/interpreter/stop.rb +66 -0
  63. data/lib/statifier/interpreter/targets.rb +107 -0
  64. data/lib/statifier/interpreter.rb +57 -0
  65. data/lib/statifier/lowering/branches.rb +84 -0
  66. data/lib/statifier/lowering/data.rb +102 -0
  67. data/lib/statifier/lowering/error.rb +102 -0
  68. data/lib/statifier/lowering/executable.rb +106 -0
  69. data/lib/statifier/lowering/placement.rb +65 -0
  70. data/lib/statifier/lowering/reading.rb +53 -0
  71. data/lib/statifier/lowering/states.rb +111 -0
  72. data/lib/statifier/lowering.rb +129 -0
  73. data/lib/statifier/machine/content.rb +81 -0
  74. data/lib/statifier/machine/data.rb +42 -0
  75. data/lib/statifier/machine/states.rb +56 -0
  76. data/lib/statifier/machine/values.rb +37 -0
  77. data/lib/statifier/machine.rb +103 -0
  78. data/lib/statifier/position/rebuild.rb +84 -0
  79. data/lib/statifier/position/shape.rb +73 -0
  80. data/lib/statifier/position/text.rb +43 -0
  81. data/lib/statifier/position.rb +206 -0
  82. data/lib/statifier/validator/attributes.rb +35 -0
  83. data/lib/statifier/validator/context.rb +74 -0
  84. data/lib/statifier/validator/data.rb +136 -0
  85. data/lib/statifier/validator/error.rb +90 -0
  86. data/lib/statifier/validator/executable.rb +139 -0
  87. data/lib/statifier/validator/history.rb +81 -0
  88. data/lib/statifier/validator/messages.rb +142 -0
  89. data/lib/statifier/validator/states.rb +161 -0
  90. data/lib/statifier/validator.rb +94 -0
  91. data/lib/statifier/version.rb +4 -1
  92. data/lib/statifier/xml/character_data.rb +142 -0
  93. data/lib/statifier/xml/cursor.rb +131 -0
  94. data/lib/statifier/xml/markup.rb +153 -0
  95. data/lib/statifier/xml/nodes.rb +50 -0
  96. data/lib/statifier/xml/parser.rb +121 -0
  97. data/lib/statifier/xml/scope.rb +46 -0
  98. data/lib/statifier/xml/tags.rb +112 -0
  99. data/lib/statifier/xml.rb +85 -0
  100. data/lib/statifier.rb +386 -2
  101. metadata +129 -20
  102. data/.gitignore +0 -8
  103. data/CODE_OF_CONDUCT.md +0 -74
  104. data/Gemfile +0 -11
  105. data/Gemfile.lock +0 -33
  106. data/Rakefile +0 -10
  107. data/bin/console +0 -8
  108. data/bin/setup +0 -6
  109. data/statifier.gemspec +0 -34
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Statifier
4
+ module XML
5
+ # One parse of one document. The element tree is built with an explicit
6
+ # stack, so nesting depth never touches the call stack.
7
+ #
8
+ # @api private
9
+ class Parser
10
+ include CharacterData
11
+ include Markup
12
+ include Tags
13
+
14
+ # The constructs inside the root element that open with `<` and are
15
+ # not tags.
16
+ OTHER_MARKUP = /<(?:!--|!\[CDATA\[|\?)/
17
+
18
+ # @param text [String] the document, valid UTF-8 with no byte order mark
19
+ def initialize(text)
20
+ @cursor = Cursor.new(text)
21
+ @stack = []
22
+ @run = nil
23
+ @doctype = false
24
+ end
25
+
26
+ # @return [Element] the root element
27
+ # @raise [Refused] when the document is malformed
28
+ def parse
29
+ skip_misc(prolog: true)
30
+ @cursor.refuse(:missing_root, "no root element") if @cursor.eos?
31
+ @cursor.refuse(:content_before_root, "character data before the root element") unless @cursor.at?(START_TAG)
32
+ root = read_root
33
+ skip_misc(prolog: false)
34
+ @cursor.refuse(:content_after_root, "content after the root element") unless @cursor.eos?
35
+ root
36
+ end
37
+
38
+ private
39
+
40
+ def read_root
41
+ root = open_tag
42
+ root = step until root
43
+ root
44
+ end
45
+
46
+ # Reads one construct inside the root element; answers the root once
47
+ # its end tag is read.
48
+ def step
49
+ refuse_unclosed if @cursor.eos?
50
+ return close_tag if @cursor.at?("</")
51
+ return open_tag if start_tag?
52
+
53
+ read_content
54
+ nil
55
+ end
56
+
57
+ # Whether a start tag begins at the read position: a `<` that opens no
58
+ # other construct.
59
+ def start_tag? = @cursor.at?("<") && !@cursor.at?(OTHER_MARKUP)
60
+
61
+ # Reads a comment, a CDATA section, a processing instruction or a run of
62
+ # character data.
63
+ def read_content
64
+ if @cursor.at?("<!--") then skip_comment
65
+ elsif @cursor.at?("<![CDATA[") then read_cdata(run_buffer)
66
+ elsif @cursor.at?("<?") then skip_processing_instruction
67
+ else read_character_data(run_buffer)
68
+ end
69
+ end
70
+
71
+ # Reads a start tag; answers the root when the root closes itself.
72
+ def open_tag
73
+ flush
74
+ element, closed = read_start_tag(@stack.last&.scope || Scope::EMPTY)
75
+ @tag_end = @cursor.mark
76
+ return adopt(element.finish(@cursor.span(element.start))) if closed
77
+
78
+ @stack.push(element)
79
+ nil
80
+ end
81
+
82
+ # Reads an end tag; answers the root when it closes the root.
83
+ def close_tag
84
+ flush
85
+ element = read_end_tag(@stack.pop)
86
+ @tag_end = @cursor.mark
87
+ adopt(element)
88
+ end
89
+
90
+ # Appends a finished element to its parent, or answers it when it is
91
+ # the root.
92
+ def adopt(element)
93
+ return element if @stack.empty?
94
+
95
+ @stack.last.children << element
96
+ nil
97
+ end
98
+
99
+ # The text run open since the last tag. It starts where that tag ends, as
100
+ # the reference's spans do, so a comment or processing instruction
101
+ # before the first character data is inside the span.
102
+ def run_buffer
103
+ @run ||= [@tag_end, +""]
104
+ @run.last
105
+ end
106
+
107
+ # Closes the open text run, if any, at the read position: the `<` of the
108
+ # next tag.
109
+ def flush
110
+ return unless @run
111
+
112
+ @stack.last.children << Text.new(value: @run.last.freeze, location: @cursor.span(@run.first))
113
+ @run = nil
114
+ end
115
+
116
+ def refuse_unclosed
117
+ @cursor.refuse(:unclosed_element, "element <#{@stack.last.name}> is never closed")
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Statifier
4
+ module XML
5
+ # The namespace declarations in scope at an element: the default
6
+ # namespace (nil when none is declared) and the bound prefixes. Each
7
+ # element folds its own `xmlns` and `xmlns:prefix` attributes over the
8
+ # scope it inherits, so a declaration reaches its element's subtree and
9
+ # no further. A value binds as written, the empty string included: an
10
+ # empty declaration (`xmlns=""`, `xmlns:p=""`) binds the empty URI and
11
+ # undeclares nothing, as the reference's namespace scope does.
12
+ #
13
+ # @api private
14
+ Scope = Data.define(:default, :prefixes) do
15
+ # @param attributes [Array<Attribute>] an element's own attributes
16
+ # @return [Scope] the scope for that element's subtree
17
+ def declare(attributes)
18
+ declared = attributes.select { |attribute| attribute.name.start_with?("xmlns") }
19
+ return self if declared.empty?
20
+
21
+ declared.reduce(self) { |scope, attribute| scope.bind(attribute.name, attribute.value) }
22
+ end
23
+
24
+ # @param name [String] an attribute name, perhaps a declaration
25
+ # @param uri [String] its value
26
+ # @return [Scope] this scope with the declaration folded in, if it is one
27
+ def bind(name, uri)
28
+ return with(default: uri) if name == "xmlns"
29
+ return self unless name.start_with?("xmlns:") && name.length > 6
30
+
31
+ with(prefixes: prefixes.merge(name.delete_prefix("xmlns:") => uri).freeze)
32
+ end
33
+
34
+ # @param name [String] a qualified element name, prefix included
35
+ # @return [String, nil] the namespace it resolves to, or nil when no
36
+ # declaration is in scope for it
37
+ def resolve(name)
38
+ prefix, local = name.split(":", 2)
39
+ local.nil? ? default : prefixes[prefix]
40
+ end
41
+ end
42
+
43
+ # The scope outside the root element: nothing declared.
44
+ Scope::EMPTY = Scope.new(default: nil, prefixes: {}.freeze)
45
+ end
46
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Statifier
4
+ module XML
5
+ # Reading start tags, end tags and attributes.
6
+ #
7
+ # @api private
8
+ module Tags
9
+ # XML 1.0's NameStartChar and NameChar productions.
10
+ NAME_START = "A-Za-z_:\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF" \
11
+ "\\u200C\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD" \
12
+ "\\u{10000}-\\u{EFFFF}"
13
+ NAME = /[#{NAME_START}][#{NAME_START}\-.0-9\u00B7\u0300-\u036F\u203F\u2040]*/
14
+ WHOLE_NAME = /\A#{NAME}\z/
15
+ START_TAG = /<[#{NAME_START}]/
16
+ TAG_END = %r{/?>}
17
+
18
+ # An element whose end tag is still to come.
19
+ OpenElement = Struct.new(:name, :namespace, :attributes, :children, :start, :scope) do
20
+ # @param location [Location] the element's whole span
21
+ # @return [Element] the element, its children frozen
22
+ def finish(location) = Element.new(name:, namespace:, attributes:, children: children.freeze, location:)
23
+ end
24
+
25
+ private
26
+
27
+ # Reads a name at the read position.
28
+ def read_name
29
+ name = @cursor.scan(NAME)
30
+ return name.freeze if name
31
+
32
+ refuse_unexpected_end if @cursor.eos?
33
+ @cursor.refuse(:expected_name, "expected a name")
34
+ end
35
+
36
+ # Reads a start tag, `<name (S attribute)* S? (> | />)`, under the
37
+ # namespace scope it inherits. Answers the open element and whether
38
+ # the tag closed itself.
39
+ def read_start_tag(inherited)
40
+ start = @cursor.mark
41
+ @cursor.skip("<")
42
+ name = read_name
43
+ attributes = read_attributes
44
+ closed = !@cursor.skip("/>").nil?
45
+ @cursor.skip(">") unless closed
46
+ scope = inherited.declare(attributes)
47
+ [OpenElement.new(name, scope.resolve(name), attributes, [], start, scope), closed]
48
+ end
49
+
50
+ def read_attributes
51
+ attributes = []
52
+ loop do
53
+ spaced = @cursor.skip_whitespace
54
+ return attributes.freeze if @cursor.at?(TAG_END)
55
+
56
+ refuse_unexpected_end if @cursor.eos?
57
+ @cursor.refuse(:expected_whitespace, "expected whitespace before an attribute") unless spaced
58
+ attributes << read_attribute
59
+ end
60
+ end
61
+
62
+ # Reads `name S? = S? quoted-value`.
63
+ def read_attribute
64
+ start = @cursor.mark
65
+ name = read_name
66
+ expect_equals(name)
67
+ quote = expect_quote(name)
68
+ value_start = @cursor.mark
69
+ value = read_value(quote)
70
+ value_location = @cursor.span(value_start)
71
+ @cursor.skip(quote)
72
+ Attribute.new(name:, value:, location: @cursor.span(start), value_location:)
73
+ end
74
+
75
+ def expect_equals(name)
76
+ @cursor.skip_whitespace
77
+ unless @cursor.skip("=")
78
+ refuse_unexpected_end if @cursor.eos?
79
+ @cursor.refuse(:expected_equals, "expected '=' after attribute #{name}")
80
+ end
81
+ @cursor.skip_whitespace
82
+ end
83
+
84
+ def expect_quote(name)
85
+ quote = @cursor.scan(/["']/)
86
+ return quote if quote
87
+
88
+ refuse_unexpected_end if @cursor.eos?
89
+ @cursor.refuse(:expected_quote, "expected a quoted value for attribute #{name}")
90
+ end
91
+
92
+ # Reads `</name S? >` and closes `open` with it.
93
+ def read_end_tag(open)
94
+ start = @cursor.mark
95
+ @cursor.skip("</")
96
+ name = read_name
97
+ @cursor.skip_whitespace
98
+ expect_tag_end
99
+ return open.finish(@cursor.span(open.start)) if name == open.name
100
+
101
+ @cursor.refuse(:mismatched_end_tag, "end tag </#{name}> where </#{open.name}> was due", start)
102
+ end
103
+
104
+ def expect_tag_end
105
+ return if @cursor.skip(">")
106
+
107
+ refuse_unexpected_end if @cursor.eos?
108
+ @cursor.refuse(:expected_tag_end, "expected '>' to end the tag")
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "strscan"
4
+
5
+ require_relative "xml/nodes"
6
+ require_relative "xml/cursor"
7
+ require_relative "xml/scope"
8
+ require_relative "xml/character_data"
9
+ require_relative "xml/markup"
10
+ require_relative "xml/tags"
11
+ require_relative "xml/parser"
12
+
13
+ module Statifier
14
+ # XML source in, a generic element tree out: the first step of reading a
15
+ # chart. This layer knows XML and nothing else; a later one gives the names
16
+ # meaning. It is a small hand-written reader of the XML that SCXML
17
+ # documents use, and needs no XML library.
18
+ #
19
+ # Beyond splitting the markup, it normalizes attribute values (XML 1.0
20
+ # section 3.3.3), folds line breaks in character data (section 2.11),
21
+ # expands the five predefined entities and character references, and gives
22
+ # every element the namespace its name resolves to under the declarations
23
+ # in scope. It validates no name against any vocabulary, keeps duplicate
24
+ # attributes and whitespace-only text, and reads no document type
25
+ # definition. Comments, processing instructions, the XML declaration and a
26
+ # document type declaration produce no nodes, and anything outside the
27
+ # root element is discarded.
28
+ #
29
+ # @api private
30
+ module XML
31
+ # The encodings whose strings are read from their bytes as UTF-8.
32
+ READ_AS_BYTES = [Encoding::BINARY, Encoding::US_ASCII].freeze
33
+ private_constant :READ_AS_BYTES
34
+
35
+ # Parses `source` into its root element, or answers why it cannot.
36
+ # Malformed input is never raised: the result carries the refusal, with
37
+ # a message and the line and column where reading stopped.
38
+ #
39
+ # The document is read as UTF-8. A string tagged binary or US-ASCII is
40
+ # read from its bytes; a string in any other encoding is transcoded
41
+ # first, and the byte offsets in every {Location} are then offsets into
42
+ # the transcoded text. A leading byte order mark is refused, as the
43
+ # reference refuses it.
44
+ #
45
+ # @param source [String] the document
46
+ # @return [Result] the root {Element} as `value`, or a {ParseError} as
47
+ # `error`
48
+ # @raise [TypeError] when `source` is not a String
49
+ def self.parse(source)
50
+ raise TypeError, "expected a String, got #{source.class}" unless source.is_a?(String)
51
+
52
+ Result.new(value: Parser.new(utf8(source)).parse, error: nil)
53
+ rescue Refused => e
54
+ Result.new(value: nil, error: e.error)
55
+ end
56
+
57
+ # @api private
58
+ def self.utf8(source)
59
+ text = transcode(source)
60
+ unless text.valid_encoding?
61
+ Cursor.new(text).tap { _1.move_to(first_invalid_byte(text)) }
62
+ .refuse(:invalid_encoding, "the document is not valid UTF-8")
63
+ end
64
+ Cursor.new(text).refuse(:byte_order_mark, "a byte order mark is not read") if text.start_with?("\uFEFF")
65
+ text
66
+ end
67
+
68
+ # @api private
69
+ def self.transcode(source)
70
+ return source if source.encoding == Encoding::UTF_8
71
+ return source.dup.force_encoding(Encoding::UTF_8) if READ_AS_BYTES.include?(source.encoding)
72
+
73
+ source.encode(Encoding::UTF_8)
74
+ rescue EncodingError
75
+ Cursor.new("").refuse(:invalid_encoding, "the document cannot be read as UTF-8")
76
+ end
77
+
78
+ # @api private
79
+ def self.first_invalid_byte(text)
80
+ text.each_char.take_while(&:valid_encoding?).sum(&:bytesize)
81
+ end
82
+
83
+ private_class_method :utf8, :transcode, :first_invalid_byte
84
+ end
85
+ end