asciidoctor 0.1.3 → 0.1.4

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of asciidoctor might be problematic. Click here for more details.

Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.adoc +387 -0
  3. data/README.adoc +358 -348
  4. data/asciidoctor.gemspec +30 -9
  5. data/bin/asciidoctor +3 -0
  6. data/bin/asciidoctor-safe +3 -0
  7. data/compat/asciidoc.conf +76 -4
  8. data/lib/asciidoctor.rb +174 -79
  9. data/lib/asciidoctor/abstract_block.rb +131 -101
  10. data/lib/asciidoctor/abstract_node.rb +108 -26
  11. data/lib/asciidoctor/attribute_list.rb +1 -1
  12. data/lib/asciidoctor/backends/_stylesheets.rb +204 -62
  13. data/lib/asciidoctor/backends/base_template.rb +11 -22
  14. data/lib/asciidoctor/backends/docbook45.rb +158 -163
  15. data/lib/asciidoctor/backends/docbook5.rb +103 -0
  16. data/lib/asciidoctor/backends/html5.rb +662 -445
  17. data/lib/asciidoctor/block.rb +54 -44
  18. data/lib/asciidoctor/cli/invoker.rb +41 -20
  19. data/lib/asciidoctor/cli/options.rb +66 -20
  20. data/lib/asciidoctor/debug.rb +1 -1
  21. data/lib/asciidoctor/document.rb +265 -100
  22. data/lib/asciidoctor/extensions.rb +443 -0
  23. data/lib/asciidoctor/helpers.rb +38 -6
  24. data/lib/asciidoctor/inline.rb +5 -5
  25. data/lib/asciidoctor/lexer.rb +532 -250
  26. data/lib/asciidoctor/{list_item.rb → list.rb} +33 -13
  27. data/lib/asciidoctor/path_resolver.rb +28 -2
  28. data/lib/asciidoctor/reader.rb +814 -455
  29. data/lib/asciidoctor/renderer.rb +128 -42
  30. data/lib/asciidoctor/section.rb +55 -41
  31. data/lib/asciidoctor/substituters.rb +380 -107
  32. data/lib/asciidoctor/table.rb +40 -30
  33. data/lib/asciidoctor/version.rb +1 -1
  34. data/man/asciidoctor.1 +32 -96
  35. data/man/{asciidoctor.ad → asciidoctor.adoc} +57 -48
  36. data/test/attributes_test.rb +200 -27
  37. data/test/blocks_test.rb +361 -22
  38. data/test/document_test.rb +496 -81
  39. data/test/extensions_test.rb +448 -0
  40. data/test/fixtures/basic-docinfo-footer.html +6 -0
  41. data/test/fixtures/basic-docinfo-footer.xml +8 -0
  42. data/test/fixtures/basic-docinfo.xml +3 -3
  43. data/test/fixtures/basic.asciidoc +1 -0
  44. data/test/fixtures/child-include.adoc +5 -0
  45. data/test/fixtures/custom-backends/haml/docbook45/block_paragraph.xml.haml +6 -0
  46. data/test/fixtures/custom-backends/haml/html5-tweaks/block_paragraph.html.haml +1 -0
  47. data/test/fixtures/custom-backends/haml/html5/block_paragraph.html.haml +3 -0
  48. data/test/fixtures/custom-backends/haml/html5/block_sidebar.html.haml +5 -0
  49. data/test/fixtures/custom-backends/slim/docbook45/block_paragraph.xml.slim +6 -0
  50. data/test/fixtures/custom-backends/slim/html5/block_paragraph.html.slim +3 -0
  51. data/test/fixtures/custom-backends/slim/html5/block_sidebar.html.slim +5 -0
  52. data/test/fixtures/docinfo-footer.html +1 -0
  53. data/test/fixtures/docinfo-footer.xml +9 -0
  54. data/test/fixtures/docinfo.xml +1 -0
  55. data/test/fixtures/grandchild-include.adoc +3 -0
  56. data/test/fixtures/parent-include-restricted.adoc +5 -0
  57. data/test/fixtures/parent-include.adoc +5 -0
  58. data/test/invoker_test.rb +82 -8
  59. data/test/lexer_test.rb +21 -3
  60. data/test/links_test.rb +34 -2
  61. data/test/lists_test.rb +304 -7
  62. data/test/options_test.rb +19 -3
  63. data/test/paragraphs_test.rb +13 -0
  64. data/test/paths_test.rb +22 -0
  65. data/test/preamble_test.rb +20 -0
  66. data/test/reader_test.rb +1096 -644
  67. data/test/renderer_test.rb +152 -12
  68. data/test/sections_test.rb +417 -76
  69. data/test/substitutions_test.rb +339 -138
  70. data/test/tables_test.rb +109 -4
  71. data/test/test_helper.rb +79 -13
  72. data/test/text_test.rb +111 -11
  73. metadata +54 -18
@@ -3,74 +3,84 @@ module Asciidoctor
3
3
  #
4
4
  # Examples
5
5
  #
6
- # block = Asciidoctor::Block.new(document, :paragraph, ["`This` is a <test>"])
6
+ # block = Asciidoctor::Block.new(parent, :paragraph, :source => '_This_ is a <test>')
7
7
  # block.content
8
- # => ["<em>This</em> is a &lt;test&gt;"]
8
+ # => "<em>This</em> is a &lt;test&gt;"
9
9
  class Block < AbstractBlock
10
10
 
11
11
  # Public: Create alias for context to be consistent w/ AsciiDoc
12
12
  alias :blockname :context
13
13
 
14
- # Public: Get/Set the original Array content for this section block.
15
- attr_accessor :buffer
14
+ # Public: Get/Set the original Array content for this block, if applicable
15
+ attr_accessor :lines
16
16
 
17
17
  # Public: Initialize an Asciidoctor::Block object.
18
18
  #
19
- # parent - The parent Asciidoc Object.
20
- # context - The Symbol context name for the type of content.
21
- # buffer - The Array buffer of source data (default: nil).
22
-
23
- def initialize(parent, context, buffer = nil)
19
+ # parent - The parent AbstractBlock with a compound content model to which this Block will be appended.
20
+ # context - The Symbol context name for the type of content (e.g., :paragraph).
21
+ # opts - a Hash of options to customize block initialization: (default: {})
22
+ # * :content_model indicates whether blocks can be nested in this Block (:compound), otherwise
23
+ # how the lines should be processed (:simple, :verbatim, :raw, :empty). (default: :simple)
24
+ # * :attributes a Hash of attributes (key/value pairs) to assign to this Block. (default: {})
25
+ # * :source a String or Array of raw source for this Block. (default: nil)
26
+ #--
27
+ # QUESTION should we store source_data as lines for blocks that have compound content models?
28
+ def initialize(parent, context, opts = {})
24
29
  super(parent, context)
25
- @buffer = buffer
26
- end
27
-
28
- # Public: Get the rendered String content for this Block. If the block
29
- # has child blocks, the content method should cause them to be
30
- # rendered and returned as content that can be included in the
31
- # parent block's template.
32
- def render
33
- @document.playback_attributes @attributes
34
- out = renderer.render("block_#{@context}", self)
35
- @document.callouts.next_list if @context == :colist
36
- out
30
+ @content_model = opts.fetch(:content_model, nil) || :simple
31
+ @attributes = opts.fetch(:attributes, nil) || {}
32
+ @subs = opts[:subs] if opts.has_key? :subs
33
+ raw_source = opts.fetch(:source, nil) || nil
34
+ if raw_source.nil?
35
+ @lines = []
36
+ elsif raw_source.class == String
37
+ # FIXME make line normalization a utility method since it's used multiple times in code base!!
38
+ if ::Asciidoctor::FORCE_ENCODING
39
+ @lines = raw_source.lines.map {|line| "#{line.rstrip.force_encoding(::Encoding::UTF_8)}\n" }
40
+ else
41
+ @lines = raw_source.lines.map {|line| "#{line.rstrip}\n" }
42
+ end
43
+ if (last = @lines.pop)
44
+ @lines.push last.chomp
45
+ end
46
+ else
47
+ @lines = raw_source.dup
48
+ end
37
49
  end
38
50
 
39
- # Public: Get an HTML-ified version of the source buffer, with special
40
- # Asciidoc characters and entities converted to their HTML equivalents.
51
+ # Public: Get an rendered version of the block content, performing
52
+ # any substitutions on the content.
41
53
  #
42
54
  # Examples
43
55
  #
44
56
  # doc = Asciidoctor::Document.new
45
57
  # block = Asciidoctor::Block.new(doc, :paragraph,
46
- # ['`This` is what happens when you <meet> a stranger in the <alps>!'])
58
+ # :source => '_This_ is what happens when you <meet> a stranger in the <alps>!')
47
59
  # block.content
48
- # => ["<em>This</em> is what happens when you &lt;meet&gt; a stranger in the &lt;alps&gt;!"]
60
+ # => "<em>This</em> is what happens when you &lt;meet&gt; a stranger in the &lt;alps&gt;!"
49
61
  def content
50
- case @context
51
- when :preamble
52
- @blocks.map {|b| b.render }.join
53
- # lists get iterated in the template (for now)
54
- # list items recurse into this block when their text and content methods are called
55
- when :ulist, :olist, :dlist, :colist
56
- @buffer
57
- when :listing, :literal
58
- apply_literal_subs(@buffer)
59
- when :pass
60
- apply_passthrough_subs(@buffer)
61
- when :admonition, :example, :sidebar, :quote, :verse, :open
62
- if !@buffer.nil?
63
- apply_para_subs(@buffer)
64
- else
65
- @blocks.map {|b| b.render }.join
66
- end
62
+ case @content_model
63
+ when :compound
64
+ super
65
+ when :simple, :verbatim, :raw
66
+ apply_subs @lines.join, @subs
67
67
  else
68
- apply_para_subs(@buffer)
68
+ warn "Unknown content model '#@content_model' for block: #{to_s}" unless @content_model == :empty
69
+ nil
69
70
  end
70
71
  end
71
72
 
73
+ # Public: Returns the preprocessed source of this block
74
+ #
75
+ # Returns the a String containing the lines joined together or nil if there
76
+ # are no lines
77
+ def source
78
+ @lines.join
79
+ end
80
+
72
81
  def to_s
73
- "#{super.to_s} - #@context [blocks:#{(@blocks || []).size}]"
82
+ content_summary = @content_model == :compound ? %(# of blocks = #{@blocks.size}) : %(# of lines = #{@lines.size})
83
+ %(Block[@context: :#@context, @content_model: :#@content_model, #{content_summary}])
74
84
  end
75
85
  end
76
86
  end
@@ -3,11 +3,11 @@ module Asciidoctor
3
3
  # Public Invocation class for starting Asciidoctor via CLI
4
4
  class Invoker
5
5
  attr_reader :options
6
- attr_reader :document
6
+ attr_reader :documents
7
7
  attr_reader :code
8
8
 
9
9
  def initialize(*options)
10
- @document = nil
10
+ @documents = []
11
11
  @out = nil
12
12
  @err = nil
13
13
  @code = 0
@@ -31,13 +31,14 @@ module Asciidoctor
31
31
 
32
32
  begin
33
33
  opts = {}
34
- monitor = {}
35
- infile = nil
34
+ profile = false
35
+ infiles = []
36
36
  outfile = nil
37
+ tofile = nil
37
38
  @options.map {|k, v|
38
39
  case k
39
- when :input_file
40
- infile = v
40
+ when :input_files
41
+ infiles = v
41
42
  when :output_file
42
43
  outfile = v
43
44
  when :destination_dir
@@ -46,7 +47,7 @@ module Asciidoctor
46
47
  when :attributes
47
48
  opts[:attributes] = v.dup
48
49
  when :verbose
49
- opts[:monitor] = monitor if v
50
+ profile = true if v
50
51
  when :trace
51
52
  # currently, nothing
52
53
  else
@@ -54,28 +55,44 @@ module Asciidoctor
54
55
  end
55
56
  }
56
57
 
57
- if infile == '-'
58
- # allow use of block to supply stdin, particularly useful for tests
59
- input = block_given? ? yield : STDIN
58
+ if infiles.size == 1 && infiles.first == '-'
59
+ # allows use of block to supply stdin, particularly useful for tests
60
+ inputs = [block_given? ? yield : STDIN]
60
61
  else
61
- input = File.new(infile)
62
+ inputs = infiles.map {|infile| File.new infile}
62
63
  end
63
64
 
64
- if outfile == '-' || (infile == '-' && (outfile.to_s.empty? || outfile != '/dev/null'))
65
- opts[:to_file] = (@out || $stdout)
65
+ # NOTE: if infile is stdin, default to outfile as stout
66
+ if outfile == '-' || (infiles.size == 1 && infiles.first == '-' && outfile.to_s.empty?)
67
+ tofile = (@out || $stdout)
66
68
  elsif !outfile.nil?
67
- opts[:to_file] = outfile
69
+ tofile = outfile
70
+ opts[:mkdirs] = true
68
71
  else
72
+ tofile = nil
73
+ # automatically calculate outfile based on infile
69
74
  opts[:in_place] = true unless opts.has_key? :to_dir
75
+ opts[:mkdirs] = true
70
76
  end
71
77
 
72
- @document = Asciidoctor.render(input, opts)
78
+ original_opts = opts
79
+ inputs.each do |input|
80
+
81
+ opts = Helpers.clone_options(original_opts) if inputs.size > 1
82
+ opts[:to_file] = tofile unless tofile.nil?
83
+ opts[:monitor] = {} if profile
73
84
 
74
- # FIXME this should be :monitor, :profile or :timings rather than :verbose
75
- if @options[:verbose]
76
- puts "Time to read and parse source: #{'%05.5f' % monitor[:parse]}"
77
- puts "Time to render document: #{'%05.5f' % monitor[:render]}"
78
- puts "Total time to read, parse and render: #{'%05.5f' % monitor[:load_render]}"
85
+ @documents ||= []
86
+ @documents.push Asciidoctor.render(input, opts)
87
+
88
+ if profile
89
+ monitor = opts[:monitor]
90
+ err = (@err || $stderr)
91
+ err.puts "Input file: #{input.respond_to?(:path) ? input.path : '-'}"
92
+ err.puts " Time to read and parse source: #{'%05.5f' % monitor[:parse]}"
93
+ err.puts " Time to render document: #{monitor.has_key?(:render) ? '%05.5f' % monitor[:render] : 'n/a'}"
94
+ err.puts " Total time to read, parse and render: #{'%05.5f' % (monitor[:load_render] || monitor[:parse])}"
95
+ end
79
96
  end
80
97
  rescue Exception => e
81
98
  raise e if @options[:trace] || SystemExit === e
@@ -87,6 +104,10 @@ module Asciidoctor
87
104
  end
88
105
  end
89
106
 
107
+ def document
108
+ @documents.size > 0 ? @documents.first : nil
109
+ end
110
+
90
111
  def redirect_streams(out, err = nil)
91
112
  @out = out
92
113
  @err = err
@@ -8,11 +8,12 @@ module Asciidoctor
8
8
 
9
9
  def initialize(options = {})
10
10
  self[:attributes] = options[:attributes] || {}
11
- self[:input_file] = options[:input_file] || nil
11
+ self[:input_files] = options[:input_files] || nil
12
12
  self[:output_file] = options[:output_file] || nil
13
13
  self[:safe] = options[:safe] || SafeMode::UNSAFE
14
14
  self[:header_footer] = options[:header_footer] || true
15
- self[:template_dir] = options[:template_dir] || nil
15
+ self[:template_dirs] = options[:template_dirs] || nil
16
+ self[:template_engine] = options[:template_engine] || nil
16
17
  if options[:doctype]
17
18
  self[:attributes]['doctype'] = options[:doctype]
18
19
  end
@@ -34,8 +35,8 @@ module Asciidoctor
34
35
  def parse!(args)
35
36
  opts_parser = OptionParser.new do |opts|
36
37
  opts.banner = <<-EOS
37
- Usage: asciidoctor [OPTION]... [FILE]
38
- Translate the AsciiDoc source FILE into the backend output format (e.g., HTML 5, DocBook 4.5, etc.)
38
+ Usage: asciidoctor [OPTION]... FILE...
39
+ Translate the AsciiDoc source FILE or FILE(s) into the backend output format (e.g., HTML 5, DocBook 4.5, etc.)
39
40
  By default, the output is written to a file with the basename of the source file and the appropriate extension.
40
41
  Example: asciidoctor -b html5 source.asciidoc
41
42
 
@@ -47,8 +48,8 @@ Example: asciidoctor -b html5 source.asciidoc
47
48
  opts.on('-b', '--backend BACKEND', 'set output format backend (default: html5)') do |backend|
48
49
  self[:attributes]['backend'] = backend
49
50
  end
50
- opts.on('-d', '--doctype DOCTYPE', ['article', 'book', 'inline'],
51
- 'document type to use when rendering output: [article, book, inline] (default: article)') do |doc_type|
51
+ opts.on('-d', '--doctype DOCTYPE', ['article', 'book', 'manpage', 'inline'],
52
+ 'document type to use when rendering output: [article, book, manpage, inline] (default: article)') do |doc_type|
52
53
  self[:attributes]['doctype'] = doc_type
53
54
  end
54
55
  opts.on('-o', '--out-file FILE', 'output file (default: based on input file path); use - to output to STDOUT') do |output_file|
@@ -84,11 +85,25 @@ Example: asciidoctor -b html5 source.asciidoc
84
85
  'defined in the source document') do |attribs|
85
86
  attribs.each do |attrib|
86
87
  key, val = attrib.split '=', 2
88
+ # move leading ! to end for internal processing
89
+ #if val.nil? && key.start_with?('!')
90
+ # key = "#{key[1..-1]}!"
91
+ #end
87
92
  self[:attributes][key] = val || ''
88
93
  end
89
94
  end
90
- opts.on('-T', '--template-dir DIR', 'directory containing custom render templates the override the built-in set') do |template_dir|
91
- self[:template_dir] = template_dir
95
+ opts.on('-T', '--template-dir DIR', 'a directory containing custom render templates that override the built-in set (requires tilt gem)',
96
+ 'may be specified multiple times') do |template_dir|
97
+ if self[:template_dirs].nil?
98
+ self[:template_dirs] = [template_dir]
99
+ elsif self[:template_dirs].is_a? Array
100
+ self[:template_dirs].push template_dir
101
+ else
102
+ self[:template_dirs] = [self[:template_dirs], template_dir]
103
+ end
104
+ end
105
+ opts.on('-E', '--template-engine NAME', 'template engine to use for the custom render templates (loads gem on demand)') do |template_engine|
106
+ self[:template_engine] = template_engine
92
107
  end
93
108
  opts.on('-B', '--base-dir DIR', 'base directory containing the document and resources (default: directory of source file)') do |base_dir|
94
109
  self[:base_dir] = base_dir
@@ -113,23 +128,54 @@ Example: asciidoctor -b html5 source.asciidoc
113
128
  end
114
129
 
115
130
  begin
131
+ infiles = []
132
+ opts_parser.parse! args
133
+
134
+ if args.empty?
135
+ $stderr.puts opts_parser
136
+ return 1
137
+ end
138
+
116
139
  # shave off the file to process so that options errors appear correctly
117
- if args.last && (args.last == '-' || !args.last.start_with?('-'))
118
- self[:input_file] = args.pop
140
+ if args.size == 1 && args.first == '-'
141
+ infiles.push args.pop
142
+ elsif
143
+ args.each do |file|
144
+ if (file == '-' || file.start_with?('-'))
145
+ # warn, but don't panic; we may have enough to proceed, so we won't force a failure
146
+ $stderr.puts "asciidoctor: WARNING: extra arguments detected (unparsed arguments: #{args.map{|a| "'#{a}'"} * ', '}) or incorrect usage of stdin"
147
+ else
148
+ # TODO this glob may not be necessary as the shell should have already performed expansion
149
+ matches = Dir.glob file
150
+
151
+ if matches.empty?
152
+ $stderr.puts "asciidoctor: FAILED: input file #{file} missing or cannot be read"
153
+ return 1
154
+ end
155
+
156
+ infiles.concat matches
157
+ end
158
+ end
119
159
  end
120
- opts_parser.parse!(args)
121
- if args.size > 0
122
- # warn, but don't panic; we may have enough to proceed, so we won't force a failure
123
- $stderr.puts "asciidoctor: WARNING: extra arguments detected (unparsed arguments: #{args.map{|a| "'#{a}'"} * ', '})"
160
+
161
+ infiles.each do |file|
162
+ unless file == '-' || File.readable?(file)
163
+ $stderr.puts "asciidoctor: FAILED: input file #{file} missing or cannot be read"
164
+ return 1
165
+ end
124
166
  end
125
167
 
126
- if self[:input_file].nil? || self[:input_file].empty?
127
- $stderr.puts opts_parser
128
- return 1
129
- elsif self[:input_file] != '-' && !File.readable?(self[:input_file])
130
- $stderr.puts "asciidoctor: FAILED: input file #{self[:input_file]} missing or cannot be read"
131
- return 1
168
+ self[:input_files] = infiles
169
+
170
+ if !self[:template_dirs].nil?
171
+ begin
172
+ require 'tilt'
173
+ rescue LoadError
174
+ $stderr.puts 'asciidoctor: FAILED: tilt could not be loaded; to use a custom backend, you must have the tilt gem installed (gem install tilt)'
175
+ return 1
176
+ end
132
177
  end
178
+
133
179
  rescue OptionParser::MissingArgument
134
180
  $stderr.puts "asciidoctor: option #{$!.message}"
135
181
  $stdout.puts opts_parser
@@ -3,7 +3,7 @@ module Debug
3
3
  @show_debug = nil
4
4
 
5
5
  def self.debug
6
- puts yield if self.show_debug_output?
6
+ warn yield if self.show_debug_output?
7
7
  end
8
8
 
9
9
  def self.set_debug(value)
@@ -25,9 +25,12 @@ class Document < AbstractBlock
25
25
  end
26
26
 
27
27
  def save_to(block_attributes)
28
- block_attributes[:attribute_entries] ||= []
29
- block_attributes[:attribute_entries] << self
28
+ (block_attributes[:attribute_entries] ||= []) << self
30
29
  end
30
+
31
+ #def save_to_next_block(document)
32
+ # (document.attributes[:pending_attribute_entries] ||= []) << self
33
+ #end
31
34
  end
32
35
 
33
36
  # Public A read-only integer value indicating the level of security that
@@ -84,34 +87,55 @@ class Document < AbstractBlock
84
87
  # Public: A reference to the parent document of this nested document.
85
88
  attr_reader :parent_document
86
89
 
90
+ # Public: The extensions registry
91
+ attr_reader :extensions
92
+
87
93
  # Public: Initialize an Asciidoc object.
88
94
  #
89
95
  # data - The Array of Strings holding the Asciidoc source document. (default: [])
90
96
  # options - A Hash of options to control processing, such as setting the safe mode (:safe),
91
97
  # suppressing the header/footer (:header_footer) and attribute overrides (:attributes)
92
98
  # (default: {})
93
- # block - A block that can be used to retrieve external Asciidoc
94
- # data to include in this document.
95
99
  #
96
100
  # Examples
97
101
  #
98
102
  # data = File.readlines(filename)
99
103
  # doc = Asciidoctor::Document.new(data)
100
104
  # puts doc.render
101
- def initialize(data = [], options = {}, &block)
105
+ def initialize(data = [], options = {})
102
106
  super(self, :document)
103
- @renderer = nil
104
107
 
105
108
  if options[:parent]
106
109
  @parent_document = options.delete(:parent)
107
- # should we dup attributes here?
108
- options[:attributes] = @parent_document.attributes
109
110
  options[:base_dir] ||= @parent_document.base_dir
111
+ # QUESTION should we support setting attribute in parent document from nested document?
112
+ # NOTE we must dup or else all the assignments to the overrides clobbers the real attributes
113
+ @attribute_overrides = @parent_document.attributes.dup
110
114
  @safe = @parent_document.safe
111
115
  @renderer = @parent_document.renderer
116
+ initialize_extensions = false
117
+ @extensions = @parent_document.extensions
112
118
  else
113
119
  @parent_document = nil
120
+ # copy attributes map and normalize keys
121
+ # attribute overrides are attributes that can only be set from the commandline
122
+ # a direct assignment effectively makes the attribute a constant
123
+ # a nil value or name with leading or trailing ! will result in the attribute being unassigned
124
+ @attribute_overrides = (options[:attributes] || {}).inject({}) do |collector,(key,value)|
125
+ if key.start_with?('!')
126
+ key = key[1..-1]
127
+ value = nil
128
+ elsif key.end_with?('!')
129
+ key = key[0..-2]
130
+ value = nil
131
+ end
132
+ collector[key.downcase] = value
133
+ collector
134
+ end
114
135
  @safe = nil
136
+ @renderer = nil
137
+ initialize_extensions = Asciidoctor.const_defined?('Extensions')
138
+ @extensions = nil # initialize furthur down
115
139
  end
116
140
 
117
141
  @header = nil
@@ -120,22 +144,26 @@ class Document < AbstractBlock
120
144
  :footnotes => [],
121
145
  :links => [],
122
146
  :images => [],
123
- :indexterms => []
147
+ :indexterms => [],
148
+ :includes => Set.new,
124
149
  }
125
150
  @counters = {}
126
151
  @callouts = Callouts.new
152
+ @attributes_modified = Set.new
127
153
  @options = options
128
- # safely resolve the safe mode from const, int or string
129
- if @safe.nil? && !(safe_mode = @options[:safe])
130
- @safe = SafeMode::SECURE
131
- elsif safe_mode.is_a?(Fixnum)
132
- # be permissive in case API user wants to define new levels
133
- @safe = safe_mode
134
- else
135
- begin
136
- @safe = SafeMode.const_get(safe_mode.to_s.upcase).to_i
137
- rescue
138
- @safe = SafeMode::SECURE.to_i
154
+ unless @parent_document
155
+ # safely resolve the safe mode from const, int or string
156
+ if @safe.nil? && !(safe_mode = @options[:safe])
157
+ @safe = SafeMode::SECURE
158
+ elsif safe_mode.is_a?(Fixnum)
159
+ # be permissive in case API user wants to define new levels
160
+ @safe = safe_mode
161
+ else
162
+ begin
163
+ @safe = SafeMode.const_get(safe_mode.to_s.upcase).to_i
164
+ rescue
165
+ @safe = SafeMode::SECURE.to_i
166
+ end
139
167
  end
140
168
  end
141
169
  @options[:header_footer] = @options.fetch(:header_footer, false)
@@ -145,7 +173,10 @@ class Document < AbstractBlock
145
173
  @attributes['notitle'] = '' unless @options[:header_footer]
146
174
  @attributes['toc-placement'] = 'auto'
147
175
  @attributes['stylesheet'] = ''
148
- @attributes['linkcss'] = ''
176
+ @attributes['copycss'] = '' if @options[:header_footer]
177
+ @attributes['prewrap'] = ''
178
+ @attributes['attribute-undefined'] = COMPLIANCE[:attribute_undefined]
179
+ @attributes['attribute-missing'] = COMPLIANCE[:attribute_missing]
149
180
 
150
181
  # language strings
151
182
  # TODO load these based on language settings
@@ -160,11 +191,10 @@ class Document < AbstractBlock
160
191
  #@attributes['listing-caption'] = 'Listing'
161
192
  @attributes['table-caption'] = 'Table'
162
193
  @attributes['toc-title'] = 'Table of Contents'
163
-
164
- # attribute overrides are attributes that can only be set from the commandline
165
- # a direct assignment effectively makes the attribute a constant
166
- # assigning a nil value will result in the attribute being unset
167
- @attribute_overrides = options[:attributes] || {}
194
+ @attributes['manname-title'] = 'NAME'
195
+ @attributes['untitled-label'] = 'Untitled'
196
+ @attributes['version-label'] = 'Version'
197
+ @attributes['last-update-label'] = 'Last updated'
168
198
 
169
199
  @attribute_overrides['asciidoctor'] = ''
170
200
  @attribute_overrides['asciidoctor-version'] = VERSION
@@ -177,22 +207,27 @@ class Document < AbstractBlock
177
207
  # sync the embedded attribute w/ the value of options...do not allow override
178
208
  @attribute_overrides['embedded'] = @options[:header_footer] ? nil : ''
179
209
 
180
- # the only way to set the include-depth attribute is via the document options
181
- # 10 is the AsciiDoc default, though currently Asciidoctor only supports 1 level
182
- @attribute_overrides['include-depth'] ||= 10
210
+ # the only way to set the max-include-depth attribute is via the document options
211
+ # 64 is the AsciiDoc default
212
+ @attribute_overrides['max-include-depth'] ||= 64
213
+
214
+ # the only way to enable uri reads is via the document options, disabled by default
215
+ unless !@attribute_overrides['allow-uri-read'].nil?
216
+ @attribute_overrides['allow-uri-read'] = nil
217
+ end
183
218
 
184
219
  # if the base_dir option is specified, it overrides docdir as the root for relative paths
185
220
  # otherwise, the base_dir is the directory of the source file (docdir) or the current
186
221
  # directory of the input is a string
187
- if options[:base_dir].nil?
222
+ if @options[:base_dir].nil?
188
223
  if @attribute_overrides['docdir']
189
224
  @base_dir = @attribute_overrides['docdir'] = File.expand_path(@attribute_overrides['docdir'])
190
225
  else
191
- #puts 'asciidoctor: WARNING: setting base_dir is recommended when working with string documents' unless nested?
226
+ #warn 'asciidoctor: WARNING: setting base_dir is recommended when working with string documents' unless nested?
192
227
  @base_dir = @attribute_overrides['docdir'] = File.expand_path(Dir.pwd)
193
228
  end
194
229
  else
195
- @base_dir = @attribute_overrides['docdir'] = File.expand_path(options[:base_dir])
230
+ @base_dir = @attribute_overrides['docdir'] = File.expand_path(@options[:base_dir])
196
231
  end
197
232
 
198
233
  # allow common attributes backend and doctype to be set using options hash
@@ -205,18 +240,19 @@ class Document < AbstractBlock
205
240
  end
206
241
 
207
242
  if @safe >= SafeMode::SERVER
208
- # restrict document from setting linkcss, copycss, source-highlighter and backend
243
+ # restrict document from setting copycss, source-highlighter and backend
209
244
  @attribute_overrides['copycss'] ||= nil
210
245
  @attribute_overrides['source-highlighter'] ||= nil
211
246
  @attribute_overrides['backend'] ||= DEFAULT_BACKEND
212
247
  # restrict document from seeing the docdir and trim docfile to relative path
213
- if @attribute_overrides.has_key?('docfile') && @parent_document.nil?
248
+ if !@parent_document && @attribute_overrides.has_key?('docfile')
214
249
  @attribute_overrides['docfile'] = @attribute_overrides['docfile'][(@attribute_overrides['docdir'].length + 1)..-1]
215
250
  end
216
251
  @attribute_overrides['docdir'] = ''
217
252
  if @safe >= SafeMode::SECURE
218
- # assign linkcss (preventing css embedding) unless disabled from the commandline
219
- unless @attribute_overrides.fetch('linkcss', '').nil? || @attribute_overrides.has_key?('linkcss!')
253
+ # assign linkcss (preventing css embedding) unless explicitly disabled from the commandline or API
254
+ # effectively the same has "has key 'linkcss' and value == nil"
255
+ unless @attribute_overrides.fetch('linkcss', '').nil?
220
256
  @attribute_overrides['linkcss'] = ''
221
257
  end
222
258
  # restrict document from enabling icons
@@ -229,15 +265,20 @@ class Document < AbstractBlock
229
265
  # a nil value undefines the attribute
230
266
  if val.nil?
231
267
  @attributes.delete(key)
232
- # a negative key undefines the attribute
233
- elsif key.end_with? '!'
234
- @attributes.delete(key[0..-2])
268
+ # a negative key (trailing !) undefines the attribute
269
+ # NOTE already normalize above as key with nil value
270
+ #elsif key.end_with? '!'
271
+ # @attributes.delete(key[0..-2])
272
+ # a negative key (leading !) undefines the attribute
273
+ # NOTE already normalize above as key with nil value
274
+ #elsif key.start_with? '!'
275
+ # @attributes.delete(key[1..-1])
235
276
  # otherwise it's an attribute assignment
236
277
  else
237
278
  # a value ending in @ indicates this attribute does not override
238
279
  # an attribute with the same key in the document souce
239
280
  if val.is_a?(String) && val.end_with?('@')
240
- val.chop!
281
+ val = val.chop
241
282
  verdict = true
242
283
  end
243
284
  @attributes[key] = val
@@ -245,43 +286,55 @@ class Document < AbstractBlock
245
286
  verdict
246
287
  }
247
288
 
248
- @attributes['backend'] ||= DEFAULT_BACKEND
249
- @attributes['doctype'] ||= DEFAULT_DOCTYPE
250
- update_backend_attributes
251
- # make toc and numbered the default for the docbook backend
252
- # FIXME this doesn't take into account the backend being set in the document
253
- #if @attributes.has_key?('basebackend-docbook')
254
- # @attributes['toc'] = '' unless @attribute_overrides.has_key?('toc!')
255
- # @attributes['numbered'] = '' unless @attribute_overrides.has_key?('numbered!')
256
- #end
257
-
258
- if !@parent_document.nil?
259
- # don't need to do the extra processing within our own document
260
- @reader = Reader.new(data)
289
+ if !@parent_document
290
+ # setup default backend and doctype
291
+ @attributes['backend'] ||= DEFAULT_BACKEND
292
+ @attributes['doctype'] ||= DEFAULT_DOCTYPE
293
+ update_backend_attributes
294
+
295
+ #@attributes['indir'] = @attributes['docdir']
296
+ #@attributes['infile'] = @attributes['docfile']
297
+
298
+ # dynamic intrinstic attribute values
299
+ now = Time.new
300
+ @attributes['localdate'] ||= now.strftime('%Y-%m-%d')
301
+ @attributes['localtime'] ||= now.strftime('%H:%M:%S %Z')
302
+ @attributes['localdatetime'] ||= [@attributes['localdate'], @attributes['localtime']] * ' '
303
+
304
+ # docdate, doctime and docdatetime should default to
305
+ # localdate, localtime and localdatetime if not otherwise set
306
+ @attributes['docdate'] ||= @attributes['localdate']
307
+ @attributes['doctime'] ||= @attributes['localtime']
308
+ @attributes['docdatetime'] ||= @attributes['localdatetime']
309
+
310
+ # fallback directories
311
+ @attributes['stylesdir'] ||= '.'
312
+ @attributes['iconsdir'] ||= File.join(@attributes.fetch('imagesdir', './images'), 'icons')
313
+
314
+ @extensions = initialize_extensions ? Extensions::Registry.new(self) : nil
315
+ @reader = PreprocessorReader.new self, data, Asciidoctor::Reader::Cursor.new(@attributes['docfile'], @base_dir)
316
+
317
+ if @extensions && @extensions.preprocessors?
318
+ @extensions.load_preprocessors(self).each do |processor|
319
+ @reader = processor.process(@reader, @reader.lines) || @reader
320
+ end
321
+ end
261
322
  else
262
- @reader = Reader.new(data, self, true, &block)
323
+ # don't need to do the extra processing within our own document
324
+ # FIXME line info isn't reported correctly within include files in nested document
325
+ @reader = Reader.new data, options[:cursor]
263
326
  end
264
327
 
265
- # dynamic intrinstic attribute values
266
- now = Time.new
267
- @attributes['localdate'] ||= now.strftime('%Y-%m-%d')
268
- @attributes['localtime'] ||= now.strftime('%H:%M:%S %Z')
269
- @attributes['localdatetime'] ||= [@attributes['localdate'], @attributes['localtime']] * ' '
270
-
271
- # docdate, doctime and docdatetime should default to
272
- # localdate, localtime and localdatetime if not otherwise set
273
- @attributes['docdate'] ||= @attributes['localdate']
274
- @attributes['doctime'] ||= @attributes['localtime']
275
- @attributes['docdatetime'] ||= @attributes['localdatetime']
276
-
277
- # fallback directories
278
- @attributes['stylesdir'] ||= '.'
279
- @attributes['iconsdir'] ||= File.join(@attributes.fetch('imagesdir', './images'), 'icons')
280
-
281
328
  # Now parse the lines in the reader into blocks
282
329
  Lexer.parse(@reader, self, :header_only => @options.fetch(:parse_header_only, false))
283
330
 
284
331
  @callouts.rewind
332
+
333
+ if !@parent_document && @extensions && @extensions.treeprocessors?
334
+ @extensions.load_treeprocessors(self).each do |processor|
335
+ processor.process
336
+ end
337
+ end
285
338
  end
286
339
 
287
340
  # Public: Get the named counter and take the next number in the sequence.
@@ -363,7 +416,7 @@ class Document < AbstractBlock
363
416
  end
364
417
 
365
418
  def nested?
366
- !@parent_document.nil?
419
+ @parent_document ? true : false
367
420
  end
368
421
 
369
422
  def embedded?
@@ -371,14 +424,18 @@ class Document < AbstractBlock
371
424
  @attributes.has_key? 'embedded'
372
425
  end
373
426
 
427
+ def extensions?
428
+ @extensions ? true : false
429
+ end
430
+
374
431
  # Make the raw source for the Document available.
375
432
  def source
376
- @reader.source.join if @reader
433
+ @reader.source if @reader
377
434
  end
378
435
 
379
436
  # Make the raw source lines for the Document available.
380
437
  def source_lines
381
- @reader.source if @reader
438
+ @reader.source_lines if @reader
382
439
  end
383
440
 
384
441
  def doctype
@@ -389,24 +446,34 @@ class Document < AbstractBlock
389
446
  @attributes['backend']
390
447
  end
391
448
 
449
+ def basebackend? base
450
+ @attributes['basebackend'] == base
451
+ end
452
+
392
453
  # The title explicitly defined in the document attributes
393
454
  def title
394
455
  @attributes['title']
395
456
  end
396
457
 
397
458
  def title=(title)
398
- @header ||= Section.new self
459
+ @header ||= Section.new(self, 0)
399
460
  @header.title = title
400
461
  end
401
462
 
402
463
  # We need to be able to return some semblance of a title
403
- def doctitle
404
- if !(title = @attributes.fetch('title', '')).empty?
405
- title
464
+ def doctitle(opts = {})
465
+ if !(val = @attributes.fetch('title', '')).empty?
466
+ val = title
406
467
  elsif !(sect = first_section).nil? && sect.title?
407
- sect.title
468
+ val = sect.title
408
469
  else
409
- nil
470
+ return nil
471
+ end
472
+
473
+ if opts[:sanitize] && val.include?('<')
474
+ val.gsub(/<[^>]+>/, '').tr_s(' ', ' ').strip
475
+ else
476
+ val
410
477
  end
411
478
  end
412
479
  alias :name :doctitle
@@ -426,7 +493,7 @@ class Document < AbstractBlock
426
493
  end
427
494
 
428
495
  def notitle
429
- @attributes.has_key? 'notitle'
496
+ !@attributes.has_key?('showtitle') && @attributes.has_key?('notitle')
430
497
  end
431
498
 
432
499
  def noheader
@@ -439,27 +506,94 @@ class Document < AbstractBlock
439
506
  end
440
507
 
441
508
  def has_header?
442
- !@header.nil?
509
+ @header ? true : false
510
+ end
511
+
512
+ # Public: Append a content Block to this Document.
513
+ #
514
+ # If the child block is a Section, assign an index to it.
515
+ #
516
+ # block - The child Block to append to this parent Block
517
+ #
518
+ # Returns nothing.
519
+ def <<(block)
520
+ super
521
+ if block.context == :section
522
+ assign_index block
523
+ end
524
+ end
525
+
526
+ # Internal: called after the header has been parsed and before the content
527
+ # will be parsed.
528
+ #--
529
+ # QUESTION should we invoke the Treeprocessors here, passing in a phase?
530
+ # QUESTION is finalize_header the right name?
531
+ def finalize_header unrooted_attributes, header_valid = true
532
+ clear_playback_attributes unrooted_attributes
533
+ save_attributes
534
+ unrooted_attributes['invalid-header'] = true unless header_valid
535
+ unrooted_attributes
443
536
  end
444
537
 
445
538
  # Internal: Branch the attributes so that the original state can be restored
446
539
  # at a future time.
447
540
  def save_attributes
541
+ # enable toc and numbered by default in DocBook backend
542
+ # NOTE the attributes_modified should go away once we have a proper attribute storage & tracking facility
543
+ if @attributes['basebackend'] == 'docbook'
544
+ @attributes['toc'] = '' unless attribute_locked?('toc') || @attributes_modified.include?('toc')
545
+ @attributes['numbered'] = '' unless attribute_locked?('numbered') || @attributes_modified.include?('numbered')
546
+ end
547
+
448
548
  unless @attributes.has_key?('doctitle') || (val = doctitle).nil?
449
549
  @attributes['doctitle'] = val
450
550
  end
451
551
 
452
552
  # css-signature cannot be updated after header attributes are processed
453
- if @id.nil? && @attributes.has_key?('css-signature')
553
+ if !@id && @attributes.has_key?('css-signature')
454
554
  @id = @attributes['css-signature']
455
555
  end
456
556
 
457
- if @attributes.has_key? 'toc2'
557
+ toc_val = @attributes['toc']
558
+ toc2_val = @attributes['toc2']
559
+ toc_position_val = @attributes['toc-position']
560
+
561
+ if (!toc_val.nil? && (toc_val != '' || toc_position_val.to_s != '')) || !toc2_val.nil?
562
+ default_toc_position = 'left'
563
+ default_toc_class = 'toc2'
564
+ position = [toc_position_val, toc2_val, toc_val].find {|pos| pos.to_s != ''}
565
+ position = default_toc_position if !position && !toc2_val.nil?
458
566
  @attributes['toc'] = ''
459
- @attributes['toc-class'] ||= 'toc2'
567
+ case position
568
+ when 'left', '<', '&lt;'
569
+ @attributes['toc-position'] = 'left'
570
+ when 'right', '>', '&gt;'
571
+ @attributes['toc-position'] = 'right'
572
+ when 'top', '^'
573
+ @attributes['toc-position'] = 'top'
574
+ when 'bottom', 'v'
575
+ @attributes['toc-position'] = 'bottom'
576
+ when 'center'
577
+ @attributes.delete('toc2')
578
+ default_toc_class = nil
579
+ default_toc_position = 'center'
580
+ end
581
+ @attributes['toc-class'] ||= default_toc_class if default_toc_class
582
+ @attributes['toc-position'] ||= default_toc_position if default_toc_position
460
583
  end
461
584
 
462
585
  @original_attributes = @attributes.dup
586
+
587
+ # unfreeze "flexible" attributes
588
+ unless nested?
589
+ FLEXIBLE_ATTRIBUTES.each do |name|
590
+ # turning a flexible attribute off should be permanent
591
+ # (we may need more config if that's not always the case)
592
+ if @attribute_overrides.has_key?(name) && !@attribute_overrides[name].nil?
593
+ @attribute_overrides.delete(name)
594
+ end
595
+ end
596
+ end
463
597
  end
464
598
 
465
599
  # Internal: Restore the attributes to the previously saved state
@@ -501,6 +635,7 @@ class Document < AbstractBlock
501
635
  false
502
636
  else
503
637
  @attributes[name] = apply_attribute_value_subs(value)
638
+ @attributes_modified << name
504
639
  if name == 'backend'
505
640
  update_backend_attributes()
506
641
  end
@@ -520,6 +655,7 @@ class Document < AbstractBlock
520
655
  false
521
656
  else
522
657
  @attributes.delete(name)
658
+ @attributes_modified << name
523
659
  true
524
660
  end
525
661
  end
@@ -530,7 +666,7 @@ class Document < AbstractBlock
530
666
  #
531
667
  # Returns true if the attribute is locked, false otherwise
532
668
  def attribute_locked?(name)
533
- @attribute_overrides.has_key?(name) || @attribute_overrides.has_key?("#{name}!")
669
+ @attribute_overrides.has_key?(name)
534
670
  end
535
671
 
536
672
  # Internal: Apply substitutions to the attribute value
@@ -546,12 +682,9 @@ class Document < AbstractBlock
546
682
  if value.match(REGEXP[:pass_macro_basic])
547
683
  # copy match for Ruby 1.8.7 compat
548
684
  m = $~
549
- subs = []
550
685
  if !m[1].empty?
551
- subs = resolve_subs(m[1])
552
- end
553
- if !subs.empty?
554
- apply_subs(m[2], subs)
686
+ subs = resolve_pass_subs m[1]
687
+ subs.empty? ? m[2] : apply_subs(m[2], subs)
555
688
  else
556
689
  m[2]
557
690
  end
@@ -566,7 +699,7 @@ class Document < AbstractBlock
566
699
  if BACKEND_ALIASES.has_key? backend
567
700
  backend = @attributes['backend'] = BACKEND_ALIASES[backend]
568
701
  end
569
- basebackend = backend.sub(/[[:digit:]]+$/, '')
702
+ basebackend = backend.sub(REGEXP[:trailing_digit], '')
570
703
  page_width = DEFAULT_PAGE_WIDTHS[basebackend]
571
704
  if page_width
572
705
  @attributes['pagewidth'] = page_width
@@ -593,9 +726,12 @@ class Document < AbstractBlock
593
726
 
594
727
  # Load up relevant Document @options
595
728
  if @options.has_key? :template_dir
596
- render_options[:template_dir] = @options[:template_dir]
729
+ render_options[:template_dirs] = [@options[:template_dir]]
730
+ elsif @options.has_key? :template_dirs
731
+ render_options[:template_dirs] = @options[:template_dirs]
597
732
  end
598
733
 
734
+ render_options[:template_cache] = @options.fetch(:template_cache, true)
599
735
  render_options[:backend] = @attributes.fetch('backend', 'html5')
600
736
  render_options[:template_engine] = @options[:template_engine]
601
737
  render_options[:eruby] = @options.fetch(:eruby, 'erb')
@@ -614,23 +750,42 @@ class Document < AbstractBlock
614
750
  def render(opts = {})
615
751
  restore_attributes
616
752
  r = renderer(opts)
753
+
754
+ # QUESTION should we add Preserializeprocessors? is it the right name?
755
+ #if !@parent_document && @extensions && @extensions.preserializeprocessors?
756
+ # @extensions.load_preserializeprocessors(self).each do |processor|
757
+ # processor.process r
758
+ # end
759
+ #end
760
+
617
761
  if doctype == 'inline'
618
762
  # QUESTION should we warn if @blocks.size > 0 and the first block is not a paragraph?
619
- if @blocks.size > 0 && (block = @blocks.first).context == :paragraph
620
- block.content
763
+ if !(block = @blocks.first).nil? && block.content_model != :compound
764
+ output = block.content
621
765
  else
622
- ''
766
+ output = ''
623
767
  end
624
768
  else
625
- @options.merge(opts)[:header_footer] ? r.render('document', self).strip : r.render('embedded', self)
769
+ output = @options.merge(opts)[:header_footer] ? r.render('document', self).strip : r.render('embedded', self)
626
770
  end
771
+
772
+ if !@parent_document && @extensions
773
+ if @extensions.postprocessors?
774
+ @extensions.load_postprocessors(self).each do |processor|
775
+ output = processor.process output
776
+ end
777
+ end
778
+ @extensions.reset
779
+ end
780
+
781
+ output
627
782
  end
628
783
 
629
784
  def content
630
785
  # per AsciiDoc-spec, remove the title before rendering the body,
631
786
  # regardless of whether the header is rendered)
632
787
  @attributes.delete('title')
633
- @blocks.map {|b| b.render }.join
788
+ super
634
789
  end
635
790
 
636
791
  # Public: Read the docinfo file(s) for inclusion in the
@@ -640,14 +795,21 @@ class Document < AbstractBlock
640
795
  # attribute is set, read the doc-name.docinfo.ext file. If the docinfo2
641
796
  # attribute is set, read both files in that order.
642
797
  #
798
+ # pos - The Symbol position of the docinfo, either :header or :footer. (default: :header)
643
799
  # ext - The extension of the docinfo file(s). If not set, the extension
644
800
  # will be determined based on the basebackend. (default: nil)
645
801
  #
646
802
  # returns The contents of the docinfo file(s)
647
- def docinfo(ext = nil)
803
+ def docinfo(pos = :header, ext = nil)
648
804
  if safe >= SafeMode::SECURE
649
805
  ''
650
806
  else
807
+ case pos
808
+ when :footer
809
+ qualifier = '-footer'
810
+ else
811
+ qualifier = nil
812
+ end
651
813
  ext = @attributes['outfilesuffix'] if ext.nil?
652
814
 
653
815
  content = nil
@@ -655,21 +817,24 @@ class Document < AbstractBlock
655
817
  docinfo = @attributes.has_key?('docinfo')
656
818
  docinfo1 = @attributes.has_key?('docinfo1')
657
819
  docinfo2 = @attributes.has_key?('docinfo2')
658
- docinfo_filename = "docinfo#{ext}"
820
+ docinfo_filename = "docinfo#{qualifier}#{ext}"
659
821
  if docinfo1 || docinfo2
660
822
  docinfo_path = normalize_system_path(docinfo_filename)
661
823
  content = read_asset(docinfo_path)
824
+ content = sub_attributes(content.lines.entries).join unless content.nil?
662
825
  end
663
826
 
664
827
  if (docinfo || docinfo2) && @attributes.has_key?('docname')
665
828
  docinfo_path = normalize_system_path("#{@attributes['docname']}-#{docinfo_filename}")
666
829
  content2 = read_asset(docinfo_path)
667
830
  unless content2.nil?
831
+ content2 = sub_attributes(content2.lines.entries).join
668
832
  content = content.nil? ? content2 : "#{content}\n#{content2}"
669
833
  end
670
834
  end
671
835
 
672
- content.nil? ? '' : content
836
+ # to_s forces nil to empty string
837
+ content.to_s
673
838
  end
674
839
  end
675
840