mdphlex 0.1.0 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 30fde45581cd7d2edeada97dd0c281cbf4bf6447e6938f5002ad99231c4ac2f0
4
- data.tar.gz: 1168f4d22457f7965160e4248f468d7bce4e0a45eb56a44b6285cb3aebe94d83
3
+ metadata.gz: 7df543d1ff68faf2b25ce38d74a767d5957a4322ce83d7ad424190cd7ee0bd7f
4
+ data.tar.gz: 427fecb092f11651e9f5d463fd544ed6d6bb4891bfdd7a338c0e5312a51d5f35
5
5
  SHA512:
6
- metadata.gz: 8aab63e64b863d0fdba508d8e9d2a570eb7b8bc8d9c11081cf4b8718a9c06365a6069df65024a2c868efd6bc60e1163f4adf20251f220f7c63d0d6d4b4ffd05b
7
- data.tar.gz: 66efd4524781b9fcc52568879f352285db7477f078039c40f1c209c09e13d63c3dc0c24f95c20876839203a68982d79166bf125d54abe4017dc6c9c3c2008e69
6
+ metadata.gz: 1f895e699d4d037f6f3aaf8d33a6ff9e263fb24a1ee07d455fcdd7c8e3fff11b707ba85a17b7f053c33e78fe864dbaf18e8e67a97c62e520aafd3a747af68aef
7
+ data.tar.gz: dc0f27b7faadd20203f588733b14236b6b1cbdd47db87aae3ec299a767a58534d51a5e1a5088f9b10021ce83f81d41554112e0d67ac1fb94d73a0a4874ee6698
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## 0.2.0 - 2025-09-11
4
+
5
+ - Add to_markdown for Rails 8.1 support
6
+
3
7
  ## [0.1.0] - 2025-09-03
4
8
 
5
9
  - Initial release
data/README.md CHANGED
@@ -1,74 +1,242 @@
1
1
  # MDPhlex
2
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.
3
+ MDPhlex is a Phlex component for rendering Markdown. No... not rendering markdown into HTML, rendering plain Markdown, programmatically.
4
+
5
+ MDPhlex is perfect for dynamically creating context for LLMs, generating an llms.txt file from real content, or composing markdown out of componentized pieces.
4
6
 
5
7
  ## Installation
6
8
 
7
9
  Add this line to your application's Gemfile:
8
10
 
9
- ```ruby
11
+ ~~~ruby
10
12
  gem 'mdphlex'
11
- ```
13
+ ~~~
14
+
15
+ ## Creating LLM Prompts with MDPhlex
16
+
17
+ MDPhlex shines when creating structured prompts for LLMs allowing comments and organization without cluttering the prompt. Here's a simple example using custom tags:
18
+
19
+ ~~~ruby
20
+ class LLMPrompt < MDPhlex::MD
21
+ # Register custom elements for structured prompts
22
+ register_block_element :system
23
+ register_block_element :tools
24
+ register_block_element :tool
25
+
26
+ def initialize(task:, tools: [])
27
+ @task = task
28
+ @tools = tools
29
+ end
30
+
31
+ def view_template
32
+ system do
33
+ p "You are an AI assistant specialized in #{@task}."
34
+
35
+ h2 "Goal"
36
+ # we should define the goal more clearly.
37
+ p "Use the available tools to help the user."
38
+
39
+ # what about guardrails?
40
+ end
41
+
42
+ if @tools.any?
43
+ tools do
44
+ @tools.each do |tool_def|
45
+ tool name: tool_def[:name] do
46
+ plain tool_def[:description]
47
+ # need to add input schemas
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
12
54
 
13
- ## Basic Usage
55
+ # Dynamic prompt generation
56
+ prompt = LLMPrompt.new(
57
+ task: "Ruby code analysis",
58
+ tools: [
59
+ { name: "analyze_code", description: "Analyze Ruby code for improvements" },
60
+ { name: "explain_concept", description: "Explain Ruby concepts and patterns" }
61
+ ]
62
+ )
14
63
 
15
- Create an MDPhlex::MD component just like you would create a Phlex component:
64
+ puts prompt.call
65
+ ~~~
66
+
67
+ This outputs clean, structured markdown perfect for LLMs:
68
+
69
+ ~~~xml
70
+ <system>
71
+ You are an AI assistant specialized in Ruby code analysis.
72
+
73
+ ## Goal
74
+ Use the available tools to help the user.
75
+
76
+ </system>
77
+ <tools>
78
+ <tool name="analyze_code">
79
+ Analyze Ruby code for improvements</tool>
80
+ <tool name="explain_concept">
81
+ Explain Ruby concepts and patterns</tool>
82
+ </tools>
83
+ ~~~
84
+
85
+ ## Traditional Markdown Generation
86
+
87
+ MDPhlex also works great for generating regular Markdown content.
88
+
89
+ ### Rails Integration with Markdown Rendering
90
+
91
+ With Rails 8.1's native markdown support, MDPhlex components integrate seamlessly:
92
+
93
+ ~~~ruby
94
+ # app/components/page_component.rb
95
+ class PageComponent < MDPhlex::MD
96
+ def initialize(page)
97
+ @page = page
98
+ end
99
+
100
+ def view_template
101
+ h1 @page.title
102
+
103
+ p do
104
+ em "Last updated: #{@page.updated_at.strftime('%B %d, %Y')}"
105
+ end
106
+
107
+ hr
108
+
109
+ # Render page sections with proper markdown structure
110
+ @page.sections.each do |section|
111
+ h2 section.heading
112
+
113
+ p section.content
114
+
115
+ if section.code_example?
116
+ pre(language: section.language) { plain section.code }
117
+ end
118
+ end
119
+
120
+ if @page.references.any?
121
+ h2 "References"
122
+ ul do
123
+ @page.references.each do |ref|
124
+ li do
125
+ a(href: ref.url) { ref.title }
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
132
+
133
+ # app/controllers/pages_controller.rb
134
+ class PagesController < ApplicationController
135
+ def show
136
+ @page = Page.find(params[:id])
137
+
138
+ respond_to do |format|
139
+ format.html
140
+ format.md { render markdown: PageComponent.new(@page) }
141
+ end
142
+ end
143
+ end
144
+ ~~~
145
+
146
+ When someone visits `/pages/123.md`, Rails will render:
147
+
148
+ ~~~markdown
149
+ # Getting Started with Ruby
150
+
151
+ *Last updated: September 12, 2025*
152
+
153
+ ---
154
+
155
+ ## Introduction
156
+
157
+ Ruby is a dynamic, object-oriented programming language...
158
+
159
+ ## Basic Syntax
160
+
161
+ Here's how to define a method in Ruby:
16
162
 
17
163
  ```ruby
18
- class HelloWorld < MDPhlex::MD
164
+ def greet(name)
165
+ puts "Hello, #{name}!"
166
+ end
167
+ ```
168
+
169
+ ## References
170
+ - [Official Ruby Documentation](https://ruby-doc.org)
171
+ - [Ruby Style Guide](https://rubystyle.guide)
172
+ ~~~
173
+
174
+ ### Dynamic Content Generation
175
+
176
+ ~~~ruby
177
+ class ArticleContent < MDPhlex::MD
178
+ def initialize(title:, sections:)
179
+ @title = title
180
+ @sections = sections
181
+ end
182
+
19
183
  def view_template
20
- h1 "Hello, World!"
184
+ h1 @title
21
185
 
22
- p "Welcome to MDPhlex - writing Markdown with Phlex syntax!"
186
+ @sections.each do |section|
187
+ h2 section[:heading]
23
188
 
24
- h2 "Features"
189
+ p section[:content]
25
190
 
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"
191
+ if section[:code_example]
192
+ pre(language: "ruby") { plain section[:code_example] }
35
193
  end
36
- li "Full Phlex component composition"
37
194
  end
38
195
 
39
196
  p do
40
- plain "Check out the "
41
- a(href: "https://github.com/martinemde/mdphlex") { "MDPhlex repository" }
42
- plain " for more information."
197
+ plain "Learn more in the "
198
+ a(href: "https://docs.example.com") { "documentation" }
199
+ plain "."
43
200
  end
44
201
  end
45
202
  end
46
203
 
47
- # Render the component
48
- puts HelloWorld.new.call
49
- ```
204
+ article = ArticleContent.new(
205
+ title: "Getting Started with Ruby",
206
+ sections: [
207
+ {
208
+ heading: "Variables",
209
+ content: "Ruby variables are dynamically typed and don't require declaration.",
210
+ code_example: "name = 'Alice'\nage = 30"
211
+ }
212
+ ]
213
+ )
50
214
 
51
- This outputs the following Markdown:
215
+ puts article.call
216
+ ~~~
52
217
 
53
- ```markdown
54
- # Hello, World!
218
+ Outputs:
55
219
 
56
- Welcome to MDPhlex - writing Markdown with Phlex syntax!
220
+ ~~~markdown
221
+ # Getting Started with Ruby
57
222
 
58
- ## Features
223
+ ## Variables
59
224
 
60
- - Write Markdown using Ruby
61
- - Support for **bold**, *italic*, and `inline code`
62
- - Full Phlex component composition
225
+ Ruby variables are dynamically typed and don't require declaration.
63
226
 
64
- Check out the [MDPhlex repository](https://github.com/martinemde/mdphlex) for more information.
227
+ ```ruby
228
+ name = 'Alice'
229
+ age = 30
65
230
  ```
66
231
 
67
- ## Rendering MDPhlex inside Phlex::HTML
232
+ Learn more in the [documentation](https://docs.example.com).
233
+ ~~~
68
234
 
69
- MDPhlex components can be seamlessly integrated into your Phlex::HTML views:
235
+ ## Rendering MDPhlex inside Phlex::HTML (and vice versa)
70
236
 
71
- ```ruby
237
+ MDPhlex components are Phlex compatible. Integrate them into any Phlex::HTML views or show HTML or other wcomented in Markdown:
238
+
239
+ ~~~ruby
72
240
  class ArticlePage < Phlex::HTML
73
241
  def initialize(article)
74
242
  @article = article
@@ -109,14 +277,15 @@ class ArticleContent < MDPhlex::MD
109
277
  end
110
278
  end
111
279
  end
112
- ```
280
+ ~~~
113
281
 
114
282
  ## Why MDPhlex?
115
283
 
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
284
+ *But markdown is just text!?* Yes, but have you ever tried to render clean markdown from a lot of conditional logic? MDPhlex tames the mess with a simple and familiar API.
285
+
286
+ - **Component-based**: Build reusable Markdown with simple ruby classes
287
+ - **Dynamic Markdown**: Generate markdown from dynamic data
288
+ - **Composable**: Mix Phlex and MDPhlex components freely
120
289
 
121
290
  ## License
122
291
 
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/mdphlex"
5
+ require "json"
6
+
7
+ # Example: Creating custom block elements for specialized markdown output
8
+ class DocumentationTemplate < MDPhlex::MD
9
+ # Register custom elements for API documentation
10
+ register_block_element :api_endpoint
11
+ register_block_element :request
12
+ register_block_element :response
13
+ register_block_element :warning
14
+ register_block_element :note
15
+ register_block_element :deprecated
16
+
17
+ def initialize(endpoint_name:, method:, path:)
18
+ @endpoint_name = endpoint_name
19
+ @method = method
20
+ @path = path
21
+ end
22
+
23
+ def view_template
24
+ h1 "API Documentation: #{@endpoint_name}"
25
+
26
+ api_endpoint method: @method, path: @path do
27
+ h2 "Overview"
28
+ p "This endpoint handles user authentication and returns a JWT token."
29
+
30
+ warning do
31
+ p "This endpoint rate limits requests to 10 per minute per IP address."
32
+ end
33
+
34
+ h2 "Request"
35
+
36
+ request do
37
+ h3 "Headers"
38
+ pre do
39
+ <<~HEADERS
40
+ Content-Type: application/json
41
+ Accept: application/json
42
+ HEADERS
43
+ end
44
+
45
+ h3 "Body"
46
+ pre language: "json" do
47
+ JSON.pretty_generate({ email: "user@example.com", password: "secure_password123" })
48
+ end
49
+ end
50
+
51
+ h2 "Response"
52
+
53
+ response status: "200" do
54
+ h3 "Success Response"
55
+ pre language: "json" do
56
+ plain JSON.pretty_generate({
57
+ token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
58
+ user: {
59
+ id: 123,
60
+ email: "user@example.com",
61
+ name: "John Doe",
62
+ },
63
+ })
64
+ end
65
+ end
66
+
67
+ response status: "401" do
68
+ h3 "Authentication Failed"
69
+ pre language: "json" do
70
+ JSON.pretty_generate({
71
+ error: "Invalid credentials",
72
+ })
73
+ end
74
+ end
75
+
76
+ note do
77
+ p "The JWT token expires after 24 hours. Use the refresh endpoint to get a new token."
78
+ end
79
+
80
+ deprecated version: "2.0" do
81
+ p "The 'username' field in the request body is deprecated. Use 'email' instead."
82
+ end
83
+ end
84
+ end
85
+ end
86
+
87
+ # Example usage
88
+ if __FILE__ == $0
89
+ doc = DocumentationTemplate.new(
90
+ endpoint_name: "User Login",
91
+ method: "POST",
92
+ path: "/api/v1/auth/login"
93
+ )
94
+
95
+ output = doc.call
96
+ puts output
97
+
98
+ # Save to file
99
+ File.write(
100
+ File.join(File.dirname(__FILE__), "custom_elements_output.md"),
101
+ output
102
+ )
103
+
104
+ puts "\n---"
105
+ puts "Output saved to examples/custom_elements_output.md"
106
+ end
@@ -0,0 +1,60 @@
1
+ # API Documentation: User Login
2
+ <api-endpoint method="POST" path="/api/v1/auth/login">
3
+ ## Overview
4
+ This endpoint handles user authentication and returns a JWT token.
5
+
6
+ <warning>
7
+ This endpoint rate limits requests to 10 per minute per IP address.
8
+
9
+ </warning>
10
+ ## Request
11
+ <request>
12
+ ### Headers
13
+ ```
14
+ Content-Type: application/json
15
+ Accept: application/json
16
+
17
+ ```
18
+
19
+ ### Body
20
+ ```json
21
+ {
22
+ "email": "user@example.com",
23
+ "password": "secure_password123"
24
+ }
25
+ ```
26
+
27
+ </request>
28
+ ## Response
29
+ <response status="200">
30
+ ### Success Response
31
+ ```json
32
+ {
33
+ "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
34
+ "user": {
35
+ "id": 123,
36
+ "email": "user@example.com",
37
+ "name": "John Doe"
38
+ }
39
+ }
40
+ ```
41
+
42
+ </response>
43
+ <response status="401">
44
+ ### Authentication Failed
45
+ ```json
46
+ {
47
+ "error": "Invalid credentials"
48
+ }
49
+ ```
50
+
51
+ </response>
52
+ <note>
53
+ The JWT token expires after 24 hours. Use the refresh endpoint to get a new token.
54
+
55
+ </note>
56
+ <deprecated version="2.0">
57
+ The 'username' field in the request body is deprecated. Use 'email' instead.
58
+
59
+ </deprecated>
60
+ </api-endpoint>
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "mdphlex"
6
+
7
+ class HTMLInMarkdownExample < MDPhlex::MD
8
+ def view_template
9
+ h1 { "HTML in Markdown" }
10
+
11
+ p { "Markdown allows inline HTML like <strong>this bold text</strong> and <em>this italic text</em>." }
12
+
13
+ p do
14
+ plain "You can use HTML entities directly: &copy; &amp; &lt;div&gt;"
15
+ end
16
+
17
+ h2 { "Complex HTML" }
18
+
19
+ p do
20
+ plain <<~HTML
21
+ You can even include complex HTML structures:
22
+ <div class="alert" style="border: 1px solid red;">
23
+ <h3>Warning!</h3>
24
+ <p>This is an alert box created with HTML.</p>
25
+ </div>
26
+ HTML
27
+ end
28
+
29
+ h2 { "Mixing Markdown and HTML" }
30
+
31
+ p do
32
+ plain "You can mix "
33
+ strong { "Markdown formatting" }
34
+ plain " with <code>HTML tags</code> seamlessly."
35
+ end
36
+ end
37
+ end
38
+
39
+ if __FILE__ == $0
40
+ output = HTMLInMarkdownExample.new.call
41
+ puts output
42
+
43
+ File.write(
44
+ File.join(File.dirname(__FILE__), "html_in_markdown_output.md"),
45
+ output
46
+ )
47
+
48
+ puts "\n---"
49
+ puts "Output saved to examples/html_in_markdown_output.md"
50
+ end
@@ -0,0 +1,16 @@
1
+ # HTML in Markdown
2
+ Markdown allows inline HTML like <strong>this bold text</strong> and <em>this italic text</em>.
3
+
4
+ You can use HTML entities directly: &copy; &amp; &lt;div&gt;
5
+
6
+ ## Complex HTML
7
+ You can even include complex HTML structures:
8
+ <div class="alert" style="border: 1px solid red;">
9
+ <h3>Warning!</h3>
10
+ <p>This is an alert box created with HTML.</p>
11
+ </div>
12
+
13
+
14
+ ## Mixing Markdown and HTML
15
+ You can mix **Markdown formatting** with <code>HTML tags</code> seamlessly.
16
+
@@ -0,0 +1,181 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/mdphlex"
5
+
6
+ # Example: Creating structured LLM prompts with custom XML-style tags
7
+ class LLMPrompt < MDPhlex::MD
8
+ # Register custom block elements for LLM prompt structure
9
+ register_block_element :system
10
+ register_block_element :tools
11
+ register_block_element :tool
12
+ register_element :description
13
+ register_block_element :parameters
14
+ register_block_element :param
15
+ register_block_element :examples
16
+ register_block_element :example
17
+ register_block_element :user
18
+ register_block_element :assistant
19
+ register_block_element :context
20
+ register_block_element :constraints
21
+
22
+ def initialize(task:, tools: [], examples: [], context: nil, constraints: [])
23
+ @task = task
24
+ @tools = tools
25
+ @examples = examples
26
+ @context = context
27
+ @constraints = constraints
28
+ end
29
+
30
+ def view_template
31
+ # System instructions
32
+ system do
33
+ plain "You are an AI assistant specialized in #{@task}."
34
+ plain "\n"
35
+ plain "You have access to tools and should use them when appropriate to help the user."
36
+ plain "\n"
37
+ plain "Always be helpful, accurate, and follow the constraints provided."
38
+ end
39
+
40
+ plain "\n"
41
+
42
+ # Context section
43
+ if @context
44
+ context do
45
+ plain @context
46
+ end
47
+ plain "\n"
48
+ end
49
+
50
+ # Constraints
51
+ if @constraints.any?
52
+ constraints do
53
+ @constraints.each do |constraint|
54
+ plain "- #{constraint}\n"
55
+ end
56
+ end
57
+ plain "\n"
58
+ end
59
+
60
+ # Available tools
61
+ if @tools.any?
62
+ tools do
63
+ @tools.each do |tool_def|
64
+ tool name: tool_def[:name], category: tool_def[:category] do
65
+ description { plain tool_def[:description] }
66
+
67
+ if tool_def[:parameters]&.any?
68
+ parameters do
69
+ tool_def[:parameters].each do |param|
70
+ param name: param[:name], type: param[:type], required: param[:required].to_s do
71
+ plain param[:description]
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ plain "\n"
80
+ end
81
+
82
+ # Examples
83
+ if @examples.any?
84
+ examples do
85
+ @examples.each_with_index do |ex, i|
86
+ example id: (i + 1).to_s do
87
+ user { plain ex[:user] }
88
+ assistant { plain ex[:assistant] }
89
+ end
90
+ end
91
+ end
92
+ plain "\n"
93
+ end
94
+
95
+ # Instructions footer
96
+ h2 "Instructions"
97
+
98
+ p "When responding to user queries, follow the system guidelines above and use the available tools as needed."
99
+ end
100
+ end
101
+
102
+ # Example usage
103
+ if __FILE__ == $0
104
+ prompt = LLMPrompt.new(
105
+ task: "code review and refactoring",
106
+ context: "The user is working on a Ruby on Rails application and needs help improving code quality.",
107
+ constraints: [
108
+ "Focus on Ruby idioms and best practices",
109
+ "Consider performance implications",
110
+ "Suggest tests when appropriate",
111
+ "Keep explanations clear and concise",
112
+ ],
113
+ tools: [
114
+ {
115
+ name: "analyze_code",
116
+ category: "analysis",
117
+ description: "Analyzes code for potential improvements, security issues, and best practices",
118
+ parameters: [
119
+ {
120
+ name: "code",
121
+ type: "string",
122
+ required: true,
123
+ description: "The code snippet to analyze",
124
+ },
125
+ {
126
+ name: "language",
127
+ type: "string",
128
+ required: false,
129
+ description: "Programming language (defaults to auto-detect)",
130
+ },
131
+ {
132
+ name: "focus_areas",
133
+ type: "array",
134
+ required: false,
135
+ description: "Specific areas to focus on: performance, security, readability, testing",
136
+ },
137
+ ],
138
+ },
139
+ {
140
+ name: "suggest_refactoring",
141
+ category: "refactoring",
142
+ description: "Provides specific refactoring suggestions with before/after examples",
143
+ parameters: [
144
+ {
145
+ name: "code",
146
+ type: "string",
147
+ required: true,
148
+ description: "The code to refactor",
149
+ },
150
+ {
151
+ name: "refactoring_type",
152
+ type: "string",
153
+ required: false,
154
+ description: "Type of refactoring: extract_method, simplify_conditionals, remove_duplication",
155
+ },
156
+ ],
157
+ },
158
+ ],
159
+ examples: [
160
+ {
161
+ user: "Review this Ruby method for improvements:\n```ruby\ndef process_items(items)\n results = []\n items.each do |item|\n if item.valid?\n results << item.process\n end\n end\n results\nend\n```",
162
+ assistant: "I'll analyze your Ruby method and suggest improvements.\n\nThe method can be refactored to be more idiomatic and concise using Ruby's enumerable methods:\n\n```ruby\ndef process_items(items)\n items.select(&:valid?).map(&:process)\nend\n```\n\nKey improvements:\n1. **Use `select` and `map`**: More idiomatic than manual array building\n2. **Symbol-to-proc syntax**: `&:valid?` is cleaner than blocks\n3. **Method chaining**: Reads naturally left-to-right\n4. **No temporary variable**: Eliminates the `results` array\n\nThis refactored version is more functional, easier to test, and clearly expresses the intent.",
163
+ },
164
+ ]
165
+ )
166
+
167
+ # Generate the output once and store it
168
+ output = prompt.call
169
+
170
+ # Print to console
171
+ puts output
172
+
173
+ # Also save the output to a file for reference
174
+ File.write(
175
+ File.join(File.dirname(__FILE__), "llm_prompt_output.md"),
176
+ output
177
+ )
178
+
179
+ puts "\n---"
180
+ puts "Output saved to examples/llm_prompt_output.md"
181
+ end
@@ -0,0 +1,75 @@
1
+ <system>
2
+ You are an AI assistant specialized in code review and refactoring.
3
+ You have access to tools and should use them when appropriate to help the user.
4
+ Always be helpful, accurate, and follow the constraints provided.</system>
5
+
6
+ <context>
7
+ The user is working on a Ruby on Rails application and needs help improving code quality.</context>
8
+
9
+ <constraints>
10
+ - Focus on Ruby idioms and best practices
11
+ - Consider performance implications
12
+ - Suggest tests when appropriate
13
+ - Keep explanations clear and concise
14
+ </constraints>
15
+
16
+ <tools>
17
+ <tool name="analyze_code" category="analysis">
18
+ <description>Analyzes code for potential improvements, security issues, and best practices</description><parameters>
19
+ <param name="code" type="string" required="true">
20
+ The code snippet to analyze</param>
21
+ <param name="language" type="string" required="false">
22
+ Programming language (defaults to auto-detect)</param>
23
+ <param name="focus_areas" type="array" required="false">
24
+ Specific areas to focus on: performance, security, readability, testing</param>
25
+ </parameters>
26
+ </tool>
27
+ <tool name="suggest_refactoring" category="refactoring">
28
+ <description>Provides specific refactoring suggestions with before/after examples</description><parameters>
29
+ <param name="code" type="string" required="true">
30
+ The code to refactor</param>
31
+ <param name="refactoring_type" type="string" required="false">
32
+ Type of refactoring: extract_method, simplify_conditionals, remove_duplication</param>
33
+ </parameters>
34
+ </tool>
35
+ </tools>
36
+
37
+ <examples>
38
+ <example id="1">
39
+ <user>
40
+ Review this Ruby method for improvements:
41
+ ```ruby
42
+ def process_items(items)
43
+ results = []
44
+ items.each do |item|
45
+ if item.valid?
46
+ results << item.process
47
+ end
48
+ end
49
+ results
50
+ end
51
+ ```</user>
52
+ <assistant>
53
+ I'll analyze your Ruby method and suggest improvements.
54
+
55
+ The method can be refactored to be more idiomatic and concise using Ruby's enumerable methods:
56
+
57
+ ```ruby
58
+ def process_items(items)
59
+ items.select(&:valid?).map(&:process)
60
+ end
61
+ ```
62
+
63
+ Key improvements:
64
+ 1. **Use `select` and `map`**: More idiomatic than manual array building
65
+ 2. **Symbol-to-proc syntax**: `&:valid?` is cleaner than blocks
66
+ 3. **Method chaining**: Reads naturally left-to-right
67
+ 4. **No temporary variable**: Eliminates the `results` array
68
+
69
+ This refactored version is more functional, easier to test, and clearly expresses the intent.</assistant>
70
+ </example>
71
+ </examples>
72
+
73
+ ## Instructions
74
+ When responding to user queries, follow the system guidelines above and use the available tools as needed.
75
+
data/lib/mdphlex/md.rb CHANGED
@@ -22,7 +22,7 @@ module MDPhlex
22
22
 
23
23
  if attributes.length > 0
24
24
  attributes.each do |key, value|
25
- buffer << " #{key}=\"#{Phlex::Escape.html_escape(value.to_s)}\""
25
+ buffer << " #{key}=\"#{value}\""
26
26
  end
27
27
  end
28
28
 
@@ -40,6 +40,11 @@ module MDPhlex
40
40
  end
41
41
  end
42
42
 
43
+ # Render the markdown to a String using the Rails 8.1 standard to_markdown alias
44
+ def to_markdown
45
+ call
46
+ end
47
+
43
48
  def h1(content = nil, &)
44
49
  heading(1, content, &)
45
50
  end
@@ -299,5 +304,53 @@ module MDPhlex
299
304
  buffer << marker
300
305
  nil
301
306
  end
307
+
308
+ # Override __text__ to avoid HTML escaping since markdown allows raw HTML
309
+ private def __text__(content)
310
+ state = @_state
311
+ return true unless state.should_render?
312
+
313
+ case content
314
+ when String
315
+ state.buffer << content
316
+ when Symbol
317
+ state.buffer << content.name
318
+ when nil
319
+ nil
320
+ else
321
+ if (formatted_object = format_object(content))
322
+ state.buffer << formatted_object
323
+ else
324
+ return false
325
+ end
326
+ end
327
+
328
+ true
329
+ end
330
+
331
+ # Override __implicit_output__ to avoid HTML escaping
332
+ private def __implicit_output__(content)
333
+ state = @_state
334
+ return true unless state.should_render?
335
+
336
+ case content
337
+ when Phlex::SGML::SafeObject
338
+ state.buffer << content.to_s
339
+ when String
340
+ state.buffer << content
341
+ when Symbol
342
+ state.buffer << content.name
343
+ when nil
344
+ nil
345
+ else
346
+ if (formatted_object = format_object(content))
347
+ state.buffer << formatted_object
348
+ else
349
+ return false
350
+ end
351
+ end
352
+
353
+ true
354
+ end
302
355
  end
303
356
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MDPhlex
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ test "markdown does not escape HTML entities" do
4
+ component = Class.new(MDPhlex::MD) do
5
+ def view_template
6
+ p { "HTML tags: <strong>bold</strong> & <em>italic</em>" }
7
+ end
8
+ end
9
+
10
+ output = component.new.call
11
+ assert_equal output, "HTML tags: <strong>bold</strong> & <em>italic</em>\n\n"
12
+ end
13
+
14
+ test "markdown preserves raw HTML in plain text" do
15
+ component = Class.new(MDPhlex::MD) do
16
+ def view_template
17
+ p do
18
+ plain "Raw HTML: <div>content</div> & entities"
19
+ end
20
+ end
21
+ end
22
+
23
+ output = component.new.call
24
+ assert_equal output, "Raw HTML: <div>content</div> & entities\n\n"
25
+ end
26
+
27
+ test "markdown preserves HTML in inline elements" do
28
+ component = Class.new(MDPhlex::MD) do
29
+ def view_template
30
+ p do
31
+ strong { "<b>nested</b> & more" }
32
+ plain " "
33
+ em { "<i>italic</i>" }
34
+ end
35
+ end
36
+ end
37
+
38
+ output = component.new.call
39
+ assert_equal output, "**<b>nested</b> & more** *<i>italic</i>*\n\n"
40
+ end
41
+
42
+ test "custom elements do not escape attribute values" do
43
+ component = Class.new(MDPhlex::MD) do
44
+ register_block_element :custom_div, tag: "div"
45
+
46
+ def view_template
47
+ custom_div(class: "test & class", "data-value": "x > 5") do
48
+ p { "Content" }
49
+ end
50
+ end
51
+ end
52
+
53
+ output = component.new.call
54
+ assert_includes output, '<div class="test & class" data-value="x > 5">'
55
+ end
56
+
57
+ test "code blocks preserve HTML content" do
58
+ component = Class.new(MDPhlex::MD) do
59
+ def view_template
60
+ pre(language: "html") do
61
+ plain "<div class=\"example\">\n <p>HTML code</p>\n</div>"
62
+ end
63
+ end
64
+ end
65
+
66
+ output = component.new.call
67
+ expected = <<~MD
68
+ ```html
69
+ <div class="example">
70
+ <p>HTML code</p>
71
+ </div>
72
+ ```
73
+
74
+ MD
75
+ assert_equal output, expected
76
+ end
77
+
78
+ test "code blocks with JSON.pretty_generate preserve quotes and special characters" do
79
+ require "json"
80
+
81
+ component = Class.new(MDPhlex::MD) do
82
+ def view_template
83
+ pre language: "json" do
84
+ JSON.pretty_generate({ email: "user@example.com", password: "secure_password123" })
85
+ end
86
+ end
87
+ end
88
+
89
+ output = component.new.call
90
+ expected = <<~MD
91
+ ```json
92
+ {
93
+ "email": "user@example.com",
94
+ "password": "secure_password123"
95
+ }
96
+ ```
97
+
98
+ MD
99
+ assert_equal output, expected
100
+ end
101
+
102
+ test "code blocks preserve complex JSON with special characters" do
103
+ require "json"
104
+
105
+ component = Class.new(MDPhlex::MD) do
106
+ def view_template
107
+ pre language: "json" do
108
+ JSON.pretty_generate({
109
+ users: [
110
+ { name: "John & Jane", role: "admin" },
111
+ { name: "Bob <Smith>", role: "user" },
112
+ ],
113
+ config: {
114
+ api_key: "key-with-special-chars-!@#$%^&*()",
115
+ html_template: "<div class=\"test\">Content</div>",
116
+ },
117
+ })
118
+ end
119
+ end
120
+ end
121
+
122
+ output = component.new.call
123
+ expected = <<~MD
124
+ ```json
125
+ {
126
+ "users": [
127
+ {
128
+ "name": "John & Jane",
129
+ "role": "admin"
130
+ },
131
+ {
132
+ "name": "Bob <Smith>",
133
+ "role": "user"
134
+ }
135
+ ],
136
+ "config": {
137
+ "api_key": "key-with-special-chars-!@#$%^&*()",
138
+ "html_template": "<div class=\\"test\\">Content</div>"
139
+ }
140
+ }
141
+ ```
142
+
143
+ MD
144
+ assert_equal output, expected
145
+ end
@@ -421,3 +421,21 @@ test "multiple register_block_elements" do
421
421
  </aside>
422
422
  MD
423
423
  end
424
+
425
+ test "to_markdown works" do
426
+ example = Class.new(MDPhlex::MD) do
427
+ register_block_element :note
428
+
429
+ def view_template
430
+ note(type: "info") do
431
+ h3 "This is a note."
432
+ end
433
+ end
434
+ end
435
+
436
+ assert_equal example.new.to_markdown, <<~MD
437
+ <note type="info">
438
+ ### This is a note.
439
+ </note>
440
+ MD
441
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mdphlex
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Martin Emde
@@ -49,9 +49,16 @@ files:
49
49
  - README.md
50
50
  - Rakefile
51
51
  - config/quickdraw.rb
52
+ - examples/custom_elements.rb
53
+ - examples/custom_elements_output.md
54
+ - examples/html_in_markdown.rb
55
+ - examples/html_in_markdown_output.md
56
+ - examples/llm_prompt.rb
57
+ - examples/llm_prompt_output.md
52
58
  - lib/mdphlex.rb
53
59
  - lib/mdphlex/md.rb
54
60
  - lib/mdphlex/version.rb
61
+ - quickdraw/html_escaping.test.rb
55
62
  - quickdraw/interoperability.test.rb
56
63
  - quickdraw/mdphlex.test.rb
57
64
  homepage: https://github.com/martinemde/mdphlex