mirror of
https://github.com/KevinMidboe/linguist.git
synced 2026-07-24 02:21:54 +00:00
57429f3194
This is pretty common (e.g., if the package contains the grammar in both XML and JSON).
307 lines
6.6 KiB
Ruby
Executable File
307 lines
6.6 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'
|
|
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}"]
|
|
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 '.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')
|
|
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']
|
|
|
|
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']
|
|
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
|