Skip to content

Commit

Permalink
Merge pull request #74 from DannyBen/add/component
Browse files Browse the repository at this point in the history
Add `Victor::Component` for component-driven SVG composition
  • Loading branch information
DannyBen authored Aug 29, 2024
2 parents 23ffa0a + 79940d6 commit 3461552
Show file tree
Hide file tree
Showing 15 changed files with 287 additions and 55 deletions.
2 changes: 1 addition & 1 deletion .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ AllCops:
TargetRubyVersion: 3.0
SuggestExtensions: false
Exclude:
- 'dev/*'
- 'dev/**/*'

# There is a special use case that needs this
Lint/LiteralAsCondition:
Expand Down
16 changes: 10 additions & 6 deletions lib/victor.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
require 'victor/version'
require 'victor/marshaling'
require 'victor/svg_base'
require 'victor/svg'
require 'victor/attributes'
require 'victor/css'
require 'victor/dsl'

module Victor
autoload :Attributes, 'victor/attributes'
autoload :Component, 'victor/component'
autoload :CSS, 'victor/css'
autoload :DSL, 'victor/dsl'
autoload :Marshaling, 'victor/marshaling'
autoload :SVG, 'victor/svg'
autoload :SVGBase, 'victor/svg_base'
end
61 changes: 61 additions & 0 deletions lib/victor/component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
require 'forwardable'

module Victor
class Component
extend Forwardable
include Marshaling

def_delegators :svg, :save, :render, :content, :element, :css, :to_s

# Marshaling data
def marshaling = %i[width height x y svg merged_css]

# Subclasses MUST implement this
def body
raise(NotImplementedError, "#{self.class.name} must implement `body'")
end

# Subclasses MUST override these methods, OR assign instance vars
def height
@height || raise(NotImplementedError,
"#{self.class.name} must implement `height' or `@height'")
end

def width
@width || raise(NotImplementedError,
"#{self.class.name} must implement `width' or `@width'")
end

# Subclasses MAY override these methods, OR assign instance vars
def style = @style ||= {}
def x = @x ||= 0
def y = @y ||= 0

# Appending/Embedding - DSL for the `#body` implementation
def append(component)
svg_instance.append component.svg
merged_css.merge! component.merged_css
end
alias embed append

# SVG / CSS
def svg
@svg ||= begin
body
svg_instance.css = merged_css
svg_instance
end
end

protected

# Start with an ordinary SVG instance
def svg_instance = @svg_instance ||= SVG.new(viewBox: "#{x} #{y} #{width} #{height}")

# Internal DSL to enable `add.anything` in the `#body` implementation
alias add svg_instance

# Start with a copy of our own style
def merged_css = @merged_css ||= style.dup
end
end
38 changes: 16 additions & 22 deletions lib/victor/marshaling.rb
Original file line number Diff line number Diff line change
@@ -1,39 +1,33 @@
module Victor
module Marshaling
def marshaling
raise NotImplementedError, "#{self.class.name} must implement `marshaling'"
end

# YAML serialization methods
def encode_with(coder)
coder['template'] = @template
coder['glue'] = @glue
coder['svg_attributes'] = @svg_attributes
coder['css'] = @css
coder['content'] = @content
marshaling.each do |attr|
coder[attr.to_s] = send(attr)
end
end

def init_with(coder)
@template = coder['template']
@glue = coder['glue']
@svg_attributes = coder['svg_attributes']
@css = coder['css']
@content = coder['content']
marshaling.each do |attr|
instance_variable_set(:"@#{attr}", coder[attr.to_s])
end
end

# Marshal serialization methods
def marshal_dump
{
template: @template,
glue: @glue,
svg_attributes: @svg_attributes,
css: @css,
content: @content,
}
marshaling.to_h do |attr|
[attr, send(attr)]
end
end

def marshal_load(data)
@template = data[:template]
@glue = data[:glue]
@svg_attributes = data[:svg_attributes]
@css = data[:css]
@content = data[:content]
marshaling.each do |attr|
instance_variable_set(:"@#{attr}", data[attr])
end
end
end
end
5 changes: 5 additions & 0 deletions lib/victor/svg_base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,15 @@ def initialize(attributes = nil, &block)
build(&block) if block
end

def marshaling
%i[template glue svg_attributes css content]
end

def <<(additional_content)
content.push additional_content.to_s
end
alias append <<
alias embed <<

def setup(attributes = nil)
attributes ||= {}
Expand Down
6 changes: 1 addition & 5 deletions lib/victor/templates/default.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 22 additions & 0 deletions spec/approvals/component/set1/render
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<svg viewBox="0 0 100 100" width="100%" height="100%" xmlns="http://www.w3.org/2000/svg">
<style>
.one {
stroke: magenta;
}
.two {
stroke: magenta;
}
.three {
stroke: magenta;
}
</style>

<g transform="translate(10, 10)">
<text>
Two
</text>
<text>
Tada
</text>
</g>
</svg>
6 changes: 1 addition & 5 deletions spec/approvals/svg/css
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
<svg width="100%" height="100%"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">

<svg width="100%" height="100%" xmlns="http://www.w3.org/2000/svg">
<style>
.main {
stroke: green;
Expand All @@ -11,5 +8,4 @@

<circle radius="10"/>
<circle radius="20"/>

</svg>
6 changes: 1 addition & 5 deletions spec/approvals/svg/full
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
<svg width="100%" height="100%"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">

<svg width="100%" height="100%" xmlns="http://www.w3.org/2000/svg">

<circle radius="10"/>
<circle radius="20"/>

</svg>
6 changes: 1 addition & 5 deletions spec/approvals/svg/glue
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
<svg width="100%" height="100%"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">

<svg width="100%" height="100%" xmlns="http://www.w3.org/2000/svg">

<circle radius="10"/><circle radius="20"/>

</svg>
33 changes: 33 additions & 0 deletions spec/fixtures/components/component_set1.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
module ComponentSet1
class Base < Victor::Component
def width = 100
def height = 100
end

class Main < Base
def body
add.g transform: 'translate(10, 10)' do
append Two.new
end
end

def style = { '.one': { stroke: :magenta } }
end

class Two < Base
def body
add.text 'Two'
append Three.new
end

def style = { '.two': { stroke: :magenta } }
end

class Three < Base
def body
add.text 'Tada'
end

def style = { '.three': { stroke: :magenta } }
end
end
103 changes: 103 additions & 0 deletions spec/victor/component_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
describe Victor::Component do
describe '#body' do
it 'raises a NotImplementedError' do
expect { subject.body }.to raise_error(NotImplementedError)
end
end

describe '#height' do
it 'raises a NotImplementedError' do
expect { subject.height }.to raise_error(NotImplementedError)
end
end

describe '#width' do
it 'raises a NotImplementedError' do
expect { subject.width }.to raise_error(NotImplementedError)
end
end

describe '#style' do
it 'returns an empty hash' do
expect(subject.style).to eq({})
end
end

describe '#x' do
it 'returns 0' do
expect(subject.x).to eq 0
end
end

describe '#y' do
it 'returns 0' do
expect(subject.y).to eq 0
end
end

context 'when all required methods are implemented' do
let(:svg) do
double save: true, render: true, content: true, element: true, to_s: true
end

before do
allow(subject).to receive_messages(body: nil, width: 100, height: 100)
allow(subject).to receive(:svg).and_return(svg)
end

describe '#save' do
it 'delegates to SVG' do
expect(svg).to receive(:save).with('filename')
subject.save 'filename'
end
end

describe '#render' do
it 'delegates to SVG' do
expect(svg).to receive(:render).with(template: :minimal)
subject.render template: :minimal
end
end

describe '#content' do
it 'delegates to SVG' do
expect(svg).to receive(:content)
subject.content
end
end

describe '#element' do
it 'delegates to SVG' do
expect(svg).to receive(:element).with(:rect)
subject.element :rect
end
end

describe '#to_s' do
it 'delegates to SVG' do
expect(svg).to receive(:to_s)
subject.to_s
end
end

describe '#append' do
let(:component) { double svg: 'mocked_svg', merged_css: { color: 'red' } }
let(:svg_instance) { double append: true }
let(:merged_css) { double merge!: true }

it 'appends another component and merges its css' do
allow(subject).to receive_messages(svg_instance: svg_instance, merged_css: merged_css)
expect(svg_instance).to receive(:append).with('mocked_svg')
expect(merged_css).to receive(:merge!).with({ color: 'red' })

subject.append component
end
end

describe '#embed' do
it 'is an alias to #append' do
expect(subject.method(:embed)).to eq subject.method(:append)
end
end
end
end
11 changes: 11 additions & 0 deletions spec/victor/component_subclass_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
require_relative '../fixtures/components/component_set1'

describe 'Component subclassing' do
subject { ComponentSet1::Main.new }

describe '#render' do
it 'returns the expected SVG' do
expect(subject.render).to match_approval 'component/set1/render'
end
end
end
13 changes: 13 additions & 0 deletions spec/victor/marshaling_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
describe Victor::Marshaling do
subject do
Class.new do
include Victor::Marshaling
end.new
end

describe '#marshaling' do
it 'raises a NotImplementedError' do
expect { subject.marshaling }.to raise_error(NotImplementedError)
end
end
end
Loading

0 comments on commit 3461552

Please sign in to comment.