panda-editor 0.6.0 → 0.8.2

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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -0
  3. data/app/javascript/panda/editor/application.js +8 -6
  4. data/app/javascript/panda/editor/controllers/index.js +5 -0
  5. data/app/javascript/panda/editor/editor_js_config.js +7 -5
  6. data/app/javascript/panda/editor/editor_js_initializer.js +10 -3
  7. data/app/javascript/panda/editor/plugins/embed.min.js +2 -0
  8. data/app/javascript/panda/editor/plugins/header.min.js +9 -0
  9. data/app/javascript/panda/editor/plugins/nested-list.min.js +2 -0
  10. data/app/javascript/panda/editor/plugins/paragraph.min.js +9 -0
  11. data/app/javascript/panda/editor/plugins/quote.min.js +2 -0
  12. data/app/javascript/panda/editor/plugins/simple-image.min.js +2 -0
  13. data/app/javascript/panda/editor/plugins/table.min.js +2 -0
  14. data/app/javascript/panda/editor/rich_text_editor.js +2 -3
  15. data/app/services/panda/editor/html_to_editor_js_converter.rb +68 -68
  16. data/app/stylesheets/editor.css +120 -0
  17. data/config/importmap.rb +22 -11
  18. data/docs/FOOTNOTES.md +96 -3
  19. data/lefthook.yml +16 -0
  20. data/lib/panda/editor/asset_loader.rb +27 -27
  21. data/lib/panda/editor/blocks/alert.rb +10 -10
  22. data/lib/panda/editor/blocks/base.rb +1 -1
  23. data/lib/panda/editor/blocks/header.rb +2 -2
  24. data/lib/panda/editor/blocks/image.rb +11 -11
  25. data/lib/panda/editor/blocks/list.rb +25 -6
  26. data/lib/panda/editor/blocks/paragraph.rb +41 -10
  27. data/lib/panda/editor/blocks/quote.rb +6 -6
  28. data/lib/panda/editor/blocks/table.rb +6 -6
  29. data/lib/panda/editor/content.rb +11 -8
  30. data/lib/panda/editor/engine.rb +29 -9
  31. data/lib/panda/editor/footnote_registry.rb +10 -5
  32. data/lib/panda/editor/html_to_editor_js_converter.rb +1 -1
  33. data/lib/panda/editor/renderer.rb +31 -31
  34. data/lib/panda/editor/version.rb +1 -1
  35. data/lib/panda/editor.rb +18 -16
  36. data/lib/tasks/assets.rake +27 -27
  37. data/mise.toml +2 -0
  38. data/panda-editor.gemspec +25 -24
  39. metadata +32 -3
@@ -0,0 +1,120 @@
1
+ /* Base content styles, where .codex-editor applies them to the Panda editor too */
2
+ @layer components {
3
+ .codex-editor__redactor .ce-block .ce-block__content {
4
+ @apply text-base font-normal font-sans text-dark leading-[1.6] space-y-[1.6rem];
5
+
6
+ h1.ce-header {
7
+ @apply text-3xl md:text-4xl font-semibold font-sans text-[#104071] leading-[1.2] max-w-[85ch];
8
+ }
9
+
10
+ h2.ce-header {
11
+ @apply text-2xl font-medium font-sans text-[#104071] leading-[1.3] mb-4 mt-8 max-w-[85ch];
12
+ }
13
+
14
+ h3.ce-header {
15
+ @apply text-xl font-normal font-sans text-[#104071] leading-[1.3] mb-4 mt-6 max-w-[85ch];
16
+ }
17
+
18
+ p,
19
+ li {
20
+ @apply leading-[1.6] tracking-wide max-w-[85ch];
21
+
22
+ a {
23
+ @apply text-[#1A9597] underline underline-offset-2 hover:text-[#158486] focus:outline-2 focus:outline-offset-2 focus:outline-[#1A9597];
24
+ }
25
+
26
+ strong,
27
+ b {
28
+ @apply font-semibold;
29
+ }
30
+ }
31
+
32
+ p {
33
+ @apply mb-4;
34
+ }
35
+
36
+ .cdx-quote {
37
+ @apply bg-[#eef0f3] border-l-inactive border-l-8 p-6 mb-4;
38
+
39
+ .cdx-quote__caption {
40
+ @apply block ml-6 mt-2 text-sm text-dark;
41
+ }
42
+
43
+ .cdx-quote__text {
44
+ quotes: "\201C" "\201D" "\2018" "\2019";
45
+ @apply pl-6;
46
+
47
+ &:before {
48
+ @apply -ml-8 mr-2 text-dark text-6xl leading-4 align-text-bottom font-serif;
49
+ content: open-quote;
50
+ }
51
+
52
+ p {
53
+ @apply inline italic text-lg;
54
+ }
55
+ }
56
+ }
57
+
58
+ .cdx-list {
59
+ @apply mb-4 pl-6;
60
+
61
+ &--ordered {
62
+ @apply list-decimal;
63
+ }
64
+
65
+ &--unordered {
66
+ @apply list-disc;
67
+ }
68
+
69
+ .cdx-list {
70
+ @apply mt-2 mb-0;
71
+ }
72
+
73
+ .cdx-list__item {
74
+ @apply mb-2 pl-2;
75
+ }
76
+ }
77
+
78
+ .cdx-nested-list {
79
+ @apply mb-4 pl-6;
80
+
81
+ &--ordered {
82
+ @apply list-decimal;
83
+ }
84
+
85
+ &--unordered {
86
+ @apply list-disc;
87
+ }
88
+
89
+ .cdx-nested-list {
90
+ @apply mt-2 mb-0;
91
+ }
92
+
93
+ .cdx-nested-list__item {
94
+ @apply mb-2 pl-2;
95
+ }
96
+ }
97
+
98
+ .cdx-table {
99
+ @apply w-full border-collapse border-2 border-dark my-6;
100
+
101
+ &__head {
102
+ @apply font-semibold border-dark border-r-2 p-3 bg-light;
103
+ }
104
+
105
+ &__row {
106
+ @apply border-dark border-b-2;
107
+ }
108
+
109
+ &__cell {
110
+ @apply border-dark border-r-2 p-3;
111
+ }
112
+ }
113
+
114
+ .cdx-embed {
115
+ iframe {
116
+ @apply w-full border-none;
117
+ }
118
+ }
119
+ }
120
+ }
data/config/importmap.rb CHANGED
@@ -2,14 +2,25 @@
2
2
 
3
3
  # Pin npm packages by running ./bin/importmap
4
4
 
5
- pin_all_from 'app/javascript/panda/editor', under: 'panda/editor'
6
-
7
- # EditorJS Core and plugins (from CDN)
8
- pin '@editorjs/editorjs', to: 'https://cdn.jsdelivr.net/npm/@editorjs/editorjs@2.28.2/+esm'
9
- pin '@editorjs/paragraph', to: 'https://cdn.jsdelivr.net/npm/@editorjs/paragraph@2.11.3/+esm'
10
- pin '@editorjs/header', to: 'https://cdn.jsdelivr.net/npm/@editorjs/header@2.8.1/+esm'
11
- pin '@editorjs/nested-list', to: 'https://cdn.jsdelivr.net/npm/@editorjs/nested-list@1.4.2/+esm'
12
- pin '@editorjs/quote', to: 'https://cdn.jsdelivr.net/npm/@editorjs/quote@2.6.0/+esm'
13
- pin '@editorjs/simple-image', to: 'https://cdn.jsdelivr.net/npm/@editorjs/simple-image@1.6.0/+esm'
14
- pin '@editorjs/table', to: 'https://cdn.jsdelivr.net/npm/@editorjs/table@2.3.0/+esm'
15
- pin '@editorjs/embed', to: 'https://cdn.jsdelivr.net/npm/@editorjs/embed@2.7.0/+esm'
5
+ # Entry points (required by panda_core_javascript helper)
6
+ # These must use absolute paths to work with JavaScriptMiddleware
7
+ pin "panda/editor/application", to: "/panda/editor/application.js", preload: true
8
+ pin "panda/editor/controllers/index", to: "/panda/editor/controllers/index.js"
9
+
10
+ # Individual modules used by panda-cms controllers
11
+ pin "panda/editor/editor_js_config", to: "/panda/editor/editor_js_config.js"
12
+ pin "panda/editor/editor_js_initializer", to: "/panda/editor/editor_js_initializer.js"
13
+ pin "panda/editor/resource_loader", to: "/panda/editor/resource_loader.js"
14
+ pin "panda/editor/plain_text_editor", to: "/panda/editor/plain_text_editor.js"
15
+ pin "panda/editor/css_extractor", to: "/panda/editor/css_extractor.js"
16
+ pin "panda/editor/rich_text_editor", to: "/panda/editor/rich_text_editor.js"
17
+
18
+ # EditorJS Core and plugins (from esm.sh - better ES module handling than jsdelivr)
19
+ pin "@editorjs/editorjs", to: "https://esm.sh/@editorjs/editorjs@2.28.2"
20
+ pin "@editorjs/paragraph", to: "https://esm.sh/@editorjs/paragraph@2.11.3"
21
+ pin "@editorjs/header", to: "https://esm.sh/@editorjs/header@2.8.1"
22
+ pin "@editorjs/nested-list", to: "https://esm.sh/@editorjs/nested-list@1.4.2"
23
+ pin "@editorjs/quote", to: "https://esm.sh/@editorjs/quote@2.6.0"
24
+ pin "@editorjs/simple-image", to: "https://esm.sh/@editorjs/simple-image@1.6.0"
25
+ pin "@editorjs/table", to: "https://esm.sh/@editorjs/table@2.3.0"
26
+ pin "@editorjs/embed", to: "https://esm.sh/@editorjs/embed@2.7.0"
data/docs/FOOTNOTES.md CHANGED
@@ -29,6 +29,7 @@ The footnote system consists of three main components:
29
29
  - 📍 **Position-based injection**: Place footnote markers at any character position
30
30
  - 🎨 **Collapsible UI**: Clean, accessible sources section
31
31
  - 🔗 **Bidirectional links**: Navigate between citations and sources
32
+ - 💬 **Hover tooltips**: Preview footnote content without scrolling to sources
32
33
 
33
34
  ## How It Works
34
35
 
@@ -181,10 +182,17 @@ Both paragraphs will reference the same footnote number, and the source will app
181
182
 
182
183
  ### Inline Markers
183
184
 
185
+ Footnote markers include native browser tooltips and data attributes for custom tooltip implementations:
186
+
184
187
  ```html
185
- <p>Climate change has accelerated significantly since 1980<sup id="fnref:1"><a href="#fn:1" class="footnote">1</a></sup></p>
188
+ <p>Climate change has accelerated significantly since 1980<sup id="fnref:1" class="footnote-ref" data-footnote-content="IPCC. (2023). Climate Change 2023: Synthesis Report." title="IPCC. (2023). Climate Change 2023: Synthesis Report."><a href="#fn:1" class="footnote">1</a></sup></p>
186
189
  ```
187
190
 
191
+ **Tooltip Attributes:**
192
+ - `class="footnote-ref"` - Identifies footnote markers for styling
193
+ - `data-footnote-content` - Contains processed footnote content (with markdown/HTML if enabled) for custom tooltips
194
+ - `title` - Contains plain text version for native browser tooltips on hover
195
+
188
196
  ### Sources Section
189
197
 
190
198
  ```html
@@ -442,6 +450,91 @@ def generate_cached_content
442
450
  end
443
451
  ```
444
452
 
453
+ ## Tooltips
454
+
455
+ Footnote markers automatically include tooltip support through two mechanisms:
456
+
457
+ ### Native Browser Tooltips
458
+
459
+ The `title` attribute provides instant, zero-JavaScript tooltips:
460
+
461
+ ```html
462
+ <sup title="IPCC. (2023). Climate Change 2023: Synthesis Report.">
463
+ <a href="#fn:1" class="footnote">1</a>
464
+ </sup>
465
+ ```
466
+
467
+ This works immediately in all browsers with no additional code required. However, native tooltips have limitations:
468
+ - Cannot contain HTML formatting
469
+ - Limited styling options
470
+ - Inconsistent behavior across browsers
471
+
472
+ ### Custom Tooltips
473
+
474
+ For richer tooltips, use the `data-footnote-content` attribute with your preferred tooltip library:
475
+
476
+ **With Tippy.js:**
477
+
478
+ ```javascript
479
+ import tippy from 'tippy.js'
480
+ import 'tippy.js/dist/tippy.css'
481
+
482
+ // Initialize tooltips for all footnote markers
483
+ tippy('[data-footnote-content]', {
484
+ content: (reference) => reference.getAttribute('data-footnote-content'),
485
+ allowHTML: true,
486
+ theme: 'light',
487
+ placement: 'top',
488
+ maxWidth: 400
489
+ })
490
+ ```
491
+
492
+ **With Bootstrap:**
493
+
494
+ ```javascript
495
+ // Initialize Bootstrap tooltips
496
+ document.querySelectorAll('[data-footnote-content]').forEach(element => {
497
+ new bootstrap.Tooltip(element, {
498
+ title: element.getAttribute('data-footnote-content'),
499
+ html: true,
500
+ placement: 'top'
501
+ })
502
+ })
503
+ ```
504
+
505
+ **With Custom CSS Tooltips:**
506
+
507
+ ```css
508
+ /* Pure CSS tooltip */
509
+ .footnote-ref {
510
+ position: relative;
511
+ }
512
+
513
+ .footnote-ref::after {
514
+ content: attr(data-footnote-content);
515
+ position: absolute;
516
+ bottom: 100%;
517
+ left: 50%;
518
+ transform: translateX(-50%);
519
+ padding: 0.5rem;
520
+ background: #333;
521
+ color: white;
522
+ border-radius: 0.25rem;
523
+ font-size: 0.875rem;
524
+ white-space: nowrap;
525
+ opacity: 0;
526
+ pointer-events: none;
527
+ transition: opacity 0.2s;
528
+ z-index: 1000;
529
+ }
530
+
531
+ .footnote-ref:hover::after {
532
+ opacity: 1;
533
+ }
534
+ ```
535
+
536
+ **Security Note:** The `data-footnote-content` attribute is properly HTML-escaped to prevent XSS attacks. When using `allowHTML: true` with tooltip libraries, the content is safe because special characters are already escaped.
537
+
445
538
  ## CSS Styling
446
539
 
447
540
  The rendered HTML includes Tailwind CSS classes. You can customize the appearance:
@@ -621,11 +714,11 @@ bundle exec rspec spec/lib/panda/editor/renderer_spec.rb
621
714
 
622
715
  ## Future Enhancements
623
716
 
624
- Potential improvements for future versions:
717
+ See GitHub issue [#2](https://github.com/tastybamboo/panda-editor/issues/2) for planned improvements including:
625
718
 
626
719
  - [ ] Support for footnotes in other block types (headers, quotes, etc.)
627
720
  - [x] Rich text formatting within footnote content (implemented via markdown support)
628
- - [ ] Footnote tooltips on hover
721
+ - [x] Footnote tooltips on hover (implemented with native browser tooltips and custom tooltip data attributes)
629
722
  - [ ] Customizable footnote markers (*, †, ‡, etc.)
630
723
  - [ ] Export footnotes to bibliography formats (BibTeX, etc.)
631
724
  - [ ] Footnote management UI in EditorJS
data/lefthook.yml ADDED
@@ -0,0 +1,16 @@
1
+ ---
2
+ assert_lefthook_installed: true
3
+ colors: true
4
+ pre-commit:
5
+ parallel: true
6
+ jobs:
7
+ - name: standardrb
8
+ run: bundle exec standardrb
9
+ glob: "*.rb"
10
+ stage_fixed: true
11
+
12
+ pre-push:
13
+ parallel: true
14
+ jobs:
15
+ - name: standardrb
16
+ run: bundle exec standardrb
@@ -1,13 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'net/http'
4
- require 'json'
3
+ require "net/http"
4
+ require "json"
5
5
 
6
6
  module Panda
7
7
  module Editor
8
8
  class AssetLoader
9
- GITHUB_RELEASES_URL = 'https://api.github.com/repos/tastybamboo/panda-editor/releases/latest'
10
- ASSET_CACHE_DIR = Rails.root.join('tmp', 'panda_editor_assets')
9
+ GITHUB_RELEASES_URL = "https://api.github.com/repos/tastybamboo/panda-editor/releases/latest"
10
+ ASSET_CACHE_DIR = Rails.root.join("tmp", "panda_editor_assets")
11
11
 
12
12
  class << self
13
13
  def load_assets
@@ -22,7 +22,7 @@ module Panda
22
22
  if use_compiled_assets?
23
23
  compiled_javascript_url
24
24
  else
25
- '/assets/panda/editor/application.js'
25
+ "/assets/panda/editor/application.js"
26
26
  end
27
27
  end
28
28
 
@@ -30,7 +30,7 @@ module Panda
30
30
  if use_compiled_assets?
31
31
  compiled_stylesheet_url
32
32
  else
33
- '/assets/panda/editor/application.css'
33
+ "/assets/panda/editor/application.css"
34
34
  end
35
35
  end
36
36
 
@@ -39,7 +39,7 @@ module Panda
39
39
  def use_compiled_assets?
40
40
  Rails.env.production? ||
41
41
  Rails.env.test? ||
42
- ENV['PANDA_EDITOR_USE_COMPILED_ASSETS'] == 'true'
42
+ ENV["PANDA_EDITOR_USE_COMPILED_ASSETS"] == "true"
43
43
  end
44
44
 
45
45
  def load_compiled_assets
@@ -52,23 +52,23 @@ module Panda
52
52
 
53
53
  def load_development_assets
54
54
  {
55
- javascript: '/assets/panda/editor/application.js',
56
- stylesheet: '/assets/panda/editor/application.css'
55
+ javascript: "/assets/panda/editor/application.js",
56
+ stylesheet: "/assets/panda/editor/application.css"
57
57
  }
58
58
  end
59
59
 
60
60
  def compiled_javascript_url
61
- asset_path = find_latest_asset('js')
61
+ asset_path = find_latest_asset("js")
62
62
  asset_path ? "/panda-editor-assets/#{File.basename(asset_path)}" : nil
63
63
  end
64
64
 
65
65
  def compiled_stylesheet_url
66
- asset_path = find_latest_asset('css')
66
+ asset_path = find_latest_asset("css")
67
67
  asset_path ? "/panda-editor-assets/#{File.basename(asset_path)}" : nil
68
68
  end
69
69
 
70
70
  def find_latest_asset(extension)
71
- pattern = Rails.root.join('public', 'panda-editor-assets', "panda-editor-*.#{extension}")
71
+ pattern = Rails.root.join("public", "panda-editor-assets", "panda-editor-*.#{extension}")
72
72
  Dir.glob(pattern).max_by { |f| File.mtime(f) }
73
73
  end
74
74
 
@@ -79,18 +79,18 @@ module Panda
79
79
  end
80
80
 
81
81
  def assets_exist?
82
- js_exists = Dir.glob(Rails.root.join('public', 'panda-editor-assets', 'panda-editor-*.js')).any?
83
- css_exists = Dir.glob(Rails.root.join('public', 'panda-editor-assets', 'panda-editor-*.css')).any?
82
+ js_exists = Dir.glob(Rails.root.join("public", "panda-editor-assets", "panda-editor-*.js")).any?
83
+ css_exists = Dir.glob(Rails.root.join("public", "panda-editor-assets", "panda-editor-*.css")).any?
84
84
  js_exists && css_exists
85
85
  end
86
86
 
87
87
  def download_assets_from_github
88
- Rails.logger.info '[Panda Editor] Downloading assets from GitHub releases...'
88
+ Rails.logger.info "[Panda Editor] Downloading assets from GitHub releases..."
89
89
 
90
90
  begin
91
91
  release_data = fetch_latest_release
92
- download_release_assets(release_data['assets'])
93
- rescue StandardError => e
92
+ download_release_assets(release_data["assets"])
93
+ rescue => e
94
94
  Rails.logger.error "[Panda Editor] Failed to download assets: #{e.message}"
95
95
  use_fallback_assets
96
96
  end
@@ -100,30 +100,30 @@ module Panda
100
100
  uri = URI(GITHUB_RELEASES_URL)
101
101
  response = Net::HTTP.get_response(uri)
102
102
 
103
- raise "GitHub API returned #{response.code}" unless response.code == '200'
103
+ raise "GitHub API returned #{response.code}" unless response.code == "200"
104
104
 
105
105
  JSON.parse(response.body)
106
106
  end
107
107
 
108
108
  def download_release_assets(assets)
109
109
  assets.each do |asset|
110
- next unless asset['name'].match?(/panda-editor.*\.(js|css)$/)
110
+ next unless asset["name"].match?(/panda-editor.*\.(js|css)$/)
111
111
 
112
112
  download_asset(asset)
113
113
  end
114
114
  end
115
115
 
116
116
  def download_asset(asset)
117
- uri = URI(asset['browser_download_url'])
117
+ uri = URI(asset["browser_download_url"])
118
118
  response = Net::HTTP.get_response(uri)
119
119
 
120
- return unless response.code == '200'
120
+ return unless response.code == "200"
121
121
 
122
- save_asset(asset['name'], response.body)
122
+ save_asset(asset["name"], response.body)
123
123
  end
124
124
 
125
125
  def save_asset(filename, content)
126
- dir = Rails.root.join('public', 'panda-editor-assets')
126
+ dir = Rails.root.join("public", "panda-editor-assets")
127
127
  FileUtils.mkdir_p(dir)
128
128
 
129
129
  File.write(dir.join(filename), content)
@@ -131,17 +131,17 @@ module Panda
131
131
  end
132
132
 
133
133
  def use_fallback_assets
134
- Rails.logger.warn '[Panda Editor] Using fallback embedded assets'
134
+ Rails.logger.warn "[Panda Editor] Using fallback embedded assets"
135
135
  # Copy embedded assets from gem to public directory
136
136
  copy_embedded_assets
137
137
  end
138
138
 
139
139
  def copy_embedded_assets
140
- source_dir = Panda::Editor::Engine.root.join('public', 'panda-editor-assets')
141
- dest_dir = Rails.root.join('public', 'panda-editor-assets')
140
+ source_dir = Panda::Editor::Engine.root.join("public", "panda-editor-assets")
141
+ dest_dir = Rails.root.join("public", "panda-editor-assets")
142
142
 
143
143
  FileUtils.mkdir_p(dest_dir)
144
- FileUtils.cp_r(Dir.glob(source_dir.join('*')), dest_dir)
144
+ FileUtils.cp_r(Dir.glob(source_dir.join("*")), dest_dir)
145
145
  end
146
146
  end
147
147
  end
@@ -5,13 +5,13 @@ module Panda
5
5
  module Blocks
6
6
  class Alert < Base
7
7
  def render
8
- message = sanitize(data['message'])
9
- type = data['type'] || 'primary'
8
+ message = sanitize(data["message"])
9
+ type = data["type"] || "primary"
10
10
 
11
11
  html_safe(
12
12
  "<div class=\"#{alert_classes(type)} p-4 mb-4 rounded-lg\">" \
13
13
  "#{message}" \
14
- '</div>'
14
+ "</div>"
15
15
  )
16
16
  end
17
17
 
@@ -19,13 +19,13 @@ module Panda
19
19
 
20
20
  def alert_classes(type)
21
21
  case type
22
- when 'primary' then 'bg-blue-100 text-blue-800'
23
- when 'secondary' then 'bg-gray-100 text-gray-800'
24
- when 'success' then 'bg-green-100 text-green-800'
25
- when 'danger' then 'bg-red-100 text-red-800'
26
- when 'warning' then 'bg-yellow-100 text-yellow-800'
27
- when 'info' then 'bg-indigo-100 text-indigo-800'
28
- else 'bg-blue-100 text-blue-800'
22
+ when "primary" then "bg-blue-100 text-blue-800"
23
+ when "secondary" then "bg-gray-100 text-gray-800"
24
+ when "success" then "bg-green-100 text-green-800"
25
+ when "danger" then "bg-red-100 text-red-800"
26
+ when "warning" then "bg-yellow-100 text-yellow-800"
27
+ when "info" then "bg-indigo-100 text-indigo-800"
28
+ else "bg-blue-100 text-blue-800"
29
29
  end
30
30
  end
31
31
  end
@@ -15,7 +15,7 @@ module Panda
15
15
  end
16
16
 
17
17
  def render
18
- ''
18
+ ""
19
19
  end
20
20
 
21
21
  protected
@@ -5,8 +5,8 @@ module Panda
5
5
  module Blocks
6
6
  class Header < Base
7
7
  def render
8
- content = sanitize(data['text'])
9
- level = data['level'] || 2
8
+ content = sanitize(data["text"])
9
+ level = data["level"] || 2
10
10
  html_safe("<h#{level}>#{content}</h#{level}>")
11
11
  end
12
12
  end
@@ -5,19 +5,19 @@ module Panda
5
5
  module Blocks
6
6
  class Image < Base
7
7
  def render
8
- url = data['url']
9
- caption = sanitize(data['caption'])
10
- with_border = data['withBorder']
11
- with_background = data['withBackground']
12
- stretched = data['stretched']
8
+ url = data["url"]
9
+ caption = sanitize(data["caption"])
10
+ with_border = data["withBorder"]
11
+ with_background = data["withBackground"]
12
+ stretched = data["stretched"]
13
13
 
14
- css_classes = ['prose']
15
- css_classes << 'border' if with_border
16
- css_classes << 'bg-gray-100' if with_background
17
- css_classes << 'w-full' if stretched
14
+ css_classes = ["prose"]
15
+ css_classes << "border" if with_border
16
+ css_classes << "bg-gray-100" if with_background
17
+ css_classes << "w-full" if stretched
18
18
 
19
19
  html_safe(<<~HTML)
20
- <figure class="#{css_classes.join(' ')}">
20
+ <figure class="#{css_classes.join(" ")}">
21
21
  <img src="#{url}" alt="#{caption}" />
22
22
  #{caption_element(caption)}
23
23
  </figure>
@@ -27,7 +27,7 @@ module Panda
27
27
  private
28
28
 
29
29
  def caption_element(caption)
30
- return '' if caption.blank?
30
+ return "" if caption.blank?
31
31
 
32
32
  "<figcaption>#{caption}</figcaption>"
33
33
  end
@@ -5,10 +5,11 @@ module Panda
5
5
  module Blocks
6
6
  class List < Base
7
7
  def render
8
- list_type = data['style'] == 'ordered' ? 'ol' : 'ul'
8
+ style = data["style"] || data[:style]
9
+ list_type = (style == "ordered") ? "ol" : "ul"
9
10
  html_safe(
10
11
  "<#{list_type}>" \
11
- "#{render_items(data['items'])}" \
12
+ "#{render_items(data["items"] || data[:items] || [])}" \
12
13
  "</#{list_type}>"
13
14
  )
14
15
  end
@@ -16,15 +17,33 @@ module Panda
16
17
  private
17
18
 
18
19
  def render_items(items)
20
+ return "" unless items.is_a?(Array)
21
+
19
22
  items.map do |item|
20
- content = item.is_a?(Hash) ? item['content'] : item
21
- nested = item.is_a?(Hash) && item['items'].present? ? render_nested(item['items']) : ''
22
- "<li>#{sanitize(content)}#{nested}</li>"
23
+ content = extract_content(item)
24
+ nested_items = extract_nested_items(item)
25
+ nested = nested_items.present? ? render_nested(nested_items) : ""
26
+ "<li>#{sanitize(content.to_s)}#{nested}</li>"
23
27
  end.join
24
28
  end
25
29
 
30
+ def extract_content(item)
31
+ return item unless item.is_a?(Hash)
32
+
33
+ # Handle both string and symbol keys
34
+ item["content"] || item[:content] || ""
35
+ end
36
+
37
+ def extract_nested_items(item)
38
+ return [] unless item.is_a?(Hash)
39
+
40
+ # Handle both string and symbol keys
41
+ item["items"] || item[:items] || []
42
+ end
43
+
26
44
  def render_nested(items)
27
- self.class.new({ 'items' => items, 'style' => data['style'] }).render
45
+ style = data["style"] || data[:style]
46
+ self.class.new({"items" => items, "style" => style}).render
28
47
  end
29
48
  end
30
49
  end