mdom 0.1.0 → 0.2.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
- data/lib/mdom/builder.rb +271 -0
- data/lib/mdom/serializer.rb +4 -4
- data/lib/mdom/version.rb +1 -1
- data/lib/mdom.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f5fd39950b36dde37a473d2ed62086a94a218bf3d7fc17bfc107fe2b443c2003
|
|
4
|
+
data.tar.gz: 15d24864d48fe97e4d82e5990f9163578b6a4b55104d195e87c4e24f14d26f1f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 995dfd2579600430baedb7592b22ef2c94908c2bf3e16b1daa33550f3058d25783275af0237d00e7152876e14156befe18a2894d087f9cc556acdc7fe36f32ad
|
|
7
|
+
data.tar.gz: f286b92331b99bb57250df4d13ea1d4e5e08dc9020b2c8cb3e90e5f5522430897cf112c0ffce0dd05f280a63de5f560869c8a8dda444946d072a0296977cb0c6
|
data/lib/mdom/builder.rb
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MDOM
|
|
4
|
+
# A builder (DSL) for constructing MDOM trees programmatically, as an
|
|
5
|
+
# alternative to parsing Markdown text.
|
|
6
|
+
#
|
|
7
|
+
# Every method both appends the constructed node to the node currently being
|
|
8
|
+
# built and returns the builder itself, so calls chain. Blocks nest: while a
|
|
9
|
+
# block runs, the node it creates becomes the builder's current target, and
|
|
10
|
+
# the previous target is restored when the block returns.
|
|
11
|
+
#
|
|
12
|
+
# doc = MDOM.build do |b|
|
|
13
|
+
# b.h1 "Title"
|
|
14
|
+
# b.p "A paragraph with *emphasis*."
|
|
15
|
+
# b.list do
|
|
16
|
+
# b.item "one"
|
|
17
|
+
# b.item "two"
|
|
18
|
+
# end
|
|
19
|
+
# end
|
|
20
|
+
# doc.to_markdown
|
|
21
|
+
#
|
|
22
|
+
# String arguments to block- and inline-level methods are run through the
|
|
23
|
+
# inline parser, so *emphasis*, **strong**, `code`, [links](url) and
|
|
24
|
+
#  are recognised. Use the explicit inline methods
|
|
25
|
+
# (+em+, +strong+, +code_span+, +link+, +image+) when you need an exact
|
|
26
|
+
# structure; their string arguments are treated as literal text unless you
|
|
27
|
+
# nest an explicit method inside a block.
|
|
28
|
+
class Builder
|
|
29
|
+
# Build a document with +block+ (or a nested block passed to an explicit
|
|
30
|
+
# +builder+) and return it. See MDOM.build.
|
|
31
|
+
def self.build(builder = new, &block)
|
|
32
|
+
builder.document(&block)
|
|
33
|
+
builder.to_document
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def initialize
|
|
37
|
+
@document = Document.new
|
|
38
|
+
@stack = [@document]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# -- block level ---------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
# Build a document (the root node). With a block, the block is evaluated
|
|
44
|
+
# against this builder and its content is added to the root document.
|
|
45
|
+
# Returns the builder; use +to_document+ / +finish+ for the tree.
|
|
46
|
+
def document(&block)
|
|
47
|
+
push(@document) { instance_eval(&block) } if block
|
|
48
|
+
self
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# An ATX heading. +level+ defaults to 1; the remaining arguments are the
|
|
52
|
+
# heading's inline content.
|
|
53
|
+
def heading(level = 1, *parts, **attributes)
|
|
54
|
+
node = Heading.new(level, children: inline_nodes(parts), attributes: attributes)
|
|
55
|
+
append(node)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def h1(*parts, **attributes) = heading(1, *parts, **attributes)
|
|
59
|
+
def h2(*parts, **attributes) = heading(2, *parts, **attributes)
|
|
60
|
+
def h3(*parts, **attributes) = heading(3, *parts, **attributes)
|
|
61
|
+
def h4(*parts, **attributes) = heading(4, *parts, **attributes)
|
|
62
|
+
def h5(*parts, **attributes) = heading(5, *parts, **attributes)
|
|
63
|
+
def h6(*parts, **attributes) = heading(6, *parts, **attributes)
|
|
64
|
+
|
|
65
|
+
# A paragraph. With a block, the block's inline nodes become its children.
|
|
66
|
+
def paragraph(*parts, &block)
|
|
67
|
+
node = Paragraph.new
|
|
68
|
+
build_inline(node, parts, &block)
|
|
69
|
+
append(node)
|
|
70
|
+
end
|
|
71
|
+
alias p paragraph
|
|
72
|
+
|
|
73
|
+
# A list. +ordered: true+ produces an ordered list; +start:+ sets its first
|
|
74
|
+
# number. Without a block, +items+ are wrapped into single-paragraph list
|
|
75
|
+
# items.
|
|
76
|
+
def list(*items, ordered: false, tight: true, start: nil, &block)
|
|
77
|
+
node = List.new(ordered: ordered, tight: tight, start: start)
|
|
78
|
+
if block
|
|
79
|
+
push(node) { instance_eval(&block) }
|
|
80
|
+
else
|
|
81
|
+
items.each { node.append(ListItem.new(children: [Paragraph.new])) }
|
|
82
|
+
refill_items(node, items)
|
|
83
|
+
end
|
|
84
|
+
append(node)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# An unordered list, as a shorthand for +list(ordered: false)+.
|
|
88
|
+
def ul(*items, &block) = list(*items, ordered: false, &block)
|
|
89
|
+
# An ordered list, as a shorthand for +list(ordered: true)+.
|
|
90
|
+
def ol(*items, start: nil, &block) = list(*items, ordered: true, start: start, &block)
|
|
91
|
+
|
|
92
|
+
# A list item. Without a block, +parts+ become a single paragraph.
|
|
93
|
+
def item(*parts, task: nil, checked: false, &block)
|
|
94
|
+
node = ListItem.new
|
|
95
|
+
unless task.nil?
|
|
96
|
+
node.attributes[:task] = true
|
|
97
|
+
node.attributes[:checked] = checked ? true : false
|
|
98
|
+
end
|
|
99
|
+
if block
|
|
100
|
+
push(node) { instance_eval(&block) }
|
|
101
|
+
elsif !parts.empty?
|
|
102
|
+
node.append(Paragraph.new(children: inline_nodes(parts)))
|
|
103
|
+
end
|
|
104
|
+
append(node)
|
|
105
|
+
end
|
|
106
|
+
alias li item
|
|
107
|
+
|
|
108
|
+
# A task list item (+checked+ marks it done).
|
|
109
|
+
def task(checked, *parts, &block)
|
|
110
|
+
item(*parts, task: true, checked: checked, &block)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# A blockquote. +parts+/+block+ are parsed as block content.
|
|
114
|
+
def blockquote(*parts, &block)
|
|
115
|
+
node = Blockquote.new
|
|
116
|
+
build_blocks(node, parts, &block)
|
|
117
|
+
append(node)
|
|
118
|
+
end
|
|
119
|
+
alias quote blockquote
|
|
120
|
+
|
|
121
|
+
# A fenced code block. +lang+ sets the info string.
|
|
122
|
+
def code_block(literal, lang: nil, fenced: true)
|
|
123
|
+
append(CodeBlock.new(literal.to_s, lang: lang, fenced: fenced))
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# A horizontal rule.
|
|
127
|
+
def hrule(*, **)
|
|
128
|
+
append(Hrule.new)
|
|
129
|
+
end
|
|
130
|
+
alias hr hrule
|
|
131
|
+
|
|
132
|
+
# -- inline --------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
# Emphasis (*x*). With a block, nested explicit inline methods are used;
|
|
135
|
+
# otherwise string arguments are parsed as inline content. Appends to the
|
|
136
|
+
# current target and returns the node.
|
|
137
|
+
def em(*parts, &block)
|
|
138
|
+
node = Emphasis.new
|
|
139
|
+
build_inline(node, parts, &block)
|
|
140
|
+
append(node)
|
|
141
|
+
node
|
|
142
|
+
end
|
|
143
|
+
alias emphasis em
|
|
144
|
+
|
|
145
|
+
# Strong (**x**).
|
|
146
|
+
def strong(*parts, &block)
|
|
147
|
+
node = Strong.new
|
|
148
|
+
build_inline(node, parts, &block)
|
|
149
|
+
append(node)
|
|
150
|
+
node
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# A code span (`x`). The argument is literal.
|
|
154
|
+
def code_span(value)
|
|
155
|
+
append(Code.new(value.to_s))
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# A link. Without a block the label is the literal +*label+ parts; with a
|
|
159
|
+
# block, nested explicit inline methods build the label.
|
|
160
|
+
def link(destination, *label, title: nil, &block)
|
|
161
|
+
node = Link.new(destination, title: title)
|
|
162
|
+
build_inline(node, label, &block)
|
|
163
|
+
append(node)
|
|
164
|
+
node
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# An image.
|
|
168
|
+
def image(destination, alt: nil, title: nil)
|
|
169
|
+
append(Image.new(destination, alt: alt, title: title))
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# A soft line break.
|
|
173
|
+
def softbreak
|
|
174
|
+
append(Softbreak.new)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# A hard line break (two trailing spaces).
|
|
178
|
+
def hardbreak
|
|
179
|
+
append(Hardbreak.new)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# -- results -------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
# The document built so far.
|
|
185
|
+
def to_document
|
|
186
|
+
@document
|
|
187
|
+
end
|
|
188
|
+
alias finish to_document
|
|
189
|
+
|
|
190
|
+
# Serialize the document built so far to Markdown.
|
|
191
|
+
def to_markdown
|
|
192
|
+
@document.to_markdown
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# A human-readable tree rendering of the document built so far.
|
|
196
|
+
def inspect
|
|
197
|
+
to_document.inspect
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
private
|
|
201
|
+
|
|
202
|
+
# Append +node+ to the current target and return the builder, so both
|
|
203
|
+
# +b.p "x"+ chains and top-level calls accumulate in the document.
|
|
204
|
+
def append(node)
|
|
205
|
+
@stack.last.append(node)
|
|
206
|
+
self
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Make +node+ the current target while the block runs, then restore.
|
|
210
|
+
def push(node)
|
|
211
|
+
@stack.push(node)
|
|
212
|
+
yield
|
|
213
|
+
ensure
|
|
214
|
+
@stack.pop
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Fill +node+ with inline content: a block plus direct parts, or the parts
|
|
218
|
+
# alone. With a block, parts are rejected (there is only one content site).
|
|
219
|
+
def build_inline(node, parts, &block)
|
|
220
|
+
if block
|
|
221
|
+
raise ArgumentError, "cannot combine inline arguments with a block" unless parts.empty?
|
|
222
|
+
|
|
223
|
+
push(node) { instance_eval(&block) }
|
|
224
|
+
else
|
|
225
|
+
node.append_parts(inline_nodes(parts))
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Fill +node+ with block content: a block plus direct parts (paragraphs),
|
|
230
|
+
# or the parts alone.
|
|
231
|
+
def build_blocks(node, parts, &block)
|
|
232
|
+
parts.each { |part| node.append(Paragraph.new(children: inline_nodes([part]))) }
|
|
233
|
+
push(node) { instance_eval(&block) } if block
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Build the item paragraphs for a block-less +list(*items)+ call.
|
|
237
|
+
def refill_items(list_node, items)
|
|
238
|
+
list_node.children.each_with_index do |item_node, idx|
|
|
239
|
+
item_node.children.first.append_parts(inline_nodes([items[idx]]))
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Convert arguments into inline nodes: strings are parsed as inline
|
|
244
|
+
# Markdown, existing nodes pass through (and are detached from any parent).
|
|
245
|
+
def inline_nodes(parts)
|
|
246
|
+
parts.flat_map do |part|
|
|
247
|
+
if part.is_a?(Node)
|
|
248
|
+
part.remove
|
|
249
|
+
part
|
|
250
|
+
else
|
|
251
|
+
InlineParser.parse(part.to_s)
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def self.build(&block)
|
|
258
|
+
Builder.build(&block)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Adds a batch-append helper to the block/inline nodes that carry inline
|
|
262
|
+
# content, so the builder can attach parsed parts in one call.
|
|
263
|
+
module InlineContainer
|
|
264
|
+
def append_parts(nodes)
|
|
265
|
+
nodes.each { |node| append(node) }
|
|
266
|
+
self
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
[Paragraph, Emphasis, Strong, Link].each { |klass| klass.include(InlineContainer) }
|
|
271
|
+
end
|
data/lib/mdom/serializer.rb
CHANGED
|
@@ -6,17 +6,17 @@ module MDOM
|
|
|
6
6
|
# flows downward so nested lists and blockquotes render correctly.
|
|
7
7
|
class Serializer
|
|
8
8
|
# Serialize +node+ to Markdown. Returns a String.
|
|
9
|
-
def self.serialize(node, indent: 0, blockquote: false)
|
|
10
|
-
new.serialize(node, indent: indent, blockquote: blockquote)
|
|
9
|
+
def self.serialize(node, indent: 0, blockquote: false, marker: "-")
|
|
10
|
+
new.serialize(node, indent: indent, blockquote: blockquote, marker: marker)
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
-
def serialize(node, indent: 0, blockquote: false)
|
|
13
|
+
def serialize(node, indent: 0, blockquote: false, marker: "-")
|
|
14
14
|
case node.type
|
|
15
15
|
when Types::DOCUMENT then serialize_document(node)
|
|
16
16
|
when Types::HEADING then serialize_heading(node, indent: indent, blockquote: blockquote)
|
|
17
17
|
when Types::PARAGRAPH then serialize_paragraph(node, indent: indent, blockquote: blockquote)
|
|
18
18
|
when Types::LIST then serialize_list(node, indent: indent, blockquote: blockquote)
|
|
19
|
-
when Types::LIST_ITEM then serialize_list_item(node, indent: indent, blockquote: blockquote)
|
|
19
|
+
when Types::LIST_ITEM then serialize_list_item(node, indent: indent, blockquote: blockquote, marker: marker)
|
|
20
20
|
when Types::BLOCKQUOTE then serialize_blockquote(node, indent: indent)
|
|
21
21
|
when Types::CODE_BLOCK then serialize_code_block(node, indent: indent, blockquote: blockquote)
|
|
22
22
|
when Types::HRULE then serialize_hrule(indent: indent, blockquote: blockquote)
|
data/lib/mdom/version.rb
CHANGED
data/lib/mdom.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mdom
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- mdom
|
|
@@ -16,6 +16,7 @@ extensions: []
|
|
|
16
16
|
extra_rdoc_files: []
|
|
17
17
|
files:
|
|
18
18
|
- lib/mdom.rb
|
|
19
|
+
- lib/mdom/builder.rb
|
|
19
20
|
- lib/mdom/inline_parser.rb
|
|
20
21
|
- lib/mdom/node.rb
|
|
21
22
|
- lib/mdom/nodes.rb
|