@piqit/resolvers 0.0.2 → 0.0.3

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/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, "&amp;")
163
- .replace(/</g, "&lt;")
164
- .replace(/>/g, "&gt;")
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
- }
@@ -1,226 +0,0 @@
1
- /**
2
- * Path Pattern Parsing and Matching
3
- *
4
- * Parses patterns like `{year}/{slug}.md` and provides:
5
- * - Type-level extraction of parameters
6
- * - Glob pattern generation
7
- * - Path matching to extract params
8
- * - Path building from params
9
- */
10
-
11
- // =============================================================================
12
- // Type-level Parameter Extraction
13
- // =============================================================================
14
-
15
- /**
16
- * Extract params from a path pattern string at the type level.
17
- *
18
- * @example
19
- * type Params = ExtractParams<'{year}/{slug}.md'>
20
- * // = { year: string } & { slug: string }
21
- */
22
- export type ExtractParams<P extends string> = P extends `${string}{${infer Param}}${infer Rest}`
23
- ? { [K in Param]: string } & ExtractParams<Rest>
24
- : {}
25
-
26
- /**
27
- * Simplify a type by flattening intersections.
28
- * Converts { a: string } & { b: string } to { a: string; b: string }
29
- */
30
- export type Simplify<T> = { [K in keyof T]: T[K] }
31
-
32
- /**
33
- * Extract and simplify params from a path pattern.
34
- */
35
- export type PathParams<P extends string> = Simplify<ExtractParams<P>>
36
-
37
- // =============================================================================
38
- // Compiled Pattern Interface
39
- // =============================================================================
40
-
41
- /**
42
- * A compiled path pattern that can match paths and generate globs.
43
- */
44
- export interface CompiledPattern {
45
- /**
46
- * The original pattern string.
47
- */
48
- pattern: string
49
-
50
- /**
51
- * The extracted parameter names.
52
- */
53
- paramNames: string[]
54
-
55
- /**
56
- * Generate a glob pattern, optionally constraining specific params.
57
- *
58
- * @param constraints - Optional param values to use instead of wildcards
59
- * @returns A glob pattern string
60
- *
61
- * @example
62
- * pattern.toGlob() // '** / *.md' (with {year}/{slug}.md)
63
- * pattern.toGlob({ year: '2024' }) // '2024/*.md'
64
- */
65
- toGlob(constraints?: Record<string, unknown>): string
66
-
67
- /**
68
- * Match a path against this pattern and extract params.
69
- *
70
- * @param path - The file path to match (relative to base)
71
- * @returns The extracted params, or null if no match
72
- *
73
- * @example
74
- * pattern.match('2024/hello-world.md') // { year: '2024', slug: 'hello-world' }
75
- * pattern.match('invalid') // null
76
- */
77
- match(path: string): Record<string, string> | null
78
-
79
- /**
80
- * Build a path from params.
81
- *
82
- * @param params - The param values
83
- * @returns The constructed path
84
- *
85
- * @example
86
- * pattern.build({ year: '2024', slug: 'hello' }) // '2024/hello.md'
87
- */
88
- build(params: Record<string, string>): string
89
- }
90
-
91
- // =============================================================================
92
- // Pattern Parsing
93
- // =============================================================================
94
-
95
- /**
96
- * Regex to match parameter placeholders in patterns.
97
- * Matches {paramName} where paramName is alphanumeric + underscores.
98
- */
99
- const PARAM_REGEX = /\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g
100
-
101
- /**
102
- * Characters that need escaping in regex.
103
- */
104
- const REGEX_ESCAPE = /[.*+?^${}()|[\]\\]/g
105
-
106
- /**
107
- * Compile a path pattern string into a usable pattern object.
108
- *
109
- * @param pattern - The pattern string like '{year}/{slug}.md'
110
- * @returns A CompiledPattern instance
111
- *
112
- * @example
113
- * const pattern = compilePattern('{year}/{slug}.md')
114
- * pattern.toGlob() // '** / *.md'
115
- * pattern.match('2024/hello.md') // { year: '2024', slug: 'hello' }
116
- */
117
- export function compilePattern(pattern: string): CompiledPattern {
118
- // Extract all parameter names in order
119
- const paramNames: string[] = []
120
- let match: RegExpExecArray | null
121
- const regex = new RegExp(PARAM_REGEX.source, "g")
122
- while ((match = regex.exec(pattern)) !== null) {
123
- paramNames.push(match[1])
124
- }
125
-
126
- // Build the match regex by replacing {param} with capturing groups
127
- // Each param can match anything except path separators
128
- const regexPattern = pattern
129
- .replace(REGEX_ESCAPE, (char) => {
130
- // Don't escape our param placeholders - we'll handle them separately
131
- if (char === "{" || char === "}") return char
132
- return "\\" + char
133
- })
134
- .replace(PARAM_REGEX, "([^/]+)")
135
-
136
- const matchRegex = new RegExp(`^${regexPattern}$`)
137
-
138
- return {
139
- pattern,
140
- paramNames,
141
-
142
- toGlob(constraints?: Record<string, unknown>): string {
143
- let glob = pattern
144
-
145
- // Replace each param with either its constrained value or a wildcard
146
- for (const name of paramNames) {
147
- const value = constraints?.[name]
148
- if (value !== undefined && value !== null) {
149
- // Use the constrained value
150
- glob = glob.replace(`{${name}}`, String(value))
151
- } else {
152
- // Use a wildcard - * matches anything except /
153
- glob = glob.replace(`{${name}}`, "*")
154
- }
155
- }
156
-
157
- return glob
158
- },
159
-
160
- match(path: string): Record<string, string> | null {
161
- const result = matchRegex.exec(path)
162
- if (!result) return null
163
-
164
- const params: Record<string, string> = {}
165
- for (let i = 0; i < paramNames.length; i++) {
166
- params[paramNames[i]] = result[i + 1]
167
- }
168
- return params
169
- },
170
-
171
- build(params: Record<string, string>): string {
172
- let result = pattern
173
- for (const name of paramNames) {
174
- const value = params[name]
175
- if (value === undefined) {
176
- throw new Error(`Missing required param: ${name}`)
177
- }
178
- result = result.replace(`{${name}}`, value)
179
- }
180
- return result
181
- },
182
- }
183
- }
184
-
185
- // =============================================================================
186
- // Schema for Path Params
187
- // =============================================================================
188
-
189
- /**
190
- * Create a StandardSchema for path params extracted from a pattern.
191
- * This is used by the resolver to expose scanParams to piq core.
192
- *
193
- * @param pattern - The compiled pattern
194
- * @returns A StandardSchema that validates param objects
195
- */
196
- export function createParamsSchema(
197
- pattern: CompiledPattern
198
- ): import("piqit").StandardSchema<Record<string, string>> {
199
- return {
200
- "~standard": {
201
- version: 1,
202
- vendor: "piqit/resolvers",
203
- validate(value: unknown) {
204
- if (value === null || typeof value !== "object") {
205
- return {
206
- issues: [{ message: "Expected object" }],
207
- }
208
- }
209
-
210
- const obj = value as Record<string, unknown>
211
-
212
- // All params should be strings if present
213
- for (const name of pattern.paramNames) {
214
- const val = obj[name]
215
- if (val !== undefined && typeof val !== "string") {
216
- return {
217
- issues: [{ message: `Param ${name} must be a string`, path: [name] }],
218
- }
219
- }
220
- }
221
-
222
- return { value: obj as Record<string, string> }
223
- },
224
- },
225
- }
226
- }