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.
@@ -0,0 +1,3 @@
1
+ module AsciidoctorAsciiDoc
2
+ VERSION = "0.0.2.dev"
3
+ end
@@ -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 = /&#8212;(?:&#8203;)?/
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