archieml 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +3 -0
- data/Gemfile +4 -0
- data/LICENSE +13 -0
- data/README.md +172 -0
- data/archieml.gemspec +15 -0
- data/examples/google_drive.rb +103 -0
- data/lib/archieml.rb +14 -0
- data/lib/archieml/loader.rb +189 -0
- data/lib/archieml/version.rb +3 -0
- data/spec/lib/archieml/loader_spec.rb +522 -0
- data/spec/spec_helper.rb +2 -0
- metadata +57 -0
checksums.yaml
ADDED
@@ -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
|
data/.gitignore
ADDED
data/Gemfile
ADDED
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.
|
data/README.md
ADDED
@@ -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).
|
data/archieml.gemspec
ADDED
@@ -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
|
data/lib/archieml.rb
ADDED
@@ -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,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
|
data/spec/spec_helper.rb
ADDED
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
|