asciidoctor 2.0.10 → 2.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.adoc +294 -30
  3. data/LICENSE +1 -1
  4. data/README-de.adoc +16 -20
  5. data/README-fr.adoc +15 -22
  6. data/README-jp.adoc +15 -26
  7. data/README-zh_CN.adoc +21 -25
  8. data/README.adoc +161 -138
  9. data/asciidoctor.gemspec +6 -13
  10. data/data/locale/attributes-ar.adoc +4 -3
  11. data/data/locale/attributes-be.adoc +23 -0
  12. data/data/locale/attributes-bg.adoc +4 -3
  13. data/data/locale/attributes-ca.adoc +6 -5
  14. data/data/locale/attributes-cs.adoc +4 -3
  15. data/data/locale/attributes-da.adoc +6 -5
  16. data/data/locale/attributes-de.adoc +4 -4
  17. data/data/locale/attributes-en.adoc +4 -4
  18. data/data/locale/attributes-es.adoc +6 -5
  19. data/data/locale/attributes-fa.adoc +4 -3
  20. data/data/locale/attributes-fi.adoc +4 -3
  21. data/data/locale/attributes-fr.adoc +8 -7
  22. data/data/locale/attributes-hu.adoc +4 -3
  23. data/data/locale/attributes-id.adoc +4 -3
  24. data/data/locale/attributes-it.adoc +6 -5
  25. data/data/locale/attributes-ja.adoc +4 -3
  26. data/data/locale/{attributes-kr.adoc → attributes-ko.adoc} +4 -3
  27. data/data/locale/attributes-nb.adoc +4 -3
  28. data/data/locale/attributes-nl.adoc +6 -5
  29. data/data/locale/attributes-nn.adoc +4 -3
  30. data/data/locale/attributes-pl.adoc +8 -7
  31. data/data/locale/attributes-pt.adoc +6 -5
  32. data/data/locale/attributes-pt_BR.adoc +6 -5
  33. data/data/locale/attributes-ro.adoc +4 -3
  34. data/data/locale/attributes-ru.adoc +6 -5
  35. data/data/locale/attributes-sr.adoc +4 -4
  36. data/data/locale/attributes-sr_Latn.adoc +4 -4
  37. data/data/locale/attributes-sv.adoc +4 -4
  38. data/data/locale/attributes-th.adoc +23 -0
  39. data/data/locale/attributes-tr.adoc +4 -3
  40. data/data/locale/attributes-uk.adoc +6 -5
  41. data/data/locale/attributes-vi.adoc +23 -0
  42. data/data/locale/attributes-zh_CN.adoc +4 -3
  43. data/data/locale/attributes-zh_TW.adoc +4 -3
  44. data/data/reference/syntax.adoc +14 -7
  45. data/data/stylesheets/asciidoctor-default.css +76 -76
  46. data/data/stylesheets/coderay-asciidoctor.css +9 -9
  47. data/lib/asciidoctor/abstract_block.rb +20 -13
  48. data/lib/asciidoctor/abstract_node.rb +23 -12
  49. data/lib/asciidoctor/attribute_list.rb +64 -72
  50. data/lib/asciidoctor/block.rb +6 -6
  51. data/lib/asciidoctor/cli/invoker.rb +3 -2
  52. data/lib/asciidoctor/cli/options.rb +32 -31
  53. data/lib/asciidoctor/convert.rb +168 -162
  54. data/lib/asciidoctor/converter/docbook5.rb +49 -34
  55. data/lib/asciidoctor/converter/html5.rb +180 -139
  56. data/lib/asciidoctor/converter/manpage.rb +118 -90
  57. data/lib/asciidoctor/converter/template.rb +15 -13
  58. data/lib/asciidoctor/converter.rb +19 -16
  59. data/lib/asciidoctor/core_ext/hash/merge.rb +1 -1
  60. data/lib/asciidoctor/document.rb +77 -86
  61. data/lib/asciidoctor/extensions.rb +22 -16
  62. data/lib/asciidoctor/helpers.rb +20 -15
  63. data/lib/asciidoctor/list.rb +2 -6
  64. data/lib/asciidoctor/load.rb +103 -101
  65. data/lib/asciidoctor/logging.rb +10 -8
  66. data/lib/asciidoctor/parser.rb +211 -220
  67. data/lib/asciidoctor/path_resolver.rb +17 -15
  68. data/lib/asciidoctor/reader.rb +87 -79
  69. data/lib/asciidoctor/rx.rb +9 -7
  70. data/lib/asciidoctor/section.rb +7 -0
  71. data/lib/asciidoctor/substitutors.rb +167 -148
  72. data/lib/asciidoctor/syntax_highlighter/coderay.rb +3 -2
  73. data/lib/asciidoctor/syntax_highlighter/highlightjs.rb +13 -5
  74. data/lib/asciidoctor/syntax_highlighter/prettify.rb +7 -4
  75. data/lib/asciidoctor/syntax_highlighter/pygments.rb +19 -11
  76. data/lib/asciidoctor/syntax_highlighter/rouge.rb +35 -20
  77. data/lib/asciidoctor/syntax_highlighter.rb +16 -16
  78. data/lib/asciidoctor/table.rb +70 -43
  79. data/lib/asciidoctor/timings.rb +3 -3
  80. data/lib/asciidoctor/version.rb +1 -1
  81. data/lib/asciidoctor.rb +45 -19
  82. data/man/asciidoctor.1 +29 -31
  83. data/man/asciidoctor.adoc +35 -29
  84. metadata +17 -70
@@ -1,193 +1,199 @@
1
+ # frozen_string_literal: true
1
2
  module Asciidoctor
2
- module_function
3
+ class << self
4
+ # Public: Parse the AsciiDoc source input into an Asciidoctor::Document and
5
+ # convert it to the specified backend format.
6
+ #
7
+ # Accepts input as an IO (or StringIO), String or String Array object. If the
8
+ # input is a File, the object is expected to be opened for reading and is not
9
+ # closed afterwards by this method. Information about the file (filename,
10
+ # directory name, etc) gets assigned to attributes on the Document object.
11
+ #
12
+ # If the :to_file option is true, and the input is a File, the output is
13
+ # written to a file adjacent to the input file, having an extension that
14
+ # corresponds to the backend format. Otherwise, if the :to_file option is
15
+ # specified, the file is written to that file. If :to_file is not an absolute
16
+ # path, it is resolved relative to :to_dir, if given, otherwise the
17
+ # Document#base_dir. If the target directory does not exist, it will not be
18
+ # created unless the :mkdirs option is set to true. If the file cannot be
19
+ # written because the target directory does not exist, or because it falls
20
+ # outside of the Document#base_dir in safe mode, an IOError is raised.
21
+ #
22
+ # If the output is going to be written to a file, the header and footer are
23
+ # included unless specified otherwise (writing to a file implies creating a
24
+ # standalone document). Otherwise, the header and footer are not included by
25
+ # default and the converted result is returned.
26
+ #
27
+ # input - the String AsciiDoc source filename
28
+ # options - a String, Array or Hash of options to control processing (default: {})
29
+ # String and Array values are converted into a Hash.
30
+ # See Asciidoctor::Document#initialize for details about options.
31
+ #
32
+ # Returns the Document object if the converted String is written to a
33
+ # file, otherwise the converted String
34
+ def convert input, options = {}
35
+ (options = options.merge).delete :parse
36
+ to_dir = options.delete :to_dir
37
+ mkdirs = options.delete :mkdirs
3
38
 
4
- # Public: Parse the AsciiDoc source input into an Asciidoctor::Document and
5
- # convert it to the specified backend format.
6
- #
7
- # Accepts input as an IO (or StringIO), String or String Array object. If the
8
- # input is a File, the object is expected to be opened for reading and is not
9
- # closed afterwards by this method. Information about the file (filename,
10
- # directory name, etc) gets assigned to attributes on the Document object.
11
- #
12
- # If the :to_file option is true, and the input is a File, the output is
13
- # written to a file adjacent to the input file, having an extension that
14
- # corresponds to the backend format. Otherwise, if the :to_file option is
15
- # specified, the file is written to that file. If :to_file is not an absolute
16
- # path, it is resolved relative to :to_dir, if given, otherwise the
17
- # Document#base_dir. If the target directory does not exist, it will not be
18
- # created unless the :mkdirs option is set to true. If the file cannot be
19
- # written because the target directory does not exist, or because it falls
20
- # outside of the Document#base_dir in safe mode, an IOError is raised.
21
- #
22
- # If the output is going to be written to a file, the header and footer are
23
- # included unless specified otherwise (writing to a file implies creating a
24
- # standalone document). Otherwise, the header and footer are not included by
25
- # default and the converted result is returned.
26
- #
27
- # input - the String AsciiDoc source filename
28
- # options - a String, Array or Hash of options to control processing (default: {})
29
- # String and Array values are converted into a Hash.
30
- # See Asciidoctor::Document#initialize for details about options.
31
- #
32
- # Returns the Document object if the converted String is written to a
33
- # file, otherwise the converted String
34
- def convert input, options = {}
35
- (options = options.merge).delete :parse
36
- to_dir = options.delete :to_dir
37
- mkdirs = options.delete :mkdirs
38
-
39
- case (to_file = options.delete :to_file)
40
- when true, nil
41
- unless (write_to_target = to_dir)
42
- sibling_path = ::File.absolute_path input.path if ::File === input
39
+ case (to_file = options.delete :to_file)
40
+ when true, nil
41
+ unless (write_to_target = to_dir)
42
+ sibling_path = ::File.absolute_path input.path if ::File === input
43
+ end
44
+ to_file = nil
45
+ when false
46
+ to_file = nil
47
+ when '/dev/null'
48
+ return load input, options
49
+ else
50
+ options[:to_file] = write_to_target = to_file unless (stream_output = to_file.respond_to? :write)
43
51
  end
44
- to_file = nil
45
- when false
46
- to_file = nil
47
- when '/dev/null'
48
- return load input, options
49
- else
50
- options[:to_file] = write_to_target = to_file unless (stream_output = to_file.respond_to? :write)
51
- end
52
52
 
53
- unless options.key? :standalone
54
- if sibling_path || write_to_target
55
- options[:standalone] = options.fetch :header_footer, true
56
- elsif options.key? :header_footer
57
- options[:standalone] = options[:header_footer]
53
+ unless options.key? :standalone
54
+ if sibling_path || write_to_target
55
+ options[:standalone] = options.fetch :header_footer, true
56
+ elsif options.key? :header_footer
57
+ options[:standalone] = options[:header_footer]
58
+ end
58
59
  end
59
- end
60
60
 
61
- # NOTE outfile may be controlled by document attributes, so resolve outfile after loading
62
- if sibling_path
63
- options[:to_dir] = outdir = ::File.dirname sibling_path
64
- elsif write_to_target
65
- if to_dir
66
- if to_file
67
- options[:to_dir] = ::File.dirname ::File.expand_path ::File.join to_dir, to_file
68
- else
69
- options[:to_dir] = ::File.expand_path to_dir
61
+ # NOTE outfile may be controlled by document attributes, so resolve outfile after loading
62
+ if sibling_path
63
+ options[:to_dir] = outdir = ::File.dirname sibling_path
64
+ elsif write_to_target
65
+ if to_dir
66
+ if to_file
67
+ options[:to_dir] = ::File.dirname ::File.expand_path to_file, to_dir
68
+ else
69
+ options[:to_dir] = ::File.expand_path to_dir
70
+ end
71
+ elsif to_file
72
+ options[:to_dir] = ::File.dirname ::File.expand_path to_file
70
73
  end
71
- elsif to_file
72
- options[:to_dir] = ::File.dirname ::File.expand_path to_file
73
74
  end
74
- end
75
75
 
76
- # NOTE :to_dir is always set when outputting to a file
77
- # NOTE :to_file option only passed if assigned an explicit path
78
- doc = load input, options
76
+ # NOTE :to_dir is always set when outputting to a file
77
+ # NOTE :to_file option only passed if assigned an explicit path
78
+ doc = load input, options
79
79
 
80
- if sibling_path # write to file in same directory
81
- outfile = ::File.join outdir, %(#{doc.attributes['docname']}#{doc.outfilesuffix})
82
- raise ::IOError, %(input file and output file cannot be the same: #{outfile}) if outfile == sibling_path
83
- elsif write_to_target # write to explicit file or directory
84
- working_dir = (options.key? :base_dir) ? (::File.expand_path options[:base_dir]) : ::Dir.pwd
85
- # QUESTION should the jail be the working_dir or doc.base_dir???
86
- jail = doc.safe >= SafeMode::SAFE ? working_dir : nil
87
- if to_dir
88
- outdir = doc.normalize_system_path(to_dir, working_dir, jail, target_name: 'to_dir', recover: false)
89
- if to_file
90
- outfile = doc.normalize_system_path(to_file, outdir, nil, target_name: 'to_dir', recover: false)
91
- # reestablish outdir as the final target directory (in the case to_file had directory segments)
80
+ if sibling_path # write to file in same directory
81
+ outfile = ::File.join outdir, %(#{doc.attributes['docname']}#{doc.outfilesuffix})
82
+ raise ::IOError, %(input file and output file cannot be the same: #{outfile}) if outfile == sibling_path
83
+ elsif write_to_target # write to explicit file or directory
84
+ working_dir = (options.key? :base_dir) ? (::File.expand_path options[:base_dir]) : ::Dir.pwd
85
+ # QUESTION should the jail be the working_dir or doc.base_dir???
86
+ jail = doc.safe >= SafeMode::SAFE ? working_dir : nil
87
+ if to_dir
88
+ outdir = doc.normalize_system_path(to_dir, working_dir, jail, target_name: 'to_dir', recover: false)
89
+ if to_file
90
+ outfile = doc.normalize_system_path(to_file, outdir, nil, target_name: 'to_dir', recover: false)
91
+ # reestablish outdir as the final target directory (in the case to_file had directory segments)
92
+ outdir = ::File.dirname outfile
93
+ else
94
+ outfile = ::File.join outdir, %(#{doc.attributes['docname']}#{doc.outfilesuffix})
95
+ end
96
+ elsif to_file
97
+ outfile = doc.normalize_system_path(to_file, working_dir, jail, target_name: 'to_dir', recover: false)
98
+ # establish outdir as the final target directory (in the case to_file had directory segments)
92
99
  outdir = ::File.dirname outfile
93
- else
94
- outfile = ::File.join outdir, %(#{doc.attributes['docname']}#{doc.outfilesuffix})
95
100
  end
96
- elsif to_file
97
- outfile = doc.normalize_system_path(to_file, working_dir, jail, target_name: 'to_dir', recover: false)
98
- # establish outdir as the final target directory (in the case to_file had directory segments)
99
- outdir = ::File.dirname outfile
100
- end
101
101
 
102
- if ::File === input && outfile == (::File.absolute_path input.path)
103
- raise ::IOError, %(input file and output file cannot be the same: #{outfile})
102
+ if ::File === input && outfile == (::File.absolute_path input.path)
103
+ raise ::IOError, %(input file and output file cannot be the same: #{outfile})
104
+ end
105
+
106
+ if mkdirs
107
+ Helpers.mkdir_p outdir
108
+ else
109
+ # NOTE we intentionally refer to the directory as it was passed to the API
110
+ raise ::IOError, %(target directory does not exist: #{to_dir} (hint: set :mkdirs option)) unless ::File.directory? outdir
111
+ end
112
+ else # write to stream
113
+ outfile = to_file
114
+ outdir = nil
104
115
  end
105
116
 
106
- if mkdirs
107
- Helpers.mkdir_p outdir
117
+ if outfile && !stream_output
118
+ output = doc.convert 'outfile' => outfile, 'outdir' => outdir
108
119
  else
109
- # NOTE we intentionally refer to the directory as it was passed to the API
110
- raise ::IOError, %(target directory does not exist: #{to_dir} (hint: set :mkdirs option)) unless ::File.directory? outdir
120
+ output = doc.convert
111
121
  end
112
- else # write to stream
113
- outfile = to_file
114
- outdir = nil
115
- end
116
-
117
- if outfile && !stream_output
118
- output = doc.convert 'outfile' => outfile, 'outdir' => outdir
119
- else
120
- output = doc.convert
121
- end
122
122
 
123
- if outfile
124
- doc.write output, outfile
123
+ if outfile
124
+ doc.write output, outfile
125
125
 
126
- # NOTE document cannot control this behavior if safe >= SafeMode::SERVER
127
- # NOTE skip if stylesdir is a URI
128
- if !stream_output && doc.safe < SafeMode::SECURE && (doc.attr? 'linkcss') && (doc.attr? 'copycss') &&
129
- (doc.basebackend? 'html') && !((stylesdir = (doc.attr 'stylesdir')) && (Helpers.uriish? stylesdir))
130
- if (stylesheet = doc.attr 'stylesheet')
131
- if DEFAULT_STYLESHEET_KEYS.include? stylesheet
132
- copy_asciidoctor_stylesheet = true
133
- elsif !(Helpers.uriish? stylesheet)
134
- copy_user_stylesheet = true
135
- end
136
- end
137
- copy_syntax_hl_stylesheet = (syntax_hl = doc.syntax_highlighter) && (syntax_hl.write_stylesheet? doc)
138
- if copy_asciidoctor_stylesheet || copy_user_stylesheet || copy_syntax_hl_stylesheet
139
- stylesoutdir = doc.normalize_system_path(stylesdir, outdir, doc.safe >= SafeMode::SAFE ? outdir : nil)
140
- if mkdirs
141
- Helpers.mkdir_p stylesoutdir
142
- else
143
- raise ::IOError, %(target stylesheet directory does not exist: #{stylesoutdir} (hint: set :mkdirs option)) unless ::File.directory? stylesoutdir
126
+ # NOTE document cannot control this behavior if safe >= SafeMode::SERVER
127
+ # NOTE skip if stylesdir is a URI
128
+ if !stream_output && doc.safe < SafeMode::SECURE && (doc.attr? 'linkcss') && (doc.attr? 'copycss') &&
129
+ (doc.basebackend? 'html') && !((stylesdir = (doc.attr 'stylesdir')) && (Helpers.uriish? stylesdir))
130
+ if (stylesheet = doc.attr 'stylesheet')
131
+ if DEFAULT_STYLESHEET_KEYS.include? stylesheet
132
+ copy_asciidoctor_stylesheet = true
133
+ elsif !(Helpers.uriish? stylesheet)
134
+ copy_user_stylesheet = true
135
+ end
144
136
  end
145
-
146
- if copy_asciidoctor_stylesheet
147
- Stylesheets.instance.write_primary_stylesheet stylesoutdir
148
- # FIXME should Stylesheets also handle the user stylesheet?
149
- elsif copy_user_stylesheet
150
- if (stylesheet_src = doc.attr 'copycss').empty?
151
- stylesheet_src = doc.normalize_system_path stylesheet
137
+ copy_syntax_hl_stylesheet = (syntax_hl = doc.syntax_highlighter) && (syntax_hl.write_stylesheet? doc)
138
+ if copy_asciidoctor_stylesheet || copy_user_stylesheet || copy_syntax_hl_stylesheet
139
+ stylesoutdir = doc.normalize_system_path(stylesdir, outdir, doc.safe >= SafeMode::SAFE ? outdir : nil)
140
+ if mkdirs
141
+ Helpers.mkdir_p stylesoutdir
152
142
  else
153
- # NOTE in this case, copycss is a source location (but cannot be a URI)
154
- stylesheet_src = doc.normalize_system_path stylesheet_src
143
+ raise ::IOError, %(target stylesheet directory does not exist: #{stylesoutdir} (hint: set :mkdirs option)) unless ::File.directory? stylesoutdir
155
144
  end
156
- stylesheet_dest = doc.normalize_system_path stylesheet, stylesoutdir, (doc.safe >= SafeMode::SAFE ? outdir : nil)
157
- # NOTE don't warn if src can't be read and dest already exists (see #2323)
158
- if stylesheet_src != stylesheet_dest && (stylesheet_data = doc.read_asset stylesheet_src,
159
- warn_on_failure: !(::File.file? stylesheet_dest), label: 'stylesheet')
160
- ::File.write stylesheet_dest, stylesheet_data, mode: FILE_WRITE_MODE
145
+
146
+ if copy_asciidoctor_stylesheet
147
+ Stylesheets.instance.write_primary_stylesheet stylesoutdir
148
+ # FIXME should Stylesheets also handle the user stylesheet?
149
+ elsif copy_user_stylesheet
150
+ if (stylesheet_src = doc.attr 'copycss') == '' || stylesheet_src == true
151
+ stylesheet_src = doc.normalize_system_path stylesheet
152
+ else
153
+ # NOTE in this case, copycss is a source location (but cannot be a URI)
154
+ stylesheet_src = doc.normalize_system_path stylesheet_src.to_s
155
+ end
156
+ stylesheet_dest = doc.normalize_system_path stylesheet, stylesoutdir, (doc.safe >= SafeMode::SAFE ? outdir : nil)
157
+ # NOTE don't warn if src can't be read and dest already exists (see #2323)
158
+ if stylesheet_src != stylesheet_dest && (stylesheet_data = doc.read_asset stylesheet_src,
159
+ warn_on_failure: !(::File.file? stylesheet_dest), label: 'stylesheet')
160
+ if (stylesheet_outdir = ::File.dirname stylesheet_dest) != stylesoutdir && !(::File.directory? stylesheet_outdir)
161
+ if mkdirs
162
+ Helpers.mkdir_p stylesheet_outdir
163
+ else
164
+ raise ::IOError, %(target stylesheet directory does not exist: #{stylesheet_outdir} (hint: set :mkdirs option))
165
+ end
166
+ end
167
+ ::File.write stylesheet_dest, stylesheet_data, mode: FILE_WRITE_MODE
168
+ end
161
169
  end
170
+ syntax_hl.write_stylesheet doc, stylesoutdir if copy_syntax_hl_stylesheet
162
171
  end
163
- syntax_hl.write_stylesheet doc, stylesoutdir if copy_syntax_hl_stylesheet
164
172
  end
173
+ doc
174
+ else
175
+ output
165
176
  end
166
- doc
167
- else
168
- output
169
177
  end
170
- end
171
178
 
172
- # Public: Parse the contents of the AsciiDoc source file into an
173
- # Asciidoctor::Document and convert it to the specified backend format.
174
- #
175
- # input - the String AsciiDoc source filename
176
- # options - a String, Array or Hash of options to control processing (default: {})
177
- # String and Array values are converted into a Hash.
178
- # See Asciidoctor::Document#initialize for details about options.
179
- #
180
- # Returns the Document object if the converted String is written to a
181
- # file, otherwise the converted String
182
- def convert_file filename, options = {}
183
- ::File.open(filename, FILE_READ_MODE) {|file| convert file, options }
184
- end
179
+ # Public: Parse the contents of the AsciiDoc source file into an
180
+ # Asciidoctor::Document and convert it to the specified backend format.
181
+ #
182
+ # input - the String AsciiDoc source filename
183
+ # options - a String, Array or Hash of options to control processing (default: {})
184
+ # String and Array values are converted into a Hash.
185
+ # See Asciidoctor::Document#initialize for details about options.
186
+ #
187
+ # Returns the Document object if the converted String is written to a
188
+ # file, otherwise the converted String
189
+ def convert_file filename, options = {}
190
+ ::File.open(filename, FILE_READ_MODE) {|file| convert file, options }
191
+ end
185
192
 
186
- # Deprecated: Use {Asciidoctor.convert} instead.
187
- alias render convert
188
- module_function :render
193
+ # Deprecated: Use {Asciidoctor.convert} instead.
194
+ alias render convert
189
195
 
190
- # Deprecated: Use {Asciidoctor.convert_file} instead.
191
- alias render_file convert_file
192
- module_function :render_file
196
+ # Deprecated: Use {Asciidoctor.convert_file} instead.
197
+ alias render_file convert_file
198
+ end
193
199
  end
@@ -1,15 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
  module Asciidoctor
3
3
  # A built-in {Converter} implementation that generates DocBook 5 output. The output is inspired by the output produced
4
- # by the docbook45 backend from AsciiDoc Python, except it has been migrated to the DocBook 5 specification.
4
+ # by the docbook45 backend from AsciiDoc.py, except it has been migrated to the DocBook 5 specification.
5
5
  class Converter::DocBook5Converter < Converter::Base
6
6
  register_for 'docbook5'
7
7
 
8
8
  # default represents variablelist
9
9
  (DLIST_TAGS = {
10
- 'qanda' => { list: 'qandaset', entry: 'qandaentry', label: 'question', term: 'simpara', item: 'answer' },
11
- 'glossary' => { list: nil, entry: 'glossentry', term: 'glossterm', item: 'glossdef' },
12
- }).default = { list: 'variablelist', entry: 'varlistentry', term: 'term', item: 'listitem' }
10
+ 'qanda' => { list: 'qandaset', entry: 'qandaentry', label: 'question', term: 'simpara', item: 'answer' },
11
+ 'glossary' => { list: nil, entry: 'glossentry', term: 'glossterm', item: 'glossdef' },
12
+ }).default = { list: 'variablelist', entry: 'varlistentry', term: 'term', item: 'listitem' }
13
13
 
14
14
  (QUOTE_TAGS = {
15
15
  monospaced: ['<literal>', '</literal>'],
@@ -25,7 +25,7 @@ class Converter::DocBook5Converter < Converter::Base
25
25
  MANPAGE_SECTION_TAGS = { 'section' => 'refsection', 'synopsis' => 'refsynopsisdiv' }
26
26
  TABLE_PI_NAMES = ['dbhtml', 'dbfo', 'dblatex']
27
27
 
28
- CopyrightRx = /^(#{CC_ANY}+?)(?: ((?:\d{4}\-)?\d{4}))?$/
28
+ CopyrightRx = /^(#{CC_ANY}+?)(?: ((?:\d{4}-)?\d{4}))?$/
29
29
  ImageMacroRx = /^image::?(\S|\S#{CC_ANY}*?\S)\[(#{CC_ANY}+)?\]$/
30
30
 
31
31
  def initialize backend, opts = {}
@@ -41,7 +41,8 @@ class Converter::DocBook5Converter < Converter::Base
41
41
  if (root_tag_name = node.doctype) == 'manpage'
42
42
  root_tag_name = 'refentry'
43
43
  end
44
- result << %(<#{root_tag_name} xmlns="http://docbook.org/ns/docbook" xmlns:xl="http://www.w3.org/1999/xlink" version="5.0"#{lang_attribute}#{common_attributes node.id}>)
44
+ root_tag_idx = result.size
45
+ id = node.id
45
46
  result << (document_info_tag node) unless node.noheader
46
47
  unless (docinfo_content = node.docinfo :header).empty?
47
48
  result << docinfo_content
@@ -50,6 +51,9 @@ class Converter::DocBook5Converter < Converter::Base
50
51
  unless (docinfo_content = node.docinfo :footer).empty?
51
52
  result << docinfo_content
52
53
  end
54
+ id, node.id = node.id, nil unless id
55
+ # defer adding root tag in case document ID is auto-generated on demand
56
+ result.insert root_tag_idx, %(<#{root_tag_name} xmlns="http://docbook.org/ns/docbook" xmlns:xl="http://www.w3.org/1999/xlink" version="5.0"#{lang_attribute}#{common_attributes id}>)
53
57
  result << %(</#{root_tag_name}>)
54
58
  result.join LF
55
59
  end
@@ -298,13 +302,13 @@ class Converter::DocBook5Converter < Converter::Base
298
302
  </abstract>)
299
303
  end
300
304
  when 'partintro'
301
- unless node.level == 0 && node.parent.context == :section && node.document.doctype == 'book'
302
- logger.error 'partintro block can only be used when doctype is book and must be a child of a book part. Excluding block content.'
303
- ''
304
- else
305
+ if node.level == 0 && node.parent.context == :section && node.document.doctype == 'book'
305
306
  %(<partintro#{common_attributes node.id, node.role, node.reftext}>
306
307
  #{title_tag node}#{enclose_content node}
307
308
  </partintro>)
309
+ else
310
+ logger.error 'partintro block can only be used when doctype is book and must be a child of a book part. Excluding block content.'
311
+ ''
308
312
  end
309
313
  else
310
314
  reftext = node.reftext if (id = node.id)
@@ -371,24 +375,22 @@ class Converter::DocBook5Converter < Converter::Base
371
375
  has_body = false
372
376
  result = []
373
377
  pgwide_attribute = (node.option? 'pgwide') ? ' pgwide="1"' : ''
374
- if (frame = node.attr 'frame', 'all', 'table-frame') == 'ends'
375
- frame = 'topbot'
376
- end
378
+ frame = 'topbot' if (frame = node.attr 'frame', 'all', 'table-frame') == 'ends'
377
379
  grid = node.attr 'grid', nil, 'table-grid'
378
380
  result << %(<#{tag_name = node.title? ? 'table' : 'informaltable'}#{common_attributes node.id, node.role, node.reftext}#{pgwide_attribute} frame="#{frame}" rowsep="#{['none', 'cols'].include?(grid) ? 0 : 1}" colsep="#{['none', 'rows'].include?(grid) ? 0 : 1}"#{(node.attr? 'orientation', 'landscape', 'table-orientation') ? ' orient="land"' : ''}>)
379
- if (node.option? 'unbreakable')
381
+ if node.option? 'unbreakable'
380
382
  result << '<?dbfo keep-together="always"?>'
381
- elsif (node.option? 'breakable')
383
+ elsif node.option? 'breakable'
382
384
  result << '<?dbfo keep-together="auto"?>'
383
385
  end
384
386
  result << %(<title>#{node.title}</title>) if tag_name == 'table'
385
- col_width_key = if (width = (node.attr? 'width') ? (node.attr 'width') : nil)
387
+ if (width = (node.attr? 'width') ? (node.attr 'width') : nil)
386
388
  TABLE_PI_NAMES.each do |pi_name|
387
389
  result << %(<?#{pi_name} table-width="#{width}"?>)
388
390
  end
389
- 'colabswidth'
391
+ col_width_key = 'colabswidth'
390
392
  else
391
- 'colpcwidth'
393
+ col_width_key = 'colpcwidth'
392
394
  end
393
395
  result << %(<tgroup cols="#{node.attr 'colcount'}">)
394
396
  node.columns.each do |col|
@@ -401,12 +403,10 @@ class Converter::DocBook5Converter < Converter::Base
401
403
  rows.each do |row|
402
404
  result << '<row>'
403
405
  row.each do |cell|
404
- halign_attribute = (cell.attr? 'halign') ? %( align="#{cell.attr 'halign'}") : ''
405
- valign_attribute = (cell.attr? 'valign') ? %( valign="#{cell.attr 'valign'}") : ''
406
406
  colspan_attribute = cell.colspan ? %( namest="col_#{colnum = cell.column.attr 'colnumber'}" nameend="col_#{colnum + cell.colspan - 1}") : ''
407
407
  rowspan_attribute = cell.rowspan ? %( morerows="#{cell.rowspan - 1}") : ''
408
408
  # NOTE <entry> may not have whitespace (e.g., line breaks) as a direct descendant according to DocBook rules
409
- entry_start = %(<entry#{halign_attribute}#{valign_attribute}#{colspan_attribute}#{rowspan_attribute}>)
409
+ entry_start = %(<entry align="#{cell.attr 'halign'}" valign="#{cell.attr 'valign'}"#{colspan_attribute}#{rowspan_attribute}>)
410
410
  if tsec == :head
411
411
  cell_content = cell.text
412
412
  else
@@ -478,16 +478,22 @@ class Converter::DocBook5Converter < Converter::Base
478
478
  %(<anchor#{common_attributes((id = node.id), nil, node.reftext || %([#{id}]))}/>)
479
479
  when :xref
480
480
  if (path = node.attributes['path'])
481
- # QUESTION should we use refid as fallback text instead? (like the html5 backend?)
482
481
  %(<link xl:href="#{node.target}">#{node.text || path}</link>)
483
482
  else
484
- linkend = node.attributes['fragment'] || node.target
483
+ if (linkend = node.attributes['refid']).nil_or_empty?
484
+ root_doc = get_root_document node
485
+ # Q: should we warn instead of generating a document ID on demand?
486
+ linkend = (root_doc.id ||= generate_document_id root_doc)
487
+ end
488
+ # NOTE the xref tag in DocBook does not support explicit link text, so the link tag must be used instead
489
+ # The section at http://www.sagehill.net/docbookxsl/CrossRefs.html#IdrefLinks gives an explanation for this choice
490
+ # "link - a cross reference where you supply the text of the reference as the content of the link element."
485
491
  (text = node.text) ? %(<link linkend="#{linkend}">#{text}</link>) : %(<xref linkend="#{linkend}"/>)
486
492
  end
487
493
  when :link
488
494
  %(<link xl:href="#{node.target}">#{node.text}</link>)
489
495
  when :bibref
490
- %(<anchor#{common_attributes node.id, nil, "[#{node.reftext || node.id}]"}/>#{text})
496
+ %(<anchor#{common_attributes node.id, nil, (text = "[#{node.reftext || node.id}]")}/>#{text})
491
497
  else
492
498
  logger.warn %(unknown anchor type: #{node.type.inspect})
493
499
  nil
@@ -517,7 +523,7 @@ class Converter::DocBook5Converter < Converter::Base
517
523
  def convert_inline_image node
518
524
  width_attribute = (node.attr? 'width') ? %( contentwidth="#{node.attr 'width'}") : ''
519
525
  depth_attribute = (node.attr? 'height') ? %( contentdepth="#{node.attr 'height'}") : ''
520
- %(<inlinemediaobject>
526
+ %(<inlinemediaobject#{common_attributes nil, node.role}>
521
527
  <imageobject>
522
528
  <imagedata fileref="#{node.type == 'icon' ? (node.icon_uri node.target) : (node.image_uri node.target)}"#{width_attribute}#{depth_attribute}/>
523
529
  </imageobject>
@@ -537,9 +543,8 @@ class Converter::DocBook5Converter < Converter::Base
537
543
  %(<indexterm>
538
544
  <primary>#{node.text}</primary>#{rel}
539
545
  </indexterm>#{node.text})
540
- else
541
- if (numterms = (terms = node.attr 'terms').size) > 2
542
- %(<indexterm>
546
+ elsif (numterms = (terms = node.attr 'terms').size) > 2
547
+ %(<indexterm>
543
548
  <primary>#{terms[0]}</primary><secondary>#{terms[1]}</secondary><tertiary>#{terms[2]}</tertiary>#{rel}
544
549
  </indexterm>#{(node.document.option? 'indexterm-promotion') ? %[
545
550
  <indexterm>
@@ -548,18 +553,17 @@ class Converter::DocBook5Converter < Converter::Base
548
553
  <indexterm>
549
554
  <primary>#{terms[2]}</primary>
550
555
  </indexterm>] : ''})
551
- elsif numterms > 1
552
- %(<indexterm>
556
+ elsif numterms > 1
557
+ %(<indexterm>
553
558
  <primary>#{terms[0]}</primary><secondary>#{terms[1]}</secondary>#{rel}
554
- </indexterm>#{(node.document.option? 'indexterm-promotion') ? %[
559
+ </indexterm>#{(node.document.option? 'indexterm-promotion') ? %[
555
560
  <indexterm>
556
561
  <primary>#{terms[1]}</primary>
557
562
  </indexterm>] : ''})
558
- else
559
- %(<indexterm>
563
+ else
564
+ %(<indexterm>
560
565
  <primary>#{terms[0]}</primary>#{rel}
561
566
  </indexterm>)
562
- end
563
567
  end
564
568
  end
565
569
 
@@ -714,6 +718,17 @@ class Converter::DocBook5Converter < Converter::Base
714
718
  result.join LF
715
719
  end
716
720
 
721
+ def get_root_document node
722
+ while (node = node.document).nested?
723
+ node = node.parent_document
724
+ end
725
+ node
726
+ end
727
+
728
+ def generate_document_id doc
729
+ %(__#{doc.doctype}-root__)
730
+ end
731
+
717
732
  # FIXME this should be handled through a template mechanism
718
733
  def enclose_content node
719
734
  node.content_model == :compound ? node.content : %(<simpara>#{node.content}</simpara>)