asciidoctor 1.5.6.2 → 1.5.7
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of asciidoctor might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.adoc +330 -143
- data/README-fr.adoc +441 -0
- data/README-jp.adoc +418 -0
- data/README-zh_CN.adoc +430 -0
- data/README.adoc +454 -0
- data/Rakefile +57 -0
- data/asciidoctor.gemspec +7 -1
- data/data/locale/attributes-ar.adoc +22 -0
- data/data/locale/attributes-bg.adoc +22 -0
- data/data/locale/attributes-ca.adoc +22 -0
- data/data/locale/attributes-cs.adoc +22 -0
- data/data/locale/attributes-da.adoc +22 -0
- data/data/locale/attributes-de.adoc +22 -0
- data/data/locale/attributes-en.adoc +23 -0
- data/data/locale/attributes-es.adoc +22 -0
- data/data/locale/attributes-fa.adoc +22 -0
- data/data/locale/attributes-fi.adoc +22 -0
- data/data/locale/attributes-fr.adoc +22 -0
- data/data/locale/attributes-hu.adoc +22 -0
- data/data/locale/attributes-id.adoc +22 -0
- data/data/locale/attributes-it.adoc +22 -0
- data/data/locale/attributes-ja.adoc +22 -0
- data/data/locale/attributes-kr.adoc +22 -0
- data/data/locale/attributes-nb.adoc +22 -0
- data/data/locale/attributes-nl.adoc +22 -0
- data/data/locale/attributes-nn.adoc +22 -0
- data/data/locale/attributes-pl.adoc +22 -0
- data/data/locale/attributes-pt.adoc +22 -0
- data/data/locale/attributes-pt_BR.adoc +22 -0
- data/data/locale/attributes-ro.adoc +22 -0
- data/data/locale/attributes-ru.adoc +22 -0
- data/data/locale/attributes-sr.adoc +22 -0
- data/data/locale/attributes-sr_Latn.adoc +22 -0
- data/data/locale/attributes-tr.adoc +22 -0
- data/data/locale/attributes-uk.adoc +22 -0
- data/data/locale/attributes-zh_CN.adoc +22 -0
- data/data/locale/attributes-zh_TW.adoc +22 -0
- data/data/locale/attributes.adoc +8 -649
- data/data/stylesheets/asciidoctor-default.css +77 -72
- data/features/xref.feature +366 -7
- data/lib/asciidoctor.rb +107 -93
- data/lib/asciidoctor/abstract_block.rb +247 -239
- data/lib/asciidoctor/abstract_node.rb +56 -58
- data/lib/asciidoctor/block.rb +3 -3
- data/lib/asciidoctor/callouts.rb +1 -1
- data/lib/asciidoctor/cli/invoker.rb +36 -9
- data/lib/asciidoctor/cli/options.rb +63 -25
- data/lib/asciidoctor/converter.rb +23 -13
- data/lib/asciidoctor/converter/base.rb +4 -0
- data/lib/asciidoctor/converter/docbook45.rb +16 -9
- data/lib/asciidoctor/converter/docbook5.rb +115 -97
- data/lib/asciidoctor/converter/factory.rb +29 -31
- data/lib/asciidoctor/converter/html5.rb +229 -192
- data/lib/asciidoctor/converter/manpage.rb +72 -50
- data/lib/asciidoctor/converter/template.rb +12 -12
- data/lib/asciidoctor/core_ext.rb +5 -1
- data/lib/asciidoctor/core_ext/1.8.7/base64/strict_encode64.rb +6 -0
- data/lib/asciidoctor/document.rb +168 -77
- data/lib/asciidoctor/extensions.rb +79 -47
- data/lib/asciidoctor/helpers.rb +33 -11
- data/lib/asciidoctor/inline.rb +3 -2
- data/lib/asciidoctor/list.rb +2 -1
- data/lib/asciidoctor/logging.rb +122 -0
- data/lib/asciidoctor/parser.rb +406 -382
- data/lib/asciidoctor/path_resolver.rb +169 -162
- data/lib/asciidoctor/reader.rb +166 -121
- data/lib/asciidoctor/section.rb +45 -28
- data/lib/asciidoctor/stylesheets.rb +13 -5
- data/lib/asciidoctor/substitutors.rb +328 -254
- data/lib/asciidoctor/table.rb +105 -48
- data/lib/asciidoctor/timings.rb +34 -6
- data/lib/asciidoctor/version.rb +1 -1
- data/man/asciidoctor.1 +41 -23
- data/man/asciidoctor.adoc +14 -8
- data/test/api_test.rb +1004 -0
- data/test/attributes_test.rb +241 -50
- data/test/blocks_test.rb +549 -124
- data/test/converter_test.rb +170 -78
- data/test/document_test.rb +208 -767
- data/test/extensions_test.rb +188 -53
- data/test/fixtures/custom-backends/slim/html5/block_paragraph.html.slim +1 -1
- data/test/fixtures/custom-backends/slim/html5/block_sidebar.html.slim +1 -1
- data/test/fixtures/file-with-missing-include.adoc +1 -0
- data/test/fixtures/include-file.jsx +8 -0
- data/test/fixtures/lists.adoc +96 -0
- data/test/fixtures/other-chapters.adoc +11 -0
- data/test/fixtures/outer-include.adoc +5 -0
- data/test/fixtures/sample.asciidoc +5 -1
- data/test/fixtures/subdir/index.adoc +3 -0
- data/test/fixtures/subdir/inner-include.adoc +3 -0
- data/test/fixtures/subdir/middle-include.adoc +5 -0
- data/test/fixtures/tagged-class-enclosed.rb +0 -1
- data/test/fixtures/unclosed-tag.adoc +3 -0
- data/test/fixtures/unexpected-end-tag.adoc +4 -0
- data/test/invoker_test.rb +101 -40
- data/test/links_test.rb +266 -72
- data/test/lists_test.rb +243 -45
- data/test/logger_test.rb +211 -0
- data/test/manpage_test.rb +124 -6
- data/test/options_test.rb +46 -1
- data/test/paragraphs_test.rb +23 -10
- data/test/parser_test.rb +30 -1
- data/test/paths_test.rb +115 -33
- data/test/preamble_test.rb +1 -1
- data/test/reader_test.rb +337 -81
- data/test/sections_test.rb +656 -72
- data/test/substitutions_test.rb +182 -57
- data/test/tables_test.rb +324 -57
- data/test/test_helper.rb +77 -32
- data/test/text_test.rb +7 -7
- metadata +67 -3
data/lib/asciidoctor/parser.rb
CHANGED
@@ -23,6 +23,7 @@ module Asciidoctor
|
|
23
23
|
# block.class
|
24
24
|
# # => Asciidoctor::Block
|
25
25
|
class Parser
|
26
|
+
include Logging
|
26
27
|
|
27
28
|
BlockMatchData = Struct.new :context, :masq, :tip, :terminator
|
28
29
|
|
@@ -93,7 +94,10 @@ class Parser
|
|
93
94
|
|
94
95
|
while reader.has_more_lines?
|
95
96
|
new_section, block_attributes = next_section(reader, document, block_attributes)
|
96
|
-
|
97
|
+
if new_section
|
98
|
+
document.assign_numeral new_section
|
99
|
+
document.blocks << new_section
|
100
|
+
end
|
97
101
|
end unless options[:header_only]
|
98
102
|
|
99
103
|
document
|
@@ -113,65 +117,63 @@ class Parser
|
|
113
117
|
# returns the Hash of orphan block attributes captured above the header
|
114
118
|
def self.parse_document_header(reader, document)
|
115
119
|
# capture lines of block-level metadata and plow away comment lines that precede first block
|
116
|
-
|
120
|
+
block_attrs = parse_block_metadata_lines reader, document
|
121
|
+
doc_attrs = document.attributes
|
117
122
|
|
118
123
|
# special case, block title is not allowed above document title,
|
119
124
|
# carry attributes over to the document body
|
120
|
-
if (implicit_doctitle = is_next_line_doctitle? reader,
|
121
|
-
|
122
|
-
return document.finalize_header block_attributes, false
|
125
|
+
if (implicit_doctitle = is_next_line_doctitle? reader, block_attrs, doc_attrs['leveloffset']) && block_attrs['title']
|
126
|
+
return document.finalize_header block_attrs, false
|
123
127
|
end
|
124
128
|
|
125
129
|
# yep, document title logic in AsciiDoc is just insanity
|
126
130
|
# definitely an area for spec refinement
|
127
131
|
assigned_doctitle = nil
|
128
|
-
unless (val =
|
132
|
+
unless (val = doc_attrs['doctitle']).nil_or_empty?
|
129
133
|
document.title = assigned_doctitle = val
|
130
134
|
end
|
131
135
|
|
132
|
-
section_title = nil
|
133
136
|
# if the first line is the document title, add a header to the document and parse the header metadata
|
134
137
|
if implicit_doctitle
|
135
138
|
source_location = reader.cursor if document.sourcemap
|
136
139
|
document.id, _, doctitle, _, atx = parse_section_title reader, document
|
137
|
-
unless assigned_doctitle
|
138
|
-
|
139
|
-
end
|
140
|
+
document.title = assigned_doctitle = doctitle unless assigned_doctitle
|
141
|
+
document.header.source_location = source_location if source_location
|
140
142
|
# default to compat-mode if document uses atx-style doctitle
|
141
|
-
|
142
|
-
if (separator =
|
143
|
-
|
143
|
+
doc_attrs['compat-mode'] = '' unless atx || (document.attribute_locked? 'compat-mode')
|
144
|
+
if (separator = block_attrs['separator'])
|
145
|
+
doc_attrs['title-separator'] = separator unless document.attribute_locked? 'title-separator'
|
144
146
|
end
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
if document.id
|
149
|
-
block_attributes.delete 1
|
150
|
-
block_attributes.delete 'id'
|
147
|
+
doc_attrs['doctitle'] = section_title = doctitle
|
148
|
+
if (doc_id = block_attrs['id'])
|
149
|
+
document.id = doc_id
|
151
150
|
else
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
end
|
157
|
-
document.id = block_attributes.delete 'id'
|
151
|
+
doc_id = document.id
|
152
|
+
end
|
153
|
+
if (doc_role = block_attrs['role'])
|
154
|
+
doc_attrs['docrole'] = doc_role
|
158
155
|
end
|
156
|
+
if (doc_reftext = block_attrs['reftext'])
|
157
|
+
doc_attrs['reftext'] = doc_reftext
|
158
|
+
end
|
159
|
+
block_attrs = {}
|
159
160
|
parse_header_metadata reader, document
|
161
|
+
document.register :refs, [doc_id, document] if doc_id
|
160
162
|
end
|
161
163
|
|
162
|
-
unless (val =
|
164
|
+
unless (val = doc_attrs['doctitle']).nil_or_empty? || val == section_title
|
163
165
|
document.title = assigned_doctitle = val
|
164
166
|
end
|
165
167
|
|
166
168
|
# restore doctitle attribute to original assignment
|
167
|
-
|
169
|
+
doc_attrs['doctitle'] = assigned_doctitle if assigned_doctitle
|
168
170
|
|
169
171
|
# parse title and consume name section of manpage document
|
170
172
|
parse_manpage_header(reader, document) if document.doctype == 'manpage'
|
171
173
|
|
172
|
-
# NOTE
|
174
|
+
# NOTE block_attrs are the block-level attributes (not document attributes) that
|
173
175
|
# precede the first line of content (document title, first section or first block)
|
174
|
-
document.finalize_header
|
176
|
+
document.finalize_header block_attrs
|
175
177
|
end
|
176
178
|
|
177
179
|
# Public: Parses the manpage header of the AsciiDoc source read from the Reader
|
@@ -179,10 +181,10 @@ class Parser
|
|
179
181
|
# returns Nothing
|
180
182
|
def self.parse_manpage_header(reader, document)
|
181
183
|
if ManpageTitleVolnumRx =~ document.attributes['doctitle']
|
182
|
-
document.attributes['mantitle'] = document.sub_attributes $1.downcase
|
184
|
+
document.attributes['mantitle'] = (($1.include? ATTR_REF_HEAD) ? (document.sub_attributes $1) : $1).downcase
|
183
185
|
document.attributes['manvolnum'] = $2
|
184
186
|
else
|
185
|
-
|
187
|
+
logger.error message_with_context 'malformed manpage title', :source_location => reader.cursor_at_prev_line
|
186
188
|
# provide sensible fallbacks
|
187
189
|
document.attributes['mantitle'] = document.attributes['doctitle']
|
188
190
|
document.attributes['manvolnum'] = '1'
|
@@ -190,28 +192,36 @@ class Parser
|
|
190
192
|
|
191
193
|
reader.skip_blank_lines
|
192
194
|
|
193
|
-
if is_next_line_section?
|
194
|
-
name_section = initialize_section
|
195
|
+
if is_next_line_section? reader, {}
|
196
|
+
name_section = initialize_section reader, document, {}
|
195
197
|
if name_section.level == 1
|
196
|
-
name_section_buffer = reader.read_lines_until
|
197
|
-
if
|
198
|
-
document.attributes['manname']
|
199
|
-
document.attributes['
|
200
|
-
|
198
|
+
name_section_buffer = (reader.read_lines_until :break_on_blank_lines => true, :skip_line_comments => true) * ' '
|
199
|
+
if ManpageNamePurposeRx =~ name_section_buffer
|
200
|
+
document.attributes['manname-title'] ||= name_section.title
|
201
|
+
document.attributes['manname-id'] = name_section.id if name_section.id
|
202
|
+
document.attributes['manpurpose'] = $2
|
203
|
+
if (manname = ($1.include? ATTR_REF_HEAD) ? (document.sub_attributes $1) : $1).include? ','
|
204
|
+
manname = (mannames = (manname.split ',').map {|n| n.lstrip })[0]
|
205
|
+
else
|
206
|
+
mannames = [manname]
|
207
|
+
end
|
208
|
+
document.attributes['manname'] = manname
|
209
|
+
document.attributes['mannames'] = mannames
|
201
210
|
|
202
211
|
if document.backend == 'manpage'
|
203
|
-
document.attributes['docname'] =
|
212
|
+
document.attributes['docname'] = manname
|
204
213
|
document.attributes['outfilesuffix'] = %(.#{document.attributes['manvolnum']})
|
205
214
|
end
|
206
215
|
else
|
207
|
-
|
216
|
+
logger.error message_with_context 'malformed name section body', :source_location => reader.cursor_at_prev_line
|
208
217
|
end
|
209
218
|
else
|
210
|
-
|
219
|
+
logger.error message_with_context 'name section title must be at level 1', :source_location => reader.cursor_at_prev_line
|
211
220
|
end
|
212
221
|
else
|
213
|
-
|
222
|
+
logger.error message_with_context 'name section expected', :source_location => reader.cursor_at_prev_line
|
214
223
|
end
|
224
|
+
nil
|
215
225
|
end
|
216
226
|
|
217
227
|
# Public: Return the next section from the Reader.
|
@@ -254,37 +264,41 @@ class Parser
|
|
254
264
|
def self.next_section reader, parent, attributes = {}
|
255
265
|
preamble = intro = part = false
|
256
266
|
|
257
|
-
# FIXME if attributes[1] is a verbatim style, then don't check for section
|
258
|
-
|
259
267
|
# check if we are at the start of processing the document
|
260
268
|
# NOTE we could drop a hint in the attributes to indicate
|
261
269
|
# that we are at a section title (so we don't have to check)
|
262
270
|
if parent.context == :document && parent.blocks.empty? && ((has_header = parent.has_header?) ||
|
263
271
|
(attributes.delete 'invalid-header') || !(is_next_line_section? reader, attributes))
|
264
|
-
|
265
|
-
if has_header || (
|
272
|
+
book = (document = parent).doctype == 'book'
|
273
|
+
if has_header || (book && attributes[1] != 'abstract')
|
266
274
|
preamble = intro = (Block.new parent, :preamble, :content_model => :compound)
|
267
|
-
preamble.title = parent.attr 'preface-title' if
|
268
|
-
parent << preamble
|
275
|
+
preamble.title = parent.attr 'preface-title' if book && (parent.attr? 'preface-title')
|
276
|
+
parent.blocks << preamble
|
269
277
|
end
|
270
278
|
section = parent
|
271
|
-
|
272
279
|
current_level = 0
|
273
280
|
if parent.attributes.key? 'fragment'
|
274
|
-
|
281
|
+
expected_next_level = -1
|
275
282
|
# small tweak to allow subsequent level-0 sections for book doctype
|
276
|
-
elsif
|
277
|
-
|
283
|
+
elsif book
|
284
|
+
expected_next_level, expected_next_level_alt = 1, 0
|
278
285
|
else
|
279
|
-
|
286
|
+
expected_next_level = 1
|
280
287
|
end
|
281
288
|
else
|
282
|
-
|
289
|
+
book = (document = parent.document).doctype == 'book'
|
283
290
|
section = initialize_section reader, parent, attributes
|
284
291
|
# clear attributes except for title attribute, which must be carried over to next content block
|
285
292
|
attributes = (title = attributes['title']) ? { 'title' => title } : {}
|
286
|
-
|
287
|
-
|
293
|
+
expected_next_level = (current_level = section.level) + 1
|
294
|
+
if current_level == 0
|
295
|
+
part = book
|
296
|
+
elsif current_level == 1 && section.special
|
297
|
+
# NOTE technically preface and abstract sections are only permitted in the book doctype
|
298
|
+
unless (sectname = section.sectname) == 'appendix' || sectname == 'preface' || sectname == 'abstract'
|
299
|
+
expected_next_level = nil
|
300
|
+
end
|
301
|
+
end
|
288
302
|
end
|
289
303
|
|
290
304
|
reader.skip_blank_lines
|
@@ -300,28 +314,32 @@ class Parser
|
|
300
314
|
# otherwise subsequent metadata lines get interpreted as block content
|
301
315
|
while reader.has_more_lines?
|
302
316
|
parse_block_metadata_lines reader, document, attributes
|
303
|
-
|
304
317
|
if (next_level = is_next_line_section?(reader, attributes))
|
305
318
|
next_level += document.attr('leveloffset').to_i if document.attr?('leveloffset')
|
306
|
-
if next_level > current_level
|
307
|
-
if
|
308
|
-
|
309
|
-
|
310
|
-
|
319
|
+
if next_level > current_level
|
320
|
+
if expected_next_level
|
321
|
+
unless next_level == expected_next_level || (expected_next_level_alt && next_level == expected_next_level_alt) || expected_next_level < 0
|
322
|
+
expected_condition = expected_next_level_alt ? %(expected levels #{expected_next_level_alt} or #{expected_next_level}) : %(expected level #{expected_next_level})
|
323
|
+
logger.warn message_with_context %(section title out of sequence: #{expected_condition}, got level #{next_level}), :source_location => reader.cursor
|
324
|
+
end
|
325
|
+
else
|
326
|
+
logger.error message_with_context %(#{sectname} sections do not support nested sections), :source_location => reader.cursor
|
311
327
|
end
|
312
|
-
# the attributes returned are those that are orphaned
|
313
328
|
new_section, attributes = next_section reader, section, attributes
|
314
|
-
section
|
329
|
+
section.assign_numeral new_section
|
330
|
+
section.blocks << new_section
|
331
|
+
elsif next_level == 0 && section == document
|
332
|
+
logger.error message_with_context 'level 0 sections can only be used when doctype is book', :source_location => reader.cursor unless book
|
333
|
+
new_section, attributes = next_section reader, section, attributes
|
334
|
+
section.assign_numeral new_section
|
335
|
+
section.blocks << new_section
|
315
336
|
else
|
316
|
-
if next_level == 0 && doctype != 'book'
|
317
|
-
warn %(asciidoctor: ERROR: #{reader.line_info}: only book doctypes can contain level 0 sections)
|
318
|
-
end
|
319
337
|
# close this section (and break out of the nesting) to begin a new one
|
320
338
|
break
|
321
339
|
end
|
322
340
|
else
|
323
341
|
# just take one block or else we run the risk of overrunning section boundaries
|
324
|
-
|
342
|
+
block_cursor = reader.cursor
|
325
343
|
if (new_block = next_block reader, intro || section, attributes, :parse_metadata => false)
|
326
344
|
# REVIEW this may be doing too much
|
327
345
|
if part
|
@@ -334,36 +352,32 @@ class Parser
|
|
334
352
|
new_block.style = 'partintro'
|
335
353
|
# emulate [partintro] open block
|
336
354
|
else
|
337
|
-
intro = Block.new section, :open, :content_model => :compound
|
355
|
+
new_block.parent = (intro = Block.new section, :open, :content_model => :compound)
|
338
356
|
intro.style = 'partintro'
|
339
|
-
|
340
|
-
section << intro
|
357
|
+
section.blocks << intro
|
341
358
|
end
|
342
359
|
end
|
343
360
|
elsif section.blocks.size == 1
|
344
361
|
first_block = section.blocks[0]
|
345
362
|
# open the [partintro] open block for appending
|
346
363
|
if !intro && first_block.content_model == :compound
|
347
|
-
|
348
|
-
warn %(asciidoctor: ERROR: #{block_line_info}: illegal block content outside of partintro block)
|
364
|
+
logger.error message_with_context 'illegal block content outside of partintro block', :source_location => block_cursor
|
349
365
|
# rebuild [partintro] paragraph as an open block
|
350
366
|
elsif first_block.content_model != :compound
|
351
|
-
intro = Block.new section, :open, :content_model => :compound
|
367
|
+
new_block.parent = (intro = Block.new section, :open, :content_model => :compound)
|
352
368
|
intro.style = 'partintro'
|
353
369
|
section.blocks.shift
|
354
370
|
if first_block.style == 'partintro'
|
355
371
|
first_block.context = :paragraph
|
356
372
|
first_block.style = nil
|
357
373
|
end
|
358
|
-
first_block.parent = intro
|
359
374
|
intro << first_block
|
360
|
-
|
361
|
-
section << intro
|
375
|
+
section.blocks << intro
|
362
376
|
end
|
363
377
|
end
|
364
378
|
end
|
365
379
|
|
366
|
-
(intro || section) << new_block
|
380
|
+
(intro || section).blocks << new_block
|
367
381
|
attributes = {}
|
368
382
|
#else
|
369
383
|
# # don't clear attributes if we don't find a block because they may
|
@@ -376,18 +390,17 @@ class Parser
|
|
376
390
|
|
377
391
|
if part
|
378
392
|
unless section.blocks? && section.blocks[-1].context == :section
|
379
|
-
|
393
|
+
logger.error message_with_context 'invalid part, must have at least one section (e.g., chapter, appendix, etc.)', :source_location => reader.cursor
|
380
394
|
end
|
381
395
|
# NOTE we could try to avoid creating a preamble in the first place, though
|
382
396
|
# that would require reworking assumptions in next_section since the preamble
|
383
397
|
# is treated like an untitled section
|
384
398
|
elsif preamble # implies parent == document
|
385
399
|
if preamble.blocks?
|
386
|
-
# unwrap standalone preamble (i.e., no sections), if permissible
|
387
|
-
|
400
|
+
# unwrap standalone preamble (i.e., document has no sections) except for books, if permissible
|
401
|
+
unless book || document.blocks[1] || !Compliance.unwrap_standalone_preamble
|
388
402
|
document.blocks.shift
|
389
403
|
while (child_block = preamble.blocks.shift)
|
390
|
-
child_block.parent = document
|
391
404
|
document << child_block
|
392
405
|
end
|
393
406
|
end
|
@@ -453,10 +466,9 @@ class Parser
|
|
453
466
|
end
|
454
467
|
|
455
468
|
# QUESTION should we introduce a parsing context object?
|
456
|
-
|
457
|
-
|
469
|
+
reader.mark
|
470
|
+
this_line, doc_attrs, style = reader.read_line, document.attributes, attributes[1]
|
458
471
|
block = block_context = cloaked_context = terminator = nil
|
459
|
-
style = attributes[1] ? (parse_style_attribute attributes, reader) : nil
|
460
472
|
|
461
473
|
if (delimited_block = is_delimited_block? this_line, true)
|
462
474
|
block_context = cloaked_context = delimited_block.context
|
@@ -471,7 +483,7 @@ class Parser
|
|
471
483
|
elsif block_extensions && extensions.registered_for_block?(style, block_context)
|
472
484
|
block_context = style.to_sym
|
473
485
|
else
|
474
|
-
warn %(
|
486
|
+
logger.warn message_with_context %(invalid style for #{block_context} block: #{style}), :source_location => reader.cursor_at_mark
|
475
487
|
style = block_context.to_s
|
476
488
|
end
|
477
489
|
end
|
@@ -517,24 +529,26 @@ class Parser
|
|
517
529
|
break
|
518
530
|
# NOTE very rare that a text-only line will end in ] (e.g., inline macro), so check that first
|
519
531
|
elsif (this_line.end_with? ']') && (this_line.include? '::')
|
520
|
-
#if (this_line.start_with? 'image', 'video', 'audio') &&
|
521
|
-
if (ch0 == 'i' || (this_line.start_with? 'video:', 'audio:')) &&
|
522
|
-
blk_ctx, target =
|
523
|
-
block = Block.new
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
532
|
+
#if (this_line.start_with? 'image', 'video', 'audio') && BlockMediaMacroRx =~ this_line
|
533
|
+
if (ch0 == 'i' || (this_line.start_with? 'video:', 'audio:')) && BlockMediaMacroRx =~ this_line
|
534
|
+
blk_ctx, target, blk_attrs = $1.to_sym, $2, $3
|
535
|
+
block = Block.new parent, blk_ctx, :content_model => :empty
|
536
|
+
if blk_attrs
|
537
|
+
case blk_ctx
|
538
|
+
when :video
|
539
|
+
posattrs = ['poster', 'width', 'height']
|
540
|
+
when :audio
|
541
|
+
posattrs = []
|
542
|
+
else # :image
|
543
|
+
posattrs = ['alt', 'width', 'height']
|
544
|
+
end
|
545
|
+
block.parse_attributes blk_attrs, posattrs, :sub_input => true, :sub_result => false, :into => attributes
|
531
546
|
end
|
532
|
-
block.parse_attributes(match[3], posattrs, :sub_input => true, :sub_result => false, :into => attributes)
|
533
547
|
# style doesn't have special meaning for media macros
|
534
548
|
attributes.delete 'style' if attributes.key? 'style'
|
535
549
|
if (target.include? ATTR_REF_HEAD) && (target = block.sub_attributes target, :attribute_missing => 'drop-line').empty?
|
536
550
|
# retain as unparsed if attribute-missing is skip
|
537
|
-
if
|
551
|
+
if (doc_attrs['attribute-missing'] || Compliance.attribute_missing) == 'skip'
|
538
552
|
return Block.new(parent, :paragraph, :content_model => :simple, :source => [this_line])
|
539
553
|
# otherwise, drop the line
|
540
554
|
else
|
@@ -543,7 +557,7 @@ class Parser
|
|
543
557
|
end
|
544
558
|
end
|
545
559
|
if blk_ctx == :image
|
546
|
-
|
560
|
+
document.register :images, target
|
547
561
|
# NOTE style is the value of the first positional attribute in the block attribute line
|
548
562
|
attributes['alt'] ||= style || (attributes['default-alt'] = Helpers.basename(target, true).tr('_-', ' '))
|
549
563
|
unless (scaledwidth = attributes.delete 'scaledwidth').nil_or_empty?
|
@@ -556,22 +570,20 @@ class Parser
|
|
556
570
|
attributes['target'] = target
|
557
571
|
break
|
558
572
|
|
559
|
-
elsif ch0 == 't' && (this_line.start_with? 'toc:') &&
|
560
|
-
block = Block.new
|
561
|
-
block.parse_attributes(
|
573
|
+
elsif ch0 == 't' && (this_line.start_with? 'toc:') && BlockTocMacroRx =~ this_line
|
574
|
+
block = Block.new parent, :toc, :content_model => :empty
|
575
|
+
block.parse_attributes($1, [], :sub_result => false, :into => attributes) if $1
|
562
576
|
break
|
563
577
|
|
564
|
-
elsif block_macro_extensions &&
|
565
|
-
(extension = extensions.registered_for_block_macro?
|
566
|
-
target =
|
567
|
-
content = match[3]
|
578
|
+
elsif block_macro_extensions && CustomBlockMacroRx =~ this_line &&
|
579
|
+
(extension = extensions.registered_for_block_macro? $1)
|
580
|
+
target, content = $2, $3
|
568
581
|
if extension.config[:content_model] == :attributes
|
569
|
-
|
570
|
-
document.parse_attributes
|
571
|
-
:sub_input => true, :sub_result => false, :into => attributes)
|
582
|
+
if content
|
583
|
+
document.parse_attributes content, extension.config[:pos_attrs] || [], :sub_input => true, :sub_result => false, :into => attributes
|
572
584
|
end
|
573
585
|
else
|
574
|
-
attributes['text'] = content
|
586
|
+
attributes['text'] = content || ''
|
575
587
|
end
|
576
588
|
if (default_attrs = extension.config[:default_attrs])
|
577
589
|
attributes.update(default_attrs) {|_, old_v| old_v }
|
@@ -594,23 +606,22 @@ class Parser
|
|
594
606
|
block = List.new(parent, :colist)
|
595
607
|
attributes['style'] = 'arabic'
|
596
608
|
reader.unshift_line this_line
|
597
|
-
|
609
|
+
next_index = 1
|
598
610
|
# NOTE skip the match on the first time through as we've already done it (emulates begin...while)
|
599
|
-
while match || (
|
600
|
-
list_item_lineno = reader.lineno
|
611
|
+
while match || ((match = CalloutListRx.match reader.peek_line) && reader.mark)
|
601
612
|
# might want to move this check to a validate method
|
602
|
-
unless match[1] ==
|
603
|
-
warn %(
|
613
|
+
unless match[1] == next_index.to_s
|
614
|
+
logger.warn message_with_context %(callout list item index: expected #{next_index}, got #{match[1]}), :source_location => reader.cursor_at_mark
|
604
615
|
end
|
605
616
|
if (list_item = next_list_item reader, block, match)
|
606
|
-
block << list_item
|
617
|
+
block.items << list_item
|
607
618
|
if (coids = document.callouts.callout_ids block.items.size).empty?
|
608
|
-
warn %(
|
619
|
+
logger.warn message_with_context %(no callout found for <#{block.items.size}>), :source_location => reader.cursor_at_mark
|
609
620
|
else
|
610
621
|
list_item.attributes['coids'] = coids
|
611
622
|
end
|
612
623
|
end
|
613
|
-
|
624
|
+
next_index += 1
|
614
625
|
match = nil
|
615
626
|
end
|
616
627
|
|
@@ -619,17 +630,16 @@ class Parser
|
|
619
630
|
|
620
631
|
elsif UnorderedListRx.match? this_line
|
621
632
|
reader.unshift_line this_line
|
622
|
-
|
623
|
-
|
624
|
-
|
625
|
-
|
626
|
-
end
|
633
|
+
if Section === parent && parent.sectname == 'bibliography'
|
634
|
+
style = attributes['style'] = 'bibliography'
|
635
|
+
end unless style
|
636
|
+
block = next_list(reader, :ulist, parent, style)
|
627
637
|
break
|
628
638
|
|
629
639
|
elsif (match = OrderedListRx.match(this_line))
|
630
640
|
reader.unshift_line this_line
|
631
|
-
block =
|
632
|
-
# FIXME move this logic into
|
641
|
+
block = next_list(reader, :olist, parent)
|
642
|
+
# FIXME move this logic into next_list
|
633
643
|
unless style
|
634
644
|
marker = block.items[0].marker
|
635
645
|
if marker.start_with? '.'
|
@@ -651,13 +661,12 @@ class Parser
|
|
651
661
|
elsif (style == 'float' || style == 'discrete') && (Compliance.underline_style_section_titles ?
|
652
662
|
(is_section_title? this_line, reader.peek_line) : !indented && (atx_section_title? this_line))
|
653
663
|
reader.unshift_line this_line
|
654
|
-
float_id, float_reftext, float_title, float_level
|
664
|
+
float_id, float_reftext, float_title, float_level = parse_section_title reader, document, attributes['id']
|
655
665
|
attributes['reftext'] = float_reftext if float_reftext
|
656
666
|
block = Block.new(parent, :floating_title, :content_model => :empty)
|
657
667
|
block.title = float_title
|
658
668
|
attributes.delete 'title'
|
659
|
-
block.id = float_id ||
|
660
|
-
((document.attributes.key? 'sectids') ? (Section.generate_id block.title, document) : nil)
|
669
|
+
block.id = float_id || ((doc_attrs.key? 'sectids') ? (Section.generate_id block.title, document) : nil)
|
661
670
|
block.level = float_level
|
662
671
|
break
|
663
672
|
|
@@ -683,32 +692,26 @@ class Parser
|
|
683
692
|
# advance to block parsing =>
|
684
693
|
break
|
685
694
|
else
|
686
|
-
warn %(
|
695
|
+
logger.warn message_with_context %(invalid style for paragraph: #{style}), :source_location => reader.cursor_at_mark
|
687
696
|
style = nil
|
688
697
|
# continue to process paragraph
|
689
698
|
end
|
690
699
|
end
|
691
700
|
|
692
|
-
break_at_list = (skipped == 0 && in_list)
|
693
701
|
reader.unshift_line this_line
|
694
702
|
|
695
703
|
# a literal paragraph: contiguous lines starting with at least one whitespace character
|
696
704
|
# NOTE style can only be nil or "normal" at this point
|
697
705
|
if indented && !style
|
698
|
-
lines = read_paragraph_lines reader,
|
699
|
-
|
706
|
+
lines = read_paragraph_lines reader, (in_list = ListItem === parent) && skipped == 0, :skip_line_comments => text_only
|
700
707
|
adjust_indentation! lines
|
701
|
-
|
702
708
|
block = Block.new(parent, :literal, :content_model => :verbatim, :source => lines, :attributes => attributes)
|
703
709
|
# a literal gets special meaning inside of a description list
|
704
710
|
# TODO this feels hacky, better way to distinguish from explicit literal block?
|
705
711
|
block.set_option('listparagraph') if in_list
|
706
|
-
|
707
712
|
# a normal paragraph: contiguous non-blank/non-continuation lines (left-indented or normal style)
|
708
713
|
else
|
709
|
-
|
710
|
-
lines = read_paragraph_lines reader, break_at_list, :skip_line_comments => true
|
711
|
-
|
714
|
+
lines = read_paragraph_lines reader, skipped == 0 && ListItem === parent, :skip_line_comments => true
|
712
715
|
# NOTE don't check indented here since it's extremely rare
|
713
716
|
#if text_only || indented
|
714
717
|
if text_only
|
@@ -719,14 +722,12 @@ class Parser
|
|
719
722
|
elsif (ADMONITION_STYLE_HEADS.include? ch0) && (this_line.include? ':') && (AdmonitionParagraphRx =~ this_line)
|
720
723
|
lines[0] = $' # string after match
|
721
724
|
attributes['name'] = admonition_name = (attributes['style'] = $1).downcase
|
722
|
-
attributes['textlabel'] = (attributes.delete 'caption') ||
|
725
|
+
attributes['textlabel'] = (attributes.delete 'caption') || doc_attrs[%(#{admonition_name}-caption)]
|
723
726
|
block = Block.new(parent, :admonition, :content_model => :simple, :source => lines, :attributes => attributes)
|
724
727
|
elsif md_syntax && ch0 == '>' && this_line.start_with?('> ')
|
725
728
|
lines.map! {|line| line == '>' ? line[1..-1] : ((line.start_with? '> ') ? line[2..-1] : line) }
|
726
729
|
if lines[-1].start_with? '-- '
|
727
|
-
|
728
|
-
attributes['attribution'] = attribution if attribution
|
729
|
-
attributes['citetitle'] = citetitle if citetitle
|
730
|
+
credit_line = (credit_line = lines.pop[3..-1])
|
730
731
|
lines.pop while lines[-1].empty?
|
731
732
|
end
|
732
733
|
attributes['style'] = 'quote'
|
@@ -734,15 +735,21 @@ class Parser
|
|
734
735
|
# TODO could assume a discrete heading when inside a block context
|
735
736
|
# FIXME Reader needs to be created w/ line info
|
736
737
|
block = build_block(:quote, :compound, false, parent, Reader.new(lines), attributes)
|
738
|
+
if credit_line
|
739
|
+
attribution, citetitle = (block.apply_subs credit_line).split ', ', 2
|
740
|
+
attributes['attribution'] = attribution if attribution
|
741
|
+
attributes['citetitle'] = citetitle if citetitle
|
742
|
+
end
|
737
743
|
elsif ch0 == '"' && lines.size > 1 && (lines[-1].start_with? '-- ') && (lines[-2].end_with? '"')
|
738
744
|
lines[0] = this_line[1..-1] # strip leading quote
|
739
|
-
|
740
|
-
attributes['attribution'] = attribution if attribution
|
741
|
-
attributes['citetitle'] = citetitle if citetitle
|
745
|
+
credit_line = (credit_line = lines.pop).slice(3, credit_line.length)
|
742
746
|
lines.pop while lines[-1].empty?
|
743
747
|
lines[-1] = lines[-1].chop # strip trailing quote
|
744
748
|
attributes['style'] = 'quote'
|
745
749
|
block = Block.new(parent, :quote, :content_model => :simple, :source => lines, :attributes => attributes)
|
750
|
+
attribution, citetitle = (block.apply_subs credit_line).split ', ', 2
|
751
|
+
attributes['attribution'] = attribution if attribution
|
752
|
+
attributes['citetitle'] = citetitle if citetitle
|
746
753
|
else
|
747
754
|
# if [normal] is used over an indented paragraph, shift content to left margin
|
748
755
|
# QUESTION do we even need to shift since whitespace is normalized by XML in this case?
|
@@ -765,7 +772,7 @@ class Parser
|
|
765
772
|
case block_context
|
766
773
|
when :admonition
|
767
774
|
attributes['name'] = admonition_name = style.downcase
|
768
|
-
attributes['textlabel'] = (attributes.delete 'caption') ||
|
775
|
+
attributes['textlabel'] = (attributes.delete 'caption') || doc_attrs[%(#{admonition_name}-caption)]
|
769
776
|
block = build_block(block_context, :compound, terminator, parent, reader, attributes)
|
770
777
|
|
771
778
|
when :comment
|
@@ -781,14 +788,14 @@ class Parser
|
|
781
788
|
|
782
789
|
when :source
|
783
790
|
AttributeList.rekey attributes, [nil, 'language', 'linenums']
|
784
|
-
if
|
785
|
-
attributes['language'] =
|
791
|
+
if doc_attrs.key? 'source-language'
|
792
|
+
attributes['language'] = doc_attrs['source-language'] || 'text'
|
786
793
|
end unless attributes.key? 'language'
|
787
|
-
if (attributes.key? 'linenums-option') || (
|
794
|
+
if (attributes.key? 'linenums-option') || (doc_attrs.key? 'source-linenums-option')
|
788
795
|
attributes['linenums'] = ''
|
789
796
|
end unless attributes.key? 'linenums'
|
790
|
-
if
|
791
|
-
attributes['indent'] =
|
797
|
+
if doc_attrs.key? 'source-indent'
|
798
|
+
attributes['indent'] = doc_attrs['source-indent']
|
792
799
|
end unless attributes.key? 'indent'
|
793
800
|
block = build_block(:listing, :verbatim, terminator, parent, reader, attributes)
|
794
801
|
|
@@ -808,17 +815,17 @@ class Parser
|
|
808
815
|
language = language.lstrip
|
809
816
|
end
|
810
817
|
if language.nil_or_empty?
|
811
|
-
if
|
812
|
-
attributes['language'] =
|
818
|
+
if doc_attrs.key? 'source-language'
|
819
|
+
attributes['language'] = doc_attrs['source-language'] || 'text'
|
813
820
|
end
|
814
821
|
else
|
815
822
|
attributes['language'] = language
|
816
823
|
end
|
817
|
-
if (attributes.key? 'linenums-option') || (
|
824
|
+
if (attributes.key? 'linenums-option') || (doc_attrs.key? 'source-linenums-option')
|
818
825
|
attributes['linenums'] = ''
|
819
826
|
end unless attributes.key? 'linenums'
|
820
|
-
if
|
821
|
-
attributes['indent'] =
|
827
|
+
if doc_attrs.key? 'source-indent'
|
828
|
+
attributes['indent'] = doc_attrs['source-indent']
|
822
829
|
end unless attributes.key? 'indent'
|
823
830
|
terminator = terminator.slice 0, 3
|
824
831
|
block = build_block(:listing, :verbatim, terminator, parent, reader, attributes)
|
@@ -827,22 +834,15 @@ class Parser
|
|
827
834
|
block = build_block(block_context, :raw, terminator, parent, reader, attributes)
|
828
835
|
|
829
836
|
when :stem, :latexmath, :asciimath
|
830
|
-
if block_context == :stem
|
831
|
-
attributes['style'] = if (explicit_stem_syntax = attributes[2])
|
832
|
-
explicit_stem_syntax.include?('tex') ? 'latexmath' : 'asciimath'
|
833
|
-
elsif (default_stem_syntax = document.attributes['stem']).nil_or_empty?
|
834
|
-
'asciimath'
|
835
|
-
else
|
836
|
-
default_stem_syntax
|
837
|
-
end
|
838
|
-
end
|
837
|
+
attributes['style'] = STEM_TYPE_ALIASES[attributes[2] || doc_attrs['stem']] if block_context == :stem
|
839
838
|
block = build_block(:stem, :raw, terminator, parent, reader, attributes)
|
840
839
|
|
841
840
|
when :open, :sidebar
|
842
841
|
block = build_block(block_context, :compound, terminator, parent, reader, attributes)
|
843
842
|
|
844
843
|
when :table
|
845
|
-
|
844
|
+
block_cursor = reader.cursor
|
845
|
+
block_reader = Reader.new reader.read_lines_until(:terminator => terminator, :skip_line_comments => true, :context => :table, :cursor => :at_mark), block_cursor
|
846
846
|
# NOTE it's very rare that format is set when using a format hint char, so short-circuit
|
847
847
|
unless terminator.start_with? '|', '!'
|
848
848
|
# NOTE infer dsv once all other format hint chars are ruled out
|
@@ -873,32 +873,29 @@ class Parser
|
|
873
873
|
end
|
874
874
|
else
|
875
875
|
# this should only happen if there's a misconfiguration
|
876
|
-
raise %(Unsupported block type #{block_context} at #{reader.
|
876
|
+
raise %(Unsupported block type #{block_context} at #{reader.cursor})
|
877
877
|
end
|
878
878
|
end
|
879
879
|
end
|
880
880
|
|
881
881
|
# FIXME we've got to clean this up, it's horrible!
|
882
|
-
block.source_location =
|
882
|
+
block.source_location = reader.cursor_at_mark if document.sourcemap
|
883
883
|
# FIXME title should be assigned when block is constructed
|
884
884
|
block.title = attributes.delete 'title' if attributes.key? 'title'
|
885
|
-
#unless attributes.key? 'reftext'
|
886
|
-
# attributes['reftext'] = document.attributes['reftext'] if document.attributes.key? 'reftext'
|
887
|
-
#end
|
888
885
|
# TODO eventually remove the style attribute from the attributes hash
|
889
886
|
#block.style = attributes.delete 'style'
|
890
887
|
block.style = attributes['style']
|
891
888
|
if (block_id = (block.id ||= attributes['id']))
|
892
889
|
unless document.register :refs, [block_id, block, attributes['reftext'] || (block.title? ? block.title : nil)]
|
893
|
-
warn %(
|
890
|
+
logger.warn message_with_context %(id assigned to block already in use: #{block_id}), :source_location => reader.cursor_at_mark
|
894
891
|
end
|
895
892
|
end
|
896
893
|
# FIXME remove the need for this update!
|
897
894
|
block.attributes.update(attributes) unless attributes.empty?
|
898
895
|
block.lock_in_subs
|
899
896
|
|
900
|
-
#if
|
901
|
-
#
|
897
|
+
#if doc_attrs.key? :pending_attribute_entries
|
898
|
+
# doc_attrs.delete(:pending_attribute_entries).each do |entry|
|
902
899
|
# entry.save_to block.attributes
|
903
900
|
# end
|
904
901
|
#end
|
@@ -1015,7 +1012,7 @@ class Parser
|
|
1015
1012
|
end
|
1016
1013
|
block_reader = nil
|
1017
1014
|
elsif parse_as_content_model != :compound
|
1018
|
-
lines = reader.read_lines_until :terminator => terminator, :skip_processing => skip_processing
|
1015
|
+
lines = reader.read_lines_until :terminator => terminator, :skip_processing => skip_processing, :context => block_context, :cursor => :at_mark
|
1019
1016
|
block_reader = nil
|
1020
1017
|
# terminator is false when reader has already been prepared
|
1021
1018
|
elsif terminator == false
|
@@ -1023,7 +1020,8 @@ class Parser
|
|
1023
1020
|
block_reader = reader
|
1024
1021
|
else
|
1025
1022
|
lines = nil
|
1026
|
-
|
1023
|
+
block_cursor = reader.cursor
|
1024
|
+
block_reader = Reader.new reader.read_lines_until(:terminator => terminator, :skip_processing => skip_processing, :context => block_context, :cursor => :at_mark), block_cursor
|
1027
1025
|
end
|
1028
1026
|
|
1029
1027
|
if content_model == :verbatim
|
@@ -1081,27 +1079,24 @@ class Parser
|
|
1081
1079
|
#
|
1082
1080
|
# Returns nothing.
|
1083
1081
|
def self.parse_blocks(reader, parent)
|
1084
|
-
while ((block = next_block reader, parent) && parent << block) || reader.has_more_lines?
|
1082
|
+
while ((block = next_block reader, parent) && parent.blocks << block) || reader.has_more_lines?
|
1085
1083
|
end
|
1086
1084
|
end
|
1087
1085
|
|
1088
|
-
# Internal: Parse and construct an
|
1086
|
+
# Internal: Parse and construct an ordered or unordered list at the current position of the Reader
|
1089
1087
|
#
|
1090
|
-
# reader - The Reader from which to retrieve the
|
1088
|
+
# reader - The Reader from which to retrieve the list
|
1091
1089
|
# list_type - A Symbol representing the list type (:olist for ordered, :ulist for unordered)
|
1092
|
-
# parent - The parent Block to which this
|
1090
|
+
# parent - The parent Block to which this list belongs
|
1091
|
+
# style - The block style assigned to this list (optional, default: nil)
|
1093
1092
|
#
|
1094
|
-
# Returns the Block encapsulating the parsed
|
1095
|
-
def self.
|
1093
|
+
# Returns the Block encapsulating the parsed unordered or ordered list
|
1094
|
+
def self.next_list(reader, list_type, parent, style = nil)
|
1096
1095
|
list_block = List.new(parent, list_type)
|
1097
|
-
|
1098
|
-
list_block.level = parent.level + 1
|
1099
|
-
else
|
1100
|
-
list_block.level = 1
|
1101
|
-
end
|
1096
|
+
list_block.level = parent.context == list_type ? (parent.level + 1) : 1
|
1102
1097
|
|
1103
|
-
while reader.has_more_lines? &&
|
1104
|
-
marker = resolve_list_marker(list_type,
|
1098
|
+
while reader.has_more_lines? && ListRxMap[list_type] =~ reader.peek_line
|
1099
|
+
match, marker = $~, resolve_list_marker(list_type, $1)
|
1105
1100
|
|
1106
1101
|
# if we are moving to the next item, and the marker is different
|
1107
1102
|
# determine if we are moving up or down in nesting
|
@@ -1122,7 +1117,7 @@ class Parser
|
|
1122
1117
|
end
|
1123
1118
|
|
1124
1119
|
if !list_block.items? || this_item_level == list_block.level
|
1125
|
-
list_item = next_list_item(reader, list_block, match)
|
1120
|
+
list_item = next_list_item(reader, list_block, match, nil, style)
|
1126
1121
|
elsif this_item_level < list_block.level
|
1127
1122
|
# leave this block
|
1128
1123
|
break
|
@@ -1132,7 +1127,7 @@ class Parser
|
|
1132
1127
|
list_block.items[-1] << next_block(reader, list_block)
|
1133
1128
|
end
|
1134
1129
|
|
1135
|
-
list_block << list_item if list_item
|
1130
|
+
list_block.items << list_item if list_item
|
1136
1131
|
list_item = nil
|
1137
1132
|
|
1138
1133
|
reader.skip_blank_lines || break
|
@@ -1159,6 +1154,25 @@ class Parser
|
|
1159
1154
|
found
|
1160
1155
|
end
|
1161
1156
|
|
1157
|
+
# Internal: Catalog a matched inline anchor.
|
1158
|
+
#
|
1159
|
+
# id - The String id of the anchor
|
1160
|
+
# reftext - The optional String reference text of the anchor
|
1161
|
+
# node - The AbstractNode parent node of the anchor node
|
1162
|
+
# location - The source location (file and line) where the anchor was found
|
1163
|
+
# doc - The document to which the node belongs; computed from node if not specified
|
1164
|
+
#
|
1165
|
+
# Returns nothing
|
1166
|
+
def self.catalog_inline_anchor id, reftext, node, location, doc = nil
|
1167
|
+
doc ||= node.document
|
1168
|
+
reftext = doc.sub_attributes reftext if reftext && (reftext.include? ATTR_REF_HEAD)
|
1169
|
+
unless doc.register :refs, [id, (Inline.new node, :anchor, reftext, :type => :ref, :id => id), reftext]
|
1170
|
+
location = location.cursor if Reader === location
|
1171
|
+
logger.warn message_with_context %(id assigned to anchor already in use: #{id}), :source_location => location
|
1172
|
+
end
|
1173
|
+
nil
|
1174
|
+
end
|
1175
|
+
|
1162
1176
|
# Internal: Catalog any inline anchors found in the text (but don't convert)
|
1163
1177
|
#
|
1164
1178
|
# text - The String text in which to look for inline anchors
|
@@ -1180,7 +1194,7 @@ class Parser
|
|
1180
1194
|
end
|
1181
1195
|
end
|
1182
1196
|
unless document.register :refs, [id, (Inline.new block, :anchor, reftext, :type => :ref, :id => id), reftext]
|
1183
|
-
warn %(
|
1197
|
+
logger.warn message_with_context %(id assigned to anchor already in use: #{id}), :source_location => document.reader.cursor_at_prev_line
|
1184
1198
|
end
|
1185
1199
|
end if (text.include? '[[') || (text.include? 'or:')
|
1186
1200
|
nil
|
@@ -1188,17 +1202,16 @@ class Parser
|
|
1188
1202
|
|
1189
1203
|
# Internal: Catalog the bibliography inline anchor found in the start of the list item (but don't convert)
|
1190
1204
|
#
|
1191
|
-
#
|
1192
|
-
#
|
1193
|
-
#
|
1205
|
+
# id - The String id of the anchor
|
1206
|
+
# reftext - The optional String reference text of the anchor
|
1207
|
+
# node - The AbstractNode parent node of the anchor node
|
1208
|
+
# reader - The source Reader for the current Document, positioned at the current list item
|
1194
1209
|
#
|
1195
1210
|
# Returns nothing
|
1196
|
-
def self.catalog_inline_biblio_anchor
|
1197
|
-
|
1198
|
-
|
1199
|
-
|
1200
|
-
warn %(asciidoctor: WARNING: #{document.reader.path}: id assigned to bibliography anchor already in use: #{id})
|
1201
|
-
end
|
1211
|
+
def self.catalog_inline_biblio_anchor id, reftext, node, reader
|
1212
|
+
# QUESTION should we sub attributes in reftext (like with regular anchors)?
|
1213
|
+
unless node.document.register :refs, [id, (Inline.new node, :anchor, (styled_reftext = %([#{reftext || id}])), :type => :bibref, :id => id), styled_reftext]
|
1214
|
+
logger.warn message_with_context %(id assigned to bibliography anchor already in use: #{id}), :source_location => reader.cursor
|
1202
1215
|
end
|
1203
1216
|
nil
|
1204
1217
|
end
|
@@ -1220,12 +1233,10 @@ class Parser
|
|
1220
1233
|
# NOTE skip the match on the first time through as we've already done it (emulates begin...while)
|
1221
1234
|
while match || (reader.has_more_lines? && (match = sibling_pattern.match(reader.peek_line)))
|
1222
1235
|
term, item = next_list_item(reader, list_block, match, sibling_pattern)
|
1223
|
-
if previous_pair && !previous_pair[
|
1224
|
-
previous_pair.pop
|
1236
|
+
if previous_pair && !previous_pair[1]
|
1225
1237
|
previous_pair[0] << term
|
1226
|
-
previous_pair
|
1238
|
+
previous_pair[1] = item
|
1227
1239
|
else
|
1228
|
-
# FIXME this misses the automatic parent assignment
|
1229
1240
|
list_block.items << (previous_pair = [[term], item])
|
1230
1241
|
end
|
1231
1242
|
match = nil
|
@@ -1234,9 +1245,9 @@ class Parser
|
|
1234
1245
|
list_block
|
1235
1246
|
end
|
1236
1247
|
|
1237
|
-
# Internal: Parse and construct the next ListItem for the current
|
1238
|
-
# (unordered or
|
1239
|
-
#
|
1248
|
+
# Internal: Parse and construct the next ListItem for the current list Block
|
1249
|
+
# (unordered, ordered, or callout list) or the term ListItem and description
|
1250
|
+
# ListItem pair for the description list Block.
|
1240
1251
|
#
|
1241
1252
|
# First collect and process all the lines that constitute the next list
|
1242
1253
|
# item for the parent list (according to its type). Next, parse those lines
|
@@ -1247,48 +1258,65 @@ class Parser
|
|
1247
1258
|
# reader - The Reader from which to retrieve the next list item
|
1248
1259
|
# list_block - The parent list Block of this ListItem. Also provides access to the list type.
|
1249
1260
|
# match - The match Array which contains the marker and text (first-line) of the ListItem
|
1250
|
-
# sibling_trait - The list marker or the Regexp to match a sibling item
|
1261
|
+
# sibling_trait - The list marker or the Regexp to match a sibling item (optional, default: nil)
|
1262
|
+
# style - The block style assigned to this list (optional, default: nil)
|
1251
1263
|
#
|
1252
1264
|
# Returns the next ListItem or ListItem pair (depending on the list type)
|
1253
1265
|
# for the parent list Block.
|
1254
|
-
def self.next_list_item(reader, list_block, match, sibling_trait = nil)
|
1266
|
+
def self.next_list_item(reader, list_block, match, sibling_trait = nil, style = nil)
|
1255
1267
|
if (list_type = list_block.context) == :dlist
|
1256
|
-
|
1257
|
-
|
1258
|
-
|
1259
|
-
|
1260
|
-
|
1261
|
-
|
1262
|
-
|
1263
|
-
if
|
1264
|
-
|
1265
|
-
|
1266
|
-
|
1267
|
-
|
1268
|
-
|
1269
|
-
checkbox = true
|
1270
|
-
checked = true
|
1271
|
-
text = text[3..-1].lstrip
|
1268
|
+
dlist = true
|
1269
|
+
list_term = ListItem.new(list_block, (term_text = match[1]))
|
1270
|
+
if term_text.start_with?('[[') && LeadingInlineAnchorRx =~ term_text
|
1271
|
+
catalog_inline_anchor $1, ($2 || $'.lstrip), list_term, reader
|
1272
|
+
end
|
1273
|
+
has_text = true if (item_text = match[3])
|
1274
|
+
list_item = ListItem.new(list_block, item_text)
|
1275
|
+
if list_block.document.sourcemap
|
1276
|
+
list_term.source_location = reader.cursor
|
1277
|
+
if has_text
|
1278
|
+
list_item.source_location = list_term.source_location
|
1279
|
+
else
|
1280
|
+
sourcemap_assignment_deferred = true
|
1272
1281
|
end
|
1273
1282
|
end
|
1274
|
-
|
1275
|
-
|
1276
|
-
if checkbox
|
1277
|
-
# FIXME checklist never makes it into the options attribute
|
1278
|
-
list_block.attributes['checklist-option'] = ''
|
1279
|
-
list_item.attributes['checkbox'] = ''
|
1280
|
-
list_item.attributes['checked'] = '' if checked
|
1281
|
-
end
|
1282
|
-
|
1283
|
-
sibling_trait ||= resolve_list_marker(list_type, match[1], list_block.items.size, true, reader)
|
1284
|
-
list_item.marker = sibling_trait
|
1283
|
+
else
|
1285
1284
|
has_text = true
|
1285
|
+
list_item = ListItem.new(list_block, (item_text = match[2]))
|
1286
|
+
list_item.source_location = reader.cursor if list_block.document.sourcemap
|
1287
|
+
if list_type == :ulist
|
1288
|
+
list_item.marker = (sibling_trait ||= match[1])
|
1289
|
+
if item_text.start_with?('[')
|
1290
|
+
if style && style == 'bibliography'
|
1291
|
+
if InlineBiblioAnchorRx =~ item_text
|
1292
|
+
catalog_inline_biblio_anchor $1, $2, list_item, reader
|
1293
|
+
end
|
1294
|
+
elsif item_text.start_with?('[[')
|
1295
|
+
if LeadingInlineAnchorRx =~ item_text
|
1296
|
+
catalog_inline_anchor $1, $2, list_item, reader
|
1297
|
+
end
|
1298
|
+
elsif item_text.start_with?('[ ] ', '[x] ', '[*] ')
|
1299
|
+
# FIXME next_block wipes out update to options attribute
|
1300
|
+
#list_block.set_option 'checklist' unless list_block.attributes['checklist-option']
|
1301
|
+
list_block.attributes['checklist-option'] = ''
|
1302
|
+
list_item.attributes['checkbox'] = ''
|
1303
|
+
list_item.attributes['checked'] = '' unless item_text.start_with? '[ '
|
1304
|
+
list_item.text = item_text.slice(4, item_text.length)
|
1305
|
+
end
|
1306
|
+
end
|
1307
|
+
elsif list_type == :olist
|
1308
|
+
list_item.marker = (sibling_trait ||= resolve_ordered_list_marker(match[1], list_block.items.size, true, reader))
|
1309
|
+
else # :colist
|
1310
|
+
list_item.marker = (sibling_trait ||= '<1>')
|
1311
|
+
end
|
1286
1312
|
end
|
1287
1313
|
|
1288
|
-
# first skip the line with the marker / term
|
1314
|
+
# first skip the line with the marker / term (it gets put back onto the reader by next_block)
|
1289
1315
|
reader.shift
|
1290
|
-
|
1316
|
+
block_cursor = reader.cursor
|
1317
|
+
list_item_reader = Reader.new read_lines_for_list_item(reader, list_type, sibling_trait, has_text), block_cursor
|
1291
1318
|
if list_item_reader.has_more_lines?
|
1319
|
+
list_item.source_location = block_cursor if sourcemap_assignment_deferred
|
1292
1320
|
# NOTE peek on the other side of any comment lines
|
1293
1321
|
comment_lines = list_item_reader.skip_line_comments
|
1294
1322
|
if (subsequent_line = list_item_reader.peek_line)
|
@@ -1297,8 +1325,8 @@ class Parser
|
|
1297
1325
|
content_adjacent = false
|
1298
1326
|
else
|
1299
1327
|
content_adjacent = true
|
1300
|
-
# treat lines as paragraph text if continuation does not connect first block (i.e., has_text =
|
1301
|
-
has_text =
|
1328
|
+
# treat lines as paragraph text if continuation does not connect first block (i.e., has_text = nil)
|
1329
|
+
has_text = nil unless dlist
|
1302
1330
|
end
|
1303
1331
|
else
|
1304
1332
|
# NOTE we have no use for any trailing comment lines we might have found
|
@@ -1306,24 +1334,22 @@ class Parser
|
|
1306
1334
|
content_adjacent = false
|
1307
1335
|
end
|
1308
1336
|
|
1309
|
-
# only
|
1310
|
-
|
1337
|
+
# reader is confined to boundaries of list, which means only blocks will be found (no sections)
|
1338
|
+
if (block = next_block(list_item_reader, list_item, (attrs = {}), :text => !has_text))
|
1339
|
+
list_item.blocks << block
|
1340
|
+
end
|
1311
1341
|
|
1312
|
-
|
1313
|
-
|
1314
|
-
|
1315
|
-
|
1342
|
+
while list_item_reader.has_more_lines?
|
1343
|
+
if (block = next_block(list_item_reader, list_item, attrs))
|
1344
|
+
list_item.blocks << block
|
1345
|
+
end
|
1316
1346
|
end
|
1317
1347
|
|
1318
1348
|
list_item.fold_first(continuation_connects_first_block, content_adjacent)
|
1319
1349
|
end
|
1320
1350
|
|
1321
|
-
if
|
1322
|
-
|
1323
|
-
[list_term, list_item]
|
1324
|
-
else
|
1325
|
-
[list_term, nil]
|
1326
|
-
end
|
1351
|
+
if dlist
|
1352
|
+
list_item.text? || list_item.blocks? ? [list_term, list_item] : [list_term]
|
1327
1353
|
else
|
1328
1354
|
list_item
|
1329
1355
|
end
|
@@ -1396,7 +1422,7 @@ class Parser
|
|
1396
1422
|
buffer << this_line
|
1397
1423
|
# grab all the lines in the block, leaving the delimiters in place
|
1398
1424
|
# we're being more strict here about the terminator, but I think that's a good thing
|
1399
|
-
buffer.concat reader.read_lines_until(:terminator => match.terminator, :read_last_line => true)
|
1425
|
+
buffer.concat reader.read_lines_until(:terminator => match.terminator, :read_last_line => true, :context => nil)
|
1400
1426
|
continuation = :inactive
|
1401
1427
|
else
|
1402
1428
|
break
|
@@ -1528,58 +1554,57 @@ class Parser
|
|
1528
1554
|
# reader - the source reader
|
1529
1555
|
# parent - the parent Section or Document of this Section
|
1530
1556
|
# attributes - a Hash of attributes to assign to this section (default: {})
|
1557
|
+
#
|
1558
|
+
# Returns the section [Block]
|
1531
1559
|
def self.initialize_section reader, parent, attributes = {}
|
1532
1560
|
document = parent.document
|
1561
|
+
book = (doctype = document.doctype) == 'book'
|
1533
1562
|
source_location = reader.cursor if document.sourcemap
|
1534
|
-
|
1563
|
+
sect_style = attributes[1]
|
1564
|
+
sect_id, sect_reftext, sect_title, sect_level, sect_atx = parse_section_title reader, document, attributes['id']
|
1565
|
+
|
1535
1566
|
if sect_reftext
|
1536
1567
|
attributes['reftext'] = sect_reftext
|
1537
|
-
|
1568
|
+
else
|
1538
1569
|
sect_reftext = attributes['reftext']
|
1539
|
-
#elsif document.attributes.key? 'reftext'
|
1540
|
-
# sect_reftext = attributes['reftext'] = document.attributes['reftext']
|
1541
1570
|
end
|
1542
1571
|
|
1543
|
-
|
1544
|
-
|
1545
|
-
if style
|
1546
|
-
if style == 'abstract' && document.doctype == 'book'
|
1572
|
+
if sect_style
|
1573
|
+
if book && sect_style == 'abstract'
|
1547
1574
|
sect_name, sect_level = 'chapter', 1
|
1548
1575
|
else
|
1549
|
-
sect_name, sect_special =
|
1576
|
+
sect_name, sect_special = sect_style, true
|
1550
1577
|
sect_level = 1 if sect_level == 0
|
1551
|
-
|
1578
|
+
sect_numbered = sect_style == 'appendix'
|
1552
1579
|
end
|
1580
|
+
elsif book
|
1581
|
+
sect_name = sect_level == 0 ? 'part' : (sect_level > 1 ? 'section' : 'chapter')
|
1582
|
+
elsif doctype == 'manpage' && (sect_title.casecmp 'synopsis') == 0
|
1583
|
+
sect_name, sect_special = 'synopsis', true
|
1553
1584
|
else
|
1554
|
-
|
1555
|
-
when 'book'
|
1556
|
-
sect_name = sect_level == 0 ? 'part' : (sect_level == 1 ? 'chapter' : 'section')
|
1557
|
-
when 'manpage'
|
1558
|
-
if (sect_title.casecmp 'synopsis') == 0
|
1559
|
-
sect_name, sect_special = 'synopsis', true
|
1560
|
-
else
|
1561
|
-
sect_name = 'section'
|
1562
|
-
end
|
1563
|
-
else
|
1564
|
-
sect_name = 'section'
|
1565
|
-
end
|
1585
|
+
sect_name = 'section'
|
1566
1586
|
end
|
1567
1587
|
|
1568
|
-
section = Section.new parent, sect_level
|
1588
|
+
section = Section.new parent, sect_level
|
1569
1589
|
section.id, section.title, section.sectname, section.source_location = sect_id, sect_title, sect_name, source_location
|
1570
|
-
# TODO honor special section numbering option (#661)
|
1571
1590
|
if sect_special
|
1572
1591
|
section.special = true
|
1573
|
-
|
1574
|
-
|
1575
|
-
|
1592
|
+
if sect_numbered
|
1593
|
+
section.numbered = true
|
1594
|
+
elsif document.attributes['sectnums'] == 'all'
|
1595
|
+
section.numbered = book && sect_level == 1 ? :chapter : true
|
1596
|
+
end
|
1597
|
+
elsif document.attributes['sectnums'] && sect_level > 0
|
1598
|
+
# NOTE a special section here is guaranteed to be nested in another section
|
1599
|
+
section.numbered = section.special ? parent.numbered && true : true
|
1600
|
+
elsif book && sect_level == 0 && document.attributes['partnums']
|
1601
|
+
section.numbered = true
|
1576
1602
|
end
|
1577
1603
|
|
1578
1604
|
# generate an ID if one was not embedded or specified as anchor above section title
|
1579
|
-
if (id = section.id ||= (attributes
|
1580
|
-
((document.attributes.key? 'sectids') ? (Section.generate_id section.title, document) : nil)))
|
1605
|
+
if (id = section.id ||= ((document.attributes.key? 'sectids') ? (Section.generate_id section.title, document) : nil))
|
1581
1606
|
unless document.register :refs, [id, section, sect_reftext || section.title]
|
1582
|
-
warn %(
|
1607
|
+
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))
|
1583
1608
|
end
|
1584
1609
|
end
|
1585
1610
|
|
@@ -1596,12 +1621,13 @@ class Parser
|
|
1596
1621
|
#
|
1597
1622
|
# Returns the Integer section level if the Reader is positioned at a section title or nil otherwise
|
1598
1623
|
def self.is_next_line_section?(reader, attributes)
|
1599
|
-
if (style = attributes[1]) && (style
|
1624
|
+
if (style = attributes[1]) && (style == 'discrete' || style == 'float')
|
1600
1625
|
return
|
1601
|
-
elsif
|
1602
|
-
|
1603
|
-
|
1604
|
-
|
1626
|
+
elsif Compliance.underline_style_section_titles
|
1627
|
+
next_lines = reader.peek_lines 2, style && style == 'comment'
|
1628
|
+
is_section_title?(next_lines[0] || '', next_lines[1])
|
1629
|
+
else
|
1630
|
+
atx_section_title?(reader.peek_line || '')
|
1605
1631
|
end
|
1606
1632
|
end
|
1607
1633
|
|
@@ -1701,8 +1727,8 @@ class Parser
|
|
1701
1727
|
# Returns an 5-element [Array] containing the id (String), reftext (String),
|
1702
1728
|
# title (String), level (Integer), and flag (Boolean) indicating whether an
|
1703
1729
|
# atx section title was matched, or nothing.
|
1704
|
-
def self.parse_section_title(reader, document)
|
1705
|
-
|
1730
|
+
def self.parse_section_title(reader, document, sect_id = nil)
|
1731
|
+
sect_reftext = nil
|
1706
1732
|
line1 = reader.read_line
|
1707
1733
|
|
1708
1734
|
if Compliance.markdown_syntax ? ((line1.start_with? '=', '#') && ExtAtxSectionTitleRx =~ line1) :
|
@@ -1711,7 +1737,7 @@ class Parser
|
|
1711
1737
|
sect_level, sect_title, atx = $1.length - 1, $2, true
|
1712
1738
|
if sect_title.end_with?(']]') && InlineSectionAnchorRx =~ sect_title && !$1 # escaped
|
1713
1739
|
sect_title, sect_id, sect_reftext = (sect_title.slice 0, sect_title.length - $&.length), $2, $3
|
1714
|
-
end
|
1740
|
+
end unless sect_id
|
1715
1741
|
elsif Compliance.underline_style_section_titles && (line2 = reader.peek_line(true)) &&
|
1716
1742
|
(sect_level = SETEXT_SECTION_LEVELS[line2_ch1 = line2.chr]) &&
|
1717
1743
|
line2_ch1 * (line2_len = line2.length) == line2 && (sect_title = SetextSectionTitleRx =~ line1 && $1) &&
|
@@ -1719,10 +1745,10 @@ class Parser
|
|
1719
1745
|
atx = false
|
1720
1746
|
if sect_title.end_with?(']]') && InlineSectionAnchorRx =~ sect_title && !$1 # escaped
|
1721
1747
|
sect_title, sect_id, sect_reftext = (sect_title.slice 0, sect_title.length - $&.length), $2, $3
|
1722
|
-
end
|
1748
|
+
end unless sect_id
|
1723
1749
|
reader.shift
|
1724
1750
|
else
|
1725
|
-
raise %(Unrecognized section at #{reader.
|
1751
|
+
raise %(Unrecognized section at #{reader.cursor_at_prev_line})
|
1726
1752
|
end
|
1727
1753
|
sect_level += document.attr('leveloffset').to_i if document.attr?('leveloffset')
|
1728
1754
|
[sect_id, sect_reftext, sect_title, sect_level, atx]
|
@@ -1758,7 +1784,7 @@ class Parser
|
|
1758
1784
|
# # => {'author' => 'Author Name', 'firstname' => 'Author', 'lastname' => 'Name', 'email' => 'author@example.org',
|
1759
1785
|
# # 'revnumber' => '1.0', 'revdate' => '2012-12-21', 'revremark' => 'Coincide w/ end of world.'}
|
1760
1786
|
def self.parse_header_metadata(reader, document = nil)
|
1761
|
-
# NOTE this will discard
|
1787
|
+
# NOTE this will discard any comment lines, but not skip blank lines
|
1762
1788
|
process_attribute_entries reader, document
|
1763
1789
|
|
1764
1790
|
metadata, implicit_author, implicit_authors = {}, nil, nil
|
@@ -1821,6 +1847,8 @@ class Parser
|
|
1821
1847
|
process_attribute_entries reader, document
|
1822
1848
|
|
1823
1849
|
reader.skip_blank_lines
|
1850
|
+
else
|
1851
|
+
author_metadata = {}
|
1824
1852
|
end
|
1825
1853
|
|
1826
1854
|
# process author attribute entries that override (or stand in for) the implicit author line
|
@@ -1863,7 +1891,9 @@ class Parser
|
|
1863
1891
|
end
|
1864
1892
|
end
|
1865
1893
|
|
1866
|
-
|
1894
|
+
if author_metadata.empty?
|
1895
|
+
metadata['authorcount'] ||= (document.attributes['authorcount'] = 0)
|
1896
|
+
else
|
1867
1897
|
document.attributes.update author_metadata
|
1868
1898
|
|
1869
1899
|
# special case
|
@@ -1887,22 +1917,23 @@ class Parser
|
|
1887
1917
|
# returns a Hash of author metadata
|
1888
1918
|
def self.process_authors author_line, names_only = false, multiple = true
|
1889
1919
|
author_metadata = {}
|
1920
|
+
author_idx = 0
|
1890
1921
|
keys = ['author', 'authorinitials', 'firstname', 'middlename', 'lastname', 'email']
|
1891
1922
|
author_entries = multiple ? (author_line.split ';').map {|it| it.strip } : Array(author_line)
|
1892
|
-
author_entries.
|
1923
|
+
author_entries.each do |author_entry|
|
1893
1924
|
next if author_entry.empty?
|
1925
|
+
author_idx += 1
|
1894
1926
|
key_map = {}
|
1895
|
-
if
|
1927
|
+
if author_idx == 1
|
1896
1928
|
keys.each do |key|
|
1897
1929
|
key_map[key.to_sym] = key
|
1898
1930
|
end
|
1899
1931
|
else
|
1900
1932
|
keys.each do |key|
|
1901
|
-
key_map[key.to_sym] = %(#{key}_#{
|
1933
|
+
key_map[key.to_sym] = %(#{key}_#{author_idx})
|
1902
1934
|
end
|
1903
1935
|
end
|
1904
1936
|
|
1905
|
-
segments = nil
|
1906
1937
|
if names_only # when parsing an attribute value
|
1907
1938
|
# QUESTION should we rstrip author_entry?
|
1908
1939
|
if author_entry.include? '<'
|
@@ -1939,20 +1970,20 @@ class Parser
|
|
1939
1970
|
author_metadata[key_map[:authorinitials]] = fname.chr
|
1940
1971
|
end
|
1941
1972
|
|
1942
|
-
|
1943
|
-
# only assign the _1 attributes if there are multiple authors
|
1944
|
-
if idx == 1
|
1945
|
-
keys.each do |key|
|
1946
|
-
author_metadata[%(#{key}_1)] = author_metadata[key] if author_metadata.key? key
|
1947
|
-
end
|
1948
|
-
end
|
1949
|
-
if idx == 0
|
1973
|
+
if author_idx == 1
|
1950
1974
|
author_metadata['authors'] = author_metadata[key_map[:author]]
|
1951
1975
|
else
|
1976
|
+
# only assign the _1 attributes once we see the second author
|
1977
|
+
if author_idx == 2
|
1978
|
+
keys.each do |key|
|
1979
|
+
author_metadata[%(#{key}_1)] = author_metadata[key] if author_metadata.key? key
|
1980
|
+
end
|
1981
|
+
end
|
1952
1982
|
author_metadata['authors'] = %(#{author_metadata['authors']}, #{author_metadata[key_map[:author]]})
|
1953
1983
|
end
|
1954
1984
|
end
|
1955
1985
|
|
1986
|
+
author_metadata['authorcount'] = author_idx
|
1956
1987
|
author_metadata
|
1957
1988
|
end
|
1958
1989
|
|
@@ -2012,7 +2043,11 @@ class Parser
|
|
2012
2043
|
return true
|
2013
2044
|
end
|
2014
2045
|
elsif (next_line.end_with? ']') && BlockAttributeListRx =~ next_line
|
2015
|
-
|
2046
|
+
current_style = attributes[1]
|
2047
|
+
# extract id, role, and options from first positional attribute and remove, if present
|
2048
|
+
if (document.parse_attributes $1, [], :sub_input => true, :into => attributes)[1]
|
2049
|
+
attributes[1] = (parse_style_attribute attributes, reader) || current_style
|
2050
|
+
end
|
2016
2051
|
return true
|
2017
2052
|
end
|
2018
2053
|
elsif normal && (next_line.start_with? '.')
|
@@ -2027,7 +2062,7 @@ class Parser
|
|
2027
2062
|
return true
|
2028
2063
|
elsif normal && '/' * (ll = next_line.length) == next_line
|
2029
2064
|
unless ll == 3
|
2030
|
-
reader.read_lines_until :skip_first_line => true, :preserve_last_line => true, :
|
2065
|
+
reader.read_lines_until :terminator => next_line, :skip_first_line => true, :preserve_last_line => true, :skip_processing => true, :context => :comment
|
2031
2066
|
return true
|
2032
2067
|
end
|
2033
2068
|
else
|
@@ -2041,6 +2076,9 @@ class Parser
|
|
2041
2076
|
end
|
2042
2077
|
end
|
2043
2078
|
|
2079
|
+
# Process consecutive attribute entry lines, ignoring adjacent line comments and comment blocks.
|
2080
|
+
#
|
2081
|
+
# Returns nothing
|
2044
2082
|
def self.process_attribute_entries reader, document, attributes = nil
|
2045
2083
|
reader.skip_comment_lines
|
2046
2084
|
while process_attribute_entry reader, document, attributes
|
@@ -2135,12 +2173,12 @@ class Parser
|
|
2135
2173
|
#
|
2136
2174
|
# Returns the String 0-index marker for this list item
|
2137
2175
|
def self.resolve_list_marker(list_type, marker, ordinal = 0, validate = false, reader = nil)
|
2138
|
-
if list_type == :
|
2139
|
-
(marker.start_with? '.') ? marker : (resolve_ordered_list_marker marker, ordinal, validate, reader)
|
2140
|
-
elsif list_type == :colist
|
2141
|
-
'<1>'
|
2142
|
-
else
|
2176
|
+
if list_type == :ulist
|
2143
2177
|
marker
|
2178
|
+
elsif list_type == :olist
|
2179
|
+
resolve_ordered_list_marker(marker, ordinal, validate, reader)
|
2180
|
+
else # :colist
|
2181
|
+
'<1>'
|
2144
2182
|
end
|
2145
2183
|
end
|
2146
2184
|
|
@@ -2166,6 +2204,7 @@ class Parser
|
|
2166
2204
|
#
|
2167
2205
|
# Returns the String of the first marker in this number series
|
2168
2206
|
def self.resolve_ordered_list_marker(marker, ordinal = 0, validate = false, reader = nil)
|
2207
|
+
return marker if marker.start_with? '.'
|
2169
2208
|
expected = actual = nil
|
2170
2209
|
case ORDERED_LIST_STYLES.find {|s| OrderedListMarkerRxMap[s].match? marker }
|
2171
2210
|
when :arabic
|
@@ -2188,22 +2227,20 @@ class Parser
|
|
2188
2227
|
marker = 'A.'
|
2189
2228
|
when :lowerroman
|
2190
2229
|
if validate
|
2191
|
-
|
2192
|
-
|
2193
|
-
actual = roman_numeral_to_int(marker.chop) # remove trailing ) and coerce to int
|
2230
|
+
expected = Helpers.int_to_roman(ordinal + 1).downcase
|
2231
|
+
actual = marker.chop # remove trailing )
|
2194
2232
|
end
|
2195
2233
|
marker = 'i)'
|
2196
2234
|
when :upperroman
|
2197
2235
|
if validate
|
2198
|
-
|
2199
|
-
|
2200
|
-
actual = roman_numeral_to_int(marker.chop) # remove trailing ) and coerce to int
|
2236
|
+
expected = Helpers.int_to_roman(ordinal + 1)
|
2237
|
+
actual = marker.chop # remove trailing )
|
2201
2238
|
end
|
2202
2239
|
marker = 'I)'
|
2203
2240
|
end
|
2204
2241
|
|
2205
2242
|
if validate && expected != actual
|
2206
|
-
warn %(
|
2243
|
+
logger.warn message_with_context %(list item index: expected #{expected}, got #{actual}), :source_location => reader.cursor
|
2207
2244
|
end
|
2208
2245
|
|
2209
2246
|
marker
|
@@ -2221,18 +2258,13 @@ class Parser
|
|
2221
2258
|
def self.is_sibling_list_item?(line, list_type, sibling_trait)
|
2222
2259
|
if ::Regexp === sibling_trait
|
2223
2260
|
matcher = sibling_trait
|
2224
|
-
expected_marker = false
|
2225
2261
|
else
|
2226
2262
|
matcher = ListRxMap[list_type]
|
2227
2263
|
expected_marker = sibling_trait
|
2228
2264
|
end
|
2229
2265
|
|
2230
|
-
if
|
2231
|
-
|
2232
|
-
expected_marker == resolve_list_marker(list_type, m[1])
|
2233
|
-
else
|
2234
|
-
true
|
2235
|
-
end
|
2266
|
+
if matcher =~ line
|
2267
|
+
expected_marker ? expected_marker == resolve_list_marker(list_type, $1) : true
|
2236
2268
|
else
|
2237
2269
|
false
|
2238
2270
|
end
|
@@ -2263,7 +2295,7 @@ class Parser
|
|
2263
2295
|
implicit_header = true unless skipped > 0 || (attributes.key? 'header-option') || (attributes.key? 'noheader-option')
|
2264
2296
|
|
2265
2297
|
while (line = table_reader.read_line)
|
2266
|
-
if (loop_idx += 1) > 0 && line.empty?
|
2298
|
+
if (beyond_first = (loop_idx += 1) > 0) && line.empty?
|
2267
2299
|
line = nil
|
2268
2300
|
implicit_header_boundary += 1 if implicit_header_boundary
|
2269
2301
|
elsif format == 'psv'
|
@@ -2285,58 +2317,63 @@ class Parser
|
|
2285
2317
|
end
|
2286
2318
|
end
|
2287
2319
|
|
2288
|
-
|
2289
|
-
|
2290
|
-
|
2291
|
-
|
2292
|
-
|
2293
|
-
|
2320
|
+
unless beyond_first
|
2321
|
+
table_reader.mark
|
2322
|
+
# NOTE implicit header is offset by at least one blank line; implicit_header_boundary tracks size of gap
|
2323
|
+
if implicit_header
|
2324
|
+
if table_reader.has_more_lines? && table_reader.peek_line.empty?
|
2325
|
+
implicit_header_boundary = 1
|
2326
|
+
else
|
2327
|
+
implicit_header = false
|
2328
|
+
end
|
2294
2329
|
end
|
2295
2330
|
end
|
2296
2331
|
|
2297
2332
|
# this loop is used for flow control; internal logic controls how many times it executes
|
2298
2333
|
while true
|
2299
2334
|
if line && (m = parser_ctx.match_delimiter line)
|
2335
|
+
pre_match, post_match = m.pre_match, m.post_match
|
2300
2336
|
case format
|
2301
2337
|
when 'csv'
|
2302
|
-
if parser_ctx.buffer_has_unclosed_quotes?
|
2303
|
-
|
2338
|
+
if parser_ctx.buffer_has_unclosed_quotes? pre_match
|
2339
|
+
parser_ctx.skip_past_delimiter pre_match
|
2340
|
+
break if (line = post_match).empty?
|
2304
2341
|
redo
|
2305
2342
|
end
|
2306
|
-
parser_ctx.buffer = %(#{parser_ctx.buffer}#{
|
2343
|
+
parser_ctx.buffer = %(#{parser_ctx.buffer}#{pre_match})
|
2307
2344
|
when 'dsv'
|
2308
|
-
if
|
2309
|
-
|
2345
|
+
if pre_match.end_with? '\\'
|
2346
|
+
parser_ctx.skip_past_escaped_delimiter pre_match
|
2347
|
+
if (line = post_match).empty?
|
2310
2348
|
parser_ctx.buffer = %(#{parser_ctx.buffer}#{LF})
|
2311
2349
|
parser_ctx.keep_cell_open
|
2312
2350
|
break
|
2313
2351
|
end
|
2314
2352
|
redo
|
2315
2353
|
end
|
2316
|
-
parser_ctx.buffer = %(#{parser_ctx.buffer}#{
|
2354
|
+
parser_ctx.buffer = %(#{parser_ctx.buffer}#{pre_match})
|
2317
2355
|
else # psv
|
2318
|
-
if
|
2319
|
-
|
2356
|
+
if pre_match.end_with? '\\'
|
2357
|
+
parser_ctx.skip_past_escaped_delimiter pre_match
|
2358
|
+
if (line = post_match).empty?
|
2320
2359
|
parser_ctx.buffer = %(#{parser_ctx.buffer}#{LF})
|
2321
2360
|
parser_ctx.keep_cell_open
|
2322
2361
|
break
|
2323
2362
|
end
|
2324
2363
|
redo
|
2325
2364
|
end
|
2326
|
-
next_cellspec, cell_text = parse_cellspec
|
2365
|
+
next_cellspec, cell_text = parse_cellspec pre_match
|
2327
2366
|
parser_ctx.push_cellspec next_cellspec
|
2328
2367
|
parser_ctx.buffer = %(#{parser_ctx.buffer}#{cell_text})
|
2329
2368
|
end
|
2330
2369
|
# don't break if empty to preserve empty cell found at end of line (see issue #1106)
|
2331
|
-
line = nil if (line =
|
2370
|
+
line = nil if (line = post_match).empty?
|
2332
2371
|
parser_ctx.close_cell
|
2333
2372
|
else
|
2334
2373
|
# no other delimiters to see here; suck up this line into the buffer and move on
|
2335
2374
|
parser_ctx.buffer = %(#{parser_ctx.buffer}#{line}#{LF})
|
2336
2375
|
case format
|
2337
2376
|
when 'csv'
|
2338
|
-
# QUESTION make stripping endlines in csv data an option? (unwrap-option?)
|
2339
|
-
parser_ctx.buffer = %(#{parser_ctx.buffer.rstrip} )
|
2340
2377
|
if parser_ctx.buffer_has_unclosed_quotes?
|
2341
2378
|
implicit_header, implicit_header_boundary = false, nil if implicit_header_boundary && loop_idx == 0
|
2342
2379
|
parser_ctx.keep_cell_open
|
@@ -2412,9 +2449,12 @@ class Parser
|
|
2412
2449
|
end
|
2413
2450
|
end
|
2414
2451
|
|
2415
|
-
|
2416
|
-
|
2417
|
-
|
2452
|
+
if (width = m[3])
|
2453
|
+
# to_i will strip the optional %
|
2454
|
+
spec['width'] = width == '~' ? -1 : width.to_i
|
2455
|
+
else
|
2456
|
+
spec['width'] = 1
|
2457
|
+
end
|
2418
2458
|
|
2419
2459
|
# make this an operation
|
2420
2460
|
if m[4] && TableCellStyles.key?(m[4])
|
@@ -2527,7 +2567,11 @@ class Parser
|
|
2527
2567
|
save_current = lambda {
|
2528
2568
|
if collector.empty?
|
2529
2569
|
unless type == :style
|
2530
|
-
|
2570
|
+
if reader
|
2571
|
+
logger.warn message_with_context %(invalid empty #{type} detected in style attribute), :source_location => reader.cursor_at_prev_line
|
2572
|
+
else
|
2573
|
+
logger.warn %(invalid empty #{type} detected in style attribute)
|
2574
|
+
end
|
2531
2575
|
end
|
2532
2576
|
else
|
2533
2577
|
case type
|
@@ -2535,7 +2579,11 @@ class Parser
|
|
2535
2579
|
(parsed[type] ||= []) << collector.join
|
2536
2580
|
when :id
|
2537
2581
|
if parsed.key? :id
|
2538
|
-
|
2582
|
+
if reader
|
2583
|
+
logger.warn message_with_context 'multiple ids detected in style attribute', :source_location => reader.cursor_at_prev_line
|
2584
|
+
else
|
2585
|
+
logger.warn 'multiple ids detected in style attribute'
|
2586
|
+
end
|
2539
2587
|
end
|
2540
2588
|
parsed[type] = collector.join
|
2541
2589
|
else
|
@@ -2571,15 +2619,13 @@ class Parser
|
|
2571
2619
|
|
2572
2620
|
attributes['id'] = parsed[:id] if parsed.key? :id
|
2573
2621
|
|
2574
|
-
|
2622
|
+
if parsed.key? :role
|
2623
|
+
attributes['role'] = (existing_role = attributes['role']).nil_or_empty? ? (parsed[:role].join ' ') : %(#{existing_role} #{parsed[:role].join ' '})
|
2624
|
+
end
|
2575
2625
|
|
2576
2626
|
if parsed.key? :option
|
2577
|
-
(
|
2578
|
-
|
2579
|
-
attributes['options'] = (options + existing_opts.split(',')) * ','
|
2580
|
-
else
|
2581
|
-
attributes['options'] = options * ','
|
2582
|
-
end
|
2627
|
+
(opts = parsed[:option]).each {|opt| attributes[%(#{opt}-option)] = '' }
|
2628
|
+
attributes['options'] = (existing_opts = attributes['options']).nil_or_empty? ? (opts.join ',') : %(#{existing_opts},#{opts.join ','})
|
2583
2629
|
end
|
2584
2630
|
|
2585
2631
|
parsed_style
|
@@ -2710,27 +2756,5 @@ class Parser
|
|
2710
2756
|
def self.sanitize_attribute_name(name)
|
2711
2757
|
name.gsub(InvalidAttributeNameCharsRx, '').downcase
|
2712
2758
|
end
|
2713
|
-
|
2714
|
-
# Internal: Converts a Roman numeral to an integer value.
|
2715
|
-
#
|
2716
|
-
# value - The String Roman numeral to convert
|
2717
|
-
#
|
2718
|
-
# Returns the Integer for this Roman numeral
|
2719
|
-
def self.roman_numeral_to_int(value)
|
2720
|
-
value = value.downcase
|
2721
|
-
digits = { 'i' => 1, 'v' => 5, 'x' => 10 }
|
2722
|
-
result = 0
|
2723
|
-
|
2724
|
-
(0..value.length - 1).each {|i|
|
2725
|
-
digit = digits[value[i..i]]
|
2726
|
-
if i + 1 < value.length && digits[value[i+1..i+1]] > digit
|
2727
|
-
result -= digit
|
2728
|
-
else
|
2729
|
-
result += digit
|
2730
|
-
end
|
2731
|
-
}
|
2732
|
-
|
2733
|
-
result
|
2734
|
-
end
|
2735
2759
|
end
|
2736
2760
|
end
|