xmlish 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +2 -0
- data/Gemfile +3 -0
- data/README.md +20 -0
- data/lib/xmlish.rb +199 -0
- data/spec/example_spec.rb +71 -0
- data/spec/spec_helper.rb +8 -0
- data/spec/xmlish_spec.rb +115 -0
- data/xmlish.gemspec +18 -0
- metadata +72 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
xmlish
|
2
|
+
======
|
3
|
+
|
4
|
+
String interpolation from xml-like text.
|
5
|
+
|
6
|
+
Usage Examples
|
7
|
+
--------------
|
8
|
+
|
9
|
+
A simple example
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
template = "Press <red><bold>ENTER</bold></red> or <bold><red>ESC</red></bold>"
|
13
|
+
callbacks = {
|
14
|
+
'red' => lambda { |str| "$#{str}$" },
|
15
|
+
'bold' => lambda { |str| "**#{str}**" }
|
16
|
+
}
|
17
|
+
Xmlish.parse(template, callbacks) #=> "Press $**ENTER**$ or **$ESC$**"
|
18
|
+
```
|
19
|
+
|
20
|
+
See spec directory for more examples.
|
data/lib/xmlish.rb
ADDED
@@ -0,0 +1,199 @@
|
|
1
|
+
module Xmlish
|
2
|
+
|
3
|
+
# @param str [String]
|
4
|
+
# @param callbacks [Array<#call>]
|
5
|
+
# @return [String]
|
6
|
+
def self.parse str, callbacks=nil
|
7
|
+
reconstruct_from_nodes parse_to_nodes(str, callbacks.keys), callbacks
|
8
|
+
end
|
9
|
+
|
10
|
+
# @param str [String]
|
11
|
+
# @param callbacks [Array<#call>]
|
12
|
+
# @return [void]
|
13
|
+
def self.walk str, callbacks=nil
|
14
|
+
walk_nodes parse_to_nodes(str, callbacks.keys), callbacks
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
# @param str [String]
|
20
|
+
# @return [Array<Node>]
|
21
|
+
def self.parse_to_nodes str, tag_names
|
22
|
+
Parser.new(str, tag_names).parse
|
23
|
+
end
|
24
|
+
|
25
|
+
# @param nodes [Array<Node>]
|
26
|
+
# @param callbacks [Array<#call>]
|
27
|
+
# @return [String]
|
28
|
+
def self.reconstruct_from_nodes nodes, callbacks={}
|
29
|
+
Reconstructor.new(nodes, callbacks).reconstruct
|
30
|
+
end
|
31
|
+
|
32
|
+
# @param nodes [Array<Node>]
|
33
|
+
# @param callbacks [Array<#call>]
|
34
|
+
# @return [void]
|
35
|
+
def self.walk_nodes nodes, callbacks={}
|
36
|
+
Walker.new(nodes, callbacks).walk
|
37
|
+
end
|
38
|
+
|
39
|
+
class Node < Struct.new(:nodes, :attr_name)
|
40
|
+
end
|
41
|
+
|
42
|
+
class Parser
|
43
|
+
def initialize str, tag_names
|
44
|
+
@str = str
|
45
|
+
@tag_names = tag_names
|
46
|
+
end
|
47
|
+
|
48
|
+
def parse
|
49
|
+
normalise(parse_string @str)
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
# @param s [String] like "Press <red>e</red> to edit"
|
55
|
+
def parse_string s
|
56
|
+
[].tap do |nodes|
|
57
|
+
while s.length > 0
|
58
|
+
head, tag_name = nil, nil
|
59
|
+
begin
|
60
|
+
head, tag_name = (m=s.match(/[^<]*<([A-Za-z0-9]+)>/)).to_a
|
61
|
+
end until tag_name.nil? || begin
|
62
|
+
@tag_names.include?(tag_name).tap do |incl|
|
63
|
+
unless incl
|
64
|
+
nodes << head
|
65
|
+
s = s[head.length, s.length]
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
if tag_name.nil?
|
70
|
+
nodes << s
|
71
|
+
break
|
72
|
+
end
|
73
|
+
text = head[0, head.length - tag_name.length - '<>'.length]
|
74
|
+
nodes << text unless text.length == 0
|
75
|
+
s = s[head.length, s.length]
|
76
|
+
head = s.match(/.*?<\/#{tag_name}>/).to_a.first
|
77
|
+
node_text = head[0, head.length - tag_name.length - '</>'.length]
|
78
|
+
node_text = parse_string(node_text)
|
79
|
+
nodes << Node.new(node_text, tag_name)
|
80
|
+
s = s[head.length, s.length]
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# @param nodes [Array<Node,String>]
|
86
|
+
# @return [Array<Node,String>]
|
87
|
+
def normalise nodes
|
88
|
+
nodes.each do |node|
|
89
|
+
if Node === node
|
90
|
+
node.nodes = join_adjacent_strings node.nodes
|
91
|
+
normalise node.nodes
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# @param nodes [Array<Node,String>]
|
97
|
+
# @return [Array<Node,String>]
|
98
|
+
def join_adjacent_strings nodes
|
99
|
+
nodes.inject [] do |a, b|
|
100
|
+
if b.is_a?(String) && a.last.is_a?(String)
|
101
|
+
(a[0, a.length-1] + ["#{a.last}#{b}"])
|
102
|
+
else
|
103
|
+
[a, b]
|
104
|
+
end.flatten
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
class Reconstructor
|
110
|
+
# @param nodes [Array<Node>]
|
111
|
+
# @param callbacks [Array<#call>]
|
112
|
+
def initialize nodes, callbacks
|
113
|
+
@nodes = nodes
|
114
|
+
@callbacks = callbacks
|
115
|
+
end
|
116
|
+
|
117
|
+
# @return [String]
|
118
|
+
def reconstruct nodes=@nodes
|
119
|
+
nodes.collect do |node|
|
120
|
+
if Node === node
|
121
|
+
callback = callback_for_tag node.attr_name
|
122
|
+
if callback
|
123
|
+
callback.call(reconstruct node.nodes)
|
124
|
+
else
|
125
|
+
reconstruct node.nodes
|
126
|
+
end
|
127
|
+
else
|
128
|
+
callback = callback_for_tag 'text'
|
129
|
+
if callback
|
130
|
+
callback.call(node)
|
131
|
+
else
|
132
|
+
node
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end.join('')
|
136
|
+
end
|
137
|
+
|
138
|
+
private
|
139
|
+
|
140
|
+
def callback_for_tag tag
|
141
|
+
if @callbacks.has_key? tag
|
142
|
+
@callbacks[tag] || identity_callback
|
143
|
+
else
|
144
|
+
nil
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def identity_callback
|
149
|
+
lambda{|s|s}
|
150
|
+
end
|
151
|
+
|
152
|
+
end
|
153
|
+
|
154
|
+
class Walker
|
155
|
+
# @param nodes [Array<Node>]
|
156
|
+
# @param callbacks [Array<#call>]
|
157
|
+
def initialize nodes, callbacks
|
158
|
+
@nodes = nodes
|
159
|
+
@callbacks = callbacks
|
160
|
+
end
|
161
|
+
|
162
|
+
# @return [String]
|
163
|
+
def walk nodes=@nodes
|
164
|
+
nodes.collect do |node|
|
165
|
+
if Node === node
|
166
|
+
callback = callback_for_tag node.attr_name
|
167
|
+
if callback
|
168
|
+
callback.call(Proc.new{walk node.nodes})
|
169
|
+
else
|
170
|
+
walk node.nodes
|
171
|
+
end
|
172
|
+
else
|
173
|
+
callback = callback_for_tag 'text'
|
174
|
+
if callback
|
175
|
+
callback.call(Proc.new{node})
|
176
|
+
else
|
177
|
+
node
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end.join('')
|
181
|
+
end
|
182
|
+
|
183
|
+
private
|
184
|
+
|
185
|
+
def callback_for_tag tag
|
186
|
+
if @callbacks.has_key? tag
|
187
|
+
@callbacks[tag] || identity_callback
|
188
|
+
else
|
189
|
+
nil
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
def identity_callback
|
194
|
+
lambda{|s|s.call}
|
195
|
+
end
|
196
|
+
|
197
|
+
end
|
198
|
+
|
199
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Xmlish do
|
4
|
+
|
5
|
+
describe ".parse" do
|
6
|
+
specify "repeated nested tags" do
|
7
|
+
template = "Press <red><bold>ENTER</bold></red> or <bold><red>ESC</red></bold>"
|
8
|
+
callbacks = {
|
9
|
+
'red' => lambda{|str|"$#{str}$"},
|
10
|
+
'bold' => lambda{|str|"**#{str}**"}
|
11
|
+
}
|
12
|
+
Xmlish.parse(template, callbacks).should == 'Press $**ENTER**$ or **$ESC$**'
|
13
|
+
end
|
14
|
+
|
15
|
+
specify "more complex repeated nested tags" do
|
16
|
+
template = "Press <red><u><</u><bold><downcase>ENTER</downcase></bold><u>></u></red> or <bold><red>ESC</red></bold>"
|
17
|
+
callbacks = {
|
18
|
+
'red' => lambda{|str|"$#{str}$"},
|
19
|
+
'bold' => lambda{|str|"**#{str}**"},
|
20
|
+
'u' => lambda{|str|"_#{str}_"},
|
21
|
+
'downcase' => lambda{|str|str.downcase},
|
22
|
+
}
|
23
|
+
Xmlish.parse(template, callbacks).should == 'Press $_<_**enter**_>_$ or **$ESC$**'
|
24
|
+
end
|
25
|
+
|
26
|
+
specify "plain text callbacks" do
|
27
|
+
template = "Press <red>ENTER</red> or <red>ESC</red>"
|
28
|
+
callbacks = {
|
29
|
+
'red' => lambda{|str|"$#{str}$"},
|
30
|
+
'text' => lambda{|str|"(#{str.upcase})"},
|
31
|
+
}
|
32
|
+
Xmlish.parse(template, callbacks).should == '(PRESS )$(ENTER)$( OR )$(ESC)$'
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should leave tags without callback intact" do
|
36
|
+
template = "Press <red><bold>ENTER</bold></red> or <bold><red>ESC</red></bold>"
|
37
|
+
callbacks = {
|
38
|
+
'red' => lambda{|str|"$#{str}$"}
|
39
|
+
}
|
40
|
+
Xmlish.parse(template, callbacks).should == 'Press $<bold>ENTER</bold>$ or <bold>$ESC$</bold>'
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should tolerate bad syntax in tags without callbacks" do
|
44
|
+
template = "Press <red>ENTER</bold></red> or <bold><red>ESC</red></penguins></>"
|
45
|
+
callbacks = {
|
46
|
+
'red' => lambda{|str|"$#{str}$"}
|
47
|
+
}
|
48
|
+
Xmlish.parse(template, callbacks).should == 'Press $ENTER</bold>$ or <bold>$ESC$</penguins></>'
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe ".walk" do
|
53
|
+
specify "building output in application" do
|
54
|
+
template = "Press <red>ENTER</red> or <red>ESC</red>"
|
55
|
+
output = []
|
56
|
+
callbacks = {
|
57
|
+
'red' => lambda{|str|
|
58
|
+
output << "{red-on}"
|
59
|
+
str.call
|
60
|
+
output << "{red-off}"
|
61
|
+
},
|
62
|
+
'text' => lambda{|str|
|
63
|
+
output << str.call.upcase
|
64
|
+
},
|
65
|
+
}
|
66
|
+
Xmlish.walk(template, callbacks)
|
67
|
+
output.join("").should == 'PRESS {red-on}ENTER{red-off} OR {red-on}ESC{red-off}'
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
data/spec/spec_helper.rb
ADDED
data/spec/xmlish_spec.rb
ADDED
@@ -0,0 +1,115 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Xmlish do
|
4
|
+
|
5
|
+
Node = Xmlish::Node
|
6
|
+
|
7
|
+
describe "#parse_to_nodes" do
|
8
|
+
|
9
|
+
def parse_to_nodes *args
|
10
|
+
Xmlish.send(:parse_to_nodes, *args)
|
11
|
+
end
|
12
|
+
|
13
|
+
specify "plain text" do
|
14
|
+
parse_to_nodes("test", []).should == ["test"]
|
15
|
+
end
|
16
|
+
|
17
|
+
specify "single tag" do
|
18
|
+
parse_to_nodes("Press <red>ENTER</red>", ['red']).should ==
|
19
|
+
["Press ", Node.new(["ENTER"], 'red')]
|
20
|
+
parse_to_nodes("Press <bold>ENTER</bold>", ['bold']).should ==
|
21
|
+
["Press ", Node.new(["ENTER"], 'bold')]
|
22
|
+
end
|
23
|
+
|
24
|
+
specify "nested tags" do
|
25
|
+
parse_to_nodes("Press <red><bold>ENTER</bold></red>", ['red', 'bold']).should ==
|
26
|
+
["Press ", Node.new([Node.new(["ENTER"], 'bold')], 'red')]
|
27
|
+
end
|
28
|
+
|
29
|
+
specify "repeated tag" do
|
30
|
+
parse_to_nodes("Press <red>ENTER</red> or <red>ESC</red>", ['red']).should ==
|
31
|
+
["Press ", Node.new(["ENTER"], 'red'), " or ", Node.new(["ESC"], 'red')]
|
32
|
+
end
|
33
|
+
|
34
|
+
specify "repeated nested tags" do
|
35
|
+
parse_to_nodes("Press <red><bold>ENTER</bold></red> or <red><bold>ESC</bold></red>", ['red', 'bold']).should ==
|
36
|
+
["Press ", Node.new([Node.new(["ENTER"], 'bold')], 'red'), " or ", Node.new([Node.new(["ESC"], 'bold')], 'red')]
|
37
|
+
end
|
38
|
+
|
39
|
+
specify "non-listed tags pass through like normal text" do
|
40
|
+
parse_to_nodes("Press <red><bold>ENTER</bold></red> or <bold><red>ESC</red></bold>", ['red']).should ==
|
41
|
+
["Press ", Node.new(["<bold>ENTER</bold>"], 'red'), " or <bold>", Node.new(["ESC"], 'red'), "</bold>"]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
describe "#reconstruct_from_nodes" do
|
46
|
+
|
47
|
+
def reconstruct_from_nodes *args
|
48
|
+
Xmlish.send(:reconstruct_from_nodes, *args)
|
49
|
+
end
|
50
|
+
|
51
|
+
specify "only string nodes" do
|
52
|
+
reconstruct_from_nodes(["test"]).should == "test"
|
53
|
+
end
|
54
|
+
|
55
|
+
context "with null callbacks" do
|
56
|
+
specify "single tag" do
|
57
|
+
nodes = ["Press ", Node.new(["ENTER"], 'red')]
|
58
|
+
callbacks = {'red' => nil}
|
59
|
+
reconstruct_from_nodes(nodes).should == 'Press ENTER'
|
60
|
+
end
|
61
|
+
|
62
|
+
specify "nested tags" do
|
63
|
+
nodes = ["Press ", Node.new([Node.new(["ENTER"], 'bold')], 'red')]
|
64
|
+
callbacks = {'red' => nil, 'bold' => nil}
|
65
|
+
reconstruct_from_nodes(nodes, callbacks).should == 'Press ENTER'
|
66
|
+
end
|
67
|
+
|
68
|
+
specify "repeated tag" do
|
69
|
+
nodes = ["Press ", Node.new(["ENTER"], 'red'), " or ", Node.new(["ESC"], 'red')]
|
70
|
+
callbacks = {'red' => nil}
|
71
|
+
reconstruct_from_nodes(nodes, callbacks).should == 'Press ENTER or ESC'
|
72
|
+
end
|
73
|
+
|
74
|
+
specify "repeated nested tags" do
|
75
|
+
nodes = ["Press ", Node.new([Node.new(["ENTER"], 'bold')], 'red'), " or ", Node.new([Node.new(["ESC"], 'bold')], 'red')]
|
76
|
+
callbacks = {'red' => nil, 'bold' => nil}
|
77
|
+
reconstruct_from_nodes(nodes, callbacks).should == 'Press ENTER or ESC'
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
context "with modifying callbacks" do
|
82
|
+
specify "single tag" do
|
83
|
+
nodes = ["Press ", Node.new(["ENTER"], 'red')]
|
84
|
+
callbacks = {'red' => lambda{|str|"$#{str}$"}}
|
85
|
+
reconstruct_from_nodes(nodes, callbacks).should == 'Press $ENTER$'
|
86
|
+
end
|
87
|
+
|
88
|
+
specify "nested tags" do
|
89
|
+
nodes = ["Press ", Node.new([Node.new(["ENTER"], 'bold')], 'red')]
|
90
|
+
callbacks = {
|
91
|
+
'red' => lambda{|str|"$#{str}$"},
|
92
|
+
'bold' => lambda{|str|"**#{str}**"}
|
93
|
+
}
|
94
|
+
reconstruct_from_nodes(nodes, callbacks).should == 'Press $**ENTER**$'
|
95
|
+
end
|
96
|
+
|
97
|
+
specify "repeated tag" do
|
98
|
+
nodes = ["Press ", Node.new(["ENTER"], 'red'), " or ", Node.new(["ESC"], 'red')]
|
99
|
+
callbacks = {'red' => lambda{|str|"$#{str}$"}}
|
100
|
+
reconstruct_from_nodes(nodes, callbacks).should == 'Press $ENTER$ or $ESC$'
|
101
|
+
end
|
102
|
+
|
103
|
+
specify "repeated nested tags" do
|
104
|
+
nodes = ["Press ", Node.new([Node.new(["ENTER"], 'bold')], 'red'), " or ", Node.new([Node.new(["ESC"], 'bold')], 'red')]
|
105
|
+
callbacks = {
|
106
|
+
'red' => lambda{|str|"$#{str}$"},
|
107
|
+
'bold' => lambda{|str|"**#{str}**"}
|
108
|
+
}
|
109
|
+
reconstruct_from_nodes(nodes, callbacks).should == 'Press $**ENTER**$ or $**ESC**$'
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
data/xmlish.gemspec
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = "xmlish"
|
6
|
+
s.version = "0.0.1"
|
7
|
+
s.authors = ["Joel Plane"]
|
8
|
+
s.email = ["joel.plane@gmail.com"]
|
9
|
+
s.homepage = "https://github.com/joelplane/xmlish"
|
10
|
+
s.summary = %q{string interpolation from xml-like text}
|
11
|
+
s.description = %q{small almost-xml parser for complex string interpolation}
|
12
|
+
|
13
|
+
s.files = `git ls-files`.split("\n")
|
14
|
+
s.test_files = `git ls-files -- spec/*`.split("\n")
|
15
|
+
s.require_paths = ["lib"]
|
16
|
+
|
17
|
+
s.add_development_dependency "rspec"
|
18
|
+
end
|
metadata
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: xmlish
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Joel Plane
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-10-13 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rspec
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
description: small almost-xml parser for complex string interpolation
|
31
|
+
email:
|
32
|
+
- joel.plane@gmail.com
|
33
|
+
executables: []
|
34
|
+
extensions: []
|
35
|
+
extra_rdoc_files: []
|
36
|
+
files:
|
37
|
+
- .gitignore
|
38
|
+
- Gemfile
|
39
|
+
- README.md
|
40
|
+
- lib/xmlish.rb
|
41
|
+
- spec/example_spec.rb
|
42
|
+
- spec/spec_helper.rb
|
43
|
+
- spec/xmlish_spec.rb
|
44
|
+
- xmlish.gemspec
|
45
|
+
homepage: https://github.com/joelplane/xmlish
|
46
|
+
licenses: []
|
47
|
+
post_install_message:
|
48
|
+
rdoc_options: []
|
49
|
+
require_paths:
|
50
|
+
- lib
|
51
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
52
|
+
none: false
|
53
|
+
requirements:
|
54
|
+
- - ! '>='
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: '0'
|
57
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
58
|
+
none: false
|
59
|
+
requirements:
|
60
|
+
- - ! '>='
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
requirements: []
|
64
|
+
rubyforge_project:
|
65
|
+
rubygems_version: 1.8.24
|
66
|
+
signing_key:
|
67
|
+
specification_version: 3
|
68
|
+
summary: string interpolation from xml-like text
|
69
|
+
test_files:
|
70
|
+
- spec/example_spec.rb
|
71
|
+
- spec/spec_helper.rb
|
72
|
+
- spec/xmlish_spec.rb
|