robin_cms 0.1.7 → 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/README.md +8 -3
- data/lib/robin_cms/cms.rb +29 -20
- data/lib/robin_cms/configuration-schema.json +2 -2
- data/lib/robin_cms/configuration.rb +20 -12
- data/lib/robin_cms/data_library.rb +10 -3
- data/lib/robin_cms/helpers.rb +46 -10
- data/lib/robin_cms/item.rb +65 -24
- data/lib/robin_cms/library.rb +8 -1
- data/lib/robin_cms/version.rb +1 -1
- data/lib/robin_cms/views/change_password.erb +1 -1
- data/lib/robin_cms/views/image_field.erb +0 -1
- data/lib/robin_cms/views/layout.erb +1 -1
- data/lib/robin_cms/views/library.erb +2 -2
- data/lib/robin_cms/views/library_item.erb +2 -2
- data/lib/robin_cms/views/new_tab.svg.erb +0 -1
- data/lib/robin_cms/views/profile.erb +1 -1
- data/lib/robin_cms/views/richtext_field.erb +88 -2
- data/lib/robin_cms/views/select_field.erb +1 -1
- data/lib/robin_cms/views/stylesheet.css.erb +2 -2
- data/lib/robin_cms/views/trix.css.erb +75 -0
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ee6ad0de352f4750144a35a2b258d5d2da00e8fc345f269be1aeec80a45bf5ee
|
|
4
|
+
data.tar.gz: 5bed86dbdf4f9c4041e324d60284583beb1c65551b3ff15964d3950fed8c9cd3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: dc0022fb5043ef5a2a640111ca650926c542cbd0a2a39f5895e6e2f6461edb4c6e4f44d1b8a9fbab01096dfb23ded529bc1458ac6923938eed8a82bcde2e2e71
|
|
7
|
+
data.tar.gz: 7cc63df15ad7619328ac1c9cf0c45c0dddf106e724081ad49a6b60d5f61645dab33dd6d7329e4b8ff39ebbfad0188aef714952e64c100b9ad59827eb0cfc69e1
|
data/README.md
CHANGED
|
@@ -72,7 +72,8 @@ standalone CMS for any Static Site Generator, you can embed it in a dynamic
|
|
|
72
72
|
Sinatra app, or you can use it as a Jekyll plugin. If you're already using
|
|
73
73
|
Jekyll as your SSG, this is the simplest approach.
|
|
74
74
|
|
|
75
|
-
See the [documentation](
|
|
75
|
+
See the [documentation](https://robincms.org/docs.html) for further information
|
|
76
|
+
on usage.
|
|
76
77
|
|
|
77
78
|
## Configuring
|
|
78
79
|
|
|
@@ -100,8 +101,8 @@ schema for your content. An example YAML file looks like this:
|
|
|
100
101
|
- { label: Title, id: title, type: input }
|
|
101
102
|
- { label: Author, id: author_name, type: input }
|
|
102
103
|
|
|
103
|
-
See the [docs](
|
|
104
|
-
also find some examples [here](./examples).
|
|
104
|
+
See the [docs](https://robincms.org/docs.html) for more details on
|
|
105
|
+
configuration. You can also find some examples [here](./examples).
|
|
105
106
|
|
|
106
107
|
## Testing
|
|
107
108
|
|
|
@@ -109,6 +110,10 @@ Unit test are written in RSpec. To run them:
|
|
|
109
110
|
|
|
110
111
|
rake test
|
|
111
112
|
|
|
113
|
+
## Screenshot
|
|
114
|
+
|
|
115
|
+

|
|
116
|
+
|
|
112
117
|
## Roadmap
|
|
113
118
|
|
|
114
119
|
* [ ] Support markdown content
|
data/lib/robin_cms/cms.rb
CHANGED
|
@@ -37,7 +37,7 @@ module RobinCMS
|
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
get "/" do
|
|
40
|
-
redirect to(
|
|
40
|
+
redirect to(home)
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
get "/login" do
|
|
@@ -47,7 +47,7 @@ module RobinCMS
|
|
|
47
47
|
post "/login" do
|
|
48
48
|
authenticate!(params[:username], params[:password])
|
|
49
49
|
session[:auth_user] = params[:username]
|
|
50
|
-
redirect to(
|
|
50
|
+
redirect to(home)
|
|
51
51
|
rescue AuthenticationError
|
|
52
52
|
flash[:error] = "Incorrect username or password"
|
|
53
53
|
redirect to("/login")
|
|
@@ -154,6 +154,21 @@ module RobinCMS
|
|
|
154
154
|
redirect to("/libraries/#{params[:kind]}")
|
|
155
155
|
end
|
|
156
156
|
|
|
157
|
+
# Handle uploading attachments from the trix editor. Responds with the path
|
|
158
|
+
# of the uploaded file in JSON format.
|
|
159
|
+
post "/libraries/:kind/attachment" do
|
|
160
|
+
static_path = @library.create_static(
|
|
161
|
+
params[:key],
|
|
162
|
+
params[:file][:tempfile].to_path
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
{ :url => "/" + static_path }.to_json
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
post "/libraries/:kind/attachment/delete" do
|
|
169
|
+
@library.delete_static(params[:key])
|
|
170
|
+
end
|
|
171
|
+
|
|
157
172
|
post "/publish" do
|
|
158
173
|
system(@config[:build_command])
|
|
159
174
|
if $?.exitstatus == 0
|
|
@@ -162,27 +177,21 @@ module RobinCMS
|
|
|
162
177
|
flash[:error_dialog] = "There was an error publishing the site."
|
|
163
178
|
end
|
|
164
179
|
|
|
165
|
-
redirect to(
|
|
180
|
+
redirect to(home)
|
|
166
181
|
end
|
|
167
182
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
# the app is mounted at i.e. the virtual location.
|
|
178
|
-
uri = env["PATH_INFO"]
|
|
179
|
-
|
|
180
|
-
library = @config[:libraries].find do |c|
|
|
181
|
-
uri.start_with?("/#{c[:static_location]}")
|
|
182
|
-
end
|
|
183
|
+
# Explicitly serve static files for previewing in the CMS. This is
|
|
184
|
+
# necessary because until you publish the site, static files are not copied
|
|
185
|
+
# to the site output, and therefore requests for the file will return a
|
|
186
|
+
# 404. Note that if using it as a Jekyll plugin, this is not needed as the
|
|
187
|
+
# files will be served by Jekyll's dev server.
|
|
188
|
+
#
|
|
189
|
+
# This route needs to be defined last since it catches a wide range of URLs.
|
|
190
|
+
get "/:static_location/:key" do
|
|
191
|
+
static_locations = @config[:libraries].map { |l| l[:static_location] }.uniq
|
|
183
192
|
|
|
184
|
-
if
|
|
185
|
-
send_file(File.join(
|
|
193
|
+
if static_locations.include?(params[:static_location])
|
|
194
|
+
send_file(File.join(params[:static_location], params[:key]))
|
|
186
195
|
else
|
|
187
196
|
pass
|
|
188
197
|
end
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"build_command": { "type": "string" },
|
|
10
10
|
"accent_color": {
|
|
11
11
|
"type": "string",
|
|
12
|
-
"pattern": "
|
|
12
|
+
"pattern": "^\\#{1}[a-fA-F0-9]{6}"
|
|
13
13
|
},
|
|
14
14
|
"libraries": {
|
|
15
15
|
"type": "array",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"label": { "type": "string" },
|
|
62
62
|
"type": {
|
|
63
63
|
"type": "string",
|
|
64
|
-
"enum": ["text", "richtext", "date", "hidden", "number", "color", "email", "url", "select", "image"]
|
|
64
|
+
"enum": ["text", "richtext", "date", "hidden", "number", "color", "email", "url", "select", "image", "array"]
|
|
65
65
|
},
|
|
66
66
|
"default": { "type": "string" },
|
|
67
67
|
"required": { "type": "boolean" },
|
|
@@ -10,9 +10,10 @@ module RobinCMS
|
|
|
10
10
|
|
|
11
11
|
COLLECTION_DEFAULTS = {
|
|
12
12
|
type: "collection",
|
|
13
|
-
location: "
|
|
14
|
-
static_location: "
|
|
13
|
+
location: "_posts",
|
|
14
|
+
static_location: "uploads",
|
|
15
15
|
filetype: "html",
|
|
16
|
+
pattern: ":year-:month-:day-:title",
|
|
16
17
|
description: "",
|
|
17
18
|
can_create: true,
|
|
18
19
|
can_delete: true,
|
|
@@ -21,8 +22,8 @@ module RobinCMS
|
|
|
21
22
|
|
|
22
23
|
DATA_DEFAULTS = {
|
|
23
24
|
type: "data",
|
|
24
|
-
location: "
|
|
25
|
-
static_location: "
|
|
25
|
+
location: "_data",
|
|
26
|
+
static_location: "uploads",
|
|
26
27
|
filetype: "yml",
|
|
27
28
|
description: "",
|
|
28
29
|
can_create: true,
|
|
@@ -58,7 +59,6 @@ module RobinCMS
|
|
|
58
59
|
label: "Published",
|
|
59
60
|
id: "published",
|
|
60
61
|
type: "select",
|
|
61
|
-
default: false,
|
|
62
62
|
options: [{
|
|
63
63
|
label: "Draft",
|
|
64
64
|
value: false
|
|
@@ -94,9 +94,16 @@ module RobinCMS
|
|
|
94
94
|
YAML.load_file(config_file, symbolize_names: true)
|
|
95
95
|
end
|
|
96
96
|
|
|
97
|
+
# TODO: use a ruby schema instead of a json file. Makes it easier to
|
|
98
|
+
# reuse elements and what not.
|
|
97
99
|
schema_file = File.join(__dir__, "configuration-schema.json")
|
|
98
100
|
schema = JSON.parse(File.read(schema_file))
|
|
99
|
-
JSON::Validator.
|
|
101
|
+
errors = JSON::Validator.fully_validate(schema, data)
|
|
102
|
+
|
|
103
|
+
unless errors.empty?
|
|
104
|
+
puts errors
|
|
105
|
+
raise ValidationError, "There is an error in your configuration file"
|
|
106
|
+
end
|
|
100
107
|
|
|
101
108
|
config = from_h(DEFAULTS).merge_config(data).merge_automatic_fields
|
|
102
109
|
config.validate!
|
|
@@ -115,10 +122,6 @@ module RobinCMS
|
|
|
115
122
|
library[:type].nil? || library[:type] == "collection"
|
|
116
123
|
end
|
|
117
124
|
|
|
118
|
-
def has_image?(library)
|
|
119
|
-
library[:fields].find { |f| f[:type] == "image" }
|
|
120
|
-
end
|
|
121
|
-
|
|
122
125
|
def merge_config(config)
|
|
123
126
|
merge(config, {
|
|
124
127
|
libraries: config[:libraries].map do |library|
|
|
@@ -144,8 +147,11 @@ module RobinCMS
|
|
|
144
147
|
library[:fields].append(*AUTOMATIC_DRAFT_FIELDS)
|
|
145
148
|
end
|
|
146
149
|
|
|
147
|
-
|
|
148
|
-
|
|
150
|
+
# Ensure the automatic image fields are inserted directly after the
|
|
151
|
+
# declared image field.
|
|
152
|
+
image_index = library[:fields].find_index { |f| f[:type] == "image" }
|
|
153
|
+
if image_index
|
|
154
|
+
library[:fields].insert(image_index + 1, *AUTOMATIC_IMAGE_FIELDS)
|
|
149
155
|
end
|
|
150
156
|
|
|
151
157
|
library[:fields].uniq! { |f| f[:id] }
|
|
@@ -156,6 +162,8 @@ module RobinCMS
|
|
|
156
162
|
|
|
157
163
|
# This method provides custom validation which can't be expressed (or is
|
|
158
164
|
# too difficult to express) using the Json Schema standard.
|
|
165
|
+
#
|
|
166
|
+
# TODO: validate that options are given if the field type is select.
|
|
159
167
|
def validate!
|
|
160
168
|
self[:libraries].each do |library|
|
|
161
169
|
next if library[:fields].nil?
|
|
@@ -14,10 +14,11 @@ module RobinCMS
|
|
|
14
14
|
def all
|
|
15
15
|
return [] unless File.exist?(filepath)
|
|
16
16
|
|
|
17
|
-
items = YAML.load_file(filepath, symbolize_names: true)
|
|
17
|
+
items = YAML.load_file(filepath, symbolize_names: true, permitted_classes: [Date])
|
|
18
18
|
|
|
19
19
|
return [] unless items
|
|
20
20
|
|
|
21
|
+
# Use the index of the item as it's id.
|
|
21
22
|
items.each_with_index.map do |attrs, i|
|
|
22
23
|
Item.new(i, attrs, **@schema)
|
|
23
24
|
end
|
|
@@ -29,11 +30,17 @@ module RobinCMS
|
|
|
29
30
|
|
|
30
31
|
item.update_timestamps!
|
|
31
32
|
|
|
33
|
+
# The item's id is it's index in the YAML array. If the id is nil, we are
|
|
34
|
+
# creating a new item; append it to the end of the array. If the id is
|
|
35
|
+
# not nil, we are editing an item; swap out the item at the index
|
|
36
|
+
# corresponding to it's id.
|
|
32
37
|
all_items = YAML.load_file(filepath) || []
|
|
33
38
|
if item.id.nil?
|
|
34
|
-
|
|
39
|
+
# Data items don't have a published state.
|
|
40
|
+
all_items.append(item.frontmatter(ignore_published: true))
|
|
35
41
|
else
|
|
36
|
-
|
|
42
|
+
# As above.
|
|
43
|
+
all_items[item.id.to_i] = item.frontmatter(ignore_published: true)
|
|
37
44
|
end
|
|
38
45
|
|
|
39
46
|
serialized = YAML.dump(all_items, stringify_names: true)
|
data/lib/robin_cms/helpers.rb
CHANGED
|
@@ -37,44 +37,64 @@ module RobinCMS
|
|
|
37
37
|
URI.decode_www_form(request.query_string).to_h
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
+
def home
|
|
41
|
+
"/libraries/#{@config[:libraries].first[:id]}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def path(addr)
|
|
45
|
+
url(addr, absolute = false)
|
|
46
|
+
end
|
|
47
|
+
|
|
40
48
|
def root_url
|
|
41
|
-
|
|
49
|
+
path("/")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def home_url
|
|
53
|
+
path(home)
|
|
42
54
|
end
|
|
43
55
|
|
|
44
56
|
def profile_url
|
|
45
|
-
|
|
57
|
+
path("/profile")
|
|
46
58
|
end
|
|
47
59
|
|
|
48
60
|
def logout_url
|
|
49
|
-
|
|
61
|
+
path("/logout")
|
|
50
62
|
end
|
|
51
63
|
|
|
52
64
|
def change_password_url
|
|
53
|
-
|
|
65
|
+
path("/change-password")
|
|
54
66
|
end
|
|
55
67
|
|
|
56
68
|
def library_url(library)
|
|
57
|
-
|
|
69
|
+
path("/libraries/#{library[:id]}")
|
|
58
70
|
end
|
|
59
71
|
|
|
60
72
|
def library_item_url(library, item = nil)
|
|
61
73
|
if item
|
|
62
|
-
|
|
74
|
+
path("/libraries/#{library[:id]}/item/#{item.id}")
|
|
63
75
|
else
|
|
64
|
-
|
|
76
|
+
path("/libraries/#{library[:id]}/item")
|
|
65
77
|
end
|
|
66
78
|
end
|
|
67
79
|
|
|
68
80
|
def delete_library_item_url(library, item)
|
|
69
|
-
|
|
81
|
+
path("/libraries/#{library[:id]}/item/#{item.id}/delete")
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def library_attachment_url(library)
|
|
85
|
+
path("/libraries/#{library[:id]}/attachment")
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def delete_library_attachment_url(library)
|
|
89
|
+
path("/libraries/#{library[:id]}/attachment/delete")
|
|
70
90
|
end
|
|
71
91
|
|
|
72
92
|
def item_image_url(item)
|
|
73
|
-
|
|
93
|
+
path(item.image_src)
|
|
74
94
|
end
|
|
75
95
|
|
|
76
96
|
def publish_url
|
|
77
|
-
|
|
97
|
+
path("/publish")
|
|
78
98
|
end
|
|
79
99
|
|
|
80
100
|
def current_page?(library)
|
|
@@ -90,6 +110,22 @@ module RobinCMS
|
|
|
90
110
|
"__#{prefix}_#{id}"
|
|
91
111
|
end
|
|
92
112
|
|
|
113
|
+
# The attachments for Trix need to be served from the admin site root
|
|
114
|
+
# (usually "/admin") in order for the catch-all route handler to be able
|
|
115
|
+
# to serve them.
|
|
116
|
+
#
|
|
117
|
+
# TODO: You can set a "preview URL" for Trix (see
|
|
118
|
+
# https://github.com/basecamp/trix#previewing-attached-files). But I
|
|
119
|
+
# couldn't get it to work.
|
|
120
|
+
def escape_for_trix(value)
|
|
121
|
+
value = value
|
|
122
|
+
.sub(""href":"/uploads", ""href":"#{root_url}uploads")
|
|
123
|
+
.sub(""url":"/uploads", ""url":"#{root_url}uploads")
|
|
124
|
+
.sub("href=\"/uploads", "href=\"#{root_url}uploads")
|
|
125
|
+
.sub("url=\"/uploads", "url=\"#{root_url}uploads")
|
|
126
|
+
CGI::escapeHTML(value)
|
|
127
|
+
end
|
|
128
|
+
|
|
93
129
|
def flash
|
|
94
130
|
@flash ||= Flash.new(session ? session[:flash] : {})
|
|
95
131
|
end
|
data/lib/robin_cms/item.rb
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module RobinCMS
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
# The in-memory data model for all items.
|
|
5
|
+
#
|
|
6
|
+
# The Item objects model the domain, while the Library objects model disk
|
|
7
|
+
# IO/persistence. Since the distinction between Data and Collection items is
|
|
8
|
+
# purely a distinction of how they are written to disk, this is abstracted
|
|
9
|
+
# entirely by the Library objects and therefore Item's don't care what type
|
|
10
|
+
# they are.
|
|
11
|
+
#
|
|
12
|
+
# There are two boundaries for serialization/deserialization of items. One is
|
|
13
|
+
# to/from the file used to store the data on disk. This happens in the
|
|
14
|
+
# find_one, all, and write methods of the Library classes. The other is
|
|
15
|
+
# to/from the html representation of the item. This happens in the
|
|
16
|
+
# attributes= and field_value_or_default methods in this class.
|
|
17
|
+
class Item attr_reader :id, :attributes, :schema
|
|
6
18
|
|
|
7
19
|
DATETIME_FORMAT = "%Y-%m-%d"
|
|
8
20
|
|
|
@@ -17,15 +29,49 @@ module RobinCMS
|
|
|
17
29
|
@id = id
|
|
18
30
|
@schema = schema
|
|
19
31
|
|
|
20
|
-
# Be sure to use the setter here
|
|
32
|
+
# Be sure to use the setter here.
|
|
21
33
|
self.attributes = attrs
|
|
22
34
|
end
|
|
23
35
|
|
|
24
36
|
def attributes=(attributes)
|
|
25
37
|
@attributes = attributes.to_h.transform_keys(&:to_sym)
|
|
26
38
|
|
|
27
|
-
|
|
28
|
-
|
|
39
|
+
# Parse array fields.
|
|
40
|
+
@schema[:fields].each do |f|
|
|
41
|
+
next unless f[:type] == "array"
|
|
42
|
+
|
|
43
|
+
field_id = f[:id].to_sym
|
|
44
|
+
if @attributes[field_id] && !@attributes[field_id].is_a?(Array)
|
|
45
|
+
@attributes[field_id] = @attributes[field_id].split(",").map(&:strip)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Parse published status. The logic here is a bit weird, but basically an
|
|
50
|
+
# item without a 'published' key is considered published. Draft times are
|
|
51
|
+
# marked with 'published: false'. This is in line with how it works in
|
|
52
|
+
# Jekyll.
|
|
53
|
+
if blank?
|
|
54
|
+
@attributes[:published] = false # New items always default to draft.
|
|
55
|
+
elsif attributes.has_key?(:published)
|
|
56
|
+
@attributes[:published] = attributes[:published].to_s != "false"
|
|
57
|
+
else
|
|
58
|
+
@attributes[:published] = true
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def field_value_or_default(field_id)
|
|
63
|
+
field = @schema[:fields].find { |f| f[:id] == field_id }
|
|
64
|
+
value = @attributes[field[:id].to_sym]
|
|
65
|
+
|
|
66
|
+
# Explicitly check for nil here because a field might have a value of
|
|
67
|
+
# false.
|
|
68
|
+
value = field[:default] if value.nil?
|
|
69
|
+
|
|
70
|
+
case field[:type]
|
|
71
|
+
when "array"
|
|
72
|
+
value&.join(",")
|
|
73
|
+
else
|
|
74
|
+
value.to_s
|
|
29
75
|
end
|
|
30
76
|
end
|
|
31
77
|
|
|
@@ -34,11 +80,11 @@ module RobinCMS
|
|
|
34
80
|
end
|
|
35
81
|
|
|
36
82
|
def published?
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
83
|
+
@attributes[:published]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def blank?
|
|
87
|
+
@id.nil?
|
|
42
88
|
end
|
|
43
89
|
|
|
44
90
|
def published_label
|
|
@@ -73,8 +119,8 @@ module RobinCMS
|
|
|
73
119
|
def display_name
|
|
74
120
|
return @attributes[:title] unless @schema[:display_name_pattern]
|
|
75
121
|
|
|
76
|
-
@schema[:display_name_pattern].
|
|
77
|
-
@attributes.each { |key, value| name.gsub!(":#{key}", value) }
|
|
122
|
+
@schema[:display_name_pattern].dup.tap do |name|
|
|
123
|
+
@attributes.each { |key, value| name.gsub!(":#{key}", value.to_s) }
|
|
78
124
|
end
|
|
79
125
|
end
|
|
80
126
|
|
|
@@ -86,12 +132,17 @@ module RobinCMS
|
|
|
86
132
|
@attributes[:image_alt]
|
|
87
133
|
end
|
|
88
134
|
|
|
135
|
+
def can_delete?
|
|
136
|
+
@id && @schema[:can_delete]
|
|
137
|
+
end
|
|
138
|
+
|
|
89
139
|
# Get the frontmatter as a hash with stringified keys.
|
|
90
|
-
def frontmatter
|
|
140
|
+
def frontmatter(ignore_published: false)
|
|
91
141
|
frontmatter = @attributes.clone
|
|
92
142
|
FRONTMATTER_IGNORE_KEYS.each { |key| frontmatter.delete(key) }
|
|
93
143
|
|
|
94
|
-
|
|
144
|
+
# See comment in the attributes= method.
|
|
145
|
+
if ignore_published || published?
|
|
95
146
|
frontmatter.delete(:published)
|
|
96
147
|
else
|
|
97
148
|
frontmatter[:published] = false
|
|
@@ -103,15 +154,5 @@ module RobinCMS
|
|
|
103
154
|
# to support earlier versions of Ruby.
|
|
104
155
|
frontmatter.to_h.transform_keys(&:to_s)
|
|
105
156
|
end
|
|
106
|
-
|
|
107
|
-
def field_value_or_default(field_id)
|
|
108
|
-
field = @schema[:fields].find { |f| f[:id] == field_id }
|
|
109
|
-
value = @attributes[field[:id].to_sym] || field[:default]
|
|
110
|
-
value.to_s
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
def can_delete?
|
|
114
|
-
@id && @schema[:can_delete]
|
|
115
|
-
end
|
|
116
157
|
end
|
|
117
158
|
end
|
data/lib/robin_cms/library.rb
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module RobinCMS
|
|
4
|
+
# The data access layer for Items.
|
|
5
|
+
#
|
|
6
|
+
# This class implements a Data Mapper pattern for persisting items to disk.
|
|
7
|
+
# It works well for this domain because there are two ways that items are
|
|
8
|
+
# written to disk, depending on whether they are a Data or Collection item.
|
|
9
|
+
# Since the Data Mapper is independent of the in-memory representation of
|
|
10
|
+
# Items, Items don't need to know about which 'type' they are.
|
|
4
11
|
class Library
|
|
5
12
|
include Editable
|
|
6
13
|
include Sluggable
|
|
@@ -26,7 +33,7 @@ module RobinCMS
|
|
|
26
33
|
end
|
|
27
34
|
|
|
28
35
|
def blank
|
|
29
|
-
Item.new(nil, **@schema)
|
|
36
|
+
Item.new(nil, {}, **@schema)
|
|
30
37
|
end
|
|
31
38
|
|
|
32
39
|
def create_static(filename, tempfile)
|
data/lib/robin_cms/version.rb
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
<% if session[:auth_user] %>
|
|
20
20
|
<header id="site-header">
|
|
21
21
|
<span class="controls">
|
|
22
|
-
<h1><a href="<%=
|
|
22
|
+
<h1><a href="<%= home_url %>"><%= @config[:title] %> admin</a></h1>
|
|
23
23
|
</span>
|
|
24
24
|
<span class="controls --gap-md">
|
|
25
25
|
<a href="<%= @config[:url] %>" target="_blank">
|
|
@@ -55,12 +55,12 @@
|
|
|
55
55
|
<% end %>
|
|
56
56
|
<td class="date">
|
|
57
57
|
<a href="<%= library_item_url(@library, item) %>">
|
|
58
|
-
<%= item.created_at
|
|
58
|
+
<%= item.created_at&.strftime("%b %d, %Y") %>
|
|
59
59
|
</a>
|
|
60
60
|
</td>
|
|
61
61
|
<td class="date">
|
|
62
62
|
<a href="<%= library_item_url(@library, item) %>">
|
|
63
|
-
<%= item.updated_at
|
|
63
|
+
<%= item.updated_at&.strftime("%b %d, %Y") %>
|
|
64
64
|
</a>
|
|
65
65
|
</td>
|
|
66
66
|
</tr>
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
<div class="field">
|
|
20
20
|
<%=
|
|
21
21
|
case field[:type]
|
|
22
|
-
when "text", "date", "number", "color", "email", "url"
|
|
22
|
+
when "text", "date", "number", "color", "email", "url", "array"
|
|
23
23
|
erb :input_field, locals: { field: field }
|
|
24
24
|
when "hidden"
|
|
25
25
|
erb :hidden_field, locals: { field: field }
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
when "image"
|
|
29
29
|
erb :image_field, locals: { field: field }
|
|
30
30
|
when "richtext"
|
|
31
|
-
erb :richtext_field, locals: { field: field }
|
|
31
|
+
erb :richtext_field, locals: { field: field, library: @library }
|
|
32
32
|
else
|
|
33
33
|
""
|
|
34
34
|
end
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="utf-8"?>
|
|
2
1
|
<svg fill="#000000" width="12px" height="12px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
3
2
|
<path fill-rule="evenodd" d="M5,2 L7,2 C7.55228475,2 8,2.44771525 8,3 C8,3.51283584 7.61395981,3.93550716 7.11662113,3.99327227 L7,4 L5,4 C4.48716416,4 4.06449284,4.38604019 4.00672773,4.88337887 L4,5 L4,19 C4,19.5128358 4.38604019,19.9355072 4.88337887,19.9932723 L5,20 L19,20 C19.5128358,20 19.9355072,19.6139598 19.9932723,19.1166211 L20,19 L20,17 C20,16.4477153 20.4477153,16 21,16 C21.5128358,16 21.9355072,16.3860402 21.9932723,16.8833789 L22,17 L22,19 C22,20.5976809 20.75108,21.9036609 19.1762728,21.9949073 L19,22 L5,22 C3.40231912,22 2.09633912,20.75108 2.00509269,19.1762728 L2,19 L2,5 C2,3.40231912 3.24891996,2.09633912 4.82372721,2.00509269 L5,2 L7,2 L5,2 Z M21,2 L21.081,2.003 L21.2007258,2.02024007 L21.2007258,2.02024007 L21.3121425,2.04973809 L21.3121425,2.04973809 L21.4232215,2.09367336 L21.5207088,2.14599545 L21.5207088,2.14599545 L21.6167501,2.21278596 L21.7071068,2.29289322 L21.7071068,2.29289322 L21.8036654,2.40469339 L21.8036654,2.40469339 L21.8753288,2.5159379 L21.9063462,2.57690085 L21.9063462,2.57690085 L21.9401141,2.65834962 L21.9401141,2.65834962 L21.9641549,2.73400703 L21.9641549,2.73400703 L21.9930928,2.8819045 L21.9930928,2.8819045 L22,3 L22,3 L22,9 C22,9.55228475 21.5522847,10 21,10 C20.4477153,10 20,9.55228475 20,9 L20,5.414 L13.7071068,11.7071068 C13.3466228,12.0675907 12.7793918,12.0953203 12.3871006,11.7902954 L12.2928932,11.7071068 C11.9324093,11.3466228 11.9046797,10.7793918 12.2097046,10.3871006 L12.2928932,10.2928932 L18.584,4 L15,4 C14.4477153,4 14,3.55228475 14,3 C14,2.44771525 14.4477153,2 15,2 L21,2 Z"/>
|
|
4
3
|
</svg>
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<section class="card">
|
|
5
5
|
<p>You are currently logged in as <b><%= @username %></b>.</p>
|
|
6
6
|
<div class="controls --align-right">
|
|
7
|
-
<a href="<%=
|
|
7
|
+
<a href="<%= home_url %>">Back</a>
|
|
8
8
|
<form action="<%= change_password_url %>">
|
|
9
9
|
<button type="submit">Change password</button>
|
|
10
10
|
</form>
|
|
@@ -1,10 +1,96 @@
|
|
|
1
1
|
<link rel="stylesheet" type="text/css" href="https://unpkg.com/trix@2.0.8/dist/trix.css">
|
|
2
|
+
|
|
3
|
+
<style>
|
|
4
|
+
<%= erb :"trix.css" %>
|
|
5
|
+
</style>
|
|
6
|
+
|
|
2
7
|
<script type="text/javascript" src="https://unpkg.com/trix@2.0.8/dist/trix.umd.min.js"></script>
|
|
8
|
+
<script>
|
|
9
|
+
addEventListener("trix-before-initialize", function () {
|
|
10
|
+
Trix.config.attachments.preview.caption = false;
|
|
11
|
+
Trix.config.attachments.file.caption = false;
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
addEventListener("trix-attachment-add", function (e) {
|
|
15
|
+
if (e.attachment.file) {
|
|
16
|
+
uploadFileAttachment(e.attachment);
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
addEventListener("trix-attachment-remove", function (e) {
|
|
21
|
+
if (e.attachment.file) {
|
|
22
|
+
removeFile(e.attachment);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
function uploadFileAttachment(attachment) {
|
|
27
|
+
uploadFile(attachment.file, setProgress, setAttributes);
|
|
28
|
+
|
|
29
|
+
function setProgress(progress) {
|
|
30
|
+
attachment.setUploadProgress(progress);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function setAttributes(attributes) {
|
|
34
|
+
attachment.setAttributes(attributes);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function uploadFile(file, progressCallback, successCallback) {
|
|
39
|
+
var formData = new FormData();
|
|
40
|
+
formData.append("key", createStorageKey(file));
|
|
41
|
+
formData.append("Content-Type", file.type);
|
|
42
|
+
formData.append("file", file);
|
|
43
|
+
|
|
44
|
+
var xhr = new XMLHttpRequest();
|
|
45
|
+
|
|
46
|
+
xhr.open("POST", "<%= library_attachment_url(library) %>", true);
|
|
47
|
+
|
|
48
|
+
xhr.upload.addEventListener("progress", function (e) {
|
|
49
|
+
var progress = e.loaded / e.total * 100;
|
|
50
|
+
progressCallback(progress);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
xhr.addEventListener("load", function (e) {
|
|
54
|
+
if (xhr.status == 200) {
|
|
55
|
+
var res = JSON.parse(xhr.response);
|
|
56
|
+
var attributes = {
|
|
57
|
+
url: res.url,
|
|
58
|
+
href: res.url + "?content-disposition=attachment"
|
|
59
|
+
};
|
|
60
|
+
successCallback(attributes);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
xhr.send(formData);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function removeFile(attachment) {
|
|
68
|
+
var url = attachment.getAttribute("url");
|
|
69
|
+
var parts = url.split("/");
|
|
70
|
+
var key = parts[parts.length - 1];
|
|
71
|
+
|
|
72
|
+
var formData = new FormData();
|
|
73
|
+
formData.append("key", key);
|
|
74
|
+
|
|
75
|
+
var xhr = new XMLHttpRequest();
|
|
76
|
+
|
|
77
|
+
xhr.open("POST", "<%= delete_library_attachment_url(library) %>", true);
|
|
78
|
+
xhr.send(formData);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function createStorageKey(file) {
|
|
82
|
+
var date = new Date();
|
|
83
|
+
var day = date.toISOString().slice(0, 10);
|
|
84
|
+
return date.getTime() + "-" + file.name;
|
|
85
|
+
}
|
|
86
|
+
</script>
|
|
87
|
+
|
|
3
88
|
<%= erb :field_label, locals: { field: field } %>
|
|
89
|
+
<%# TODO: label for attribute doesn't reference correct id %>
|
|
4
90
|
<input
|
|
5
91
|
id="richtext-content"
|
|
6
92
|
type="hidden"
|
|
7
93
|
name="<%= field[:id] %>"
|
|
8
|
-
value="<%= @item.field_value_or_default(field[:id]) %>"
|
|
94
|
+
value="<%= escape_for_trix(@item.field_value_or_default(field[:id])) %>"
|
|
9
95
|
>
|
|
10
|
-
<trix-editor input="richtext-content"></trix-editor>
|
|
96
|
+
<trix-editor class="trix-content" input="richtext-content"></trix-editor>
|
|
@@ -194,7 +194,7 @@ select {
|
|
|
194
194
|
input,
|
|
195
195
|
select,
|
|
196
196
|
trix-toolbar,
|
|
197
|
-
img
|
|
197
|
+
img.image-preview {
|
|
198
198
|
display: block;
|
|
199
199
|
margin-top: var(--padding-xs);
|
|
200
200
|
}
|
|
@@ -425,7 +425,6 @@ hr {
|
|
|
425
425
|
/* trix editor overrides */
|
|
426
426
|
|
|
427
427
|
trix-editor {
|
|
428
|
-
height: 16rem;
|
|
429
428
|
border: 1px solid var(--border-color) !important;
|
|
430
429
|
border-radius: var(--border-radius) !important;
|
|
431
430
|
box-shadow: var(--box-shadow-input);
|
|
@@ -446,3 +445,4 @@ trix-editor {
|
|
|
446
445
|
.trix-button.trix-active {
|
|
447
446
|
background: var(--accent-color-light) !important;
|
|
448
447
|
}
|
|
448
|
+
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
.trix-content {
|
|
2
|
+
line-height: 1.5; }
|
|
3
|
+
.trix-content * {
|
|
4
|
+
box-sizing: border-box;
|
|
5
|
+
margin: 0;
|
|
6
|
+
padding: 0; }
|
|
7
|
+
.trix-content h1 {
|
|
8
|
+
font-size: 1.2em;
|
|
9
|
+
line-height: 1.2; }
|
|
10
|
+
.trix-content blockquote {
|
|
11
|
+
border: 0 solid #ccc;
|
|
12
|
+
border-left-width: 0.3em;
|
|
13
|
+
margin-left: 0.3em;
|
|
14
|
+
padding-left: 0.6em; }
|
|
15
|
+
.trix-content [dir=rtl] blockquote,
|
|
16
|
+
.trix-content blockquote[dir=rtl] {
|
|
17
|
+
border-width: 0;
|
|
18
|
+
border-right-width: 0.3em;
|
|
19
|
+
margin-right: 0.3em;
|
|
20
|
+
padding-right: 0.6em; }
|
|
21
|
+
.trix-content li {
|
|
22
|
+
margin-left: 1em; }
|
|
23
|
+
.trix-content [dir=rtl] li {
|
|
24
|
+
margin-right: 1em; }
|
|
25
|
+
.trix-content pre {
|
|
26
|
+
display: inline-block;
|
|
27
|
+
width: 100%;
|
|
28
|
+
vertical-align: top;
|
|
29
|
+
font-family: monospace;
|
|
30
|
+
font-size: 0.9em;
|
|
31
|
+
padding: 0.5em;
|
|
32
|
+
white-space: pre;
|
|
33
|
+
background-color: #eee;
|
|
34
|
+
overflow-x: auto; }
|
|
35
|
+
.trix-content img {
|
|
36
|
+
max-width: 100%;
|
|
37
|
+
height: auto; }
|
|
38
|
+
.trix-content .attachment {
|
|
39
|
+
display: inline-block;
|
|
40
|
+
position: relative;
|
|
41
|
+
max-width: 100%; }
|
|
42
|
+
.trix-content .attachment a {
|
|
43
|
+
color: inherit;
|
|
44
|
+
text-decoration: none; }
|
|
45
|
+
.trix-content .attachment a:hover, .trix-content .attachment a:visited:hover {
|
|
46
|
+
color: inherit; }
|
|
47
|
+
.trix-content .attachment__caption {
|
|
48
|
+
text-align: center; }
|
|
49
|
+
.trix-content .attachment__caption .attachment__name + .attachment__size::before {
|
|
50
|
+
content: ' \2022 '; }
|
|
51
|
+
.trix-content .attachment--preview {
|
|
52
|
+
width: 50%;
|
|
53
|
+
text-align: center; }
|
|
54
|
+
.trix-content .attachment--preview .attachment__caption {
|
|
55
|
+
color: #666;
|
|
56
|
+
font-size: 0.9em;
|
|
57
|
+
line-height: 1.2; }
|
|
58
|
+
.trix-content .attachment--file {
|
|
59
|
+
color: #333;
|
|
60
|
+
line-height: 1;
|
|
61
|
+
margin: 0 2px 2px 2px;
|
|
62
|
+
padding: 0.4em 1em;
|
|
63
|
+
border: 1px solid #bbb;
|
|
64
|
+
border-radius: 5px; }
|
|
65
|
+
.trix-content .attachment-gallery {
|
|
66
|
+
display: flex;
|
|
67
|
+
flex-wrap: wrap;
|
|
68
|
+
position: relative; }
|
|
69
|
+
.trix-content .attachment-gallery .attachment {
|
|
70
|
+
flex: 1 0 33%;
|
|
71
|
+
padding: 0 0.5em;
|
|
72
|
+
max-width: 33%; }
|
|
73
|
+
.trix-content .attachment-gallery.attachment-gallery--2 .attachment, .trix-content .attachment-gallery.attachment-gallery--4 .attachment {
|
|
74
|
+
flex-basis: 50%;
|
|
75
|
+
max-width: 50%; }
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: robin_cms
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Aron Lebani
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-09-11 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bcrypt
|
|
@@ -112,6 +112,7 @@ files:
|
|
|
112
112
|
- lib/robin_cms/views/richtext_field.erb
|
|
113
113
|
- lib/robin_cms/views/select_field.erb
|
|
114
114
|
- lib/robin_cms/views/stylesheet.css.erb
|
|
115
|
+
- lib/robin_cms/views/trix.css.erb
|
|
115
116
|
homepage: https://robincms.org
|
|
116
117
|
licenses:
|
|
117
118
|
- MIT
|
|
@@ -133,7 +134,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
133
134
|
- !ruby/object:Gem::Version
|
|
134
135
|
version: '0'
|
|
135
136
|
requirements: []
|
|
136
|
-
rubygems_version: 3.5.
|
|
137
|
+
rubygems_version: 3.5.22
|
|
137
138
|
signing_key:
|
|
138
139
|
specification_version: 4
|
|
139
140
|
summary: A simple, flat-file CMS for Static Site Generators.
|