filepress 0.2.0 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e03605fd0950c7f477b42f3c7e0a1945fb36beb2e36712d07b3f3afea0589174
4
- data.tar.gz: bb918105f4fcfd3e0c69c0c989a6be41894e364f86243f42ec609f251548cf66
3
+ metadata.gz: cd4430ee794157675a4edeeb36bf4d09ca137300f1ca4b6dc3f1f1b1dd23c387
4
+ data.tar.gz: 6a164c8673b1cd8d1be639f6ff03955bee8f3306a6e25ad8bc4f0274c6fd93ed
5
5
  SHA512:
6
- metadata.gz: 340b5ac2730761c80eb9c7168ba034df36c99035cb9fce1dc6ced9fab58535c81983c49386141e03c824765f0962810249e75abb485df445aedf288aad8038fd
7
- data.tar.gz: 80ed126ff2e59a217d061f88f183d7ea2fde0d5f352547061f0b30dbf5c325862534ef8b0e995367fd19ee2694c83c2ad4b7df75dac53aa6111a2b767c53ce5c
6
+ metadata.gz: b707d65fb21ec3d1621705e63c7d7bd1707fb4804982f4029e93946ddc15ada6b155c2bc434aae8e60a16f2d13990815743b6c6d2ab413d1395949a0287d6a10
7
+ data.tar.gz: 1577478c2cbcbfb39931ef4cd00ad920802646da96ba0cdd2d40548bc8d883519b80abd7a3d5fefaf1fbdaa2b02313a07e51deb400ca3a675d7e1ba9101d62a9
data/README.md CHANGED
@@ -1,17 +1,114 @@
1
1
  # Filepress
2
2
 
3
- **Write content as flat files. Use it anywhere in your Rails app.**
3
+ Filepress is a minimal Rails plugin that offers the best of both worlds for content management:
4
+ - Write and version control your content in Markdown (or a similar format)
5
+ - Sync it seamlessly to ActiveRecord models so you can harness the full power of a Rails backend.
4
6
 
5
- Filepress lets you manage content in plain text files with frontmatter — like Markdown, HTML, or anything else and sync them directly into your ActiveRecord models. It's the simplicity of static files, with the full power of a Rails backend.
7
+ Ideal for blogs, documentation, or any content-driven Rails app where you want the flexibility of flat files and the structure of a database.
6
8
 
7
- ---
9
+ ## Getting Started
10
+
11
+ 1. Install the gem
12
+
13
+ Add it to your Gemfile:
14
+
15
+ ```ruby
16
+ gem "filepress"
17
+ ```
18
+
19
+ 2. Create a model to represent your content
20
+
21
+ Be sure to include a field that can serve as a unique identifier (e.g. `slug`):
22
+
23
+ ```bash
24
+ rails g model Post title:string slug:string body:text
25
+ ```
26
+
27
+ 3. Enable Filepress for your model
28
+
29
+ Use the `filepress` method in your model class:
8
30
 
9
- ## ✨ Why use Filepress?
31
+ ```ruby
32
+ class Post < ApplicationRecord
33
+ filepress
34
+ end
35
+ ```
10
36
 
11
- - 📝 Write content in any editor — no CMS required
12
- - 🔁 Keep content version-controlled with Git
13
- - ⚡ Instantly available in your Rails app via ActiveRecord
14
- - 🧠 Works with any file type (Markdown, HTML, plain text, etc.)
15
- - 🧹 Automatically adds, updates, and deletes records
37
+ 4. Add some content
16
38
 
39
+ Create Markdown files in `app/content/posts`. The filename becomes the slug, and YAML frontmatter maps to your model's attributes:
40
+
41
+ ```markdown
42
+ ---
43
+ title: My First Post
44
+ published: true
17
45
  ---
46
+
47
+ Hello, this is my first post!
48
+ ```
49
+
50
+ That's it! Filepress syncs your content to the database automatically — on boot, on deploy, and whenever files change in development. No rake tasks, no deploy hooks.
51
+
52
+ You're free to query your content via ActiveRecord like any other model. Filepress doesn't dictate how you render the body — use a Markdown parser like Kramdown, Redcarpet, or similar.
53
+
54
+ ## How it works
55
+
56
+ Filepress reads your content files, extracts YAML frontmatter, and uses the values to populate or update model attributes. The rest of the file becomes the value of the body attribute (or another field, if configured).
57
+
58
+ - Syncs are wrapped in a transaction — if any file fails, nothing changes
59
+ - Unknown frontmatter keys (that don't match a column) are silently ignored
60
+ - In development, Rails' built-in file watcher picks up live edits without a server restart
61
+
62
+ ## Configuration
63
+
64
+ You can customize how Filepress behaves by passing options to `filepress`:
65
+
66
+ ### `from:` — Set a custom content directory
67
+
68
+ ```ruby
69
+ class Post < ApplicationRecord
70
+ filepress from: "app/my_custom_content_folder"
71
+ end
72
+ ```
73
+
74
+ ### `extensions:` — Use filetypes besides Markdown
75
+
76
+ ```ruby
77
+ class Post < ApplicationRecord
78
+ filepress extensions: ["html", "txt"]
79
+ end
80
+ ```
81
+
82
+ ### `key:` — Use a unique identifier other than `slug`
83
+
84
+ ```ruby
85
+ class Post < ApplicationRecord
86
+ filepress key: :name
87
+ end
88
+ ```
89
+
90
+ ### `body:` — Set which attribute stores the main content
91
+
92
+ ```ruby
93
+ class Post < ApplicationRecord
94
+ filepress body: :content
95
+ end
96
+ ```
97
+
98
+ ### `destroy_stale:` — Prevent deletion of records when files are removed
99
+
100
+ By default, Filepress deletes records when the corresponding file is removed. Disable this behaviour with:
101
+
102
+ ```ruby
103
+ class Post < ApplicationRecord
104
+ filepress destroy_stale: false
105
+ end
106
+ ```
107
+
108
+ ## Why Filepress?
109
+
110
+ Filepress is for you if:
111
+
112
+ - You prefer writing content in files
113
+ - You want the convenience of version control for content
114
+ - And you still want powerful querying, associations, validations and all the other Rails goodness
@@ -0,0 +1,22 @@
1
+ module Filepress
2
+ class Engine < ::Rails::Engine
3
+ initializer "filepress.model" do
4
+ ActiveSupport.on_load(:active_record) do
5
+ extend Filepress::Model
6
+ end
7
+ end
8
+
9
+ initializer "filepress.file_watcher" do |app|
10
+ content_path = app.root.join("app", "content")
11
+
12
+ app.config.after_initialize do
13
+ if content_path.exist?
14
+ extensions = Filepress.watched_extensions
15
+ app.reloaders << app.config.file_watcher.new([], { content_path.to_s => extensions }) do
16
+ Filepress.sync
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,27 @@
1
+ require "yaml"
2
+
3
+ module Filepress
4
+ class FileParser
5
+ FRONTMATTER_PATTERN = /\A---\s*\n(.*?\n?)---\s*\n?(.*)\z/m
6
+
7
+ attr_reader :attributes, :body
8
+
9
+ def initialize(raw_content)
10
+ @attributes, @body = parse(raw_content)
11
+ end
12
+
13
+ private
14
+
15
+ def parse(raw_content)
16
+ if (match = raw_content.match(FRONTMATTER_PATTERN))
17
+ attributes = YAML.safe_load(match[1], permitted_classes: [Date, Time, Symbol]) || {}
18
+ body = match[2].strip
19
+ else
20
+ attributes = {}
21
+ body = raw_content.strip
22
+ end
23
+
24
+ [attributes.symbolize_keys, body]
25
+ end
26
+ end
27
+ end
@@ -1,15 +1,17 @@
1
1
  module Filepress
2
2
  module Model
3
- def filepress(from: nil, glob: "*.md", key: :slug, body: :body, destroy_stale: true)
4
- class_attribute :filepress_options, instance_accessor: false, default: {}
3
+ def filepress(from: nil, extensions: ["md"], key: :slug, body: :body, destroy_stale: true)
4
+ class_attribute :filepress_options, instance_accessor: false
5
5
 
6
6
  self.filepress_options = {
7
- from: from || "app/content/#{name.underscore.pluralize}",
8
- glob: glob,
7
+ from: from || Rails.root.join("app", "content", model_name.plural).to_s,
8
+ extensions: extensions,
9
9
  key: key,
10
10
  body: body,
11
11
  destroy_stale: destroy_stale
12
12
  }
13
+
14
+ Filepress.register(self, filepress_options)
13
15
  end
14
16
  end
15
17
  end
@@ -0,0 +1,58 @@
1
+ module Filepress
2
+ class Sync
3
+ def initialize(config)
4
+ @model_class = config[:model_class]
5
+ @from = config[:from]
6
+ @extensions = config[:extensions]
7
+ @key = config[:key]
8
+ @body = config[:body]
9
+ @destroy_stale = config[:destroy_stale]
10
+ end
11
+
12
+ def perform
13
+ return unless table_exists?
14
+ return unless File.directory?(@from)
15
+
16
+ synced_identifiers = []
17
+
18
+ @model_class.transaction do
19
+ content_files.each do |file_path|
20
+ identifier_value = File.basename(file_path, ".*")
21
+ parsed = FileParser.new(File.read(file_path))
22
+
23
+ record = @model_class.find_or_initialize_by(@key => identifier_value)
24
+ record.assign_attributes(permitted_attributes(record, parsed.attributes))
25
+ record.public_send(:"#{@body}=", parsed.body)
26
+ record.save!
27
+
28
+ synced_identifiers << identifier_value
29
+ end
30
+
31
+ if @destroy_stale
32
+ @model_class.where.not(@key => synced_identifiers).destroy_all
33
+ end
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def table_exists?
40
+ @model_class.connection_pool.with_connection do
41
+ @model_class.table_exists?
42
+ end
43
+ rescue ActiveRecord::NoDatabaseError, ActiveRecord::StatementInvalid
44
+ false
45
+ end
46
+
47
+ def permitted_attributes(record, attributes)
48
+ known_columns = record.class.column_names.map(&:to_sym)
49
+ attributes.select { |key, _| known_columns.include?(key) }
50
+ end
51
+
52
+ def content_files
53
+ @extensions.flat_map do |ext|
54
+ Dir[File.join(@from, "*.#{ext}")]
55
+ end.sort
56
+ end
57
+ end
58
+ end
@@ -1,3 +1,3 @@
1
1
  module Filepress
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.0"
3
3
  end
data/lib/filepress.rb CHANGED
@@ -1,39 +1,29 @@
1
1
  require "filepress/version"
2
- require "filepress/railtie"
2
+ require "filepress/engine"
3
+ require "filepress/file_parser"
3
4
  require "filepress/model"
5
+ require "filepress/sync"
4
6
 
5
7
  module Filepress
6
8
  class << self
7
- def sync
8
- Rails.application.eager_load!
9
- models = ActiveRecord::Base.descendants.select { |model| model.respond_to? :filepress_options }
10
-
11
- models.each do |model|
12
- options = model.filepress_options
13
- raise "Filepress options missing for #{model.name}" unless options
14
-
15
- path = Rails.root.join(options[:from])
16
- key = options[:key]
17
- body_attribute = options[:body]
18
-
19
- keys = Dir.glob("#{path}/#{options[:glob]}").map do |file|
20
- raw = File.read(file)
21
- frontmatter, body = raw.split(/^---\s*$/, 3).reject(&:empty?)
22
- data = YAML.safe_load(frontmatter, symbolize_names: true)
23
- content = body.strip
24
-
25
- identifier = data[key]
26
- raise "Missing key `#{key}` in frontmatter for #{file}" unless identifier
9
+ def registry
10
+ @registry ||= {}
11
+ end
27
12
 
28
- record = model.find_or_initialize_by(key => identifier)
29
- record.assign_attributes(data)
30
- record.send("#{body_attribute}=", content)
31
- record.save!
13
+ def register(model_class, options)
14
+ config = options.merge(model_class: model_class)
15
+ registry[model_class.name] = config
16
+ Sync.new(config).perform
17
+ end
32
18
 
33
- identifier
34
- end
19
+ def watched_extensions
20
+ exts = registry.values.flat_map { |config| config[:extensions] }.uniq
21
+ exts.empty? ? ["md"] : exts
22
+ end
35
23
 
36
- model.where.not(key => keys).destroy_all if options[:destroy_stale]
24
+ def sync
25
+ registry.each_value do |config|
26
+ Sync.new(config).perform
37
27
  end
38
28
  end
39
29
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: filepress
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Carl Dawson
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: '5.2'
18
+ version: '7.0'
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
- version: '5.2'
25
+ version: '7.0'
26
26
  description: Filepress lets you manage content as simple flat files, with all the
27
27
  power of ActiveRecord.
28
28
  email:
@@ -35,17 +35,18 @@ files:
35
35
  - README.md
36
36
  - Rakefile
37
37
  - lib/filepress.rb
38
+ - lib/filepress/engine.rb
39
+ - lib/filepress/file_parser.rb
38
40
  - lib/filepress/model.rb
39
- - lib/filepress/railtie.rb
41
+ - lib/filepress/sync.rb
40
42
  - lib/filepress/version.rb
41
- - lib/tasks/filepress_tasks.rake
42
43
  homepage: https://github.com/carldaws/filepress
43
44
  licenses:
44
45
  - MIT
45
46
  metadata:
46
47
  allowed_push_host: https://rubygems.org
47
48
  homepage_uri: https://github.com/carldaws/filepress
48
- source_code_uri: https://github.com/carldawson/filepress
49
+ source_code_uri: https://github.com/carldaws/filepress
49
50
  rdoc_options: []
50
51
  require_paths:
51
52
  - lib
@@ -53,14 +54,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
53
54
  requirements:
54
55
  - - ">="
55
56
  - !ruby/object:Gem::Version
56
- version: '0'
57
+ version: '3.1'
57
58
  required_rubygems_version: !ruby/object:Gem::Requirement
58
59
  requirements:
59
60
  - - ">="
60
61
  - !ruby/object:Gem::Version
61
62
  version: '0'
62
63
  requirements: []
63
- rubygems_version: 3.6.7
64
+ rubygems_version: 4.0.3
64
65
  specification_version: 4
65
66
  summary: Write content as flat files, use it anywhere in your Rails app.
66
67
  test_files: []
@@ -1,11 +0,0 @@
1
- module Filepress
2
- class Railtie < ::Rails::Railtie
3
- ActiveSupport.on_load(:active_record) do
4
- extend Filepress::Model
5
- end
6
-
7
- rake_tasks do
8
- Dir[File.expand_path("../tasks/**/*.rake", __dir__)].each { |f| load f }
9
- end
10
- end
11
- end
@@ -1,6 +0,0 @@
1
- namespace :filepress do
2
- desc "Synchronise Filepress models"
3
- task sync: :environment do
4
- Filepress.sync
5
- end
6
- end