railspress-engine 0.1.2 → 1.0.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/LICENSE +20 -0
- data/README.md +193 -25
- data/app/assets/javascripts/railspress/admin.js +39 -0
- data/app/assets/javascripts/railspress/markdown_mode.js +343 -0
- data/app/assets/stylesheets/application.css +0 -0
- data/app/assets/stylesheets/railspress/admin/badges.css +70 -0
- data/app/assets/stylesheets/railspress/admin/base.css +25 -0
- data/app/assets/stylesheets/railspress/admin/buttons.css +140 -0
- data/app/assets/stylesheets/railspress/admin/cards.css +52 -0
- data/app/assets/stylesheets/railspress/admin/components/exports.css +55 -0
- data/app/assets/stylesheets/railspress/admin/components/focal_point.css +801 -0
- data/app/assets/stylesheets/railspress/admin/components/imports.css +144 -0
- data/app/assets/stylesheets/railspress/admin/components/lexxy.css +145 -0
- data/app/assets/stylesheets/railspress/admin/filters.css +73 -0
- data/app/assets/stylesheets/railspress/admin/flash.css +26 -0
- data/app/assets/stylesheets/railspress/admin/forms.css +459 -0
- data/app/assets/stylesheets/railspress/admin/layout.css +256 -0
- data/app/assets/stylesheets/railspress/admin/lists.css +24 -0
- data/app/assets/stylesheets/railspress/admin/page.css +111 -0
- data/app/assets/stylesheets/railspress/admin/responsive.css +174 -0
- data/app/assets/stylesheets/railspress/admin/stats.css +43 -0
- data/app/assets/stylesheets/railspress/admin/tables.css +163 -0
- data/app/assets/stylesheets/railspress/admin/utilities.css +202 -0
- data/app/assets/stylesheets/railspress/admin/variables.css +58 -0
- data/app/assets/stylesheets/railspress/application.css +44 -13
- data/app/controllers/railspress/admin/base_controller.rb +6 -3
- data/app/controllers/railspress/admin/categories_controller.rb +1 -1
- data/app/controllers/railspress/admin/cms_transfers_controller.rb +49 -0
- data/app/controllers/railspress/admin/content_element_versions_controller.rb +12 -0
- data/app/controllers/railspress/admin/content_elements_controller.rb +143 -0
- data/app/controllers/railspress/admin/content_groups_controller.rb +69 -0
- data/app/controllers/railspress/admin/dashboard_controller.rb +6 -0
- data/app/controllers/railspress/admin/entities_controller.rb +157 -0
- data/app/controllers/railspress/admin/exports_controller.rb +55 -0
- data/app/controllers/railspress/admin/focal_points_controller.rb +100 -0
- data/app/controllers/railspress/admin/imports_controller.rb +63 -0
- data/app/controllers/railspress/admin/posts_controller.rb +58 -4
- data/app/controllers/railspress/admin/prototypes_controller.rb +30 -0
- data/app/controllers/railspress/admin/tags_controller.rb +1 -1
- data/app/controllers/railspress/application_controller.rb +1 -0
- data/app/helpers/railspress/admin_helper.rb +733 -0
- data/app/helpers/railspress/application_helper.rb +23 -0
- data/app/helpers/railspress/cms_helper.rb +338 -0
- data/app/javascript/railspress/controllers/cms_inline_editor_controller.js +130 -0
- data/app/javascript/railspress/controllers/content_element_form_controller.js +15 -0
- data/app/javascript/railspress/controllers/crop_controller.js +224 -0
- data/app/javascript/railspress/controllers/dropzone_controller.js +261 -0
- data/app/javascript/railspress/controllers/focal_point_controller.js +124 -0
- data/app/javascript/railspress/controllers/image_section_controller.js +94 -0
- data/app/javascript/railspress/controllers/index.js +37 -0
- data/app/javascript/railspress/index.js +59 -0
- data/app/jobs/railspress/export_posts_job.rb +16 -0
- data/app/jobs/railspress/import_posts_job.rb +44 -0
- data/app/models/concerns/railspress/has_focal_point.rb +242 -0
- data/app/models/concerns/railspress/soft_deletable.rb +23 -0
- data/app/models/concerns/railspress/taggable.rb +23 -0
- data/app/models/railspress/content_element.rb +103 -0
- data/app/models/railspress/content_element_version.rb +32 -0
- data/app/models/railspress/content_group.rb +39 -0
- data/app/models/railspress/export.rb +67 -0
- data/app/models/railspress/focal_point.rb +70 -0
- data/app/models/railspress/import.rb +65 -0
- data/app/models/railspress/post.rb +102 -15
- data/app/models/railspress/post_export_processor.rb +162 -0
- data/app/models/railspress/post_import_processor.rb +382 -0
- data/app/models/railspress/tag.rb +7 -2
- data/app/models/railspress/tagging.rb +11 -0
- data/app/services/railspress/content_export_service.rb +122 -0
- data/app/services/railspress/content_import_service.rb +228 -0
- data/app/views/action_text/attachables/_remote_image.html.erb +8 -0
- data/app/views/active_storage/blobs/_blob.html.erb +1 -1
- data/app/views/layouts/railspress/admin.html.erb +3 -1
- data/app/views/railspress/admin/categories/index.html.erb +11 -15
- data/app/views/railspress/admin/cms_transfers/show.html.erb +167 -0
- data/app/views/railspress/admin/content_element_versions/show.html.erb +42 -0
- data/app/views/railspress/admin/content_elements/_form.html.erb +71 -0
- data/app/views/railspress/admin/content_elements/_inline_form.html.erb +32 -0
- data/app/views/railspress/admin/content_elements/_inline_form_frame.html.erb +6 -0
- data/app/views/railspress/admin/content_elements/edit.html.erb +6 -0
- data/app/views/railspress/admin/content_elements/index.html.erb +74 -0
- data/app/views/railspress/admin/content_elements/new.html.erb +6 -0
- data/app/views/railspress/admin/content_elements/show.html.erb +124 -0
- data/app/views/railspress/admin/content_groups/_form.html.erb +9 -0
- data/app/views/railspress/admin/content_groups/edit.html.erb +6 -0
- data/app/views/railspress/admin/content_groups/index.html.erb +42 -0
- data/app/views/railspress/admin/content_groups/new.html.erb +6 -0
- data/app/views/railspress/admin/content_groups/show.html.erb +92 -0
- data/app/views/railspress/admin/dashboard/index.html.erb +36 -1
- data/app/views/railspress/admin/entities/_form.html.erb +53 -0
- data/app/views/railspress/admin/entities/edit.html.erb +4 -0
- data/app/views/railspress/admin/entities/index.html.erb +74 -0
- data/app/views/railspress/admin/entities/new.html.erb +4 -0
- data/app/views/railspress/admin/entities/show.html.erb +117 -0
- data/app/views/railspress/admin/exports/show.html.erb +62 -0
- data/app/views/railspress/admin/imports/_instructions.html.erb +56 -0
- data/app/views/railspress/admin/imports/show.html.erb +137 -0
- data/app/views/railspress/admin/posts/_form.html.erb +102 -28
- data/app/views/railspress/admin/posts/_post_row.html.erb +40 -0
- data/app/views/railspress/admin/posts/index.html.erb +47 -36
- data/app/views/railspress/admin/posts/show.html.erb +55 -19
- data/app/views/railspress/admin/prototypes/image_section.html.erb +42 -0
- data/app/views/railspress/admin/shared/_dropzone.html.erb +84 -0
- data/app/views/railspress/admin/shared/_focal_point_editor.html.erb +102 -0
- data/app/views/railspress/admin/shared/_image_section.html.erb +159 -0
- data/app/views/railspress/admin/shared/_image_section_compact.html.erb +90 -0
- data/app/views/railspress/admin/shared/_image_section_editor.html.erb +171 -0
- data/app/views/railspress/admin/shared/_image_section_v2.html.erb +205 -0
- data/app/views/railspress/admin/shared/_sidebar.html.erb +73 -5
- data/app/views/railspress/admin/tags/index.html.erb +12 -16
- data/config/brakeman.ignore +18 -0
- data/config/importmap.rb +20 -0
- data/config/routes.rb +62 -1
- data/db/migrate/20241218000004_create_railspress_post_tags.rb +1 -1
- data/db/migrate/20241218000005_create_railspress_imports.rb +21 -0
- data/db/migrate/20241218000006_create_railspress_exports.rb +20 -0
- data/db/migrate/20241218000007_create_railspress_taggings.rb +20 -0
- data/db/migrate/20241218000008_drop_railspress_post_tags.rb +14 -0
- data/db/migrate/20241218000010_add_reading_time_to_railspress_posts.rb +5 -0
- data/db/migrate/20250105000002_create_railspress_focal_points.rb +20 -0
- data/db/migrate/20260206000001_create_railspress_content_groups.rb +18 -0
- data/db/migrate/20260206000002_create_railspress_content_elements.rb +21 -0
- data/db/migrate/20260206000003_create_railspress_content_element_versions.rb +20 -0
- data/db/migrate/20260207000001_add_unique_index_to_content_elements.rb +11 -0
- data/db/migrate/20260211112812_add_image_hint_to_railspress_content_elements.rb +7 -0
- data/db/migrate/20260211154040_add_required_to_railspress_content_elements.rb +5 -0
- data/lib/generators/railspress/entity/entity_generator.rb +89 -0
- data/lib/generators/railspress/entity/templates/migration.rb.tt +13 -0
- data/lib/generators/railspress/entity/templates/model.rb.tt +21 -0
- data/lib/generators/railspress/install/install_generator.rb +11 -20
- data/lib/generators/railspress/install/templates/initializer.rb +29 -0
- data/lib/railspress/engine.rb +38 -0
- data/lib/railspress/entity.rb +239 -0
- data/lib/railspress/version.rb +1 -1
- data/lib/railspress.rb +198 -8
- data/lib/tasks/railspress_tasks.rake +49 -4
- metadata +203 -14
- data/MIT-LICENSE +0 -20
- data/app/assets/stylesheets/railspress/admin.css +0 -1207
- data/app/models/railspress/post_tag.rb +0 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 00fa3ef64f615a9f1a7bb10c79085c50657dfa462e3f61e57002dfdc9e181d5e
|
|
4
|
+
data.tar.gz: c241000c9554ac492a6fed87f1d1bded59ea3257541d14e6a855c3102463cf03
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: aa8e9ad5cf137778317c7c94bf9c75fdffe5a32356a3a2681ef6278868e16d16d9b9686edab3bcd4c190136f8fcfc0cefab0799d50fed20c37a74978cfea7e7b
|
|
7
|
+
data.tar.gz: 2fe456078ff4a52b014a43b56f3ad8e434b62f001ac42b07dd5cc5eb335af88a5ae428c13863cf467df69e0c749bd74792b7590d94ccacc6e63b2aa528fc9089
|
data/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
O'Saasy License Agreement - https://osaasy.dev/
|
|
2
|
+
|
|
3
|
+
Copyright © 2026, Avi Flombaum (https://avi.nyc).
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
No licensee or downstream recipient may use the Software (including any modified or derivative versions) to directly compete with the original Licensor by offering it to third parties as a hosted, managed, or Software-as-a-Service (SaaS) product or cloud service where the primary value of the service is the functionality of the Software itself.
|
|
9
|
+
|
|
10
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
11
|
+
|
|
12
|
+
--
|
|
13
|
+
|
|
14
|
+
Trademarks
|
|
15
|
+
|
|
16
|
+
The Rails trademarks are the intellectual property of David Heinemeier Hansson,
|
|
17
|
+
and exclusively licensed to the Rails Foundation. Uses of "Rails" and "Ruby on
|
|
18
|
+
Rails" in this project are for identification purposes only and do not imply an
|
|
19
|
+
endorsement by or affiliation with Rails, the trademark owner, or the Rails
|
|
20
|
+
Foundation.
|
data/README.md
CHANGED
|
@@ -1,60 +1,224 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<strong>RailsPress</strong><br>
|
|
3
|
+
A mountable blog + CMS engine for Rails 8
|
|
4
|
+
</p>
|
|
2
5
|
|
|
3
|
-
|
|
6
|
+
<p align="center">
|
|
7
|
+
<a href="https://rubygems.org/gems/railspress-engine"><img src="https://img.shields.io/gem/v/railspress-engine.svg?style=flat" alt="Gem Version"></a>
|
|
8
|
+
<img src="https://img.shields.io/badge/Rails-8.1%2B-red.svg?style=flat" alt="Rails 8.1+">
|
|
9
|
+
<img src="https://img.shields.io/badge/Ruby-3.3%2B-red.svg?style=flat" alt="Ruby 3.3+">
|
|
10
|
+
<a href="https://osaasy.dev/"><img src="https://img.shields.io/badge/License-O'Saasy-blue.svg?style=flat" alt="License"></a>
|
|
11
|
+
</p>
|
|
4
12
|
|
|
5
|
-
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
RailsPress is a mountable Rails engine that gives your app a complete content management system — namespaced and isolated so it stays out of your way.
|
|
16
|
+
|
|
17
|
+
It manages three kinds of content:
|
|
18
|
+
|
|
19
|
+
## Posts, Entities, and Blocks
|
|
20
|
+
|
|
21
|
+
### Posts
|
|
6
22
|
|
|
7
|
-
|
|
8
|
-
|
|
23
|
+
Your blog. Chronological content published over time — articles, news, announcements. Categories, tags, draft/published workflow.
|
|
24
|
+
|
|
25
|
+
- Rich text editing with [Lexxy](https://github.com/aviflombaum/lexxy) + markdown mode toggle
|
|
26
|
+
- Categories, tags, draft/published workflow
|
|
9
27
|
- SEO metadata (meta title, meta description)
|
|
10
|
-
-
|
|
11
|
-
-
|
|
28
|
+
- Reading time auto-calculation
|
|
29
|
+
- Header images with focal point cropping
|
|
30
|
+
- Import/export (markdown + YAML frontmatter)
|
|
31
|
+
|
|
32
|
+
### Entities
|
|
33
|
+
|
|
34
|
+
Your pages of structured content. A portfolio with projects, a collection of case studies, a resources page with links — anything with its own schema that isn't a blog post.
|
|
35
|
+
|
|
36
|
+
You define a regular ActiveRecord model, include `Railspress::Entity`, and RailsPress gives it a full admin interface with CRUD, search, pagination, image uploads, and tagging — no scaffolding or custom views required.
|
|
37
|
+
|
|
38
|
+
- Define fields with `railspress_fields` DSL or let RailsPress auto-detect from your schema
|
|
39
|
+
- Supports string, text, rich text, boolean, datetime, attachments, array fields, and more
|
|
40
|
+
- Focal point image cropping for any attachment
|
|
41
|
+
- Polymorphic tagging
|
|
42
|
+
- Custom index columns, searchable fields, scopes
|
|
43
|
+
- Generator: `rails generate railspress:entity Project title:string description:text`
|
|
44
|
+
|
|
45
|
+
### Blocks
|
|
46
|
+
|
|
47
|
+
The copy and images on your site itself. Your homepage hero headline, an "About Us" blurb, a call-to-action, a footer tagline — the content that normally lives hardcoded in templates and requires a developer to change.
|
|
48
|
+
|
|
49
|
+
Blocks are organized into **groups** (e.g., "Homepage Hero", "Contact Info") and each block is either text or an image. You reference them in your views and they become editable in the admin — or inline, right on the page.
|
|
50
|
+
|
|
51
|
+
- Chainable Ruby API: `Railspress::CMS.find("Hero").load("headline").value`
|
|
52
|
+
- View helpers: `cms_value("Hero", "headline")` and `cms_element("Hero", "headline")`
|
|
53
|
+
- Inline editing — right-click any `cms_element` in the frontend to edit in place
|
|
54
|
+
- Auto-versioning with full audit trail
|
|
55
|
+
- Required blocks that can't be accidentally deleted
|
|
56
|
+
- Image blocks with upload, hints, and focal points
|
|
57
|
+
- Export/import block groups as ZIP
|
|
58
|
+
|
|
59
|
+
### Why all three?
|
|
60
|
+
|
|
61
|
+
Most content doesn't fit neatly into "blog posts." A portfolio piece isn't a post. A homepage headline isn't a post. RailsPress gives you the right tool for each kind of content instead of forcing everything through one model.
|
|
62
|
+
|
|
63
|
+
## Features
|
|
64
|
+
|
|
65
|
+
**Admin Interface**
|
|
66
|
+
- Dashboard with content stats and recent activity
|
|
67
|
+
- Full CRUD for posts, categories, tags, block groups, blocks, and entities
|
|
68
|
+
- Drag-and-drop image uploads with progress
|
|
69
|
+
- Collapsible sidebar, responsive design
|
|
70
|
+
- Vanilla CSS with BEM naming (`rp-` prefix) — no framework dependencies
|
|
71
|
+
|
|
72
|
+
**Developer Experience**
|
|
73
|
+
- Focal point image concern for any model: `focal_point_image :cover_photo`
|
|
74
|
+
- CSS variable theming
|
|
75
|
+
- Generators for installation and custom entities
|
|
12
76
|
|
|
13
77
|
## Requirements
|
|
14
78
|
|
|
15
|
-
- Rails 8.
|
|
79
|
+
- Rails 8.1+
|
|
16
80
|
- Ruby 3.3+
|
|
17
|
-
- ActionText
|
|
18
|
-
- Active Storage
|
|
81
|
+
- ActionText
|
|
82
|
+
- Active Storage
|
|
19
83
|
|
|
20
84
|
## Installation
|
|
21
85
|
|
|
86
|
+
Ensure ActionText and Active Storage are installed:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
rails action_text:install
|
|
90
|
+
rails active_storage:install
|
|
91
|
+
```
|
|
92
|
+
|
|
22
93
|
Add to your Gemfile:
|
|
23
94
|
|
|
24
95
|
```ruby
|
|
25
|
-
gem "railspress"
|
|
96
|
+
gem "railspress-engine"
|
|
26
97
|
```
|
|
27
98
|
|
|
28
|
-
|
|
99
|
+
Run the install generator:
|
|
29
100
|
|
|
30
101
|
```bash
|
|
31
102
|
bundle install
|
|
32
|
-
rails railspress:install
|
|
103
|
+
rails generate railspress:install
|
|
33
104
|
rails db:migrate
|
|
34
105
|
```
|
|
35
106
|
|
|
36
|
-
Mount the engine
|
|
107
|
+
Mount the engine (the install generator does this automatically):
|
|
37
108
|
|
|
38
109
|
```ruby
|
|
39
110
|
# config/routes.rb
|
|
40
111
|
Rails.application.routes.draw do
|
|
41
|
-
mount Railspress::Engine => "/
|
|
112
|
+
mount Railspress::Engine => "/railspress"
|
|
42
113
|
end
|
|
43
114
|
```
|
|
44
115
|
|
|
45
|
-
##
|
|
116
|
+
## Authentication
|
|
117
|
+
|
|
118
|
+
The admin interface is publicly accessible by default. Add authentication before deploying:
|
|
46
119
|
|
|
47
|
-
|
|
120
|
+
```ruby
|
|
121
|
+
# app/controllers/application_controller.rb
|
|
122
|
+
class ApplicationController < ActionController::Base
|
|
123
|
+
before_action :authenticate_user!, if: :railspress_admin?
|
|
48
124
|
|
|
49
|
-
|
|
50
|
-
- Create and manage blog posts with rich text content
|
|
51
|
-
- Organize posts with categories
|
|
52
|
-
- Tag posts (enter tags as comma-separated values)
|
|
53
|
-
- Save posts as drafts or publish them
|
|
125
|
+
private
|
|
54
126
|
|
|
55
|
-
|
|
127
|
+
def railspress_admin?
|
|
128
|
+
request.path.start_with?("/railspress/admin")
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
See [CONFIGURING.md](docs/CONFIGURING.md) for more authentication patterns including Devise integration.
|
|
134
|
+
|
|
135
|
+
## Quick Start
|
|
136
|
+
|
|
137
|
+
Access the admin at `/railspress/admin`. From there you can manage posts, entities, and blocks.
|
|
138
|
+
|
|
139
|
+
### Posts
|
|
140
|
+
|
|
141
|
+
Create posts with rich text, categories, tags, and header images. Build your own frontend controllers and views — see the [Blogging guide](docs/BLOGGING.md).
|
|
142
|
+
|
|
143
|
+
### Entities
|
|
56
144
|
|
|
57
|
-
|
|
145
|
+
Generate a model and register it:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
rails generate railspress:entity Project title:string client:string description:text
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
```ruby
|
|
152
|
+
class Project < ApplicationRecord
|
|
153
|
+
include Railspress::Entity
|
|
154
|
+
|
|
155
|
+
railspress_config do |c|
|
|
156
|
+
c.admin_title = "Projects"
|
|
157
|
+
c.searchable_columns = [:title, :client]
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
It now has a full admin interface at `/railspress/admin/entities/projects`. See the [Entity System guide](docs/ENTITIES.md).
|
|
163
|
+
|
|
164
|
+
### Blocks
|
|
165
|
+
|
|
166
|
+
Set up inline editing for admins:
|
|
167
|
+
|
|
168
|
+
```ruby
|
|
169
|
+
# config/initializers/railspress.rb
|
|
170
|
+
Railspress.configure do |config|
|
|
171
|
+
config.inline_editing_check = ->(ctx) { ctx.current_user&.admin? }
|
|
172
|
+
end
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Reference blocks in your views:
|
|
176
|
+
|
|
177
|
+
```erb
|
|
178
|
+
<%# Raw value (no editing wrapper) %>
|
|
179
|
+
<h1><%= cms_value("Homepage", "headline") %></h1>
|
|
180
|
+
|
|
181
|
+
<%# With inline editing (admins can right-click to edit in place) %>
|
|
182
|
+
<h1><%= cms_element("Homepage", "headline") %></h1>
|
|
183
|
+
|
|
184
|
+
<p><%= cms_element("Homepage", "subheadline") %></p>
|
|
185
|
+
<%= image_tag cms_value("Homepage", "hero_image"), alt: "Hero" %>
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Create the "Homepage" group and its blocks in the admin at `/railspress/admin/content_groups`. See the [Inline Editing guide](docs/INLINE_EDITING.md).
|
|
189
|
+
|
|
190
|
+
## Generators
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
rails generate railspress:install # Full setup
|
|
194
|
+
rails generate railspress:entity Project title:string # Add a managed entity
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Documentation
|
|
198
|
+
|
|
199
|
+
**Posts, Entities, and Blocks:**
|
|
200
|
+
|
|
201
|
+
| Guide | Description |
|
|
202
|
+
|-------|-------------|
|
|
203
|
+
| [Building a Blog](docs/BLOGGING.md) | Frontend views, RSS, SEO for posts |
|
|
204
|
+
| [Entity System](docs/ENTITIES.md) | Structured content pages (portfolios, case studies, etc.) |
|
|
205
|
+
| [Blocks & Inline Editing](docs/INLINE_EDITING.md) | Editable site copy and images |
|
|
206
|
+
|
|
207
|
+
**Everything else:**
|
|
208
|
+
|
|
209
|
+
| Guide | Description |
|
|
210
|
+
|-------|-------------|
|
|
211
|
+
| [Reference](docs/README.md) | Models, routes, and API reference |
|
|
212
|
+
| [Configuring](docs/CONFIGURING.md) | Authors, images, inline editing, and all options |
|
|
213
|
+
| [Image Focal Points](docs/image-focal-point-system.md) | Smart cropping with focal points |
|
|
214
|
+
| [Import/Export](docs/IMPORT_EXPORT.md) | Bulk operations for posts and CMS content |
|
|
215
|
+
| [Theming](docs/THEMING.md) | CSS variable customization |
|
|
216
|
+
| [Admin Helpers](docs/ADMIN_HELPERS.md) | View helper reference |
|
|
217
|
+
| [Troubleshooting](docs/TROUBLESHOOTING.md) | Common issues |
|
|
218
|
+
| [Upgrading](docs/UPGRADING.md) | Version upgrades and migrations |
|
|
219
|
+
| [Changelog](CHANGELOG.md) | Version history |
|
|
220
|
+
|
|
221
|
+
## Development
|
|
58
222
|
|
|
59
223
|
```bash
|
|
60
224
|
bundle install
|
|
@@ -64,4 +228,8 @@ bundle exec rspec
|
|
|
64
228
|
|
|
65
229
|
## License
|
|
66
230
|
|
|
67
|
-
|
|
231
|
+
Available as open source under the terms of the [O'Saasy License](https://osaasy.dev/).
|
|
232
|
+
|
|
233
|
+
## Trademarks
|
|
234
|
+
|
|
235
|
+
The Rails trademarks are the intellectual property of David Heinemeier Hansson, and exclusively licensed to the Rails Foundation. Uses of "Rails" and "Ruby on Rails" in this project are for identification purposes only and do not imply an endorsement by or affiliation with Rails, the trademark owner, or the Rails Foundation.
|
|
@@ -6,6 +6,44 @@
|
|
|
6
6
|
(function() {
|
|
7
7
|
'use strict';
|
|
8
8
|
|
|
9
|
+
// ============================================
|
|
10
|
+
// Sidebar Collapse Toggle
|
|
11
|
+
// ============================================
|
|
12
|
+
|
|
13
|
+
function initSidebarCollapse() {
|
|
14
|
+
var sidebar = document.querySelector('.rp-sidebar');
|
|
15
|
+
var layout = document.querySelector('.rp-admin-layout');
|
|
16
|
+
var toggle = document.querySelector('.rp-sidebar-toggle');
|
|
17
|
+
|
|
18
|
+
if (!sidebar || !toggle || !layout) return;
|
|
19
|
+
|
|
20
|
+
var STORAGE_KEY = 'rp-sidebar-collapsed';
|
|
21
|
+
|
|
22
|
+
function isCollapsed() {
|
|
23
|
+
return localStorage.getItem(STORAGE_KEY) === 'true';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function setCollapsed(collapsed) {
|
|
27
|
+
if (collapsed) {
|
|
28
|
+
sidebar.classList.add('rp-sidebar--collapsed');
|
|
29
|
+
layout.classList.add('rp-admin-layout--sidebar-collapsed');
|
|
30
|
+
} else {
|
|
31
|
+
sidebar.classList.remove('rp-sidebar--collapsed');
|
|
32
|
+
layout.classList.remove('rp-admin-layout--sidebar-collapsed');
|
|
33
|
+
}
|
|
34
|
+
localStorage.setItem(STORAGE_KEY, collapsed ? 'true' : 'false');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Restore state on load
|
|
38
|
+
if (isCollapsed()) {
|
|
39
|
+
setCollapsed(true);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
toggle.addEventListener('click', function() {
|
|
43
|
+
setCollapsed(!sidebar.classList.contains('rp-sidebar--collapsed'));
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
9
47
|
// ============================================
|
|
10
48
|
// Mobile Menu Toggle
|
|
11
49
|
// ============================================
|
|
@@ -194,6 +232,7 @@
|
|
|
194
232
|
// ============================================
|
|
195
233
|
|
|
196
234
|
function init() {
|
|
235
|
+
initSidebarCollapse();
|
|
197
236
|
initMobileMenu();
|
|
198
237
|
initSlugGenerator();
|
|
199
238
|
initDeleteConfirmation();
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RailsPress Markdown Mode
|
|
3
|
+
* Toggle between rich text (Lexxy) and raw markdown editing
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
(function() {
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
// ============================================
|
|
10
|
+
// HTML to Markdown Converter (Simple)
|
|
11
|
+
// ============================================
|
|
12
|
+
|
|
13
|
+
function htmlToMarkdown(html) {
|
|
14
|
+
if (!html || html.trim() === '' || html === '<p></p>' || html === '<p><br></p>') {
|
|
15
|
+
return '';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let md = html;
|
|
19
|
+
|
|
20
|
+
// Normalize whitespace
|
|
21
|
+
md = md.replace(/\r\n/g, '\n');
|
|
22
|
+
|
|
23
|
+
// Convert block elements first
|
|
24
|
+
|
|
25
|
+
// Headings
|
|
26
|
+
md = md.replace(/<h1[^>]*>(.*?)<\/h1>/gi, '# $1\n\n');
|
|
27
|
+
md = md.replace(/<h2[^>]*>(.*?)<\/h2>/gi, '## $1\n\n');
|
|
28
|
+
md = md.replace(/<h3[^>]*>(.*?)<\/h3>/gi, '### $1\n\n');
|
|
29
|
+
md = md.replace(/<h4[^>]*>(.*?)<\/h4>/gi, '#### $1\n\n');
|
|
30
|
+
md = md.replace(/<h5[^>]*>(.*?)<\/h5>/gi, '##### $1\n\n');
|
|
31
|
+
md = md.replace(/<h6[^>]*>(.*?)<\/h6>/gi, '###### $1\n\n');
|
|
32
|
+
|
|
33
|
+
// Code blocks (before inline code)
|
|
34
|
+
md = md.replace(/<pre[^>]*><code[^>]*class="[^"]*language-(\w+)[^"]*"[^>]*>([\s\S]*?)<\/code><\/pre>/gi, function(match, lang, code) {
|
|
35
|
+
return '```' + lang + '\n' + decodeHtmlEntities(code.trim()) + '\n```\n\n';
|
|
36
|
+
});
|
|
37
|
+
md = md.replace(/<pre[^>]*><code[^>]*>([\s\S]*?)<\/code><\/pre>/gi, function(match, code) {
|
|
38
|
+
return '```\n' + decodeHtmlEntities(code.trim()) + '\n```\n\n';
|
|
39
|
+
});
|
|
40
|
+
md = md.replace(/<pre[^>]*>([\s\S]*?)<\/pre>/gi, function(match, code) {
|
|
41
|
+
return '```\n' + decodeHtmlEntities(code.trim()) + '\n```\n\n';
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Blockquotes
|
|
45
|
+
md = md.replace(/<blockquote[^>]*>([\s\S]*?)<\/blockquote>/gi, function(match, content) {
|
|
46
|
+
const lines = content.replace(/<\/?p[^>]*>/gi, '\n').trim().split('\n');
|
|
47
|
+
return lines.map(line => '> ' + line.trim()).join('\n') + '\n\n';
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Horizontal rules
|
|
51
|
+
md = md.replace(/<hr[^>]*>/gi, '---\n\n');
|
|
52
|
+
|
|
53
|
+
// Lists
|
|
54
|
+
md = md.replace(/<ul[^>]*>([\s\S]*?)<\/ul>/gi, function(match, content) {
|
|
55
|
+
return content.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, '- $1\n') + '\n';
|
|
56
|
+
});
|
|
57
|
+
md = md.replace(/<ol[^>]*>([\s\S]*?)<\/ol>/gi, function(match, content) {
|
|
58
|
+
let index = 1;
|
|
59
|
+
return content.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, function(m, item) {
|
|
60
|
+
return (index++) + '. ' + item.trim() + '\n';
|
|
61
|
+
}) + '\n';
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Paragraphs
|
|
65
|
+
md = md.replace(/<p[^>]*>([\s\S]*?)<\/p>/gi, '$1\n\n');
|
|
66
|
+
|
|
67
|
+
// Line breaks
|
|
68
|
+
md = md.replace(/<br\s*\/?>/gi, ' \n');
|
|
69
|
+
|
|
70
|
+
// Now inline elements
|
|
71
|
+
|
|
72
|
+
// Images (before links to avoid nested issues)
|
|
73
|
+
md = md.replace(/<img[^>]*src="([^"]*)"[^>]*alt="([^"]*)"[^>]*>/gi, '');
|
|
74
|
+
md = md.replace(/<img[^>]*alt="([^"]*)"[^>]*src="([^"]*)"[^>]*>/gi, '');
|
|
75
|
+
md = md.replace(/<img[^>]*src="([^"]*)"[^>]*>/gi, '');
|
|
76
|
+
|
|
77
|
+
// Links
|
|
78
|
+
md = md.replace(/<a[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi, '[$2]($1)');
|
|
79
|
+
|
|
80
|
+
// Bold and italic (order matters)
|
|
81
|
+
md = md.replace(/<(strong|b)[^>]*>([\s\S]*?)<\/\1>/gi, '**$2**');
|
|
82
|
+
md = md.replace(/<(em|i)[^>]*>([\s\S]*?)<\/\1>/gi, '*$2*');
|
|
83
|
+
|
|
84
|
+
// Inline code
|
|
85
|
+
md = md.replace(/<code[^>]*>(.*?)<\/code>/gi, '`$1`');
|
|
86
|
+
|
|
87
|
+
// Strikethrough
|
|
88
|
+
md = md.replace(/<(del|s|strike)[^>]*>([\s\S]*?)<\/\1>/gi, '~~$2~~');
|
|
89
|
+
|
|
90
|
+
// Remove remaining tags
|
|
91
|
+
md = md.replace(/<[^>]+>/g, '');
|
|
92
|
+
|
|
93
|
+
// Decode HTML entities
|
|
94
|
+
md = decodeHtmlEntities(md);
|
|
95
|
+
|
|
96
|
+
// Clean up whitespace
|
|
97
|
+
md = md.replace(/\n{3,}/g, '\n\n');
|
|
98
|
+
md = md.trim();
|
|
99
|
+
|
|
100
|
+
return md;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ============================================
|
|
104
|
+
// Markdown to HTML Converter (Simple)
|
|
105
|
+
// ============================================
|
|
106
|
+
|
|
107
|
+
function markdownToHtml(md) {
|
|
108
|
+
if (!md || md.trim() === '') {
|
|
109
|
+
return '<p></p>';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let html = md;
|
|
113
|
+
|
|
114
|
+
// Escape HTML entities in the source
|
|
115
|
+
html = html.replace(/&/g, '&');
|
|
116
|
+
html = html.replace(/</g, '<');
|
|
117
|
+
html = html.replace(/>/g, '>');
|
|
118
|
+
|
|
119
|
+
// Code blocks first (to protect their contents)
|
|
120
|
+
const codeBlocks = [];
|
|
121
|
+
html = html.replace(/```(\w*)\n([\s\S]*?)```/g, function(match, lang, code) {
|
|
122
|
+
const index = codeBlocks.length;
|
|
123
|
+
const langClass = lang ? ` class="language-${lang}"` : '';
|
|
124
|
+
codeBlocks.push(`<pre><code${langClass}>${code.trim()}</code></pre>`);
|
|
125
|
+
return `%%CODEBLOCK${index}%%`;
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// Inline code (protect before other processing)
|
|
129
|
+
const inlineCodes = [];
|
|
130
|
+
html = html.replace(/`([^`]+)`/g, function(match, code) {
|
|
131
|
+
const index = inlineCodes.length;
|
|
132
|
+
inlineCodes.push(`<code>${code}</code>`);
|
|
133
|
+
return `%%INLINECODE${index}%%`;
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Headings
|
|
137
|
+
html = html.replace(/^###### (.*)$/gm, '<h6>$1</h6>');
|
|
138
|
+
html = html.replace(/^##### (.*)$/gm, '<h5>$1</h5>');
|
|
139
|
+
html = html.replace(/^#### (.*)$/gm, '<h4>$1</h4>');
|
|
140
|
+
html = html.replace(/^### (.*)$/gm, '<h3>$1</h3>');
|
|
141
|
+
html = html.replace(/^## (.*)$/gm, '<h2>$1</h2>');
|
|
142
|
+
html = html.replace(/^# (.*)$/gm, '<h1>$1</h1>');
|
|
143
|
+
|
|
144
|
+
// Horizontal rules
|
|
145
|
+
html = html.replace(/^---$/gm, '<hr>');
|
|
146
|
+
html = html.replace(/^\*\*\*$/gm, '<hr>');
|
|
147
|
+
html = html.replace(/^___$/gm, '<hr>');
|
|
148
|
+
|
|
149
|
+
// Blockquotes
|
|
150
|
+
html = html.replace(/^> (.*)$/gm, '<blockquote>$1</blockquote>');
|
|
151
|
+
// Merge consecutive blockquotes
|
|
152
|
+
html = html.replace(/<\/blockquote>\n<blockquote>/g, '\n');
|
|
153
|
+
|
|
154
|
+
// Unordered lists
|
|
155
|
+
html = html.replace(/^[\*\-] (.*)$/gm, '<li>$1</li>');
|
|
156
|
+
html = html.replace(/(<li>.*<\/li>\n?)+/g, function(match) {
|
|
157
|
+
return '<ul>\n' + match + '</ul>\n';
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// Ordered lists
|
|
161
|
+
html = html.replace(/^\d+\. (.*)$/gm, '<oli>$1</oli>');
|
|
162
|
+
html = html.replace(/(<oli>.*<\/oli>\n?)+/g, function(match) {
|
|
163
|
+
return '<ol>\n' + match.replace(/oli>/g, 'li>') + '</ol>\n';
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Images
|
|
167
|
+
html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1">');
|
|
168
|
+
|
|
169
|
+
// Links
|
|
170
|
+
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
|
|
171
|
+
|
|
172
|
+
// Bold and italic
|
|
173
|
+
html = html.replace(/\*\*\*([^*]+)\*\*\*/g, '<strong><em>$1</em></strong>');
|
|
174
|
+
html = html.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
|
|
175
|
+
html = html.replace(/\*([^*]+)\*/g, '<em>$1</em>');
|
|
176
|
+
html = html.replace(/__([^_]+)__/g, '<strong>$1</strong>');
|
|
177
|
+
html = html.replace(/_([^_]+)_/g, '<em>$1</em>');
|
|
178
|
+
|
|
179
|
+
// Strikethrough
|
|
180
|
+
html = html.replace(/~~([^~]+)~~/g, '<del>$1</del>');
|
|
181
|
+
|
|
182
|
+
// Line breaks (two spaces at end of line)
|
|
183
|
+
html = html.replace(/ \n/g, '<br>\n');
|
|
184
|
+
|
|
185
|
+
// Paragraphs - wrap remaining text blocks
|
|
186
|
+
html = html.split(/\n\n+/).map(function(block) {
|
|
187
|
+
block = block.trim();
|
|
188
|
+
if (!block) return '';
|
|
189
|
+
// Don't wrap if already a block element
|
|
190
|
+
if (/^<(h[1-6]|ul|ol|blockquote|pre|hr|div|p)/i.test(block)) {
|
|
191
|
+
return block;
|
|
192
|
+
}
|
|
193
|
+
// Don't wrap code block placeholders
|
|
194
|
+
if (/^%%CODEBLOCK\d+%%$/.test(block)) {
|
|
195
|
+
return block;
|
|
196
|
+
}
|
|
197
|
+
return '<p>' + block.replace(/\n/g, '<br>') + '</p>';
|
|
198
|
+
}).join('\n');
|
|
199
|
+
|
|
200
|
+
// Restore code blocks
|
|
201
|
+
codeBlocks.forEach(function(code, index) {
|
|
202
|
+
html = html.replace(`%%CODEBLOCK${index}%%`, code);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// Restore inline code
|
|
206
|
+
inlineCodes.forEach(function(code, index) {
|
|
207
|
+
html = html.replace(`%%INLINECODE${index}%%`, code);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
return html;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ============================================
|
|
214
|
+
// Helper Functions
|
|
215
|
+
// ============================================
|
|
216
|
+
|
|
217
|
+
function decodeHtmlEntities(text) {
|
|
218
|
+
const textarea = document.createElement('textarea');
|
|
219
|
+
textarea.innerHTML = text;
|
|
220
|
+
return textarea.value;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ============================================
|
|
224
|
+
// Markdown Mode Controller
|
|
225
|
+
// ============================================
|
|
226
|
+
|
|
227
|
+
function initMarkdownMode() {
|
|
228
|
+
console.log('[MarkdownMode] Initializing...');
|
|
229
|
+
const containers = document.querySelectorAll('[data-markdown-mode]');
|
|
230
|
+
console.log('[MarkdownMode] Found containers:', containers.length);
|
|
231
|
+
|
|
232
|
+
containers.forEach(function(container, index) {
|
|
233
|
+
console.log('[MarkdownMode] Container', index, ':', container);
|
|
234
|
+
const editor = container.querySelector('lexxy-editor');
|
|
235
|
+
const textarea = container.querySelector('[data-markdown-textarea]');
|
|
236
|
+
const toggleBtn = container.querySelector('[data-markdown-toggle]');
|
|
237
|
+
const richLabel = container.querySelector('[data-mode-label="rich"]');
|
|
238
|
+
const mdLabel = container.querySelector('[data-mode-label="markdown"]');
|
|
239
|
+
|
|
240
|
+
console.log('[MarkdownMode] Container', index, 'elements:', {
|
|
241
|
+
editor: !!editor,
|
|
242
|
+
textarea: !!textarea,
|
|
243
|
+
toggleBtn: !!toggleBtn
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
if (!editor || !textarea || !toggleBtn) {
|
|
247
|
+
console.log('[MarkdownMode] Container', index, 'missing required elements, skipping');
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
console.log('[MarkdownMode] Container', index, 'initialized successfully');
|
|
251
|
+
|
|
252
|
+
let isMarkdownMode = false;
|
|
253
|
+
|
|
254
|
+
function updateLabels() {
|
|
255
|
+
if (richLabel) richLabel.hidden = isMarkdownMode;
|
|
256
|
+
if (mdLabel) mdLabel.hidden = !isMarkdownMode;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function switchToMarkdown() {
|
|
260
|
+
// Get HTML from Lexxy
|
|
261
|
+
const html = editor.value || '';
|
|
262
|
+
|
|
263
|
+
// Convert to markdown
|
|
264
|
+
const markdown = htmlToMarkdown(html);
|
|
265
|
+
|
|
266
|
+
// Show textarea, hide editor
|
|
267
|
+
textarea.value = markdown;
|
|
268
|
+
textarea.hidden = false;
|
|
269
|
+
editor.style.display = 'none';
|
|
270
|
+
|
|
271
|
+
isMarkdownMode = true;
|
|
272
|
+
toggleBtn.setAttribute('aria-pressed', 'true');
|
|
273
|
+
updateLabels();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function switchToRichText() {
|
|
277
|
+
// Get markdown from textarea
|
|
278
|
+
const markdown = textarea.value || '';
|
|
279
|
+
|
|
280
|
+
// Convert to HTML
|
|
281
|
+
const html = markdownToHtml(markdown);
|
|
282
|
+
|
|
283
|
+
// Show editor, hide textarea
|
|
284
|
+
editor.style.display = '';
|
|
285
|
+
textarea.hidden = true;
|
|
286
|
+
|
|
287
|
+
// Load HTML into Lexxy
|
|
288
|
+
editor.value = html;
|
|
289
|
+
|
|
290
|
+
isMarkdownMode = false;
|
|
291
|
+
toggleBtn.setAttribute('aria-pressed', 'false');
|
|
292
|
+
updateLabels();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
toggleBtn.addEventListener('click', function() {
|
|
296
|
+
console.log('[MarkdownMode] Toggle clicked, current mode:', isMarkdownMode ? 'markdown' : 'rich');
|
|
297
|
+
if (isMarkdownMode) {
|
|
298
|
+
switchToRichText();
|
|
299
|
+
} else {
|
|
300
|
+
switchToMarkdown();
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// Sync textarea changes back to hidden input on form submit
|
|
305
|
+
const form = container.closest('form');
|
|
306
|
+
if (form) {
|
|
307
|
+
form.addEventListener('submit', function() {
|
|
308
|
+
if (isMarkdownMode) {
|
|
309
|
+
// Convert markdown to HTML before submit
|
|
310
|
+
const markdown = textarea.value || '';
|
|
311
|
+
const html = markdownToHtml(markdown);
|
|
312
|
+
editor.value = html;
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Initialize labels
|
|
318
|
+
updateLabels();
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// ============================================
|
|
323
|
+
// Export for testing/external use
|
|
324
|
+
// ============================================
|
|
325
|
+
|
|
326
|
+
window.RailsPress = window.RailsPress || {};
|
|
327
|
+
window.RailsPress.MarkdownMode = {
|
|
328
|
+
htmlToMarkdown: htmlToMarkdown,
|
|
329
|
+
markdownToHtml: markdownToHtml,
|
|
330
|
+
init: initMarkdownMode
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
// ============================================
|
|
334
|
+
// Initialize
|
|
335
|
+
// ============================================
|
|
336
|
+
|
|
337
|
+
if (document.readyState === 'loading') {
|
|
338
|
+
document.addEventListener('DOMContentLoaded', initMarkdownMode);
|
|
339
|
+
} else {
|
|
340
|
+
initMarkdownMode();
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
})();
|