Use house ssr
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
source "https://rubygems.org"
|
||||
git_source(:bc) { |repo| "https://github.com/basecamp/#{repo}" }
|
||||
ruby file: ".ruby-version"
|
||||
|
||||
gem "rails", github: "rails/rails", branch: "main"
|
||||
@@ -9,6 +10,7 @@ gem "propshaft"
|
||||
gem "stimulus-rails"
|
||||
gem "turbo-rails"
|
||||
gem "hotwire_combobox", github: "josefarias/hotwire_combobox", branch: :main
|
||||
gem "house_md", bc: "house", branch: :renderer
|
||||
|
||||
# Deployment and drivers
|
||||
gem "bootsnap", require: false
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
GIT
|
||||
remote: https://github.com/basecamp/house
|
||||
revision: a8b19e70bbb95a7a0d27e38985a99c2d697a6650
|
||||
branch: renderer
|
||||
specs:
|
||||
house_md (1.0.0)
|
||||
rails
|
||||
zeitwerk
|
||||
|
||||
GIT
|
||||
remote: https://github.com/josefarias/hotwire_combobox.git
|
||||
revision: 57ee6c9087320fab9383a07fa919fe75e0ea72ba
|
||||
@@ -346,6 +355,7 @@ DEPENDENCIES
|
||||
capybara
|
||||
debug
|
||||
hotwire_combobox!
|
||||
house_md!
|
||||
importmap-rails
|
||||
jbuilder
|
||||
propshaft
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
ActiveSupport.on_load :action_text_markdown do
|
||||
require "markdown_renderer"
|
||||
ActionText::Markdown.renderer = -> { MarkdownRenderer.new }
|
||||
ActionText::Markdown.html_renderer = -> { MarkdownRenderer.new }
|
||||
end
|
||||
|
||||
+41
-34
@@ -1,40 +1,47 @@
|
||||
class MarkdownRenderer
|
||||
require_relative "markdown_renderer/parsing"
|
||||
require_relative "markdown_renderer/markup"
|
||||
require "rouge/plugins/redcarpet"
|
||||
|
||||
include Parsing, Markup
|
||||
class MarkdownRenderer < HouseMd::Renderer
|
||||
class Generator < HouseMd::Generator::Html
|
||||
include Rouge::Plugins::Redcarpet
|
||||
|
||||
def initialize
|
||||
@id_counts = Hash.new(0)
|
||||
end
|
||||
def initialize
|
||||
@id_counts = Hash.new 0
|
||||
end
|
||||
|
||||
def render(content)
|
||||
content
|
||||
.then { |c| parse_paragraphs(c) }
|
||||
.then { |c| parse_bold_italics(c) }
|
||||
.then { |c| parse_bold(c) }
|
||||
.then { |c| parse_italics(c) }
|
||||
.then { |c| parse_strikethrough(c) }
|
||||
.then { |c| parse_highlight(c) }
|
||||
.then { |c| parse_headers(c) }
|
||||
.then { |c| parse_tables(c) }
|
||||
.then { |c| parse_ordered_lists(c) }
|
||||
.then { |c| parse_unordered_lists(c) }
|
||||
.then { |c| parse_block_quotes(c) }
|
||||
.then { |c| parse_horizontal_rules(c) }
|
||||
.then { |c| parse_images(c) }
|
||||
.then { |c| parse_links(c) }
|
||||
.then { |c| parse_code_blocks(c) }
|
||||
.then { |c| parse_code_spans(c) }
|
||||
end
|
||||
|
||||
private
|
||||
attr_reader :id_counts
|
||||
|
||||
def unique_id(text)
|
||||
text.parameterize.then do |base_id|
|
||||
id_counts[base_id] += 1
|
||||
id_counts[base_id] > 1 ? "#{base_id}-#{id_counts[base_id]}" : base_id
|
||||
def header(text, header_level)
|
||||
unique_id(text).then do |id|
|
||||
<<~HTML.chomp
|
||||
<h#{header_level} id="#{id}">
|
||||
#{text} <a href="##{id}" class="heading__link" aria-hidden="true">#</a>
|
||||
</h#{header_level}>
|
||||
HTML
|
||||
end
|
||||
end
|
||||
|
||||
def image(url, alt_text)
|
||||
<<~HTML.chomp
|
||||
<a href="#{url}" data-action="lightbox#open:prevent" data-lightbox-target="image" data-lightbox-url-value="#{url}?disposition=attachment">
|
||||
<img src="#{url}" alt="#{alt_text}">
|
||||
</a>
|
||||
HTML
|
||||
end
|
||||
|
||||
def code_block(code, language)
|
||||
block_code(code, language) # call Rouge Redcarpet plugin
|
||||
end
|
||||
|
||||
private
|
||||
attr_reader :id_counts
|
||||
|
||||
def unique_id(text)
|
||||
text.parameterize.then do |base_id|
|
||||
id_counts[base_id] += 1
|
||||
id_counts[base_id] > 1 ? "#{base_id}-#{id_counts[base_id]}" : base_id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def initialize
|
||||
@generator = Generator.new
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
require "rouge/plugins/redcarpet"
|
||||
|
||||
module MarkdownRenderer::Markup
|
||||
include Rouge::Plugins::Redcarpet
|
||||
|
||||
TABLE_ROW_INDENT = " " * 4
|
||||
LIST_ITEM_INDENT = " " * 2
|
||||
BLOCK_QUOTE_INDENT = " " * 2
|
||||
TABLE_HEADER_INDENT = " " * 6
|
||||
|
||||
# FIXME: the attributes suggest this should be an app-level override instead
|
||||
def header(text, header_level)
|
||||
unique_id(text).then do |id|
|
||||
<<~HTML.chomp
|
||||
<h#{header_level} id="#{id}">
|
||||
#{text} <a href="##{id}" class="heading__link" aria-hidden="true">#</a>
|
||||
</h#{header_level}>
|
||||
HTML
|
||||
end
|
||||
end
|
||||
|
||||
def bold(text)
|
||||
"<strong>#{text}</strong>"
|
||||
end
|
||||
|
||||
def italics(text)
|
||||
"<em>#{text}</em>"
|
||||
end
|
||||
|
||||
def strikethrough(text)
|
||||
"<s>#{text}</s>"
|
||||
end
|
||||
|
||||
def highlight(text)
|
||||
"<mark>#{text}</mark>"
|
||||
end
|
||||
|
||||
def table(headers, rows)
|
||||
<<~HTML
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
#{headers.map { |header| "<th>#{header}</th>" }.join("\n" + TABLE_HEADER_INDENT)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
#{rows.map { |row| "<tr>#{row.map { |cell| "<td>#{cell}</td>" }.join}</tr>" }.join("\n" + TABLE_ROW_INDENT)}
|
||||
</tbody>
|
||||
</table>
|
||||
HTML
|
||||
end
|
||||
|
||||
def ordered_list(contents)
|
||||
<<~HTML
|
||||
<ol>
|
||||
#{contents.split("\n").join("\n" + LIST_ITEM_INDENT)}
|
||||
</ol>
|
||||
HTML
|
||||
end
|
||||
|
||||
def unordered_list(contents)
|
||||
<<~HTML
|
||||
<ul>
|
||||
#{contents.split("\n").join("\n" + LIST_ITEM_INDENT)}
|
||||
</ul>
|
||||
HTML
|
||||
end
|
||||
|
||||
def list_item(text)
|
||||
"<li>#{text}</li>"
|
||||
end
|
||||
|
||||
def block_quote(text)
|
||||
<<~HTML
|
||||
<blockquote>
|
||||
#{text.gsub(/^>\s?/, '').split("\n").join("\n" + BLOCK_QUOTE_INDENT)}
|
||||
</blockquote>
|
||||
HTML
|
||||
end
|
||||
|
||||
def horizontal_rule
|
||||
"<hr>"
|
||||
end
|
||||
|
||||
def link(text, href)
|
||||
"<a href=\"#{href}\">#{text}</a>"
|
||||
end
|
||||
|
||||
def code_block(code, language)
|
||||
block_code(code, language) # call Rouge Redcarpet plugin
|
||||
end
|
||||
|
||||
def code_span(text)
|
||||
"<code>#{text}</code>"
|
||||
end
|
||||
|
||||
# FIXME: the attributes suggest this should be an app-level override instead
|
||||
def image(url, alt_text)
|
||||
<<~HTML.chomp
|
||||
<a href="#{url}" data-action="lightbox#open:prevent" data-lightbox-target="image" data-lightbox-url-value="#{url}?disposition=attachment">
|
||||
<img src="#{url}" alt="#{alt_text}">
|
||||
</a>
|
||||
HTML
|
||||
end
|
||||
|
||||
def paragraph(text)
|
||||
"<p>#{text}</p>"
|
||||
end
|
||||
|
||||
def soft_line_break
|
||||
"\n#{line_break}\n"
|
||||
end
|
||||
|
||||
def line_break
|
||||
"<br>"
|
||||
end
|
||||
end
|
||||
@@ -1,136 +0,0 @@
|
||||
module MarkdownRenderer::Parsing
|
||||
HR = /^---$/
|
||||
INLINE_HTML_BLOCK_START = /^</
|
||||
HEADING_START = /^#/
|
||||
CODE_BLOCK_START = /^```/
|
||||
|
||||
NEWLINE_OR_EOF = /\n|$/
|
||||
SOFT_LINE_BREAK = /(?<=[^\n])\n(?=[^\n])/
|
||||
|
||||
NUMBERED_LIST_START = /^\d+\.\s/
|
||||
NUMBERED_LIST_ITEM = /#{NUMBERED_LIST_START}[^\n]*#{NEWLINE_OR_EOF}/
|
||||
|
||||
UNORDERED_LIST_START = /^-\s/
|
||||
UNORDERED_LIST_ITEM = /#{UNORDERED_LIST_START}[^\n]*#{NEWLINE_OR_EOF}/
|
||||
|
||||
BLOCK_QUOTE_START = /^>\s?/
|
||||
BLOCK_QUOTE_CONTENT = /#{BLOCK_QUOTE_START}[^\n]*#{NEWLINE_OR_EOF}/
|
||||
|
||||
TABLE_START = /^\|/
|
||||
TABLE_HEADER_ROW = /^\|(.+)\|/
|
||||
TABLE_SEPARATOR = /^\|[-|]+\|/
|
||||
TABLE_DATA_ROW = /^\|.+\|/
|
||||
TABLE = /#{TABLE_HEADER_ROW}\r?\n#{TABLE_SEPARATOR}\r?\n((?:#{TABLE_DATA_ROW}\r?\n)*)/
|
||||
|
||||
BLOCK_STARTERS = Regexp.union [
|
||||
HR,
|
||||
TABLE_START,
|
||||
HEADING_START,
|
||||
CODE_BLOCK_START,
|
||||
BLOCK_QUOTE_START,
|
||||
NUMBERED_LIST_START,
|
||||
UNORDERED_LIST_START,
|
||||
INLINE_HTML_BLOCK_START
|
||||
]
|
||||
|
||||
private
|
||||
def parse_paragraphs(content)
|
||||
content.split(/\n\n+/).map do |text|
|
||||
if text.match?(BLOCK_STARTERS)
|
||||
text
|
||||
else
|
||||
paragraph text.gsub(SOFT_LINE_BREAK, soft_line_break)
|
||||
end
|
||||
end.join("\n\n")
|
||||
end
|
||||
|
||||
def parse_bold_italics(content)
|
||||
transform = ->(match) { bold(italics(match)) }
|
||||
|
||||
content
|
||||
.gsub(/(?<!\*)\*\*\*([^*]+)\*\*\*(?!\*)/) { transform.($1) }
|
||||
.gsub(/(?<!_)___([^_]+)___(?!_)/) { transform.($1) }
|
||||
end
|
||||
|
||||
def parse_bold(content)
|
||||
transform = ->(match) { bold(match) }
|
||||
|
||||
content
|
||||
.gsub(/(?<!\*)\*\*([^*]+)\*\*(?!\*)/) { transform.($1) }
|
||||
.gsub(/(?<!_)__([^_]+)__(?!_)/) { transform.($1) }
|
||||
end
|
||||
|
||||
def parse_italics(content)
|
||||
transform = ->(match) { italics(match) }
|
||||
|
||||
content
|
||||
.gsub(/(?<!\*)\*([^*]+)\*(?!\*)/) { transform.($1) }
|
||||
.gsub(/(?<!_)_([^_]+)_(?!_)/) { transform.($1) }
|
||||
end
|
||||
|
||||
def parse_strikethrough(content)
|
||||
content.gsub(/~~(.*?)~~/) { strikethrough($1) }
|
||||
end
|
||||
|
||||
def parse_highlight(content)
|
||||
content.gsub(/==(.*?)==/) { highlight($1) }
|
||||
end
|
||||
|
||||
def parse_headers(content)
|
||||
content
|
||||
.gsub(/^# (.*)$/) { header($1, 1) }
|
||||
.gsub(/^## (.*)$/) { header($1, 2) }
|
||||
.gsub(/^### (.*)$/) { header($1, 3) }
|
||||
.gsub(/^#### (.*)$/) { header($1, 4) }
|
||||
.gsub(/^##### (.*)$/) { header($1, 5) }
|
||||
.gsub(/^###### (.*)$/) { header($1, 6) }
|
||||
end
|
||||
|
||||
def parse_tables(content)
|
||||
content.gsub(TABLE) do
|
||||
headers = $1.split("|").map(&:strip).compact_blank
|
||||
rows = $2.split("\n").map { |row| row.split("|").map(&:strip).compact_blank unless row.blank? }.compact
|
||||
table(headers, rows) if rows.map(&:size).all?(headers.size)
|
||||
end
|
||||
end
|
||||
|
||||
def parse_ordered_lists(content)
|
||||
content.gsub(/(?:#{NUMBERED_LIST_ITEM})+/) { |list| ordered_list(parse_ordered_list_items(list.strip)) }
|
||||
end
|
||||
|
||||
def parse_ordered_list_items(content)
|
||||
content.gsub(/#{NUMBERED_LIST_START}(.*)$/) { list_item($1) }
|
||||
end
|
||||
|
||||
def parse_unordered_lists(content)
|
||||
content.gsub(/(?:#{UNORDERED_LIST_ITEM})+/) { |list| unordered_list(parse_unordered_list_items(list.strip)) }
|
||||
end
|
||||
|
||||
def parse_unordered_list_items(content)
|
||||
content.gsub(/#{UNORDERED_LIST_START}(.*)$/) { list_item($1) }
|
||||
end
|
||||
|
||||
def parse_block_quotes(content)
|
||||
content.gsub(/(?:#{BLOCK_QUOTE_CONTENT})+/) { |text| block_quote(text.strip) }
|
||||
end
|
||||
|
||||
def parse_horizontal_rules(content)
|
||||
content.gsub(HR) { horizontal_rule }
|
||||
end
|
||||
|
||||
def parse_images(content)
|
||||
content.gsub(/!\[(.*?)\]\((.*?)\)/) { image($2, $1) }
|
||||
end
|
||||
|
||||
def parse_links(content)
|
||||
content.gsub(/\[(.*?)\]\((.*?)\)/) { link($1, $2) }
|
||||
end
|
||||
|
||||
def parse_code_blocks(content)
|
||||
content.gsub(/#{CODE_BLOCK_START}(\w*)\n(.*?)```$/m) { code_block($2, $1) }
|
||||
end
|
||||
|
||||
def parse_code_spans(content)
|
||||
content.gsub(/`([^`\n]+)`/) { code_span($1) }
|
||||
end
|
||||
end
|
||||
@@ -1,28 +1,16 @@
|
||||
require "redcarpet/render_strip"
|
||||
|
||||
module ActionText
|
||||
class Markdown < Record
|
||||
DEFAULT_RENDERER_OPTIONS = {
|
||||
filter_html: false
|
||||
}
|
||||
|
||||
DEFAULT_MARKDOWN_EXTENSIONS = {
|
||||
autolink: true,
|
||||
highlight: true,
|
||||
no_intra_emphasis: true,
|
||||
fenced_code_blocks: true,
|
||||
lax_spacing: true,
|
||||
strikethrough: true,
|
||||
tables: true
|
||||
}
|
||||
|
||||
mattr_accessor :renderer, default: Redcarpet::Markdown.new(Redcarpet::Render::HTML.new(DEFAULT_RENDERER_OPTIONS), DEFAULT_MARKDOWN_EXTENSIONS)
|
||||
mattr_accessor :plain_renderer, default: Redcarpet::Markdown.new(Redcarpet::Render::StripDown)
|
||||
mattr_accessor :html_renderer
|
||||
mattr_accessor :plain_renderer
|
||||
|
||||
belongs_to :record, polymorphic: true, touch: true
|
||||
|
||||
def to_html
|
||||
(renderer.try(:call) || renderer).render(content).html_safe
|
||||
to_unsafe_html.html_safe
|
||||
end
|
||||
|
||||
def to_unsafe_html
|
||||
(html_renderer.try(:call) || html_renderer).render(content)
|
||||
end
|
||||
|
||||
def to_plain_text
|
||||
|
||||
@@ -1,230 +0,0 @@
|
||||
require "test_helper"
|
||||
|
||||
class MarkdownRendererTest < ActiveSupport::TestCase
|
||||
setup do
|
||||
@renderer = MarkdownRenderer.new
|
||||
end
|
||||
|
||||
test "renders a complete markdown document" do
|
||||
assert_equal expected_html, @renderer.render(markdown_content)
|
||||
end
|
||||
|
||||
private
|
||||
def expected_html
|
||||
<<~HTML
|
||||
<h1 id="welcome-to-my-document">
|
||||
Welcome to My Document <a href="#welcome-to-my-document" class="heading__link" aria-hidden="true">#</a>
|
||||
</h1>
|
||||
|
||||
<p>This is an introduction paragraph with some text.</p>
|
||||
|
||||
<hr>
|
||||
|
||||
<p><strong><em>Hello, world!</em></strong> <strong>Hello, world!</strong> <em>Hello, world!</em> <s>Hello, world!</s> <mark>Hello, world!</mark></p>
|
||||
|
||||
<p><strong><em>Hello, world!</em></strong>
|
||||
<br>
|
||||
<strong>Hello, world!</strong>
|
||||
<br>
|
||||
<em>Hello, world!</em>
|
||||
<br>
|
||||
<s>Hello, world!</s>
|
||||
<br>
|
||||
<mark>Hello, world!</mark></p>
|
||||
|
||||
<h2 id="getting-started">
|
||||
Getting Started <a href="#getting-started" class="heading__link" aria-hidden="true">#</a>
|
||||
</h2>
|
||||
|
||||
<p>Here’s what you need to know:</p>
|
||||
|
||||
<ol>
|
||||
<li>First important point</li>
|
||||
<li>Second crucial thing</li>
|
||||
<li>Don’t forget this</li>
|
||||
</ol>
|
||||
|
||||
<h2 id="key-features">
|
||||
Key Features <a href="#key-features" class="heading__link" aria-hidden="true">#</a>
|
||||
</h2>
|
||||
|
||||
<p>Our product offers:</p>
|
||||
|
||||
<ul>
|
||||
<li>Simple interface</li>
|
||||
<li>Powerful features</li>
|
||||
<li>Great documentation</li>
|
||||
</ul>
|
||||
|
||||
<p><a href="https://basecamp.com">Get Basecamp</a></p>
|
||||
|
||||
<h3 id="important-note-2">
|
||||
Important Note <a href="#important-note-2" class="heading__link" aria-hidden="true">#</a>
|
||||
</h3>
|
||||
|
||||
<p>Here’s some
|
||||
<br>
|
||||
multiline text.
|
||||
<br>
|
||||
It has line breaks
|
||||
<br>
|
||||
in-between.</p>
|
||||
|
||||
<blockquote>
|
||||
Please read this carefully
|
||||
It contains vital information
|
||||
That you shouldn’t miss
|
||||
</blockquote>
|
||||
|
||||
<p><a href="https://placehold.co/600x400" data-action="lightbox#open:prevent" data-lightbox-target="image" data-lightbox-url-value="https://placehold.co/600x400?disposition=attachment">
|
||||
<img src="https://placehold.co/600x400" alt="Placeholder image">
|
||||
</a></p>
|
||||
|
||||
<h2 id="important-note">
|
||||
Important Note <a href="#important-note" class="heading__link" aria-hidden="true">#</a>
|
||||
</h2>
|
||||
|
||||
<p><a href="https://placehold.co/400x400" data-action="lightbox#open:prevent" data-lightbox-target="image" data-lightbox-url-value="https://placehold.co/400x400?disposition=attachment">
|
||||
<img src="https://placehold.co/400x400" alt="Placeholder image">
|
||||
</a></p>
|
||||
|
||||
<ol>
|
||||
<li>Remember to save</li>
|
||||
<li>Back up your work</li>
|
||||
</ol>
|
||||
|
||||
<p><a href="https://basecamp.com">Get Basecamp</a></p>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Table 1 Header 1</th>
|
||||
<th>Table 1 Header 2</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>Table 1 Data 1</td><td>Table 1 Data 2</td></tr>
|
||||
<tr><td>Table 1 Data 3</td><td>Table 1 Data 4</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p>Some content between tables...</p>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Table 2 Header 1</th>
|
||||
<th>Table 2 Header 2</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>Table 2 Data 1</td><td>Table 2 Data 2</td></tr>
|
||||
<tr><td>Table 2 Data 3</td><td>Table 2 Data 4</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="highlight"><pre class="highlight ruby"><code><span class="k">class</span> <span class="nc">Post</span> <span class="o"><</span> <span class="no">ApplicationRecord</span>
|
||||
<span class="k">def</span> <span class="nf">title</span>
|
||||
<span class="s2">"foo"</span>
|
||||
<span class="k">end</span>
|
||||
<span class="k">end</span>
|
||||
</code></pre></div>
|
||||
|
||||
<p><code>puts "Hello, world!"</code></p>
|
||||
|
||||
<p>Thanks for reading!</p>
|
||||
|
||||
<details>
|
||||
<summary>Summary</summary>
|
||||
Details
|
||||
</details>
|
||||
HTML
|
||||
end
|
||||
|
||||
def markdown_content
|
||||
<<~MARKDOWN
|
||||
# Welcome to My Document
|
||||
|
||||
This is an introduction paragraph with some text.
|
||||
|
||||
---
|
||||
|
||||
***Hello, world!*** **Hello, world!** *Hello, world!* ~~Hello, world!~~ ==Hello, world!==
|
||||
|
||||
***Hello, world!***
|
||||
**Hello, world!**
|
||||
*Hello, world!*
|
||||
~~Hello, world!~~
|
||||
==Hello, world!==
|
||||
|
||||
## Getting Started
|
||||
|
||||
Here’s what you need to know:
|
||||
|
||||
1. First important point
|
||||
2. Second crucial thing
|
||||
3. Don’t forget this
|
||||
|
||||
## Key Features
|
||||
|
||||
Our product offers:
|
||||
|
||||
- Simple interface
|
||||
- Powerful features
|
||||
- Great documentation
|
||||
|
||||
[Get Basecamp](https://basecamp.com)
|
||||
|
||||
### Important Note
|
||||
|
||||
Here’s some
|
||||
multiline text.
|
||||
It has line breaks
|
||||
in-between.
|
||||
|
||||
> Please read this carefully
|
||||
> It contains vital information
|
||||
> That you shouldn’t miss
|
||||
|
||||

|
||||
|
||||
## Important Note
|
||||
|
||||

|
||||
|
||||
1. Remember to save
|
||||
2. Back up your work
|
||||
|
||||
[Get Basecamp](https://basecamp.com)
|
||||
|
||||
| Table 1 Header 1 | Table 1 Header 2 |
|
||||
|-|-|
|
||||
| Table 1 Data 1 | Table 1 Data 2 |
|
||||
| Table 1 Data 3 | Table 1 Data 4 |
|
||||
|
||||
Some content between tables...
|
||||
|
||||
| Table 2 Header 1 | Table 2 Header 2 |
|
||||
|------------------|------------------|
|
||||
| Table 2 Data 1 | Table 2 Data 2 |
|
||||
| Table 2 Data 3 | Table 2 Data 4 |
|
||||
|
||||
```rb
|
||||
class Post < ApplicationRecord
|
||||
def title
|
||||
"foo"
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
`puts "Hello, world!"`
|
||||
|
||||
Thanks for reading!
|
||||
|
||||
<details>
|
||||
<summary>Summary</summary>
|
||||
Details
|
||||
</details>
|
||||
MARKDOWN
|
||||
end
|
||||
end
|
||||
@@ -10,7 +10,7 @@ class BubbleTest < ActiveSupport::TestCase
|
||||
bubbles(:logo).capture Comment.new(body: "Agreed.")
|
||||
end
|
||||
|
||||
assert_equal "Agreed.\n", bubbles(:logo).messages.last.messageable.body.to_plain_text
|
||||
assert_equal "Agreed.", bubbles(:logo).messages.last.messageable.body.to_plain_text
|
||||
end
|
||||
|
||||
test "boosting" do
|
||||
|
||||
Reference in New Issue
Block a user