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.
Files changed (38) hide show
  1. package/.github/workflows/ci.yml +17 -0
  2. package/.github/workflows/npm-publish.yml +26 -0
  3. package/dist/src/framer-to-markdown/cli.js +1 -0
  4. package/dist/src/framer-to-markdown/config.js +1 -1
  5. package/dist/src/framer-to-markdown/export.js +3 -1
  6. package/dist/src/framer-to-markdown/frontmatter.js +1 -1
  7. package/dist/src/libs/framer/framer-client.js +12 -4
  8. package/dist/src/markdown-to-framer/__tests__/import.spec.js +1 -4
  9. package/dist/src/markdown-to-framer/__tests__/json-ld-faq.spec.js +99 -0
  10. package/dist/src/markdown-to-framer/cli.js +1 -0
  11. package/dist/src/markdown-to-framer/import.js +13 -3
  12. package/dist/src/markdown-to-framer/index.js +1 -1
  13. package/dist/src/markdown-to-framer/json-ld-faq.js +96 -0
  14. package/package.json +4 -4
  15. package/src/markdown-to-framer/cli.ts +2 -0
  16. package/dist/src/__tests__/filename.spec.js +0 -15
  17. package/dist/src/__tests__/frontmatter.spec.js +0 -15
  18. package/dist/src/__tests__/markdown.spec.js +0 -9
  19. package/dist/src/cli.js +0 -62
  20. package/dist/src/export.js +0 -83
  21. package/dist/src/filename.js +0 -42
  22. package/dist/src/framer-client.js +0 -106
  23. package/dist/src/framer-sync/config.js +0 -50
  24. package/dist/src/framer-sync/filename.js +0 -41
  25. package/dist/src/framer-sync/format.js +0 -9
  26. package/dist/src/framer-sync/framer-client.js +0 -180
  27. package/dist/src/framer-sync/frontmatter.js +0 -49
  28. package/dist/src/framer-sync/markdown.js +0 -70
  29. package/dist/src/framer-to-markdown/__tests__/faq.spec.js +0 -23
  30. package/dist/src/framer-to-markdown/__tests__/framer-ref.spec.js +0 -12
  31. package/dist/src/framer-to-markdown/faq.js +0 -44
  32. package/dist/src/framer-to-markdown/framer-ref.js +0 -28
  33. package/dist/src/frontmatter.js +0 -39
  34. package/dist/src/index.js +0 -22
  35. package/dist/src/markdown.js +0 -21
  36. package/dist/test/filename.test.js +0 -19
  37. package/dist/test/frontmatter.test.js +0 -19
  38. package/dist/test/markdown.test.js +0 -13
@@ -0,0 +1,17 @@
1
+ name: CI
2
+
3
+ on: pull_request
4
+
5
+ jobs:
6
+ ci:
7
+ runs-on: ubuntu-latest
8
+ steps:
9
+ - uses: actions/checkout@v6
10
+ - uses: actions/setup-node@v6
11
+ with:
12
+ node-version: '20'
13
+ registry-url: 'https://registry.npmjs.org'
14
+ scope: '@collaborne'
15
+ - run: npm ci --unsafe-perm
16
+ - run: npm run build
17
+ - run: npm test
@@ -0,0 +1,26 @@
1
+ name: Publish to NPM
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - '*'
7
+
8
+ permissions:
9
+ id-token: write # Required for npmjs publish (OIDC)
10
+ contents: read
11
+
12
+ jobs:
13
+ publish-npm:
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@v6
17
+ - uses: actions/setup-node@v6
18
+ with:
19
+ node-version: '20'
20
+ registry-url: 'https://registry.npmjs.org'
21
+ scope: '@collaborne'
22
+ - run: npm ci --unsafe-perm
23
+ - run: npm run build
24
+ - run: npm test
25
+ # npmjs requires more recent npm version than Github Action runs by default
26
+ - run: npx -y npm@^11.5.1 publish
@@ -1,3 +1,4 @@
1
+ #!/usr/bin/env node
1
2
  import { runCli } from "./index";
2
3
  await runCli().catch((error) => {
3
4
  console.error(error instanceof Error ? error.message : error);
@@ -1 +1 @@
1
- export { loadConfig } from "../libs/framer/config";
1
+ export { loadConfig, } from "../libs/framer/config";
@@ -115,7 +115,9 @@ export function renderMarkdownBody(item, bodyFieldName) {
115
115
  .concat(sectionFields.length > 0 ? "\n" : ""));
116
116
  }
117
117
  const bodyField = resolveBodyField(item, bodyFieldName);
118
- return bodyField ? renderBodyContent(item.fieldData[bodyField]) : Promise.resolve("");
118
+ return bodyField
119
+ ? renderBodyContent(item.fieldData[bodyField])
120
+ : Promise.resolve("");
119
121
  }
120
122
  export async function exportItemsToMarkdown(items, options = {}) {
121
123
  const outputDir = options.outputDir ??
@@ -1 +1 @@
1
- export { parseFrontmatterDocument, toFrontmatter } from "../libs/framer/frontmatter";
1
+ export { parseFrontmatterDocument, toFrontmatter, } from "../libs/framer/frontmatter";
@@ -54,9 +54,13 @@ export function createFieldDataEntryInput(field, value, options = {}) {
54
54
  ? { type: field.type, value }
55
55
  : undefined;
56
56
  case "number":
57
- return typeof value === "number" ? { type: field.type, value } : undefined;
57
+ return typeof value === "number"
58
+ ? { type: field.type, value }
59
+ : undefined;
58
60
  case "string":
59
- return typeof value === "string" ? { type: field.type, value } : undefined;
61
+ return typeof value === "string"
62
+ ? { type: field.type, value }
63
+ : undefined;
60
64
  case "formattedText":
61
65
  return typeof value === "string"
62
66
  ? {
@@ -66,9 +70,13 @@ export function createFieldDataEntryInput(field, value, options = {}) {
66
70
  }
67
71
  : undefined;
68
72
  case "enum":
69
- return typeof value === "string" ? { type: field.type, value } : undefined;
73
+ return typeof value === "string"
74
+ ? { type: field.type, value }
75
+ : undefined;
70
76
  case "link":
71
- return typeof value === "string" ? { type: field.type, value } : undefined;
77
+ return typeof value === "string"
78
+ ? { type: field.type, value }
79
+ : undefined;
72
80
  case "date":
73
81
  return typeof value === "string" || typeof value === "number"
74
82
  ? { type: field.type, value: value }
@@ -30,10 +30,7 @@ describe("markdown-to-framer import", () => {
30
30
  field("s02-title-id", "Section 02 Title", "string"),
31
31
  field("s01-title-id", "Section 01 Title", "string"),
32
32
  ];
33
- expect(resolveSectionHtmlFields(fields).map((entry) => entry.name)).toEqual([
34
- "Section 01 HTML",
35
- "Section 02 HTML",
36
- ]);
33
+ expect(resolveSectionHtmlFields(fields).map((entry) => entry.name)).toEqual(["Section 01 HTML", "Section 02 HTML"]);
37
34
  const item = await buildCollectionItemInput(document, fields, {
38
35
  markdown: "sections",
39
36
  metadata: ["Title", "Excerpt"],
@@ -0,0 +1,99 @@
1
+ import { markdownFaqToJsonLd } from "../json-ld-faq";
2
+ describe("markdownFaqToJsonLdScript", () => {
3
+ it("returns null when no FAQ section exists", () => {
4
+ const markdown = `
5
+ # Introduction
6
+
7
+ Some content.
8
+
9
+ ## Features
10
+
11
+ More content.
12
+ `;
13
+ expect(markdownFaqToJsonLd(markdown)).toBeNull();
14
+ });
15
+ it("converts a FAQ section into JSON-LD", () => {
16
+ const markdown = `
17
+ # Article
18
+
19
+ Some text.
20
+
21
+ ## FAQ
22
+
23
+ ### Question 1?
24
+
25
+ Answer 1.
26
+
27
+ ### Question 2?
28
+
29
+ Answer 2.
30
+ `;
31
+ const json = markdownFaqToJsonLd(markdown);
32
+ expect(json).not.toBeNull();
33
+ expect(json).toEqual({
34
+ "@context": "https://schema.org",
35
+ "@type": "FAQPage",
36
+ mainEntity: [
37
+ {
38
+ "@type": "Question",
39
+ name: "Question 1?",
40
+ acceptedAnswer: {
41
+ "@type": "Answer",
42
+ text: "Answer 1.",
43
+ },
44
+ },
45
+ {
46
+ "@type": "Question",
47
+ name: "Question 2?",
48
+ acceptedAnswer: {
49
+ "@type": "Answer",
50
+ text: "Answer 2.",
51
+ },
52
+ },
53
+ ],
54
+ });
55
+ });
56
+ it("supports FAQ at any heading level", () => {
57
+ const markdown = `
58
+ # Page
59
+
60
+ ### FAQ
61
+
62
+ #### What is NEXT?
63
+
64
+ A platform for customer intelligence.
65
+ `;
66
+ const json = markdownFaqToJsonLd(markdown);
67
+ expect(json.mainEntity).toHaveLength(1);
68
+ expect(json.mainEntity[0].name).toBe("What is NEXT?");
69
+ });
70
+ it("stops parsing at the next sibling section", () => {
71
+ const markdown = `
72
+ ## FAQ
73
+
74
+ ### Question 1?
75
+
76
+ Answer 1.
77
+
78
+ ## Pricing
79
+
80
+ ### Should not be included
81
+
82
+ Answer 2.
83
+ `;
84
+ const json = markdownFaqToJsonLd(markdown);
85
+ expect(json.mainEntity).toHaveLength(1);
86
+ expect(json.mainEntity[0].name).toBe("Question 1?");
87
+ });
88
+ it("normalizes markdown inside answers", () => {
89
+ const markdown = `
90
+ ## FAQ
91
+
92
+ ### What is it?
93
+
94
+ This is **bold**, *italic*, and a [link](https://example.com).
95
+ `;
96
+ const json = markdownFaqToJsonLd(markdown);
97
+ expect(json.mainEntity[0].acceptedAnswer.text).toBe("This is bold, italic, and a link.");
98
+ });
99
+ });
@@ -1,3 +1,4 @@
1
+ #!/usr/bin/env node
1
2
  import { runCli } from "./index";
2
3
  await runCli().catch((error) => {
3
4
  console.error(error instanceof Error ? error.message : error);
@@ -1,9 +1,10 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { createFieldDataEntryInput, } from "../libs/framer/framer-client";
4
3
  import { slugify } from "../libs/framer/filename";
5
- import { markdownToHtml, splitMarkdownIntoSections, } from "../libs/framer/markdown";
4
+ import { createFieldDataEntryInput, } from "../libs/framer/framer-client";
6
5
  import { parseFrontmatterDocument } from "../libs/framer/frontmatter";
6
+ import { markdownToHtml, splitMarkdownIntoSections, } from "../libs/framer/markdown";
7
+ import { markdownFaqToJsonLd } from "./json-ld-faq";
7
8
  const SECTION_HTML_FIELD_PATTERN = /^Section\s+(\d+)\s+HTML$/i;
8
9
  const SECTION_TITLE_FIELD_PATTERN = /^Section\s+(\d+)\s+Title$/i;
9
10
  export async function listMarkdownFiles(rootDir) {
@@ -36,7 +37,8 @@ export async function readMarkdownDocuments(contentDir) {
36
37
  export function resolveSectionHtmlFields(fields) {
37
38
  return fields
38
39
  .filter((field) => {
39
- return field.type === "formattedText" && SECTION_HTML_FIELD_PATTERN.test(field.name);
40
+ return (field.type === "formattedText" &&
41
+ SECTION_HTML_FIELD_PATTERN.test(field.name));
40
42
  })
41
43
  .sort((left, right) => {
42
44
  const leftOrder = Number.parseInt(left.name.match(SECTION_HTML_FIELD_PATTERN)?.[1] ?? "0", 10);
@@ -63,6 +65,14 @@ export function extractSectionTitle(markdown) {
63
65
  export async function buildFieldDataInput(document, fields, config) {
64
66
  const fieldByName = new Map(fields.map((field) => [field.name, field]));
65
67
  const fieldData = {};
68
+ const jsonLd = markdownFaqToJsonLd(document.body);
69
+ if (jsonLd) {
70
+ const field = fieldByName.get("JSON-LD");
71
+ fieldData[field.id] = {
72
+ type: "string",
73
+ value: JSON.stringify(jsonLd),
74
+ };
75
+ }
66
76
  for (const metadataFieldName of config.metadata) {
67
77
  const field = fieldByName.get(metadataFieldName);
68
78
  if (!field) {
@@ -4,7 +4,7 @@ import yargs from "yargs";
4
4
  import { hideBin } from "yargs/helpers";
5
5
  import { loadConfig } from "../libs/framer/config";
6
6
  import { FramerClient, } from "../libs/framer/framer-client";
7
- import { buildCollectionItemInput, readMarkdownDocuments, } from "./import";
7
+ import { buildCollectionItemInput, readMarkdownDocuments } from "./import";
8
8
  export async function runCli(argv = hideBin(process.argv)) {
9
9
  const args = await yargs(argv)
10
10
  .scriptName("markdown-to-framer")
@@ -0,0 +1,96 @@
1
+ function parseHeading(line) {
2
+ const match = /^(#{1,6})\s+(.+?)\s*#*\s*$/.exec(line);
3
+ if (!match) {
4
+ return null;
5
+ }
6
+ return {
7
+ level: match[1].length,
8
+ text: match[2].trim(),
9
+ };
10
+ }
11
+ function normalizeMarkdownText(markdown) {
12
+ return markdown
13
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
14
+ .replace(/[*_`>#-]/g, "")
15
+ .replace(/\s+/g, " ")
16
+ .trim();
17
+ }
18
+ function findFaqHeading(lines) {
19
+ for (let i = 0; i < lines.length; i++) {
20
+ const heading = parseHeading(lines[i]);
21
+ if (heading && heading.text.trim().toLowerCase() === "faq") {
22
+ return { index: i, level: heading.level };
23
+ }
24
+ }
25
+ return null;
26
+ }
27
+ function collectSection(lines, startIndex, parentLevel) {
28
+ const section = [];
29
+ for (let i = startIndex; i < lines.length; i++) {
30
+ const heading = parseHeading(lines[i]);
31
+ if (heading && heading.level <= parentLevel) {
32
+ break;
33
+ }
34
+ section.push(lines[i]);
35
+ }
36
+ return section;
37
+ }
38
+ function extractQuestions(lines, faqLevel) {
39
+ const items = [];
40
+ let currentQuestion = null;
41
+ let currentAnswerLines = [];
42
+ const flush = () => {
43
+ if (!currentQuestion) {
44
+ return;
45
+ }
46
+ const answer = normalizeMarkdownText(currentAnswerLines.join("\n"));
47
+ if (answer.length > 0) {
48
+ items.push({
49
+ question: currentQuestion,
50
+ answer,
51
+ });
52
+ }
53
+ };
54
+ for (const line of lines) {
55
+ const heading = parseHeading(line);
56
+ if (heading && heading.level > faqLevel) {
57
+ flush();
58
+ currentQuestion = heading.text;
59
+ currentAnswerLines = [];
60
+ }
61
+ else if (currentQuestion) {
62
+ currentAnswerLines.push(line);
63
+ }
64
+ }
65
+ flush();
66
+ return items;
67
+ }
68
+ function extractFaqItems(markdown) {
69
+ const lines = markdown.split(/\r?\n/);
70
+ const faqStart = findFaqHeading(lines);
71
+ if (!faqStart) {
72
+ return [];
73
+ }
74
+ const { index: faqIndex, level: faqLevel } = faqStart;
75
+ const faqLines = collectSection(lines, faqIndex + 1, faqLevel);
76
+ return extractQuestions(faqLines, faqLevel);
77
+ }
78
+ export function markdownFaqToJsonLd(markdown) {
79
+ const items = extractFaqItems(markdown);
80
+ if (items.length === 0) {
81
+ return null;
82
+ }
83
+ const jsonLd = {
84
+ "@context": "https://schema.org",
85
+ "@type": "FAQPage",
86
+ mainEntity: items.map((item) => ({
87
+ "@type": "Question",
88
+ name: item.question,
89
+ acceptedAnswer: {
90
+ "@type": "Answer",
91
+ text: item.answer,
92
+ },
93
+ })),
94
+ };
95
+ return jsonLd;
96
+ }
package/package.json CHANGED
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "framer-cms-markdown",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "Sync's Framer CMS content from/to markdown",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",
7
7
  "bin": {
8
- "framer-to-markdown": "dist/src/framer-to-markdown/cli.js",
9
- "markdown-to-framer": "dist/src/markdown-to-framer/cli.js"
8
+ "framer-to-markdown": "./dist/src/framer-to-markdown/cli.js",
9
+ "markdown-to-framer": "./dist/src/markdown-to-framer/cli.js"
10
10
  },
11
11
  "scripts": {
12
12
  "build": "tsc -p tsconfig.json",
13
13
  "lint": "eslint --fix \"**/*.ts\"",
14
- "test": "npm run test:unit",
14
+ "test": "npm run lint && npm run test:unit",
15
15
  "test:unit": "NODE_OPTIONS='--experimental-vm-modules --max_old_space_size=3072' jest --maxWorkers=50%",
16
16
  "framer-to-markdown": "AWS_SDK_LOAD_CONFIG=1 tsx -r source-map-support/register src/framer-to-markdown/cli.ts",
17
17
  "markdown-to-framer": "AWS_SDK_LOAD_CONFIG=1 tsx -r source-map-support/register src/markdown-to-framer/cli.ts"
@@ -1,3 +1,5 @@
1
+ #!/usr/bin/env node
2
+
1
3
  import { runCli } from "./index";
2
4
 
3
5
  await runCli().catch((error) => {
@@ -1,15 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- const globals_1 = require("@jest/globals");
4
- const filename_1 = require("../filename");
5
- (0, globals_1.test)("slugify normalizes text", () => {
6
- (0, globals_1.expect)((0, filename_1.slugify)("Hello, Framer CMS!")).toBe("hello-framer-cms");
7
- });
8
- (0, globals_1.test)("uniqueName adds suffixes", () => {
9
- const existing = new Set(["post", "post-2"]);
10
- (0, globals_1.expect)((0, filename_1.uniqueName)("post", existing)).toBe("post-3");
11
- });
12
- (0, globals_1.test)("resolveMarkdownFilename prefers slug", () => {
13
- const existing = new Set();
14
- (0, globals_1.expect)((0, filename_1.resolveMarkdownFilename)({ slug: "hello-world" }, existing)).toBe("hello-world.md");
15
- });
@@ -1,15 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- const globals_1 = require("@jest/globals");
4
- const frontmatter_1 = require("../frontmatter");
5
- (0, globals_1.test)("toFrontmatter serializes yaml frontmatter", () => {
6
- const output = (0, frontmatter_1.toFrontmatter)({
7
- title: "Hello",
8
- tags: ["a", "b"],
9
- published: true,
10
- });
11
- (0, globals_1.expect)(output).toMatch(/^---\n/);
12
- (0, globals_1.expect)(output).toMatch(/title: Hello/);
13
- (0, globals_1.expect)(output).toMatch(/tags:/);
14
- (0, globals_1.expect)(output).toMatch(/published: true/);
15
- });
@@ -1,9 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- const globals_1 = require("@jest/globals");
4
- const markdown_1 = require("../markdown");
5
- (0, globals_1.test)("htmlToMarkdown converts simple html", () => {
6
- const output = (0, markdown_1.htmlToMarkdown)("<h1>Hello</h1><p>World</p>");
7
- (0, globals_1.expect)(output).toMatch(/^# Hello/);
8
- (0, globals_1.expect)(output).toMatch(/World/);
9
- });
package/dist/src/cli.js DELETED
@@ -1,62 +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.runCli = runCli;
7
- const node_path_1 = __importDefault(require("node:path"));
8
- // eslint-disable-next-line @typescript-eslint/no-var-requires
9
- const yargs = require("yargs");
10
- // eslint-disable-next-line @typescript-eslint/no-var-requires
11
- const { hideBin } = require("yargs/helpers");
12
- const export_1 = require("./export");
13
- const framer_client_1 = require("./framer-client");
14
- async function runCli(argv = hideBin(process.argv)) {
15
- const args = await yargs(argv)
16
- .scriptName("framer-to-markdown")
17
- .usage("$0 --token <token> --site <site-id> --collection <collection-id>")
18
- .option("token", {
19
- type: "string",
20
- demandOption: true,
21
- describe: "Framer API token",
22
- })
23
- .option("site", {
24
- type: "string",
25
- demandOption: true,
26
- describe: "Framer site/project identifier",
27
- })
28
- .option("collection", {
29
- type: "string",
30
- demandOption: true,
31
- describe: "Framer CMS collection identifier",
32
- })
33
- .option("baseUrl", {
34
- type: "string",
35
- default: "https://api.framer.com",
36
- describe: "Framer API base URL",
37
- })
38
- .strict()
39
- .help()
40
- .parse();
41
- const cliArgs = args;
42
- const client = new framer_client_1.FramerClient({
43
- token: cliArgs.token,
44
- baseUrl: cliArgs.baseUrl,
45
- });
46
- const collection = await client.resolveCollection(cliArgs.site, cliArgs.collection);
47
- const items = await client.listCollectionItems(cliArgs.site, collection.id);
48
- const outputDir = node_path_1.default.join(process.cwd(), "content", "framer");
49
- const writtenFiles = await (0, export_1.exportItemsToMarkdown)(items, {
50
- outputRoot: outputDir,
51
- collectionName: collection.slug ?? collection.name ?? collection.id,
52
- });
53
- for (const file of writtenFiles) {
54
- console.log(file);
55
- }
56
- }
57
- if (require.main === module) {
58
- runCli().catch((error) => {
59
- console.error(error instanceof Error ? error.message : error);
60
- process.exitCode = 1;
61
- });
62
- }
@@ -1,83 +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.exportItemsToMarkdown = exportItemsToMarkdown;
7
- exports.resolveBodyField = resolveBodyField;
8
- exports.buildFrontmatter = buildFrontmatter;
9
- const promises_1 = __importDefault(require("node:fs/promises"));
10
- const node_path_1 = __importDefault(require("node:path"));
11
- const filename_1 = require("./filename");
12
- const frontmatter_1 = require("./frontmatter");
13
- const markdown_1 = require("./markdown");
14
- const BODY_FIELD_CANDIDATES = ["content", "body", "html", "richText", "rich_text"];
15
- async function exportItemsToMarkdown(items, options = {}) {
16
- const outputRoot = options.outputRoot ?? node_path_1.default.join(process.cwd(), "content", "framer");
17
- const collectionFolder = (0, filename_1.slugify)(options.collectionName ?? "collection");
18
- const outputDir = node_path_1.default.join(outputRoot, collectionFolder);
19
- await promises_1.default.mkdir(outputDir, { recursive: true });
20
- const usedNames = new Set();
21
- const writtenFiles = [];
22
- for (const item of items) {
23
- const filename = (0, filename_1.resolveMarkdownFilename)(item, usedNames);
24
- const filepath = node_path_1.default.join(outputDir, filename);
25
- const bodyField = resolveBodyField(item);
26
- const frontmatter = buildFrontmatter(item, bodyField);
27
- const markdownBody = bodyField ? (0, markdown_1.htmlToMarkdown)(stringValue(item[bodyField])) : "";
28
- const fileContents = `${(0, frontmatter_1.toFrontmatter)(frontmatter)}${markdownBody}`;
29
- await promises_1.default.writeFile(filepath, fileContents, "utf8");
30
- writtenFiles.push(filepath);
31
- }
32
- return writtenFiles;
33
- }
34
- function resolveBodyField(item) {
35
- for (const candidate of BODY_FIELD_CANDIDATES) {
36
- if (typeof item[candidate] === "string" && stringValue(item[candidate]).trim().length > 0) {
37
- return candidate;
38
- }
39
- }
40
- for (const [key, value] of Object.entries(item)) {
41
- if (typeof value === "string" && looksLikeHtml(value)) {
42
- return key;
43
- }
44
- }
45
- return undefined;
46
- }
47
- function buildFrontmatter(item, bodyField) {
48
- const frontmatter = {};
49
- for (const [key, value] of Object.entries(item)) {
50
- if (key === bodyField) {
51
- continue;
52
- }
53
- if (value === undefined || typeof value === "function" || typeof value === "symbol") {
54
- continue;
55
- }
56
- frontmatter[key] = normalizeFrontmatterValue(value);
57
- }
58
- return frontmatter;
59
- }
60
- function normalizeFrontmatterValue(value) {
61
- if (value instanceof Date) {
62
- return value.toISOString();
63
- }
64
- if (Array.isArray(value)) {
65
- return value.map(normalizeFrontmatterValue);
66
- }
67
- if (value && typeof value === "object") {
68
- const record = {};
69
- for (const [key, nestedValue] of Object.entries(value)) {
70
- if (nestedValue !== undefined) {
71
- record[key] = normalizeFrontmatterValue(nestedValue);
72
- }
73
- }
74
- return record;
75
- }
76
- return value;
77
- }
78
- function stringValue(value) {
79
- return typeof value === "string" ? value : "";
80
- }
81
- function looksLikeHtml(value) {
82
- return /<\/?[a-z][\s\S]*>/i.test(value);
83
- }
@@ -1,42 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.slugify = slugify;
4
- exports.uniqueName = uniqueName;
5
- exports.resolveMarkdownFilename = resolveMarkdownFilename;
6
- function slugify(input) {
7
- const normalized = input
8
- .normalize("NFKD")
9
- .replace(/[\u0300-\u036f]/g, "")
10
- .toLowerCase()
11
- .replace(/[^a-z0-9]+/g, "-")
12
- .replace(/^-+|-+$/g, "")
13
- .replace(/-{2,}/g, "-");
14
- return normalized.length > 0 ? normalized : "item";
15
- }
16
- function uniqueName(baseName, existingNames) {
17
- if (!existingNames.has(baseName)) {
18
- return baseName;
19
- }
20
- let suffix = 2;
21
- while (existingNames.has(`${baseName}-${suffix}`)) {
22
- suffix += 1;
23
- }
24
- return `${baseName}-${suffix}`;
25
- }
26
- function resolveMarkdownFilename(item, existingNames = new Set()) {
27
- const candidates = [item.slug, item.title, item.id != null ? String(item.id) : null];
28
- for (const candidate of candidates) {
29
- if (!candidate) {
30
- continue;
31
- }
32
- const name = slugify(candidate);
33
- if (name) {
34
- const unique = uniqueName(name, existingNames);
35
- existingNames.add(unique);
36
- return `${unique}.md`;
37
- }
38
- }
39
- const fallback = uniqueName("item", existingNames);
40
- existingNames.add(fallback);
41
- return `${fallback}.md`;
42
- }