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,452 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Attempt to source a provider for the wide char width calculator
|
|
4
|
+
# (TODO)
|
|
5
|
+
|
|
6
|
+
module MMMD
|
|
7
|
+
# Module for managing terminal output
|
|
8
|
+
module TextManager
|
|
9
|
+
# ANSI SGR escape code for bg color
|
|
10
|
+
# @param text [String]
|
|
11
|
+
# @param options [Hash]
|
|
12
|
+
# @return [String]
|
|
13
|
+
def bg(text, options)
|
|
14
|
+
color = options['bg']
|
|
15
|
+
if color.is_a? Integer
|
|
16
|
+
"\e[48;5;#{color}m#{text}\e[49m"
|
|
17
|
+
elsif color.is_a? String and color.match?(/\A#[A-Fa-f0-9]{6}\Z/)
|
|
18
|
+
vector = color.scan(/[A-Fa-f0-9]{2}/).map { |x| x.to_i(16) }
|
|
19
|
+
"\e[48;2;#{vector[0]};#{vector[1]};#{vector[2]}\e[49m"
|
|
20
|
+
else
|
|
21
|
+
Kernel.warn "WARNING: Invalid color - #{color}"
|
|
22
|
+
text
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# ANSI SGR escape code for fg color
|
|
27
|
+
# @param text [String]
|
|
28
|
+
# @param options [Hash]
|
|
29
|
+
# @return [String]
|
|
30
|
+
def fg(text, options)
|
|
31
|
+
color = options['fg']
|
|
32
|
+
if color.is_a? Integer
|
|
33
|
+
"\e[38;5;#{color}m#{text}\e[39m"
|
|
34
|
+
elsif color.is_a? String and color.match?(/\A#[A-Fa-f0-9]{6}\Z/)
|
|
35
|
+
vector = color.scan(/[A-Fa-f0-9]{2}/).map { |x| x.to_i(16) }
|
|
36
|
+
"\e[38;2;#{vector[0]};#{vector[1]};#{vector[2]}\e[39m"
|
|
37
|
+
else
|
|
38
|
+
Kernel.warn "WARNING: Invalid color - #{color}"
|
|
39
|
+
text
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# ANSI SGR escape code for bold text
|
|
44
|
+
# @param text [String]
|
|
45
|
+
# @param options [Hash]
|
|
46
|
+
# @return [String]
|
|
47
|
+
def bold(text, _options)
|
|
48
|
+
"\e[1m#{text}\e[22m"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# ANSI SGR escape code for italics text
|
|
52
|
+
# @param text [String]
|
|
53
|
+
# @param options [Hash]
|
|
54
|
+
# @return [String]
|
|
55
|
+
def italics(text, _options)
|
|
56
|
+
"\e[3m#{text}\e[23m"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# ANSI SGR escape code for underline text
|
|
60
|
+
# @param text [String]
|
|
61
|
+
# @param options [Hash]
|
|
62
|
+
# @return [String]
|
|
63
|
+
def underline(text, _options)
|
|
64
|
+
"\e[4m#{text}\e[24m"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# ANSI SGR escape code for strikethrough text
|
|
68
|
+
# @param text [String]
|
|
69
|
+
# @param options [Hash]
|
|
70
|
+
# @return [String]
|
|
71
|
+
def strikethrough(text, _options)
|
|
72
|
+
"\e[9m#{text}\e[29m"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Word wrapping algorithm
|
|
76
|
+
# @param text [String]
|
|
77
|
+
# @param width [Integer]
|
|
78
|
+
# @return [String]
|
|
79
|
+
def wordwrap(text, width)
|
|
80
|
+
words = text.split(/( +)/)
|
|
81
|
+
output = []
|
|
82
|
+
line = ""
|
|
83
|
+
length = 0
|
|
84
|
+
until words.empty?
|
|
85
|
+
word = words.shift
|
|
86
|
+
wordlength = smort_length(word)
|
|
87
|
+
if wordlength > width
|
|
88
|
+
words.prepend(word[width..])
|
|
89
|
+
word = word[..width - 1]
|
|
90
|
+
end
|
|
91
|
+
if length + wordlength + 1 > width
|
|
92
|
+
output.append(line.lstrip)
|
|
93
|
+
line = word
|
|
94
|
+
length = wordlength
|
|
95
|
+
next
|
|
96
|
+
end
|
|
97
|
+
length += wordlength
|
|
98
|
+
line += word
|
|
99
|
+
end
|
|
100
|
+
output.append(line.lstrip)
|
|
101
|
+
output.join("\n")
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# (TODO: smorter stronger better faster)
|
|
105
|
+
# SmЯt™ word length
|
|
106
|
+
# @param text [String]
|
|
107
|
+
# @return [Integer]
|
|
108
|
+
def smort_length(text)
|
|
109
|
+
text.gsub(/\e\[[^m]+m/, '').length
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Left-justify a line while ignoring terminal control codes
|
|
113
|
+
# @param text [String]
|
|
114
|
+
# @param size [Integer]
|
|
115
|
+
# @return [String]
|
|
116
|
+
def ljust_cc(text, size)
|
|
117
|
+
text.lines.map do |line|
|
|
118
|
+
textlength = smort_length(line)
|
|
119
|
+
textlength < size ? line + " " * (size - textlength) : line
|
|
120
|
+
end.join("\n")
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Right-justify a line while ignoring terminal control codes
|
|
124
|
+
# @param text [String]
|
|
125
|
+
# @param size [Integer]
|
|
126
|
+
# @return [String]
|
|
127
|
+
def rjust_cc(text, size)
|
|
128
|
+
text.lines.map do |line|
|
|
129
|
+
textlength = smort_length(line)
|
|
130
|
+
textlength < size ? " " * (size - textlength) + line : line
|
|
131
|
+
end.join("\n")
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Center-justify a line while ignoring terminal control codes
|
|
135
|
+
# @param text [String]
|
|
136
|
+
# @param size [Integer]
|
|
137
|
+
# @return [String]
|
|
138
|
+
def center_cc(text, size)
|
|
139
|
+
text.lines.map do |line|
|
|
140
|
+
textlength = smort_length(line)
|
|
141
|
+
if textlength < size
|
|
142
|
+
freelength = size - textlength
|
|
143
|
+
rightlength = freelength / 2
|
|
144
|
+
leftlength = freelength - rightlength
|
|
145
|
+
" " * leftlength + line + " " * rightlength
|
|
146
|
+
else
|
|
147
|
+
line
|
|
148
|
+
end
|
|
149
|
+
end.join("\n")
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Draw a screen-width box around text
|
|
153
|
+
# @param text [String]
|
|
154
|
+
# @param options [Hash]
|
|
155
|
+
# @return [String]
|
|
156
|
+
def box(text, options)
|
|
157
|
+
size = options[:hsize] - 2
|
|
158
|
+
text = wordwrap(text, (size * 0.8).floor).lines.filter_map do |line|
|
|
159
|
+
"│#{ljust_cc(line, size)}│" unless line.empty?
|
|
160
|
+
end.join("\n")
|
|
161
|
+
<<~TEXT
|
|
162
|
+
╭#{'─' * size}╮
|
|
163
|
+
#{text}
|
|
164
|
+
╰#{'─' * size}╯
|
|
165
|
+
TEXT
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Draw text right-justified
|
|
169
|
+
def rjust(text, options)
|
|
170
|
+
size = options[:hsize]
|
|
171
|
+
wordwrap(text, (size * 0.8).floor).lines.filter_map do |line|
|
|
172
|
+
rjust_cc(line, size) unless line.empty?
|
|
173
|
+
end.join("\n")
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Draw text centered
|
|
177
|
+
def center(text, options)
|
|
178
|
+
size = options[:hsize]
|
|
179
|
+
wordwrap(text, (size * 0.8).floor).lines.filter_map do |line|
|
|
180
|
+
center_cc(line, size) unless line.empty?
|
|
181
|
+
end.join("\n")
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Underline the last line of the text piece
|
|
185
|
+
def underline_block(text, options)
|
|
186
|
+
textlines = text.lines
|
|
187
|
+
last = "".match(/()()()/)
|
|
188
|
+
textlines.each do |x|
|
|
189
|
+
current = x.match(/\A(\s*)(.+?)(\s*)\Z/)
|
|
190
|
+
last = current if smort_length(current[2]) > smort_length(last[2])
|
|
191
|
+
end
|
|
192
|
+
ltxt = last[1]
|
|
193
|
+
ctxt = textlines.last.slice(last.offset(2)[0]..last.offset(2)[1] - 1)
|
|
194
|
+
rtxt = last[3]
|
|
195
|
+
textlines[-1] = [ltxt, underline(ctxt, options), rtxt].join('')
|
|
196
|
+
textlines.join("")
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Add extra newlines around the text
|
|
200
|
+
def extra_newlines(text, options)
|
|
201
|
+
size = options[:hsize]
|
|
202
|
+
textlines = text.lines
|
|
203
|
+
textlines.prepend("#{' ' * size}\n")
|
|
204
|
+
textlines.append("\n#{' ' * size}\n")
|
|
205
|
+
textlines.join("")
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Underline last line edge to edge
|
|
209
|
+
def underline_full_block(text, options)
|
|
210
|
+
textlines = text.lines
|
|
211
|
+
last_line = textlines.last.match(/^.*$/)[0]
|
|
212
|
+
textlines[-1] = "#{underline(last_line, options)}\n"
|
|
213
|
+
textlines.join("")
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Indent all lines
|
|
217
|
+
def indent(text, _options)
|
|
218
|
+
_indent(text)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Indent all lines (inner)
|
|
222
|
+
def _indent(text)
|
|
223
|
+
text.lines.map do |line|
|
|
224
|
+
" #{line}"
|
|
225
|
+
end.join("")
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Left overline all lines
|
|
229
|
+
def leftline(text, _options)
|
|
230
|
+
text.lines.map do |line|
|
|
231
|
+
" │ #{line}"
|
|
232
|
+
end.join("")
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Bulletpoints
|
|
236
|
+
def bullet(text, _options)
|
|
237
|
+
"-#{_indent(text)[1..]}"
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Numbers
|
|
241
|
+
def numbered(text, options)
|
|
242
|
+
number = options[:number]
|
|
243
|
+
length = number.to_s.length + 1
|
|
244
|
+
(length / 4 + 1).times { text = _indent(text) }
|
|
245
|
+
"#{number}.#{text[length..]}"
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
module Renderers
|
|
250
|
+
module PlaintermConstants
|
|
251
|
+
DEFAULT_STYLE = {
|
|
252
|
+
"PointBlank::DOM::Paragraph" => {
|
|
253
|
+
indent: true,
|
|
254
|
+
increase_level: true
|
|
255
|
+
},
|
|
256
|
+
"PointBlank::DOM::Text" => {},
|
|
257
|
+
"PointBlank::DOM::SetextHeading1" => {
|
|
258
|
+
center: true,
|
|
259
|
+
bold: true,
|
|
260
|
+
extra_newlines: true,
|
|
261
|
+
underline_full_block: true
|
|
262
|
+
},
|
|
263
|
+
"PointBlank::DOM::SetextHeading2" => {
|
|
264
|
+
center: true,
|
|
265
|
+
underline_block: true
|
|
266
|
+
},
|
|
267
|
+
"PointBlank::DOM::ATXHeading1" => {
|
|
268
|
+
center: true,
|
|
269
|
+
bold: true,
|
|
270
|
+
extra_newlines: true,
|
|
271
|
+
underline_full_block: true
|
|
272
|
+
},
|
|
273
|
+
"PointBlank::DOM::ATXHeading2" => {
|
|
274
|
+
center: true,
|
|
275
|
+
underline_block: true
|
|
276
|
+
},
|
|
277
|
+
"PointBlank::DOM::ATXHeading3" => {
|
|
278
|
+
underline: true,
|
|
279
|
+
bold: true
|
|
280
|
+
},
|
|
281
|
+
"PointBlank::DOM::ATXHeading4" => {
|
|
282
|
+
bold: true,
|
|
283
|
+
underline: true
|
|
284
|
+
},
|
|
285
|
+
"PointBlank::DOM::ATXHeading5" => {
|
|
286
|
+
underline: true
|
|
287
|
+
},
|
|
288
|
+
"PointBlank::DOM::ATXHeading6" => {
|
|
289
|
+
underline: true
|
|
290
|
+
},
|
|
291
|
+
"PointBlank::DOM::InlineImage" => {
|
|
292
|
+
underline: true
|
|
293
|
+
},
|
|
294
|
+
"PointBlank::DOM::InlineLink" => {
|
|
295
|
+
underline: true
|
|
296
|
+
},
|
|
297
|
+
"PointBlank::DOM::InlinePre" => {},
|
|
298
|
+
"PointBlank::DOM::InlineEmphasis" => {
|
|
299
|
+
italics: true
|
|
300
|
+
},
|
|
301
|
+
"PointBlank::DOM::InlineStrong" => {
|
|
302
|
+
bold: true
|
|
303
|
+
},
|
|
304
|
+
"PointBlank::DOM::ULListElement" => {
|
|
305
|
+
bullet: true,
|
|
306
|
+
increase_level: true
|
|
307
|
+
},
|
|
308
|
+
"PointBlank::DOM::OLListElement" => {
|
|
309
|
+
numbered: true,
|
|
310
|
+
increase_level: true
|
|
311
|
+
},
|
|
312
|
+
"PointBlank::DOM::QuoteBlock" => {
|
|
313
|
+
leftline: true,
|
|
314
|
+
increase_level: true
|
|
315
|
+
},
|
|
316
|
+
"PointBlank::DOM::HorizontalRule" => {
|
|
317
|
+
underline_full_block: true
|
|
318
|
+
}
|
|
319
|
+
}.freeze
|
|
320
|
+
|
|
321
|
+
DEFAULT_EFFECT_PRIORITY = {
|
|
322
|
+
numbered: 10_000,
|
|
323
|
+
leftline: 9500,
|
|
324
|
+
bullet: 9000,
|
|
325
|
+
indent: 8500,
|
|
326
|
+
underline_full_block: 8000,
|
|
327
|
+
underline_block: 7500,
|
|
328
|
+
extra_newlines: 7000,
|
|
329
|
+
center: 6000,
|
|
330
|
+
rjust: 5500,
|
|
331
|
+
box: 5000,
|
|
332
|
+
underline: 4000,
|
|
333
|
+
italics: 3500,
|
|
334
|
+
bold: 3000,
|
|
335
|
+
fg: 2500,
|
|
336
|
+
bg: 2000,
|
|
337
|
+
strikethrough: 1500
|
|
338
|
+
}.freeze
|
|
339
|
+
|
|
340
|
+
# Class for managing styles and style overrides
|
|
341
|
+
class StyleManager
|
|
342
|
+
class << self
|
|
343
|
+
# Define a default style for specified class
|
|
344
|
+
# @param key [String] class name
|
|
345
|
+
# @param style [Hash] style
|
|
346
|
+
# @return [void]
|
|
347
|
+
def define_style(key, style)
|
|
348
|
+
@style ||= DEFAULT_STYLE.dup
|
|
349
|
+
@style[key] = style
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
# Define an effect priority value
|
|
353
|
+
# @param key [String] effect name
|
|
354
|
+
# @param priority [Integer] value of the priority
|
|
355
|
+
# @return [void]
|
|
356
|
+
def define_effect_priority(key, priority)
|
|
357
|
+
@effect_priority ||= DEFAULT_EFFECT_PRIORITY.dup
|
|
358
|
+
@effect_priority[key] = priority
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# Get computed style
|
|
362
|
+
# @return [Hash]
|
|
363
|
+
def style
|
|
364
|
+
@style ||= DEFAULT_STYLE.dup
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
# Get computed effect priority
|
|
368
|
+
# @return [Hash]
|
|
369
|
+
def effect_priority
|
|
370
|
+
@effect_priority ||= DEFAULT_EFFECT_PRIORITY.dup
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def initialize(overrides)
|
|
375
|
+
@style = self.class.style
|
|
376
|
+
@effect_priority = self.class.effect_priority
|
|
377
|
+
@style = @style.merge(overrides["style"]) if overrides["style"]
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
attr_reader :style, :effect_priority
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
# Primary document renderer
|
|
385
|
+
class Plainterm
|
|
386
|
+
include ::MMMD::TextManager
|
|
387
|
+
|
|
388
|
+
# @param input [String]
|
|
389
|
+
# @param options [Hash]
|
|
390
|
+
def initialize(input, options)
|
|
391
|
+
@doc = input
|
|
392
|
+
@color_mode = options.fetch("color", true)
|
|
393
|
+
@ansi_mode = options.fetch("ansi", true)
|
|
394
|
+
style_manager = PlaintermConstants::StyleManager.new(options)
|
|
395
|
+
@style = style_manager.style
|
|
396
|
+
@effect_priority = style_manager.effect_priority
|
|
397
|
+
@effects = @effect_priority.to_a.sort_by(&:last).map(&:first)
|
|
398
|
+
@options = options
|
|
399
|
+
@options["hsize"] ||= 80
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
# Return rendered text
|
|
403
|
+
# @return [String]
|
|
404
|
+
def render
|
|
405
|
+
_render(@doc, @options)
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
private
|
|
409
|
+
|
|
410
|
+
def _render(element, options, inline: false, level: 0, index: 0)
|
|
411
|
+
modeswitch = element.is_a?(::PointBlank::DOM::LeafBlock) ||
|
|
412
|
+
element.is_a?(::PointBlank::DOM::Paragraph)
|
|
413
|
+
inline ||= modeswitch
|
|
414
|
+
level += calculate_level_increase(element)
|
|
415
|
+
text = if element.children.empty?
|
|
416
|
+
element.content
|
|
417
|
+
else
|
|
418
|
+
element.children.map.with_index do |child, index|
|
|
419
|
+
_render(child, options, inline: inline,
|
|
420
|
+
level: level,
|
|
421
|
+
index: index)
|
|
422
|
+
end.join(inline ? '' : "\n\n")
|
|
423
|
+
end
|
|
424
|
+
run_filters(text, element, level: level,
|
|
425
|
+
modeswitch: modeswitch,
|
|
426
|
+
index: index)
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
def run_filters(text, element, level:, modeswitch:, index:)
|
|
430
|
+
element_style = @style[element.class.name]
|
|
431
|
+
return text unless element_style
|
|
432
|
+
|
|
433
|
+
hsize = @options["hsize"] - (4 * level)
|
|
434
|
+
text = wordwrap(text, hsize) if modeswitch
|
|
435
|
+
params = element_style.dup
|
|
436
|
+
params[:hsize] = hsize
|
|
437
|
+
params[:number] = index + 1
|
|
438
|
+
@effects.each do |effect|
|
|
439
|
+
text = method(effect).call(text, params) if element_style[effect]
|
|
440
|
+
end
|
|
441
|
+
text
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
def calculate_level_increase(element)
|
|
445
|
+
level = 0
|
|
446
|
+
element_style = @style[element.class.name]
|
|
447
|
+
level += 1 if element_style && element_style[:increase_level]
|
|
448
|
+
level
|
|
449
|
+
end
|
|
450
|
+
end
|
|
451
|
+
end
|
|
452
|
+
end
|
data/lib/mmmd/util.rb
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module MMMD
|
|
6
|
+
# Utils for working with entities in strings
|
|
7
|
+
module EntityUtils
|
|
8
|
+
ENTITY_DATA = JSON.parse(File.read("#{__dir__}/entities.json"))
|
|
9
|
+
|
|
10
|
+
# Decode html entities in string
|
|
11
|
+
# @param string [String]
|
|
12
|
+
# @return [String]
|
|
13
|
+
def self.decode_entities(string)
|
|
14
|
+
string = string.gsub(/&#\d{1,7};/) do |match|
|
|
15
|
+
match[1..-2].to_i.chr("UTF-8")
|
|
16
|
+
end
|
|
17
|
+
string = string.gsub(/&#[xX][\dA-Fa-f]{1,6};/) do |match|
|
|
18
|
+
match[3..-2].to_i(16).chr("UTF-8")
|
|
19
|
+
end
|
|
20
|
+
string.gsub(/&\w+;/) do |match|
|
|
21
|
+
ENTITY_DATA[match] ? ENTITY_DATA[match]["characters"] : match
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Encode unsafe html entities in string (ASCII-compatible)
|
|
26
|
+
# @param string [String]
|
|
27
|
+
# @return [String]
|
|
28
|
+
# @sg-ignore
|
|
29
|
+
def self.encode_entities_ascii(string)
|
|
30
|
+
string.gsub("&", "&")
|
|
31
|
+
.gsub("<", "<")
|
|
32
|
+
.gsub(">", ">")
|
|
33
|
+
.gsub('"', """)
|
|
34
|
+
.gsub("'", "'")
|
|
35
|
+
.gsub(/[^\x00-\x7F]/) do |match|
|
|
36
|
+
"&#x#{match.codepoints[0]};"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Encode unsafe html entities in string
|
|
41
|
+
# @param string [String]
|
|
42
|
+
# @return [String]
|
|
43
|
+
# @sg-ignore
|
|
44
|
+
def self.encode_entities(string)
|
|
45
|
+
string.gsub("&", "&")
|
|
46
|
+
.gsub("<", "<")
|
|
47
|
+
.gsub(">", ">")
|
|
48
|
+
.gsub('"', """)
|
|
49
|
+
.gsub("'", "'")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Encode uri components that may break HTML syntax
|
|
53
|
+
# @param string [String]
|
|
54
|
+
# @return [String]
|
|
55
|
+
def self.encode_uri(string)
|
|
56
|
+
string.gsub('"', "%22")
|
|
57
|
+
.gsub("'", "%27")
|
|
58
|
+
.gsub(" ", "%20")
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
data/lib/mmmd.rb
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'mmmd/blankshell'
|
|
4
|
+
require_relative 'mmmd/renderers'
|
|
5
|
+
|
|
6
|
+
# Extensible, multi-format markdown processor
|
|
7
|
+
module MMMD
|
|
8
|
+
# Parse a Markdown document into a DOM form
|
|
9
|
+
# @param doc [String]
|
|
10
|
+
# @return [::PointBlank::DOM::Document]
|
|
11
|
+
def self.parse(doc)
|
|
12
|
+
::PointBlank::DOM::Document.parse(doc)
|
|
13
|
+
end
|
|
14
|
+
end
|
data/security.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Security acknowledgements
|
|
2
|
+
=========================
|
|
3
|
+
|
|
4
|
+
While special care has been taken to prevent some of the more common common
|
|
5
|
+
vulnerabilities that might arise from using this parser, it does not prevent
|
|
6
|
+
certain issues which **which should be acknowledged**.
|
|
7
|
+
|
|
8
|
+
- It is possible to inject a form of one-click XSS into the website. In
|
|
9
|
+
particular, there are no restrictions placed on urls embedded within the links
|
|
10
|
+
(as per the description of CommonMark specification). As such, something as
|
|
11
|
+
simple as `[test](<javascript:dangerous code here>)` would be more than enough
|
|
12
|
+
to employ such an exploit.
|
|
13
|
+
- While generally speaking the parser acts stable on most tests, and precents
|
|
14
|
+
stray HTML tokens from occuring in the output text where appropriate, due to
|
|
15
|
+
the nontrivial nature of the task some form of XSS injection may or may not
|
|
16
|
+
occur. If such an incident occurs, please report it to the current maintainer
|
|
17
|
+
of the project.
|
|
18
|
+
- User input should NOT be trusted when it comes to applying options to
|
|
19
|
+
rendering. Some renderers, such as the HTML renderer, allow modifying the
|
|
20
|
+
style parameter for rendered tags, which when passed control of to an
|
|
21
|
+
untrusted party may become an XSS attack vector.
|
metadata
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: mmmd
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Yessiest
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir:
|
|
10
|
+
- bin
|
|
11
|
+
cert_chain: []
|
|
12
|
+
date: 2025-03-08 00:00:00.000000000 Z
|
|
13
|
+
dependencies: []
|
|
14
|
+
description: |
|
|
15
|
+
MMMD (short for Mark My Manuscript Down) is a Markdown processor
|
|
16
|
+
(as in "parser and translator") with a CLI interface utility and
|
|
17
|
+
multiple modes of output (currently HTML and terminal).
|
|
18
|
+
email: yessiest@text.512mb.org
|
|
19
|
+
executables:
|
|
20
|
+
- mmmdpp
|
|
21
|
+
extensions: []
|
|
22
|
+
extra_rdoc_files:
|
|
23
|
+
- README.md
|
|
24
|
+
- architecture.md
|
|
25
|
+
- security.md
|
|
26
|
+
files:
|
|
27
|
+
- README.md
|
|
28
|
+
- architecture.md
|
|
29
|
+
- bin/mmmdpp
|
|
30
|
+
- lib/mmmd.rb
|
|
31
|
+
- lib/mmmd/blankshell.rb
|
|
32
|
+
- lib/mmmd/entities.json
|
|
33
|
+
- lib/mmmd/renderers.rb
|
|
34
|
+
- lib/mmmd/renderers/html.rb
|
|
35
|
+
- lib/mmmd/renderers/plainterm.rb
|
|
36
|
+
- lib/mmmd/util.rb
|
|
37
|
+
- security.md
|
|
38
|
+
homepage: https://adastra7.net/git/Yessiest/rubymark
|
|
39
|
+
licenses:
|
|
40
|
+
- AGPL-3.0-or-later
|
|
41
|
+
metadata: {}
|
|
42
|
+
post_install_message:
|
|
43
|
+
rdoc_options: []
|
|
44
|
+
require_paths:
|
|
45
|
+
- lib
|
|
46
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
47
|
+
requirements:
|
|
48
|
+
- - ">="
|
|
49
|
+
- !ruby/object:Gem::Version
|
|
50
|
+
version: 3.0.0
|
|
51
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
52
|
+
requirements:
|
|
53
|
+
- - ">="
|
|
54
|
+
- !ruby/object:Gem::Version
|
|
55
|
+
version: '0'
|
|
56
|
+
requirements: []
|
|
57
|
+
rubygems_version: 3.5.22
|
|
58
|
+
signing_key:
|
|
59
|
+
specification_version: 4
|
|
60
|
+
summary: Modular, compliant Markdown processor
|
|
61
|
+
test_files: []
|