@herb-tools/formatter 0.4.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 +59 -0
- package/bin/herb-formatter +3 -0
- package/dist/herb-formatter.js +9070 -0
- package/dist/herb-formatter.js.map +1 -0
- package/dist/index.cjs +3215 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.esm.js +3211 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/types/cli.d.ts +5 -0
- package/dist/types/formatter.d.ts +16 -0
- package/dist/types/herb-formatter.d.ts +2 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/options.d.ts +22 -0
- package/dist/types/printer.d.ts +55 -0
- package/package.json +47 -0
- package/src/cli.ts +70 -0
- package/src/formatter.ts +36 -0
- package/src/herb-formatter.ts +6 -0
- package/src/index.ts +5 -0
- package/src/options.ts +34 -0
- package/src/printer.ts +635 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formatting options for the Herb formatter.
|
|
3
|
+
*
|
|
4
|
+
* indentWidth: number of spaces per indentation level.
|
|
5
|
+
* maxLineLength: maximum line length before wrapping text or attributes.
|
|
6
|
+
*/
|
|
7
|
+
export interface FormatOptions {
|
|
8
|
+
/** number of spaces per indentation level; defaults to 2 */
|
|
9
|
+
indentWidth?: number;
|
|
10
|
+
/** maximum line length before wrapping; defaults to 80 */
|
|
11
|
+
maxLineLength?: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Default values for formatting options.
|
|
15
|
+
*/
|
|
16
|
+
export declare const defaultFormatOptions: Required<FormatOptions>;
|
|
17
|
+
/**
|
|
18
|
+
* Merge provided options with defaults for any missing values.
|
|
19
|
+
* @param options partial formatting options
|
|
20
|
+
* @returns a complete set of formatting options
|
|
21
|
+
*/
|
|
22
|
+
export declare function resolveFormatOptions(options?: FormatOptions): Required<FormatOptions>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Visitor } from "@herb-tools/core";
|
|
2
|
+
import { Node, DocumentNode, HTMLOpenTagNode, HTMLCloseTagNode, HTMLSelfCloseTagNode, HTMLElementNode, HTMLAttributeNode, HTMLAttributeValueNode, HTMLAttributeNameNode, HTMLTextNode, HTMLCommentNode, HTMLDoctypeNode, ERBContentNode, ERBBlockNode, ERBEndNode, ERBElseNode, ERBIfNode, ERBWhenNode, ERBCaseNode, ERBCaseMatchNode, ERBWhileNode, ERBUntilNode, ERBForNode, ERBRescueNode, ERBEnsureNode, ERBBeginNode, ERBUnlessNode, ERBYieldNode, ERBInNode, Token } from "@herb-tools/core";
|
|
3
|
+
import type { FormatOptions } from "./options.js";
|
|
4
|
+
/**
|
|
5
|
+
* Printer traverses the Herb AST using the Visitor pattern
|
|
6
|
+
* and emits a formatted string with proper indentation, line breaks, and attribute wrapping.
|
|
7
|
+
*/
|
|
8
|
+
export declare class Printer extends Visitor {
|
|
9
|
+
private indentWidth;
|
|
10
|
+
private maxLineLength;
|
|
11
|
+
private source;
|
|
12
|
+
private lines;
|
|
13
|
+
private indentLevel;
|
|
14
|
+
constructor(source: string, options: Required<FormatOptions>);
|
|
15
|
+
print(object: Node | Token, indentLevel?: number): string;
|
|
16
|
+
private push;
|
|
17
|
+
private withIndent;
|
|
18
|
+
private indent;
|
|
19
|
+
/**
|
|
20
|
+
* Print an ERB tag (<% %> or <%= %>) with single spaces around inner content.
|
|
21
|
+
*/
|
|
22
|
+
private printERBNode;
|
|
23
|
+
visitDocumentNode(node: DocumentNode): void;
|
|
24
|
+
visitHTMLElementNode(node: HTMLElementNode): void;
|
|
25
|
+
visitHTMLOpenTagNode(node: HTMLOpenTagNode): void;
|
|
26
|
+
visitHTMLSelfCloseTagNode(node: HTMLSelfCloseTagNode): void;
|
|
27
|
+
visitHTMLCloseTagNode(node: HTMLCloseTagNode): void;
|
|
28
|
+
visitHTMLTextNode(node: HTMLTextNode): void;
|
|
29
|
+
visitHTMLAttributeNode(node: HTMLAttributeNode): void;
|
|
30
|
+
visitHTMLAttributeNameNode(node: HTMLAttributeNameNode): void;
|
|
31
|
+
visitHTMLAttributeValueNode(node: HTMLAttributeValueNode): void;
|
|
32
|
+
visitHTMLCommentNode(node: HTMLCommentNode): void;
|
|
33
|
+
visitERBCommentNode(node: ERBContentNode): void;
|
|
34
|
+
visitHTMLDoctypeNode(node: HTMLDoctypeNode): void;
|
|
35
|
+
visitERBContentNode(node: ERBContentNode): void;
|
|
36
|
+
visitERBEndNode(node: ERBEndNode): void;
|
|
37
|
+
visitERBYieldNode(node: ERBYieldNode): void;
|
|
38
|
+
visitERBInNode(node: ERBInNode): void;
|
|
39
|
+
visitERBCaseMatchNode(node: ERBCaseMatchNode): void;
|
|
40
|
+
visitERBBlockNode(node: ERBBlockNode): void;
|
|
41
|
+
visitERBIfNode(node: ERBIfNode): void;
|
|
42
|
+
visitERBElseNode(node: ERBElseNode): void;
|
|
43
|
+
visitERBWhenNode(node: ERBWhenNode): void;
|
|
44
|
+
visitERBCaseNode(node: ERBCaseNode): void;
|
|
45
|
+
visitERBBeginNode(node: ERBBeginNode): void;
|
|
46
|
+
visitERBWhileNode(node: ERBWhileNode): void;
|
|
47
|
+
visitERBUntilNode(node: ERBUntilNode): void;
|
|
48
|
+
visitERBForNode(node: ERBForNode): void;
|
|
49
|
+
visitERBRescueNode(node: ERBRescueNode): void;
|
|
50
|
+
visitERBEnsureNode(node: ERBEnsureNode): void;
|
|
51
|
+
visitERBUnlessNode(node: ERBUnlessNode): void;
|
|
52
|
+
private visitERBGeneric;
|
|
53
|
+
private renderInlineOpen;
|
|
54
|
+
renderAttribute(attribute: HTMLAttributeNode): string;
|
|
55
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@herb-tools/formatter",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"homepage": "https://herb-tools.dev",
|
|
7
|
+
"bugs": "https://github.com/marcoroth/herb/issues/new?title=Package%20%60@herb-tools/formatter%60:%20",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/marcoroth/herb.git",
|
|
11
|
+
"directory": "javascript/packages/formatter"
|
|
12
|
+
},
|
|
13
|
+
"main": "./dist/index.cjs",
|
|
14
|
+
"module": "./dist/index.esm.js",
|
|
15
|
+
"require": "./dist/index.cjs",
|
|
16
|
+
"types": "./dist/types/index.d.ts",
|
|
17
|
+
"bin": {
|
|
18
|
+
"herb-formatter": "bin/herb-formatter"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "yarn clean && rollup -c rollup.config.mjs",
|
|
22
|
+
"dev": "rollup -c rollup.config.mjs -w",
|
|
23
|
+
"clean": "rimraf dist",
|
|
24
|
+
"test": "vitest run",
|
|
25
|
+
"test:watch": "vitest --watch",
|
|
26
|
+
"prepublishOnly": "yarn clean && yarn build && yarn test"
|
|
27
|
+
},
|
|
28
|
+
"exports": {
|
|
29
|
+
"./package.json": "./package.json",
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./dist/types/index.d.ts",
|
|
32
|
+
"import": "./dist/index.esm.js",
|
|
33
|
+
"require": "./dist/index.cjs",
|
|
34
|
+
"default": "./dist/index.esm.js"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@herb-tools/core": "0.4.0"
|
|
39
|
+
},
|
|
40
|
+
"files": [
|
|
41
|
+
"package.json",
|
|
42
|
+
"README.md",
|
|
43
|
+
"dist/",
|
|
44
|
+
"src/",
|
|
45
|
+
"bin/"
|
|
46
|
+
]
|
|
47
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { readFileSync } from "fs"
|
|
2
|
+
import { Herb } from "@herb-tools/node-wasm"
|
|
3
|
+
import { Formatter } from "./formatter.js"
|
|
4
|
+
import { name, version } from "../package.json"
|
|
5
|
+
|
|
6
|
+
export class CLI {
|
|
7
|
+
private usage = `
|
|
8
|
+
Usage: herb-formatter [file] [options]
|
|
9
|
+
|
|
10
|
+
Arguments:
|
|
11
|
+
file File to format (use '-' or omit for stdin)
|
|
12
|
+
|
|
13
|
+
Options:
|
|
14
|
+
-h, --help show help
|
|
15
|
+
-v, --version show version
|
|
16
|
+
|
|
17
|
+
Examples:
|
|
18
|
+
herb-formatter templates/index.html.erb
|
|
19
|
+
cat template.html.erb | herb-formatter
|
|
20
|
+
herb-formatter - < template.html.erb
|
|
21
|
+
`
|
|
22
|
+
|
|
23
|
+
async run() {
|
|
24
|
+
const args = process.argv.slice(2)
|
|
25
|
+
|
|
26
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
27
|
+
console.log(this.usage)
|
|
28
|
+
process.exit(0)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
await Herb.load()
|
|
33
|
+
|
|
34
|
+
if (args.includes("--version") || args.includes("-v")) {
|
|
35
|
+
console.log("Versions:")
|
|
36
|
+
console.log(` ${name}@${version}, ${Herb.version}`.split(", ").join("\n "))
|
|
37
|
+
process.exit(0)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let source: string
|
|
41
|
+
|
|
42
|
+
// Find the first non-flag argument (the file)
|
|
43
|
+
const file = args.find(arg => !arg.startsWith("-"))
|
|
44
|
+
|
|
45
|
+
// Read from file or stdin
|
|
46
|
+
if (file && file !== "-") {
|
|
47
|
+
source = readFileSync(file, "utf-8")
|
|
48
|
+
} else {
|
|
49
|
+
source = await this.readStdin()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const formatter = new Formatter(Herb)
|
|
53
|
+
const result = formatter.format(source)
|
|
54
|
+
process.stdout.write(result)
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.error(error)
|
|
57
|
+
process.exit(1)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private async readStdin(): Promise<string> {
|
|
62
|
+
const chunks: Buffer[] = []
|
|
63
|
+
|
|
64
|
+
for await (const chunk of process.stdin) {
|
|
65
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return Buffer.concat(chunks).toString("utf8")
|
|
69
|
+
}
|
|
70
|
+
}
|
package/src/formatter.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Printer } from "./printer.js"
|
|
2
|
+
import { resolveFormatOptions } from "./options.js"
|
|
3
|
+
|
|
4
|
+
import type { FormatOptions } from "./options.js"
|
|
5
|
+
import type { HerbBackend, ParseResult } from "@herb-tools/core"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Formatter uses a Herb Backend to parse the source and then
|
|
9
|
+
* formats the resulting AST into a well-indented, wrapped string.
|
|
10
|
+
*/
|
|
11
|
+
export class Formatter {
|
|
12
|
+
private herb: HerbBackend
|
|
13
|
+
private options: Required<FormatOptions>
|
|
14
|
+
|
|
15
|
+
constructor(herb: HerbBackend, options: FormatOptions = {}) {
|
|
16
|
+
this.herb = herb
|
|
17
|
+
this.options = resolveFormatOptions(options)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Format a source string, optionally overriding format options per call.
|
|
22
|
+
*/
|
|
23
|
+
format(source: string, options: FormatOptions = {}): string {
|
|
24
|
+
const result = this.parse(source)
|
|
25
|
+
if (result.failed) return source
|
|
26
|
+
|
|
27
|
+
const resolvedOptions = resolveFormatOptions({ ...this.options, ...options })
|
|
28
|
+
|
|
29
|
+
return new Printer(source, resolvedOptions).print(result.value)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private parse(source: string): ParseResult {
|
|
33
|
+
this.herb.ensureBackend()
|
|
34
|
+
return this.herb.parse(source)
|
|
35
|
+
}
|
|
36
|
+
}
|
package/src/index.ts
ADDED
package/src/options.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formatting options for the Herb formatter.
|
|
3
|
+
*
|
|
4
|
+
* indentWidth: number of spaces per indentation level.
|
|
5
|
+
* maxLineLength: maximum line length before wrapping text or attributes.
|
|
6
|
+
*/
|
|
7
|
+
export interface FormatOptions {
|
|
8
|
+
/** number of spaces per indentation level; defaults to 2 */
|
|
9
|
+
indentWidth?: number
|
|
10
|
+
/** maximum line length before wrapping; defaults to 80 */
|
|
11
|
+
maxLineLength?: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Default values for formatting options.
|
|
16
|
+
*/
|
|
17
|
+
export const defaultFormatOptions: Required<FormatOptions> = {
|
|
18
|
+
indentWidth: 2,
|
|
19
|
+
maxLineLength: 80,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Merge provided options with defaults for any missing values.
|
|
24
|
+
* @param options partial formatting options
|
|
25
|
+
* @returns a complete set of formatting options
|
|
26
|
+
*/
|
|
27
|
+
export function resolveFormatOptions(
|
|
28
|
+
options: FormatOptions = {},
|
|
29
|
+
): Required<FormatOptions> {
|
|
30
|
+
return {
|
|
31
|
+
indentWidth: options.indentWidth ?? defaultFormatOptions.indentWidth,
|
|
32
|
+
maxLineLength: options.maxLineLength ?? defaultFormatOptions.maxLineLength,
|
|
33
|
+
}
|
|
34
|
+
}
|