framer-cms-markdown 0.0.1 → 0.0.3
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/.github/workflows/ci.yml +17 -0
- package/.github/workflows/npm-publish.yml +26 -0
- package/dist/src/framer-to-markdown/cli.js +1 -0
- package/dist/src/framer-to-markdown/config.js +1 -1
- package/dist/src/framer-to-markdown/export.js +3 -1
- package/dist/src/framer-to-markdown/frontmatter.js +1 -1
- package/dist/src/libs/framer/framer-client.js +12 -4
- package/dist/src/markdown-to-framer/__tests__/import.spec.js +1 -4
- package/dist/src/markdown-to-framer/__tests__/json-ld-faq.spec.js +99 -0
- package/dist/src/markdown-to-framer/cli.js +1 -0
- package/dist/src/markdown-to-framer/import.js +13 -3
- package/dist/src/markdown-to-framer/index.js +1 -1
- package/dist/src/markdown-to-framer/json-ld-faq.js +96 -0
- package/package.json +4 -4
- package/src/markdown-to-framer/cli.ts +2 -0
- package/dist/src/__tests__/filename.spec.js +0 -15
- package/dist/src/__tests__/frontmatter.spec.js +0 -15
- package/dist/src/__tests__/markdown.spec.js +0 -9
- package/dist/src/cli.js +0 -62
- package/dist/src/export.js +0 -83
- package/dist/src/filename.js +0 -42
- package/dist/src/framer-client.js +0 -106
- package/dist/src/framer-sync/config.js +0 -50
- package/dist/src/framer-sync/filename.js +0 -41
- package/dist/src/framer-sync/format.js +0 -9
- package/dist/src/framer-sync/framer-client.js +0 -180
- package/dist/src/framer-sync/frontmatter.js +0 -49
- package/dist/src/framer-sync/markdown.js +0 -70
- package/dist/src/framer-to-markdown/__tests__/faq.spec.js +0 -23
- package/dist/src/framer-to-markdown/__tests__/framer-ref.spec.js +0 -12
- package/dist/src/framer-to-markdown/faq.js +0 -44
- package/dist/src/framer-to-markdown/framer-ref.js +0 -28
- package/dist/src/frontmatter.js +0 -39
- package/dist/src/index.js +0 -22
- package/dist/src/markdown.js +0 -21
- package/dist/test/filename.test.js +0 -19
- package/dist/test/frontmatter.test.js +0 -19
- package/dist/test/markdown.test.js +0 -13
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.FramerClient = void 0;
|
|
4
|
-
class FramerClient {
|
|
5
|
-
constructor(options) {
|
|
6
|
-
this.token = options.token;
|
|
7
|
-
this.baseUrl = (options.baseUrl ?? "https://api.framer.com").replace(/\/+$/, "");
|
|
8
|
-
}
|
|
9
|
-
async resolveCollection(siteId, collectionRef) {
|
|
10
|
-
const collections = await this.listCollections(siteId);
|
|
11
|
-
const match = collections.find((collection) => {
|
|
12
|
-
return (collection.id === collectionRef ||
|
|
13
|
-
collection.slug === collectionRef ||
|
|
14
|
-
collection.name === collectionRef);
|
|
15
|
-
});
|
|
16
|
-
if (match) {
|
|
17
|
-
return match;
|
|
18
|
-
}
|
|
19
|
-
return { id: collectionRef };
|
|
20
|
-
}
|
|
21
|
-
async listCollections(siteId) {
|
|
22
|
-
const response = await this.getJson(`/v1/sites/${encodeURIComponent(siteId)}/collections`);
|
|
23
|
-
return normalizeCollectionList(response);
|
|
24
|
-
}
|
|
25
|
-
async listCollectionItems(siteId, collectionId) {
|
|
26
|
-
const items = [];
|
|
27
|
-
let cursor;
|
|
28
|
-
while (true) {
|
|
29
|
-
const path = new URL(`/v1/sites/${encodeURIComponent(siteId)}/collections/${encodeURIComponent(collectionId)}/items`, this.baseUrl);
|
|
30
|
-
if (cursor) {
|
|
31
|
-
path.searchParams.set("cursor", cursor);
|
|
32
|
-
}
|
|
33
|
-
const response = await this.getJson(path.pathname + path.search);
|
|
34
|
-
const pageItems = normalizeItemList(response);
|
|
35
|
-
items.push(...pageItems);
|
|
36
|
-
const nextCursor = readNextCursor(response);
|
|
37
|
-
if (!nextCursor) {
|
|
38
|
-
break;
|
|
39
|
-
}
|
|
40
|
-
cursor = nextCursor;
|
|
41
|
-
}
|
|
42
|
-
return items;
|
|
43
|
-
}
|
|
44
|
-
async getJson(path) {
|
|
45
|
-
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
46
|
-
headers: {
|
|
47
|
-
Authorization: `Bearer ${this.token}`,
|
|
48
|
-
Accept: "application/json",
|
|
49
|
-
},
|
|
50
|
-
});
|
|
51
|
-
if (!response.ok) {
|
|
52
|
-
const errorBody = await response.text();
|
|
53
|
-
throw new Error(`Framer API request failed (${response.status} ${response.statusText}): ${errorBody}`);
|
|
54
|
-
}
|
|
55
|
-
return response.json();
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
exports.FramerClient = FramerClient;
|
|
59
|
-
function normalizeCollectionList(response) {
|
|
60
|
-
const list = extractArray(response);
|
|
61
|
-
return list
|
|
62
|
-
.map((item) => item)
|
|
63
|
-
.map((item) => ({
|
|
64
|
-
id: String(item.id ?? item.collectionId ?? item._id ?? item.slug ?? item.name ?? ""),
|
|
65
|
-
name: typeof item.name === "string" ? item.name : undefined,
|
|
66
|
-
slug: typeof item.slug === "string" ? item.slug : undefined,
|
|
67
|
-
}))
|
|
68
|
-
.filter((collection) => collection.id.length > 0);
|
|
69
|
-
}
|
|
70
|
-
function normalizeItemList(response) {
|
|
71
|
-
return extractArray(response).map((item) => item);
|
|
72
|
-
}
|
|
73
|
-
function extractArray(response) {
|
|
74
|
-
if (Array.isArray(response)) {
|
|
75
|
-
return response;
|
|
76
|
-
}
|
|
77
|
-
if (!response || typeof response !== "object") {
|
|
78
|
-
return [];
|
|
79
|
-
}
|
|
80
|
-
const record = response;
|
|
81
|
-
for (const key of ["items", "data", "results", "collectionItems"]) {
|
|
82
|
-
const value = record[key];
|
|
83
|
-
if (Array.isArray(value)) {
|
|
84
|
-
return value;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
return [];
|
|
88
|
-
}
|
|
89
|
-
function readNextCursor(response) {
|
|
90
|
-
if (!response || typeof response !== "object") {
|
|
91
|
-
return undefined;
|
|
92
|
-
}
|
|
93
|
-
const record = response;
|
|
94
|
-
const direct = record.nextCursor ?? record.next_cursor ?? record.cursor;
|
|
95
|
-
if (typeof direct === "string" && direct.length > 0) {
|
|
96
|
-
return direct;
|
|
97
|
-
}
|
|
98
|
-
const pageInfo = record.pageInfo;
|
|
99
|
-
if (pageInfo) {
|
|
100
|
-
const pageCursor = pageInfo.endCursor ?? pageInfo.nextCursor;
|
|
101
|
-
if (typeof pageCursor === "string" && pageCursor.length > 0) {
|
|
102
|
-
return pageCursor;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
return undefined;
|
|
106
|
-
}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import yaml from "yaml";
|
|
4
|
-
function isRecord(value) {
|
|
5
|
-
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
6
|
-
}
|
|
7
|
-
function parseMetadataConfig(value) {
|
|
8
|
-
if (!Array.isArray(value)) {
|
|
9
|
-
throw new Error('Config "metadata" must be an array.');
|
|
10
|
-
}
|
|
11
|
-
const fields = value.map((entry, index) => {
|
|
12
|
-
if (typeof entry !== "string" || entry.trim().length === 0) {
|
|
13
|
-
throw new Error(`Config "metadata[${index}]" must be a non-empty string.`);
|
|
14
|
-
}
|
|
15
|
-
return entry;
|
|
16
|
-
});
|
|
17
|
-
if (fields.length === 0) {
|
|
18
|
-
throw new Error('Config "metadata" must define at least one field.');
|
|
19
|
-
}
|
|
20
|
-
return fields;
|
|
21
|
-
}
|
|
22
|
-
export async function loadConfig(configPath) {
|
|
23
|
-
const resolvedPath = path.resolve(configPath);
|
|
24
|
-
const raw = await fs.readFile(resolvedPath, "utf8");
|
|
25
|
-
const parsed = yaml.parse(raw);
|
|
26
|
-
if (!isRecord(parsed)) {
|
|
27
|
-
throw new Error("Config file must contain a single object.");
|
|
28
|
-
}
|
|
29
|
-
if (typeof parsed.site !== "string" || parsed.site.trim().length === 0) {
|
|
30
|
-
throw new Error('Config "site" must be a non-empty string.');
|
|
31
|
-
}
|
|
32
|
-
if (typeof parsed.collection !== "string" ||
|
|
33
|
-
parsed.collection.trim().length === 0) {
|
|
34
|
-
throw new Error('Config "collection" must be a non-empty string.');
|
|
35
|
-
}
|
|
36
|
-
if (typeof parsed.markdown !== "string" ||
|
|
37
|
-
parsed.markdown.trim().length === 0) {
|
|
38
|
-
throw new Error('Config "markdown" must be a non-empty string.');
|
|
39
|
-
}
|
|
40
|
-
return {
|
|
41
|
-
resolvedPath,
|
|
42
|
-
config: {
|
|
43
|
-
site: parsed.site,
|
|
44
|
-
collection: parsed.collection,
|
|
45
|
-
content: typeof parsed.content === "string" ? parsed.content : undefined,
|
|
46
|
-
markdown: parsed.markdown,
|
|
47
|
-
metadata: parseMetadataConfig(parsed.metadata),
|
|
48
|
-
},
|
|
49
|
-
};
|
|
50
|
-
}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
export function slugify(input) {
|
|
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
|
-
return normalized.length > 0 ? normalized : "item";
|
|
10
|
-
}
|
|
11
|
-
export function uniqueName(baseName, existingNames) {
|
|
12
|
-
if (!existingNames.has(baseName)) {
|
|
13
|
-
return baseName;
|
|
14
|
-
}
|
|
15
|
-
let suffix = 2;
|
|
16
|
-
while (existingNames.has(`${baseName}-${suffix}`)) {
|
|
17
|
-
suffix += 1;
|
|
18
|
-
}
|
|
19
|
-
return `${baseName}-${suffix}`;
|
|
20
|
-
}
|
|
21
|
-
export function resolveMarkdownFilename(item, existingNames = new Set()) {
|
|
22
|
-
const candidates = [
|
|
23
|
-
item.slug,
|
|
24
|
-
item.title,
|
|
25
|
-
item.id != null ? String(item.id) : null,
|
|
26
|
-
];
|
|
27
|
-
for (const candidate of candidates) {
|
|
28
|
-
if (!candidate) {
|
|
29
|
-
continue;
|
|
30
|
-
}
|
|
31
|
-
const name = slugify(candidate);
|
|
32
|
-
if (name) {
|
|
33
|
-
const unique = uniqueName(name, existingNames);
|
|
34
|
-
existingNames.add(unique);
|
|
35
|
-
return `${unique}.md`;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
const fallback = uniqueName("item", existingNames);
|
|
39
|
-
existingNames.add(fallback);
|
|
40
|
-
return `${fallback}.md`;
|
|
41
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import * as markdownPlugin from "prettier/plugins/markdown";
|
|
2
|
-
import * as prettier from "prettier/standalone";
|
|
3
|
-
export async function formatMarkdownDocument(markdown) {
|
|
4
|
-
return prettier.format(markdown, {
|
|
5
|
-
parser: "markdown",
|
|
6
|
-
plugins: [markdownPlugin],
|
|
7
|
-
proseWrap: "preserve",
|
|
8
|
-
});
|
|
9
|
-
}
|
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
import { connect, } from "framer-api";
|
|
2
|
-
function extractAssetUrl(value) {
|
|
3
|
-
if (!value || typeof value !== "object") {
|
|
4
|
-
return undefined;
|
|
5
|
-
}
|
|
6
|
-
const record = value;
|
|
7
|
-
return typeof record.url === "string" ? record.url : undefined;
|
|
8
|
-
}
|
|
9
|
-
function extractIdOrString(value) {
|
|
10
|
-
if (typeof value === "string") {
|
|
11
|
-
return value;
|
|
12
|
-
}
|
|
13
|
-
if (!value || typeof value !== "object") {
|
|
14
|
-
return undefined;
|
|
15
|
-
}
|
|
16
|
-
const record = value;
|
|
17
|
-
if (typeof record.id === "string") {
|
|
18
|
-
return record.id;
|
|
19
|
-
}
|
|
20
|
-
if (typeof record.name === "string") {
|
|
21
|
-
return record.name;
|
|
22
|
-
}
|
|
23
|
-
return undefined;
|
|
24
|
-
}
|
|
25
|
-
function normalizeFieldEntry(entry) {
|
|
26
|
-
switch (entry.type) {
|
|
27
|
-
case "boolean":
|
|
28
|
-
case "number":
|
|
29
|
-
case "string":
|
|
30
|
-
case "enum":
|
|
31
|
-
case "link":
|
|
32
|
-
return entry.value;
|
|
33
|
-
case "date":
|
|
34
|
-
return entry.value ?? undefined;
|
|
35
|
-
case "formattedText":
|
|
36
|
-
return typeof entry.value === "string" ? entry.value : undefined;
|
|
37
|
-
case "image":
|
|
38
|
-
case "file":
|
|
39
|
-
return extractAssetUrl(entry.value);
|
|
40
|
-
case "color":
|
|
41
|
-
return extractIdOrString(entry.value);
|
|
42
|
-
case "collectionReference":
|
|
43
|
-
return entry.value ?? undefined;
|
|
44
|
-
case "multiCollectionReference":
|
|
45
|
-
return Array.isArray(entry.value) ? entry.value : undefined;
|
|
46
|
-
default:
|
|
47
|
-
return undefined;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
export function createFieldDataEntryInput(field, value, options = {}) {
|
|
51
|
-
switch (field.type) {
|
|
52
|
-
case "boolean":
|
|
53
|
-
return typeof value === "boolean"
|
|
54
|
-
? { type: field.type, value }
|
|
55
|
-
: undefined;
|
|
56
|
-
case "number":
|
|
57
|
-
return typeof value === "number" ? { type: field.type, value } : undefined;
|
|
58
|
-
case "string":
|
|
59
|
-
return typeof value === "string" ? { type: field.type, value } : undefined;
|
|
60
|
-
case "formattedText":
|
|
61
|
-
return typeof value === "string"
|
|
62
|
-
? {
|
|
63
|
-
type: field.type,
|
|
64
|
-
value,
|
|
65
|
-
contentType: options.formattedTextContentType ?? "html",
|
|
66
|
-
}
|
|
67
|
-
: undefined;
|
|
68
|
-
case "enum":
|
|
69
|
-
return typeof value === "string" ? { type: field.type, value } : undefined;
|
|
70
|
-
case "link":
|
|
71
|
-
return typeof value === "string" ? { type: field.type, value } : undefined;
|
|
72
|
-
case "date":
|
|
73
|
-
return typeof value === "string" || typeof value === "number"
|
|
74
|
-
? { type: field.type, value: value }
|
|
75
|
-
: undefined;
|
|
76
|
-
case "collectionReference":
|
|
77
|
-
return typeof value === "string" || value === null
|
|
78
|
-
? { type: field.type, value: value }
|
|
79
|
-
: undefined;
|
|
80
|
-
case "multiCollectionReference":
|
|
81
|
-
return Array.isArray(value) &&
|
|
82
|
-
value.every((entry) => typeof entry === "string")
|
|
83
|
-
? { type: field.type, value: value }
|
|
84
|
-
: undefined;
|
|
85
|
-
case "color":
|
|
86
|
-
case "file":
|
|
87
|
-
case "image":
|
|
88
|
-
return typeof value === "string" || value === null
|
|
89
|
-
? { type: field.type, value: value }
|
|
90
|
-
: undefined;
|
|
91
|
-
case "array":
|
|
92
|
-
case "divider":
|
|
93
|
-
case "unsupported":
|
|
94
|
-
return undefined;
|
|
95
|
-
default:
|
|
96
|
-
return undefined;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
export class FramerClient {
|
|
100
|
-
constructor(options) {
|
|
101
|
-
this.projectRef = options.projectRef;
|
|
102
|
-
this.token = options.token;
|
|
103
|
-
}
|
|
104
|
-
async connect() {
|
|
105
|
-
if (!this.framer) {
|
|
106
|
-
this.framer = await connect(this.projectRef, this.token);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
async disconnect() {
|
|
110
|
-
if (this.framer) {
|
|
111
|
-
await this.framer.disconnect();
|
|
112
|
-
this.framer = undefined;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
async getCollections() {
|
|
116
|
-
return (await this.getFramer()).getCollections();
|
|
117
|
-
}
|
|
118
|
-
async resolveCollection(collectionRef) {
|
|
119
|
-
const collections = await this.getCollections();
|
|
120
|
-
const normalizedRef = collectionRef.trim().toLowerCase();
|
|
121
|
-
const match = collections.find((collection) => {
|
|
122
|
-
return (collection.id.toLowerCase() === normalizedRef ||
|
|
123
|
-
collection.name.toLowerCase() === normalizedRef);
|
|
124
|
-
});
|
|
125
|
-
if (!match) {
|
|
126
|
-
const available = collections
|
|
127
|
-
.map((collection) => `${collection.name} (${collection.id})`)
|
|
128
|
-
.join(", ");
|
|
129
|
-
throw new Error(`Collection "${collectionRef}" was not found. Available collections: ${available || "none"}.`);
|
|
130
|
-
}
|
|
131
|
-
return match;
|
|
132
|
-
}
|
|
133
|
-
async getCollectionData(collectionRef) {
|
|
134
|
-
const collection = await this.resolveCollection(collectionRef);
|
|
135
|
-
const fields = await collection.getFields();
|
|
136
|
-
const items = await collection.getItems();
|
|
137
|
-
return { collection, fields, items };
|
|
138
|
-
}
|
|
139
|
-
async getCollectionExportData(collectionRef) {
|
|
140
|
-
const { collection, fields, items } = await this.getCollectionData(collectionRef);
|
|
141
|
-
return {
|
|
142
|
-
collection,
|
|
143
|
-
fields,
|
|
144
|
-
items: items.map((item) => this.toExportItem(item, fields)),
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
async upsertCollectionItems(collection, items) {
|
|
148
|
-
await collection.addItems(items);
|
|
149
|
-
}
|
|
150
|
-
async getFramer() {
|
|
151
|
-
await this.connect();
|
|
152
|
-
if (!this.framer) {
|
|
153
|
-
throw new Error("Failed to connect to Framer");
|
|
154
|
-
}
|
|
155
|
-
return this.framer;
|
|
156
|
-
}
|
|
157
|
-
toExportItem(item, fields) {
|
|
158
|
-
const fieldData = {};
|
|
159
|
-
for (const field of fields) {
|
|
160
|
-
if (field.type === "divider") {
|
|
161
|
-
continue;
|
|
162
|
-
}
|
|
163
|
-
const entry = item.fieldData[field.id];
|
|
164
|
-
if (!entry) {
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
167
|
-
const value = normalizeFieldEntry(entry);
|
|
168
|
-
if (value === undefined) {
|
|
169
|
-
continue;
|
|
170
|
-
}
|
|
171
|
-
fieldData[field.name] = value;
|
|
172
|
-
}
|
|
173
|
-
return {
|
|
174
|
-
id: String(item.id),
|
|
175
|
-
slug: item.slug,
|
|
176
|
-
draft: item.draft,
|
|
177
|
-
fieldData,
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import yaml from "yaml";
|
|
2
|
-
function normalizeYamlValue(value) {
|
|
3
|
-
if (value instanceof Date) {
|
|
4
|
-
return value.toISOString();
|
|
5
|
-
}
|
|
6
|
-
if (Array.isArray(value)) {
|
|
7
|
-
return value.map(normalizeYamlValue);
|
|
8
|
-
}
|
|
9
|
-
if (value && typeof value === "object") {
|
|
10
|
-
const output = {};
|
|
11
|
-
for (const [key, nestedValue] of Object.entries(value)) {
|
|
12
|
-
if (nestedValue !== undefined) {
|
|
13
|
-
output[key] = normalizeYamlValue(nestedValue);
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
return output;
|
|
17
|
-
}
|
|
18
|
-
return value;
|
|
19
|
-
}
|
|
20
|
-
export function toFrontmatter(data) {
|
|
21
|
-
const filtered = {};
|
|
22
|
-
for (const [key, value] of Object.entries(data)) {
|
|
23
|
-
if (value === undefined) {
|
|
24
|
-
continue;
|
|
25
|
-
}
|
|
26
|
-
filtered[key] = normalizeYamlValue(value);
|
|
27
|
-
}
|
|
28
|
-
const rendered = yaml.stringify(filtered, {
|
|
29
|
-
lineWidth: 0,
|
|
30
|
-
singleQuote: true,
|
|
31
|
-
});
|
|
32
|
-
return `---\n${rendered.trimEnd()}\n---\n\n`;
|
|
33
|
-
}
|
|
34
|
-
export function parseFrontmatterDocument(content) {
|
|
35
|
-
const normalized = content.replace(/\r\n/g, "\n");
|
|
36
|
-
const match = normalized.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
37
|
-
if (!match) {
|
|
38
|
-
return { data: {}, body: normalized };
|
|
39
|
-
}
|
|
40
|
-
const [, rawFrontmatter, rawBody] = match;
|
|
41
|
-
const parsed = yaml.parse(rawFrontmatter);
|
|
42
|
-
const data = parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
43
|
-
? parsed
|
|
44
|
-
: {};
|
|
45
|
-
return {
|
|
46
|
-
data,
|
|
47
|
-
body: rawBody.replace(/^\n+/, ""),
|
|
48
|
-
};
|
|
49
|
-
}
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import rehypeParse from "rehype-parse";
|
|
2
|
-
import rehypeRemark from "rehype-remark";
|
|
3
|
-
import rehypeStringify from "rehype-stringify";
|
|
4
|
-
import remarkGfm from "remark-gfm";
|
|
5
|
-
import remarkParse from "remark-parse";
|
|
6
|
-
import remarkRehype from "remark-rehype";
|
|
7
|
-
import remarkStringify from "remark-stringify";
|
|
8
|
-
import { unified } from "unified";
|
|
9
|
-
const htmlToMarkdownProcessor = unified()
|
|
10
|
-
.use(rehypeParse, { fragment: true })
|
|
11
|
-
.use(rehypeRemark)
|
|
12
|
-
.use(remarkGfm, {
|
|
13
|
-
tablePipeAlign: true,
|
|
14
|
-
tableCellPadding: true,
|
|
15
|
-
})
|
|
16
|
-
.use(remarkStringify, {
|
|
17
|
-
bullet: "-",
|
|
18
|
-
emphasis: "_",
|
|
19
|
-
fences: true,
|
|
20
|
-
setext: false,
|
|
21
|
-
});
|
|
22
|
-
const markdownToHtmlProcessor = unified()
|
|
23
|
-
.use(remarkParse)
|
|
24
|
-
.use(remarkGfm)
|
|
25
|
-
.use(remarkRehype)
|
|
26
|
-
.use(rehypeStringify, { closeSelfClosing: true });
|
|
27
|
-
function normalizeFragment(markdown) {
|
|
28
|
-
return markdown.trim().replace(/\n{3,}/g, "\n\n");
|
|
29
|
-
}
|
|
30
|
-
function stripMarkdownComments(markdown) {
|
|
31
|
-
return markdown.replace(/<!--[\s\S]*?-->\s*/g, "").trim();
|
|
32
|
-
}
|
|
33
|
-
export async function htmlToMarkdown(html) {
|
|
34
|
-
const input = html.trim();
|
|
35
|
-
if (!input) {
|
|
36
|
-
return "";
|
|
37
|
-
}
|
|
38
|
-
const file = await htmlToMarkdownProcessor.process(input);
|
|
39
|
-
const markdown = normalizeFragment(String(file));
|
|
40
|
-
return markdown.length > 0 ? `${markdown}\n` : "";
|
|
41
|
-
}
|
|
42
|
-
export async function markdownToHtml(markdown) {
|
|
43
|
-
const input = stripMarkdownComments(markdown);
|
|
44
|
-
if (!input) {
|
|
45
|
-
return "";
|
|
46
|
-
}
|
|
47
|
-
const file = await markdownToHtmlProcessor.process(input);
|
|
48
|
-
return String(file).trim();
|
|
49
|
-
}
|
|
50
|
-
export function splitMarkdownIntoSections(markdown) {
|
|
51
|
-
const normalized = markdown.replace(/\r\n/g, "\n").trim();
|
|
52
|
-
if (!normalized) {
|
|
53
|
-
return [];
|
|
54
|
-
}
|
|
55
|
-
const lines = normalized.split("\n");
|
|
56
|
-
const sections = [];
|
|
57
|
-
let current = [];
|
|
58
|
-
for (const line of lines) {
|
|
59
|
-
if (/^##\s+/.test(line) && current.length > 0) {
|
|
60
|
-
sections.push(current.join("\n").trim());
|
|
61
|
-
current = [line];
|
|
62
|
-
continue;
|
|
63
|
-
}
|
|
64
|
-
current.push(line);
|
|
65
|
-
}
|
|
66
|
-
if (current.length > 0) {
|
|
67
|
-
sections.push(current.join("\n").trim());
|
|
68
|
-
}
|
|
69
|
-
return sections.filter((section) => section.length > 0);
|
|
70
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "@jest/globals";
|
|
2
|
-
import { containsFaqHeading, parseFaqEntries, renderFaqMarkdown, } from "../faq";
|
|
3
|
-
describe("faq", () => {
|
|
4
|
-
it("parses faq json entries", () => {
|
|
5
|
-
const entries = parseFaqEntries(JSON.stringify([
|
|
6
|
-
{ question: "What is this?", answer: "An FAQ." },
|
|
7
|
-
{ question: "", answer: "ignored" },
|
|
8
|
-
]));
|
|
9
|
-
expect(entries).toEqual([{ question: "What is this?", answer: "An FAQ." }]);
|
|
10
|
-
});
|
|
11
|
-
it("renders faq markdown", () => {
|
|
12
|
-
const markdown = renderFaqMarkdown([
|
|
13
|
-
{ question: "What is this?", answer: "An FAQ." },
|
|
14
|
-
]);
|
|
15
|
-
expect(markdown).toContain("## FAQ");
|
|
16
|
-
expect(markdown).toContain("### What is this?");
|
|
17
|
-
expect(markdown).toContain("An FAQ.");
|
|
18
|
-
});
|
|
19
|
-
it("detects existing faq headings", () => {
|
|
20
|
-
expect(containsFaqHeading("## FAQ\n\nHello\n")).toBe(true);
|
|
21
|
-
expect(containsFaqHeading("# Not FAQ\n")).toBe(false);
|
|
22
|
-
});
|
|
23
|
-
});
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
const globals_1 = require("@jest/globals");
|
|
4
|
-
const framer_ref_1 = require("../framer-ref");
|
|
5
|
-
(0, globals_1.describe)("framer ref", () => {
|
|
6
|
-
(0, globals_1.it)("extracts the project id from a framer project url", () => {
|
|
7
|
-
(0, globals_1.expect)((0, framer_ref_1.normalizeFramerProjectRef)("https://framer.com/projects/NEXT-Production-v2--JCo5wuC0juGVNOJSk41P-dcViX")).toBe("JCo5wuC0juGVNOJSk41P-dcViX");
|
|
8
|
-
});
|
|
9
|
-
(0, globals_1.it)("keeps a bare project id unchanged", () => {
|
|
10
|
-
(0, globals_1.expect)((0, framer_ref_1.normalizeFramerProjectRef)("JCo5wuC0juGVNOJSk41P-dcViX")).toBe("JCo5wuC0juGVNOJSk41P-dcViX");
|
|
11
|
-
});
|
|
12
|
-
});
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
function isRecord(value) {
|
|
2
|
-
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
3
|
-
}
|
|
4
|
-
export function parseFaqEntries(value) {
|
|
5
|
-
if (typeof value !== "string" || value.trim().length === 0) {
|
|
6
|
-
return [];
|
|
7
|
-
}
|
|
8
|
-
let parsed;
|
|
9
|
-
try {
|
|
10
|
-
parsed = JSON.parse(value);
|
|
11
|
-
}
|
|
12
|
-
catch {
|
|
13
|
-
return [];
|
|
14
|
-
}
|
|
15
|
-
if (!Array.isArray(parsed)) {
|
|
16
|
-
return [];
|
|
17
|
-
}
|
|
18
|
-
return parsed.flatMap((entry) => {
|
|
19
|
-
if (!isRecord(entry)) {
|
|
20
|
-
return [];
|
|
21
|
-
}
|
|
22
|
-
const question = typeof entry.question === "string" ? entry.question.trim() : "";
|
|
23
|
-
const answer = typeof entry.answer === "string" ? entry.answer.trim() : "";
|
|
24
|
-
if (!question || !answer) {
|
|
25
|
-
return [];
|
|
26
|
-
}
|
|
27
|
-
return [{ question, answer }];
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
export function containsFaqHeading(markdown, heading = "FAQ") {
|
|
31
|
-
const escapedHeading = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
32
|
-
const pattern = new RegExp(`^#{2,6}\\s+${escapedHeading}\\s*$`, "im");
|
|
33
|
-
return pattern.test(markdown);
|
|
34
|
-
}
|
|
35
|
-
export function renderFaqMarkdown(entries, heading = "FAQ") {
|
|
36
|
-
if (entries.length === 0) {
|
|
37
|
-
return "";
|
|
38
|
-
}
|
|
39
|
-
const lines = [`## ${heading}`, ""];
|
|
40
|
-
for (const entry of entries) {
|
|
41
|
-
lines.push(`### ${entry.question}`, "", entry.answer, "");
|
|
42
|
-
}
|
|
43
|
-
return `${lines.join("\n").trimEnd()}\n`;
|
|
44
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.normalizeFramerProjectRef = normalizeFramerProjectRef;
|
|
4
|
-
function extractFramerId(value) {
|
|
5
|
-
const trimmed = value.trim();
|
|
6
|
-
const parts = trimmed.split("--");
|
|
7
|
-
if (parts.length > 1) {
|
|
8
|
-
return parts[parts.length - 1] || trimmed;
|
|
9
|
-
}
|
|
10
|
-
return trimmed;
|
|
11
|
-
}
|
|
12
|
-
function normalizeFramerProjectRef(input) {
|
|
13
|
-
const trimmed = input.trim();
|
|
14
|
-
if (!trimmed) {
|
|
15
|
-
return trimmed;
|
|
16
|
-
}
|
|
17
|
-
try {
|
|
18
|
-
const parsed = new URL(trimmed);
|
|
19
|
-
const lastPathSegment = parsed.pathname.split("/").filter(Boolean).pop();
|
|
20
|
-
if (!lastPathSegment) {
|
|
21
|
-
return trimmed;
|
|
22
|
-
}
|
|
23
|
-
return extractFramerId(lastPathSegment);
|
|
24
|
-
}
|
|
25
|
-
catch {
|
|
26
|
-
return extractFramerId(trimmed);
|
|
27
|
-
}
|
|
28
|
-
}
|
package/dist/src/frontmatter.js
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.toFrontmatter = toFrontmatter;
|
|
7
|
-
const yaml_1 = __importDefault(require("yaml"));
|
|
8
|
-
function toFrontmatter(data) {
|
|
9
|
-
const filtered = {};
|
|
10
|
-
for (const [key, value] of Object.entries(data)) {
|
|
11
|
-
if (value === undefined) {
|
|
12
|
-
continue;
|
|
13
|
-
}
|
|
14
|
-
filtered[key] = normalizeYamlValue(value);
|
|
15
|
-
}
|
|
16
|
-
const yaml = yaml_1.default.stringify(filtered, {
|
|
17
|
-
lineWidth: 0,
|
|
18
|
-
singleQuote: true,
|
|
19
|
-
});
|
|
20
|
-
return `---\n${yaml.trimEnd()}\n---\n\n`;
|
|
21
|
-
}
|
|
22
|
-
function normalizeYamlValue(value) {
|
|
23
|
-
if (value instanceof Date) {
|
|
24
|
-
return value.toISOString();
|
|
25
|
-
}
|
|
26
|
-
if (Array.isArray(value)) {
|
|
27
|
-
return value.map(normalizeYamlValue);
|
|
28
|
-
}
|
|
29
|
-
if (value && typeof value === "object") {
|
|
30
|
-
const output = {};
|
|
31
|
-
for (const [key, nestedValue] of Object.entries(value)) {
|
|
32
|
-
if (nestedValue !== undefined) {
|
|
33
|
-
output[key] = normalizeYamlValue(nestedValue);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
return output;
|
|
37
|
-
}
|
|
38
|
-
return value;
|
|
39
|
-
}
|
package/dist/src/index.js
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
-
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
-
};
|
|
16
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
__exportStar(require("./cli"), exports);
|
|
18
|
-
__exportStar(require("./export"), exports);
|
|
19
|
-
__exportStar(require("./framer-client"), exports);
|
|
20
|
-
__exportStar(require("./frontmatter"), exports);
|
|
21
|
-
__exportStar(require("./markdown"), exports);
|
|
22
|
-
__exportStar(require("./filename"), exports);
|