npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@lnsy/markdown-editor

v0.3.2

Published

A lightweight Markdown editor Web Component built on CodeMirror 6

Readme

Markdown Editor Web Component (@lnsy/markdown-editor)

A lightweight Markdown editor custom element built on CodeMirror 6. It registers the tag when imported and is themeable via CSS variables.

Highlights:

  • Web Component: with zero framework dependencies
  • CodeMirror 6 under the hood (minimalSetup, Markdown, HTML, autocompletion)
  • Smart light-DOM bootstrapping: initial content can be authored between the tags and is normalized/dedented
  • Convenience keybinding: Tab inserts two spaces
  • Extra line decorations: horizontal rules (---), fenced code blocks (```), and asides (:::) with full-line styling
  • Emits EDITOR-UPDATED when content changes
  • Fully themeable using CSS custom properties (variables)

Quick start (rspack)

Install the package:

npm install @lnsy/markdown-editor

For local development in this repo, install dependencies and run the dev server:

npm install
npm run start

By default this serves index.html and emits dist/markdown-editor.min.js. Open http://localhost:3000.

Build for production:

npm run build

This writes optimized assets to dist/ (e.g., dist/markdown-editor.min.js).

Optional .env customization:

# Output file name (defaults to markdown-editor.min.js)
OUTPUT_FILE_NAME=my-app.js
# Dev server port (defaults to 3000)
PORT=5173

Using the element

The component is automatically registered when you import the bundle. You can either use the built bundle (no-bundler) or import the source into your own bundler (rspack, webpack, Vite, etc.).

  • Using the built bundle (no bundler):
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Markdown Editor</title>
    <script type="module" src="./dist/markdown-editor.min.js"></script>
  </head>
  <body>
    <markdown-editor>
# Hello Markdown

Type here. Use --- for an HR line, ::: for asides, and fenced ```js blocks.
    </markdown-editor>
  </body>
</html>
  • Importing in your bundler entry (rspack example):
// index.js
import "./index.css";            // your global styles (optional)
import "./src/markdown-editor.js"; // registers <markdown-editor>
// rspack.config.js (already provided in this repo)
module.exports = {
  entry: "./index.js",
  // ...
};
  • Adding an instance in your app markup:
<markdown-editor>
# Title

Some initial content here. It will be normalized (dedented and without leading/trailing blank noise).
</markdown-editor>

Programmatic API

The element provides convenience methods for common operations, as well as direct access to the CodeMirror EditorView instance (el.view) for advanced use.

Convenience methods

  • insertString(str, line_number, char_number): Insert text at a specific position
    • str (string): The text to insert
    • line_number (number): 1-indexed line number
    • char_number (number): 0-indexed character position within the line
    • Returns: true on success, false on failure
const el = document.querySelector("markdown-editor");
el.insertString("Hello world", 1, 0); // Insert at beginning of line 1
  • replaceString(str, start_line, start_char, end_line, end_char): Replace text in a range
    • str (string): The replacement text
    • start_line (number): 1-indexed starting line number
    • start_char (number): 0-indexed starting character position
    • end_line (number): 1-indexed ending line number
    • end_char (number): 0-indexed ending character position
    • Returns: true on success, false on failure
const el = document.querySelector("markdown-editor");
// Replace first 5 characters of line 1 with "# Title"
el.replaceString("# Title", 1, 0, 1, 5);
  • getCursor(): Get current cursor position and context information
    • Returns: Object with cursor details, or null on error
      • line (number): 1-indexed line number
      • character (number): 0-indexed character position within line
      • context (string[]): Array of context tags describing cursor location
      • selected (string, optional): Selected text if a selection exists
const el = document.querySelector("markdown-editor");
const cursor = el.getCursor();
console.log(cursor);
// Example outputs:
// { line: 5, character: 12, context: ["js", "codeblock"] }
// { line: 2, character: 0, context: ["heading", "h1"] }
// { line: 10, character: 5, context: ["note", "aside"] }
// { line: 3, character: 2, context: [], selected: "some text" }

Context tags:

  • "codeblock": Cursor is inside a fenced code block (```)
  • Language name (e.g., "js", "python"): Specific code block language
  • "aside": Cursor is inside an aside block (:::)
  • Aside type (e.g., "note", "warning"): Specific aside type
  • "heading": Current line is a heading
  • Heading level (e.g., "h1", "h2"): Specific heading level

Direct CodeMirror access

The element exposes its CodeMirror EditorView instance as el.view. You can read and write the document programmatically.

  • Get current text:
const el = document.querySelector("markdown-editor");
const text = el.view.state.doc.toString();
console.log(text);
  • Replace entire document:
const el = document.querySelector("markdown-editor");
const { state } = el.view;
el.view.dispatch({
  changes: { from: 0, to: state.doc.length, insert: "# Replaced\n\nNew content." }
});
  • Append text at the end:
const el = document.querySelector("markdown-editor");
const end = el.view.state.doc.length;
el.view.dispatch({ changes: { from: end, insert: "\n\nAppended line." } });
  • Focus the editor:
const el = document.querySelector("markdown-editor");
el.view.focus();

Notes:

  • Tab inserts two spaces by default.
  • Markdown language support includes fenced code languages. JavaScript (js/javascript/node) is loaded eagerly; other languages are provided via @codemirror/language-data.

Events

The editor dispatches bubbling, composed CustomEvents for various interactions:

EDITOR-UPDATED

Fires whenever the document content changes.

const el = document.querySelector("markdown-editor");
el.addEventListener("EDITOR-UPDATED", (evt) => {
  // Respond to content changes
  console.log("markdown changed", el.view.state.doc.toString());
});

IMAGE-DROPPED

Fires when an image file is dropped onto the editor. The event detail contains image metadata and a data URL.

Event detail properties:

  • fileName (string): Original filename
  • fileSize (number): File size in bytes
  • fileType (string): MIME type (e.g., "image/png")
  • lastModified (number): Unix timestamp
  • lastModifiedDate (string): ISO 8601 date string
  • dataURL (string): Base64-encoded data URL of the image
const el = document.querySelector("markdown-editor");
el.addEventListener("IMAGE-DROPPED", (evt) => {
  const { fileName, fileSize, fileType, dataURL } = evt.detail;
  console.log(`Image dropped: ${fileName} (${fileSize} bytes)`);
  // Use dataURL to display or upload the image
  // The editor automatically inserts ![[filename]] syntax at cursor
});

IMAGE-DROP-ERROR

Fires if an error occurs while processing a dropped image.

Event detail properties:

  • error (string): Error message
  • fileName (string): Name of the file that caused the error
const el = document.querySelector("markdown-editor");
el.addEventListener("IMAGE-DROP-ERROR", (evt) => {
  const { error, fileName } = evt.detail;
  console.error(`Failed to process ${fileName}: ${error}`);
});

Styling and CSS variables

All base theming is driven by CSS custom properties. The component imports styles/variables.css (defaults) and styles/theme.css (rules that consume the variables). You can override any variable globally (via :root) or locally (scoped to a container).

Common variables (from styles/variables.css):

  • --bg-color, --fg-color
  • --darker-neutral-color, --neutral-bg-color, --lighter-neutral-bg-color, --neutral-fg-color
  • --highlight-color
  • --secondary-color, --trinary-color, --quaternary-color
  • --link-color
  • --code-secondary-highlight-color, --code-foreground-color
  • --code-background-color, --code-background-color-darker
  • --code-highlight-color, --code-neutral-color
  • --font-size, --line-height
  • --md-heading-weight, --md-h1-size, --md-h2-size, --md-h3-size, --md-h4-size, --md-h5-size, --md-h6-size

Additional variables used by theme.css line decorations:

  • --md-hr-line-bg
  • --md-aside-fence-bg
  • --md-aside-bg

The theme also respects a body font variable if you provide it:

  • --body-font-family

  • Global, site-wide overrides:

:root {
  --body-font-family: "Fira Code", monospace;
  --bg-color: #0d1117;
  --fg-color: #e6edf3;
  --neutral-bg-color: #30363d;
  --lighter-neutral-bg-color: #21262d;
  --highlight-color: rgba(56, 139, 253, 0.35);
  --secondary-color: #58a6ff;
  --trinary-color: #7ee787;
  --quaternary-color: #d2a8ff;
  --font-size: 15px;
  --line-height: 1.6;
  /* line decoration specifics */
  --md-hr-line-bg: rgba(125,125,125,0.12);
  --md-aside-fence-bg: rgba(253,186,116,0.18);
  --md-aside-bg: rgba(253,186,116,0.12);
}
  • Scoped overrides (only affect editors inside .note):
.note {
  --bg-color: #fff8e7;
  --fg-color: #5b4931;
  --highlight-color: #fff176; /* selection color */
  --code-background-color: #e8e1d6;
}
  • Override only selection/active-line accents:
:root {
  --highlight-color: rgba(255, 199, 0, 0.35); /* selection */
  --neutral-bg-color: #b0bec5;                 /* active line left bar */
}
  • Heading line accents (applied by decoration classes):
:root {
  /* The theme maps heading decoration backgrounds to these */
  --darker-neutral-color: #cfd8dc; /* h1 */
  --secondary-color: #a5d6a7;      /* h2 */
  --trinary-color: #90caf9;        /* h3 */
  --quaternary-color: #b39ddb;     /* h4–h6 */
}
  • Example: index.html with inline overrides
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Demo</title>
    <script type="module" src="/markdown-editor.min.js"></script>
    <style>
      :root {
        --body-font-family: "Fira Code", monospace;
        --bg-color: pink;
        --fg-color: blue;
        --darker-neutral-color: green;
        --neutral-bg-color: yellow;
        --lighter-neutral-bg-color: purple;
      }
    </style>
  </head>
  <body>
    <markdown-editor>
# Markdown Editor

Try typing, or paste code fences and asides.
    </markdown-editor>
  </body>
</html>

Advanced styling

  • Token colors come from classHighlighter and theme.css selectors like .tok-keyword, .tok-string, .tok-comment, etc. You can further refine these with custom CSS if needed.
  • Line decoration classes you can target: .cm-hr-line, .cm-code-fence-line, .cm-code-block-line, .cm-aside-fence-line, .cm-aside-block-line, .cm-md-heading with variants .cm-md-h1..h6.

Behavior details

  • Initial content: The editor reads the element’s textContent on first mount, normalizes CRLF to LF, removes an initial stray newline, dedents common indentation, and trims trailing whitespace at the end of the block. The light-DOM content is then cleared and the editor is mounted.
  • Keybindings: Tab inserts two spaces. Ctrl-Space triggers completion (via CodeMirror completionKeymap).

Project scripts (rspack)

  • Start dev server:
npm run start
  • Build production bundle:
npm run build
  • Optional .env settings:
OUTPUT_FILE_NAME=my-custom-filename.js
PORT=8080

License

See LICENSE.