myaidev-method 0.3.4 → 0.3.6
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/.claude-plugin/plugin.json +0 -1
- package/.env.example +5 -4
- package/CHANGELOG.md +2 -2
- package/CONTENT_CREATION_GUIDE.md +489 -3211
- package/DEVELOPER_USE_CASES.md +1 -1
- package/MODULAR_INSTALLATION.md +2 -2
- package/README.md +39 -33
- package/TECHNICAL_ARCHITECTURE.md +1 -1
- package/USER_GUIDE.md +242 -190
- package/agents/content-editor-agent.md +90 -0
- package/agents/content-planner-agent.md +97 -0
- package/agents/content-research-agent.md +62 -0
- package/agents/content-seo-agent.md +101 -0
- package/agents/content-writer-agent.md +69 -0
- package/agents/infographic-analyzer-agent.md +63 -0
- package/agents/infographic-designer-agent.md +72 -0
- package/bin/cli.js +846 -427
- package/{content-rules.example.md → content-rules-example.md} +2 -2
- package/dist/mcp/health-check.js +82 -68
- package/dist/mcp/mcp-config.json +8 -0
- package/dist/mcp/openstack-server.js +1746 -1262
- package/dist/server/.tsbuildinfo +1 -1
- package/extension.json +21 -4
- package/package.json +181 -184
- package/skills/company-config/SKILL.md +133 -0
- package/skills/configure/SKILL.md +1 -1
- package/skills/myai-configurator/SKILL.md +77 -0
- package/skills/myai-configurator/content-creation-configurator/SKILL.md +516 -0
- package/skills/myai-configurator/content-maintenance-configurator/SKILL.md +397 -0
- package/skills/myai-content-enrichment/SKILL.md +114 -0
- package/skills/myai-content-ideation/SKILL.md +288 -0
- package/skills/myai-content-ideation/evals/evals.json +182 -0
- package/skills/myai-content-production-coordinator/SKILL.md +946 -0
- package/skills/{content-rules-setup → myai-content-rules-setup}/SKILL.md +1 -1
- package/skills/{content-verifier → myai-content-verifier}/SKILL.md +1 -1
- package/skills/myai-content-writer/SKILL.md +333 -0
- package/skills/{infographic → myai-infographic}/SKILL.md +1 -1
- package/skills/myai-proprietary-content-verifier/SKILL.md +175 -0
- package/skills/myai-proprietary-content-verifier/evals/evals.json +36 -0
- package/skills/myai-skill-builder/SKILL.md +699 -0
- package/skills/myai-skill-builder/agents/analyzer-agent.md +137 -0
- package/skills/myai-skill-builder/agents/comparator-agent.md +77 -0
- package/skills/myai-skill-builder/agents/grader-agent.md +103 -0
- package/skills/myai-skill-builder/assets/eval_review.html +131 -0
- package/skills/myai-skill-builder/references/schemas.md +211 -0
- package/skills/myai-skill-builder/scripts/aggregate_benchmark.py +190 -0
- package/skills/myai-skill-builder/scripts/generate_review.py +381 -0
- package/skills/myai-skill-builder/scripts/package_skill.py +91 -0
- package/skills/myai-skill-builder/scripts/run_eval.py +105 -0
- package/skills/myai-skill-builder/scripts/run_loop.py +211 -0
- package/skills/myai-skill-builder/scripts/utils.py +123 -0
- package/skills/myai-visual-generator/SKILL.md +125 -0
- package/skills/myai-visual-generator/evals/evals.json +155 -0
- package/skills/myai-visual-generator/references/infographic-pipeline.md +73 -0
- package/skills/myai-visual-generator/references/research-visuals.md +57 -0
- package/skills/myai-visual-generator/references/services.md +89 -0
- package/skills/myai-visual-generator/scripts/visual-generation-utils.js +1272 -0
- package/skills/myaidev-figma/SKILL.md +212 -0
- package/skills/myaidev-figma/capture.js +133 -0
- package/skills/myaidev-figma/crawl.js +130 -0
- package/skills/myaidev-figma-configure/SKILL.md +130 -0
- package/skills/openstack-manager/SKILL.md +1 -1
- package/skills/payloadcms-publisher/SKILL.md +141 -77
- package/skills/payloadcms-publisher/references/field-mapping.md +142 -0
- package/skills/payloadcms-publisher/references/lexical-format.md +97 -0
- package/skills/security-auditor/SKILL.md +1 -1
- package/src/cli/commands/addon.js +105 -7
- package/src/config/workflows.js +172 -228
- package/src/lib/ascii-banner.js +197 -182
- package/src/lib/{content-coordinator.js → content-production-coordinator.js} +649 -459
- package/src/lib/installation-detector.js +93 -59
- package/src/lib/payloadcms-utils.js +285 -510
- package/src/lib/workflow-installer.js +55 -0
- package/src/mcp/health-check.js +82 -68
- package/src/mcp/openstack-server.js +1746 -1262
- package/src/scripts/configure-visual-apis.js +224 -173
- package/src/scripts/configure-wordpress-mcp.js +96 -66
- package/src/scripts/init/install.js +109 -85
- package/src/scripts/init-project.js +138 -67
- package/src/scripts/utils/write-content.js +67 -52
- package/src/scripts/wordpress/publish-to-wordpress.js +128 -128
- package/src/templates/claude/CLAUDE.md +19 -12
- package/hooks/hooks.json +0 -26
- package/skills/content-coordinator/SKILL.md +0 -130
- package/skills/content-enrichment/SKILL.md +0 -80
- package/skills/content-writer/SKILL.md +0 -285
- package/skills/skill-builder/SKILL.md +0 -417
- package/skills/visual-generator/SKILL.md +0 -140
- /package/skills/{content-writer → myai-content-writer}/agents/editor-agent.md +0 -0
- /package/skills/{content-writer → myai-content-writer}/agents/planner-agent.md +0 -0
- /package/skills/{content-writer → myai-content-writer}/agents/research-agent.md +0 -0
- /package/skills/{content-writer → myai-content-writer}/agents/seo-agent.md +0 -0
- /package/skills/{content-writer → myai-content-writer}/agents/visual-planner-agent.md +0 -0
- /package/skills/{content-writer → myai-content-writer}/agents/writer-agent.md +0 -0
|
@@ -4,9 +4,95 @@
|
|
|
4
4
|
* Optimized for Claude Code 2.0 agent integration
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { readFileSync } from
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
7
|
+
import { readFileSync } from "fs";
|
|
8
|
+
import { dirname, resolve } from "path";
|
|
9
|
+
import { parse } from "dotenv";
|
|
10
|
+
import { createHeadlessEditor } from "@payloadcms/richtext-lexical/lexical/headless";
|
|
11
|
+
import {
|
|
12
|
+
$convertFromMarkdownString,
|
|
13
|
+
CHECK_LIST,
|
|
14
|
+
TRANSFORMERS,
|
|
15
|
+
} from "@payloadcms/richtext-lexical/lexical/markdown";
|
|
16
|
+
import {
|
|
17
|
+
ListNode,
|
|
18
|
+
ListItemNode,
|
|
19
|
+
} from "@payloadcms/richtext-lexical/lexical/list";
|
|
20
|
+
import {
|
|
21
|
+
HeadingNode,
|
|
22
|
+
QuoteNode,
|
|
23
|
+
} from "@payloadcms/richtext-lexical/lexical/rich-text";
|
|
24
|
+
import { TableNode, TableRowNode, TableCellNode } from "@lexical/table";
|
|
25
|
+
|
|
26
|
+
// PayloadCMS internal nodes and transformers (file:// import to bypass exports map)
|
|
27
|
+
const _base = new URL(
|
|
28
|
+
"../../node_modules/@payloadcms/richtext-lexical/",
|
|
29
|
+
import.meta.url,
|
|
30
|
+
).href;
|
|
31
|
+
const { HorizontalRuleServerNode } = await import(
|
|
32
|
+
_base + "dist/features/horizontalRule/server/nodes/HorizontalRuleNode.js"
|
|
33
|
+
);
|
|
34
|
+
const { MarkdownTransformer: HRTransformer } = await import(
|
|
35
|
+
_base + "dist/features/horizontalRule/server/markdownTransformer.js"
|
|
36
|
+
);
|
|
37
|
+
const { LinkMarkdownTransformer } = await import(
|
|
38
|
+
_base + "dist/features/link/markdownTransformer.js"
|
|
39
|
+
);
|
|
40
|
+
const { LinkNode } = await import(
|
|
41
|
+
_base + "dist/features/link/nodes/LinkNode.js"
|
|
42
|
+
);
|
|
43
|
+
const { TableMarkdownTransformer: TableTransformerFactory } = await import(
|
|
44
|
+
_base + "dist/features/experimental_table/markdownTransformer.js"
|
|
45
|
+
);
|
|
46
|
+
const { UploadServerNode } = await import(
|
|
47
|
+
_base + "dist/features/upload/server/nodes/UploadNode.js"
|
|
48
|
+
);
|
|
49
|
+
const { ServerBlockNode } = await import(
|
|
50
|
+
_base + "dist/features/blocks/server/nodes/BlocksNode.js"
|
|
51
|
+
);
|
|
52
|
+
const { getBlockMarkdownTransformers } = await import(
|
|
53
|
+
_base + "dist/features/blocks/server/markdown/markdownTransformer.js"
|
|
54
|
+
);
|
|
55
|
+
const { CodeBlock } = await import(
|
|
56
|
+
_base + "dist/features/blocks/premade/CodeBlock/index.js"
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// Build transformers: CHECK_LIST must precede UNORDERED_LIST to match first
|
|
60
|
+
const BASE_TRANSFORMERS = [
|
|
61
|
+
CHECK_LIST,
|
|
62
|
+
...TRANSFORMERS,
|
|
63
|
+
HRTransformer,
|
|
64
|
+
LinkMarkdownTransformer,
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
// Code block support via PayloadCMS Blocks feature (converts ```lang blocks to block nodes)
|
|
68
|
+
const codeBlockDef = CodeBlock();
|
|
69
|
+
const blockTransformerFactories = getBlockMarkdownTransformers({
|
|
70
|
+
blocks: [codeBlockDef],
|
|
71
|
+
inlineBlocks: [],
|
|
72
|
+
});
|
|
73
|
+
const LEXICAL_NODES = [
|
|
74
|
+
HeadingNode,
|
|
75
|
+
QuoteNode,
|
|
76
|
+
ListNode,
|
|
77
|
+
ListItemNode,
|
|
78
|
+
LinkNode,
|
|
79
|
+
HorizontalRuleServerNode,
|
|
80
|
+
TableNode,
|
|
81
|
+
TableRowNode,
|
|
82
|
+
TableCellNode,
|
|
83
|
+
UploadServerNode,
|
|
84
|
+
ServerBlockNode,
|
|
85
|
+
];
|
|
86
|
+
const resolvedBlockTransformers = blockTransformerFactories.map((f) =>
|
|
87
|
+
typeof f === "function"
|
|
88
|
+
? f({ allNodes: LEXICAL_NODES, allTransformers: BASE_TRANSFORMERS })
|
|
89
|
+
: f,
|
|
90
|
+
);
|
|
91
|
+
const LEXICAL_TRANSFORMERS = [
|
|
92
|
+
...BASE_TRANSFORMERS,
|
|
93
|
+
TableTransformerFactory({ allTransformers: BASE_TRANSFORMERS }),
|
|
94
|
+
...resolvedBlockTransformers,
|
|
95
|
+
];
|
|
10
96
|
|
|
11
97
|
export class PayloadCMSUtils {
|
|
12
98
|
constructor(config = {}) {
|
|
@@ -16,7 +102,7 @@ export class PayloadCMSUtils {
|
|
|
16
102
|
config = { ...envConfig, ...config };
|
|
17
103
|
}
|
|
18
104
|
|
|
19
|
-
this.url = config.url?.replace(/\/$/,
|
|
105
|
+
this.url = config.url?.replace(/\/$/, "");
|
|
20
106
|
this.email = config.email;
|
|
21
107
|
this.password = config.password;
|
|
22
108
|
this.token = null;
|
|
@@ -24,17 +110,19 @@ export class PayloadCMSUtils {
|
|
|
24
110
|
|
|
25
111
|
loadEnvConfig() {
|
|
26
112
|
try {
|
|
27
|
-
const envPath = process.env.ENV_PATH ||
|
|
28
|
-
const envContent = readFileSync(envPath,
|
|
113
|
+
const envPath = process.env.ENV_PATH || ".env";
|
|
114
|
+
const envContent = readFileSync(envPath, "utf8");
|
|
29
115
|
const parsed = parse(envContent);
|
|
30
116
|
|
|
31
117
|
return {
|
|
32
118
|
url: parsed.PAYLOADCMS_URL,
|
|
33
119
|
email: parsed.PAYLOADCMS_EMAIL,
|
|
34
|
-
password: parsed.PAYLOADCMS_PASSWORD
|
|
120
|
+
password: parsed.PAYLOADCMS_PASSWORD,
|
|
35
121
|
};
|
|
36
122
|
} catch (error) {
|
|
37
|
-
throw new Error(
|
|
123
|
+
throw new Error(
|
|
124
|
+
`Failed to load PayloadCMS configuration: ${error.message}`,
|
|
125
|
+
);
|
|
38
126
|
}
|
|
39
127
|
}
|
|
40
128
|
|
|
@@ -44,19 +132,21 @@ export class PayloadCMSUtils {
|
|
|
44
132
|
*/
|
|
45
133
|
async authenticate() {
|
|
46
134
|
if (!this.email || !this.password) {
|
|
47
|
-
throw new Error(
|
|
135
|
+
throw new Error(
|
|
136
|
+
"PAYLOADCMS_EMAIL and PAYLOADCMS_PASSWORD required for authentication",
|
|
137
|
+
);
|
|
48
138
|
}
|
|
49
139
|
|
|
50
140
|
try {
|
|
51
141
|
const response = await fetch(`${this.url}/api/users/login`, {
|
|
52
|
-
method:
|
|
142
|
+
method: "POST",
|
|
53
143
|
headers: {
|
|
54
|
-
|
|
144
|
+
"Content-Type": "application/json",
|
|
55
145
|
},
|
|
56
146
|
body: JSON.stringify({
|
|
57
147
|
email: this.email,
|
|
58
|
-
password: this.password
|
|
59
|
-
})
|
|
148
|
+
password: this.password,
|
|
149
|
+
}),
|
|
60
150
|
});
|
|
61
151
|
|
|
62
152
|
if (!response.ok) {
|
|
@@ -69,9 +159,9 @@ export class PayloadCMSUtils {
|
|
|
69
159
|
|
|
70
160
|
return {
|
|
71
161
|
success: true,
|
|
72
|
-
method:
|
|
162
|
+
method: "jwt",
|
|
73
163
|
user: data.user,
|
|
74
|
-
token: this.token
|
|
164
|
+
token: this.token,
|
|
75
165
|
};
|
|
76
166
|
} catch (error) {
|
|
77
167
|
throw new Error(`PayloadCMS authentication failed: ${error.message}`);
|
|
@@ -88,21 +178,24 @@ export class PayloadCMSUtils {
|
|
|
88
178
|
|
|
89
179
|
const url = `${this.url}${endpoint}`;
|
|
90
180
|
const headers = {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
...options.headers
|
|
181
|
+
"Content-Type": "application/json",
|
|
182
|
+
Authorization: `JWT ${this.token}`,
|
|
183
|
+
...options.headers,
|
|
94
184
|
};
|
|
95
185
|
|
|
96
186
|
try {
|
|
97
187
|
const response = await fetch(url, {
|
|
98
188
|
...options,
|
|
99
|
-
headers
|
|
189
|
+
headers,
|
|
100
190
|
});
|
|
101
191
|
|
|
102
192
|
const data = await response.json().catch(() => ({}));
|
|
103
193
|
|
|
104
194
|
if (!response.ok) {
|
|
105
|
-
|
|
195
|
+
const detail = data.errors
|
|
196
|
+
? JSON.stringify(data.errors)
|
|
197
|
+
: data.message || response.statusText;
|
|
198
|
+
throw new Error(`HTTP ${response.status}: ${detail}`);
|
|
106
199
|
}
|
|
107
200
|
|
|
108
201
|
return data;
|
|
@@ -112,479 +205,27 @@ export class PayloadCMSUtils {
|
|
|
112
205
|
}
|
|
113
206
|
|
|
114
207
|
/**
|
|
115
|
-
* Convert markdown to Lexical rich text format
|
|
116
|
-
*
|
|
208
|
+
* Convert markdown to Lexical rich text format using PayloadCMS's own Lexical
|
|
209
|
+
* node classes and markdown transformers via a headless editor. This produces
|
|
210
|
+
* serialized JSON identical to what PayloadCMS's editor generates, so
|
|
211
|
+
* parseEditorState() on the server will find all registered node types.
|
|
117
212
|
*/
|
|
118
213
|
convertMarkdownToLexical(markdown) {
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
children: [{ type: 'text', text: token.text, format: 0 }],
|
|
128
|
-
direction: 'ltr',
|
|
129
|
-
format: '',
|
|
130
|
-
indent: 0,
|
|
131
|
-
version: 1
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
case 'paragraph':
|
|
135
|
-
return {
|
|
136
|
-
type: 'paragraph',
|
|
137
|
-
children: this.parseInlineText(token.text),
|
|
138
|
-
direction: 'ltr',
|
|
139
|
-
format: '',
|
|
140
|
-
indent: 0,
|
|
141
|
-
version: 1
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
case 'list':
|
|
145
|
-
return {
|
|
146
|
-
type: 'list',
|
|
147
|
-
listType: token.ordered ? 'number' : 'bullet',
|
|
148
|
-
start: token.start || 1,
|
|
149
|
-
tag: token.ordered ? 'ol' : 'ul',
|
|
150
|
-
children: token.items.map(item => ({
|
|
151
|
-
type: 'listitem',
|
|
152
|
-
value: 1,
|
|
153
|
-
children: [{
|
|
154
|
-
type: 'paragraph',
|
|
155
|
-
children: this.parseInlineText(item.text),
|
|
156
|
-
direction: 'ltr',
|
|
157
|
-
format: '',
|
|
158
|
-
indent: 0
|
|
159
|
-
}],
|
|
160
|
-
direction: 'ltr',
|
|
161
|
-
format: '',
|
|
162
|
-
indent: 0
|
|
163
|
-
})),
|
|
164
|
-
direction: 'ltr',
|
|
165
|
-
format: '',
|
|
166
|
-
indent: 0,
|
|
167
|
-
version: 1
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
case 'code':
|
|
171
|
-
return {
|
|
172
|
-
type: 'code',
|
|
173
|
-
language: token.lang || 'plaintext',
|
|
174
|
-
children: [{ type: 'text', text: token.text, format: 0 }],
|
|
175
|
-
direction: 'ltr',
|
|
176
|
-
format: '',
|
|
177
|
-
indent: 0,
|
|
178
|
-
version: 1
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
case 'blockquote':
|
|
182
|
-
return {
|
|
183
|
-
type: 'quote',
|
|
184
|
-
children: [{
|
|
185
|
-
type: 'paragraph',
|
|
186
|
-
children: this.parseInlineText(token.text),
|
|
187
|
-
direction: 'ltr',
|
|
188
|
-
format: '',
|
|
189
|
-
indent: 0
|
|
190
|
-
}],
|
|
191
|
-
direction: 'ltr',
|
|
192
|
-
format: '',
|
|
193
|
-
indent: 0,
|
|
194
|
-
version: 1
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
case 'hr':
|
|
198
|
-
return {
|
|
199
|
-
type: 'horizontalrule',
|
|
200
|
-
version: 1
|
|
201
|
-
};
|
|
202
|
-
|
|
203
|
-
default:
|
|
204
|
-
return {
|
|
205
|
-
type: 'paragraph',
|
|
206
|
-
children: [{ type: 'text', text: token.raw || '', format: 0 }],
|
|
207
|
-
direction: 'ltr',
|
|
208
|
-
format: '',
|
|
209
|
-
indent: 0,
|
|
210
|
-
version: 1
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
};
|
|
214
|
-
|
|
215
|
-
// Filter out space tokens and convert remaining tokens
|
|
216
|
-
const children = tokens
|
|
217
|
-
.filter(token => token.type !== 'space')
|
|
218
|
-
.map(convertToken);
|
|
219
|
-
|
|
220
|
-
return {
|
|
221
|
-
root: {
|
|
222
|
-
type: 'root',
|
|
223
|
-
format: '',
|
|
224
|
-
indent: 0,
|
|
225
|
-
version: 1,
|
|
226
|
-
children,
|
|
227
|
-
direction: 'ltr'
|
|
228
|
-
}
|
|
229
|
-
};
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* Parse inline text with formatting (bold, italic, code, links)
|
|
234
|
-
* Supports markdown formatters and converts to Lexical format codes:
|
|
235
|
-
* - **bold** → format: 1
|
|
236
|
-
* - *italic* → format: 2
|
|
237
|
-
* - `code` → format: 16
|
|
238
|
-
* - ***bold+italic*** → format: 3
|
|
239
|
-
*/
|
|
240
|
-
parseInlineText(text) {
|
|
241
|
-
const children = [];
|
|
242
|
-
|
|
243
|
-
// Strip HTML tags first
|
|
244
|
-
text = text.replace(/<[^>]+>/g, '');
|
|
245
|
-
|
|
246
|
-
// Parse markdown inline formatting using marked's inline lexer
|
|
247
|
-
try {
|
|
248
|
-
const tokens = marked.lexer(text);
|
|
249
|
-
|
|
250
|
-
// Extract inline tokens from paragraph if present
|
|
251
|
-
let inlineTokens = [];
|
|
252
|
-
if (tokens[0]?.type === 'paragraph' && tokens[0].tokens) {
|
|
253
|
-
inlineTokens = tokens[0].tokens;
|
|
254
|
-
} else if (tokens[0]?.tokens) {
|
|
255
|
-
inlineTokens = tokens[0].tokens;
|
|
256
|
-
} else {
|
|
257
|
-
// No inline tokens, return plain text
|
|
258
|
-
return [{
|
|
259
|
-
type: 'text',
|
|
260
|
-
text: text,
|
|
261
|
-
format: 0,
|
|
262
|
-
mode: 'normal',
|
|
263
|
-
style: '',
|
|
264
|
-
detail: 0,
|
|
265
|
-
version: 1
|
|
266
|
-
}];
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// Convert each inline token to Lexical format
|
|
270
|
-
for (const token of inlineTokens) {
|
|
271
|
-
switch (token.type) {
|
|
272
|
-
case 'text':
|
|
273
|
-
children.push({
|
|
274
|
-
type: 'text',
|
|
275
|
-
text: token.text,
|
|
276
|
-
format: 0,
|
|
277
|
-
mode: 'normal',
|
|
278
|
-
style: '',
|
|
279
|
-
detail: 0,
|
|
280
|
-
version: 1
|
|
281
|
-
});
|
|
282
|
-
break;
|
|
283
|
-
|
|
284
|
-
case 'strong':
|
|
285
|
-
children.push({
|
|
286
|
-
type: 'text',
|
|
287
|
-
text: token.text,
|
|
288
|
-
format: 1, // IS_BOLD
|
|
289
|
-
mode: 'normal',
|
|
290
|
-
style: '',
|
|
291
|
-
detail: 0,
|
|
292
|
-
version: 1
|
|
293
|
-
});
|
|
294
|
-
break;
|
|
295
|
-
|
|
296
|
-
case 'em':
|
|
297
|
-
children.push({
|
|
298
|
-
type: 'text',
|
|
299
|
-
text: token.text,
|
|
300
|
-
format: 2, // IS_ITALIC
|
|
301
|
-
mode: 'normal',
|
|
302
|
-
style: '',
|
|
303
|
-
detail: 0,
|
|
304
|
-
version: 1
|
|
305
|
-
});
|
|
306
|
-
break;
|
|
307
|
-
|
|
308
|
-
case 'codespan':
|
|
309
|
-
children.push({
|
|
310
|
-
type: 'text',
|
|
311
|
-
text: token.text,
|
|
312
|
-
format: 16, // IS_CODE
|
|
313
|
-
mode: 'normal',
|
|
314
|
-
style: '',
|
|
315
|
-
detail: 0,
|
|
316
|
-
version: 1
|
|
317
|
-
});
|
|
318
|
-
break;
|
|
319
|
-
|
|
320
|
-
case 'link':
|
|
321
|
-
children.push({
|
|
322
|
-
type: 'link',
|
|
323
|
-
url: token.href,
|
|
324
|
-
children: [{
|
|
325
|
-
type: 'text',
|
|
326
|
-
text: token.text,
|
|
327
|
-
format: 0,
|
|
328
|
-
mode: 'normal',
|
|
329
|
-
style: '',
|
|
330
|
-
detail: 0,
|
|
331
|
-
version: 1
|
|
332
|
-
}],
|
|
333
|
-
direction: 'ltr',
|
|
334
|
-
format: '',
|
|
335
|
-
indent: 0,
|
|
336
|
-
version: 3
|
|
337
|
-
});
|
|
338
|
-
break;
|
|
339
|
-
|
|
340
|
-
default:
|
|
341
|
-
// Fallback for any unhandled token types
|
|
342
|
-
children.push({
|
|
343
|
-
type: 'text',
|
|
344
|
-
text: token.raw || token.text || '',
|
|
345
|
-
format: 0,
|
|
346
|
-
mode: 'normal',
|
|
347
|
-
style: '',
|
|
348
|
-
detail: 0,
|
|
349
|
-
version: 1
|
|
350
|
-
});
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
} catch (error) {
|
|
354
|
-
// If parsing fails, return plain text
|
|
355
|
-
children.push({
|
|
356
|
-
type: 'text',
|
|
357
|
-
text: text,
|
|
358
|
-
format: 0,
|
|
359
|
-
mode: 'normal',
|
|
360
|
-
style: '',
|
|
361
|
-
detail: 0,
|
|
362
|
-
version: 1
|
|
363
|
-
});
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
return children.length > 0 ? children : [{
|
|
367
|
-
type: 'text',
|
|
368
|
-
text: text,
|
|
369
|
-
format: 0,
|
|
370
|
-
mode: 'normal',
|
|
371
|
-
style: '',
|
|
372
|
-
detail: 0,
|
|
373
|
-
version: 1
|
|
374
|
-
}];
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
/**
|
|
378
|
-
* Validate Lexical JSON structure
|
|
379
|
-
* Ensures output matches PayloadCMS Lexical editor format
|
|
380
|
-
* @param {Object} lexicalJSON - The Lexical JSON to validate
|
|
381
|
-
* @param {Object} options - Validation options
|
|
382
|
-
* @returns {Object} Validation result with errors array
|
|
383
|
-
*/
|
|
384
|
-
validateLexicalStructure(lexicalJSON, options = {}) {
|
|
385
|
-
const errors = [];
|
|
386
|
-
const warnings = [];
|
|
387
|
-
|
|
388
|
-
// Check root structure
|
|
389
|
-
if (!lexicalJSON || typeof lexicalJSON !== 'object') {
|
|
390
|
-
errors.push('Lexical JSON must be an object');
|
|
391
|
-
return { valid: false, errors, warnings };
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
if (!lexicalJSON.root) {
|
|
395
|
-
errors.push('Missing root node');
|
|
396
|
-
return { valid: false, errors, warnings };
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
const root = lexicalJSON.root;
|
|
400
|
-
|
|
401
|
-
// Validate root node
|
|
402
|
-
if (root.type !== 'root') {
|
|
403
|
-
errors.push(`Root type must be 'root', got '${root.type}'`);
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
if (!Array.isArray(root.children)) {
|
|
407
|
-
errors.push('Root must have children array');
|
|
408
|
-
return { valid: false, errors, warnings };
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
// Validate each child node
|
|
412
|
-
const validateNode = (node, path = 'root') => {
|
|
413
|
-
if (!node || typeof node !== 'object') {
|
|
414
|
-
errors.push(`${path}: Node must be an object`);
|
|
415
|
-
return;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
if (!node.type) {
|
|
419
|
-
errors.push(`${path}: Node missing type property`);
|
|
420
|
-
return;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
// Validate based on node type
|
|
424
|
-
switch (node.type) {
|
|
425
|
-
case 'heading':
|
|
426
|
-
if (!node.tag || !/^h[1-6]$/.test(node.tag)) {
|
|
427
|
-
errors.push(`${path}: Heading must have tag h1-h6, got '${node.tag}'`);
|
|
428
|
-
}
|
|
429
|
-
if (!Array.isArray(node.children)) {
|
|
430
|
-
errors.push(`${path}: Heading must have children array`);
|
|
431
|
-
}
|
|
432
|
-
break;
|
|
433
|
-
|
|
434
|
-
case 'paragraph':
|
|
435
|
-
if (!Array.isArray(node.children)) {
|
|
436
|
-
errors.push(`${path}: Paragraph must have children array`);
|
|
437
|
-
}
|
|
438
|
-
break;
|
|
439
|
-
|
|
440
|
-
case 'list':
|
|
441
|
-
if (!['bullet', 'number', 'check'].includes(node.listType)) {
|
|
442
|
-
errors.push(`${path}: List listType must be bullet/number/check, got '${node.listType}'`);
|
|
443
|
-
}
|
|
444
|
-
if (!['ul', 'ol'].includes(node.tag)) {
|
|
445
|
-
errors.push(`${path}: List tag must be ul/ol, got '${node.tag}'`);
|
|
446
|
-
}
|
|
447
|
-
if (!Array.isArray(node.children)) {
|
|
448
|
-
errors.push(`${path}: List must have children array`);
|
|
449
|
-
}
|
|
450
|
-
// Validate list items
|
|
451
|
-
node.children?.forEach((item, i) => {
|
|
452
|
-
if (item.type !== 'listitem') {
|
|
453
|
-
errors.push(`${path}.children[${i}]: List child must be listitem, got '${item.type}'`);
|
|
454
|
-
}
|
|
455
|
-
});
|
|
456
|
-
break;
|
|
457
|
-
|
|
458
|
-
case 'listitem':
|
|
459
|
-
if (!Array.isArray(node.children)) {
|
|
460
|
-
errors.push(`${path}: Listitem must have children array`);
|
|
461
|
-
}
|
|
462
|
-
break;
|
|
463
|
-
|
|
464
|
-
case 'text':
|
|
465
|
-
if (typeof node.text !== 'string') {
|
|
466
|
-
errors.push(`${path}: Text node must have string text property`);
|
|
467
|
-
}
|
|
468
|
-
if (typeof node.format !== 'number') {
|
|
469
|
-
errors.push(`${path}: Text node format must be number, got ${typeof node.format}`);
|
|
470
|
-
}
|
|
471
|
-
// Validate format is valid bitwise combination
|
|
472
|
-
if (node.format !== 0 && options.strictFormat) {
|
|
473
|
-
const validFormats = [1, 2, 3, 4, 8, 16, 32, 64, 128];
|
|
474
|
-
const isValidCombination = (format) => {
|
|
475
|
-
// Check if format is valid bitwise combination of base values
|
|
476
|
-
return format <= 255 && (format & ~255) === 0;
|
|
477
|
-
};
|
|
478
|
-
if (!isValidCombination(node.format)) {
|
|
479
|
-
warnings.push(`${path}: Unusual format value ${node.format}, expected bitwise combination`);
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
break;
|
|
483
|
-
|
|
484
|
-
case 'link':
|
|
485
|
-
if (typeof node.url !== 'string') {
|
|
486
|
-
errors.push(`${path}: Link must have string url property`);
|
|
487
|
-
}
|
|
488
|
-
if (!Array.isArray(node.children)) {
|
|
489
|
-
errors.push(`${path}: Link must have children array`);
|
|
490
|
-
}
|
|
491
|
-
break;
|
|
492
|
-
|
|
493
|
-
case 'code':
|
|
494
|
-
if (!Array.isArray(node.children)) {
|
|
495
|
-
errors.push(`${path}: Code must have children array`);
|
|
496
|
-
}
|
|
497
|
-
break;
|
|
498
|
-
|
|
499
|
-
case 'quote':
|
|
500
|
-
if (!Array.isArray(node.children)) {
|
|
501
|
-
errors.push(`${path}: Quote must have children array`);
|
|
502
|
-
}
|
|
503
|
-
break;
|
|
504
|
-
|
|
505
|
-
case 'horizontalrule':
|
|
506
|
-
// HR nodes don't have children
|
|
507
|
-
break;
|
|
508
|
-
|
|
509
|
-
default:
|
|
510
|
-
if (options.strict) {
|
|
511
|
-
warnings.push(`${path}: Unknown node type '${node.type}'`);
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
// Recursively validate children
|
|
516
|
-
if (Array.isArray(node.children)) {
|
|
517
|
-
node.children.forEach((child, i) => {
|
|
518
|
-
validateNode(child, `${path}.children[${i}]`);
|
|
519
|
-
});
|
|
520
|
-
}
|
|
521
|
-
};
|
|
522
|
-
|
|
523
|
-
// Validate all root children
|
|
524
|
-
root.children.forEach((child, i) => {
|
|
525
|
-
validateNode(child, `root.children[${i}]`);
|
|
526
|
-
});
|
|
527
|
-
|
|
528
|
-
return {
|
|
529
|
-
valid: errors.length === 0,
|
|
530
|
-
errors,
|
|
531
|
-
warnings
|
|
532
|
-
};
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
/**
|
|
536
|
-
* Validate and optionally fix Lexical format codes
|
|
537
|
-
* Ensures format codes follow Lexical bitwise specification
|
|
538
|
-
* @param {number} format - The format code to validate
|
|
539
|
-
* @returns {Object} Validation result
|
|
540
|
-
*/
|
|
541
|
-
validateFormatCode(format) {
|
|
542
|
-
const VALID_FORMATS = {
|
|
543
|
-
BOLD: 1, // 1 << 0
|
|
544
|
-
ITALIC: 2, // 1 << 1
|
|
545
|
-
STRIKETHROUGH: 4, // 1 << 2
|
|
546
|
-
UNDERLINE: 8, // 1 << 3
|
|
547
|
-
CODE: 16, // 1 << 4
|
|
548
|
-
SUBSCRIPT: 32, // 1 << 5
|
|
549
|
-
SUPERSCRIPT: 64, // 1 << 6
|
|
550
|
-
HIGHLIGHT: 128 // 1 << 7
|
|
551
|
-
};
|
|
552
|
-
|
|
553
|
-
if (typeof format !== 'number') {
|
|
554
|
-
return {
|
|
555
|
-
valid: false,
|
|
556
|
-
error: `Format must be number, got ${typeof format}`
|
|
557
|
-
};
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
if (format < 0 || format > 255) {
|
|
561
|
-
return {
|
|
562
|
-
valid: false,
|
|
563
|
-
error: `Format must be between 0 and 255, got ${format}`
|
|
564
|
-
};
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
// Decompose format into individual flags
|
|
568
|
-
const flags = [];
|
|
569
|
-
Object.entries(VALID_FORMATS).forEach(([name, value]) => {
|
|
570
|
-
if ((format & value) === value) {
|
|
571
|
-
flags.push(name);
|
|
572
|
-
}
|
|
573
|
-
});
|
|
574
|
-
|
|
575
|
-
return {
|
|
576
|
-
valid: true,
|
|
577
|
-
format,
|
|
578
|
-
flags,
|
|
579
|
-
description: flags.length > 0 ? flags.join(' + ') : 'Plain text'
|
|
580
|
-
};
|
|
214
|
+
const editor = createHeadlessEditor({ nodes: LEXICAL_NODES });
|
|
215
|
+
editor.update(
|
|
216
|
+
() => {
|
|
217
|
+
$convertFromMarkdownString(markdown, LEXICAL_TRANSFORMERS);
|
|
218
|
+
},
|
|
219
|
+
{ discrete: true },
|
|
220
|
+
);
|
|
221
|
+
return editor.getEditorState().toJSON();
|
|
581
222
|
}
|
|
582
223
|
|
|
583
224
|
/**
|
|
584
225
|
* List all collections
|
|
585
226
|
*/
|
|
586
227
|
async listCollections() {
|
|
587
|
-
return await this.request(
|
|
228
|
+
return await this.request("/api");
|
|
588
229
|
}
|
|
589
230
|
|
|
590
231
|
/**
|
|
@@ -592,11 +233,11 @@ export class PayloadCMSUtils {
|
|
|
592
233
|
*/
|
|
593
234
|
async getDocuments(collection, options = {}) {
|
|
594
235
|
const params = new URLSearchParams();
|
|
595
|
-
if (options.limit) params.append(
|
|
596
|
-
if (options.page) params.append(
|
|
597
|
-
if (options.where) params.append(
|
|
236
|
+
if (options.limit) params.append("limit", options.limit);
|
|
237
|
+
if (options.page) params.append("page", options.page);
|
|
238
|
+
if (options.where) params.append("where", JSON.stringify(options.where));
|
|
598
239
|
|
|
599
|
-
const query = params.toString() ? `?${params.toString()}` :
|
|
240
|
+
const query = params.toString() ? `?${params.toString()}` : "";
|
|
600
241
|
return await this.request(`/api/${collection}${query}`);
|
|
601
242
|
}
|
|
602
243
|
|
|
@@ -612,8 +253,8 @@ export class PayloadCMSUtils {
|
|
|
612
253
|
*/
|
|
613
254
|
async createDocument(collection, data) {
|
|
614
255
|
return await this.request(`/api/${collection}`, {
|
|
615
|
-
method:
|
|
616
|
-
body: JSON.stringify(data)
|
|
256
|
+
method: "POST",
|
|
257
|
+
body: JSON.stringify(data),
|
|
617
258
|
});
|
|
618
259
|
}
|
|
619
260
|
|
|
@@ -622,8 +263,8 @@ export class PayloadCMSUtils {
|
|
|
622
263
|
*/
|
|
623
264
|
async updateDocument(collection, id, data) {
|
|
624
265
|
return await this.request(`/api/${collection}/${id}`, {
|
|
625
|
-
method:
|
|
626
|
-
body: JSON.stringify(data)
|
|
266
|
+
method: "PATCH",
|
|
267
|
+
body: JSON.stringify(data),
|
|
627
268
|
});
|
|
628
269
|
}
|
|
629
270
|
|
|
@@ -632,38 +273,59 @@ export class PayloadCMSUtils {
|
|
|
632
273
|
*/
|
|
633
274
|
async deleteDocument(collection, id) {
|
|
634
275
|
return await this.request(`/api/${collection}/${id}`, {
|
|
635
|
-
method:
|
|
276
|
+
method: "DELETE",
|
|
636
277
|
});
|
|
637
278
|
}
|
|
638
279
|
|
|
639
280
|
/**
|
|
640
281
|
* Upload media file
|
|
282
|
+
* Uses Node.js native FormData + Blob (requires Node 20+)
|
|
641
283
|
*/
|
|
642
|
-
async uploadMedia(filePath, altText =
|
|
284
|
+
async uploadMedia(filePath, altText = "") {
|
|
643
285
|
if (!this.token) {
|
|
644
286
|
await this.authenticate();
|
|
645
287
|
}
|
|
646
288
|
|
|
647
|
-
const
|
|
648
|
-
const
|
|
289
|
+
const { readFileSync } = await import("fs");
|
|
290
|
+
const { basename, extname } = await import("path");
|
|
291
|
+
|
|
292
|
+
const MIME_TYPES = {
|
|
293
|
+
".png": "image/png",
|
|
294
|
+
".jpg": "image/jpeg",
|
|
295
|
+
".jpeg": "image/jpeg",
|
|
296
|
+
".gif": "image/gif",
|
|
297
|
+
".webp": "image/webp",
|
|
298
|
+
".svg": "image/svg+xml",
|
|
299
|
+
".avif": "image/avif",
|
|
300
|
+
".ico": "image/x-icon",
|
|
301
|
+
".bmp": "image/bmp",
|
|
302
|
+
".pdf": "application/pdf",
|
|
303
|
+
".mp4": "video/mp4",
|
|
304
|
+
".webm": "video/webm",
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const fileName = basename(filePath);
|
|
308
|
+
const mimeType =
|
|
309
|
+
MIME_TYPES[extname(filePath).toLowerCase()] || "application/octet-stream";
|
|
310
|
+
const fileBuffer = readFileSync(filePath);
|
|
311
|
+
const blob = new Blob([fileBuffer], { type: mimeType });
|
|
649
312
|
|
|
650
313
|
const formData = new FormData();
|
|
651
|
-
formData.append(
|
|
652
|
-
if (altText) formData.append(
|
|
314
|
+
formData.append("file", blob, fileName);
|
|
315
|
+
if (altText) formData.append("alt", altText);
|
|
653
316
|
|
|
654
317
|
try {
|
|
655
318
|
const response = await fetch(`${this.url}/api/media`, {
|
|
656
|
-
method:
|
|
319
|
+
method: "POST",
|
|
657
320
|
headers: {
|
|
658
|
-
|
|
659
|
-
...formData.getHeaders()
|
|
321
|
+
Authorization: `JWT ${this.token}`,
|
|
660
322
|
},
|
|
661
|
-
body: formData
|
|
323
|
+
body: formData,
|
|
662
324
|
});
|
|
663
325
|
|
|
664
326
|
if (!response.ok) {
|
|
665
327
|
const error = await response.text();
|
|
666
|
-
throw new Error(`Upload failed: ${error}`);
|
|
328
|
+
throw new Error(`Upload failed (${response.status}): ${error}`);
|
|
667
329
|
}
|
|
668
330
|
|
|
669
331
|
return await response.json();
|
|
@@ -672,26 +334,139 @@ export class PayloadCMSUtils {
|
|
|
672
334
|
}
|
|
673
335
|
}
|
|
674
336
|
|
|
337
|
+
/**
|
|
338
|
+
* Pre-process markdown for publishing: strip HTML wrappers, upload images,
|
|
339
|
+
* and return cleaned markdown + a map of upload placeholders to media IDs.
|
|
340
|
+
*/
|
|
341
|
+
async preprocessMarkdownForPublish(markdown, markdownFilePath, frontmatter = {}) {
|
|
342
|
+
const fs = await import("fs");
|
|
343
|
+
const uploads = []; // { placeholder, mediaId }
|
|
344
|
+
|
|
345
|
+
// Strip leading H1 if it duplicates the frontmatter title (title is rendered by the page template)
|
|
346
|
+
let cleaned = markdown.trimStart();
|
|
347
|
+
if (frontmatter.title) {
|
|
348
|
+
cleaned = cleaned.replace(/^#\s+.+\n+/, '');
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Strip <div class="infographic" ...> and </div> wrapper lines
|
|
352
|
+
cleaned = cleaned.replace(
|
|
353
|
+
/^<div\s+class="infographic"[^>]*>\s*$/gm,
|
|
354
|
+
"",
|
|
355
|
+
);
|
|
356
|
+
cleaned = cleaned.replace(/^<\/div>\s*$/gm, "");
|
|
357
|
+
|
|
358
|
+
// Collapse runs of 3+ consecutive newlines into exactly 2 (one blank line)
|
|
359
|
+
// This prevents div stripping from leaving excessive blank lines
|
|
360
|
+
cleaned = cleaned.replace(/\n{3,}/g, "\n\n");
|
|
361
|
+
|
|
362
|
+
// Find all markdown images: 
|
|
363
|
+
const imageRegex = /^!\[([^\]]*)\]\(([^)]+)\)\s*$/gm;
|
|
364
|
+
const images = [];
|
|
365
|
+
let match;
|
|
366
|
+
while ((match = imageRegex.exec(cleaned)) !== null) {
|
|
367
|
+
images.push({ full: match[0], alt: match[1], path: match[2] });
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Upload each image and replace with placeholder
|
|
371
|
+
const baseDir = markdownFilePath
|
|
372
|
+
? dirname(markdownFilePath)
|
|
373
|
+
: process.cwd();
|
|
374
|
+
for (const img of images) {
|
|
375
|
+
const filePath = resolve(baseDir, img.path);
|
|
376
|
+
if (!fs.existsSync(filePath)) continue;
|
|
377
|
+
|
|
378
|
+
try {
|
|
379
|
+
const result = await this.uploadMedia(filePath, img.alt);
|
|
380
|
+
const mediaId = result.doc?.id || result.id;
|
|
381
|
+
if (mediaId) {
|
|
382
|
+
const placeholder = `%%UPLOAD:${mediaId}%%`;
|
|
383
|
+
cleaned = cleaned.replace(img.full, placeholder);
|
|
384
|
+
uploads.push({ placeholder, mediaId });
|
|
385
|
+
}
|
|
386
|
+
} catch (err) {
|
|
387
|
+
console.error(`Warning: Failed to upload ${img.path}: ${err.message}`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return { markdown: cleaned, uploads };
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Post-process Lexical JSON to replace upload placeholders with upload nodes.
|
|
396
|
+
* Handles cases where Lexical merges the placeholder with adjacent inline
|
|
397
|
+
* content (e.g. an italic caption on the next line) into a single paragraph.
|
|
398
|
+
*/
|
|
399
|
+
injectUploadNodes(lexicalJson, uploads) {
|
|
400
|
+
if (!uploads.length) return lexicalJson;
|
|
401
|
+
|
|
402
|
+
const placeholderMap = new Map(
|
|
403
|
+
uploads.map((u) => [u.placeholder, u.mediaId]),
|
|
404
|
+
);
|
|
405
|
+
const newChildren = [];
|
|
406
|
+
|
|
407
|
+
for (const node of lexicalJson.root.children) {
|
|
408
|
+
if (
|
|
409
|
+
node.type === "paragraph" &&
|
|
410
|
+
node.children?.length >= 1 &&
|
|
411
|
+
node.children[0].type === "text"
|
|
412
|
+
) {
|
|
413
|
+
const text = node.children[0].text.trim();
|
|
414
|
+
const mediaId = placeholderMap.get(text);
|
|
415
|
+
if (mediaId) {
|
|
416
|
+
// Emit the upload node
|
|
417
|
+
newChildren.push({
|
|
418
|
+
type: "upload",
|
|
419
|
+
version: 3,
|
|
420
|
+
format: "",
|
|
421
|
+
relationTo: "media",
|
|
422
|
+
value: mediaId,
|
|
423
|
+
id: crypto.randomUUID().replace(/-/g, "").slice(0, 24),
|
|
424
|
+
fields: {},
|
|
425
|
+
});
|
|
426
|
+
// If the paragraph had additional children (e.g. merged italic caption),
|
|
427
|
+
// emit them as a separate paragraph
|
|
428
|
+
if (node.children.length > 1) {
|
|
429
|
+
newChildren.push({ ...node, children: node.children.slice(1) });
|
|
430
|
+
}
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
newChildren.push(node);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return {
|
|
438
|
+
...lexicalJson,
|
|
439
|
+
root: { ...lexicalJson.root, children: newChildren },
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
675
443
|
/**
|
|
676
444
|
* Publish markdown content to PayloadCMS
|
|
677
445
|
*/
|
|
678
446
|
async publishContent(markdownFile, collection, options = {}) {
|
|
679
|
-
const fs = await import(
|
|
680
|
-
const grayMatter = (await import(
|
|
447
|
+
const fs = await import("fs");
|
|
448
|
+
const grayMatter = (await import("gray-matter")).default;
|
|
681
449
|
|
|
682
450
|
// Read and parse markdown file
|
|
683
|
-
const fileContent = fs.readFileSync(markdownFile,
|
|
451
|
+
const fileContent = fs.readFileSync(markdownFile, "utf8");
|
|
684
452
|
const { data: frontmatter, content } = grayMatter(fileContent);
|
|
685
453
|
|
|
454
|
+
// Pre-process: strip HTML wrappers, duplicate H1 title, upload images
|
|
455
|
+
const { markdown: cleanedContent, uploads } =
|
|
456
|
+
await this.preprocessMarkdownForPublish(content, markdownFile, frontmatter);
|
|
457
|
+
|
|
686
458
|
// Convert markdown to Lexical format
|
|
687
|
-
|
|
459
|
+
let richText = this.convertMarkdownToLexical(cleanedContent);
|
|
460
|
+
|
|
461
|
+
// Inject upload nodes where placeholders exist
|
|
462
|
+
richText = this.injectUploadNodes(richText, uploads);
|
|
688
463
|
|
|
689
464
|
// Prepare document data
|
|
690
465
|
const documentData = {
|
|
691
466
|
...frontmatter,
|
|
692
467
|
content: richText,
|
|
693
|
-
status: options.status || frontmatter.status ||
|
|
694
|
-
...options.additionalFields
|
|
468
|
+
status: options.status || frontmatter.status || "draft",
|
|
469
|
+
...options.additionalFields,
|
|
695
470
|
};
|
|
696
471
|
|
|
697
472
|
// Create or update document
|