markita 1.1.210831 → 3.1.210913

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