markita 1.0.210828 → 3.0.210912

Sign up to get free protection for your applications and to get access to all the features.
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