sitepress-core 4.1.1 → 5.0.0.beta2

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
  SHA256:
3
- metadata.gz: e15a99e5f701876c5de27b3d6af643814010867088c9bfc58807a099e11a44a8
4
- data.tar.gz: c1ad99de08178a0cd884a6e499712e16fee09501436e674d83e74c34ef533f03
3
+ metadata.gz: e2fb8dab8d987a3743411bfbdfd9acb8311988c609f903558fa5f21f05008325
4
+ data.tar.gz: 9af2d1acb781fbbaa6ba63ff0b43a4eca27be8db94cde531689e573298b89c38
5
5
  SHA512:
6
- metadata.gz: bd9755ddebfc9f65894a680f73fb4494abb27bc7a7f73b94c66733f2119a15ab7a69b70c22b19b6654af72235a71cc888b1f40b274d4ed8ccb5e53180a221f0b
7
- data.tar.gz: e70dd46ef793fa6734c6fe8859a1f95cd4e12392c416a55994a56f4bed6b07a59b1d2b916fb359c77b3c5e2563278ccfce8b4991fc1c04bb8c6a12fecfb39150
6
+ metadata.gz: 49778c133de40cd3a8f8aed1826ba742b1d6e4c7440b2076178ed18f6724cf1dbab3c78dd97e25536a547e0fecedf20233962a39198e962b7aac9a2cf8cd62b0
7
+ data.tar.gz: 523c0c1d7394fb75ff8864f206ca5c2891d712db1572f82bd8d722b0810fe3c910a346644817dba744d88621d513119f9cd7709b345095c10400b3c8934a6dd8
@@ -1,3 +1,5 @@
1
+ require "forwardable"
2
+
1
3
  module Sitepress
2
4
  module Data
3
5
  def self.manage(value)
@@ -44,7 +46,7 @@ module Sitepress
44
46
  def_delegators :@hash, :keys, :values, :key?
45
47
 
46
48
  def initialize(hash)
47
- @hash = hash
49
+ @hash = hash || {}
48
50
  end
49
51
 
50
52
  def fetch(key, *args, &block)
@@ -70,22 +72,23 @@ module Sitepress
70
72
  end
71
73
 
72
74
  def method_missing(name, *args, **kwargs, &block)
73
- if respond_to? name
74
- self.send name, *args, **kwargs, &block
75
- else
76
- key, modifier, _ = name.to_s.partition(/[!?]/)
77
-
78
- case modifier
79
- when ""
80
- self[key]
81
- when "!"
82
- self.fetch(key, *args, &block)
83
- when "?"
84
- !!self[key]
85
- end
75
+ key, modifier, _ = name.to_s.partition(/[!?]/)
76
+
77
+ case modifier
78
+ when ""
79
+ self[key]
80
+ when "!"
81
+ self.fetch(key, *args, &block)
82
+ when "?"
83
+ !!self[key]
86
84
  end
87
85
  end
88
86
 
87
+ def respond_to_missing?(name, include_private = false)
88
+ key, _, _ = name.to_s.partition(/[!?]/)
89
+ @hash.key?(key) || @hash.key?(key.to_sym) || super
90
+ end
91
+
89
92
  def dig(*args, **kwargs, &block)
90
93
  Data.manage @hash.dig(*args, **kwargs, &block)
91
94
  end
@@ -0,0 +1,56 @@
1
+ require "mime/types"
2
+
3
+ module Sitepress
4
+ # Maps a directory of files into a tree of nodes that form the navigational
5
+ # structure of a website. You can subclass this to handle different file
6
+ # types or directory structures.
7
+ class Directory
8
+ attr_reader :asset_paths
9
+
10
+ def initialize(path)
11
+ @asset_paths = AssetPaths.new(path: path)
12
+ end
13
+
14
+ # Mounts the source files from the path into the given node.
15
+ def mount(node)
16
+ asset_paths.each do |path|
17
+ if path.directory?
18
+ process_directory path, node
19
+ else
20
+ process_asset path, node
21
+ end
22
+ end
23
+ end
24
+
25
+ protected
26
+
27
+ def process_directory(path, node)
28
+ node_name = File.basename path
29
+ self.class.new(path).mount(node.child(node_name))
30
+ end
31
+
32
+ def process_asset(path, node)
33
+ source = source_for(path)
34
+ # Parse the path to get node_name and format for tree building
35
+ parsed_path = Path.new(path.to_s)
36
+ node.child(parsed_path.node_name).resources.add_asset(source, format: parsed_path.format)
37
+ end
38
+
39
+ def source_for(path)
40
+ mime = MIME::Types.type_for(path.to_s).first&.content_type
41
+
42
+ case mime
43
+ when *Image.mime_types
44
+ Image.new(path: path)
45
+ when *Page.mime_types, nil
46
+ # nil handles template files like .erb that have no MIME type
47
+ Page.new(path: path)
48
+ else
49
+ Static.new(path: path)
50
+ end
51
+ end
52
+ end
53
+
54
+ # Backwards compatibility
55
+ AssetNodeMapper = Directory
56
+ end
@@ -0,0 +1,53 @@
1
+ require "fastimage"
2
+
3
+ module Sitepress
4
+ # A source for image files. Extracts dimensions via fastimage.
5
+ #
6
+ # Example:
7
+ # image = Image.new(path: "photos/sunset.jpg")
8
+ # image.width # => 1920
9
+ # image.height # => 1080
10
+ # image.data["width"] # => 1920
11
+ #
12
+ class Image < Static
13
+ MIME_TYPES = %w[image/png image/jpeg image/gif image/webp].freeze
14
+
15
+ def self.mime_types
16
+ MIME_TYPES
17
+ end
18
+
19
+ def filename
20
+ path.basename.to_s
21
+ end
22
+
23
+ def size
24
+ exists? ? File.size(path) : nil
25
+ end
26
+
27
+ def width
28
+ dimensions[0]
29
+ end
30
+
31
+ def height
32
+ dimensions[1]
33
+ end
34
+
35
+ def data
36
+ @data ||= Data.manage({
37
+ "width" => width,
38
+ "height" => height
39
+ }.compact)
40
+ end
41
+
42
+ private
43
+
44
+ def dimensions
45
+ @dimensions ||= begin
46
+ return [nil, nil] unless exists?
47
+ FastImage.size(path.to_s) || [nil, nil]
48
+ rescue
49
+ [nil, nil]
50
+ end
51
+ end
52
+ end
53
+ end
@@ -1,3 +1,5 @@
1
+ require "forwardable"
2
+
1
3
  module Sitepress
2
4
  # Resource nodes give resources their parent/sibling/child relationships. The relationship are determined
3
5
  # by the `request_path` given to an asset when its added to a node. Given the `request_path` `/foo/bar/biz/buz.html`,
@@ -96,6 +98,10 @@ module Sitepress
96
98
  "<#{self.class}: name=#{name.inspect}, formats=#{formats.inspect}, children=#{children.map(&:name).inspect}, resource_request_paths=#{resources.map(&:request_path)}>"
97
99
  end
98
100
 
101
+ def <<(mountable)
102
+ mountable.mount(self)
103
+ end
104
+
99
105
  def dig(*args)
100
106
  head, *tail = args
101
107
  if (head.nil? or head.empty? or head == default_name) and tail.empty?
@@ -0,0 +1,107 @@
1
+ require "fileutils"
2
+ require "forwardable"
3
+
4
+ module Sitepress
5
+ # A source for text-based files that may have frontmatter.
6
+ # Handles parsing of YAML frontmatter and provides access to data and body.
7
+ class Page < Static
8
+ extend Forwardable
9
+
10
+ # MIME types that Page can handle - text-based content that may have frontmatter
11
+ MIME_TYPES = %w[
12
+ text/html
13
+ text/plain
14
+ text/markdown
15
+ text/x-web-markdown
16
+ text/css
17
+ text/javascript
18
+ application/json
19
+ application/xml
20
+ text/xml
21
+ image/svg+xml
22
+ text/x-haml
23
+ ].freeze
24
+
25
+ def self.mime_types
26
+ MIME_TYPES
27
+ end
28
+
29
+ # Parsers can be swapped out to deal with different types of resources, like Notion
30
+ # documents, JSON, exif data on images, etc.
31
+ DEFAULT_PARSER = Parsers::Frontmatter
32
+
33
+ attr_writer :body
34
+
35
+ def_delegator :renderer, :render, :serialize
36
+
37
+ def initialize(path:, parser: DEFAULT_PARSER)
38
+ super(path: path)
39
+ @parser_klass = parser
40
+ end
41
+
42
+ def data
43
+ @data ||= Data.manage(exists? ? parse_error { parser.data } : {})
44
+ end
45
+
46
+ def data=(data)
47
+ @data = Data.manage(data)
48
+ end
49
+
50
+ def body
51
+ @body ||= exists? ? parse_error { parser.body } : nil
52
+ end
53
+
54
+ # Returns the line number where the body starts in the original file.
55
+ # Used to adjust error line numbers when frontmatter is present.
56
+ def body_line_offset
57
+ exists? ? parser.body_line_offset : 1
58
+ end
59
+
60
+ # Treat sources with the same path as equal.
61
+ def ==(other)
62
+ path == other.path
63
+ end
64
+
65
+ # When changing the parser, clear all cached parsed data.
66
+ def parser=(parser_klass)
67
+ @parser = nil
68
+ @data = nil
69
+ @body = nil
70
+ @parser_klass = parser_klass
71
+ end
72
+
73
+ def updated_at
74
+ File.mtime path
75
+ end
76
+
77
+ def created_at
78
+ File.ctime path
79
+ end
80
+
81
+ def destroy
82
+ FileUtils.rm path
83
+ end
84
+
85
+ def save
86
+ File.write path, serialize
87
+ end
88
+
89
+ def renderer
90
+ @parser_klass::Renderer.new(data: data, body: body)
91
+ end
92
+
93
+ private
94
+ def parse_error(&parse)
95
+ parse.call
96
+ rescue StandardError => e
97
+ raise ParseError, "Error parsing #{File.expand_path(path)}: #{e.class} - #{e.message}"
98
+ end
99
+
100
+ def parser
101
+ @parser ||= @parser_klass.new File.read path
102
+ end
103
+ end
104
+
105
+ # Backwards compatibility
106
+ Asset = Page
107
+ end
@@ -10,6 +10,12 @@ module Sitepress
10
10
  @body = source
11
11
  @data = {}
12
12
  end
13
+
14
+ # Returns the line number where the body starts in the original file.
15
+ # Subclasses should override this if they strip content from the beginning.
16
+ def body_line_offset
17
+ 1
18
+ end
13
19
  end
14
20
  end
15
21
  end
@@ -14,12 +14,7 @@ module Sitepress
14
14
  end
15
15
 
16
16
  def dump_yaml(data)
17
- if YAML.respond_to? :safe_dump
18
- YAML.safe_dump data, permitted_classes: Frontmatter.permitted_classes
19
- else
20
- # Live dangerously, lol
21
- YAML.dump data
22
- end
17
+ YAML.safe_dump data, permitted_classes: Frontmatter.permitted_classes
23
18
  end
24
19
 
25
20
  def render
@@ -46,7 +41,13 @@ module Sitepress
46
41
  PATTERN = /\A(#{DELIMITER}#{NEWLINE}(.+?)#{NEWLINE}#{DELIMITER}#{NEWLINE}*)?(.+)\Z/m
47
42
 
48
43
  def initialize(content)
49
- _, @data, @body = content.match(PATTERN).captures
44
+ @frontmatter_block, @data, @body = content.match(PATTERN).captures
45
+ end
46
+
47
+ # Returns the line number where the body starts in the original file.
48
+ def body_line_offset
49
+ return 1 unless @frontmatter_block
50
+ @frontmatter_block.count("\n") + 1
50
51
  end
51
52
 
52
53
  def data
@@ -54,12 +55,7 @@ module Sitepress
54
55
  end
55
56
 
56
57
  def load_yaml(data)
57
- if YAML.respond_to? :safe_load
58
- YAML.safe_load data, permitted_classes: self.class.permitted_classes
59
- else
60
- # Live dangerously, lol
61
- YAML.load data
62
- end
58
+ YAML.safe_load data, permitted_classes: self.class.permitted_classes
63
59
  end
64
60
 
65
61
  class << self
@@ -1,4 +1,5 @@
1
1
  require "yaml"
2
+ require "strscan"
2
3
 
3
4
  module Sitepress
4
5
  module Parsers
@@ -19,10 +20,17 @@ module Sitepress
19
20
  @raw_data.append scanner.captures
20
21
  end
21
22
  scanner.scan(/\n/)
23
+ # Track where body starts for line number offset
24
+ @header_content = content[0, scanner.pos]
22
25
  # Parse body
23
26
  @body = scanner.rest
24
27
  end
25
28
 
29
+ # Returns the line number where the body starts in the original file.
30
+ def body_line_offset
31
+ @header_content.count("\n") + 1
32
+ end
33
+
26
34
  def data
27
35
  Hash[@raw_data.prepend([TITLE_KEY, @title])]
28
36
  end
@@ -23,14 +23,6 @@ module Sitepress
23
23
  action_view_template_handlers_extensions || HANDLER_EXTENSIONS
24
24
  end
25
25
 
26
- # I tried to hook this into Rails engines in the `config.after_initialize` block,
27
- # but the way template handlers register their extensions is across the board.
28
- #
29
- # config.after_initialize do
30
- # Sitepress::Path.handler_extensions = ActionView::Template::Handlers.method(:extensions)
31
- # ends
32
- #
33
- # I couldn't get that working, instead I do this check to find the handlers.
34
26
  def action_view_template_handlers_extensions
35
27
  ActionView::Template::Handlers.extensions if defined?(ActionView::Template::Handlers)
36
28
  end
@@ -1,26 +1,70 @@
1
1
  require "forwardable"
2
+ require "mime/types"
2
3
 
3
4
  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.
5
+ # Represents the web-facing view of a source file. A Resource wraps a source
6
+ # and adds web-serving concerns: request path, handler, format, and rendering.
7
+ #
8
+ # Source = file on disk (Pathname)
9
+ # Resource = web representation (Path parsing, rendering)
7
10
  class Resource
8
11
  extend Forwardable
9
- def_delegators :asset, :body, :data, :renderable?
12
+ def_delegators :source, :body
10
13
 
11
- attr_reader :node, :asset
14
+ # If we can't resolve a mime type for the resource, we'll fall
15
+ # back to this binary octet-stream type so the client can download
16
+ # the resource and figure out what to do with it.
17
+ DEFAULT_MIME_TYPE = MIME::Types["application/octet-stream"].first
18
+
19
+ attr_reader :node, :source, :source_path
20
+
21
+ alias :asset :source # Backwards compatibility
22
+
23
+ # Check if the source implements the data protocol.
24
+ def has_data?
25
+ source.respond_to?(:data)
26
+ end
27
+
28
+ # Delegate to source.
29
+ def data
30
+ has_data? ? source.data : Data.manage({})
31
+ end
32
+
33
+ # Delegate to source.
34
+ def fetch_data(key, *args, &block)
35
+ source.fetch_data(key, *args, &block)
36
+ end
12
37
 
13
38
  attr_accessor :format, :mime_type, :handler
14
39
 
15
40
  # Default scope for querying parent/child/sibling resources.
16
41
  DEFAULT_FILTER_SCOPE = :same
17
42
 
18
- def initialize(asset:, node:, format: nil, mime_type: nil, handler: nil)
19
- @asset = asset
43
+ def initialize(asset: nil, source: nil, node:, format: nil, mime_type: nil, handler: nil)
44
+ @source = source || asset
45
+ raise ArgumentError, "Either asset: or source: must be provided" unless @source
20
46
  @node = node
21
- @format = format || asset.format
22
- @mime_type = mime_type || asset.mime_type
23
- @handler = handler || asset.handler
47
+ # Parse the source path to extract handler, format, node_name (if source has path)
48
+ if @source.respond_to?(:path)
49
+ @source_path = Path.new(@source.path.to_s)
50
+ @format = format || @source_path.format
51
+ @handler = handler || @source_path.handler
52
+ else
53
+ @source_path = nil
54
+ @format = format
55
+ @handler = handler
56
+ end
57
+ @mime_type = mime_type || inferred_mime_type || DEFAULT_MIME_TYPE
58
+ end
59
+
60
+ # The node name is derived from the source path
61
+ def node_name
62
+ @source_path&.node_name
63
+ end
64
+
65
+ # Whether this resource can be rendered (has a template handler)
66
+ def renderable?
67
+ !!handler
24
68
  end
25
69
 
26
70
  def request_path
@@ -71,7 +115,7 @@ module Sitepress
71
115
  # Clones should be initialized with a nil node. Initializing with a node would mean that multiple resources
72
116
  # are pointing to the same node, which shouldn't be possible.
73
117
  def clone
74
- self.class.new(asset: @asset, node: nil, format: @format, mime_type: @mime_type, handler: @handler)
118
+ self.class.new(source: @source, node: nil, format: @format, mime_type: @mime_type, handler: @handler)
75
119
  end
76
120
 
77
121
  # Removes the resource from the node's resources list.
@@ -80,7 +124,7 @@ module Sitepress
80
124
  end
81
125
 
82
126
  def inspect
83
- "<#{self.class}:#{object_id} request_path=#{request_path.inspect} asset_path=#{asset.path.to_s.inspect}>"
127
+ "#<#{self.class}:0x#{object_id.to_s(16)} request_path=#{request_path.inspect} source=#{source.inspect}>"
84
128
  end
85
129
 
86
130
  def parent(**args)
@@ -108,8 +152,16 @@ module Sitepress
108
152
  node.parents.reject(&:root?).reverse.map(&:name)
109
153
  end
110
154
 
155
+ # Renders the resource in a view context using the appropriate template handler.
111
156
  def render_in(view_context)
112
- view_context.render inline: body, type: handler
157
+ return nil unless renderable?
158
+ template = ActionView::Template.new(
159
+ body,
160
+ source.path.to_s,
161
+ ActionView::Template.handler_for_extension(handler),
162
+ locals: []
163
+ )
164
+ template.render(view_context, {})
113
165
  end
114
166
 
115
167
  private
@@ -135,6 +187,12 @@ module Sitepress
135
187
  end
136
188
  end
137
189
 
190
+ # Returns the mime type inferred from the format extension
191
+ def inferred_mime_type
192
+ format_extension = format&.to_s
193
+ MIME::Types.type_for(format_extension).first if format_extension
194
+ end
195
+
138
196
  # Deals with situations, particularly in the root node and other "index" nodes, for the `request_path`
139
197
  def request_filename
140
198
  if node.root? and node.default_format == format
@@ -1,3 +1,6 @@
1
+ require "forwardable"
2
+ require "set"
3
+
1
4
  module Sitepress
2
5
  # Flattens a tree of Sitepress::Node and Sitepress:Resource classes into a collection of
3
6
  # resources that can be quickly globbed, queried, or accessed.
@@ -17,8 +20,8 @@ module Sitepress
17
20
  end
18
21
 
19
22
  def glob(pattern)
20
- paths = Dir.glob root_path.join(pattern)
21
- resources.select { |r| paths.include? r.asset.path.to_s }
23
+ paths = Dir.glob(root_path.join(pattern)).to_set
24
+ resources.select { |r| paths.include?(r.asset.path.to_s) }
22
25
  end
23
26
 
24
27
  def get(request_path)
@@ -1,5 +1,3 @@
1
- require "forwardable"
2
-
3
1
  module Sitepress
4
2
  # Processes a collection of resources
5
3
  class ResourcesPipeline < Array
@@ -19,18 +19,18 @@ module Sitepress
19
19
  self.root_path = root_path
20
20
  end
21
21
 
22
- # A tree representation of the resourecs wthin the site. The root is a node that's
22
+ # A tree representation of the resources within the site. The root is a node that's
23
23
  # processed by the `resources_pipeline`.
24
24
  def root
25
25
  @root ||= Node.new.tap do |root|
26
- asset_node_mapper(root).map
26
+ root << asset_node_mapper
27
27
  resources_pipeline.process root
28
28
  end
29
29
  end
30
30
 
31
31
  # Maps a path of directories and files into the root node.
32
- def asset_node_mapper(root_node)
33
- AssetNodeMapper.new(path: pages_path, node: root_node)
32
+ def asset_node_mapper
33
+ Directory.new(pages_path)
34
34
  end
35
35
 
36
36
  # Returns a list of all the resources within #root.
@@ -0,0 +1,45 @@
1
+ require "mime/types"
2
+
3
+ module Sitepress
4
+ # Base class for source files. A source represents a file on disk
5
+ # without any web-serving concerns (handlers, formats, etc.).
6
+ #
7
+ # Example:
8
+ # source = Static.new(path: "fonts/roboto.woff2")
9
+ # source.mime_type # => #<MIME::Type font/woff2>
10
+ # source.body # => binary content
11
+ #
12
+ class Static
13
+ attr_reader :path
14
+
15
+ def initialize(path:)
16
+ @path = Pathname.new(path)
17
+ end
18
+
19
+ def exists?
20
+ path.exist?
21
+ end
22
+
23
+ def mime_type
24
+ MIME::Types.type_for(path.to_s).first
25
+ end
26
+
27
+ def body
28
+ File.binread(path)
29
+ end
30
+
31
+ def data
32
+ @data ||= Data.manage({})
33
+ end
34
+
35
+ def fetch_data(key, *args, &block)
36
+ data.fetch(key, *args, &block)
37
+ rescue KeyError
38
+ raise KeyError, "key not found: #{key.inspect} in #{path}"
39
+ end
40
+
41
+ def inspect
42
+ "#<#{self.class}:0x#{object_id.to_s(16)} path=#{path.to_s.inspect}>"
43
+ end
44
+ end
45
+ end
@@ -1,3 +1,3 @@
1
1
  module Sitepress
2
- VERSION = "4.1.1".freeze
2
+ VERSION = "5.0.0.beta2".freeze
3
3
  end
@@ -4,17 +4,23 @@ module Sitepress
4
4
  # Errors raised by Sitepress
5
5
  Error = Class.new(StandardError)
6
6
 
7
+ # Raised when an asset fails to parse (e.g., invalid YAML frontmatter)
8
+ ParseError = Class.new(Error)
9
+
7
10
  # Raised by Resources if a path is added that's not a valid path.
8
11
  InvalidRequestPathError = Class.new(RuntimeError)
9
12
 
10
13
  # Raised by Resources if a path is already in its index
11
14
  ExistingRequestPathError = Class.new(InvalidRequestPathError)
12
15
 
13
- autoload :Asset, "sitepress/asset"
14
- autoload :AssetNodeMapper, "sitepress/asset_node_mapper"
16
+ autoload :Asset, "sitepress/page" # Backwards compatibility
17
+ autoload :AssetNodeMapper, "sitepress/directory" # Backwards compatibility
15
18
  autoload :AssetPaths, "sitepress/asset_paths"
19
+ autoload :Directory, "sitepress/directory"
16
20
  autoload :Data, "sitepress/data"
21
+ autoload :Image, "sitepress/image"
17
22
  autoload :Node, "sitepress/node"
23
+ autoload :Page, "sitepress/page"
18
24
  autoload :Path, "sitepress/path"
19
25
  autoload :Parsers, "sitepress/parsers"
20
26
  autoload :Resource, "sitepress/resource"
@@ -22,4 +28,5 @@ module Sitepress
22
28
  autoload :ResourceIndexer, "sitepress/resource_indexer"
23
29
  autoload :ResourcesPipeline, "sitepress/resources_pipeline"
24
30
  autoload :Site, "sitepress/site"
31
+ autoload :Static, "sitepress/static"
25
32
  end
@@ -26,4 +26,5 @@ Gem::Specification.new do |spec|
26
26
  spec.add_development_dependency "rspec", "~> 3.0"
27
27
 
28
28
  spec.add_runtime_dependency "mime-types", ">= 2.99"
29
+ spec.add_runtime_dependency "fastimage", "~> 2.0"
29
30
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sitepress-core
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.1.1
4
+ version: 5.0.0.beta2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brad Gessler
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-11-17 00:00:00.000000000 Z
10
+ date: 2026-03-04 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: bundler
@@ -65,6 +65,20 @@ dependencies:
65
65
  - - ">="
66
66
  - !ruby/object:Gem::Version
67
67
  version: '2.99'
68
+ - !ruby/object:Gem::Dependency
69
+ name: fastimage
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '2.0'
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '2.0'
68
82
  email:
69
83
  - bradgessler@gmail.com
70
84
  executables: []
@@ -72,13 +86,14 @@ extensions: []
72
86
  extra_rdoc_files: []
73
87
  files:
74
88
  - lib/sitepress-core.rb
75
- - lib/sitepress/asset.rb
76
- - lib/sitepress/asset_node_mapper.rb
77
89
  - lib/sitepress/asset_paths.rb
78
90
  - lib/sitepress/data.rb
91
+ - lib/sitepress/directory.rb
79
92
  - lib/sitepress/extensions/layouts.rb
80
93
  - lib/sitepress/extensions/proc_manipulator.rb
94
+ - lib/sitepress/image.rb
81
95
  - lib/sitepress/node.rb
96
+ - lib/sitepress/page.rb
82
97
  - lib/sitepress/parsers.rb
83
98
  - lib/sitepress/parsers/base.rb
84
99
  - lib/sitepress/parsers/frontmatter.rb
@@ -89,6 +104,7 @@ files:
89
104
  - lib/sitepress/resources.rb
90
105
  - lib/sitepress/resources_pipeline.rb
91
106
  - lib/sitepress/site.rb
107
+ - lib/sitepress/static.rb
92
108
  - lib/sitepress/version.rb
93
109
  - sitepress-core.gemspec
94
110
  homepage: https://sitepress.cc/
@@ -1,107 +0,0 @@
1
- require "mime/types"
2
- require "forwardable"
3
- require "fileutils"
4
-
5
- module Sitepress
6
- # Represents a file on a web server that may be parsed to extract
7
- # metadata 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
- # Parsers can be swapped out to deal with different types of resources, like Notion
17
- # documents, JSON, exif data on images, etc.
18
- DEFAULT_PARSER = Parsers::Frontmatter
19
-
20
- attr_reader :path
21
- attr_writer :body
22
-
23
- extend Forwardable
24
- def_delegators :renderer, :render
25
- def_delegators :path, :handler, :node_name, :format, :exists?
26
-
27
- def initialize(path:, mime_type: nil, parser: DEFAULT_PARSER)
28
- # The MIME::Types gem returns an array when types are looked up.
29
- # This grabs the first one, which is likely the intent on these lookups.
30
- @mime_type = Array(mime_type).first
31
- @path = Path.new path
32
- @parser_klass = parser
33
- end
34
-
35
- def data
36
- @data ||= Data.manage(exists? ? parse_error { parser.data } : {})
37
- end
38
-
39
- def data=(data)
40
- @data = Data.manage(data)
41
- end
42
-
43
- def body
44
- @body ||= exists? ? parse_error { parser.body } : nil
45
- end
46
-
47
- # Treat resources with the same request path as equal.
48
- def ==(asset)
49
- path == asset.path
50
- end
51
-
52
- def mime_type
53
- @mime_type ||= inferred_mime_type || DEFAULT_MIME_TYPE
54
- end
55
-
56
- # Certain files, like binary file types, aren't something that we should try to
57
- # parse. When this returns true in some cases, a reference to the file will be
58
- # passed and skip all the overhead of trying to parse and render.
59
- def renderable?
60
- !!handler
61
- end
62
-
63
- # Mmm.... that's the smell of cache busting, which means the hiearchy of this is wrong.
64
- def parser=(parser_klass)
65
- @parser = nil
66
- @parser_klass = parser_klass
67
- end
68
-
69
- def updated_at
70
- File.mtime path
71
- end
72
-
73
- def created_at
74
- File.ctime path
75
- end
76
-
77
- def destroy
78
- FileUtils.rm path
79
- end
80
-
81
- def save
82
- File.write path, render
83
- end
84
-
85
- def renderer
86
- @parser_klass::Renderer.new(data: data, body: body)
87
- end
88
-
89
- private
90
- def parse_error(&parse)
91
- parse.call
92
- rescue StandardError => e
93
- raise ParseError.new("Error parsing asset at #{path.to_s}: #{e}")
94
- end
95
-
96
- def parser
97
- @parser ||= @parser_klass.new File.read path
98
- end
99
-
100
- # Returns the mime type of the file extension. If a type can't
101
- # be resolved then we'll just grab the first type.
102
- def inferred_mime_type
103
- format_extension = path.format&.to_s
104
- MIME::Types.type_for(format_extension).first if format_extension
105
- end
106
- end
107
- end
@@ -1,45 +0,0 @@
1
- module Sitepress
2
- # Maps a tree of Directory and Asset objects in a a tree of nodes that
3
- # format the navigational structure of a website. You can override this
4
- # this in a site to deal with different file systems. For example, Notion
5
- # has a completely different file structure for its content than Rails, so
6
- # we could extend this class to properly map those differences into a tree
7
- # of nodes.
8
- class AssetNodeMapper
9
- attr_reader :node, :asset_paths
10
-
11
- def initialize(path:, node:)
12
- @asset_paths = AssetPaths.new(path: path)
13
- @node = node
14
- end
15
-
16
- # Mounts the source files from the path to the given node.
17
- def map
18
- asset_paths.each do |path|
19
- if path.directory?
20
- process_directory path
21
- else
22
- process_asset path
23
- end
24
- end
25
- end
26
-
27
- protected
28
-
29
- def process_directory(path)
30
- node_name = File.basename path
31
- node_mapper path: path, node: node.child(node_name)
32
- end
33
-
34
- def process_asset(path)
35
- asset = Asset.new(path: path)
36
- node.child(asset.node_name).resources.add_asset(asset, format: asset.format)
37
- end
38
-
39
- private
40
-
41
- def node_mapper(*args, **kwargs)
42
- self.class.new(*args, **kwargs).map
43
- end
44
- end
45
- end