Class: Inkcite::View
- Inherits:
-
Object
- Object
- Inkcite::View
- Defined in:
- lib/inkcite/view.rb,
lib/inkcite/view/context.rb,
lib/inkcite/view/tag_stack.rb,
lib/inkcite/view/media_query.rb
Defined Under Namespace
Classes: Context, MediaQuery, TagStack
Instance Attribute Summary collapse
-
#config ⇒ Object
The configuration hash for the view.
-
#content ⇒ Object
readonly
The rendered html or content available after render! has been called.
-
#email ⇒ Object
readonly
The base Email object this is a view of.
-
#environment ⇒ Object
readonly
One of :development, :preview or :production.
-
#errors ⇒ Object
The array of error messages collected during rendering.
-
#format ⇒ Object
readonly
The format of the email (e.g. :email or :text).
-
#js_compressor ⇒ Object
Will be populated with the css and js compressor objects after first use.
-
#line_number ⇒ Object
Line number of the email file being processed.
-
#media_query ⇒ Object
readonly
Manages the Responsive::Rules applied to this email view.
-
#version ⇒ Object
readonly
The version of the email (e.g. :default).
Instance Method Summary collapse
- #[](key) ⇒ Object
-
#assert_image_exists(src) ⇒ Object
Verifies that the provided image file (e.g. “banner.jpg”) exists in the project’s image subdirectory.
- #browser? ⇒ Boolean
-
#data ⇒ Object
Arbitrary storage of data.
- #default? ⇒ Boolean
- #development? ⇒ Boolean
- #email? ⇒ Boolean
-
#error(message, obj = nil) ⇒ Object
Records an error message on the currently processing line of the source.
- #eval_erb(source, file_name) ⇒ Object
- #file_name(ext = nil) ⇒ Object
- #footer ⇒ Object
- #footnotes ⇒ Object
-
#image_url(src) ⇒ Object
Returns the fully-qualified URL to the designated image (e.g. logo.gif) appropriate for the current rendering environment.
-
#initialize(email, environment, format, version) ⇒ View
constructor
A new instance of View.
-
#is_disabled?(key) ⇒ Boolean
Tests if a configuration value has been disabled.
-
#is_enabled?(key) ⇒ Boolean
Tests if a configuration value has been enabled.
-
#links ⇒ Object
Map of hrefs by their unique ID.
- #links_file_name ⇒ Object
-
#links_tsv ⇒ Object
Returns a hash of the links.tsv file from the project which is used to populate the a and button hrefs when an href isn’t defined.
- #meta(key) ⇒ Object
-
#parent_opts(tag) ⇒ Object
Returns the opts for the parent matching the designated tag, if any are presently open.
-
#prefixes ⇒ Object
Returns the array of browser prefixes that need to be included in CSS styles based on which version of the email this is.
- #preview? ⇒ Boolean
- #production? ⇒ Boolean
-
#read_source(source_file) ⇒ Object
Helper method which reads the designated file (e.g. source.html) and performs ERB on it, strips illegal characters and comments (if minified) and returns the filtered content.
- #render! ⇒ Object
- #rendered? ⇒ Boolean
- #scripts ⇒ Object
- #set_meta(key, value) ⇒ Object
- #styles ⇒ Object
- #subject ⇒ Object
- #tag_stack(tag) ⇒ Object
-
#test! ⇒ Object
Sends this version of the email to Litmus for testing.
- #text? ⇒ Boolean
- #title ⇒ Object
- #track_links? ⇒ Boolean
-
#unique_id(key) ⇒ Object
Generates an incremental ID for the designated key.
-
#vml_enabled? ⇒ Boolean
Returns true if vml is enabled in this context.
-
#vml_used! ⇒ Object
Signifies that VML was used during the rendering and that.
- #vml_used? ⇒ Boolean
- #write(out) ⇒ Object
- #write_links_csv(out) ⇒ Object
Constructor Details
#initialize(email, environment, format, version) ⇒ View
Returns a new instance of View.
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 |
# File 'lib/inkcite/view.rb', line 40 def initialize email, environment, format, version @email = email @environment = environment @format = format @version = version # Read the helper(s) for this view of the email. This will load # the default helpers.tsv and any version-specific (e.g. returning-customer.tsv) # helper allowing for overrides. @config = load_helpers # Merge in the email's configuration for convience - providing access # to the renderers. @config.merge!(email.config) # Expose the version, format as a properties so that it can be resolved when # processing pathnames and such. These need to be strings because they are # cloned during rendering. @config[:version] = version.to_s @config[:format] = format.to_s @config[FILE_NAME] = file_name # Expose the project's directory name as the project entry. @config[:project] = File.basename(@email.path) # The MediaQuery object manages the responsive styles that are applied to # the email during rendering. Check to see if a breakwidth has been supplied # in helpers.tsv so the designer can control the primary breakpoint. breakpoint = @config[:'mobile-breakpoint'].to_i if breakpoint <= 0 breakpoint = @config[:width].to_i - 1 breakpoint = 480 if breakpoint <= 0 end @media_query = MediaQuery.new(self, breakpoint) # Set the version index based on the position of this # version in the list of those defined. @config[:'version-index'] = (email.versions.index(version) + 1).to_s # Tracks the line number and is recorded when errors are encountered # while rendering said line. @line_number = 0 # True if VML is used during the preparation of this email. @vml_used = false # Initializing to prevent a ruby verbose warning. @footnotes = nil end |
Instance Attribute Details
#config ⇒ Object
The configuration hash for the view
30 31 32 |
# File 'lib/inkcite/view.rb', line 30 def config @config end |
#content ⇒ Object (readonly)
The rendered html or content available after render! has been called.
12 13 14 |
# File 'lib/inkcite/view.rb', line 12 def content @content end |
#email ⇒ Object (readonly)
The base Email object this is a view of
9 10 11 |
# File 'lib/inkcite/view.rb', line 9 def email @email end |
#environment ⇒ Object (readonly)
One of :development, :preview or :production
15 16 17 |
# File 'lib/inkcite/view.rb', line 15 def environment @environment end |
#errors ⇒ Object
The array of error messages collected during rendering
33 34 35 |
# File 'lib/inkcite/view.rb', line 33 def errors @errors end |
#format ⇒ Object (readonly)
The format of the email (e.g. :email or :text)
21 22 23 |
# File 'lib/inkcite/view.rb', line 21 def format @format end |
#js_compressor ⇒ Object
Will be populated with the css and js compressor objects after first use. Ensures we can reset the compressors after a rendering is complete.
38 39 40 |
# File 'lib/inkcite/view.rb', line 38 def js_compressor @js_compressor end |
#line_number ⇒ Object
Line number of the email file being processed
27 28 29 |
# File 'lib/inkcite/view.rb', line 27 def line_number @line_number end |
#media_query ⇒ Object (readonly)
Manages the Responsive::Rules applied to this email view.
24 25 26 |
# File 'lib/inkcite/view.rb', line 24 def media_query @media_query end |
#version ⇒ Object (readonly)
The version of the email (e.g. :default)
18 19 20 |
# File 'lib/inkcite/view.rb', line 18 def version @version end |
Instance Method Details
#[](key) ⇒ Object
92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 |
# File 'lib/inkcite/view.rb', line 92 def [] key key = key.to_sym # Look for configuration specific to the environment and then format. env_cfg = config[@environment] || EMPTY_HASH ver_cfg = env_cfg[@version] || config[@version] || EMPTY_HASH fmt_cfg = env_cfg[@format] || EMPTY_HASH # Not using || operator because the value can be legitimately false (e.g. minify # is disabled) so only a nil should trigger moving on to the next level up the # hierarchy. val = ver_cfg[key] val = fmt_cfg[key] if val.nil? val = env_cfg[key] if val.nil? val = config[key] if val.nil? val end |
#assert_image_exists(src) ⇒ Object
Verifies that the provided image file (e.g. “banner.jpg”) exists in the project’s image subdirectory. If not, reports the missing image to the developer (unless that is explicitly disabled).
114 115 116 117 118 119 120 121 122 123 |
# File 'lib/inkcite/view.rb', line 114 def assert_image_exists src # This is the full path to the image on the dev's harddrive. path = @email.image_path(src) exists = File.exist?(path) error('Missing image', { :src => src }) if !exists exists end |
#browser? ⇒ Boolean
125 126 127 |
# File 'lib/inkcite/view.rb', line 125 def browser? @format == :browser end |
#data ⇒ Object
Arbitrary storage of data
130 131 132 |
# File 'lib/inkcite/view.rb', line 130 def data @data ||= {} end |
#default? ⇒ Boolean
134 135 136 |
# File 'lib/inkcite/view.rb', line 134 def default? @version == :default end |
#development? ⇒ Boolean
138 139 140 |
# File 'lib/inkcite/view.rb', line 138 def development? @environment == :development end |
#email? ⇒ Boolean
142 143 144 |
# File 'lib/inkcite/view.rb', line 142 def email? @format == :email end |
#error(message, obj = nil) ⇒ Object
Records an error message on the currently processing line of the source.
151 152 153 154 155 156 157 158 159 160 161 162 163 164 |
# File 'lib/inkcite/view.rb', line 151 def error , obj=nil << " (line #{self.line_number.to_i})" unless obj.blank? << ' [' << obj.collect { |k, v| "#{k}=#{v}" }.join(', ') << ']' end @errors ||= [] @errors << true end |
#eval_erb(source, file_name) ⇒ Object
146 147 148 |
# File 'lib/inkcite/view.rb', line 146 def eval_erb source, file_name Erubis::Eruby.new(source, :filename => file_name, :trim => false, :numbering => true).evaluate(Context.new(self)) end |
#file_name(ext = nil) ⇒ Object
199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 |
# File 'lib/inkcite/view.rb', line 199 def file_name ext=nil # Check to see if the file name has been configured. fn = self[FILE_NAME] if fn.blank? # Default naming based on the number of versions - only the format if there is # a single version or version and format when there are multiple versions. fn = if email.versions.length > 1 '{version}-{format}' elsif text? 'email' else '{format}' end end # Need to render the name to convert embedded tags to actual values. fn = Renderer.render(fn, self) # Sanity check to ensure there is an appropriate extension on the # file name. ext ||= (text?? TXT_EXTENSION : HTML_EXTENSION) fn << ext unless File.extname(fn) == ext fn end |
#footer ⇒ Object
166 167 168 |
# File 'lib/inkcite/view.rb', line 166 def @footer ||= [] end |
#footnotes ⇒ Object
170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 |
# File 'lib/inkcite/view.rb', line 170 def footnotes if @footnotes.nil? @footnotes = [] # Preload the array of footnotes if they exist footnotes_tsv_file = @email.project_file(FOOTNOTES_TSV_FILE) if File.exist?(footnotes_tsv_file) CSV.foreach(footnotes_tsv_file, { :col_sep => "\t" }) do |fn| id = fn[0] next if id.blank? text = fn[2] next if text.blank? # Read the symbol and replace it with nil (so that one will be auto-generated) symbol = fn[1] symbol = nil if symbol.blank? @footnotes << Renderer::Footnote::Instance.new(id, symbol, text, false) end end end @footnotes end |
#image_url(src) ⇒ Object
Returns the fully-qualified URL to the designated image (e.g. logo.gif) appropriate for the current rendering environment. In development mode, local will have either images/ or images-optim/ prepended on them depending on the status of image optimization.
For non-development builds, fully-qualified URLs may be returned depending on the state of the config.yml and how image-host attributes have been configured.
If a fully-qualified URL is provided, the URL will be returned with the possible addition of the cache-breaker tag.
240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 |
# File 'lib/inkcite/view.rb', line 240 def image_url src src_url = '' if Util.is_fully_qualified?(src) src_url << src else # Prepend the image host onto the src if one is specified in the properties. # During local development, images are always expected in an images/ subdirectory. image_host = if development? (@email.optimize_images?? Minifier::IMAGE_CACHE : Email::IMAGES) + '/' else # Use the image host defined in config.yml or, out-of-the-box refer to images/ # in the build directory. self[Email::IMAGE_HOST] || (Email::IMAGES + '/') end src_url << image_host unless image_host.blank? # Add the source of the image. src_url << src end # Cache-bust the image if the caller is expecting it to be there. Util::add_query_param(src_url, Time.now.to_i) if !production? && is_enabled?(Email::CACHE_BUST) # Transpose any embedded tags into actual values. Renderer.render(src_url, self) end |
#is_disabled?(key) ⇒ Boolean
Tests if a configuration value has been disabled. This assumes it is enabled by default but that a value of false, ‘false’ or 0 will indicate it is disabled.
286 287 288 289 |
# File 'lib/inkcite/view.rb', line 286 def is_disabled? key val = self[key] !val.nil? && (val == false || val == false.to_s) end |
#is_enabled?(key) ⇒ Boolean
Tests if a configuration value has been enabled. This assumes it is disabled by default but that a value of true, ‘true’ or 1 for the value indicates it is enabled.
278 279 280 281 |
# File 'lib/inkcite/view.rb', line 278 def is_enabled? key val = self[key] !val.blank? && val != false && (val == true || val == true.to_s || val.to_i == 1) end |
#links ⇒ Object
Map of hrefs by their unique ID
307 308 309 |
# File 'lib/inkcite/view.rb', line 307 def links @links ||= {} end |
#links_file_name ⇒ Object
292 293 294 295 296 297 298 299 300 301 302 303 304 |
# File 'lib/inkcite/view.rb', line 292 def links_file_name # There is nothing to return if trackable links aren't enabled. return nil unless track_links? fn = '' fn << "#{@version}-" if email.versions.length > 1 fn << 'links.csv' # Need to render the name to convert embedded tags to actual values. Renderer.render(fn, self) end |
#links_tsv ⇒ Object
Returns a hash of the links.tsv file from the project which is used to populate the a and button hrefs when an href isn’t defined.
313 314 315 316 317 318 319 320 321 322 323 324 325 |
# File 'lib/inkcite/view.rb', line 313 def links_tsv @links_tsv ||= begin links_tsv_file = @email.project_file(LINKS_TSV_FILE) if File.exist?(links_tsv_file) Hash[CSV.read(links_tsv_file, { :col_sep => "\t" })] else {} end rescue Exception => e error("There was a problem reading #{LINKS_TSV_FILE}: #{e.}") {} end end |
#meta(key) ⇒ Object
327 328 329 330 |
# File 'lib/inkcite/view.rb', line 327 def key md = md.nil?? nil : md[key] end |
#parent_opts(tag) ⇒ Object
Returns the opts for the parent matching the designated tag, if any are presently open.
334 335 336 |
# File 'lib/inkcite/view.rb', line 334 def parent_opts tag tag_stack(tag).opts end |
#prefixes ⇒ Object
Returns the array of browser prefixes that need to be included in CSS styles based on which version of the email this is.
340 341 342 |
# File 'lib/inkcite/view.rb', line 340 def prefixes [ '', '-webkit-' ] end |
#preview? ⇒ Boolean
344 345 346 |
# File 'lib/inkcite/view.rb', line 344 def preview? @environment == :preview end |
#production? ⇒ Boolean
348 349 350 |
# File 'lib/inkcite/view.rb', line 348 def production? @environment == :production end |
#read_source(source_file) ⇒ Object
Helper method which reads the designated file (e.g. source.html) and performs ERB on it, strips illegal characters and comments (if minified) and returns the filtered content.
355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 |
# File 'lib/inkcite/view.rb', line 355 def read_source source_file # Will be used to assemble the parameters passed to File.open. # First, always open the file in read mode. mode = [ 'r' ] # Detect abnormal file encoding and construct the string to # convert such encoding to UTF-8 if specified. encoding = self[SOURCE_ENCODING] unless encoding.blank? || encoding == UTF_8 mode << encoding mode << UTF_8 end # Read the original source which may include embedded Ruby. source = File.open(source_file, mode.join(':')).read # Run the content through Erubis source = self.eval_erb(source, source_file) # If minification is enabled this will remove anything that has been # <!-- commented out --> to ensure the email is as small as possible. source = Minifier.remove_comments(source, self) # Protect against unsupported characters source = Renderer.fix_illegal_characters(source, self) source end |
#render! ⇒ Object
385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 |
# File 'lib/inkcite/view.rb', line 385 def render! raise "Already rendered" unless @content.blank? source_file = 'source' source_file << (text?? TXT_EXTENSION : HTML_EXTENSION) # Read the original source which may include embedded Ruby. filtered = read_source(@email.project_file(source_file)) # Filter each of the lines of text and push them onto the stack of lines # that we be written into the text or html file. lines = render_each(filtered) @content = if text? lines.join(NEW_LINE) else # Minify the content of the email. minified = Minifier.html(lines, self) # Some last-minute fixes before we assemble the wrapping content. prevent_ios_date_detection minified # Prepare a copy of the HTML for saving as the file. html = [] # Using HTML5 DOCTYPE # https://emails.hteumeuleu.com/which-doctype-should-you-use-in-html-emails-cd323fdb793c#.cxet9febe html << '<!DOCTYPE html>' # Resolve the HTML declaration for this email based on whether or not VML was used. html_declaration = '<html xmlns="http://www.w3.org/1999/xhtml"' html_declaration << ' xmlns:v="urn:schemas-microsoft-com:vml" lang="en" xml:lang="en"' if vml_used? html_declaration << '>' html << html_declaration html << '<head>' html << '<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>' html << '<meta name="viewport" content="width=device-width"/>' html << "<meta name=\"generator\" content=\"Inkcite #{Inkcite::VERSION}\"/>" # Enable responsive media queries on Windows phones courtesy of @jamesmacwhite # https://blog.jmwhite.co.uk/2014/03/01/windows-phone-does-support-css3-media-queries-in-html-email/ html << Renderer.render('{not-outlook}<meta http-equiv="X-UA-Compatible" content="IE=edge" />{/not-outlook}', self) # Some native Android clients display the title before the preheader so # don't include it in non-development or email rendering per @moonstrips html << "<title>#{self.title if (development? || browser?)}</title>" # Add external script sources. html += external_scripts # Add external styles html += external_styles html << inline_styles html << '</head>' # Intentionally not setting the link colors because those should be entirely # controlled by the styles and attributes of the links themselves. By not # setting it, links created sans-helper should be visually distinct. html << '<body style="width: 100% !important; min-width: 100% !important; margin: 0 !important; padding: 0; -webkit-text-size-adjust: none; -ms-text-size-adjust: none;' # A pleasing but obvious background exposed in development mode to alert # the designer that they have exposed the body background - which means # unpredictable results if sent. if development? html << " background: #ccc url('data:image/png;base64,#{Inkcite.blueprint_image64}');" end html << %q(">) html << minified # Append any arbitrary footer content html << # Add inline scripts html << inline_scripts html << '</body></html>' # Remove all blank lines and assemble the wrapped content into a # a single string. html.select { |l| !l.blank? }.join(NEW_LINE) end # Ensure that all failsafes pass assert_failsafes # Verify that the tag stack is open which indicates all opened tags were # properly closed - e.g. all {table}s have matching {/table}s. #open_stack = @tag_stack && @tag_stack.select { |k, v| !v.empty? } #raise open_stack.inspect #error 'One or more {tags} may have been left open', { :open_stack => open_stack.collect(&:tag) } if open_stack @content end |
#rendered? ⇒ Boolean
486 487 488 |
# File 'lib/inkcite/view.rb', line 486 def rendered? !@content.blank? end |
#scripts ⇒ Object
490 491 492 |
# File 'lib/inkcite/view.rb', line 490 def scripts @scripts ||= [] end |
#set_meta(key, value) ⇒ Object
494 495 496 497 498 499 500 501 502 |
# File 'lib/inkcite/view.rb', line 494 def key, value md = || {} md[key.to_sym] = value # Write the hash back to the email's meta data. @email. version, md value end |
#styles ⇒ Object
504 505 506 |
# File 'lib/inkcite/view.rb', line 504 def styles @styles ||= [] end |
#subject ⇒ Object
508 509 510 |
# File 'lib/inkcite/view.rb', line 508 def subject @subject ||= Renderer.render((self[:subject] || self[:title] || UNTITLED_EMAIL), self) end |
#tag_stack(tag) ⇒ Object
512 513 514 515 |
# File 'lib/inkcite/view.rb', line 512 def tag_stack tag @tag_stack ||= Hash.new() @tag_stack[tag] ||= TagStack.new(tag, self) end |
#test! ⇒ Object
Sends this version of the email to Litmus for testing.
522 523 524 |
# File 'lib/inkcite/view.rb', line 522 def test! EmailTest.test! self end |
#text? ⇒ Boolean
526 527 528 |
# File 'lib/inkcite/view.rb', line 526 def text? @format == :text end |
#title ⇒ Object
517 518 519 |
# File 'lib/inkcite/view.rb', line 517 def title @title ||= Renderer.render((self[:title] || UNTITLED_EMAIL), self) end |
#track_links? ⇒ Boolean
530 531 532 |
# File 'lib/inkcite/view.rb', line 530 def track_links? !self[Email::TRACK_LINKS].blank? end |
#unique_id(key) ⇒ Object
Generates an incremental ID for the designated key. The first time a key is used, it will return a 1. Subsequent requests for said key will return 2, 3, etc.
537 538 539 540 |
# File 'lib/inkcite/view.rb', line 537 def unique_id key @unique_ids ||= Hash.new(0) @unique_ids[key] += 1 end |
#vml_enabled? ⇒ Boolean
Returns true if vml is enabled in this context. This requires that the context is for an email and that the VML property is enabled.
544 545 546 |
# File 'lib/inkcite/view.rb', line 544 def vml_enabled? email? && !is_disabled?(:vml) end |
#vml_used! ⇒ Object
Signifies that VML was used during the rendering and that
549 550 551 552 |
# File 'lib/inkcite/view.rb', line 549 def vml_used! raise 'VML was used but is not enabled' unless vml_enabled? @vml_used = true end |
#vml_used? ⇒ Boolean
554 555 556 |
# File 'lib/inkcite/view.rb', line 554 def vml_used? @vml_used == true end |
#write(out) ⇒ Object
558 559 560 561 562 563 564 565 566 567 568 |
# File 'lib/inkcite/view.rb', line 558 def write out # Ensure that the version has been rendered fully render! # Fully-qualify the filename - e.g. public/project/issue/file_name and then write the # contents of the HTML to said file. out.write(@content) true end |
#write_links_csv(out) ⇒ Object
570 571 572 573 574 575 576 577 578 579 580 |
# File 'lib/inkcite/view.rb', line 570 def write_links_csv out unless @links.blank? csv = CSV.new(out, :force_quotes => true) # Write each link to the CSV file. @links.keys.sort.each { |k| csv << [k, @links[k]] } end true end |