Class: Domternal::RichText

Inherits:
ActiveRecord::Base
  • Object
show all
Defined in:
app/models/domternal/rich_text.rb

Overview

Holds the HTML body + Active Storage embeds for a single has_rich_domternal attribute. Body is stored as the same sanitized HTML the Domternal editor renders/re-parses (Domternal's Editor/DomternalEditor accept HTML directly as content), so no separate JSON<->HTML conversion step is needed on the Ruby side.

Constant Summary collapse

PARAGRAPH_TAGS =

How #to_plain_text separates elements: paragraph-level blocks get a blank line between them, list items and table rows a single newline, and table cells a space.

%w[
  p div h1 h2 h3 h4 h5 h6 blockquote pre figure figcaption ul ol table details
].freeze
LINE_TAGS =
%w[li tr summary].freeze
CELL_TAGS =
%w[td th].freeze
BREAK =

Private-use codepoint: not whitespace, so the collapse in #to_plain_text leaves it alone, and Nokogiri accepts it in a text node (unlike NUL).

"\uE000"

Instance Method Summary collapse

Instance Method Details

#to_plain_textObject

Block-aware, like ActionText's plain text conversion: block boundaries and
become newlines rather than being collapsed into spaces. Callers rely on this — line-oriented content (one list item or answer per line) has to survive the round trip, and search index columns built from rich text read far better with the structure kept.



38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# File 'app/models/domternal/rich_text.rb', line 38

def to_plain_text
  html = body.to_s
  return "" if html.blank?

  fragment = Nokogiri::HTML5.fragment(html)
  # Breaks are marked with a sentinel rather than a real newline so that the whitespace
  # collapse below can't tell them apart from incidental newlines in the source markup.
  fragment.css("br").each { |node| node.replace(text_node(node, BREAK)) }
  fragment.css(*CELL_TAGS).each { |node| node.add_next_sibling(text_node(node, " ")) }
  fragment.css(*LINE_TAGS).each { |node| node.add_next_sibling(text_node(node, BREAK)) }
  fragment.css(*PARAGRAPH_TAGS).each { |node| node.add_next_sibling(text_node(node, BREAK * 2)) }

  fragment.text
    .gsub(/[^\S\n]+/, " ")  # spaces and tabs collapse; newlines in text are content
    .gsub(/ ?(#{BREAK}(?: ?#{BREAK})*) ?/o) { "\n" * Regexp.last_match(1).count(BREAK) }
    .gsub(/ *\n */, "\n")
    .gsub(/\n{3,}/, "\n\n")
    .strip
end

#to_sObject



30
31
32
# File 'app/models/domternal/rich_text.rb', line 30

def to_s
  body.to_s.html_safe # rubocop:disable Rails/OutputSafety -- sanitized in #sanitize_body before persistence
end