xmlish 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,2 @@
1
+ Gemfile.lock
2
+
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
@@ -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.
@@ -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
@@ -0,0 +1,8 @@
1
+ require 'xmlish'
2
+
3
+ RSpec.configure do |config|
4
+ config.treat_symbols_as_metadata_keys_with_true_values = true
5
+ config.run_all_when_everything_filtered = true
6
+ config.filter_run :focus
7
+ config.order = 'random'
8
+ end
@@ -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
@@ -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