pressroom 0.1.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 +7 -0
- data/CHANGELOG.md +22 -0
- data/MIT-LICENSE +20 -0
- data/README.md +172 -0
- data/Rakefile +8 -0
- data/app/assets/javascripts/pressroom/application.js +18 -0
- data/app/assets/javascripts/pressroom/controllers/editor_controller.js +146 -0
- data/app/assets/javascripts/pressroom/vendor/code.js +181 -0
- data/app/assets/javascripts/pressroom/vendor/delimiter.js +98 -0
- data/app/assets/javascripts/pressroom/vendor/editorjs.js +11217 -0
- data/app/assets/javascripts/pressroom/vendor/header.js +321 -0
- data/app/assets/javascripts/pressroom/vendor/image.js +1103 -0
- data/app/assets/javascripts/pressroom/vendor/inline-code.js +99 -0
- data/app/assets/javascripts/pressroom/vendor/list.js +2091 -0
- data/app/assets/javascripts/pressroom/vendor/marker.js +95 -0
- data/app/assets/javascripts/pressroom/vendor/quote.js +990 -0
- data/app/assets/javascripts/pressroom/vendor/stimulus.js +2563 -0
- data/app/assets/javascripts/pressroom/vendor/turbo.js +7256 -0
- data/app/assets/stylesheets/pressroom/admin.css +216 -0
- data/app/assets/stylesheets/pressroom/content.css +18 -0
- data/app/controllers/pressroom/application_controller.rb +41 -0
- data/app/controllers/pressroom/categories_controller.rb +50 -0
- data/app/controllers/pressroom/posts_controller.rb +98 -0
- data/app/controllers/pressroom/tags_controller.rb +39 -0
- data/app/controllers/pressroom/uploads_controller.rb +52 -0
- data/app/helpers/pressroom/application_helper.rb +4 -0
- data/app/helpers/pressroom/blocks_helper.rb +13 -0
- data/app/jobs/pressroom/application_job.rb +4 -0
- data/app/mailers/pressroom/application_mailer.rb +6 -0
- data/app/models/concerns/pressroom/authorable.rb +23 -0
- data/app/models/concerns/pressroom/sluggable.rb +66 -0
- data/app/models/pressroom/application_record.rb +7 -0
- data/app/models/pressroom/category.rb +54 -0
- data/app/models/pressroom/post.rb +90 -0
- data/app/models/pressroom/tag.rb +30 -0
- data/app/models/pressroom/tagging.rb +12 -0
- data/app/views/layouts/pressroom/admin.html.erb +34 -0
- data/app/views/layouts/pressroom/application.html.erb +17 -0
- data/app/views/pressroom/blocks/_code.html.erb +2 -0
- data/app/views/pressroom/blocks/_delimiter.html.erb +1 -0
- data/app/views/pressroom/blocks/_header.html.erb +5 -0
- data/app/views/pressroom/blocks/_image.html.erb +18 -0
- data/app/views/pressroom/blocks/_list.html.erb +6 -0
- data/app/views/pressroom/blocks/_list_items.html.erb +13 -0
- data/app/views/pressroom/blocks/_paragraph.html.erb +1 -0
- data/app/views/pressroom/blocks/_quote.html.erb +6 -0
- data/app/views/pressroom/categories/_form.html.erb +31 -0
- data/app/views/pressroom/categories/edit.html.erb +3 -0
- data/app/views/pressroom/categories/index.html.erb +34 -0
- data/app/views/pressroom/categories/new.html.erb +3 -0
- data/app/views/pressroom/posts/_form.html.erb +49 -0
- data/app/views/pressroom/posts/_post_row.html.erb +17 -0
- data/app/views/pressroom/posts/edit.html.erb +34 -0
- data/app/views/pressroom/posts/index.html.erb +38 -0
- data/app/views/pressroom/posts/new.html.erb +3 -0
- data/app/views/pressroom/tags/edit.html.erb +24 -0
- data/app/views/pressroom/tags/index.html.erb +27 -0
- data/config/editorjs_manifest.yml +53 -0
- data/config/importmap.rb +18 -0
- data/config/locales/pressroom.en.yml +16 -0
- data/config/locales/transliteration.ru.yml +70 -0
- data/config/locales/transliteration.uk.yml +70 -0
- data/config/routes.rb +17 -0
- data/db/migrate/20260821000001_create_pressroom_posts.rb +42 -0
- data/db/migrate/20260821000002_create_pressroom_taxonomy.rb +33 -0
- data/lib/pressroom/blocks/definition.rb +103 -0
- data/lib/pressroom/blocks/registry.rb +56 -0
- data/lib/pressroom/blocks/renderer.rb +97 -0
- data/lib/pressroom/blocks/sanitizer.rb +36 -0
- data/lib/pressroom/blocks/schema.rb +247 -0
- data/lib/pressroom/blocks/text_extractor.rb +46 -0
- data/lib/pressroom/built_in_blocks.rb +97 -0
- data/lib/pressroom/configuration.rb +64 -0
- data/lib/pressroom/engine.rb +33 -0
- data/lib/pressroom/errors.rb +11 -0
- data/lib/pressroom/feed.rb +69 -0
- data/lib/pressroom/transliteration.rb +213 -0
- data/lib/pressroom/version.rb +3 -0
- data/lib/pressroom.rb +70 -0
- data/lib/tasks/pressroom_tasks.rake +4 -0
- data/lib/tasks/pressroom_vendor.rake +31 -0
- metadata +171 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
ru:
|
|
2
|
+
i18n:
|
|
3
|
+
transliterate:
|
|
4
|
+
rule:
|
|
5
|
+
а: "a"
|
|
6
|
+
б: "b"
|
|
7
|
+
в: "v"
|
|
8
|
+
г: "g"
|
|
9
|
+
д: "d"
|
|
10
|
+
е: "e"
|
|
11
|
+
ё: "e"
|
|
12
|
+
ж: "zh"
|
|
13
|
+
з: "z"
|
|
14
|
+
и: "i"
|
|
15
|
+
й: "i"
|
|
16
|
+
к: "k"
|
|
17
|
+
л: "l"
|
|
18
|
+
м: "m"
|
|
19
|
+
н: "n"
|
|
20
|
+
о: "o"
|
|
21
|
+
п: "p"
|
|
22
|
+
р: "r"
|
|
23
|
+
с: "s"
|
|
24
|
+
т: "t"
|
|
25
|
+
у: "u"
|
|
26
|
+
ф: "f"
|
|
27
|
+
х: "h"
|
|
28
|
+
ц: "ts"
|
|
29
|
+
ч: "ch"
|
|
30
|
+
ш: "sh"
|
|
31
|
+
щ: "sch"
|
|
32
|
+
ъ: ""
|
|
33
|
+
ы: "y"
|
|
34
|
+
ь: ""
|
|
35
|
+
э: "e"
|
|
36
|
+
ю: "yu"
|
|
37
|
+
я: "ya"
|
|
38
|
+
А: "A"
|
|
39
|
+
Б: "B"
|
|
40
|
+
В: "V"
|
|
41
|
+
Г: "G"
|
|
42
|
+
Д: "D"
|
|
43
|
+
Е: "E"
|
|
44
|
+
Ё: "E"
|
|
45
|
+
Ж: "Zh"
|
|
46
|
+
З: "Z"
|
|
47
|
+
И: "I"
|
|
48
|
+
Й: "I"
|
|
49
|
+
К: "K"
|
|
50
|
+
Л: "L"
|
|
51
|
+
М: "M"
|
|
52
|
+
Н: "N"
|
|
53
|
+
О: "O"
|
|
54
|
+
П: "P"
|
|
55
|
+
Р: "R"
|
|
56
|
+
С: "S"
|
|
57
|
+
Т: "T"
|
|
58
|
+
У: "U"
|
|
59
|
+
Ф: "F"
|
|
60
|
+
Х: "H"
|
|
61
|
+
Ц: "Ts"
|
|
62
|
+
Ч: "Ch"
|
|
63
|
+
Ш: "Sh"
|
|
64
|
+
Щ: "Sch"
|
|
65
|
+
Ъ: ""
|
|
66
|
+
Ы: "Y"
|
|
67
|
+
Ь: ""
|
|
68
|
+
Э: "E"
|
|
69
|
+
Ю: "Yu"
|
|
70
|
+
Я: "Ya"
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
uk:
|
|
2
|
+
i18n:
|
|
3
|
+
transliterate:
|
|
4
|
+
rule:
|
|
5
|
+
а: "a"
|
|
6
|
+
б: "b"
|
|
7
|
+
в: "v"
|
|
8
|
+
г: "h"
|
|
9
|
+
ґ: "g"
|
|
10
|
+
д: "d"
|
|
11
|
+
е: "e"
|
|
12
|
+
є: "ie"
|
|
13
|
+
ж: "zh"
|
|
14
|
+
з: "z"
|
|
15
|
+
и: "y"
|
|
16
|
+
і: "i"
|
|
17
|
+
ї: "i"
|
|
18
|
+
й: "i"
|
|
19
|
+
к: "k"
|
|
20
|
+
л: "l"
|
|
21
|
+
м: "m"
|
|
22
|
+
н: "n"
|
|
23
|
+
о: "o"
|
|
24
|
+
п: "p"
|
|
25
|
+
р: "r"
|
|
26
|
+
с: "s"
|
|
27
|
+
т: "t"
|
|
28
|
+
у: "u"
|
|
29
|
+
ф: "f"
|
|
30
|
+
х: "kh"
|
|
31
|
+
ц: "ts"
|
|
32
|
+
ч: "ch"
|
|
33
|
+
ш: "sh"
|
|
34
|
+
щ: "shch"
|
|
35
|
+
ь: ""
|
|
36
|
+
ю: "iu"
|
|
37
|
+
я: "ia"
|
|
38
|
+
А: "A"
|
|
39
|
+
Б: "B"
|
|
40
|
+
В: "V"
|
|
41
|
+
Г: "H"
|
|
42
|
+
Ґ: "G"
|
|
43
|
+
Д: "D"
|
|
44
|
+
Е: "E"
|
|
45
|
+
Є: "Ie"
|
|
46
|
+
Ж: "Zh"
|
|
47
|
+
З: "Z"
|
|
48
|
+
И: "Y"
|
|
49
|
+
І: "I"
|
|
50
|
+
Ї: "I"
|
|
51
|
+
Й: "I"
|
|
52
|
+
К: "K"
|
|
53
|
+
Л: "L"
|
|
54
|
+
М: "M"
|
|
55
|
+
Н: "N"
|
|
56
|
+
О: "O"
|
|
57
|
+
П: "P"
|
|
58
|
+
Р: "R"
|
|
59
|
+
С: "S"
|
|
60
|
+
Т: "T"
|
|
61
|
+
У: "U"
|
|
62
|
+
Ф: "F"
|
|
63
|
+
Х: "Kh"
|
|
64
|
+
Ц: "Ts"
|
|
65
|
+
Ч: "Ch"
|
|
66
|
+
Ш: "Sh"
|
|
67
|
+
Щ: "Shch"
|
|
68
|
+
Ь: ""
|
|
69
|
+
Ю: "Iu"
|
|
70
|
+
Я: "Ia"
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Pressroom::Engine.routes.draw do
|
|
4
|
+
resources :posts, except: :show do
|
|
5
|
+
member do
|
|
6
|
+
patch :publish
|
|
7
|
+
patch :unpublish
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
resources :categories, except: :show
|
|
12
|
+
resources :tags, only: %i[index edit update destroy]
|
|
13
|
+
|
|
14
|
+
resource :upload, only: :create
|
|
15
|
+
|
|
16
|
+
root to: "posts#index"
|
|
17
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreatePressroomPosts < ActiveRecord::Migration[7.2]
|
|
4
|
+
def change
|
|
5
|
+
create_table :pressroom_posts do |t|
|
|
6
|
+
t.string :title, null: false
|
|
7
|
+
t.string :slug, null: false
|
|
8
|
+
|
|
9
|
+
# jsonb where it exists, text elsewhere; the model declares
|
|
10
|
+
# attribute :content, :json so both behave identically in Ruby.
|
|
11
|
+
if postgres?
|
|
12
|
+
t.jsonb :content, null: false, default: {}
|
|
13
|
+
else
|
|
14
|
+
t.text :content
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
t.text :plain_text
|
|
18
|
+
t.text :excerpt
|
|
19
|
+
t.string :status, null: false, default: "draft"
|
|
20
|
+
t.datetime :published_at
|
|
21
|
+
|
|
22
|
+
# No foreign key: the author class is configurable, so the target
|
|
23
|
+
# table is unknown at migration time.
|
|
24
|
+
t.bigint :author_id
|
|
25
|
+
t.bigint :category_id
|
|
26
|
+
|
|
27
|
+
t.timestamps
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
add_index :pressroom_posts, :slug, unique: true
|
|
31
|
+
add_index :pressroom_posts, :status
|
|
32
|
+
add_index :pressroom_posts, :published_at
|
|
33
|
+
add_index :pressroom_posts, :author_id
|
|
34
|
+
add_index :pressroom_posts, :category_id
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def postgres?
|
|
40
|
+
connection.adapter_name.match?(/postg/i)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreatePressroomTaxonomy < ActiveRecord::Migration[7.2]
|
|
4
|
+
def change
|
|
5
|
+
create_table :pressroom_categories do |t|
|
|
6
|
+
t.string :name, null: false
|
|
7
|
+
t.string :slug, null: false
|
|
8
|
+
t.bigint :parent_id
|
|
9
|
+
t.text :description
|
|
10
|
+
t.timestamps
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
add_index :pressroom_categories, :slug, unique: true
|
|
14
|
+
add_index :pressroom_categories, :parent_id
|
|
15
|
+
|
|
16
|
+
create_table :pressroom_tags do |t|
|
|
17
|
+
t.string :name, null: false
|
|
18
|
+
t.string :slug, null: false
|
|
19
|
+
t.timestamps
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
add_index :pressroom_tags, :slug, unique: true
|
|
23
|
+
|
|
24
|
+
create_table :pressroom_taggings do |t|
|
|
25
|
+
t.bigint :post_id, null: false
|
|
26
|
+
t.bigint :tag_id, null: false
|
|
27
|
+
t.timestamps
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
add_index :pressroom_taggings, %i[post_id tag_id], unique: true
|
|
31
|
+
add_index :pressroom_taggings, :tag_id
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails-html-sanitizer"
|
|
4
|
+
|
|
5
|
+
module Pressroom
|
|
6
|
+
module Blocks
|
|
7
|
+
# One registered block type: how it is edited, rendered, and indexed.
|
|
8
|
+
#
|
|
9
|
+
# The reader/writer methods double as the registration DSL, so
|
|
10
|
+
# +b.partial "x"+ sets the value and +definition.partial+ reads it.
|
|
11
|
+
class Definition
|
|
12
|
+
FULL_SANITIZER = Rails::HTML4::FullSanitizer.new
|
|
13
|
+
|
|
14
|
+
attr_reader :type
|
|
15
|
+
|
|
16
|
+
def initialize(type)
|
|
17
|
+
@type = type.to_s
|
|
18
|
+
@schema = Schema.build
|
|
19
|
+
@tool = nil
|
|
20
|
+
@tool_config = {}
|
|
21
|
+
@partial = nil
|
|
22
|
+
@searchable = nil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def tool(name = nil, **config)
|
|
26
|
+
if name
|
|
27
|
+
@tool = name.to_s
|
|
28
|
+
@tool_config = config
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
@tool ||= default_tool_name
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def tool_config
|
|
35
|
+
tool # ensure the default is materialized
|
|
36
|
+
@tool_config
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def schema(&block)
|
|
40
|
+
@schema = Schema.build(&block) if block
|
|
41
|
+
@schema
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def partial(path = nil)
|
|
45
|
+
@partial = path.to_s if path
|
|
46
|
+
@partial
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def searchable(&block)
|
|
50
|
+
@searchable = block if block
|
|
51
|
+
@searchable
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Plain text for the host application's search index.
|
|
55
|
+
def text_for(data)
|
|
56
|
+
return @searchable.call(data).to_s if @searchable
|
|
57
|
+
|
|
58
|
+
default_text_for(data)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def validate!
|
|
62
|
+
return if @partial
|
|
63
|
+
|
|
64
|
+
raise Pressroom::Error, "block #{type.inspect} must declare a partial"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def default_tool_name
|
|
70
|
+
type.split("_").map { |part| part[0].to_s.upcase + part[1..].to_s }.join
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def default_text_for(data)
|
|
74
|
+
parts = @schema.fields.each_value.flat_map do |field|
|
|
75
|
+
value = data[field.name]
|
|
76
|
+
|
|
77
|
+
case field.type
|
|
78
|
+
when :string, :html then [value]
|
|
79
|
+
when :array then Array(value)
|
|
80
|
+
when :tree then flatten_tree(value)
|
|
81
|
+
else []
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
parts.filter_map { |part| strip_tags(part) }.reject(&:empty?).join(" ")
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def flatten_tree(items)
|
|
89
|
+
Array(items).flat_map do |item|
|
|
90
|
+
next [] unless item.is_a?(Hash)
|
|
91
|
+
|
|
92
|
+
[item["content"]] + flatten_tree(item["items"])
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def strip_tags(value)
|
|
97
|
+
return nil if value.nil?
|
|
98
|
+
|
|
99
|
+
FULL_SANITIZER.sanitize(value.to_s).to_s.strip
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pressroom
|
|
4
|
+
module Blocks
|
|
5
|
+
# Ordered collection of block definitions.
|
|
6
|
+
#
|
|
7
|
+
# Registration order matters: it is the order tools appear in the
|
|
8
|
+
# editor's toolbar.
|
|
9
|
+
class Registry
|
|
10
|
+
include Enumerable
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
@definitions = {}
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def register(type, &block)
|
|
17
|
+
key = type.to_s
|
|
18
|
+
|
|
19
|
+
if @definitions.key?(key)
|
|
20
|
+
raise Pressroom::Error, "block #{key.inspect} is already registered"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
definition = Definition.new(key)
|
|
24
|
+
block&.call(definition)
|
|
25
|
+
definition.validate!
|
|
26
|
+
|
|
27
|
+
@definitions[key] = definition
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def fetch(type)
|
|
31
|
+
@definitions[type.to_s]
|
|
32
|
+
end
|
|
33
|
+
alias [] fetch
|
|
34
|
+
|
|
35
|
+
def key?(type)
|
|
36
|
+
@definitions.key?(type.to_s)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def types
|
|
40
|
+
@definitions.keys
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def each(&block)
|
|
44
|
+
@definitions.each_value(&block)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def delete(type)
|
|
48
|
+
@definitions.delete(type.to_s)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def clear!
|
|
52
|
+
@definitions = {}
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/core_ext/string/output_safety"
|
|
4
|
+
|
|
5
|
+
module Pressroom
|
|
6
|
+
module Blocks
|
|
7
|
+
# Walks a stored document and renders each block through its partial.
|
|
8
|
+
#
|
|
9
|
+
# The +strategy+ is injected rather than read from configuration so this
|
|
10
|
+
# layer stays independent of the engine. Callers pass
|
|
11
|
+
# Pressroom.config.resolved_unknown_block_strategy.
|
|
12
|
+
class Renderer
|
|
13
|
+
STRATEGIES = %i[raise skip].freeze
|
|
14
|
+
|
|
15
|
+
def initialize(registry:, sanitizer:, strategy: :raise, logger: nil)
|
|
16
|
+
@registry = registry
|
|
17
|
+
@sanitizer = sanitizer
|
|
18
|
+
@strategy = strategy
|
|
19
|
+
@logger = logger
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def render(document, view_context)
|
|
23
|
+
blocks_of(document)
|
|
24
|
+
.filter_map { |block| render_block(block, view_context) }
|
|
25
|
+
.join
|
|
26
|
+
.html_safe
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def blocks_of(document)
|
|
30
|
+
case document
|
|
31
|
+
when Array then document.select { |item| item.is_a?(Hash) }
|
|
32
|
+
when Hash then Array(document["blocks"] || document[:blocks])
|
|
33
|
+
.select { |item| item.is_a?(Hash) }
|
|
34
|
+
else []
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def render_block(block, view_context)
|
|
41
|
+
type = (block["type"] || block[:type]).to_s
|
|
42
|
+
definition = @registry.fetch(type)
|
|
43
|
+
return unknown(type) unless definition
|
|
44
|
+
|
|
45
|
+
data = safe_data(definition, block["data"] || block[:data])
|
|
46
|
+
view_context.render(partial: definition.partial,
|
|
47
|
+
locals: { data: data, block: block })
|
|
48
|
+
rescue Pressroom::UnknownBlockError
|
|
49
|
+
raise
|
|
50
|
+
rescue StandardError => e
|
|
51
|
+
raise e if @strategy == :raise
|
|
52
|
+
|
|
53
|
+
log("pressroom: failed to render block #{type.inspect}: #{e.message}")
|
|
54
|
+
nil
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def safe_data(definition, raw)
|
|
58
|
+
data, errors = definition.schema.coerce(raw)
|
|
59
|
+
|
|
60
|
+
if errors.any?
|
|
61
|
+
message = "pressroom: invalid data for block #{definition.type.inspect}: " \
|
|
62
|
+
"#{errors.join(', ')}"
|
|
63
|
+
raise Pressroom::Error, message if @strategy == :raise
|
|
64
|
+
|
|
65
|
+
log(message)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# The schema owns sanitization dispatch: only it knows whether a
|
|
69
|
+
# field is a flat string, an array, a nested object or a tree.
|
|
70
|
+
definition.schema.sanitize(data, @sanitizer)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def unknown(type)
|
|
74
|
+
if @strategy == :raise
|
|
75
|
+
raise Pressroom::UnknownBlockError,
|
|
76
|
+
"no block registered for type #{type.inspect}"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
log("pressroom: skipping unknown block type #{type.inspect}")
|
|
80
|
+
nil
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Accepts a real Rails logger or anything that appends, which keeps
|
|
84
|
+
# this layer free of a Rails.logger dependency and keeps the unit
|
|
85
|
+
# tests able to pass a plain Array.
|
|
86
|
+
def log(message)
|
|
87
|
+
if @logger.respond_to?(:warn)
|
|
88
|
+
@logger.warn(message)
|
|
89
|
+
elsif @logger.respond_to?(:<<)
|
|
90
|
+
@logger << message
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
nil
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails-html-sanitizer"
|
|
4
|
+
require "active_support/core_ext/string/output_safety"
|
|
5
|
+
|
|
6
|
+
module Pressroom
|
|
7
|
+
module Blocks
|
|
8
|
+
# Allow-list sanitizer applied to every field the schema marks as +html+.
|
|
9
|
+
#
|
|
10
|
+
# This is the single trust boundary between editor output and rendered
|
|
11
|
+
# pages. Everything else is escaped by ERB.
|
|
12
|
+
class Sanitizer
|
|
13
|
+
def self.backend_class
|
|
14
|
+
if Rails::HTML::Sanitizer.respond_to?(:html5_support?) &&
|
|
15
|
+
Rails::HTML::Sanitizer.html5_support?
|
|
16
|
+
Rails::HTML5::SafeListSanitizer
|
|
17
|
+
else
|
|
18
|
+
Rails::HTML4::SafeListSanitizer
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def initialize(tags:, attributes:)
|
|
23
|
+
@tags = tags
|
|
24
|
+
@attributes = attributes
|
|
25
|
+
@backend = self.class.backend_class.new
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def sanitize(value)
|
|
29
|
+
return "".html_safe if value.nil?
|
|
30
|
+
|
|
31
|
+
@backend.sanitize(value.to_s, tags: @tags, attributes: @attributes)
|
|
32
|
+
.to_s.html_safe
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|