Render Result
Added in 4.3.
MdTextRender used to resolve to undefined, which meant a render that quietly dropped an image, an unsupported element, or a subtree past the nesting limit produced a PDF that looked successful and was not. It now resolves to a summary of what happened.
const result = await MdTextRender(doc, markdown, options)The result object
interface RenderResult {
endY: number // cursor Y when rendering finished
startPage: number // page the render began on
pageCount: number // pages this render produced, including the first
warnings: RenderWarning[] // everything that could not be drawn
droppedNodes: number // total nodes discarded, summed across warnings
violations: SecurityViolation[]
}startPage and pageCount matter when you render into a document that already has content — they describe this render, not the document as a whole.
Continuing after a render
const result = await MdTextRender(doc, markdown, options)
doc.setFontSize(9)
doc.text('Generated ' + new Date().toISOString(), 20, result.endY + 8)
console.log(`rendered ${result.pageCount} page(s) from page ${result.startPage}`)Warnings
interface RenderWarning {
code: RenderWarningCode
message: string
context?: string // an element type, a URL, a font family
droppedNodes?: number // present when content was discarded
}A warning means the render continued but the output is not what the Markdown described. An empty warnings array means the document is complete.
Warning codes
| Code | What happened |
|---|---|
TOKEN_CONVERSION_FAILED | A Markdown token could not be converted to a renderable element, and was dropped |
UNSUPPORTED_ELEMENT | A parsed element type has no renderer |
IMAGE_LOAD_FAILED | An image could not be fetched or decoded |
IMAGE_RENDER_FAILED | The image loaded but jsPDF refused to draw it |
IMAGE_ATTRS_IGNORED | An image's {width=… height=… align=…} block was too long to parse, so the image drew at its natural size |
IMAGE_SIZE_UNKNOWN | An image's intrinsic size could not be read, so a fallback size was used |
TABLE_SKIPPED | A table had no header row, so it was not rendered |
TABLE_CALLBACK_FAILED | A table option callback threw |
TABLE_POSITION_UNKNOWN | autoTable reported no finalY, so content after the table may be misplaced |
CODE_BLOCK_OVERFLOW | A code block's line height exceeds the usable page height, so it renders one line per page |
CONTENT_DROPPED | A subtree was discarded for nesting deeper than security.maxNestedDepth |
COMPONENT_OVERRIDE_FAILED | A component override threw or was async |
SECURITY_CALLBACK_FAILED | An onSecurityViolation callback threw; the violation was still handled |
SSRF_CHECKS_UNAVAILABLE | IP-level checks could not run because DNS resolution is unavailable (browser runtime) |
Codes are stable API. New codes may be added in a minor release, so treat an unrecognised code as a generic warning rather than an error.
Delivery
Warnings are always collected on the result. Two options control everything else:
await MdTextRender(doc, markdown, {
...options,
silent: true, // no console output from the library
onWarning: (w) => logger.warn(w), // fires as each warning is recorded
})silent— the library logs each warning withconsole.warnby default, which is useful during development and noise in production.silent: trueturns that off without hiding anything:result.warningsis unaffected.onWarning— receives each warning as it happens, which is what you want for streaming to a logger or for attributing a warning to the point in a long document where it occurred. A listener that throws is caught, so your logging can never take a render down.
Patterns
Fail when a document must be complete
An invoice or a contract that silently lost a clause is worse than one that failed to generate:
const result = await MdTextRender(doc, markdown, { ...options, silent: true })
if (result.droppedNodes > 0) {
throw new Error(
`Refusing to issue an incomplete document: ${result.droppedNodes} node(s) dropped — ` +
result.warnings.map((w) => `${w.code}: ${w.message}`).join('; '),
)
}
doc.save('invoice.pdf')droppedNodes is the right check here: it counts lost content, whereas warnings.length also includes cosmetic issues such as a missing font variant.
Degrade instead of failing
const result = await MdTextRender(doc, markdown, { ...options, silent: true })
const brokenImages = result.warnings.filter((w) => w.code === 'IMAGE_LOAD_FAILED')
if (brokenImages.length) {
// w.context is the URL that failed
report.missingAssets = brokenImages.map((w) => w.context)
}Surface warnings in a build
if (result.warnings.length) {
for (const w of result.warnings) {
console.error(`::warning::${w.code} ${w.message}`)
}
process.exitCode = 1
}A throwing validateUrl aborts the render
onSecurityViolation is a notification, so a failure in it is caught and reported as a warning — losing your audit log should not change what the PDF contains. validateUrl is a decision, so a failure in it rejects the whole render rather than proceeding as if the URL had been approved.
Security violations
violations is populated when security is enabled and violationMode is 'skip' or 'placeholder' — the modes that keep rendering. With 'throw' the render rejects instead, so there is no result to inspect.
const result = await MdTextRender(doc, untrusted, {
...options,
security: { enabled: true, violationMode: 'skip' },
})
for (const v of result.violations) {
audit.record(v.code, v.url)
}Migrating from endCursorYHandler
endCursorYHandler still fires and still receives the same value as result.endY. It is deprecated because a callback can only report one number, while the result carries everything.
// Before
let endY = 0
await MdTextRender(doc, md, { ...options, endCursorYHandler: (y) => { endY = y } })
// After
const { endY } = await MdTextRender(doc, md, options)