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 🙏

© 2024 – Pkg Stats / Ryan Hefner

build-md

v0.4.2

Published

Markdown builder for JS/TS

Downloads

3,342

Readme

build-md

npm CI codecov

Comprehensive Markdown builder for JavaScript/TypeScript.

Diagram

📖 Full documentation is hosted at https://matejchalk.github.io/build-md/.

⭐ Key features

  • ✍️ Its intuitive syntax makes it convenient for generating Markdown from JavaScript/TypeScript code.
  • ✅ Has comprehensive support for most commonly used Markdown elements.
  • 🗂️ Enables logical nesting of Markdown elements and uses contextual rendering to ensure output will be rendered correctly.
    • Blocks may contain inline elements or even other blocks (e.g. nested lists), inline elements may contain other inline elements, etc.
    • Each element may be rendered as HTML instead of Markdown if needed. For example, block elements in Markdown tables will automatically render using equivalent HTML tags. And if a parent element is rendered as HTML, so will all its children.
  • 🧮 Document builder enables writing conditional and iterative logic in a declarative way.
    • Falsy values from regular JavaScript expressions are ignored.
    • Special methods provided for adding multiple related elements conditionally or in a loop.
    • Even for very complex dynamic documents, there should be no need to resort to imperative logic like if/else branches or for loops. But if you prefer this coding style, then its supported in mutable mode (immutable is default).
    • 📖 See Dynamic content.
  • 🎀 Markdown output is well-formatted.
    • Automatically inserts line breaks and indentation when appropriate. Even Markdown tables are aligned to be more readable.
    • No need to run additional tools like Prettier to have nicely formatted Markdown.
  • ♻️ Is lightweight with zero dependencies, as well as being completely runtime agnostic with regards to browser vs Node, CJS vs ESM, etc.

🚀 Quickstart

Install build-md with your package manager in the usual way. E.g. to install as a dev dependency using NPM:

npm install -D build-md

Import the MarkdownDocument class, add some basic Markdown blocks and render as string:

import { MarkdownDocument } from 'build-md';

new MarkdownDocument()
  .heading(1, 'Contributing')
  .heading(2, 'Setup')
  .paragraph('Install dependencies with:')
  .code('sh', 'npm install')
  .heading(2, 'Development')
  .list([
    'npm test - run unit tests with Vitest',
    'npm run docs - generate documenation with TypeDoc',
  ])
  .toString();

To add inline formatting, import the md tagged template literal:

import { MarkdownDocument, md } from 'build-md';

new MarkdownDocument()
  // ...
  .list([
    md`${md.code('npm test')} - run unit tests with ${md.link(
      'https://vitest.dev/',
      'Vitest'
    )}`,
    md`${md.code('npm run docs')} - generate documenation with ${md.link(
      'https://typedoc.org/',
      'TypeDoc'
    )}`,
  ])
  .toString();

To see it in action, copy/paste this complete example into a docs.mjs file and run node docs.mjs to generate a CONTRIBUTING.md file:

import { MarkdownDocument, md } from 'build-md';
import { writeFile } from 'node:fs/promises';

const markdown = new MarkdownDocument()
  .heading(1, 'Contributing')
  .heading(2, 'Setup')
  .paragraph('Install dependencies with:')
  .code('sh', 'npm install')
  .heading(2, 'Development')
  .list([
    md`${md.code('npm test')} - run unit tests with ${md.link(
      'https://vitest.dev/',
      'Vitest'
    )}`,
    md`${md.code('npm run docs')} - generate documenation with ${md.link(
      'https://typedoc.org/',
      'TypeDoc'
    )}`,
  ])
  .toString();

await writeFile('CONTRIBUTING.md', markdown);

📋 List of supported Markdown elements

| Element | Usage | Example | | :----------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Bold | md.bold(text) | important text | | Italic | md.italic(text) | emphasized text | | Link | md.link(href, text?, title?) | link | | Image | md.image(src, alt) | image | | Code | md.code(text) | source_code | | Strikethrough [^1] | md.strikethrough(text) | ~~crossed out~~ | | Footnote [^1] | md.footnote(text, label?) | [^2] | | Heading | MarkdownDocument#heading(level, text)md.heading(level, text) | Title | | Paragraph | MarkdownDocument#paragraph(text)md.paragraph(text) | Some long text spanning a few sentences. | | Code block | MarkdownDocument#code(lang?, text)md.codeBlock(lang?, text) | sourceCode({  multiLine: true,  syntaxHighlighting: true }) | | Horizontal rule | MarkdownDocument#rule()md.rule() | | | Blockquote | MarkdownDocument#quote(text)md.quote(text) | interesting quote | | Unordered list | MarkdownDocument#list(items)md.list(items) | list item 1list item 2 | | Ordered list | MarkdownDocument#list('ordered', items)md.list('ordered', items) | list item 1list item 2 | | Task list [^1] | MarkdownDocument#list('task', items)md.list('task', items) | ☑ list item 1☐ list item 2 | | Table [^1] | MarkdownDocument#table(columns, rows)md.table(columns, rows) | heading 1heading 2row 1, col. 1row 1, col. 2row 2, col. 1row 2, col. 2 | | Details [^3] | MarkdownDocument#details(summary?, text)md.details(summary?, text) | expandable content |

[^1]: Not part of basic Markdown syntax, but supported by some Markdown extensions like GFM. [^2]: Footnotes render a label in place of insertion, as well as appending a block to the end of the document with the content. [^3]: Always rendered as HTML.

🥽 Diving in

🧩 Dynamic content

While the Quickstart example shows how to render static Markdown, the main purpose of a Markdown builder is to generate content dynamically. The MarkdownDocument class is designed for writing conditional or iterative logic in a simple and declarative way, without having to break out of the builder chain.

For starters, document blocks with empty content are automatically skipped. So if the expression you write for a top-level block's content evaluates to some empty value (falsy or empty array), then the block won't be appended to the document.

function createMarkdownComment(
  totalCount: number,
  passedCount: number,
  logsUrl: string | null,
  failedChecks?: string[]
): string {
  return (
    new MarkdownDocument()
      .heading(1, `🛡️ Quality gate - ${passedCount}/${totalCount}`)
      // 👇 `false` will skip quote
      .quote(passedCount === totalCount && '✅ Everything in order!')
      // 👇 `undefined` or `0` will skip heading
      .heading(2, failedChecks?.length && '❌ Failed checks')
      // 👇 `undefined` or `[]` will skip list
      .list(failedChecks?.map(md.code))
      // 👇 `""` or `null` will skip paragraph
      .paragraph(logsUrl && md.link(logsUrl, '🔗 CI logs'))
      .toString()
  );
}

🧮 Control flow methods

The conditional expressions approach outlined above is convenient for toggling individual blocks. But if your logic affects multiple blocks at once, you may reach instead for one of the provided control flow methods – $if and $foreach.

The $if method is useful for subjecting multiple blocks to a single condition. Provide a callback function which returns the MarkdownDocument instance with added blocks. This callback will only be used if the condition is true.

new MarkdownDocument()
  .heading(1, `🛡️ Quality gate - ${passedCount}/${totalCount}`)
  .quote(passedCount === totalCount && '✅ Everything in order!')
  // 👇 heading and list added if `passedCount < totalCount`, otherwise both skipped
  .$if(passedCount < totalCount, doc =>
    doc.heading(2, '❌ Failed checks').list(failedChecks?.map(md.code))
  )
  .paragraph(logsUrl && md.link(logsUrl, '🔗 CI logs'))
  .toString();

Optionally, you may provide another callback which will be used if the condition is false (think of it as the else-branch).

new MarkdownDocument()
  .heading(1, `🛡️ Quality gate - ${passedCount}/${totalCount}`)
  .$if(
    passedCount === totalCount,
    // 👇 quote added if `passedCount === totalCount` is true
    doc => doc.quote('✅ Everything in order!'),
    // 👇 heading and list added if `passedCount === totalCount` is false
    doc => doc.heading(2, '❌ Failed checks').list(failedChecks?.map(md.code))
  )
  .paragraph(logsUrl && md.link(logsUrl, '🔗 CI logs'))
  .toString();

When it comes to iterative logic, then for individual blocks like lists and tables you can use the usual array methods (.map, .filter, etc.) to make the content dynamic. But if you need to generate multiple blocks per array item, the $foreach method comes in handy.

Provide an array for the 1st argument, and a callback for the 2nd. The callback function is called for each item in the array, and is expected to add blocks to the current MarkdownDocument instance.

function createMarkdownCommentForMonorepo(
  projects: {
    name: string;
    totalCount: number;
    passedCount: number;
    logsUrl: string | null;
    failedChecks?: string[];
  }[]
): string {
  return new MarkdownDocument()
    .heading(1, `🛡️ Quality gate (${projects.length} projects)`)
    .$foreach(
      projects,
      (doc, { name, totalCount, passedCount, logsUrl, failedChecks }) =>
        doc
          .heading(2, `💼 ${name} - ${passedCount}/${totalCount}`)
          .$if(
            passedCount === totalCount,
            doc => doc.quote('✅ Everything in order!'),
            doc =>
              doc
                .heading(3, '❌ Failed checks')
                .list(failedChecks?.map(md.code))
          )
          .paragraph(logsUrl && md.link(logsUrl, '🔗 CI logs'))
    )
    .toString();
}

🧊 Immutable vs mutable

By default, instances of MarkdownDocument are immutable. Methods for appending document blocks return a new instance, leaving the original instance unaffected.

// 👇 `extendedDocument` has additional blocks, `baseDocument` unmodified
const extendedDocument = baseDocument
  .rule()
  .paragraph(md`Made with ❤️ by ${md.link(OWNER_LINK, OWNER_NAME)}`);

This is an intentional design decision to encourage building Markdown documents declaratively, instead of an imperative approach using if/else branches, for loops, etc.

However, if you prefer to write your logic imperatively, then you have the option of setting mutable: true when instantiating a document.

function createMarkdownCommentForMonorepo(
  projects: {
    name: string;
    totalCount: number;
    passedCount: number;
    logsUrl: string | null;
    failedChecks?: string[];
  }[]
): string {
  // 👇 all method calls will mutate document
  const doc = new MarkdownDocument({ mutable: true });

  // 👇 ignoring return value would have no effect in immutable mode
  doc.heading(1, `🛡️ Quality gate (${projects.length} projects)`);

  // 👇 imperative loops work because of side-effects
  for (const project of projects) {
    const { name, totalCount, passedCount, logsUrl, failedChecks } = project;

    doc.heading(2, `💼 ${name} - ${passedCount}/${totalCount}`);

    // 👇 imperative conditions work because of side-effects
    if (passedCount === totalCount) {
      doc.quote('✅ Everything in order!');
    } else {
      doc.heading(3, '❌ Failed checks').list(failedChecks?.map(md.code));
    }

    if (logsUrl) {
      doc.paragraph(md.link(logsUrl, '🔗 CI logs'));
    }
  }

  return doc.toString();
}

🪗 Composing documents

When building complex documents, extracting some sections to other functions helps keep the code more mantainable. This is where the $concat method comes in useful. It accepts one or more other documents and appends their blocks to the current document. This makes it convenient to break up pieces of builder logic into functions, as well as making sections of documents easily reusable.

function createMarkdownComment(
  totalCount: number,
  passedCount: number,
  logsUrl: string | null,
  failedChecks?: string[]
): string {
  return new MarkdownDocument()
    .$concat(
      // 👇 adds heading and quote from other document
      createMarkdownCommentSummary(totalCount, passedCount),
      // 👇 adds heading, list and paragraph from other document
      createMarkdownCommentDetails(logsUrl, failedChecks)
    )
    .toString();
}

function createMarkdownCommentSummary(
  totalCount: number,
  passedCount: number
): MarkdownDocument {
  return new MarkdownDocument()
    .heading(1, `🛡️ Quality gate - ${passedCount}/${totalCount}`)
    .quote(passedCount === totalCount && '✅ Everything in order!');
}

function createMarkdownCommentDetails(
  logsUrl: string | null,
  failedChecks?: string[]
): MarkdownDocument {
  return new MarkdownDocument()
    .heading(2, failedChecks?.length && '❌ Failed checks')
    .list(failedChecks?.map(md.code))
    .paragraph(logsUrl && md.link(logsUrl, '🔗 CI logs'));
}

📝 Inline formatting

The md tagged template literal is for composing text which includes Markdown elements. It provides an intuitive syntax for adding inline formatting, as well as embedding nested blocks within top-level document blocks. Its output is embeddable into all elements (with a few logical exceptions like code blocks), so it acts as the glue for building documents with a complex hierarchy.

It also comes in handy when you don't want to render a full document, but only need a one-line Markdown string. Just like for the MarkdownDocument class, calling .toString() returns the converted Markdown text.

md`${md.bold(severity)} severity vulnerability in ${md.code(name)}`.toString();

🤝 Contributing

  • Prerequisite is having Node.js installed.
  • Install dev dependencies with npm install.
  • Run tests with npm test or npm run test:watch (uses Vitest).
  • Generate documentation with npm run docs (uses TypeDoc).
  • Compile TypeScript sources with npm run build (uses tsup).
  • Use Conventional Commits prompts with npm run commit.