@herb-tools/printer 0.6.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.
package/README.md ADDED
@@ -0,0 +1,123 @@
1
+ # Herb Syntax Tree Printer
2
+
3
+ **Package:** [`@herb-tools/printer`](https://www.npmjs.com/package/@herb-tools/printer)
4
+
5
+ ---
6
+
7
+ AST printer infrastructure for lossless HTML+ERB reconstruction and AST-to-source code conversion for the Herb Parser Syntax Tree.
8
+
9
+ ### Installation
10
+
11
+ :::code-group
12
+ ```shell [npm]
13
+ npm add @herb-tools/printer
14
+ ```
15
+
16
+ ```shell [pnpm]
17
+ pnpm add @herb-tools/printer
18
+ ```
19
+
20
+ ```shell [yarn]
21
+ yarn add @herb-tools/printer
22
+ ```
23
+
24
+ ```shell [bun]
25
+ bun add @herb-tools/printer
26
+ ```
27
+ :::
28
+
29
+ ### Usage
30
+
31
+ #### IdentityPrinter (Provides lossless reconstruction of the original source)
32
+
33
+ For lossless reconstruction of the original source:
34
+
35
+ ```javascript
36
+ import { IdentityPrinter } from '@herb-tools/printer'
37
+ import { Herb } from '@herb-tools/node-wasm'
38
+
39
+ await Herb.load()
40
+
41
+ const parseResult = Herb.parse(
42
+ '<div class="hello" > Hello </div>',
43
+ { track_whitespace: true }
44
+ )
45
+
46
+ const printer = new IdentityPrinter()
47
+ const output = printer.print(parseResult.value)
48
+
49
+ // output === '<div class="hello" > Hello </div>' (exact preservation)
50
+ ```
51
+
52
+ #### Custom Printers
53
+
54
+ Create custom printers by extending the base `Printer` class and override specific visitors for custom behavior:
55
+
56
+ ```typescript
57
+ import { Printer } from "@herb-tools/printer"
58
+ import { HTMLAttributeNode } from "@herb-tools/core"
59
+
60
+ class CustomPrinter extends Printer {
61
+ protected write(content: string) {
62
+ super.write(content.toUpperCase())
63
+ }
64
+
65
+ protected visitHTMLAttributeNode(node: HTMLAttributeNode) {
66
+ // do nothing to strip attributes
67
+ }
68
+ }
69
+ ```
70
+
71
+ and then printing the result using `print`
72
+
73
+ ```js
74
+ import { Herb } from "@herb-tools/node-wasm"
75
+
76
+ await Herb.load()
77
+
78
+ const parseResult = Herb.parse(
79
+ '<div class="hello"> Hello </div>',
80
+ { track_whitespace: true }
81
+ )
82
+
83
+ const printer = new CustomPrinter()
84
+ const output = printer.print(parseResult.value)
85
+
86
+ // output === '<div > HELLO </div>'
87
+ ```
88
+
89
+ #### Print Options
90
+
91
+ The printer supports options to control how nodes are printed:
92
+
93
+ ```typescript
94
+ import { IdentityPrinter, DEFAULT_PRINT_OPTIONS } from "@herb-tools/printer"
95
+ import type { PrintOptions } from "@herb-tools/printer"
96
+
97
+ const printer = new IdentityPrinter()
98
+
99
+ // Will throw error if node has parse errors (default behavior)
100
+ const output1 = printer.print(nodeWithErrors)
101
+
102
+ // Will print the node despite parse errors
103
+ const output2 = printer.print(nodeWithErrors, { ignoreErrors: true })
104
+ ```
105
+
106
+ When `ignoreErrors` is `false` (default), the printer will throw an error if you attempt to print a node that contains parse errors. Set `ignoreErrors` to `true` to print nodes with errors, which can be useful for debugging or partial AST reconstruction.
107
+
108
+ :::warning Important
109
+ The Printer expects the source to be parsed using the `track_whitespace: true` parser option for accurate source reconstruction.
110
+ :::
111
+
112
+ #### CLI Usage
113
+
114
+ ```bash
115
+ # Basic round-trip printing
116
+ herb-print input.html.erb > output.html.erb
117
+
118
+ # Verify parser accuracy
119
+ herb-print input.html.erb --verify
120
+
121
+ # Show parsing statistics
122
+ herb-print input.html.erb --stats
123
+ ```
package/bin/herb-print ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+
3
+ import("../dist/herb-print.js")
package/dist/cli.js ADDED
@@ -0,0 +1,190 @@
1
+ #!/usr/bin/env node
2
+ import dedent from "dedent";
3
+ import { readFileSync, writeFileSync } from "fs";
4
+ import { resolve } from "path";
5
+ import { glob } from "glob";
6
+ import { Herb } from "@herb-tools/node-wasm";
7
+ import { IdentityPrinter } from "./index.js";
8
+ export class CLI {
9
+ parseArgs(args) {
10
+ const options = {};
11
+ for (let i = 2; i < args.length; i++) {
12
+ const arg = args[i];
13
+ switch (arg) {
14
+ case '-i':
15
+ case '--input':
16
+ options.input = args[++i];
17
+ break;
18
+ case '-o':
19
+ case '--output':
20
+ options.output = args[++i];
21
+ break;
22
+ case '--verify':
23
+ options.verify = true;
24
+ break;
25
+ case '--stats':
26
+ options.stats = true;
27
+ break;
28
+ case '--glob':
29
+ options.glob = true;
30
+ break;
31
+ case '-h':
32
+ case '--help':
33
+ options.help = true;
34
+ break;
35
+ default:
36
+ if (!arg.startsWith('-') && !options.input) {
37
+ options.input = arg;
38
+ }
39
+ }
40
+ }
41
+ return options;
42
+ }
43
+ showHelp() {
44
+ console.log(dedent `
45
+ herb-print - Print HTML+ERB AST back to source code
46
+
47
+ This tool parses HTML+ERB templates and prints them back, preserving the original
48
+ formatting as closely as possible. Useful for testing parser accuracy and as a
49
+ baseline for other transformations.
50
+
51
+ Usage:
52
+ herb-print [options] <input-file-or-pattern>
53
+ herb-print -i <input-file> -o <output-file>
54
+
55
+ Options:
56
+ -i, --input <file> Input file path
57
+ -o, --output <file> Output file path (defaults to stdout)
58
+ --verify Verify that output matches input exactly
59
+ --stats Show parsing and printing statistics
60
+ --glob Treat input as glob pattern (default: **/*.html.erb)
61
+ -h, --help Show this help message
62
+
63
+ Examples:
64
+ # Single file
65
+ herb-print input.html.erb > output.html.erb
66
+ herb-print -i input.html.erb -o output.html.erb --verify
67
+ herb-print input.html.erb --stats
68
+
69
+ # Glob patterns (batch verification)
70
+ herb-print --glob --verify # All .html.erb files
71
+ herb-print "app/views/**/*.html.erb" --glob --verify --stats
72
+ herb-print "*.erb" --glob --verify
73
+ herb-print "/path/to/templates" --glob --verify # Directory (auto-appends /**/*.html.erb)
74
+ herb-print "/path/to/templates/**/*.html.erb" --glob --verify
75
+
76
+ # The --verify flag is useful to test parser fidelity:
77
+ herb-print input.html.erb --verify
78
+ # Checks if parsing and printing results in identical content
79
+ `);
80
+ }
81
+ async run() {
82
+ const options = this.parseArgs(process.argv);
83
+ if (options.help || (!options.input && !options.glob)) {
84
+ this.showHelp();
85
+ process.exit(0);
86
+ }
87
+ try {
88
+ await Herb.load();
89
+ if (options.glob) {
90
+ const pattern = options.input || "**/*.html.erb";
91
+ const files = await glob(pattern);
92
+ if (files.length === 0) {
93
+ console.error(`No files found matching pattern: ${pattern}`);
94
+ process.exit(1);
95
+ }
96
+ let totalFiles = 0;
97
+ let failedFiles = 0;
98
+ let verificationFailures = 0;
99
+ let totalBytes = 0;
100
+ console.log(`Processing ${files.length} files...\n`);
101
+ for (const file of files) {
102
+ try {
103
+ const input = readFileSync(file, 'utf-8');
104
+ const parseResult = Herb.parse(input, { track_whitespace: true });
105
+ if (parseResult.errors.length > 0) {
106
+ console.error(`\x1b[31m✗\x1b[0m \x1b[1m${file}\x1b[0m: \x1b[1m\x1b[31mFailed\x1b[0m to parse`);
107
+ failedFiles++;
108
+ continue;
109
+ }
110
+ const printer = new IdentityPrinter();
111
+ const output = printer.print(parseResult.value);
112
+ totalFiles++;
113
+ totalBytes += input.length;
114
+ if (options.verify) {
115
+ if (input === output) {
116
+ console.log(`\x1b[32m✓\x1b[0m \x1b[1m${file}\x1b[0m: \x1b[32mPerfect match\x1b[0m`);
117
+ }
118
+ else {
119
+ console.error(`\x1b[31m✗\x1b[0m \x1b[1m${file}\x1b[0m: \x1b[1m\x1b[31mVerification failed\x1b[0m - differences detected`);
120
+ verificationFailures++;
121
+ }
122
+ }
123
+ else {
124
+ console.log(`\x1b[32m✓\x1b[0m \x1b[1m${file}\x1b[0m: \x1b[32mProcessed\x1b[0m`);
125
+ }
126
+ }
127
+ catch (error) {
128
+ console.error(`\x1b[31m✗\x1b[0m \x1b[1m${file}\x1b[0m: \x1b[1m\x1b[31mError\x1b[0m - ${error}`);
129
+ failedFiles++;
130
+ }
131
+ }
132
+ console.log(`\nSummary:`);
133
+ console.log(` Files processed: ${totalFiles}`);
134
+ console.log(` Files failed: ${failedFiles}`);
135
+ if (options.verify) {
136
+ console.log(` Verifications: ${totalFiles - verificationFailures} passed, ${verificationFailures} failed`);
137
+ }
138
+ if (options.stats) {
139
+ console.log(` Total bytes: ${totalBytes}`);
140
+ }
141
+ process.exit(failedFiles > 0 || verificationFailures > 0 ? 1 : 0);
142
+ }
143
+ else {
144
+ const inputPath = resolve(options.input);
145
+ const input = readFileSync(inputPath, 'utf-8');
146
+ const parseResult = Herb.parse(input, { track_whitespace: true });
147
+ if (parseResult.errors.length > 0) {
148
+ console.error('Parse errors:', parseResult.errors.map(e => e.message).join(', '));
149
+ process.exit(1);
150
+ }
151
+ const printer = new IdentityPrinter();
152
+ const output = printer.print(parseResult.value);
153
+ if (options.output) {
154
+ const outputPath = resolve(options.output);
155
+ writeFileSync(outputPath, output, 'utf-8');
156
+ console.log(`Output written to: ${outputPath}`);
157
+ }
158
+ else {
159
+ console.log(output);
160
+ }
161
+ if (options.verify) {
162
+ if (input === output) {
163
+ console.error('\x1b[32m✓ Verification passed\x1b[0m - output matches input exactly');
164
+ }
165
+ else {
166
+ console.error('\x1b[31m✗ Verification failed\x1b[0m - output differs from input');
167
+ process.exit(1);
168
+ }
169
+ }
170
+ if (options.stats) {
171
+ const errors = parseResult.errors?.length || 0;
172
+ const warnings = parseResult.warnings?.length || 0;
173
+ console.error(dedent `
174
+ Printing Statistics:
175
+ Input size: ${input.length} bytes
176
+ Output size: ${output.length} bytes
177
+ Parse errors: ${errors}
178
+ Parse warnings: ${warnings}
179
+ Round-trip: ${input === output ? 'Perfect' : 'Differences detected'}
180
+ `);
181
+ }
182
+ }
183
+ }
184
+ catch (error) {
185
+ console.error('Error:', error instanceof Error ? error.message : error);
186
+ process.exit(1);
187
+ }
188
+ }
189
+ }
190
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}