laze 0.2.0
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/.document +5 -0
- data/.gitignore +22 -0
- data/LICENSE +20 -0
- data/README.rdoc +150 -0
- data/Rakefile +76 -0
- data/VERSION.yml +5 -0
- data/bin/laze +126 -0
- data/examples/website/includes/tagline.html +1 -0
- data/examples/website/input/contact/email.md +6 -0
- data/examples/website/input/css/screen.css +5 -0
- data/examples/website/input/css/test.less +6 -0
- data/examples/website/input/img/ruby.gif +0 -0
- data/examples/website/input/img/test.jpg +0 -0
- data/examples/website/input/img/test.png +0 -0
- data/examples/website/input/index.md +13 -0
- data/examples/website/input/js/foo.js +1 -0
- data/examples/website/input/js/lib.js +5 -0
- data/examples/website/layouts/default.html +15 -0
- data/examples/website/layouts/subpage.html +5 -0
- data/examples/website/laze.yml +1 -0
- data/features/create_sites.feature +73 -0
- data/features/plugins.feature +49 -0
- data/features/site_data.feature +26 -0
- data/features/step_definitions/laze_steps.rb +77 -0
- data/features/support/env.rb +17 -0
- data/lib/laze.rb +64 -0
- data/lib/laze/asset.rb +7 -0
- data/lib/laze/core_extensions.rb +15 -0
- data/lib/laze/item.rb +59 -0
- data/lib/laze/javascript.rb +5 -0
- data/lib/laze/layout.rb +105 -0
- data/lib/laze/page.rb +33 -0
- data/lib/laze/plugins.rb +56 -0
- data/lib/laze/plugins/cache_buster.rb +49 -0
- data/lib/laze/plugins/css_imports.rb +42 -0
- data/lib/laze/plugins/cssmin.rb +31 -0
- data/lib/laze/plugins/image_check.rb +28 -0
- data/lib/laze/plugins/image_optimizer.rb +52 -0
- data/lib/laze/plugins/js_requires.rb +63 -0
- data/lib/laze/plugins/jsmin.rb +31 -0
- data/lib/laze/plugins/less.rb +35 -0
- data/lib/laze/plugins/robots.rb +28 -0
- data/lib/laze/plugins/sitemap.rb +63 -0
- data/lib/laze/renderer.rb +47 -0
- data/lib/laze/renderers/javascript_renderer.rb +16 -0
- data/lib/laze/renderers/page_renderer.rb +40 -0
- data/lib/laze/renderers/stylesheet_renderer.rb +16 -0
- data/lib/laze/secretary.rb +74 -0
- data/lib/laze/section.rb +39 -0
- data/lib/laze/store.rb +51 -0
- data/lib/laze/stores/filesystem.rb +127 -0
- data/lib/laze/stylesheet.rb +6 -0
- data/lib/laze/target.rb +56 -0
- data/lib/laze/targets/filesystem.rb +41 -0
- data/test/helper.rb +12 -0
- data/test/test_assets.rb +28 -0
- data/test/test_core_extensions.rb +19 -0
- data/test/test_item.rb +59 -0
- data/test/test_layout.rb +47 -0
- data/test/test_renderer.rb +71 -0
- data/test/test_renderers.rb +40 -0
- data/test/test_secretary.rb +48 -0
- data/test/test_store.rb +18 -0
- data/test/test_target.rb +84 -0
- metadata +207 -0
data/lib/laze/layout.rb
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
module Laze
|
2
|
+
# A layout is a special kind of file that can be used to remove duplication
|
3
|
+
# of HTML boilerplate code. Store a header and footer of a page in a layout
|
4
|
+
# and simply wrap that around a given piece of content to create a page.
|
5
|
+
#
|
6
|
+
# == Inserting content into a layout
|
7
|
+
#
|
8
|
+
# A layout can contain anything you like. The only rule is you should insert
|
9
|
+
# an insertion point for the content of a page. Here's a simple example:
|
10
|
+
#
|
11
|
+
# <html>
|
12
|
+
# <head>
|
13
|
+
# <title>My awesome website</title>
|
14
|
+
# </head>
|
15
|
+
# <body>
|
16
|
+
# {{ yield }}
|
17
|
+
# </body>
|
18
|
+
# </html>
|
19
|
+
#
|
20
|
+
# When your page's content is "Welcome" your resulting page output would be:
|
21
|
+
#
|
22
|
+
# <html>
|
23
|
+
# <head>
|
24
|
+
# <title>My awesome website</title>
|
25
|
+
# </head>
|
26
|
+
# <body>
|
27
|
+
# Welcome
|
28
|
+
# </body>
|
29
|
+
# </html>
|
30
|
+
#
|
31
|
+
# Laze does its best to preserve whitespace in your content.
|
32
|
+
#
|
33
|
+
# == Nested templates
|
34
|
+
#
|
35
|
+
# You can nest multiple layouts, i.e. a layout can itself have a layout.
|
36
|
+
# This way you can create a master template for your site, with
|
37
|
+
# subtemplates for various page types.
|
38
|
+
#
|
39
|
+
# Simply give your layout file a layout like so:
|
40
|
+
#
|
41
|
+
# layout: my_layout
|
42
|
+
# ---
|
43
|
+
# <html>
|
44
|
+
# <head>
|
45
|
+
# <title>My awesome website</title>
|
46
|
+
# </head>
|
47
|
+
# <body>
|
48
|
+
# {{ yield }}
|
49
|
+
# </body>
|
50
|
+
# </html>
|
51
|
+
#
|
52
|
+
# == Template data in layout files
|
53
|
+
#
|
54
|
+
# You can use any variables defined in your pages in your layouts, and
|
55
|
+
# the other way around. You could, for example, include the page title in
|
56
|
+
# your <tt><title></tt> element:
|
57
|
+
#
|
58
|
+
# <html>
|
59
|
+
# <head>
|
60
|
+
# <title>{{ page.title }} - My awesome website</title>
|
61
|
+
# </head>
|
62
|
+
# <body>
|
63
|
+
# {{ yield }}
|
64
|
+
# </body>
|
65
|
+
# </html>
|
66
|
+
#
|
67
|
+
class Layout < Item
|
68
|
+
|
69
|
+
# Regex matching where to insert a layout's content
|
70
|
+
YIELD = /\{\{\s*yield\s*\}\}/
|
71
|
+
|
72
|
+
# Special regex matching an insertion point on a single blank line. In
|
73
|
+
# this case we can preserve whitespace indents.
|
74
|
+
SINGLE_LINE_YIELD = /^(\s+)#{YIELD}\s*$/
|
75
|
+
|
76
|
+
# Apply this layout to a piece of text, returning this layout's content
|
77
|
+
# with the +YIELD+ replacd with the given string.
|
78
|
+
#
|
79
|
+
# If the +YIELD+ is on a single line that is matched by +SINGLE_LINE_YIELD+
|
80
|
+
# it will prefix every line in +string+ with the whitespace indent of the
|
81
|
+
# +YIELD+.
|
82
|
+
def wrap(string)
|
83
|
+
if content =~ SINGLE_LINE_YIELD
|
84
|
+
whitespace = $1
|
85
|
+
content.sub(SINGLE_LINE_YIELD, string.split(/\n/).map { |l| whitespace + l }.join("\n"))
|
86
|
+
else
|
87
|
+
content.sub(YIELD, string)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# Return this layout's own layout, to enable nested layouts. Simple
|
92
|
+
# shortcut function.
|
93
|
+
def layout
|
94
|
+
properties[:layout]
|
95
|
+
end
|
96
|
+
|
97
|
+
# Use the currently active data store to look for a layout by a given
|
98
|
+
# name. Returns +nil+ when given +nil+, and a new layout instance
|
99
|
+
# otherwise.
|
100
|
+
def self.find(layout_name)
|
101
|
+
return unless layout_name
|
102
|
+
Secretary.current.store.find_layout(layout_name)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
data/lib/laze/page.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
module Laze
|
2
|
+
# A special kind of Item aimed at HTML files (could be files ending in
|
3
|
+
# html, htm, md, mkd, mdn, markdown or liquid).
|
4
|
+
class Page < Item
|
5
|
+
# Convert this page's content to HTML using a text filter. You can set
|
6
|
+
# the text filter to use with a property +text_filter+.
|
7
|
+
#
|
8
|
+
# Current filters supported are markdown, textile and none. Default is
|
9
|
+
# markdown.
|
10
|
+
def filtered_content
|
11
|
+
text_filter ? text_filter.new(content).to_html : content
|
12
|
+
end
|
13
|
+
|
14
|
+
def filename #:nodoc:
|
15
|
+
@filename ||= properties[:filename].sub(/(?:md|mkd|liquid|markdown|htm)$/, 'html')
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
# Try to read the text filter to use from the properties. Return nil when
|
21
|
+
# 'none' is defined, RDiscount when undefined or otherwise the
|
22
|
+
# associated filter.
|
23
|
+
def text_filter
|
24
|
+
case properties[:filter]
|
25
|
+
when 'markdown': RDiscount
|
26
|
+
when 'textile': RedCloth
|
27
|
+
when 'none': nil
|
28
|
+
else
|
29
|
+
RDiscount
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/lib/laze/plugins.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
module Laze
|
2
|
+
# Plugins provides access to all Laze plugins.
|
3
|
+
#
|
4
|
+
# A plugin is a simple decorator that can wrap an Item object. Every plugin
|
5
|
+
# itself can tell you to what objects it applies, and an Item includes
|
6
|
+
# all available plugins by using the +include_plugins+ macro:
|
7
|
+
#
|
8
|
+
# class MyItem < Item
|
9
|
+
# include_plugins :my_item
|
10
|
+
# end
|
11
|
+
#
|
12
|
+
# == Example
|
13
|
+
#
|
14
|
+
# A plugin is a simple module that usually replaces methods on Item objects.
|
15
|
+
# Here's a simple example:
|
16
|
+
#
|
17
|
+
# class MyItem < Item
|
18
|
+
# include_plugins :my_item
|
19
|
+
# def foo
|
20
|
+
# 'bar'
|
21
|
+
# end
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# module Plugins
|
25
|
+
# module MyPlugin
|
26
|
+
# def applies_to?(kind)
|
27
|
+
# kind == :my_item
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# def foo
|
31
|
+
# super + '!'
|
32
|
+
# end
|
33
|
+
# end
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
# MyItem.new.foo # => 'bar!'
|
37
|
+
#
|
38
|
+
# == Working with plugins
|
39
|
+
#
|
40
|
+
# Usually, each plugin should have its own file, require any third-party
|
41
|
+
# libraries and be responsible for when things go wrong.
|
42
|
+
#
|
43
|
+
# A plugin should be stored in /plugins, where it is automatically loaded.
|
44
|
+
module Plugins
|
45
|
+
# Loop over all available plugins, yielding each.
|
46
|
+
def self.each(for_kind = nil) # :yields: module
|
47
|
+
constants.each do |c|
|
48
|
+
const = Laze::Plugins.const_get(c)
|
49
|
+
yield const if const.is_a?(Module) && (for_kind.nil? || const.applies_to?(for_kind))
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Load all plugins from /plugins
|
56
|
+
Dir[File.join(File.dirname(__FILE__), 'plugins', '*.rb')].each { |f| require f }
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Laze #:nodoc:
|
2
|
+
module Plugins #:nodoc:
|
3
|
+
# This plugin applies cache busters to CSS files, Javascript files and
|
4
|
+
# image files in your stylesheets. This means it will append the last
|
5
|
+
# modified time of these referenced files to the URL references as
|
6
|
+
# query parameters, so you force the server to bypass caches and give
|
7
|
+
# you a new file when a file has changed.
|
8
|
+
#
|
9
|
+
# Example:
|
10
|
+
#
|
11
|
+
# # css file
|
12
|
+
# body {
|
13
|
+
# background: url(image.png);
|
14
|
+
# }
|
15
|
+
#
|
16
|
+
# # HTML file
|
17
|
+
# <link href="screen.css">
|
18
|
+
#
|
19
|
+
# Becomes:
|
20
|
+
#
|
21
|
+
# # css file
|
22
|
+
# body {
|
23
|
+
# background: url(image.png?201009231441);
|
24
|
+
# }
|
25
|
+
#
|
26
|
+
# # HTML file
|
27
|
+
# <link href="screen.css?201009231441">
|
28
|
+
#
|
29
|
+
module CacheBuster
|
30
|
+
def self.applies_to?(kind) #:nodoc:
|
31
|
+
kind == 'Laze::Renderers::PageRenderer' || kind == 'Laze::Renderers::StylesheetRenderer'
|
32
|
+
end
|
33
|
+
|
34
|
+
def render(locals = {})
|
35
|
+
Laze.info("Adding cache busters to #{options[:locals][:filename]}")
|
36
|
+
[ /((?:href|src)=(?:'|"))(.+\.(?:css|js))("|')/,
|
37
|
+
/(url\([\s"']*)([^\)"'\s]*\.(?:png|gif|jpg))([\s"']*\))/m
|
38
|
+
].inject(super) do |output, regex|
|
39
|
+
output.gsub(regex) do |match|
|
40
|
+
filename = File.expand_path(File.join('input', options[:locals][:path], $2))
|
41
|
+
output = "#{$1}#{$2}%s#{$3}"
|
42
|
+
cache_buster = File.exists?(filename) ? '?' + File.mtime(filename).strftime('%Y%m%d%H%M') : ''
|
43
|
+
output % cache_buster
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Laze #:nodoc:
|
2
|
+
module Plugins #:nodoc:
|
3
|
+
# This plugin will replace any import statements in your stylesheets with
|
4
|
+
# the actual contents of the referenced files. This reduces the number
|
5
|
+
# of HTTP requests and speeds up your website loading time.
|
6
|
+
#
|
7
|
+
# This plugin is a decorator for Target and fires before Target#save.
|
8
|
+
module CssImports
|
9
|
+
def self.applies_to?(kind) #:nodoc:
|
10
|
+
kind == :target
|
11
|
+
end
|
12
|
+
|
13
|
+
def save
|
14
|
+
expand_css_imports
|
15
|
+
super
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def expand_css(content, filename)
|
21
|
+
Laze.info("Expanding imports in #{filename}")
|
22
|
+
content.gsub!(/@import\s*url\(\s*('|")?\s*([^'"]+)\s*\1?\s*\);?/) do |match|
|
23
|
+
referenced_file = File.expand_path(File.join(File.dirname(filename), $2))
|
24
|
+
if File.exists?(referenced_file)
|
25
|
+
"/* #{match} */\n" + expand_css(File.read(referenced_file), referenced_file)
|
26
|
+
else
|
27
|
+
Laze.warn "CSS Imported file not found: #{referenced_file}"
|
28
|
+
match
|
29
|
+
end
|
30
|
+
end
|
31
|
+
content
|
32
|
+
end
|
33
|
+
|
34
|
+
def expand_css_imports
|
35
|
+
Dir[File.join(output_dir, '**', '*.css')].each do |filename|
|
36
|
+
content = File.read(filename)
|
37
|
+
File.open(filename, 'w') { |f| f.write expand_css(content, filename) }
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
begin
|
2
|
+
require 'cssmin'
|
3
|
+
module Laze #:nodoc:
|
4
|
+
module Plugins #:nodoc:
|
5
|
+
# This plugin minifies the output of your CSS files, reducing your
|
6
|
+
# website's bandwidth usage.
|
7
|
+
#
|
8
|
+
# This plugin is a decorator for StylesheetRenderer and fires after
|
9
|
+
# StylesheetRenderer#render.
|
10
|
+
#
|
11
|
+
# *Note*: when the cssmin gem is unavailable this plugin will quietly
|
12
|
+
# fail, leaving the .css files untouched and generating a warning in the
|
13
|
+
# logs.
|
14
|
+
module Cssmin
|
15
|
+
def self.applies_to?(kind) #:nodoc:
|
16
|
+
kind == 'Laze::Renderers::StylesheetRenderer'
|
17
|
+
end
|
18
|
+
|
19
|
+
def render(locals = {})
|
20
|
+
return super unless Secretary.current.options[:minify_css]
|
21
|
+
@minified_content = begin
|
22
|
+
Laze.info "Minifying #{options[:locals][:filename]}"
|
23
|
+
::CSSMin.minify(super)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
rescue LoadError
|
30
|
+
Laze.warn 'The cssmin gem is required to minify .css files.'
|
31
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Laze #:nodoc:
|
2
|
+
module Plugins #:nodoc:
|
3
|
+
# This plugin checks your stylesheets for missing images and spits out
|
4
|
+
# a log warning message when an image file referenced in your
|
5
|
+
# stylesheets could not be found.
|
6
|
+
#
|
7
|
+
# This plugin is a decorator for StylesheetRenderer and fires after
|
8
|
+
# StylesheetRenderer#render.
|
9
|
+
module ImageCheck
|
10
|
+
def self.applies_to?(kind) #:nodoc:
|
11
|
+
kind == 'Laze::Renderers::StylesheetRenderer'
|
12
|
+
end
|
13
|
+
|
14
|
+
def render(locals = {})
|
15
|
+
Laze.debug 'Checking for missing stylesheet images'
|
16
|
+
super.scan(/url\(\s*('|")?\s*(.+?)\s*\1?\s*\)/) do |match|
|
17
|
+
filename = match[1].split("?", 2).first
|
18
|
+
if %w(.gif .png .jpg .jpeg).include?(File.extname(filename))
|
19
|
+
path = File.expand_path(File.join(Secretary.current.options[:source], 'input', options[:locals][:path], filename))
|
20
|
+
unless File.exists?(path)
|
21
|
+
Laze.warn "Image #{path} not found (#{options[:locals][:filename]})."
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Laze #:nodoc:
|
2
|
+
module Plugins #:nodoc:
|
3
|
+
# This plugin tries to optimize the images in your website.
|
4
|
+
# You will need to have +pngcrush+ and +jpegtran+ installed on your
|
5
|
+
# system for this plugin to work. If not, it will fail silently.
|
6
|
+
#
|
7
|
+
# This plugin is a decorator for Target and fires before Target#save.
|
8
|
+
module ImageOptimizer
|
9
|
+
def self.applies_to?(kind) #:nodoc:
|
10
|
+
kind == :target
|
11
|
+
end
|
12
|
+
|
13
|
+
def save
|
14
|
+
optimize_images
|
15
|
+
super
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def optimize_images
|
21
|
+
pngcrush = `which pngcrush`
|
22
|
+
jpegtran = `which jpegtran`
|
23
|
+
|
24
|
+
Laze.warn 'pngcrush not found; skipping PNG optimization' if pngcrush.empty?
|
25
|
+
Laze.warn 'jpegtran not found; skipping JPG optimization' if jpegtran.empty?
|
26
|
+
|
27
|
+
Dir[File.join(output_dir, '**/*.{png,jpg}')].each do |filename|
|
28
|
+
temp_file = filename + '_optimized'
|
29
|
+
if File.extname(filename).downcase == '.png' && !pngcrush.empty?
|
30
|
+
optimize_png(filename, temp_file)
|
31
|
+
elsif File.extname(filename).downcase == '.jpg' && !jpegtran.empty?
|
32
|
+
optimize_jpg(filename, temp_file)
|
33
|
+
end
|
34
|
+
if File.exists?(temp_file)
|
35
|
+
FileUtils.rm(filename)
|
36
|
+
FileUtils.mv(temp_file, filename)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def optimize_png(filename, temp_file)
|
42
|
+
Laze.info "Optimizing #{filename}"
|
43
|
+
system("pngcrush -q -reduce -brute #{filename} #{temp_file}")
|
44
|
+
end
|
45
|
+
|
46
|
+
def optimize_jpg(filename, temp_file)
|
47
|
+
Laze.info "Optimizing #{filename}"
|
48
|
+
system("jpegtran -copy none -progressive -outfile #{temp_file} #{filename}")
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Laze #:nodoc:
|
2
|
+
module Plugins #:nodoc:
|
3
|
+
# This plugin introduces the +require+ statement to your javascript files.
|
4
|
+
# This is a simple mechanism to concatenate all your javascript files
|
5
|
+
# into a single library and thereby reduce the number of HTTP requests
|
6
|
+
# (improving your website load time).
|
7
|
+
#
|
8
|
+
# You can include another javascript file with a special comment like so:
|
9
|
+
#
|
10
|
+
# # foo.js
|
11
|
+
# var foo = 'foo';
|
12
|
+
#
|
13
|
+
# # bar.js
|
14
|
+
# // require 'foo.js'
|
15
|
+
# alert(foo);
|
16
|
+
#
|
17
|
+
# This will compile into the following file:
|
18
|
+
#
|
19
|
+
# # bar.js
|
20
|
+
# var foo = 'foo';
|
21
|
+
# alert(foo);
|
22
|
+
#
|
23
|
+
# Note that this leaves the original files untouched, so your website
|
24
|
+
# will end up with both files. The point is you only have to reference
|
25
|
+
# the one with that is concatenated.
|
26
|
+
#
|
27
|
+
# This plugin is a decorator for Target and fires before Target#save.
|
28
|
+
module JsRequires
|
29
|
+
# This plugin is a decorator for Target and fires before Target#save.
|
30
|
+
def self.applies_to?(kind) #:nodoc:
|
31
|
+
kind == :target
|
32
|
+
end
|
33
|
+
|
34
|
+
def save
|
35
|
+
expand_js_requires
|
36
|
+
super
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def expand_js(content, filename)
|
42
|
+
Laze.info("Expanding requires in #{filename}")
|
43
|
+
content.gsub!(/^\s*\/\/\s*requires? ('|")?(.+)\1$/) do |match|
|
44
|
+
referenced_file = File.expand_path(File.join(File.dirname(filename), $2))
|
45
|
+
if File.exists?(referenced_file)
|
46
|
+
"#{match}\n" + expand_js(File.read(referenced_file), referenced_file)
|
47
|
+
else
|
48
|
+
Laze.warn "JS required file not found: #{referenced_file}"
|
49
|
+
match
|
50
|
+
end
|
51
|
+
end
|
52
|
+
content
|
53
|
+
end
|
54
|
+
|
55
|
+
def expand_js_requires
|
56
|
+
Dir[File.join(output_dir, '**', '*.js')].each do |filename|
|
57
|
+
content = File.read(filename)
|
58
|
+
File.open(filename, 'w') { |f| f.write expand_js(content, filename) }
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|