Skip to content

Commit f5957d8

Browse files
committed
Collapse plain-text formatter into a context option
Same pattern as the static-HTML collapse: the `markdown_as_text` format symbol was a thin subclass setting a context flag and swapping the filter list. Replace it with `plain_text: true` on the existing rich formatter, which now picks between `RICH_FILTERS` and `TEXT_FILTERS` constants based on the flag. `static_html:` and `plain_text:` now sit as peer options on one format. Rename the `as_text` context key to `plain_text` for symmetry with `static_html`. Update both mailer `.text.erb` views and the two handler predicates that branch on the flag.
1 parent c607b36 commit f5957d8

10 files changed

Lines changed: 57 additions & 106 deletions

File tree

app/views/work_package_mailer/mentioned.text.erb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
<%= I18n.t(:label_comment_added) %>:
1111
<%= format_text(
1212
@journal.notes,
13-
format: :markdown_as_text,
13+
plain_text: true,
1414
only_path: false,
1515
object: @work_package,
1616
project: @work_package.project

app/views/work_package_mailer/watcher_changed.text.erb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ See COPYRIGHT and LICENSE files for more details.
3333
<%= render partial: "work_package_details", locals: { work_package: @work_package } %>
3434
<%= format_text(
3535
t(:text_latest_note, note: last_work_package_note(@work_package)),
36-
format: :markdown_as_text,
36+
plain_text: true,
3737
only_path: false,
3838
object: @work_package,
3939
project: @work_package.project

lib/open_project/text_formatting/filters/mention_filter.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ def work_package_mention(work_package, mention)
124124
# The hover-card endpoint a quickinfo would link to is unreachable
125125
# for plain-text recipients and for viewers without view permission.
126126
def text_only?(work_package)
127-
context[:as_text] || @visible_mentioned_ids.exclude?(work_package.id)
127+
context[:plain_text] || @visible_mentioned_ids.exclude?(work_package.id)
128128
end
129129

130130
def work_package_quickinfo(work_package, detailed:)

lib/open_project/text_formatting/filters/plain_text_output_filter.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@
3030

3131
module OpenProject::TextFormatting
3232
module Filters
33-
# Final stage of the `markdown_as_text` pipeline — strips remaining
34-
# markup so the output is safe for `text/plain` bodies.
33+
# Terminal stage when the rich pipeline is rendering for `text/plain`
34+
# bodies — collapses the DOM to its visible text so no HTML escapes.
3535
class PlainTextOutputFilter < HTML::Pipeline::Filter
3636
def call
3737
doc.text

lib/open_project/text_formatting/formats/markdown/formatter.rb

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -31,37 +31,53 @@
3131

3232
module OpenProject::TextFormatting::Formats::Markdown
3333
class Formatter < OpenProject::TextFormatting::Formats::BaseFormatter
34+
RICH_FILTERS = [
35+
OpenProject::TextFormatting::Filters::SettingMacrosFilter,
36+
OpenProject::TextFormatting::Filters::MarkdownFilter,
37+
OpenProject::TextFormatting::Filters::SanitizationFilter,
38+
OpenProject::TextFormatting::Filters::TaskListFilter,
39+
OpenProject::TextFormatting::Filters::TableOfContentsFilter,
40+
OpenProject::TextFormatting::Filters::MacroFilter,
41+
OpenProject::TextFormatting::Filters::MentionFilter,
42+
OpenProject::TextFormatting::Filters::PatternMatcherFilter,
43+
OpenProject::TextFormatting::Filters::SyntaxHighlightFilter,
44+
OpenProject::TextFormatting::Filters::AttachmentFilter,
45+
OpenProject::TextFormatting::Filters::AutolinkFilter,
46+
OpenProject::TextFormatting::Filters::AutolinkCustomProtocolsFilter,
47+
OpenProject::TextFormatting::Filters::RelativeLinkFilter,
48+
OpenProject::TextFormatting::Filters::LinkAttributeFilter,
49+
OpenProject::TextFormatting::Filters::ExternalLinkCaptureFilter,
50+
OpenProject::TextFormatting::Filters::FigureWrappedFilter,
51+
OpenProject::TextFormatting::Filters::BemCssFilter
52+
].freeze
53+
54+
# `text/plain` mailer bodies share the matcher and mention stages so
55+
# work-package references resolve consistently with the HTML channel,
56+
# then `PlainTextOutputFilter` collapses the DOM to text. Filters that
57+
# only shape HTML (TOC, syntax highlight, autolink, link-attribute,
58+
# figure, BEM) are omitted because `doc.text` would discard their work.
59+
TEXT_FILTERS = [
60+
OpenProject::TextFormatting::Filters::SettingMacrosFilter,
61+
OpenProject::TextFormatting::Filters::MarkdownFilter,
62+
OpenProject::TextFormatting::Filters::SanitizationFilter,
63+
OpenProject::TextFormatting::Filters::MentionFilter,
64+
OpenProject::TextFormatting::Filters::PatternMatcherFilter,
65+
OpenProject::TextFormatting::Filters::PlainTextOutputFilter
66+
].freeze
67+
3468
def to_html(text)
3569
result = pipeline.call(text, context)
3670
output = result[:output].to_s
3771

38-
output.html_safe
72+
context[:plain_text] ? output : output.html_safe # rubocop:disable Rails/OutputSafety
3973
end
4074

4175
def to_document(text)
4276
pipeline.to_document text, context
4377
end
4478

4579
def filters
46-
[
47-
OpenProject::TextFormatting::Filters::SettingMacrosFilter,
48-
OpenProject::TextFormatting::Filters::MarkdownFilter,
49-
OpenProject::TextFormatting::Filters::SanitizationFilter,
50-
OpenProject::TextFormatting::Filters::TaskListFilter,
51-
OpenProject::TextFormatting::Filters::TableOfContentsFilter,
52-
OpenProject::TextFormatting::Filters::MacroFilter,
53-
OpenProject::TextFormatting::Filters::MentionFilter,
54-
OpenProject::TextFormatting::Filters::PatternMatcherFilter,
55-
OpenProject::TextFormatting::Filters::SyntaxHighlightFilter,
56-
OpenProject::TextFormatting::Filters::AttachmentFilter,
57-
OpenProject::TextFormatting::Filters::AutolinkFilter,
58-
OpenProject::TextFormatting::Filters::AutolinkCustomProtocolsFilter,
59-
OpenProject::TextFormatting::Filters::RelativeLinkFilter,
60-
OpenProject::TextFormatting::Filters::LinkAttributeFilter,
61-
OpenProject::TextFormatting::Filters::ExternalLinkCaptureFilter,
62-
OpenProject::TextFormatting::Filters::FigureWrappedFilter,
63-
OpenProject::TextFormatting::Filters::BemCssFilter
64-
]
80+
context[:plain_text] ? TEXT_FILTERS : RICH_FILTERS
6581
end
6682

6783
def self.format

lib/open_project/text_formatting/formats/markdown/text_formatter.rb

Lines changed: 0 additions & 61 deletions
This file was deleted.

lib/open_project/text_formatting/matchers/link_handlers/work_packages.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ def render_work_package_link(work_package, fallback_id:)
142142
# A nil WP means classic mode skipped the preload, or the reference
143143
# didn't resolve — neither case needs visibility gating.
144144
def text_only?(work_package)
145-
context[:as_text] || (work_package && !preload_cache.visible?(work_package.id))
145+
context[:plain_text] || (work_package && !preload_cache.visible?(work_package.id))
146146
end
147147

148148
def preload_cache

lib/open_project/text_formatting/renderer.rb

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,12 @@ def format_text(text, format: :rich, **context)
4949
.to_html(text)
5050
end
5151

52-
# @param [:plain, :markdown_as_text, :rich] format the text format.
52+
# @param [:plain, :rich] format the text format.
5353
# @return [Formats::BaseFormatter] a formatter implementation.
5454
def formatter_for(format)
5555
case format.to_sym
5656
when :plain
5757
Formats.plain_formatter
58-
when :markdown_as_text
59-
Formats::Markdown::TextFormatter
6058
else
6159
Formats.rich_formatter
6260
end

spec/lib/open_project/text_formatting/filters/mention_filter_spec.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -204,17 +204,17 @@ def mention_tag(work_package, sep: "#", data_id: nil, data_text: nil)
204204

205205
context "in plain-text rendering mode",
206206
with_settings: { work_packages_identifier: "semantic" } do
207-
# The `:markdown_as_text` channel must collapse mention envelopes
208-
# to their current `formatted_id` so the plain-text mailer doesn't
209-
# leak `<mention>` HTML or stale envelope text.
207+
# `plain_text: true` must collapse mention envelopes to their current
208+
# `formatted_id` so the `text/plain` mailer doesn't leak `<mention>`
209+
# HTML or stale envelope text.
210210
let(:project) { create(:project, identifier: "MACROPROJ") }
211211
let(:work_package) { create(:work_package, project:, author:) }
212212

213213
before { work_package.allocate_and_register_semantic_id }
214214

215215
it "renders the formatted_id without an anchor or quickinfo" do
216216
wp = work_package.reload
217-
rendered = format_text(mention_tag(wp), format: :markdown_as_text)
217+
rendered = format_text(mention_tag(wp), plain_text: true)
218218

219219
expect(rendered).to include(wp.formatted_id)
220220
expect(rendered).not_to include("<a")
@@ -229,7 +229,7 @@ def mention_tag(work_package, sep: "#", data_id: nil, data_text: nil)
229229
let(:work_package) { create(:work_package, project:, author:) }
230230

231231
it "renders the hash-prefixed numeric id without an anchor or quickinfo" do
232-
rendered = format_text(mention_tag(work_package), format: :markdown_as_text)
232+
rendered = format_text(mention_tag(work_package), plain_text: true)
233233

234234
expect(rendered).to include("##{work_package.id}")
235235
expect(rendered).not_to include("<a")

spec/lib/open_project/text_formatting/formats/markdown/text_formatter_spec.rb renamed to spec/lib/open_project/text_formatting/formats/markdown/text_rendering_spec.rb

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,12 @@
3030

3131
require "spec_helper"
3232

33-
RSpec.describe OpenProject::TextFormatting::Formats::Markdown::TextFormatter do
34-
subject(:formatted) { described_class.new(context).to_html(input).strip }
33+
RSpec.describe "Markdown plain-text rendering" do # rubocop:disable RSpec/DescribeClass
34+
subject(:formatted) { render(input).strip }
3535

36-
let(:context) { {} }
36+
def render(text)
37+
OpenProject::TextFormatting::Renderer.format_text(text, plain_text: true)
38+
end
3739

3840
describe "plain markdown" do
3941
let(:input) { "Hello *world*" }
@@ -85,26 +87,22 @@
8587
end
8688

8789
it "renders ##N as bare semantic identifier" do
88-
input = "see #{'##'}#{work_package.id} please"
89-
expect(described_class.new({}).to_html(input).strip).to eq("see DEMO-1 please")
90+
expect(render("see #{'##'}#{work_package.id} please").strip).to eq("see DEMO-1 please")
9091
end
9192

9293
it "renders ###N as bare semantic identifier" do
93-
input = "see #{'###'}#{work_package.id} please"
94-
expect(described_class.new({}).to_html(input).strip).to eq("see DEMO-1 please")
94+
expect(render("see #{'###'}#{work_package.id} please").strip).to eq("see DEMO-1 please")
9595
end
9696
end
9797

9898
context "in classic mode",
9999
with_settings: { work_packages_identifier: "classic" } do
100100
it "renders ##N as the hash-prefixed numeric id" do
101-
input = "see #{'##'}#{work_package.id} please"
102-
expect(described_class.new({}).to_html(input).strip).to eq("see ##{work_package.id} please")
101+
expect(render("see #{'##'}#{work_package.id} please").strip).to eq("see ##{work_package.id} please")
103102
end
104103

105104
it "renders ###N as the hash-prefixed numeric id" do
106-
input = "see #{'###'}#{work_package.id} please"
107-
expect(described_class.new({}).to_html(input).strip).to eq("see ##{work_package.id} please")
105+
expect(render("see #{'###'}#{work_package.id} please").strip).to eq("see ##{work_package.id} please")
108106
end
109107
end
110108
end

0 commit comments

Comments
 (0)