/*! * jspdf-md-renderer * * MIT License * * Copyright (c) 2026 Jeel Gajera * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. * */ import jsPDF, { jsPDFOptions } from "jspdf"; import { UserOptions } from "jspdf-autotable"; //#region src/types/security.d.ts type ViolationMode = 'skip' | 'throw' | 'placeholder'; type ViolationAction = 'skip' | 'placeholder'; type SecurityViolationType = 'link' | 'image' | 'markdown' | 'render'; type SecurityViolationCode = 'MARKDOWN_TOO_LARGE' | 'MAX_NESTED_DEPTH_EXCEEDED' | 'MAX_IMAGE_COUNT_EXCEEDED' | 'RENDER_TIMEOUT_EXCEEDED' | 'INVALID_URL' | 'LINK_PROTOCOL_BLOCKED' | 'IMAGE_PROTOCOL_BLOCKED' | 'IMAGE_DOMAIN_BLOCKED' | 'DATA_URL_BLOCKED' | 'SVG_BLOCKED' | 'LOCALHOST_BLOCKED' | 'PRIVATE_IP_BLOCKED' | 'LINK_LOCAL_IP_BLOCKED' | 'METADATA_IP_BLOCKED' | 'IMAGE_SIZE_EXCEEDED' | 'CUSTOM_VALIDATOR_BLOCKED'; interface SecurityViolation { /** Machine-readable violation code. */ code: SecurityViolationCode; /** High-level category of the violating input. */ type: SecurityViolationType; /** Human-readable explanation of the violation. */ message: string; /** Raw value that triggered the violation (URL, length, etc.). */ value?: string; /** Optional execution context to help debugging (e.g. 'markdown-link'). */ context?: string; /** ISO timestamp when the violation was recorded. */ timestamp: string; } declare class SecurityViolationError extends Error { /** Structured violation payload used to construct this error. */ readonly violation: SecurityViolation; constructor(violation: SecurityViolation); } interface RenderSecurityOptions { /** Enables all built-in security checks. Default: false (opt-in). */ enabled?: boolean; /** Allowed URI protocols for markdown links. Example: ['https:', 'mailto:']. */ allowedLinkProtocols?: string[]; /** If true, link text is rendered but PDF link actions are disabled. */ disablePdfLinks?: boolean; /** Whether remote image fetching is allowed (http/https). */ allowRemoteImages?: boolean; /** Allowed protocols for image URLs. Example: ['https:', 'http:']. */ allowedImageProtocols?: string[]; /** * Optional domain allowlist for remote image hosts. * - `undefined` (default): all domains are allowed. * - `[]` (empty array): no domains are allowed. * - `['example.com']`: only `example.com` and its subdomains are allowed. */ allowedImageDomains?: string[]; /** Whether inline data: image URLs are allowed. */ allowDataUrls?: boolean; /** Whether SVG images are allowed. */ allowSvgImages?: boolean; /** Blocks localhost image/link destinations when true. */ blockLocalhost?: boolean; /** * Blocks private IPv4 ranges (10/8, 172.16/12, 192.168/16) when true. * NOTE: In browser environments, IP-based checks cannot be enforced due to * lack of DNS resolution APIs. Use a trusted server-side proxy for strict enforcement. */ blockPrivateIPs?: boolean; /** * Blocks link-local IPv4 ranges (169.254/16) when true. * NOTE: In browser environments, IP-based checks cannot be enforced due to * lack of DNS resolution APIs. Use a trusted server-side proxy for strict enforcement. */ blockLinkLocalIPs?: boolean; /** * Blocks known cloud metadata endpoints when true. * NOTE: In browser environments, IP-based checks cannot be enforced due to * lack of DNS resolution APIs. Use a trusted server-side proxy for strict enforcement. */ blockMetadataIPs?: boolean; /** Maximum markdown input length in characters. */ maxMarkdownLength?: number; /** Maximum number of markdown images allowed per render. */ maxImageCount?: number; /** Maximum image payload size in bytes (fetched blob size or decoded data URL bytes). */ maxImageSizeBytes?: number; /** Maximum supported markdown nesting depth. */ maxNestedDepth?: number; /** Maximum total render time in milliseconds. */ renderTimeoutMs?: number; /** Action taken when a violation occurs. */ violationMode?: ViolationMode; /** Placeholder text used for blocked text-like content in placeholder mode. */ placeholderText?: string; /** Placeholder text used for blocked images in placeholder mode. */ placeholderImageText?: string; /** Optional custom URL validator. Return false to reject the URL. */ validateUrl?: (url: URL, type: 'link' | 'image') => boolean | Promise; /** Callback fired for every security violation, regardless of mode. */ onSecurityViolation?: (violation: SecurityViolation) => void; } //#endregion //#region src/types/renderOption.d.ts type RenderOption = { cursor: { x: number; y: number; }; page: { format?: string | number[]; unit: jsPDFOptions['unit']; orientation?: jsPDFOptions['orientation']; maxContentWidth: number; maxContentHeight: number; lineSpace: number; defaultLineHeightFactor: number; defaultFontSize: number; defaultTitleFontSize: number; topmargin: number; xpading: number; xmargin: number; indent: number; }; font: { bold: FontItem; regular: FontItem; light: FontItem; italic?: FontItem; boldItalic?: FontItem; code?: FontItem; }; heading?: { /** Whether headings should use bold font. Default: true */bold?: boolean; /** Font size for h1-h6. Values are absolute (e.g. 22, 20, 18, 16, 14, 12). */ h1?: number; h2?: number; h3?: number; h4?: number; h5?: number; h6?: number; /** Space below heading before next element, in doc units. Default: 2 */ bottomSpacing?: number; /** Text color for all headings as hex. Default: '#000000' */ color?: string; h1Color?: string; h2Color?: string; h3Color?: string; h4Color?: string; h5Color?: string; h6Color?: string; }; list?: { /** Bullet character for unordered lists. Default: '\u2022 ' */bulletChar?: string; /** Extra indent per nesting level in doc units. Default: uses page.indent */ indentSize?: number; /** Vertical space between list items. Used when spacing.betweenListItems is not provided. */ itemSpacing?: number; }; paragraph?: { /** Space below each paragraph in doc units. Default: lineSpace */bottomSpacing?: number; /** Text color for paragraph text as hex. Default: '#000000' */ color?: string; }; blockquote?: { /** Left bar color as hex. Default: '#AAAAAA' */barColor?: string; /** Left bar width in doc units. Default: 1 */ barWidth?: number; /** Left padding from bar to text in doc units. Default: 4 */ paddingLeft?: number; /** Background color as hex. Default: undefined (transparent) */ backgroundColor?: string; /** Space below blockquote before next element, in doc units. Default: lineSpace */ bottomSpacing?: number; }; content?: { textAlignment: 'left' | 'right' | 'center' | 'justify'; }; codespan?: { /** Background color for inline code. Default: '#EEEEEE' */backgroundColor?: string; /** Padding around inline code text. Default: 0.5 */ padding?: number; /** Whether to show background rectangle. Default: true */ showBackground?: boolean; /** Font size scale factor for code. Default: 0.9 */ fontSizeScale?: number; }; link?: { linkColor: [number, number, number]; }; table?: UserOptions; image?: { /** Default alignment for images: 'left' | 'center' | 'right'. Default: 'left' */defaultAlign?: 'left' | 'center' | 'right'; }; codeBlock?: { backgroundColor?: string; borderColor?: string; borderRadius?: number; padding?: number; fontSizeScale?: number; /** Whether to show language label. Default: true */ showLanguageLabel?: boolean; /** Text color for code content as hex. Default: '#000000' */ textColor?: string; /** Language label color as hex. Default: '#666666' */ labelColor?: string; }; spacing?: { /** Space below headings in doc units. Default: 2 */afterHeading?: number; /** Space below paragraphs in doc units. Default: 3 */ afterParagraph?: number; /** Space below code blocks in doc units. Default: 3 */ afterCodeBlock?: number; /** Space below blockquotes in doc units. Default: 3 */ afterBlockquote?: number; /** Space below images in doc units. Default: 2 */ afterImage?: number; /** Space below horizontal rules in doc units. Default: 2 */ afterHR?: number; /** Space between list items in doc units. Default: 0 */ betweenListItems?: number; /** Space below a complete list in doc units. Default: 3 */ afterList?: number; /** Space below tables in doc units. Default: 3 */ afterTable?: number; }; header?: { /** Text to render in header area of each page */text?: string | ((pageNumber: number, totalPages: number) => string); /** Y position of header text from top in doc units. Default: 5 */ y?: number; /** Font size for header text. Default: 9 */ fontSize?: number; /** Text color for header. Default: '#666666' */ color?: string; /** Alignment of header text. Default: 'center' */ align?: 'left' | 'center' | 'right'; }; footer?: { /** Text to render in footer area of each page */text?: string | ((pageNumber: number, totalPages: number) => string); /** Y position from top of page in doc units. Default: pageHeight - 5 */ y?: number; /** Font size for footer text. Default: 9 */ fontSize?: number; /** Text color for footer. Default: '#666666' */ color?: string; /** Alignment of footer text. Default: 'right' */ align?: 'right' | 'left' | 'center'; /** Shortcut: render page numbers with format "Page X of Y" */ showPageNumbers?: boolean; }; pageBreakHandler?: (doc: jsPDF) => void; endCursorYHandler: (y: number) => void; security?: RenderSecurityOptions; }; type Cursor = { x: number; y: number; }; type FontItem = { name: string; style: string; }; //#endregion //#region src/renderer/MdTextRender.d.ts /** * Renders parsed markdown text into jsPDF document. * * @param doc - The jsPDF document. * @param text - The markdown content to render. * @param options - The render options (fonts, page margins, etc.). */ declare const MdTextRender: (doc: jsPDF, text: string, options: RenderOption) => Promise; //#endregion //#region src/types/parsedElement.d.ts type ParsedElement = { type: string; content?: string; depth?: number; items?: ParsedElement[]; ordered?: boolean; start?: number; lang?: string; code?: string; src?: string; alt?: string; href?: string; text?: string; header?: ParsedElement[]; rows?: ParsedElement[][]; data?: string; width?: number; height?: number; align?: 'left' | 'center' | 'right'; naturalWidth?: number; naturalHeight?: number; /** Whether this list item is a task (checkbox) item */ task?: boolean; /** Whether the task checkbox is checked */ checked?: boolean; }; //#endregion //#region src/parser/MdTextParser.d.ts /** * Parses markdown into tokens and converts to a custom parsed structure. * * @param text - The markdown content to parse. * @returns Parsed markdown elements. */ declare const MdTextParser: (text: string) => Promise; //#endregion //#region src/types/styledWordInfo.d.ts /** * Enhanced word info types for styled text justification. * These types allow tracking font styles per word for proper justified rendering. */ type TextStyle = 'normal' | 'bold' | 'italic' | 'bolditalic' | 'codespan'; /** * Information about a single styled word for justified text rendering. */ interface StyledWordInfo { /** The word text */ text: string; /** Measured width in PDF points */ width: number; /** Font style to apply when rendering */ style: TextStyle; /** Whether this word is part of a link */ isLink?: boolean; /** URL if this is a link */ href?: string; /** Optional link color override */ linkColor?: number[]; /** Whether this is an inline image */ isImage?: boolean; /** The image parsed element */ imageElement?: ParsedElement; /** The height of the image to reserve */ imageHeight?: number; /** Marks a hard line break */ isBr?: boolean; /** Whether this word was followed by whitespace in the source Markdown */ hasTrailingSpace?: boolean; } /** * Represents a line of styled words for justified rendering. */ interface StyledLine { /** Words in this line */ words: StyledWordInfo[]; /** Sum of word widths (without inter-word spacing) */ totalTextWidth: number; /** Last line of paragraph shouldn't be fully justified */ isLastLine: boolean; /** Line height (max of text height and inline image heights) */ lineHeight: number; } //#endregion //#region src/store/renderStore.d.ts declare class RenderStore { private cursor; private lastContentY_; private options_; private inlineLock; constructor(options: RenderOption); getCursor(): Cursor; setCursor(newCursor: Cursor): void; get options(): RenderOption; get isInlineLockActive(): boolean; activateInlineLock(): void; deactivateInlineLock(): void; /** * Updates the x pointer of the cursor. * @param value The value to set or add. * @param operation 'set' to assign a new value, 'add' to increment the current value. * @default operation = 'set' */ updateX(value: number, operation?: 'set' | 'add'): void; /** * Updates the y pointer of the cursor. * @param value The value to set or add. * @param operation 'set' to assign a new value, 'add' to increment the current value. * @default operation = 'set' */ updateY(value: number, operation?: 'set' | 'add'): void; /** * Records a Y position as the bottom of rendered content. * This is useful for container components (like blockquotes) to know * where their actual text content ends, ignoring any trailing margins. * @param specificY Optional Y value to record. Defaults to current cursor Y. */ recordContentY(specificY?: number): void; /** * Gets the last Y position recorded as content bottom. */ get lastContentY(): number; get X(): number; get Y(): number; } //#endregion //#region src/layout/layoutEngine.d.ts interface LayoutOptions { alignment?: 'left' | 'right' | 'center' | 'justify'; /** When true, the last line advances Y by only the raw text height * (no trailing inter-line spacing), so the caller's explicit * bottomSpacing is the sole gap after the block. */ trimLastLine?: boolean; } /** * THE single entry point for rendering any mixed inline content. * All paragraph, heading, list item, and blockquote text must go through here. * * Handles: word splitting, line breaking, page breaks, styled rendering. * Returns the final Y position after rendering. */ declare const renderInlineContent: (doc: jsPDF, elements: ParsedElement[], x: number, y: number, maxWidth: number, store: RenderStore, opts?: LayoutOptions) => number; /** * Render a single string of plain (unstyled) text with wrapping and page breaks. * Use this for simple text in list items and raw items where there are no inline styles. */ declare const renderPlainText: (doc: jsPDF, text: string, x: number, y: number, maxWidth: number, store: RenderStore, opts?: LayoutOptions) => number; //#endregion //#region src/enums/mdTokenType.d.ts declare enum MdTokenType { Heading = "heading", Paragraph = "paragraph", List = "list", ListItem = "list_item", Blockquote = "blockquote", Code = "code", CodeSpan = "codespan", Table = "table", Html = "html", Hr = "hr", Image = "image", Link = "link", Strong = "strong", Em = "em", TableHeader = "table_header", TableCell = "table_cell", Raw = "raw", Text = "text", Br = "br" } //#endregion //#region src/utils/options-validation.d.ts declare const validateOptions: (options: RenderOption) => RenderOption; //#endregion export { MdTextParser, MdTextRender, MdTokenType, type ParsedElement, type RenderOption, type RenderSecurityOptions, type SecurityViolation, type SecurityViolationCode, SecurityViolationError, type StyledLine, type StyledWordInfo, type TextStyle, type ViolationAction, type ViolationMode, renderInlineContent, renderPlainText, validateOptions };