diff --git a/README.md b/README.md
index 5b787d8..870e92b 100644
--- a/README.md
+++ b/README.md
@@ -241,6 +241,17 @@ text(str: 'Gain 1 :health:') do |embed|
end
```
+###Markup
+
+If you want to do specialized formatting within a given string, Squib has lots of options. By setting `markup: true`, you enable tons of text processing. This includes:
+
+* Pango Markup. This is an HTML-like formatting language that specifies formatting inside your string. Pango Markup essentially supports any formatting option, but on a letter-by-letter basis. Such as: font options, letter spacing, gravity, color, etc. See the [Pango docs](https://developer.gnome.org/pango/stable/PangoMarkupFormat.html) for details.
+* Quotes are converted to their curly counterparts where appropriate (i.e. “smart quotes” instead of "straight quotes").
+* Apostraphes are converted to curly as well.
+* LaTeX-style quotes are explicitly converted (``like this'')
+* Em-dash and en-dash are converted with triple and double-dashes respectively (-- is an en-dash, and --- becomes an em-dash.)
+* Ellipses can be specified with .... Note that this is entirely different from the `ellipsize` option (which determines what to do with overflowing text).
+
### Text Samples
Examples of all of the above are crammed into the `text_options.rb` sample [found here](https://github.com/andymeneely/squib/tree/master/samples/text_options.rb).
diff --git a/lib/squib/api/text.rb b/lib/squib/api/text.rb
index 63fade0..925db77 100644
--- a/lib/squib/api/text.rb
+++ b/lib/squib/api/text.rb
@@ -1,5 +1,4 @@
require 'squib/api/text_embed'
-require 'squib/args/smart_quotes'
module Squib
class Deck
@@ -26,7 +25,7 @@ module Squib
# @option opts x [Integer] (0) the x-coordinate to place. Supports Unit Conversion, see {file:README.md#Units Units}.
# @option opts y [Integer] (0) the y-coordinate to place. Supports Unit Conversion, see {file:README.md#Units Units}.
# @option opts color [String] (:black) the color the font will render to. Gradients supported. See {file:README.md#Specifying_Colors___Gradients Specifying Colors}
- # @option opts markup: [Boolean] (false) Enable markup parsing of `str` using the HTML-like Pango Markup syntax, defined [here](http://ruby-gnome2.sourceforge.jp/hiki.cgi?pango-markup) and [here](https://developer.gnome.org/pango/stable/PangoMarkupFormat.html).
+ # @option opts markup: [Boolean] (false) Enable markup parsing of `str` using the HTML-like Pango Markup syntax, defined [here](http://ruby-gnome2.sourceforge.jp/hiki.cgi?pango-markup) and [here](https://developer.gnome.org/pango/stable/PangoMarkupFormat.html). Also does other replacements, such as smart quotes, curly apostraphes, en- and em-dashes, and explict ellipses (not to be confused with ellipsize option). See README for full explanation.
# @option opts width [Integer, :native] (:native) the width of the box the string will be placed in. Stretches to the content by default.. Supports Unit Conversion, see {file:README.md#Units Units}.
# @option opts height [Integer, :native] the height of the box the string will be placed in. Stretches to the content by default. Supports Unit Conversion, see {file:README.md#Units Units}.
# @option opts layout [String, Symbol] (nil) entry in the layout to use as defaults for this command. See {file:README.md#Custom_Layouts Custom Layouts}
@@ -39,7 +38,6 @@ module Squib
# @option opts ellipsize [:none, :start, :middle, :end, true, false] (:end) When width and height are set, determines the behavior of overflowing text. Also: `true` maps to `:end` and `false` maps to `:none`. Default `:end`
# @option opts angle [FixNum] (0) Rotation of the text in radians. Note that this rotates around the upper-left corner of the text box, making the placement of x-y coordinates slightly tricky.
# @option opts hint [String] (:nil) draw a rectangle around the text with the given color. Overrides global hints (see {Deck#hint}).
- # @options ops quotes [:smart, :dumb, or Array]. Convert straight ("dumb") quotes to curly ("smart") quotes. The 'smart' option assumes UTF-8 characters. If you supply a two-element array of characters, those will be used (first is left, second is right). Smart quoting looks for a quote next to a letter, word, number, or underscore character. Default is to show straight quotes.
# @return [Array] Returns an Array of hashes keyed by :width and :height that mark the ink extents of the text rendered.
# @api public
def text(opts = {})
@@ -50,8 +48,7 @@ module Squib
yield(embed) if block_given? #store the opts for later use
extents = Array.new(@cards.size)
opts[:range].each do |i|
- str = Args::SmartQuotes.new.process(opts[:str][i], opts[:quotes][i])
- extents[i] = @cards[i].text(embed, str,
+ extents[i] = @cards[i].text(embed, opts[:str][i],
opts[:font][i], opts[:font_size][i], opts[:color][i],
opts[:x][i], opts[:y][i], opts[:width][i], opts[:height][i],
opts[:markup][i], opts[:justify][i], opts[:wrap][i],
diff --git a/lib/squib/args/smart_quotes.rb b/lib/squib/args/smart_quotes.rb
deleted file mode 100644
index a220b0b..0000000
--- a/lib/squib/args/smart_quotes.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-module Squib
- module Args
- class SmartQuotes
-
- def process(str, opt)
- clean_opt = opt.to_s.downcase.strip
- return str if clean_opt.eql? 'dumb'
- if clean_opt.eql? 'smart'
- quotify(str) # default to UTF-8
- else
- quotify(str, opt) # supplied quotes
- end
- end
-
- # Convert regular quotes to smart quotes by looking for
- # a boundary between a word character (letters, numbers, underscore)
- # and a quote. Replaces with the UTF-8 equivalent.
- # :nodoc:
- # @api private
- def quotify(str, quote_chars = ["\u201C", "\u201D"])
- left_regex = /(\")(\w)/
- right_regex = /(\w)(\")/
- str.gsub(left_regex, quote_chars[0] + '\2')
- .gsub(right_regex, '\1' + quote_chars[1])
- end
-
- end
- end
-end
\ No newline at end of file
diff --git a/lib/squib/args/typographer.rb b/lib/squib/args/typographer.rb
new file mode 100644
index 0000000..1ab62ab
--- /dev/null
+++ b/lib/squib/args/typographer.rb
@@ -0,0 +1,96 @@
+require 'squib/constants'
+module Squib
+ module Args
+ class Typographer
+
+ def initialize(config = CONFIG_DEFAULTS)
+ @config = config
+ end
+
+ def process(str)
+ [
+ :left_curly,
+ :right_curly,
+ :apostraphize,
+ :right_double_quote,
+ :left_double_quote,
+ :right_single_quote,
+ :left_single_quote,
+ :ellipsificate,
+ :em_dash,
+ :en_dash ].each do |sym|
+ str = each_non_tag(str) do |token|
+ self.method(sym).call(token)
+ end
+ end
+ str
+ end
+
+ # Iterate over each non-tag for processing
+ # Allows us to ignore anything inside < and >
+ def each_non_tag(str)
+ full_str = ''
+ tag_delimit = /(<(?:(?!<).)*>)/ # use non-capturing group w/ negative lookahead
+ str.split(tag_delimit).each do |token|
+ if token.start_with? '<'
+ full_str << token # don't process tags
+ else
+ full_str << yield(token)
+ end
+ end
+ return full_str
+ end
+
+ # Straightforward replace
+ def left_curly(str)
+ str.gsub('``', "\u201C")
+ end
+
+ # Straightforward replace
+ def right_curly(str)
+ str.gsub(%{''}, "\u201D")
+ end
+
+ # A quote between two letters is an apostraphe
+ def apostraphize(str)
+ str.gsub(/(\w)(\')(\w)/, '\1' + "\u2019" + '\3')
+ end
+
+ # Quote next to non-whitespace curls
+ def right_double_quote(str)
+ str.gsub(/(\S)(\")/, '\1' + "\u201D")
+ end
+
+ # Quote next to non-whitespace curls
+ def left_double_quote(str)
+ str.gsub(/(\")(\S)/, "\u201C" + '\2')
+ end
+
+ # Quote next to non-whitespace curls
+ def right_single_quote(str)
+ str.gsub(/(\S)(\')/, '\1' + "\u2019")
+ end
+
+ # Quote next to non-whitespace curls
+ def left_single_quote(str)
+ str.gsub(/(\')(\S)/, "\u2018" + '\2')
+ end
+
+ # Straightforward replace
+ def ellipsificate(str)
+ str.gsub('...', "\u2026")
+ end
+
+ # Straightforward replace
+ def en_dash(str)
+ str.gsub('--', "\u2013")
+ end
+
+ # Straightforward replace
+ def em_dash(str)
+ str.gsub('---', "\u2014")
+ end
+
+ end
+ end
+end
\ No newline at end of file
diff --git a/lib/squib/graphics/text.rb b/lib/squib/graphics/text.rb
index 78b6964..d39c612 100644
--- a/lib/squib/graphics/text.rb
+++ b/lib/squib/graphics/text.rb
@@ -1,4 +1,5 @@
require 'pango'
+require 'squib/args/typographer'
module Squib
class Card
@@ -153,7 +154,9 @@ module Squib
layout = cc.create_pango_layout
layout.font_description = font_desc
layout.text = str
- layout.markup = str if markup
+ if markup
+ layout.markup = Args::Typographer.new(@deck.config).process(layout.text)
+ end
set_wh!(layout, width, height)
set_wrap!(layout, wrap)
diff --git a/samples/text_options.rb b/samples/text_options.rb
index 72f783d..3c31b76 100644
--- a/samples/text_options.rb
+++ b/samples/text_options.rb
@@ -78,12 +78,12 @@ Squib::Deck.new(width: 825, height: 1125, cards: 3) do
embed.svg key: ':health:', width: 28, height: 28, file: 'glass-heart.svg'
end
- text str: 'Markup is also quite easy awesome',
+ text str: "Markup is quite 'easy' awesome. Can't beat those \"smart\" 'quotes', now with 10--20% more en-dashes --- and em-dashes --- with explicit ellipses too...",
markup: true,
x: 50, y: 1000,
width: 750, height: 100,
valign: :bottom,
- font: 'Arial 32', hint: :cyan
+ font: 'Serif 18', hint: :cyan
save prefix: 'text_', format: :png
diff --git a/spec/args/smart_quotes_spec.rb b/spec/args/smart_quotes_spec.rb
deleted file mode 100644
index 04ea087..0000000
--- a/spec/args/smart_quotes_spec.rb
+++ /dev/null
@@ -1,46 +0,0 @@
-require 'spec_helper'
-require 'squib/args/smart_quotes'
-
-describe Squib::Args::SmartQuotes do
-
- it 'does nothing on a non-quoted string' do
- expect(subject.quotify('nothing')).to eq('nothing')
- end
-
- it 'left quotes at the beginning' do
- expect(subject.quotify('"foo')).to eq("\u201Cfoo")
- end
-
- it 'left quotes in the middle of the string' do
- expect(subject.quotify('hello "foo')).to eq("hello \u201Cfoo")
- end
-
- it 'right quotes at the end of a string' do
- expect(subject.quotify('foo"')).to eq("foo\u201D")
- end
-
- it 'handles the entire string quoted' do
- expect(subject.quotify('"foo"')).to eq("\u201Cfoo\u201D")
- end
-
- it "quotes in the middle of the string" do
- expect(subject.quotify('hello "foo" world')).to eq("hello \u201Cfoo\u201D world")
- end
-
- it "allows custom quotes for different character sets" do
- expect(subject.quotify('hello "foo" world', %w({ }))).to eq("hello {foo} world")
- end
-
- it "processes dumb quotes" do
- expect(subject.process('hello "foo" world', :dumb)).to eq("hello \"foo\" world")
- end
-
- it "processes smart quotes" do
- expect(subject.process('hello "foo" world', :smart)).to eq("hello \u201Cfoo\u201D world")
- end
-
- it "processes custom quotes" do
- expect(subject.process('hello "foo" world', %w({ }))).to eq("hello {foo} world")
- end
-
-end
\ No newline at end of file
diff --git a/spec/args/typographer_spec.rb b/spec/args/typographer_spec.rb
new file mode 100644
index 0000000..c9243d0
--- /dev/null
+++ b/spec/args/typographer_spec.rb
@@ -0,0 +1,71 @@
+require 'spec_helper'
+require 'squib/args/typographer'
+
+describe Squib::Args::Typographer do
+ subject(:t) { Squib::Args::Typographer.new }
+
+ it 'does nothing on a non-quoted string' do
+ expect(t.process(%{nothing})).to eq(%{nothing})
+ end
+
+ it 'left quotes at the beginning' do
+ expect(t.process(%{"foo})).to eq(%{\u201Cfoo})
+ end
+
+ it 'left quotes in the middle of the string' do
+ expect(t.process(%{hello "foo})).to eq(%{hello \u201Cfoo})
+ end
+
+ it 'right quotes at the end' do
+ expect(t.process(%{foo"})).to eq(%{foo\u201D})
+ end
+
+ it 'handles the entire string quoted' do
+ expect(t.process(%{"foo"})).to eq(%{\u201Cfoo\u201D})
+ end
+
+ it "quotes in the middle of the string" do
+ expect(t.process(%{hello "foo" world})).to eq(%{hello \u201Cfoo\u201D world})
+ end
+
+ it "single left quotes at the beginning" do
+ expect(t.process(%{'foo})).to eq(%{\u2018foo})
+ end
+
+ it "single right quotes at the end" do
+ expect(t.process(%{foo'})).to eq(%{foo\u2019})
+ end
+
+ it "single quotes in the middle" do
+ expect(t.process(%{a 'foo' bar})).to eq(%{a \u2018foo\u2019 bar})
+ end
+
+ it "handles apostraphes" do
+ expect(t.process(%{can't})).to eq(%{can\u2019t})
+ end
+
+ it "single quotes inside double quotes" do
+ expect(t.process(%{"'I can't do that', he said"})).to eq(%{\u201C\u2019I can\u2019t do that\u2019, he said\u201D})
+ end
+
+ it "replaces the straightforward ones" do
+ expect(t.process(%{``hi...'' -- ---})).to eq(%{\u201Chi\u2026\u201D \u2013 \u2014})
+ end
+
+ it "does nothing on lone quotes" do
+ expect(t.process(%{"})).to eq(%{"})
+ expect(t.process(%{'})).to eq(%{'})
+ end
+
+ it "ignores stuff in " do
+ expect(t.process(%{"can't"})).to eq(%{\u201Ccan\u2019t\u201D})
+ end
+
+
+ context 'configured' do
+ #TODO
+ end
+
+
+
+end
\ No newline at end of file
diff --git a/spec/data/samples/embed_text.rb.txt b/spec/data/samples/embed_text.rb.txt
index db71fe4..979b66d 100644
--- a/spec/data/samples/embed_text.rb.txt
+++ b/spec/data/samples/embed_text.rb.txt
@@ -12,13 +12,67 @@ cairo: rounded_rectangle([0, 0, 825, 1125, 0, 0])
cairo: set_source_color(["#0000"])
cairo: fill([])
cairo: restore([])
+cairo: antialias=(["subpixel"])
+cairo: antialias=(["subpixel"])
+cairo: antialias=(["subpixel"])
+cairo: save([])
+cairo: set_source_color([:black])
+cairo: translate([0, 0])
+cairo: rotate([0])
+cairo: move_to([0, 0])
+pango font: size=([18432])
+pango: font_description=([MockDouble])
+pango: text=(["Take 1 :tool: and gain 2 :health:."])
+pango: width=([184320])
+pango: height=([307200])
+pango: wrap=([#])
+pango: ellipsize=([#])
+pango: alignment=([#])
+pango: justify=([false])
+pango: spacing=([0])
+cairo: update_pango_layout([MockDouble])
+pango: markup=(["Take 1 :tool: and gain 2 :health:."])
+cairo: move_to([0, 0.0])
+cairo: update_pango_layout([MockDouble])
+cairo: show_pango_layout([MockDouble])
+cairo: rounded_rectangle([0, 0, 0, 0, 0, 0])
+cairo: set_source_color([:cyan])
+cairo: set_line_width([2.0])
+cairo: stroke([])
+cairo: restore([])
+cairo: save([])
+cairo: set_source_color([:black])
+cairo: translate([0, 0])
+cairo: rotate([0])
+cairo: move_to([0, 0])
+pango font: size=([32768])
+pango: font_description=([MockDouble])
+pango: text=(["Take 1 :tool: and gain 2 :health:."])
+pango: width=([184320])
+pango: height=([307200])
+pango: wrap=([#])
+pango: ellipsize=([#])
+pango: alignment=([#])
+pango: justify=([false])
+pango: spacing=([0])
+cairo: update_pango_layout([MockDouble])
+pango: markup=(["Take 1 :tool: and gain 2 :health:."])
+cairo: move_to([0, 0.0])
+cairo: update_pango_layout([MockDouble])
+cairo: show_pango_layout([MockDouble])
+cairo: rounded_rectangle([0, 0, 0, 0, 0, 0])
+cairo: set_source_color([:cyan])
+cairo: set_line_width([2.0])
+cairo: stroke([])
+cairo: restore([])
cairo: save([])
cairo: set_source_color([:black])
-cairo: translate([200, 0])
+cairo: translate([0, 0])
cairo: rotate([0])
cairo: move_to([0, 0])
+pango font: size=([46080])
pango: font_description=([MockDouble])
-pango: text=(["Middle align: Take 1 :tool: and gain 2 :health:. Take 2 :tool: and gain 3 :purse:"])
+pango: text=(["Take 1 :tool: and gain 2 :health:."])
pango: width=([184320])
pango: height=([307200])
pango: wrap=([#])
@@ -27,7 +81,7 @@ pango: alignment=([#])
pango: justify=([false])
pango: spacing=([0])
cairo: update_pango_layout([MockDouble])
-pango: markup=(["Middle align: Take 1 :tool: and gain 2 :health:. Take 2 :tool: and gain 3 :purse:"])
+pango: markup=(["Take 1 :tool: and gain 2 :health:."])
cairo: move_to([0, 0.0])
cairo: update_pango_layout([MockDouble])
cairo: show_pango_layout([MockDouble])
@@ -36,4 +90,6 @@ cairo: set_source_color([:cyan])
cairo: set_line_width([2.0])
cairo: stroke([])
cairo: restore([])
-surface: write_to_png(["_output/embed_00.png"])
+surface: write_to_png(["_output/embed_multi_00.png"])
+surface: write_to_png(["_output/embed_multi_01.png"])
+surface: write_to_png(["_output/embed_multi_02.png"])
diff --git a/spec/data/samples/text_options.rb.txt b/spec/data/samples/text_options.rb.txt
index e34bf98..9cf7931 100644
--- a/spec/data/samples/text_options.rb.txt
+++ b/spec/data/samples/text_options.rb.txt
@@ -331,7 +331,7 @@ cairo: translate([65, 400])
cairo: rotate([0])
cairo: move_to([0, 0])
pango: font_description=([MockDouble])
-pango: text=(["This text has fixed width, fixed height, center-aligned, middle-valigned, has a red hint, and \u201Csmart quotes\u201D"])
+pango: text=(["This text has fixed width, fixed height, center-aligned, middle-valigned, has a red hint, and \"smart quotes\""])
pango: width=([307200])
pango: height=([128000])
pango: wrap=([#])
@@ -354,7 +354,7 @@ cairo: translate([65, 400])
cairo: rotate([0])
cairo: move_to([0, 0])
pango: font_description=([MockDouble])
-pango: text=(["This text has fixed width, fixed height, center-aligned, middle-valigned, has a red hint, and \u201Csmart quotes\u201D"])
+pango: text=(["This text has fixed width, fixed height, center-aligned, middle-valigned, has a red hint, and \"smart quotes\""])
pango: width=([307200])
pango: height=([128000])
pango: wrap=([#])
@@ -829,8 +829,8 @@ cairo: translate([50, 1000])
cairo: rotate([0])
cairo: move_to([0, 0])
pango: font_description=([MockDouble])
-pango: text=(["Markup is also quite easy awesome"])
-pango: markup=(["Markup is also quite easy awesome"])
+pango: text=(["Markup is quite 'easy' awesome. Can't beat those \"smart\" 'quotes', now with 10--20% more en-dashes --- and em-dashes --- with explicit ellipses too..."])
+pango: markup=(["foo"])
pango: width=([768000])
pango: height=([102400])
pango: wrap=([#])
@@ -853,8 +853,8 @@ cairo: translate([50, 1000])
cairo: rotate([0])
cairo: move_to([0, 0])
pango: font_description=([MockDouble])
-pango: text=(["Markup is also quite easy awesome"])
-pango: markup=(["Markup is also quite easy awesome"])
+pango: text=(["Markup is quite 'easy' awesome. Can't beat those \"smart\" 'quotes', now with 10--20% more en-dashes --- and em-dashes --- with explicit ellipses too..."])
+pango: markup=(["foo"])
pango: width=([768000])
pango: height=([102400])
pango: wrap=([#])
@@ -877,8 +877,8 @@ cairo: translate([50, 1000])
cairo: rotate([0])
cairo: move_to([0, 0])
pango: font_description=([MockDouble])
-pango: text=(["Markup is also quite easy awesome"])
-pango: markup=(["Markup is also quite easy awesome"])
+pango: text=(["Markup is quite 'easy' awesome. Can't beat those \"smart\" 'quotes', now with 10--20% more en-dashes --- and em-dashes --- with explicit ellipses too..."])
+pango: markup=(["foo"])
pango: width=([768000])
pango: height=([102400])
pango: wrap=([#])
diff --git a/squib.sublime-project b/squib.sublime-project
index 351e93f..857278a 100644
--- a/squib.sublime-project
+++ b/squib.sublime-project
@@ -29,6 +29,11 @@
"shell_cmd": "rspec spec/samples/samples_regression_spec.rb",
"working_dir": "${project_path:${folder}}"
},
+ {
+ "name": "rspec spec/args/typographer_spec.rb",
+ "shell_cmd": "rspec spec/args/typographer_spec.rb",
+ "working_dir": "${project_path:${folder}}"
+ },
{
"name": "rspec spec/graphics/graphics_text_spec.rb",
"shell_cmd": "rspec spec/graphics/graphics_text_spec.rb",