diff --git a/.rubocop.yml b/.rubocop.yml index 878fc63..916fa4a 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -16,7 +16,7 @@ AllCops: TargetRubyVersion: 3.0 SuggestExtensions: false Exclude: - - 'dev/*' + - 'dev/**/*' # There is a special use case that needs this Lint/LiteralAsCondition: diff --git a/lib/victor.rb b/lib/victor.rb index b25b21f..6891134 100644 --- a/lib/victor.rb +++ b/lib/victor.rb @@ -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 diff --git a/lib/victor/component.rb b/lib/victor/component.rb new file mode 100644 index 0000000..4f4bfba --- /dev/null +++ b/lib/victor/component.rb @@ -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 diff --git a/lib/victor/marshaling.rb b/lib/victor/marshaling.rb index 6c1771a..ea6bc67 100644 --- a/lib/victor/marshaling.rb +++ b/lib/victor/marshaling.rb @@ -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 diff --git a/lib/victor/svg_base.rb b/lib/victor/svg_base.rb index 0f490c7..1b9ead8 100644 --- a/lib/victor/svg_base.rb +++ b/lib/victor/svg_base.rb @@ -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 ||= {} diff --git a/lib/victor/templates/default.svg b/lib/victor/templates/default.svg index 134e25e..4e1ac24 100644 --- a/lib/victor/templates/default.svg +++ b/lib/victor/templates/default.svg @@ -1,8 +1,4 @@ - - + %{style} %{content} - diff --git a/spec/approvals/component/set1/render b/spec/approvals/component/set1/render new file mode 100644 index 0000000..c0e1289 --- /dev/null +++ b/spec/approvals/component/set1/render @@ -0,0 +1,22 @@ + + + + + +Two + + +Tada + + + diff --git a/spec/approvals/svg/css b/spec/approvals/svg/css index ed8825d..2b8a386 100644 --- a/spec/approvals/svg/css +++ b/spec/approvals/svg/css @@ -1,7 +1,4 @@ - - + diff --git a/spec/approvals/svg/full b/spec/approvals/svg/full index 7ffd384..320ec88 100644 --- a/spec/approvals/svg/full +++ b/spec/approvals/svg/full @@ -1,9 +1,5 @@ - - + - diff --git a/spec/approvals/svg/glue b/spec/approvals/svg/glue index 9aaa8e3..3e15066 100644 --- a/spec/approvals/svg/glue +++ b/spec/approvals/svg/glue @@ -1,8 +1,4 @@ - - + - diff --git a/spec/fixtures/components/component_set1.rb b/spec/fixtures/components/component_set1.rb new file mode 100644 index 0000000..8725b84 --- /dev/null +++ b/spec/fixtures/components/component_set1.rb @@ -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 diff --git a/spec/victor/component_spec.rb b/spec/victor/component_spec.rb new file mode 100644 index 0000000..9be14e8 --- /dev/null +++ b/spec/victor/component_spec.rb @@ -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 diff --git a/spec/victor/component_subclass_spec.rb b/spec/victor/component_subclass_spec.rb new file mode 100644 index 0000000..7c2eb27 --- /dev/null +++ b/spec/victor/component_subclass_spec.rb @@ -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 diff --git a/spec/victor/marshaling_spec.rb b/spec/victor/marshaling_spec.rb new file mode 100644 index 0000000..ee2a092 --- /dev/null +++ b/spec/victor/marshaling_spec.rb @@ -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 diff --git a/spec/victor/svg_spec.rb b/spec/victor/svg_spec.rb index 83ce33d..3c47cc3 100644 --- a/spec/victor/svg_spec.rb +++ b/spec/victor/svg_spec.rb @@ -1,5 +1,5 @@ describe Victor::SVG do - describe '#new' do + describe '#initialize' do it 'sets default attributes' do expect(subject.svg_attributes[:height]).to eq '100%' expect(subject.svg_attributes[:width]).to eq '100%' @@ -58,12 +58,14 @@ end describe '#append' do - it 'pushes stringable objects as content' do - subject.append fire - subject.append earth - subject.append water + it 'is an alias to #<<' do + expect(subject.method(:append)).to eq subject.method(:<<) + end + end - expect(subject.to_s).to eq "\n\n" + describe '#embed' do + it 'is an alias to #<<' do + expect(subject.method(:embed)).to eq subject.method(:<<) end end end