markita 1.0.210828 → 3.0.210912

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/markita/base.rb CHANGED
@@ -18,100 +18,10 @@ class Base < Sinatra::Base
18
18
  end
19
19
  end
20
20
 
21
- def Base.header(key)
22
- <<-HEADER
23
- <!DOCTYPE html>
24
- <html>
25
- <head>
26
- <title>#{key}</title>
27
- #{HEADER_LINKS}</head>
28
- <body>
29
-
30
- HEADER
31
- end
32
-
33
- def Base.footer
34
- <<-FOOTER
35
-
36
- </body>
37
- </html>
38
- FOOTER
39
- end
40
-
41
- def Base.pre_process(text)
42
- val,string,_ = {},'',nil
43
- text.each_line do |line|
44
- line.chomp!
45
- case line
46
- when ''
47
- val.clear
48
- when val[:regx]
49
- # Template/Substitutions
50
- line=_ if _=val[:template]
51
- $~.named_captures.each do |name, value|
52
- line = line.gsub("&#{name};", value)
53
- line = line.gsub("&#{name.upcase};", CGI.escape(value))
54
- end
55
- when %r(^<!-- (.*) -->$)
56
- directive = $1
57
- case directive
58
- when %r(^(\w+): "(.*)"$)
59
- val[$1.to_sym] = $2
60
- when %r(^(\w+): /(.*)/)
61
- val[$1.to_sym] = Regexp.new $2
62
- else
63
- $stderr.puts "Unrecognized directive: "+directive
64
- end
65
- next
66
- end
67
- string << line << "\n"
68
- end
69
- return string
70
- end
71
-
72
- def Base.post_process(text)
73
- string,_ = '',nil
74
- text.each_line do |line|
75
- line.chomp!
76
- case line
77
- when %r(^(\s*)<li>\[(x| )\] (.*)</li>$)
78
- # Task Lists
79
- s,x,item = $1,$2,$3
80
- li = (x=='x')?
81
- %q{<li style="list-style-type: '&#9745; '">} :
82
- %q{<li style="list-style-type: '&#9744; '">}
83
- line = s+li+item+"</li>"
84
- when %r(^<p>(\w+:\[\*?\w+\] )+\((\S+)\)</p>$)
85
- # One Line Forms
86
- action,method,form = $2,'get',''
87
- line.scan(/(\w+):\[(\*)?(\w+)\] /).each do |field, pwd, name|
88
- type = (pwd)? 'password' : 'text'
89
- method = 'post' if pwd
90
- form << %Q{ #{field}:<input type="#{type}" name="#{name}">\n}
91
- end
92
- line = %Q(<form action="#{action}" method="#{method}">\n) +
93
- form + %Q( <input type="submit">\n</form>)
94
- when %r(^<p><img (src="[^"]*" alt=" [^"]* ") /></p>$)
95
- line = %Q(<img style="display: block; margin-left: auto; margin-right: auto;" #{$1} />)
96
- when %r(^<p><img (src="[^"]*" alt=" [^"]*") />$)
97
- line = %Q(<p><img style="float: left;" #{$1} />)
98
- when %r(^<p><img (src="[^"]*" alt="[^"]* ") />$)
99
- line = %Q(<p><img style="float: right;" #{$1} />)
100
- end
101
- string << line << "\n"
102
- end
103
- return string
104
- end
105
-
106
- def Base.page(key)
107
- Base.header(key) + yield + Base.footer
108
- end
109
-
110
21
  get PAGE_KEY do |key|
111
22
  filepath = File.join ROOT, key+'.md'
112
23
  raise Sinatra::NotFound unless File.exist? filepath
113
- text = File.read(filepath).force_encoding('utf-8')
114
- Base.page(key){ Base.post_process markdown Base.pre_process text}
24
+ Markdown.new(key).filepath filepath
115
25
  end
116
26
 
117
27
  get IMAGE_PATH do |path, *_|
@@ -11,6 +11,8 @@ module Markita
11
11
  end
12
12
  NOT_FOUND = File.read PATH['not_found.html']
13
13
 
14
+ EMOJIS = Hash[*File.read(PATH['emojis.tsv']).split(/\s+/)]
15
+
14
16
  PAGE_KEY = %r{/(\w[\w\/\-]*\w)}
15
17
  IMAGE_PATH = %r{/(\w[\w\/\-]*\w\.((png)|(gif)))}
16
18
 
@@ -0,0 +1,21 @@
1
+ module Markita
2
+ module HTML
3
+ def HTML.header(key)
4
+ <<~HEADER
5
+ <!DOCTYPE html>
6
+ <html>
7
+ <head>
8
+ <title>#{key}</title>
9
+ #{HEADER_LINKS}</head>
10
+ <body>
11
+ HEADER
12
+ end
13
+
14
+ def HTML.footer
15
+ <<~FOOTER
16
+ </body>
17
+ </html>
18
+ FOOTER
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,435 @@
1
+ module Markita
2
+ class Markdown
3
+ ROUGE = Rouge::Formatters::HTML.new
4
+ PARSERS = []
5
+
6
+ def initialize(title)
7
+ @title = title
8
+ @line=@html=@file=@opt=nil
9
+ end
10
+
11
+ def start
12
+ @line,@html,@opt = HTML.header(@title),'',{}
13
+ end
14
+
15
+ def finish
16
+ @html << HTML.footer
17
+ end
18
+
19
+ def default
20
+ @html << @line
21
+ @line = @file.gets
22
+ end
23
+
24
+ def parse(fh)
25
+ @file = Preprocess.new(fh)
26
+ start
27
+ while @line
28
+ PARSERS.detect{method(_1).call} or default
29
+ end
30
+ finish
31
+ end
32
+
33
+ def markdown(string)
34
+ parse StringIO.new string
35
+ @html
36
+ end
37
+
38
+ def filepath(filepath)
39
+ File.open(filepath, 'r'){|fh| parse fh}
40
+ @html
41
+ end
42
+
43
+ Ux = /_([^_]+)_/
44
+ U = lambda {|m| "<u>#{m[1]}</u>"}
45
+
46
+ Sx = /~([^~]+)~/
47
+ S = lambda {|m| "<s>#{m[1]}</s>"}
48
+
49
+ Ix = /"([^"]+)"/
50
+ I = lambda {|m| "<i>#{m[1]}</i>"}
51
+
52
+ Bx = /\*([^\*]+)\*/
53
+ B = lambda {|m| "<b>#{m[1]}</b>"}
54
+
55
+ CODEx = /`([^`]+)`/
56
+ CODE = lambda {|m| "<code>#{m[1]}</code>"}
57
+
58
+ Ax = /\[([^\[\]]+)\]\(([^()]+)\)/
59
+ A = lambda {|m| %Q(<a href="#{m[2]}">#{m[1]}</a>)}
60
+
61
+ URLx = %r(\[(https?://[\w\.\-\/\&\+\?\%]+)\])
62
+ URL = lambda {|m| %Q(<a href="#{m[1]}">#{m[1]}</a>)}
63
+
64
+ EMOJIx = /:(\w+):/
65
+ EMOJI = lambda {|m| (_=EMOJIS[m[1]])? "&\#x#{_};" : m[0]}
66
+
67
+ FOOTNOTEx = /\[\^(\d+)\](:)?/
68
+ FOOTNOTE = lambda do |m|
69
+ if m[2]
70
+ %Q(<a id="fn:#{m[1]}" href="\#fnref:#{m[1]}">#{m[1]}:</a>)
71
+ else
72
+ %Q(<a id="fnref:#{m[1]}" href="\#fn:#{m[1]}"><sup>#{m[1]}</sup></a>)
73
+ end
74
+ end
75
+
76
+ def Markdown.tag(entry, regx, m2string, &block)
77
+ if m = regx.match(entry)
78
+ pre_match = (block ? block.call(m.pre_match) : m.pre_match)
79
+ string = pre_match + m2string[m]
80
+ post_match = m.post_match
81
+ while m = regx.match(post_match)
82
+ pre_match = (block ? block.call(m.pre_match) : m.pre_match)
83
+ string << pre_match + m2string[m]
84
+ post_match = m.post_match
85
+ end
86
+ string << (block ? block.call(post_match) : post_match)
87
+ return string
88
+ end
89
+ return (block ? block.call(entry) : entry)
90
+ end
91
+
92
+ INLINE = lambda do |entry|
93
+ string = Markdown.tag(entry, CODEx, CODE) do |entry|
94
+ Markdown.tag(entry, Ax, A) do |entry|
95
+ Markdown.tag(entry, URLx, URL) do |entry|
96
+ entry = Markdown.tag(entry, EMOJIx, EMOJI)
97
+ entry = Markdown.tag(entry, Bx, B)
98
+ entry = Markdown.tag(entry, Ix, I)
99
+ entry = Markdown.tag(entry, Sx, S)
100
+ entry = Markdown.tag(entry, Ux, U)
101
+ entry = Markdown.tag(entry, FOOTNOTEx, FOOTNOTE)
102
+ end
103
+ end
104
+ end
105
+ string.sub(/ $/,'<br>')
106
+ end
107
+
108
+ # Empty
109
+ EMPTY = /^$/
110
+ PARSERS << :empty
111
+ def empty
112
+ EMPTY.match?(@line) or return false
113
+ @line = @file.gets
114
+ true
115
+ end
116
+
117
+ # Ordered list
118
+ ORDERED = /^\d+. (.*)$/
119
+ PARSERS << :ordered
120
+ def ordered
121
+ md = ORDERED.match(@line) or return false
122
+ @html << "<ol#{@opt[:attributes]}>\n"
123
+ @opt.delete(:attributes)
124
+ while md
125
+ @html << " <li>#{INLINE[md[1]]}</li>\n"
126
+ md = (@line=@file.gets)&.match ORDERED
127
+ end
128
+ @html << "</ol>\n"
129
+ true
130
+ end
131
+
132
+ # Paragraph
133
+ PARAGRAPHS = /^[\[`*"~_]?\w/
134
+ PARSERS << :paragraphs
135
+ def paragraphs
136
+ md = PARAGRAPHS.match(@line) or return false
137
+ @html << "<p#{@opt[:attributes]}>\n"
138
+ @opt.delete(:attributes)
139
+ while md
140
+ @html << INLINE[@line]
141
+ md = (@line=@file.gets)&.match PARAGRAPHS
142
+ end
143
+ @html << "</p>\n"
144
+ true
145
+ end
146
+
147
+ # Unordered list
148
+ UNORDERED = /^[*] (.*)$/
149
+ PARSERS << :unordered
150
+ def unordered
151
+ md = UNORDERED.match(@line) or return false
152
+ @html << "<ul#{@opt[:attributes]}>\n"
153
+ @opt.delete(:attributes)
154
+ while md
155
+ @html << " <li>#{INLINE[md[1]]}</li>\n"
156
+ md = (@line=@file.gets)&.match UNORDERED
157
+ end
158
+ @html << "</ul>\n"
159
+ true
160
+ end
161
+
162
+ # Ballot box
163
+ BALLOTS = /^- \[(x| )\] (.*)$/
164
+ PARSERS << :ballots
165
+ def ballots
166
+ md = BALLOTS.match(@line) or return false
167
+ @html << "<ul#{@opt[:attributes]}>\n"
168
+ @opt.delete(:attributes)
169
+ while md
170
+ x,t = md[1],md[2]
171
+ li = (x=='x')?
172
+ %q{<li style="list-style-type: '&#9745; '">} :
173
+ %q{<li style="list-style-type: '&#9744; '">}
174
+ @html << " #{li}#{INLINE[t]}</li>\n"
175
+ md = (@line=@file.gets)&.match BALLOTS
176
+ end
177
+ @html << "</ul>\n"
178
+ true
179
+ end
180
+
181
+ # Definition list
182
+ DEFINITIONS = /^: (.*)$/
183
+ PARSERS << :definitions
184
+ def definitions
185
+ md = DEFINITIONS.match(@line) or return false
186
+ @html << "<dl#{@opt[:attributes]}>\n"
187
+ @opt.delete(:attributes)
188
+ while md
189
+ item = md[1]
190
+ @html << ((item[-1]==':')? "<dt>#{INLINE[item[0..-2]]}</dt>\n" :
191
+ "<dd>#{INLINE[item]}</dd>\n")
192
+ md = (@line=@file.gets)&.match DEFINITIONS
193
+ end
194
+ @html << "</dl>\n"
195
+ true
196
+ end
197
+
198
+ # Headers
199
+ HEADERS = /^([#]{1,6}) (.*)$/
200
+ PARSERS << :headers
201
+ def headers
202
+ md = HEADERS.match(@line) or return false
203
+ i,header = md[1].length,md[2]
204
+ id = header.strip.gsub(/\s+/,'+')
205
+ @html << %Q(<a id="#{id}">\n)
206
+ @html << " <h#{i}#{@opt[:attributes]}>#{INLINE[header]}</h#{i}>\n"
207
+ @html << "</a>\n"
208
+ @opt.delete(:attributes)
209
+ @line = @file.gets
210
+ true
211
+ end
212
+
213
+ # Block-quote
214
+ BLOCKQS = /^> (.*)$/
215
+ PARSERS << :blockqs
216
+ def blockqs
217
+ md = BLOCKQS.match(@line) or return false
218
+ @html << "<blockquote#{@opt[:attributes]}>\n"
219
+ @opt.delete(:attributes)
220
+ while md
221
+ @html << INLINE[md[1]]
222
+ @html << "\n"
223
+ md = (@line=@file.gets)&.match BLOCKQS
224
+ end
225
+ @html << "</blockquote>\n"
226
+ true
227
+ end
228
+
229
+
230
+ # Code
231
+ CODES = /^[`~]{3}\s*(\w+)?$/
232
+ PARSERS << :codes
233
+ def codes
234
+ md = CODES.match(@line) or return false
235
+ lang = Rouge::Lexer.find md[1]
236
+ klass = lang ? ' class="highlight"' : nil
237
+ @html << "<pre#{klass}#{@opt[:attributes]}><code>\n"
238
+ @opt.delete(:attributes)
239
+ code = ''
240
+ while @line=@file.gets and not CODES.match(@line)
241
+ code << @line
242
+ end
243
+ @html << (lang ? ROUGE.format(lang.new.lex(code)) : code)
244
+ @html << "</code></pre>\n"
245
+ @line = @file.gets if @line # then it's code close and thus need next @line.
246
+ true
247
+ end
248
+
249
+ # Preform
250
+ PREFORMS = /^ {4}(.*)$/
251
+ PARSERS << :preforms
252
+ def preforms
253
+ md = PREFORMS.match(@line) or return false
254
+ @html << "<pre#{@opt[:attributes]}>\n"
255
+ @opt.delete(:attributes)
256
+ while md
257
+ @html << md[1]
258
+ @html << "\n"
259
+ md = (@line=@file.gets)&.match PREFORMS
260
+ end
261
+ @html << "</pre>\n"
262
+ true
263
+ end
264
+
265
+ # Horizontal rule
266
+ HRS = /^---+$/
267
+ PARSERS << :hrs
268
+ def hrs
269
+ HRS.match? @line or return false
270
+ @html << "<hr#{@opt[:attributes]}>\n"
271
+ @opt.delete(:attributes)
272
+ @line = @file.gets
273
+ true
274
+ end
275
+
276
+ # Table
277
+ TABLES = /^\|.+\|$/
278
+ PARSERS << :tables
279
+ def tables
280
+ TABLES.match? @line or return false
281
+ @html << "<table#{@opt[:attributes]}>\n"
282
+ @opt.delete(:attributes)
283
+ @html << '<thead><tr><th>'
284
+ @html << @line[1...-1].split('|').map{INLINE[_1.strip]}.join('</th><th>')
285
+ @html << "</th></tr></thead>\n"
286
+ align = []
287
+ while (@line=@file.gets)&.match TABLES
288
+ @html << '<tr>'
289
+ @line[1...-1].split('|').each_with_index do |cell, i|
290
+ case cell
291
+ when /^\s*:-+:\s*$/
292
+ align[i] = ' align="center"'
293
+ @html << '<td><hr></td>'
294
+ when /^\s*-+:\s*$/
295
+ align[i] = ' align="right"'
296
+ @html << '<td><hr></td>'
297
+ when /^\s*:-+\s*$/
298
+ align[i] = ' align="left"'
299
+ @html << '<td><hr></td>'
300
+ else
301
+ @html << "<td#{align[i]}>#{INLINE[cell.strip]}</td>"
302
+ end
303
+ end
304
+ @html << "</tr>\n"
305
+ end
306
+ @html << "</table>\n"
307
+ true
308
+ end
309
+
310
+ # Splits
311
+ SPLITS = /^:?\|:?$/
312
+ PARSERS << :splits
313
+ def splits
314
+ SPLITS.match? @line or return false
315
+ case @line.chomp
316
+ when '|:'
317
+ @html << %Q(<table><tr><td#{@opt[:attributes]}>\n)
318
+ when '|'
319
+ @html << %Q(</td><td#{@opt[:attributes]}>\n)
320
+ when ':|:'
321
+ @html << %Q(</td></tr><tr><td#{@opt[:attributes]}>\n)
322
+ when ':|'
323
+ @html << %Q(</td></tr></table>\n)
324
+ end
325
+ @opt.delete(:attributes)
326
+ @line = @file.gets
327
+ true
328
+ end
329
+
330
+ # Image
331
+ IMAGES = /^!\[([^\[\]]+)\]\(([^\(\)]+)\)$/
332
+ PARSERS << :images
333
+ def images
334
+ md = IMAGES.match(@line) or return false
335
+ alt,src=md[1],md[2]
336
+ style = ' '
337
+ case alt
338
+ when /^ .* $/
339
+ style = %Q( style="display: block; margin-left: auto; margin-right: auto;" )
340
+ when / $/
341
+ style = %Q( style="float:left;" )
342
+ when /^ /
343
+ style = %Q( style="float:right;" )
344
+ end
345
+ @html << %Q(<img src="#{src}"#{style}alt="#{alt.strip}"#{@opt[:attributes]}>\n)
346
+ @opt.delete(:attributes)
347
+ @line = @file.gets
348
+ true
349
+ end
350
+
351
+ # Forms
352
+ FORMS = /^!( (\w+:)?\[\*?\w+(="[^"]*")?\])+/
353
+ PARSERS << :forms
354
+ def forms
355
+ md = FORMS.match(@line) or return false
356
+ form = []
357
+ n,fields,submit,method = 0,0,nil,nil
358
+ action = (_=/\(([^\(\)]*)\)$/.match(@line))? _[1] : nil
359
+ while md
360
+ n += 1
361
+ form << ' <br>' if n > 1
362
+ @line.scan(/(\w+:)?\[(\*)?(\w+)(="[^"]*")?\]/).each do |field, pwd, name, value|
363
+ method ||= ' method="post"' if pwd
364
+ field &&= field[0...-1]
365
+ value &&= value[2...-1]
366
+ if field
367
+ fields += 1
368
+ type = (pwd)? 'password' : 'text'
369
+ if value
370
+ form << %Q{ #{field}:<input type="#{type}" name="#{name}" value="#{value}">}
371
+ else
372
+ form << %Q{ #{field}:<input type="#{type}" name="#{name}">}
373
+ end
374
+ elsif name=='submit'
375
+ submit = value
376
+ else
377
+ form << %Q{ <input type="hidden" name="#{name}" value="#{value}">}
378
+ end
379
+ end
380
+ md = (@line=@file.gets)&.match FORMS
381
+ end
382
+ if submit or not fields==1
383
+ submit ||= 'Submit'
384
+ form << ' <br>' if n > 1
385
+ form << %Q( <input type="submit" value="#{submit}">)
386
+ end
387
+ form.unshift %Q(<form action="#{action}"#{method}#{@opt[:attributes]}>)
388
+ form << %Q(</form>)
389
+ @html << form.join("\n")
390
+ @html << "\n"
391
+ @opt.delete(:attributes)
392
+ true
393
+ end
394
+
395
+ # Embed text
396
+ EMBED_TEXTS = /^!> (#{PAGE_KEY}\.txt)$/
397
+ PARSERS << :embed_texts
398
+ def embed_texts
399
+ md = EMBED_TEXTS.match(@line) or return false
400
+ if File.exist?(filename=File.join(ROOT, md[1]))
401
+ @html << "<pre>\n"
402
+ @html << File.read(filename)
403
+ @html << "</pre>\n"
404
+ else
405
+ @html << @line
406
+ end
407
+ @line = @file.gets
408
+ true
409
+ end
410
+
411
+ # Footnotes
412
+ FOOTNOTES = /^\[\^\d+\]:/
413
+ PARSERS << :footnotes
414
+ def footnotes
415
+ md = FOOTNOTES.match(@line) or return false
416
+ @html << "<small>\n"
417
+ while md
418
+ @html << INLINE[@line.chomp]+"<br>\n"
419
+ md = (@line=@file.gets)&.match FOOTNOTES
420
+ end
421
+ @html << "</small>\n"
422
+ true
423
+ end
424
+
425
+ # Attributes
426
+ ATTRIBUTES = /^\{:( .*)\}/
427
+ PARSERS << :attributes
428
+ def attributes
429
+ md = ATTRIBUTES.match(@line) or return false
430
+ @opt[:attributes] = md[1]
431
+ @line = md.post_match
432
+ true
433
+ end
434
+ end
435
+ end