bbortcodes 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 84f733c21d25ceaaf619d48918acad7c2b9ce8591d4c377229f96d938dff75b9
4
+ data.tar.gz: cb0c54df42dc99db3dc621ae75470b1700e91d03193658669f7f50f770f79ef4
5
+ SHA512:
6
+ metadata.gz: be9a9d74d3cc2962c24d26acf3bb7a0450bfb6cc51f44cd334e5f65b12e47eb8795cf2ab83d2a1414cd2028113a7e8cf735c9bda5e3bd2192848fbe3e2195b9c
7
+ data.tar.gz: 0641d34d28eb37e3d12b7915d053308fd5e3352d609b1d9b290824a18c5ca2e48257e177735fac9afefe99cb65e8ff50babbf5e63ee6cf60aeca107709039ee4
data/CHANGELOG.md ADDED
@@ -0,0 +1,38 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2025-10-15
11
+
12
+ ### Added
13
+ - Initial release
14
+ - Grammar-based parsing using Parslet
15
+ - Type safety with Literal gem
16
+ - Support for self-closing shortcodes
17
+ - Support for paired shortcodes with content
18
+ - Support for shortcode attributes
19
+ - Support for nested shortcodes with configurable depth limits
20
+ - Configurable child shortcode validation
21
+ - Shared rendering context with thread-safe operations
22
+ - Thread-safe global registry with overwrite protection
23
+ - Flexible error handling via Anyway Config
24
+ - Comprehensive documentation and examples
25
+
26
+ ### Security
27
+ - **XSS Protection**: Automatic HTML escaping for attribute values via `escape_html()` method
28
+ - Configurable via `auto_escape_attributes` setting (default: true)
29
+ - Per-attribute opt-out with `escape: false` parameter
30
+ - **DoS Protection**: Input size limits to prevent memory exhaustion
31
+ - Configurable via `max_input_length` setting (default: 1MB)
32
+ - **ReDoS Protection**: Parse timeout to prevent Regular Expression Denial of Service
33
+ - Configurable via `parse_timeout` setting (default: 5 seconds)
34
+ - **Stack Overflow Protection**: Maximum nesting depth enforcement
35
+ - Configurable via `max_nesting_depth` setting (default: 50 levels)
36
+ - **Registry Integrity**: Protection against accidental shortcode overwrites
37
+ - Configurable via `allow_shortcode_overwrite` setting (default: false)
38
+ - **Thread Safety**: All registry and context operations are thread-safe using Monitor
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Bbortcodes Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,648 @@
1
+ # BBortcodes
2
+
3
+ A state-of-the-art Ruby gem for parsing WordPress-like shortcodes with grammar-based parsing, type safety, and support for nested shortcodes.
4
+
5
+ ## Features
6
+
7
+ - **Grammar-based parsing** using [Parslet](https://github.com/kschiess/parslet) - no fragile regex matching
8
+ - **Type safety** with [Literal](https://github.com/joeldrapper/literal) for runtime type checking
9
+ - **Nested shortcodes** with configurable child validation
10
+ - **Self-closing and paired shortcodes** support
11
+ - **Named attributes** with quoted values
12
+ - **Shared rendering context** for state management across shortcodes
13
+ - **Flexible error handling** via [Anyway Config](https://github.com/palkan/anyway_config)
14
+ - **Pure Ruby** - no Rails dependencies required
15
+ - **Thread-safe** shortcode registry
16
+
17
+ ## Installation
18
+
19
+ Add this line to your application's Gemfile:
20
+
21
+ ```ruby
22
+ gem 'bbortcodes'
23
+ ```
24
+
25
+ And then execute:
26
+
27
+ ```bash
28
+ bundle install
29
+ ```
30
+
31
+ Or install it yourself as:
32
+
33
+ ```bash
34
+ gem install bbortcodes
35
+ ```
36
+
37
+ ## Quick Start
38
+
39
+ ### 1. Define a Shortcode
40
+
41
+ ```ruby
42
+ class YoutubeShortcode < BBortcodes::Shortcode
43
+ def self.tag_name
44
+ "youtube"
45
+ end
46
+
47
+ def render(context)
48
+ url = attribute("url")
49
+ "<iframe src=\"#{url}\" frameborder=\"0\"></iframe>"
50
+ end
51
+ end
52
+ ```
53
+
54
+ ### 2. Register the Shortcode
55
+
56
+ ```ruby
57
+ BBortcodes.register(YoutubeShortcode)
58
+ ```
59
+
60
+ ### 3. Parse Text
61
+
62
+ ```ruby
63
+ text = "Check out this video: [youtube url=\"https://youtube.com/watch?v=abc\"]"
64
+ output, shortcodes = BBortcodes.parse(text)
65
+
66
+ puts output
67
+ # => "Check out this video: <iframe src=\"https://youtube.com/watch?v=abc\" frameborder=\"0\"></iframe>"
68
+
69
+ puts shortcodes.length
70
+ # => 1
71
+
72
+ puts shortcodes.first.class
73
+ # => YoutubeShortcode
74
+ ```
75
+
76
+ ## Usage
77
+
78
+ ### Defining Shortcodes
79
+
80
+ All shortcodes inherit from `BBortcodes::Shortcode` and use Literal for type safety:
81
+
82
+ ```ruby
83
+ class BoldShortcode < BBortcodes::Shortcode
84
+ # Optional: customize the tag name (defaults to snake_case of class name)
85
+ def self.tag_name
86
+ "bold"
87
+ end
88
+
89
+ # Required: implement the render method
90
+ def render(context)
91
+ content_text = render_content(context, nil)
92
+ "<strong>#{content_text}</strong>"
93
+ end
94
+ end
95
+ ```
96
+
97
+ ### Types of Shortcodes
98
+
99
+ #### Self-Closing Shortcodes
100
+
101
+ ```ruby
102
+ # Usage: [image url="photo.jpg" alt="A photo"]
103
+
104
+ class ImageShortcode < BBortcodes::Shortcode
105
+ def self.tag_name
106
+ "image"
107
+ end
108
+
109
+ def render(context)
110
+ url = attribute("url")
111
+ alt = attribute("alt", "")
112
+ "<img src=\"#{url}\" alt=\"#{alt}\" />"
113
+ end
114
+ end
115
+ ```
116
+
117
+ #### Paired Shortcodes with Content
118
+
119
+ ```ruby
120
+ # Usage: [bold]This is bold text[/bold]
121
+
122
+ class BoldShortcode < BBortcodes::Shortcode
123
+ def self.tag_name
124
+ "bold"
125
+ end
126
+
127
+ def render(context)
128
+ content_text = render_content(context, nil)
129
+ "<strong>#{content_text}</strong>"
130
+ end
131
+ end
132
+ ```
133
+
134
+ #### Shortcodes with Attributes and Content
135
+
136
+ ```ruby
137
+ # Usage: [button url="/signup" color="blue"]Sign Up[/button]
138
+
139
+ class ButtonShortcode < BBortcodes::Shortcode
140
+ def self.tag_name
141
+ "button"
142
+ end
143
+
144
+ def render(context)
145
+ url = attribute("url", "#")
146
+ color = attribute("color", "primary")
147
+ text = render_content(context, nil)
148
+
149
+ "<a href=\"#{url}\" class=\"btn btn-#{color}\">#{text}</a>"
150
+ end
151
+ end
152
+ ```
153
+
154
+ ### Nested Shortcodes
155
+
156
+ Control which shortcodes can be nested within others:
157
+
158
+ ```ruby
159
+ # Gallery that only allows Image children
160
+ class GalleryShortcode < BBortcodes::Shortcode
161
+ def self.tag_name
162
+ "gallery"
163
+ end
164
+
165
+ # Only allow ImageShortcode children
166
+ def self.allowed_children
167
+ [ImageShortcode]
168
+ end
169
+
170
+ def render(context)
171
+ content_html = render_content(context, nil)
172
+ "<div class=\"gallery\">#{content_html}</div>"
173
+ end
174
+ end
175
+
176
+ # Allow any children
177
+ class QuoteShortcode < BBortcodes::Shortcode
178
+ def self.tag_name
179
+ "quote"
180
+ end
181
+
182
+ def self.allowed_children
183
+ :all # Allow any nested shortcodes
184
+ end
185
+
186
+ def render(context)
187
+ content_html = render_content(context, nil)
188
+ author = attribute("author")
189
+
190
+ html = "<blockquote>#{content_html}"
191
+ html += "<cite>#{author}</cite>" if author
192
+ html += "</blockquote>"
193
+ html
194
+ end
195
+ end
196
+
197
+ # Allow no children (content is plain text only)
198
+ class CodeShortcode < BBortcodes::Shortcode
199
+ def self.tag_name
200
+ "code"
201
+ end
202
+
203
+ def self.allowed_children
204
+ [] # No nested shortcodes allowed
205
+ end
206
+
207
+ def render(context)
208
+ # Content will only be plain text
209
+ content_text = render_content(context, nil)
210
+ "<code>#{content_text}</code>"
211
+ end
212
+ end
213
+ ```
214
+
215
+ ### Using Context for Shared State
216
+
217
+ The context allows shortcodes to share state during rendering:
218
+
219
+ ```ruby
220
+ class NumberedItemShortcode < BBortcodes::Shortcode
221
+ def self.tag_name
222
+ "item"
223
+ end
224
+
225
+ def render(context)
226
+ # Increment a counter
227
+ number = context.increment(:item_counter)
228
+
229
+ content_text = render_content(context, nil)
230
+ "<div class=\"item\">#{number}. #{content_text}</div>"
231
+ end
232
+ end
233
+
234
+ # Usage
235
+ context = BBortcodes::Context.new
236
+ text = "[item]First[/item] [item]Second[/item] [item]Third[/item]"
237
+ output, _ = BBortcodes.parse(text, context: context)
238
+
239
+ # Output:
240
+ # <div class="item">1. First</div> <div class="item">2. Second</div> <div class="item">3. Third</div>
241
+ ```
242
+
243
+ ### Placeholder Pattern (API Use Case)
244
+
245
+ For APIs that need to separate shortcode data from content, you can use the returned `shortcodes` array:
246
+
247
+ ```ruby
248
+ class VideoShortcode < BBortcodes::Shortcode
249
+ def self.tag_name
250
+ "video"
251
+ end
252
+
253
+ def render(context)
254
+ # Generate unique placeholder ID using context counter
255
+ id = context.increment(:shortcode)
256
+ "{{SHORTCODE-#{id}}}"
257
+ end
258
+ end
259
+
260
+ # Usage
261
+ context = BBortcodes::Context.new
262
+ text = "Watch this: [video url=\"video.mp4\" thumbnail=\"thumb.jpg\"]"
263
+ output, shortcodes = BBortcodes.parse(text, context: context)
264
+
265
+ puts output
266
+ # => "Watch this: {{SHORTCODE-1}}"
267
+
268
+ # Build API response data directly from the shortcodes array
269
+ shortcodes_data = {}
270
+ shortcodes.each_with_index do |shortcode, index|
271
+ id = index + 1
272
+ shortcodes_data["SHORTCODE-#{id}"] = {
273
+ type: shortcode.name,
274
+ attributes: shortcode.attributes,
275
+ self_closing: shortcode.self_closing
276
+ }
277
+ end
278
+
279
+ # In your API response:
280
+ {
281
+ content: output,
282
+ shortcodes: shortcodes_data
283
+ }
284
+ # => {
285
+ # content: "Watch this: {{SHORTCODE-1}}",
286
+ # shortcodes: {
287
+ # "SHORTCODE-1" => {
288
+ # type: "video",
289
+ # attributes: {"url" => "video.mp4", "thumbnail" => "thumb.jpg"},
290
+ # self_closing: true
291
+ # }
292
+ # }
293
+ # }
294
+ ```
295
+
296
+ Note: The `context` object can still be used for shared state (like the counter above) or for passing data between nested shortcodes, but for API responses, it's better to use the returned `shortcodes` array to access all shortcode data.
297
+
298
+ ### Filtering Shortcodes
299
+
300
+ Process only specific shortcode types using the `only:` option:
301
+
302
+ ```ruby
303
+ text = "Text with [youtube url=\"video.mp4\"] and [button url=\"/click\"]Click[/button]"
304
+
305
+ # Only process youtube shortcodes, leave others as-is
306
+ output, shortcodes = BBortcodes.parse(text, only: ["youtube"])
307
+
308
+ puts output
309
+ # => "Text with <iframe...> and [button url=\"/click\"]Click[/button]"
310
+
311
+ puts shortcodes.length
312
+ # => 1 (only youtube was processed)
313
+ ```
314
+
315
+ ## Security
316
+
317
+ BBortcodes includes multiple security features to protect against common attacks:
318
+
319
+ ### HTML Escaping (XSS Protection)
320
+
321
+ By default, all attribute values are automatically HTML-escaped to prevent Cross-Site Scripting (XSS) attacks:
322
+
323
+ ```ruby
324
+ text = '[quote author="<script>alert(1)</script>"]Hello[/quote]'
325
+ output, _ = BBortcodes.parse(text)
326
+
327
+ # Output: <blockquote>Hello<cite>&lt;script&gt;alert(1)&lt;/script&gt;</cite></blockquote>
328
+ # The script tag is escaped and won't execute
329
+ ```
330
+
331
+ You can disable escaping for specific attributes when you need raw HTML:
332
+
333
+ ```ruby
334
+ class MyShortcode < BBortcodes::Shortcode
335
+ def render(context)
336
+ # Escaped by default
337
+ safe_value = attribute("safe_attr")
338
+
339
+ # Explicitly disable escaping for this attribute
340
+ raw_html = attribute("html_attr", escape: false)
341
+
342
+ # Or use escape_html() helper manually
343
+ user_input = escape_html(attribute("user_attr", escape: false))
344
+ end
345
+ end
346
+ ```
347
+
348
+ **Warning**: Only disable escaping when you fully control the input source. Never disable escaping for user-provided content.
349
+
350
+ ### Input Size Limits
351
+
352
+ BBortcodes limits the maximum input size to prevent memory exhaustion:
353
+
354
+ ```ruby
355
+ BBortcodes.configure do |config|
356
+ config.max_input_length = 1_000_000 # 1MB default
357
+ end
358
+
359
+ # Inputs larger than the limit will raise BBortcodes::ParseError
360
+ ```
361
+
362
+ ### Parse Timeout Protection
363
+
364
+ Parsing is protected by a timeout to prevent ReDoS (Regular Expression Denial of Service) attacks:
365
+
366
+ ```ruby
367
+ BBortcodes.configure do |config|
368
+ config.parse_timeout = 5 # 5 seconds default
369
+ end
370
+
371
+ # Complex inputs that take too long to parse will raise BBortcodes::ParseError
372
+ ```
373
+
374
+ ### Nesting Depth Limits
375
+
376
+ Maximum nesting depth prevents stack overflow from deeply nested shortcodes:
377
+
378
+ ```ruby
379
+ BBortcodes.configure do |config|
380
+ config.max_nesting_depth = 50 # default
381
+ end
382
+
383
+ # Deeply nested shortcodes exceeding the limit will raise BBortcodes::ParseError
384
+ ```
385
+
386
+ ### Registry Overwrite Protection
387
+
388
+ By default, shortcodes cannot be silently overwritten in the registry:
389
+
390
+ ```ruby
391
+ BBortcodes.register(MyShortcode)
392
+ BBortcodes.register(MyShortcode) # Raises BBortcodes::RegistryError
393
+
394
+ # Allow overwrites if needed
395
+ BBortcodes.configure do |config|
396
+ config.allow_shortcode_overwrite = true
397
+ end
398
+ ```
399
+
400
+ ### Thread Safety
401
+
402
+ Both the Registry and Context classes are thread-safe using Ruby's Monitor:
403
+
404
+ ```ruby
405
+ # Safe to use across threads
406
+ context = BBortcodes::Context.new
407
+
408
+ threads = 10.times.map do
409
+ Thread.new do
410
+ context.increment(:counter)
411
+ end
412
+ end
413
+
414
+ threads.each(&:join)
415
+ puts context.counter(:counter) # Reliably outputs 10
416
+ ```
417
+
418
+ ### Security Best Practices
419
+
420
+ 1. **Always escape user input**: Keep `auto_escape_attributes: true` (default)
421
+ 2. **Set appropriate limits**: Adjust `max_input_length`, `parse_timeout`, and `max_nesting_depth` based on your use case
422
+ 3. **Validate shortcode sources**: Only parse content from trusted sources or properly sanitize user input
423
+ 4. **Use `only:` filter**: When parsing untrusted content, use the `only:` parameter to limit which shortcodes can be processed
424
+ 5. **Audit custom shortcodes**: Ensure your custom shortcode `render()` methods properly escape output
425
+
426
+ ## Configuration
427
+
428
+ Configure error handling and security settings:
429
+
430
+ ```ruby
431
+ BBortcodes.configure do |config|
432
+ # Error handling
433
+ # ---------------
434
+
435
+ # How to handle parse errors for unknown shortcodes
436
+ # Options: :raise, :skip (leave as-is), :strip (remove)
437
+ config.on_parse_error = :raise # default
438
+
439
+ # How to handle disallowed nested shortcodes
440
+ # Options: :raise, :skip (leave as-is), :strip (remove)
441
+ config.on_disallowed_child = :raise # default
442
+
443
+ # Validate shortcode classes on registration
444
+ config.validate_on_register = true # default
445
+
446
+ # Security settings
447
+ # -----------------
448
+
449
+ # Automatically escape HTML in attribute values to prevent XSS
450
+ config.auto_escape_attributes = true # default (recommended)
451
+
452
+ # Maximum input text size in bytes (default: 1MB)
453
+ config.max_input_length = 1_000_000
454
+
455
+ # Parse timeout in seconds to prevent ReDoS attacks
456
+ config.parse_timeout = 5
457
+
458
+ # Maximum nesting depth for shortcodes to prevent stack overflow
459
+ config.max_nesting_depth = 50
460
+
461
+ # Allow overwriting existing shortcodes in the registry
462
+ config.allow_shortcode_overwrite = false # default (recommended)
463
+ end
464
+ ```
465
+
466
+ #### Example: Skip Unknown Shortcodes
467
+
468
+ ```ruby
469
+ BBortcodes.configure do |config|
470
+ config.on_parse_error = :skip
471
+ end
472
+
473
+ text = "Text with [unknown]shortcode[/unknown]"
474
+ output, _ = BBortcodes.parse(text)
475
+
476
+ puts output
477
+ # => "Text with [unknown]shortcode[/unknown]"
478
+ ```
479
+
480
+ #### Example: Strip Disallowed Children
481
+
482
+ ```ruby
483
+ BBortcodes.configure do |config|
484
+ config.on_disallowed_child = :strip
485
+ end
486
+
487
+ # Assuming GalleryShortcode only allows ImageShortcode children
488
+ text = "[gallery][image url=\"1.jpg\"][bold]Invalid[/bold][/gallery]"
489
+ output, _ = BBortcodes.parse(text)
490
+
491
+ puts output
492
+ # => "<div class=\"gallery\"><img src=\"1.jpg\" /></div>"
493
+ # The [bold] shortcode was stripped
494
+ ```
495
+
496
+ ### Environment-based Configuration
497
+
498
+ Using [Anyway Config](https://github.com/palkan/anyway_config), you can configure via environment variables or YAML:
499
+
500
+ ```bash
501
+ # Environment variables
502
+ BBORTCODES_ON_PARSE_ERROR=skip
503
+ BBORTCODES_ON_DISALLOWED_CHILD=strip
504
+ ```
505
+
506
+ Or via `config/bbortcodes.yml`:
507
+
508
+ ```yaml
509
+ production:
510
+ on_parse_error: skip
511
+ on_disallowed_child: skip
512
+
513
+ development:
514
+ on_parse_error: raise
515
+ on_disallowed_child: raise
516
+ ```
517
+
518
+ ## Advanced Usage
519
+
520
+ ### Custom Parser Instance
521
+
522
+ Create isolated parser instances with custom registries:
523
+
524
+ ```ruby
525
+ # Create a custom registry
526
+ registry = BBortcodes::Registry.new
527
+ registry.register(MyShortcode)
528
+
529
+ # Create a parser with custom registry
530
+ parser = BBortcodes::Parser.new(registry: registry)
531
+ output, shortcodes = parser.parse(text)
532
+ ```
533
+
534
+ ### Accessing Attributes
535
+
536
+ Several helper methods are available for working with attributes:
537
+
538
+ ```ruby
539
+ class MyShortcode < BBortcodes::Shortcode
540
+ def render(context)
541
+ # Get attribute with default value
542
+ color = attribute("color", "blue")
543
+
544
+ # Check if attribute exists
545
+ if has_attribute?("url")
546
+ url = attribute("url")
547
+ end
548
+
549
+ # Access all attributes
550
+ attributes.each do |key, value|
551
+ puts "#{key}: #{value}"
552
+ end
553
+ end
554
+ end
555
+ ```
556
+
557
+ ### Working with Content
558
+
559
+ ```ruby
560
+ class WrapperShortcode < BBortcodes::Shortcode
561
+ def render(context)
562
+ # Check if shortcode has content
563
+ if content.nil? || content.empty?
564
+ return "<div>No content</div>"
565
+ end
566
+
567
+ # Render nested content
568
+ content_html = render_content(context, nil)
569
+
570
+ "<div class=\"wrapper\">#{content_html}</div>"
571
+ end
572
+ end
573
+ ```
574
+
575
+ ## Architecture
576
+
577
+ ### Grammar-based Parsing
578
+
579
+ BBortcodes uses [Parslet](https://github.com/kschiess/parslet), a PEG (Parsing Expression Grammar) parser, instead of regular expressions. This provides:
580
+
581
+ - **Robust parsing** of complex nested structures
582
+ - **Better error messages** when syntax is invalid
583
+ - **Composable grammar rules** for maintainability
584
+ - **Unambiguous parsing** of edge cases
585
+
586
+ ### Type Safety
587
+
588
+ Using [Literal](https://github.com/joeldrapper/literal), shortcode properties are type-checked at runtime:
589
+
590
+ ```ruby
591
+ shortcode = MyShortcode.new(
592
+ name: "my_shortcode",
593
+ attributes: {"key" => "value"},
594
+ content: [],
595
+ self_closing: false
596
+ )
597
+
598
+ # Properties are type-checked
599
+ shortcode.name # => String
600
+ shortcode.attributes # => Hash
601
+ shortcode.content # => Array
602
+ shortcode.self_closing # => Boolean
603
+ ```
604
+
605
+ ### Thread Safety
606
+
607
+ The global registry uses a Monitor for thread-safe concurrent access:
608
+
609
+ ```ruby
610
+ # Safe to use across threads
611
+ Thread.new { BBortcodes.register(Shortcode1) }
612
+ Thread.new { BBortcodes.register(Shortcode2) }
613
+ ```
614
+
615
+ ## Development
616
+
617
+ After checking out the repo, run:
618
+
619
+ ```bash
620
+ bundle install
621
+ ```
622
+
623
+ Run the examples:
624
+
625
+ ```bash
626
+ ruby examples/basic_shortcodes.rb
627
+ ruby examples/error_handling.rb
628
+ ```
629
+
630
+ Run tests:
631
+
632
+ ```bash
633
+ bundle exec rspec
634
+ ```
635
+
636
+ Run linter:
637
+
638
+ ```bash
639
+ bundle exec standardrb
640
+ ```
641
+
642
+ ## Contributing
643
+
644
+ Bug reports and pull requests are welcome on GitHub at https://github.com/yourusername/bbortcodes.
645
+
646
+ ## License
647
+
648
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).