markbridge 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/lib/markbridge/all.rb +9 -0
- data/lib/markbridge/ast/align.rb +24 -0
- data/lib/markbridge/ast/attachment.rb +42 -0
- data/lib/markbridge/ast/bold.rb +13 -0
- data/lib/markbridge/ast/code.rb +27 -0
- data/lib/markbridge/ast/color.rb +25 -0
- data/lib/markbridge/ast/document.rb +27 -0
- data/lib/markbridge/ast/element.rb +47 -0
- data/lib/markbridge/ast/email.rb +27 -0
- data/lib/markbridge/ast/event.rb +59 -0
- data/lib/markbridge/ast/heading.rb +23 -0
- data/lib/markbridge/ast/horizontal_rule.rb +12 -0
- data/lib/markbridge/ast/image.rb +35 -0
- data/lib/markbridge/ast/italic.rb +13 -0
- data/lib/markbridge/ast/line_break.rb +12 -0
- data/lib/markbridge/ast/list.rb +52 -0
- data/lib/markbridge/ast/list_item.rb +13 -0
- data/lib/markbridge/ast/markdown_text.rb +37 -0
- data/lib/markbridge/ast/mention.rb +29 -0
- data/lib/markbridge/ast/node.rb +19 -0
- data/lib/markbridge/ast/paragraph.rb +13 -0
- data/lib/markbridge/ast/poll.rb +74 -0
- data/lib/markbridge/ast/quote.rb +46 -0
- data/lib/markbridge/ast/size.rb +25 -0
- data/lib/markbridge/ast/spoiler.rb +27 -0
- data/lib/markbridge/ast/strikethrough.rb +13 -0
- data/lib/markbridge/ast/subscript.rb +13 -0
- data/lib/markbridge/ast/superscript.rb +13 -0
- data/lib/markbridge/ast/text.rb +38 -0
- data/lib/markbridge/ast/underline.rb +13 -0
- data/lib/markbridge/ast/upload.rb +74 -0
- data/lib/markbridge/ast/url.rb +27 -0
- data/lib/markbridge/ast.rb +42 -0
- data/lib/markbridge/configuration.rb +11 -0
- data/lib/markbridge/gem_loader.rb +23 -0
- data/lib/markbridge/parsers/bbcode/closing_strategies/base.rb +37 -0
- data/lib/markbridge/parsers/bbcode/closing_strategies/reordering.rb +17 -0
- data/lib/markbridge/parsers/bbcode/closing_strategies/strict.rb +12 -0
- data/lib/markbridge/parsers/bbcode/closing_strategies/tag_reconciler.rb +121 -0
- data/lib/markbridge/parsers/bbcode/errors/max_depth_exceeded_error.rb +13 -0
- data/lib/markbridge/parsers/bbcode/handler_registry.rb +160 -0
- data/lib/markbridge/parsers/bbcode/handlers/align_handler.rb +26 -0
- data/lib/markbridge/parsers/bbcode/handlers/attachment_handler.rb +104 -0
- data/lib/markbridge/parsers/bbcode/handlers/base_handler.rb +44 -0
- data/lib/markbridge/parsers/bbcode/handlers/code_handler.rb +25 -0
- data/lib/markbridge/parsers/bbcode/handlers/color_handler.rb +31 -0
- data/lib/markbridge/parsers/bbcode/handlers/email_handler.rb +25 -0
- data/lib/markbridge/parsers/bbcode/handlers/image_handler.rb +51 -0
- data/lib/markbridge/parsers/bbcode/handlers/list_handler.rb +36 -0
- data/lib/markbridge/parsers/bbcode/handlers/list_item_handler.rb +26 -0
- data/lib/markbridge/parsers/bbcode/handlers/quote_handler.rb +64 -0
- data/lib/markbridge/parsers/bbcode/handlers/raw_handler.rb +48 -0
- data/lib/markbridge/parsers/bbcode/handlers/self_closing_handler.rb +28 -0
- data/lib/markbridge/parsers/bbcode/handlers/simple_handler.rb +28 -0
- data/lib/markbridge/parsers/bbcode/handlers/size_handler.rb +31 -0
- data/lib/markbridge/parsers/bbcode/handlers/spoiler_handler.rb +28 -0
- data/lib/markbridge/parsers/bbcode/handlers/url_handler.rb +24 -0
- data/lib/markbridge/parsers/bbcode/parser.rb +123 -0
- data/lib/markbridge/parsers/bbcode/parser_state.rb +93 -0
- data/lib/markbridge/parsers/bbcode/peekable_enumerator.rb +126 -0
- data/lib/markbridge/parsers/bbcode/raw_content_collector.rb +35 -0
- data/lib/markbridge/parsers/bbcode/raw_content_result.rb +25 -0
- data/lib/markbridge/parsers/bbcode/scanner.rb +231 -0
- data/lib/markbridge/parsers/bbcode/tokens/tag_end_token.rb +21 -0
- data/lib/markbridge/parsers/bbcode/tokens/tag_start_token.rb +23 -0
- data/lib/markbridge/parsers/bbcode/tokens/text_token.rb +23 -0
- data/lib/markbridge/parsers/bbcode/tokens/token.rb +16 -0
- data/lib/markbridge/parsers/bbcode.rb +56 -0
- data/lib/markbridge/parsers/html/handler_registry.rb +87 -0
- data/lib/markbridge/parsers/html/handlers/base_handler.rb +27 -0
- data/lib/markbridge/parsers/html/handlers/image_handler.rb +40 -0
- data/lib/markbridge/parsers/html/handlers/list_handler.rb +29 -0
- data/lib/markbridge/parsers/html/handlers/list_item_handler.rb +26 -0
- data/lib/markbridge/parsers/html/handlers/paragraph_handler.rb +17 -0
- data/lib/markbridge/parsers/html/handlers/quote_handler.rb +28 -0
- data/lib/markbridge/parsers/html/handlers/raw_handler.rb +33 -0
- data/lib/markbridge/parsers/html/handlers/simple_handler.rb +26 -0
- data/lib/markbridge/parsers/html/handlers/url_handler.rb +27 -0
- data/lib/markbridge/parsers/html/parser.rb +113 -0
- data/lib/markbridge/parsers/html.rb +30 -0
- data/lib/markbridge/parsers/media_wiki/inline_parser.rb +332 -0
- data/lib/markbridge/parsers/media_wiki/parser.rb +279 -0
- data/lib/markbridge/parsers/media_wiki.rb +15 -0
- data/lib/markbridge/parsers/text_formatter/handler_registry.rb +130 -0
- data/lib/markbridge/parsers/text_formatter/handlers/attachment_handler.rb +33 -0
- data/lib/markbridge/parsers/text_formatter/handlers/attribute_handler.rb +40 -0
- data/lib/markbridge/parsers/text_formatter/handlers/base_handler.rb +45 -0
- data/lib/markbridge/parsers/text_formatter/handlers/code_handler.rb +28 -0
- data/lib/markbridge/parsers/text_formatter/handlers/email_handler.rb +27 -0
- data/lib/markbridge/parsers/text_formatter/handlers/image_handler.rb +32 -0
- data/lib/markbridge/parsers/text_formatter/handlers/list_handler.rb +31 -0
- data/lib/markbridge/parsers/text_formatter/handlers/quote_handler.rb +33 -0
- data/lib/markbridge/parsers/text_formatter/handlers/simple_handler.rb +37 -0
- data/lib/markbridge/parsers/text_formatter/handlers/url_handler.rb +29 -0
- data/lib/markbridge/parsers/text_formatter/parser.rb +132 -0
- data/lib/markbridge/parsers/text_formatter.rb +31 -0
- data/lib/markbridge/processors/discourse_markdown/code_block_tracker.rb +199 -0
- data/lib/markbridge/processors/discourse_markdown/detectors/base.rb +57 -0
- data/lib/markbridge/processors/discourse_markdown/detectors/event.rb +73 -0
- data/lib/markbridge/processors/discourse_markdown/detectors/mention.rb +57 -0
- data/lib/markbridge/processors/discourse_markdown/detectors/poll.rb +90 -0
- data/lib/markbridge/processors/discourse_markdown/detectors/upload.rb +123 -0
- data/lib/markbridge/processors/discourse_markdown/scanner.rb +199 -0
- data/lib/markbridge/processors/discourse_markdown.rb +16 -0
- data/lib/markbridge/processors.rb +8 -0
- data/lib/markbridge/renderers/discourse/builders/list_item_builder.rb +83 -0
- data/lib/markbridge/renderers/discourse/markdown_escaper.rb +468 -0
- data/lib/markbridge/renderers/discourse/render_context.rb +80 -0
- data/lib/markbridge/renderers/discourse/renderer.rb +63 -0
- data/lib/markbridge/renderers/discourse/rendering_interface.rb +86 -0
- data/lib/markbridge/renderers/discourse/tag.rb +29 -0
- data/lib/markbridge/renderers/discourse/tag_library.rb +67 -0
- data/lib/markbridge/renderers/discourse/tags/align_tag.rb +24 -0
- data/lib/markbridge/renderers/discourse/tags/attachment_tag.rb +46 -0
- data/lib/markbridge/renderers/discourse/tags/bold_tag.rb +18 -0
- data/lib/markbridge/renderers/discourse/tags/code_tag.rb +54 -0
- data/lib/markbridge/renderers/discourse/tags/color_tag.rb +27 -0
- data/lib/markbridge/renderers/discourse/tags/email_tag.rb +24 -0
- data/lib/markbridge/renderers/discourse/tags/event_tag.rb +49 -0
- data/lib/markbridge/renderers/discourse/tags/heading_tag.rb +21 -0
- data/lib/markbridge/renderers/discourse/tags/horizontal_rule_tag.rb +16 -0
- data/lib/markbridge/renderers/discourse/tags/image_tag.rb +29 -0
- data/lib/markbridge/renderers/discourse/tags/italic_tag.rb +18 -0
- data/lib/markbridge/renderers/discourse/tags/line_break_tag.rb +16 -0
- data/lib/markbridge/renderers/discourse/tags/list_item_tag.rb +87 -0
- data/lib/markbridge/renderers/discourse/tags/list_tag.rb +39 -0
- data/lib/markbridge/renderers/discourse/tags/mention_tag.rb +34 -0
- data/lib/markbridge/renderers/discourse/tags/paragraph_tag.rb +21 -0
- data/lib/markbridge/renderers/discourse/tags/poll_tag.rb +51 -0
- data/lib/markbridge/renderers/discourse/tags/quote_tag.rb +32 -0
- data/lib/markbridge/renderers/discourse/tags/size_tag.rb +27 -0
- data/lib/markbridge/renderers/discourse/tags/spoiler_tag.rb +24 -0
- data/lib/markbridge/renderers/discourse/tags/strikethrough_tag.rb +18 -0
- data/lib/markbridge/renderers/discourse/tags/subscript_tag.rb +19 -0
- data/lib/markbridge/renderers/discourse/tags/superscript_tag.rb +19 -0
- data/lib/markbridge/renderers/discourse/tags/underline_tag.rb +19 -0
- data/lib/markbridge/renderers/discourse/tags/upload_tag.rb +80 -0
- data/lib/markbridge/renderers/discourse/tags/url_tag.rb +24 -0
- data/lib/markbridge/renderers/discourse.rb +50 -0
- data/lib/markbridge/version.rb +5 -0
- data/lib/markbridge.rb +201 -0
- metadata +186 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Markbridge
|
|
4
|
+
module AST
|
|
5
|
+
# Represents a Discourse poll.
|
|
6
|
+
#
|
|
7
|
+
# @example Basic poll
|
|
8
|
+
# poll = AST::Poll.new(
|
|
9
|
+
# name: "poll",
|
|
10
|
+
# type: "regular",
|
|
11
|
+
# options: ["A", "B", "C"]
|
|
12
|
+
# )
|
|
13
|
+
#
|
|
14
|
+
# @example Poll with all attributes
|
|
15
|
+
# poll = AST::Poll.new(
|
|
16
|
+
# name: "favorite-color",
|
|
17
|
+
# type: "multiple",
|
|
18
|
+
# results: "on_vote",
|
|
19
|
+
# public: true,
|
|
20
|
+
# chart_type: "pie",
|
|
21
|
+
# options: ["Red", "Blue", "Green"],
|
|
22
|
+
# raw: "[poll name=\"favorite-color\"]..."
|
|
23
|
+
# )
|
|
24
|
+
class Poll < Node
|
|
25
|
+
# @return [String] the poll name/identifier
|
|
26
|
+
attr_reader :name
|
|
27
|
+
|
|
28
|
+
# @return [String, nil] poll type (regular, multiple, number)
|
|
29
|
+
attr_reader :type
|
|
30
|
+
|
|
31
|
+
# @return [String, nil] when to show results (always, on_vote, on_close, staff_only)
|
|
32
|
+
attr_reader :results
|
|
33
|
+
|
|
34
|
+
# @return [Boolean] whether votes are public
|
|
35
|
+
attr_reader :public
|
|
36
|
+
|
|
37
|
+
# @return [String, nil] chart type (bar, pie)
|
|
38
|
+
attr_reader :chart_type
|
|
39
|
+
|
|
40
|
+
# @return [Array<String>] poll options
|
|
41
|
+
attr_reader :options
|
|
42
|
+
|
|
43
|
+
# @return [String, nil] the original raw BBCode
|
|
44
|
+
attr_reader :raw
|
|
45
|
+
|
|
46
|
+
# Create a new Poll node.
|
|
47
|
+
#
|
|
48
|
+
# @param name [String] the poll name/identifier
|
|
49
|
+
# @param type [String, nil] poll type
|
|
50
|
+
# @param results [String, nil] when to show results
|
|
51
|
+
# @param public [Boolean] whether votes are public
|
|
52
|
+
# @param chart_type [String, nil] chart type
|
|
53
|
+
# @param options [Array<String>] poll options
|
|
54
|
+
# @param raw [String, nil] the original raw BBCode
|
|
55
|
+
def initialize(
|
|
56
|
+
name: "poll",
|
|
57
|
+
type: nil,
|
|
58
|
+
results: nil,
|
|
59
|
+
public: false,
|
|
60
|
+
chart_type: nil,
|
|
61
|
+
options: [],
|
|
62
|
+
raw: nil
|
|
63
|
+
)
|
|
64
|
+
@name = name
|
|
65
|
+
@type = type
|
|
66
|
+
@results = results
|
|
67
|
+
@public = public
|
|
68
|
+
@chart_type = chart_type
|
|
69
|
+
@options = options
|
|
70
|
+
@raw = raw
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Markbridge
|
|
4
|
+
module AST
|
|
5
|
+
# Represents a quote/blockquote element.
|
|
6
|
+
#
|
|
7
|
+
# @example Basic quote
|
|
8
|
+
# quote = AST::Quote.new
|
|
9
|
+
# quote << AST::Text.new("quoted text")
|
|
10
|
+
#
|
|
11
|
+
# @example Quote with author attribution
|
|
12
|
+
# quote = AST::Quote.new(author: "John")
|
|
13
|
+
# quote << AST::Text.new("quoted text")
|
|
14
|
+
#
|
|
15
|
+
# @example Quote with full Discourse context
|
|
16
|
+
# quote = AST::Quote.new(author: "John", post: "123", topic: "456", username: "john123")
|
|
17
|
+
# quote << AST::Text.new("quoted text")
|
|
18
|
+
class Quote < Element
|
|
19
|
+
# @return [String, nil] the author/username of the quote
|
|
20
|
+
attr_reader :author
|
|
21
|
+
|
|
22
|
+
# @return [String, nil] the post ID for Discourse quotes
|
|
23
|
+
attr_reader :post
|
|
24
|
+
|
|
25
|
+
# @return [String, nil] the topic ID for Discourse quotes
|
|
26
|
+
attr_reader :topic
|
|
27
|
+
|
|
28
|
+
# @return [String, nil] the username for Discourse quotes
|
|
29
|
+
attr_reader :username
|
|
30
|
+
|
|
31
|
+
# Create a new Quote element.
|
|
32
|
+
#
|
|
33
|
+
# @param author [String, nil] the author attribution
|
|
34
|
+
# @param post [String, nil] the post ID (Discourse-specific)
|
|
35
|
+
# @param topic [String, nil] the topic ID (Discourse-specific)
|
|
36
|
+
# @param username [String, nil] the username (Discourse-specific)
|
|
37
|
+
def initialize(author: nil, post: nil, topic: nil, username: nil)
|
|
38
|
+
super()
|
|
39
|
+
@author = author
|
|
40
|
+
@post = post
|
|
41
|
+
@topic = topic
|
|
42
|
+
@username = username
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Markbridge
|
|
4
|
+
module AST
|
|
5
|
+
# Represents sized text.
|
|
6
|
+
# Note: Discourse doesn't support inline size changes by default,
|
|
7
|
+
# but this preserves size information for migration/custom rendering.
|
|
8
|
+
#
|
|
9
|
+
# @example Sized text
|
|
10
|
+
# size = AST::Size.new(size: "20")
|
|
11
|
+
# size << AST::Text.new("Big text")
|
|
12
|
+
class Size < Element
|
|
13
|
+
# @return [String, nil] the font size value
|
|
14
|
+
attr_reader :size
|
|
15
|
+
|
|
16
|
+
# Create a new Size element.
|
|
17
|
+
#
|
|
18
|
+
# @param size [String, nil] font size value
|
|
19
|
+
def initialize(size: nil)
|
|
20
|
+
super()
|
|
21
|
+
@size = size
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Markbridge
|
|
4
|
+
module AST
|
|
5
|
+
# Represents a spoiler element (hidden content).
|
|
6
|
+
#
|
|
7
|
+
# @example Basic spoiler
|
|
8
|
+
# spoiler = AST::Spoiler.new
|
|
9
|
+
# spoiler << AST::Text.new("Hidden content")
|
|
10
|
+
#
|
|
11
|
+
# @example Spoiler with title
|
|
12
|
+
# spoiler = AST::Spoiler.new(title: "Click to reveal")
|
|
13
|
+
# spoiler << AST::Text.new("Hidden content")
|
|
14
|
+
class Spoiler < Element
|
|
15
|
+
# @return [String, nil] the spoiler title/label
|
|
16
|
+
attr_reader :title
|
|
17
|
+
|
|
18
|
+
# Create a new Spoiler element.
|
|
19
|
+
#
|
|
20
|
+
# @param title [String, nil] optional title for the spoiler
|
|
21
|
+
def initialize(title: nil)
|
|
22
|
+
super()
|
|
23
|
+
@title = title
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Markbridge
|
|
4
|
+
module AST
|
|
5
|
+
# Represents strikethrough/deleted text formatting.
|
|
6
|
+
#
|
|
7
|
+
# @example
|
|
8
|
+
# strikethrough = AST::Strikethrough.new
|
|
9
|
+
# strikethrough << AST::Text.new("deleted text")
|
|
10
|
+
class Strikethrough < Element
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Markbridge
|
|
4
|
+
module AST
|
|
5
|
+
# Represents subscript text (e.g., for chemical formulas).
|
|
6
|
+
#
|
|
7
|
+
# @example Subscript text
|
|
8
|
+
# sub = AST::Subscript.new
|
|
9
|
+
# sub << AST::Text.new("2") # For H2O
|
|
10
|
+
class Subscript < Element
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Markbridge
|
|
4
|
+
module AST
|
|
5
|
+
# Represents superscript text (e.g., for exponents).
|
|
6
|
+
#
|
|
7
|
+
# @example Superscript text
|
|
8
|
+
# sup = AST::Superscript.new
|
|
9
|
+
# sup << AST::Text.new("2") # For x^2
|
|
10
|
+
class Superscript < Element
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Markbridge
|
|
4
|
+
module AST
|
|
5
|
+
# Represents a text node (leaf node) in the AST.
|
|
6
|
+
# Text nodes contain the actual text content and cannot have children.
|
|
7
|
+
#
|
|
8
|
+
# @example Creating a text node
|
|
9
|
+
# text = AST::Text.new("Hello, world!")
|
|
10
|
+
#
|
|
11
|
+
# @example Merging text nodes
|
|
12
|
+
# text1 = AST::Text.new("Hello")
|
|
13
|
+
# text2 = AST::Text.new(" world")
|
|
14
|
+
# text1.merge(text2)
|
|
15
|
+
# text1.text # => "Hello world"
|
|
16
|
+
class Text < Node
|
|
17
|
+
# @return [String] the text content of this node
|
|
18
|
+
attr_reader :text
|
|
19
|
+
|
|
20
|
+
# Create a new text node with the given content.
|
|
21
|
+
#
|
|
22
|
+
# @param text [String] the text content
|
|
23
|
+
def initialize(text)
|
|
24
|
+
@text = +text
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Merge another text node's content into this one.
|
|
28
|
+
# This mutates the current text node by appending the other's text.
|
|
29
|
+
#
|
|
30
|
+
# @param other [Text] the text node to merge from
|
|
31
|
+
# @return [Text] self for method chaining
|
|
32
|
+
def merge(other)
|
|
33
|
+
@text << other.text
|
|
34
|
+
self
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Markbridge
|
|
4
|
+
module AST
|
|
5
|
+
# Represents underlined text formatting.
|
|
6
|
+
#
|
|
7
|
+
# @example
|
|
8
|
+
# underline = AST::Underline.new
|
|
9
|
+
# underline << AST::Text.new("underlined text")
|
|
10
|
+
class Underline < Element
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Markbridge
|
|
4
|
+
module AST
|
|
5
|
+
# Represents a Discourse upload reference (image or file attachment).
|
|
6
|
+
# Uses the upload:// URL scheme.
|
|
7
|
+
#
|
|
8
|
+
# @example Image upload
|
|
9
|
+
# upload = AST::Upload.new(
|
|
10
|
+
# sha1: "RBhXLF6381Te3mneJQNnnyNNt5",
|
|
11
|
+
# filename: "image.png",
|
|
12
|
+
# type: :image,
|
|
13
|
+
# alt: "My image",
|
|
14
|
+
# dimensions: "64x64"
|
|
15
|
+
# )
|
|
16
|
+
#
|
|
17
|
+
# @example File attachment
|
|
18
|
+
# upload = AST::Upload.new(
|
|
19
|
+
# sha1: "ppJp89TTiLOo6Vl4mAmo21MV28w",
|
|
20
|
+
# filename: "document.pdf",
|
|
21
|
+
# type: :attachment,
|
|
22
|
+
# size: "502.1 KB"
|
|
23
|
+
# )
|
|
24
|
+
class Upload < Node
|
|
25
|
+
# @return [String] the base62 SHA1 identifier from upload:// URL
|
|
26
|
+
attr_reader :sha1
|
|
27
|
+
|
|
28
|
+
# @return [String, nil] original filename
|
|
29
|
+
attr_reader :filename
|
|
30
|
+
|
|
31
|
+
# @return [Symbol] type of upload (:image or :attachment)
|
|
32
|
+
attr_reader :type
|
|
33
|
+
|
|
34
|
+
# @return [String, nil] alt text (for images)
|
|
35
|
+
attr_reader :alt
|
|
36
|
+
|
|
37
|
+
# @return [String, nil] dimensions string like "64x64" (for images)
|
|
38
|
+
attr_reader :dimensions
|
|
39
|
+
|
|
40
|
+
# @return [String, nil] file size string like "502.1 KB" (for attachments)
|
|
41
|
+
attr_reader :size
|
|
42
|
+
|
|
43
|
+
# @return [String, nil] the original raw Markdown
|
|
44
|
+
attr_reader :raw
|
|
45
|
+
|
|
46
|
+
# Create a new Upload node.
|
|
47
|
+
#
|
|
48
|
+
# @param sha1 [String] the base62 SHA1 identifier
|
|
49
|
+
# @param filename [String, nil] original filename
|
|
50
|
+
# @param type [Symbol] type of upload (:image or :attachment)
|
|
51
|
+
# @param alt [String, nil] alt text
|
|
52
|
+
# @param dimensions [String, nil] dimensions string
|
|
53
|
+
# @param size [String, nil] file size string
|
|
54
|
+
# @param raw [String, nil] the original raw Markdown
|
|
55
|
+
def initialize(
|
|
56
|
+
sha1:,
|
|
57
|
+
filename: nil,
|
|
58
|
+
type: :image,
|
|
59
|
+
alt: nil,
|
|
60
|
+
dimensions: nil,
|
|
61
|
+
size: nil,
|
|
62
|
+
raw: nil
|
|
63
|
+
)
|
|
64
|
+
@sha1 = sha1
|
|
65
|
+
@filename = filename
|
|
66
|
+
@type = type
|
|
67
|
+
@alt = alt
|
|
68
|
+
@dimensions = dimensions
|
|
69
|
+
@size = size
|
|
70
|
+
@raw = raw
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Markbridge
|
|
4
|
+
module AST
|
|
5
|
+
# Represents a hyperlink/URL element.
|
|
6
|
+
#
|
|
7
|
+
# @example URL with explicit href
|
|
8
|
+
# url = AST::Url.new(href: "https://example.com")
|
|
9
|
+
# url << AST::Text.new("Click here")
|
|
10
|
+
#
|
|
11
|
+
# @example URL with text as href
|
|
12
|
+
# url = AST::Url.new(href: "https://example.com")
|
|
13
|
+
# url << AST::Text.new("https://example.com")
|
|
14
|
+
class Url < Element
|
|
15
|
+
# @return [String, nil] the URL/href for this link
|
|
16
|
+
attr_reader :href
|
|
17
|
+
|
|
18
|
+
# Create a new URL element.
|
|
19
|
+
#
|
|
20
|
+
# @param href [String, nil] the URL/href for this link
|
|
21
|
+
def initialize(href: nil)
|
|
22
|
+
super()
|
|
23
|
+
@href = href
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# base / dependency order first
|
|
4
|
+
require_relative "ast/node"
|
|
5
|
+
require_relative "ast/element"
|
|
6
|
+
require_relative "ast/document"
|
|
7
|
+
|
|
8
|
+
require_relative "ast/align"
|
|
9
|
+
require_relative "ast/attachment"
|
|
10
|
+
require_relative "ast/bold"
|
|
11
|
+
require_relative "ast/code"
|
|
12
|
+
require_relative "ast/color"
|
|
13
|
+
require_relative "ast/email"
|
|
14
|
+
require_relative "ast/heading"
|
|
15
|
+
require_relative "ast/horizontal_rule"
|
|
16
|
+
require_relative "ast/image"
|
|
17
|
+
require_relative "ast/italic"
|
|
18
|
+
require_relative "ast/line_break"
|
|
19
|
+
require_relative "ast/list"
|
|
20
|
+
require_relative "ast/list_item"
|
|
21
|
+
require_relative "ast/paragraph"
|
|
22
|
+
require_relative "ast/quote"
|
|
23
|
+
require_relative "ast/size"
|
|
24
|
+
require_relative "ast/spoiler"
|
|
25
|
+
require_relative "ast/strikethrough"
|
|
26
|
+
require_relative "ast/subscript"
|
|
27
|
+
require_relative "ast/superscript"
|
|
28
|
+
require_relative "ast/text"
|
|
29
|
+
require_relative "ast/markdown_text"
|
|
30
|
+
require_relative "ast/underline"
|
|
31
|
+
require_relative "ast/url"
|
|
32
|
+
|
|
33
|
+
# Discourse-specific nodes
|
|
34
|
+
require_relative "ast/event"
|
|
35
|
+
require_relative "ast/mention"
|
|
36
|
+
require_relative "ast/poll"
|
|
37
|
+
require_relative "ast/upload"
|
|
38
|
+
|
|
39
|
+
module Markbridge
|
|
40
|
+
module AST
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Markbridge
|
|
4
|
+
module GemLoader
|
|
5
|
+
class << self
|
|
6
|
+
def require_gem(gem, feature:)
|
|
7
|
+
require gem.to_s
|
|
8
|
+
rescue LoadError
|
|
9
|
+
raise LoadError, missing_message(gem, feature)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def missing_message(gem, feature)
|
|
15
|
+
gem_name = gem.to_s
|
|
16
|
+
[
|
|
17
|
+
"#{gem_name.capitalize} is required for #{feature}.",
|
|
18
|
+
"Add 'gem \"#{gem_name}\"' to your Gemfile or install it with 'gem install #{gem_name}'.",
|
|
19
|
+
].join(" ")
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module Markbridge
|
|
3
|
+
module Parsers
|
|
4
|
+
module BBCode
|
|
5
|
+
module ClosingStrategies
|
|
6
|
+
class Base
|
|
7
|
+
def initialize(reconciler)
|
|
8
|
+
@reconciler = reconciler
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def handle_close(token:, context:, registry:, tokens: nil)
|
|
12
|
+
return unless context.current.is_a?(AST::Element)
|
|
13
|
+
|
|
14
|
+
current_handler = registry.handler_for_element(context.current)
|
|
15
|
+
closing_handler = registry[token.tag]
|
|
16
|
+
|
|
17
|
+
if current_handler == closing_handler
|
|
18
|
+
context.pop
|
|
19
|
+
elsif try_reorder(context:, tokens:, closing_handler:)
|
|
20
|
+
# Reordering handled
|
|
21
|
+
elsif @reconciler.try_auto_close(handler: closing_handler, context:)
|
|
22
|
+
# Auto-close succeeded
|
|
23
|
+
else
|
|
24
|
+
context.add_child(AST::Text.new(token.source))
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def try_reorder(context:, tokens:, closing_handler:)
|
|
31
|
+
false
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module Markbridge
|
|
3
|
+
module Parsers
|
|
4
|
+
module BBCode
|
|
5
|
+
module ClosingStrategies
|
|
6
|
+
class Reordering < Base
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def try_reorder(context:, tokens:, closing_handler:)
|
|
10
|
+
return false unless tokens
|
|
11
|
+
@reconciler.try_reorder(handler: closing_handler, tokens:, context:)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Markbridge
|
|
4
|
+
module Parsers
|
|
5
|
+
module BBCode
|
|
6
|
+
module ClosingStrategies
|
|
7
|
+
# Encapsulates logic for reconciling mismatched closing tags
|
|
8
|
+
class TagReconciler
|
|
9
|
+
MAX_AUTO_CLOSE_DEPTH = 5
|
|
10
|
+
|
|
11
|
+
def initialize(registry:)
|
|
12
|
+
@registry = registry
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Attempt to auto-close tags to match a closing tag
|
|
16
|
+
#
|
|
17
|
+
# @param handler [BaseHandler] the handler for the closing tag
|
|
18
|
+
# @param context [ParserState]
|
|
19
|
+
# @return [Boolean] true if successful, false if auto-close not possible
|
|
20
|
+
def try_auto_close(handler:, context:)
|
|
21
|
+
match_depth = find_matching_handler_depth(handler, context)
|
|
22
|
+
|
|
23
|
+
return false if match_depth.nil? || match_depth >= MAX_AUTO_CLOSE_DEPTH
|
|
24
|
+
return false unless all_auto_closeable?(context, match_depth)
|
|
25
|
+
|
|
26
|
+
count = match_depth + 1
|
|
27
|
+
count.times { context.pop }
|
|
28
|
+
context.auto_close!(count)
|
|
29
|
+
|
|
30
|
+
true
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Attempt to reorder closing tags
|
|
34
|
+
#
|
|
35
|
+
# @param handler [BaseHandler] the handler for the closing tag
|
|
36
|
+
# @param tokens [Object] the token stream
|
|
37
|
+
# @param context [ParserState]
|
|
38
|
+
# @return [Boolean] true if successful, false otherwise
|
|
39
|
+
def try_reorder(handler:, tokens:, context:)
|
|
40
|
+
match_depth = find_matching_handler_depth(handler, context)
|
|
41
|
+
return false if match_depth.nil? || match_depth >= MAX_AUTO_CLOSE_DEPTH
|
|
42
|
+
|
|
43
|
+
opening_handlers = collect_auto_closeable_handlers(context, match_depth)
|
|
44
|
+
return false if opening_handlers.empty?
|
|
45
|
+
|
|
46
|
+
closing_handlers = [handler]
|
|
47
|
+
closing_handlers.concat(peek_closing_handlers(tokens, opening_handlers.size - 1))
|
|
48
|
+
return false if closing_handlers.size != opening_handlers.size
|
|
49
|
+
unless opening_handlers.sort_by(&:object_id) == closing_handlers.sort_by(&:object_id)
|
|
50
|
+
return false
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Consume the extra closing tags
|
|
54
|
+
(opening_handlers.size - 1).times do
|
|
55
|
+
peeked = tokens.peek
|
|
56
|
+
break unless peeked.is_a?(TagEndToken)
|
|
57
|
+
tokens.next
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
opening_handlers.each { context.pop }
|
|
61
|
+
context.auto_close!(opening_handlers.size)
|
|
62
|
+
|
|
63
|
+
true
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def find_matching_handler_depth(handler, context)
|
|
69
|
+
elements = context.elements_from_current(MAX_AUTO_CLOSE_DEPTH)
|
|
70
|
+
|
|
71
|
+
elements.each_with_index do |element, depth|
|
|
72
|
+
next unless element.is_a?(AST::Element)
|
|
73
|
+
|
|
74
|
+
element_handler = @registry.handler_for_element(element)
|
|
75
|
+
return depth if element_handler == handler
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
nil
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def all_auto_closeable?(context, target_depth)
|
|
82
|
+
context
|
|
83
|
+
.elements_from_current(target_depth)
|
|
84
|
+
.all? { |element| @registry.auto_closeable?(element.class) }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def collect_auto_closeable_handlers(context, target_depth)
|
|
88
|
+
handlers = []
|
|
89
|
+
|
|
90
|
+
context
|
|
91
|
+
.elements_from_current(target_depth)
|
|
92
|
+
.each do |element|
|
|
93
|
+
return [] unless @registry.auto_closeable?(element.class)
|
|
94
|
+
|
|
95
|
+
handler = @registry.handler_for_element(element)
|
|
96
|
+
handlers << handler if handler
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
handlers
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def peek_closing_handlers(tokens, max_count)
|
|
103
|
+
handlers = []
|
|
104
|
+
peeked_tokens = tokens.peek_ahead(max_count)
|
|
105
|
+
|
|
106
|
+
peeked_tokens.each do |token|
|
|
107
|
+
break unless token.is_a?(TagEndToken)
|
|
108
|
+
|
|
109
|
+
handler = @registry[token.tag]
|
|
110
|
+
break unless handler
|
|
111
|
+
|
|
112
|
+
handlers << handler
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
handlers
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|