<commit_before>const fs = require('fs')
const path = require('path')
const grayMatter = require('gray-matter')
const fetchPluginDocs = require('./fetch-plugin-docs')
const validateFilePaths = require('@hashicorp/react-docs-sidenav/utils/validate-file-paths')
const validateRouteStructure = require('@hashicorp/react-docs-sidenav/utils/validate-route-structure')

/**
 * Resolves nav-data from file, including optional
 * resolution of remote plugin docs entries
 *
 * @param {string} navDataFile path to the nav-data.json file, relative to the cwd. Example: "data/docs-nav-data.json".
 * @param {string} localContentDir path to the content root, relative to the cwd. Example: "content/docs".
 * @param {object} options optional configuration object
 * @param {string} options.remotePluginsFile path to a remote-plugins.json file, relative to the cwd. Example: "data/docs-remote-plugins.json".
 * @returns {array} the resolved navData. This includes NavBranch nodes pulled from remote plugin repositories, as well as filePath properties on all local NavLeaf nodes, and remoteFile properties on all NavLeafRemote nodes.
 */
async function resolveNavData(navDataFile, localContentDir, options = {}) {
  const { remotePluginsFile } = options
  // Read in files
  const navDataPath = path.join(process.cwd(), navDataFile)
  const navData = JSON.parse(fs.readFileSync(navDataPath, 'utf8'))
  // Fetch remote plugin docs, if applicable
  let withPlugins = navData
  if (remotePluginsFile) {
    // Resolve plugins, this yields branches with NavLeafRemote nodes
    withPlugins = await mergeRemotePlugins(remotePluginsFile, navData)
  }
  // Resolve local filePaths for NavLeaf nodes
  const withFilePaths = await validateFilePaths(withPlugins, localContentDir)
  validateRouteStructure(withFilePaths)
  // Return the nav data with:
  // 1. Plugins merged, transformed into navData structures with NavLeafRemote nodes
  // 2. filePaths added to all local NavLeaf nodes
  return withFilePaths
}

// Given a remote plugins config file, and the full tree of docs navData which
// contains top-level branch routes that match plugin component types,
// fetch and parse all remote plugin docs, merge them into the
// broader tree of docs navData, and return the docs navData
// with the merged plugin docs
async function mergeRemotePlugins(remotePluginsFile, navData) {
  // Read in and parse the plugin configuration JSON
  const remotePluginsPath = path.join(process.cwd(), remotePluginsFile)
  const pluginEntries = JSON.parse(fs.readFileSync(remotePluginsPath, 'utf-8'))
  // Add navData for each plugin's component.
  // Note that leaf nodes include a remoteFile property object with the full MDX fileString
  const pluginEntriesWithDocs = await Promise.all(
    pluginEntries.map(resolvePluginEntryDocs)
  )
  // group navData by component type, to prepare to merge plugin docs
  // into the broader tree of navData.
  const pluginDocsByComponent = pluginEntriesWithDocs.reduce(
    (acc, pluginEntry) => {
      const { components } = pluginEntry
      Object.keys(components).forEach((type) => {
        const navData = components[type]
        if (!navData) return
        if (!acc[type]) acc[type] = []
        acc[type].push(navData[0])
      })
      return acc
    },
    {}
  )
  // merge plugin docs, by plugin component type,
  // into the corresponding top-level component NavBranch
  const navDataWithPlugins = navData.slice().map((n) => {
    // we only care about top-level NavBranch nodes
    if (!n.routes) return n
    // for each component type, check if this NavBranch
    // is the parent route for that type
    const componentTypes = Object.keys(pluginDocsByComponent)
    let typeMatch = false
    for (var i = 0; i < componentTypes.length; i++) {
      const componentType = componentTypes[i]
      const routeMatches = n.routes.filter((r) => r.path === componentType)
      if (routeMatches.length > 0) {
        typeMatch = componentType
        break
      }
    }
    // if this NavBranch does not match a component type slug,
    // then return it unmodified
    if (!typeMatch) return n
    // if there are no matching remote plugin components,
    // then return the navBranch unmodified
    const pluginsOfType = pluginDocsByComponent[typeMatch]
    if (!pluginsOfType || pluginsOfType.length == 0) return n
    // if this NavBranch is the parent route for the type,
    // then append all remote plugins of this type to the
    // NavBranch's child routes
    const routesWithPlugins = n.routes.slice().concat(pluginsOfType)
    // console.log(JSON.stringify(routesWithPlugins, null, 2))
    // Also, sort the child routes so the order is alphabetical
    routesWithPlugins.sort((a, b) => {
      // (exception: "Overview" comes first)
      if (a.title == 'Overview') return -1
      if (b.title === 'Overview') return 1
      // (exception: "Community-Supported" comes last)
      if (a.title == 'Community-Supported') return 1
      if (b.title === 'Community-Supported') return -1
      // (exception: "Custom" comes second-last)
      if (a.title == 'Custom') return 1
      if (b.title === 'Custom') return -1
      return a.title < b.title ? -1 : a.title > b.title ? 1 : 0
    })
    // return n
    return { ...n, routes: routesWithPlugins }
  })
  // return the merged navData, which now includes special NavLeaf nodes
  // for plugin docs with { filePath, fileString } remoteFile properties
  return navDataWithPlugins
}

// Fetch remote plugin docs .mdx files, and
// transform each plugin's array of .mdx files into navData.
// Organize this navData by component, add it to the plugin config entry,
// and return the modified entry.
//
// Note that navData leaf nodes have a special remoteFile property,
// which contains { filePath, fileString } data for the remote
// plugin doc .mdx file
async function resolvePluginEntryDocs(pluginConfigEntry) {
  const { title, path: slug, repo, version } = pluginConfigEntry
  const docsMdxFiles = await fetchPluginDocs({ repo, tag: version })
  // We construct a special kind of "NavLeaf" node, with a remoteFile property,
  // consisting of a { filePath, fileString, sourceUrl }, where:
  // - filePath is the path to the source file in the source repo
  // - fileString is a string representing the file source
  // - sourceUrl is a link to the original file in the source repo
  // We also add a pluginTier attribute
  const navNodes = docsMdxFiles.map((mdxFile) => {
    const { filePath, fileString } = mdxFile
    // Process into a NavLeaf, with a remoteFile attribute
    const dirs = path.dirname(filePath).split('/')
    const dirUrl = dirs.slice(2).join('/')
    const basename = path.basename(filePath)
    // build urlPath
    // note that this will be prefixed to get to our final path
    const isIndexFile = basename === 'index'
    const urlPath = isIndexFile ? dirUrl : path.join(dirUrl, basename)
    // parse title, either from frontmatter or file name
    const { data: frontmatter } = grayMatter(fileString)
    const { nav_title, sidebar_title } = frontmatter
    const title = nav_title || sidebar_title || basename
    // construct sourceUrl
    const sourceUrl = `https://github.com/${repo}/blob/${version}/${filePath}`
    // determine pluginTier
    const pluginOwner = repo.split('/')[0]
    const pluginTier = pluginOwner === 'hashicorp' ? 'official' : 'community'
    // Construct and return a NavLeafRemote node
    return {
      title,
      path: urlPath,
      remoteFile: { filePath, fileString, sourceUrl },
      pluginTier,
    }
  })
  //
  const navNodesByComponent = navNodes.reduce((acc, navLeaf) => {
    const componentType = navLeaf.remoteFile.filePath.split('/')[1]
    if (!acc[componentType]) acc[componentType] = []
    acc[componentType].push(navLeaf)
    return acc
  }, {})
  //
  const components = Object.keys(navNodesByComponent).map((type) => {
    // Plugins many not contain every component type,
    // we return null if this is the case
    const rawNavNodes = navNodesByComponent[type]
    if (!rawNavNodes) return null
    // Avoid unnecessary nesting if there's only a single doc file
    const navData = normalizeNavNodes(title, rawNavNodes)
    // Prefix paths to fit into broader docs nav-data
    const pathPrefix = path.join(type, slug)
    const withPrefixedPaths = visitNavLeaves(navData, (n) => {
      const prefixedPath = path.join(pathPrefix, n.path)<commit_msg>website: clarify purpose of sourceUrl<commit_after>const fs = require('fs')
const path = require('path')
const grayMatter = require('gray-matter')
const fetchPluginDocs = require('./fetch-plugin-docs')
const validateFilePaths = require('@hashicorp/react-docs-sidenav/utils/validate-file-paths')
const validateRouteStructure = require('@hashicorp/react-docs-sidenav/utils/validate-route-structure')

/**
 * Resolves nav-data from file, including optional
 * resolution of remote plugin docs entries
 *
 * @param {string} navDataFile path to the nav-data.json file, relative to the cwd. Example: "data/docs-nav-data.json".
 * @param {string} localContentDir path to the content root, relative to the cwd. Example: "content/docs".
 * @param {object} options optional configuration object
 * @param {string} options.remotePluginsFile path to a remote-plugins.json file, relative to the cwd. Example: "data/docs-remote-plugins.json".
 * @returns {array} the resolved navData. This includes NavBranch nodes pulled from remote plugin repositories, as well as filePath properties on all local NavLeaf nodes, and remoteFile properties on all NavLeafRemote nodes.
 */
async function resolveNavData(navDataFile, localContentDir, options = {}) {
  const { remotePluginsFile } = options
  // Read in files
  const navDataPath = path.join(process.cwd(), navDataFile)
  const navData = JSON.parse(fs.readFileSync(navDataPath, 'utf8'))
  // Fetch remote plugin docs, if applicable
  let withPlugins = navData
  if (remotePluginsFile) {
    // Resolve plugins, this yields branches with NavLeafRemote nodes
    withPlugins = await mergeRemotePlugins(remotePluginsFile, navData)
  }
  // Resolve local filePaths for NavLeaf nodes
  const withFilePaths = await validateFilePaths(withPlugins, localContentDir)
  validateRouteStructure(withFilePaths)
  // Return the nav data with:
  // 1. Plugins merged, transformed into navData structures with NavLeafRemote nodes
  // 2. filePaths added to all local NavLeaf nodes
  return withFilePaths
}

// Given a remote plugins config file, and the full tree of docs navData which
// contains top-level branch routes that match plugin component types,
// fetch and parse all remote plugin docs, merge them into the
// broader tree of docs navData, and return the docs navData
// with the merged plugin docs
async function mergeRemotePlugins(remotePluginsFile, navData) {
  // Read in and parse the plugin configuration JSON
  const remotePluginsPath = path.join(process.cwd(), remotePluginsFile)
  const pluginEntries = JSON.parse(fs.readFileSync(remotePluginsPath, 'utf-8'))
  // Add navData for each plugin's component.
  // Note that leaf nodes include a remoteFile property object with the full MDX fileString
  const pluginEntriesWithDocs = await Promise.all(
    pluginEntries.map(resolvePluginEntryDocs)
  )
  // group navData by component type, to prepare to merge plugin docs
  // into the broader tree of navData.
  const pluginDocsByComponent = pluginEntriesWithDocs.reduce(
    (acc, pluginEntry) => {
      const { components } = pluginEntry
      Object.keys(components).forEach((type) => {
        const navData = components[type]
        if (!navData) return
        if (!acc[type]) acc[type] = []
        acc[type].push(navData[0])
      })
      return acc
    },
    {}
  )
  // merge plugin docs, by plugin component type,
  // into the corresponding top-level component NavBranch
  const navDataWithPlugins = navData.slice().map((n) => {
    // we only care about top-level NavBranch nodes
    if (!n.routes) return n
    // for each component type, check if this NavBranch
    // is the parent route for that type
    const componentTypes = Object.keys(pluginDocsByComponent)
    let typeMatch = false
    for (var i = 0; i < componentTypes.length; i++) {
      const componentType = componentTypes[i]
      const routeMatches = n.routes.filter((r) => r.path === componentType)
      if (routeMatches.length > 0) {
        typeMatch = componentType
        break
      }
    }
    // if this NavBranch does not match a component type slug,
    // then return it unmodified
    if (!typeMatch) return n
    // if there are no matching remote plugin components,
    // then return the navBranch unmodified
    const pluginsOfType = pluginDocsByComponent[typeMatch]
    if (!pluginsOfType || pluginsOfType.length == 0) return n
    // if this NavBranch is the parent route for the type,
    // then append all remote plugins of this type to the
    // NavBranch's child routes
    const routesWithPlugins = n.routes.slice().concat(pluginsOfType)
    // console.log(JSON.stringify(routesWithPlugins, null, 2))
    // Also, sort the child routes so the order is alphabetical
    routesWithPlugins.sort((a, b) => {
      // (exception: "Overview" comes first)
      if (a.title == 'Overview') return -1
      if (b.title === 'Overview') return 1
      // (exception: "Community-Supported" comes last)
      if (a.title == 'Community-Supported') return 1
      if (b.title === 'Community-Supported') return -1
      // (exception: "Custom" comes second-last)
      if (a.title == 'Custom') return 1
      if (b.title === 'Custom') return -1
      return a.title < b.title ? -1 : a.title > b.title ? 1 : 0
    })
    // return n
    return { ...n, routes: routesWithPlugins }
  })
  // return the merged navData, which now includes special NavLeaf nodes
  // for plugin docs with { filePath, fileString } remoteFile properties
  return navDataWithPlugins
}

// Fetch remote plugin docs .mdx files, and
// transform each plugin's array of .mdx files into navData.
// Organize this navData by component, add it to the plugin config entry,
// and return the modified entry.
//
// Note that navData leaf nodes have a special remoteFile property,
// which contains { filePath, fileString } data for the remote
// plugin doc .mdx file
async function resolvePluginEntryDocs(pluginConfigEntry) {
  const { title, path: slug, repo, version } = pluginConfigEntry
  const docsMdxFiles = await fetchPluginDocs({ repo, tag: version })
  // We construct a special kind of "NavLeaf" node, with a remoteFile property,
  // consisting of a { filePath, fileString, sourceUrl }, where:
  // - filePath is the path to the source file in the source repo
  // - fileString is a string representing the file source
  // - sourceUrl is a link to the original file in the source repo
  // We also add a pluginTier attribute
  const navNodes = docsMdxFiles.map((mdxFile) => {
    const { filePath, fileString } = mdxFile
    // Process into a NavLeaf, with a remoteFile attribute
    const dirs = path.dirname(filePath).split('/')
    const dirUrl = dirs.slice(2).join('/')
    const basename = path.basename(filePath)
    // build urlPath
    // note that this will be prefixed to get to our final path
    const isIndexFile = basename === 'index'
    const urlPath = isIndexFile ? dirUrl : path.join(dirUrl, basename)
    // parse title, either from frontmatter or file name
    const { data: frontmatter } = grayMatter(fileString)
    const { nav_title, sidebar_title } = frontmatter
    const title = nav_title || sidebar_title || basename
    // construct sourceUrl (used for "Edit this page" link)
    const sourceUrl = `https://github.com/${repo}/blob/${version}/${filePath}`
    // determine pluginTier
    const pluginOwner = repo.split('/')[0]
    const pluginTier = pluginOwner === 'hashicorp' ? 'official' : 'community'
    // Construct and return a NavLeafRemote node
    return {
      title,
      path: urlPath,
      remoteFile: { filePath, fileString, sourceUrl },
      pluginTier,
    }
  })
  //
  const navNodesByComponent = navNodes.reduce((acc, navLeaf) => {
    const componentType = navLeaf.remoteFile.filePath.split('/')[1]
    if (!acc[componentType]) acc[componentType] = []
    acc[componentType].push(navLeaf)
    return acc
  }, {})
  //
  const components = Object.keys(navNodesByComponent).map((type) => {
    // Plugins many not contain every component type,
    // we return null if this is the case
    const rawNavNodes = navNodesByComponent[type]
    if (!rawNavNodes) return null
    // Avoid unnecessary nesting if there's only a single doc file
    const navData = normalizeNavNodes(title, rawNavNodes)
    // Prefix paths to fit into broader docs nav-data
    const pathPrefix = path.join(type, slug)
    const withPrefixedPaths = visitNavLeaves(navData, (n) => {
      const prefixedPath = path.join(pathPrefix, n.path)
{'fraction_non_alphanumeric': 0.09895185645763013, 'fraction_numerical': 0.0024871202700302007, 'mean_word_length': 3.4547612767079925, 'pattern_counts': {'":': 0, '<': 7, '<?xml version=': 0, '>': 25, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1564057', 'n_tokens_mistral': 4799, 'n_tokens_neox': 4725, 'n_words': 1703}