OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git / lib / breadcrumb.js

224 lines · javascript ref:
/**
 * Breadcrumb Navigation Utility
 *
 * Generates navigation path for repository file browsing
 */

/**
 * Build breadcrumb navigation data
 * @param {object} options
 * @param {string} options.username - Repository owner
 * @param {string} options.repoName - Repository name
 * @param {string} options.ref - Git ref (branch/tag/commit)
 * @param {string} options.filePath - Current file/directory path
 * @param {string} options.pathPrefix - URL path prefix (e.g., '/git')
 * @param {string} options.type - 'tree' or 'blob'
 * @returns {Array<{name: string, url: string, isCurrent: boolean}>}
 */
export function buildBreadcrumb(options) {
  const {
    username,
    repoName,
    ref = 'HEAD',
    filePath = '',
    pathPrefix = '',
    type = 'tree'
  } = options

  const parts = []

  // Repository root
  parts.push({
    name: repoName,
    url: `${pathPrefix}/${username}/${repoName}`,
    isCurrent: !filePath
  })

  if (filePath) {
    const segments = filePath.split('/').filter(s => s)
    let currentPath = ''

    segments.forEach((segment, index) => {
      currentPath += (currentPath ? '/' : '') + segment
      const isLast = index === segments.length - 1

      parts.push({
        name: segment,
        url: isLast && type === 'blob'
          ? `${pathPrefix}/${username}/${repoName}/blob/${currentPath}`
          : `${pathPrefix}/${username}/${repoName}/tree/${currentPath}`,
        isCurrent: isLast
      })
    })
  }

  return parts
}

/**
 * Render breadcrumb as HTML
 * @param {Array} parts - Breadcrumb parts from buildBreadcrumb
 * @param {object} options
 * @param {string} options.separator - Separator character
 * @param {string} options.icon - Leading icon
 * @returns {string} HTML string
 */
export function renderBreadcrumb(parts, options = {}) {
  const { separator = '/', icon = '📁' } = options

  const links = parts.map((part, index) => {
    if (part.isCurrent) {
      return `<span class="breadcrumb-current">${escapeHtml(part.name)}</span>`
    }
    return `<a href="${part.url}" class="breadcrumb-link">${escapeHtml(part.name)}</a>`
  })

  return `
    <nav class="breadcrumb">
      <span class="breadcrumb-icon">${icon}</span>
      ${links.join(`<span class="breadcrumb-separator">${separator}</span>`)}
    </nav>
  `
}

/**
 * Render breadcrumb with branch selector
 * @param {Array} parts - Breadcrumb parts
 * @param {object} options
 * @param {string} options.currentBranch - Current branch/ref
 * @param {Array<string>} options.branches - Available branches
 * @param {string} options.baseUrl - Base URL for branch switching
 * @returns {string} HTML string
 */
export function renderBreadcrumbWithBranch(parts, options = {}) {
  const {
    currentBranch = 'HEAD',
    branches = [],
    baseUrl = ''
  } = options

  const branchSelector = branches.length > 0 ? `
    <select class="branch-selector" onchange="switchBranch(this.value)">
      ${branches.map(b => `
        <option value="${b}" ${b === currentBranch ? 'selected' : ''}>${escapeHtml(b)}</option>
      `).join('')}
    </select>
  ` : `<span class="branch-name">${escapeHtml(currentBranch)}</span>`

  const links = parts.map((part, index) => {
    if (index === 0) {
      // First part is repo name - add branch selector after it
      if (part.isCurrent) {
        return `<span class="breadcrumb-current">${escapeHtml(part.name)}</span>${branchSelector}`
      }
      return `<a href="${part.url}" class="breadcrumb-link">${escapeHtml(part.name)}</a>${branchSelector}`
    }

    if (part.isCurrent) {
      return `<span class="breadcrumb-current">${escapeHtml(part.name)}</span>`
    }
    return `<a href="${part.url}" class="breadcrumb-link">${escapeHtml(part.name)}</a>`
  })

  return `
    <nav class="breadcrumb">
      <span class="breadcrumb-icon">📁</span>
      ${links.join('<span class="breadcrumb-separator">/</span>')}
    </nav>
    <script>
      function switchBranch(branch) {
        const baseUrl = '${baseUrl}';
        if (baseUrl) {
          window.location.href = baseUrl + '?branch=' + encodeURIComponent(branch);
        }
      }
    </script>
  `
}

/**
 * Get breadcrumb CSS styles
 * @returns {string} CSS string
 */
export function getBreadcrumbStyles() {
  return `
    .breadcrumb {
      display: flex;
      align-items: center;
      gap: 8px;
      font-size: 16px;
      margin-bottom: 16px;
      padding: 12px 16px;
      background: #161b22;
      border: 1px solid #30363d;
      border-radius: 6px;
      flex-wrap: wrap;
    }

    .breadcrumb-icon {
      font-size: 20px;
    }

    .breadcrumb-link {
      color: #58a6ff;
      text-decoration: none;
    }

    .breadcrumb-link:hover {
      text-decoration: underline;
    }

    .breadcrumb-separator {
      color: #8b949e;
      margin: 0 4px;
    }

    .breadcrumb-current {
      color: #c9d1d9;
      font-weight: 600;
    }

    .branch-selector {
      background: #21262d;
      color: #c9d1d9;
      border: 1px solid #30363d;
      border-radius: 6px;
      padding: 4px 8px;
      font-size: 14px;
      margin-left: 8px;
      cursor: pointer;
    }

    .branch-selector:hover {
      background: #30363d;
    }

    .branch-name {
      background: #21262d;
      color: #c9d1d9;
      border-radius: 6px;
      padding: 4px 8px;
      font-size: 14px;
      margin-left: 8px;
    }
  `
}

// Helper function
function escapeHtml(text) {
  if (!text) return ''
  return String(text)
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#039;')
}

export default {
  buildBreadcrumb,
  renderBreadcrumb,
  renderBreadcrumbWithBranch,
  getBreadcrumbStyles
}