livetext 0.9.52 → 0.9.56

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.
Files changed (74) hide show
  1. checksums.yaml +4 -4
  2. data/imports/bookish.rb +3 -3
  3. data/lib/livetext/ast/show_ast_clean.rb +10 -0
  4. data/lib/livetext/ast/show_ast_result.rb +60 -0
  5. data/lib/livetext/ast/show_raw_arrays.rb +13 -0
  6. data/lib/livetext/ast.rb +464 -0
  7. data/lib/livetext/ast_to_html.rb +32 -0
  8. data/lib/livetext/core.rb +110 -53
  9. data/lib/livetext/errors.rb +1 -0
  10. data/lib/livetext/expansion.rb +21 -21
  11. data/lib/livetext/formatter.rb +70 -200
  12. data/lib/livetext/formatter_component.rb +189 -0
  13. data/lib/livetext/function_registry.rb +163 -0
  14. data/lib/livetext/functions.rb +26 -0
  15. data/lib/livetext/handler/mixin.rb +53 -0
  16. data/lib/livetext/helpers.rb +33 -16
  17. data/lib/livetext/reopen.rb +2 -0
  18. data/lib/livetext/skeleton.rb +0 -3
  19. data/lib/livetext/standard.rb +120 -72
  20. data/lib/livetext/userapi.rb +20 -1
  21. data/lib/livetext/variable_manager.rb +78 -0
  22. data/lib/livetext/variables.rb +9 -1
  23. data/lib/livetext/version.rb +1 -1
  24. data/lib/livetext.rb +9 -3
  25. data/plugin/booktool.rb +14 -14
  26. data/plugin/lt3scriptor.rb +914 -0
  27. data/plugin/mixin_functions_class.rb +33 -0
  28. data/test/snapshots/complex_body/expected-error.txt +0 -0
  29. data/test/snapshots/complex_body/expected-output.txt +8 -0
  30. data/test/snapshots/complex_body/source.lt3 +19 -0
  31. data/test/snapshots/debug_command/expected-error.txt +0 -0
  32. data/test/snapshots/debug_command/expected-output.txt +1 -0
  33. data/test/snapshots/debug_command/source.lt3 +3 -0
  34. data/test/snapshots/def_parameters/expected-error.txt +0 -0
  35. data/test/snapshots/def_parameters/expected-output.txt +21 -0
  36. data/test/snapshots/def_parameters/source.lt3 +44 -0
  37. data/test/snapshots/error_missing_end/match-error.txt +1 -1
  38. data/test/snapshots/functions_reflection/expected-error.txt +0 -0
  39. data/test/snapshots/functions_reflection/expected-output.txt +27 -0
  40. data/test/snapshots/functions_reflection/source.lt3 +5 -0
  41. data/test/snapshots/mixin_functions_class/expected-error.txt +0 -0
  42. data/test/snapshots/mixin_functions_class/expected-output.txt +20 -0
  43. data/test/snapshots/mixin_functions_class/mixin_functions_class.rb +33 -0
  44. data/test/snapshots/mixin_functions_class/source.lt3 +17 -0
  45. data/test/snapshots/multiple_functions/expected-error.txt +0 -0
  46. data/test/snapshots/multiple_functions/expected-output.txt +5 -0
  47. data/test/snapshots/multiple_functions/source.lt3 +16 -0
  48. data/test/snapshots/nested_includes/expected-error.txt +0 -0
  49. data/test/snapshots/nested_includes/expected-output.txt +68 -0
  50. data/test/snapshots/nested_includes/level2.inc +34 -0
  51. data/test/snapshots/nested_includes/level3.inc +20 -0
  52. data/test/snapshots/nested_includes/source.lt3 +49 -0
  53. data/test/snapshots/parameter_handling/expected-error.txt +0 -0
  54. data/test/snapshots/parameter_handling/expected-output.txt +7 -0
  55. data/test/snapshots/parameter_handling/source.lt3 +10 -0
  56. data/test/snapshots/subset.txt +1 -0
  57. data/test/snapshots/system_info/expected-error.txt +0 -0
  58. data/test/snapshots/system_info/match-output.txt +18 -0
  59. data/test/snapshots/system_info/source.lt3 +16 -0
  60. data/test/unit/all.rb +7 -0
  61. data/test/unit/ast.rb +90 -0
  62. data/test/unit/ast_directives.rb +104 -0
  63. data/test/unit/ast_variables.rb +71 -0
  64. data/test/unit/core_methods.rb +317 -0
  65. data/test/unit/formatter.rb +84 -0
  66. data/test/unit/formatter_component.rb +84 -0
  67. data/test/unit/function_registry.rb +132 -0
  68. data/test/unit/mixin_functions_class.rb +131 -0
  69. data/test/unit/stringparser.rb +14 -32
  70. data/test/unit/variable_manager.rb +71 -0
  71. metadata +51 -5
  72. data/imports/markdown.rb +0 -44
  73. data/lib/livetext/processor.rb +0 -88
  74. data/plugin/markdown.rb +0 -43
@@ -0,0 +1,914 @@
1
+ require 'ostruct'
2
+ require 'pp'
3
+ require 'date'
4
+ require 'find'
5
+ require 'fileutils' # Added for FileUtils
6
+
7
+ # require 'pathmagic'
8
+ # require 'processing'
9
+
10
+
11
+ ####
12
+ #### NOTE: Much of this was salvaged from liveblog.rb (for Runeblog support)
13
+ #### Double-commented stuff *may* be useful if ported.
14
+ ####
15
+
16
+ =begin
17
+ ChatGPT recommends minimal metadata:
18
+
19
+ Key Purpose
20
+ id Unique numeric or UUID identifier for the post (e.g. 0123)
21
+ title Human-readable title for display and slug generation
22
+ date Original creation date
23
+ updated (Optional) Last modified or published date
24
+ slug URL-safe identifier (usually derived from title, e.g. my-first-post)
25
+ views (Optional) Names of views this post belongs to
26
+ status e.g., draft, published, archived
27
+ tags List of tags or categories
28
+ blurb (Optional) Short summary or excerpt
29
+
30
+ Some will go into meta.lt3 - I will use: id, title, created, views
31
+
32
+ =end
33
+
34
+ # Dot commands:
35
+
36
+ def page_title(args, data)
37
+ setvar("page.title", data)
38
+ end
39
+
40
+ def copyright
41
+ author = Livetext::Vars["author"]
42
+ year = Time.now.year
43
+ setvar("page.copyright", "© #{author} #{year}")
44
+ end
45
+
46
+ def title(args, data)
47
+ setvar("post.title", data)
48
+ end
49
+
50
+ def created
51
+ setvar("post.created", Time.now.strftime("%Y-%m-%d-%H-%M-%S"))
52
+ end
53
+
54
+ def last_updated
55
+ pub_date = Livetext::Vars["post.created"] || "unknown date"
56
+ api.out "<p><em>Published: #{pub_date}</em></p>"
57
+ end
58
+
59
+ def wordcount
60
+ text = File.read(Livetext::Vars[:File])
61
+ words = text.split
62
+ setvar("wordcount", words.size.to_s)
63
+ end
64
+
65
+ def stats
66
+ text = File.read(Livetext::Vars[:File])
67
+ words = text.split
68
+ word_count = words.size
69
+
70
+ # Calculate reading time (average 200 words per minute)
71
+ reading_time = (word_count / 200.0).ceil
72
+
73
+ # Calculate character count
74
+ char_count = text.length
75
+
76
+ # Set all the variables with file. prefix
77
+ setvar("file.wordcount", word_count.to_s)
78
+ setvar("file.readingtime", reading_time.to_s)
79
+ setvar("file.charcount", char_count.to_s)
80
+ end
81
+
82
+ def views(args, data)
83
+ setvar("post.views", data.strip)
84
+ end
85
+
86
+ def tags(args, data)
87
+ setvar("post.tags", data.strip)
88
+ end
89
+
90
+ def blurb(args, data)
91
+ setvar("post.blurb", data.strip)
92
+ end
93
+
94
+
95
+ # Old liveblog code:
96
+
97
+
98
+ ##################
99
+ # "dot" commands
100
+ ##################
101
+
102
+ def dropcap(args, data)
103
+ # Bad form: adds another HEAD
104
+ text = data
105
+ api.out " "
106
+ letter = text[0]
107
+ remain = text[1..-1]
108
+ api.out %[<div class='mydrop'>#{letter}</div>]
109
+ api.out %[<div style="padding-top: 1px">#{remain}]
110
+ end
111
+
112
+ def faq(args, data, body)
113
+ @faq_count ||= 0
114
+ api.out "<br>" if @faq_count == 0
115
+ @faq_count += 1
116
+ ques = data.chomp
117
+ ans = body.join("\n")
118
+ id = "faq#@faq_count"
119
+ api.out %[&nbsp;<a data-toggle="collapse" href="##{id}" role="button" aria-expanded="false" aria-controls="collapseExample"><font size=+3>&#8964;</font></a>]
120
+ api.out %[&nbsp;<b>#{ques}</b>]
121
+ api.out %[<div class="collapse" id="#{id}"><br><font size=+1>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;#{ans}</font></div>\n]
122
+ api.out "<br>" # unless @faq_count == 1
123
+ api.optional_blank_line
124
+ end
125
+
126
+ def quote(args, data, body)
127
+ # was _passthru??? via runeblog
128
+ api.out "<blockquote>"
129
+ api.out body.join(" ")
130
+ api.out "</blockquote>"
131
+ api.optional_blank_line
132
+ end
133
+
134
+ # Move elsewhere later!
135
+ # was _passthru??? via runeblog
136
+ def h1(args, data); api.out "<h1>#{data}</h1>"; end
137
+ def h2(args, data); api.out "<h2>#{data}</h2>"; end
138
+ def h3(args, data); api.out "<h3>#{data}</h3>"; end
139
+ def h4(args, data); api.out "<h4>#{data}</h4>"; end
140
+ def h5(args, data); api.out "<h5>#{data}</h5>"; end
141
+ def h6(args, data); api.out "<h6>#{data}</h6>"; end
142
+ def hr(args, data); api.out "<hr>"; end
143
+
144
+ def nlist(args, data, body)
145
+ api.out "<ol>"
146
+ body.each {|line| api.out "<li>#{line}</li>" }
147
+ api.out "</ol>"
148
+ api.optional_blank_line
149
+ end
150
+
151
+ def list(args, data, body)
152
+ api.out "<ul>"
153
+ body.each {|line| api.out "<li>#{line}</li>" }
154
+ api.out "</ul>"
155
+ api.optional_blank_line
156
+ end
157
+
158
+ def list!(args, data, body)
159
+ api.out "<ul>"
160
+ lines = body.each
161
+ loop do
162
+ line = lines.next
163
+ line = api.format(line)
164
+ if line[0] == " "
165
+ api.out line
166
+ else
167
+ api.out "<li>#{line}</li>"
168
+ end
169
+ end
170
+ api.out "</ul>"
171
+ api.optional_blank_line
172
+ end
173
+
174
+ ### inset
175
+
176
+ def inset(args, data, body)
177
+ lines = body
178
+ box = ""
179
+ output = []
180
+ lines.each do |line|
181
+ line = line
182
+ case line[0]
183
+ when "/" # Only into inset
184
+ line[0] = ' '
185
+ box << line
186
+ line.replace(" ")
187
+ when "|" # Into inset and body
188
+ line[0] = ' '
189
+ box << line
190
+ output << line
191
+ else # Only into body
192
+ output << line
193
+ end
194
+ end
195
+ lr = args.first
196
+ wide = args[1] || "25"
197
+ stuff = "<div style='float:#{lr}; width: #{wide}%; padding:8px; padding-right:12px'>"
198
+ stuff << '<b><i>' + box + '</i></b></div>'
199
+ api.out "</p>" # kludge!! nopara
200
+ 0.upto(2) {|i| api.out output[i] }
201
+ api.out stuff
202
+ 3.upto(output.length-1) {|i| _passthru output[i] }
203
+ api.out "<p>" # kludge!! para
204
+ api.optional_blank_line
205
+ end
206
+
207
+ class Livetext::Functions
208
+
209
+ def asset(param)
210
+ begin
211
+ # Debug output to file
212
+ debug_file = "/tmp/asset_debug.log"
213
+ File.write(debug_file, "DEBUG: asset called with param: #{param}\n", mode: 'a')
214
+ root = Scriptorium::Repo.root
215
+ vname = Livetext::Vars.to_h[:View]
216
+ postid = Livetext::Vars.to_h[:"post.id"] # search post first
217
+ num = d4(postid)
218
+ File.write(debug_file, "DEBUG: root=#{root}, vname=#{vname}, postid=#{postid}, num=#{num}\n", mode: 'a')
219
+
220
+ # Dump all Livetext variables
221
+ File.write(debug_file, "DEBUG: All Livetext::Vars:\n", mode: 'a')
222
+ Livetext::Vars.to_h.each do |key, value|
223
+ File.write(debug_file, " #{key.inspect} => #{value.inspect}\n", mode: 'a')
224
+ end
225
+
226
+ # Define search paths and their corresponding output paths (in priority order)
227
+ search_paths = []
228
+
229
+ # Add post assets if we have a post ID (highest priority)
230
+ if num
231
+ search_paths << ["#{root}/posts/#{num}/assets/#{param}", "assets/#{num}/#{param}"]
232
+ end
233
+
234
+ # Add view assets
235
+ search_paths << ["#{root}/views/#{vname}/assets/#{param}", "assets/#{param}"]
236
+
237
+ # Add global assets
238
+ search_paths << ["#{root}/assets/#{param}", "assets/#{param}"]
239
+
240
+ # Add library assets (lowest priority)
241
+ search_paths << ["#{root}/assets/library/#{param}", "assets/#{param}"]
242
+
243
+ # Search for the asset
244
+ search_paths.each do |source_path, output_path|
245
+ File.write(debug_file, "DEBUG: Checking #{source_path} -> #{output_path}\n", mode: 'a')
246
+ if File.exist?(source_path)
247
+ File.write(debug_file, "DEBUG: Found asset at #{source_path}\n", mode: 'a')
248
+ # Copy to output directory
249
+ output_dir = "#{root}/views/#{vname}/output/assets"
250
+ if output_path.start_with?("assets/#{num}/")
251
+ # Post assets go in subdirectory
252
+ output_dir += "/#{num}"
253
+ end
254
+ FileUtils.mkdir_p(output_dir)
255
+ FileUtils.cp(source_path, "#{output_dir}/#{param}")
256
+ File.write(debug_file, "DEBUG: Returning #{output_path}\n", mode: 'a')
257
+ return output_path
258
+ end
259
+ end
260
+
261
+ # Asset not found - generate placeholder SVG
262
+ File.write(debug_file, "DEBUG: Asset not found, generating placeholder\n", mode: 'a')
263
+ placeholder_svg = generate_missing_asset_svg(param, width: 200, height: 150)
264
+ placeholder_dir = "#{root}/views/#{vname}/output/assets/missing"
265
+ FileUtils.mkdir_p(placeholder_dir)
266
+ File.write("#{placeholder_dir}/#{param}.svg", placeholder_svg)
267
+ File.write(debug_file, "DEBUG: Returning assets/missing/#{param}.svg\n", mode: 'a')
268
+ return "assets/missing/#{param}.svg"
269
+ rescue => e
270
+ # Return error message for debugging
271
+ return "[Asset error: #{e.message}\n#{e.backtrace.join("\n")}]"
272
+ end
273
+ end
274
+
275
+ def image_asset(param)
276
+ asset_path = asset(param)
277
+ "<img src=\"#{asset_path}\" alt=\"#{param}\">"
278
+ end
279
+
280
+ def generate_missing_asset_svg(filename, width: 200, height: 150)
281
+ # Truncate filename if too long for display
282
+ display_name = filename.length > 20 ? filename[0..16] + "..." : filename
283
+
284
+ # Generate SVG with broken image icon and filename
285
+ svg = <<~SVG
286
+ <svg width="#{width}" height="#{height}" xmlns="http://www.w3.org/2000/svg">
287
+ <!-- Background -->
288
+ <rect fill="#f8f9fa" stroke="#ddd" stroke-width="1" width="#{width}" height="#{height}" rx="4"/>
289
+
290
+ <!-- Broken image icon -->
291
+ <g transform="translate(#{width/2}, #{height/2 - 20})">
292
+ <!-- Image frame -->
293
+ <rect x="-15" y="-10" width="30" height="20" fill="none" stroke="#999" stroke-width="1"/>
294
+ <!-- Broken corner -->
295
+ <path d="M 15 -10 L 25 -20 M 15 -10 L 25 0" stroke="#999" stroke-width="1" fill="none"/>
296
+ <!-- Image icon -->
297
+ <rect x="-12" y="-7" width="24" height="14" fill="#e9ecef"/>
298
+ <circle cx="-5" cy="-2" r="2" fill="#999"/>
299
+ <polygon points="-8,8 -2,2 2,6 8,0" fill="#999"/>
300
+ </g>
301
+
302
+ <!-- Filename -->
303
+ <text x="#{width/2}" y="#{height/2 + 15}" text-anchor="middle" fill="#666" font-family="Arial, sans-serif" font-size="11">
304
+ #{display_name}
305
+ </text>
306
+
307
+ <!-- "Asset not found" message -->
308
+ <text x="#{width/2}" y="#{height/2 + 30}" text-anchor="middle" fill="#999" font-family="Arial, sans-serif" font-size="9">
309
+ Asset not found
310
+ </text>
311
+ </svg>
312
+ SVG
313
+
314
+ svg.strip
315
+ end
316
+
317
+ def d4(num)
318
+ "%04d" % num.to_i
319
+ end
320
+
321
+ def br(n="1")
322
+ # Thought: Maybe make a way for functions to "simply" call the
323
+ # dot command of the same name?? Is this trivial??
324
+ n = n.empty? ? 1 : n.to_i
325
+ "<br>"*n
326
+ end
327
+
328
+ def h1(param); "<h1>#{param}</h1>"; end
329
+ def h2(param); "<h2>#{param}</h2>"; end
330
+ def h3(param); "<h3>#{param}</h3>"; end
331
+ def h4(param); "<h4>#{param}</h4>"; end
332
+ def h5(param); "<h5>#{param}</h5>"; end
333
+ def h6(param); "<h6>#{param}</h6>"; end
334
+
335
+ def image(param)
336
+ "<img src='#{param}'></img>"
337
+ end
338
+ end
339
+
340
+ # Removed old wordcount function - replaced with dot command below
341
+
342
+ def _passthru(line)
343
+ return if line.nil?
344
+ line = _format(line)
345
+ api.out line + "\n"
346
+ api.out "<p>" if line.empty? && ! api.nopara
347
+ end
348
+
349
+ def _passthru_noline(line)
350
+ return if line.nil?
351
+ line = _format(line)
352
+ api.out line
353
+ api.out "<p>" if line.empty? && ! api.nopara
354
+ end
355
+
356
+ ## def backlink
357
+ ## log!(enter: __method__)
358
+ ## api.out %[<br><a href="javascript:history.go(-1)">[Back]</a>]
359
+ ## end
360
+ ##
361
+ ## def code
362
+ ## log!(enter: __method__)
363
+ ## lines = api.body # _text
364
+ ## api.out "<font size=+1><pre>\n#{lines}\n</pre></font>"
365
+ ## end
366
+ ##
367
+ ## def _read_navbar_data
368
+ ## log!(enter: __method__)
369
+ ## vdir = @blog.root/:views/@blog.view
370
+ ## dir = vdir/"themes/standard/banner/navbar/"
371
+ ## datafile = dir/"list.data"
372
+ ## _get_data(datafile)
373
+ ## end
374
+ ##
375
+ ## def banner
376
+ ## log!(enter: __method__)
377
+ ## count = 0
378
+ ## bg = "white" # outside loop
379
+ ## wide = nil
380
+ ## high = 250
381
+ ## str2 = ""
382
+ ## navbar = nil
383
+ ## # vdir = @blog.root/:views/@blog.view
384
+ ## lines = api.body.to_a
385
+ ##
386
+ ## lines.each do |line|
387
+ ## count += 1
388
+ ## tag, *data = line.split
389
+ ## data ||= []
390
+ ## deps = [@blog.view.globals[:ViewDir]/"global.lt3"]
391
+ ## case tag
392
+ ## when "width"; wide = data[0]
393
+ ## when "height"; high = data[0]
394
+ ## when "bgcolor"; bg = data[0] || "white"
395
+ ## when "image"
396
+ ## image = data[0] || "banner.jpg"
397
+ ## image = "banner"/image
398
+ ## wide = data[0]
399
+ ## width = wide ? "width=#{wide}" : ""
400
+ ## str2 << " <td><img src=#{image} #{width} height=#{high}></img></td>" + "\n"
401
+ ## when "svg_title"
402
+ ## stuff, hash = _svg_title(*data)
403
+ ## wide = hash["width"]
404
+ ## str2 << " <td width=#{wide}>#{stuff}</td>" + "\n"
405
+ ## when "text"
406
+ ## data[0] ||= "top.html"
407
+ ## file = "banner"/data[0]
408
+ ## if ! File.exist?(file)
409
+ ## src = file.sub(/html$/, "lt3")
410
+ ## if File.exist?(src)
411
+ ## preprocess src: src, dst: file, deps: deps, call: ".nopara", vars: @blog.view.globals
412
+ ## else
413
+ ## raise FoundNeither(file, src)
414
+ ## end
415
+ ## end
416
+ ## str2 << "<td>" + File.read(file) + "</td>" + "\n"
417
+ ## when "navbar"
418
+ ## navbar = _make_navbar # horiz is default
419
+ ## when "vnavbar"
420
+ ## navbar = _make_navbar(:vert)
421
+ ## when "break"
422
+ ## str2 << " </tr>\n <tr>" + "\n"
423
+ ## else
424
+ ## str2 << " '#{tag}' isn't known" + "\n"
425
+ ## end
426
+ ## end
427
+ ## api.out <<~HTML
428
+ ## <table width=100% bgcolor=#{bg}>
429
+ ## <tr>
430
+ ## #{str2}
431
+ ## </tr>
432
+ ## </table>
433
+ ## HTML
434
+ ## api.out navbar if navbar
435
+ ## rescue => err
436
+ ## STDERR.puts "err = #{err}"
437
+ ## STDERR.puts err.backtrace.join("\n") if err.respond_to?(:backtrace)
438
+ ## end
439
+ ##
440
+ ## def _svg_title(*args)
441
+ ## log!(enter: __method__)
442
+ ## width = "95%"
443
+ ## height = 90
444
+ ## # bgcolor = "black"
445
+ ## style = nil
446
+ ## size = ""
447
+ ## font = "sans-serif"
448
+ ## color = "white"
449
+ ## xy = "5,5"
450
+ ## align = "center"
451
+ ## style2 = nil
452
+ ## size2 = ""
453
+ ## font2 = "sans-serif"
454
+ ## color2 = "white"
455
+ ## xy2 = "5,5"
456
+ ## align2 = "center"
457
+ ##
458
+ ## e = args.each
459
+ ## hash = {} # TODO get rid of hash??
460
+ ##
461
+ ## valid = %w[width height bgcolor style size font color xy
462
+ ## align style2 size2 font2 color2 xy2 align2]
463
+ ## os = OpenStruct.new
464
+ ## loop do
465
+ ## arg = e.next
466
+ ## arg = arg.chop
467
+ ## raise "Don't know '#{arg}'" unless valid.include?(arg)
468
+ ## os.send(arg+"=", e.next)
469
+ ## end
470
+ ## x, y = xy.split(",")
471
+ ## x2, y2 = xy2.split(",")
472
+ ## names = %w[x y x2 y2] + valid
473
+ ## names.each {|name| hash[name] = os.send(name) }
474
+ ## result = <<~HTML
475
+ ## <svg width="#{width}" height="#{height}"
476
+ ## viewBox="0 0 #{width} #{height}">
477
+ ## <defs>
478
+ ## <linearGradient id="grad1" x1="100%" y1="100%" x2="0%" y2="100%">
479
+ ## <stop offset="0%" style="stop-color:rgb(198,198,228);stop-opacity:1" />
480
+ ## <stop offset="100%" style="stop-color:rgb(30,30,50);stop-opacity:1" />
481
+ ## </linearGradient>
482
+ ## </defs>
483
+ ## <style>
484
+ ## .title { font: #{style} #{size} #{font}; fill: #{color} }
485
+ ## .subtitle { font: #{style2} #{size2} #{font2}; fill: #{color2} }
486
+ ## </style>
487
+ ## <rect x="10" y="10" rx="10" ry="10" width="#{width}" height="#{height}" fill="url(#grad1)"/>
488
+ ## <text text-anchor="#{align}" x="#{x}" y="#{y}" class="title">#{Livetext::Vars["view.title"]} </text>
489
+ ## <text text-anchor="#{align2}" x="#{x2}" y="#{y2}" class="subtitle">#{Livetext::Vars["view.subtitle"]} </text>
490
+ ## </svg>
491
+ ## <!-- ^ how does syntax highlighting get messed up? </svg> -->
492
+ ## HTML
493
+ ## [result, hash]
494
+ ## end
495
+ ## def categories # does nothing right now
496
+ ## log!(enter: __method__)
497
+ ## end
498
+ ##
499
+ ## def style
500
+ ## log!(enter: __method__)
501
+ ## fname = api.args[0]
502
+ ## _passthru %[<link rel="stylesheet" href="???/etc/#{fname}')">]
503
+ ## end
504
+
505
+
506
+ ## def pin
507
+ ## log!(enter: __method__)
508
+ ## raise NoPostCall unless @meta
509
+ ## api.debug "args = #{api.args}" # verify only valid views?
510
+ ## pinned = api.args
511
+ ## @meta.pinned = pinned
512
+ ## pinned.each do |pinview|
513
+ ## dir = @blog.root/:views/pinview/"widgets/pinned/"
514
+ ## datafile = dir/"list.data"
515
+ ## pins = _get_data?(datafile)
516
+ ## pins << "#{@meta.num} #{@meta.title}\n"
517
+ ## pins.uniq!
518
+ ## File.open(datafile, "w") {|out| pins.each {|pin| out.puts pin } }
519
+ ## end
520
+ ## api.optional_blank_line
521
+ ## rescue => err
522
+ ## STDERR.puts "err = #{err}"
523
+ ## STDERR.puts err.backtrace.join("\n") if err.respond_to?(:backtrace)
524
+ ## end
525
+ ##
526
+ ## def write_post
527
+ ## log!(enter: __method__)
528
+ ## raise NoPostCall unless @meta
529
+ ## @meta.views = @meta.views.join(" ") if @meta.views.is_a? Array
530
+ ## @meta.tags = @meta.tags.join(" ") if @meta.tags.is_a? Array
531
+ ## _write_metadata
532
+ ## rescue => err
533
+ ## puts "err = #{err}"
534
+ ## puts err.backtrace.join("\n") if err.respond_to?(:backtrace)
535
+ ## end
536
+ ##
537
+ ## def teaser
538
+ ## log!(enter: __method__)
539
+ ## raise NoPostCall unless @meta
540
+ ## text = api.body.join("\n")
541
+ ## @meta.teaser = text
542
+ ## setvar :teaser, @meta.teaser
543
+ ## if api.args[0] == "dropcap" # FIXME doesn't work yet!
544
+ ## letter, remain = text[0], text[1..-1]
545
+ ## api.out %[<div class='mydrop'>#{letter}</div>]
546
+ ## api.out %[<div style="padding-top: 1px">#{remain}] + "\n"
547
+ ## else
548
+ ## api.out @meta.teaser + "\n"
549
+ ## end
550
+ ## end
551
+ ##
552
+ ## def finalize
553
+ ## log!(str: "Now exiting livetext processing...")
554
+ ## return unless @meta
555
+ ## return @meta if @blog.nil?
556
+ ##
557
+ ## @slug = @blog.make_slug(@meta)
558
+ ## slug_dir = @slug
559
+ ## @postdir = @blog.view.dir/:posts/slug_dir
560
+ ## write_post
561
+ ## @meta
562
+ ## end
563
+ ##
564
+ ## def head # Does NOT output <head> tags
565
+ ## log!(enter: __method__)
566
+ ## args = api.args
567
+ ## args.each do |inc|
568
+ ## self.data = inc
569
+ ## dot_include
570
+ ## end
571
+ ##
572
+ ## # Depends on vars: title, desc, host
573
+ ## defaults = { "charset" => %[<meta charset="utf-8">],
574
+ ## "http-equiv" => %[<meta http-equiv="X-UA-Compatible" content="IE=edge">],
575
+ ## "title" => %[<title>\n #{_var("view.title")} | #{_var("view.subtitle")}\n </title>],
576
+ ## "generator" => %[<meta name="generator" content="Runeblog v #@version">],
577
+ ## "og:title" => %[<meta property="og:title" content="#{_var("view.title")}">],
578
+ ## "og:locale" => %[<meta property="og:locale" content="#{_var(:locale)}">],
579
+ ## "description" => %[<meta name="description" content="#{_var("view.subtitle")}">],
580
+ ## "og:description" => %[<meta property="og:description" content="#{_var("view.subtitle")}">],
581
+ ## "linkc" => %[<link rel="canonical" href="#{_var(:host)}">],
582
+ ## "og:url" => %[<meta property="og:url" content="#{_var(:host)}">],
583
+ ## "og:site_name" => %[<meta property="og:site_name" content="#{_var("view.title")}">],
584
+ ## # "style" => %[<link rel="stylesheet" href="etc/blog.css">],
585
+ ## # ^ FIXME
586
+ ## "feed" => %[<link type="application/atom+xml" rel="alternate"] +
587
+ ## %[href="#{_var(:host)}/feed.xml" title="#{_var("view.title")}">],
588
+ ## "favicon" => %[<link rel="shortcut icon" type="image/x-icon" href="etc/favicon.ico">\n ] +
589
+ ## %[<link rel="apple-touch-icon" href="etc/favicon.ico">]
590
+ ## }
591
+ ##
592
+ ## result = {}
593
+ ## lines = api.body
594
+ ## lines.each do |line|
595
+ ## line.chomp
596
+ ## word, remain = line.split(" ", 2)
597
+ ## case word
598
+ ## when "viewport"
599
+ ## result["viewport"] = %[<meta name="viewport" content="#{remain}">]
600
+ ## when "script" # FIXME this is broken
601
+ ## file = remain
602
+ ## text = File.read(file)
603
+ ## result["script"] = Livetext.new.transform(text)
604
+ ## when "style"
605
+ ## result["style"] = %[<link rel="stylesheet" href="etc/#{remain}">]
606
+ ## # Later: allow other overrides
607
+ ## when ""; break
608
+ ## else
609
+ ## if defaults[word]
610
+ ## result[word] = %[<meta property="#{word}" content="#{remain}">]
611
+ ## else
612
+ ## puts "Unknown tag '#{word}'"
613
+ ## end
614
+ ## end
615
+ ## end
616
+ ## hash = defaults.dup.update(result) # FIXME collisions?
617
+ ##
618
+ ## hash.each_value {|x| api.out " " + x }
619
+ ## end
620
+ ##
621
+ ## ########## newer stuff...
622
+ ##
623
+ ## def meta
624
+ ## log!(enter: __method__)
625
+ ## args = api.args
626
+ ## enum = args.each
627
+ ## str = "<meta"
628
+ ## arg = enum.next
629
+ ## loop do
630
+ ## if arg.end_with?(":")
631
+ ## str << " " << arg[0..-2] << "="
632
+ ## a2 = enum.next
633
+ ## str << %["#{a2}"]
634
+ ## else
635
+ ## STDERR.puts "=== meta error?"
636
+ ## end
637
+ ## arg = enum.next
638
+ ## end
639
+ ## str << ">"
640
+ ## api.out str
641
+ ## end
642
+ ##
643
+ ## def _make_class_name(app)
644
+ ## log!(enter: __method__)
645
+ ## if app =~ /[-_]/
646
+ ## words = app.split(/[-_]/)
647
+ ## name = words.map(&:capitalize).join
648
+ ## else
649
+ ## name = app.capitalize
650
+ ## end
651
+ ## return name
652
+ ## end
653
+ ##
654
+ ## def _load_local(widget)
655
+ ## log!(enter: __method__)
656
+ ## rclass = _make_class_name(widget)
657
+ ## found = require("./#{widget}")
658
+ ## code = found ? ::RuneBlog::Widget.class_eval(rclass) : nil
659
+ ## code
660
+ ## rescue => err
661
+ ## STDERR.puts err.to_s
662
+ ## STDERR.puts err.backtrace.join("\n") if err.respond_to?(:backtrace)
663
+ ## sleep 6; RubyText.stop
664
+ ## exit
665
+ ## end
666
+ ##
667
+ ## def _handle_standard_widget(tag)
668
+ ## log!(enter: __method__)
669
+ ## wtag = "../../widgets"/tag
670
+ ## Dir.chdir(wtag) do
671
+ ## code = _load_local(tag)
672
+ ## if code
673
+ ## widget = code.new(@blog)
674
+ ## widget.build
675
+ ## end
676
+ ## end
677
+ ## end
678
+ ##
679
+ ## def sidebar
680
+ ## log!(enter: __method__)
681
+ ## api.debug "--- handling sidebar\r"
682
+ ## $debug = true
683
+ ## if api.args.include? "off"
684
+ ## api.body { } # iterate, do nothing
685
+ ## return
686
+ ## end
687
+ ##
688
+ ## api.out %[<div class="col-lg-3 col-md-3 col-sm-3 col-xs-12">]
689
+ ##
690
+ ## standard = %w[pinned pages links news]
691
+ ##
692
+ ## lines = api.body.to_a
693
+ ## lines.each do |token|
694
+ ## tag = token.chomp.strip.downcase
695
+ ## wtag = "../../widgets"/tag
696
+ ## raise CantFindWidgetDir(wtag) unless Dir.exist?(wtag)
697
+ ## tcard = "#{tag}-card.html"
698
+ ## case
699
+ ## when standard.include?(tag)
700
+ ## _handle_standard_widget(tag)
701
+ ## else
702
+ ## raise "Nonstandard widget?"
703
+ ## end
704
+ ##
705
+ ## api.include_file wtag/tcard
706
+ ## end
707
+ ## api.out %[</div>]
708
+ ## rescue => err
709
+ ## puts "err = #{err}"
710
+ ## puts err.backtrace.join("\n") if err.respond_to?(:backtrace)
711
+ ## if RubyText.started?
712
+ ## puts "Sleeping 6..."
713
+ ## sleep 6; RubyText.stop
714
+ ## end
715
+ ## puts "Exiting.\n "
716
+ ## exit
717
+ ## end
718
+ ##
719
+ ## def stylesheet
720
+ ## log!(enter: __method__)
721
+ ## lines = api.body
722
+ ## url = lines[0]
723
+ ## integ = lines[1]
724
+ ## cross = lines[2] || "anonymous"
725
+ ## api.out %[<link rel="stylesheet" href="#{url}" integrity="#{integ}" crossorigin="#{cross}"></link>]
726
+ ## end
727
+ ##
728
+ ## def script
729
+ ## log!(enter: __method__)
730
+ ## lines = api.body
731
+ ## url = lines[0]
732
+ ## integ = lines[1]
733
+ ## cross = lines[2] || "anonymous"
734
+ ## api.out %[<script src="#{url}" integrity="#{integ}" crossorigin="#{cross}"></script>]
735
+ ## end
736
+
737
+ ## ###### experimental...
738
+ ##
739
+ ## class Livetext::Functions
740
+ ## def _var(name)
741
+ ## ::Livetext::Vars[name] || "[:#{name} is undefined]"
742
+ ## end
743
+ ## end
744
+ ##
745
+ ## ###
746
+ ##
747
+ ##
748
+ ## def tag_cloud
749
+ ## log!(enter: __method__)
750
+ ## title = api.data
751
+ ## title = "Tag Cloud" if title.empty?
752
+ ## open = <<-HTML
753
+ ## <div class="card mb-3">
754
+ ## <div class="card-body">
755
+ ## <h5 class="card-title">
756
+ ## <button type="button" class="btn btn-primary" data-toggle="collapse" data-target="#tag-cloud">+</button>
757
+ ## #{title}
758
+ ## </h5>
759
+ ## <div class="collapse" id="tag-cloud">
760
+ ## HTML
761
+ ## api.out open
762
+ ## api.body do |line|
763
+ ## line.chomp!
764
+ ## url, classname, cdata = line.split(",", 3)
765
+ ## main = _main(url)
766
+ ## api.out %[<a #{main} class="#{classname}">#{cdata}</a>]
767
+ ## end
768
+ ## close = %[ </div>\n </div>\n </div>]
769
+ ## api.out close
770
+ ## end
771
+ ##
772
+ ## def vnavbar
773
+ ## log!(enter: __method__)
774
+ ## return _make_navbar(:vert)
775
+ ## end
776
+ ##
777
+ ## def hnavbar
778
+ ## log!(enter: __method__)
779
+ ## return _make_navbar # horiz is default
780
+ ## end
781
+ ##
782
+ ## def navbar
783
+ ## log!(enter: __method__)
784
+ ## return _make_navbar # horiz is default
785
+ ## end
786
+ ##
787
+ ## def _make_navbar(orient = :horiz)
788
+ ## log!(enter: __method__)
789
+ ## vdir = @root/:views/@blog.view
790
+ ## # title = _var("view.title")
791
+ ##
792
+ ## if orient == :horiz
793
+ ## name = "navbar.html"
794
+ ## li1, li2 = "", ""
795
+ ## extra = "navbar-expand-lg"
796
+ ## list1 = list2 = ""
797
+ ## else
798
+ ## name = "vnavbar.html"
799
+ ## li1, li2 = '<li class="nav-item">', "</li>"
800
+ ## extra = ""
801
+ ## list1, list2 = '<l class="navbar-nav mr-auto">', "</ul>"
802
+ ## end
803
+ ##
804
+ ## start = <<-HTML
805
+ ## <table><tr><td>
806
+ ## <nav class="navbar #{extra} navbar-light bg-light">
807
+ ## #{list1}
808
+ ## HTML
809
+ ## finish = <<-HTML
810
+ ## #{list2}
811
+ ## </nav>
812
+ ## </td></tr></table>
813
+ ## HTML
814
+ ##
815
+ ## html_file = @blog.root/:views/@blog.view/"themes/standard/banner/navbar"/name
816
+ ## output = File.new(html_file, "w")
817
+ ## output.puts start
818
+ ## lines = _read_navbar_data
819
+ ## lines = ["index Home"] + lines unless api.args.include?("nohome")
820
+ ## lines.each do |line|
821
+ ## basename, cdata = line.chomp.strip.split(" ", 2)
822
+ ## full = :banner/:navbar/basename+".html"
823
+ ## href_main = _main(full)
824
+ ## if basename == "index" # special case
825
+ ## output.puts %[#{li1} <a class="nav-link" href="index.html">#{cdata}<span class="sr-only">(current)</span></a> #{li2}]
826
+ ## else
827
+ ## dir = @blog.root/:views/@blog.view/"themes/standard/banner/navbar"
828
+ ## dest = vdir/"remote/banner/navbar"/basename+".html"
829
+ ## preprocess cwd: dir, src: basename, dst: dest, call: ".nopara", vars: @blog.view.globals # , debug: true
830
+ ## output.puts %[#{li1} <a class="nav-link" #{href_main}>#{cdata}</a> #{li2}]
831
+ ## end
832
+ ## end
833
+ ## output.puts finish
834
+ ## output.close
835
+ ## return File.read(html_file)
836
+ ## end
837
+ ##
838
+ ##
839
+ ## ##################
840
+ ## # helper methods
841
+ ## ##################
842
+ ##
843
+ ## def _html_body(file, css = nil)
844
+ ## log!(enter: __method__)
845
+ ## file.puts "<html>"
846
+ ## if css
847
+ ## file.puts " <head>"
848
+ ## file.puts " <style>\n#{css}\n </style>"
849
+ ## file.puts " </head>"
850
+ ## end
851
+ ## file.puts " <body>"
852
+ ## yield
853
+ ## file.puts " </body>\n</html>"
854
+ ## end
855
+ ##
856
+ ## def _errout(*args)
857
+ ## log!(enter: __method__)
858
+ ## ::STDERR.puts args
859
+ ## end
860
+ ## def _post_lookup(postid) # side-effect
861
+ ## log!(enter: __method__)
862
+ ## # .. = templates, ../.. = views/thisview
863
+ ##
864
+ ## view = @blog.view
865
+ ## vdir = view.dir rescue "NONAME"
866
+ ## setvar("View", view.name)
867
+ ## setvar("ViewDir", @blog.root/:views/view.name)
868
+ ## tmp = File.new("/tmp/PL-#{Time.now.to_i}.txt", "w")
869
+ ## tmp.puts "_post_lookup: blog.view = #{@blog.view.inspect}"
870
+ ## tmp.puts "_post_lookup: vdir = #{vdir}"
871
+ ## dir_posts = @vdir/:posts
872
+ ## posts = Dir.entries(dir_posts).grep(/^\d\d\d\d/).map {|x| dir_posts/x }
873
+ ## posts.select! {|x| File.directory?(x) }
874
+ ##
875
+ ## tmp.puts "_post_lookup: postid = #{postid}"
876
+ ## tmp.puts "_post_lookup: posts = \n#{posts.inspect}"
877
+ ## tmp.close
878
+ ## posts = posts.select {|x| File.basename(x).to_i == postid }
879
+ ## postdir = exactly_one(posts, posts.inspect)
880
+ ## vp = RuneBlog::ViewPost.new(@blog.view, postdir)
881
+ ## vp
882
+ ## end
883
+ ##
884
+ ## def _card_generic(card_title:, middle:, extra: "")
885
+ ## log!(enter: __method__)
886
+ ## front = <<-HTML
887
+ ## <div class="card #{extra} mb-3">
888
+ ## <div class="card-body">
889
+ ## <h5 class="card-title">#{card_title}</h5>
890
+ ## HTML
891
+ ##
892
+ ## tail = <<-HTML
893
+ ## </div>
894
+ ## </div>
895
+ ## HTML
896
+ ## text = front + middle + tail
897
+ ## api.out text + "\n "
898
+ ## end
899
+ ##
900
+ ## def _var(name) # FIXME scope issue!
901
+ ## log!(enter: __method__)
902
+ ## ::Livetext::Vars[name] || "[:#{name} is undefined]"
903
+ ## end
904
+ ##
905
+ ## def _main(url)
906
+ ## log!(enter: __method__)
907
+ ## %[href="javascript: void(0)" onclick="javascript:open_main('#{url}')"]
908
+ ## end
909
+ ##
910
+ ## def _blank(url)
911
+ ## log!(enter: __method__)
912
+ ## %[href='#{url}' target='blank']
913
+ ## end
914
+ ##