markly 0.16.0 → 0.18.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.
data/ext/markly/node.c CHANGED
@@ -145,16 +145,154 @@ cmark_node *cmark_node_new(cmark_node_type type) {
145
145
  return cmark_node_new_with_ext(type, NULL);
146
146
  }
147
147
 
148
+ static cmark_chunk S_clone_chunk(cmark_mem *mem, const cmark_chunk *source) {
149
+ cmark_chunk clone = {NULL, source->len, 1};
150
+ clone.data = (unsigned char *)mem->calloc(source->len + 1, 1);
151
+ if (source->len > 0) {
152
+ memcpy(clone.data, source->data, source->len);
153
+ }
154
+ return clone;
155
+ }
156
+
157
+ static cmark_node *S_clone_node(cmark_node *node) {
158
+ cmark_mem *mem = NODE_MEM(node);
159
+ cmark_node *clone = cmark_node_new_with_mem_and_ext(
160
+ (cmark_node_type)node->type, mem, node->extension);
161
+
162
+ if (!clone) {
163
+ return NULL;
164
+ }
165
+
166
+ cmark_strbuf_set(&clone->content, node->content.ptr, node->content.size);
167
+ clone->start_line = node->start_line;
168
+ clone->start_column = node->start_column;
169
+ clone->end_line = node->end_line;
170
+ clone->end_column = node->end_column;
171
+ clone->internal_offset = node->internal_offset;
172
+ clone->flags = node->flags;
173
+ clone->footnote = node->footnote;
174
+
175
+ switch (node->type) {
176
+ case CMARK_NODE_HEADING:
177
+ clone->as.heading = node->as.heading;
178
+ break;
179
+ case CMARK_NODE_LIST:
180
+ case CMARK_NODE_ITEM:
181
+ clone->as.list = node->as.list;
182
+ break;
183
+ case CMARK_NODE_CODE_BLOCK:
184
+ case CMARK_NODE_FRONT_MATTER:
185
+ case CMARK_NODE_CODE:
186
+ clone->as.code = node->as.code;
187
+ clone->as.code.info = S_clone_chunk(mem, &node->as.code.info);
188
+ clone->as.code.literal = S_clone_chunk(mem, &node->as.code.literal);
189
+ break;
190
+ case CMARK_NODE_TEXT:
191
+ case CMARK_NODE_HTML_INLINE:
192
+ case CMARK_NODE_HTML_BLOCK:
193
+ case CMARK_NODE_FOOTNOTE_REFERENCE:
194
+ case CMARK_NODE_FOOTNOTE_DEFINITION:
195
+ clone->as.literal = S_clone_chunk(mem, &node->as.literal);
196
+ break;
197
+ case CMARK_NODE_LINK:
198
+ case CMARK_NODE_IMAGE:
199
+ clone->as.link.url = S_clone_chunk(mem, &node->as.link.url);
200
+ clone->as.link.title = S_clone_chunk(mem, &node->as.link.title);
201
+ break;
202
+ case CMARK_NODE_CUSTOM_BLOCK:
203
+ case CMARK_NODE_CUSTOM_INLINE:
204
+ clone->as.custom.on_enter = S_clone_chunk(mem, &node->as.custom.on_enter);
205
+ clone->as.custom.on_exit = S_clone_chunk(mem, &node->as.custom.on_exit);
206
+ break;
207
+ default:
208
+ break;
209
+ }
210
+
211
+ if (node->extension && node->extension->opaque_alloc_func) {
212
+ if (!node->extension->opaque_copy_func) {
213
+ cmark_node_free(clone);
214
+ return NULL;
215
+ }
216
+
217
+ node->extension->opaque_copy_func(node->extension, mem, clone, node);
218
+ }
219
+
220
+ for (cmark_node *child = node->first_child; child; child = child->next) {
221
+ cmark_node *child_clone = S_clone_node(child);
222
+ if (!child_clone) {
223
+ cmark_node_free(clone);
224
+ return NULL;
225
+ }
226
+
227
+ if (!cmark_node_append_child(clone, child_clone)) {
228
+ cmark_node_free(child_clone);
229
+ cmark_node_free(clone);
230
+ return NULL;
231
+ }
232
+ }
233
+
234
+ return clone;
235
+ }
236
+
237
+ static cmark_node *S_find_clone(cmark_node *source, cmark_node *clone,
238
+ cmark_node *target) {
239
+ if (source == target) {
240
+ return clone;
241
+ }
242
+
243
+ cmark_node *source_child = source->first_child;
244
+ cmark_node *clone_child = clone->first_child;
245
+ while (source_child && clone_child) {
246
+ cmark_node *result = S_find_clone(source_child, clone_child, target);
247
+ if (result) {
248
+ return result;
249
+ }
250
+ source_child = source_child->next;
251
+ clone_child = clone_child->next;
252
+ }
253
+
254
+ return NULL;
255
+ }
256
+
257
+ static void S_clone_footnote_links(cmark_node *source, cmark_node *clone,
258
+ cmark_node *source_root,
259
+ cmark_node *clone_root) {
260
+ if (source->parent_footnote_def) {
261
+ clone->parent_footnote_def =
262
+ S_find_clone(source_root, clone_root, source->parent_footnote_def);
263
+ }
264
+
265
+ cmark_node *source_child = source->first_child;
266
+ cmark_node *clone_child = clone->first_child;
267
+ while (source_child && clone_child) {
268
+ S_clone_footnote_links(source_child, clone_child, source_root, clone_root);
269
+ source_child = source_child->next;
270
+ clone_child = clone_child->next;
271
+ }
272
+ }
273
+
274
+ cmark_node *cmark_node_clone(cmark_node *node) {
275
+ if (!node) {
276
+ return NULL;
277
+ }
278
+
279
+ cmark_node *clone = S_clone_node(node);
280
+ if (clone) {
281
+ S_clone_footnote_links(node, clone, node, clone);
282
+ }
283
+ return clone;
284
+ }
285
+
148
286
  static void free_node_as(cmark_node *node) {
149
287
  switch (node->type) {
150
288
  case CMARK_NODE_CODE_BLOCK:
151
289
  case CMARK_NODE_FRONT_MATTER:
290
+ case CMARK_NODE_CODE:
152
291
  cmark_chunk_free(NODE_MEM(node), &node->as.code.info);
153
292
  cmark_chunk_free(NODE_MEM(node), &node->as.code.literal);
154
293
  break;
155
294
  case CMARK_NODE_TEXT:
156
295
  case CMARK_NODE_HTML_INLINE:
157
- case CMARK_NODE_CODE:
158
296
  case CMARK_NODE_HTML_BLOCK:
159
297
  case CMARK_NODE_FOOTNOTE_REFERENCE:
160
298
  case CMARK_NODE_FOOTNOTE_DEFINITION:
@@ -378,11 +516,11 @@ const char *cmark_node_get_literal(cmark_node *node) {
378
516
  case CMARK_NODE_HTML_BLOCK:
379
517
  case CMARK_NODE_TEXT:
380
518
  case CMARK_NODE_HTML_INLINE:
381
- case CMARK_NODE_CODE:
382
519
  case CMARK_NODE_FOOTNOTE_REFERENCE:
383
520
  case CMARK_NODE_FOOTNOTE_DEFINITION:
384
521
  return cmark_chunk_to_cstr(NODE_MEM(node), &node->as.literal);
385
522
 
523
+ case CMARK_NODE_CODE:
386
524
  case CMARK_NODE_CODE_BLOCK:
387
525
  case CMARK_NODE_FRONT_MATTER:
388
526
  return cmark_chunk_to_cstr(NODE_MEM(node), &node->as.code.literal);
@@ -403,11 +541,11 @@ int cmark_node_set_literal(cmark_node *node, const char *content) {
403
541
  case CMARK_NODE_HTML_BLOCK:
404
542
  case CMARK_NODE_TEXT:
405
543
  case CMARK_NODE_HTML_INLINE:
406
- case CMARK_NODE_CODE:
407
544
  case CMARK_NODE_FOOTNOTE_REFERENCE:
408
545
  cmark_chunk_set_cstr(NODE_MEM(node), &node->as.literal, content);
409
546
  return 1;
410
547
 
548
+ case CMARK_NODE_CODE:
411
549
  case CMARK_NODE_CODE_BLOCK:
412
550
  case CMARK_NODE_FRONT_MATTER:
413
551
  cmark_chunk_set_cstr(NODE_MEM(node), &node->as.code.literal, content);
@@ -595,6 +733,59 @@ int cmark_node_set_item_index(cmark_node *node, int idx) {
595
733
  }
596
734
  }
597
735
 
736
+ const char *cmark_node_get_code_info(cmark_node *node) {
737
+ if (node == NULL) {
738
+ return NULL;
739
+ }
740
+
741
+ if (node->type == CMARK_NODE_CODE ||
742
+ node->type == CMARK_NODE_CODE_BLOCK ||
743
+ node->type == CMARK_NODE_FRONT_MATTER) {
744
+ return cmark_chunk_to_cstr(NODE_MEM(node), &node->as.code.info);
745
+ } else {
746
+ return NULL;
747
+ }
748
+ }
749
+
750
+ static int valid_inline_code_info(const char *info) {
751
+ if (info == NULL || *info == '\0') {
752
+ return 1;
753
+ }
754
+
755
+ if (!cmark_is_inline_code_info_start_char(*info)) {
756
+ return 0;
757
+ }
758
+
759
+ while (*++info) {
760
+ if (!cmark_is_inline_code_info_char(*info)) {
761
+ return 0;
762
+ }
763
+ }
764
+
765
+ return 1;
766
+ }
767
+
768
+ int cmark_node_set_code_info(cmark_node *node, const char *info) {
769
+ if (node == NULL) {
770
+ return 0;
771
+ }
772
+
773
+ if (node->type == CMARK_NODE_CODE) {
774
+ if (!valid_inline_code_info(info)) {
775
+ return 0;
776
+ }
777
+
778
+ cmark_chunk_set_cstr(NODE_MEM(node), &node->as.code.info, info);
779
+ return 1;
780
+ } else if (node->type == CMARK_NODE_CODE_BLOCK ||
781
+ node->type == CMARK_NODE_FRONT_MATTER) {
782
+ cmark_chunk_set_cstr(NODE_MEM(node), &node->as.code.info, info);
783
+ return 1;
784
+ } else {
785
+ return 0;
786
+ }
787
+ }
788
+
598
789
  const char *cmark_node_get_fence_info(cmark_node *node) {
599
790
  if (node == NULL) {
600
791
  return NULL;
@@ -143,6 +143,11 @@ void cmark_syntax_extension_set_opaque_free_func(cmark_syntax_extension *extensi
143
143
  extension->opaque_free_func = func;
144
144
  }
145
145
 
146
+ void cmark_syntax_extension_set_opaque_copy_func(
147
+ cmark_syntax_extension *extension, cmark_opaque_copy_func func) {
148
+ extension->opaque_copy_func = func;
149
+ }
150
+
146
151
  void cmark_syntax_extension_set_commonmark_escape_func(cmark_syntax_extension *extension,
147
152
  cmark_commonmark_escape_func func) {
148
153
  extension->commonmark_escape_func = func;
@@ -28,6 +28,7 @@ struct cmark_syntax_extension {
28
28
  cmark_postprocess_func postprocess_func;
29
29
  cmark_opaque_alloc_func opaque_alloc_func;
30
30
  cmark_opaque_free_func opaque_free_func;
31
+ cmark_opaque_copy_func opaque_copy_func;
31
32
  cmark_commonmark_escape_func commonmark_escape_func;
32
33
  };
33
34
 
data/ext/markly/xml.c CHANGED
@@ -72,7 +72,6 @@ static int S_render_node(cmark_node *node, cmark_event_type ev_type,
72
72
  literal = true;
73
73
  break;
74
74
  case CMARK_NODE_TEXT:
75
- case CMARK_NODE_CODE:
76
75
  case CMARK_NODE_HTML_BLOCK:
77
76
  case CMARK_NODE_HTML_INLINE:
78
77
  cmark_strbuf_puts(xml, " xml:space=\"preserve\">");
@@ -81,6 +80,18 @@ static int S_render_node(cmark_node *node, cmark_event_type ev_type,
81
80
  cmark_strbuf_puts(xml, cmark_node_get_type_string(node));
82
81
  literal = true;
83
82
  break;
83
+ case CMARK_NODE_CODE:
84
+ if (node->as.code.info.len > 0) {
85
+ cmark_strbuf_puts(xml, " info=\"");
86
+ escape_xml(xml, node->as.code.info.data, node->as.code.info.len);
87
+ cmark_strbuf_putc(xml, '"');
88
+ }
89
+ cmark_strbuf_puts(xml, " xml:space=\"preserve\">");
90
+ escape_xml(xml, node->as.code.literal.data, node->as.code.literal.len);
91
+ cmark_strbuf_puts(xml, "</");
92
+ cmark_strbuf_puts(xml, cmark_node_get_type_string(node));
93
+ literal = true;
94
+ break;
84
95
  case CMARK_NODE_LIST:
85
96
  switch (cmark_node_get_list_type(node)) {
86
97
  case CMARK_ORDERED_LIST:
data/lib/markly/flags.rb CHANGED
@@ -20,12 +20,15 @@ module Markly
20
20
  UNSAFE = 1 << 17
21
21
  # Parse front matter ("---" delimited block at start of document).
22
22
  # The raw content is available via node.string_content and the optional
23
- # format hint (e.g. "yaml", "toml") via node.fence_info; interpretation
23
+ # format hint (e.g. "yaml", "toml") via node.code_info; interpretation
24
24
  # is left to the caller.
25
25
  FRONT_MATTER = 1 << 18
26
+ # Parse language prefixes on inline code spans, e.g. ruby:`Object.new`.
27
+ INLINE_CODE_INFO = 1 << 19
26
28
 
27
29
  PARSE_FLAGS = {
28
30
  front_matter: FRONT_MATTER,
31
+ inline_code_info: INLINE_CODE_INFO,
29
32
  validate_utf8: VALIDATE_UTF8,
30
33
  smart_quotes: SMART,
31
34
  liberal_html_tags: LIBERAL_HTML_TAG,
@@ -4,20 +4,27 @@
4
4
  # Copyright, 2017, by Goro Fuji.
5
5
  # Copyright, 2017-2019, by Garen Torikian.
6
6
  # Copyright, 2020, by Olle Jonsson.
7
- # Copyright, 2020-2025, by Samuel Williams.
7
+ # Copyright, 2020-2026, by Samuel Williams.
8
8
 
9
9
  require "pp"
10
10
 
11
11
  module Markly
12
12
  class Node
13
+ # Provides concise pretty-printing for Markdown nodes.
13
14
  module Inspect
15
+ # @constant [Integer] The indentation width used for nested node attributes.
14
16
  PP_INDENT_SIZE = 2
15
17
 
18
+ # Returns a pretty-printed representation of the node.
19
+ #
20
+ # @returns [String] The formatted node representation.
16
21
  def inspect
17
22
  PP.pp(self, +"", Float::INFINITY)
18
23
  end
19
24
 
20
- # @param printer [PrettyPrint] pp
25
+ # Pretty-print this node and its children.
26
+ #
27
+ # @parameter printer [PrettyPrint] The pretty-print formatter.
21
28
  def pretty_print(printer)
22
29
  printer.group(PP_INDENT_SIZE, "#<#{self.class}(#{type}):", ">") do
23
30
  printer.breakable
@@ -31,7 +38,7 @@ module Markly
31
38
  list_type
32
39
  list_start
33
40
  list_tight
34
- fence_info
41
+ code_info
35
42
  ].map do |name|
36
43
  begin
37
44
  [name, __send__(name)]
data/lib/markly/node.rb CHANGED
@@ -5,32 +5,29 @@
5
5
  # Copyright, 2016-2017, by Yuki Izumi.
6
6
  # Copyright, 2017, by Goro Fuji.
7
7
  # Copyright, 2018, by Jerry van Leeuwen.
8
- # Copyright, 2020-2025, by Samuel Williams.
8
+ # Copyright, 2020-2026, by Samuel Williams.
9
9
  # Copyright, 2025, by Olle Jonsson.
10
10
 
11
11
  require_relative "node/inspect"
12
12
 
13
13
  module Markly
14
+ # Represents a node in a parsed Markdown document tree.
14
15
  class Node
15
16
  include Enumerable
16
17
  include Inspect
17
18
 
18
- # Duplicate the current node and all children.
19
+ # Duplicate the current node and all its children.
20
+ #
21
+ # @returns [Markly::Node] The duplicated node tree.
19
22
  def dup
20
- # This is a bit crazy, but it's the best I can come up with right now:
21
- node = Markly.parse(self.to_markdown)
22
-
23
- # If we aren't duplicating a document, we return `first_child` as the root will be a document node:
24
- if self.type == :document
25
- return node
26
- else
27
- return node.first_child
28
- end
23
+ _dup
29
24
  end
30
25
 
31
- # Public: An iterator that "walks the tree," descending into children recursively.
26
+ # Walk the node tree recursively.
32
27
  #
33
- # block - A {Proc} representing the action to take for each child
28
+ # @yields {|node| ...} Each node in depth-first order, including this node.
29
+ # @parameter node [Markly::Node] The current node.
30
+ # @returns [Enumerator | Nil] An enumerator when no block is given.
34
31
  def walk(&block)
35
32
  return enum_for(:walk) unless block_given?
36
33
 
@@ -40,39 +37,47 @@ module Markly
40
37
  end
41
38
  end
42
39
 
43
- # Public: Convert the node to an HTML string.
40
+ # Convert the node to an HTML string.
44
41
  #
45
- # flags - A {Symbol} or {Array of Symbol}s indicating the render options
46
- # extensions - An {Array of Symbol}s indicating the extensions to use
47
- #
48
- # Returns a {String}.
42
+ # @parameter flags [Integer] The enabled rendering flags.
43
+ # @parameter extensions [Array(Symbol)] The extensions to enable.
44
+ # @returns [String] The rendered HTML.
49
45
  def to_html(flags: DEFAULT, extensions: [])
50
46
  _render_html(flags, extensions).force_encoding("utf-8")
51
47
  end
52
48
 
53
- # Public: Convert the node to a CommonMark string.
54
- #
55
- # flags - A {Symbol} or {Array of Symbol}s indicating the render options
56
- # width - Column to wrap the output at
49
+ # Convert the node to a CommonMark string.
57
50
  #
58
- # Returns a {String}.
51
+ # @parameter flags [Integer] The enabled rendering flags.
52
+ # @parameter width [Integer] The column at which to wrap output, or `0` to disable wrapping.
53
+ # @returns [String] The rendered CommonMark text.
59
54
  def to_commonmark(flags: DEFAULT, width: 0)
60
55
  _render_commonmark(flags, width).force_encoding("utf-8")
61
56
  end
62
57
 
63
58
  alias to_markdown to_commonmark
64
59
 
65
- # Public: Convert the node to a plain text string.
60
+ # Return the language identifier from the code info string.
66
61
  #
67
- # flags - A {Symbol} or {Array of Symbol}s indicating the render options
68
- # width - Column to wrap the output at
62
+ # @returns [String | Nil] The language identifier, or `nil` when none is present.
63
+ def code_language
64
+ code_info.split(/\s+/, 2).first
65
+ end
66
+
67
+ # Convert the node to a plain-text string.
69
68
  #
70
- # Returns a {String}.
69
+ # @parameter flags [Integer] The enabled rendering flags.
70
+ # @parameter width [Integer] The column at which to wrap output, or `0` to disable wrapping.
71
+ # @returns [String] The rendered plain text.
71
72
  def to_plaintext(flags: DEFAULT, width: 0)
72
73
  _render_plaintext(flags, width).force_encoding("utf-8")
73
74
  end
74
75
 
75
- # Public: Iterate over the children (if any) of the current pointer.
76
+ # Iterate over the direct children of this node.
77
+ #
78
+ # @yields {|child| ...} Each direct child of this node.
79
+ # @parameter child [Markly::Node] The current child node.
80
+ # @returns [Enumerator | Nil] An enumerator when no block is given.
76
81
  def each
77
82
  return enum_for(:each) unless block_given?
78
83
 
@@ -84,6 +89,10 @@ module Markly
84
89
  end
85
90
  end
86
91
 
92
+ # Finds a direct child header with the given text.
93
+ #
94
+ # @parameter title [String] The header text to match.
95
+ # @returns [Markly::Node | Nil] The matching header, if present.
87
96
  def find_header(title)
88
97
  each do |child|
89
98
  if child.type == :header && child.first_child.string_content == title
@@ -94,7 +103,9 @@ module Markly
94
103
 
95
104
  # Delete all nodes until the block returns true.
96
105
  #
97
- # @returns [Markly::Node] the node that returned true.
106
+ # @yields {|node| ...} Each node before it is deleted.
107
+ # @parameter node [Markly::Node] The current node.
108
+ # @returns [Markly::Node | Nil] The node for which the block returned `true`, if any.
98
109
  def delete_until
99
110
  current = self
100
111
  while current
@@ -107,9 +118,9 @@ module Markly
107
118
 
108
119
  # Replace a section (header + content) with a new node.
109
120
  #
110
- # @parameter new_node [Markly::Node] the node to replace the section with.
111
- # @parameter replace_header [Boolean] whether to replace the header itself or not.
112
- # @parameter remove_subsections [Boolean] whether to remove subsections or not.
121
+ # @parameter new_node [Markly::Node | Nil] The node with which to replace the section.
122
+ # @parameter replace_header [Boolean] Whether to replace the header itself.
123
+ # @parameter remove_subsections [Boolean] Whether to remove subsections.
113
124
  def replace_section(new_node, replace_header: true, remove_subsections: true)
114
125
  # Delete until the next heading:
115
126
  self.next&.delete_until do |node|
@@ -120,21 +131,27 @@ module Markly
120
131
  self.delete if replace_header
121
132
  end
122
133
 
123
- def next_heading
134
+ # Finds the next sibling header.
135
+ #
136
+ # @returns [Markly::Node | Nil] The next header, if present.
137
+ def next_header
124
138
  current = self.next
125
139
  while current
126
- if current.type == :heading
140
+ if current.type == :header
127
141
  return current
128
142
  end
129
143
  current = current.next
130
144
  end
131
145
  end
132
146
 
147
+ # An alias for {ruby Markly::Node#next_header}.
148
+ alias next_heading next_header
149
+
133
150
  # Append the given node after the current node.
134
151
  #
135
152
  # It's okay to provide a document node, its children will be appended.
136
153
  #
137
- # @parameter node [Markly::Node] the node to append.
154
+ # @parameter node [Markly::Node] The node to append.
138
155
  def append_after(node)
139
156
  if node.type == :document
140
157
  node = node.first_child
@@ -153,7 +170,7 @@ module Markly
153
170
  #
154
171
  # It's okay to provide a document node, its children will be appended.
155
172
  #
156
- # @parameter node [Markly::Node] the node to append.
173
+ # @parameter node [Markly::Node] The node to append.
157
174
  def append_before(node)
158
175
  if node.type == :document
159
176
  node = node.first_child
@@ -169,7 +186,7 @@ module Markly
169
186
 
170
187
  # Extract the children as a fragment.
171
188
  #
172
- # @returns [Markly::Node] the fragment.
189
+ # @returns [Markly::Node] The fragment.
173
190
  def extract_children
174
191
  fragment = Markly::Node.new(:custom_inline)
175
192
 
@@ -3,26 +3,37 @@
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2015-2019, by Garen Torikian.
5
5
  # Copyright, 2016-2017, by Yuki Izumi.
6
- # Copyright, 2020-2025, by Samuel Williams.
6
+ # Copyright, 2020-2026, by Samuel Williams.
7
7
 
8
8
  require "set"
9
9
  require "stringio"
10
10
 
11
11
  module Markly
12
+ # @namespace
12
13
  module Renderer
14
+ # Base class for renderers implemented in Ruby.
13
15
  class Generic
16
+ # Initializes a renderer with rendering flags and extensions.
17
+ #
18
+ # @parameter flags [Integer] The enabled rendering flags.
19
+ # @parameter extensions [Array(Symbol)] The enabled extensions.
14
20
  def initialize(flags: DEFAULT, extensions: [])
15
21
  @flags = flags
16
22
  @stream = StringIO.new(+"")
17
- @need_blocksep = false
18
23
  @in_tight = false
19
24
  @in_plain = false
20
25
  @tagfilter = extensions.include?(:tagfilter)
21
26
  end
22
27
 
28
+ # @attribute [Boolean] Whether the renderer is inside a tight container.
23
29
  attr_accessor :in_tight
30
+
31
+ # @attribute [Boolean] Whether the renderer is emitting plain text.
24
32
  attr_accessor :in_plain
25
33
 
34
+ # Writes strings, nodes, arrays of nodes, or child-node markers to the output.
35
+ #
36
+ # @parameter args [Array(Object)] Values to append or render.
26
37
  def out(*args)
27
38
  args.each do |arg|
28
39
  if arg == :children
@@ -37,6 +48,10 @@ module Markly
37
48
  end
38
49
  end
39
50
 
51
+ # Renders a node and returns the completed output for document nodes.
52
+ #
53
+ # @parameter node [Markly::Node] The node to render.
54
+ # @returns [String | Nil] The output string when rendering a document.
40
55
  def render(node)
41
56
  @node = node
42
57
  if node.type == :document
@@ -49,42 +64,57 @@ module Markly
49
64
  end
50
65
  end
51
66
 
67
+ # Renders a document node and all of its children.
68
+ #
69
+ # @parameter _node [Markly::Node] The document node.
52
70
  def document(_node)
53
71
  out(:children)
54
72
  end
55
73
 
56
- def code_block(node)
57
- code_block(node)
74
+ # Renders a code block node.
75
+ #
76
+ # Subclasses should override this callback.
77
+ # @parameter _node [Markly::Node] The code block node.
78
+ def code_block(_node)
79
+ raise NotImplementedError, "#{self.class} must implement #code_block"
58
80
  end
59
81
 
82
+ # Ignores reference-definition nodes, which have no direct output.
83
+ #
84
+ # @parameter _node [Markly::Node] The reference-definition node.
60
85
  def reference_def(_node); end
61
86
 
87
+ # Writes a newline unless the output is empty or already ends with one.
88
+ #
62
89
  def cr
63
90
  return if @stream.string.empty? || @stream.string[-1] == "\n"
64
91
 
65
92
  out("\n")
66
93
  end
67
94
 
68
- def blocksep
69
- out("\n")
70
- end
71
-
72
- def containersep
73
- cr unless @in_tight
74
- end
75
-
95
+ # Renders a block surrounded by normalized newlines.
96
+ #
97
+ # @yields {|| ...} The block content to render.
76
98
  def block
77
99
  cr
78
100
  yield
79
101
  cr
80
102
  end
81
103
 
104
+ # Renders content between opening and closing strings.
105
+ #
106
+ # @parameter starter [String] The opening output.
107
+ # @parameter ender [String] The closing output.
108
+ # @yields {|| ...} The container content to render.
82
109
  def container(starter, ender)
83
110
  out(starter)
84
111
  yield
85
112
  out(ender)
86
113
  end
87
114
 
115
+ # Renders a block in plain-text mode, suppressing structural markup.
116
+ #
117
+ # @yields {|| ...} The content to render as plain text.
88
118
  def plain
89
119
  old_in_plain = @in_plain
90
120
  @in_plain = true