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 +7 -0
- data/CHANGELOG.md +38 -0
- data/LICENSE.txt +21 -0
- data/README.md +648 -0
- data/lib/bbortcodes/config.rb +42 -0
- data/lib/bbortcodes/context.rb +84 -0
- data/lib/bbortcodes/errors.rb +9 -0
- data/lib/bbortcodes/grammar.rb +81 -0
- data/lib/bbortcodes/parser.rb +224 -0
- data/lib/bbortcodes/registry.rb +95 -0
- data/lib/bbortcodes/shortcode.rb +166 -0
- data/lib/bbortcodes/transform.rb +101 -0
- data/lib/bbortcodes/version.rb +5 -0
- data/lib/bbortcodes.rb +57 -0
- metadata +158 -0
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><script>alert(1)</script></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).
|