@htnabe/prettier-plugin-hugo-post 0.0.1-rc.1
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/LICENSE +21 -0
- package/README.md +158 -0
- package/dist/index.d.mts +63 -0
- package/dist/index.mjs +374 -0
- package/package.json +85 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Chad Metcalf
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# @htnabe/prettier-plugin-hugo-post
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@htnabe/prettier-plugin-hugo-post)
|
|
4
|
+
[](https://github.com/htnabe/prettier-plugin-hugo-post)
|
|
5
|
+
[](https://codecov.io/gh/htnabe/prettier-plugin-hugo-post)
|
|
6
|
+
[](https://github.com/htnabe/prettier-plugin-hugo-post/blob/main/LICENSE)
|
|
7
|
+
|
|
8
|
+
A Prettier plugin for formatting Hugo content files that mix front matter, Markdown, and Go template syntax.
|
|
9
|
+
|
|
10
|
+
## Why this plugin?
|
|
11
|
+
|
|
12
|
+
Hugo content files are not plain Markdown. They often contain:
|
|
13
|
+
|
|
14
|
+
- YAML, TOML, or JSON front matter
|
|
15
|
+
- Markdown prose and lists
|
|
16
|
+
- Hugo shortcodes
|
|
17
|
+
- Go template expressions and pipelines
|
|
18
|
+
|
|
19
|
+
This plugin keeps those pieces formatted consistently while leaving the rest of your Prettier setup alone.
|
|
20
|
+
|
|
21
|
+
## Features
|
|
22
|
+
|
|
23
|
+
- Front matter formatting for YAML, TOML, and JSON
|
|
24
|
+
- Markdown formatting via Prettier
|
|
25
|
+
- Hugo shortcode spacing and normalization
|
|
26
|
+
- Template expression formatting for `.Title`, pipelines, conditions, and range blocks
|
|
27
|
+
- Works with standard Prettier overrides
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
Using Bun:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
bun add -d prettier @htnabe/prettier-plugin-hugo-post
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Using npm:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm install --save-dev prettier @htnabe/prettier-plugin-hugo-post
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
If you also format Hugo layout templates, install the companion plugin:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
bun add -d @htnabe/prettier-plugin-go-template
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Basic configuration
|
|
50
|
+
|
|
51
|
+
Add the plugin to Prettier and set the parser for Hugo content files:
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"plugins": ["@htnabe/prettier-plugin-hugo-post"],
|
|
56
|
+
"overrides": [
|
|
57
|
+
{
|
|
58
|
+
"files": ["content/**/*.md", "**/*.md", "**/*.hugo"],
|
|
59
|
+
"options": {
|
|
60
|
+
"parser": "hugo-post"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
For a mixed Hugo project, combine it with `@htnabe/prettier-plugin-go-template`:
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
{
|
|
71
|
+
"plugins": ["@htnabe/prettier-plugin-hugo-post", "@htnabe/prettier-plugin-go-template"],
|
|
72
|
+
"overrides": [
|
|
73
|
+
{
|
|
74
|
+
"files": ["content/**/*.md", "**/*.md"],
|
|
75
|
+
"options": {
|
|
76
|
+
"parser": "hugo-post"
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"files": ["layouts/**/*.html", "**/*.html"],
|
|
81
|
+
"options": {
|
|
82
|
+
"parser": "go-template"
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
]
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Usage
|
|
90
|
+
|
|
91
|
+
Format a single file:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
bunx prettier --write content/posts/my-post.md
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Check formatting without writing:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
bunx prettier --check "content/**/*.md"
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Example
|
|
104
|
+
|
|
105
|
+
This input:
|
|
106
|
+
|
|
107
|
+
```markdown
|
|
108
|
+
---
|
|
109
|
+
title: "My Post"
|
|
110
|
+
tags: [ "hugo", "blog" ]
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
{{<figure src="/img.jpg"alt="Test">}}
|
|
114
|
+
{{ .Title|upper }}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
is normalized to:
|
|
118
|
+
|
|
119
|
+
```markdown
|
|
120
|
+
---
|
|
121
|
+
title: "My Post"
|
|
122
|
+
tags: ["hugo", "blog"]
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
{{< figure src="/img.jpg" alt="Test" >}}
|
|
126
|
+
{{ .Title | upper }}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Project scripts
|
|
130
|
+
|
|
131
|
+
The repository uses these package scripts:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
bun test
|
|
135
|
+
bun run lint
|
|
136
|
+
bun run lint:fix
|
|
137
|
+
bun run format
|
|
138
|
+
bun run format:check
|
|
139
|
+
bun run example
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Contributing
|
|
143
|
+
|
|
144
|
+
Contributions are welcome. Please read the [contributing guide](docs/development/CONTRIBUTING.md) before opening a PR.
|
|
145
|
+
|
|
146
|
+
## License
|
|
147
|
+
|
|
148
|
+
MIT
|
|
149
|
+
|
|
150
|
+
## Acknowledgments
|
|
151
|
+
|
|
152
|
+
- [Prettier](https://prettier.io/) for the excellent formatting engine
|
|
153
|
+
- [@htnabe/prettier-plugin-go-template](https://github.com/htnabe/prettier-plugin-go-template) for inspiration on Go template formatting
|
|
154
|
+
- [Hugo](https://gohugo.io/) for the amazing static site generator
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
Made with ❤️ for the Hugo community.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
//#region src/types/hugo-post-node.d.ts
|
|
2
|
+
interface HugoPostNode {
|
|
3
|
+
source?: string;
|
|
4
|
+
}
|
|
5
|
+
//#endregion
|
|
6
|
+
//#region src/features/printers/printers.d.ts
|
|
7
|
+
declare const printers: {
|
|
8
|
+
'hugo-post-ast': {
|
|
9
|
+
print: typeof printHugoPost;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Print Hugo post content
|
|
14
|
+
*/
|
|
15
|
+
declare function printHugoPost(path: any, options: any): Promise<string>;
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region src/index.d.ts
|
|
18
|
+
declare const parsers: {
|
|
19
|
+
'hugo-post': {
|
|
20
|
+
parse: typeof parseHugoPost;
|
|
21
|
+
astFormat: string;
|
|
22
|
+
locStart: (_node: HugoPostNode) => number;
|
|
23
|
+
locEnd: (node: HugoPostNode) => number;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Parse Hugo post content
|
|
28
|
+
*/
|
|
29
|
+
declare function parseHugoPost(text: string): {
|
|
30
|
+
type: string;
|
|
31
|
+
source: string;
|
|
32
|
+
frontMatter: {
|
|
33
|
+
content: string;
|
|
34
|
+
delimiter: string | null;
|
|
35
|
+
} | null;
|
|
36
|
+
content: string;
|
|
37
|
+
};
|
|
38
|
+
declare const _default: {
|
|
39
|
+
languages: import("prettier").SupportLanguage[];
|
|
40
|
+
parsers: {
|
|
41
|
+
'hugo-post': {
|
|
42
|
+
parse: typeof parseHugoPost;
|
|
43
|
+
astFormat: string;
|
|
44
|
+
locStart: (_node: HugoPostNode) => number;
|
|
45
|
+
locEnd: (node: HugoPostNode) => number;
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
printers: {
|
|
49
|
+
'hugo-post-ast': {
|
|
50
|
+
print: (path: any, options: any) => Promise<string>;
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
options: {
|
|
54
|
+
hugoTemplateBracketSpacing: {
|
|
55
|
+
type: string;
|
|
56
|
+
category: string;
|
|
57
|
+
default: boolean;
|
|
58
|
+
description: string;
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
//#endregion
|
|
63
|
+
export { _default as default, parsers, printers };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
//#region src/config/options.ts
|
|
2
|
+
const options = { hugoTemplateBracketSpacing: {
|
|
3
|
+
type: "boolean",
|
|
4
|
+
category: "Hugo",
|
|
5
|
+
default: true,
|
|
6
|
+
description: "Print spaces between go template brackets"
|
|
7
|
+
} };
|
|
8
|
+
//#endregion
|
|
9
|
+
//#region src/config/languages.ts
|
|
10
|
+
const languages = [{
|
|
11
|
+
name: "Hugo Post",
|
|
12
|
+
parsers: ["hugo-post"],
|
|
13
|
+
extensions: [".md", ".hugo"],
|
|
14
|
+
filenames: []
|
|
15
|
+
}];
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region src/features/printers/printers.ts
|
|
18
|
+
const printers = { "hugo-post-ast": { print: printHugoPost } };
|
|
19
|
+
/**
|
|
20
|
+
* Print Hugo post content
|
|
21
|
+
*/
|
|
22
|
+
async function printHugoPost(path, options) {
|
|
23
|
+
const node = path.getValue();
|
|
24
|
+
const parts = [];
|
|
25
|
+
if (node.frontMatter) {
|
|
26
|
+
if (node.frontMatter.delimiter === "yaml") {
|
|
27
|
+
const formattedYaml = await formatYaml(node.frontMatter.content, options);
|
|
28
|
+
parts.push(`---\n${formattedYaml}\n---`);
|
|
29
|
+
} else if (node.frontMatter.delimiter === "toml") {
|
|
30
|
+
const formattedToml = await formatToml(node.frontMatter.content, options);
|
|
31
|
+
parts.push(`+++\n${formattedToml}\n+++`);
|
|
32
|
+
} else if (node.frontMatter.delimiter === "json") {
|
|
33
|
+
const formattedJson = await formatJson(node.frontMatter.content, options);
|
|
34
|
+
parts.push(formattedJson);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (node.content && node.content.trim()) {
|
|
38
|
+
const formattedContent = await formatHugoContent(node.content, options);
|
|
39
|
+
parts.push(formattedContent);
|
|
40
|
+
}
|
|
41
|
+
return parts.join("\n\n");
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Format TOML front matter using prettier-plugin-toml
|
|
45
|
+
*/
|
|
46
|
+
async function formatToml(tomlContent, options) {
|
|
47
|
+
try {
|
|
48
|
+
const { format } = await import("prettier");
|
|
49
|
+
return (await format(tomlContent, {
|
|
50
|
+
...options,
|
|
51
|
+
parser: "toml",
|
|
52
|
+
plugins: ["prettier-plugin-toml"]
|
|
53
|
+
})).trim();
|
|
54
|
+
} catch (error) {
|
|
55
|
+
if (error instanceof Error) console.warn("TOML formatting failed:", error.message);
|
|
56
|
+
else console.warn("TOML formatting failed with unknown error:", error);
|
|
57
|
+
return tomlContent.trim();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Format JSON front matter using Prettier's built-in JSON parser
|
|
62
|
+
*/
|
|
63
|
+
async function formatJson(jsonContent, options) {
|
|
64
|
+
try {
|
|
65
|
+
const { format } = await import("prettier");
|
|
66
|
+
return (await format(jsonContent, {
|
|
67
|
+
...options,
|
|
68
|
+
parser: "json"
|
|
69
|
+
})).trim();
|
|
70
|
+
} catch (error) {
|
|
71
|
+
if (error instanceof Error) console.warn("JSON formatting failed:", error.message);
|
|
72
|
+
else console.warn("JSON formatting failed with unknown error:", error);
|
|
73
|
+
return jsonContent.trim();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Format Hugo content (markdown + templates)
|
|
78
|
+
*/
|
|
79
|
+
async function formatHugoContent(content, options) {
|
|
80
|
+
content = formatHugoTemplates(content);
|
|
81
|
+
try {
|
|
82
|
+
const { format } = await import("prettier");
|
|
83
|
+
return (await format(content, {
|
|
84
|
+
...options,
|
|
85
|
+
parser: "markdown"
|
|
86
|
+
})).trim();
|
|
87
|
+
} catch {
|
|
88
|
+
return content.trim();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function formatHugoTemplates(content) {
|
|
92
|
+
try {
|
|
93
|
+
content = content.replace(/(\{\{[<%]\s*)(.*?)(\s*[>%]\}\})/g, (match, open, inner, _close) => {
|
|
94
|
+
try {
|
|
95
|
+
inner = inner.replace(/\/$/, "");
|
|
96
|
+
const formatted = formatShortcodeFromTokens(tokenizeShortcode(inner));
|
|
97
|
+
const isPercent = open.includes("%");
|
|
98
|
+
const openDelim = isPercent ? "{{% " : "{{< ";
|
|
99
|
+
const closeDelim = isPercent ? " %}}" : " >}}";
|
|
100
|
+
return openDelim + formatted + closeDelim;
|
|
101
|
+
} catch (error) {
|
|
102
|
+
if (error instanceof Error) console.warn(`Failed to format shortcode: ${match}. Error: ${error.message}`);
|
|
103
|
+
else console.warn(`Failed to format shortcode: ${match}. Unknown error:`, error);
|
|
104
|
+
return match;
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
content = content.replace(/\{\{(?!<|%|\/\*)\s*([^}]*?)\s*\}\}/g, (match, inner) => {
|
|
108
|
+
try {
|
|
109
|
+
return formatTemplateVariable(match, inner);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
if (error instanceof Error) console.warn(`Failed to format variable: ${match}. Error: ${error.message}`);
|
|
112
|
+
else console.warn(`Failed to format variable: ${match}. Unknown error:`, error);
|
|
113
|
+
return match;
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
content = content.replace(/\{\{\/\*\s*([\s\S]*?)\s*\*\/\}\}/g, (match, inner) => {
|
|
117
|
+
try {
|
|
118
|
+
return `{{/* ${inner.trim()} */}}`;
|
|
119
|
+
} catch (error) {
|
|
120
|
+
if (error instanceof Error) console.warn(`Failed to format comment: ${match}. Error: ${error.message}`);
|
|
121
|
+
else console.warn(`Failed to format comment: ${match}. Unknown error:`, error);
|
|
122
|
+
return match;
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
content = ensureProperBlockSpacing(content);
|
|
126
|
+
return content;
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (error instanceof Error) console.error(`Critical error in formatHugoTemplates: ${error.message}`);
|
|
129
|
+
else console.error(`Critical error in formatHugoTemplates with unknown error:`, error);
|
|
130
|
+
return content;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Format Hugo templates manually using regex
|
|
135
|
+
*/
|
|
136
|
+
/**
|
|
137
|
+
* Tokenization-based Hugo shortcode formatter
|
|
138
|
+
* Treats shortcode content as a mini-language to parse properly
|
|
139
|
+
*/
|
|
140
|
+
function tokenizeShortcode(content) {
|
|
141
|
+
const tokens = [];
|
|
142
|
+
let i = 0;
|
|
143
|
+
const maxIterations = Math.max(1e3, content.length * 2);
|
|
144
|
+
let iterations = 0;
|
|
145
|
+
while (i < content.length) {
|
|
146
|
+
iterations++;
|
|
147
|
+
if (iterations > maxIterations) {
|
|
148
|
+
console.warn(`Tokenizer stopped at position ${i} to prevent infinite loop. Content: ${content.substring(i, i + 20)}...`);
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
const char = content[i];
|
|
152
|
+
if (/\s/.test(char)) {
|
|
153
|
+
i++;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (char === "\"" || char === "'") {
|
|
157
|
+
const quote = char;
|
|
158
|
+
let value = quote;
|
|
159
|
+
i++;
|
|
160
|
+
while (i < content.length && iterations < maxIterations) {
|
|
161
|
+
iterations++;
|
|
162
|
+
const current = content[i];
|
|
163
|
+
if (current === "\\" && i + 1 < content.length) {
|
|
164
|
+
value += current + content[i + 1];
|
|
165
|
+
i += 2;
|
|
166
|
+
} else if (current === quote) {
|
|
167
|
+
value += current;
|
|
168
|
+
i++;
|
|
169
|
+
break;
|
|
170
|
+
} else {
|
|
171
|
+
value += current;
|
|
172
|
+
i++;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const trimmed = value.slice(1, -1).replace(/^\s+|\s+$/g, "");
|
|
176
|
+
tokens.push({
|
|
177
|
+
type: "quoted",
|
|
178
|
+
value: quote + trimmed + quote
|
|
179
|
+
});
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
let token = "";
|
|
183
|
+
while (i < content.length && !/[\s"']/.test(content[i]) && iterations < maxIterations) {
|
|
184
|
+
iterations++;
|
|
185
|
+
token += content[i];
|
|
186
|
+
i++;
|
|
187
|
+
}
|
|
188
|
+
if (token) tokens.push({
|
|
189
|
+
type: "unquoted",
|
|
190
|
+
value: token
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
return tokens;
|
|
194
|
+
}
|
|
195
|
+
function formatShortcodeFromTokens(tokens) {
|
|
196
|
+
if (tokens.length === 0) return "";
|
|
197
|
+
const result = [];
|
|
198
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
199
|
+
const token = tokens[i];
|
|
200
|
+
const nextToken = tokens[i + 1];
|
|
201
|
+
result.push(token.value);
|
|
202
|
+
if (nextToken) {
|
|
203
|
+
if (token.value.endsWith("=")) continue;
|
|
204
|
+
if (nextToken.value.startsWith("=")) continue;
|
|
205
|
+
if (token.type === "unquoted" && nextToken.type === "quoted") {
|
|
206
|
+
result.push(" ");
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
result.push(" ");
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return result.join("");
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Enhanced template variable formatter inspired by @htnabe/prettier-plugin-go-template
|
|
216
|
+
* Handles Go template syntax with better spacing and structure
|
|
217
|
+
*/
|
|
218
|
+
function formatTemplateVariable(match, inner) {
|
|
219
|
+
const startControl = match.match(/^\{\{-/) ? "{{- " : "{{ ";
|
|
220
|
+
const endControl = match.match(/-\}\}$/) ? " -}}" : " }}";
|
|
221
|
+
inner = inner.replace(/^-\s*/, "").replace(/\s*-$/, "");
|
|
222
|
+
return `${startControl}${formatTemplateExpression(inner.trim())}${endControl}`;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Ensure block-level control structures have proper spacing to prevent
|
|
226
|
+
* Prettier's markdown formatter from treating them as part of other constructs
|
|
227
|
+
*/
|
|
228
|
+
function ensureProperBlockSpacing(content) {
|
|
229
|
+
const lines = content.split("\n");
|
|
230
|
+
const result = [];
|
|
231
|
+
for (let i = 0; i < lines.length; i++) {
|
|
232
|
+
const line = lines[i];
|
|
233
|
+
const prevLine = i > 0 ? lines[i - 1] : "";
|
|
234
|
+
const isEndControl = line.trim().match(/^\{\{\s*end\s*\}\}$/);
|
|
235
|
+
const prevIsListItem = prevLine.trim().match(/^[-*+]\s/);
|
|
236
|
+
if (isEndControl && prevIsListItem) {
|
|
237
|
+
result.push("");
|
|
238
|
+
result.push(line);
|
|
239
|
+
} else result.push(line);
|
|
240
|
+
}
|
|
241
|
+
return result.join("\n");
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Format a template expression with proper spacing and structure
|
|
245
|
+
*/
|
|
246
|
+
function formatTemplateExpression(expr) {
|
|
247
|
+
if (!expr) return "";
|
|
248
|
+
if (expr.match(/^\s*(if|range|with|block|define|template)\b/)) return formatControlStructure(expr);
|
|
249
|
+
if (expr.match(/^\s*end\s*$/)) return "end";
|
|
250
|
+
if (expr.match(/^\s*else(\s+if\b)?/)) return formatControlStructure(expr);
|
|
251
|
+
return formatExpression(expr);
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Format control structures like if, range, with
|
|
255
|
+
*/
|
|
256
|
+
function formatControlStructure(expr) {
|
|
257
|
+
return expr.trim().replace(/\s+/g, " ");
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Format regular expressions with proper pipe spacing and function calls
|
|
261
|
+
*/
|
|
262
|
+
function formatExpression(expr) {
|
|
263
|
+
if (expr.includes("|")) return formatPipeExpression(expr);
|
|
264
|
+
if (expr.includes(" ")) return formatFunctionCall(expr);
|
|
265
|
+
return expr.trim();
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Format pipe expressions with proper spacing
|
|
269
|
+
*/
|
|
270
|
+
function formatPipeExpression(expr) {
|
|
271
|
+
return expr.split("|").map((part) => formatFunctionCall(part.trim())).filter((part) => part).join(" | ");
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Format function calls with proper argument spacing
|
|
275
|
+
*/
|
|
276
|
+
function formatFunctionCall(expr) {
|
|
277
|
+
const parts = [];
|
|
278
|
+
let current = "";
|
|
279
|
+
let inQuotes = false;
|
|
280
|
+
let quoteChar = "";
|
|
281
|
+
for (let i = 0; i < expr.length; i++) {
|
|
282
|
+
const char = expr[i];
|
|
283
|
+
if ((char === "\"" || char === "'") && !inQuotes) {
|
|
284
|
+
inQuotes = true;
|
|
285
|
+
quoteChar = char;
|
|
286
|
+
current += char;
|
|
287
|
+
} else if (char === quoteChar && inQuotes) {
|
|
288
|
+
inQuotes = false;
|
|
289
|
+
current += char;
|
|
290
|
+
parts.push(current.trim());
|
|
291
|
+
current = "";
|
|
292
|
+
} else if (char === " " && !inQuotes) {
|
|
293
|
+
if (current.trim()) {
|
|
294
|
+
parts.push(current.trim());
|
|
295
|
+
current = "";
|
|
296
|
+
}
|
|
297
|
+
} else current += char;
|
|
298
|
+
}
|
|
299
|
+
if (current.trim()) parts.push(current.trim());
|
|
300
|
+
return parts.join(" ");
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Format YAML front matter using Prettier
|
|
304
|
+
*/
|
|
305
|
+
async function formatYaml(yamlContent, options) {
|
|
306
|
+
try {
|
|
307
|
+
const { format } = await import("prettier");
|
|
308
|
+
return (await format(yamlContent, {
|
|
309
|
+
...options,
|
|
310
|
+
parser: "yaml"
|
|
311
|
+
})).trim();
|
|
312
|
+
} catch {
|
|
313
|
+
return yamlContent.trim();
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
//#endregion
|
|
317
|
+
//#region src/index.ts
|
|
318
|
+
const parsers = { "hugo-post": {
|
|
319
|
+
parse: parseHugoPost,
|
|
320
|
+
astFormat: "hugo-post-ast",
|
|
321
|
+
locStart: (_node) => 0,
|
|
322
|
+
locEnd: (node) => node.source?.length || 0
|
|
323
|
+
} };
|
|
324
|
+
/**
|
|
325
|
+
* Parse Hugo post content
|
|
326
|
+
*/
|
|
327
|
+
function parseHugoPost(text) {
|
|
328
|
+
const parts = splitFrontMatter(text);
|
|
329
|
+
return {
|
|
330
|
+
type: "hugo-post",
|
|
331
|
+
source: text,
|
|
332
|
+
frontMatter: parts.frontMatter !== null ? {
|
|
333
|
+
content: parts.frontMatter,
|
|
334
|
+
delimiter: parts.delimiter
|
|
335
|
+
} : null,
|
|
336
|
+
content: parts.content || ""
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Split text into front matter and content
|
|
341
|
+
*/
|
|
342
|
+
function splitFrontMatter(text) {
|
|
343
|
+
const yamlMatch = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
|
|
344
|
+
if (yamlMatch) return {
|
|
345
|
+
frontMatter: yamlMatch[1],
|
|
346
|
+
delimiter: "yaml",
|
|
347
|
+
content: yamlMatch[2]
|
|
348
|
+
};
|
|
349
|
+
const tomlMatch = text.match(/^\+\+\+\r?\n([\s\S]*?)\r?\n\+\+\+\r?\n([\s\S]*)$/);
|
|
350
|
+
if (tomlMatch) return {
|
|
351
|
+
frontMatter: tomlMatch[1],
|
|
352
|
+
delimiter: "toml",
|
|
353
|
+
content: tomlMatch[2]
|
|
354
|
+
};
|
|
355
|
+
const jsonMatch = text.match(/^{\r?\n([\s\S]*?)\r?\n}\r?\n([\s\S]*)$/);
|
|
356
|
+
if (jsonMatch) return {
|
|
357
|
+
frontMatter: `{\n${jsonMatch[1]}\n}`,
|
|
358
|
+
delimiter: "json",
|
|
359
|
+
content: jsonMatch[2]
|
|
360
|
+
};
|
|
361
|
+
return {
|
|
362
|
+
frontMatter: null,
|
|
363
|
+
delimiter: null,
|
|
364
|
+
content: text
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
var src_default = {
|
|
368
|
+
languages,
|
|
369
|
+
parsers,
|
|
370
|
+
printers,
|
|
371
|
+
options
|
|
372
|
+
};
|
|
373
|
+
//#endregion
|
|
374
|
+
export { src_default as default, parsers, printers };
|
package/package.json
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@htnabe/prettier-plugin-hugo-post",
|
|
3
|
+
"version": "0.0.1-rc.1",
|
|
4
|
+
"description": "A Prettier plugin for formatting Hugo content files with YAML, TOML, or JSON front matter, Markdown content, and Go template syntax.",
|
|
5
|
+
"main": "dist/index.mjs",
|
|
6
|
+
"types": "dist/index.d.mts",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"prettier",
|
|
10
|
+
"plugin",
|
|
11
|
+
"hugo",
|
|
12
|
+
"markdown",
|
|
13
|
+
"yaml",
|
|
14
|
+
"toml",
|
|
15
|
+
"json",
|
|
16
|
+
"go-template",
|
|
17
|
+
"front-matter",
|
|
18
|
+
"static-site-generator"
|
|
19
|
+
],
|
|
20
|
+
"author": "htnabe",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/htnabe/prettier-plugin-hugo-post.git"
|
|
25
|
+
},
|
|
26
|
+
"bugs": {
|
|
27
|
+
"url": "https://github.com/htnabe/prettier-plugin-hugo-post/issues"
|
|
28
|
+
},
|
|
29
|
+
"homepage": "https://github.com/htnabe/prettier-plugin-hugo-post#readme",
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=22.0.0",
|
|
32
|
+
"bun": ">=1.1.0"
|
|
33
|
+
},
|
|
34
|
+
"packageManager": "bun@1.2.0",
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"prettier": ">=3.0.0"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"prettier-plugin-toml": "^2.0.6"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/node": "^26.2.0",
|
|
46
|
+
"@typescript/native-preview": "^7.0.0-dev.20260707.2",
|
|
47
|
+
"@vitest/coverage-v8": "^4.1.11",
|
|
48
|
+
"jsdom": "^30.0.1",
|
|
49
|
+
"lefthook": "^2.1.10",
|
|
50
|
+
"oxlint": "^1.79.0",
|
|
51
|
+
"oxlint-tsgolint": "^7.0.2001",
|
|
52
|
+
"prettier": "^3.9.6",
|
|
53
|
+
"ts-node": "^10.9.2",
|
|
54
|
+
"tsdown": "^0.22.14",
|
|
55
|
+
"typescript": "^7.0.2",
|
|
56
|
+
"vitest": "^4.1.11"
|
|
57
|
+
},
|
|
58
|
+
"scripts": {
|
|
59
|
+
"format": "prettier --write .",
|
|
60
|
+
"format:check": "prettier --check .",
|
|
61
|
+
"lint": "oxlint",
|
|
62
|
+
"lint:fix": "oxlint --fix",
|
|
63
|
+
"test": "vitest run",
|
|
64
|
+
"test:coverage": "vitest run --coverage",
|
|
65
|
+
"build": "tsdown",
|
|
66
|
+
"example": "bun run build && bunx prettier --plugin ./dist/index.mjs examples/test.md"
|
|
67
|
+
},
|
|
68
|
+
"files": [
|
|
69
|
+
"dist/",
|
|
70
|
+
"README.md",
|
|
71
|
+
"LICENSE"
|
|
72
|
+
],
|
|
73
|
+
"prettier": {
|
|
74
|
+
"semi": true,
|
|
75
|
+
"singleQuote": true,
|
|
76
|
+
"tabWidth": 2,
|
|
77
|
+
"trailingComma": "es5",
|
|
78
|
+
"printWidth": 100,
|
|
79
|
+
"bracketSpacing": true,
|
|
80
|
+
"arrowParens": "avoid"
|
|
81
|
+
},
|
|
82
|
+
"allowScripts": {
|
|
83
|
+
"lefthook@2.1.10": true
|
|
84
|
+
}
|
|
85
|
+
}
|