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.
Files changed (82) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +22 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +172 -0
  5. data/Rakefile +8 -0
  6. data/app/assets/javascripts/pressroom/application.js +18 -0
  7. data/app/assets/javascripts/pressroom/controllers/editor_controller.js +146 -0
  8. data/app/assets/javascripts/pressroom/vendor/code.js +181 -0
  9. data/app/assets/javascripts/pressroom/vendor/delimiter.js +98 -0
  10. data/app/assets/javascripts/pressroom/vendor/editorjs.js +11217 -0
  11. data/app/assets/javascripts/pressroom/vendor/header.js +321 -0
  12. data/app/assets/javascripts/pressroom/vendor/image.js +1103 -0
  13. data/app/assets/javascripts/pressroom/vendor/inline-code.js +99 -0
  14. data/app/assets/javascripts/pressroom/vendor/list.js +2091 -0
  15. data/app/assets/javascripts/pressroom/vendor/marker.js +95 -0
  16. data/app/assets/javascripts/pressroom/vendor/quote.js +990 -0
  17. data/app/assets/javascripts/pressroom/vendor/stimulus.js +2563 -0
  18. data/app/assets/javascripts/pressroom/vendor/turbo.js +7256 -0
  19. data/app/assets/stylesheets/pressroom/admin.css +216 -0
  20. data/app/assets/stylesheets/pressroom/content.css +18 -0
  21. data/app/controllers/pressroom/application_controller.rb +41 -0
  22. data/app/controllers/pressroom/categories_controller.rb +50 -0
  23. data/app/controllers/pressroom/posts_controller.rb +98 -0
  24. data/app/controllers/pressroom/tags_controller.rb +39 -0
  25. data/app/controllers/pressroom/uploads_controller.rb +52 -0
  26. data/app/helpers/pressroom/application_helper.rb +4 -0
  27. data/app/helpers/pressroom/blocks_helper.rb +13 -0
  28. data/app/jobs/pressroom/application_job.rb +4 -0
  29. data/app/mailers/pressroom/application_mailer.rb +6 -0
  30. data/app/models/concerns/pressroom/authorable.rb +23 -0
  31. data/app/models/concerns/pressroom/sluggable.rb +66 -0
  32. data/app/models/pressroom/application_record.rb +7 -0
  33. data/app/models/pressroom/category.rb +54 -0
  34. data/app/models/pressroom/post.rb +90 -0
  35. data/app/models/pressroom/tag.rb +30 -0
  36. data/app/models/pressroom/tagging.rb +12 -0
  37. data/app/views/layouts/pressroom/admin.html.erb +34 -0
  38. data/app/views/layouts/pressroom/application.html.erb +17 -0
  39. data/app/views/pressroom/blocks/_code.html.erb +2 -0
  40. data/app/views/pressroom/blocks/_delimiter.html.erb +1 -0
  41. data/app/views/pressroom/blocks/_header.html.erb +5 -0
  42. data/app/views/pressroom/blocks/_image.html.erb +18 -0
  43. data/app/views/pressroom/blocks/_list.html.erb +6 -0
  44. data/app/views/pressroom/blocks/_list_items.html.erb +13 -0
  45. data/app/views/pressroom/blocks/_paragraph.html.erb +1 -0
  46. data/app/views/pressroom/blocks/_quote.html.erb +6 -0
  47. data/app/views/pressroom/categories/_form.html.erb +31 -0
  48. data/app/views/pressroom/categories/edit.html.erb +3 -0
  49. data/app/views/pressroom/categories/index.html.erb +34 -0
  50. data/app/views/pressroom/categories/new.html.erb +3 -0
  51. data/app/views/pressroom/posts/_form.html.erb +49 -0
  52. data/app/views/pressroom/posts/_post_row.html.erb +17 -0
  53. data/app/views/pressroom/posts/edit.html.erb +34 -0
  54. data/app/views/pressroom/posts/index.html.erb +38 -0
  55. data/app/views/pressroom/posts/new.html.erb +3 -0
  56. data/app/views/pressroom/tags/edit.html.erb +24 -0
  57. data/app/views/pressroom/tags/index.html.erb +27 -0
  58. data/config/editorjs_manifest.yml +53 -0
  59. data/config/importmap.rb +18 -0
  60. data/config/locales/pressroom.en.yml +16 -0
  61. data/config/locales/transliteration.ru.yml +70 -0
  62. data/config/locales/transliteration.uk.yml +70 -0
  63. data/config/routes.rb +17 -0
  64. data/db/migrate/20260821000001_create_pressroom_posts.rb +42 -0
  65. data/db/migrate/20260821000002_create_pressroom_taxonomy.rb +33 -0
  66. data/lib/pressroom/blocks/definition.rb +103 -0
  67. data/lib/pressroom/blocks/registry.rb +56 -0
  68. data/lib/pressroom/blocks/renderer.rb +97 -0
  69. data/lib/pressroom/blocks/sanitizer.rb +36 -0
  70. data/lib/pressroom/blocks/schema.rb +247 -0
  71. data/lib/pressroom/blocks/text_extractor.rb +46 -0
  72. data/lib/pressroom/built_in_blocks.rb +97 -0
  73. data/lib/pressroom/configuration.rb +64 -0
  74. data/lib/pressroom/engine.rb +33 -0
  75. data/lib/pressroom/errors.rb +11 -0
  76. data/lib/pressroom/feed.rb +69 -0
  77. data/lib/pressroom/transliteration.rb +213 -0
  78. data/lib/pressroom/version.rb +3 -0
  79. data/lib/pressroom.rb +70 -0
  80. data/lib/tasks/pressroom_tasks.rake +4 -0
  81. data/lib/tasks/pressroom_vendor.rake +31 -0
  82. metadata +171 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f4713e4ba3f3eaae896ab1ad8b152749fa9c0be2a66cf11210a57fd43ba5c2d7
4
+ data.tar.gz: cde078d6ebe1273874c9da4610b92866489f163e8b5fbeb4cd6feb1e58177640
5
+ SHA512:
6
+ metadata.gz: f712eaa9e76b6ffa4bdab60c5fb14e09d2609872d4fdbe2dd5ab8e7e7f247684a4cee01a8de66b3771fbac532381fcf2750fbfde534954ecfc8bbff5d84e2986
7
+ data.tar.gz: 37524af128ab5f3d6f27516a41d51a1a5e52086396d3f3f169acf574f981364e20f191b8e0929803acdf0fcf43292d4b3e6c549f0c1ae129ecb069c2ed121d31
data/CHANGELOG.md ADDED
@@ -0,0 +1,22 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ First release.
6
+
7
+ - Block registry: one Ruby description drives the editor, server-side
8
+ rendering, sanitization and search-text extraction.
9
+ - Seven built-in blocks: paragraph, header, list (nested), quote, code,
10
+ image, delimiter.
11
+ - Article model with drafts, archiving and schedule-free scheduled
12
+ publication.
13
+ - Slugs that survive Cyrillic titles. Transliteration rules for :ru and
14
+ :uk ship with the gem and are applied automatically when the configured
15
+ locale has none, so a Russian title yields a real slug out of the box.
16
+ - Hierarchical categories and flat tags, with no external gems.
17
+ - Mountable admin that inherits the host controller and fails closed
18
+ without an authentication hook.
19
+ - Editor.js, Stimulus and Turbo vendored and pinned through importmap; no
20
+ Node build required.
21
+ - RSS 2.0 and Atom feeds.
22
+ - Optional ActiveStorage-backed image uploads.
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2026 Alex Andreiev
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,172 @@
1
+ # Pressroom
2
+
3
+ Block-based article publishing for Rails, as a mountable engine.
4
+
5
+ Pressroom adds a block editor, an article model with drafts and scheduled
6
+ publication, categories, tags and feeds to an application you already have.
7
+ It brings no authentication, no user model and no public routes — those stay
8
+ yours.
9
+
10
+ Its distinguishing feature is the **block registry**: a block is described
11
+ once in Ruby, and that single description drives the editor, server-side
12
+ rendering, sanitization and search-text extraction.
13
+
14
+ ## Installation
15
+
16
+ ```ruby
17
+ # Gemfile
18
+ gem "pressroom"
19
+ ```
20
+
21
+ ```bash
22
+ bundle install
23
+ bin/rails db:migrate
24
+ ```
25
+
26
+ Mount the admin:
27
+
28
+ ```ruby
29
+ # config/routes.rb
30
+ mount Pressroom::Engine => "/admin"
31
+ ```
32
+
33
+ ## Configuration
34
+
35
+ ```ruby
36
+ # config/initializers/pressroom.rb
37
+ Pressroom.configure do |config|
38
+ # Required. Without it the admin refuses to open.
39
+ config.authenticate_with do
40
+ redirect_to main_app.root_path unless current_user&.editor?
41
+ end
42
+
43
+ config.author_class = "User"
44
+ config.transliteration_locale = :ru # slug rules for :ru and :uk ship with the gem
45
+ end
46
+ ```
47
+
48
+ Pressroom **fails closed**: an admin with no `authenticate_with` hook raises
49
+ rather than letting anyone in.
50
+
51
+ ## Rendering an article
52
+
53
+ Pressroom does not own your public routes. Write your own controller and
54
+ view, and render the document:
55
+
56
+ ```erb
57
+ <h1><%= @post.title %></h1>
58
+ <%= render_blocks @post %>
59
+ ```
60
+
61
+ Optionally include the structural stylesheet. It contains no typography,
62
+ colour or spacing — only the handful of rules without which a block is
63
+ broken rather than merely unstyled:
64
+
65
+ ```erb
66
+ <%= stylesheet_link_tag "pressroom/content" %>
67
+ ```
68
+
69
+ ## Registering your own block
70
+
71
+ ```ruby
72
+ Pressroom.register_block :callout do |b|
73
+ b.tool "Callout" # the Editor.js tool class
74
+ b.schema do
75
+ string :title, required: true
76
+ html :body # html fields are sanitized; string fields are escaped
77
+ enum :tone, in: %w[info warning], default: "info"
78
+ end
79
+ b.partial "blocks/callout"
80
+ b.searchable { |data| data["title"] }
81
+ end
82
+ ```
83
+
84
+ Pin the JavaScript tool from your own importmap and it appears in the
85
+ toolbar. Field types: `string`, `html`, `integer`, `boolean`, `enum`,
86
+ `array`, `nested`, `tree`.
87
+
88
+ ## Publication
89
+
90
+ Publication is a timestamp comparison, not a background job:
91
+
92
+ ```ruby
93
+ post.update!(status: :published, published_at: Time.current) # live now
94
+ post.update!(status: :published, published_at: 3.days.from_now) # scheduled
95
+
96
+ Pressroom::Post.published # status published AND published_at <= now
97
+ ```
98
+
99
+ Nothing polls and nothing is enqueued. A scheduled post becomes visible the
100
+ moment its timestamp passes. The admin exposes both as buttons: *Publish
101
+ now*, *Unpublish*, and a *Schedule publication* field.
102
+
103
+ ## Feeds
104
+
105
+ ```ruby
106
+ feed = Pressroom::Feed.new(
107
+ Pressroom::Post.published.recent.limit(20),
108
+ title: "Example blog",
109
+ url: "https://example.com",
110
+ description: "Latest posts"
111
+ ) { |post| post_url(post) }
112
+
113
+ render xml: feed.to_rss # or feed.to_atom
114
+ ```
115
+
116
+ ## Search
117
+
118
+ Pressroom maintains a `plain_text` column on every save and does not search
119
+ it. Index it with whatever you already use:
120
+
121
+ ```ruby
122
+ Pressroom::Post.published.where("plain_text ILIKE ?", "%rails%")
123
+ ```
124
+
125
+ ## Images
126
+
127
+ The image block renders from a URL always. File uploads additionally require
128
+ ActiveStorage; when it is present the upload endpoint and the editor's image
129
+ tool are enabled automatically. Uploads keep CSRF protection — the editor
130
+ sends the token as a request header.
131
+
132
+ ## Built-in blocks
133
+
134
+ `paragraph`, `header`, `list` (nested), `quote`, `code`, `image`,
135
+ `delimiter`. Tables, checklists, embeds and attachments are deliberately not
136
+ included — register them yourself.
137
+
138
+ ## JavaScript
139
+
140
+ Editor.js, Stimulus and Turbo are vendored into the gem and pinned through
141
+ importmap. No Node build step is required in your application, and nothing
142
+ is fetched from a CDN, so a strict CSP and an offline network are both fine.
143
+
144
+ Stimulus and Turbo are pinned under private names (`pressroom/stimulus`,
145
+ `pressroom/turbo`) so they cannot collide with your own pins for those
146
+ packages.
147
+
148
+ ## Security
149
+
150
+ - Fields declared `html` are sanitized through an allow-list; every other
151
+ field is escaped by ERB.
152
+ - The allow-list is configurable via `sanitizer_tags` and
153
+ `sanitizer_attributes`.
154
+ - An unknown block type, malformed stored JSON, a missing partial or a
155
+ deleted author degrade to a logged skip in production and raise in
156
+ development, so one damaged article cannot take a page down.
157
+
158
+ ## Public API and versioning
159
+
160
+ Semver applies to: `Pressroom.register_block` and its block DSL,
161
+ `Pressroom.configure`, `Pressroom::Post`, `Pressroom::Category`,
162
+ `Pressroom::Tag`, `Pressroom::Feed`, the `render_blocks` helper, and the
163
+ stored JSON format. Everything else is internal.
164
+
165
+ ## Requirements
166
+
167
+ Ruby >= 3.4, Rails >= 7.2. Tested against Rails 7.2, 8.0 and 8.1 on
168
+ PostgreSQL and SQLite.
169
+
170
+ ## License
171
+
172
+ MIT.
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require "bundler/setup"
2
+
3
+ APP_RAKEFILE = File.expand_path("spec/dummy/Rakefile", __dir__)
4
+ load "rails/tasks/engine.rake"
5
+
6
+ require "bundler/gem_tasks"
7
+
8
+ Dir.glob("lib/tasks/*.rake").each { |task_file| load task_file }
@@ -0,0 +1,18 @@
1
+ import "pressroom/turbo"
2
+ import { Application } from "pressroom/stimulus"
3
+ import PressroomEditorController from "pressroom/controllers/editor_controller"
4
+
5
+ // Turbo is loaded here on purpose. A host application almost certainly runs
6
+ // Turbo Drive, so navigating into the admin caches these pages whether or not
7
+ // this layout asks for it -- which means the editor's teardown really does
8
+ // have to work. Loading it here makes the admin behave in tests the way it
9
+ // behaves in production instead of hiding the problem.
10
+ //
11
+ // The admin runs its own Stimulus application rather than reusing the host's.
12
+ // The admin layout is ours and loads only this module, so there is nothing to
13
+ // share, and a self-contained instance means the gem never depends on how the
14
+ // host names or boots its controllers.
15
+ const application = Application.start()
16
+ application.register("pressroom-editor", PressroomEditorController)
17
+
18
+ export { application }
@@ -0,0 +1,146 @@
1
+ import { Controller } from "pressroom/stimulus"
2
+ import EditorJS from "@editorjs/editorjs"
3
+ import Header from "@editorjs/header"
4
+ import List from "@editorjs/list"
5
+ import Quote from "@editorjs/quote"
6
+ import CodeTool from "@editorjs/code"
7
+ import ImageTool from "@editorjs/image"
8
+ import Delimiter from "@editorjs/delimiter"
9
+ import Marker from "@editorjs/marker"
10
+ import InlineCode from "@editorjs/inline-code"
11
+
12
+ export default class extends Controller {
13
+ static targets = ["field", "holder"]
14
+ static values = { uploadUrl: String }
15
+
16
+ connect() {
17
+ this.beforeCache = this.beforeCache.bind(this)
18
+ this.onSubmitClick = this.onSubmitClick.bind(this)
19
+ this.flushing = false
20
+
21
+ this.editor = new EditorJS({
22
+ holder: this.holderTarget,
23
+ data: this.readDocument(),
24
+ tools: this.tools(),
25
+ // The hidden field is kept current as the author types rather than
26
+ // serialized when the form is submitted. Intercepting submit would
27
+ // mean preventDefault, an await, and then re-submitting -- and a
28
+ // nested requestSubmit() raised from inside a submit event is
29
+ // silently ignored by the browser. Keeping the field fresh leaves
30
+ // form submission entirely to Turbo, with nothing to re-enter.
31
+ onChange: () => this.persist()
32
+ })
33
+
34
+ document.addEventListener("turbo:before-cache", this.beforeCache)
35
+ this.form?.addEventListener("click", this.onSubmitClick, true)
36
+ }
37
+
38
+ disconnect() {
39
+ document.removeEventListener("turbo:before-cache", this.beforeCache)
40
+ this.form?.removeEventListener("click", this.onSubmitClick, true)
41
+ this.teardown()
42
+ }
43
+
44
+ get form() {
45
+ return this.element.closest("form")
46
+ }
47
+
48
+ csrfToken() {
49
+ return document.querySelector('meta[name="csrf-token"]')?.content || ""
50
+ }
51
+
52
+ // Flush the editor before the form is submitted.
53
+ //
54
+ // This hooks the submit button's click rather than the form's submit
55
+ // event on purpose. Serializing is asynchronous, so the handler must
56
+ // stop the default action, await, and then let it happen again -- and a
57
+ // requestSubmit() raised from inside a submit event is silently dropped
58
+ // by the browser. Re-dispatching a click is not, so Turbo still handles
59
+ // the submission normally.
60
+ //
61
+ // Editor.js debounces onChange, so relying on that alone would lose the
62
+ // last thing an author typed before hitting save.
63
+ async onSubmitClick(event) {
64
+ if (this.flushing) return
65
+
66
+ const submitter = event.target.closest?.("input[type=submit], button[type=submit]")
67
+ if (!submitter) return
68
+
69
+ event.preventDefault()
70
+ event.stopPropagation()
71
+
72
+ this.flushing = true
73
+ await this.persist()
74
+ submitter.click()
75
+ this.flushing = false
76
+ }
77
+
78
+ readDocument() {
79
+ try {
80
+ const parsed = JSON.parse(this.fieldTarget.value || "{}")
81
+ return { blocks: parsed.blocks || [] }
82
+ } catch {
83
+ return { blocks: [] }
84
+ }
85
+ }
86
+
87
+ tools() {
88
+ const tools = {
89
+ header: Header,
90
+ list: List,
91
+ quote: Quote,
92
+ code: CodeTool,
93
+ delimiter: Delimiter,
94
+ marker: Marker,
95
+ inlineCode: InlineCode
96
+ }
97
+
98
+ // The image tool is only useful when the host exposes an upload
99
+ // endpoint, which it does only when ActiveStorage is available.
100
+ if (this.hasUploadUrlValue && this.uploadUrlValue) {
101
+ tools.image = {
102
+ class: ImageTool,
103
+ config: {
104
+ endpoints: { byFile: this.uploadUrlValue },
105
+ // Editor.js posts FormData without a Rails CSRF token, so it is
106
+ // supplied as a header instead of disabling CSRF protection.
107
+ additionalRequestHeaders: { "X-CSRF-Token": this.csrfToken() }
108
+ }
109
+ }
110
+ }
111
+
112
+ return tools
113
+ }
114
+
115
+ async persist() {
116
+ if (!this.editor) return
117
+
118
+ try {
119
+ const output = await this.editor.save()
120
+ this.fieldTarget.value = JSON.stringify(output)
121
+ } catch (error) {
122
+ console.error("pressroom: could not serialize the editor", error)
123
+ }
124
+ }
125
+
126
+ // Turbo caches a snapshot of the page on navigation. A live editor left
127
+ // in that snapshot is restored as markup and then initialized a second
128
+ // time, producing two editors and duplicated content.
129
+ beforeCache() {
130
+ this.teardown()
131
+ this.holderTarget.innerHTML = ""
132
+ }
133
+
134
+ teardown() {
135
+ const editor = this.editor
136
+ if (!editor) return
137
+
138
+ this.editor = null
139
+
140
+ try {
141
+ editor.destroy()
142
+ } catch {
143
+ // The editor may not have finished initializing; nothing to clean up.
144
+ }
145
+ }
146
+ }
@@ -0,0 +1,181 @@
1
+ (function(){"use strict";try{if(typeof document<"u"){var e=document.createElement("style");e.appendChild(document.createTextNode(".ce-code__textarea{min-height:200px;font-family:Menlo,Monaco,Consolas,Courier New,monospace;color:#41314e;line-height:1.6em;font-size:12px;background:#f8f7fa;border:1px solid #f1f1f4;box-shadow:none;white-space:pre;word-wrap:normal;overflow-x:auto;resize:vertical}")),document.head.appendChild(e)}}catch(o){console.error("vite-plugin-css-injected-by-js",o)}})();
2
+ function c(l, t) {
3
+ let a = "";
4
+ for (; a !== `
5
+ ` && t > 0; )
6
+ t = t - 1, a = l.substr(t, 1);
7
+ return a === `
8
+ ` && (t += 1), t;
9
+ }
10
+ const h = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 8L5 12L9 16"/><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 8L19 12L15 16"/></svg>';
11
+ /**
12
+ * CodeTool for Editor.js
13
+ * @version 2.0.0
14
+ * @license MIT
15
+ */
16
+ class d {
17
+ /**
18
+ * Notify core that read-only mode is supported
19
+ * @returns true if read-only mode is supported
20
+ */
21
+ static get isReadOnlySupported() {
22
+ return !0;
23
+ }
24
+ /**
25
+ * Allows pressing Enter key to create line breaks inside the CodeTool textarea
26
+ * This enables multi-line input within the code editor.
27
+ * @returns true if line breaks are allowed in the textarea
28
+ */
29
+ static get enableLineBreaks() {
30
+ return !0;
31
+ }
32
+ /**
33
+ * Render plugin`s main Element and fill it with saved data
34
+ * @param options - tool constricting options
35
+ * @param options.data — previously saved plugin code
36
+ * @param options.config - user config for Tool
37
+ * @param options.api - Editor.js API
38
+ * @param options.readOnly - read only mode flag
39
+ */
40
+ constructor({ data: t, config: e, api: a, readOnly: s }) {
41
+ this.api = a, this.readOnly = s, this.placeholder = this.api.i18n.t(e.placeholder || d.DEFAULT_PLACEHOLDER), this.CSS = {
42
+ baseClass: this.api.styles.block,
43
+ input: this.api.styles.input,
44
+ wrapper: "ce-code",
45
+ textarea: "ce-code__textarea"
46
+ }, this.nodes = {
47
+ holder: null,
48
+ textarea: null
49
+ }, this.data = {
50
+ code: t.code ?? ""
51
+ }, this.nodes.holder = this.drawView();
52
+ }
53
+ /**
54
+ * Return Tool's view
55
+ * @returns this.nodes.holder - Code's wrapper
56
+ */
57
+ render() {
58
+ return this.nodes.holder;
59
+ }
60
+ /**
61
+ * Extract Tool's data from the view
62
+ * @param codeWrapper - CodeTool's wrapper, containing textarea with code
63
+ * @returns - saved plugin code
64
+ */
65
+ save(t) {
66
+ return {
67
+ code: t.querySelector("textarea").value
68
+ };
69
+ }
70
+ /**
71
+ * onPaste callback fired from Editor`s core
72
+ * @param event - event with pasted content
73
+ */
74
+ onPaste(t) {
75
+ switch (t.type) {
76
+ case "tag": {
77
+ const e = t.detail.data;
78
+ this.handleHTMLPaste(e);
79
+ break;
80
+ }
81
+ }
82
+ }
83
+ /**
84
+ * Returns Tool`s data from private property
85
+ * @returns
86
+ */
87
+ get data() {
88
+ return this._data;
89
+ }
90
+ /**
91
+ * Set Tool`s data to private property and update view
92
+ * @param data - saved tool data
93
+ */
94
+ set data(t) {
95
+ this._data = t, this.nodes.textarea && (this.nodes.textarea.value = t.code);
96
+ }
97
+ /**
98
+ * Get Tool toolbox settings.
99
+ * Provides the icon and title to display in the toolbox for the CodeTool.
100
+ * @returns An object containing:
101
+ * - icon: SVG representation of the Tool's icon
102
+ * - title: Title to show in the toolbox
103
+ */
104
+ static get toolbox() {
105
+ return {
106
+ icon: h,
107
+ title: "Code"
108
+ };
109
+ }
110
+ /**
111
+ * Default placeholder for CodeTool's textarea
112
+ * @returns
113
+ */
114
+ static get DEFAULT_PLACEHOLDER() {
115
+ return "Enter a code";
116
+ }
117
+ /**
118
+ * Used by Editor.js paste handling API.
119
+ * Provides configuration to handle CODE tag.
120
+ * @returns
121
+ */
122
+ static get pasteConfig() {
123
+ return {
124
+ tags: ["pre"]
125
+ };
126
+ }
127
+ /**
128
+ * Automatic sanitize config
129
+ * @returns
130
+ */
131
+ static get sanitize() {
132
+ return {
133
+ code: !0
134
+ // Allow HTML tags
135
+ };
136
+ }
137
+ /**
138
+ * Handles Tab key pressing (adds/removes indentations)
139
+ * @param event - keydown
140
+ */
141
+ tabHandler(t) {
142
+ t.stopPropagation(), t.preventDefault();
143
+ const e = t.target, a = t.shiftKey, s = e.selectionStart, r = e.value, n = " ";
144
+ let i;
145
+ if (!a)
146
+ i = s + n.length, e.value = r.substring(0, s) + n + r.substring(s);
147
+ else {
148
+ const o = c(r, s);
149
+ if (r.substr(o, n.length) !== n)
150
+ return;
151
+ e.value = r.substring(0, o) + r.substring(o + n.length), i = s - n.length;
152
+ }
153
+ e.setSelectionRange(i, i);
154
+ }
155
+ /**
156
+ * Create Tool's view
157
+ * @returns
158
+ */
159
+ drawView() {
160
+ const t = document.createElement("div"), e = document.createElement("textarea");
161
+ return t.classList.add(this.CSS.baseClass, this.CSS.wrapper), e.classList.add(this.CSS.textarea, this.CSS.input), e.value = this.data.code, e.placeholder = this.placeholder, this.readOnly && (e.disabled = !0), t.appendChild(e), e.addEventListener("keydown", (a) => {
162
+ switch (a.code) {
163
+ case "Tab":
164
+ this.tabHandler(a);
165
+ break;
166
+ }
167
+ }), this.nodes.textarea = e, t;
168
+ }
169
+ /**
170
+ * Extracts the code content from the pasted element's innerHTML and populates the tool's data.
171
+ * @param element - pasted HTML element
172
+ */
173
+ handleHTMLPaste(t) {
174
+ this.data = {
175
+ code: t.innerHTML
176
+ };
177
+ }
178
+ }
179
+ export {
180
+ d as default
181
+ };
@@ -0,0 +1,98 @@
1
+ (function(){"use strict";try{if(typeof document<"u"){var e=document.createElement("style");e.appendChild(document.createTextNode('.ce-delimiter{line-height:1.6em;width:100%;text-align:center}.ce-delimiter:before{display:inline-block;content:"***";font-size:30px;line-height:65px;height:30px;letter-spacing:.2em}')),document.head.appendChild(e)}}catch(t){console.error("vite-plugin-css-injected-by-js",t)}})();
2
+ const r = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><line x1="6" x2="10" y1="12" y2="12" stroke="currentColor" stroke-linecap="round" stroke-width="2"/><line x1="14" x2="18" y1="12" y2="12" stroke="currentColor" stroke-linecap="round" stroke-width="2"/></svg>';
3
+ /**
4
+ * Delimiter Block for the Editor.js.
5
+ *
6
+ * @author CodeX (team@ifmo.su)
7
+ * @copyright CodeX 2018
8
+ * @license The MIT License (MIT)
9
+ * @version 2.0.0
10
+ */
11
+ class n {
12
+ /**
13
+ * Notify core that read-only mode is supported
14
+ * @return {boolean}
15
+ */
16
+ static get isReadOnlySupported() {
17
+ return !0;
18
+ }
19
+ /**
20
+ * Allow Tool to have no content
21
+ * @return {boolean}
22
+ */
23
+ static get contentless() {
24
+ return !0;
25
+ }
26
+ /**
27
+ * Render plugin`s main Element and fill it with saved data
28
+ *
29
+ * @param {{data: DelimiterData, config: object, api: object}}
30
+ * data — previously saved data
31
+ * config - user config for Tool
32
+ * api - Editor.js API
33
+ */
34
+ constructor({ data: t, config: s, api: e }) {
35
+ this.api = e, this._CSS = {
36
+ block: this.api.styles.block,
37
+ wrapper: "ce-delimiter"
38
+ }, this._element = this.drawView(), this.data = t;
39
+ }
40
+ /**
41
+ * Create Tool's view
42
+ * @return {HTMLDivElement}
43
+ * @private
44
+ */
45
+ drawView() {
46
+ let t = document.createElement("div");
47
+ return t.classList.add(this._CSS.wrapper, this._CSS.block), t;
48
+ }
49
+ /**
50
+ * Return Tool's view
51
+ * @returns {HTMLDivElement}
52
+ * @public
53
+ */
54
+ render() {
55
+ return this._element;
56
+ }
57
+ /**
58
+ * Extract Tool's data from the view
59
+ * @param {HTMLDivElement} toolsContent - Paragraph tools rendered view
60
+ * @returns {DelimiterData} - saved data
61
+ * @public
62
+ */
63
+ save(t) {
64
+ return {};
65
+ }
66
+ /**
67
+ * Get Tool toolbox settings
68
+ * icon - Tool icon's SVG
69
+ * title - title to show in toolbox
70
+ *
71
+ * @return {{icon: string, title: string}}
72
+ */
73
+ static get toolbox() {
74
+ return {
75
+ icon: r,
76
+ title: "Delimiter"
77
+ };
78
+ }
79
+ /**
80
+ * Delimiter onPaste configuration
81
+ *
82
+ * @public
83
+ */
84
+ static get pasteConfig() {
85
+ return { tags: ["HR"] };
86
+ }
87
+ /**
88
+ * On paste callback that is fired from Editor
89
+ *
90
+ * @param {PasteEvent} event - event with pasted data
91
+ */
92
+ onPaste(t) {
93
+ this.data = {};
94
+ }
95
+ }
96
+ export {
97
+ n as default
98
+ };