class Inkcite::View
Constants
- ASSETS
- COMMENT
Used to mark a helper.tsv line as a comment.
- EMPTY_HASH
Empty hash used when there is no environment or format-specific configuration
- FILE_NAME
- FILE_SCHEME
- FONT_CACHE_FILE
Name of the local font cache file used for local storage of Google Font CSS
- FOOTNOTES_TSV_FILE
Tab-separated file containing footnote declarations.
- HTML_EXTENSION
- IMAGES_OFF
Config value which disables images (e.g. shows their size, position background color and alt text).
- LINKS_EXTENSION
- LINKS_TSV_FILE
Tab-separated file containing links declarations.
- MULTILINE_END
- MULTILINE_START
Used to denote the start and end of a multi-line helper entry. e.g. feature-story <<-START
I'm a multiline helper where line breaks and indendentation can be used to make the helper file more readable, debuggable.
END->>
- NEW_LINE
- REGEX_SLASH
- SOURCE_ENCODING
- SUBSTITUTIONS_TSV_FILE
Tab-separated file containing global substitution values.
- TAB
- TAB_TO_SPACE
Tabs within a multiline helper are converted to spaces.
- TXT_EXTENSION
- UNTITLED_EMAIL
Used when there is no subject or title for this email.
- UTF_8
Attributes
The configuration hash for the view
The rendered html or content available after render! has been called.
The base Email
object this is a view of
One of :development, :preview or :production
The array of error messages collected during rendering
The format of the email (e.g. :email or :text)
Will be populated with the css and js compressor objects after first use. Ensures we can reset the compressors after a rendering is complete.
The most recently processed tag populated from the renderer and used when reporting errors.
Manages the Responsive::Rules applied to this email view.
Array of post-processors that can modify the HTML of the email after it is rendered but before it is saved.
The version of the email (e.g. :default)
Public Class Methods
# File lib/inkcite/view.rb, line 45 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 # True if VML is used during the preparation of this email. @vml_used = false # Initializing to prevent a ruby verbose warning. @footnotes = nil @substitutions = nil # Will hold any post processors installed during rendering. @post_processors = [] end
Public Instance Methods
# File lib/inkcite/view.rb, line 97 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
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).
# File lib/inkcite/view.rb, line 119 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
# File lib/inkcite/view.rb, line 130 def browser? @format == :browser end
Arbitrary storage of data
# File lib/inkcite/view.rb, line 135 def data @data ||= {} end
# File lib/inkcite/view.rb, line 139 def default? @version == :default end
# File lib/inkcite/view.rb, line 143 def development? @environment == :development end
# File lib/inkcite/view.rb, line 147 def email? @format == :email end
Records an error message on the currently processing line of the source.
# File lib/inkcite/view.rb, line 156 def error message, obj={} obj[:markup] = %Q({#{self.last_rendered_markup}}) unless obj.blank? message << ' [' message << obj.collect { |k, v| "#{k}=#{v}" }.join(', ') message << ']' end @errors ||= [] @errors << message true end
# File lib/inkcite/view.rb, line 151 def eval_erb source, file_name Erubis::Eruby.new(source, :filename => file_name, :trim => false, :numbering => true).evaluate(Context.new(self)) end
# File lib/inkcite/view.rb, line 205 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
# File lib/inkcite/view.rb, line 176 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
Defines a new helper, which allows designers to keep helper markup alongside the usage of it inside of partial. Helps keep code clean and prevents helper.tsv pollution for one-offs
# File lib/inkcite/view.rb, line 238 def helper tag, open, close=nil tag = tag.to_sym @config[tag] = open.to_s @config[:"/#{tag}"] = close.to_s end
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.
# File lib/inkcite/view.rb, line 258 def image_url src src_url = '' # Check to see if images are disabled - if so, return a broken image. if self[IMAGES_OFF] src_url = "__#{src}" elsif 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? ? ImageMinifier::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
# File lib/inkcite/view.rb, line 301 def images_off= off @config[IMAGES_OFF] = !!off end
# File lib/inkcite/view.rb, line 297 def images_off? is_enabled?(IMAGES_OFF) end
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.
# File lib/inkcite/view.rb, line 316 def is_disabled? key val = self[key] !val.nil? && (val == false || val == false.to_s) end
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.
# File lib/inkcite/view.rb, line 308 def is_enabled? key val = self[key] !val.blank? && val != false && (val == true || val == true.to_s || val.to_i == 1) end
Map of hrefs by their unique ID
# File lib/inkcite/view.rb, line 336 def links @links ||= {} end
# File lib/inkcite/view.rb, line 321 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
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.
# File lib/inkcite/view.rb, line 342 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.message}") {} end end
# File lib/inkcite/view.rb, line 356 def meta key md = meta_data md.nil? ? nil : md[key] end
Returns true if the specified key has been queried once and only once. Any additional queries will return false
# File lib/inkcite/view.rb, line 363 def once? key # Initialize the 'once' data hash which maps data[:once] ||= {} key = key.to_sym # True if this is the first time we've encountered this key. first_time = data[:once][key].nil? data[:once][key] = true if first_time first_time end
Returns the opts for the parent matching the designated tag, if any are presently open.
# File lib/inkcite/view.rb, line 379 def parent_opts tag tag_stack(tag).opts end
Returns the array of browser prefixes that need to be included in CSS styles based on which version of the email this is.
# File lib/inkcite/view.rb, line 385 def prefixes _prefixes = [''] # In development mode, include all prefixes for maximum compatibility. _prefixes += %w(-moz- -ms- -o-) if development? # Always include webkit for targeting mobile devices. _prefixes << '-webkit-' _prefixes end
# File lib/inkcite/view.rb, line 397 def preview? @environment == :preview end
# File lib/inkcite/view.rb, line 401 def production? @environment == :production end
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.
# File lib/inkcite/view.rb, line 408 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) # Perform global substitution of characters - e.g. ’ into ' if that's substitution_map.each_pair { |orig, replace| source.gsub!(orig, replace) } # Protect against unsupported characters source = Renderer.fix_illegal_characters(source, self) source end
# File lib/inkcite/view.rb, line 441 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"' if vml_used? html_declaration << ' xmlns:o="urn:schemas-microsoft-com:office:office"' if email? html_declaration << '>' html << html_declaration html << '<head>' html << '<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>' # iOS 10 auto-scale fix # https://emails.hteumeuleu.com/what-you-need-to-know-about-apple-mail-in-ios-10-c7031f6d704d html << '<meta name="x-apple-disable-message-reformatting"/>' html << '<meta name="viewport" content="width=device-width"/>' html << %Q(<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 << outlook_styles html << outlook_high_dpi_fix if email? html << '</head>' html << body_declaration html << minified # Append any arbitrary footer content html << inline_footer # 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 # Provide each post processor (if any were installed during the rendering # process) with a chance to operate on the final HTML. @content = PostProcessor.run_all(@content, self) # Some ESPs have link tracking/opt-out code that uses greater-than and # less-than symbols. Encode them as their HTML escape code equivalents # in the source and replace them with their original characters in the # final output. @content.gsub!('>', '>') @content.gsub!('<', '<') # 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
# File lib/inkcite/view.rb, line 552 def rendered? !@content.blank? end
# File lib/inkcite/view.rb, line 556 def scripts @scripts ||= [] end
# File lib/inkcite/view.rb, line 560 def set_meta key, value md = meta_data || {} md[key.to_sym] = value # Write the hash back to the email's meta data. @email.set_meta version, md value end
# File lib/inkcite/view.rb, line 570 def styles @styles ||= [] end
# File lib/inkcite/view.rb, line 574 def subject @subject ||= Renderer.render((self[:subject] || self[:title] || UNTITLED_EMAIL), self) end
Returns a map where the strings are an original string and the value is what should replace it - used for automating common replacements - e.g. trademark symbols or apostrophes going in the wrong direction
# File lib/inkcite/view.rb, line 582 def substitution_map if @substitutions.nil? # Initialize the substitutions map which will be populated # if the local file exists. @substitutions = {} # Preload the array of footnotes if they exist substitutions_tsv_file = @email.project_file(SUBSTITUTIONS_TSV_FILE) if File.exist?(substitutions_tsv_file) CSV.foreach(substitutions_tsv_file, { :col_sep => "\t" }) do |fn| original = fn[0] @substitutions[original] = fn[1].to_s unless original.blank? end end end @substitutions end
# File lib/inkcite/view.rb, line 604 def tag_stack tag @tag_stack ||= Hash.new() @tag_stack[tag] ||= TagStack.new(tag, self) end
Sends this version of the email to Litmus for testing.
# File lib/inkcite/view.rb, line 614 def test! EmailTest.test! self end
# File lib/inkcite/view.rb, line 618 def text? @format == :text end
# File lib/inkcite/view.rb, line 609 def title @title ||= Renderer.render((self[:title] || UNTITLED_EMAIL), self) end
# File lib/inkcite/view.rb, line 622 def track_links? !self[Email::TRACK_LINKS].blank? end
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.
# File lib/inkcite/view.rb, line 629 def unique_id key @unique_ids ||= Hash.new(0) @unique_ids[key] += 1 end
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.
# File lib/inkcite/view.rb, line 636 def vml_enabled? email? && !is_disabled?(:vml) end
Signifies that VML was used during the rendering and that
# File lib/inkcite/view.rb, line 641 def vml_used! raise 'VML was used but is not enabled' unless vml_enabled? @vml_used = true end
# File lib/inkcite/view.rb, line 646 def vml_used? @vml_used == true end
# File lib/inkcite/view.rb, line 650 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
# File lib/inkcite/view.rb, line 662 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
Private Instance Methods
# File lib/inkcite/view.rb, line 729 def assert_failsafes passes = true failsafes = self[:failsafe] || self[:failsafes] unless failsafes.blank? _includes = failsafes[:includes] [*_includes].each do |rule| if !content_matches?(rule) error "Failsafe! Email does not include \"#{rule}\"" passes = false end end _excludes = failsafes[:excludes] [*_excludes].each do |rule| if content_matches?(rule) error("Failsafe! Email must not include \"#{rule}\"") passes = false end end end passes end
# File lib/inkcite/view.rb, line 725 def assert_in_browser msg raise msg if email? && !development? end
# File lib/inkcite/view.rb, line 757 def body_declaration # 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. body = '<body' # The body class is used to fix the width problem in new Gmail iOS. # https://litmus.com/community/discussions/5913-new-gmail-app-not-respect-full-width body << ' class="body"' if email? body << ' style="margin: 0; 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? body << %Q( background: #ccc url('data:image/png;base64,#{Inkcite.blueprint_image64}');) end body << '">' body end
Returns true if the content in this email either matches the regular expression provided or if it includes the exact string that is provided.
# File lib/inkcite/view.rb, line 785 def content_matches? rule # Check to see if the failsafe rule is a regular expression. if rule[0, 1] == REGEX_SLASH && rule[-1, 1] == REGEX_SLASH @content.match(Regexp.new(rule[1..-2])) else @content.include?(rule) end end
# File lib/inkcite/view.rb, line 794 def external_scripts html = [] self.scripts.each do |js| if js.is_a?(URI::HTTP) assert_in_browser 'External scripts prohibited in emails' html << "<script src=\"#{js.to_s}\"></script>" end end html end
# File lib/inkcite/view.rb, line 807 def external_styles html = [] self.styles.each do |css| if css.is_a?(URI::HTTP) assert_in_browser 'External stylesheets prohibited in emails' html << "<link href=\"#{css.to_s}\" rel=\"stylesheet\">" end end html end
# File lib/inkcite/view.rb, line 820 def from_uri uri if uri.is_a?(URI) if uri.scheme == FILE_SCHEME # e.g. file://facebook-like.js return Util.read(ASSETS, uri.host) else raise "Unsupported URI scheme: #{uri.to_s}" end end # Otherwise, return the string which is assumed to be already uri end
# File lib/inkcite/view.rb, line 839 def inline_google_fonts css = '' # Google Web Fonts support courtesy of # http://www.emaildesignreview.com/html-email-coding/web-fonts-in-email-1482/ font_urls = self[:fonts] unless font_urls.blank? require 'open-uri' # Load the previously cached font, if any font_cache_path = @email.project_file(FONT_CACHE_FILE) font_cache = Util.read_yml(font_cache_path, :symbolize_keys => false) # True if the cache needs to be updated because a new URL was # added to it. updated = false # If you use @font-face in HTML email, Outlook 07/10/13 will default all # text back to Times New Roman. # http://www.emaildesignreview.com/html-email-coding/web-fonts-in-email-1482/ css << '@media screen {' # Iterate through the configured fonts. Check to see if we've already cached # Google's response. If not, retrieve it and add it to the font_urls.each do |url| if font_cache[url].blank? begin font_cache[url] = open(url).read updated = true rescue Exception => error error 'Unable to load Google Web Font', { :url => url, :error => error.inspect } next end end css << font_cache[url] end css << '}' # If the fontcache was updated, update it in our sekret file. File.write(font_cache_path, font_cache.to_yaml) if updated end css end
# File lib/inkcite/view.rb, line 910 def inline_reset_styles reset = [] if email? # Forces Hotmail to display emails at full width reset << '.ExternalClass, .ReadMsgBody { width:100%; }' # Forces Hotmail to display normal line spacing, here is more on that: # http://www.emailonacid.com/forum/viewthread/43/ reset << '.ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div { line-height: 100%; }' # Not sure where I got this fix from. reset << '#outlook a { padding: 0; }' # Body text color for the New Yahoo. reset << '.yshortcuts, .yshortcuts a, .yshortcuts a:link,.yshortcuts a:visited, .yshortcuts a:hover, .yshortcuts a span { color: black; text-decoration: none !important; border-bottom: none !important; background: none !important; }' # Hides 'Today' ads in Yahoo! # https://litmus.com/blog/hiding-today-ads-yahoo?utm_source=newsletter&utm_medium=email&utm_campaign=april2012news */ reset << 'XHTML-STRIPONREPLY { display:none; }' # This resolves the Outlook 07, 10, and Gmail td padding issue. Here's more info: # http://www.ianhoar.com/2008/04/29/outlook-2007-borders-and-1px-padding-on-table-cells # http://www.campaignmonitor.com/blog/post/3392/1px-borders-padding-on-table-cells-in-outlook-07 reset << 'table { border-spacing: 0; }' reset << 'table, td { border-collapse: collapse; mso-table-lspace: 0pt !important; mso-table-rspace: 0pt !important; }' # Ensure that telephone numbers are displayed using the same style as links. reset << "a[href^=tel] { color: #{self[Renderer::Base::LINK_COLOR]}; text-decoration:none;}" # Remove extraneous left-margins on Android 4.4 # https://litmus.com/community/code/4194-why-is-email-not-centered-on-android-4-4#comment-5727 reset << 'div[style*="margin: 16px 0"] { margin:0 !important; }' end # Reset the font on every cell to the default family. reset << "td { font-family: #{self[Renderer::Base::FONT_FAMILY]}; }" # Allow smoother rendering of resized image in Internet Explorer reset << "img { -ms-interpolation-mode:bicubic !important; }" reset.join(NEW_LINE) end
# File lib/inkcite/view.rb, line 888 def inline_scripts code = '' self.scripts.each do |js| next if js.is_a?(URI::HTTP) # Check to see if we've received a URI to a local asset file or if it's just javascript # to be included in the file. code << from_uri(js) end unless code.blank? assert_in_browser 'Scripts prohibited in emails' code = Minifier.js(code, self) code = "<script>\n#{code}\n</script>" end code end
# File lib/inkcite/view.rb, line 957 def inline_styles # Separating <style> blocks to prevent Gmail from filtering # the entire CSS section because of its strict parsing. # https://emails.hteumeuleu.com/troubleshooting-gmails-responsive-design-support-ad124178bf81#.khhj4u4b5 style_blocks = [] # Add a section for the bootstrap/reset styles common to every email # that address common rendering issues in many clients. style_blocks << inline_reset_styles # Web font styles need to be in their own block because # they have multiple @ signs which Gmail doesn't like. style_blocks << inline_google_fonts # Responsive styles. style_blocks << @media_query.to_css unless @media_query.blank? # Filter the URI-based styles and add the remaining styles as a # separate block. Possibly need to consider making these style # blocks separate in the future - e.g. special effects blocks # should probably be separated since there is a high likelihood # of @ within @ style_blocks << self.styles.select { |s| !s.is_a?(URI::HTTP) }.join(NEW_LINE) # Filter empty style blocks style_blocks.select! { |s| !s.blank? } # Minify each of the blocks style_blocks.each { |s| Minifier.css(s, self) } # Join all of the style blocks into a single block if this # is not an email - otherwise, keep them separate. style_blocks = [style_blocks.join(NEW_LINE)] unless email? html = [] # Iterate through the style blocks and append them style_blocks.each do |s| html << '<style>' html << s html << '</style>' end html.join(NEW_LINE) end
# File lib/inkcite/view.rb, line 1053 def load_google_sheets_helpers url, into # Add a timestamp to break google's cache and force the page to # be reloaded each time url = Util::add_query_param(url, Time.now.to_i) raw = Net::HTTP.get(URI.parse(url)) # Consolidate line-breaks for simplicity raw.gsub!(/[\r\f\n]{1,}/, NEW_LINE) raw.gsub!(/ \& /, " & ") fields = nil raw.split(NEW_LINE).each do |line| columns = line.split(TAB) if fields.nil? fields = columns.collect(&:to_sym) else version = columns[0] # Check to see if this version includes an asterisk is_any_version = if version == '*' true elsif version.include?('*') prefix, suffix = version.split('*') (prefix.blank? || @version.to_s.start_with?(prefix)) && (suffix.blank? || @version.to_s.end_with?(suffix)) else false end is_version = version.to_sym == @version if is_any_version || is_version fields.each_with_index do |field, index| unless index == 0 value = columns[index] into[field] = value unless value.blank? end end break if is_version end end end true end
# File lib/inkcite/view.rb, line 1106 def load_helper_file file, into, abort_on_fail=true unless File.exist?(file) error("Unable to load helper file", :file => file) if abort_on_fail return end # Consolidate line-breaks for simplicity raw = File.read(file) raw.gsub!(/[\r\f\n]{1,}/, NEW_LINE) # Initial position of the multiline_starts_at = 0 # Determine if there are any multiline declarations - those that are wrapped with # <<-START and END->> and reduce them to single line declarations. while (multiline_starts_at = raw.index(MULTILINE_START, multiline_starts_at)) break unless (multiline_ends_at = raw.index(MULTILINE_END, multiline_starts_at)) declaration = raw[(multiline_starts_at+MULTILINE_START.length)..multiline_ends_at - 1] declaration.strip! declaration.gsub!(/\t/, TAB_TO_SPACE) declaration.gsub!(/\n/, "\r") raw[multiline_starts_at..multiline_ends_at+MULTILINE_END.length - 1] = declaration end raw.split(NEW_LINE).each do |line| next if line.starts_with?(COMMENT) line.gsub!(/\r/, NEW_LINE) line.strip! key, open, close = line.split(TAB) next if key.blank? into[key.to_sym] = open.to_s.freeze # Prepend the key with a "/" and populate the closing tag. into["/#{key}".to_sym] = close.to_s.freeze end true end
Reads the helpers.tsv and any version-specific override (e.g. helpers-owners.tsv)
# File lib/inkcite/view.rb, line 1155 def load_helpers _helpers = { :n => NEW_LINE } # Get the project path from which most helpers will be loaded. path = @email.path # First load the built-in helpers. load_helper_file(File.join(Inkcite.asset_path, 'builtin-helpers.tsv'), _helpers, false) # Check to see if the config.yml includes a "helpers:" array which allows # additional out-of-project, shared helper files to be loaded. included_helpers = [*@email.config[:helpers]] included_helpers.each do |file| if file.start_with?('http') load_google_sheets_helpers file, _helpers else load_helper_file(File.join(path, file), _helpers) end end # Load the project's properties, which may include references to additional # properties in other directories. load_helper_file File.join(path, 'helpers.tsv'), _helpers # Look for a version-specific override allowing restyling of an email based # on its version - e.g. difference colors in the "no orders for 30 days" email. load_helper_file File.join(path, "helpers-#{@version}.tsv"), _helpers, false project_path = File.basename(@email.path) # If the project directory name is all numbers and is either six or eight # characters in length, assume it is a date - e.g. 201706 or 20170513. # Automatically extract the year, month and day (optional). if project_path.to_i.to_s == project_path path_length = project_path.length if path_length >= 4 && path_length <= 8 # These substrings need to remain strings so that properties can be # cloned when a view is instanted. _helpers[:yyyy] = project_path[0..3] _helpers[:mm] = project_path[4..5] if path_length >= 6 _helpers[:dd] = project_path[6..7] if path_length == 8 end end # As a convenience pre-populate the month name of the email. mm = _helpers[:mm].to_i if mm > 0 _helpers[:month] = Date::MONTHNAMES[mm] _helpers[:MONTH] = _helpers[:month].upcase end _helpers end
Retrieves the version-specific meta data for this view.
# File lib/inkcite/view.rb, line 1215 def meta_data @email.meta version end
# File lib/inkcite/view.rb, line 1004 def outlook_high_dpi_fix # This is the XML required to ensure high-DPI Outlook plays nicely with the # email when it is displayed. # https://www.emailonacid.com/blog/article/email-development/dpi_scaling_in_outlook_2007-2013 html = [] html << '<!--[if gte mso 9]>' html << '<xml>' html << '<o:OfficeDocumentSettings>' html << '<o:AllowPNG/>' html << '<o:PixelsPerInch>96</o:PixelsPerInch>' html << '</o:OfficeDocumentSettings>' html << '</xml>' html << '<![endif]-->' return html.join(NEW_LINE) end
# File lib/inkcite/view.rb, line 1022 def outlook_styles html = [] html << '<!--[if mso]>' if email? html << '<style>' if email? # These are the Outlook-specific styles necessary to block the # visited link from changing color in Outlook 2007-2013. # https://litmus.com/community/discussions/4164-outlook-07-13-visited-link-color-fix vlink_styles = { :'mso-style-priority' => 99, :color => 'inherit' } %w(MsoHyperlink MsoHyperlinkFollowed).each do |l| html << Inkcite::Renderer::Style.new("span.#{l}", self, vlink_styles).to_s end end # VML-specific CSS needed only if VML was used in the email. if vml_used? %w(v o).each do |l| html << Inkcite::Renderer::Style.new("#{l}\:*", self, { :behavior => 'url(#default#VML)', :display => 'inline-block' }).to_s end end html << '</style>' html << '<![endif]-->' if email? html.join(NEW_LINE) end
# File lib/inkcite/view.rb, line 1219 def prevent_ios_date_detection raw # Currently always performed in email but may want a configuration setting # that allows a creator to disable this default functionality. enabled = email? if enabled # Prevent dates (e.g. "February 28") from getting turned into unsightly blue # links on iOS by putting non-rendering whitespace within. # http://www.industrydive.com/blog/how-to-fix-email-marketing-for-iphone-ipad/ Date::MONTHNAMES.select { |mon| !mon.blank? }.each do |mon| # Look for full month names (e.g. February) and add a zero-width space # afterwards which prevents iOS from detecting said date. raw.gsub!(/#{mon}/, "#{mon.dup.insert(1, Renderer::Base::ZERO_WIDTH_NON_BREAKING_SPACE)}") end end enabled end
# File lib/inkcite/view.rb, line 1242 def render_each filtered lines = [] filtered.split("\n").each do |line| begin line = Renderer.render(line, self) rescue Exception => e error e.message, { :trace => e.backtrace.first.gsub(/%2F/, '/') } end if text? # No additional splitting should be performed on the text version of the email. # Otherwise blank lines are lost. lines << line else # Sometimes the renderer inserts additional new-lines so we need to split them out # into individual lines if necessary. Push all of the lines onto the issue's line array. lines += line.split(NEW_LINE) end end lines end