export interface CapturePayload { title: string url: string selection: string } // Limits for capture payloads const MAX_TITLE_LENGTH = 500 const MAX_URL_LENGTH = 2000 const MAX_SELECTION_LENGTH = 10000 export interface CaptureValidationResult { valid: boolean errors: string[] } export function validateCapturePayload(payload: CapturePayload): CaptureValidationResult { const errors: string[] = [] if (!payload.title || typeof payload.title !== 'string') { errors.push('Title is required') } else if (payload.title.length > MAX_TITLE_LENGTH) { errors.push(`Title too long: ${payload.title.length} chars (max: ${MAX_TITLE_LENGTH})`) } if (payload.url && typeof payload.url !== 'string') { errors.push('URL must be a string') } else if (payload.url && payload.url.length > MAX_URL_LENGTH) { errors.push(`URL too long: ${payload.url.length} chars (max: ${MAX_URL_LENGTH})`) } if (payload.selection && typeof payload.selection !== 'string') { errors.push('Selection must be a string') } else if (payload.selection && payload.selection.length > MAX_SELECTION_LENGTH) { errors.push(`Selection too long: ${payload.selection.length} chars (max: ${MAX_SELECTION_LENGTH})`) } return { valid: errors.length === 0, errors, } } export function encodeCapturePayload(payload: CapturePayload): string { const params = new URLSearchParams({ title: payload.title.slice(0, MAX_TITLE_LENGTH), url: payload.url.slice(0, MAX_URL_LENGTH), selection: payload.selection.slice(0, MAX_SELECTION_LENGTH), }) return params.toString() } export function generateBookmarklet(): string { // Get the current origin (where the app is running) const origin = typeof window !== 'undefined' ? window.location.origin : '' const code = ` var title = document.title; var url = location.href; var selection = window.getSelection().toString(); var params = new URLSearchParams({title, url, selection}); var base = ${JSON.stringify(origin)}; window.open(base + '/capture?' + params.toString(), '_blank'); `.replace(/\s+/g, ' ').trim() return `javascript:${code}` }