rich_text_renderer turns a Contentful RichText document into a string. The
default output is HTML, but the renderer is a lookup table from node type to
renderer class, so any node type can be redirected to arbitrary output.
The whole gem is pure computation: no network access, no file IO, no global state, no runtime dependencies. Output is a function of the node hash plus the mappings table. That is why the test suite needs no Contentful credentials.
A RichText field is a JSON tree. Every node carries a nodeType string.
Container nodes carry a content array of child nodes. Text nodes carry a
value string and an optional marks array, where each mark is itself a hash
with a type (bold, italic, code, underline, superscript,
subscript). Embedded entries and assets carry the referenced object under
data.target; hyperlinks carry data.uri.
RichTextRenderer::Renderer.new(mappings)merges the caller's overrides intoDEFAULT_MAPPINGS(lib/rich_text_renderer/renderer.rb) — 28 node types plus anilfallback key.Renderer#render(document)calls the inherited protectedBaseNodeRenderer#find_renderer, which looks upnode.to_h['nodeType']in the table, instantiates the class with the full mappings hash, and calls#renderon it. If no entry matches and anilkey exists, thenilentry is used instead.DocumentRenderer(lib/rich_text_renderer/document_renderers/) iteratesdocument['content'], renders each child through the same lookup, and joins the results with a newline.BaseBlockRendereremits<tag>+ rendered children +</tag>, wheretagcomes from the protected#render_tag.#render_contentrecurses back throughfind_renderer, which is how nesting works at arbitrary depth.BaseInlineRendereremits<tag>#{node['value']}</tag>— inline renderers consume a text node'svaluerather than recursing.TextRendererdeep-copies the node, escapesvaluewithCGI.escapeHTML, then folds each entry inmarksover the value, looking each mark'stypeup in the same mappings table.
Because every renderer is constructed with the complete mappings hash rather than with its own local configuration, a single override at the top propagates to every level of the tree. That is the property the "global mappings" refactor in 1234a9f was after.
| Path | Contents |
|---|---|
lib/rich_text_renderer.rb |
Public require; pulls in renderer and version |
lib/rich_text_renderer/renderer.rb |
Renderer and DEFAULT_MAPPINGS |
lib/rich_text_renderer/base_node_renderer.rb |
Abstract base; holds mappings, provides find_renderer |
lib/rich_text_renderer/null_renderer.rb |
Catch-all registered under the nil key |
lib/rich_text_renderer/document_renderers/ |
DocumentRenderer, the top-level node |
lib/rich_text_renderer/block_renderers/ |
BaseBlockRenderer and 20 block-level renderers |
lib/rich_text_renderer/text_renderers/ |
BaseInlineRenderer, TextRenderer, and the mark renderers |
lib/rich_text_renderer/version.rb |
RichTextRenderer::VERSION |
spec/ |
Mirrors lib/ under spec/lib/; doubles live in spec/spec_helper.rb |
Block renderers cover headings 1-6, paragraph, blockquote, hyperlink,
hr, the three list node types, the four table node types, and the three
reference types (embedded-entry-block, embedded-asset-block,
asset-hyperlink).
- Any node type. Pass
{'paragraph' => MyRenderer}toRenderer.new. The class needs aninitialize(mappings = {})and a#render(node)returning a string; subclassingBaseNodeRenderergives you both plusfind_rendererfor recursion. - Unknown node types.
NullRendereris registered under thenilkey and raises"No renderer defined for '<type>' nodes". Failing loudly is the default so that a new Contentful node type does not silently vanish from output. Callers who want the opposite behaviour override thenilkey with a renderer that returns""— this is documented inREADME.md. - Embedded entries.
EntryBlockRendereronly wrapsnode['data']['target'].inspectin a<div>. It exists to be replaced; any application rendering embedded entries is expected to override'embedded-entry-block'.
Asset rendering has to work both for Contentful::Asset objects returned by
contentful.rb and for raw JSON hashes from the API. AssetHyperlinkRenderer
handles this by checking whether any ancestor class name contains Asset, then
falling back to a hash path that validates fields.file is present.
AssetBlockRenderer subclasses it and overrides both paths to emit <img>
when the asset's content type contains image. The reason it duck-types rather
than referencing SDK constants is recorded in
docs/ADRs/2026-08-25-no-runtime-dependency-on-contentful-sdk.md.
Specs are unit tests over single renderers: build the node hash, instantiate
the renderer with whatever slice of the mappings table it needs, assert on the
returned string. spec/lib/rich_text_renderer/renderer_spec.rb and
spec/spec_helper.rb's *MarkdownRenderer classes cover the override path by
rendering the same document to Markdown instead of HTML.