markly 0.17.0 → 0.18.1
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/extensions.md +232 -0
- data/context/getting-started.md +9 -45
- data/context/index.yaml +3 -0
- data/ext/markly/cmark-gfm-extension_api.h +10 -0
- data/ext/markly/cmark-gfm.h +6 -0
- data/ext/markly/extensions/table.c +24 -0
- data/ext/markly/html.c +3 -2
- data/ext/markly/markly.c +16 -0
- data/ext/markly/node.c +138 -0
- data/ext/markly/syntax_extension.c +5 -0
- data/ext/markly/syntax_extension.h +1 -0
- data/lib/markly/node.rb +1 -9
- data/lib/markly/version.rb +1 -1
- data/readme.md +6 -0
- data/releases.md +4 -0
- data.tar.gz.sig +0 -0
- metadata +2 -1
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2ef6552d303ea4620a87168fb130a802de075b747bcc737986b86083e46825bc
|
|
4
|
+
data.tar.gz: 1f9ac70b49a3170929bf56cfae562214b86fc59d58d3a07d42c14f74fbcd47c0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7f3f69c8c4844f3c28621c6e301d4785c342d9660d305fc8f0452764e72784257f0a76ec608e5c0375171cf18455a46f0a6729e898eb805645ba8bea7c8f1ac3
|
|
7
|
+
data.tar.gz: 6c4c3505f5e0b47c927dd405d20e44b4c10043e6e2657b6ec12cbd7f02f8efaa56723f885bfa3daa33052e32e6d1e24a238d1fb7dec49e227ac9802ca54ec76f
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# Extensions
|
|
2
|
+
|
|
3
|
+
This guide explains how to enable and use Markly's Markdown extensions.
|
|
4
|
+
|
|
5
|
+
## Choosing a Markdown Dialect
|
|
6
|
+
|
|
7
|
+
Markly parses standard CommonMark by default. Extensions change which syntax is
|
|
8
|
+
recognized, so applications should enable them explicitly and consistently.
|
|
9
|
+
This is especially important when several components parse or render the same
|
|
10
|
+
document.
|
|
11
|
+
|
|
12
|
+
Pass extension names as symbols using the `extensions:` keyword:
|
|
13
|
+
|
|
14
|
+
``` ruby
|
|
15
|
+
EXTENSIONS = %i[table tasklist strikethrough autolink].freeze
|
|
16
|
+
|
|
17
|
+
html = Markly.render_html(markdown, extensions: EXTENSIONS)
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
When parsing and rendering separately, use the same configuration at both
|
|
21
|
+
boundaries:
|
|
22
|
+
|
|
23
|
+
``` ruby
|
|
24
|
+
document = Markly.parse(markdown, extensions: EXTENSIONS)
|
|
25
|
+
html = document.to_html(extensions: EXTENSIONS)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Extension names must be symbols. A string such as `"table"` raises `TypeError`,
|
|
29
|
+
and an unknown symbol raises `ArgumentError`.
|
|
30
|
+
|
|
31
|
+
## GitHub Flavored Markdown Extensions
|
|
32
|
+
|
|
33
|
+
Markly includes the five syntax extensions defined by GitHub Flavored Markdown.
|
|
34
|
+
None are enabled by default.
|
|
35
|
+
|
|
36
|
+
### Tables
|
|
37
|
+
|
|
38
|
+
The `:table` extension recognizes a paragraph followed by a delimiter row as a
|
|
39
|
+
table. Colons in the delimiter row specify column alignment:
|
|
40
|
+
|
|
41
|
+
``` ruby
|
|
42
|
+
markdown = <<~MARKDOWN
|
|
43
|
+
| Package | Status |
|
|
44
|
+
| :--- | ---: |
|
|
45
|
+
| Markly | Ready |
|
|
46
|
+
MARKDOWN
|
|
47
|
+
|
|
48
|
+
document = Markly.parse(markdown, extensions: [:table])
|
|
49
|
+
table = document.first_child
|
|
50
|
+
|
|
51
|
+
table.type
|
|
52
|
+
# => :table
|
|
53
|
+
|
|
54
|
+
table.table_alignments
|
|
55
|
+
# => [:left, :right]
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The table AST contains `:table`, `:table_header`, `:table_row`, and
|
|
59
|
+
`:table_cell` nodes. By default, HTML rendering uses `align` attributes for
|
|
60
|
+
aligned cells. Pass `Markly::TABLE_PREFER_STYLE_ATTRIBUTES` when rendering to
|
|
61
|
+
use `style="text-align: ..."` instead:
|
|
62
|
+
|
|
63
|
+
``` ruby
|
|
64
|
+
document.to_html(
|
|
65
|
+
flags: Markly::TABLE_PREFER_STYLE_ATTRIBUTES,
|
|
66
|
+
extensions: [:table],
|
|
67
|
+
)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Task Lists
|
|
71
|
+
|
|
72
|
+
The `:tasklist` extension recognizes checked and unchecked list items:
|
|
73
|
+
|
|
74
|
+
``` markdown
|
|
75
|
+
- [x] Parse Markdown
|
|
76
|
+
- [ ] Render HTML
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Task-list state is available on the list-item node and can be changed before
|
|
80
|
+
rendering:
|
|
81
|
+
|
|
82
|
+
``` ruby
|
|
83
|
+
document = Markly.parse("- [x] Parse Markdown", extensions: [:tasklist])
|
|
84
|
+
item = document.first_child.first_child
|
|
85
|
+
|
|
86
|
+
item.tasklist_item_checked?
|
|
87
|
+
# => true
|
|
88
|
+
|
|
89
|
+
item.tasklist_item_checked = false
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Strikethrough
|
|
93
|
+
|
|
94
|
+
The `:strikethrough` extension renders strikethrough text using `<del>`:
|
|
95
|
+
|
|
96
|
+
``` ruby
|
|
97
|
+
Markly.render_html("~~obsolete~~", extensions: [:strikethrough])
|
|
98
|
+
# => "<p><del>obsolete</del></p>\n"
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Pass `Markly::STRIKETHROUGH_DOUBLE_TILDE` while parsing to accept only spans
|
|
102
|
+
surrounded by exactly two tildes. This is useful when compatibility requires
|
|
103
|
+
single or longer runs of tildes to remain literal:
|
|
104
|
+
|
|
105
|
+
``` ruby
|
|
106
|
+
Markly.render_html(
|
|
107
|
+
"~one~ ~~two~~ ~~~three~~~",
|
|
108
|
+
flags: Markly::STRIKETHROUGH_DOUBLE_TILDE,
|
|
109
|
+
extensions: [:strikethrough],
|
|
110
|
+
)
|
|
111
|
+
# => "<p>~one~ <del>two</del> ~~~three~~~</p>\n"
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Autolinks
|
|
115
|
+
|
|
116
|
+
The `:autolink` extension turns plain URLs and email addresses into links
|
|
117
|
+
without requiring angle brackets or Markdown link syntax:
|
|
118
|
+
|
|
119
|
+
``` ruby
|
|
120
|
+
Markly.render_html(
|
|
121
|
+
"Visit https://socketry.io or email hello@example.com.",
|
|
122
|
+
extensions: [:autolink],
|
|
123
|
+
)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Use this extension when rendering prose where authors expect GitHub-style link
|
|
127
|
+
detection. Leave it disabled when plain URL-like text must remain unchanged.
|
|
128
|
+
|
|
129
|
+
### Tag Filtering
|
|
130
|
+
|
|
131
|
+
The `:tagfilter` extension escapes the raw HTML tags prohibited by the GitHub
|
|
132
|
+
Flavored Markdown specification. It is typically combined with
|
|
133
|
+
`Markly::UNSAFE`, which otherwise permits raw HTML:
|
|
134
|
+
|
|
135
|
+
``` ruby
|
|
136
|
+
Markly.render_html(
|
|
137
|
+
"<script>alert('no')</script><strong>yes</strong>",
|
|
138
|
+
flags: Markly::UNSAFE,
|
|
139
|
+
extensions: [:tagfilter],
|
|
140
|
+
)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Tag filtering is not a general-purpose HTML sanitizer. It filters the specific
|
|
144
|
+
tag names required by the GFM specification, while other raw HTML remains
|
|
145
|
+
available when `Markly::UNSAFE` is enabled. Sanitize untrusted HTML separately
|
|
146
|
+
when the application requires a stricter policy.
|
|
147
|
+
|
|
148
|
+
## Markly Syntax Features
|
|
149
|
+
|
|
150
|
+
Markly also provides syntax features that are not named GFM extensions. Enable
|
|
151
|
+
these with parser flags rather than adding names to `extensions:`.
|
|
152
|
+
|
|
153
|
+
### Front Matter
|
|
154
|
+
|
|
155
|
+
`Markly::FRONT_MATTER` recognizes a `---` delimited block only at the beginning
|
|
156
|
+
of a document:
|
|
157
|
+
|
|
158
|
+
``` ruby
|
|
159
|
+
markdown = <<~MARKDOWN
|
|
160
|
+
--- yaml
|
|
161
|
+
title: Extensions
|
|
162
|
+
---
|
|
163
|
+
# Document
|
|
164
|
+
MARKDOWN
|
|
165
|
+
|
|
166
|
+
document = Markly.parse(markdown, flags: Markly::FRONT_MATTER)
|
|
167
|
+
front_matter = document.first_child
|
|
168
|
+
|
|
169
|
+
front_matter.type
|
|
170
|
+
# => :front_matter
|
|
171
|
+
|
|
172
|
+
front_matter.string_content
|
|
173
|
+
# => "title: Extensions\n"
|
|
174
|
+
|
|
175
|
+
front_matter.code_info
|
|
176
|
+
# => "yaml"
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Front matter is omitted from HTML and plain-text output. Markly exposes its raw
|
|
180
|
+
contents but does not interpret YAML, TOML, or any other format. Treat the
|
|
181
|
+
contents as untrusted input and parse them according to the application's own
|
|
182
|
+
policy.
|
|
183
|
+
|
|
184
|
+
The closing delimiter must be an exact `---` line. If it is missing, the rest
|
|
185
|
+
of the document belongs to the front-matter node.
|
|
186
|
+
|
|
187
|
+
### Inline Code Information
|
|
188
|
+
|
|
189
|
+
`Markly::INLINE_CODE_INFO` recognizes a language prefix immediately before an
|
|
190
|
+
inline code span:
|
|
191
|
+
|
|
192
|
+
``` ruby
|
|
193
|
+
document = Markly.parse(
|
|
194
|
+
"ruby:`Object.new`",
|
|
195
|
+
flags: Markly::INLINE_CODE_INFO,
|
|
196
|
+
)
|
|
197
|
+
code = document.first_child.first_child
|
|
198
|
+
|
|
199
|
+
code.code_info
|
|
200
|
+
# => "ruby"
|
|
201
|
+
|
|
202
|
+
code.code_language
|
|
203
|
+
# => "ruby"
|
|
204
|
+
|
|
205
|
+
document.to_html
|
|
206
|
+
# => "<p><code class=\"language-ruby\">Object.new</code></p>\n"
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Without the flag, the prefix remains ordinary text. Inline code information is
|
|
210
|
+
a single language token; richer code-block information belongs on a fenced code
|
|
211
|
+
block instead.
|
|
212
|
+
|
|
213
|
+
### Code Block Metadata
|
|
214
|
+
|
|
215
|
+
`Node#code_info` is the general information-string accessor for fenced code
|
|
216
|
+
blocks and front matter. `Node#code_language` returns the first token of that
|
|
217
|
+
information string.
|
|
218
|
+
|
|
219
|
+
For a fenced code block, `Node#fence` returns a {ruby Markly::Node::Fence}
|
|
220
|
+
containing the fence character, length, and indentation:
|
|
221
|
+
|
|
222
|
+
``` ruby
|
|
223
|
+
block = Markly.parse(" ~~~~ ruby\n Object.new\n ~~~~").first_child
|
|
224
|
+
|
|
225
|
+
block.code_info
|
|
226
|
+
# => "ruby"
|
|
227
|
+
|
|
228
|
+
block.fence
|
|
229
|
+
# => #<struct Markly::Node::Fence character="~", length=4, indent=2>
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Indented code blocks and other node types return `nil` from `Node#fence`.
|
data/context/getting-started.md
CHANGED
|
@@ -78,57 +78,21 @@ To have multiple options applied, `|` (or) the flags together:
|
|
|
78
78
|
Markly.render_html("\"'Shelob' is my name.\"", flags: Markly::HARD_BREAKS|Markly::SOURCE_POSITION)
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
-
Inline code language prefixes are opt-in. The language is available through
|
|
82
|
-
`Node#code_info` and is rendered as a `language-...` class:
|
|
83
|
-
|
|
84
|
-
``` ruby
|
|
85
|
-
document = Markly.parse("ruby:`Object.new`", flags: Markly::INLINE_CODE_INFO)
|
|
86
|
-
code = document.first_child.first_child
|
|
87
|
-
|
|
88
|
-
code.code_info
|
|
89
|
-
# => "ruby"
|
|
90
|
-
|
|
91
|
-
code.code_language
|
|
92
|
-
# => "ruby"
|
|
93
|
-
|
|
94
|
-
document.to_html
|
|
95
|
-
# => <p><code class="language-ruby">Object.new</code></p>
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
`Node#code_info` is also the general info-string accessor for fenced code
|
|
99
|
-
blocks and front matter. `Node#code_language` returns the first token of that
|
|
100
|
-
info string, while `Node#fence_info` remains available for compatibility on
|
|
101
|
-
those block nodes.
|
|
102
|
-
|
|
103
|
-
For a fenced code block, `Node#fence` returns a `Node::Fence` structure with
|
|
104
|
-
the fence character, length, and indentation:
|
|
105
|
-
|
|
106
|
-
``` ruby
|
|
107
|
-
block = Markly.parse(" ~~~~ ruby\n Object.new\n ~~~~").first_child
|
|
108
|
-
|
|
109
|
-
block.fence
|
|
110
|
-
# => #<struct Markly::Node::Fence character="~", length=4, indent=2>
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
Indented code blocks and other node types return `nil`.
|
|
114
|
-
|
|
115
81
|
## Extensions
|
|
116
82
|
|
|
117
|
-
|
|
83
|
+
Markly parses standard CommonMark by default. GitHub Flavored Markdown syntax
|
|
84
|
+
and Markly-specific syntax are opt-in so applications can choose their accepted
|
|
85
|
+
Markdown dialect explicitly:
|
|
118
86
|
|
|
119
87
|
``` ruby
|
|
120
|
-
Markly.render_html(
|
|
88
|
+
Markly.render_html(
|
|
89
|
+
"| Name | Status |\n| --- | --- |\n| Markly | Ready |",
|
|
90
|
+
extensions: [:table],
|
|
91
|
+
)
|
|
121
92
|
```
|
|
122
93
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
The available extensions are:
|
|
126
|
-
|
|
127
|
-
- `:table` - This provides support for tables.
|
|
128
|
-
- `:tasklist` - This provides support for task list items.
|
|
129
|
-
- `:strikethrough` - This provides support for strikethroughs.
|
|
130
|
-
- `:autolink` - This provides support for automatically converting URLs to anchor tags.
|
|
131
|
-
- `:tagfilter` - This escapes [several "unsafe" HTML tags](https://github.github.com/gfm/#disallowed-raw-html-extension-), causing them to not have any effect.
|
|
94
|
+
See [Extensions](../extensions/index) for the supported extensions, related
|
|
95
|
+
flags, and generated AST.
|
|
132
96
|
|
|
133
97
|
## Developing Locally
|
|
134
98
|
|
data/context/index.yaml
CHANGED
|
@@ -10,6 +10,9 @@ files:
|
|
|
10
10
|
- path: getting-started.md
|
|
11
11
|
title: Getting Started
|
|
12
12
|
description: This guide explains now to install and use Markly.
|
|
13
|
+
- path: extensions.md
|
|
14
|
+
title: Extensions
|
|
15
|
+
description: This guide explains how to enable and use Markly's Markdown extensions.
|
|
13
16
|
- path: abstract-syntax-tree.md
|
|
14
17
|
title: Abstract Syntax Tree
|
|
15
18
|
description: This guide explains how to use Markly's abstract syntax tree (AST)
|
|
@@ -264,6 +264,10 @@ typedef void (*cmark_opaque_free_func) (cmark_syntax_extension *extension,
|
|
|
264
264
|
cmark_mem *mem,
|
|
265
265
|
cmark_node *node);
|
|
266
266
|
|
|
267
|
+
typedef void (*cmark_opaque_copy_func)(cmark_syntax_extension *extension,
|
|
268
|
+
cmark_mem *mem, cmark_node *node,
|
|
269
|
+
cmark_node *source);
|
|
270
|
+
|
|
267
271
|
/** Free a cmark_syntax_extension.
|
|
268
272
|
*/
|
|
269
273
|
CMARK_GFM_EXPORT
|
|
@@ -406,6 +410,12 @@ CMARK_GFM_EXPORT
|
|
|
406
410
|
void cmark_syntax_extension_set_opaque_free_func(cmark_syntax_extension *extension,
|
|
407
411
|
cmark_opaque_free_func func);
|
|
408
412
|
|
|
413
|
+
/** See the documentation for 'cmark_syntax_extension'
|
|
414
|
+
*/
|
|
415
|
+
CMARK_GFM_EXPORT
|
|
416
|
+
void cmark_syntax_extension_set_opaque_copy_func(
|
|
417
|
+
cmark_syntax_extension *extension, cmark_opaque_copy_func func);
|
|
418
|
+
|
|
409
419
|
/** See the documentation for 'cmark_syntax_extension'
|
|
410
420
|
*/
|
|
411
421
|
CMARK_GFM_EXPORT
|
data/ext/markly/cmark-gfm.h
CHANGED
|
@@ -196,6 +196,12 @@ CMARK_GFM_EXPORT cmark_node *cmark_node_new_with_mem_and_ext(cmark_node_type typ
|
|
|
196
196
|
cmark_mem *mem,
|
|
197
197
|
cmark_syntax_extension *extension);
|
|
198
198
|
|
|
199
|
+
/** Creates an independent deep copy of 'node' and all its children.
|
|
200
|
+
* User data is not copied. Returns NULL when extension-specific node data
|
|
201
|
+
* cannot be copied.
|
|
202
|
+
*/
|
|
203
|
+
CMARK_GFM_EXPORT cmark_node *cmark_node_clone(cmark_node *node);
|
|
204
|
+
|
|
199
205
|
/** Frees the memory allocated for a node and any children.
|
|
200
206
|
*/
|
|
201
207
|
CMARK_GFM_EXPORT void cmark_node_free(cmark_node *node);
|
|
@@ -842,6 +842,29 @@ static void opaque_free(cmark_syntax_extension *self, cmark_mem *mem, cmark_node
|
|
|
842
842
|
}
|
|
843
843
|
}
|
|
844
844
|
|
|
845
|
+
static void opaque_copy(cmark_syntax_extension *self, cmark_mem *mem,
|
|
846
|
+
cmark_node *node, cmark_node *source) {
|
|
847
|
+
if (node->type == CMARK_NODE_TABLE) {
|
|
848
|
+
node_table *table = (node_table *)node->as.opaque;
|
|
849
|
+
node_table *source_table = (node_table *)source->as.opaque;
|
|
850
|
+
|
|
851
|
+
table->n_columns = source_table->n_columns;
|
|
852
|
+
table->n_rows = source_table->n_rows;
|
|
853
|
+
table->n_nonempty_cells = source_table->n_nonempty_cells;
|
|
854
|
+
|
|
855
|
+
if (source_table->alignments) {
|
|
856
|
+
table->alignments = mem->calloc(source_table->n_columns, sizeof(uint8_t));
|
|
857
|
+
memcpy(table->alignments, source_table->alignments,
|
|
858
|
+
source_table->n_columns * sizeof(uint8_t));
|
|
859
|
+
}
|
|
860
|
+
} else if (node->type == CMARK_NODE_TABLE_ROW) {
|
|
861
|
+
*(node_table_row *)node->as.opaque = *(node_table_row *)source->as.opaque;
|
|
862
|
+
} else if (node->type == CMARK_NODE_TABLE_CELL) {
|
|
863
|
+
mem->free(node->as.opaque);
|
|
864
|
+
node->as.cell_index = source->as.cell_index;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
845
868
|
static int escape(cmark_syntax_extension *self, cmark_node *node, int c) {
|
|
846
869
|
return
|
|
847
870
|
node->type != CMARK_NODE_TABLE &&
|
|
@@ -867,6 +890,7 @@ cmark_syntax_extension *create_table_extension(void) {
|
|
|
867
890
|
cmark_syntax_extension_set_html_render_func(self, html_render);
|
|
868
891
|
cmark_syntax_extension_set_opaque_alloc_func(self, opaque_alloc);
|
|
869
892
|
cmark_syntax_extension_set_opaque_free_func(self, opaque_free);
|
|
893
|
+
cmark_syntax_extension_set_opaque_copy_func(self, opaque_copy);
|
|
870
894
|
cmark_syntax_extension_set_commonmark_escape_func(self, escape);
|
|
871
895
|
CMARK_NODE_TABLE = cmark_syntax_extension_add_node(0);
|
|
872
896
|
CMARK_NODE_TABLE_ROW = cmark_syntax_extension_add_node(0);
|
data/ext/markly/html.c
CHANGED
|
@@ -303,7 +303,7 @@ static int S_render_node(cmark_html_renderer *renderer, cmark_node *node,
|
|
|
303
303
|
|
|
304
304
|
case CMARK_NODE_PARAGRAPH:
|
|
305
305
|
parent = cmark_node_parent(node);
|
|
306
|
-
grandparent = cmark_node_parent(parent);
|
|
306
|
+
grandparent = parent ? cmark_node_parent(parent) : NULL;
|
|
307
307
|
if (grandparent != NULL && grandparent->type == CMARK_NODE_LIST) {
|
|
308
308
|
tight = grandparent->as.list.tight;
|
|
309
309
|
} else {
|
|
@@ -316,7 +316,8 @@ static int S_render_node(cmark_html_renderer *renderer, cmark_node *node,
|
|
|
316
316
|
cmark_html_render_sourcepos(node, html, options);
|
|
317
317
|
cmark_strbuf_putc(html, '>');
|
|
318
318
|
} else {
|
|
319
|
-
if (parent->type == CMARK_NODE_FOOTNOTE_DEFINITION &&
|
|
319
|
+
if (parent && parent->type == CMARK_NODE_FOOTNOTE_DEFINITION &&
|
|
320
|
+
node->next == NULL) {
|
|
320
321
|
cmark_strbuf_putc(html, ' ');
|
|
321
322
|
S_put_footnote_backref(renderer, html, parent);
|
|
322
323
|
}
|
data/ext/markly/markly.c
CHANGED
|
@@ -271,6 +271,21 @@ static VALUE Markly_Node_new(VALUE self, VALUE type) {
|
|
|
271
271
|
return Markly_Node_wrap(node);
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
+
/*
|
|
275
|
+
* Duplicate the current node and all its children.
|
|
276
|
+
*/
|
|
277
|
+
static VALUE Markly_Node_duplicate(VALUE self) {
|
|
278
|
+
cmark_node *node;
|
|
279
|
+
TypedData_Get_Struct(self, cmark_node, &Markly_Node_Type, node);
|
|
280
|
+
|
|
281
|
+
cmark_node *copy = cmark_node_clone(node);
|
|
282
|
+
if (copy == NULL) {
|
|
283
|
+
rb_raise(Markly_Error, "could not duplicate node");
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return Markly_Node_wrap(copy);
|
|
287
|
+
}
|
|
288
|
+
|
|
274
289
|
static VALUE Markly_Node_replace(VALUE self, VALUE other) {
|
|
275
290
|
cmark_node *current_node = NULL, *replacement_node = NULL;
|
|
276
291
|
|
|
@@ -1273,6 +1288,7 @@ static void Init_Markly_Node(VALUE Markly) {
|
|
|
1273
1288
|
rb_undef_alloc_func(Markly_Node);
|
|
1274
1289
|
rb_define_singleton_method(Markly_Node, "new", Markly_Node_new, 1);
|
|
1275
1290
|
Markly_Node_Fence = rb_struct_define_under(Markly_Node, "Fence", "character", "length", "indent", NULL);
|
|
1291
|
+
rb_define_method(Markly_Node, "_dup", Markly_Node_duplicate, 0);
|
|
1276
1292
|
|
|
1277
1293
|
rb_define_method(Markly_Node, "replace", Markly_Node_replace, 1);
|
|
1278
1294
|
|
data/ext/markly/node.c
CHANGED
|
@@ -145,6 +145,144 @@ 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:
|
|
@@ -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/lib/markly/node.rb
CHANGED
|
@@ -20,15 +20,7 @@ module Markly
|
|
|
20
20
|
#
|
|
21
21
|
# @returns [Markly::Node] The duplicated node tree.
|
|
22
22
|
def dup
|
|
23
|
-
|
|
24
|
-
node = Markly.parse(self.to_markdown)
|
|
25
|
-
|
|
26
|
-
# If we aren't duplicating a document, we return `first_child` as the root will be a document node:
|
|
27
|
-
if self.type == :document
|
|
28
|
-
return node
|
|
29
|
-
else
|
|
30
|
-
return node.first_child
|
|
31
|
-
end
|
|
23
|
+
_dup
|
|
32
24
|
end
|
|
33
25
|
|
|
34
26
|
# Walk the node tree recursively.
|
data/lib/markly/version.rb
CHANGED
data/readme.md
CHANGED
|
@@ -16,6 +16,8 @@ Please see the [project documentation](https://socketry.github.io/markly/) for m
|
|
|
16
16
|
|
|
17
17
|
- [Getting Started](https://socketry.github.io/markly/guides/getting-started/index) - This guide explains now to install and use Markly.
|
|
18
18
|
|
|
19
|
+
- [Extensions](https://socketry.github.io/markly/guides/extensions/index) - This guide explains how to enable and use Markly's Markdown extensions.
|
|
20
|
+
|
|
19
21
|
- [Abstract Syntax Tree](https://socketry.github.io/markly/guides/abstract-syntax-tree/index) - This guide explains how to use Markly's abstract syntax tree (AST) to parse and manipulate Markdown documents.
|
|
20
22
|
|
|
21
23
|
- [Headings](https://socketry.github.io/markly/guides/headings/index) - This guide explains how to work with headings in Markly, including extracting them for navigation and handling duplicate heading text.
|
|
@@ -24,6 +26,10 @@ Please see the [project documentation](https://socketry.github.io/markly/) for m
|
|
|
24
26
|
|
|
25
27
|
Please see the [project releases](https://socketry.github.io/markly/releases/index) for all releases.
|
|
26
28
|
|
|
29
|
+
### v0.18.0
|
|
30
|
+
|
|
31
|
+
- Preserve complete node and extension metadata when duplicating node trees.
|
|
32
|
+
|
|
27
33
|
### v0.17.0
|
|
28
34
|
|
|
29
35
|
- Add opt-in language prefixes for inline code spans with `Markly::INLINE_CODE_INFO`, expose code metadata through `Node#code_info`, and provide `Node#code_language` as a convenient language accessor.
|
data/releases.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Releases
|
|
2
2
|
|
|
3
|
+
## v0.18.0
|
|
4
|
+
|
|
5
|
+
- Preserve complete node and extension metadata when duplicating node trees.
|
|
6
|
+
|
|
3
7
|
## v0.17.0
|
|
4
8
|
|
|
5
9
|
- Add opt-in language prefixes for inline code spans with `Markly::INLINE_CODE_INFO`, expose code metadata through `Node#code_info`, and provide `Node#code_language` as a convenient language accessor.
|
data.tar.gz.sig
CHANGED
|
Binary file
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: markly
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.18.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Garen Torikian
|
|
@@ -65,6 +65,7 @@ extensions:
|
|
|
65
65
|
extra_rdoc_files: []
|
|
66
66
|
files:
|
|
67
67
|
- context/abstract-syntax-tree.md
|
|
68
|
+
- context/extensions.md
|
|
68
69
|
- context/getting-started.md
|
|
69
70
|
- context/headings.md
|
|
70
71
|
- context/index.yaml
|
metadata.gz.sig
CHANGED
|
Binary file
|