Browse Source

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 00e8b59144
...but with something useful!
dev
Andy Meneely 11 years ago
parent
commit
f2a2ab0573
  1. 14
      README.md
  2. 59
      lib/squib/deck.rb
  3. 38
      samples/custom-layout.yml
  4. 9
      samples/use_layout.rb
  5. 6
      spec/data/easy-circular-extends.yml
  6. 9
      spec/data/hard-circular-extends.yml
  7. 9
      spec/data/multi-extends-single-entry.yml
  8. 10
      spec/data/multi-level-extends.yml
  9. 7
      spec/data/pre-extends.yml
  10. 3
      spec/data/self-circular-extends.yml
  11. 4
      spec/data/single-extends.yml
  12. 6
      spec/data/single-level-multi-extends.yml
  13. 41
      spec/deck_spec.rb

14
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: 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 icon: &icon
width: 50 width: 50
height: 50 height: 50
@ -183,6 +183,18 @@ icon_left
# The layout for icon_left will have the width/height from icon! # 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/) See the `use_layout` sample found [here](https://github.com/andymeneely/squib/tree/master/samples/)
{include:file:samples/use_layout.rb} {include:file:samples/use_layout.rb}

59
lib/squib/deck.rb

@ -49,8 +49,7 @@ module Squib
# @param height: [Integer] the height of each card in pixels # @param height: [Integer] the height of each card in pixels
# @param cards: [Integer] the number of cards in the deck # @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 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 config: [String] the file used for global settings of this deck
# @param layout: [String] the Yaml file used for layouts.
# @param block [Block] the main body of the script. # @param block [Block] the main body of the script.
# @api public # @api public
def initialize(width: 825, height: 1125, cards: 1, dpi: 300, config: 'config.yml', layout: nil, &block) 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) @progress_bar = Squib::Progress.new(false)
cards.times{ @cards << Squib::Card.new(self, width, height) } cards.times{ @cards << Squib::Card.new(self, width, height) }
load_config(config) load_config(config)
@layout = YAML.load_file(layout) unless layout.nil? load_layout(layout)
if block_given? if block_given?
instance_eval(&block) instance_eval(&block)
end end
@ -101,6 +100,60 @@ module Squib
end end
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 ### ### PUBLIC API ###
################## ##################

38
samples/custom-layout.yml

@ -18,22 +18,42 @@ subtitle:
height: 60 height: 60
align: !ruby/symbol center align: !ruby/symbol center
valign: !ruby/symbol middle valign: !ruby/symbol middle
icon:
# 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
width: 125 width: 125
height: 125 height: 125
y: 250 y: 250
icon_left: icon_left:
<<: *icon extends: icon
x: 150 x: 150
icon_middle: icon_middle:
<<: *icon extends: icon
x: 350 x: 350
y: 400 #overrides the y inherited from icon y: 400 #overrides the y inherited from icon
icon_right: icon_right:
<<: *icon extends: icon
x: 550 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

9
samples/use_layout.rb

@ -13,17 +13,22 @@ Squib::Deck.new(layout: 'custom-layout.yml') do
# Lots of commands have the :layout option # Lots of commands have the :layout option
text str: 'The Title', layout: :title 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 svg file: 'spanner.svg', layout: :icon_left
png file: 'shiny-purse.png', layout: :icon_middle png file: 'shiny-purse.png', layout: :icon_middle
svg file: 'spanner.svg', layout: :icon_right 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) # Strings can also be used to specify a layout (e.g. from a data file)
text str: 'subtitle', layout: 'subtitle' text str: 'subtitle', layout: 'subtitle'
# For debugging purposes, you can always print out the loaded layout # For debugging purposes, you can always print out the loaded layout
#require 'pp' #require 'pp'
#pp @layout #pp @layout
save_png prefix: 'layout_' save_png prefix: 'layout_'
end end

6
spec/data/easy-circular-extends.yml

@ -0,0 +1,6 @@
a:
extends: b
x: 50
b:
extends: a
x: 150

9
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

9
spec/data/multi-extends-single-entry.yml

@ -1,13 +1,14 @@
aunt: &aunt aunt:
a: 101 a: 101
b: 102 b: 102
c: 103 c: 103
uncle: &uncle uncle:
x: 104 x: 104
y: 105 y: 105
b: 106 b: 106
child: child:
<<: *uncle extends:
<<: *aunt - uncle
- aunt
a: 107 a: 107
x: 108 x: 108

10
spec/data/multi-level-extends.yml

@ -1,10 +1,10 @@
frame: &frame frame:
x: 38 x: 38
y: 38 y: 38
title: &title title:
<<: *frame extends: frame
y: 50 y: 50
width: 100 width: 100
subtitle: subtitle:
<<: *title extends: title
y: 150 y: 150

7
spec/data/pre-extends.yml

@ -0,0 +1,7 @@
title:
extends: frame
y: 50
width: 100
frame:
x: 38
y: 38

3
spec/data/self-circular-extends.yml

@ -0,0 +1,3 @@
a:
extends: a
x: 50

4
spec/data/single-extends.yml

@ -1,7 +1,7 @@
frame: &frame frame:
x: 38 x: 38
y: 38 y: 38
title: title:
<<: *frame extends: frame
y: 50 y: 50
width: 100 width: 100

6
spec/data/single-level-multi-extends.yml

@ -1,12 +1,12 @@
frame: &frame frame:
x: 38 x: 38
y: 38 y: 38
title: title:
<<: *frame extends: frame
y: 50 y: 50
width: 100 width: 100
title2: title2:
<<: *frame extends: frame
x: 75 x: 75
y: 150 y: 150
width: 150 width: 150

41
spec/deck_spec.rb

@ -68,6 +68,24 @@ describe Squib::Deck do
'y' => 38, 'y' => 38,
}, },
'title' => { '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, 'x' => 38,
'y' => 50, 'y' => 50,
'width' => 100, 'width' => 100,
@ -84,11 +102,13 @@ describe Squib::Deck do
'y' => 38, 'y' => 38,
}, },
'title' => { 'title' => {
'extends' => 'frame',
'x' => 38, 'x' => 38,
'y' => 50, 'y' => 50,
'width' => 100, 'width' => 100,
}, },
'title2' => { 'title2' => {
'extends' => 'frame',
'x' => 75, 'x' => 75,
'y' => 150, 'y' => 150,
'width' => 150, 'width' => 150,
@ -111,6 +131,7 @@ describe Squib::Deck do
'b' => 106, 'b' => 106,
}, },
'child' => { 'child' => {
'extends' => ['uncle','aunt'],
'a' => 107, # my own 'a' => 107, # my own
'b' => 102, # from the younger aunt 'b' => 102, # from the younger aunt
'c' => 103, # from aunt 'c' => 103, # from aunt
@ -129,11 +150,13 @@ describe Squib::Deck do
'y' => 38, 'y' => 38,
}, },
'title' => { 'title' => {
'extends' => 'frame',
'x' => 38, 'x' => 38,
'y' => 50, 'y' => 50,
'width' => 100, 'width' => 100,
}, },
'subtitle' => { 'subtitle' => {
'extends' => 'title',
'x' => 38, 'x' => 38,
'y' => 150, 'y' => 150,
'width' => 100, 'width' => 100,
@ -142,6 +165,24 @@ describe Squib::Deck do
) )
end 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
end end

Loading…
Cancel
Save