mirror of
				https://github.com/KevinMidboe/linguist.git
				synced 2025-10-29 17:50:22 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			318 lines
		
	
	
		
			7.0 KiB
		
	
	
	
		
			Ruby
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			318 lines
		
	
	
		
			7.0 KiB
		
	
	
	
		
			Ruby
		
	
	
		
			Executable File
		
	
	
	
	
| #!/usr/bin/env ruby
 | |
| 
 | |
| require 'json'
 | |
| require 'net/http'
 | |
| require 'optparse'
 | |
| require 'plist'
 | |
| require 'set'
 | |
| require 'thread'
 | |
| require 'tmpdir'
 | |
| require 'uri'
 | |
| require 'yaml'
 | |
| 
 | |
| ROOT = File.expand_path("../..", __FILE__)
 | |
| GRAMMARS_PATH = File.join(ROOT, "grammars")
 | |
| SOURCES_FILE = File.join(ROOT, "grammars.yml")
 | |
| CSONC = File.join(ROOT, "node_modules", ".bin", "csonc")
 | |
| 
 | |
| $options = {
 | |
|   :add => false,
 | |
|   :install => true,
 | |
|   :output => SOURCES_FILE,
 | |
|   :remote => true,
 | |
| }
 | |
| 
 | |
| class SingleFile
 | |
|   def initialize(path)
 | |
|     @path = path
 | |
|   end
 | |
| 
 | |
|   def url
 | |
|     @path
 | |
|   end
 | |
| 
 | |
|   def fetch(tmp_dir)
 | |
|     [@path]
 | |
|   end
 | |
| end
 | |
| 
 | |
| class DirectoryPackage
 | |
|   def self.fetch(dir)
 | |
|     Dir["#{dir}/**/*"].select do |path|
 | |
|       case File.extname(path.downcase)
 | |
|       when '.plist'
 | |
|         path.split('/')[-2] == 'Syntaxes'
 | |
|       when '.tmlanguage', '.yaml-tmlanguage'
 | |
|         true
 | |
|       when '.cson', '.json'
 | |
|         path.split('/')[-2] == 'grammars'
 | |
|       else
 | |
|         false
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   def initialize(directory)
 | |
|     @directory = directory
 | |
|   end
 | |
| 
 | |
|   def url
 | |
|     @directory
 | |
|   end
 | |
| 
 | |
|   def fetch(tmp_dir)
 | |
|     self.class.fetch(File.join(ROOT, @directory))
 | |
|   end
 | |
| end
 | |
| 
 | |
| class TarballPackage
 | |
|   def self.fetch(tmp_dir, url)
 | |
|     `curl --silent --location --max-time 30 --output "#{tmp_dir}/archive" "#{url}"`
 | |
|     raise "Failed to fetch GH package: #{url} #{$?.to_s}" unless $?.success?
 | |
| 
 | |
|     output = File.join(tmp_dir, 'extracted')
 | |
|     Dir.mkdir(output)
 | |
|     `tar -C "#{output}" -xf "#{tmp_dir}/archive"`
 | |
|     raise "Failed to uncompress tarball: #{tmp_dir}/archive (from #{url}) #{$?.to_s}" unless $?.success?
 | |
| 
 | |
|     DirectoryPackage.fetch(output)
 | |
|   end
 | |
| 
 | |
|   attr_reader :url
 | |
| 
 | |
|   def initialize(url)
 | |
|     @url = url
 | |
|   end
 | |
| 
 | |
|   def fetch(tmp_dir)
 | |
|     self.class.fetch(tmp_dir, url)
 | |
|   end
 | |
| end
 | |
| 
 | |
| class SingleGrammar
 | |
|   attr_reader :url
 | |
| 
 | |
|   def initialize(url)
 | |
|     @url = url
 | |
|   end
 | |
| 
 | |
|   def fetch(tmp_dir)
 | |
|     filename = File.join(tmp_dir, File.basename(url))
 | |
|     `curl --silent --location --max-time 10 --output "#{filename}" "#{url}"`
 | |
|     raise "Failed to fetch grammar: #{url}: #{$?.to_s}" unless $?.success?
 | |
|     [filename]
 | |
|   end
 | |
| end
 | |
| 
 | |
| class SVNPackage
 | |
|   attr_reader :url
 | |
| 
 | |
|   def initialize(url)
 | |
|     @url = url
 | |
|   end
 | |
| 
 | |
|   def fetch(tmp_dir)
 | |
|     `svn export -q "#{url}/Syntaxes" "#{tmp_dir}/Syntaxes"`
 | |
|     raise "Failed to export SVN repository: #{url}: #{$?.to_s}" unless $?.success?
 | |
|     Dir["#{tmp_dir}/Syntaxes/*.{plist,tmLanguage,tmlanguage,YAML-tmLanguage}"]
 | |
|   end
 | |
| end
 | |
| 
 | |
| class GitHubPackage
 | |
|   def self.parse_url(url)
 | |
|     url, ref = url.split("@", 2)
 | |
|     path = URI.parse(url).path.split('/')
 | |
|     [path[1], path[2].chomp('.git'), ref || "master"]
 | |
|   end
 | |
| 
 | |
|   attr_reader :user
 | |
|   attr_reader :repo
 | |
|   attr_reader :ref
 | |
| 
 | |
|   def initialize(url)
 | |
|     @user, @repo, @ref = self.class.parse_url(url)
 | |
|   end
 | |
| 
 | |
|   def url
 | |
|     suffix = "@#{ref}" unless ref == "master"
 | |
|     "https://github.com/#{user}/#{repo}#{suffix}"
 | |
|   end
 | |
| 
 | |
|   def fetch(tmp_dir)
 | |
|     url = "https://github.com/#{user}/#{repo}/archive/#{ref}.tar.gz"
 | |
|     TarballPackage.fetch(tmp_dir, url)
 | |
|   end
 | |
| end
 | |
| 
 | |
| def load_grammar(path)
 | |
|   case File.extname(path.downcase)
 | |
|   when '.plist', '.tmlanguage'
 | |
|     Plist::parse_xml(path)
 | |
|   when '.yaml-tmlanguage'
 | |
|     content = File.read(path)
 | |
|     # Attempt to parse YAML file even if it has a YAML 1.2 header
 | |
|     if content.lines[0] =~ /^%YAML[ :]1\.2/
 | |
|       content = content.lines[1..-1].join
 | |
|     end
 | |
|     begin
 | |
|       YAML.load(content)
 | |
|     rescue Psych::SyntaxError => e
 | |
|       $stderr.puts "Failed to parse YAML grammar '#{path}'"
 | |
|     end
 | |
|   when '.cson'
 | |
|     cson = `"#{CSONC}" "#{path}"`
 | |
|     raise "Failed to convert CSON grammar '#{path}': #{$?.to_s}" unless $?.success?
 | |
|     JSON.parse(cson)
 | |
|   when '.json'
 | |
|     JSON.parse(File.read(path))
 | |
|   else
 | |
|     raise "Invalid document type #{path}"
 | |
|   end
 | |
| end
 | |
| 
 | |
| def load_grammars(tmp_dir, source, all_scopes)
 | |
|   is_url = source.start_with?("http:", "https:")
 | |
|   return [] if is_url && !$options[:remote]
 | |
| 
 | |
|   p = if !is_url
 | |
|         if File.directory?(source)
 | |
|           DirectoryPackage.new(source)
 | |
|         else
 | |
|           SingleFile.new(source)
 | |
|         end
 | |
|       elsif source.end_with?('.tmLanguage', '.plist', '.YAML-tmLanguage')
 | |
|         SingleGrammar.new(source)
 | |
|       elsif source.start_with?('https://github.com')
 | |
|         GitHubPackage.new(source)
 | |
|       elsif source.start_with?('http://svn.textmate.org')
 | |
|         SVNPackage.new(source)
 | |
|       elsif source.end_with?('.tar.gz')
 | |
|         TarballPackage.new(source)
 | |
|       else
 | |
|         nil
 | |
|       end
 | |
| 
 | |
|   raise "Unsupported source: #{source}" unless p
 | |
| 
 | |
|   p.fetch(tmp_dir).map do |path|
 | |
|     grammar = load_grammar(path)
 | |
|     scope = grammar['scopeName'] || grammar['scope']
 | |
| 
 | |
|     if all_scopes.key?(scope)
 | |
|       unless all_scopes[scope] == p.url
 | |
|         $stderr.puts "WARN: Duplicated scope #{scope}\n" +
 | |
|           "  Current package: #{p.url}\n" +
 | |
|           "  Previous package: #{all_scopes[scope]}"
 | |
|       end
 | |
|       next
 | |
|     end
 | |
|     all_scopes[scope] = p.url
 | |
|     grammar
 | |
|   end.compact
 | |
| end
 | |
| 
 | |
| def install_grammars(grammars, path)
 | |
|   installed = []
 | |
| 
 | |
|   grammars.each do |grammar|
 | |
|     scope = grammar['scopeName'] || grammar['scope']
 | |
|     File.write(File.join(GRAMMARS_PATH, "#{scope}.json"), JSON.pretty_generate(grammar))
 | |
|     installed << scope
 | |
|   end
 | |
| 
 | |
|   $stderr.puts("OK #{path} (#{installed.join(', ')})")
 | |
| end
 | |
| 
 | |
| def run_thread(queue, all_scopes)
 | |
|   Dir.mktmpdir do |tmpdir|
 | |
|     loop do
 | |
|       source, index = begin
 | |
|         queue.pop(true)
 | |
|       rescue ThreadError
 | |
|         # The queue is empty.
 | |
|         break
 | |
|       end
 | |
| 
 | |
|       dir = "#{tmpdir}/#{index}"
 | |
|       Dir.mkdir(dir)
 | |
| 
 | |
|       grammars = load_grammars(dir, source, all_scopes)
 | |
|       install_grammars(grammars, source) if $options[:install]
 | |
|     end
 | |
|   end
 | |
| end
 | |
| 
 | |
| def generate_yaml(all_scopes, base)
 | |
|   yaml = all_scopes.each_with_object(base) do |(key,value),out|
 | |
|     out[value] ||= []
 | |
|     out[value] << key
 | |
|   end
 | |
| 
 | |
|   yaml = Hash[yaml.sort]
 | |
|   yaml.each { |k, v| v.sort! }
 | |
|   yaml
 | |
| end
 | |
| 
 | |
| def main(sources)
 | |
|   begin
 | |
|     Dir.mkdir(GRAMMARS_PATH)
 | |
|   rescue Errno::EEXIST
 | |
|   end
 | |
| 
 | |
|   `npm install`
 | |
| 
 | |
|   all_scopes = {}
 | |
| 
 | |
|   if source = $options[:add]
 | |
|     Dir.mktmpdir do |tmpdir|
 | |
|       grammars = load_grammars(tmpdir, source, all_scopes)
 | |
|       install_grammars(grammars, source) if $options[:install]
 | |
|     end
 | |
|     generate_yaml(all_scopes, sources)
 | |
|   else
 | |
|     queue = Queue.new
 | |
| 
 | |
|     sources.each do |url, scopes|
 | |
|       queue.push([url, queue.length])
 | |
|     end
 | |
| 
 | |
|     threads = 8.times.map do
 | |
|       Thread.new { run_thread(queue, all_scopes) }
 | |
|     end
 | |
|     threads.each(&:join)
 | |
|     generate_yaml(all_scopes, {})
 | |
|   end
 | |
| end
 | |
| 
 | |
| OptionParser.new do |opts|
 | |
|   opts.banner = "Usage: #{$0} [options]"
 | |
| 
 | |
|   opts.on("--add GRAMMAR", "Add a new grammar. GRAMMAR may be a file path or URL.") do |a|
 | |
|     $options[:add] = a
 | |
|   end
 | |
| 
 | |
|   opts.on("--[no-]install", "Install grammars into grammars/ directory.") do |i|
 | |
|     $options[:install] = i
 | |
|   end
 | |
| 
 | |
|   opts.on("--output FILE", "Write output to FILE. Use - for stdout.") do |o|
 | |
|     $options[:output] = o == "-" ? $stdout : o
 | |
|   end
 | |
| 
 | |
|   opts.on("--[no-]remote", "Download remote grammars.") do |r|
 | |
|     $options[:remote] = r
 | |
|   end
 | |
| end.parse!
 | |
| 
 | |
| sources = File.open(SOURCES_FILE) do |file|
 | |
|   YAML.load(file)
 | |
| end
 | |
| 
 | |
| yaml = main(sources)
 | |
| 
 | |
| if $options[:output].is_a?(IO)
 | |
|   $options[:output].write(YAML.dump(yaml))
 | |
| else
 | |
|   File.write($options[:output], YAML.dump(yaml))
 | |
| end
 |