panda-editor 0.2.1 → 0.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.
data/docs/FOOTNOTES.md ADDED
@@ -0,0 +1,640 @@
1
+ # Footnotes in Panda Editor
2
+
3
+ Panda Editor provides a powerful footnote system that allows you to add inline citations and references to your content. Footnotes are automatically collected, numbered, and rendered in a collapsible "Sources/References" section at the end of your document.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Overview](#overview)
8
+ - [How It Works](#how-it-works)
9
+ - [JSON Structure](#json-structure)
10
+ - [Field Reference](#field-reference)
11
+ - [Rendered Output](#rendered-output)
12
+ - [Frontend Integration](#frontend-integration)
13
+ - [Advanced Features](#advanced-features)
14
+ - [CSS Styling](#css-styling)
15
+ - [Examples](#examples)
16
+
17
+ ## Overview
18
+
19
+ The footnote system consists of three main components:
20
+
21
+ 1. **Paragraph Block**: Accepts footnote data within paragraph blocks and injects inline markers
22
+ 2. **FootnoteRegistry**: Collects and numbers footnotes across the entire document
23
+ 3. **Renderer**: Coordinates footnote processing and generates the sources section
24
+
25
+ ### Key Features
26
+
27
+ - ✨ **Automatic numbering**: Sequential numbering across the entire document
28
+ - 🔄 **De-duplication**: Same source cited multiple times uses the same footnote number
29
+ - 📍 **Position-based injection**: Place footnote markers at any character position
30
+ - 🎨 **Collapsible UI**: Clean, accessible sources section
31
+ - 🔗 **Bidirectional links**: Navigate between citations and sources
32
+
33
+ ## How It Works
34
+
35
+ ### Architecture
36
+
37
+ ```
38
+ ┌─────────────────────────────────────────────────────────┐
39
+ │ EditorJS JSON │
40
+ │ (Contains blocks with embedded footnote data) │
41
+ └─────────────────────┬───────────────────────────────────┘
42
+
43
+
44
+ ┌─────────────────────────────────────────────────────────┐
45
+ │ Renderer │
46
+ │ - Creates FootnoteRegistry │
47
+ │ - Passes registry to all blocks via options │
48
+ └─────────────────────┬───────────────────────────────────┘
49
+
50
+
51
+ ┌─────────────────────────────────────────────────────────┐
52
+ │ Paragraph Block │
53
+ │ - Processes footnotes array │
54
+ │ - Registers each footnote with registry │
55
+ │ - Injects <sup> markers at specified positions │
56
+ └─────────────────────┬───────────────────────────────────┘
57
+
58
+
59
+ ┌─────────────────────────────────────────────────────────┐
60
+ │ FootnoteRegistry │
61
+ │ - Assigns sequential numbers │
62
+ │ - Tracks footnotes by ID for de-duplication │
63
+ │ - Generates sources section HTML │
64
+ └─────────────────────────────────────────────────────────┘
65
+ ```
66
+
67
+ ### Processing Flow
68
+
69
+ 1. **Initialization**: Renderer creates a `FootnoteRegistry` instance
70
+ 2. **Block Rendering**: Each paragraph block processes its footnotes:
71
+ - Sorts footnotes by position (descending) to avoid position shifts
72
+ - Registers each footnote with the registry
73
+ - Receives a footnote number back
74
+ - Injects superscript marker at the specified position
75
+ 3. **Sources Generation**: After all blocks are rendered, the renderer:
76
+ - Checks if any footnotes were collected
77
+ - Appends the sources section if footnotes exist
78
+
79
+ ## JSON Structure
80
+
81
+ Add footnotes to any paragraph block in your EditorJS JSON:
82
+
83
+ ```json
84
+ {
85
+ "type": "paragraph",
86
+ "data": {
87
+ "text": "Climate change has accelerated significantly since 1980",
88
+ "footnotes": [
89
+ {
90
+ "id": "fn-uuid-123",
91
+ "content": "IPCC. (2023). Climate Change 2023: Synthesis Report.",
92
+ "position": 55
93
+ }
94
+ ]
95
+ }
96
+ }
97
+ ```
98
+
99
+ ### Multiple Footnotes
100
+
101
+ ```json
102
+ {
103
+ "type": "paragraph",
104
+ "data": {
105
+ "text": "Global temperature has risen 1.1°C since pre-industrial times",
106
+ "footnotes": [
107
+ {
108
+ "id": "fn-uuid-456",
109
+ "content": "NASA. (2023). Global Climate Change: Vital Signs.",
110
+ "position": 35
111
+ },
112
+ {
113
+ "id": "fn-uuid-789",
114
+ "content": "NOAA. (2023). State of the Climate Report.",
115
+ "position": 62
116
+ }
117
+ ]
118
+ }
119
+ }
120
+ ```
121
+
122
+ ### Reusing Sources
123
+
124
+ To cite the same source multiple times, use the same `id`:
125
+
126
+ ```json
127
+ {
128
+ "blocks": [
129
+ {
130
+ "type": "paragraph",
131
+ "data": {
132
+ "text": "First mention of the study",
133
+ "footnotes": [{
134
+ "id": "fn-study-2023",
135
+ "content": "Smith et al. (2023). Important Study.",
136
+ "position": 26
137
+ }]
138
+ }
139
+ },
140
+ {
141
+ "type": "paragraph",
142
+ "data": {
143
+ "text": "Second mention of the same study",
144
+ "footnotes": [{
145
+ "id": "fn-study-2023",
146
+ "content": "Smith et al. (2023). Important Study.",
147
+ "position": 33
148
+ }]
149
+ }
150
+ }
151
+ ]
152
+ }
153
+ ```
154
+
155
+ Both paragraphs will reference the same footnote number, and the source will appear only once in the sources section.
156
+
157
+ ## Field Reference
158
+
159
+ ### `id` (required)
160
+
161
+ - **Type**: String
162
+ - **Purpose**: Unique identifier for the footnote
163
+ - **Best Practice**: Use UUIDs or descriptive IDs like `fn-study-name-year`
164
+ - **De-duplication**: Footnotes with the same `id` will share the same number
165
+
166
+ ### `content` (required)
167
+
168
+ - **Type**: String
169
+ - **Purpose**: The citation or reference text
170
+ - **HTML Support**: Basic HTML tags are sanitized and allowed
171
+ - **Best Practice**: Use standard citation formats (APA, MLA, etc.)
172
+
173
+ ### `position` (required)
174
+
175
+ - **Type**: Integer
176
+ - **Purpose**: Character position where the footnote marker should be inserted
177
+ - **Zero-indexed**: Position 0 is before the first character
178
+ - **Validation**: Must be between 0 and text length (inclusive)
179
+
180
+ ## Rendered Output
181
+
182
+ ### Inline Markers
183
+
184
+ ```html
185
+ <p>Climate change has accelerated significantly since 1980<sup id="fnref:1"><a href="#fn:1" class="footnote">1</a></sup></p>
186
+ ```
187
+
188
+ ### Sources Section
189
+
190
+ ```html
191
+ <div class="mx-6 lg:mx-8 mt-4 mb-8">
192
+ <div class="footnotes-section bg-gray-50 rounded-lg overflow-hidden">
193
+ <button class="footnotes-header w-full px-4 py-3 flex items-center justify-between cursor-pointer hover:bg-gray-100 transition-colors"
194
+ data-footnotes-target="toggle"
195
+ data-action="click->footnotes#toggle">
196
+ <h3 class="text-sm font-unbounded font-medium text-gray-900 m-0">Sources/References</h3>
197
+ <svg class="footnotes-chevron w-5 h-5 text-gray-600"
198
+ data-footnotes-target="chevron"
199
+ fill="none"
200
+ stroke="currentColor"
201
+ viewBox="0 0 24 24">
202
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
203
+ </svg>
204
+ </button>
205
+ <div class="footnotes-content" data-footnotes-target="content">
206
+ <ol class="footnotes text-sm text-gray-700 space-y-2 px-4 pb-3">
207
+ <li id="fn:1">
208
+ <p>
209
+ IPCC. (2023). Climate Change 2023: Synthesis Report.
210
+ <a href="#fnref:1" class="footnote-backref">↩</a>
211
+ </p>
212
+ </li>
213
+ </ol>
214
+ </div>
215
+ </div>
216
+ </div>
217
+ ```
218
+
219
+ ## Frontend Integration
220
+
221
+ The sources section includes data attributes designed for use with JavaScript frameworks like Stimulus:
222
+
223
+ ### Data Attributes
224
+
225
+ - `data-footnotes-target="toggle"` - The clickable header button
226
+ - `data-footnotes-target="content"` - The collapsible content section
227
+ - `data-footnotes-target="chevron"` - The chevron icon for rotation animation
228
+
229
+ ### Example Stimulus Controller
230
+
231
+ ```javascript
232
+ import { Controller } from "@hotwired/stimulus"
233
+
234
+ export default class extends Controller {
235
+ static targets = ["toggle", "content", "chevron"]
236
+
237
+ connect() {
238
+ // Start collapsed
239
+ this.contentTarget.classList.add("hidden")
240
+ }
241
+
242
+ toggle(event) {
243
+ event.preventDefault()
244
+
245
+ const isHidden = this.contentTarget.classList.contains("hidden")
246
+
247
+ if (isHidden) {
248
+ this.contentTarget.classList.remove("hidden")
249
+ this.chevronTarget.style.transform = "rotate(180deg)"
250
+ } else {
251
+ this.contentTarget.classList.add("hidden")
252
+ this.chevronTarget.style.transform = "rotate(0deg)"
253
+ }
254
+ }
255
+ }
256
+ ```
257
+
258
+ ### Accessible Mode
259
+
260
+ The footnotes section is designed to be accessible:
261
+
262
+ - Semantic HTML (`<button>`, `<ol>`, proper heading levels)
263
+ - ARIA-ready structure (add `aria-expanded` as needed)
264
+ - Keyboard navigation (links and buttons are focusable)
265
+ - Clear visual hierarchy
266
+
267
+ ## Advanced Features
268
+
269
+ ### Position Calculation
270
+
271
+ When determining where to place a footnote marker, count characters from the beginning of the text:
272
+
273
+ ```javascript
274
+ const text = "Hello world"
275
+ // 0123456789...
276
+
277
+ // To place marker after "Hello"
278
+ position = 5
279
+
280
+ // Result: "Hello<sup>1</sup> world"
281
+ ```
282
+
283
+ ### Handling HTML in Text
284
+
285
+ If your paragraph contains HTML tags, remember that `position` refers to the character position in the **rendered HTML string**:
286
+
287
+ ```javascript
288
+ const text = "Text with <b>bold</b> formatting"
289
+ // Position 10 is after "Text with "
290
+ // Position 13 is inside the <b> tag (after "<b>")
291
+ ```
292
+
293
+ Best practice: Calculate positions based on plain text, not HTML.
294
+
295
+ ### Sorting and Insertion
296
+
297
+ The paragraph block sorts footnotes by position in **descending order** before insertion. This prevents position shifts:
298
+
299
+ ```ruby
300
+ # Without sorting (wrong):
301
+ text = "Hello world"
302
+ insert at position 5: "Hello<sup>1</sup> world" # Now position 11 shifted!
303
+ insert at position 11: Error - position too far
304
+
305
+ # With sorting (correct):
306
+ text = "Hello world"
307
+ insert at position 11: "Hello world<sup>2</sup>"
308
+ insert at position 5: "Hello<sup>1</sup> world<sup>2</sup>"
309
+ ```
310
+
311
+ ### Markdown Support
312
+
313
+ The footnote system supports markdown formatting, allowing you to use rich text formatting in your citations.
314
+
315
+ **Enable markdown:**
316
+
317
+ ```ruby
318
+ renderer = Panda::Editor::Renderer.new(content, markdown: true)
319
+ output = renderer.render
320
+ ```
321
+
322
+ **Supported markdown features:**
323
+
324
+ - **Bold text** (`**bold**` or `__bold__`)
325
+ - *Italic text* (`*italic*` or `_italic_`)
326
+ - `Inline code` (`` `code` ``)
327
+ - ~~Strikethrough~~ (`~~text~~`)
328
+ - [Links](url) (`[text](url)`)
329
+ - Automatic URL linking
330
+
331
+ **Example:**
332
+
333
+ ```ruby
334
+ content = {
335
+ "blocks" => [{
336
+ "type" => "paragraph",
337
+ "data" => {
338
+ "text" => "Research findings",
339
+ "footnotes" => [{
340
+ "id" => "fn-1",
341
+ "content" => "Smith, J. (2023). **Important study** on *ADHD treatment*. See https://example.com for details.",
342
+ "position" => 17
343
+ }]
344
+ }
345
+ }]
346
+ }
347
+
348
+ renderer = Panda::Editor::Renderer.new(content, markdown: true)
349
+ # Output will include: Smith, J. (2023). <strong>Important study</strong> on <em>ADHD treatment</em>. See <a href="https://example.com">https://example.com</a> for details.
350
+ ```
351
+
352
+ **Important notes:**
353
+
354
+ - Markdown includes built-in URL autolinking, so you typically don't need `autolink_urls: true` when using markdown
355
+ - However, both options can be used together if needed - the custom autolink_urls will skip URLs that markdown already linked
356
+ - Markdown links are rendered with `target="_blank"` and `rel="noopener noreferrer"` for security
357
+ - Images are disabled in markdown footnotes for security
358
+ - HTML styles are stripped from markdown output
359
+
360
+ ### Auto-linking URLs
361
+
362
+ The footnote system can automatically convert plain URLs in footnote content into clickable links when markdown is not enabled.
363
+
364
+ **Enable auto-linking:**
365
+
366
+ ```ruby
367
+ renderer = Panda::Editor::Renderer.new(content, autolink_urls: true)
368
+ output = renderer.render
369
+ ```
370
+
371
+ **How it works:**
372
+
373
+ Plain URLs in footnote content are detected and wrapped in `<a>` tags:
374
+
375
+ ```ruby
376
+ # Input footnote content:
377
+ "Ward, J.H. & Curran, S. (2021). https://doi.org/10.1111/camh.12471"
378
+
379
+ # Rendered output:
380
+ "Ward, J.H. & Curran, S. (2021). <a href=\"https://doi.org/10.1111/camh.12471\" target=\"_blank\" rel=\"noopener noreferrer\">https://doi.org/10.1111/camh.12471</a>"
381
+ ```
382
+
383
+ **Features:**
384
+
385
+ - **Supported protocols**: `http://`, `https://`, `ftp://`, and `www.` (automatically prefixed with `https://`)
386
+ - **Security attributes**: All links include `target="_blank"` and `rel="noopener noreferrer"`
387
+ - **Smart detection**: Won't double-link URLs already in `<a>` tags
388
+ - **Multiple URLs**: Handles multiple URLs in the same footnote
389
+ - **Punctuation handling**: Excludes trailing punctuation (periods, commas, etc.) from URLs
390
+
391
+ **Pattern matching:**
392
+
393
+ The auto-linker uses a sophisticated regex pattern that:
394
+ - Matches complete URLs without breaking on query parameters
395
+ - Avoids linking URLs that are already part of href attributes
396
+ - Handles complex URLs with paths, query strings, and fragments
397
+
398
+ **Example with multiple URLs:**
399
+
400
+ ```ruby
401
+ content = {
402
+ "blocks" => [{
403
+ "type" => "paragraph",
404
+ "data" => {
405
+ "text" => "Research findings",
406
+ "footnotes" => [{
407
+ "id" => "fn-1",
408
+ "content" => "See https://example.com/study and https://doi.org/10.1234/example for details",
409
+ "position" => 17
410
+ }]
411
+ }
412
+ }]
413
+ }
414
+
415
+ renderer = Panda::Editor::Renderer.new(content, autolink_urls: true)
416
+ # Both URLs will be converted to clickable links
417
+ ```
418
+
419
+ **Disabling auto-linking:**
420
+
421
+ By default, auto-linking is disabled. Only enable it when needed:
422
+
423
+ ```ruby
424
+ # Auto-linking disabled (default)
425
+ renderer = Panda::Editor::Renderer.new(content)
426
+
427
+ # Auto-linking enabled
428
+ renderer = Panda::Editor::Renderer.new(content, autolink_urls: true)
429
+ ```
430
+
431
+ **Use in Content Concern:**
432
+
433
+ For applications using the `Panda::Editor::Content` concern, enable auto-linking in the `generate_cached_content` method:
434
+
435
+ ```ruby
436
+ def generate_cached_content
437
+ renderer_options = {autolink_urls: true}
438
+
439
+ if content.is_a?(Hash) && content["blocks"].present?
440
+ self.cached_content = Panda::Editor::Renderer.new(content, renderer_options).render
441
+ end
442
+ end
443
+ ```
444
+
445
+ ## CSS Styling
446
+
447
+ The rendered HTML includes Tailwind CSS classes. You can customize the appearance:
448
+
449
+ ### Default Classes
450
+
451
+ ```html
452
+ <!-- Container -->
453
+ <div class="mx-6 lg:mx-8 mt-4 mb-8">
454
+
455
+ <!-- Section wrapper -->
456
+ <div class="footnotes-section bg-gray-50 rounded-lg overflow-hidden">
457
+
458
+ <!-- Toggle button -->
459
+ <button class="footnotes-header w-full px-4 py-3 flex items-center justify-between cursor-pointer hover:bg-gray-100 transition-colors">
460
+
461
+ <!-- Header text -->
462
+ <h3 class="text-sm font-unbounded font-medium text-gray-900 m-0">
463
+
464
+ <!-- Content area -->
465
+ <div class="footnotes-content">
466
+ <ol class="footnotes text-sm text-gray-700 space-y-2 px-4 pb-3">
467
+ ```
468
+
469
+ ### Custom Styling
470
+
471
+ To customize the appearance, override these classes in your application CSS:
472
+
473
+ ```css
474
+ /* Custom container spacing */
475
+ .footnotes-section {
476
+ @apply my-12;
477
+ }
478
+
479
+ /* Custom header styling */
480
+ .footnotes-header h3 {
481
+ @apply text-lg font-bold;
482
+ }
483
+
484
+ /* Custom footnote list styling */
485
+ .footnotes-content ol {
486
+ @apply text-base leading-relaxed;
487
+ }
488
+
489
+ /* Inline footnote markers */
490
+ sup.footnote a {
491
+ @apply text-blue-600 hover:text-blue-800;
492
+ }
493
+ ```
494
+
495
+ ## Examples
496
+
497
+ ### Academic Citation
498
+
499
+ ```json
500
+ {
501
+ "type": "paragraph",
502
+ "data": {
503
+ "text": "Research shows significant improvements in renewable energy efficiency",
504
+ "footnotes": [{
505
+ "id": "fn-smith-2023",
506
+ "content": "Smith, J., & Jones, M. (2023). Advances in solar panel technology. <em>Journal of Renewable Energy</em>, 45(2), 123-145.",
507
+ "position": 70
508
+ }]
509
+ }
510
+ }
511
+ ```
512
+
513
+ ### Web Resource
514
+
515
+ ```json
516
+ {
517
+ "type": "paragraph",
518
+ "data": {
519
+ "text": "According to NASA, global sea levels have risen 8-9 inches since 1880",
520
+ "footnotes": [{
521
+ "id": "fn-nasa-2023",
522
+ "content": "NASA. (2023). <a href=\"https://climate.nasa.gov/vital-signs/sea-level/\" target=\"_blank\">Sea Level Change Data</a>. Retrieved October 26, 2025.",
523
+ "position": 70
524
+ }]
525
+ }
526
+ }
527
+ ```
528
+
529
+ ### Multiple Citations
530
+
531
+ ```json
532
+ {
533
+ "type": "paragraph",
534
+ "data": {
535
+ "text": "Studies from 2022 and 2023 confirm these findings",
536
+ "footnotes": [
537
+ {
538
+ "id": "fn-study-2022",
539
+ "content": "Johnson, A. (2022). Early intervention strategies.",
540
+ "position": 18
541
+ },
542
+ {
543
+ "id": "fn-study-2023",
544
+ "content": "Williams, B. (2023). Long-term outcomes.",
545
+ "position": 31
546
+ }
547
+ ]
548
+ }
549
+ }
550
+ ```
551
+
552
+ ## Testing
553
+
554
+ The footnote system includes comprehensive test coverage:
555
+
556
+ ### Paragraph Block Tests
557
+
558
+ Located in `spec/lib/panda/editor/blocks/paragraph_spec.rb`:
559
+
560
+ - ✓ Injects footnote markers at correct positions
561
+ - ✓ Registers footnotes with the registry
562
+ - ✓ Handles multiple footnotes in correct order
563
+ - ✓ Returns same number for duplicate IDs
564
+ - ✓ Works without footnotes
565
+
566
+ ### Renderer Tests
567
+
568
+ Located in `spec/lib/panda/editor/renderer_spec.rb`:
569
+
570
+ - ✓ Appends sources section when footnotes exist
571
+ - ✓ Does not append sources section when no footnotes
572
+ - ✓ Handles duplicate footnote IDs correctly
573
+ - ✓ Numbers footnotes sequentially across paragraphs
574
+
575
+ ### Running Tests
576
+
577
+ ```bash
578
+ cd /path/to/panda-editor
579
+ bundle exec rspec spec/lib/panda/editor/blocks/paragraph_spec.rb
580
+ bundle exec rspec spec/lib/panda/editor/renderer_spec.rb
581
+ ```
582
+
583
+ ## Troubleshooting
584
+
585
+ ### Footnote marker not appearing
586
+
587
+ **Problem**: The superscript marker doesn't show up in the rendered HTML.
588
+
589
+ **Solutions**:
590
+ - Check that `position` is within the text length (0 to text.length)
591
+ - Verify the footnote has both `id` and `content` fields
592
+ - Ensure the `FootnoteRegistry` is passed in the options
593
+
594
+ ### Wrong footnote number
595
+
596
+ **Problem**: Footnote shows number 2 when it should be 1.
597
+
598
+ **Solutions**:
599
+ - Check if another footnote is being registered first
600
+ - Verify footnote IDs are unique (unless intentionally reusing)
601
+ - Review the order of blocks in your JSON
602
+
603
+ ### Sources section not appearing
604
+
605
+ **Problem**: Inline markers work but no sources section at bottom.
606
+
607
+ **Solutions**:
608
+ - Verify footnotes are actually being registered (check `FootnoteRegistry#any?`)
609
+ - Ensure the renderer is calling `render_sources_section`
610
+ - Check that blocks are receiving the footnote registry in options
611
+
612
+ ### Position calculation off by one
613
+
614
+ **Problem**: Footnote appears one character before/after expected position.
615
+
616
+ **Solutions**:
617
+ - Remember positions are zero-indexed
618
+ - Position 0 is before the first character
619
+ - Position equal to text.length is after the last character
620
+ - Test with plain text first, then add HTML formatting
621
+
622
+ ## Future Enhancements
623
+
624
+ Potential improvements for future versions:
625
+
626
+ - [ ] Support for footnotes in other block types (headers, quotes, etc.)
627
+ - [x] Rich text formatting within footnote content (implemented via markdown support)
628
+ - [ ] Footnote tooltips on hover
629
+ - [ ] Customizable footnote markers (*, †, ‡, etc.)
630
+ - [ ] Export footnotes to bibliography formats (BibTeX, etc.)
631
+ - [ ] Footnote management UI in EditorJS
632
+ - [ ] Smart position recalculation when text changes
633
+
634
+ ## License
635
+
636
+ This documentation is part of Panda Editor, available under the BSD-3-Clause License.
637
+
638
+ ## Contributing
639
+
640
+ Found a bug or have a suggestion? Please open an issue on GitHub at https://github.com/tastybamboo/panda-editor.
@@ -8,8 +8,46 @@ module Panda
8
8
  content = sanitize(data["text"])
9
9
  return "" if content.blank?
10
10
 
11
+ content = inject_footnotes(content) if data["footnotes"].present?
12
+
11
13
  html_safe("<p>#{content}</p>")
12
14
  end
15
+
16
+ private
17
+
18
+ def inject_footnotes(text)
19
+ return text unless data["footnotes"].is_a?(Array)
20
+
21
+ # Sort footnotes by position in descending order to avoid position shifts
22
+ footnotes = data["footnotes"].sort_by { |fn| -fn["position"].to_i }
23
+
24
+ footnotes.each do |footnote|
25
+ position = footnote["position"].to_i
26
+ # Skip if position is beyond text length
27
+ next if position < 0 || position > text.length
28
+
29
+ # Register footnote with renderer's footnote registry
30
+ footnote_number = register_footnote(footnote)
31
+ next unless footnote_number
32
+
33
+ # Create footnote marker
34
+ marker = "<sup id=\"fnref:#{footnote_number}\"><a href=\"#fn:#{footnote_number}\" class=\"footnote\">#{footnote_number}</a></sup>"
35
+
36
+ # Insert marker at position
37
+ text = text.insert(position, marker)
38
+ end
39
+
40
+ text
41
+ end
42
+
43
+ def register_footnote(footnote)
44
+ return nil unless options[:footnote_registry]
45
+
46
+ options[:footnote_registry].add(
47
+ id: footnote["id"],
48
+ content: footnote["content"]
49
+ )
50
+ end
13
51
  end
14
52
  end
15
53
  end
@@ -36,11 +36,13 @@ module Panda
36
36
  end
37
37
 
38
38
  def generate_cached_content
39
+ renderer_options = {autolink_urls: true}
40
+
39
41
  if content.is_a?(String)
40
42
  begin
41
43
  parsed_content = JSON.parse(content)
42
44
  self.cached_content = if parsed_content.is_a?(Hash) && parsed_content["blocks"].present?
43
- Panda::Editor::Renderer.new(parsed_content).render
45
+ Panda::Editor::Renderer.new(parsed_content, renderer_options).render
44
46
  else
45
47
  content
46
48
  end
@@ -50,7 +52,7 @@ module Panda
50
52
  end
51
53
  elsif content.is_a?(Hash) && content["blocks"].present?
52
54
  # Process EditorJS content
53
- self.cached_content = Panda::Editor::Renderer.new(content).render
55
+ self.cached_content = Panda::Editor::Renderer.new(content, renderer_options).render
54
56
  else
55
57
  # For any other case, store as is
56
58
  self.cached_content = content.to_s