@peaceroad/markdown-it-renderer-image
v0.14.0
Published
A markdown-it image plugin plus a browser DOM helper for src rewriting and image sizing.
Readme
p7d-markdown-it-renderer-image
A markdown-it image plugin plus a browser DOM helper.
- Node plugin: rewrites image
src, setswidth/height, and can addloading/decoding. - DOM helper: applies the same logic to preview HTML and supports live observation.
Install
npm i @peaceroad/markdown-it-renderer-imageQuick Start
Node (markdown-it)
import fs from 'fs'
import mdit from 'markdown-it'
import mditRendererImage from '@peaceroad/markdown-it-renderer-image'
const mdFile = '/tmp/doc.md'
const mdText = fs.readFileSync(mdFile, 'utf-8')
const md = mdit().use(mditRendererImage, { mdPath: mdFile })
console.log(md.render(mdText))mdPath accepts a markdown file path or a directory path.
You can also pass mdPath at render time:
md.render(mdText, { mdPath: mdFile })For frontmatter-driven URL resolution on Node, the runtime precedence is:
env.frontmatterpassed tomd.render(src, env)md.frontmatterormd.metaonly when the current markdown source begins with YAML frontmatter
md.env.frontmatter is not part of the supported contract.
Browser / Preview DOM
<script type="module">
import { runInPreview, applyImageTransformsToString } from '@peaceroad/markdown-it-renderer-image/script/set-img-attributes.js'
const { observer } = await runInPreview({
root: document,
markdownCont,
readMeta: true,
observe: true,
})
const transformed = await applyImageTransformsToString(htmlSource, { readMeta: true }, markdownCont)
observer?.disconnect()
</script>Runtime Notes
- In browser builds, package-root import resolves to the DOM helper (named exports + no-op default export).
- The default export in the DOM helper is intentionally a no-op and returns a resolved Promise.
createContext()reads YAML frontmatter frommarkdownCont(first argument).- DOM and Node frontmatter normalization accepts legacy flat keys, dotted keys, and simple nested object forms.
- With
readMeta: true, DOM helper additionally readsmeta[name="markdown-frontmatter"](JSON).
Common Presets
1) VS Code live preview (low load)
await runInPreview({
root: document,
markdownCont,
readMeta: true,
observe: true,
previewMode: 'output',
enableSizeProbe: false,
observeDebounceMs: 200,
})2) Output parity between Node and DOM
- DOM:
previewMode: 'output',setDomSrc: true - Node:
resolveSrc: true
3) Keep markdown src for display, probe from mapped URL
await runInPreview({
root: document,
markdownCont,
previewMode: 'markdown',
loadSrcStrategy: 'raw',
loadSrcPrefixMap: { '/img/': 'http://localhost:3000/img/' },
})Options
Shared (Node + DOM)
scaleSuffix(default:false): Scale by filename suffix (@2x,300dpi,300ppi).resize(default:false): Resize by title hint (for exampleresize:50%,resize:200px).autoHideResizeTitle(default:true): Remove title when it is a resize hint.resizeDataAttr(default:'data-img-resize'): Store normalized effective resize metadata (titleresize orimagescale). When enabled,${resizeDataAttr}-originis also emitted forimagescale-derived values (''disables both).lazyLoad(default:false): Addloading="lazy".asyncDecode(default:false): Adddecoding="async".checkImgExtensions(default:'png,jpg,jpeg,gif,webp'): Extensions eligible for sizing (query/hash ignored).resolveSrc(default:true): Resolve outputsrcusing frontmatter /urlImageBase.urlImageBase(default:''): Fallback when frontmatter has nourlimagebase.outputUrlMode(default:'absolute'):absolute|protocol-relative|path-only.suppressErrors(default:'none'):none|all|local|remote.
Notes:
- Final size is always capped to original dimensions (no upscaling behavior).
outputUrlMode: 'path-only'assumes same-origin URL usage.
Node-only
mdPath(default:''): Markdown file path or directory for local image sizing.disableRemoteSize(default:false): Skip remote image sizing.remoteTimeout(default:5000):sync-fetchtimeout (ms).remoteMaxBytes(default:16MB): Skip remote image whencontent-lengthexceeds this value.cacheMax(default:64): Per-render size cache entries (0disables).
DOM-only
readMeta(default:false): Readmeta[name="markdown-frontmatter"]JSON.previewMode(default:'output'):output|markdown|local.previewOutputSrcAttr(default:'data-img-output-src'): Stores final output URL when preview mode is notoutput(''disables).observeAttributeFilter(default:['src','title','alt']): Image attributes watched by MutationObserver.observeDebounceMs(default:0): Quiet-period debounce before re-processing.setDomSrc(default:true): Write display URL toimg.src.loadSrcStrategy(default:'output'): Probe source:output|raw|display.loadSrcPrefixMap(default:null): Prefix remap for probe URL.loadSrcResolver(default:null): Function override for probe URL.loadSrcMap(default:null): Static map override for probe URL.enableSizeProbe(default:true): Enable browser image probing for dimensions.awaitSizeProbes(default:true): Await all probe promises before returning summary.sizeProbeTimeoutMs(default:3000): Probe timeout (0disables timeout).probeCacheMaxEntries(default:0): Cross-run probe cache size (0disables).probeCacheTtlMs(default:0): Success cache TTL (ms).probeNegativeCacheTtlMs(default:0): Failure/timeout cache TTL (ms).keepPreviousDimensionsDuringResizeEdit(default:false): While resize title is in apendingstate, keep currentwidth/heightwhensrcis unchanged and size attrs already exist.onImageProcessed(default:null): Hook:(imgEl, info) => {}.onResizeHintEditingStateChange(default:null): Hook called on resize hint state transitions:(imgEl, { state, previousState, title, normalizedResizeValue, previousSize }) => {}.suppressNoopWarning(default:false): Silence browser default-export warning.
Additional DOM behavior:
- On
file://pages, if not explicitly overridden:suppressErrorsis auto-set tolocalenableSizeProbeis auto-set tofalse
loadSrcStrategy: 'final'is accepted as a backward-compatible alias of'output'.
Frontmatter Resolution Workflow
When resolveSrc: true, frontmatter normalization accepts these logical fields:
page.urlwith flat compatibility aliasurlimages.baseUrlwith flat compatibility aliasurlimagebaseimages.stripLocalPrefixwith flat compatibility aliaslidlocal.markdownDirwith flat compatibility aliaslmdimages.scalewith flat compatibility aliasimagescaleimages.dirUrlwith flat compatibility aliasurlimagefor an absolute public image directory URL
Supported aliases are resolved in this order:
- dotted keys
- nested object keys
- flat compatibility aliases
Conflicting values emit a warning. images.dirUrl and urlimage must be absolute; invalid values are ignored.
Relative or empty urlimage values are not treated as subdirectory hints anymore.
Node precedence:
- Prefer
env.frontmatterwhen provided. - Fall back to
md.frontmatter/md.metaonly for the current render when the markdown source itself starts with YAML frontmatter. - This avoids leaking metadata from a previous render on a reused
mdinstance.
Base URL selection order:
- Absolute
images.dirUrlorurlimage images.baseUrl/urlimagebase+ path extracted frompage.url/urlpage.url/url
Rules:
lidstrips a local prefix from relativesrc.- Query/hash are preserved.
Only .html, .htm, .xhtml are treated as file names when deriving URL path from url.
Size Calculation Workflow
Order:
- Read intrinsic dimensions
- Apply
scaleSuffix(if enabled) - Apply title resize (
resize) if present - Apply
imagescaleonly when step 3 is not used - Cap to original size (no upscaling)
Metadata emitted to HTML:
data-img-resize: effective normalized resize value (50%,320px, ...)data-img-resize-origin: emitted only forimagescaledata-img-scale-suffix: canonical filename suffix whenscaleSuffixapplies (2x,300dpi,300ppi)
DOM Probe Source Workflow
Probe URL (loadSrc) is decided in this order:
- Base from
loadSrcStrategy(output/raw/display) - Apply
loadSrcPrefixMap(if set) - Override by
loadSrcResolver(if provided) orloadSrcMap
Probe cache behavior:
probeCacheMaxEntries > 0enables cache.- Success results are cached by effective
loadSrcand useprobeCacheTtlMs. - Failure/timeout results are policy-aware: they use
probeNegativeCacheTtlMsat read time and are keyed byloadSrcplus currentsizeProbeTimeoutMs. - In-flight probe requests for the same
loadSrcplus currentsizeProbeTimeoutMsare deduplicated, even when persistent cache entries are disabled.
Observer Workflow (DOM)
startObserver():
- Watches DOM mutations and re-applies transforms.
- Uses
observeAttributeFilteras event gate. - Adds
contentto observer attributeFilter whenreadMeta: true. - Supports rAF batching and optional quiet-period debounce (
observeDebounceMs). - On meta changes, context is rebuilt and observer filter is refreshed.
API Summary (DOM Helper)
classifyResizeHint(title)->{ state, normalizedResizeValue }createContext(markdownCont, options, root)applyImageTransforms(root, contextOrOptions, markdownCont?)applyImageTransformsToString(html, contextOrOptions, markdownCont?)startObserver(root, contextOrOptions, markdownCont?)runInPreview({ root, markdownCont, observe, context, ...options })
Resize Hint Classification (DOM)
classifyResizeHint(title)returnsstate: 'valid' | 'pending' | 'invalid' | 'empty'.normalizedResizeValueis non-empty only forstate === 'valid'.
Remote Images on Node
Node remote sizing uses synchronous fetch (sync-fetch).
Recommendations for extension hosts (for example VS Code):
- Prefer
disableRemoteSize: true. - Let DOM helper handle preview sizing.
Important limit:
remoteMaxBytesis effective only whencontent-lengthis present.- Protocol-relative remote images (
//cdn.example.com/cat.jpg) are measured withhttps:first and thenhttp:if HTTPS fails. The emittedsrcitself is not rewritten by this fallback.
Testing
npm test(Node plugin + YAML/frontmatter tests)npm run test:script(DOM helper tests)
VS Code / Webview Notes
- Webview blocks raw
file://access. - For local probing in Webview, provide
lmdas a webview URI (for example viaasWebviewUri). - If
previewMode: 'markdown'is used in Webview, relative paths may not resolve; preferpreviewMode: 'output'there.
With @peaceroad/markdown-it-figure-with-p-caption
Do not overload title with both caption text and resize hints.
- If title is used for resize hints: keep caption in paragraph/alt.
- If title is used for caption: disable auto title hiding (
autoHideResizeTitle: false) or avoid resize hints in title.
Performance Checklist
- Node:
- Disable remote sizing in extension hosts:
disableRemoteSize: true - Keep
cacheMax > 0for repeated images in same render
- Disable remote sizing in extension hosts:
- DOM:
- For editing-heavy preview:
enableSizeProbe: false - If probing is needed: tune
observeDebounceMsand probe cache TTLs - If only metadata transform is needed:
setDomSrc: false
- For editing-heavy preview:
