sevgi-showcase 0.73.2 → 0.94.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c2dc7c21825c07ed66a185187025a0fd58969df58105d0d55ba4da7ec3d1f78f
4
- data.tar.gz: 53a825990707906e4386cf104b8d83d13d33bd41752496044fd65b1493c1b9cf
3
+ metadata.gz: 36b54e0f7d4b51e0815f49a28194c841e5e56ace94742c2598f95dcf4677e72c
4
+ data.tar.gz: b31295af208cee2aa2aad3061556f9848190c78610733510e73dd557b541995e
5
5
  SHA512:
6
- metadata.gz: bc0bd992d47b00eac1a4bd65f5835759c04e0b657bcb459109362ee0dab2bad0ec35090ce03223c64a45740f860352c26d56f286945793fb80377886246201f7
7
- data.tar.gz: bd1f18ae77d395b4905b233bea49e80e5c2c6c68545847cc0a4b0a4a6668f2a1bcd3edcf9248b45e6613e80a5029399197428b9a801bbc95e7be997195fdf918
6
+ metadata.gz: 646bd6e535ee9c3d61efbc49ee0e443dec23af2e94cd5b16210bae05f0a70e1a68cbf75272efbcb1d9b2af7ab84d55a2fca470b8bb15f4a806b99822bc30432e
7
+ data.tar.gz: 7ff66e247c4d5f0a98dd9fd69b27ee7e14088f67390482fd596630d1a21067fc9c9a16983af747201f44479bba547bb36f8001af26486adb0d2e1d2be5ed1f08
data/CHANGELOG.md ADDED
@@ -0,0 +1,4 @@
1
+ # Changelog
2
+
3
+ Sevgi Showcase follows the root Sevgi release notes:
4
+ https://github.com/roktas/sevgi/blob/main/CHANGELOG.md
data/LICENSE ADDED
@@ -0,0 +1,5 @@
1
+ Sevgi Showcase is distributed under the GNU General Public License v3.0 or later.
2
+
3
+ SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ Full project license: https://github.com/roktas/sevgi/blob/main/LICENSE
data/README.md CHANGED
@@ -0,0 +1,53 @@
1
+ # Sevgi Showcase
2
+
3
+ Executable examples, rendered outputs, and documentation-site support.
4
+
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 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,38 +1,70 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sevgi
4
+ # Minitest helpers used by the showcase component.
5
+ # @api private
4
6
  module Test
7
+ # Executable showcase script descriptor.
8
+ # @api private
5
9
  class Script
10
+ # @return [String] absolute script path
6
11
  attr_reader :file
7
12
 
13
+ # Creates a script descriptor.
14
+ # @param file [String] executable .sevgi file
15
+ # @return [void]
16
+ # @raise [Sevgi::ArgumentError] when the file is missing or not executable
8
17
  def initialize(file) = @file = ::File.expand_path(sanitize(file))
9
18
 
19
+ # Returns the script directory.
20
+ # @return [String]
10
21
  def dir = @dir ||= ::File.dirname(file)
11
22
 
23
+ # Returns the script basename.
24
+ # @return [String]
12
25
  def file! = ::File.basename(file)
13
26
 
27
+ # Returns the script name without extension.
28
+ # @return [String]
14
29
  def name = @name ||= ::File.basename(file, ".*")
15
30
 
31
+ # @overload run(*args)
32
+ # Runs the script directly.
33
+ # @param args [Array<String>] extra command arguments
34
+ # @return [Sevgi::Test::Shell::Result]
16
35
  def run(*) = Shell.run(file, *)
17
36
 
37
+ # Returns the script suite name.
38
+ # @return [String]
18
39
  def suite = @suite ||= ::File.basename(dir)
19
40
 
41
+ # Reports whether the expected SVG output exists.
42
+ # @return [Boolean]
20
43
  def svg? = ::File.exist?(svg)
21
44
 
45
+ # Returns the expected SVG output path.
46
+ # @return [String]
22
47
  def svg = @svg ||= ::File.expand_path("#{dir}/#{name}.svg")
23
48
 
49
+ # Returns the expected SVG output basename.
50
+ # @return [String]
24
51
  def svg! = ::File.basename(svg)
25
52
 
53
+ # Returns the optional YAML metadata path.
54
+ # @return [String]
26
55
  def yml = @yml ||= ::File.expand_path("#{dir}/#{name}.yml")
27
56
 
57
+ # Returns the optional YAML metadata basename.
58
+ # @return [String]
28
59
  def yml! = ::File.basename(yml)
29
60
 
30
- # A gross hack to avoid touching filesystem during testing. Intercept the Save methods to display output in stdout
31
- # instead of saving an actual file.
32
-
61
+ # @overload run_passive(*args)
62
+ # Runs the script without writing Save output to files.
63
+ # @param args [Array<String>] extra command arguments
64
+ # @return [Sevgi::Test::Shell::Result]
33
65
  def run_passive(*)
34
66
  warn("...#{name}")
35
- Shell.run("sevgi", "-r", "sevgi/showcase/kludge", file, *)
67
+ Shell.run("sevgi", "-r", "sevgi/showcase/passive", file, *)
36
68
  end
37
69
 
38
70
  private
@@ -3,30 +3,55 @@
3
3
  require "open3"
4
4
 
5
5
  module Sevgi
6
+ # Minitest helpers used by the showcase component.
7
+ # @api private
6
8
  module Test
9
+ # Shell runner helpers for showcase tests.
10
+ # @api private
7
11
  module Shell
12
+ # Shell command result.
13
+ # @api private
8
14
  Result = Data.define(:args, :out, :err, :exit_code) do
15
+ # Returns the command string.
16
+ # @return [String]
9
17
  def cmd = args.join(" ")
10
18
 
19
+ # Reports whether the command failed.
20
+ # @return [Boolean]
11
21
  def notok? = !ok?
12
22
 
23
+ # Reports whether the command exited successfully.
24
+ # @return [Boolean]
13
25
  def ok? = exit_code&.zero?
14
26
 
27
+ # Returns the first stdout line.
28
+ # @return [String, nil]
15
29
  def outline = out.first
16
30
 
31
+ # Returns stdout as a newline-joined string.
32
+ # @return [String]
17
33
  def to_s = out.join("\n")
18
34
  end
19
35
 
20
36
  # Adapted to popen3 from github.com/mina-deploy/mina
37
+ # Shell command runner.
38
+ # @api private
21
39
  class Runner
40
+ # Creates a runner.
41
+ # @return [void]
22
42
  def initialize
23
43
  @coathooks = 0
24
44
  end
25
45
 
46
+ # Runs a command and captures stdout, stderr, and exit status.
47
+ # @param args [Array<String>] command and arguments
48
+ # @yield optional stdin writer evaluated with stdin as receiver
49
+ # @yieldreturn [Object]
50
+ # @return [Sevgi::Test::Shell::Result]
51
+ # @raise [StandardError] when the input block raises; the child is terminated and reaped before propagation
26
52
  def run(*args, &block)
27
53
  out, err, status = Open3.popen3(*args) do |stdin, stdout, stderr, thread|
28
- inputs(stdin, thread, &block) if block
29
- outputs(stdout, stderr, thread)
54
+ capture(stdin, stdout, stderr, thread, &block)
30
55
  end
31
56
 
32
57
  Result[args, out, err, status.exitstatus]
@@ -34,19 +59,38 @@ module Sevgi
34
59
 
35
60
  private
36
61
 
37
- def inputs(stdin, thread, &block)
38
- stdin.instance_exec(thread, &block)
39
- stdin.close unless stdin.closed?
62
+ # rubocop:disable Lint/RescueException
63
+ def capture(stdin, stdout, stderr, thread, &block)
64
+ trap = trap("INT") { handle_sigint(thread.pid) }
65
+ readers = outputs(stdout, stderr)
66
+
67
+ collect_capture(stdin, thread, readers, &block)
68
+ rescue Exception
69
+ cleanup_failed_capture(stdin, thread, readers)
70
+ raise
71
+ ensure
72
+ close_input(stdin)
73
+ trap("INT", trap) if trap
40
74
  end
75
+ # rubocop:enable Lint/RescueException
41
76
 
42
- def outputs(stdout, stderr, thread)
43
- # handle `^C`
44
- trap("INT") { handle_sigint(thread.pid) }
77
+ def cleanup_failed_capture(stdin, thread, readers)
78
+ close_input(stdin)
79
+ stop_process(thread)
80
+ Array(readers).each(&:join)
81
+ end
82
+
83
+ def close_input(stdin)
84
+ stdin.close unless stdin.closed?
85
+ rescue IOError
86
+ nil
87
+ end
45
88
 
46
- out = stdout.readlines.map(&:chomp)
47
- err = stderr.readlines.map(&:chomp)
89
+ def collect_capture(stdin, thread, readers, &block)
90
+ inputs(stdin, thread, &block)
91
+ close_input(stdin)
48
92
 
49
- [out, err, thread.value]
93
+ [readers[0].value, readers[1].value, thread.value]
50
94
  end
51
95
 
52
96
  def handle_sigint(pid)
@@ -62,10 +106,41 @@ module Sevgi
62
106
  rescue Errno::ESRCH
63
107
  warn("No process to kill.")
64
108
  end
109
+
110
+ def inputs(stdin, thread, &block)
111
+ stdin.instance_exec(thread, &block) if block
112
+ end
113
+
114
+ def kill_process(signal, pid)
115
+ ::Process.kill(signal, pid)
116
+ rescue Errno::ESRCH
117
+ nil
118
+ end
119
+
120
+ def outputs(stdout, stderr)
121
+ [
122
+ Thread.new { stdout.readlines.map(&:chomp) },
123
+ Thread.new { stderr.readlines.map(&:chomp) }
124
+ ]
125
+ end
126
+
127
+ def stop_process(thread)
128
+ return unless thread&.alive?
129
+
130
+ kill_process("TERM", thread.pid)
131
+ return if thread.join(1)
132
+
133
+ kill_process("KILL", thread.pid)
134
+ thread.join
135
+ end
65
136
  end
66
137
 
67
138
  extend self
68
139
 
140
+ # @overload run(*args)
141
+ # Runs a command through a fresh runner.
142
+ # @param args [Array<String>] command and arguments
143
+ # @return [Sevgi::Test::Shell::Result]
69
144
  def run(...) = Runner.new.run(...)
70
145
  end
71
146
  end
@@ -1,26 +1,43 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sevgi
4
+ # Minitest helpers used by the showcase component.
5
+ # @api private
4
6
  module Test
7
+ # Showcase script suite collection.
8
+ # @api private
5
9
  class Suite
10
+ # Creates a suite from a showcase root directory.
11
+ # @param rootdir [String] showcase root directory
12
+ # @return [void]
6
13
  def initialize(rootdir)
7
14
  @scripts = load(rootdir)
8
15
  end
9
16
 
17
+ # Returns scripts belonging to a suite.
18
+ # @param suite [String, Symbol] suite name
19
+ # @return [Array<Sevgi::Test::Script>]
10
20
  def [](suite)
11
21
  (@cache ||= {})[suite] ||= @scripts.select { |script| script.suite == suite.to_s }
12
22
  end
13
23
 
24
+ # Returns all suite names.
25
+ # @return [Array<String>]
14
26
  def suites
15
27
  @suites ||= @scripts.map(&:suite).uniq
16
28
  end
17
29
 
30
+ # Suite names that are expected to be invalid examples.
18
31
  NON_VALIDS = ["gotcha"].freeze
19
32
 
33
+ # Returns scripts expected to be invalid.
34
+ # @return [Array<Sevgi::Test::Script>]
20
35
  def non_valids
21
36
  NON_VALIDS.map { self[it] }.flatten
22
37
  end
23
38
 
39
+ # Returns scripts expected to render successfully.
40
+ # @return [Array<Sevgi::Test::Script>]
24
41
  def valids
25
42
  suites.reject { NON_VALIDS.include?(it) }.map { self[it] }.flatten
26
43
  end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sevgi
4
+ module Graphics
5
+ module Document
6
+ class Base
7
+ # @overload Save(*args, **kwargs)
8
+ # Redirects showcase saves to stdout during passive integration tests.
9
+ # @param args [Array<Object>] ignored save arguments
10
+ # @param kwargs [Hash] render options
11
+ # @return [Object] F.out return value
12
+ # @api private
13
+ def Save(*, **) = Out(**)
14
+
15
+ # @overload Save!(*args, **kwargs)
16
+ # Redirects forced showcase saves to stdout during passive integration tests.
17
+ # @param args [Array<Object>] ignored save arguments
18
+ # @param kwargs [Hash] render options
19
+ # @return [Object] F.out return value
20
+ # @api private
21
+ def Save!(...) = Save(...)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sevgi
4
+ # Showcase component namespace.
4
5
  module Showcase
5
- VERSION = "0.73.2"
6
+ # Current showcase component version.
7
+ VERSION = "0.94.0"
6
8
  end
7
9
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "showcase/dark"
3
4
  require_relative "showcase/minitest"
4
5
 
5
6
  require_relative "showcase/version"
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env -S ruby -S sevgi
2
+ # frozen_string_literal: true
3
+
4
+ class CheckerBoard
5
+ BOARD_MARGIN = 50
6
+ CELL_SIZE = 100
7
+ PIECE_SIZE = 80
8
+
9
+ STYLE = {
10
+ ".board-frame" => { fill: "#d1bfa7", stroke: "#6a3f2b" },
11
+ ".inner-frame" => {
12
+ fill: "#6a3f2b",
13
+ stroke: "#6a3f2b",
14
+ "stroke-width": (BOARD_MARGIN * 0.7).round
15
+ },
16
+ ".cell" => { stroke: "white", "stroke-width": 1 },
17
+ ".light.cell" => { fill: "#d4a76f" },
18
+ ".dark.cell" => { fill: "#6a3f2b" },
19
+ ".piece" => {
20
+ filter: "drop-shadow(8px 8px 8px rgba(0, 0, 0, 0.5))",
21
+ stroke: "none"
22
+ },
23
+ ".light.piece" => { fill: "#f5f5f5" },
24
+ ".dark.piece" => { fill: "#333" }
25
+ }.freeze
26
+
27
+ def initialize
28
+ @pieces = []
29
+ end
30
+
31
+ def width = (CELL_SIZE * 8) + (BOARD_MARGIN * 2)
32
+
33
+ def height = width
34
+
35
+ def view_box = "0 0 #{width} #{height}"
36
+
37
+ def add_piece(row, col, dark: false)
38
+ @pieces << [row, col, dark]
39
+ end
40
+
41
+ def render_on(svg)
42
+ svg.css STYLE
43
+ add_frame(svg)
44
+ add_cells(svg)
45
+ add_pieces(svg)
46
+ end
47
+
48
+ private
49
+
50
+ def add_frame(svg)
51
+ svg.rect class: "board-frame", x: 0, y: 0, width: width, height: height
52
+ svg.rect class: "inner-frame", x: BOARD_MARGIN, y: BOARD_MARGIN,
53
+ width: width - (BOARD_MARGIN * 2),
54
+ height: height - (BOARD_MARGIN * 2)
55
+ end
56
+
57
+ def add_cells(svg)
58
+ 8.times do |row|
59
+ 8.times do |col|
60
+ svg.rect class: "#{(row + col).even? ? "dark" : "light"} cell",
61
+ x: (col * CELL_SIZE) + BOARD_MARGIN,
62
+ y: (row * CELL_SIZE) + BOARD_MARGIN,
63
+ width: CELL_SIZE,
64
+ height: CELL_SIZE
65
+ end
66
+ end
67
+ end
68
+
69
+ def add_pieces(svg)
70
+ @pieces.each do |row, col, dark|
71
+ x = (col * CELL_SIZE) + BOARD_MARGIN + 10
72
+ y = (row * CELL_SIZE) + BOARD_MARGIN + 10
73
+
74
+ svg.circle class: "#{dark ? "dark" : "light"} piece",
75
+ cx: x + (PIECE_SIZE / 2),
76
+ cy: y + (PIECE_SIZE / 2),
77
+ r: PIECE_SIZE / 2
78
+ end
79
+ end
80
+ end
81
+
82
+ board = CheckerBoard.new
83
+
84
+ [1, 3, 5, 7].each do |col|
85
+ board.add_piece 0, col, dark: true
86
+ board.add_piece 6, col, dark: false
87
+ end
88
+
89
+ [0, 2, 4, 6].each do |col|
90
+ board.add_piece 1, col, dark: true
91
+ board.add_piece 7, col, dark: false
92
+ end
93
+
94
+ SVG :minimal, width: board.width, height: board.height, viewBox: board.view_box do
95
+ board.render_on(self)
96
+ end.Save
@@ -0,0 +1,117 @@
1
+ <svg width="900" height="900" viewBox="0 0 900 900">
2
+ <style type="text/css">
3
+ <![CDATA[
4
+ .board-frame {
5
+ fill: #d1bfa7;
6
+ stroke: #6a3f2b;
7
+ }
8
+ .inner-frame {
9
+ fill: #6a3f2b;
10
+ stroke: #6a3f2b;
11
+ stroke-width: 35;
12
+ }
13
+ .cell {
14
+ stroke: white;
15
+ stroke-width: 1;
16
+ }
17
+ .light.cell {
18
+ fill: #d4a76f;
19
+ }
20
+ .dark.cell {
21
+ fill: #6a3f2b;
22
+ }
23
+ .piece {
24
+ filter: drop-shadow(8px 8px 8px rgba(0, 0, 0, 0.5));
25
+ stroke: none;
26
+ }
27
+ .light.piece {
28
+ fill: #f5f5f5;
29
+ }
30
+ .dark.piece {
31
+ fill: #333;
32
+ }
33
+ ]]>
34
+ </style>
35
+ <rect class="board-frame" x="0" y="0" width="900" height="900"/>
36
+ <rect class="inner-frame" x="50" y="50" width="800" height="800"/>
37
+ <rect class="dark cell" x="50" y="50" width="100" height="100"/>
38
+ <rect class="light cell" x="150" y="50" width="100" height="100"/>
39
+ <rect class="dark cell" x="250" y="50" width="100" height="100"/>
40
+ <rect class="light cell" x="350" y="50" width="100" height="100"/>
41
+ <rect class="dark cell" x="450" y="50" width="100" height="100"/>
42
+ <rect class="light cell" x="550" y="50" width="100" height="100"/>
43
+ <rect class="dark cell" x="650" y="50" width="100" height="100"/>
44
+ <rect class="light cell" x="750" y="50" width="100" height="100"/>
45
+ <rect class="light cell" x="50" y="150" width="100" height="100"/>
46
+ <rect class="dark cell" x="150" y="150" width="100" height="100"/>
47
+ <rect class="light cell" x="250" y="150" width="100" height="100"/>
48
+ <rect class="dark cell" x="350" y="150" width="100" height="100"/>
49
+ <rect class="light cell" x="450" y="150" width="100" height="100"/>
50
+ <rect class="dark cell" x="550" y="150" width="100" height="100"/>
51
+ <rect class="light cell" x="650" y="150" width="100" height="100"/>
52
+ <rect class="dark cell" x="750" y="150" width="100" height="100"/>
53
+ <rect class="dark cell" x="50" y="250" width="100" height="100"/>
54
+ <rect class="light cell" x="150" y="250" width="100" height="100"/>
55
+ <rect class="dark cell" x="250" y="250" width="100" height="100"/>
56
+ <rect class="light cell" x="350" y="250" width="100" height="100"/>
57
+ <rect class="dark cell" x="450" y="250" width="100" height="100"/>
58
+ <rect class="light cell" x="550" y="250" width="100" height="100"/>
59
+ <rect class="dark cell" x="650" y="250" width="100" height="100"/>
60
+ <rect class="light cell" x="750" y="250" width="100" height="100"/>
61
+ <rect class="light cell" x="50" y="350" width="100" height="100"/>
62
+ <rect class="dark cell" x="150" y="350" width="100" height="100"/>
63
+ <rect class="light cell" x="250" y="350" width="100" height="100"/>
64
+ <rect class="dark cell" x="350" y="350" width="100" height="100"/>
65
+ <rect class="light cell" x="450" y="350" width="100" height="100"/>
66
+ <rect class="dark cell" x="550" y="350" width="100" height="100"/>
67
+ <rect class="light cell" x="650" y="350" width="100" height="100"/>
68
+ <rect class="dark cell" x="750" y="350" width="100" height="100"/>
69
+ <rect class="dark cell" x="50" y="450" width="100" height="100"/>
70
+ <rect class="light cell" x="150" y="450" width="100" height="100"/>
71
+ <rect class="dark cell" x="250" y="450" width="100" height="100"/>
72
+ <rect class="light cell" x="350" y="450" width="100" height="100"/>
73
+ <rect class="dark cell" x="450" y="450" width="100" height="100"/>
74
+ <rect class="light cell" x="550" y="450" width="100" height="100"/>
75
+ <rect class="dark cell" x="650" y="450" width="100" height="100"/>
76
+ <rect class="light cell" x="750" y="450" width="100" height="100"/>
77
+ <rect class="light cell" x="50" y="550" width="100" height="100"/>
78
+ <rect class="dark cell" x="150" y="550" width="100" height="100"/>
79
+ <rect class="light cell" x="250" y="550" width="100" height="100"/>
80
+ <rect class="dark cell" x="350" y="550" width="100" height="100"/>
81
+ <rect class="light cell" x="450" y="550" width="100" height="100"/>
82
+ <rect class="dark cell" x="550" y="550" width="100" height="100"/>
83
+ <rect class="light cell" x="650" y="550" width="100" height="100"/>
84
+ <rect class="dark cell" x="750" y="550" width="100" height="100"/>
85
+ <rect class="dark cell" x="50" y="650" width="100" height="100"/>
86
+ <rect class="light cell" x="150" y="650" width="100" height="100"/>
87
+ <rect class="dark cell" x="250" y="650" width="100" height="100"/>
88
+ <rect class="light cell" x="350" y="650" width="100" height="100"/>
89
+ <rect class="dark cell" x="450" y="650" width="100" height="100"/>
90
+ <rect class="light cell" x="550" y="650" width="100" height="100"/>
91
+ <rect class="dark cell" x="650" y="650" width="100" height="100"/>
92
+ <rect class="light cell" x="750" y="650" width="100" height="100"/>
93
+ <rect class="light cell" x="50" y="750" width="100" height="100"/>
94
+ <rect class="dark cell" x="150" y="750" width="100" height="100"/>
95
+ <rect class="light cell" x="250" y="750" width="100" height="100"/>
96
+ <rect class="dark cell" x="350" y="750" width="100" height="100"/>
97
+ <rect class="light cell" x="450" y="750" width="100" height="100"/>
98
+ <rect class="dark cell" x="550" y="750" width="100" height="100"/>
99
+ <rect class="light cell" x="650" y="750" width="100" height="100"/>
100
+ <rect class="dark cell" x="750" y="750" width="100" height="100"/>
101
+ <circle class="dark piece" cx="200" cy="100" r="40"/>
102
+ <circle class="light piece" cx="200" cy="700" r="40"/>
103
+ <circle class="dark piece" cx="400" cy="100" r="40"/>
104
+ <circle class="light piece" cx="400" cy="700" r="40"/>
105
+ <circle class="dark piece" cx="600" cy="100" r="40"/>
106
+ <circle class="light piece" cx="600" cy="700" r="40"/>
107
+ <circle class="dark piece" cx="800" cy="100" r="40"/>
108
+ <circle class="light piece" cx="800" cy="700" r="40"/>
109
+ <circle class="dark piece" cx="100" cy="200" r="40"/>
110
+ <circle class="light piece" cx="100" cy="800" r="40"/>
111
+ <circle class="dark piece" cx="300" cy="200" r="40"/>
112
+ <circle class="light piece" cx="300" cy="800" r="40"/>
113
+ <circle class="dark piece" cx="500" cy="200" r="40"/>
114
+ <circle class="light piece" cx="500" cy="800" r="40"/>
115
+ <circle class="dark piece" cx="700" cy="200" r="40"/>
116
+ <circle class="light piece" cx="700" cy="800" r="40"/>
117
+ </svg>