Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Victor::Component for component-driven SVG composition #74

Merged
merged 11 commits into from
Aug 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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