asciidoctor-asciidoc 0.0.2.dev
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 +7 -0
- data/lib/asciidoctor-asciidoc/const.rb +8 -0
- data/lib/asciidoctor-asciidoc/conv-node.rb +74 -0
- data/lib/asciidoctor-asciidoc/linked-list.rb +126 -0
- data/lib/asciidoctor-asciidoc/unescape.rb +407 -0
- data/lib/asciidoctor-asciidoc/version.rb +3 -0
- data/lib/asciidoctor-asciidoc.rb +925 -0
- metadata +128 -0
@@ -0,0 +1,925 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require 'asciidoctor/converter'
|
4
|
+
require 'asciidoctor-asciidoc/conv-node'
|
5
|
+
require 'asciidoctor-asciidoc/unescape'
|
6
|
+
require 'asciidoctor-asciidoc/const'
|
7
|
+
|
8
|
+
class AsciiDoctorAsciiDocConverter < Asciidoctor::Converter::Base
|
9
|
+
|
10
|
+
include AsciiDoctorAsciiDocConst
|
11
|
+
|
12
|
+
MY_BACKEND = "asciidoc"
|
13
|
+
MY_FILETYPE = "asciidoc"
|
14
|
+
MY_EXTENSION = ".adoc"
|
15
|
+
|
16
|
+
ATTR_ID = "id"
|
17
|
+
ATTR_ROLE = "role"
|
18
|
+
ATTR_TITLE = "title"
|
19
|
+
ATTR_STYLE = "style"
|
20
|
+
ATTR_WINDOW = "window"
|
21
|
+
ATTR_LANGUAGE = "language"
|
22
|
+
ATTR_OPTS = "opts"
|
23
|
+
ATTR_DOC_FILE = "docfile"
|
24
|
+
ATTR_DOC_TITLE = "doctitle"
|
25
|
+
ATTR_DOC_TYPE = "doctype"
|
26
|
+
ATTR_ICONS_DIR = "iconsdir"
|
27
|
+
ATTR_IMAGES_DIR = "imagesdir"
|
28
|
+
ATTR_NAME = "name"
|
29
|
+
ATTR_TEXT_LABEL = "textlabel"
|
30
|
+
ATTR_COL_COUNT = "colcount"
|
31
|
+
ATTR_ROW_COUNT = "rowcount"
|
32
|
+
ATTR_TABLE_PC_WIDTH = "tablepcwidth"
|
33
|
+
ATTR_SEPARATOR = "separator"
|
34
|
+
ATTR_VALIGN = "valign"
|
35
|
+
ATTR_HALIGN = "halign"
|
36
|
+
|
37
|
+
TBL_STYLE_HEADER = "h"
|
38
|
+
|
39
|
+
STYLE_ARABIC = "arabic"
|
40
|
+
|
41
|
+
HALIGN_LEFT = "left"
|
42
|
+
HALIGN_RIGHT = "right"
|
43
|
+
HALIGN_CENTER = "center"
|
44
|
+
|
45
|
+
TYPE_ASCIIDOC = :asciidoc
|
46
|
+
TYPE_NONE = :none
|
47
|
+
TYPE_EMPHASIS = :emphasis
|
48
|
+
TYPE_HEADER = :header
|
49
|
+
TYPE_LITERAL = :literal
|
50
|
+
TYPE_MONOSPACE = :monospaced
|
51
|
+
TYPE_STRONG = :strong
|
52
|
+
TYPE_SINGLE = :single
|
53
|
+
TYPE_DOUBLE = :double
|
54
|
+
TYPE_MARK = :mark # italic
|
55
|
+
TYPE_SUBSCRIPT = :subscript # italic
|
56
|
+
TYPE_SUPERSCRIPT = :superscript # italic
|
57
|
+
|
58
|
+
ESC_INLINE_BRK = "#{ESC}2b#{ESC_E}" # +
|
59
|
+
ESC_HASH = "#{ESC}23#{ESC_E}" # #
|
60
|
+
ESC_BOLD = "#{ESC}2a2a#{ESC_E}" # **
|
61
|
+
ESC_MONO = "#{ESC}6060#{ESC_E}" # ``
|
62
|
+
ESC_START_SINGLE_QUOTE = "#{ESC}2760#{ESC_E}" # '`
|
63
|
+
ESC_END_SINGLE_QUOTE = "#{ESC}6027#{ESC_E}" # '`
|
64
|
+
ESC_START_DOUBLE_QUOTE = "#{ESC}2260#{ESC_E}" # '`
|
65
|
+
ESC_END_DOUBLE_QUOTE = "#{ESC}6022#{ESC_E}" # '`
|
66
|
+
ESC_ITALIC = "#{ESC}5f5f#{ESC_E}" # __
|
67
|
+
ESC_SUBSCRIPT = "#{ESC}7e#{ESC_E}" # ~
|
68
|
+
ESC_SUPERSCRIPT = "#{ESC}5e#{ESC_E}" # ^
|
69
|
+
|
70
|
+
VALIGN_TOP = "top"
|
71
|
+
VALIGN_BOTTOM = "bottom"
|
72
|
+
VALIGN_CENTER = "middle"
|
73
|
+
|
74
|
+
CFG_NO_LF = :no_new_line
|
75
|
+
CFG_COLLAPSE = :collapse
|
76
|
+
CFG_CONTENT = :content
|
77
|
+
CFG_DELIMITER = :delimiter
|
78
|
+
CFG_DEFAULT_ATTR = :default_attr
|
79
|
+
CFG_STYLE = :style
|
80
|
+
|
81
|
+
OPT_INCLUDE_EMPTY = :include_empty
|
82
|
+
OPT_FOR_BLOCK = :for_block
|
83
|
+
|
84
|
+
PARAGRAPH_CONFIG = {
|
85
|
+
CFG_COLLAPSE => { ATTR_STYLE => 1, ATTR_TITLE => 0},
|
86
|
+
CFG_CONTENT => -> (node) { node.content },
|
87
|
+
CFG_DELIMITER => "===="
|
88
|
+
}
|
89
|
+
|
90
|
+
LISTING_CONFIG = PARAGRAPH_CONFIG.merge(
|
91
|
+
{
|
92
|
+
CFG_COLLAPSE => PARAGRAPH_CONFIG[CFG_COLLAPSE].merge({ATTR_LANGUAGE=>2}),
|
93
|
+
CFG_DELIMITER => "----"
|
94
|
+
})
|
95
|
+
|
96
|
+
ADMONITION_CONFIG = PARAGRAPH_CONFIG.merge(
|
97
|
+
{
|
98
|
+
CFG_COLLAPSE => PARAGRAPH_CONFIG[CFG_COLLAPSE].merge({
|
99
|
+
ATTR_STYLE=>0,
|
100
|
+
ATTR_NAME=>0,
|
101
|
+
ATTR_TEXT_LABEL=>0 # name and text label are not documented, so
|
102
|
+
# I assume it's OK to throw them out...
|
103
|
+
}),
|
104
|
+
CFG_CONTENT => -> (node) { %(#{node.attr(ATTR_STYLE)}: #{node.content}) }
|
105
|
+
})
|
106
|
+
|
107
|
+
TABLE_CONFIG = PARAGRAPH_CONFIG.merge(
|
108
|
+
{
|
109
|
+
CFG_COLLAPSE => PARAGRAPH_CONFIG[CFG_COLLAPSE].merge(
|
110
|
+
{
|
111
|
+
ATTR_STYLE=>0,
|
112
|
+
ATTR_COL_COUNT=>0,
|
113
|
+
ATTR_ROW_COUNT=>0,
|
114
|
+
ATTR_TABLE_PC_WIDTH=>0,
|
115
|
+
}),
|
116
|
+
})
|
117
|
+
|
118
|
+
ROLE_BARE = "bare"
|
119
|
+
|
120
|
+
RX_NUM = /^[1-9][0-9]*$/
|
121
|
+
|
122
|
+
EmDashCharRefRx = /—(?:​)?/
|
123
|
+
|
124
|
+
LF = Asciidoctor::LF
|
125
|
+
|
126
|
+
ANCHOR_ATTRIBUTES = [ATTR_ID, ATTR_ROLE, ATTR_WINDOW, ATTR_OPTS].to_set
|
127
|
+
|
128
|
+
# https://docs.asciidoctor.org/asciidoc/latest/attributes/document-attributes-reference/
|
129
|
+
INTRINSIC_DOC_ATTRIBUTES = %w(
|
130
|
+
backend basebackend docdate docdatetime docdir docfile
|
131
|
+
docfilesuffix docname doctime docyear embedded filetype
|
132
|
+
htmlsyntax localdate localdatetime localtime localyear
|
133
|
+
outdir outfile outfilesuffix safe-mode-level safe-mode-name
|
134
|
+
safe-mode-unsafe safe-mode-safe safe-mode-server safe-mode-secure user-home
|
135
|
+
asciidoctor asciidoctor-version authorcount table-number
|
136
|
+
).to_set
|
137
|
+
# last line in INTRINSIC_DOC_ATTRIBUTES contains undocumented attributes
|
138
|
+
|
139
|
+
DEFAULT_DOC_ATTRIBUTES = {
|
140
|
+
"attribute-missing" => "skip",
|
141
|
+
"attribute-undefined" => "drop-line",
|
142
|
+
"appendix-caption" => "Appendix",
|
143
|
+
"appendix-refsig" => "Appendix",
|
144
|
+
"caution-caption" => "Caution",
|
145
|
+
"chapter-refsig" => "Chapter",
|
146
|
+
"example-caption" => "Example",
|
147
|
+
"figure-caption" => "Figure",
|
148
|
+
"important-caption" => "Important",
|
149
|
+
"last-update-label" => "Last updated",
|
150
|
+
"note-caption" => "Note",
|
151
|
+
"part-refsig" => "Part",
|
152
|
+
"section-refsig" => "Section",
|
153
|
+
"table-caption" => "Table",
|
154
|
+
"tip-caption" => "Tip",
|
155
|
+
"toc-title" => "Table of Contents",
|
156
|
+
"untitled-label" => "Untitled",
|
157
|
+
"version-label" => "Version",
|
158
|
+
"warning-caption" => "Warning",
|
159
|
+
"doctype" => "article",
|
160
|
+
"prewrap" => "",
|
161
|
+
"sectids" => "",
|
162
|
+
"toc-placement" => "auto", # undocumented
|
163
|
+
"notitle" => "", # incorrectly documented
|
164
|
+
"max-include-depth" => 64,
|
165
|
+
"max-attribute-value-size" => 4096,
|
166
|
+
"linkcss" => "", # incorrectly documented
|
167
|
+
"stylesdir" => "."
|
168
|
+
}
|
169
|
+
|
170
|
+
register_for MY_BACKEND.to_sym
|
171
|
+
|
172
|
+
def initialize(backend, opts = {})
|
173
|
+
@backend = backend
|
174
|
+
@config = []
|
175
|
+
@current_node = nil
|
176
|
+
@next_anchor = nil
|
177
|
+
init_backend_traits basebackend: MY_BACKEND, filetype: MY_FILETYPE, outfilesuffix: MY_EXTENSION
|
178
|
+
end
|
179
|
+
|
180
|
+
def convert(node, transform = node.node_name, opts = nil)
|
181
|
+
new_node = AsciiDoctorAsciiDocNode.new(parent: @current_node, node: node, transform: transform)
|
182
|
+
old_node = @current_node
|
183
|
+
@current_node.add_child(new_node) if @current_node
|
184
|
+
@current_node = new_node
|
185
|
+
unless @next_anchor.nil?
|
186
|
+
@current_node.anchor = @next_anchor
|
187
|
+
@next_anchor = nil
|
188
|
+
end
|
189
|
+
begin
|
190
|
+
return super
|
191
|
+
ensure
|
192
|
+
if @current_node.is_anchor
|
193
|
+
@next_anchor = @current_node
|
194
|
+
end
|
195
|
+
@current_node = old_node
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def convert_document(node)
|
200
|
+
|
201
|
+
push_config({})
|
202
|
+
|
203
|
+
doctype = node.doctype
|
204
|
+
|
205
|
+
dynamic_exclusions = %W(
|
206
|
+
backend-#{MY_BACKEND}-doctype-#{doctype}
|
207
|
+
doctype-#{doctype}
|
208
|
+
backend-#{MY_BACKEND}
|
209
|
+
filetype-#{MY_FILETYPE}
|
210
|
+
basebackend-#{MY_BACKEND}-doctype-#{doctype}
|
211
|
+
basebackend-#{MY_BACKEND}
|
212
|
+
).to_set
|
213
|
+
|
214
|
+
title = unescape(node.title)
|
215
|
+
|
216
|
+
result = []
|
217
|
+
result << %(= #{title}) unless title.nil?
|
218
|
+
node.attributes.each do |k,v|
|
219
|
+
skip = -> {
|
220
|
+
INTRINSIC_DOC_ATTRIBUTES.include?(k) ||
|
221
|
+
dynamic_exclusions.include?(k) ||
|
222
|
+
DEFAULT_DOC_ATTRIBUTES[k] == v ||
|
223
|
+
(k == ATTR_DOC_TITLE && v == undo_escape(title)) ||
|
224
|
+
(k == ATTR_ICONS_DIR && v == default_icons_dir(node))
|
225
|
+
}
|
226
|
+
result << %(:#{k}: #{v}) unless skip.call
|
227
|
+
end
|
228
|
+
|
229
|
+
result << '' unless result.empty?
|
230
|
+
|
231
|
+
result << node.content
|
232
|
+
result = result.join LF
|
233
|
+
|
234
|
+
if @current_node.parent.nil?
|
235
|
+
# be a good boy and add an EOL at the end
|
236
|
+
undo_escape(result.rstrip << LF)
|
237
|
+
else
|
238
|
+
undo_escape(result)
|
239
|
+
end
|
240
|
+
|
241
|
+
end
|
242
|
+
|
243
|
+
alias convert_embedded convert_document
|
244
|
+
|
245
|
+
def convert_section(node)
|
246
|
+
|
247
|
+
result = %(#{need_lf})
|
248
|
+
unless node.title.nil?
|
249
|
+
(0..node.level).each { result << '=' }
|
250
|
+
result << %( #{node.title}#{LF}#{LF})
|
251
|
+
end
|
252
|
+
|
253
|
+
result << node.content
|
254
|
+
|
255
|
+
end
|
256
|
+
|
257
|
+
def convert_block node
|
258
|
+
out = my_paragraph_header(node, PARAGRAPH_CONFIG)
|
259
|
+
out << %(#{node.style}: ) unless node.style.nil?
|
260
|
+
out << %(#{unescape node.content}#{LF}#{LF})
|
261
|
+
end
|
262
|
+
|
263
|
+
def convert_list(node)
|
264
|
+
|
265
|
+
@current_node.is_list = true
|
266
|
+
|
267
|
+
cfg = PARAGRAPH_CONFIG
|
268
|
+
|
269
|
+
numeric = true
|
270
|
+
contents=''
|
271
|
+
node.items.each do |li|
|
272
|
+
|
273
|
+
contents << LF unless contents.empty?
|
274
|
+
|
275
|
+
(sub, first_child) = @current_node.next_child { my_mixed_content(li) }
|
276
|
+
|
277
|
+
contents << li.marker << " " unless first_child&.is_list
|
278
|
+
contents << sub
|
279
|
+
|
280
|
+
numeric = false if li.marker != "."
|
281
|
+
end
|
282
|
+
|
283
|
+
# if the list is numeric, we need to re-declare default attributes
|
284
|
+
if numeric
|
285
|
+
# TODO: this is probably more complicated than this - the "default"
|
286
|
+
# style probably depends on the nesting...
|
287
|
+
cfg = cfg.merge({
|
288
|
+
CFG_DEFAULT_ATTR=>{
|
289
|
+
ATTR_STYLE => STYLE_ARABIC
|
290
|
+
}
|
291
|
+
})
|
292
|
+
end
|
293
|
+
|
294
|
+
out = my_paragraph_header(node, cfg)
|
295
|
+
out << list_break(out) << contents
|
296
|
+
|
297
|
+
end
|
298
|
+
|
299
|
+
def convert_admonition(node)
|
300
|
+
my_convert_paragraph(node, ADMONITION_CONFIG)
|
301
|
+
end
|
302
|
+
|
303
|
+
def convert_audio node
|
304
|
+
'TODO audio'
|
305
|
+
end
|
306
|
+
|
307
|
+
def convert_colist node
|
308
|
+
'TODO colist'
|
309
|
+
end
|
310
|
+
|
311
|
+
def convert_dlist(node)
|
312
|
+
|
313
|
+
out = my_paragraph_header(node, PARAGRAPH_CONFIG)
|
314
|
+
out << list_break(out)
|
315
|
+
|
316
|
+
first = true
|
317
|
+
node.items.each do |li|
|
318
|
+
if first
|
319
|
+
first = false
|
320
|
+
else
|
321
|
+
out << LF
|
322
|
+
end
|
323
|
+
out << unescape(li[0][0].text) << '::' << LF
|
324
|
+
out << my_mixed_content(li[1])
|
325
|
+
end
|
326
|
+
|
327
|
+
out
|
328
|
+
end
|
329
|
+
|
330
|
+
def convert_example node
|
331
|
+
'TODO example'
|
332
|
+
end
|
333
|
+
|
334
|
+
def convert_floating_title node
|
335
|
+
'TODO floating_title'
|
336
|
+
end
|
337
|
+
|
338
|
+
def convert_listing node
|
339
|
+
my_convert_paragraph(node, LISTING_CONFIG)
|
340
|
+
end
|
341
|
+
|
342
|
+
def convert_literal(node)
|
343
|
+
%(`$#{node.text}`)
|
344
|
+
end
|
345
|
+
|
346
|
+
def convert_stem node
|
347
|
+
'TODO stem'
|
348
|
+
end
|
349
|
+
|
350
|
+
alias convert_olist convert_list
|
351
|
+
|
352
|
+
def convert_open node
|
353
|
+
'TODO open'
|
354
|
+
end
|
355
|
+
|
356
|
+
def convert_page_break node
|
357
|
+
'TODO page_break'
|
358
|
+
end
|
359
|
+
|
360
|
+
def convert_paragraph node
|
361
|
+
|
362
|
+
my_convert_paragraph(node, PARAGRAPH_CONFIG)
|
363
|
+
|
364
|
+
end
|
365
|
+
|
366
|
+
def convert_pass node
|
367
|
+
"TODO pass"
|
368
|
+
end
|
369
|
+
|
370
|
+
# preamble is just a regular paragraph, the only
|
371
|
+
# thing special about it is its location
|
372
|
+
alias convert_preamble convert_paragraph
|
373
|
+
|
374
|
+
def convert_quote node
|
375
|
+
'TODO quote'
|
376
|
+
end
|
377
|
+
|
378
|
+
def convert_thematic_break node
|
379
|
+
%(#{need_lf}''')
|
380
|
+
end
|
381
|
+
|
382
|
+
def convert_sidebar node
|
383
|
+
'TODO sidebar'
|
384
|
+
end
|
385
|
+
|
386
|
+
def convert_table(node)
|
387
|
+
|
388
|
+
# TODO: We can get rid of "format" attribute by changing
|
389
|
+
# the separator, but why bother?
|
390
|
+
out = my_paragraph_header(node, TABLE_CONFIG)
|
391
|
+
out << %(|===#{LF})
|
392
|
+
|
393
|
+
node.rows.head.each { |row| out << my_table_row(node, row, TBL_STYLE_HEADER) }
|
394
|
+
node.rows.body.each { |row| out << my_table_row(node, row) }
|
395
|
+
# TODO: footer rows are of style "header", right?
|
396
|
+
node.rows.foot.each { |row| out << my_table_row(node, row, TBL_STYLE_HEADER) }
|
397
|
+
|
398
|
+
# no terminating LF. This is because abstract_node joins with LF.
|
399
|
+
out << '|==='
|
400
|
+
|
401
|
+
end
|
402
|
+
|
403
|
+
def convert_toc node
|
404
|
+
''
|
405
|
+
end
|
406
|
+
|
407
|
+
alias convert_ulist convert_list
|
408
|
+
|
409
|
+
def convert_verse node
|
410
|
+
'TODO verse'
|
411
|
+
end
|
412
|
+
|
413
|
+
def convert_video node
|
414
|
+
'TODO video'
|
415
|
+
end
|
416
|
+
|
417
|
+
def convert_inline_anchor node
|
418
|
+
|
419
|
+
title = choose node.text, node.attr(ATTR_TITLE), node.attr(1)
|
420
|
+
attrs = node.attributes.clone.keep_if { |k| ANCHOR_ATTRIBUTES.include? k }
|
421
|
+
|
422
|
+
target = node.target
|
423
|
+
|
424
|
+
if attrs.length == 1 && attrs[ATTR_ROLE] == ROLE_BARE && target == title
|
425
|
+
# bare link
|
426
|
+
return target
|
427
|
+
end
|
428
|
+
|
429
|
+
attrs[1] = title || ''
|
430
|
+
|
431
|
+
if target == "#"
|
432
|
+
target = File.basename(node.document.attr(ATTR_DOC_FILE))
|
433
|
+
end
|
434
|
+
|
435
|
+
out = %(#{node.type}:#{target})
|
436
|
+
out << write_attributes(attrs, {:include_empty=>true})
|
437
|
+
|
438
|
+
@current_node.is_anchor = true
|
439
|
+
|
440
|
+
return out
|
441
|
+
|
442
|
+
end
|
443
|
+
|
444
|
+
def convert_inline_break node
|
445
|
+
%(#{node.text} #{ESC_INLINE_BRK})
|
446
|
+
end
|
447
|
+
|
448
|
+
def convert_inline_button node
|
449
|
+
'TODO inline_button'
|
450
|
+
end
|
451
|
+
|
452
|
+
def convert_inline_callout node
|
453
|
+
'TODO inline_callout'
|
454
|
+
end
|
455
|
+
|
456
|
+
def convert_inline_footnote node
|
457
|
+
'TODO inline_footnote'
|
458
|
+
end
|
459
|
+
|
460
|
+
def convert_inline_image node
|
461
|
+
'TODO inline_image'
|
462
|
+
end
|
463
|
+
|
464
|
+
def convert_inline_indexterm node
|
465
|
+
'TODO inline_indexterm'
|
466
|
+
end
|
467
|
+
|
468
|
+
def convert_inline_kbd node
|
469
|
+
'TODO inline_kbd'
|
470
|
+
end
|
471
|
+
|
472
|
+
def convert_inline_menu node
|
473
|
+
'TODO inline_menu'
|
474
|
+
end
|
475
|
+
|
476
|
+
def convert_inline_quoted node
|
477
|
+
|
478
|
+
text = unescape(node.text)
|
479
|
+
|
480
|
+
# if implicit style matches the style here, just return the text.
|
481
|
+
# this is used for table cells.
|
482
|
+
if get_config(CFG_STYLE) == node.type
|
483
|
+
return text
|
484
|
+
end
|
485
|
+
|
486
|
+
# $TODO: We are using unconstrained formatting pairs everywhere right now
|
487
|
+
# because it's somewhat complicated to ensure a constrained pair can be used.
|
488
|
+
|
489
|
+
case node.type
|
490
|
+
when TYPE_EMPHASIS # called "highlight" in docs
|
491
|
+
%(#{ESC_ITALIC}#{text}#{ESC_ITALIC})
|
492
|
+
when TYPE_STRONG # called "bold" in docs
|
493
|
+
%(#{ESC_BOLD}#{text}#{ESC_BOLD})
|
494
|
+
when TYPE_MONOSPACE
|
495
|
+
%(#{ESC_MONO}#{text}#{ESC_MONO})
|
496
|
+
when TYPE_LITERAL
|
497
|
+
%(`+#{text}+`)
|
498
|
+
when TYPE_SINGLE
|
499
|
+
%(#{ESC_START_SINGLE_QUOTE}#{text}#{ESC_END_SINGLE_QUOTE})
|
500
|
+
when TYPE_DOUBLE
|
501
|
+
%(#{ESC_START_DOUBLE_QUOTE}#{text}#{ESC_END_DOUBLE_QUOTE})
|
502
|
+
when TYPE_MARK
|
503
|
+
%(#{ESC_HASH}#{text}#{ESC_HASH})
|
504
|
+
when TYPE_SUBSCRIPT
|
505
|
+
%(#{ESC_SUBSCRIPT}#{text}#{ESC_SUBSCRIPT})
|
506
|
+
when TYPE_SUPERSCRIPT
|
507
|
+
%(#{ESC_SUPERSCRIPT}#{text}#{ESC_SUPERSCRIPT})
|
508
|
+
when TYPE_NONE
|
509
|
+
text
|
510
|
+
else
|
511
|
+
raise "Unknown inline type #{node.type}"
|
512
|
+
end
|
513
|
+
end
|
514
|
+
|
515
|
+
private
|
516
|
+
|
517
|
+
def write_title(title)
|
518
|
+
return %(.#{title}#{LF}) if title
|
519
|
+
''
|
520
|
+
end
|
521
|
+
|
522
|
+
def default_icons_dir node
|
523
|
+
images_dir = node.attributes[ATTR_IMAGES_DIR]
|
524
|
+
return './images/icons' if images_dir.nil? || "" == images_dir
|
525
|
+
"#{images_dir}/icons"
|
526
|
+
end
|
527
|
+
|
528
|
+
def choose(*multiple)
|
529
|
+
r = multiple.select { |p| !p.nil?}
|
530
|
+
return nil unless r.length > 0
|
531
|
+
r[0]
|
532
|
+
end
|
533
|
+
|
534
|
+
# collapse_map is a hash that maps positional attributes
|
535
|
+
# to named attributes. AsciiDoctor duplicates named attributes
|
536
|
+
# from positional ones, leaving both in place, but only when
|
537
|
+
# positional attributes are recognized. We need to remove
|
538
|
+
# named attributes (key) if the positional attribute (value) is
|
539
|
+
# present. Special value 0 indicates that the attribute should be ignored at all.
|
540
|
+
def write_attributes(attrs, opts={}, config = {})
|
541
|
+
|
542
|
+
out = ''
|
543
|
+
|
544
|
+
list = []
|
545
|
+
|
546
|
+
collapse = config[CFG_COLLAPSE]
|
547
|
+
collapse = {} unless collapse
|
548
|
+
|
549
|
+
defaults = config[CFG_DEFAULT_ATTR]
|
550
|
+
defaults = {} unless defaults
|
551
|
+
|
552
|
+
unless attrs.nil?
|
553
|
+
|
554
|
+
attrs = attrs.clone
|
555
|
+
|
556
|
+
# deal with ID and roles, those are quite special.
|
557
|
+
attr1 = attrs[1]
|
558
|
+
attr1 = '' if attr1.nil?
|
559
|
+
id = attrs[ATTR_ID]
|
560
|
+
unless id.nil?
|
561
|
+
attr1 << %(##{id})
|
562
|
+
attrs.delete(ATTR_ID)
|
563
|
+
end
|
564
|
+
roles = attrs[ATTR_ROLE]
|
565
|
+
unless roles.nil?
|
566
|
+
roles.split.each do |role|
|
567
|
+
attr1 << %(.#{role})
|
568
|
+
end
|
569
|
+
attrs.delete(ATTR_ROLE)
|
570
|
+
end
|
571
|
+
|
572
|
+
# deal with options
|
573
|
+
attrs.clone.each do |attr, val|
|
574
|
+
attrs.delete(attr) if val.nil?
|
575
|
+
next if attr.is_a?(Numeric)
|
576
|
+
if attr.end_with?("-option") && val == ""
|
577
|
+
attr1 << %(%#{attr[0..-8]})
|
578
|
+
attrs.delete(attr)
|
579
|
+
end
|
580
|
+
end
|
581
|
+
|
582
|
+
attrs[1] = attr1 unless attr1 == ''
|
583
|
+
|
584
|
+
# if this returns true, the attribute should be thrown out.
|
585
|
+
collapsed = -> (key) {
|
586
|
+
return false unless collapse.key?(key)
|
587
|
+
pos = collapse[key]
|
588
|
+
return true if pos == 0
|
589
|
+
attrs.key?(pos)
|
590
|
+
}
|
591
|
+
|
592
|
+
# if this returns true, the attr/value pair should be thrown out
|
593
|
+
default = -> (key, val) {
|
594
|
+
if key.is_a?(Numeric)
|
595
|
+
|
596
|
+
return false if key == 1 && val == attr1
|
597
|
+
|
598
|
+
# we have to first find the real attr key
|
599
|
+
collapse.each do |c_key, c_value|
|
600
|
+
if c_value == key
|
601
|
+
key = c_key
|
602
|
+
break
|
603
|
+
end
|
604
|
+
end
|
605
|
+
|
606
|
+
if key.is_a?(Numeric)
|
607
|
+
raise %(Positional key #{key} with value #{val} does not translate into named attribute!)
|
608
|
+
end
|
609
|
+
|
610
|
+
end
|
611
|
+
|
612
|
+
return defaults.key?(key) && defaults[key] == val
|
613
|
+
|
614
|
+
}
|
615
|
+
|
616
|
+
named = []
|
617
|
+
|
618
|
+
attrs.each do |key,val|
|
619
|
+
next if key.is_a?(Symbol)
|
620
|
+
next if default.call(key,val)
|
621
|
+
if key.is_a?(Numeric)
|
622
|
+
list[key - 1] = val.nil? ? '' : val
|
623
|
+
else
|
624
|
+
named.push %(#{key}="#{val}") unless collapsed.call(key)
|
625
|
+
end
|
626
|
+
end
|
627
|
+
|
628
|
+
|
629
|
+
named.each do |n|
|
630
|
+
i = list.index(nil)
|
631
|
+
if i.nil?
|
632
|
+
list.push(n)
|
633
|
+
else
|
634
|
+
list[i] = n
|
635
|
+
end
|
636
|
+
end
|
637
|
+
end
|
638
|
+
|
639
|
+
list.pop while !list.nil? && !list.empty? && list[-1].nil?
|
640
|
+
|
641
|
+
if list.nil? || list.empty?
|
642
|
+
opts[OPT_INCLUDE_EMPTY] ? "[]" : ""
|
643
|
+
else
|
644
|
+
first = true
|
645
|
+
list.each do |item|
|
646
|
+
if first
|
647
|
+
first = false
|
648
|
+
out << '['
|
649
|
+
else
|
650
|
+
out << ','
|
651
|
+
end
|
652
|
+
out << (item.nil? ? '' : item.to_s)
|
653
|
+
end
|
654
|
+
out << ']'
|
655
|
+
out << LF if opts[OPT_FOR_BLOCK]
|
656
|
+
end
|
657
|
+
|
658
|
+
out
|
659
|
+
|
660
|
+
end
|
661
|
+
|
662
|
+
def my_paragraph_header(node, config)
|
663
|
+
|
664
|
+
title = write_title(node.title)
|
665
|
+
attrs = write_attributes(node.attributes, {OPT_FOR_BLOCK=>true}, config)
|
666
|
+
|
667
|
+
%(#{need_lf}#{title}#{attrs})
|
668
|
+
end
|
669
|
+
|
670
|
+
def need_lf
|
671
|
+
# it's possible to not add LFs in certain cases, but for readability
|
672
|
+
# it's just simpler to add an LF any time there is a sibling.
|
673
|
+
# exceptions are:
|
674
|
+
# * parent node is a list
|
675
|
+
need_lf = !@current_node.prev_sibling.nil? && !@current_node.parent_is_list?
|
676
|
+
need_lf ? LF : ''
|
677
|
+
end
|
678
|
+
|
679
|
+
def list_break(out)
|
680
|
+
if out.strip.empty?
|
681
|
+
fore = @current_node.prev_sibling
|
682
|
+
if fore&.is_list
|
683
|
+
return %(//-#{LF})
|
684
|
+
end
|
685
|
+
end
|
686
|
+
''
|
687
|
+
end
|
688
|
+
|
689
|
+
def my_convert_paragraph(node, config)
|
690
|
+
|
691
|
+
if node.blocks.nil? || node.blocks.empty?
|
692
|
+
content = unescape(config[CFG_CONTENT].call(node))
|
693
|
+
else
|
694
|
+
push_config({CFG_NO_LF=>true})
|
695
|
+
content = %(#{config[CFG_DELIMITER]}#{LF}#{node.content.rstrip}#{LF}#{config[CFG_DELIMITER]}#{LF})
|
696
|
+
pop_config
|
697
|
+
end
|
698
|
+
|
699
|
+
%(#{my_paragraph_header(node, config)}#{content})
|
700
|
+
end
|
701
|
+
|
702
|
+
def my_table_row(node, row, style="")
|
703
|
+
|
704
|
+
# there isn't really a good way to reconstruct how the cells
|
705
|
+
# were arranged. Because we force-set the header/footer style,
|
706
|
+
# we'll just use a cell per line output
|
707
|
+
|
708
|
+
# TODO: Support CSV/DSV/TSV formats
|
709
|
+
|
710
|
+
separator = node.attributes[ATTR_SEPARATOR]
|
711
|
+
separator = '|' if separator.nil?
|
712
|
+
|
713
|
+
out = ''
|
714
|
+
|
715
|
+
col_out = []
|
716
|
+
|
717
|
+
(0..row.length-1).each do |i|
|
718
|
+
|
719
|
+
col_def = node.columns[i]
|
720
|
+
cell = row[i]
|
721
|
+
# check for span
|
722
|
+
col_span = cell.colspan ? cell.colspan : 1
|
723
|
+
row_span = cell.rowspan ? cell.rowspan : 1
|
724
|
+
|
725
|
+
out_cell = {
|
726
|
+
:out => '',
|
727
|
+
:spans => false,
|
728
|
+
:duplicates => 1
|
729
|
+
}
|
730
|
+
|
731
|
+
col_out.push(out_cell)
|
732
|
+
|
733
|
+
out_cell[:out] << col_span.to_s if col_span > 1
|
734
|
+
out_cell[:out] << '.' << row_span.to_s if row_span > 1
|
735
|
+
unless out_cell[:out] == ''
|
736
|
+
out_cell[:out] << '+'
|
737
|
+
out_cell[:spans] = true
|
738
|
+
end
|
739
|
+
|
740
|
+
# horizontal alignment
|
741
|
+
def_attr = col_def.attributes
|
742
|
+
cell_attr = cell.attributes
|
743
|
+
|
744
|
+
if def_attr[ATTR_HALIGN] != (attr_val = cell_attr[ATTR_HALIGN])
|
745
|
+
case attr_val
|
746
|
+
when HALIGN_LEFT then out_cell[:out] << '<'
|
747
|
+
when HALIGN_RIGHT then out_cell[:out] << '>'
|
748
|
+
when HALIGN_CENTER then out_cell[:out] << '^'
|
749
|
+
else raise "Unknown horizontal alignment #{attr_val}"
|
750
|
+
end
|
751
|
+
end
|
752
|
+
|
753
|
+
if def_attr[ATTR_VALIGN] != (attr_val = cell_attr[ATTR_VALIGN])
|
754
|
+
case attr_val
|
755
|
+
when VALIGN_TOP then out_cell[:out] << '.<'
|
756
|
+
when VALIGN_BOTTOM then out_cell[:out] << '.>'
|
757
|
+
when VALIGN_CENTER then out_cell[:out] << '.^'
|
758
|
+
else raise "Unknown vertical alignment #{attr_val}"
|
759
|
+
end
|
760
|
+
end
|
761
|
+
|
762
|
+
get_style = -> (s) { s.nil? ? :none : s }
|
763
|
+
|
764
|
+
declared_style = nil
|
765
|
+
if get_style.call(col_def.style) != (attr_val = get_style.call(cell.style))
|
766
|
+
|
767
|
+
case attr_val
|
768
|
+
when TYPE_ASCIIDOC then out_cell[:out] << 'a'
|
769
|
+
when TYPE_NONE then out_cell[:out] << 'd'
|
770
|
+
when TYPE_EMPHASIS then out_cell[:out] << 'e'
|
771
|
+
when TYPE_HEADER then out_cell[:out] << 'h'
|
772
|
+
when TYPE_LITERAL then out_cell[:out] << 'l'
|
773
|
+
when TYPE_MONOSPACE then out_cell[:out] << 'm'
|
774
|
+
when TYPE_STRONG then out_cell[:out] << 's'
|
775
|
+
else raise "Unknown style #{attr_val}"
|
776
|
+
end
|
777
|
+
|
778
|
+
declared_style = attr_val
|
779
|
+
|
780
|
+
elsif style != ''
|
781
|
+
|
782
|
+
out_cell[:out] << style
|
783
|
+
|
784
|
+
end
|
785
|
+
|
786
|
+
push_config({CFG_STYLE=>declared_style}) unless declared_style.nil?
|
787
|
+
content = cell.content
|
788
|
+
# we can get a string, or an array for table cells.
|
789
|
+
# I believe the array is the list of paragraphs, if there is ever more
|
790
|
+
# than one element.
|
791
|
+
|
792
|
+
# TODO There is a problem with trailing newlines. If the input has trailing
|
793
|
+
# newlines, (at least tested with '2*h' column), we don't seem to be able to
|
794
|
+
# determine that. However, Asciidoctor will wrap the inner contents in a <p>
|
795
|
+
# block if there is a newline, and will not wrap if there isn't. We always
|
796
|
+
# don't write the newline (because there is no reason to), causing difference
|
797
|
+
# in rendered output between the input and the converted documents. So for now
|
798
|
+
# tests have no empty lines between trivial table cells.
|
799
|
+
|
800
|
+
out_cell[:out] << separator
|
801
|
+
if content.is_a?(Array)
|
802
|
+
out_cell[:out] << content.join(%(#{LF}#{LF}))
|
803
|
+
else
|
804
|
+
out_cell[:out] << content
|
805
|
+
end
|
806
|
+
pop_config unless declared_style.nil?
|
807
|
+
|
808
|
+
# we have no idea how the cells were formatted in the original
|
809
|
+
# document, but it's safe to add an EOL after each cell
|
810
|
+
# will prevent overly long lines at least.
|
811
|
+
out_cell[:out] << LF
|
812
|
+
|
813
|
+
end
|
814
|
+
|
815
|
+
out = ''
|
816
|
+
|
817
|
+
print_cell = -> (cell) do
|
818
|
+
return if cell.nil?
|
819
|
+
out << cell[:duplicates].to_s << '*' if cell[:duplicates] > 1
|
820
|
+
out << cell[:out]
|
821
|
+
end
|
822
|
+
|
823
|
+
prev = nil
|
824
|
+
col_out.each do |cell|
|
825
|
+
|
826
|
+
begin
|
827
|
+
|
828
|
+
next if prev.nil? || prev[:spans] || cell[:spans]
|
829
|
+
|
830
|
+
if prev[:out] == cell[:out]
|
831
|
+
# ugh, the cell has probably been multiplied
|
832
|
+
cell = nil
|
833
|
+
prev[:duplicates] += 1
|
834
|
+
end
|
835
|
+
|
836
|
+
ensure
|
837
|
+
unless cell.nil?
|
838
|
+
print_cell.call(prev)
|
839
|
+
prev = cell
|
840
|
+
end
|
841
|
+
end
|
842
|
+
|
843
|
+
end
|
844
|
+
|
845
|
+
print_cell.call(prev)
|
846
|
+
|
847
|
+
out
|
848
|
+
|
849
|
+
end
|
850
|
+
|
851
|
+
# the reason we need this method is that, apparently, Asciidoctor
|
852
|
+
# re-processes returned converted text for more Asciidoctor processing,
|
853
|
+
# and we don't really know at the end how to unwind that. So, instead, we
|
854
|
+
# use special encoding to encode non-Asciidoctor characters in text. We now
|
855
|
+
# need to undo this.
|
856
|
+
def undo_escape(text)
|
857
|
+
|
858
|
+
out = ''
|
859
|
+
idx = 0
|
860
|
+
while true
|
861
|
+
next_idx = text[idx..-1].index(ESC)
|
862
|
+
if next_idx.nil?
|
863
|
+
out << text[idx..-1]
|
864
|
+
break
|
865
|
+
end
|
866
|
+
next_idx += idx
|
867
|
+
out << text[idx..next_idx-1] unless next_idx == idx
|
868
|
+
next_idx += 1
|
869
|
+
while text[next_idx] != ESC_E
|
870
|
+
out << (text[next_idx] + text[next_idx+1]).to_i(16).chr
|
871
|
+
next_idx += 2
|
872
|
+
end
|
873
|
+
idx = next_idx + 1
|
874
|
+
end
|
875
|
+
|
876
|
+
out
|
877
|
+
|
878
|
+
end
|
879
|
+
|
880
|
+
def my_mixed_content(node)
|
881
|
+
|
882
|
+
out = ''
|
883
|
+
text = node.text
|
884
|
+
if text
|
885
|
+
out << unescape(text)
|
886
|
+
@current_node.add_text_child(text)
|
887
|
+
end
|
888
|
+
|
889
|
+
sub = node.content
|
890
|
+
|
891
|
+
out << LF unless sub.empty? || out.empty?
|
892
|
+
out << sub
|
893
|
+
|
894
|
+
end
|
895
|
+
|
896
|
+
def push_config(obj)
|
897
|
+
|
898
|
+
if @config.length == 0
|
899
|
+
@config = [obj]
|
900
|
+
return
|
901
|
+
end
|
902
|
+
|
903
|
+
@config.push(@config.last.merge(obj))
|
904
|
+
|
905
|
+
end
|
906
|
+
|
907
|
+
def get_config(sym)
|
908
|
+
@config.last[sym]
|
909
|
+
end
|
910
|
+
|
911
|
+
def pop_config
|
912
|
+
@config.pop
|
913
|
+
end
|
914
|
+
|
915
|
+
# taken from manify in
|
916
|
+
# https://github.com/asciidoctor/asciidoctor/blob/master/lib/asciidoctor/converter/manpage.rb
|
917
|
+
# Undo conversions done by AsciiDoctor according to:
|
918
|
+
# https://docs.asciidoctor.org/asciidoc/latest/subs/special-characters/#table-special
|
919
|
+
# https://docs.asciidoctor.org/asciidoc/latest/subs/replacements
|
920
|
+
def unescape(str, encode = true)
|
921
|
+
return nil if str.nil?
|
922
|
+
Unescape.unescape(str, encode)
|
923
|
+
end
|
924
|
+
|
925
|
+
end
|