comfy_middle_seat 4.2.0 → 4.4.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/.github/workflows/coveralls.yml +3 -0
- data/.github/workflows/rubyonrails.yml +3 -0
- data/CHANGELOG.md +28 -3
- data/README.md +121 -1
- data/app/assets/javascripts/comfy/admin/cms/cms_tag_completions.js +177 -0
- data/app/assets/javascripts/comfy/admin/cms/codemirror.js +3 -0
- data/app/assets/stylesheets/comfy/admin/cms/application.sass +1 -0
- data/app/assets/stylesheets/comfy/admin/cms/codemirror_overrides.sass +11 -0
- data/app/controllers/comfy/admin/cms/files_controller.rb +7 -1
- data/app/models/comfy/cms/file.rb +3 -3
- data/app/views/comfy/admin/cms/fragments/_form_fragment_attachments.html.haml +1 -1
- data/config/initializers/comfy_middle_seat.rb +4 -0
- data/lib/comfy_middle_seat/configuration.rb +4 -0
- data/lib/comfy_middle_seat/content/image_transformations.rb +100 -0
- data/lib/comfy_middle_seat/content/renderer.rb +53 -8
- data/lib/comfy_middle_seat/content/tags/file.rb +3 -4
- data/lib/comfy_middle_seat/content/tags/file_link.rb +3 -7
- data/lib/comfy_middle_seat/content/tags/image.rb +3 -7
- data/lib/comfy_middle_seat/content/tags/markdown.rb +4 -1
- data/lib/comfy_middle_seat/content/tags/mixins/file_content.rb +6 -3
- data/lib/comfy_middle_seat/content/tags/page_file_link.rb +3 -7
- data/lib/comfy_middle_seat/content/tags/snippet.rb +2 -1
- data/lib/comfy_middle_seat/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 654bed46bea3e8580f57ce3b9ba688ceb7b592d83f76c6762b771ee21c11266c
|
|
4
|
+
data.tar.gz: 6164127c75dd90045b376a401887ab95976a6722f9b084bc3e7b937056d7570e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1c2c3126c49194876cb5794df8d986f524ed93fc159337d08651f54dcd88fbcd9a2f9ccfb3ee80ea8eb6046217a0a82b94f73dcf442aa4c33877c196b61bad01
|
|
7
|
+
data.tar.gz: 235b0a6ecc612706f168d2daf513f1935addbd9083afea5a2f80726dffb65e80fc74021dae0cd3de1284d26f4df19ab084f336907489b409324f071c0e49171b
|
|
@@ -39,6 +39,9 @@ jobs:
|
|
|
39
39
|
ruby-version: ${{ matrix.ruby-version }}
|
|
40
40
|
bundler-cache: true
|
|
41
41
|
|
|
42
|
+
- name: Install image processors
|
|
43
|
+
run: sudo apt-get update && sudo apt-get install -y imagemagick libvips-dev
|
|
44
|
+
|
|
42
45
|
- name: Update CMS gem
|
|
43
46
|
run: bundle update comfy_middle_seat
|
|
44
47
|
|
|
@@ -40,6 +40,9 @@ jobs:
|
|
|
40
40
|
ruby-version: ${{ matrix.ruby-version }}
|
|
41
41
|
bundler-cache: true
|
|
42
42
|
|
|
43
|
+
- name: Install image processors
|
|
44
|
+
run: sudo apt-get update && sudo apt-get install -y imagemagick libvips-dev
|
|
45
|
+
|
|
43
46
|
- name: Update CMS gem
|
|
44
47
|
run: bundle update comfy_middle_seat
|
|
45
48
|
|
data/CHANGELOG.md
CHANGED
|
@@ -12,16 +12,39 @@ This changelog follows the project's lineage: **Comfortable Mexican Sofa → Com
|
|
|
12
12
|
|
|
13
13
|
## [Unreleased]
|
|
14
14
|
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- Resize, crop, and pad images with ImageMagick or Vips.
|
|
18
|
+
- Validate image dimensions with a configurable size limit.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- Restore supported legacy image resizing and modernize admin thumbnails.
|
|
23
|
+
- Keep original file links for images that cannot be resized.
|
|
24
|
+
|
|
25
|
+
## [v4.3.0] - 2026-10-17
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- Hide content with CMS comment blocks and add short inline notes.
|
|
30
|
+
- Insert CMS tags with editor shortcuts and selected parameter placeholders.
|
|
31
|
+
- Find shortcuts with `;;` + Tab or Ctrl+Space and fuzzy search.
|
|
32
|
+
|
|
33
|
+
## [v4.2.0] - 2026-10-13
|
|
34
|
+
|
|
15
35
|
### Changed
|
|
16
36
|
|
|
17
37
|
- Keep Page editing actions together in a sticky top toolbar.
|
|
18
38
|
- Save edits and publish or unpublish a page with one button.
|
|
19
|
-
-
|
|
20
|
-
-
|
|
39
|
+
- Show unpublished status below the heading and save hints as button tooltips.
|
|
40
|
+
- Simplify the save confirmation to “Page updated.”
|
|
41
|
+
- Fade success and informational notices after five seconds; pause on hover or focus.
|
|
42
|
+
- Keep warnings and errors visible and respect reduced-motion preferences.
|
|
21
43
|
|
|
22
44
|
### Fixed
|
|
23
45
|
|
|
24
46
|
- Keep editing buttons aligned with the page heading above publication status.
|
|
47
|
+
- Keep editing controls usable after Preview.
|
|
25
48
|
- Restore dismissal of admin flash notices.
|
|
26
49
|
- Keep Add Child Layout on one line when space allows.
|
|
27
50
|
|
|
@@ -251,7 +274,9 @@ First release of `comfortable_media_surfer`. This new gem is a revival of [Comfo
|
|
|
251
274
|
|
|
252
275
|
- Rebranded **ComfortableMexicanSofa** as **ComfortableMediaSurfer** in order to publish new gem (database table names and schema have not changed).
|
|
253
276
|
|
|
254
|
-
[Unreleased]: https://github.com/avonderluft/comfy-middle-seat/compare/v4.
|
|
277
|
+
[Unreleased]: https://github.com/avonderluft/comfy-middle-seat/compare/v4.3.0...master
|
|
278
|
+
[v4.3.0]: https://github.com/avonderluft/comfy-middle-seat/compare/v4.2.0...v4.3.0
|
|
279
|
+
[v4.2.0]: https://github.com/avonderluft/comfy-middle-seat/compare/v4.1.0...v4.2.0
|
|
255
280
|
[v4.1.0]: https://github.com/avonderluft/comfy-middle-seat/compare/v4.0.4...v4.1.0
|
|
256
281
|
[v4.0.4]: https://github.com/avonderluft/comfy-middle-seat/compare/ea2bb99c0398e1f4d6e850a25e0c13afc577c3ad...v4.0.4
|
|
257
282
|
[v4.0.3]: https://github.com/avonderluft/comfy-middle-seat/compare/v4.0.2...ea2bb99c0398e1f4d6e850a25e0c13afc577c3ad
|
data/README.md
CHANGED
|
@@ -114,6 +114,126 @@ A Site defines a hostname, path, and language. Layouts define page structure and
|
|
|
114
114
|
|
|
115
115
|
When an editor creates a Page using this Layout, the admin automatically presents fields for `title` and `content`. Content tags can also provide Markdown, files, images, dates, numbers, checkboxes, snippets, partials, helpers, navigation, and other reusable content.
|
|
116
116
|
|
|
117
|
+
### Editor shortcuts
|
|
118
|
+
|
|
119
|
+
In HTML and Markdown code editors, type a shortcut and press **Tab** to insert a CMS tag. The parameter placeholder is selected so you can replace it immediately. For example, `;md` inserts `{{ cms:markdown content }}` with `content` selected.
|
|
120
|
+
|
|
121
|
+
Type **`;;` then Tab**, or press **Ctrl+Space**, for a searchable shortcut list. Search by shortcut or tag name: `c` prioritizes comment, children, and other `c` shortcuts; `p fl` finds `page_file_link`. Exact and prefix matches appear before fuzzy matches.
|
|
122
|
+
|
|
123
|
+
Use the arrow keys and **Enter** or **Tab** to choose. The tag replaces the entire trigger and search text. **Escape** closes the list without changing your text. Full names such as `;markdown` also work for direct expansion. One **Undo** reverses the expansion without removing what you typed.
|
|
124
|
+
|
|
125
|
+
| Shortcut | Inserts | Selected placeholder |
|
|
126
|
+
| --- | --- | --- |
|
|
127
|
+
| `;md` | `markdown` | `content` |
|
|
128
|
+
| `;tx` | `text` | `field` |
|
|
129
|
+
| `;ta` | `textarea` | `content` |
|
|
130
|
+
| `;wys` | `wysiwyg` | `content` |
|
|
131
|
+
| `;da` | `date` | `field` |
|
|
132
|
+
| `;dt` | `datetime` | `field` |
|
|
133
|
+
| `;num` | `number` | `field` |
|
|
134
|
+
| `;cb` | `checkbox` | `field` |
|
|
135
|
+
| `;fi` | `file`, as a link | `attachment` |
|
|
136
|
+
| `;fs` | `files`, as links | `attachments` |
|
|
137
|
+
| `;im` | `image` | `image label` |
|
|
138
|
+
| `;au` | `audio` | `path/to/audio` |
|
|
139
|
+
| `;fl` | `file_link`, as a link | `file_id` |
|
|
140
|
+
| `;pfl` | `page_file_link`, as a link | `attachment` |
|
|
141
|
+
| `;sn` | `snippet` | `identifier` |
|
|
142
|
+
| `;as`, `;css` | CSS `asset` tag | `layout_identifier` |
|
|
143
|
+
| `;js` | JavaScript `asset` tag | `layout_identifier` |
|
|
144
|
+
| `;hlp` | `helper` | `method_name` |
|
|
145
|
+
| `;par` | `partial` | `path/to/partial` |
|
|
146
|
+
| `;tpl` | `template` | `path/to/template` |
|
|
147
|
+
| `;cr` | `breadcrumbs` | — |
|
|
148
|
+
| `;ch` | `children` | — |
|
|
149
|
+
| `;si` | `siblings` | — |
|
|
150
|
+
| `;com` | Complete multiline comment block | `Comment here` |
|
|
151
|
+
| `;nt` | `comment "Note"` (also `;note`) | `Note` |
|
|
152
|
+
| `;ec` | `endcomment` | — |
|
|
153
|
+
| `;end` | `end` | — |
|
|
154
|
+
|
|
155
|
+
Placeholders are examples: replace them with your field names, identifiers, paths, or IDs. `;asset` inserts the CSS version; `;comment` inserts a full comment block.
|
|
156
|
+
|
|
157
|
+
Shortcuts work at the start of a line, after whitespace, or after `>`, outside an existing CMS tag. Unrecognized shortcuts retain normal Tab behavior. CSS, JavaScript, ordinary text fields, and visual editors are unchanged. Saved content uses the standard `{{ cms:... }}` syntax.
|
|
158
|
+
|
|
159
|
+
### CMS comments
|
|
160
|
+
|
|
161
|
+
Use a quoted note for a short comment:
|
|
162
|
+
|
|
163
|
+
```html
|
|
164
|
+
{{cms:comment "Replace this banner in December"}}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Use a block to hide a section, including any CMS tags inside it:
|
|
168
|
+
|
|
169
|
+
```html
|
|
170
|
+
{{cms:comment}}
|
|
171
|
+
Internal note: this section is not ready.
|
|
172
|
+
{{cms:markdown old_content}}
|
|
173
|
+
{{cms:snippet seasonal_banner}}
|
|
174
|
+
{{cms:endcomment}}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Comments produce no rendered output and do not create editor fields. Unlike HTML comments, enclosed CMS tags are not processed. Source text and existing field values remain stored; removing a comment restores normal processing.
|
|
178
|
+
|
|
179
|
+
Blocks may be nested and must end with `{{cms:endcomment}}`, not `{{cms:end}}`. Short notes accept single or double quotes, with backslash-escaped quotes. Unclosed blocks and malformed comment tags raise a clear syntax error. Comments work in layouts, snippets, and rendered fragment content, including Markdown. They are not a place for secrets: original source remains available to editors and may be exposed by APIs that include raw content.
|
|
180
|
+
|
|
181
|
+
### Image resizing
|
|
182
|
+
|
|
183
|
+
Resize images from the media library or page attachments without changing the originals:
|
|
184
|
+
|
|
185
|
+
```liquid
|
|
186
|
+
{{ cms:image "Homepage banner", resize_to_limit: "1600x600" }}
|
|
187
|
+
{{ cms:file card_photo, as: image, resize_to_fill: "600x400" }}
|
|
188
|
+
{{ cms:image "Product photo", resize_and_pad: "500x500" }}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
The same options work with `file`, `files`, `image`, `file_link`, and `page_file_link`:
|
|
192
|
+
|
|
193
|
+
| Option | Result |
|
|
194
|
+
| --- | --- |
|
|
195
|
+
| `resize_to_limit` | Fit within the dimensions; never enlarge. |
|
|
196
|
+
| `resize_to_fit` | Fit within the dimensions; allow enlargement. |
|
|
197
|
+
| `resize_to_fill` | Fill the exact dimensions with a centered crop. |
|
|
198
|
+
| `resize_and_pad` | Fit the whole image into the exact dimensions with padding. |
|
|
199
|
+
|
|
200
|
+
Specify one operation and positive dimensions as `"WIDTHxHEIGHT"`. Invalid values, conflicting operations, and dimensions above the configured limit raise an error. Padding uses the processor's defaults; its color/transparency can differ between processors and image formats. Sizing options are ignored for missing attachments and files that Active Storage cannot transform, such as PDFs and SVGs under its default configuration. Missing attachments render nothing; non-transformable files retain their original URLs.
|
|
201
|
+
|
|
202
|
+
The default maximum requested width or height is 4096 pixels. Change it in `config/initializers/comfy_middle_seat.rb`:
|
|
203
|
+
|
|
204
|
+
```ruby
|
|
205
|
+
ComfyMiddleSeat.configure do |config|
|
|
206
|
+
config.max_image_dimension = 6000
|
|
207
|
+
end
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
This limits requested tag dimensions, not upload sizes or all intermediate processing memory. Admin thumbnails use fixed sizes.
|
|
211
|
+
|
|
212
|
+
The CMS respects your application's Active Storage processor. Use ImageMagick with `:mini_magick`, or install libvips and add `ruby-vips` to your application's Gemfile, then configure:
|
|
213
|
+
|
|
214
|
+
```ruby
|
|
215
|
+
# config/application.rb, inside your application class
|
|
216
|
+
config.active_storage.variant_processor = :vips
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Restart after changing processors. No processor switch is required to use the new options; the gem retains its existing MiniMagick dependency for compatibility.
|
|
220
|
+
|
|
221
|
+
#### Existing image tags
|
|
222
|
+
|
|
223
|
+
- `resize: "400x300"` translates to `resize_to_fit`; `resize: "400x300>"` translates to `resize_to_limit`. Both work with either processor.
|
|
224
|
+
- Supported legacy combinations of `resize`, `gravity`, and `crop` retain their operation order under MiniMagick. Resize accepts `WxH` with an optional `>`, `<`, `!`, or `^` suffix; crop requires `WxH+X+Y` (signed offsets). Offset-free crops are rejected because ImageMagick treats them as tiling, not a single crop. Gravity accepts the standard compass positions and `center`.
|
|
225
|
+
- Other legacy geometry, including percentages and one-dimensional sizes, raises an actionable error. Replace it with an explicit two-dimensional operation. The size limit applies to legacy dimensions too.
|
|
226
|
+
- Complex legacy expressions are not silently approximated on Vips. Convert them before switching processors; do not mix legacy and modern options in a tag.
|
|
227
|
+
|
|
228
|
+
After upgrading, clear rendered page and translation caches once so image links use the new transformations:
|
|
229
|
+
|
|
230
|
+
```ruby
|
|
231
|
+
Comfy::Cms::Page.update_all(content_cache: nil)
|
|
232
|
+
Comfy::Cms::Translation.update_all(content_cache: nil)
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
No database migration or re-upload is needed. New variants are generated on demand and may increase storage use temporarily; originals and old variants are not deleted. Purge any external page/CDN HTML caches separately. Clear rendered caches again after changing the sizing policy. Active Storage's variant keys do not include the processor: switching processors reuses existing variants with the same options, even after clearing page caches. To regenerate those images with the new processor, invalidate the affected derived variants separately, preserving the original uploads, and clear any cached derivative files on your CDN.
|
|
236
|
+
|
|
117
237
|
### Publishing pages
|
|
118
238
|
|
|
119
239
|
A page is publicly visible only when it and all its ancestors are published. Unpublishing a parent hides its branch without changing descendants’ individual publication settings. Republishing the parent restores only descendants whose full ancestor chain is published; drafts stay unpublished. Translations must also satisfy their own publication settings.
|
|
@@ -130,7 +250,7 @@ Development mode continues to show unpublished pages for preview. Check public v
|
|
|
130
250
|
| Rails | CI tests 7.2, 8.0, and 8.1 |
|
|
131
251
|
| Node.js | 18 or newer for asset compilation |
|
|
132
252
|
| File storage | Active Storage |
|
|
133
|
-
| Image processing | ImageMagick |
|
|
253
|
+
| Image processing | ImageMagick or libvips (via Active Storage) |
|
|
134
254
|
|
|
135
255
|
Pagination is provided by [Kaminari](https://github.com/amatsuda/kaminari). The admin uses Bootstrap, CodeMirror, and Redactor.
|
|
136
256
|
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import CodeMirror from "codemirror";
|
|
2
|
+
import "codemirror/addon/hint/show-hint";
|
|
3
|
+
|
|
4
|
+
const tag = (shortcut, name, params = "", placeholder = null, aliases = []) => ({
|
|
5
|
+
shortcut,
|
|
6
|
+
name,
|
|
7
|
+
aliases: [shortcut, name, ...aliases],
|
|
8
|
+
text: `{{ cms:${name}${params ? ` ${params}` : ""} }}`,
|
|
9
|
+
placeholder,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const completions = [
|
|
13
|
+
tag("md", "markdown", "content", "content"),
|
|
14
|
+
tag("tx", "text", "field", "field"),
|
|
15
|
+
tag("ta", "textarea", "content", "content"),
|
|
16
|
+
tag("wys", "wysiwyg", "content", "content"),
|
|
17
|
+
tag("da", "date", "field", "field"),
|
|
18
|
+
tag("dt", "datetime", "field", "field"),
|
|
19
|
+
tag("num", "number", "field", "field"),
|
|
20
|
+
tag("cb", "checkbox", "field", "field"),
|
|
21
|
+
tag("fi", "file", "attachment, as: link", "attachment"),
|
|
22
|
+
tag("fs", "files", "attachments, as: link", "attachments"),
|
|
23
|
+
tag("im", "image", '"image label"', "image label"),
|
|
24
|
+
tag("au", "audio", '"path/to/audio"', "path/to/audio"),
|
|
25
|
+
tag("fl", "file_link", "file_id, as: link", "file_id"),
|
|
26
|
+
tag("pfl", "page_file_link", "attachment, as: link", "attachment"),
|
|
27
|
+
tag("sn", "snippet", "identifier", "identifier"),
|
|
28
|
+
tag("as", "asset", "layout_identifier, type: css, as: tag", "layout_identifier"),
|
|
29
|
+
tag("css", "asset", "layout_identifier, type: css, as: tag", "layout_identifier"),
|
|
30
|
+
tag("js", "asset", "layout_identifier, type: js, as: tag", "layout_identifier"),
|
|
31
|
+
tag("hlp", "helper", "method_name", "method_name"),
|
|
32
|
+
tag("par", "partial", '"path/to/partial"', "path/to/partial"),
|
|
33
|
+
tag("tpl", "template", '"path/to/template"', "path/to/template"),
|
|
34
|
+
tag("cr", "breadcrumbs"),
|
|
35
|
+
tag("ch", "children"),
|
|
36
|
+
tag("si", "siblings"),
|
|
37
|
+
{
|
|
38
|
+
...tag("com", "comment", "", "Comment here"),
|
|
39
|
+
text: "{{ cms:comment }}\nComment here\n{{ cms:endcomment }}",
|
|
40
|
+
},
|
|
41
|
+
tag("nt", "comment", '"Note"', "Note", ["note"]),
|
|
42
|
+
tag("ec", "endcomment"),
|
|
43
|
+
tag("end", "end"),
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
const byAlias = new Map();
|
|
47
|
+
for (const completion of completions) {
|
|
48
|
+
for (const alias of completion.aliases) {
|
|
49
|
+
// Shared full names use the primary entry: asset => ;as, comment => ;com.
|
|
50
|
+
if (!byAlias.has(alias)) byAlias.set(alias, completion);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const completionContext = (editor, requireSemicolon = false, start = null) => {
|
|
55
|
+
if (editor.getOption("readOnly") || editor.somethingSelected() ||
|
|
56
|
+
editor.listSelections().length !== 1) return null;
|
|
57
|
+
|
|
58
|
+
const to = editor.getCursor();
|
|
59
|
+
const line = editor.getLine(to.line);
|
|
60
|
+
if (/\w/.test(line.charAt(to.ch))) return null;
|
|
61
|
+
|
|
62
|
+
if (start && (start.line !== to.line || start.ch > to.ch)) return null;
|
|
63
|
+
// Keep the menu's original start so spaces in a fuzzy query are not lost.
|
|
64
|
+
const match = start
|
|
65
|
+
? /^(;{0,2}[\w ]*)$/.exec(line.slice(start.ch, to.ch))
|
|
66
|
+
: /(?:^|[\s>])(;;[\w ]*|;?\w*)$/.exec(line.slice(0, to.ch));
|
|
67
|
+
if (!match || (requireSemicolon && !match[1].startsWith(";"))) return null;
|
|
68
|
+
|
|
69
|
+
const from = start || CodeMirror.Pos(to.line, to.ch - match[1].length);
|
|
70
|
+
const before = editor.getRange(CodeMirror.Pos(0, 0), from);
|
|
71
|
+
if (before.lastIndexOf("{{") > before.lastIndexOf("}}")) return null;
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
from,
|
|
75
|
+
to,
|
|
76
|
+
selector: match[1].startsWith(";;"),
|
|
77
|
+
query: match[1].replace(/^;{1,2}/, "").toLowerCase().replace(/ /g, ""),
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const insertCompletion = (editor, completion, from, to) => {
|
|
82
|
+
const indent = editor.getLine(from.line).match(/^\s*/)[0];
|
|
83
|
+
const text = completion.text.replace(/\n/g, `\n${indent}`);
|
|
84
|
+
const start = editor.indexFromPos(from);
|
|
85
|
+
|
|
86
|
+
editor.operation(() => {
|
|
87
|
+
// A distinct history origin keeps the expansion separate from preceding typing.
|
|
88
|
+
editor.replaceRange(text, from, to, "cms-completion");
|
|
89
|
+
if (completion.placeholder) {
|
|
90
|
+
const offset = text.indexOf(completion.placeholder);
|
|
91
|
+
editor.setSelection(
|
|
92
|
+
editor.posFromIndex(start + offset),
|
|
93
|
+
editor.posFromIndex(start + offset + completion.placeholder.length)
|
|
94
|
+
);
|
|
95
|
+
} else {
|
|
96
|
+
editor.setCursor(editor.posFromIndex(start + text.length));
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const matchRank = (alias, query) => {
|
|
102
|
+
if (alias === query) return 0;
|
|
103
|
+
if (alias.startsWith(query)) return 1;
|
|
104
|
+
if (alias.includes(query)) return 2;
|
|
105
|
+
|
|
106
|
+
let position = 0;
|
|
107
|
+
for (const character of query) {
|
|
108
|
+
position = alias.indexOf(character, position);
|
|
109
|
+
if (position === -1) return Infinity;
|
|
110
|
+
position += 1;
|
|
111
|
+
}
|
|
112
|
+
return 3;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const displayNames = {
|
|
116
|
+
as: "asset (CSS)",
|
|
117
|
+
css: "asset (CSS)",
|
|
118
|
+
js: "asset (JavaScript)",
|
|
119
|
+
com: "comment (block)",
|
|
120
|
+
nt: "comment (note)",
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const hints = (editor, start) => {
|
|
124
|
+
const context = completionContext(editor, false, start);
|
|
125
|
+
if (!context) return null;
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
from: context.from,
|
|
129
|
+
to: context.to,
|
|
130
|
+
list: completions
|
|
131
|
+
.map((completion) => ({
|
|
132
|
+
completion,
|
|
133
|
+
rank: Math.min(...completion.aliases.map((alias) => matchRank(alias, context.query))),
|
|
134
|
+
}))
|
|
135
|
+
.filter(({ rank }) => Number.isFinite(rank))
|
|
136
|
+
.sort((a, b) => a.rank - b.rank)
|
|
137
|
+
.map(({ completion }) => ({
|
|
138
|
+
text: completion.text,
|
|
139
|
+
displayText: `;${completion.shortcut} ${displayNames[completion.shortcut] || completion.name}`,
|
|
140
|
+
hint(cm, data) {
|
|
141
|
+
insertCompletion(cm, completion, data.from, data.to);
|
|
142
|
+
},
|
|
143
|
+
})),
|
|
144
|
+
};
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const showCompletions = (editor, context) => {
|
|
148
|
+
editor.showHint({
|
|
149
|
+
hint: (cm) => hints(cm, context.from),
|
|
150
|
+
completeSingle: false,
|
|
151
|
+
closeCharacters: /[\t\r\n()[\]{}:>,]/,
|
|
152
|
+
});
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
export const cmsTagCompletionKeys = (mode) => {
|
|
156
|
+
if (!["text/html", "htmlmixed", "text/x-markdown", "markdown"].includes(mode)) return {};
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
Tab(editor) {
|
|
160
|
+
const context = completionContext(editor, true);
|
|
161
|
+
if (context?.selector) {
|
|
162
|
+
showCompletions(editor, context);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const completion = context && byAlias.get(context.query);
|
|
166
|
+
if (!completion) return CodeMirror.Pass;
|
|
167
|
+
|
|
168
|
+
insertCompletion(editor, completion, context.from, context.to);
|
|
169
|
+
},
|
|
170
|
+
"Ctrl-Space"(editor) {
|
|
171
|
+
const context = completionContext(editor);
|
|
172
|
+
if (!context) return CodeMirror.Pass;
|
|
173
|
+
|
|
174
|
+
showCompletions(editor, context);
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import jQuery from "jquery";
|
|
2
2
|
import CodeMirror from "codemirror";
|
|
3
|
+
import { cmsTagCompletionKeys } from "./cms_tag_completions";
|
|
3
4
|
import "codemirror/mode/markdown/markdown";
|
|
4
5
|
import "codemirror/mode/htmlmixed/htmlmixed";
|
|
5
6
|
|
|
@@ -12,6 +13,7 @@ import "codemirror/mode/htmlmixed/htmlmixed";
|
|
|
12
13
|
)) {
|
|
13
14
|
const codemirror = CodeMirror.fromTextArea(textarea, {
|
|
14
15
|
mode: textarea.dataset.cmsCmMode,
|
|
16
|
+
extraKeys: cmsTagCompletionKeys(textarea.dataset.cmsCmMode),
|
|
15
17
|
tabSize: 2,
|
|
16
18
|
lineWrapping: true,
|
|
17
19
|
autoCloseTags: true,
|
|
@@ -52,6 +54,7 @@ import "codemirror/mode/htmlmixed/htmlmixed";
|
|
|
52
54
|
},
|
|
53
55
|
dispose() {
|
|
54
56
|
for (const codemirror of codeMirrorInstances) {
|
|
57
|
+
codemirror.closeHint();
|
|
55
58
|
codemirror.toTextArea();
|
|
56
59
|
}
|
|
57
60
|
codeMirrorInstances.length = 0;
|
|
@@ -12,6 +12,17 @@ body#comfy
|
|
|
12
12
|
.CodeMirror-scroll
|
|
13
13
|
min-height: 160px
|
|
14
14
|
|
|
15
|
+
.CodeMirror-hints
|
|
16
|
+
z-index: 1060
|
|
17
|
+
max-width: calc(100vw - 1rem)
|
|
18
|
+
border-color: $gray-400
|
|
19
|
+
background-color: $white
|
|
20
|
+
.CodeMirror-hint
|
|
21
|
+
color: $body-color
|
|
22
|
+
.CodeMirror-hint-active
|
|
23
|
+
background-color: $primary
|
|
24
|
+
color: $white
|
|
25
|
+
|
|
15
26
|
.CodeMirror-focused
|
|
16
27
|
border-color: rgba(82, 168, 236, 0.8)
|
|
17
28
|
outline: 0
|
|
@@ -23,7 +23,13 @@ class Comfy::Admin::Cms::FilesController < Comfy::Admin::Cms::BaseController
|
|
|
23
23
|
case params[:type]
|
|
24
24
|
when 'image'
|
|
25
25
|
file_scope.with_images.collect do |file|
|
|
26
|
-
|
|
26
|
+
attachment = file.attachment
|
|
27
|
+
thumbnail = if attachment.variable?
|
|
28
|
+
attachment.variant(Comfy::Cms::File::VARIANT_SIZE[:redactor])
|
|
29
|
+
else
|
|
30
|
+
attachment
|
|
31
|
+
end
|
|
32
|
+
{ thumb: url_for(thumbnail),
|
|
27
33
|
image: url_for(file.attachment),
|
|
28
34
|
title: file.label }
|
|
29
35
|
end
|
|
@@ -6,9 +6,9 @@ class Comfy::Cms::File < ActiveRecord::Base
|
|
|
6
6
|
include Comfy::Cms::WithCategories
|
|
7
7
|
|
|
8
8
|
VARIANT_SIZE = {
|
|
9
|
-
redactor: {
|
|
10
|
-
thumb: {
|
|
11
|
-
icon: {
|
|
9
|
+
redactor: { resize_to_fill: [100, 75] },
|
|
10
|
+
thumb: { resize_to_fill: [200, 150] },
|
|
11
|
+
icon: { resize_to_fill: [28, 28] }
|
|
12
12
|
}.freeze
|
|
13
13
|
|
|
14
14
|
# temporary place to store attachment
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
- attachments.each do |attachment|
|
|
3
3
|
:ruby
|
|
4
4
|
thumb_url = if attachment.variable?
|
|
5
|
-
url_for(attachment.variant(
|
|
5
|
+
url_for(attachment.variant(Comfy::Cms::File::VARIANT_SIZE[:thumb]))
|
|
6
6
|
end
|
|
7
7
|
filename = attachment.filename.to_s
|
|
8
8
|
truncated_filename = truncate(filename, length: 40, omission: "...#{filename.last(10)}")
|
|
@@ -46,6 +46,10 @@ ComfyMiddleSeat.configure do |config|
|
|
|
46
46
|
# object you want to keep. Set it to 0 if you wish to turn this feature off.
|
|
47
47
|
# config.revisions_limit = 25
|
|
48
48
|
|
|
49
|
+
# Maximum requested width or height for image resizing in CMS tags.
|
|
50
|
+
# Values above this limit raise an error rather than being silently reduced.
|
|
51
|
+
# config.max_image_dimension = 4096
|
|
52
|
+
|
|
49
53
|
# Locale definitions. If you want to define your own locale merge
|
|
50
54
|
# {:locale => 'Locale Title'} with this.
|
|
51
55
|
# config.locales = {:en => 'English', :es => 'Español'}
|
|
@@ -84,6 +84,9 @@ class ComfyMiddleSeat::Configuration
|
|
|
84
84
|
# For example: include fragments into json data with `config.page_to_json_options = { include: [:fragments] }`
|
|
85
85
|
attr_accessor :page_to_json_options
|
|
86
86
|
|
|
87
|
+
# Maximum requested width or height for image transformations in CMS tags.
|
|
88
|
+
attr_accessor :max_image_dimension
|
|
89
|
+
|
|
87
90
|
# Configuration defaults
|
|
88
91
|
def initialize
|
|
89
92
|
@cms_title = 'ComfyMiddleSeat CMS Engine'
|
|
@@ -99,6 +102,7 @@ class ComfyMiddleSeat::Configuration
|
|
|
99
102
|
@enable_seeds = false
|
|
100
103
|
@seeds_path = File.expand_path('db/cms_seeds', Rails.root)
|
|
101
104
|
@revisions_limit = 25
|
|
105
|
+
@max_image_dimension = 4096
|
|
102
106
|
@locales = {
|
|
103
107
|
'ar' => 'عربي',
|
|
104
108
|
'ca' => 'Català',
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Restrict CMS-authored transformations to bounded image operations. Never pass
|
|
4
|
+
# arbitrary tag options through to an image processor.
|
|
5
|
+
class ComfyMiddleSeat::Content::ImageTransformations
|
|
6
|
+
OPERATIONS = %w[resize_to_limit resize_to_fit resize_to_fill resize_and_pad].freeze
|
|
7
|
+
LEGACY_OPTIONS = %w[resize gravity crop].freeze
|
|
8
|
+
OPTIONS = (OPERATIONS + LEGACY_OPTIONS).freeze
|
|
9
|
+
GRAVITIES = %w[northwest north northeast west center east southwest south southeast].freeze
|
|
10
|
+
|
|
11
|
+
def self.normalize(options, processor: ActiveStorage.variant_processor)
|
|
12
|
+
new(options, processor).normalize
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize(options, processor)
|
|
16
|
+
@options = options.stringify_keys
|
|
17
|
+
@processor = processor
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def normalize
|
|
21
|
+
return {} if @options.empty?
|
|
22
|
+
|
|
23
|
+
unknown = @options.keys - OPTIONS
|
|
24
|
+
error!("Unknown image options: #{unknown.join(', ')}") if unknown.any?
|
|
25
|
+
modern = @options.keys & OPERATIONS
|
|
26
|
+
if modern.size > 1 || (modern.any? && @options.keys.intersect?(LEGACY_OPTIONS))
|
|
27
|
+
error!('Use one resize operation; do not mix modern and legacy image options')
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
if modern.any?
|
|
31
|
+
operation = modern.first
|
|
32
|
+
{ operation.to_sym => dimensions(@options[operation], operation) }
|
|
33
|
+
else
|
|
34
|
+
legacy_transformations
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def dimensions(value, operation)
|
|
41
|
+
match = value.is_a?(String) && %r{\A\s*(\d+)\s*x\s*(\d+)\s*\z}.match(value)
|
|
42
|
+
error!("#{operation} must be dimensions such as \"400x300\"") unless match
|
|
43
|
+
validate_dimensions(match[1].to_i, match[2].to_i)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def validate_dimensions(width, height)
|
|
47
|
+
limit = ComfyMiddleSeat.config.max_image_dimension
|
|
48
|
+
unless limit.is_a?(Integer) && limit.positive?
|
|
49
|
+
error!('max_image_dimension must be a positive integer')
|
|
50
|
+
end
|
|
51
|
+
unless width.positive? && height.positive? && width <= limit && height <= limit
|
|
52
|
+
error!("Image dimensions must be between 1 and #{limit} pixels; adjust max_image_dimension if needed")
|
|
53
|
+
end
|
|
54
|
+
[width, height]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def legacy_transformations
|
|
58
|
+
if @options.key?('resize')
|
|
59
|
+
size, modifier = legacy_resize
|
|
60
|
+
if @options.keys == ['resize'] && ['', '>'].include?(modifier)
|
|
61
|
+
operation = modifier == '>' ? :resize_to_limit : :resize_to_fit
|
|
62
|
+
return { operation => size }
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
validate_legacy_crop if @options.key?('crop')
|
|
67
|
+
|
|
68
|
+
if @options.key?('gravity') && !GRAVITIES.include?(@options['gravity'])
|
|
69
|
+
error!("Unsupported gravity; choose #{GRAVITIES.join(', ')}")
|
|
70
|
+
end
|
|
71
|
+
unless @options.key?('resize') || @options.key?('crop')
|
|
72
|
+
error!('Legacy gravity requires resize or crop')
|
|
73
|
+
end
|
|
74
|
+
unless @processor == :mini_magick
|
|
75
|
+
error!('Legacy crop, gravity, and resize modifiers require MiniMagick; use a modern resize option with Vips')
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# image_processing combines these operations itself. The order matters for
|
|
79
|
+
# existing resize/gravity/crop expressions; combine_options is unsupported.
|
|
80
|
+
@options.slice(*LEGACY_OPTIONS).symbolize_keys
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def legacy_resize
|
|
84
|
+
value = @options['resize']
|
|
85
|
+
match = value.is_a?(String) && %r{\A(\d+)x(\d+)([!<>^]?)\z}.match(value)
|
|
86
|
+
error!('Legacy resize must be WxH with an optional >, <, !, or ^ suffix; use a modern resize option') unless match
|
|
87
|
+
[validate_dimensions(match[1].to_i, match[2].to_i), match[3]]
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def validate_legacy_crop
|
|
91
|
+
value = @options['crop']
|
|
92
|
+
match = value.is_a?(String) && %r{\A(\d+)x(\d+)[+-]\d+[+-]\d+\z}.match(value)
|
|
93
|
+
error!('Legacy crop requires explicit offsets: WxH+X+Y; use resize_to_fill for a centered crop') unless match
|
|
94
|
+
validate_dimensions(match[1].to_i, match[2].to_i)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def error!(message)
|
|
98
|
+
raise ComfyMiddleSeat::Content::Tag::Error, message
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -22,6 +22,9 @@ class ComfyMiddleSeat::Content::Renderer
|
|
|
22
22
|
|
|
23
23
|
# tags are in this format: {{ cms:tag_class params }}
|
|
24
24
|
TAG_REGEX = %r{\{\{\s*?cms:(?<class>\w+)(?<params>.*?)\}\}}
|
|
25
|
+
COMMENT_START_REGEX = %r{\{\{\s*cms:(?<comment>comment|endcomment)(?=\s|\}\}|\z)}
|
|
26
|
+
TOKEN_REGEX = %r{#{COMMENT_START_REGEX}|#{TAG_REGEX}}
|
|
27
|
+
COMMENT_NOTE_REGEX = %r{(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|“(?:\\.|[^”\\])*”|‘(?:\\.|[^’\\])*’)\s*\}\}}m
|
|
25
28
|
|
|
26
29
|
class << self
|
|
27
30
|
# @return [Hash<String, Class<ComfyMiddleSeat::Content::Tag>>]
|
|
@@ -73,23 +76,35 @@ class ComfyMiddleSeat::Content::Renderer
|
|
|
73
76
|
|
|
74
77
|
# Splitting text with tags into tokens we can process down the line
|
|
75
78
|
# @return [Array<String, {Symbol => String}>]
|
|
76
|
-
def tokenize(string)
|
|
79
|
+
def tokenize(string, preserve_whitespace: false)
|
|
77
80
|
tokens = []
|
|
78
81
|
ss = StringScanner.new(string.to_s)
|
|
79
|
-
while (string = ss.scan_until(
|
|
82
|
+
while (string = ss.scan_until(TOKEN_REGEX))
|
|
80
83
|
text = string.sub(ss[0], '')
|
|
81
84
|
tokens << text unless text.empty?
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
case ss[:comment]
|
|
86
|
+
when 'comment'
|
|
87
|
+
skip_comment(ss)
|
|
88
|
+
when 'endcomment'
|
|
89
|
+
raise SyntaxError, 'Unexpected {{cms:endcomment}} without {{cms:comment}}'
|
|
90
|
+
else
|
|
91
|
+
tokens << {
|
|
92
|
+
tag_class: ss[:class],
|
|
93
|
+
tag_params: ss[:params].strip,
|
|
94
|
+
source: ss[0]
|
|
95
|
+
}
|
|
96
|
+
end
|
|
87
97
|
end
|
|
88
98
|
text = ss.rest
|
|
89
|
-
tokens << text if text.present?
|
|
99
|
+
tokens << text if text.present? || (preserve_whitespace && !text.empty?)
|
|
90
100
|
tokens
|
|
91
101
|
end
|
|
92
102
|
|
|
103
|
+
# Remove comments before formatters such as Markdown can transform their contents.
|
|
104
|
+
def strip_comments(string)
|
|
105
|
+
tokenize(string, preserve_whitespace: true).map { |token| token.is_a?(Hash) ? token[:source] : token }.join
|
|
106
|
+
end
|
|
107
|
+
|
|
93
108
|
# Constructing node tree for content. It's a list of strings and tags with
|
|
94
109
|
# their own `nodes` method that has array of strings and tags with their own
|
|
95
110
|
# `nodes` method that... you get the idea.
|
|
@@ -143,4 +158,34 @@ class ComfyMiddleSeat::Content::Renderer
|
|
|
143
158
|
|
|
144
159
|
nodes.flatten
|
|
145
160
|
end
|
|
161
|
+
|
|
162
|
+
private
|
|
163
|
+
|
|
164
|
+
# Comments are lexical: their contents must never become tag instances or form fields.
|
|
165
|
+
def skip_comment(scanner)
|
|
166
|
+
return unless comment_block?(scanner)
|
|
167
|
+
|
|
168
|
+
depth = 1
|
|
169
|
+
while scanner.scan_until(COMMENT_START_REGEX)
|
|
170
|
+
if scanner[:comment] == 'comment'
|
|
171
|
+
depth += 1 if comment_block?(scanner)
|
|
172
|
+
else
|
|
173
|
+
unless scanner.skip(%r{\s*\}\}})
|
|
174
|
+
raise SyntaxError, 'Invalid CMS comment closing tag: use {{cms:endcomment}} without parameters'
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
depth -= 1
|
|
178
|
+
return if depth.zero?
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
raise SyntaxError, 'Unclosed CMS comment: expected {{cms:endcomment}}'
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def comment_block?(scanner)
|
|
185
|
+
scanner.skip(%r{\s*})
|
|
186
|
+
return true if scanner.skip(%r{\}\}})
|
|
187
|
+
return false if scanner.skip(COMMENT_NOTE_REGEX)
|
|
188
|
+
|
|
189
|
+
raise SyntaxError, 'Invalid CMS comment: use {{cms:comment "note"}} or {{cms:comment}}...{{cms:endcomment}}'
|
|
190
|
+
end
|
|
146
191
|
end
|
|
@@ -8,9 +8,8 @@ require_relative 'mixins/file_content'
|
|
|
8
8
|
#
|
|
9
9
|
# `as` - url (default) | link | image - render out link or image tag
|
|
10
10
|
# `label` - attach label attribute to link or image tag
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
# `crop` - imagemagic option. For example: "100x50+0+0"
|
|
11
|
+
# Image sizing: resize_to_limit, resize_to_fit, resize_to_fill, resize_and_pad ("100x50").
|
|
12
|
+
# Legacy resize/gravity/crop expressions remain supported for MiniMagick.
|
|
14
13
|
# `class` - any html classes that you want on the result link or image tag. For example "class1 class2"
|
|
15
14
|
#
|
|
16
15
|
class ComfyMiddleSeat::Content::Tags::File < ComfyMiddleSeat::Content::Tags::Fragment
|
|
@@ -28,7 +27,7 @@ class ComfyMiddleSeat::Content::Tags::File < ComfyMiddleSeat::Content::Tags::Fra
|
|
|
28
27
|
@as = options['as'] || 'url'
|
|
29
28
|
@label = options['label']
|
|
30
29
|
@class = options['class']
|
|
31
|
-
@variant_attrs = options.slice(
|
|
30
|
+
@variant_attrs = options.slice(*ComfyMiddleSeat::Content::ImageTransformations::OPTIONS)
|
|
32
31
|
end
|
|
33
32
|
|
|
34
33
|
def form_field(object_name, view, index)
|
|
@@ -9,12 +9,8 @@ require_relative 'mixins/file_content'
|
|
|
9
9
|
# `as` - url (default) | link | image - how file gets rendered out
|
|
10
10
|
# `class` - any html classes that you want on the result link or image tag. For example "class1 class2"
|
|
11
11
|
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
# `label` - attach label attribute to link or image tag
|
|
15
|
-
# `resize` - imagemagick option. For example: "100x50>"
|
|
16
|
-
# `gravity` - imagemagick option. For example: "center"
|
|
17
|
-
# `crop` - imagemagick option. For example: "100x50+0+0"
|
|
12
|
+
# Image sizing: resize_to_limit, resize_to_fit, resize_to_fill, resize_and_pad ("100x50").
|
|
13
|
+
# Legacy resize/gravity/crop expressions remain supported for MiniMagick.
|
|
18
14
|
#
|
|
19
15
|
class ComfyMiddleSeat::Content::Tags::FileLink < ComfyMiddleSeat::Content::Tag
|
|
20
16
|
include ComfyMiddleSeat::Content::Tags::Mixins::FileContent
|
|
@@ -28,7 +24,7 @@ class ComfyMiddleSeat::Content::Tags::FileLink < ComfyMiddleSeat::Content::Tag
|
|
|
28
24
|
@identifier = params[0]
|
|
29
25
|
@as = options['as'] || 'url'
|
|
30
26
|
@class = options['class']
|
|
31
|
-
@variant_attrs = options.slice(
|
|
27
|
+
@variant_attrs = options.slice(*ComfyMiddleSeat::Content::ImageTransformations::OPTIONS)
|
|
32
28
|
|
|
33
29
|
return if @identifier.present?
|
|
34
30
|
|
|
@@ -7,12 +7,8 @@ require_relative 'mixins/file_content'
|
|
|
7
7
|
#
|
|
8
8
|
# `class` - any html classes that you want on the image tag. For example "class1 class2"
|
|
9
9
|
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
# `label` - attach label attribute to link or image tag
|
|
13
|
-
# `resize` - imagemagick option. For example: "100x50>"
|
|
14
|
-
# `gravity` - imagemagick option. For example: "center"
|
|
15
|
-
# `crop` - imagemagick option. For example: "100x50+0+0"
|
|
10
|
+
# Image sizing: resize_to_limit, resize_to_fit, resize_to_fill, resize_and_pad ("100x50").
|
|
11
|
+
# Legacy resize/gravity/crop expressions remain supported for MiniMagick.
|
|
16
12
|
|
|
17
13
|
class ComfyMiddleSeat::Content::Tags::Image < ComfyMiddleSeat::Content::Tag
|
|
18
14
|
include ComfyMiddleSeat::Content::Tags::Mixins::FileContent
|
|
@@ -26,7 +22,7 @@ class ComfyMiddleSeat::Content::Tags::Image < ComfyMiddleSeat::Content::Tag
|
|
|
26
22
|
@identifier = params[0]
|
|
27
23
|
@as = 'image'
|
|
28
24
|
@class = options['class']
|
|
29
|
-
@variant_attrs = options.slice(
|
|
25
|
+
@variant_attrs = options.slice(*ComfyMiddleSeat::Content::ImageTransformations::OPTIONS)
|
|
30
26
|
|
|
31
27
|
return if @identifier.present?
|
|
32
28
|
|
|
@@ -5,7 +5,10 @@
|
|
|
5
5
|
#
|
|
6
6
|
class ComfyMiddleSeat::Content::Tags::Markdown < ComfyMiddleSeat::Content::Tags::Fragment
|
|
7
7
|
def render
|
|
8
|
-
|
|
8
|
+
return '' unless renderable
|
|
9
|
+
|
|
10
|
+
source = ComfyMiddleSeat::Content::Renderer.new(context).strip_comments(content)
|
|
11
|
+
Kramdown::Document.new(source).to_html
|
|
9
12
|
end
|
|
10
13
|
|
|
11
14
|
def form_field(object_name, view, index)
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative '../../image_transformations'
|
|
4
|
+
|
|
3
5
|
# A mixin for tags that returns the file as their content.
|
|
4
6
|
module ComfyMiddleSeat::Content::Tags::Mixins
|
|
5
7
|
module FileContent
|
|
6
8
|
# @param [ActiveStorage::Blob] file
|
|
7
9
|
# @param ["link", "image", "url"] as
|
|
8
|
-
# @param [{String => String}] variant_attrs
|
|
10
|
+
# @param [{String => String}] variant_attrs Modern or legacy image options
|
|
9
11
|
# @param [String] label alt text for `as: "image"`, link text for `as: "link"`
|
|
10
12
|
# @return [String]
|
|
11
13
|
def content(file: self.file, as: self.as, variant_attrs: self.variant_attrs, label: self.label)
|
|
@@ -14,8 +16,9 @@ module ComfyMiddleSeat::Content::Tags::Mixins
|
|
|
14
16
|
url_helpers = Rails.application.routes.url_helpers
|
|
15
17
|
|
|
16
18
|
attachment_url =
|
|
17
|
-
if variant_attrs.present? && file.
|
|
18
|
-
|
|
19
|
+
if variant_attrs.present? && file.variable?
|
|
20
|
+
transformations = ComfyMiddleSeat::Content::ImageTransformations.normalize(variant_attrs)
|
|
21
|
+
variant = file.variant(transformations)
|
|
19
22
|
url_helpers.rails_representation_path(variant, only_path: true)
|
|
20
23
|
else
|
|
21
24
|
url_helpers.rails_blob_path(file, only_path: true)
|
|
@@ -18,12 +18,8 @@ require_relative 'mixins/file_content'
|
|
|
18
18
|
# `as` - url (default) | link | image - how file gets rendered out
|
|
19
19
|
# `class` - any html classes that you want on the result link or image tag. For example "class1 class2"
|
|
20
20
|
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
# `label` - attach label attribute to link or image tag
|
|
24
|
-
# `resize` - imagemagick option. For example: "100x50>"
|
|
25
|
-
# `gravity` - imagemagick option. For example: "center"
|
|
26
|
-
# `crop` - imagemagick option. For example: "100x50+0+0"
|
|
21
|
+
# Image sizing: resize_to_limit, resize_to_fit, resize_to_fill, resize_and_pad ("100x50").
|
|
22
|
+
# Legacy resize/gravity/crop expressions remain supported for MiniMagick.
|
|
27
23
|
#
|
|
28
24
|
class ComfyMiddleSeat::Content::Tags::PageFileLink < ComfyMiddleSeat::Content::Tag
|
|
29
25
|
include ComfyMiddleSeat::Content::Tags::Mixins::FileContent
|
|
@@ -48,7 +44,7 @@ class ComfyMiddleSeat::Content::Tags::PageFileLink < ComfyMiddleSeat::Content::T
|
|
|
48
44
|
@identifier = params[0]
|
|
49
45
|
@as = options['as'] || 'url'
|
|
50
46
|
@class = options['class']
|
|
51
|
-
@variant_attrs = options.slice(
|
|
47
|
+
@variant_attrs = options.slice(*ComfyMiddleSeat::Content::ImageTransformations::OPTIONS)
|
|
52
48
|
@filename = options['filename']
|
|
53
49
|
|
|
54
50
|
return if @identifier.present?
|
|
@@ -18,7 +18,8 @@ class ComfyMiddleSeat::Content::Tags::Snippet < ComfyMiddleSeat::Content::Tag
|
|
|
18
18
|
|
|
19
19
|
def content
|
|
20
20
|
if snippet.markdown
|
|
21
|
-
|
|
21
|
+
source = ComfyMiddleSeat::Content::Renderer.new(context).strip_comments(snippet.content)
|
|
22
|
+
Kramdown::Document.new(source).to_html
|
|
22
23
|
else
|
|
23
24
|
snippet.content
|
|
24
25
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: comfy_middle_seat
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 4.
|
|
4
|
+
version: 4.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Oleg Khabarov
|
|
@@ -230,6 +230,7 @@ files:
|
|
|
230
230
|
- app/assets/javascripts/comfy/admin/cms/application.js
|
|
231
231
|
- app/assets/javascripts/comfy/admin/cms/base.js
|
|
232
232
|
- app/assets/javascripts/comfy/admin/cms/categories.js
|
|
233
|
+
- app/assets/javascripts/comfy/admin/cms/cms_tag_completions.js
|
|
233
234
|
- app/assets/javascripts/comfy/admin/cms/codemirror.js
|
|
234
235
|
- app/assets/javascripts/comfy/admin/cms/custom.js
|
|
235
236
|
- app/assets/javascripts/comfy/admin/cms/diff.js
|
|
@@ -573,6 +574,7 @@ files:
|
|
|
573
574
|
- lib/comfy_middle_seat/configuration.rb
|
|
574
575
|
- lib/comfy_middle_seat/content.rb
|
|
575
576
|
- lib/comfy_middle_seat/content/block.rb
|
|
577
|
+
- lib/comfy_middle_seat/content/image_transformations.rb
|
|
576
578
|
- lib/comfy_middle_seat/content/params_parser.rb
|
|
577
579
|
- lib/comfy_middle_seat/content/renderer.rb
|
|
578
580
|
- lib/comfy_middle_seat/content/tag.rb
|