Module: AlEmailProtect

Defined in:
lib/al_email_protect.rb,
lib/al_email_protect/version.rb

Overview

Keeps published email addresses away from harvesters.

The scraper-facing half is deliberately server-side: when the feature is on, no mailto: string and no user@host string is present in the HTML at all. Address harvesters read markup, not JavaScript, so an approach that ships the plaintext and merely rewrites it on DOMContentLoaded protects nobody. The address is emitted as two halves and only rejoined in the browser.

Defined Under Namespace

Modules: Filters Classes: AssetsGenerator, LinkTag, PluginStaticFile, ScriptsTag, StylesTag

Constant Summary collapse

PLUGIN_ROOT =
File.expand_path("..", __dir__)
LIB_ROOT =

Jekyll writes a StaticFile to /

/, where is relative to the base below. That base must be lib/, not the gem root, or assets land at /lib/assets/... while every tag in this file points at /assets/... .

__dir__
ASSETS_ROOT =
File.join(LIB_ROOT, "assets")
ANCHOR =

cv.email | al_email_obfuscate } Rewrites every mailto: anchor in a fragment of already-rendered HTML.

This exists because the markup that needs protecting is frequently not ours. al-folio's social links come from the third-party jekyll-socials gem, which owns the whole block and emits mailto:%s for the email entry — there is no hook to render that one link differently. Capturing its output and rewriting it afterwards is the only interception point, and it works regardless of which gem produced the markup.

Deliberately narrow: it matches an whose href begins with mailto: and replaces the whole element. It does not attempt to parse HTML generally.

%r{<a\b([^>]*?)href=(["'])mailto:([^"']+)\2([^>]*)>(.*?)</a>}im.freeze
VERSION =
"1.0.1"

Class Method Summary collapse

Class Method Details

.asset_entriesObject

[relative_dir, filename] for everything this gem publishes. Exposed so the destination path can be asserted without booting a full Jekyll site.



73
74
75
76
77
# File 'lib/al_email_protect.rb', line 73

def asset_entries
  Dir.glob(File.join(ASSETS_ROOT, "**", "*")).sort.reject { |p| File.directory?(p) }.map do |source_path|
    [File.dirname(source_path).sub("#{LIB_ROOT}/", ""), File.basename(source_path)]
  end
end

.decode_target(value) ⇒ Object

mailto: targets are commonly percent-encoded (jekyll-email-protect's encode_email does exactly that), so decode before splitting or the local and domain halves come out as escape sequences.



167
168
169
170
171
# File 'lib/al_email_protect.rb', line 167

def decode_target(value)
  CGI.unescape(value.to_s.split("?").first.to_s)
rescue StandardError
  value.to_s
end

.domain_markup(domain) ⇒ Object

Builds "example [dot] com" as alternating styled spans.



66
67
68
69
# File 'lib/al_email_protect.rb', line 66

def domain_markup(domain)
  separator = %(<span class="al-email-sep"> [dot] </span>)
  domain.split(".").map { |label| %(<span class="al-email-text">#{escape(label)}</span>) }.join(separator)
end

.enabled?(site) ⇒ Boolean

Returns:

  • (Boolean)


27
28
29
30
31
# File 'lib/al_email_protect.rb', line 27

def enabled?(site)
  return false unless site

  site.config["protect_email"] == true
end

.escape(value) ⇒ Object



61
62
63
# File 'lib/al_email_protect.rb', line 61

def escape(value)
  CGI.escapeHTML(value.to_s)
end

.obfuscate_text(value) ⇒ Object

"someone [at] example [dot] com", for places that show an address as text rather than as a link (CV contact blocks, for instance).



53
54
55
56
57
58
59
# File 'lib/al_email_protect.rb', line 53

def obfuscate_text(value)
  parts = split_address(value)
  return value.to_s unless parts

  local, domain = parts
  "#{local} [at] #{domain.gsub(".", " [dot] ")}"
end

.rewrite_html(html) ⇒ Object



173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/al_email_protect.rb', line 173

def rewrite_html(html)
  html.to_s.gsub(ANCHOR) do
    before = Regexp.last_match(1)
    after = Regexp.last_match(4)
    address = decode_target(Regexp.last_match(3))
    label = Regexp.last_match(5)
    parts = split_address(address)

    # Leave anything that is not a usable address exactly as it was; a
    # half-rewritten contact link is worse than an unprotected one.
    next Regexp.last_match(0) unless parts

    local, domain = parts
    attributes = "#{before}#{after}".gsub(/\s+/, " ").strip
    # The visible text often *is* the address. Replace it with the split
    # rendering, or the plaintext survives in the markup and the rewrite
    # achieves nothing.
    inner = label.to_s.include?("@") ? "#{escape(local)}<span class=\"al-email-sep\"> [at] </span>#{domain_markup(domain)}" : label

    %(<a href="#" class="al-email-protect #{attributes}" data-eu="#{escape(local)}" data-ed="#{escape(domain)}">#{inner}</a>)
  end
end

.split_address(value) ⇒ Object

Splits "someone@example.ac.uk" into ["someone", "example.ac.uk"].

Splits on the LAST "@" because the local part of an address may legally contain one when quoted (RFC 5321), and getting this backwards would emit a broken address rather than a protected one. Returns nil for anything not usable as an address, so callers can fall back to rendering it verbatim instead of silently publishing a mangled contact.



40
41
42
43
44
45
46
47
48
49
# File 'lib/al_email_protect.rb', line 40

def split_address(value)
  address = value.to_s.strip
  return nil if address.empty?

  local, _, domain = address.rpartition("@")
  return nil if local.empty? || domain.empty?
  return nil unless domain.include?(".")

  [local, domain]
end