diff --git a/CHANGELOG.md b/CHANGELOG.md index 382d486..4d96b89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Features: Bugs: * Fresh installs of Squib were broken due to two hidden dependencies, gio2 and gobject-introspection. (#172) * Embedding icons in text show unicode placeholders on some OSs. This is a workaround until we get a better solution for embedding icons. See #170, #171, and #176. For that matter, see #103, #153, and #30 if you really want the whole story. +* Unit conversion is supported when using `extends` in layouts, as promised in the docs (#173) ## v0.10.0 / 2016-05-06 diff --git a/lib/squib/api/settings.rb b/lib/squib/api/settings.rb index 6cb181e..739df60 100644 --- a/lib/squib/api/settings.rb +++ b/lib/squib/api/settings.rb @@ -14,7 +14,7 @@ module Squib # DSL method. See http://squib.readthedocs.io def use_layout(file: 'layout.yml') - @layout = LayoutParser.load_layout(file, @layout) + @layout = LayoutParser.new(@dpi).load_layout(file, @layout) end end diff --git a/lib/squib/deck.rb b/lib/squib/deck.rb index 8b93f1b..315a194 100644 --- a/lib/squib/deck.rb +++ b/lib/squib/deck.rb @@ -68,7 +68,7 @@ module Squib @width = Args::UnitConversion.parse width, dpi @height = Args::UnitConversion.parse height, dpi cards.times{ |i| @cards << Squib::Card.new(self, @width, @height, i) } - @layout = LayoutParser.load_layout(layout) + @layout = LayoutParser.new(dpi).load_layout(layout) enable_groups_from_env! if block_given? instance_eval(&block) # here we go. wheeeee! diff --git a/lib/squib/layout_parser.rb b/lib/squib/layout_parser.rb index 1841ad1..d7f6f8b 100644 --- a/lib/squib/layout_parser.rb +++ b/lib/squib/layout_parser.rb @@ -5,16 +5,21 @@ module Squib # @api private class LayoutParser + def initialize(dpi = 300) + @dpi = dpi + end + # Load the layout file(s), if exists # @api private - def self.load_layout(files, initial = {}) + def load_layout(files, initial = {}) layout = initial Squib::logger.info { " using layout(s): #{files}" } Array(files).each do |file| thefile = file thefile = builtin(file) unless File.exists?(file) if File.exists? thefile - yml = layout.merge(YAML.load_file(thefile) || {}) # load_file returns false on empty file + # note: YAML.load_file returns false on empty file + yml = layout.merge(YAML.load_file(thefile) || {}) yml.each do |key, value| layout[key] = recurse_extends(yml, key, {}) end @@ -25,15 +30,17 @@ module Squib return layout end + private + # Determine the file path of the built-in layout file - def self.builtin(file) + def builtin(file) "#{File.dirname(__FILE__)}/layouts/#{file}" end # Process the extends recursively # :nodoc: # @api private - def self.recurse_extends(yml, key, visited) + def recurse_extends(yml, key, visited) assert_not_visited(key, visited) return yml[key] unless has_extends?(yml, key) return yml[key] unless parents_exist?(yml, key) @@ -43,9 +50,9 @@ module Squib parent_keys.each do |parent_key| from_extends = yml[key].merge(recurse_extends(yml, parent_key, visited)) do |key, child_val, parent_val| if child_val.to_s.strip.start_with?('+=') - parent_val + child_val.sub('+=', '').strip.to_f + add_parent_child(parent_val, child_val) elsif child_val.to_s.strip.start_with?('-=') - parent_val - child_val.sub('-=', '').strip.to_f + sub_parent_child(parent_val, child_val) else child_val # child overrides parent when merging, no += end @@ -57,17 +64,29 @@ module Squib return h end + def add_parent_child(parent, child) + parent_pixels = Args::UnitConversion.parse(parent, @dpi).to_f + child_pixels = Args::UnitConversion.parse(child.sub('+=', ''), @dpi).to_f + parent_pixels + child_pixels + end + + def sub_parent_child(parent, child) + parent_pixels = Args::UnitConversion.parse(parent, @dpi).to_f + child_pixels = Args::UnitConversion.parse(child.sub('-=', ''), @dpi).to_f + parent_pixels - child_pixels + end + # Does this layout entry have an extends field? # i.e. is it a base-case or will it need recursion? # :nodoc: # @api private - def self.has_extends?(yml, key) + def has_extends?(yml, key) !!yml[key] && yml[key].key?('extends') end # Checks if we have any absentee parents # @api private - def self.parents_exist?(yml, key) + def parents_exist?(yml, key) exists = true Array(yml[key]['extends']).each do |parent| unless yml.key?(parent) @@ -81,7 +100,7 @@ module Squib # Safeguard against malformed circular extends # :nodoc: # @api private - def self.assert_not_visited(key, visited) + def assert_not_visited(key, visited) if visited.key? key raise "Invalid layout: circular extends with '#{key}'" end diff --git a/spec/data/layouts/extends-units-mixed.yml b/spec/data/layouts/extends-units-mixed.yml new file mode 100644 index 0000000..0cbac7e --- /dev/null +++ b/spec/data/layouts/extends-units-mixed.yml @@ -0,0 +1,8 @@ +parent: + x: 0.5in + y: 1in + +child: + extends: parent + x: += 1in + y: -= 0.5in diff --git a/spec/data/layouts/extends-units.yml b/spec/data/layouts/extends-units.yml new file mode 100644 index 0000000..0cbac7e --- /dev/null +++ b/spec/data/layouts/extends-units.yml @@ -0,0 +1,8 @@ +parent: + x: 0.5in + y: 1in + +child: + extends: parent + x: += 1in + y: -= 0.5in diff --git a/spec/layout_parser_spec.rb b/spec/layout_parser_spec.rb index 0be202b..b57918b 100644 --- a/spec/layout_parser_spec.rb +++ b/spec/layout_parser_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe Squib::LayoutParser do it 'loads a normal layout with no extends' do - layout = Squib::LayoutParser.load_layout(layout_file('no-extends.yml')) + layout = subject.load_layout(layout_file('no-extends.yml')) expect(layout).to eq({ 'frame' => { 'x' => 38, 'valign' => :middle, @@ -15,7 +15,7 @@ describe Squib::LayoutParser do end it 'loads with a single extends' do - layout = Squib::LayoutParser.load_layout(layout_file('single-extends.yml')) + layout = subject.load_layout(layout_file('single-extends.yml')) expect(layout).to eq({ 'frame' => { 'x' => 38, 'y' => 38, @@ -31,7 +31,7 @@ describe Squib::LayoutParser do end it 'applies the extends regardless of order' do - layout = Squib::LayoutParser.load_layout(layout_file('pre-extends.yml')) + layout = subject.load_layout(layout_file('pre-extends.yml')) expect(layout).to eq({ 'frame' => { 'x' => 38, 'y' => 38, @@ -47,7 +47,7 @@ describe Squib::LayoutParser do end it 'applies the single-level extends multiple times' do - layout = Squib::LayoutParser.load_layout(layout_file('single-level-multi-extends.yml')) + layout = subject.load_layout(layout_file('single-level-multi-extends.yml')) expect(layout).to eq({ 'frame' => { 'x' => 38, 'y' => 38, @@ -69,7 +69,7 @@ describe Squib::LayoutParser do end it 'applies multiple extends in a single rule' do - layout = Squib::LayoutParser.load_layout(layout_file('multi-extends-single-entry.yml')) + layout = subject.load_layout(layout_file('multi-extends-single-entry.yml')) expect(layout).to eq({ 'aunt' => { 'a' => 101, 'b' => 102, @@ -93,7 +93,7 @@ describe Squib::LayoutParser do end it 'applies multi-level extends' do - layout = Squib::LayoutParser.load_layout(layout_file('multi-level-extends.yml')) + layout = subject.load_layout(layout_file('multi-level-extends.yml')) expect(layout).to eq({ 'frame' => { 'x' => 38, 'y' => 38, @@ -116,26 +116,26 @@ describe Squib::LayoutParser do it 'fails on a self-circular extends' do file = layout_file('self-circular-extends.yml') - expect { Squib::LayoutParser.load_layout(file) } + expect { subject.load_layout(file)} .to raise_error(RuntimeError, 'Invalid layout: circular extends with \'a\'') end it 'fails on a easy-circular extends' do file = layout_file('easy-circular-extends.yml') - expect { Squib::LayoutParser.load_layout(file) } + expect { subject.load_layout(file)} .to raise_error(RuntimeError, 'Invalid layout: circular extends with \'a\'') end it 'hard on a easy-circular extends' do file = layout_file('hard-circular-extends.yml') - expect { Squib::LayoutParser.load_layout(file) } + expect { subject.load_layout(file)} .to raise_error(RuntimeError, 'Invalid layout: circular extends with \'a\'') end it 'redefines keys on multiple layouts' do a = layout_file('multifile-a.yml') b = layout_file('multifile-b.yml') - layout = Squib::LayoutParser.load_layout([a, b]) + layout = subject.load_layout([a, b]) expect(layout).to eq({ 'title' => { 'x' => 300 }, 'subtitle' => { 'x' => 200 }, @@ -146,7 +146,7 @@ describe Squib::LayoutParser do it 'evaluates extends on each file first' do a = layout_file('multifile-extends-a.yml') b = layout_file('multifile-extends-b.yml') - layout = Squib::LayoutParser.load_layout([a, b]) + layout = subject.load_layout([a, b]) expect(layout).to eq({ 'grandparent' => { 'x' => 100 }, 'parent_a' => { 'x' => 110, 'extends' => 'grandparent' }, @@ -157,12 +157,12 @@ describe Squib::LayoutParser do end it 'loads nothing on an empty layout file' do - layout = Squib::LayoutParser.load_layout(layout_file('empty.yml')) + layout = subject.load_layout(layout_file('empty.yml')) expect(layout).to eq({}) end it 'handles extends on a rule with no args' do - layout = Squib::LayoutParser.load_layout(layout_file('empty-rule.yml')) + layout = subject.load_layout(layout_file('empty-rule.yml')) expect(layout).to eq({ 'empty' => nil }) @@ -170,14 +170,14 @@ describe Squib::LayoutParser do it 'logs an error when a file is not found' do expect(Squib.logger).to receive(:error).once - Squib::LayoutParser.load_layout('yeti') + subject.load_layout('yeti') end it 'freaks out if you extend something doesn\'t exist' do expect(Squib.logger) .to receive(:error) .with("Processing layout: 'verbal' attempts to extend a missing 'kaisersoze'") - layout = Squib::LayoutParser.load_layout(layout_file('extends-nonexists.yml')) + layout = subject.load_layout(layout_file('extends-nonexists.yml')) expect(layout).to eq({ 'verbal' => { 'font_size' => 25, @@ -186,11 +186,36 @@ describe Squib::LayoutParser do }) end + it 'does unit conversion when extending' do + layout = subject.load_layout(layout_file('extends-units.yml')) + expect(layout).to eq({ + 'parent' => { 'x' => '0.5in', 'y' => '1in'}, + 'child' => { 'x' => 450.0, 'y' => 150.0, 'extends' => 'parent' }, + }) + end + + it 'does unit conversion on non-300 dpis' do + parser = Squib::LayoutParser.new(100) + layout = parser.load_layout(layout_file('extends-units.yml')) + expect(layout).to eq({ + 'parent' => { 'x' => '0.5in', 'y' => '1in'}, + 'child' => { 'x' => 150.0, 'y' => 50.0, 'extends' => 'parent' }, + }) + end + + it 'does mixed unit conversion when extending' do + layout = subject.load_layout(layout_file('extends-units-mixed.yml')) + expect(layout).to eq({ + 'parent' => { 'x' => '0.5in', 'y' => '1in'}, + 'child' => { 'x' => 450.0, 'y' => 150.0, 'extends' => 'parent' }, + }) + end + it 'loads progressively on multiple calls' do a = layout_file('multifile-a.yml') b = layout_file('multifile-b.yml') - layout = Squib::LayoutParser.load_layout(a) - layout = Squib::LayoutParser.load_layout(b, layout) + layout = subject.load_layout(a) + layout = subject.load_layout(b, layout) expect(layout).to eq({ 'title' => { 'x' => 300 }, 'subtitle' => { 'x' => 200 },