OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git / lib / syntax.js

316 lines · javascript ref:
/**
 * Syntax Highlighting Utilities
 *
 * Provides language detection and Prism.js integration
 */

/**
 * Map file extensions to Prism.js language identifiers
 */
const EXTENSION_MAP = {
  // JavaScript/TypeScript
  js: 'javascript',
  mjs: 'javascript',
  cjs: 'javascript',
  jsx: 'jsx',
  ts: 'typescript',
  tsx: 'tsx',

  // Web
  html: 'html',
  htm: 'html',
  xml: 'xml',
  svg: 'svg',
  css: 'css',
  scss: 'scss',
  sass: 'sass',
  less: 'less',

  // Data formats
  json: 'json',
  yaml: 'yaml',
  yml: 'yaml',
  toml: 'toml',
  ini: 'ini',

  // Shell/Scripts
  sh: 'bash',
  bash: 'bash',
  zsh: 'bash',
  fish: 'bash',
  ps1: 'powershell',
  bat: 'batch',
  cmd: 'batch',

  // Python
  py: 'python',
  pyw: 'python',
  pyx: 'python',

  // Ruby
  rb: 'ruby',
  erb: 'erb',
  rake: 'ruby',
  gemspec: 'ruby',

  // Go
  go: 'go',
  mod: 'go-module',

  // Rust
  rs: 'rust',

  // C/C++
  c: 'c',
  h: 'c',
  cpp: 'cpp',
  cc: 'cpp',
  cxx: 'cpp',
  hpp: 'cpp',
  hxx: 'cpp',

  // Java/Kotlin
  java: 'java',
  kt: 'kotlin',
  kts: 'kotlin',
  gradle: 'groovy',

  // C#/F#
  cs: 'csharp',
  fs: 'fsharp',

  // PHP
  php: 'php',

  // Swift/Objective-C
  swift: 'swift',
  m: 'objectivec',
  mm: 'objectivec',

  // SQL
  sql: 'sql',

  // Markdown/Docs
  md: 'markdown',
  markdown: 'markdown',
  rst: 'rest',
  tex: 'latex',

  // Config files
  dockerfile: 'docker',
  makefile: 'makefile',
  cmake: 'cmake',
  nginx: 'nginx',
  apache: 'apacheconf',

  // Other
  lua: 'lua',
  perl: 'perl',
  pl: 'perl',
  r: 'r',
  scala: 'scala',
  clj: 'clojure',
  cljs: 'clojure',
  ex: 'elixir',
  exs: 'elixir',
  erl: 'erlang',
  hrl: 'erlang',
  hs: 'haskell',
  ml: 'ocaml',
  vim: 'vim',
  diff: 'diff',
  patch: 'diff',
  gitignore: 'gitignore',
  env: 'dotenv',
  proto: 'protobuf',
  graphql: 'graphql',
  gql: 'graphql',
  wasm: 'wasm',
  asm: 'nasm',
  s: 'nasm',
  zig: 'zig',
  v: 'v',
  nim: 'nim',
  dart: 'dart',
  sol: 'solidity'
}

/**
 * Special filename mappings (no extension)
 */
const FILENAME_MAP = {
  dockerfile: 'docker',
  makefile: 'makefile',
  cmakelists: 'cmake',
  gemfile: 'ruby',
  rakefile: 'ruby',
  vagrantfile: 'ruby',
  brewfile: 'ruby',
  guardfile: 'ruby',
  podfile: 'ruby',
  fastfile: 'ruby',
  appfile: 'ruby',
  dangerfile: 'ruby',
  berksfile: 'ruby',
  thorfile: 'ruby',
  capfile: 'ruby',
  puppetfile: 'ruby',
  appraisals: 'ruby',
  procfile: 'yaml',
  '.gitignore': 'gitignore',
  '.gitattributes': 'gitignore',
  '.gitmodules': 'gitignore',
  '.editorconfig': 'ini',
  '.prettierrc': 'json',
  '.eslintrc': 'json',
  '.babelrc': 'json',
  'tsconfig.json': 'json',
  'package.json': 'json',
  'package-lock.json': 'json',
  'composer.json': 'json',
  'cargo.toml': 'toml',
  'go.mod': 'go-module',
  'go.sum': 'go-module'
}

/**
 * Detect language from file path
 * @param {string} filePath - Full file path or filename
 * @returns {string} Prism.js language identifier or 'plaintext'
 */
export function detectLanguage(filePath) {
  if (!filePath) return 'plaintext'

  // Get filename (last part of path)
  const parts = filePath.split('/')
  const filename = parts[parts.length - 1].toLowerCase()

  // Check special filenames first
  if (FILENAME_MAP[filename]) {
    return FILENAME_MAP[filename]
  }

  // Get extension
  const extParts = filename.split('.')
  if (extParts.length > 1) {
    const ext = extParts[extParts.length - 1].toLowerCase()
    if (EXTENSION_MAP[ext]) {
      return EXTENSION_MAP[ext]
    }
  }

  return 'plaintext'
}

/**
 * Get Prism.js CDN URL for a specific language
 * @param {string} language - Prism language identifier
 * @returns {string} CDN URL for the language component
 */
export function getPrismLanguageUrl(language, pathPrefix = '') {
  if (language === 'plaintext') return null
  return `${pathPrefix}/vendor/prism-components/prism-${language}.min.js`
}

/**
 * Get all required Prism.js CDN URLs (core + theme + language)
 * @param {string} language - Prism language identifier
 * @returns {object} { css: string[], js: string[] }
 */
export function getPrismAssets(language, pathPrefix = '') {
  const v = pathPrefix + '/vendor'
  const assets = {
    css: [
      v + '/prism-tomorrow.min.css',
      v + '/prism-line-numbers.min.css'
    ],
    js: [
      v + '/prism.min.js',
      v + '/prism-line-numbers.min.js',
      v + '/prism-autoloader.min.js'
    ]
  }

  // Add language-specific component if not plaintext
  const langUrl = getPrismLanguageUrl(language)
  if (langUrl) {
    assets.js.push(langUrl)
  }

  return assets
}

/**
 * Check if content is likely binary
 * @param {string} content - File content
 * @returns {boolean}
 */
export function isBinaryContent(content) {
  if (!content) return false

  // Check for null bytes (common in binary files)
  if (content.includes('\x00')) return true

  // Check for high ratio of non-printable characters
  let nonPrintable = 0
  const sample = content.slice(0, 8000) // Check first 8KB

  for (let i = 0; i < sample.length; i++) {
    const code = sample.charCodeAt(i)
    // Count characters outside printable ASCII range (excluding common whitespace)
    if (code < 32 && code !== 9 && code !== 10 && code !== 13) {
      nonPrintable++
    }
  }

  // If more than 10% non-printable, likely binary
  return (nonPrintable / sample.length) > 0.1
}

/**
 * Get file icon based on language/type
 * @param {string} language - Prism language identifier
 * @param {string} fileType - 'file', 'directory', 'submodule'
 * @returns {string} Emoji icon
 */
export function getFileIcon(language, fileType = 'file') {
  if (fileType === 'directory') return '📁'
  if (fileType === 'submodule') return '📦'

  // Language-specific icons
  const iconMap = {
    javascript: '📜',
    typescript: '📘',
    python: '🐍',
    ruby: '💎',
    go: '🐹',
    rust: '🦀',
    java: '☕',
    kotlin: '🎯',
    swift: '🍎',
    c: '⚙️',
    cpp: '⚙️',
    csharp: '🎮',
    php: '🐘',
    html: '🌐',
    css: '🎨',
    json: '📋',
    yaml: '📋',
    markdown: '📝',
    docker: '🐳',
    bash: '💻',
    sql: '🗃️',
    graphql: '◼️'
  }

  return iconMap[language] || '📄'
}

export default {
  detectLanguage,
  getPrismLanguageUrl,
  getPrismAssets,
  isBinaryContent,
  getFileIcon
}