🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

dom-md

Package Overview
Dependencies
Maintainers
1
Versions
2
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

dom-md

Render DOM into readable Markdown.

latest
Source
npmnpm
Version
0.1.0
Version published
Weekly downloads
1K
12425%
Maintainers
1
Weekly downloads
 
Created
Source

dom-md

dom-md renders the DOM already on screen as readable Markdown.

import { renderMarkdown } from 'dom-md';

const markdown = renderMarkdown();

The DOM remains the source of truth. Start with semantic HTML and accurate ARIA, then add data-md or plugins where the visual UI needs a clearer text form.

Render a page or region

With no root, renderMarkdown() uses the first data-md-root, then main, then document.body.

renderMarkdown();
renderMarkdown(document.querySelector('[data-request-panel]'));

const parsed = new DOMParser().parseFromString(html, 'text/html');
renderMarkdown(parsed);

Use a renderer when several call sites share configuration:

import { createMarkdownRenderer } from 'dom-md';
import { budget, frontmatter } from 'dom-md/plugins';

const renderer = createMarkdownRenderer({
  plugins: [
    budget({ maxChars: 20_000 }),
    frontmatter(() => ({ title: document.title, url: location.href })),
  ],
});

const tree = renderer.toTree(document.body);
const markdown = renderer.stringify(tree);

// Pure tree serialization:
renderer.stringify(tree);

// Full render, including terminal postprocess plugins:
renderer.render(document.body);

The tree is a small Markdown tree that is compatible with the MDAST shapes dom-md emits. stringify(tree) stays pure; render(root) also applies terminal postprocess plugins.

DOM attributes

Exclude interface chrome:

<button data-md-ignore aria-label="Close"><CloseIcon /></button>

Replace a region with authored Markdown:

<section data-md={'## Consumption\n\n- Compute: 42 GB-hours'}>
  <UsageChart />
</section>

data-md is trusted as raw Markdown and emitted as-is. Put a children token on its own line to splice live descendants between raw Markdown chunks:

<section data-md={'## Request\n\n{{children}}'}>
  <p>Status: 200</p>
</section>

Use data-md-children-token to choose another token, data-md-max-chars for a local cap, and data-md-root to select the default capture root.

Default behavior

The defaults cover semantic text, headings, lists, links, code, native and ARIA tables, live form values, common ARIA widget state, buttons, images, labeled SVGs, canvases, hidden content, and ignored content. Password values are marked as omitted and hidden inputs are skipped.

Text content gets contextual minimal escaping: characters that would change Markdown structure (backticks, brackets, asterisks, emphasis-forming underscores, and paragraph-leading heading, list, or blockquote markers) are escaped, while ordinary prose such as snake_case stays untouched. Markdown provided through data-md or rawMarkdown nodes is never escaped.

Plugins

Callers pass one plugins list. A plugin is made of steps, and each step runs in one phase:

type MarkdownPlugin =
  | AnnotateElementPlugin
  | RenderElementPlugin
  | TransformPlugin
  | PostprocessPlugin;

Array order matters inside a phase. A user plugin with the same phase and id replaces that default. The default renderElement IDs are data-md, visuals, tables, aria-widgets, forms, and buttons; max-chars runs as a default annotate/transform pair. To disable a default, replace it with a step that always passes:

const markdown = renderMarkdown(root, {
  plugins: [{ id: 'tables', phase: 'renderElement', render: () => undefined }],
});

Most code should author plugins with createPlugin().

Render an element

Render element plugins return one or more Markdown tree nodes, null to omit, or undefined to continue to the next renderer/default.

import { createPlugin } from 'dom-md/plugins';
import { paragraph, text } from 'dom-md/builders';

const compactButtons = createPlugin('buttons', ({ renderElement }) => [
  renderElement((element) => {
    if (element.tagName !== 'BUTTON') return undefined;

    const label =
      element.getAttribute('aria-label') ??
      element.textContent?.trim() ??
      'Unlabeled action';

    if (label === 'Copy') return null;
    return paragraph([text(`Action: ${label}`)]);
  }),
]);

The context provides:

  • children(parent?) — extracted child nodes
  • inline(parent?) — extract child content as inline nodes for a paragraph or heading
  • extract(element) — extract another element through the full pipeline
  • renderDefault() — render the current element through later plugins and built-ins

Most render plugins use inline(), children(), or extract(). renderDefault() is available for rare cases that intentionally build on the default output for the same element.

Annotate an element

Use annotateElement() when a DOM attribute should become typed metadata for a later transform, without changing how the element renders.

Annotations are merged onto the Markdown nodes emitted for that element. Read the metadata later from node.data. For wrapper elements, the emitted nodes may come from descendants.

import { createPlugin } from 'dom-md/plugins';
import { visit } from 'dom-md/tree';

type ViewportData = {
  viewport?: {
    visible: boolean;
  };
};

const viewport = createPlugin<ViewportData>('viewport', ({ annotateElement, transform }) => [
  annotateElement((element) => {
    if (!element.hasAttribute('data-offscreen')) return undefined;
    return { viewport: { visible: false } };
  }),

  transform((tree) => {
    visit(tree, (node) => {
      if (node.data?.viewport?.visible === false && node.type === 'paragraph') {
        node.children = [text('Offscreen content omitted')];
      }
    });
  }),
]);

Transform the tree

Transforms receive the Markdown tree Root. They may mutate it and return nothing, or return a replacement root. context.stringify(node) uses the renderer's canonical Markdown serializer, which makes measurements reliable.

import { heading, text } from 'dom-md/builders';
import { createPlugin } from 'dom-md/plugins';

function generatedTitle(title: string) {
  return createPlugin('generated-title', ({ transform }) => [
    transform((tree) => {
      tree.children.unshift(heading(1, [text(title)]));
    }),
  ]);
}

Built-in plugins include budget(), outline(), frontmatter(), and redactSecrets(). Frontmatter inserts a YAML node; it is not a string postprocessor. Secret redaction combines a render step, a tree transform, and a final string postprocess pass — entirely on the public plugin API, so your app could ship the same plugin in userland.

redactSecrets() is opt-in and redacts common token formats plus credentialed URL passwords before Markdown leaves a capture surface:

import { redactSecrets } from 'dom-md/plugins';

const markdown = renderMarkdown(document.body, {
  plugins: [
    redactSecrets({
      patterns: [/tenant_secret_[a-z0-9]+/g],
      replacement: (value) => `[REDACTED:${value.length}]`,
    }),
  ],
});

Add data-md-redact when markup knows a field is sensitive even if it does not match a built-in pattern:

<span data-md-redact>internal-only-value</span>

Use visit() from dom-md/tree when a transform needs to inspect every node:

import { visit } from 'dom-md/tree';
import { createPlugin } from 'dom-md/plugins';

const markFields = createPlugin('mark-fields', ({ transform }) => [
  transform((tree) => {
    visit(tree, (node) => {
      if (node.data?.domMd?.kind === 'field') {
        node.data.app = { important: true };
      }
    });
  }),
]);

Postprocess the final string

Postprocess plugins run only during render(), after the Markdown tree has already been serialized. Use them for terminal string concerns such as wrapping, transport framing, or final redaction:

import { createPlugin } from 'dom-md/plugins';

const xmlEnvelope = createPlugin('xml-envelope', ({ postprocess }) => [
  postprocess((markdown) =>
    `<page-markdown>\n${markdown}\n</page-markdown>`),
]);

Tree interoperability

The public tree is MDAST-compatible, with a small rawMarkdown extension for trusted authored Markdown and optional provenance under node.data.domMd:

type DomMdMetadata = {
  authored?: boolean;
  chunk?: boolean;
  kind?: 'action' | 'field' | 'override' | 'visual';
  label?: string;
  sourceId?: string;
};

This works directly with unist-util-visit and with Remark plugins that operate on the node shapes dom-md emits. dom-md itself does not host a unified runtime because its primary job is DOM interpretation, not Markdown parsing.

For example, a Remark plugin can be adapted into the same plugin list without making unified part of the renderer runtime. Install the Remark/unified packages in your app if you want this adapter:

import remarkToc from 'remark-toc';
import { unified } from 'unified';
import type { MdastRoot } from 'dom-md';
import { createPlugin } from 'dom-md/plugins';

const tocProcessor = unified().use(remarkToc);
const toc = createPlugin('remark-toc', ({ transform }) => [
  transform((tree) =>
    tocProcessor.runSync(tree as never) as MdastRoot),
]);

The renderer validates supported Markdown tree content models after extraction, after each transform, and before public stringification. A malformed plugin throws a DomMarkdownError with the phase and plugin id. If a plugin needs to emit Markdown syntax outside dom-md's tree model, use a rawMarkdown node.

Next.js

dom-md/next provides createMarkdownRoute() for a same-origin catch-all Route Handler. The adapter forwards cookie and authorization headers only to the application origin, rejects cross-origin redirects, bounds response size and fetch time, marks output noindex, and defaults to private, no-store.

import { renderMarkdown } from 'dom-md';
import { createMarkdownRoute } from 'dom-md/next';
import { DOMParser } from 'linkedom';

export const GET = createMarkdownRoute(
  ({ document }) => renderMarkdown(document),
  { parser: new DOMParser() },
);

Entry points

Entry pointPurpose
dom-mdRendering, Markdown tree access, and plugin contracts
dom-md/pluginsBuilt-in plugins and the plugin authoring API
dom-md/buildersSmall Markdown tree builders
dom-md/treeMarkdown tree traversal helpers
dom-md/mdString helpers for authoring data-md values
dom-md/devtoolsInspection, provenance, audit, and browser panel
dom-md/nextNext.js route adapter

API

renderMarkdown(root?, options?): string;
toMarkdownTree(root?, options?): MarkdownTree;
stringifyMarkdown(tree, options?): string;

createMarkdownRenderer(options?): {
  render(root?): string;
  toTree(root?): MarkdownTree;
  stringify(tree): string;
};

FAQs

Package last updated on 03 Jul 2026

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts