Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/markbridge/ast.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
require_relative "ast/strikethrough"
require_relative "ast/subscript"
require_relative "ast/superscript"
require_relative "ast/table"
require_relative "ast/text"
require_relative "ast/markdown_text"
require_relative "ast/underline"
Expand Down
14 changes: 14 additions & 0 deletions lib/markbridge/ast/table.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# frozen_string_literal: true

module Markbridge
module AST
class Table < Element
end

class TableRow < Element
end

class TableCell < Element
end
end
end
1 change: 1 addition & 0 deletions lib/markbridge/parsers/bbcode.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
require_relative "bbcode/handlers/color_handler"
require_relative "bbcode/handlers/email_handler"
require_relative "bbcode/handlers/image_handler"
require_relative "bbcode/handlers/img2_handler"
require_relative "bbcode/handlers/list_handler"
require_relative "bbcode/handlers/list_item_handler"
require_relative "bbcode/handlers/quote_handler"
Expand Down
11 changes: 11 additions & 0 deletions lib/markbridge/parsers/bbcode/handler_registry.rb
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ def self.default(closing_strategy: nil)
# Image handler
registry.register("img", Handlers::ImageHandler.new)

# IMG2 handler (vBulletin 5 enhanced image)
registry.register("img2", Handlers::Img2Handler.new)

# Attachment handler
registry.register(%w[attach attachment], Handlers::AttachmentHandler.new)

Expand Down Expand Up @@ -132,6 +135,14 @@ def self.default(closing_strategy: nil)
registry.register(%w[list ul ol ulist olist], Handlers::ListHandler.new)
registry.register(%w[* li .], Handlers::ListItemHandler.new)

# Table handlers (passthrough: strip wrapper tags, preserve cell content)
registry.register("table", Handlers::SimpleHandler.new(AST::Table, auto_closeable: true))
registry.register("tr", Handlers::SimpleHandler.new(AST::TableRow, auto_closeable: true))
registry.register(
%w[td th],
Handlers::SimpleHandler.new(AST::TableCell, auto_closeable: true),
)

# Set the closing strategy
registry.closing_strategy = closing_strategy || default_closing_strategy(registry)

Expand Down
58 changes: 58 additions & 0 deletions lib/markbridge/parsers/bbcode/handlers/img2_handler.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# frozen_string_literal: true

module Markbridge
module Parsers
module BBCode
module Handlers
# Handler for vBulletin 5 [IMG2] tags.
#
# Supports:
# - [IMG2=JSON]{"src":"http://example.com/image.png",...}[/IMG2]
# - [IMG2]http://example.com/image.png[/IMG2]
class Img2Handler < BaseHandler
def initialize(collector: RawContentCollector.new)
@collector = collector
@element_class = AST::Image
end

def on_open(token:, context:, registry:, tokens: nil)
content = collect_content(token:, tokens:)
return unless content

src = extract_src(token, content.strip)
return if src.nil? || src.empty?

context.add_child(AST::Image.new(src:))
end

def on_close(token:, context:, registry:, tokens: nil)
context.add_child(AST::Text.new(token.source))
end

attr_reader :element_class

private

def collect_content(token:, tokens:)
return unless tokens
return unless closing_tag_ahead?(token.tag, tokens)

@collector.collect(token.tag, tokens).content
end

def closing_tag_ahead?(tag, tokens)
tokens.peek_ahead(100).any? { |t| t.is_a?(TagEndToken) && t.tag == tag }
end

def extract_src(token, content)
if token.attrs[:option]&.downcase == "json" && content.start_with?("{")
$1 if content =~ /"src"\s*:\s*"([^"]+)"/
else
content
end
end
end
end
end
end
end
9 changes: 9 additions & 0 deletions lib/markbridge/renderers/discourse/tag_library.rb
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,15 @@ def self.default
library.register AST::LineBreak, Tags::LineBreakTag.new
library.register AST::HorizontalRule, Tags::HorizontalRuleTag.new

# Table tags: passthrough — strip wrapper tags, preserve cell content
passthrough =
Tag.new do |element, interface|
interface.render_children(element, context: interface.with_parent(element))
end
library.register AST::Table, passthrough
library.register AST::TableRow, passthrough
library.register AST::TableCell, passthrough

library
end
end
Expand Down
7 changes: 3 additions & 4 deletions lib/markbridge/renderers/discourse/tags/quote_tag.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,12 @@ def render(element, interface)
# Format: [quote="username, post:123, topic:456"]content[/quote]
if element.post && element.topic && element.username
# Full Discourse quote with context
"[quote=\"#{element.username}, post:#{element.post}, topic:#{element.topic}\"]\n#{content}\n[/quote]"
"[quote=\"#{element.username}, post:#{element.post}, topic:#{element.topic}\"]\n#{content}\n[/quote]\n\n"
elsif element.author
# Quote with author attribution only
"[quote=\"#{element.author}\"]\n#{content}\n[/quote]"
"[quote=\"#{element.author}\"]\n#{content}\n[/quote]\n\n"
else
# Plain quote - could use Markdown blockquote or BBCode
# Using Markdown blockquote for plain quotes
# Plain Markdown blockquote — no trailing \n\n needed; surrounding paragraph context handles spacing
content.split("\n").map { |line| "> #{line}" }.join("\n")
end
end
Expand Down
6 changes: 6 additions & 0 deletions playground/ast_presenter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ class ASTPresenter
"Subscript" => "formatting",
"Superscript" => "formatting",
"Text" => "text",
"Table" => "block",
"TableRow" => "block",
"TableCell" => "block",
"Underline" => "formatting",
"Upload" => "media",
"Url" => "link",
Expand Down Expand Up @@ -62,6 +65,9 @@ class ASTPresenter
"Subscript" => "subscript",
"Superscript" => "superscript",
"Text" => "textCursor",
"Table" => "table",
"TableRow" => "tableRow",
"TableCell" => "tableCell",
"Underline" => "underline",
"Upload" => "upload",
"Url" => "link",
Expand Down
32 changes: 32 additions & 0 deletions spec/system/bbcode_to_markdown_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -508,4 +508,36 @@
expect(result).to eq(expected)
end
end

describe "table tags" do
it "strips [TABLE][TR][TD] wrapper tags and preserves cell content" do
bbcode = "[TABLE][TR][TD]cell content[/TD][/TR][/TABLE]"
expect(Markbridge.bbcode_to_markdown(bbcode)).to eq("cell content")
end

it "preserves content from multiple cells" do
bbcode = "[TABLE][TR][TD]first[/TD][TD]second[/TD][/TR][/TABLE]"
expect(Markbridge.bbcode_to_markdown(bbcode)).to eq("firstsecond")
end

it "strips [TH] header cells and preserves content" do
bbcode = "[TABLE][TR][TH]Header[/TH][/TR][TR][TD]value[/TD][/TR][/TABLE]"
expect(Markbridge.bbcode_to_markdown(bbcode)).to eq("Headervalue")
end

it "preserves formatted content inside table cells" do
bbcode = "[TABLE][TR][TD][B]bold cell[/B][/TD][/TR][/TABLE]"
expect(Markbridge.bbcode_to_markdown(bbcode)).to eq("**bold cell**")
end

it "handles empty cells" do
bbcode = "[TABLE][TR][TD][/TD][/TR][/TABLE]"
expect(Markbridge.bbcode_to_markdown(bbcode)).to eq("")
end

it "handles whitespace-only cells" do
bbcode = "[TABLE][TR][TD] [/TD][/TR][/TABLE]"
expect(Markbridge.bbcode_to_markdown(bbcode)).to eq("")
end
end
end
37 changes: 37 additions & 0 deletions spec/unit/markbridge/ast/table_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# frozen_string_literal: true

RSpec.describe Markbridge::AST::Table do
it "is an Element" do
expect(described_class.new).to be_a(Markbridge::AST::Element)
end

it "can have children" do
table = described_class.new
table << Markbridge::AST::TableRow.new
expect(table.children.size).to eq(1)
end
end

RSpec.describe Markbridge::AST::TableRow do
it "is an Element" do
expect(described_class.new).to be_a(Markbridge::AST::Element)
end

it "can have children" do
row = described_class.new
row << Markbridge::AST::TableCell.new
expect(row.children.size).to eq(1)
end
end

RSpec.describe Markbridge::AST::TableCell do
it "is an Element" do
expect(described_class.new).to be_a(Markbridge::AST::Element)
end

it "can have children" do
cell = described_class.new
cell << Markbridge::AST::Text.new("content")
expect(cell.children.size).to eq(1)
end
end
Loading