cms-renderer 0.0.0 → 0.1.1
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/chunk-HVKFEZBT.js +116 -0
- package/dist/chunk-HVKFEZBT.js.map +1 -0
- package/dist/chunk-JHKDRASN.js +39 -0
- package/dist/chunk-JHKDRASN.js.map +1 -0
- package/dist/chunk-RPM73PQZ.js +17 -0
- package/dist/chunk-RPM73PQZ.js.map +1 -0
- package/dist/lib/block-renderer.d.ts +32 -0
- package/dist/lib/block-renderer.js +7 -0
- package/dist/lib/block-renderer.js.map +1 -0
- package/dist/lib/cms-api.d.ts +25 -0
- package/dist/lib/cms-api.js +7 -0
- package/dist/lib/cms-api.js.map +1 -0
- package/dist/lib/data-utils.d.ts +218 -0
- package/dist/lib/data-utils.js +247 -0
- package/dist/lib/data-utils.js.map +1 -0
- package/dist/lib/image/lazy-load.d.ts +75 -0
- package/dist/lib/image/lazy-load.js +83 -0
- package/dist/lib/image/lazy-load.js.map +1 -0
- package/dist/lib/markdown-utils.d.ts +172 -0
- package/dist/lib/markdown-utils.js +137 -0
- package/dist/lib/markdown-utils.js.map +1 -0
- package/dist/lib/renderer.d.ts +40 -0
- package/dist/lib/renderer.js +371 -0
- package/dist/lib/renderer.js.map +1 -0
- package/{lib/result.ts → dist/lib/result.d.ts} +32 -146
- package/dist/lib/result.js +37 -0
- package/dist/lib/result.js.map +1 -0
- package/dist/lib/schema.d.ts +15 -0
- package/dist/lib/schema.js +35 -0
- package/dist/lib/schema.js.map +1 -0
- package/{lib/trpc.ts → dist/lib/trpc.d.ts} +6 -4
- package/dist/lib/trpc.js +7 -0
- package/dist/lib/trpc.js.map +1 -0
- package/dist/lib/types.d.ts +163 -0
- package/dist/lib/types.js +1 -0
- package/dist/lib/types.js.map +1 -0
- package/package.json +50 -11
- package/.turbo/turbo-check-types.log +0 -2
- package/lib/__tests__/enrich-block-images.test.ts +0 -394
- package/lib/block-renderer.tsx +0 -60
- package/lib/cms-api.ts +0 -86
- package/lib/data-utils.ts +0 -572
- package/lib/image/lazy-load.ts +0 -209
- package/lib/markdown-utils.ts +0 -368
- package/lib/renderer.tsx +0 -189
- package/lib/schema.ts +0 -74
- package/lib/types.ts +0 -201
- package/next.config.ts +0 -39
- package/postcss.config.mjs +0 -5
- package/tsconfig.json +0 -12
- package/tsconfig.tsbuildinfo +0 -1
package/lib/image/lazy-load.ts
DELETED
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Lazy Loading Utilities
|
|
3
|
-
*
|
|
4
|
-
* Reusable IntersectionObserver-based lazy loading with
|
|
5
|
-
* connection-aware quality adjustment.
|
|
6
|
-
*
|
|
7
|
-
* This module provides:
|
|
8
|
-
* - useLazyLoad: Hook for deferred loading when elements enter viewport
|
|
9
|
-
* - getConnectionAwareQuality: Adjusts image quality based on network speed
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
'use client';
|
|
13
|
-
|
|
14
|
-
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
15
|
-
|
|
16
|
-
// =============================================================================
|
|
17
|
-
// Types
|
|
18
|
-
// =============================================================================
|
|
19
|
-
|
|
20
|
-
export interface LazyLoadOptions {
|
|
21
|
-
/** Root margin for early loading (default: "200px") */
|
|
22
|
-
rootMargin?: string;
|
|
23
|
-
/** Threshold for intersection (default: 0) */
|
|
24
|
-
threshold?: number;
|
|
25
|
-
/** Callback when element enters viewport */
|
|
26
|
-
onEnter?: () => void;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface LazyLoadResult {
|
|
30
|
-
/** Callback ref to attach to the element */
|
|
31
|
-
ref: (node: HTMLElement | null) => void;
|
|
32
|
-
/** Whether the element has entered the viewport */
|
|
33
|
-
inView: boolean;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// =============================================================================
|
|
37
|
-
// useLazyLoad Hook
|
|
38
|
-
// =============================================================================
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* IntersectionObserver-based lazy loading hook.
|
|
42
|
-
*
|
|
43
|
-
* Triggers when an element approaches the viewport. Once triggered,
|
|
44
|
-
* the observer disconnects (one-shot behavior).
|
|
45
|
-
*
|
|
46
|
-
* @param options - Configuration options
|
|
47
|
-
* @returns Object with ref callback and inView state
|
|
48
|
-
*
|
|
49
|
-
* @example
|
|
50
|
-
* ```tsx
|
|
51
|
-
* function LazyComponent() {
|
|
52
|
-
* const { ref, inView } = useLazyLoad({ rootMargin: '200px' });
|
|
53
|
-
*
|
|
54
|
-
* return (
|
|
55
|
-
* <div ref={ref}>
|
|
56
|
-
* {inView && <ExpensiveContent />}
|
|
57
|
-
* </div>
|
|
58
|
-
* );
|
|
59
|
-
* }
|
|
60
|
-
* ```
|
|
61
|
-
*/
|
|
62
|
-
export function useLazyLoad(options: LazyLoadOptions = {}): LazyLoadResult {
|
|
63
|
-
const { rootMargin = '200px', threshold = 0, onEnter } = options;
|
|
64
|
-
const [inView, setInView] = useState(false);
|
|
65
|
-
const observerRef = useRef<IntersectionObserver | null>(null);
|
|
66
|
-
|
|
67
|
-
const ref = useCallback(
|
|
68
|
-
(node: HTMLElement | null) => {
|
|
69
|
-
// Disconnect previous observer
|
|
70
|
-
if (observerRef.current) {
|
|
71
|
-
observerRef.current.disconnect();
|
|
72
|
-
observerRef.current = null;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// No node to observe
|
|
76
|
-
if (!node) return;
|
|
77
|
-
|
|
78
|
-
// Create new observer
|
|
79
|
-
observerRef.current = new IntersectionObserver(
|
|
80
|
-
([entry]) => {
|
|
81
|
-
if (entry?.isIntersecting) {
|
|
82
|
-
setInView(true);
|
|
83
|
-
onEnter?.();
|
|
84
|
-
// Disconnect after first intersection (one-shot)
|
|
85
|
-
observerRef.current?.disconnect();
|
|
86
|
-
observerRef.current = null;
|
|
87
|
-
}
|
|
88
|
-
},
|
|
89
|
-
{ rootMargin, threshold }
|
|
90
|
-
);
|
|
91
|
-
|
|
92
|
-
observerRef.current.observe(node);
|
|
93
|
-
},
|
|
94
|
-
[rootMargin, threshold, onEnter]
|
|
95
|
-
);
|
|
96
|
-
|
|
97
|
-
// Cleanup on unmount
|
|
98
|
-
useEffect(() => {
|
|
99
|
-
return () => {
|
|
100
|
-
if (observerRef.current) {
|
|
101
|
-
observerRef.current.disconnect();
|
|
102
|
-
observerRef.current = null;
|
|
103
|
-
}
|
|
104
|
-
};
|
|
105
|
-
}, []);
|
|
106
|
-
|
|
107
|
-
return { ref, inView };
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// =============================================================================
|
|
111
|
-
// Connection-Aware Quality
|
|
112
|
-
// =============================================================================
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Network Information API types (not in standard TypeScript lib).
|
|
116
|
-
* @see https://developer.mozilla.org/en-US/docs/Web/API/Network_Information_API
|
|
117
|
-
*/
|
|
118
|
-
interface NetworkInformation {
|
|
119
|
-
effectiveType?: 'slow-2g' | '2g' | '3g' | '4g';
|
|
120
|
-
downlink?: number;
|
|
121
|
-
saveData?: boolean;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
interface NavigatorWithConnection extends Navigator {
|
|
125
|
-
connection?: NetworkInformation;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Quality presets based on connection type.
|
|
130
|
-
*/
|
|
131
|
-
const QUALITY_PRESETS = {
|
|
132
|
-
'slow-2g': 30,
|
|
133
|
-
'2g': 50,
|
|
134
|
-
'3g': 65,
|
|
135
|
-
'4g': 80,
|
|
136
|
-
saveData: 40,
|
|
137
|
-
default: 80,
|
|
138
|
-
} as const;
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Returns appropriate image quality based on network conditions.
|
|
142
|
-
*
|
|
143
|
-
* Uses the Network Information API when available to detect:
|
|
144
|
-
* - Data saver mode (returns lowest quality)
|
|
145
|
-
* - Effective connection type (slow-2g, 2g, 3g, 4g)
|
|
146
|
-
*
|
|
147
|
-
* Falls back to default quality (80) when:
|
|
148
|
-
* - Running on server (SSR)
|
|
149
|
-
* - Network Information API not supported
|
|
150
|
-
*
|
|
151
|
-
* @returns Quality value between 30-80
|
|
152
|
-
*
|
|
153
|
-
* @example
|
|
154
|
-
* ```typescript
|
|
155
|
-
* const quality = getConnectionAwareQuality();
|
|
156
|
-
* const imageUrl = `${baseUrl}?q=${quality}`;
|
|
157
|
-
* ```
|
|
158
|
-
*/
|
|
159
|
-
export function getConnectionAwareQuality(): number {
|
|
160
|
-
// SSR fallback
|
|
161
|
-
if (typeof navigator === 'undefined') {
|
|
162
|
-
return QUALITY_PRESETS.default;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const nav = navigator as NavigatorWithConnection;
|
|
166
|
-
const connection = nav.connection;
|
|
167
|
-
|
|
168
|
-
// No Network Information API support
|
|
169
|
-
if (!connection) {
|
|
170
|
-
return QUALITY_PRESETS.default;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// User has enabled data saver - respect their preference
|
|
174
|
-
if (connection.saveData) {
|
|
175
|
-
return QUALITY_PRESETS.saveData;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Adjust based on effective connection type
|
|
179
|
-
const effectiveType = connection.effectiveType;
|
|
180
|
-
if (effectiveType && effectiveType in QUALITY_PRESETS) {
|
|
181
|
-
return QUALITY_PRESETS[effectiveType];
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
return QUALITY_PRESETS.default;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Check if the user prefers reduced data usage.
|
|
189
|
-
*
|
|
190
|
-
* @returns true if Save-Data is enabled or connection is slow
|
|
191
|
-
*/
|
|
192
|
-
export function prefersReducedData(): boolean {
|
|
193
|
-
if (typeof navigator === 'undefined') {
|
|
194
|
-
return false;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
const nav = navigator as NavigatorWithConnection;
|
|
198
|
-
const connection = nav.connection;
|
|
199
|
-
|
|
200
|
-
if (!connection) {
|
|
201
|
-
return false;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
return (
|
|
205
|
-
connection.saveData === true ||
|
|
206
|
-
connection.effectiveType === 'slow-2g' ||
|
|
207
|
-
connection.effectiveType === '2g'
|
|
208
|
-
);
|
|
209
|
-
}
|
package/lib/markdown-utils.ts
DELETED
|
@@ -1,368 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Markdown Parsing Utilities
|
|
3
|
-
*
|
|
4
|
-
* Three implementations showing the progression from naive to production-ready:
|
|
5
|
-
* - v1 (Naive): Direct call, crashes on bad input
|
|
6
|
-
* - v2 (Defensive): Try-catch with null fallback
|
|
7
|
-
* - v3 (Robust): Result type, validation, sanitization
|
|
8
|
-
*
|
|
9
|
-
* The robust version (v3) is exported as the default `parseMarkdown`.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import type { MDTree } from '@repo/markdown-wasm';
|
|
13
|
-
import { mdToJSON } from '@repo/markdown-wasm';
|
|
14
|
-
|
|
15
|
-
import { err, ok, type Result } from './result';
|
|
16
|
-
|
|
17
|
-
// -----------------------------------------------------------------------------
|
|
18
|
-
// Types
|
|
19
|
-
// -----------------------------------------------------------------------------
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Error codes for markdown parsing failures.
|
|
23
|
-
*/
|
|
24
|
-
export const ParseErrorCode = {
|
|
25
|
-
INVALID_INPUT: 'INVALID_INPUT',
|
|
26
|
-
EMPTY_CONTENT: 'EMPTY_CONTENT',
|
|
27
|
-
PARSE_FAILED: 'PARSE_FAILED',
|
|
28
|
-
CONTENT_TOO_LONG: 'CONTENT_TOO_LONG',
|
|
29
|
-
NESTED_TOO_DEEP: 'NESTED_TOO_DEEP',
|
|
30
|
-
} as const;
|
|
31
|
-
|
|
32
|
-
export type ParseErrorCode = (typeof ParseErrorCode)[keyof typeof ParseErrorCode];
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Structured error for markdown parsing failures.
|
|
36
|
-
*/
|
|
37
|
-
export interface ParseError {
|
|
38
|
-
code: ParseErrorCode;
|
|
39
|
-
message: string;
|
|
40
|
-
cause?: Error;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Options for robust markdown parsing.
|
|
45
|
-
*/
|
|
46
|
-
export interface ParseOptions {
|
|
47
|
-
/**
|
|
48
|
-
* Maximum content length in characters.
|
|
49
|
-
* @default 500000 (500KB of text, ~100K words)
|
|
50
|
-
*/
|
|
51
|
-
maxLength?: number;
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Whether to sanitize XSS attempts in the output.
|
|
55
|
-
* @default true
|
|
56
|
-
*/
|
|
57
|
-
sanitize?: boolean;
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Whether to allow empty content.
|
|
61
|
-
* @default false
|
|
62
|
-
*/
|
|
63
|
-
allowEmpty?: boolean;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// -----------------------------------------------------------------------------
|
|
67
|
-
// Constants
|
|
68
|
-
// -----------------------------------------------------------------------------
|
|
69
|
-
|
|
70
|
-
const DEFAULT_MAX_LENGTH = 500_000; // 500KB of text
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Patterns that indicate potential XSS attempts in markdown.
|
|
74
|
-
* These are checked in raw content before parsing.
|
|
75
|
-
*/
|
|
76
|
-
const XSS_PATTERNS = [
|
|
77
|
-
/<script\b/i,
|
|
78
|
-
/javascript:/i,
|
|
79
|
-
/on\w+\s*=/i, // onclick=, onerror=, etc.
|
|
80
|
-
/data:/i, // data: URIs can be dangerous
|
|
81
|
-
/vbscript:/i,
|
|
82
|
-
] as const;
|
|
83
|
-
|
|
84
|
-
// -----------------------------------------------------------------------------
|
|
85
|
-
// V1: Naive Implementation
|
|
86
|
-
// -----------------------------------------------------------------------------
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Naive markdown parser - crashes on bad input.
|
|
90
|
-
*
|
|
91
|
-
* DO NOT USE IN PRODUCTION. This is for demonstration only.
|
|
92
|
-
*
|
|
93
|
-
* Problems:
|
|
94
|
-
* - No input validation
|
|
95
|
-
* - No error handling
|
|
96
|
-
* - Will crash the entire app on malformed input
|
|
97
|
-
* - No protection against XSS
|
|
98
|
-
* - No content limits
|
|
99
|
-
*
|
|
100
|
-
* @example
|
|
101
|
-
* ```ts
|
|
102
|
-
* // This crashes if content is null, undefined, or malformed
|
|
103
|
-
* const ast = parseMarkdownV1(userInput);
|
|
104
|
-
* ```
|
|
105
|
-
*/
|
|
106
|
-
export function parseMarkdownV1(content: string): MDTree {
|
|
107
|
-
return mdToJSON(content);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// -----------------------------------------------------------------------------
|
|
111
|
-
// V2: Defensive Implementation
|
|
112
|
-
// -----------------------------------------------------------------------------
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Defensive markdown parser - catches errors but loses context.
|
|
116
|
-
*
|
|
117
|
-
* Better than v1 but still problematic:
|
|
118
|
-
* - Returns null on any error (loses error details)
|
|
119
|
-
* - Caller can't distinguish between empty content and parse failure
|
|
120
|
-
* - Still no XSS protection
|
|
121
|
-
* - Still no content limits
|
|
122
|
-
*
|
|
123
|
-
* @example
|
|
124
|
-
* ```ts
|
|
125
|
-
* const ast = parseMarkdownV2(userInput);
|
|
126
|
-
* if (!ast) {
|
|
127
|
-
* // What went wrong? We don't know.
|
|
128
|
-
* return <ErrorFallback />;
|
|
129
|
-
* }
|
|
130
|
-
* ```
|
|
131
|
-
*/
|
|
132
|
-
export function parseMarkdownV2(content: string): MDTree | null {
|
|
133
|
-
try {
|
|
134
|
-
// Basic null check
|
|
135
|
-
if (content == null) {
|
|
136
|
-
return null;
|
|
137
|
-
}
|
|
138
|
-
return mdToJSON(content);
|
|
139
|
-
} catch {
|
|
140
|
-
return null;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// -----------------------------------------------------------------------------
|
|
145
|
-
// V3: Robust Implementation
|
|
146
|
-
// -----------------------------------------------------------------------------
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Creates a ParseError with the given code and message.
|
|
150
|
-
*/
|
|
151
|
-
function createParseError(code: ParseErrorCode, message: string, cause?: Error): ParseError {
|
|
152
|
-
return { code, message, cause };
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Checks content for potential XSS patterns.
|
|
157
|
-
* Returns the first matched pattern or null if clean.
|
|
158
|
-
*/
|
|
159
|
-
function detectXssPattern(content: string): RegExp | null {
|
|
160
|
-
for (const pattern of XSS_PATTERNS) {
|
|
161
|
-
if (pattern.test(content)) {
|
|
162
|
-
return pattern;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
return null;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Sanitizes content by removing dangerous patterns.
|
|
170
|
-
* This is a basic sanitizer; production should use DOMPurify.
|
|
171
|
-
*/
|
|
172
|
-
function sanitizeContent(content: string): string {
|
|
173
|
-
let sanitized = content;
|
|
174
|
-
|
|
175
|
-
// Remove script tags
|
|
176
|
-
sanitized = sanitized.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
|
|
177
|
-
|
|
178
|
-
// Remove javascript: and vbscript: URLs
|
|
179
|
-
sanitized = sanitized.replace(/javascript:/gi, 'removed:');
|
|
180
|
-
sanitized = sanitized.replace(/vbscript:/gi, 'removed:');
|
|
181
|
-
|
|
182
|
-
// Remove event handlers (onclick, onerror, etc.)
|
|
183
|
-
sanitized = sanitized.replace(/\bon\w+\s*=\s*["'][^"']*["']/gi, '');
|
|
184
|
-
sanitized = sanitized.replace(/\bon\w+\s*=\s*\S+/gi, '');
|
|
185
|
-
|
|
186
|
-
return sanitized;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Robust markdown parser - production-ready with full error context.
|
|
191
|
-
*
|
|
192
|
-
* Features:
|
|
193
|
-
* - Input validation with specific error codes
|
|
194
|
-
* - Result type for explicit error handling
|
|
195
|
-
* - XSS pattern detection and optional sanitization
|
|
196
|
-
* - Content length limits
|
|
197
|
-
* - Preserves error context for debugging
|
|
198
|
-
*
|
|
199
|
-
* @param content - Markdown string to parse
|
|
200
|
-
* @param options - Parsing options
|
|
201
|
-
* @returns Result containing the AST or a structured error
|
|
202
|
-
*
|
|
203
|
-
* @example
|
|
204
|
-
* ```ts
|
|
205
|
-
* const result = parseMarkdownV3(userInput);
|
|
206
|
-
*
|
|
207
|
-
* if (!result.ok) {
|
|
208
|
-
* switch (result.error.code) {
|
|
209
|
-
* case 'INVALID_INPUT':
|
|
210
|
-
* return <InvalidInputError message={result.error.message} />;
|
|
211
|
-
* case 'CONTENT_TOO_LONG':
|
|
212
|
-
* return <ContentTooLongError />;
|
|
213
|
-
* case 'PARSE_FAILED':
|
|
214
|
-
* console.error('Parse error:', result.error.cause);
|
|
215
|
-
* return <ParseError />;
|
|
216
|
-
* default:
|
|
217
|
-
* return <GenericError />;
|
|
218
|
-
* }
|
|
219
|
-
* }
|
|
220
|
-
*
|
|
221
|
-
* return <MarkdownRenderer ast={result.value} />;
|
|
222
|
-
* ```
|
|
223
|
-
*/
|
|
224
|
-
export function parseMarkdownV3(
|
|
225
|
-
content: string,
|
|
226
|
-
options: ParseOptions = {}
|
|
227
|
-
): Result<MDTree, ParseError> {
|
|
228
|
-
const { maxLength = DEFAULT_MAX_LENGTH, sanitize = true, allowEmpty = false } = options;
|
|
229
|
-
|
|
230
|
-
// 1. Validate input type
|
|
231
|
-
if (content === null || content === undefined) {
|
|
232
|
-
return err(
|
|
233
|
-
createParseError(
|
|
234
|
-
ParseErrorCode.INVALID_INPUT,
|
|
235
|
-
`Content must be a string, received ${content === null ? 'null' : 'undefined'}`
|
|
236
|
-
)
|
|
237
|
-
);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
if (typeof content !== 'string') {
|
|
241
|
-
return err(
|
|
242
|
-
createParseError(
|
|
243
|
-
ParseErrorCode.INVALID_INPUT,
|
|
244
|
-
`Content must be a string, received ${typeof content}`
|
|
245
|
-
)
|
|
246
|
-
);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// 2. Handle empty content
|
|
250
|
-
const trimmed = content.trim();
|
|
251
|
-
if (trimmed.length === 0 && !allowEmpty) {
|
|
252
|
-
return err(
|
|
253
|
-
createParseError(ParseErrorCode.EMPTY_CONTENT, 'Content is empty or contains only whitespace')
|
|
254
|
-
);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// 3. Check content length
|
|
258
|
-
if (content.length > maxLength) {
|
|
259
|
-
return err(
|
|
260
|
-
createParseError(
|
|
261
|
-
ParseErrorCode.CONTENT_TOO_LONG,
|
|
262
|
-
`Content exceeds maximum length of ${maxLength.toLocaleString()} characters ` +
|
|
263
|
-
`(received ${content.length.toLocaleString()})`
|
|
264
|
-
)
|
|
265
|
-
);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// 4. Check for XSS patterns
|
|
269
|
-
const xssPattern = detectXssPattern(content);
|
|
270
|
-
if (xssPattern) {
|
|
271
|
-
if (process.env.NODE_ENV === 'development') {
|
|
272
|
-
console.warn(
|
|
273
|
-
`[parseMarkdownV3] XSS pattern detected: ${xssPattern.toString()}`,
|
|
274
|
-
sanitize ? 'Content will be sanitized.' : 'Sanitization is disabled!'
|
|
275
|
-
);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// 5. Optionally sanitize content
|
|
280
|
-
const processedContent = sanitize ? sanitizeContent(content) : content;
|
|
281
|
-
|
|
282
|
-
// 6. Parse markdown with error handling
|
|
283
|
-
try {
|
|
284
|
-
const ast = mdToJSON(processedContent);
|
|
285
|
-
return ok(ast);
|
|
286
|
-
} catch (error) {
|
|
287
|
-
const cause = error instanceof Error ? error : new Error(String(error));
|
|
288
|
-
return err(
|
|
289
|
-
createParseError(
|
|
290
|
-
ParseErrorCode.PARSE_FAILED,
|
|
291
|
-
`Failed to parse markdown: ${cause.message}`,
|
|
292
|
-
cause
|
|
293
|
-
)
|
|
294
|
-
);
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// -----------------------------------------------------------------------------
|
|
299
|
-
// Default Export
|
|
300
|
-
// -----------------------------------------------------------------------------
|
|
301
|
-
|
|
302
|
-
/**
|
|
303
|
-
* Production-ready markdown parser.
|
|
304
|
-
*
|
|
305
|
-
* This is an alias for `parseMarkdownV3` - the robust implementation
|
|
306
|
-
* with validation, sanitization, and Result-based error handling.
|
|
307
|
-
*
|
|
308
|
-
* @example
|
|
309
|
-
* ```ts
|
|
310
|
-
* import { parseMarkdown } from '@/lib/markdown-utils';
|
|
311
|
-
*
|
|
312
|
-
* const result = parseMarkdown(content);
|
|
313
|
-
* if (!result.ok) {
|
|
314
|
-
* // Handle error with full context
|
|
315
|
-
* console.error(`[${result.error.code}] ${result.error.message}`);
|
|
316
|
-
* return null;
|
|
317
|
-
* }
|
|
318
|
-
* return result.value;
|
|
319
|
-
* ```
|
|
320
|
-
*/
|
|
321
|
-
export const parseMarkdown = parseMarkdownV3;
|
|
322
|
-
|
|
323
|
-
// -----------------------------------------------------------------------------
|
|
324
|
-
// Utility Functions
|
|
325
|
-
// -----------------------------------------------------------------------------
|
|
326
|
-
|
|
327
|
-
/**
|
|
328
|
-
* Checks if content is safe markdown (no XSS patterns detected).
|
|
329
|
-
*
|
|
330
|
-
* @param content - Markdown string to check
|
|
331
|
-
* @returns true if no XSS patterns detected
|
|
332
|
-
*/
|
|
333
|
-
export function isSafeMarkdown(content: string): boolean {
|
|
334
|
-
return detectXssPattern(content) === null;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
/**
|
|
338
|
-
* Estimates the word count of markdown content.
|
|
339
|
-
* Useful for content length limits and reading time estimates.
|
|
340
|
-
*
|
|
341
|
-
* @param content - Markdown string to count
|
|
342
|
-
* @returns Estimated word count
|
|
343
|
-
*/
|
|
344
|
-
export function estimateWordCount(content: string): number {
|
|
345
|
-
// Remove markdown syntax for more accurate count
|
|
346
|
-
const plainText = content
|
|
347
|
-
.replace(/#+\s/g, '') // Remove headings
|
|
348
|
-
.replace(/\*\*|__|~~|`/g, '') // Remove formatting
|
|
349
|
-
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // Convert links to text
|
|
350
|
-
.replace(/!\[([^\]]*)\]\([^)]+\)/g, '') // Remove images
|
|
351
|
-
.replace(/```[\s\S]*?```/g, '') // Remove code blocks
|
|
352
|
-
.replace(/`[^`]+`/g, ''); // Remove inline code
|
|
353
|
-
|
|
354
|
-
const words = plainText.trim().split(/\s+/).filter(Boolean);
|
|
355
|
-
return words.length;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
/**
|
|
359
|
-
* Estimates reading time for markdown content.
|
|
360
|
-
*
|
|
361
|
-
* @param content - Markdown string
|
|
362
|
-
* @param wordsPerMinute - Reading speed (default 200 WPM)
|
|
363
|
-
* @returns Reading time in minutes
|
|
364
|
-
*/
|
|
365
|
-
export function estimateReadingTime(content: string, wordsPerMinute = 200): number {
|
|
366
|
-
const wordCount = estimateWordCount(content);
|
|
367
|
-
return Math.ceil(wordCount / wordsPerMinute);
|
|
368
|
-
}
|