aa2img 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 (42) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +12 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +247 -0
  5. data/Rakefile +8 -0
  6. data/examples/basic_usage.rb +21 -0
  7. data/examples/sample_diagrams/architecture.txt +10 -0
  8. data/examples/sample_diagrams/flowchart.txt +3 -0
  9. data/exe/aa2img +6 -0
  10. data/lib/aa2img/ast/annotation.rb +9 -0
  11. data/lib/aa2img/ast/base_element.rb +13 -0
  12. data/lib/aa2img/ast/box.rb +43 -0
  13. data/lib/aa2img/ast/edge.rb +15 -0
  14. data/lib/aa2img/ast/label.rb +14 -0
  15. data/lib/aa2img/ast/scene.rb +79 -0
  16. data/lib/aa2img/ast/section.rb +16 -0
  17. data/lib/aa2img/char_width.rb +15 -0
  18. data/lib/aa2img/cli.rb +44 -0
  19. data/lib/aa2img/grid.rb +35 -0
  20. data/lib/aa2img/layout/calculator.rb +97 -0
  21. data/lib/aa2img/layout/font_metrics.rb +22 -0
  22. data/lib/aa2img/layout/grid_metrics.rb +22 -0
  23. data/lib/aa2img/parser/arrow_detector.rb +136 -0
  24. data/lib/aa2img/parser/box_builder.rb +51 -0
  25. data/lib/aa2img/parser/char_classifier.rb +68 -0
  26. data/lib/aa2img/parser/corner_detector.rb +70 -0
  27. data/lib/aa2img/parser/edge_tracer.rb +114 -0
  28. data/lib/aa2img/parser/nesting_analyzer.rb +20 -0
  29. data/lib/aa2img/parser/section_detector.rb +45 -0
  30. data/lib/aa2img/parser/text_extractor.rb +78 -0
  31. data/lib/aa2img/parser.rb +48 -0
  32. data/lib/aa2img/renderer/base.rb +11 -0
  33. data/lib/aa2img/renderer/png_renderer.rb +25 -0
  34. data/lib/aa2img/renderer/svg_renderer.rb +148 -0
  35. data/lib/aa2img/theme.rb +130 -0
  36. data/lib/aa2img/version.rb +5 -0
  37. data/lib/aa2img.rb +65 -0
  38. data/themes/blueprint.yml +18 -0
  39. data/themes/default.yml +18 -0
  40. data/themes/modern.yml +18 -0
  41. data/themes/monochrome.yml +18 -0
  42. metadata +140 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: cffb5142c6353b13b54d484e6879b928bf2618ca9b48263506dfec84d9addacd
4
+ data.tar.gz: 9309a77b67f39e24c44b42243ee9b057132527fae99e3a86a8131d2de6df1383
5
+ SHA512:
6
+ metadata.gz: 4260c2b672c7f533008bb53bc016e1420392e80adef03bcdc63cff64688e326dee7ea3bce363991e0b8a402ce9ea3ccf3e8005ff9bf5e3f959c377faaa340cc1
7
+ data.tar.gz: 0f6d68dd8ce1f000f56925606ca9c83fe67debaff6dcfb8ef05750942060c8f19ab9c03e526370bdd61dcdd8f70f73884a0a47db65bcd205ac0a2a9bb02d5326
data/CHANGELOG.md ADDED
@@ -0,0 +1,12 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## Unreleased
9
+
10
+ ## 0.1.0 (2026-02-07)
11
+
12
+ - Initial release.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Yudai Takada
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,247 @@
1
+ <h1 align="center">aa2img</h1>
2
+ <p align="center">
3
+ <strong>Convert ASCII/Unicode diagrams into clean SVG and PNG images</strong>
4
+ </p>
5
+ <p align="center">
6
+ <img src="https://img.shields.io/badge/ruby-%3E%3D%203.2-red.svg" alt="Ruby 3.2+">
7
+ <img src="https://img.shields.io/badge/output-SVG%20%2F%20PNG-2f855a.svg" alt="Output SVG/PNG">
8
+ <img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License MIT">
9
+ <a href="https://github.com/ydah/aa2img/actions/workflows/main.yml">
10
+ <img src="https://github.com/ydah/aa2img/actions/workflows/main.yml/badge.svg" alt="CI">
11
+ </a>
12
+ </p>
13
+
14
+ <p align="center">
15
+ <a href="#features">Features</a> •
16
+ <a href="#installation">Installation</a> •
17
+ <a href="#usage">Usage</a> •
18
+ <a href="#diagram-syntax">Diagram Syntax</a> •
19
+ <a href="#themes">Themes</a>
20
+ </p>
21
+
22
+ ---
23
+
24
+ `aa2img` parses text-based box diagrams and renders them as themed images.
25
+ It is useful for docs, README files, and architecture notes where you want to keep diagrams editable as plain text.
26
+
27
+ ## Features
28
+
29
+ - Parses both Unicode and ASCII box-drawing styles (`┌─┐ │ └─┘` and `+--+ | | +--+`)
30
+ - Detects section separators (`├──┤`) inside boxes
31
+ - Detects nested parent-child box structures
32
+ - Renders right-side annotations in `← note` format
33
+ - Handles full-width characters, including Japanese labels
34
+ - Supports `SVG` and `PNG` output
35
+ - Ships with 4 built-in themes: `default`, `blueprint`, `monochrome`, `modern`
36
+ - Supports vertical label alignment: `top`, `center`, `bottom`
37
+
38
+ ## Installation
39
+
40
+ ### Requirements
41
+
42
+ - Ruby `3.2+`
43
+ - ImageMagick (required only for PNG output)
44
+
45
+ ```bash
46
+ # macOS
47
+ brew install imagemagick
48
+
49
+ # Ubuntu / Debian
50
+ sudo apt install imagemagick
51
+ ```
52
+
53
+ ### Add to Gemfile
54
+
55
+ ```ruby
56
+ gem "aa2img", github: "ydah/aa2img"
57
+ ```
58
+
59
+ After the gem is published to RubyGems, you can also install it with:
60
+
61
+ ```bash
62
+ gem install aa2img
63
+ ```
64
+
65
+ ## Usage
66
+
67
+ ### Quick Start (CLI)
68
+
69
+ 1. Create a text diagram file:
70
+
71
+ ```txt
72
+ ┌────────────────────────────┐
73
+ │ API Gateway │
74
+ ├────────────────────────────┤
75
+ │ Application │
76
+ │ ┌──────────────────────┐ │
77
+ │ │ Database │ │
78
+ │ └──────────────────────┘ │
79
+ └────────────────────────────┘
80
+ ```
81
+
82
+ 2. Convert to SVG:
83
+
84
+ ```bash
85
+ aa2img convert diagram.txt output.svg
86
+ ```
87
+
88
+ 3. Convert to PNG:
89
+
90
+ ```bash
91
+ aa2img convert diagram.txt output.png --scale 3
92
+ ```
93
+
94
+ ### Commands
95
+
96
+ | Command | Description |
97
+ |---|---|
98
+ | `aa2img convert INPUT_FILE OUTPUT_FILE` | Convert text diagram to image (format inferred from extension) |
99
+ | `aa2img themes` | List available themes |
100
+ | `aa2img preview INPUT_FILE` | Show parsed AST tree for debugging |
101
+ | `aa2img version` | Show version |
102
+
103
+ ### `convert` Options
104
+
105
+ | Option | Description |
106
+ |---|---|
107
+ | `--theme NAME_OR_PATH` | Built-in theme name (`default`, `blueprint`, `monochrome`, `modern`) or YAML file path |
108
+ | `--scale N` | Scale factor for PNG output (default: `2`) |
109
+ | `--valign POS` | Vertical label alignment (`top`, `center`, `bottom`) |
110
+
111
+ Notes:
112
+ - Use `-` as `INPUT_FILE` to read from stdin.
113
+ - Output format is inferred from `OUTPUT_FILE` extension (`.svg` or `.png`).
114
+
115
+ ```bash
116
+ # Read from stdin
117
+ cat diagram.txt | aa2img convert - output.svg
118
+
119
+ # Use blueprint theme
120
+ aa2img convert diagram.txt blueprint.svg --theme blueprint
121
+
122
+ # Use custom theme file
123
+ aa2img convert diagram.txt custom.svg --theme ./themes/my-theme.yml
124
+
125
+ # Center label alignment
126
+ aa2img convert diagram.txt centered.svg --valign center
127
+ ```
128
+
129
+ ### Ruby API
130
+
131
+ ```ruby
132
+ require "aa2img"
133
+
134
+ aa = <<~AA
135
+ ┌──────┐
136
+ │ Test │
137
+ └──────┘
138
+ AA
139
+
140
+ # Get SVG string
141
+ svg = AA2img.convert(aa, format: :svg, theme: "default", valign: :center)
142
+
143
+ # Write to file (format inferred from extension)
144
+ AA2img.convert_to_file(aa, "diagram.png", theme: "monochrome", scale: 2, valign: :top)
145
+ ```
146
+
147
+ ## Diagram Syntax
148
+
149
+ ### Basic Boxes
150
+
151
+ Unicode:
152
+
153
+ ```txt
154
+ ┌──────┐
155
+ │ API │
156
+ └──────┘
157
+ ```
158
+
159
+ ASCII:
160
+
161
+ ```txt
162
+ +------+
163
+ | API |
164
+ +------+
165
+ ```
166
+
167
+ ### Sectioned Boxes
168
+
169
+ ```txt
170
+ ┌────────────────┐
171
+ │ Presentation │
172
+ ├────────────────┤
173
+ │ Application │
174
+ ├────────────────┤
175
+ │ Infrastructure │
176
+ └────────────────┘
177
+ ```
178
+
179
+ ### Annotations
180
+
181
+ ```txt
182
+ ┌──────────┐ ← user input
183
+ │ REPL │
184
+ └──────────┘
185
+ ```
186
+
187
+ ## Themes
188
+
189
+ Built-in themes:
190
+
191
+ - `default`: balanced light theme
192
+ - `blueprint`: deep-blue blueprint-like style
193
+ - `monochrome`: print-friendly grayscale style
194
+ - `modern`: clean, rounded, modern UI style
195
+
196
+ ```bash
197
+ aa2img themes
198
+ ```
199
+
200
+ CLI also accepts a YAML path for custom themes:
201
+
202
+ ```bash
203
+ aa2img convert diagram.txt output.svg --theme ./themes/my-theme.yml
204
+ ```
205
+
206
+ You can also pass a custom theme hash from Ruby:
207
+
208
+ ```ruby
209
+ custom_theme = {
210
+ "background_color" => "#ffffff",
211
+ "box_fill" => "#f8fafc",
212
+ "box_stroke" => "#334155",
213
+ "text_color" => "#0f172a",
214
+ "font_family" => "'Noto Sans JP', sans-serif"
215
+ }
216
+
217
+ svg = AA2img.convert(aa, format: :svg, theme: custom_theme)
218
+ ```
219
+
220
+ ## Notes
221
+
222
+ - Current parser behavior is optimized for box/layer diagrams.
223
+ - Annotation detection currently supports right-side `←` markers.
224
+ - `preview --format` is reserved for future expansion; current output is tree-style.
225
+
226
+ ## Development
227
+
228
+ ```bash
229
+ git clone https://github.com/ydah/aa2img.git
230
+ cd aa2img
231
+ bin/setup
232
+ bundle exec rspec
233
+ ```
234
+
235
+ Run the example script:
236
+
237
+ ```bash
238
+ bundle exec ruby examples/basic_usage.rb
239
+ ```
240
+
241
+ ## Contributing
242
+
243
+ Issues and pull requests are welcome at `https://github.com/ydah/aa2img`.
244
+
245
+ ## License
246
+
247
+ [MIT License](LICENSE.txt)
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "aa2img"
5
+
6
+ aa_text = <<~'AA'
7
+ ┌─────────────────────────────────────┐
8
+ │ REPL (UI) │
9
+ ├─────────────────────────────────────┤
10
+ │ Command Parser │
11
+ ├─────────────────────────────────────┤
12
+ │ Database │
13
+ │ ┌─────────────────────────────┐ │
14
+ │ │ StringHashMap │ │
15
+ │ └─────────────────────────────┘ │
16
+ └─────────────────────────────────────┘
17
+ AA
18
+
19
+ svg = AA2img.convert(aa_text, format: :svg, valign: :center, theme: :modern)
20
+ File.write("output.svg", svg)
21
+ puts "Generated output.svg"
@@ -0,0 +1,10 @@
1
+ ┌─────────────────────────────────────┐
2
+ │ REPL (UI) │ ← ユーザーとの対話
3
+ ├─────────────────────────────────────┤
4
+ │ Command Parser │ ← コマンド解析
5
+ ├─────────────────────────────────────┤
6
+ │ Database │ ← コア機能
7
+ │ ┌─────────────────────────────┐ │
8
+ │ │ StringHashMap │ │ ← データ保存
9
+ │ └─────────────────────────────┘ │
10
+ └─────────────────────────────────────┘
@@ -0,0 +1,3 @@
1
+ ┌──────┐ ┌──────┐ ┌──────┐
2
+ │Start │---->│Process│---->│ End │
3
+ └──────┘ └──────┘ └──────┘
data/exe/aa2img ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "aa2img"
5
+
6
+ AA2img::CLI.start(ARGV)
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AA2img
4
+ module AST
5
+ class Annotation < BaseElement
6
+ attr_accessor :text, :row, :arrow_col
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AA2img
4
+ module AST
5
+ class BaseElement
6
+ def initialize(**attrs)
7
+ attrs.each do |key, value|
8
+ send(:"#{key}=", value)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AA2img
4
+ module AST
5
+ class Box < BaseElement
6
+ attr_accessor :top, :left, :bottom, :right
7
+ attr_accessor :sections, :children, :labels, :annotations
8
+
9
+ def initialize(**attrs)
10
+ @sections = []
11
+ @children = []
12
+ @labels = []
13
+ @annotations = []
14
+ super
15
+ end
16
+
17
+ def area
18
+ (bottom - top) * (right - left)
19
+ end
20
+
21
+ def contains?(other)
22
+ top < other.top && bottom > other.bottom &&
23
+ left < other.left && right > other.right
24
+ end
25
+
26
+ def add_child(child)
27
+ @children << child
28
+ end
29
+
30
+ def all_labels
31
+ own = labels || []
32
+ from_sections = (sections || []).flat_map { |s| s.labels || [] }
33
+ own + from_sections
34
+ end
35
+
36
+ def all_annotations
37
+ own = annotations || []
38
+ from_sections = (sections || []).flat_map { |s| s.annotation ? [s.annotation] : [] }
39
+ own + from_sections
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AA2img
4
+ module AST
5
+ class Edge < BaseElement
6
+ attr_accessor :from, :to, :style, :arrow
7
+
8
+ def initialize(**attrs)
9
+ @style = :solid
10
+ @arrow = :none
11
+ super
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AA2img
4
+ module AST
5
+ class Label < BaseElement
6
+ attr_accessor :text, :row, :col, :align
7
+
8
+ def initialize(**attrs)
9
+ @align = :center
10
+ super
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AA2img
4
+ module AST
5
+ class Scene
6
+ attr_accessor :elements, :width, :height
7
+
8
+ def initialize
9
+ @elements = []
10
+ @width = 0
11
+ @height = 0
12
+ end
13
+
14
+ def boxes
15
+ elements.select { |e| e.is_a?(Box) }
16
+ end
17
+
18
+ def edges
19
+ elements.select { |e| e.is_a?(Edge) }
20
+ end
21
+
22
+ def all_boxes
23
+ result = []
24
+ boxes.each do |box|
25
+ result << box
26
+ result.concat(collect_nested_boxes(box))
27
+ end
28
+ result
29
+ end
30
+
31
+ def to_tree_string(indent = 0)
32
+ lines = []
33
+ prefix = " " * indent
34
+ lines << "#{prefix}Scene (width: #{width}, height: #{height})"
35
+ elements.each do |el|
36
+ lines << element_to_tree(el, indent + 1)
37
+ end
38
+ lines.join("\n")
39
+ end
40
+
41
+ private
42
+
43
+ def collect_nested_boxes(box)
44
+ result = []
45
+ (box.children || []).each do |child|
46
+ result << child
47
+ result.concat(collect_nested_boxes(child))
48
+ end
49
+ result
50
+ end
51
+
52
+ def element_to_tree(el, indent)
53
+ prefix = " " * indent
54
+ case el
55
+ when Box
56
+ box_to_tree(el, indent)
57
+ when Edge
58
+ "#{prefix}Edge #{el.from} -> #{el.to} (#{el.style}, #{el.arrow})"
59
+ else
60
+ "#{prefix}#{el.class.name}"
61
+ end
62
+ end
63
+
64
+ def box_to_tree(box, indent)
65
+ prefix = " " * indent
66
+ lines = ["#{prefix}└── Box (#{box.top},#{box.left})-(#{box.bottom},#{box.right})"]
67
+ (box.sections || []).each_with_index do |section, i|
68
+ section_labels = (section.labels || []).map(&:text).join(", ")
69
+ ann_text = section.annotation ? " + annotation \"#{section.annotation.text}\"" : ""
70
+ lines << "#{prefix} ├── Section[#{i}] (row #{section.top}..#{section.bottom}): \"#{section_labels}\"#{ann_text}"
71
+ end
72
+ (box.children || []).each do |child|
73
+ lines << box_to_tree(child, indent + 2)
74
+ end
75
+ lines.join("\n")
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AA2img
4
+ module AST
5
+ class Section < BaseElement
6
+ attr_accessor :top, :bottom
7
+ attr_accessor :labels, :annotation
8
+
9
+ def initialize(**attrs)
10
+ @labels = []
11
+ @annotation = nil
12
+ super
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "unicode/display_width"
4
+
5
+ module AA2img
6
+ module CharWidth
7
+ def self.width(char)
8
+ Unicode::DisplayWidth.of(char)
9
+ end
10
+
11
+ def self.wide?(char)
12
+ width(char) > 1
13
+ end
14
+ end
15
+ end
data/lib/aa2img/cli.rb ADDED
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+
5
+ module AA2img
6
+ class CLI < Thor
7
+ desc "convert INPUT_FILE OUTPUT_FILE", "Convert AA text file to image"
8
+ option :theme, type: :string, default: "default", desc: "Theme name or YAML path"
9
+ option :scale, type: :numeric, default: 2, desc: "PNG scale factor"
10
+ option :valign, type: :string, default: "top", desc: "Vertical text alignment (top / center / bottom)"
11
+ def convert(input_file, output_file)
12
+ text = if input_file == "-"
13
+ $stdin.read
14
+ else
15
+ File.read(input_file)
16
+ end
17
+ AA2img.convert_to_file(text, output_file,
18
+ theme: options[:theme],
19
+ scale: options[:scale],
20
+ valign: options[:valign].to_sym)
21
+ puts "✓ #{output_file} generated"
22
+ end
23
+
24
+ desc "themes", "List available themes"
25
+ def themes
26
+ Theme.available.each { |t| puts " - #{t}" }
27
+ end
28
+
29
+ desc "preview INPUT_FILE", "Parse AA and display AST (debug)"
30
+ option :format, type: :string, default: "tree", desc: "Output format (tree / json)"
31
+ def preview(input_file)
32
+ text = File.read(input_file)
33
+ grid = Grid.new(text)
34
+ parser = AA2img::Parser::Orchestrator.new(grid)
35
+ scene = parser.parse
36
+ puts scene.to_tree_string
37
+ end
38
+
39
+ desc "version", "Show version"
40
+ def version
41
+ puts "aa2img #{AA2img::VERSION}"
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "unicode/display_width"
4
+
5
+ module AA2img
6
+ class Grid
7
+ attr_reader :rows, :width, :height
8
+
9
+ def initialize(text)
10
+ lines = text.lines.map(&:chomp)
11
+ @height = lines.size
12
+ @rows = lines.map { |line| normalize_line(line) }
13
+ @width = @rows.map(&:size).max || 0
14
+ @rows.each { |row| row.fill(" ", row.size...@width) }
15
+ end
16
+
17
+ def at(row, col)
18
+ return nil if row < 0 || row >= @height || col < 0 || col >= @width
19
+
20
+ @rows[row][col]
21
+ end
22
+
23
+ private
24
+
25
+ def normalize_line(line)
26
+ cells = []
27
+ line.each_char do |ch|
28
+ w = Unicode::DisplayWidth.of(ch)
29
+ cells << ch
30
+ (w - 1).times { cells << :wide_placeholder }
31
+ end
32
+ cells
33
+ end
34
+ end
35
+ end