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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +35 -0
- data/{LICENSE.txt → LICENSE} +1 -1
- data/README.md +601 -16
- data/lib/statifier/accepts.rb +167 -0
- data/lib/statifier/basichttp/decode.rb +142 -0
- data/lib/statifier/basichttp/encoding.rb +224 -0
- data/lib/statifier/basichttp/net_http_transport.rb +157 -0
- data/lib/statifier/basichttp/outbound.rb +87 -0
- data/lib/statifier/basichttp/processor.rb +170 -0
- data/lib/statifier/basichttp/request.rb +94 -0
- data/lib/statifier/basichttp/response.rb +34 -0
- data/lib/statifier/basichttp.rb +47 -0
- data/lib/statifier/compiler/content.rb +131 -0
- data/lib/statifier/compiler/error.rb +30 -0
- data/lib/statifier/compiler/expressions.rb +64 -0
- data/lib/statifier/compiler/numbering.rb +145 -0
- data/lib/statifier/compiler/parts.rb +144 -0
- data/lib/statifier/compiler/walk.rb +61 -0
- data/lib/statifier/compiler.rb +82 -0
- data/lib/statifier/datamodel/data_binding.rb +114 -0
- data/lib/statifier/datamodel/delay.rb +54 -0
- data/lib/statifier/datamodel/location.rb +145 -0
- data/lib/statifier/datamodel/program.rb +82 -0
- data/lib/statifier/datamodel/reason_value.rb +42 -0
- data/lib/statifier/datamodel/reasons.rb +88 -0
- data/lib/statifier/datamodel/stores.rb +169 -0
- data/lib/statifier/datamodel/system_variables.rb +69 -0
- data/lib/statifier/datamodel.rb +120 -0
- data/lib/statifier/document/data.rb +36 -0
- data/lib/statifier/document/executable.rb +69 -0
- data/lib/statifier/document/invoke.rb +17 -0
- data/lib/statifier/document/states.rb +40 -0
- data/lib/statifier/document.rb +51 -0
- data/lib/statifier/driver/calls.rb +183 -0
- data/lib/statifier/driver/checks.rb +110 -0
- data/lib/statifier/driver/delivery.rb +45 -0
- data/lib/statifier/driver/invocations.rb +129 -0
- data/lib/statifier/driver/mailbox.rb +120 -0
- data/lib/statifier/driver/routing.rb +193 -0
- data/lib/statifier/driver/session_id.rb +40 -0
- data/lib/statifier/driver/work.rb +196 -0
- data/lib/statifier/driver.rb +205 -0
- data/lib/statifier/effect.rb +208 -0
- data/lib/statifier/event.rb +67 -0
- data/lib/statifier/frozen.rb +22 -0
- data/lib/statifier/interpreter/blocks.rb +142 -0
- data/lib/statifier/interpreter/content.rb +228 -0
- data/lib/statifier/interpreter/entering.rb +141 -0
- data/lib/statifier/interpreter/exit_entry.rb +174 -0
- data/lib/statifier/interpreter/foreach.rb +83 -0
- data/lib/statifier/interpreter/invoke/arguments.rb +52 -0
- data/lib/statifier/interpreter/invoke/passes.rb +143 -0
- data/lib/statifier/interpreter/invoke.rb +145 -0
- data/lib/statifier/interpreter/loop.rb +170 -0
- data/lib/statifier/interpreter/machine_state.rb +152 -0
- data/lib/statifier/interpreter/macrostep.rb +185 -0
- data/lib/statifier/interpreter/name_match.rb +43 -0
- data/lib/statifier/interpreter/selection.rb +221 -0
- data/lib/statifier/interpreter/send.rb +223 -0
- data/lib/statifier/interpreter/step.rb +138 -0
- data/lib/statifier/interpreter/stop.rb +66 -0
- data/lib/statifier/interpreter/targets.rb +107 -0
- data/lib/statifier/interpreter.rb +57 -0
- data/lib/statifier/lowering/branches.rb +84 -0
- data/lib/statifier/lowering/data.rb +102 -0
- data/lib/statifier/lowering/error.rb +102 -0
- data/lib/statifier/lowering/executable.rb +106 -0
- data/lib/statifier/lowering/placement.rb +65 -0
- data/lib/statifier/lowering/reading.rb +53 -0
- data/lib/statifier/lowering/states.rb +111 -0
- data/lib/statifier/lowering.rb +129 -0
- data/lib/statifier/machine/content.rb +81 -0
- data/lib/statifier/machine/data.rb +42 -0
- data/lib/statifier/machine/states.rb +56 -0
- data/lib/statifier/machine/values.rb +37 -0
- data/lib/statifier/machine.rb +103 -0
- data/lib/statifier/position/rebuild.rb +84 -0
- data/lib/statifier/position/shape.rb +73 -0
- data/lib/statifier/position/text.rb +43 -0
- data/lib/statifier/position.rb +206 -0
- data/lib/statifier/validator/attributes.rb +35 -0
- data/lib/statifier/validator/context.rb +74 -0
- data/lib/statifier/validator/data.rb +136 -0
- data/lib/statifier/validator/error.rb +90 -0
- data/lib/statifier/validator/executable.rb +139 -0
- data/lib/statifier/validator/history.rb +81 -0
- data/lib/statifier/validator/messages.rb +142 -0
- data/lib/statifier/validator/states.rb +161 -0
- data/lib/statifier/validator.rb +94 -0
- data/lib/statifier/version.rb +4 -1
- data/lib/statifier/xml/character_data.rb +142 -0
- data/lib/statifier/xml/cursor.rb +131 -0
- data/lib/statifier/xml/markup.rb +153 -0
- data/lib/statifier/xml/nodes.rb +50 -0
- data/lib/statifier/xml/parser.rb +121 -0
- data/lib/statifier/xml/scope.rb +46 -0
- data/lib/statifier/xml/tags.rb +112 -0
- data/lib/statifier/xml.rb +85 -0
- data/lib/statifier.rb +386 -2
- metadata +129 -20
- data/.gitignore +0 -8
- data/CODE_OF_CONDUCT.md +0 -74
- data/Gemfile +0 -11
- data/Gemfile.lock +0 -33
- data/Rakefile +0 -10
- data/bin/console +0 -8
- data/bin/setup +0 -6
- 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
|