markly 0.15.3 → 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/context/index.yaml +3 -3
- data/ext/markly/blocks.c +24 -6
- data/ext/markly/cmark-gfm.h +24 -0
- data/ext/markly/cmark-gfm_export.h +1 -0
- data/ext/markly/cmark-gfm_version.h +2 -2
- data/ext/markly/cmark.c +1 -1
- data/ext/markly/cmark_ctype.c +9 -0
- data/ext/markly/cmark_ctype.h +5 -0
- data/ext/markly/commonmark.c +21 -1
- data/ext/markly/config.h +0 -4
- data/ext/markly/extconf.rb +6 -1
- data/ext/markly/{table.c → extensions/table.c} +91 -46
- data/ext/markly/front_matter.c +141 -0
- data/ext/markly/front_matter.h +24 -0
- data/ext/markly/html.c +47 -23
- data/ext/markly/inlines.c +85 -3
- data/ext/markly/iterator.c +6 -1
- data/ext/markly/latex.c +3 -0
- data/ext/markly/man.c +3 -0
- data/ext/markly/markly.c +288 -201
- data/ext/markly/node.c +65 -5
- data/ext/markly/node.h +1 -0
- data/ext/markly/parser.h +19 -0
- data/ext/markly/plaintext.c +3 -0
- data/ext/markly/xml.c +19 -1
- data/lib/markly/flags.rb +10 -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 +4 -2
- data/lib/markly.rb +14 -13
- data/readme.md +22 -6
- data/releases.md +16 -0
- data.tar.gz.sig +0 -0
- metadata +23 -21
- metadata.gz.sig +0 -0
- /data/ext/markly/{autolink.c → extensions/autolink.c} +0 -0
- /data/ext/markly/{autolink.h → extensions/autolink.h} +0 -0
- /data/ext/markly/{cmark-gfm-core-extensions.h → extensions/cmark-gfm-core-extensions.h} +0 -0
- /data/ext/markly/{cmark-gfm-extensions_export.h → extensions/cmark-gfm-extensions_export.h} +0 -0
- /data/ext/markly/{core-extensions.c → extensions/core-extensions.c} +0 -0
- /data/ext/markly/{ext_scanners.c → extensions/ext_scanners.c} +0 -0
- /data/ext/markly/{ext_scanners.h → extensions/ext_scanners.h} +0 -0
- /data/ext/markly/{strikethrough.c → extensions/strikethrough.c} +0 -0
- /data/ext/markly/{strikethrough.h → extensions/strikethrough.h} +0 -0
- /data/ext/markly/{table.h → extensions/table.h} +0 -0
- /data/ext/markly/{tagfilter.c → extensions/tagfilter.c} +0 -0
- /data/ext/markly/{tagfilter.h → extensions/tagfilter.h} +0 -0
- /data/ext/markly/{tasklist.c → extensions/tasklist.c} +0 -0
- /data/ext/markly/{tasklist.h → extensions/tasklist.h} +0 -0
data/ext/markly/node.c
CHANGED
|
@@ -148,12 +148,13 @@ cmark_node *cmark_node_new(cmark_node_type type) {
|
|
|
148
148
|
static void free_node_as(cmark_node *node) {
|
|
149
149
|
switch (node->type) {
|
|
150
150
|
case CMARK_NODE_CODE_BLOCK:
|
|
151
|
+
case CMARK_NODE_FRONT_MATTER:
|
|
152
|
+
case CMARK_NODE_CODE:
|
|
151
153
|
cmark_chunk_free(NODE_MEM(node), &node->as.code.info);
|
|
152
154
|
cmark_chunk_free(NODE_MEM(node), &node->as.code.literal);
|
|
153
155
|
break;
|
|
154
156
|
case CMARK_NODE_TEXT:
|
|
155
157
|
case CMARK_NODE_HTML_INLINE:
|
|
156
|
-
case CMARK_NODE_CODE:
|
|
157
158
|
case CMARK_NODE_HTML_BLOCK:
|
|
158
159
|
case CMARK_NODE_FOOTNOTE_REFERENCE:
|
|
159
160
|
case CMARK_NODE_FOOTNOTE_DEFINITION:
|
|
@@ -288,6 +289,8 @@ const char *cmark_node_get_type_string(cmark_node *node) {
|
|
|
288
289
|
return "link";
|
|
289
290
|
case CMARK_NODE_IMAGE:
|
|
290
291
|
return "image";
|
|
292
|
+
case CMARK_NODE_FRONT_MATTER:
|
|
293
|
+
return "front_matter";
|
|
291
294
|
}
|
|
292
295
|
|
|
293
296
|
return "<unknown>";
|
|
@@ -375,12 +378,13 @@ const char *cmark_node_get_literal(cmark_node *node) {
|
|
|
375
378
|
case CMARK_NODE_HTML_BLOCK:
|
|
376
379
|
case CMARK_NODE_TEXT:
|
|
377
380
|
case CMARK_NODE_HTML_INLINE:
|
|
378
|
-
case CMARK_NODE_CODE:
|
|
379
381
|
case CMARK_NODE_FOOTNOTE_REFERENCE:
|
|
380
382
|
case CMARK_NODE_FOOTNOTE_DEFINITION:
|
|
381
383
|
return cmark_chunk_to_cstr(NODE_MEM(node), &node->as.literal);
|
|
382
384
|
|
|
385
|
+
case CMARK_NODE_CODE:
|
|
383
386
|
case CMARK_NODE_CODE_BLOCK:
|
|
387
|
+
case CMARK_NODE_FRONT_MATTER:
|
|
384
388
|
return cmark_chunk_to_cstr(NODE_MEM(node), &node->as.code.literal);
|
|
385
389
|
|
|
386
390
|
default:
|
|
@@ -399,12 +403,13 @@ int cmark_node_set_literal(cmark_node *node, const char *content) {
|
|
|
399
403
|
case CMARK_NODE_HTML_BLOCK:
|
|
400
404
|
case CMARK_NODE_TEXT:
|
|
401
405
|
case CMARK_NODE_HTML_INLINE:
|
|
402
|
-
case CMARK_NODE_CODE:
|
|
403
406
|
case CMARK_NODE_FOOTNOTE_REFERENCE:
|
|
404
407
|
cmark_chunk_set_cstr(NODE_MEM(node), &node->as.literal, content);
|
|
405
408
|
return 1;
|
|
406
409
|
|
|
410
|
+
case CMARK_NODE_CODE:
|
|
407
411
|
case CMARK_NODE_CODE_BLOCK:
|
|
412
|
+
case CMARK_NODE_FRONT_MATTER:
|
|
408
413
|
cmark_chunk_set_cstr(NODE_MEM(node), &node->as.code.literal, content);
|
|
409
414
|
return 1;
|
|
410
415
|
|
|
@@ -590,12 +595,66 @@ int cmark_node_set_item_index(cmark_node *node, int idx) {
|
|
|
590
595
|
}
|
|
591
596
|
}
|
|
592
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
|
+
|
|
593
651
|
const char *cmark_node_get_fence_info(cmark_node *node) {
|
|
594
652
|
if (node == NULL) {
|
|
595
653
|
return NULL;
|
|
596
654
|
}
|
|
597
655
|
|
|
598
|
-
if (node->type == CMARK_NODE_CODE_BLOCK
|
|
656
|
+
if (node->type == CMARK_NODE_CODE_BLOCK ||
|
|
657
|
+
node->type == CMARK_NODE_FRONT_MATTER) {
|
|
599
658
|
return cmark_chunk_to_cstr(NODE_MEM(node), &node->as.code.info);
|
|
600
659
|
} else {
|
|
601
660
|
return NULL;
|
|
@@ -607,7 +666,8 @@ int cmark_node_set_fence_info(cmark_node *node, const char *info) {
|
|
|
607
666
|
return 0;
|
|
608
667
|
}
|
|
609
668
|
|
|
610
|
-
if (node->type == CMARK_NODE_CODE_BLOCK
|
|
669
|
+
if (node->type == CMARK_NODE_CODE_BLOCK ||
|
|
670
|
+
node->type == CMARK_NODE_FRONT_MATTER) {
|
|
611
671
|
cmark_chunk_set_cstr(NODE_MEM(node), &node->as.code.info, info);
|
|
612
672
|
return 1;
|
|
613
673
|
} else {
|
data/ext/markly/node.h
CHANGED
data/ext/markly/parser.h
CHANGED
|
@@ -50,6 +50,25 @@ struct cmark_parser {
|
|
|
50
50
|
cmark_llist *syntax_extensions;
|
|
51
51
|
cmark_llist *inline_syntax_extensions;
|
|
52
52
|
cmark_ispunct_func backslash_ispunct;
|
|
53
|
+
|
|
54
|
+
/* Front matter scanning state (CMARK_OPT_FRONT_MATTER).
|
|
55
|
+
*
|
|
56
|
+
* cmark_front_matter_process_line() is called from S_process_line() in
|
|
57
|
+
* blocks.c immediately after parser->line_number is incremented, so the
|
|
58
|
+
* first line of the document arrives with line_number == 1. The function
|
|
59
|
+
* relies on this: it uses line_number == 1 as the trigger to decide
|
|
60
|
+
* whether the document opens with a front matter block.
|
|
61
|
+
*
|
|
62
|
+
* front_matter_scanning is set to true when a valid opening "---" is seen
|
|
63
|
+
* on line 1 and remains true until the matching closing "---" is found or
|
|
64
|
+
* the document ends. While scanning, each content line is accumulated in
|
|
65
|
+
* front_matter_buf. Both fields are reset to zero/empty by
|
|
66
|
+
* cmark_parser_reset() (via memset + strbuf re-init) and the strbuf is
|
|
67
|
+
* freed explicitly in cmark_parser_finish() and cmark_parser_free().
|
|
68
|
+
*/
|
|
69
|
+
bool front_matter_scanning;
|
|
70
|
+
cmark_strbuf front_matter_buf; /* accumulated content lines */
|
|
71
|
+
cmark_strbuf front_matter_info; /* optional format hint from opening "--- <info>" */
|
|
53
72
|
};
|
|
54
73
|
|
|
55
74
|
#ifdef __cplusplus
|
data/ext/markly/plaintext.c
CHANGED
|
@@ -196,6 +196,9 @@ static int S_render_node(cmark_renderer *renderer, cmark_node *node,
|
|
|
196
196
|
cmark_strbuf_truncate(renderer->prefix, renderer->prefix->size - 4);
|
|
197
197
|
}
|
|
198
198
|
break;
|
|
199
|
+
case CMARK_NODE_FRONT_MATTER:
|
|
200
|
+
break;
|
|
201
|
+
|
|
199
202
|
default:
|
|
200
203
|
assert(false);
|
|
201
204
|
break;
|
data/ext/markly/xml.c
CHANGED
|
@@ -64,8 +64,14 @@ static int S_render_node(cmark_node *node, cmark_event_type ev_type,
|
|
|
64
64
|
case CMARK_NODE_DOCUMENT:
|
|
65
65
|
cmark_strbuf_puts(xml, " xmlns=\"http://commonmark.org/xml/1.0\"");
|
|
66
66
|
break;
|
|
67
|
+
case CMARK_NODE_FRONT_MATTER:
|
|
68
|
+
cmark_strbuf_puts(xml, " xml:space=\"preserve\">");
|
|
69
|
+
escape_xml(xml, node->as.code.literal.data, node->as.code.literal.len);
|
|
70
|
+
cmark_strbuf_puts(xml, "</");
|
|
71
|
+
cmark_strbuf_puts(xml, cmark_node_get_type_string(node));
|
|
72
|
+
literal = true;
|
|
73
|
+
break;
|
|
67
74
|
case CMARK_NODE_TEXT:
|
|
68
|
-
case CMARK_NODE_CODE:
|
|
69
75
|
case CMARK_NODE_HTML_BLOCK:
|
|
70
76
|
case CMARK_NODE_HTML_INLINE:
|
|
71
77
|
cmark_strbuf_puts(xml, " xml:space=\"preserve\">");
|
|
@@ -74,6 +80,18 @@ static int S_render_node(cmark_node *node, cmark_event_type ev_type,
|
|
|
74
80
|
cmark_strbuf_puts(xml, cmark_node_get_type_string(node));
|
|
75
81
|
literal = true;
|
|
76
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;
|
|
77
95
|
case CMARK_NODE_LIST:
|
|
78
96
|
switch (cmark_node_get_list_type(node)) {
|
|
79
97
|
case CMARK_ORDERED_LIST:
|
data/lib/markly/flags.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2020-
|
|
4
|
+
# Copyright, 2020-2026, by Samuel Williams.
|
|
5
5
|
|
|
6
6
|
module Markly
|
|
7
7
|
# The default parsing system.
|
|
@@ -18,8 +18,17 @@ module Markly
|
|
|
18
18
|
STRIKETHROUGH_DOUBLE_TILDE = 1 << 14
|
|
19
19
|
# Allow raw/custom HTML and unsafe links.
|
|
20
20
|
UNSAFE = 1 << 17
|
|
21
|
+
# Parse front matter ("---" delimited block at start of document).
|
|
22
|
+
# The raw content is available via node.string_content and the optional
|
|
23
|
+
# format hint (e.g. "yaml", "toml") via node.code_info; interpretation
|
|
24
|
+
# is left to the caller.
|
|
25
|
+
FRONT_MATTER = 1 << 18
|
|
26
|
+
# Parse language prefixes on inline code spans, e.g. ruby:`Object.new`.
|
|
27
|
+
INLINE_CODE_INFO = 1 << 19
|
|
21
28
|
|
|
22
29
|
PARSE_FLAGS = {
|
|
30
|
+
front_matter: FRONT_MATTER,
|
|
31
|
+
inline_code_info: INLINE_CODE_INFO,
|
|
23
32
|
validate_utf8: VALIDATE_UTF8,
|
|
24
33
|
smart_quotes: SMART,
|
|
25
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
|
-
|