decant 0.1.0 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +13 -0
- data/README.md +4 -5
- data/lib/decant/collection.rb +56 -17
- data/lib/decant/content.rb +66 -0
- data/lib/decant/file.rb +21 -0
- data/lib/decant/frontmatter.rb +27 -0
- data/lib/decant/path_utils.rb +38 -0
- data/lib/decant/version.rb +1 -1
- data/lib/decant.rb +35 -4
- metadata +5 -4
- data/lib/decant/content_methods.rb +0 -26
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ff0fe21b57ea4db012b2cedce3a14b6e8b34186a85071525949431524f0cdc4b
|
4
|
+
data.tar.gz: a6953eace2994504c734d002f8357aacb1b0563a6a31513684826888c7aefe59
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 66aee785137091d9a134161e3b45cd60b85e7e8d1d4f5a9a7601d4b389b83a1f408d0b08bd15460a101379c00137d568ee6070a699b28571dfaab6aea1a8ec40
|
7
|
+
data.tar.gz: fc536bc67d8a596add87ebb75ded2cd81bbbda22f93e0ae7b1b9e6de01fc46c5b565b83415a119bf4edf37552e36efc98ca91550784cfb91e54e0781e03decd7
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,18 @@
|
|
1
1
|
# CHANGELOG
|
2
2
|
|
3
|
+
## Version 0.2.0 - 2024-10-13
|
4
|
+
|
5
|
+
- Add support for a content instance knowing its own `#slug` - its relative path within its collection:
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
Page = Decant.define(dir: 'content', ext: 'md')
|
9
|
+
|
10
|
+
page = Page.find('features/slugs')
|
11
|
+
page.path.expand_path # => "/Users/dave/my-website/content/features/slugs.md"
|
12
|
+
page.slug # => "features/slugs"
|
13
|
+
```
|
14
|
+
- `Collection` is no immutable and can no longer be changed after initialisation.
|
15
|
+
|
3
16
|
## Version 0.1.0 - 2024-08-11
|
4
17
|
|
5
18
|
- Initial release
|
data/README.md
CHANGED
@@ -22,9 +22,8 @@ Page = Decant.define(dir: '_pages', ext: 'md') do
|
|
22
22
|
frontmatter :title
|
23
23
|
|
24
24
|
# Add custom methods - it's a standard Ruby class.
|
25
|
-
def
|
26
|
-
#
|
27
|
-
Kramdown::Document.new(content).to_html
|
25
|
+
def shouty
|
26
|
+
"#{title.upcase}!!!"
|
28
27
|
end
|
29
28
|
end
|
30
29
|
```
|
@@ -45,10 +44,10 @@ You can fetch a `Page` instance by `.find`ing it by its extension-less path with
|
|
45
44
|
|
46
45
|
```ruby
|
47
46
|
about = Page.find('about')
|
48
|
-
about.content # => "About\n\nMore words.\n"
|
47
|
+
about.content # => "# About\n\nMore words.\n"
|
49
48
|
about.frontmatter # => {:title=>"About", :stuff=>"nonsense"}
|
50
|
-
about.html # => "<h1 id=\"about\">About</h1>\n\n<p>More words.</p>\n"
|
51
49
|
about.title # => "About"
|
50
|
+
about.shouty # => "ABOUT!!!"
|
52
51
|
```
|
53
52
|
|
54
53
|
## Contributing
|
data/lib/decant/collection.rb
CHANGED
@@ -1,38 +1,77 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
require 'pathname'
|
3
|
+
require_relative 'path_utils'
|
3
4
|
|
4
5
|
module Decant
|
5
6
|
class Collection
|
6
|
-
|
7
|
+
# @return [Pathname]
|
8
|
+
attr_reader :dir
|
7
9
|
|
10
|
+
# @return [String, nil]
|
11
|
+
attr_reader :ext
|
12
|
+
|
13
|
+
# @param dir [Pathname, String]
|
14
|
+
# @param ext [String, nil]
|
8
15
|
def initialize(dir:, ext: nil)
|
9
16
|
self.dir = dir
|
10
17
|
self.ext = ext
|
11
18
|
end
|
12
19
|
|
13
|
-
|
14
|
-
@dir = Pathname.new(value)
|
15
|
-
end
|
16
|
-
|
20
|
+
# @return [Array<Pathname>]
|
17
21
|
def entries
|
18
|
-
glob(
|
22
|
+
glob('**/*')
|
19
23
|
end
|
20
24
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
25
|
+
# If {#ext} is defined then +pattern+ MUST NOT include the file's extension
|
26
|
+
# as it will automatically be added, if {#ext} is +nil+ then +pattern+ MUST
|
27
|
+
# include the file's extension - essentially becoming the file's full
|
28
|
+
# relative path within {#dir}.
|
29
|
+
#
|
30
|
+
# Technically +pattern+ can be any pattern supported by +Dir.glob+ though
|
31
|
+
# it's more likely to simply be a file name.
|
32
|
+
#
|
33
|
+
# @param pattern [String]
|
34
|
+
#
|
35
|
+
# @return [Pathname, nil]
|
36
|
+
def find(pattern)
|
31
37
|
glob(pattern).first
|
32
38
|
end
|
33
39
|
|
40
|
+
# @param pattern [String]
|
41
|
+
#
|
42
|
+
# @return [Array<Pathname>]
|
34
43
|
def glob(pattern)
|
35
|
-
dir.glob(pattern).select { |path| path.file? }
|
44
|
+
dir.glob("#{pattern}#{ext}").select { |path| path.file? }
|
45
|
+
end
|
46
|
+
|
47
|
+
# The extension-less relative path of +path+ within {#dir}.
|
48
|
+
#
|
49
|
+
# @param path [Pathname]
|
50
|
+
#
|
51
|
+
# @return [String]
|
52
|
+
def slug_for(path)
|
53
|
+
relative_path = path.relative_path_from(dir).to_s
|
54
|
+
|
55
|
+
# The collection has no configured extension, files are identified by
|
56
|
+
# their full (relative) path so there's no extension to remove.
|
57
|
+
return relative_path if @delete_ext_regexp.nil?
|
58
|
+
|
59
|
+
relative_path.sub(@delete_ext_regexp, '')
|
36
60
|
end
|
61
|
+
|
62
|
+
private
|
63
|
+
def dir=(value)
|
64
|
+
@dir = Pathname.new(value)
|
65
|
+
end
|
66
|
+
|
67
|
+
def ext=(value)
|
68
|
+
if value
|
69
|
+
@ext = value.start_with?('.') ? value : ".#{value}"
|
70
|
+
@delete_ext_regexp = PathUtils.delete_ext_regexp(ext)
|
71
|
+
else
|
72
|
+
@ext = nil
|
73
|
+
@delete_ext_regexp = nil
|
74
|
+
end
|
75
|
+
end
|
37
76
|
end
|
38
77
|
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require_relative 'errors'
|
3
|
+
require_relative 'file'
|
4
|
+
|
5
|
+
module Decant
|
6
|
+
class Content < File
|
7
|
+
class << self
|
8
|
+
# When using {Decant.define} the returned {Content} subclass comes with
|
9
|
+
# its own {Collection} instance.
|
10
|
+
#
|
11
|
+
# @return [Collection]
|
12
|
+
attr_reader :collection
|
13
|
+
|
14
|
+
# Return all matching files within {.collection}.
|
15
|
+
#
|
16
|
+
# @return [Array<Content>]
|
17
|
+
def all
|
18
|
+
collection.entries.map { |path| new(path) }
|
19
|
+
end
|
20
|
+
|
21
|
+
# Find a file within the {.collection} by passing its relative path.
|
22
|
+
#
|
23
|
+
# @example Find a specific nested file within a content directory
|
24
|
+
# Page = Decant.define(dir: 'content', ext: '.md')
|
25
|
+
#
|
26
|
+
# # Return an instance for the file `content/features/nesting.md`.
|
27
|
+
# Page.find('features/nesting')
|
28
|
+
#
|
29
|
+
# @param pattern [String]
|
30
|
+
#
|
31
|
+
# @return [Content]
|
32
|
+
#
|
33
|
+
# @raise [FileNotFound] if a matching file cannot be found
|
34
|
+
def find(pattern)
|
35
|
+
path = collection.find(pattern)
|
36
|
+
raise FileNotFound, %(Couldn't find "#{pattern}" in "#{collection.dir}") unless path
|
37
|
+
new(path)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Define convenience frontmatter readers - see {Decant.define}.
|
41
|
+
#
|
42
|
+
# @param attrs [Array<Symbol>] a list of convenience frontmatter readers.
|
43
|
+
def frontmatter(*attrs)
|
44
|
+
attrs.each do |name|
|
45
|
+
define_method name do
|
46
|
+
frontmatter&.[](name.to_sym)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# The extension-less relative path of the file within its collection.
|
53
|
+
#
|
54
|
+
# @example
|
55
|
+
# Page = Decant.define(dir: 'content', ext: 'md')
|
56
|
+
#
|
57
|
+
# page = Page.find('features/slugs')
|
58
|
+
# page.path.expand_path # => "/Users/dave/my-website/content/features/slugs.md"
|
59
|
+
# page.slug # => "features/slugs"
|
60
|
+
#
|
61
|
+
# @return [String]
|
62
|
+
def slug
|
63
|
+
self.class.collection.slug_for(path)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
data/lib/decant/file.rb
CHANGED
@@ -3,25 +3,46 @@ require_relative 'frontmatter'
|
|
3
3
|
|
4
4
|
module Decant
|
5
5
|
class File
|
6
|
+
# @return [Pathname]
|
6
7
|
attr_reader :path
|
7
8
|
|
9
|
+
# @param path [Pathname]
|
8
10
|
def initialize(path)
|
9
11
|
@path = path
|
10
12
|
end
|
11
13
|
|
14
|
+
# The "content" part of the file at {#path} - everything after the end of
|
15
|
+
# the frontmatter definition (see {Frontmatter.load} for more about
|
16
|
+
# frontmatter).
|
17
|
+
#
|
18
|
+
# @return [String]
|
12
19
|
def content
|
13
20
|
frontmatter_content[1]
|
14
21
|
end
|
15
22
|
|
23
|
+
# The frontmatter data contained in the file at {#path} or +nil+ if there's
|
24
|
+
# none (see {Frontmatter.load} for more about frontmatter).
|
25
|
+
#
|
26
|
+
# @return [Hash<Symbol, anything>, nil]
|
16
27
|
def frontmatter
|
17
28
|
frontmatter_content[0]
|
18
29
|
end
|
19
30
|
|
31
|
+
# When passing a +key+ the return value indicates whether {#frontmatter} has
|
32
|
+
# the +key+, when no +key+ is passed it indicates whether the file has any
|
33
|
+
# frontmatter at all.
|
34
|
+
#
|
35
|
+
# @param key [Symbol, nil]
|
36
|
+
#
|
37
|
+
# @return [Boolean]
|
20
38
|
def frontmatter?(key = nil)
|
21
39
|
return false if frontmatter.nil?
|
22
40
|
key ? frontmatter.key?(key) : true
|
23
41
|
end
|
24
42
|
|
43
|
+
# The full untouched contents of the file at {#path}.
|
44
|
+
#
|
45
|
+
# @return [String]
|
25
46
|
def read
|
26
47
|
path.read
|
27
48
|
end
|
data/lib/decant/frontmatter.rb
CHANGED
@@ -4,6 +4,33 @@ require 'yaml'
|
|
4
4
|
|
5
5
|
module Decant
|
6
6
|
module Frontmatter
|
7
|
+
# Parse a +String+ input (the contents of a file) into its frontmatter /
|
8
|
+
# content constituents.
|
9
|
+
#
|
10
|
+
# For frontmatter to be valid/detected the +input+ must start with a line
|
11
|
+
# consisting of three dashes +---+, then the YAML, then another line of
|
12
|
+
# three dashes. The returned +Hash+ will have +Symbol+ keys.
|
13
|
+
#
|
14
|
+
# Technically frontmatter can be any valid YAML not just key/value pairs but
|
15
|
+
# this would be very unusual and wouldn't be compatible with other
|
16
|
+
# frontmatter-related expectations like {Content.frontmatter}.
|
17
|
+
#
|
18
|
+
# @example Input with valid frontmatter
|
19
|
+
# ---
|
20
|
+
# title: Frontmatter
|
21
|
+
# ---
|
22
|
+
# The rest of the content
|
23
|
+
#
|
24
|
+
# @example Result of loading the above input
|
25
|
+
# Decant::Frontmatter.load(string)
|
26
|
+
# # => [{:title=>"Frontmatter"}, "The rest of the content"]
|
27
|
+
#
|
28
|
+
# @param input [String]
|
29
|
+
#
|
30
|
+
# @return [Array([Hash<Symbol, anything>, nil], String)] a frontmatter /
|
31
|
+
# content tuple. If +input+ contains frontmatter then the YAML will be
|
32
|
+
# parsed into a +Hash+ with +Symbol+ keys, if it doesn't have frontmatter
|
33
|
+
# then it will be +nil+.
|
7
34
|
def self.load(input)
|
8
35
|
return [nil, input] unless input.start_with?("---\n")
|
9
36
|
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'strscan'
|
3
|
+
|
4
|
+
module Decant
|
5
|
+
module PathUtils
|
6
|
+
# Generate a regular expression to strip a matching extension from a string.
|
7
|
+
# Supports similar shell-like pattern syntax as +Dir.glob+ including +.*+
|
8
|
+
# to remove any extension and +.{a,b}+ to remove either an +.a+ or +.b+
|
9
|
+
# extension. Used internally by {Content#slug}.
|
10
|
+
#
|
11
|
+
# @param pattern [String]
|
12
|
+
#
|
13
|
+
# @return [Regexp]
|
14
|
+
#
|
15
|
+
# @raise [RegexpError] if the regular expression cannot be generated, for
|
16
|
+
# instance if +pattern+ includes unbalanced shell-like expansion brackets
|
17
|
+
def self.delete_ext_regexp(pattern)
|
18
|
+
scanner = StringScanner.new(pattern)
|
19
|
+
regexp = String.new
|
20
|
+
|
21
|
+
while (ch = scanner.getch)
|
22
|
+
regexp << case ch
|
23
|
+
when '.' then '\.'
|
24
|
+
when '{' then '(?:'
|
25
|
+
when ',' then '|'
|
26
|
+
when '}' then ')'
|
27
|
+
when '*' then '[^\.]+'
|
28
|
+
else
|
29
|
+
ch
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
regexp << '$'
|
34
|
+
|
35
|
+
Regexp.new(regexp)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
data/lib/decant/version.rb
CHANGED
data/lib/decant.rb
CHANGED
@@ -1,13 +1,44 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
require_relative 'decant/collection'
|
3
|
-
require_relative 'decant/
|
4
|
-
require_relative 'decant/file'
|
3
|
+
require_relative 'decant/content'
|
5
4
|
require_relative 'decant/version'
|
6
5
|
|
7
6
|
module Decant
|
7
|
+
# Defines a new {Content} subclass and assigns it a new {Collection} with
|
8
|
+
# +dir+/+ext+. Passing a block lets you to declare convenience frontmatter
|
9
|
+
# readers and add your own methods.
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# Page = Decant.define(dir: 'content', ext: 'md') do
|
13
|
+
# frontmatter :title
|
14
|
+
#
|
15
|
+
# def shouty
|
16
|
+
# "#{title.upcase}!!!"
|
17
|
+
# end
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# # Given a file `content/about.md` with the following contents:
|
21
|
+
# #
|
22
|
+
# # ---
|
23
|
+
# # title: About
|
24
|
+
# # ---
|
25
|
+
# # About Decant
|
26
|
+
#
|
27
|
+
# about = Page.find('about')
|
28
|
+
# about.content # => "About Decant"
|
29
|
+
# about.frontmatter # => {:title=>"About"}
|
30
|
+
# about.title # => "About"
|
31
|
+
# about.shouty # => "ABOUT!!!"
|
32
|
+
#
|
33
|
+
# @param dir [Pathname, String]
|
34
|
+
# @param ext [String, nil]
|
35
|
+
#
|
36
|
+
# @yield pass an optional block to declare convenience frontmatter readers
|
37
|
+
# with {Content.frontmatter} and add your own methods to the class.
|
38
|
+
#
|
39
|
+
# @return [Class<Content>]
|
8
40
|
def self.define(dir:, ext: nil, &block)
|
9
|
-
Class.new(
|
10
|
-
extend ContentMethods
|
41
|
+
Class.new(Content) do
|
11
42
|
@collection = Collection.new(dir: dir, ext: ext)
|
12
43
|
class_eval(&block) if block_given?
|
13
44
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: decant
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ben Pickles
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-10-13 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: A frontmatter-aware wrapper around a directory of static content
|
14
14
|
email:
|
@@ -25,10 +25,11 @@ files:
|
|
25
25
|
- Rakefile
|
26
26
|
- lib/decant.rb
|
27
27
|
- lib/decant/collection.rb
|
28
|
-
- lib/decant/
|
28
|
+
- lib/decant/content.rb
|
29
29
|
- lib/decant/errors.rb
|
30
30
|
- lib/decant/file.rb
|
31
31
|
- lib/decant/frontmatter.rb
|
32
|
+
- lib/decant/path_utils.rb
|
32
33
|
- lib/decant/version.rb
|
33
34
|
homepage: https://github.com/benpickles/decant
|
34
35
|
licenses:
|
@@ -53,7 +54,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
53
54
|
- !ruby/object:Gem::Version
|
54
55
|
version: '0'
|
55
56
|
requirements: []
|
56
|
-
rubygems_version: 3.5.
|
57
|
+
rubygems_version: 3.5.16
|
57
58
|
signing_key:
|
58
59
|
specification_version: 4
|
59
60
|
summary: A frontmatter-aware wrapper around a directory of static content
|
@@ -1,26 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
require_relative 'errors'
|
3
|
-
|
4
|
-
module Decant
|
5
|
-
module ContentMethods
|
6
|
-
attr_reader :collection
|
7
|
-
|
8
|
-
def all
|
9
|
-
collection.entries.map { |path| new(path) }
|
10
|
-
end
|
11
|
-
|
12
|
-
def find(pattern)
|
13
|
-
path = collection.find(pattern)
|
14
|
-
raise FileNotFound, %(Couldn't find "#{pattern}" in "#{collection.dir}") unless path
|
15
|
-
new(path)
|
16
|
-
end
|
17
|
-
|
18
|
-
def frontmatter(*attrs)
|
19
|
-
attrs.each do |name|
|
20
|
-
define_method name do
|
21
|
-
frontmatter&.[](name.to_sym)
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|
25
|
-
end
|
26
|
-
end
|