markly 0.16.0 → 0.17.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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/context/getting-started.md +36 -0
- data/ext/markly/cmark-gfm.h +16 -0
- data/ext/markly/cmark-gfm_export.h +1 -0
- data/ext/markly/cmark-gfm_version.h +2 -2
- data/ext/markly/cmark_ctype.c +9 -0
- data/ext/markly/cmark_ctype.h +5 -0
- data/ext/markly/commonmark.c +10 -3
- data/ext/markly/config.h +0 -4
- data/ext/markly/html.c +44 -23
- data/ext/markly/inlines.c +85 -3
- data/ext/markly/iterator.c +6 -1
- data/ext/markly/markly.c +282 -201
- data/ext/markly/node.c +56 -3
- data/ext/markly/xml.c +12 -1
- data/lib/markly/flags.rb +4 -1
- data/lib/markly/node/inspect.rb +10 -3
- data/lib/markly/node.rb +53 -28
- data/lib/markly/renderer/generic.rb +42 -12
- data/lib/markly/renderer/headings.rb +24 -19
- data/lib/markly/renderer/html.rb +111 -10
- data/lib/markly/version.rb +3 -1
- data/lib/markly.rb +14 -13
- data/readme.md +11 -1
- data/releases.md +11 -1
- data.tar.gz.sig +0 -0
- metadata +2 -2
- metadata.gz.sig +0 -0
data/ext/markly/node.c
CHANGED
|
@@ -149,12 +149,12 @@ static void free_node_as(cmark_node *node) {
|
|
|
149
149
|
switch (node->type) {
|
|
150
150
|
case CMARK_NODE_CODE_BLOCK:
|
|
151
151
|
case CMARK_NODE_FRONT_MATTER:
|
|
152
|
+
case CMARK_NODE_CODE:
|
|
152
153
|
cmark_chunk_free(NODE_MEM(node), &node->as.code.info);
|
|
153
154
|
cmark_chunk_free(NODE_MEM(node), &node->as.code.literal);
|
|
154
155
|
break;
|
|
155
156
|
case CMARK_NODE_TEXT:
|
|
156
157
|
case CMARK_NODE_HTML_INLINE:
|
|
157
|
-
case CMARK_NODE_CODE:
|
|
158
158
|
case CMARK_NODE_HTML_BLOCK:
|
|
159
159
|
case CMARK_NODE_FOOTNOTE_REFERENCE:
|
|
160
160
|
case CMARK_NODE_FOOTNOTE_DEFINITION:
|
|
@@ -378,11 +378,11 @@ const char *cmark_node_get_literal(cmark_node *node) {
|
|
|
378
378
|
case CMARK_NODE_HTML_BLOCK:
|
|
379
379
|
case CMARK_NODE_TEXT:
|
|
380
380
|
case CMARK_NODE_HTML_INLINE:
|
|
381
|
-
case CMARK_NODE_CODE:
|
|
382
381
|
case CMARK_NODE_FOOTNOTE_REFERENCE:
|
|
383
382
|
case CMARK_NODE_FOOTNOTE_DEFINITION:
|
|
384
383
|
return cmark_chunk_to_cstr(NODE_MEM(node), &node->as.literal);
|
|
385
384
|
|
|
385
|
+
case CMARK_NODE_CODE:
|
|
386
386
|
case CMARK_NODE_CODE_BLOCK:
|
|
387
387
|
case CMARK_NODE_FRONT_MATTER:
|
|
388
388
|
return cmark_chunk_to_cstr(NODE_MEM(node), &node->as.code.literal);
|
|
@@ -403,11 +403,11 @@ int cmark_node_set_literal(cmark_node *node, const char *content) {
|
|
|
403
403
|
case CMARK_NODE_HTML_BLOCK:
|
|
404
404
|
case CMARK_NODE_TEXT:
|
|
405
405
|
case CMARK_NODE_HTML_INLINE:
|
|
406
|
-
case CMARK_NODE_CODE:
|
|
407
406
|
case CMARK_NODE_FOOTNOTE_REFERENCE:
|
|
408
407
|
cmark_chunk_set_cstr(NODE_MEM(node), &node->as.literal, content);
|
|
409
408
|
return 1;
|
|
410
409
|
|
|
410
|
+
case CMARK_NODE_CODE:
|
|
411
411
|
case CMARK_NODE_CODE_BLOCK:
|
|
412
412
|
case CMARK_NODE_FRONT_MATTER:
|
|
413
413
|
cmark_chunk_set_cstr(NODE_MEM(node), &node->as.code.literal, content);
|
|
@@ -595,6 +595,59 @@ int cmark_node_set_item_index(cmark_node *node, int idx) {
|
|
|
595
595
|
}
|
|
596
596
|
}
|
|
597
597
|
|
|
598
|
+
const char *cmark_node_get_code_info(cmark_node *node) {
|
|
599
|
+
if (node == NULL) {
|
|
600
|
+
return NULL;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (node->type == CMARK_NODE_CODE ||
|
|
604
|
+
node->type == CMARK_NODE_CODE_BLOCK ||
|
|
605
|
+
node->type == CMARK_NODE_FRONT_MATTER) {
|
|
606
|
+
return cmark_chunk_to_cstr(NODE_MEM(node), &node->as.code.info);
|
|
607
|
+
} else {
|
|
608
|
+
return NULL;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
static int valid_inline_code_info(const char *info) {
|
|
613
|
+
if (info == NULL || *info == '\0') {
|
|
614
|
+
return 1;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
if (!cmark_is_inline_code_info_start_char(*info)) {
|
|
618
|
+
return 0;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
while (*++info) {
|
|
622
|
+
if (!cmark_is_inline_code_info_char(*info)) {
|
|
623
|
+
return 0;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
return 1;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
int cmark_node_set_code_info(cmark_node *node, const char *info) {
|
|
631
|
+
if (node == NULL) {
|
|
632
|
+
return 0;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (node->type == CMARK_NODE_CODE) {
|
|
636
|
+
if (!valid_inline_code_info(info)) {
|
|
637
|
+
return 0;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
cmark_chunk_set_cstr(NODE_MEM(node), &node->as.code.info, info);
|
|
641
|
+
return 1;
|
|
642
|
+
} else if (node->type == CMARK_NODE_CODE_BLOCK ||
|
|
643
|
+
node->type == CMARK_NODE_FRONT_MATTER) {
|
|
644
|
+
cmark_chunk_set_cstr(NODE_MEM(node), &node->as.code.info, info);
|
|
645
|
+
return 1;
|
|
646
|
+
} else {
|
|
647
|
+
return 0;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
598
651
|
const char *cmark_node_get_fence_info(cmark_node *node) {
|
|
599
652
|
if (node == NULL) {
|
|
600
653
|
return NULL;
|
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.
|
|
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,
|
data/lib/markly/node/inspect.rb
CHANGED
|
@@ -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-
|
|
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
|
-
#
|
|
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
|
-
|
|
41
|
+
code_info
|
|
35
42
|
].map do |name|
|
|
36
43
|
begin
|
|
37
44
|
[name, __send__(name)]
|
data/lib/markly/node.rb
CHANGED
|
@@ -5,17 +5,20 @@
|
|
|
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-
|
|
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
23
|
# This is a bit crazy, but it's the best I can come up with right now:
|
|
21
24
|
node = Markly.parse(self.to_markdown)
|
|
@@ -28,9 +31,11 @@ module Markly
|
|
|
28
31
|
end
|
|
29
32
|
end
|
|
30
33
|
|
|
31
|
-
#
|
|
34
|
+
# Walk the node tree recursively.
|
|
32
35
|
#
|
|
33
|
-
#
|
|
36
|
+
# @yields {|node| ...} Each node in depth-first order, including this node.
|
|
37
|
+
# @parameter node [Markly::Node] The current node.
|
|
38
|
+
# @returns [Enumerator | Nil] An enumerator when no block is given.
|
|
34
39
|
def walk(&block)
|
|
35
40
|
return enum_for(:walk) unless block_given?
|
|
36
41
|
|
|
@@ -40,39 +45,47 @@ module Markly
|
|
|
40
45
|
end
|
|
41
46
|
end
|
|
42
47
|
|
|
43
|
-
#
|
|
44
|
-
#
|
|
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
|
|
48
|
+
# Convert the node to an HTML string.
|
|
47
49
|
#
|
|
48
|
-
#
|
|
50
|
+
# @parameter flags [Integer] The enabled rendering flags.
|
|
51
|
+
# @parameter extensions [Array(Symbol)] The extensions to enable.
|
|
52
|
+
# @returns [String] The rendered HTML.
|
|
49
53
|
def to_html(flags: DEFAULT, extensions: [])
|
|
50
54
|
_render_html(flags, extensions).force_encoding("utf-8")
|
|
51
55
|
end
|
|
52
56
|
|
|
53
|
-
#
|
|
54
|
-
#
|
|
55
|
-
# flags - A {Symbol} or {Array of Symbol}s indicating the render options
|
|
56
|
-
# width - Column to wrap the output at
|
|
57
|
+
# Convert the node to a CommonMark string.
|
|
57
58
|
#
|
|
58
|
-
#
|
|
59
|
+
# @parameter flags [Integer] The enabled rendering flags.
|
|
60
|
+
# @parameter width [Integer] The column at which to wrap output, or `0` to disable wrapping.
|
|
61
|
+
# @returns [String] The rendered CommonMark text.
|
|
59
62
|
def to_commonmark(flags: DEFAULT, width: 0)
|
|
60
63
|
_render_commonmark(flags, width).force_encoding("utf-8")
|
|
61
64
|
end
|
|
62
65
|
|
|
63
66
|
alias to_markdown to_commonmark
|
|
64
67
|
|
|
65
|
-
#
|
|
68
|
+
# Return the language identifier from the code info string.
|
|
66
69
|
#
|
|
67
|
-
#
|
|
68
|
-
|
|
70
|
+
# @returns [String | Nil] The language identifier, or `nil` when none is present.
|
|
71
|
+
def code_language
|
|
72
|
+
code_info.split(/\s+/, 2).first
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Convert the node to a plain-text string.
|
|
69
76
|
#
|
|
70
|
-
#
|
|
77
|
+
# @parameter flags [Integer] The enabled rendering flags.
|
|
78
|
+
# @parameter width [Integer] The column at which to wrap output, or `0` to disable wrapping.
|
|
79
|
+
# @returns [String] The rendered plain text.
|
|
71
80
|
def to_plaintext(flags: DEFAULT, width: 0)
|
|
72
81
|
_render_plaintext(flags, width).force_encoding("utf-8")
|
|
73
82
|
end
|
|
74
83
|
|
|
75
|
-
#
|
|
84
|
+
# Iterate over the direct children of this node.
|
|
85
|
+
#
|
|
86
|
+
# @yields {|child| ...} Each direct child of this node.
|
|
87
|
+
# @parameter child [Markly::Node] The current child node.
|
|
88
|
+
# @returns [Enumerator | Nil] An enumerator when no block is given.
|
|
76
89
|
def each
|
|
77
90
|
return enum_for(:each) unless block_given?
|
|
78
91
|
|
|
@@ -84,6 +97,10 @@ module Markly
|
|
|
84
97
|
end
|
|
85
98
|
end
|
|
86
99
|
|
|
100
|
+
# Finds a direct child header with the given text.
|
|
101
|
+
#
|
|
102
|
+
# @parameter title [String] The header text to match.
|
|
103
|
+
# @returns [Markly::Node | Nil] The matching header, if present.
|
|
87
104
|
def find_header(title)
|
|
88
105
|
each do |child|
|
|
89
106
|
if child.type == :header && child.first_child.string_content == title
|
|
@@ -94,7 +111,9 @@ module Markly
|
|
|
94
111
|
|
|
95
112
|
# Delete all nodes until the block returns true.
|
|
96
113
|
#
|
|
97
|
-
# @
|
|
114
|
+
# @yields {|node| ...} Each node before it is deleted.
|
|
115
|
+
# @parameter node [Markly::Node] The current node.
|
|
116
|
+
# @returns [Markly::Node | Nil] The node for which the block returned `true`, if any.
|
|
98
117
|
def delete_until
|
|
99
118
|
current = self
|
|
100
119
|
while current
|
|
@@ -107,9 +126,9 @@ module Markly
|
|
|
107
126
|
|
|
108
127
|
# Replace a section (header + content) with a new node.
|
|
109
128
|
#
|
|
110
|
-
# @parameter new_node [Markly::Node]
|
|
111
|
-
# @parameter replace_header [Boolean]
|
|
112
|
-
# @parameter remove_subsections [Boolean]
|
|
129
|
+
# @parameter new_node [Markly::Node | Nil] The node with which to replace the section.
|
|
130
|
+
# @parameter replace_header [Boolean] Whether to replace the header itself.
|
|
131
|
+
# @parameter remove_subsections [Boolean] Whether to remove subsections.
|
|
113
132
|
def replace_section(new_node, replace_header: true, remove_subsections: true)
|
|
114
133
|
# Delete until the next heading:
|
|
115
134
|
self.next&.delete_until do |node|
|
|
@@ -120,21 +139,27 @@ module Markly
|
|
|
120
139
|
self.delete if replace_header
|
|
121
140
|
end
|
|
122
141
|
|
|
123
|
-
|
|
142
|
+
# Finds the next sibling header.
|
|
143
|
+
#
|
|
144
|
+
# @returns [Markly::Node | Nil] The next header, if present.
|
|
145
|
+
def next_header
|
|
124
146
|
current = self.next
|
|
125
147
|
while current
|
|
126
|
-
if current.type == :
|
|
148
|
+
if current.type == :header
|
|
127
149
|
return current
|
|
128
150
|
end
|
|
129
151
|
current = current.next
|
|
130
152
|
end
|
|
131
153
|
end
|
|
132
154
|
|
|
155
|
+
# An alias for {ruby Markly::Node#next_header}.
|
|
156
|
+
alias next_heading next_header
|
|
157
|
+
|
|
133
158
|
# Append the given node after the current node.
|
|
134
159
|
#
|
|
135
160
|
# It's okay to provide a document node, its children will be appended.
|
|
136
161
|
#
|
|
137
|
-
# @parameter node [Markly::Node]
|
|
162
|
+
# @parameter node [Markly::Node] The node to append.
|
|
138
163
|
def append_after(node)
|
|
139
164
|
if node.type == :document
|
|
140
165
|
node = node.first_child
|
|
@@ -153,7 +178,7 @@ module Markly
|
|
|
153
178
|
#
|
|
154
179
|
# It's okay to provide a document node, its children will be appended.
|
|
155
180
|
#
|
|
156
|
-
# @parameter node [Markly::Node]
|
|
181
|
+
# @parameter node [Markly::Node] The node to append.
|
|
157
182
|
def append_before(node)
|
|
158
183
|
if node.type == :document
|
|
159
184
|
node = node.first_child
|
|
@@ -169,7 +194,7 @@ module Markly
|
|
|
169
194
|
|
|
170
195
|
# Extract the children as a fragment.
|
|
171
196
|
#
|
|
172
|
-
# @returns [Markly::Node]
|
|
197
|
+
# @returns [Markly::Node] The fragment.
|
|
173
198
|
def extract_children
|
|
174
199
|
fragment = Markly::Node.new(:custom_inline)
|
|
175
200
|
|
|
@@ -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-
|
|
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
|
-
|
|
57
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2025, by Samuel Williams.
|
|
4
|
+
# Copyright, 2025-2026, by Samuel Williams.
|
|
5
5
|
|
|
6
6
|
module Markly
|
|
7
7
|
module Renderer
|
|
8
8
|
# Extracts headings from a markdown document with unique anchor IDs.
|
|
9
9
|
# Handles duplicate heading text by appending counters (e.g., "deployment", "deployment-2", "deployment-3").
|
|
10
10
|
class Headings
|
|
11
|
+
# Initializes an empty heading-anchor registry.
|
|
11
12
|
def initialize
|
|
12
13
|
@ids = {}
|
|
13
14
|
end
|
|
14
15
|
|
|
15
16
|
# Generate a unique anchor for a node.
|
|
16
|
-
#
|
|
17
|
-
# @
|
|
17
|
+
#
|
|
18
|
+
# @parameter node [Markly::Node] The heading node.
|
|
19
|
+
# @returns [String] A unique anchor ID.
|
|
18
20
|
def anchor_for(node)
|
|
19
21
|
base = base_anchor_for(node)
|
|
20
22
|
|
|
@@ -28,10 +30,11 @@ module Markly
|
|
|
28
30
|
end
|
|
29
31
|
|
|
30
32
|
# Extract all headings from a document root with unique anchors.
|
|
31
|
-
#
|
|
32
|
-
# @parameter
|
|
33
|
-
# @parameter
|
|
34
|
-
# @
|
|
33
|
+
#
|
|
34
|
+
# @parameter root [Markly::Node] The document root node.
|
|
35
|
+
# @parameter min_level [Integer] The minimum heading level to extract.
|
|
36
|
+
# @parameter max_level [Integer] The maximum heading level to extract.
|
|
37
|
+
# @returns [Array(Heading)] The extracted headings with unique anchors.
|
|
35
38
|
def extract(root, min_level: 1, max_level: 6)
|
|
36
39
|
headings = []
|
|
37
40
|
root.walk do |node|
|
|
@@ -50,11 +53,12 @@ module Markly
|
|
|
50
53
|
headings
|
|
51
54
|
end
|
|
52
55
|
|
|
53
|
-
#
|
|
54
|
-
#
|
|
55
|
-
# @parameter
|
|
56
|
-
# @parameter
|
|
57
|
-
# @
|
|
56
|
+
# Extract all headings using a new heading-anchor registry.
|
|
57
|
+
#
|
|
58
|
+
# @parameter root [Markly::Node] The document root node.
|
|
59
|
+
# @parameter min_level [Integer] The minimum heading level to extract.
|
|
60
|
+
# @parameter max_level [Integer] The maximum heading level to extract.
|
|
61
|
+
# @returns [Array(Heading)] The extracted headings with unique anchors.
|
|
58
62
|
def self.extract(root, min_level: 1, max_level: 6)
|
|
59
63
|
new.extract(root, min_level: min_level, max_level: max_level)
|
|
60
64
|
end
|
|
@@ -62,8 +66,9 @@ module Markly
|
|
|
62
66
|
private
|
|
63
67
|
|
|
64
68
|
# Generate a base anchor from a node's text content.
|
|
65
|
-
#
|
|
66
|
-
# @
|
|
69
|
+
#
|
|
70
|
+
# @parameter node [Markly::Node] The heading node.
|
|
71
|
+
# @returns [String] The lowercase, hyphenated base anchor.
|
|
67
72
|
def base_anchor_for(node)
|
|
68
73
|
text = node.to_plaintext.chomp.downcase
|
|
69
74
|
text.gsub(/\s+/, "-")
|
|
@@ -71,11 +76,11 @@ module Markly
|
|
|
71
76
|
end
|
|
72
77
|
|
|
73
78
|
# Represents a heading extracted from a document.
|
|
74
|
-
#
|
|
75
|
-
#
|
|
76
|
-
#
|
|
77
|
-
#
|
|
79
|
+
#
|
|
80
|
+
# The structure contains the original `node`, its `level`, its plain-text
|
|
81
|
+
# `text`, and its unique `anchor`.
|
|
82
|
+
#
|
|
83
|
+
# @constant [Class] The heading data class.
|
|
78
84
|
Heading = Struct.new(:node, :level, :text, :anchor, keyword_init: true)
|
|
79
85
|
end
|
|
80
86
|
end
|
|
81
|
-
|