OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git / lib / markdown.js

120 lines · javascript ref:
/**
 * Shared Markdown & Label Utilities
 *
 * Extracted from IssueManager for reuse across issue backends.
 */

/**
 * Default labels created for new repositories
 */
export const DEFAULT_LABELS = [
  { name: 'bug', color: '#d73a4a', description: 'Something isn\'t working' },
  { name: 'enhancement', color: '#a2eeef', description: 'New feature or request' },
  { name: 'documentation', color: '#0075ca', description: 'Improvements or additions to documentation' },
  { name: 'question', color: '#d876e3', description: 'Further information is requested' },
  { name: 'help-wanted', color: '#008672', description: 'Extra attention is needed' },
  { name: 'good-first-issue', color: '#7057ff', description: 'Good for newcomers' },
  { name: 'duplicate', color: '#cfd3d7', description: 'This issue already exists' },
  { name: 'wontfix', color: '#ffffff', description: 'This will not be worked on' }
]

/**
 * Get label color styles for display
 * @param {string} color - Hex color
 * @returns {Object} CSS styles { background, foreground }
 */
export function getLabelStyles (color) {
  if (!color) {
    return { background: '#ccc', foreground: '#000' }
  }

  const hex = color.replace('#', '')
  const r = parseInt(hex.substr(0, 2), 16)
  const g = parseInt(hex.substr(2, 2), 16)
  const b = parseInt(hex.substr(4, 2), 16)
  const brightness = (r * 299 + g * 587 + b * 114) / 1000

  return {
    background: color,
    foreground: brightness > 125 ? '#000000' : '#ffffff'
  }
}

/**
 * Parse markdown with image references to HTML
 * @param {string} markdown - Markdown text
 * @param {Object} images - Hash -> data URL map
 * @returns {string} HTML
 */
export function parseMarkdown (markdown, images = {}) {
  if (!markdown) return ''

  // Escape HTML
  let html = markdown
    .replace(/&/g, '&')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;')

  // Code blocks first (with language support): ```lang\ncode```
  html = html.replace(/```(\w*)\n([\s\S]*?)```/g, (match, lang, code) => {
    return `<pre><code class="language-${lang || 'plaintext'}">${code.trim()}</code></pre>`
  })

  // Inline code: `code`
  html = html.replace(/`([^`]+)`/g, '<code>$1</code>')

  // Headers (must be at start of line)
  html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>')
  html = html.replace(/^## (.+)$/gm, '<h2>$1</h2>')
  html = html.replace(/^# (.+)$/gm, '<h1>$1</h1>')

  // Bold and italic (order matters - bold+italic first)
  html = html.replace(/\*\*\*(.+?)\*\*\*/g, '<strong><em>$1</em></strong>')
  html = html.replace(/___(.+?)___/g, '<strong><em>$1</em></strong>')
  html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
  html = html.replace(/__(.+?)__/g, '<strong>$1</strong>')
  html = html.replace(/\*(.+?)\*/g, '<em>$1</em>')
  html = html.replace(/_(.+?)_/g, '<em>$1</em>')

  // Links: [text](url) - but not images
  html = html.replace(/(?<!!)\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>')

  // Images: ![alt](hash) -> <img>
  html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (match, alt, hash) => {
    const dataUrl = images[hash]
    if (dataUrl) {
      return `<img src="${dataUrl}" alt="${alt || 'Image'}" class="issue-image" />`
    }
    return match // Keep original if image not found
  })

  // Unordered lists
  html = html.replace(/^[\*\-] (.+)$/gm, '<li>$1</li>')
  html = html.replace(/(<li>.*<\/li>)/s, '<ul>$1</ul>')

  // Ordered lists
  html = html.replace(/^\d+\. (.+)$/gm, '<li>$1</li>')

  // Blockquotes
  html = html.replace(/^> (.+)$/gm, '<blockquote>$1</blockquote>')

  // Horizontal rules
  html = html.replace(/^---$/gm, '<hr>')
  html = html.replace(/^\*\*\*$/gm, '<hr>')

  // Paragraphs (double newline = new paragraph)
  html = html.replace(/\n\n/g, '</p><p>')
  html = '<p>' + html + '</p>'

  // Single line breaks
  html = html.replace(/\n/g, '<br>')

  // Clean up empty paragraphs
  html = html.replace(/<p><\/p>/g, '')
  html = html.replace(/<p>\s*<\/p>/g, '')

  return html
}