Home Reference Source Test

src/lib/image-link-replacer.js

import Fs from 'fs'
import Path from 'path'
import NodeUtil from 'util'
import Request from 'request'
import Util from './util.js'

const RequestGet = NodeUtil.promisify(Request)

/**
 * Regular expression.of an image link.
 *
 * @type {RegExp}
 */
const REGEX_IMAGE_LINK = /(\[!\[[^\]]*\]\((.*?)\s*("(?:.*[^"])")?\s*\)\]\((.*?)\s*("(?:.*[^"])")?\s*\))|!\[[^\]]*\]\((.*?)\s*("(?:.*[^"])")?\s*\)/g

/**
 * Regular expression.of an image URL.
 *
 * @type {RegExp}
 */
const REGEX_IMAGE_URL = /(http)?s?:?(\/\/[^"']*?\.(?:png|jpg|jpeg|gif|png|svg))/

/**
 * Recursively enumurate a file paths from directory.
 *
 * @param {String} dir Path of root directory.
 *
 * @return {String[]} File paths.
 */
const enumFiles = (dir) => {
  let results = []
  const items = Fs.readdirSync(dir)

  items.forEach((item) => {
    const path = Path.join(dir, item)
    const stat = Fs.statSync(path)

    if (stat.isDirectory()) {
      results = results.concat(enumFiles(path))
    } else {
      results.push(path)
    }
  })

  return results
}

/**
 * Download an images.
 *
 * @param {Object} images Image URL/Name.
 * @param {String} dir Save directory.
 * @param {Logger} logger Logger.
 *
 * @return {Promise} Asynchronous task.
 */
const downloadImages = async (images, dir, logger) => {
  const results = []
  for (let image of images) {
    logger.log(`Download: "${image.url}" => "${image.fileName}"`)

    try {
      const { error, response, body } = await RequestGet({ method: 'GET', url: image.url, encoding: null })
      if (error) {
        throw error
      } else if (response && response.statusCode !== 200) {
        throw new Error(`ERROR: status code ${response.statusCode}`)
      }

      Fs.writeFileSync(Path.join(dir, image.fileName), body, 'binary')
      results.push(image)
    } catch (err) {
      logger.error(err)
    }
  }

  return results
}

/**
 * Get image link and URL list from Markdown.
 *
 * @param {String} markdown Markdown text.
 * @param {String} basename Name on which to base the saved image file name.
 *
 * @return {Object} Link and image (URL/Saved file name) list.
 */
const parseImageLink = (markdown, basename) => {
  if (!(markdown)) {
    return { links: [], images: [] }
  }

  const urls = []
  const links = markdown.match(REGEX_IMAGE_LINK)
  if (!(links && 0 < links.length)) {
    return { links: [], images: [] }
  }

  links.forEach((link) => {
    link.replace(REGEX_IMAGE_URL, (url) => {
      urls.push(url)
    })
  })

  return {
    links,
    images: urls
      .filter((url, i, arr) => arr.indexOf(url) === i)
      .map((url, i) => {
        return {
          url,
          fileName: `${basename}-${i + 1}${Path.extname(url)}`
        }
      })
  }
}

/**
 * Replace a link syntaxes.
 *
 * @param {String[]} links Link syntaxes in markdown.
 * @param {Object[]} images Image URL/Name.
 *
 * @return {Object[]} Replaced link syntaxes.
 */
const replaceLinks = (links, images) => {
  const results = []
  for (let link of links) {
    let newLink = link
    for (let image of images) {
      const regexp = new RegExp(Util.escapeRegExp(image.url), 'g')
      newLink = newLink.replace(regexp, image.fileName)
    }

    if (newLink !== link) {
      // Make it a replacement candidate for markdown if it is replaced
      results.push({ link, newLink })
    }
  }

  return results
}

/**
 * Download the linked image from Markdown and rewrite the link.
 *
 * @param {String} markdown Markdown text.
 * @param {String} dir Directory where Markdown was output.
 * @param {String} basename Name to be the base of the image file to be saved.
 * @param {Logger} logger Logger.
 *
 * @return {Promise} Asynchronous task.
 */
const ImageLinkReplacer = async (markdown, dir, basename, logger) => {
  try {
    let data = parseImageLink(markdown, basename)
    if (data.images.length === 0) {
      return markdown
    }

    const succeededImages = await downloadImages(data.images, dir, logger)
    const targets = replaceLinks(data.links, succeededImages)

    for (let target of targets) {
      markdown = markdown.replace(target.link, target.newLink)
    }
  } catch (err) {
    logger.error(err)
  }

  return markdown
}

export default ImageLinkReplacer