From f2a2ab05739b595733fcfa2d6b645ab0386ae156 Mon Sep 17 00:00:00 2001 From: Andy Meneely Date: Sun, 21 Sep 2014 21:56:18 -0400 Subject: [PATCH] Bringing back the extends feature Adding in my own merge key implementation that allows you to bring in other keys and modfiy the data. Makes moving boxes around easier. A quasi-revert of 00e8b591444566bce720f08b971de55d9dd3f650 ...but with something useful! --- README.md | 14 +++++- lib/squib/deck.rb | 59 ++++++++++++++++++++++-- samples/custom-layout.yml | 38 +++++++++++---- samples/use_layout.rb | 9 +++- spec/data/easy-circular-extends.yml | 6 +++ spec/data/hard-circular-extends.yml | 9 ++++ spec/data/multi-extends-single-entry.yml | 9 ++-- spec/data/multi-level-extends.yml | 10 ++-- spec/data/pre-extends.yml | 7 +++ spec/data/self-circular-extends.yml | 3 ++ spec/data/single-extends.yml | 4 +- spec/data/single-level-multi-extends.yml | 6 +-- spec/deck_spec.rb | 41 ++++++++++++++++ 13 files changed, 186 insertions(+), 29 deletions(-) create mode 100644 spec/data/easy-circular-extends.yml create mode 100644 spec/data/hard-circular-extends.yml create mode 100644 spec/data/pre-extends.yml create mode 100644 spec/data/self-circular-extends.yml diff --git a/README.md b/README.md index 259bf24..850956f 100644 --- a/README.md +++ b/README.md @@ -173,7 +173,7 @@ Layouts will override Squib's defaults, but are overriden by anything specified Since Layouts are in Yaml, we have the full power of that data format. One particular feature you should look into are ["merge keys"](http://www.yaml.org/YAML_for_ruby.html#merge_key). With merge keys, you can define base styles in one entry, then include those keys elsewhere. For example: -```sh +```yaml icon: &icon width: 50 height: 50 @@ -183,6 +183,18 @@ icon_left # The layout for icon_left will have the width/height from icon! ``` +Also!! Squib provides a more feature-rich way of merging: the `extends` key in layouts. When defining an extends key, we can merge in another key and modify data coming in if we want to. This allows us to do things like set an inner object that changes its location based on its parent. + +```yaml +yin: + x: 100 + y: 100 + radius: 100 +yang: + extends: yin + x: += 50 +``` + See the `use_layout` sample found [here](https://github.com/andymeneely/squib/tree/master/samples/) {include:file:samples/use_layout.rb} diff --git a/lib/squib/deck.rb b/lib/squib/deck.rb index c16d7c8..3407d7e 100644 --- a/lib/squib/deck.rb +++ b/lib/squib/deck.rb @@ -49,8 +49,7 @@ module Squib # @param height: [Integer] the height of each card in pixels # @param cards: [Integer] the number of cards in the deck # @param dpi: [Integer] the pixels per inch when rendering out to PDF or calculating using inches. - # @param config: [String] the Yaml file used for global settings of this deck - # @param layout: [String] the Yaml file used for layouts. + # @param config: [String] the file used for global settings of this deck # @param block [Block] the main body of the script. # @api public def initialize(width: 825, height: 1125, cards: 1, dpi: 300, config: 'config.yml', layout: nil, &block) @@ -63,7 +62,7 @@ module Squib @progress_bar = Squib::Progress.new(false) cards.times{ @cards << Squib::Card.new(self, width, height) } load_config(config) - @layout = YAML.load_file(layout) unless layout.nil? + load_layout(layout) if block_given? instance_eval(&block) end @@ -101,6 +100,60 @@ module Squib end end + # Load the layout configuration file, if exists + # @api private + def load_layout(file) + return if file.nil? + @layout = {} + yml = YAML.load_file(file) + yml.each do |key, value| + @layout[key] = recurse_extends(yml, key, {}) + end + end + + # Process the extends recursively + # :nodoc: + # @api private + def recurse_extends(yml, key, visited ) + assert_not_visited(key, visited) + return yml[key] unless has_extends?(yml, key) + visited[key] = key + parent_keys = [yml[key]['extends']].flatten + h = {} + 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 + elsif child_val.to_s.strip.start_with?('-=') + parent_val - child_val.sub("-=",'').strip.to_f + else + child_val #child overrides parent when merging, no += + end + end + h = h.merge(from_extends) do |key, older_sibling, younger_sibling| + younger_sibling #when two siblings have the same entry, the "younger" (lower one) overrides + end + end + return h + 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 has_extends?(yml, key) + yml[key].key?('extends') + end + + # Safeguard against malformed circular extends + # :nodoc: + # @api private + def assert_not_visited(key, visited) + if visited.key? key + raise "Invalid layout: circular extends with '#{key}'" + end + end + ################## ### PUBLIC API ### ################## diff --git a/samples/custom-layout.yml b/samples/custom-layout.yml index 2042952..6145920 100644 --- a/samples/custom-layout.yml +++ b/samples/custom-layout.yml @@ -18,22 +18,42 @@ subtitle: height: 60 align: !ruby/symbol center valign: !ruby/symbol middle - -# Yaml has this beautfiul feature for us: merge keys -# http://www.yaml.org/YAML_for_ruby.html#merge_key -# Define an alias with &foo, then use <<: *foo to include it -# Everything gets merged, with later merges overriding -icon: &icon +icon: width: 125 height: 125 y: 250 icon_left: - <<: *icon + extends: icon x: 150 icon_middle: - <<: *icon + extends: icon x: 350 y: 400 #overrides the y inherited from icon icon_right: - <<: *icon + extends: icon x: 550 + +# Squib also supports its own merging-and-modify feature +# Called "extends" +# Any layout can extend another layout, so long as it's not a circle +# Order doesn't matter since it's done after YAML procesing +# And, if the entry overrides +bonus: #becomes our bonus rectangle + x: 250 + y: 600 + width: 300 + height: 200 + radius: 32 +bonus_inner: + extends: bonus + x: += 10 # i.e. 260 + y: += 10 # i.e. 610 + width: -= 20 # i.e. 180 + height: -= 20 # i.e. 180 + radius: -= 8 +bonus_text: + extends: bonus_inner + x: +=10 + y: +=10 + width: -= 20 + height: -= 20 diff --git a/samples/use_layout.rb b/samples/use_layout.rb index 4775013..b00554b 100644 --- a/samples/use_layout.rb +++ b/samples/use_layout.rb @@ -13,17 +13,22 @@ Squib::Deck.new(layout: 'custom-layout.yml') do # Lots of commands have the :layout option text str: 'The Title', layout: :title - # Layouts also support an "extends" attribute to reuse settings + # Layouts also support YAML merge keys toreuse settings svg file: 'spanner.svg', layout: :icon_left png file: 'shiny-purse.png', layout: :icon_middle svg file: 'spanner.svg', layout: :icon_right + # Squib has its own, richer merge key: "extends" + rect fill_color: :black, layout: :bonus + rect fill_color: :white, layout: :bonus_inner + text str: 'Extends!', layout: :bonus_text + # Strings can also be used to specify a layout (e.g. from a data file) text str: 'subtitle', layout: 'subtitle' # For debugging purposes, you can always print out the loaded layout #require 'pp' #pp @layout - + save_png prefix: 'layout_' end \ No newline at end of file diff --git a/spec/data/easy-circular-extends.yml b/spec/data/easy-circular-extends.yml new file mode 100644 index 0000000..ccfc559 --- /dev/null +++ b/spec/data/easy-circular-extends.yml @@ -0,0 +1,6 @@ +a: + extends: b + x: 50 +b: + extends: a + x: 150 diff --git a/spec/data/hard-circular-extends.yml b/spec/data/hard-circular-extends.yml new file mode 100644 index 0000000..6ef16fc --- /dev/null +++ b/spec/data/hard-circular-extends.yml @@ -0,0 +1,9 @@ +a: + extends: c + x: 50 +b: + extends: a + x: 150 +c: + extends: b + y: 250 \ No newline at end of file diff --git a/spec/data/multi-extends-single-entry.yml b/spec/data/multi-extends-single-entry.yml index 0fb15cb..805c98b 100644 --- a/spec/data/multi-extends-single-entry.yml +++ b/spec/data/multi-extends-single-entry.yml @@ -1,13 +1,14 @@ -aunt: &aunt +aunt: a: 101 b: 102 c: 103 -uncle: &uncle +uncle: x: 104 y: 105 b: 106 child: - <<: *uncle - <<: *aunt + extends: + - uncle + - aunt a: 107 x: 108 diff --git a/spec/data/multi-level-extends.yml b/spec/data/multi-level-extends.yml index a37b5b5..9f8867e 100644 --- a/spec/data/multi-level-extends.yml +++ b/spec/data/multi-level-extends.yml @@ -1,10 +1,10 @@ -frame: &frame +frame: x: 38 y: 38 -title: &title - <<: *frame +title: + extends: frame y: 50 width: 100 -subtitle: - <<: *title +subtitle: + extends: title y: 150 \ No newline at end of file diff --git a/spec/data/pre-extends.yml b/spec/data/pre-extends.yml new file mode 100644 index 0000000..753cd5a --- /dev/null +++ b/spec/data/pre-extends.yml @@ -0,0 +1,7 @@ +title: + extends: frame + y: 50 + width: 100 +frame: + x: 38 + y: 38 diff --git a/spec/data/self-circular-extends.yml b/spec/data/self-circular-extends.yml new file mode 100644 index 0000000..f988d3b --- /dev/null +++ b/spec/data/self-circular-extends.yml @@ -0,0 +1,3 @@ +a: + extends: a + x: 50 diff --git a/spec/data/single-extends.yml b/spec/data/single-extends.yml index 1019965..2788822 100644 --- a/spec/data/single-extends.yml +++ b/spec/data/single-extends.yml @@ -1,7 +1,7 @@ -frame: &frame +frame: x: 38 y: 38 title: - <<: *frame + extends: frame y: 50 width: 100 diff --git a/spec/data/single-level-multi-extends.yml b/spec/data/single-level-multi-extends.yml index 06ef128..6cbd349 100644 --- a/spec/data/single-level-multi-extends.yml +++ b/spec/data/single-level-multi-extends.yml @@ -1,12 +1,12 @@ -frame: &frame +frame: x: 38 y: 38 title: - <<: *frame + extends: frame y: 50 width: 100 title2: - <<: *frame + extends: frame x: 75 y: 150 width: 150 \ No newline at end of file diff --git a/spec/deck_spec.rb b/spec/deck_spec.rb index 9f0f189..43d7719 100644 --- a/spec/deck_spec.rb +++ b/spec/deck_spec.rb @@ -68,6 +68,24 @@ describe Squib::Deck do 'y' => 38, }, 'title' => { + 'extends' => 'frame', + 'x' => 38, + 'y' => 50, + 'width' => 100, + } + } + ) + end + + it "applies the extends regardless of order" do + d = Squib::Deck.new(layout: test_file('pre-extends.yml')) + expect(d.layout).to \ + eq({'frame' => { + 'x' => 38, + 'y' => 38, + }, + 'title' => { + 'extends' => 'frame', 'x' => 38, 'y' => 50, 'width' => 100, @@ -84,11 +102,13 @@ describe Squib::Deck do 'y' => 38, }, 'title' => { + 'extends' => 'frame', 'x' => 38, 'y' => 50, 'width' => 100, }, 'title2' => { + 'extends' => 'frame', 'x' => 75, 'y' => 150, 'width' => 150, @@ -111,6 +131,7 @@ describe Squib::Deck do 'b' => 106, }, 'child' => { + 'extends' => ['uncle','aunt'], 'a' => 107, # my own 'b' => 102, # from the younger aunt 'c' => 103, # from aunt @@ -129,11 +150,13 @@ describe Squib::Deck do 'y' => 38, }, 'title' => { + 'extends' => 'frame', 'x' => 38, 'y' => 50, 'width' => 100, }, 'subtitle' => { + 'extends' => 'title', 'x' => 38, 'y' => 150, 'width' => 100, @@ -142,6 +165,24 @@ describe Squib::Deck do ) end + it "fails on a self-circular extends" do + file = test_file('self-circular-extends.yml') + expect { Squib::Deck.new(layout: file) }.to \ + raise_error(RuntimeError, "Invalid layout: circular extends with 'a'") + end + + it "fails on a easy-circular extends" do + file = test_file('easy-circular-extends.yml') + expect { Squib::Deck.new(layout: file) }.to \ + raise_error(RuntimeError, "Invalid layout: circular extends with 'a'") + end + + it "hard on a easy-circular extends" do + file = test_file('hard-circular-extends.yml') + expect { Squib::Deck.new(layout: file) }.to \ + raise_error(RuntimeError, "Invalid layout: circular extends with 'a'") + end + end end