@piqit/resolvers 0.0.2 → 0.0.5
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/dist/file-markdown.d.ts +1 -1
- package/dist/file-markdown.d.ts.map +1 -1
- package/dist/file-markdown.js +2 -2
- package/dist/file-markdown.js.map +1 -1
- package/dist/path-pattern.d.ts +2 -1
- package/dist/path-pattern.d.ts.map +1 -1
- package/dist/path-pattern.js.map +1 -1
- package/dist/static.d.ts +1 -1
- package/dist/static.d.ts.map +1 -1
- package/package.json +9 -9
- package/src/edge.ts +0 -15
- package/src/file-markdown.ts +0 -352
- package/src/frontmatter.ts +0 -269
- package/src/index.ts +0 -65
- package/src/markdown.ts +0 -262
- package/src/path-pattern.ts +0 -226
- package/src/static.ts +0 -227
package/src/frontmatter.ts
DELETED
|
@@ -1,269 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* YAML Frontmatter Extraction
|
|
3
|
-
*
|
|
4
|
-
* Optimized frontmatter parsing that reads only what's needed.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
// =============================================================================
|
|
8
|
-
// Frontmatter Parsing
|
|
9
|
-
// =============================================================================
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Regex to match YAML frontmatter block at the start of content.
|
|
13
|
-
* Matches --- at start, captures content, ends with ---
|
|
14
|
-
*/
|
|
15
|
-
const FRONTMATTER_REGEX = /^---\r?\n([\s\S]*?)\r?\n---/
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Parse YAML frontmatter from content string.
|
|
19
|
-
*
|
|
20
|
-
* Uses a simple YAML parser that handles common cases:
|
|
21
|
-
* - key: value
|
|
22
|
-
* - key: "quoted value"
|
|
23
|
-
* - key: 'quoted value'
|
|
24
|
-
* - key: [array, items]
|
|
25
|
-
* - Multi-line values with proper indentation
|
|
26
|
-
*
|
|
27
|
-
* @param content - The file content (or just the frontmatter portion)
|
|
28
|
-
* @returns The parsed frontmatter object, or null if not found
|
|
29
|
-
*/
|
|
30
|
-
export function parseFrontmatter(content: string): Record<string, unknown> | null {
|
|
31
|
-
const match = FRONTMATTER_REGEX.exec(content)
|
|
32
|
-
if (!match) return null
|
|
33
|
-
|
|
34
|
-
const yaml = match[1]
|
|
35
|
-
return parseSimpleYaml(yaml)
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Extract just the frontmatter portion from content.
|
|
40
|
-
* Returns the raw YAML string without the --- delimiters.
|
|
41
|
-
*
|
|
42
|
-
* @param content - The file content
|
|
43
|
-
* @returns The YAML content, or null if no frontmatter
|
|
44
|
-
*/
|
|
45
|
-
export function extractFrontmatterString(content: string): string | null {
|
|
46
|
-
const match = FRONTMATTER_REGEX.exec(content)
|
|
47
|
-
return match ? match[1] : null
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Get the byte offset where frontmatter ends (after closing ---).
|
|
52
|
-
* Returns 0 if no frontmatter found.
|
|
53
|
-
*
|
|
54
|
-
* @param content - The file content
|
|
55
|
-
* @returns Byte offset after frontmatter, or 0
|
|
56
|
-
*/
|
|
57
|
-
export function getFrontmatterEndOffset(content: string): number {
|
|
58
|
-
const match = FRONTMATTER_REGEX.exec(content)
|
|
59
|
-
if (!match) return 0
|
|
60
|
-
return match[0].length
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// =============================================================================
|
|
64
|
-
// Simple YAML Parser
|
|
65
|
-
// =============================================================================
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Parse simple YAML into an object.
|
|
69
|
-
* Handles the common patterns found in markdown frontmatter.
|
|
70
|
-
*/
|
|
71
|
-
function parseSimpleYaml(yaml: string): Record<string, unknown> {
|
|
72
|
-
const result: Record<string, unknown> = {}
|
|
73
|
-
const lines = yaml.split(/\r?\n/)
|
|
74
|
-
|
|
75
|
-
let currentKey: string | null = null
|
|
76
|
-
let currentValue: string[] = []
|
|
77
|
-
let inMultiline = false
|
|
78
|
-
let inBlockArray = false
|
|
79
|
-
let blockArrayItems: unknown[] = []
|
|
80
|
-
let multilineIndent = 0
|
|
81
|
-
|
|
82
|
-
function commitValue() {
|
|
83
|
-
if (currentKey) {
|
|
84
|
-
if (inBlockArray) {
|
|
85
|
-
// Commit block array
|
|
86
|
-
result[currentKey] = blockArrayItems
|
|
87
|
-
blockArrayItems = []
|
|
88
|
-
inBlockArray = false
|
|
89
|
-
} else if (currentValue.length === 1) {
|
|
90
|
-
result[currentKey] = parseYamlValue(currentValue[0])
|
|
91
|
-
} else if (currentValue.length > 1) {
|
|
92
|
-
result[currentKey] = currentValue.join("\n")
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
currentKey = null
|
|
96
|
-
currentValue = []
|
|
97
|
-
inMultiline = false
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
for (const line of lines) {
|
|
101
|
-
// Check for block array item (- value)
|
|
102
|
-
const arrayItemMatch = line.match(/^(\s+)-\s+(.*)$/)
|
|
103
|
-
if (arrayItemMatch && currentKey && (inBlockArray || (inMultiline && currentValue.length === 0))) {
|
|
104
|
-
inBlockArray = true
|
|
105
|
-
inMultiline = false
|
|
106
|
-
const itemValue = arrayItemMatch[2].trim()
|
|
107
|
-
blockArrayItems.push(parseYamlValue(itemValue))
|
|
108
|
-
continue
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Check for key: value pattern
|
|
112
|
-
const keyMatch = line.match(/^([a-zA-Z_][a-zA-Z0-9_-]*)\s*:\s*(.*)$/)
|
|
113
|
-
|
|
114
|
-
if (keyMatch && !inMultiline) {
|
|
115
|
-
// Commit previous key-value
|
|
116
|
-
commitValue()
|
|
117
|
-
|
|
118
|
-
currentKey = keyMatch[1]
|
|
119
|
-
const valueStr = keyMatch[2].trim()
|
|
120
|
-
|
|
121
|
-
if (valueStr === "" || valueStr === "|" || valueStr === ">") {
|
|
122
|
-
// Multiline value or block array starts on next line
|
|
123
|
-
inMultiline = true
|
|
124
|
-
multilineIndent = 0
|
|
125
|
-
} else {
|
|
126
|
-
currentValue = [valueStr]
|
|
127
|
-
}
|
|
128
|
-
} else if (inMultiline && currentKey && !inBlockArray) {
|
|
129
|
-
// Continuation of multiline value
|
|
130
|
-
if (line.trim() === "") {
|
|
131
|
-
currentValue.push("")
|
|
132
|
-
} else {
|
|
133
|
-
const indent = line.search(/\S/)
|
|
134
|
-
if (indent > 0) {
|
|
135
|
-
if (multilineIndent === 0) {
|
|
136
|
-
multilineIndent = indent
|
|
137
|
-
}
|
|
138
|
-
currentValue.push(line.slice(multilineIndent))
|
|
139
|
-
} else {
|
|
140
|
-
// No indent means new key or end of multiline
|
|
141
|
-
commitValue()
|
|
142
|
-
// Re-process this line
|
|
143
|
-
const reKeyMatch = line.match(/^([a-zA-Z_][a-zA-Z0-9_-]*)\s*:\s*(.*)$/)
|
|
144
|
-
if (reKeyMatch) {
|
|
145
|
-
currentKey = reKeyMatch[1]
|
|
146
|
-
const valueStr = reKeyMatch[2].trim()
|
|
147
|
-
if (valueStr === "" || valueStr === "|" || valueStr === ">") {
|
|
148
|
-
inMultiline = true
|
|
149
|
-
multilineIndent = 0
|
|
150
|
-
} else {
|
|
151
|
-
currentValue = [valueStr]
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
} else if (currentKey && line.trim() === "") {
|
|
157
|
-
// Empty line after value - keep going
|
|
158
|
-
} else if (!currentKey && line.trim() !== "") {
|
|
159
|
-
// New key-value
|
|
160
|
-
const newKeyMatch = line.match(/^([a-zA-Z_][a-zA-Z0-9_-]*)\s*:\s*(.*)$/)
|
|
161
|
-
if (newKeyMatch) {
|
|
162
|
-
currentKey = newKeyMatch[1]
|
|
163
|
-
const valueStr = newKeyMatch[2].trim()
|
|
164
|
-
if (valueStr === "" || valueStr === "|" || valueStr === ">") {
|
|
165
|
-
inMultiline = true
|
|
166
|
-
multilineIndent = 0
|
|
167
|
-
} else {
|
|
168
|
-
currentValue = [valueStr]
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Commit final value
|
|
175
|
-
commitValue()
|
|
176
|
-
|
|
177
|
-
return result
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Parse a single YAML value.
|
|
182
|
-
*/
|
|
183
|
-
function parseYamlValue(value: string): unknown {
|
|
184
|
-
// Remove quotes
|
|
185
|
-
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
186
|
-
return value.slice(1, -1)
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// Check for array
|
|
190
|
-
if (value.startsWith("[") && value.endsWith("]")) {
|
|
191
|
-
const inner = value.slice(1, -1)
|
|
192
|
-
if (inner.trim() === "") return []
|
|
193
|
-
return inner.split(",").map((item) => parseYamlValue(item.trim()))
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// Check for booleans
|
|
197
|
-
if (value === "true") return true
|
|
198
|
-
if (value === "false") return false
|
|
199
|
-
|
|
200
|
-
// Check for null
|
|
201
|
-
if (value === "null" || value === "~") return null
|
|
202
|
-
|
|
203
|
-
// Check for numbers
|
|
204
|
-
const num = Number(value)
|
|
205
|
-
if (!isNaN(num) && value !== "") return num
|
|
206
|
-
|
|
207
|
-
// Check for dates (ISO format)
|
|
208
|
-
if (/^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2})?/.test(value)) {
|
|
209
|
-
const date = new Date(value)
|
|
210
|
-
if (!isNaN(date.getTime())) return date.toISOString()
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
return value
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// =============================================================================
|
|
217
|
-
// Optimized File Reading
|
|
218
|
-
// =============================================================================
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* Read only the frontmatter from a file, optimized for large files.
|
|
222
|
-
*
|
|
223
|
-
* Reads incrementally until we find the closing ---, avoiding reading
|
|
224
|
-
* the entire file into memory.
|
|
225
|
-
*
|
|
226
|
-
* @param path - Path to the markdown file
|
|
227
|
-
* @param maxBytes - Maximum bytes to read looking for frontmatter (default 8KB)
|
|
228
|
-
* @returns The parsed frontmatter, or null if not found
|
|
229
|
-
*/
|
|
230
|
-
export async function readFrontmatter(
|
|
231
|
-
path: string,
|
|
232
|
-
maxBytes = 8192
|
|
233
|
-
): Promise<Record<string, unknown> | null> {
|
|
234
|
-
try {
|
|
235
|
-
const file = Bun.file(path)
|
|
236
|
-
const size = file.size
|
|
237
|
-
|
|
238
|
-
// Read up to maxBytes or file size
|
|
239
|
-
const bytesToRead = Math.min(maxBytes, size)
|
|
240
|
-
const buffer = await file.slice(0, bytesToRead).text()
|
|
241
|
-
|
|
242
|
-
return parseFrontmatter(buffer)
|
|
243
|
-
} catch {
|
|
244
|
-
return null
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* Read frontmatter and return the offset where body starts.
|
|
250
|
-
* Useful when you need both frontmatter and body.
|
|
251
|
-
*
|
|
252
|
-
* @param path - Path to the markdown file
|
|
253
|
-
* @returns Object with frontmatter and body start offset
|
|
254
|
-
*/
|
|
255
|
-
export async function readFrontmatterWithOffset(
|
|
256
|
-
path: string
|
|
257
|
-
): Promise<{ frontmatter: Record<string, unknown> | null; bodyOffset: number }> {
|
|
258
|
-
try {
|
|
259
|
-
const file = Bun.file(path)
|
|
260
|
-
const content = await file.text()
|
|
261
|
-
|
|
262
|
-
const frontmatter = parseFrontmatter(content)
|
|
263
|
-
const bodyOffset = getFrontmatterEndOffset(content)
|
|
264
|
-
|
|
265
|
-
return { frontmatter, bodyOffset }
|
|
266
|
-
} catch {
|
|
267
|
-
return { frontmatter: null, bodyOffset: 0 }
|
|
268
|
-
}
|
|
269
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @piqit/resolvers - Resolver implementations for piq v2
|
|
3
|
-
*
|
|
4
|
-
* Provides resolvers for querying structured content:
|
|
5
|
-
* - fileMarkdown: Query markdown files from filesystem (Node.js/Bun)
|
|
6
|
-
* - staticContent: Query pre-compiled content (Edge/Workers)
|
|
7
|
-
*
|
|
8
|
-
* @packageDocumentation
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
// =============================================================================
|
|
12
|
-
// Filesystem Resolver (Node.js/Bun only)
|
|
13
|
-
// =============================================================================
|
|
14
|
-
|
|
15
|
-
export { fileMarkdown } from "./file-markdown"
|
|
16
|
-
export type { FileMarkdownOptions, FileMarkdownResult } from "./file-markdown"
|
|
17
|
-
|
|
18
|
-
// =============================================================================
|
|
19
|
-
// Static Content Resolver (Edge/Workers compatible)
|
|
20
|
-
// =============================================================================
|
|
21
|
-
|
|
22
|
-
export { staticContent, staticResolver } from "./static"
|
|
23
|
-
|
|
24
|
-
// =============================================================================
|
|
25
|
-
// Path Pattern Utilities
|
|
26
|
-
// =============================================================================
|
|
27
|
-
|
|
28
|
-
export { compilePattern, createParamsSchema } from "./path-pattern"
|
|
29
|
-
export type {
|
|
30
|
-
CompiledPattern,
|
|
31
|
-
ExtractParams,
|
|
32
|
-
PathParams,
|
|
33
|
-
} from "./path-pattern"
|
|
34
|
-
|
|
35
|
-
// =============================================================================
|
|
36
|
-
// Frontmatter Utilities
|
|
37
|
-
// =============================================================================
|
|
38
|
-
|
|
39
|
-
export {
|
|
40
|
-
parseFrontmatter,
|
|
41
|
-
extractFrontmatterString,
|
|
42
|
-
getFrontmatterEndOffset,
|
|
43
|
-
readFrontmatter,
|
|
44
|
-
readFrontmatterWithOffset,
|
|
45
|
-
} from "./frontmatter"
|
|
46
|
-
|
|
47
|
-
// =============================================================================
|
|
48
|
-
// Markdown Utilities
|
|
49
|
-
// =============================================================================
|
|
50
|
-
|
|
51
|
-
export {
|
|
52
|
-
parseMarkdownBody,
|
|
53
|
-
extractHeadings,
|
|
54
|
-
slugify,
|
|
55
|
-
markdownToHtml,
|
|
56
|
-
readMarkdownBody,
|
|
57
|
-
readParsedBody,
|
|
58
|
-
} from "./markdown"
|
|
59
|
-
|
|
60
|
-
export type {
|
|
61
|
-
BodyOptions,
|
|
62
|
-
BodyResult,
|
|
63
|
-
BodyShape,
|
|
64
|
-
Heading,
|
|
65
|
-
} from "./markdown"
|
package/src/markdown.ts
DELETED
|
@@ -1,262 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Markdown Body Parsing
|
|
3
|
-
*
|
|
4
|
-
* Parses markdown body content to extract:
|
|
5
|
-
* - Raw markdown text
|
|
6
|
-
* - HTML (basic conversion)
|
|
7
|
-
* - Headings with slugs
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { getFrontmatterEndOffset } from "./frontmatter"
|
|
11
|
-
|
|
12
|
-
// =============================================================================
|
|
13
|
-
// Types
|
|
14
|
-
// =============================================================================
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Options for parsing markdown body.
|
|
18
|
-
*/
|
|
19
|
-
export interface BodyOptions {
|
|
20
|
-
/** Include raw markdown text */
|
|
21
|
-
raw?: boolean
|
|
22
|
-
/** Include HTML conversion */
|
|
23
|
-
html?: boolean
|
|
24
|
-
/** Extract headings with depth and slugs */
|
|
25
|
-
headings?: boolean
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* A heading extracted from markdown.
|
|
30
|
-
*/
|
|
31
|
-
export interface Heading {
|
|
32
|
-
/** Heading depth (1-6) */
|
|
33
|
-
depth: number
|
|
34
|
-
/** Heading text content */
|
|
35
|
-
text: string
|
|
36
|
-
/** URL-safe slug */
|
|
37
|
-
slug: string
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Result of parsing markdown body.
|
|
42
|
-
*/
|
|
43
|
-
export interface BodyResult {
|
|
44
|
-
/** Raw markdown text (if requested) */
|
|
45
|
-
raw?: string
|
|
46
|
-
/** HTML content (if requested) */
|
|
47
|
-
html?: string
|
|
48
|
-
/** Extracted headings (if requested) */
|
|
49
|
-
headings?: Heading[]
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Type-level shape of body result based on options.
|
|
54
|
-
*/
|
|
55
|
-
export type BodyShape<T extends BodyOptions> = {
|
|
56
|
-
[K in keyof T as T[K] extends true ? K : never]: K extends "raw"
|
|
57
|
-
? string
|
|
58
|
-
: K extends "html"
|
|
59
|
-
? string
|
|
60
|
-
: K extends "headings"
|
|
61
|
-
? Heading[]
|
|
62
|
-
: never
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// =============================================================================
|
|
66
|
-
// Markdown Parsing
|
|
67
|
-
// =============================================================================
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Parse markdown body content.
|
|
71
|
-
*
|
|
72
|
-
* @param content - The full file content (including frontmatter if present)
|
|
73
|
-
* @param options - What to extract from the markdown
|
|
74
|
-
* @returns The requested body parts
|
|
75
|
-
*/
|
|
76
|
-
export function parseMarkdownBody(content: string, options: BodyOptions): BodyResult {
|
|
77
|
-
// Strip frontmatter if present
|
|
78
|
-
const bodyOffset = getFrontmatterEndOffset(content)
|
|
79
|
-
const rawBody = content.slice(bodyOffset).replace(/^[\r\n]+/, "")
|
|
80
|
-
|
|
81
|
-
const result: BodyResult = {}
|
|
82
|
-
|
|
83
|
-
if (options.raw) {
|
|
84
|
-
result.raw = rawBody
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
if (options.html) {
|
|
88
|
-
result.html = markdownToHtml(rawBody)
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if (options.headings) {
|
|
92
|
-
result.headings = extractHeadings(rawBody)
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return result
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// =============================================================================
|
|
99
|
-
// Heading Extraction
|
|
100
|
-
// =============================================================================
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Regex to match ATX-style headings (# Heading).
|
|
104
|
-
*/
|
|
105
|
-
const HEADING_REGEX = /^(#{1,6})\s+(.+)$/gm
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Extract headings from markdown text.
|
|
109
|
-
*
|
|
110
|
-
* @param markdown - The raw markdown content
|
|
111
|
-
* @returns Array of headings with depth, text, and slug
|
|
112
|
-
*/
|
|
113
|
-
export function extractHeadings(markdown: string): Heading[] {
|
|
114
|
-
const headings: Heading[] = []
|
|
115
|
-
let match: RegExpExecArray | null
|
|
116
|
-
|
|
117
|
-
const regex = new RegExp(HEADING_REGEX.source, "gm")
|
|
118
|
-
while ((match = regex.exec(markdown)) !== null) {
|
|
119
|
-
const depth = match[1].length
|
|
120
|
-
const text = match[2].trim()
|
|
121
|
-
const slug = slugify(text)
|
|
122
|
-
|
|
123
|
-
headings.push({ depth, text, slug })
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
return headings
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Convert text to a URL-safe slug.
|
|
131
|
-
*
|
|
132
|
-
* @param text - The text to slugify
|
|
133
|
-
* @returns A URL-safe slug
|
|
134
|
-
*/
|
|
135
|
-
export function slugify(text: string): string {
|
|
136
|
-
return text
|
|
137
|
-
.toLowerCase()
|
|
138
|
-
.replace(/[^\w\s-]/g, "") // Remove non-word chars
|
|
139
|
-
.replace(/\s+/g, "-") // Replace spaces with dashes
|
|
140
|
-
.replace(/--+/g, "-") // Replace multiple dashes
|
|
141
|
-
.replace(/^-|-$/g, "") // Trim dashes from ends
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// =============================================================================
|
|
145
|
-
// Basic Markdown to HTML
|
|
146
|
-
// =============================================================================
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Convert markdown to HTML.
|
|
150
|
-
*
|
|
151
|
-
* This is a basic implementation that handles common patterns.
|
|
152
|
-
* For production use, consider using a proper markdown parser.
|
|
153
|
-
*
|
|
154
|
-
* @param markdown - The raw markdown content
|
|
155
|
-
* @returns HTML string
|
|
156
|
-
*/
|
|
157
|
-
export function markdownToHtml(markdown: string): string {
|
|
158
|
-
let html = markdown
|
|
159
|
-
|
|
160
|
-
// Escape HTML entities first
|
|
161
|
-
html = html
|
|
162
|
-
.replace(/&/g, "&")
|
|
163
|
-
.replace(/</g, "<")
|
|
164
|
-
.replace(/>/g, ">")
|
|
165
|
-
|
|
166
|
-
// Headings (must be on their own line)
|
|
167
|
-
html = html.replace(/^######\s+(.+)$/gm, "<h6>$1</h6>")
|
|
168
|
-
html = html.replace(/^#####\s+(.+)$/gm, "<h5>$1</h5>")
|
|
169
|
-
html = html.replace(/^####\s+(.+)$/gm, "<h4>$1</h4>")
|
|
170
|
-
html = html.replace(/^###\s+(.+)$/gm, "<h3>$1</h3>")
|
|
171
|
-
html = html.replace(/^##\s+(.+)$/gm, "<h2>$1</h2>")
|
|
172
|
-
html = html.replace(/^#\s+(.+)$/gm, "<h1>$1</h1>")
|
|
173
|
-
|
|
174
|
-
// Code blocks (fenced)
|
|
175
|
-
html = html.replace(/```(\w*)\n([\s\S]*?)```/g, (_match, _lang, code) => {
|
|
176
|
-
return `<pre><code>${code.trim()}</code></pre>`
|
|
177
|
-
})
|
|
178
|
-
|
|
179
|
-
// Inline code
|
|
180
|
-
html = html.replace(/`([^`]+)`/g, "<code>$1</code>")
|
|
181
|
-
|
|
182
|
-
// Bold
|
|
183
|
-
html = html.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
|
|
184
|
-
html = html.replace(/__([^_]+)__/g, "<strong>$1</strong>")
|
|
185
|
-
|
|
186
|
-
// Italic
|
|
187
|
-
html = html.replace(/\*([^*]+)\*/g, "<em>$1</em>")
|
|
188
|
-
html = html.replace(/_([^_]+)_/g, "<em>$1</em>")
|
|
189
|
-
|
|
190
|
-
// Links
|
|
191
|
-
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>')
|
|
192
|
-
|
|
193
|
-
// Images
|
|
194
|
-
html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1">')
|
|
195
|
-
|
|
196
|
-
// Horizontal rules
|
|
197
|
-
html = html.replace(/^---+$/gm, "<hr>")
|
|
198
|
-
html = html.replace(/^\*\*\*+$/gm, "<hr>")
|
|
199
|
-
|
|
200
|
-
// Unordered lists (simplified)
|
|
201
|
-
html = html.replace(/^[-*+]\s+(.+)$/gm, "<li>$1</li>")
|
|
202
|
-
|
|
203
|
-
// Ordered lists (simplified)
|
|
204
|
-
html = html.replace(/^\d+\.\s+(.+)$/gm, "<li>$1</li>")
|
|
205
|
-
|
|
206
|
-
// Wrap consecutive <li> in <ul> or <ol>
|
|
207
|
-
html = html.replace(/((?:<li>.*<\/li>\n?)+)/g, "<ul>\n$1</ul>\n")
|
|
208
|
-
|
|
209
|
-
// Blockquotes
|
|
210
|
-
html = html.replace(/^>\s+(.+)$/gm, "<blockquote>$1</blockquote>")
|
|
211
|
-
|
|
212
|
-
// Paragraphs (wrap remaining text blocks)
|
|
213
|
-
// Split by double newlines and wrap non-block elements
|
|
214
|
-
const blocks = html.split(/\n\n+/)
|
|
215
|
-
html = blocks
|
|
216
|
-
.map((block) => {
|
|
217
|
-
const trimmed = block.trim()
|
|
218
|
-
if (!trimmed) return ""
|
|
219
|
-
// Don't wrap if already a block element
|
|
220
|
-
if (
|
|
221
|
-
/^<(h[1-6]|ul|ol|li|pre|blockquote|hr|p)/.test(trimmed) ||
|
|
222
|
-
/^<\/?(h[1-6]|ul|ol|li|pre|blockquote)>$/.test(trimmed)
|
|
223
|
-
) {
|
|
224
|
-
return trimmed
|
|
225
|
-
}
|
|
226
|
-
return `<p>${trimmed.replace(/\n/g, "<br>")}</p>`
|
|
227
|
-
})
|
|
228
|
-
.filter(Boolean)
|
|
229
|
-
.join("\n")
|
|
230
|
-
|
|
231
|
-
return html
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// =============================================================================
|
|
235
|
-
// File Reading Utilities
|
|
236
|
-
// =============================================================================
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* Read just the markdown body from a file (skipping frontmatter).
|
|
240
|
-
*
|
|
241
|
-
* @param path - Path to the markdown file
|
|
242
|
-
* @returns The raw markdown body
|
|
243
|
-
*/
|
|
244
|
-
export async function readMarkdownBody(path: string): Promise<string> {
|
|
245
|
-
const file = Bun.file(path)
|
|
246
|
-
const content = await file.text()
|
|
247
|
-
const bodyOffset = getFrontmatterEndOffset(content)
|
|
248
|
-
return content.slice(bodyOffset).replace(/^\r?\n/, "")
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
* Read and parse markdown body from a file.
|
|
253
|
-
*
|
|
254
|
-
* @param path - Path to the markdown file
|
|
255
|
-
* @param options - What to extract from the markdown
|
|
256
|
-
* @returns The requested body parts
|
|
257
|
-
*/
|
|
258
|
-
export async function readParsedBody(path: string, options: BodyOptions): Promise<BodyResult> {
|
|
259
|
-
const file = Bun.file(path)
|
|
260
|
-
const content = await file.text()
|
|
261
|
-
return parseMarkdownBody(content, options)
|
|
262
|
-
}
|