asciidoctor 1.5.5 → 1.5.6
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 +4 -4
- data/CHANGELOG.adoc +216 -1
- data/CONTRIBUTING.adoc +2 -2
- data/Gemfile +20 -1
- data/LICENSE.adoc +1 -1
- data/README-fr.adoc +4 -3
- data/README-jp.adoc +11 -10
- data/README-zh_CN.adoc +4 -3
- data/README.adoc +17 -202
- data/Rakefile +41 -25
- data/asciidoctor.gemspec +9 -10
- data/data/locale/attributes.adoc +216 -34
- data/data/stylesheets/asciidoctor-default.css +23 -16
- data/features/step_definitions.rb +15 -19
- data/features/xref.feature +584 -20
- data/lib/asciidoctor.rb +292 -278
- data/lib/asciidoctor/abstract_block.rb +155 -94
- data/lib/asciidoctor/abstract_node.rb +108 -94
- data/lib/asciidoctor/attribute_list.rb +30 -22
- data/lib/asciidoctor/block.rb +7 -7
- data/lib/asciidoctor/cli/invoker.rb +47 -34
- data/lib/asciidoctor/cli/options.rb +22 -11
- data/lib/asciidoctor/converter.rb +3 -3
- data/lib/asciidoctor/converter/base.rb +2 -2
- data/lib/asciidoctor/converter/composite.rb +1 -1
- data/lib/asciidoctor/converter/docbook45.rb +2 -2
- data/lib/asciidoctor/converter/docbook5.rb +132 -87
- data/lib/asciidoctor/converter/factory.rb +0 -1
- data/lib/asciidoctor/converter/html5.rb +116 -98
- data/lib/asciidoctor/converter/manpage.rb +51 -52
- data/lib/asciidoctor/converter/template.rb +47 -36
- data/lib/asciidoctor/core_ext.rb +8 -2
- data/lib/asciidoctor/core_ext/1.8.7/hash/key.rb +4 -0
- data/lib/asciidoctor/core_ext/1.8.7/io/binread.rb +6 -0
- data/lib/asciidoctor/core_ext/1.8.7/io/write.rb +5 -0
- data/lib/asciidoctor/core_ext/1.8.7/string/chr.rb +1 -1
- data/lib/asciidoctor/core_ext/1.8.7/string/{limit.rb → limit_bytesize.rb} +7 -6
- data/lib/asciidoctor/core_ext/1.8.7/symbol/empty.rb +6 -0
- data/lib/asciidoctor/core_ext/1.8.7/symbol/length.rb +1 -1
- data/lib/asciidoctor/core_ext/nil_or_empty.rb +5 -5
- data/lib/asciidoctor/core_ext/regexp/is_match.rb +3 -0
- data/lib/asciidoctor/core_ext/string/{limit.rb → limit_bytesize.rb} +2 -2
- data/lib/asciidoctor/document.rb +216 -213
- data/lib/asciidoctor/extensions.rb +318 -185
- data/lib/asciidoctor/helpers.rb +35 -35
- data/lib/asciidoctor/inline.rb +32 -1
- data/lib/asciidoctor/list.rb +22 -6
- data/lib/asciidoctor/parser.rb +1008 -1038
- data/lib/asciidoctor/path_resolver.rb +46 -50
- data/lib/asciidoctor/reader.rb +275 -251
- data/lib/asciidoctor/section.rb +86 -58
- data/lib/asciidoctor/stylesheets.rb +6 -6
- data/lib/asciidoctor/substitutors.rb +567 -649
- data/lib/asciidoctor/table.rb +163 -108
- data/lib/asciidoctor/version.rb +1 -1
- data/man/asciidoctor.1 +18 -16
- data/man/asciidoctor.adoc +15 -13
- data/test/attributes_test.rb +138 -22
- data/test/blocks_test.rb +377 -97
- data/test/converter_test.rb +13 -0
- data/test/document_test.rb +244 -34
- data/test/extensions_test.rb +409 -42
- data/test/fixtures/asciidoc_index.txt +521 -0
- data/test/fixtures/basic-docinfo-footer.html +6 -0
- data/test/fixtures/basic-docinfo-footer.xml +8 -0
- data/test/fixtures/basic-docinfo.html +1 -0
- data/test/fixtures/basic-docinfo.xml +4 -0
- data/test/fixtures/basic.asciidoc +5 -0
- data/test/fixtures/chapter-a.adoc +3 -0
- data/test/fixtures/child-include.adoc +5 -0
- data/test/fixtures/circle.svg +9 -0
- data/test/fixtures/custom-backends/erb/html5/block_paragraph.html.erb +6 -0
- data/test/fixtures/custom-backends/haml/docbook45/block_paragraph.xml.haml +6 -0
- data/test/fixtures/custom-backends/haml/html5-tweaks/block_paragraph.html.haml +1 -0
- data/test/fixtures/custom-backends/haml/html5/block_paragraph.html.haml +3 -0
- data/test/fixtures/custom-backends/haml/html5/block_sidebar.html.haml +5 -0
- data/test/fixtures/custom-backends/slim/docbook45/block_paragraph.xml.slim +6 -0
- data/test/fixtures/custom-backends/slim/html5/block_paragraph.html.slim +3 -0
- data/test/fixtures/custom-backends/slim/html5/block_sidebar.html.slim +5 -0
- data/test/fixtures/custom-docinfodir/basic-docinfo.html +1 -0
- data/test/fixtures/custom-docinfodir/docinfo.html +1 -0
- data/test/fixtures/docinfo-footer.html +1 -0
- data/test/fixtures/docinfo-footer.xml +9 -0
- data/test/fixtures/docinfo.html +1 -0
- data/test/fixtures/docinfo.xml +3 -0
- data/test/fixtures/dot.gif +0 -0
- data/test/fixtures/encoding.asciidoc +13 -0
- data/test/fixtures/grandchild-include.adoc +3 -0
- data/test/fixtures/hello-asciidoctor.pdf +69 -0
- data/test/fixtures/include-file.asciidoc +24 -0
- data/test/fixtures/include-file.ml +3 -0
- data/test/fixtures/include-file.xml +5 -0
- data/test/fixtures/master.adoc +5 -0
- data/test/fixtures/mismatched-end-tag.adoc +7 -0
- data/test/fixtures/parent-include-restricted.adoc +5 -0
- data/test/fixtures/parent-include.adoc +5 -0
- data/test/fixtures/sample.asciidoc +26 -0
- data/test/fixtures/stylesheets/custom.css +3 -0
- data/test/fixtures/subs-docinfo.html +2 -0
- data/test/fixtures/subs.adoc +7 -0
- data/test/fixtures/tagged-class-enclosed.rb +26 -0
- data/test/fixtures/tagged-class.rb +23 -0
- data/test/fixtures/tip.gif +0 -0
- data/test/invoker_test.rb +82 -4
- data/test/links_test.rb +312 -37
- data/test/lists_test.rb +204 -25
- data/test/manpage_test.rb +191 -4
- data/test/options_test.rb +18 -1
- data/test/paragraphs_test.rb +32 -7
- data/test/parser_test.rb +150 -30
- data/test/paths_test.rb +47 -13
- data/test/preamble_test.rb +1 -1
- data/test/reader_test.rb +366 -126
- data/test/sections_test.rb +203 -56
- data/test/substitutions_test.rb +339 -131
- data/test/tables_test.rb +315 -15
- data/test/test_helper.rb +400 -0
- data/test/text_test.rb +5 -5
- metadata +110 -22
data/lib/asciidoctor/helpers.rb
CHANGED
@@ -5,7 +5,7 @@ module Helpers
|
|
5
5
|
#
|
6
6
|
# Attempts to load the library specified in the first argument using the
|
7
7
|
# Kernel#require. Rescues the LoadError if the library is not available and
|
8
|
-
# passes a message to Kernel#
|
8
|
+
# passes a message to Kernel#raise if on_failure is :abort or Kernel#warn if
|
9
9
|
# on_failure is :warn to communicate to the user that processing is being
|
10
10
|
# aborted or functionality is disabled, respectively. If a gem_name is
|
11
11
|
# specified, the message communicates that a required gem is not installed.
|
@@ -17,7 +17,7 @@ module Helpers
|
|
17
17
|
# on_failure - a Symbol that indicates how to handle a load failure (:abort, :warn, :ignore) (default: :abort)
|
18
18
|
#
|
19
19
|
# returns The return value of Kernel#require if the library is available and can be, or was previously, loaded.
|
20
|
-
# Otherwise, Kernel#
|
20
|
+
# Otherwise, Kernel#raise is called with an appropriate message if on_failure is :abort.
|
21
21
|
# Otherwise, Kernel#warn is called with an appropriate message and nil returned if on_failure is :warn.
|
22
22
|
# Otherwise, nil is returned.
|
23
23
|
def self.require_library name, gem_name = true, on_failure = :abort
|
@@ -27,14 +27,14 @@ module Helpers
|
|
27
27
|
gem_name = name if gem_name == true
|
28
28
|
case on_failure
|
29
29
|
when :abort
|
30
|
-
|
30
|
+
raise ::LoadError, %(asciidoctor: FAILED: required gem '#{gem_name}' is not installed. Processing aborted.)
|
31
31
|
when :warn
|
32
32
|
warn %(asciidoctor: WARNING: optional gem '#{gem_name}' is not installed. Functionality disabled.)
|
33
33
|
end
|
34
34
|
else
|
35
35
|
case on_failure
|
36
36
|
when :abort
|
37
|
-
|
37
|
+
raise ::LoadError, %(asciidoctor: FAILED: #{e.message.chomp '.'}. Processing aborted.)
|
38
38
|
when :warn
|
39
39
|
warn %(asciidoctor: WARNING: #{e.message.chomp '.'}. Functionality disabled.)
|
40
40
|
end
|
@@ -62,19 +62,18 @@ module Helpers
|
|
62
62
|
#
|
63
63
|
# returns a String Array of normalized lines
|
64
64
|
def self.normalize_lines_array data
|
65
|
-
return
|
65
|
+
return data if data.empty?
|
66
66
|
|
67
|
-
|
68
|
-
leading_bytes = (first_line = data[0])[0..2].bytes.to_a
|
67
|
+
leading_bytes = (first_line = data[0]).unpack 'C3'
|
69
68
|
if COERCE_ENCODING
|
70
69
|
utf8 = ::Encoding::UTF_8
|
71
|
-
if (leading_2_bytes = leading_bytes
|
72
|
-
# Ruby messes up trailing whitespace on UTF-16LE, so take a different route
|
73
|
-
return ((data.join.force_encoding ::Encoding::UTF_16LE)[1..-1].encode utf8).
|
70
|
+
if (leading_2_bytes = leading_bytes.slice 0, 2) == BOM_BYTES_UTF_16LE
|
71
|
+
# HACK Ruby messes up trailing whitespace on UTF-16LE, so take a different route
|
72
|
+
return ((data.join.force_encoding ::Encoding::UTF_16LE)[1..-1].encode utf8).each_line.map {|line| line.rstrip }
|
74
73
|
elsif leading_2_bytes == BOM_BYTES_UTF_16BE
|
75
74
|
data[0] = (first_line.force_encoding ::Encoding::UTF_16BE)[1..-1]
|
76
|
-
return data.map {|line|
|
77
|
-
elsif leading_bytes
|
75
|
+
return data.map {|line| %(#{((line.force_encoding ::Encoding::UTF_16BE).encode utf8).rstrip}) }
|
76
|
+
elsif leading_bytes == BOM_BYTES_UTF_8
|
78
77
|
data[0] = (first_line.force_encoding utf8)[1..-1]
|
79
78
|
end
|
80
79
|
|
@@ -102,22 +101,21 @@ module Helpers
|
|
102
101
|
def self.normalize_lines_from_string data
|
103
102
|
return [] if data.nil_or_empty?
|
104
103
|
|
104
|
+
leading_bytes = data.unpack 'C3'
|
105
105
|
if COERCE_ENCODING
|
106
106
|
utf8 = ::Encoding::UTF_8
|
107
|
-
|
108
|
-
leading_bytes = data[0..2].bytes.to_a
|
109
|
-
if (leading_2_bytes = leading_bytes[0..1]) == BOM_BYTES_UTF_16LE
|
107
|
+
if (leading_2_bytes = leading_bytes.slice 0, 2) == BOM_BYTES_UTF_16LE
|
110
108
|
data = (data.force_encoding ::Encoding::UTF_16LE)[1..-1].encode utf8
|
111
109
|
elsif leading_2_bytes == BOM_BYTES_UTF_16BE
|
112
110
|
data = (data.force_encoding ::Encoding::UTF_16BE)[1..-1].encode utf8
|
113
|
-
elsif leading_bytes
|
111
|
+
elsif leading_bytes == BOM_BYTES_UTF_8
|
114
112
|
data = data.encoding == utf8 ? data[1..-1] : (data.force_encoding utf8)[1..-1]
|
115
113
|
else
|
116
114
|
data = data.force_encoding utf8 unless data.encoding == utf8
|
117
115
|
end
|
118
116
|
else
|
119
117
|
# Ruby 1.8 has no built-in re-encoding, so no point in removing the UTF-16 BOMs
|
120
|
-
if
|
118
|
+
if leading_bytes == BOM_BYTES_UTF_8
|
121
119
|
data = data[3..-1]
|
122
120
|
end
|
123
121
|
end
|
@@ -133,7 +131,7 @@ module Helpers
|
|
133
131
|
#
|
134
132
|
# returns true if the String is a URI, false if it is not
|
135
133
|
def self.uriish? str
|
136
|
-
(str.include? ':') && str
|
134
|
+
(str.include? ':') && (UriSniffRx.match? str)
|
137
135
|
end
|
138
136
|
|
139
137
|
# Public: Efficiently retrieves the URI prefix of the specified String
|
@@ -145,26 +143,24 @@ module Helpers
|
|
145
143
|
#
|
146
144
|
# returns the string URI prefix if the string is a URI, otherwise nil
|
147
145
|
def self.uri_prefix str
|
148
|
-
(str.include? ':') &&
|
146
|
+
(str.include? ':') && UriSniffRx =~ str ? $& : nil
|
149
147
|
end
|
150
148
|
|
151
149
|
# Matches the characters in a URI to encode
|
152
150
|
REGEXP_ENCODE_URI_CHARS = /[^\w\-.!~*';:@=+$,()\[\]]/
|
153
151
|
|
154
|
-
# Public: Encode a
|
152
|
+
# Public: Encode a String for inclusion in a URI.
|
155
153
|
#
|
156
|
-
# str - the
|
154
|
+
# str - the String to URI encode
|
157
155
|
#
|
158
|
-
#
|
159
|
-
def self.
|
160
|
-
str.gsub(REGEXP_ENCODE_URI_CHARS)
|
161
|
-
$&.each_byte.map {|c| sprintf '%%%02X', c}.join
|
162
|
-
end
|
156
|
+
# Returns the String with all URI reserved characters encoded.
|
157
|
+
def self.uri_encode str
|
158
|
+
str.gsub(REGEXP_ENCODE_URI_CHARS) { $&.each_byte.map {|c| sprintf '%%%02X', c }.join }
|
163
159
|
end
|
164
160
|
|
165
161
|
# Public: Removes the file extension from filename and returns the result
|
166
162
|
#
|
167
|
-
#
|
163
|
+
# filename - The String file name to process
|
168
164
|
#
|
169
165
|
# Examples
|
170
166
|
#
|
@@ -172,26 +168,30 @@ module Helpers
|
|
172
168
|
# # => "part1/chapter1"
|
173
169
|
#
|
174
170
|
# Returns the String filename with the file extension removed
|
175
|
-
def self.rootname
|
176
|
-
|
171
|
+
def self.rootname filename
|
172
|
+
filename.slice 0, ((filename.rindex '.') || filename.length)
|
177
173
|
end
|
178
174
|
|
179
175
|
# Public: Retrieves the basename of the filename, optionally removing the extension, if present
|
180
176
|
#
|
181
|
-
#
|
182
|
-
#
|
177
|
+
# filename - The String file name to process.
|
178
|
+
# drop_ext - A Boolean flag indicating whether to drop the extension
|
179
|
+
# or an explicit String extension to drop (default: nil).
|
183
180
|
#
|
184
181
|
# Examples
|
185
182
|
#
|
186
183
|
# Helpers.basename('images/tiger.png', true)
|
187
184
|
# # => "tiger"
|
188
185
|
#
|
186
|
+
# Helpers.basename('images/tiger.png', '.png')
|
187
|
+
# # => "tiger"
|
188
|
+
#
|
189
189
|
# Returns the String filename with leading directories removed and, if specified, the extension removed
|
190
|
-
def self.basename(
|
191
|
-
if
|
192
|
-
::File.basename
|
190
|
+
def self.basename(filename, drop_ext = nil)
|
191
|
+
if drop_ext
|
192
|
+
::File.basename filename, (drop_ext == true ? (::File.extname filename) : drop_ext)
|
193
193
|
else
|
194
|
-
::File.basename
|
194
|
+
::File.basename filename
|
195
195
|
end
|
196
196
|
end
|
197
197
|
|
data/lib/asciidoctor/inline.rb
CHANGED
@@ -39,6 +39,37 @@ class Inline < AbstractNode
|
|
39
39
|
end
|
40
40
|
|
41
41
|
# Alias render to convert to maintain backwards compatibility
|
42
|
-
alias
|
42
|
+
alias render convert
|
43
|
+
|
44
|
+
# Public: Returns the converted alt text for this inline image.
|
45
|
+
#
|
46
|
+
# Returns the [String] value of the alt attribute.
|
47
|
+
def alt
|
48
|
+
attr 'alt'
|
49
|
+
end
|
50
|
+
|
51
|
+
# (see AbstractNode#reftext?)
|
52
|
+
def reftext?
|
53
|
+
@text && (@type == :ref || @type == :bibref)
|
54
|
+
end
|
55
|
+
|
56
|
+
# (see AbstractNode#reftext)
|
57
|
+
def reftext
|
58
|
+
(val = @text) ? (apply_reftext_subs val) : nil
|
59
|
+
end
|
60
|
+
|
61
|
+
# Public: Generate cross reference text (xreftext) that can be used to refer
|
62
|
+
# to this inline node.
|
63
|
+
#
|
64
|
+
# Use the explicit reftext for this inline node, if specified, retrieved by
|
65
|
+
# calling the reftext method. Otherwise, returns nil.
|
66
|
+
#
|
67
|
+
# xrefstyle - Not currently used (default: nil).
|
68
|
+
#
|
69
|
+
# Returns the [String] reftext to refer to this inline node or nothing if no
|
70
|
+
# reftext is defined.
|
71
|
+
def xreftext xrefstyle = nil
|
72
|
+
reftext
|
73
|
+
end
|
43
74
|
end
|
44
75
|
end
|
data/lib/asciidoctor/list.rb
CHANGED
@@ -4,11 +4,11 @@ module Asciidoctor
|
|
4
4
|
class List < AbstractBlock
|
5
5
|
|
6
6
|
# Public: Create alias for blocks
|
7
|
-
alias
|
7
|
+
alias items blocks
|
8
8
|
# Public: Get the items in this list as an Array
|
9
|
-
alias
|
9
|
+
alias content blocks
|
10
10
|
# Public: Create alias to check if this list has blocks
|
11
|
-
alias
|
11
|
+
alias items? blocks?
|
12
12
|
|
13
13
|
def initialize parent, context
|
14
14
|
super
|
@@ -32,7 +32,7 @@ class List < AbstractBlock
|
|
32
32
|
end
|
33
33
|
|
34
34
|
# Alias render to convert to maintain backwards compatibility
|
35
|
-
alias
|
35
|
+
alias render convert
|
36
36
|
|
37
37
|
def to_s
|
38
38
|
%(#<#{self.class}@#{object_id} {context: #{@context.inspect}, style: #{@style.inspect}, items: #{items.size}}>)
|
@@ -44,7 +44,7 @@ end
|
|
44
44
|
class ListItem < AbstractBlock
|
45
45
|
|
46
46
|
# A contextual alias for the list parent node; counterpart to the items alias on List
|
47
|
-
alias
|
47
|
+
alias list parent
|
48
48
|
|
49
49
|
# Public: Get/Set the String used to mark this list item
|
50
50
|
attr_accessor :marker
|
@@ -57,14 +57,30 @@ class ListItem < AbstractBlock
|
|
57
57
|
super parent, :list_item
|
58
58
|
@text = text
|
59
59
|
@level = parent.level
|
60
|
+
@subs = NORMAL_SUBS.dup
|
60
61
|
end
|
61
62
|
|
63
|
+
# Public: A convenience method that checks whether the text of this list item
|
64
|
+
# is not blank (i.e., not nil or empty string).
|
62
65
|
def text?
|
63
66
|
!@text.nil_or_empty?
|
64
67
|
end
|
65
68
|
|
69
|
+
# Public: Get the String text of this ListItem with substitutions applied.
|
70
|
+
#
|
71
|
+
# By default, normal substitutions are applied to the text. The substitutions
|
72
|
+
# can be modified by altering the subs property of this object.
|
73
|
+
#
|
74
|
+
# Returns the converted String text for this ListItem
|
66
75
|
def text
|
67
|
-
apply_subs @text
|
76
|
+
apply_subs @text, @subs
|
77
|
+
end
|
78
|
+
|
79
|
+
# Public: Set the String text.
|
80
|
+
#
|
81
|
+
# Returns the new String text assigned to this ListItem
|
82
|
+
def text= val
|
83
|
+
@text = val
|
68
84
|
end
|
69
85
|
|
70
86
|
# Check whether this list item has simple content (no nested blocks aside from a single outline list).
|
data/lib/asciidoctor/parser.rb
CHANGED
@@ -32,19 +32,48 @@ class Parser
|
|
32
32
|
# Regexp for leading tab indentation
|
33
33
|
TabIndentRx = /^\t+/
|
34
34
|
|
35
|
-
StartOfBlockProc = lambda {|l| ((l.start_with? '[') && BlockAttributeLineRx
|
35
|
+
StartOfBlockProc = lambda {|l| ((l.start_with? '[') && (BlockAttributeLineRx.match? l)) || (is_delimited_block? l) }
|
36
36
|
|
37
|
-
StartOfListProc = lambda {|l| AnyListRx
|
37
|
+
StartOfListProc = lambda {|l| AnyListRx.match? l }
|
38
38
|
|
39
|
-
StartOfBlockOrListProc = lambda {|l| (is_delimited_block? l) || ((l.start_with? '[') && BlockAttributeLineRx
|
39
|
+
StartOfBlockOrListProc = lambda {|l| (is_delimited_block? l) || ((l.start_with? '[') && (BlockAttributeLineRx.match? l)) || (AnyListRx.match? l) }
|
40
40
|
|
41
41
|
NoOp = nil
|
42
42
|
|
43
|
+
# Internal: A Hash mapping horizontal alignment abbreviations to alignments
|
44
|
+
# that can be applied to a table cell (or to all cells in a column)
|
45
|
+
TableCellHorzAlignments = {
|
46
|
+
'<' => 'left',
|
47
|
+
'>' => 'right',
|
48
|
+
'^' => 'center'
|
49
|
+
}
|
50
|
+
|
51
|
+
# Internal: A Hash mapping vertical alignment abbreviations to alignments
|
52
|
+
# that can be applied to a table cell (or to all cells in a column)
|
53
|
+
TableCellVertAlignments = {
|
54
|
+
'<' => 'top',
|
55
|
+
'>' => 'bottom',
|
56
|
+
'^' => 'middle'
|
57
|
+
}
|
58
|
+
|
59
|
+
# Internal: A Hash mapping styles abbreviations to styles that can be applied
|
60
|
+
# to a table cell (or to all cells in a column)
|
61
|
+
TableCellStyles = {
|
62
|
+
'd' => :none,
|
63
|
+
's' => :strong,
|
64
|
+
'e' => :emphasis,
|
65
|
+
'm' => :monospaced,
|
66
|
+
'h' => :header,
|
67
|
+
'l' => :literal,
|
68
|
+
'v' => :verse,
|
69
|
+
'a' => :asciidoc
|
70
|
+
}
|
71
|
+
|
43
72
|
# Public: Make sure the Parser object doesn't get initialized.
|
44
73
|
#
|
45
74
|
# Raises RuntimeError if this constructor is invoked.
|
46
75
|
def initialize
|
47
|
-
raise 'Au contraire, mon frere. No
|
76
|
+
raise 'Au contraire, mon frere. No parser instances will be running around.'
|
48
77
|
end
|
49
78
|
|
50
79
|
# Public: Parses AsciiDoc source read from the Reader into the Document
|
@@ -62,12 +91,10 @@ class Parser
|
|
62
91
|
def self.parse(reader, document, options = {})
|
63
92
|
block_attributes = parse_document_header(reader, document)
|
64
93
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
end
|
70
|
-
end
|
94
|
+
while reader.has_more_lines?
|
95
|
+
new_section, block_attributes = next_section(reader, document, block_attributes)
|
96
|
+
document << new_section if new_section
|
97
|
+
end unless options[:header_only]
|
71
98
|
|
72
99
|
document
|
73
100
|
end
|
@@ -86,12 +113,12 @@ class Parser
|
|
86
113
|
# returns the Hash of orphan block attributes captured above the header
|
87
114
|
def self.parse_document_header(reader, document)
|
88
115
|
# capture lines of block-level metadata and plow away comment lines that precede first block
|
89
|
-
block_attributes = parse_block_metadata_lines
|
116
|
+
block_attributes = parse_block_metadata_lines reader, document
|
90
117
|
|
91
118
|
# special case, block title is not allowed above document title,
|
92
119
|
# carry attributes over to the document body
|
93
|
-
if (
|
94
|
-
block_attributes.
|
120
|
+
if (implicit_doctitle = is_next_line_doctitle? reader, block_attributes, document.attributes['leveloffset']) &&
|
121
|
+
(block_attributes.key? 'title')
|
95
122
|
return document.finalize_header block_attributes, false
|
96
123
|
end
|
97
124
|
|
@@ -104,16 +131,16 @@ class Parser
|
|
104
131
|
|
105
132
|
section_title = nil
|
106
133
|
# if the first line is the document title, add a header to the document and parse the header metadata
|
107
|
-
if
|
134
|
+
if implicit_doctitle
|
108
135
|
source_location = reader.cursor if document.sourcemap
|
109
136
|
document.id, _, doctitle, _, single_line = parse_section_title reader, document
|
110
137
|
unless assigned_doctitle
|
111
138
|
document.title = assigned_doctitle = doctitle
|
112
139
|
end
|
113
140
|
# default to compat-mode if document uses atx-style doctitle
|
114
|
-
document.
|
141
|
+
document.set_attr 'compat-mode' unless single_line || (document.attribute_locked? 'compat-mode')
|
115
142
|
if (separator = block_attributes.delete 'separator')
|
116
|
-
document.
|
143
|
+
document.set_attr 'title-separator', separator unless document.attribute_locked? 'title-separator'
|
117
144
|
end
|
118
145
|
document.header.source_location = source_location if source_location
|
119
146
|
document.attributes['doctitle'] = section_title = doctitle
|
@@ -151,9 +178,9 @@ class Parser
|
|
151
178
|
#
|
152
179
|
# returns Nothing
|
153
180
|
def self.parse_manpage_header(reader, document)
|
154
|
-
if
|
155
|
-
document.attributes['mantitle'] = document.sub_attributes
|
156
|
-
document.attributes['manvolnum'] =
|
181
|
+
if ManpageTitleVolnumRx =~ document.attributes['doctitle']
|
182
|
+
document.attributes['mantitle'] = document.sub_attributes $1.downcase
|
183
|
+
document.attributes['manvolnum'] = $2
|
157
184
|
else
|
158
185
|
warn %(asciidoctor: ERROR: #{reader.prev_line_info}: malformed manpage title)
|
159
186
|
# provide sensible fallbacks
|
@@ -166,7 +193,7 @@ class Parser
|
|
166
193
|
if is_next_line_section?(reader, {})
|
167
194
|
name_section = initialize_section(reader, document, {})
|
168
195
|
if name_section.level == 1
|
169
|
-
name_section_buffer = reader.read_lines_until(:break_on_blank_lines => true)
|
196
|
+
name_section_buffer = reader.read_lines_until(:break_on_blank_lines => true) * ' '
|
170
197
|
if (m = ManpageNamePurposeRx.match(name_section_buffer))
|
171
198
|
document.attributes['manname'] = document.sub_attributes m[1]
|
172
199
|
document.attributes['manpurpose'] = m[2]
|
@@ -217,37 +244,33 @@ class Parser
|
|
217
244
|
# # and hold attributes extracted from header
|
218
245
|
# doc = Document.new
|
219
246
|
#
|
220
|
-
# Parser.next_section(reader, doc).
|
247
|
+
# Parser.next_section(reader, doc)[0].title
|
221
248
|
# # => "Greetings"
|
222
249
|
#
|
223
|
-
# Parser.next_section(reader, doc).
|
250
|
+
# Parser.next_section(reader, doc)[0].title
|
224
251
|
# # => "Salutations"
|
225
252
|
#
|
226
253
|
# returns a two-element Array containing the Section and Hash of orphaned attributes
|
227
|
-
def self.next_section
|
228
|
-
preamble = false
|
229
|
-
part = false
|
230
|
-
intro = false
|
254
|
+
def self.next_section reader, parent, attributes = {}
|
255
|
+
preamble = intro = part = false
|
231
256
|
|
232
257
|
# FIXME if attributes[1] is a verbatim style, then don't check for section
|
233
258
|
|
234
259
|
# check if we are at the start of processing the document
|
235
260
|
# NOTE we could drop a hint in the attributes to indicate
|
236
261
|
# that we are at a section title (so we don't have to check)
|
237
|
-
if parent.context == :document && parent.blocks.empty? &&
|
238
|
-
(
|
239
|
-
doctype = parent.doctype
|
262
|
+
if parent.context == :document && parent.blocks.empty? && ((has_header = parent.has_header?) ||
|
263
|
+
(attributes.delete 'invalid-header') || !(is_next_line_section? reader, attributes))
|
264
|
+
doctype = (document = parent).doctype
|
240
265
|
if has_header || (doctype == 'book' && attributes[1] != 'abstract')
|
241
|
-
preamble = intro = Block.new
|
242
|
-
if doctype == 'book' && (parent.attr? 'preface-title')
|
243
|
-
preamble.title = parent.attr 'preface-title'
|
244
|
-
end
|
266
|
+
preamble = intro = (Block.new parent, :preamble, :content_model => :compound)
|
267
|
+
preamble.title = parent.attr 'preface-title' if doctype == 'book' && (parent.attr? 'preface-title')
|
245
268
|
parent << preamble
|
246
269
|
end
|
247
270
|
section = parent
|
248
271
|
|
249
272
|
current_level = 0
|
250
|
-
if parent.attributes.
|
273
|
+
if parent.attributes.key? 'fragment'
|
251
274
|
expected_next_levels = nil
|
252
275
|
# small tweak to allow subsequent level-0 sections for book doctype
|
253
276
|
elsif doctype == 'book'
|
@@ -256,23 +279,12 @@ class Parser
|
|
256
279
|
expected_next_levels = [1]
|
257
280
|
end
|
258
281
|
else
|
259
|
-
doctype = parent.document.doctype
|
260
|
-
section = initialize_section
|
261
|
-
# clear attributes
|
262
|
-
# section title to next block of content
|
282
|
+
doctype = (document = parent.document).doctype
|
283
|
+
section = initialize_section reader, parent, attributes
|
284
|
+
# clear attributes except for title attribute, which must be carried over to next content block
|
263
285
|
attributes = (title = attributes['title']) ? { 'title' => title } : {}
|
264
|
-
|
265
|
-
|
266
|
-
part = !section.special
|
267
|
-
# subsections in preface & appendix in multipart books start at level 2
|
268
|
-
if section.special && (['preface', 'appendix'].include? section.sectname)
|
269
|
-
expected_next_levels = [current_level + 2]
|
270
|
-
else
|
271
|
-
expected_next_levels = [current_level + 1]
|
272
|
-
end
|
273
|
-
else
|
274
|
-
expected_next_levels = [current_level + 1]
|
275
|
-
end
|
286
|
+
part = section.sectname == 'part'
|
287
|
+
expected_next_levels = [(current_level = section.level) + 1]
|
276
288
|
end
|
277
289
|
|
278
290
|
reader.skip_blank_lines
|
@@ -287,20 +299,18 @@ class Parser
|
|
287
299
|
# We have to parse all the metadata lines before continuing with the loop,
|
288
300
|
# otherwise subsequent metadata lines get interpreted as block content
|
289
301
|
while reader.has_more_lines?
|
290
|
-
parse_block_metadata_lines
|
302
|
+
parse_block_metadata_lines reader, document, attributes
|
291
303
|
|
292
|
-
if (next_level = is_next_line_section?
|
293
|
-
next_level +=
|
294
|
-
if next_level > current_level || (
|
304
|
+
if (next_level = is_next_line_section?(reader, attributes))
|
305
|
+
next_level += document.attr('leveloffset').to_i if document.attr?('leveloffset')
|
306
|
+
if next_level > current_level || (next_level == 0 && section.context == :document)
|
295
307
|
if next_level == 0 && doctype != 'book'
|
296
308
|
warn %(asciidoctor: ERROR: #{reader.line_info}: only book doctypes can contain level 0 sections)
|
297
309
|
elsif expected_next_levels && !expected_next_levels.include?(next_level)
|
298
|
-
warn %(asciidoctor: WARNING: #{reader.line_info}: section title out of sequence: )
|
299
|
-
%(expected #{expected_next_levels.size > 1 ? 'levels' : 'level'} #{expected_next_levels * ' or '}, ) +
|
300
|
-
%(got level #{next_level})
|
310
|
+
warn %(asciidoctor: WARNING: #{reader.line_info}: section title out of sequence: expected #{expected_next_levels.size > 1 ? 'levels' : 'level'} #{expected_next_levels * ' or '}, got level #{next_level})
|
301
311
|
end
|
302
312
|
# the attributes returned are those that are orphaned
|
303
|
-
new_section, attributes = next_section
|
313
|
+
new_section, attributes = next_section reader, section, attributes
|
304
314
|
section << new_section
|
305
315
|
else
|
306
316
|
if next_level == 0 && doctype != 'book'
|
@@ -312,7 +322,7 @@ class Parser
|
|
312
322
|
else
|
313
323
|
# just take one block or else we run the risk of overrunning section boundaries
|
314
324
|
block_line_info = reader.line_info
|
315
|
-
if (new_block = next_block reader,
|
325
|
+
if (new_block = next_block reader, intro || section, attributes, :parse_metadata => false)
|
316
326
|
# REVIEW this may be doing too much
|
317
327
|
if part
|
318
328
|
if !section.blocks?
|
@@ -372,7 +382,6 @@ class Parser
|
|
372
382
|
# that would require reworking assumptions in next_section since the preamble
|
373
383
|
# is treated like an untitled section
|
374
384
|
elsif preamble # implies parent == document
|
375
|
-
document = parent
|
376
385
|
if preamble.blocks?
|
377
386
|
# unwrap standalone preamble (i.e., no sections), if permissible
|
378
387
|
if Compliance.unwrap_standalone_preamble && document.blocks.size == 1 && doctype != 'book'
|
@@ -395,23 +404,26 @@ class Parser
|
|
395
404
|
[section != parent ? section : nil, attributes.dup]
|
396
405
|
end
|
397
406
|
|
398
|
-
# Public:
|
407
|
+
# Public: Parse and return the next Block at the Reader's current location
|
399
408
|
#
|
400
|
-
#
|
401
|
-
#
|
402
|
-
#
|
409
|
+
# This method begins by skipping over blank lines to find the start of the
|
410
|
+
# next block (paragraph, block macro, or delimited block). If a block is
|
411
|
+
# found, that block is parsed, initialized as a Block object, and returned.
|
412
|
+
# Otherwise, the method returns nothing.
|
403
413
|
#
|
404
|
-
#
|
405
|
-
#
|
406
|
-
#
|
414
|
+
# Regular expressions from the Asciidoctor module are used to match block
|
415
|
+
# boundaries. The ensuing lines are then processed according to the content
|
416
|
+
# model.
|
407
417
|
#
|
408
|
-
# reader
|
409
|
-
# parent
|
418
|
+
# reader - The Reader from which to retrieve the next Block.
|
419
|
+
# parent - The Document, Section or Block to which the next Block belongs.
|
420
|
+
# attributes - A Hash of attributes that will become the attributes
|
421
|
+
# associated with the parsed Block (default: {}).
|
422
|
+
# options - An options Hash to control parsing (default: {}):
|
423
|
+
# * :text indicates that the parser is only looking for text content
|
410
424
|
#
|
411
|
-
# Returns a
|
412
|
-
|
413
|
-
# QUESTION should next_block have an option for whether it should keep looking until
|
414
|
-
# a block is found? right now it bails when it encounters a line to be skipped
|
425
|
+
# Returns a Block object built from the parsed content of the processed
|
426
|
+
# lines, or nothing if no block is found.
|
415
427
|
def self.next_block(reader, parent, attributes = {}, options = {})
|
416
428
|
# Skip ahead to the block content
|
417
429
|
skipped = reader.skip_blank_lines
|
@@ -423,121 +435,109 @@ class Parser
|
|
423
435
|
# if skipped a line, assume a list continuation was
|
424
436
|
# used and block content is acceptable
|
425
437
|
if (text_only = options[:text]) && skipped > 0
|
426
|
-
options.delete
|
438
|
+
options.delete :text
|
427
439
|
text_only = false
|
428
440
|
end
|
429
441
|
|
430
|
-
parse_metadata = options.fetch(:parse_metadata, true)
|
431
|
-
#parse_sections = options.fetch(:parse_sections, false)
|
432
|
-
|
433
442
|
document = parent.document
|
443
|
+
|
444
|
+
if options.fetch :parse_metadata, true
|
445
|
+
# read lines until there are no more metadata lines to read
|
446
|
+
while parse_block_metadata_line reader, document, attributes, options
|
447
|
+
advanced = reader.advance
|
448
|
+
end
|
449
|
+
if advanced && !reader.has_more_lines?
|
450
|
+
# NOTE there are no cases when these attributes are used, but clear them anyway
|
451
|
+
attributes.clear
|
452
|
+
return
|
453
|
+
end
|
454
|
+
end
|
455
|
+
|
434
456
|
if (extensions = document.extensions)
|
435
|
-
block_extensions = extensions.blocks?
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
source_location = reader.cursor if sourcemap
|
460
|
-
this_line = reader.read_line
|
461
|
-
delimited_block = false
|
462
|
-
block_context = nil
|
463
|
-
cloaked_context = nil
|
464
|
-
terminator = nil
|
465
|
-
# QUESTION put this inside call to rekey attributes?
|
466
|
-
if attributes[1]
|
467
|
-
style, explicit_style = parse_style_attribute(attributes, reader)
|
468
|
-
end
|
469
|
-
|
470
|
-
if (delimited_blk_match = is_delimited_block? this_line, true)
|
471
|
-
delimited_block = true
|
472
|
-
block_context = cloaked_context = delimited_blk_match.context
|
473
|
-
terminator = delimited_blk_match.terminator
|
474
|
-
if !style
|
475
|
-
style = attributes['style'] = block_context.to_s
|
476
|
-
elsif style != block_context.to_s
|
477
|
-
if delimited_blk_match.masq.include? style
|
478
|
-
block_context = style.to_sym
|
479
|
-
elsif delimited_blk_match.masq.include?('admonition') && ADMONITION_STYLES.include?(style)
|
480
|
-
block_context = :admonition
|
481
|
-
elsif block_extensions && extensions.registered_for_block?(style, block_context)
|
482
|
-
block_context = style.to_sym
|
483
|
-
else
|
484
|
-
warn %(asciidoctor: WARNING: #{reader.prev_line_info}: invalid style for #{block_context} block: #{style})
|
485
|
-
style = block_context.to_s
|
486
|
-
end
|
457
|
+
block_extensions, block_macro_extensions = extensions.blocks?, extensions.block_macros?
|
458
|
+
end
|
459
|
+
|
460
|
+
# QUESTION should we introduce a parsing context object?
|
461
|
+
source_location = reader.cursor if document.sourcemap
|
462
|
+
this_path, this_lineno, this_line, in_list = reader.path, reader.lineno, reader.read_line, ListItem === parent
|
463
|
+
block = block_context = cloaked_context = terminator = nil
|
464
|
+
style = attributes[1] ? (parse_style_attribute attributes, reader) : nil
|
465
|
+
|
466
|
+
if (delimited_block = is_delimited_block? this_line, true)
|
467
|
+
block_context = cloaked_context = delimited_block.context
|
468
|
+
terminator = delimited_block.terminator
|
469
|
+
if !style
|
470
|
+
style = attributes['style'] = block_context.to_s
|
471
|
+
elsif style != block_context.to_s
|
472
|
+
if delimited_block.masq.include? style
|
473
|
+
block_context = style.to_sym
|
474
|
+
elsif delimited_block.masq.include?('admonition') && ADMONITION_STYLES.include?(style)
|
475
|
+
block_context = :admonition
|
476
|
+
elsif block_extensions && extensions.registered_for_block?(style, block_context)
|
477
|
+
block_context = style.to_sym
|
478
|
+
else
|
479
|
+
warn %(asciidoctor: WARNING: #{this_path}: line #{this_lineno}: invalid style for #{block_context} block: #{style})
|
480
|
+
style = block_context.to_s
|
487
481
|
end
|
488
482
|
end
|
483
|
+
end
|
489
484
|
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
485
|
+
# this loop is used for flow control; it only executes once, and only when delimited_block is set
|
486
|
+
# break once a block is found or at end of loop
|
487
|
+
# returns nil if the line should be dropped
|
488
|
+
while true
|
489
|
+
# process lines verbatim
|
490
|
+
if style && Compliance.strict_verbatim_paragraphs && VERBATIM_STYLES.include?(style)
|
491
|
+
block_context = style.to_sym
|
492
|
+
reader.unshift_line this_line
|
493
|
+
# advance to block parsing =>
|
494
|
+
break
|
495
|
+
end
|
497
496
|
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
497
|
+
# process lines normally
|
498
|
+
if text_only
|
499
|
+
indented = this_line.start_with? ' ', TAB
|
500
|
+
else
|
501
|
+
# NOTE move this declaration up if we need it when text_only is false
|
502
|
+
md_syntax = Compliance.markdown_syntax
|
503
|
+
if this_line.start_with? ' '
|
504
|
+
indented, ch0 = true, ' '
|
505
|
+
# QUESTION should we test line length?
|
506
|
+
if md_syntax && this_line.lstrip.start_with?(*MARKDOWN_THEMATIC_BREAK_CHARS.keys) &&
|
507
|
+
#!(this_line.start_with? ' ') &&
|
508
|
+
(MarkdownThematicBreakRx.match? this_line)
|
509
|
+
# NOTE we're letting break lines (horizontal rule, page_break, etc) have attributes
|
510
|
+
block = Block.new(parent, :thematic_break, :content_model => :empty)
|
503
511
|
break
|
504
512
|
end
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
513
|
+
elsif this_line.start_with? TAB
|
514
|
+
indented, ch0 = true, TAB
|
515
|
+
else
|
516
|
+
indented, ch0 = false, this_line.chr
|
517
|
+
layout_break_chars = md_syntax ? HYBRID_LAYOUT_BREAK_CHARS : LAYOUT_BREAK_CHARS
|
518
|
+
if (layout_break_chars.key? ch0) && (md_syntax ? (HybridLayoutBreakRx.match? this_line) :
|
519
|
+
(this_line == ch0 * (ll = this_line.length) && ll > 2))
|
509
520
|
# NOTE we're letting break lines (horizontal rule, page_break, etc) have attributes
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
blk_ctx = match[1].to_sym
|
521
|
+
block = Block.new(parent, layout_break_chars[ch0], :content_model => :empty)
|
522
|
+
break
|
523
|
+
# NOTE very rare that a text-only line will end in ] (e.g., inline macro), so check that first
|
524
|
+
elsif (this_line.end_with? ']') && (this_line.include? '::')
|
525
|
+
#if (this_line.start_with? 'image', 'video', 'audio') && (match = BlockMediaMacroRx.match(this_line))
|
526
|
+
if (ch0 == 'i' || (this_line.start_with? 'video:', 'audio:')) && (match = BlockMediaMacroRx.match(this_line))
|
527
|
+
blk_ctx, target = match[1].to_sym, match[2]
|
517
528
|
block = Block.new(parent, blk_ctx, :content_model => :empty)
|
518
|
-
|
519
|
-
|
520
|
-
elsif blk_ctx == :video
|
529
|
+
case blk_ctx
|
530
|
+
when :video
|
521
531
|
posattrs = ['poster', 'width', 'height']
|
522
|
-
|
532
|
+
when :audio
|
523
533
|
posattrs = []
|
534
|
+
else # :image
|
535
|
+
posattrs = ['alt', 'width', 'height']
|
524
536
|
end
|
525
|
-
|
526
|
-
#
|
527
|
-
|
528
|
-
if
|
529
|
-
attributes['alt'] = style if blk_ctx == :image
|
530
|
-
attributes.delete 'style'
|
531
|
-
style = nil
|
532
|
-
end
|
533
|
-
|
534
|
-
block.parse_attributes(match[3], posattrs,
|
535
|
-
:unescape_input => (blk_ctx == :image),
|
536
|
-
:sub_input => true,
|
537
|
-
:sub_result => false,
|
538
|
-
:into => attributes)
|
539
|
-
target = block.sub_attributes(match[2], :attribute_missing => 'drop-line')
|
540
|
-
if target.empty?
|
537
|
+
block.parse_attributes(match[3], posattrs, :sub_input => true, :sub_result => false, :into => attributes)
|
538
|
+
# style doesn't have special meaning for media macros
|
539
|
+
attributes.delete 'style' if attributes.key? 'style'
|
540
|
+
if (target.include? '{') && (target = block.sub_attributes target, :attribute_missing => 'drop-line').empty?
|
541
541
|
# retain as unparsed if attribute-missing is skip
|
542
542
|
if document.attributes.fetch('attribute-missing', Compliance.attribute_missing) == 'skip'
|
543
543
|
return Block.new(parent, :paragraph, :content_model => :simple, :source => [this_line])
|
@@ -547,414 +547,382 @@ class Parser
|
|
547
547
|
return
|
548
548
|
end
|
549
549
|
end
|
550
|
-
|
550
|
+
if blk_ctx == :image
|
551
|
+
block.document.register :images, target
|
552
|
+
# NOTE style is the value of the first positional attribute in the block attribute line
|
553
|
+
attributes['alt'] ||= style || (attributes['default-alt'] = Helpers.basename(target, true).tr('_-', ' '))
|
554
|
+
unless (scaledwidth = attributes.delete 'scaledwidth').nil_or_empty?
|
555
|
+
# NOTE assume % units if not specified
|
556
|
+
attributes['scaledwidth'] = (TrailingDigitsRx.match? scaledwidth) ? %(#{scaledwidth}%) : scaledwidth
|
557
|
+
end
|
558
|
+
block.title = attributes.delete 'title'
|
559
|
+
block.assign_caption((attributes.delete 'caption'), 'figure')
|
560
|
+
end
|
551
561
|
attributes['target'] = target
|
552
|
-
# now done down below
|
553
|
-
#block.title = attributes.delete('title') if attributes.has_key?('title')
|
554
|
-
#if blk_ctx == :image
|
555
|
-
# if attributes.has_key? 'scaledwidth'
|
556
|
-
# # append % to scaledwidth if ends in number (no units present)
|
557
|
-
# if (48..57).include?((attributes['scaledwidth'][-1] || 0).ord)
|
558
|
-
# attributes['scaledwidth'] = %(#{attributes['scaledwidth']}%)
|
559
|
-
# end
|
560
|
-
# end
|
561
|
-
# document.register(:images, target)
|
562
|
-
# attributes['alt'] ||= Helpers.basename(target, true).tr('_-', ' ')
|
563
|
-
# # QUESTION should video or audio have an auto-numbered caption?
|
564
|
-
# block.assign_caption attributes.delete('caption'), 'figure'
|
565
|
-
#end
|
566
562
|
break
|
567
563
|
|
568
|
-
|
569
|
-
elsif first_char == 't' && (match = TocBlockMacroRx.match(this_line))
|
564
|
+
elsif ch0 == 't' && (this_line.start_with? 'toc:') && (match = BlockTocMacroRx.match(this_line))
|
570
565
|
block = Block.new(parent, :toc, :content_model => :empty)
|
571
566
|
block.parse_attributes(match[1], [], :sub_result => false, :into => attributes)
|
572
567
|
break
|
573
568
|
|
574
|
-
elsif block_macro_extensions && (match =
|
569
|
+
elsif block_macro_extensions && (match = CustomBlockMacroRx.match(this_line)) &&
|
575
570
|
(extension = extensions.registered_for_block_macro?(match[1]))
|
576
571
|
target = match[2]
|
577
|
-
|
572
|
+
content = match[3]
|
578
573
|
if extension.config[:content_model] == :attributes
|
579
|
-
unless
|
580
|
-
document.parse_attributes(
|
574
|
+
unless content.empty?
|
575
|
+
document.parse_attributes(content, extension.config[:pos_attrs] || [],
|
581
576
|
:sub_input => true, :sub_result => false, :into => attributes)
|
582
577
|
end
|
583
578
|
else
|
584
|
-
attributes['text'] =
|
579
|
+
attributes['text'] = content
|
585
580
|
end
|
586
581
|
if (default_attrs = extension.config[:default_attrs])
|
587
|
-
default_attrs
|
582
|
+
attributes.update(default_attrs) {|_, old_v| old_v }
|
588
583
|
end
|
589
|
-
if (block = extension.process_method[parent, target, attributes
|
584
|
+
if (block = extension.process_method[parent, target, attributes])
|
590
585
|
attributes.replace block.attributes
|
586
|
+
break
|
591
587
|
else
|
592
588
|
attributes.clear
|
593
589
|
return
|
594
590
|
end
|
595
|
-
break
|
596
591
|
end
|
597
592
|
end
|
593
|
+
end
|
594
|
+
end
|
598
595
|
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
|
605
|
-
|
606
|
-
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
|
617
|
-
if !coids.empty?
|
618
|
-
list_item.attributes['coids'] = coids
|
619
|
-
else
|
620
|
-
# FIXME this lineno - 2 hack means we need a proper look-behind cursor
|
621
|
-
warn %(asciidoctor: WARNING: #{reader.path}: line #{reader.lineno - 2}: no callouts refer to list item #{block.items.size})
|
622
|
-
end
|
623
|
-
end
|
624
|
-
match = nil
|
625
|
-
end
|
626
|
-
|
627
|
-
document.callouts.next_list
|
628
|
-
break
|
629
|
-
|
630
|
-
elsif UnorderedListRx =~ this_line
|
631
|
-
reader.unshift_line this_line
|
632
|
-
block = next_outline_list(reader, :ulist, parent)
|
633
|
-
break
|
634
|
-
|
635
|
-
elsif (match = OrderedListRx.match(this_line))
|
636
|
-
reader.unshift_line this_line
|
637
|
-
block = next_outline_list(reader, :olist, parent)
|
638
|
-
# TODO move this logic into next_outline_list
|
639
|
-
if !attributes['style'] && !block.attributes['style']
|
640
|
-
marker = block.items[0].marker
|
641
|
-
if marker.start_with? '.'
|
642
|
-
# first one makes more sense, but second one is AsciiDoc-compliant
|
643
|
-
#attributes['style'] = (ORDERED_LIST_STYLES[block.level - 1] || ORDERED_LIST_STYLES[0]).to_s
|
644
|
-
attributes['style'] = (ORDERED_LIST_STYLES[marker.length - 1] || ORDERED_LIST_STYLES[0]).to_s
|
645
|
-
else
|
646
|
-
style = ORDERED_LIST_STYLES.find {|s| OrderedListMarkerRxMap[s] =~ marker }
|
647
|
-
attributes['style'] = (style || ORDERED_LIST_STYLES[0]).to_s
|
648
|
-
end
|
649
|
-
end
|
650
|
-
break
|
651
|
-
|
652
|
-
elsif (match = DescriptionListRx.match(this_line))
|
653
|
-
reader.unshift_line this_line
|
654
|
-
block = next_labeled_list(reader, match, parent)
|
655
|
-
break
|
656
|
-
|
657
|
-
elsif (style == 'float' || style == 'discrete') &&
|
658
|
-
is_section_title?(this_line, (Compliance.underline_style_section_titles ? reader.peek_line(true) : nil))
|
659
|
-
reader.unshift_line this_line
|
660
|
-
float_id, float_reftext, float_title, float_level, _ = parse_section_title(reader, document)
|
661
|
-
attributes['reftext'] = float_reftext if float_reftext
|
662
|
-
float_id ||= attributes['id'] if attributes.has_key?('id')
|
663
|
-
block = Block.new(parent, :floating_title, :content_model => :empty)
|
664
|
-
if float_id.nil_or_empty?
|
665
|
-
# FIXME remove hack of creating throwaway Section to get at the generate_id method
|
666
|
-
tmp_sect = Section.new(parent)
|
667
|
-
tmp_sect.title = float_title
|
668
|
-
block.id = tmp_sect.generate_id
|
669
|
-
else
|
670
|
-
block.id = float_id
|
671
|
-
end
|
672
|
-
block.level = float_level
|
673
|
-
block.title = float_title
|
674
|
-
break
|
675
|
-
|
676
|
-
# FIXME create another set for "passthrough" styles
|
677
|
-
# FIXME make this more DRY!
|
678
|
-
elsif style && style != 'normal'
|
679
|
-
if PARAGRAPH_STYLES.include?(style)
|
680
|
-
block_context = style.to_sym
|
681
|
-
cloaked_context = :paragraph
|
682
|
-
reader.unshift_line this_line
|
683
|
-
# advance to block parsing =>
|
684
|
-
break
|
685
|
-
elsif ADMONITION_STYLES.include?(style)
|
686
|
-
block_context = :admonition
|
687
|
-
cloaked_context = :paragraph
|
688
|
-
reader.unshift_line this_line
|
689
|
-
# advance to block parsing =>
|
690
|
-
break
|
691
|
-
elsif block_extensions && extensions.registered_for_block?(style, :paragraph)
|
692
|
-
block_context = style.to_sym
|
693
|
-
cloaked_context = :paragraph
|
694
|
-
reader.unshift_line this_line
|
695
|
-
# advance to block parsing =>
|
696
|
-
break
|
596
|
+
# haven't found anything yet, continue
|
597
|
+
if !indented && CALLOUT_LIST_LEADERS.include?(ch0 ||= this_line.chr) &&
|
598
|
+
(CalloutListSniffRx.match? this_line) && (match = CalloutListRx.match this_line)
|
599
|
+
block = List.new(parent, :colist)
|
600
|
+
attributes['style'] = 'arabic'
|
601
|
+
reader.unshift_line this_line
|
602
|
+
expected_index = 1
|
603
|
+
# NOTE skip the match on the first time through as we've already done it (emulates begin...while)
|
604
|
+
while match || (reader.has_more_lines? && (match = CalloutListRx.match(reader.peek_line)))
|
605
|
+
list_item_lineno = reader.lineno
|
606
|
+
# might want to move this check to a validate method
|
607
|
+
unless match[1] == expected_index.to_s
|
608
|
+
warn %(asciidoctor: WARNING: #{reader.path}: line #{list_item_lineno}: callout list item index: expected #{expected_index} got #{match[1]})
|
609
|
+
end
|
610
|
+
if (list_item = next_list_item reader, block, match)
|
611
|
+
block << list_item
|
612
|
+
if (coids = document.callouts.callout_ids block.items.size).empty?
|
613
|
+
warn %(asciidoctor: WARNING: #{reader.path}: line #{list_item_lineno}: no callouts refer to list item #{block.items.size})
|
697
614
|
else
|
698
|
-
|
699
|
-
style = nil
|
700
|
-
# continue to process paragraph
|
615
|
+
list_item.attributes['coids'] = coids
|
701
616
|
end
|
702
617
|
end
|
618
|
+
expected_index += 1
|
619
|
+
match = nil
|
620
|
+
end
|
703
621
|
|
704
|
-
|
705
|
-
|
706
|
-
# a literal paragraph is contiguous lines starting at least one space
|
707
|
-
if style != 'normal' && LiteralParagraphRx =~ this_line
|
708
|
-
# So we need to actually include this one in the read_lines group
|
709
|
-
reader.unshift_line this_line
|
710
|
-
lines = read_paragraph_lines reader, break_at_list, :skip_line_comments => text_only
|
711
|
-
|
712
|
-
adjust_indentation! lines
|
622
|
+
document.callouts.next_list
|
623
|
+
break
|
713
624
|
|
714
|
-
|
715
|
-
|
716
|
-
|
717
|
-
|
625
|
+
elsif UnorderedListRx.match? this_line
|
626
|
+
reader.unshift_line this_line
|
627
|
+
block = next_item_list(reader, :ulist, parent)
|
628
|
+
if (style || (Section === parent && parent.sectname)) == 'bibliography'
|
629
|
+
attributes['style'] = 'bibliography' unless style
|
630
|
+
block.items.each {|item| catalog_inline_biblio_anchor item.instance_variable_get(:@text), item, document }
|
631
|
+
end
|
632
|
+
break
|
718
633
|
|
719
|
-
|
634
|
+
elsif (match = OrderedListRx.match(this_line))
|
635
|
+
reader.unshift_line this_line
|
636
|
+
block = next_item_list(reader, :olist, parent)
|
637
|
+
# FIXME move this logic into next_item_list
|
638
|
+
unless style
|
639
|
+
marker = block.items[0].marker
|
640
|
+
if marker.start_with? '.'
|
641
|
+
# first one makes more sense, but second one is AsciiDoc-compliant
|
642
|
+
# TODO control behavior using a compliance setting
|
643
|
+
#attributes['style'] = (ORDERED_LIST_STYLES[block.level - 1] || 'arabic').to_s
|
644
|
+
attributes['style'] = (ORDERED_LIST_STYLES[marker.length - 1] || 'arabic').to_s
|
720
645
|
else
|
721
|
-
|
722
|
-
|
723
|
-
|
724
|
-
|
725
|
-
# line comments, which may leave us w/ an empty buffer if those
|
726
|
-
# were the only lines found
|
727
|
-
if lines.empty?
|
728
|
-
# call advance since the reader preserved the last line
|
729
|
-
reader.advance
|
730
|
-
return
|
731
|
-
end
|
732
|
-
|
733
|
-
catalog_inline_anchors(lines.join(EOL), document)
|
734
|
-
|
735
|
-
first_line = lines[0]
|
736
|
-
if !text_only && (admonition_match = AdmonitionParagraphRx.match(first_line))
|
737
|
-
lines[0] = admonition_match.post_match.lstrip
|
738
|
-
attributes['style'] = admonition_match[1]
|
739
|
-
attributes['name'] = admonition_name = admonition_match[1].downcase
|
740
|
-
attributes['caption'] ||= document.attributes[%(#{admonition_name}-caption)]
|
741
|
-
block = Block.new(parent, :admonition, :content_model => :simple, :source => lines, :attributes => attributes)
|
742
|
-
elsif !text_only && Compliance.markdown_syntax && first_line.start_with?('> ')
|
743
|
-
lines.map! {|line|
|
744
|
-
if line == '>'
|
745
|
-
line[1..-1]
|
746
|
-
elsif line.start_with? '> '
|
747
|
-
line[2..-1]
|
748
|
-
else
|
749
|
-
line
|
750
|
-
end
|
751
|
-
}
|
646
|
+
attributes['style'] = (ORDERED_LIST_STYLES.find {|s| OrderedListMarkerRxMap[s].match? marker } || 'arabic').to_s
|
647
|
+
end
|
648
|
+
end
|
649
|
+
break
|
752
650
|
|
753
|
-
|
754
|
-
|
755
|
-
|
756
|
-
|
757
|
-
attribution, citetitle = nil
|
758
|
-
end
|
759
|
-
attributes['style'] = 'quote'
|
760
|
-
attributes['attribution'] = attribution if attribution
|
761
|
-
attributes['citetitle'] = citetitle if citetitle
|
762
|
-
# NOTE will only detect headings that are floating titles (not section titles)
|
763
|
-
# TODO could assume a floating title when inside a block context
|
764
|
-
# FIXME Reader needs to be created w/ line info
|
765
|
-
block = build_block(:quote, :compound, false, parent, Reader.new(lines), attributes)
|
766
|
-
elsif !text_only && (blockquote? lines, first_line)
|
767
|
-
lines[0] = first_line[1..-1]
|
768
|
-
attribution, citetitle = lines.pop[3..-1].split(', ', 2)
|
769
|
-
lines.pop while lines[-1].empty?
|
770
|
-
# strip trailing quote
|
771
|
-
lines[-1] = lines[-1].chop
|
772
|
-
attributes['style'] = 'quote'
|
773
|
-
attributes['attribution'] = attribution if attribution
|
774
|
-
attributes['citetitle'] = citetitle if citetitle
|
775
|
-
block = Block.new(parent, :quote, :content_model => :simple, :source => lines, :attributes => attributes)
|
776
|
-
else
|
777
|
-
# if [normal] is used over an indented paragraph, shift content to left margin
|
778
|
-
if style == 'normal'
|
779
|
-
# QUESTION do we even need to shift since whitespace is normalized by XML in this case?
|
780
|
-
adjust_indentation! lines
|
781
|
-
end
|
651
|
+
elsif (match = DescriptionListRx.match(this_line))
|
652
|
+
reader.unshift_line this_line
|
653
|
+
block = next_description_list(reader, match, parent)
|
654
|
+
break
|
782
655
|
|
783
|
-
|
784
|
-
|
785
|
-
|
656
|
+
elsif (style == 'float' || style == 'discrete') && (Compliance.underline_style_section_titles ?
|
657
|
+
(is_section_title? this_line, (reader.peek_line true)) : !indented && (is_section_title? this_line))
|
658
|
+
reader.unshift_line this_line
|
659
|
+
float_id, float_reftext, float_title, float_level, _ = parse_section_title(reader, document)
|
660
|
+
attributes['reftext'] = float_reftext if float_reftext
|
661
|
+
block = Block.new(parent, :floating_title, :content_model => :empty)
|
662
|
+
block.title = float_title
|
663
|
+
attributes.delete 'title'
|
664
|
+
block.id = float_id || attributes['id'] ||
|
665
|
+
((document.attributes.key? 'sectids') ? (Section.generate_id block.title, document) : nil)
|
666
|
+
block.level = float_level
|
667
|
+
break
|
786
668
|
|
787
|
-
|
669
|
+
# FIXME create another set for "passthrough" styles
|
670
|
+
# FIXME make this more DRY!
|
671
|
+
elsif style && style != 'normal'
|
672
|
+
if PARAGRAPH_STYLES.include?(style)
|
673
|
+
block_context = style.to_sym
|
674
|
+
cloaked_context = :paragraph
|
675
|
+
reader.unshift_line this_line
|
676
|
+
# advance to block parsing =>
|
677
|
+
break
|
678
|
+
elsif ADMONITION_STYLES.include?(style)
|
679
|
+
block_context = :admonition
|
680
|
+
cloaked_context = :paragraph
|
681
|
+
reader.unshift_line this_line
|
682
|
+
# advance to block parsing =>
|
683
|
+
break
|
684
|
+
elsif block_extensions && extensions.registered_for_block?(style, :paragraph)
|
685
|
+
block_context = style.to_sym
|
686
|
+
cloaked_context = :paragraph
|
687
|
+
reader.unshift_line this_line
|
688
|
+
# advance to block parsing =>
|
788
689
|
break
|
690
|
+
else
|
691
|
+
warn %(asciidoctor: WARNING: #{this_path}: line #{this_lineno}: invalid style for paragraph: #{style})
|
692
|
+
style = nil
|
693
|
+
# continue to process paragraph
|
789
694
|
end
|
790
695
|
end
|
791
696
|
|
792
|
-
|
793
|
-
|
794
|
-
|
795
|
-
|
796
|
-
|
697
|
+
break_at_list = (skipped == 0 && in_list)
|
698
|
+
reader.unshift_line this_line
|
699
|
+
|
700
|
+
# a literal paragraph: contiguous lines starting with at least one whitespace character
|
701
|
+
# NOTE style can only be nil or "normal" at this point
|
702
|
+
if indented && !style
|
703
|
+
lines = read_paragraph_lines reader, break_at_list, :skip_line_comments => text_only
|
797
704
|
|
798
|
-
|
799
|
-
when :admonition
|
800
|
-
attributes['name'] = admonition_name = style.downcase
|
801
|
-
attributes['caption'] ||= document.attributes[%(#{admonition_name}-caption)]
|
802
|
-
block = build_block(block_context, :compound, terminator, parent, reader, attributes)
|
705
|
+
adjust_indentation! lines
|
803
706
|
|
804
|
-
|
805
|
-
|
707
|
+
block = Block.new(parent, :literal, :content_model => :verbatim, :source => lines, :attributes => attributes)
|
708
|
+
# a literal gets special meaning inside of a description list
|
709
|
+
# TODO this feels hacky, better way to distinguish from explicit literal block?
|
710
|
+
block.set_option('listparagraph') if in_list
|
711
|
+
|
712
|
+
# a normal paragraph: contiguous non-blank/non-continuation lines (left-indented or normal style)
|
713
|
+
else
|
714
|
+
lines = read_paragraph_lines reader, break_at_list, :skip_line_comments => true
|
715
|
+
|
716
|
+
# NOTE we need this logic because we've asked the reader to skip
|
717
|
+
# line comments, which may leave us w/ an empty buffer if those
|
718
|
+
# were the only lines found
|
719
|
+
if in_list && lines.empty?
|
720
|
+
# call advance since the reader preserved the last line
|
721
|
+
reader.advance
|
806
722
|
return
|
723
|
+
end
|
807
724
|
|
808
|
-
|
809
|
-
|
810
|
-
|
811
|
-
|
812
|
-
|
813
|
-
|
814
|
-
|
815
|
-
|
816
|
-
|
817
|
-
|
818
|
-
|
819
|
-
|
820
|
-
|
821
|
-
|
822
|
-
|
823
|
-
|
824
|
-
|
825
|
-
|
826
|
-
|
827
|
-
unless attributes.key? 'language'
|
828
|
-
if (default_language = document.attributes['source-language'])
|
829
|
-
attributes['language'] = default_language
|
830
|
-
end
|
831
|
-
end
|
832
|
-
if !attributes.key?('indent') && document.attributes.key?('source-indent')
|
833
|
-
attributes['indent'] = document.attributes['source-indent']
|
834
|
-
end
|
725
|
+
# NOTE don't check indented here since it's extremely rare
|
726
|
+
#if text_only || indented
|
727
|
+
if text_only
|
728
|
+
# if [normal] is used over an indented paragraph, shift content to left margin
|
729
|
+
# QUESTION do we even need to shift since whitespace is normalized by XML in this case?
|
730
|
+
adjust_indentation! lines if indented && style == 'normal'
|
731
|
+
block = Block.new(parent, :paragraph, :content_model => :simple, :source => lines, :attributes => attributes)
|
732
|
+
elsif (ADMONITION_STYLE_LEADERS.include? ch0) && (this_line.include? ':') && (AdmonitionParagraphRx =~ this_line)
|
733
|
+
lines[0] = $' # string after match
|
734
|
+
attributes['name'] = admonition_name = (attributes['style'] = $1).downcase
|
735
|
+
attributes['textlabel'] = (attributes.delete 'caption') || document.attributes[%(#{admonition_name}-caption)]
|
736
|
+
block = Block.new(parent, :admonition, :content_model => :simple, :source => lines, :attributes => attributes)
|
737
|
+
elsif md_syntax && ch0 == '>' && this_line.start_with?('> ')
|
738
|
+
lines.map! {|line| line == '>' ? line[1..-1] : ((line.start_with? '> ') ? line[2..-1] : line) }
|
739
|
+
if lines[-1].start_with? '-- '
|
740
|
+
attribution, citetitle = lines.pop[3..-1].split(', ', 2)
|
741
|
+
attributes['attribution'] = attribution if attribution
|
742
|
+
attributes['citetitle'] = citetitle if citetitle
|
743
|
+
lines.pop while lines[-1].empty?
|
835
744
|
end
|
836
|
-
|
745
|
+
attributes['style'] = 'quote'
|
746
|
+
# NOTE will only detect headings that are floating titles (not section titles)
|
747
|
+
# TODO could assume a floating title when inside a block context
|
748
|
+
# FIXME Reader needs to be created w/ line info
|
749
|
+
block = build_block(:quote, :compound, false, parent, Reader.new(lines), attributes)
|
750
|
+
elsif ch0 == '"' && lines.size > 1 && (lines[-1].start_with? '-- ') && (lines[-2].end_with? '"')
|
751
|
+
lines[0] = this_line[1..-1] # strip leading quote
|
752
|
+
attribution, citetitle = lines.pop[3..-1].split(', ', 2)
|
753
|
+
attributes['attribution'] = attribution if attribution
|
754
|
+
attributes['citetitle'] = citetitle if citetitle
|
755
|
+
lines.pop while lines[-1].empty?
|
756
|
+
lines[-1] = lines[-1].chop # strip trailing quote
|
757
|
+
attributes['style'] = 'quote'
|
758
|
+
block = Block.new(parent, :quote, :content_model => :simple, :source => lines, :attributes => attributes)
|
759
|
+
else
|
760
|
+
# if [normal] is used over an indented paragraph, shift content to left margin
|
761
|
+
# QUESTION do we even need to shift since whitespace is normalized by XML in this case?
|
762
|
+
adjust_indentation! lines if indented && style == 'normal'
|
763
|
+
block = Block.new(parent, :paragraph, :content_model => :simple, :source => lines, :attributes => attributes)
|
764
|
+
end
|
837
765
|
|
838
|
-
|
839
|
-
|
766
|
+
catalog_inline_anchors lines * LF, block, document
|
767
|
+
end
|
840
768
|
|
841
|
-
|
842
|
-
|
769
|
+
break # forbid loop from executing more than once
|
770
|
+
end unless delimited_block
|
843
771
|
|
844
|
-
|
845
|
-
|
846
|
-
|
847
|
-
|
848
|
-
|
849
|
-
|
850
|
-
|
851
|
-
|
852
|
-
|
772
|
+
# either delimited block or styled paragraph
|
773
|
+
unless block
|
774
|
+
# abstract and partintro should be handled by open block
|
775
|
+
# FIXME kind of hackish...need to sort out how to generalize this
|
776
|
+
block_context = :open if block_context == :abstract || block_context == :partintro
|
777
|
+
|
778
|
+
case block_context
|
779
|
+
when :admonition
|
780
|
+
attributes['name'] = admonition_name = style.downcase
|
781
|
+
attributes['textlabel'] = (attributes.delete 'caption') || document.attributes[%(#{admonition_name}-caption)]
|
782
|
+
block = build_block(block_context, :compound, terminator, parent, reader, attributes)
|
783
|
+
|
784
|
+
when :comment
|
785
|
+
build_block(block_context, :skip, terminator, parent, reader, attributes)
|
786
|
+
return
|
787
|
+
|
788
|
+
when :example
|
789
|
+
block = build_block(block_context, :compound, terminator, parent, reader, attributes)
|
790
|
+
|
791
|
+
when :listing, :literal
|
792
|
+
block = build_block(block_context, :verbatim, terminator, parent, reader, attributes)
|
793
|
+
|
794
|
+
when :source
|
795
|
+
AttributeList.rekey attributes, [nil, 'language', 'linenums']
|
796
|
+
if document.attributes.key? 'source-language'
|
797
|
+
attributes['language'] = document.attributes['source-language'] || 'text'
|
798
|
+
end unless attributes.key? 'language'
|
799
|
+
if (attributes.key? 'linenums-option') || (document.attributes.key? 'source-linenums-option')
|
800
|
+
attributes['linenums'] = ''
|
801
|
+
end unless attributes.key? 'linenums'
|
802
|
+
if document.attributes.key? 'source-indent'
|
803
|
+
attributes['indent'] = document.attributes['source-indent']
|
804
|
+
end unless attributes.key? 'indent'
|
805
|
+
block = build_block(:listing, :verbatim, terminator, parent, reader, attributes)
|
806
|
+
|
807
|
+
when :fenced_code
|
808
|
+
attributes['style'] = 'source'
|
809
|
+
if (ll = this_line.length) == 3
|
810
|
+
language = nil
|
811
|
+
elsif (comma_idx = (language = this_line.slice 3, ll).index ',')
|
812
|
+
if comma_idx > 0
|
813
|
+
language = (language.slice 0, comma_idx).strip
|
814
|
+
attributes['linenums'] = '' if comma_idx < ll - 4
|
815
|
+
else
|
816
|
+
language = nil
|
817
|
+
attributes['linenums'] = '' if ll > 4
|
853
818
|
end
|
854
|
-
|
855
|
-
|
856
|
-
|
857
|
-
|
858
|
-
|
859
|
-
|
860
|
-
cursor = reader.cursor
|
861
|
-
block_reader = Reader.new reader.read_lines_until(:terminator => terminator, :skip_line_comments => true), cursor
|
862
|
-
case terminator.chr
|
863
|
-
when ','
|
864
|
-
attributes['format'] = 'csv'
|
865
|
-
when ':'
|
866
|
-
attributes['format'] = 'dsv'
|
819
|
+
else
|
820
|
+
language = language.lstrip
|
821
|
+
end
|
822
|
+
if language.nil_or_empty?
|
823
|
+
if document.attributes.key? 'source-language'
|
824
|
+
attributes['language'] = document.attributes['source-language'] || 'text'
|
867
825
|
end
|
868
|
-
|
826
|
+
else
|
827
|
+
attributes['language'] = language
|
828
|
+
end
|
829
|
+
if (attributes.key? 'linenums-option') || (document.attributes.key? 'source-linenums-option')
|
830
|
+
attributes['linenums'] = ''
|
831
|
+
end unless attributes.key? 'linenums'
|
832
|
+
if document.attributes.key? 'source-indent'
|
833
|
+
attributes['indent'] = document.attributes['source-indent']
|
834
|
+
end unless attributes.key? 'indent'
|
835
|
+
terminator = terminator.slice 0, 3
|
836
|
+
block = build_block(:listing, :verbatim, terminator, parent, reader, attributes)
|
837
|
+
|
838
|
+
when :pass
|
839
|
+
block = build_block(block_context, :raw, terminator, parent, reader, attributes)
|
840
|
+
|
841
|
+
when :stem, :latexmath, :asciimath
|
842
|
+
if block_context == :stem
|
843
|
+
attributes['style'] = if (explicit_stem_syntax = attributes[2])
|
844
|
+
explicit_stem_syntax.include?('tex') ? 'latexmath' : 'asciimath'
|
845
|
+
elsif (default_stem_syntax = document.attributes['stem']).nil_or_empty?
|
846
|
+
'asciimath'
|
847
|
+
else
|
848
|
+
default_stem_syntax
|
849
|
+
end
|
850
|
+
end
|
851
|
+
block = build_block(:stem, :raw, terminator, parent, reader, attributes)
|
869
852
|
|
870
|
-
|
871
|
-
|
872
|
-
block = build_block(block_context, (block_context == :verse ? :verbatim : :compound), terminator, parent, reader, attributes)
|
853
|
+
when :open, :sidebar
|
854
|
+
block = build_block(block_context, :compound, terminator, parent, reader, attributes)
|
873
855
|
|
874
|
-
|
875
|
-
|
876
|
-
|
877
|
-
|
878
|
-
|
879
|
-
|
880
|
-
|
881
|
-
|
882
|
-
|
883
|
-
|
856
|
+
when :table
|
857
|
+
block_reader = Reader.new reader.read_lines_until(:terminator => terminator, :skip_line_comments => true), reader.cursor
|
858
|
+
# NOTE it's very rare that format is set when using a format hint char, so short-circuit
|
859
|
+
unless terminator.start_with? '|', '!'
|
860
|
+
# NOTE infer dsv once all other format hint chars are ruled out
|
861
|
+
attributes['format'] ||= (terminator.start_with? ',') ? 'csv' : 'dsv'
|
862
|
+
end
|
863
|
+
block = next_table(block_reader, parent, attributes)
|
864
|
+
|
865
|
+
when :quote, :verse
|
866
|
+
AttributeList.rekey(attributes, [nil, 'attribution', 'citetitle'])
|
867
|
+
block = build_block(block_context, (block_context == :verse ? :verbatim : :compound), terminator, parent, reader, attributes)
|
868
|
+
|
869
|
+
else
|
870
|
+
if block_extensions && (extension = extensions.registered_for_block?(block_context, cloaked_context))
|
871
|
+
if (content_model = extension.config[:content_model]) != :skip
|
872
|
+
if !(pos_attrs = extension.config[:pos_attrs] || []).empty?
|
873
|
+
AttributeList.rekey(attributes, [nil].concat(pos_attrs))
|
884
874
|
end
|
885
|
-
|
886
|
-
|
887
|
-
attributes.clear
|
888
|
-
return
|
875
|
+
if (default_attrs = extension.config[:default_attrs])
|
876
|
+
default_attrs.each {|k, v| attributes[k] ||= v }
|
889
877
|
end
|
890
|
-
|
891
|
-
|
892
|
-
raise %(Unsupported block type #{block_context} at #{reader.line_info})
|
878
|
+
# QUESTION should we clone the extension for each cloaked context and set in config?
|
879
|
+
attributes['cloaked-context'] = cloaked_context
|
893
880
|
end
|
881
|
+
block = build_block block_context, content_model, terminator, parent, reader, attributes, :extension => extension
|
882
|
+
unless block && content_model != :skip
|
883
|
+
attributes.clear
|
884
|
+
return
|
885
|
+
end
|
886
|
+
else
|
887
|
+
# this should only happen if there's a misconfiguration
|
888
|
+
raise %(Unsupported block type #{block_context} at #{reader.line_info})
|
894
889
|
end
|
895
890
|
end
|
896
891
|
end
|
897
892
|
|
898
|
-
# when looking for nested content, one or more line comments, comment
|
899
|
-
# blocks or trailing attribute lists could leave us without a block,
|
900
|
-
# so handle accordingly
|
901
|
-
# REVIEW we may no longer need this nil check
|
902
893
|
# FIXME we've got to clean this up, it's horrible!
|
903
|
-
if
|
904
|
-
|
905
|
-
|
906
|
-
|
907
|
-
|
908
|
-
|
909
|
-
|
910
|
-
|
911
|
-
|
912
|
-
|
913
|
-
|
914
|
-
|
915
|
-
|
916
|
-
|
917
|
-
|
918
|
-
|
919
|
-
|
920
|
-
|
921
|
-
|
922
|
-
|
923
|
-
|
924
|
-
|
925
|
-
|
926
|
-
|
927
|
-
|
928
|
-
if
|
929
|
-
|
930
|
-
document.register(:ids, [block_id, (attributes['reftext'] || (block.title? ? block.title : nil))])
|
931
|
-
end
|
932
|
-
# FIXME remove the need for this update!
|
933
|
-
block.attributes.update(attributes) unless attributes.empty?
|
934
|
-
block.lock_in_subs
|
935
|
-
|
936
|
-
#if document.attributes.has_key? :pending_attribute_entries
|
937
|
-
# document.attributes.delete(:pending_attribute_entries).each do |entry|
|
938
|
-
# entry.save_to block.attributes
|
939
|
-
# end
|
940
|
-
#end
|
941
|
-
|
942
|
-
if block.sub? :callouts
|
943
|
-
unless (catalog_callouts block.source, document)
|
944
|
-
# No need to sub callouts if they aren't there
|
945
|
-
block.remove_sub :callouts
|
946
|
-
end
|
947
|
-
end
|
894
|
+
block.source_location = source_location if source_location
|
895
|
+
# FIXME title should be assigned when block is constructed
|
896
|
+
block.title = attributes.delete 'title' if attributes.key? 'title'
|
897
|
+
#unless attributes.key? 'reftext'
|
898
|
+
# attributes['reftext'] = document.attributes['reftext'] if document.attributes.key? 'reftext'
|
899
|
+
#end
|
900
|
+
# TODO eventually remove the style attribute from the attributes hash
|
901
|
+
#block.style = attributes.delete 'style'
|
902
|
+
block.style = attributes['style']
|
903
|
+
if (block_id = (block.id ||= attributes['id']))
|
904
|
+
unless document.register :refs, [block_id, block, attributes['reftext'] || (block.title? ? block.title : nil)]
|
905
|
+
warn %(asciidoctor: WARNING: #{this_path}: line #{this_lineno}: id assigned to block already in use: #{block_id})
|
906
|
+
end
|
907
|
+
end
|
908
|
+
# FIXME remove the need for this update!
|
909
|
+
block.attributes.update(attributes) unless attributes.empty?
|
910
|
+
block.lock_in_subs
|
911
|
+
|
912
|
+
#if document.attributes.key? :pending_attribute_entries
|
913
|
+
# document.attributes.delete(:pending_attribute_entries).each do |entry|
|
914
|
+
# entry.save_to block.attributes
|
915
|
+
# end
|
916
|
+
#end
|
917
|
+
|
918
|
+
if block.sub? :callouts
|
919
|
+
# No need to sub callouts if none are found when cataloging
|
920
|
+
block.remove_sub :callouts unless catalog_callouts block.source, document
|
948
921
|
end
|
949
922
|
|
950
923
|
block
|
951
924
|
end
|
952
925
|
|
953
|
-
def self.blockquote? lines, first_line = nil
|
954
|
-
lines.size > 1 && ((first_line || lines[0]).start_with? '"') &&
|
955
|
-
(lines[-1].start_with? '-- ') && (lines[-2].end_with? '"')
|
956
|
-
end
|
957
|
-
|
958
926
|
def self.read_paragraph_lines reader, break_at_list, opts = {}
|
959
927
|
opts[:break_on_blank_lines] = true
|
960
928
|
opts[:break_on_list_continuation] = true
|
@@ -970,7 +938,7 @@ class Parser
|
|
970
938
|
# returns the match data if this line is the first line of a delimited block or nil if not
|
971
939
|
def self.is_delimited_block? line, return_match_data = false
|
972
940
|
# highly optimized for best performance
|
973
|
-
return unless (line_len = line.length) > 1 &&
|
941
|
+
return unless (line_len = line.length) > 1 && DELIMITED_BLOCK_LEADERS.include?(line.slice 0, 2)
|
974
942
|
# catches open block
|
975
943
|
if line_len == 2
|
976
944
|
tip = line
|
@@ -981,7 +949,7 @@ class Parser
|
|
981
949
|
tip = line
|
982
950
|
tl = line_len
|
983
951
|
else
|
984
|
-
tip = line
|
952
|
+
tip = line.slice 0, 4
|
985
953
|
tl = 4
|
986
954
|
end
|
987
955
|
|
@@ -1004,18 +972,18 @@ class Parser
|
|
1004
972
|
return if tl == 3 && !fenced_code
|
1005
973
|
end
|
1006
974
|
|
1007
|
-
if DELIMITED_BLOCKS.
|
975
|
+
if DELIMITED_BLOCKS.key? tip
|
1008
976
|
# tip is the full line when delimiter is minimum length
|
1009
977
|
if tl < 4 || tl == line_len
|
1010
978
|
if return_match_data
|
1011
|
-
context, masq =
|
979
|
+
context, masq = DELIMITED_BLOCKS[tip]
|
1012
980
|
BlockMatchData.new(context, masq, tip, tip)
|
1013
981
|
else
|
1014
982
|
true
|
1015
983
|
end
|
1016
984
|
elsif %(#{tip}#{tip[-1..-1] * (line_len - tl)}) == line
|
1017
985
|
if return_match_data
|
1018
|
-
context, masq =
|
986
|
+
context, masq = DELIMITED_BLOCKS[tip]
|
1019
987
|
BlockMatchData.new(context, masq, tip, line)
|
1020
988
|
else
|
1021
989
|
true
|
@@ -1023,7 +991,7 @@ class Parser
|
|
1023
991
|
# only enable if/when we decide to support non-congruent block delimiters
|
1024
992
|
#elsif (match = BlockDelimiterRx.match(line))
|
1025
993
|
# if return_match_data
|
1026
|
-
# context, masq =
|
994
|
+
# context, masq = DELIMITED_BLOCKS[tip]
|
1027
995
|
# BlockMatchData.new(context, masq, tip, match[0])
|
1028
996
|
# else
|
1029
997
|
# true
|
@@ -1040,8 +1008,11 @@ class Parser
|
|
1040
1008
|
# if terminator is false, that means the all the lines in the reader should be parsed
|
1041
1009
|
# NOTE could invoke filter in here, before and after parsing
|
1042
1010
|
def self.build_block(block_context, content_model, terminator, parent, reader, attributes, options = {})
|
1043
|
-
if content_model == :skip
|
1044
|
-
skip_processing =
|
1011
|
+
if content_model == :skip
|
1012
|
+
skip_processing = true
|
1013
|
+
parse_as_content_model = :simple
|
1014
|
+
elsif content_model == :raw
|
1015
|
+
skip_processing = false
|
1045
1016
|
parse_as_content_model = :simple
|
1046
1017
|
else
|
1047
1018
|
skip_processing = false
|
@@ -1050,15 +1021,16 @@ class Parser
|
|
1050
1021
|
|
1051
1022
|
if terminator.nil?
|
1052
1023
|
if parse_as_content_model == :verbatim
|
1053
|
-
lines = reader.read_lines_until
|
1024
|
+
lines = reader.read_lines_until :break_on_blank_lines => true, :break_on_list_continuation => true
|
1054
1025
|
else
|
1055
1026
|
content_model = :simple if content_model == :compound
|
1056
|
-
|
1027
|
+
# TODO we could also skip processing if we're able to detect reader is a BlockReader
|
1028
|
+
lines = read_paragraph_lines reader, false, :skip_line_comments => true, :skip_processing => skip_processing
|
1057
1029
|
# QUESTION check for empty lines after grabbing lines for simple content model?
|
1058
1030
|
end
|
1059
1031
|
block_reader = nil
|
1060
1032
|
elsif parse_as_content_model != :compound
|
1061
|
-
lines = reader.read_lines_until
|
1033
|
+
lines = reader.read_lines_until :terminator => terminator, :skip_processing => skip_processing
|
1062
1034
|
block_reader = nil
|
1063
1035
|
# terminator is false when reader has already been prepared
|
1064
1036
|
elsif terminator == false
|
@@ -1066,8 +1038,7 @@ class Parser
|
|
1066
1038
|
block_reader = reader
|
1067
1039
|
else
|
1068
1040
|
lines = nil
|
1069
|
-
|
1070
|
-
block_reader = Reader.new reader.read_lines_until(:terminator => terminator, :skip_processing => skip_processing), cursor
|
1041
|
+
block_reader = Reader.new reader.read_lines_until(:terminator => terminator, :skip_processing => skip_processing), reader.cursor
|
1071
1042
|
end
|
1072
1043
|
|
1073
1044
|
if content_model == :skip
|
@@ -1105,17 +1076,16 @@ class Parser
|
|
1105
1076
|
end
|
1106
1077
|
|
1107
1078
|
# QUESTION should we have an explicit map or can we rely on check for *-caption attribute?
|
1108
|
-
if (attributes.
|
1079
|
+
if (attributes.key? 'title') && block.context != :admonition &&
|
1080
|
+
(parent.document.attributes.key? %(#{block.context}-caption))
|
1109
1081
|
block.title = attributes.delete 'title'
|
1110
|
-
block.assign_caption
|
1082
|
+
block.assign_caption(attributes.delete 'caption')
|
1111
1083
|
end
|
1112
1084
|
|
1113
|
-
|
1114
|
-
|
1115
|
-
|
1116
|
-
|
1117
|
-
parse_blocks block_reader, block
|
1118
|
-
end
|
1085
|
+
# reader is confined within boundaries of a delimited block, so look for
|
1086
|
+
# blocks until there are no more lines
|
1087
|
+
parse_blocks block_reader, block if content_model == :compound
|
1088
|
+
|
1119
1089
|
block
|
1120
1090
|
end
|
1121
1091
|
|
@@ -1130,20 +1100,19 @@ class Parser
|
|
1130
1100
|
#
|
1131
1101
|
# Returns nothing.
|
1132
1102
|
def self.parse_blocks(reader, parent)
|
1133
|
-
while reader
|
1134
|
-
|
1135
|
-
parent << block if block
|
1103
|
+
while (block = next_block reader, parent)
|
1104
|
+
parent << block
|
1136
1105
|
end
|
1137
1106
|
end
|
1138
1107
|
|
1139
|
-
# Internal: Parse and construct an
|
1108
|
+
# Internal: Parse and construct an item list (ordered or unordered) from the current position of the Reader
|
1140
1109
|
#
|
1141
1110
|
# reader - The Reader from which to retrieve the outline list
|
1142
1111
|
# list_type - A Symbol representing the list type (:olist for ordered, :ulist for unordered)
|
1143
1112
|
# parent - The parent Block to which this outline list belongs
|
1144
1113
|
#
|
1145
1114
|
# Returns the Block encapsulating the parsed outline (unordered or ordered) list
|
1146
|
-
def self.
|
1115
|
+
def self.next_item_list(reader, list_type, parent)
|
1147
1116
|
list_block = List.new(parent, list_type)
|
1148
1117
|
if parent.context == list_type
|
1149
1118
|
list_block.level = parent.level + 1
|
@@ -1195,61 +1164,76 @@ class Parser
|
|
1195
1164
|
# Internal: Catalog any callouts found in the text, but don't process them
|
1196
1165
|
#
|
1197
1166
|
# text - The String of text in which to look for callouts
|
1198
|
-
# document - The current document
|
1167
|
+
# document - The current document in which the callouts are stored
|
1199
1168
|
#
|
1200
1169
|
# Returns A Boolean indicating whether callouts were found
|
1201
1170
|
def self.catalog_callouts(text, document)
|
1202
1171
|
found = false
|
1203
|
-
|
1204
|
-
|
1205
|
-
|
1206
|
-
|
1207
|
-
|
1208
|
-
|
1209
|
-
|
1210
|
-
# we have to mark as found even if it's escaped so it can be unescaped
|
1211
|
-
found = true
|
1212
|
-
}
|
1213
|
-
end
|
1172
|
+
text.scan(CalloutScanRx) {
|
1173
|
+
# lead with assignments for Ruby 1.8.7 compat
|
1174
|
+
captured, num = $&, $2
|
1175
|
+
document.callouts.register num unless captured.start_with? '\\'
|
1176
|
+
# we have to mark as found even if it's escaped so it can be unescaped
|
1177
|
+
found = true
|
1178
|
+
} if text.include? '<'
|
1214
1179
|
found
|
1215
1180
|
end
|
1216
1181
|
|
1217
|
-
# Internal: Catalog any inline anchors found in the text
|
1182
|
+
# Internal: Catalog any inline anchors found in the text (but don't convert)
|
1218
1183
|
#
|
1219
1184
|
# text - The String text in which to look for inline anchors
|
1220
|
-
#
|
1185
|
+
# block - The block in which the references should be searched
|
1186
|
+
# document - The current Document on which the references are stored
|
1221
1187
|
#
|
1222
1188
|
# Returns nothing
|
1223
|
-
def self.catalog_inline_anchors
|
1224
|
-
|
1225
|
-
|
1226
|
-
|
1227
|
-
|
1228
|
-
|
1229
|
-
|
1230
|
-
|
1231
|
-
|
1232
|
-
|
1233
|
-
|
1234
|
-
|
1235
|
-
|
1236
|
-
|
1237
|
-
|
1189
|
+
def self.catalog_inline_anchors text, block, document
|
1190
|
+
text.scan(InlineAnchorScanRx) do
|
1191
|
+
if (id = $1)
|
1192
|
+
if (reftext = $2)
|
1193
|
+
next if (reftext.include? '{') && (reftext = document.sub_attributes reftext).empty?
|
1194
|
+
end
|
1195
|
+
else
|
1196
|
+
id = $3
|
1197
|
+
if (reftext = $4)
|
1198
|
+
reftext = reftext.gsub '\]', ']' if reftext.include? ']'
|
1199
|
+
next if (reftext.include? '{') && (reftext = document.sub_attributes reftext).empty?
|
1200
|
+
end
|
1201
|
+
end
|
1202
|
+
unless document.register :refs, [id, (Inline.new block, :anchor, reftext, :type => :ref, :id => id), reftext]
|
1203
|
+
warn %(asciidoctor: WARNING: #{document.reader.path}: id assigned to anchor already in use: #{id})
|
1204
|
+
end
|
1205
|
+
end if (text.include? '[[') || (text.include? 'or:')
|
1206
|
+
nil
|
1207
|
+
end
|
1208
|
+
|
1209
|
+
# Internal: Catalog the bibliography inline anchor found in the start of the list item (but don't convert)
|
1210
|
+
#
|
1211
|
+
# text - The String text in which to look for an inline bibliography anchor
|
1212
|
+
# block - The ListItem block in which the reference should be searched
|
1213
|
+
# document - The current document in which the reference is stored
|
1214
|
+
#
|
1215
|
+
# Returns nothing
|
1216
|
+
def self.catalog_inline_biblio_anchor text, block, document
|
1217
|
+
if InlineBiblioAnchorRx =~ text
|
1218
|
+
# QUESTION should we sub attributes in reftext (like with regular anchors)?
|
1219
|
+
unless document.register :refs, [(id = $1), (Inline.new block, :anchor, (reftext = %([#{$2 || id}])), :type => :bibref, :id => id), reftext]
|
1220
|
+
warn %(asciidoctor: WARNING: #{document.reader.path}: id assigned to bibliography anchor already in use: #{id})
|
1221
|
+
end
|
1238
1222
|
end
|
1239
1223
|
nil
|
1240
1224
|
end
|
1241
1225
|
|
1242
1226
|
# Internal: Parse and construct a description list Block from the current position of the Reader
|
1243
1227
|
#
|
1244
|
-
# reader - The Reader from which to retrieve the
|
1228
|
+
# reader - The Reader from which to retrieve the description list
|
1245
1229
|
# match - The Regexp match for the head of the list
|
1246
|
-
# parent - The parent Block to which this
|
1230
|
+
# parent - The parent Block to which this description list belongs
|
1247
1231
|
#
|
1248
|
-
# Returns the Block encapsulating the parsed
|
1249
|
-
def self.
|
1232
|
+
# Returns the Block encapsulating the parsed description list
|
1233
|
+
def self.next_description_list(reader, match, parent)
|
1250
1234
|
list_block = List.new(parent, :dlist)
|
1251
1235
|
previous_pair = nil
|
1252
|
-
# allows us to capture until we find a
|
1236
|
+
# allows us to capture until we find a description item
|
1253
1237
|
# that uses the same delimiter (::, :::, :::: or ;;)
|
1254
1238
|
sibling_pattern = DescriptionListSiblingRx[match[2]]
|
1255
1239
|
|
@@ -1272,12 +1256,12 @@ class Parser
|
|
1272
1256
|
|
1273
1257
|
# Internal: Parse and construct the next ListItem for the current bulleted
|
1274
1258
|
# (unordered or ordered) list Block, callout lists included, or the next
|
1275
|
-
# term ListItem and description ListItem pair for the
|
1259
|
+
# term ListItem and description ListItem pair for the description list Block.
|
1276
1260
|
#
|
1277
1261
|
# First collect and process all the lines that constitute the next list
|
1278
1262
|
# item for the parent list (according to its type). Next, parse those lines
|
1279
1263
|
# into blocks and associate them with the ListItem (in the case of a
|
1280
|
-
#
|
1264
|
+
# description list, the description ListItem). Finally, fold the first block
|
1281
1265
|
# into the item's text attribute according to rules described in ListItem.
|
1282
1266
|
#
|
1283
1267
|
# reader - The Reader from which to retrieve the next list item
|
@@ -1301,7 +1285,7 @@ class Parser
|
|
1301
1285
|
checkbox = true
|
1302
1286
|
checked = false
|
1303
1287
|
text = text[3..-1].lstrip
|
1304
|
-
elsif text.start_with?('[x] '
|
1288
|
+
elsif text.start_with?('[x] ', '[*] ')
|
1305
1289
|
checkbox = true
|
1306
1290
|
checked = true
|
1307
1291
|
text = text[3..-1].lstrip
|
@@ -1323,22 +1307,21 @@ class Parser
|
|
1323
1307
|
|
1324
1308
|
# first skip the line with the marker / term
|
1325
1309
|
reader.advance
|
1326
|
-
|
1327
|
-
list_item_reader = Reader.new read_lines_for_list_item(reader, list_type, sibling_trait, has_text), cursor
|
1310
|
+
list_item_reader = Reader.new read_lines_for_list_item(reader, list_type, sibling_trait, has_text), reader.cursor
|
1328
1311
|
if list_item_reader.has_more_lines?
|
1312
|
+
# NOTE peek on the other side of any comment lines
|
1329
1313
|
comment_lines = list_item_reader.skip_line_comments
|
1330
|
-
subsequent_line = list_item_reader.peek_line
|
1331
|
-
|
1332
|
-
|
1333
|
-
|
1334
|
-
|
1335
|
-
|
1336
|
-
|
1337
|
-
|
1338
|
-
has_text = false
|
1314
|
+
if (subsequent_line = list_item_reader.peek_line)
|
1315
|
+
list_item_reader.unshift_lines comment_lines unless comment_lines.empty?
|
1316
|
+
if (continuation_connects_first_block = subsequent_line.empty?)
|
1317
|
+
content_adjacent = false
|
1318
|
+
else
|
1319
|
+
content_adjacent = true
|
1320
|
+
# treat lines as paragraph text if continuation does not connect first block (i.e., has_text = false)
|
1321
|
+
has_text = false unless list_type == :dlist
|
1339
1322
|
end
|
1340
|
-
content_adjacent = !continuation_connects_first_block && !subsequent_line.empty?
|
1341
1323
|
else
|
1324
|
+
# NOTE we have no use for any trailing comment lines we might have found
|
1342
1325
|
continuation_connects_first_block = false
|
1343
1326
|
content_adjacent = false
|
1344
1327
|
end
|
@@ -1359,10 +1342,11 @@ class Parser
|
|
1359
1342
|
end
|
1360
1343
|
|
1361
1344
|
if list_type == :dlist
|
1362
|
-
|
1363
|
-
list_item
|
1345
|
+
if list_item.text? || list_item.blocks?
|
1346
|
+
[list_term, list_item]
|
1347
|
+
else
|
1348
|
+
[list_term, nil]
|
1364
1349
|
end
|
1365
|
-
[list_term, list_item]
|
1366
1350
|
else
|
1367
1351
|
list_item
|
1368
1352
|
end
|
@@ -1379,7 +1363,7 @@ class Parser
|
|
1379
1363
|
# list_type - The Symbol context of the list (:ulist, :olist, :colist or :dlist)
|
1380
1364
|
# sibling_trait - A Regexp that matches a sibling of this list item or String list marker
|
1381
1365
|
# of the items in this list (default: nil)
|
1382
|
-
# has_text - Whether the list item has text defined inline (always true except for
|
1366
|
+
# has_text - Whether the list item has text defined inline (always true except for description lists)
|
1383
1367
|
#
|
1384
1368
|
# Returns an Array of lines belonging to the current list item.
|
1385
1369
|
def self.read_lines_for_list_item(reader, list_type, sibling_trait = nil, has_text = true)
|
@@ -1443,7 +1427,7 @@ class Parser
|
|
1443
1427
|
# technically BlockAttributeLineRx only breaks if ensuing line is not a list item
|
1444
1428
|
# which really means BlockAttributeLineRx only breaks if it's acting as a block delimiter
|
1445
1429
|
# FIXME to be AsciiDoc compliant, we shouldn't break if style in attribute line is "literal" (i.e., [literal])
|
1446
|
-
elsif list_type == :dlist && continuation != :active && BlockAttributeLineRx
|
1430
|
+
elsif list_type == :dlist && continuation != :active && (BlockAttributeLineRx.match? this_line)
|
1447
1431
|
break
|
1448
1432
|
else
|
1449
1433
|
if continuation == :active && !this_line.empty?
|
@@ -1451,7 +1435,7 @@ class Parser
|
|
1451
1435
|
# two entry points into one)
|
1452
1436
|
# if we don't process it as a whole, then a line in it that looks like a
|
1453
1437
|
# list item will throw off the exit from it
|
1454
|
-
if LiteralParagraphRx
|
1438
|
+
if LiteralParagraphRx.match? this_line
|
1455
1439
|
reader.unshift_line this_line
|
1456
1440
|
buffer.concat reader.read_lines_until(
|
1457
1441
|
:preserve_last_line => true,
|
@@ -1463,12 +1447,12 @@ class Parser
|
|
1463
1447
|
}
|
1464
1448
|
continuation = :inactive
|
1465
1449
|
# let block metadata play out until we find the block
|
1466
|
-
elsif BlockTitleRx
|
1450
|
+
elsif (BlockTitleRx.match? this_line) || (BlockAttributeLineRx.match? this_line) || (AttributeEntryRx.match? this_line)
|
1467
1451
|
buffer << this_line
|
1468
1452
|
else
|
1469
|
-
if nested_list_type = (within_nested_list ? [:dlist] : NESTABLE_LIST_CONTEXTS).find {|ctx| ListRxMap[ctx]
|
1453
|
+
if nested_list_type = (within_nested_list ? [:dlist] : NESTABLE_LIST_CONTEXTS).find {|ctx| ListRxMap[ctx].match? this_line }
|
1470
1454
|
within_nested_list = true
|
1471
|
-
if nested_list_type == :dlist &&
|
1455
|
+
if nested_list_type == :dlist && $3.nil_or_empty?
|
1472
1456
|
# get greedy again
|
1473
1457
|
has_text = false
|
1474
1458
|
end
|
@@ -1476,13 +1460,13 @@ class Parser
|
|
1476
1460
|
buffer << this_line
|
1477
1461
|
continuation = :inactive
|
1478
1462
|
end
|
1479
|
-
elsif
|
1463
|
+
elsif prev_line && prev_line.empty?
|
1480
1464
|
# advance to the next line of content
|
1481
1465
|
if this_line.empty?
|
1482
1466
|
reader.skip_blank_lines
|
1483
1467
|
this_line = reader.read_line
|
1484
|
-
# if we hit eof or a sibling
|
1485
|
-
break
|
1468
|
+
# stop reading if we hit eof or a sibling list item
|
1469
|
+
break unless this_line && !is_sibling_list_item?(this_line, list_type, sibling_trait)
|
1486
1470
|
end
|
1487
1471
|
|
1488
1472
|
if this_line == LIST_CONTINUATION
|
@@ -1499,13 +1483,13 @@ class Parser
|
|
1499
1483
|
elsif nested_list_type = NESTABLE_LIST_CONTEXTS.find {|ctx| ListRxMap[ctx] =~ this_line }
|
1500
1484
|
buffer << this_line
|
1501
1485
|
within_nested_list = true
|
1502
|
-
if nested_list_type == :dlist &&
|
1486
|
+
if nested_list_type == :dlist && $3.nil_or_empty?
|
1503
1487
|
# get greedy again
|
1504
1488
|
has_text = false
|
1505
1489
|
end
|
1506
1490
|
# slurp up any literal paragraph offset by blank lines
|
1507
1491
|
# NOTE we have to check for indented list items first
|
1508
|
-
elsif LiteralParagraphRx
|
1492
|
+
elsif LiteralParagraphRx.match? this_line
|
1509
1493
|
reader.unshift_line this_line
|
1510
1494
|
buffer.concat reader.read_lines_until(
|
1511
1495
|
:preserve_last_line => true,
|
@@ -1529,7 +1513,7 @@ class Parser
|
|
1529
1513
|
has_text = true if !this_line.empty?
|
1530
1514
|
if nested_list_type = (within_nested_list ? [:dlist] : NESTABLE_LIST_CONTEXTS).find {|ctx| ListRxMap[ctx] =~ this_line }
|
1531
1515
|
within_nested_list = true
|
1532
|
-
if nested_list_type == :dlist &&
|
1516
|
+
if nested_list_type == :dlist && $3.nil_or_empty?
|
1533
1517
|
# get greedy again
|
1534
1518
|
has_text = false
|
1535
1519
|
end
|
@@ -1553,7 +1537,7 @@ class Parser
|
|
1553
1537
|
# a blank line would have served the same purpose in the document
|
1554
1538
|
buffer.pop if !buffer.empty? && buffer[-1] == LIST_CONTINUATION
|
1555
1539
|
|
1556
|
-
#warn "BUFFER[#{list_type},#{sibling_trait}]>#{buffer *
|
1540
|
+
#warn "BUFFER[#{list_type},#{sibling_trait}]>#{buffer * LF}<BUFFER"
|
1557
1541
|
#warn "BUFFER[#{list_type},#{sibling_trait}]>#{buffer.inspect}<BUFFER"
|
1558
1542
|
|
1559
1543
|
buffer
|
@@ -1567,93 +1551,94 @@ class Parser
|
|
1567
1551
|
# reader - the source reader
|
1568
1552
|
# parent - the parent Section or Document of this Section
|
1569
1553
|
# attributes - a Hash of attributes to assign to this section (default: {})
|
1570
|
-
def self.initialize_section
|
1554
|
+
def self.initialize_section reader, parent, attributes = {}
|
1571
1555
|
document = parent.document
|
1572
1556
|
source_location = reader.cursor if document.sourcemap
|
1573
|
-
sect_id, sect_reftext, sect_title, sect_level,
|
1574
|
-
|
1575
|
-
|
1576
|
-
|
1577
|
-
|
1578
|
-
|
1579
|
-
#
|
1580
|
-
|
1581
|
-
|
1582
|
-
|
1583
|
-
|
1584
|
-
|
1585
|
-
|
1586
|
-
|
1587
|
-
if section.sectname == 'abstract' && document.doctype == 'book'
|
1588
|
-
section.sectname = 'sect1'
|
1589
|
-
section.special = false
|
1590
|
-
section.level = 1
|
1591
|
-
end
|
1557
|
+
sect_id, sect_reftext, sect_title, sect_level, single_line = parse_section_title reader, document
|
1558
|
+
if sect_reftext
|
1559
|
+
attributes['reftext'] = sect_reftext
|
1560
|
+
elsif attributes.key? 'reftext'
|
1561
|
+
sect_reftext = attributes['reftext']
|
1562
|
+
#elsif document.attributes.key? 'reftext'
|
1563
|
+
# sect_reftext = attributes['reftext'] = document.attributes['reftext']
|
1564
|
+
end
|
1565
|
+
|
1566
|
+
# parse style, id, and role attributes from first positional attribute if present
|
1567
|
+
style = attributes[1] ? (parse_style_attribute attributes, reader) : nil
|
1568
|
+
if style
|
1569
|
+
if style == 'abstract' && document.doctype == 'book'
|
1570
|
+
sect_name, sect_level = 'chapter', 1
|
1592
1571
|
else
|
1593
|
-
|
1572
|
+
sect_name, sect_special = style, true
|
1573
|
+
sect_level = 1 if sect_level == 0
|
1574
|
+
sect_numbered_force = style == 'appendix'
|
1594
1575
|
end
|
1595
|
-
elsif sect_title.downcase == 'synopsis' && document.doctype == 'manpage'
|
1596
|
-
section.special = true
|
1597
|
-
section.sectname = 'synopsis'
|
1598
1576
|
else
|
1599
|
-
|
1577
|
+
case document.doctype
|
1578
|
+
when 'book'
|
1579
|
+
sect_name = sect_level == 0 ? 'part' : (sect_level == 1 ? 'chapter' : 'section')
|
1580
|
+
when 'manpage'
|
1581
|
+
if (sect_title.casecmp 'synopsis') == 0
|
1582
|
+
sect_name, sect_special = 'synopsis', true
|
1583
|
+
else
|
1584
|
+
sect_name = 'section'
|
1585
|
+
end
|
1586
|
+
else
|
1587
|
+
sect_name = 'section'
|
1588
|
+
end
|
1600
1589
|
end
|
1601
1590
|
|
1602
|
-
|
1603
|
-
|
1604
|
-
|
1605
|
-
|
1606
|
-
|
1607
|
-
section.
|
1591
|
+
section = Section.new parent, sect_level, false
|
1592
|
+
section.id, section.title, section.sectname, section.source_location = sect_id, sect_title, sect_name, source_location
|
1593
|
+
# TODO honor special section numbering option (#661)
|
1594
|
+
if sect_special
|
1595
|
+
section.special = true
|
1596
|
+
section.numbered = true if sect_numbered_force
|
1597
|
+
elsif sect_level > 0 && (document.attributes.key? 'sectnums')
|
1598
|
+
section.numbered = section.special ? (parent.context == :section && parent.numbered) : true
|
1608
1599
|
end
|
1609
1600
|
|
1610
|
-
if section
|
1611
|
-
|
1612
|
-
|
1601
|
+
# generate an ID if one was not embedded or specified as anchor above section title
|
1602
|
+
if (id = section.id ||= (attributes['id'] ||
|
1603
|
+
((document.attributes.key? 'sectids') ? (Section.generate_id section.title, document) : nil)))
|
1604
|
+
unless document.register :refs, [id, section, sect_reftext || section.title]
|
1605
|
+
warn %(asciidoctor: WARNING: #{reader.path}: line #{reader.lineno - (single_line ? 1 : 2)}: id assigned to section already in use: #{id})
|
1606
|
+
end
|
1613
1607
|
end
|
1608
|
+
|
1614
1609
|
section.update_attributes(attributes)
|
1615
1610
|
reader.skip_blank_lines
|
1616
1611
|
|
1617
1612
|
section
|
1618
1613
|
end
|
1619
1614
|
|
1620
|
-
# Private: Get the Integer section level based on the characters
|
1621
|
-
# used in the ASCII line under the section title.
|
1622
|
-
#
|
1623
|
-
# line - the String line from under the section title.
|
1624
|
-
def self.section_level(line)
|
1625
|
-
SECTION_LEVELS[line.chr]
|
1626
|
-
end
|
1627
|
-
|
1628
|
-
#--
|
1629
|
-
# = is level 0, == is level 1, etc.
|
1630
|
-
def self.single_line_section_level(marker)
|
1631
|
-
marker.length - 1
|
1632
|
-
end
|
1633
|
-
|
1634
1615
|
# Internal: Checks if the next line on the Reader is a section title
|
1635
1616
|
#
|
1636
1617
|
# reader - the source Reader
|
1637
1618
|
# attributes - a Hash of attributes collected above the current line
|
1638
1619
|
#
|
1639
|
-
#
|
1640
|
-
# false otherwise
|
1620
|
+
# Returns the Integer section level if the Reader is positioned at a section title or nil otherwise
|
1641
1621
|
def self.is_next_line_section?(reader, attributes)
|
1642
|
-
if
|
1643
|
-
return
|
1622
|
+
if attributes.key?(1) && (attr1 = attributes[1] || '').start_with?('float', 'discrete') && FloatingTitleStyleRx.match?(attr1)
|
1623
|
+
return
|
1624
|
+
elsif reader.has_more_lines?
|
1625
|
+
Compliance.underline_style_section_titles ? is_section_title?(*reader.peek_lines(2)) : is_section_title?(reader.peek_line)
|
1644
1626
|
end
|
1645
|
-
return false unless reader.has_more_lines?
|
1646
|
-
Compliance.underline_style_section_titles ? is_section_title?(*reader.peek_lines(2)) : is_section_title?(reader.peek_line)
|
1647
1627
|
end
|
1648
1628
|
|
1649
1629
|
# Internal: Convenience API for checking if the next line on the Reader is the document title
|
1650
1630
|
#
|
1651
|
-
# reader
|
1652
|
-
# attributes
|
1631
|
+
# reader - the source Reader
|
1632
|
+
# attributes - a Hash of attributes collected above the current line
|
1633
|
+
# leveloffset - an Integer (or integer String value) the represents the current leveloffset
|
1653
1634
|
#
|
1654
1635
|
# returns true if the Reader is positioned at the document title, false otherwise
|
1655
|
-
def self.
|
1656
|
-
|
1636
|
+
def self.is_next_line_doctitle? reader, attributes, leveloffset
|
1637
|
+
if leveloffset
|
1638
|
+
(sect_level = is_next_line_section? reader, attributes) && (sect_level + leveloffset.to_i == 0)
|
1639
|
+
else
|
1640
|
+
(is_next_line_section? reader, attributes) == 0
|
1641
|
+
end
|
1657
1642
|
end
|
1658
1643
|
|
1659
1644
|
# Public: Checks if these lines are a section title
|
@@ -1661,36 +1646,24 @@ class Parser
|
|
1661
1646
|
# line1 - the first line as a String
|
1662
1647
|
# line2 - the second line as a String (default: nil)
|
1663
1648
|
#
|
1664
|
-
#
|
1665
|
-
# false otherwise
|
1649
|
+
# Returns the Integer section level if these lines are a section title or nil otherwise
|
1666
1650
|
def self.is_section_title?(line1, line2 = nil)
|
1667
|
-
|
1668
|
-
level
|
1669
|
-
elsif line2 && (level = is_two_line_section_title?(line1, line2))
|
1670
|
-
level
|
1671
|
-
else
|
1672
|
-
false
|
1673
|
-
end
|
1651
|
+
is_single_line_section_title?(line1) || (line2.nil_or_empty? ? nil : is_two_line_section_title?(line1, line2))
|
1674
1652
|
end
|
1675
1653
|
|
1676
1654
|
def self.is_single_line_section_title?(line1)
|
1677
|
-
|
1678
|
-
if (
|
1679
|
-
|
1680
|
-
|
1681
|
-
else
|
1682
|
-
false
|
1655
|
+
if (line1.start_with?('=') || (Compliance.markdown_syntax && line1.start_with?('#'))) && AtxSectionRx =~ line1
|
1656
|
+
#if line1.start_with?('=', '#') && AtxSectionRx =~ line1 && (line1.start_with?('=') || Compliance.markdown_syntax)
|
1657
|
+
# NOTE level is 1 less than number of line markers
|
1658
|
+
$1.length - 1
|
1683
1659
|
end
|
1684
1660
|
end
|
1685
1661
|
|
1686
1662
|
def self.is_two_line_section_title?(line1, line2)
|
1687
|
-
if
|
1688
|
-
line2
|
1689
|
-
|
1690
|
-
|
1691
|
-
section_level line2
|
1692
|
-
else
|
1693
|
-
false
|
1663
|
+
if (level = SETEXT_SECTION_LEVELS[line2_ch1 = line2.chr]) &&
|
1664
|
+
line2_ch1 * (line2_len = line2.length) == line2 && SetextSectionTitleRx.match?(line1) &&
|
1665
|
+
(line_length(line1) - line2_len).abs < 2
|
1666
|
+
level
|
1694
1667
|
end
|
1695
1668
|
end
|
1696
1669
|
|
@@ -1738,46 +1711,29 @@ class Parser
|
|
1738
1711
|
#--
|
1739
1712
|
# NOTE for efficiency, we don't reuse methods that check for a section title
|
1740
1713
|
def self.parse_section_title(reader, document)
|
1714
|
+
sect_id = sect_reftext = nil
|
1741
1715
|
line1 = reader.read_line
|
1742
|
-
|
1743
|
-
|
1744
|
-
|
1745
|
-
|
1746
|
-
|
1747
|
-
|
1748
|
-
|
1749
|
-
|
1750
|
-
|
1751
|
-
|
1752
|
-
|
1753
|
-
|
1754
|
-
|
1755
|
-
|
1756
|
-
|
1757
|
-
sect_reftext = anchor_match[4]
|
1758
|
-
end
|
1759
|
-
end
|
1760
|
-
elsif Compliance.underline_style_section_titles
|
1761
|
-
if (line2 = reader.peek_line(true)) && SECTION_LEVELS.has_key?(line2.chr) && line2 =~ SetextSectionLineRx &&
|
1762
|
-
(name_match = SetextSectionTitleRx.match(line1)) &&
|
1763
|
-
# chomp so that a (non-visible) endline does not impact calculation
|
1764
|
-
(line_length(line1) - line_length(line2)).abs <= 1
|
1765
|
-
sect_title = name_match[1]
|
1766
|
-
if sect_title.end_with?(']]') && (anchor_match = InlineSectionAnchorRx.match(sect_title))
|
1767
|
-
if anchor_match[2].nil?
|
1768
|
-
sect_title = anchor_match[1]
|
1769
|
-
sect_id = anchor_match[3]
|
1770
|
-
sect_reftext = anchor_match[4]
|
1771
|
-
end
|
1772
|
-
end
|
1773
|
-
sect_level = section_level line2
|
1774
|
-
single_line = false
|
1775
|
-
reader.advance
|
1716
|
+
|
1717
|
+
#if line1.start_with?('=', '#') && AtxSectionRx =~ line1 && (line1.start_with?('=') || Compliance.markdown_syntax)
|
1718
|
+
if (line1.start_with?('=') || (Compliance.markdown_syntax && line1.start_with?('#'))) && AtxSectionRx =~ line1
|
1719
|
+
# NOTE level is 1 less than number of line markers
|
1720
|
+
sect_level, sect_title, single_line = $1.length - 1, $2, true
|
1721
|
+
if sect_title.end_with?(']]') && InlineSectionAnchorRx =~ sect_title && !$1 # escaped
|
1722
|
+
sect_title, sect_id, sect_reftext = (sect_title.slice 0, sect_title.length - $&.length), $2, $3
|
1723
|
+
end
|
1724
|
+
elsif Compliance.underline_style_section_titles && (line2 = reader.peek_line(true)) &&
|
1725
|
+
(sect_level = SETEXT_SECTION_LEVELS[line2_ch1 = line2.chr]) &&
|
1726
|
+
line2_ch1 * (line2_len = line2.length) == line2 && (sect_title = SetextSectionTitleRx =~ line1 && $1) &&
|
1727
|
+
(line_length(line1) - line2_len).abs < 2
|
1728
|
+
single_line = false
|
1729
|
+
if sect_title.end_with?(']]') && InlineSectionAnchorRx =~ sect_title && !$1 # escaped
|
1730
|
+
sect_title, sect_id, sect_reftext = (sect_title.slice 0, sect_title.length - $&.length), $2, $3
|
1776
1731
|
end
|
1732
|
+
reader.advance
|
1733
|
+
else
|
1734
|
+
raise %(Unrecognized section at #{reader.prev_line_info})
|
1777
1735
|
end
|
1778
|
-
|
1779
|
-
sect_level += document.attr('leveloffset', 0).to_i
|
1780
|
-
end
|
1736
|
+
sect_level += document.attr('leveloffset').to_i if document.attr?('leveloffset')
|
1781
1737
|
[sect_id, sect_reftext, sect_title, sect_level, single_line]
|
1782
1738
|
end
|
1783
1739
|
|
@@ -1786,8 +1742,14 @@ class Parser
|
|
1786
1742
|
# line - the String to calculate
|
1787
1743
|
#
|
1788
1744
|
# returns the number of unicode characters in the line
|
1789
|
-
|
1790
|
-
|
1745
|
+
if FORCE_UNICODE_LINE_LENGTH
|
1746
|
+
def self.line_length(line)
|
1747
|
+
line.scan(UnicodeCharScanRx).size
|
1748
|
+
end
|
1749
|
+
else
|
1750
|
+
def self.line_length(line)
|
1751
|
+
line.length
|
1752
|
+
end
|
1791
1753
|
end
|
1792
1754
|
|
1793
1755
|
# Public: Consume and parse the two header lines (line 1 = author info, line 2 = revision info).
|
@@ -1806,20 +1768,16 @@ class Parser
|
|
1806
1768
|
# # 'revnumber' => '1.0', 'revdate' => '2012-12-21', 'revremark' => 'Coincide w/ end of world.'}
|
1807
1769
|
def self.parse_header_metadata(reader, document = nil)
|
1808
1770
|
# NOTE this will discard away any comment lines, but not skip blank lines
|
1809
|
-
process_attribute_entries
|
1771
|
+
process_attribute_entries reader, document
|
1810
1772
|
|
1811
|
-
metadata = {}
|
1812
|
-
implicit_author = nil
|
1813
|
-
implicit_authors = nil
|
1773
|
+
metadata, implicit_author, implicit_authors = {}, nil, nil
|
1814
1774
|
|
1815
1775
|
if reader.has_more_lines? && !reader.next_line_empty?
|
1816
|
-
author_metadata = process_authors reader.read_line
|
1817
|
-
|
1818
|
-
unless author_metadata.empty?
|
1776
|
+
unless (author_metadata = process_authors reader.read_line).empty?
|
1819
1777
|
if document
|
1820
1778
|
# apply header subs and assign to document
|
1821
1779
|
author_metadata.each do |key, val|
|
1822
|
-
unless document.attributes.
|
1780
|
+
unless document.attributes.key? key
|
1823
1781
|
document.attributes[key] = ::String === val ? (document.apply_header_subs val) : val
|
1824
1782
|
end
|
1825
1783
|
end
|
@@ -1832,7 +1790,7 @@ class Parser
|
|
1832
1790
|
end
|
1833
1791
|
|
1834
1792
|
# NOTE this will discard any comment lines, but not skip blank lines
|
1835
|
-
process_attribute_entries
|
1793
|
+
process_attribute_entries reader, document
|
1836
1794
|
|
1837
1795
|
rev_metadata = {}
|
1838
1796
|
|
@@ -1859,7 +1817,7 @@ class Parser
|
|
1859
1817
|
if document
|
1860
1818
|
# apply header subs and assign to document
|
1861
1819
|
rev_metadata.each do |key, val|
|
1862
|
-
unless document.attributes.
|
1820
|
+
unless document.attributes.key? key
|
1863
1821
|
document.attributes[key] = document.apply_header_subs(val)
|
1864
1822
|
end
|
1865
1823
|
end
|
@@ -1869,43 +1827,56 @@ class Parser
|
|
1869
1827
|
end
|
1870
1828
|
|
1871
1829
|
# NOTE this will discard any comment lines, but not skip blank lines
|
1872
|
-
process_attribute_entries
|
1830
|
+
process_attribute_entries reader, document
|
1873
1831
|
|
1874
1832
|
reader.skip_blank_lines
|
1875
1833
|
end
|
1876
1834
|
|
1835
|
+
# process author attribute entries that override (or stand in for) the implicit author line
|
1877
1836
|
if document
|
1878
|
-
|
1879
|
-
author_metadata = nil
|
1880
|
-
if document.attributes.has_key?('author') &&
|
1881
|
-
(author_line = document.attributes['author']) != implicit_author
|
1837
|
+
if document.attributes.key?('author') && (author_line = document.attributes['author']) != implicit_author
|
1882
1838
|
# do not allow multiple, process as names only
|
1883
1839
|
author_metadata = process_authors author_line, true, false
|
1884
|
-
elsif document.attributes.
|
1885
|
-
(author_line = document.attributes['authors']) != implicit_authors
|
1840
|
+
elsif document.attributes.key?('authors') && (author_line = document.attributes['authors']) != implicit_authors
|
1886
1841
|
# allow multiple, process as names only
|
1887
1842
|
author_metadata = process_authors author_line, true
|
1888
1843
|
else
|
1889
|
-
authors = []
|
1890
|
-
|
1891
|
-
|
1892
|
-
|
1893
|
-
|
1844
|
+
authors, author_idx, author_key, explicit, sparse = [], 1, 'author_1', false, false
|
1845
|
+
while document.attributes.key? author_key
|
1846
|
+
# only use indexed author attribute if value is different
|
1847
|
+
# leaves corner case if line matches with underscores converted to spaces; use double space to force
|
1848
|
+
if (author_override = document.attributes[author_key]) == author_metadata[author_key]
|
1849
|
+
authors << nil
|
1850
|
+
sparse = true
|
1851
|
+
else
|
1852
|
+
authors << author_override
|
1853
|
+
explicit = true
|
1854
|
+
end
|
1855
|
+
author_key = %(author_#{author_idx += 1})
|
1894
1856
|
end
|
1895
|
-
if
|
1896
|
-
#
|
1897
|
-
|
1898
|
-
|
1899
|
-
|
1900
|
-
|
1857
|
+
if explicit
|
1858
|
+
# rebuild implicit author names to reparse
|
1859
|
+
authors.each_with_index do |author, idx|
|
1860
|
+
unless author
|
1861
|
+
authors[idx] = [
|
1862
|
+
author_metadata[%(firstname_#{name_idx = idx + 1})],
|
1863
|
+
author_metadata[%(middlename_#{name_idx})],
|
1864
|
+
author_metadata[%(lastname_#{name_idx})]
|
1865
|
+
].compact.map {|it| it.tr ' ', '_' } * ' '
|
1866
|
+
end
|
1867
|
+
end if sparse
|
1868
|
+
# process as names only
|
1869
|
+
author_metadata = process_authors authors, true, false
|
1870
|
+
else
|
1871
|
+
author_metadata = {}
|
1901
1872
|
end
|
1902
1873
|
end
|
1903
1874
|
|
1904
|
-
|
1875
|
+
unless author_metadata.empty?
|
1905
1876
|
document.attributes.update author_metadata
|
1906
1877
|
|
1907
1878
|
# special case
|
1908
|
-
if !document.attributes.
|
1879
|
+
if !document.attributes.key?('email') && document.attributes.key?('email_1')
|
1909
1880
|
document.attributes['email'] = document.attributes['email_1']
|
1910
1881
|
end
|
1911
1882
|
end
|
@@ -1923,10 +1894,10 @@ class Parser
|
|
1923
1894
|
# semicolon-separated entries in the author line (default: true)
|
1924
1895
|
#
|
1925
1896
|
# returns a Hash of author metadata
|
1926
|
-
def self.process_authors
|
1897
|
+
def self.process_authors author_line, names_only = false, multiple = true
|
1927
1898
|
author_metadata = {}
|
1928
1899
|
keys = ['author', 'authorinitials', 'firstname', 'middlename', 'lastname', 'email']
|
1929
|
-
author_entries = multiple ? (author_line.split ';').map {|
|
1900
|
+
author_entries = multiple ? (author_line.split ';').map {|it| it.strip } : Array(author_line)
|
1930
1901
|
author_entries.each_with_index do |author_entry, idx|
|
1931
1902
|
next if author_entry.empty?
|
1932
1903
|
key_map = {}
|
@@ -1941,42 +1912,47 @@ class Parser
|
|
1941
1912
|
end
|
1942
1913
|
|
1943
1914
|
segments = nil
|
1944
|
-
if names_only
|
1945
|
-
#
|
1946
|
-
|
1947
|
-
|
1948
|
-
|
1915
|
+
if names_only # when parsing an attribute value
|
1916
|
+
# QUESTION should we rstrip author_entry?
|
1917
|
+
if author_entry.include? '<'
|
1918
|
+
author_metadata[key_map[:author]] = author_entry.tr('_', ' ')
|
1919
|
+
author_entry = author_entry.gsub XmlSanitizeRx, ''
|
1920
|
+
end
|
1921
|
+
# NOTE split names and collapse repeating whitespace (split drops any leading whitespace)
|
1922
|
+
if (segments = author_entry.split nil, 3).size == 3
|
1923
|
+
segments << (segments.pop.squeeze ' ')
|
1949
1924
|
end
|
1950
1925
|
elsif (match = AuthorInfoLineRx.match(author_entry))
|
1951
|
-
segments = match.to_a
|
1952
|
-
|
1953
|
-
|
1954
|
-
|
1955
|
-
|
1956
|
-
author_metadata[key_map[:
|
1957
|
-
|
1958
|
-
|
1959
|
-
|
1960
|
-
|
1961
|
-
|
1962
|
-
|
1963
|
-
|
1964
|
-
|
1965
|
-
|
1966
|
-
|
1967
|
-
|
1926
|
+
(segments = match.to_a).shift
|
1927
|
+
end
|
1928
|
+
|
1929
|
+
if segments
|
1930
|
+
author = author_metadata[key_map[:firstname]] = fname = segments[0].tr('_', ' ')
|
1931
|
+
author_metadata[key_map[:authorinitials]] = fname.chr
|
1932
|
+
if segments[1]
|
1933
|
+
if segments[2]
|
1934
|
+
author_metadata[key_map[:middlename]] = mname = segments[1].tr('_', ' ')
|
1935
|
+
author_metadata[key_map[:lastname]] = lname = segments[2].tr('_', ' ')
|
1936
|
+
author = fname + ' ' + mname + ' ' + lname
|
1937
|
+
author_metadata[key_map[:authorinitials]] = %(#{fname.chr}#{mname.chr}#{lname.chr})
|
1938
|
+
else
|
1939
|
+
author_metadata[key_map[:lastname]] = lname = segments[1].tr('_', ' ')
|
1940
|
+
author = fname + ' ' + lname
|
1941
|
+
author_metadata[key_map[:authorinitials]] = %(#{fname.chr}#{lname.chr})
|
1942
|
+
end
|
1968
1943
|
end
|
1969
|
-
author_metadata[key_map[:
|
1944
|
+
author_metadata[key_map[:author]] ||= author
|
1945
|
+
author_metadata[key_map[:email]] = segments[3] unless names_only || !segments[3]
|
1970
1946
|
else
|
1971
|
-
author_metadata[key_map[:author]] = author_metadata[key_map[:firstname]] = fname = author_entry.
|
1972
|
-
author_metadata[key_map[:authorinitials]] = fname
|
1947
|
+
author_metadata[key_map[:author]] = author_metadata[key_map[:firstname]] = fname = author_entry.squeeze(' ').strip
|
1948
|
+
author_metadata[key_map[:authorinitials]] = fname.chr
|
1973
1949
|
end
|
1974
1950
|
|
1975
1951
|
author_metadata['authorcount'] = idx + 1
|
1976
1952
|
# only assign the _1 attributes if there are multiple authors
|
1977
1953
|
if idx == 1
|
1978
1954
|
keys.each do |key|
|
1979
|
-
author_metadata[%(#{key}_1)] = author_metadata[key] if author_metadata.
|
1955
|
+
author_metadata[%(#{key}_1)] = author_metadata[key] if author_metadata.key? key
|
1980
1956
|
end
|
1981
1957
|
end
|
1982
1958
|
if idx == 0
|
@@ -1995,15 +1971,15 @@ class Parser
|
|
1995
1971
|
# blank lines and comments.
|
1996
1972
|
#
|
1997
1973
|
# reader - the source reader
|
1998
|
-
#
|
1974
|
+
# document - the current Document
|
1999
1975
|
# attributes - a Hash of attributes in which any metadata found will be stored (default: {})
|
2000
1976
|
# options - a Hash of options to control processing: (default: {})
|
2001
|
-
# * :text indicates that
|
1977
|
+
# * :text indicates that parser is only looking for text content
|
2002
1978
|
# and thus the block title should not be captured
|
2003
1979
|
#
|
2004
1980
|
# returns the Hash of attributes including any metadata found
|
2005
|
-
def self.parse_block_metadata_lines
|
2006
|
-
while parse_block_metadata_line
|
1981
|
+
def self.parse_block_metadata_lines reader, document, attributes = {}, options = {}
|
1982
|
+
while parse_block_metadata_line reader, document, attributes, options
|
2007
1983
|
# discard the line just processed
|
2008
1984
|
reader.advance
|
2009
1985
|
reader.skip_blank_lines
|
@@ -2024,124 +2000,128 @@ class Parser
|
|
2024
2000
|
# If the line contains block metadata, the method returns true, otherwise false.
|
2025
2001
|
#
|
2026
2002
|
# reader - the source reader
|
2027
|
-
#
|
2003
|
+
# document - the current Document
|
2028
2004
|
# attributes - a Hash of attributes in which any metadata found will be stored
|
2029
2005
|
# options - a Hash of options to control processing: (default: {})
|
2030
|
-
# * :text indicates
|
2031
|
-
#
|
2006
|
+
# * :text indicates the parser is only looking for text content,
|
2007
|
+
# thus neither a block title or attribute entry should be captured
|
2032
2008
|
#
|
2033
2009
|
# returns true if the line contains metadata, otherwise false
|
2034
|
-
def self.parse_block_metadata_line
|
2035
|
-
|
2036
|
-
|
2037
|
-
|
2038
|
-
|
2039
|
-
|
2040
|
-
|
2041
|
-
|
2042
|
-
|
2043
|
-
|
2044
|
-
|
2045
|
-
|
2046
|
-
|
2047
|
-
|
2048
|
-
|
2049
|
-
|
2050
|
-
|
2051
|
-
|
2052
|
-
|
2053
|
-
|
2054
|
-
|
2055
|
-
|
2056
|
-
|
2057
|
-
|
2058
|
-
|
2059
|
-
|
2010
|
+
def self.parse_block_metadata_line reader, document, attributes, options = {}
|
2011
|
+
if (next_line = reader.peek_line) &&
|
2012
|
+
(options[:text] ? (next_line.start_with? '[', '/') : (normal = next_line.start_with? '[', '.', '/', ':'))
|
2013
|
+
if next_line.start_with? '['
|
2014
|
+
if next_line.start_with? '[['
|
2015
|
+
if (next_line.end_with? ']]') && BlockAnchorRx =~ next_line
|
2016
|
+
# NOTE registration of id and reftext is deferred until block is processed
|
2017
|
+
attributes['id'] = $1
|
2018
|
+
if (reftext = $2)
|
2019
|
+
attributes['reftext'] = (reftext.include? '{') ? (document.sub_attributes reftext) : reftext
|
2020
|
+
end
|
2021
|
+
return true
|
2022
|
+
end
|
2023
|
+
elsif (next_line.end_with? ']') && BlockAttributeListRx =~ next_line
|
2024
|
+
document.parse_attributes $1, [], :sub_input => true, :into => attributes
|
2025
|
+
return true
|
2026
|
+
end
|
2027
|
+
elsif normal && (next_line.start_with? '.')
|
2028
|
+
if BlockTitleRx =~ next_line
|
2029
|
+
# NOTE title doesn't apply to section, but we need to stash it for the first block
|
2030
|
+
# TODO should issue an error if this is found above the document title
|
2031
|
+
attributes['title'] = $1
|
2032
|
+
return true
|
2033
|
+
end
|
2034
|
+
elsif !normal || (next_line.start_with? '/')
|
2035
|
+
if next_line == '//'
|
2036
|
+
return true
|
2037
|
+
elsif normal && '/' * (ll = next_line.length) == next_line
|
2038
|
+
unless ll == 3
|
2039
|
+
reader.read_lines_until :skip_first_line => true, :preserve_last_line => true, :terminator => next_line, :skip_processing => true
|
2040
|
+
return true
|
2041
|
+
end
|
2042
|
+
else
|
2043
|
+
return true unless next_line.start_with? '///'
|
2044
|
+
end if next_line.start_with? '//'
|
2045
|
+
# NOTE the final condition can be consolidated into single line
|
2046
|
+
elsif normal && (next_line.start_with? ':') && AttributeEntryRx =~ next_line
|
2047
|
+
process_attribute_entry reader, document, attributes, $~
|
2048
|
+
return true
|
2049
|
+
end
|
2060
2050
|
end
|
2061
|
-
|
2062
|
-
true
|
2063
2051
|
end
|
2064
2052
|
|
2065
|
-
def self.process_attribute_entries
|
2053
|
+
def self.process_attribute_entries reader, document, attributes = nil
|
2066
2054
|
reader.skip_comment_lines
|
2067
|
-
while process_attribute_entry
|
2055
|
+
while process_attribute_entry reader, document, attributes
|
2068
2056
|
# discard line just processed
|
2069
2057
|
reader.advance
|
2070
2058
|
reader.skip_comment_lines
|
2071
2059
|
end
|
2072
2060
|
end
|
2073
2061
|
|
2074
|
-
def self.process_attribute_entry
|
2075
|
-
match ||= (reader.has_more_lines? ? AttributeEntryRx.match
|
2076
|
-
|
2077
|
-
|
2078
|
-
|
2079
|
-
|
2080
|
-
|
2081
|
-
|
2082
|
-
|
2083
|
-
break if (next_line = reader.peek_line.strip).empty?
|
2084
|
-
if (keep_open = next_line.end_with? line_continuation)
|
2085
|
-
next_line = next_line.chop.rstrip
|
2086
|
-
end
|
2087
|
-
separator = (value.end_with? LINE_BREAK) ? EOL : ' '
|
2088
|
-
value = %(#{value}#{separator}#{next_line})
|
2089
|
-
break unless keep_open
|
2062
|
+
def self.process_attribute_entry reader, document, attributes = nil, match = nil
|
2063
|
+
if (match ||= (reader.has_more_lines? ? (AttributeEntryRx.match reader.peek_line) : nil))
|
2064
|
+
if (value = match[2]).nil_or_empty?
|
2065
|
+
value = ''
|
2066
|
+
elsif value.end_with? LINE_CONTINUATION, LINE_CONTINUATION_LEGACY
|
2067
|
+
con, value = value.slice(-2, 2), value.slice(0, value.length - 2).rstrip
|
2068
|
+
while reader.advance && !(next_line = reader.peek_line.lstrip).empty?
|
2069
|
+
if (keep_open = next_line.end_with? con)
|
2070
|
+
next_line = (next_line.slice 0, next_line.length - 2).rstrip
|
2090
2071
|
end
|
2072
|
+
value = %(#{value}#{(value.end_with? HARD_LINE_BREAK) ? LF : ' '}#{next_line})
|
2073
|
+
break unless keep_open
|
2091
2074
|
end
|
2092
2075
|
end
|
2093
2076
|
|
2094
|
-
store_attribute
|
2077
|
+
store_attribute match[1], value, document, attributes
|
2095
2078
|
true
|
2096
|
-
else
|
2097
|
-
false
|
2098
2079
|
end
|
2099
2080
|
end
|
2100
2081
|
|
2101
2082
|
# Public: Store the attribute in the document and register attribute entry if accessible
|
2102
2083
|
#
|
2103
|
-
# name - the String name of the attribute to store
|
2084
|
+
# name - the String name of the attribute to store;
|
2085
|
+
# if name begins or ends with !, it signals to remove the attribute with that root name
|
2104
2086
|
# value - the String value of the attribute to store
|
2105
2087
|
# doc - the Document being parsed
|
2106
2088
|
# attrs - the attributes for the current context
|
2107
2089
|
#
|
2108
|
-
# returns a 2-element array containing the attribute name and value
|
2109
|
-
def self.store_attribute
|
2090
|
+
# returns a 2-element array containing the resolved attribute name (minus the ! indicator) and value
|
2091
|
+
def self.store_attribute name, value, doc = nil, attrs = nil
|
2110
2092
|
# TODO move processing of attribute value to utility method
|
2111
|
-
if name.end_with?
|
2112
|
-
# a nil value signals the attribute should be deleted (
|
2113
|
-
value = nil
|
2114
|
-
|
2115
|
-
|
2116
|
-
|
2117
|
-
|
2118
|
-
|
2119
|
-
|
2120
|
-
|
2121
|
-
name =
|
2122
|
-
|
2093
|
+
if name.end_with? '!'
|
2094
|
+
# a nil value signals the attribute should be deleted (unset)
|
2095
|
+
name, value = name.chop, nil
|
2096
|
+
elsif name.start_with? '!'
|
2097
|
+
# a nil value signals the attribute should be deleted (unset)
|
2098
|
+
name, value = (name.slice 1, name.length), nil
|
2099
|
+
end
|
2100
|
+
|
2101
|
+
name = sanitize_attribute_name name
|
2102
|
+
# alias numbered attribute to sectnums
|
2103
|
+
name = 'sectnums' if name == 'numbered'
|
2104
|
+
|
2123
2105
|
if doc
|
2124
|
-
|
2125
|
-
|
2126
|
-
|
2127
|
-
|
2128
|
-
elsif name == 'leveloffset'
|
2129
|
-
if value
|
2130
|
-
case value.chr
|
2131
|
-
when '+'
|
2106
|
+
if value
|
2107
|
+
if name == 'leveloffset'
|
2108
|
+
# support relative leveloffset values
|
2109
|
+
if value.start_with? '+'
|
2132
2110
|
value = ((doc.attr 'leveloffset', 0).to_i + (value[1..-1] || 0).to_i).to_s
|
2133
|
-
|
2111
|
+
elsif value.start_with? '-'
|
2134
2112
|
value = ((doc.attr 'leveloffset', 0).to_i - (value[1..-1] || 0).to_i).to_s
|
2135
2113
|
end
|
2136
2114
|
end
|
2115
|
+
# QUESTION should we set value to locked value if set_attribute returns false?
|
2116
|
+
if (resolved_value = doc.set_attribute name, value)
|
2117
|
+
value = resolved_value
|
2118
|
+
(Document::AttributeEntry.new name, value).save_to attrs if attrs
|
2119
|
+
end
|
2120
|
+
elsif (doc.delete_attribute name) && attrs
|
2121
|
+
(Document::AttributeEntry.new name, value).save_to attrs
|
2137
2122
|
end
|
2138
|
-
|
2139
|
-
|
2140
|
-
|
2141
|
-
if accessible && attrs
|
2142
|
-
# NOTE lookup resolved value (resolution occurs inside set_attribute)
|
2143
|
-
value = doc.attributes[name] if value
|
2144
|
-
Document::AttributeEntry.new(name, value).save_to(attrs)
|
2123
|
+
elsif attrs
|
2124
|
+
(Document::AttributeEntry.new name, value).save_to attrs
|
2145
2125
|
end
|
2146
2126
|
|
2147
2127
|
[name, value]
|
@@ -2164,8 +2144,8 @@ class Parser
|
|
2164
2144
|
#
|
2165
2145
|
# Returns the String 0-index marker for this list item
|
2166
2146
|
def self.resolve_list_marker(list_type, marker, ordinal = 0, validate = false, reader = nil)
|
2167
|
-
if list_type == :olist
|
2168
|
-
|
2147
|
+
if list_type == :olist
|
2148
|
+
(marker.start_with? '.') ? marker : (resolve_ordered_list_marker marker, ordinal, validate, reader)
|
2169
2149
|
elsif list_type == :colist
|
2170
2150
|
'<1>'
|
2171
2151
|
else
|
@@ -2195,41 +2175,40 @@ class Parser
|
|
2195
2175
|
#
|
2196
2176
|
# Returns the String of the first marker in this number series
|
2197
2177
|
def self.resolve_ordered_list_marker(marker, ordinal = 0, validate = false, reader = nil)
|
2198
|
-
number_style = ORDERED_LIST_STYLES.find {|s| OrderedListMarkerRxMap[s] =~ marker }
|
2199
2178
|
expected = actual = nil
|
2200
|
-
case
|
2201
|
-
|
2202
|
-
|
2203
|
-
|
2204
|
-
|
2205
|
-
|
2206
|
-
|
2207
|
-
|
2208
|
-
|
2209
|
-
|
2210
|
-
|
2211
|
-
|
2212
|
-
|
2213
|
-
|
2214
|
-
|
2215
|
-
|
2216
|
-
|
2217
|
-
|
2218
|
-
|
2219
|
-
|
2220
|
-
|
2221
|
-
|
2222
|
-
|
2223
|
-
|
2224
|
-
|
2225
|
-
|
2226
|
-
|
2227
|
-
|
2228
|
-
|
2229
|
-
|
2230
|
-
|
2231
|
-
|
2232
|
-
|
2179
|
+
case ORDERED_LIST_STYLES.find {|s| OrderedListMarkerRxMap[s].match? marker }
|
2180
|
+
when :arabic
|
2181
|
+
if validate
|
2182
|
+
expected = ordinal + 1
|
2183
|
+
actual = marker.to_i # remove trailing . and coerce to int
|
2184
|
+
end
|
2185
|
+
marker = '1.'
|
2186
|
+
when :loweralpha
|
2187
|
+
if validate
|
2188
|
+
expected = ('a'[0].ord + ordinal).chr
|
2189
|
+
actual = marker.chop # remove trailing .
|
2190
|
+
end
|
2191
|
+
marker = 'a.'
|
2192
|
+
when :upperalpha
|
2193
|
+
if validate
|
2194
|
+
expected = ('A'[0].ord + ordinal).chr
|
2195
|
+
actual = marker.chop # remove trailing .
|
2196
|
+
end
|
2197
|
+
marker = 'A.'
|
2198
|
+
when :lowerroman
|
2199
|
+
if validate
|
2200
|
+
# TODO report this in roman numerals; see https://github.com/jamesshipton/roman-numeral/blob/master/lib/roman_numeral.rb
|
2201
|
+
expected = ordinal + 1
|
2202
|
+
actual = roman_numeral_to_int(marker.chop) # remove trailing ) and coerce to int
|
2203
|
+
end
|
2204
|
+
marker = 'i)'
|
2205
|
+
when :upperroman
|
2206
|
+
if validate
|
2207
|
+
# TODO report this in roman numerals; see https://github.com/jamesshipton/roman-numeral/blob/master/lib/roman_numeral.rb
|
2208
|
+
expected = ordinal + 1
|
2209
|
+
actual = roman_numeral_to_int(marker.chop) # remove trailing ) and coerce to int
|
2210
|
+
end
|
2211
|
+
marker = 'I)'
|
2233
2212
|
end
|
2234
2213
|
|
2235
2214
|
if validate && expected != actual
|
@@ -2277,106 +2256,112 @@ class Parser
|
|
2277
2256
|
# returns an instance of Asciidoctor::Table parsed from the provided reader
|
2278
2257
|
def self.next_table(table_reader, parent, attributes)
|
2279
2258
|
table = Table.new(parent, attributes)
|
2280
|
-
if
|
2259
|
+
if attributes.key? 'title'
|
2281
2260
|
table.title = attributes.delete 'title'
|
2282
|
-
table.assign_caption
|
2261
|
+
table.assign_caption(attributes.delete 'caption')
|
2283
2262
|
end
|
2284
2263
|
|
2285
2264
|
if (attributes.key? 'cols') && !(colspecs = parse_colspecs attributes['cols']).empty?
|
2286
2265
|
table.create_columns colspecs
|
2287
2266
|
explicit_colspecs = true
|
2288
|
-
else
|
2289
|
-
explicit_colspecs = false
|
2290
2267
|
end
|
2291
2268
|
|
2292
2269
|
skipped = table_reader.skip_blank_lines
|
2293
2270
|
|
2294
|
-
parser_ctx = Table::ParserContext.new
|
2295
|
-
|
2296
|
-
|
2297
|
-
while table_reader.
|
2298
|
-
loop_idx += 1
|
2299
|
-
|
2300
|
-
|
2301
|
-
|
2302
|
-
!(next_line = table_reader.peek_line).nil? && next_line.empty?
|
2303
|
-
table.has_header_option = true
|
2304
|
-
attributes['header-option'] = ''
|
2305
|
-
attributes['options'] = (attributes.key? 'options') ? %(#{attributes['options']},header) : 'header'
|
2306
|
-
end
|
2307
|
-
|
2308
|
-
if parser_ctx.format == 'psv'
|
2271
|
+
parser_ctx = Table::ParserContext.new table_reader, table, attributes
|
2272
|
+
format, loop_idx, implicit_header_boundary = parser_ctx.format, -1, nil
|
2273
|
+
implicit_header = true unless skipped > 0 || (attributes.key? 'header-option') || (attributes.key? 'noheader-option')
|
2274
|
+
while (line = table_reader.read_line)
|
2275
|
+
if (loop_idx += 1) > 0 && line.empty?
|
2276
|
+
line = nil
|
2277
|
+
implicit_header_boundary += 1 if implicit_header_boundary
|
2278
|
+
elsif format == 'psv'
|
2309
2279
|
if parser_ctx.starts_with_delimiter? line
|
2310
|
-
line = line
|
2311
|
-
# push
|
2280
|
+
line = line.slice 1, line.length
|
2281
|
+
# push empty cell spec if cell boundary appears at start of line
|
2312
2282
|
parser_ctx.close_open_cell
|
2283
|
+
implicit_header_boundary = nil if implicit_header_boundary
|
2313
2284
|
else
|
2314
|
-
next_cellspec, line = parse_cellspec
|
2315
|
-
# if
|
2316
|
-
if
|
2285
|
+
next_cellspec, line = parse_cellspec line, :start, parser_ctx.delimiter
|
2286
|
+
# if cellspec is not nil, we're at a cell boundary
|
2287
|
+
if next_cellspec
|
2317
2288
|
parser_ctx.close_open_cell next_cellspec
|
2318
|
-
|
2319
|
-
|
2289
|
+
implicit_header_boundary = nil if implicit_header_boundary
|
2290
|
+
# otherwise, the cell continues from previous line
|
2291
|
+
elsif implicit_header_boundary && implicit_header_boundary == loop_idx
|
2292
|
+
implicit_header, implicit_header_boundary = false, nil
|
2320
2293
|
end
|
2321
2294
|
end
|
2322
2295
|
end
|
2323
2296
|
|
2324
|
-
|
2325
|
-
|
2326
|
-
|
2327
|
-
|
2328
|
-
|
2329
|
-
|
2330
|
-
|
2331
|
-
|
2332
|
-
|
2297
|
+
# NOTE implicit header is offset by at least one blank line; implicit_header_boundary tracks size of gap
|
2298
|
+
if loop_idx == 0 && implicit_header
|
2299
|
+
if table_reader.has_more_lines? && table_reader.peek_line.empty?
|
2300
|
+
implicit_header_boundary = 1
|
2301
|
+
else
|
2302
|
+
implicit_header = false
|
2303
|
+
end
|
2304
|
+
end
|
2305
|
+
|
2306
|
+
# this loop is used for flow control; internal logic controls how many times it executes
|
2307
|
+
while true
|
2308
|
+
if line && (m = parser_ctx.match_delimiter line)
|
2309
|
+
case format
|
2310
|
+
when 'csv'
|
2311
|
+
if parser_ctx.buffer_has_unclosed_quotes? m.pre_match
|
2312
|
+
break if (line = parser_ctx.skip_past_delimiter m).empty?
|
2313
|
+
redo
|
2333
2314
|
end
|
2334
|
-
|
2315
|
+
parser_ctx.buffer = %(#{parser_ctx.buffer}#{m.pre_match})
|
2316
|
+
when 'dsv'
|
2335
2317
|
if m.pre_match.end_with? '\\'
|
2336
|
-
|
2337
|
-
|
2338
|
-
if (line = parser_ctx.skip_matched_delimiter(m, true)).empty?
|
2339
|
-
parser_ctx.buffer = %(#{parser_ctx.buffer}#{EOL})
|
2318
|
+
if (line = parser_ctx.skip_past_escaped_delimiter m).empty?
|
2319
|
+
parser_ctx.buffer = %(#{parser_ctx.buffer}#{LF})
|
2340
2320
|
parser_ctx.keep_cell_open
|
2341
2321
|
break
|
2342
2322
|
end
|
2343
|
-
|
2323
|
+
redo
|
2344
2324
|
end
|
2345
|
-
|
2346
|
-
|
2347
|
-
|
2348
|
-
|
2325
|
+
parser_ctx.buffer = %(#{parser_ctx.buffer}#{m.pre_match})
|
2326
|
+
else # psv
|
2327
|
+
if m.pre_match.end_with? '\\'
|
2328
|
+
if (line = parser_ctx.skip_past_escaped_delimiter m).empty?
|
2329
|
+
parser_ctx.buffer = %(#{parser_ctx.buffer}#{LF})
|
2330
|
+
parser_ctx.keep_cell_open
|
2331
|
+
break
|
2332
|
+
end
|
2333
|
+
redo
|
2334
|
+
end
|
2335
|
+
next_cellspec, cell_text = parse_cellspec m.pre_match
|
2349
2336
|
parser_ctx.push_cellspec next_cellspec
|
2350
2337
|
parser_ctx.buffer = %(#{parser_ctx.buffer}#{cell_text})
|
2351
|
-
else
|
2352
|
-
parser_ctx.buffer = %(#{parser_ctx.buffer}#{m.pre_match})
|
2353
|
-
end
|
2354
|
-
|
2355
|
-
if (line = m.post_match).empty?
|
2356
|
-
# hack to prevent dropping empty cell found at end of line (see issue #1106)
|
2357
|
-
seen = false
|
2358
2338
|
end
|
2359
|
-
|
2339
|
+
# don't break if empty to preserve empty cell found at end of line (see issue #1106)
|
2340
|
+
line = nil if (line = m.post_match).empty?
|
2360
2341
|
parser_ctx.close_cell
|
2361
2342
|
else
|
2362
|
-
# no other delimiters to see here
|
2363
|
-
|
2364
|
-
|
2365
|
-
|
2366
|
-
|
2343
|
+
# no other delimiters to see here; suck up this line into the buffer and move on
|
2344
|
+
parser_ctx.buffer = %(#{parser_ctx.buffer}#{line}#{LF})
|
2345
|
+
case format
|
2346
|
+
when 'csv'
|
2347
|
+
# QUESTION make stripping endlines in csv data an option? (unwrap-option?)
|
2367
2348
|
parser_ctx.buffer = %(#{parser_ctx.buffer.rstrip} )
|
2368
|
-
|
2369
|
-
|
2370
|
-
|
2371
|
-
|
2372
|
-
|
2373
|
-
|
2349
|
+
if parser_ctx.buffer_has_unclosed_quotes?
|
2350
|
+
implicit_header, implicit_header_boundary = false, nil if implicit_header_boundary && loop_idx == 0
|
2351
|
+
parser_ctx.keep_cell_open
|
2352
|
+
else
|
2353
|
+
parser_ctx.close_cell true
|
2354
|
+
end
|
2355
|
+
when 'dsv'
|
2374
2356
|
parser_ctx.close_cell true
|
2357
|
+
else # psv
|
2358
|
+
parser_ctx.keep_cell_open
|
2375
2359
|
end
|
2360
|
+
break
|
2376
2361
|
end
|
2377
2362
|
end
|
2378
2363
|
|
2379
|
-
|
2364
|
+
table_reader.skip_blank_lines unless parser_ctx.cell_open?
|
2380
2365
|
|
2381
2366
|
unless table_reader.has_more_lines?
|
2382
2367
|
# NOTE may have already closed cell in csv or dsv table (see previous call to parser_ctx.close_cell(true))
|
@@ -2388,6 +2373,12 @@ class Parser
|
|
2388
2373
|
table.assign_column_widths
|
2389
2374
|
end
|
2390
2375
|
|
2376
|
+
if implicit_header
|
2377
|
+
table.has_header_option = true
|
2378
|
+
attributes['header-option'] = ''
|
2379
|
+
attributes['options'] = (attributes.key? 'options') ? %(#{attributes['options']},header) : 'header'
|
2380
|
+
end
|
2381
|
+
|
2391
2382
|
table.partition_header_footer attributes
|
2392
2383
|
|
2393
2384
|
table
|
@@ -2405,7 +2396,7 @@ class Parser
|
|
2405
2396
|
# returns a Hash of attributes that specify how to format
|
2406
2397
|
# and layout the cells in the table.
|
2407
2398
|
def self.parse_colspecs records
|
2408
|
-
records = records.
|
2399
|
+
records = records.delete ' ' if records.include? ' '
|
2409
2400
|
# check for deprecated syntax: single number, equal column spread
|
2410
2401
|
if records == records.to_i.to_s
|
2411
2402
|
return ::Array.new(records.to_i) { { 'width' => 1 } }
|
@@ -2422,11 +2413,11 @@ class Parser
|
|
2422
2413
|
if m[2]
|
2423
2414
|
# make this an operation
|
2424
2415
|
colspec, rowspec = m[2].split '.'
|
2425
|
-
if !colspec.nil_or_empty? &&
|
2426
|
-
spec['halign'] =
|
2416
|
+
if !colspec.nil_or_empty? && TableCellHorzAlignments.key?(colspec)
|
2417
|
+
spec['halign'] = TableCellHorzAlignments[colspec]
|
2427
2418
|
end
|
2428
|
-
if !rowspec.nil_or_empty? &&
|
2429
|
-
spec['valign'] =
|
2419
|
+
if !rowspec.nil_or_empty? && TableCellVertAlignments.key?(rowspec)
|
2420
|
+
spec['valign'] = TableCellVertAlignments[rowspec]
|
2430
2421
|
end
|
2431
2422
|
end
|
2432
2423
|
|
@@ -2435,8 +2426,8 @@ class Parser
|
|
2435
2426
|
spec['width'] = (m[3] ? m[3].to_i : 1)
|
2436
2427
|
|
2437
2428
|
# make this an operation
|
2438
|
-
if m[4] &&
|
2439
|
-
spec['style'] =
|
2429
|
+
if m[4] && TableCellStyles.key?(m[4])
|
2430
|
+
spec['style'] = TableCellStyles[m[4]]
|
2440
2431
|
end
|
2441
2432
|
|
2442
2433
|
if m[1]
|
@@ -2462,12 +2453,10 @@ class Parser
|
|
2462
2453
|
#
|
2463
2454
|
# returns the Hash of attributes that indicate how to layout
|
2464
2455
|
# and style this cell in the table.
|
2465
|
-
def self.parse_cellspec(line, pos = :
|
2466
|
-
m = nil
|
2467
|
-
rest = ''
|
2456
|
+
def self.parse_cellspec(line, pos = :end, delimiter = nil)
|
2457
|
+
m, rest = nil, ''
|
2468
2458
|
|
2469
|
-
|
2470
|
-
when :start
|
2459
|
+
if pos == :start
|
2471
2460
|
if line.include? delimiter
|
2472
2461
|
spec_part, rest = line.split delimiter, 2
|
2473
2462
|
if (m = CellSpecStartRx.match spec_part)
|
@@ -2478,7 +2467,7 @@ class Parser
|
|
2478
2467
|
else
|
2479
2468
|
return [nil, line]
|
2480
2469
|
end
|
2481
|
-
|
2470
|
+
else # pos == :end
|
2482
2471
|
if (m = CellSpecEndRx.match line)
|
2483
2472
|
# NOTE return the line stripped of trailing whitespace if no cellspec is found in this case
|
2484
2473
|
return [{}, line.rstrip] if m[0].lstrip.empty?
|
@@ -2503,16 +2492,16 @@ class Parser
|
|
2503
2492
|
|
2504
2493
|
if m[3]
|
2505
2494
|
colspec, rowspec = m[3].split '.'
|
2506
|
-
if !colspec.nil_or_empty? &&
|
2507
|
-
spec['halign'] =
|
2495
|
+
if !colspec.nil_or_empty? && TableCellHorzAlignments.key?(colspec)
|
2496
|
+
spec['halign'] = TableCellHorzAlignments[colspec]
|
2508
2497
|
end
|
2509
|
-
if !rowspec.nil_or_empty? &&
|
2510
|
-
spec['valign'] =
|
2498
|
+
if !rowspec.nil_or_empty? && TableCellVertAlignments.key?(rowspec)
|
2499
|
+
spec['valign'] = TableCellVertAlignments[rowspec]
|
2511
2500
|
end
|
2512
2501
|
end
|
2513
2502
|
|
2514
|
-
if m[4] &&
|
2515
|
-
spec['style'] =
|
2503
|
+
if m[4] && TableCellStyles.key?(m[4])
|
2504
|
+
spec['style'] = TableCellStyles[m[4]]
|
2516
2505
|
end
|
2517
2506
|
|
2518
2507
|
[spec, rest]
|
@@ -2522,48 +2511,40 @@ class Parser
|
|
2522
2511
|
#
|
2523
2512
|
# Parse the first positional attribute to extract the style, role and id
|
2524
2513
|
# parts, assign the values to their cooresponding attribute keys and return
|
2525
|
-
#
|
2526
|
-
# positional attribute.
|
2514
|
+
# the parsed style from the first positional attribute.
|
2527
2515
|
#
|
2528
2516
|
# attributes - The Hash of attributes to process and update
|
2529
2517
|
#
|
2530
2518
|
# Examples
|
2531
2519
|
#
|
2532
2520
|
# puts attributes
|
2533
|
-
# => {1 => "abstract#intro.lead%fragment", "style" => "preamble"}
|
2521
|
+
# => { 1 => "abstract#intro.lead%fragment", "style" => "preamble" }
|
2534
2522
|
#
|
2535
2523
|
# parse_style_attribute(attributes)
|
2536
|
-
# =>
|
2524
|
+
# => "abstract"
|
2537
2525
|
#
|
2538
2526
|
# puts attributes
|
2539
|
-
# => {1 => "abstract#intro.lead", "style" => "abstract", "id" => "intro",
|
2540
|
-
# "role" => "lead", "options" =>
|
2527
|
+
# => { 1 => "abstract#intro.lead%fragment", "style" => "abstract", "id" => "intro",
|
2528
|
+
# "role" => "lead", "options" => "fragment", "fragment-option" => '' }
|
2541
2529
|
#
|
2542
|
-
# Returns
|
2543
|
-
# first positional attribute and the original style that was
|
2544
|
-
# replaced
|
2530
|
+
# Returns the String style parsed from the first positional attribute
|
2545
2531
|
def self.parse_style_attribute(attributes, reader = nil)
|
2546
|
-
|
2547
|
-
raw_style = attributes[1]
|
2548
|
-
|
2549
|
-
if raw_style && !raw_style.include?(' ') && Compliance.shorthand_property_syntax
|
2550
|
-
type = :style
|
2551
|
-
collector = []
|
2552
|
-
parsed = {}
|
2532
|
+
# NOTE spaces are not allowed in shorthand, so if we detect one, this ain't no shorthand
|
2533
|
+
if (raw_style = attributes[1]) && !raw_style.include?(' ') && Compliance.shorthand_property_syntax
|
2534
|
+
type, collector, parsed = :style, [], {}
|
2553
2535
|
# QUESTION should this be a private method? (though, it's never called if shorthand isn't used)
|
2554
2536
|
save_current = lambda {
|
2555
2537
|
if collector.empty?
|
2556
|
-
|
2557
|
-
warn %(asciidoctor: WARNING:#{reader
|
2538
|
+
unless type == :style
|
2539
|
+
warn %(asciidoctor: WARNING:#{reader ? " #{reader.prev_line_info}:" : nil} invalid empty #{type} detected in style attribute)
|
2558
2540
|
end
|
2559
2541
|
else
|
2560
2542
|
case type
|
2561
2543
|
when :role, :option
|
2562
|
-
parsed[type] ||= []
|
2563
|
-
parsed[type].push collector.join
|
2544
|
+
(parsed[type] ||= []) << collector.join
|
2564
2545
|
when :id
|
2565
|
-
if parsed.
|
2566
|
-
warn %(asciidoctor: WARNING:#{reader
|
2546
|
+
if parsed.key? :id
|
2547
|
+
warn %(asciidoctor: WARNING:#{reader ? " #{reader.prev_line_info}:" : nil} multiple ids detected in style attribute)
|
2567
2548
|
end
|
2568
2549
|
parsed[type] = collector.join
|
2569
2550
|
else
|
@@ -2585,46 +2566,35 @@ class Parser
|
|
2585
2566
|
type = :option
|
2586
2567
|
end
|
2587
2568
|
else
|
2588
|
-
collector
|
2569
|
+
collector << c
|
2589
2570
|
end
|
2590
2571
|
end
|
2591
2572
|
|
2592
2573
|
# small optimization if no shorthand is found
|
2593
2574
|
if type == :style
|
2594
|
-
|
2575
|
+
attributes['style'] = raw_style
|
2595
2576
|
else
|
2596
2577
|
save_current.call
|
2597
2578
|
|
2598
|
-
if parsed.
|
2599
|
-
parsed_style = attributes['style'] = parsed[:style]
|
2600
|
-
else
|
2601
|
-
parsed_style = nil
|
2602
|
-
end
|
2579
|
+
parsed_style = attributes['style'] = parsed[:style] if parsed.key? :style
|
2603
2580
|
|
2604
|
-
if parsed.
|
2605
|
-
attributes['id'] = parsed[:id]
|
2606
|
-
end
|
2581
|
+
attributes['id'] = parsed[:id] if parsed.key? :id
|
2607
2582
|
|
2608
|
-
if parsed.
|
2609
|
-
attributes['role'] = parsed[:role] * ' '
|
2610
|
-
end
|
2583
|
+
attributes['role'] = parsed[:role] * ' ' if parsed.key? :role
|
2611
2584
|
|
2612
|
-
if parsed.
|
2613
|
-
(options = parsed[:option]).each
|
2614
|
-
attributes[%(#{option}-option)] = ''
|
2615
|
-
end
|
2585
|
+
if parsed.key? :option
|
2586
|
+
(options = parsed[:option]).each {|option| attributes[%(#{option}-option)] = '' }
|
2616
2587
|
if (existing_opts = attributes['options'])
|
2617
2588
|
attributes['options'] = (options + existing_opts.split(',')) * ','
|
2618
2589
|
else
|
2619
2590
|
attributes['options'] = options * ','
|
2620
2591
|
end
|
2621
2592
|
end
|
2622
|
-
end
|
2623
2593
|
|
2624
|
-
|
2594
|
+
parsed_style
|
2595
|
+
end
|
2625
2596
|
else
|
2626
2597
|
attributes['style'] = raw_style
|
2627
|
-
[raw_style, original_style]
|
2628
2598
|
end
|
2629
2599
|
end
|
2630
2600
|
|
@@ -2644,16 +2614,16 @@ class Parser
|
|
2644
2614
|
#
|
2645
2615
|
# source = <<EOS
|
2646
2616
|
# def names
|
2647
|
-
# @name.split
|
2617
|
+
# @name.split
|
2648
2618
|
# end
|
2649
2619
|
# EOS
|
2650
2620
|
#
|
2651
2621
|
# source.split "\n"
|
2652
|
-
# # => [" def names", " @names.split
|
2622
|
+
# # => [" def names", " @names.split", " end"]
|
2653
2623
|
#
|
2654
2624
|
# puts Parser.adjust_indentation!(source.split "\n") * "\n"
|
2655
2625
|
# # => def names
|
2656
|
-
# # => @names.split
|
2626
|
+
# # => @names.split
|
2657
2627
|
# # => end
|
2658
2628
|
#
|
2659
2629
|
# returns Nothing
|
@@ -2670,7 +2640,7 @@ class Parser
|
|
2670
2640
|
next line if line.empty?
|
2671
2641
|
|
2672
2642
|
# NOTE Opal has to patch this use of sub!
|
2673
|
-
line.sub!(TabIndentRx) {
|
2643
|
+
line.sub!(TabIndentRx) { full_tab_space * $&.length } if line.start_with? TAB
|
2674
2644
|
|
2675
2645
|
if line.include? TAB
|
2676
2646
|
# keeps track of how many spaces were added to adjust offset in match data
|