xrb 0.11.2 → 0.12.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.
data/lib/xrb/markup.rb CHANGED
@@ -6,20 +6,33 @@
6
6
  require "cgi"
7
7
 
8
8
  module XRB
9
- # A wrapper which indicates that `value` can be appended to the output buffer without any changes.
9
+ # Provides the default markup rendering protocol, escaping object string representations.
10
10
  module Markup
11
+ # Wrap a string as markup without escaping it.
12
+ # @parameter string [String] The already-safe markup string.
13
+ # @returns [MarkupString] The unescaped markup string.
11
14
  def self.raw(string)
12
15
  MarkupString.raw(string)
13
16
  end
14
17
 
18
+ # Append a value to an output buffer using its markup representation.
19
+ # @parameter output [Interface(:<<)] The output buffer.
20
+ # @parameter value [Interface(:append_markup)] The value to append.
21
+ # @returns [Object] The result of appending the value.
15
22
  def self.append(output, value)
16
23
  value.append_markup(output)
17
24
  end
18
25
 
26
+ # Append the escaped string representation of the receiver to an output buffer.
27
+ # @parameter output [Interface(:<<)] The output buffer.
28
+ # @returns [Object] The output buffer.
19
29
  def append_markup(output)
20
30
  output << ::CGI.escape_html(self.to_s)
21
31
  end
22
32
 
33
+ # Append the escaped string representation of the receiver using a builder.
34
+ # @parameter builder [Builder] The builder receiving the escaped markup.
35
+ # @returns [Object] The builder's output buffer.
23
36
  def build_markup(builder)
24
37
  append_markup(builder.output)
25
38
  end
@@ -29,8 +42,8 @@ module XRB
29
42
 
30
43
  # Initialized from text which is escaped to use HTML entities.
31
44
  class MarkupString < String
32
- # @param string [String] the string value itself.
33
- # @param escape [Boolean] whether or not to escape the string.
45
+ # @parameter string [String | Nil] The string value itself.
46
+ # @parameter escape [Boolean] Whether to escape the string.
34
47
  def initialize(string = nil, escape = true)
35
48
  if string
36
49
  if escape
@@ -54,12 +67,19 @@ module XRB
54
67
  true
55
68
  end
56
69
 
70
+ # Append this already-safe markup string to an output buffer.
71
+ # @parameter output [Interface(:<<)] The output buffer.
72
+ # @returns [Object] The output buffer.
57
73
  def append_markup(output)
58
74
  output << self
59
75
  end
60
76
  end
61
77
 
78
+ # Helpers for constructing script-compatible markup values.
62
79
  module Script
80
+ # Serialize a value as raw JSON markup without HTML escaping it.
81
+ # @parameter value [Object] The value to serialize.
82
+ # @returns [MarkupString] The serialized JSON markup.
63
83
  def self.json(value)
64
84
  MarkupString.new(JSON.dump(value), false)
65
85
  end
data/lib/xrb/query.rb CHANGED
@@ -9,14 +9,21 @@ require_relative "parsers"
9
9
  require "uri"
10
10
 
11
11
  module XRB
12
+ # Parses URL query strings into nested Ruby objects.
12
13
  module Query
14
+ # Parse a query string buffer.
15
+ # @parameter buffer [Buffer] The buffer containing the query string.
16
+ # @returns [Hash] The parsed query parameters.
13
17
  def self.parse(buffer)
14
18
  Hash.new.tap do |query|
15
19
  Parsers.parse_query(buffer, Delegate.new(query))
16
20
  end
17
21
  end
18
22
 
23
+ # Receives query parser events and assembles them into nested hashes and arrays.
19
24
  class Delegate
25
+ # Initialize a query delegate with the top-level result hash.
26
+ # @parameter top [Hash] The hash which receives parsed query parameters.
20
27
  def initialize(top = {})
21
28
  @top = top
22
29
 
@@ -24,6 +31,10 @@ module XRB
24
31
  @index = nil
25
32
  end
26
33
 
34
+ # Select a symbolic key parsed from a query string.
35
+ # @parameter key [String] The parsed key.
36
+ # @parameter encoded [Boolean] Whether the key is URL encoded.
37
+ # @returns [Symbol] The selected key.
27
38
  def string(key, encoded)
28
39
  if encoded
29
40
  key = ::URI.decode_www_form_component(key)
@@ -32,10 +43,16 @@ module XRB
32
43
  index(key.to_sym)
33
44
  end
34
45
 
46
+ # Select an integer key parsed from an array or hash index.
47
+ # @parameter key [String] The parsed integer key.
48
+ # @returns [Integer] The selected key.
35
49
  def integer(key)
36
50
  index(key.to_i)
37
51
  end
38
52
 
53
+ # Descend into the previously selected key and select the next nested key.
54
+ # @parameter key [Symbol | Integer] The next key to select.
55
+ # @returns [Symbol | Integer] The selected key.
39
56
  def index(key)
40
57
  if @index
41
58
  @current = @current.fetch(@index) do
@@ -46,6 +63,8 @@ module XRB
46
63
  @index = key
47
64
  end
48
65
 
66
+ # Descend into an array and select its next index.
67
+ # @returns [Integer] The selected array index.
49
68
  def append
50
69
  if @index
51
70
  @current = @current.fetch(@index) do
@@ -56,6 +75,10 @@ module XRB
56
75
  @index = @current.size
57
76
  end
58
77
 
78
+ # Assign a parsed value to the selected key.
79
+ # @parameter value [String] The parsed value.
80
+ # @parameter encoded [Boolean] Whether the value is URL encoded.
81
+ # @returns [Nil] This method always returns `nil`.
59
82
  def assign(value, encoded)
60
83
  if encoded
61
84
  value = ::URI.decode_www_form_component(value)
@@ -67,6 +90,8 @@ module XRB
67
90
  @index = nil
68
91
  end
69
92
 
93
+ # Assign `true` to a query key which has no explicit value.
94
+ # @returns [Nil] This method always returns `nil`.
70
95
  def pair
71
96
  if @index
72
97
  @current[@index] = true
data/lib/xrb/strings.rb CHANGED
@@ -4,14 +4,21 @@
4
4
  # Copyright, 2012-2024, by Samuel Williams.
5
5
 
6
6
  module XRB
7
+ # String transformation helpers used when generating markup and identifiers.
7
8
  module Strings
8
9
  HTML_ESCAPE = {"&" => "&amp;", "<" => "&lt;", ">" => "&gt;", "\"" => "&quot;"}
9
10
  HTML_ESCAPE_PATTERN = Regexp.new("[" + Regexp.quote(HTML_ESCAPE.keys.join) + "]")
10
-
11
+
12
+ # Escape HTML-sensitive characters in a string.
13
+ # @parameter string [String] The string to escape.
14
+ # @returns [String] The HTML-escaped string.
11
15
  def self.to_html(string)
12
16
  string.gsub(HTML_ESCAPE_PATTERN){|c| HTML_ESCAPE[c]}
13
17
  end
14
-
18
+
19
+ # Quote a string and escape quotes and line breaks within it.
20
+ # @parameter string [String] The string to quote.
21
+ # @returns [String] The quoted string.
15
22
  def self.to_quoted_string(string)
16
23
  string = string.gsub('"', '\\"')
17
24
  string.gsub!(/\r/, "\\r")
@@ -25,10 +32,17 @@ module XRB
25
32
  %Q{#{key}="#{value}"}
26
33
  end
27
34
 
35
+ # Format a boolean-style attribute.
36
+ # @parameter key [String | Symbol] The attribute name.
37
+ # @parameter strict [Boolean] Whether to include an explicit attribute value.
38
+ # @returns [String] The formatted attribute.
28
39
  def self.to_simple_attribute(key, strict)
29
40
  strict ? %Q{#{key}="#{key}"} : key.to_s
30
41
  end
31
42
 
43
+ # Convert a separated identifier to a human-readable title.
44
+ # @parameter string [String] The identifier to transform.
45
+ # @returns [String] The title-cased string.
32
46
  def self.to_title(string)
33
47
  string = string.gsub(/(^|[ \-_])(.)/){" " + $2.upcase}
34
48
  string.strip!
@@ -36,6 +50,9 @@ module XRB
36
50
  return string
37
51
  end
38
52
 
53
+ # Convert a constant-like name to a snake-case identifier.
54
+ # @parameter string [String] The name to transform.
55
+ # @returns [String] The snake-case identifier.
39
56
  def self.to_snake(string)
40
57
  string = string.gsub("::", "")
41
58
  string.gsub!(/([A-Z]+)/){"_" + $1.downcase}
data/lib/xrb/tag.rb CHANGED
@@ -8,6 +8,9 @@ require_relative "markup"
8
8
  module XRB
9
9
  # This represents an individual SGML tag, e.g. <a>, </a> or <a />, with attributes. Attribute values must be escaped.
10
10
  Tag = Struct.new(:name, :closed, :attributes) do
11
+ # Split a qualified tag name into its namespace and local name.
12
+ # @parameter qualified_name [String] The potentially namespaced tag name.
13
+ # @returns [Array(String | Nil)] The namespace and local name.
11
14
  def self.split(qualified_name)
12
15
  if i = qualified_name.index(":")
13
16
  return qualified_name.slice(0...i), qualified_name.slice(i+1..-1)
@@ -16,38 +19,63 @@ module XRB
16
19
  end
17
20
  end
18
21
 
22
+ # Create a self-closing tag.
23
+ # @parameter name [String] The tag name.
24
+ # @parameter attributes [Hash] The tag attributes.
25
+ # @returns [Tag] The self-closing tag.
19
26
  def self.closed(name, attributes = {})
20
27
  self.new(name, true, attributes)
21
28
  end
22
29
 
30
+ # Create a tag with separate opening and closing markup.
31
+ # @parameter name [String] The tag name.
32
+ # @parameter attributes [Hash] The tag attributes.
33
+ # @returns [Tag] The opened tag.
23
34
  def self.opened(name, attributes = {})
24
35
  self.new(name, false, attributes)
25
36
  end
26
37
 
38
+ # Append the tag to an output buffer.
39
+ # @parameter output [Interface(:<<)] The output buffer.
40
+ # @returns [Nil] This method always returns `nil`.
27
41
  def append_markup(output)
28
42
  self.write(output)
29
43
  end
30
44
 
45
+ # Append the tag using a markup builder.
46
+ # @parameter builder [Builder] The builder receiving the tag.
47
+ # @returns [Nil] This method always returns `nil`.
31
48
  def build_markup(builder)
32
49
  self.append_markup(builder.output)
33
50
  end
34
51
 
52
+ # Fetch an attribute value by key.
53
+ # @parameter key [Object] The attribute key.
54
+ # @returns [Object | Nil] The attribute value.
35
55
  def [] key
36
56
  attributes[key]
37
57
  end
38
58
 
39
59
  alias to_hash attributes
40
60
 
61
+ # Format the tag and optional content as markup.
62
+ # @parameter content [Object | Nil] The optional tag content.
63
+ # @returns [String] The formatted tag markup.
41
64
  def to_s(content = nil)
42
65
  self.class.format_tag(name, attributes, content || !closed)
43
66
  end
44
67
 
45
68
  alias to_str to_s
46
69
 
70
+ # Determine whether the tag is self-closing.
71
+ # @returns [Boolean] Whether the tag is self-closing.
47
72
  def self_closed?
48
73
  closed
49
74
  end
50
75
 
76
+ # Write the opening or self-closing tag to a buffer.
77
+ # @parameter buffer [Interface(:<<)] The output buffer.
78
+ # @returns [Object] The output buffer.
51
79
  def write_opening_tag(buffer)
52
80
  buffer << "<" << name
53
81
 
@@ -60,14 +88,26 @@ module XRB
60
88
  end
61
89
  end
62
90
 
91
+ # Write the closing tag to a buffer.
92
+ # @parameter buffer [Interface(:<<)] The output buffer.
93
+ # @returns [Object] The output buffer.
63
94
  def write_closing_tag(buffer)
64
95
  buffer << "</" << name << ">"
65
96
  end
66
97
 
98
+ # Write the complete tag and optional content to a buffer.
99
+ # @parameter buffer [Interface(:<<)] The output buffer.
100
+ # @parameter content [Object | Nil] The optional tag content.
101
+ # @returns [Nil] This method always returns `nil`.
67
102
  def write(buffer, content = nil)
68
103
  self.class.append_tag(buffer, name, attributes, content || !closed)
69
104
  end
70
105
 
106
+ # Format a tag as a new string.
107
+ # @parameter name [String] The tag name.
108
+ # @parameter attributes [Hash] The tag attributes.
109
+ # @parameter content [Object | Boolean | Nil] The tag content, `true` for empty content, or `nil` for a self-closing tag.
110
+ # @returns [String] The formatted tag markup.
71
111
  def self.format_tag(name, attributes, content)
72
112
  buffer = String.new.force_encoding(name.encoding)
73
113
 
@@ -76,6 +116,12 @@ module XRB
76
116
  return buffer
77
117
  end
78
118
 
119
+ # Append a complete tag to an output buffer.
120
+ # @parameter buffer [Interface(:<<)] The output buffer.
121
+ # @parameter name [String] The tag name.
122
+ # @parameter attributes [Hash] The tag attributes.
123
+ # @parameter content [Object | Boolean | Nil] The tag content, `true` for empty content, or `nil` for a self-closing tag.
124
+ # @returns [Nil] This method always returns `nil`.
79
125
  def self.append_tag(buffer, name, attributes, content)
80
126
  buffer << "<" << name.to_s
81
127
 
data/lib/xrb/template.rb CHANGED
@@ -16,6 +16,11 @@ module XRB
16
16
  BINDING = binding
17
17
 
18
18
  class Builder
19
+ # Capture markup generated by a block using this builder as its output.
20
+ # @parameter arguments [Array] The arguments passed to the block.
21
+ # @yields {|*arguments| ...} The block whose generated markup will be captured.
22
+ # @parameter arguments [Array] The arguments supplied to {capture}.
23
+ # @returns [String] The captured markup.
19
24
  def capture(*arguments, &block)
20
25
  Template.capture(*arguments, output: self, &block)
21
26
  end
@@ -46,7 +51,10 @@ module XRB
46
51
  binding.local_variable_get(OUT)
47
52
  end
48
53
 
54
+ # Assembles parser events into executable Ruby source code for a template.
49
55
  class Assembler
56
+ # Initialize an empty template assembler.
57
+ # @parameter encoding [Encoding] The encoding of the generated source code.
50
58
  def initialize(encoding: Encoding::UTF_8)
51
59
  @code = String.new.force_encoding(encoding)
52
60
  end
@@ -60,7 +68,7 @@ module XRB
60
68
  text = text.gsub("'", "\\\\'")
61
69
  @code << "#{OUT}.raw('#{text}');"
62
70
  end
63
-
71
+
64
72
  # Output a ruby expression (or part of).
65
73
  def instruction(text, postfix = nil)
66
74
  @code << text << (postfix || ";")
@@ -93,6 +101,8 @@ module XRB
93
101
  @compiled = nil
94
102
  end
95
103
 
104
+ # Compile and freeze the template.
105
+ # @returns [Template] The frozen template.
96
106
  def freeze
97
107
  return self if frozen?
98
108
 
data/lib/xrb/version.rb CHANGED
@@ -1,8 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2012-2024, by Samuel Williams.
4
+ # Copyright, 2012-2026, by Samuel Williams.
5
5
 
6
+ # @namespace
6
7
  module XRB
7
- VERSION = "0.11.2"
8
+ VERSION = "0.12.0"
8
9
  end
data/lib/xrb.rb CHANGED
@@ -7,5 +7,3 @@ require_relative "xrb/version"
7
7
  require_relative "xrb/native"
8
8
  require_relative "xrb/builder"
9
9
  require_relative "xrb/template"
10
-
11
- require_relative "xrb/reference"
data/readme.md CHANGED
@@ -46,6 +46,14 @@ Please see the [project documentation](https://socketry.github.io/xrb/) for more
46
46
 
47
47
  - [Markup Parser](https://socketry.github.io/xrb/guides/markup-parser/index) - This guide explains how to parse and manipulate Markup using the XRB templating system.
48
48
 
49
+ ## Releases
50
+
51
+ Please see the [project releases](https://socketry.github.io/xrb/releases/index) for all releases.
52
+
53
+ ### v0.12.0
54
+
55
+ - [URL Helpers](https://socketry.github.io/xrb/releases/index#url-helpers)
56
+
49
57
  ## See Also
50
58
 
51
59
  - [xrb-rails](https://github.com/socketry/xrb-rails) - A Rails ActionView integration for rendering XRB templates.
@@ -57,11 +65,27 @@ Please see the [project documentation](https://socketry.github.io/xrb/) for more
57
65
 
58
66
  We welcome contributions to this project.
59
67
 
60
- 1. Fork it.
68
+ 1. Fork the repository.
61
69
  2. Create your feature branch (`git checkout -b my-new-feature`).
62
- 3. Commit your changes (`git commit -am 'Add some feature'`).
70
+ 3. Commit your changes (`git commit -am 'Add some feature.'`).
63
71
  4. Push to the branch (`git push origin my-new-feature`).
64
- 5. Create new Pull Request.
72
+ 5. Create a new pull request.
73
+
74
+ ### Running Tests
75
+
76
+ To run the test suite:
77
+
78
+ ``` bash
79
+ $ bundle exec sus
80
+ ```
81
+
82
+ ### Making Releases
83
+
84
+ To make a new release:
85
+
86
+ ``` bash
87
+ $ bundle exec bake gem:release:patch # or minor or major
88
+ ```
65
89
 
66
90
  ### Developer Certificate of Origin
67
91
 
data/releases.md ADDED
@@ -0,0 +1,26 @@
1
+ # Releases
2
+
3
+ ## v0.12.0
4
+
5
+ ### URL Helpers
6
+
7
+ **Breaking:** XRB no longer provides the `XRB::Reference` and `XRB::URI` classes or their factory methods. Use `Protocol::URL::Reference` from the `protocol-url` gem instead.
8
+
9
+ Add `gem "protocol-url"` to your application dependencies and replace `require "xrb/reference"` or `require "xrb/uri"` with `require "protocol/url"`.
10
+
11
+ Replace `XRB::Reference(path, **parameters)` with:
12
+
13
+ ``` ruby
14
+ reference = Protocol::URL::Reference.parse(path)
15
+ reference.parse_query!.update(parameters)
16
+ ```
17
+
18
+ This preserves the previous behavior of parsing existing query parameters before updating them with the supplied parameters.
19
+
20
+ Replace direct `XRB::Reference.new(path, parameters, fragment: fragment)` calls with `Protocol::URL::Reference.new(path, nil, fragment, parameters)`.
21
+
22
+ Replace `XRB::URI(path, parameters)` with `Protocol::URL::Reference.parse(path, parameters)`. This preserves an existing query string and appends the supplied parameters.
23
+
24
+ Replace direct `XRB::URI.new(path, query, fragment, parameters)` calls with `Protocol::URL::Reference.new(path, query, fragment, parameters)`.
25
+
26
+ `protocol-url` treats path strings as already encoded URL paths. When starting with decoded path components, encode them explicitly with `Protocol::URL::Path.for(components)`.
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: xrb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.2
4
+ version: 0.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -80,22 +80,22 @@ files:
80
80
  - lib/xrb/native.rb
81
81
  - lib/xrb/parsers.rb
82
82
  - lib/xrb/query.rb
83
- - lib/xrb/reference.rb
84
83
  - lib/xrb/strings.rb
85
84
  - lib/xrb/tag.rb
86
85
  - lib/xrb/template.rb
87
- - lib/xrb/uri.rb
88
86
  - lib/xrb/version.rb
89
87
  - license.md
90
88
  - notes.md
91
89
  - readme.md
92
- homepage: https://github.com/ioquatix/xrb
90
+ - releases.md
91
+ homepage: https://github.com/socketry/xrb
93
92
  licenses:
94
93
  - MIT
95
94
  metadata:
95
+ bug_tracker_uri: https://github.com/socketry/xrb/issues
96
96
  documentation_uri: https://socketry.github.io/xrb/
97
97
  funding_uri: https://github.com/sponsors/ioquatix
98
- source_code_uri: https://github.com/ioquatix/xrb.git
98
+ source_code_uri: https://github.com/socketry/xrb.git
99
99
  rdoc_options: []
100
100
  require_paths:
101
101
  - lib
@@ -103,14 +103,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
103
103
  requirements:
104
104
  - - ">="
105
105
  - !ruby/object:Gem::Version
106
- version: '3.2'
106
+ version: '3.3'
107
107
  required_rubygems_version: !ruby/object:Gem::Requirement
108
108
  requirements:
109
109
  - - ">="
110
110
  - !ruby/object:Gem::Version
111
111
  version: '0'
112
112
  requirements: []
113
- rubygems_version: 3.6.7
113
+ rubygems_version: 4.0.10
114
114
  specification_version: 4
115
115
  summary: A fast native templating system that compiles directly to Ruby code.
116
116
  test_files: []
metadata.gz.sig CHANGED
Binary file
data/lib/xrb/reference.rb DELETED
@@ -1,108 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Released under the MIT License.
4
- # Copyright, 2020-2024, by Samuel Williams.
5
-
6
- require_relative "query"
7
-
8
- module XRB
9
- class Reference
10
- def initialize(path, query = {}, fragment: nil)
11
- @path = path.to_s
12
- @query = query
13
- @fragment = fragment
14
- end
15
-
16
- # The path component of the URI, e.g. /foo/bar/index.html
17
- attr :path
18
-
19
- # The query parameters.
20
- attr :query
21
-
22
- # A fragment identifier, the part after the '#'
23
- attr :fragment
24
-
25
- def append(buffer)
26
- buffer << escape_path(@path)
27
-
28
- unless @query.empty?
29
- buffer << "?" << query_string
30
- end
31
-
32
- if @fragment
33
- buffer << "#" << escape(@fragment)
34
- end
35
-
36
- return buffer
37
- end
38
-
39
- def to_str
40
- append(String.new)
41
- end
42
-
43
- alias to_s to_str
44
-
45
- private
46
-
47
- # According to https://tools.ietf.org/html/rfc3986#section-3.3, we escape non-pchar.
48
- NON_PCHAR = /([^
49
- a-zA-Z0-9
50
- \-\._~
51
- !\$&'\(\)\*\+,;=
52
- :@\/
53
- ]+)/x.freeze
54
-
55
- def escape_path(path)
56
- encoding = path.encoding
57
- path.b.gsub(NON_PCHAR) do |m|
58
- "%" + m.unpack("H2" * m.bytesize).join("%").upcase
59
- end.force_encoding(encoding)
60
- end
61
-
62
- # Escapes a generic string, using percent encoding.
63
- def escape(string)
64
- encoding = string.encoding
65
- string.b.gsub(/([^a-zA-Z0-9_.\-]+)/) do |m|
66
- "%" + m.unpack("H2" * m.bytesize).join("%").upcase
67
- end.force_encoding(encoding)
68
- end
69
-
70
- def query_string
71
- build_nested_query(@query)
72
- end
73
-
74
- def build_nested_query(value, prefix = nil)
75
- case value
76
- when Array
77
- value.map { |v|
78
- build_nested_query(v, "#{prefix}[]")
79
- }.join("&")
80
- when Hash
81
- value.map { |k, v|
82
- build_nested_query(v, prefix ? "#{prefix}[#{escape(k.to_s)}]" : escape(k.to_s))
83
- }.reject(&:empty?).join("&")
84
- when nil
85
- prefix
86
- else
87
- raise ArgumentError, "value must be a Hash" if prefix.nil?
88
- "#{prefix}=#{escape(value.to_s)}"
89
- end
90
- end
91
- end
92
-
93
- # Generate a URI from a path and user parameters. The path may contain a `#fragment` or `?query=parameters`.
94
- def self.Reference(path = "", **parameters)
95
- base, fragment = path.split("#", 2)
96
- path, query_string = base.split("?", 2)
97
-
98
- if query_string
99
- query = Query.parse(Buffer.new(query_string))
100
- else
101
- query = {}
102
- end
103
-
104
- query.update(parameters)
105
-
106
- Reference.new(path, query, fragment: fragment)
107
- end
108
- end