Skip to content

Web-component to render markdown into html with syntax highlighting

License

Notifications You must be signed in to change notification settings

xan105/web-component-markdown

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

62 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

About

Web-component to load an external markdown file (.md) and render it into sanitized HTML.

  • GFM (GitHub Flavored Markdown spec)
  • Light DOM CSS styling
  • Optional JavaScript API
  • Code syntax highlighting
  • Table of contents
  • Copy code to clipboard
  • Media embedding (image, audio, video)

πŸ›  Under the hood it's powered by Marked and Highlight.js.

πŸ“¦ Scoped @xan105 packages are for my own personal use but feel free to use them.

πŸ€” Curious to see it in real use? This package powers my personal blog.

Published on webcomponents.org

Example

Import and define the Web-component:

import { Markdown } from "/path/to/markdown.js"
customElements.define("mark-down", Markdown);

HTML:

  <mark-down src="/path/to/md"></mark-down>

Optional JavaScript API:

  const el = document.querySelector("mark-down");
  // or
  const el = document.appendChild(new Markdown);
  
  el.addEventListener("load", ()=>{
    console.log("loading...");
  });
  el.addEventListener("success", ()=>{
    console.log("ok");
  });
  el.addEventListener("failure", ({detail})=>{
    console.error(detail.error);
  });

  //auto rendering (default)
  el.integrity = "sha384-0xABCD...";
  el.src = "/path/to/md";

  //manual rendering
  el.manual = true;
  el.src = "/path/to/md";
  el.render().catch((err)=>{
    console.error(err);
  });
  
  //Table of contents
  const toc = el.headings.createElement({ depth: 4 });
  document.querySelector("#toc").replaceWith(toc);
  
  el.addEventListener("intersect", ({detail})=>{
    //Do something when a heading (h1, h2, ...) has entered the top of the viewport
    document.querySelector(`#toc a[href="#${detail.id}"]`).classList.add("active");
  });

Install

npm i @xan105/markdown

πŸ’‘ The bundled library and its minified version can be found in the ./dist folder.

Via importmap

Create an importmap and add it to your html:

  <script type="importmap">
  {
    "imports": {
      "@xan105/markdown": "./path/to/node_modules/@xan105/markdown/dist/markdown.min.js"
    }
  }
  </script>
  <script type="module">
    import { Markdown } from "@xan105/markdown"
    customElements.define("mark-down", Markdown);
  </script>
  </body>
</html>

Styling

Markdown is rendered into the light DOM without any predefined CSS styling, this is by design.
Use regular selectors to style just like you would for the rest of the page.

For code syntax highlighting you can use one of the many hljs themes available.

πŸ’‘That being said, there is a basic CSS style with Github-like syntax highlighting available in the ./dist folder to get you started.

Copy to clipboard

To target the "copy to clipboard" unstyled button added to "code blocks" use CSS ::part() selector:

clipboard-copy-code { display: block } //by default it is not rendered (display: none)
clipboard-copy-code::part(button) { ... }
clipboard-copy-code::part(button)::before { /*go nuts this also works*/ }

clipboard-copy-code will have the attribute copied set when the content has been copied to the clipboard; You can target it via CSS and add a timeout (ms) attribute/property value if you wish to do some kind of animation on copy.

clipboard-copy-code also fires a copied event just in case.

Media embedding

The markdown image syntax has been extended to support audio and video in addition to image. Media are represented inside a <figure> with an optional <figcaption> and rendered with their corresponding html tag.

![text](url "mime @size")
![text](url "mime")
![text](url "@size")
![text](url)
![](url)
  • url: The URL of the media file. Can be an image, audio, or video file.

  • text (optional): The text caption (also used as the alt text for images).

  • @size (optional): Size override in pixels as width x height.

    For advanced sizing requirements, consider using CSS instead.

  • mime (optional): The MIME type of the file (e.g., image/png, audio/ogg; codecs=opus, video/mp4).

    If the MIME type is omitted, this library will try to infer it from the file extension. If the file extension is ambiguous (e.g., .mp4, .webm, .ogg), it performs a HEAD request to fetch the Content-Type from the server.

    The "mime" attribute is mainly for audio/video containers, providing it:

    • avoid extra network request for MIME detection.
    • ensure correct codec/container handling for audio/video.

Example

![Big Buck Bunny](./mov_bbb.mp4 "video/mp4 @640x480")

Renders as:

<figure>
  <video controls preload="metadata" width="640px" height="480px">
    <source src="./mov_bbb.mp4" type="video/mp4">
  </video>
  <figcaption>Big Buck Bunny</figcaption>
</figure>

For more advanced media type (e.g., canvas, iframe, web-component) you should use the raw html "as is" within the markdown file, and if necesarry, allow the corresponding element(s) / attribute(s) in the HTML sanitizer (see below).

Example

I personally do this for my STL renderer: xan105/web-component-3DViewer (experimental).

  const md = document.querySelector("mark-down");
  md.sanitizer.allowElement({
    name: "stl-viewer",
    attributes: [
      "src",
      "gizmos",
      "pan",
      "zoom",
      "rotate",
      "inertia"
    ]
  });
  await md.render();
  
  //Conditional Import
  if (document.querySelector("stl-viewer")) {
    const { STLViewer } = await import("@xan105/3dviewer");
    customElements.define("stl-viewer", STLViewer);
    await customElements.whenDefined("stl-viewer")
  }

API

⚠️ This module is only available as an ECMAScript module (ESM) and is intended for the browser.

Named export

Markdown(): Class

This is a Web-component as such you need to define it:

import { Markdown } from "/path/to/markdown.js"
customElements.define("mark-down", Markdown);

Events

  • change()

    The source (src) attribute has changed.

  • load()

    Markdown is being loaded.

  • render()

    Markdown is being rendered.

  • success()

    Markdown was rendered without any issue.

  • failure(detail: object)

    Something went wrong, see detail:

    { error: Error }
  • intersect(detail: object)

    A heading (h1, h2, ...) has entered the top of the viewport, see detail:

    { id: string }

Attribute / Property

  • src: string

    Path/URL to the .md file to load.

  • integrity: string

    Integrity hash passed to fetch(). See Subresource Integrity for more details.

  • manual: boolean

    If set markdown will not be rendered automatically and you will have to call the render() method yourself (see below).

  • rendered: boolean (Read-only)

    Whether the markdown was succesfuly rendered or not. You can use :not([rendered]) in your CSS to style the element differently before rendering.

Property

  • sanitizer: Sanitizer (Read-only)

    Expose the HTML Sanitizer used when rendering markdown into HTML.

    The sanitizer defines what elements of the input will be allowed or removed.

    By default the sanitizer is set to the default configuration of the spec, extended to allow media embedding and code syntax highlighting among other things.

    You may find yourself in a situation where you'd like to modify or add some elements / attributes.

    Example:

    A common use case is to allow a web-component

    const md = document.querySelector("mark-down");
    md.sanitizer.allowElement({
      name: "foo-bar",
      attributes: ["baz"]
    });
    await md.render();

    πŸ“– For more details, please kindly see the HTML Sanitizer API.

  • headings: Set<object> (Read-only)

    List of all headings (h1, h2, ...) with an id and text content represented as follows:

    {
      id: string,
      level: number,
      title: string
    }

    Example:

      //<h2 id="user-content-links">Links</h2>
      { id: "user-content-links", title: "Links", level: 2 }

    The returned Set is extended with additional functions: createElement() and toHTML():

    • createElement(options?: object): HTMLElement

      Returns a HTMLElement representing the table of contents from the headings (nested list).

      Options:

      • ordered?: boolean (false)

        Whether to use ul (false) or ol (true) as HTMLElement.

      • depth?: number (6)

        How deep to list ? Headings start from 1 to 6.

    • toHTML(options?: object): string

      Same as above but returns the list as a raw HTML string, eg:

      <ul>
        <li><a href="#id">title</a></li>
        <li>
          <ul>
            <li><a href="#id">title</a></li>
            <li><a href="#id">title</a></li>
          </ul>
        </li>
      <ul/>

Methods

  • render(sanitizer?: Sanitizer | SanitizerConfig): Promise<void>

    Load and render markdown into sanitized HTML.

    βœ”οΈ Resolves when markdown has been sucesfully rendered.
    ❌ Rejects on error

    ⚠️ You only need to call this function when you are in "manual" mode:

    <mark-down src="/path/to/md" manual></mark-down>

    or

    const md = document.appendChild(new Markdown);
    md.manual = true;
    md.src = "/path/to/md";

    πŸ’‘ Invoking this method still triggers related events.

    πŸ‘·πŸ›  Sanitizer (Advanced)

    You can override the sanitizer used when rendering markdown into HTML by passing a Sanitizer or SanitizerConfig object which defines what elements of the input will be allowed or removed.

    Example

    await md.render({
      elements: ["p", "span", "pre", "code"],
      attributes: ["id", "class"]
    });

    If omitted, the sanitizer used is the one exposed by the sanitizer property of the Markdown class (see above) which is set to the default configuration of the spec, extended to allow media embedding and code syntax highlighting among other things.

    ️> ⚠️ If you just want to modify or add to the default sanitizer configuration, you should use the sanitizer property instead of overriding the whole thing.

    // Yes βœ”οΈ
    md.sanitizer.allowElement({
      name: "foo",
      attributes: ["bar"]
    });
    await md.render();
    // This adds the "foo" element to the sanitizer
    
    // No ❌
    await md.render({
      elements: [{
        name: "foo",
        attributes: ["bar"]
      }]
    });
    // This allows only the foo element

    πŸ“– For more details, please kindly see the HTML Sanitizer API.

  • estimateReadingTime(speed?: number): number

    Estimate the "time to read" of the markdown's content in minutes.
    By default speed is 265 words per minute; the average reading speed of an adult (English).

  • unsafeExtend(...extensions)

    ⚠️ Advanced: Extending the Markdown parser (unsafe).

    This component uses a scoped instance of Marked.

    Marked can be extended using custom extensions via marked.use(...) (recommended way).

    This is a shorthand to the underlying Marked instance marked.use(...) method.

    πŸ‘· This is unsafe because extensions can override renderers, tokenizers, hooks, or options which may break built-in features and/or guarantees of this component.

    ➑️ So basically if something breaks, you are on your own πŸ™ƒ.

    Example:

    Render katex code (math typesetting)

    import markedKatex from "marked-katex-extension";
    
    const md = document.querySelector("mark-down");
    md.unsafeExtend(markedKatex({
      throwOnError: false
    }));
    await md.render();

    πŸ“– For more details, please kindly see the marked documentation.

About

Web-component to render markdown into html with syntax highlighting

Topics

Resources

License

Stars

Watchers

Forks

Sponsor this project

  •