@takazudo/mdx-formatter 0.1.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.
Files changed (44) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +72 -0
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +128 -0
  5. package/dist/html-block-formatter.d.ts +46 -0
  6. package/dist/html-block-formatter.js +370 -0
  7. package/dist/hybrid-formatter.d.ts +59 -0
  8. package/dist/hybrid-formatter.js +977 -0
  9. package/dist/indent-detector.d.ts +62 -0
  10. package/dist/indent-detector.js +358 -0
  11. package/dist/index.d.ts +28 -0
  12. package/dist/index.js +57 -0
  13. package/dist/load-config.d.ts +13 -0
  14. package/dist/load-config.js +71 -0
  15. package/dist/plugins/docusaurus-admonitions.d.ts +5 -0
  16. package/dist/plugins/docusaurus-admonitions.js +42 -0
  17. package/dist/plugins/fix-autolink-output.d.ts +4 -0
  18. package/dist/plugins/fix-autolink-output.js +24 -0
  19. package/dist/plugins/fix-formatting-issues.d.ts +4 -0
  20. package/dist/plugins/fix-formatting-issues.js +42 -0
  21. package/dist/plugins/fix-paragraph-spacing.d.ts +5 -0
  22. package/dist/plugins/fix-paragraph-spacing.js +96 -0
  23. package/dist/plugins/html-definition-list.d.ts +5 -0
  24. package/dist/plugins/html-definition-list.js +64 -0
  25. package/dist/plugins/japanese-text.d.ts +5 -0
  26. package/dist/plugins/japanese-text.js +79 -0
  27. package/dist/plugins/normalize-lists.d.ts +5 -0
  28. package/dist/plugins/normalize-lists.js +58 -0
  29. package/dist/plugins/preprocess-japanese.d.ts +7 -0
  30. package/dist/plugins/preprocess-japanese.js +15 -0
  31. package/dist/plugins/preserve-image-alt.d.ts +8 -0
  32. package/dist/plugins/preserve-image-alt.js +19 -0
  33. package/dist/plugins/preserve-jsx.d.ts +6 -0
  34. package/dist/plugins/preserve-jsx.js +48 -0
  35. package/dist/settings.d.ts +7 -0
  36. package/dist/settings.js +91 -0
  37. package/dist/specific-formatter.d.ts +30 -0
  38. package/dist/specific-formatter.js +469 -0
  39. package/dist/types.d.ts +226 -0
  40. package/dist/types.js +4 -0
  41. package/dist/utils.d.ts +12 -0
  42. package/dist/utils.js +47 -0
  43. package/format-stdin.js +36 -0
  44. package/package.json +107 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Takazudo
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,72 @@
1
+ # @takazudo/mdx-formatter
2
+
3
+ AST-based markdown and MDX formatter with Japanese text support. Built on top of the unified ecosystem with remark plugins.
4
+
5
+ ## Features
6
+
7
+ - **AST-based formatting** - Uses remark's AST for reliable formatting
8
+ - **MDX support** - Full support for MDX syntax including JSX components
9
+ - **Japanese text formatting** - Special handling for Japanese punctuation and URLs
10
+ - **Docusaurus support** - Preserves Docusaurus admonitions (:::note, :::tip, etc.)
11
+ - **HTML block formatting** - Proper indentation for HTML blocks (dl, table, ul, div, etc.)
12
+ - **GFM features** - Tables, strikethrough, task lists
13
+ - **Frontmatter preservation** - YAML frontmatter support
14
+ - **CLI and API** - Use as command-line tool or import as library
15
+ - **Configurable** - Customize component lists and rules via config file or API
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ npm install @takazudo/mdx-formatter
21
+ ```
22
+
23
+ Or use directly with npx:
24
+
25
+ ```bash
26
+ npx @takazudo/mdx-formatter --write "**/*.md"
27
+ ```
28
+
29
+ ## Usage
30
+
31
+ ### CLI
32
+
33
+ ```bash
34
+ # Check files (exit with error if formatting needed)
35
+ mdx-formatter --check "**/*.{md,mdx}"
36
+
37
+ # Format files in place
38
+ mdx-formatter --write "**/*.{md,mdx}"
39
+ ```
40
+
41
+ ### API
42
+
43
+ ```javascript
44
+ import { format } from '@takazudo/mdx-formatter';
45
+
46
+ const formatted = await format('# Hello\nWorld');
47
+ console.log(formatted); // '# Hello\n\nWorld'
48
+ ```
49
+
50
+ ## Documentation
51
+
52
+ For full documentation including configuration, options reference, formatting rules, and API reference, visit the [documentation site](https://takazudo.github.io/mdx-formatter/).
53
+
54
+ ## Development
55
+
56
+ ```bash
57
+ # Install dependencies
58
+ pnpm install
59
+
60
+ # Run tests
61
+ pnpm test
62
+
63
+ # Run tests in watch mode
64
+ pnpm test:watch
65
+
66
+ # Run tests with coverage
67
+ pnpm test:coverage
68
+ ```
69
+
70
+ ## License
71
+
72
+ MIT
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,128 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from 'fs';
3
+ import { program } from 'commander';
4
+ import { glob } from 'glob';
5
+ import chalk from 'chalk';
6
+ import { formatFile, checkFile } from './index.js';
7
+ const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf-8'));
8
+ program
9
+ .name('mdx-formatter')
10
+ .description('AST-based markdown and MDX formatter')
11
+ .version(pkg.version)
12
+ .argument('[patterns...]', 'Glob patterns for files to format', ['**/*.{md,mdx}'])
13
+ .option('-w, --write', 'Write formatted files in place')
14
+ .option('-c, --check', 'Check if files need formatting')
15
+ .option('--config <path>', 'Path to config file (.mdx-formatter.json)')
16
+ .option('--ignore <patterns>', 'Comma-separated patterns to ignore', 'node_modules/**,dist/**,build/**,.git/**,worktrees/**')
17
+ .action(async (patterns, options) => {
18
+ try {
19
+ await main(patterns, options);
20
+ }
21
+ catch (error) {
22
+ const message = error instanceof Error ? error.message : String(error);
23
+ console.error(chalk.red('Error:'), message);
24
+ process.exit(1);
25
+ }
26
+ });
27
+ program.parse();
28
+ /**
29
+ * Main CLI function
30
+ */
31
+ async function main(patterns, options) {
32
+ const ignorePatterns = options.ignore.split(',').map((p) => p.trim());
33
+ // Build format options from CLI flags
34
+ const formatOptions = {};
35
+ if (options.config) {
36
+ formatOptions.config = options.config;
37
+ }
38
+ // Find all matching files
39
+ const files = [];
40
+ for (const pattern of patterns) {
41
+ const matches = await glob(pattern, {
42
+ ignore: ignorePatterns,
43
+ nodir: true,
44
+ });
45
+ files.push(...matches);
46
+ }
47
+ // Remove duplicates
48
+ const uniqueFiles = [...new Set(files)];
49
+ if (uniqueFiles.length === 0) {
50
+ console.log(chalk.yellow('No files found matching the patterns.'));
51
+ return;
52
+ }
53
+ console.log(chalk.blue(`Processing ${uniqueFiles.length} file(s)...`));
54
+ let changedCount = 0;
55
+ let errorCount = 0;
56
+ for (const file of uniqueFiles) {
57
+ try {
58
+ if (options.write) {
59
+ const changed = await formatFile(file, formatOptions);
60
+ if (changed) {
61
+ changedCount++;
62
+ console.log(chalk.green('✓'), chalk.gray(file), chalk.green('formatted'));
63
+ }
64
+ else {
65
+ console.log(chalk.gray('○'), chalk.gray(file), chalk.gray('unchanged'));
66
+ }
67
+ }
68
+ else if (options.check) {
69
+ const needsFormatting = await checkFile(file, formatOptions);
70
+ if (needsFormatting) {
71
+ changedCount++;
72
+ console.log(chalk.yellow('⚠'), chalk.gray(file), chalk.yellow('needs formatting'));
73
+ }
74
+ else {
75
+ console.log(chalk.green('✓'), chalk.gray(file), chalk.green('formatted correctly'));
76
+ }
77
+ }
78
+ else {
79
+ // Default: just show what would be done
80
+ const needsFormatting = await checkFile(file, formatOptions);
81
+ if (needsFormatting) {
82
+ changedCount++;
83
+ console.log(chalk.blue('→'), chalk.gray(file), chalk.blue('would be formatted'));
84
+ }
85
+ else {
86
+ console.log(chalk.gray('○'), chalk.gray(file), chalk.gray('already formatted'));
87
+ }
88
+ }
89
+ }
90
+ catch (error) {
91
+ errorCount++;
92
+ const message = error instanceof Error ? error.message : String(error);
93
+ console.error(chalk.red('✗'), chalk.gray(file), chalk.red(message));
94
+ }
95
+ }
96
+ // Summary
97
+ console.log();
98
+ if (options.write) {
99
+ if (changedCount > 0) {
100
+ console.log(chalk.green(`✓ Formatted ${changedCount} file(s)`));
101
+ }
102
+ else {
103
+ console.log(chalk.gray('All files are already formatted'));
104
+ }
105
+ }
106
+ else if (options.check) {
107
+ if (changedCount > 0) {
108
+ console.log(chalk.yellow(`⚠ ${changedCount} file(s) need formatting`));
109
+ process.exit(1); // Exit with error code for CI
110
+ }
111
+ else {
112
+ console.log(chalk.green('✓ All files are formatted correctly'));
113
+ }
114
+ }
115
+ else {
116
+ if (changedCount > 0) {
117
+ console.log(chalk.blue(`→ ${changedCount} file(s) would be formatted`));
118
+ console.log(chalk.gray('Use --write to apply changes'));
119
+ }
120
+ else {
121
+ console.log(chalk.gray('All files are already formatted'));
122
+ }
123
+ }
124
+ if (errorCount > 0) {
125
+ console.log(chalk.red(`✗ ${errorCount} error(s) occurred`));
126
+ process.exit(1);
127
+ }
128
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * HTML Block Formatter
3
+ * Formats HTML blocks within MDX content using Prettier or built-in formatting
4
+ */
5
+ import type { FormatHtmlBlocksInMdxSetting } from './types.js';
6
+ export declare class HtmlBlockFormatter {
7
+ private settings;
8
+ private readonly htmlElements;
9
+ constructor(settings?: Partial<FormatHtmlBlocksInMdxSetting>);
10
+ /**
11
+ * Check if a tag name is an HTML element (not a JSX component)
12
+ */
13
+ isHtmlElement(tagName: string): boolean;
14
+ /**
15
+ * Format HTML content using Prettier
16
+ */
17
+ formatWithPrettier(html: string): Promise<string>;
18
+ /**
19
+ * Extract HTML block from position in original content
20
+ */
21
+ extractHtmlBlock(content: string, startPos: {
22
+ line: number;
23
+ column: number;
24
+ }, endPos: {
25
+ line: number;
26
+ column: number;
27
+ }): string;
28
+ /**
29
+ * Find matching closing tag for an opening tag
30
+ */
31
+ findMatchingClosingTag(content: string, startIndex: number, tagName: string): number;
32
+ /**
33
+ * Format MDX content with HTML block formatting
34
+ */
35
+ format(content: string): Promise<string>;
36
+ /**
37
+ * Replace HTML block in content with formatted version
38
+ */
39
+ replaceHtmlBlock(content: string, startPos: {
40
+ line: number;
41
+ column: number;
42
+ }, endPos: {
43
+ line: number;
44
+ column: number;
45
+ }, replacement: string): string;
46
+ }
@@ -0,0 +1,370 @@
1
+ /**
2
+ * HTML Block Formatter
3
+ * Formats HTML blocks within MDX content using Prettier or built-in formatting
4
+ */
5
+ import * as prettier from 'prettier';
6
+ export class HtmlBlockFormatter {
7
+ settings;
8
+ htmlElements;
9
+ constructor(settings = {}) {
10
+ this.settings = {
11
+ enabled: true,
12
+ description: '',
13
+ formatterConfig: {
14
+ parser: 'html',
15
+ tabWidth: 2,
16
+ useTabs: false,
17
+ },
18
+ ...settings,
19
+ };
20
+ // List of HTML elements (not JSX components which start with uppercase)
21
+ this.htmlElements = new Set([
22
+ // Structure
23
+ 'html',
24
+ 'head',
25
+ 'body',
26
+ 'div',
27
+ 'span',
28
+ 'section',
29
+ 'article',
30
+ 'aside',
31
+ 'header',
32
+ 'footer',
33
+ 'main',
34
+ 'nav',
35
+ 'figure',
36
+ 'figcaption',
37
+ // Text
38
+ 'p',
39
+ 'h1',
40
+ 'h2',
41
+ 'h3',
42
+ 'h4',
43
+ 'h5',
44
+ 'h6',
45
+ 'blockquote',
46
+ 'pre',
47
+ 'code',
48
+ 'em',
49
+ 'strong',
50
+ 'i',
51
+ 'b',
52
+ 'u',
53
+ 's',
54
+ 'mark',
55
+ 'small',
56
+ 'del',
57
+ 'ins',
58
+ 'sub',
59
+ 'sup',
60
+ 'cite',
61
+ 'q',
62
+ 'abbr',
63
+ 'address',
64
+ 'time',
65
+ // Lists
66
+ 'ul',
67
+ 'ol',
68
+ 'li',
69
+ 'dl',
70
+ 'dt',
71
+ 'dd',
72
+ // Tables
73
+ 'table',
74
+ 'thead',
75
+ 'tbody',
76
+ 'tfoot',
77
+ 'tr',
78
+ 'td',
79
+ 'th',
80
+ 'caption',
81
+ 'colgroup',
82
+ 'col',
83
+ // Forms
84
+ 'form',
85
+ 'input',
86
+ 'textarea',
87
+ 'button',
88
+ 'select',
89
+ 'option',
90
+ 'optgroup',
91
+ 'label',
92
+ 'fieldset',
93
+ 'legend',
94
+ 'datalist',
95
+ 'output',
96
+ 'progress',
97
+ 'meter',
98
+ // Media
99
+ 'img',
100
+ 'audio',
101
+ 'video',
102
+ 'source',
103
+ 'track',
104
+ 'picture',
105
+ 'iframe',
106
+ 'embed',
107
+ 'object',
108
+ 'param',
109
+ 'canvas',
110
+ 'svg',
111
+ // Other
112
+ 'a',
113
+ 'br',
114
+ 'hr',
115
+ 'details',
116
+ 'summary',
117
+ 'dialog',
118
+ 'menu',
119
+ 'menuitem',
120
+ 'script',
121
+ 'noscript',
122
+ 'template',
123
+ 'slot',
124
+ ]);
125
+ }
126
+ /**
127
+ * Check if a tag name is an HTML element (not a JSX component)
128
+ */
129
+ isHtmlElement(tagName) {
130
+ if (!tagName)
131
+ return false;
132
+ // HTML elements are lowercase or known HTML elements
133
+ return this.htmlElements.has(tagName.toLowerCase());
134
+ }
135
+ /**
136
+ * Format HTML content using Prettier
137
+ */
138
+ async formatWithPrettier(html) {
139
+ try {
140
+ // Preprocess: Remove newlines within dd and dt tags to keep them on single lines
141
+ // This is important for Japanese text readability in definition lists
142
+ const preprocessed = html
143
+ .replace(/<dd>([\s\S]*?)<\/dd>/g, (_match, content) => {
144
+ // Replace multiple whitespaces (including newlines) with single space
145
+ const cleaned = content.replace(/\s+/g, ' ').trim();
146
+ return `<dd>${cleaned}</dd>`;
147
+ })
148
+ .replace(/<dt>([\s\S]*?)<\/dt>/g, (_match, content) => {
149
+ // Same for dt tags
150
+ const cleaned = content.replace(/\s+/g, ' ').trim();
151
+ return `<dt>${cleaned}</dt>`;
152
+ });
153
+ const formatted = await prettier.format(preprocessed, {
154
+ parser: this.settings.formatterConfig.parser || 'html',
155
+ printWidth: 999999, // Never wrap lines
156
+ tabWidth: this.settings.formatterConfig.tabWidth || 2,
157
+ useTabs: this.settings.formatterConfig.useTabs || false,
158
+ htmlWhitespaceSensitivity: 'css', // Use CSS mode to handle whitespace better
159
+ bracketSameLine: true, // Keep closing bracket on same line to prevent broken tags
160
+ singleAttributePerLine: false,
161
+ });
162
+ // Remove trailing newline that prettier adds
163
+ let result = formatted.replace(/\n$/, '');
164
+ // Remove self-closing slashes from void elements if not present in original
165
+ // This maintains compatibility with existing MDX content
166
+ const voidElements = ['input', 'br', 'hr', 'img', 'meta', 'link'];
167
+ for (const elem of voidElements) {
168
+ const originalHasSelfClosing = new RegExp(`<${elem}[^>]*/>`, 'i').test(html);
169
+ if (!originalHasSelfClosing) {
170
+ // Remove the self-closing slash that Prettier adds
171
+ result = result.replace(new RegExp(`(<${elem}[^>]*?)\\s*/>`, 'gi'), '$1>');
172
+ }
173
+ }
174
+ // Special handling for dt/dd tags - trim content inside them
175
+ // This preserves the original formatting requirement for definition lists
176
+ result = result.replace(/<(dt|dd)>\s*(.*?)\s*<\/(dt|dd)>/g, '<$1>$2</$1>');
177
+ return result;
178
+ }
179
+ catch {
180
+ return html;
181
+ }
182
+ }
183
+ /**
184
+ * Extract HTML block from position in original content
185
+ */
186
+ extractHtmlBlock(content, startPos, endPos) {
187
+ const lines = content.split('\n');
188
+ const startLine = startPos.line - 1;
189
+ const endLine = endPos.line - 1;
190
+ const startCol = startPos.column - 1;
191
+ const endCol = endPos.column - 1;
192
+ if (startLine === endLine) {
193
+ // Single line
194
+ return lines[startLine].substring(startCol, endCol);
195
+ }
196
+ else {
197
+ // Multi-line
198
+ const extractedLines = [];
199
+ extractedLines.push(lines[startLine].substring(startCol));
200
+ for (let i = startLine + 1; i < endLine; i++) {
201
+ extractedLines.push(lines[i]);
202
+ }
203
+ extractedLines.push(lines[endLine].substring(0, endCol));
204
+ return extractedLines.join('\n');
205
+ }
206
+ }
207
+ /**
208
+ * Find matching closing tag for an opening tag
209
+ */
210
+ findMatchingClosingTag(content, startIndex, tagName) {
211
+ let depth = 1;
212
+ let index = startIndex;
213
+ const openPattern = new RegExp(`<${tagName}(?:\\s[^>]*)?>`, 'gi');
214
+ const closePattern = new RegExp(`<\\/${tagName}>`, 'gi');
215
+ while (depth > 0 && index < content.length) {
216
+ // Reset lastIndex for each search
217
+ openPattern.lastIndex = index;
218
+ closePattern.lastIndex = index;
219
+ const openMatch = openPattern.exec(content);
220
+ const closeMatch = closePattern.exec(content);
221
+ if (!closeMatch) {
222
+ // No closing tag found
223
+ return -1;
224
+ }
225
+ if (!openMatch || closeMatch.index < openMatch.index) {
226
+ // Found closing tag before next opening tag
227
+ depth--;
228
+ index = closeMatch.index + closeMatch[0].length;
229
+ }
230
+ else {
231
+ // Found opening tag before closing tag
232
+ depth++;
233
+ index = openMatch.index + openMatch[0].length;
234
+ }
235
+ }
236
+ return depth === 0 ? index : -1;
237
+ }
238
+ /**
239
+ * Format MDX content with HTML block formatting
240
+ */
241
+ async format(content) {
242
+ if (!this.settings.enabled) {
243
+ return content;
244
+ }
245
+ try {
246
+ // Find HTML blocks - handle nested tags properly
247
+ const blocks = [];
248
+ const processedRanges = [];
249
+ // First pass: find all opening tags
250
+ const openingTagPattern = /<(\w+)(?:\s[^>]*)?>(?!.*\/>)/g;
251
+ let match;
252
+ while ((match = openingTagPattern.exec(content)) !== null) {
253
+ const tagName = match[1];
254
+ if (this.isHtmlElement(tagName)) {
255
+ const startIndex = match.index;
256
+ const afterOpenTag = match.index + match[0].length;
257
+ // Find the matching closing tag
258
+ const endIndex = this.findMatchingClosingTag(content, afterOpenTag, tagName);
259
+ if (endIndex !== -1) {
260
+ // Check if this range overlaps with any processed range
261
+ let overlaps = false;
262
+ for (const range of processedRanges) {
263
+ if ((startIndex >= range[0] && startIndex < range[1]) ||
264
+ (endIndex > range[0] && endIndex <= range[1])) {
265
+ overlaps = true;
266
+ break;
267
+ }
268
+ }
269
+ if (!overlaps) {
270
+ blocks.push({
271
+ start: startIndex,
272
+ end: endIndex,
273
+ content: content.substring(startIndex, endIndex),
274
+ tagName: tagName,
275
+ });
276
+ processedRanges.push([startIndex, endIndex]);
277
+ }
278
+ }
279
+ }
280
+ }
281
+ // Also handle self-closing tags
282
+ const selfClosingPattern = /<(\w+)(?:\s[^>]*)?\/>/g;
283
+ while ((match = selfClosingPattern.exec(content)) !== null) {
284
+ const tagName = match[1];
285
+ if (this.isHtmlElement(tagName)) {
286
+ const startIndex = match.index;
287
+ const endIndex = match.index + match[0].length;
288
+ // Check if this is within an already processed block
289
+ let inBlock = false;
290
+ for (const range of processedRanges) {
291
+ if (startIndex >= range[0] && endIndex <= range[1]) {
292
+ inBlock = true;
293
+ break;
294
+ }
295
+ }
296
+ if (!inBlock) {
297
+ blocks.push({
298
+ start: startIndex,
299
+ end: endIndex,
300
+ content: match[0],
301
+ tagName: tagName,
302
+ });
303
+ }
304
+ }
305
+ }
306
+ // Sort blocks by position (reverse order to maintain positions)
307
+ blocks.sort((a, b) => b.start - a.start);
308
+ // Apply formatting to each HTML block
309
+ let result = content;
310
+ for (const block of blocks) {
311
+ // Always use Prettier for HTML formatting
312
+ const formatted = await this.formatWithPrettier(block.content);
313
+ // Replace the original HTML with formatted version
314
+ result = result.substring(0, block.start) + formatted + result.substring(block.end);
315
+ }
316
+ return result;
317
+ }
318
+ catch {
319
+ return content;
320
+ }
321
+ }
322
+ /**
323
+ * Replace HTML block in content with formatted version
324
+ */
325
+ replaceHtmlBlock(content, startPos, endPos, replacement) {
326
+ const lines = content.split('\n');
327
+ const startLine = startPos.line - 1;
328
+ const endLine = endPos.line - 1;
329
+ const startCol = startPos.column - 1;
330
+ const endCol = endPos.column - 1;
331
+ if (startLine === endLine) {
332
+ // Single line replacement
333
+ lines[startLine] =
334
+ lines[startLine].substring(0, startCol) + replacement + lines[startLine].substring(endCol);
335
+ }
336
+ else {
337
+ // Multi-line replacement
338
+ const replacementLines = replacement.split('\n');
339
+ // Create new line array
340
+ const newLines = [];
341
+ // Add lines before the block
342
+ for (let i = 0; i < startLine; i++) {
343
+ newLines.push(lines[i]);
344
+ }
345
+ // Add first line (partial) + first replacement line
346
+ if (replacementLines.length === 1) {
347
+ // Single line replacement for multi-line original
348
+ newLines.push(lines[startLine].substring(0, startCol) +
349
+ replacementLines[0] +
350
+ lines[endLine].substring(endCol));
351
+ }
352
+ else {
353
+ // Multi-line replacement
354
+ newLines.push(lines[startLine].substring(0, startCol) + replacementLines[0]);
355
+ // Add middle replacement lines
356
+ for (let i = 1; i < replacementLines.length - 1; i++) {
357
+ newLines.push(replacementLines[i]);
358
+ }
359
+ // Add last replacement line + rest of last original line
360
+ newLines.push(replacementLines[replacementLines.length - 1] + lines[endLine].substring(endCol));
361
+ }
362
+ // Add lines after the block
363
+ for (let i = endLine + 1; i < lines.length; i++) {
364
+ newLines.push(lines[i]);
365
+ }
366
+ return newLines.join('\n');
367
+ }
368
+ return lines.join('\n');
369
+ }
370
+ }