contentfs 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []