mmmd 0.1.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 +7 -0
- data/README.md +3 -0
- data/architecture.md +278 -0
- data/bin/mmmdpp +168 -0
- data/lib/mmmd/blankshell.rb +1895 -0
- data/lib/mmmd/entities.json +2233 -0
- data/lib/mmmd/renderers/html.rb +356 -0
- data/lib/mmmd/renderers/plainterm.rb +452 -0
- data/lib/mmmd/renderers.rb +11 -0
- data/lib/mmmd/util.rb +61 -0
- data/lib/mmmd.rb +14 -0
- data/security.md +21 -0
- metadata +61 -0
@@ -0,0 +1,356 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../util"
|
4
|
+
|
5
|
+
module MMMD
|
6
|
+
module Renderers
|
7
|
+
module HTMLConstants
|
8
|
+
ELEMENT_MAP = {
|
9
|
+
"PointBlank::DOM::InlinePre" => {
|
10
|
+
tag: "code",
|
11
|
+
style: "white-space: pre;"
|
12
|
+
},
|
13
|
+
"PointBlank::DOM::InlineBreak" => {
|
14
|
+
tag: "br"
|
15
|
+
},
|
16
|
+
"PointBlank::DOM::InlineStrong" => {
|
17
|
+
tag: "strong"
|
18
|
+
},
|
19
|
+
"PointBlank::DOM::InlineEmphasis" => {
|
20
|
+
tag: "em"
|
21
|
+
},
|
22
|
+
"PointBlank::DOM::InlineUnder" => {
|
23
|
+
tag: "span",
|
24
|
+
style: "text-decoration: underline;"
|
25
|
+
},
|
26
|
+
"PointBlank::DOM::InlineStrike" => {
|
27
|
+
tag: "s"
|
28
|
+
},
|
29
|
+
"PointBlank::DOM::InlineLink" => {
|
30
|
+
tag: "a",
|
31
|
+
href: true,
|
32
|
+
title: true
|
33
|
+
},
|
34
|
+
"PointBlank::DOM::InlineImage" => {
|
35
|
+
tag: "img",
|
36
|
+
src: true,
|
37
|
+
inline: true,
|
38
|
+
alt: true,
|
39
|
+
title: true
|
40
|
+
},
|
41
|
+
"PointBlank::DOM::ULBlock" => {
|
42
|
+
tag: "ul"
|
43
|
+
},
|
44
|
+
"PointBlank::DOM::OLBlock" => {
|
45
|
+
tag: "ol"
|
46
|
+
},
|
47
|
+
"PointBlank::DOM::IndentBlock" => {
|
48
|
+
tag: "pre"
|
49
|
+
},
|
50
|
+
"PointBlank::DOM::ULListElement" => {
|
51
|
+
tag: "li"
|
52
|
+
},
|
53
|
+
"PointBlank::DOM::OLListElement" => {
|
54
|
+
tag: "li"
|
55
|
+
},
|
56
|
+
"PointBlank::DOM::Paragraph" => {
|
57
|
+
tag: "p"
|
58
|
+
},
|
59
|
+
"PointBlank::DOM::SetextHeading1" => {
|
60
|
+
tag: "h1"
|
61
|
+
},
|
62
|
+
"PointBlank::DOM::SetextHeading2" => {
|
63
|
+
tag: "h2"
|
64
|
+
},
|
65
|
+
"PointBlank::DOM::ATXHeading1" => {
|
66
|
+
tag: "h1"
|
67
|
+
},
|
68
|
+
"PointBlank::DOM::ATXHeading2" => {
|
69
|
+
tag: "h2"
|
70
|
+
},
|
71
|
+
"PointBlank::DOM::ATXHeading3" => {
|
72
|
+
tag: "h3"
|
73
|
+
},
|
74
|
+
"PointBlank::DOM::ATXHeading4" => {
|
75
|
+
tag: "h4"
|
76
|
+
},
|
77
|
+
"PointBlank::DOM::ATXHeading5" => {
|
78
|
+
tag: "h5"
|
79
|
+
},
|
80
|
+
"PointBlank::DOM::ATXHeading6" => {
|
81
|
+
tag: "h6"
|
82
|
+
},
|
83
|
+
"PointBlank::DOM::Document" => {
|
84
|
+
tag: "main"
|
85
|
+
},
|
86
|
+
"PointBlank::DOM::CodeBlock" => {
|
87
|
+
tag: "pre",
|
88
|
+
outer: {
|
89
|
+
tag: "code"
|
90
|
+
}
|
91
|
+
},
|
92
|
+
"PointBlank::DOM::QuoteBlock" => {
|
93
|
+
tag: "blockquote"
|
94
|
+
},
|
95
|
+
"PointBlank::DOM::HorizontalRule" => {
|
96
|
+
tag: "hr",
|
97
|
+
inline: true
|
98
|
+
},
|
99
|
+
"PointBlank::DOM::Text" => {
|
100
|
+
sanitize: true
|
101
|
+
},
|
102
|
+
"PointBlank::DOM::InlineAutolink" => {
|
103
|
+
tag: "a",
|
104
|
+
href: true
|
105
|
+
}
|
106
|
+
}.freeze
|
107
|
+
|
108
|
+
# Class for managing styles and style overrides
|
109
|
+
class MapManager
|
110
|
+
class << self
|
111
|
+
# Define a default mapping for specified class
|
112
|
+
# @param key [String] class name
|
113
|
+
# @param mapping [Hash] mapping
|
114
|
+
# @return [void]
|
115
|
+
def define_mapping(key, mapping)
|
116
|
+
@mapping ||= ELEMENT_MAP.dup
|
117
|
+
@mapping[key] = mapping
|
118
|
+
end
|
119
|
+
|
120
|
+
# Get computed mapping
|
121
|
+
# @return [Hash]
|
122
|
+
def mapping
|
123
|
+
@mapping ||= ELEMENT_MAP.dup
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def initialize(overrides)
|
128
|
+
@mapping = self.class.mapping
|
129
|
+
@mapping = @mapping.merge(overrides["mapping"]) if overrides["mapping"]
|
130
|
+
end
|
131
|
+
|
132
|
+
attr_reader :mapping
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# HTML Renderer
|
137
|
+
class HTML
|
138
|
+
def initialize(dom, options)
|
139
|
+
@document = dom
|
140
|
+
@options = options
|
141
|
+
@options["linewrap"] ||= 80
|
142
|
+
@options["init_level"] ||= 2
|
143
|
+
@options["indent"] ||= 2
|
144
|
+
mapmanager = HTMLConstants::MapManager.new(options)
|
145
|
+
@mapping = mapmanager.mapping
|
146
|
+
return unless @options["nowrap"]
|
147
|
+
|
148
|
+
@options["init_level"] = 0
|
149
|
+
@mapping.delete("PointBlank::DOM::Document")
|
150
|
+
end
|
151
|
+
|
152
|
+
# Render document to HTML
|
153
|
+
def render
|
154
|
+
text = _render(@document, @options, level: @options["init_level"])
|
155
|
+
@options["init_level"].times { text = indent(text) }
|
156
|
+
if @options["nowrap"]
|
157
|
+
text
|
158
|
+
else
|
159
|
+
[
|
160
|
+
preambule,
|
161
|
+
remove_pre_spaces(text),
|
162
|
+
postambule
|
163
|
+
].join("\n")
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
private
|
168
|
+
|
169
|
+
# Find and remove extra spaces inbetween preformatted text
|
170
|
+
# @param string [String]
|
171
|
+
# @return [String]
|
172
|
+
def remove_pre_spaces(string)
|
173
|
+
output = []
|
174
|
+
buffer = []
|
175
|
+
open = nil
|
176
|
+
string.lines.each do |line|
|
177
|
+
opentoken = line.match?(/<pre>/)
|
178
|
+
closetoken = line.match?(/<\/pre>/)
|
179
|
+
if closetoken
|
180
|
+
open = false
|
181
|
+
buffer = strip_leading_spaces_in_buffer(buffer)
|
182
|
+
output.append(*buffer)
|
183
|
+
buffer = []
|
184
|
+
end
|
185
|
+
(open ? buffer : output).append(line)
|
186
|
+
open = true if opentoken && !closetoken
|
187
|
+
end
|
188
|
+
output.append(*buffer) unless buffer.empty?
|
189
|
+
output.join('')
|
190
|
+
end
|
191
|
+
|
192
|
+
# Strip leading spaces in the buffer
|
193
|
+
# @param lines [Array<String>]
|
194
|
+
# @return [Array<String>]
|
195
|
+
def strip_leading_spaces_in_buffer(buffer)
|
196
|
+
minprefix = buffer.map { |x| x.match(/^ */)[0] }
|
197
|
+
.min_by(&:length)
|
198
|
+
buffer.map do |line|
|
199
|
+
line.delete_prefix(minprefix)
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
# Word wrapping algorithm
|
204
|
+
# @param text [String]
|
205
|
+
# @param width [Integer]
|
206
|
+
# @return [String]
|
207
|
+
def wordwrap(text, width)
|
208
|
+
words = text.split(/( +|<[^>]+>)/)
|
209
|
+
output = []
|
210
|
+
line = ""
|
211
|
+
length = 0
|
212
|
+
until words.empty?
|
213
|
+
word = words.shift
|
214
|
+
wordlength = word.length
|
215
|
+
if length + wordlength + 1 > width
|
216
|
+
output.append(line.lstrip)
|
217
|
+
line = word
|
218
|
+
length = wordlength
|
219
|
+
next
|
220
|
+
end
|
221
|
+
length += wordlength
|
222
|
+
line += word
|
223
|
+
end
|
224
|
+
output.append(line.lstrip)
|
225
|
+
output.join("\n")
|
226
|
+
end
|
227
|
+
|
228
|
+
def _render(element, options, inline: false, level: 0, literaltext: false)
|
229
|
+
modeswitch = element.is_a?(::PointBlank::DOM::LeafBlock) ||
|
230
|
+
element.is_a?(::PointBlank::DOM::Paragraph)
|
231
|
+
inline ||= modeswitch
|
232
|
+
level += 1 unless inline
|
233
|
+
text = if element.children.empty?
|
234
|
+
element.content
|
235
|
+
else
|
236
|
+
literal = @mapping[element.class.name]
|
237
|
+
&.fetch(:inline, false) ||
|
238
|
+
literaltext
|
239
|
+
element.children.map do |child|
|
240
|
+
_render(child, options, inline: inline,
|
241
|
+
level: level,
|
242
|
+
literaltext: literal)
|
243
|
+
end.join(inline ? '' : "\n")
|
244
|
+
end
|
245
|
+
run_filters(text, element, level: level,
|
246
|
+
inline: inline,
|
247
|
+
modeswitch: modeswitch,
|
248
|
+
literaltext: literaltext)
|
249
|
+
end
|
250
|
+
|
251
|
+
def run_filters(text, element, level:, inline:, modeswitch:,
|
252
|
+
literaltext:)
|
253
|
+
element_style = @mapping[element.class.name]
|
254
|
+
return text unless element_style
|
255
|
+
return text if literaltext
|
256
|
+
|
257
|
+
hsize = @options["linewrap"] - (level * @options["indent"])
|
258
|
+
text = wordwrap(text, hsize) if modeswitch
|
259
|
+
if element_style[:sanitize]
|
260
|
+
text = MMMD::EntityUtils.encode_entities(text)
|
261
|
+
end
|
262
|
+
if element_style[:inline]
|
263
|
+
innerclose(element, element_style, text)
|
264
|
+
else
|
265
|
+
openclose(text, element, element_style, inline)
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
def openclose(text, element, element_style, inline)
|
270
|
+
opentag, closetag = construct_tags(element_style, element)
|
271
|
+
if inline
|
272
|
+
opentag + text + closetag
|
273
|
+
else
|
274
|
+
[opentag,
|
275
|
+
indent(text),
|
276
|
+
closetag].join("\n")
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
def innerclose(element, style, text)
|
281
|
+
props = element.properties
|
282
|
+
tag = "<#{style[:tag]}"
|
283
|
+
tag += " style=#{style[:style].inspect}" if style[:style]
|
284
|
+
tag += " href=#{read_link(element)}" if style[:href]
|
285
|
+
tag += " alt=#{text.inspect}" if style[:alt]
|
286
|
+
tag += " src=#{read_link(element)}" if style[:src]
|
287
|
+
tag += " title=#{read_title(element)}" if style[:title] && props[:title]
|
288
|
+
tag += ">"
|
289
|
+
if style[:outer]
|
290
|
+
outeropen, outerclose = construct_tags(style[:outer], element)
|
291
|
+
tag = outeropen + tag + outerclose
|
292
|
+
end
|
293
|
+
tag
|
294
|
+
end
|
295
|
+
|
296
|
+
def construct_tags(style, element)
|
297
|
+
return ["", ""] unless style && style[:tag]
|
298
|
+
|
299
|
+
props = element.properties
|
300
|
+
opentag = "<#{style[:tag]}"
|
301
|
+
closetag = "</#{style[:tag]}>"
|
302
|
+
opentag += " style=#{style[:style].inspect}" if style[:style]
|
303
|
+
opentag += " href=#{read_link(element)}" if style[:href]
|
304
|
+
opentag += " src=#{read_link(element)}" if style[:src]
|
305
|
+
opentag += " title=#{read_title(element)}" if style[:title] &&
|
306
|
+
props[:title]
|
307
|
+
opentag += ">"
|
308
|
+
if style[:outer]
|
309
|
+
outeropen, outerclose = construct_tags(style[:outer], element)
|
310
|
+
opentag = outeropen + opentag
|
311
|
+
closetag += outerclose
|
312
|
+
end
|
313
|
+
[opentag, closetag]
|
314
|
+
end
|
315
|
+
|
316
|
+
def read_title(element)
|
317
|
+
title = element.properties[:title]
|
318
|
+
title = ::MMMD::EntityUtils.encode_entities(title)
|
319
|
+
title.inspect
|
320
|
+
end
|
321
|
+
|
322
|
+
def read_link(element)
|
323
|
+
link = element.properties[:uri]
|
324
|
+
link.inspect
|
325
|
+
end
|
326
|
+
|
327
|
+
def indent(text)
|
328
|
+
text.lines.map do |line|
|
329
|
+
"#{' ' * @options["indent"]}#{line}"
|
330
|
+
end.join('')
|
331
|
+
end
|
332
|
+
|
333
|
+
def preambule
|
334
|
+
head = @options['head']
|
335
|
+
headinfo = "#{indent(<<~HEAD.rstrip)}\n " if head
|
336
|
+
<head>
|
337
|
+
#{head.is_a?(Array) ? head.join("\n") : head}
|
338
|
+
</head>
|
339
|
+
HEAD
|
340
|
+
headinfo ||= " "
|
341
|
+
@options['preambule'] or <<~TEXT.rstrip
|
342
|
+
<!DOCTYPE HTML>
|
343
|
+
<html>
|
344
|
+
#{headinfo}<body>
|
345
|
+
TEXT
|
346
|
+
end
|
347
|
+
|
348
|
+
def postambule
|
349
|
+
@options['postambule'] or <<~TEXT
|
350
|
+
</body>
|
351
|
+
</html>
|
352
|
+
TEXT
|
353
|
+
end
|
354
|
+
end
|
355
|
+
end
|
356
|
+
end
|