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).

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

config[RW]

The configuration hash for the view

content[R]

The rendered html or content available after render! has been called.

email[R]

The base Email object this is a view of

environment[R]

One of :development, :preview or :production

errors[RW]

The array of error messages collected during rendering

format[R]

The format of the email (e.g. :email or :text)

js_compressor[RW]

Will be populated with the css and js compressor objects after first use. Ensures we can reset the compressors after a rendering is complete.

last_rendered_markup[RW]

The most recently processed tag populated from the renderer and used when reporting errors.

media_query[R]

Manages the Responsive::Rules applied to this email view.

post_processors[RW]

Array of post-processors that can modify the HTML of the email after it is rendered but before it is saved.

version[R]

The version of the email (e.g. :default)

Public Class Methods

new(email, environment, format, version) click to toggle source
# 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

[](key) click to toggle source
# 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
assert_image_exists(src) click to toggle source

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
browser?() click to toggle source
# File lib/inkcite/view.rb, line 130
def browser?
  @format == :browser
end
data() click to toggle source

Arbitrary storage of data

# File lib/inkcite/view.rb, line 135
def data
  @data ||= {}
end
default?() click to toggle source
# File lib/inkcite/view.rb, line 139
def default?
  @version == :default
end
development?() click to toggle source
# File lib/inkcite/view.rb, line 143
def development?
  @environment == :development
end
email?() click to toggle source
# File lib/inkcite/view.rb, line 147
def email?
  @format == :email
end
error(message, obj={}) click to toggle source

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
eval_erb(source, file_name) click to toggle source
# 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_name(ext=nil) click to toggle source
# 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
footnotes() click to toggle source
# 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
helper(tag, open, close=nil) click to toggle source

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
image_url(src) click to toggle source

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
images_off=(off) click to toggle source
# File lib/inkcite/view.rb, line 301
def images_off= off
  @config[IMAGES_OFF] = !!off
end
images_off?() click to toggle source
# File lib/inkcite/view.rb, line 297
def images_off?
  is_enabled?(IMAGES_OFF)
end
is_disabled?(key) click to toggle source

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
is_enabled?(key) click to toggle source

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
meta(key) click to toggle source
# File lib/inkcite/view.rb, line 356
def meta key
  md = meta_data
  md.nil? ? nil : md[key]
end
once?(key) click to toggle source

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
parent_opts(tag) click to toggle source

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
prefixes() click to toggle source

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
preview?() click to toggle source
# File lib/inkcite/view.rb, line 397
def preview?
  @environment == :preview
end
production?() click to toggle source
# File lib/inkcite/view.rb, line 401
def production?
  @environment == :production
end
read_source(source_file) click to toggle source

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
render!() click to toggle source
# 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!('&#62;', '>')
  @content.gsub!('&#60;', '<')

  # 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?() click to toggle source
# File lib/inkcite/view.rb, line 552
def rendered?
  !@content.blank?
end
scripts() click to toggle source
# File lib/inkcite/view.rb, line 556
def scripts
  @scripts ||= []
end
set_meta(key, value) click to toggle source
# 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
styles() click to toggle source
# File lib/inkcite/view.rb, line 570
def styles
  @styles ||= []
end
subject() click to toggle source
# File lib/inkcite/view.rb, line 574
def subject
  @subject ||= Renderer.render((self[:subject] || self[:title] || UNTITLED_EMAIL), self)
end
substitution_map() click to toggle source

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
tag_stack(tag) click to toggle source
# File lib/inkcite/view.rb, line 604
def tag_stack tag
  @tag_stack ||= Hash.new()
  @tag_stack[tag] ||= TagStack.new(tag, self)
end
test!() click to toggle source

Sends this version of the email to Litmus for testing.

# File lib/inkcite/view.rb, line 614
def test!
  EmailTest.test! self
end
text?() click to toggle source
# File lib/inkcite/view.rb, line 618
def text?
  @format == :text
end
title() click to toggle source
# File lib/inkcite/view.rb, line 609
def title
  @title ||= Renderer.render((self[:title] || UNTITLED_EMAIL), self)
end
unique_id(key) click to toggle source

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
vml_enabled?() click to toggle source

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
vml_used!() click to toggle source

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
vml_used?() click to toggle source
# File lib/inkcite/view.rb, line 646
def vml_used?
  @vml_used == true
end
write(out) click to toggle source
# 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

Private Instance Methods

assert_failsafes() click to toggle source
# 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
assert_in_browser(msg) click to toggle source
# File lib/inkcite/view.rb, line 725
def assert_in_browser msg
  raise msg if email? && !development?
end
body_declaration() click to toggle source
# 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
content_matches?(rule) click to toggle source

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
external_scripts() click to toggle source
# 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
external_styles() click to toggle source
# 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
from_uri(uri) click to toggle source
# 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
inline_google_fonts() click to toggle source
# 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
inline_reset_styles() click to toggle source
# 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
inline_scripts() click to toggle source
# 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
inline_styles() click to toggle source
# 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
load_google_sheets_helpers(url, into) click to toggle source
# 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!(/ \& /, " &amp; ")

  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
load_helper_file(file, into, abort_on_fail=true) click to toggle source
# 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
load_helpers() click to toggle source

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
meta_data() click to toggle source

Retrieves the version-specific meta data for this view.

# File lib/inkcite/view.rb, line 1215
def meta_data
  @email.meta version
end
outlook_high_dpi_fix() click to toggle source
# 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
outlook_styles() click to toggle source
# 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
prevent_ios_date_detection(raw) click to toggle source
# 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
render_each(filtered) click to toggle source
# 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