mdphlex 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: 30fde45581cd7d2edeada97dd0c281cbf4bf6447e6938f5002ad99231c4ac2f0
4
+ data.tar.gz: 1168f4d22457f7965160e4248f468d7bce4e0a45eb56a44b6285cb3aebe94d83
5
+ SHA512:
6
+ metadata.gz: 8aab63e64b863d0fdba508d8e9d2a570eb7b8bc8d9c11081cf4b8718a9c06365a6069df65024a2c868efd6bc60e1163f4adf20251f220f7c63d0d6d4b4ffd05b
7
+ data.tar.gz: 66efd4524781b9fcc52568879f352285db7477f078039c40f1c209c09e13d63c3dc0c24f95c20876839203a68982d79166bf125d54abe4017dc6c9c3c2008e69
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2025-09-03
4
+
5
+ - Initial release
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Martin Emde
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,123 @@
1
+ # MDPhlex
2
+
3
+ MDPhlex is a Phlex component for rendering Markdown. Write your Markdown in Ruby with Phlex's familiar syntax and render it as a Markdown string.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'mdphlex'
11
+ ```
12
+
13
+ ## Basic Usage
14
+
15
+ Create an MDPhlex::MD component just like you would create a Phlex component:
16
+
17
+ ```ruby
18
+ class HelloWorld < MDPhlex::MD
19
+ def view_template
20
+ h1 "Hello, World!"
21
+
22
+ p "Welcome to MDPhlex - writing Markdown with Phlex syntax!"
23
+
24
+ h2 "Features"
25
+
26
+ ul do
27
+ li "Write Markdown using Ruby"
28
+ li do
29
+ plain "Support for "
30
+ strong "bold"
31
+ plain ", "
32
+ em "italic"
33
+ plain ", and "
34
+ code "inline code"
35
+ end
36
+ li "Full Phlex component composition"
37
+ end
38
+
39
+ p do
40
+ plain "Check out the "
41
+ a(href: "https://github.com/martinemde/mdphlex") { "MDPhlex repository" }
42
+ plain " for more information."
43
+ end
44
+ end
45
+ end
46
+
47
+ # Render the component
48
+ puts HelloWorld.new.call
49
+ ```
50
+
51
+ This outputs the following Markdown:
52
+
53
+ ```markdown
54
+ # Hello, World!
55
+
56
+ Welcome to MDPhlex - writing Markdown with Phlex syntax!
57
+
58
+ ## Features
59
+
60
+ - Write Markdown using Ruby
61
+ - Support for **bold**, *italic*, and `inline code`
62
+ - Full Phlex component composition
63
+
64
+ Check out the [MDPhlex repository](https://github.com/martinemde/mdphlex) for more information.
65
+ ```
66
+
67
+ ## Rendering MDPhlex inside Phlex::HTML
68
+
69
+ MDPhlex components can be seamlessly integrated into your Phlex::HTML views:
70
+
71
+ ```ruby
72
+ class ArticlePage < Phlex::HTML
73
+ def initialize(article)
74
+ @article = article
75
+ end
76
+
77
+ def view_template
78
+ html do
79
+ head do
80
+ title { @article.title }
81
+ end
82
+ body do
83
+ article do
84
+ # Render MDPhlex component inside HTML
85
+ div(class: "markdown-content") do
86
+ plain render(ArticleContent.new(@article))
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+
94
+ class ArticleContent < MDPhlex::MD
95
+ def initialize(article)
96
+ @article = article
97
+ end
98
+
99
+ def view_template
100
+ h1 @article.title
101
+
102
+ p @article.summary
103
+
104
+ h2 "Contents"
105
+
106
+ @article.sections.each do |section|
107
+ h3 section.title
108
+ p section.content
109
+ end
110
+ end
111
+ end
112
+ ```
113
+
114
+ ## Why MDPhlex?
115
+
116
+ - **Component-based**: Build reusable Markdown components
117
+ - **Type-safe**: Get Ruby's type checking and IDE support
118
+ - **Composable**: Mix Phlex::HTML and MDPhlex components freely
119
+ - **Familiar**: Uses the same syntax as Phlex
120
+
121
+ ## License
122
+
123
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+
5
+ require "rubocop/rake_task"
6
+
7
+ RuboCop::RakeTask.new
8
+
9
+ task default: %i[rubocop]
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ Bundler.require :test
4
+
5
+ require "mdphlex"
data/lib/mdphlex/md.rb ADDED
@@ -0,0 +1,303 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "phlex"
4
+
5
+ module MDPhlex
6
+ class MD < Phlex::SGML
7
+ extend Phlex::SGML::Elements
8
+
9
+ # Register a block-level custom element that ensures proper newlines
10
+ def self.register_block_element(method_name, tag: method_name.to_s.tr("_", "-"))
11
+ define_method(method_name) do |**attributes, &block|
12
+ state = @_state
13
+ buffer = state.buffer
14
+
15
+ unless state.should_render?
16
+ __yield_content__(&block) if block
17
+ return nil
18
+ end
19
+
20
+ # Render the opening tag - always followed by newline
21
+ buffer << "<#{tag}"
22
+
23
+ if attributes.length > 0
24
+ attributes.each do |key, value|
25
+ buffer << " #{key}=\"#{Phlex::Escape.html_escape(value.to_s)}\""
26
+ end
27
+ end
28
+
29
+ buffer << ">\n"
30
+
31
+ # Render content if block given
32
+ if block
33
+ __yield_content__(&block)
34
+ end
35
+
36
+ # Render closing tag - always followed by newline for block elements
37
+ buffer << "</#{tag}>\n"
38
+
39
+ nil
40
+ end
41
+ end
42
+
43
+ def h1(content = nil, &)
44
+ heading(1, content, &)
45
+ end
46
+
47
+ def h2(content = nil, &)
48
+ heading(2, content, &)
49
+ end
50
+
51
+ def h3(content = nil, &)
52
+ heading(3, content, &)
53
+ end
54
+
55
+ def h4(content = nil, &)
56
+ heading(4, content, &)
57
+ end
58
+
59
+ def h5(content = nil, &)
60
+ heading(5, content, &)
61
+ end
62
+
63
+ def h6(content = nil, &)
64
+ heading(6, content, &)
65
+ end
66
+
67
+ def p(content = nil, &)
68
+ state = @_state
69
+ return unless state.should_render?
70
+
71
+ buffer = state.buffer
72
+
73
+ if block_given?
74
+ __yield_content__(&)
75
+ elsif content
76
+ buffer << content.to_s
77
+ end
78
+
79
+ buffer << "\n\n"
80
+ nil
81
+ end
82
+
83
+ def strong(content = nil, &)
84
+ wrap_inline("**", content, &)
85
+ end
86
+
87
+ def em(content = nil, &)
88
+ wrap_inline("*", content, &)
89
+ end
90
+
91
+ def code(content = nil, &)
92
+ wrap_inline("`", content, &)
93
+ end
94
+
95
+ def a(href: nil, **attributes, &)
96
+ state = @_state
97
+ return unless state.should_render?
98
+
99
+ buffer = state.buffer
100
+ buffer << "["
101
+
102
+ __yield_content__(&) if block_given?
103
+
104
+ buffer << "]("
105
+ buffer << href.to_s if href
106
+ buffer << ")"
107
+ nil
108
+ end
109
+
110
+ def plain(content)
111
+ state = @_state
112
+ return unless state.should_render?
113
+
114
+ state.buffer << content.to_s
115
+ nil
116
+ end
117
+
118
+ def blockquote(content = nil, &)
119
+ state = @_state
120
+ return unless state.should_render?
121
+
122
+ buffer = state.buffer
123
+
124
+ if block_given?
125
+ # Save current position to capture content
126
+ start_pos = buffer.length
127
+ __yield_content__(&)
128
+ # Extract the content that was added
129
+ captured = buffer[start_pos..]
130
+ # Remove it from buffer to re-add with > prefix
131
+ buffer.slice!(start_pos..)
132
+
133
+ # Add > prefix to each line
134
+ captured.lines.each do |line|
135
+ buffer << "> " << line
136
+ end
137
+ else
138
+ content.to_s.lines.each do |line|
139
+ buffer << "> " << line
140
+ end
141
+ end
142
+
143
+ buffer << "\n\n"
144
+ nil
145
+ end
146
+
147
+ def pre(language: nil, &)
148
+ state = @_state
149
+ return unless state.should_render?
150
+
151
+ buffer = state.buffer
152
+ buffer << "```"
153
+ buffer << language.to_s if language
154
+ buffer << "\n"
155
+
156
+ __yield_content__(&) if block_given?
157
+
158
+ buffer << "\n```\n\n"
159
+ nil
160
+ end
161
+
162
+ def ul(&)
163
+ state = @_state
164
+ return unless state.should_render?
165
+
166
+ buffer = state.buffer
167
+ @_list_type ||= []
168
+
169
+ # If we're in a list item and there's already content, add a newline
170
+ buffer << "\n" if @_list_type.length > 0 && buffer.length > 0 && !buffer.end_with?("\n")
171
+
172
+ @_list_type.push(:ul)
173
+
174
+ __yield_content__(&) if block_given?
175
+
176
+ @_list_type.pop
177
+ # Only add extra newline at the end of top-level lists
178
+ buffer << "\n" if @_list_type.empty?
179
+ nil
180
+ end
181
+
182
+ def ol(&)
183
+ state = @_state
184
+ return unless state.should_render?
185
+
186
+ buffer = state.buffer
187
+ @_list_type ||= []
188
+
189
+ # If we're in a list item and there's already content, add a newline
190
+ buffer << "\n" if @_list_type.length > 0 && buffer.length > 0 && !buffer.end_with?("\n")
191
+
192
+ @_list_type.push(:ol)
193
+ @_ol_counter ||= []
194
+ @_ol_counter.push(0)
195
+
196
+ __yield_content__(&) if block_given?
197
+
198
+ @_list_type.pop
199
+ @_ol_counter.pop
200
+ # Only add extra newline at the end of top-level lists
201
+ buffer << "\n" if @_list_type.empty?
202
+ nil
203
+ end
204
+
205
+ def li(content = nil, &)
206
+ state = @_state
207
+ return unless state.should_render?
208
+
209
+ buffer = state.buffer
210
+ @_list_type ||= []
211
+ list_depth = @_list_type.length
212
+
213
+ # Add indentation for nested lists
214
+ buffer << (" " * [list_depth - 1, 0].max)
215
+
216
+ # Add list marker
217
+ if @_list_type.last == :ol
218
+ @_ol_counter ||= []
219
+ @_ol_counter[-1] += 1
220
+ buffer << "#{@_ol_counter.last}. "
221
+ else
222
+ buffer << "- "
223
+ end
224
+
225
+ if block_given?
226
+ # Mark position before yielding content
227
+ start_pos = buffer.length
228
+ __yield_content__(&)
229
+ # Only add newline if we haven't already ended with one
230
+ # (nested lists already add their trailing newline)
231
+ buffer << "\n" unless buffer.end_with?("\n")
232
+ else
233
+ buffer << content.to_s if content
234
+ buffer << "\n"
235
+ end
236
+
237
+ nil
238
+ end
239
+
240
+ def img(src: nil, alt: nil, **attributes)
241
+ state = @_state
242
+ return unless state.should_render?
243
+
244
+ buffer = state.buffer
245
+ buffer << "!["
246
+ buffer << alt.to_s if alt
247
+ buffer << "]("
248
+ buffer << src.to_s if src
249
+ buffer << ")"
250
+ nil
251
+ end
252
+
253
+ def hr(**attributes)
254
+ state = @_state
255
+ return unless state.should_render?
256
+
257
+ state.buffer << "---\n\n"
258
+ nil
259
+ end
260
+
261
+ def br
262
+ state = @_state
263
+ return unless state.should_render?
264
+
265
+ state.buffer << " \n"
266
+ nil
267
+ end
268
+
269
+ private def heading(level, content = nil, &)
270
+ state = @_state
271
+ return unless state.should_render?
272
+
273
+ buffer = state.buffer
274
+ buffer << ("#" * level) << " "
275
+
276
+ if block_given?
277
+ __yield_content__(&)
278
+ elsif content
279
+ buffer << content.to_s
280
+ end
281
+
282
+ buffer << "\n"
283
+ nil
284
+ end
285
+
286
+ private def wrap_inline(marker, content = nil, &)
287
+ state = @_state
288
+ return unless state.should_render?
289
+
290
+ buffer = state.buffer
291
+ buffer << marker
292
+
293
+ if block_given?
294
+ __yield_content__(&)
295
+ elsif content
296
+ buffer << content.to_s
297
+ end
298
+
299
+ buffer << marker
300
+ nil
301
+ end
302
+ end
303
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MDPhlex
4
+ VERSION = "0.1.0"
5
+ end
data/lib/mdphlex.rb ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "phlex"
4
+ require "zeitwerk"
5
+
6
+ loader = Zeitwerk::Loader.for_gem
7
+ loader.inflector.inflect("mdphlex" => "MDPhlex", "md" => "MD")
8
+ loader.setup
9
+
10
+ module MDPhlex
11
+ end
@@ -0,0 +1,330 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "phlex"
4
+
5
+ test "MDPhlex::MD component rendered inside Phlex::HTML component" do
6
+ # Define a simple Markdown component
7
+ markdown_component = Class.new(MDPhlex::MD) do
8
+ def view_template
9
+ h2 "Markdown Section"
10
+ p "This is a paragraph with **bold** text."
11
+ ul do
12
+ li "First item"
13
+ li "Second item"
14
+ end
15
+ end
16
+ end
17
+
18
+ # Define an HTML component that renders the Markdown component
19
+ html_component = Class.new(Phlex::HTML) do
20
+ def initialize(markdown_component)
21
+ @markdown_component = markdown_component
22
+ end
23
+
24
+ def view_template
25
+ article do
26
+ h1 { "HTML Article" }
27
+ div(class: "markdown-content") do
28
+ plain render(@markdown_component.new)
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ # Render and test
35
+ output = html_component.new(markdown_component).call
36
+
37
+ assert output.include?("<article>")
38
+ assert output.include?("<h1>HTML Article</h1>")
39
+ assert output.include?('<div class="markdown-content">')
40
+ assert output.include?("## Markdown Section")
41
+ assert output.include?("This is a paragraph with **bold** text.")
42
+ assert output.include?("- First item")
43
+ assert output.include?("- Second item")
44
+ assert output.include?("</article>")
45
+ end
46
+
47
+ test "MDPhlex::MD component rendered inside Phlex::HTML without .new" do
48
+ # Define a Markdown component that doesn't need initialization
49
+ simple_markdown = Class.new(MDPhlex::MD) do
50
+ def view_template
51
+ h1 "About"
52
+ p "This component can be rendered without calling .new"
53
+ end
54
+ end
55
+
56
+ # HTML component that renders the class directly
57
+ html_wrapper = Class.new(Phlex::HTML) do
58
+ def initialize(markdown_class)
59
+ @markdown_class = markdown_class
60
+ end
61
+
62
+ def view_template
63
+ div(class: "wrapper") do
64
+ # Render without .new - Phlex should automatically initialize
65
+ plain render(@markdown_class)
66
+ end
67
+ end
68
+ end
69
+
70
+ output = html_wrapper.new(simple_markdown).call
71
+
72
+ assert output.include?('<div class="wrapper">')
73
+ assert output.include?("# About")
74
+ assert output.include?("This component can be rendered without calling .new")
75
+ end
76
+
77
+ test "Phlex::HTML component rendered inside MDPhlex::MD component" do
78
+ # Define an HTML component
79
+ button_component = Class.new(Phlex::HTML) do
80
+ def initialize(text:, variant: "primary")
81
+ @text = text
82
+ @variant = variant
83
+ end
84
+
85
+ def view_template
86
+ button(type: "button", class: "btn btn-#{@variant}") { @text }
87
+ end
88
+ end
89
+
90
+ # Define a Markdown component that renders HTML components
91
+ markdown_component = Class.new(MDPhlex::MD) do
92
+ def initialize(button_component)
93
+ @button_component = button_component
94
+ end
95
+
96
+ def view_template
97
+ h1 "Interactive Documentation"
98
+
99
+ p "Here's an example of a button component:"
100
+
101
+ # Render the HTML component directly
102
+ render @button_component.new(text: "Click me!", variant: "primary")
103
+
104
+ p "You can also use different variants:"
105
+
106
+ render @button_component.new(text: "Warning", variant: "warning")
107
+ render @button_component.new(text: "Danger", variant: "danger")
108
+ end
109
+ end
110
+
111
+ # Render and test
112
+ output = markdown_component.new(button_component).call
113
+
114
+ assert output.include?("# Interactive Documentation")
115
+ assert output.include?("Here's an example of a button component:")
116
+ assert output.include?('<button type="button" class="btn btn-primary">Click me!</button>')
117
+ assert output.include?("You can also use different variants:")
118
+ assert output.include?('<button type="button" class="btn btn-warning">Warning</button>')
119
+ assert output.include?('<button type="button" class="btn btn-danger">Danger</button>')
120
+ end
121
+
122
+ test "MDPhlex::MD component rendered inside another MDPhlex::MD component" do
123
+ # Define a reusable Markdown component for code examples
124
+ code_example = Class.new(MDPhlex::MD) do
125
+ def initialize(title:, code:, language: "ruby")
126
+ @title = title
127
+ @code = code
128
+ @language = language
129
+ end
130
+
131
+ def view_template
132
+ h3 @title
133
+ pre(language: @language) { plain @code }
134
+ end
135
+ end
136
+
137
+ # Define a parent Markdown component that uses the code example component
138
+ tutorial_component = Class.new(MDPhlex::MD) do
139
+ def initialize(code_example_component)
140
+ @code_example_component = code_example_component
141
+ end
142
+
143
+ def view_template
144
+ h1 "MDPhlex Tutorial"
145
+
146
+ p "Learn how to use MDPhlex for creating beautiful documentation."
147
+
148
+ h2 "Examples"
149
+
150
+ # Render child MDPhlex::MD components
151
+ render @code_example_component.new(
152
+ title: "Basic Usage",
153
+ code: <<~RUBY
154
+ class MyDoc < MDPhlex
155
+ def view_template
156
+ h1 "Hello World"
157
+ p "This is MDPhlex!"
158
+ end
159
+ end
160
+ RUBY
161
+ )
162
+
163
+ render @code_example_component.new(
164
+ title: "Lists and Formatting",
165
+ code: <<~RUBY
166
+ ul do
167
+ li "Item with **bold** text"
168
+ li "Item with `code`"
169
+ end
170
+ RUBY
171
+ )
172
+
173
+ p "These examples show the flexibility of component composition."
174
+ end
175
+ end
176
+
177
+ # Render and test
178
+ output = tutorial_component.new(code_example).call
179
+
180
+ assert output.include?("# MDPhlex Tutorial")
181
+ assert output.include?("Learn how to use MDPhlex for creating beautiful documentation.")
182
+ assert output.include?("## Examples")
183
+ assert output.include?("### Basic Usage")
184
+ assert output.include?("```ruby")
185
+ assert output.include?('h1 "Hello World"')
186
+ assert output.include?("### Lists and Formatting")
187
+ assert output.include?('li "Item with **bold** text"')
188
+ assert output.include?("These examples show the flexibility of component composition.")
189
+ end
190
+
191
+ test "Components yielding content blocks across Phlex::HTML and MDPhlex::MD" do
192
+ # Define a card component that yields content
193
+ card_component = Class.new(Phlex::HTML) do
194
+ def initialize(title:)
195
+ @title = title
196
+ end
197
+
198
+ def view_template
199
+ div(class: "card") do
200
+ h2(class: "card-title") { @title }
201
+ div(class: "card-content") do
202
+ yield
203
+ end
204
+ end
205
+ end
206
+ end
207
+
208
+ # MDPhlex::MD component that uses the card and passes content
209
+ markdown_component = Class.new(MDPhlex::MD) do
210
+ def initialize(card_class)
211
+ @card_class = card_class
212
+ end
213
+
214
+ def view_template
215
+ h1 "Documentation with Cards"
216
+
217
+ # Render HTML component with a content block
218
+ render @card_class.new(title: "Important Note") do
219
+ p "This is **important** information inside a card."
220
+ ul do
221
+ li "First point"
222
+ li "Second point"
223
+ end
224
+ end
225
+ end
226
+ end
227
+
228
+ output = markdown_component.new(card_component).call
229
+
230
+ assert output.include?("# Documentation with Cards")
231
+ assert output.include?('<div class="card">')
232
+ assert output.include?('<h2 class="card-title">Important Note</h2>')
233
+ assert output.include?('<div class="card-content">')
234
+ assert output.include?("This is **important** information inside a card.")
235
+ assert output.include?("- First point")
236
+ end
237
+
238
+ test "Interface yielding pattern between MDPhlex::MD and Phlex::HTML" do
239
+ # Define a nav component with interface yielding
240
+ nav_component = Class.new(Phlex::HTML) do
241
+ def view_template(&)
242
+ nav(class: "special-nav", &)
243
+ end
244
+
245
+ def item(href, &)
246
+ a(class: "nav-item", href:, &)
247
+ end
248
+
249
+ def divider
250
+ span(class: "nav-divider") { "|" }
251
+ end
252
+ end
253
+
254
+ # MDPhlex::MD component that uses the nav interface
255
+ docs_layout = Class.new(MDPhlex::MD) do
256
+ def initialize(nav_class)
257
+ @nav_class = nav_class
258
+ end
259
+
260
+ def view_template
261
+ h1 "Site Navigation Example"
262
+
263
+ p "Here's how to use the nav component:"
264
+
265
+ # Use the yielded interface
266
+ render @nav_class.new do |nav|
267
+ nav.item("/") { "Home" }
268
+ nav.item("/docs") { "Documentation" }
269
+ nav.divider
270
+ nav.item("/about") { "About" }
271
+ end
272
+
273
+ p "The nav component yields an interface for building navigation."
274
+ end
275
+ end
276
+
277
+ output = docs_layout.new(nav_component).call
278
+
279
+ assert output.include?("# Site Navigation Example")
280
+ assert output.include?('<nav class="special-nav">')
281
+ assert output.include?('<a class="nav-item" href="/">Home</a>')
282
+ assert output.include?('<a class="nav-item" href="/docs">Documentation</a>')
283
+ assert output.include?('<span class="nav-divider">|</span>')
284
+ assert output.include?('<a class="nav-item" href="/about">About</a>')
285
+ end
286
+
287
+ test "Conditional rendering with render? method" do
288
+ # HTML component with conditional rendering
289
+ notification_badge = Class.new(Phlex::HTML) do
290
+ def initialize(count:)
291
+ @count = count
292
+ end
293
+
294
+ def view_template
295
+ span(class: "badge") { @count }
296
+ end
297
+
298
+ def render?
299
+ @count > 0
300
+ end
301
+ end
302
+
303
+ # MDPhlex::MD component that conditionally renders the badge
304
+ user_profile = Class.new(MDPhlex::MD) do
305
+ def initialize(badge_class, notifications_count)
306
+ @badge_class = badge_class
307
+ @notifications_count = notifications_count
308
+ end
309
+
310
+ def view_template
311
+ h1 "User Profile"
312
+
313
+ p do
314
+ plain "Notifications "
315
+ render @badge_class.new(count: @notifications_count)
316
+ end
317
+ end
318
+ end
319
+
320
+ # Test with notifications
321
+ output_with = user_profile.new(notification_badge, 5).call
322
+ assert output_with.include?("# User Profile")
323
+ assert output_with.include?("Notifications <span class=\"badge\">5</span>")
324
+
325
+ # Test without notifications
326
+ output_without = user_profile.new(notification_badge, 0).call
327
+ assert output_without.include?("# User Profile")
328
+ assert output_without.include?("Notifications ")
329
+ assert !output_without.include?("badge")
330
+ end
@@ -0,0 +1,423 @@
1
+ # frozen_string_literal: true
2
+
3
+ test "renders h1 heading" do
4
+ example = Class.new(MDPhlex::MD) do
5
+ def view_template
6
+ h1 "Hello World"
7
+ end
8
+ end
9
+
10
+ assert_equal example.new.call, "# Hello World\n"
11
+ end
12
+
13
+ test "renders h2 heading" do
14
+ example = Class.new(MDPhlex::MD) do
15
+ def view_template
16
+ h2 "Subheading"
17
+ end
18
+ end
19
+
20
+ assert_equal example.new.call, "## Subheading\n"
21
+ end
22
+
23
+ test "renders h3 heading" do
24
+ example = Class.new(MDPhlex::MD) do
25
+ def view_template
26
+ h3 "Section"
27
+ end
28
+ end
29
+
30
+ assert_equal example.new.call, "### Section\n"
31
+ end
32
+
33
+ test "renders h4-h6 headings" do
34
+ example = Class.new(MDPhlex::MD) do
35
+ def view_template
36
+ h4 "Level 4"
37
+ h5 "Level 5"
38
+ h6 "Level 6"
39
+ end
40
+ end
41
+
42
+ assert_equal example.new.call, "#### Level 4\n##### Level 5\n###### Level 6\n"
43
+ end
44
+
45
+ test "renders paragraph" do
46
+ example = Class.new(MDPhlex::MD) do
47
+ def view_template
48
+ p "This is a paragraph."
49
+ end
50
+ end
51
+
52
+ assert_equal example.new.call, "This is a paragraph.\n\n"
53
+ end
54
+
55
+ test "renders strong text" do
56
+ example = Class.new(MDPhlex::MD) do
57
+ def view_template
58
+ p do
59
+ plain "This is "
60
+ strong "bold"
61
+ plain " text."
62
+ end
63
+ end
64
+ end
65
+
66
+ assert_equal example.new.call, "This is **bold** text.\n\n"
67
+ end
68
+
69
+ test "renders emphasized text" do
70
+ example = Class.new(MDPhlex::MD) do
71
+ def view_template
72
+ p do
73
+ plain "This is "
74
+ em "italic"
75
+ plain " text."
76
+ end
77
+ end
78
+ end
79
+
80
+ assert_equal example.new.call, "This is *italic* text.\n\n"
81
+ end
82
+
83
+ test "renders inline code" do
84
+ example = Class.new(MDPhlex::MD) do
85
+ def view_template
86
+ p do
87
+ plain "Use "
88
+ code "puts 'hello'"
89
+ plain " to print."
90
+ end
91
+ end
92
+ end
93
+
94
+ assert_equal example.new.call, "Use `puts 'hello'` to print.\n\n"
95
+ end
96
+
97
+ test "renders links" do
98
+ example = Class.new(MDPhlex::MD) do
99
+ def view_template
100
+ p do
101
+ a(href: "https://example.com") { "Click here" }
102
+ end
103
+ end
104
+ end
105
+
106
+ assert_equal example.new.call, "[Click here](https://example.com)\n\n"
107
+ end
108
+
109
+ test "separates multiple paragraphs with blank line" do
110
+ example = Class.new(MDPhlex::MD) do
111
+ def view_template
112
+ p "First paragraph."
113
+ p "Second paragraph."
114
+ end
115
+ end
116
+
117
+ assert_equal example.new.call, "First paragraph.\n\nSecond paragraph.\n\n"
118
+ end
119
+
120
+ test "renders blockquotes" do
121
+ example = Class.new(MDPhlex::MD) do
122
+ def view_template
123
+ blockquote "This is a quote."
124
+ end
125
+ end
126
+
127
+ assert_equal example.new.call, "> This is a quote.\n\n"
128
+ end
129
+
130
+ test "renders blockquotes with block content" do
131
+ example = Class.new(MDPhlex::MD) do
132
+ def view_template
133
+ blockquote do
134
+ plain "This is a "
135
+ strong "quoted"
136
+ plain " text."
137
+ end
138
+ end
139
+ end
140
+
141
+ assert_equal example.new.call, "> This is a **quoted** text.\n\n"
142
+ end
143
+
144
+ test "renders multiline blockquotes" do
145
+ example = Class.new(MDPhlex::MD) do
146
+ def view_template
147
+ blockquote "Line 1\nLine 2\nLine 3"
148
+ end
149
+ end
150
+
151
+ assert_equal example.new.call, "> Line 1\n> Line 2\n> Line 3\n\n"
152
+ end
153
+
154
+ test "renders code blocks" do
155
+ example = Class.new(MDPhlex::MD) do
156
+ def view_template
157
+ pre do
158
+ plain "def hello\n puts 'world'\nend"
159
+ end
160
+ end
161
+ end
162
+
163
+ assert_equal example.new.call, "```\ndef hello\n puts 'world'\nend\n```\n\n"
164
+ end
165
+
166
+ test "renders code blocks with language" do
167
+ example = Class.new(MDPhlex::MD) do
168
+ def view_template
169
+ pre(language: "ruby") do
170
+ plain "def hello\n puts 'world'\nend"
171
+ end
172
+ end
173
+ end
174
+
175
+ assert_equal example.new.call, "```ruby\ndef hello\n puts 'world'\nend\n```\n\n"
176
+ end
177
+
178
+ test "renders unordered lists" do
179
+ example = Class.new(MDPhlex::MD) do
180
+ def view_template
181
+ ul do
182
+ li "First item"
183
+ li "Second item"
184
+ li "Third item"
185
+ end
186
+ end
187
+ end
188
+
189
+ assert_equal example.new.call, "- First item\n- Second item\n- Third item\n\n"
190
+ end
191
+
192
+ test "renders ordered lists" do
193
+ example = Class.new(MDPhlex::MD) do
194
+ def view_template
195
+ ol do
196
+ li "First item"
197
+ li "Second item"
198
+ li "Third item"
199
+ end
200
+ end
201
+ end
202
+
203
+ assert_equal example.new.call, "1. First item\n2. Second item\n3. Third item\n\n"
204
+ end
205
+
206
+ test "renders nested lists" do
207
+ example = Class.new(MDPhlex::MD) do
208
+ def view_template
209
+ ul do
210
+ li "Item 1"
211
+ li do
212
+ plain "Item 2"
213
+ ul do
214
+ li "Nested item 2.1"
215
+ li "Nested item 2.2"
216
+ end
217
+ end
218
+ li "Item 3"
219
+ end
220
+ end
221
+ end
222
+
223
+ assert_equal example.new.call, "- Item 1\n- Item 2\n - Nested item 2.1\n - Nested item 2.2\n- Item 3\n\n"
224
+ end
225
+
226
+ test "renders images" do
227
+ example = Class.new(MDPhlex::MD) do
228
+ def view_template
229
+ img(src: "https://example.com/image.jpg", alt: "Example image")
230
+ end
231
+ end
232
+
233
+ assert_equal example.new.call, "![Example image](https://example.com/image.jpg)"
234
+ end
235
+
236
+ test "renders images without alt text" do
237
+ example = Class.new(MDPhlex::MD) do
238
+ def view_template
239
+ img(src: "https://example.com/image.jpg")
240
+ end
241
+ end
242
+
243
+ assert_equal example.new.call, "![](https://example.com/image.jpg)"
244
+ end
245
+
246
+ test "renders horizontal rules" do
247
+ example = Class.new(MDPhlex::MD) do
248
+ def view_template
249
+ p "Above the line"
250
+ hr
251
+ p "Below the line"
252
+ end
253
+ end
254
+
255
+ assert_equal example.new.call, "Above the line\n\n---\n\nBelow the line\n\n"
256
+ end
257
+
258
+ test "renders line breaks" do
259
+ example = Class.new(MDPhlex::MD) do
260
+ def view_template
261
+ p do
262
+ plain "Line 1"
263
+ br
264
+ plain "Line 2"
265
+ end
266
+ end
267
+ end
268
+
269
+ assert_equal example.new.call, "Line 1 \nLine 2\n\n"
270
+ end
271
+
272
+ test "renders custom element registered with register_element" do
273
+ example = Class.new(MDPhlex::MD) do
274
+ register_element :system
275
+
276
+ def view_template
277
+ system { "System message" }
278
+ end
279
+ end
280
+
281
+ assert_equal example.new.call, "<system>System message</system>"
282
+ end
283
+
284
+ test "renders custom element with attributes" do
285
+ example = Class.new(MDPhlex::MD) do
286
+ register_element :system
287
+
288
+ def view_template
289
+ system(type: "warning", level: "high") { "Alert!" }
290
+ end
291
+ end
292
+
293
+ assert_equal example.new.call, '<system type="warning" level="high">Alert!</system>'
294
+ end
295
+
296
+ test "renders custom element within markdown context" do
297
+ example = Class.new(MDPhlex::MD) do
298
+ register_element :system
299
+
300
+ def view_template
301
+ p do
302
+ plain "This is a "
303
+ system { "system message" }
304
+ plain " in a paragraph."
305
+ end
306
+ end
307
+ end
308
+
309
+ assert_equal example.new.call, "This is a <system>system message</system> in a paragraph.\n\n"
310
+ end
311
+
312
+ test "renders nested custom block elements" do
313
+ example = Class.new(MDPhlex::MD) do
314
+ register_block_element :system
315
+ register_block_element :tools
316
+
317
+ def view_template
318
+ system do
319
+ h1 "Main:"
320
+ tools do
321
+ ul do
322
+ li "nested content 1"
323
+ li "nested content 2"
324
+ end
325
+ end
326
+ end
327
+ end
328
+ end
329
+
330
+ assert_equal example.new.call, <<~XML
331
+ <system>
332
+ # Main:
333
+ <tools>
334
+ - nested content 1
335
+ - nested content 2
336
+
337
+ </tools>
338
+ </system>
339
+ XML
340
+ end
341
+
342
+ test "register_block_element vs register_element spacing" do
343
+ example = Class.new(MDPhlex::MD) do
344
+ register_element :inline_tag
345
+ register_block_element :block_tag
346
+
347
+ def view_template
348
+ p do
349
+ plain "Text with "
350
+ inline_tag { "inline" }
351
+ plain " content."
352
+ end
353
+
354
+ block_tag { p "Block content" }
355
+
356
+ p "After block"
357
+ end
358
+ end
359
+
360
+ assert_equal example.new.call, <<~MD
361
+ Text with <inline-tag>inline</inline-tag> content.
362
+
363
+ <block-tag>
364
+ Block content
365
+
366
+ </block-tag>
367
+ After block
368
+
369
+ MD
370
+ end
371
+
372
+ test "register_block_element with attributes" do
373
+ example = Class.new(MDPhlex::MD) do
374
+ register_block_element :section
375
+
376
+ def view_template
377
+ section(id: "main", class: "container") do
378
+ h2 "Section Title"
379
+ p "Section content"
380
+ end
381
+ end
382
+ end
383
+
384
+ assert_equal example.new.call, <<~MD
385
+ <section id="main" class="container">
386
+ ## Section Title
387
+ Section content
388
+
389
+ </section>
390
+ MD
391
+ end
392
+
393
+ test "multiple register_block_elements" do
394
+ example = Class.new(MDPhlex::MD) do
395
+ register_block_element :article
396
+ register_block_element :aside
397
+
398
+ def view_template
399
+ article do
400
+ h1 "Main Article"
401
+ p "Article content"
402
+ end
403
+
404
+ aside do
405
+ h2 "Related"
406
+ p "Sidebar content"
407
+ end
408
+ end
409
+ end
410
+
411
+ assert_equal example.new.call, <<~MD
412
+ <article>
413
+ # Main Article
414
+ Article content
415
+
416
+ </article>
417
+ <aside>
418
+ ## Related
419
+ Sidebar content
420
+
421
+ </aside>
422
+ MD
423
+ end
metadata ADDED
@@ -0,0 +1,82 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mdphlex
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Martin Emde
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: phlex
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '2.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '2.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: zeitwerk
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '2.7'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '2.7'
40
+ description: A Phlex-based Markdown renderer that generates Markdown output
41
+ email:
42
+ - me@martinemde.com
43
+ executables: []
44
+ extensions: []
45
+ extra_rdoc_files: []
46
+ files:
47
+ - CHANGELOG.md
48
+ - LICENSE.txt
49
+ - README.md
50
+ - Rakefile
51
+ - config/quickdraw.rb
52
+ - lib/mdphlex.rb
53
+ - lib/mdphlex/md.rb
54
+ - lib/mdphlex/version.rb
55
+ - quickdraw/interoperability.test.rb
56
+ - quickdraw/mdphlex.test.rb
57
+ homepage: https://github.com/martinemde/mdphlex
58
+ licenses:
59
+ - MIT
60
+ metadata:
61
+ allowed_push_host: https://rubygems.org
62
+ homepage_uri: https://github.com/martinemde/mdphlex
63
+ source_code_uri: https://github.com/martinemde/mdphlex
64
+ changelog_uri: https://github.com/martinemde/mdphlex/blob/main/CHANGELOG.md
65
+ rdoc_options: []
66
+ require_paths:
67
+ - lib
68
+ required_ruby_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: 3.2.0
73
+ required_rubygems_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ requirements: []
79
+ rubygems_version: 3.6.9
80
+ specification_version: 4
81
+ summary: Markdown renderer for Phlex
82
+ test_files: []