decant 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|