framer-cms-markdown 0.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/.eslintrc.json +7 -0
- package/LICENSE +201 -0
- package/README.md +49 -0
- package/dist/src/__tests__/filename.spec.js +15 -0
- package/dist/src/__tests__/frontmatter.spec.js +15 -0
- package/dist/src/__tests__/markdown.spec.js +9 -0
- package/dist/src/cli.js +62 -0
- package/dist/src/export.js +83 -0
- package/dist/src/filename.js +42 -0
- package/dist/src/framer-client.js +106 -0
- package/dist/src/framer-sync/config.js +50 -0
- package/dist/src/framer-sync/filename.js +41 -0
- package/dist/src/framer-sync/format.js +9 -0
- package/dist/src/framer-sync/framer-client.js +180 -0
- package/dist/src/framer-sync/frontmatter.js +49 -0
- package/dist/src/framer-sync/markdown.js +70 -0
- package/dist/src/framer-to-markdown/__tests__/export.spec.js +64 -0
- package/dist/src/framer-to-markdown/__tests__/faq.spec.js +23 -0
- package/dist/src/framer-to-markdown/__tests__/filename.spec.js +15 -0
- package/dist/src/framer-to-markdown/__tests__/framer-ref.spec.js +12 -0
- package/dist/src/framer-to-markdown/__tests__/frontmatter.spec.js +15 -0
- package/dist/src/framer-to-markdown/__tests__/markdown.spec.js +20 -0
- package/dist/src/framer-to-markdown/cli.js +5 -0
- package/dist/src/framer-to-markdown/config.js +1 -0
- package/dist/src/framer-to-markdown/export.js +138 -0
- package/dist/src/framer-to-markdown/faq.js +44 -0
- package/dist/src/framer-to-markdown/filename.js +1 -0
- package/dist/src/framer-to-markdown/format.js +1 -0
- package/dist/src/framer-to-markdown/framer-client.js +1 -0
- package/dist/src/framer-to-markdown/framer-ref.js +28 -0
- package/dist/src/framer-to-markdown/frontmatter.js +1 -0
- package/dist/src/framer-to-markdown/index.js +49 -0
- package/dist/src/framer-to-markdown/markdown.js +1 -0
- package/dist/src/frontmatter.js +39 -0
- package/dist/src/index.js +22 -0
- package/dist/src/libs/framer/config.js +50 -0
- package/dist/src/libs/framer/filename.js +41 -0
- package/dist/src/libs/framer/format.js +9 -0
- package/dist/src/libs/framer/framer-client.js +180 -0
- package/dist/src/libs/framer/frontmatter.js +49 -0
- package/dist/src/libs/framer/markdown.js +70 -0
- package/dist/src/markdown-to-framer/__tests__/import.spec.js +69 -0
- package/dist/src/markdown-to-framer/cli.js +5 -0
- package/dist/src/markdown-to-framer/import.js +142 -0
- package/dist/src/markdown-to-framer/index.js +51 -0
- package/dist/src/markdown.js +21 -0
- package/dist/test/filename.test.js +19 -0
- package/dist/test/frontmatter.test.js +19 -0
- package/dist/test/markdown.test.js +13 -0
- package/package.json +63 -0
- package/src/framer-to-markdown/__tests__/export.spec.ts +85 -0
- package/src/framer-to-markdown/__tests__/filename.spec.ts +21 -0
- package/src/framer-to-markdown/__tests__/frontmatter.spec.ts +18 -0
- package/src/framer-to-markdown/__tests__/markdown.spec.ts +26 -0
- package/src/framer-to-markdown/cli.ts +8 -0
- package/src/framer-to-markdown/config.ts +4 -0
- package/src/framer-to-markdown/export.ts +221 -0
- package/src/framer-to-markdown/filename.ts +5 -0
- package/src/framer-to-markdown/format.ts +1 -0
- package/src/framer-to-markdown/framer-client.ts +6 -0
- package/src/framer-to-markdown/frontmatter.ts +4 -0
- package/src/framer-to-markdown/index.ts +63 -0
- package/src/framer-to-markdown/markdown.ts +5 -0
- package/src/libs/framer/config.ts +79 -0
- package/src/libs/framer/filename.ts +59 -0
- package/src/libs/framer/format.ts +12 -0
- package/src/libs/framer/framer-client.ts +267 -0
- package/src/libs/framer/frontmatter.ts +69 -0
- package/src/libs/framer/markdown.ts +84 -0
- package/src/markdown-to-framer/__tests__/import.spec.ts +89 -0
- package/src/markdown-to-framer/__tests__/json-ld-faq.spec.ts +116 -0
- package/src/markdown-to-framer/cli.ts +6 -0
- package/src/markdown-to-framer/import.ts +251 -0
- package/src/markdown-to-framer/index.ts +83 -0
- package/src/markdown-to-framer/json-ld-faq.ts +146 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { resolveMarkdownFilename, slugify } from "./filename";
|
|
5
|
+
import { formatMarkdownDocument } from "./format";
|
|
6
|
+
import type { FramerCollectionItemExport } from "./framer-client";
|
|
7
|
+
import { toFrontmatter } from "./frontmatter";
|
|
8
|
+
import { htmlToMarkdown } from "./markdown";
|
|
9
|
+
|
|
10
|
+
export type ExportOptions = {
|
|
11
|
+
outputDir?: string;
|
|
12
|
+
collectionName?: string;
|
|
13
|
+
bodyFieldName?: string;
|
|
14
|
+
frontmatterFields?: string[];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const SECTION_HTML_FIELD_PATTERN = /^Section\s+(\d+)\s+HTML$/i;
|
|
18
|
+
const BODY_FIELD_CANDIDATES = [
|
|
19
|
+
"content",
|
|
20
|
+
"body",
|
|
21
|
+
"html",
|
|
22
|
+
"richText",
|
|
23
|
+
"rich_text",
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
function stringValue(value: unknown): string {
|
|
27
|
+
return typeof value === "string" ? value : "";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function looksLikeHtml(value: string): boolean {
|
|
31
|
+
return /<\/?[a-z][\s\S]*>/i.test(value);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function normalizeFrontmatterValue(value: unknown): unknown {
|
|
35
|
+
if (value instanceof Date) {
|
|
36
|
+
return value.toISOString();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (Array.isArray(value)) {
|
|
40
|
+
return value.map(normalizeFrontmatterValue);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (value && typeof value === "object") {
|
|
44
|
+
const record: Record<string, unknown> = {};
|
|
45
|
+
for (const [key, nestedValue] of Object.entries(
|
|
46
|
+
value as Record<string, unknown>,
|
|
47
|
+
)) {
|
|
48
|
+
if (nestedValue !== undefined) {
|
|
49
|
+
record[key] = normalizeFrontmatterValue(nestedValue);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return record;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function resolveBodyField(
|
|
59
|
+
item: FramerCollectionItemExport,
|
|
60
|
+
bodyFieldName?: string,
|
|
61
|
+
): string | undefined {
|
|
62
|
+
if (bodyFieldName === "sections") {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (bodyFieldName && item.fieldData[bodyFieldName] !== undefined) {
|
|
67
|
+
return bodyFieldName;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
for (const candidate of BODY_FIELD_CANDIDATES) {
|
|
71
|
+
const value = item.fieldData[candidate];
|
|
72
|
+
if (typeof value === "string" && stringValue(value).trim().length > 0) {
|
|
73
|
+
return candidate;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
for (const [key, value] of Object.entries(item.fieldData)) {
|
|
78
|
+
if (typeof value === "string" && looksLikeHtml(value)) {
|
|
79
|
+
return key;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
type SectionField = {
|
|
87
|
+
name: string;
|
|
88
|
+
order: number;
|
|
89
|
+
value: string;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export function resolveSectionFields(
|
|
93
|
+
item: FramerCollectionItemExport,
|
|
94
|
+
): SectionField[] {
|
|
95
|
+
return Object.entries(item.fieldData)
|
|
96
|
+
.flatMap(([key, value]) => {
|
|
97
|
+
if (typeof value !== "string") {
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const match = key.match(SECTION_HTML_FIELD_PATTERN);
|
|
102
|
+
if (!match) {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return [
|
|
107
|
+
{
|
|
108
|
+
name: key,
|
|
109
|
+
order: Number.parseInt(match[1], 10),
|
|
110
|
+
value,
|
|
111
|
+
},
|
|
112
|
+
];
|
|
113
|
+
})
|
|
114
|
+
.sort(
|
|
115
|
+
(left, right) =>
|
|
116
|
+
left.order - right.order || left.name.localeCompare(right.name),
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function buildFrontmatter(
|
|
121
|
+
item: FramerCollectionItemExport,
|
|
122
|
+
options: {
|
|
123
|
+
frontmatterFields?: string[];
|
|
124
|
+
},
|
|
125
|
+
): Record<string, unknown> {
|
|
126
|
+
const frontmatter: Record<string, unknown> = {
|
|
127
|
+
slug: item.slug,
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const { frontmatterFields } = options;
|
|
131
|
+
|
|
132
|
+
for (const fieldName of frontmatterFields ?? []) {
|
|
133
|
+
const value = item.fieldData[fieldName];
|
|
134
|
+
if (
|
|
135
|
+
value === undefined ||
|
|
136
|
+
typeof value === "function" ||
|
|
137
|
+
typeof value === "symbol"
|
|
138
|
+
) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
frontmatter[fieldName] = normalizeFrontmatterValue(value);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return frontmatter;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function stripMarkdownComments(markdown: string): string {
|
|
149
|
+
return markdown.replace(/<!--[\s\S]*?-->\s*/g, "").trim();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function renderBodyContent(value: unknown): Promise<string> {
|
|
153
|
+
const text = stringValue(value).trim();
|
|
154
|
+
if (!text) {
|
|
155
|
+
return "";
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const markdown = looksLikeHtml(text) ? await htmlToMarkdown(text) : text;
|
|
159
|
+
const normalized = stripMarkdownComments(markdown);
|
|
160
|
+
return normalized.length > 0 ? `${normalized}\n` : "";
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function renderMarkdownBody(
|
|
164
|
+
item: FramerCollectionItemExport,
|
|
165
|
+
bodyFieldName?: string,
|
|
166
|
+
): Promise<string> {
|
|
167
|
+
const sectionFields = resolveSectionFields(item);
|
|
168
|
+
if (bodyFieldName === "sections" || sectionFields.length > 0) {
|
|
169
|
+
return Promise.all(
|
|
170
|
+
sectionFields.map((section) => renderBodyContent(section.value)),
|
|
171
|
+
).then((sections) =>
|
|
172
|
+
sections
|
|
173
|
+
.map((section) => section.trim())
|
|
174
|
+
.filter((section) => section.length > 0)
|
|
175
|
+
.join("\n\n")
|
|
176
|
+
.trim()
|
|
177
|
+
.concat(sectionFields.length > 0 ? "\n" : ""),
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const bodyField = resolveBodyField(item, bodyFieldName);
|
|
182
|
+
return bodyField
|
|
183
|
+
? renderBodyContent(item.fieldData[bodyField])
|
|
184
|
+
: Promise.resolve("");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export async function exportItemsToMarkdown(
|
|
188
|
+
items: FramerCollectionItemExport[],
|
|
189
|
+
options: ExportOptions = {},
|
|
190
|
+
): Promise<string[]> {
|
|
191
|
+
const outputDir =
|
|
192
|
+
options.outputDir ??
|
|
193
|
+
path.join(
|
|
194
|
+
process.cwd(),
|
|
195
|
+
"export",
|
|
196
|
+
slugify(options.collectionName ?? "collection"),
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
200
|
+
|
|
201
|
+
const usedNames = new Set<string>();
|
|
202
|
+
const writtenFiles: string[] = [];
|
|
203
|
+
|
|
204
|
+
for (const item of items) {
|
|
205
|
+
const filename = resolveMarkdownFilename(item, usedNames);
|
|
206
|
+
const filepath = path.join(outputDir, filename);
|
|
207
|
+
const frontmatter = buildFrontmatter(item, {
|
|
208
|
+
frontmatterFields: options.frontmatterFields,
|
|
209
|
+
});
|
|
210
|
+
const markdownBody = await renderMarkdownBody(item, options.bodyFieldName);
|
|
211
|
+
|
|
212
|
+
const fileContents = await formatMarkdownDocument(
|
|
213
|
+
`${toFrontmatter(frontmatter)}${markdownBody}`,
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
await fs.writeFile(filepath, fileContents, "utf8");
|
|
217
|
+
writtenFiles.push(filepath);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return writtenFiles;
|
|
221
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { formatMarkdownDocument } from "../libs/framer/format";
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
|
|
4
|
+
import yargs from "yargs";
|
|
5
|
+
import { hideBin } from "yargs/helpers";
|
|
6
|
+
|
|
7
|
+
import { loadConfig } from "./config";
|
|
8
|
+
import { exportItemsToMarkdown } from "./export";
|
|
9
|
+
import { FramerClient } from "./framer-client";
|
|
10
|
+
|
|
11
|
+
export type CliArgs = {
|
|
12
|
+
token?: string;
|
|
13
|
+
config: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export async function runCli(argv = hideBin(process.argv)): Promise<void> {
|
|
17
|
+
const args = await yargs(argv)
|
|
18
|
+
.scriptName("framer-to-markdown")
|
|
19
|
+
.usage("$0 --token <token> --config config/use-cases.yml")
|
|
20
|
+
.option("token", {
|
|
21
|
+
type: "string",
|
|
22
|
+
describe: "Framer API token. Falls back to FRAMER_TOKEN.",
|
|
23
|
+
})
|
|
24
|
+
.option("config", {
|
|
25
|
+
type: "string",
|
|
26
|
+
demandOption: true,
|
|
27
|
+
describe: "Path to a single export config file",
|
|
28
|
+
})
|
|
29
|
+
.strict()
|
|
30
|
+
.help()
|
|
31
|
+
.parse();
|
|
32
|
+
|
|
33
|
+
const cliArgs = args as unknown as CliArgs;
|
|
34
|
+
const token = cliArgs.token ?? process.env.FRAMER_TOKEN;
|
|
35
|
+
if (!token) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
"Framer token is required. Pass --token or set FRAMER_TOKEN.",
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const { config } = await loadConfig(cliArgs.config);
|
|
42
|
+
|
|
43
|
+
const client = new FramerClient({
|
|
44
|
+
projectRef: config.site,
|
|
45
|
+
token,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const data = await client.getCollectionExportData(config.collection);
|
|
50
|
+
const writtenFiles = await exportItemsToMarkdown(data.items, {
|
|
51
|
+
outputDir: config.content ? path.resolve(config.content) : undefined,
|
|
52
|
+
collectionName: data.collection.name ?? data.collection.id,
|
|
53
|
+
bodyFieldName: config.markdown,
|
|
54
|
+
frontmatterFields: config.metadata,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
for (const file of writtenFiles) {
|
|
58
|
+
console.log(file);
|
|
59
|
+
}
|
|
60
|
+
} finally {
|
|
61
|
+
await client.disconnect();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import yaml from "yaml";
|
|
5
|
+
|
|
6
|
+
export type SyncConfig = {
|
|
7
|
+
site: string;
|
|
8
|
+
collection: string;
|
|
9
|
+
content?: string;
|
|
10
|
+
markdown: string;
|
|
11
|
+
metadata: string[];
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
15
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function parseMetadataConfig(value: unknown): string[] {
|
|
19
|
+
if (!Array.isArray(value)) {
|
|
20
|
+
throw new Error('Config "metadata" must be an array.');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const fields = value.map((entry, index) => {
|
|
24
|
+
if (typeof entry !== "string" || entry.trim().length === 0) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`Config "metadata[${index}]" must be a non-empty string.`,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return entry;
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
if (fields.length === 0) {
|
|
34
|
+
throw new Error('Config "metadata" must define at least one field.');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return fields;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function loadConfig(
|
|
41
|
+
configPath: string,
|
|
42
|
+
): Promise<{ config: SyncConfig; resolvedPath: string }> {
|
|
43
|
+
const resolvedPath = path.resolve(configPath);
|
|
44
|
+
const raw = await fs.readFile(resolvedPath, "utf8");
|
|
45
|
+
const parsed = yaml.parse(raw) as unknown;
|
|
46
|
+
|
|
47
|
+
if (!isRecord(parsed)) {
|
|
48
|
+
throw new Error("Config file must contain a single object.");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (typeof parsed.site !== "string" || parsed.site.trim().length === 0) {
|
|
52
|
+
throw new Error('Config "site" must be a non-empty string.');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (
|
|
56
|
+
typeof parsed.collection !== "string" ||
|
|
57
|
+
parsed.collection.trim().length === 0
|
|
58
|
+
) {
|
|
59
|
+
throw new Error('Config "collection" must be a non-empty string.');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (
|
|
63
|
+
typeof parsed.markdown !== "string" ||
|
|
64
|
+
parsed.markdown.trim().length === 0
|
|
65
|
+
) {
|
|
66
|
+
throw new Error('Config "markdown" must be a non-empty string.');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
resolvedPath,
|
|
71
|
+
config: {
|
|
72
|
+
site: parsed.site,
|
|
73
|
+
collection: parsed.collection,
|
|
74
|
+
content: typeof parsed.content === "string" ? parsed.content : undefined,
|
|
75
|
+
markdown: parsed.markdown,
|
|
76
|
+
metadata: parseMetadataConfig(parsed.metadata),
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export function slugify(input: string): string {
|
|
2
|
+
const normalized = input
|
|
3
|
+
.normalize("NFKD")
|
|
4
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
5
|
+
.toLowerCase()
|
|
6
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
7
|
+
.replace(/^-+|-+$/g, "")
|
|
8
|
+
.replace(/-{2,}/g, "-");
|
|
9
|
+
|
|
10
|
+
return normalized.length > 0 ? normalized : "item";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function uniqueName(
|
|
14
|
+
baseName: string,
|
|
15
|
+
existingNames: Set<string>,
|
|
16
|
+
): string {
|
|
17
|
+
if (!existingNames.has(baseName)) {
|
|
18
|
+
return baseName;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let suffix = 2;
|
|
22
|
+
while (existingNames.has(`${baseName}-${suffix}`)) {
|
|
23
|
+
suffix += 1;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return `${baseName}-${suffix}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function resolveMarkdownFilename(
|
|
30
|
+
item: {
|
|
31
|
+
slug?: string | null;
|
|
32
|
+
title?: string | null;
|
|
33
|
+
id?: string | number | null;
|
|
34
|
+
},
|
|
35
|
+
existingNames: Set<string> = new Set(),
|
|
36
|
+
): string {
|
|
37
|
+
const candidates = [
|
|
38
|
+
item.slug,
|
|
39
|
+
item.title,
|
|
40
|
+
item.id != null ? String(item.id) : null,
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
for (const candidate of candidates) {
|
|
44
|
+
if (!candidate) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const name = slugify(candidate);
|
|
49
|
+
if (name) {
|
|
50
|
+
const unique = uniqueName(name, existingNames);
|
|
51
|
+
existingNames.add(unique);
|
|
52
|
+
return `${unique}.md`;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const fallback = uniqueName("item", existingNames);
|
|
57
|
+
existingNames.add(fallback);
|
|
58
|
+
return `${fallback}.md`;
|
|
59
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as markdownPlugin from "prettier/plugins/markdown";
|
|
2
|
+
import * as prettier from "prettier/standalone";
|
|
3
|
+
|
|
4
|
+
export async function formatMarkdownDocument(
|
|
5
|
+
markdown: string,
|
|
6
|
+
): Promise<string> {
|
|
7
|
+
return prettier.format(markdown, {
|
|
8
|
+
parser: "markdown",
|
|
9
|
+
plugins: [markdownPlugin],
|
|
10
|
+
proseWrap: "preserve",
|
|
11
|
+
});
|
|
12
|
+
}
|