mdom 0.2.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +71 -0
- data/LICENSE.txt +21 -0
- data/README.md +285 -0
- data/USAGE.md +789 -0
- data/lib/mdom/block_detector.rb +200 -0
- data/lib/mdom/builder.rb +378 -163
- data/lib/mdom/error.rb +22 -0
- data/lib/mdom/hooks.rb +81 -0
- data/lib/mdom/html_serializer.rb +330 -0
- data/lib/mdom/inline_parser.rb +541 -142
- data/lib/mdom/markdown_serializer.rb +332 -0
- data/lib/mdom/node/inline_nodes.rb +239 -0
- data/lib/mdom/node/nodes.rb +336 -0
- data/lib/mdom/node.rb +677 -199
- data/lib/mdom/parser.rb +257 -235
- data/lib/mdom/tokenizer.rb +415 -0
- data/lib/mdom/translator.rb +379 -0
- data/lib/mdom/version.rb +1 -1
- data/lib/mdom.rb +58 -7
- metadata +40 -7
- data/lib/mdom/nodes.rb +0 -214
- data/lib/mdom/serializer.rb +0 -165
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
|
data/lib/mdom/serializer.rb
DELETED
|
@@ -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, marker: "-")
|
|
10
|
-
new.serialize(node, indent: indent, blockquote: blockquote, marker: marker)
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def serialize(node, indent: 0, blockquote: false, marker: "-")
|
|
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, marker: marker)
|
|
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
|
-
""
|
|
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
|