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,661 @@
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 # side-effect
413
+ _out %[<div class="col-lg-9 col-md-9 col-sm-9 col-xs-12">]
414
+ all_teasers
415
+ _out %[</div>]
416
+ end
417
+
418
+ def sidebar
419
+ _out %[<div class="col-lg-3 col-md-3 col-sm-3 col-xs-12">]
420
+ _args do |token|
421
+ tag = token.chomp.strip.downcase
422
+ Dir.chdir("widgets/#{tag}") do
423
+ livetext tag, "card-#{tag}.html"
424
+ _include_file "card-#{tag}.html"
425
+ end
426
+ end
427
+ _out %[</div>]
428
+ end
429
+
430
+ def stylesheet
431
+ lines = _body
432
+ url = lines[0]
433
+ integ = lines[1]
434
+ cross = lines[2] || "anonymous"
435
+ _out %[<link rel="stylesheet" href="#{url}" integrity="#{integ}" crossorigin="#{cross}"></link>]
436
+ end
437
+
438
+ def script
439
+ lines = _body
440
+ url = lines[0]
441
+ integ = lines[1]
442
+ cross = lines[2] || "anonymous"
443
+ _out %[<script src="#{url}" integrity="#{integ}" crossorigin="#{cross}"></script>]
444
+ end
445
+
446
+
447
+ ### How this next bit works:
448
+ ###
449
+ ### all_teasers will call _find_recent_posts
450
+ ###
451
+ ### _find_recent_posts will search higher in the directory structure
452
+ ### for where the posts are (0001, 0002, ...) NOTE: This implies you
453
+ ### must be in some specific place when this code is run.
454
+ ### It returns the 20 most recent posts.
455
+ ###
456
+ ### all_teasers will then pick a small number of posts and call _teaser
457
+
458
+ ### on each one. (The code in _teaser really belongs in a small template
459
+ ### somewhere.)
460
+ ###
461
+
462
+ def _find_recent_posts
463
+ @vdir = _var(:FileDir).match(%r[(^.*/views/.*?)/])[1]
464
+ posts = nil
465
+ dir_posts = @vdir + "/posts"
466
+ entries = Dir.entries(dir_posts)
467
+ posts = entries.grep(/^\d\d\d\d/).map {|x| dir_posts + "/" + x }
468
+ posts.select! {|x| File.directory?(x) }
469
+ # directories that start with four digits
470
+ posts = posts.sort {|a, b| b.to_i <=> a.to_i } # sort descending
471
+ posts[0..19] # return 20 at most
472
+ end
473
+
474
+ def all_teasers
475
+ text = <<-HTML
476
+ <html>
477
+ <head><link rel="stylesheet" href="etc/blog.css"></head>
478
+ <body>
479
+ HTML
480
+ posts = _find_recent_posts
481
+ wanted = [5, posts.size].min # estimate how many we want?
482
+ enum = posts.each
483
+ wanted.times do
484
+ postid = File.basename(enum.next)
485
+ postid = postid.to_i
486
+ text << _teaser(postid) # side effect! calls _out
487
+ end
488
+ text << "</body></html>"
489
+ File.write("recent.html", text)
490
+ _out %[<iframe id="main" style="width: 100vw; height: 100vh; position: relative;" src='recent.html' width=100% frameborder="0" allowfullscreen></iframe>]
491
+ end
492
+
493
+ def _post_lookup(postid) # side-effect
494
+ # .. = templates, ../.. = views/thisview
495
+ slug = title = date = teaser_text = nil
496
+
497
+ dir_posts = @vdir + "/posts"
498
+ posts = Dir.entries(dir_posts).grep(/^\d\d\d\d/).map {|x| dir_posts + "/" + x }
499
+ posts.select! {|x| File.directory?(x) }
500
+
501
+ post = posts.select {|x| File.basename(x).to_i == postid }
502
+ raise "Error: More than one post #{postid}" if post.size > 1
503
+ postdir = post.first
504
+ vp = RuneBlog::ViewPost.new(@blog.view, postdir)
505
+ vp
506
+ end
507
+
508
+ def _interpolate(str, context) # FIXME move this later
509
+ wrapped = "%[" + str.dup + "]" # could fail...
510
+ eval(wrapped, context)
511
+ end
512
+
513
+ def _teaser(slug)
514
+ id = slug.to_i
515
+ text = nil
516
+ post_entry_name = @theme + "blog/post_entry.lt3"
517
+ @_post_entry ||= File.read(post_entry_name)
518
+ vp = _post_lookup(id)
519
+ nslug, aslug, title, date, teaser_text =
520
+ vp.nslug, vp.aslug, vp.title, vp.date, vp.teaser_text
521
+ path = vp.path
522
+ url = "#{path}/#{aslug}.html" # Should be relative to .blogs!! FIXME
523
+ date = Date.parse(date)
524
+ date = date.strftime("%B %e<br>%Y")
525
+ text = _interpolate(@_post_entry, binding)
526
+ text
527
+ end
528
+
529
+ def _card_generic(card_title:, middle:, extra: "")
530
+ front = <<-HTML
531
+ <div class="card #{extra} mb-3">
532
+ <div class="card-body">
533
+ <h5 class="card-title">#{card_title}</h5>
534
+ HTML
535
+
536
+ tail = <<-HTML
537
+ </div>
538
+ </div>
539
+ HTML
540
+ text = front + middle + tail
541
+ _out text + "\n "
542
+ end
543
+
544
+ def card_iframe
545
+ title, lines = _data, _body
546
+ lines.map!(&:chomp)
547
+ url = lines[0].chomp
548
+ stuff = lines[1..-1].join(" ") # FIXME later
549
+ middle = <<-HTML
550
+ <iframe src="#{url}" #{stuff}
551
+ style="border: 0" #{stuff}
552
+ frameborder="0" scrolling="no">
553
+ </iframe>
554
+ HTML
555
+
556
+ _card_generic(card_title: title, middle: middle, extra: "bg-dark text-white")
557
+ end
558
+
559
+ def _main(url)
560
+ %[href="javascript: void(0)" onclick="javascript:open_main('#{url}')"]
561
+ end
562
+
563
+ def card1
564
+ title, lines = _data, _body
565
+ lines.map!(&:chomp)
566
+
567
+ card_text = lines[0]
568
+ url, classname, cdata = lines[1].split(",", 4)
569
+ main = _main(url)
570
+
571
+ middle = <<-HTML
572
+ <p class="card-text">#{card_text}</p>
573
+ <a #{main} class="#{classname}">#{cdata}</a>
574
+ HTML
575
+
576
+ _card_generic(card_title: title, middle: middle, extra: "bg-dark text-white")
577
+ end
578
+
579
+ def card2
580
+ str = _data
581
+ file, card_title = str.chomp.split(" ", 2)
582
+ card_title = %[<a #{_main(file)} style="text-decoration: none; color: black">#{card_title}</a>]
583
+
584
+ # FIXME is this wrong??
585
+
586
+ open = <<-HTML
587
+ <div class="card mb-3">
588
+ <div class="card-body">
589
+ <h5 class="card-title">#{card_title}</h5>
590
+ <ul class="list-group list-group-flush">
591
+ HTML
592
+ _out open
593
+ _body do |line|
594
+ url, cdata = line.chomp.split(",", 3)
595
+ main = _main(url)
596
+ _out %[<li class="list-group-item"><a #{main}}">#{cdata}</a> </li>]
597
+ end
598
+ close = %[ </ul>\n </div>\n </div>]
599
+ _out close
600
+ end
601
+
602
+ def tag_cloud
603
+ title = _data
604
+ title = "Tag Cloud" if title.empty?
605
+ open = <<-HTML
606
+ <div class="card mb-3">
607
+ <div class="card-body">
608
+ <h5 class="card-title">#{title}</h5>
609
+ HTML
610
+ _out open
611
+ _body do |line|
612
+ line.chomp!
613
+ url, classname, cdata = line.split(",", 4)
614
+ main = _main(url)
615
+ _out %[<a #{main} class="#{classname}">#{cdata}</a>]
616
+ end
617
+ close = %[ </div>\n </div>]
618
+ _out close
619
+ end
620
+
621
+ def navbar
622
+ title = _var(:blog)
623
+
624
+ open = <<-HTML
625
+ <nav class="navbar navbar-expand-lg navbar-light bg-light">
626
+ <a class="navbar-brand" href="index.html">#{title}</a>
627
+ <button class="navbar-toggler"
628
+ type="button"
629
+ data-toggle="collapse"
630
+ data-target="#navbarSupportedContent"
631
+ aria-controls="navbarSupportedContent"
632
+ aria-expanded="false"
633
+ aria-label="Toggle navigation">
634
+ <span class="navbar-toggler-icon"></span>
635
+ </button>
636
+ <div class="collapse navbar-collapse pull-right"
637
+ id="navbarSupportedContent">
638
+ <ul class="navbar-nav mr-auto">
639
+ HTML
640
+ close = <<-HTML
641
+ </ul>
642
+ </div>
643
+ </nav>
644
+ HTML
645
+
646
+ first = true
647
+ _out open
648
+ _body do |line|
649
+ href, cdata = line.chomp.strip.split(" ", 2)
650
+ main = _main(href)
651
+ if first
652
+ first = false
653
+ _out %[<li class="nav-item active"> <a class="nav-link" href="#{href}">#{cdata}<span class="sr-only">(current)</span></a> </li>]
654
+ else
655
+ main = _main("navbar/#{href}")
656
+ _out %[<li class="nav-item"> <a class="nav-link" #{main}>#{cdata}</a> </li>]
657
+ end
658
+ end
659
+ _out close
660
+ end
661
+