myaidev-method 0.3.2 → 0.3.4

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 (80) hide show
  1. package/.claude-plugin/plugin.json +52 -48
  2. package/DEV_WORKFLOW_GUIDE.md +6 -6
  3. package/MCP_INTEGRATION.md +4 -4
  4. package/README.md +81 -64
  5. package/TECHNICAL_ARCHITECTURE.md +112 -18
  6. package/USER_GUIDE.md +57 -40
  7. package/bin/cli.js +49 -127
  8. package/dist/mcp/gutenberg-converter.js +667 -413
  9. package/dist/mcp/wordpress-server.js +1558 -1181
  10. package/extension.json +3 -3
  11. package/package.json +2 -1
  12. package/skills/content-writer/SKILL.md +130 -178
  13. package/skills/infographic/SKILL.md +191 -0
  14. package/skills/myaidev-analyze/SKILL.md +242 -0
  15. package/skills/myaidev-analyze/agents/dependency-mapper-agent.md +236 -0
  16. package/skills/myaidev-analyze/agents/pattern-detector-agent.md +240 -0
  17. package/skills/myaidev-analyze/agents/structure-scanner-agent.md +171 -0
  18. package/skills/myaidev-analyze/agents/tech-profiler-agent.md +291 -0
  19. package/skills/myaidev-architect/SKILL.md +389 -0
  20. package/skills/myaidev-architect/agents/compliance-checker-agent.md +287 -0
  21. package/skills/myaidev-architect/agents/requirements-analyst-agent.md +194 -0
  22. package/skills/myaidev-architect/agents/system-designer-agent.md +315 -0
  23. package/skills/myaidev-coder/SKILL.md +291 -0
  24. package/skills/myaidev-coder/agents/implementer-agent.md +185 -0
  25. package/skills/myaidev-coder/agents/integration-agent.md +168 -0
  26. package/skills/myaidev-coder/agents/pattern-scanner-agent.md +161 -0
  27. package/skills/myaidev-coder/agents/self-reviewer-agent.md +168 -0
  28. package/skills/myaidev-debug/SKILL.md +308 -0
  29. package/skills/myaidev-debug/agents/fix-agent-debug.md +317 -0
  30. package/skills/myaidev-debug/agents/hypothesis-agent.md +226 -0
  31. package/skills/myaidev-debug/agents/investigator-agent.md +250 -0
  32. package/skills/myaidev-debug/agents/symptom-collector-agent.md +231 -0
  33. package/skills/myaidev-documenter/SKILL.md +194 -0
  34. package/skills/myaidev-documenter/agents/code-reader-agent.md +172 -0
  35. package/skills/myaidev-documenter/agents/doc-validator-agent.md +174 -0
  36. package/skills/myaidev-documenter/agents/doc-writer-agent.md +379 -0
  37. package/skills/myaidev-migrate/SKILL.md +300 -0
  38. package/skills/myaidev-migrate/agents/migration-planner-agent.md +237 -0
  39. package/skills/myaidev-migrate/agents/migration-writer-agent.md +248 -0
  40. package/skills/myaidev-migrate/agents/schema-analyzer-agent.md +190 -0
  41. package/skills/myaidev-performance/SKILL.md +270 -0
  42. package/skills/myaidev-performance/agents/benchmark-agent.md +281 -0
  43. package/skills/myaidev-performance/agents/optimizer-agent.md +277 -0
  44. package/skills/myaidev-performance/agents/profiler-agent.md +252 -0
  45. package/skills/myaidev-refactor/SKILL.md +296 -0
  46. package/skills/myaidev-refactor/agents/refactor-executor-agent.md +221 -0
  47. package/skills/myaidev-refactor/agents/refactor-planner-agent.md +213 -0
  48. package/skills/myaidev-refactor/agents/regression-guard-agent.md +242 -0
  49. package/skills/myaidev-refactor/agents/smell-detector-agent.md +233 -0
  50. package/skills/myaidev-reviewer/SKILL.md +385 -0
  51. package/skills/myaidev-reviewer/agents/auto-fixer-agent.md +238 -0
  52. package/skills/myaidev-reviewer/agents/code-analyst-agent.md +220 -0
  53. package/skills/myaidev-reviewer/agents/security-scanner-agent.md +262 -0
  54. package/skills/myaidev-tester/SKILL.md +331 -0
  55. package/skills/myaidev-tester/agents/coverage-analyst-agent.md +163 -0
  56. package/skills/myaidev-tester/agents/tdd-driver-agent.md +242 -0
  57. package/skills/myaidev-tester/agents/test-runner-agent.md +176 -0
  58. package/skills/myaidev-tester/agents/test-strategist-agent.md +154 -0
  59. package/skills/myaidev-tester/agents/test-writer-agent.md +242 -0
  60. package/skills/myaidev-workflow/SKILL.md +567 -0
  61. package/skills/myaidev-workflow/agents/analyzer-agent.md +317 -0
  62. package/skills/myaidev-workflow/agents/coordinator-agent.md +253 -0
  63. package/skills/security-auditor/SKILL.md +1 -1
  64. package/skills/skill-builder/SKILL.md +417 -0
  65. package/src/cli/commands/addon.js +146 -135
  66. package/src/cli/commands/auth.js +9 -1
  67. package/src/config/workflows.js +11 -6
  68. package/src/lib/ascii-banner.js +3 -3
  69. package/src/lib/update-manager.js +120 -61
  70. package/src/mcp/gutenberg-converter.js +667 -413
  71. package/src/mcp/wordpress-server.js +1558 -1181
  72. package/src/statusline/statusline.sh +279 -0
  73. package/src/templates/claude/CLAUDE.md +124 -0
  74. package/skills/sparc-architect/SKILL.md +0 -127
  75. package/skills/sparc-coder/SKILL.md +0 -90
  76. package/skills/sparc-documenter/SKILL.md +0 -155
  77. package/skills/sparc-reviewer/SKILL.md +0 -138
  78. package/skills/sparc-tester/SKILL.md +0 -100
  79. package/skills/sparc-workflow/SKILL.md +0 -130
  80. /package/{marketplace.json → .claude-plugin/marketplace.json} +0 -0
@@ -5,443 +5,697 @@
5
5
  */
6
6
 
7
7
  export class GutenbergConverter {
8
- /**
9
- * Convert HTML content to Gutenberg blocks
10
- * @param {string} html - HTML content to convert
11
- * @returns {string} - Gutenberg block formatted content
12
- */
13
- static htmlToGutenberg(html) {
14
- // Parse HTML and convert to blocks
15
- let gutenbergContent = '';
16
-
17
- // Split content into sections for processing
18
- const sections = this.parseHTMLSections(html);
19
-
20
- sections.forEach(section => {
21
- const block = this.createBlock(section);
22
- if (block) {
23
- gutenbergContent += block + '\n\n';
24
- }
25
- });
26
-
27
- return gutenbergContent.trim();
28
- }
29
-
30
- /**
31
- * Parse HTML into sections for block conversion
32
- */
33
- static parseHTMLSections(html) {
34
- const sections = [];
35
-
36
- // Remove excess whitespace and normalize
37
- const normalizedHtml = html.replace(/\n\s*\n/g, '\n').trim();
38
-
39
- // Regular expressions for different HTML elements
40
- const patterns = {
41
- heading: /<h([1-6])(?:\s[^>]*)?>(.+?)<\/h\1>/gi,
42
- paragraph: /<p(?:\s[^>]*)?>(.+?)<\/p>/gi,
43
- list: /<(ul|ol)(?:\s[^>]*)?>(.+?)<\/\1>/gis,
44
- blockquote: /<blockquote(?:\s[^>]*)?>(.+?)<\/blockquote>/gis,
45
- pre: /<pre(?:\s[^>]*)?><code(?:\s[^>]*)?>(.+?)<\/code><\/pre>/gis,
46
- image: /<img\s+([^>]+)>/gi,
47
- hr: /<hr(?:\s[^>]*)?>/gi
48
- };
49
-
50
- // Process the HTML string sequentially
51
- let lastIndex = 0;
52
- const processedParts = [];
53
-
54
- // Create a combined pattern to find all blocks
55
- const combinedPattern = new RegExp(
56
- '(' +
57
- '<h[1-6](?:\\s[^>]*)?>.*?</h[1-6]>|' +
58
- '<p(?:\\s[^>]*)?>.*?</p>|' +
59
- '<(?:ul|ol)(?:\\s[^>]*)?>.*?</(?:ul|ol)>|' +
60
- '<blockquote(?:\\s[^>]*)?>.*?</blockquote>|' +
61
- '<pre(?:\\s[^>]*)?><code(?:\\s[^>]*)?>.*?</code></pre>|' +
62
- '<img\\s+[^>]+>|' +
63
- '<hr(?:\\s[^>]*)?>' +
64
- ')',
65
- 'gis'
66
- );
67
-
68
- let match;
69
- while ((match = combinedPattern.exec(normalizedHtml)) !== null) {
70
- // Add any text between matches as a paragraph
71
- if (match.index > lastIndex) {
72
- const text = normalizedHtml.substring(lastIndex, match.index).trim();
73
- if (text && !text.match(/^\s*$/)) {
74
- sections.push({ type: 'paragraph', content: this.stripTags(text) });
75
- }
76
- }
77
-
78
- const fullMatch = match[0];
79
-
80
- // Determine block type and extract content
81
- if (fullMatch.match(/<h([1-6])/i)) {
82
- const level = fullMatch.match(/<h([1-6])/i)[1];
83
- const content = fullMatch.replace(/<\/?h[1-6](?:\s[^>]*)?>/gi, '');
84
- sections.push({ type: 'heading', level: parseInt(level), content: this.stripTags(content) });
85
- }
86
- else if (fullMatch.match(/<p(?:\s|>)/i)) {
87
- const content = fullMatch.replace(/<\/?p(?:\s[^>]*)?>/gi, '');
88
- sections.push({ type: 'paragraph', content: this.stripTags(content) });
89
- }
90
- else if (fullMatch.match(/<(ul|ol)/i)) {
91
- const listType = fullMatch.match(/<(ul|ol)/i)[1];
92
- const items = this.parseListItems(fullMatch);
93
- sections.push({ type: 'list', ordered: listType === 'ol', items });
94
- }
95
- else if (fullMatch.match(/<blockquote/i)) {
96
- const content = fullMatch.replace(/<\/?blockquote(?:\s[^>]*)?>/gi, '');
97
- sections.push({ type: 'quote', content: this.stripTags(content) });
98
- }
99
- else if (fullMatch.match(/<pre/i)) {
100
- const content = fullMatch.replace(/<\/?(?:pre|code)(?:\s[^>]*)?>/gi, '');
101
- sections.push({ type: 'code', content: this.decodeHtml(content) });
102
- }
103
- else if (fullMatch.match(/<img/i)) {
104
- const attrs = this.parseImageAttributes(fullMatch);
105
- sections.push({ type: 'image', ...attrs });
106
- }
107
- else if (fullMatch.match(/<hr/i)) {
108
- sections.push({ type: 'separator' });
109
- }
110
-
111
- lastIndex = match.index + fullMatch.length;
112
- }
113
-
114
- // Add any remaining content
115
- if (lastIndex < normalizedHtml.length) {
116
- const text = normalizedHtml.substring(lastIndex).trim();
117
- if (text && !text.match(/^\s*$/)) {
118
- sections.push({ type: 'paragraph', content: this.stripTags(text) });
119
- }
120
- }
121
-
122
- return sections;
123
- }
124
-
125
- /**
126
- * Parse list items from HTML list
127
- */
128
- static parseListItems(listHtml) {
129
- const items = [];
130
- const itemPattern = /<li(?:\s[^>]*)?>(.+?)<\/li>/gis;
131
- let match;
132
-
133
- while ((match = itemPattern.exec(listHtml)) !== null) {
134
- items.push(this.stripTags(match[1].trim()));
135
- }
136
-
137
- return items;
138
- }
139
-
140
- /**
141
- * Parse image attributes from img tag
142
- */
143
- static parseImageAttributes(imgTag) {
144
- const attrs = {};
145
-
146
- // Extract src
147
- const srcMatch = imgTag.match(/src=["']([^"']+)["']/i);
148
- if (srcMatch) attrs.url = srcMatch[1];
149
-
150
- // Extract alt text
151
- const altMatch = imgTag.match(/alt=["']([^"']+)["']/i);
152
- if (altMatch) attrs.alt = altMatch[1];
153
-
154
- // Extract title
155
- const titleMatch = imgTag.match(/title=["']([^"']+)["']/i);
156
- if (titleMatch) attrs.caption = titleMatch[1];
157
-
158
- return attrs;
159
- }
160
-
161
- /**
162
- * Create a Gutenberg block from a section
163
- */
164
- static createBlock(section) {
165
- switch (section.type) {
166
- case 'heading':
167
- return this.createHeadingBlock(section.level, section.content);
168
-
169
- case 'paragraph':
170
- return this.createParagraphBlock(section.content);
171
-
172
- case 'list':
173
- return this.createListBlock(section.items, section.ordered);
174
-
175
- case 'quote':
176
- return this.createQuoteBlock(section.content);
177
-
178
- case 'code':
179
- return this.createCodeBlock(section.content);
180
-
181
- case 'image':
182
- return this.createImageBlock(section.url, section.alt, section.caption);
183
-
184
- case 'separator':
185
- return this.createSeparatorBlock();
186
-
187
- default:
188
- return this.createParagraphBlock(section.content || '');
189
- }
190
- }
191
-
192
- /**
193
- * Create heading block
194
- */
195
- static createHeadingBlock(level, content) {
196
- return `<!-- wp:heading {"level":${level}} -->
8
+ /**
9
+ * Convert HTML content to Gutenberg blocks
10
+ * @param {string} html - HTML content to convert
11
+ * @returns {string} - Gutenberg block formatted content
12
+ */
13
+ static htmlToGutenberg(html) {
14
+ // Parse HTML and convert to blocks
15
+ let gutenbergContent = "";
16
+
17
+ // Split content into sections for processing
18
+ const sections = this.parseHTMLSections(html);
19
+
20
+ sections.forEach((section) => {
21
+ const block = this.createBlock(section);
22
+ if (block) {
23
+ gutenbergContent += block + "\n\n";
24
+ }
25
+ });
26
+
27
+ return gutenbergContent.trim();
28
+ }
29
+
30
+ /**
31
+ * Parse HTML into sections for block conversion
32
+ */
33
+ static parseHTMLSections(html) {
34
+ const sections = [];
35
+
36
+ // Remove excess whitespace and normalize
37
+ const normalizedHtml = html.replace(/\n\s*\n/g, "\n").trim();
38
+
39
+ // Regular expressions for different HTML elements
40
+ const patterns = {
41
+ heading: /<h([1-6])(?:\s[^>]*)?>(.+?)<\/h\1>/gi,
42
+ paragraph: /<p(?:\s[^>]*)?>(.+?)<\/p>/gi,
43
+ list: /<(ul|ol)(?:\s[^>]*)?>(.+?)<\/\1>/gis,
44
+ blockquote: /<blockquote(?:\s[^>]*)?>(.+?)<\/blockquote>/gis,
45
+ pre: /<pre(?:\s[^>]*)?><code(?:\s[^>]*)?>(.+?)<\/code><\/pre>/gis,
46
+ image: /<img\s+([^>]+)>/gi,
47
+ hr: /<hr(?:\s[^>]*)?>/gi,
48
+ };
49
+
50
+ // Process the HTML string sequentially
51
+ let lastIndex = 0;
52
+ const processedParts = [];
53
+
54
+ // Create a combined pattern to find all blocks
55
+ const combinedPattern = new RegExp(
56
+ "(" +
57
+ "<h[1-6](?:\\s[^>]*)?>.*?</h[1-6]>|" +
58
+ "<p(?:\\s[^>]*)?>.*?</p>|" +
59
+ "<(?:ul|ol)(?:\\s[^>]*)?>.*?</(?:ul|ol)>|" +
60
+ "<blockquote(?:\\s[^>]*)?>.*?</blockquote>|" +
61
+ "<pre(?:\\s[^>]*)?><code(?:\\s[^>]*)?>.*?</code></pre>|" +
62
+ "<img\\s+[^>]+>|" +
63
+ "<hr(?:\\s[^>]*)?>" +
64
+ ")",
65
+ "gis",
66
+ );
67
+
68
+ let match;
69
+ while ((match = combinedPattern.exec(normalizedHtml)) !== null) {
70
+ // Add any text between matches as a paragraph
71
+ if (match.index > lastIndex) {
72
+ const text = normalizedHtml.substring(lastIndex, match.index).trim();
73
+ if (text && !text.match(/^\s*$/)) {
74
+ sections.push({ type: "paragraph", content: this.stripTags(text) });
75
+ }
76
+ }
77
+
78
+ const fullMatch = match[0];
79
+
80
+ // Determine block type and extract content
81
+ if (fullMatch.match(/<h([1-6])/i)) {
82
+ const level = fullMatch.match(/<h([1-6])/i)[1];
83
+ const content = fullMatch.replace(/<\/?h[1-6](?:\s[^>]*)?>/gi, "");
84
+ sections.push({
85
+ type: "heading",
86
+ level: parseInt(level),
87
+ content: this.stripTags(content),
88
+ });
89
+ } else if (fullMatch.match(/<p(?:\s|>)/i)) {
90
+ const content = fullMatch.replace(/<\/?p(?:\s[^>]*)?>/gi, "");
91
+ sections.push({ type: "paragraph", content: this.stripTags(content) });
92
+ } else if (fullMatch.match(/<(ul|ol)/i)) {
93
+ const listType = fullMatch.match(/<(ul|ol)/i)[1];
94
+ const items = this.parseListItems(fullMatch);
95
+ sections.push({ type: "list", ordered: listType === "ol", items });
96
+ } else if (fullMatch.match(/<blockquote/i)) {
97
+ const content = fullMatch.replace(/<\/?blockquote(?:\s[^>]*)?>/gi, "");
98
+ sections.push({ type: "quote", content: this.stripTags(content) });
99
+ } else if (fullMatch.match(/<pre/i)) {
100
+ const content = fullMatch.replace(
101
+ /<\/?(?:pre|code)(?:\s[^>]*)?>/gi,
102
+ "",
103
+ );
104
+ sections.push({ type: "code", content: this.decodeHtml(content) });
105
+ } else if (fullMatch.match(/<img/i)) {
106
+ const attrs = this.parseImageAttributes(fullMatch);
107
+ sections.push({ type: "image", ...attrs });
108
+ } else if (fullMatch.match(/<hr/i)) {
109
+ sections.push({ type: "separator" });
110
+ }
111
+
112
+ lastIndex = match.index + fullMatch.length;
113
+ }
114
+
115
+ // Add any remaining content
116
+ if (lastIndex < normalizedHtml.length) {
117
+ const text = normalizedHtml.substring(lastIndex).trim();
118
+ if (text && !text.match(/^\s*$/)) {
119
+ sections.push({ type: "paragraph", content: this.stripTags(text) });
120
+ }
121
+ }
122
+
123
+ return sections;
124
+ }
125
+
126
+ /**
127
+ * Parse list items from HTML list
128
+ */
129
+ static parseListItems(listHtml) {
130
+ const items = [];
131
+ const itemPattern = /<li(?:\s[^>]*)?>(.+?)<\/li>/gis;
132
+ let match;
133
+
134
+ while ((match = itemPattern.exec(listHtml)) !== null) {
135
+ items.push(this.stripTags(match[1].trim()));
136
+ }
137
+
138
+ return items;
139
+ }
140
+
141
+ /**
142
+ * Parse image attributes from img tag
143
+ */
144
+ static parseImageAttributes(imgTag) {
145
+ const attrs = {};
146
+
147
+ // Extract src
148
+ const srcMatch = imgTag.match(/src=["']([^"']+)["']/i);
149
+ if (srcMatch) attrs.url = srcMatch[1];
150
+
151
+ // Extract alt text
152
+ const altMatch = imgTag.match(/alt=["']([^"']+)["']/i);
153
+ if (altMatch) attrs.alt = altMatch[1];
154
+
155
+ // Extract title
156
+ const titleMatch = imgTag.match(/title=["']([^"']+)["']/i);
157
+ if (titleMatch) attrs.caption = titleMatch[1];
158
+
159
+ return attrs;
160
+ }
161
+
162
+ /**
163
+ * Create a Gutenberg block from a section
164
+ */
165
+ static createBlock(section) {
166
+ switch (section.type) {
167
+ case "heading":
168
+ return this.createHeadingBlock(section.level, section.content);
169
+
170
+ case "paragraph":
171
+ return this.createParagraphBlock(section.content);
172
+
173
+ case "list":
174
+ return this.createListBlock(section.items, section.ordered);
175
+
176
+ case "quote":
177
+ return this.createQuoteBlock(section.content);
178
+
179
+ case "code":
180
+ return this.createCodeBlock(section.content);
181
+
182
+ case "image":
183
+ return this.createImageBlock(section.url, section.alt, section.caption);
184
+
185
+ case "separator":
186
+ return this.createSeparatorBlock();
187
+
188
+ default:
189
+ return this.createParagraphBlock(section.content || "");
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Create heading block
195
+ */
196
+ static createHeadingBlock(level, content) {
197
+ return `<!-- wp:heading {"level":${level}} -->
197
198
  <h${level} class="wp-block-heading">${this.escapeHtml(content)}</h${level}>
198
199
  <!-- /wp:heading -->`;
199
- }
200
-
201
- /**
202
- * Create paragraph block
203
- */
204
- static createParagraphBlock(content) {
205
- // Handle empty paragraphs
206
- if (!content || content.trim() === '') {
207
- return '';
208
- }
209
-
210
- return `<!-- wp:paragraph -->
200
+ }
201
+
202
+ /**
203
+ * Create paragraph block
204
+ */
205
+ static createParagraphBlock(content) {
206
+ // Handle empty paragraphs
207
+ if (!content || content.trim() === "") {
208
+ return "";
209
+ }
210
+
211
+ return `<!-- wp:paragraph -->
211
212
  <p>${this.escapeHtml(content)}</p>
212
213
  <!-- /wp:paragraph -->`;
213
- }
214
-
215
- /**
216
- * Create list block
217
- */
218
- static createListBlock(items, ordered = false) {
219
- const tag = ordered ? 'ol' : 'ul';
220
- const blockName = ordered ? 'list' : 'list';
221
- const listItems = items.map(item => `<li>${this.escapeHtml(item)}</li>`).join('\n');
222
-
223
- const attributes = ordered ? ' {"ordered":true}' : '';
224
-
225
- return `<!-- wp:list${attributes} -->
214
+ }
215
+
216
+ /**
217
+ * Create list block
218
+ */
219
+ static createListBlock(items, ordered = false) {
220
+ const tag = ordered ? "ol" : "ul";
221
+ const blockName = ordered ? "list" : "list";
222
+ const listItems = items
223
+ .map((item) => `<li>${this.escapeHtml(item)}</li>`)
224
+ .join("\n");
225
+
226
+ const attributes = ordered ? ' {"ordered":true}' : "";
227
+
228
+ return `<!-- wp:list${attributes} -->
226
229
  <${tag} class="wp-block-list">${listItems}</${tag}>
227
230
  <!-- /wp:list -->`;
228
- }
231
+ }
229
232
 
230
- /**
231
- * Create quote block
232
- */
233
- static createQuoteBlock(content) {
234
- return `<!-- wp:quote -->
233
+ /**
234
+ * Create quote block
235
+ */
236
+ static createQuoteBlock(content) {
237
+ return `<!-- wp:quote -->
235
238
  <blockquote class="wp-block-quote">
236
239
  <p>${this.escapeHtml(content)}</p>
237
240
  </blockquote>
238
241
  <!-- /wp:quote -->`;
239
- }
240
-
241
- /**
242
- * Create code block
243
- */
244
- static createCodeBlock(code) {
245
- // Escape the code content for HTML
246
- const escapedCode = this.escapeHtml(code);
247
-
248
- return `<!-- wp:code -->
242
+ }
243
+
244
+ /**
245
+ * Create code block
246
+ */
247
+ static createCodeBlock(code) {
248
+ // Escape the code content for HTML
249
+ const escapedCode = this.escapeHtml(code);
250
+
251
+ return `<!-- wp:code -->
249
252
  <pre class="wp-block-code"><code>${escapedCode}</code></pre>
250
253
  <!-- /wp:code -->`;
251
- }
252
-
253
- /**
254
- * Create image block
255
- */
256
- static createImageBlock(url, alt = '', caption = '') {
257
- let attributes = {};
258
- if (alt) attributes.alt = alt;
259
-
260
- const attributesJson = Object.keys(attributes).length > 0
261
- ? ' ' + JSON.stringify(attributes)
262
- : '';
263
-
264
- let imageHtml = `<!-- wp:image${attributesJson} -->
265
- <figure class="wp-block-image"><img src="${url}"${alt ? ` alt="${this.escapeHtml(alt)}"` : ''}/>`;
266
-
267
- if (caption) {
268
- imageHtml += `<figcaption class="wp-element-caption">${this.escapeHtml(caption)}</figcaption>`;
269
- }
270
-
271
- imageHtml += `</figure>
254
+ }
255
+
256
+ /**
257
+ * Create image block
258
+ */
259
+ static createImageBlock(url, alt = "", caption = "") {
260
+ let attributes = {};
261
+ if (alt) attributes.alt = alt;
262
+
263
+ const attributesJson =
264
+ Object.keys(attributes).length > 0
265
+ ? " " + JSON.stringify(attributes)
266
+ : "";
267
+
268
+ let imageHtml = `<!-- wp:image${attributesJson} -->
269
+ <figure class="wp-block-image"><img src="${url}"${alt ? ` alt="${this.escapeHtml(alt)}"` : ""}/>`;
270
+
271
+ if (caption) {
272
+ imageHtml += `<figcaption class="wp-element-caption">${this.escapeHtml(caption)}</figcaption>`;
273
+ }
274
+
275
+ imageHtml += `</figure>
272
276
  <!-- /wp:image -->`;
273
-
274
- return imageHtml;
275
- }
276
-
277
- /**
278
- * Create separator block
279
- */
280
- static createSeparatorBlock() {
281
- return `<!-- wp:separator -->
277
+
278
+ return imageHtml;
279
+ }
280
+
281
+ /**
282
+ * Create separator block
283
+ */
284
+ static createSeparatorBlock() {
285
+ return `<!-- wp:separator -->
282
286
  <hr class="wp-block-separator has-alpha-channel-opacity"/>
283
287
  <!-- /wp:separator -->`;
284
- }
285
-
286
- /**
287
- * Create columns block for advanced layouts
288
- */
289
- static createColumnsBlock(columns) {
290
- const columnCount = columns.length;
291
- let columnsHtml = `<!-- wp:columns {"columns":${columnCount}} -->\n<div class="wp-block-columns">`;
292
-
293
- columns.forEach(column => {
294
- columnsHtml += `\n<!-- wp:column -->\n<div class="wp-block-column">`;
295
- columnsHtml += `\n${column}`;
296
- columnsHtml += `\n</div>\n<!-- /wp:column -->`;
297
- });
298
-
299
- columnsHtml += `\n</div>\n<!-- /wp:columns -->`;
300
- return columnsHtml;
301
- }
302
-
303
- /**
304
- * Create button block
305
- */
306
- static createButtonBlock(text, url = '#', align = 'none') {
307
- return `<!-- wp:buttons {"layout":{"type":"flex","justifyContent":"${align}"}} -->
288
+ }
289
+
290
+ /**
291
+ * Create columns block for advanced layouts
292
+ */
293
+ static createColumnsBlock(columns) {
294
+ const columnCount = columns.length;
295
+ let columnsHtml = `<!-- wp:columns {"columns":${columnCount}} -->\n<div class="wp-block-columns">`;
296
+
297
+ columns.forEach((column) => {
298
+ columnsHtml += `\n<!-- wp:column -->\n<div class="wp-block-column">`;
299
+ columnsHtml += `\n${column}`;
300
+ columnsHtml += `\n</div>\n<!-- /wp:column -->`;
301
+ });
302
+
303
+ columnsHtml += `\n</div>\n<!-- /wp:columns -->`;
304
+ return columnsHtml;
305
+ }
306
+
307
+ /**
308
+ * Create button block
309
+ */
310
+ static createButtonBlock(text, url = "#", align = "none") {
311
+ return `<!-- wp:buttons {"layout":{"type":"flex","justifyContent":"${align}"}} -->
308
312
  <div class="wp-block-buttons">
309
313
  <!-- wp:button -->
310
314
  <div class="wp-block-button"><a class="wp-block-button__link wp-element-button" href="${url}">${this.escapeHtml(text)}</a></div>
311
315
  <!-- /wp:button -->
312
316
  </div>
313
317
  <!-- /wp:buttons -->`;
314
- }
318
+ }
315
319
 
316
- /**
317
- * Create table block
318
- */
319
- static createTableBlock(headers, rows) {
320
- let tableHtml = `<!-- wp:table -->
320
+ /**
321
+ * Create table block
322
+ */
323
+ static createTableBlock(headers, rows) {
324
+ let tableHtml = `<!-- wp:table -->
321
325
  <figure class="wp-block-table"><table class="wp-block-table">`;
322
-
323
- // Add headers
324
- if (headers && headers.length > 0) {
325
- tableHtml += '\n<thead>\n<tr>';
326
- headers.forEach(header => {
327
- tableHtml += `<th>${this.escapeHtml(header)}</th>`;
328
- });
329
- tableHtml += '</tr>\n</thead>';
330
- }
331
-
332
- // Add rows
333
- tableHtml += '\n<tbody>';
334
- rows.forEach(row => {
335
- tableHtml += '\n<tr>';
336
- row.forEach(cell => {
337
- tableHtml += `<td>${this.escapeHtml(cell)}</td>`;
338
- });
339
- tableHtml += '</tr>';
340
- });
341
- tableHtml += '\n</tbody>';
342
-
343
- tableHtml += `\n</table></figure>
326
+
327
+ // Add headers
328
+ if (headers && headers.length > 0) {
329
+ tableHtml += "\n<thead>\n<tr>";
330
+ headers.forEach((header) => {
331
+ tableHtml += `<th>${this.escapeHtml(header)}</th>`;
332
+ });
333
+ tableHtml += "</tr>\n</thead>";
334
+ }
335
+
336
+ // Add rows
337
+ tableHtml += "\n<tbody>";
338
+ rows.forEach((row) => {
339
+ tableHtml += "\n<tr>";
340
+ row.forEach((cell) => {
341
+ tableHtml += `<td>${this.escapeHtml(cell)}</td>`;
342
+ });
343
+ tableHtml += "</tr>";
344
+ });
345
+ tableHtml += "\n</tbody>";
346
+
347
+ tableHtml += `\n</table></figure>
344
348
  <!-- /wp:table -->`;
345
-
346
- return tableHtml;
347
- }
348
-
349
- /**
350
- * Utility: Strip HTML tags from text
351
- */
352
- static stripTags(text) {
353
- return text.replace(/<[^>]+>/g, '').trim();
354
- }
355
-
356
- /**
357
- * Utility: Escape HTML special characters
358
- */
359
- static escapeHtml(text) {
360
- const map = {
361
- '&': '&amp;',
362
- '<': '&lt;',
363
- '>': '&gt;',
364
- '"': '&quot;',
365
- "'": '&#x27;',
366
- "/": '&#x2F;'
367
- };
368
-
369
- return text.replace(/[&<>"'/]/g, char => map[char]);
370
- }
371
-
372
- /**
373
- * Utility: Decode HTML entities
374
- */
375
- static decodeHtml(text) {
376
- const entities = {
377
- '&amp;': '&',
378
- '&lt;': '<',
379
- '&gt;': '>',
380
- '&quot;': '"',
381
- '&#x27;': "'",
382
- '&#x2F;': '/',
383
- '&#39;': "'",
384
- '&nbsp;': ' '
385
- };
386
-
387
- return text.replace(/&[#a-z0-9]+;/gi, entity => entities[entity] || entity);
388
- }
389
-
390
- /**
391
- * Convert markdown to Gutenberg blocks (bonus feature)
392
- */
393
- static markdownToGutenberg(markdown) {
394
- // First convert markdown to HTML (simplified version)
395
- let html = markdown
396
- // Headers
397
- .replace(/^### (.+)$/gm, '<h3>$1</h3>')
398
- .replace(/^## (.+)$/gm, '<h2>$1</h2>')
399
- .replace(/^# (.+)$/gm, '<h1>$1</h1>')
400
- // Bold
401
- .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
402
- // Italic
403
- .replace(/\*(.+?)\*/g, '<em>$1</em>')
404
- // Code blocks
405
- .replace(/```[\s\S]*?```/g, match => {
406
- const code = match.slice(3, -3).trim();
407
- return `<pre><code>${code}</code></pre>`;
408
- })
409
- // Inline code
410
- .replace(/`(.+?)`/g, '<code>$1</code>')
411
- // Blockquotes
412
- .replace(/^> (.+)$/gm, '<blockquote>$1</blockquote>')
413
- // Horizontal rules
414
- .replace(/^---$/gm, '<hr>')
415
- // Lists (simplified)
416
- .replace(/^- (.+)$/gm, '<li>$1</li>')
417
- .replace(/^(\d+)\. (.+)$/gm, '<li>$2</li>');
418
-
419
- // Wrap consecutive li elements in ul/ol
420
- html = html.replace(/(<li>.*<\/li>\n?)+/g, match => {
421
- return `<ul>${match}</ul>`;
422
- });
423
-
424
- // Convert paragraphs
425
- const lines = html.split('\n');
426
- const processedLines = [];
427
- let inBlock = false;
428
-
429
- lines.forEach(line => {
430
- const trimmed = line.trim();
431
- if (trimmed === '') {
432
- inBlock = false;
433
- } else if (!trimmed.startsWith('<')) {
434
- processedLines.push(`<p>${trimmed}</p>`);
435
- } else {
436
- processedLines.push(trimmed);
437
- }
438
- });
439
-
440
- html = processedLines.join('\n');
441
-
442
- // Now convert HTML to Gutenberg
443
- return this.htmlToGutenberg(html);
444
- }
349
+
350
+ return tableHtml;
351
+ }
352
+
353
+ /**
354
+ * Utility: Strip HTML tags from text
355
+ */
356
+ static stripTags(text) {
357
+ return text.replace(/<[^>]+>/g, "").trim();
358
+ }
359
+
360
+ /**
361
+ * Utility: Escape HTML special characters
362
+ */
363
+ static escapeHtml(text) {
364
+ const map = {
365
+ "&": "&amp;",
366
+ "<": "&lt;",
367
+ ">": "&gt;",
368
+ '"': "&quot;",
369
+ "'": "&#x27;",
370
+ "/": "&#x2F;",
371
+ };
372
+
373
+ return text.replace(/[&<>"'/]/g, (char) => map[char]);
374
+ }
375
+
376
+ /**
377
+ * Utility: Decode HTML entities
378
+ */
379
+ static decodeHtml(text) {
380
+ const entities = {
381
+ "&amp;": "&",
382
+ "&lt;": "<",
383
+ "&gt;": ">",
384
+ "&quot;": '"',
385
+ "&#x27;": "'",
386
+ "&#x2F;": "/",
387
+ "&#39;": "'",
388
+ "&nbsp;": " ",
389
+ };
390
+
391
+ return text.replace(
392
+ /&[#a-z0-9]+;/gi,
393
+ (entity) => entities[entity] || entity,
394
+ );
395
+ }
396
+
397
+ /**
398
+ * Strip YAML frontmatter from markdown content
399
+ */
400
+ static stripFrontmatter(markdown) {
401
+ const frontmatterPattern = /^---\n[\s\S]*?\n---\n*/;
402
+ return markdown.replace(frontmatterPattern, "").trim();
403
+ }
404
+
405
+ /**
406
+ * Detect whether content is markdown or HTML
407
+ */
408
+ static isMarkdown(content) {
409
+ const trimmed = content.trim();
410
+ // If it starts with frontmatter, it's markdown
411
+ if (trimmed.startsWith("---\n")) return true;
412
+ // If first meaningful line starts with # heading
413
+ const firstLine = trimmed.split("\n").find((l) => l.trim() !== "");
414
+ if (firstLine && /^#{1,6}\s/.test(firstLine)) return true;
415
+ // If it has markdown links but no HTML tags
416
+ if (/\[.+?\]\(.+?\)/.test(trimmed) && !/<[a-z][\s\S]*>/i.test(trimmed))
417
+ return true;
418
+ // If it has markdown-style bold/italic but no HTML
419
+ if (
420
+ /\*\*.+?\*\*/.test(trimmed) &&
421
+ !/<(strong|b|em|i|p|div|h[1-6])[\s>]/i.test(trimmed)
422
+ )
423
+ return true;
424
+ return false;
425
+ }
426
+
427
+ /**
428
+ * Convert content to Gutenberg blocks, auto-detecting markdown vs HTML
429
+ */
430
+ static toGutenberg(content) {
431
+ if (this.isMarkdown(content)) {
432
+ return this.markdownToGutenberg(content);
433
+ }
434
+ return this.htmlToGutenberg(content);
435
+ }
436
+
437
+ /**
438
+ * Convert inline markdown formatting to HTML (bold, italic, links, code)
439
+ */
440
+ static convertInlineMarkdown(text) {
441
+ return (
442
+ text
443
+ // Links: [text](url)
444
+ .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>')
445
+ // Bold: **text**
446
+ .replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")
447
+ // Italic: *text* (but not inside URLs or already-converted tags)
448
+ .replace(/(?<!\w)\*(?!\*)(.+?)(?<!\*)\*(?!\w)/g, "<em>$1</em>")
449
+ // Inline code: `text`
450
+ .replace(/`([^`]+)`/g, "<code>$1</code>")
451
+ );
452
+ }
453
+
454
+ /**
455
+ * Parse a markdown table into headers and rows
456
+ */
457
+ static parseMarkdownTable(lines) {
458
+ if (lines.length < 2) return null;
459
+
460
+ const parseCells = (line) => {
461
+ return line
462
+ .replace(/^\|/, "")
463
+ .replace(/\|$/, "")
464
+ .split("|")
465
+ .map((cell) => cell.trim());
466
+ };
467
+
468
+ // Check if second line is a separator (|---|---|)
469
+ if (!/^\|?\s*[-:]+\s*\|/.test(lines[1])) return null;
470
+
471
+ const headers = parseCells(lines[0]);
472
+ const rows = [];
473
+
474
+ for (let i = 2; i < lines.length; i++) {
475
+ if (!lines[i].includes("|")) break;
476
+ rows.push(parseCells(lines[i]));
477
+ }
478
+
479
+ return { headers, rows, lineCount: 2 + rows.length };
480
+ }
481
+
482
+ /**
483
+ * Build a Gutenberg block wrapper (inline HTML preserved, no escaping)
484
+ */
485
+ static wrapBlock(blockType, innerHtml, attrs = {}) {
486
+ const attrStr =
487
+ Object.keys(attrs).length > 0 ? " " + JSON.stringify(attrs) : "";
488
+ return `<!-- wp:${blockType}${attrStr} -->\n${innerHtml}\n<!-- /wp:${blockType} -->`;
489
+ }
490
+
491
+ /**
492
+ * Convert markdown to Gutenberg blocks
493
+ * Builds block HTML directly to preserve inline formatting (links, bold, etc.)
494
+ */
495
+ static markdownToGutenberg(markdown) {
496
+ // Strip frontmatter if present
497
+ const content = this.stripFrontmatter(markdown);
498
+ const lines = content.split("\n");
499
+ const blocks = [];
500
+ let i = 0;
501
+
502
+ while (i < lines.length) {
503
+ const line = lines[i];
504
+ const trimmed = line.trim();
505
+
506
+ // Skip empty lines
507
+ if (trimmed === "") {
508
+ i++;
509
+ continue;
510
+ }
511
+
512
+ // Headings: # through ######
513
+ const headingMatch = trimmed.match(/^(#{1,6})\s+(.+)$/);
514
+ if (headingMatch) {
515
+ const level = headingMatch[1].length;
516
+ const text = this.convertInlineMarkdown(headingMatch[2]);
517
+ blocks.push(
518
+ this.wrapBlock(
519
+ "heading",
520
+ `<h${level} class="wp-block-heading">${text}</h${level}>`,
521
+ { level },
522
+ ),
523
+ );
524
+ i++;
525
+ continue;
526
+ }
527
+
528
+ // Horizontal rule: --- or *** or ___
529
+ if (/^(-{3,}|\*{3,}|_{3,})$/.test(trimmed)) {
530
+ blocks.push(
531
+ this.wrapBlock(
532
+ "separator",
533
+ '<hr class="wp-block-separator has-alpha-channel-opacity"/>',
534
+ ),
535
+ );
536
+ i++;
537
+ continue;
538
+ }
539
+
540
+ // Code blocks: ```
541
+ if (trimmed.startsWith("```")) {
542
+ const codeLines = [];
543
+ i++;
544
+ while (i < lines.length && !lines[i].trim().startsWith("```")) {
545
+ codeLines.push(lines[i]);
546
+ i++;
547
+ }
548
+ i++; // skip closing ```
549
+ const code = this.escapeHtml(codeLines.join("\n"));
550
+ blocks.push(
551
+ this.wrapBlock(
552
+ "code",
553
+ `<pre class="wp-block-code"><code>${code}</code></pre>`,
554
+ ),
555
+ );
556
+ continue;
557
+ }
558
+
559
+ // Tables: | header | header |
560
+ if (
561
+ trimmed.startsWith("|") ||
562
+ (trimmed.includes("|") &&
563
+ i + 1 < lines.length &&
564
+ /^\|?\s*[-:]+/.test(lines[i + 1].trim()))
565
+ ) {
566
+ const tableLines = [];
567
+ let j = i;
568
+ while (j < lines.length && lines[j].trim().includes("|")) {
569
+ tableLines.push(lines[j].trim());
570
+ j++;
571
+ }
572
+ const table = this.parseMarkdownTable(tableLines);
573
+ if (table) {
574
+ const headers = table.headers.map((h) =>
575
+ this.convertInlineMarkdown(h),
576
+ );
577
+ const rows = table.rows.map((row) =>
578
+ row.map((cell) => this.convertInlineMarkdown(cell)),
579
+ );
580
+ let tableHtml =
581
+ '<figure class="wp-block-table"><table class="wp-block-table">';
582
+ tableHtml += "\n<thead>\n<tr>";
583
+ headers.forEach((h) => {
584
+ tableHtml += `<th>${h}</th>`;
585
+ });
586
+ tableHtml += "</tr>\n</thead>";
587
+ tableHtml += "\n<tbody>";
588
+ rows.forEach((row) => {
589
+ tableHtml += "\n<tr>";
590
+ row.forEach((cell) => {
591
+ tableHtml += `<td>${cell}</td>`;
592
+ });
593
+ tableHtml += "</tr>";
594
+ });
595
+ tableHtml += "\n</tbody>\n</table></figure>";
596
+ blocks.push(this.wrapBlock("table", tableHtml));
597
+ i += table.lineCount;
598
+ continue;
599
+ }
600
+ // If table parsing failed, fall through to paragraph
601
+ }
602
+
603
+ // Blockquotes: > text
604
+ if (trimmed.startsWith("> ")) {
605
+ const quoteLines = [];
606
+ while (i < lines.length && lines[i].trim().startsWith("> ")) {
607
+ quoteLines.push(lines[i].trim().slice(2));
608
+ i++;
609
+ }
610
+ const quoteText = this.convertInlineMarkdown(quoteLines.join(" "));
611
+ blocks.push(
612
+ this.wrapBlock(
613
+ "quote",
614
+ `<blockquote class="wp-block-quote">\n<p>${quoteText}</p>\n</blockquote>`,
615
+ ),
616
+ );
617
+ continue;
618
+ }
619
+
620
+ // Unordered lists: - item or * item
621
+ if (/^[-*]\s+/.test(trimmed)) {
622
+ const items = [];
623
+ while (i < lines.length && /^[-*]\s+/.test(lines[i].trim())) {
624
+ const itemText = lines[i].trim().replace(/^[-*]\s+/, "");
625
+ items.push(this.convertInlineMarkdown(itemText));
626
+ i++;
627
+ }
628
+ const listItems = items.map((item) => `<li>${item}</li>`).join("\n");
629
+ blocks.push(
630
+ this.wrapBlock("list", `<ul class="wp-block-list">${listItems}</ul>`),
631
+ );
632
+ continue;
633
+ }
634
+
635
+ // Ordered lists: 1. item
636
+ if (/^\d+\.\s+/.test(trimmed)) {
637
+ const items = [];
638
+ while (i < lines.length && /^\d+\.\s+/.test(lines[i].trim())) {
639
+ const itemText = lines[i].trim().replace(/^\d+\.\s+/, "");
640
+ items.push(this.convertInlineMarkdown(itemText));
641
+ i++;
642
+ }
643
+ const listItems = items.map((item) => `<li>${item}</li>`).join("\n");
644
+ blocks.push(
645
+ this.wrapBlock(
646
+ "list",
647
+ `<ol class="wp-block-list">${listItems}</ol>`,
648
+ { ordered: true },
649
+ ),
650
+ );
651
+ continue;
652
+ }
653
+
654
+ // Images: ![alt](url)
655
+ const imageMatch = trimmed.match(/^!\[([^\]]*)\]\(([^)]+)\)$/);
656
+ if (imageMatch) {
657
+ const [, alt, url] = imageMatch;
658
+ const imgAttrs = alt ? ` alt="${this.escapeHtml(alt)}"` : "";
659
+ blocks.push(
660
+ this.wrapBlock(
661
+ "image",
662
+ `<figure class="wp-block-image"><img src="${url}"${imgAttrs}/></figure>`,
663
+ ),
664
+ );
665
+ i++;
666
+ continue;
667
+ }
668
+
669
+ // Paragraph: everything else (collect consecutive non-empty lines)
670
+ const paraLines = [];
671
+ while (
672
+ i < lines.length &&
673
+ lines[i].trim() !== "" &&
674
+ !lines[i].trim().match(/^#{1,6}\s/) &&
675
+ !lines[i].trim().match(/^(-{3,}|\*{3,}|_{3,})$/) &&
676
+ !lines[i].trim().startsWith("```") &&
677
+ !lines[i].trim().startsWith("> ") &&
678
+ !lines[i].trim().match(/^[-*]\s+/) &&
679
+ !lines[i].trim().match(/^\d+\.\s+/) &&
680
+ !lines[i].trim().match(/^!\[/) &&
681
+ !(
682
+ lines[i].trim().startsWith("|") &&
683
+ i + 1 < lines.length &&
684
+ lines[i + 1] &&
685
+ /^\|?\s*[-:]/.test(lines[i + 1].trim())
686
+ )
687
+ ) {
688
+ paraLines.push(lines[i].trim());
689
+ i++;
690
+ }
691
+ if (paraLines.length > 0) {
692
+ const paraText = this.convertInlineMarkdown(paraLines.join(" "));
693
+ blocks.push(this.wrapBlock("paragraph", `<p>${paraText}</p>`));
694
+ }
695
+ }
696
+
697
+ return blocks.join("\n\n");
698
+ }
445
699
  }
446
700
 
447
- export default GutenbergConverter;
701
+ export default GutenbergConverter;