Modernize inky-rb as a standalone v2.0.0 gem#1
Conversation
… 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.
There was a problem hiding this comment.
💡 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".
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.
There was a problem hiding this comment.
💡 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".
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.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ 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.
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.
There was a problem hiding this comment.
💡 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".
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.

Summary
Modernizes the dormant (2019) inky-rb transpiler into a publishable, generalist v2.0.0 gem following 2026 email best practices.
Architecture
Inky::Components::*), publicregister_componentAPI, built-ins dogfooded through the same mechanismtyped: strictacrosslib/(Rails template handler excepted), Rubocop cleanModern generated markup
width:100%; max-width:600px+ MSO ghost tables/cells (Outlook Word support through ~2029), configurablecontainer_widthrole="presentation"injected on every generated table<a>)!importantorborder-radius(Orange.fr webmail constraints), no hard-coded colors (dark-mode safe)Compatibility & upstream
<raw>blocks containing backslashesfoundation_emailsruntime dependency dropped; install generator rewritten foundation-agnosticBreaking changes (see CHANGELOG)
Generated markup changed (consumers must re-record snapshots); custom-component API is now class-based.