├── .github ├── FUNDING.yml └── workflows │ └── test.yml ├── .gitattributes ├── lib ├── victor │ ├── version.rb │ ├── script.rb │ ├── templates │ │ ├── minimal.svg │ │ └── default.svg │ ├── svg.rb │ ├── dsl.rb │ ├── marshaling.rb │ ├── attributes.rb │ ├── css.rb │ ├── component.rb │ └── svg_base.rb └── victor.rb ├── spec ├── approvals │ ├── css │ │ ├── css3 │ │ ├── css1 │ │ ├── render │ │ └── css2 │ ├── svg │ │ ├── minimal │ │ ├── glue │ │ ├── full │ │ └── css │ └── component │ │ └── set1 │ │ └── render ├── fixtures │ ├── custom_template.svg │ ├── dsl_script.rb │ └── components │ │ └── component_set1.rb ├── README.md ├── victor │ ├── svg_base_spec.rb │ ├── script_spec.rb │ ├── marshaling_spec.rb │ ├── component_subclass_spec.rb │ ├── svg_marshaling_spec.rb │ ├── dsl_spec.rb │ ├── css_spec.rb │ ├── attributes_spec.rb │ ├── component_spec.rb │ └── svg_spec.rb └── spec_helper.rb ├── .rspec ├── .codespellrc ├── .gitignore ├── .envrc.example ├── Runfile ├── Gemfile ├── config.reek ├── assets ├── ghost.svg └── logo.svg ├── examples └── README.md ├── victor.gemspec ├── .rubocop.yml ├── LICENSE ├── README.md └── CHANGELOG.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: DannyBen 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | Runfile linguist-language=Ruby 2 | -------------------------------------------------------------------------------- /lib/victor/version.rb: -------------------------------------------------------------------------------- 1 | module Victor 2 | VERSION = '0.5.0' 3 | end 4 | -------------------------------------------------------------------------------- /spec/approvals/css/css3: -------------------------------------------------------------------------------- 1 | @import some url; 2 | @import another url; -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | --color 3 | --format documentation 4 | --fail-fast -------------------------------------------------------------------------------- /lib/victor/script.rb: -------------------------------------------------------------------------------- 1 | require 'victor' 2 | include Victor 3 | include Victor::DSL 4 | -------------------------------------------------------------------------------- /spec/approvals/css/css1: -------------------------------------------------------------------------------- 1 | .main { 2 | color: black; 3 | background: white; 4 | } -------------------------------------------------------------------------------- /lib/victor/templates/minimal.svg: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /spec/approvals/css/render: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /.codespellrc: -------------------------------------------------------------------------------- 1 | [codespell] 2 | skip = coverage,tmp,Gemfile.lock,sublime* 3 | ; ignore-words-list = rouge 4 | -------------------------------------------------------------------------------- /spec/fixtures/custom_template.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /spec/fixtures/dsl_script.rb: -------------------------------------------------------------------------------- 1 | require 'victor/script' 2 | 3 | p SVG 4 | p svg.class 5 | p respond_to? :build 6 | -------------------------------------------------------------------------------- /lib/victor/templates/default.svg: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /spec/approvals/svg/minimal: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /spec/README.md: -------------------------------------------------------------------------------- 1 | Testing 2 | ================================================== 3 | 4 | Run tests with: 5 | 6 | $ bundle exec run spec 7 | 8 | -------------------------------------------------------------------------------- /spec/approvals/css/css2: -------------------------------------------------------------------------------- 1 | @keyframes animation { 2 | 0% { 3 | font-size: 10px; 4 | } 5 | 30% { 6 | font-size: 15px; 7 | } 8 | } -------------------------------------------------------------------------------- /spec/approvals/svg/glue: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /spec/approvals/svg/full: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | /.yardoc 3 | /coverage 4 | /debug.runfile 5 | /dev 6 | /doc 7 | /Gemfile.lock 8 | /gems 9 | /spec/coverage 10 | /spec/status.txt 11 | /test.svg 12 | /tmp 13 | -------------------------------------------------------------------------------- /spec/victor/svg_base_spec.rb: -------------------------------------------------------------------------------- 1 | describe Victor::SVGBase do 2 | it 'does not define #method_missing' do 3 | expect { subject.polygon }.to raise_error NoMethodError 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /.envrc.example: -------------------------------------------------------------------------------- 1 | # for git-changelog (https://github.com/dannyben/git-changelog) 2 | export CHANGELOG_COMMIT_URL=https://github.com/DannyBen/victor/commit/%h 3 | export CHANGELOG_COMPARE_URL=https://github.com/dannyben/victor/compare/%s 4 | -------------------------------------------------------------------------------- /lib/victor/svg.rb: -------------------------------------------------------------------------------- 1 | module Victor 2 | class SVG < SVGBase 3 | def method_missing(method_sym, ...) 4 | tag(method_sym, ...) 5 | end 6 | 7 | def respond_to_missing?(*) 8 | true 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /Runfile: -------------------------------------------------------------------------------- 1 | require 'filewatcher' 2 | require 'debug' 3 | require 'victor' 4 | 5 | title "Victor Developer Toolbelt" 6 | summary "Runfile tasks for building the Victor gem" 7 | version Victor::VERSION 8 | 9 | import_gem 'runfile-tasks/gem' 10 | import 'debug' 11 | -------------------------------------------------------------------------------- /spec/approvals/svg/css: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /spec/victor/script_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'victor/script' do 2 | subject { File.read 'spec/fixtures/dsl_script.rb' } 3 | 4 | it 'includes Victor and Victor::DSL' do 5 | expect { eval subject }.to output("Victor::SVG\nVictor::SVG\ntrue\n").to_stdout 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'debug' 4 | gem 'filewatcher', require: false 5 | gem 'rentacop', require: false 6 | gem 'rspec' 7 | gem 'rspec_approvals' 8 | gem 'runfile', require: false 9 | gem 'runfile-tasks', require: false 10 | gem 'simplecov' 11 | 12 | gemspec 13 | -------------------------------------------------------------------------------- /lib/victor/dsl.rb: -------------------------------------------------------------------------------- 1 | require 'forwardable' 2 | 3 | module Victor 4 | module DSL 5 | extend Forwardable 6 | def_delegators :svg, :setup, :build, :save, :render, :append, :element, :tag, :css 7 | 8 | def svg 9 | @svg ||= Victor::SVG.new 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /config.reek: -------------------------------------------------------------------------------- 1 | --- 2 | exclude_paths: 3 | - dev 4 | 5 | Attribute: 6 | enabled: false 7 | 8 | FeatureEnvy: 9 | exclude: 10 | - Victor::Attributes#to_s 11 | - Victor::CSS#convert_hash 12 | - Victor::CSS#css_block 13 | 14 | TooManyStatements: 15 | max_statements: 7 16 | exclude: 17 | - Victor::CSS#css_block 18 | - Victor::SVG#element 19 | -------------------------------------------------------------------------------- /spec/victor/marshaling_spec.rb: -------------------------------------------------------------------------------- 1 | describe Victor::Marshaling do 2 | subject do 3 | Class.new do 4 | include Victor::Marshaling 5 | end.new 6 | end 7 | 8 | describe '#marshaling' do 9 | it 'raises a NotImplementedError' do 10 | expect { subject.marshaling }.to raise_error(NotImplementedError) 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/victor.rb: -------------------------------------------------------------------------------- 1 | require 'victor/version' 2 | 3 | module Victor 4 | autoload :Attributes, 'victor/attributes' 5 | autoload :Component, 'victor/component' 6 | autoload :CSS, 'victor/css' 7 | autoload :DSL, 'victor/dsl' 8 | autoload :Marshaling, 'victor/marshaling' 9 | autoload :SVG, 'victor/svg' 10 | autoload :SVGBase, 'victor/svg_base' 11 | end 12 | -------------------------------------------------------------------------------- /spec/victor/component_subclass_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../fixtures/components/component_set1' 2 | 3 | describe 'Component subclassing' do 4 | subject { ComponentSet1::Main.new } 5 | 6 | describe '#render' do 7 | it 'returns the expected SVG' do 8 | expect(subject.render).to match_approval 'component/set1/render' 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'simplecov' 2 | unless ENV['NOCOV'] 3 | SimpleCov.start do 4 | enable_coverage :branch if ENV['BRANCH_COV'] 5 | coverage_dir 'spec/coverage' 6 | # track_files 'lib/**/*.rb' 7 | end 8 | end 9 | 10 | require 'bundler' 11 | Bundler.require :default, :development 12 | 13 | RSpec.configure do |config| 14 | config.example_status_persistence_file_path = 'spec/status.txt' 15 | end 16 | -------------------------------------------------------------------------------- /spec/approvals/component/set1/render: -------------------------------------------------------------------------------- 1 | 23 | -------------------------------------------------------------------------------- /assets/ghost.svg: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | pull_request: 4 | push: { branches: master } 5 | 6 | jobs: 7 | test: 8 | name: Ruby ${{ matrix.ruby }} 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: { ruby: ['3.1', '3.2', '3.3', '3.4'] } 12 | 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v4 16 | 17 | - name: Install OS dependencies 18 | run: sudo apt-get -y install libyaml-dev 19 | 20 | - name: Setup Ruby 21 | uses: ruby/setup-ruby@v1 22 | with: 23 | ruby-version: '${{ matrix.ruby }}' 24 | bundler-cache: true 25 | 26 | - name: Run tests 27 | run: bundle exec rspec 28 | -------------------------------------------------------------------------------- /spec/fixtures/components/component_set1.rb: -------------------------------------------------------------------------------- 1 | module ComponentSet1 2 | class Base < Victor::Component 3 | def width = 100 4 | def height = 100 5 | end 6 | 7 | class Main < Base 8 | def body 9 | add.g transform: 'translate(10, 10)' do 10 | append Two.new 11 | end 12 | end 13 | 14 | def style = { '.one': { stroke: :magenta } } 15 | end 16 | 17 | class Two < Base 18 | def body 19 | add.text 'Two' 20 | append Three.new 21 | end 22 | 23 | def style = { '.two': { stroke: :magenta } } 24 | end 25 | 26 | class Three < Base 27 | def body 28 | add.text 'Tada' 29 | end 30 | 31 | def style = { '.three': { stroke: :magenta } } 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | Victor offers a wide range of possibilities for creating SVG images. To help you 4 | get started or explore more advanced techniques, we've provided a variety of 5 | examples. 6 | 7 | --- 8 | 9 | ### ➜ [Victor Documentation][1] ❯ [Ways to Use Victor][2] 10 | 11 | For comprehensive explanations and step-by-step guides: 12 | 13 | This section is designed to guide you through different usage patterns, from basic usage to 14 | more complex implementations, allowing you to fully leverage Victor's 15 | capabilities. 16 | 17 | --- 18 | 19 | ### ➜ [Victor Documentation Source Code / examples folder][3] 20 | 21 | If you prefer browsing the example source code. 22 | 23 | [1]: https://victor.dannyb.co/ 24 | [2]: https://victor.dannyb.co/usage-patterns/ 25 | [3]: https://github.com/DannyBen/victor-book/tree/master/src/examples 26 | -------------------------------------------------------------------------------- /lib/victor/marshaling.rb: -------------------------------------------------------------------------------- 1 | module Victor 2 | module Marshaling 3 | def marshaling 4 | raise NotImplementedError, "#{self.class.name} must implement `marshaling'" 5 | end 6 | 7 | # YAML serialization methods 8 | def encode_with(coder) 9 | marshaling.each do |attr| 10 | coder[attr.to_s] = send(attr) 11 | end 12 | end 13 | 14 | def init_with(coder) 15 | marshaling.each do |attr| 16 | instance_variable_set(:"@#{attr}", coder[attr.to_s]) 17 | end 18 | end 19 | 20 | # Marshal serialization methods 21 | def marshal_dump 22 | marshaling.to_h do |attr| 23 | [attr, send(attr)] 24 | end 25 | end 26 | 27 | def marshal_load(data) 28 | marshaling.each do |attr| 29 | instance_variable_set(:"@#{attr}", data[attr]) 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /victor.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path('lib', __dir__) 2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 3 | require 'victor/version' 4 | 5 | Gem::Specification.new do |s| 6 | s.name = 'victor' 7 | s.version = Victor::VERSION 8 | s.summary = 'SVG Builder' 9 | s.description = 'Build SVG images with ease' 10 | s.authors = ['Danny Ben Shitrit'] 11 | s.email = 'db@dannyben.com' 12 | s.files = Dir['README.md', 'lib/**/*.*'] 13 | s.homepage = 'https://github.com/DannyBen/victor' 14 | s.license = 'MIT' 15 | 16 | s.required_ruby_version = '>= 3.1' 17 | 18 | s.metadata = { 19 | 'bug_tracker_uri' => 'https://github.com/DannyBen/victor/issues', 20 | 'changelog_uri' => 'https://github.com/DannyBen/victor/blob/master/CHANGELOG.md', 21 | 'homepage_uri' => 'https://victor.dannyb.co/', 22 | 'source_code_uri' => 'https://github.com/DannyBen/victor', 23 | 'rubygems_mfa_required' => 'true', 24 | } 25 | end 26 | -------------------------------------------------------------------------------- /spec/victor/svg_marshaling_spec.rb: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | 3 | describe Victor::SVG do 4 | subject do 5 | described_class.new viewBox: '0 0 10 100' do 6 | text 'Hello world' 7 | rect x: 0, y: 0, width: 10, height: 100 8 | css['*'] = { font_family: 'Assistant' } 9 | end 10 | end 11 | 12 | describe 'YAML marshaling' do 13 | it 'serializes and deserializes correctly using YAML' do 14 | yaml_data = YAML.dump subject 15 | restored_object = YAML.unsafe_load yaml_data 16 | 17 | expect(restored_object).to be_a described_class 18 | expect(restored_object.render).to eq subject.render 19 | end 20 | end 21 | 22 | describe 'Ruby marshaling' do 23 | it 'serializes and deserializes correctly using Marshal' do 24 | marshaled_data = Marshal.dump subject 25 | restored_object = Marshal.load marshaled_data 26 | 27 | expect(restored_object).to be_a described_class 28 | expect(restored_object.render).to eq subject.render 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | require: 2 | - rubocop-rspec 3 | - rubocop-performance 4 | 5 | inherit_gem: 6 | rentacop: 7 | - rentacop.yml 8 | - rspec.yml 9 | 10 | # Merge `Exclude` arrays 11 | inherit_mode: 12 | merge: 13 | - Exclude 14 | 15 | AllCops: 16 | TargetRubyVersion: 3.1 17 | SuggestExtensions: false 18 | Exclude: 19 | - 'dev/**/*' 20 | 21 | # There is a special use case that needs this 22 | Lint/LiteralAsCondition: 23 | Exclude: 24 | - 'spec/**/*' 25 | 26 | # `SVGBase#element` is a bit complex 27 | Metrics/PerceivedComplexity: 28 | Max: 11 29 | 30 | # This test is allowed to use `eval` 31 | Security/Eval: 32 | Exclude: 33 | - spec/victor/script_spec.rb 34 | 35 | # Allow `include Victor` in some places 36 | Style/MixinUsage: 37 | Exclude: 38 | - lib/victor/script.rb 39 | 40 | # We use Marshal.load to test that it *can* be done. Allow it. 41 | Security/MarshalLoad: 42 | Exclude: 43 | - spec/victor/svg_marshaling_spec.rb 44 | 45 | RSpec/ExampleLength: 46 | Exclude: 47 | - spec/victor/svg_spec.rb 48 | -------------------------------------------------------------------------------- /lib/victor/attributes.rb: -------------------------------------------------------------------------------- 1 | module Victor 2 | # Handles conversion from a Hash of attributes, to an XML string or 3 | # a CSS string. 4 | class Attributes 5 | attr_reader :attributes 6 | 7 | def initialize(attributes = {}) 8 | @attributes = attributes 9 | end 10 | 11 | def to_s 12 | mapped = attributes.map do |key, value| 13 | key = key.to_s.tr '_', '-' 14 | 15 | case value 16 | when Hash 17 | style = Attributes.new(value).to_style 18 | "#{key}=\"#{style}\"" 19 | when Array 20 | "#{key}=\"#{value.join ' '}\"" 21 | else 22 | "#{key}=#{value.to_s.encode(xml: :attr)}" 23 | end 24 | end 25 | 26 | mapped.join ' ' 27 | end 28 | 29 | def to_style 30 | mapped = attributes.map do |key, value| 31 | key = key.to_s.tr '_', '-' 32 | "#{key}:#{value}" 33 | end 34 | 35 | mapped.join '; ' 36 | end 37 | 38 | def [](key) 39 | attributes[key] 40 | end 41 | 42 | def []=(key, value) 43 | attributes[key] = value 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Danny Ben Shitrit 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. -------------------------------------------------------------------------------- /spec/victor/dsl_spec.rb: -------------------------------------------------------------------------------- 1 | describe Victor::DSL do 2 | subject { Class.new { include Victor::DSL }.new } 3 | 4 | describe '#svg' do 5 | it 'returns a Victor::SVG.instance' do 6 | expect(subject.svg).to be_a Victor::SVG 7 | end 8 | end 9 | 10 | describe '#setup' do 11 | it 'forwards the call to the svg object' do 12 | expect(subject.svg).to receive(:setup).with(attrib: :utes) 13 | subject.setup attrib: :utes 14 | end 15 | end 16 | 17 | describe '#build' do 18 | it 'forwards the call to the svg object' do 19 | expect(subject.svg).to receive(:build) 20 | subject.build 21 | end 22 | end 23 | 24 | describe '#save' do 25 | it 'forwards the call to the svg object' do 26 | expect(subject.svg).to receive(:save).with('filename') 27 | subject.save 'filename' 28 | end 29 | end 30 | 31 | describe '#render' do 32 | it 'forwards the call to the svg object' do 33 | expect(subject.svg).to receive(:render) 34 | subject.render 35 | end 36 | end 37 | 38 | describe '#css' do 39 | it 'forwards the call to the svg object' do 40 | expect(subject.svg).to receive(:css) 41 | subject.css 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /assets/logo.svg: -------------------------------------------------------------------------------- 1 | 38 | -------------------------------------------------------------------------------- /lib/victor/css.rb: -------------------------------------------------------------------------------- 1 | module Victor 2 | class CSS 3 | attr_reader :attributes 4 | 5 | def initialize(attributes = nil) 6 | @attributes = attributes || {} 7 | end 8 | 9 | def to_s 10 | convert_hash attributes 11 | end 12 | 13 | def render 14 | return '' if attributes.empty? 15 | 16 | %[\n] 17 | end 18 | 19 | protected 20 | 21 | def convert_hash(hash, indent = 2) 22 | return hash unless hash.is_a? Hash 23 | 24 | result = [] 25 | hash.each do |key, value| 26 | key = key.to_s.tr '_', '-' 27 | result += css_block(key, value, indent) 28 | end 29 | 30 | result.join "\n" 31 | end 32 | 33 | def css_block(key, value, indent) 34 | result = [] 35 | 36 | my_indent = ' ' * indent 37 | 38 | case value 39 | when Hash 40 | result.push "#{my_indent}#{key} {" 41 | result.push convert_hash(value, indent + 2) 42 | result.push "#{my_indent}}" 43 | when Array 44 | value.each do |row| 45 | result.push "#{my_indent}#{key} #{row};" 46 | end 47 | else 48 | result.push "#{my_indent}#{key}: #{value};" 49 | end 50 | 51 | result 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
|
26 |
27 | | 30 | 31 | ```ruby 32 | setup viewBox: '0 0 100 100' 33 | 34 | build do 35 | rect x: 0, y: 0, width: 100, height: 100, fill: :white 36 | circle cx: 50, cy: 50, r: 40, fill: 'yellow' 37 | rect x: 10, y: 50, width: 80, height: 50, fill: :yellow 38 | 39 | [25, 50].each do |x| 40 | circle cx: x, cy: 40, r: 8, fill: :white 41 | end 42 | 43 | path fill: 'white', d: %w[ 44 | M11 100 l13 -15 l13 15 l13 -15 45 | l13 15 l13 -15 l13 15 Z 46 | ] 47 | end 48 | ``` 49 | 50 | |