docs.thor 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. class DocsCLI < Thor
  2. include Thor::Actions
  3. def self.to_s
  4. 'Docs'
  5. end
  6. def initialize(*args)
  7. require 'docs'
  8. trap('INT') { puts; exit! } # hide backtrace on ^C
  9. super
  10. end
  11. desc 'list', 'List available documentations'
  12. def list
  13. output = Docs.all.flat_map do |doc|
  14. name = doc.to_s.demodulize.underscore
  15. if doc.versioned?
  16. doc.versions.map { |_doc| "#{name}@#{_doc.version}" }
  17. else
  18. name
  19. end
  20. end.join("\n")
  21. require 'tty-pager'
  22. TTY::Pager.new.page(output)
  23. end
  24. desc 'page <doc> [path] [--version] [--verbose] [--debug]', 'Generate a page (no indexing)'
  25. option :version, type: :string
  26. option :verbose, type: :boolean
  27. option :debug, type: :boolean
  28. def page(name, path = '')
  29. unless path.empty? || path.start_with?('/')
  30. return puts 'ERROR: [path] must be an absolute path.'
  31. end
  32. Docs.install_report :store if options[:verbose]
  33. if options[:debug]
  34. GC.disable
  35. Docs.install_report :filter, :request
  36. end
  37. if Docs.generate_page(name, options[:version], path)
  38. puts 'Done'
  39. else
  40. puts "Failed!#{' (try running with --debug for more information)' unless options[:debug]}"
  41. end
  42. rescue Docs::DocNotFound => error
  43. handle_doc_not_found_error(error)
  44. end
  45. desc 'generate <doc> [--version] [--verbose] [--debug] [--force] [--package]', 'Generate a documentation'
  46. option :version, type: :string
  47. option :verbose, type: :boolean
  48. option :debug, type: :boolean
  49. option :force, type: :boolean
  50. option :package, type: :boolean
  51. def generate(name)
  52. Docs.install_report :store if options[:verbose]
  53. Docs.install_report :scraper if options[:debug]
  54. Docs.install_report :progress_bar, :doc if $stdout.tty?
  55. unless options[:force]
  56. puts <<-TEXT.strip_heredoc
  57. Note: this command will scrape the documentation from the source.
  58. Some scrapers require a local setup. Others will send thousands of
  59. HTTP requests, potentially slowing down the source site.
  60. Please don't use it unless you are modifying the code.
  61. To download the latest tested version of a documentation, use:
  62. thor docs:download #{name}\n
  63. TEXT
  64. return unless yes? 'Proceed? (y/n)'
  65. end
  66. if Docs.generate(name, options[:version])
  67. generate_manifest
  68. if options[:package]
  69. require 'unix_utils'
  70. package_doc(Docs.find(name, options[:version]))
  71. end
  72. puts 'Done'
  73. else
  74. puts "Failed!#{' (try running with --debug for more information)' unless options[:debug]}"
  75. end
  76. rescue Docs::DocNotFound => error
  77. handle_doc_not_found_error(error)
  78. end
  79. desc 'manifest', 'Create the manifest'
  80. def manifest
  81. generate_manifest
  82. puts 'Done'
  83. end
  84. desc 'download (<doc> <doc@version>... | --default | --installed)', 'Download documentations'
  85. option :default, type: :boolean
  86. option :installed, type: :boolean
  87. option :all, type: :boolean
  88. def download(*names)
  89. require 'unix_utils'
  90. docs = if options[:default]
  91. Docs.defaults
  92. elsif options[:installed]
  93. Docs.installed
  94. elsif options[:all]
  95. Docs.all_versions
  96. else
  97. find_docs(names)
  98. end
  99. assert_docs(docs)
  100. download_docs(docs)
  101. generate_manifest
  102. puts 'Done'
  103. rescue Docs::DocNotFound => error
  104. handle_doc_not_found_error(error)
  105. end
  106. desc 'package <doc> <doc@version>...', 'Package documentations'
  107. def package(*names)
  108. require 'unix_utils'
  109. docs = find_docs(names)
  110. assert_docs(docs)
  111. docs.each(&method(:package_doc))
  112. puts 'Done'
  113. rescue Docs::DocNotFound => error
  114. handle_doc_not_found_error(error)
  115. end
  116. desc 'clean', 'Delete documentation packages'
  117. def clean
  118. File.delete(*Dir[File.join Docs.store_path, '*.tar.gz'])
  119. puts 'Done'
  120. end
  121. private
  122. def find_docs(names)
  123. names.map do |name|
  124. name, version = name.split('@')
  125. Docs.find(name, version)
  126. end
  127. end
  128. def assert_docs(docs)
  129. if docs.empty?
  130. puts 'ERROR: called with no arguments.'
  131. puts 'Run "thor list" for usage patterns.'
  132. exit
  133. end
  134. end
  135. def handle_doc_not_found_error(error)
  136. puts %(ERROR: #{error}.)
  137. puts 'Run "thor docs:list" to see the list of docs and versions.'
  138. end
  139. def download_docs(docs)
  140. # Don't allow downloaded files to be created as StringIO
  141. require 'open-uri'
  142. OpenURI::Buffer.send :remove_const, 'StringMax' if OpenURI::Buffer.const_defined?('StringMax')
  143. OpenURI::Buffer.const_set 'StringMax', 0
  144. require 'thread'
  145. length = docs.length
  146. mutex = Mutex.new
  147. i = 0
  148. (1..4).map do
  149. Thread.new do
  150. while doc = docs.shift
  151. status = begin
  152. download_doc(doc)
  153. 'OK'
  154. rescue => e
  155. "FAILED (#{e.class}: #{e.message})"
  156. end
  157. mutex.synchronize { puts "(#{i += 1}/#{length}) #{doc.name}#{ " #{doc.version}" if doc.version} #{status}" }
  158. end
  159. end
  160. end.map(&:join)
  161. end
  162. def download_doc(doc)
  163. target = File.join(Docs.store_path, "#{doc.path}.tar.gz")
  164. open "http://dl.devdocs.io/#{doc.path}.tar.gz" do |file|
  165. FileUtils.mkpath(Docs.store_path)
  166. FileUtils.mv(file, target)
  167. unpackage_doc(doc)
  168. end
  169. end
  170. def unpackage_doc(doc)
  171. path = File.join(Docs.store_path, doc.path)
  172. FileUtils.mkpath(path)
  173. tar = UnixUtils.gunzip("#{path}.tar.gz")
  174. dir = UnixUtils.untar(tar)
  175. FileUtils.rm_rf(path)
  176. FileUtils.mv(dir, path)
  177. FileUtils.rm(tar)
  178. FileUtils.rm("#{path}.tar.gz")
  179. end
  180. def package_doc(doc)
  181. path = File.join Docs.store_path, doc.path
  182. if File.exist?(path)
  183. tar = UnixUtils.tar(path)
  184. gzip = UnixUtils.gzip(tar)
  185. FileUtils.mv(gzip, "#{path}.tar.gz")
  186. FileUtils.rm(tar)
  187. else
  188. puts %(ERROR: can't find "#{doc.name}" documentation files.)
  189. end
  190. end
  191. def generate_manifest
  192. Docs.generate_manifest
  193. end
  194. end