|
|
TODO |
TODO |
|
|
- implement interaction wrapper for "/skin", "/request", etc. data paths |
- Address traversal nesting for referenced data |
|
|
|
- Dynamic attributes, independent of element? |
|
|
|
- Phase out old PWT syntax |
|
|
- implement sub-template support (convert doc->DOMlet in another doc) |
- implement sub-template support (convert doc->DOMlet in another doc) |
|
|
from interfaces import * |
from interfaces import * |
from xml.sax.saxutils import quoteattr, escape |
from xml.sax.saxutils import quoteattr, escape |
from publish import TraversalPath |
from publish import TraversalPath |
from environ import getAbsoluteURL, getInteraction, getCurrent |
from peak.util import SOX, imports |
from environ import childContext, parentContext |
from places import Decorator |
from peak.util import SOX |
from environ import traverseItem, traverseDefault |
|
from errors import NotFound |
|
|
__all__ = [ |
__all__ = [ |
'TEMPLATE_NS', 'DOMLETS_PROPERTY', 'TemplateDocument' |
'TEMPLATE_NS', 'DOMLETS_PROPERTY', 'TemplateDocument' |
|
|
unicodeJoin = u''.join |
unicodeJoin = u''.join |
|
|
|
|
def infiniter(sequence): |
def infiniter(sequence): |
while 1: |
while 1: |
for item in sequence: |
for item in sequence: |
yield item |
yield item |
|
|
|
|
class DOMletState(binding.Component): |
class DOMletVars(Decorator): |
|
|
"""Execution state for a DOMlet""" |
state = None |
|
|
protocols.advise( |
|
instancesProvide = [IDOMletState], |
|
) |
|
|
|
write = binding.Require("Unicode output stream write() method") |
def traverseTo(self, name, ctx, default=NOT_GIVEN): |
|
loc = traverseItem(ctx, self.state, 'item', name, name, NOT_FOUND) |
|
if loc is not NOT_FOUND: |
|
return loc |
|
|
|
# attribute is absent or private, fall through to underlying object |
|
return traverseDefault(ctx, self.ob, 'attr', name, name, default) |
|
|
def findState(self, iface): |
|
|
|
"""Find nearest DOMletState implementing 'iface'""" |
|
|
|
for c in binding.iterParents(self): # XXX not covered by tests! |
|
state = adapt(c,iface,None) |
|
if state is not None: |
|
return state |
|
|
|
|
class DOMletMethod(object): |
|
"""Bind an 'IDOMletRenderable' to a specific context""" |
|
|
|
protocols.advise( |
|
instancesProvide = [IDOMletRenderable] |
|
) |
|
|
|
__slots__ = 'template','ctx' |
|
|
|
def __init__(self,ctx,template): |
|
self.ctx = ctx |
|
self.template = template |
|
|
|
def renderFor(self,ctx,state): |
|
return self.template.renderFor(self.ctx,state) |
|
|
|
|
|
|
|
|
|
|
|
|
|
class Parameters: |
|
"""'params' object for templates""" |
|
|
|
protocols.advise( instancesProvide = [IWebTraversable] ) |
|
|
|
def __init__(self,ctx,data): |
|
self.ctx = ctx |
|
self.data = data |
|
self.cache = {} |
|
|
|
def traverseTo(self, name, ctx, default=NOT_GIVEN): |
|
try: |
|
item = self.cache[name] |
|
except KeyError: |
|
try: |
|
item = self.data[name] |
|
except KeyError: |
|
if default is not NOT_GIVEN: |
|
return default |
|
raise NotFound(ctx,name,self) |
|
else: |
|
tmpl = IDOMletRenderable(item,None) |
|
if tmpl is not None: |
|
item = self.cache[name] = DOMletMethod(self.ctx,tmpl) |
|
else: |
|
path = adapt(item,TraversalPath,None) |
|
if path is not None: |
|
self.data[name] = path |
|
return path.traverse(self.ctx) |
|
return ctx.childContext(name,item) |
|
|
|
|
|
def beforeHTTP(self, ctx): |
|
return ctx |
|
|
|
def getURL(self,ctx): |
|
return ctx.traversedURL |
|
|
|
|
|
|
|
|
class DOMletAsHTTP(binding.Component): |
class DOMletState(binding.Component): |
|
|
"""Render a template component""" |
"""Execution state for a DOMlet""" |
|
|
protocols.advise( |
protocols.advise( |
instancesProvide = [IHTTPHandler], |
instancesProvide = [IDOMletState], |
asAdapterForProtocols = [IDOMletNode], |
|
factoryMethod = 'fromNode' |
|
) |
) |
|
|
templateNode = binding.Require("""Node to render""") |
write = binding.Require("Unicode output stream write() method") |
|
|
def fromNode(klass, subject): |
|
return klass(templateNode = subject) |
|
|
|
fromNode = classmethod(fromNode) |
data = binding.Make(dict) |
|
|
def handle_http(self, environ, input, errors): |
def __getitem__(self,key): |
myOwner = parentContext(environ) |
return self.data[key] |
|
|
data = [] |
def withData(self,**kw): |
|
data = self.data.copy() |
|
data.update(kw) |
|
return self.__class__(self,data=data,write=self.write) |
|
|
self.templateNode.renderFor( |
def wrapContext(self,ctx): |
myOwner, |
return DOMletVars(ob=ctx, state=self) |
DOMletState(myOwner, write=data.append) |
|
) |
|
|
|
return '200 OK', [], [unicodeJoin(data)] # XXX content-type |
def findState(self, iface): |
|
|
|
"""Find nearest DOMletState implementing 'iface'""" |
|
|
|
for c in binding.iterParents(self): # XXX not covered by tests! |
|
state = adapt(c,iface,None) |
|
if state is not None: |
|
return state |
|
|
|
|
|
|
|
|
|
|
|
|
|
def startElement(parser,data): |
|
|
class ElementAsBuilder(protocols.Adapter): |
parent = data['previous']['pwt.content'] |
|
factory = data.get('this.factory', parent.tagFactory) |
|
|
protocols.advise( |
data['pwt.content'] = outer = factory(parent, |
instancesProvide = [SOX.IXMLBuilder], |
tagName=data['name'], |
asAdapterForProtocols=[IDOMletElement] |
attribItems=data['attributes'], |
|
domletProperty = data.get('this.domlet'), |
|
dataSpec = data.get('this.data',''), |
|
paramName = data.get('this.is'), |
|
) |
|
|
|
inner = data.get('content.factory') or ('content.register' in data and parent.tagFactory) |
|
if inner: |
|
data['pwt.this'] = outer |
|
data['pwt.content'] = inner(outer, |
|
tagName='', |
|
attribItems=[], |
|
domletProperty = data.get('content.domlet'), |
|
dataSpec=data.get('content.data',''), |
|
paramName = data.get('content.is'), |
) |
) |
|
|
def _xml_newTag(self, name,attrs,stack,parser): |
|
self.nsUri = parser.nsInfo |
|
myNs = self.myNs or ('',) # use unprefixed NS if no NS defined |
|
top = self.subject |
|
factory = top.tagFactory |
|
domletName = dataSpec = paramName = None |
|
a = []; append = a.append |
|
|
|
for k,v in attrs: |
def finishElement(parser,data): |
|
content = data['pwt.content'] |
if ':' in k: |
for f in data.get('content.register',()): |
ns, n = k.split(':',1) |
f(content) |
|
if 'pwt.this' in data: |
|
this = data['pwt.this'] |
|
this.addChild(content) |
else: |
else: |
ns, n = '', k |
this = content |
|
for f in data.get('this.register',()): |
|
f(this) |
|
if 'previous' in data: |
|
data['previous']['pwt.content'].addChild(this) |
|
return this |
|
|
|
|
if n=='domlet' and ns in myNs: |
def negotiateDomlet(parser, data, name, value): |
# XXX if domletName is not None or dataSpec is not None: |
data['attributes'].remove((name,value)) |
# XXX raise ??? |
if ':' in value: |
if ':' in v: |
data['this.domlet'],data['this.data'] = value.split(':',1) |
domletName, dataSpec = v.split(':',1) |
domlet = data['this.domlet'] |
else: |
else: |
domletName, dataSpec = v, '' |
data['this.domlet'] = domlet = value |
|
|
if domletName: |
factory = DOMLETS_PROPERTY.of(data['previous']['pwt.content'])[domlet] |
factory = DOMLETS_PROPERTY.of(top)[domletName] |
if data.setdefault('this.factory',factory) is not factory: |
factory = adapt(factory, IDOMletElementFactory) |
parser.err('More than one "domlet" or "this:" replacement defined') |
|
|
elif n=='define' and ns in myNs: |
|
# XXX if paramName is not None: |
|
# XXX raise ??? |
|
paramName = v |
|
else: |
|
append((k,v)) |
|
|
|
element = factory(top, tagName=name, attribItems=a, |
def negotiateDefine(parser, data, name, value): |
domletProperty = domletName or None, dataSpec = dataSpec or '', |
data['attributes'].remove((name,value)) |
paramName = paramName or None, |
data['this.is'] = value |
|
parent = data['previous']['pwt.content'] |
|
data.setdefault('this.register',[]).append( |
|
lambda ob: parent.addParameter(value,ob) |
) |
) |
|
|
if paramName: |
|
top.addParameter(paramName,element) |
|
|
|
return element |
def negotiatorFactory(domletFactory): |
|
def negotiate(mode, parser, data, name, value): |
|
data['attributes'].remove((name,value)) |
|
factory = data.setdefault(mode+'.factory',domletFactory) |
|
if factory is not domletFactory: |
|
parser.err('More than one "domlet" or "this:" replacement defined') |
|
data[mode+'.data'] = value |
|
data[mode+'.domlet'] = parser.splitName(name)[1] |
|
return negotiate |
|
|
|
def nodeIs(mode, parser, data, name, value): |
|
data['attributes'].remove((name,value)) |
|
data[mode+'.is'] = value |
|
data.setdefault(mode+'.register',[]).append( |
|
lambda ob: binding.getParentComponent(ob).addParameter(value,ob) |
|
) |
|
|
|
|
|
|
|
def setupElement(parser,data): |
|
|
def _xml_addChild(self,data): |
d = dict(data.get('attributes',())) |
self.subject.addChild(data) |
|
|
|
def _xml_finish(self): |
if 'domlet' in d: |
return self.subject |
negotiateDomlet(parser,data,'domlet',d['domlet']) |
|
|
def _xml_addText(self,xml): |
if 'define' in d: |
top = self.subject |
negotiateDefine(parser,data,'define',d['define']) |
|
|
|
def text(xml): |
|
top = data['pwt.content'] |
top.addChild(top.textFactory(top,xml=escape(xml))) |
top.addChild(top.textFactory(top,xml=escape(xml))) |
|
|
def _xml_addLiteral(self,xml): |
def literal(xml): |
top = self.subject |
top = data['pwt.content'] |
top.addChild(top.literalFactory(top,xml=xml)) |
top.addChild(top.literalFactory(top,xml=xml)) |
|
|
|
data['start'] = startElement |
|
data['finish'] = finishElement |
|
data['text'] = text |
|
data['literal'] = literal |
|
|
myNs = binding.Make( # prefixes that currently map to TEMPLATE_NS |
|
lambda self: dict( |
|
[(p,1) for (p,u) in self.nsUri.items() if u and u[-1]==TEMPLATE_NS] |
|
), |
|
attrName = 'myNs' |
|
) |
|
|
|
|
def setupDocument(parser,data): |
|
setupElement(parser,data) |
|
data['pwt.content'] = data['pwt_document'] |
|
|
|
|
|
def withParam(parser,data,name,value): |
|
data['attributes'].remove((name,value)) |
|
data.setdefault('content.register',[]).append( |
|
lambda ob: ob.addParameter(name.split(':',1)[-1],value) |
|
) |
|
|
|
|
|
|
|
|
domletProperty = None |
domletProperty = None |
dataSpec = binding.Make(lambda: '', adaptTo=TraversalPath) |
dataSpec = binding.Make(lambda: '', adaptTo=TraversalPath) |
paramName = None |
paramName = None |
acceptParams = binding.Obtain('domletProperty') |
acceptParams = () |
|
multiParams = () |
|
|
# IDOMletNode |
# IDOMletNode |
|
|
|
|
|
|
|
|
|
|
def optimizedChildren(self): |
def optimizedChildren(self): |
|
|
"""Child nodes with as many separate text nodes combined as possible""" |
"""Child nodes with as many separate text nodes combined as possible""" |
|
|
|
|
def _traverse(self, data, state): |
def _traverse(self, data, state): |
|
return self.dataSpec.traverse(data,state.wrapContext), state |
|
|
|
|
|
|
return self.dataSpec.traverse( |
|
data, lambda ctx: self._wrapInteraction(ctx) |
|
), state |
|
|
|
|
|
|
|
self.children.append(node) |
self.children.append(node) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def addParameter(self, name, element): |
def addParameter(self, name, element): |
"""Declare 'element' as part of parameter 'name'""" |
"""Declare 'element' as part of parameter 'name'""" |
if self.acceptParams: |
|
self.params.setdefault(name,[]).append(element) |
if not self.acceptParams: |
|
return self.getParentComponent().addParameter(name,element) |
|
|
|
if name not in self.acceptParams and '*' not in self.acceptParams: |
|
# XXX need line info |
|
raise SyntaxError("Unrecognized parameter: %r" % name) |
|
|
|
is_multi = ( |
|
name in self.multiParams or |
|
name not in self.acceptParams and '*' in self.multiParams |
|
) |
|
|
|
if name in self.params: |
|
|
|
if not is_multi: |
|
raise SyntaxError( |
|
"Multiple definitions for parameter: %r" % name |
|
) # XXX need line info |
|
|
|
self.params[name].append(element) |
|
|
|
elif is_multi: |
|
self.params[name] = [element] |
|
|
else: |
else: |
self.getParentComponent().addParameter(name,element) |
self.params[name] = element |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Override in subclasses |
|
|
|
def _wrapInteraction(self,interaction): |
|
# XXX This should wrap the interaction in an IWebTraversable simulator, |
|
# XXX which should include access to this element's parameters as well |
|
# XXX as interaction variables. |
|
raise NotImplementedError |
|
|
|
|
|
|
|
|
|
|
# Override in subclasses |
|
|
_emptyTag = binding.Make( |
_emptyTag = binding.Make( |
lambda self: self._openTag[:-1]+u' />' |
lambda self: self.tagName and self._openTag[:-1]+u' />' or '' |
) |
) |
|
|
_closeTag = binding.Make( |
_closeTag = binding.Make( |
lambda self: u'</%s>' % self.tagName |
lambda self: self.tagName and u'</%s>' % self.tagName or '' |
) |
) |
|
|
_openTag = binding.Make( |
_openTag = binding.Make( |
lambda self: u'<%s%s>' % ( self.tagName, |
lambda self: self.tagName and u'<%s%s>' % ( self.tagName, |
unicodeJoin([ |
unicodeJoin([ |
u' %s=%s' % (k,quoteattr(v)) for (k,v) in self.attribItems |
u' %s=%s' % (k,quoteattr(v)) for (k,v) in self.attribItems |
]) |
]) |
|
) or '' |
) |
) |
) |
|
|
|
tagFactory = None # real value is set below |
tagFactory = None # real value is set below |
textFactory = Literal |
textFactory = Literal |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TaglessElement(Element): |
class TaglessElement(Element): |
|
|
"""Element w/out tags""" |
"""Element w/out tags""" |
_openTag = _closeTag = _emptyTag = '' |
_openTag = _closeTag = _emptyTag = '' |
|
|
|
|
|
class Uses(Element): |
|
|
|
"""Render child elements with target data, or skip element altogether""" |
|
|
|
staticText = None |
|
render_if = True |
|
|
|
def renderFor(self, data, state): |
|
try: |
|
if self.dataSpec: |
|
data, state = self._traverse(data, state) |
|
except (web.NotFound,web.NotAllowed): |
|
if self.render_if: |
|
return |
|
else: |
|
if not self.render_if: |
|
return |
|
|
|
state.write(self._openTag) |
|
|
|
for child in self.optimizedChildren: |
|
child.renderFor(data,state) |
|
|
|
state.write(self._closeTag) |
|
|
|
|
|
class Unless(Uses): |
|
|
|
"""Skip child elements if target data is available""" |
|
|
|
render_if = False |
|
|
|
|
|
|
class TemplateDocument(TaglessElement): |
class TemplateDocument(TaglessElement): |
|
|
"""Document-level template element""" |
"""Document-level template element""" |
|
|
parserClass = SOX.ExpatBuilder |
protocols.advise( |
|
instancesProvide = [IHTTPHandler], |
|
classProvides = [naming.IObjectFactory], |
|
) |
|
|
|
acceptParams = '*', # handle any top-level parameters |
|
|
acceptParams = True # handle any top-level parameters |
def renderFor(self, ctx, state): |
|
if not self.fragment: |
|
raise TypeError("Can't be used as a fragment") |
|
return self.fragment.renderFor(ctx.parentContext(),state) |
|
|
|
def handle_http(self, ctx): |
|
name = ctx.shift() |
|
if name is not None: |
|
raise web.NotFound(ctx,name,self) # No traversal to subobjects! |
|
if not self.page: |
|
raise web.UnsupportedMethod(ctx) # We're not a page! |
|
data = [] |
|
self.page.renderFor( |
|
ctx.parentContext(), DOMletState(self, write=data.append) |
|
) |
|
h = [] |
|
if self.content_type: |
|
h.append(('Content-type',self.content_type)) |
|
return '200 OK', h, [str(unicodeJoin(data))] # XXX encoding |
|
|
def parseFile(self, stream): |
|
self.parserClass().parseFile(stream,self) |
|
|
|
|
def getObjectInstance(klass, context, refInfo, name, attrs=None): |
|
url, = refInfo.addresses |
|
return config.processXML( |
|
web.TEMPLATE_SCHEMA(context),str(url),pwt_document=klass(context), |
|
) |
|
|
|
getObjectInstance = classmethod(getObjectInstance) |
|
|
|
|
|
content_type = binding.Make(lambda self: |
|
str(self.params.get('content-type')) |
|
) |
|
|
|
def layoutDOMlet(self,d,attrName): |
|
|
|
if attrName+'-layout' in self.params: |
|
path = self.params[attrName+'-layout'] + '' # ensure stringness |
|
if path=='/nothing': |
|
return None |
|
elif path=='/default': |
|
return super(TemplateDocument,self) |
|
else: |
|
return Replace(self, dataSpec=path, params=self.params.copy()) |
|
|
|
if attrName in self.params: |
|
return IDOMletRenderable(self.params[attrName]) |
|
|
|
if attrName=='fragment': |
|
# It's okay to be a fragment by default |
|
return super(TemplateDocument,self) |
|
|
|
fragment = page = binding.Make(layoutDOMlet) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Replace(Element): |
|
|
|
staticText = None |
|
acceptParams = '*', |
|
escaped = True |
|
|
|
def renderFor(self,data,state): |
|
|
|
if self.dataSpec: |
|
ctx, state = self._traverse(data, state) |
|
|
|
if self.params: |
|
state = state.withData(params=Parameters(data,self.params)) |
|
|
|
current = ctx.current |
|
|
|
domlet = IDOMletRenderable(current,None) |
|
if domlet is not None: |
|
return domlet.renderFor(ctx,state) |
|
|
|
# XXX dyn var comp goes here |
|
# XXX if NOT_FOUND -> return |
|
# XXX if NOT_GIVEN -> render original content |
|
|
|
current = unicode(current) |
|
if self.escaped: |
|
current = escape(current) |
|
|
|
state.write(current) |
|
|
|
|
|
class ReplaceXML(Replace): |
|
escaped = False |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ContentReplacer(Element): |
class ContentReplacer(Element): |
|
|
"""Abstract base for elements that replace their contents""" |
"""Abstract base for elements that replace their contents""" |
|
|
write = state.write |
write = state.write |
write(self._openTag) |
write(self._openTag) |
write(escape(unicode(getCurrent(data)))) |
write(escape(unicode(data.current))) |
write(self._closeTag) |
write(self._closeTag) |
|
|
|
|
|
|
write = state.write |
write = state.write |
write(self._openTag) |
write(self._openTag) |
write(unicode(getCurrent(data))) |
write(unicode(data.current)) |
write(self._closeTag) |
write(self._closeTag) |
|
|
|
|
_openTag = _closeTag = _emptyTag = '' |
_openTag = _closeTag = _emptyTag = '' |
|
|
|
|
|
class Expects(Element): |
|
|
|
"""Render child elements with target data, or skip element altogether""" |
|
|
|
staticText = None |
|
|
|
dataSpec = '' # to disable conversion to path |
|
|
|
protocol = binding.Make( |
|
lambda self: imports.importString(self.dataSpec),uponAssembly=True |
|
) |
|
|
|
def renderFor(self, data, state): |
|
|
|
data = data.clone(current=adapt(data.current,self.protocol)) |
|
|
|
state.write(self._openTag) |
|
for child in self.optimizedChildren: |
|
child.renderFor(data,state) |
|
state.write(self._closeTag) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if self.dataSpec: |
if self.dataSpec: |
data, state = self._traverse(data, state) |
data, state = self._traverse(data, state) |
|
|
url = unicode(getAbsoluteURL(data)) |
url = unicode(data.url) |
|
|
if not self.optimizedChildren and not self.nonEmpty: |
if not self.optimizedChildren and not self.nonEmpty: |
state.write(self._emptyTag % locals()) |
state.write(self._emptyTag % locals()) |
write = state.write |
write = state.write |
|
|
write(self._openTag) |
write(self._openTag) |
write(unicode(getAbsoluteURL(data))) |
write(unicode(data.url)) |
write(self._closeTag) |
write(self._closeTag) |
|
|
|
class TaglessURLText(URLText): |
|
_openTag = _closeTag = _emptyTag = '' |
|
|
def URLTag(parentComponent, componentName=None, domletProperty=None, **kw): |
def URLTag(parentComponent, componentName=None, domletProperty=None, **kw): |
|
|
|
|
class List(ContentReplacer): |
class List(ContentReplacer): |
|
|
|
acceptParams = 'listItem','header','emptyList','footer' |
|
multiParams = 'listItem', |
|
|
def renderFor(self, data, state): |
def renderFor(self, data, state): |
|
|
if self.dataSpec: |
if self.dataSpec: |
data, state = self._traverse(data, state) |
data, state = self._traverse(data, state) |
|
|
state.write(self._openTag) |
state.write(self._openTag) |
|
|
nextPattern = infiniter(self.params['listItem']).next |
nextPattern = infiniter(self.params['listItem']).next |
allowed = getInteraction(data).allows |
allowed = data.allows |
ct = 0 |
ct = 0 |
|
|
# XXX this should probably use an iteration location, or maybe |
# XXX this should probably use an iteration location, or maybe |
# XXX put some properties in execution context for loop vars? |
# XXX put some properties in execution context for loop vars? |
|
|
for item in getCurrent(data): |
for item in data.current: |
|
|
if not allowed(item): |
if not allowed(item): |
continue |
continue |
|
|
if not ct: |
if not ct: |
for child in self.params.get('header',()): |
if 'header' in self.params: |
child.renderFor(data,state) |
self.params['header'].renderFor(data,state) |
|
|
loc = childContext(data, str(ct), item) |
loc = data.childContext(str(ct), item) |
nextPattern().renderFor(loc, state) |
nextPattern().renderFor(loc, state) |
ct += 1 |
ct += 1 |
|
|
if not ct: |
if not ct: |
# Handle list being empty |
# Handle list being empty |
for child in self.params.get('emptyList',()): |
if 'emptyList' in self.params: |
child.renderFor(data, state) |
self.params['emptyList'].renderFor(data,state) |
else: |
else: |
for child in self.params.get('footer',()): |
if 'footer' in self.params: |
child.renderFor(data,state) |
self.params['footer'].renderFor(data,state) |
|
|
state.write(self._closeTag) |
state.write(self._closeTag) |
|
|
|
class TaglessList(List): |
|
_openTag = _closeTag = _emptyTag = '' |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|