layouts: support unit conversion in extends
fixes bug #173 Also: LayoutParser is now a proper class, as God intended.dev
parent
73bf116d97
commit
4ec1a33cfd
|
|
@ -9,6 +9,7 @@ Features:
|
||||||
Bugs:
|
Bugs:
|
||||||
* Fresh installs of Squib were broken due to two hidden dependencies, gio2 and gobject-introspection. (#172)
|
* 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.
|
* 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
|
## v0.10.0 / 2016-05-06
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ module Squib
|
||||||
|
|
||||||
# DSL method. See http://squib.readthedocs.io
|
# DSL method. See http://squib.readthedocs.io
|
||||||
def use_layout(file: 'layout.yml')
|
def use_layout(file: 'layout.yml')
|
||||||
@layout = LayoutParser.load_layout(file, @layout)
|
@layout = LayoutParser.new(@dpi).load_layout(file, @layout)
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ module Squib
|
||||||
@width = Args::UnitConversion.parse width, dpi
|
@width = Args::UnitConversion.parse width, dpi
|
||||||
@height = Args::UnitConversion.parse height, dpi
|
@height = Args::UnitConversion.parse height, dpi
|
||||||
cards.times{ |i| @cards << Squib::Card.new(self, @width, @height, i) }
|
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!
|
enable_groups_from_env!
|
||||||
if block_given?
|
if block_given?
|
||||||
instance_eval(&block) # here we go. wheeeee!
|
instance_eval(&block) # here we go. wheeeee!
|
||||||
|
|
|
||||||
|
|
@ -5,16 +5,21 @@ module Squib
|
||||||
# @api private
|
# @api private
|
||||||
class LayoutParser
|
class LayoutParser
|
||||||
|
|
||||||
|
def initialize(dpi = 300)
|
||||||
|
@dpi = dpi
|
||||||
|
end
|
||||||
|
|
||||||
# Load the layout file(s), if exists
|
# Load the layout file(s), if exists
|
||||||
# @api private
|
# @api private
|
||||||
def self.load_layout(files, initial = {})
|
def load_layout(files, initial = {})
|
||||||
layout = initial
|
layout = initial
|
||||||
Squib::logger.info { " using layout(s): #{files}" }
|
Squib::logger.info { " using layout(s): #{files}" }
|
||||||
Array(files).each do |file|
|
Array(files).each do |file|
|
||||||
thefile = file
|
thefile = file
|
||||||
thefile = builtin(file) unless File.exists?(file)
|
thefile = builtin(file) unless File.exists?(file)
|
||||||
if File.exists? thefile
|
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|
|
yml.each do |key, value|
|
||||||
layout[key] = recurse_extends(yml, key, {})
|
layout[key] = recurse_extends(yml, key, {})
|
||||||
end
|
end
|
||||||
|
|
@ -25,15 +30,17 @@ module Squib
|
||||||
return layout
|
return layout
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
# Determine the file path of the built-in layout file
|
# Determine the file path of the built-in layout file
|
||||||
def self.builtin(file)
|
def builtin(file)
|
||||||
"#{File.dirname(__FILE__)}/layouts/#{file}"
|
"#{File.dirname(__FILE__)}/layouts/#{file}"
|
||||||
end
|
end
|
||||||
|
|
||||||
# Process the extends recursively
|
# Process the extends recursively
|
||||||
# :nodoc:
|
# :nodoc:
|
||||||
# @api private
|
# @api private
|
||||||
def self.recurse_extends(yml, key, visited)
|
def recurse_extends(yml, key, visited)
|
||||||
assert_not_visited(key, visited)
|
assert_not_visited(key, visited)
|
||||||
return yml[key] unless has_extends?(yml, key)
|
return yml[key] unless has_extends?(yml, key)
|
||||||
return yml[key] unless parents_exist?(yml, key)
|
return yml[key] unless parents_exist?(yml, key)
|
||||||
|
|
@ -43,9 +50,9 @@ module Squib
|
||||||
parent_keys.each do |parent_key|
|
parent_keys.each do |parent_key|
|
||||||
from_extends = yml[key].merge(recurse_extends(yml, parent_key, visited)) do |key, child_val, parent_val|
|
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?('+=')
|
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?('-=')
|
elsif child_val.to_s.strip.start_with?('-=')
|
||||||
parent_val - child_val.sub('-=', '').strip.to_f
|
sub_parent_child(parent_val, child_val)
|
||||||
else
|
else
|
||||||
child_val # child overrides parent when merging, no +=
|
child_val # child overrides parent when merging, no +=
|
||||||
end
|
end
|
||||||
|
|
@ -57,17 +64,29 @@ module Squib
|
||||||
return h
|
return h
|
||||||
end
|
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?
|
# Does this layout entry have an extends field?
|
||||||
# i.e. is it a base-case or will it need recursion?
|
# i.e. is it a base-case or will it need recursion?
|
||||||
# :nodoc:
|
# :nodoc:
|
||||||
# @api private
|
# @api private
|
||||||
def self.has_extends?(yml, key)
|
def has_extends?(yml, key)
|
||||||
!!yml[key] && yml[key].key?('extends')
|
!!yml[key] && yml[key].key?('extends')
|
||||||
end
|
end
|
||||||
|
|
||||||
# Checks if we have any absentee parents
|
# Checks if we have any absentee parents
|
||||||
# @api private
|
# @api private
|
||||||
def self.parents_exist?(yml, key)
|
def parents_exist?(yml, key)
|
||||||
exists = true
|
exists = true
|
||||||
Array(yml[key]['extends']).each do |parent|
|
Array(yml[key]['extends']).each do |parent|
|
||||||
unless yml.key?(parent)
|
unless yml.key?(parent)
|
||||||
|
|
@ -81,7 +100,7 @@ module Squib
|
||||||
# Safeguard against malformed circular extends
|
# Safeguard against malformed circular extends
|
||||||
# :nodoc:
|
# :nodoc:
|
||||||
# @api private
|
# @api private
|
||||||
def self.assert_not_visited(key, visited)
|
def assert_not_visited(key, visited)
|
||||||
if visited.key? key
|
if visited.key? key
|
||||||
raise "Invalid layout: circular extends with '#{key}'"
|
raise "Invalid layout: circular extends with '#{key}'"
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
parent:
|
||||||
|
x: 0.5in
|
||||||
|
y: 1in
|
||||||
|
|
||||||
|
child:
|
||||||
|
extends: parent
|
||||||
|
x: += 1in
|
||||||
|
y: -= 0.5in
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
parent:
|
||||||
|
x: 0.5in
|
||||||
|
y: 1in
|
||||||
|
|
||||||
|
child:
|
||||||
|
extends: parent
|
||||||
|
x: += 1in
|
||||||
|
y: -= 0.5in
|
||||||
|
|
@ -3,7 +3,7 @@ require 'spec_helper'
|
||||||
describe Squib::LayoutParser do
|
describe Squib::LayoutParser do
|
||||||
|
|
||||||
it 'loads a normal layout with no extends' 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' => {
|
expect(layout).to eq({ 'frame' => {
|
||||||
'x' => 38,
|
'x' => 38,
|
||||||
'valign' => :middle,
|
'valign' => :middle,
|
||||||
|
|
@ -15,7 +15,7 @@ describe Squib::LayoutParser do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'loads with a single extends' do
|
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' => {
|
expect(layout).to eq({ 'frame' => {
|
||||||
'x' => 38,
|
'x' => 38,
|
||||||
'y' => 38,
|
'y' => 38,
|
||||||
|
|
@ -31,7 +31,7 @@ describe Squib::LayoutParser do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'applies the extends regardless of order' do
|
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' => {
|
expect(layout).to eq({ 'frame' => {
|
||||||
'x' => 38,
|
'x' => 38,
|
||||||
'y' => 38,
|
'y' => 38,
|
||||||
|
|
@ -47,7 +47,7 @@ describe Squib::LayoutParser do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'applies the single-level extends multiple times' do
|
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' => {
|
expect(layout).to eq({ 'frame' => {
|
||||||
'x' => 38,
|
'x' => 38,
|
||||||
'y' => 38,
|
'y' => 38,
|
||||||
|
|
@ -69,7 +69,7 @@ describe Squib::LayoutParser do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'applies multiple extends in a single rule' do
|
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' => {
|
expect(layout).to eq({ 'aunt' => {
|
||||||
'a' => 101,
|
'a' => 101,
|
||||||
'b' => 102,
|
'b' => 102,
|
||||||
|
|
@ -93,7 +93,7 @@ describe Squib::LayoutParser do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'applies multi-level extends' do
|
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' => {
|
expect(layout).to eq({ 'frame' => {
|
||||||
'x' => 38,
|
'x' => 38,
|
||||||
'y' => 38,
|
'y' => 38,
|
||||||
|
|
@ -116,26 +116,26 @@ describe Squib::LayoutParser do
|
||||||
|
|
||||||
it 'fails on a self-circular extends' do
|
it 'fails on a self-circular extends' do
|
||||||
file = layout_file('self-circular-extends.yml')
|
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\'')
|
.to raise_error(RuntimeError, 'Invalid layout: circular extends with \'a\'')
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'fails on a easy-circular extends' do
|
it 'fails on a easy-circular extends' do
|
||||||
file = layout_file('easy-circular-extends.yml')
|
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\'')
|
.to raise_error(RuntimeError, 'Invalid layout: circular extends with \'a\'')
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'hard on a easy-circular extends' do
|
it 'hard on a easy-circular extends' do
|
||||||
file = layout_file('hard-circular-extends.yml')
|
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\'')
|
.to raise_error(RuntimeError, 'Invalid layout: circular extends with \'a\'')
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'redefines keys on multiple layouts' do
|
it 'redefines keys on multiple layouts' do
|
||||||
a = layout_file('multifile-a.yml')
|
a = layout_file('multifile-a.yml')
|
||||||
b = layout_file('multifile-b.yml')
|
b = layout_file('multifile-b.yml')
|
||||||
layout = Squib::LayoutParser.load_layout([a, b])
|
layout = subject.load_layout([a, b])
|
||||||
expect(layout).to eq({
|
expect(layout).to eq({
|
||||||
'title' => { 'x' => 300 },
|
'title' => { 'x' => 300 },
|
||||||
'subtitle' => { 'x' => 200 },
|
'subtitle' => { 'x' => 200 },
|
||||||
|
|
@ -146,7 +146,7 @@ describe Squib::LayoutParser do
|
||||||
it 'evaluates extends on each file first' do
|
it 'evaluates extends on each file first' do
|
||||||
a = layout_file('multifile-extends-a.yml')
|
a = layout_file('multifile-extends-a.yml')
|
||||||
b = layout_file('multifile-extends-b.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({
|
expect(layout).to eq({
|
||||||
'grandparent' => { 'x' => 100 },
|
'grandparent' => { 'x' => 100 },
|
||||||
'parent_a' => { 'x' => 110, 'extends' => 'grandparent' },
|
'parent_a' => { 'x' => 110, 'extends' => 'grandparent' },
|
||||||
|
|
@ -157,12 +157,12 @@ describe Squib::LayoutParser do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'loads nothing on an empty layout file' do
|
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({})
|
expect(layout).to eq({})
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'handles extends on a rule with no args' do
|
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({
|
expect(layout).to eq({
|
||||||
'empty' => nil
|
'empty' => nil
|
||||||
})
|
})
|
||||||
|
|
@ -170,14 +170,14 @@ describe Squib::LayoutParser do
|
||||||
|
|
||||||
it 'logs an error when a file is not found' do
|
it 'logs an error when a file is not found' do
|
||||||
expect(Squib.logger).to receive(:error).once
|
expect(Squib.logger).to receive(:error).once
|
||||||
Squib::LayoutParser.load_layout('yeti')
|
subject.load_layout('yeti')
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'freaks out if you extend something doesn\'t exist' do
|
it 'freaks out if you extend something doesn\'t exist' do
|
||||||
expect(Squib.logger)
|
expect(Squib.logger)
|
||||||
.to receive(:error)
|
.to receive(:error)
|
||||||
.with("Processing layout: 'verbal' attempts to extend a missing 'kaisersoze'")
|
.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({
|
expect(layout).to eq({
|
||||||
'verbal' => {
|
'verbal' => {
|
||||||
'font_size' => 25,
|
'font_size' => 25,
|
||||||
|
|
@ -186,11 +186,36 @@ describe Squib::LayoutParser do
|
||||||
})
|
})
|
||||||
end
|
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
|
it 'loads progressively on multiple calls' do
|
||||||
a = layout_file('multifile-a.yml')
|
a = layout_file('multifile-a.yml')
|
||||||
b = layout_file('multifile-b.yml')
|
b = layout_file('multifile-b.yml')
|
||||||
layout = Squib::LayoutParser.load_layout(a)
|
layout = subject.load_layout(a)
|
||||||
layout = Squib::LayoutParser.load_layout(b, layout)
|
layout = subject.load_layout(b, layout)
|
||||||
expect(layout).to eq({
|
expect(layout).to eq({
|
||||||
'title' => { 'x' => 300 },
|
'title' => { 'x' => 300 },
|
||||||
'subtitle' => { 'x' => 200 },
|
'subtitle' => { 'x' => 200 },
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue