sevgi-showcase 0.93.1 → 0.95.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/README.md CHANGED
@@ -1,5 +1,53 @@
1
1
  # Sevgi Showcase
2
2
 
3
- Provides executable examples, rendered outputs, and documentation-site support.
3
+ Executable examples, rendered outputs, and documentation-site support.
4
4
 
5
- See the root [README](../README.md) and the documentation site for usage.
5
+ ## Install
6
+
7
+ ```sh
8
+ gem install sevgi-showcase
9
+ ```
10
+
11
+ ## Require
12
+
13
+ ```ruby
14
+ require "sevgi/showcase"
15
+ ```
16
+
17
+ ## Example
18
+
19
+ ```ruby
20
+ suite = Sevgi::Test::Suite.new("srv")
21
+ suite.valids.map(&:name)
22
+ ```
23
+
24
+ ## Ruby compatibility
25
+
26
+ Requires Ruby 3.4.0 or newer. CI verifies Ruby 3.4.0 and the current development Ruby from `.ruby-version`.
27
+
28
+ ## Native prerequisites
29
+
30
+ Showcase examples use the top-level Sevgi stack. SVG-only examples need no native export gems.
31
+
32
+ PDF/PNG export workflows match `sevgi-sundries` and require the optional Ruby gems `cairo`, `rsvg2`, and `hexapdf`.
33
+ On Debian/Ubuntu:
34
+
35
+ ```sh
36
+ sudo apt-get update
37
+ sudo apt-get install -y libcairo2-dev libgdk-pixbuf-2.0-dev libgirepository1.0-dev libglib2.0-dev librsvg2-dev pkg-config
38
+ gem install cairo rsvg2 hexapdf
39
+ ```
40
+
41
+ On macOS with Homebrew:
42
+
43
+ ```sh
44
+ brew install cairo gdk-pixbuf gobject-introspection librsvg pkg-config
45
+ gem install cairo rsvg2 hexapdf
46
+ ```
47
+
48
+ ## Links
49
+
50
+ - Documentation: https://sevgi.roktas.dev
51
+ - API documentation: https://www.rubydoc.info/gems/sevgi-showcase
52
+ - Source: https://github.com/roktas/sevgi/tree/main/showcase
53
+ - Changelog: https://github.com/roktas/sevgi/blob/main/CHANGELOG.md
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sevgi
4
+ module Showcase
5
+ # Applies dark-theme color mappings to showcase source files.
6
+ # @api private
7
+ module Dark
8
+ extend self
9
+
10
+ # Applies a color mapping to source text.
11
+ # @param source [String] source text
12
+ # @param mapping [Hash{String => String}] source color to dark color mapping
13
+ # @return [String] transformed source text
14
+ # @raise [Sevgi::ArgumentError] when a mapping key is not found
15
+ def apply(source, mapping)
16
+ applied = Hash.new(0)
17
+ content = replace_quoted(source, mapping, applied)
18
+ content = replace_percent_words(content, mapping, applied)
19
+ missing = mapping.keys.reject { applied[it].positive? }
20
+
21
+ ArgumentError.("Unapplied dark mapping(s): #{missing.join(", ")}") unless missing.empty?
22
+
23
+ content
24
+ end
25
+
26
+ # Applies a color mapping to a source file and preserves its mode.
27
+ # @param source [String] source file path
28
+ # @param target [String] target file path
29
+ # @param mapping [Hash{String => String}] source color to dark color mapping
30
+ # @return [String] target file path
31
+ # @raise [Sevgi::ArgumentError] when a mapping key is not found
32
+ def apply_file(source, target, mapping)
33
+ target.tap do
34
+ File.write(target, apply(File.read(source), mapping))
35
+ File.chmod(File.stat(source).mode & 0o777, target)
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def replace_percent_words(source, mapping, applied)
42
+ source.gsub(/%[wW]\[[^\]]*\]/m) do |literal|
43
+ mapping.reduce(literal) do |text, (key, value)|
44
+ text.gsub(/(^|[\s\[])(#{Regexp.escape(key)})(?=$|[\s\]])/) do
45
+ applied[key] += 1
46
+ "#{Regexp.last_match(1)}#{value}"
47
+ end
48
+ end
49
+ end
50
+ end
51
+
52
+ def replace_quoted(source, mapping, applied)
53
+ source.gsub(/(?<quote>["'])(?<value>.*?)\k<quote>/) do |literal|
54
+ key = Regexp.last_match[:value]
55
+ value = mapping[key]
56
+ next literal unless value
57
+
58
+ applied[key] += 1
59
+ "#{Regexp.last_match[:quote]}#{value}#{Regexp.last_match[:quote]}"
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "open3"
4
+ require "sevgi/function"
4
5
 
5
6
  module Sevgi
6
7
  # Minitest helpers used by the showcase component.
@@ -9,6 +10,12 @@ module Sevgi
9
10
  # Shell runner helpers for showcase tests.
10
11
  # @api private
11
12
  module Shell
13
+ # Shared process-global SIGINT state.
14
+ # @api private
15
+ Signals = ::Sevgi::Function::Shell.const_get(:Signals, false)
16
+
17
+ private_constant :Signals
18
+
12
19
  # Shell command result.
13
20
  # @api private
14
21
  Result = Data.define(:args, :out, :err, :exit_code) do
@@ -45,11 +52,16 @@ module Sevgi
45
52
 
46
53
  # Runs a command and captures stdout, stderr, and exit status.
47
54
  # @param args [Array<String>] command and arguments
55
+ # @yield optional stdin writer evaluated with stdin as receiver
56
+ # @yieldreturn [Object]
48
57
  # @return [Sevgi::Test::Shell::Result]
58
+ # @raise [StandardError] when the input block raises; the child is terminated and reaped before propagation
59
+ # @note The first SIGINT sends TERM and the second sends KILL outside trap context. The handler is restored after
60
+ # the last active run.
49
61
  def run(*args, &block)
62
+ @coathooks = 0
50
63
  out, err, status = Open3.popen3(*args) do |stdin, stdout, stderr, thread|
51
- inputs(stdin, thread, &block) if block
52
- outputs(stdout, stderr, thread)
64
+ capture(stdin, stdout, stderr, thread, &block)
53
65
  end
54
66
 
55
67
  Result[args, out, err, status.exitstatus]
@@ -57,24 +69,51 @@ module Sevgi
57
69
 
58
70
  private
59
71
 
60
- def inputs(stdin, thread, &block)
61
- stdin.instance_exec(thread, &block)
62
- stdin.close unless stdin.closed?
72
+ # rubocop:disable Lint/RescueException
73
+ def capture(stdin, stdout, stderr, thread, &block)
74
+ register_capture(thread)
75
+ registered = true
76
+ readers = outputs(stdout, stderr)
77
+
78
+ collect_capture(stdin, thread, readers, &block)
79
+ rescue Exception
80
+ cleanup_failed_capture(stdin, thread, readers)
81
+ raise
82
+ ensure
83
+ finish_capture(stdin, registered)
63
84
  end
85
+ # rubocop:enable Lint/RescueException
64
86
 
65
- def outputs(stdout, stderr, thread)
66
- # handle `^C`
67
- trap = trap("INT") { handle_sigint(thread.pid) }
87
+ def finish_capture(stdin, registered)
88
+ close_input(stdin)
89
+ Signals.unregister(self) if registered
90
+ end
68
91
 
69
- out = Thread.new { stdout.readlines.map(&:chomp) }
70
- err = Thread.new { stderr.readlines.map(&:chomp) }
92
+ def register_capture(thread)
93
+ Signals.register(self, thread.pid)
94
+ end
71
95
 
72
- [out.value, err.value, thread.value]
73
- ensure
74
- trap("INT", trap) if trap
96
+ def cleanup_failed_capture(stdin, thread, readers)
97
+ close_input(stdin)
98
+ stop_process(thread)
99
+ Array(readers).each(&:join)
100
+ end
101
+
102
+ def close_input(stdin)
103
+ stdin.close unless stdin.closed?
104
+ rescue IOError
105
+ nil
106
+ end
107
+
108
+ def collect_capture(stdin, thread, readers, &block)
109
+ inputs(stdin, thread, &block)
110
+ close_input(stdin)
111
+
112
+ [readers[0].value, readers[1].value, thread.value]
75
113
  end
76
114
 
77
115
  def handle_sigint(pid)
116
+ @coathooks += 1
78
117
  message, signal = if @coathooks > 1
79
118
  ["SIGINT received again. Force quitting...", "KILL"]
80
119
  else
@@ -83,10 +122,36 @@ module Sevgi
83
122
 
84
123
  warn("\n#{message}")
85
124
  ::Process.kill(signal, pid)
86
- @coathooks += 1
87
125
  rescue Errno::ESRCH
88
126
  warn("No process to kill.")
89
127
  end
128
+
129
+ def inputs(stdin, thread, &block)
130
+ stdin.instance_exec(thread, &block) if block
131
+ end
132
+
133
+ def kill_process(signal, pid)
134
+ ::Process.kill(signal, pid)
135
+ rescue Errno::ESRCH
136
+ nil
137
+ end
138
+
139
+ def outputs(stdout, stderr)
140
+ [
141
+ Thread.new { stdout.readlines.map(&:chomp) },
142
+ Thread.new { stderr.readlines.map(&:chomp) }
143
+ ]
144
+ end
145
+
146
+ def stop_process(thread)
147
+ return unless thread&.alive?
148
+
149
+ kill_process("TERM", thread.pid)
150
+ return if thread.join(1)
151
+
152
+ kill_process("KILL", thread.pid)
153
+ thread.join
154
+ end
90
155
  end
91
156
 
92
157
  extend self
@@ -4,6 +4,6 @@ module Sevgi
4
4
  # Showcase component namespace.
5
5
  module Showcase
6
6
  # Current showcase component version.
7
- VERSION = "0.93.1"
7
+ VERSION = "0.95.0"
8
8
  end
9
9
  end
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "sevgi"
4
+
5
+ require_relative "showcase/dark"
3
6
  require_relative "showcase/minitest"
4
7
 
5
8
  require_relative "showcase/version"
@@ -91,6 +91,6 @@ end
91
91
  board.add_piece 7, col, dark: false
92
92
  end
93
93
 
94
- SVG :minimal, viewBox: board.view_box do
94
+ SVG :minimal, width: board.width, height: board.height, viewBox: board.view_box do
95
95
  board.render_on(self)
96
96
  end.Save
@@ -1,4 +1,4 @@
1
- <svg viewBox="0 0 900 900">
1
+ <svg width="900" height="900" viewBox="0 0 900 900">
2
2
  <style type="text/css">
3
3
  <![CDATA[
4
4
  .board-frame {
data/srv/gear-wheel.sevgi CHANGED
@@ -6,8 +6,13 @@ GEAR_STYLE = {
6
6
  stroke: "none"
7
7
  }.freeze
8
8
 
9
- SVG :minimal, viewBox: "0 0 100 100", style: { background: "lightgreen" } do
10
- mask id: "hole" do
9
+ SVG :minimal, width: 100, height: 100, viewBox: "0 0 100 100" do
10
+ rect x: 0, y: 0, width: 100, height: 100, fill: "lightgreen"
11
+
12
+ mask id: "hole",
13
+ maskUnits: "userSpaceOnUse",
14
+ maskContentUnits: "userSpaceOnUse",
15
+ x: 0, y: 0, width: 100, height: 100 do
11
16
  rect x: 0, y: 0, width: 100, height: 100, fill: "white"
12
17
  circle cx: 50, cy: 50, r: 15, fill: "black"
13
18
  end
data/srv/gear-wheel.svg CHANGED
@@ -1,5 +1,6 @@
1
- <svg viewBox="0 0 100 100" style="background:lightgreen">
2
- <mask id="hole">
1
+ <svg width="100" height="100" viewBox="0 0 100 100">
2
+ <rect x="0" y="0" width="100" height="100" fill="lightgreen"/>
3
+ <mask id="hole" maskUnits="userSpaceOnUse" maskContentUnits="userSpaceOnUse" x="0" y="0" width="100" height="100">
3
4
  <rect x="0" y="0" width="100" height="100" fill="white"/>
4
5
  <circle cx="50" cy="50" r="15" fill="black"/>
5
6
  </mask>
data/srv/grid-cells.sevgi CHANGED
@@ -6,14 +6,18 @@ COLORS = %w[#FF6961 #77DD77 #FFD700].freeze
6
6
  COLS = 8
7
7
  ROWS = 3
8
8
 
9
- SVG :minimal, viewBox: "0 0 #{COLS * CELL_SIZE} #{ROWS * CELL_SIZE}" do
10
- css ".cell" => { stroke: "white", rx: (CELL_SIZE * 0.1).round }
9
+ SVG :minimal,
10
+ width: COLS * CELL_SIZE,
11
+ height: ROWS * CELL_SIZE,
12
+ viewBox: "0 0 #{COLS * CELL_SIZE} #{ROWS * CELL_SIZE}" do
13
+ css ".cell" => { stroke: "white" }
11
14
 
12
15
  COLS.times do |col|
13
16
  ROWS.times do |row|
14
17
  rect class: "cell",
15
18
  x: col * CELL_SIZE, y: row * CELL_SIZE,
16
19
  width: CELL_SIZE, height: CELL_SIZE,
20
+ rx: (CELL_SIZE * 0.1).round,
17
21
  fill: COLORS[(col + row) % COLORS.length]
18
22
  end
19
23
  end
data/srv/grid-cells.svg CHANGED
@@ -1,34 +1,33 @@
1
- <svg viewBox="0 0 160 60">
1
+ <svg width="160" height="60" viewBox="0 0 160 60">
2
2
  <style type="text/css">
3
3
  <![CDATA[
4
4
  .cell {
5
5
  stroke: white;
6
- rx: 2;
7
6
  }
8
7
  ]]>
9
8
  </style>
10
- <rect class="cell" x="0" y="0" width="20" height="20" fill="#FF6961"/>
11
- <rect class="cell" x="0" y="20" width="20" height="20" fill="#77DD77"/>
12
- <rect class="cell" x="0" y="40" width="20" height="20" fill="#FFD700"/>
13
- <rect class="cell" x="20" y="0" width="20" height="20" fill="#77DD77"/>
14
- <rect class="cell" x="20" y="20" width="20" height="20" fill="#FFD700"/>
15
- <rect class="cell" x="20" y="40" width="20" height="20" fill="#FF6961"/>
16
- <rect class="cell" x="40" y="0" width="20" height="20" fill="#FFD700"/>
17
- <rect class="cell" x="40" y="20" width="20" height="20" fill="#FF6961"/>
18
- <rect class="cell" x="40" y="40" width="20" height="20" fill="#77DD77"/>
19
- <rect class="cell" x="60" y="0" width="20" height="20" fill="#FF6961"/>
20
- <rect class="cell" x="60" y="20" width="20" height="20" fill="#77DD77"/>
21
- <rect class="cell" x="60" y="40" width="20" height="20" fill="#FFD700"/>
22
- <rect class="cell" x="80" y="0" width="20" height="20" fill="#77DD77"/>
23
- <rect class="cell" x="80" y="20" width="20" height="20" fill="#FFD700"/>
24
- <rect class="cell" x="80" y="40" width="20" height="20" fill="#FF6961"/>
25
- <rect class="cell" x="100" y="0" width="20" height="20" fill="#FFD700"/>
26
- <rect class="cell" x="100" y="20" width="20" height="20" fill="#FF6961"/>
27
- <rect class="cell" x="100" y="40" width="20" height="20" fill="#77DD77"/>
28
- <rect class="cell" x="120" y="0" width="20" height="20" fill="#FF6961"/>
29
- <rect class="cell" x="120" y="20" width="20" height="20" fill="#77DD77"/>
30
- <rect class="cell" x="120" y="40" width="20" height="20" fill="#FFD700"/>
31
- <rect class="cell" x="140" y="0" width="20" height="20" fill="#77DD77"/>
32
- <rect class="cell" x="140" y="20" width="20" height="20" fill="#FFD700"/>
33
- <rect class="cell" x="140" y="40" width="20" height="20" fill="#FF6961"/>
9
+ <rect class="cell" x="0" y="0" width="20" height="20" rx="2" fill="#FF6961"/>
10
+ <rect class="cell" x="0" y="20" width="20" height="20" rx="2" fill="#77DD77"/>
11
+ <rect class="cell" x="0" y="40" width="20" height="20" rx="2" fill="#FFD700"/>
12
+ <rect class="cell" x="20" y="0" width="20" height="20" rx="2" fill="#77DD77"/>
13
+ <rect class="cell" x="20" y="20" width="20" height="20" rx="2" fill="#FFD700"/>
14
+ <rect class="cell" x="20" y="40" width="20" height="20" rx="2" fill="#FF6961"/>
15
+ <rect class="cell" x="40" y="0" width="20" height="20" rx="2" fill="#FFD700"/>
16
+ <rect class="cell" x="40" y="20" width="20" height="20" rx="2" fill="#FF6961"/>
17
+ <rect class="cell" x="40" y="40" width="20" height="20" rx="2" fill="#77DD77"/>
18
+ <rect class="cell" x="60" y="0" width="20" height="20" rx="2" fill="#FF6961"/>
19
+ <rect class="cell" x="60" y="20" width="20" height="20" rx="2" fill="#77DD77"/>
20
+ <rect class="cell" x="60" y="40" width="20" height="20" rx="2" fill="#FFD700"/>
21
+ <rect class="cell" x="80" y="0" width="20" height="20" rx="2" fill="#77DD77"/>
22
+ <rect class="cell" x="80" y="20" width="20" height="20" rx="2" fill="#FFD700"/>
23
+ <rect class="cell" x="80" y="40" width="20" height="20" rx="2" fill="#FF6961"/>
24
+ <rect class="cell" x="100" y="0" width="20" height="20" rx="2" fill="#FFD700"/>
25
+ <rect class="cell" x="100" y="20" width="20" height="20" rx="2" fill="#FF6961"/>
26
+ <rect class="cell" x="100" y="40" width="20" height="20" rx="2" fill="#77DD77"/>
27
+ <rect class="cell" x="120" y="0" width="20" height="20" rx="2" fill="#FF6961"/>
28
+ <rect class="cell" x="120" y="20" width="20" height="20" rx="2" fill="#77DD77"/>
29
+ <rect class="cell" x="120" y="40" width="20" height="20" rx="2" fill="#FFD700"/>
30
+ <rect class="cell" x="140" y="0" width="20" height="20" rx="2" fill="#77DD77"/>
31
+ <rect class="cell" x="140" y="20" width="20" height="20" rx="2" fill="#FFD700"/>
32
+ <rect class="cell" x="140" y="40" width="20" height="20" rx="2" fill="#FF6961"/>
34
33
  </svg>
data/srv/heart-mask.sevgi CHANGED
@@ -3,8 +3,11 @@
3
3
 
4
4
  HEART_PATH = "M10,35 A20,20,0,0,1,50,35 A20,20,0,0,1,90,35 Q90,65,50,95 Q10,65,10,35 Z"
5
5
 
6
- SVG :minimal, viewBox: "-10 -10 120 120" do
7
- mask id: "heart" do
6
+ SVG :minimal, width: 120, height: 120, viewBox: "-10 -10 120 120" do
7
+ mask id: "heart",
8
+ maskUnits: "userSpaceOnUse",
9
+ maskContentUnits: "userSpaceOnUse",
10
+ x: 0, y: 0, width: 100, height: 100 do
8
11
  rect x: 0, y: 0, width: 100, height: 100, fill: "white"
9
12
  path d: HEART_PATH, fill: "black"
10
13
  end
data/srv/heart-mask.svg CHANGED
@@ -1,5 +1,5 @@
1
- <svg viewBox="-10 -10 120 120">
2
- <mask id="heart">
1
+ <svg width="120" height="120" viewBox="-10 -10 120 120">
2
+ <mask id="heart" maskUnits="userSpaceOnUse" maskContentUnits="userSpaceOnUse" x="0" y="0" width="100" height="100">
3
3
  <rect x="0" y="0" width="100" height="100" fill="white"/>
4
4
  <path d="M10,35 A20,20,0,0,1,50,35 A20,20,0,0,1,90,35 Q90,65,50,95 Q10,65,10,35 Z" fill="black"/>
5
5
  </mask>
data/srv/meter-face.sevgi CHANGED
@@ -6,7 +6,7 @@ LED_GAP = 2
6
6
  LED_WIDTH = 10
7
7
  TOTAL_WIDTH = (LED_COUNT * (LED_WIDTH + LED_GAP)) + 8
8
8
 
9
- SVG :minimal, viewBox: "0 0 #{TOTAL_WIDTH} 30" do
9
+ SVG :minimal, width: TOTAL_WIDTH, height: 30, viewBox: "0 0 #{TOTAL_WIDTH} 30" do
10
10
  rect x: 2, y: 2, width: TOTAL_WIDTH - 4, height: 26,
11
11
  rx: 4, fill: "none", stroke: "#666"
12
12
 
data/srv/meter-face.svg CHANGED
@@ -1,4 +1,4 @@
1
- <svg viewBox="0 0 128 30">
1
+ <svg width="128" height="30" viewBox="0 0 128 30">
2
2
  <rect x="2" y="2" width="124" height="26" rx="4" fill="none" stroke="#666"/>
3
3
  <rect x="5" y="5" width="10" height="20" rx="3" fill="green" fill-opacity="1"/>
4
4
  <rect x="17" y="5" width="10" height="20" rx="3" fill="green" fill-opacity="1"/>
@@ -1,52 +1,55 @@
1
1
  #!/usr/bin/env -S ruby -S sevgi
2
2
  # frozen_string_literal: true
3
3
 
4
- WIDTH = 150 # 15 cm width (ruler's length)
5
- HEIGHT = 20 # 2 cm height
4
+ # 15 cm width (ruler's length)
5
+ WIDTH = 150
6
+ # 2 cm height
7
+ HEIGHT = 20
6
8
 
7
9
  STYLE = {
8
- ".labels": { fill: "black", font: "3pt monospace" },
9
- ".majors": { stroke: "black", "stroke-width": 0.2 },
10
- ".halves": { stroke: "black", "stroke-width": 0.1 },
11
- ".minors": { stroke: "black", "stroke-width": 0.1 },
12
- ".frame": { stroke: "black", "stroke-width": 0.1, fill: "none" }
10
+ ".labels": {fill: "black", font: "3pt monospace"},
11
+ ".majors": {stroke: "black", "stroke-width": 0.2},
12
+ ".halves": {stroke: "black", "stroke-width": 0.1},
13
+ ".minors": {stroke: "black", "stroke-width": 0.1},
14
+ ".frame": {stroke: "black", "stroke-width": 0.1, fill: "none"}
13
15
  }.freeze
14
16
 
15
- SVG width: "210mm", height: "50mm", viewBox: "0 0 210 50" do
16
- css STYLE
17
+ SVG(width: "210mm", height: "50mm", viewBox: "0 0 210 50") do
18
+ css(STYLE)
17
19
 
18
- rect id: "frame", width: WIDTH, height: HEIGHT, class: "frame"
20
+ rect(id: "frame", width: WIDTH, height: HEIGHT, class: "frame")
19
21
 
20
- layer id: "minors" do
22
+ layer(id: "minors") do
21
23
  length = 2
22
24
 
23
25
  (0..WIDTH).step(1) do |x|
24
- VLineBy x:, y: 0, length: length, class: "minors"
25
- VLineBy x:, y: HEIGHT, length: -length, class: "minors"
26
+ VLineBy(x:, y: 0, length: length, class: "minors")
27
+ VLineBy(x:, y: HEIGHT, length: -length, class: "minors")
26
28
  end
27
29
  end
28
30
 
29
- layer id: "halves" do
31
+ layer(id: "halves") do
30
32
  length = 4
31
33
 
32
34
  (0..WIDTH).step(5) do |x|
33
- VLineBy x:, y: 0, length: length, class: "minors"
34
- VLineBy x:, y: HEIGHT, length: -length, class: "minors"
35
+ VLineBy(x:, y: 0, length: length, class: "halves")
36
+ VLineBy(x:, y: HEIGHT, length: -length, class: "halves")
35
37
  end
36
38
  end
37
39
 
38
- layer id: "majors" do
40
+ layer(id: "majors") do
39
41
  length = 6
40
42
 
41
43
  (10..(WIDTH - 10)).step(10) do |x|
42
- VLineBy x:, y: 0, length: length, class: "minors"
43
- VLineBy x:, y: HEIGHT, length: -length, class: "minors"
44
+ VLineBy(x:, y: 0, length: length, class: "majors")
45
+ VLineBy(x:, y: HEIGHT, length: -length, class: "majors")
44
46
  end
45
47
 
46
- layer id: "labels" do
48
+ layer(id: "labels") do
47
49
  (10..(WIDTH - 10)).step(10).each_with_index do |x, i|
48
- text (i + 1).to_s, "text-anchor": "middle", x:, y: length + 5.5, class: "labels"
50
+ text((i + 1).to_s, "text-anchor": "middle", x:, y: length + 5.5, class: "labels")
49
51
  end
50
52
  end
51
53
  end
52
- end.Save
54
+ end
55
+ .Save()