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 @@ -18,6 +18,7 @@
require_relative "ast/line_break"
require_relative "ast/list"
require_relative "ast/list_item"
require_relative "ast/table"
require_relative "ast/paragraph"
require_relative "ast/quote"
require_relative "ast/size"
Expand Down
67 changes: 67 additions & 0 deletions lib/markbridge/ast/table.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# frozen_string_literal: true

module Markbridge
module AST
# Represents a table element containing rows.
#
# @example
# table = AST::Table.new
# table << AST::TableRow.new
class Table < Element
# Add a child node to the table.
# Whitespace-only Text nodes are ignored.
#
# @param child [Node] the node to add
# @return [Table] self for chaining
def <<(child)
return self if child.is_a?(Text) && child.text.strip.empty?

super
end
end

# Represents a table row containing cells.
#
# @example
# row = AST::TableRow.new
# row << AST::TableCell.new
class TableRow < Element
# Add a child node to the row.
# Whitespace-only Text nodes are ignored.
#
# @param child [Node] the node to add
# @return [TableRow] self for chaining
def <<(child)
return self if child.is_a?(Text) && child.text.strip.empty?

super
end
end

# Represents a table cell (td or th).
#
# @example Data cell
# cell = AST::TableCell.new
# cell << AST::Text.new("data")
#
# @example Header cell
# cell = AST::TableCell.new(header: true)
# cell << AST::Text.new("header")
class TableCell < Element
# Create a new table cell.
#
# @param header [Boolean] whether this is a header cell (th)
def initialize(header: false)
super()
@header = header
end

# Check if this is a header cell.
#
# @return [Boolean] true if this is a header cell
def header?
@header
end
end
end
end
3 changes: 3 additions & 0 deletions lib/markbridge/parsers/bbcode.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@
require_relative "bbcode/handlers/simple_handler"
require_relative "bbcode/handlers/size_handler"
require_relative "bbcode/handlers/spoiler_handler"
require_relative "bbcode/handlers/table_handler"
require_relative "bbcode/handlers/table_row_handler"
require_relative "bbcode/handlers/table_cell_handler"
require_relative "bbcode/handlers/url_handler"

# Parser components
Expand Down
5 changes: 5 additions & 0 deletions lib/markbridge/parsers/bbcode/handler_registry.rb
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,11 @@ 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
registry.register("table", Handlers::TableHandler.new)
registry.register("tr", Handlers::TableRowHandler.new)
registry.register(%w[td th], Handlers::TableCellHandler.new)

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

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

module Markbridge
module Parsers
module BBCode
module Handlers
# Handler for table cell tags (td, th)
class TableCellHandler < BaseHandler
def initialize
@element_class = AST::TableCell
end

def on_open(token:, context:, registry:, tokens: nil)
# Auto-close previous cell if still open
context.pop if context.current.is_a?(AST::TableCell)

element = AST::TableCell.new(header: token.tag == "th")
context.push(element, token:)
end

attr_reader :element_class
end
end
end
end
end
32 changes: 32 additions & 0 deletions lib/markbridge/parsers/bbcode/handlers/table_handler.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# frozen_string_literal: true

module Markbridge
module Parsers
module BBCode
module Handlers
# Handler for table tags
class TableHandler < BaseHandler
def initialize
@element_class = AST::Table
end

def on_open(token:, context:, registry:, tokens: nil)
element = AST::Table.new
context.push(element, token:)
end

def on_close(token:, context:, registry:, tokens: nil)
# Auto-close open cell before closing row
context.pop if context.current.is_a?(AST::TableCell)
# Auto-close open row before closing table
context.pop if context.current.is_a?(AST::TableRow)

super
end

attr_reader :element_class
end
end
end
end
end
35 changes: 35 additions & 0 deletions lib/markbridge/parsers/bbcode/handlers/table_row_handler.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# frozen_string_literal: true

module Markbridge
module Parsers
module BBCode
module Handlers
# Handler for table row tags (tr)
class TableRowHandler < BaseHandler
def initialize
@element_class = AST::TableRow
end

def on_open(token:, context:, registry:, tokens: nil)
# Auto-close open cell before starting new row
context.pop if context.current.is_a?(AST::TableCell)
# Auto-close previous row if still open
context.pop if context.current.is_a?(AST::TableRow)

element = AST::TableRow.new
context.push(element, token:)
end

def on_close(token:, context:, registry:, tokens: nil)
# Auto-close open cell before closing row
context.pop if context.current.is_a?(AST::TableCell)

super
end

attr_reader :element_class
end
end
end
end
end
3 changes: 3 additions & 0 deletions lib/markbridge/parsers/html.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
require_relative "html/handlers/list_item_handler"
require_relative "html/handlers/quote_handler"
require_relative "html/handlers/paragraph_handler"
require_relative "html/handlers/table_handler"
require_relative "html/handlers/table_row_handler"
require_relative "html/handlers/table_cell_handler"

# Parser components
require_relative "html/handler_registry"
Expand Down
5 changes: 5 additions & 0 deletions lib/markbridge/parsers/html/handler_registry.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ def self.default
registry.register(%w[ul ol], Handlers::ListHandler.new)
registry.register("li", Handlers::ListItemHandler.new)

# Table handlers (thead/tbody/tfoot are transparent - unregistered tags pass through)
registry.register("table", Handlers::TableHandler.new)
registry.register("tr", Handlers::TableRowHandler.new)
registry.register(%w[td th], Handlers::TableCellHandler.new)

# Paragraph handler (transparent - doesn't create AST node)
registry.register("p", Handlers::ParagraphHandler.new)

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

module Markbridge
module Parsers
module HTML
module Handlers
# Handler for table cell tags (<td>, <th>)
class TableCellHandler < BaseHandler
def initialize
@element_class = AST::TableCell
end

def process(element:, parent:)
ast_element = AST::TableCell.new(header: element.name.downcase == "th")
parent << ast_element
ast_element
end

attr_reader :element_class
end
end
end
end
end
24 changes: 24 additions & 0 deletions lib/markbridge/parsers/html/handlers/table_handler.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# frozen_string_literal: true

module Markbridge
module Parsers
module HTML
module Handlers
# Handler for table tags (<table>)
class TableHandler < BaseHandler
def initialize
@element_class = AST::Table
end

def process(element:, parent:)
ast_element = AST::Table.new
parent << ast_element
ast_element
end

attr_reader :element_class
end
end
end
end
end
24 changes: 24 additions & 0 deletions lib/markbridge/parsers/html/handlers/table_row_handler.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# frozen_string_literal: true

module Markbridge
module Parsers
module HTML
module Handlers
# Handler for table row tags (<tr>)
class TableRowHandler < BaseHandler
def initialize
@element_class = AST::TableRow
end

def process(element:, parent:)
ast_element = AST::TableRow.new
parent << ast_element
ast_element
end

attr_reader :element_class
end
end
end
end
end
81 changes: 81 additions & 0 deletions lib/markbridge/parsers/media_wiki/parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ module MediaWiki
# - Internal links ([[target]] / [[target|display]])
# - External links ([url text])
# - Preformatted text (lines starting with a space)
# - Tables ({| ... |})
# - HTML tags: <nowiki>, <code>, <pre>, <br>, <s>, <del>, <u>, <ins>, <sup>, <sub>
#
# @example Basic usage
Expand Down Expand Up @@ -67,6 +68,9 @@ def process_lines(lines)
elsif horizontal_rule_line?(line)
close_open_lists
@document << AST::HorizontalRule.new
elsif table_start_line?(line)
close_open_lists
i = process_table(lines, i)
elsif list_line?(line)
process_list_item(line)
elsif preformatted_line?(line)
Expand Down Expand Up @@ -134,6 +138,83 @@ def blank_line?(line)
line.strip.empty?
end

# Check if a line starts a table ({|).
#
# @param line [String]
# @return [Boolean]
def table_start_line?(line)
line.match?(/\A\s*\{\|/)
end

# Process a table block from {| to |}.
# Consumes lines until the closing |} is found.
#
# @param lines [Array<String>]
# @param start_index [Integer]
# @return [Integer] the last index consumed
def process_table(lines, start_index)
table = AST::Table.new
current_row = nil
i = start_index + 1 # Skip the {| line

while i < lines.length
stripped = lines[i].strip

if stripped.start_with?("|}")
break
elsif stripped.start_with?("|-")
# Row separator - next cells will go in a new row
current_row = nil
elsif stripped.start_with?("!")
# Header cells
current_row = ensure_table_row(table, current_row)
parse_table_cells(stripped[1..], header: true, row: current_row)
elsif stripped.start_with?("|")
# Data cells
current_row = ensure_table_row(table, current_row)
parse_table_cells(stripped[1..], header: false, row: current_row)
end

i += 1
end

@document << table
i
end

# Ensure a row exists for the table, creating one if needed.
#
# @param table [AST::Table]
# @param current_row [AST::TableRow, nil]
# @return [AST::TableRow]
def ensure_table_row(table, current_row)
return current_row if current_row

row = AST::TableRow.new
table << row
row
end

# Parse cell content from a line and add cells to the row.
# Cells are separated by !! (headers) or || (data cells).
#
# @param content [String] the line content after the leading ! or |
# @param header [Boolean] whether these are header cells
# @param row [AST::TableRow]
def parse_table_cells(content, header:, row:)
separator = header ? "!!" : "||"
cells = content.split(separator)

cells.each do |raw_cell|
# A single | in a cell separates attributes from content
cell_text = raw_cell.include?("|") ? raw_cell.split("|", 2).last : raw_cell

cell = AST::TableCell.new(header:)
@inline_parser.parse(cell_text.strip, parent: cell)
row << cell
end
end

# Process a heading line and add it to the document.
#
# @param line [String]
Expand Down
Loading