asciidoctor 0.1.1 → 0.1.2
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.
- checksums.yaml +7 -0
- data/Gemfile +1 -1
- data/LICENSE +2 -2
- data/README.adoc +461 -0
- data/asciidoctor.gemspec +27 -16
- data/compat/asciidoc.conf +139 -0
- data/lib/asciidoctor.rb +212 -69
- data/lib/asciidoctor/abstract_block.rb +41 -0
- data/lib/asciidoctor/abstract_node.rb +128 -81
- data/lib/asciidoctor/attribute_list.rb +5 -2
- data/lib/asciidoctor/backends/base_template.rb +16 -4
- data/lib/asciidoctor/backends/docbook45.rb +112 -42
- data/lib/asciidoctor/backends/html5.rb +206 -90
- data/lib/asciidoctor/block.rb +5 -5
- data/lib/asciidoctor/cli/invoker.rb +38 -34
- data/lib/asciidoctor/cli/options.rb +3 -3
- data/lib/asciidoctor/document.rb +115 -13
- data/lib/asciidoctor/helpers.rb +16 -0
- data/lib/asciidoctor/lexer.rb +486 -359
- data/lib/asciidoctor/path_resolver.rb +360 -0
- data/lib/asciidoctor/reader.rb +122 -23
- data/lib/asciidoctor/renderer.rb +1 -33
- data/lib/asciidoctor/section.rb +1 -1
- data/lib/asciidoctor/substituters.rb +103 -19
- data/lib/asciidoctor/version.rb +1 -1
- data/man/asciidoctor.1 +6 -6
- data/man/asciidoctor.ad +5 -3
- data/stylesheets/asciidoctor.css +274 -0
- data/test/attributes_test.rb +133 -10
- data/test/blocks_test.rb +302 -17
- data/test/document_test.rb +269 -6
- data/test/fixtures/basic-docinfo.html +1 -0
- data/test/fixtures/basic-docinfo.xml +4 -0
- data/test/fixtures/basic.asciidoc +4 -0
- data/test/fixtures/docinfo.html +1 -0
- data/test/fixtures/docinfo.xml +2 -0
- data/test/fixtures/include-file.asciidoc +22 -1
- data/test/fixtures/stylesheets/custom.css +3 -0
- data/test/invoker_test.rb +38 -6
- data/test/lexer_test.rb +64 -21
- data/test/links_test.rb +4 -0
- data/test/lists_test.rb +251 -12
- data/test/paragraphs_test.rb +225 -30
- data/test/paths_test.rb +174 -0
- data/test/reader_test.rb +89 -2
- data/test/sections_test.rb +518 -16
- data/test/substitutions_test.rb +121 -10
- data/test/tables_test.rb +53 -13
- data/test/test_helper.rb +2 -2
- data/test/text_test.rb +5 -5
- metadata +46 -50
- data/README.asciidoc +0 -296
- data/lib/asciidoctor/errors.rb +0 -5
data/lib/asciidoctor/block.rb
CHANGED
@@ -26,6 +26,7 @@ class Block < AbstractBlock
|
|
26
26
|
def initialize(parent, context, buffer = nil)
|
27
27
|
super(parent, context)
|
28
28
|
@buffer = buffer
|
29
|
+
@caption = nil
|
29
30
|
end
|
30
31
|
|
31
32
|
# Public: Get the rendered String content for this Block. If the block
|
@@ -94,9 +95,8 @@ class Block < AbstractBlock
|
|
94
95
|
# block.content
|
95
96
|
# => ["<em>This</em> is what happens when you <meet> a stranger in the <alps>!"]
|
96
97
|
def content
|
97
|
-
|
98
98
|
case @context
|
99
|
-
when :preamble, :open
|
99
|
+
when :preamble, :open
|
100
100
|
@blocks.map {|b| b.render }.join
|
101
101
|
# lists get iterated in the template (for now)
|
102
102
|
# list items recurse into this block when their text and content methods are called
|
@@ -106,14 +106,14 @@ class Block < AbstractBlock
|
|
106
106
|
apply_literal_subs(@buffer)
|
107
107
|
when :pass
|
108
108
|
apply_passthrough_subs(@buffer)
|
109
|
-
when :
|
109
|
+
when :admonition, :example, :sidebar, :quote, :verse
|
110
110
|
if !@buffer.nil?
|
111
|
-
|
111
|
+
apply_para_subs(@buffer)
|
112
112
|
else
|
113
113
|
@blocks.map {|b| b.render }.join
|
114
114
|
end
|
115
115
|
else
|
116
|
-
|
116
|
+
apply_para_subs(@buffer)
|
117
117
|
end
|
118
118
|
end
|
119
119
|
|
@@ -5,14 +5,12 @@ module Asciidoctor
|
|
5
5
|
attr_reader :options
|
6
6
|
attr_reader :document
|
7
7
|
attr_reader :code
|
8
|
-
attr_reader :timings
|
9
8
|
|
10
9
|
def initialize(*options)
|
11
10
|
@document = nil
|
12
11
|
@out = nil
|
13
12
|
@err = nil
|
14
13
|
@code = 0
|
15
|
-
@timings = {}
|
16
14
|
options = options.flatten
|
17
15
|
if !options.empty? && options.first.is_a?(Cli::Options)
|
18
16
|
@options = options.first
|
@@ -32,46 +30,52 @@ module Asciidoctor
|
|
32
30
|
return if @options.nil?
|
33
31
|
|
34
32
|
begin
|
35
|
-
|
36
|
-
|
37
|
-
|
33
|
+
opts = {}
|
34
|
+
monitor = {}
|
35
|
+
infile = nil
|
36
|
+
outfile = nil
|
37
|
+
@options.map {|k, v|
|
38
|
+
case k
|
39
|
+
when :input_file
|
40
|
+
infile = v
|
41
|
+
when :output_file
|
42
|
+
outfile = v
|
43
|
+
when :destination_dir
|
44
|
+
#opts[:to_dir] = File.expand_path(v) unless v.nil?
|
45
|
+
opts[:to_dir] = v unless v.nil?
|
46
|
+
when :attributes
|
47
|
+
opts[:attributes] = v.dup
|
48
|
+
when :verbose
|
49
|
+
opts[:monitor] = monitor if v
|
50
|
+
when :trace
|
51
|
+
# currently, nothing
|
52
|
+
else
|
53
|
+
opts[k] = v unless v.nil?
|
54
|
+
end
|
55
|
+
}
|
56
|
+
|
38
57
|
if infile == '-'
|
39
58
|
# allow use of block to supply stdin, particularly useful for tests
|
40
59
|
input = block_given? ? yield : STDIN
|
41
60
|
else
|
42
61
|
input = File.new(infile)
|
43
62
|
end
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
timings[:render] = Time.now - start
|
50
|
-
if @options[:verbose]
|
51
|
-
puts "Time to read and parse source: #{timings[:parse]}"
|
52
|
-
puts "Time to render document: #{timings[:render]}"
|
53
|
-
puts "Total time to read, parse and render: #{timings.reduce(0) {|sum, (_, v)| sum += v}}"
|
54
|
-
end
|
55
|
-
if outfile == '/dev/null'
|
56
|
-
# output nothing
|
57
|
-
elsif outfile == '-' || (infile == '-' && (outfile.nil? || outfile.empty?))
|
58
|
-
(@out || $stdout).puts output
|
63
|
+
|
64
|
+
if outfile == '-' || (infile == '-' && (outfile.to_s.empty? || outfile != '/dev/null'))
|
65
|
+
opts[:to_file] = (@out || $stdout)
|
66
|
+
elsif !outfile.nil?
|
67
|
+
opts[:to_file] = outfile
|
59
68
|
else
|
60
|
-
|
61
|
-
|
62
|
-
destination_dir = File.expand_path(@options[:destination_dir])
|
63
|
-
else
|
64
|
-
destination_dir = @document.base_dir
|
65
|
-
end
|
66
|
-
outfile = File.join(destination_dir, "#{@document.attributes['docname']}#{@document.attributes['outfilesuffix']}")
|
67
|
-
else
|
68
|
-
outfile = @document.normalize_asset_path outfile
|
69
|
-
end
|
69
|
+
opts[:in_place] = true unless opts.has_key? :to_dir
|
70
|
+
end
|
70
71
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
72
|
+
@document = Asciidoctor.render(input, opts)
|
73
|
+
|
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]}"
|
75
79
|
end
|
76
80
|
rescue Exception => e
|
77
81
|
raise e if @options[:trace] || SystemExit === e
|
@@ -22,7 +22,7 @@ module Asciidoctor
|
|
22
22
|
self[:eruby] = options[:eruby] || nil
|
23
23
|
self[:compact] = options[:compact] || false
|
24
24
|
self[:verbose] = options[:verbose] || false
|
25
|
-
self[:base_dir] = options[:base_dir]
|
25
|
+
self[:base_dir] = options[:base_dir]
|
26
26
|
self[:destination_dir] = options[:destination_dir] || nil
|
27
27
|
self[:trace] = false
|
28
28
|
end
|
@@ -125,8 +125,8 @@ Example: asciidoctor -b html5 source.asciidoc
|
|
125
125
|
if self[:input_file].nil? || self[:input_file].empty?
|
126
126
|
$stderr.puts opts_parser
|
127
127
|
return 1
|
128
|
-
elsif self[:input_file] != '-' && !File.
|
129
|
-
$stderr.puts "asciidoctor: FAILED: input file #{self[:input_file]} missing"
|
128
|
+
elsif self[:input_file] != '-' && !File.readable?(self[:input_file])
|
129
|
+
$stderr.puts "asciidoctor: FAILED: input file #{self[:input_file]} missing or cannot be read"
|
130
130
|
return 1
|
131
131
|
end
|
132
132
|
rescue OptionParser::MissingArgument
|
data/lib/asciidoctor/document.rb
CHANGED
@@ -127,11 +127,12 @@ class Document < AbstractBlock
|
|
127
127
|
@safe = @options.fetch(:safe, SafeMode::SECURE).to_i
|
128
128
|
@options[:header_footer] = @options.fetch(:header_footer, false)
|
129
129
|
|
130
|
-
@attributes['asciidoctor'] = ''
|
131
|
-
@attributes['asciidoctor-version'] = VERSION
|
132
|
-
@attributes['sectids'] = ''
|
133
130
|
@attributes['encoding'] = 'UTF-8'
|
134
|
-
@attributes['
|
131
|
+
@attributes['sectids'] = ''
|
132
|
+
@attributes['notitle'] = '' unless @options[:header_footer]
|
133
|
+
@attributes['toc-placement'] = 'auto'
|
134
|
+
@attributes['stylesheet'] = ''
|
135
|
+
@attributes['linkcss'] = ''
|
135
136
|
|
136
137
|
# language strings
|
137
138
|
# TODO load these based on language settings
|
@@ -143,11 +144,26 @@ class Document < AbstractBlock
|
|
143
144
|
@attributes['appendix-caption'] = 'Appendix'
|
144
145
|
@attributes['example-caption'] = 'Example'
|
145
146
|
@attributes['figure-caption'] = 'Figure'
|
147
|
+
#@attributes['listing-caption'] = 'Listing'
|
146
148
|
@attributes['table-caption'] = 'Table'
|
147
149
|
@attributes['toc-title'] = 'Table of Contents'
|
148
150
|
|
151
|
+
# attribute overrides are attributes that can only be set from the commandline
|
152
|
+
# a direct assignment effectively makes the attribute a constant
|
153
|
+
# assigning a nil value will result in the attribute being unset
|
149
154
|
@attribute_overrides = options[:attributes] || {}
|
150
155
|
|
156
|
+
@attribute_overrides['asciidoctor'] = ''
|
157
|
+
@attribute_overrides['asciidoctor-version'] = VERSION
|
158
|
+
|
159
|
+
safe_mode_name = SafeMode.constants.detect {|l| SafeMode.const_get(l) == @safe}.to_s.downcase
|
160
|
+
@attribute_overrides['safe-mode-name'] = safe_mode_name
|
161
|
+
@attribute_overrides["safe-mode-#{safe_mode_name}"] = ''
|
162
|
+
@attribute_overrides['safe-mode-level'] = @safe
|
163
|
+
|
164
|
+
# sync the embedded attribute w/ the value of options...do not allow override
|
165
|
+
@attribute_overrides['embedded'] = @options[:header_footer] ? nil : ''
|
166
|
+
|
151
167
|
# the only way to set the include-depth attribute is via the document options
|
152
168
|
# 10 is the AsciiDoc default, though currently Asciidoctor only supports 1 level
|
153
169
|
@attribute_overrides['include-depth'] ||= 10
|
@@ -159,8 +175,8 @@ class Document < AbstractBlock
|
|
159
175
|
if @attribute_overrides['docdir']
|
160
176
|
@base_dir = @attribute_overrides['docdir'] = File.expand_path(@attribute_overrides['docdir'])
|
161
177
|
else
|
162
|
-
#
|
163
|
-
@base_dir = @attribute_overrides['docdir'] = Dir.pwd
|
178
|
+
#puts 'asciidoctor: WARNING: setting base_dir is recommended when working with string documents' unless nested?
|
179
|
+
@base_dir = @attribute_overrides['docdir'] = File.expand_path(Dir.pwd)
|
164
180
|
end
|
165
181
|
else
|
166
182
|
@base_dir = @attribute_overrides['docdir'] = File.expand_path(options[:base_dir])
|
@@ -176,7 +192,8 @@ class Document < AbstractBlock
|
|
176
192
|
end
|
177
193
|
|
178
194
|
if @safe >= SafeMode::SERVER
|
179
|
-
# restrict document from setting source-highlighter and backend
|
195
|
+
# restrict document from setting linkcss, copycss, source-highlighter and backend
|
196
|
+
@attribute_overrides['copycss'] ||= nil
|
180
197
|
@attribute_overrides['source-highlighter'] ||= nil
|
181
198
|
@attribute_overrides['backend'] ||= DEFAULT_BACKEND
|
182
199
|
# restrict document from seeing the docdir and trim docfile to relative path
|
@@ -184,17 +201,24 @@ class Document < AbstractBlock
|
|
184
201
|
@attribute_overrides['docfile'] = @attribute_overrides['docfile'][(@attribute_overrides['docdir'].length + 1)..-1]
|
185
202
|
end
|
186
203
|
@attribute_overrides['docdir'] = ''
|
187
|
-
# restrict document from enabling icons
|
188
204
|
if @safe >= SafeMode::SECURE
|
205
|
+
# assign linkcss (preventing css embedding) unless disabled from the commandline
|
206
|
+
unless @attribute_overrides.fetch('linkcss', '').nil? || @attribute_overrides.has_key?('linkcss!')
|
207
|
+
@attribute_overrides['linkcss'] = ''
|
208
|
+
end
|
209
|
+
# restrict document from enabling icons
|
189
210
|
@attribute_overrides['icons'] ||= nil
|
190
211
|
end
|
191
212
|
end
|
192
213
|
|
193
214
|
@attribute_overrides.delete_if {|key, val|
|
194
215
|
verdict = false
|
195
|
-
# a nil
|
196
|
-
if val.nil?
|
197
|
-
@attributes.delete(key
|
216
|
+
# a nil value undefines the attribute
|
217
|
+
if val.nil?
|
218
|
+
@attributes.delete(key)
|
219
|
+
# a negative key undefines the attribute
|
220
|
+
elsif key.end_with? '!'
|
221
|
+
@attributes.delete(key[0..-2])
|
198
222
|
# otherwise it's an attribute assignment
|
199
223
|
else
|
200
224
|
# a value ending in @ indicates this attribute does not override
|
@@ -211,6 +235,12 @@ class Document < AbstractBlock
|
|
211
235
|
@attributes['backend'] ||= DEFAULT_BACKEND
|
212
236
|
@attributes['doctype'] ||= DEFAULT_DOCTYPE
|
213
237
|
update_backend_attributes
|
238
|
+
# make toc and numbered the default for the docbook backend
|
239
|
+
# FIXME this doesn't take into account the backend being set in the document
|
240
|
+
#if @attributes.has_key?('basebackend-docbook')
|
241
|
+
# @attributes['toc'] = '' unless @attribute_overrides.has_key?('toc!')
|
242
|
+
# @attributes['numbered'] = '' unless @attribute_overrides.has_key?('numbered!')
|
243
|
+
#end
|
214
244
|
|
215
245
|
if !@parent_document.nil?
|
216
246
|
# don't need to do the extra processing within our own document
|
@@ -230,8 +260,10 @@ class Document < AbstractBlock
|
|
230
260
|
@attributes['docdate'] ||= @attributes['localdate']
|
231
261
|
@attributes['doctime'] ||= @attributes['localtime']
|
232
262
|
@attributes['docdatetime'] ||= @attributes['localdatetime']
|
233
|
-
|
234
|
-
|
263
|
+
|
264
|
+
# fallback directories
|
265
|
+
@attributes['stylesdir'] ||= '.'
|
266
|
+
@attributes['iconsdir'] ||= File.join(@attributes.fetch('imagesdir', './images'), 'icons')
|
235
267
|
|
236
268
|
# Now parse the lines in the reader into blocks
|
237
269
|
Lexer.parse(@reader, self, :header_only => @options.fetch(:parse_header_only, false))
|
@@ -269,6 +301,18 @@ class Document < AbstractBlock
|
|
269
301
|
(@attributes[name] = @counters[name])
|
270
302
|
end
|
271
303
|
|
304
|
+
# Public: Increment the specified counter and store it in the block's attributes
|
305
|
+
#
|
306
|
+
# counter_name - the String name of the counter attribute
|
307
|
+
# block - the Block on which to save the counter
|
308
|
+
#
|
309
|
+
# returns the next number in the sequence for the specified counter
|
310
|
+
def counter_increment(counter_name, block)
|
311
|
+
val = counter(counter_name)
|
312
|
+
AttributeEntry.new(counter_name, val).save_to(block.attributes)
|
313
|
+
val
|
314
|
+
end
|
315
|
+
|
272
316
|
# Internal: Get the next value in the sequence.
|
273
317
|
#
|
274
318
|
# Handles both integer and character sequences.
|
@@ -318,6 +362,11 @@ class Document < AbstractBlock
|
|
318
362
|
!@parent_document.nil?
|
319
363
|
end
|
320
364
|
|
365
|
+
def embedded?
|
366
|
+
# QUESTION should this be !@options[:header_footer] ?
|
367
|
+
@attributes.has_key? 'embedded'
|
368
|
+
end
|
369
|
+
|
321
370
|
# Make the raw source for the Document available.
|
322
371
|
def source
|
323
372
|
@reader.source.join if @reader
|
@@ -396,6 +445,12 @@ class Document < AbstractBlock
|
|
396
445
|
if @id.nil? && @attributes.has_key?('css-signature')
|
397
446
|
@id = @attributes['css-signature']
|
398
447
|
end
|
448
|
+
|
449
|
+
if @attributes.has_key? 'toc2'
|
450
|
+
@attributes['toc'] = ''
|
451
|
+
@attributes['toc-class'] ||= 'toc2'
|
452
|
+
end
|
453
|
+
|
399
454
|
@original_attributes = @attributes.dup
|
400
455
|
end
|
401
456
|
|
@@ -581,6 +636,53 @@ class Document < AbstractBlock
|
|
581
636
|
@blocks.map {|b| b.render }.join
|
582
637
|
end
|
583
638
|
|
639
|
+
# Public: Read the docinfo file(s) for inclusion in the
|
640
|
+
# document template
|
641
|
+
#
|
642
|
+
# If the docinfo1 attribute is set, read the docinfo.ext file. If the docinfo
|
643
|
+
# attribute is set, read the doc-name.docinfo.ext file. If the docinfo2
|
644
|
+
# attribute is set, read both files in that order.
|
645
|
+
#
|
646
|
+
# ext - The extension of the docinfo file(s). If not set, the extension
|
647
|
+
# will be determined based on the basebackend. (default: nil)
|
648
|
+
#
|
649
|
+
# returns The contents of the docinfo file(s)
|
650
|
+
def docinfo(ext = nil)
|
651
|
+
if safe >= SafeMode::SECURE
|
652
|
+
''
|
653
|
+
else
|
654
|
+
if ext.nil?
|
655
|
+
case @attributes['basebackend']
|
656
|
+
when 'docbook'
|
657
|
+
ext = '.xml'
|
658
|
+
when 'html'
|
659
|
+
ext = '.html'
|
660
|
+
end
|
661
|
+
end
|
662
|
+
|
663
|
+
content = nil
|
664
|
+
|
665
|
+
docinfo = @attributes.has_key?('docinfo')
|
666
|
+
docinfo1 = @attributes.has_key?('docinfo1')
|
667
|
+
docinfo2 = @attributes.has_key?('docinfo2')
|
668
|
+
docinfo_filename = "docinfo#{ext}"
|
669
|
+
if docinfo1 || docinfo2
|
670
|
+
docinfo_path = normalize_system_path(docinfo_filename)
|
671
|
+
content = read_asset(docinfo_path)
|
672
|
+
end
|
673
|
+
|
674
|
+
if (docinfo || docinfo2) && @attributes.has_key?('docname')
|
675
|
+
docinfo_path = normalize_system_path("#{@attributes['docname']}-#{docinfo_filename}")
|
676
|
+
content2 = read_asset(docinfo_path)
|
677
|
+
unless content2.nil?
|
678
|
+
content = content.nil? ? content2 : "#{content}\n#{content2}"
|
679
|
+
end
|
680
|
+
end
|
681
|
+
|
682
|
+
content.nil? ? '' : content
|
683
|
+
end
|
684
|
+
end
|
685
|
+
|
584
686
|
def to_s
|
585
687
|
%[#{super.to_s} - #{doctitle}]
|
586
688
|
end
|
data/lib/asciidoctor/helpers.rb
CHANGED
@@ -21,6 +21,22 @@ module Helpers
|
|
21
21
|
require name
|
22
22
|
end
|
23
23
|
|
24
|
+
# Public: Encode a string for inclusion in a URI
|
25
|
+
#
|
26
|
+
# str - the string to encode
|
27
|
+
#
|
28
|
+
# returns an encoded version of the str
|
29
|
+
def self.encode_uri(str)
|
30
|
+
str.gsub(REGEXP[:uri_encode_chars]) do
|
31
|
+
match = $&
|
32
|
+
buf = ''
|
33
|
+
match.each_byte do |c|
|
34
|
+
buf << sprintf('%%%02X', c)
|
35
|
+
end
|
36
|
+
buf
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
24
40
|
# Public: A generic capture output routine to be used in templates
|
25
41
|
#def self.capture_output(*args, &block)
|
26
42
|
# Proc.new { block.call(*args) }
|
data/lib/asciidoctor/lexer.rb
CHANGED
@@ -23,7 +23,7 @@ module Asciidoctor
|
|
23
23
|
# # => Asciidoctor::Block
|
24
24
|
class Lexer
|
25
25
|
|
26
|
-
BlockMatchData = Struct.new(:
|
26
|
+
BlockMatchData = Struct.new(:context, :masq, :tip, :terminator)
|
27
27
|
|
28
28
|
# Public: Make sure the Lexer object doesn't get initialized.
|
29
29
|
#
|
@@ -77,7 +77,7 @@ class Lexer
|
|
77
77
|
# check if the first line is the document title
|
78
78
|
# if so, add a header to the document and parse the header metadata
|
79
79
|
if is_next_line_document_title?(reader, block_attributes)
|
80
|
-
document.id, document.title, _, _ = parse_section_title(reader)
|
80
|
+
document.id, document.title, _, _ = parse_section_title(reader, document)
|
81
81
|
# QUESTION: should this be encapsulated in document?
|
82
82
|
if document.id.nil? && block_attributes.has_key?('id')
|
83
83
|
document.id = block_attributes.delete('id')
|
@@ -137,6 +137,8 @@ class Lexer
|
|
137
137
|
def self.next_section(reader, parent, attributes = {})
|
138
138
|
preamble = false
|
139
139
|
|
140
|
+
# FIXME if attributes[1] is a verbatim style, then don't check for section
|
141
|
+
|
140
142
|
# check if we are at the start of processing the document
|
141
143
|
# NOTE we could drop a hint in the attributes to indicate
|
142
144
|
# that we are at a section title (so we don't have to check)
|
@@ -164,7 +166,13 @@ class Lexer
|
|
164
166
|
# section title to next block of content
|
165
167
|
attributes = attributes.delete_if {|k, v| k != 'title'}
|
166
168
|
current_level = section.level
|
167
|
-
|
169
|
+
# subsections in preface & appendix in multipart books start at level 2
|
170
|
+
if current_level == 0 && section.special &&
|
171
|
+
section.document.doctype == 'book' && ['preface', 'appendix'].include?(section.sectname)
|
172
|
+
expected_next_levels = [current_level + 2]
|
173
|
+
else
|
174
|
+
expected_next_levels = [current_level + 1]
|
175
|
+
end
|
168
176
|
end
|
169
177
|
|
170
178
|
reader.skip_blank_lines
|
@@ -183,6 +191,7 @@ class Lexer
|
|
183
191
|
|
184
192
|
next_level = is_next_line_section? reader, attributes
|
185
193
|
if next_level
|
194
|
+
next_level += section.document.attr('leveloffset', 0).to_i
|
186
195
|
doctype = parent.document.doctype
|
187
196
|
if next_level == 0 && doctype != 'book'
|
188
197
|
puts "asciidoctor: ERROR: line #{reader.lineno + 1}: only book doctypes can contain level 0 sections"
|
@@ -215,7 +224,7 @@ class Lexer
|
|
215
224
|
reader.skip_blank_lines
|
216
225
|
end
|
217
226
|
|
218
|
-
#
|
227
|
+
# drop the preamble if it has no content
|
219
228
|
if preamble && preamble.blocks.empty?
|
220
229
|
section.delete_at(0)
|
221
230
|
end
|
@@ -241,357 +250,365 @@ class Lexer
|
|
241
250
|
# parent - The Document, Section or Block to which the next block belongs
|
242
251
|
#
|
243
252
|
# Returns a Section or Block object holding the parsed content of the processed lines
|
253
|
+
#--
|
254
|
+
# QUESTION should next_block have an option for whether it should keep looking until
|
255
|
+
# a block is found? right now it bails when it encounters a line to be skipped
|
244
256
|
def self.next_block(reader, parent, attributes = {}, options = {})
|
245
257
|
# Skip ahead to the block content
|
246
258
|
skipped = reader.skip_blank_lines
|
247
259
|
|
248
|
-
# bail if we've reached the end of the
|
260
|
+
# bail if we've reached the end of the parent block or document
|
249
261
|
return nil unless reader.has_more_lines?
|
250
262
|
|
263
|
+
# check for option to find list item text only
|
264
|
+
# if skipped a line, assume a list continuation was
|
265
|
+
# used and block content is acceptable
|
251
266
|
if options[:text] && skipped > 0
|
252
267
|
options.delete(:text)
|
253
268
|
end
|
254
|
-
|
255
|
-
Debug.debug {
|
256
|
-
msg = []
|
257
|
-
msg << '/' * 64
|
258
|
-
msg << 'next_block() - First two lines are:'
|
259
|
-
msg.concat reader.peek_lines(2)
|
260
|
-
msg << '/' * 64
|
261
|
-
msg * "\n"
|
262
|
-
}
|
263
269
|
|
264
|
-
parse_metadata = options
|
265
|
-
parse_sections = options
|
270
|
+
parse_metadata = options.fetch(:parse_metadata, true)
|
271
|
+
#parse_sections = options.fetch(:parse_sections, false)
|
266
272
|
|
267
273
|
document = parent.document
|
268
|
-
|
274
|
+
parent_context = parent.is_a?(Block) ? parent.context : nil
|
269
275
|
block = nil
|
276
|
+
style = nil
|
277
|
+
explicit_style = nil
|
270
278
|
|
271
279
|
while reader.has_more_lines? && block.nil?
|
280
|
+
# if parsing metadata, read until there is no more to read
|
272
281
|
if parse_metadata && parse_block_metadata_line(reader, document, attributes, options)
|
273
282
|
reader.advance
|
274
283
|
next
|
275
|
-
elsif parse_sections &&
|
276
|
-
|
277
|
-
|
284
|
+
#elsif parse_sections && parent_context.nil? && is_next_line_section?(reader, attributes)
|
285
|
+
# block, attributes = next_section(reader, parent, attributes)
|
286
|
+
# break
|
278
287
|
end
|
279
288
|
|
289
|
+
# QUESTION introduce parsing context object?
|
280
290
|
this_line = reader.get_line
|
281
|
-
|
291
|
+
delimited_block = false
|
282
292
|
block_context = nil
|
283
293
|
terminator = nil
|
294
|
+
# QUESTION put this inside call to rekey attributes?
|
295
|
+
if attributes.has_key? 1
|
296
|
+
explicit_style = attributes['style']
|
297
|
+
style = attributes['style'] = attributes[1]
|
298
|
+
end
|
299
|
+
|
284
300
|
if delimited_blk_match = is_delimited_block?(this_line, true)
|
285
|
-
|
301
|
+
delimited_block = true
|
302
|
+
block_context = delimited_blk_match.context
|
286
303
|
terminator = delimited_blk_match.terminator
|
304
|
+
if !style
|
305
|
+
style = attributes['style'] = block_context.to_s
|
306
|
+
elsif style != block_context.to_s
|
307
|
+
if delimited_blk_match.masq.include? style
|
308
|
+
block_context = style.to_sym
|
309
|
+
elsif delimited_blk_match.masq.include?('admonition') && ADMONITION_STYLES.include?(style)
|
310
|
+
block_context = :admonition
|
311
|
+
else
|
312
|
+
puts "asciidoctor: WARNING: line #{reader.lineno}: invalid style for #{block_context} block: #{style}"
|
313
|
+
style = block_context.to_s
|
314
|
+
end
|
315
|
+
end
|
287
316
|
end
|
288
317
|
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
if block.title? && !attributes.has_key?('caption') && !block.attr?('caption')
|
304
|
-
number = document.counter('figure-number')
|
305
|
-
attributes['caption'] = "#{document.attributes['figure-caption']} #{number}. "
|
306
|
-
Document::AttributeEntry.new('figure-number', number).save_to(attributes)
|
318
|
+
if !delimited_block
|
319
|
+
|
320
|
+
# this loop only executes once; used for flow control
|
321
|
+
# break once a block is found or at end of loop
|
322
|
+
# returns nil if the line must be dropped
|
323
|
+
# Implementation note - while(true) is twice as fast as loop
|
324
|
+
while true
|
325
|
+
|
326
|
+
# process lines verbatim
|
327
|
+
if !style.nil? && COMPLIANCE[:strict_verbatim_paragraphs] && VERBATIM_STYLES.include?(style)
|
328
|
+
block_context = style.to_sym
|
329
|
+
reader.unshift_line this_line
|
330
|
+
# advance to block parsing =>
|
331
|
+
break
|
307
332
|
end
|
308
|
-
else
|
309
|
-
# drop the line if target resolves to nothing
|
310
|
-
block = nil
|
311
|
-
end
|
312
|
-
reader.skip_blank_lines
|
313
333
|
|
314
|
-
|
315
|
-
|
316
|
-
|
334
|
+
# process lines normally
|
335
|
+
if !options[:text]
|
336
|
+
# NOTE we're letting break lines (ruler, page_break, etc) have attributes
|
337
|
+
if (match = this_line.match(REGEXP[:break_line]))
|
338
|
+
block = Block.new(parent, BREAK_LINES[match[0][0..2]])
|
339
|
+
break
|
340
|
+
|
341
|
+
# TODO make this a media_blk and handle image, video & audio
|
342
|
+
elsif (match = this_line.match(REGEXP[:media_blk_macro]))
|
343
|
+
blk_ctx = match[1].to_sym
|
344
|
+
block = Block.new(parent, blk_ctx)
|
345
|
+
if blk_ctx == :image
|
346
|
+
posattrs = ['alt', 'width', 'height']
|
347
|
+
elsif blk_ctx == :video
|
348
|
+
posattrs = ['poster', 'width', 'height']
|
349
|
+
else
|
350
|
+
posattrs = []
|
351
|
+
end
|
317
352
|
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
353
|
+
unless style.nil? || explicit_style
|
354
|
+
attributes['alt'] = style if blk_ctx == :image
|
355
|
+
attributes.delete('style')
|
356
|
+
style = nil
|
357
|
+
end
|
322
358
|
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
359
|
+
block.parse_attributes(match[3], posattrs,
|
360
|
+
:unescape_input => (blk_ctx == :image),
|
361
|
+
:sub_input => true,
|
362
|
+
:sub_result => false,
|
363
|
+
:into => attributes)
|
364
|
+
target = block.sub_attributes(match[2])
|
365
|
+
if target.empty?
|
366
|
+
# drop the line if target resolves to nothing
|
367
|
+
return nil
|
368
|
+
end
|
328
369
|
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
370
|
+
attributes['target'] = target
|
371
|
+
block.title = attributes.delete('title') if attributes.has_key?('title')
|
372
|
+
if blk_ctx == :image
|
373
|
+
document.register(:images, target)
|
374
|
+
attributes['alt'] ||= File.basename(target, File.extname(target))
|
375
|
+
# QUESTION should video or audio have an auto-numbered caption?
|
376
|
+
block.assign_caption attributes.delete('caption'), 'figure'
|
377
|
+
end
|
378
|
+
break
|
335
379
|
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
380
|
+
# NOTE we're letting the toc macro have attributes
|
381
|
+
elsif (match = this_line.match(REGEXP[:toc]))
|
382
|
+
block = Block.new(parent, :toc)
|
383
|
+
block.parse_attributes(match[1], [], :sub_result => false, :into => attributes)
|
384
|
+
break
|
340
385
|
|
341
|
-
|
342
|
-
block = Block.new(parent, :colist)
|
343
|
-
attributes['style'] = 'arabic'
|
344
|
-
items = []
|
345
|
-
block.buffer = items
|
346
|
-
reader.unshift_line this_line
|
347
|
-
expected_index = 1
|
348
|
-
begin
|
349
|
-
# might want to move this check to a validate method
|
350
|
-
if match[1].to_i != expected_index
|
351
|
-
puts "asciidoctor: WARNING: line #{reader.lineno + 1}: callout list item index: expected #{expected_index} got #{match[1]}"
|
386
|
+
end
|
352
387
|
end
|
353
|
-
|
354
|
-
|
355
|
-
if
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
388
|
+
|
389
|
+
# haven't found anything yet, continue
|
390
|
+
if (match = this_line.match(REGEXP[:colist]))
|
391
|
+
block = Block.new(parent, :colist)
|
392
|
+
attributes['style'] = 'arabic'
|
393
|
+
items = []
|
394
|
+
block.buffer = items
|
395
|
+
reader.unshift_line this_line
|
396
|
+
expected_index = 1
|
397
|
+
begin
|
398
|
+
# might want to move this check to a validate method
|
399
|
+
if match[1].to_i != expected_index
|
400
|
+
puts "asciidoctor: WARNING: line #{reader.lineno + 1}: callout list item index: expected #{expected_index} got #{match[1]}"
|
401
|
+
end
|
402
|
+
list_item = next_list_item(reader, block, match)
|
403
|
+
expected_index += 1
|
404
|
+
if !list_item.nil?
|
405
|
+
items << list_item
|
406
|
+
coids = document.callouts.callout_ids(items.size)
|
407
|
+
if !coids.empty?
|
408
|
+
list_item.attributes['coids'] = coids
|
409
|
+
else
|
410
|
+
puts "asciidoctor: WARNING: line #{reader.lineno}: no callouts refer to list item #{items.size}"
|
411
|
+
end
|
412
|
+
end
|
413
|
+
end while reader.has_more_lines? && match = reader.peek_line.match(REGEXP[:colist])
|
414
|
+
|
415
|
+
document.callouts.next_list
|
416
|
+
break
|
417
|
+
|
418
|
+
elsif (match = this_line.match(REGEXP[:ulist]))
|
419
|
+
reader.unshift_line this_line
|
420
|
+
block = next_outline_list(reader, :ulist, parent)
|
421
|
+
break
|
422
|
+
|
423
|
+
elsif (match = this_line.match(REGEXP[:olist]))
|
424
|
+
reader.unshift_line this_line
|
425
|
+
block = next_outline_list(reader, :olist, parent)
|
426
|
+
# QUESTION move this logic to next_outline_list?
|
427
|
+
if !(attributes.has_key? 'style') && !(block.attributes.has_key? 'style')
|
428
|
+
marker = block.buffer.first.marker
|
429
|
+
if marker.start_with? '.'
|
430
|
+
# first one makes more sense, but second on is AsciiDoc-compliant
|
431
|
+
#attributes['style'] = (ORDERED_LIST_STYLES[block.level - 1] || ORDERED_LIST_STYLES.first).to_s
|
432
|
+
attributes['style'] = (ORDERED_LIST_STYLES[marker.length - 1] || ORDERED_LIST_STYLES.first).to_s
|
433
|
+
else
|
434
|
+
style = ORDERED_LIST_STYLES.detect{|s| marker.match(ORDERED_LIST_MARKER_PATTERNS[s]) }
|
435
|
+
attributes['style'] = (style || ORDERED_LIST_STYLES.first).to_s
|
436
|
+
end
|
437
|
+
end
|
438
|
+
break
|
439
|
+
|
440
|
+
elsif (match = this_line.match(REGEXP[:dlist]))
|
441
|
+
reader.unshift_line this_line
|
442
|
+
block = next_labeled_list(reader, match, parent)
|
443
|
+
break
|
444
|
+
|
445
|
+
elsif (style == 'float' || style == 'discrete') && is_section_title?(this_line, reader.peek_line)
|
446
|
+
reader.unshift_line this_line
|
447
|
+
float_id, float_title, float_level, _ = parse_section_title(reader, document)
|
448
|
+
float_id ||= attributes['id'] if attributes.has_key?('id')
|
449
|
+
block = Block.new(parent, :floating_title)
|
450
|
+
if float_id.nil? || float_id.empty?
|
451
|
+
# FIXME remove hack of creating throwaway Section to get at the generate_id method
|
452
|
+
tmp_sect = Section.new(parent)
|
453
|
+
tmp_sect.title = float_title
|
454
|
+
block.id = tmp_sect.generate_id
|
360
455
|
else
|
361
|
-
|
456
|
+
block.id = float_id
|
362
457
|
end
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
elsif block_context.nil? && (match = this_line.match(REGEXP[:ulist]))
|
369
|
-
AttributeList.rekey(attributes, ['style'])
|
370
|
-
reader.unshift_line this_line
|
371
|
-
block = next_outline_list(reader, :ulist, parent)
|
372
|
-
|
373
|
-
elsif block_context.nil? && (match = this_line.match(REGEXP[:olist]))
|
374
|
-
AttributeList.rekey(attributes, ['style'])
|
375
|
-
reader.unshift_line this_line
|
376
|
-
block = next_outline_list(reader, :olist, parent)
|
377
|
-
# QUESTION move this logic to next_outline_list?
|
378
|
-
if !(attributes.has_key? 'style') && !(block.attributes.has_key? 'style')
|
379
|
-
marker = block.buffer.first.marker
|
380
|
-
if marker.start_with? '.'
|
381
|
-
# first one makes more sense, but second on is AsciiDoc-compliant
|
382
|
-
#attributes['style'] = (ORDERED_LIST_STYLES[block.level - 1] || ORDERED_LIST_STYLES.first).to_s
|
383
|
-
attributes['style'] = (ORDERED_LIST_STYLES[marker.length - 1] || ORDERED_LIST_STYLES.first).to_s
|
384
|
-
else
|
385
|
-
style = ORDERED_LIST_STYLES.detect{|s| marker.match(ORDERED_LIST_MARKER_PATTERNS[s]) }
|
386
|
-
attributes['style'] = (style || ORDERED_LIST_STYLES.first).to_s
|
387
|
-
end
|
388
|
-
end
|
458
|
+
document.register(:ids, [block.id, float_title]) if block.id
|
459
|
+
block.level = float_level
|
460
|
+
block.title = float_title
|
461
|
+
break
|
389
462
|
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
elsif block_context == :example
|
409
|
-
# example is surrounded by lines with 4 or more '=' chars
|
410
|
-
AttributeList.rekey(attributes, ['style'])
|
411
|
-
if admonition_style = ADMONITION_STYLES.detect {|s| attributes['style'] == s}
|
412
|
-
block = Block.new(parent, :admonition)
|
413
|
-
attributes['name'] = admonition_name = admonition_style.downcase
|
414
|
-
attributes['caption'] ||= document.attributes["#{admonition_name}-caption"]
|
415
|
-
else
|
416
|
-
block = Block.new(parent, block_context)
|
417
|
-
block.title = attributes['title']
|
418
|
-
if block.title? && !attributes.has_key?('caption') && !block.attr?('caption')
|
419
|
-
number = document.counter('example-number')
|
420
|
-
attributes['caption'] = "#{document.attributes['example-caption']} #{number}. "
|
421
|
-
Document::AttributeEntry.new('example-number', number).save_to(attributes)
|
463
|
+
# FIXME create another set for "passthrough" styles
|
464
|
+
# though partintro should likely be a dedicated block
|
465
|
+
elsif !style.nil? && style != 'normal' && style != 'partintro'
|
466
|
+
if PARAGRAPH_STYLES.include?(style)
|
467
|
+
block_context = style.to_sym
|
468
|
+
reader.unshift_line this_line
|
469
|
+
# advance to block parsing =>
|
470
|
+
break
|
471
|
+
elsif ADMONITION_STYLES.include?(style)
|
472
|
+
block_context = :admonition
|
473
|
+
reader.unshift_line this_line
|
474
|
+
# advance to block parsing =>
|
475
|
+
break
|
476
|
+
else
|
477
|
+
puts "asciidoctor: WARNING: line #{reader.lineno}: invalid style for paragraph: #{style}"
|
478
|
+
style = nil
|
479
|
+
# continue to process paragraph
|
480
|
+
end
|
422
481
|
end
|
423
|
-
end
|
424
|
-
buffer = Reader.new reader.grab_lines_until(:terminator => terminator)
|
425
482
|
|
426
|
-
|
427
|
-
new_block = next_block(buffer, block)
|
428
|
-
block.blocks << new_block unless new_block.nil?
|
429
|
-
end
|
483
|
+
break_at_list = (skipped == 0 && parent_context.to_s.end_with?('list'))
|
430
484
|
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
block.title? && !attributes.has_key?('caption') && !block.attr?('caption')
|
447
|
-
number = document.counter('listing-number')
|
448
|
-
attributes['caption'] = "#{document.attributes['listing-caption']} #{number}. "
|
449
|
-
Document::AttributeEntry.new('listing-number', number).save_to(attributes)
|
450
|
-
end
|
485
|
+
# a literal paragraph is contiguous lines starting at least one space
|
486
|
+
if style != 'normal' && this_line.match(REGEXP[:lit_par])
|
487
|
+
# So we need to actually include this one in the grab_lines group
|
488
|
+
reader.unshift_line this_line
|
489
|
+
buffer = reader.grab_lines_until(
|
490
|
+
:break_on_blank_lines => true,
|
491
|
+
:break_on_list_continuation => true,
|
492
|
+
:preserve_last_line => true) {|line|
|
493
|
+
# a preceding blank line (skipped > 0) indicates we are in a list continuation
|
494
|
+
# and therefore we should not break at a list item
|
495
|
+
# (this won't stop breaking on item of same level since we've already parsed them out)
|
496
|
+
# QUESTION can we turn this block into a lambda or function call?
|
497
|
+
(break_at_list && line.match(REGEXP[:any_list])) ||
|
498
|
+
(COMPLIANCE[:block_terminates_paragraph] && (is_delimited_block?(line) || line.match(REGEXP[:attr_line])))
|
499
|
+
}
|
451
500
|
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
501
|
+
# trim off the indentation equivalent to the size of the least indented line
|
502
|
+
if !buffer.empty?
|
503
|
+
offset = buffer.map {|line| line.match(REGEXP[:leading_blanks])[1].length }.min
|
504
|
+
if offset > 0
|
505
|
+
buffer = buffer.map {|l| l.sub(/^\s{1,#{offset}}/, '') }
|
506
|
+
end
|
507
|
+
end
|
457
508
|
|
458
|
-
|
459
|
-
|
509
|
+
block = Block.new(parent, :literal, buffer)
|
510
|
+
# a literal gets special meaning inside of a definition list
|
511
|
+
if LIST_CONTEXTS.include?(parent_context)
|
512
|
+
attributes['options'] ||= []
|
513
|
+
# TODO this feels hacky, better way to distinguish from explicit literal block?
|
514
|
+
attributes['options'] << 'listparagraph'
|
515
|
+
end
|
460
516
|
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
517
|
+
# a paragraph is contiguous nonblank/noncontinuation lines
|
518
|
+
else
|
519
|
+
reader.unshift_line this_line
|
520
|
+
buffer = reader.grab_lines_until(
|
521
|
+
:break_on_blank_lines => true,
|
522
|
+
:break_on_list_continuation => true,
|
523
|
+
:preserve_last_line => true,
|
524
|
+
:skip_line_comments => true) {|line|
|
525
|
+
# a preceding blank line (skipped > 0) indicates we are in a list continuation
|
526
|
+
# and therefore we should not break at a list item
|
527
|
+
# (this won't stop breaking on item of same level since we've already parsed them out)
|
528
|
+
# QUESTION can we turn this block into a lambda or function call?
|
529
|
+
(break_at_list && line.match(REGEXP[:any_list])) ||
|
530
|
+
(COMPLIANCE[:block_terminates_paragraph] && (is_delimited_block?(line) || line.match(REGEXP[:attr_line])))
|
531
|
+
}
|
471
532
|
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
elsif this_line.match(REGEXP[:lit_par])
|
484
|
-
# literal paragraph is contiguous lines starting with
|
485
|
-
# one or more space or tab characters
|
486
|
-
|
487
|
-
# So we need to actually include this one in the grab_lines group
|
488
|
-
reader.unshift_line this_line
|
489
|
-
buffer = reader.grab_lines_until(:preserve_last_line => true, :break_on_blank_lines => true) {|line|
|
490
|
-
# labeled list terms can be indented, but a preceding blank indicates
|
491
|
-
# we are in a list continuation and therefore literals should be strictly literal
|
492
|
-
(context == :dlist && skipped == 0 && line.match(REGEXP[:dlist])) ||
|
493
|
-
is_delimited_block?(line)
|
494
|
-
}
|
533
|
+
# NOTE we need this logic because we've asked the reader to skip
|
534
|
+
# line comments, which may leave us w/ an empty buffer if those
|
535
|
+
# were the only lines found
|
536
|
+
if buffer.empty?
|
537
|
+
# call get_line since the reader preserved the last line
|
538
|
+
reader.get_line
|
539
|
+
return nil
|
540
|
+
end
|
541
|
+
|
542
|
+
catalog_inline_anchors(buffer.join, document)
|
495
543
|
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
544
|
+
if !options[:text] && (admonition_match = buffer.first.match(REGEXP[:admonition_inline]))
|
545
|
+
buffer[0] = admonition_match.post_match.lstrip
|
546
|
+
block = Block.new(parent, :admonition, buffer)
|
547
|
+
attributes['style'] = admonition_match[1]
|
548
|
+
attributes['name'] = admonition_name = admonition_match[1].downcase
|
549
|
+
attributes['caption'] ||= document.attributes["#{admonition_name}-caption"]
|
550
|
+
else
|
551
|
+
# QUESTION is this necessary?
|
552
|
+
#if style == 'normal' && [' ', "\t"].include?(buffer.first[0..0])
|
553
|
+
# # QUESTION should we only trim leading blanks?
|
554
|
+
# buffer.map! &:lstrip
|
555
|
+
#end
|
556
|
+
|
557
|
+
block = Block.new(parent, :paragraph, buffer)
|
558
|
+
end
|
501
559
|
end
|
502
|
-
buffer.last.chomp!
|
503
|
-
end
|
504
560
|
|
505
|
-
|
506
|
-
|
507
|
-
if LIST_CONTEXTS.include?(context)
|
508
|
-
attributes['options'] ||= []
|
509
|
-
# TODO this feels hacky, better way to distinguish from explicit literal block?
|
510
|
-
attributes['options'] << 'listparagraph'
|
561
|
+
# forbid loop from executing more than once
|
562
|
+
break
|
511
563
|
end
|
564
|
+
end
|
512
565
|
|
513
|
-
|
566
|
+
# either delimited block or styled paragraph
|
567
|
+
if block.nil? && !block_context.nil?
|
568
|
+
case block_context
|
569
|
+
when :admonition
|
570
|
+
attributes['name'] = admonition_name = style.downcase
|
571
|
+
attributes['caption'] ||= document.attributes["#{admonition_name}-caption"]
|
572
|
+
block = build_block(block_context, :complex, terminator, parent, reader, attributes)
|
573
|
+
|
574
|
+
when :comment
|
575
|
+
reader.grab_lines_until(:break_on_blank_lines => true, :chomp_last_line => false)
|
576
|
+
return nil
|
577
|
+
|
578
|
+
when :example
|
579
|
+
block = build_block(block_context, :complex, terminator, parent, reader, attributes, true)
|
580
|
+
|
581
|
+
when :listing, :fenced_code, :source
|
582
|
+
if block_context == :fenced_code
|
583
|
+
style = attributes['style'] = 'source'
|
584
|
+
lang = this_line[3..-1].strip
|
585
|
+
attributes['language'] = lang unless lang.empty?
|
586
|
+
terminator = terminator[0..2] if terminator.length > 3
|
587
|
+
elsif block_context == :source
|
588
|
+
AttributeList.rekey(attributes, [nil, 'language', 'linenums'])
|
589
|
+
end
|
590
|
+
block = build_block(:listing, :verbatim, terminator, parent, reader, attributes, true)
|
514
591
|
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
buffer = reader.grab_lines_until(:break_on_blank_lines => true)
|
521
|
-
buffer.last.chomp! unless buffer.empty?
|
522
|
-
block = Block.new(parent, :listing, buffer)
|
523
|
-
|
524
|
-
elsif attributes[1] == 'literal'
|
525
|
-
reader.unshift_line this_line
|
526
|
-
buffer = reader.grab_lines_until(:break_on_blank_lines => true)
|
527
|
-
buffer.last.chomp! unless buffer.empty?
|
528
|
-
block = Block.new(parent, :literal, buffer)
|
529
|
-
|
530
|
-
elsif admonition_style = ADMONITION_STYLES.detect{|s| attributes[1] == s}
|
531
|
-
# an admonition preceded by [<TYPE>] and lasts until a blank line
|
532
|
-
reader.unshift_line this_line
|
533
|
-
buffer = reader.grab_lines_until(:break_on_blank_lines => true)
|
534
|
-
buffer.last.chomp! unless buffer.empty?
|
535
|
-
block = Block.new(parent, :admonition, buffer)
|
536
|
-
attributes['style'] = admonition_style
|
537
|
-
attributes['name'] = admonition_name = admonition_style.downcase
|
538
|
-
attributes['caption'] ||= document.attributes["#{admonition_name}-caption"]
|
539
|
-
|
540
|
-
elsif quote_context = [:quote, :verse].detect{|s| attributes[1] == s.to_s}
|
541
|
-
# single-paragraph verse or quote is preceded by [verse] or [quote], respectively, and lasts until a blank line
|
542
|
-
AttributeList.rekey(attributes, ['style', 'attribution', 'citetitle'])
|
543
|
-
reader.unshift_line this_line
|
544
|
-
buffer = reader.grab_lines_until(:break_on_blank_lines => true)
|
545
|
-
buffer.last.chomp! unless buffer.empty?
|
546
|
-
block = Block.new(parent, quote_context, buffer)
|
547
|
-
|
548
|
-
# a floating (i.e., discrete) title
|
549
|
-
elsif ['float', 'discrete'].include?(attributes[1]) && is_section_title?(this_line, reader.peek_line)
|
550
|
-
attributes['style'] = attributes[1]
|
551
|
-
reader.unshift_line this_line
|
552
|
-
float_id, float_title, float_level, _ = parse_section_title reader
|
553
|
-
block = Block.new(parent, :floating_title)
|
554
|
-
if float_id.nil? || float_id.empty?
|
555
|
-
# FIXME remove hack of creating throwaway Section to get at the generate_id method
|
556
|
-
tmp_sect = Section.new(parent)
|
557
|
-
tmp_sect.title = float_title
|
558
|
-
block.id = tmp_sect.generate_id
|
559
|
-
else
|
560
|
-
block.id = float_id
|
561
|
-
@document.register(:ids, [float_id, float_title])
|
562
|
-
end
|
563
|
-
block.level = float_level
|
564
|
-
block.title = float_title
|
592
|
+
when :literal
|
593
|
+
block = build_block(block_context, :verbatim, terminator, parent, reader, attributes)
|
594
|
+
|
595
|
+
when :pass
|
596
|
+
block = build_block(block_context, :simple, terminator, parent, reader, attributes)
|
565
597
|
|
566
|
-
|
567
|
-
|
568
|
-
reader.unshift_line this_line
|
569
|
-
buffer = reader.grab_lines_until(:break_on_blank_lines => true, :preserve_last_line => true, :skip_line_comments => true) {|line|
|
570
|
-
is_delimited_block?(line) || line.match(REGEXP[:attr_line]) ||
|
571
|
-
# next list item can be directly adjacent to paragraph of previous list item
|
572
|
-
context == :dlist && line.match(REGEXP[:dlist])
|
573
|
-
# not sure if there are any cases when we need this check for other list types
|
574
|
-
#LIST_CONTEXTS.include?(context) && line.match(REGEXP[context])
|
575
|
-
}
|
598
|
+
when :open, :sidebar
|
599
|
+
block = build_block(block_context, :complex, terminator, parent, reader, attributes)
|
576
600
|
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
reader.get_line
|
581
|
-
break
|
582
|
-
end
|
601
|
+
when :table
|
602
|
+
block_reader = Reader.new reader.grab_lines_until(:terminator => terminator, :skip_line_comments => true)
|
603
|
+
block = next_table(block_reader, parent, attributes)
|
583
604
|
|
584
|
-
|
605
|
+
when :quote, :verse
|
606
|
+
AttributeList.rekey(attributes, [nil, 'attribution', 'citetitle'])
|
607
|
+
block = build_block(block_context, (block_context == :verse ? :verbatim : :complex), terminator, parent, reader, attributes)
|
585
608
|
|
586
|
-
if !options[:text] && (admonition = buffer.first.match(Regexp.new('^(' + ADMONITION_STYLES.join('|') + '):\s+')))
|
587
|
-
buffer[0] = admonition.post_match
|
588
|
-
block = Block.new(parent, :admonition, buffer)
|
589
|
-
attributes['style'] = admonition[1]
|
590
|
-
attributes['name'] = admonition_name = admonition[1].downcase
|
591
|
-
attributes['caption'] ||= document.attributes["#{admonition_name}-caption"]
|
592
609
|
else
|
593
|
-
|
594
|
-
block
|
610
|
+
# this should only happen if there is a misconfiguration
|
611
|
+
raise "Unsupported block type #{block_context} at line #{reader.lineno}"
|
595
612
|
end
|
596
613
|
end
|
597
614
|
end
|
@@ -599,8 +616,10 @@ class Lexer
|
|
599
616
|
# when looking for nested content, one or more line comments, comment
|
600
617
|
# blocks or trailing attribute lists could leave us without a block,
|
601
618
|
# so handle accordingly
|
619
|
+
# REVIEW we may no longer need this check
|
602
620
|
if !block.nil?
|
603
|
-
|
621
|
+
# REVIEW seems like there is a better way to organize this wrap-up
|
622
|
+
block.id ||= attributes['id'] if attributes.has_key?('id')
|
604
623
|
block.title = attributes['title'] unless block.title?
|
605
624
|
block.caption ||= attributes['caption'] unless block.is_a?(Section)
|
606
625
|
# AsciiDoc always use [id] as the reftext in HTML output,
|
@@ -643,9 +662,21 @@ class Lexer
|
|
643
662
|
if DELIMITED_BLOCKS.has_key? tip
|
644
663
|
# if tip is the full line
|
645
664
|
if tl == line_len - 1
|
646
|
-
return_match_data ? BlockMatchData.new(DELIMITED_BLOCKS[tip], tip, tip) : true
|
665
|
+
#return_match_data ? BlockMatchData.new(DELIMITED_BLOCKS[tip], tip, tip) : true
|
666
|
+
if return_match_data
|
667
|
+
context, masq = *DELIMITED_BLOCKS[tip]
|
668
|
+
BlockMatchData.new(context, masq, tip, tip)
|
669
|
+
else
|
670
|
+
true
|
671
|
+
end
|
647
672
|
elsif match = line.match(REGEXP[:any_blk])
|
648
|
-
return_match_data ? BlockMatchData.new(DELIMITED_BLOCKS[tip], tip, match[0]) : true
|
673
|
+
#return_match_data ? BlockMatchData.new(DELIMITED_BLOCKS[tip], tip, match[0]) : true
|
674
|
+
if return_match_data
|
675
|
+
context, masq = *DELIMITED_BLOCKS[tip]
|
676
|
+
BlockMatchData.new(context, masq, tip, match[0])
|
677
|
+
else
|
678
|
+
true
|
679
|
+
end
|
649
680
|
else
|
650
681
|
nil
|
651
682
|
end
|
@@ -657,6 +688,48 @@ class Lexer
|
|
657
688
|
end
|
658
689
|
end
|
659
690
|
|
691
|
+
# whether a block supports complex content should be a config setting
|
692
|
+
# NOTE could invoke filter in here, before and after parsing
|
693
|
+
def self.build_block(block_context, content_type, terminator, parent, reader, attributes, supports_caption = false)
|
694
|
+
if terminator.nil?
|
695
|
+
if content_type == :verbatim
|
696
|
+
buffer = reader.grab_lines_until(:break_on_blank_lines => true, :break_on_list_continuation => true)
|
697
|
+
else
|
698
|
+
buffer = reader.grab_lines_until(
|
699
|
+
:break_on_blank_lines => true,
|
700
|
+
:break_on_list_continuation => true,
|
701
|
+
:preserve_last_line => true,
|
702
|
+
:skip_line_comments => true) {|line|
|
703
|
+
COMPLIANCE[:block_terminates_paragraph] && (is_delimited_block?(line) || line.match(REGEXP[:attr_line]))
|
704
|
+
}
|
705
|
+
# QUESTION check for empty buffer?
|
706
|
+
end
|
707
|
+
elsif content_type != :complex
|
708
|
+
buffer = reader.grab_lines_until(:terminator => terminator, :chomp_last_line => true)
|
709
|
+
else
|
710
|
+
buffer = nil
|
711
|
+
block_reader = Reader.new reader.grab_lines_until(:terminator => terminator)
|
712
|
+
end
|
713
|
+
|
714
|
+
block = Block.new(parent, block_context, buffer)
|
715
|
+
# should supports_caption be necessary?
|
716
|
+
if supports_caption
|
717
|
+
block.title = attributes.delete('title') if attributes.has_key?('title')
|
718
|
+
block.assign_caption attributes.delete('caption')
|
719
|
+
end
|
720
|
+
|
721
|
+
if buffer.nil?
|
722
|
+
# we can look for blocks until there are no more lines (and not worry
|
723
|
+
# about sections) since the reader is confined within the boundaries of a
|
724
|
+
# delimited block
|
725
|
+
while block_reader.has_more_lines?
|
726
|
+
parsed_block = next_block(block_reader, block)
|
727
|
+
block.blocks << parsed_block unless parsed_block.nil?
|
728
|
+
end
|
729
|
+
end
|
730
|
+
block
|
731
|
+
end
|
732
|
+
|
660
733
|
# Internal: Parse and construct an outline list Block from the current position of the Reader
|
661
734
|
#
|
662
735
|
# reader - The Reader from which to retrieve the outline list
|
@@ -744,9 +817,9 @@ class Lexer
|
|
744
817
|
m = $~
|
745
818
|
next if m[0].start_with? '\\'
|
746
819
|
id, reftext = m[1].split(',')
|
747
|
-
id.sub!(
|
820
|
+
id.sub!(REGEXP[:dbl_quoted], '\2')
|
748
821
|
if !reftext.nil?
|
749
|
-
reftext.sub!(
|
822
|
+
reftext.sub!(REGEXP[:m_dbl_quoted], '\2')
|
750
823
|
end
|
751
824
|
document.register(:ids, [id, reftext])
|
752
825
|
}
|
@@ -834,6 +907,9 @@ class Lexer
|
|
834
907
|
# only relevant for :dlist
|
835
908
|
options = {:text => !has_text}
|
836
909
|
|
910
|
+
# we can look for blocks until there are no more lines (and not worry
|
911
|
+
# about sections) since the reader is confined within the boundaries of a
|
912
|
+
# list
|
837
913
|
while list_item_reader.has_more_lines?
|
838
914
|
new_block = next_block(list_item_reader, list_block, {}, options)
|
839
915
|
list_item.blocks << new_block unless new_block.nil?
|
@@ -898,7 +974,7 @@ class Lexer
|
|
898
974
|
if continuation == :inactive
|
899
975
|
continuation = :active
|
900
976
|
has_text = true
|
901
|
-
buffer[
|
977
|
+
buffer[-1] = "\n" unless within_nested_list
|
902
978
|
end
|
903
979
|
|
904
980
|
# dealing with adjacent list continuations (which is really a syntax error)
|
@@ -937,12 +1013,12 @@ class Lexer
|
|
937
1013
|
if this_line.match(REGEXP[:lit_par])
|
938
1014
|
reader.unshift_line this_line
|
939
1015
|
buffer.concat reader.grab_lines_until(
|
940
|
-
|
941
|
-
|
942
|
-
|
943
|
-
|
944
|
-
|
945
|
-
|
1016
|
+
:preserve_last_line => true,
|
1017
|
+
:break_on_blank_lines => true,
|
1018
|
+
:break_on_list_continuation => true) {|line|
|
1019
|
+
# we may be in an indented list disguised as a literal paragraph
|
1020
|
+
# so we need to make sure we don't slurp up a legitimate sibling
|
1021
|
+
list_type == :dlist && is_sibling_list_item?(line, list_type, sibling_trait)
|
946
1022
|
}
|
947
1023
|
continuation = :inactive
|
948
1024
|
# let block metadata play out until we find the block
|
@@ -980,13 +1056,13 @@ class Lexer
|
|
980
1056
|
if this_line.match(REGEXP[:lit_par])
|
981
1057
|
reader.unshift_line this_line
|
982
1058
|
buffer.concat reader.grab_lines_until(
|
983
|
-
|
984
|
-
|
985
|
-
|
986
|
-
|
987
|
-
|
988
|
-
|
989
|
-
|
1059
|
+
:preserve_last_line => true,
|
1060
|
+
:break_on_blank_lines => true,
|
1061
|
+
:break_on_list_continuation => true) {|line|
|
1062
|
+
# we may be in an indented list disguised as a literal paragraph
|
1063
|
+
# so we need to make sure we don't slurp up a legitimate sibling
|
1064
|
+
list_type == :dlist && is_sibling_list_item?(line, list_type, sibling_trait)
|
1065
|
+
}
|
990
1066
|
# TODO any way to combine this with the check after skipping blank lines?
|
991
1067
|
elsif is_sibling_list_item?(this_line, list_type, sibling_trait)
|
992
1068
|
break
|
@@ -1053,7 +1129,7 @@ class Lexer
|
|
1053
1129
|
# attributes - a Hash of attributes to assign to this section (default: {})
|
1054
1130
|
def self.initialize_section(reader, parent, attributes = {})
|
1055
1131
|
section = Section.new parent
|
1056
|
-
section.id, section.title, section.level, _ = parse_section_title(reader)
|
1132
|
+
section.id, section.title, section.level, _ = parse_section_title(reader, section.document)
|
1057
1133
|
if section.id.nil? && attributes.has_key?('id')
|
1058
1134
|
section.id = attributes['id']
|
1059
1135
|
else
|
@@ -1062,10 +1138,15 @@ class Lexer
|
|
1062
1138
|
section.id ||= section.generate_id
|
1063
1139
|
end
|
1064
1140
|
|
1141
|
+
if section.id
|
1142
|
+
section.document.register(:ids, [section.id, section.title])
|
1143
|
+
end
|
1144
|
+
|
1065
1145
|
if attributes[1]
|
1066
1146
|
section.sectname = attributes[1]
|
1067
1147
|
section.special = true
|
1068
1148
|
document = parent.document
|
1149
|
+
# FIXME refactor to use assign_caption (also check requirements)
|
1069
1150
|
if section.sectname == 'appendix' &&
|
1070
1151
|
!attributes.has_key?('caption') &&
|
1071
1152
|
!document.attributes.has_key?('caption')
|
@@ -1087,14 +1168,7 @@ class Lexer
|
|
1087
1168
|
#
|
1088
1169
|
# line - the String line from under the section title.
|
1089
1170
|
def self.section_level(line)
|
1090
|
-
|
1091
|
-
case char
|
1092
|
-
when ['=']; 0
|
1093
|
-
when ['-']; 1
|
1094
|
-
when ['~']; 2
|
1095
|
-
when ['^']; 3
|
1096
|
-
when ['+']; 4
|
1097
|
-
end
|
1171
|
+
SECTION_LEVELS[line[0..0]]
|
1098
1172
|
end
|
1099
1173
|
|
1100
1174
|
#--
|
@@ -1168,13 +1242,14 @@ class Lexer
|
|
1168
1242
|
# the Reader will be positioned at the line after the section title.
|
1169
1243
|
#
|
1170
1244
|
# reader - the source reader, positioned at a section title
|
1245
|
+
# document- the current document
|
1171
1246
|
#
|
1172
1247
|
# Examples
|
1173
1248
|
#
|
1174
1249
|
# reader.lines
|
1175
1250
|
# # => ["Foo\n", "~~~\n"]
|
1176
1251
|
#
|
1177
|
-
# title, level, id, single = parse_section_title(reader)
|
1252
|
+
# title, level, id, single = parse_section_title(reader, document)
|
1178
1253
|
#
|
1179
1254
|
# title
|
1180
1255
|
# # => "Foo"
|
@@ -1188,7 +1263,7 @@ class Lexer
|
|
1188
1263
|
# line1
|
1189
1264
|
# # => "==== Foo\n"
|
1190
1265
|
#
|
1191
|
-
# title, level, id, single = parse_section_title(reader)
|
1266
|
+
# title, level, id, single = parse_section_title(reader, document)
|
1192
1267
|
#
|
1193
1268
|
# title
|
1194
1269
|
# # => "Foo"
|
@@ -1204,7 +1279,7 @@ class Lexer
|
|
1204
1279
|
#
|
1205
1280
|
#--
|
1206
1281
|
# NOTE for efficiency, we don't reuse methods that check for a section title
|
1207
|
-
def self.parse_section_title(reader)
|
1282
|
+
def self.parse_section_title(reader, document)
|
1208
1283
|
line1 = reader.get_line
|
1209
1284
|
sect_id = nil
|
1210
1285
|
sect_title = nil
|
@@ -1232,7 +1307,10 @@ class Lexer
|
|
1232
1307
|
reader.get_line
|
1233
1308
|
end
|
1234
1309
|
end
|
1235
|
-
|
1310
|
+
if sect_level >= 0
|
1311
|
+
sect_level += document.attr('leveloffset', 0).to_i
|
1312
|
+
end
|
1313
|
+
[sect_id, sect_title, sect_level, single_line]
|
1236
1314
|
end
|
1237
1315
|
|
1238
1316
|
# Public: Consume and parse the two header lines (line 1 = author info, line 2 = revision info).
|
@@ -1255,55 +1333,102 @@ class Lexer
|
|
1255
1333
|
metadata = {}
|
1256
1334
|
|
1257
1335
|
if reader.has_more_lines? && !reader.peek_line.chomp.empty?
|
1336
|
+
author_metadata = {}
|
1337
|
+
keys = ['author', 'authorinitials', 'firstname', 'middlename', 'lastname', 'email']
|
1258
1338
|
author_line = reader.get_line
|
1259
|
-
|
1260
|
-
|
1261
|
-
|
1262
|
-
|
1263
|
-
if
|
1264
|
-
|
1265
|
-
|
1266
|
-
|
1267
|
-
|
1268
|
-
|
1269
|
-
|
1270
|
-
|
1271
|
-
|
1339
|
+
author_line.split(REGEXP[:semicolon_delim]).each_with_index do |author_entry, idx|
|
1340
|
+
author_entry.strip!
|
1341
|
+
next if author_entry.empty?
|
1342
|
+
map = {}
|
1343
|
+
if idx.zero?
|
1344
|
+
keys.each do |key|
|
1345
|
+
map[key.to_sym] = key
|
1346
|
+
end
|
1347
|
+
else
|
1348
|
+
keys.each do |key|
|
1349
|
+
map[key.to_sym] = "#{key}_#{idx + 1}"
|
1350
|
+
end
|
1351
|
+
end
|
1352
|
+
|
1353
|
+
if match = author_entry.match(REGEXP[:author_info])
|
1354
|
+
author_metadata[map[:firstname]] = fname = match[1].tr('_', ' ')
|
1355
|
+
author_metadata[map[:author]] = fname
|
1356
|
+
author_metadata[map[:authorinitials]] = fname[0, 1]
|
1357
|
+
if !match[2].nil? && !match[3].nil?
|
1358
|
+
author_metadata[map[:middlename]] = mname = match[2].tr('_', ' ')
|
1359
|
+
author_metadata[map[:lastname]] = lname = match[3].tr('_', ' ')
|
1360
|
+
author_metadata[map[:author]] = [fname, mname, lname].join ' '
|
1361
|
+
author_metadata[map[:authorinitials]] = [fname[0, 1], mname[0, 1], lname[0, 1]].join
|
1362
|
+
elsif !match[2].nil?
|
1363
|
+
author_metadata[map[:lastname]] = lname = match[2].tr('_', ' ')
|
1364
|
+
author_metadata[map[:author]] = [fname, lname].join ' '
|
1365
|
+
author_metadata[map[:authorinitials]] = [fname[0, 1], lname[0, 1]].join
|
1366
|
+
end
|
1367
|
+
author_metadata[map[:email]] = match[4] unless match[4].nil?
|
1368
|
+
else
|
1369
|
+
author_metadata[map[:author]] = author_metadata[map[:firstname]] = author_entry.strip.squeeze(' ')
|
1370
|
+
author_metadata[map[:authorinitials]] = author_metadata[map[:firstname]][0, 1]
|
1371
|
+
end
|
1372
|
+
|
1373
|
+
author_metadata['authorcount'] = idx + 1
|
1374
|
+
# only assign the _1 attributes if there are multiple authors
|
1375
|
+
if idx == 1
|
1376
|
+
keys.each do |key|
|
1377
|
+
author_metadata["#{key}_1"] = author_metadata[key] if author_metadata.has_key? key
|
1378
|
+
end
|
1379
|
+
end
|
1380
|
+
if idx.zero?
|
1381
|
+
author_metadata['authors'] = author_metadata[map[:author]]
|
1382
|
+
else
|
1383
|
+
author_metadata['authors'] = "#{author_metadata['authors']}, #{author_metadata[map[:author]]}"
|
1272
1384
|
end
|
1273
|
-
metadata['email'] = match[4] unless match[4].nil?
|
1274
|
-
else
|
1275
|
-
metadata['author'] = metadata['firstname'] = author_line.strip.squeeze(' ')
|
1276
|
-
metadata['authorinitials'] = metadata['firstname'][0, 1]
|
1277
1385
|
end
|
1278
1386
|
|
1279
|
-
#
|
1387
|
+
# apply header subs and assign to document
|
1388
|
+
if !document.nil?
|
1389
|
+
author_metadata.map do |key, val|
|
1390
|
+
val = val.is_a?(String) ? document.apply_header_subs(val) : val
|
1391
|
+
document.attributes[key] = val if !document.attributes.has_key?(key)
|
1392
|
+
val
|
1393
|
+
end
|
1394
|
+
end
|
1395
|
+
|
1396
|
+
metadata = author_metadata.dup
|
1397
|
+
|
1398
|
+
# NOTE this will discard any comment lines, but not skip blank lines
|
1280
1399
|
process_attribute_entries(reader, document)
|
1281
1400
|
|
1401
|
+
rev_metadata = {}
|
1402
|
+
|
1282
1403
|
if reader.has_more_lines? && !reader.peek_line.chomp.empty?
|
1283
1404
|
rev_line = reader.get_line
|
1284
1405
|
if match = rev_line.match(REGEXP[:revision_info])
|
1285
|
-
|
1286
|
-
|
1287
|
-
|
1406
|
+
rev_metadata['revdate'] = match[2].strip
|
1407
|
+
rev_metadata['revnumber'] = match[1].rstrip unless match[1].nil?
|
1408
|
+
rev_metadata['revremark'] = match[3].rstrip unless match[3].nil?
|
1288
1409
|
else
|
1289
1410
|
# throw it back
|
1290
1411
|
reader.unshift_line rev_line
|
1291
1412
|
end
|
1292
1413
|
end
|
1293
1414
|
|
1294
|
-
# NOTE this will discard away any comment lines, but not skip blank lines
|
1295
|
-
process_attribute_entries(reader, document)
|
1296
|
-
|
1297
|
-
reader.skip_blank_lines
|
1298
|
-
|
1299
1415
|
# apply header subs and assign to document
|
1300
1416
|
if !document.nil?
|
1301
|
-
|
1417
|
+
rev_metadata.map do |key, val|
|
1302
1418
|
val = document.apply_header_subs(val)
|
1303
1419
|
document.attributes[key] = val if !document.attributes.has_key?(key)
|
1304
1420
|
val
|
1305
1421
|
end
|
1306
1422
|
end
|
1423
|
+
|
1424
|
+
rev_metadata.each {|k, v|
|
1425
|
+
metadata[k] = v
|
1426
|
+
}
|
1427
|
+
|
1428
|
+
# NOTE this will discard any comment lines, but not skip blank lines
|
1429
|
+
process_attribute_entries(reader, document)
|
1430
|
+
|
1431
|
+
reader.skip_blank_lines
|
1307
1432
|
end
|
1308
1433
|
|
1309
1434
|
metadata
|
@@ -1372,7 +1497,7 @@ class Lexer
|
|
1372
1497
|
parent.document.register(:ids, [id, reftext])
|
1373
1498
|
end
|
1374
1499
|
elsif match = next_line.match(REGEXP[:blk_attr_list])
|
1375
|
-
|
1500
|
+
parent.document.parse_attributes(match[1], [], :sub_input => true, :into => attributes)
|
1376
1501
|
# NOTE title doesn't apply to section, but we need to stash it for the first block
|
1377
1502
|
# TODO should issue an error if this is found above the document title
|
1378
1503
|
elsif !options[:text] && (match = next_line.match(REGEXP[:blk_title]))
|
@@ -1566,6 +1691,8 @@ class Lexer
|
|
1566
1691
|
# returns an instance of Asciidoctor::Table parsed from the provided reader
|
1567
1692
|
def self.next_table(table_reader, parent, attributes)
|
1568
1693
|
table = Table.new(parent, attributes)
|
1694
|
+
table.title = attributes.delete('title') if attributes.has_key?('title')
|
1695
|
+
table.assign_caption attributes.delete('caption')
|
1569
1696
|
|
1570
1697
|
if attributes.has_key? 'cols'
|
1571
1698
|
table.create_columns(parse_col_specs(attributes['cols']))
|