Class: Markbridge::Renderers::Discourse::Renderer

Inherits:
Object
  • Object
show all
Defined in:
lib/markbridge/renderers/discourse/renderer.rb

Overview

Renders AST to Discourse-flavored Markdown in-memory.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(tag_library: nil, escaper: nil, html_escaper: nil, postprocessor: nil) ⇒ Renderer

Returns a new instance of Renderer.



10
11
12
13
14
15
16
17
18
# File 'lib/markbridge/renderers/discourse/renderer.rb', line 10

def initialize(tag_library: nil, escaper: nil, html_escaper: nil, postprocessor: nil)
  @tag_library = tag_library || TagLibrary.shared_default
  @escaper = escaper || MarkdownEscaper.new
  @html_escaper = html_escaper || HtmlEscaper
  @postprocessor = postprocessor || Postprocessor::DEFAULT
  # @interface_cache, @resolved_tags, and @resolved_default_tags
  # are lazily initialized during a top-level #render /
  # #render_default call and reset to nil after the call completes.
end

Instance Attribute Details

#postprocessorObject (readonly)

Returns the value of attribute postprocessor.



8
9
10
# File 'lib/markbridge/renderers/discourse/renderer.rb', line 8

def postprocessor
  @postprocessor
end

Instance Method Details

#render(node, context: RenderContext.new) ⇒ String

Render a node to Markdown

Parameters:

  • node (AST::Node)
  • context (RenderContext) (defaults to: RenderContext.new)

    rendering context with parent chain

Returns:

  • (String)

Raises:

  • (TypeError)

    when the tag bound to the node's class returns something other than a String (a nil from a custom tag would otherwise surface as an inscrutable concatenation error deep inside render_children)



28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/markbridge/renderers/discourse/renderer.rb', line 28

def render(node, context: RenderContext.new)
  root_call = @interface_cache.nil?
  @interface_cache = {} if root_call

  # Exact-class hit first (a single Hash lookup, the common case),
  # then the memoized ancestry fallback so a subclass without its
  # own tag renders through its nearest ancestor's tag.
  tag = @tag_library[node.class] || resolved_tag(node.class)
  if tag
    result = tag.render(node, interface_for(context))
    unless result.is_a?(String)
      raise TypeError,
            "#{tag.class} rendered #{node.class} to " \
              "#{result.inspect} — tags must return a String " \
              "(use interface.render_default(node) to fall back " \
              "to the stock rendering)"
    end
    return result
  end

  render_without_tag(node, context)
ensure
  if root_call
    @interface_cache = nil
    @resolved_tags = nil
    @resolved_default_tags = nil
  end
end

#render_children(node, context:) {|result, child| ... } ⇒ String

Render all children of a node

With a block, the block runs at every join point, right before a non-empty child output is appended. It receives the buffer built so far and the child about to be rendered into it, and may change the buffer in place. A tag uses this to adjust the text in front of a specific child without iterating the children itself, which would lose the emphasis-boundary rule below.

Examples:

Attach a nested list directly to the text in front of it

interface.render_children(item, context:) do |buffer, child|
  buffer.rstrip! if child.is_a?(AST::List)
end

Parameters:

Yield Parameters:

  • result (String)

    the buffer built so far

  • child (AST::Node)

    the child about to be appended

Returns:

  • (String)


108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/markbridge/renderers/discourse/renderer.rb', line 108

def render_children(node, context:)
  result = +""
  node.children.each do |child|
    part = render(child, context:)
    next if part.empty?

    yield(result, child) if block_given?

    # Integer-byte check avoids allocating substrings for the
    # per-child adjacency probe. EMPHASIS_DELIMITER_BYTES.include?
    # over a 4-element Set is O(1). On an empty buffer getbyte
    # returns nil, which matches no byte of a non-empty part, so
    # the first child needs no extra guard.
    if (last_byte = result.getbyte(-1)) == part.getbyte(0) &&
         EMPHASIS_DELIMITER_BYTES.include?(last_byte)
      result << EMPHASIS_BOUNDARY
    end
    result << part
  end
  result
end

#render_default(node, context: RenderContext.new) ⇒ String

Render a node with the stock tag for its class, ignoring any override registered in this renderer's tag library. Lets a custom tag intercept only the nodes it cares about and delegate the rest:

library.register(AST::Quote, Tag.new do |node, interface|
next interface.render_default(node) unless node.username&.start_with?("legacy_")
...custom rendering...
end)

Children still render through this renderer, so overrides for other node classes keep applying inside the delegated subtree.

Parameters:

  • node (AST::Node)
  • context (RenderContext) (defaults to: RenderContext.new)

    rendering context with parent chain

Returns:

  • (String)


73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# File 'lib/markbridge/renderers/discourse/renderer.rb', line 73

def render_default(node, context: RenderContext.new)
  root_call = @interface_cache.nil?
  @interface_cache = {} if root_call

  tag = default_tag_library[node.class] || resolved_default_tag(node.class)
  return tag.render(node, interface_for(context)) if tag

  render_without_tag(node, context)
ensure
  if root_call
    @interface_cache = nil
    @resolved_tags = nil
    @resolved_default_tags = nil
  end
end