@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/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.target = '_blank'
186
- props.rel = 'noopener noreferrer'
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
+ }
@@ -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
- const items = fs.readdirSync(articlesDirectory, { withFileTypes: true })
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 {