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",