Skip to content

Modernize inky-rb as a standalone v2.0.0 gem#1

Merged
PhilMeyr merged 34 commits into
developfrom
feat/modernization
Jun 11, 2026
Merged

Modernize inky-rb as a standalone v2.0.0 gem#1
PhilMeyr merged 34 commits into
developfrom
feat/modernization

Conversation

@PhilMeyr

Copy link
Copy Markdown
Contributor

Summary

Modernizes the dormant (2019) inky-rb transpiler into a publishable, generalist v2.0.0 gem following 2026 email best practices.

Architecture

  • Extensible component registry: one Sorbet-typed class per component (Inky::Components::*), public register_component API, built-ins dogfooded through the same mechanism
  • Sorbet typed: strict across lib/ (Rails template handler excepted), Rubocop clean
  • Minitest suite: 80 runs / 194 assertions — golden-markup tests per component, Rails template handler integration tests, malformed HTML and raw-block edge cases

Modern generated markup

  • Fluid-hybrid layout: width:100%; max-width:600px + MSO ghost tables/cells (Outlook Word support through ~2029), configurable container_width
  • role="presentation" injected on every generated table
  • Bulletproof buttons (padding on the <a>)
  • No reliance on !important or border-radius (Orange.fr webmail constraints), no hard-coded colors (dark-mode safe)

Compatibility & upstream

  • Ruby 3.2 → 4.0 × Rails 7.1 → 8.1 (CI matrix)
  • Upstream PRs integrated: #108 (encoding), #101 (multi-line raw), #102 (custom components, reimplemented)
  • Fixed a String#sub backreference bug corrupting <raw> blocks containing backslashes
  • foundation_emails runtime dependency dropped; install generator rewritten foundation-agnostic

Breaking changes (see CHANGELOG)

Generated markup changed (consumers must re-record snapshots); custom-component API is now class-based.

PhilMeyr added 11 commits June 11, 2026 00:42
… email markup

- Replace the ComponentFactory mixin with one Sorbet-typed class per component
  (Inky::Components::*) sharing a Base interface, dogfooded by the built-ins.
- Add Inky.configuration.register_component for app-defined tags (open/closed).
- Modernize generated markup: fluid-hybrid layout, MSO ghost tables/cells,
  role=presentation on every table, bulletproof button, mso-line-height-rule.
- Add container_width config (default 600).
- Integrate upstream PRs: #108 (encoding identity check), #101 (multi-line raw).
- Bump to v2.0.0; modernize gemspec (metadata, required_ruby_version >= 3.2).
…akefile

- sorbet/config + tapioca gem RBIs; all of lib/ typechecks at typed: strict
  (Rails template handler/engine/generator stay typed:false as the genuinely
  dynamic ActionView frontier).
- Rewrite .rubocop.yml for a modern gem (NewCops, Ruby 3.2 target).
- Replace RSpec rake tasks with Rake::TestTask for minitest.
- Drop RSpec specs/test_app, old gemfiles and .travis.yml.
- Golden-file assertions freezing the modern markup (MSO ghost tables,
  role=presentation, bulletproof button, mso-line-height-rule) for every
  built-in component.
- Cover error/edge cases: encoding (binary/utf-8), doctype, multi-root,
  passthrough, multi-line and multiple <raw> blocks, custom-component registry
  (register_component, constructor override, DOM access), config type guards.
- README: install, per-component source->render reference, configuration
  (column_count, container_width, template_engine, register_component),
  custom-component guide, 1.x migration guide, email-client compatibility policy.
- CHANGELOG following Keep a Changelog (2.0.0): breaking changes documented,
  notably the dropped foundation_emails runtime dependency.
- CI matrix: Ruby 3.2/3.3/3.4/4.0 x Rails 7.1/8.0/8.1 + rubocop/sorbet lint job.
…ocks

The 2-arg sub form expands \0/\1/\& found in <raw> content (Liquid, MSO
conditionals, backslashes). Use the block form and add a regression test.
A quote inside an attribute value previously truncated the generated
attribute markup before Nokogiri re-parsed it.
- Oversized large attributes no longer push the MSO ghost cell past the
  container (forced wrap in Outlook Word).
- Inline font-weight:normal;text-align:left on the content th reproduces the
  reset foundation-emails used to apply via CSS; the th tag itself is kept for
  1.x class-hook compatibility (th.columns selectors) and inky.js parity.
Drop the foundation_emails.scss step (the runtime dependency was removed in
v2) and replace the generated layouts with a neutral modern email shell:
HTML5 doctype, MSO OfficeDocumentSettings, color-scheme metas,
x-apple-disable-message-reformatting, role=article wrapper, and an
enhancement-only style block.
- TemplateHandler: registration, engine composition, compiled-source
  wrapping, unknown-engine error, and a real render through ActionView.
- Malformed HTML: unclosed component tags, mismatched nesting, missing
  component attributes.
… Ruby 4.0

- Document the gutter-less ghost-width model and the per-column clamp.
- Add the Sorbet sig to the custom-component example; note role=presentation
  on the container ghost table and the th inline reset.
- Reword 'golden-file' to golden-markup (structural) assertions.
- Lint job now runs on Ruby 4.0, the newest supported version.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1e692b28cb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

PhilMeyr added 3 commits June 11, 2026 08:26
ActionView 7.1 references URI without requiring it, which raises NameError
on Ruby 3.3+ when action_view is loaded outside a full Rails app.
component_factory looks up by node name (a String); Symbol keys — the 1.x
API convention — were silently ignored, leaving built-ins active instead of
the custom override. Normalize in the components= setter, register_component
and the per-instance components: option.
…ating

Components emitting their own inline style while passing through the
author's style attribute produced a duplicated style= ; parsers keep only
the first, silently dropping either the author styling or the critical
fluid-hybrid CSS. style is now excluded from passthrough and merged after
the layout style (author wins on overlapping properties) across columns,
container, row, button anchor, callout, h-line, wrapper, menu, menu item,
spacer and block-grid.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 627aaddd37

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

PhilMeyr added 14 commits June 11, 2026 13:58
heredoc.delete("\n") in container and columns also ate the newlines inside
inner, gluing words together (a newline is an inter-word space in HTML) and
destroying <pre> blocks. Build the wrapper from single-line fragments joined
together; inner is never transformed. Raw-string regression tests added (the
golden comparator normalizes whitespace and could not catch this).
role/border/cellpadding/cellspacing was repeated 13 times across components.
align=center now sits after the reset on container/wrapper (goldens updated,
markup semantically identical).
- Remove Metrics excludes on lib (AbcSize/MethodLength/Cyclomatic) and the
  global Naming/PredicateMethod disable (class? already returns a real
  boolean and passes natively).
- Columns: split transform into column_sizes/column_classes/expander/
  ghost_width/column_markup helpers; Button: extract anchor; Core: extract
  build_components/normalize_input; Center: hoist repeated node.elements;
  Spacer: guard clause.
- Test files keep relaxed metrics (project policy).
…_attr

href (button, menu item), target and the author style were interpolated raw
while pass_through_attributes already escaped quotes; a double quote in any
of them broke the generated attribute. DRY: single Base#escape_attr helper.
…error

A 1.x string map ({ button: 'tag-name' }) passed the to_hash guard and
crashed later with NoMethodError in Core#initialize. Shared
Inky::Components.validate_component! now rejects non-Base values in
components=, register_component and the per-instance components: option,
pointing migrating users to register_component.
- column_count read from the global configuration (not just the option)
- html_safe? contract of the rendered output under ActiveSupport
- explicit empty href on <item> without href
- inky-interim-th literal in author content survives the final swap
force_encoding alone can hand Nokogiri an invalid String whose mangling
depends on the Nokogiri version; scrub degrades to U+FFFD deterministically
and is a no-op on valid input.
…nents

The aea8a43 cleanup removed Metrics excludes but left LineLength excluded
for lib/inky/components. Markup templates are now built from joined
single-line fragments; no production exclude remains.
Malformed markup was silently repaired, shipping a different email than
authored. New Inky.configuration.on_parse_error (:ignore/:warn/:raise,
default :warn) reports parser errors via ParseErrorReporter; unknown-tag
errors (libxml2 code 801) for registered component tags are filtered so
legitimate inky markup stays silent. Fragment tests no longer wrap content
in a misplaced <body>.
small="abc".to_i silently produced a small-0 column; same for block-grid
up and spacer sizes. Shared Base#positive_int falls back to the component
default on invalid values; columns also guard against a nil parent and a
zero result of the per-sibling division.
A regression in IGNORED_ON_PASSTHROUGH would leak href/size/style twice
without any test noticing; covers case-insensitive filtering too.
escape_attr now uses CGI.escapeHTML (& " < > and apostrophe) on the single
shared path, producing conforming HTML for all interpolated values.
- component_instances disambiguates from Configuration#components (classes).
- validate_component! tag typed String|Symbol instead of T.untyped.
- Comments: drop PR references, compact WHAT docstrings; public extension
  points keep a one-line doc.
- changelog_uri points at master.
Sigs everywhere; T.untyped only on the genuine dynamic frontier (ActionView
handlers share no interface: Procs, objects, classes). Runtime checking
immediately caught that template_handler_extensions yields Strings, not
Symbols.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 6170aa1. Configure here.

PhilMeyr added 4 commits June 11, 2026 15:56
A typo silently fell back to the configured template_engine; the author
believed they were composing with :slim and got :erb.
A UTF-8-marked String with invalid bytes raised ArgumentError on the first
gsub before reaching Nokogiri; scrub now applies whenever the input is not
valid_encoding?, not only for BINARY input. Also type the public entry point
Object instead of T.untyped (ActionView::OutputBuffer is no longer a String
since Rails 7.1, so String would break every Rails render).
The 801 error does not carry the tag name; the regex on the message text is
version/locale dependent. This test fails on a Nokogiri bump that changes
the format, before users drown in false warnings.
config.components['x'] = Garbage bypassed validate_component!; the getter
now returns a frozen copy. Also drop three WHAT comments.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 326486c24f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

PhilMeyr added 2 commits June 11, 2026 16:52
column_count: 0 reached ghost_width as a division by zero (Infinity, then
FloatDomainError on round). Shared Inky.assert_positive_dimension! guards
both configuration setters and the per-instance constructor options.
The OfficeDocumentSettings/PixelsPerInch conditional was only in the ERB
template.
@AdVitam AdVitam deleted a comment from cursor Bot Jun 11, 2026
@AdVitam AdVitam deleted a comment from chatgpt-codex-connector Bot Jun 11, 2026
@AdVitam AdVitam deleted a comment from chatgpt-codex-connector Bot Jun 11, 2026
@AdVitam AdVitam deleted a comment from cursor Bot Jun 11, 2026
@AdVitam AdVitam deleted a comment from chatgpt-codex-connector Bot Jun 11, 2026
@AdVitam AdVitam deleted a comment from chatgpt-codex-connector Bot Jun 11, 2026
@AdVitam AdVitam deleted a comment from chatgpt-codex-connector Bot Jun 11, 2026
@PhilMeyr
PhilMeyr merged this pull request into develop Jun 11, 2026
12 checks passed
@PhilMeyr
PhilMeyr deleted the feat/modernization branch June 11, 2026 15:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant