diff --git a/assets/javascripts/vendor/prism-invoke.js b/assets/javascripts/vendor/prism-invoke.js new file mode 100644 index 00000000..9b48f616 --- /dev/null +++ b/assets/javascripts/vendor/prism-invoke.js @@ -0,0 +1,42 @@ +var HL = { + 'javascript': [ + '._coffeescript pre:last-child', + '._angular .prettyprint', + '._d3 .highlight > pre', + '._underscore pre', + '._node pre > code', + '._jquery .syntaxhighlighter .javascript', + '._ember pre .javascript', + ['._knockout pre', 'data-bind="', false], + '._mdn pre[class*="js"]' + ], + + 'c': [ '._ruby pre.c' ], + 'ruby': [ '._ruby pre.ruby' ], + 'coffeescript': [ '._coffeescript .code > pre:first-child' ], + 'python': [ '._sphinx pre.python' ], + + 'markup': [ + ['._knockout pre', 'data-bind="', true], + '._ember pre.html', + '._mdn pre[class*="html"]' + ] +}; + +function highlightAll(sels, language){ + for(var i = 0; i < sels.length; ++i){ + var sel = sels[i] instanceof Array ? sels[i] : [sels[i]]; + var nodes = document.querySelectorAll(sel[0]); + + for(var j = 0, c = nodes.length; j < c; ++j){ + if(!sel[1] || nodes[j].innerHTML.indexOf(sel[i][1]) != -1 == sel[2]){ + nodes[j].classList.add('language-' + language) + Prism.highlightElement(nodes[j]); + } + } + } +} + +for(var lang in HL) + if(HL.hasOwnProperty(lang)) + highlightAll(HL[lang], lang); diff --git a/assets/stylesheets/application.css.scss b/assets/stylesheets/application.css.scss index de05a2e0..17b70f4f 100644 --- a/assets/stylesheets/application.css.scss +++ b/assets/stylesheets/application.css.scss @@ -14,7 +14,8 @@ @import 'global/variables', 'global/icons', 'global/classes', - 'global/base'; + 'global/base', + 'global/devhelp'; @import 'components/app', 'components/header', diff --git a/assets/stylesheets/global/_devhelp.scss b/assets/stylesheets/global/_devhelp.scss new file mode 100644 index 00000000..09f44ef5 --- /dev/null +++ b/assets/stylesheets/global/_devhelp.scss @@ -0,0 +1,22 @@ +._devhelp { + width: 90%; + height: auto !important; + margin: 0 auto !important; + + border: none !important; + box-shadow: none !important; + + font-family: Arial, sans-serif; + + pre, code, samp, ._redis > .example { + font-family: "Lucida Console", "Sans Mono", "Courier New", monospace; + font-size: 1.05em; + } + + ._content { + height: auto !important; + margin-left: 0px !important; + overflow-y: visible !important; + font-size: 1.1em; + } +} diff --git a/lib/devhelp.rb b/lib/devhelp.rb new file mode 100644 index 00000000..5d43f92e --- /dev/null +++ b/lib/devhelp.rb @@ -0,0 +1,218 @@ +require 'find' +require 'fileutils' +require 'nokogiri' +require 'json' +require 'downloader' + +class DevHelp + SKIP_ASSETS = %{.json .js .gz} + DEVHELP_NS = 'http://www.devhelp.net/book' + + def initialize(options) + @options = options + end + + def book_options(doc) + { + xmlns: DEVHELP_NS, + title: doc.name, + name: "#{doc.slug}-#{doc.version}", + version: 2, + author: '', + language: doc.language, + link: 'index.html' + } + end + + def normalize_url(link) + link.gsub(/^([^.]+?)(?=$|#)/, '\1.html\2') + end + + def build_devhelp(doc, structure) + builder do |xml| + xml.book book_options(doc) do + xml.doc.create_internal_subset('book', '-//W3C//DTD HTML 4.01 Transitional//EN', '') + xml.chapters do + structure[:terms].each do |term, link| + xml.sub name: term, link: normalize_url(link) + end + end + end + end.to_xml + end + + def for_docs(*docs) + docs.flatten.each(&method(:for_doc)) + end + + def cp_r(src, dst) + t = File.dirname(dst) + FileUtils.mkdir_p(t) unless File.directory?(t) + FileUtils.cp_r(src, dst) + end + + def prepare_assets(src, dst) + cp_r(src, dst) + Find.find(dst).select do |file| + ext = File.extname(file).downcase + File.unlink(file) if File.file?(file) && SKIP_ASSETS.include?(ext) + ext == '.css' + end + end + + def prepare_js(js, dst) + js.map do |file| + npath = File.join(dst, File.basename(file)) + FileUtils.cp(file, npath) + npath + end + end + + def make_devhelp_file(doc, src, dst, unlink = false) + structure = parse_index(src) + File.write(dst, build_devhelp(doc, structure)) + File.unlink(src) + structure + end + + def downloader + @downloader ||= Downloader.new + end + + def document_structure(doc, type) + doc.internal_subset.remove + doc.create_internal_subset('html', nil, nil) + + unless doc.at_css('head') + title = Nokogiri::XML::Node.new 'head', doc + doc.root.children.first.add_previous_sibling title + end + + body = doc.at_css('body') + content = body.children.remove + + body.add_child <<-EOF +
+
+ #{content} +
+
+ EOF + end + + def set_title(doc, text = nil) + unless text + h1 = doc.at_css('h1, h2, h3, h4, h5') + return unless h1 + text = h1.text.strip + end + + title = doc.at_css('title') + unless title + title = Nokogiri::XML::Node.new 'title', doc + doc.at_css('head').add_child(title) + end + + title.content = text + end + + def inject_assets(doc, path, css, js, skip) + head = doc.at_css('head') + level = ['..'] * path[skip].count(File::SEPARATOR) + + css.each do |asset| + link = Nokogiri::XML::Node.new 'link', doc + link['rel'] = 'stylesheet' + link['media'] = 'all' + link['charset'] = 'UTF-8' + link['href'] = File.join(level + [asset[skip]]) + head.add_child(link) + end + + body = doc.at_css('body') + js.each do |asset| + script = Nokogiri::XML::Node.new 'script', doc + script['type'] = 'text/javascript' + script['charset'] = 'UTF-8' + script['src'] = File.join(level + [asset[skip]]) + body.add_child(script) + end + end + + def src_for(doc) + src_path = File.join(@options[:base_path], doc.path) + + unless File.exists?(src_path) + puts %(ERROR: can't find "#{doc.name}" documentation files. Please download/scrape it first.) + return nil + end + + src_path + end + + def dst_for(doc) + dst_path = File.join(@options[:devhelp_path], doc.path) + + if File.exists?(dst_path) + unless @options[:force] + puts %(ERROR: #{doc.name} was already converted. Use --force to overwrite.) + return nil + end + + FileUtils.rm_rf(dst_path) + end + + dst_path + end + + def for_doc(doc) + src_path = src_for(doc) || return + dst_path = dst_for(doc) || return + + cp_r(src_path, dst_path) + + css = prepare_assets(@options[:asset_path], File.join(dst_path, 'assets')) + js = prepare_js(@options[:js], File.join(dst_path, 'assets')) + + titles = make_devhelp_file(doc, + File.join(dst_path, @options[:index]), + File.join(dst_path, "#{doc.slug}.devhelp2"), + true) + + skip = (dst_path.length + 1) .. -1 + + downloader.processor do |file, parser| + document_structure(parser, doc.type) + set_title(parser, titles[:files][file[skip]]) + inject_assets(parser, file, css, js, skip) + end + + Find.find(dst_path). + select(&method(:is_document?)). + each {|d| downloader.process_page(nil, d)} + + downloader.run + end + + def is_document?(p) + !File.basename(p).starts_with?('.') && p.ends_with?('.html') && File.file?(p) + end + + def builder(&block) + Nokogiri::XML::Builder.new(encoding: 'UTF-8', &block) + end + + def parse_index(path) + structure = {terms: {}, files: {}} + + JSON.load(open(path))['entries'].each do |e, o| + structure[:terms][e['name']] = e['path'] + + unless e['path'].include?('#') + structure[:files][e['path']] = e['name'] + end + end + + structure + end +end diff --git a/lib/docs.rb b/lib/docs.rb index d6aaa20c..d27de263 100644 --- a/lib/docs.rb +++ b/lib/docs.rb @@ -24,6 +24,9 @@ module Docs mattr_accessor :store_path self.store_path = File.expand_path '../public/docs', @@root_path + mattr_accessor :devhelp_store_path + self.devhelp_store_path = File.expand_path '../public/devhelp', @@root_path + class DocNotFound < NameError; end def self.all diff --git a/lib/docs/core/doc.rb b/lib/docs/core/doc.rb index e4177e4b..ed4775cb 100644 --- a/lib/docs/core/doc.rb +++ b/lib/docs/core/doc.rb @@ -3,7 +3,7 @@ module Docs INDEX_FILENAME = 'index.json' class << self - attr_accessor :name, :slug, :type, :version, :abstract + attr_accessor :name, :slug, :type, :version, :abstract, :language def inherited(subclass) subclass.type = type diff --git a/lib/docs/core/subscriber.rb b/lib/docs/core/subscriber.rb index 5c21acb5..1f0e6d3c 100644 --- a/lib/docs/core/subscriber.rb +++ b/lib/docs/core/subscriber.rb @@ -44,7 +44,7 @@ module Docs elsif ENV['COLUMNS'] ENV['COLUMNS'].to_i else - `stty size`.scan(/\d+/).last.to_i + `tput cols`.scan(/\d+/).last.to_i end rescue @terminal_width = nil diff --git a/lib/docs/scrapers/angular.rb b/lib/docs/scrapers/angular.rb index f20a588a..6e15e49d 100644 --- a/lib/docs/scrapers/angular.rb +++ b/lib/docs/scrapers/angular.rb @@ -15,5 +15,6 @@ module Docs self.type = 'angular' self.version = '1.0.7' self.base_url = '' + self.language = 'javascript' end end diff --git a/lib/docs/scrapers/backbone.rb b/lib/docs/scrapers/backbone.rb index b5fb3e53..04ff7e7f 100644 --- a/lib/docs/scrapers/backbone.rb +++ b/lib/docs/scrapers/backbone.rb @@ -5,6 +5,7 @@ module Docs self.type = 'underscore' self.version = '1.1.0' self.base_url = 'http://backbonejs.org' + self.language = 'javascript' html_filters.push 'backbone/clean_html', 'backbone/entries', 'title' diff --git a/lib/docs/scrapers/coffeescript.rb b/lib/docs/scrapers/coffeescript.rb index ecd45040..86538c94 100644 --- a/lib/docs/scrapers/coffeescript.rb +++ b/lib/docs/scrapers/coffeescript.rb @@ -4,6 +4,7 @@ module Docs self.type = 'coffeescript' self.version = '1.6.3' self.base_url = 'http://coffeescript.org' + self.language = 'coffeescript' html_filters.push 'coffeescript/clean_html', 'coffeescript/entries', 'title' diff --git a/lib/docs/scrapers/d3.rb b/lib/docs/scrapers/d3.rb index 81e6758e..f6e1b6da 100644 --- a/lib/docs/scrapers/d3.rb +++ b/lib/docs/scrapers/d3.rb @@ -6,6 +6,7 @@ module Docs self.version = '3.4.1' self.base_url = 'https://github.com/mbostock/d3/wiki/' self.root_path = 'API-Reference' + self.language = 'javascript' html_filters.push 'd3/clean_html', 'd3/entries' diff --git a/lib/docs/scrapers/ember.rb b/lib/docs/scrapers/ember.rb index becd2c29..75abbea3 100644 --- a/lib/docs/scrapers/ember.rb +++ b/lib/docs/scrapers/ember.rb @@ -5,6 +5,7 @@ module Docs self.type = 'ember' self.version = '1.3.0' self.base_url = 'http://emberjs.com/api/' + self.language = 'javascript' html_filters.push 'ember/clean_html', 'ember/entries', 'title' diff --git a/lib/docs/scrapers/git.rb b/lib/docs/scrapers/git.rb index eb0796c1..4392e601 100644 --- a/lib/docs/scrapers/git.rb +++ b/lib/docs/scrapers/git.rb @@ -4,6 +4,7 @@ module Docs self.version = '1.8.5' self.base_url = 'http://git-scm.com/docs' self.initial_paths = %w(/git.html) + self.language = 'git' html_filters.push 'git/clean_html', 'git/entries' diff --git a/lib/docs/scrapers/http.rb b/lib/docs/scrapers/http.rb index 2f517c75..91fec1e6 100644 --- a/lib/docs/scrapers/http.rb +++ b/lib/docs/scrapers/http.rb @@ -4,6 +4,7 @@ module Docs self.type = 'rfc' self.base_url = 'http://www.w3.org/Protocols/rfc2616/' self.root_path = 'rfc2616.html' + self.language = 'http' html_filters.push 'http/clean_html', 'http/entries' diff --git a/lib/docs/scrapers/knockout.rb b/lib/docs/scrapers/knockout.rb index 941927aa..d672fdac 100644 --- a/lib/docs/scrapers/knockout.rb +++ b/lib/docs/scrapers/knockout.rb @@ -6,6 +6,7 @@ module Docs self.version = '3.0.0' self.base_url = 'http://knockoutjs.com/documentation/' self.root_path = 'introduction.html' + self.language = 'javascript' html_filters.push 'knockout/clean_html', 'knockout/entries' diff --git a/lib/docs/scrapers/less.rb b/lib/docs/scrapers/less.rb index 310b8ea9..df614b63 100644 --- a/lib/docs/scrapers/less.rb +++ b/lib/docs/scrapers/less.rb @@ -3,6 +3,7 @@ module Docs self.type = 'less' self.version = '1.6.0' self.base_url = 'http://lesscss.org' + self.language = 'less' html_filters.push 'less/clean_html', 'less/entries', 'title' diff --git a/lib/docs/scrapers/lodash.rb b/lib/docs/scrapers/lodash.rb index 4cb9d52b..231611fb 100644 --- a/lib/docs/scrapers/lodash.rb +++ b/lib/docs/scrapers/lodash.rb @@ -5,6 +5,7 @@ module Docs self.type = 'lodash' self.version = '2.4.1' self.base_url = 'http://lodash.com/docs' + self.language = 'javascript' html_filters.push 'lodash/clean_html', 'lodash/entries', 'title' diff --git a/lib/docs/scrapers/node.rb b/lib/docs/scrapers/node.rb index afd780af..eb87ee39 100644 --- a/lib/docs/scrapers/node.rb +++ b/lib/docs/scrapers/node.rb @@ -5,6 +5,7 @@ module Docs self.type = 'node' self.version = '0.10.24' self.base_url = 'http://nodejs.org/api/' + self.language = 'javascript' html_filters.push 'node/clean_html', 'node/entries', 'title' diff --git a/lib/docs/scrapers/php.rb b/lib/docs/scrapers/php.rb index ae2f0bb4..a99c2739 100644 --- a/lib/docs/scrapers/php.rb +++ b/lib/docs/scrapers/php.rb @@ -16,6 +16,7 @@ module Docs # Downloaded from php.net/download-docs.php self.dir = '/Users/Thibaut/DevDocs/Docs/PHP' + self.language = 'php' html_filters.push 'php/internal_urls', 'php/entries', 'php/clean_html', 'title' text_filters.push 'php/fix_urls' diff --git a/lib/docs/scrapers/postgresql.rb b/lib/docs/scrapers/postgresql.rb index 909036fd..dda1aa61 100644 --- a/lib/docs/scrapers/postgresql.rb +++ b/lib/docs/scrapers/postgresql.rb @@ -7,6 +7,7 @@ module Docs self.base_url = 'http://www.postgresql.org/docs/9.3/static/' self.root_path = 'reference.html' self.initial_paths = %w(sql.html runtime-config.html charset.html) + self.language = 'postgresql' html_filters.insert_before 'normalize_urls', 'postgresql/clean_nav' html_filters.push 'postgresql/clean_html', 'postgresql/entries', 'title' diff --git a/lib/docs/scrapers/python.rb b/lib/docs/scrapers/python.rb index c1cd7f97..05b50c5a 100644 --- a/lib/docs/scrapers/python.rb +++ b/lib/docs/scrapers/python.rb @@ -5,6 +5,7 @@ module Docs self.dir = '/Users/Thibaut/DevDocs/Docs/Python' # downloaded from docs.python.org/3/download.html self.base_url = 'http://docs.python.org/3/' self.root_path = 'library/index.html' + self.language = 'python' html_filters.push 'python/entries', 'python/clean_html' diff --git a/lib/docs/scrapers/redis.rb b/lib/docs/scrapers/redis.rb index 026a071a..0406149c 100644 --- a/lib/docs/scrapers/redis.rb +++ b/lib/docs/scrapers/redis.rb @@ -3,6 +3,7 @@ module Docs self.type = 'redis' self.version = 'up to 2.8.4' self.base_url = 'http://redis.io/commands' + self.language = 'redis' html_filters.push 'redis/entries', 'redis/clean_html', 'title' diff --git a/lib/docs/scrapers/sass.rb b/lib/docs/scrapers/sass.rb index d60f5b04..4f0178dd 100644 --- a/lib/docs/scrapers/sass.rb +++ b/lib/docs/scrapers/sass.rb @@ -4,6 +4,7 @@ module Docs self.version = '3.2.12' self.base_url = 'http://sass-lang.com/docs/yardoc/' self.root_path = 'file.SASS_REFERENCE.html' + self.language = 'sass' html_filters.push 'sass/clean_html', 'sass/entries', 'title' diff --git a/lib/docs/scrapers/underscore.rb b/lib/docs/scrapers/underscore.rb index 9728e117..20fcc8d8 100644 --- a/lib/docs/scrapers/underscore.rb +++ b/lib/docs/scrapers/underscore.rb @@ -5,6 +5,7 @@ module Docs self.type = 'underscore' self.version = '1.5.2' self.base_url = 'http://underscorejs.org' + self.language = 'javascript' html_filters.push 'underscore/clean_html', 'underscore/entries', 'title' diff --git a/lib/downloader.rb b/lib/downloader.rb new file mode 100644 index 00000000..ee11c294 --- /dev/null +++ b/lib/downloader.rb @@ -0,0 +1,161 @@ +require 'typhoeus' +require 'nokogiri' +require 'delegate' +require 'fileutils' +require 'cgi' + +class Downloader < SimpleDelegator + include Typhoeus + + MAX_QUEUE_SIZE = 20 + + def initialize(*args) + super(Hydra.new(*args)) + end + + def processor(&block) + @processor = block + end + + def queue_size + queued_requests.size + end + + def file(src, dst, &block) + file = nil + + request = Request.new(src) + + request.on_headers do |response| + if response.response_code == 200 + dname = File.dirname(dst) + FileUtils.mkdir_p(dname) unless File.directory?(dname) + file = open(dst, 'wb') + else + failed(src, dst, response) + end + end + + request.on_body do |chunk| + file.write(chunk) if file + end + + request.on_complete do |response| + if file + file.close + block.call(dst) if block + end + end + + queue request + dst + end + + def queue(*args, &block) + run while queue_size > MAX_QUEUE_SIZE + __getobj__(*args, &block) + end + + def page(src, target) + file(src, target) { process_page(src, target) } + end + + def process_page(src, path) + doc = Nokogiri::HTML.parse(File.read(path), 'UTF-8') + rdir = path.gsub(%r{\.[^./]*$}, '') + '_files' + skip = dirname_range(path) + + doc.css('iframe[src], img[src], script[src], link[href][rel="stylesheet"], link[href][rel="shortcut icon"]').each do |elem| + uri = url_join(src, elem['src'] || elem['href']) + + case elem.name + when 'iframe' + elem['src'] = page(uri, resource_path_for(rdir, uri, 'html'))[skip] + when 'link' + elem['href'] = file(uri, resource_path_for(rdir, uri, 'css')) do |f| + process_stylesheet_file(uri, f) if elem['rel'] == 'stylesheet' + end[skip] + when 'script' + elem['src'] = file(uri, resource_path_for(rdir, uri, 'js'))[skip] + when 'img' + elem['src'] = file(uri, resource_path_for(rdir, uri, 'png'))[skip] + end + end + + doc.css('style').each do |style| + style.content = process_stylesheet(src, style.content, rdir) + end + + @processor.call(path, doc) if @processor + + File.write(path, doc.to_html) + end + + protected + + def dirname_range(path, dirname = false) + path = File.dirname(path) unless dirname + l = path.length + l += 1 if l > 0 + l..-1 + end + + def process_stylesheet_file(src, fname) + File.write(fname, process_stylesheet(src, File.read(fname), File.dirname(fname))) + end + + def process_stylesheet(src, style, dir) + skip = dirname_range(dir, true) + + style = style.gsub(/@import\s*(?:url\s*)?(?:\()?(?:\s*)["']?([^'"\s\)]*)["']?\)?([\w\s\,^\]\(\)]*)\)?[;\n]?/) do + uri = url_join(src, $1) + fname = resource_path_for(dir, uri, 'css') + file(uri, fname) { process_stylesheet_file(uri, fname) } + %{@import url("#{fname[skip]}") #$2;\n} + end + + style = style.gsub(/(?!@import )url\s*\(["']?(.+?)["']?\)/) do + uri = url_join(src, $1) + fname = resource_path_for(dir, uri, 'png') + file(uri, fname) + %{url("#{fname[skip]}")} + end + + style + end + + def url_join(base, new) + if base && new + URI.join(base, new).to_s + else + base || new + end + end + + def resource_path_for(dir, resource, ext = nil) + rfile = CGI.unescape(resource.gsub(%r{.*/|[#?].*}, '')) + + if rfile.empty? + rfile = "downloaded#{'%04d' % @counter}" + @counter += 1 + end + + rfile << ".#{ext}" if ext && File.extname(rfile).empty? + + prefix = 1 + tfile = rfile + + puts rfile + + loop do + path = File.join(dir, tfile) + break path unless File.exists?(path) + tfile = "#{prefix}_#{rfile}" + prefix += 1 + end + end + + def failed(src, dst, response) + puts "#{src} -> #{dst} failed: #{response.status_message}" + end +end diff --git a/lib/tasks/docs.thor b/lib/tasks/docs.thor index da3eced3..8862151d 100644 --- a/lib/tasks/docs.thor +++ b/lib/tasks/docs.thor @@ -109,6 +109,9 @@ class DocsCLI < Thor desc 'verify ( ... | --all)', 'Verify documentations' option :all, type: :boolean def verify(*names) + require 'find' + require 'cgi' + docs = options[:all] ? Docs.all : find_docs(names) assert_docs(docs) docs.each(&method(:verify_doc)) @@ -117,6 +120,36 @@ class DocsCLI < Thor invalid_doc(error.name) end + desc 'devhelp-book ( ... | --all)', 'Generate DevHelp book' + option :all, type: :boolean + option :force, type: :boolean + def devhelp_book(*names) + require 'app' + require 'devhelp' + + unless File.exists?(App.assets_path) + AssetsCLI.new.invoke(:compile) + end + + docs = options[:all] ? Docs.all : find_docs(names) + assert_docs(docs) + + js = File.expand_path('../assets/javascripts/vendor', Docs.root_path) + + DevHelp.new({ + force: options[:force], + base_path: Docs.store_path, + devhelp_path: Docs.devhelp_store_path, + asset_path: App.assets_path, + index: Docs::Doc::INDEX_FILENAME, + js: [File.join(js, 'prism.js'), File.join(js, 'prism-invoke.js')] + }).for_docs(docs) + + puts 'Done' + rescue Docs::DocNotFound => error + invalid_doc(error.name) + end + desc 'clean', 'Delete documentation packages' def clean File.delete(*Dir[File.join Docs.store_path, '*.tar.gz']) @@ -215,9 +248,6 @@ class DocsCLI < Thor skip_path = (doc_path.length + 1)..-1 - require 'find' - require 'cgi' - Find.find(doc_path) do |path| next unless is_document?(path) diff --git a/public/docs/docs.json b/public/docs/docs.json index 0637a088..e32ca251 100644 --- a/public/docs/docs.json +++ b/public/docs/docs.json @@ -1 +1 @@ -[] \ No newline at end of file +[{"name":"Angular.js","slug":"angular","type":"angular","version":"1.0.7","index_path":"angular/index.json","mtime":1376320383},{"name":"Backbone.js","slug":"backbone","type":"underscore","version":"1.1.0","index_path":"backbone/index.json","mtime":1381653808},{"name":"CoffeeScript","slug":"coffeescript","type":"coffeescript","version":"1.6.3","index_path":"coffeescript/index.json","mtime":1381655883},{"name":"CSS","slug":"css","type":"mdn","version":null,"index_path":"css/index.json","mtime":1386415462},{"name":"D3.js","slug":"d3","type":"d3","version":"3.4.1","index_path":"d3/index.json","mtime":1390150423},{"name":"DOM","slug":"dom","type":"mdn","version":null,"index_path":"dom/index.json","mtime":1386432655},{"name":"DOM Events","slug":"dom_events","type":"mdn","version":null,"index_path":"dom_events/index.json","mtime":1381908561},{"name":"Ember.js","slug":"ember","type":"ember","version":"1.3.0","index_path":"ember/index.json","mtime":1389536714},{"name":"Git","slug":"git","type":"git","version":"1.8.5","index_path":"git/index.json","mtime":1386958029},{"name":"HTML","slug":"html","type":"mdn","version":null,"index_path":"html/index.json","mtime":1386413422},{"name":"HTTP","slug":"http","type":"rfc","version":null,"index_path":"http/index.json","mtime":1381675313},{"name":"JavaScript","slug":"javascript","type":"mdn","version":null,"index_path":"javascript/index.json","mtime":1386425468},{"name":"jQuery","slug":"jquery","type":"jquery","version":"up to 2.0.3","index_path":"jquery/index.json","mtime":1388954827},{"name":"jQuery Mobile","slug":"jquerymobile","type":"jquery","version":"1.4.0","index_path":"jquerymobile/index.json","mtime":1388956495},{"name":"jQuery UI","slug":"jqueryui","type":"jquery","version":"1.10.3","index_path":"jqueryui/index.json","mtime":1388955780},{"name":"Knockout.js","slug":"knockout","type":"knockout","version":"3.0.0","index_path":"knockout/index.json","mtime":1390167124},{"name":"Less","slug":"less","type":"less","version":"1.6.0","index_path":"less/index.json","mtime":1388929594},{"name":"Lo-Dash","slug":"lodash","type":"lodash","version":"2.4.1","index_path":"lodash/index.json","mtime":1386144288},{"name":"Node.js","slug":"node","type":"node","version":"0.10.24","index_path":"node/index.json","mtime":1387611542},{"name":"PHP","slug":"php","type":"php","version":"up to 5.5.8","index_path":"php/index.json","mtime":1389535846},{"name":"PostgreSQL","slug":"postgresql","type":"postgres","version":"up to 9.3.2","index_path":"postgresql/index.json","mtime":1387061550},{"name":"Python","slug":"python","type":"sphinx","version":"3.3.3","index_path":"python/index.json","mtime":1385469242},{"name":"Ruby on Rails","slug":"rails","type":"rdoc","version":"4.0.2","index_path":"rails/index.json","mtime":1386228555},{"name":"Redis","slug":"redis","type":"redis","version":"up to 2.8.4","index_path":"redis/index.json","mtime":1389660376},{"name":"Ruby","slug":"ruby","type":"rdoc","version":"2.1.0","index_path":"ruby/index.json","mtime":1388961427},{"name":"Sass","slug":"sass","type":"yard","version":"3.2.12","index_path":"sass/index.json","mtime":1381679946},{"name":"Underscore.js","slug":"underscore","type":"underscore","version":"1.5.2","index_path":"underscore/index.json","mtime":1381654139}] \ No newline at end of file