asciidoctor-moodle 1.0.0.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,793 @@
1
+ # frozen_string_literal: true
2
+ require 'asciidoctor' unless defined? ::Asciidoctor::VERSION ##Rajout
3
+ module Asciidoctor
4
+ ROOT_TAG_NAME = 'quiz'
5
+ # A built-in {Converter} implementation that generates DocBook 5 output. The output is inspired by the output produced
6
+ # by the docbook45 backend from AsciiDoc.py, except it has been migrated to the DocBook 5 specification.
7
+ class Converter::MoodleConverter < Converter::Base
8
+ register_for 'moodle'
9
+
10
+ # default represents variablelist
11
+ (DLIST_TAGS = {
12
+ 'qanda' => { list: 'qandaset', entry: 'qandaentry', label: 'question', term: 'simpara', item: 'answer' },
13
+ 'glossary' => { list: nil, entry: 'glossentry', term: 'glossterm', item: 'glossdef' },
14
+ }).default = { list: 'variablelist', entry: 'varlistentry', term: 'term', item: 'listitem' }
15
+
16
+ (QUOTE_TAGS = {
17
+ monospaced: ['<literal>', '</literal>'],
18
+ emphasis: ['<em>', '</em>', true],
19
+ strong: ['<strong>','</strong>', true],
20
+ double: ['<quote role="double">', '</quote>', true],
21
+ single: ['<quote role="single">', '</quote>', true],
22
+ mark: ['<emphasis role="marked">', '</emphasis>'],
23
+ superscript: ['<superscript>', '</superscript>'],
24
+ subscript: ['<subscript>', '</subscript>'],
25
+ }).default = ['', '', true]
26
+
27
+ MANPAGE_SECTION_TAGS = { 'section' => 'refsection', 'synopsis' => 'refsynopsisdiv' }
28
+ TABLE_PI_NAMES = %w(dbhtml dbfo dblatex)
29
+
30
+ CopyrightRx = /^(#{CC_ANY}+?)(?: ((?:\d{4}-)?\d{4}))?$/
31
+ ImageMacroRx = /^image::?(\S|\S#{CC_ANY}*?\S)\[(#{CC_ANY}+)?\]$/
32
+
33
+ def initialize backend, opts = {}
34
+ @backend = backend
35
+ init_backend_traits basebackend: 'docbook', filetype: 'xml', outfilesuffix: '.xml', supports_templates: true
36
+ end
37
+
38
+
39
+ def convert_document node
40
+ result = ['<?xml version="1.0" encoding="UTF-8"?> <quiz>']
41
+
42
+
43
+ result << (node.blocks.map {|block| block.convert }.compact.join LF) if node.blocks?
44
+ #restore_abstract abstract if abstract
45
+ unless (docinfo_content = node.docinfo :footer).empty?
46
+ result << docinfo_content
47
+ end
48
+ id, node.id = node.id, nil unless id
49
+
50
+
51
+ result = result.join LF
52
+ result << '</quiz>'
53
+
54
+ result
55
+ end
56
+
57
+ def convert_embedded node
58
+ # NOTE in DocBook 5, the root abstract must be in the info tag and is thus not part of the body
59
+ if @backend == 'docbook5' && (abstract = find_root_abstract node)
60
+ abstract = extract_abstract node, abstract
61
+ end
62
+ result = node.blocks.map {|block| block.convert }.compact.join LF
63
+ restore_abstract abstract if abstract
64
+ result
65
+ end
66
+
67
+ def convert_section node
68
+ if node.document.doctype == 'manpage'
69
+ tag_name = MANPAGE_SECTION_TAGS[tag_name = node.sectname] || tag_name
70
+ else
71
+ tag_name = node.sectname
72
+ end
73
+ title_el = node.special && ((node.option? 'notitle') || (node.option? 'untitled')) ? '' : %(<title>#{node.title}</title>\n)
74
+ %(<#{tag_name}#{common_attributes node.id, node.role, node.reftext}>
75
+ #{title_el}#{node.content}
76
+ </#{tag_name}>)
77
+ end
78
+
79
+ def convert_admonition node
80
+ %(<#{tag_name = node.attr 'name'}#{common_attributes node.id, node.role, node.reftext}>
81
+ #{title_tag node}#{enclose_content node}
82
+ </#{tag_name}>)
83
+ end
84
+
85
+ alias convert_audio skip
86
+
87
+ def convert_colist node
88
+ result = []
89
+ result << %(<calloutlist#{common_attributes node.id, node.role, node.reftext}>)
90
+ result << %(<title>#{node.title}</title>) if node.title?
91
+ node.items.each do |item|
92
+ result << %(<callout arearefs="#{item.attr 'coids'}">)
93
+ result << %(<para>#{item.text}</para>)
94
+ result << item.content if item.blocks?
95
+ result << '</callout>'
96
+ end
97
+ result << %(</calloutlist>)
98
+ result.join LF
99
+ end
100
+
101
+ def convert_dlist node
102
+ result = []
103
+ if node.style == 'horizontal'
104
+ result << %(<#{tag_name = node.title? ? 'table' : 'informaltable'}#{common_attributes node.id, node.role, node.reftext} tabstyle="horizontal" frame="none" colsep="0" rowsep="0">
105
+ #{title_tag node}<tgroup cols="2">
106
+ <colspec colwidth="#{node.attr 'labelwidth', 15}*"/>
107
+ <colspec colwidth="#{node.attr 'itemwidth', 85}*"/>
108
+ <tbody valign="top">)
109
+ node.items.each do |terms, dd|
110
+ result << %(<row>
111
+ <entry>)
112
+ terms.each {|dt| result << %(<simpara>#{dt.text}</simpara>) }
113
+ result << %(</entry>
114
+ <entry>)
115
+ if dd
116
+ result << %(<simpara>#{dd.text}</simpara>) if dd.text?
117
+ result << dd.content if dd.blocks?
118
+ end
119
+ result << %(</entry>
120
+ </row>)
121
+ end
122
+ result << %(</tbody>
123
+ </tgroup>
124
+ </#{tag_name}>)
125
+ else
126
+ tags = DLIST_TAGS[node.style]
127
+ list_tag = tags[:list]
128
+ entry_tag = tags[:entry]
129
+ label_tag = tags[:label]
130
+ term_tag = tags[:term]
131
+ item_tag = tags[:item]
132
+ if list_tag
133
+ result << %(<#{list_tag}#{common_attributes node.id, node.role, node.reftext}>)
134
+ result << %(<title>#{node.title}</title>) if node.title?
135
+ end
136
+
137
+ node.items.each do |terms, dd|
138
+ result << %(<#{entry_tag}>)
139
+ result << %(<#{label_tag}>) if label_tag
140
+ terms.each {|dt| result << %(<#{term_tag}>#{dt.text}</#{term_tag}>) }
141
+ result << %(</#{label_tag}>) if label_tag
142
+ result << %(<#{item_tag}>)
143
+ if dd
144
+ result << %(<simpara>#{dd.text}</simpara>) if dd.text?
145
+ result << dd.content if dd.blocks?
146
+ end
147
+ result << %(</#{item_tag}>)
148
+ result << %(</#{entry_tag}>)
149
+ end
150
+
151
+ result << %(</#{list_tag}>) if list_tag
152
+ end
153
+
154
+ result.join LF
155
+ end
156
+
157
+ def convert_example node
158
+ if node.title?
159
+ %(<example#{common_attributes node.id, node.role, node.reftext}>
160
+ <title>#{node.title}</title>
161
+ #{enclose_content node}
162
+ </example>)
163
+ else
164
+ %(<informalexample#{common_attributes node.id, node.role, node.reftext}>
165
+ #{enclose_content node}
166
+ </informalexample>)
167
+ end
168
+ end
169
+
170
+ def convert_floating_title node
171
+ %(<bridgehead#{common_attributes node.id, node.role, node.reftext} renderas="sect#{node.level}">#{node.title}</bridgehead>)
172
+ end
173
+
174
+ def convert_image node
175
+ align_attribute = (node.attr? 'align') ? %( align="#{node.attr 'align'}") : ''
176
+
177
+ mediaobject = %(<mediaobject>
178
+ <imageobject>
179
+ <imagedata fileref="#{node.image_uri node.attr 'target'}"#{image_size_attributes node.attributes}#{align_attribute}/>
180
+ </imageobject>
181
+ <textobject><phrase>#{node.alt}</phrase></textobject>
182
+ </mediaobject>)
183
+
184
+ if node.title?
185
+ %(<figure#{common_attributes node.id, node.role, node.reftext}>
186
+ <title>#{node.title}</title>
187
+ #{mediaobject}
188
+ </figure>)
189
+ else
190
+ %(<informalfigure#{common_attributes node.id, node.role, node.reftext}>
191
+ #{mediaobject}
192
+ </informalfigure>)
193
+ end
194
+ end
195
+
196
+ def convert_listing node
197
+ informal = !node.title?
198
+ common_attrs = common_attributes node.id, node.role, node.reftext
199
+ if node.style == 'source'
200
+ attrs = node.attributes
201
+ if node.option? 'linenums'
202
+ numbering_attrs = (attrs.key? 'start') ? %( linenumbering="numbered" startinglinenumber="#{attrs['start'].to_i}") : ' linenumbering="numbered"'
203
+ else
204
+ numbering_attrs = ' linenumbering="unnumbered"'
205
+ end
206
+ if attrs.key? 'language'
207
+ wrapped_content = %(<programlisting#{informal ? common_attrs : ''} language="#{attrs['language']}"#{numbering_attrs}>#{node.content}</programlisting>)
208
+ else
209
+ wrapped_content = %(<screen#{informal ? common_attrs : ''}#{numbering_attrs}>#{node.content}</screen>)
210
+ end
211
+ else
212
+ wrapped_content = %(<screen#{informal ? common_attrs : ''}>#{node.content}</screen>)
213
+ end
214
+ informal ? wrapped_content : %(<formalpara#{common_attrs}>
215
+ <title>#{node.title}</title>
216
+ <para>
217
+ #{wrapped_content}
218
+ </para>
219
+ </formalpara>)
220
+ end
221
+
222
+ def convert_literal node
223
+ if node.title?
224
+ %(#{node.content}) #J'ai effacé toutes les balises ici pour avoir juste le contenu
225
+ else
226
+ %(#{node.content}) #J'ai effacé toutes les balises ici pour avoir juste le contenu
227
+ end #On rentre ici
228
+ end
229
+
230
+ alias convert_pass content_only
231
+
232
+ def convert_stem node
233
+ if (idx = node.subs.index :specialcharacters)
234
+ node.subs.delete_at idx
235
+ equation = node.content
236
+ idx > 0 ? (node.subs.insert idx, :specialcharacters) : (node.subs.unshift :specialcharacters)
237
+ else
238
+ equation = node.content
239
+ end
240
+ if node.style == 'asciimath'
241
+ # NOTE fop requires jeuclid to process mathml markup
242
+ equation_data = asciimath_available? ? ((::AsciiMath.parse equation).to_mathml 'mml:', 'xmlns:mml' => 'http://www.w3.org/1998/Math/MathML') : %(<mathphrase><![CDATA[#{equation}]]></mathphrase>)
243
+ else
244
+ # unhandled math; pass source to alt and required mathphrase element; dblatex will process alt as LaTeX math
245
+ equation_data = %(<alt><![CDATA[#{equation}]]></alt>
246
+ <mathphrase><![CDATA[#{equation}]]></mathphrase>)
247
+ end
248
+ if node.title?
249
+ %(<equation#{common_attributes node.id, node.role, node.reftext}>
250
+ <title>#{node.title}</title>
251
+ #{equation_data}
252
+ </equation>)
253
+ else
254
+ # WARNING dblatex displays the <informalequation> element inline instead of block as documented (except w/ mathml)
255
+ %(<informalequation#{common_attributes node.id, node.role, node.reftext}>
256
+ #{equation_data}
257
+ </informalequation>)
258
+ end
259
+ end
260
+
261
+ def convert_olist node
262
+ puts('#convert_olist')
263
+
264
+ result = []
265
+ num_attribute = node.style ? %( numeration="#{node.style}") : ''
266
+ start_attribute = (node.attr? 'start') ? %( startingnumber="#{node.attr 'start'}") : ''
267
+ result << %(<orderedlist#{common_attributes node.id, node.role, node.reftext}#{num_attribute}#{start_attribute}>)
268
+ result << %(<title>#{node.title}</title>) if node.title?
269
+ node.items.each do |item|
270
+ result << %(<listitem#{common_attributes item.id, item.role}>)
271
+ result << %(<simpara>#{item.text}</simpara>)
272
+ result << item.content if item.blocks?
273
+ result << '</listitem>'
274
+ end
275
+ result << %(</orderedlist>)
276
+ result.join LF
277
+ end
278
+
279
+ def convert_open node
280
+ case node.style
281
+ when 'abstract'
282
+ if (parent = node.parent) == node.document && node.document.doctype == 'book'
283
+ logger.warn 'abstract block cannot be used in a document without a doctitle when doctype is book. Excluding block content.'
284
+ ''
285
+ else
286
+ result = %(<abstract>
287
+ #{title_tag node}#{enclose_content node}
288
+ </abstract>)
289
+ if @backend == 'docbook5' && !(node.option? 'root') && (parent.context == :open ? parent.style == 'partintro' : parent.context == :section && parent.sectname == 'partintro') && node == parent.blocks[0]
290
+ result = %(<info>\n#{result}\n</info>)
291
+ end
292
+ result
293
+ end
294
+ when 'partintro'
295
+ if node.level == 0 && node.parent.context == :section && node.document.doctype == 'book'
296
+ %(<partintro#{common_attributes node.id, node.role, node.reftext}>
297
+ #{title_tag node}#{enclose_content node}
298
+ </partintro>)
299
+ else
300
+ logger.error 'partintro block can only be used when doctype is book and must be a child of a book part. Excluding block content.'
301
+ ''
302
+ end
303
+ else #Par défaut
304
+ reftext = node.reftext if (id = node.id)
305
+ role = node.role
306
+ if node.title?
307
+ %(<formalpara#{common_attributes id, role, reftext}>
308
+ <title>#{node.title}</title>
309
+ <para>#{content_spacer = node.content_model == :compound ? LF : ''}#{node.content}#{content_spacer}</para>
310
+ </formalpara>)
311
+ elsif id || role
312
+ if node.content_model == :compound
313
+ %(#{common_attributes id, role, reftext}
314
+ #{node.content}
315
+ ) #ici lesparagraphes
316
+ else
317
+ %(<simpara#{common_attributes id, role, reftext}>#{node.content}</simpara>)
318
+ end
319
+ else
320
+ enclose_content node
321
+ end
322
+ end
323
+ end
324
+
325
+ def convert_page_break node
326
+ '<simpara><?asciidoc-pagebreak?></simpara>'
327
+ end
328
+
329
+ def convert_paragraph node
330
+ if node.title?
331
+ %(<formalpara#{common_attributes node.id, node.role, node.reftext}>
332
+ <title>#{node.title}</title>
333
+ <para>#{node.content}</para>
334
+ </formalpara>)
335
+ else
336
+ %(#{node.content}) #Rajout : plutot effacelment ici les simpra de paragraphe
337
+ end
338
+ end
339
+
340
+ def convert_preamble node
341
+ if node.document.doctype == 'book'
342
+ %(<preface#{common_attributes node.id, node.role, node.reftext}>
343
+ #{title_tag node, false}#{node.content}
344
+ </preface>)
345
+ else
346
+ node.content
347
+ end
348
+ end
349
+
350
+ def convert_quote node
351
+ blockquote_tag(node, (node.has_role? 'epigraph') && 'epigraph') { enclose_content node }
352
+ end
353
+
354
+ def convert_thematic_break node
355
+ '<simpara><?asciidoc-hr?></simpara>'
356
+ end
357
+
358
+ def convert_sidebar node
359
+ %(<sidebar#{common_attributes node.id, node.role, node.reftext}>
360
+ #{title_tag node}#{enclose_content node}
361
+ </sidebar>)
362
+ end
363
+
364
+ def convert_table node
365
+ has_body = false
366
+ result = []
367
+ pgwide_attribute = (node.option? 'pgwide') ? ' pgwide="1"' : ''
368
+ frame = 'topbot' if (frame = node.attr 'frame', 'all', 'table-frame') == 'ends'
369
+ grid = node.attr 'grid', nil, 'table-grid'
370
+ result << %(<#{tag_name = node.title? ? 'table' : 'informaltable'}#{common_attributes node.id, node.role, node.reftext}#{pgwide_attribute} frame="#{frame}" rowsep="#{(%w(none cols).include? grid) ? 0 : 1}" colsep="#{(%w(none rows).include? grid) ? 0 : 1}"#{(node.attr? 'orientation', 'landscape', 'table-orientation') ? ' orient="land"' : ''}>)
371
+ if node.option? 'unbreakable'
372
+ result << '<?dbfo keep-together="always"?>'
373
+ elsif node.option? 'breakable'
374
+ result << '<?dbfo keep-together="auto"?>'
375
+ end
376
+ result << %(<title>#{node.title}</title>) if tag_name == 'table'
377
+ if (width = (node.attr? 'width') ? (node.attr 'width') : nil)
378
+ TABLE_PI_NAMES.each do |pi_name|
379
+ result << %(<?#{pi_name} table-width="#{width}"?>)
380
+ end
381
+ col_width_key = 'colabswidth'
382
+ else
383
+ col_width_key = 'colpcwidth'
384
+ end
385
+ result << %(<tgroup cols="#{node.attr 'colcount'}">)
386
+ node.columns.each do |col|
387
+ result << %(<colspec colname="col_#{col.attr 'colnumber'}" colwidth="#{col.attr col_width_key}*"/>)
388
+ end
389
+ node.rows.to_h.each do |tsec, rows|
390
+ next if rows.empty?
391
+ has_body = true if tsec == :body
392
+ result << %(<t#{tsec}>)
393
+ rows.each do |row|
394
+ result << '<row>'
395
+ row.each do |cell|
396
+ colspan_attribute = cell.colspan ? %( namest="col_#{colnum = cell.column.attr 'colnumber'}" nameend="col_#{colnum + cell.colspan - 1}") : ''
397
+ rowspan_attribute = cell.rowspan ? %( morerows="#{cell.rowspan - 1}") : ''
398
+ # NOTE <entry> may not have whitespace (e.g., line breaks) as a direct descendant according to DocBook rules
399
+ entry_start = %(<entry align="#{cell.attr 'halign'}" valign="#{cell.attr 'valign'}"#{colspan_attribute}#{rowspan_attribute}>)
400
+ if tsec == :head
401
+ cell_content = cell.text
402
+ else
403
+ case cell.style
404
+ when :asciidoc
405
+ cell_content = cell.content
406
+ when :literal
407
+ cell_content = %(<literallayout class="monospaced">#{cell.text}</literallayout>)
408
+ when :header
409
+ cell_content = (cell_content = cell.content).empty? ? '' : %(<simpara><emphasis role="strong">#{cell_content.join '</emphasis></simpara><simpara><emphasis role="strong">'}</emphasis></simpara>)
410
+ else
411
+ cell_content = (cell_content = cell.content).empty? ? '' : %(<simpara>#{cell_content.join '</simpara><simpara>'}</simpara>)
412
+ end
413
+ end
414
+ entry_end = (node.document.attr? 'cellbgcolor') ? %(<?dbfo bgcolor="#{node.document.attr 'cellbgcolor'}"?></entry>) : '</entry>'
415
+ result << %(#{entry_start}#{cell_content}#{entry_end})
416
+ end
417
+ result << '</row>'
418
+ end
419
+ result << %(</t#{tsec}>)
420
+ end
421
+ result << '</tgroup>'
422
+ result << %(</#{tag_name}>)
423
+
424
+ logger.warn 'tables must have at least one body row' unless has_body
425
+ result.join LF
426
+ end
427
+
428
+ alias convert_toc skip
429
+
430
+ def convert_ulist node
431
+ puts('#convert_ulist')
432
+ result = []
433
+ if node.style == 'bibliography'
434
+ result << %(<bibliodiv#{common_attributes node.id, node.role, node.reftext}>)
435
+ result << %(<title>#{node.title}</title>) if node.title?
436
+ node.items.each do |item|
437
+ result << '<bibliomixed>'
438
+ result << %(<bibliomisc>#{item.text}</bibliomisc>)
439
+ result << item.content if item.blocks?
440
+ result << '</bibliomixed>'
441
+ end
442
+ result << '</bibliodiv>'
443
+ else
444
+ mark_type = (checklist = node.option? 'checklist') ? 'none' : node.style
445
+ mark_attribute = mark_type ? %( mark="#{mark_type}") : ''
446
+ result << %(<itemizedlist#{common_attributes node.id, node.role, node.reftext}#{mark_attribute}>)
447
+ result << %(<title>#{node.title}</title>) if node.title?
448
+ node.items.each do |item|
449
+ text_marker = (item.attr? 'checked') ? '&#10003; ' : '&#10063; ' if checklist && (item.attr? 'checkbox')
450
+ result << %(<listitem#{common_attributes item.id, item.role}>)
451
+ result << %(<simpara>#{text_marker || ''}#{item.text}</simpara>)
452
+ result << item.content if item.blocks?
453
+ result << '</listitem>'
454
+ end
455
+ result << '</itemizedlist>'
456
+ end
457
+ result.join LF
458
+ end
459
+
460
+ def convert_verse node
461
+ blockquote_tag(node, (node.has_role? 'epigraph') && 'epigraph') { %(<literallayout>#{node.content}</literallayout>) }
462
+ end
463
+
464
+ alias convert_video skip
465
+
466
+ def convert_inline_anchor node
467
+ case node.type
468
+ when :ref
469
+ %(<anchor#{common_attributes((id = node.id), nil, node.reftext || %([#{id}]))}/>)
470
+ when :xref
471
+ if (path = node.attributes['path'])
472
+ %(<link xl:href="#{node.target}">#{node.text || path}</link>)
473
+ else
474
+ if (linkend = node.attributes['refid']).nil_or_empty?
475
+ root_doc = get_root_document node
476
+ # Q: should we warn instead of generating a document ID on demand?
477
+ linkend = (root_doc.id ||= generate_document_id root_doc)
478
+ end
479
+ # NOTE the xref tag in DocBook does not support explicit link text, so the link tag must be used instead
480
+ # The section at http://www.sagehill.net/docbookxsl/CrossRefs.html#IdrefLinks gives an explanation for this choice
481
+ # "link - a cross reference where you supply the text of the reference as the content of the link element."
482
+ (text = node.text) ? %(<link linkend="#{linkend}">#{text}</link>) : %(<xref linkend="#{linkend}"/>)
483
+ end
484
+ when :link
485
+ %(<link xl:href="#{node.target}">#{node.text}</link>)
486
+ when :bibref
487
+ %(<anchor#{common_attributes node.id, nil, (text = "[#{node.reftext || node.id}]")}/>#{text})
488
+ else
489
+ logger.warn %(unknown anchor type: #{node.type.inspect})
490
+ nil
491
+ end
492
+ end
493
+
494
+ def convert_inline_break node
495
+ %(#{node.text}<?asciidoc-br?>)
496
+ end
497
+
498
+ def convert_inline_button node
499
+ %(<guibutton>#{node.text}</guibutton>)
500
+ end
501
+
502
+ def convert_inline_callout node
503
+ %(<co#{common_attributes node.id}/>)
504
+ end
505
+
506
+ def convert_inline_footnote node
507
+ if node.type == :xref
508
+ %(<footnoteref linkend="#{node.target}"/>)
509
+ else
510
+ %(<footnote#{common_attributes node.id}><simpara>#{node.text}</simpara></footnote>)
511
+ end
512
+ end
513
+
514
+ def convert_inline_image node
515
+ %(<inlinemediaobject#{common_attributes nil, node.role}>
516
+ <imageobject>
517
+ <imagedata fileref="#{node.type == 'icon' ? (node.icon_uri node.target) : (node.image_uri node.target)}"#{image_size_attributes node.attributes}/>
518
+ </imageobject>
519
+ <textobject><phrase>#{node.alt}</phrase></textobject>
520
+ </inlinemediaobject>)
521
+ end
522
+
523
+ def convert_inline_indexterm node
524
+ if (see = node.attr 'see')
525
+ rel = %(\n<see>#{see}</see>)
526
+ elsif (see_also_list = node.attr 'see-also')
527
+ rel = see_also_list.map {|see_also| %(\n<seealso>#{see_also}</seealso>) }.join
528
+ else
529
+ rel = ''
530
+ end
531
+ if node.type == :visible
532
+ %(<indexterm>
533
+ <primary>#{node.text}</primary>#{rel}
534
+ </indexterm>#{node.text})
535
+ elsif (numterms = (terms = node.attr 'terms').size) > 2
536
+ %(<indexterm>
537
+ <primary>#{terms[0]}</primary><secondary>#{terms[1]}</secondary><tertiary>#{terms[2]}</tertiary>#{rel}
538
+ </indexterm>#{(node.document.option? 'indexterm-promotion') ? %(
539
+ <indexterm>
540
+ <primary>#{terms[1]}</primary><secondary>#{terms[2]}</secondary>
541
+ </indexterm>
542
+ <indexterm>
543
+ <primary>#{terms[2]}</primary>
544
+ </indexterm>) : ''})
545
+ elsif numterms > 1
546
+ %(<indexterm>
547
+ <primary>#{terms[0]}</primary><secondary>#{terms[1]}</secondary>#{rel}
548
+ </indexterm>#{(node.document.option? 'indexterm-promotion') ? %(
549
+ <indexterm>
550
+ <primary>#{terms[1]}</primary>
551
+ </indexterm>) : ''})
552
+ else
553
+ %(<indexterm>
554
+ <primary>#{terms[0]}</primary>#{rel}
555
+ </indexterm>)
556
+ end
557
+ end
558
+
559
+ def convert_inline_kbd node
560
+ if (keys = node.attr 'keys').size == 1
561
+ %(<keycap>#{keys[0]}</keycap>)
562
+ else
563
+ %(<keycombo><keycap>#{keys.join '</keycap><keycap>'}</keycap></keycombo>)
564
+ end
565
+ end
566
+
567
+ def convert_inline_menu node
568
+ menu = node.attr 'menu'
569
+ if (submenus = node.attr 'submenus').empty?
570
+ if (menuitem = node.attr 'menuitem')
571
+ %(<menuchoice><guimenu>#{menu}</guimenu> <guimenuitem>#{menuitem}</guimenuitem></menuchoice>)
572
+ else
573
+ %(<guimenu>#{menu}</guimenu>)
574
+ end
575
+ else
576
+ %(<menuchoice><guimenu>#{menu}</guimenu> <guisubmenu>#{submenus.join '</guisubmenu> <guisubmenu>'}</guisubmenu> <guimenuitem>#{node.attr 'menuitem'}</guimenuitem></menuchoice>)
577
+ end
578
+ end
579
+
580
+ def convert_inline_quoted node
581
+ if (type = node.type) == :asciimath
582
+ # NOTE fop requires jeuclid to process mathml markup
583
+ asciimath_available? ? %(<inlineequation>#{(::AsciiMath.parse node.text).to_mathml 'mml:', 'xmlns:mml' => 'http://www.w3.org/1998/Math/MathML'}</inlineequation>) : %(<inlineequation><mathphrase><![CDATA[#{node.text}]]></mathphrase></inlineequation>)
584
+ elsif type == :latexmath
585
+ # unhandled math; pass source to alt and required mathphrase element; dblatex will process alt as LaTeX math
586
+ %(<inlineequation><alt><![CDATA[#{equation = node.text}]]></alt><mathphrase><![CDATA[#{equation}]]></mathphrase></inlineequation>)
587
+ else
588
+ open, close, supports_phrase = QUOTE_TAGS[type]
589
+ text = node.text
590
+ if node.role
591
+ if supports_phrase
592
+ quoted_text = %(#{open}<phrase role="#{node.role}">#{text}</phrase>#{close})
593
+ else
594
+ quoted_text = %(#{open.chop} role="#{node.role}">#{text}#{close})
595
+ end
596
+ else
597
+ quoted_text = %(#{open}#{text}#{close})
598
+ end
599
+
600
+ node.id ? %(<anchor#{common_attributes node.id, nil, text}/>#{quoted_text}) : quoted_text
601
+ end
602
+ end
603
+
604
+ private
605
+
606
+ def common_attributes id, role = nil, reftext = nil
607
+
608
+ attrs = ''
609
+
610
+ end
611
+
612
+ def image_size_attributes attributes
613
+
614
+ if attributes.key? 'scaledwidth'
615
+ %( width="#{attributes['scaledwidth']}")
616
+ elsif attributes.key? 'scale'
617
+
618
+ %( scale="#{attributes['scale']}")
619
+ else
620
+ width_attribute = (attributes.key? 'width') ? %( contentwidth="#{attributes['width']}") : ''
621
+ depth_attribute = (attributes.key? 'height') ? %( contentdepth="#{attributes['height']}") : ''
622
+ %(#{width_attribute}#{depth_attribute})
623
+ end
624
+ end
625
+
626
+ def author_tag doc, author
627
+ result = []
628
+ result << '<author>'
629
+ result << '<personname>'
630
+ result << %(<firstname>#{doc.sub_replacements author.firstname}</firstname>) if author.firstname
631
+ result << %(<othername>#{doc.sub_replacements author.middlename}</othername>) if author.middlename
632
+ result << %(<surname>#{doc.sub_replacements author.lastname}</surname>) if author.lastname
633
+ result << '</personname>'
634
+ result << %(<email>#{author.email}</email>) if author.email
635
+ result << '</author>'
636
+ result.join LF
637
+ end
638
+
639
+ def document_info_tag doc, abstract
640
+ result = ['<info>']
641
+ unless doc.notitle
642
+ if (title = doc.doctitle partition: true, use_fallback: true).subtitle?
643
+ result << %(<title>#{title.main}</title>
644
+ <subtitle>#{title.subtitle}</subtitle>)
645
+ else
646
+ result << %(<title>#{title}</title>)
647
+ end
648
+ end
649
+ if (date = (doc.attr? 'revdate') ? (doc.attr 'revdate') : ((doc.attr? 'reproducible') ? nil : (doc.attr 'docdate')))
650
+ result << %(<date>#{date}</date>)
651
+ end
652
+ if doc.attr? 'copyright'
653
+ CopyrightRx =~ (doc.attr 'copyright')
654
+ result << '<copyright>'
655
+ result << %(<holder>#{$1}</holder>)
656
+ result << %(<year>#{$2}</year>) if $2
657
+ result << '</copyright>'
658
+ end
659
+ if doc.header?
660
+ unless (authors = doc.authors).empty?
661
+ if authors.size > 1
662
+ result << '<authorgroup>'
663
+ authors.each {|author| result << (author_tag doc, author) }
664
+ result << '</authorgroup>'
665
+ else
666
+ result << (author_tag doc, (author = authors[0]))
667
+ result << %(<authorinitials>#{author.initials}</authorinitials>) if author.initials
668
+ end
669
+ end
670
+ if (doc.attr? 'revdate') && ((doc.attr? 'revnumber') || (doc.attr? 'revremark'))
671
+ result << %(<revhistory>
672
+ <revision>)
673
+ result << %(<revnumber>#{doc.attr 'revnumber'}</revnumber>) if doc.attr? 'revnumber'
674
+ result << %(<date>#{doc.attr 'revdate'}</date>) if doc.attr? 'revdate'
675
+ result << %(<authorinitials>#{doc.attr 'authorinitials'}</authorinitials>) if doc.attr? 'authorinitials'
676
+ result << %(<revremark>#{doc.attr 'revremark'}</revremark>) if doc.attr? 'revremark'
677
+ result << %(</revision>
678
+ </revhistory>)
679
+ end
680
+ if (doc.attr? 'front-cover-image') || (doc.attr? 'back-cover-image')
681
+ if (back_cover_tag = cover_tag doc, 'back')
682
+ result << (cover_tag doc, 'front', true)
683
+ result << back_cover_tag
684
+ elsif (front_cover_tag = cover_tag doc, 'front')
685
+ result << front_cover_tag
686
+ end
687
+ end
688
+ result << %(<orgname>#{doc.attr 'orgname'}</orgname>) if doc.attr? 'orgname'
689
+ unless (docinfo_content = doc.docinfo).empty?
690
+ result << docinfo_content
691
+ end
692
+ end
693
+ if abstract
694
+ abstract.set_option 'root'
695
+ result << (convert abstract, abstract.node_name)
696
+ abstract.remove_attr 'root-option'
697
+ end
698
+ result << '</info>'
699
+
700
+ result.join LF
701
+ end
702
+
703
+ def find_root_abstract doc
704
+ return unless doc.blocks?
705
+ if (first_block = doc.blocks[0]).context == :preamble
706
+ return unless (first_block = first_block.blocks[0])
707
+ elsif first_block.context == :section
708
+ return first_block if first_block.sectname == 'abstract'
709
+ return unless first_block.sectname == 'preface' && (first_block = first_block.blocks[0])
710
+ end
711
+ return first_block if first_block.style == 'abstract' && first_block.context == :open
712
+ end
713
+
714
+ def extract_abstract document, abstract
715
+ parent = abstract.parent
716
+ parent = parent.parent while parent != document && parent.blocks.length == 1
717
+ parent.blocks.delete_at 0
718
+ end
719
+
720
+ def restore_abstract abstract
721
+ abstract.parent.blocks.insert 0, abstract
722
+ end
723
+
724
+ def get_root_document node
725
+ while (node = node.document).nested?
726
+ node = node.parent_document
727
+ end
728
+ node
729
+ end
730
+
731
+ def generate_document_id doc
732
+ %(__#{doc.doctype}-root__)
733
+ end
734
+
735
+ # FIXME this should be handled through a template mechanism
736
+ def enclose_content node
737
+ node.content_model == :compound ? node.content : %(<simpara>#{node.content}</simpara>)
738
+ end
739
+
740
+ def title_tag node, optional = true
741
+ !optional || node.title? ? %(<title>#{node.title}</title>\n) : ''
742
+ end
743
+
744
+ def cover_tag doc, face, use_placeholder = false
745
+ if (cover_image = doc.attr %(#{face}-cover-image))
746
+ if (cover_image.include? ':') && ImageMacroRx =~ cover_image
747
+ target, attrlist = $1, $2
748
+ cover_image = doc.image_uri target
749
+ # NOTE scalefit="1" is the default for a cover image
750
+ size_attrs = image_size_attributes (AttributeList.new attrlist).parse %w(alt width height) if attrlist
751
+ else
752
+ size_attrs = ''
753
+ end
754
+ %(<cover role="#{face}">
755
+ <mediaobject>
756
+ <imageobject>
757
+ <imagedata fileref="#{cover_image}"#{size_attrs}/>
758
+ </imageobject>
759
+ </mediaobject>
760
+ </cover>)
761
+ elsif use_placeholder
762
+ %(<cover role="#{face}"/>)
763
+ end
764
+ end
765
+
766
+ def blockquote_tag node, tag_name = nil
767
+ if tag_name
768
+ start_tag, end_tag = %(<#{tag_name}), %(</#{tag_name}>)
769
+ else
770
+ start_tag, end_tag = '<blockquote', '</blockquote>'
771
+ end
772
+ result = [%(#{start_tag}#{common_attributes node.id, node.role, node.reftext}>)]
773
+ result << %(<title>#{node.title}</title>) if node.title?
774
+ if (node.attr? 'attribution') || (node.attr? 'citetitle')
775
+ result << '<attribution>'
776
+ result << (node.attr 'attribution') if node.attr? 'attribution'
777
+ result << %(<citetitle>#{node.attr 'citetitle'}</citetitle>) if node.attr? 'citetitle'
778
+ result << '</attribution>'
779
+ end
780
+ result << yield
781
+ result << end_tag
782
+ result.join LF
783
+ end
784
+
785
+ def asciimath_available?
786
+ (@asciimath_status ||= load_asciimath) == :loaded
787
+ end
788
+
789
+ def load_asciimath
790
+ (defined? ::AsciiMath.parse) ? :loaded : (Helpers.require_library 'asciimath', true, :warn).nil? ? :unavailable : :loaded
791
+ end
792
+ end
793
+ end