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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 46b3e2018949cf138e98983729796223a7388a612d3b8a7257a55abd9f1bcb53
4
- data.tar.gz: df13bcf4a9d0aac9a9e7150a80a9cdf82ba6081a02cfeb1d1d43b63bb467cbbb
3
+ metadata.gz: f5fd39950b36dde37a473d2ed62086a94a218bf3d7fc17bfc107fe2b443c2003
4
+ data.tar.gz: 15d24864d48fe97e4d82e5990f9163578b6a4b55104d195e87c4e24f14d26f1f
5
5
  SHA512:
6
- metadata.gz: 8662a2697905070ec926e79d9b47dfc9753747dc43437a9498304db9f353d848f13b4cd73113663826c4e9bffb887204fb274ca39c6a0c984f368f95696c6a3f
7
- data.tar.gz: 0e4e44ba65089ab2562715fba447ecc5cc0ae1211e700424692c13a8ed98fc5e94bc8145f46a7c9566b26e1d8d4ca594a1c77862c54a7937677c6a09a8373a74
6
+ metadata.gz: 995dfd2579600430baedb7592b22ef2c94908c2bf3e16b1daa33550f3058d25783275af0237d00e7152876e14156befe18a2894d087f9cc556acdc7fe36f32ad
7
+ data.tar.gz: f286b92331b99bb57250df4d13ea1d4e5e08dc9020b2c8cb3e90e5f5522430897cf112c0ffce0dd05f280a63de5f560869c8a8dda444946d072a0296977cb0c6
@@ -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
+ # ![images](src) 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
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MDOM
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/mdom.rb CHANGED
@@ -6,6 +6,7 @@ require_relative "mdom/nodes"
6
6
  require_relative "mdom/parser"
7
7
  require_relative "mdom/inline_parser"
8
8
  require_relative "mdom/serializer"
9
+ require_relative "mdom/builder"
9
10
 
10
11
  module MDOM
11
12
  # Serialize a node (or subtree) back to a Markdown string.
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.1.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