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.
@@ -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