shift-lang 0.1.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 +17 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +29 -0
- data/Rakefile +2 -0
- data/bin/sft +5 -0
- data/bin/shift +5 -0
- data/lib/app_engine_templates/app.yaml +13 -0
- data/lib/app_engine_templates/pystache/__init__.py +8 -0
- data/lib/app_engine_templates/pystache/__init__.pyc +0 -0
- data/lib/app_engine_templates/pystache/loader.py +47 -0
- data/lib/app_engine_templates/pystache/loader.pyc +0 -0
- data/lib/app_engine_templates/pystache/template.py +177 -0
- data/lib/app_engine_templates/pystache/template.pyc +0 -0
- data/lib/app_engine_templates/pystache/view.py +94 -0
- data/lib/app_engine_templates/pystache/view.pyc +0 -0
- data/lib/heroku_templates/Gemfile +6 -0
- data/lib/heroku_templates/Procfile +1 -0
- data/lib/shift-lang/builder/db_model.rb +20 -0
- data/lib/shift-lang/builder/url_handler.rb +17 -0
- data/lib/shift-lang/builder.rb +103 -0
- data/lib/shift-lang/generator/javascript_generator.rb +410 -0
- data/lib/shift-lang/generator/javascript_templates.rb +37 -0
- data/lib/shift-lang/generator/python_generator.rb +330 -0
- data/lib/shift-lang/generator/python_templates.rb +44 -0
- data/lib/shift-lang/generator/ruby_generator.rb +334 -0
- data/lib/shift-lang/generator/ruby_templates.rb +50 -0
- data/lib/shift-lang/generator.rb +41 -0
- data/lib/shift-lang/parser/control_statement_parser.rb +17 -0
- data/lib/shift-lang/parser/db_query_parser.rb +20 -0
- data/lib/shift-lang/parser/keyword_parser.rb +52 -0
- data/lib/shift-lang/parser/operator_parser.rb +35 -0
- data/lib/shift-lang/parser/routing_expression_parser.rb +33 -0
- data/lib/shift-lang/parser/simple_expression_parser.rb +87 -0
- data/lib/shift-lang/parser/token_parser.rb +69 -0
- data/lib/shift-lang/parser/value_returning_expression_parser.rb +18 -0
- data/lib/shift-lang/parser/void_statement_parser.rb +12 -0
- data/lib/shift-lang/parser.rb +120 -0
- data/lib/shift-lang/version.rb +3 -0
- data/lib/shift-lang.rb +224 -0
- data/shift-lang.gemspec +19 -0
- metadata +106 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Pradeek
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# Shift::Lang
|
2
|
+
|
3
|
+
Shift is a Domain-Specific Language that can be used to quickly write server-side applications
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'shift-lang'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install shift-lang
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
TODO: Write usage instructions here
|
22
|
+
|
23
|
+
## Contributing
|
24
|
+
|
25
|
+
1. Fork it
|
26
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
27
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
28
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
29
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/bin/sft
ADDED
data/bin/shift
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
from pystache.template import Template
|
2
|
+
from pystache.view import View
|
3
|
+
from pystache.loader import Loader
|
4
|
+
|
5
|
+
def render(template, context=None, **kwargs):
|
6
|
+
context = context and context.copy() or {}
|
7
|
+
context.update(kwargs)
|
8
|
+
return Template(template, context).render()
|
Binary file
|
@@ -0,0 +1,47 @@
|
|
1
|
+
import os
|
2
|
+
|
3
|
+
class Loader(object):
|
4
|
+
|
5
|
+
template_extension = 'mustache'
|
6
|
+
template_path = '.'
|
7
|
+
template_encoding = None
|
8
|
+
|
9
|
+
def load_template(self, template_name, template_dirs=None, encoding=None, extension=None):
|
10
|
+
'''Returns the template string from a file or throws IOError if it non existent'''
|
11
|
+
if None == template_dirs:
|
12
|
+
template_dirs = self.template_path
|
13
|
+
|
14
|
+
if encoding is not None:
|
15
|
+
self.template_encoding = encoding
|
16
|
+
|
17
|
+
if extension is not None:
|
18
|
+
self.template_extension = extension
|
19
|
+
|
20
|
+
file_name = template_name + '.' + self.template_extension
|
21
|
+
|
22
|
+
# Given a single directory we'll load from it
|
23
|
+
if isinstance(template_dirs, basestring):
|
24
|
+
file_path = os.path.join(template_dirs, file_name)
|
25
|
+
|
26
|
+
return self._load_template_file(file_path)
|
27
|
+
|
28
|
+
# Given a list of directories we'll check each for our file
|
29
|
+
for path in template_dirs:
|
30
|
+
file_path = os.path.join(path, file_name)
|
31
|
+
if os.path.exists(file_path):
|
32
|
+
return self._load_template_file(file_path)
|
33
|
+
|
34
|
+
raise IOError('"%s" not found in "%s"' % (template_name, ':'.join(template_dirs),))
|
35
|
+
|
36
|
+
def _load_template_file(self, file_path):
|
37
|
+
'''Loads and returns the template file from disk'''
|
38
|
+
f = open(file_path, 'r')
|
39
|
+
|
40
|
+
try:
|
41
|
+
template = f.read()
|
42
|
+
if self.template_encoding:
|
43
|
+
template = unicode(template, self.template_encoding)
|
44
|
+
finally:
|
45
|
+
f.close()
|
46
|
+
|
47
|
+
return template
|
Binary file
|
@@ -0,0 +1,177 @@
|
|
1
|
+
import re
|
2
|
+
import cgi
|
3
|
+
import collections
|
4
|
+
import os
|
5
|
+
import copy
|
6
|
+
|
7
|
+
try:
|
8
|
+
import markupsafe
|
9
|
+
escape = markupsafe.escape
|
10
|
+
literal = markupsafe.Markup
|
11
|
+
|
12
|
+
except ImportError:
|
13
|
+
escape = lambda x: cgi.escape(unicode(x))
|
14
|
+
literal = unicode
|
15
|
+
|
16
|
+
|
17
|
+
class Modifiers(dict):
|
18
|
+
"""Dictionary with a decorator for assigning functions to keys."""
|
19
|
+
|
20
|
+
def set(self, key):
|
21
|
+
"""
|
22
|
+
Decorator function to set the given key to the decorated function.
|
23
|
+
|
24
|
+
>>> modifiers = {}
|
25
|
+
>>> @modifiers.set('P')
|
26
|
+
... def render_tongue(self, tag_name=None, context=None):
|
27
|
+
... return ":P %s" % tag_name
|
28
|
+
>>> modifiers
|
29
|
+
{'P': <function render_tongue at 0x...>}
|
30
|
+
"""
|
31
|
+
|
32
|
+
def setter(func):
|
33
|
+
self[key] = func
|
34
|
+
return func
|
35
|
+
return setter
|
36
|
+
|
37
|
+
|
38
|
+
class Template(object):
|
39
|
+
|
40
|
+
tag_re = None
|
41
|
+
|
42
|
+
otag = '{{'
|
43
|
+
|
44
|
+
ctag = '}}'
|
45
|
+
|
46
|
+
modifiers = Modifiers()
|
47
|
+
|
48
|
+
def __init__(self, template=None, context=None, **kwargs):
|
49
|
+
from view import View
|
50
|
+
|
51
|
+
self.template = template
|
52
|
+
|
53
|
+
if kwargs:
|
54
|
+
context.update(kwargs)
|
55
|
+
|
56
|
+
self.view = context if isinstance(context, View) else View(context=context)
|
57
|
+
self._compile_regexps()
|
58
|
+
|
59
|
+
def _compile_regexps(self):
|
60
|
+
tags = {
|
61
|
+
'otag': re.escape(self.otag),
|
62
|
+
'ctag': re.escape(self.ctag)
|
63
|
+
}
|
64
|
+
|
65
|
+
section = r"%(otag)s[\#|^]([^\}]*)%(ctag)s\s*(.+?\s*)%(otag)s/\1%(ctag)s"
|
66
|
+
self.section_re = re.compile(section % tags, re.M|re.S)
|
67
|
+
|
68
|
+
tag = r"%(otag)s(#|=|&|!|>|\{)?(.+?)\1?%(ctag)s+"
|
69
|
+
self.tag_re = re.compile(tag % tags)
|
70
|
+
|
71
|
+
def _render_sections(self, template, view):
|
72
|
+
while True:
|
73
|
+
match = self.section_re.search(template)
|
74
|
+
if match is None:
|
75
|
+
break
|
76
|
+
|
77
|
+
section, section_name, inner = match.group(0, 1, 2)
|
78
|
+
section_name = section_name.strip()
|
79
|
+
it = self.view.get(section_name, None)
|
80
|
+
replacer = ''
|
81
|
+
|
82
|
+
# Callable
|
83
|
+
if it and isinstance(it, collections.Callable):
|
84
|
+
replacer = it(inner)
|
85
|
+
# Dictionary
|
86
|
+
elif it and hasattr(it, 'keys') and hasattr(it, '__getitem__'):
|
87
|
+
if section[2] != '^':
|
88
|
+
replacer = self._render_dictionary(inner, it)
|
89
|
+
# Lists
|
90
|
+
elif it and hasattr(it, '__iter__'):
|
91
|
+
if section[2] != '^':
|
92
|
+
replacer = self._render_list(inner, it)
|
93
|
+
# Other objects
|
94
|
+
elif it and isinstance(it, object):
|
95
|
+
if section[2] != '^':
|
96
|
+
replacer = self._render_dictionary(inner, it)
|
97
|
+
# Falsey and Negated or Truthy and Not Negated
|
98
|
+
elif (not it and section[2] == '^') or (it and section[2] != '^'):
|
99
|
+
replacer = self._render_dictionary(inner, it)
|
100
|
+
|
101
|
+
template = literal(template.replace(section, replacer))
|
102
|
+
|
103
|
+
return template
|
104
|
+
|
105
|
+
def _render_tags(self, template):
|
106
|
+
while True:
|
107
|
+
match = self.tag_re.search(template)
|
108
|
+
if match is None:
|
109
|
+
break
|
110
|
+
|
111
|
+
tag, tag_type, tag_name = match.group(0, 1, 2)
|
112
|
+
tag_name = tag_name.strip()
|
113
|
+
func = self.modifiers[tag_type]
|
114
|
+
replacement = func(self, tag_name)
|
115
|
+
template = template.replace(tag, replacement)
|
116
|
+
|
117
|
+
return template
|
118
|
+
|
119
|
+
def _render_dictionary(self, template, context):
|
120
|
+
self.view.context_list.insert(0, context)
|
121
|
+
template = Template(template, self.view)
|
122
|
+
out = template.render()
|
123
|
+
self.view.context_list.pop(0)
|
124
|
+
return out
|
125
|
+
|
126
|
+
def _render_list(self, template, listing):
|
127
|
+
insides = []
|
128
|
+
for item in listing:
|
129
|
+
insides.append(self._render_dictionary(template, item))
|
130
|
+
|
131
|
+
return ''.join(insides)
|
132
|
+
|
133
|
+
@modifiers.set(None)
|
134
|
+
def _render_tag(self, tag_name):
|
135
|
+
raw = self.view.get(tag_name, '')
|
136
|
+
|
137
|
+
# For methods with no return value
|
138
|
+
if not raw and raw is not 0:
|
139
|
+
if tag_name == '.':
|
140
|
+
raw = self.view.context_list[0]
|
141
|
+
else:
|
142
|
+
return ''
|
143
|
+
|
144
|
+
return escape(raw)
|
145
|
+
|
146
|
+
@modifiers.set('!')
|
147
|
+
def _render_comment(self, tag_name):
|
148
|
+
return ''
|
149
|
+
|
150
|
+
@modifiers.set('>')
|
151
|
+
def _render_partial(self, template_name):
|
152
|
+
from pystache import Loader
|
153
|
+
markup = Loader().load_template(template_name, self.view.template_path, encoding=self.view.template_encoding)
|
154
|
+
template = Template(markup, self.view)
|
155
|
+
return template.render()
|
156
|
+
|
157
|
+
@modifiers.set('=')
|
158
|
+
def _change_delimiter(self, tag_name):
|
159
|
+
"""Changes the Mustache delimiter."""
|
160
|
+
self.otag, self.ctag = tag_name.split(' ')
|
161
|
+
self._compile_regexps()
|
162
|
+
return ''
|
163
|
+
|
164
|
+
@modifiers.set('{')
|
165
|
+
@modifiers.set('&')
|
166
|
+
def render_unescaped(self, tag_name):
|
167
|
+
"""Render a tag without escaping it."""
|
168
|
+
return literal(self.view.get(tag_name, ''))
|
169
|
+
|
170
|
+
def render(self, encoding=None):
|
171
|
+
template = self._render_sections(self.template, self.view)
|
172
|
+
result = self._render_tags(template)
|
173
|
+
|
174
|
+
if encoding is not None:
|
175
|
+
result = result.encode(encoding)
|
176
|
+
|
177
|
+
return result
|
Binary file
|
@@ -0,0 +1,94 @@
|
|
1
|
+
from pystache import Template
|
2
|
+
import os.path
|
3
|
+
import re
|
4
|
+
from types import *
|
5
|
+
|
6
|
+
def get_or_attr(context_list, name, default=None):
|
7
|
+
if not context_list:
|
8
|
+
return default
|
9
|
+
|
10
|
+
for obj in context_list:
|
11
|
+
try:
|
12
|
+
return obj[name]
|
13
|
+
except KeyError:
|
14
|
+
pass
|
15
|
+
except:
|
16
|
+
try:
|
17
|
+
return getattr(obj, name)
|
18
|
+
except AttributeError:
|
19
|
+
pass
|
20
|
+
return default
|
21
|
+
|
22
|
+
class View(object):
|
23
|
+
|
24
|
+
template_name = None
|
25
|
+
template_path = None
|
26
|
+
template = None
|
27
|
+
template_encoding = None
|
28
|
+
template_extension = 'mustache'
|
29
|
+
|
30
|
+
def __init__(self, template=None, context=None, **kwargs):
|
31
|
+
self.template = template
|
32
|
+
context = context or {}
|
33
|
+
context.update(**kwargs)
|
34
|
+
|
35
|
+
self.context_list = [context]
|
36
|
+
|
37
|
+
def get(self, attr, default=None):
|
38
|
+
attr = get_or_attr(self.context_list, attr, getattr(self, attr, default))
|
39
|
+
if hasattr(attr, '__call__') and type(attr) is UnboundMethodType:
|
40
|
+
return attr()
|
41
|
+
else:
|
42
|
+
return attr
|
43
|
+
|
44
|
+
def get_template(self, template_name):
|
45
|
+
if not self.template:
|
46
|
+
from pystache import Loader
|
47
|
+
template_name = self._get_template_name(template_name)
|
48
|
+
self.template = Loader().load_template(template_name, self.template_path, encoding=self.template_encoding, extension=self.template_extension)
|
49
|
+
|
50
|
+
return self.template
|
51
|
+
|
52
|
+
def _get_template_name(self, template_name=None):
|
53
|
+
"""TemplatePartial => template_partial
|
54
|
+
Takes a string but defaults to using the current class' name or
|
55
|
+
the `template_name` attribute
|
56
|
+
"""
|
57
|
+
if template_name:
|
58
|
+
return template_name
|
59
|
+
|
60
|
+
template_name = self.__class__.__name__
|
61
|
+
|
62
|
+
def repl(match):
|
63
|
+
return '_' + match.group(0).lower()
|
64
|
+
|
65
|
+
return re.sub('[A-Z]', repl, template_name)[1:]
|
66
|
+
|
67
|
+
def _get_context(self):
|
68
|
+
context = {}
|
69
|
+
for item in self.context_list:
|
70
|
+
if hasattr(item, 'keys') and hasattr(item, '__getitem__'):
|
71
|
+
context.update(item)
|
72
|
+
return context
|
73
|
+
|
74
|
+
def render(self, encoding=None):
|
75
|
+
return Template(self.get_template(self.template_name), self).render(encoding=encoding)
|
76
|
+
|
77
|
+
def __contains__(self, needle):
|
78
|
+
return needle in self.context or hasattr(self, needle)
|
79
|
+
|
80
|
+
def __getitem__(self, attr):
|
81
|
+
val = self.get(attr, None)
|
82
|
+
|
83
|
+
if not val and val is not 0:
|
84
|
+
raise KeyError("Key '%s' does not exist in View" % attr)
|
85
|
+
return val
|
86
|
+
|
87
|
+
def __getattr__(self, attr):
|
88
|
+
if attr == 'context':
|
89
|
+
return self._get_context()
|
90
|
+
|
91
|
+
raise AttributeError("Attribute '%s' does not exist in View" % attr)
|
92
|
+
|
93
|
+
def __str__(self):
|
94
|
+
return self.render()
|
Binary file
|
@@ -0,0 +1 @@
|
|
1
|
+
web: bundle exec ruby app.rb -p $PORT
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Shift
|
2
|
+
module Builder
|
3
|
+
class DBModel
|
4
|
+
attr_accessor :name, :attributes
|
5
|
+
|
6
|
+
def initialize(name)
|
7
|
+
@name = name.to_s
|
8
|
+
@attributes = []
|
9
|
+
end
|
10
|
+
|
11
|
+
def add_attribute(name, type)
|
12
|
+
attribute = {
|
13
|
+
:name => name.to_s,
|
14
|
+
:type => type.to_s
|
15
|
+
}
|
16
|
+
@attributes.push attribute
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Shift
|
2
|
+
module Builder
|
3
|
+
class UrlHandler
|
4
|
+
attr_accessor :url, :method, :handler
|
5
|
+
|
6
|
+
def initialize(url, method)
|
7
|
+
@url = url
|
8
|
+
@method = method
|
9
|
+
@handler = []
|
10
|
+
end
|
11
|
+
|
12
|
+
def add_statement(statement)
|
13
|
+
handler.push statement
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
require_relative 'builder/db_model'
|
2
|
+
require_relative 'builder/url_handler'
|
3
|
+
|
4
|
+
require_relative 'parser'
|
5
|
+
|
6
|
+
module Shift
|
7
|
+
class ShiftBuilder
|
8
|
+
attr_accessor :models, :handlers, :clean
|
9
|
+
|
10
|
+
def initialize()
|
11
|
+
@models = []
|
12
|
+
@handlers = []
|
13
|
+
@clean = true
|
14
|
+
end
|
15
|
+
|
16
|
+
def get_data_from_file(file_name)
|
17
|
+
parser = Shift::ShiftParser.new
|
18
|
+
|
19
|
+
model = nil
|
20
|
+
|
21
|
+
url_handler = nil
|
22
|
+
|
23
|
+
last_statement = ""
|
24
|
+
this_statement = ""
|
25
|
+
|
26
|
+
url = ""
|
27
|
+
|
28
|
+
line_number = 0
|
29
|
+
|
30
|
+
File.open(file_name) do |file|
|
31
|
+
while line = file.gets
|
32
|
+
parts = line.split("\t")
|
33
|
+
num_tabs = parts.length - 1
|
34
|
+
line = line.strip
|
35
|
+
line_number += 1
|
36
|
+
if line != ""
|
37
|
+
begin
|
38
|
+
statement = parser.parse(line)
|
39
|
+
|
40
|
+
this_statement = statement.keys[0]
|
41
|
+
|
42
|
+
case this_statement
|
43
|
+
|
44
|
+
when :model_definition_statement
|
45
|
+
model = Builder::DBModel.new(statement[:model_definition_statement][:model_name])
|
46
|
+
|
47
|
+
when :model_attribute_definition_statement
|
48
|
+
model_attribute = statement[:model_attribute_definition_statement]
|
49
|
+
model.add_attribute(model_attribute[:attribute_name], model_attribute[:attribute_type])
|
50
|
+
|
51
|
+
when :url
|
52
|
+
if (last_statement == :model_attribute_definition_statement)
|
53
|
+
add_model model
|
54
|
+
end
|
55
|
+
|
56
|
+
url = statement[:url]
|
57
|
+
when :url_method
|
58
|
+
if url_handler
|
59
|
+
add_handler url_handler
|
60
|
+
end
|
61
|
+
|
62
|
+
url_handler = Builder::UrlHandler.new(url, statement[:url_method].to_s)
|
63
|
+
else
|
64
|
+
stmt = {
|
65
|
+
:statement => statement,
|
66
|
+
:num_tabs => num_tabs
|
67
|
+
}
|
68
|
+
if !url_handler
|
69
|
+
url_handler = Builder::UrlHandler.new("", "")
|
70
|
+
end
|
71
|
+
url_handler.add_statement stmt
|
72
|
+
end
|
73
|
+
|
74
|
+
last_statement = this_statement
|
75
|
+
|
76
|
+
rescue Parslet::ParseFailed => parse_error
|
77
|
+
error = parse_error.to_s
|
78
|
+
error = error.split(" ")
|
79
|
+
puts "Error at line #{line_number} character #{error[error.length - 1]} : #{line}"
|
80
|
+
@clean = false
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
if (last_statement == :model_attribute_definition_statement)
|
87
|
+
add_model model
|
88
|
+
else
|
89
|
+
add_handler url_handler
|
90
|
+
end
|
91
|
+
|
92
|
+
self
|
93
|
+
end
|
94
|
+
|
95
|
+
def add_model(model)
|
96
|
+
@models.push model
|
97
|
+
end
|
98
|
+
|
99
|
+
def add_handler(handler)
|
100
|
+
@handlers.push handler
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|