@fullstackdatasolutions/articles 0.8.2 → 0.10.0
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/CHANGELOG.md +237 -0
- package/README.md +209 -78
- package/dist/index.cjs +635 -274
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +164 -56
- package/dist/index.d.ts +164 -56
- package/dist/index.js +614 -250
- package/dist/index.js.map +1 -1
- package/dist/nextjs.cjs +113 -38
- package/dist/nextjs.cjs.map +1 -1
- package/dist/nextjs.d.cts +71 -0
- package/dist/nextjs.d.ts +71 -0
- package/dist/nextjs.js +113 -38
- package/dist/nextjs.js.map +1 -1
- package/dist/server.cjs +394 -52
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +96 -6
- package/dist/server.d.ts +96 -6
- package/dist/server.js +382 -52
- package/dist/server.js.map +1 -1
- package/package.json +8 -5
- package/src/ArticleContent.tsx +8 -3
- package/src/ArticleDetailHero.tsx +27 -2
- package/src/ArticleSchemas.tsx +27 -27
- package/src/AuthorArticlesPage.tsx +60 -0
- package/src/AuthorCard.tsx +112 -0
- package/src/AuthorDetailHero.tsx +56 -0
- package/src/Breadcrumb.tsx +78 -0
- package/src/CategoryArticlesPage.tsx +62 -11
- package/src/__tests__/ArticleContent.test.tsx +18 -2
- package/src/__tests__/ArticleDetailHero.test.tsx +21 -1
- package/src/__tests__/ArticleSchemas.test.tsx +47 -2
- package/src/__tests__/AuthorArticlesPage.test.tsx +74 -0
- package/src/__tests__/AuthorCard.test.tsx +98 -0
- package/src/__tests__/AuthorDetailHero.test.tsx +51 -0
- package/src/__tests__/CategoryArticlesPage.test.tsx +31 -5
- package/src/__tests__/authorUtils.test.ts +89 -0
- package/src/__tests__/markdown.test.ts +79 -3
- package/src/__tests__/renderMdx.test.tsx +57 -0
- package/src/__tests__/seoUtils-authors.test.ts +160 -0
- package/src/__tests__/seoUtils.test.ts +106 -0
- package/src/__tests__/server-articles.test.ts +174 -3
- package/src/articleTypes.ts +33 -0
- package/src/articlesConfig.ts +67 -0
- package/src/authorUtils.ts +95 -0
- package/src/index.ts +32 -9
- package/src/markdown.ts +67 -8
- package/src/renderMdx.tsx +6 -3
- package/src/seoUtils.ts +279 -6
- package/src/server-articles.ts +124 -34
- package/src/server.ts +21 -2
package/dist/nextjs.js
CHANGED
|
@@ -70,6 +70,48 @@ function reportArticlesError(report) {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
// src/markdown.ts
|
|
73
|
+
var DEFAULT_LINK_TARGET_STRATEGY = "external-new-tab";
|
|
74
|
+
function isNonBrowserNavigationLink(href) {
|
|
75
|
+
return /^[a-zA-Z][a-zA-Z\d+.-]*:/.test(href) && !href.startsWith("http://") && !href.startsWith("https://");
|
|
76
|
+
}
|
|
77
|
+
function getOrigin(url) {
|
|
78
|
+
if (!url) return null;
|
|
79
|
+
try {
|
|
80
|
+
return new URL(url).origin;
|
|
81
|
+
} catch (e) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function isExternalHttpLink(href, siteUrl) {
|
|
86
|
+
if (!href.startsWith("http://") && !href.startsWith("https://")) return false;
|
|
87
|
+
const siteOrigin = getOrigin(siteUrl);
|
|
88
|
+
if (!siteOrigin) return true;
|
|
89
|
+
return getOrigin(href) !== siteOrigin;
|
|
90
|
+
}
|
|
91
|
+
function shouldOpenInNewTab(href, options = {}) {
|
|
92
|
+
var _a;
|
|
93
|
+
if (!href || href.startsWith("#") || isNonBrowserNavigationLink(href)) return false;
|
|
94
|
+
const strategy = (_a = options.strategy) != null ? _a : DEFAULT_LINK_TARGET_STRATEGY;
|
|
95
|
+
if (strategy === "same-tab") return false;
|
|
96
|
+
if (strategy === "all-new-tab") return true;
|
|
97
|
+
return isExternalHttpLink(href, options.siteUrl);
|
|
98
|
+
}
|
|
99
|
+
function applyLinkTarget(props, options) {
|
|
100
|
+
const href = typeof props.href === "string" ? props.href : "";
|
|
101
|
+
if (shouldOpenInNewTab(href, options)) {
|
|
102
|
+
props.target = "_blank";
|
|
103
|
+
props.rel = "noopener noreferrer";
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
delete props.target;
|
|
107
|
+
delete props.rel;
|
|
108
|
+
}
|
|
109
|
+
function getLinkTargetOptions(config) {
|
|
110
|
+
return {
|
|
111
|
+
strategy: config == null ? void 0 : config.linkTargetStrategy,
|
|
112
|
+
siteUrl: config == null ? void 0 : config.siteUrl
|
|
113
|
+
};
|
|
114
|
+
}
|
|
73
115
|
function sanitizeImagePath(rawPath, articleSlug) {
|
|
74
116
|
if (!rawPath || typeof rawPath !== "string") {
|
|
75
117
|
return null;
|
|
@@ -191,7 +233,7 @@ function processFootnotesSection(node) {
|
|
|
191
233
|
node.children = [hr, h3, ol];
|
|
192
234
|
if (node.properties) node.properties.className = void 0;
|
|
193
235
|
}
|
|
194
|
-
var customRenderer = () => {
|
|
236
|
+
var customRenderer = (linkTargetOptions = {}) => {
|
|
195
237
|
return (tree) => {
|
|
196
238
|
visit(tree, "element", (node) => {
|
|
197
239
|
if (node.tagName) {
|
|
@@ -214,11 +256,7 @@ var customRenderer = () => {
|
|
|
214
256
|
break;
|
|
215
257
|
case "a": {
|
|
216
258
|
props.className = "text-primary hover:underline transition-colors duration-200";
|
|
217
|
-
|
|
218
|
-
if (!href.startsWith("#")) {
|
|
219
|
-
props.target = "_blank";
|
|
220
|
-
props.rel = "noopener noreferrer";
|
|
221
|
-
}
|
|
259
|
+
applyLinkTarget(props, linkTargetOptions);
|
|
222
260
|
break;
|
|
223
261
|
}
|
|
224
262
|
case "ul":
|
|
@@ -288,10 +326,10 @@ var rehypeProcessImages = (options = {}) => {
|
|
|
288
326
|
});
|
|
289
327
|
};
|
|
290
328
|
};
|
|
291
|
-
function markdownToHtml(markdown, articleSlug) {
|
|
329
|
+
function markdownToHtml(markdown, articleSlug, config) {
|
|
292
330
|
return __async(this, null, function* () {
|
|
293
331
|
try {
|
|
294
|
-
let processor = remark().use(remarkParse).use(remarkGfm).use(remarkGithubBlockquoteAlert).use(remarkRehype).use(customRenderer).use(rehypeSlug).use(rehypePrism).use(rehypeSanitize, {
|
|
332
|
+
let processor = remark().use(remarkParse).use(remarkGfm).use(remarkGithubBlockquoteAlert).use(remarkRehype).use(customRenderer, getLinkTargetOptions(config)).use(rehypeSlug).use(rehypePrism).use(rehypeSanitize, {
|
|
295
333
|
attributes: {
|
|
296
334
|
"*": ["className", "class", "id"],
|
|
297
335
|
a: ["href", "target", "rel", "id"],
|
|
@@ -467,7 +505,41 @@ function parseHowToSteps(raw) {
|
|
|
467
505
|
);
|
|
468
506
|
return steps.length ? steps : void 0;
|
|
469
507
|
}
|
|
470
|
-
function
|
|
508
|
+
function parseAuthors(raw) {
|
|
509
|
+
if (!Array.isArray(raw)) return void 0;
|
|
510
|
+
const authors = raw.filter(
|
|
511
|
+
(author) => typeof author === "string" && author.trim().length > 0
|
|
512
|
+
);
|
|
513
|
+
return authors;
|
|
514
|
+
}
|
|
515
|
+
function getAuthorBySlug(slug, config) {
|
|
516
|
+
var _a, _b;
|
|
517
|
+
const profile = (_a = config.authors) == null ? void 0 : _a[slug];
|
|
518
|
+
if (!profile) return null;
|
|
519
|
+
return __spreadProps(__spreadValues({}, profile), {
|
|
520
|
+
url: (_b = profile.url) != null ? _b : `${config.siteUrl.replace(/\/$/, "")}/articles/authors/${profile.slug}`
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
function getConfiguredAuthorByName(name, config) {
|
|
524
|
+
var _a;
|
|
525
|
+
const normalizedName = name.trim().toLowerCase();
|
|
526
|
+
const profile = Object.values((_a = config.authors) != null ? _a : {}).find(
|
|
527
|
+
(author) => author.name.toLowerCase() === normalizedName
|
|
528
|
+
);
|
|
529
|
+
return profile ? getAuthorBySlug(profile.slug, config) : null;
|
|
530
|
+
}
|
|
531
|
+
function resolveArticleAuthorName(rawAuthor, rawAuthors, config) {
|
|
532
|
+
var _a, _b, _c, _d;
|
|
533
|
+
const authorArray = parseAuthors(rawAuthors);
|
|
534
|
+
const firstAuthor = authorArray == null ? void 0 : authorArray[0];
|
|
535
|
+
const rawAuthorValue = typeof rawAuthor === "string" && rawAuthor.trim() ? rawAuthor : void 0;
|
|
536
|
+
const author = firstAuthor != null ? firstAuthor : rawAuthorValue;
|
|
537
|
+
const resolved = author != null ? author : config == null ? void 0 : config.defaultAuthor;
|
|
538
|
+
if (!resolved) return "";
|
|
539
|
+
if (!config) return resolved;
|
|
540
|
+
return (_d = (_c = (_a = getAuthorBySlug(resolved, config)) == null ? void 0 : _a.name) != null ? _c : (_b = getConfiguredAuthorByName(resolved, config)) == null ? void 0 : _b.name) != null ? _d : resolved;
|
|
541
|
+
}
|
|
542
|
+
function getArticleSummary(slug, config) {
|
|
471
543
|
return __async(this, null, function* () {
|
|
472
544
|
try {
|
|
473
545
|
const found = findArticleFile(slug);
|
|
@@ -483,7 +555,8 @@ function getArticleSummary(slug) {
|
|
|
483
555
|
excerpt: data.excerpt || "",
|
|
484
556
|
date: parseDateField(data.date),
|
|
485
557
|
lastmod: parseDateField(data.lastmod),
|
|
486
|
-
author: data.author
|
|
558
|
+
author: resolveArticleAuthorName(data.author, data.authors, config),
|
|
559
|
+
authors: parseAuthors(data.authors),
|
|
487
560
|
category: categories[0],
|
|
488
561
|
categories,
|
|
489
562
|
readTime,
|
|
@@ -509,36 +582,38 @@ function getArticleSummary(slug) {
|
|
|
509
582
|
}
|
|
510
583
|
});
|
|
511
584
|
}
|
|
512
|
-
var getArticleMetadata = cache(
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
585
|
+
var getArticleMetadata = cache(
|
|
586
|
+
(slug, config) => __async(void 0, null, function* () {
|
|
587
|
+
try {
|
|
588
|
+
const summary = yield getArticleSummary(slug, config);
|
|
589
|
+
if (!summary) return null;
|
|
590
|
+
const found = findArticleFile(slug);
|
|
591
|
+
if (!found) return null;
|
|
592
|
+
const fileContent = fs.readFileSync(found.filePath, "utf8");
|
|
593
|
+
const { content: markdownContent } = matter(fileContent);
|
|
594
|
+
const toc = yield extractToc(markdownContent);
|
|
595
|
+
let htmlContent;
|
|
596
|
+
let mdxSource;
|
|
597
|
+
if (found.contentType === "mdx") {
|
|
598
|
+
mdxSource = markdownContent;
|
|
599
|
+
} else {
|
|
600
|
+
htmlContent = yield markdownToHtml(markdownContent, slug, config);
|
|
601
|
+
}
|
|
602
|
+
return __spreadProps(__spreadValues({}, summary), { content: markdownContent, htmlContent, mdxSource, toc });
|
|
603
|
+
} catch (error) {
|
|
604
|
+
reportArticlesError({
|
|
605
|
+
code: "article-load-failed",
|
|
606
|
+
message: "Unable to load article metadata.",
|
|
607
|
+
error,
|
|
608
|
+
context: { slug }
|
|
609
|
+
});
|
|
610
|
+
return null;
|
|
527
611
|
}
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
code: "article-load-failed",
|
|
532
|
-
message: "Unable to load article metadata.",
|
|
533
|
-
error,
|
|
534
|
-
context: { slug }
|
|
535
|
-
});
|
|
536
|
-
return null;
|
|
537
|
-
}
|
|
538
|
-
}));
|
|
539
|
-
var getAllArticles = cache(() => __async(void 0, null, function* () {
|
|
612
|
+
})
|
|
613
|
+
);
|
|
614
|
+
var getAllArticles = cache((config) => __async(void 0, null, function* () {
|
|
540
615
|
const slugs = getAvailableArticleSlugs();
|
|
541
|
-
const articles = yield Promise.all(slugs.map((slug) => getArticleSummary(slug)));
|
|
616
|
+
const articles = yield Promise.all(slugs.map((slug) => getArticleSummary(slug, config)));
|
|
542
617
|
const currentDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
543
618
|
return articles.filter((article) => article !== null).filter((article) => !article.date || article.date <= currentDate).filter((article) => !(article.draft && process.env.NODE_ENV === "production")).sort((a, b) => {
|
|
544
619
|
if (!a.date && !b.date) return 0;
|
package/dist/nextjs.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/server-articles.ts","../src/markdown.ts","../src/errorReporting.ts","../src/nextjs.ts"],"sourcesContent":["import { cache } from 'react'\nimport matter from 'gray-matter'\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport readingTime from 'reading-time'\nimport { markdownToHtml, extractToc } from './markdown'\nimport type { Article, CategoryInfo, FaqItem, HowToStep } from './articleTypes'\nimport type { ArticlesConfig } from './articlesConfig'\nimport { reportArticlesError } from './errorReporting'\n\nconst articlesDirectory = path.join(/* turbopackIgnore: true */ process.cwd(), 'public/articles')\n\nfunction getReadingTime(content: string): string {\n return readingTime(content).text\n}\n\nfunction findArticleImage(slug: string): string | null {\n try {\n const articleDir = path.join(articlesDirectory, slug)\n const entries: (fs.Dirent | string)[] = fs.readdirSync(articleDir, { withFileTypes: true })\n const names = entries\n .map((entry) => {\n if (typeof entry === 'string') return entry\n if (entry && typeof entry.name === 'string') return entry.name\n return null\n })\n .filter((name): name is string => Boolean(name))\n const imageExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.webp']\n const imageFileName = names.find((name) =>\n imageExtensions.some((ext) => name.toLowerCase().endsWith(ext))\n )\n return imageFileName ? sanitizeImagePath(imageFileName, slug) : null\n } catch {\n return null\n }\n}\n\nfunction sanitizeImagePath(rawPath: string, articleSlug: string): string | null {\n if (!rawPath || typeof rawPath !== 'string') return null\n const cleanPath = rawPath.replaceAll(/[\\x00-\\x1f\\x7f-\\x9f]/g, '')\n if (cleanPath.startsWith('http://') || cleanPath.startsWith('https://')) return cleanPath\n if (cleanPath.includes('..') || cleanPath.includes('\\\\') || cleanPath.startsWith('/')) {\n reportArticlesError({\n code: 'unsafe-image-path',\n message: 'Rejected unsafe article image path.',\n context: { articleSlug, path: cleanPath },\n })\n return null\n }\n if (!/^[a-zA-Z0-9._/-]+$/.test(cleanPath)) {\n reportArticlesError({\n code: 'unsafe-image-path',\n message: 'Rejected article image path with invalid characters.',\n context: { articleSlug, path: cleanPath },\n })\n return null\n }\n if (cleanPath.includes('/')) {\n const normalizedPath = path.normalize(cleanPath)\n if (normalizedPath.startsWith('..') || normalizedPath.includes('../')) {\n reportArticlesError({\n code: 'unsafe-image-path',\n message: 'Rejected article image path traversal attempt.',\n context: { articleSlug, path: cleanPath },\n })\n return null\n }\n return `/articles/${articleSlug}/${cleanPath}`\n }\n return `/articles/${articleSlug}/${cleanPath}`\n}\n\nfunction findArticleFile(slug: string): { filePath: string; contentType: 'md' | 'mdx' } | null {\n const mdPath = path.join(articlesDirectory, slug, 'article.md')\n const mdxPath = path.join(articlesDirectory, slug, 'article.mdx')\n if (fs.existsSync(mdPath)) return { filePath: mdPath, contentType: 'md' }\n if (fs.existsSync(mdxPath)) return { filePath: mdxPath, contentType: 'mdx' }\n return null\n}\n\nexport function getAvailableArticleSlugs(): string[] {\n try {\n if (!fs.existsSync(articlesDirectory)) return []\n return walkArticleDir(articlesDirectory, '')\n } catch (error) {\n reportArticlesError({\n code: 'article-directory-read-failed',\n message: 'Unable to read articles directory.',\n error,\n context: { directory: articlesDirectory },\n })\n return []\n }\n}\n\nfunction walkArticleDir(dir: string, baseSlug: string): string[] {\n const slugs: string[] = []\n try {\n const items = fs.readdirSync(dir, { withFileTypes: true })\n for (const item of items) {\n if (!item.isDirectory()) continue\n const slug = baseSlug ? `${baseSlug}/${item.name}` : item.name\n if (findArticleFile(slug) !== null) {\n slugs.push(slug)\n }\n slugs.push(...walkArticleDir(path.join(dir, item.name), slug))\n }\n } catch {\n // ignore unreadable directories\n }\n return slugs\n}\n\nfunction parseDateField(rawDate: unknown): string | undefined {\n if (!rawDate) return undefined\n try {\n const parsed = new Date(rawDate as string)\n if (!Number.isNaN(parsed.getTime())) return parsed.toISOString().split('T')[0]\n } catch {\n // ignore invalid dates\n }\n return undefined\n}\n\nfunction resolveFeaturedImage(rawImage: unknown, slug: string): string {\n const img = typeof rawImage === 'string' ? rawImage : ''\n if (img && !img.startsWith('http')) return sanitizeImagePath(img, slug) || '/placeholder-logo.png'\n if (!img) return findArticleImage(slug) || '/placeholder-logo.png'\n return img\n}\n\nfunction parseFaqItems(raw: unknown): FaqItem[] | undefined {\n if (!Array.isArray(raw)) return undefined\n const items = raw.filter(\n (item): item is FaqItem =>\n typeof item === 'object' &&\n item !== null &&\n typeof (item as FaqItem).question === 'string' &&\n typeof (item as FaqItem).answer === 'string'\n )\n return items.length ? items : undefined\n}\n\nfunction parseHowToSteps(raw: unknown): HowToStep[] | undefined {\n if (!Array.isArray(raw)) return undefined\n const steps = raw.filter(\n (item): item is HowToStep =>\n typeof item === 'object' &&\n item !== null &&\n typeof (item as HowToStep).name === 'string' &&\n typeof (item as HowToStep).text === 'string'\n )\n return steps.length ? steps : undefined\n}\n\nasync function getArticleSummary(slug: string): Promise<Article | null> {\n try {\n const found = findArticleFile(slug)\n if (!found) return null\n const fileContent = fs.readFileSync(found.filePath, 'utf8')\n const { data, content: markdownContent } = matter(fileContent)\n const readTime = getReadingTime(markdownContent)\n const allTags: string[] = Array.isArray(data.tags)\n ? data.tags.filter((t: unknown) => typeof t === 'string' && String(t).trim())\n : []\n const categories: string[] =\n allTags.length > 0 ? allTags.map((t: string) => t.replaceAll('-', ' ').trim()) : ['Campaigns']\n return {\n slug,\n title: data.title || slug.replaceAll('-', ' '),\n excerpt: data.excerpt || '',\n date: parseDateField(data.date),\n lastmod: parseDateField(data.lastmod),\n author: data.author || 'Andrew Blase',\n category: categories[0],\n categories,\n readTime,\n featuredImage: resolveFeaturedImage(data.featuredImage, slug),\n tags: data.tags || [],\n contentType: found.contentType,\n draft: data.draft === true,\n faq: parseFaqItems(data.faq),\n howTo: parseHowToSteps(data.howTo),\n canonicalUrl: typeof data.canonicalUrl === 'string' ? data.canonicalUrl : undefined,\n articleType: typeof data.articleType === 'string' ? data.articleType : undefined,\n series: typeof data.series === 'string' ? data.series : undefined,\n aiCrawl: data.aiCrawl === true,\n }\n } catch (error) {\n reportArticlesError({\n code: 'article-load-failed',\n message: 'Unable to load article summary.',\n error,\n context: { slug },\n })\n return null\n }\n}\n\nexport const getArticleMetadata = cache(async (slug: string): Promise<Article | null> => {\n try {\n const summary = await getArticleSummary(slug)\n if (!summary) return null\n const found = findArticleFile(slug)\n if (!found) return null\n const fileContent = fs.readFileSync(found.filePath, 'utf8')\n const { content: markdownContent } = matter(fileContent)\n const toc = await extractToc(markdownContent)\n let htmlContent: string | undefined\n let mdxSource: string | undefined\n if (found.contentType === 'mdx') {\n mdxSource = markdownContent\n } else {\n htmlContent = await markdownToHtml(markdownContent, slug)\n }\n return { ...summary, content: markdownContent, htmlContent, mdxSource, toc }\n } catch (error) {\n reportArticlesError({\n code: 'article-load-failed',\n message: 'Unable to load article metadata.',\n error,\n context: { slug },\n })\n return null\n }\n})\n\nexport const getAllArticles = cache(async (): Promise<Article[]> => {\n const slugs = getAvailableArticleSlugs()\n const articles = await Promise.all(slugs.map((slug) => getArticleSummary(slug)))\n const currentDate = new Date().toISOString().split('T')[0]\n return articles\n .filter((article): article is Article => article !== null)\n .filter((article) => !article.date || article.date <= currentDate)\n .filter((article) => !(article.draft && process.env.NODE_ENV === 'production'))\n .sort((a, b) => {\n if (!a.date && !b.date) return 0\n if (!a.date) return 1\n if (!b.date) return -1\n return new Date(b.date).getTime() - new Date(a.date).getTime()\n })\n})\n\nexport async function getAdjacentArticles(\n currentSlug: string\n): Promise<{ previous: Article | null; next: Article | null }> {\n const allArticles = await getAllArticles()\n const currentIndex = allArticles.findIndex((article) => article.slug === currentSlug)\n if (currentIndex === -1) return { previous: null, next: null }\n const previous = currentIndex < allArticles.length - 1 ? allArticles[currentIndex + 1] : null\n const next = currentIndex > 0 ? allArticles[currentIndex - 1] : null\n return { previous, next }\n}\n\nexport async function getArticleMarkdown(slug: string): Promise<string | null> {\n try {\n const summary = await getArticleSummary(slug)\n if (!summary?.aiCrawl) return null\n const found = findArticleFile(slug)\n if (!found) return null\n const fileContent = fs.readFileSync(found.filePath, 'utf8')\n const { content: markdownContent } = matter(fileContent)\n return markdownContent\n } catch (error) {\n reportArticlesError({\n code: 'article-markdown-load-failed',\n message: 'Unable to load article markdown.',\n error,\n context: { slug },\n })\n return null\n }\n}\n\nexport async function getArticleMarkdownResponse(\n slug: string,\n config: ArticlesConfig\n): Promise<Response> {\n const markdown = await getArticleMarkdown(slug)\n if (markdown === null) return new Response('Not Found', { status: 404 })\n const article = await getArticleMetadata(slug)\n return new Response(markdown, {\n headers: {\n 'Content-Type': 'text/markdown; charset=utf-8',\n 'Cache-Control': 'public, max-age=3600, s-maxage=3600',\n ...(article ? getArticleAiHeaders(article, config) : {}),\n },\n })\n}\n\nexport function getArticleMarkdownUrl(\n article: Pick<Article, 'slug' | 'aiCrawl'>,\n config?: Pick<ArticlesConfig, 'siteUrl'>\n): string | undefined {\n if (article.aiCrawl !== true) return undefined\n const pathname = `/articles/${article.slug}.md`\n if (!config) return pathname\n return `${config.siteUrl.replace(/\\/$/, '')}${pathname}`\n}\n\nexport function getArticleAiHeaders(\n article: Pick<Article, 'slug' | 'aiCrawl'>,\n config?: Pick<ArticlesConfig, 'siteUrl'>\n): Record<string, string> {\n const markdownUrl = getArticleMarkdownUrl(article, config)\n if (markdownUrl) {\n return {\n Link: `<${markdownUrl}>; rel=\"alternate\"; type=\"text/markdown\"`,\n }\n }\n return {\n 'X-Robots-Tag': 'noai, noimageai',\n }\n}\n\nexport async function getAiRobotsTxtRules(): Promise<string> {\n const articles = await getAllArticles()\n const blockedArticles = articles.filter((article) => article.aiCrawl !== true)\n if (blockedArticles.length === 0) return ''\n\n const aiCrawlers = [\n 'GPTBot',\n 'ChatGPT-User',\n 'CCBot',\n 'ClaudeBot',\n 'Claude-User',\n 'PerplexityBot',\n 'Google-Extended',\n ]\n const disallowRules = blockedArticles\n .map((article) => `Disallow: /articles/${article.slug}`)\n .join('\\n')\n\n return aiCrawlers\n .map((crawler) => [`User-agent: ${crawler}`, disallowRules].join('\\n'))\n .join('\\n\\n')\n}\n\nexport async function searchArticles(query: string, config?: ArticlesConfig): Promise<Article[]> {\n if (!query?.trim()) return getAllArticles()\n const articles = await getAllArticles()\n const searchTerm = query.toLowerCase().trim()\n const includeAuthor = config?.showAuthor !== false\n return articles.filter((article) => {\n const matchesTitle = article.title.toLowerCase().includes(searchTerm)\n const matchesExcerpt = article.excerpt.toLowerCase().includes(searchTerm)\n const matchesAuthor = includeAuthor && article.author.toLowerCase().includes(searchTerm)\n const matchesCategory = article.categories.some((cat) => cat.toLowerCase().includes(searchTerm))\n const matchesTags = article.tags?.some((tag) => tag.toLowerCase().includes(searchTerm))\n return (\n matchesTitle || matchesExcerpt || matchesAuthor || matchesCategory || Boolean(matchesTags)\n )\n })\n}\n\nexport function categoryToSlug(category: string): string {\n return category\n .toLowerCase()\n .replaceAll(/\\s+/g, '-')\n .replaceAll(/[^a-z0-9-]/g, '')\n}\n\nexport async function getAllCategories(): Promise<CategoryInfo[]> {\n const articles = await getAllArticles()\n const categoryMap = new Map<string, { count: number; featuredImage: string }>()\n for (const article of articles) {\n for (const cat of article.categories) {\n if (!categoryMap.has(cat)) {\n categoryMap.set(cat, { count: 0, featuredImage: article.featuredImage })\n }\n categoryMap.get(cat)!.count++\n }\n }\n return Array.from(categoryMap.entries())\n .map(([name, { count, featuredImage }]) => ({\n name,\n slug: categoryToSlug(name),\n count,\n featuredImage,\n }))\n .sort((a, b) => b.count - a.count)\n}\n\nexport async function getArticlesByCategory(categorySlug: string): Promise<Article[]> {\n const articles = await getAllArticles()\n return articles.filter((article) =>\n article.categories.some((cat) => categoryToSlug(cat) === categorySlug)\n )\n}\n\nexport { sanitizeImagePath }\n","import type { Element, Root, ElementContent } from 'hast'\nimport rehypePrism from 'rehype-prism-plus'\nimport rehypeSanitize from 'rehype-sanitize'\nimport rehypeSlug from 'rehype-slug'\nimport rehypeStringify from 'rehype-stringify'\nimport { remark } from 'remark'\nimport remarkGfm from 'remark-gfm'\nimport remarkGithubBlockquoteAlert from 'remark-github-blockquote-alert'\nimport remarkParse from 'remark-parse'\nimport remarkRehype from 'remark-rehype'\nimport { Plugin } from 'unified'\nimport { visit } from 'unist-util-visit'\nimport type { TocItem } from './articleTypes'\nimport { reportArticlesError } from './errorReporting'\n\n// Import the sanitizeImagePath function\nfunction sanitizeImagePath(rawPath: string, articleSlug: string): string | null {\n if (!rawPath || typeof rawPath !== 'string') {\n return null\n }\n\n // Remove any null bytes or control characters\n const cleanPath = rawPath.replaceAll(/[\\x00-\\x1f\\x7f-\\x9f]/g, '')\n\n // Check for absolute URLs (http/https)\n if (cleanPath.startsWith('http://') || cleanPath.startsWith('https://')) {\n // For external URLs, just return as-is (they're safe)\n return cleanPath\n }\n\n // For relative paths, ensure they don't contain dangerous patterns\n if (cleanPath.includes('..') || cleanPath.includes('\\\\') || cleanPath.startsWith('/')) {\n reportArticlesError({\n code: 'unsafe-image-path',\n message: 'Rejected unsafe markdown image path.',\n context: { articleSlug, path: cleanPath },\n })\n return null\n }\n\n // Only allow alphanumeric characters, hyphens, underscores, dots, and forward slashes\n if (!/^[a-zA-Z0-9._/-]+$/.test(cleanPath)) {\n reportArticlesError({\n code: 'unsafe-image-path',\n message: 'Rejected markdown image path with invalid characters.',\n context: { articleSlug, path: cleanPath },\n })\n return null\n }\n\n // Construct safe path within articles directory\n if (cleanPath.includes('/')) {\n // Relative path, ensure it's within the article directory\n const normalizedPath = cleanPath.replaceAll('\\\\', '/')\n if (normalizedPath.startsWith('..') || normalizedPath.includes('../')) {\n reportArticlesError({\n code: 'unsafe-image-path',\n message: 'Rejected markdown image path traversal attempt.',\n context: { articleSlug, path: cleanPath },\n })\n return null\n }\n return `/articles/${articleSlug}/${normalizedPath}`\n } else {\n // Just a filename, construct full path\n return `/articles/${articleSlug}/${cleanPath}`\n }\n}\n\nfunction styleFootnoteLinks(nodes: ElementContent[]): void {\n nodes.forEach((n) => {\n if (n.type !== 'element') return\n const el = n as Element\n if (el.tagName === 'a') {\n const isBackRef =\n el.properties?.['dataFootnoteBackref'] !== undefined ||\n (el.children[0]?.type === 'text' && el.children[0].value === '↩')\n if (isBackRef) {\n el.properties.className = 'text-primary hover:underline ml-1'\n if (typeof el.properties.href === 'string') {\n el.properties.href = el.properties.href.replaceAll('#user-content-fnref-', '#ref-')\n }\n } else {\n el.properties.className = 'text-primary hover:underline break-all'\n }\n }\n if (el.children) styleFootnoteLinks(el.children)\n })\n}\n\nfunction processFootnoteRef(node: Element): void {\n if (\n node.tagName !== 'sup' ||\n node.children?.[0]?.type !== 'element' ||\n (node.children[0] as Element).tagName !== 'a'\n )\n return\n\n const link = node.children[0] as Element\n const href = link.properties?.href\n if (typeof href !== 'string' || !href.startsWith('#user-content-fn-')) return\n\n delete link.properties.target\n delete link.properties.rel\n link.properties.className = 'text-primary hover:underline'\n link.properties.href = href.replaceAll('#user-content-fn-', '#footnote-')\n\n if (typeof node.properties?.id === 'string') {\n link.properties.id = node.properties.id.replaceAll('user-content-fnref-', 'ref-')\n delete node.properties.id\n }\n\n if (link.children?.[0]?.type === 'text') {\n link.children[0].value = `[${link.children[0].value}]`\n }\n}\n\nfunction processFootnotesSection(node: Element): void {\n const cls = node.properties?.className\n const isFootnotes =\n node.tagName === 'section' &&\n (Array.isArray(cls) ? cls.includes('footnotes') : cls === 'footnotes')\n if (!isFootnotes) return\n\n const olCandidate = node.children.find(\n (child) => child.type === 'element' && (child as Element).tagName === 'ol'\n )\n if (olCandidate?.type !== 'element') return\n\n const ol = olCandidate as Element\n ol.properties.className = 'list-decimal ml-6 space-y-2 text-sm text-muted-foreground'\n\n ol.children.forEach((li) => {\n if (li.type !== 'element' || li.tagName !== 'li') return\n const liEl = li as Element\n\n if (liEl.properties) {\n liEl.properties.className = 'pl-2'\n if (typeof liEl.properties.id === 'string') {\n liEl.properties.id = liEl.properties.id.replaceAll('user-content-fn-', 'footnote-')\n }\n }\n\n const pIndex = liEl.children.findIndex(\n (child) => child.type === 'element' && (child as Element).tagName === 'p'\n )\n if (pIndex !== -1) {\n const p = liEl.children[pIndex] as Element\n liEl.children.splice(pIndex, 1, ...p.children)\n }\n\n styleFootnoteLinks(liEl.children)\n })\n\n const hr: Element = {\n type: 'element',\n tagName: 'hr',\n properties: { className: 'my-8 border-border' },\n children: [],\n }\n const h3: Element = {\n type: 'element',\n tagName: 'h3',\n properties: { className: 'text-lg font-semibold mb-4' },\n children: [{ type: 'text', value: 'References' }],\n }\n\n node.children = [hr, h3, ol]\n if (node.properties) node.properties.className = undefined\n}\n\nexport const customRenderer: Plugin<[], Root> = () => {\n return (tree: Root) => {\n // First pass: Apply general styles\n visit(tree, 'element', (node: Element) => {\n if (node.tagName) {\n const props = node.properties || {}\n\n switch (node.tagName) {\n case 'h1':\n props.className = 'text-3xl font-bold text-foreground mt-8 mb-4 scroll-mt-20'\n break\n case 'h2':\n props.className = 'text-2xl font-semibold text-foreground mt-6 mb-3 scroll-mt-20'\n break\n case 'h3':\n props.className = 'text-xl font-semibold text-foreground mt-4 mb-2 scroll-mt-20'\n break\n case 'h4':\n props.className = 'text-lg font-semibold text-foreground mt-3 mb-2 scroll-mt-20'\n break\n case 'p':\n props.className = 'text-muted-foreground leading-relaxed mb-4'\n break\n case 'a': {\n props.className = 'text-primary hover:underline transition-colors duration-200'\n const href = typeof props.href === 'string' ? props.href : ''\n if (!href.startsWith('#')) {\n props.target = '_blank'\n props.rel = 'noopener noreferrer'\n }\n break\n }\n case 'ul':\n props.className = 'list-disc list-inside mb-4 space-y-2 ml-4'\n break\n case 'ol':\n props.className = 'list-decimal list-inside mb-4 space-y-2 ml-4'\n break\n case 'li':\n props.className = 'mb-1'\n break\n case 'blockquote':\n props.className = 'border-l-4 border-primary pl-4 italic my-4 text-muted-foreground'\n break\n case 'code':\n props.className =\n (props.className ? props.className + ' ' : '') +\n 'bg-muted px-1 py-0.5 rounded text-sm font-mono'\n break\n case 'pre':\n props.className = 'bg-muted rounded-lg p-4 overflow-x-auto my-4'\n break\n case 'img':\n props.className = 'rounded-lg my-6 w-full max-w-2xl mx-auto'\n break\n case 'table':\n props.className = 'border-collapse border border-border my-4 w-full'\n break\n case 'th':\n props.className = 'border border-border px-2 py-1 bg-muted font-semibold'\n break\n case 'td':\n props.className = 'border border-border px-2 py-1'\n break\n case 'hr':\n props.className = 'my-8 border-border'\n break\n default:\n break\n }\n\n node.properties = props\n }\n })\n\n // Second pass: Fix footnotes and references\n visit(tree, 'element', (node: Element) => {\n processFootnoteRef(node)\n processFootnotesSection(node)\n })\n }\n}\n\n// Custom rehype plugin to process image URLs\nconst rehypeProcessImages: Plugin<[{ articleSlug?: string }], Root> = (options = {}) => {\n return (tree: Root) => {\n visit(tree, 'element', (node: Element) => {\n if (node.tagName === 'img' && node.properties) {\n const src = node.properties.src\n if (src && typeof src === 'string' && options.articleSlug) {\n // Sanitize the image path\n const sanitizedSrc = sanitizeImagePath(src, options.articleSlug)\n if (sanitizedSrc) {\n node.properties.src = sanitizedSrc\n } else {\n // If sanitization fails, use a placeholder\n reportArticlesError({\n code: 'unsafe-image-path',\n message: 'Using placeholder for unsafe markdown image path.',\n context: { articleSlug: options.articleSlug, path: src },\n })\n node.properties.src = '/placeholder-logo.png'\n }\n }\n }\n })\n }\n}\n\nexport async function markdownToHtml(markdown: string, articleSlug?: string) {\n try {\n // Start building the remark processor\n let processor = remark()\n .use(remarkParse)\n .use(remarkGfm)\n .use(remarkGithubBlockquoteAlert)\n .use(remarkRehype)\n .use(customRenderer)\n .use(rehypeSlug)\n // @ts-ignore\n .use(rehypePrism)\n .use(rehypeSanitize, {\n attributes: {\n '*': ['className', 'class', 'id'],\n a: ['href', 'target', 'rel', 'id'],\n img: ['src', 'alt'],\n },\n })\n\n // Add image processing plugin if articleSlug is provided\n if (articleSlug) {\n processor = processor.use(rehypeProcessImages, { articleSlug })\n }\n\n const result = await processor.use(rehypeStringify).process(markdown)\n\n return result.toString()\n } catch (error) {\n reportArticlesError({\n code: 'markdown-conversion-failed',\n message: 'Unable to convert markdown to HTML.',\n error,\n context: { articleSlug },\n })\n // Return the original markdown as fallback\n return markdown\n }\n}\n\nfunction nodeTextValue(c: ElementContent): string {\n return c.type === 'text' ? (c as { value: string }).value : ''\n}\n\nfunction extractHeadingItem(node: Element): TocItem | null {\n const match = /^h([1-6])$/.exec(node.tagName)\n if (!match) return null\n const id = typeof node.properties?.id === 'string' ? node.properties.id : ''\n const text = node.children.map(nodeTextValue).join('')\n if (!id || !text) return null\n return { id, depth: Number.parseInt(match[1], 10), text }\n}\n\nexport async function extractToc(markdown: string): Promise<TocItem[]> {\n const headings: TocItem[] = []\n const collectHeadings: Plugin<[], Root> = () => (tree: Root) => {\n visit(tree, 'element', (node: Element) => {\n const item = extractHeadingItem(node)\n if (item) headings.push(item)\n })\n }\n await remark()\n .use(remarkParse)\n .use(remarkGfm)\n .use(remarkRehype)\n .use(rehypeSlug)\n .use(collectHeadings)\n .use(rehypeStringify)\n .process(markdown)\n return headings\n}\n","export type ArticlesErrorCode =\n | 'article-directory-read-failed'\n | 'article-load-failed'\n | 'article-markdown-load-failed'\n | 'markdown-conversion-failed'\n | 'unsafe-image-path'\n\nexport type ArticlesErrorContext = Readonly<Record<string, string | number | boolean | undefined>>\n\nexport type ArticlesErrorReport = Readonly<{\n code: ArticlesErrorCode\n message: string\n error?: unknown\n context?: ArticlesErrorContext\n}>\n\nexport type ArticlesErrorHandler = (report: ArticlesErrorReport) => void\n\nfunction defaultArticlesErrorHandler(report: ArticlesErrorReport): void {\n if (process.env.NODE_ENV === 'production') return\n const context = report.context ? ` ${JSON.stringify(report.context)}` : ''\n const detail = report.error instanceof Error ? `: ${report.error.message}` : ''\n console.warn(`[articles:${report.code}] ${report.message}${context}${detail}`)\n}\n\nlet articlesErrorHandler: ArticlesErrorHandler = defaultArticlesErrorHandler\n\nexport function setArticlesErrorHandler(handler?: ArticlesErrorHandler): void {\n articlesErrorHandler = handler ?? defaultArticlesErrorHandler\n}\n\nexport function reportArticlesError(report: ArticlesErrorReport): void {\n articlesErrorHandler(report)\n}\n","// Next.js API route handler factory — Node.js runtime only\n// Import from '@fullstackdatasolutions/articles/nextjs'\nimport type { NextRequest } from 'next/server'\nimport { getArticleMarkdownResponse } from './server-articles'\nimport type { ArticlesConfig } from './articlesConfig'\n\n/**\n * Returns a Next.js App Router GET handler for serving article markdown.\n *\n * Usage in app/api/articles-markdown/[...slug]/route.ts:\n * import { createArticleMarkdownHandler } from '@fullstackdatasolutions/articles/nextjs'\n * import { siteConfig } from '@/config/articles'\n * export const { GET } = createArticleMarkdownHandler(siteConfig)\n */\nexport function createArticleMarkdownHandler(config: ArticlesConfig) {\n async function GET(\n _request: NextRequest,\n context: { params: Promise<Record<string, string | string[]>> }\n ) {\n const params = await context.params\n const slugParts = params['slug']\n const slug = Array.isArray(slugParts) ? slugParts.join('/') : (slugParts ?? '')\n const cleanSlug = slug.endsWith('.md') ? slug.slice(0, -3) : slug\n return getArticleMarkdownResponse(cleanSlug, config)\n }\n return { GET }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,SAAS,aAAa;AACtB,OAAO,YAAY;AACnB,OAAO,QAAQ;AACf,OAAO,UAAU;AACjB,OAAO,iBAAiB;;;ACHxB,OAAO,iBAAiB;AACxB,OAAO,oBAAoB;AAC3B,OAAO,gBAAgB;AACvB,OAAO,qBAAqB;AAC5B,SAAS,cAAc;AACvB,OAAO,eAAe;AACtB,OAAO,iCAAiC;AACxC,OAAO,iBAAiB;AACxB,OAAO,kBAAkB;AAEzB,SAAS,aAAa;;;ACOtB,SAAS,4BAA4B,QAAmC;AACtE,MAAI,QAAQ,IAAI,aAAa,aAAc;AAC3C,QAAM,UAAU,OAAO,UAAU,IAAI,KAAK,UAAU,OAAO,OAAO,CAAC,KAAK;AACxE,QAAM,SAAS,OAAO,iBAAiB,QAAQ,KAAK,OAAO,MAAM,OAAO,KAAK;AAC7E,UAAQ,KAAK,aAAa,OAAO,IAAI,KAAK,OAAO,OAAO,GAAG,OAAO,GAAG,MAAM,EAAE;AAC/E;AAEA,IAAI,uBAA6C;AAM1C,SAAS,oBAAoB,QAAmC;AACrE,uBAAqB,MAAM;AAC7B;;;ADjBA,SAAS,kBAAkB,SAAiB,aAAoC;AAC9E,MAAI,CAAC,WAAW,OAAO,YAAY,UAAU;AAC3C,WAAO;AAAA,EACT;AAGA,QAAM,YAAY,QAAQ,WAAW,yBAAyB,EAAE;AAGhE,MAAI,UAAU,WAAW,SAAS,KAAK,UAAU,WAAW,UAAU,GAAG;AAEvE,WAAO;AAAA,EACT;AAGA,MAAI,UAAU,SAAS,IAAI,KAAK,UAAU,SAAS,IAAI,KAAK,UAAU,WAAW,GAAG,GAAG;AACrF,wBAAoB;AAAA,MAClB,MAAM;AAAA,MACN,SAAS;AAAA,MACT,SAAS,EAAE,aAAa,MAAM,UAAU;AAAA,IAC1C,CAAC;AACD,WAAO;AAAA,EACT;AAGA,MAAI,CAAC,qBAAqB,KAAK,SAAS,GAAG;AACzC,wBAAoB;AAAA,MAClB,MAAM;AAAA,MACN,SAAS;AAAA,MACT,SAAS,EAAE,aAAa,MAAM,UAAU;AAAA,IAC1C,CAAC;AACD,WAAO;AAAA,EACT;AAGA,MAAI,UAAU,SAAS,GAAG,GAAG;AAE3B,UAAM,iBAAiB,UAAU,WAAW,MAAM,GAAG;AACrD,QAAI,eAAe,WAAW,IAAI,KAAK,eAAe,SAAS,KAAK,GAAG;AACrE,0BAAoB;AAAA,QAClB,MAAM;AAAA,QACN,SAAS;AAAA,QACT,SAAS,EAAE,aAAa,MAAM,UAAU;AAAA,MAC1C,CAAC;AACD,aAAO;AAAA,IACT;AACA,WAAO,aAAa,WAAW,IAAI,cAAc;AAAA,EACnD,OAAO;AAEL,WAAO,aAAa,WAAW,IAAI,SAAS;AAAA,EAC9C;AACF;AAEA,SAAS,mBAAmB,OAA+B;AACzD,QAAM,QAAQ,CAAC,MAAM;AAtEvB;AAuEI,QAAI,EAAE,SAAS,UAAW;AAC1B,UAAM,KAAK;AACX,QAAI,GAAG,YAAY,KAAK;AACtB,YAAM,cACJ,QAAG,eAAH,mBAAgB,4BAA2B,YAC1C,QAAG,SAAS,CAAC,MAAb,mBAAgB,UAAS,UAAU,GAAG,SAAS,CAAC,EAAE,UAAU;AAC/D,UAAI,WAAW;AACb,WAAG,WAAW,YAAY;AAC1B,YAAI,OAAO,GAAG,WAAW,SAAS,UAAU;AAC1C,aAAG,WAAW,OAAO,GAAG,WAAW,KAAK,WAAW,wBAAwB,OAAO;AAAA,QACpF;AAAA,MACF,OAAO;AACL,WAAG,WAAW,YAAY;AAAA,MAC5B;AAAA,IACF;AACA,QAAI,GAAG,SAAU,oBAAmB,GAAG,QAAQ;AAAA,EACjD,CAAC;AACH;AAEA,SAAS,mBAAmB,MAAqB;AA1FjD;AA2FE,MACE,KAAK,YAAY,WACjB,gBAAK,aAAL,mBAAgB,OAAhB,mBAAoB,UAAS,aAC5B,KAAK,SAAS,CAAC,EAAc,YAAY;AAE1C;AAEF,QAAM,OAAO,KAAK,SAAS,CAAC;AAC5B,QAAM,QAAO,UAAK,eAAL,mBAAiB;AAC9B,MAAI,OAAO,SAAS,YAAY,CAAC,KAAK,WAAW,mBAAmB,EAAG;AAEvE,SAAO,KAAK,WAAW;AACvB,SAAO,KAAK,WAAW;AACvB,OAAK,WAAW,YAAY;AAC5B,OAAK,WAAW,OAAO,KAAK,WAAW,qBAAqB,YAAY;AAExE,MAAI,SAAO,UAAK,eAAL,mBAAiB,QAAO,UAAU;AAC3C,SAAK,WAAW,KAAK,KAAK,WAAW,GAAG,WAAW,uBAAuB,MAAM;AAChF,WAAO,KAAK,WAAW;AAAA,EACzB;AAEA,QAAI,gBAAK,aAAL,mBAAgB,OAAhB,mBAAoB,UAAS,QAAQ;AACvC,SAAK,SAAS,CAAC,EAAE,QAAQ,IAAI,KAAK,SAAS,CAAC,EAAE,KAAK;AAAA,EACrD;AACF;AAEA,SAAS,wBAAwB,MAAqB;AArHtD;AAsHE,QAAM,OAAM,UAAK,eAAL,mBAAiB;AAC7B,QAAM,cACJ,KAAK,YAAY,cAChB,MAAM,QAAQ,GAAG,IAAI,IAAI,SAAS,WAAW,IAAI,QAAQ;AAC5D,MAAI,CAAC,YAAa;AAElB,QAAM,cAAc,KAAK,SAAS;AAAA,IAChC,CAAC,UAAU,MAAM,SAAS,aAAc,MAAkB,YAAY;AAAA,EACxE;AACA,OAAI,2CAAa,UAAS,UAAW;AAErC,QAAM,KAAK;AACX,KAAG,WAAW,YAAY;AAE1B,KAAG,SAAS,QAAQ,CAAC,OAAO;AAC1B,QAAI,GAAG,SAAS,aAAa,GAAG,YAAY,KAAM;AAClD,UAAM,OAAO;AAEb,QAAI,KAAK,YAAY;AACnB,WAAK,WAAW,YAAY;AAC5B,UAAI,OAAO,KAAK,WAAW,OAAO,UAAU;AAC1C,aAAK,WAAW,KAAK,KAAK,WAAW,GAAG,WAAW,oBAAoB,WAAW;AAAA,MACpF;AAAA,IACF;AAEA,UAAM,SAAS,KAAK,SAAS;AAAA,MAC3B,CAAC,UAAU,MAAM,SAAS,aAAc,MAAkB,YAAY;AAAA,IACxE;AACA,QAAI,WAAW,IAAI;AACjB,YAAM,IAAI,KAAK,SAAS,MAAM;AAC9B,WAAK,SAAS,OAAO,QAAQ,GAAG,GAAG,EAAE,QAAQ;AAAA,IAC/C;AAEA,uBAAmB,KAAK,QAAQ;AAAA,EAClC,CAAC;AAED,QAAM,KAAc;AAAA,IAClB,MAAM;AAAA,IACN,SAAS;AAAA,IACT,YAAY,EAAE,WAAW,qBAAqB;AAAA,IAC9C,UAAU,CAAC;AAAA,EACb;AACA,QAAM,KAAc;AAAA,IAClB,MAAM;AAAA,IACN,SAAS;AAAA,IACT,YAAY,EAAE,WAAW,6BAA6B;AAAA,IACtD,UAAU,CAAC,EAAE,MAAM,QAAQ,OAAO,aAAa,CAAC;AAAA,EAClD;AAEA,OAAK,WAAW,CAAC,IAAI,IAAI,EAAE;AAC3B,MAAI,KAAK,WAAY,MAAK,WAAW,YAAY;AACnD;AAEO,IAAM,iBAAmC,MAAM;AACpD,SAAO,CAAC,SAAe;AAErB,UAAM,MAAM,WAAW,CAAC,SAAkB;AACxC,UAAI,KAAK,SAAS;AAChB,cAAM,QAAQ,KAAK,cAAc,CAAC;AAElC,gBAAQ,KAAK,SAAS;AAAA,UACpB,KAAK;AACH,kBAAM,YAAY;AAClB;AAAA,UACF,KAAK;AACH,kBAAM,YAAY;AAClB;AAAA,UACF,KAAK;AACH,kBAAM,YAAY;AAClB;AAAA,UACF,KAAK;AACH,kBAAM,YAAY;AAClB;AAAA,UACF,KAAK;AACH,kBAAM,YAAY;AAClB;AAAA,UACF,KAAK,KAAK;AACR,kBAAM,YAAY;AAClB,kBAAM,OAAO,OAAO,MAAM,SAAS,WAAW,MAAM,OAAO;AAC3D,gBAAI,CAAC,KAAK,WAAW,GAAG,GAAG;AACzB,oBAAM,SAAS;AACf,oBAAM,MAAM;AAAA,YACd;AACA;AAAA,UACF;AAAA,UACA,KAAK;AACH,kBAAM,YAAY;AAClB;AAAA,UACF,KAAK;AACH,kBAAM,YAAY;AAClB;AAAA,UACF,KAAK;AACH,kBAAM,YAAY;AAClB;AAAA,UACF,KAAK;AACH,kBAAM,YAAY;AAClB;AAAA,UACF,KAAK;AACH,kBAAM,aACH,MAAM,YAAY,MAAM,YAAY,MAAM,MAC3C;AACF;AAAA,UACF,KAAK;AACH,kBAAM,YAAY;AAClB;AAAA,UACF,KAAK;AACH,kBAAM,YAAY;AAClB;AAAA,UACF,KAAK;AACH,kBAAM,YAAY;AAClB;AAAA,UACF,KAAK;AACH,kBAAM,YAAY;AAClB;AAAA,UACF,KAAK;AACH,kBAAM,YAAY;AAClB;AAAA,UACF,KAAK;AACH,kBAAM,YAAY;AAClB;AAAA,UACF;AACE;AAAA,QACJ;AAEA,aAAK,aAAa;AAAA,MACpB;AAAA,IACF,CAAC;AAGD,UAAM,MAAM,WAAW,CAAC,SAAkB;AACxC,yBAAmB,IAAI;AACvB,8BAAwB,IAAI;AAAA,IAC9B,CAAC;AAAA,EACH;AACF;AAGA,IAAM,sBAAgE,CAAC,UAAU,CAAC,MAAM;AACtF,SAAO,CAAC,SAAe;AACrB,UAAM,MAAM,WAAW,CAAC,SAAkB;AACxC,UAAI,KAAK,YAAY,SAAS,KAAK,YAAY;AAC7C,cAAM,MAAM,KAAK,WAAW;AAC5B,YAAI,OAAO,OAAO,QAAQ,YAAY,QAAQ,aAAa;AAEzD,gBAAM,eAAe,kBAAkB,KAAK,QAAQ,WAAW;AAC/D,cAAI,cAAc;AAChB,iBAAK,WAAW,MAAM;AAAA,UACxB,OAAO;AAEL,gCAAoB;AAAA,cAClB,MAAM;AAAA,cACN,SAAS;AAAA,cACT,SAAS,EAAE,aAAa,QAAQ,aAAa,MAAM,IAAI;AAAA,YACzD,CAAC;AACD,iBAAK,WAAW,MAAM;AAAA,UACxB;AAAA,QACF;AAAA,MACF;AAAA,IACF,CAAC;AAAA,EACH;AACF;AAEA,SAAsB,eAAe,UAAkB,aAAsB;AAAA;AAC3E,QAAI;AAEF,UAAI,YAAY,OAAO,EACpB,IAAI,WAAW,EACf,IAAI,SAAS,EACb,IAAI,2BAA2B,EAC/B,IAAI,YAAY,EAChB,IAAI,cAAc,EAClB,IAAI,UAAU,EAEd,IAAI,WAAW,EACf,IAAI,gBAAgB;AAAA,QACnB,YAAY;AAAA,UACV,KAAK,CAAC,aAAa,SAAS,IAAI;AAAA,UAChC,GAAG,CAAC,QAAQ,UAAU,OAAO,IAAI;AAAA,UACjC,KAAK,CAAC,OAAO,KAAK;AAAA,QACpB;AAAA,MACF,CAAC;AAGH,UAAI,aAAa;AACf,oBAAY,UAAU,IAAI,qBAAqB,EAAE,YAAY,CAAC;AAAA,MAChE;AAEA,YAAM,SAAS,MAAM,UAAU,IAAI,eAAe,EAAE,QAAQ,QAAQ;AAEpE,aAAO,OAAO,SAAS;AAAA,IACzB,SAAS,OAAO;AACd,0BAAoB;AAAA,QAClB,MAAM;AAAA,QACN,SAAS;AAAA,QACT;AAAA,QACA,SAAS,EAAE,YAAY;AAAA,MACzB,CAAC;AAED,aAAO;AAAA,IACT;AAAA,EACF;AAAA;AAEA,SAAS,cAAc,GAA2B;AAChD,SAAO,EAAE,SAAS,SAAU,EAAwB,QAAQ;AAC9D;AAEA,SAAS,mBAAmB,MAA+B;AApU3D;AAqUE,QAAM,QAAQ,aAAa,KAAK,KAAK,OAAO;AAC5C,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,KAAK,SAAO,UAAK,eAAL,mBAAiB,QAAO,WAAW,KAAK,WAAW,KAAK;AAC1E,QAAM,OAAO,KAAK,SAAS,IAAI,aAAa,EAAE,KAAK,EAAE;AACrD,MAAI,CAAC,MAAM,CAAC,KAAM,QAAO;AACzB,SAAO,EAAE,IAAI,OAAO,OAAO,SAAS,MAAM,CAAC,GAAG,EAAE,GAAG,KAAK;AAC1D;AAEA,SAAsB,WAAW,UAAsC;AAAA;AACrE,UAAM,WAAsB,CAAC;AAC7B,UAAM,kBAAoC,MAAM,CAAC,SAAe;AAC9D,YAAM,MAAM,WAAW,CAAC,SAAkB;AACxC,cAAM,OAAO,mBAAmB,IAAI;AACpC,YAAI,KAAM,UAAS,KAAK,IAAI;AAAA,MAC9B,CAAC;AAAA,IACH;AACA,UAAM,OAAO,EACV,IAAI,WAAW,EACf,IAAI,SAAS,EACb,IAAI,YAAY,EAChB,IAAI,UAAU,EACd,IAAI,eAAe,EACnB,IAAI,eAAe,EACnB,QAAQ,QAAQ;AACnB,WAAO;AAAA,EACT;AAAA;;;ADpVA,IAAM,oBAAoB,KAAK;AAAA;AAAA,EAAiC,QAAQ,IAAI;AAAA,EAAG;AAAiB;AAEhG,SAAS,eAAe,SAAyB;AAC/C,SAAO,YAAY,OAAO,EAAE;AAC9B;AAEA,SAAS,iBAAiB,MAA6B;AACrD,MAAI;AACF,UAAM,aAAa,KAAK,KAAK,mBAAmB,IAAI;AACpD,UAAM,UAAkC,GAAG,YAAY,YAAY,EAAE,eAAe,KAAK,CAAC;AAC1F,UAAM,QAAQ,QACX,IAAI,CAAC,UAAU;AACd,UAAI,OAAO,UAAU,SAAU,QAAO;AACtC,UAAI,SAAS,OAAO,MAAM,SAAS,SAAU,QAAO,MAAM;AAC1D,aAAO;AAAA,IACT,CAAC,EACA,OAAO,CAAC,SAAyB,QAAQ,IAAI,CAAC;AACjD,UAAM,kBAAkB,CAAC,QAAQ,QAAQ,SAAS,QAAQ,OAAO;AACjE,UAAM,gBAAgB,MAAM;AAAA,MAAK,CAAC,SAChC,gBAAgB,KAAK,CAAC,QAAQ,KAAK,YAAY,EAAE,SAAS,GAAG,CAAC;AAAA,IAChE;AACA,WAAO,gBAAgBA,mBAAkB,eAAe,IAAI,IAAI;AAAA,EAClE,SAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAASA,mBAAkB,SAAiB,aAAoC;AAC9E,MAAI,CAAC,WAAW,OAAO,YAAY,SAAU,QAAO;AACpD,QAAM,YAAY,QAAQ,WAAW,yBAAyB,EAAE;AAChE,MAAI,UAAU,WAAW,SAAS,KAAK,UAAU,WAAW,UAAU,EAAG,QAAO;AAChF,MAAI,UAAU,SAAS,IAAI,KAAK,UAAU,SAAS,IAAI,KAAK,UAAU,WAAW,GAAG,GAAG;AACrF,wBAAoB;AAAA,MAClB,MAAM;AAAA,MACN,SAAS;AAAA,MACT,SAAS,EAAE,aAAa,MAAM,UAAU;AAAA,IAC1C,CAAC;AACD,WAAO;AAAA,EACT;AACA,MAAI,CAAC,qBAAqB,KAAK,SAAS,GAAG;AACzC,wBAAoB;AAAA,MAClB,MAAM;AAAA,MACN,SAAS;AAAA,MACT,SAAS,EAAE,aAAa,MAAM,UAAU;AAAA,IAC1C,CAAC;AACD,WAAO;AAAA,EACT;AACA,MAAI,UAAU,SAAS,GAAG,GAAG;AAC3B,UAAM,iBAAiB,KAAK,UAAU,SAAS;AAC/C,QAAI,eAAe,WAAW,IAAI,KAAK,eAAe,SAAS,KAAK,GAAG;AACrE,0BAAoB;AAAA,QAClB,MAAM;AAAA,QACN,SAAS;AAAA,QACT,SAAS,EAAE,aAAa,MAAM,UAAU;AAAA,MAC1C,CAAC;AACD,aAAO;AAAA,IACT;AACA,WAAO,aAAa,WAAW,IAAI,SAAS;AAAA,EAC9C;AACA,SAAO,aAAa,WAAW,IAAI,SAAS;AAC9C;AAEA,SAAS,gBAAgB,MAAsE;AAC7F,QAAM,SAAS,KAAK,KAAK,mBAAmB,MAAM,YAAY;AAC9D,QAAM,UAAU,KAAK,KAAK,mBAAmB,MAAM,aAAa;AAChE,MAAI,GAAG,WAAW,MAAM,EAAG,QAAO,EAAE,UAAU,QAAQ,aAAa,KAAK;AACxE,MAAI,GAAG,WAAW,OAAO,EAAG,QAAO,EAAE,UAAU,SAAS,aAAa,MAAM;AAC3E,SAAO;AACT;AAEO,SAAS,2BAAqC;AACnD,MAAI;AACF,QAAI,CAAC,GAAG,WAAW,iBAAiB,EAAG,QAAO,CAAC;AAC/C,WAAO,eAAe,mBAAmB,EAAE;AAAA,EAC7C,SAAS,OAAO;AACd,wBAAoB;AAAA,MAClB,MAAM;AAAA,MACN,SAAS;AAAA,MACT;AAAA,MACA,SAAS,EAAE,WAAW,kBAAkB;AAAA,IAC1C,CAAC;AACD,WAAO,CAAC;AAAA,EACV;AACF;AAEA,SAAS,eAAe,KAAa,UAA4B;AAC/D,QAAM,QAAkB,CAAC;AACzB,MAAI;AACF,UAAM,QAAQ,GAAG,YAAY,KAAK,EAAE,eAAe,KAAK,CAAC;AACzD,eAAW,QAAQ,OAAO;AACxB,UAAI,CAAC,KAAK,YAAY,EAAG;AACzB,YAAM,OAAO,WAAW,GAAG,QAAQ,IAAI,KAAK,IAAI,KAAK,KAAK;AAC1D,UAAI,gBAAgB,IAAI,MAAM,MAAM;AAClC,cAAM,KAAK,IAAI;AAAA,MACjB;AACA,YAAM,KAAK,GAAG,eAAe,KAAK,KAAK,KAAK,KAAK,IAAI,GAAG,IAAI,CAAC;AAAA,IAC/D;AAAA,EACF,SAAQ;AAAA,EAER;AACA,SAAO;AACT;AAEA,SAAS,eAAe,SAAsC;AAC5D,MAAI,CAAC,QAAS,QAAO;AACrB,MAAI;AACF,UAAM,SAAS,IAAI,KAAK,OAAiB;AACzC,QAAI,CAAC,OAAO,MAAM,OAAO,QAAQ,CAAC,EAAG,QAAO,OAAO,YAAY,EAAE,MAAM,GAAG,EAAE,CAAC;AAAA,EAC/E,SAAQ;AAAA,EAER;AACA,SAAO;AACT;AAEA,SAAS,qBAAqB,UAAmB,MAAsB;AACrE,QAAM,MAAM,OAAO,aAAa,WAAW,WAAW;AACtD,MAAI,OAAO,CAAC,IAAI,WAAW,MAAM,EAAG,QAAOA,mBAAkB,KAAK,IAAI,KAAK;AAC3E,MAAI,CAAC,IAAK,QAAO,iBAAiB,IAAI,KAAK;AAC3C,SAAO;AACT;AAEA,SAAS,cAAc,KAAqC;AAC1D,MAAI,CAAC,MAAM,QAAQ,GAAG,EAAG,QAAO;AAChC,QAAM,QAAQ,IAAI;AAAA,IAChB,CAAC,SACC,OAAO,SAAS,YAChB,SAAS,QACT,OAAQ,KAAiB,aAAa,YACtC,OAAQ,KAAiB,WAAW;AAAA,EACxC;AACA,SAAO,MAAM,SAAS,QAAQ;AAChC;AAEA,SAAS,gBAAgB,KAAuC;AAC9D,MAAI,CAAC,MAAM,QAAQ,GAAG,EAAG,QAAO;AAChC,QAAM,QAAQ,IAAI;AAAA,IAChB,CAAC,SACC,OAAO,SAAS,YAChB,SAAS,QACT,OAAQ,KAAmB,SAAS,YACpC,OAAQ,KAAmB,SAAS;AAAA,EACxC;AACA,SAAO,MAAM,SAAS,QAAQ;AAChC;AAEA,SAAe,kBAAkB,MAAuC;AAAA;AACtE,QAAI;AACF,YAAM,QAAQ,gBAAgB,IAAI;AAClC,UAAI,CAAC,MAAO,QAAO;AACnB,YAAM,cAAc,GAAG,aAAa,MAAM,UAAU,MAAM;AAC1D,YAAM,EAAE,MAAM,SAAS,gBAAgB,IAAI,OAAO,WAAW;AAC7D,YAAM,WAAW,eAAe,eAAe;AAC/C,YAAM,UAAoB,MAAM,QAAQ,KAAK,IAAI,IAC7C,KAAK,KAAK,OAAO,CAAC,MAAe,OAAO,MAAM,YAAY,OAAO,CAAC,EAAE,KAAK,CAAC,IAC1E,CAAC;AACL,YAAM,aACJ,QAAQ,SAAS,IAAI,QAAQ,IAAI,CAAC,MAAc,EAAE,WAAW,KAAK,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,WAAW;AAC/F,aAAO;AAAA,QACL;AAAA,QACA,OAAO,KAAK,SAAS,KAAK,WAAW,KAAK,GAAG;AAAA,QAC7C,SAAS,KAAK,WAAW;AAAA,QACzB,MAAM,eAAe,KAAK,IAAI;AAAA,QAC9B,SAAS,eAAe,KAAK,OAAO;AAAA,QACpC,QAAQ,KAAK,UAAU;AAAA,QACvB,UAAU,WAAW,CAAC;AAAA,QACtB;AAAA,QACA;AAAA,QACA,eAAe,qBAAqB,KAAK,eAAe,IAAI;AAAA,QAC5D,MAAM,KAAK,QAAQ,CAAC;AAAA,QACpB,aAAa,MAAM;AAAA,QACnB,OAAO,KAAK,UAAU;AAAA,QACtB,KAAK,cAAc,KAAK,GAAG;AAAA,QAC3B,OAAO,gBAAgB,KAAK,KAAK;AAAA,QACjC,cAAc,OAAO,KAAK,iBAAiB,WAAW,KAAK,eAAe;AAAA,QAC1E,aAAa,OAAO,KAAK,gBAAgB,WAAW,KAAK,cAAc;AAAA,QACvE,QAAQ,OAAO,KAAK,WAAW,WAAW,KAAK,SAAS;AAAA,QACxD,SAAS,KAAK,YAAY;AAAA,MAC5B;AAAA,IACF,SAAS,OAAO;AACd,0BAAoB;AAAA,QAClB,MAAM;AAAA,QACN,SAAS;AAAA,QACT;AAAA,QACA,SAAS,EAAE,KAAK;AAAA,MAClB,CAAC;AACD,aAAO;AAAA,IACT;AAAA,EACF;AAAA;AAEO,IAAM,qBAAqB,MAAM,CAAO,SAA0C;AACvF,MAAI;AACF,UAAM,UAAU,MAAM,kBAAkB,IAAI;AAC5C,QAAI,CAAC,QAAS,QAAO;AACrB,UAAM,QAAQ,gBAAgB,IAAI;AAClC,QAAI,CAAC,MAAO,QAAO;AACnB,UAAM,cAAc,GAAG,aAAa,MAAM,UAAU,MAAM;AAC1D,UAAM,EAAE,SAAS,gBAAgB,IAAI,OAAO,WAAW;AACvD,UAAM,MAAM,MAAM,WAAW,eAAe;AAC5C,QAAI;AACJ,QAAI;AACJ,QAAI,MAAM,gBAAgB,OAAO;AAC/B,kBAAY;AAAA,IACd,OAAO;AACL,oBAAc,MAAM,eAAe,iBAAiB,IAAI;AAAA,IAC1D;AACA,WAAO,iCAAK,UAAL,EAAc,SAAS,iBAAiB,aAAa,WAAW,IAAI;AAAA,EAC7E,SAAS,OAAO;AACd,wBAAoB;AAAA,MAClB,MAAM;AAAA,MACN,SAAS;AAAA,MACT;AAAA,MACA,SAAS,EAAE,KAAK;AAAA,IAClB,CAAC;AACD,WAAO;AAAA,EACT;AACF,EAAC;AAEM,IAAM,iBAAiB,MAAM,MAAgC;AAClE,QAAM,QAAQ,yBAAyB;AACvC,QAAM,WAAW,MAAM,QAAQ,IAAI,MAAM,IAAI,CAAC,SAAS,kBAAkB,IAAI,CAAC,CAAC;AAC/E,QAAM,eAAc,oBAAI,KAAK,GAAE,YAAY,EAAE,MAAM,GAAG,EAAE,CAAC;AACzD,SAAO,SACJ,OAAO,CAAC,YAAgC,YAAY,IAAI,EACxD,OAAO,CAAC,YAAY,CAAC,QAAQ,QAAQ,QAAQ,QAAQ,WAAW,EAChE,OAAO,CAAC,YAAY,EAAE,QAAQ,SAAS,QAAQ,IAAI,aAAa,aAAa,EAC7E,KAAK,CAAC,GAAG,MAAM;AACd,QAAI,CAAC,EAAE,QAAQ,CAAC,EAAE,KAAM,QAAO;AAC/B,QAAI,CAAC,EAAE,KAAM,QAAO;AACpB,QAAI,CAAC,EAAE,KAAM,QAAO;AACpB,WAAO,IAAI,KAAK,EAAE,IAAI,EAAE,QAAQ,IAAI,IAAI,KAAK,EAAE,IAAI,EAAE,QAAQ;AAAA,EAC/D,CAAC;AACL,EAAC;AAaD,SAAsB,mBAAmB,MAAsC;AAAA;AAC7E,QAAI;AACF,YAAM,UAAU,MAAM,kBAAkB,IAAI;AAC5C,UAAI,EAAC,mCAAS,SAAS,QAAO;AAC9B,YAAM,QAAQ,gBAAgB,IAAI;AAClC,UAAI,CAAC,MAAO,QAAO;AACnB,YAAM,cAAc,GAAG,aAAa,MAAM,UAAU,MAAM;AAC1D,YAAM,EAAE,SAAS,gBAAgB,IAAI,OAAO,WAAW;AACvD,aAAO;AAAA,IACT,SAAS,OAAO;AACd,0BAAoB;AAAA,QAClB,MAAM;AAAA,QACN,SAAS;AAAA,QACT;AAAA,QACA,SAAS,EAAE,KAAK;AAAA,MAClB,CAAC;AACD,aAAO;AAAA,IACT;AAAA,EACF;AAAA;AAEA,SAAsB,2BACpB,MACA,QACmB;AAAA;AACnB,UAAM,WAAW,MAAM,mBAAmB,IAAI;AAC9C,QAAI,aAAa,KAAM,QAAO,IAAI,SAAS,aAAa,EAAE,QAAQ,IAAI,CAAC;AACvE,UAAM,UAAU,MAAM,mBAAmB,IAAI;AAC7C,WAAO,IAAI,SAAS,UAAU;AAAA,MAC5B,SAAS;AAAA,QACP,gBAAgB;AAAA,QAChB,iBAAiB;AAAA,SACb,UAAU,oBAAoB,SAAS,MAAM,IAAI,CAAC;AAAA,IAE1D,CAAC;AAAA,EACH;AAAA;AAEO,SAAS,sBACd,SACA,QACoB;AACpB,MAAI,QAAQ,YAAY,KAAM,QAAO;AACrC,QAAM,WAAW,aAAa,QAAQ,IAAI;AAC1C,MAAI,CAAC,OAAQ,QAAO;AACpB,SAAO,GAAG,OAAO,QAAQ,QAAQ,OAAO,EAAE,CAAC,GAAG,QAAQ;AACxD;AAEO,SAAS,oBACd,SACA,QACwB;AACxB,QAAM,cAAc,sBAAsB,SAAS,MAAM;AACzD,MAAI,aAAa;AACf,WAAO;AAAA,MACL,MAAM,IAAI,WAAW;AAAA,IACvB;AAAA,EACF;AACA,SAAO;AAAA,IACL,gBAAgB;AAAA,EAClB;AACF;;;AG3SO,SAAS,6BAA6B,QAAwB;AACnE,WAAe,IACb,UACA,SACA;AAAA;AACA,YAAM,SAAS,MAAM,QAAQ;AAC7B,YAAM,YAAY,OAAO,MAAM;AAC/B,YAAM,OAAO,MAAM,QAAQ,SAAS,IAAI,UAAU,KAAK,GAAG,IAAK,gCAAa;AAC5E,YAAM,YAAY,KAAK,SAAS,KAAK,IAAI,KAAK,MAAM,GAAG,EAAE,IAAI;AAC7D,aAAO,2BAA2B,WAAW,MAAM;AAAA,IACrD;AAAA;AACA,SAAO,EAAE,IAAI;AACf;","names":["sanitizeImagePath"]}
|
|
1
|
+
{"version":3,"sources":["../src/server-articles.ts","../src/markdown.ts","../src/errorReporting.ts","../src/nextjs.ts"],"sourcesContent":["import { cache } from 'react'\nimport matter from 'gray-matter'\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport readingTime from 'reading-time'\nimport { markdownToHtml, extractToc } from './markdown'\nimport type { Article, AuthorProfile, CategoryInfo, FaqItem, HowToStep } from './articleTypes'\nimport type { ArticlesConfig } from './articlesConfig'\nimport { reportArticlesError } from './errorReporting'\n\nconst articlesDirectory = path.join(/* turbopackIgnore: true */ process.cwd(), 'public/articles')\n\nfunction getReadingTime(content: string): string {\n return readingTime(content).text\n}\n\nfunction findArticleImage(slug: string): string | null {\n try {\n const articleDir = path.join(articlesDirectory, slug)\n const entries: (fs.Dirent | string)[] = fs.readdirSync(articleDir, { withFileTypes: true })\n const names = entries\n .map((entry) => {\n if (typeof entry === 'string') return entry\n if (entry && typeof entry.name === 'string') return entry.name\n return null\n })\n .filter((name): name is string => Boolean(name))\n const imageExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.webp']\n const imageFileName = names.find((name) =>\n imageExtensions.some((ext) => name.toLowerCase().endsWith(ext))\n )\n return imageFileName ? sanitizeImagePath(imageFileName, slug) : null\n } catch {\n return null\n }\n}\n\nfunction sanitizeImagePath(rawPath: string, articleSlug: string): string | null {\n if (!rawPath || typeof rawPath !== 'string') return null\n const cleanPath = rawPath.replaceAll(/[\\x00-\\x1f\\x7f-\\x9f]/g, '')\n if (cleanPath.startsWith('http://') || cleanPath.startsWith('https://')) return cleanPath\n if (cleanPath.includes('..') || cleanPath.includes('\\\\') || cleanPath.startsWith('/')) {\n reportArticlesError({\n code: 'unsafe-image-path',\n message: 'Rejected unsafe article image path.',\n context: { articleSlug, path: cleanPath },\n })\n return null\n }\n if (!/^[a-zA-Z0-9._/-]+$/.test(cleanPath)) {\n reportArticlesError({\n code: 'unsafe-image-path',\n message: 'Rejected article image path with invalid characters.',\n context: { articleSlug, path: cleanPath },\n })\n return null\n }\n if (cleanPath.includes('/')) {\n const normalizedPath = path.normalize(cleanPath)\n if (normalizedPath.startsWith('..') || normalizedPath.includes('../')) {\n reportArticlesError({\n code: 'unsafe-image-path',\n message: 'Rejected article image path traversal attempt.',\n context: { articleSlug, path: cleanPath },\n })\n return null\n }\n return `/articles/${articleSlug}/${cleanPath}`\n }\n return `/articles/${articleSlug}/${cleanPath}`\n}\n\nfunction findArticleFile(slug: string): { filePath: string; contentType: 'md' | 'mdx' } | null {\n const mdPath = path.join(articlesDirectory, slug, 'article.md')\n const mdxPath = path.join(articlesDirectory, slug, 'article.mdx')\n if (fs.existsSync(mdPath)) return { filePath: mdPath, contentType: 'md' }\n if (fs.existsSync(mdxPath)) return { filePath: mdxPath, contentType: 'mdx' }\n return null\n}\n\nexport function getAvailableArticleSlugs(): string[] {\n try {\n if (!fs.existsSync(articlesDirectory)) return []\n return walkArticleDir(articlesDirectory, '')\n } catch (error) {\n reportArticlesError({\n code: 'article-directory-read-failed',\n message: 'Unable to read articles directory.',\n error,\n context: { directory: articlesDirectory },\n })\n return []\n }\n}\n\nfunction walkArticleDir(dir: string, baseSlug: string): string[] {\n const slugs: string[] = []\n try {\n const items = fs.readdirSync(dir, { withFileTypes: true })\n for (const item of items) {\n if (!item.isDirectory()) continue\n const slug = baseSlug ? `${baseSlug}/${item.name}` : item.name\n if (findArticleFile(slug) !== null) {\n slugs.push(slug)\n }\n slugs.push(...walkArticleDir(path.join(dir, item.name), slug))\n }\n } catch {\n // ignore unreadable directories\n }\n return slugs\n}\n\nfunction parseDateField(rawDate: unknown): string | undefined {\n if (!rawDate) return undefined\n try {\n const parsed = new Date(rawDate as string)\n if (!Number.isNaN(parsed.getTime())) return parsed.toISOString().split('T')[0]\n } catch {\n // ignore invalid dates\n }\n return undefined\n}\n\nfunction resolveFeaturedImage(rawImage: unknown, slug: string): string {\n const img = typeof rawImage === 'string' ? rawImage : ''\n if (img && !img.startsWith('http')) return sanitizeImagePath(img, slug) || '/placeholder-logo.png'\n if (!img) return findArticleImage(slug) || '/placeholder-logo.png'\n return img\n}\n\nfunction parseFaqItems(raw: unknown): FaqItem[] | undefined {\n if (!Array.isArray(raw)) return undefined\n const items = raw.filter(\n (item): item is FaqItem =>\n typeof item === 'object' &&\n item !== null &&\n typeof (item as FaqItem).question === 'string' &&\n typeof (item as FaqItem).answer === 'string'\n )\n return items.length ? items : undefined\n}\n\nfunction parseHowToSteps(raw: unknown): HowToStep[] | undefined {\n if (!Array.isArray(raw)) return undefined\n const steps = raw.filter(\n (item): item is HowToStep =>\n typeof item === 'object' &&\n item !== null &&\n typeof (item as HowToStep).name === 'string' &&\n typeof (item as HowToStep).text === 'string'\n )\n return steps.length ? steps : undefined\n}\n\nfunction parseAuthors(raw: unknown): string[] | undefined {\n if (!Array.isArray(raw)) return undefined\n const authors = raw.filter(\n (author): author is string => typeof author === 'string' && author.trim().length > 0\n )\n return authors\n}\n\nexport function getAuthorBySlug(slug: string, config: ArticlesConfig): AuthorProfile | null {\n const profile = config.authors?.[slug]\n if (!profile) return null\n return {\n ...profile,\n url: profile.url ?? `${config.siteUrl.replace(/\\/$/, '')}/articles/authors/${profile.slug}`,\n }\n}\n\nfunction getConfiguredAuthorByName(name: string, config: ArticlesConfig): AuthorProfile | null {\n const normalizedName = name.trim().toLowerCase()\n const profile = Object.values(config.authors ?? {}).find(\n (author) => author.name.toLowerCase() === normalizedName\n )\n return profile ? getAuthorBySlug(profile.slug, config) : null\n}\n\nfunction resolveArticleAuthorName(\n rawAuthor: unknown,\n rawAuthors: unknown,\n config?: ArticlesConfig\n): string {\n const authorArray = parseAuthors(rawAuthors)\n const firstAuthor = authorArray?.[0]\n const rawAuthorValue = typeof rawAuthor === 'string' && rawAuthor.trim() ? rawAuthor : undefined\n const author = firstAuthor ?? rawAuthorValue\n const resolved = author ?? config?.defaultAuthor\n if (!resolved) return ''\n if (!config) return resolved\n return (\n getAuthorBySlug(resolved, config)?.name ??\n getConfiguredAuthorByName(resolved, config)?.name ??\n resolved\n )\n}\n\nexport function getArticleAuthors(article: Article, config: ArticlesConfig): AuthorProfile[] {\n const fallbackAuthors = Array.from(\n new Set(\n [article.author, config.defaultAuthor].filter(\n (author): author is string => typeof author === 'string' && author.trim().length > 0\n )\n )\n )\n const authorValues = article.authors ?? fallbackAuthors\n if (authorValues.length === 0) return []\n const resolvedAuthors = authorValues\n .map((author) => getAuthorBySlug(author, config) ?? getConfiguredAuthorByName(author, config))\n .filter((author): author is AuthorProfile => author !== null)\n .filter((author, index, all) => all.findIndex((a) => a.slug === author.slug) === index)\n\n if (resolvedAuthors.length > 0) return resolvedAuthors\n\n return authorValues.map((fallbackName) => ({\n name: fallbackName,\n slug: categoryToSlug(fallbackName),\n bio: '',\n }))\n}\n\nexport function getAllAuthors(config: ArticlesConfig): AuthorProfile[] {\n return Object.keys(config.authors ?? {})\n .map((slug) => getAuthorBySlug(slug, config))\n .filter((author): author is AuthorProfile => author !== null)\n}\n\nasync function getArticleSummary(slug: string, config?: ArticlesConfig): Promise<Article | null> {\n try {\n const found = findArticleFile(slug)\n if (!found) return null\n const fileContent = fs.readFileSync(found.filePath, 'utf8')\n const { data, content: markdownContent } = matter(fileContent)\n const readTime = getReadingTime(markdownContent)\n const allTags: string[] = Array.isArray(data.tags)\n ? data.tags.filter((t: unknown) => typeof t === 'string' && String(t).trim())\n : []\n const categories: string[] =\n allTags.length > 0 ? allTags.map((t: string) => t.replaceAll('-', ' ').trim()) : ['Campaigns']\n return {\n slug,\n title: data.title || slug.replaceAll('-', ' '),\n excerpt: data.excerpt || '',\n date: parseDateField(data.date),\n lastmod: parseDateField(data.lastmod),\n author: resolveArticleAuthorName(data.author, data.authors, config),\n authors: parseAuthors(data.authors),\n category: categories[0],\n categories,\n readTime,\n featuredImage: resolveFeaturedImage(data.featuredImage, slug),\n tags: data.tags || [],\n contentType: found.contentType,\n draft: data.draft === true,\n faq: parseFaqItems(data.faq),\n howTo: parseHowToSteps(data.howTo),\n canonicalUrl: typeof data.canonicalUrl === 'string' ? data.canonicalUrl : undefined,\n articleType: typeof data.articleType === 'string' ? data.articleType : undefined,\n series: typeof data.series === 'string' ? data.series : undefined,\n aiCrawl: data.aiCrawl === true,\n }\n } catch (error) {\n reportArticlesError({\n code: 'article-load-failed',\n message: 'Unable to load article summary.',\n error,\n context: { slug },\n })\n return null\n }\n}\n\nexport const getArticleMetadata = cache(\n async (slug: string, config?: ArticlesConfig): Promise<Article | null> => {\n try {\n const summary = await getArticleSummary(slug, config)\n if (!summary) return null\n const found = findArticleFile(slug)\n if (!found) return null\n const fileContent = fs.readFileSync(found.filePath, 'utf8')\n const { content: markdownContent } = matter(fileContent)\n const toc = await extractToc(markdownContent)\n let htmlContent: string | undefined\n let mdxSource: string | undefined\n if (found.contentType === 'mdx') {\n mdxSource = markdownContent\n } else {\n htmlContent = await markdownToHtml(markdownContent, slug, config)\n }\n return { ...summary, content: markdownContent, htmlContent, mdxSource, toc }\n } catch (error) {\n reportArticlesError({\n code: 'article-load-failed',\n message: 'Unable to load article metadata.',\n error,\n context: { slug },\n })\n return null\n }\n }\n)\n\nexport const getAllArticles = cache(async (config?: ArticlesConfig): Promise<Article[]> => {\n const slugs = getAvailableArticleSlugs()\n const articles = await Promise.all(slugs.map((slug) => getArticleSummary(slug, config)))\n const currentDate = new Date().toISOString().split('T')[0]\n return articles\n .filter((article): article is Article => article !== null)\n .filter((article) => !article.date || article.date <= currentDate)\n .filter((article) => !(article.draft && process.env.NODE_ENV === 'production'))\n .sort((a, b) => {\n if (!a.date && !b.date) return 0\n if (!a.date) return 1\n if (!b.date) return -1\n return new Date(b.date).getTime() - new Date(a.date).getTime()\n })\n})\n\nexport async function getAdjacentArticles(\n currentSlug: string\n): Promise<{ previous: Article | null; next: Article | null }> {\n const allArticles = await getAllArticles()\n const currentIndex = allArticles.findIndex((article) => article.slug === currentSlug)\n if (currentIndex === -1) return { previous: null, next: null }\n const previous = currentIndex < allArticles.length - 1 ? allArticles[currentIndex + 1] : null\n const next = currentIndex > 0 ? allArticles[currentIndex - 1] : null\n return { previous, next }\n}\n\nexport async function getArticleMarkdown(slug: string): Promise<string | null> {\n try {\n const summary = await getArticleSummary(slug)\n if (!summary?.aiCrawl) return null\n const found = findArticleFile(slug)\n if (!found) return null\n const fileContent = fs.readFileSync(found.filePath, 'utf8')\n const { content: markdownContent } = matter(fileContent)\n return markdownContent\n } catch (error) {\n reportArticlesError({\n code: 'article-markdown-load-failed',\n message: 'Unable to load article markdown.',\n error,\n context: { slug },\n })\n return null\n }\n}\n\nexport async function getArticleMarkdownResponse(\n slug: string,\n config: ArticlesConfig\n): Promise<Response> {\n const markdown = await getArticleMarkdown(slug)\n if (markdown === null) return new Response('Not Found', { status: 404 })\n const article = await getArticleMetadata(slug)\n return new Response(markdown, {\n headers: {\n 'Content-Type': 'text/markdown; charset=utf-8',\n 'Cache-Control': 'public, max-age=3600, s-maxage=3600',\n ...(article ? getArticleAiHeaders(article, config) : {}),\n },\n })\n}\n\nexport function getArticleMarkdownUrl(\n article: Pick<Article, 'slug' | 'aiCrawl'>,\n config?: Pick<ArticlesConfig, 'siteUrl'>\n): string | undefined {\n if (article.aiCrawl !== true) return undefined\n const pathname = `/articles/${article.slug}.md`\n if (!config) return pathname\n return `${config.siteUrl.replace(/\\/$/, '')}${pathname}`\n}\n\nexport function getArticleAiHeaders(\n article: Pick<Article, 'slug' | 'aiCrawl'>,\n config?: Pick<ArticlesConfig, 'siteUrl'>\n): Record<string, string> {\n const markdownUrl = getArticleMarkdownUrl(article, config)\n if (markdownUrl) {\n return {\n Link: `<${markdownUrl}>; rel=\"alternate\"; type=\"text/markdown\"`,\n }\n }\n return {\n 'X-Robots-Tag': 'noai, noimageai',\n }\n}\n\nexport async function getAiRobotsTxtRules(): Promise<string> {\n const articles = await getAllArticles()\n const blockedArticles = articles.filter((article) => article.aiCrawl !== true)\n if (blockedArticles.length === 0) return ''\n\n const aiCrawlers = [\n 'GPTBot',\n 'ChatGPT-User',\n 'CCBot',\n 'ClaudeBot',\n 'Claude-User',\n 'PerplexityBot',\n 'Google-Extended',\n ]\n const disallowRules = blockedArticles\n .map((article) => `Disallow: /articles/${article.slug}`)\n .join('\\n')\n\n return aiCrawlers\n .map((crawler) => [`User-agent: ${crawler}`, disallowRules].join('\\n'))\n .join('\\n\\n')\n}\n\nexport async function searchArticles(query: string, config?: ArticlesConfig): Promise<Article[]> {\n if (!query?.trim()) return getAllArticles(config)\n const articles = await getAllArticles(config)\n const searchTerm = query.toLowerCase().trim()\n const includeAuthor = config?.showAuthor !== false\n return articles.filter((article) => {\n const matchesTitle = article.title.toLowerCase().includes(searchTerm)\n const matchesExcerpt = article.excerpt.toLowerCase().includes(searchTerm)\n const matchesAuthor = includeAuthor && article.author.toLowerCase().includes(searchTerm)\n const matchesCategory = article.categories.some((cat) => cat.toLowerCase().includes(searchTerm))\n const matchesTags = article.tags?.some((tag) => tag.toLowerCase().includes(searchTerm))\n return (\n matchesTitle || matchesExcerpt || matchesAuthor || matchesCategory || Boolean(matchesTags)\n )\n })\n}\n\nexport function categoryToSlug(category: string): string {\n return category\n .toLowerCase()\n .replaceAll(/\\s+/g, '-')\n .replaceAll(/[^a-z0-9-]/g, '')\n}\n\nexport async function getAllCategories(): Promise<CategoryInfo[]> {\n const articles = await getAllArticles()\n const categoryMap = new Map<string, { count: number; featuredImage: string }>()\n for (const article of articles) {\n for (const cat of article.categories) {\n if (!categoryMap.has(cat)) {\n categoryMap.set(cat, { count: 0, featuredImage: article.featuredImage })\n }\n categoryMap.get(cat)!.count++\n }\n }\n return Array.from(categoryMap.entries())\n .map(([name, { count, featuredImage }]) => ({\n name,\n slug: categoryToSlug(name),\n count,\n featuredImage,\n }))\n .sort((a, b) => b.count - a.count)\n}\n\nexport async function getArticlesByCategory(\n categorySlug: string,\n config?: ArticlesConfig\n): Promise<Article[]> {\n const articles = await getAllArticles(config)\n return articles.filter((article) =>\n article.categories.some((cat) => categoryToSlug(cat) === categorySlug)\n )\n}\n\nexport async function getArticlesByAuthor(\n authorSlug: string,\n config: ArticlesConfig\n): Promise<Article[]> {\n const articles = await getAllArticles(config)\n return articles.filter((article) =>\n getArticleAuthors(article, config).some((author) => author.slug === authorSlug)\n )\n}\n\nexport { sanitizeImagePath }\n","import type { Element, Root, ElementContent } from 'hast'\nimport rehypePrism from 'rehype-prism-plus'\nimport rehypeSanitize from 'rehype-sanitize'\nimport rehypeSlug from 'rehype-slug'\nimport rehypeStringify from 'rehype-stringify'\nimport { remark } from 'remark'\nimport remarkGfm from 'remark-gfm'\nimport remarkGithubBlockquoteAlert from 'remark-github-blockquote-alert'\nimport remarkParse from 'remark-parse'\nimport remarkRehype from 'remark-rehype'\nimport { Plugin } from 'unified'\nimport { visit } from 'unist-util-visit'\nimport type { TocItem } from './articleTypes'\nimport type { ArticlesConfig, LinkTargetStrategy } from './articlesConfig'\nimport { reportArticlesError } from './errorReporting'\n\ntype LinkTargetOptions = Readonly<{\n strategy?: LinkTargetStrategy\n siteUrl?: string\n}>\n\nconst DEFAULT_LINK_TARGET_STRATEGY: LinkTargetStrategy = 'external-new-tab'\n\nfunction isNonBrowserNavigationLink(href: string): boolean {\n return (\n /^[a-zA-Z][a-zA-Z\\d+.-]*:/.test(href) &&\n !href.startsWith('http://') &&\n !href.startsWith('https://')\n )\n}\n\nfunction getOrigin(url: string | undefined): string | null {\n if (!url) return null\n try {\n return new URL(url).origin\n } catch {\n return null\n }\n}\n\nfunction isExternalHttpLink(href: string, siteUrl?: string): boolean {\n if (!href.startsWith('http://') && !href.startsWith('https://')) return false\n const siteOrigin = getOrigin(siteUrl)\n if (!siteOrigin) return true\n return getOrigin(href) !== siteOrigin\n}\n\nfunction shouldOpenInNewTab(href: string, options: LinkTargetOptions = {}): boolean {\n if (!href || href.startsWith('#') || isNonBrowserNavigationLink(href)) return false\n\n const strategy = options.strategy ?? DEFAULT_LINK_TARGET_STRATEGY\n if (strategy === 'same-tab') return false\n if (strategy === 'all-new-tab') return true\n return isExternalHttpLink(href, options.siteUrl)\n}\n\nfunction applyLinkTarget(props: Record<string, unknown>, options?: LinkTargetOptions): void {\n const href = typeof props.href === 'string' ? props.href : ''\n if (shouldOpenInNewTab(href, options)) {\n props.target = '_blank'\n props.rel = 'noopener noreferrer'\n return\n }\n delete props.target\n delete props.rel\n}\n\nfunction getLinkTargetOptions(config?: ArticlesConfig): LinkTargetOptions {\n return {\n strategy: config?.linkTargetStrategy,\n siteUrl: config?.siteUrl,\n }\n}\n\n// Import the sanitizeImagePath function\nfunction sanitizeImagePath(rawPath: string, articleSlug: string): string | null {\n if (!rawPath || typeof rawPath !== 'string') {\n return null\n }\n\n // Remove any null bytes or control characters\n const cleanPath = rawPath.replaceAll(/[\\x00-\\x1f\\x7f-\\x9f]/g, '')\n\n // Check for absolute URLs (http/https)\n if (cleanPath.startsWith('http://') || cleanPath.startsWith('https://')) {\n // For external URLs, just return as-is (they're safe)\n return cleanPath\n }\n\n // For relative paths, ensure they don't contain dangerous patterns\n if (cleanPath.includes('..') || cleanPath.includes('\\\\') || cleanPath.startsWith('/')) {\n reportArticlesError({\n code: 'unsafe-image-path',\n message: 'Rejected unsafe markdown image path.',\n context: { articleSlug, path: cleanPath },\n })\n return null\n }\n\n // Only allow alphanumeric characters, hyphens, underscores, dots, and forward slashes\n if (!/^[a-zA-Z0-9._/-]+$/.test(cleanPath)) {\n reportArticlesError({\n code: 'unsafe-image-path',\n message: 'Rejected markdown image path with invalid characters.',\n context: { articleSlug, path: cleanPath },\n })\n return null\n }\n\n // Construct safe path within articles directory\n if (cleanPath.includes('/')) {\n // Relative path, ensure it's within the article directory\n const normalizedPath = cleanPath.replaceAll('\\\\', '/')\n if (normalizedPath.startsWith('..') || normalizedPath.includes('../')) {\n reportArticlesError({\n code: 'unsafe-image-path',\n message: 'Rejected markdown image path traversal attempt.',\n context: { articleSlug, path: cleanPath },\n })\n return null\n }\n return `/articles/${articleSlug}/${normalizedPath}`\n } else {\n // Just a filename, construct full path\n return `/articles/${articleSlug}/${cleanPath}`\n }\n}\n\nfunction styleFootnoteLinks(nodes: ElementContent[]): void {\n nodes.forEach((n) => {\n if (n.type !== 'element') return\n const el = n as Element\n if (el.tagName === 'a') {\n const isBackRef =\n el.properties?.['dataFootnoteBackref'] !== undefined ||\n (el.children[0]?.type === 'text' && el.children[0].value === '↩')\n if (isBackRef) {\n el.properties.className = 'text-primary hover:underline ml-1'\n if (typeof el.properties.href === 'string') {\n el.properties.href = el.properties.href.replaceAll('#user-content-fnref-', '#ref-')\n }\n } else {\n el.properties.className = 'text-primary hover:underline break-all'\n }\n }\n if (el.children) styleFootnoteLinks(el.children)\n })\n}\n\nfunction processFootnoteRef(node: Element): void {\n if (\n node.tagName !== 'sup' ||\n node.children?.[0]?.type !== 'element' ||\n (node.children[0] as Element).tagName !== 'a'\n )\n return\n\n const link = node.children[0] as Element\n const href = link.properties?.href\n if (typeof href !== 'string' || !href.startsWith('#user-content-fn-')) return\n\n delete link.properties.target\n delete link.properties.rel\n link.properties.className = 'text-primary hover:underline'\n link.properties.href = href.replaceAll('#user-content-fn-', '#footnote-')\n\n if (typeof node.properties?.id === 'string') {\n link.properties.id = node.properties.id.replaceAll('user-content-fnref-', 'ref-')\n delete node.properties.id\n }\n\n if (link.children?.[0]?.type === 'text') {\n link.children[0].value = `[${link.children[0].value}]`\n }\n}\n\nfunction processFootnotesSection(node: Element): void {\n const cls = node.properties?.className\n const isFootnotes =\n node.tagName === 'section' &&\n (Array.isArray(cls) ? cls.includes('footnotes') : cls === 'footnotes')\n if (!isFootnotes) return\n\n const olCandidate = node.children.find(\n (child) => child.type === 'element' && (child as Element).tagName === 'ol'\n )\n if (olCandidate?.type !== 'element') return\n\n const ol = olCandidate as Element\n ol.properties.className = 'list-decimal ml-6 space-y-2 text-sm text-muted-foreground'\n\n ol.children.forEach((li) => {\n if (li.type !== 'element' || li.tagName !== 'li') return\n const liEl = li as Element\n\n if (liEl.properties) {\n liEl.properties.className = 'pl-2'\n if (typeof liEl.properties.id === 'string') {\n liEl.properties.id = liEl.properties.id.replaceAll('user-content-fn-', 'footnote-')\n }\n }\n\n const pIndex = liEl.children.findIndex(\n (child) => child.type === 'element' && (child as Element).tagName === 'p'\n )\n if (pIndex !== -1) {\n const p = liEl.children[pIndex] as Element\n liEl.children.splice(pIndex, 1, ...p.children)\n }\n\n styleFootnoteLinks(liEl.children)\n })\n\n const hr: Element = {\n type: 'element',\n tagName: 'hr',\n properties: { className: 'my-8 border-border' },\n children: [],\n }\n const h3: Element = {\n type: 'element',\n tagName: 'h3',\n properties: { className: 'text-lg font-semibold mb-4' },\n children: [{ type: 'text', value: 'References' }],\n }\n\n node.children = [hr, h3, ol]\n if (node.properties) node.properties.className = undefined\n}\n\nexport const customRenderer: Plugin<[LinkTargetOptions?], Root> = (linkTargetOptions = {}) => {\n return (tree: Root) => {\n // First pass: Apply general styles\n visit(tree, 'element', (node: Element) => {\n if (node.tagName) {\n const props = node.properties || {}\n\n switch (node.tagName) {\n case 'h1':\n props.className = 'text-3xl font-bold text-foreground mt-8 mb-4 scroll-mt-20'\n break\n case 'h2':\n props.className = 'text-2xl font-semibold text-foreground mt-6 mb-3 scroll-mt-20'\n break\n case 'h3':\n props.className = 'text-xl font-semibold text-foreground mt-4 mb-2 scroll-mt-20'\n break\n case 'h4':\n props.className = 'text-lg font-semibold text-foreground mt-3 mb-2 scroll-mt-20'\n break\n case 'p':\n props.className = 'text-muted-foreground leading-relaxed mb-4'\n break\n case 'a': {\n props.className = 'text-primary hover:underline transition-colors duration-200'\n applyLinkTarget(props, linkTargetOptions)\n break\n }\n case 'ul':\n props.className = 'list-disc list-inside mb-4 space-y-2 ml-4'\n break\n case 'ol':\n props.className = 'list-decimal list-inside mb-4 space-y-2 ml-4'\n break\n case 'li':\n props.className = 'mb-1'\n break\n case 'blockquote':\n props.className = 'border-l-4 border-primary pl-4 italic my-4 text-muted-foreground'\n break\n case 'code':\n props.className =\n (props.className ? props.className + ' ' : '') +\n 'bg-muted px-1 py-0.5 rounded text-sm font-mono'\n break\n case 'pre':\n props.className = 'bg-muted rounded-lg p-4 overflow-x-auto my-4'\n break\n case 'img':\n props.className = 'rounded-lg my-6 w-full max-w-2xl mx-auto'\n break\n case 'table':\n props.className = 'border-collapse border border-border my-4 w-full'\n break\n case 'th':\n props.className = 'border border-border px-2 py-1 bg-muted font-semibold'\n break\n case 'td':\n props.className = 'border border-border px-2 py-1'\n break\n case 'hr':\n props.className = 'my-8 border-border'\n break\n default:\n break\n }\n\n node.properties = props\n }\n })\n\n // Second pass: Fix footnotes and references\n visit(tree, 'element', (node: Element) => {\n processFootnoteRef(node)\n processFootnotesSection(node)\n })\n }\n}\n\n// Custom rehype plugin to process image URLs\nconst rehypeProcessImages: Plugin<[{ articleSlug?: string }], Root> = (options = {}) => {\n return (tree: Root) => {\n visit(tree, 'element', (node: Element) => {\n if (node.tagName === 'img' && node.properties) {\n const src = node.properties.src\n if (src && typeof src === 'string' && options.articleSlug) {\n // Sanitize the image path\n const sanitizedSrc = sanitizeImagePath(src, options.articleSlug)\n if (sanitizedSrc) {\n node.properties.src = sanitizedSrc\n } else {\n // If sanitization fails, use a placeholder\n reportArticlesError({\n code: 'unsafe-image-path',\n message: 'Using placeholder for unsafe markdown image path.',\n context: { articleSlug: options.articleSlug, path: src },\n })\n node.properties.src = '/placeholder-logo.png'\n }\n }\n }\n })\n }\n}\n\nexport async function markdownToHtml(\n markdown: string,\n articleSlug?: string,\n config?: ArticlesConfig\n) {\n try {\n // Start building the remark processor\n let processor = remark()\n .use(remarkParse)\n .use(remarkGfm)\n .use(remarkGithubBlockquoteAlert)\n .use(remarkRehype)\n .use(customRenderer, getLinkTargetOptions(config))\n .use(rehypeSlug)\n // @ts-ignore\n .use(rehypePrism)\n .use(rehypeSanitize, {\n attributes: {\n '*': ['className', 'class', 'id'],\n a: ['href', 'target', 'rel', 'id'],\n img: ['src', 'alt'],\n },\n })\n\n // Add image processing plugin if articleSlug is provided\n if (articleSlug) {\n processor = processor.use(rehypeProcessImages, { articleSlug })\n }\n\n const result = await processor.use(rehypeStringify).process(markdown)\n\n return result.toString()\n } catch (error) {\n reportArticlesError({\n code: 'markdown-conversion-failed',\n message: 'Unable to convert markdown to HTML.',\n error,\n context: { articleSlug },\n })\n // Return the original markdown as fallback\n return markdown\n }\n}\n\nfunction nodeTextValue(c: ElementContent): string {\n return c.type === 'text' ? (c as { value: string }).value : ''\n}\n\nfunction extractHeadingItem(node: Element): TocItem | null {\n const match = /^h([1-6])$/.exec(node.tagName)\n if (!match) return null\n const id = typeof node.properties?.id === 'string' ? node.properties.id : ''\n const text = node.children.map(nodeTextValue).join('')\n if (!id || !text) return null\n return { id, depth: Number.parseInt(match[1], 10), text }\n}\n\nexport async function extractToc(markdown: string): Promise<TocItem[]> {\n const headings: TocItem[] = []\n const collectHeadings: Plugin<[], Root> = () => (tree: Root) => {\n visit(tree, 'element', (node: Element) => {\n const item = extractHeadingItem(node)\n if (item) headings.push(item)\n })\n }\n await remark()\n .use(remarkParse)\n .use(remarkGfm)\n .use(remarkRehype)\n .use(rehypeSlug)\n .use(collectHeadings)\n .use(rehypeStringify)\n .process(markdown)\n return headings\n}\n","export type ArticlesErrorCode =\n | 'article-directory-read-failed'\n | 'article-load-failed'\n | 'article-markdown-load-failed'\n | 'markdown-conversion-failed'\n | 'unsafe-image-path'\n\nexport type ArticlesErrorContext = Readonly<Record<string, string | number | boolean | undefined>>\n\nexport type ArticlesErrorReport = Readonly<{\n code: ArticlesErrorCode\n message: string\n error?: unknown\n context?: ArticlesErrorContext\n}>\n\nexport type ArticlesErrorHandler = (report: ArticlesErrorReport) => void\n\nfunction defaultArticlesErrorHandler(report: ArticlesErrorReport): void {\n if (process.env.NODE_ENV === 'production') return\n const context = report.context ? ` ${JSON.stringify(report.context)}` : ''\n const detail = report.error instanceof Error ? `: ${report.error.message}` : ''\n console.warn(`[articles:${report.code}] ${report.message}${context}${detail}`)\n}\n\nlet articlesErrorHandler: ArticlesErrorHandler = defaultArticlesErrorHandler\n\nexport function setArticlesErrorHandler(handler?: ArticlesErrorHandler): void {\n articlesErrorHandler = handler ?? defaultArticlesErrorHandler\n}\n\nexport function reportArticlesError(report: ArticlesErrorReport): void {\n articlesErrorHandler(report)\n}\n","// Next.js API route handler factory — Node.js runtime only\n// Import from '@fullstackdatasolutions/articles/nextjs'\nimport type { NextRequest } from 'next/server'\nimport { getArticleMarkdownResponse } from './server-articles'\nimport type { ArticlesConfig } from './articlesConfig'\n\n/**\n * Returns a Next.js App Router GET handler for serving article markdown.\n *\n * Usage in app/api/articles-markdown/[...slug]/route.ts:\n * import { createArticleMarkdownHandler } from '@fullstackdatasolutions/articles/nextjs'\n * import { siteConfig } from '@/config/articles'\n * export const { GET } = createArticleMarkdownHandler(siteConfig)\n */\nexport function createArticleMarkdownHandler(config: ArticlesConfig) {\n async function GET(\n _request: NextRequest,\n context: { params: Promise<Record<string, string | string[]>> }\n ) {\n const params = await context.params\n const slugParts = params['slug']\n const slug = Array.isArray(slugParts) ? slugParts.join('/') : (slugParts ?? '')\n const cleanSlug = slug.endsWith('.md') ? slug.slice(0, -3) : slug\n return getArticleMarkdownResponse(cleanSlug, config)\n }\n return { GET }\n}\n"],"mappings":"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","names":["sanitizeImagePath"]}
|