contentfs 0.0.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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 9dd7fc4b06ab77f27e8998c275cb3d4659dc450d013ebf2df47fd5609d9fe322
4
+ data.tar.gz: d289facaae4ca3c791054b632397efb2e2286c964602885888215b5584e06d4d
5
+ SHA512:
6
+ metadata.gz: dedfb6034eb79b0b09bbbb9645c8de6f04b9a8e6ff78a31186c11798317ca02807e00eed644bd980f57cb5407d13843d7cec1d0c6a3ec5efa8a0a755865c752a
7
+ data.tar.gz: 8a9010f2faf608345653e321310208dc2b85afafbc2985a6c9392ddccbfe548e6f500b146a66525aa3adda88320913544e9605ac4f5348de9598f276c6f7f8b1
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ This software is licensed under the MIT License.
2
+
3
+ Copyright 2020 Metabahn.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a
6
+ copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to permit
10
+ persons to whom the Software is furnished to do so, subject to the
11
+ following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included
14
+ in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
17
+ OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
19
+ NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
20
+ DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
21
+ OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
22
+ USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,92 @@
1
+ # contentfs
2
+
3
+ A structured content file system.
4
+
5
+ ## Install
6
+
7
+ ```
8
+ gem install contentfs
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ Content can be defined in a structure like this:
14
+
15
+ ```
16
+ docs/
17
+ api/
18
+ application/
19
+ content.md
20
+
21
+ class_api/
22
+ new.md
23
+ instance_api/
24
+ ...
25
+ guides/
26
+ ...
27
+ ```
28
+
29
+ Once defined, content can be accessed through ContentFS:
30
+
31
+ ```ruby
32
+ require "contentfs"
33
+
34
+ database = ContentFS::Database.load("path/to/docs")
35
+
36
+ database.docs.api.application.render
37
+ ```
38
+
39
+ The `content` name is special in that it defines content for the containing folder.
40
+
41
+ ### Formats
42
+
43
+ Markdown is supported by default. Simply add the `redcarpet` gem to your project's `Gemfile`. For automatic syntax highlighting, add the `rouge` to your `Gemfile` as well.
44
+
45
+ Unknown formats default to plain text.
46
+
47
+ ### Metadata
48
+
49
+ Metadata can be defined on content through front-matter:
50
+
51
+ ```
52
+ ---
53
+ option: value
54
+ ---
55
+
56
+ ...
57
+ ```
58
+
59
+ Metadata can be applied to all content in a folder by defining a `_metadata.yml` file. The folder's metadata is merged with metadata defined in front-matter, with precedence given to the front-matter metadata values.
60
+
61
+ ### Filtering
62
+
63
+ Content can be filtered by one or more metadata values:
64
+
65
+ ```ruby
66
+ database.filter(option: "value") do |content|
67
+ ...
68
+ end
69
+ ```
70
+
71
+ ### Iterating
72
+
73
+ Iterate over content using the `all` method:
74
+
75
+ ```ruby
76
+ database.content.all do |content|
77
+ ...
78
+ end
79
+ ```
80
+
81
+ ### Prefixes
82
+
83
+ Both folders and content can be defined with prefixes, useful for ordering:
84
+
85
+ ```
86
+ docs/
87
+ api/
88
+ 0000__application/
89
+ ...
90
+ ```
91
+
92
+ Characters up to `__` (double underscore) are considered part of the prefix.
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "contentfs/database"
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+ require "yaml"
5
+
6
+ require_relative "renderers"
7
+ require_relative "prefix"
8
+ require_relative "slug"
9
+
10
+ module ContentFS
11
+ # Structured content, loaded from the filesystem and usually exposed through a database.
12
+ #
13
+ class Content
14
+ class << self
15
+ def load(path, metadata: {})
16
+ new(path: path, metadata: metadata)
17
+ end
18
+ end
19
+
20
+ FRONT_MATTER_REGEXP = /\A---\s*\n(.*?\n?)^---\s*$\n?/m
21
+
22
+ attr_reader :format, :prefix, :slug, :metadata
23
+
24
+ def initialize(path:, metadata: {})
25
+ path = Pathname.new(path)
26
+ extname = path.extname
27
+ name = path.basename(extname)
28
+ prefix, remainder = Prefix.build(name)
29
+ @prefix = prefix
30
+ @format = extname.to_s[1..-1]&.to_sym
31
+ @slug = Slug.build(remainder)
32
+ @content = path.read
33
+ @metadata = metadata.merge(parse_metadata(@content))
34
+ end
35
+
36
+ def to_s
37
+ @content
38
+ end
39
+
40
+ def render
41
+ if @format && (renderer = Renderers.resolve(@format))
42
+ renderer.render(@content)
43
+ else
44
+ to_s
45
+ end
46
+ end
47
+
48
+ private def parse_metadata(content)
49
+ if (match = content.match(FRONT_MATTER_REGEXP))
50
+ YAML.safe_load(match.captures[0]).to_h
51
+ else
52
+ {}
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+
5
+ require_relative "content"
6
+ require_relative "prefix"
7
+ require_relative "slug"
8
+
9
+ module ContentFS
10
+ # Structured content database, loaded from the filesystem.
11
+ #
12
+ class Database
13
+ class << self
14
+ def load(path)
15
+ new(path: path)
16
+ end
17
+ end
18
+
19
+ METADATA_FILE = "_metadata.yml"
20
+
21
+ attr_reader :prefix, :slug
22
+
23
+ def initialize(path:)
24
+ path = Pathname.new(path)
25
+ name = path.basename(path.extname)
26
+ prefix, remainder = Prefix.build(name)
27
+ @prefix = prefix
28
+ @slug = Slug.build(remainder)
29
+ @children = {}
30
+ @nested = {}
31
+
32
+ metadata_path = path.join(METADATA_FILE)
33
+
34
+ metadata = if metadata_path.exist?
35
+ YAML.safe_load(metadata_path.read).to_h
36
+ else
37
+ {}
38
+ end
39
+
40
+ Pathname.new(path).glob("*") do |path|
41
+ next if path.basename.to_s.start_with?("_")
42
+
43
+ if path.directory?
44
+ database = Database.load(path)
45
+ @nested[database.slug] = database
46
+ else
47
+ content = Content.load(path, metadata: metadata)
48
+
49
+ if content.slug == :content
50
+ @content = content
51
+ else
52
+ @children[content.slug] = content
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ def all
59
+ return to_enum(:all) unless block_given?
60
+
61
+ @children.each_value do |value|
62
+ yield value
63
+ end
64
+ end
65
+
66
+ def filter(**filters)
67
+ return to_enum(:filter, **filters) unless block_given?
68
+
69
+ filters = filters.each_with_object({}) { |(key, value), hash|
70
+ hash[key.to_s] = value
71
+ }
72
+
73
+ @children.each_value.select { |content|
74
+ yield content if content.metadata.all? { |key, value|
75
+ filters[key] == value
76
+ }
77
+ }
78
+ end
79
+
80
+ def to_s
81
+ @content&.to_s.to_s
82
+ end
83
+
84
+ def render
85
+ @content&.render
86
+ end
87
+
88
+ def method_missing(name, *nested, **)
89
+ if @children.key?(name)
90
+ @children[name]
91
+ elsif @nested.key?(name)
92
+ nested.inject(@nested[name]) { |database, next_nested|
93
+ database.public_send(next_nested.to_sym)
94
+ }
95
+ else
96
+ super
97
+ end
98
+ end
99
+
100
+ def respond_to_missing?(name, *)
101
+ @children.key?(name) || super
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ContentFS
4
+ # @api private
5
+ class Prefix
6
+ class << self
7
+ def build(value)
8
+ parts = value.to_s.split("__", 2)
9
+
10
+ case parts.length
11
+ when 2
12
+ parts
13
+ when 1
14
+ [nil, parts[0]]
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ContentFS
4
+ module Renderers
5
+ class << self
6
+ def resolve(format)
7
+ renderers[format.to_sym].to_a.each do |renderer|
8
+ if (resolved = try(renderer))
9
+ return resolved
10
+ end
11
+ end
12
+
13
+ nil
14
+ end
15
+
16
+ def register(name, format:, constant:, path:)
17
+ (renderers[format.to_sym] ||= []) << {
18
+ name: name.to_sym,
19
+ constant: constant.to_s,
20
+ path: Pathname.new(path)
21
+ }
22
+ end
23
+
24
+ # @api private
25
+ private def try(renderer)
26
+ require(renderer[:path])
27
+ const_get(renderer[:constant])
28
+ rescue LoadError
29
+ # swallow load errors
30
+ rescue NameError
31
+ # TODO: maybe print name errors
32
+ rescue
33
+ # TODO: maybe print other errors
34
+ end
35
+
36
+ # @api private
37
+ private def renderers
38
+ @_renderers ||= {}
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ ContentFS::Renderers.register(
45
+ :markdown_code,
46
+ format: :md,
47
+ constant: "ContentFS::Renderers::Markdown::Code",
48
+ path: File.expand_path("../renderers/markdown/code", __FILE__)
49
+ )
50
+
51
+ ContentFS::Renderers.register(
52
+ :markdown,
53
+ format: :md,
54
+ constant: "ContentFS::Renderers::Markdown",
55
+ path: File.expand_path("../renderers/markdown", __FILE__)
56
+ )
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "redcarpet"
4
+
5
+ module ContentFS
6
+ module Renderers
7
+ # @api private
8
+ class Markdown
9
+ class << self
10
+ OPTIONS = {
11
+ autolink: true,
12
+ footnotes: true,
13
+ fenced_code_blocks: true,
14
+ tables: true
15
+ }.freeze
16
+
17
+ def render(content)
18
+ renderer.render(content)
19
+ end
20
+
21
+ def options
22
+ OPTIONS
23
+ end
24
+
25
+ private def renderer
26
+ @_renderer ||= Redcarpet::Markdown.new(Renderer, options)
27
+ end
28
+ end
29
+
30
+ class Renderer < Redcarpet::Render::HTML
31
+ def block_quote(quote)
32
+ if (match = quote.match(/<p>\[(.*)\]/))
33
+ %(<blockquote class="#{match[1]}">#{quote.gsub("[#{match[1]}]", "")}</blockquote>)
34
+ else
35
+ super
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rouge"
4
+ require "rouge/plugins/redcarpet"
5
+
6
+ require_relative "../markdown"
7
+
8
+ module ContentFS
9
+ module Renderers
10
+ class Markdown
11
+ # @api private
12
+ class Code
13
+ class << self
14
+ def render(content)
15
+ renderer.render(content)
16
+ end
17
+
18
+ private def renderer
19
+ @_renderer ||= Redcarpet::Markdown.new(Renderer, Markdown.options)
20
+ end
21
+ end
22
+
23
+ class Renderer < Markdown::Renderer
24
+ include Rouge::Plugins::Redcarpet
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ContentFS
4
+ # @api private
5
+ class Slug
6
+ class << self
7
+ SPECIAL_CHARACTER_REGEXP = /[^a-zA-Z0-9\-_]*/
8
+
9
+ def build(value)
10
+ value.to_s.gsub(SPECIAL_CHARACTER_REGEXP, "").to_sym
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ContentFS
4
+ VERSION = "0.0.0"
5
+
6
+ def self.version
7
+ VERSION
8
+ end
9
+ end
metadata ADDED
@@ -0,0 +1,53 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: contentfs
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Bryan Powell
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-11-12 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: A structured content file system.
14
+ email: bryan@metabahn.com
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - LICENSE
20
+ - README.md
21
+ - lib/contentfs.rb
22
+ - lib/contentfs/content.rb
23
+ - lib/contentfs/database.rb
24
+ - lib/contentfs/prefix.rb
25
+ - lib/contentfs/renderers.rb
26
+ - lib/contentfs/renderers/markdown.rb
27
+ - lib/contentfs/renderers/markdown/code.rb
28
+ - lib/contentfs/slug.rb
29
+ - lib/contentfs/version.rb
30
+ homepage: https://github.com/metabahn/contentfs/
31
+ licenses:
32
+ - MIT
33
+ metadata: {}
34
+ post_install_message:
35
+ rdoc_options: []
36
+ require_paths:
37
+ - lib
38
+ required_ruby_version: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: 2.5.0
43
+ required_rubygems_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ requirements: []
49
+ rubygems_version: 3.1.2
50
+ signing_key:
51
+ specification_version: 4
52
+ summary: A structured content file system.
53
+ test_files: []