Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
4044789
refacto: rebuild Inky on an extensible component registry with modern…
PhilMeyr Jun 10, 2026
7df523e
chore: add Sorbet (strict lib), modern Rubocop config, and minitest R…
PhilMeyr Jun 10, 2026
3f960b9
test: port the suite to minitest with golden-file coverage per component
PhilMeyr Jun 10, 2026
bad2e6d
docs: rewrite README for v2, add CHANGELOG and GitHub Actions CI
PhilMeyr Jun 10, 2026
4142b05
chore: trim gemspec metadata and ship only lib in the packaged gem
PhilMeyr Jun 10, 2026
41cf541
fix: stop String#sub backreference expansion when re-injecting raw bl…
PhilMeyr Jun 10, 2026
c33eb6d
fix: escape double quotes in passed-through attribute values
PhilMeyr Jun 10, 2026
c1753d1
fix: clamp column ghost width and neutralize th defaults on content cell
PhilMeyr Jun 10, 2026
7da371b
refacto: make the install generator foundation-agnostic
PhilMeyr Jun 10, 2026
e33ca98
test: cover the Rails template handler path and malformed HTML inputs
PhilMeyr Jun 10, 2026
1e692b2
docs: align README/CHANGELOG with generated markup and run lint CI on…
PhilMeyr Jun 10, 2026
0d099b8
fix: require uri before action_view in template handler test
PhilMeyr Jun 11, 2026
84a549c
fix: normalize component registry keys to String
PhilMeyr Jun 11, 2026
627aadd
fix: merge author style into component layout style instead of duplic…
PhilMeyr Jun 11, 2026
b052ba7
fix: stop stripping newlines from component inner content
PhilMeyr Jun 11, 2026
44211ed
refacto: extract shared TABLE_RESET constant for layout table attributes
PhilMeyr Jun 11, 2026
aea8a43
refacto: drop production-code Rubocop excludes, fix the code instead
PhilMeyr Jun 11, 2026
314a9f2
fix: route every interpolated attribute value through a shared escape…
PhilMeyr Jun 11, 2026
ef35af3
fix: validate component classes at registration time with actionable …
PhilMeyr Jun 11, 2026
fa0deca
test: close coverage gaps flagged in external review
PhilMeyr Jun 11, 2026
ab61a58
fix: scrub invalid bytes when coercing binary input to UTF-8
PhilMeyr Jun 11, 2026
ae58838
refacto: split long markup literals, drop LineLength exclude on compo…
PhilMeyr Jun 11, 2026
86f7e23
feat: surface HTML parse errors instead of silent libxml2 recovery
PhilMeyr Jun 11, 2026
bdd7177
fix: validate numeric author attributes instead of to_i coercion
PhilMeyr Jun 11, 2026
3c1679d
test: negative assertions on consumed-attribute passthrough
PhilMeyr Jun 11, 2026
210715e
fix: harden attribute escaping to full HTML entity set
PhilMeyr Jun 11, 2026
c540be7
refacto: rename Core#components to component_instances, tighten nits
PhilMeyr Jun 11, 2026
6170aa1
refacto: raise Rails template handler to typed: strict
PhilMeyr Jun 11, 2026
3eeefdd
fix: raise when compose_with does not resolve to a template handler
PhilMeyr Jun 11, 2026
89832b2
fix: scrub invalid bytes regardless of claimed encoding
PhilMeyr Jun 11, 2026
d4db053
test: lock the libxml2 unknown-tag message format the filter parses
PhilMeyr Jun 11, 2026
326486c
fix: freeze the components getter to keep validation airtight
PhilMeyr Jun 11, 2026
2b9af1d
fix: reject non-positive column_count and container_width
PhilMeyr Jun 11, 2026
fc2d258
fix: align Haml/Slim generated layouts with the ERB MSO settings block
PhilMeyr Jun 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: CI

on:
push:
branches: [develop, master, main]
pull_request:

jobs:
test:
name: Ruby ${{ matrix.ruby }} / Rails ${{ matrix.rails }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
ruby: ['3.2', '3.3', '3.4']
rails: ['7.1', '8.0', '8.1']
include:
- ruby: '4.0'
rails: '8.1'
env:
RAILS_VERSION: "~> ${{ matrix.rails }}.0"
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true
- name: Run tests
run: bundle exec rake test

lint:
name: Rubocop and Sorbet
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
ruby-version: '4.0'
bundler-cache: true
- name: Rubocop
run: bundle exec rubocop
- name: Sorbet
run: bundle exec srb tc
7 changes: 3 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
*.swp
pkg
spec/_cases_output
spec/test_app/log/*.log
spec/test_app/tmp/
/pkg/
/tmp/
Gemfile.lock
sorbet/tapioca/
175 changes: 31 additions & 144 deletions .rubocop.yml
Original file line number Diff line number Diff line change
@@ -1,160 +1,47 @@
AllCops:
DisplayCopNames: true
TargetRubyVersion: 2.2
# Relaxed.Ruby.Style

Style/Alias:
Enabled: false
StyleGuide: http://relaxed.ruby.style/#stylealias

Style/BeginBlock:
Enabled: false
StyleGuide: http://relaxed.ruby.style/#stylebeginblock
TargetRubyVersion: 3.2
NewCops: enable
SuggestExtensions: false
Exclude:
- 'sorbet/**/*'
- 'vendor/**/*'
- 'tmp/**/*'
- 'lib/generators/inky/templates/**/*'

Style/BlockDelimiters:
Enabled: false
StyleGuide: http://relaxed.ruby.style/#styleblockdelimiters
Layout/LineLength:
Max: 160
Exclude:
- 'test/**/*'

Style/Documentation:
Enabled: false
StyleGuide: http://relaxed.ruby.style/#styledocumentation

Layout/DotPosition:
Enabled: false
StyleGuide: http://relaxed.ruby.style/#styledotposition

Style/DoubleNegation:
Enabled: false
StyleGuide: http://relaxed.ruby.style/#styledoublenegation

Style/EndBlock:
Enabled: false
StyleGuide: http://relaxed.ruby.style/#styleendblock

Style/FormatString:
Enabled: false
StyleGuide: http://relaxed.ruby.style/#styleformatstring

Style/IfUnlessModifier:
Enabled: false
StyleGuide: http://relaxed.ruby.style/#styleifunlessmodifier

Style/Lambda:
Enabled: false
StyleGuide: http://relaxed.ruby.style/#stylelambda

Style/ModuleFunction:
Enabled: false
StyleGuide: http://relaxed.ruby.style/#stylemodulefunction

Style/MultilineBlockChain:
Enabled: false
StyleGuide: http://relaxed.ruby.style/#stylemultilineblockchain

Style/NegatedIf:
Enabled: false
StyleGuide: http://relaxed.ruby.style/#stylenegatedif

Style/NegatedWhile:
Enabled: false
StyleGuide: http://relaxed.ruby.style/#stylenegatedwhile

Style/ParallelAssignment:
Enabled: false
StyleGuide: http://relaxed.ruby.style/#styleparallelassignment

Style/PercentLiteralDelimiters:
Enabled: false
StyleGuide: http://relaxed.ruby.style/#stylepercentliteraldelimiters

Style/PerlBackrefs:
Enabled: false
StyleGuide: http://relaxed.ruby.style/#styleperlbackrefs

Style/Semicolon:
Enabled: false
StyleGuide: http://relaxed.ruby.style/#stylesemicolon

Style/SignalException:
Enabled: false
StyleGuide: http://relaxed.ruby.style/#stylesignalexception

Style/SingleLineBlockParams:
Enabled: false
StyleGuide: http://relaxed.ruby.style/#stylesinglelineblockparams

Style/SingleLineMethods:
Enabled: false
StyleGuide: http://relaxed.ruby.style/#stylesinglelinemethods

Layout/SpaceBeforeBlockBraces:
Enabled: false
StyleGuide: http://relaxed.ruby.style/#stylespacebeforeblockbraces

Layout/SpaceInsideParens:
Enabled: false
StyleGuide: http://relaxed.ruby.style/#stylespaceinsideparens

Style/SpecialGlobalVars:
Enabled: false
StyleGuide: http://relaxed.ruby.style/#stylespecialglobalvars

Style/StringLiterals:
Enabled: false
StyleGuide: http://relaxed.ruby.style/#stylestringliterals

Style/WhileUntilModifier:
Enabled: false
StyleGuide: http://relaxed.ruby.style/#stylewhileuntilmodifier

Lint/AmbiguousRegexpLiteral:
Enabled: false
StyleGuide: http://relaxed.ruby.style/#lintambiguousregexpliteral

Lint/AssignmentInCondition:
Enabled: false
StyleGuide: http://relaxed.ruby.style/#lintassignmentincondition

Metrics/AbcSize:
Enabled: false
Style/FrozenStringLiteralComment:
Enabled: true

Metrics/BlockNesting:
Enabled: false
Metrics/BlockLength:
Exclude:
- 'test/**/*'
- 'inky.gemspec'

# Test files: autofix only, relaxed metrics.
Metrics/ClassLength:
Enabled: false

Metrics/ModuleLength:
Enabled: false

Metrics/CyclomaticComplexity:
Enabled: false

Metrics/LineLength:
Enabled: false
Exclude:
- 'test/**/*'

Metrics/MethodLength:
Enabled: false

Metrics/ParameterLists:
Enabled: false

Metrics/PerceivedComplexity:
Enabled: false

# inky-rb gem specifics
Lint/HandleExceptions:
Exclude:
- 'Rakefile'
- 'lib/inky.rb'
- 'test/**/*'

Metrics/BlockLength:
Metrics/AbcSize:
Exclude:
- 'Rakefile'
- 'spec/**/*.rb'
- 'test/**/*'

Style/MutableConstant:
Enabled: false
Naming/VariableNumber:
Exclude:
- 'test/**/*'

Style/Encoding:
Enabled: false
# Test classes share a file by component; that is intentional grouping.
Style/ClassAndModuleChildren:
Exclude:
- 'test/**/*'
18 changes: 0 additions & 18 deletions .travis.yml

This file was deleted.

77 changes: 77 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Changelog

All notable changes to this project are documented here.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.0.0] - 2026-06-11

A full modernization. v2 is a generalist gem usable by any Rails project, with
markup updated to 2026 email best practices.

### Added

- **Extensible component registry (open/closed).** Each component is now a class
(`Inky::Components::*`) inheriting from `Inky::Components::Base`. Register your
own tags with `Inky.configuration.register_component('my-tag', MyComponent)`;
custom components receive the matched Nokogiri node and full DOM access.
- **`role="presentation"` on every generated layout table** for accessibility.
- **MSO ghost tables/cells** around `<container>`, `<row>`/`<columns>`, so the
fluid-hybrid layout still renders as a grid in Outlook (Word engine).
- **`container_width` configuration** (default `600`), settable globally or per
`Inky::Core.new(container_width:)`.
- **Bulletproof `<button>`**: padding carried by the `<a>` so the whole button is
clickable.
- **`mso-line-height-rule:exactly`** on `<spacer>` to stop Outlook inflating it.
- **Multi-line `<raw>` support** (upstream PR #101).
- **Sorbet `# typed: strict`** across `lib/`, with full signatures.
- **Minitest suite** with golden-markup assertions (structural comparison:
whitespace, attribute and class ordering normalized) for every component, plus
error and edge cases and the Rails template-handler integration path.
- **GitHub Actions CI**: Ruby 3.2/3.3/3.4/4.0 × Rails 7.1/8.0/8.1.

### Changed

- **Layout is now fluid-hybrid.** `<columns>` use `display:inline-block` with a
pixel `max-width` so columns stack naturally on small screens without a media
query, and are restored to a grid in Outlook via ghost cells. The `small-*`,
`large-*`, `first`, `last` classes are preserved for media-query enhancement.
Ghost-cell widths are `container_width × large / column_count`, capped at
`container_width`, with **no gutter model** (unlike 1.x where gutters came from
Foundation's CSS padding) — add padding inside columns for gutters.
- Column content cells carry an inline `font-weight:normal;text-align:left`
reset, replacing the equivalent rule foundation-emails applied via CSS.
- The install generator no longer emits Foundation's SCSS; it generates a
neutral modern mailer layout (MSO settings, color-scheme metas, `role="article"`).
- All generated tables carry explicit `border="0" cellpadding="0" cellspacing="0"`
and inline `style` (no reliance on `!important` or `border-radius`, both stripped
by Orange.fr webmail).
- Component classes are keyed by tag string in the registry; the constructor
`components:` option now maps tag strings to component classes.

### Removed

- **`foundation_emails` runtime dependency.** The gem no longer ships Foundation's
SCSS; styling is the application's responsibility (inline critical CSS, plus a
`<style>` block for media-query/dark-mode enhancement). To keep the previous
behavior, add `foundation_emails` to your own Gemfile and import its SCSS.
- Legacy `gemfiles/` (Rails 3-6) and `.travis.yml`. CI is now GitHub Actions.

### Compatibility

- Ruby `>= 3.2` (tested up to 4.0).
- Rails `>= 7.1` (tested up to 8.1).
- Nokogiri `>= 1.16`.

### Migration from 1.x

- The generated markup changed. If you snapshot-test rendered emails, re-record
the snapshots.
- If you relied on Foundation's SCSS classes for styling, add `foundation_emails`
to your Gemfile yourself, or provide your own styles for the preserved class
hooks (`.row`, `.columns`, `.button`, `.menu`, `.callout`, `.spacer`, ...).
- Custom components previously passed as `components: { tag: 'tag-name' }` string
maps are replaced by the class-based registry. Use `register_component`.

[2.0.0]: https://github.com/foundation/inky-rb/releases/tag/v2.0.0
13 changes: 10 additions & 3 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
# frozen_string_literal: true

source 'https://rubygems.org'

# Specify your gem's dependencies in inky.gemspec
# Specify the gem's runtime dependencies in inky.gemspec
gemspec

gem 'rspec-core'
gem 'rspec-expectations'
gem 'minitest', '~> 5.20'
gem 'rails', ENV.fetch('RAILS_VERSION', '>= 7.1')
gem 'rake', '~> 13.0'
gem 'rubocop', '~> 1.60', require: false
gem 'sorbet', '>= 0.5', require: false
gem 'sorbet-static-and-runtime', '>= 0.5', require: false
gem 'tapioca', '>= 0.13', require: false
Loading
Loading