mdom 0.1.0 → 0.3.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.
data/lib/mdom/nodes.rb DELETED
@@ -1,214 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module MDOM
4
- # Node type constants, shared across the code base.
5
- module Types
6
- DOCUMENT = :document
7
- HEADING = :heading
8
- PARAGRAPH = :paragraph
9
- LIST = :list
10
- LIST_ITEM = :list_item
11
- LIST_ITEM_LINE = :list_item_line
12
- BLOCKQUOTE = :blockquote
13
- CODE_BLOCK = :code_block
14
- HRULE = :hrule
15
-
16
- TEXT = :text
17
- EMPHASIS = :emphasis
18
- STRONG = :strong
19
- CODE = :code
20
- LINK = :link
21
- IMAGE = :image
22
- SOFTBREAK = :softbreak
23
- HARDBREAK = :hardbreak
24
- end
25
-
26
- # -- Block nodes ----------------------------------------------------------
27
-
28
- # Each concrete node class fixes its +type+ and declares which children it
29
- # accepts. Subclassing Node keeps everything a Node so traversal is uniform.
30
- class BlockNode < Node
31
- end
32
-
33
- class Document < BlockNode
34
- def initialize(children: [])
35
- super(Types::DOCUMENT, children: children)
36
- end
37
- end
38
-
39
- class Heading < BlockNode
40
- attr_reader :level
41
-
42
- def initialize(level, children: [], attributes: {})
43
- @level = level.to_i
44
- super(Types::HEADING, attributes: attributes.merge(level: @level), children: children)
45
- end
46
- end
47
-
48
- class Paragraph < BlockNode
49
- def initialize(children: [])
50
- super(Types::PARAGRAPH, children: children)
51
- end
52
- end
53
-
54
- class List < BlockNode
55
- # ordered: true/false, tight: true/false, start: integer for ordered lists
56
- def initialize(ordered: false, tight: true, start: nil, children: [], attributes: {})
57
- attrs = { ordered: ordered, tight: tight }.merge(attributes)
58
- attrs[:start] = start unless start.nil?
59
- super(Types::LIST, attributes: attrs, children: children)
60
- end
61
-
62
- def ordered?
63
- attributes[:ordered]
64
- end
65
- end
66
-
67
- class ListItem < BlockNode
68
- def initialize(children: [])
69
- super(Types::LIST_ITEM, children: children)
70
- end
71
- end
72
-
73
- class Blockquote < BlockNode
74
- def initialize(children: [])
75
- super(Types::BLOCKQUOTE, children: children)
76
- end
77
- end
78
-
79
- class CodeBlock < BlockNode
80
- # lang: language string (may be nil), fenced: true/false, literal: source text
81
- def initialize(literal, lang: nil, fenced: true, attributes: {})
82
- attrs = { fenced: fenced }.merge(attributes)
83
- attrs[:lang] = lang unless lang.nil?
84
- super(Types::CODE_BLOCK, attributes: attrs)
85
- # Code block text is kept as a raw string, not child nodes.
86
- @literal = literal
87
- end
88
-
89
- attr_reader :literal
90
-
91
- def lang
92
- attributes[:lang]
93
- end
94
-
95
- def fenced?
96
- attributes[:fenced]
97
- end
98
-
99
- def to_s
100
- "#<MDOM::CodeBlock fenced=#{fenced?} lang=#{lang.inspect}>"
101
- end
102
- end
103
-
104
- class Hrule < BlockNode
105
- def initialize(attributes: {})
106
- super(Types::HRULE, attributes: attributes)
107
- end
108
- end
109
-
110
- def self.allocate_node(type, **attrs)
111
- case type
112
- when Types::HEADING then Heading.new(attrs.fetch(:level, 1), attributes: attrs[:attributes])
113
- when Types::PARAGRAPH then Paragraph.new
114
- when Types::LIST then List.new(**attrs)
115
- when Types::LIST_ITEM then ListItem.new
116
- when Types::BLOCKQUOTE then Blockquote.new
117
- when Types::HRULE then Hrule.new
118
- else
119
- BlockNode.new(type, attributes: attrs)
120
- end
121
- end
122
-
123
- # -- Inline nodes ---------------------------------------------------------
124
-
125
- class InlineNode < Node
126
- end
127
-
128
- class Text < InlineNode
129
- def initialize(value)
130
- @value = value
131
- super(Types::TEXT)
132
- end
133
-
134
- attr_reader :value
135
-
136
- def to_s
137
- @value
138
- end
139
- end
140
-
141
- class Emphasis < InlineNode
142
- def initialize(children: [])
143
- super(Types::EMPHASIS, children: children)
144
- end
145
- end
146
-
147
- class Strong < InlineNode
148
- def initialize(children: [])
149
- super(Types::STRONG, children: children)
150
- end
151
- end
152
-
153
- class Code < InlineNode
154
- def initialize(value)
155
- @value = value
156
- super(Types::CODE)
157
- end
158
-
159
- attr_reader :value
160
-
161
- def to_s
162
- @value
163
- end
164
- end
165
-
166
- class Link < InlineNode
167
- def initialize(dest, title: nil, children: [])
168
- attrs = { destination: dest }
169
- attrs[:title] = title unless title.nil?
170
- super(Types::LINK, attributes: attrs, children: children)
171
- end
172
-
173
- def destination
174
- attributes[:destination]
175
- end
176
-
177
- def title
178
- attributes[:title]
179
- end
180
- end
181
-
182
- class Image < InlineNode
183
- def initialize(dest, alt: nil, title: nil)
184
- attrs = { destination: dest }
185
- attrs[:alt] = alt unless alt.nil?
186
- attrs[:title] = title unless title.nil?
187
- super(Types::IMAGE, attributes: attrs)
188
- end
189
-
190
- def destination
191
- attributes[:destination]
192
- end
193
-
194
- def alt
195
- attributes[:alt]
196
- end
197
-
198
- def title
199
- attributes[:title]
200
- end
201
- end
202
-
203
- class Softbreak < InlineNode
204
- def initialize
205
- super(Types::SOFTBREAK)
206
- end
207
- end
208
-
209
- class Hardbreak < InlineNode
210
- def initialize
211
- super(Types::HARDBREAK)
212
- end
213
- end
214
- end
@@ -1,165 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module MDOM
4
- # Serializes an MDOM tree back to a Markdown string. Traversal is driven by
5
- # node type; the context (indentation level, in-blockquote flag, list marker)
6
- # flows downward so nested lists and blockquotes render correctly.
7
- class Serializer
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)
11
- end
12
-
13
- def serialize(node, indent: 0, blockquote: false)
14
- case node.type
15
- when Types::DOCUMENT then serialize_document(node)
16
- when Types::HEADING then serialize_heading(node, indent: indent, blockquote: blockquote)
17
- when Types::PARAGRAPH then serialize_paragraph(node, indent: indent, blockquote: blockquote)
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)
20
- when Types::BLOCKQUOTE then serialize_blockquote(node, indent: indent)
21
- when Types::CODE_BLOCK then serialize_code_block(node, indent: indent, blockquote: blockquote)
22
- when Types::HRULE then serialize_hrule(indent: indent, blockquote: blockquote)
23
- else serialize_inline(node)
24
- end
25
- end
26
-
27
- private
28
-
29
- def serialize_document(node)
30
- blocks = node.children.map { |c| serialize(c) }
31
- join_blocks(blocks)
32
- end
33
-
34
- def serialize_heading(node, indent:, blockquote:)
35
- prefix = spaces(indent)
36
- text = serialize_inline_children(node.children)
37
- line = "#{'#' * node.level} #{text}"
38
- wrap(prefix + line, blockquote)
39
- end
40
-
41
- def serialize_paragraph(node, indent:, blockquote:)
42
- prefix = spaces(indent)
43
- inner = serialize_inline_children(node.children)
44
- inner_lines = inner.split("\n").map { |l| prefix + l }
45
- # Hard breaks (two trailing spaces) were captured as soft/hardbreak nodes;
46
- # join preserved '\n' and treat each line under the same prefix.
47
- wrap_inline_lines(inner_lines.join("\n"), blockquote)
48
- end
49
-
50
- def serialize_list(node, indent:, blockquote:)
51
- marker = node.ordered? ? nil : "-"
52
- start = node.ordered? ? (node.attributes[:start] || 1) : nil
53
- lines = node.children.each_with_index.map do |item, idx|
54
- num = start ? "#{start + idx}." : marker
55
- serialize_list_item(item, marker: num, indent: indent, blockquote: blockquote)
56
- end
57
- wrap(lines.join("\n"), blockquote)
58
- end
59
-
60
- def serialize_list_item(node, indent:, blockquote:, marker:)
61
- head = marker || "-"
62
- prefix = spaces(indent)
63
- marker_col = indent + head.length + 1
64
- task = node.attributes[:task] ? "[#{node.attributes[:checked] ? 'x' : ' '}] " : ""
65
-
66
- first = node.children.first
67
- first_line =
68
- if first && first.type == Types::PARAGRAPH
69
- "#{prefix}#{head} #{task}#{first.plain_text}"
70
- elsif first
71
- "#{prefix}#{head} #{task}#{serialize(first, indent: marker_col, blockquote: blockquote).strip}"
72
- else
73
- "#{prefix}#{head} #{task}"
74
- end
75
-
76
- rest = node.children[1..] || []
77
- rest_lines = rest.map { |c| serialize(c, indent: marker_col, blockquote: blockquote) }
78
- [first_line, *rest_lines].join("\n")
79
- end
80
-
81
- def serialize_blockquote(node, indent:)
82
- inner = node.children.map { |c| serialize(c) }.join("\n\n")
83
- inner.lines.map(&:chomp).map { |l| l.strip.empty? ? ">" : "#{spaces(indent)}> #{l}" }.join("\n")
84
- end
85
-
86
- def serialize_code_block(node, indent:, blockquote:)
87
- prefix = spaces(indent)
88
- if node.fenced?
89
- fence = "```"
90
- lang = node.lang ? node.lang : ""
91
- content = node.literal.to_s.lines.map { |l| prefix + l.chomp }.join("\n")
92
- wrap("#{prefix}#{fence}#{lang}\n#{content}\n#{prefix}#{fence}", blockquote)
93
- else
94
- # Indented code: 4 spaces.
95
- lines = node.literal.to_s.split("\n").map { |l| prefix + " " + l }
96
- wrap(lines.join("\n"), blockquote)
97
- end
98
- end
99
-
100
- def serialize_hrule(indent:, blockquote:)
101
- wrap(spaces(indent) + "---", blockquote)
102
- end
103
-
104
- # -- inline -------------------------------------------------------------
105
-
106
- def serialize_inline(node)
107
- serialize_inline_children(node.children)
108
- end
109
-
110
- def serialize_inline_children(children)
111
- children.map { |c| serialize_inline_node(c) }.join
112
- end
113
-
114
- def serialize_inline_node(node)
115
- case node.type
116
- when Types::TEXT then node.value.to_s
117
- when Types::EMPHASIS then "*#{serialize_inline_children(node.children)}*"
118
- when Types::STRONG then "**#{serialize_inline_children(node.children)}**"
119
- when Types::CODE then "`#{node.value}`"
120
- when Types::LINK then serialize_link(node)
121
- when Types::IMAGE then serialize_image(node)
122
- when Types::SOFTBREAK then "\n"
123
- when Types::HARDBREAK then " \n"
124
- when Types::DOCUMENT, Types::PARAGRAPH, Types::HEADING
125
- serialize_inline_children(node.children)
126
- else node.respond_to?(:value) ? node.value.to_s : ""
127
- end
128
- end
129
-
130
- def serialize_link(node)
131
- label = serialize_inline_children(node.children)
132
- title = node.title ? %( "#{node.title}") : ""
133
- "[#{label}](#{node.destination}#{title})"
134
- end
135
-
136
- def serialize_image(node)
137
- title = node.title ? %( "#{node.title}") : ""
138
- "![#{node.alt}](#{node.destination}#{title})"
139
- end
140
-
141
- # -- helpers ------------------------------------------------------------
142
-
143
- def spaces(n)
144
- " " * n
145
- end
146
-
147
- # Join top-level blocks with a single blank line between them.
148
- def join_blocks(blocks)
149
- blocks.map { |b| b.sub(/\A\n+|\n+\z/, "") }.join("\n\n")
150
- end
151
-
152
- # Prefix already-indented block content with a blockquote marker per line.
153
- def wrap(content, blockquote)
154
- return content unless blockquote
155
-
156
- content.lines.map { |l| l.strip.empty? ? ">" : "> #{l}" }.join
157
- end
158
-
159
- def wrap_inline_lines(content, blockquote)
160
- return content unless blockquote
161
-
162
- content.lines.map { |l| l.strip.empty? ? ">" : "> #{l}" }.join
163
- end
164
- end
165
- end