@org-press/fmt 0.9.12
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 +29 -0
- package/dist/index.js +361 -0
- package/package.json +50 -0
- package/src/command.ts +163 -0
- package/src/index.ts +33 -0
- package/src/plugin.ts +30 -0
- package/src/types.ts +70 -0
- package/src/utils.ts +358 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
GNU GENERAL PUBLIC LICENSE
|
|
2
|
+
Version 2, June 1991
|
|
3
|
+
|
|
4
|
+
Copyright (C) 1989, 1991 Free Software Foundation, Inc.
|
|
5
|
+
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
|
|
6
|
+
|
|
7
|
+
Everyone is permitted to copy and distribute verbatim copies
|
|
8
|
+
of this license document, but changing it is not allowed.
|
|
9
|
+
|
|
10
|
+
For the full license text, see: https://www.gnu.org/licenses/old-licenses/gpl-2.0.txt
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
Org-Press - Static site generator for org-mode files
|
|
15
|
+
Copyright (C) 2024-2026
|
|
16
|
+
|
|
17
|
+
This program is free software; you can redistribute it and/or modify
|
|
18
|
+
it under the terms of the GNU General Public License as published by
|
|
19
|
+
the Free Software Foundation; either version 2 of the License, or
|
|
20
|
+
(at your option) any later version.
|
|
21
|
+
|
|
22
|
+
This program is distributed in the hope that it will be useful,
|
|
23
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
24
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
25
|
+
GNU General Public License for more details.
|
|
26
|
+
|
|
27
|
+
You should have received a copy of the GNU General Public License
|
|
28
|
+
along with this program; if not, write to the Free Software
|
|
29
|
+
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
// src/plugin.ts
|
|
2
|
+
import { CreateCommand } from "org-press";
|
|
3
|
+
|
|
4
|
+
// src/command.ts
|
|
5
|
+
import * as prettier from "prettier";
|
|
6
|
+
|
|
7
|
+
// src/utils.ts
|
|
8
|
+
import * as fs from "fs";
|
|
9
|
+
import * as path from "path";
|
|
10
|
+
import { pathToFileURL } from "url";
|
|
11
|
+
import { parse } from "uniorg-parse/lib/parser.js";
|
|
12
|
+
function findOrgFiles(dir) {
|
|
13
|
+
const files = [];
|
|
14
|
+
if (!fs.existsSync(dir)) {
|
|
15
|
+
return files;
|
|
16
|
+
}
|
|
17
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
18
|
+
for (const entry of entries) {
|
|
19
|
+
const fullPath = path.join(dir, entry.name);
|
|
20
|
+
if (entry.isDirectory()) {
|
|
21
|
+
if (entry.name !== "node_modules" && !entry.name.startsWith(".")) {
|
|
22
|
+
files.push(...findOrgFiles(fullPath));
|
|
23
|
+
}
|
|
24
|
+
} else if (entry.name.endsWith(".org")) {
|
|
25
|
+
files.push(fullPath);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return files;
|
|
29
|
+
}
|
|
30
|
+
function extractBlocksFromFile(orgFilePath, projectRoot, options) {
|
|
31
|
+
const absolutePath = path.isAbsolute(orgFilePath) ? orgFilePath : path.join(projectRoot, orgFilePath);
|
|
32
|
+
const relativePath = path.relative(projectRoot, absolutePath);
|
|
33
|
+
const content = fs.readFileSync(absolutePath, "utf-8");
|
|
34
|
+
const lines = content.split("\n");
|
|
35
|
+
const ast = parse(content);
|
|
36
|
+
const blocks = [];
|
|
37
|
+
let blockIndex = 0;
|
|
38
|
+
const blockPositions = [];
|
|
39
|
+
for (let i = 0; i < lines.length; i++) {
|
|
40
|
+
const line = lines[i];
|
|
41
|
+
const beginMatch = line.match(/^\s*#\+begin_src\s+(\w+)(.*)$/i);
|
|
42
|
+
if (beginMatch) {
|
|
43
|
+
const language = beginMatch[1].toLowerCase();
|
|
44
|
+
let name;
|
|
45
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
46
|
+
const prevLine = lines[j].trim();
|
|
47
|
+
const nameMatch = prevLine.match(/^#\+name:\s*(.+)$/i);
|
|
48
|
+
if (nameMatch) {
|
|
49
|
+
name = nameMatch[1].trim();
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
if (prevLine && !prevLine.startsWith("#")) break;
|
|
53
|
+
}
|
|
54
|
+
let endLine = i;
|
|
55
|
+
for (let k = i + 1; k < lines.length; k++) {
|
|
56
|
+
if (lines[k].match(/^\s*#\+end_src\s*$/i)) {
|
|
57
|
+
endLine = k;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
blockPositions.push({
|
|
62
|
+
startLine: i + 1,
|
|
63
|
+
// 1-based
|
|
64
|
+
endLine: endLine + 1,
|
|
65
|
+
// 1-based
|
|
66
|
+
name,
|
|
67
|
+
language
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function walk(node) {
|
|
72
|
+
if (!node || typeof node !== "object") return;
|
|
73
|
+
const nodeObj = node;
|
|
74
|
+
if (nodeObj.type === "src-block") {
|
|
75
|
+
const position = blockPositions[blockIndex];
|
|
76
|
+
if (!position) {
|
|
77
|
+
blockIndex++;
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const language = nodeObj.language?.toLowerCase() || "";
|
|
81
|
+
if (options?.languages && options.languages.length > 0 && !options.languages.includes(language)) {
|
|
82
|
+
blockIndex++;
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
blocks.push({
|
|
86
|
+
orgFilePath: relativePath,
|
|
87
|
+
blockIndex,
|
|
88
|
+
blockName: position.name,
|
|
89
|
+
code: nodeObj.value || "",
|
|
90
|
+
language,
|
|
91
|
+
startLine: position.startLine,
|
|
92
|
+
endLine: position.endLine
|
|
93
|
+
});
|
|
94
|
+
blockIndex++;
|
|
95
|
+
}
|
|
96
|
+
if (nodeObj.children && Array.isArray(nodeObj.children)) {
|
|
97
|
+
for (const child of nodeObj.children) {
|
|
98
|
+
walk(child);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
walk(ast);
|
|
103
|
+
return blocks;
|
|
104
|
+
}
|
|
105
|
+
async function collectCodeBlocks(contentDir, projectRoot = process.cwd(), options) {
|
|
106
|
+
const absoluteContentDir = path.isAbsolute(contentDir) ? contentDir : path.join(projectRoot, contentDir);
|
|
107
|
+
let orgFiles = findOrgFiles(absoluteContentDir);
|
|
108
|
+
if (options?.files && options.files.length > 0) {
|
|
109
|
+
const patterns = options.files.map(
|
|
110
|
+
(f) => path.isAbsolute(f) ? f : path.join(projectRoot, f)
|
|
111
|
+
);
|
|
112
|
+
orgFiles = orgFiles.filter(
|
|
113
|
+
(file) => patterns.some((pattern) => {
|
|
114
|
+
if (pattern.endsWith(".org")) {
|
|
115
|
+
return file === pattern || file.endsWith(pattern);
|
|
116
|
+
}
|
|
117
|
+
return file.includes(pattern);
|
|
118
|
+
})
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
const allBlocks = [];
|
|
122
|
+
for (const orgFile of orgFiles) {
|
|
123
|
+
try {
|
|
124
|
+
const blocks = extractBlocksFromFile(orgFile, projectRoot, options);
|
|
125
|
+
allBlocks.push(...blocks);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.warn(
|
|
128
|
+
`[fmt] Warning: Failed to parse ${orgFile}: ${error instanceof Error ? error.message : String(error)}`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return allBlocks;
|
|
133
|
+
}
|
|
134
|
+
function writeBlockContentBatch(updates, projectRoot = process.cwd()) {
|
|
135
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
136
|
+
for (const update of updates) {
|
|
137
|
+
const filePath = update.block.orgFilePath;
|
|
138
|
+
if (!byFile.has(filePath)) {
|
|
139
|
+
byFile.set(filePath, []);
|
|
140
|
+
}
|
|
141
|
+
byFile.get(filePath).push(update);
|
|
142
|
+
}
|
|
143
|
+
for (const [filePath, fileUpdates] of byFile) {
|
|
144
|
+
fileUpdates.sort((a, b) => b.block.blockIndex - a.block.blockIndex);
|
|
145
|
+
const absolutePath = path.isAbsolute(filePath) ? filePath : path.join(projectRoot, filePath);
|
|
146
|
+
let fileContent = fs.readFileSync(absolutePath, "utf-8");
|
|
147
|
+
let lines = fileContent.split("\n");
|
|
148
|
+
for (const { block, newContent } of fileUpdates) {
|
|
149
|
+
const beginLineIndex = block.startLine - 1;
|
|
150
|
+
const endLineIndex = block.endLine - 1;
|
|
151
|
+
if (beginLineIndex < 0 || endLineIndex >= lines.length || beginLineIndex >= endLineIndex) {
|
|
152
|
+
console.warn(
|
|
153
|
+
`[fmt] Warning: Invalid block position in ${filePath}: lines ${block.startLine}-${block.endLine}`
|
|
154
|
+
);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const beginLine = lines[beginLineIndex];
|
|
158
|
+
const endLine = lines[endLineIndex];
|
|
159
|
+
if (!beginLine.match(/^\s*#\+begin_src\s+\w+/i)) {
|
|
160
|
+
console.warn(
|
|
161
|
+
`[fmt] Warning: Expected #+begin_src at line ${block.startLine} in ${filePath}`
|
|
162
|
+
);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (!endLine.match(/^\s*#\+end_src\s*$/i)) {
|
|
166
|
+
console.warn(
|
|
167
|
+
`[fmt] Warning: Expected #+end_src at line ${block.endLine} in ${filePath}`
|
|
168
|
+
);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const trimmedContent = newContent.replace(/\n$/, "");
|
|
172
|
+
const beforeBlock = lines.slice(0, beginLineIndex + 1);
|
|
173
|
+
const afterBlock = lines.slice(endLineIndex);
|
|
174
|
+
lines = [...beforeBlock, trimmedContent, ...afterBlock];
|
|
175
|
+
}
|
|
176
|
+
fs.writeFileSync(absolutePath, lines.join("\n"), "utf-8");
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
async function loadPrettierConfig(projectRoot) {
|
|
180
|
+
const configFiles = [
|
|
181
|
+
".prettierrc",
|
|
182
|
+
".prettierrc.json",
|
|
183
|
+
".prettierrc.js",
|
|
184
|
+
".prettierrc.cjs",
|
|
185
|
+
".prettierrc.mjs",
|
|
186
|
+
"prettier.config.js",
|
|
187
|
+
"prettier.config.cjs",
|
|
188
|
+
"prettier.config.mjs"
|
|
189
|
+
];
|
|
190
|
+
for (const configFile of configFiles) {
|
|
191
|
+
const configPath = path.join(projectRoot, configFile);
|
|
192
|
+
if (!fs.existsSync(configPath)) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
try {
|
|
196
|
+
if (configFile.endsWith(".json") || configFile === ".prettierrc") {
|
|
197
|
+
const content = fs.readFileSync(configPath, "utf-8");
|
|
198
|
+
return JSON.parse(content);
|
|
199
|
+
}
|
|
200
|
+
if (configFile.endsWith(".js") || configFile.endsWith(".cjs") || configFile.endsWith(".mjs")) {
|
|
201
|
+
const configUrl = pathToFileURL(configPath).href;
|
|
202
|
+
const module = await import(configUrl);
|
|
203
|
+
return module.default || module;
|
|
204
|
+
}
|
|
205
|
+
} catch (error) {
|
|
206
|
+
console.warn(
|
|
207
|
+
`[fmt] Warning: Failed to load ${configFile}: ${error instanceof Error ? error.message : String(error)}`
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return {};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// src/types.ts
|
|
215
|
+
var PRETTIER_PARSERS = {
|
|
216
|
+
typescript: "typescript",
|
|
217
|
+
ts: "typescript",
|
|
218
|
+
tsx: "typescript",
|
|
219
|
+
javascript: "babel",
|
|
220
|
+
js: "babel",
|
|
221
|
+
jsx: "babel",
|
|
222
|
+
json: "json",
|
|
223
|
+
css: "css",
|
|
224
|
+
scss: "scss",
|
|
225
|
+
less: "less",
|
|
226
|
+
html: "html",
|
|
227
|
+
yaml: "yaml",
|
|
228
|
+
yml: "yaml",
|
|
229
|
+
markdown: "markdown",
|
|
230
|
+
md: "markdown",
|
|
231
|
+
graphql: "graphql",
|
|
232
|
+
gql: "graphql"
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
// src/command.ts
|
|
236
|
+
function parseFmtArgs(args) {
|
|
237
|
+
const options = {
|
|
238
|
+
check: false,
|
|
239
|
+
write: true,
|
|
240
|
+
languages: [],
|
|
241
|
+
files: []
|
|
242
|
+
};
|
|
243
|
+
for (let i = 0; i < args.length; i++) {
|
|
244
|
+
const arg = args[i];
|
|
245
|
+
if (arg === "--check" || arg === "-c") {
|
|
246
|
+
options.check = true;
|
|
247
|
+
options.write = false;
|
|
248
|
+
} else if (arg === "--write" || arg === "-w") {
|
|
249
|
+
options.write = true;
|
|
250
|
+
} else if (arg === "--languages" || arg === "-l") {
|
|
251
|
+
const next = args[++i];
|
|
252
|
+
if (next) {
|
|
253
|
+
options.languages = next.split(",").map((l) => l.trim().toLowerCase());
|
|
254
|
+
}
|
|
255
|
+
} else if (!arg.startsWith("-")) {
|
|
256
|
+
options.files.push(arg);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return options;
|
|
260
|
+
}
|
|
261
|
+
function getParserForLanguage(language) {
|
|
262
|
+
return PRETTIER_PARSERS[language.toLowerCase()] || null;
|
|
263
|
+
}
|
|
264
|
+
async function runFmt(args, ctx) {
|
|
265
|
+
const options = parseFmtArgs(args);
|
|
266
|
+
const prettierConfig = await loadPrettierConfig(ctx.projectRoot);
|
|
267
|
+
const collectOptions = {
|
|
268
|
+
files: options.files?.length ? options.files : void 0,
|
|
269
|
+
languages: options.languages?.length ? options.languages : void 0
|
|
270
|
+
};
|
|
271
|
+
const blocks = await collectCodeBlocks(
|
|
272
|
+
ctx.contentDir,
|
|
273
|
+
ctx.projectRoot,
|
|
274
|
+
collectOptions
|
|
275
|
+
);
|
|
276
|
+
if (blocks.length === 0) {
|
|
277
|
+
console.log("[fmt] No code blocks found to format");
|
|
278
|
+
return 0;
|
|
279
|
+
}
|
|
280
|
+
let changedCount = 0;
|
|
281
|
+
let skippedCount = 0;
|
|
282
|
+
const updates = [];
|
|
283
|
+
for (const block of blocks) {
|
|
284
|
+
const parser = getParserForLanguage(block.language);
|
|
285
|
+
if (!parser) {
|
|
286
|
+
skippedCount++;
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
try {
|
|
290
|
+
const formatted2 = await prettier.format(block.code, {
|
|
291
|
+
parser,
|
|
292
|
+
...prettierConfig
|
|
293
|
+
});
|
|
294
|
+
const normalizedFormatted = formatted2.replace(/\n$/, "");
|
|
295
|
+
const normalizedOriginal = block.code.replace(/\n$/, "");
|
|
296
|
+
if (normalizedFormatted !== normalizedOriginal) {
|
|
297
|
+
changedCount++;
|
|
298
|
+
if (options.check) {
|
|
299
|
+
const location = block.blockName ? `${block.orgFilePath}:${block.startLine} (${block.blockName})` : `${block.orgFilePath}:${block.startLine}`;
|
|
300
|
+
console.log(`[fmt] Would format: ${location}`);
|
|
301
|
+
} else {
|
|
302
|
+
updates.push({ block, newContent: normalizedFormatted });
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
} catch (error) {
|
|
306
|
+
const location = block.blockName ? `${block.orgFilePath}:${block.startLine} (${block.blockName})` : `${block.orgFilePath}:${block.startLine}`;
|
|
307
|
+
console.warn(
|
|
308
|
+
`[fmt] Warning: Failed to format ${location}: ${error instanceof Error ? error.message : String(error)}`
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (!options.check && updates.length > 0) {
|
|
313
|
+
writeBlockContentBatch(updates, ctx.projectRoot);
|
|
314
|
+
}
|
|
315
|
+
const total = blocks.length;
|
|
316
|
+
const formatted = options.check ? 0 : changedCount;
|
|
317
|
+
const unchanged = total - changedCount - skippedCount;
|
|
318
|
+
if (options.check) {
|
|
319
|
+
if (changedCount > 0) {
|
|
320
|
+
console.log(
|
|
321
|
+
`[fmt] ${changedCount} block(s) need formatting (${unchanged} unchanged, ${skippedCount} skipped)`
|
|
322
|
+
);
|
|
323
|
+
return 1;
|
|
324
|
+
} else {
|
|
325
|
+
console.log(
|
|
326
|
+
`[fmt] All ${unchanged} block(s) are formatted (${skippedCount} skipped)`
|
|
327
|
+
);
|
|
328
|
+
return 0;
|
|
329
|
+
}
|
|
330
|
+
} else {
|
|
331
|
+
if (formatted > 0) {
|
|
332
|
+
console.log(
|
|
333
|
+
`[fmt] Formatted ${formatted} block(s) (${unchanged} unchanged, ${skippedCount} skipped)`
|
|
334
|
+
);
|
|
335
|
+
} else {
|
|
336
|
+
console.log(
|
|
337
|
+
`[fmt] All ${unchanged} block(s) are formatted (${skippedCount} skipped)`
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
return 0;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// src/plugin.ts
|
|
345
|
+
var fmtPlugin = CreateCommand("fmt", {
|
|
346
|
+
description: "Format code blocks in org files using Prettier",
|
|
347
|
+
args: [
|
|
348
|
+
{ name: "check", type: "boolean", description: "Check if files are formatted" },
|
|
349
|
+
{ name: "languages", type: "string", description: "Comma-separated list of languages" }
|
|
350
|
+
],
|
|
351
|
+
execute: (args, ctx) => runFmt(args._, ctx)
|
|
352
|
+
});
|
|
353
|
+
export {
|
|
354
|
+
PRETTIER_PARSERS,
|
|
355
|
+
collectCodeBlocks,
|
|
356
|
+
fmtPlugin,
|
|
357
|
+
loadPrettierConfig,
|
|
358
|
+
parseFmtArgs,
|
|
359
|
+
runFmt,
|
|
360
|
+
writeBlockContentBatch
|
|
361
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@org-press/fmt",
|
|
3
|
+
"version": "0.9.12",
|
|
4
|
+
"description": "Format code blocks in org files using Prettier",
|
|
5
|
+
"license": "GPL-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"main": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"src",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"peerDependencies": {
|
|
21
|
+
"org-press": ">=0.9.0"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"prettier": "^3.0.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"tsup": "^8.0.0",
|
|
28
|
+
"typescript": "~5.7.3",
|
|
29
|
+
"uniorg": "^1.3.0",
|
|
30
|
+
"uniorg-parse": "^3.2.0",
|
|
31
|
+
"@types/node": "^20.0.0",
|
|
32
|
+
"org-press": "0.9.13"
|
|
33
|
+
},
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://orgp.dev",
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "https://github.com/org-press/org-press.git"
|
|
41
|
+
},
|
|
42
|
+
"bugs": {
|
|
43
|
+
"url": "https://github.com/org-press/org-press/issues"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsup",
|
|
47
|
+
"dev": "tsup --watch",
|
|
48
|
+
"clean": "rm -rf dist"
|
|
49
|
+
}
|
|
50
|
+
}
|
package/src/command.ts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format Command Implementation
|
|
3
|
+
*
|
|
4
|
+
* Formats code blocks in org files using Prettier.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as prettier from "prettier";
|
|
8
|
+
import type { CommandContext } from "org-press";
|
|
9
|
+
import { collectCodeBlocks, writeBlockContentBatch, loadPrettierConfig } from "./utils.js";
|
|
10
|
+
import { PRETTIER_PARSERS, type FmtOptions } from "./types.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Parse command line arguments for fmt command
|
|
14
|
+
*/
|
|
15
|
+
export function parseFmtArgs(args: string[]): FmtOptions {
|
|
16
|
+
const options: FmtOptions = {
|
|
17
|
+
check: false,
|
|
18
|
+
write: true,
|
|
19
|
+
languages: [],
|
|
20
|
+
files: [],
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
for (let i = 0; i < args.length; i++) {
|
|
24
|
+
const arg = args[i];
|
|
25
|
+
|
|
26
|
+
if (arg === "--check" || arg === "-c") {
|
|
27
|
+
options.check = true;
|
|
28
|
+
options.write = false;
|
|
29
|
+
} else if (arg === "--write" || arg === "-w") {
|
|
30
|
+
options.write = true;
|
|
31
|
+
} else if (arg === "--languages" || arg === "-l") {
|
|
32
|
+
const next = args[++i];
|
|
33
|
+
if (next) {
|
|
34
|
+
options.languages = next.split(",").map((l) => l.trim().toLowerCase());
|
|
35
|
+
}
|
|
36
|
+
} else if (!arg.startsWith("-")) {
|
|
37
|
+
// Positional argument - file pattern
|
|
38
|
+
options.files!.push(arg);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return options;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get Prettier parser for a language
|
|
47
|
+
*/
|
|
48
|
+
function getParserForLanguage(language: string): string | null {
|
|
49
|
+
return PRETTIER_PARSERS[language.toLowerCase()] || null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Run the format command
|
|
54
|
+
*
|
|
55
|
+
* @param args - Command line arguments
|
|
56
|
+
* @param ctx - CLI context
|
|
57
|
+
* @returns Exit code (0 for success, 1 if check found changes)
|
|
58
|
+
*/
|
|
59
|
+
export async function runFmt(
|
|
60
|
+
args: string[],
|
|
61
|
+
ctx: CommandContext
|
|
62
|
+
): Promise<number> {
|
|
63
|
+
const options = parseFmtArgs(args);
|
|
64
|
+
const prettierConfig = await loadPrettierConfig(ctx.projectRoot);
|
|
65
|
+
|
|
66
|
+
// Collect blocks, filtering by language if formatting specific languages
|
|
67
|
+
const collectOptions = {
|
|
68
|
+
files: options.files?.length ? options.files : undefined,
|
|
69
|
+
languages: options.languages?.length ? options.languages : undefined,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const blocks = await collectCodeBlocks(
|
|
73
|
+
ctx.contentDir,
|
|
74
|
+
ctx.projectRoot,
|
|
75
|
+
collectOptions
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
if (blocks.length === 0) {
|
|
79
|
+
console.log("[fmt] No code blocks found to format");
|
|
80
|
+
return 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let changedCount = 0;
|
|
84
|
+
let skippedCount = 0;
|
|
85
|
+
const updates: Array<{ block: (typeof blocks)[0]; newContent: string }> = [];
|
|
86
|
+
|
|
87
|
+
for (const block of blocks) {
|
|
88
|
+
const parser = getParserForLanguage(block.language);
|
|
89
|
+
|
|
90
|
+
if (!parser) {
|
|
91
|
+
skippedCount++;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const formatted = await prettier.format(block.code, {
|
|
97
|
+
parser,
|
|
98
|
+
...prettierConfig,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Prettier adds trailing newline, but we store without it
|
|
102
|
+
const normalizedFormatted = formatted.replace(/\n$/, "");
|
|
103
|
+
const normalizedOriginal = block.code.replace(/\n$/, "");
|
|
104
|
+
|
|
105
|
+
if (normalizedFormatted !== normalizedOriginal) {
|
|
106
|
+
changedCount++;
|
|
107
|
+
|
|
108
|
+
if (options.check) {
|
|
109
|
+
const location = block.blockName
|
|
110
|
+
? `${block.orgFilePath}:${block.startLine} (${block.blockName})`
|
|
111
|
+
: `${block.orgFilePath}:${block.startLine}`;
|
|
112
|
+
console.log(`[fmt] Would format: ${location}`);
|
|
113
|
+
} else {
|
|
114
|
+
updates.push({ block, newContent: normalizedFormatted });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
} catch (error) {
|
|
118
|
+
const location = block.blockName
|
|
119
|
+
? `${block.orgFilePath}:${block.startLine} (${block.blockName})`
|
|
120
|
+
: `${block.orgFilePath}:${block.startLine}`;
|
|
121
|
+
console.warn(
|
|
122
|
+
`[fmt] Warning: Failed to format ${location}: ${
|
|
123
|
+
error instanceof Error ? error.message : String(error)
|
|
124
|
+
}`
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Write changes if not in check mode
|
|
130
|
+
if (!options.check && updates.length > 0) {
|
|
131
|
+
writeBlockContentBatch(updates, ctx.projectRoot);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Summary
|
|
135
|
+
const total = blocks.length;
|
|
136
|
+
const formatted = options.check ? 0 : changedCount;
|
|
137
|
+
const unchanged = total - changedCount - skippedCount;
|
|
138
|
+
|
|
139
|
+
if (options.check) {
|
|
140
|
+
if (changedCount > 0) {
|
|
141
|
+
console.log(
|
|
142
|
+
`[fmt] ${changedCount} block(s) need formatting (${unchanged} unchanged, ${skippedCount} skipped)`
|
|
143
|
+
);
|
|
144
|
+
return 1;
|
|
145
|
+
} else {
|
|
146
|
+
console.log(
|
|
147
|
+
`[fmt] All ${unchanged} block(s) are formatted (${skippedCount} skipped)`
|
|
148
|
+
);
|
|
149
|
+
return 0;
|
|
150
|
+
}
|
|
151
|
+
} else {
|
|
152
|
+
if (formatted > 0) {
|
|
153
|
+
console.log(
|
|
154
|
+
`[fmt] Formatted ${formatted} block(s) (${unchanged} unchanged, ${skippedCount} skipped)`
|
|
155
|
+
);
|
|
156
|
+
} else {
|
|
157
|
+
console.log(
|
|
158
|
+
`[fmt] All ${unchanged} block(s) are formatted (${skippedCount} skipped)`
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
return 0;
|
|
162
|
+
}
|
|
163
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @org-press/fmt
|
|
3
|
+
*
|
|
4
|
+
* Format code blocks in org files using Prettier.
|
|
5
|
+
*
|
|
6
|
+
* This package provides the `orgp fmt` CLI command plugin.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* // .org-press/config.ts
|
|
11
|
+
* import { fmtPlugin } from '@org-press/fmt';
|
|
12
|
+
*
|
|
13
|
+
* export default {
|
|
14
|
+
* contentDir: 'content',
|
|
15
|
+
* plugins: [fmtPlugin],
|
|
16
|
+
* };
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
// Plugin export
|
|
21
|
+
export { fmtPlugin } from "./plugin.js";
|
|
22
|
+
|
|
23
|
+
// Command export (for programmatic use)
|
|
24
|
+
export { runFmt, parseFmtArgs } from "./command.js";
|
|
25
|
+
|
|
26
|
+
// Type exports
|
|
27
|
+
export type { CollectedBlock, CollectOptions, FmtOptions } from "./types.js";
|
|
28
|
+
|
|
29
|
+
// Constant exports
|
|
30
|
+
export { PRETTIER_PARSERS } from "./types.js";
|
|
31
|
+
|
|
32
|
+
// Utility exports (for advanced use cases)
|
|
33
|
+
export { collectCodeBlocks, writeBlockContentBatch, loadPrettierConfig } from "./utils.js";
|
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format Plugin
|
|
3
|
+
*
|
|
4
|
+
* Provides the `orgp fmt` command for formatting code blocks with Prettier.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { CreateCommand } from "org-press";
|
|
8
|
+
import type { ParsedArgs, CommandContext } from "org-press";
|
|
9
|
+
import { runFmt } from "./command.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Format plugin
|
|
13
|
+
*
|
|
14
|
+
* Registers the `fmt` CLI command for formatting code blocks
|
|
15
|
+
* in org files using Prettier.
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* orgp fmt # Format all blocks
|
|
19
|
+
* orgp fmt --check # Check if blocks are formatted
|
|
20
|
+
* orgp fmt --languages ts,tsx # Format only TypeScript blocks
|
|
21
|
+
* orgp fmt content/api.org # Format specific file
|
|
22
|
+
*/
|
|
23
|
+
export const fmtPlugin = CreateCommand("fmt", {
|
|
24
|
+
description: "Format code blocks in org files using Prettier",
|
|
25
|
+
args: [
|
|
26
|
+
{ name: "check", type: "boolean", description: "Check if files are formatted" },
|
|
27
|
+
{ name: "languages", type: "string", description: "Comma-separated list of languages" },
|
|
28
|
+
],
|
|
29
|
+
execute: (args: ParsedArgs, ctx: CommandContext) => runFmt(args._, ctx),
|
|
30
|
+
});
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for @org-press/fmt
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A collected code block from an org file
|
|
7
|
+
*/
|
|
8
|
+
export interface CollectedBlock {
|
|
9
|
+
/** Relative path to the org file */
|
|
10
|
+
orgFilePath: string;
|
|
11
|
+
/** 0-based index of the block in the file */
|
|
12
|
+
blockIndex: number;
|
|
13
|
+
/** Block name from #+NAME: directive */
|
|
14
|
+
blockName?: string;
|
|
15
|
+
/** The source code content */
|
|
16
|
+
code: string;
|
|
17
|
+
/** Block language (e.g., "typescript", "javascript") */
|
|
18
|
+
language: string;
|
|
19
|
+
/** 1-based line number where the block starts (#+begin_src line) */
|
|
20
|
+
startLine: number;
|
|
21
|
+
/** 1-based line number where the block ends (#+end_src line) */
|
|
22
|
+
endLine: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Options for collecting code blocks
|
|
27
|
+
*/
|
|
28
|
+
export interface CollectOptions {
|
|
29
|
+
/** Filter by file path patterns */
|
|
30
|
+
files?: string[];
|
|
31
|
+
/** Filter by languages */
|
|
32
|
+
languages?: string[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Options for the format command
|
|
37
|
+
*/
|
|
38
|
+
export interface FmtOptions {
|
|
39
|
+
/** Check only, don't write changes */
|
|
40
|
+
check?: boolean;
|
|
41
|
+
/** Write changes (default: true when not checking) */
|
|
42
|
+
write?: boolean;
|
|
43
|
+
/** Filter by languages */
|
|
44
|
+
languages?: string[];
|
|
45
|
+
/** Filter by file patterns */
|
|
46
|
+
files?: string[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Language to Prettier parser mapping
|
|
51
|
+
*/
|
|
52
|
+
export const PRETTIER_PARSERS: Record<string, string> = {
|
|
53
|
+
typescript: "typescript",
|
|
54
|
+
ts: "typescript",
|
|
55
|
+
tsx: "typescript",
|
|
56
|
+
javascript: "babel",
|
|
57
|
+
js: "babel",
|
|
58
|
+
jsx: "babel",
|
|
59
|
+
json: "json",
|
|
60
|
+
css: "css",
|
|
61
|
+
scss: "scss",
|
|
62
|
+
less: "less",
|
|
63
|
+
html: "html",
|
|
64
|
+
yaml: "yaml",
|
|
65
|
+
yml: "yaml",
|
|
66
|
+
markdown: "markdown",
|
|
67
|
+
md: "markdown",
|
|
68
|
+
graphql: "graphql",
|
|
69
|
+
gql: "graphql",
|
|
70
|
+
};
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities for @org-press/fmt
|
|
3
|
+
*
|
|
4
|
+
* Block collection, writing, and config loading functions.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as fs from "node:fs";
|
|
8
|
+
import * as path from "node:path";
|
|
9
|
+
import { pathToFileURL } from "node:url";
|
|
10
|
+
import { parse } from "uniorg-parse/lib/parser.js";
|
|
11
|
+
import type { OrgData } from "uniorg";
|
|
12
|
+
import type { Options as PrettierOptions } from "prettier";
|
|
13
|
+
import type { CollectedBlock, CollectOptions } from "./types.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Find all org files recursively in a directory
|
|
17
|
+
*/
|
|
18
|
+
function findOrgFiles(dir: string): string[] {
|
|
19
|
+
const files: string[] = [];
|
|
20
|
+
|
|
21
|
+
if (!fs.existsSync(dir)) {
|
|
22
|
+
return files;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
26
|
+
|
|
27
|
+
for (const entry of entries) {
|
|
28
|
+
const fullPath = path.join(dir, entry.name);
|
|
29
|
+
|
|
30
|
+
if (entry.isDirectory()) {
|
|
31
|
+
// Skip node_modules and hidden directories
|
|
32
|
+
if (entry.name !== "node_modules" && !entry.name.startsWith(".")) {
|
|
33
|
+
files.push(...findOrgFiles(fullPath));
|
|
34
|
+
}
|
|
35
|
+
} else if (entry.name.endsWith(".org")) {
|
|
36
|
+
files.push(fullPath);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return files;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Extract code blocks from a single org file
|
|
45
|
+
*/
|
|
46
|
+
function extractBlocksFromFile(
|
|
47
|
+
orgFilePath: string,
|
|
48
|
+
projectRoot: string,
|
|
49
|
+
options?: CollectOptions
|
|
50
|
+
): CollectedBlock[] {
|
|
51
|
+
const absolutePath = path.isAbsolute(orgFilePath)
|
|
52
|
+
? orgFilePath
|
|
53
|
+
: path.join(projectRoot, orgFilePath);
|
|
54
|
+
const relativePath = path.relative(projectRoot, absolutePath);
|
|
55
|
+
const content = fs.readFileSync(absolutePath, "utf-8");
|
|
56
|
+
const lines = content.split("\n");
|
|
57
|
+
const ast = parse(content) as OrgData;
|
|
58
|
+
|
|
59
|
+
const blocks: CollectedBlock[] = [];
|
|
60
|
+
let blockIndex = 0;
|
|
61
|
+
|
|
62
|
+
// Find block positions and names by scanning raw content
|
|
63
|
+
const blockPositions: Array<{
|
|
64
|
+
startLine: number;
|
|
65
|
+
endLine: number;
|
|
66
|
+
name?: string;
|
|
67
|
+
language: string;
|
|
68
|
+
}> = [];
|
|
69
|
+
|
|
70
|
+
for (let i = 0; i < lines.length; i++) {
|
|
71
|
+
const line = lines[i];
|
|
72
|
+
const beginMatch = line.match(/^\s*#\+begin_src\s+(\w+)(.*)$/i);
|
|
73
|
+
|
|
74
|
+
if (beginMatch) {
|
|
75
|
+
const language = beginMatch[1].toLowerCase();
|
|
76
|
+
|
|
77
|
+
// Look backwards for #+NAME: directive
|
|
78
|
+
let name: string | undefined;
|
|
79
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
80
|
+
const prevLine = lines[j].trim();
|
|
81
|
+
const nameMatch = prevLine.match(/^#\+name:\s*(.+)$/i);
|
|
82
|
+
if (nameMatch) {
|
|
83
|
+
name = nameMatch[1].trim();
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
// Stop looking if we hit a non-comment, non-empty line
|
|
87
|
+
if (prevLine && !prevLine.startsWith("#")) break;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Find matching #+end_src
|
|
91
|
+
let endLine = i;
|
|
92
|
+
for (let k = i + 1; k < lines.length; k++) {
|
|
93
|
+
if (lines[k].match(/^\s*#\+end_src\s*$/i)) {
|
|
94
|
+
endLine = k;
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
blockPositions.push({
|
|
100
|
+
startLine: i + 1, // 1-based
|
|
101
|
+
endLine: endLine + 1, // 1-based
|
|
102
|
+
name,
|
|
103
|
+
language,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Walk AST and match blocks with their positions
|
|
109
|
+
function walk(node: unknown): void {
|
|
110
|
+
if (!node || typeof node !== "object") return;
|
|
111
|
+
|
|
112
|
+
const nodeObj = node as Record<string, unknown>;
|
|
113
|
+
|
|
114
|
+
if (nodeObj.type === "src-block") {
|
|
115
|
+
const position = blockPositions[blockIndex];
|
|
116
|
+
if (!position) {
|
|
117
|
+
blockIndex++;
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const language = (nodeObj.language as string)?.toLowerCase() || "";
|
|
122
|
+
|
|
123
|
+
// Filter by language if specified
|
|
124
|
+
if (
|
|
125
|
+
options?.languages &&
|
|
126
|
+
options.languages.length > 0 &&
|
|
127
|
+
!options.languages.includes(language)
|
|
128
|
+
) {
|
|
129
|
+
blockIndex++;
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
blocks.push({
|
|
134
|
+
orgFilePath: relativePath,
|
|
135
|
+
blockIndex,
|
|
136
|
+
blockName: position.name,
|
|
137
|
+
code: (nodeObj.value as string) || "",
|
|
138
|
+
language,
|
|
139
|
+
startLine: position.startLine,
|
|
140
|
+
endLine: position.endLine,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
blockIndex++;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Recurse into children
|
|
147
|
+
if (nodeObj.children && Array.isArray(nodeObj.children)) {
|
|
148
|
+
for (const child of nodeObj.children) {
|
|
149
|
+
walk(child);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
walk(ast);
|
|
155
|
+
return blocks;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Collect all code blocks from a content directory
|
|
160
|
+
*
|
|
161
|
+
* @param contentDir - Content directory to scan
|
|
162
|
+
* @param projectRoot - Project root directory
|
|
163
|
+
* @param options - Collection options
|
|
164
|
+
* @returns Array of collected code blocks
|
|
165
|
+
*/
|
|
166
|
+
export async function collectCodeBlocks(
|
|
167
|
+
contentDir: string,
|
|
168
|
+
projectRoot: string = process.cwd(),
|
|
169
|
+
options?: CollectOptions
|
|
170
|
+
): Promise<CollectedBlock[]> {
|
|
171
|
+
const absoluteContentDir = path.isAbsolute(contentDir)
|
|
172
|
+
? contentDir
|
|
173
|
+
: path.join(projectRoot, contentDir);
|
|
174
|
+
|
|
175
|
+
// Find all org files
|
|
176
|
+
let orgFiles = findOrgFiles(absoluteContentDir);
|
|
177
|
+
|
|
178
|
+
// Filter by file patterns if specified
|
|
179
|
+
if (options?.files && options.files.length > 0) {
|
|
180
|
+
const patterns = options.files.map((f) =>
|
|
181
|
+
path.isAbsolute(f) ? f : path.join(projectRoot, f)
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
orgFiles = orgFiles.filter((file) =>
|
|
185
|
+
patterns.some((pattern) => {
|
|
186
|
+
// Support both exact match and contains match
|
|
187
|
+
if (pattern.endsWith(".org")) {
|
|
188
|
+
return file === pattern || file.endsWith(pattern);
|
|
189
|
+
}
|
|
190
|
+
return file.includes(pattern);
|
|
191
|
+
})
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const allBlocks: CollectedBlock[] = [];
|
|
196
|
+
|
|
197
|
+
// Process each file
|
|
198
|
+
for (const orgFile of orgFiles) {
|
|
199
|
+
try {
|
|
200
|
+
const blocks = extractBlocksFromFile(orgFile, projectRoot, options);
|
|
201
|
+
allBlocks.push(...blocks);
|
|
202
|
+
} catch (error) {
|
|
203
|
+
console.warn(
|
|
204
|
+
`[fmt] Warning: Failed to parse ${orgFile}: ${
|
|
205
|
+
error instanceof Error ? error.message : String(error)
|
|
206
|
+
}`
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return allBlocks;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Batch write multiple block updates to minimize file I/O
|
|
216
|
+
*
|
|
217
|
+
* Updates are grouped by file and applied in reverse order
|
|
218
|
+
* (to preserve line numbers for earlier blocks in the same file)
|
|
219
|
+
*
|
|
220
|
+
* @param updates - Array of {block, newContent} pairs
|
|
221
|
+
* @param projectRoot - Project root directory
|
|
222
|
+
*/
|
|
223
|
+
export function writeBlockContentBatch(
|
|
224
|
+
updates: Array<{ block: CollectedBlock; newContent: string }>,
|
|
225
|
+
projectRoot: string = process.cwd()
|
|
226
|
+
): void {
|
|
227
|
+
// Group updates by file
|
|
228
|
+
const byFile = new Map<string, Array<{ block: CollectedBlock; newContent: string }>>();
|
|
229
|
+
|
|
230
|
+
for (const update of updates) {
|
|
231
|
+
const filePath = update.block.orgFilePath;
|
|
232
|
+
if (!byFile.has(filePath)) {
|
|
233
|
+
byFile.set(filePath, []);
|
|
234
|
+
}
|
|
235
|
+
byFile.get(filePath)!.push(update);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Process each file
|
|
239
|
+
for (const [filePath, fileUpdates] of byFile) {
|
|
240
|
+
// Sort by blockIndex in reverse order so we can apply changes
|
|
241
|
+
// from the end of the file backwards (preserving line numbers)
|
|
242
|
+
fileUpdates.sort((a, b) => b.block.blockIndex - a.block.blockIndex);
|
|
243
|
+
|
|
244
|
+
const absolutePath = path.isAbsolute(filePath)
|
|
245
|
+
? filePath
|
|
246
|
+
: path.join(projectRoot, filePath);
|
|
247
|
+
|
|
248
|
+
let fileContent = fs.readFileSync(absolutePath, "utf-8");
|
|
249
|
+
let lines = fileContent.split("\n");
|
|
250
|
+
|
|
251
|
+
for (const { block, newContent } of fileUpdates) {
|
|
252
|
+
const beginLineIndex = block.startLine - 1;
|
|
253
|
+
const endLineIndex = block.endLine - 1;
|
|
254
|
+
|
|
255
|
+
// Validate
|
|
256
|
+
if (
|
|
257
|
+
beginLineIndex < 0 ||
|
|
258
|
+
endLineIndex >= lines.length ||
|
|
259
|
+
beginLineIndex >= endLineIndex
|
|
260
|
+
) {
|
|
261
|
+
console.warn(
|
|
262
|
+
`[fmt] Warning: Invalid block position in ${filePath}: lines ${block.startLine}-${block.endLine}`
|
|
263
|
+
);
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const beginLine = lines[beginLineIndex];
|
|
268
|
+
const endLine = lines[endLineIndex];
|
|
269
|
+
|
|
270
|
+
if (!beginLine.match(/^\s*#\+begin_src\s+\w+/i)) {
|
|
271
|
+
console.warn(
|
|
272
|
+
`[fmt] Warning: Expected #+begin_src at line ${block.startLine} in ${filePath}`
|
|
273
|
+
);
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (!endLine.match(/^\s*#\+end_src\s*$/i)) {
|
|
278
|
+
console.warn(
|
|
279
|
+
`[fmt] Warning: Expected #+end_src at line ${block.endLine} in ${filePath}`
|
|
280
|
+
);
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Replace content
|
|
285
|
+
const trimmedContent = newContent.replace(/\n$/, "");
|
|
286
|
+
const beforeBlock = lines.slice(0, beginLineIndex + 1);
|
|
287
|
+
const afterBlock = lines.slice(endLineIndex);
|
|
288
|
+
lines = [...beforeBlock, trimmedContent, ...afterBlock];
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
fs.writeFileSync(absolutePath, lines.join("\n"), "utf-8");
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Load Prettier configuration from the project root
|
|
297
|
+
*
|
|
298
|
+
* Searches for:
|
|
299
|
+
* - .prettierrc
|
|
300
|
+
* - .prettierrc.json
|
|
301
|
+
* - .prettierrc.js
|
|
302
|
+
* - .prettierrc.cjs
|
|
303
|
+
* - .prettierrc.mjs
|
|
304
|
+
* - prettier.config.js
|
|
305
|
+
* - prettier.config.cjs
|
|
306
|
+
* - prettier.config.mjs
|
|
307
|
+
*
|
|
308
|
+
* @param projectRoot - Project root directory
|
|
309
|
+
* @returns Prettier options or empty object if not found
|
|
310
|
+
*/
|
|
311
|
+
export async function loadPrettierConfig(
|
|
312
|
+
projectRoot: string
|
|
313
|
+
): Promise<PrettierOptions> {
|
|
314
|
+
const configFiles = [
|
|
315
|
+
".prettierrc",
|
|
316
|
+
".prettierrc.json",
|
|
317
|
+
".prettierrc.js",
|
|
318
|
+
".prettierrc.cjs",
|
|
319
|
+
".prettierrc.mjs",
|
|
320
|
+
"prettier.config.js",
|
|
321
|
+
"prettier.config.cjs",
|
|
322
|
+
"prettier.config.mjs",
|
|
323
|
+
];
|
|
324
|
+
|
|
325
|
+
for (const configFile of configFiles) {
|
|
326
|
+
const configPath = path.join(projectRoot, configFile);
|
|
327
|
+
|
|
328
|
+
if (!fs.existsSync(configPath)) {
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
try {
|
|
333
|
+
if (configFile.endsWith(".json") || configFile === ".prettierrc") {
|
|
334
|
+
const content = fs.readFileSync(configPath, "utf-8");
|
|
335
|
+
return JSON.parse(content) as PrettierOptions;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (
|
|
339
|
+
configFile.endsWith(".js") ||
|
|
340
|
+
configFile.endsWith(".cjs") ||
|
|
341
|
+
configFile.endsWith(".mjs")
|
|
342
|
+
) {
|
|
343
|
+
const configUrl = pathToFileURL(configPath).href;
|
|
344
|
+
const module = await import(configUrl);
|
|
345
|
+
return (module.default || module) as PrettierOptions;
|
|
346
|
+
}
|
|
347
|
+
} catch (error) {
|
|
348
|
+
console.warn(
|
|
349
|
+
`[fmt] Warning: Failed to load ${configFile}: ${
|
|
350
|
+
error instanceof Error ? error.message : String(error)
|
|
351
|
+
}`
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Return default options if no config found
|
|
357
|
+
return {};
|
|
358
|
+
}
|