@fullstackdatasolutions/articles 0.5.1 → 0.7.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/README.md +51 -6
- package/dist/index.cjs +64 -38
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -1
- package/dist/index.d.ts +11 -1
- package/dist/index.js +62 -37
- package/dist/index.js.map +1 -1
- package/dist/server.cjs +87 -24
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +3 -1
- package/dist/server.d.ts +3 -1
- package/dist/server.js +86 -23
- package/dist/server.js.map +1 -1
- package/package.json +11 -11
- package/src/ArticleBackLink.tsx +32 -0
- package/src/ArticleContent.tsx +4 -7
- package/src/__tests__/ArticleBackLink.test.tsx +89 -0
- package/src/__tests__/ArticleContent.test.tsx +72 -28
- package/src/__tests__/articlesConfig.test.ts +57 -0
- package/src/__tests__/markdown.test.ts +278 -0
- package/src/__tests__/renderMdx.test.tsx +280 -0
- package/src/__tests__/server-articles.test.ts +36 -1
- package/src/articlesConfig.ts +2 -0
- package/src/index.ts +1 -0
- package/src/markdown.ts +8 -6
- package/src/renderMdx.tsx +47 -0
- package/src/server-articles.ts +20 -11
package/src/markdown.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { Element, Root, ElementContent } from 'hast'
|
|
2
|
-
import rehypeAutolinkHeadings from 'rehype-autolink-headings'
|
|
3
2
|
import rehypePrism from 'rehype-prism-plus'
|
|
4
3
|
import rehypeSanitize from 'rehype-sanitize'
|
|
5
4
|
import rehypeSlug from 'rehype-slug'
|
|
@@ -157,7 +156,7 @@ function processFootnotesSection(node: Element): void {
|
|
|
157
156
|
if (node.properties) node.properties.className = undefined
|
|
158
157
|
}
|
|
159
158
|
|
|
160
|
-
const customRenderer: Plugin<[], Root> = () => {
|
|
159
|
+
export const customRenderer: Plugin<[], Root> = () => {
|
|
161
160
|
return (tree: Root) => {
|
|
162
161
|
// First pass: Apply general styles
|
|
163
162
|
visit(tree, 'element', (node: Element) => {
|
|
@@ -180,11 +179,15 @@ const customRenderer: Plugin<[], Root> = () => {
|
|
|
180
179
|
case 'p':
|
|
181
180
|
props.className = 'text-muted-foreground leading-relaxed mb-4'
|
|
182
181
|
break
|
|
183
|
-
case 'a':
|
|
182
|
+
case 'a': {
|
|
184
183
|
props.className = 'text-primary hover:underline transition-colors duration-200'
|
|
185
|
-
props.
|
|
186
|
-
|
|
184
|
+
const href = typeof props.href === 'string' ? props.href : ''
|
|
185
|
+
if (!href.startsWith('#')) {
|
|
186
|
+
props.target = '_blank'
|
|
187
|
+
props.rel = 'noopener noreferrer'
|
|
188
|
+
}
|
|
187
189
|
break
|
|
190
|
+
}
|
|
188
191
|
case 'ul':
|
|
189
192
|
props.className = 'list-disc list-inside mb-4 space-y-2 ml-4'
|
|
190
193
|
break
|
|
@@ -268,7 +271,6 @@ export async function markdownToHtml(markdown: string, articleSlug?: string) {
|
|
|
268
271
|
.use(remarkRehype)
|
|
269
272
|
.use(customRenderer)
|
|
270
273
|
.use(rehypeSlug)
|
|
271
|
-
.use(rehypeAutolinkHeadings, { behavior: 'wrap' })
|
|
272
274
|
// @ts-ignore
|
|
273
275
|
.use(rehypePrism)
|
|
274
276
|
.use(rehypeSanitize, {
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// MDX files must use JSX prop syntax:
|
|
2
|
+
// Correct: <span style={{ color: '#3b82f6' }}>Text</span>
|
|
3
|
+
// Incorrect: <span style="color: #3b82f6">Text</span>
|
|
4
|
+
// Correct: className="..."
|
|
5
|
+
// Incorrect: class="..."
|
|
6
|
+
import React from 'react'
|
|
7
|
+
import type { ComponentType, ImgHTMLAttributes } from 'react'
|
|
8
|
+
import * as devRuntime from 'react/jsx-dev-runtime'
|
|
9
|
+
import * as runtime from 'react/jsx-runtime'
|
|
10
|
+
import { evaluate } from '@mdx-js/mdx'
|
|
11
|
+
import rehypePrism from 'rehype-prism-plus'
|
|
12
|
+
import rehypeSlug from 'rehype-slug'
|
|
13
|
+
import remarkGfm from 'remark-gfm'
|
|
14
|
+
import remarkGithubBlockquoteAlert from 'remark-github-blockquote-alert'
|
|
15
|
+
import { customRenderer } from './markdown'
|
|
16
|
+
|
|
17
|
+
type MdxContent = ComponentType<{
|
|
18
|
+
components?: Record<string, ComponentType<unknown>>
|
|
19
|
+
}>
|
|
20
|
+
|
|
21
|
+
function makeImgComponent(basePath: string) {
|
|
22
|
+
return function MdxImage({ src, alt, ...props }: ImgHTMLAttributes<HTMLImageElement>) {
|
|
23
|
+
const resolvedSrc =
|
|
24
|
+
src && !src.startsWith('http') && !src.startsWith('/') ? `${basePath}/${src}` : src
|
|
25
|
+
return React.createElement('img', { src: resolvedSrc, alt, ...props })
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function renderMdxSource(source: string, basePath?: string) {
|
|
30
|
+
const isDevelopment = process.env.NODE_ENV === 'development'
|
|
31
|
+
|
|
32
|
+
const mdxModule = await evaluate(source, {
|
|
33
|
+
...(isDevelopment ? devRuntime : runtime),
|
|
34
|
+
development: isDevelopment,
|
|
35
|
+
remarkPlugins: [remarkGfm, remarkGithubBlockquoteAlert],
|
|
36
|
+
rehypePlugins: [
|
|
37
|
+
customRenderer,
|
|
38
|
+
rehypeSlug,
|
|
39
|
+
// @ts-ignore
|
|
40
|
+
rehypePrism,
|
|
41
|
+
],
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const Content = mdxModule.default as MdxContent
|
|
45
|
+
const components = basePath ? { img: makeImgComponent(basePath) } : undefined
|
|
46
|
+
return <Content components={components as Record<string, ComponentType<unknown>>} />
|
|
47
|
+
}
|
package/src/server-articles.ts
CHANGED
|
@@ -67,23 +67,32 @@ function findArticleFile(slug: string): { filePath: string; contentType: 'md' |
|
|
|
67
67
|
export function getAvailableArticleSlugs(): string[] {
|
|
68
68
|
try {
|
|
69
69
|
if (!fs.existsSync(articlesDirectory)) return []
|
|
70
|
-
|
|
71
|
-
return items
|
|
72
|
-
.filter((item) => item.isDirectory())
|
|
73
|
-
.filter((dir) => {
|
|
74
|
-
try {
|
|
75
|
-
return findArticleFile(dir.name) !== null
|
|
76
|
-
} catch {
|
|
77
|
-
return false
|
|
78
|
-
}
|
|
79
|
-
})
|
|
80
|
-
.map((dir) => dir.name)
|
|
70
|
+
return walkArticleDir(articlesDirectory, '')
|
|
81
71
|
} catch (error) {
|
|
82
72
|
console.error('Error reading articles directory:', error)
|
|
83
73
|
return []
|
|
84
74
|
}
|
|
85
75
|
}
|
|
86
76
|
|
|
77
|
+
function walkArticleDir(dir: string, baseSlug: string): string[] {
|
|
78
|
+
const slugs: string[] = []
|
|
79
|
+
try {
|
|
80
|
+
const items = fs.readdirSync(dir, { withFileTypes: true })
|
|
81
|
+
for (const item of items) {
|
|
82
|
+
if (!item.isDirectory()) continue
|
|
83
|
+
const slug = baseSlug ? `${baseSlug}/${item.name}` : item.name
|
|
84
|
+
if (findArticleFile(slug) !== null) {
|
|
85
|
+
slugs.push(slug)
|
|
86
|
+
} else {
|
|
87
|
+
slugs.push(...walkArticleDir(path.join(dir, item.name), slug))
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
} catch {
|
|
91
|
+
// ignore unreadable directories
|
|
92
|
+
}
|
|
93
|
+
return slugs
|
|
94
|
+
}
|
|
95
|
+
|
|
87
96
|
function parseDateField(rawDate: unknown): string | undefined {
|
|
88
97
|
if (!rawDate) return undefined
|
|
89
98
|
try {
|