@peaceroad/markdown-it-renderer-image
v0.10.0
Published
A markdown-it plugin. This add width and height attributes to img elements.
Readme
p7d-markdown-it-renderer-image
A markdown-it plugin plus a browser DOM helper to set img attributes (width/height, loading, decoding) and optionally resolve src using frontmatter.
Quick start
Node usage (markdown-it)
import fs from 'fs'
import mdit from 'markdown-it'
import mditRendererImage from '@peaceroad/markdown-it-renderer-image'
const mdFile = '/tmp/markdown.md'
const md = mdit().use(mditRendererImage, { mdPath: mdFile })
const mdCont = fs.readFileSync(mdFile, 'utf-8')
console.log(md.render(mdCont))mdPath accepts either a markdown file path or a directory path. If a directory is provided, it is used as-is for local image resolution.
You can also pass mdPath via the render env:
const md = mdit().use(mditRendererImage)
console.log(md.render(mdCont, { mdPath: mdFile }))Frontmatter-based rewriting on Node:
const frontmatter = { url: 'https://example.com/page' }
const html = md.render(mdCont, { mdPath: mdFile, frontmatter })
console.log(html)Browser / DOM usage
<script type="module">
import { runInPreview, applyImageTransformsToString } from '<package>/script/set-img-attributes.js'
const { observer } = await runInPreview({
root: document,
markdownCont,
readMeta: true, // read frontmatter from <meta>
observe: true, // start MutationObserver
})
// Process an HTML string (e.g. source view)
const transformed = await applyImageTransformsToString(htmlSource, { readMeta: true }, markdownCont)
// Stop observing when needed
observer?.disconnect()
</script>Notes:
- The DOM helper provides named functions; the default export is a no-op compatibility shim.
runInPreview({ root, markdownCont, observe, ...options })is a high-level helper for preview flows (create context + apply + optional observer).- The default-export no-op returns a resolved Promise for safer
.catch(...)chaining in accidental async usage. - The no-op warning is shown once in non-production. Set
suppressNoopWarning: trueto silence it. - In browser builds, importing the package root resolves to the DOM helper (named exports + no-op default). Use the script path if you want to be explicit.
- Default option objects are exported (
defaultSharedOptions,defaultDomOptions,defaultNodeOptions) to keep CLI and DOM configs in sync. createContextreads YAML frontmatter from the first argument (markdown text). Passnull/''to skip YAML parsing and rely onreadMeta(JSON inmeta[name="markdown-frontmatter"]).- The DOM script imports
./img-util.jsas a module; bundle it or ensure the base URL resolves correctly. - For source views or HTML previews, prefer
applyImageTransformsToString()before mounting into the live DOM.
Example (bundler or app code that rerenders HTML):
import { runInPreview } from '@peaceroad/markdown-it-renderer-image/script/set-img-attributes.js'
import { defaultDomOptions } from '@peaceroad/markdown-it-renderer-image'
txt.addEventListener('input', async () => {
const markdownCont = txt.value
html.innerHTML = renderedHtml
await runInPreview({
root: html,
markdownCont,
...defaultDomOptions,
readMeta: true,
})
})Node vs DOM parity
To make Node and DOM results match (final src + width/height):
- DOM:
previewMode: 'output'andsetDomSrc: true - Node:
resolveSrc: true
Intentional differences:
- DOM:
previewModecan separate displaysrcfrom final output URL (Node has no equivalent). - DOM:
setDomSrc: falsecan skip DOMsrcrewriting while still sizing (Node has no equivalent). - Node:
mdPath,disableRemoteSize,remoteTimeoutcontrol local/remote sizing (DOM has no equivalent).
Options (summary)
Node plugin options (defaults)
scaleSuffix(false): scale by@2x,300dpi,300ppisuffixes.resize(false): resize by title hint.autoHideResizeTitle(true): remove title when resize hint is used.resizeDataAttr(data-img-resize): store resize hint when title is removed (set''to disable).lazyLoad(false): addloading="lazy".asyncDecode(false): adddecoding="async".checkImgExtensions(png,jpg,jpeg,gif,webp): extensions to size.resolveSrc(true): resolve finalsrcusing frontmatter (no-op without frontmatter orurlImageBase).mdPath(empty): markdown file path or markdown directory for local sizing. When the path exists, Node usesfs.statSyncto decide file vs directory. When the path does not exist, the heuristic is: trailing slash -> directory, otherwise treat paths with an extension as files and everything else as a directory. If you pass an extension-less file path that doesn't exist yet, it will be treated as a directory.disableRemoteSize(false): skip remote sizing.remoteTimeout(5000): sync fetch timeout in ms.remoteMaxBytes(16MB): skip large remote images when content-length is present.cacheMax(64): per-render cache size (0 disables cache).suppressErrors(none):none|all|local|remote.urlImageBase(empty): fallback base when frontmatter lacksurlimagebase.outputUrlMode(absolute):absolute|protocol-relative|path-only.
DOM script options (defaults)
Same as Node options except remote sizing options, plus:
Scale/resize/title-handling and lazy/decoding options behave the same as the Node plugin: they affect width/height calculation and set loading/decoding attributes on the DOM.
readMeta(false): readmeta[name="markdown-frontmatter"](JSON).previewMode(output):output|markdown|local.output: display final URL (default).markdown: display the original markdownsrc(best for drag & drop/blob mapping).local: display a local file URL whenlmdis an absolute path.
previewOutputSrcAttr(data-img-output-src): attribute name to store the final URL whenpreviewMode !== output(set''to disable).loadSrcStrategy(output): pick the source for size probing.output|raw|display.loadSrcStrategy: 'final'is accepted for backward compatibility and treated asoutput.loadSrcPrefixMap(null): object map to rewriteloadSrcby prefix before probing (e.g.,{ "/img": "http://localhost:3000/img" }).loadSrcResolver(null): function to override the measurement source (loadSrc) for size calculation (DOM only).loadSrcMap(null): map ofsrc->loadSrcoverrides for size calculation (DOM only).setDomSrc(true): when false, leavesimg.srcuntouched (size probing still runs).enableSizeProbe(true): when false, skips size probing entirely (no network or image load).awaitSizeProbes(true): wait for image load before resolvingapplyImageTransforms.sizeProbeTimeoutMs(3000): timeout for size probes (0 disables).onImageProcessed(null): per-image callback(imgEl, info) => {}.suppressNoopWarning(false): silence the browser default-export warning.
readMeta is opt-in to avoid extra DOM work in normal pages; enable it for live preview scenarios (e.g., VS Code). When running a page from file://, the DOM script defaults suppressErrors to local and disables enableSizeProbe unless you explicitly override them, to reduce noisy console errors from local image probes.
suppressErrors only silences renderer-image logs; browser network errors (CORS/404) can still appear in the console.
Editing/draft mode tip: to avoid noisy requests while users type, set setDomSrc: false, enableSizeProbe: false, and previewMode: 'markdown', then re-run transforms when the input stabilizes.
Options (details)
Resolve output image src from frontmatter or options
When resolveSrc: true (default), image src is resolved using frontmatter keys.
Frontmatter is used only when resolveSrc: true. If frontmatter (and urlImageBase) is missing, src is left untouched.
Frontmatter keys (lowercase only):
url: page base URL.urlimage: image base URL (absolute) or image directory (relative/empty).urlimagebase: base URL used with the path fromurl.lid: local image directory prefix to strip from relativesrcso the remaining subpath can be reused in the final URL.lmd: local media directory for DOM size loading. If it is an absolute path without a scheme, it is converted to afile:///URL with encoded segments; relative paths are kept as-is.imagescale: scale factor applied to all images (e.g.60%or0.6, values above 100% are capped).
Base selection order:
urlimagewhen it is absolute (has a domain or starts with//).urlimagebase(frontmatter) orurlImageBase(option) + path fromurl.url.
If urlimage is relative (no domain), it becomes an image directory inserted between base and filename, and only the basename from src is used. Use urlimage: (empty) or urlimage: . to force basename-only without adding a directory.
Examples:
---
url: https://example.com/page
urlimage: images
---

# -> https://example.com/page/images/cat.jpg (relative urlimage uses basename-only)---
urlimage: https://image.example.com/assets/
---

# -> https://image.example.com/assets/cat.jpg---
url: https://example.com/page
urlimagebase: https://image.example.com/assets/
urlimage: images
---

# -> https://image.example.com/assets/page/images/cat.jpglid removes only the matching prefix and keeps the remaining subpath:
---
lid: image
---
 # -> cat.jpg
 # -> chapter/cat.jpgExample of a global scale factor:
---
imagescale: 60%
---
 # -> width/height scaled to 60%imagescale is applied after scaleSuffix and only when no title resize hint is present (resize takes priority). Values above 100% are capped. Order:
- Read original size
- Apply
scaleSuffix(e.g.@2x) - Apply title resize (
resize: true) if present - Apply
imagescale(global scale) only when step 3 is not used
Example: 400x300 image with @2x, title resize:50%, and imagescale: 0.5
-> 400x300 -> 200x150 -> 100x75 (imagescale skipped)
Local sizing in DOM (lmd)
In browsers, local file access is restricted. For local sizing in the DOM script, provide lmd (local markdown directory) as a path or a Webview URI:
---
lmd: C:\Users\User\Documents\manuscript
---In VS Code, pass a Webview URI (e.g., asWebviewUri) instead of a raw file:// path.
Preview modes in DOM
When previewMode is markdown or local, the DOM script stores the final URL in previewOutputSrcAttr (default data-img-output-src) so you can copy/export HTML with the CDN URL. The original markdown src is cached in data-img-src-raw to keep reprocessing stable; lmd is still used for size measurement when available. In VS Code Webview, relative src may not resolve, so use previewMode: 'output' there.
If you need to measure sizes from Blob URLs (e.g., drag-and-drop files), pass loadSrcResolver or loadSrcMap so the DOM script uses those URLs only for measurement without changing the displayed src. Functions cannot be provided via readMeta JSON, so use direct options for loadSrcResolver. For JSON-only settings, use loadSrcStrategy (e.g. raw) and loadSrcPrefixMap to rewrite probing URLs without custom code.
Load source selection order for probing:
- Base source is picked by
loadSrcStrategy(output,raw, ordisplay). loadSrcPrefixMap(if set) rewrites the probe URL by prefix.loadSrcResolver(if provided) orloadSrcMapoverrides everything.
raw uses the original markdown src and does not pass through lmd or urlImageBase. If raw is a relative path, size probes will likely fail unless you also provide lmd, loadSrcPrefixMap, or a resolver/map to convert it into a loadable URL.
JSON-only example (VS Code settings):
{
"rendererImage.loadSrcStrategy": "raw",
"rendererImage.loadSrcPrefixMap": {
"/img/": "http://localhost:3000/img/"
}
}Only .html, .htm, .xhtml are treated as file names when deriving the path from url (used by urlimagebase).
url: https://example.com/page->/page/url: https://example.com/page/index.html->/page/url: https://example.com/v1.2/->/v1.2/
outputUrlMode
Applied at the end:
protocol-relative:https://a/b->//a/bpath-only:https://a/b->/b(same-origin only)
Modify output width/height attributes from filename suffixes
When scaleSuffix: true, scales dimensions by:
@2x(half size)_300dpi/_300ppi(convert to 96dpi)
This is identified by imageFileName.match(/[@._-]([0-9]+)(x|dpi|ppi)$/)
Example:
const md = mdit().use(mditRendererImage, { scaleSuffix: true })
md.render('', { mdPath: mdFile })
// <img ... width="200" height="150"> //cat.jpg is 400px wide and 300px high.Modify output width/height attributes from title resize hints
When resize: true, resizes dimensions by title patterns. Example:
const md = mdit().use(mditRendererImage, { resize: true })
md.render('', { mdPath: mdPat })
// <img ... width="200" height="150"> //cat.jpg is 400px wide and 300px high.Title patterns include:
Resize:50%リサイズ:50%サイズ変更:50%
This is identified by imgTitle.match(/(?:(?:(?:大きさ|サイズ)の?変更|リサイズ|resize(?:d to)?) *[::]? *([0-9]+)([%%]|px)|([0-9]+)([%%]|px)[にへ](?:(?:大きさ|サイズ)を?変更|リサイズ))/i)
For px values, the number is treated as the target width and height is scaled to preserve aspect ratio (e.g. 400x300 with resize:200px -> 200x150). The final size is capped at the original image dimensions (no upscaling).
When autoHideResizeTitle: true (default), titles with resize hints are removed (Node/DOM). Set autoHideResizeTitle: false to keep titles even when resize hints are used. Resize hints are preserved in resizeDataAttr by default (data-img-resize) using normalized values like 50% or 200px; set resizeDataAttr: '' to disable.
Default behavior example (when resize: true):
const md = mdit().use(mditRendererImage, { resize: true })
md.render('', { mdPath: mdPat })
// <img ... width="200" height="150" data-img-resize="50%">If you render HTML with the Node plugin and then run the DOM script, keep resizeDataAttr: 'data-img-resize' (default) so the resize hint survives title removal. If you do not need DOM reprocessing, set resizeDataAttr: '' to avoid extra attributes.
Set loading and decoding attributes
lazyLoad: true->loading="lazy"asyncDecode: true->decoding="async"
Check image extensions
Only files matching checkImgExtensions are sized. Query/hash is ignored.
Remote images (Node)
Remote sizing is synchronous. For extension hosts (e.g., VS Code), set disableRemoteSize: true and let the DOM script size remote images.
Testing
npm test(Node plugin + frontmatter tests)npm run test:script(DOM script tests)
VS Code / Webview notes
- Webview blocks
file://. Passlmdas a Webview URI (asWebviewUri) if you need local sizing in the DOM. - The DOM script imports
./img-util.jsas a module; bundle it or ensure the base URL resolves correctly.
Note: Using with @peaceroad/markdown-it-figure-with-p-caption
markdown-it-figure-with-p-caption can auto-generate captions from title text when its detection rules match. To avoid conflicts, use title for one purpose only:
- Title as resize hint (recommended): keep captions in a paragraph or alt text, and avoid caption-like strings in title. This lets
autoHideResizeTitlework as intended. - Title as caption: do not put resize hints in title, or set
autoHideResizeTitle: falseso the title remains for caption detection.
In short: don’t mix resize hints and caption text in the same title.
Performance tips
- Node: remote sizing is synchronous; set
disableRemoteSize: truefor extension hosts and rely on DOM sizing. - Node: keep
cacheMax> 0 to avoid repeated I/O on the same image set. - DOM: for fast previews, set
enableSizeProbe: falseorawaitSizeProbes: false. - DOM: if you only need metadata, set
setDomSrc: falseto avoid triggering image loads.
