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.
- checksums.yaml +4 -4
- data/imports/bookish.rb +3 -3
- data/lib/livetext/ast/show_ast_clean.rb +10 -0
- data/lib/livetext/ast/show_ast_result.rb +60 -0
- data/lib/livetext/ast/show_raw_arrays.rb +13 -0
- data/lib/livetext/ast.rb +464 -0
- data/lib/livetext/ast_to_html.rb +32 -0
- data/lib/livetext/core.rb +110 -53
- data/lib/livetext/errors.rb +1 -0
- data/lib/livetext/expansion.rb +21 -21
- data/lib/livetext/formatter.rb +70 -200
- data/lib/livetext/formatter_component.rb +189 -0
- data/lib/livetext/function_registry.rb +163 -0
- data/lib/livetext/functions.rb +26 -0
- data/lib/livetext/handler/mixin.rb +53 -0
- data/lib/livetext/helpers.rb +33 -16
- data/lib/livetext/reopen.rb +2 -0
- data/lib/livetext/skeleton.rb +0 -3
- data/lib/livetext/standard.rb +120 -72
- data/lib/livetext/userapi.rb +20 -1
- data/lib/livetext/variable_manager.rb +78 -0
- data/lib/livetext/variables.rb +9 -1
- data/lib/livetext/version.rb +1 -1
- data/lib/livetext.rb +9 -3
- data/plugin/booktool.rb +14 -14
- data/plugin/lt3scriptor.rb +914 -0
- data/plugin/mixin_functions_class.rb +33 -0
- data/test/snapshots/complex_body/expected-error.txt +0 -0
- data/test/snapshots/complex_body/expected-output.txt +8 -0
- data/test/snapshots/complex_body/source.lt3 +19 -0
- data/test/snapshots/debug_command/expected-error.txt +0 -0
- data/test/snapshots/debug_command/expected-output.txt +1 -0
- data/test/snapshots/debug_command/source.lt3 +3 -0
- data/test/snapshots/def_parameters/expected-error.txt +0 -0
- data/test/snapshots/def_parameters/expected-output.txt +21 -0
- data/test/snapshots/def_parameters/source.lt3 +44 -0
- data/test/snapshots/error_missing_end/match-error.txt +1 -1
- data/test/snapshots/functions_reflection/expected-error.txt +0 -0
- data/test/snapshots/functions_reflection/expected-output.txt +27 -0
- data/test/snapshots/functions_reflection/source.lt3 +5 -0
- data/test/snapshots/mixin_functions_class/expected-error.txt +0 -0
- data/test/snapshots/mixin_functions_class/expected-output.txt +20 -0
- data/test/snapshots/mixin_functions_class/mixin_functions_class.rb +33 -0
- data/test/snapshots/mixin_functions_class/source.lt3 +17 -0
- data/test/snapshots/multiple_functions/expected-error.txt +0 -0
- data/test/snapshots/multiple_functions/expected-output.txt +5 -0
- data/test/snapshots/multiple_functions/source.lt3 +16 -0
- data/test/snapshots/nested_includes/expected-error.txt +0 -0
- data/test/snapshots/nested_includes/expected-output.txt +68 -0
- data/test/snapshots/nested_includes/level2.inc +34 -0
- data/test/snapshots/nested_includes/level3.inc +20 -0
- data/test/snapshots/nested_includes/source.lt3 +49 -0
- data/test/snapshots/parameter_handling/expected-error.txt +0 -0
- data/test/snapshots/parameter_handling/expected-output.txt +7 -0
- data/test/snapshots/parameter_handling/source.lt3 +10 -0
- data/test/snapshots/subset.txt +1 -0
- data/test/snapshots/system_info/expected-error.txt +0 -0
- data/test/snapshots/system_info/match-output.txt +18 -0
- data/test/snapshots/system_info/source.lt3 +16 -0
- data/test/unit/all.rb +7 -0
- data/test/unit/ast.rb +90 -0
- data/test/unit/ast_directives.rb +104 -0
- data/test/unit/ast_variables.rb +71 -0
- data/test/unit/core_methods.rb +317 -0
- data/test/unit/formatter.rb +84 -0
- data/test/unit/formatter_component.rb +84 -0
- data/test/unit/function_registry.rb +132 -0
- data/test/unit/mixin_functions_class.rb +131 -0
- data/test/unit/stringparser.rb +14 -32
- data/test/unit/variable_manager.rb +71 -0
- metadata +51 -5
- data/imports/markdown.rb +0 -44
- data/lib/livetext/processor.rb +0 -88
- 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 %[ <a data-toggle="collapse" href="##{id}" role="button" aria-expanded="false" aria-controls="collapseExample"><font size=+3>⌄</font></a>]
|
120
|
+
api.out %[ <b>#{ques}</b>]
|
121
|
+
api.out %[<div class="collapse" id="#{id}"><br><font size=+1> #{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
|
+
##
|