sitepress 0.1.20 → 0.1.21

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 57a26550fd2e4633c7ab382fad0b0877a77d7536
4
- data.tar.gz: ff402977fa074f30352f85ecce144728cafd2347
3
+ metadata.gz: 344bb99aa58dcaaa2ce9610d93bfd37dfef3c4c5
4
+ data.tar.gz: b8813d806775de3ada67302cbc66d10e5f1ddb4f
5
5
  SHA512:
6
- metadata.gz: 566f9550bd7d77e8023229f676a243043526515c0259e0977c6aa6e2da0bba01da6575c008e11e86cbbf30f5ae5c55c7e148c2b5ba15912c10a9348377bb5702
7
- data.tar.gz: b0e4269455ae324c9022dcc75003b7cf36e0e234c587b49eaab52cfbf93fe8d5a40874f7cc34313ba9f31e1cee730f36a52ade4ff3a678cb9575c6b71d75ee9e
6
+ metadata.gz: 6befa582a4f32510f440adb1947eeed4bd6d0b5e57cc4a83b19013999a77a9555b88622f72169dc5c37f5b39f0ffce723870ce48ebf9017de417361acd55fe65
7
+ data.tar.gz: 31c9418efd9de1b09506c12bf858685b979b9e4e43df914cfebe7b524661b3bc796f1b53d7e2d916b0ac614f2834ddc96d11a0763ccec96121ea1f2536941502
data/exe/sitepress ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "sitepress-cli"
4
+
5
+ Sitepress::CLI.start(ARGV)
data/lib/sitepress.rb CHANGED
@@ -1,19 +1,4 @@
1
1
  require "sitepress/version"
2
2
 
3
3
  module Sitepress
4
- # Raised by Resources if a path is added that's not a valid path.
5
- InvalidRequestPathError = Class.new(RuntimeError)
6
-
7
- # Raised by Resources if a path is already in its index
8
- ExistingRequestPathError = Class.new(InvalidRequestPathError)
9
-
10
- autoload :Asset, "sitepress/asset"
11
- autoload :DirectoryCollection, "sitepress/directory_collection"
12
- autoload :Formats, "sitepress/formats"
13
- autoload :Frontmatter, "sitepress/frontmatter"
14
- autoload :Resource, "sitepress/resource"
15
- autoload :ResourceCollection, "sitepress/resource_collection"
16
- autoload :ResourcesPipeline, "sitepress/resources_pipeline"
17
- autoload :ResourcesNode, "sitepress/resources_node"
18
- autoload :Site, "sitepress/site"
19
4
  end
@@ -1,3 +1,3 @@
1
1
  module Sitepress
2
- VERSION = "0.1.20"
2
+ VERSION = "0.1.21"
3
3
  end
data/sitepress.gemspec CHANGED
@@ -17,9 +17,5 @@ Gem::Specification.new do |spec|
17
17
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
18
18
  spec.require_paths = ["lib"]
19
19
 
20
- spec.add_development_dependency "bundler", "~> 1.11"
21
- spec.add_development_dependency "rake", "~> 10.0"
22
- spec.add_development_dependency "rspec", "~> 3.0"
23
-
24
- spec.add_runtime_dependency "mime-types", ">= 2.99"
20
+ spec.add_runtime_dependency "sitepress-cli", spec.version
25
21
  end
metadata CHANGED
@@ -1,90 +1,39 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sitepress
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.20
4
+ version: 0.1.21
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brad Gessler
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2016-10-12 00:00:00.000000000 Z
11
+ date: 2016-10-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: bundler
14
+ name: sitepress-cli
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - '='
18
18
  - !ruby/object:Gem::Version
19
- version: '1.11'
20
- type: :development
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - "~>"
25
- - !ruby/object:Gem::Version
26
- version: '1.11'
27
- - !ruby/object:Gem::Dependency
28
- name: rake
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - "~>"
32
- - !ruby/object:Gem::Version
33
- version: '10.0'
34
- type: :development
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - "~>"
39
- - !ruby/object:Gem::Version
40
- version: '10.0'
41
- - !ruby/object:Gem::Dependency
42
- name: rspec
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - "~>"
46
- - !ruby/object:Gem::Version
47
- version: '3.0'
48
- type: :development
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - "~>"
53
- - !ruby/object:Gem::Version
54
- version: '3.0'
55
- - !ruby/object:Gem::Dependency
56
- name: mime-types
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - ">="
60
- - !ruby/object:Gem::Version
61
- version: '2.99'
19
+ version: 0.1.21
62
20
  type: :runtime
63
21
  prerelease: false
64
22
  version_requirements: !ruby/object:Gem::Requirement
65
23
  requirements:
66
- - - ">="
24
+ - - '='
67
25
  - !ruby/object:Gem::Version
68
- version: '2.99'
26
+ version: 0.1.21
69
27
  description:
70
28
  email:
71
29
  - bradgessler@gmail.com
72
- executables: []
30
+ executables:
31
+ - sitepress
73
32
  extensions: []
74
33
  extra_rdoc_files: []
75
34
  files:
35
+ - exe/sitepress
76
36
  - lib/sitepress.rb
77
- - lib/sitepress/asset.rb
78
- - lib/sitepress/directory_collection.rb
79
- - lib/sitepress/extensions/layouts.rb
80
- - lib/sitepress/extensions/proc_manipulator.rb
81
- - lib/sitepress/formats.rb
82
- - lib/sitepress/frontmatter.rb
83
- - lib/sitepress/resource.rb
84
- - lib/sitepress/resource_collection.rb
85
- - lib/sitepress/resources_node.rb
86
- - lib/sitepress/resources_pipeline.rb
87
- - lib/sitepress/site.rb
88
37
  - lib/sitepress/version.rb
89
38
  - sitepress.gemspec
90
39
  homepage: https://github.com/sitepress/sitepress
@@ -1,91 +0,0 @@
1
- require "mime/types"
2
- require "forwardable"
3
- require "pathname"
4
-
5
- module Sitepress
6
- # Represents a file on a web server that may be parsed to extract
7
- # frontmatter or be renderable via a template. Multiple resources
8
- # may point to the same asset. Properties of an asset should be mutable.
9
- # The Resource object is immutable and may be modified by the Resources proxy.
10
- class Asset
11
- # If we can't resolve a mime type for the resource, we'll fall
12
- # back to this binary octet-stream type so the client can download
13
- # the resource and figure out what to do with it.
14
- DEFAULT_MIME_TYPE = MIME::Types["application/octet-stream"].first
15
-
16
- attr_reader :path
17
-
18
- extend Forwardable
19
- def_delegators :frontmatter, :data, :body
20
-
21
- def initialize(path: , mime_type: nil)
22
- # The MIME::Types gem returns an array when types are looked up.
23
- # This grabs the first one, which is likely the intent on these lookups.
24
- @mime_type = Array(mime_type).first
25
- @path = Pathname.new path
26
- end
27
-
28
- # List of all file extensions.
29
- def extensions
30
- path.basename.to_s.split(".").drop(1)
31
- end
32
-
33
- # TODO: This is really a "key" or "leafname".
34
- def basename
35
- path.basename.to_s.split(".").first
36
- end
37
-
38
- def format_basename
39
- [basename, format_extension].join(".")
40
- end
41
-
42
- # Returns the format extension.
43
- def format_extension
44
- extensions.first
45
- end
46
-
47
- # The base name with the format extension.
48
- def format_name
49
- [basename, format_extension].join(".")
50
- end
51
-
52
- # Returns a list of the rendering extensions.
53
- def template_extensions
54
- extensions.drop(1)
55
- end
56
-
57
- # Treat resources with the same request path as equal.
58
- def ==(asset)
59
- path == asset.path
60
- end
61
-
62
- def mime_type
63
- @mime_type ||= inferred_mime_type || DEFAULT_MIME_TYPE
64
- end
65
-
66
- def exists?
67
- File.exists? path
68
- end
69
-
70
- # Spits out a reasonable default request path. This may be changed
71
- # via Resource#request_path.
72
- def to_request_path
73
- if ext = format_extension
74
- path.dirname.join(basename).sub_ext(".#{ext}").to_s
75
- else
76
- path.to_s
77
- end
78
- end
79
-
80
- private
81
- def frontmatter
82
- Frontmatter.new File.read @path
83
- end
84
-
85
- # Returns the mime type of the file extension. If a type can't
86
- # be resolved then we'll just grab the first type.
87
- def inferred_mime_type
88
- MIME::Types.type_for(format_extension).first if format_extension
89
- end
90
- end
91
- end
@@ -1,24 +0,0 @@
1
- module Sitepress
2
- # Maps a directory of assets into a set of routes that correspond with
3
- # the `path` root.
4
- class DirectoryCollection
5
- attr_reader :assets, :path
6
-
7
- def initialize(path: , assets:)
8
- @path = path
9
- @assets = assets
10
- end
11
-
12
- def mount(node)
13
- assets.each { |a| node.add path: asset_path_to_request_path(a), asset: a }
14
- end
15
-
16
- private
17
- # Given a @file_path of `/hi`, this method changes `/hi/there/friend.html.erb`
18
- # to an absolute `/there/friend` format by removing the file extensions
19
- def asset_path_to_request_path(asset)
20
- # Relative path of resource to the file_path of this project.
21
- asset.path.dirname.join(asset.format_basename).relative_path_from(path).to_s
22
- end
23
- end
24
- end
@@ -1,27 +0,0 @@
1
- module Sitepress
2
- module Extensions
3
- # Register layouts with resources that match certain patterns.
4
- class Layouts
5
- Rule = Struct.new(:layout, :processor)
6
-
7
- def initialize
8
- @rules = Array.new
9
- end
10
-
11
- # Register a layout for a set of resources.
12
- def layout(layout, &block)
13
- @rules << Rule.new(layout, block)
14
- end
15
-
16
- def process_resources(node)
17
- node.flatten.each do |resource|
18
- @rules.each do |rule|
19
- if rule.processor.call(resource)
20
- resource.data["layout"] ||= rule.layout
21
- end
22
- end
23
- end
24
- end
25
- end
26
- end
27
- end
@@ -1,19 +0,0 @@
1
- module Sitepress
2
- module Extensions
3
- class ProcManipulator
4
- def initialize(block)
5
- @block = block
6
- end
7
-
8
- def process_resources(node)
9
- node.flatten.each do |resource|
10
- if @block.arity == 1
11
- @block.call resource
12
- else # This will blow up if 0 or greater than 2.
13
- @block.call resource, node
14
- end
15
- end
16
- end
17
- end
18
- end
19
- end
@@ -1,45 +0,0 @@
1
- module Sitepress
2
- # Manages collections of resources that share the same ResourceNode. Given the files `/a.html` and `/a.gif`,
3
- # both of these assets would be stored in the `ResourceNode#name = "a"` under `ResourceNode#formats` with
4
- # the extensions `.gif`, and `.html`.
5
- class Formats
6
- include Enumerable
7
-
8
- extend Forwardable
9
- def_delegators :@formats, :size, :clear
10
-
11
- def initialize(node: )
12
- @node = node
13
- @formats = Hash.new
14
- end
15
-
16
- def each(&block)
17
- @formats.values.each(&block)
18
- end
19
-
20
- def remove(ext)
21
- @formats.delete(ext)
22
- end
23
-
24
- def ext(ext)
25
- @formats[ext]
26
- end
27
-
28
- def mime_type(mime_type)
29
- find { |f| f.mime_type == mime_type }
30
- end
31
-
32
- def add(asset: , ext: )
33
- resource = Resource.new(asset: asset, node: @node, ext: ext)
34
- if @formats.has_key? ext
35
- raise Sitepress::ExistingRequestPathError, "Resource at #{resource.request_path} already set"
36
- else
37
- @formats[ext] = resource
38
- end
39
- end
40
-
41
- def inspect
42
- "<#{self.class}: resources=#{map(&:request_path)}>"
43
- end
44
- end
45
- end
@@ -1,22 +0,0 @@
1
- require "yaml"
2
-
3
- module Sitepress
4
- # Parses metadata from the header of the page.
5
-
6
- # TODO: Redo this to use File readline and pos to
7
- # perform faster
8
- class Frontmatter
9
- DELIMITER = "---".freeze
10
- PATTERN = /\A(#{DELIMITER}\n(.+?)\n#{DELIMITER}\n*)?(.+)\Z/m
11
-
12
- attr_reader :body
13
-
14
- def initialize(content)
15
- _, @data, @body = content.match(PATTERN).captures
16
- end
17
-
18
- def data
19
- @data ? YAML.load(@data) : {}
20
- end
21
- end
22
- end
@@ -1,92 +0,0 @@
1
- require "forwardable"
2
-
3
- module Sitepress
4
- # Represents the request path of an asset. There may be multiple
5
- # resources that point to the same asset. Resources are immutable
6
- # and may be altered by the resource proxy.
7
- class Resource
8
- extend Forwardable
9
- def_delegators :asset, :mime_type
10
-
11
- attr_writer :body, :data
12
- attr_reader :node, :asset, :ext
13
-
14
- # Default scope for querying parent/child/sibling resources.
15
- DEFAULT_FILTER_SCOPE = :same
16
-
17
- def initialize(asset: , node: , ext: "")
18
- @asset = asset
19
- @node = node
20
- @ext = ext # TODO: Meh, feels dirty but I suppose the thingy has to drop it in.
21
- end
22
-
23
- def request_path
24
- return unless node
25
- # TODO: This `compact` makes me nervous. How can we handle this better?
26
- lineage = node.parents.reverse.map(&:name).compact
27
- file_name = [node.name, @ext].join
28
- File.join("/", *lineage, file_name)
29
- end
30
-
31
- def data
32
- @data ||= asset.data
33
- end
34
-
35
- def body
36
- @body ||= asset.body
37
- end
38
-
39
- def inspect
40
- "<#{self.class}:#{object_id} request_path=#{request_path.inspect} asset_path=#{@asset.path.to_s.inspect}>"
41
- end
42
-
43
- def parent(**args)
44
- parents(**args).first
45
- end
46
-
47
- def parents(**args)
48
- filter_resources(**args){ node.parents }
49
- end
50
-
51
- def siblings(**args)
52
- filter_resources(**args){ node.siblings }.compact
53
- end
54
-
55
- def children(**args)
56
- filter_resources(**args){ node.children }.compact
57
- end
58
-
59
- def ==(resource)
60
- resource.request_path == request_path
61
- end
62
-
63
- private
64
- # Filters parent/child/sibling resources by a type. The default behavior is to only return
65
- # resources of the same type. For example given the pages `/a.html`, `/a.gif`, `/a/b.html`,
66
- # if you query the parent from page `/a/b.html` you'd only get `/a.html` by default. If you
67
- # query the parents via `parents(type: :all)` you'd get get [`/a.html`, `/a.gif`]
68
- #
69
- # TODO: When `type: :all` is scoped, some queries will mistakenly return single resources.
70
- # :all should return an array of arrays to accurately represention levels.
71
- #
72
- # TODO: Put a better extension/mime_type handler into resource tree, then instead of faltening
73
- # below and select, we could call a single map and pull out a resources
74
- def filter_resources(type: DEFAULT_FILTER_SCOPE, &block)
75
- return [] unless node
76
- nodes = block.call
77
-
78
- case type
79
- when :all
80
- nodes.map{ |node| node.formats }
81
- when :same
82
- nodes.map{ |n| n.formats.ext(ext) }.flatten
83
- when String
84
- nodes.map{ |n| n.formats.ext(type) }.flatten
85
- when MIME::Type
86
- nodes.map{ |n| n.formats.mime_type(type) }.flatten
87
- else
88
- raise ArgumentError, "Invalid type argument #{type}. Must be either :same, :all, an extension string, or a Mime::Type"
89
- end
90
- end
91
- end
92
- end
@@ -1,33 +0,0 @@
1
- module Sitepress
2
- # Represents a collection of resources. Provides interfaces to query
3
- # resource via globbing, paths, etc.
4
- class ResourceCollection
5
- extend Forwardable
6
- def_delegators :resources, :each, :size, :index, :[], :last, :length, :fetch, :count, :at
7
-
8
- include Enumerable
9
-
10
- # Used by `#glob` to determine the full path when
11
- # given a relative glob pattern.
12
- attr_reader :root_path
13
-
14
- def initialize(node: , root_path: ".")
15
- @node = node
16
- @root_path = Pathname.new(root_path)
17
- end
18
-
19
- def glob(pattern)
20
- paths = Dir.glob root_path.join(pattern)
21
- resources.select { |r| paths.include? r.asset.path.to_s }
22
- end
23
-
24
- def get(request_path)
25
- @node.get(request_path)
26
- end
27
-
28
- private
29
- def resources
30
- @node.flatten
31
- end
32
- end
33
- end
@@ -1,135 +0,0 @@
1
- module Sitepress
2
- # Resource nodes give resources their parent/sibling/child relationships. The relationship are determined
3
- # by the `request_path` given to an asset when its added to a node. Given the `request_path` `/foo/bar/biz/buz.html`,
4
- # a tree of resource nodes would be built named `foo`, `bar`, `biz`, `buz`. `foo` would be the "root" node and `buz`
5
- # a leaf node. The actual `buz.html` asset is then stored on the leaf node as a resource. This tree structure
6
- # makes it possible to reason through path relationships from code to build out elements in a website like tree navigation.
7
- class ResourcesNode
8
- attr_reader :parent, :name
9
-
10
- DELIMITER = "/".freeze
11
-
12
- def initialize(parent: nil, delimiter: ResourcesNode::DELIMITER, name: nil)
13
- @parent = parent
14
- @name = name.freeze
15
- @delimiter = delimiter.freeze
16
- end
17
-
18
- def formats
19
- @formats ||= Formats.new(node: self)
20
- end
21
-
22
- # Returns the immediate children nodes.
23
- def children
24
- child_nodes.values
25
- end
26
-
27
- # Returns sibling nodes.
28
- def siblings
29
- parent ? parent.children.reject{ |c| c == self } : []
30
- end
31
-
32
- # Returns all parents up to the root node.
33
- def parents
34
- parents = []
35
- node = parent
36
- while node do
37
- parents << node
38
- node = node.parent
39
- end
40
- parents
41
- end
42
-
43
- def root?
44
- parent.nil?
45
- end
46
-
47
- def leaf?
48
- child_nodes.empty?
49
- end
50
-
51
- def flatten(resources: [])
52
- formats.each{ |resource| resources << resource }
53
- children.each do |child|
54
- child.flatten.each{ |resource| resources << resource }
55
- end
56
- resources
57
- end
58
-
59
- def remove
60
- if leaf?
61
- # TODO: Check the parents to see if they also need to be removed if
62
- # this call orphans the tree up to a resource.
63
- parent.remove_child(name)
64
- else
65
- formats.clear
66
- end
67
- end
68
-
69
- def add(path: , asset: )
70
- head, *path = tokenize(path)
71
- if path.empty?
72
- # When there's no more paths, we're left with the format (e.g. ".html")
73
- formats.add(asset: asset, ext: head)
74
- else
75
- child_nodes[head].add(path: path, asset: asset)
76
- end
77
- end
78
- alias :[]= :add
79
-
80
- def get(path)
81
- *path, ext = tokenize(path)
82
- if node = dig(*path)
83
- node.formats.ext(ext)
84
- end
85
- end
86
-
87
- def get_node(path)
88
- *path, _ = tokenize(path)
89
- dig(*path)
90
- end
91
- alias :[] :get_node
92
-
93
- def inspect
94
- "<#{self.class}: formats=#{formats.map(&:request_path)} children=#{children.map(&:name).inspect}>"
95
- end
96
-
97
- # TODO: I don't really like how the path is broken up with the "ext" at the end.
98
- # It feels inconsistent. Either make an object/struct that encaspulates this or
99
- # just pass `index.html` through to the end.
100
- def dig(*args)
101
- head, *tail = args
102
- if head.nil? and tail.empty?
103
- self
104
- elsif child_nodes.has_key?(head)
105
- child_nodes[head].dig(*tail)
106
- else
107
- nil
108
- end
109
- end
110
-
111
- protected
112
- def remove_child(path)
113
- *_, segment, _ = tokenize(path)
114
- child_nodes.delete(segment)
115
- end
116
-
117
- private
118
- def add_child_node(name)
119
- ResourcesNode.new(parent: self, delimiter: @delimiter, name: name)
120
- end
121
-
122
- def child_nodes
123
- @child_nodes ||= Hash.new { |hash, key| hash[key] = add_child_node(key) }
124
- end
125
-
126
- # Returns all of the names for the path along with the format, if set.
127
- def tokenize(path)
128
- return path if path.respond_to? :to_a
129
- path, _, file = path.gsub(/^\//, "").rpartition(@delimiter)
130
- ext = File.extname(file)
131
- file = File.basename(file, ext)
132
- path.split(@delimiter).push(file).push(ext)
133
- end
134
- end
135
- end
@@ -1,10 +0,0 @@
1
- require "forwardable"
2
-
3
- module Sitepress
4
- # Processes a collection of resources
5
- class ResourcesPipeline < Array
6
- def process(resources)
7
- each{ |processor| processor.process_resources resources }
8
- end
9
- end
10
- end
@@ -1,69 +0,0 @@
1
- require "pathname"
2
- require "sitepress/extensions/proc_manipulator"
3
-
4
- module Sitepress
5
- # A collection of pages from a directory.
6
- class Site
7
- # Default file pattern to pick up in site
8
- DEFAULT_GLOB = "**/**".freeze
9
-
10
- # Default root_path for site.
11
- DEFAULT_ROOT_PATH = Pathname.new(".").freeze
12
-
13
- attr_reader :root_path, :resources_pipeline
14
-
15
- # Cache resources for production runs of Sitepress. Development
16
- # modes don't cache to optimize for files reloading.
17
- attr_accessor :cache_resources
18
- alias :cache_resources? :cache_resources
19
-
20
- # TODO: Get rid of these so that folks have ot call site.resources.get ...
21
- extend Forwardable
22
- def_delegators :resources, :get, :glob
23
-
24
- def initialize(root_path: DEFAULT_ROOT_PATH, cache_resources: false)
25
- self.root_path = root_path
26
- self.cache_resources = cache_resources
27
- end
28
-
29
- # A tree representation of the resourecs wthin the site.
30
- def root
31
- ResourcesNode.new.tap do |node|
32
- DirectoryCollection.new(assets: pages_assets, path: pages_path).mount(node)
33
- resources_pipeline.process node
34
- end
35
- end
36
-
37
- def pages_path
38
- root_path.join("pages")
39
- end
40
-
41
- # Returns a list of all the resources within #root.
42
- def resources
43
- @resources = nil unless cache_resources
44
- @resources ||= ResourceCollection.new(node: root, root_path: root_path)
45
- end
46
-
47
- def root_path=(path)
48
- @root_path = Pathname.new(path)
49
- end
50
-
51
- # Quick and dirty way to manipulate resources in the site without
52
- # creating classes that implement the #process_resources method
53
- def manipulate(&block)
54
- resources_pipeline << Extensions::ProcManipulator.new(block)
55
- end
56
-
57
- def resources_pipeline
58
- @resources_pipeline ||= ResourcesPipeline.new
59
- end
60
-
61
- private
62
- # Lazy stream of files that will be rendered by resources.
63
- def pages_assets(glob = DEFAULT_GLOB)
64
- Dir.glob(pages_path.join(glob)).select(&File.method(:file?)).lazy.map do |path|
65
- Asset.new(path: path)
66
- end
67
- end
68
- end
69
- end