mascot 0.1.11 → 0.1.12

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d49c4a14896b46d0fdcd7aedd1250a104e9b5048
4
- data.tar.gz: 101a6a075f65556c4dc0a86ca69e7a5c39459a4d
3
+ metadata.gz: 1d705fe26af1339418f2018ebeed0af942e6b49e
4
+ data.tar.gz: d0fbb9055d2b7214078eb1de963065e8f1949c9b
5
5
  SHA512:
6
- metadata.gz: 65db308403d25699fd58e498e09a7c390b885894709ebb60be396eeca3f1faed0d65c574dfc75164325272462bd7672e230f505694c6e1b15a7e2f5ba1b6addc
7
- data.tar.gz: eec51f4ec3bd95bf28831e097a646acd3faeba341529f75eb3bb93a844ffdff87c44d3aa9d9d78d29d703897bde3fb7b1960d0bda74abf8f96d73e3dfe127f61
6
+ metadata.gz: 95f73264703dccec615c082cf1f071237160b94e967903842c7f889da8dae7617c3a5931539ea0be5fbb2fbd070959cfc17bfc800c67eca0c758976249e88f01
7
+ data.tar.gz: ab29c4b7f17646864334e14007b1bf27feab5960c9e5fa88a805785dab23421b62727b8b12d813519105a50555c5f116c6a74a82d34e35bbc46786747b9ba457
data/lib/mascot/asset.rb CHANGED
@@ -30,15 +30,25 @@ module Mascot
30
30
  path.basename.to_s.split(".").drop(1)
31
31
  end
32
32
 
33
+ # TODO: This is really a "key" or "leafname".
33
34
  def basename
34
35
  path.basename.to_s.split(".").first
35
36
  end
36
37
 
38
+ def format_basename
39
+ [basename, format_extension].join(".")
40
+ end
41
+
37
42
  # Returns the format extension.
38
43
  def format_extension
39
44
  extensions.first
40
45
  end
41
46
 
47
+ # The base name with the format extension.
48
+ def format_name
49
+ [basename, format_extension].join(".")
50
+ end
51
+
42
52
  # Returns a list of the rendering extensions.
43
53
  def template_extensions
44
54
  extensions.drop(1)
@@ -6,51 +6,84 @@ module Mascot
6
6
  # resources that point to the same asset. Resources are immutable
7
7
  # and may be altered by the resource proxy.
8
8
  class Resource
9
- include Observable
10
-
11
9
  extend Forwardable
12
10
  def_delegators :asset, :mime_type
13
11
 
14
- attr_accessor :request_path, :asset
15
12
  attr_writer :body, :data
13
+ attr_reader :node, :asset, :ext
14
+
15
+ # Default scope for querying parent/child/sibling resources.
16
+ DEFAULT_FILTER_SCOPE = :same
16
17
 
17
- def initialize(asset: , request_path: nil)
18
- self.request_path = request_path || asset.to_request_path
18
+ def initialize(asset: , node: , ext: "")
19
19
  @asset = asset
20
+ @node = node
21
+ @ext = ext # TODO: Meh, feels dirty but I suppose the thingy has to drop it in.
20
22
  end
21
23
 
22
- # When #dup or #clone is copied, the Resource
23
- # collection observer must be removed so there's
24
- # not duplicate resources.
25
- def initialize_copy(instance)
26
- instance.delete_observers
27
- super instance
24
+ def request_path
25
+ return unless node
26
+ # TODO: This `compact` makes me nervous. How can we handle this better?
27
+ lineage = node.parents.reverse.map(&:name).compact
28
+ file_name = [node.name, @ext].join
29
+ File.join("/", *lineage, file_name)
28
30
  end
29
31
 
30
- def request_path=(request_path)
31
- old_request_path = @request_path
32
- # We freeze the value to ensure users can't modify
33
- # the request_path string in place (e.g. Resource#request_path.capitalize!)
34
- # and throw the resource out of sync with the Resources collection.
35
- @request_path = request_path.to_s.freeze
36
- changed
37
- notify_observers self, old_request_path
32
+ def data
33
+ @data ||= asset.data
34
+ end
35
+
36
+ def body
37
+ @body ||= asset.body
38
38
  end
39
39
 
40
40
  def inspect
41
- "#<#{self.class}:0x#{(object_id << 1).to_s(16)} @request_path=#{@request_path.inspect} @asset=#{@asset.inspect}>"
41
+ "<#{self.class}:#{object_id} request_path=#{request_path.inspect} asset_path=#{@asset.path.to_s.inspect}>"
42
42
  end
43
43
 
44
- def data
45
- @data ||= asset.data
44
+ def parents(**args)
45
+ filter_resources(**args){ node.parents }
46
46
  end
47
47
 
48
- def body
49
- @body ||= asset.body
48
+ def siblings(**args)
49
+ filter_resources(**args){ node.siblings }
50
+ end
51
+
52
+ def children(**args)
53
+ filter_resources(**args){ node.children }
54
+ end
55
+
56
+ def ==(resource)
57
+ resource.request_path == request_path
50
58
  end
51
59
 
52
- def ==(asset)
53
- request_path == asset.request_path
60
+ private
61
+ # Filters parent/child/sibling resources by a type. The default behavior is to only return
62
+ # resources of the same type. For example given the pages `/a.html`, `/a.gif`, `/a/b.html`,
63
+ # if you query the parent from page `/a/b.html` you'd only get `/a.html` by default. If you
64
+ # query the parents via `parents(type: :all)` you'd get get [`/a.html`, `/a.gif`]
65
+ #
66
+ # TODO: When `type: :all` is scoped, some queries will mistakenly return single resources.
67
+ # :all should return an array of arrays to accurately represention levels.
68
+ #
69
+ # TODO: Put a better extension/mime_type handler into resource tree, then instead of faltening
70
+ # below and select, we could call a single map and pull out a resources
71
+ def filter_resources(type: DEFAULT_FILTER_SCOPE, &block)
72
+ return [] unless node
73
+ resources = block.call.map(&:resources)
74
+
75
+ case type
76
+ when :all
77
+ resources
78
+ when :same
79
+ resources.flatten.select { |r| r.ext == ext }
80
+ when String
81
+ resources.flatten.select { |r| r.ext == type }
82
+ when MIME::Type
83
+ resources.flatten.select { |r| r.mime_type == type }
84
+ else
85
+ raise ArgumentError, "Invalid type argument #{type}. Must be either :same, :all, an extension string, or a Mime::Type"
86
+ end
54
87
  end
55
88
  end
56
89
  end
@@ -0,0 +1,148 @@
1
+ module Mascot
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
+ # TODO: Should it be called "formats" or "extensions"? Probably the latter, but that could conflict with Resource Manipulators.
9
+ attr_reader :parent, :name, :formats
10
+ include Enumerable
11
+
12
+ DELIMITER = "/".freeze
13
+
14
+ def initialize(parent: nil, delimiter: ResourcesNode::DELIMITER, name: nil)
15
+ @parent = parent
16
+ @name = name.freeze
17
+ @delimiter = delimiter.freeze
18
+ @children = Hash.new { |hash, key| hash[key] = ResourcesNode.new(parent: self, delimiter: delimiter, name: key) }
19
+ @resources = Hash.new
20
+ end
21
+
22
+ # Returns the immediate children nodes.
23
+ def children
24
+ @children.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
+ # Iterates through resources in the current node and all child resources.
44
+ # TODO: Should this include all children? Perhaps I should move this method
45
+ # into an eumerator that more clearly states it returns an entire sub-tree.
46
+ def each(&block)
47
+ @resources.values.each { |resource| block.call(resource) }
48
+ children.each{ |c| c.each(&block) }
49
+ end
50
+
51
+ def root?
52
+ parent.nil?
53
+ end
54
+
55
+ def leaf?
56
+ @children.empty?
57
+ end
58
+
59
+ def resources
60
+ @resources.values
61
+ end
62
+
63
+ def remove_resource(resource)
64
+ @resources.delete resource.ext #if @resources[resource.ext] == resource
65
+ end
66
+
67
+ def remove
68
+ if leaf?
69
+ # TODO: Check the parents to see if they also need to be removed if
70
+ # this call orphans the tree up to a resource.
71
+ parent.remove_child(name)
72
+ else
73
+ @resources.clear
74
+ end
75
+ end
76
+
77
+ def add(path: , asset: )
78
+ head, *path = tokenize(path)
79
+ if path.empty?
80
+ # When there's no more paths, we're left with the format (e.g. ".html")
81
+ add_format(asset: asset, ext: head)
82
+ else
83
+ @children[head].add(path: path, asset: asset)
84
+ end
85
+ end
86
+ alias :[]= :add
87
+
88
+ def get_resource(path)
89
+ *path, ext = tokenize(path)
90
+ if node = dig(*path)
91
+ node.get_format(ext: ext)
92
+ end
93
+ end
94
+
95
+ def inspect
96
+ "<#{self.class}: resources=#{resources.map(&:request_path)} children=#{children.map(&:name).inspect}>"
97
+ end
98
+
99
+ def get(path)
100
+ *path, ext = tokenize(path)
101
+ dig(*path)
102
+ end
103
+ alias :[] :get
104
+
105
+ protected
106
+ def get_format(ext: "")
107
+ @resources[ext]
108
+ end
109
+
110
+ def remove_child(path)
111
+ *_, segment, _ = tokenize(path)
112
+ @children.delete(segment)
113
+ end
114
+
115
+ def add_format(asset: , ext: )
116
+ resource = Resource.new(asset: asset, node: self, ext: ext)
117
+ if @resources.has_key? ext
118
+ raise Mascot::ExistingRequestPathError, "Resource at #{resource.request_path} already set"
119
+ else
120
+ @resources[ext] = resource
121
+ end
122
+ end
123
+
124
+ # TODO: I don't really like how the path is broken up with the "ext" at the end.
125
+ # It feels inconsistent. Either make an object/struct that encaspulates this or
126
+ # just pass `index.html` through to the end.
127
+ def dig(*args)
128
+ head, *tail = args
129
+ if head.nil? and tail.empty?
130
+ self
131
+ elsif @children.has_key?(head)
132
+ @children[head].dig(*tail)
133
+ else
134
+ nil
135
+ end
136
+ end
137
+
138
+ private
139
+ # Returns all of the names for the path along with the format, if set.
140
+ def tokenize(path)
141
+ return path if path.respond_to? :to_a
142
+ path, _, file = path.gsub(/^\//, "").rpartition(@delimiter)
143
+ ext = File.extname(file)
144
+ file = File.basename(file, ext)
145
+ path.split(@delimiter).push(file).push(ext)
146
+ end
147
+ end
148
+ end
data/lib/mascot/site.rb CHANGED
@@ -22,11 +22,16 @@ module Mascot
22
22
  end
23
23
  end
24
24
 
25
+ def glob(glob)
26
+ paths = safe_root.glob(root.join(glob))
27
+ resources.select{ |r| paths.include? r.asset.path.to_s }
28
+ end
29
+
25
30
  # Returns a list of resources.
26
31
  def resources
27
- Resources.new(root_file_path: root).tap do |resources|
28
- assets.each { |a| resources.add_asset a }
29
- resources_pipeline.process resources
32
+ ResourcesNode.new.tap do |root_node|
33
+ assets.each { |a| root_node.add path: asset_path_to_request_path(a), asset: a }
34
+ resources_pipeline.process root_node
30
35
  end
31
36
  end
32
37
 
@@ -38,7 +43,7 @@ module Mascot
38
43
 
39
44
  # Find the page with a path.
40
45
  def get(request_path)
41
- resources.get(request_path)
46
+ resources.get_resource(request_path)
42
47
  end
43
48
 
44
49
  def root=(path)
@@ -50,6 +55,13 @@ module Mascot
50
55
  end
51
56
 
52
57
  private
58
+ # Given a @file_path of `/hi`, this method changes `/hi/there/friend.html.erb`
59
+ # to an absolute `/there/friend` format by removing the file extensions
60
+ def asset_path_to_request_path(asset)
61
+ # Relative path of resource to the file_path of this project.
62
+ asset.path.dirname.join(asset.format_basename).relative_path_from(root).to_s
63
+ end
64
+
53
65
  def safe_root
54
66
  SafeRoot.new(path: root)
55
67
  end
@@ -1,3 +1,3 @@
1
1
  module Mascot
2
- VERSION = "0.1.11"
2
+ VERSION = "0.1.12"
3
3
  end
data/lib/mascot.rb CHANGED
@@ -13,8 +13,8 @@ module Mascot
13
13
  autoload :Asset, "mascot/asset"
14
14
  autoload :Frontmatter, "mascot/frontmatter"
15
15
  autoload :ResourcesPipeline, "mascot/resources_pipeline"
16
- autoload :Resources, "mascot/resources"
17
16
  autoload :Resource, "mascot/resource"
17
+ autoload :ResourcesNode, "mascot/resources_node"
18
18
  autoload :SafeRoot, "mascot/safe_root"
19
- autoload :Site, "mascot/site"
19
+ autoload :Site, "mascot/site"
20
20
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mascot
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.11
4
+ version: 0.1.12
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-08-10 00:00:00.000000000 Z
11
+ date: 2016-08-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -79,7 +79,7 @@ files:
79
79
  - lib/mascot/extensions/proc_manipulator.rb
80
80
  - lib/mascot/frontmatter.rb
81
81
  - lib/mascot/resource.rb
82
- - lib/mascot/resources.rb
82
+ - lib/mascot/resources_node.rb
83
83
  - lib/mascot/resources_pipeline.rb
84
84
  - lib/mascot/safe_root.rb
85
85
  - lib/mascot/site.rb
@@ -1,103 +0,0 @@
1
- require "forwardable"
2
- require "pathname"
3
-
4
- module Mascot
5
- class Resources
6
- include Enumerable
7
-
8
- extend Forwardable
9
- def_delegators :@routes, :size, :empty?, :any?, :clear
10
-
11
- def initialize(root_file_path: )
12
- @routes = Hash.new
13
- @root_file_path = Pathname.new(root_file_path)
14
- end
15
-
16
- def each(&block)
17
- @routes.values.each(&block)
18
- end
19
-
20
- def last
21
- @routes.values.last
22
- end
23
-
24
- def request_paths
25
- @routes.keys
26
- end
27
-
28
- def glob(pattern = "**/**")
29
- paths = safe_root.glob @root_file_path.join(pattern)
30
- select { |r| paths.include? r.asset.path.to_s}
31
- end
32
-
33
- def get(request_path)
34
- return if request_path.nil?
35
- @routes[key(request_path)]
36
- end
37
-
38
- def add(resource)
39
- validate_request_path resource
40
- validate_uniqueness resource
41
-
42
- resource.add_observer self
43
- @routes[key(resource)] = resource
44
- end
45
-
46
- def update(resource, old_request_path)
47
- validate_request_path old_request_path
48
- validate_request_path resource
49
- validate_uniqueness resource
50
-
51
- @routes.delete key(old_request_path)
52
- @routes[key(resource)] = resource
53
- end
54
-
55
- def remove(resource)
56
- validate_request_path resource
57
- resource.delete_observer self
58
- @routes.delete key(resource)
59
- resource
60
- end
61
-
62
- def add_asset(asset, request_path: nil)
63
- add Resource.new asset: asset, request_path: asset_path_to_request_path(request_path || asset.to_request_path)
64
- end
65
-
66
- private
67
- def key(path)
68
- # TODO: Conslidate this into SafeRoot.
69
- File.join "/", validate_request_path(coerce_request_path(path))
70
- end
71
-
72
- def coerce_request_path(resource)
73
- resource.respond_to?(:request_path) ? resource.request_path : resource
74
- end
75
-
76
- def validate_request_path(path)
77
- path = coerce_request_path(path)
78
- raise InvalidRequestPathError, "path can't be nil" if path.nil?
79
- path
80
- end
81
-
82
- # Raise an exception if the user tries to add a Resource with an existing request path.
83
- def validate_uniqueness(resource)
84
- path = coerce_request_path(resource)
85
- if existing_resource = get(path)
86
- raise ExistingRequestPathError, "Resource #{existing_resource} already exists at #{path}"
87
- else
88
- resource
89
- end
90
- end
91
-
92
- # Given a @file_path of `/hi`, this method changes `/hi/there/friend.html.erb`
93
- # to an absolute `/there/friend` format by removing the file extensions
94
- def asset_path_to_request_path(path)
95
- # Relative path of resource to the file_path of this project.
96
- relative_path = Pathname.new(path).relative_path_from(@root_file_path)
97
- end
98
-
99
- def safe_root
100
- @safe_root ||= SafeRoot.new(path: @root_file_path)
101
- end
102
- end
103
- end