asciidoctor 0.1.2 → 0.1.3
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/Gemfile +10 -0
- data/Guardfile +18 -0
- data/LICENSE +1 -1
- data/README.adoc +65 -21
- data/Rakefile +10 -0
- data/asciidoctor.gemspec +17 -35
- data/compat/asciidoc.conf +130 -13
- data/lib/asciidoctor.rb +107 -87
- data/lib/asciidoctor/abstract_block.rb +6 -2
- data/lib/asciidoctor/abstract_node.rb +21 -13
- data/lib/asciidoctor/attribute_list.rb +2 -5
- data/{stylesheets/asciidoctor.css → lib/asciidoctor/backends/_stylesheets.rb} +96 -46
- data/lib/asciidoctor/backends/base_template.rb +9 -4
- data/lib/asciidoctor/backends/docbook45.rb +246 -138
- data/lib/asciidoctor/backends/html5.rb +580 -381
- data/lib/asciidoctor/block.rb +2 -50
- data/lib/asciidoctor/cli/options.rb +9 -8
- data/lib/asciidoctor/document.rb +35 -45
- data/lib/asciidoctor/helpers.rb +10 -0
- data/lib/asciidoctor/lexer.rb +456 -148
- data/lib/asciidoctor/list_item.rb +0 -21
- data/lib/asciidoctor/path_resolver.rb +18 -12
- data/lib/asciidoctor/reader.rb +71 -26
- data/lib/asciidoctor/renderer.rb +2 -19
- data/lib/asciidoctor/section.rb +0 -1
- data/lib/asciidoctor/substituters.rb +150 -36
- data/lib/asciidoctor/table.rb +30 -24
- data/lib/asciidoctor/version.rb +1 -1
- data/man/asciidoctor.1 +22 -16
- data/man/asciidoctor.ad +24 -16
- data/test/attributes_test.rb +50 -0
- data/test/blocks_test.rb +660 -9
- data/test/document_test.rb +191 -14
- data/test/fixtures/encoding.asciidoc +8 -0
- data/test/invoker_test.rb +47 -0
- data/test/lexer_test.rb +172 -0
- data/test/links_test.rb +28 -0
- data/test/lists_test.rb +172 -13
- data/test/options_test.rb +29 -2
- data/test/paragraphs_test.rb +105 -47
- data/test/paths_test.rb +3 -3
- data/test/reader_test.rb +46 -0
- data/test/sections_test.rb +365 -12
- data/test/substitutions_test.rb +127 -11
- data/test/tables_test.rb +81 -14
- data/test/test_helper.rb +18 -7
- data/test/text_test.rb +17 -5
- metadata +9 -36
data/lib/asciidoctor/block.rb
CHANGED
@@ -14,9 +14,6 @@ class Block < AbstractBlock
|
|
14
14
|
# Public: Get/Set the original Array content for this section block.
|
15
15
|
attr_accessor :buffer
|
16
16
|
|
17
|
-
# Public: Get/Set the caption for this block
|
18
|
-
attr_accessor :caption
|
19
|
-
|
20
17
|
# Public: Initialize an Asciidoctor::Block object.
|
21
18
|
#
|
22
19
|
# parent - The parent Asciidoc Object.
|
@@ -26,7 +23,6 @@ class Block < AbstractBlock
|
|
26
23
|
def initialize(parent, context, buffer = nil)
|
27
24
|
super(parent, context)
|
28
25
|
@buffer = buffer
|
29
|
-
@caption = nil
|
30
26
|
end
|
31
27
|
|
32
28
|
# Public: Get the rendered String content for this Block. If the block
|
@@ -34,56 +30,12 @@ class Block < AbstractBlock
|
|
34
30
|
# rendered and returned as content that can be included in the
|
35
31
|
# parent block's template.
|
36
32
|
def render
|
37
|
-
Debug.debug { "Now rendering #{@context} block for #{self}" }
|
38
33
|
@document.playback_attributes @attributes
|
39
34
|
out = renderer.render("block_#{@context}", self)
|
40
35
|
@document.callouts.next_list if @context == :colist
|
41
36
|
out
|
42
37
|
end
|
43
38
|
|
44
|
-
def splain(parent_level = 0)
|
45
|
-
parent_level += 1
|
46
|
-
Debug.puts_indented(parent_level, "Block id: #{id}") unless self.id.nil?
|
47
|
-
Debug.puts_indented(parent_level, "Block title: #{title}") unless self.title.nil?
|
48
|
-
Debug.puts_indented(parent_level, "Block caption: #{caption}") unless self.caption.nil?
|
49
|
-
Debug.puts_indented(parent_level, "Block level: #{level}") unless self.level.nil?
|
50
|
-
Debug.puts_indented(parent_level, "Block context: #{context}") unless self.context.nil?
|
51
|
-
|
52
|
-
Debug.puts_indented(parent_level, "Blocks: #{@blocks.count}")
|
53
|
-
|
54
|
-
if buffer.is_a? Enumerable
|
55
|
-
buffer.each_with_index do |buf, i|
|
56
|
-
Debug.puts_indented(parent_level, "v" * (60 - parent_level*2))
|
57
|
-
Debug.puts_indented(parent_level, "Buffer ##{i} is a #{buf.class}")
|
58
|
-
Debug.puts_indented(parent_level, "Name is #{buf.title rescue 'n/a'}")
|
59
|
-
|
60
|
-
if buf.respond_to? :splain
|
61
|
-
buf.splain(parent_level)
|
62
|
-
else
|
63
|
-
Debug.puts_indented(parent_level, "Buffer: #{buf}")
|
64
|
-
end
|
65
|
-
Debug.puts_indented(parent_level, "^" * (60 - parent_level*2))
|
66
|
-
end
|
67
|
-
else
|
68
|
-
if buffer.respond_to? :splain
|
69
|
-
buffer.splain(parent_level)
|
70
|
-
else
|
71
|
-
Debug.puts_indented(parent_level, "Buffer: #{@buffer}")
|
72
|
-
end
|
73
|
-
end
|
74
|
-
|
75
|
-
@blocks.each_with_index do |block, i|
|
76
|
-
Debug.puts_indented(parent_level, "v" * (60 - parent_level*2))
|
77
|
-
Debug.puts_indented(parent_level, "Block ##{i} is a #{block.class}")
|
78
|
-
Debug.puts_indented(parent_level, "Name is #{block.title rescue 'n/a'}")
|
79
|
-
|
80
|
-
block.splain(parent_level) if block.respond_to? :splain
|
81
|
-
Debug.puts_indented(parent_level, "^" * (60 - parent_level*2))
|
82
|
-
end
|
83
|
-
|
84
|
-
nil
|
85
|
-
end
|
86
|
-
|
87
39
|
# Public: Get an HTML-ified version of the source buffer, with special
|
88
40
|
# Asciidoc characters and entities converted to their HTML equivalents.
|
89
41
|
#
|
@@ -96,7 +48,7 @@ class Block < AbstractBlock
|
|
96
48
|
# => ["<em>This</em> is what happens when you <meet> a stranger in the <alps>!"]
|
97
49
|
def content
|
98
50
|
case @context
|
99
|
-
when :preamble
|
51
|
+
when :preamble
|
100
52
|
@blocks.map {|b| b.render }.join
|
101
53
|
# lists get iterated in the template (for now)
|
102
54
|
# list items recurse into this block when their text and content methods are called
|
@@ -106,7 +58,7 @@ class Block < AbstractBlock
|
|
106
58
|
apply_literal_subs(@buffer)
|
107
59
|
when :pass
|
108
60
|
apply_passthrough_subs(@buffer)
|
109
|
-
when :admonition, :example, :sidebar, :quote, :verse
|
61
|
+
when :admonition, :example, :sidebar, :quote, :verse, :open
|
110
62
|
if !@buffer.nil?
|
111
63
|
apply_para_subs(@buffer)
|
112
64
|
else
|
@@ -44,11 +44,11 @@ Example: asciidoctor -b html5 source.asciidoc
|
|
44
44
|
opts.on('-v', '--verbose', 'enable verbose mode (default: false)') do |verbose|
|
45
45
|
self[:verbose] = true
|
46
46
|
end
|
47
|
-
opts.on('-b', '--backend BACKEND',
|
47
|
+
opts.on('-b', '--backend BACKEND', 'set output format backend (default: html5)') do |backend|
|
48
48
|
self[:attributes]['backend'] = backend
|
49
49
|
end
|
50
|
-
opts.on('-d', '--doctype DOCTYPE', ['article', 'book'],
|
51
|
-
'document type to use when rendering output: [article, book] (default: article)') do |doc_type|
|
50
|
+
opts.on('-d', '--doctype DOCTYPE', ['article', 'book', 'inline'],
|
51
|
+
'document type to use when rendering output: [article, book, inline] (default: article)') do |doc_type|
|
52
52
|
self[:attributes]['doctype'] = doc_type
|
53
53
|
end
|
54
54
|
opts.on('-o', '--out-file FILE', 'output file (default: based on input file path); use - to output to STDOUT') do |output_file|
|
@@ -78,12 +78,13 @@ Example: asciidoctor -b html5 source.asciidoc
|
|
78
78
|
opts.on('-C', '--compact', 'compact the output by removing blank lines (default: false)') do
|
79
79
|
self[:compact] = true
|
80
80
|
end
|
81
|
-
opts.on('-a', '--attribute
|
82
|
-
'a list of attributes
|
83
|
-
'these attributes take precedence over attributes
|
81
|
+
opts.on('-a', '--attribute key[=value],key2[=value2],...', Array,
|
82
|
+
'a list of document attributes to set in the form of key, key! or key=value pair',
|
83
|
+
'unless @ is appended to the value, these attributes take precedence over attributes',
|
84
|
+
'defined in the source document') do |attribs|
|
84
85
|
attribs.each do |attrib|
|
85
|
-
|
86
|
-
self[:attributes][
|
86
|
+
key, val = attrib.split '=', 2
|
87
|
+
self[:attributes][key] = val || ''
|
87
88
|
end
|
88
89
|
end
|
89
90
|
opts.on('-T', '--template-dir DIR', 'directory containing custom render templates the override the built-in set') do |template_dir|
|
data/lib/asciidoctor/document.rb
CHANGED
@@ -104,13 +104,14 @@ class Document < AbstractBlock
|
|
104
104
|
|
105
105
|
if options[:parent]
|
106
106
|
@parent_document = options.delete(:parent)
|
107
|
-
# should we dup here?
|
107
|
+
# should we dup attributes here?
|
108
108
|
options[:attributes] = @parent_document.attributes
|
109
|
-
options[:safe] ||= @parent_document.safe
|
110
109
|
options[:base_dir] ||= @parent_document.base_dir
|
110
|
+
@safe = @parent_document.safe
|
111
111
|
@renderer = @parent_document.renderer
|
112
112
|
else
|
113
113
|
@parent_document = nil
|
114
|
+
@safe = nil
|
114
115
|
end
|
115
116
|
|
116
117
|
@header = nil
|
@@ -124,7 +125,19 @@ class Document < AbstractBlock
|
|
124
125
|
@counters = {}
|
125
126
|
@callouts = Callouts.new
|
126
127
|
@options = options
|
127
|
-
|
128
|
+
# safely resolve the safe mode from const, int or string
|
129
|
+
if @safe.nil? && !(safe_mode = @options[:safe])
|
130
|
+
@safe = SafeMode::SECURE
|
131
|
+
elsif safe_mode.is_a?(Fixnum)
|
132
|
+
# be permissive in case API user wants to define new levels
|
133
|
+
@safe = safe_mode
|
134
|
+
else
|
135
|
+
begin
|
136
|
+
@safe = SafeMode.const_get(safe_mode.to_s.upcase).to_i
|
137
|
+
rescue
|
138
|
+
@safe = SafeMode::SECURE.to_i
|
139
|
+
end
|
140
|
+
end
|
128
141
|
@options[:header_footer] = @options.fetch(:header_footer, false)
|
129
142
|
|
130
143
|
@attributes['encoding'] = 'UTF-8'
|
@@ -184,11 +197,11 @@ class Document < AbstractBlock
|
|
184
197
|
|
185
198
|
# allow common attributes backend and doctype to be set using options hash
|
186
199
|
unless @options[:backend].nil?
|
187
|
-
@attribute_overrides['backend'] = @options[:backend]
|
200
|
+
@attribute_overrides['backend'] = @options[:backend].to_s
|
188
201
|
end
|
189
202
|
|
190
203
|
unless @options[:doctype].nil?
|
191
|
-
@attribute_overrides['doctype'] = @options[:doctype]
|
204
|
+
@attribute_overrides['doctype'] = @options[:doctype].to_s
|
192
205
|
end
|
193
206
|
|
194
207
|
if @safe >= SafeMode::SERVER
|
@@ -269,15 +282,6 @@ class Document < AbstractBlock
|
|
269
282
|
Lexer.parse(@reader, self, :header_only => @options.fetch(:parse_header_only, false))
|
270
283
|
|
271
284
|
@callouts.rewind
|
272
|
-
|
273
|
-
Debug.debug {
|
274
|
-
msg = []
|
275
|
-
msg << "Found #{@blocks.size} blocks in this document:"
|
276
|
-
@blocks.each {|b|
|
277
|
-
msg << b
|
278
|
-
}
|
279
|
-
msg * "\n"
|
280
|
-
}
|
281
285
|
end
|
282
286
|
|
283
287
|
# Public: Get the named counter and take the next number in the sequence.
|
@@ -441,6 +445,10 @@ class Document < AbstractBlock
|
|
441
445
|
# Internal: Branch the attributes so that the original state can be restored
|
442
446
|
# at a future time.
|
443
447
|
def save_attributes
|
448
|
+
unless @attributes.has_key?('doctitle') || (val = doctitle).nil?
|
449
|
+
@attributes['doctitle'] = val
|
450
|
+
end
|
451
|
+
|
444
452
|
# css-signature cannot be updated after header attributes are processed
|
445
453
|
if @id.nil? && @attributes.has_key?('css-signature')
|
446
454
|
@id = @attributes['css-signature']
|
@@ -578,27 +586,6 @@ class Document < AbstractBlock
|
|
578
586
|
@attributes["filetype-#{file_type}"] = ''
|
579
587
|
end
|
580
588
|
|
581
|
-
def splain
|
582
|
-
Debug.debug {
|
583
|
-
msg = ''
|
584
|
-
if @header
|
585
|
-
msg = "Header is #{@header}"
|
586
|
-
else
|
587
|
-
msg = "No header"
|
588
|
-
end
|
589
|
-
|
590
|
-
msg += "I have #{@blocks.count} blocks"
|
591
|
-
@blocks.each_with_index do |block, i|
|
592
|
-
msg += "v" * 60
|
593
|
-
msg += "Block ##{i} is a #{block.class}"
|
594
|
-
msg += "Name is #{block.title rescue 'n/a'}"
|
595
|
-
block.splain(0) if block.respond_to? :splain
|
596
|
-
msg += "^" * 60
|
597
|
-
end
|
598
|
-
}
|
599
|
-
nil
|
600
|
-
end
|
601
|
-
|
602
589
|
def renderer(opts = {})
|
603
590
|
return @renderer if @renderer
|
604
591
|
|
@@ -627,11 +614,21 @@ class Document < AbstractBlock
|
|
627
614
|
def render(opts = {})
|
628
615
|
restore_attributes
|
629
616
|
r = renderer(opts)
|
630
|
-
|
617
|
+
if doctype == 'inline'
|
618
|
+
# QUESTION should we warn if @blocks.size > 0 and the first block is not a paragraph?
|
619
|
+
if @blocks.size > 0 && (block = @blocks.first).context == :paragraph
|
620
|
+
block.content
|
621
|
+
else
|
622
|
+
''
|
623
|
+
end
|
624
|
+
else
|
625
|
+
@options.merge(opts)[:header_footer] ? r.render('document', self).strip : r.render('embedded', self)
|
626
|
+
end
|
631
627
|
end
|
632
628
|
|
633
629
|
def content
|
634
|
-
# per AsciiDoc-spec, remove the title
|
630
|
+
# per AsciiDoc-spec, remove the title before rendering the body,
|
631
|
+
# regardless of whether the header is rendered)
|
635
632
|
@attributes.delete('title')
|
636
633
|
@blocks.map {|b| b.render }.join
|
637
634
|
end
|
@@ -651,14 +648,7 @@ class Document < AbstractBlock
|
|
651
648
|
if safe >= SafeMode::SECURE
|
652
649
|
''
|
653
650
|
else
|
654
|
-
if ext.nil?
|
655
|
-
case @attributes['basebackend']
|
656
|
-
when 'docbook'
|
657
|
-
ext = '.xml'
|
658
|
-
when 'html'
|
659
|
-
ext = '.html'
|
660
|
-
end
|
661
|
-
end
|
651
|
+
ext = @attributes['outfilesuffix'] if ext.nil?
|
662
652
|
|
663
653
|
content = nil
|
664
654
|
|
data/lib/asciidoctor/helpers.rb
CHANGED
@@ -37,6 +37,16 @@ module Helpers
|
|
37
37
|
end
|
38
38
|
end
|
39
39
|
|
40
|
+
def self.mkdir_p(dir)
|
41
|
+
unless File.directory? dir
|
42
|
+
parent_dir = File.dirname(dir)
|
43
|
+
if !File.directory?(parent_dir = File.dirname(dir)) && parent_dir != '.'
|
44
|
+
mkdir_p(parent_dir)
|
45
|
+
end
|
46
|
+
Dir.mkdir(dir)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
40
50
|
# Public: A generic capture output routine to be used in templates
|
41
51
|
#def self.capture_output(*args, &block)
|
42
52
|
# Proc.new { block.call(*args) }
|
data/lib/asciidoctor/lexer.rb
CHANGED
@@ -74,10 +74,33 @@ class Lexer
|
|
74
74
|
# that precede first block
|
75
75
|
block_attributes = parse_block_metadata_lines(reader, document)
|
76
76
|
|
77
|
+
# special case, block title is not allowed above document title,
|
78
|
+
# carry attributes over to the document body
|
79
|
+
if block_attributes.has_key?('title')
|
80
|
+
document.clear_playback_attributes block_attributes
|
81
|
+
document.save_attributes
|
82
|
+
block_attributes['invalid-header'] = true
|
83
|
+
return block_attributes
|
84
|
+
end
|
85
|
+
|
86
|
+
# yep, document title logic in AsciiDoc is just insanity
|
87
|
+
# definitely an area for spec refinement
|
88
|
+
assigned_doctitle = nil
|
89
|
+
unless (val = document.attributes.fetch('doctitle', '')).empty?
|
90
|
+
document.title = val
|
91
|
+
assigned_doctitle = val
|
92
|
+
end
|
93
|
+
|
94
|
+
section_title = nil
|
77
95
|
# check if the first line is the document title
|
78
96
|
# if so, add a header to the document and parse the header metadata
|
79
97
|
if is_next_line_document_title?(reader, block_attributes)
|
80
|
-
document.id,
|
98
|
+
document.id, doctitle, _, _ = parse_section_title(reader, document)
|
99
|
+
unless assigned_doctitle
|
100
|
+
document.title = doctitle
|
101
|
+
assigned_doctitle = doctitle
|
102
|
+
end
|
103
|
+
document.attributes['doctitle'] = section_title = doctitle
|
81
104
|
# QUESTION: should this be encapsulated in document?
|
82
105
|
if document.id.nil? && block_attributes.has_key?('id')
|
83
106
|
document.id = block_attributes.delete('id')
|
@@ -85,8 +108,15 @@ class Lexer
|
|
85
108
|
parse_header_metadata(reader, document)
|
86
109
|
end
|
87
110
|
|
88
|
-
if document.attributes.
|
89
|
-
|
111
|
+
if !(val = document.attributes.fetch('doctitle', '')).empty? &&
|
112
|
+
val != section_title
|
113
|
+
document.title = val
|
114
|
+
assigned_doctitle = val
|
115
|
+
end
|
116
|
+
|
117
|
+
# restore doctitle attribute to original assignment
|
118
|
+
if assigned_doctitle
|
119
|
+
document.attributes['doctitle'] = assigned_doctitle
|
90
120
|
end
|
91
121
|
|
92
122
|
document.clear_playback_attributes block_attributes
|
@@ -143,7 +173,7 @@ class Lexer
|
|
143
173
|
# NOTE we could drop a hint in the attributes to indicate
|
144
174
|
# that we are at a section title (so we don't have to check)
|
145
175
|
if parent.is_a?(Document) && parent.blocks.empty? &&
|
146
|
-
(parent.has_header? || !is_next_line_section?(reader, attributes))
|
176
|
+
(parent.has_header? || attributes.delete('invalid-header') || !is_next_line_section?(reader, attributes))
|
147
177
|
|
148
178
|
if parent.has_header?
|
149
179
|
preamble = Block.new(parent, :preamble)
|
@@ -193,11 +223,10 @@ class Lexer
|
|
193
223
|
if next_level
|
194
224
|
next_level += section.document.attr('leveloffset', 0).to_i
|
195
225
|
doctype = parent.document.doctype
|
196
|
-
if next_level == 0 && doctype != 'book'
|
197
|
-
puts "asciidoctor: ERROR: line #{reader.lineno + 1}: only book doctypes can contain level 0 sections"
|
198
|
-
end
|
199
226
|
if next_level > current_level || (section.is_a?(Document) && next_level == 0)
|
200
|
-
|
227
|
+
if next_level == 0 && doctype != 'book'
|
228
|
+
puts "asciidoctor: ERROR: line #{reader.lineno + 1}: only book doctypes can contain level 0 sections"
|
229
|
+
elsif !expected_next_levels.nil? && !expected_next_levels.include?(next_level)
|
201
230
|
puts "asciidoctor: WARNING: line #{reader.lineno + 1}: section title out of sequence: " +
|
202
231
|
"expected #{expected_next_levels.size > 1 ? 'levels' : 'level'} #{expected_next_levels * ' or '}, " +
|
203
232
|
"got level #{next_level}"
|
@@ -206,12 +235,15 @@ class Lexer
|
|
206
235
|
new_section, attributes = next_section(reader, section, attributes)
|
207
236
|
section << new_section
|
208
237
|
else
|
238
|
+
if next_level == 0 && doctype != 'book'
|
239
|
+
puts "asciidoctor: ERROR: line #{reader.lineno + 1}: only book doctypes can contain level 0 sections"
|
240
|
+
end
|
209
241
|
# close this section (and break out of the nesting) to begin a new one
|
210
242
|
break
|
211
243
|
end
|
212
244
|
else
|
213
245
|
# just take one block or else we run the risk of overrunning section boundaries
|
214
|
-
new_block = next_block(reader, section, attributes, :parse_metadata => false)
|
246
|
+
new_block = next_block(reader, (preamble || section), attributes, :parse_metadata => false)
|
215
247
|
if !new_block.nil?
|
216
248
|
(preamble || section) << new_block
|
217
249
|
attributes = {}
|
@@ -224,8 +256,8 @@ class Lexer
|
|
224
256
|
reader.skip_blank_lines
|
225
257
|
end
|
226
258
|
|
227
|
-
|
228
|
-
|
259
|
+
if preamble && !preamble.blocks?
|
260
|
+
# drop the preamble if it has no content
|
229
261
|
section.delete_at(0)
|
230
262
|
end
|
231
263
|
|
@@ -260,11 +292,13 @@ class Lexer
|
|
260
292
|
# bail if we've reached the end of the parent block or document
|
261
293
|
return nil unless reader.has_more_lines?
|
262
294
|
|
295
|
+
text_only = options[:text]
|
263
296
|
# check for option to find list item text only
|
264
297
|
# if skipped a line, assume a list continuation was
|
265
298
|
# used and block content is acceptable
|
266
|
-
if
|
299
|
+
if text_only && skipped > 0
|
267
300
|
options.delete(:text)
|
301
|
+
text_only = false
|
268
302
|
end
|
269
303
|
|
270
304
|
parse_metadata = options.fetch(:parse_metadata, true)
|
@@ -292,9 +326,8 @@ class Lexer
|
|
292
326
|
block_context = nil
|
293
327
|
terminator = nil
|
294
328
|
# QUESTION put this inside call to rekey attributes?
|
295
|
-
if attributes
|
296
|
-
explicit_style = attributes
|
297
|
-
style = attributes['style'] = attributes[1]
|
329
|
+
if attributes[1]
|
330
|
+
style, explicit_style = parse_style_attribute(attributes)
|
298
331
|
end
|
299
332
|
|
300
333
|
if delimited_blk_match = is_delimited_block?(this_line, true)
|
@@ -332,7 +365,7 @@ class Lexer
|
|
332
365
|
end
|
333
366
|
|
334
367
|
# process lines normally
|
335
|
-
if !
|
368
|
+
if !text_only
|
336
369
|
# NOTE we're letting break lines (ruler, page_break, etc) have attributes
|
337
370
|
if (match = this_line.match(REGEXP[:break_line]))
|
338
371
|
block = Block.new(parent, BREAK_LINES[match[0][0..2]])
|
@@ -461,8 +494,7 @@ class Lexer
|
|
461
494
|
break
|
462
495
|
|
463
496
|
# FIXME create another set for "passthrough" styles
|
464
|
-
|
465
|
-
elsif !style.nil? && style != 'normal' && style != 'partintro'
|
497
|
+
elsif !style.nil? && style != 'normal'
|
466
498
|
if PARAGRAPH_STYLES.include?(style)
|
467
499
|
block_context = style.to_sym
|
468
500
|
reader.unshift_line this_line
|
@@ -498,13 +530,7 @@ class Lexer
|
|
498
530
|
(COMPLIANCE[:block_terminates_paragraph] && (is_delimited_block?(line) || line.match(REGEXP[:attr_line])))
|
499
531
|
}
|
500
532
|
|
501
|
-
|
502
|
-
if !buffer.empty?
|
503
|
-
offset = buffer.map {|line| line.match(REGEXP[:leading_blanks])[1].length }.min
|
504
|
-
if offset > 0
|
505
|
-
buffer = buffer.map {|l| l.sub(/^\s{1,#{offset}}/, '') }
|
506
|
-
end
|
507
|
-
end
|
533
|
+
reset_block_indent! buffer
|
508
534
|
|
509
535
|
block = Block.new(parent, :literal, buffer)
|
510
536
|
# a literal gets special meaning inside of a definition list
|
@@ -541,12 +567,49 @@ class Lexer
|
|
541
567
|
|
542
568
|
catalog_inline_anchors(buffer.join, document)
|
543
569
|
|
544
|
-
|
570
|
+
first_line = buffer.first
|
571
|
+
if !text_only && (admonition_match = first_line.match(REGEXP[:admonition_inline]))
|
545
572
|
buffer[0] = admonition_match.post_match.lstrip
|
546
573
|
block = Block.new(parent, :admonition, buffer)
|
547
574
|
attributes['style'] = admonition_match[1]
|
548
575
|
attributes['name'] = admonition_name = admonition_match[1].downcase
|
549
576
|
attributes['caption'] ||= document.attributes["#{admonition_name}-caption"]
|
577
|
+
elsif !text_only && COMPLIANCE[:markdown_syntax] && first_line.start_with?('> ')
|
578
|
+
buffer.map! {|line|
|
579
|
+
if line.start_with?('> ')
|
580
|
+
line[2..-1]
|
581
|
+
elsif line.chomp == '>'
|
582
|
+
line[1..-1]
|
583
|
+
else
|
584
|
+
line
|
585
|
+
end
|
586
|
+
}
|
587
|
+
|
588
|
+
if buffer.last.start_with?('-- ')
|
589
|
+
attribution, citetitle = buffer.pop[3..-1].split(', ')
|
590
|
+
buffer.pop while buffer.last.chomp.empty?
|
591
|
+
buffer[-1] = buffer.last.chomp
|
592
|
+
else
|
593
|
+
attribution, citetitle = nil
|
594
|
+
end
|
595
|
+
attributes['style'] = 'quote'
|
596
|
+
attributes['attribution'] = attribution unless attribution.nil?
|
597
|
+
attributes['citetitle'] = citetitle unless citetitle.nil?
|
598
|
+
# NOTE will only detect headings that are floating titles (not section titles)
|
599
|
+
# TODO could assume a floating title when inside a block context
|
600
|
+
block = build_block(:quote, :complex, false, parent, Reader.new(buffer), attributes)
|
601
|
+
elsif !text_only && buffer.size > 1 && first_line.start_with?('"') &&
|
602
|
+
buffer.last.start_with?('-- ') && buffer[-2].chomp.end_with?('"')
|
603
|
+
buffer[0] = first_line[1..-1]
|
604
|
+
attribution, citetitle = buffer.pop[3..-1].split(', ')
|
605
|
+
buffer.pop while buffer.last.chomp.empty?
|
606
|
+
buffer[-1] = buffer.last.chomp.chop
|
607
|
+
attributes['style'] = 'quote'
|
608
|
+
attributes['attribution'] = attribution unless attribution.nil?
|
609
|
+
attributes['citetitle'] = citetitle unless citetitle.nil?
|
610
|
+
block = Block.new(parent, :quote, buffer)
|
611
|
+
#block = Block.new(parent, :quote)
|
612
|
+
#block << Block.new(block, :paragraph, buffer)
|
550
613
|
else
|
551
614
|
# QUESTION is this necessary?
|
552
615
|
#if style == 'normal' && [' ', "\t"].include?(buffer.first[0..0])
|
@@ -565,6 +628,10 @@ class Lexer
|
|
565
628
|
|
566
629
|
# either delimited block or styled paragraph
|
567
630
|
if block.nil? && !block_context.nil?
|
631
|
+
# abstract and partintro should be handled by open block
|
632
|
+
# FIXME kind of hackish...need to sort out how to generalize this
|
633
|
+
block_context = :open if block_context == :abstract || block_context == :partintro
|
634
|
+
|
568
635
|
case block_context
|
569
636
|
when :admonition
|
570
637
|
attributes['name'] = admonition_name = style.downcase
|
@@ -576,7 +643,7 @@ class Lexer
|
|
576
643
|
return nil
|
577
644
|
|
578
645
|
when :example
|
579
|
-
block = build_block(block_context, :complex, terminator, parent, reader, attributes, true)
|
646
|
+
block = build_block(block_context, :complex, terminator, parent, reader, attributes, {:supports_caption => true})
|
580
647
|
|
581
648
|
when :listing, :fenced_code, :source
|
582
649
|
if block_context == :fenced_code
|
@@ -587,7 +654,7 @@ class Lexer
|
|
587
654
|
elsif block_context == :source
|
588
655
|
AttributeList.rekey(attributes, [nil, 'language', 'linenums'])
|
589
656
|
end
|
590
|
-
block = build_block(:listing, :verbatim, terminator, parent, reader, attributes, true)
|
657
|
+
block = build_block(:listing, :verbatim, terminator, parent, reader, attributes, {:supports_caption => true})
|
591
658
|
|
592
659
|
when :literal
|
593
660
|
block = build_block(block_context, :verbatim, terminator, parent, reader, attributes)
|
@@ -600,6 +667,12 @@ class Lexer
|
|
600
667
|
|
601
668
|
when :table
|
602
669
|
block_reader = Reader.new reader.grab_lines_until(:terminator => terminator, :skip_line_comments => true)
|
670
|
+
case terminator[0..0]
|
671
|
+
when ','
|
672
|
+
attributes['format'] = 'csv'
|
673
|
+
when ':'
|
674
|
+
attributes['format'] = 'dsv'
|
675
|
+
end
|
603
676
|
block = next_table(block_reader, parent, attributes)
|
604
677
|
|
605
678
|
when :quote, :verse
|
@@ -621,7 +694,7 @@ class Lexer
|
|
621
694
|
# REVIEW seems like there is a better way to organize this wrap-up
|
622
695
|
block.id ||= attributes['id'] if attributes.has_key?('id')
|
623
696
|
block.title = attributes['title'] unless block.title?
|
624
|
-
block.caption ||= attributes
|
697
|
+
block.caption ||= attributes.delete('caption')
|
625
698
|
# AsciiDoc always use [id] as the reftext in HTML output,
|
626
699
|
# but I'd like to do better in Asciidoctor
|
627
700
|
if block.id && block.title? && !attributes.has_key?('reftext')
|
@@ -651,11 +724,13 @@ class Lexer
|
|
651
724
|
tip = line[0..3]
|
652
725
|
tl = 4
|
653
726
|
|
654
|
-
|
655
|
-
|
656
|
-
|
657
|
-
|
658
|
-
|
727
|
+
if COMPLIANCE[:markdown_syntax]
|
728
|
+
# special case for fenced code blocks
|
729
|
+
tip_alt = tip.chop
|
730
|
+
if tip_alt == '```' || tip_alt == '~~~'
|
731
|
+
tip = tip_alt
|
732
|
+
tl = 3
|
733
|
+
end
|
659
734
|
end
|
660
735
|
end
|
661
736
|
|
@@ -689,8 +764,9 @@ class Lexer
|
|
689
764
|
end
|
690
765
|
|
691
766
|
# whether a block supports complex content should be a config setting
|
767
|
+
# if terminator is false, that means the all the lines in the reader should be parsed
|
692
768
|
# NOTE could invoke filter in here, before and after parsing
|
693
|
-
def self.build_block(block_context, content_type, terminator, parent, reader, attributes,
|
769
|
+
def self.build_block(block_context, content_type, terminator, parent, reader, attributes, options = {})
|
694
770
|
if terminator.nil?
|
695
771
|
if content_type == :verbatim
|
696
772
|
buffer = reader.grab_lines_until(:break_on_blank_lines => true, :break_on_list_continuation => true)
|
@@ -706,14 +782,21 @@ class Lexer
|
|
706
782
|
end
|
707
783
|
elsif content_type != :complex
|
708
784
|
buffer = reader.grab_lines_until(:terminator => terminator, :chomp_last_line => true)
|
785
|
+
elsif terminator == false
|
786
|
+
buffer = nil
|
787
|
+
block_reader = reader
|
709
788
|
else
|
710
789
|
buffer = nil
|
711
790
|
block_reader = Reader.new reader.grab_lines_until(:terminator => terminator)
|
712
791
|
end
|
713
792
|
|
793
|
+
if content_type == :verbatim && attributes.has_key?('indent')
|
794
|
+
reset_block_indent! buffer, attributes['indent'].to_i
|
795
|
+
end
|
796
|
+
|
714
797
|
block = Block.new(parent, block_context, buffer)
|
715
798
|
# should supports_caption be necessary?
|
716
|
-
if supports_caption
|
799
|
+
if options.fetch(:supports_caption, false)
|
717
800
|
block.title = attributes.delete('title') if attributes.has_key?('title')
|
718
801
|
block.assign_caption attributes.delete('caption')
|
719
802
|
end
|
@@ -1114,7 +1197,7 @@ class Lexer
|
|
1114
1197
|
end
|
1115
1198
|
|
1116
1199
|
#puts "BUFFER[#{list_type},#{sibling_trait}]>#{buffer.join}<BUFFER"
|
1117
|
-
#puts "BUFFER[#{list_type},#{sibling_trait}]>#{buffer}<BUFFER"
|
1200
|
+
#puts "BUFFER[#{list_type},#{sibling_trait}]>#{buffer.inspect}<BUFFER"
|
1118
1201
|
|
1119
1202
|
buffer
|
1120
1203
|
end
|
@@ -1130,33 +1213,39 @@ class Lexer
|
|
1130
1213
|
def self.initialize_section(reader, parent, attributes = {})
|
1131
1214
|
section = Section.new parent
|
1132
1215
|
section.id, section.title, section.level, _ = parse_section_title(reader, section.document)
|
1133
|
-
|
1134
|
-
section.id = attributes['id']
|
1135
|
-
else
|
1136
|
-
# generate an id if one was not *embedded* in the heading line
|
1137
|
-
# or as an anchor above the section
|
1138
|
-
section.id ||= section.generate_id
|
1139
|
-
end
|
1140
|
-
|
1141
|
-
if section.id
|
1142
|
-
section.document.register(:ids, [section.id, section.title])
|
1143
|
-
end
|
1144
|
-
|
1216
|
+
# parse style, id and role from first positional attribute
|
1145
1217
|
if attributes[1]
|
1146
|
-
section.sectname = attributes
|
1218
|
+
section.sectname, _ = parse_style_attribute(attributes)
|
1147
1219
|
section.special = true
|
1148
1220
|
document = parent.document
|
1221
|
+
# HACK needs to be refactored so it's driven by config
|
1222
|
+
if section.sectname == 'abstract' && document.doctype == 'book'
|
1223
|
+
section.sectname = "sect1"
|
1224
|
+
section.special = false
|
1225
|
+
section.level = 1
|
1149
1226
|
# FIXME refactor to use assign_caption (also check requirements)
|
1150
|
-
|
1227
|
+
elsif section.sectname == 'appendix' &&
|
1151
1228
|
!attributes.has_key?('caption') &&
|
1152
1229
|
!document.attributes.has_key?('caption')
|
1153
1230
|
number = document.counter('appendix-number', 'A')
|
1154
|
-
|
1231
|
+
section.caption = "#{document.attributes['appendix-caption']} #{number}: "
|
1155
1232
|
Document::AttributeEntry.new('appendix-number', number).save_to(attributes)
|
1156
1233
|
end
|
1157
1234
|
else
|
1158
1235
|
section.sectname = "sect#{section.level}"
|
1159
1236
|
end
|
1237
|
+
|
1238
|
+
if section.id.nil? && (id = attributes['id'])
|
1239
|
+
section.id = id
|
1240
|
+
else
|
1241
|
+
# generate an id if one was not *embedded* in the heading line
|
1242
|
+
# or as an anchor above the section
|
1243
|
+
section.id ||= section.generate_id
|
1244
|
+
end
|
1245
|
+
|
1246
|
+
if section.id
|
1247
|
+
section.document.register(:ids, [section.id, section.title])
|
1248
|
+
end
|
1160
1249
|
section.update_attributes(attributes)
|
1161
1250
|
reader.skip_blank_lines
|
1162
1251
|
|
@@ -1173,8 +1262,8 @@ class Lexer
|
|
1173
1262
|
|
1174
1263
|
#--
|
1175
1264
|
# = is level 0, == is level 1, etc.
|
1176
|
-
def self.single_line_section_level(
|
1177
|
-
|
1265
|
+
def self.single_line_section_level(marker)
|
1266
|
+
marker.length - 1
|
1178
1267
|
end
|
1179
1268
|
|
1180
1269
|
# Internal: Checks if the next line on the Reader is a section title
|
@@ -1185,7 +1274,7 @@ class Lexer
|
|
1185
1274
|
# returns the section level if the Reader is positioned at a section title,
|
1186
1275
|
# false otherwise
|
1187
1276
|
def self.is_next_line_section?(reader, attributes)
|
1188
|
-
return false if !attributes[1].nil? && ['float', 'discrete'].include?(
|
1277
|
+
return false if !(val = attributes[1]).nil? && ['float', 'discrete'].include?(val)
|
1189
1278
|
return false if !reader.has_more_lines?
|
1190
1279
|
is_section_title?(*reader.peek_lines(2))
|
1191
1280
|
end
|
@@ -1218,7 +1307,8 @@ class Lexer
|
|
1218
1307
|
end
|
1219
1308
|
|
1220
1309
|
def self.is_single_line_section_title?(line1)
|
1221
|
-
if !line1.nil? && (
|
1310
|
+
if !line1.nil? && (line1.start_with?('=') || (COMPLIANCE[:markdown_syntax] && line1.start_with?('#'))) &&
|
1311
|
+
(match = line1.match(REGEXP[:section_title]))
|
1222
1312
|
single_line_section_level match[1]
|
1223
1313
|
else
|
1224
1314
|
false
|
@@ -1226,8 +1316,8 @@ class Lexer
|
|
1226
1316
|
end
|
1227
1317
|
|
1228
1318
|
def self.is_two_line_section_title?(line1, line2)
|
1229
|
-
if !line1.nil? && !line2.nil? &&
|
1230
|
-
line2.match(REGEXP[:section_underline]) &&
|
1319
|
+
if !line1.nil? && !line2.nil? && SECTION_LEVELS.has_key?(line2[0..0]) &&
|
1320
|
+
line2.match(REGEXP[:section_underline]) && line1.match(REGEXP[:section_name]) &&
|
1231
1321
|
# chomp so that a (non-visible) endline does not impact calculation
|
1232
1322
|
(line1.chomp.size - line2.chomp.size).abs <= 1
|
1233
1323
|
section_level line2
|
@@ -1286,14 +1376,15 @@ class Lexer
|
|
1286
1376
|
sect_level = -1
|
1287
1377
|
single_line = true
|
1288
1378
|
|
1289
|
-
if
|
1379
|
+
if (line1.start_with?('=') || (COMPLIANCE[:markdown_syntax] && line1.start_with?('#'))) &&
|
1380
|
+
(match = line1.match(REGEXP[:section_title]))
|
1290
1381
|
sect_id = match[3]
|
1291
1382
|
sect_title = match[2]
|
1292
1383
|
sect_level = single_line_section_level match[1]
|
1293
1384
|
else
|
1294
1385
|
line2 = reader.peek_line
|
1295
|
-
if !line2.nil? && (
|
1296
|
-
|
1386
|
+
if !line2.nil? && SECTION_LEVELS.has_key?(line2[0..0]) && line2.match(REGEXP[:section_underline]) &&
|
1387
|
+
(name_match = line1.match(REGEXP[:section_name])) &&
|
1297
1388
|
# chomp so that a (non-visible) endline does not impact calculation
|
1298
1389
|
(line1.chomp.size - line2.chomp.size).abs <= 1
|
1299
1390
|
if anchor_match = name_match[1].match(REGEXP[:anchor_embedded])
|
@@ -1331,70 +1422,28 @@ class Lexer
|
|
1331
1422
|
process_attribute_entries(reader, document)
|
1332
1423
|
|
1333
1424
|
metadata = {}
|
1425
|
+
implicit_author = nil
|
1426
|
+
implicit_authors = nil
|
1334
1427
|
|
1335
1428
|
if reader.has_more_lines? && !reader.peek_line.chomp.empty?
|
1336
|
-
author_metadata =
|
1337
|
-
|
1338
|
-
|
1339
|
-
|
1340
|
-
|
1341
|
-
|
1342
|
-
|
1343
|
-
|
1344
|
-
|
1345
|
-
map[key.to_sym] = key
|
1346
|
-
end
|
1347
|
-
else
|
1348
|
-
keys.each do |key|
|
1349
|
-
map[key.to_sym] = "#{key}_#{idx + 1}"
|
1429
|
+
author_metadata = process_authors reader.get_line
|
1430
|
+
|
1431
|
+
unless author_metadata.empty?
|
1432
|
+
# apply header subs and assign to document
|
1433
|
+
if !document.nil?
|
1434
|
+
author_metadata.map do |key, val|
|
1435
|
+
val = val.is_a?(String) ? document.apply_header_subs(val) : val
|
1436
|
+
document.attributes[key] = val if !document.attributes.has_key?(key)
|
1437
|
+
val
|
1350
1438
|
end
|
1351
|
-
end
|
1352
1439
|
|
1353
|
-
|
1354
|
-
|
1355
|
-
author_metadata[map[:author]] = fname
|
1356
|
-
author_metadata[map[:authorinitials]] = fname[0, 1]
|
1357
|
-
if !match[2].nil? && !match[3].nil?
|
1358
|
-
author_metadata[map[:middlename]] = mname = match[2].tr('_', ' ')
|
1359
|
-
author_metadata[map[:lastname]] = lname = match[3].tr('_', ' ')
|
1360
|
-
author_metadata[map[:author]] = [fname, mname, lname].join ' '
|
1361
|
-
author_metadata[map[:authorinitials]] = [fname[0, 1], mname[0, 1], lname[0, 1]].join
|
1362
|
-
elsif !match[2].nil?
|
1363
|
-
author_metadata[map[:lastname]] = lname = match[2].tr('_', ' ')
|
1364
|
-
author_metadata[map[:author]] = [fname, lname].join ' '
|
1365
|
-
author_metadata[map[:authorinitials]] = [fname[0, 1], lname[0, 1]].join
|
1366
|
-
end
|
1367
|
-
author_metadata[map[:email]] = match[4] unless match[4].nil?
|
1368
|
-
else
|
1369
|
-
author_metadata[map[:author]] = author_metadata[map[:firstname]] = author_entry.strip.squeeze(' ')
|
1370
|
-
author_metadata[map[:authorinitials]] = author_metadata[map[:firstname]][0, 1]
|
1440
|
+
implicit_author = document.attributes['author']
|
1441
|
+
implicit_authors = document.attributes['authors']
|
1371
1442
|
end
|
1372
1443
|
|
1373
|
-
|
1374
|
-
# only assign the _1 attributes if there are multiple authors
|
1375
|
-
if idx == 1
|
1376
|
-
keys.each do |key|
|
1377
|
-
author_metadata["#{key}_1"] = author_metadata[key] if author_metadata.has_key? key
|
1378
|
-
end
|
1379
|
-
end
|
1380
|
-
if idx.zero?
|
1381
|
-
author_metadata['authors'] = author_metadata[map[:author]]
|
1382
|
-
else
|
1383
|
-
author_metadata['authors'] = "#{author_metadata['authors']}, #{author_metadata[map[:author]]}"
|
1384
|
-
end
|
1444
|
+
metadata = author_metadata
|
1385
1445
|
end
|
1386
1446
|
|
1387
|
-
# apply header subs and assign to document
|
1388
|
-
if !document.nil?
|
1389
|
-
author_metadata.map do |key, val|
|
1390
|
-
val = val.is_a?(String) ? document.apply_header_subs(val) : val
|
1391
|
-
document.attributes[key] = val if !document.attributes.has_key?(key)
|
1392
|
-
val
|
1393
|
-
end
|
1394
|
-
end
|
1395
|
-
|
1396
|
-
metadata = author_metadata.dup
|
1397
|
-
|
1398
1447
|
# NOTE this will discard any comment lines, but not skip blank lines
|
1399
1448
|
process_attribute_entries(reader, document)
|
1400
1449
|
|
@@ -1412,18 +1461,18 @@ class Lexer
|
|
1412
1461
|
end
|
1413
1462
|
end
|
1414
1463
|
|
1415
|
-
|
1416
|
-
|
1417
|
-
|
1418
|
-
|
1419
|
-
|
1420
|
-
|
1464
|
+
unless rev_metadata.empty?
|
1465
|
+
# apply header subs and assign to document
|
1466
|
+
if !document.nil?
|
1467
|
+
rev_metadata.map do |key, val|
|
1468
|
+
val = document.apply_header_subs(val)
|
1469
|
+
document.attributes[key] = val if !document.attributes.has_key?(key)
|
1470
|
+
val
|
1471
|
+
end
|
1421
1472
|
end
|
1422
|
-
end
|
1423
1473
|
|
1424
|
-
|
1425
|
-
|
1426
|
-
}
|
1474
|
+
metadata.update rev_metadata
|
1475
|
+
end
|
1427
1476
|
|
1428
1477
|
# NOTE this will discard any comment lines, but not skip blank lines
|
1429
1478
|
process_attribute_entries(reader, document)
|
@@ -1431,9 +1480,119 @@ class Lexer
|
|
1431
1480
|
reader.skip_blank_lines
|
1432
1481
|
end
|
1433
1482
|
|
1483
|
+
if !document.nil?
|
1484
|
+
# process author attribute entries that override (or stand in for) the implicit author line
|
1485
|
+
author_metadata = nil
|
1486
|
+
if document.attributes.has_key?('author') &&
|
1487
|
+
(author_line = document.attributes['author']) != implicit_author
|
1488
|
+
# do not allow multiple, process as names only
|
1489
|
+
author_metadata = process_authors author_line, true, false
|
1490
|
+
elsif document.attributes.has_key?('authors') &&
|
1491
|
+
(author_line = document.attributes['authors']) != implicit_authors
|
1492
|
+
# allow multiple, process as names only
|
1493
|
+
author_metadata = process_authors author_line, true
|
1494
|
+
else
|
1495
|
+
authors = []
|
1496
|
+
author_key = "author_#{authors.size + 1}"
|
1497
|
+
while document.attributes.has_key? author_key
|
1498
|
+
authors << document.attributes[author_key]
|
1499
|
+
author_key = "author_#{authors.size + 1}"
|
1500
|
+
end
|
1501
|
+
if authors.size == 1
|
1502
|
+
# do not allow multiple, process as names only
|
1503
|
+
author_metadata = process_authors authors.first, true, false
|
1504
|
+
elsif authors.size > 1
|
1505
|
+
# allow multiple, process as names only
|
1506
|
+
author_metadata = process_authors authors.join('; '), true
|
1507
|
+
end
|
1508
|
+
end
|
1509
|
+
|
1510
|
+
unless author_metadata.nil?
|
1511
|
+
document.attributes.update author_metadata
|
1512
|
+
|
1513
|
+
# special case
|
1514
|
+
if !document.attributes.has_key?('email') && document.attributes.has_key?('email_1')
|
1515
|
+
document.attributes['email'] = document.attributes['email_1']
|
1516
|
+
end
|
1517
|
+
end
|
1518
|
+
end
|
1519
|
+
|
1434
1520
|
metadata
|
1435
1521
|
end
|
1436
1522
|
|
1523
|
+
# Internal: Parse the author line into a Hash of author metadata
|
1524
|
+
#
|
1525
|
+
# author_line - the String author line
|
1526
|
+
# names_only - a Boolean flag that indicates whether to process line as
|
1527
|
+
# names only or names with emails (default: false)
|
1528
|
+
# multiple - a Boolean flag that indicates whether to process multiple
|
1529
|
+
# semicolon-separated entries in the author line (default: true)
|
1530
|
+
#
|
1531
|
+
# returns a Hash of author metadata
|
1532
|
+
def self.process_authors(author_line, names_only = false, multiple = true)
|
1533
|
+
author_metadata = {}
|
1534
|
+
keys = ['author', 'authorinitials', 'firstname', 'middlename', 'lastname', 'email']
|
1535
|
+
author_entries = multiple ? author_line.split(';').map(&:strip) : [author_line]
|
1536
|
+
author_entries.each_with_index do |author_entry, idx|
|
1537
|
+
author_entry.strip!
|
1538
|
+
next if author_entry.empty?
|
1539
|
+
key_map = {}
|
1540
|
+
if idx.zero?
|
1541
|
+
keys.each do |key|
|
1542
|
+
key_map[key.to_sym] = key
|
1543
|
+
end
|
1544
|
+
else
|
1545
|
+
keys.each do |key|
|
1546
|
+
key_map[key.to_sym] = "#{key}_#{idx + 1}"
|
1547
|
+
end
|
1548
|
+
end
|
1549
|
+
|
1550
|
+
segments = nil
|
1551
|
+
if names_only
|
1552
|
+
# splitting on ' ' will collapse repeating spaces
|
1553
|
+
segments = author_entry.split(' ', 3)
|
1554
|
+
elsif (match = author_entry.match(REGEXP[:author_info]))
|
1555
|
+
segments = match.to_a
|
1556
|
+
segments.shift
|
1557
|
+
end
|
1558
|
+
|
1559
|
+
unless segments.nil?
|
1560
|
+
author_metadata[key_map[:firstname]] = fname = segments[0].tr('_', ' ')
|
1561
|
+
author_metadata[key_map[:author]] = fname
|
1562
|
+
author_metadata[key_map[:authorinitials]] = fname[0, 1]
|
1563
|
+
if !segments[1].nil? && !segments[2].nil?
|
1564
|
+
author_metadata[key_map[:middlename]] = mname = segments[1].tr('_', ' ')
|
1565
|
+
author_metadata[key_map[:lastname]] = lname = segments[2].tr('_', ' ')
|
1566
|
+
author_metadata[key_map[:author]] = [fname, mname, lname].join ' '
|
1567
|
+
author_metadata[key_map[:authorinitials]] = [fname[0, 1], mname[0, 1], lname[0, 1]].join
|
1568
|
+
elsif !segments[1].nil?
|
1569
|
+
author_metadata[key_map[:lastname]] = lname = segments[1].tr('_', ' ')
|
1570
|
+
author_metadata[key_map[:author]] = [fname, lname].join ' '
|
1571
|
+
author_metadata[key_map[:authorinitials]] = [fname[0, 1], lname[0, 1]].join
|
1572
|
+
end
|
1573
|
+
author_metadata[key_map[:email]] = segments[3] unless names_only || segments[3].nil?
|
1574
|
+
else
|
1575
|
+
author_metadata[key_map[:author]] = author_metadata[key_map[:firstname]] = fname = author_entry.strip.squeeze(' ')
|
1576
|
+
author_metadata[key_map[:authorinitials]] = fname[0, 1]
|
1577
|
+
end
|
1578
|
+
|
1579
|
+
author_metadata['authorcount'] = idx + 1
|
1580
|
+
# only assign the _1 attributes if there are multiple authors
|
1581
|
+
if idx == 1
|
1582
|
+
keys.each do |key|
|
1583
|
+
author_metadata["#{key}_1"] = author_metadata[key] if author_metadata.has_key? key
|
1584
|
+
end
|
1585
|
+
end
|
1586
|
+
if idx.zero?
|
1587
|
+
author_metadata['authors'] = author_metadata[key_map[:author]]
|
1588
|
+
else
|
1589
|
+
author_metadata['authors'] = "#{author_metadata['authors']}, #{author_metadata[key_map[:author]]}"
|
1590
|
+
end
|
1591
|
+
end
|
1592
|
+
|
1593
|
+
author_metadata
|
1594
|
+
end
|
1595
|
+
|
1437
1596
|
# Internal: Parse lines of metadata until a line of metadata is not found.
|
1438
1597
|
#
|
1439
1598
|
# This method processes sequential lines containing block metadata, ignoring
|
@@ -1537,29 +1696,41 @@ class Lexer
|
|
1537
1696
|
end
|
1538
1697
|
end
|
1539
1698
|
|
1540
|
-
|
1541
|
-
# a nil value signals the attribute should be deleted (undefined)
|
1542
|
-
value = nil
|
1543
|
-
name = name.chop
|
1544
|
-
end
|
1545
|
-
|
1546
|
-
name = sanitize_attribute_name(name)
|
1547
|
-
accessible = true
|
1548
|
-
if !parent.nil?
|
1549
|
-
accessible = value.nil? ?
|
1550
|
-
parent.document.delete_attribute(name) :
|
1551
|
-
parent.document.set_attribute(name, value)
|
1552
|
-
end
|
1553
|
-
|
1554
|
-
if !attributes.nil?
|
1555
|
-
Document::AttributeEntry.new(name, value).save_to(attributes) if accessible
|
1556
|
-
end
|
1699
|
+
store_attribute(name, value, parent.nil? ? nil : parent.document, attributes)
|
1557
1700
|
true
|
1558
1701
|
else
|
1559
1702
|
false
|
1560
1703
|
end
|
1561
1704
|
end
|
1562
1705
|
|
1706
|
+
# Public: Store the attribute in the document and register attribute entry if accessible
|
1707
|
+
#
|
1708
|
+
# name - the String name of the attribute to store
|
1709
|
+
# value - the String value of the attribute to store
|
1710
|
+
# doc - the Document being parsed
|
1711
|
+
# attrs - the attributes for the current context
|
1712
|
+
#
|
1713
|
+
# returns a 2-element array containing the attribute name and value
|
1714
|
+
def self.store_attribute(name, value, doc = nil, attrs = nil)
|
1715
|
+
if name.end_with?('!')
|
1716
|
+
# a nil value signals the attribute should be deleted (undefined)
|
1717
|
+
value = nil
|
1718
|
+
name = name.chop
|
1719
|
+
end
|
1720
|
+
|
1721
|
+
name = sanitize_attribute_name(name)
|
1722
|
+
accessible = true
|
1723
|
+
unless doc.nil?
|
1724
|
+
accessible = value.nil? ? doc.delete_attribute(name) : doc.set_attribute(name, value)
|
1725
|
+
end
|
1726
|
+
|
1727
|
+
unless !accessible || attrs.nil?
|
1728
|
+
Document::AttributeEntry.new(name, value).save_to(attrs)
|
1729
|
+
end
|
1730
|
+
|
1731
|
+
[name, value]
|
1732
|
+
end
|
1733
|
+
|
1563
1734
|
# Internal: Resolve the 0-index marker for this list item
|
1564
1735
|
#
|
1565
1736
|
# For ordered lists, match the marker used for this list item against the
|
@@ -1742,9 +1913,9 @@ class Lexer
|
|
1742
1913
|
if parser_ctx.format == 'psv'
|
1743
1914
|
next_cell_spec, cell_text = parse_cell_spec(m.pre_match, :end)
|
1744
1915
|
parser_ctx.push_cell_spec next_cell_spec
|
1745
|
-
parser_ctx.buffer
|
1916
|
+
parser_ctx.buffer = %(#{parser_ctx.buffer}#{cell_text})
|
1746
1917
|
else
|
1747
|
-
parser_ctx.buffer
|
1918
|
+
parser_ctx.buffer = %(#{parser_ctx.buffer}#{m.pre_match})
|
1748
1919
|
end
|
1749
1920
|
|
1750
1921
|
line = m.post_match
|
@@ -1752,10 +1923,10 @@ class Lexer
|
|
1752
1923
|
else
|
1753
1924
|
# no other delimiters to see here
|
1754
1925
|
# suck up this line into the buffer and move on
|
1755
|
-
parser_ctx.buffer
|
1926
|
+
parser_ctx.buffer = %(#{parser_ctx.buffer}#{line})
|
1756
1927
|
# QUESTION make this an option? (unwrap-option?)
|
1757
1928
|
if parser_ctx.format == 'csv'
|
1758
|
-
parser_ctx.buffer.rstrip
|
1929
|
+
parser_ctx.buffer = %(#{parser_ctx.buffer.rstrip} )
|
1759
1930
|
end
|
1760
1931
|
line = ''
|
1761
1932
|
if parser_ctx.format == 'psv' || (parser_ctx.format == 'csv' &&
|
@@ -1891,6 +2062,143 @@ class Lexer
|
|
1891
2062
|
[spec, rest]
|
1892
2063
|
end
|
1893
2064
|
|
2065
|
+
# Public: Parse the first positional attribute and assign named attributes
|
2066
|
+
#
|
2067
|
+
# Parse the first positional attribute to extract the style, role and id
|
2068
|
+
# parts, assign the values to their cooresponding attribute keys and return
|
2069
|
+
# both the original style attribute and the parsed value from the first
|
2070
|
+
# positional attribute.
|
2071
|
+
#
|
2072
|
+
# attributes - The Hash of attributes to process
|
2073
|
+
#
|
2074
|
+
# Examples
|
2075
|
+
#
|
2076
|
+
# puts attributes
|
2077
|
+
# => {1 => "abstract#intro.lead", "style" => "preamble"}
|
2078
|
+
#
|
2079
|
+
# parse_style_attribute(attributes)
|
2080
|
+
# => ["abstract", "preamble"]
|
2081
|
+
#
|
2082
|
+
# puts attributes
|
2083
|
+
# => {1 => "abstract#intro.lead", "style" => "abstract", "id" => "intro", "role" => "lead"}
|
2084
|
+
#
|
2085
|
+
# Returns a two-element Array of the parsed style from the
|
2086
|
+
# first positional attribute and the original style that was
|
2087
|
+
# replaced
|
2088
|
+
def self.parse_style_attribute(attributes)
|
2089
|
+
original_style = attributes['style']
|
2090
|
+
raw_style = attributes[1]
|
2091
|
+
if !raw_style || raw_style.include?(' ')
|
2092
|
+
attributes['style'] = raw_style
|
2093
|
+
[raw_style, original_style]
|
2094
|
+
# FIXME this logic could be condensed
|
2095
|
+
else
|
2096
|
+
hash_index = raw_style.index('#')
|
2097
|
+
dot_index = raw_style.index('.')
|
2098
|
+
if !hash_index.nil? && (dot_index.nil? || hash_index < dot_index)
|
2099
|
+
parsed_style = attributes['style'] = (hash_index > 0 ? raw_style[0..(hash_index - 1)] : nil)
|
2100
|
+
id = raw_style[(hash_index + 1)..-1]
|
2101
|
+
if !dot_index.nil?
|
2102
|
+
id, roles = id.split('.', 2)
|
2103
|
+
attributes['id'] = id
|
2104
|
+
attributes['role'] = roles.tr('.', ' ')
|
2105
|
+
else
|
2106
|
+
attributes['id'] = id
|
2107
|
+
end
|
2108
|
+
elsif !dot_index.nil? && (hash_index.nil? || dot_index < hash_index)
|
2109
|
+
parsed_style = attributes['style'] = (dot_index > 0 ? raw_style[0..(dot_index - 1)] : nil)
|
2110
|
+
roles = raw_style[(dot_index + 1)..-1]
|
2111
|
+
if !hash_index.nil?
|
2112
|
+
roles, id = roles.split('#', 2)
|
2113
|
+
attributes['id'] = id
|
2114
|
+
attributes['role'] = roles.tr('.', ' ')
|
2115
|
+
else
|
2116
|
+
attributes['role'] = roles.tr('.', ' ')
|
2117
|
+
end
|
2118
|
+
else
|
2119
|
+
parsed_style = attributes['style'] = raw_style
|
2120
|
+
end
|
2121
|
+
|
2122
|
+
[parsed_style, original_style]
|
2123
|
+
end
|
2124
|
+
end
|
2125
|
+
|
2126
|
+
# Remove the indentation (block offset) shared by all the lines, then
|
2127
|
+
# indent the lines by the specified amount if specified
|
2128
|
+
#
|
2129
|
+
# Trim the leading whitespace (indentation) equivalent to the length
|
2130
|
+
# of the indent on the least indented line. If the indent argument
|
2131
|
+
# is specified, indent the lines by this many spaces (columns).
|
2132
|
+
#
|
2133
|
+
# The purpose of this method is to shift a block of text to
|
2134
|
+
# align to the left margin, while still preserving the relative
|
2135
|
+
# indentation between lines
|
2136
|
+
#
|
2137
|
+
# lines - the Array of String lines to process
|
2138
|
+
# indent - the integer number of spaces to add to the beginning
|
2139
|
+
# of each line; if this value is nil, the existing
|
2140
|
+
# space is preserved (optional, default: 0)
|
2141
|
+
#
|
2142
|
+
# Examples
|
2143
|
+
#
|
2144
|
+
# source = <<EOS
|
2145
|
+
# def names
|
2146
|
+
# @name.split ' ')
|
2147
|
+
# end
|
2148
|
+
# EOS
|
2149
|
+
#
|
2150
|
+
# source.lines.entries
|
2151
|
+
# # => [" def names\n", " @names.split ' '\n", " end\n"]
|
2152
|
+
#
|
2153
|
+
# Lexer.reset_block_indent(source.lines.entries)
|
2154
|
+
# # => ["def names\n", " @names.split ' '\n", "end\n"]
|
2155
|
+
#
|
2156
|
+
# puts Lexer.reset_block_indent(source.lines.entries).join
|
2157
|
+
# # => def names
|
2158
|
+
# # => @names.split ' '
|
2159
|
+
# # => end
|
2160
|
+
#
|
2161
|
+
# returns the Array of String lines with block offset removed
|
2162
|
+
def self.reset_block_indent!(lines, indent = 0)
|
2163
|
+
return if indent.nil? || lines.empty?
|
2164
|
+
|
2165
|
+
tab_detected = false
|
2166
|
+
# TODO make tab size configurable
|
2167
|
+
tab_expansion = ' '
|
2168
|
+
# strip leading block indent
|
2169
|
+
offsets = lines.map do |line|
|
2170
|
+
# break if the first char is non-whitespace
|
2171
|
+
break [] unless line.chomp[0..0].lstrip.empty?
|
2172
|
+
if line.include? "\t"
|
2173
|
+
tab_detected = true
|
2174
|
+
line = line.gsub("\t", tab_expansion)
|
2175
|
+
end
|
2176
|
+
if (flush_line = line.lstrip).empty?
|
2177
|
+
nil
|
2178
|
+
elsif (offset = line.length - flush_line.length) == 0
|
2179
|
+
break []
|
2180
|
+
else
|
2181
|
+
offset
|
2182
|
+
end
|
2183
|
+
end
|
2184
|
+
|
2185
|
+
unless offsets.empty? || (offsets = offsets.compact).empty?
|
2186
|
+
if (offset = offsets.min) > 0
|
2187
|
+
lines.map! {|line|
|
2188
|
+
line = line.gsub("\t", tab_expansion) if tab_detected
|
2189
|
+
line[offset..-1] || "\n"
|
2190
|
+
}
|
2191
|
+
end
|
2192
|
+
end
|
2193
|
+
|
2194
|
+
if indent > 0
|
2195
|
+
padding = ' ' * indent
|
2196
|
+
lines.map! {|line| %(#{padding}#{line}) }
|
2197
|
+
end
|
2198
|
+
|
2199
|
+
nil
|
2200
|
+
end
|
2201
|
+
|
1894
2202
|
# Public: Convert a string to a legal attribute name.
|
1895
2203
|
#
|
1896
2204
|
# name - the String name of the attribute
|