asciidoctor 1.5.8 → 2.0.17
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.yardopts +11 -0
- data/CHANGELOG.adoc +628 -45
- data/LICENSE +2 -1
- data/README-de.adoc +28 -38
- data/README-fr.adoc +30 -43
- data/README-jp.adoc +255 -201
- data/README-zh_CN.adoc +40 -44
- data/README.adoc +170 -143
- data/asciidoctor.gemspec +22 -34
- data/bin/asciidoctor +5 -4
- data/data/locale/attributes-ar.adoc +4 -3
- data/data/locale/attributes-be.adoc +23 -0
- data/data/locale/attributes-bg.adoc +4 -3
- data/data/locale/attributes-ca.adoc +6 -5
- data/data/locale/attributes-cs.adoc +4 -3
- data/data/locale/attributes-da.adoc +6 -5
- data/data/locale/attributes-de.adoc +6 -5
- data/data/locale/attributes-en.adoc +4 -4
- data/data/locale/attributes-es.adoc +6 -5
- data/data/locale/attributes-fa.adoc +4 -3
- data/data/locale/attributes-fi.adoc +4 -3
- data/data/locale/attributes-fr.adoc +8 -7
- data/data/locale/attributes-hu.adoc +4 -3
- data/data/locale/attributes-id.adoc +4 -3
- data/data/locale/attributes-it.adoc +6 -5
- data/data/locale/attributes-ja.adoc +4 -3
- data/data/locale/{attributes-kr.adoc → attributes-ko.adoc} +4 -3
- data/data/locale/attributes-nb.adoc +4 -3
- data/data/locale/attributes-nl.adoc +6 -5
- data/data/locale/attributes-nn.adoc +4 -3
- data/data/locale/attributes-pl.adoc +8 -7
- data/data/locale/attributes-pt.adoc +6 -5
- data/data/locale/attributes-pt_BR.adoc +6 -5
- data/data/locale/attributes-ro.adoc +4 -3
- data/data/locale/attributes-ru.adoc +6 -5
- data/data/locale/attributes-sr.adoc +4 -4
- data/data/locale/attributes-sr_Latn.adoc +4 -4
- data/data/locale/attributes-sv.adoc +4 -4
- data/data/locale/attributes-th.adoc +23 -0
- data/data/locale/attributes-tr.adoc +4 -3
- data/data/locale/attributes-uk.adoc +6 -5
- data/data/locale/attributes-vi.adoc +23 -0
- data/data/locale/attributes-zh_CN.adoc +4 -3
- data/data/locale/attributes-zh_TW.adoc +4 -3
- data/data/reference/syntax.adoc +296 -0
- data/data/stylesheets/asciidoctor-default.css +120 -114
- data/data/stylesheets/coderay-asciidoctor.css +15 -17
- data/lib/asciidoctor/abstract_block.rb +146 -140
- data/lib/asciidoctor/abstract_node.rb +152 -170
- data/lib/asciidoctor/attribute_list.rb +77 -89
- data/lib/asciidoctor/block.rb +29 -28
- data/lib/asciidoctor/callouts.rb +4 -2
- data/lib/asciidoctor/cli/invoker.rb +20 -24
- data/lib/asciidoctor/cli/options.rb +107 -96
- data/lib/asciidoctor/cli.rb +3 -2
- data/lib/asciidoctor/convert.rb +199 -0
- data/lib/asciidoctor/converter/composite.rb +40 -48
- data/lib/asciidoctor/converter/docbook5.rb +627 -644
- data/lib/asciidoctor/converter/html5.rb +1053 -951
- data/lib/asciidoctor/converter/manpage.rb +581 -532
- data/lib/asciidoctor/converter/template.rb +232 -271
- data/lib/asciidoctor/converter.rb +370 -185
- data/lib/asciidoctor/core_ext/float/truncate.rb +20 -0
- data/lib/asciidoctor/core_ext/hash/merge.rb +8 -0
- data/lib/asciidoctor/core_ext/match_data/names.rb +7 -0
- data/lib/asciidoctor/core_ext/nil_or_empty.rb +1 -0
- data/lib/asciidoctor/core_ext/regexp/is_match.rb +4 -2
- data/lib/asciidoctor/core_ext.rb +8 -17
- data/lib/asciidoctor/document.rb +503 -461
- data/lib/asciidoctor/extensions.rb +127 -174
- data/lib/asciidoctor/helpers.rb +184 -107
- data/lib/asciidoctor/inline.rb +9 -12
- data/lib/asciidoctor/list.rb +11 -29
- data/lib/asciidoctor/load.rb +119 -0
- data/lib/asciidoctor/logging.rb +22 -17
- data/lib/asciidoctor/parser.rb +673 -719
- data/lib/asciidoctor/path_resolver.rb +48 -33
- data/lib/asciidoctor/reader.rb +383 -338
- data/lib/asciidoctor/rouge_ext.rb +39 -0
- data/lib/asciidoctor/rx.rb +723 -0
- data/lib/asciidoctor/section.rb +17 -16
- data/lib/asciidoctor/stylesheets.rb +19 -37
- data/lib/asciidoctor/substitutors.rb +926 -1022
- data/lib/asciidoctor/syntax_highlighter/coderay.rb +88 -0
- data/lib/asciidoctor/syntax_highlighter/highlightjs.rb +34 -0
- data/lib/asciidoctor/syntax_highlighter/html_pipeline.rb +10 -0
- data/lib/asciidoctor/syntax_highlighter/prettify.rb +30 -0
- data/lib/asciidoctor/syntax_highlighter/pygments.rb +157 -0
- data/lib/asciidoctor/syntax_highlighter/rouge.rb +143 -0
- data/lib/asciidoctor/syntax_highlighter.rb +253 -0
- data/lib/asciidoctor/table.rb +152 -114
- data/lib/asciidoctor/timings.rb +7 -5
- data/lib/asciidoctor/version.rb +2 -1
- data/lib/asciidoctor/writer.rb +30 -0
- data/lib/asciidoctor.rb +266 -1340
- data/man/asciidoctor.1 +49 -47
- data/man/asciidoctor.adoc +54 -45
- metadata +50 -245
- data/CONTRIBUTING.adoc +0 -185
- data/Gemfile +0 -60
- data/Rakefile +0 -129
- data/bin/asciidoctor-safe +0 -15
- data/features/open_block.feature +0 -92
- data/features/pass_block.feature +0 -66
- data/features/step_definitions.rb +0 -49
- data/features/text_formatting.feature +0 -57
- data/features/xref.feature +0 -1039
- data/lib/asciidoctor/converter/base.rb +0 -59
- data/lib/asciidoctor/converter/docbook45.rb +0 -93
- data/lib/asciidoctor/converter/factory.rb +0 -226
- data/lib/asciidoctor/core_ext/1.8.7/base64/strict_encode64.rb +0 -6
- data/lib/asciidoctor/core_ext/1.8.7/concurrent/hash.rb +0 -5
- data/lib/asciidoctor/core_ext/1.8.7/hash/key.rb +0 -4
- data/lib/asciidoctor/core_ext/1.8.7/io/binread.rb +0 -6
- data/lib/asciidoctor/core_ext/1.8.7/io/write.rb +0 -5
- data/lib/asciidoctor/core_ext/1.8.7/string/chr.rb +0 -6
- data/lib/asciidoctor/core_ext/1.8.7/string/limit_bytesize.rb +0 -29
- data/lib/asciidoctor/core_ext/1.8.7/symbol/empty.rb +0 -6
- data/lib/asciidoctor/core_ext/1.8.7/symbol/length.rb +0 -6
- data/lib/asciidoctor/core_ext/string/limit_bytesize.rb +0 -10
- data/test/api_test.rb +0 -1240
- data/test/attribute_list_test.rb +0 -242
- data/test/attributes_test.rb +0 -1623
- data/test/blocks_test.rb +0 -3870
- data/test/converter_test.rb +0 -470
- data/test/document_test.rb +0 -1853
- data/test/extensions_test.rb +0 -1560
- data/test/fixtures/asciidoc_index.txt +0 -521
- data/test/fixtures/basic-docinfo-footer.html +0 -6
- data/test/fixtures/basic-docinfo-footer.xml +0 -8
- data/test/fixtures/basic-docinfo.html +0 -1
- data/test/fixtures/basic-docinfo.xml +0 -4
- data/test/fixtures/basic.asciidoc +0 -5
- data/test/fixtures/chapter-a.adoc +0 -3
- data/test/fixtures/child-include.adoc +0 -5
- data/test/fixtures/circle.svg +0 -9
- data/test/fixtures/custom-backends/erb/html5/block_paragraph.html.erb +0 -6
- data/test/fixtures/custom-backends/haml/docbook45/block_paragraph.xml.haml +0 -6
- data/test/fixtures/custom-backends/haml/html5/block_paragraph.html.haml +0 -3
- data/test/fixtures/custom-backends/haml/html5/block_sidebar.html.haml +0 -5
- data/test/fixtures/custom-backends/haml/html5-tweaks/block_paragraph.html.haml +0 -1
- data/test/fixtures/custom-backends/slim/docbook45/block_paragraph.xml.slim +0 -6
- data/test/fixtures/custom-backends/slim/html5/block_paragraph.html.slim +0 -3
- data/test/fixtures/custom-backends/slim/html5/block_sidebar.html.slim +0 -5
- data/test/fixtures/custom-docinfodir/basic-docinfo.html +0 -1
- data/test/fixtures/custom-docinfodir/docinfo.html +0 -1
- data/test/fixtures/docinfo-footer.html +0 -1
- data/test/fixtures/docinfo-footer.xml +0 -9
- data/test/fixtures/docinfo.html +0 -1
- data/test/fixtures/docinfo.xml +0 -3
- data/test/fixtures/doctime-localtime.adoc +0 -2
- data/test/fixtures/dot.gif +0 -0
- data/test/fixtures/encoding.asciidoc +0 -13
- data/test/fixtures/file-with-missing-include.adoc +0 -1
- data/test/fixtures/grandchild-include.adoc +0 -3
- data/test/fixtures/hello-asciidoctor.pdf +0 -69
- data/test/fixtures/include-file.asciidoc +0 -24
- data/test/fixtures/include-file.jsx +0 -8
- data/test/fixtures/include-file.ml +0 -3
- data/test/fixtures/include-file.xml +0 -5
- data/test/fixtures/lists.adoc +0 -96
- data/test/fixtures/master.adoc +0 -5
- data/test/fixtures/mismatched-end-tag.adoc +0 -7
- data/test/fixtures/other-chapters.adoc +0 -11
- data/test/fixtures/outer-include.adoc +0 -5
- data/test/fixtures/parent-include-restricted.adoc +0 -5
- data/test/fixtures/parent-include.adoc +0 -5
- data/test/fixtures/sample.asciidoc +0 -30
- data/test/fixtures/section-a.adoc +0 -4
- data/test/fixtures/stylesheets/custom.css +0 -3
- data/test/fixtures/subdir/index.adoc +0 -3
- data/test/fixtures/subdir/inner-include.adoc +0 -3
- data/test/fixtures/subdir/middle-include.adoc +0 -5
- data/test/fixtures/subs-docinfo.html +0 -2
- data/test/fixtures/subs.adoc +0 -6
- data/test/fixtures/tagged-class-enclosed.rb +0 -25
- data/test/fixtures/tagged-class.rb +0 -23
- data/test/fixtures/tip.gif +0 -0
- data/test/fixtures/unclosed-tag.adoc +0 -3
- data/test/fixtures/unexpected-end-tag.adoc +0 -4
- data/test/invoker_test.rb +0 -745
- data/test/links_test.rb +0 -855
- data/test/lists_test.rb +0 -5151
- data/test/logger_test.rb +0 -211
- data/test/manpage_test.rb +0 -660
- data/test/options_test.rb +0 -262
- data/test/paragraphs_test.rb +0 -562
- data/test/parser_test.rb +0 -742
- data/test/paths_test.rb +0 -395
- data/test/preamble_test.rb +0 -173
- data/test/reader_test.rb +0 -2161
- data/test/sections_test.rb +0 -3575
- data/test/substitutions_test.rb +0 -2066
- data/test/tables_test.rb +0 -2036
- data/test/test_helper.rb +0 -447
- data/test/text_test.rb +0 -309
data/lib/asciidoctor/parser.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
|
-
#
|
1
|
+
# frozen_string_literal: true
|
2
2
|
module Asciidoctor
|
3
|
-
#
|
3
|
+
# Internal: Methods to parse lines of AsciiDoc into an object hierarchy
|
4
4
|
# representing the structure of the document. All methods are class methods and
|
5
5
|
# should be invoked from the Parser class. The main entry point is ::next_block.
|
6
6
|
# No Parser instances shall be discovered running around. (Any attempt to
|
@@ -27,20 +27,22 @@ class Parser
|
|
27
27
|
|
28
28
|
BlockMatchData = Struct.new :context, :masq, :tip, :terminator
|
29
29
|
|
30
|
-
#
|
31
|
-
|
30
|
+
# String for matching tab character
|
31
|
+
TAB = ?\t
|
32
32
|
|
33
33
|
# Regexp for leading tab indentation
|
34
34
|
TabIndentRx = /^\t+/
|
35
35
|
|
36
|
-
StartOfBlockProc =
|
36
|
+
StartOfBlockProc = proc {|l| ((l.start_with? '[') && (BlockAttributeLineRx.match? l)) || (is_delimited_block? l) }
|
37
37
|
|
38
|
-
StartOfListProc =
|
38
|
+
StartOfListProc = proc {|l| AnyListRx.match? l }
|
39
39
|
|
40
|
-
StartOfBlockOrListProc =
|
40
|
+
StartOfBlockOrListProc = proc {|l| (is_delimited_block? l) || ((l.start_with? '[') && (BlockAttributeLineRx.match? l)) || (AnyListRx.match? l) }
|
41
41
|
|
42
42
|
NoOp = nil
|
43
43
|
|
44
|
+
AuthorKeys = ['author', 'authorinitials', 'firstname', 'middlename', 'lastname', 'email']
|
45
|
+
|
44
46
|
# Internal: A Hash mapping horizontal alignment abbreviations to alignments
|
45
47
|
# that can be applied to a table cell (or to all cells in a column)
|
46
48
|
TableCellHorzAlignments = {
|
@@ -66,16 +68,13 @@ class Parser
|
|
66
68
|
'm' => :monospaced,
|
67
69
|
'h' => :header,
|
68
70
|
'l' => :literal,
|
69
|
-
'v' => :verse,
|
70
71
|
'a' => :asciidoc
|
71
72
|
}
|
72
73
|
|
73
|
-
#
|
74
|
+
# Hide the default constructor to make sure this class doesn't get instantiated.
|
74
75
|
#
|
75
|
-
# Raises
|
76
|
-
|
77
|
-
raise 'Au contraire, mon frere. No parser instances will be running around.'
|
78
|
-
end
|
76
|
+
# Raises NoMethodError if an attempt is made to invoke the constructor.
|
77
|
+
private_class_method :new
|
79
78
|
|
80
79
|
# Public: Parses AsciiDoc source read from the Reader into the Document
|
81
80
|
#
|
@@ -90,10 +89,10 @@ class Parser
|
|
90
89
|
#
|
91
90
|
# returns the Document object
|
92
91
|
def self.parse(reader, document, options = {})
|
93
|
-
block_attributes = parse_document_header(reader, document)
|
92
|
+
block_attributes = parse_document_header(reader, document, (header_only = options[:header_only]))
|
94
93
|
|
95
94
|
# NOTE don't use a postfix conditional here as it's known to confuse JRuby in certain circumstances
|
96
|
-
unless
|
95
|
+
unless header_only
|
97
96
|
while reader.has_more_lines?
|
98
97
|
new_section, block_attributes = next_section(reader, document, block_attributes)
|
99
98
|
if new_section
|
@@ -118,61 +117,82 @@ class Parser
|
|
118
117
|
# which are automatically removed by the reader.
|
119
118
|
#
|
120
119
|
# returns the Hash of orphan block attributes captured above the header
|
121
|
-
def self.parse_document_header(reader, document)
|
120
|
+
def self.parse_document_header(reader, document, header_only = false)
|
122
121
|
# capture lines of block-level metadata and plow away comment lines that precede first block
|
123
|
-
block_attrs = parse_block_metadata_lines reader, document
|
122
|
+
block_attrs = reader.skip_blank_lines ? (parse_block_metadata_lines reader, document) : {}
|
124
123
|
doc_attrs = document.attributes
|
125
124
|
|
126
125
|
# special case, block title is not allowed above document title,
|
127
126
|
# carry attributes over to the document body
|
128
127
|
if (implicit_doctitle = is_next_line_doctitle? reader, block_attrs, doc_attrs['leveloffset']) && block_attrs['title']
|
128
|
+
doc_attrs['authorcount'] = 0
|
129
129
|
return document.finalize_header block_attrs, false
|
130
130
|
end
|
131
131
|
|
132
132
|
# yep, document title logic in AsciiDoc is just insanity
|
133
133
|
# definitely an area for spec refinement
|
134
|
-
|
134
|
+
|
135
135
|
unless (val = doc_attrs['doctitle']).nil_or_empty?
|
136
|
-
document.title =
|
136
|
+
document.title = doctitle_attr_val = val
|
137
137
|
end
|
138
138
|
|
139
139
|
# if the first line is the document title, add a header to the document and parse the header metadata
|
140
140
|
if implicit_doctitle
|
141
141
|
source_location = reader.cursor if document.sourcemap
|
142
|
-
document.id, _,
|
143
|
-
|
142
|
+
document.id, _, l0_section_title, _, atx = parse_section_title reader, document
|
143
|
+
if doctitle_attr_val
|
144
|
+
# NOTE doctitle attribute (set above or below implicit doctitle) overrides implicit doctitle
|
145
|
+
l0_section_title = nil
|
146
|
+
else
|
147
|
+
document.title = l0_section_title
|
148
|
+
if (doc_attrs['doctitle'] = doctitle_attr_val = document.sub_specialchars l0_section_title).include? ATTR_REF_HEAD
|
149
|
+
# QUESTION should we defer substituting attributes until the end of the header? or should we substitute again if necessary?
|
150
|
+
doc_attrs['doctitle'] = doctitle_attr_val = document.sub_attributes doctitle_attr_val, attribute_missing: 'skip'
|
151
|
+
end
|
152
|
+
end
|
144
153
|
document.header.source_location = source_location if source_location
|
145
|
-
# default to compat-mode if document
|
154
|
+
# default to compat-mode if document has setext doctitle
|
146
155
|
doc_attrs['compat-mode'] = '' unless atx || (document.attribute_locked? 'compat-mode')
|
147
156
|
if (separator = block_attrs['separator'])
|
148
157
|
doc_attrs['title-separator'] = separator unless document.attribute_locked? 'title-separator'
|
149
158
|
end
|
150
|
-
doc_attrs['doctitle'] = section_title = doctitle
|
151
159
|
if (doc_id = block_attrs['id'])
|
152
160
|
document.id = doc_id
|
153
161
|
else
|
154
162
|
doc_id = document.id
|
155
163
|
end
|
156
|
-
if (
|
157
|
-
doc_attrs['
|
164
|
+
if (role = block_attrs['role'])
|
165
|
+
doc_attrs['role'] = role
|
166
|
+
end
|
167
|
+
if (reftext = block_attrs['reftext'])
|
168
|
+
doc_attrs['reftext'] = reftext
|
158
169
|
end
|
159
|
-
|
160
|
-
|
170
|
+
block_attrs.clear
|
171
|
+
(modified_attrs = document.instance_variable_get :@attributes_modified).delete 'doctitle'
|
172
|
+
parse_header_metadata reader, document, nil
|
173
|
+
if modified_attrs.include? 'doctitle'
|
174
|
+
if (val = doc_attrs['doctitle']).nil_or_empty? || val == doctitle_attr_val
|
175
|
+
doc_attrs['doctitle'] = doctitle_attr_val
|
176
|
+
else
|
177
|
+
document.title = val
|
178
|
+
end
|
179
|
+
elsif !l0_section_title
|
180
|
+
modified_attrs << 'doctitle'
|
161
181
|
end
|
162
|
-
block_attrs = {}
|
163
|
-
parse_header_metadata reader, document
|
164
182
|
document.register :refs, [doc_id, document] if doc_id
|
183
|
+
elsif (author = doc_attrs['author'])
|
184
|
+
author_metadata = process_authors author, true, false
|
185
|
+
author_metadata.delete 'authorinitials' if doc_attrs['authorinitials']
|
186
|
+
doc_attrs.update author_metadata
|
187
|
+
elsif (author = doc_attrs['authors'])
|
188
|
+
author_metadata = process_authors author, true
|
189
|
+
doc_attrs.update author_metadata
|
190
|
+
else
|
191
|
+
doc_attrs['authorcount'] = 0
|
165
192
|
end
|
166
193
|
|
167
|
-
unless (val = doc_attrs['doctitle']).nil_or_empty? || val == section_title
|
168
|
-
document.title = assigned_doctitle = val
|
169
|
-
end
|
170
|
-
|
171
|
-
# restore doctitle attribute to original assignment
|
172
|
-
doc_attrs['doctitle'] = assigned_doctitle if assigned_doctitle
|
173
|
-
|
174
194
|
# parse title and consume name section of manpage document
|
175
|
-
parse_manpage_header
|
195
|
+
parse_manpage_header reader, document, block_attrs, header_only if document.doctype == 'manpage'
|
176
196
|
|
177
197
|
# NOTE block_attrs are the block-level attributes (not document attributes) that
|
178
198
|
# precede the first line of content (document title, first section or first block)
|
@@ -182,12 +202,12 @@ class Parser
|
|
182
202
|
# Public: Parses the manpage header of the AsciiDoc source read from the Reader
|
183
203
|
#
|
184
204
|
# returns Nothing
|
185
|
-
def self.parse_manpage_header(reader, document, block_attributes)
|
205
|
+
def self.parse_manpage_header(reader, document, block_attributes, header_only = false)
|
186
206
|
if ManpageTitleVolnumRx =~ (doc_attrs = document.attributes)['doctitle']
|
187
207
|
doc_attrs['manvolnum'] = manvolnum = $2
|
188
208
|
doc_attrs['mantitle'] = (((mantitle = $1).include? ATTR_REF_HEAD) ? (document.sub_attributes mantitle) : mantitle).downcase
|
189
209
|
else
|
190
|
-
logger.error message_with_context 'non-conforming manpage title', :
|
210
|
+
logger.error message_with_context 'non-conforming manpage title', source_location: (reader.cursor_at_line 1)
|
191
211
|
# provide sensible fallbacks
|
192
212
|
doc_attrs['mantitle'] = doc_attrs['doctitle'] || doc_attrs['docname'] || 'command'
|
193
213
|
doc_attrs['manvolnum'] = manvolnum = '1'
|
@@ -199,6 +219,8 @@ class Parser
|
|
199
219
|
doc_attrs['docname'] = manname
|
200
220
|
doc_attrs['outfilesuffix'] = %(.#{manvolnum})
|
201
221
|
end
|
222
|
+
elsif header_only
|
223
|
+
# done
|
202
224
|
else
|
203
225
|
reader.skip_blank_lines
|
204
226
|
reader.save
|
@@ -206,11 +228,8 @@ class Parser
|
|
206
228
|
if (name_section_level = is_next_line_section? reader, {})
|
207
229
|
if name_section_level == 1
|
208
230
|
name_section = initialize_section reader, document, {}
|
209
|
-
name_section_buffer = (reader.read_lines_until :
|
231
|
+
name_section_buffer = (reader.read_lines_until break_on_blank_lines: true, skip_line_comments: true).map {|l| l.lstrip }.join ' '
|
210
232
|
if ManpageNamePurposeRx =~ name_section_buffer
|
211
|
-
doc_attrs['manname-title'] ||= name_section.title
|
212
|
-
doc_attrs['manname-id'] = name_section.id if name_section.id
|
213
|
-
doc_attrs['manpurpose'] = $2
|
214
233
|
if (manname = $1).include? ATTR_REF_HEAD
|
215
234
|
manname = document.sub_attributes manname
|
216
235
|
end
|
@@ -219,8 +238,14 @@ class Parser
|
|
219
238
|
else
|
220
239
|
mannames = [manname]
|
221
240
|
end
|
241
|
+
if (manpurpose = $2).include? ATTR_REF_HEAD
|
242
|
+
manpurpose = document.sub_attributes manpurpose
|
243
|
+
end
|
244
|
+
doc_attrs['manname-title'] ||= name_section.title
|
245
|
+
doc_attrs['manname-id'] = name_section.id if name_section.id
|
222
246
|
doc_attrs['manname'] = manname
|
223
247
|
doc_attrs['mannames'] = mannames
|
248
|
+
doc_attrs['manpurpose'] = manpurpose
|
224
249
|
if document.backend == 'manpage'
|
225
250
|
doc_attrs['docname'] = manname
|
226
251
|
doc_attrs['outfilesuffix'] = %(.#{manvolnum})
|
@@ -236,8 +261,8 @@ class Parser
|
|
236
261
|
end
|
237
262
|
if error_msg
|
238
263
|
reader.restore_save
|
239
|
-
logger.error message_with_context error_msg, :
|
240
|
-
doc_attrs['manname'] =
|
264
|
+
logger.error message_with_context error_msg, source_location: reader.cursor
|
265
|
+
doc_attrs['manname'] = manname = doc_attrs['docname'] || 'command'
|
241
266
|
doc_attrs['mannames'] = [manname]
|
242
267
|
if document.backend == 'manpage'
|
243
268
|
doc_attrs['docname'] = manname
|
@@ -275,7 +300,7 @@ class Parser
|
|
275
300
|
# source
|
276
301
|
# # => "= Greetings\n\nThis is my doc.\n\n== Salutations\n\nIt is awesome."
|
277
302
|
#
|
278
|
-
# reader = Reader.new source, nil, :
|
303
|
+
# reader = Reader.new source, nil, normalize: true
|
279
304
|
# # create empty document to parent the section
|
280
305
|
# # and hold attributes extracted from header
|
281
306
|
# doc = Document.new
|
@@ -293,11 +318,11 @@ class Parser
|
|
293
318
|
# check if we are at the start of processing the document
|
294
319
|
# NOTE we could drop a hint in the attributes to indicate
|
295
320
|
# that we are at a section title (so we don't have to check)
|
296
|
-
if parent.context == :document && parent.blocks.empty? && ((has_header = parent.
|
321
|
+
if parent.context == :document && parent.blocks.empty? && ((has_header = parent.header?) ||
|
297
322
|
(attributes.delete 'invalid-header') || !(is_next_line_section? reader, attributes))
|
298
323
|
book = (document = parent).doctype == 'book'
|
299
324
|
if has_header || (book && attributes[1] != 'abstract')
|
300
|
-
preamble = intro =
|
325
|
+
preamble = intro = Block.new parent, :preamble, content_model: :compound
|
301
326
|
preamble.title = parent.attr 'preface-title' if book && (parent.attr? 'preface-title')
|
302
327
|
parent.blocks << preamble
|
303
328
|
end
|
@@ -320,7 +345,7 @@ class Parser
|
|
320
345
|
if current_level == 0
|
321
346
|
part = book
|
322
347
|
elsif current_level == 1 && section.special
|
323
|
-
# NOTE technically preface
|
348
|
+
# NOTE technically preface sections are only permitted in the book doctype
|
324
349
|
unless (sectname = section.sectname) == 'appendix' || sectname == 'preface' || sectname == 'abstract'
|
325
350
|
expected_next_level = nil
|
326
351
|
end
|
@@ -341,21 +366,24 @@ class Parser
|
|
341
366
|
while reader.has_more_lines?
|
342
367
|
parse_block_metadata_lines reader, document, attributes
|
343
368
|
if (next_level = is_next_line_section?(reader, attributes))
|
344
|
-
|
369
|
+
if document.attr? 'leveloffset'
|
370
|
+
next_level += (document.attr 'leveloffset').to_i
|
371
|
+
next_level = 0 if next_level < 0
|
372
|
+
end
|
345
373
|
if next_level > current_level
|
346
374
|
if expected_next_level
|
347
375
|
unless next_level == expected_next_level || (expected_next_level_alt && next_level == expected_next_level_alt) || expected_next_level < 0
|
348
376
|
expected_condition = expected_next_level_alt ? %(expected levels #{expected_next_level_alt} or #{expected_next_level}) : %(expected level #{expected_next_level})
|
349
|
-
logger.warn message_with_context %(section title out of sequence: #{expected_condition}, got level #{next_level}), :
|
377
|
+
logger.warn message_with_context %(section title out of sequence: #{expected_condition}, got level #{next_level}), source_location: reader.cursor
|
350
378
|
end
|
351
379
|
else
|
352
|
-
logger.error message_with_context %(#{sectname} sections do not support nested sections), :
|
380
|
+
logger.error message_with_context %(#{sectname} sections do not support nested sections), source_location: reader.cursor
|
353
381
|
end
|
354
382
|
new_section, attributes = next_section reader, section, attributes
|
355
383
|
section.assign_numeral new_section
|
356
384
|
section.blocks << new_section
|
357
385
|
elsif next_level == 0 && section == document
|
358
|
-
logger.error message_with_context 'level 0 sections can only be used when doctype is book', :
|
386
|
+
logger.error message_with_context 'level 0 sections can only be used when doctype is book', source_location: reader.cursor unless book
|
359
387
|
new_section, attributes = next_section reader, section, attributes
|
360
388
|
section.assign_numeral new_section
|
361
389
|
section.blocks << new_section
|
@@ -366,7 +394,7 @@ class Parser
|
|
366
394
|
else
|
367
395
|
# just take one block or else we run the risk of overrunning section boundaries
|
368
396
|
block_cursor = reader.cursor
|
369
|
-
if (new_block = next_block reader, intro || section, attributes, :
|
397
|
+
if (new_block = next_block reader, intro || section, attributes, parse_metadata: false)
|
370
398
|
# REVIEW this may be doing too much
|
371
399
|
if part
|
372
400
|
if !section.blocks?
|
@@ -378,7 +406,7 @@ class Parser
|
|
378
406
|
new_block.style = 'partintro'
|
379
407
|
# emulate [partintro] open block
|
380
408
|
else
|
381
|
-
new_block.parent = (intro = Block.new section, :open, :
|
409
|
+
new_block.parent = (intro = Block.new section, :open, content_model: :compound)
|
382
410
|
intro.style = 'partintro'
|
383
411
|
section.blocks << intro
|
384
412
|
end
|
@@ -387,10 +415,10 @@ class Parser
|
|
387
415
|
first_block = section.blocks[0]
|
388
416
|
# open the [partintro] open block for appending
|
389
417
|
if !intro && first_block.content_model == :compound
|
390
|
-
logger.error message_with_context 'illegal block content outside of partintro block', :
|
418
|
+
logger.error message_with_context 'illegal block content outside of partintro block', source_location: block_cursor
|
391
419
|
# rebuild [partintro] paragraph as an open block
|
392
420
|
elsif first_block.content_model != :compound
|
393
|
-
new_block.parent = (intro = Block.new section, :open, :
|
421
|
+
new_block.parent = (intro = Block.new section, :open, content_model: :compound)
|
394
422
|
intro.style = 'partintro'
|
395
423
|
section.blocks.shift
|
396
424
|
if first_block.style == 'partintro'
|
@@ -404,10 +432,7 @@ class Parser
|
|
404
432
|
end
|
405
433
|
|
406
434
|
(intro || section).blocks << new_block
|
407
|
-
attributes
|
408
|
-
#else
|
409
|
-
# # don't clear attributes if we don't find a block because they may
|
410
|
-
# # be trailing attributes that didn't get associated with a block
|
435
|
+
attributes.clear
|
411
436
|
end
|
412
437
|
end
|
413
438
|
|
@@ -416,15 +441,17 @@ class Parser
|
|
416
441
|
|
417
442
|
if part
|
418
443
|
unless section.blocks? && section.blocks[-1].context == :section
|
419
|
-
logger.error message_with_context 'invalid part, must have at least one section (e.g., chapter, appendix, etc.)', :
|
444
|
+
logger.error message_with_context 'invalid part, must have at least one section (e.g., chapter, appendix, etc.)', source_location: reader.cursor
|
420
445
|
end
|
421
446
|
# NOTE we could try to avoid creating a preamble in the first place, though
|
422
447
|
# that would require reworking assumptions in next_section since the preamble
|
423
448
|
# is treated like an untitled section
|
424
449
|
elsif preamble # implies parent == document
|
425
450
|
if preamble.blocks?
|
451
|
+
if book || document.blocks[1] || !Compliance.unwrap_standalone_preamble
|
452
|
+
preamble.source_location = preamble.blocks[0].source_location if document.sourcemap
|
426
453
|
# unwrap standalone preamble (i.e., document has no sections) except for books, if permissible
|
427
|
-
|
454
|
+
else
|
428
455
|
document.blocks.shift
|
429
456
|
while (child_block = preamble.blocks.shift)
|
430
457
|
document << child_block
|
@@ -437,10 +464,10 @@ class Parser
|
|
437
464
|
end
|
438
465
|
|
439
466
|
# The attributes returned here are orphaned attributes that fall at the end
|
440
|
-
# of a section that need to get
|
467
|
+
# of a section that need to get transferred to the next section
|
441
468
|
# see "trailing block attributes transfer to the following section" in
|
442
469
|
# test/attributes_test.rb for an example
|
443
|
-
[section
|
470
|
+
[section == parent ? nil : section, attributes.merge]
|
444
471
|
end
|
445
472
|
|
446
473
|
# Public: Parse and return the next Block at the Reader's current location
|
@@ -459,7 +486,8 @@ class Parser
|
|
459
486
|
# attributes - A Hash of attributes that will become the attributes
|
460
487
|
# associated with the parsed Block (default: {}).
|
461
488
|
# options - An options Hash to control parsing (default: {}):
|
462
|
-
# * :
|
489
|
+
# * :text_only indicates that the parser is only looking for text content
|
490
|
+
# * :list_type indicates this block will be attached to a list item in a list of the specified type
|
463
491
|
#
|
464
492
|
# Returns a Block object built from the parsed content of the processed
|
465
493
|
# lines, or nothing if no block is found.
|
@@ -470,15 +498,15 @@ class Parser
|
|
470
498
|
# check for option to find list item text only
|
471
499
|
# if skipped a line, assume a list continuation was
|
472
500
|
# used and block content is acceptable
|
473
|
-
if (text_only = options[:
|
474
|
-
options.delete :
|
475
|
-
text_only =
|
501
|
+
if (text_only = options[:text_only]) && skipped > 0
|
502
|
+
options.delete :text_only
|
503
|
+
text_only = nil
|
476
504
|
end
|
477
505
|
|
478
506
|
document = parent.document
|
479
507
|
|
480
508
|
if options.fetch :parse_metadata, true
|
481
|
-
# read lines until there are no more metadata lines to read
|
509
|
+
# read lines until there are no more metadata lines to read; note that :text_only option impacts parsing rules
|
482
510
|
while parse_block_metadata_line reader, document, attributes, options
|
483
511
|
# discard the line just processed
|
484
512
|
reader.shift
|
@@ -499,19 +527,21 @@ class Parser
|
|
499
527
|
if (delimited_block = is_delimited_block? this_line, true)
|
500
528
|
block_context = cloaked_context = delimited_block.context
|
501
529
|
terminator = delimited_block.terminator
|
502
|
-
if
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
530
|
+
if style
|
531
|
+
unless style == block_context.to_s
|
532
|
+
if delimited_block.masq.include? style
|
533
|
+
block_context = style.to_sym
|
534
|
+
elsif delimited_block.masq.include?('admonition') && ADMONITION_STYLES.include?(style)
|
535
|
+
block_context = :admonition
|
536
|
+
elsif block_extensions && extensions.registered_for_block?(style, block_context)
|
537
|
+
block_context = style.to_sym
|
538
|
+
else
|
539
|
+
logger.debug message_with_context %(unknown style for #{block_context} block: #{style}), source_location: reader.cursor_at_mark if logger.debug?
|
540
|
+
style = block_context.to_s
|
541
|
+
end
|
514
542
|
end
|
543
|
+
else
|
544
|
+
style = attributes['style'] = block_context.to_s
|
515
545
|
end
|
516
546
|
end
|
517
547
|
|
@@ -520,7 +550,7 @@ class Parser
|
|
520
550
|
# returns nil if the line should be dropped
|
521
551
|
while true
|
522
552
|
# process lines verbatim
|
523
|
-
if style && Compliance.strict_verbatim_paragraphs && VERBATIM_STYLES.include?
|
553
|
+
if style && Compliance.strict_verbatim_paragraphs && (VERBATIM_STYLES.include? style)
|
524
554
|
block_context = style.to_sym
|
525
555
|
reader.unshift_line this_line
|
526
556
|
# advance to block parsing =>
|
@@ -540,7 +570,7 @@ class Parser
|
|
540
570
|
#!(this_line.start_with? ' ') &&
|
541
571
|
(MarkdownThematicBreakRx.match? this_line)
|
542
572
|
# NOTE we're letting break lines (horizontal rule, page_break, etc) have attributes
|
543
|
-
block = Block.new(parent, :thematic_break, :
|
573
|
+
block = Block.new(parent, :thematic_break, content_model: :empty)
|
544
574
|
break
|
545
575
|
end
|
546
576
|
elsif this_line.start_with? TAB
|
@@ -548,17 +578,17 @@ class Parser
|
|
548
578
|
else
|
549
579
|
indented, ch0 = false, this_line.chr
|
550
580
|
layout_break_chars = md_syntax ? HYBRID_LAYOUT_BREAK_CHARS : LAYOUT_BREAK_CHARS
|
551
|
-
if (layout_break_chars.key? ch0) &&
|
552
|
-
(this_line
|
581
|
+
if (layout_break_chars.key? ch0) &&
|
582
|
+
(md_syntax ? (ExtLayoutBreakRx.match? this_line) : (uniform? this_line, ch0, (ll = this_line.length)) && ll > 2)
|
553
583
|
# NOTE we're letting break lines (horizontal rule, page_break, etc) have attributes
|
554
|
-
block = Block.new(parent, layout_break_chars[ch0], :
|
584
|
+
block = Block.new(parent, layout_break_chars[ch0], content_model: :empty)
|
555
585
|
break
|
556
586
|
# NOTE very rare that a text-only line will end in ] (e.g., inline macro), so check that first
|
557
587
|
elsif (this_line.end_with? ']') && (this_line.include? '::')
|
558
588
|
#if (this_line.start_with? 'image', 'video', 'audio') && BlockMediaMacroRx =~ this_line
|
559
589
|
if (ch0 == 'i' || (this_line.start_with? 'video:', 'audio:')) && BlockMediaMacroRx =~ this_line
|
560
590
|
blk_ctx, target, blk_attrs = $1.to_sym, $2, $3
|
561
|
-
block = Block.new parent, blk_ctx, :
|
591
|
+
block = Block.new parent, blk_ctx, content_model: :empty
|
562
592
|
if blk_attrs
|
563
593
|
case blk_ctx
|
564
594
|
when :video
|
@@ -568,63 +598,74 @@ class Parser
|
|
568
598
|
else # :image
|
569
599
|
posattrs = ['alt', 'width', 'height']
|
570
600
|
end
|
571
|
-
block.parse_attributes blk_attrs, posattrs, :
|
601
|
+
block.parse_attributes blk_attrs, posattrs, sub_input: true, into: attributes
|
572
602
|
end
|
573
603
|
# style doesn't have special meaning for media macros
|
574
604
|
attributes.delete 'style' if attributes.key? 'style'
|
575
|
-
if
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
# otherwise, drop the line
|
580
|
-
else
|
605
|
+
if target.include? ATTR_REF_HEAD
|
606
|
+
if (expanded_target = block.sub_attributes target).empty? &&
|
607
|
+
(doc_attrs['attribute-missing'] || Compliance.attribute_missing) == 'drop-line' &&
|
608
|
+
(block.sub_attributes target + ' ', attribute_missing: 'drop-line', drop_line_severity: :ignore).empty?
|
581
609
|
attributes.clear
|
582
610
|
return
|
611
|
+
else
|
612
|
+
target = expanded_target
|
583
613
|
end
|
584
614
|
end
|
585
615
|
if blk_ctx == :image
|
586
|
-
document.register :images,
|
616
|
+
document.register :images, target
|
617
|
+
attributes['imagesdir'] = doc_attrs['imagesdir']
|
587
618
|
# NOTE style is the value of the first positional attribute in the block attribute line
|
588
619
|
attributes['alt'] ||= style || (attributes['default-alt'] = Helpers.basename(target, true).tr('_-', ' '))
|
589
620
|
unless (scaledwidth = attributes.delete 'scaledwidth').nil_or_empty?
|
590
621
|
# NOTE assume % units if not specified
|
591
622
|
attributes['scaledwidth'] = (TrailingDigitsRx.match? scaledwidth) ? %(#{scaledwidth}%) : scaledwidth
|
592
623
|
end
|
593
|
-
if attributes
|
594
|
-
block.title = attributes.delete 'title'
|
595
|
-
block.assign_caption(
|
624
|
+
if attributes['title']
|
625
|
+
block.title = block_title = attributes.delete 'title'
|
626
|
+
block.assign_caption (attributes.delete 'caption'), 'figure'
|
596
627
|
end
|
597
628
|
end
|
598
629
|
attributes['target'] = target
|
599
630
|
break
|
600
631
|
|
601
632
|
elsif ch0 == 't' && (this_line.start_with? 'toc:') && BlockTocMacroRx =~ this_line
|
602
|
-
block = Block.new parent, :toc, :
|
603
|
-
block.parse_attributes $1, [], :
|
633
|
+
block = Block.new parent, :toc, content_model: :empty
|
634
|
+
block.parse_attributes $1, [], into: attributes if $1
|
604
635
|
break
|
605
636
|
|
606
|
-
elsif block_macro_extensions
|
607
|
-
(extension = extensions.registered_for_block_macro? $1)
|
608
|
-
|
609
|
-
if
|
610
|
-
(
|
611
|
-
attributes.clear
|
612
|
-
return
|
613
|
-
end
|
614
|
-
if extension.config[:content_model] == :attributes
|
615
|
-
document.parse_attributes content, extension.config[:pos_attrs] || [], :sub_input => true, :into => attributes if content
|
616
|
-
else
|
617
|
-
attributes['text'] = content || ''
|
618
|
-
end
|
619
|
-
if (default_attrs = extension.config[:default_attrs])
|
620
|
-
attributes.update(default_attrs) {|_, old_v| old_v }
|
621
|
-
end
|
622
|
-
if (block = extension.process_method[parent, target, attributes])
|
623
|
-
attributes.replace block.attributes
|
624
|
-
break
|
637
|
+
elsif block_macro_extensions ? (CustomBlockMacroRx =~ this_line &&
|
638
|
+
(extension = extensions.registered_for_block_macro? $1) || (report_unknown_block_macro = logger.debug?)) :
|
639
|
+
(logger.debug? && (report_unknown_block_macro = CustomBlockMacroRx =~ this_line))
|
640
|
+
if report_unknown_block_macro
|
641
|
+
logger.debug message_with_context %(unknown name for block macro: #{$1}), source_location: reader.cursor_at_mark
|
625
642
|
else
|
626
|
-
|
627
|
-
|
643
|
+
content = $3
|
644
|
+
if (target = $2).include? ATTR_REF_HEAD
|
645
|
+
if (expanded_target = parent.sub_attributes target).empty? &&
|
646
|
+
(doc_attrs['attribute-missing'] || Compliance.attribute_missing) == 'drop-line' &&
|
647
|
+
(parent.sub_attributes target + ' ', attribute_missing: 'drop-line', drop_line_severity: :ignore).empty?
|
648
|
+
attributes.clear
|
649
|
+
return
|
650
|
+
else
|
651
|
+
target = expanded_target
|
652
|
+
end
|
653
|
+
end
|
654
|
+
if (ext_config = extension.config)[:content_model] == :attributes
|
655
|
+
document.parse_attributes content, ext_config[:positional_attrs] || ext_config[:pos_attrs] || [], sub_input: true, into: attributes if content
|
656
|
+
else
|
657
|
+
attributes['text'] = content || ''
|
658
|
+
end
|
659
|
+
if (default_attrs = ext_config[:default_attrs])
|
660
|
+
attributes.update(default_attrs) {|_, old_v| old_v }
|
661
|
+
end
|
662
|
+
if (block = extension.process_method[parent, target, attributes]) && block != parent
|
663
|
+
attributes.replace block.attributes
|
664
|
+
break
|
665
|
+
else
|
666
|
+
attributes.clear
|
667
|
+
return
|
668
|
+
end
|
628
669
|
end
|
629
670
|
end
|
630
671
|
end
|
@@ -640,28 +681,28 @@ class Parser
|
|
640
681
|
|
641
682
|
elsif UnorderedListRx.match? this_line
|
642
683
|
reader.unshift_line this_line
|
643
|
-
attributes['style'] =
|
684
|
+
attributes['style'] = style = 'bibliography' if !style && Section === parent && parent.sectname == 'bibliography'
|
644
685
|
block = parse_list(reader, :ulist, parent, style)
|
645
686
|
break
|
646
687
|
|
647
|
-
elsif
|
688
|
+
elsif OrderedListRx.match? this_line
|
648
689
|
reader.unshift_line this_line
|
649
690
|
block = parse_list(reader, :olist, parent, style)
|
650
691
|
attributes['style'] = block.style if block.style
|
651
692
|
break
|
652
693
|
|
653
|
-
elsif (
|
694
|
+
elsif ((this_line.include? '::') || (this_line.include? ';;')) && DescriptionListRx =~ this_line
|
654
695
|
reader.unshift_line this_line
|
655
|
-
block = parse_description_list(reader,
|
696
|
+
block = parse_description_list(reader, $~, parent)
|
656
697
|
break
|
657
698
|
|
658
699
|
elsif (style == 'float' || style == 'discrete') && (Compliance.underline_style_section_titles ?
|
659
700
|
(is_section_title? this_line, reader.peek_line) : !indented && (atx_section_title? this_line))
|
660
701
|
reader.unshift_line this_line
|
661
|
-
float_id, float_reftext,
|
702
|
+
float_id, float_reftext, block_title, float_level = parse_section_title reader, document, attributes['id']
|
662
703
|
attributes['reftext'] = float_reftext if float_reftext
|
663
|
-
block = Block.new(parent, :floating_title, :
|
664
|
-
block.title =
|
704
|
+
block = Block.new(parent, :floating_title, content_model: :empty)
|
705
|
+
block.title = block_title
|
665
706
|
attributes.delete 'title'
|
666
707
|
block.id = float_id || ((doc_attrs.key? 'sectids') ? (Section.generate_id block.title, document) : nil)
|
667
708
|
block.level = float_level
|
@@ -689,7 +730,7 @@ class Parser
|
|
689
730
|
# advance to block parsing =>
|
690
731
|
break
|
691
732
|
else
|
692
|
-
logger.
|
733
|
+
logger.debug message_with_context %(unknown style for paragraph: #{style}), source_location: reader.cursor_at_mark if logger.debug?
|
693
734
|
style = nil
|
694
735
|
# continue to process paragraph
|
695
736
|
end
|
@@ -700,32 +741,36 @@ class Parser
|
|
700
741
|
# a literal paragraph: contiguous lines starting with at least one whitespace character
|
701
742
|
# NOTE style can only be nil or "normal" at this point
|
702
743
|
if indented && !style
|
703
|
-
lines = read_paragraph_lines reader, (
|
744
|
+
lines = read_paragraph_lines reader, (content_adjacent = skipped == 0 ? options[:list_type] : nil), skip_line_comments: text_only
|
704
745
|
adjust_indentation! lines
|
705
|
-
|
706
|
-
|
707
|
-
|
708
|
-
|
746
|
+
if text_only || content_adjacent == :dlist
|
747
|
+
# this block gets folded into the list item text
|
748
|
+
block = Block.new(parent, :paragraph, content_model: :simple, source: lines, attributes: attributes)
|
749
|
+
else
|
750
|
+
block = Block.new(parent, :literal, content_model: :verbatim, source: lines, attributes: attributes)
|
751
|
+
end
|
709
752
|
# a normal paragraph: contiguous non-blank/non-continuation lines (left-indented or normal style)
|
710
753
|
else
|
711
|
-
lines = read_paragraph_lines reader, skipped == 0 &&
|
754
|
+
lines = read_paragraph_lines reader, skipped == 0 && options[:list_type], skip_line_comments: true
|
712
755
|
# NOTE don't check indented here since it's extremely rare
|
713
756
|
#if text_only || indented
|
714
757
|
if text_only
|
715
758
|
# if [normal] is used over an indented paragraph, shift content to left margin
|
716
759
|
# QUESTION do we even need to shift since whitespace is normalized by XML in this case?
|
717
760
|
adjust_indentation! lines if indented && style == 'normal'
|
718
|
-
block = Block.new(parent, :paragraph, :
|
761
|
+
block = Block.new(parent, :paragraph, content_model: :simple, source: lines, attributes: attributes)
|
719
762
|
elsif (ADMONITION_STYLE_HEADS.include? ch0) && (this_line.include? ':') && (AdmonitionParagraphRx =~ this_line)
|
720
763
|
lines[0] = $' # string after match
|
721
764
|
attributes['name'] = admonition_name = (attributes['style'] = $1).downcase
|
722
765
|
attributes['textlabel'] = (attributes.delete 'caption') || doc_attrs[%(#{admonition_name}-caption)]
|
723
|
-
block = Block.new(parent, :admonition, :
|
766
|
+
block = Block.new(parent, :admonition, content_model: :simple, source: lines, attributes: attributes)
|
724
767
|
elsif md_syntax && ch0 == '>' && this_line.start_with?('> ')
|
725
768
|
lines.map! {|line| line == '>' ? (line.slice 1, line.length) : ((line.start_with? '> ') ? (line.slice 2, line.length) : line) }
|
726
769
|
if lines[-1].start_with? '-- '
|
727
770
|
credit_line = (credit_line = lines.pop).slice 3, credit_line.length
|
728
|
-
|
771
|
+
unless lines.empty?
|
772
|
+
lines.pop while lines[-1].empty?
|
773
|
+
end
|
729
774
|
end
|
730
775
|
attributes['style'] = 'quote'
|
731
776
|
# NOTE will only detect discrete (aka free-floating) headings
|
@@ -743,7 +788,7 @@ class Parser
|
|
743
788
|
lines.pop while lines[-1].empty?
|
744
789
|
lines << lines.pop.chop # strip trailing quote
|
745
790
|
attributes['style'] = 'quote'
|
746
|
-
block = Block.new(parent, :quote, :
|
791
|
+
block = Block.new(parent, :quote, content_model: :simple, source: lines, attributes: attributes)
|
747
792
|
attribution, citetitle = (block.apply_subs credit_line).split ', ', 2
|
748
793
|
attributes['attribution'] = attribution if attribution
|
749
794
|
attributes['citetitle'] = citetitle if citetitle
|
@@ -751,7 +796,7 @@ class Parser
|
|
751
796
|
# if [normal] is used over an indented paragraph, shift content to left margin
|
752
797
|
# QUESTION do we even need to shift since whitespace is normalized by XML in this case?
|
753
798
|
adjust_indentation! lines if indented && style == 'normal'
|
754
|
-
block = Block.new(parent, :paragraph, :
|
799
|
+
block = Block.new(parent, :paragraph, content_model: :simple, source: lines, attributes: attributes)
|
755
800
|
end
|
756
801
|
|
757
802
|
catalog_inline_anchors((lines.join LF), block, document, reader)
|
@@ -762,63 +807,47 @@ class Parser
|
|
762
807
|
|
763
808
|
# either delimited block or styled paragraph
|
764
809
|
unless block
|
765
|
-
# abstract and partintro should be handled by open block
|
766
|
-
# FIXME kind of hackish...need to sort out how to generalize this
|
767
|
-
block_context = :open if block_context == :abstract || block_context == :partintro
|
768
|
-
|
769
810
|
case block_context
|
770
|
-
when :
|
771
|
-
attributes[
|
772
|
-
|
773
|
-
|
774
|
-
|
775
|
-
|
776
|
-
|
777
|
-
|
778
|
-
|
779
|
-
|
780
|
-
|
781
|
-
|
782
|
-
|
783
|
-
|
784
|
-
|
785
|
-
|
786
|
-
|
787
|
-
|
788
|
-
|
789
|
-
attributes['language'] = doc_attrs['source-language'] || 'text'
|
790
|
-
end unless attributes.key? 'language'
|
791
|
-
if (attributes.key? 'linenums-option') || (doc_attrs.key? 'source-linenums-option')
|
792
|
-
attributes['linenums'] = ''
|
793
|
-
end unless attributes.key? 'linenums'
|
794
|
-
if doc_attrs.key? 'source-indent'
|
795
|
-
attributes['indent'] = doc_attrs['source-indent']
|
796
|
-
end unless attributes.key? 'indent'
|
811
|
+
when :listing, :source
|
812
|
+
if block_context == :source || (!attributes[1] && (language = attributes[2] || doc_attrs['source-language']))
|
813
|
+
if language
|
814
|
+
attributes['style'] = 'source'
|
815
|
+
attributes['language'] = language
|
816
|
+
AttributeList.rekey attributes, [nil, nil, 'linenums']
|
817
|
+
else
|
818
|
+
AttributeList.rekey attributes, [nil, 'language', 'linenums']
|
819
|
+
if doc_attrs.key? 'source-language'
|
820
|
+
attributes['language'] = doc_attrs['source-language']
|
821
|
+
end unless attributes.key? 'language'
|
822
|
+
end
|
823
|
+
if attributes['linenums-option'] || doc_attrs['source-linenums-option']
|
824
|
+
attributes['linenums'] = ''
|
825
|
+
end unless attributes.key? 'linenums'
|
826
|
+
if doc_attrs.key? 'source-indent'
|
827
|
+
attributes['indent'] = doc_attrs['source-indent']
|
828
|
+
end unless attributes.key? 'indent'
|
829
|
+
end
|
797
830
|
block = build_block(:listing, :verbatim, terminator, parent, reader, attributes)
|
798
|
-
|
799
831
|
when :fenced_code
|
800
832
|
attributes['style'] = 'source'
|
801
|
-
if (ll = this_line.length)
|
802
|
-
language =
|
803
|
-
|
804
|
-
|
805
|
-
|
806
|
-
|
833
|
+
if (ll = this_line.length) > 3
|
834
|
+
if (comma_idx = (language = this_line.slice 3, ll).index ',')
|
835
|
+
if comma_idx > 0
|
836
|
+
language = (language.slice 0, comma_idx).strip
|
837
|
+
attributes['linenums'] = '' if comma_idx < ll - 4
|
838
|
+
elsif ll > 4
|
839
|
+
attributes['linenums'] = ''
|
840
|
+
end
|
807
841
|
else
|
808
|
-
language =
|
809
|
-
attributes['linenums'] = '' if ll > 4
|
842
|
+
language = language.lstrip
|
810
843
|
end
|
811
|
-
else
|
812
|
-
language = language.lstrip
|
813
844
|
end
|
814
845
|
if language.nil_or_empty?
|
815
|
-
if doc_attrs.key? 'source-language'
|
816
|
-
attributes['language'] = doc_attrs['source-language'] || 'text'
|
817
|
-
end
|
846
|
+
attributes['language'] = doc_attrs['source-language'] if doc_attrs.key? 'source-language'
|
818
847
|
else
|
819
848
|
attributes['language'] = language
|
820
849
|
end
|
821
|
-
if
|
850
|
+
if attributes['linenums-option'] || doc_attrs['source-linenums-option']
|
822
851
|
attributes['linenums'] = ''
|
823
852
|
end unless attributes.key? 'linenums'
|
824
853
|
if doc_attrs.key? 'source-indent'
|
@@ -826,45 +855,53 @@ class Parser
|
|
826
855
|
end unless attributes.key? 'indent'
|
827
856
|
terminator = terminator.slice 0, 3
|
828
857
|
block = build_block(:listing, :verbatim, terminator, parent, reader, attributes)
|
829
|
-
|
830
|
-
when :pass
|
831
|
-
block = build_block(block_context, :raw, terminator, parent, reader, attributes)
|
832
|
-
|
833
|
-
when :stem, :latexmath, :asciimath
|
834
|
-
attributes['style'] = STEM_TYPE_ALIASES[attributes[2] || doc_attrs['stem']] if block_context == :stem
|
835
|
-
block = build_block(:stem, :raw, terminator, parent, reader, attributes)
|
836
|
-
|
837
|
-
when :open, :sidebar
|
838
|
-
block = build_block(block_context, :compound, terminator, parent, reader, attributes)
|
839
|
-
|
840
858
|
when :table
|
841
859
|
block_cursor = reader.cursor
|
842
|
-
block_reader = Reader.new reader.read_lines_until(:
|
860
|
+
block_reader = Reader.new reader.read_lines_until(terminator: terminator, skip_line_comments: true, context: :table, cursor: :at_mark), block_cursor
|
843
861
|
# NOTE it's very rare that format is set when using a format hint char, so short-circuit
|
844
862
|
unless terminator.start_with? '|', '!'
|
845
863
|
# NOTE infer dsv once all other format hint chars are ruled out
|
846
864
|
attributes['format'] ||= (terminator.start_with? ',') ? 'csv' : 'dsv'
|
847
865
|
end
|
848
866
|
block = parse_table(block_reader, parent, attributes)
|
849
|
-
|
867
|
+
when :sidebar
|
868
|
+
block = build_block(block_context, :compound, terminator, parent, reader, attributes)
|
869
|
+
when :admonition
|
870
|
+
attributes['name'] = admonition_name = style.downcase
|
871
|
+
attributes['textlabel'] = (attributes.delete 'caption') || doc_attrs[%(#{admonition_name}-caption)]
|
872
|
+
block = build_block(block_context, :compound, terminator, parent, reader, attributes)
|
873
|
+
when :open, :abstract, :partintro
|
874
|
+
block = build_block(:open, :compound, terminator, parent, reader, attributes)
|
875
|
+
when :literal
|
876
|
+
block = build_block(block_context, :verbatim, terminator, parent, reader, attributes)
|
877
|
+
when :example
|
878
|
+
attributes['caption'] = '' if attributes['collapsible-option']
|
879
|
+
block = build_block(block_context, :compound, terminator, parent, reader, attributes)
|
850
880
|
when :quote, :verse
|
851
881
|
AttributeList.rekey(attributes, [nil, 'attribution', 'citetitle'])
|
852
882
|
block = build_block(block_context, (block_context == :verse ? :verbatim : :compound), terminator, parent, reader, attributes)
|
853
|
-
|
883
|
+
when :stem, :latexmath, :asciimath
|
884
|
+
attributes['style'] = STEM_TYPE_ALIASES[attributes[2] || doc_attrs['stem']] if block_context == :stem
|
885
|
+
block = build_block(:stem, :raw, terminator, parent, reader, attributes)
|
886
|
+
when :pass
|
887
|
+
block = build_block(block_context, :raw, terminator, parent, reader, attributes)
|
888
|
+
when :comment
|
889
|
+
build_block(block_context, :skip, terminator, parent, reader, attributes)
|
890
|
+
attributes.clear
|
891
|
+
return
|
854
892
|
else
|
855
|
-
if block_extensions && (extension = extensions.registered_for_block?
|
856
|
-
|
857
|
-
|
858
|
-
AttributeList.rekey(attributes, [nil]
|
893
|
+
if block_extensions && (extension = extensions.registered_for_block? block_context, cloaked_context)
|
894
|
+
unless (content_model = (ext_config = extension.config)[:content_model]) == :skip
|
895
|
+
unless (positional_attrs = ext_config[:positional_attrs] || ext_config[:pos_attrs]).nil_or_empty?
|
896
|
+
AttributeList.rekey(attributes, [nil] + positional_attrs)
|
859
897
|
end
|
860
|
-
if (default_attrs =
|
898
|
+
if (default_attrs = ext_config[:default_attrs])
|
861
899
|
default_attrs.each {|k, v| attributes[k] ||= v }
|
862
900
|
end
|
863
901
|
# QUESTION should we clone the extension for each cloaked context and set in config?
|
864
902
|
attributes['cloaked-context'] = cloaked_context
|
865
903
|
end
|
866
|
-
block = build_block block_context, content_model, terminator, parent, reader, attributes, :
|
867
|
-
unless block
|
904
|
+
unless (block = build_block block_context, content_model, terminator, parent, reader, attributes, extension: extension)
|
868
905
|
attributes.clear
|
869
906
|
return
|
870
907
|
end
|
@@ -877,19 +914,24 @@ class Parser
|
|
877
914
|
|
878
915
|
# FIXME we've got to clean this up, it's horrible!
|
879
916
|
block.source_location = reader.cursor_at_mark if document.sourcemap
|
880
|
-
# FIXME title should be assigned when block is constructed
|
881
|
-
|
917
|
+
# FIXME title and caption should be assigned when block is constructed (though we need to handle all cases)
|
918
|
+
if attributes['title']
|
919
|
+
block.title = block_title = attributes.delete 'title'
|
920
|
+
block.assign_caption attributes.delete 'caption' if CAPTION_ATTRIBUTE_NAMES[block.context]
|
921
|
+
end
|
882
922
|
# TODO eventually remove the style attribute from the attributes hash
|
883
923
|
#block.style = attributes.delete 'style'
|
884
924
|
block.style = attributes['style']
|
885
|
-
if (block_id = (block.id
|
886
|
-
|
887
|
-
|
925
|
+
if (block_id = block.id || (block.id = attributes['id']))
|
926
|
+
# convert title to resolve attributes while in scope
|
927
|
+
block.title if block_title ? (block_title.include? ATTR_REF_HEAD) : block.title?
|
928
|
+
unless document.register :refs, [block_id, block]
|
929
|
+
logger.warn message_with_context %(id assigned to block already in use: #{block_id}), source_location: reader.cursor_at_mark
|
888
930
|
end
|
889
931
|
end
|
890
932
|
# FIXME remove the need for this update!
|
891
|
-
block.attributes
|
892
|
-
block.
|
933
|
+
block.update_attributes attributes unless attributes.empty?
|
934
|
+
block.commit_subs
|
893
935
|
|
894
936
|
#if doc_attrs.key? :pending_attribute_entries
|
895
937
|
# doc_attrs.delete(:pending_attribute_entries).each do |entry|
|
@@ -915,74 +957,44 @@ class Parser
|
|
915
957
|
reader.read_lines_until opts, &break_condition
|
916
958
|
end
|
917
959
|
|
918
|
-
# Public: Determines whether this line is the start of
|
960
|
+
# Public: Determines whether this line is the start of a known delimited block.
|
919
961
|
#
|
920
|
-
#
|
921
|
-
|
962
|
+
# Returns the BlockMatchData (if return_match_data is true) or true (if return_match_data is false) if this line is
|
963
|
+
# the start of a delimited block, otherwise nothing.
|
964
|
+
def self.is_delimited_block? line, return_match_data = nil
|
922
965
|
# highly optimized for best performance
|
923
|
-
return unless (line_len = line.length) > 1 && DELIMITED_BLOCK_HEADS
|
924
|
-
#
|
966
|
+
return unless (line_len = line.length) > 1 && DELIMITED_BLOCK_HEADS[line.slice 0, 2]
|
967
|
+
# open block
|
925
968
|
if line_len == 2
|
926
969
|
tip = line
|
927
|
-
|
970
|
+
tip_len = 2
|
928
971
|
else
|
929
|
-
#
|
930
|
-
if line_len
|
972
|
+
# all other delimited blocks, including fenced code
|
973
|
+
if line_len < 5
|
931
974
|
tip = line
|
932
|
-
|
975
|
+
tip_len = line_len
|
933
976
|
else
|
934
|
-
tip = line.slice 0, 4
|
935
|
-
tl = 4
|
977
|
+
tip = line.slice 0, (tip_len = 4)
|
936
978
|
end
|
937
|
-
|
938
979
|
# special case for fenced code blocks
|
939
|
-
|
940
|
-
|
941
|
-
|
942
|
-
tip_3 = (tl == 4 ? tip.chop : tip)
|
943
|
-
if tip_3 == '```'
|
944
|
-
if tl == 4 && tip.end_with?('`')
|
980
|
+
if Compliance.markdown_syntax && (tip.start_with? '`')
|
981
|
+
if tip_len == 4
|
982
|
+
if tip == '````' || (tip = tip.chop) != '```'
|
945
983
|
return
|
946
984
|
end
|
947
|
-
|
948
|
-
|
949
|
-
|
985
|
+
line = tip
|
986
|
+
line_len = tip_len = 3
|
987
|
+
elsif tip != '```'
|
988
|
+
return
|
950
989
|
end
|
990
|
+
elsif tip_len == 3
|
991
|
+
return
|
951
992
|
end
|
952
|
-
|
953
|
-
# short circuit if not a fenced code block
|
954
|
-
return if tl == 3 && !fenced_code
|
955
993
|
end
|
956
|
-
|
957
|
-
|
958
|
-
|
959
|
-
|
960
|
-
if return_match_data
|
961
|
-
context, masq = DELIMITED_BLOCKS[tip]
|
962
|
-
BlockMatchData.new(context, masq, tip, tip)
|
963
|
-
else
|
964
|
-
true
|
965
|
-
end
|
966
|
-
elsif %(#{tip}#{tip.slice(-1, 1) * (line_len - tl)}) == line
|
967
|
-
if return_match_data
|
968
|
-
context, masq = DELIMITED_BLOCKS[tip]
|
969
|
-
BlockMatchData.new(context, masq, tip, line)
|
970
|
-
else
|
971
|
-
true
|
972
|
-
end
|
973
|
-
# only enable if/when we decide to support non-congruent block delimiters
|
974
|
-
#elsif (match = BlockDelimiterRx.match(line))
|
975
|
-
# if return_match_data
|
976
|
-
# context, masq = DELIMITED_BLOCKS[tip]
|
977
|
-
# BlockMatchData.new(context, masq, tip, match[0])
|
978
|
-
# else
|
979
|
-
# true
|
980
|
-
# end
|
981
|
-
else
|
982
|
-
nil
|
983
|
-
end
|
984
|
-
else
|
985
|
-
nil
|
994
|
+
# NOTE line matches the tip when delimiter is minimum length or fenced code
|
995
|
+
context, masq = DELIMITED_BLOCKS[tip]
|
996
|
+
if context && (line_len == tip_len || (uniform? (line.slice 1, line_len), DELIMITED_BLOCK_TAILS[tip], (line_len - 1)))
|
997
|
+
return_match_data ? (BlockMatchData.new context, masq, tip, line) : true
|
986
998
|
end
|
987
999
|
end
|
988
1000
|
|
@@ -990,9 +1002,10 @@ class Parser
|
|
990
1002
|
# if terminator is false, that means the all the lines in the reader should be parsed
|
991
1003
|
# NOTE could invoke filter in here, before and after parsing
|
992
1004
|
def self.build_block(block_context, content_model, terminator, parent, reader, attributes, options = {})
|
993
|
-
|
1005
|
+
case content_model
|
1006
|
+
when :skip
|
994
1007
|
skip_processing, parse_as_content_model = true, :simple
|
995
|
-
|
1008
|
+
when :raw
|
996
1009
|
skip_processing, parse_as_content_model = false, :simple
|
997
1010
|
else
|
998
1011
|
skip_processing, parse_as_content_model = false, content_model
|
@@ -1000,16 +1013,16 @@ class Parser
|
|
1000
1013
|
|
1001
1014
|
if terminator.nil?
|
1002
1015
|
if parse_as_content_model == :verbatim
|
1003
|
-
lines = reader.read_lines_until :
|
1016
|
+
lines = reader.read_lines_until break_on_blank_lines: true, break_on_list_continuation: true
|
1004
1017
|
else
|
1005
1018
|
content_model = :simple if content_model == :compound
|
1006
1019
|
# TODO we could also skip processing if we're able to detect reader is a BlockReader
|
1007
|
-
lines = read_paragraph_lines reader, false, :
|
1020
|
+
lines = read_paragraph_lines reader, false, skip_line_comments: true, skip_processing: skip_processing
|
1008
1021
|
# QUESTION check for empty lines after grabbing lines for simple content model?
|
1009
1022
|
end
|
1010
1023
|
block_reader = nil
|
1011
1024
|
elsif parse_as_content_model != :compound
|
1012
|
-
lines = reader.read_lines_until :
|
1025
|
+
lines = reader.read_lines_until terminator: terminator, skip_processing: skip_processing, context: block_context, cursor: :at_mark
|
1013
1026
|
block_reader = nil
|
1014
1027
|
# terminator is false when reader has already been prepared
|
1015
1028
|
elsif terminator == false
|
@@ -1018,16 +1031,18 @@ class Parser
|
|
1018
1031
|
else
|
1019
1032
|
lines = nil
|
1020
1033
|
block_cursor = reader.cursor
|
1021
|
-
block_reader = Reader.new reader.read_lines_until(:
|
1034
|
+
block_reader = Reader.new reader.read_lines_until(terminator: terminator, skip_processing: skip_processing, context: block_context, cursor: :at_mark), block_cursor
|
1022
1035
|
end
|
1023
1036
|
|
1024
|
-
|
1037
|
+
case content_model
|
1038
|
+
when :verbatim
|
1039
|
+
tab_size = (attributes['tabsize'] || parent.document.attributes['tabsize']).to_i
|
1025
1040
|
if (indent = attributes['indent'])
|
1026
|
-
adjust_indentation! lines, indent,
|
1027
|
-
elsif
|
1028
|
-
adjust_indentation! lines,
|
1041
|
+
adjust_indentation! lines, indent.to_i, tab_size
|
1042
|
+
elsif tab_size > 0
|
1043
|
+
adjust_indentation! lines, -1, tab_size
|
1029
1044
|
end
|
1030
|
-
|
1045
|
+
when :skip
|
1031
1046
|
# QUESTION should we still invoke process method if extension is specified?
|
1032
1047
|
return
|
1033
1048
|
end
|
@@ -1035,12 +1050,12 @@ class Parser
|
|
1035
1050
|
if (extension = options[:extension])
|
1036
1051
|
# QUESTION do we want to delete the style?
|
1037
1052
|
attributes.delete('style')
|
1038
|
-
if (block = extension.process_method[parent, block_reader || (Reader.new lines), attributes.
|
1053
|
+
if (block = extension.process_method[parent, block_reader || (Reader.new lines), attributes.merge]) && block != parent
|
1039
1054
|
attributes.replace block.attributes
|
1040
1055
|
# FIXME if the content model is set to compound, but we only have simple in this context, then
|
1041
1056
|
# forcefully set the content_model to simple to prevent parsing blocks from children
|
1042
1057
|
# TODO document this behavior!!
|
1043
|
-
if block.content_model == :compound && !(lines = block.lines).
|
1058
|
+
if block.content_model == :compound && Block === block && !(lines = block.lines).empty?
|
1044
1059
|
content_model = :compound
|
1045
1060
|
block_reader = Reader.new lines
|
1046
1061
|
end
|
@@ -1048,14 +1063,7 @@ class Parser
|
|
1048
1063
|
return
|
1049
1064
|
end
|
1050
1065
|
else
|
1051
|
-
block = Block.new(parent, block_context, :
|
1052
|
-
end
|
1053
|
-
|
1054
|
-
# QUESTION should we have an explicit map or can we rely on check for *-caption attribute?
|
1055
|
-
if (attributes.key? 'title') && block.context != :admonition &&
|
1056
|
-
(parent.document.attributes.key? %(#{block.context}-caption))
|
1057
|
-
block.title = attributes.delete 'title'
|
1058
|
-
block.assign_caption(attributes.delete 'caption')
|
1066
|
+
block = Block.new(parent, block_context, content_model: content_model, source: lines, attributes: attributes)
|
1059
1067
|
end
|
1060
1068
|
|
1061
1069
|
# reader is confined within boundaries of a delimited block, so look for
|
@@ -1075,9 +1083,13 @@ class Parser
|
|
1075
1083
|
# parent - The parent Block to which to attach the parsed blocks
|
1076
1084
|
#
|
1077
1085
|
# Returns nothing.
|
1078
|
-
def self.parse_blocks(reader, parent)
|
1079
|
-
|
1086
|
+
def self.parse_blocks(reader, parent, attributes = nil)
|
1087
|
+
if attributes
|
1088
|
+
while ((block = next_block reader, parent, attributes.merge) && parent.blocks << block) || reader.has_more_lines?; end
|
1089
|
+
else
|
1090
|
+
while ((block = next_block reader, parent) && parent.blocks << block) || reader.has_more_lines?; end
|
1080
1091
|
end
|
1092
|
+
nil
|
1081
1093
|
end
|
1082
1094
|
|
1083
1095
|
# Internal: Parse and construct an ordered or unordered list at the current position of the Reader
|
@@ -1088,10 +1100,11 @@ class Parser
|
|
1088
1100
|
# style - The block style assigned to this list (optional, default: nil)
|
1089
1101
|
#
|
1090
1102
|
# Returns the Block encapsulating the parsed unordered or ordered list
|
1091
|
-
def self.parse_list
|
1092
|
-
list_block = List.new
|
1103
|
+
def self.parse_list reader, list_type, parent, style
|
1104
|
+
list_block = List.new parent, list_type
|
1105
|
+
list_rx = ListRxMap[list_type]
|
1093
1106
|
|
1094
|
-
while reader.has_more_lines? &&
|
1107
|
+
while reader.has_more_lines? && list_rx =~ reader.peek_line
|
1095
1108
|
# NOTE parse_list_item will stop at sibling item or end of list; never sees ancestor items
|
1096
1109
|
if (list_item = parse_list_item reader, list_block, $~, $1, style)
|
1097
1110
|
list_block.items << list_item
|
@@ -1112,13 +1125,11 @@ class Parser
|
|
1112
1125
|
def self.catalog_callouts(text, document)
|
1113
1126
|
found = false
|
1114
1127
|
autonum = 0
|
1115
|
-
text.scan
|
1116
|
-
|
1117
|
-
captured, num = $&, $2
|
1118
|
-
document.callouts.register num == '.' ? (autonum += 1).to_s : num unless captured.start_with? '\\'
|
1128
|
+
text.scan CalloutScanRx do
|
1129
|
+
document.callouts.register $2 == '.' ? (autonum += 1).to_s : $2 unless $&.start_with? '\\'
|
1119
1130
|
# we have to mark as found even if it's escaped so it can be unescaped
|
1120
1131
|
found = true
|
1121
|
-
|
1132
|
+
end if text.include? '<'
|
1122
1133
|
found
|
1123
1134
|
end
|
1124
1135
|
|
@@ -1131,12 +1142,11 @@ class Parser
|
|
1131
1142
|
# doc - The document to which the node belongs; computed from node if not specified
|
1132
1143
|
#
|
1133
1144
|
# Returns nothing
|
1134
|
-
def self.catalog_inline_anchor id, reftext, node, location, doc =
|
1135
|
-
doc ||= node.document
|
1145
|
+
def self.catalog_inline_anchor id, reftext, node, location, doc = node.document
|
1136
1146
|
reftext = doc.sub_attributes reftext if reftext && (reftext.include? ATTR_REF_HEAD)
|
1137
|
-
unless doc.register :refs, [id, (Inline.new node, :anchor, reftext, :
|
1147
|
+
unless doc.register :refs, [id, (Inline.new node, :anchor, reftext, type: :ref, id: id)]
|
1138
1148
|
location = location.cursor if Reader === location
|
1139
|
-
logger.warn message_with_context %(id assigned to anchor already in use: #{id}), :
|
1149
|
+
logger.warn message_with_context %(id assigned to anchor already in use: #{id}), source_location: location
|
1140
1150
|
end
|
1141
1151
|
nil
|
1142
1152
|
end
|
@@ -1149,26 +1159,26 @@ class Parser
|
|
1149
1159
|
#
|
1150
1160
|
# Returns nothing
|
1151
1161
|
def self.catalog_inline_anchors text, block, document, reader
|
1152
|
-
text.scan
|
1153
|
-
# alias match for Ruby 1.8.7 compat
|
1154
|
-
m = $~
|
1162
|
+
text.scan InlineAnchorScanRx do
|
1155
1163
|
if (id = $1)
|
1156
|
-
if (reftext = $2)
|
1157
|
-
next if (reftext.include? ATTR_REF_HEAD) && (reftext = document.sub_attributes reftext).empty?
|
1158
|
-
end
|
1164
|
+
next if (reftext = $2) && (reftext.include? ATTR_REF_HEAD) && (reftext = document.sub_attributes reftext).empty?
|
1159
1165
|
else
|
1160
1166
|
id = $3
|
1161
1167
|
if (reftext = $4)
|
1162
|
-
|
1163
|
-
|
1168
|
+
if reftext.include? ']'
|
1169
|
+
reftext = reftext.gsub '\]', ']'
|
1170
|
+
reftext = document.sub_attributes reftext if reftext.include? ATTR_REF_HEAD
|
1171
|
+
elsif (reftext.include? ATTR_REF_HEAD) && (reftext = document.sub_attributes reftext).empty?
|
1172
|
+
next
|
1173
|
+
end
|
1164
1174
|
end
|
1165
1175
|
end
|
1166
|
-
unless document.register :refs, [id, (Inline.new block, :anchor, reftext, :
|
1176
|
+
unless document.register :refs, [id, (Inline.new block, :anchor, reftext, type: :ref, id: id)]
|
1167
1177
|
location = reader.cursor_at_mark
|
1168
|
-
if (offset = (
|
1178
|
+
if (offset = ($`.count LF) + (($&.start_with? LF) ? 1 : 0)) > 0
|
1169
1179
|
(location = location.dup).advance offset
|
1170
1180
|
end
|
1171
|
-
logger.warn message_with_context %(id assigned to anchor already in use: #{id}), :
|
1181
|
+
logger.warn message_with_context %(id assigned to anchor already in use: #{id}), source_location: location
|
1172
1182
|
end
|
1173
1183
|
end if (text.include? '[[') || (text.include? 'or:')
|
1174
1184
|
nil
|
@@ -1184,8 +1194,8 @@ class Parser
|
|
1184
1194
|
# Returns nothing
|
1185
1195
|
def self.catalog_inline_biblio_anchor id, reftext, node, reader
|
1186
1196
|
# QUESTION should we sub attributes in reftext (like with regular anchors)?
|
1187
|
-
unless node.document.register :refs, [id, (Inline.new node, :anchor,
|
1188
|
-
logger.warn message_with_context %(id assigned to bibliography anchor already in use: #{id}), :
|
1197
|
+
unless node.document.register :refs, [id, (Inline.new node, :anchor, reftext && %([#{reftext}]), type: :bibref, id: id)]
|
1198
|
+
logger.warn message_with_context %(id assigned to bibliography anchor already in use: #{id}), source_location: reader.cursor
|
1189
1199
|
end
|
1190
1200
|
nil
|
1191
1201
|
end
|
@@ -1197,23 +1207,20 @@ class Parser
|
|
1197
1207
|
# parent - The parent Block to which this description list belongs
|
1198
1208
|
#
|
1199
1209
|
# Returns the Block encapsulating the parsed description list
|
1200
|
-
def self.parse_description_list
|
1201
|
-
list_block = List.new
|
1202
|
-
|
1203
|
-
# allows us to capture until we find a description item
|
1204
|
-
# that uses the same delimiter (::, :::, :::: or ;;)
|
1210
|
+
def self.parse_description_list reader, match, parent
|
1211
|
+
list_block = List.new parent, :dlist
|
1212
|
+
# detects a description list item that uses the same delimiter (::, :::, :::: or ;;)
|
1205
1213
|
sibling_pattern = DescriptionListSiblingRx[match[2]]
|
1214
|
+
list_block.items << (current_pair = parse_list_item reader, list_block, match, sibling_pattern)
|
1206
1215
|
|
1207
|
-
|
1208
|
-
|
1209
|
-
|
1210
|
-
|
1211
|
-
previous_pair[0] << term
|
1212
|
-
previous_pair[1] = item
|
1216
|
+
while reader.has_more_lines? && sibling_pattern =~ reader.peek_line
|
1217
|
+
next_pair = parse_list_item reader, list_block, $~, sibling_pattern
|
1218
|
+
if current_pair[1]
|
1219
|
+
list_block.items << (current_pair = next_pair)
|
1213
1220
|
else
|
1214
|
-
|
1221
|
+
current_pair[0] << next_pair[0][0]
|
1222
|
+
current_pair[1] = next_pair[1]
|
1215
1223
|
end
|
1216
|
-
match = nil
|
1217
1224
|
end
|
1218
1225
|
|
1219
1226
|
list_block
|
@@ -1239,12 +1246,12 @@ class Parser
|
|
1239
1246
|
end
|
1240
1247
|
# might want to move this check to a validate method
|
1241
1248
|
unless num == next_index.to_s
|
1242
|
-
logger.warn message_with_context %(callout list item index: expected #{next_index}, got #{num}), :
|
1249
|
+
logger.warn message_with_context %(callout list item index: expected #{next_index}, got #{num}), source_location: reader.cursor_at_mark
|
1243
1250
|
end
|
1244
1251
|
if (list_item = parse_list_item reader, list_block, match, '<1>')
|
1245
1252
|
list_block.items << list_item
|
1246
1253
|
if (coids = callouts.callout_ids list_block.items.size).empty?
|
1247
|
-
logger.warn message_with_context %(no callout found for <#{list_block.items.size}>), :
|
1254
|
+
logger.warn message_with_context %(no callout found for <#{list_block.items.size}>), source_location: reader.cursor_at_mark
|
1248
1255
|
else
|
1249
1256
|
list_item.attributes['coids'] = coids
|
1250
1257
|
end
|
@@ -1274,7 +1281,7 @@ class Parser
|
|
1274
1281
|
# marker pattern.
|
1275
1282
|
# style - The block style assigned to this list (optional, default: nil)
|
1276
1283
|
#
|
1277
|
-
# Returns the next ListItem or ListItem pair (description list) for the parent list Block.
|
1284
|
+
# Returns the next ListItem or [[ListItem], ListItem] pair (description list) for the parent list Block.
|
1278
1285
|
def self.parse_list_item(reader, list_block, match, sibling_trait, style = nil)
|
1279
1286
|
if (list_type = list_block.context) == :dlist
|
1280
1287
|
dlist = true
|
@@ -1296,7 +1303,8 @@ class Parser
|
|
1296
1303
|
has_text = true
|
1297
1304
|
list_item = ListItem.new(list_block, (item_text = match[2]))
|
1298
1305
|
list_item.source_location = reader.cursor if list_block.document.sourcemap
|
1299
|
-
|
1306
|
+
case list_type
|
1307
|
+
when :ulist
|
1300
1308
|
list_item.marker = sibling_trait
|
1301
1309
|
if item_text.start_with?('[')
|
1302
1310
|
if style && style == 'bibliography'
|
@@ -1308,27 +1316,28 @@ class Parser
|
|
1308
1316
|
catalog_inline_anchor $1, $2, list_item, reader
|
1309
1317
|
end
|
1310
1318
|
elsif item_text.start_with?('[ ] ', '[x] ', '[*] ')
|
1311
|
-
|
1312
|
-
#list_block.set_option 'checklist' unless list_block.attributes['checklist-option']
|
1313
|
-
list_block.attributes['checklist-option'] = ''
|
1319
|
+
list_block.set_option 'checklist'
|
1314
1320
|
list_item.attributes['checkbox'] = ''
|
1315
1321
|
list_item.attributes['checked'] = '' unless item_text.start_with? '[ '
|
1316
1322
|
list_item.text = item_text.slice(4, item_text.length)
|
1317
1323
|
end
|
1318
1324
|
end
|
1319
|
-
|
1325
|
+
when :olist
|
1320
1326
|
sibling_trait, implicit_style = resolve_ordered_list_marker(sibling_trait, (ordinal = list_block.items.size), true, reader)
|
1321
1327
|
list_item.marker = sibling_trait
|
1322
1328
|
if ordinal == 0 && !style
|
1323
1329
|
# using list level makes more sense, but we don't track it
|
1324
|
-
# basing style on marker level is compliant with AsciiDoc
|
1325
|
-
list_block.style = implicit_style || (
|
1330
|
+
# basing style on marker level is compliant with AsciiDoc.py
|
1331
|
+
list_block.style = implicit_style || (ORDERED_LIST_STYLES[sibling_trait.length - 1] || 'arabic').to_s
|
1326
1332
|
end
|
1327
1333
|
if item_text.start_with?('[[') && LeadingInlineAnchorRx =~ item_text
|
1328
1334
|
catalog_inline_anchor $1, $2, list_item, reader
|
1329
1335
|
end
|
1330
1336
|
else # :colist
|
1331
1337
|
list_item.marker = sibling_trait
|
1338
|
+
if item_text.start_with?('[[') && LeadingInlineAnchorRx =~ item_text
|
1339
|
+
catalog_inline_anchor $1, $2, list_item, reader
|
1340
|
+
end
|
1332
1341
|
end
|
1333
1342
|
end
|
1334
1343
|
|
@@ -1342,38 +1351,28 @@ class Parser
|
|
1342
1351
|
comment_lines = list_item_reader.skip_line_comments
|
1343
1352
|
if (subsequent_line = list_item_reader.peek_line)
|
1344
1353
|
list_item_reader.unshift_lines comment_lines unless comment_lines.empty?
|
1345
|
-
|
1346
|
-
content_adjacent = false
|
1347
|
-
else
|
1354
|
+
unless subsequent_line.empty?
|
1348
1355
|
content_adjacent = true
|
1349
1356
|
# treat lines as paragraph text if continuation does not connect first block (i.e., has_text = nil)
|
1350
1357
|
has_text = nil unless dlist
|
1351
1358
|
end
|
1352
|
-
else
|
1353
|
-
# NOTE we have no use for any trailing comment lines we might have found
|
1354
|
-
continuation_connects_first_block = false
|
1355
|
-
content_adjacent = false
|
1356
1359
|
end
|
1357
1360
|
|
1358
1361
|
# reader is confined to boundaries of list, which means only blocks will be found (no sections)
|
1359
|
-
if (block = next_block(list_item_reader, list_item, {}, :
|
1362
|
+
if (block = next_block(list_item_reader, list_item, {}, text_only: has_text ? nil : true, list_type: list_type))
|
1360
1363
|
list_item.blocks << block
|
1361
1364
|
end
|
1362
1365
|
|
1363
1366
|
while list_item_reader.has_more_lines?
|
1364
|
-
if (block = next_block(list_item_reader, list_item))
|
1367
|
+
if (block = next_block(list_item_reader, list_item, {}, list_type: list_type))
|
1365
1368
|
list_item.blocks << block
|
1366
1369
|
end
|
1367
1370
|
end
|
1368
1371
|
|
1369
|
-
list_item.fold_first(
|
1372
|
+
list_item.fold_first if content_adjacent && (first_block = list_item.blocks[0]) && first_block.context == :paragraph
|
1370
1373
|
end
|
1371
1374
|
|
1372
|
-
|
1373
|
-
list_item.text? || list_item.blocks? ? [list_term, list_item] : [list_term]
|
1374
|
-
else
|
1375
|
-
list_item
|
1376
|
-
end
|
1375
|
+
dlist ? [[list_term], (list_item.text? || list_item.blocks? ? list_item : nil)] : list_item
|
1377
1376
|
end
|
1378
1377
|
|
1379
1378
|
# Internal: Collect the lines belonging to the current list item, navigating
|
@@ -1407,6 +1406,8 @@ class Parser
|
|
1407
1406
|
# it gets associated with the outermost block
|
1408
1407
|
detached_continuation = nil
|
1409
1408
|
|
1409
|
+
dlist = list_type == :dlist
|
1410
|
+
|
1410
1411
|
while reader.has_more_lines?
|
1411
1412
|
this_line = reader.read_line
|
1412
1413
|
|
@@ -1443,7 +1444,7 @@ class Parser
|
|
1443
1444
|
buffer << this_line
|
1444
1445
|
# grab all the lines in the block, leaving the delimiters in place
|
1445
1446
|
# we're being more strict here about the terminator, but I think that's a good thing
|
1446
|
-
buffer.concat reader.read_lines_until(:
|
1447
|
+
buffer.concat reader.read_lines_until(terminator: match.terminator, read_last_line: true, context: nil)
|
1447
1448
|
continuation = :inactive
|
1448
1449
|
else
|
1449
1450
|
break
|
@@ -1451,118 +1452,110 @@ class Parser
|
|
1451
1452
|
# technically BlockAttributeLineRx only breaks if ensuing line is not a list item
|
1452
1453
|
# which really means BlockAttributeLineRx only breaks if it's acting as a block delimiter
|
1453
1454
|
# FIXME to be AsciiDoc compliant, we shouldn't break if style in attribute line is "literal" (i.e., [literal])
|
1454
|
-
elsif
|
1455
|
+
elsif dlist && continuation != :active && (BlockAttributeLineRx.match? this_line)
|
1455
1456
|
break
|
1456
|
-
|
1457
|
-
|
1458
|
-
|
1459
|
-
|
1460
|
-
|
1461
|
-
|
1462
|
-
|
1463
|
-
|
1464
|
-
|
1465
|
-
|
1466
|
-
|
1467
|
-
:break_on_list_continuation => true) {|line|
|
1468
|
-
# we may be in an indented list disguised as a literal paragraph
|
1469
|
-
# so we need to make sure we don't slurp up a legitimate sibling
|
1470
|
-
list_type == :dlist && is_sibling_list_item?(line, list_type, sibling_trait)
|
1471
|
-
}
|
1472
|
-
continuation = :inactive
|
1473
|
-
# let block metadata play out until we find the block
|
1474
|
-
elsif (BlockTitleRx.match? this_line) || (BlockAttributeLineRx.match? this_line) || (AttributeEntryRx.match? this_line)
|
1475
|
-
buffer << this_line
|
1457
|
+
elsif continuation == :active && !this_line.empty?
|
1458
|
+
# literal paragraphs have special considerations (and this is one of
|
1459
|
+
# two entry points into one)
|
1460
|
+
# if we don't process it as a whole, then a line in it that looks like a
|
1461
|
+
# list item will throw off the exit from it
|
1462
|
+
if LiteralParagraphRx.match? this_line
|
1463
|
+
reader.unshift_line this_line
|
1464
|
+
if dlist
|
1465
|
+
# we may be in an indented list disguised as a literal paragraph
|
1466
|
+
# so we need to make sure we don't slurp up a legitimate sibling
|
1467
|
+
buffer.concat reader.read_lines_until(preserve_last_line: true, break_on_blank_lines: true, break_on_list_continuation: true) {|line| is_sibling_list_item? line, list_type, sibling_trait }
|
1476
1468
|
else
|
1477
|
-
|
1478
|
-
within_nested_list = true
|
1479
|
-
if nested_list_type == :dlist && $3.nil_or_empty?
|
1480
|
-
# get greedy again
|
1481
|
-
has_text = false
|
1482
|
-
end
|
1483
|
-
end
|
1484
|
-
buffer << this_line
|
1485
|
-
continuation = :inactive
|
1469
|
+
buffer.concat reader.read_lines_until(preserve_last_line: true, break_on_blank_lines: true, break_on_list_continuation: true)
|
1486
1470
|
end
|
1487
|
-
|
1488
|
-
|
1489
|
-
|
1490
|
-
|
1491
|
-
|
1492
|
-
|
1493
|
-
|
1471
|
+
continuation = :inactive
|
1472
|
+
# let block metadata play out until we find the block
|
1473
|
+
elsif (BlockTitleRx.match? this_line) || (BlockAttributeLineRx.match? this_line) || (AttributeEntryRx.match? this_line)
|
1474
|
+
buffer << this_line
|
1475
|
+
else
|
1476
|
+
if (nested_list_type = (within_nested_list ? [:dlist] : NESTABLE_LIST_CONTEXTS).find {|ctx| ListRxMap[ctx].match? this_line })
|
1477
|
+
within_nested_list = true
|
1478
|
+
if nested_list_type == :dlist && $3.nil_or_empty?
|
1479
|
+
# get greedy again
|
1480
|
+
has_text = false
|
1481
|
+
end
|
1494
1482
|
end
|
1483
|
+
buffer << this_line
|
1484
|
+
continuation = :inactive
|
1485
|
+
end
|
1486
|
+
elsif prev_line && prev_line.empty?
|
1487
|
+
# advance to the next line of content
|
1488
|
+
if this_line.empty?
|
1489
|
+
# stop reading if we reach eof
|
1490
|
+
break unless (this_line = reader.skip_blank_lines && reader.read_line)
|
1491
|
+
# stop reading if we hit a sibling list item
|
1492
|
+
break if is_sibling_list_item? this_line, list_type, sibling_trait
|
1493
|
+
end
|
1495
1494
|
|
1496
|
-
|
1497
|
-
|
1495
|
+
if this_line == LIST_CONTINUATION
|
1496
|
+
detached_continuation = buffer.size
|
1497
|
+
buffer << this_line
|
1498
|
+
elsif has_text # has_text only relevant for dlist, which is more greedy until it has text for an item; has_text is always true for all other lists
|
1499
|
+
# in this block, we have to see whether we stay in the list
|
1500
|
+
# TODO any way to combine this with the check after skipping blank lines?
|
1501
|
+
if is_sibling_list_item?(this_line, list_type, sibling_trait)
|
1502
|
+
break
|
1503
|
+
elsif (nested_list_type = NESTABLE_LIST_CONTEXTS.find {|ctx| ListRxMap[ctx] =~ this_line })
|
1498
1504
|
buffer << this_line
|
1499
|
-
else
|
1500
|
-
# has_text is only relevant for dlist, which is more greedy until it has text for an item
|
1501
|
-
# for all other lists, has_text is always true
|
1502
|
-
# in this block, we have to see whether we stay in the list
|
1503
|
-
if has_text
|
1504
|
-
# TODO any way to combine this with the check after skipping blank lines?
|
1505
|
-
if is_sibling_list_item?(this_line, list_type, sibling_trait)
|
1506
|
-
break
|
1507
|
-
elsif nested_list_type = NESTABLE_LIST_CONTEXTS.find {|ctx| ListRxMap[ctx] =~ this_line }
|
1508
|
-
buffer << this_line
|
1509
|
-
within_nested_list = true
|
1510
|
-
if nested_list_type == :dlist && $3.nil_or_empty?
|
1511
|
-
# get greedy again
|
1512
|
-
has_text = false
|
1513
|
-
end
|
1514
|
-
# slurp up any literal paragraph offset by blank lines
|
1515
|
-
# NOTE we have to check for indented list items first
|
1516
|
-
elsif LiteralParagraphRx.match? this_line
|
1517
|
-
reader.unshift_line this_line
|
1518
|
-
buffer.concat reader.read_lines_until(
|
1519
|
-
:preserve_last_line => true,
|
1520
|
-
:break_on_blank_lines => true,
|
1521
|
-
:break_on_list_continuation => true) {|line|
|
1522
|
-
# we may be in an indented list disguised as a literal paragraph
|
1523
|
-
# so we need to make sure we don't slurp up a legitimate sibling
|
1524
|
-
list_type == :dlist && is_sibling_list_item?(line, list_type, sibling_trait)
|
1525
|
-
}
|
1526
|
-
else
|
1527
|
-
break
|
1528
|
-
end
|
1529
|
-
else # only dlist in need of item text, so slurp it up!
|
1530
|
-
# pop the blank line so it's not interpretted as a list continuation
|
1531
|
-
buffer.pop unless within_nested_list
|
1532
|
-
buffer << this_line
|
1533
|
-
has_text = true
|
1534
|
-
end
|
1535
|
-
end
|
1536
|
-
else
|
1537
|
-
has_text = true if !this_line.empty?
|
1538
|
-
if nested_list_type = (within_nested_list ? [:dlist] : NESTABLE_LIST_CONTEXTS).find {|ctx| ListRxMap[ctx] =~ this_line }
|
1539
1505
|
within_nested_list = true
|
1540
1506
|
if nested_list_type == :dlist && $3.nil_or_empty?
|
1541
1507
|
# get greedy again
|
1542
1508
|
has_text = false
|
1543
1509
|
end
|
1510
|
+
# slurp up any literal paragraph offset by blank lines
|
1511
|
+
# NOTE we have to check for indented list items first
|
1512
|
+
elsif LiteralParagraphRx.match? this_line
|
1513
|
+
reader.unshift_line this_line
|
1514
|
+
if dlist
|
1515
|
+
# we may be in an indented list disguised as a literal paragraph
|
1516
|
+
# so we need to make sure we don't slurp up a legitimate sibling
|
1517
|
+
buffer.concat reader.read_lines_until(preserve_last_line: true, break_on_blank_lines: true, break_on_list_continuation: true) {|line| is_sibling_list_item? line, list_type, sibling_trait }
|
1518
|
+
else
|
1519
|
+
buffer.concat reader.read_lines_until(preserve_last_line: true, break_on_blank_lines: true, break_on_list_continuation: true)
|
1520
|
+
end
|
1521
|
+
else
|
1522
|
+
break
|
1544
1523
|
end
|
1524
|
+
else # only dlist in need of item text, so slurp it up!
|
1525
|
+
# pop the blank line so it's not interpreted as a list continuation
|
1526
|
+
buffer.pop unless within_nested_list
|
1545
1527
|
buffer << this_line
|
1528
|
+
has_text = true
|
1546
1529
|
end
|
1530
|
+
else
|
1531
|
+
has_text = true unless this_line.empty?
|
1532
|
+
if (nested_list_type = (within_nested_list ? [:dlist] : NESTABLE_LIST_CONTEXTS).find {|ctx| ListRxMap[ctx] =~ this_line })
|
1533
|
+
within_nested_list = true
|
1534
|
+
if nested_list_type == :dlist && $3.nil_or_empty?
|
1535
|
+
# get greedy again
|
1536
|
+
has_text = false
|
1537
|
+
end
|
1538
|
+
end
|
1539
|
+
buffer << this_line
|
1547
1540
|
end
|
1548
1541
|
this_line = nil
|
1549
1542
|
end
|
1550
1543
|
|
1551
1544
|
reader.unshift_line this_line if this_line
|
1552
1545
|
|
1553
|
-
if detached_continuation
|
1554
|
-
buffer.delete_at detached_continuation
|
1555
|
-
end
|
1556
|
-
|
1557
|
-
# strip trailing blank lines to prevent empty blocks
|
1558
|
-
buffer.pop while !buffer.empty? && buffer[-1].empty?
|
1546
|
+
buffer[detached_continuation] = '' if detached_continuation
|
1559
1547
|
|
1560
|
-
|
1561
|
-
|
1562
|
-
|
1563
|
-
|
1564
|
-
|
1565
|
-
|
1548
|
+
until buffer.empty?
|
1549
|
+
# strip trailing blank lines to prevent empty blocks
|
1550
|
+
if (last_line = buffer[-1]).empty?
|
1551
|
+
buffer.pop
|
1552
|
+
else
|
1553
|
+
# drop optional trailing continuation
|
1554
|
+
# (a blank line would have served the same purpose in the document)
|
1555
|
+
buffer.pop if last_line == LIST_CONTINUATION
|
1556
|
+
break
|
1557
|
+
end
|
1558
|
+
end
|
1566
1559
|
|
1567
1560
|
buffer
|
1568
1561
|
end
|
@@ -1584,19 +1577,15 @@ class Parser
|
|
1584
1577
|
sect_style = attributes[1]
|
1585
1578
|
sect_id, sect_reftext, sect_title, sect_level, sect_atx = parse_section_title reader, document, attributes['id']
|
1586
1579
|
|
1587
|
-
if sect_reftext
|
1588
|
-
attributes['reftext'] = sect_reftext
|
1589
|
-
else
|
1590
|
-
sect_reftext = attributes['reftext']
|
1591
|
-
end
|
1592
|
-
|
1593
1580
|
if sect_style
|
1594
1581
|
if book && sect_style == 'abstract'
|
1595
1582
|
sect_name, sect_level = 'chapter', 1
|
1583
|
+
elsif (sect_style.start_with? 'sect') && (SectionLevelStyleRx.match? sect_style)
|
1584
|
+
sect_name = 'section'
|
1596
1585
|
else
|
1597
1586
|
sect_name, sect_special = sect_style, true
|
1598
1587
|
sect_level = 1 if sect_level == 0
|
1599
|
-
sect_numbered =
|
1588
|
+
sect_numbered = sect_name == 'appendix'
|
1600
1589
|
end
|
1601
1590
|
elsif book
|
1602
1591
|
sect_name = sect_level == 0 ? 'part' : (sect_level > 1 ? 'section' : 'chapter')
|
@@ -1606,6 +1595,7 @@ class Parser
|
|
1606
1595
|
sect_name = 'section'
|
1607
1596
|
end
|
1608
1597
|
|
1598
|
+
attributes['reftext'] = sect_reftext if sect_reftext
|
1609
1599
|
section = Section.new parent, sect_level
|
1610
1600
|
section.id, section.title, section.sectname, section.source_location = sect_id, sect_title, sect_name, source_location
|
1611
1601
|
if sect_special
|
@@ -1613,7 +1603,7 @@ class Parser
|
|
1613
1603
|
if sect_numbered
|
1614
1604
|
section.numbered = true
|
1615
1605
|
elsif document.attributes['sectnums'] == 'all'
|
1616
|
-
section.numbered = book && sect_level == 1 ? :chapter : true
|
1606
|
+
section.numbered = (book && sect_level == 1 ? :chapter : true)
|
1617
1607
|
end
|
1618
1608
|
elsif document.attributes['sectnums'] && sect_level > 0
|
1619
1609
|
# NOTE a special section here is guaranteed to be nested in another section
|
@@ -1623,9 +1613,11 @@ class Parser
|
|
1623
1613
|
end
|
1624
1614
|
|
1625
1615
|
# generate an ID if one was not embedded or specified as anchor above section title
|
1626
|
-
if (id = section.id
|
1627
|
-
|
1628
|
-
|
1616
|
+
if (id = section.id || (section.id = (document.attributes.key? 'sectids') ? (generated_id = Section.generate_id section.title, document) : nil))
|
1617
|
+
# convert title to resolve attributes while in scope
|
1618
|
+
section.title unless generated_id || !(sect_title.include? ATTR_REF_HEAD)
|
1619
|
+
unless document.register :refs, [id, section]
|
1620
|
+
logger.warn message_with_context %(id assigned to section already in use: #{id}), source_location: (reader.cursor_at_line reader.lineno - (sect_atx ? 1 : 2))
|
1629
1621
|
end
|
1630
1622
|
end
|
1631
1623
|
|
@@ -1642,9 +1634,8 @@ class Parser
|
|
1642
1634
|
#
|
1643
1635
|
# Returns the Integer section level if the Reader is positioned at a section title or nil otherwise
|
1644
1636
|
def self.is_next_line_section?(reader, attributes)
|
1645
|
-
if (style = attributes[1]) && (style == 'discrete' || style == 'float')
|
1646
|
-
|
1647
|
-
elsif Compliance.underline_style_section_titles
|
1637
|
+
return if (style = attributes[1]) && (style == 'discrete' || style == 'float')
|
1638
|
+
if Compliance.underline_style_section_titles
|
1648
1639
|
next_lines = reader.peek_lines 2, style && style == 'comment'
|
1649
1640
|
is_section_title?(next_lines[0] || '', next_lines[1])
|
1650
1641
|
else
|
@@ -1698,9 +1689,8 @@ class Parser
|
|
1698
1689
|
#
|
1699
1690
|
# Returns the [Integer] section level if these lines are an setext section title, otherwise nothing.
|
1700
1691
|
def self.setext_section_title? line1, line2
|
1701
|
-
if (level = SETEXT_SECTION_LEVELS[
|
1702
|
-
|
1703
|
-
(line_length(line1) - line2_len).abs < 2
|
1692
|
+
if (level = SETEXT_SECTION_LEVELS[line2_ch0 = line2.chr]) && (uniform? line2, line2_ch0, (line2_len = line2.length)) &&
|
1693
|
+
(SetextSectionTitleRx.match? line1) && (line1.length - line2_len).abs < 2
|
1704
1694
|
level
|
1705
1695
|
end
|
1706
1696
|
end
|
@@ -1760,9 +1750,8 @@ class Parser
|
|
1760
1750
|
sect_title, sect_id, sect_reftext = (sect_title.slice 0, sect_title.length - $&.length), $2, $3
|
1761
1751
|
end unless sect_id
|
1762
1752
|
elsif Compliance.underline_style_section_titles && (line2 = reader.peek_line(true)) &&
|
1763
|
-
(sect_level = SETEXT_SECTION_LEVELS[
|
1764
|
-
|
1765
|
-
(line_length(line1) - line2_len).abs < 2
|
1753
|
+
(sect_level = SETEXT_SECTION_LEVELS[line2_ch0 = line2.chr]) && (uniform? line2, line2_ch0, (line2_len = line2.length)) &&
|
1754
|
+
(sect_title = SetextSectionTitleRx =~ line1 && $1) && (line1.length - line2_len).abs < 2
|
1766
1755
|
atx = false
|
1767
1756
|
if sect_title.end_with?(']]') && InlineSectionAnchorRx =~ sect_title && !$1 # escaped
|
1768
1757
|
sect_title, sect_id, sect_reftext = (sect_title.slice 0, sect_title.length - $&.length), $2, $3
|
@@ -1771,23 +1760,11 @@ class Parser
|
|
1771
1760
|
else
|
1772
1761
|
raise %(Unrecognized section at #{reader.cursor_at_prev_line})
|
1773
1762
|
end
|
1774
|
-
|
1775
|
-
|
1776
|
-
|
1777
|
-
|
1778
|
-
# Public: Calculate the number of unicode characters in the line, excluding the endline
|
1779
|
-
#
|
1780
|
-
# line - the String to calculate
|
1781
|
-
#
|
1782
|
-
# returns the number of unicode characters in the line
|
1783
|
-
if FORCE_UNICODE_LINE_LENGTH
|
1784
|
-
def self.line_length(line)
|
1785
|
-
line.scan(UnicodeCharScanRx).size
|
1786
|
-
end
|
1787
|
-
else
|
1788
|
-
def self.line_length(line)
|
1789
|
-
line.length
|
1763
|
+
if document.attr? 'leveloffset'
|
1764
|
+
sect_level += (document.attr 'leveloffset').to_i
|
1765
|
+
sect_level = 0 if sect_level < 0
|
1790
1766
|
end
|
1767
|
+
[sect_id, sect_reftext, sect_title, sect_level, atx]
|
1791
1768
|
end
|
1792
1769
|
|
1793
1770
|
# Public: Consume and parse the two header lines (line 1 = author info, line 2 = revision info).
|
@@ -1801,42 +1778,34 @@ class Parser
|
|
1801
1778
|
# Examples
|
1802
1779
|
#
|
1803
1780
|
# data = ["Author Name <author@example.org>\n", "v1.0, 2012-12-21: Coincide w/ end of world.\n"]
|
1804
|
-
# parse_header_metadata(Reader.new data, nil, :
|
1805
|
-
# # => {'author' => 'Author Name', 'firstname' => 'Author', 'lastname' => 'Name', 'email' => 'author@example.org',
|
1806
|
-
# # 'revnumber' => '1.0', 'revdate' => '2012-12-21', 'revremark' => 'Coincide w/ end of world.'}
|
1807
|
-
def self.parse_header_metadata
|
1781
|
+
# parse_header_metadata(Reader.new data, nil, normalize: true)
|
1782
|
+
# # => { 'author' => 'Author Name', 'firstname' => 'Author', 'lastname' => 'Name', 'email' => 'author@example.org',
|
1783
|
+
# # 'revnumber' => '1.0', 'revdate' => '2012-12-21', 'revremark' => 'Coincide w/ end of world.' }
|
1784
|
+
def self.parse_header_metadata reader, document = nil, retrieve = true
|
1808
1785
|
doc_attrs = document && document.attributes
|
1809
1786
|
# NOTE this will discard any comment lines, but not skip blank lines
|
1810
1787
|
process_attribute_entries reader, document
|
1811
1788
|
|
1812
|
-
metadata, implicit_author, implicit_authorinitials = implicit_authors = {}, nil, nil
|
1813
|
-
|
1814
1789
|
if reader.has_more_lines? && !reader.next_line_empty?
|
1815
|
-
|
1816
|
-
|
1817
|
-
|
1818
|
-
|
1819
|
-
|
1820
|
-
doc_attrs[key] = ::String === val ? (document.apply_header_subs val) : val
|
1821
|
-
end
|
1822
|
-
end
|
1823
|
-
|
1824
|
-
implicit_author = doc_attrs['author']
|
1825
|
-
implicit_authorinitials = doc_attrs['authorinitials']
|
1826
|
-
implicit_authors = doc_attrs['authors']
|
1790
|
+
authorcount = (implicit_author_metadata = process_authors reader.read_line).delete 'authorcount'
|
1791
|
+
if document && (doc_attrs['authorcount'] = authorcount) > 0
|
1792
|
+
implicit_author_metadata.each do |key, val|
|
1793
|
+
# apply header subs and assign to document; attributes substitution only relevant for email
|
1794
|
+
doc_attrs[key] = document.apply_header_subs val unless doc_attrs.key? key
|
1827
1795
|
end
|
1828
|
-
|
1829
|
-
|
1796
|
+
implicit_author = doc_attrs['author']
|
1797
|
+
implicit_authorinitials = doc_attrs['authorinitials']
|
1798
|
+
implicit_authors = doc_attrs['authors']
|
1830
1799
|
end
|
1800
|
+
implicit_author_metadata['authorcount'] = authorcount
|
1831
1801
|
|
1832
1802
|
# NOTE this will discard any comment lines, but not skip blank lines
|
1833
1803
|
process_attribute_entries reader, document
|
1834
1804
|
|
1835
|
-
rev_metadata = {}
|
1836
|
-
|
1837
1805
|
if reader.has_more_lines? && !reader.next_line_empty?
|
1838
1806
|
rev_line = reader.read_line
|
1839
|
-
if (match = RevisionInfoLineRx.match
|
1807
|
+
if (match = RevisionInfoLineRx.match rev_line)
|
1808
|
+
rev_metadata = {}
|
1840
1809
|
rev_metadata['revnumber'] = match[1].rstrip if match[1]
|
1841
1810
|
unless (component = match[2].strip).empty?
|
1842
1811
|
# version must begin with 'v' if date is absent
|
@@ -1847,31 +1816,24 @@ class Parser
|
|
1847
1816
|
end
|
1848
1817
|
end
|
1849
1818
|
rev_metadata['revremark'] = match[3].rstrip if match[3]
|
1819
|
+
if document && !rev_metadata.empty?
|
1820
|
+
# apply header subs and assign to document
|
1821
|
+
rev_metadata.each do |key, val|
|
1822
|
+
doc_attrs[key] = document.apply_header_subs val unless doc_attrs.key? key
|
1823
|
+
end
|
1824
|
+
end
|
1850
1825
|
else
|
1851
1826
|
# throw it back
|
1852
1827
|
reader.unshift_line rev_line
|
1853
1828
|
end
|
1854
1829
|
end
|
1855
1830
|
|
1856
|
-
unless rev_metadata.empty?
|
1857
|
-
if document
|
1858
|
-
# apply header subs and assign to document
|
1859
|
-
rev_metadata.each do |key, val|
|
1860
|
-
unless doc_attrs.key? key
|
1861
|
-
doc_attrs[key] = document.apply_header_subs val
|
1862
|
-
end
|
1863
|
-
end
|
1864
|
-
end
|
1865
|
-
|
1866
|
-
metadata.update rev_metadata
|
1867
|
-
end
|
1868
|
-
|
1869
1831
|
# NOTE this will discard any comment lines, but not skip blank lines
|
1870
1832
|
process_attribute_entries reader, document
|
1871
1833
|
|
1872
1834
|
reader.skip_blank_lines
|
1873
1835
|
else
|
1874
|
-
|
1836
|
+
implicit_author_metadata = {}
|
1875
1837
|
end
|
1876
1838
|
|
1877
1839
|
# process author attribute entries that override (or stand in for) the implicit author line
|
@@ -1888,7 +1850,7 @@ class Parser
|
|
1888
1850
|
while doc_attrs.key? author_key
|
1889
1851
|
# only use indexed author attribute if value is different
|
1890
1852
|
# leaves corner case if line matches with underscores converted to spaces; use double space to force
|
1891
|
-
if (author_override = doc_attrs[author_key]) ==
|
1853
|
+
if (author_override = doc_attrs[author_key]) == implicit_author_metadata[author_key]
|
1892
1854
|
authors << nil
|
1893
1855
|
sparse = true
|
1894
1856
|
else
|
@@ -1900,23 +1862,26 @@ class Parser
|
|
1900
1862
|
if explicit
|
1901
1863
|
# rebuild implicit author names to reparse
|
1902
1864
|
authors.each_with_index do |author, idx|
|
1903
|
-
|
1904
|
-
|
1905
|
-
|
1906
|
-
|
1907
|
-
|
1908
|
-
|
1909
|
-
end
|
1865
|
+
next if author
|
1866
|
+
authors[idx] = [
|
1867
|
+
implicit_author_metadata[%(firstname_#{name_idx = idx + 1})],
|
1868
|
+
implicit_author_metadata[%(middlename_#{name_idx})],
|
1869
|
+
implicit_author_metadata[%(lastname_#{name_idx})]
|
1870
|
+
].compact.map {|it| it.tr ' ', '_' }.join ' '
|
1910
1871
|
end if sparse
|
1911
1872
|
# process as names only
|
1912
1873
|
author_metadata = process_authors authors, true, false
|
1913
1874
|
else
|
1914
|
-
author_metadata = {}
|
1875
|
+
author_metadata = { 'authorcount' => 0 }
|
1915
1876
|
end
|
1916
1877
|
end
|
1917
1878
|
|
1918
|
-
if author_metadata
|
1919
|
-
|
1879
|
+
if author_metadata['authorcount'] == 0
|
1880
|
+
if authorcount
|
1881
|
+
author_metadata = nil
|
1882
|
+
else
|
1883
|
+
doc_attrs['authorcount'] = 0
|
1884
|
+
end
|
1920
1885
|
else
|
1921
1886
|
doc_attrs.update author_metadata
|
1922
1887
|
|
@@ -1927,7 +1892,7 @@ class Parser
|
|
1927
1892
|
end
|
1928
1893
|
end
|
1929
1894
|
|
1930
|
-
|
1895
|
+
implicit_author_metadata.merge rev_metadata.to_h, author_metadata.to_h if retrieve
|
1931
1896
|
end
|
1932
1897
|
|
1933
1898
|
# Internal: Parse the author line into a Hash of author metadata
|
@@ -1942,20 +1907,13 @@ class Parser
|
|
1942
1907
|
def self.process_authors author_line, names_only = false, multiple = true
|
1943
1908
|
author_metadata = {}
|
1944
1909
|
author_idx = 0
|
1945
|
-
|
1946
|
-
author_entries = multiple ? (author_line.split ';').map {|it| it.strip } : Array(author_line)
|
1947
|
-
author_entries.each do |author_entry|
|
1910
|
+
(multiple && (author_line.include? ';') ? (author_line.split AuthorDelimiterRx) : [*author_line]).each do |author_entry|
|
1948
1911
|
next if author_entry.empty?
|
1949
|
-
author_idx += 1
|
1950
1912
|
key_map = {}
|
1951
|
-
if author_idx == 1
|
1952
|
-
|
1953
|
-
key_map[key.to_sym] = key
|
1954
|
-
end
|
1913
|
+
if (author_idx += 1) == 1
|
1914
|
+
AuthorKeys.each {|key| key_map[key.to_sym] = key }
|
1955
1915
|
else
|
1956
|
-
|
1957
|
-
key_map[key.to_sym] = %(#{key}_#{author_idx})
|
1958
|
-
end
|
1916
|
+
AuthorKeys.each {|key| key_map[key.to_sym] = %(#{key}_#{author_idx}) }
|
1959
1917
|
end
|
1960
1918
|
|
1961
1919
|
if names_only # when parsing an attribute value
|
@@ -1999,9 +1957,7 @@ class Parser
|
|
1999
1957
|
else
|
2000
1958
|
# only assign the _1 attributes once we see the second author
|
2001
1959
|
if author_idx == 2
|
2002
|
-
|
2003
|
-
author_metadata[%(#{key}_1)] = author_metadata[key] if author_metadata.key? key
|
2004
|
-
end
|
1960
|
+
AuthorKeys.each {|key| author_metadata[%(#{key}_1)] = author_metadata[key] if author_metadata.key? key }
|
2005
1961
|
end
|
2006
1962
|
author_metadata['authors'] = %(#{author_metadata['authors']}, #{author_metadata[key_map[:author]]})
|
2007
1963
|
end
|
@@ -2020,7 +1976,7 @@ class Parser
|
|
2020
1976
|
# document - the current Document
|
2021
1977
|
# attributes - a Hash of attributes in which any metadata found will be stored (default: {})
|
2022
1978
|
# options - a Hash of options to control processing: (default: {})
|
2023
|
-
# * :
|
1979
|
+
# * :text_only indicates that parser is only looking for text content
|
2024
1980
|
# and thus the block title should not be captured
|
2025
1981
|
#
|
2026
1982
|
# returns the Hash of attributes including any metadata found
|
@@ -2049,13 +2005,13 @@ class Parser
|
|
2049
2005
|
# document - the current Document
|
2050
2006
|
# attributes - a Hash of attributes in which any metadata found will be stored
|
2051
2007
|
# options - a Hash of options to control processing: (default: {})
|
2052
|
-
# * :
|
2008
|
+
# * :text_only indicates the parser is only looking for text content,
|
2053
2009
|
# thus neither a block title or attribute entry should be captured
|
2054
2010
|
#
|
2055
|
-
# returns true if the line contains metadata, otherwise
|
2011
|
+
# returns true if the line contains metadata, otherwise falsy
|
2056
2012
|
def self.parse_block_metadata_line reader, document, attributes, options = {}
|
2057
2013
|
if (next_line = reader.peek_line) &&
|
2058
|
-
(options[:
|
2014
|
+
(options[:text_only] ? (next_line.start_with? '[', '/') : (normal = next_line.start_with? '[', '.', '/', ':'))
|
2059
2015
|
if next_line.start_with? '['
|
2060
2016
|
if next_line.start_with? '[['
|
2061
2017
|
if (next_line.end_with? ']]') && BlockAnchorRx =~ next_line
|
@@ -2069,7 +2025,7 @@ class Parser
|
|
2069
2025
|
elsif (next_line.end_with? ']') && BlockAttributeListRx =~ next_line
|
2070
2026
|
current_style = attributes[1]
|
2071
2027
|
# extract id, role, and options from first positional attribute and remove, if present
|
2072
|
-
if (document.parse_attributes $1, [], :
|
2028
|
+
if (document.parse_attributes $1, [], sub_input: true, sub_result: true, into: attributes)[1]
|
2073
2029
|
attributes[1] = (parse_style_attribute attributes, reader) || current_style
|
2074
2030
|
end
|
2075
2031
|
return true
|
@@ -2084,9 +2040,9 @@ class Parser
|
|
2084
2040
|
elsif !normal || (next_line.start_with? '/')
|
2085
2041
|
if next_line == '//'
|
2086
2042
|
return true
|
2087
|
-
elsif normal && '/'
|
2043
|
+
elsif normal && (uniform? next_line, '/', (ll = next_line.length))
|
2088
2044
|
unless ll == 3
|
2089
|
-
reader.read_lines_until :
|
2045
|
+
reader.read_lines_until terminator: next_line, skip_first_line: true, preserve_last_line: true, skip_processing: true, context: :comment
|
2090
2046
|
return true
|
2091
2047
|
end
|
2092
2048
|
else
|
@@ -2098,6 +2054,7 @@ class Parser
|
|
2098
2054
|
return true
|
2099
2055
|
end
|
2100
2056
|
end
|
2057
|
+
nil
|
2101
2058
|
end
|
2102
2059
|
|
2103
2060
|
# Process consecutive attribute entry lines, ignoring adjacent line comments and comment blocks.
|
@@ -2113,11 +2070,11 @@ class Parser
|
|
2113
2070
|
end
|
2114
2071
|
|
2115
2072
|
def self.process_attribute_entry reader, document, attributes = nil, match = nil
|
2116
|
-
if (match
|
2073
|
+
if match || (match = reader.has_more_lines? ? (AttributeEntryRx.match reader.peek_line) : nil)
|
2117
2074
|
if (value = match[2]).nil_or_empty?
|
2118
2075
|
value = ''
|
2119
2076
|
elsif value.end_with? LINE_CONTINUATION, LINE_CONTINUATION_LEGACY
|
2120
|
-
con, value = value.slice
|
2077
|
+
con, value = (value.slice value.length - 2, 2), (value.slice 0, value.length - 2).rstrip
|
2121
2078
|
while reader.advance && !(next_line = reader.peek_line || '').empty?
|
2122
2079
|
next_line = next_line.lstrip
|
2123
2080
|
next_line = (next_line.slice 0, next_line.length - 2).rstrip if (keep_open = next_line.end_with? con)
|
@@ -2144,15 +2101,21 @@ class Parser
|
|
2144
2101
|
# TODO move processing of attribute value to utility method
|
2145
2102
|
if name.end_with? '!'
|
2146
2103
|
# a nil value signals the attribute should be deleted (unset)
|
2147
|
-
name
|
2104
|
+
name = name.chop
|
2105
|
+
value = nil
|
2148
2106
|
elsif name.start_with? '!'
|
2149
2107
|
# a nil value signals the attribute should be deleted (unset)
|
2150
|
-
name
|
2108
|
+
name = (name.slice 1, name.length)
|
2109
|
+
value = nil
|
2151
2110
|
end
|
2152
2111
|
|
2153
|
-
name = sanitize_attribute_name name
|
2154
|
-
|
2155
|
-
|
2112
|
+
if (name = sanitize_attribute_name name) == 'numbered'
|
2113
|
+
name = 'sectnums'
|
2114
|
+
elsif name == 'hardbreaks'
|
2115
|
+
name = 'hardbreaks-option'
|
2116
|
+
elsif name == 'showtitle'
|
2117
|
+
store_attribute 'notitle', (value ? nil : ''), doc, attrs
|
2118
|
+
end
|
2156
2119
|
|
2157
2120
|
if doc
|
2158
2121
|
if value
|
@@ -2196,9 +2159,10 @@ class Parser
|
|
2196
2159
|
#
|
2197
2160
|
# Returns the String 0-index marker for this list item
|
2198
2161
|
def self.resolve_list_marker(list_type, marker, ordinal = 0, validate = false, reader = nil)
|
2199
|
-
|
2162
|
+
case list_type
|
2163
|
+
when :ulist
|
2200
2164
|
marker
|
2201
|
-
|
2165
|
+
when :olist
|
2202
2166
|
resolve_ordered_list_marker(marker, ordinal, validate, reader)[0]
|
2203
2167
|
else # :colist
|
2204
2168
|
'<1>'
|
@@ -2268,7 +2232,7 @@ class Parser
|
|
2268
2232
|
end
|
2269
2233
|
|
2270
2234
|
if validate && expected != actual
|
2271
|
-
logger.warn message_with_context %(list item index: expected #{expected}, got #{actual}), :
|
2235
|
+
logger.warn message_with_context %(list item index: expected #{expected}, got #{actual}), source_location: reader.cursor
|
2272
2236
|
end
|
2273
2237
|
|
2274
2238
|
[marker, style]
|
@@ -2281,20 +2245,12 @@ class Parser
|
|
2281
2245
|
# list_type - The context of the list (:olist, :ulist, :colist, :dlist)
|
2282
2246
|
# sibling_trait - The String marker for the list or the Regexp to match a sibling
|
2283
2247
|
#
|
2284
|
-
# Returns a Boolean indicating whether this line is a sibling list item given
|
2285
|
-
|
2286
|
-
def self.is_sibling_list_item?(line, list_type, sibling_trait)
|
2248
|
+
# Returns a Boolean indicating whether this line is a sibling list item given the criteria provided
|
2249
|
+
def self.is_sibling_list_item? line, list_type, sibling_trait
|
2287
2250
|
if ::Regexp === sibling_trait
|
2288
|
-
|
2289
|
-
else
|
2290
|
-
matcher = ListRxMap[list_type]
|
2291
|
-
expected_marker = sibling_trait
|
2292
|
-
end
|
2293
|
-
|
2294
|
-
if matcher =~ line
|
2295
|
-
expected_marker ? expected_marker == resolve_list_marker(list_type, $1) : true
|
2251
|
+
sibling_trait.match? line
|
2296
2252
|
else
|
2297
|
-
|
2253
|
+
ListRxMap[list_type] =~ line && sibling_trait == (resolve_list_marker list_type, $1)
|
2298
2254
|
end
|
2299
2255
|
end
|
2300
2256
|
|
@@ -2307,10 +2263,6 @@ class Parser
|
|
2307
2263
|
# returns an instance of Asciidoctor::Table parsed from the provided reader
|
2308
2264
|
def self.parse_table(table_reader, parent, attributes)
|
2309
2265
|
table = Table.new(parent, attributes)
|
2310
|
-
if attributes.key? 'title'
|
2311
|
-
table.title = attributes.delete 'title'
|
2312
|
-
table.assign_caption(attributes.delete 'caption')
|
2313
|
-
end
|
2314
2266
|
|
2315
2267
|
if (attributes.key? 'cols') && !(colspecs = parse_colspecs attributes['cols']).empty?
|
2316
2268
|
table.create_columns colspecs
|
@@ -2318,9 +2270,15 @@ class Parser
|
|
2318
2270
|
end
|
2319
2271
|
|
2320
2272
|
skipped = table_reader.skip_blank_lines || 0
|
2273
|
+
if attributes['header-option']
|
2274
|
+
table.has_header_option = true
|
2275
|
+
elsif skipped == 0 && !attributes['noheader-option']
|
2276
|
+
# NOTE: assume table has header until we know otherwise; if it doesn't (nil), cells in first row get reprocessed
|
2277
|
+
table.has_header_option = :implicit
|
2278
|
+
implicit_header = true
|
2279
|
+
end
|
2321
2280
|
parser_ctx = Table::ParserContext.new table_reader, table, attributes
|
2322
2281
|
format, loop_idx, implicit_header_boundary = parser_ctx.format, -1, nil
|
2323
|
-
implicit_header = true unless skipped > 0 || (attributes.key? 'header-option') || (attributes.key? 'noheader-option')
|
2324
2282
|
|
2325
2283
|
while (line = table_reader.read_line)
|
2326
2284
|
if (beyond_first = (loop_idx += 1) > 0) && line.empty?
|
@@ -2340,7 +2298,7 @@ class Parser
|
|
2340
2298
|
implicit_header_boundary = nil if implicit_header_boundary
|
2341
2299
|
# otherwise, the cell continues from previous line
|
2342
2300
|
elsif implicit_header_boundary && implicit_header_boundary == loop_idx
|
2343
|
-
implicit_header
|
2301
|
+
table.has_header_option = implicit_header = implicit_header_boundary = nil
|
2344
2302
|
end
|
2345
2303
|
end
|
2346
2304
|
end
|
@@ -2352,7 +2310,7 @@ class Parser
|
|
2352
2310
|
if table_reader.has_more_lines? && table_reader.peek_line.empty?
|
2353
2311
|
implicit_header_boundary = 1
|
2354
2312
|
else
|
2355
|
-
implicit_header =
|
2313
|
+
table.has_header_option = implicit_header = nil
|
2356
2314
|
end
|
2357
2315
|
end
|
2358
2316
|
end
|
@@ -2403,7 +2361,7 @@ class Parser
|
|
2403
2361
|
case format
|
2404
2362
|
when 'csv'
|
2405
2363
|
if parser_ctx.buffer_has_unclosed_quotes?
|
2406
|
-
implicit_header
|
2364
|
+
table.has_header_option = implicit_header = implicit_header_boundary = nil if implicit_header_boundary && loop_idx == 0
|
2407
2365
|
parser_ctx.keep_cell_open
|
2408
2366
|
else
|
2409
2367
|
parser_ctx.close_cell true
|
@@ -2425,16 +2383,8 @@ class Parser
|
|
2425
2383
|
end
|
2426
2384
|
end
|
2427
2385
|
|
2428
|
-
unless (table.attributes['colcount'] ||= table.columns.size) == 0 || explicit_colspecs
|
2429
|
-
|
2430
|
-
end
|
2431
|
-
|
2432
|
-
if implicit_header
|
2433
|
-
table.has_header_option = true
|
2434
|
-
attributes['header-option'] = ''
|
2435
|
-
attributes['options'] = (attributes.key? 'options') ? %(#{attributes['options']},header) : 'header'
|
2436
|
-
end
|
2437
|
-
|
2386
|
+
table.assign_column_widths unless (table.attributes['colcount'] ||= table.columns.size) == 0 || explicit_colspecs
|
2387
|
+
table.has_header_option = true if implicit_header
|
2438
2388
|
table.partition_header_footer attributes
|
2439
2389
|
|
2440
2390
|
table
|
@@ -2490,9 +2440,7 @@ class Parser
|
|
2490
2440
|
end
|
2491
2441
|
|
2492
2442
|
if m[1]
|
2493
|
-
1.upto(m[1].to_i) {
|
2494
|
-
specs << spec.dup
|
2495
|
-
}
|
2443
|
+
1.upto(m[1].to_i) { specs << spec.merge }
|
2496
2444
|
else
|
2497
2445
|
specs << spec
|
2498
2446
|
end
|
@@ -2517,7 +2465,7 @@ class Parser
|
|
2517
2465
|
|
2518
2466
|
if pos == :start
|
2519
2467
|
if line.include? delimiter
|
2520
|
-
spec_part, rest = line.
|
2468
|
+
spec_part, _, rest = line.partition delimiter
|
2521
2469
|
if (m = CellSpecStartRx.match spec_part)
|
2522
2470
|
return [{}, rest] if m[0].empty?
|
2523
2471
|
else
|
@@ -2526,14 +2474,12 @@ class Parser
|
|
2526
2474
|
else
|
2527
2475
|
return [nil, line]
|
2528
2476
|
end
|
2529
|
-
|
2530
|
-
if
|
2531
|
-
|
2532
|
-
|
2533
|
-
|
2534
|
-
|
2535
|
-
return [{}, line]
|
2536
|
-
end
|
2477
|
+
elsif (m = CellSpecEndRx.match line) # when pos == :end
|
2478
|
+
# NOTE return the line stripped of trailing whitespace if no cellspec is found in this case
|
2479
|
+
return [{}, line.rstrip] if m[0].lstrip.empty?
|
2480
|
+
rest = m.pre_match
|
2481
|
+
else
|
2482
|
+
return [{}, line]
|
2537
2483
|
end
|
2538
2484
|
|
2539
2485
|
spec = {}
|
@@ -2541,10 +2487,11 @@ class Parser
|
|
2541
2487
|
colspec, rowspec = m[1].split '.'
|
2542
2488
|
colspec = colspec.nil_or_empty? ? 1 : colspec.to_i
|
2543
2489
|
rowspec = rowspec.nil_or_empty? ? 1 : rowspec.to_i
|
2544
|
-
|
2490
|
+
case m[2]
|
2491
|
+
when '+'
|
2545
2492
|
spec['colspan'] = colspec unless colspec == 1
|
2546
2493
|
spec['rowspan'] = rowspec unless rowspec == 1
|
2547
|
-
|
2494
|
+
when '*'
|
2548
2495
|
spec['repeatcol'] = colspec unless colspec == 1
|
2549
2496
|
end
|
2550
2497
|
end
|
@@ -2569,7 +2516,7 @@ class Parser
|
|
2569
2516
|
# Public: Parse the first positional attribute and assign named attributes
|
2570
2517
|
#
|
2571
2518
|
# Parse the first positional attribute to extract the style, role and id
|
2572
|
-
# parts, assign the values to their
|
2519
|
+
# parts, assign the values to their corresponding attribute keys and return
|
2573
2520
|
# the parsed style from the first positional attribute.
|
2574
2521
|
#
|
2575
2522
|
# attributes - The Hash of attributes to process and update
|
@@ -2587,93 +2534,94 @@ class Parser
|
|
2587
2534
|
# "role" => "lead", "options" => "fragment", "fragment-option" => '' }
|
2588
2535
|
#
|
2589
2536
|
# Returns the String style parsed from the first positional attribute
|
2590
|
-
def self.parse_style_attribute
|
2537
|
+
def self.parse_style_attribute attributes, reader = nil
|
2591
2538
|
# NOTE spaces are not allowed in shorthand, so if we detect one, this ain't no shorthand
|
2592
2539
|
if (raw_style = attributes[1]) && !raw_style.include?(' ') && Compliance.shorthand_property_syntax
|
2593
|
-
|
2594
|
-
|
2595
|
-
|
2596
|
-
if collector.empty?
|
2597
|
-
unless type == :style
|
2598
|
-
if reader
|
2599
|
-
logger.warn message_with_context %(invalid empty #{type} detected in style attribute), :source_location => reader.cursor_at_prev_line
|
2600
|
-
else
|
2601
|
-
logger.warn %(invalid empty #{type} detected in style attribute)
|
2602
|
-
end
|
2603
|
-
end
|
2604
|
-
else
|
2605
|
-
case type
|
2606
|
-
when :role, :option
|
2607
|
-
(parsed[type] ||= []) << collector.join
|
2608
|
-
when :id
|
2609
|
-
if parsed.key? :id
|
2610
|
-
if reader
|
2611
|
-
logger.warn message_with_context 'multiple ids detected in style attribute', :source_location => reader.cursor_at_prev_line
|
2612
|
-
else
|
2613
|
-
logger.warn 'multiple ids detected in style attribute'
|
2614
|
-
end
|
2615
|
-
end
|
2616
|
-
parsed[type] = collector.join
|
2617
|
-
else
|
2618
|
-
parsed[type] = collector.join
|
2619
|
-
end
|
2620
|
-
collector = []
|
2621
|
-
end
|
2622
|
-
}
|
2540
|
+
name = nil
|
2541
|
+
accum = ''
|
2542
|
+
parsed_attrs = {}
|
2623
2543
|
|
2624
2544
|
raw_style.each_char do |c|
|
2625
|
-
|
2626
|
-
|
2627
|
-
|
2628
|
-
|
2629
|
-
|
2630
|
-
|
2631
|
-
|
2632
|
-
|
2633
|
-
|
2634
|
-
|
2545
|
+
case c
|
2546
|
+
when '.'
|
2547
|
+
yield_buffered_attribute parsed_attrs, name, accum, reader
|
2548
|
+
accum = ''
|
2549
|
+
name = :role
|
2550
|
+
when '#'
|
2551
|
+
yield_buffered_attribute parsed_attrs, name, accum, reader
|
2552
|
+
accum = ''
|
2553
|
+
name = :id
|
2554
|
+
when '%'
|
2555
|
+
yield_buffered_attribute parsed_attrs, name, accum, reader
|
2556
|
+
accum = ''
|
2557
|
+
name = :option
|
2635
2558
|
else
|
2636
|
-
|
2559
|
+
accum += c
|
2637
2560
|
end
|
2638
2561
|
end
|
2639
2562
|
|
2640
2563
|
# small optimization if no shorthand is found
|
2641
|
-
if
|
2642
|
-
|
2643
|
-
else
|
2644
|
-
save_current.call
|
2564
|
+
if name
|
2565
|
+
yield_buffered_attribute parsed_attrs, name, accum, reader
|
2645
2566
|
|
2646
|
-
parsed_style =
|
2567
|
+
if (parsed_style = parsed_attrs[:style])
|
2568
|
+
attributes['style'] = parsed_style
|
2569
|
+
end
|
2647
2570
|
|
2648
|
-
attributes['id'] =
|
2571
|
+
attributes['id'] = parsed_attrs[:id] if parsed_attrs.key? :id
|
2649
2572
|
|
2650
|
-
if
|
2651
|
-
attributes['role'] = (existing_role = attributes['role']).nil_or_empty? ? (
|
2573
|
+
if parsed_attrs.key? :role
|
2574
|
+
attributes['role'] = (existing_role = attributes['role']).nil_or_empty? ? (parsed_attrs[:role].join ' ') : %(#{existing_role} #{parsed_attrs[:role].join ' '})
|
2652
2575
|
end
|
2653
2576
|
|
2654
|
-
if
|
2655
|
-
(opts = parsed[:option]).each {|opt| attributes[%(#{opt}-option)] = '' }
|
2656
|
-
attributes['options'] = (existing_opts = attributes['options']).nil_or_empty? ? (opts.join ',') : %(#{existing_opts},#{opts.join ','})
|
2657
|
-
end
|
2577
|
+
parsed_attrs[:option].each {|opt| attributes[%(#{opt}-option)] = '' } if parsed_attrs.key? :option
|
2658
2578
|
|
2659
2579
|
parsed_style
|
2580
|
+
else
|
2581
|
+
attributes['style'] = raw_style
|
2660
2582
|
end
|
2661
2583
|
else
|
2662
2584
|
attributes['style'] = raw_style
|
2663
2585
|
end
|
2664
2586
|
end
|
2665
2587
|
|
2666
|
-
#
|
2667
|
-
|
2668
|
-
|
2669
|
-
|
2588
|
+
# Internal: Save the collected attribute (:id, :option, :role, or nil for :style) in the attribute Hash.
|
2589
|
+
def self.yield_buffered_attribute attrs, name, value, reader
|
2590
|
+
if name
|
2591
|
+
if value.empty?
|
2592
|
+
if reader
|
2593
|
+
logger.warn message_with_context %(invalid empty #{name} detected in style attribute), source_location: reader.cursor_at_prev_line
|
2594
|
+
else
|
2595
|
+
logger.warn %(invalid empty #{name} detected in style attribute)
|
2596
|
+
end
|
2597
|
+
elsif name == :id
|
2598
|
+
if attrs.key? :id
|
2599
|
+
if reader
|
2600
|
+
logger.warn message_with_context 'multiple ids detected in style attribute', source_location: reader.cursor_at_prev_line
|
2601
|
+
else
|
2602
|
+
logger.warn 'multiple ids detected in style attribute'
|
2603
|
+
end
|
2604
|
+
end
|
2605
|
+
attrs[name] = value
|
2606
|
+
else
|
2607
|
+
(attrs[name] ||= []) << value
|
2608
|
+
end
|
2609
|
+
else
|
2610
|
+
attrs[:style] = value unless value.empty?
|
2611
|
+
end
|
2612
|
+
nil
|
2613
|
+
end
|
2614
|
+
|
2615
|
+
# Remove the block indentation (the amount of whitespace of the least indented line), replace tabs with spaces (using
|
2616
|
+
# proper tab expansion logic) and, finally, indent the lines by the margin width. Modifies the input Array directly.
|
2670
2617
|
#
|
2671
|
-
# This method preserves the
|
2618
|
+
# This method preserves the significant indentation (that exceeding the block indent) on each line.
|
2672
2619
|
#
|
2673
|
-
# lines
|
2674
|
-
#
|
2675
|
-
#
|
2676
|
-
#
|
2620
|
+
# lines - The Array of String lines to process (no trailing newlines)
|
2621
|
+
# indent_size - The Integer number of spaces to readd to the start of non-empty lines after removing the indentation.
|
2622
|
+
# If this value is < 0, the existing indentation is preserved (optional, default: 0)
|
2623
|
+
# tab_size - the Integer number of spaces to use in place of a tab. A value of <= 0 disables the replacement
|
2624
|
+
# (optional, default: 0)
|
2677
2625
|
#
|
2678
2626
|
# Examples
|
2679
2627
|
#
|
@@ -2683,89 +2631,95 @@ class Parser
|
|
2683
2631
|
# end
|
2684
2632
|
# EOS
|
2685
2633
|
#
|
2686
|
-
# source.split
|
2634
|
+
# source.split ?\n
|
2687
2635
|
# # => [" def names", " @names.split", " end"]
|
2688
2636
|
#
|
2689
|
-
# puts Parser.adjust_indentation!
|
2637
|
+
# puts (Parser.adjust_indentation! source.split ?\n).join ?\n
|
2690
2638
|
# # => def names
|
2691
2639
|
# # => @names.split
|
2692
2640
|
# # => end
|
2693
2641
|
#
|
2694
2642
|
# returns Nothing
|
2695
|
-
|
2696
|
-
# QUESTION should indent be called margin?
|
2697
|
-
def self.adjust_indentation! lines, indent = 0, tab_size = 0
|
2643
|
+
def self.adjust_indentation! lines, indent_size = 0, tab_size = 0
|
2698
2644
|
return if lines.empty?
|
2699
2645
|
|
2700
|
-
# expand tabs if a tab is detected
|
2701
|
-
if
|
2702
|
-
#if (tab_size = tab_size.to_i) > 0 && (lines.index {|line| line.include? TAB })
|
2646
|
+
# expand tabs if a tab character is detected and tab_size > 0
|
2647
|
+
if tab_size > 0 && lines.any? {|line| line.include? TAB }
|
2703
2648
|
full_tab_space = ' ' * tab_size
|
2704
2649
|
lines.map! do |line|
|
2705
|
-
|
2706
|
-
|
2707
|
-
|
2708
|
-
|
2709
|
-
|
2710
|
-
|
2650
|
+
if line.empty? || (tab_idx = line.index TAB).nil?
|
2651
|
+
line
|
2652
|
+
else
|
2653
|
+
if tab_idx == 0
|
2654
|
+
leading_tabs = 0
|
2655
|
+
line.each_byte do |b|
|
2656
|
+
break unless b == 9
|
2657
|
+
leading_tabs += 1
|
2658
|
+
end
|
2659
|
+
line = %(#{full_tab_space * leading_tabs}#{line.slice leading_tabs, line.length})
|
2660
|
+
next line unless line.include? TAB
|
2661
|
+
end
|
2711
2662
|
# keeps track of how many spaces were added to adjust offset in match data
|
2712
2663
|
spaces_added = 0
|
2713
|
-
|
2714
|
-
|
2715
|
-
|
2716
|
-
if
|
2717
|
-
|
2718
|
-
|
2719
|
-
|
2720
|
-
|
2721
|
-
|
2664
|
+
idx = 0
|
2665
|
+
result = ''
|
2666
|
+
line.each_char do |c|
|
2667
|
+
if c == TAB
|
2668
|
+
# calculate how many spaces this tab represents, then replace tab with spaces
|
2669
|
+
if (offset = idx + spaces_added) % tab_size == 0
|
2670
|
+
spaces_added += tab_size - 1
|
2671
|
+
result += full_tab_space
|
2672
|
+
else
|
2673
|
+
unless (spaces = tab_size - offset % tab_size) == 1
|
2674
|
+
spaces_added += spaces - 1
|
2675
|
+
end
|
2676
|
+
result += ' ' * spaces
|
2722
2677
|
end
|
2723
|
-
|
2678
|
+
else
|
2679
|
+
result += c
|
2724
2680
|
end
|
2725
|
-
|
2726
|
-
|
2727
|
-
|
2681
|
+
idx += 1
|
2682
|
+
end
|
2683
|
+
result
|
2728
2684
|
end
|
2729
2685
|
end
|
2730
2686
|
end
|
2731
2687
|
|
2732
|
-
# skip
|
2733
|
-
return
|
2688
|
+
# skip block indent adjustment if indent_size is < 0
|
2689
|
+
return if indent_size < 0
|
2734
2690
|
|
2735
|
-
# determine
|
2736
|
-
|
2691
|
+
# determine block indent (assumes no whitespace-only lines are present)
|
2692
|
+
block_indent = nil
|
2737
2693
|
lines.each do |line|
|
2738
2694
|
next if line.empty?
|
2739
|
-
# NOTE this logic assumes no whitespace-only lines
|
2740
2695
|
if (line_indent = line.length - line.lstrip.length) == 0
|
2741
|
-
|
2696
|
+
block_indent = nil
|
2742
2697
|
break
|
2743
|
-
else
|
2744
|
-
unless gutter_width && line_indent > gutter_width
|
2745
|
-
gutter_width = line_indent
|
2746
|
-
end
|
2747
2698
|
end
|
2699
|
+
block_indent = line_indent unless block_indent && block_indent < line_indent
|
2748
2700
|
end
|
2749
2701
|
|
2750
|
-
# remove
|
2751
|
-
# NOTE
|
2752
|
-
if
|
2753
|
-
if
|
2754
|
-
lines.map! {|line| line.empty? ? line : (line.slice gutter_width, line.length) }
|
2755
|
-
end
|
2702
|
+
# remove block indent then apply indent_size if specified
|
2703
|
+
# NOTE block_indent is > 0 if not nil
|
2704
|
+
if indent_size == 0
|
2705
|
+
lines.map! {|line| line.empty? ? line : (line.slice block_indent, line.length) } if block_indent
|
2756
2706
|
else
|
2757
|
-
|
2758
|
-
if
|
2759
|
-
lines.map! {|line| line.empty? ? line :
|
2707
|
+
new_block_indent = ' ' * indent_size
|
2708
|
+
if block_indent
|
2709
|
+
lines.map! {|line| line.empty? ? line : new_block_indent + (line.slice block_indent, line.length) }
|
2760
2710
|
else
|
2761
|
-
lines.map! {|line| line.empty? ? line :
|
2711
|
+
lines.map! {|line| line.empty? ? line : new_block_indent + line }
|
2762
2712
|
end
|
2763
2713
|
end
|
2764
2714
|
|
2765
2715
|
nil
|
2766
2716
|
end
|
2767
2717
|
|
2768
|
-
|
2718
|
+
def self.uniform? str, chr, len
|
2719
|
+
(str.count chr) == len
|
2720
|
+
end
|
2721
|
+
|
2722
|
+
# Internal: Convert a string to a legal attribute name.
|
2769
2723
|
#
|
2770
2724
|
# name - the String name of the attribute
|
2771
2725
|
#
|