runeblog 0.2.3 → 0.2.5

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.
@@ -0,0 +1,646 @@
1
+ require 'ostruct'
2
+ require 'pp'
3
+ require 'date'
4
+
5
+ require 'livetext'
6
+ require 'runeblog'
7
+
8
+ errfile = File.new("/tmp/liveblog.out", "w")
9
+ STDERR.reopen(errfile)
10
+
11
+ def init_liveblog # FIXME - a lot of this logic sucks
12
+ here = Dir.pwd
13
+ dir = here
14
+ loop { dir = Dir.pwd; break if File.exist?("config"); Dir.chdir("..") }
15
+ Dir.chdir(here)
16
+ @blog = $_blog = RuneBlog.new(dir)
17
+ @root = @blog.root
18
+ @view = @blog.view
19
+ @view_name = @blog.view.name
20
+ @vdir = @blog.view.dir
21
+ @version = RuneBlog::VERSION
22
+ @theme = @vdir + "/themes/standard/"
23
+ end
24
+
25
+ # FIXME - stale? and livetext are duplicated from helpers-blog
26
+
27
+ def stale?(src, dst)
28
+ return true unless File.exist?(dst)
29
+ return true if File.mtime(src) > File.mtime(dst)
30
+ return false
31
+ end
32
+
33
+ def livetext(src, dst=nil)
34
+ src += ".lt3" unless src.end_with?(".lt3")
35
+ if dst
36
+ dst += ".html" unless dst.end_with?(".html")
37
+ else
38
+ dst = src.sub(/.lt3$/, "")
39
+ end
40
+ return unless stale?(src, dst)
41
+ system("livetext #{src} >#{dst}")
42
+ end
43
+
44
+ def post
45
+ @meta = OpenStruct.new
46
+ @meta.num = _args[0]
47
+ _out " <!-- Post number #{@meta.num} -->\n "
48
+ end
49
+
50
+ def quote
51
+ _passthru "<blockquote>"
52
+ _passthru _body
53
+ _passthru "</blockquote>"
54
+ _optional_blank_line
55
+ end
56
+
57
+ def categories # does nothing right now
58
+ end
59
+
60
+ def style
61
+ fname = _args[0]
62
+ _passthru %[<link rel="stylesheet" href="???/etc/#{fname}')">]
63
+ end
64
+
65
+ # Move elsewhere later!
66
+
67
+ def h1; _passthru "<h1>#{@_data}</h1>"; end
68
+ def h2; _passthru "<h2>#{@_data}</h2>"; end
69
+ def h3; _passthru "<h3>#{@_data}</h3>"; end
70
+ def h4; _passthru "<h4>#{@_data}</h4>"; end
71
+ def h5; _passthru "<h5>#{@_data}</h5>"; end
72
+ def h6; _passthru "<h6>#{@_data}</h6>"; end
73
+
74
+ def hr; _passthru "<hr>"; end
75
+
76
+ def list
77
+ _out "<ul>"
78
+ _body {|line| _out "<li>#{line}</li>" }
79
+ _out "</ul>"
80
+ _optional_blank_line
81
+ end
82
+
83
+ def list!
84
+ _out "<ul>"
85
+ lines = _body.each
86
+ loop do
87
+ line = lines.next
88
+ line = _format(line)
89
+ if line[0] == " "
90
+ _out line
91
+ else
92
+ _out "<li>#{line}</li>"
93
+ end
94
+ end
95
+ _out "</ul>"
96
+ _optional_blank_line
97
+ end
98
+
99
+ def html_body(file)
100
+ file.puts "<html>\n <body>"
101
+ yield
102
+ file.puts " </body>\n</html>"
103
+ end
104
+
105
+ def make_news_links
106
+ # FIXME remember strings may not be safe
107
+ line = _data.chomp
108
+ input, cardfile, mainfile, card_title = *line.split(" ", 4)
109
+ pairs = File.readlines(input).map {|line| line.chomp.split(",", 2) }
110
+ # HTML for main area (iframe)
111
+ File.open("#{mainfile}.html", "w") do |f|
112
+ html_body(f) do
113
+ f.puts "<h1>#{card_title}</h1>"
114
+ pairs.each do |file, title|
115
+ f.puts %[<a style="text-decoration: none; font-size: 24px" href="#{file}">#{title}</a> <br>]
116
+ end
117
+ end
118
+ end
119
+ # HTML for sidebar card
120
+ File.open("#{cardfile}.html", "w") do |f|
121
+ f.puts <<-EOS
122
+ <div class="card mb-3">
123
+ <div class="card-body">
124
+ <h5 class="card-title">
125
+ <a href="javascript: void(0)"
126
+ onclick="javascript:open_main('widgets/news/#{mainfile}.html')"
127
+ style="text-decoration: none; color: black">#{card_title}</a>
128
+ </h5>
129
+ EOS
130
+ pairs.each do |file, title|
131
+ f.puts <<-EOS
132
+ <li class="list-group-item"> <a href="javascript: void(0)"
133
+ onclick="javascript:open_main('#{file}')">#{title}</a> </li>
134
+ EOS
135
+ end
136
+ f.puts <<-EOS
137
+ </div>
138
+ </div>
139
+ EOS
140
+ end
141
+ end
142
+
143
+ ### inset
144
+
145
+ def inset
146
+ lines = _body
147
+ box = ""
148
+ lines.each do |line|
149
+ line = line.dup
150
+ if line[0] == "/" # Only into inset
151
+ line[0] = ' '
152
+ box << line.dup + " "
153
+ line.replace(" ")
154
+ end
155
+ if line[0] == "|" # Into inset and body
156
+ line[0] = ' '
157
+ box << line.dup + " "
158
+ end
159
+ _passthru(line)
160
+ end
161
+ lr = _args.first
162
+ wide = _args[1] || "25"
163
+ _passthru "<div style='float:#{lr}; width: #{wide}%; padding:8px; padding-right:12px; font-family:verdana'>"
164
+ _passthru '<b><i>'
165
+ _passthru box
166
+ _passthru_noline '</i></b></div>'
167
+ _optional_blank_line
168
+ end
169
+
170
+ def _errout(*args)
171
+ ::STDERR.puts *args
172
+ end
173
+
174
+ def _passthru(line)
175
+ return if line.nil?
176
+ line = _format(line)
177
+ _out line + "\n"
178
+ _out "<p>" if line.empty? && ! @_nopara
179
+ end
180
+
181
+ def _passthru_noline(line)
182
+ return if line.nil?
183
+ line = _format(line)
184
+ _out line
185
+ _out "<p>" if line.empty? && ! @_nopara
186
+ end
187
+
188
+ def title
189
+ raise "'post' was not called" unless @meta
190
+ title = @_data.chomp
191
+ @meta.title = title
192
+ setvar :title, title
193
+ _out %[<h1 class="post-title">#{title}</h1><br>]
194
+ _optional_blank_line
195
+ end
196
+
197
+ def pubdate
198
+ raise "'post' was not called" unless @meta
199
+ _debug "data = #@_data"
200
+ # Check for discrepancy?
201
+ match = /(\d{4}).(\d{2}).(\d{2})/.match @_data
202
+ junk, y, m, d = match.to_a
203
+ y, m, d = y.to_i, m.to_i, d.to_i
204
+ @meta.date = ::Date.new(y, m, d)
205
+ @meta.pubdate = "%04d-%02d-%02d" % [y, m, d]
206
+ _optional_blank_line
207
+ end
208
+
209
+ def image # primitive so far
210
+ _debug "img: huh? <img src=#{_args.first}></img>"
211
+ fname = _args.first
212
+ path = "assets/#{fname}"
213
+ _out "<img src=#{path}></img>"
214
+ _optional_blank_line
215
+ end
216
+
217
+ def tags
218
+ raise "'post' was not called" unless @meta
219
+ _debug "args = #{_args}"
220
+ @meta.tags = _args.dup || []
221
+ _optional_blank_line
222
+ end
223
+
224
+ def views
225
+ raise "'post' was not called" unless @meta
226
+ _debug "data = #{_args}"
227
+ @meta.views = _args.dup
228
+ _optional_blank_line
229
+ end
230
+
231
+ def pin
232
+ raise "'post' was not called" unless @meta
233
+ _debug "data = #{_args}"
234
+ # verify only already-specified views?
235
+ @meta.pinned = _args.dup
236
+ _optional_blank_line
237
+ end
238
+
239
+ def write_post
240
+ raise "'post' was not called" unless @meta
241
+ save = Dir.pwd
242
+ @postdir.gsub!(/\/\//, "/") # FIXME unneeded?
243
+ Dir.mkdir(@postdir) unless Dir.exist?(@postdir) # FIXME remember assets!
244
+ Dir.chdir(@postdir)
245
+ @meta.views = @meta.views.join(" ") if @meta.views.is_a? Array
246
+ @meta.tags = @meta.tags.join(" ") if @meta.tags.is_a? Array
247
+ File.write("teaser.txt", @meta.teaser)
248
+
249
+ fields = [:num, :title, :date, :pubdate, :views, :tags]
250
+
251
+ fname2 = "metadata.txt"
252
+ f2 = File.open(fname2, "w") do |f2|
253
+ fields.each {|fld| f2.puts "#{fld}: #{@meta.send(fld)}" }
254
+ end
255
+ Dir.chdir(save)
256
+ rescue => err
257
+ puts "err = #{err}"
258
+ puts err.backtrace.join("\n")
259
+ end
260
+
261
+ def teaser
262
+ raise "'post' was not called" unless @meta
263
+ @meta.teaser = _body_text
264
+ setvar :teaser, @meta.teaser
265
+ _out @meta.teaser + "\n"
266
+ # FIXME
267
+ end
268
+
269
+ def finalize
270
+ unless @meta
271
+ puts @live.body
272
+ return
273
+ end
274
+ if @blog.nil?
275
+ return @meta
276
+ end
277
+
278
+ @slug = @blog.make_slug(@meta)
279
+ slug_dir = @slug
280
+ @postdir = @blog.view.dir + "/posts/#{slug_dir}"
281
+ STDERR.puts "--- finalize: pwd = #{Dir.pwd} postdir = #@postdir"
282
+ write_post
283
+ @meta
284
+ end
285
+
286
+ $Dot = self # Clunky! for dot commands called from Functions class
287
+
288
+ # Find a better way to do this?
289
+
290
+ class Livetext::Functions
291
+
292
+ def br(n="1")
293
+ # Thought: Maybe make a way for functions to "simply" call the
294
+ # dot command of the same name?? Is this trivial??
295
+ n = n.empty? ? 1 : n.to_i
296
+ "<br>"*n
297
+ end
298
+
299
+ def h1(param); "<h1>#{param}</h1>"; end
300
+ def h2(param); "<h2>#{param}</h2>"; end
301
+ def h3(param); "<h3>#{param}</h3>"; end
302
+ def h4(param); "<h4>#{param}</h4>"; end
303
+ def h5(param); "<h5>#{param}</h5>"; end
304
+ def h6(param); "<h6>#{param}</h6>"; end
305
+
306
+ def hr(param=nil)
307
+ $Dot.hr
308
+ end
309
+
310
+ def image(param)
311
+ "<img src='#{param}'></img>"
312
+ end
313
+
314
+ end
315
+
316
+ ###### experimental...
317
+
318
+ class Livetext::Functions
319
+ def _var(name)
320
+ ::Livetext::Vars[name] || "[:#{name} is undefined]"
321
+ end
322
+
323
+ def link
324
+ file, cdata = self.class.param.split("||", 2)
325
+ %[<link type="application/atom+xml" rel="alternate" href="#{_var(:host)}#{file}" title="#{_var(:title)}">]
326
+ end
327
+ end
328
+
329
+ ###
330
+
331
+ def _var(name) # FIXME scope issue!
332
+ ::Livetext::Vars[name] || "[:#{name} is undefined]"
333
+ end
334
+
335
+ def head # Does NOT output <head> tags
336
+ args = _args
337
+ args.each do |inc|
338
+ self.data = inc
339
+ _include
340
+ end
341
+ # Depends on vars: title, desc, host
342
+ defaults = {}
343
+ defaults = { "charset" => %[<meta charset="utf-8">],
344
+ "http-equiv" => %[<meta http-equiv="X-UA-Compatible" content="IE=edge">],
345
+ "title" => %[<title>\n #{_var(:blog)} | #{_var("blog.desc")}\n </title>],
346
+ "generator" => %[<meta name="generator" content="Runeblog v #@version">],
347
+ "og:title" => %[<meta property="og:title" content="#{_var(:blog)}">],
348
+ "og:locale" => %[<meta property="og:locale" content="#{_var(:locale)}">],
349
+ "description" => %[<meta name="description" content="#{_var("blog.desc")}">],
350
+ "og:description" => %[<meta property="og:description" content="#{_var("blog.desc")}">],
351
+ "linkc" => %[<link rel="canonical" href="#{_var(:host)}">],
352
+ "og:url" => %[<meta property="og:url" content="#{_var(:host)}">],
353
+ "og:site_name" => %[<meta property="og:site_name" content="#{_var(:blog)}">],
354
+ "style" => %[<link rel="stylesheet" href="etc/blog.css">],
355
+ "feed" => %[<link type="application/atom+xml" rel="alternate" href="#{_var(:host)}/feed.xml" title="#{_var(:blog)}">],
356
+ "favicon" => %[<link rel="shortcut icon" type="image/x-icon" href="../etc/favicon.ico">\n <link rel="apple-touch-icon" href="../etc/favicon.ico">]
357
+ }
358
+ result = {}
359
+ lines = _body
360
+ lines.each do |line|
361
+ line.chomp
362
+ word, remain = line.split(" ", 2)
363
+ case word
364
+ when "viewport"
365
+ result["viewport"] = %[<meta name="viewport" content="#{remain}">]
366
+ when "script" # FIXME this is broken
367
+ file = remain
368
+ text = File.read(file)
369
+ result["script"] = Livetext.new.transform(text)
370
+ when "style"
371
+ result["style"] = %[<link rel="stylesheet" href="('/etc/#{remain}')">]
372
+ # Later: allow other overrides
373
+ when ""; break
374
+ else
375
+ STDERR.puts "-- got '#{word}'; old value = #{result[word].inspect}"
376
+ if defaults[word]
377
+ result[word] = %[<meta property="#{word}" content="#{remain}">]
378
+ STDERR.puts "-- new value = #{result[word].inspect}"
379
+ else
380
+ puts "Unknown tag '#{word}'"
381
+ end
382
+ end
383
+ end
384
+ hash = defaults.dup.update(result) # FIXME collisions?
385
+
386
+ # _out "<!-- "; _out hash.inspect; _out "--> "
387
+ hash.each_value {|x| _out " " + x }
388
+ _out "<body>"
389
+ end
390
+
391
+ ########## newer stuff...
392
+
393
+ def meta
394
+ args = _args
395
+ enum = args.each
396
+ str = "<meta"
397
+ arg = enum.next
398
+ loop do
399
+ if arg.end_with?(":")
400
+ str << " " << arg[0..-2] << "="
401
+ a2 = enum.next
402
+ str << %["#{a2}"]
403
+ else
404
+ STDERR.puts "=== meta error?"
405
+ end
406
+ arg = enum.next
407
+ end
408
+ str << ">"
409
+ _out str
410
+ end
411
+
412
+ def recent_posts
413
+ _out %[<div class="col-lg-9 col-md-9 col-sm-9 col-xs-12">]
414
+ _out %[<iframe id="main" style="width: 100vw; height: 100vh; position: relative;" src='recent.html' width=100% frameborder="0" allowfullscreen></iframe>]
415
+ # all_teasers
416
+ _out %[</div>]
417
+ end
418
+
419
+ def _find_recent_posts
420
+ @vdir = _var(:FileDir).match(%r[(^.*/views/.*?)/])[1]
421
+ posts = nil
422
+ dir_posts = @vdir + "/posts"
423
+ entries = Dir.entries(dir_posts)
424
+ posts = entries.grep(/^\d\d\d\d/).map {|x| dir_posts + "/" + x }
425
+ posts.select! {|x| File.directory?(x) }
426
+ # directories that start with four digits
427
+ posts = posts.sort {|a, b| b.to_i <=> a.to_i } # sort descending
428
+ posts[0..19] # return 20 at most
429
+ end
430
+
431
+ def all_teasers
432
+ text = <<-HTML
433
+ <html>
434
+ <head><link rel="stylesheet" href="etc/blog.css"></head>
435
+ <body>
436
+ HTML
437
+ posts = _find_recent_posts
438
+ wanted = [5, posts.size].min # estimate how many we want?
439
+ enum = posts.each
440
+ wanted.times do
441
+ postid = File.basename(enum.next)
442
+ postid = postid.to_i
443
+ text << _teaser(postid) # side effect! calls _out
444
+ end
445
+ text << "</body></html>"
446
+ File.write("recent.html", text)
447
+ _out %[<iframe id="main" style="width: 100vw; height: 100vh; position: relative;" src='recent.html' width=100% frameborder="0" allowfullscreen></iframe>]
448
+ end
449
+
450
+ def sidebar
451
+ _out %[<div class="col-lg-3 col-md-3 col-sm-3 col-xs-12">]
452
+ _args do |token|
453
+ tag = token.chomp.strip.downcase
454
+ Dir.chdir("widgets/#{tag}") do
455
+ livetext tag, "card-#{tag}.html"
456
+ _include_file "card-#{tag}.html"
457
+ end
458
+ end
459
+ _out %[</div>]
460
+ end
461
+
462
+ def stylesheet
463
+ lines = _body
464
+ url = lines[0]
465
+ integ = lines[1]
466
+ cross = lines[2] || "anonymous"
467
+ _out %[<link rel="stylesheet" href="#{url}" integrity="#{integ}" crossorigin="#{cross}"></link>]
468
+ end
469
+
470
+ def script
471
+ lines = _body
472
+ url = lines[0]
473
+ integ = lines[1]
474
+ cross = lines[2] || "anonymous"
475
+ _out %[<script src="#{url}" integrity="#{integ}" crossorigin="#{cross}"></script>]
476
+ end
477
+
478
+ def _post_lookup(postid) # side-effect
479
+ # .. = templates, ../.. = views/thisview
480
+ slug = title = date = teaser_text = nil
481
+
482
+ dir_posts = @vdir + "/posts"
483
+ posts = Dir.entries(dir_posts).grep(/^\d\d\d\d/).map {|x| dir_posts + "/" + x }
484
+ posts.select! {|x| File.directory?(x) }
485
+
486
+ post = posts.select {|x| File.basename(x).to_i == postid }
487
+ raise "Error: More than one post #{postid}" if post.size > 1
488
+ postdir = post.first
489
+ vp = RuneBlog::ViewPost.new(@blog.view, postdir)
490
+ vp
491
+ end
492
+
493
+ def _interpolate(str, context) # FIXME move this later
494
+ wrapped = "%[" + str.dup + "]" # could fail...
495
+ eval(wrapped, context)
496
+ end
497
+
498
+ def _teaser(slug)
499
+ id = slug.to_i
500
+ text = nil
501
+ post_entry_name = @theme + "blog/post_entry.lt3"
502
+ @_post_entry ||= File.read(post_entry_name)
503
+ vp = _post_lookup(id)
504
+ nslug, aslug, title, date, teaser_text =
505
+ vp.nslug, vp.aslug, vp.title, vp.date, vp.teaser_text
506
+ path = vp.path
507
+ url = "#{path}/#{aslug}.html" # Should be relative to .blogs!! FIXME
508
+ date = Date.parse(date)
509
+ date = date.strftime("%B %e<br>%Y")
510
+ text = _interpolate(@_post_entry, binding)
511
+ text
512
+ end
513
+
514
+ def _card_generic(card_title:, middle:, extra: "")
515
+ front = <<-HTML
516
+ <div class="card #{extra} mb-3">
517
+ <div class="card-body">
518
+ <h5 class="card-title">#{card_title}</h5>
519
+ HTML
520
+
521
+ tail = <<-HTML
522
+ </div>
523
+ </div>
524
+ HTML
525
+ text = front + middle + tail
526
+ _out text + "\n "
527
+ end
528
+
529
+ def card_iframe
530
+ title, lines = _data, _body
531
+ lines.map!(&:chomp)
532
+ url = lines[0].chomp
533
+ stuff = lines[1..-1].join(" ") # FIXME later
534
+ middle = <<-HTML
535
+ <iframe src="#{url}" #{stuff}
536
+ style="border: 0" #{stuff}
537
+ frameborder="0" scrolling="no">
538
+ </iframe>
539
+ HTML
540
+
541
+ _card_generic(card_title: title, middle: middle, extra: "bg-dark text-white")
542
+ end
543
+
544
+ def _main(url)
545
+ %[href="javascript: void(0)" onclick="javascript:open_main('#{url}')"]
546
+ end
547
+
548
+ def card1
549
+ title, lines = _data, _body
550
+ lines.map!(&:chomp)
551
+
552
+ card_text = lines[0]
553
+ url, classname, cdata = lines[1].split(",", 4)
554
+ main = _main(url)
555
+
556
+ middle = <<-HTML
557
+ <p class="card-text">#{card_text}</p>
558
+ <a #{main} class="#{classname}">#{cdata}</a>
559
+ HTML
560
+
561
+ _card_generic(card_title: title, middle: middle, extra: "bg-dark text-white")
562
+ end
563
+
564
+ def card2
565
+ str = _data
566
+ file, card_title = str.chomp.split(" ", 2)
567
+ card_title = %[<a #{_main(file)} style="text-decoration: none; color: black">#{card_title}</a>]
568
+
569
+ # FIXME is this wrong??
570
+
571
+ open = <<-HTML
572
+ <div class="card mb-3">
573
+ <div class="card-body">
574
+ <h5 class="card-title">#{card_title}</h5>
575
+ <ul class="list-group list-group-flush">
576
+ HTML
577
+ _out open
578
+ _body do |line|
579
+ url, cdata = line.chomp.split(",", 3)
580
+ main = _main(url)
581
+ _out %[<li class="list-group-item"><a #{main}}">#{cdata}</a> </li>]
582
+ end
583
+ close = %[ </ul>\n </div>\n </div>]
584
+ _out close
585
+ end
586
+
587
+ def tag_cloud
588
+ title = _data
589
+ title = "Tag Cloud" if title.empty?
590
+ open = <<-HTML
591
+ <div class="card mb-3">
592
+ <div class="card-body">
593
+ <h5 class="card-title">#{title}</h5>
594
+ HTML
595
+ _out open
596
+ _body do |line|
597
+ line.chomp!
598
+ url, classname, cdata = line.split(",", 4)
599
+ main = _main(url)
600
+ _out %[<a #{main} class="#{classname}">#{cdata}</a>]
601
+ end
602
+ close = %[ </div>\n </div>]
603
+ _out close
604
+ end
605
+
606
+ def navbar
607
+ title = _var(:blog)
608
+
609
+ open = <<-HTML
610
+ <nav class="navbar navbar-expand-lg navbar-light bg-light">
611
+ <a class="navbar-brand" href="index.html">#{title}</a>
612
+ <button class="navbar-toggler"
613
+ type="button"
614
+ data-toggle="collapse"
615
+ data-target="#navbarSupportedContent"
616
+ aria-controls="navbarSupportedContent"
617
+ aria-expanded="false"
618
+ aria-label="Toggle navigation">
619
+ <span class="navbar-toggler-icon"></span>
620
+ </button>
621
+ <div class="collapse navbar-collapse pull-right"
622
+ id="navbarSupportedContent">
623
+ <ul class="navbar-nav mr-auto">
624
+ HTML
625
+ close = <<-HTML
626
+ </ul>
627
+ </div>
628
+ </nav>
629
+ HTML
630
+
631
+ first = true
632
+ _out open
633
+ _body do |line|
634
+ href, cdata = line.chomp.strip.split(" ", 2)
635
+ main = _main(href)
636
+ if first
637
+ first = false
638
+ _out %[<li class="nav-item active"> <a class="nav-link" href="#{href}">#{cdata}<span class="sr-only">(current)</span></a> </li>]
639
+ else
640
+ main = _main("navbar/#{href}")
641
+ _out %[<li class="nav-item"> <a class="nav-link" #{main}>#{cdata}</a> </li>]
642
+ end
643
+ end
644
+ _out close
645
+ end
646
+