archieml 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 38f795c262135209607462aaf552bad7daec3bc0
4
+ data.tar.gz: 44fe6ad8971f1232b65dfb34caded74fd947f8bb
5
+ SHA512:
6
+ metadata.gz: e3895fd7157bca39da81dab19a4998b31a9ac6f6aa3b4c5e792fe6b9e68f3a22a41b6b8745ec94dddc77db48fccc35a081652bbbcf822ac6ad1321c9749b2390
7
+ data.tar.gz: d0a2d979cb5cd1600f5e82b851e38c3fb39adee415641d757fad44ab8a38ce04cbb883cab26c6e5b290c6b2f74297cd159009c3cad79f827612bb79c22e64a61
@@ -0,0 +1,3 @@
1
+ Gemfile.lock
2
+ .ruby-version
3
+ .ruby-gemset
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "rspec", group: :development
4
+ gem "rspec-mocks", group: :development
data/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright (c) 2015 The New York Times Company
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this library except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
@@ -0,0 +1,172 @@
1
+ # Archieml
2
+
3
+ Parse Archie Markup Language (ArchieML) documents into Ruby Hashes.
4
+
5
+ Read about the ArchieML specification at [archieml.org](http://archieml.org).
6
+
7
+ The current version is `v0.1.0`.
8
+
9
+ ## Installation
10
+
11
+ `gem install archieml`
12
+
13
+ ## Usage
14
+
15
+ ```
16
+ require 'archieml'
17
+
18
+ Archieml.load("key: value")
19
+ => {"key"=>"value"}
20
+
21
+ File.write("text.aml", "key: value")
22
+ Archieml.load_file("text.aml")
23
+ => {"key"=>"value"}
24
+ ```
25
+
26
+ ### Using with Google Documents
27
+
28
+ We use `archieml` at The New York Times to parse Google Documents containing AML. This requires a little upfront work to download the document and convert it into text that `archieml` can load.
29
+
30
+ The first step is authenticating with the Google Drive API, and accessing the document. For this, you will need a user account that is authorized to view the document you wish to download.
31
+
32
+ For this example, I'm going to use the official `google-api-client` Ruby gem, but you can use another library or authentication method if you like. Whatever mechanism, you'll need to be able to export the document either as text, or html, at which point the instructions will be identical.
33
+
34
+ The full example is at [`examples/google_drive.rb`](https://github.com/newsdev/archieml-ruby/blob/master/examples/google_drive.rb).
35
+
36
+ First, install the gem directly, or using a Gemfile:
37
+
38
+ ```
39
+ $ gem install google-api-client
40
+ ```
41
+
42
+ Next, open up `irb` and run the follow code to authorize a user, and initialize and OAuth client. Note that if you want to use this on a server, you'll have to set up a more re-usable way of authorizing users.
43
+
44
+ ```
45
+ require 'google/api_client'
46
+ require 'google/api_client/client_secrets'
47
+ require 'google/api_client/auth/installed_app'
48
+
49
+ client = Google::APIClient.new(:application_name => 'Ruby Drive sample', :application_version => '1.0.0')
50
+ client_secrets = Google::APIClient::ClientSecrets.load
51
+ flow = Google::APIClient::InstalledAppFlow.new(
52
+ :client_id => client_secrets.client_id,
53
+ :client_secret => client_secrets.client_secret,
54
+ :scope => ['https://www.googleapis.com/auth/drive']
55
+ )
56
+ client.authorization = flow.authorize
57
+ ```
58
+
59
+ Log into your Google account and authorize the application to access your Google Drive files.
60
+
61
+ Now that you have an authenticated `client`, you can make an API call to a document saved in Drive. Create a document with some basic AML inside (such as "key: value"), save it, and note the long string of characters at the end of the URL:
62
+
63
+ `https://docs.google.com/a/nytimes.com/document/d/[FILE_ID]/edit`
64
+
65
+ FILE_ID is defaulted to a public test file.
66
+
67
+ ```
68
+ FILE_ID = "1JjYD90DyoaBuRYNxa4_nqrHKkgZf1HrUj30i3rTWX1s"
69
+ drive = client.discovered_api('drive', 'v2')
70
+
71
+ result = client.execute(
72
+ :api_method => drive.files.get,
73
+ :parameters => { 'fileId' => FILE_ID })
74
+ ```
75
+
76
+ If result executes correctly, you should now have the file's metadata. The next step is to download the body of the file. The metadata has a property called `exportLinks` which gives you URLs to different formats that you can export the document as. Let's start with `text/plain`.
77
+
78
+ ```
79
+ text_url = result.data['exportLinks']['text/plain']
80
+ text_aml = client.execute(uri: text_url).body
81
+ ```
82
+
83
+ `text_aml` should now contain your document in plain text! You're all set to run the text through the ArchieML parser.
84
+
85
+ ```
86
+ require 'archieml'
87
+ parsed = Archieml.load(text_aml)
88
+ ```
89
+
90
+ Check out parsed, and ensure that it has any data you entered into the document.
91
+
92
+ There are a few extra steps that we do to make working with Google Documents more useful. With a little more prep, we generally process the documents to:
93
+
94
+ * Include links that users enter in the google document as HTML `<a>` tags
95
+ * Remove smart quotes inside tag brackets `<>` (which Google loves to add for you)
96
+ * Ensure that list bullet points are turned into `*`s
97
+
98
+ Unfortunately, google strips out links when you export as `text/plain`, so if you want to preserve them, we have to export the document in a different format, `text/html`.
99
+
100
+ ```
101
+ html_url = result.data['exportLinks']['text/html']
102
+ html_data = client.execute(uri: html_url).body
103
+ ```
104
+
105
+ At the other extreme, `html_data` now contains far too *much* data - there's a whole DOM represented in that text! We want to turn that HTML body back into plain text so that ArchieML can load it, and we want to preserve any links that we find.
106
+
107
+ This is a lightweight DOM traverser which requires using the `nokogiri` gem: `gem install nokogiri`. It moves through the HTML document and constructs a simple text representation of the document, without things like images or tables that would be ignored by AML anyway.
108
+
109
+ ```
110
+ require 'nokogiri'
111
+
112
+ def convert(node)
113
+ str = ''
114
+ node.children.each do |child|
115
+ if func = @node_types[child.name || child.type]
116
+ str += func.call(child)
117
+ end
118
+ end
119
+ return str
120
+ end
121
+
122
+ @node_types = {
123
+ 'text' => lambda { |node| return node.content },
124
+ 'span' => lambda { |node| convert(node) },
125
+ 'p' => lambda { |node| return convert(node) + "\n" },
126
+ 'li' => lambda { |node| return '* ' + convert(node) + "\n" },
127
+ 'a' => lambda { |node|
128
+ return convert(node) unless node.attributes['href'] && node.attributes['href'].value
129
+
130
+ # Google changes all links to be served from a google domain.
131
+ # We need to strip off the real url, which has been moved to the
132
+ # "q" querystring parameter.
133
+
134
+ href = node.attributes['href'].value
135
+ if !href.index('?').nil? && parsed_url = CGI.parse(href.split('?')[1])
136
+ href = parsed_url['q'][0] if parsed_url['q']
137
+ end
138
+
139
+ str = "<a href=\"#{href}\">"
140
+ str += convert(node)
141
+ str += "</a>"
142
+ return str
143
+ }
144
+ }
145
+
146
+ %w(ul ol).each { |tag| @node_types[tag] = @node_types['span'] }
147
+ %w(h1 h2 h3 h4 h5 h6 br hr).each { |tag| @node_types[tag] = @node_types['p'] }
148
+
149
+ html_doc = Nokogiri::HTML(html_data)
150
+ html_aml = convert(html_doc.children[1].children[1])
151
+
152
+ require 'archieml'
153
+ aml = Archieml.load(html_aml)
154
+ ```
155
+
156
+ `aml` should now have your document with links included, and bullet points should continue to work (we transformed each `<li>` element into a separate line beginning with a `*`).
157
+
158
+ One additional step we perform is removing smart quotes. You can run `html_aml` through this before calling `Archieml.load`:
159
+
160
+ ```
161
+ html_aml.gsub!(/<[^<>]*>/) do |match|
162
+ match.gsub("‘", "'")
163
+ .gsub("’", "'")
164
+ .gsub("“", '"')
165
+ .gsub("”", '"')
166
+ end
167
+ aml = Archieml.load(html_aml)
168
+ ```
169
+
170
+ ## Changelog
171
+
172
+ * `0.1.0` - Initial release supporting the first version of the ArchieML spec, published [2015-03-06](http://archieml.org/spec/1.0/CR-20150306.html).
@@ -0,0 +1,15 @@
1
+ require File.join(File.dirname(__FILE__), "lib", "archieml", "version")
2
+
3
+ Gem::Specification.new do |gem|
4
+ gem.name = "archieml"
5
+ gem.version = Archieml::VERSION
6
+ gem.authors = ["Michael Strickland"]
7
+ gem.email = ["michael.strickland@nytimes.com"]
8
+ gem.description = %q{Parse Archie Markup Language documents}
9
+ gem.summary = %q{Archieml is a Ruby parser for the Archie Markup Language (ArchieML)}
10
+ gem.homepage = "http://archieml.org"
11
+ gem.license = "Apache License 2.0"
12
+ gem.files = `git ls-files`.split($\)
13
+ gem.test_files = gem.files.grep(%r{^spec/})
14
+ gem.require_paths = ["lib"]
15
+ end
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Adapted from example code Copyright 2012 by Google Inc.,
4
+ # Licensed under the Apache License, Version 2.0.
5
+ # https://github.com/google/google-api-ruby-client-samples/blob/master/drive/drive.rb
6
+
7
+ FILE_ID = "1JjYD90DyoaBuRYNxa4_nqrHKkgZf1HrUj30i3rTWX1s"
8
+
9
+ require 'archieml'
10
+ require 'google/api_client'
11
+ require 'google/api_client/client_secrets'
12
+ require 'google/api_client/auth/file_storage'
13
+ require 'google/api_client/auth/installed_app'
14
+ require 'nokogiri'
15
+ require 'pp'
16
+
17
+ client = Google::APIClient.new(:application_name => 'Ruby Drive sample', :application_version => '1.0.0')
18
+
19
+ CREDENTIAL_STORE_FILE = "oauth2.json"
20
+
21
+ # FileStorage stores auth credentials in a file, so they survive multiple runs
22
+ # of the application. This avoids prompting the user for authorization every
23
+ # time the access token expires, by remembering the refresh token.
24
+ # Note: FileStorage is not suitable for multi-user applications.
25
+ file_storage = Google::APIClient::FileStorage.new(CREDENTIAL_STORE_FILE)
26
+ if file_storage.authorization.nil?
27
+ client_secrets = Google::APIClient::ClientSecrets.load
28
+ flow = Google::APIClient::InstalledAppFlow.new(
29
+ :client_id => client_secrets.client_id,
30
+ :client_secret => client_secrets.client_secret,
31
+ :scope => ['https://www.googleapis.com/auth/drive']
32
+ )
33
+ client.authorization = flow.authorize(file_storage)
34
+ else
35
+ client.authorization = file_storage.authorization
36
+ end
37
+
38
+ drive = client.discovered_api('drive', 'v2')
39
+ result = client.execute(
40
+ :api_method => drive.files.get,
41
+ :parameters => { 'fileId' => FILE_ID })
42
+
43
+ # Text version
44
+
45
+ text_url = result.data['exportLinks']['text/plain']
46
+ text_aml = client.execute(uri: text_url).body
47
+ parsed = Archieml.load(text_aml)
48
+
49
+ # HTML version
50
+
51
+ html_url = result.data['exportLinks']['text/html']
52
+ html_data = client.execute(uri: html_url).body
53
+
54
+ def convert(node)
55
+ str = ''
56
+ node.children.each do |child|
57
+ if func = @node_types[child.name || child.type]
58
+ str += func.call(child)
59
+ end
60
+ end
61
+ return str
62
+ end
63
+
64
+ @node_types = {
65
+ 'text' => lambda { |node| return node.content },
66
+ 'span' => lambda { |node| convert(node) },
67
+ 'p' => lambda { |node| return convert(node) + "\n" },
68
+ 'li' => lambda { |node| return '* ' + convert(node) + "\n" },
69
+ 'a' => lambda { |node|
70
+ return convert(node) unless node.attributes['href'] && node.attributes['href'].value
71
+
72
+ # Google changes all links to be served from a google domain.
73
+ # We need to strip off the real url, which has been moved to the
74
+ # "q" querystring parameter.
75
+
76
+ href = node.attributes['href'].value
77
+ if !href.index('?').nil? && parsed_url = CGI.parse(href.split('?')[1])
78
+ href = parsed_url['q'][0] if parsed_url['q']
79
+ end
80
+
81
+ str = "<a href=\"#{href}\">"
82
+ str += convert(node)
83
+ str += "</a>"
84
+ return str
85
+ }
86
+ }
87
+
88
+ %w(ul ol).each { |tag| @node_types[tag] = @node_types['span'] }
89
+ %w(h1 h2 h3 h4 h5 h6 br hr).each { |tag| @node_types[tag] = @node_types['p'] }
90
+
91
+ html_doc = Nokogiri::HTML(html_data)
92
+ html_aml = convert(html_doc.children[1].children[1])
93
+
94
+ html_aml.gsub!(/<[^<>]*>/) do |match|
95
+ match.gsub("‘", "'")
96
+ .gsub("’", "'")
97
+ .gsub("“", '"')
98
+ .gsub("”", '"')
99
+ end
100
+
101
+ aml = Archieml.load(html_aml)
102
+
103
+ pp aml
@@ -0,0 +1,14 @@
1
+ require 'archieml/loader'
2
+
3
+ module Archieml
4
+ def self.load(aml)
5
+ loader = Archieml::Loader.new()
6
+ loader.load(aml)
7
+ end
8
+
9
+ def self.load_file(filename)
10
+ loader = Archieml::Loader.new()
11
+ stream = File.open(filename)
12
+ loader.load(stream)
13
+ end
14
+ end
@@ -0,0 +1,189 @@
1
+ module Archieml
2
+ class Loader
3
+
4
+ NEXT_LINE = /.*((\r|\n)+)/
5
+ START_KEY = /^\s*([A-Za-z0-9\-_\.]+)[ \t\r]*:[ \t\r]*(.*(?:\n|\r|$))/
6
+ COMMAND_KEY = /^\s*:[ \t\r]*(endskip|ignore|skip|end)/i
7
+ ARRAY_ELEMENT = /^\s*\*[ \t\r]*(.*(?:\n|\r|$))/
8
+ SCOPE_PATTERN = /^\s*(\[|\{)[ \t\r]*([A-Za-z0-9\-_\.]*)[ \t\r]*(?:\]|\})[ \t\r]*.*?(\n|\r|$)/
9
+
10
+ def initialize
11
+ @data = @scope = {}
12
+
13
+ @buffer_scope = @buffer_key = nil
14
+ @buffer_string = ''
15
+
16
+ @is_skipping = false
17
+ @done_parsing = false
18
+
19
+ self.flush_scope!
20
+ end
21
+
22
+ def load(stream)
23
+ stream.each_line do |line|
24
+ return @data if @done_parsing
25
+
26
+ if match = line.match(COMMAND_KEY)
27
+ self.parse_command_key(match[1].downcase)
28
+
29
+ elsif !@is_skipping && (match = line.match(START_KEY)) && (!@array || @array_type != 'simple')
30
+ self.parse_start_key(match[1], match[2] || '')
31
+
32
+ elsif !@is_skipping && (match = line.match(ARRAY_ELEMENT)) && @array && @array_type != 'complex'
33
+ self.parse_array_element(match[1])
34
+
35
+ elsif !@is_skipping && match = line.match(SCOPE_PATTERN)
36
+ self.parse_scope(match[1], match[2])
37
+
38
+ else
39
+ @buffer_string += line
40
+ end
41
+ end
42
+
43
+ self.flush_buffer!
44
+ return @data
45
+ end
46
+
47
+ def parse_start_key(key, rest_of_line)
48
+ self.flush_buffer!
49
+
50
+ if @array
51
+ @array_type ||= 'complex'
52
+
53
+ # Ignore complex keys inside simple arrays
54
+ return if @array_type == 'simple'
55
+
56
+ if [nil, key].include?(@array_first_key)
57
+ @array << (@scope = {})
58
+ end
59
+
60
+ @array_first_key ||= key
61
+ end
62
+
63
+ @buffer_key = key
64
+ @buffer_string = rest_of_line
65
+
66
+ self.flush_buffer_into(key, replace: true)
67
+ end
68
+
69
+ def parse_array_element(value)
70
+ self.flush_buffer!
71
+
72
+ @array_type ||= 'simple'
73
+
74
+ # Ignore simple array elements inside complex arrays
75
+ return if @array_type == 'complex'
76
+
77
+ @array << ''
78
+ @buffer_key = @array
79
+ @buffer_string = value
80
+ self.flush_buffer_into(@array, replace: true)
81
+ end
82
+
83
+ def parse_command_key(command)
84
+ if @is_skipping && !%w(endskip ignore).include?(command)
85
+ return self.flush_buffer!
86
+ end
87
+
88
+ case command
89
+ when "end"
90
+ self.flush_buffer_into(@buffer_key, replace: false) if @buffer_key
91
+ return
92
+
93
+ when "ignore"
94
+ return @done_parsing = true
95
+
96
+ when "skip"
97
+ @is_skipping = true
98
+
99
+ when "endskip"
100
+ @is_skipping = false
101
+ end
102
+
103
+ self.flush_buffer!
104
+ end
105
+
106
+ def parse_scope(scope_type, scope_key)
107
+ self.flush_buffer!
108
+ self.flush_scope!
109
+
110
+ if scope_key == ''
111
+ @scope = @data
112
+
113
+ elsif %w([ {).include?(scope_type)
114
+ key_scope = @data
115
+ key_bits = scope_key.split('.')
116
+ key_bits[0...-1].each do |bit|
117
+ key_scope = key_scope[bit] ||= {}
118
+ end
119
+
120
+ if scope_type == '['
121
+ @array = key_scope[key_bits.last] ||= []
122
+
123
+ if @array.length > 0
124
+ @array_type = @array.first.class == String ? 'simple' : 'complex'
125
+ end
126
+
127
+ elsif scope_type == '{'
128
+ @scope = key_scope[key_bits.last] ||= {}
129
+ end
130
+ end
131
+ end
132
+
133
+ def flush_buffer!
134
+ result = @buffer_string.dup
135
+ @buffer_string = ''
136
+ return result
137
+ end
138
+
139
+ def flush_buffer_into(key, options = {})
140
+ value = self.flush_buffer!
141
+
142
+ if options[:replace]
143
+ value = self.format_value(value, :replace).sub(/^\s*/, '')
144
+ @buffer_string = value.match(/\s*\Z/)[0]
145
+ else
146
+ value = self.format_value(value, :append)
147
+ end
148
+
149
+ if key.class == Array
150
+ key[key.length - 1] = '' if options[:replace]
151
+ key[key.length - 1] += value.sub(/\s*\Z/, '')
152
+
153
+ else
154
+ key_bits = key.split('.')
155
+ @buffer_scope = @scope
156
+
157
+ key_bits[0...-1].each do |bit|
158
+ @buffer_scope[bit] = {} if @buffer_scope[bit].class == String # reset
159
+ @buffer_scope = @buffer_scope[bit] ||= {}
160
+ end
161
+
162
+ @buffer_scope[key_bits.last] = '' if options[:replace]
163
+ @buffer_scope[key_bits.last] += value.sub(/\s*\Z/, '')
164
+ end
165
+ end
166
+
167
+ def flush_scope!
168
+ @array = @array_type = @array_first_key = nil
169
+ end
170
+
171
+ # type can be either :replace or :append.
172
+ # If it's :replace, then the string is assumed to be the first line of a
173
+ # value, and no escaping takes place.
174
+ # If we're appending to a multi-line string, escape special punctuation
175
+ # by prepending the line with a backslash.
176
+ # (:, [, {, *, \) surrounding the first token of any line.
177
+ def format_value(value, type)
178
+ value.gsub!(/(?:^\\)?\[[^\[\]\n\r]*\](?!\])/, '') # remove comments
179
+ value.gsub!(/\[\[([^\[\]\n\r]*)\]\]/, '[\1]') # [[]] => []
180
+
181
+ if type == :append
182
+ value.gsub!(/^(\s*)\\/, '\1')
183
+ end
184
+
185
+ value
186
+ end
187
+
188
+ end
189
+ end
@@ -0,0 +1,3 @@
1
+ module Archieml
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,522 @@
1
+ require 'spec_helper'
2
+
3
+ describe Archieml::Loader do
4
+ before(:each) do
5
+ @loader = Archieml::Loader.new()
6
+ allow(@loader).to receive(:parse_scope).with(any_args).and_call_original
7
+ allow(@loader).to receive(:parse_start_key).with(any_args).and_call_original
8
+ allow(@loader).to receive(:parse_command_key).with(any_args).and_call_original
9
+ end
10
+
11
+ describe "parsing values" do
12
+ it "parses key value pairs" do
13
+ @loader.load("key:value")['key'].should == 'value'
14
+ end
15
+ it "ignores spaces on either side of the key" do
16
+ @loader.load(" key :value")['key'].should == 'value'
17
+ end
18
+ it "ignores tabs on either side of the key" do
19
+ @loader.load("\t\tkey\t\t:value")['key'].should == 'value'
20
+ end
21
+ it "ignores spaces on either side of the value" do
22
+ @loader.load("key: value ")['key'].should == 'value'
23
+ end
24
+ it "ignores tabs on either side of the value" do
25
+ @loader.load("key:\t\tvalue\t\t")['key'].should == 'value'
26
+ end
27
+ it "dupliate keys are assigned the last given value" do
28
+ @loader.load("key:value\nkey:newvalue")['key'].should == 'newvalue'
29
+ end
30
+ it "allows non-letter characters at the start of values" do
31
+ @loader.load("key::value")['key'].should == ':value'
32
+ end
33
+ it "keys are case sensitive" do
34
+ @loader.load("key:value\nKey:Value").keys.should == ['key', 'Key']
35
+ end
36
+ it "non-keys don't affect parsing" do
37
+ @loader.load("other stuff\nkey:value\nother stuff")['key'].should == 'value'
38
+ end
39
+ end
40
+
41
+ describe "valid keys" do
42
+
43
+ it "letters, numbers, dashes and underscores are valid key components" do
44
+ @loader.load("a-_1:value")['a-_1'].should == 'value'
45
+ end
46
+ it "spaces are not allowed in keys" do
47
+ @loader.load("k ey:value").keys.length.should == 0
48
+ end
49
+ it "symbols are not allowed in keys" do
50
+ @loader.load("k&ey:value").keys.length.should == 0
51
+ end
52
+ it "keys can be nested using dot-notation" do
53
+ @loader.load("scope.key:value")['scope']['key'].should == 'value'
54
+ end
55
+ it "earlier keys within scopes aren't deleted when using dot-notation" do
56
+ @loader.load("scope.key:value\nscope.otherkey:value")['scope']['key'].should == 'value'
57
+ @loader.load("scope.key:value\nscope.otherkey:value")['scope']['otherkey'].should == 'value'
58
+ end
59
+ it "the value of key that used to be a parent object should be replaced with a string if necessary" do
60
+ @loader.load("scope.level:value\nscope.level.level:value")['scope']['level']['level'].should == 'value'
61
+ end
62
+ it "the value of key that used to be a string object should be replaced with an object if necessary" do
63
+ @loader.load("scope.level.level:value\nscope.level:value")['scope']['level'].should == 'value'
64
+ end
65
+
66
+ end
67
+
68
+ describe "valid values" do
69
+
70
+ it "HTML is allowed" do
71
+ @loader.load("key:<strong>value</strong>")['key'].should == '<strong>value</strong>'
72
+ end
73
+
74
+ end
75
+
76
+ describe "skip" do
77
+
78
+ it "ignores spaces on either side of :skip" do
79
+ expect(@loader).to receive(:parse_command_key).with('skip').once
80
+ @loader.load(" :skip \nkey:value\n:endskip").keys.length.should == 0
81
+ end
82
+ it "ignores tabs on either side of :skip" do
83
+ expect(@loader).to receive(:parse_command_key).with('skip').once
84
+ @loader.load("\t\t:skip\t\t\nkey:value\n:endskip").keys.length.should == 0
85
+ end
86
+ it "ignores spaces on either side of :endskip" do
87
+ expect(@loader).to receive(:parse_command_key).with('endskip').once
88
+ @loader.load(":skip\nkey:value\n :endskip ").keys.length.should == 0
89
+ end
90
+ it "ignores tabs on either side of :endskip" do
91
+ expect(@loader).to receive(:parse_command_key).with('endskip').once
92
+ @loader.load(":skip\nkey:value\n\t\t:endskip\t\t").keys.length.should == 0
93
+ end
94
+ it "starts parsing again after :endskip" do
95
+ expect(@loader).to receive(:parse_start_key).with('key', 'value').once
96
+ @loader.load(":skip\n:endskip\nkey:value").keys.length.should == 1
97
+ end
98
+ it ":skip and :endskip are case insensitive" do
99
+ expect(@loader).to receive(:parse_command_key).with('skip').once
100
+ expect(@loader).to receive(:parse_command_key).with('endskip').once
101
+ @loader.load(":sKiP\nkey:value\n:eNdSkIp").keys.length.should == 0
102
+ end
103
+ it "parse :skip as a special command even if more is appended to word" do
104
+ expect(@loader).to receive(:parse_command_key).with('skip')
105
+ @loader.load(":skipthis\nkey:value\n:endskip").keys.length.should == 0
106
+ end
107
+ it "ignores all content on line after :skip + space" do
108
+ expect(@loader).to receive(:parse_command_key).with('skip').once
109
+ expect(@loader).to_not receive(:parse_start_key).with('key', 'value')
110
+ @loader.load(":skip this text \nkey:value\n:endskip").keys.length.should == 0
111
+ end
112
+ it "ignores all content on line after :skip + tab" do
113
+ expect(@loader).to receive(:parse_command_key).with('skip').once
114
+ expect(@loader).to_not receive(:parse_start_key).with('key', 'value')
115
+ @loader.load(":skip\tthis text\t\t\nkey:value\n:endskip").keys.length.should == 0
116
+ end
117
+ it "parse :endskip as a special command even if more is appended to word" do
118
+ expect(@loader).to receive(:parse_command_key).with('endskip')
119
+ @loader.load(":skip\n:endskiptheabove\nkey:value").keys.length.should == 1
120
+ end
121
+ it "ignores all content on line after :endskip + space" do
122
+ expect(@loader).to receive(:parse_command_key).with('endskip').once
123
+ expect(@loader).to receive(:parse_start_key).with('key', 'value').once
124
+ @loader.load(":skip\n:endskip the above\nkey:value").keys.length.should == 1
125
+ end
126
+ it "ignores all content on line after :endskip + tab" do
127
+ expect(@loader).to receive(:parse_command_key).with('endskip').once
128
+ expect(@loader).to receive(:parse_start_key).with('key', 'value').once
129
+ @loader.load(":skip\n:endskip\tthe above\nkey:value").keys.length.should == 1
130
+ end
131
+ it "does not parse :end as an :endskip" do
132
+ expect(@loader).to_not receive(:parse_command_key).with('endskip')
133
+ @loader.load(":skip\n:end\tthe above\nkey:value").keys.length.should == 0
134
+ end
135
+ it "ignores keys within a skip block" do
136
+ expect(@loader).to_not receive(:parse_start_key).with('other', 'value')
137
+ @loader.load("key1:value1\n:skip\nother:value\n\n:endskip\n\nkey2:value2").keys.should == ['key1', 'key2']
138
+ end
139
+
140
+ end
141
+
142
+ describe "ignore" do
143
+
144
+ it "text before ':ignore' should be included" do
145
+ @loader.load("key:value\n:ignore")['key'].should == 'value'
146
+ end
147
+ it "text after ':ignore' should be ignored" do
148
+ expect(@loader).to_not receive(:parse_start_key)
149
+ @loader.load(":ignore\nkey:value").keys.length.should == 0
150
+ end
151
+ it "':ignore' is case insensitive" do
152
+ expect(@loader).to receive(:parse_command_key).with('ignore').once
153
+ @loader.load(":iGnOrE\nkey:value").keys.length.should == 0
154
+ end
155
+ it "ignores spaces on either side of :ignore" do
156
+ expect(@loader).to receive(:parse_command_key).with('ignore').once
157
+ @loader.load(":iGnOrE\nkey:value").keys.length.should == 0
158
+ @loader.load(" :ignore \nkey:value")
159
+ end
160
+ it "ignores tabs on either side of :ignore" do
161
+ expect(@loader).to receive(:parse_command_key).with('ignore').once
162
+ @loader.load(":iGnOrE\nkey:value").keys.length.should == 0
163
+ @loader.load("\t\t:ignore\t\t\nkey:value")
164
+ end
165
+ it "parses :ignore as a special command even if more is appended to word" do
166
+ expect(@loader).to receive(:parse_command_key).with('ignore')
167
+ @loader.load(":ignorethis\nkey:value").keys.length.should == 0
168
+ end
169
+ it "ignores all content on line after :ignore + space" do
170
+ expect(@loader).to receive(:parse_command_key).with('ignore').once
171
+ @loader.load(":iGnOrE\nkey:value").keys.length.should == 0
172
+ @loader.load(":ignore the below\nkey:value")
173
+ end
174
+ it "ignores all content on line after :ignore + tab" do
175
+ expect(@loader).to receive(:parse_command_key).with('ignore').once
176
+ @loader.load(":iGnOrE\nkey:value").keys.length.should == 0
177
+ @loader.load(":ignore\tthe below\nkey:value")
178
+ end
179
+
180
+ end
181
+
182
+ describe "multi line values" do
183
+
184
+ it "adds additional lines to value if followed by an ':end'" do
185
+ @loader.load("key:value\nextra\n:end")['key'].should == "value\nextra"
186
+ end
187
+ it "':end' is case insensitive" do
188
+ expect(@loader).to receive(:parse_command_key).with('end').once
189
+ @loader.load("key:value\nextra\n:EnD")
190
+ end
191
+ it "preserves blank lines and whitespace lines in the middle of content" do
192
+ @loader.load("key:value\n\n\t \nextra\n:end")['key'].should == "value\n\n\t \nextra"
193
+ end
194
+ it "doesn't preserve whitespace at the end of the key" do
195
+ @loader.load("key:value\nextra\t \n:end")['key'].should == "value\nextra"
196
+ end
197
+ it "preserves whitespace at the end of the original line" do
198
+ @loader.load("key:value\t \nextra\n:end")['key'].should == "value\t \nextra"
199
+ end
200
+ it "ignores whitespace and newlines before the ':end'" do
201
+ @loader.load("key:value\nextra\n \n\t\n:end")['key'].should == "value\nextra"
202
+ end
203
+ it "ignores spaces on either side of :end" do
204
+ expect(@loader).to receive(:parse_command_key).with('end').once
205
+ @loader.load("key:value\nextra\n :end ")
206
+ end
207
+ it "ignores tabs on either side of :end" do
208
+ expect(@loader).to receive(:parse_command_key).with('end').once
209
+ @loader.load("key:value\nextra\n\t\t:end\t\t")
210
+ end
211
+ it "parses :end as a special command even if more is appended to word" do
212
+ expect(@loader).to receive(:parse_command_key).with('end')
213
+ @loader.load("key:value\nextra\n:endthis")['key'].should == "value\nextra"
214
+ end
215
+ it "does not parse :endskip as an :end" do
216
+ expect(@loader).to_not receive(:parse_command_key).with('end')
217
+ @loader.load("key:value\nextra\n:endskip")['key'].should == "value"
218
+ end
219
+ it "ordinary text that starts with a colon is included" do
220
+ @loader.load("key:value\n:notacommand\n:end")['key'].should == "value\n:notacommand"
221
+ end
222
+ it "ignores all content on line after :end + space" do
223
+ expect(@loader).to receive(:parse_command_key).with('end').once
224
+ @loader.load("key:value\nextra\n:end this")['key'].should == "value\nextra"
225
+ end
226
+ it "ignores all content on line after :end + tab" do
227
+ expect(@loader).to receive(:parse_command_key).with('end').once
228
+ @loader.load("key:value\nextra\n:end\tthis")['key'].should == "value\nextra"
229
+ end
230
+ it "doesn't escape colons on first line" do
231
+ @loader.load("key::value\n:end")['key'].should == ":value"
232
+ @loader.load("key:\\:value\n:end")['key'].should == "\\:value"
233
+ end
234
+ it "does not allow escaping keys" do
235
+ @loader.load("key:value\nkey2\\:value\n:end")['key'].should == "value\nkey2\\:value"
236
+ end
237
+ it "allows escaping key lines with a leading backslash" do
238
+ @loader.load("key:value\n\\key2:value\n:end")['key'].should == "value\nkey2:value"
239
+ end
240
+ it "allows escaping commands at the beginning of lines" do
241
+ @loader.load("key:value\n\\:end\n:end")['key'].should == "value\n:end"
242
+ end
243
+ it "allows escaping commands with extra text at the beginning of lines" do
244
+ @loader.load("key:value\n\\:endthis\n:end")['key'].should == "value\n:endthis"
245
+ end
246
+ it "allows escaping of non-commandc at the beginning of lines" do
247
+ @loader.load("key:value\n\\:notacommand\n:end")['key'].should == "value\n:notacommand"
248
+ end
249
+ it "allows simple array style lines" do
250
+ @loader.load("key:value\n* value\n:end")['key'].should == "value\n* value"
251
+ end
252
+ it "escapes '*' within multi-line values when not in a simple array" do
253
+ @loader.load("key:value\n\\* value\n:end")['key'].should == "value\n* value"
254
+ end
255
+ it "allows escaping scope keys at the beginning of lines" do
256
+ @loader.load("key:value\n\\{scope}\n:end")['key'].should == "value\n{scope}"
257
+ @loader.load("key:value\n\\[comment]\n:end")['key'].should == "value"
258
+ @loader.load("key:value\n\\[[array]]\n:end")['key'].should == "value\n[array]"
259
+ end
260
+ it "allows escaping initial backslash at the beginning of lines" do
261
+ @loader.load("key:value\n\\\\:end\n:end")['key'].should == "value\n\\:end"
262
+ end
263
+ it "escapes only one initial backslash" do
264
+ @loader.load("key:value\n\\\\\\:end\n:end")['key'].should == "value\n\\\\:end"
265
+ end
266
+ it "doesn't escape colons after beginning of lines" do
267
+ @loader.load("key:value\nLorem key2\\:value\n:end")['key'].should == "value\nLorem key2\\:value"
268
+ end
269
+
270
+ end
271
+
272
+ describe "scopes" do
273
+
274
+ it "{scope} creates an empty object at 'scope'" do
275
+ @loader.load("{scope}")['scope'].class.should == Hash
276
+ end
277
+ it "ignores spaces on either side of {scope}" do
278
+ expect(@loader).to receive(:parse_scope).with('{', 'scope').once
279
+ @loader.load(" {scope} ")
280
+ end
281
+ it "ignores tabs on either side of {scope}" do
282
+ expect(@loader).to receive(:parse_scope).with('{', 'scope').once
283
+ @loader.load("\t\t{scope}\t\t")['scope'].should == {}
284
+ end
285
+ it "ignores text after {scope}" do
286
+ expect(@loader).to receive(:parse_scope).with('{', 'scope').once
287
+ @loader.load("{scope}a")['scope'].should == {}
288
+ end
289
+ it "ignores spaces on either side of {scope} variable name" do
290
+ expect(@loader).to receive(:parse_scope).with('{', 'scope').once
291
+ @loader.load("{ scope }")['scope'].should == {}
292
+ end
293
+ it "ignores tabs on either side of {scope} variable name" do
294
+ expect(@loader).to receive(:parse_scope).with('{', 'scope').once
295
+ @loader.load("{\t\tscope\t\t}")['scope'].should == {}
296
+ end
297
+ it "items before a {scope} are not namespaced" do
298
+ @loader.load("key:value\n{scope}")['key'].should == 'value'
299
+ end
300
+ it "items after a {scope} are namespaced" do
301
+ @loader.load("{scope}\nkey:value")['key'].should == nil
302
+ @loader.load("{scope}\nkey:value")['scope']['key'].should == 'value'
303
+ end
304
+ it "scopes can be nested using dot-notaion" do
305
+ @loader.load("{scope.scope}\nkey:value")['scope']['scope']['key'].should == 'value'
306
+ end
307
+ it "scopes can be reopened" do
308
+ @loader.load("{scope}\nkey:value\n{}\n{scope}\nother:value")['scope'].keys.should =~ ["key", "other"]
309
+ end
310
+ it "scopes do not overwrite existing values" do
311
+ @loader.load("{scope.scope}\nkey:value\n{scope.otherscope}key:value")['scope']['scope']['key'].should == 'value'
312
+ end
313
+ it "{} resets to the global scope" do
314
+ expect(@loader).to receive(:parse_scope).with('{', '').once
315
+ @loader.load("{scope}\n{}\nkey:value")['key'].should == 'value'
316
+ end
317
+ it "ignore spaces inside {}" do
318
+ expect(@loader).to receive(:parse_scope).with('{', '').once
319
+ @loader.load("{scope}\n{ }\nkey:value")['key'].should == 'value'
320
+ end
321
+ it "ignore tabs inside {}" do
322
+ expect(@loader).to receive(:parse_scope).with('{', '').once
323
+ @loader.load("{scope}\n{\t\t}\nkey:value")['key'].should == 'value'
324
+ end
325
+ it "ignore spaces on either side of {}" do
326
+ expect(@loader).to receive(:parse_scope).with('{', '').once
327
+ @loader.load("{scope}\n {} \nkey:value")['key'].should == 'value'
328
+ end
329
+ it "ignore tabs on either side of {}" do
330
+ expect(@loader).to receive(:parse_scope).with('{', '').once
331
+ @loader.load("{scope}\n\t\t{}\t\t\nkey:value")['key'].should == 'value'
332
+ end
333
+
334
+ end
335
+
336
+ describe "arrays" do
337
+
338
+ it "[array] creates an empty array at 'array'" do
339
+ @loader.load("[array]")['array'].should == []
340
+ end
341
+ it "ignores spaces on either side of [array]" do
342
+ expect(@loader).to receive(:parse_scope).with('[', 'array').once
343
+ @loader.load(" [array] ")
344
+ end
345
+ it "ignores tabs on either side of [array]" do
346
+ expect(@loader).to receive(:parse_scope).with('[', 'array').once
347
+ @loader.load("\t\t[array]\t\t")
348
+ end
349
+ it "ignores text after [array]" do
350
+ expect(@loader).to receive(:parse_scope).with('[', 'array').once
351
+ @loader.load("[array]a")['array'].should == []
352
+ end
353
+ it "ignores spaces on either side of [array] variable name" do
354
+ expect(@loader).to receive(:parse_scope).with('[', 'array').once
355
+ @loader.load("[ array ]")
356
+ end
357
+ it "ignores tabs on either side of [array] variable name" do
358
+ expect(@loader).to receive(:parse_scope).with('[', 'array').once
359
+ @loader.load("[\t\tarray\t\t]")
360
+ end
361
+ it "arrays can be nested using dot-notaion" do
362
+ @loader.load("[scope.array]")['scope']['array'].should == []
363
+ end
364
+ it "array values can be nested using dot-notaion" do
365
+ @loader.load("[array]\nscope.key: value\nscope.key: value")['array'].should == [{'scope' => {'key' => 'value'}}, {'scope' => {'key' => 'value'}}]
366
+ end
367
+ it "[] resets to the global scope" do
368
+ @loader.load("[array]\n[]\nkey:value")['key'].should == 'value'
369
+ end
370
+ it "ignore spaces inside []" do
371
+ expect(@loader).to receive(:parse_scope).with('[', '').once
372
+ @loader.load("[array]\n[ ]\nkey:value")['key'].should == 'value'
373
+ end
374
+ it "ignore tabs inside []" do
375
+ expect(@loader).to receive(:parse_scope).with('[', '').once
376
+ @loader.load("[array]\n[\t\t]\nkey:value")['key'].should == 'value'
377
+ end
378
+ it "ignore spaces on either side of []" do
379
+ expect(@loader).to receive(:parse_scope).with('[', '').once
380
+ @loader.load("[array]\n [] \nkey:value")['key'].should == 'value'
381
+ end
382
+ it "ignore tabs on either side of []" do
383
+ expect(@loader).to receive(:parse_scope).with('[', '').once
384
+ @loader.load("[array]\n\t\t[]\t\t\nkey:value")['key'].should == 'value'
385
+ end
386
+
387
+ end
388
+
389
+ describe "simple arrays" do
390
+
391
+ it "creates a simple array when an '*' is encountered first" do
392
+ @loader.load("[array]\n*Value")['array'].first.should == 'Value'
393
+ end
394
+ it "ignores spaces on either side of '*'" do
395
+ @loader.load("[array]\n * Value")['array'].first.should == 'Value'
396
+ end
397
+ it "ignores tabs on either side of '*'" do
398
+ @loader.load("[array]\n\t\t*\t\tValue")['array'].first.should == 'Value'
399
+ end
400
+ it "adds multiple elements" do
401
+ @loader.load("[array]\n*Value1\n*Value2")['array'].should == ['Value1', 'Value2']
402
+ end
403
+ it "ignores all other text between elements" do
404
+ @loader.load("[array]\n*Value1\nNon-element\n*Value2")['array'].should == ['Value1', 'Value2']
405
+ end
406
+ it "ignores key:value pairs between elements" do
407
+ @loader.load("[array]\n*Value1\nkey:value\n*Value2")['array'].should == ['Value1', 'Value2']
408
+ end
409
+ it "parses key:values normally after an end-array" do
410
+ @loader.load("[array]\n*Value1\n[]\nkey:value")['key'].should == 'value'
411
+ end
412
+ it "multi-line values are allowed" do
413
+ @loader.load("[array]\n*Value1\nextra\n:end")['array'].first.should == "Value1\nextra"
414
+ end
415
+ it "allows escaping of '*' within multi-line values in simple arrays" do
416
+ @loader.load("[array]\n*Value\n\\* extra\n:end")['array'].first.should == "Value\n* extra"
417
+ end
418
+ it "allows escaping of command keys within multi-line values" do
419
+ @loader.load("[array]\n*Value\n\\:end\n:end")['array'].first.should == "Value\n:end"
420
+ end
421
+ it "does not allow escaping of keys within multi-line values" do
422
+ @loader.load("[array]\n*Value\nkey\\:value\n:end")['array'].first.should == "Value\nkey\\:value"
423
+ end
424
+ it "allows escaping key lines with a leading backslash" do
425
+ @loader.load("[array]\n*Value\n\\key:value\n:end")['array'].first.should == "Value\nkey:value"
426
+ end
427
+ it "does not allow escaping of colons not at the beginning of lines" do
428
+ @loader.load("[array]\n*Value\nword key\\:value\n:end")['array'].first.should == "Value\nword key\\:value"
429
+ end
430
+ it "arrays that are reopened add to existing array" do
431
+ @loader.load("[array]\n*Value\n[]\n[array]\n*Value")['array'].should == ['Value', 'Value']
432
+ end
433
+ it "simple arrays that are reopened remain simple" do
434
+ @loader.load("[array]\n*Value\n[]\n[array]\nkey:value")['array'].should == ['Value']
435
+ end
436
+
437
+ end
438
+
439
+ describe "complex arrays" do
440
+
441
+ it "keys after an [array] are included as items in the array" do
442
+ @loader.load("[array]\nkey:value")['array'].first.should == {'key' => 'value' }
443
+ end
444
+ it "array items can have multiple keys" do
445
+ @loader.load("[array]\nkey:value\nsecond:value")['array'].first.keys.should =~ ['key', 'second']
446
+ end
447
+ it "when a duplicate key is encountered, a new item in the array is started" do
448
+ @loader.load("[array]\nkey:value\nsecond:value\nkey:value")['array'].length.should == 2
449
+ @loader.load("[array]\nkey:first\nkey:second")['array'].last.should == {'key' => 'second'}
450
+ @loader.load("[array]\nscope.key:first\nscope.key:second")['array'].last.should == {'scope' => {'key' => 'second'}}
451
+ end
452
+ it "duplicate keys must match on dot-notation scope" do
453
+ @loader.load("[array]\nkey:value\nscope.key:value")['array'].length.should == 1
454
+ end
455
+ it "duplicate keys must match on dot-notation scope" do
456
+ @loader.load("[array]\nscope.key:value\nkey:value\notherscope.key:value")['array'].length.should == 1
457
+ end
458
+ it "arrays that are reopened add to existing array" do
459
+ @loader.load("[array]\nkey:value\n[]\n[array]\nkey:value")['array'].length.should == 2
460
+ end
461
+ it "complex arrays that are reopened remain complex" do
462
+ @loader.load("[array]\nkey:value\n[]\n[array]\n*Value")['array'].should == [{'key' => 'value'}]
463
+ end
464
+
465
+ end
466
+
467
+ describe "inline comments" do
468
+
469
+ it "ignore comments inside of [single brackets]" do
470
+ @loader.load("key:value [inline comments] value")['key'].should == "value value"
471
+ end
472
+ it "supports multiple inline comments on a single line" do
473
+ @loader.load("key:value [inline comments] value [inline comments] value")['key'].should == "value value value"
474
+ end
475
+ it "supports adjacent comments" do
476
+ @loader.load("key:value [inline comments] [inline comments] value")['key'].should == "value value"
477
+ end
478
+ it "supports no-space adjacent comments" do
479
+ @loader.load("key:value [inline comments][inline comments] value")['key'].should == "value value"
480
+ end
481
+ it "supports comments at beginning of string" do
482
+ @loader.load("key:[inline comments] value")['key'].should == "value"
483
+ end
484
+ it "supports comments at end of string" do
485
+ @loader.load("key:value [inline comments]")['key'].should == "value"
486
+ end
487
+ it "whitespace before a comment that appears at end of line is ignored" do
488
+ @loader.load("key:value [inline comments] value [inline comments]")['key'].should == "value value"
489
+ end
490
+ it "unmatched single brackets are preserved" do
491
+ @loader.load("key:value ][ value")['key'].should == "value ][ value"
492
+ end
493
+
494
+ it "inline comments are supported on the first of multi-line values" do
495
+ @loader.load("key:value [inline comments] on\nmultiline\n:end")['key'].should == "value on\nmultiline"
496
+ end
497
+ it "inline comments are supported on subsequent lines of multi-line values" do
498
+ @loader.load("key:value\nmultiline [inline comments]\n:end")['key'].should == "value\nmultiline"
499
+ end
500
+ it "whitespace around comments is preserved, except at the beinning and end of a value" do
501
+ @loader.load("key: [] value [] \n multiline [] \n:end")['key'].should == "value \n multiline"
502
+ end
503
+
504
+ it "inline comments cannot span multiple lines" do
505
+ @loader.load("key:value [inline\ncomments] value\n:end")['key'].should == "value [inline\ncomments] value"
506
+ @loader.load("key:value \n[inline\ncomments] value\n:end")['key'].should == "value \n[inline\ncomments] value"
507
+ end
508
+ it "text inside [[double brackets]] is included as [single brackets]" do
509
+ @loader.load("key:value [[brackets]] value")['key'].should == "value [brackets] value"
510
+ end
511
+ it "unmatched double brackets are preserved" do
512
+ @loader.load("key:value ]][[ value")['key'].should == "value ]][[ value"
513
+ end
514
+ it "comments work in simple arrays" do
515
+ @loader.load("[array]\n*Val[comment]ue")['array'].first.should == "Value"
516
+ end
517
+ it "double brackets work in simple arrays" do
518
+ @loader.load("[array]\n*Val[[real]]ue")['array'].first.should == "Val[real]ue"
519
+ end
520
+
521
+ end
522
+ end
@@ -0,0 +1,2 @@
1
+ require 'archieml'
2
+ require 'rspec'
metadata ADDED
@@ -0,0 +1,57 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: archieml
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Michael Strickland
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-03-05 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Parse Archie Markup Language documents
14
+ email:
15
+ - michael.strickland@nytimes.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - .gitignore
21
+ - Gemfile
22
+ - LICENSE
23
+ - README.md
24
+ - archieml.gemspec
25
+ - examples/google_drive.rb
26
+ - lib/archieml.rb
27
+ - lib/archieml/loader.rb
28
+ - lib/archieml/version.rb
29
+ - spec/lib/archieml/loader_spec.rb
30
+ - spec/spec_helper.rb
31
+ homepage: http://archieml.org
32
+ licenses:
33
+ - Apache License 2.0
34
+ metadata: {}
35
+ post_install_message:
36
+ rdoc_options: []
37
+ require_paths:
38
+ - lib
39
+ required_ruby_version: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ required_rubygems_version: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - '>='
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ requirements: []
50
+ rubyforge_project:
51
+ rubygems_version: 2.4.5
52
+ signing_key:
53
+ specification_version: 4
54
+ summary: Archieml is a Ruby parser for the Archie Markup Language (ArchieML)
55
+ test_files:
56
+ - spec/lib/archieml/loader_spec.rb
57
+ - spec/spec_helper.rb