import 'dart:typed_data'; import 'package:re_highlight/languages/apache.dart'; import 'package:re_highlight/languages/bash.dart'; import 'package:re_highlight/languages/c.dart'; import 'package:re_highlight/languages/cmake.dart'; import 'package:re_highlight/languages/cpp.dart'; import 'package:re_highlight/languages/csharp.dart'; import 'package:re_highlight/languages/css.dart'; import 'package:re_highlight/languages/dart.dart'; import 'package:re_highlight/languages/diff.dart'; import 'package:re_highlight/languages/dockerfile.dart'; import 'package:re_highlight/languages/dos.dart'; import 'package:re_highlight/languages/go.dart'; import 'package:re_highlight/languages/gradle.dart'; import 'package:re_highlight/languages/graphql.dart'; import 'package:re_highlight/languages/ini.dart'; import 'package:re_highlight/languages/java.dart'; import 'package:re_highlight/languages/javascript.dart'; import 'package:re_highlight/languages/json.dart'; import 'package:re_highlight/languages/kotlin.dart'; import 'package:re_highlight/languages/less.dart'; import 'package:re_highlight/languages/lua.dart'; import 'package:re_highlight/languages/makefile.dart'; import 'package:re_highlight/languages/markdown.dart'; import 'package:re_highlight/languages/nginx.dart'; import 'package:re_highlight/languages/perl.dart'; import 'package:re_highlight/languages/php.dart'; import 'package:re_highlight/languages/powershell.dart'; import 'package:re_highlight/languages/properties.dart'; import 'package:re_highlight/languages/protobuf.dart'; import 'package:re_highlight/languages/python.dart'; import 'package:re_highlight/languages/ruby.dart'; import 'package:re_highlight/languages/rust.dart'; import 'package:re_highlight/languages/scss.dart'; import 'package:re_highlight/languages/sql.dart'; import 'package:re_highlight/languages/swift.dart'; import 'package:re_highlight/languages/typescript.dart'; import 'package:re_highlight/languages/xml.dart'; import 'package:re_highlight/languages/yaml.dart'; import 'package:re_highlight/re_highlight.dart' show Mode; /// A syntax the editor offers. [grammar] is the Re-Highlight grammar used /// to colour it; null means plain text (no grammar exists for it, and none /// is pretended). class Syntax { final String id, label; final String? grammar; const Syntax(this.id, this.label, [this.grammar]); bool get highlighted => grammar != null; } /// Grammars bundled from Re-Highlight. Embedded languages (CSS/JS inside /// HTML, HTML around PHP) are not coloured: the editor highlights with /// exactly one grammar. final Map grammars = { 'apache': langApache, 'bash': langBash, 'c': langC, 'cmake': langCmake, 'cpp': langCpp, 'csharp': langCsharp, 'css': langCss, 'dart': langDart, 'diff': langDiff, 'dockerfile': langDockerfile, 'dos': langDos, 'go': langGo, 'gradle': langGradle, 'graphql': langGraphql, 'ini': langIni, 'java': langJava, 'javascript': langJavascript, 'json': langJson, 'kotlin': langKotlin, 'less': langLess, 'lua': langLua, 'makefile': langMakefile, 'markdown': langMarkdown, 'nginx': langNginx, 'perl': langPerl, 'php': langPhp, 'powershell': langPowershell, 'properties': langProperties, 'protobuf': langProtobuf, 'python': langPython, 'ruby': langRuby, 'rust': langRust, 'scss': langScss, 'sql': langSql, 'swift': langSwift, 'typescript': langTypescript, 'xml': langXml, 'yaml': langYaml, }; const plainText = Syntax('text', 'Text'); /// Every syntax the user can pick by hand, by label. const syntaxes = [ plainText, Syntax('apache', 'Apache config', 'apache'), Syntax('bash', 'Shell (Bash/sh/zsh)', 'bash'), Syntax('batch', 'Batch (cmd)', 'dos'), Syntax('c', 'C', 'c'), Syntax('cpp', 'C++', 'cpp'), Syntax('csharp', 'C#', 'csharp'), Syntax('cmake', 'CMake', 'cmake'), Syntax('css', 'CSS', 'css'), Syntax('csv', 'CSV / TSV'), Syntax('dart', 'Dart', 'dart'), Syntax('diff', 'Diff / patch', 'diff'), Syntax('dockerfile', 'Dockerfile', 'dockerfile'), Syntax('env', '.env', 'properties'), Syntax('fish', 'fish'), Syntax('go', 'Go', 'go'), Syntax('gradle', 'Gradle', 'gradle'), Syntax('graphql', 'GraphQL', 'graphql'), Syntax('hcl', 'Terraform (HCL)'), Syntax('html', 'HTML', 'xml'), Syntax('ini', 'INI / config', 'ini'), Syntax('java', 'Java', 'java'), Syntax('javascript', 'JavaScript', 'javascript'), Syntax('json', 'JSON', 'json'), Syntax('jsonl', 'JSON Lines', 'json'), Syntax('kotlin', 'Kotlin', 'kotlin'), Syntax('less', 'Less', 'less'), Syntax('log', 'Log'), Syntax('lua', 'Lua', 'lua'), Syntax('makefile', 'Makefile', 'makefile'), Syntax('markdown', 'Markdown', 'markdown'), Syntax('nginx', 'NGINX', 'nginx'), Syntax('perl', 'Perl', 'perl'), Syntax('php', 'PHP', 'php'), Syntax('plist', 'Property list (XML)', 'xml'), Syntax('powershell', 'PowerShell', 'powershell'), Syntax('properties', 'Properties', 'properties'), Syntax('protobuf', 'Protocol Buffers', 'protobuf'), Syntax('python', 'Python', 'python'), Syntax('ruby', 'Ruby', 'ruby'), Syntax('rust', 'Rust', 'rust'), Syntax('sass', 'Sass (indented)'), Syntax('scss', 'SCSS', 'scss'), Syntax('sql', 'SQL', 'sql'), Syntax('sshconfig', 'SSH config'), Syntax('svg', 'SVG (XML)', 'xml'), Syntax('swift', 'Swift', 'swift'), Syntax('systemd', 'systemd unit', 'ini'), Syntax('toml', 'TOML', 'ini'), Syntax('typescript', 'TypeScript', 'typescript'), Syntax('xml', 'XML', 'xml'), Syntax('yaml', 'YAML', 'yaml'), Syntax('pem', 'PEM / key / certificate'), Syntax('unixconfig', 'Unix config'), ]; Syntax syntaxById(String id) => syntaxes.firstWhere((s) => s.id == id, orElse: () => plainText); const _byName = { 'dockerfile': 'dockerfile', 'containerfile': 'dockerfile', 'makefile': 'makefile', 'gnumakefile': 'makefile', 'cmakelists.txt': 'cmake', 'nginx.conf': 'nginx', 'httpd.conf': 'apache', 'apache2.conf': 'apache', '.htaccess': 'apache', '.htpasswd': 'unixconfig', 'sshd_config': 'sshconfig', 'ssh_config': 'sshconfig', '.bashrc': 'bash', '.bash_profile': 'bash', '.bash_aliases': 'bash', '.bash_logout': 'bash', '.zshrc': 'bash', '.zprofile': 'bash', '.profile': 'bash', '.gitignore': 'unixconfig', '.dockerignore': 'unixconfig', '.gitattributes': 'unixconfig', '.gitconfig': 'ini', '.editorconfig': 'ini', '.npmrc': 'ini', '.env': 'env', 'hosts': 'unixconfig', 'fstab': 'unixconfig', 'crontab': 'unixconfig', 'authorized_keys': 'unixconfig', 'known_hosts': 'unixconfig', }; const _byExtension = { 'html': 'html', 'htm': 'html', 'xhtml': 'html', // 'php': 'php', 'phtml': 'php', 'php5': 'php', 'css': 'css', 'scss': 'scss', 'sass': 'sass', 'less': 'less', 'js': 'javascript', 'mjs': 'javascript', 'cjs': 'javascript', 'jsx': 'javascript', 'ts': 'typescript', 'tsx': 'typescript', 'json': 'json', 'jsonc': 'json', 'jsonl': 'jsonl', 'ndjson': 'jsonl', 'ipynb': 'json', 'xml': 'xml', 'xsd': 'xml', 'xsl': 'xml', 'xslt': 'xml', 'plist': 'plist', 'svg': 'svg', 'yaml': 'yaml', 'yml': 'yaml', 'md': 'markdown', 'markdown': 'markdown', 'py': 'python', 'pyw': 'python', 'dart': 'dart', 'java': 'java', 'kt': 'kotlin', 'kts': 'kotlin', 'swift': 'swift', 'rs': 'rust', 'go': 'go', 'c': 'c', 'h': 'c', 'cpp': 'cpp', 'hpp': 'cpp', 'cc': 'cpp', 'cxx': 'cpp', 'hh': 'cpp', 'cs': 'csharp', 'rb': 'ruby', 'pl': 'perl', 'pm': 'perl', 'lua': 'lua', 'ps1': 'powershell', 'psm1': 'powershell', 'sh': 'bash', 'bash': 'bash', 'zsh': 'bash', 'fish': 'fish', 'sql': 'sql', 'graphql': 'graphql', 'gql': 'graphql', 'conf': 'ini', 'config': 'ini', 'cfg': 'ini', 'ini': 'ini', 'properties': 'properties', 'env': 'env', 'toml': 'toml', 'service': 'systemd', 'timer': 'systemd', 'socket': 'systemd', 'mount': 'systemd', 'target': 'systemd', 'path': 'systemd', 'desktop': 'systemd', 'txt': 'text', 'log': 'log', 'csv': 'csv', 'tsv': 'csv', 'diff': 'diff', 'patch': 'diff', 'pem': 'pem', 'crt': 'pem', 'cer': 'pem', 'pub': 'pem', 'csr': 'pem', 'key': 'pem', 'asc': 'pem', 'gpg': 'pem', 'tf': 'hcl', 'tfvars': 'hcl', 'gradle': 'gradle', 'proto': 'protobuf', 'bat': 'batch', 'cmd': 'batch', }; String _base(String name) { final slash = name.lastIndexOf('/'); return slash < 0 ? name : name.substring(slash + 1); } String extensionOf(String name) { final base = _base(name); final dot = base.lastIndexOf('.'); return dot <= 0 ? '' : base.substring(dot + 1).toLowerCase(); } /// Picks the syntax for a file: exact name first, then extension, then the /// shebang line, then a look at the content. Unknown files are plain text. Syntax detectSyntax(String path, {String? content}) { final base = _base(path); final lower = base.toLowerCase(); final byName = _byName[lower]; if (byName != null) return syntaxById(byName); if (lower.startsWith('dockerfile.')) return syntaxById('dockerfile'); if (lower.startsWith('.env.')) return syntaxById('env'); if (path.endsWith('.ssh/config')) return syntaxById('sshconfig'); if (lower.endsWith('.conf') && (path.contains('/nginx/') || lower.startsWith('nginx'))) { return syntaxById('nginx'); } if (lower.endsWith('.conf') && (path.contains('/apache2/') || path.contains('/httpd/'))) { return syntaxById('apache'); } final byExtension = _byExtension[extensionOf(base)]; if (byExtension != null) return syntaxById(byExtension); if (content != null) { final first = content.split('\n').first.trim(); if (first.startsWith('#!')) { for (final (word, id) in const [ ('python', 'python'), ('node', 'javascript'), ('deno', 'typescript'), ('perl', 'perl'), ('ruby', 'ruby'), ('php', 'php'), ('fish', 'fish'), ('pwsh', 'powershell'), ('bash', 'bash'), ('zsh', 'bash'), ('/sh', 'bash'), (' sh', 'bash'), ]) { if (first.contains(word)) return syntaxById(id); } } final start = content.trimLeft(); if (start.startsWith('{ 'png': FileKind.image, 'jpg': FileKind.image, 'jpeg': FileKind.image, // 'gif': FileKind.image, 'webp': FileKind.image, 'bmp': FileKind.image, 'heic': FileKind.image, 'heif': FileKind.image, 'ico': FileKind.image, 'svg': FileKind.svg, 'pdf': FileKind.pdf, 'doc': FileKind.office, 'docx': FileKind.office, 'xls': FileKind.office, 'xlsx': FileKind.office, 'ppt': FileKind.office, 'pptx': FileKind.office, 'odt': FileKind.office, 'ods': FileKind.office, 'odp': FileKind.office, 'rtf': FileKind.office, 'pages': FileKind.office, 'numbers': FileKind.office, 'key': FileKind.certificate, 'zip': FileKind.archive, 'tar': FileKind.archive, 'gz': FileKind.archive, 'tgz': FileKind.archive, 'bz2': FileKind.archive, 'tbz2': FileKind.archive, 'xz': FileKind.archive, 'txz': FileKind.archive, '7z': FileKind.archive, 'rar': FileKind.archive, 'jar': FileKind.archive, 'apk': FileKind.archive, 'ipa': FileKind.archive, 'aab': FileKind.archive, 'mp3': FileKind.audio, 'm4a': FileKind.audio, 'aac': FileKind.audio, 'wav': FileKind.audio, 'flac': FileKind.audio, 'ogg': FileKind.audio, 'opus': FileKind.audio, 'aiff': FileKind.audio, 'mp4': FileKind.video, 'mov': FileKind.video, 'm4v': FileKind.video, 'webm': FileKind.video, 'mkv': FileKind.video, 'avi': FileKind.video, '3gp': FileKind.video, 'ipynb': FileKind.notebook, 'pem': FileKind.certificate, 'crt': FileKind.certificate, 'cer': FileKind.certificate, 'der': FileKind.certificate, 'p12': FileKind.certificate, 'pfx': FileKind.certificate, 'csr': FileKind.certificate, 'pub': FileKind.certificate, 'asc': FileKind.certificate, 'gpg': FileKind.certificate, }; /// The viewer for [name]; [head] (the first bytes) separates text from /// binary for unknown extensions. FileKind fileKind(String name, {Uint8List? head}) { final kind = _kinds[extensionOf(name)]; if (kind != null) return kind; if (head != null && head.isNotEmpty) { if (_magic(head, const [0x89, 0x50, 0x4e, 0x47])) return FileKind.image; if (_magic(head, const [0xff, 0xd8, 0xff])) return FileKind.image; if (_magic(head, const [0x47, 0x49, 0x46, 0x38])) return FileKind.image; if (_magic(head, const [0x25, 0x50, 0x44, 0x46])) return FileKind.pdf; if (_magic(head, const [0x50, 0x4b, 0x03, 0x04])) return FileKind.archive; if (_magic(head, const [0x1f, 0x8b])) return FileKind.archive; } return FileKind.text; } bool _magic(Uint8List b, List m) { if (b.length < m.length) return false; for (var i = 0; i < m.length; i++) { if (b[i] != m[i]) return false; } return true; } /// MIME type for sharing, saving and system viewers. String mimeOf(String name) => switch (extensionOf(name)) { 'txt' || 'log' || 'conf' || 'cfg' || 'ini' || '' => 'text/plain', 'html' || 'htm' => 'text/html', 'css' => 'text/css', 'csv' => 'text/csv', 'js' || 'mjs' => 'text/javascript', 'json' => 'application/json', 'xml' => 'application/xml', 'yaml' || 'yml' => 'application/yaml', 'md' => 'text/markdown', 'png' => 'image/png', 'jpg' || 'jpeg' => 'image/jpeg', 'gif' => 'image/gif', 'webp' => 'image/webp', 'bmp' => 'image/bmp', 'heic' => 'image/heic', 'heif' => 'image/heif', 'svg' => 'image/svg+xml', 'pdf' => 'application/pdf', 'doc' => 'application/msword', 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'xls' => 'application/vnd.ms-excel', 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'ppt' => 'application/vnd.ms-powerpoint', 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 'odt' => 'application/vnd.oasis.opendocument.text', 'ods' => 'application/vnd.oasis.opendocument.spreadsheet', 'odp' => 'application/vnd.oasis.opendocument.presentation', 'rtf' => 'application/rtf', 'zip' => 'application/zip', 'gz' || 'tgz' => 'application/gzip', 'tar' => 'application/x-tar', 'mp3' => 'audio/mpeg', 'm4a' || 'aac' => 'audio/mp4', 'wav' => 'audio/wav', 'flac' => 'audio/flac', 'ogg' || 'opus' => 'audio/ogg', 'mp4' || 'm4v' => 'video/mp4', 'mov' => 'video/quicktime', 'webm' => 'video/webm', 'mkv' => 'video/x-matroska', _ => 'application/octet-stream', };