@se-studio/markdown-renderer 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +42 -0
- package/dist/index.js +333 -0
- package/dist/index.js.map +1 -0
- package/package.json +43 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { ContentfulConfig } from '@se-studio/contentful-rest-api';
|
|
2
|
+
import { IBasePage, IBaseArticle, IBaseCustomType, IContentContext } from '@se-studio/core-data-types';
|
|
3
|
+
|
|
4
|
+
interface ContentData {
|
|
5
|
+
contentType: 'page' | 'article' | 'customType';
|
|
6
|
+
data: IBasePage | IBaseArticle | IBaseCustomType;
|
|
7
|
+
context: IContentContext;
|
|
8
|
+
}
|
|
9
|
+
interface MarkdownConverterContext {
|
|
10
|
+
contentContext: IContentContext;
|
|
11
|
+
config: ContentfulConfig;
|
|
12
|
+
}
|
|
13
|
+
interface ConverterResult {
|
|
14
|
+
markdown: string;
|
|
15
|
+
}
|
|
16
|
+
type ConverterFunction<T = any> = (data: T, context: MarkdownConverterContext) => Promise<string> | string;
|
|
17
|
+
|
|
18
|
+
declare class MarkdownConverter {
|
|
19
|
+
convert(contentData: ContentData, context: MarkdownConverterContext): Promise<string>;
|
|
20
|
+
private generateFrontmatter;
|
|
21
|
+
private convertModule;
|
|
22
|
+
private convertGenericComponent;
|
|
23
|
+
private convertGenericCollection;
|
|
24
|
+
private renderPreHeading;
|
|
25
|
+
private renderPostHeading;
|
|
26
|
+
private renderBody;
|
|
27
|
+
private renderAdditionalCopy;
|
|
28
|
+
private renderVisual;
|
|
29
|
+
private renderLinks;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
declare class MarkdownExporter {
|
|
33
|
+
private config;
|
|
34
|
+
private preview;
|
|
35
|
+
constructor(config: ContentfulConfig, preview?: boolean);
|
|
36
|
+
fetchContent(type: 'page' | 'article' | 'blogPost' | 'customType', slug: string, params?: {
|
|
37
|
+
articleType?: string;
|
|
38
|
+
}): Promise<ContentData | null>;
|
|
39
|
+
private createContext;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export { type ContentData, type ConverterFunction, type ConverterResult, MarkdownConverter, type MarkdownConverterContext, MarkdownExporter };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
// src/MarkdownConverter.ts
|
|
2
|
+
import yaml from "js-yaml";
|
|
3
|
+
|
|
4
|
+
// src/utils/richTextConverter.ts
|
|
5
|
+
import {
|
|
6
|
+
BLOCKS,
|
|
7
|
+
INLINES,
|
|
8
|
+
MARKS
|
|
9
|
+
} from "@contentful/rich-text-types";
|
|
10
|
+
function convertRichTextToMarkdown(richText, context) {
|
|
11
|
+
if (!richText) return "";
|
|
12
|
+
const document = "json" in richText ? richText.json : richText;
|
|
13
|
+
if (!document) return "";
|
|
14
|
+
return convertNode(document, context);
|
|
15
|
+
}
|
|
16
|
+
function convertNode(node, context) {
|
|
17
|
+
if (node.nodeType === "text") {
|
|
18
|
+
return convertText(node);
|
|
19
|
+
}
|
|
20
|
+
const content = (node.content || []).map((child) => convertNode(child, context)).join("");
|
|
21
|
+
switch (node.nodeType) {
|
|
22
|
+
case BLOCKS.DOCUMENT:
|
|
23
|
+
return content;
|
|
24
|
+
case BLOCKS.PARAGRAPH:
|
|
25
|
+
return `${content}
|
|
26
|
+
|
|
27
|
+
`;
|
|
28
|
+
case BLOCKS.HEADING_1:
|
|
29
|
+
return `# ${content}
|
|
30
|
+
|
|
31
|
+
`;
|
|
32
|
+
case BLOCKS.HEADING_2:
|
|
33
|
+
return `## ${content}
|
|
34
|
+
|
|
35
|
+
`;
|
|
36
|
+
case BLOCKS.HEADING_3:
|
|
37
|
+
return `### ${content}
|
|
38
|
+
|
|
39
|
+
`;
|
|
40
|
+
case BLOCKS.HEADING_4:
|
|
41
|
+
return `#### ${content}
|
|
42
|
+
|
|
43
|
+
`;
|
|
44
|
+
case BLOCKS.HEADING_5:
|
|
45
|
+
return `##### ${content}
|
|
46
|
+
|
|
47
|
+
`;
|
|
48
|
+
case BLOCKS.HEADING_6:
|
|
49
|
+
return `###### ${content}
|
|
50
|
+
|
|
51
|
+
`;
|
|
52
|
+
case BLOCKS.UL_LIST:
|
|
53
|
+
return `${content}
|
|
54
|
+
`;
|
|
55
|
+
case BLOCKS.OL_LIST:
|
|
56
|
+
return `${content}
|
|
57
|
+
`;
|
|
58
|
+
case BLOCKS.LIST_ITEM:
|
|
59
|
+
return `- ${content.trim()}
|
|
60
|
+
`;
|
|
61
|
+
case BLOCKS.QUOTE:
|
|
62
|
+
return `> ${content.replace(/\n/g, "\n> ")}
|
|
63
|
+
|
|
64
|
+
`;
|
|
65
|
+
case BLOCKS.HR:
|
|
66
|
+
return `---
|
|
67
|
+
|
|
68
|
+
`;
|
|
69
|
+
case INLINES.HYPERLINK:
|
|
70
|
+
return `[${content}](${node.data.uri})`;
|
|
71
|
+
case INLINES.ENTRY_HYPERLINK:
|
|
72
|
+
return `[${content}](#${node.data.target?.sys?.id || "entry-link"})`;
|
|
73
|
+
case INLINES.ASSET_HYPERLINK:
|
|
74
|
+
return `[${content}](${node.data.target?.fields?.file?.url || "#"})`;
|
|
75
|
+
case BLOCKS.EMBEDDED_ASSET: {
|
|
76
|
+
const title = node.data.target?.fields?.title || "Image";
|
|
77
|
+
const url = node.data.target?.fields?.file?.url;
|
|
78
|
+
return url ? `
|
|
79
|
+
|
|
80
|
+
` : "";
|
|
81
|
+
}
|
|
82
|
+
case BLOCKS.EMBEDDED_ENTRY:
|
|
83
|
+
return `
|
|
84
|
+
|
|
85
|
+
[Embedded Entry: ${node.data.target?.sys?.id}]
|
|
86
|
+
|
|
87
|
+
`;
|
|
88
|
+
default:
|
|
89
|
+
return content;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function convertText(node) {
|
|
93
|
+
let value = node.value;
|
|
94
|
+
if (node.marks) {
|
|
95
|
+
node.marks.forEach((mark) => {
|
|
96
|
+
switch (mark.type) {
|
|
97
|
+
case MARKS.BOLD:
|
|
98
|
+
value = `**${value}**`;
|
|
99
|
+
break;
|
|
100
|
+
case MARKS.ITALIC:
|
|
101
|
+
value = `*${value}*`;
|
|
102
|
+
break;
|
|
103
|
+
case MARKS.CODE:
|
|
104
|
+
value = `\`${value}\``;
|
|
105
|
+
break;
|
|
106
|
+
case MARKS.UNDERLINE:
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return value;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// src/MarkdownConverter.ts
|
|
115
|
+
var MarkdownConverter = class {
|
|
116
|
+
async convert(contentData, context) {
|
|
117
|
+
const { data, contentType } = contentData;
|
|
118
|
+
const frontmatter = this.generateFrontmatter(data, contentType);
|
|
119
|
+
let markdown = `---
|
|
120
|
+
${yaml.dump(frontmatter)}---
|
|
121
|
+
|
|
122
|
+
`;
|
|
123
|
+
if (data.title) {
|
|
124
|
+
markdown += `# ${data.title}
|
|
125
|
+
|
|
126
|
+
`;
|
|
127
|
+
}
|
|
128
|
+
if (data.description) {
|
|
129
|
+
markdown += `> ${data.description}
|
|
130
|
+
|
|
131
|
+
`;
|
|
132
|
+
}
|
|
133
|
+
if (data.contents) {
|
|
134
|
+
for (const item of data.contents) {
|
|
135
|
+
if (!item) continue;
|
|
136
|
+
markdown += await this.convertModule(item, context);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return markdown;
|
|
140
|
+
}
|
|
141
|
+
generateFrontmatter(data, type) {
|
|
142
|
+
const slug = data.slug;
|
|
143
|
+
return {
|
|
144
|
+
contentType: type,
|
|
145
|
+
title: data.title,
|
|
146
|
+
slug,
|
|
147
|
+
id: data.id,
|
|
148
|
+
description: data.description,
|
|
149
|
+
// Add more specific fields for Article if needed (date, author, etc)
|
|
150
|
+
...type === "article" ? {
|
|
151
|
+
date: data.date,
|
|
152
|
+
articleType: data.articleType?.name
|
|
153
|
+
} : {}
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
async convertModule(item, context) {
|
|
157
|
+
if (item.type === "Component") {
|
|
158
|
+
const component = item;
|
|
159
|
+
return this.convertGenericComponent(component, context);
|
|
160
|
+
}
|
|
161
|
+
if (item.type === "Collection") {
|
|
162
|
+
const collection = item;
|
|
163
|
+
return this.convertGenericCollection(collection, context);
|
|
164
|
+
}
|
|
165
|
+
return "";
|
|
166
|
+
}
|
|
167
|
+
convertGenericComponent(component, context) {
|
|
168
|
+
let md = "";
|
|
169
|
+
if (component.heading) {
|
|
170
|
+
md += `## ${component.heading}
|
|
171
|
+
|
|
172
|
+
`;
|
|
173
|
+
} else if (component.componentType) {
|
|
174
|
+
md += `## ${component.componentType}
|
|
175
|
+
|
|
176
|
+
`;
|
|
177
|
+
}
|
|
178
|
+
md += this.renderPreHeading(component.preHeading);
|
|
179
|
+
md += this.renderPostHeading(component.postHeading);
|
|
180
|
+
md += this.renderBody(component.body, context);
|
|
181
|
+
md += this.renderAdditionalCopy(component.additionalCopy, context);
|
|
182
|
+
md += this.renderVisual(component.visual);
|
|
183
|
+
md += this.renderLinks(component.links);
|
|
184
|
+
return md;
|
|
185
|
+
}
|
|
186
|
+
async convertGenericCollection(collection, context) {
|
|
187
|
+
let md = `## ${collection.heading || collection.collectionType || "Collection"}
|
|
188
|
+
|
|
189
|
+
`;
|
|
190
|
+
md += this.renderPreHeading(collection.preHeading);
|
|
191
|
+
md += this.renderPostHeading(collection.postHeading);
|
|
192
|
+
md += this.renderBody(collection.body, context);
|
|
193
|
+
md += this.renderLinks(collection.links);
|
|
194
|
+
if (collection.contents) {
|
|
195
|
+
for (const item of collection.contents) {
|
|
196
|
+
if (!item) continue;
|
|
197
|
+
md += await this.convertModule(item, context);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
md += this.renderAdditionalCopy(collection.additionalCopy, context);
|
|
201
|
+
md += this.renderVisual(collection.visual);
|
|
202
|
+
return md;
|
|
203
|
+
}
|
|
204
|
+
renderPreHeading(preHeading) {
|
|
205
|
+
return preHeading ? `**${preHeading}**
|
|
206
|
+
|
|
207
|
+
` : "";
|
|
208
|
+
}
|
|
209
|
+
renderPostHeading(postHeading) {
|
|
210
|
+
return postHeading ? `*${postHeading}*
|
|
211
|
+
|
|
212
|
+
` : "";
|
|
213
|
+
}
|
|
214
|
+
// biome-ignore lint/suspicious/noExplicitAny: generic content body
|
|
215
|
+
renderBody(body, context) {
|
|
216
|
+
return body ? convertRichTextToMarkdown(body, context) : "";
|
|
217
|
+
}
|
|
218
|
+
// biome-ignore lint/suspicious/noExplicitAny: generic additional copy
|
|
219
|
+
renderAdditionalCopy(additionalCopy, context) {
|
|
220
|
+
return additionalCopy ? `
|
|
221
|
+
|
|
222
|
+
${convertRichTextToMarkdown(additionalCopy, context)}` : "";
|
|
223
|
+
}
|
|
224
|
+
renderVisual(visual) {
|
|
225
|
+
const image = visual?.visual?.image;
|
|
226
|
+
if (!image) return "";
|
|
227
|
+
const title = image.name || "Image";
|
|
228
|
+
const url = image.src || image.svgSrc;
|
|
229
|
+
return url ? `
|
|
230
|
+
|
|
231
|
+
` : "";
|
|
232
|
+
}
|
|
233
|
+
renderLinks(links) {
|
|
234
|
+
if (!links?.length) return "";
|
|
235
|
+
let md = "\n";
|
|
236
|
+
for (const link of links) {
|
|
237
|
+
md += `- [${link.text || link.name}](${link.href || "#"})
|
|
238
|
+
`;
|
|
239
|
+
}
|
|
240
|
+
return `${md}
|
|
241
|
+
`;
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// src/MarkdownExporter.ts
|
|
246
|
+
import {
|
|
247
|
+
contentfulArticleRest,
|
|
248
|
+
contentfulCustomTypeRest,
|
|
249
|
+
contentfulPageRest,
|
|
250
|
+
createBaseConverterContext
|
|
251
|
+
} from "@se-studio/contentful-rest-api";
|
|
252
|
+
var MarkdownExporter = class {
|
|
253
|
+
config;
|
|
254
|
+
preview;
|
|
255
|
+
constructor(config, preview = false) {
|
|
256
|
+
this.config = config;
|
|
257
|
+
this.preview = preview;
|
|
258
|
+
}
|
|
259
|
+
async fetchContent(type, slug, params) {
|
|
260
|
+
const context = this.createContext();
|
|
261
|
+
if (type === "page") {
|
|
262
|
+
const response = await contentfulPageRest(context, this.config, slug, {
|
|
263
|
+
preview: this.preview
|
|
264
|
+
});
|
|
265
|
+
if (!response.data) return null;
|
|
266
|
+
const page = response.data;
|
|
267
|
+
return {
|
|
268
|
+
contentType: "page",
|
|
269
|
+
data: page,
|
|
270
|
+
context: {
|
|
271
|
+
pageContext: {
|
|
272
|
+
page
|
|
273
|
+
},
|
|
274
|
+
current: { type: "Page", id: page.id }
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
if (type === "article" || type === "blogPost") {
|
|
279
|
+
const articleType = params?.articleType || "blog";
|
|
280
|
+
const response = await contentfulArticleRest(context, this.config, slug, articleType, {
|
|
281
|
+
preview: this.preview
|
|
282
|
+
});
|
|
283
|
+
if (!response.data) return null;
|
|
284
|
+
const article = response.data;
|
|
285
|
+
return {
|
|
286
|
+
contentType: "article",
|
|
287
|
+
data: article,
|
|
288
|
+
context: {
|
|
289
|
+
pageContext: {
|
|
290
|
+
article,
|
|
291
|
+
articleType: article.articleType
|
|
292
|
+
},
|
|
293
|
+
current: { type: "Article", id: article.id }
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
if (type === "customType") {
|
|
298
|
+
const response = await contentfulCustomTypeRest(context, this.config, slug, {
|
|
299
|
+
preview: this.preview
|
|
300
|
+
});
|
|
301
|
+
if (!response.data) return null;
|
|
302
|
+
const customType = response.data;
|
|
303
|
+
return {
|
|
304
|
+
contentType: "customType",
|
|
305
|
+
data: customType,
|
|
306
|
+
context: {
|
|
307
|
+
pageContext: {
|
|
308
|
+
customType
|
|
309
|
+
},
|
|
310
|
+
current: { type: "CustomType", id: customType.id }
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
createContext() {
|
|
317
|
+
const urlCalculators = {
|
|
318
|
+
page: (slug) => `/${slug}`,
|
|
319
|
+
pageVariant: (slug) => `/${slug}`,
|
|
320
|
+
article: (type, slug) => `/${type}/${slug}`,
|
|
321
|
+
articleType: (slug) => `/${slug}`,
|
|
322
|
+
tag: (slug) => `/topics/${slug}`,
|
|
323
|
+
person: (slug) => `/team/${slug}`,
|
|
324
|
+
customType: (slug) => `/${slug}`
|
|
325
|
+
};
|
|
326
|
+
return createBaseConverterContext(urlCalculators);
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
export {
|
|
330
|
+
MarkdownConverter,
|
|
331
|
+
MarkdownExporter
|
|
332
|
+
};
|
|
333
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/MarkdownConverter.ts","../src/utils/richTextConverter.ts","../src/MarkdownExporter.ts"],"sourcesContent":["import type { IContentfulCollection, IContentfulComponent } from '@se-studio/contentful-rest-api';\nimport type {\n IBaseArticle,\n IBaseCustomType,\n IBasePage,\n IPicture,\n IResponsiveVisual,\n ISvgImage,\n ITyped,\n} from '@se-studio/core-data-types';\nimport yaml from 'js-yaml';\nimport type { ContentData, MarkdownConverterContext } from './types';\nimport { convertRichTextToMarkdown } from './utils/richTextConverter';\n\nexport class MarkdownConverter {\n async convert(contentData: ContentData, context: MarkdownConverterContext): Promise<string> {\n const { data, contentType } = contentData;\n const frontmatter = this.generateFrontmatter(data, contentType);\n\n let markdown = `---\\n${yaml.dump(frontmatter)}---\\n\\n`;\n\n // Add Title and basic body\n if (data.title) {\n markdown += `# ${data.title}\\n\\n`;\n }\n\n if (data.description) {\n markdown += `> ${data.description}\\n\\n`;\n }\n\n // Page modules / contents\n if (data.contents) {\n for (const item of data.contents) {\n if (!item) continue;\n markdown += await this.convertModule(item, context);\n }\n }\n\n return markdown;\n }\n\n private generateFrontmatter(data: IBasePage | IBaseArticle | IBaseCustomType, type: string) {\n const slug = data.slug;\n return {\n contentType: type,\n title: data.title,\n slug: slug,\n id: data.id,\n description: data.description,\n // Add more specific fields for Article if needed (date, author, etc)\n ...(type === 'article'\n ? {\n date: (data as IBaseArticle).date,\n articleType: (data as IBaseArticle).articleType?.name,\n }\n : {}),\n };\n }\n\n private async convertModule(item: ITyped, context: MarkdownConverterContext): Promise<string> {\n if (item.type === 'Component') {\n const component = item as IContentfulComponent & { componentType: string };\n // Use generic fallback for all components\n return this.convertGenericComponent(component, context);\n }\n\n if (item.type === 'Collection') {\n const collection = item as IContentfulCollection & { collectionType: string };\n // Use generic fallback for all collections\n return this.convertGenericCollection(collection, context);\n }\n\n return '';\n }\n\n private convertGenericComponent(\n component: IContentfulComponent & { componentType: string },\n context: MarkdownConverterContext,\n ): string {\n let md = '';\n\n // Add heading hierarchy\n if (component.heading) {\n md += `## ${component.heading}\\n\\n`;\n } else if (component.componentType) {\n // Fallback to component type if no heading, but NOT cmsLabel\n md += `## ${component.componentType}\\n\\n`;\n }\n\n md += this.renderPreHeading(component.preHeading);\n md += this.renderPostHeading(component.postHeading);\n md += this.renderBody(component.body, context);\n md += this.renderAdditionalCopy(component.additionalCopy, context);\n md += this.renderVisual(component.visual);\n md += this.renderLinks(component.links);\n\n return md;\n }\n\n private async convertGenericCollection(\n collection: IContentfulCollection & { collectionType: string },\n context: MarkdownConverterContext,\n ): Promise<string> {\n let md = `## ${collection.heading || collection.collectionType || 'Collection'}\\n\\n`;\n\n md += this.renderPreHeading(collection.preHeading);\n md += this.renderPostHeading(collection.postHeading);\n md += this.renderBody(collection.body, context);\n md += this.renderLinks(collection.links);\n\n if (collection.contents) {\n for (const item of collection.contents) {\n if (!item) continue;\n md += await this.convertModule(item, context);\n }\n }\n\n md += this.renderAdditionalCopy(collection.additionalCopy, context);\n md += this.renderVisual(collection.visual);\n\n return md;\n }\n\n private renderPreHeading(preHeading: string | null | undefined): string {\n return preHeading ? `**${preHeading}**\\n\\n` : '';\n }\n\n private renderPostHeading(postHeading: string | null | undefined): string {\n return postHeading ? `*${postHeading}*\\n\\n` : '';\n }\n\n // biome-ignore lint/suspicious/noExplicitAny: generic content body\n private renderBody(body: any, context: MarkdownConverterContext): string {\n return body ? convertRichTextToMarkdown(body, context) : '';\n }\n\n // biome-ignore lint/suspicious/noExplicitAny: generic additional copy\n private renderAdditionalCopy(additionalCopy: any, context: MarkdownConverterContext): string {\n return additionalCopy ? `\\n\\n${convertRichTextToMarkdown(additionalCopy, context)}` : '';\n }\n\n private renderVisual(visual: IResponsiveVisual | undefined): string {\n const image = visual?.visual?.image;\n if (!image) return '';\n const title = image.name || 'Image';\n const url = (image as IPicture).src || (image as ISvgImage).svgSrc;\n return url ? `\\n\\n` : '';\n }\n\n private renderLinks(\n links:\n | readonly { text?: string | null; name?: string | null; href?: string | null }[]\n | undefined,\n ): string {\n if (!links?.length) return '';\n let md = '\\n';\n for (const link of links) {\n md += `- [${link.text || link.name}](${link.href || '#'}) \\n`;\n }\n return `${md}\\n`;\n }\n}\n","import {\n BLOCKS,\n type Block,\n type Document,\n INLINES,\n type Inline,\n MARKS,\n type Text,\n} from '@contentful/rich-text-types';\nimport type { MarkdownConverterContext } from '../types';\n\nexport function convertRichTextToMarkdown(\n richText: Document | { json: Document } | null | undefined,\n context: MarkdownConverterContext,\n): string {\n if (!richText) return '';\n\n // Handle IContentfulRichText wrapper\n const document = 'json' in richText ? richText.json : richText;\n if (!document) return '';\n\n return convertNode(document, context);\n}\n\nfunction convertNode(\n node: Block | Inline | Text | Document,\n context: MarkdownConverterContext,\n): string {\n if (node.nodeType === 'text') {\n return convertText(node as Text);\n }\n\n const content = ((node as Block | Inline | Document).content || [])\n .map((child) => convertNode(child, context))\n .join('');\n\n switch (node.nodeType) {\n case BLOCKS.DOCUMENT:\n return content;\n case BLOCKS.PARAGRAPH:\n return `${content}\\n\\n`;\n case BLOCKS.HEADING_1:\n return `# ${content}\\n\\n`;\n case BLOCKS.HEADING_2:\n return `## ${content}\\n\\n`;\n case BLOCKS.HEADING_3:\n return `### ${content}\\n\\n`;\n case BLOCKS.HEADING_4:\n return `#### ${content}\\n\\n`;\n case BLOCKS.HEADING_5:\n return `##### ${content}\\n\\n`;\n case BLOCKS.HEADING_6:\n return `###### ${content}\\n\\n`;\n case BLOCKS.UL_LIST:\n return `${content}\\n`;\n case BLOCKS.OL_LIST:\n return `${content}\\n`;\n case BLOCKS.LIST_ITEM:\n // This is a simplification. Proper nested list handling requires tracking depth.\n // For now, assuming direct child of list.\n // The parent UL/OL doesn't output anything, just its children.\n // But we need to know if it's UL or OL to prefix with - or 1.\n // Since we don't have parent context here easily without passing state,\n // we might need to adjust the structure or accept standard bullet for now.\n return `- ${content.trim()}\\n`;\n case BLOCKS.QUOTE:\n return `> ${content.replace(/\\n/g, '\\n> ')}\\n\\n`;\n case BLOCKS.HR:\n return `---\\n\\n`;\n case INLINES.HYPERLINK:\n return `[${content}](${node.data.uri})`;\n case INLINES.ENTRY_HYPERLINK:\n // TODO: Resolve entry link using context if possible\n // For now, falling back to a placeholder or just the content\n return `[${content}](#${node.data.target?.sys?.id || 'entry-link'})`;\n case INLINES.ASSET_HYPERLINK:\n return `[${content}](${node.data.target?.fields?.file?.url || '#'})`;\n case BLOCKS.EMBEDDED_ASSET: {\n const title = node.data.target?.fields?.title || 'Image';\n const url = node.data.target?.fields?.file?.url;\n return url ? `\\n\\n` : '';\n }\n case BLOCKS.EMBEDDED_ENTRY:\n return `\\n\\n[Embedded Entry: ${node.data.target?.sys?.id}]\\n\\n`;\n default:\n return content;\n }\n}\n\nfunction convertText(node: Text): string {\n let value = node.value;\n\n if (node.marks) {\n node.marks.forEach((mark) => {\n switch (mark.type) {\n case MARKS.BOLD:\n value = `**${value}**`;\n break;\n case MARKS.ITALIC:\n value = `*${value}*`;\n break;\n case MARKS.CODE:\n value = `\\`${value}\\``;\n break;\n case MARKS.UNDERLINE:\n // Markdown doesn't support underline natively, usually ignored or HTML\n break;\n }\n });\n }\n return value;\n}\n","import {\n type ContentfulConfig,\n contentfulArticleRest,\n contentfulCustomTypeRest,\n contentfulPageRest,\n createBaseConverterContext,\n type UrlCalculators,\n} from '@se-studio/contentful-rest-api';\nimport type { IContentContext } from '@se-studio/core-data-types';\nimport type { ContentData } from './types';\n\nexport class MarkdownExporter {\n private config: ContentfulConfig;\n private preview: boolean;\n\n constructor(config: ContentfulConfig, preview = false) {\n this.config = config;\n this.preview = preview;\n }\n\n async fetchContent(\n type: 'page' | 'article' | 'blogPost' | 'customType',\n slug: string,\n params?: { articleType?: string },\n ): Promise<ContentData | null> {\n const context = this.createContext();\n\n if (type === 'page') {\n const response = await contentfulPageRest(context, this.config, slug, {\n preview: this.preview,\n });\n\n if (!response.data) return null;\n\n const page = response.data;\n return {\n contentType: 'page',\n data: page,\n context: {\n pageContext: {\n page,\n },\n current: { type: 'Page', id: page.id },\n } as IContentContext,\n };\n }\n\n if (type === 'article' || type === 'blogPost') {\n const articleType = params?.articleType || 'blog';\n const response = await contentfulArticleRest(context, this.config, slug, articleType, {\n preview: this.preview,\n });\n\n if (!response.data) return null;\n\n const article = response.data;\n return {\n contentType: 'article',\n data: article,\n context: {\n pageContext: {\n article,\n articleType: article.articleType,\n },\n current: { type: 'Article', id: article.id },\n } as IContentContext,\n };\n }\n\n if (type === 'customType') {\n const response = await contentfulCustomTypeRest(context, this.config, slug, {\n preview: this.preview,\n });\n\n if (!response.data) return null;\n\n const customType = response.data;\n return {\n contentType: 'customType',\n data: customType,\n context: {\n pageContext: {\n customType,\n },\n current: { type: 'CustomType', id: customType.id },\n } as IContentContext,\n };\n }\n\n return null;\n }\n\n private createContext() {\n // Basic context for fetching - we might need to make this configurable if URL calculation differs per app\n // For now using simple defaults that produce relative paths\n const urlCalculators: UrlCalculators = {\n page: (slug) => `/${slug}`,\n pageVariant: (slug) => `/${slug}`,\n article: (type, slug) => `/${type}/${slug}`,\n articleType: (slug) => `/${slug}`,\n tag: (slug) => `/topics/${slug}`,\n person: (slug) => `/team/${slug}`,\n customType: (slug) => `/${slug}`,\n };\n\n return createBaseConverterContext(urlCalculators);\n }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@se-studio/markdown-renderer",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Markdown renderer for Contentful content",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/Something-Else-Studio/se-core-product",
|
|
8
|
+
"directory": "packages/markdown-renderer"
|
|
9
|
+
},
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"type": "module",
|
|
12
|
+
"main": "./dist/index.js",
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"import": "./dist/index.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist"
|
|
22
|
+
],
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@contentful/rich-text-types": "^17.2.5",
|
|
25
|
+
"js-yaml": "^4.1.0",
|
|
26
|
+
"@se-studio/contentful-rest-api": "1.0.37",
|
|
27
|
+
"@se-studio/core-data-types": "1.0.37"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@biomejs/biome": "^2.3.8",
|
|
31
|
+
"@types/js-yaml": "^4.0.9",
|
|
32
|
+
"@types/node": "^24.10.4",
|
|
33
|
+
"tsup": "^8.5.1",
|
|
34
|
+
"typescript": "^5.9.3"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsup",
|
|
38
|
+
"dev": "tsup --watch --no-clean",
|
|
39
|
+
"type-check": "tsc --noEmit",
|
|
40
|
+
"lint": "biome lint .",
|
|
41
|
+
"clean": "rm -rf dist .turbo *.tsbuildinfo"
|
|
42
|
+
}
|
|
43
|
+
}
|