Skip to content

Commit 9b0d1ef

Browse files
authored
Playground: Add ActionView → HTML rewriter tab (#1457)
This pull request adds a "Rewrite" that uses the `ActionViewTagHelperToHTMLRewriter` rewriter to transform the ActionView Tag helpers to pure HTML+ERB: <img width="4008" height="1334" alt="CleanShot 2026-03-22 at 15 35 23@2x" src="https://github.com/user-attachments/assets/1df738ca-0879-48f1-acff-e8e855902d34" />
1 parent 87cebc2 commit 9b0d1ef

File tree

4 files changed

+97
-1
lines changed

4 files changed

+97
-1
lines changed

playground/index.html

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,16 @@
373373
<span class="ml-1">Diagnostics</span>
374374
</button>
375375

376+
<button
377+
data-playground-target="viewerButton"
378+
data-viewer="rewrite"
379+
data-action="click->playground#selectViewer"
380+
class="text-gray-900 dark:text-gray-100 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-md px-3 py-1.5 text-sm font-medium data-[active=true]:!bg-green-600 data-[active=true]:hover:!bg-green-700 data-[active=true]:!text-white"
381+
>
382+
<i class="fas fa-wand-magic-sparkles"></i>
383+
<span class="ml-1">Rewrite</span>
384+
</button>
385+
376386
<button
377387
disabled
378388
data-playground-target="viewerButton"
@@ -550,6 +560,35 @@
550560
class="hidden w-full p-3 mb-3 rounded overflow-auto font-mono bg-[#282c34] text-[#dcdfe4] highlight h-[50vh] md:h-[calc(100vh-110px)] overflow-scroll"
551561
></pre>
552562

563+
<div
564+
data-viewer-target="rewrite"
565+
data-playground-target="rewriteViewer"
566+
data-action="click->playground#shrinkViewer"
567+
class="hidden w-full mb-3 rounded overflow-auto bg-[#282c34] h-[50vh] md:h-[calc(100vh-110px)] overflow-scroll"
568+
>
569+
<div class="p-3 border-b border-gray-600 flex gap-4 items-center justify-between">
570+
<div class="flex gap-2 items-center">
571+
<span class="text-sm text-gray-300 mr-2">Rewriter:</span>
572+
<span data-playground-target="rewriteStatus" class="px-2 py-1 text-xs rounded font-medium"></span>
573+
</div>
574+
<div class="flex items-center gap-2">
575+
<label class="flex items-center gap-2 text-gray-300 text-sm">
576+
<input
577+
type="checkbox"
578+
data-playground-target="rewriteActionViewHelpers"
579+
data-action="change->playground#onRewriteActionViewHelpersChange"
580+
class="rounded border-gray-300 dark:border-gray-600 text-green-600 focus:ring-green-500 dark:focus:ring-green-400"
581+
/>
582+
<span class="select-none">Action View helpers</span>
583+
</label>
584+
</div>
585+
</div>
586+
587+
<div class="w-full h-full overflow-auto">
588+
<pre data-playground-target="rewriteOutput" class="w-full p-3 m-0 font-mono bg-[#282c34] text-[#dcdfe4] language-html highlight" style="white-space: pre; line-height: 1.3"></pre>
589+
</div>
590+
</div>
591+
553592
<div
554593
data-viewer-target="format"
555594
data-playground-target="formatViewer"

playground/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"@herb-tools/browser": "0.9.2",
2323
"@herb-tools/formatter": "0.9.2",
2424
"@herb-tools/linter": "0.9.2",
25+
"@herb-tools/rewriter": "0.9.2",
2526
"@hotwired/stimulus": "^3.2.2",
2627
"dedent": "^1.7.0",
2728
"express": "^5.2.1",

playground/src/analyze.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { HerbBackend, ParseResult, LexResult, ParserOptions } from "@herb-t
33
import { Formatter } from "@herb-tools/formatter"
44
import { Linter } from "@herb-tools/linter"
55
import { IdentityPrinter, DEFAULT_PRINT_OPTIONS } from "@herb-tools/printer"
6+
import { rewrite, ActionViewTagHelperToHTMLRewriter } from "@herb-tools/rewriter"
67

78
import type { LintResult } from "@herb-tools/linter"
89
import type { FormatOptions } from "@herb-tools/formatter"
@@ -62,6 +63,19 @@ export async function analyze(herb: HerbBackend, source: string, options: Parser
6263
new Promise((resolve) => resolve((new IdentityPrinter()).print(parseResult.value, printerOptions))),
6364
)
6465

66+
let rewritten: string | null = null
67+
68+
if (parseResult && parseResult.value) {
69+
rewritten = await safeExecute<string>(
70+
new Promise((resolve) => {
71+
const rewriteParseResult = herb.parse(source, { ...options, track_whitespace: true })
72+
const rewriter = new ActionViewTagHelperToHTMLRewriter()
73+
const { output } = rewrite(rewriteParseResult.value, [rewriter], { baseDir: "/" })
74+
resolve(output)
75+
}),
76+
)
77+
}
78+
6579
let lintResult: LintResult | null = null
6680

6781
if (parseResult && parseResult.value) {
@@ -84,6 +98,7 @@ export async function analyze(herb: HerbBackend, source: string, options: Parser
8498
html,
8599
formatted,
86100
printed,
101+
rewritten,
87102
version,
88103
lintResult,
89104
duration: endTime - startTime,

playground/src/controllers/playground_controller.js

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ export default class extends Controller {
7777
"parserOptions",
7878
"rubyViewer",
7979
"htmlViewer",
80+
"rewriteViewer",
81+
"rewriteOutput",
82+
"rewriteStatus",
83+
"rewriteActionViewHelpers",
8084
"lexViewer",
8185
"formatViewer",
8286
"formatSuccess",
@@ -373,6 +377,9 @@ export default class extends Controller {
373377
case 'html':
374378
content = this.htmlViewerTarget.textContent
375379
break
380+
case 'rewrite':
381+
content = this.rewriteOutputTarget.textContent
382+
break
376383
case 'format':
377384
if (!this.formatSuccessTarget.classList.contains('hidden')) {
378385
content = this.formatSuccessTarget.textContent
@@ -522,7 +529,7 @@ export default class extends Controller {
522529
}
523530

524531
isValidTab(tab) {
525-
const validTabs = ['parse', 'lex', 'ruby', 'html', 'format', 'printer', 'diagnostics', 'full']
532+
const validTabs = ['parse', 'lex', 'ruby', 'html', 'format', 'printer', 'diagnostics', 'rewrite', 'full']
526533
return validTabs.includes(tab)
527534
}
528535

@@ -934,6 +941,28 @@ export default class extends Controller {
934941
Prism.highlightElement(this.htmlViewerTarget)
935942
}
936943

944+
if (this.hasRewriteViewerTarget) {
945+
const options = this.getParserOptions()
946+
947+
if (this.hasRewriteActionViewHelpersTarget) {
948+
this.rewriteActionViewHelpersTarget.checked = !!options.action_view_helpers
949+
}
950+
951+
if (!options.action_view_helpers) {
952+
this.rewriteStatusTarget.textContent = '⚠ Enable "Action View helpers" option'
953+
this.rewriteStatusTarget.className = 'px-2 py-1 text-xs rounded font-medium bg-yellow-600 text-yellow-100'
954+
this.rewriteOutputTarget.classList.remove("language-html")
955+
this.rewriteOutputTarget.textContent = ''
956+
} else {
957+
this.rewriteStatusTarget.textContent = 'ActionView Tag Helper → HTML'
958+
this.rewriteStatusTarget.className = 'px-2 py-1 text-xs rounded font-medium bg-green-600 text-green-100'
959+
this.rewriteOutputTarget.classList.add("language-html")
960+
this.rewriteOutputTarget.textContent = result.rewritten || 'No rewritten output available'
961+
962+
Prism.highlightElement(this.rewriteOutputTarget)
963+
}
964+
}
965+
937966
const hasParserErrors = result.parseResult ? result.parseResult.recursiveErrors().length > 0 : false
938967
const currentSource = this.editor ? this.editor.getValue() : this.inputTarget.value
939968
const isWellFormatted = currentSource === result.formatted
@@ -1232,6 +1261,18 @@ export default class extends Controller {
12321261
this.analyze()
12331262
}
12341263

1264+
onRewriteActionViewHelpersChange(event) {
1265+
const checked = event.target.checked
1266+
const parserCheckbox = this.parserOptionsTarget.querySelector('input[data-option="action_view_helpers"]')
1267+
1268+
if (parserCheckbox) {
1269+
parserCheckbox.checked = checked
1270+
}
1271+
1272+
this.updateURL()
1273+
this.analyze()
1274+
}
1275+
12351276
onFormatterOptionChange(_event) {
12361277
this.updateURL()
12371278
this.analyze()

0 commit comments

Comments
 (0)