npm.io
0.2.2 • Published 1 week agoCLI

@davidwells/dx-args

Licence
MIT
Version
0.2.2
Deps
4
Size
28 kB
Vulns
0
Weekly
0
Stars
864

Markdown Magic npm-version

Add a little magic to your markdown!

About

Markdown magic uses comment blocks in markdown files to automatically sync or transform its contents.

  • Automatically keep markdown files up to date from local or remote code sources
  • Transform markdown content with custom transform functions
  • Render markdown with any template engine
  • Automatically generate a table of contents
  • ... etc

The comments markdown magic uses are hidden in markdown and when viewed as HTML.

This README.md is generated with markdown-magic view the raw file to see how.

Video demoExample Repo

Table of Contents

Click to expand

Install

Via npm

npm install markdown-magic --save-dev

Via binary (no Node.js required)

Download the prebuilt binary for your platform from GitHub Releases:

# macOS (Apple Silicon)
curl -fsSL https://github.com/DavidWells/markdown-magic/releases/latest/download/md-magic-darwin-arm64 -o md-magic
chmod +x md-magic

# macOS (Intel)
curl -fsSL https://github.com/DavidWells/markdown-magic/releases/latest/download/md-magic-darwin-x64 -o md-magic
chmod +x md-magic

# Linux (x64)
curl -fsSL https://github.com/DavidWells/markdown-magic/releases/latest/download/md-magic-linux-x64 -o md-magic
chmod +x md-magic

# Linux (ARM64)
curl -fsSL https://github.com/DavidWells/markdown-magic/releases/latest/download/md-magic-linux-arm64 -o md-magic
chmod +x md-magic

Usage

Use comment blocks in your markdown

Example:

<!-- docs remote url=http://url-to-raw-md-file.md -->
This content will be dynamically replaced from the remote url
<!-- /docs -->

Then run markdown-magic via it's CLI or programmatically.

Running via CLI

Run markdown --help to see all available CLI options

markdown
# or
md-magic

CLI usage example with options

md-magic --file '**/*.md' --config ./config.file.js

In NPM scripts, npm run docs would run the markdown magic and parse all the .md files in the directory.

"scripts": {
  "docs": "md-magic --file '**/*.md'"
},

If you have an md.config.js or markdown.config.js file where markdown-magic is invoked, it will automatically use that as the configuration unless otherwise specified by --config flag.

Running programmatically
const { markdownMagic } = require('../src')

/* By default all .md files in cwd will be processed */
markdownMagic().then((results) => {
  console.log('result keys', Object.keys(results))
})
import path from 'path'
import markdownMagic from 'markdown-magic'

// Process a Single File
const markdownPath = path.join(__dirname, 'README.md')
markdownMagic(markdownPath)
Running in GitHub Actions

Use the prebuilt binary to automatically update markdown files on push:

name: Update Markdown

on:
  push:
    paths:
      - '**.md'
      - 'src/**'

jobs:
  update-docs:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Download md-magic
        run: |
          curl -fsSL https://github.com/DavidWells/markdown-magic/releases/latest/download/md-magic-linux-x64 -o md-magic
          chmod +x md-magic

      - name: Run markdown-magic
        run: ./md-magic --files '**/*.md'

      - name: Commit changes
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git add -A
          git diff --staged --quiet || git commit -m "docs: update markdown"
          git push
Alternative GitHub Actions

Syntax Examples

There are various syntax options. Choose your favorite.

Basic

openWord transformName [opts]

<!-- docs transformName optionOne='hello' optionTwo='there' -->
content to be replaced
<!-- /docs -->
Curly braces

openWord {transformName} [opts]

<!-- docs {transformName} optionOne='hello' optionTwo='there' -->
content to be replaced
<!-- /docs -->
Square brackets

openWord [transformName] [opts]

<!-- docs [transformName] optionOne='hello' optionTwo='there' -->
content to be replaced
<!-- /docs -->
Parentheses

openWord (transformName) [opts]

<!-- docs (transformName) optionOne='hello' optionTwo='there' -->
content to be replaced
<!-- /docs -->
Functions

openWord transformName([opts])

<!-- docs transformName(
  foo='bar'
  baz=['qux', 'quux']
) -->
content to be replaced
<!-- /docs -->
API

Markdown Magic Instance

markdownMagic(globOrOpts, options)
Name Type Description
globOrOpts FilePathsOrGlobs or MarkdownMagicOptions Files to process or config.
options (optional) MarkdownMagicOptions Markdown magic config.

Returns

Promise<MarkdownMagicResult>

Example

markdownMagic(['**.**.md'], options).then((result) => {
  console.log(`Processing complete`, result)
})
MarkdownMagicOptions

Configuration for markdown magic

Below is the main config for markdown-magic

Name Type Description
files (optional) FilePathsOrGlobs Files to process.
transforms (optional) Array Custom commands to transform block contents, see transforms & custom transforms sections below. Default: defaultTransforms
output (optional) OutputConfig Output configuration.
syntax (optional) SyntaxType Syntax to parse. Default: md
open (optional) string Opening match word. Default: docs
close (optional) string Closing match word. If not defined will be same as opening word. Default: /docs
cwd (optional) string Current working directory. Default process.cwd(). Default: process.cwd()
outputFlatten (optional) boolean Flatten files that are output.
useGitGlob (optional) boolean Use git glob for LARGE file directories.
dryRun (optional) boolean See planned execution of matched blocks. Default: false
debug (optional) boolean See debug details. Default: false
silent (optional) boolean Silence all console output. Default: false
applyTransformsToSource (optional) boolean Apply transforms to source file. Default is true. Default: true
failOnMissingTransforms (optional) boolean Fail if transform functions are missing. Default skip blocks. Default: false
failOnMissingRemote (optional) boolean Fail if remote file is missing. Default: true
allowPrivateGithub (optional) boolean Allow REMOTE/CODE blocks to use GitHub tokens or gh auth for private repository files. Default: false
logRemoteRequests (optional) boolean Log each unique remote HTTP request as it is attempted. Default: true
remoteCache (optional) RemoteCacheOptions or boolean Remote fetch cache options. Use false or { enabled: false } to disable.
RemoteCacheOptions

Remote fetch cache options

Name Type Description
enabled (optional) boolean Enable the remote fetch cache. Default: true
directory (optional) string Cache directory. Defaults to the user's OS cache directory outside the project.
ttl (optional) number Milliseconds to reuse normal remote responses. Default is 5 minutes. Default: 300000
immutableTtl (optional) number Milliseconds to reuse GitHub files pinned to a full 40-character commit SHA. Default is 30 days.
cachePrivate (optional) boolean Persist authenticated private GitHub responses. Set false to keep private reads memory-only for the current run. Default: true
logHits (optional) boolean Log cache hits as remote requests marked with (from cache). Default: true
OutputConfig

Optional output configuration

Name Type Description
directory (optional) string Change output path of new content. Default behavior is replacing the original file.
removeComments (optional) boolean Remove comments from output. Default is false. Default: false
pathFormatter (optional) function Custom function for altering output paths.
applyTransformsToSource (optional) boolean Apply transforms to source file. Default is true. This is for when outputDir is set. Default: false
MarkdownMagicResult

Result of markdown processing

Name Type Description
errors Array Any errors encountered.
filesChanged Array<string> Modified files.
results Array md data.

Transforms

Markdown Magic comes with a couple of built-in transforms for you to use or you can extend it with your own transforms. See 'Custom Transforms' below.

> TOC

Generate table of contents from markdown file

Options:

  • firstH1 - boolean - (optional): Show first h1 of doc in table of contents. Default false
  • collapse - boolean - (optional): Collapse the table of contents in a detail accordion. Default false
  • collapseText - string - (optional): Text the toc accordion summary
  • excludeText - string - (optional): Text to exclude in the table of contents. Default Table of Contents
  • maxDepth - number - (optional): Max depth of headings. Default 4

Example:

<!-- docs TOC -->
toc will be generated here
<!-- /docs -->

Default matchWord is docs


Name Type Description
content string The current content of the comment block.
options object The options passed in from the comment declaration.
> CODE

Get code from file or URL and put in markdown

Options:

  • src: The relative path to the code to pull in, or the URL where the raw code lives
  • syntax (optional): Syntax will be inferred by fileType if not specified
  • header (optional): Will add header comment to code snippet. Useful for pointing to relative source directory or adding live doc links
  • lines (optional): a range with lines of code which will then be replaced with code from the file. The line range should be defined as: "lines=startLine-EndLine" (for example: "lines=22-44"). Please see the example below

Example:

<!-- docs CODE src="./relative/path/to/code.js" -->
This content will be dynamically replaced with code from the file
<!-- /docs -->
 <!-- docs CODE src="./relative/path/to/code.js" lines=22-44 -->
 This content will be dynamically replaced with code from the file lines 22 through 44
 <!-- /docs -->

Default matchWord is docs


Name Type Description
content string The current content of the comment block.
options object The options passed in from the comment declaration.
> FILE

Get local file contents.

Options:

  • src: The relative path to the file to pull in
  • sections: Comma-separated list or array of markdown section headings to include
  • section: Single markdown section heading to include
  • headings: Array of markdown heading levels to include, such as headings={[2,3]}
  • removeLeadingH1: Remove the first H1 from imported markdown
  • shiftHeaders: Shift imported markdown headings up or down by a number

Example:

<!-- docs FILE src=./path/to/file -->
This content will be dynamically replaced from the local file
<!-- /docs -->

Default matchWord is docs


Name Type Description
content string The current content of the comment block.
options object The options passed in from the comment declaration.
> REMOTE

Get any remote Data and put in markdown

Options:

  • url: The URL of the remote content to pull in
  • src: Alias for url
  • githubToken: Optional token value for private GitHub files. Only used when private GitHub reads are enabled.
  • useGhCli: For GitHub URLs, allow fallback to gh api when private GitHub reads are enabled. Default: true
  • preferGhCli: Try gh api before the GitHub contents API. Default: false
  • sections: Comma-separated list or array of markdown section headings to include
  • section: Single markdown section heading to include
  • headings: Array of markdown heading levels to include, such as headings={[2,3]}
  • removeLeadingH1: Remove the first H1 from imported markdown
  • shiftHeaders: Shift imported markdown headings up or down by a number

Example:

<!-- docs REMOTE url=http://url-to-raw-md-file.md -->
This content will be dynamically replaced from the remote url
<!-- /docs -->

GitHub blob and raw.githubusercontent.com file URLs are resolved with GitHub-aware fallbacks. Public files use anonymous raw GitHub content first. Private files are opt-in and can be fetched with GITHUB_ACCESS_TOKEN, GITHUB_TOKEN, githubToken, or an authenticated GitHub CLI session only when allowPrivateGithub is enabled:

<!-- docs REMOTE
  src='https://github.com/owner/private-repo/blob/main/README.md'
  removeLeadingH1
-->
Existing content is kept if failOnMissingRemote is false.
<!-- /docs -->

Enable private GitHub reads from config:

module.exports = {
  allowPrivateGithub: true
}

Or for a single CLI run:

md-magic --allow-private-github --files README.md

To use local GitHub CLI auth instead of a token, run gh auth status first. Set MARKDOWN_MAGIC_GH_CLI=0 to disable gh api fallback in CI or locked-down environments.

Remote requests are logged once per unique URL as they are attempted. Set logRemoteRequests: false in config to disable this output.

Successful remote responses are cached outside the project in the user's OS cache directory by default. Normal responses are reused for 5 minutes. GitHub files pinned to a full 40-character commit SHA use a longer immutable cache TTL. Set remoteCache: false or remoteCache: { enabled: false } to disable the cache, or pass --no-cache / --no-remote-cache for a single CLI run. Set remoteCache.cachePrivate: false to avoid persisting authenticated private GitHub responses to disk. Cache hits are logged as Getting remote (from cache): unless remoteCache.logHits or logRemoteRequests is disabled.

Default matchWord is docs


Name Type Description
content string The current content of the comment block.
options object The options passed in from the comment declaration.
> fileTree

Generate a file tree table of contents

Options:

  • src (optional): The directory path to generate the file tree for. Default . (current directory)
  • maxDepth (optional): Maximum depth to traverse in the directory tree. Default 3
  • includeFiles (optional): Whether to include files in the tree or just directories. Default true
  • exclude (optional): Array of glob patterns to exclude from the tree. Default []
  • showSize (optional): Whether to show file sizes. Default false
  • format (optional): Output format: "tree" or "list". Default "tree"

Example:

<!-- docs fileTree src="./src" maxDepth=2 -->
file tree will be generated here
<!-- /docs -->

Example Output (tree format):

└── src/
    ├── transforms/
    │   ├── code/
    │   │   ...
    │   ├── fileTree.js
    │   ├── index.js
    │   └── toc.js
    ├── utils/
    │   ├── fs.js
    │   ├── logs.js
    │   └── text.js
    └── index.js

Example Output (list format):

- **src/**
  - **transforms/**
    - **code/**
      - ...
    - fileTree.js
    - index.js
    - toc.js
  - **utils/**
    - fs.js
    - logs.js
    - text.js
  - index.js

Example with file sizes:

└── src/
    ├── index.js (15.2 KB)
    └── package.json (552 B)

Default matchWord is docs


Name Type Description
content string The current content of the comment block.
options object The options passed in from the comment declaration.
> install

Generate installation instructions in a markdown table format

Options:

  • packageName (optional): The name of the package to install. If not provided, will try to read from package.json
  • isDev (optional): Whether to install the package as a dev dependency. Default false
  • header (optional): The header to use for the installation instructions. Default # Installation
  • body (optional): The body to use for the installation instructions. Default Install the \${packageName}` cli using your favorite package manager.`

Example:

<!-- docs install -->
Installation instructions will be generated here
<!-- /docs -->

Default matchWord is docs


Name Type Description
content string The current content of the comment block.
options object The options passed in from the comment declaration.

Inline transforms

Any transform, including custom transforms can be used inline as well to insert content into paragraphs and other places.

The face symbol ⊂◉‿◉つ is auto generated inline.

Example:

<!-- docs (FILE:src=./path/to/file) -->xyz<!-- /docs -->

Legacy v1 & v2 plugins

These plugins work with older versions of markdown-magic. Adapting them to the newer plugin syntax should be pretty straight forward.

Adding Custom Transforms

Markdown Magic is extendable via plugins.

Plugins allow developers to add new transforms to the config.transforms object. This allows for things like using different rendering engines, custom formatting, or any other logic you might want.

Plugins run in order of registration.

The below code is used to generate this markdown file via the plugin system.

const path = require('path')
const { readFileSync } = require('fs')
const { parseComments } = require('doxxx')
const { markdownMagic } = require('../src')
const { deepLog } = require('../src/utils/logs')

const config = {
  matchWord: 'MD-MAGIC-EXAMPLE', // default matchWord is AUTO-GENERATED-CONTENT
  transforms: {
    /* Match <!-- AUTO-GENERATED-CONTENT:START (customTransform:optionOne=hi&optionOne=DUDE) --> */
    customTransform({ content, options }) {
      console.log('original content in comment block', content)
      console.log('options defined on transform', options)
      // options = { optionOne: hi, optionOne: DUDE}
      return `This will replace all the contents of inside the comment ${options.optionOne}`
    },
    /* Match <!-- AUTO-GENERATED-CONTENT:START JSDocs path="../file.js" --> */
    JSDocs(markdownMagicPluginAPI) {
      const { options } = markdownMagicPluginAPI
      const fileContents = readFileSync(options.path, 'utf8')
      const docBlocs = parseComments(fileContents, { skipSingleStar: true })
        .filter((item) => {
          return !item.isIgnored
        })
        /* Remove empty comments with no tags */
        .filter((item) => {
          return item.tags.length
        })
        /* Remove inline type defs */
        .filter((item) => {
          return item.description.text !== ''
        })
        /* Sort types to end */
        .sort((a, b) => {
          if (a.type && !b.type) return 1
          if (!a.type && b.type) return -1
          return 0
        })

      docBlocs.forEach((data) => {
        // console.log('data', data)
        delete data.code
      })
      // console.log('docBlocs', docBlocs)

      if (docBlocs.length === 0) {
        throw new Error('No docBlocs found')
      }

      // console.log(docBlocs.length)
      let updatedContent = ''
      docBlocs.forEach((data) => {
        if (data.type) {
          updatedContent += `#### \`${data.type}\`\n\n`
        }

        updatedContent += `${data.description.text}\n`

        if (data.tags.length) {
         let table =  '| Name | Type | Description |\n'
          table += '|:---------------------------|:---------------:|:-----------|\n'
          data.tags.filter((tag) => {
            if (tag.tagType === 'param') return true
            if (tag.tagType === 'property') return true
            return false
          }).forEach((tag) => {
            const optionalText = tag.isOptional ? ' (optional) ' : ' '
            const defaultValueText = (typeof tag.defaultValue !== 'undefined') ? ` Default: \`${tag.defaultValue}\` ` : ' '
            table += `| \`${tag.name}\`${optionalText}`
            table += `| \`${tag.type.replace('|', 'or')}\` `
            table += `| ${tag.description.replace(/\.\s?$/, '')}.${defaultValueText}|\n`
          })
          updatedContent+= `\n${table}\n`

          const returnValues = data.tags.filter((tag) => tag.tagType === 'returns')
          if (returnValues.length) {
            returnValues.forEach((returnValue) => {
              updatedContent += `**Returns**\n\n`
              updatedContent += `\`${returnValue.type}\`\n\n`
            })
          }

          const examples = data.tags.filter((tag) => tag.tagType === 'example')
          if (examples.length) {
            examples.forEach((example) => {
              updatedContent += `**Example**\n\n`
              updatedContent += `\`\`\`js\n${example.tagValue}\n\`\`\`\n\n`
            })
          }
        }
      })
      return updatedContent.replace(/^\s+|\s+$/g, '')
    },
    INLINE_EXAMPLE: () => {
      return '**⊂◉‿◉つ**'
    },
    lolz() {
      return `This section was generated by the cli config md.config.js file`
    },
    /* Match <!-- AUTO-GENERATED-CONTENT:START (pluginExample) --> */
    pluginExample: require('./plugin-example')({ addNewLine: true }),
    /* Include plugins from NPM */
    // count: require('markdown-magic-wordcount'),
    // github: require('markdown-magic-github-contributors')
  }
}

const markdownPath = path.join(__dirname, '..', 'README.md')
markdownMagic(markdownPath, config, () => {
  console.log('Docs ready')
})

Plugin Example

Plugins must return a transform function with the following signature.

return function myCustomTransform (content, options)
/* Custom Transform Plugin example */
module.exports = function customPlugin(pluginOptions) {
  // set plugin defaults
  const defaultOptions = {
    addNewLine: false
  }
  const userOptions = pluginOptions || {}
  const pluginConfig = Object.assign(defaultOptions, userOptions)
  // return the transform function
  return function myCustomTransform ({ content, options }) {
    const newLine = (pluginConfig.addNewLine) ? '\n' : ''
    const updatedContent = content + newLine
    return updatedContent
  }
}

View the raw file file and run npm run docs to see this plugin run

This content is altered by the pluginExample plugin registered in examples/generate-readme.js

Other usage examples

Custom Transform Demo

View the raw source of this README.md file to see the comment block and see how the customTransform function in examples/generate-readme.js works

This will replace all the contents of inside the comment DUDE

Usage examples

Misc Markdown helpers

Prior Art

This was inspired by Kent C Dodds and jfmengels's all contributors cli project.

License

MIT DavidWells

Keywords