-
Notifications
You must be signed in to change notification settings - Fork 5.4k
Expand file tree
/
Copy pathmd.ts
More file actions
187 lines (157 loc) · 5.71 KB
/
md.ts
File metadata and controls
187 lines (157 loc) · 5.71 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
import fsp from "fs/promises"
import { extname, join } from "path"
import matter from "gray-matter"
import readingTime from "reading-time"
import type { Frontmatter, ITutorial, Skill, SlugPageParams } from "@/lib/types"
import { dateToString } from "@/lib/utils/date"
import internalTutorialSlugs from "@/data/internalTutorials.json"
import { DEFAULT_LOCALE } from "@/lib/constants"
import { toPosixPath } from "./relativePath"
function getContentRoot() {
return join(process.cwd(), "public/content")
}
export const getPostSlugs = async (dir: string, filterRegex?: RegExp) => {
const contentRoot = getContentRoot()
const _dir = join(contentRoot, dir)
try {
// Get an array of all files and directories in the passed directory using `fs.readdirSync`
const dirContents = await fsp.readdir(_dir)
const files: string[] = []
// Create the full path of the file/directory by concatenating the passed directory and file/directory name
for (const fileOrDir of dirContents) {
// file = "about", "bridges".... "translations" (<-- skip that one)...
const path = join(_dir, fileOrDir)
const stats = await fsp.stat(path)
if (stats.isDirectory()) {
// Skip nested translations directory
if (fileOrDir === "translations") continue
// Skip videos directory — video pages have their own dedicated route
if (fileOrDir === "videos") continue
// If it is a directory, recursively call the `getPostSlugs` function with the
// directory path and the files array
const nestedDir = join(dir, fileOrDir)
const nestedFiles = await getPostSlugs(nestedDir, filterRegex)
files.push(...nestedFiles)
continue
}
if (filterRegex?.test(path)) continue
// If the current file is not a markdown file, skip it
if (extname(path) !== ".md") continue
const sanitizedPath = toPosixPath(
path.replace(contentRoot, "").replace("/index.md", "")
)
files.push(sanitizedPath)
}
return files
} catch (error) {
// If directory doesn't exist (e.g., in Netlify serverless environment), return empty array
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
console.warn(
`Content directory ${_dir} not found, returning empty slug list`
)
return []
}
// Re-throw other errors
throw error
}
}
export const getTutorialsData = async (
locale: string
): Promise<ITutorial[]> => {
const contentRoot = join(process.cwd(), "public/content")
const tutorialPromises = (internalTutorialSlugs as string[]).map(
async (slug) => {
try {
let fileContents: string
let isTranslated = true
const enPath = join(
contentRoot,
"developers/tutorials",
slug,
"index.md"
)
if (locale === DEFAULT_LOCALE) {
fileContents = await fsp.readFile(enPath, "utf-8")
} else {
const translatedPath = join(
contentRoot,
"translations",
locale,
"developers/tutorials",
slug,
"index.md"
)
try {
fileContents = await fsp.readFile(translatedPath, "utf-8")
} catch {
fileContents = await fsp.readFile(enPath, "utf-8")
isTranslated = false
}
}
const { data, content } = matter(fileContents)
const frontmatter = data as Frontmatter
return {
href: `/developers/tutorials/${slug}`,
title: frontmatter.title,
description: frontmatter.description,
author: frontmatter.author || "",
tags: frontmatter.tags,
skill: frontmatter.skill as Skill,
timeToRead: Math.round(readingTime(content).minutes),
published: dateToString(frontmatter.published),
lang: frontmatter.lang,
isExternal: false,
isTranslated,
}
} catch (error) {
// Only warn if English content is missing (actual error)
console.warn(`Error reading tutorial ${slug}:`, error)
return null
}
}
)
const results = await Promise.all(tutorialPromises)
// Filter out null results (missing tutorials)
return results.filter((tutorial) => tutorial !== null) as ITutorial[]
}
export const checkPathValidity = (
validPaths: SlugPageParams[],
{ slug: slugArray }: SlugPageParams
): boolean =>
validPaths.some((path) => path.slug.join("/") === slugArray.join("/"))
/**
* Strips markdown syntax from text, leaving plain text.
* For preview/snippet text where markdown shouldn't be visible.
*
* @param text - Text with markdown syntax
* @param preserveNewlines - When true, collapses runs of 3+ newlines to 2
* instead of collapsing all whitespace to single spaces. Useful for
* structured output like JSON-LD transcripts.
* @returns Plain text with markdown markers removed
*/
export function stripMarkdown(
text: string,
preserveNewlines?: boolean
): string {
let result = text
// Remove bold/italic (**text** or __text__)
.replace(/(\*\*|__)(.*?)\1/g, "$2")
// Remove italic (*text* or _text_)
.replace(/(\*|_)(.*?)\1/g, "$2")
// Remove inline code (`code`)
.replace(/`([^`]+)`/g, "$1")
// Remove links [text](url) -> text
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
// Remove images  -> empty
.replace(/!\[([^\]]*)\]\([^)]+\)/g, "")
// Remove headings (# text)
.replace(/^#{1,6}\s+/gm, "")
// Remove list markers (- or * or 1.)
.replace(/^[\s]*[-*+]\s+/gm, "")
.replace(/^[\s]*\d+\.\s+/gm, "")
// Clean up whitespace
result = preserveNewlines
? result.replace(/\n{3,}/g, "\n\n")
: result.replace(/\s+/g, " ")
return result.trim()
}