asciidoctor 0.0.7 → 0.0.9

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.

Files changed (47) hide show
  1. data/Gemfile +2 -0
  2. data/README.asciidoc +35 -26
  3. data/Rakefile +9 -6
  4. data/asciidoctor.gemspec +27 -8
  5. data/bin/asciidoctor +1 -1
  6. data/lib/asciidoctor.rb +351 -63
  7. data/lib/asciidoctor/abstract_block.rb +218 -0
  8. data/lib/asciidoctor/abstract_node.rb +249 -0
  9. data/lib/asciidoctor/attribute_list.rb +211 -0
  10. data/lib/asciidoctor/backends/base_template.rb +99 -0
  11. data/lib/asciidoctor/backends/docbook45.rb +510 -0
  12. data/lib/asciidoctor/backends/html5.rb +585 -0
  13. data/lib/asciidoctor/block.rb +27 -254
  14. data/lib/asciidoctor/callouts.rb +117 -0
  15. data/lib/asciidoctor/debug.rb +7 -4
  16. data/lib/asciidoctor/document.rb +229 -77
  17. data/lib/asciidoctor/inline.rb +29 -0
  18. data/lib/asciidoctor/lexer.rb +1330 -502
  19. data/lib/asciidoctor/list_item.rb +33 -34
  20. data/lib/asciidoctor/reader.rb +305 -142
  21. data/lib/asciidoctor/renderer.rb +115 -19
  22. data/lib/asciidoctor/section.rb +100 -189
  23. data/lib/asciidoctor/substituters.rb +468 -0
  24. data/lib/asciidoctor/table.rb +499 -0
  25. data/lib/asciidoctor/version.rb +1 -1
  26. data/test/attributes_test.rb +301 -87
  27. data/test/blocks_test.rb +568 -0
  28. data/test/document_test.rb +221 -24
  29. data/test/fixtures/dot.gif +0 -0
  30. data/test/fixtures/encoding.asciidoc +1 -0
  31. data/test/fixtures/include-file.asciidoc +1 -0
  32. data/test/fixtures/tip.gif +0 -0
  33. data/test/headers_test.rb +411 -43
  34. data/test/lexer_test.rb +265 -45
  35. data/test/links_test.rb +144 -3
  36. data/test/lists_test.rb +2252 -74
  37. data/test/paragraphs_test.rb +21 -30
  38. data/test/preamble_test.rb +24 -0
  39. data/test/reader_test.rb +248 -12
  40. data/test/renderer_test.rb +22 -0
  41. data/test/substitutions_test.rb +414 -0
  42. data/test/tables_test.rb +484 -0
  43. data/test/test_helper.rb +70 -6
  44. data/test/text_test.rb +30 -6
  45. metadata +64 -10
  46. data/lib/asciidoctor/render_templates.rb +0 -317
  47. data/lib/asciidoctor/string.rb +0 -12
@@ -0,0 +1,468 @@
1
+ # Public: Methods to perform substitutions on lines of AsciiDoc text. This module
2
+ # is intented to be mixed-in to Section and Block to provide operations for performing
3
+ # the necessary substitutions.
4
+ module Asciidoctor
5
+ module Substituters
6
+
7
+ COMPOSITE_SUBS = {
8
+ :none => [],
9
+ :normal => [:specialcharacters, :quotes, :attributes, :replacements, :macros, :post_replacements],
10
+ :verbatim => [:specialcharacters, :callouts]
11
+ }
12
+
13
+ SUB_OPTIONS = COMPOSITE_SUBS.keys + COMPOSITE_SUBS[:normal]
14
+
15
+ # Internal: A String Array of passthough (unprocessed) text captured from this block
16
+ attr_reader :passthroughs
17
+
18
+ # Public: Apply the specified substitutions to the lines of text
19
+ #
20
+ # lines - The lines of text to process. Can be a String or a String Array
21
+ # subs - The substitutions to perform. Can be a Symbol or a Symbol Array (default: COMPOSITE_SUBS[:normal])
22
+ #
23
+ # returns Either a String or String Array, whichever matches the type of the first argument
24
+ def apply_subs(lines, subs = COMPOSITE_SUBS[:normal])
25
+ if subs.nil?
26
+ subs = []
27
+ elsif subs.is_a? Symbol
28
+ subs = [subs]
29
+ end
30
+
31
+ if !subs.empty?
32
+ # QUESTION is this most efficient operation?
33
+ subs = subs.map {|key|
34
+ COMPOSITE_SUBS.has_key?(key) ? COMPOSITE_SUBS[key] : key
35
+ }.flatten
36
+ end
37
+
38
+ return lines if subs.empty?
39
+
40
+ multiline = lines.is_a?(Array)
41
+ text = multiline ? lines.join : lines
42
+
43
+ passthroughs = subs.include?(:macros)
44
+ text = extract_passthroughs(text) if passthroughs
45
+
46
+ subs.each {|type|
47
+ case type
48
+ when :specialcharacters
49
+ text = sub_specialcharacters(text)
50
+ when :quotes
51
+ text = sub_quotes(text)
52
+ when :attributes
53
+ text = sub_attributes(text.lines.entries).join
54
+ when :replacements
55
+ text = sub_replacements(text)
56
+ when :macros
57
+ text = sub_macros(text)
58
+ when :callouts
59
+ text = sub_callouts(text)
60
+ when :post_replacements
61
+ text = sub_post_replacements(text)
62
+ else
63
+ puts "asciidoctor: WARNING: unknown substitution type #{type}"
64
+ end
65
+ }
66
+ text = restore_passthroughs(text) if passthroughs
67
+
68
+ multiline ? text.lines.entries : text
69
+ end
70
+
71
+ # Public: Apply normal substitutions.
72
+ #
73
+ # lines - The lines of text to process. Can be a String or a String Array
74
+ #
75
+ # returns - A String with normal substitutions performed
76
+ def apply_normal_subs(lines)
77
+ apply_subs(lines.is_a?(Array) ? lines.join : lines)
78
+ end
79
+
80
+ # Public: Apply substitutions for titles.
81
+ #
82
+ # title - The String title to process
83
+ #
84
+ # returns - A String with title substitutions performed
85
+ def apply_title_subs(title)
86
+ apply_subs(title, [:specialcharacters, :quotes, :replacements, :macros, :attributes, :post_replacements])
87
+ end
88
+
89
+ # Public: Apply substitutions for titles
90
+ #
91
+ # lines - A String Array containing the lines of text process
92
+ #
93
+ # returns - A String with literal (verbatim) substitutions performed
94
+ def apply_literal_subs(lines)
95
+ if @document.attr('basebackend') == 'html' && attr('style') == 'source' &&
96
+ @document.attr('source-highlighter') == 'coderay' && attr?('language')
97
+ sub_callouts(highlight_source(lines.join))
98
+ else
99
+ apply_subs(lines.join, COMPOSITE_SUBS[:verbatim])
100
+ end
101
+ end
102
+
103
+ # Public: Apply substitutions for header metadata and attribute assignments
104
+ #
105
+ # text - String containing the text process
106
+ #
107
+ # returns - A String with header substitutions performed
108
+ def apply_header_subs(text)
109
+ apply_subs(text, [:specialcharacters, :attributes])
110
+ end
111
+
112
+ # Public: Apply substitutions for passthrough text
113
+ #
114
+ # lines - A String Array containing the lines of text process
115
+ #
116
+ # returns - A String Array with passthrough substitutions performed
117
+ def apply_passthrough_subs(lines)
118
+ if attr? 'subs'
119
+ subs = resolve_subs(attr('subs'))
120
+ else
121
+ subs = [:attributes, :macros]
122
+ end
123
+ apply_subs(lines.join, subs)
124
+ end
125
+
126
+ # Internal: Extract the passthrough text from the document for reinsertion after processing.
127
+ #
128
+ # text - The String from which to extract passthrough fragements
129
+ #
130
+ # returns - The text with the passthrough region substituted with placeholders
131
+ def extract_passthroughs(text)
132
+ result = text.dup
133
+
134
+ result.gsub!(REGEXP[:pass_macro]) {
135
+ # alias match for Ruby 1.8.7 compat
136
+ m = $~
137
+ # honor the escape
138
+ if m[0].start_with? '\\'
139
+ next m[0][1..-1]
140
+ end
141
+
142
+ if m[1] == '$$'
143
+ subs = [:specialcharacters]
144
+ elsif !m[3].nil? && !m[3].empty?
145
+ subs = resolve_subs(m[3])
146
+ else
147
+ subs = []
148
+ end
149
+
150
+ @passthroughs << {:text => m[2] || m[4].gsub('\]', ']'), :subs => subs}
151
+ index = @passthroughs.size - 1
152
+ "\x0#{index}\x0"
153
+ } unless !(result.include?('+++') || result.include?('$$') || result.include?('pass:'))
154
+
155
+ result.gsub!(REGEXP[:pass_lit]) {
156
+ # alias match for Ruby 1.8.7 compat
157
+ m = $~
158
+
159
+ # honor the escape
160
+ if m[2].start_with? '\\'
161
+ next "#{m[1]}#{m[2][1..-1]}"
162
+ end
163
+
164
+ @passthroughs << {:text => m[3], :subs => [:specialcharacters], :literal => true}
165
+ index = @passthroughs.size - 1
166
+ "#{m[1]}\x0#{index}\x0"
167
+ } unless !result.include?('`')
168
+
169
+ result
170
+ end
171
+
172
+ # Internal: Restore the passthrough text by reinserting into the placeholder positions
173
+ #
174
+ # text - The String text into which to restore the passthrough text
175
+ #
176
+ # returns The String text with the passthrough text restored
177
+ def restore_passthroughs(text)
178
+ return text if @passthroughs.nil? || @passthroughs.empty? || !text.include?("\x0")
179
+
180
+ text.gsub(REGEXP[:pass_placeholder]) {
181
+ pass = @passthroughs[$1.to_i];
182
+ text = apply_subs(pass[:text], pass.fetch(:subs, []))
183
+ pass[:literal] ? Inline.new(self, :quoted, text, :type => :monospaced).render : text
184
+ }
185
+ end
186
+
187
+ # Public: Substitute special characters (i.e., encode XML)
188
+ #
189
+ # Special characters are defined in the Asciidoctor::SPECIAL_CHARS Array constant
190
+ #
191
+ # text - The String text to process
192
+ #
193
+ # returns The String text with special characters replaced
194
+ def sub_specialcharacters(text)
195
+ # this syntax only available in Ruby 1.9
196
+ #text.gsub(SPECIAL_CHARS_PATTERN, SPECIAL_CHARS)
197
+
198
+ text.gsub(SPECIAL_CHARS_PATTERN) { SPECIAL_CHARS[$&] }
199
+ end
200
+
201
+ # Public: Substitute quoted text (includes emphasis, strong, monospaced, etc)
202
+ #
203
+ # text - The String text to process
204
+ #
205
+ # returns The String text with quoted text rendered using the backend templates
206
+ def sub_quotes(text)
207
+ result = text.dup
208
+
209
+ QUOTE_SUBS.each {|type, scope, pattern|
210
+ result.gsub!(pattern) { transform_quoted_text($~, type, scope) }
211
+ }
212
+
213
+ result
214
+ end
215
+
216
+ # Public: Substitute replacement characters (e.g., copyright, trademark, etc)
217
+ #
218
+ # text - The String text to process
219
+ #
220
+ # returns The String text with the replacement characters substituted
221
+ def sub_replacements(text)
222
+ result = text.dup
223
+
224
+ REPLACEMENTS.each {|pattern, replacement|
225
+ result.gsub!(pattern, replacement)
226
+ }
227
+
228
+ result
229
+ end
230
+
231
+ # Public: Substitute attribute references
232
+ #
233
+ # Attribute references are in the format {name}.
234
+ #
235
+ # If an attribute referenced in the line is missing, the line is dropped.
236
+ #
237
+ # text - The String text to process
238
+ #
239
+ # returns The String text with the attribute references replaced with attribute values
240
+ #--
241
+ # NOTE it's necessary to perform this substitution line-by-line
242
+ # so that a missing key doesn't wipe out the whole block of data
243
+ def sub_attributes(data)
244
+ return data if data.nil? || data.empty?
245
+
246
+ # normalizes data type to an array (string becomes single-element array)
247
+ lines = Array(data)
248
+
249
+ result = lines.map {|line|
250
+ reject = false
251
+ subject = line.dup
252
+ subject.gsub!(REGEXP[:attr_ref]) {
253
+ if !$1.empty? || !$3.empty?
254
+ "{#$2}"
255
+ elsif document.attributes.has_key? $2
256
+ document.attributes[$2]
257
+ elsif INTRINSICS.has_key? $2
258
+ INTRINSICS[$2]
259
+ else
260
+ Asciidoctor.debug { "Missing attribute: #$2, line marked for removal" }
261
+ reject = true
262
+ break '{undefined}'
263
+ end
264
+ } if subject.include?('{')
265
+
266
+ !reject ? subject : nil
267
+ }.compact
268
+
269
+ data.is_a?(String) ? result.join : result
270
+ end
271
+
272
+ # Public: Substitute inline macros (e.g., links, images, etc)
273
+ #
274
+ # Replace inline macros, which may span multiple lines, in the provided text
275
+ #
276
+ # text - The String text to process
277
+ #
278
+ # returns The String with the inline macros rendered using the backend templates
279
+ def sub_macros(text)
280
+ return text if text.nil? || text.empty?
281
+
282
+ result = text.dup
283
+
284
+ # inline images, image:target.ext[Alt]
285
+ result.gsub!(REGEXP[:image_macro]) {
286
+ # alias match for Ruby 1.8.7 compat
287
+ m = $~
288
+ # honor the escape
289
+ if m[0].start_with? '\\'
290
+ next m[0][1..-1]
291
+ end
292
+ target = sub_attributes(m[1])
293
+ @document.register(:images, target)
294
+ attrs = parse_attributes(m[2], ['alt', 'width', 'height'])
295
+ if !attrs.has_key?('alt') || attrs['alt'].empty?
296
+ attrs['alt'] = File.basename(target, File.extname(target))
297
+ end
298
+ Inline.new(self, :image, nil, :target => target, :attributes => attrs).render
299
+ } unless !result.include?('image:')
300
+
301
+ # inline urls, target[text] (optionally prefixed with link: and optionally surrounded by <>)
302
+ result.gsub!(REGEXP[:link_inline]) {
303
+ # alias match for Ruby 1.8.7 compat
304
+ m = $~
305
+ # honor the escape
306
+ if m[2].start_with? '\\'
307
+ next "#{m[1]}#{m[2][1..-1]}#{m[3]}"
308
+ # not a valid macro syntax w/o trailing square brackets
309
+ # we probably shouldn't even get here...our regex is doing too much
310
+ elsif m[1] == 'link:' && m[3].nil?
311
+ next m[0]
312
+ end
313
+ prefix = (m[1] != 'link:' ? m[1] : '')
314
+ target = m[2]
315
+ # strip the <> around the link
316
+ if prefix.end_with? '&lt;'
317
+ prefix = prefix[0..-5]
318
+ end
319
+ if target.end_with? '&gt;'
320
+ target = target[0..-5]
321
+ end
322
+ @document.register(:links, target)
323
+ text = !m[3].nil? ? sub_attributes(m[3].gsub('\]', ']')) : ''
324
+ "#{prefix}#{Inline.new(self, :anchor, (!text.empty? ? text : target), :type => :link, :target => target).render}"
325
+ } unless !result.include?('http')
326
+
327
+ # inline link macros, link:target[text]
328
+ result.gsub!(REGEXP[:link_macro]) {
329
+ # alias match for Ruby 1.8.7 compat
330
+ m = $~
331
+ # honor the escape
332
+ if m[0].start_with? '\\'
333
+ next m[0][1..-1]
334
+ end
335
+ target = m[1]
336
+ @document.register(:links, target)
337
+ text = sub_attributes(m[2].gsub('\]', ']'))
338
+ Inline.new(self, :anchor, (!text.empty? ? text : target), :type => :link, :target => target).render
339
+ } unless !result.include?('link:')
340
+
341
+ result.gsub!(REGEXP[:xref_macro]) {
342
+ # alias match for Ruby 1.8.7 compat
343
+ m = $~
344
+ # honor the escape
345
+ if m[0].start_with? '\\'
346
+ next m[0][1..-1]
347
+ end
348
+ if !m[1].nil?
349
+ id, reftext = m[1].split(',', 2)
350
+ id.sub!(/^("|)(.*)\1$/, '\2')
351
+ reftext.sub!(/^("|)(.*)\1$/m, '\2') unless reftext.nil?
352
+ else
353
+ id = m[2]
354
+ reftext = !m[3].empty? ? m[3] : nil
355
+ end
356
+ Inline.new(self, :anchor, reftext, :type => :xref, :target => id).render
357
+ }
358
+
359
+ result.gsub!(REGEXP[:anchor_macro]) {
360
+ # alias match for Ruby 1.8.7 compat
361
+ m = $~
362
+ # honor the escape
363
+ if m[0].start_with? '\\'
364
+ next m[0][1..-1]
365
+ end
366
+ id, reftext = m[1].split(',')
367
+ id.sub!(/^("|)(.*)\1$/, '\2')
368
+ if reftext.nil?
369
+ reftext = "[#{id}]"
370
+ else
371
+ reftext.sub!(/^("|)(.*)\1$/m, '\2')
372
+ end
373
+ # NOTE the reftext should also match what's in our references dic
374
+ if !@document.references[:ids].has_key? id
375
+ Asciidoctor.debug { "Missing reference for anchor #{id}" }
376
+ end
377
+ Inline.new(self, :anchor, reftext, :type => :ref, :target => id).render
378
+ } unless !result.include?('[[')
379
+
380
+ result
381
+ end
382
+
383
+ # Public: Substitute callout references
384
+ #
385
+ # text - The String text to process
386
+ #
387
+ # returns The String with the callout references rendered using the backend templates
388
+ def sub_callouts(text)
389
+ text.gsub(REGEXP[:callout_render]) {
390
+ # alias match for Ruby 1.8.7 compat
391
+ m = $~
392
+ # honor the escape
393
+ if m[0].start_with? '\\'
394
+ next "&lt;#{m[1]}&gt;"
395
+ end
396
+ Inline.new(self, :callout, m[1], :id => document.callouts.read_next_id).render
397
+ }
398
+ end
399
+
400
+ # Public: Substitute post replacements
401
+ #
402
+ # text - The String text to process
403
+ #
404
+ # returns The String with the post replacements rendered using the backend templates
405
+ def sub_post_replacements(text)
406
+ text.gsub(REGEXP[:line_break]) { Inline.new(self, :break, $1, :type => :line).render }
407
+ end
408
+
409
+ # Internal: Transform (render) a quoted text region
410
+ #
411
+ # match - The MatchData for the quoted text region
412
+ # type - The quoting type (single, double, strong, emphasis, monospaced, etc)
413
+ # scope - The scope of the quoting (constrained or unconstrained)
414
+ #
415
+ # returns The rendered text for the quoted text region
416
+ def transform_quoted_text(match, type, scope)
417
+ if match[0].start_with? '\\'
418
+ match[0][1..-1]
419
+ elsif scope == :constrained
420
+ "#{match[1]}#{Inline.new(self, :quoted, match[3], :type => type, :attributes => parse_attributes(match[2])).render}"
421
+ else
422
+ Inline.new(self, :quoted, match[2], :type => type, :attributes => parse_attributes(match[1])).render
423
+ end
424
+ end
425
+
426
+ # Internal: Parse the attributes in the attribute line
427
+ #
428
+ # attrline - A String of unprocessed attributes (key/value pairs)
429
+ # posattrs - The keys for positional attributes
430
+ #
431
+ # returns nil if attrline is nil, an empty Hash if attrline is empty, otherwise a Hash of parsed attributes
432
+ def parse_attributes(attrline, posattrs = ['role'])
433
+ return nil if attrline.nil?
434
+ return {} if attrline.empty?
435
+
436
+ AttributeList.new(attrline, self).parse(posattrs)
437
+ end
438
+
439
+ # Internal: Resolve the list of comma-delimited subs against the possible options.
440
+ #
441
+ # subs - A comma-delimited String of substitution aliases
442
+ #
443
+ # returns An Array of Symbols representing the substitution operation
444
+ def resolve_subs(subs)
445
+ candidates = subs.split(',').map {|sub| sub.strip.to_sym}
446
+ resolved = candidates & SUB_OPTIONS
447
+ if (invalid = candidates - resolved).size > 0
448
+ puts "asciidoctor: WARNING: invalid passthrough macro substitution operation#{invalid.size > 1 ? 's' : ''}: #{invalid * ', '}"
449
+ end
450
+ resolved
451
+ end
452
+
453
+ # Public: Highlight the source code if a source highlighter is defined
454
+ # on the document, otherwise return the text unprocessed
455
+ #
456
+ # source - the source code String to highlight
457
+ #
458
+ # returns the highlighted source code, if a source highlighter is defined
459
+ # on the document, otherwise the unprocessed text
460
+ def highlight_source(source)
461
+ Asciidoctor.require_library 'coderay'
462
+ ::CodeRay::Duo[attr('language', 'text').to_sym, :html, {
463
+ :css => @document.attr('coderay-css', 'class').to_sym,
464
+ :line_numbers => (attr?('linenums') ? @document.attr('coderay-linenums-mode', 'table').to_sym : nil),
465
+ :line_number_anchors => false}].highlight(source).chomp
466
+ end
467
+ end
468
+ end