robin_cms 0.1.6 → 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 +32 -22
- 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 +78 -5
- data/lib/robin_cms/item.rb +74 -25
- 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/delete_dialog.erb +1 -1
- data/lib/robin_cms/views/filter_form.erb +2 -2
- data/lib/robin_cms/views/image_field.erb +2 -3
- data/lib/robin_cms/views/layout.erb +22 -7
- data/lib/robin_cms/views/library.erb +11 -11
- data/lib/robin_cms/views/library_actions.erb +2 -4
- data/lib/robin_cms/views/library_item.erb +18 -16
- data/lib/robin_cms/views/nav.erb +2 -4
- data/lib/robin_cms/views/new_tab.svg.erb +0 -1
- data/lib/robin_cms/views/profile.erb +2 -2
- data/lib/robin_cms/views/richtext_field.erb +88 -2
- data/lib/robin_cms/views/select_field.erb +2 -8
- data/lib/robin_cms/views/stylesheet.css.erb +5 -3
- 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,34 +154,44 @@ 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])
|
|
174
|
+
if $?.exitstatus == 0
|
|
159
175
|
flash[:success_dialog] = "Your site has been successfully published!"
|
|
160
176
|
else
|
|
161
|
-
flash[:
|
|
177
|
+
flash[:error_dialog] = "There was an error publishing the site."
|
|
162
178
|
end
|
|
163
179
|
|
|
164
|
-
redirect to(
|
|
180
|
+
redirect to(home)
|
|
165
181
|
end
|
|
166
182
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
# the app is mounted at i.e. the virtual location.
|
|
177
|
-
uri = env["PATH_INFO"]
|
|
178
|
-
|
|
179
|
-
library = @config[:libraries].find do |c|
|
|
180
|
-
uri.start_with?("/#{c[:static_location]}")
|
|
181
|
-
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
|
|
182
192
|
|
|
183
|
-
if
|
|
184
|
-
send_file(File.join(
|
|
193
|
+
if static_locations.include?(params[:static_location])
|
|
194
|
+
send_file(File.join(params[:static_location], params[:key]))
|
|
185
195
|
else
|
|
186
196
|
pass
|
|
187
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,13 +37,70 @@ module RobinCMS
|
|
|
37
37
|
URI.decode_www_form(request.query_string).to_h
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
-
def
|
|
41
|
-
|
|
42
|
-
item || libraries
|
|
40
|
+
def home
|
|
41
|
+
"/libraries/#{@config[:libraries].first[:id]}"
|
|
43
42
|
end
|
|
44
43
|
|
|
45
|
-
def
|
|
46
|
-
|
|
44
|
+
def path(addr)
|
|
45
|
+
url(addr, absolute = false)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def root_url
|
|
49
|
+
path("/")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def home_url
|
|
53
|
+
path(home)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def profile_url
|
|
57
|
+
path("/profile")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def logout_url
|
|
61
|
+
path("/logout")
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def change_password_url
|
|
65
|
+
path("/change-password")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def library_url(library)
|
|
69
|
+
path("/libraries/#{library[:id]}")
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def library_item_url(library, item = nil)
|
|
73
|
+
if item
|
|
74
|
+
path("/libraries/#{library[:id]}/item/#{item.id}")
|
|
75
|
+
else
|
|
76
|
+
path("/libraries/#{library[:id]}/item")
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def delete_library_item_url(library, item)
|
|
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")
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def item_image_url(item)
|
|
93
|
+
path(item.image_src)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def publish_url
|
|
97
|
+
path("/publish")
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def current_page?(library)
|
|
101
|
+
_, libraries, item = request.path_info.split("/")
|
|
102
|
+
current_page = item || libraries
|
|
103
|
+
library[:id].to_s == current_page
|
|
47
104
|
end
|
|
48
105
|
|
|
49
106
|
# For generating safe id's where the id needs to be based on user
|
|
@@ -53,6 +110,22 @@ module RobinCMS
|
|
|
53
110
|
"__#{prefix}_#{id}"
|
|
54
111
|
end
|
|
55
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
|
+
|
|
56
129
|
def flash
|
|
57
130
|
@flash ||= Flash.new(session ? session[:flash] : {})
|
|
58
131
|
end
|
data/lib/robin_cms/item.rb
CHANGED
|
@@ -1,13 +1,25 @@
|
|
|
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
|
|
|
9
21
|
# The keys which we don't want to serialize.
|
|
10
|
-
FRONTMATTER_IGNORE_KEYS = [:id, :content, :image, :captures].freeze
|
|
22
|
+
FRONTMATTER_IGNORE_KEYS = [:id, :kind, :content, :image, :captures].freeze
|
|
11
23
|
|
|
12
24
|
def initialize(id, attrs = {}, **schema)
|
|
13
25
|
if !attrs.empty? && !attrs.has_key?(:title)
|
|
@@ -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,17 +119,30 @@ 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
|
|
|
127
|
+
def image_src
|
|
128
|
+
@attributes[:image_src]
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def image_alt
|
|
132
|
+
@attributes[:image_alt]
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def can_delete?
|
|
136
|
+
@id && @schema[:can_delete]
|
|
137
|
+
end
|
|
138
|
+
|
|
81
139
|
# Get the frontmatter as a hash with stringified keys.
|
|
82
|
-
def frontmatter
|
|
140
|
+
def frontmatter(ignore_published: false)
|
|
83
141
|
frontmatter = @attributes.clone
|
|
84
142
|
FRONTMATTER_IGNORE_KEYS.each { |key| frontmatter.delete(key) }
|
|
85
143
|
|
|
86
|
-
|
|
144
|
+
# See comment in the attributes= method.
|
|
145
|
+
if ignore_published || published?
|
|
87
146
|
frontmatter.delete(:published)
|
|
88
147
|
else
|
|
89
148
|
frontmatter[:published] = false
|
|
@@ -95,15 +154,5 @@ module RobinCMS
|
|
|
95
154
|
# to support earlier versions of Ruby.
|
|
96
155
|
frontmatter.to_h.transform_keys(&:to_s)
|
|
97
156
|
end
|
|
98
|
-
|
|
99
|
-
def field_value_or_default(field_id)
|
|
100
|
-
field = @schema[:fields].find { |f| f[:id] == field_id }
|
|
101
|
-
value = @attributes[field[:id].to_sym] || field[:default]
|
|
102
|
-
value.to_s
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
def can_delete?
|
|
106
|
-
@id && @schema[:can_delete]
|
|
107
|
-
end
|
|
108
157
|
end
|
|
109
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
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
<button type="submit">Close</button>
|
|
10
10
|
</form>
|
|
11
11
|
<form
|
|
12
|
-
action=
|
|
12
|
+
action="<%= delete_library_item_url(@library, @item) %>"
|
|
13
13
|
method="post"
|
|
14
14
|
>
|
|
15
15
|
<button type="submit" class="--danger">Delete</button>
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
</select>
|
|
14
14
|
<% if @library.drafts_enabled? %>
|
|
15
15
|
<select id="published" name="published">
|
|
16
|
-
<% for option in PUBLISHED_OPTIONS%>
|
|
16
|
+
<% for option in PUBLISHED_OPTIONS %>
|
|
17
17
|
<option
|
|
18
18
|
value="<%= option[:value] %>"
|
|
19
19
|
<% if query_params["published"] == option[:value] %>
|
|
@@ -33,5 +33,5 @@
|
|
|
33
33
|
placeholder="Search <%= @library[:label].downcase %>..."
|
|
34
34
|
/>
|
|
35
35
|
<button type="submit">Apply</button>
|
|
36
|
-
<a href="
|
|
36
|
+
<a href="<%= library_url(@library) %>">Clear filters</a>
|
|
37
37
|
</form>
|
|
@@ -9,10 +9,9 @@
|
|
|
9
9
|
initial values. This one probably needs to be fixed.
|
|
10
10
|
%>
|
|
11
11
|
<img
|
|
12
|
-
id="image-preview"
|
|
13
12
|
class="image-preview"
|
|
14
|
-
src="<%=
|
|
15
|
-
alt="<%= @item.
|
|
13
|
+
src="<%= item_image_url(@item) %>"
|
|
14
|
+
alt="<%= @item.image_alt %>"
|
|
16
15
|
/>
|
|
17
16
|
<input
|
|
18
17
|
id="<%= safe_id('image', 'field') %>"
|
|
@@ -9,23 +9,29 @@
|
|
|
9
9
|
<%= erb :"stylesheet.css" %>
|
|
10
10
|
</style>
|
|
11
11
|
</head>
|
|
12
|
-
<body
|
|
12
|
+
<body
|
|
13
|
+
<% if flash[:success_dialog] %>
|
|
14
|
+
onload="successdialog.showModal()"
|
|
15
|
+
<% elsif flash[:error_dialog] %>
|
|
16
|
+
onload="errordialog.showModal()"
|
|
17
|
+
<% end %>
|
|
18
|
+
>
|
|
13
19
|
<% if session[:auth_user] %>
|
|
14
20
|
<header id="site-header">
|
|
15
21
|
<span class="controls">
|
|
16
|
-
<h1><a href="
|
|
22
|
+
<h1><a href="<%= home_url %>"><%= @config[:title] %> admin</a></h1>
|
|
17
23
|
</span>
|
|
18
24
|
<span class="controls --gap-md">
|
|
19
25
|
<a href="<%= @config[:url] %>" target="_blank">
|
|
20
26
|
<%= @config[:url] %><%= erb :"new_tab.svg" %>
|
|
21
27
|
</a>
|
|
22
28
|
<% if @config[:build_command] %>
|
|
23
|
-
<form id="publish-form" action="
|
|
29
|
+
<form id="publish-form" action="<%= publish_url %>" method="post">
|
|
24
30
|
<button type="submit" form="publish-form">Publish site</button>
|
|
25
31
|
</form>
|
|
26
32
|
<% end %>
|
|
27
|
-
<a href=
|
|
28
|
-
<a href=
|
|
33
|
+
<a href="<%= profile_url %>">Account</a>
|
|
34
|
+
<a href="<%= logout_url %>">Log out</a>
|
|
29
35
|
</span>
|
|
30
36
|
</header>
|
|
31
37
|
<% end %>
|
|
@@ -44,8 +50,17 @@
|
|
|
44
50
|
<h2>Success!</h2>
|
|
45
51
|
<p><%= flash[:success_dialog] %></p>
|
|
46
52
|
<div class="controls">
|
|
47
|
-
<form id="dialog-close" method="dialog">
|
|
48
|
-
<button type="submit" form="dialog-close">Continue</button>
|
|
53
|
+
<form id="success-dialog-close" method="dialog">
|
|
54
|
+
<button type="submit" form="success-dialog-close">Continue</button>
|
|
55
|
+
</form>
|
|
56
|
+
</div>
|
|
57
|
+
</dialog>
|
|
58
|
+
<dialog id="errordialog" class="card">
|
|
59
|
+
<h2>Oops, something went wrong.</h2>
|
|
60
|
+
<p><%= flash[:error_dialog] %></p>
|
|
61
|
+
<div class="controls">
|
|
62
|
+
<form id="err-dialog-close" method="dialog">
|
|
63
|
+
<button type="submit" form="err-dialog-close">Continue</button>
|
|
49
64
|
</form>
|
|
50
65
|
</div>
|
|
51
66
|
</dialog>
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<p><%= @library[:description] %></p>
|
|
7
7
|
<% end %>
|
|
8
8
|
<% if @library[:can_create] %>
|
|
9
|
-
<form action=
|
|
9
|
+
<form action="<%= library_item_url(@library) %>">
|
|
10
10
|
<button type="submit">
|
|
11
11
|
New <%= @library[:label_singular].downcase %>
|
|
12
12
|
</button>
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
<br />
|
|
21
21
|
<br />
|
|
22
22
|
<% if @library[:can_create] %>
|
|
23
|
-
Create a <a href=
|
|
24
|
-
or try <a href=
|
|
23
|
+
Create a <a href="<%= library_item_url(@library) %>">new <%= @library[:label_singular].downcase %></a>
|
|
24
|
+
or try <a href="<%= library_url(@library) %>">clearing</a> your search filters.
|
|
25
25
|
<% end %>
|
|
26
26
|
</div>
|
|
27
27
|
<% else %>
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
<tr>
|
|
31
31
|
<th>Name</th>
|
|
32
32
|
<% if @library.drafts_enabled? %>
|
|
33
|
-
<th>Status</th>
|
|
33
|
+
<th class="status">Status</th>
|
|
34
34
|
<% end %>
|
|
35
35
|
<th class="date">Created</th>
|
|
36
36
|
<th class="date">Last updated</th>
|
|
@@ -40,27 +40,27 @@
|
|
|
40
40
|
<% for item in @items %>
|
|
41
41
|
<tr>
|
|
42
42
|
<td>
|
|
43
|
-
<a href=
|
|
43
|
+
<a href="<%= library_item_url(@library, item) %>">
|
|
44
44
|
<%= item.display_name %>
|
|
45
45
|
</a>
|
|
46
46
|
</td>
|
|
47
47
|
<% if @library.drafts_enabled? %>
|
|
48
48
|
<td>
|
|
49
|
-
<a href=
|
|
50
|
-
<span class="badge --<%= item.
|
|
49
|
+
<a href="<%= library_item_url(@library, item) %>">
|
|
50
|
+
<span class="badge --<%= item.published_label.downcase %>">
|
|
51
51
|
<%= item.published_label %>
|
|
52
52
|
</span>
|
|
53
53
|
</a>
|
|
54
54
|
</td>
|
|
55
55
|
<% end %>
|
|
56
56
|
<td class="date">
|
|
57
|
-
<a href=
|
|
58
|
-
<%= item.created_at
|
|
57
|
+
<a href="<%= library_item_url(@library, item) %>">
|
|
58
|
+
<%= item.created_at&.strftime("%b %d, %Y") %>
|
|
59
59
|
</a>
|
|
60
60
|
</td>
|
|
61
61
|
<td class="date">
|
|
62
|
-
<a href=
|
|
63
|
-
<%= item.updated_at
|
|
62
|
+
<a href="<%= library_item_url(@library, item) %>">
|
|
63
|
+
<%= item.updated_at&.strftime("%b %d, %Y") %>
|
|
64
64
|
</a>
|
|
65
65
|
</td>
|
|
66
66
|
</tr>
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
<span class="controls">
|
|
2
|
-
<a href=
|
|
2
|
+
<a href="<%= library_url(@library) %>">Back</a>
|
|
3
3
|
<button form="<%= safe_id(@item.id, 'edit_item') %>" type="submit">Save</button>
|
|
4
|
-
|
|
5
|
-
<%= erb :delete_dialog %>
|
|
6
|
-
<% end %>
|
|
4
|
+
<%= erb :delete_dialog if has_delete %>
|
|
7
5
|
</span>
|
|
@@ -8,29 +8,31 @@
|
|
|
8
8
|
id="<%= safe_id(@item.id, 'edit_item') %>"
|
|
9
9
|
class="card"
|
|
10
10
|
<% if @item.id %>
|
|
11
|
-
action=
|
|
11
|
+
action="<%= library_item_url(@library, @item) %>"
|
|
12
12
|
<% else %>
|
|
13
|
-
action=
|
|
13
|
+
action="<%= library_item_url(@library) %>"
|
|
14
14
|
<% end %>
|
|
15
15
|
method="post"
|
|
16
16
|
enctype="multipart/form-data"
|
|
17
17
|
>
|
|
18
18
|
<% @library.sorted_fields.each do |field| %>
|
|
19
19
|
<div class="field">
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
20
|
+
<%=
|
|
21
|
+
case field[:type]
|
|
22
|
+
when "text", "date", "number", "color", "email", "url", "array"
|
|
23
|
+
erb :input_field, locals: { field: field }
|
|
24
|
+
when "hidden"
|
|
25
|
+
erb :hidden_field, locals: { field: field }
|
|
26
|
+
when "select"
|
|
27
|
+
erb :select_field, locals: { field: field }
|
|
28
|
+
when "image"
|
|
29
|
+
erb :image_field, locals: { field: field }
|
|
30
|
+
when "richtext"
|
|
31
|
+
erb :richtext_field, locals: { field: field, library: @library }
|
|
32
|
+
else
|
|
33
|
+
""
|
|
34
|
+
end
|
|
35
|
+
%>
|
|
34
36
|
</div>
|
|
35
37
|
<% end %>
|
|
36
38
|
<%= erb :flash %>
|
data/lib/robin_cms/views/nav.erb
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
<nav id="site-nav">
|
|
2
2
|
<ul class="controls">
|
|
3
3
|
<% for library in @config[:libraries] %>
|
|
4
|
-
<li <% if current_page
|
|
5
|
-
<a href=
|
|
6
|
-
<%= library[:label] %>
|
|
7
|
-
</a>
|
|
4
|
+
<li <% if current_page?(library) %>class="current"<% end %>>
|
|
5
|
+
<a href="<%= library_url(library) %>"><%= library[:label] %></a>
|
|
8
6
|
</li>
|
|
9
7
|
<% end %>
|
|
10
8
|
</ul>
|
|
@@ -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,8 +4,8 @@
|
|
|
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="
|
|
8
|
-
<form action="<%=
|
|
7
|
+
<a href="<%= home_url %>">Back</a>
|
|
8
|
+
<form action="<%= change_password_url %>">
|
|
9
9
|
<button type="submit">Change password</button>
|
|
10
10
|
</form>
|
|
11
11
|
</div>
|
|
@@ -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>
|
|
@@ -7,14 +7,8 @@
|
|
|
7
7
|
<% for option in field[:options] %>
|
|
8
8
|
<option
|
|
9
9
|
value="<%= option[:value] %>"
|
|
10
|
-
<% if
|
|
11
|
-
|
|
12
|
-
selected
|
|
13
|
-
<% end %>
|
|
14
|
-
<% else %>
|
|
15
|
-
<% if option[:value].to_s == @item.field_value_or_default(field[:id]) %>
|
|
16
|
-
selected
|
|
17
|
-
<% end %>
|
|
10
|
+
<% if option[:value].to_s == @item.field_value_or_default(field[:id]) %>
|
|
11
|
+
selected
|
|
18
12
|
<% end %>
|
|
19
13
|
>
|
|
20
14
|
<%= option[:label] %>
|
|
@@ -78,7 +78,9 @@ th {
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
th.date,
|
|
81
|
-
td.date >
|
|
81
|
+
td.date > *,
|
|
82
|
+
th.status,
|
|
83
|
+
td.status > * {
|
|
82
84
|
width: 7rem;
|
|
83
85
|
text-align: right;
|
|
84
86
|
}
|
|
@@ -192,7 +194,7 @@ select {
|
|
|
192
194
|
input,
|
|
193
195
|
select,
|
|
194
196
|
trix-toolbar,
|
|
195
|
-
img
|
|
197
|
+
img.image-preview {
|
|
196
198
|
display: block;
|
|
197
199
|
margin-top: var(--padding-xs);
|
|
198
200
|
}
|
|
@@ -423,7 +425,6 @@ hr {
|
|
|
423
425
|
/* trix editor overrides */
|
|
424
426
|
|
|
425
427
|
trix-editor {
|
|
426
|
-
height: 16rem;
|
|
427
428
|
border: 1px solid var(--border-color) !important;
|
|
428
429
|
border-radius: var(--border-radius) !important;
|
|
429
430
|
box-shadow: var(--box-shadow-input);
|
|
@@ -444,3 +445,4 @@ trix-editor {
|
|
|
444
445
|
.trix-button.trix-active {
|
|
445
446
|
background: var(--accent-color-light) !important;
|
|
446
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.
|