Module: ApplicationHelper
- Defined in:
- app/helpers/application_helper.rb
Constant Summary collapse
- MARKDOWN =
Timeline text is Markdown (agents write it constantly). escape_html turns any raw HTML in the text into visible text instead of live DOM — an agent pasting
<div class=...>inside a code fence must never restyle the page. Redcarpet::Markdown.new( Redcarpet::Render::HTML.new(escape_html: true, hard_wrap: true, safe_links_only: true), fenced_code_blocks: true, tables: true, autolink: true, strikethrough: true, no_intra_emphasis: true, lax_spacing: true )
- ATTACHMENT_TOKEN =
Pasted files live inline in message/description text as attachment tokens (card #21). Format, kept in sync with attach_controller.js:
[[cardinal:file name="foo.png" mime="image/png" size="12345"]]<base64>[[/cardinal:file]]The regex is anchored on delimiters base64 can never contain ([, ], ").
/\[\[cardinal:file name="([^"]*)" mime="([^"]*)" size="(\d+)"\]\]([A-Za-z0-9+\/=\s]*?)\[\[\/cardinal:file\]\]/- ATTACHMENT_IMAGE_MIMES =
Only these render as an inline
off a data: URL. Anything else — including a spoofed mime like text/html — falls through to an inert badge, never an image.
%w[image/png image/jpeg image/gif image/webp].freeze
Instance Method Summary collapse
-
#card_model_options(card) ⇒ Object
Model options for a card's per-card override (card #33).
-
#footer_config_text(column) ⇒ Object
Render a column's stored footer config (array of compute hashes) back into the "Label | compute" line format the gear textarea edits.
- #info_tip(text) ⇒ Object
-
#model_options(current) ⇒ Object
Curated model choices for column policies.
-
#render_attachment(name, mime, size, base64) ⇒ Object
One attachment's HTML.
- #render_markdown(text) ⇒ Object
-
#render_with_attachments(text) ⇒ Object
Render text that may carry attachment tokens: markdown for the prose, a thumbnail (images) or a badge (everything else) for each attachment.
-
#strip_attachment_tokens(text) ⇒ Object
Strip attachment tokens down to just their filename for plain-text contexts like the card-face search haystack — a pasted image must not turn a card's searchable text into base64 noise (card #21).
Instance Method Details
#card_model_options(card) ⇒ Object
Model options for a card's per-card override (card #33). Same list as the
column gear, but the blank option reads "Use column default (
19 20 21 22 23 24 |
# File 'app/helpers/application_helper.rb', line 19 def (card) = (card.model) default = card.column.model_short.presence [0] = ["Use column default#{" (#{default})" if default}", ""] end |
#footer_config_text(column) ⇒ Object
Render a column's stored footer config (array of compute hashes) back into the "Label | compute" line format the gear textarea edits.
95 96 97 98 99 |
# File 'app/helpers/application_helper.rb', line 95 def (column) Array(column.).map do |row| [row["label"], row["compute"].presence].compact.join(" | ") end.join("\n") end |
#info_tip(text) ⇒ Object
101 102 103 104 105 106 |
# File 'app/helpers/application_helper.rb', line 101 def info_tip(text) tag.span("i", class: "info", data: { controller: "tooltip", tooltip_text_value: text, action: "mouseenter->tooltip#show mouseleave->tooltip#hide focus->tooltip#show blur->tooltip#hide" }, tabindex: 0) end |
#model_options(current) ⇒ Object
Curated model choices for column policies. A custom value already saved on the column stays selectable.
4 5 6 7 8 9 10 11 12 13 14 |
# File 'app/helpers/application_helper.rb', line 4 def (current) = [ ["(inherit default)", ""], ["Haiku 4.5 — fastest, cheapest", "claude-haiku-4-5-20251001"], ["Sonnet 4.6 — balanced, recommended for workers", "claude-sonnet-4-6"], ["Opus 4.8 — most capable", "claude-opus-4-8"], ["Fable 5 — frontier, expensive", "claude-fable-5"] ] << [current, current] if current.present? && .none? { |_, v| v == current } end |
#render_attachment(name, mime, size, base64) ⇒ Object
One attachment's HTML. All interpolated values are escaped; the base64 is
only ever used as an data: URL for a whitelisted image mime.
72 73 74 75 76 77 78 79 80 81 82 83 84 |
# File 'app/helpers/application_helper.rb', line 72 def (name, mime, size, base64) caption = "#{name} · #{number_to_human_size(size)}" if ATTACHMENT_IMAGE_MIMES.include?(mime) && base64.present? tag.figure(class: "attachment attachment-image") do image_tag("data:#{mime};base64,#{base64}", alt: name, class: "attachment-thumb") + tag.figcaption(caption) end else tag.span(class: "attachment attachment-file", title: caption) do tag.span("📄", class: "attachment-icon") + tag.span(caption, class: "attachment-name") end end end |
#render_markdown(text) ⇒ Object
35 36 37 |
# File 'app/helpers/application_helper.rb', line 35 def render_markdown(text) MARKDOWN.render(text.to_s).html_safe end |
#render_with_attachments(text) ⇒ Object
Render text that may carry attachment tokens: markdown for the prose, a thumbnail (images) or a badge (everything else) for each attachment. Tokens are pulled out BEFORE markdown so the raw base64 never floods the timeline and a token can never be interpreted as HTML.
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
# File 'app/helpers/application_helper.rb', line 54 def (text) text = text.to_s return render_markdown(text) unless text.match?(ATTACHMENT_TOKEN) html = +"" last = 0 text.scan(ATTACHMENT_TOKEN) do match = Regexp.last_match html << render_markdown(text[last...match.begin(0)]) html << (match[1], match[2], match[3].to_i, match[4].gsub(/\s+/, "")) last = match.end(0) end html << render_markdown(text[last..]) html.html_safe end |
#strip_attachment_tokens(text) ⇒ Object
Strip attachment tokens down to just their filename for plain-text contexts like the card-face search haystack — a pasted image must not turn a card's searchable text into base64 noise (card #21).
89 90 91 |
# File 'app/helpers/application_helper.rb', line 89 def (text) text.to_s.gsub(ATTACHMENT_TOKEN) { Regexp.last_match(1) } end |