asciidoctor 0.1.4 → 1.5.0

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 (101) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.adoc +209 -25
  3. data/{LICENSE → LICENSE.adoc} +4 -3
  4. data/README.adoc +392 -395
  5. data/Rakefile +94 -137
  6. data/benchmark/benchmark.rb +127 -0
  7. data/benchmark/sample-data/mdbasics.adoc +334 -0
  8. data/bin/asciidoctor +5 -8
  9. data/bin/asciidoctor-safe +4 -8
  10. data/compat/asciidoc.conf +78 -11
  11. data/compat/font-awesome-3-compat.css +397 -0
  12. data/data/stylesheets/asciidoctor-default.css +399 -0
  13. data/data/stylesheets/coderay-asciidoctor.css +89 -0
  14. data/features/open_block.feature +92 -0
  15. data/features/pass_block.feature +66 -0
  16. data/features/step_definitions.rb +42 -0
  17. data/features/text_formatting.feature +55 -0
  18. data/features/xref.feature +116 -0
  19. data/lib/asciidoctor.rb +1155 -605
  20. data/lib/asciidoctor/abstract_block.rb +157 -71
  21. data/lib/asciidoctor/abstract_node.rb +150 -93
  22. data/lib/asciidoctor/attribute_list.rb +85 -90
  23. data/lib/asciidoctor/block.rb +51 -24
  24. data/lib/asciidoctor/callouts.rb +4 -7
  25. data/lib/asciidoctor/cli.rb +3 -0
  26. data/lib/asciidoctor/cli/invoker.rb +86 -76
  27. data/lib/asciidoctor/cli/options.rb +111 -61
  28. data/lib/asciidoctor/converter.rb +232 -0
  29. data/lib/asciidoctor/converter/base.rb +58 -0
  30. data/lib/asciidoctor/converter/composite.rb +66 -0
  31. data/lib/asciidoctor/converter/docbook45.rb +94 -0
  32. data/lib/asciidoctor/converter/docbook5.rb +684 -0
  33. data/lib/asciidoctor/converter/factory.rb +225 -0
  34. data/lib/asciidoctor/converter/html5.rb +1081 -0
  35. data/lib/asciidoctor/converter/template.rb +296 -0
  36. data/lib/asciidoctor/core_ext.rb +7 -0
  37. data/lib/asciidoctor/core_ext/object/nil_or_empty.rb +23 -0
  38. data/lib/asciidoctor/core_ext/string/chr.rb +6 -0
  39. data/lib/asciidoctor/core_ext/symbol/length.rb +6 -0
  40. data/lib/asciidoctor/document.rb +590 -304
  41. data/lib/asciidoctor/extensions.rb +1100 -308
  42. data/lib/asciidoctor/helpers.rb +109 -46
  43. data/lib/asciidoctor/inline.rb +16 -9
  44. data/lib/asciidoctor/list.rb +23 -15
  45. data/lib/asciidoctor/opal_ext.rb +4 -0
  46. data/lib/asciidoctor/opal_ext/comparable.rb +38 -0
  47. data/lib/asciidoctor/opal_ext/dir.rb +13 -0
  48. data/lib/asciidoctor/opal_ext/error.rb +2 -0
  49. data/lib/asciidoctor/opal_ext/file.rb +125 -0
  50. data/lib/asciidoctor/{lexer.rb → parser.rb} +646 -455
  51. data/lib/asciidoctor/path_resolver.rb +141 -77
  52. data/lib/asciidoctor/reader.rb +257 -187
  53. data/lib/asciidoctor/section.rb +12 -16
  54. data/lib/asciidoctor/stylesheets.rb +91 -0
  55. data/lib/asciidoctor/substitutors.rb +1548 -0
  56. data/lib/asciidoctor/table.rb +73 -57
  57. data/lib/asciidoctor/timings.rb +39 -0
  58. data/lib/asciidoctor/version.rb +1 -1
  59. data/man/asciidoctor.1 +22 -14
  60. data/man/asciidoctor.adoc +18 -10
  61. data/test/attributes_test.rb +314 -14
  62. data/test/blocks_test.rb +763 -118
  63. data/test/converter_test.rb +352 -0
  64. data/test/document_test.rb +518 -199
  65. data/test/extensions_test.rb +273 -103
  66. data/test/fixtures/asciidoc_index.txt +27 -13
  67. data/test/fixtures/basic-docinfo.xml +1 -1
  68. data/test/fixtures/chapter-a.adoc +3 -0
  69. data/test/fixtures/custom-backends/erb/html5/block_paragraph.html.erb +6 -0
  70. data/test/fixtures/docinfo.xml +1 -1
  71. data/test/fixtures/include-file.asciidoc +2 -0
  72. data/test/fixtures/master.adoc +5 -0
  73. data/test/invoker_test.rb +173 -61
  74. data/test/links_test.rb +97 -21
  75. data/test/lists_test.rb +181 -22
  76. data/test/options_test.rb +86 -2
  77. data/test/paragraphs_test.rb +47 -5
  78. data/test/{lexer_test.rb → parser_test.rb} +128 -57
  79. data/test/paths_test.rb +36 -1
  80. data/test/preamble_test.rb +25 -17
  81. data/test/reader_test.rb +404 -249
  82. data/test/sections_test.rb +623 -58
  83. data/test/substitutions_test.rb +609 -132
  84. data/test/tables_test.rb +198 -24
  85. data/test/test_helper.rb +101 -31
  86. data/test/text_test.rb +88 -31
  87. metadata +160 -64
  88. data/Gemfile +0 -12
  89. data/Guardfile +0 -18
  90. data/asciidoctor.gemspec +0 -143
  91. data/lib/asciidoctor/backends/_stylesheets.rb +0 -466
  92. data/lib/asciidoctor/backends/base_template.rb +0 -114
  93. data/lib/asciidoctor/backends/docbook45.rb +0 -774
  94. data/lib/asciidoctor/backends/docbook5.rb +0 -103
  95. data/lib/asciidoctor/backends/html5.rb +0 -1214
  96. data/lib/asciidoctor/renderer.rb +0 -259
  97. data/lib/asciidoctor/substituters.rb +0 -1083
  98. data/test/fixtures/asciidoc.txt +0 -105
  99. data/test/fixtures/ascshort.txt +0 -32
  100. data/test/fixtures/list_elements.asciidoc +0 -10
  101. data/test/renderer_test.rb +0 -162
@@ -1,47 +1,124 @@
1
1
  module Asciidoctor
2
2
  module Helpers
3
- # Internal: Prior to invoking Kernel#require, issues a warning urging a
4
- # manual require if running in a threaded environment.
3
+ # Internal: Require the specified library using Kernel#require.
4
+ #
5
+ # Attempts to load the library specified in the first argument using the
6
+ # Kernel#require. Rescues the LoadError if the library is not available and
7
+ # passes a message to Kernel#fail to communicate to the user that processing
8
+ # is being aborted. If a gem_name is specified, the failure message
9
+ # communicates that a required gem is not installed.
5
10
  #
6
11
  # name - the String name of the library to require.
12
+ # gem - a Boolean that indicates whether this library is provided by a RubyGem,
13
+ # or the String name of the RubyGem if it differs from the library name
14
+ # (default: true)
7
15
  #
8
- # returns false if the library is detected on the load path or the return
9
- # value of delegating to Kernel#require
10
- def self.require_library(name, gem_name = nil)
11
- if Thread.list.size > 1
12
- main_script = "#{name}.rb"
13
- main_script_path_segment = "/#{name}.rb"
14
- if !$LOADED_FEATURES.detect {|p| p == main_script || p.end_with?(main_script_path_segment) }.nil?
15
- return false
16
- else
17
- warn "WARN: asciidoctor is autoloading '#{name}' in threaded environment. " +
18
- "The use of an explicit require '#{name}' statement is recommended."
16
+ # returns the return value of Kernel#require if the library is available,
17
+ # otherwise Kernel#fail is called with an appropriate message.
18
+ def self.require_library name, gem = true
19
+ require name
20
+ rescue ::LoadError => e
21
+ if gem
22
+ fail %(asciidoctor: FAILED: required gem '#{gem == true ? name : gem}' is not installed. Processing aborted.)
23
+ else
24
+ fail %(asciidoctor: FAILED: #{e.message.chomp '.'}. Processing aborted.)
25
+ end
26
+ end
27
+
28
+ # Public: Normalize the data to prepare for parsing
29
+ #
30
+ # Delegates to Helpers#normalize_lines_from_string if data is a String.
31
+ # Delegates to Helpers#normalize_lines_array if data is a String Array.
32
+ #
33
+ # returns a String Array of normalized lines
34
+ def self.normalize_lines data
35
+ data.class == ::String ? (normalize_lines_from_string data) : (normalize_lines_array data)
36
+ end
37
+
38
+ # Public: Normalize the array of lines to prepare them for parsing
39
+ #
40
+ # Force encodes the data to UTF-8 and removes trailing whitespace from each line.
41
+ #
42
+ # If a BOM is present at the beginning of the data, a best attempt
43
+ # is made to encode from the specified encoding to UTF-8.
44
+ #
45
+ # data - a String Array of lines to normalize
46
+ #
47
+ # returns a String Array of normalized lines
48
+ def self.normalize_lines_array data
49
+ return [] if data.empty?
50
+
51
+ # NOTE if data encoding is UTF-*, we only need 0..1
52
+ leading_bytes = (first_line = data[0])[0..2].bytes.to_a
53
+ if COERCE_ENCODING
54
+ utf8 = ::Encoding::UTF_8
55
+ if (leading_2_bytes = leading_bytes[0..1]) == BOM_BYTES_UTF_16LE
56
+ # Ruby messes up trailing whitespace on UTF-16LE, so take a different route
57
+ return ((data.join.force_encoding ::Encoding::UTF_16LE)[1..-1].encode utf8).lines.map {|line| line.rstrip }
58
+ elsif leading_2_bytes == BOM_BYTES_UTF_16BE
59
+ data[0] = (first_line.force_encoding ::Encoding::UTF_16BE)[1..-1]
60
+ return data.map {|line| "#{((line.force_encoding ::Encoding::UTF_16BE).encode utf8).rstrip}" }
61
+ elsif leading_bytes[0..2] == BOM_BYTES_UTF_8
62
+ data[0] = (first_line.force_encoding utf8)[1..-1]
63
+ end
64
+
65
+ data.map {|line| line.encoding == utf8 ? line.rstrip : (line.force_encoding utf8).rstrip }
66
+ else
67
+ # Ruby 1.8 has no built-in re-encoding, so no point in removing the UTF-16 BOMs
68
+ if leading_bytes == BOM_BYTES_UTF_8
69
+ data[0] = first_line[3..-1]
19
70
  end
71
+ data.map {|line| line.rstrip }
20
72
  end
21
- begin
22
- require name
23
- rescue LoadError => e
24
- if gem_name
25
- fail "asciidoctor: FAILED: required gem '#{gem_name === true ? name : gem_name}' is not installed. Processing aborted."
73
+ end
74
+
75
+ # Public: Normalize the String and split into lines to prepare them for parsing
76
+ #
77
+ # Force encodes the data to UTF-8 and removes trailing whitespace from each line.
78
+ # Converts the data to a String Array.
79
+ #
80
+ # If a BOM is present at the beginning of the data, a best attempt
81
+ # is made to encode from the specified encoding to UTF-8.
82
+ #
83
+ # data - a String of lines to normalize
84
+ #
85
+ # returns a String Array of normalized lines
86
+ def self.normalize_lines_from_string data
87
+ return [] if data.nil_or_empty?
88
+
89
+ if COERCE_ENCODING
90
+ utf8 = ::Encoding::UTF_8
91
+ # NOTE if data encoding is UTF-*, we only need 0..1
92
+ leading_bytes = data[0..2].bytes.to_a
93
+ if (leading_2_bytes = leading_bytes[0..1]) == BOM_BYTES_UTF_16LE
94
+ data = (data.force_encoding ::Encoding::UTF_16LE)[1..-1].encode utf8
95
+ elsif leading_2_bytes == BOM_BYTES_UTF_16BE
96
+ data = (data.force_encoding ::Encoding::UTF_16BE)[1..-1].encode utf8
97
+ elsif leading_bytes[0..2] == BOM_BYTES_UTF_8
98
+ data = data.encoding == utf8 ? data[1..-1] : (data.force_encoding utf8)[1..-1]
26
99
  else
27
- fail "asciidoctor: FAILED: #{e.chomp '.'}. Processing aborted."
100
+ data = data.force_encoding utf8 unless data.encoding == utf8
101
+ end
102
+ else
103
+ # Ruby 1.8 has no built-in re-encoding, so no point in removing the UTF-16 BOMs
104
+ if data[0..2].bytes.to_a == BOM_BYTES_UTF_8
105
+ data = data[3..-1]
28
106
  end
29
107
  end
108
+ data.each_line.map {|line| line.rstrip }
30
109
  end
31
110
 
111
+ # Matches the characters in a URI to encode
112
+ REGEXP_ENCODE_URI_CHARS = /[^\w\-.!~*';:@=+$,()\[\]]/
113
+
32
114
  # Public: Encode a string for inclusion in a URI
33
115
  #
34
116
  # str - the string to encode
35
117
  #
36
118
  # returns an encoded version of the str
37
119
  def self.encode_uri(str)
38
- str.gsub(REGEXP[:uri_encode_chars]) do
39
- match = $&
40
- buf = ''
41
- match.each_byte do |c|
42
- buf << sprintf('%%%02X', c)
43
- end
44
- buf
120
+ str.gsub(REGEXP_ENCODE_URI_CHARS) do
121
+ $&.each_byte.map {|c| sprintf '%%%02X', c}.join
45
122
  end
46
123
  end
47
124
 
@@ -56,32 +133,18 @@ module Helpers
56
133
  #
57
134
  # Returns the String filename with the file extension removed
58
135
  def self.rootname(file_name)
59
- ext = File.extname(file_name)
60
- if ext.empty?
61
- file_name
62
- else
63
- file_name[0...-ext.length]
64
- end
136
+ # alternatively, this could be written as ::File.basename file_name, ((::File.extname file_name) || '')
137
+ (ext = ::File.extname(file_name)).empty? ? file_name : file_name[0...-ext.length]
65
138
  end
66
139
 
67
140
  def self.mkdir_p(dir)
68
- unless File.directory? dir
69
- parent_dir = File.dirname(dir)
70
- if !File.directory?(parent_dir = File.dirname(dir)) && parent_dir != '.'
141
+ unless ::File.directory? dir
142
+ parent_dir = ::File.dirname(dir)
143
+ if !::File.directory?(parent_dir = ::File.dirname(dir)) && parent_dir != '.'
71
144
  mkdir_p(parent_dir)
72
145
  end
73
- Dir.mkdir(dir)
74
- end
75
- end
76
-
77
- # Public: Create a copy of options such that no references are shared
78
- # returns A deep clone of the options Hash
79
- def self.clone_options(opts)
80
- clone = opts.dup
81
- if opts.has_key? :attributes
82
- clone[:attributes] = opts[:attributes].dup
146
+ ::Dir.mkdir(dir)
83
147
  end
84
- clone
85
148
  end
86
149
  end
87
150
  end
@@ -1,9 +1,6 @@
1
1
  module Asciidoctor
2
2
  # Public: Methods for managing inline elements in AsciiDoc block
3
3
  class Inline < AbstractNode
4
- # Public: Get/Set the String name of the render template
5
- attr_accessor :template_name
6
-
7
4
  # Public: Get the text of this inline element
8
5
  attr_reader :text
9
6
 
@@ -15,22 +12,32 @@ class Inline < AbstractNode
15
12
 
16
13
  def initialize(parent, context, text = nil, opts = {})
17
14
  super(parent, context)
18
- @template_name = "inline_#{context}"
15
+ @node_name = %(inline_#{context})
19
16
 
20
17
  @text = text
21
18
 
22
19
  @id = opts[:id]
23
20
  @type = opts[:type]
24
21
  @target = opts[:target]
25
-
26
- if opts.has_key?(:attributes) && (attributes = opts[:attributes]).is_a?(Hash)
27
- update_attributes(opts[:attributes]) unless attributes.empty?
22
+
23
+ unless (more_attributes = opts[:attributes]).nil_or_empty?
24
+ update_attributes more_attributes
28
25
  end
29
26
  end
30
27
 
31
- def render
32
- renderer.render(@template_name, self).chomp
28
+ def block?
29
+ false
33
30
  end
34
31
 
32
+ def inline?
33
+ true
34
+ end
35
+
36
+ def convert
37
+ converter.convert self
38
+ end
39
+
40
+ # Alias render to convert to maintain backwards compatibility
41
+ alias :render :convert
35
42
  end
36
43
  end
@@ -6,8 +6,8 @@ class List < AbstractBlock
6
6
  alias :items :blocks
7
7
  alias :items? :blocks?
8
8
 
9
- def initialize(parent, context)
10
- super(parent, context)
9
+ def initialize parent, context
10
+ super
11
11
  end
12
12
 
13
13
  # Public: Get the items in this list as an Array
@@ -15,10 +15,21 @@ class List < AbstractBlock
15
15
  @blocks
16
16
  end
17
17
 
18
- def render
19
- result = super
20
- @document.callouts.next_list if @context == :colist
21
- result
18
+ def convert
19
+ if @context == :colist
20
+ result = super
21
+ @document.callouts.next_list
22
+ result
23
+ else
24
+ super
25
+ end
26
+ end
27
+
28
+ # Alias render to convert to maintain backwards compatibility
29
+ alias :render :convert
30
+
31
+ def to_s
32
+ %(#<#{self.class}@#{object_id} {context: #{@context.inspect}, style: #{@style.inspect}, items: #{items.size}}>)
22
33
  end
23
34
 
24
35
  end
@@ -33,14 +44,14 @@ class ListItem < AbstractBlock
33
44
  #
34
45
  # parent - The parent list block for this list item
35
46
  # text - the String text (default nil)
36
- def initialize(parent, text = nil)
37
- super(parent, :list_item)
47
+ def initialize parent, text = nil
48
+ super parent, :list_item
38
49
  @text = text
39
50
  @level = parent.level
40
51
  end
41
52
 
42
53
  def text?
43
- !@text.to_s.empty?
54
+ !@text.nil_or_empty?
44
55
  end
45
56
 
46
57
  def text
@@ -59,23 +70,20 @@ class ListItem < AbstractBlock
59
70
  #
60
71
  # Returns nothing
61
72
  def fold_first(continuation_connects_first_block = false, content_adjacent = false)
62
- if !(first_block = @blocks.first).nil? && first_block.is_a?(Block) &&
73
+ if (first_block = @blocks[0]) && first_block.is_a?(Block) &&
63
74
  ((first_block.context == :paragraph && !continuation_connects_first_block) ||
64
75
  ((content_adjacent || !continuation_connects_first_block) && first_block.context == :literal &&
65
76
  first_block.option?('listparagraph')))
66
77
 
67
78
  block = blocks.shift
68
- unless @text.to_s.empty?
69
- block.lines.unshift("#@text\n")
70
- end
71
-
79
+ block.lines.unshift @text unless @text.nil_or_empty?
72
80
  @text = block.source
73
81
  end
74
82
  nil
75
83
  end
76
84
 
77
85
  def to_s
78
- "#@context [text:#@text, blocks:#{(@blocks || []).size}]"
86
+ %(#<#{self.class}@#{object_id} {list_context: #{parent.context.inspect}, text: #{@text.inspect}, blocks: #{(@blocks || []).size}}>)
79
87
  end
80
88
 
81
89
  end
@@ -0,0 +1,4 @@
1
+ require 'asciidoctor/opal_ext/comparable'
2
+ require 'asciidoctor/opal_ext/dir'
3
+ require 'asciidoctor/opal_ext/error'
4
+ require 'asciidoctor/opal_ext/file'
@@ -0,0 +1,38 @@
1
+ # workaround for an infinite loop in Opal 0.6.2 when comparing numbers
2
+ module Comparable
3
+ def == other
4
+ return true if equal? other
5
+ return false unless cmp = (self <=> other)
6
+ return `cmp == 0`
7
+ rescue StandardError
8
+ false
9
+ end
10
+
11
+ def > other
12
+ unless cmp = (self <=> other)
13
+ raise ArgumentError, "comparison of #{self.class} with #{other.class} failed"
14
+ end
15
+ `cmp > 0`
16
+ end
17
+
18
+ def >= other
19
+ unless cmp = (self <=> other)
20
+ raise ArgumentError, "comparison of #{self.class} with #{other.class} failed"
21
+ end
22
+ `cmp >= 0`
23
+ end
24
+
25
+ def < other
26
+ unless cmp = (self <=> other)
27
+ raise ArgumentError, "comparison of #{self.class} with #{other.class} failed"
28
+ end
29
+ `cmp < 0`
30
+ end
31
+
32
+ def <= other
33
+ unless cmp = (self <=> other)
34
+ raise ArgumentError, "comparison of #{self.class} with #{other.class} failed"
35
+ end
36
+ `cmp <= 0`
37
+ end
38
+ end
@@ -0,0 +1,13 @@
1
+ class Dir
2
+ def self.pwd
3
+ ENV['PWD'] || '.'
4
+ end
5
+
6
+ def self.getwd
7
+ ENV['PWD'] || '.'
8
+ end
9
+
10
+ def self.home
11
+ ENV['HOME']
12
+ end
13
+ end
@@ -0,0 +1,2 @@
1
+ class SecurityError < Exception
2
+ end
@@ -0,0 +1,125 @@
1
+ class Kernel
2
+ # basic implementation of open, enough to work
3
+ # with reading files over XmlHttpRequest
4
+ def open(path, *rest)
5
+ file = File.new(path, *rest)
6
+ if block_given?
7
+ yield file
8
+ else
9
+ file
10
+ end
11
+ end
12
+ end
13
+
14
+ class File
15
+ SEPARATOR = '/'
16
+ ALT_SEPARATOR = nil
17
+
18
+ attr_reader :eof
19
+ attr_reader :lineno
20
+ attr_reader :path
21
+
22
+ def initialize(path, mode = 'r')
23
+ @path = path
24
+ @contents = nil
25
+ @eof = false
26
+ @lineno = 0
27
+ end
28
+
29
+ def read
30
+ if @eof
31
+ ''
32
+ else
33
+ res = File.read(@path)
34
+ @eof = true
35
+ @lineno = res.size
36
+ res
37
+ end
38
+ end
39
+
40
+ def each_line(separator = $/, &block)
41
+ if @eof
42
+ return block_given? ? self : [].to_enum
43
+ end
44
+
45
+ if block_given?
46
+ lines = File.read(@path)
47
+ %x{
48
+ self.eof = false;
49
+ self.lineno = 0;
50
+ var chomped = #{lines.chomp},
51
+ trailing = lines.length != chomped.length,
52
+ splitted = chomped.split(separator);
53
+
54
+ for (var i = 0, length = splitted.length; i < length; i++) {
55
+ self.lineno += 1;
56
+ if (i < length - 1 || trailing) {
57
+ #{yield `splitted[i] + separator`};
58
+ }
59
+ else {
60
+ #{yield `splitted[i]`};
61
+ }
62
+ }
63
+ self.eof = true;
64
+ }
65
+ self
66
+ else
67
+ read.each_line
68
+ end
69
+ end
70
+
71
+ def self.expand_path(path)
72
+ path
73
+ end
74
+
75
+ def self.join(*paths)
76
+ paths * SEPARATOR
77
+ end
78
+
79
+ def self.basename(path)
80
+ (offset = path.rindex SEPARATOR) ? path[(offset + 1)..-1] : path
81
+ end
82
+
83
+ def self.dirname(path)
84
+ (offset = path.rindex SEPARATOR) ? path[0..(offset - 1)] : '.'
85
+ end
86
+
87
+ def self.extname(path)
88
+ return '' if path.nil_or_empty?
89
+ last_dot_idx = path[1..-1].rindex('.')
90
+ last_dot_idx.nil? ? '' : path[(last_dot_idx + 1)..-1]
91
+ end
92
+
93
+ # TODO use XMLHttpRequest HEAD request unless in local file mode
94
+ def self.file?(path)
95
+ true
96
+ end
97
+
98
+ def self.read(path)
99
+ %x{
100
+ var data = ''
101
+ var status = -1;
102
+ try {
103
+ var xhr = new XMLHttpRequest();
104
+ xhr.open('GET', path, false);
105
+ xhr.addEventListener('load', function() {
106
+ status = this.status;
107
+ // status is 0 for local file mode (i.e., file://)
108
+ if (status == 0 || status == 200) {
109
+ data = this.responseText;
110
+ }
111
+ });
112
+ xhr.overrideMimeType('text/plain');
113
+ xhr.send();
114
+ }
115
+ catch (e) {
116
+ status = 0;
117
+ }
118
+ // assume that no data in local file mode means it doesn't exist
119
+ if (status == 404 || (status == 0 && data == '')) {
120
+ throw #{IOError.new `'No such file or directory: ' + path`};
121
+ }
122
+ }
123
+ `data`
124
+ end
125
+ end