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 +4 -4
- data/CHANGELOG.md +4 -0
- data/README.md +211 -42
- data/examples/custom_elements.rb +106 -0
- data/examples/custom_elements_output.md +60 -0
- data/examples/html_in_markdown.rb +50 -0
- data/examples/html_in_markdown_output.md +16 -0
- data/examples/llm_prompt.rb +181 -0
- data/examples/llm_prompt_output.md +75 -0
- data/lib/mdphlex/md.rb +54 -1
- data/lib/mdphlex/version.rb +1 -1
- data/quickdraw/html_escaping.test.rb +145 -0
- data/quickdraw/mdphlex.test.rb +18 -0
- metadata +8 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7df543d1ff68faf2b25ce38d74a767d5957a4322ce83d7ad424190cd7ee0bd7f
|
4
|
+
data.tar.gz: 427fecb092f11651e9f5d463fd544ed6d6bb4891bfdd7a338c0e5312a51d5f35
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1f895e699d4d037f6f3aaf8d33a6ff9e263fb24a1ee07d455fcdd7c8e3fff11b707ba85a17b7f053c33e78fe864dbaf18e8e67a97c62e520aafd3a747af68aef
|
7
|
+
data.tar.gz: dc0f27b7faadd20203f588733b14236b6b1cbdd47db87aae3ec299a767a58534d51a5e1a5088f9b10021ce83f81d41554112e0d67ac1fb94d73a0a4874ee6698
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -1,74 +1,242 @@
|
|
1
1
|
# MDPhlex
|
2
2
|
|
3
|
-
MDPhlex is a Phlex component for rendering Markdown.
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
184
|
+
h1 @title
|
21
185
|
|
22
|
-
|
186
|
+
@sections.each do |section|
|
187
|
+
h2 section[:heading]
|
23
188
|
|
24
|
-
|
189
|
+
p section[:content]
|
25
190
|
|
26
|
-
|
27
|
-
|
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 "
|
41
|
-
a(href: "https://
|
42
|
-
plain "
|
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
|
-
|
48
|
-
|
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
|
-
|
215
|
+
puts article.call
|
216
|
+
~~~
|
52
217
|
|
53
|
-
|
54
|
-
# Hello, World!
|
218
|
+
Outputs:
|
55
219
|
|
56
|
-
|
220
|
+
~~~markdown
|
221
|
+
# Getting Started with Ruby
|
57
222
|
|
58
|
-
##
|
223
|
+
## Variables
|
59
224
|
|
60
|
-
|
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
|
-
|
227
|
+
```ruby
|
228
|
+
name = 'Alice'
|
229
|
+
age = 30
|
65
230
|
```
|
66
231
|
|
67
|
-
|
232
|
+
Learn more in the [documentation](https://docs.example.com).
|
233
|
+
~~~
|
68
234
|
|
69
|
-
|
235
|
+
## Rendering MDPhlex inside Phlex::HTML (and vice versa)
|
70
236
|
|
71
|
-
|
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
|
-
|
117
|
-
|
118
|
-
- **
|
119
|
-
- **
|
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: © & <div>"
|
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: © & <div>
|
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}=\"#{
|
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
|
data/lib/mdphlex/version.rb
CHANGED
@@ -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
|
data/quickdraw/mdphlex.test.rb
CHANGED
@@ -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.
|
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
|