|
|
@ -1,92 +1,30 @@
|
|
|
|
require 'parslet'
|
|
|
|
require 'parslet'
|
|
|
|
require 'sanitize'
|
|
|
|
require 'sanitize'
|
|
|
|
require 'uri'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
module Bristlecode
|
|
|
|
module Bristlecode
|
|
|
|
|
|
|
|
|
|
|
|
class YoutubeFilter
|
|
|
|
|
|
|
|
def call(env)
|
|
|
|
|
|
|
|
node = env[:node]
|
|
|
|
|
|
|
|
node_name = env[:node_name]
|
|
|
|
|
|
|
|
return if env[:is_whitelisted] || !node.element?
|
|
|
|
|
|
|
|
return unless node_name == 'iframe'
|
|
|
|
|
|
|
|
return unless node['src'] =~ %r|\A(?:https?:)?//(?:www\.)?youtube(?:-nocookie)?\.com/|
|
|
|
|
|
|
|
|
Sanitize.node!(node, {
|
|
|
|
|
|
|
|
:elements => %w[iframe],
|
|
|
|
|
|
|
|
:attributes => {'iframe' => %w[allowfullscreen frameborder height src width]}
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
{:node_whitelist => [node]}
|
|
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TweetFilter
|
|
|
|
|
|
|
|
def call(env)
|
|
|
|
|
|
|
|
node = env[:node]
|
|
|
|
|
|
|
|
node_name = env[:node_name]
|
|
|
|
|
|
|
|
return if env[:is_whitelisted] || !node.element?
|
|
|
|
|
|
|
|
case node_name
|
|
|
|
|
|
|
|
when 'script'
|
|
|
|
|
|
|
|
return script env
|
|
|
|
|
|
|
|
when 'blockquote'
|
|
|
|
|
|
|
|
return blockquote env
|
|
|
|
|
|
|
|
else
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def script(env)
|
|
|
|
|
|
|
|
node = env[:node]
|
|
|
|
|
|
|
|
return unless node['src'] == "//platform.twitter.com/widgets.js"
|
|
|
|
|
|
|
|
Sanitize.node!(node, {
|
|
|
|
|
|
|
|
:elements => %w[script],
|
|
|
|
|
|
|
|
:attributes => {'script' => %w[aync src charset]}
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
{:node_whitelist => [node]}
|
|
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def blockquote(env)
|
|
|
|
|
|
|
|
node = env[:node]
|
|
|
|
|
|
|
|
Sanitize.node!(node, {
|
|
|
|
|
|
|
|
:elements => %w[blockquote a],
|
|
|
|
|
|
|
|
:attributes => {'blockquote' => ['class'], 'a' => ['href']}
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
{:node_whitelist => [node]}
|
|
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Config = Sanitize::Config::freeze_config(
|
|
|
|
Config = Sanitize::Config::freeze_config(
|
|
|
|
:elements => %w[b em i strong u a strike br img],
|
|
|
|
:elements => %w[b em i strong u a strike br],
|
|
|
|
:attributes => {
|
|
|
|
:attributes => {
|
|
|
|
'a' => ['href'],
|
|
|
|
'a' => ['href']
|
|
|
|
'img' => ['src'],
|
|
|
|
|
|
|
|
},
|
|
|
|
},
|
|
|
|
:add_attributes => {
|
|
|
|
:add_attributes => {
|
|
|
|
'a' => {'rel' => 'nofollow'}
|
|
|
|
'a' => {'rel' => 'nofollow'}
|
|
|
|
},
|
|
|
|
},
|
|
|
|
:protocols => {
|
|
|
|
:protocols => {
|
|
|
|
'a' => {'href' => ['http', 'https', :relative]}
|
|
|
|
'a' => {'href' => ['http', 'https', :relative]}
|
|
|
|
},
|
|
|
|
}
|
|
|
|
:transformers => [YoutubeFilter.new, TweetFilter.new],
|
|
|
|
|
|
|
|
:remove_contents => ['script']
|
|
|
|
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def Bristlecode.to_html(text)
|
|
|
|
def Bristlecode.to_html(text)
|
|
|
|
begin
|
|
|
|
|
|
|
|
parser = Bristlecode::Parser.new
|
|
|
|
parser = Bristlecode::Parser.new
|
|
|
|
parse_tree = parser.parse(text)
|
|
|
|
parse_tree = parser.parse(text)
|
|
|
|
tree = Bristlecode::Transform.new.apply(parse_tree)
|
|
|
|
tree = Bristlecode::Transform.new.apply(parse_tree)
|
|
|
|
html = tree.to_html
|
|
|
|
html = tree.to_html
|
|
|
|
rescue Parslet::ParseFailed => parse_error
|
|
|
|
|
|
|
|
html = text
|
|
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
Bristlecode.sanitize_html(html)
|
|
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def Bristlecode.sanitize_html(html)
|
|
|
|
|
|
|
|
Sanitize.fragment(html, Bristlecode::Config)
|
|
|
|
Sanitize.fragment(html, Bristlecode::Config)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
def Bristlecode.clean!(text)
|
|
|
|
def Bristlecode.clean(text)
|
|
|
|
text.gsub!('&', '&')
|
|
|
|
text.gsub!('&', '&')
|
|
|
|
text.gsub!('<', '<')
|
|
|
|
text.gsub!('<', '<')
|
|
|
|
text.gsub!('>', '>')
|
|
|
|
text.gsub!('>', '>')
|
|
|
@ -96,6 +34,9 @@ module Bristlecode
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
class Parser < Parslet::Parser
|
|
|
|
class Parser < Parslet::Parser
|
|
|
|
|
|
|
|
rule(:space) { match('\s').repeat(1) }
|
|
|
|
|
|
|
|
rule(:space?) { space.maybe }
|
|
|
|
|
|
|
|
|
|
|
|
rule(:bold_open) { str('[b]') | str('[B]') }
|
|
|
|
rule(:bold_open) { str('[b]') | str('[B]') }
|
|
|
|
rule(:bold_close) { str('[/b]') | str('[/B]') | eof }
|
|
|
|
rule(:bold_close) { str('[/b]') | str('[/B]') | eof }
|
|
|
|
rule(:bold) { bold_open >> children.as(:bold) >> bold_close }
|
|
|
|
rule(:bold) { bold_open >> children.as(:bold) >> bold_close }
|
|
|
@ -111,7 +52,7 @@ module Bristlecode
|
|
|
|
rule(:simple_href) { (url_close.absent? >> any).repeat }
|
|
|
|
rule(:simple_href) { (url_close.absent? >> any).repeat }
|
|
|
|
rule(:simple_url) { url_open >> simple_href.as(:href) >> url_close }
|
|
|
|
rule(:simple_url) { url_open >> simple_href.as(:href) >> url_close }
|
|
|
|
rule(:url_title_open) { str('[url=') }
|
|
|
|
rule(:url_title_open) { str('[url=') }
|
|
|
|
rule(:url_title_href) { (match(']').absent? >> any).repeat(1) }
|
|
|
|
rule(:url_title_href) { (match(']').absent? >> any).repeat }
|
|
|
|
rule(:url_with_title) {
|
|
|
|
rule(:url_with_title) {
|
|
|
|
url_title_open >>
|
|
|
|
url_title_open >>
|
|
|
|
url_title_href.as(:href) >>
|
|
|
|
url_title_href.as(:href) >>
|
|
|
@ -121,33 +62,16 @@ module Bristlecode
|
|
|
|
}
|
|
|
|
}
|
|
|
|
rule(:url) { (simple_url | url_with_title).as(:url) }
|
|
|
|
rule(:url) { (simple_url | url_with_title).as(:url) }
|
|
|
|
|
|
|
|
|
|
|
|
rule(:youtube_open) { str('[youtube]') }
|
|
|
|
|
|
|
|
rule(:youtube_close) { str('[/youtube]') }
|
|
|
|
|
|
|
|
rule(:youtube_url) { (youtube_close.absent? >> any).repeat(1) }
|
|
|
|
|
|
|
|
rule(:youtube) { (youtube_open >> youtube_url.as(:src) >> youtube_close).as(:youtube) }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
rule(:tweet_open) { str('[tweet]') }
|
|
|
|
|
|
|
|
rule(:tweet_close) { str('[/tweet]') }
|
|
|
|
|
|
|
|
rule(:tweet_url) { (tweet_close.absent? >> any).repeat(1) }
|
|
|
|
|
|
|
|
rule(:tweet) { (tweet_open >> tweet_url.as(:src) >> tweet_close).as(:tweet) }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
rule(:img_open) { str('[img]') }
|
|
|
|
|
|
|
|
rule(:img_close) { str('[/img]') }
|
|
|
|
|
|
|
|
rule(:img_src) { (img_close.absent? >> any).repeat(1) }
|
|
|
|
|
|
|
|
rule(:img) { (img_open >> img_src.as(:src) >> img_close).as(:img) }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
rule(:eof) { any.absent? }
|
|
|
|
rule(:eof) { any.absent? }
|
|
|
|
rule(:tag) { bold | italic | url | linebreak | img | youtube | tweet }
|
|
|
|
rule(:tag) { bold | italic | url | linebreak }
|
|
|
|
rule(:elem) { text.as(:text) | tag }
|
|
|
|
rule(:elem) { text.as(:text) | tag }
|
|
|
|
rule(:tag_open) { bold_open | italic_open | url_open | url_title_open | img_open |
|
|
|
|
rule(:tag_open) { bold_open | italic_open | url_open | url_title_open }
|
|
|
|
youtube_open | tweet_open }
|
|
|
|
rule(:tag_close) { bold_close | italic_close | url_close }
|
|
|
|
rule(:tag_close) { bold_close | italic_close | url_close | img_close | youtube_close |
|
|
|
|
|
|
|
|
tweet_close }
|
|
|
|
|
|
|
|
rule(:tag_delim) { tag_open | tag_close | linebreak }
|
|
|
|
rule(:tag_delim) { tag_open | tag_close | linebreak }
|
|
|
|
|
|
|
|
|
|
|
|
rule(:text) { (tag_delim.absent? >> any).repeat(1) }
|
|
|
|
rule(:text) { (tag_delim.absent? >> any).repeat(1) }
|
|
|
|
rule(:children) { elem.repeat }
|
|
|
|
rule(:children) { space? >> elem.repeat }
|
|
|
|
rule(:doc) { elem.repeat.as(:doc) }
|
|
|
|
rule(:doc) { space? >> elem.repeat.as(:doc) }
|
|
|
|
root(:doc)
|
|
|
|
root(:doc)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
@ -158,9 +82,6 @@ module Bristlecode
|
|
|
|
rule(doc: subtree(:doc)) { Doc.new(doc) }
|
|
|
|
rule(doc: subtree(:doc)) { Doc.new(doc) }
|
|
|
|
rule(url: subtree(:url)) { Url.new(url) }
|
|
|
|
rule(url: subtree(:url)) { Url.new(url) }
|
|
|
|
rule(br: simple(:br)) { Linebreak.new }
|
|
|
|
rule(br: simple(:br)) { Linebreak.new }
|
|
|
|
rule(img: subtree(:img)) { Img.new(img) }
|
|
|
|
|
|
|
|
rule(youtube: subtree(:youtube)) { Youtube.new(youtube) }
|
|
|
|
|
|
|
|
rule(tweet: subtree(:tweet)) { Tweet.new(tweet) }
|
|
|
|
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
class Doc
|
|
|
|
class Doc
|
|
|
@ -175,29 +96,19 @@ module Bristlecode
|
|
|
|
children.each{|child| s << child.to_html }
|
|
|
|
children.each{|child| s << child.to_html }
|
|
|
|
s.string
|
|
|
|
s.string
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
def to_text
|
|
|
|
|
|
|
|
s = StringIO.new
|
|
|
|
|
|
|
|
children.each{|child| s << child.to_text }
|
|
|
|
|
|
|
|
s.string
|
|
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
class Text
|
|
|
|
class Text
|
|
|
|
attr_accessor :text
|
|
|
|
attr_accessor :text
|
|
|
|
|
|
|
|
|
|
|
|
def initialize(text)
|
|
|
|
def initialize(text)
|
|
|
|
self.text = text.to_str
|
|
|
|
self.text = text.to_str.strip
|
|
|
|
Bristlecode.clean!(self.text)
|
|
|
|
Bristlecode.clean(self.text)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
def to_html
|
|
|
|
def to_html
|
|
|
|
text
|
|
|
|
text
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
def to_text
|
|
|
|
|
|
|
|
text
|
|
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
class Bold
|
|
|
|
class Bold
|
|
|
@ -210,10 +121,6 @@ module Bristlecode
|
|
|
|
def to_html
|
|
|
|
def to_html
|
|
|
|
"<b>#{children.to_html}</b>"
|
|
|
|
"<b>#{children.to_html}</b>"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
def to_text
|
|
|
|
|
|
|
|
"[b]#{children.to_text}[/b]"
|
|
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
class Italic
|
|
|
|
class Italic
|
|
|
@ -226,145 +133,35 @@ module Bristlecode
|
|
|
|
def to_html
|
|
|
|
def to_html
|
|
|
|
"<i>#{children.to_html}</i>"
|
|
|
|
"<i>#{children.to_html}</i>"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
def to_text
|
|
|
|
|
|
|
|
"[i]#{children.to_text}[/i]"
|
|
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
class Url
|
|
|
|
class Url
|
|
|
|
attr_accessor :href, :title, :bad_href, :title_supplied
|
|
|
|
attr_accessor :href, :title
|
|
|
|
|
|
|
|
|
|
|
|
def initialize(args)
|
|
|
|
def initialize(args)
|
|
|
|
self.href = args[:href].to_str.strip
|
|
|
|
self.href = args[:href].to_str.strip
|
|
|
|
|
|
|
|
check_href
|
|
|
|
if args.has_key? :title
|
|
|
|
if args.has_key? :title
|
|
|
|
self.title_supplied = true
|
|
|
|
|
|
|
|
self.title = Doc.new(args[:title])
|
|
|
|
self.title = Doc.new(args[:title])
|
|
|
|
else
|
|
|
|
else
|
|
|
|
self.title_supplied = false
|
|
|
|
self.title = Text.new(href)
|
|
|
|
self.title = Text.new(args[:href].to_str.strip)
|
|
|
|
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
def href_ok?
|
|
|
|
def check_href
|
|
|
|
href =~ /^(\/|https?:\/\/)/
|
|
|
|
unless href =~ /^(\/[^\/]|https?:\/\/)/
|
|
|
|
|
|
|
|
raise "href must start with /, http, or https"
|
|
|
|
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
def to_html
|
|
|
|
def to_html
|
|
|
|
return to_text unless href_ok?
|
|
|
|
|
|
|
|
"<a href=\"#{href}\">#{title.to_html}</a>"
|
|
|
|
"<a href=\"#{href}\">#{title.to_html}</a>"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
def to_text
|
|
|
|
|
|
|
|
if title_supplied
|
|
|
|
|
|
|
|
"[url=#{href}]#{title.to_text}[/url]"
|
|
|
|
|
|
|
|
else
|
|
|
|
|
|
|
|
text = "[url]#{href}[/url]"
|
|
|
|
|
|
|
|
Bristlecode.clean!(text)
|
|
|
|
|
|
|
|
text
|
|
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
class Linebreak
|
|
|
|
class Linebreak
|
|
|
|
def to_html
|
|
|
|
def to_html
|
|
|
|
"<br>"
|
|
|
|
"<br>"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
def to_text
|
|
|
|
|
|
|
|
"[br]"
|
|
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Img
|
|
|
|
|
|
|
|
attr_accessor :src
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def initialize(img)
|
|
|
|
|
|
|
|
self.src = img[:src].to_str
|
|
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def src_ok?
|
|
|
|
|
|
|
|
src =~ /^(\/|https?:\/\/)/
|
|
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def to_html
|
|
|
|
|
|
|
|
return to_text unless src_ok?
|
|
|
|
|
|
|
|
"<img src=\"#{src}\">"
|
|
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def to_text
|
|
|
|
|
|
|
|
text = "[img]#{src}[/img]"
|
|
|
|
|
|
|
|
Bristlecode.clean!(text)
|
|
|
|
|
|
|
|
text
|
|
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Youtube
|
|
|
|
|
|
|
|
attr_accessor :raw_url, :video_id
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def initialize(args)
|
|
|
|
|
|
|
|
self.raw_url = args[:src].to_str.strip
|
|
|
|
|
|
|
|
self.video_id = parse_url
|
|
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse_url
|
|
|
|
|
|
|
|
begin
|
|
|
|
|
|
|
|
uri = URI::parse(raw_url)
|
|
|
|
|
|
|
|
return false unless ['http', 'https'].include? uri.scheme
|
|
|
|
|
|
|
|
return false unless ['www.youtube.com', 'youtube.com', 'youtu.be'].include? uri.host
|
|
|
|
|
|
|
|
if uri.host == 'youtu.be'
|
|
|
|
|
|
|
|
return uri.path[1..-1]
|
|
|
|
|
|
|
|
else
|
|
|
|
|
|
|
|
URI::decode_www_form(uri.query).each{|key, value| return value if key == 'v'}
|
|
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
rescue URI::InvalidURIError
|
|
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def to_html
|
|
|
|
|
|
|
|
return to_text unless video_id
|
|
|
|
|
|
|
|
"<iframe width=\"560\" height=\"315\" src=\"https://www.youtube.com/embed/#{video_id}\" frameborder=\"0\" allowfullscreen></iframe>"
|
|
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def to_text
|
|
|
|
|
|
|
|
text = "[youtube]#{raw_url}[/youtube]"
|
|
|
|
|
|
|
|
Bristlecode.clean!(text)
|
|
|
|
|
|
|
|
text
|
|
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Tweet
|
|
|
|
|
|
|
|
attr_accessor :raw_url, :tweet_url
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def initialize(tweet)
|
|
|
|
|
|
|
|
self.raw_url = tweet[:src].to_str.strip
|
|
|
|
|
|
|
|
self.tweet_url = parse_url(self.raw_url)
|
|
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse_url(url_in)
|
|
|
|
|
|
|
|
begin
|
|
|
|
|
|
|
|
uri = URI::parse(url_in)
|
|
|
|
|
|
|
|
return false unless ['http', 'https'].include? uri.scheme
|
|
|
|
|
|
|
|
return false unless uri.host == 'twitter.com'
|
|
|
|
|
|
|
|
return false unless uri.path =~ /^\/[^\/]+\/status\/\d+/
|
|
|
|
|
|
|
|
# strip querystring and fragment
|
|
|
|
|
|
|
|
return "#{uri.scheme}://#{uri.host}#{uri.path}"
|
|
|
|
|
|
|
|
rescue URI::InvalidURIError
|
|
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def to_html
|
|
|
|
|
|
|
|
return to_text unless tweet_url
|
|
|
|
|
|
|
|
"<blockquote class=\"twitter-tweet\"><a href=\"#{tweet_url}\"></a></blockquote><script async src=\"//platform.twitter.com/widgets.js\" charset=\"utf-8\"></script>"
|
|
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def to_text
|
|
|
|
|
|
|
|
text = "[tweet]#{raw_url}[/tweet]"
|
|
|
|
|
|
|
|
Bristlecode.clean!(text)
|
|
|
|
|
|
|
|
text
|
|
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|