├── public ├── favicon.ico ├── css │ ├── application.css │ ├── twbs.sass │ ├── custom.sass │ └── lib │ │ └── highlight.css ├── js │ ├── application.js │ ├── custom.js │ └── lib │ │ ├── jquery.lazyload.min.js │ │ ├── jquery.pjax.js │ │ └── bootstrap.min.js └── img │ ├── file.svg │ ├── folder.svg │ ├── minus-square.svg │ ├── mail-forward.svg │ ├── plus-square.svg │ ├── exclamation-circle.svg │ ├── asterisk.svg │ ├── download.svg │ ├── hdd-o.svg │ ├── edit.svg │ └── loader.svg ├── Gemfile ├── lib ├── ginatra │ ├── version.rb │ ├── errors.rb │ ├── logger.rb │ ├── config.rb │ ├── repo_stats.rb │ ├── repo_list.rb │ ├── repo.rb │ └── helpers.rb ├── git │ ├── webby │ │ ├── extensions.rb │ │ └── http_backend.rb │ └── webby.rb ├── sinatra │ └── partials.rb └── ginatra.rb ├── views ├── _footer.erb ├── empty_repo.erb ├── 404.erb ├── 500.erb ├── index.erb ├── blob.erb ├── tree.erb ├── layout.erb ├── atom.erb ├── _header.erb ├── _tree_nav.erb ├── stats.erb ├── log.erb └── commit.erb ├── .travis.yml ├── spec ├── ginatra │ ├── logger_spec.rb │ ├── repo_stats_spec.rb │ ├── repo_list_spec.rb │ ├── repo_spec.rb │ └── helpers_spec.rb ├── spec_helper.rb └── ginatra_spec.rb ├── .gitignore ├── config.yml ├── Rakefile ├── repos └── README.md ├── CHANGELOG.md ├── CONTRIBUTING.md ├── config.ru ├── LICENSE.txt ├── ginatra.gemspec ├── Gemfile.lock ├── bin └── ginatra └── README.md /public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /lib/ginatra/version.rb: -------------------------------------------------------------------------------- 1 | module Ginatra 2 | VERSION = "4.1.0" 3 | RELEASE_NAME = "Baku" 4 | end 5 | -------------------------------------------------------------------------------- /public/css/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | *= require twbs 3 | *= require ./lib/highlight 4 | *= require custom 5 | */ 6 | -------------------------------------------------------------------------------- /views/_footer.erb: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /public/js/application.js: -------------------------------------------------------------------------------- 1 | //= require ./lib/jquery.min 2 | //= require ./lib/bootstrap.min 3 | //= require ./lib/jquery.pjax 4 | //= require ./lib/jquery.lazyload.min 5 | //= require custom 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | rvm: 2 | - 1.9.3 3 | - 2.0 4 | - 2.1 5 | - 2.2 6 | notifications: 7 | email: 8 | on_success: never # default: change 9 | on_failure: change # default: always 10 | -------------------------------------------------------------------------------- /views/empty_repo.erb: -------------------------------------------------------------------------------- 1 | <% title @repo.name, headline: 'Empty repository' %> 2 | <%= partial :header, locals: { repo: @repo } %> 3 | 4 |
<%= file_icon @blob[:filemode] %> <%= @blob[:name] %>
10 | 11 |Binary file
23 | 24 | <% end %> 25 | <% else %> 26 |<%= repo.description.split("\n").first %>
27 | 28 | <% if Ginatra.config.git_clone_enabled? %> 29 |git clone <%= url repo.param %>
31 | Created at: <%= time_tag stats.created_at %>
56 || Author | 41 |Commits | 42 ||
|---|---|---|
| 48 | <%= gravatar_image_tag contributor.first, size: 20, alt: contributor.last[:author], lazy: true %> 49 | <%= secure_mail contributor.first %> 50 | | 51 |<%= contributor.last[:author] %> | 52 |<%= contributor.last[:commits_count] %> | 53 |
| Author | 23 |Commit message | 24 |Date | 25 |
|---|---|---|
| <%= gravatar_image_tag commit.author[:email], class: 'img-rounded', alt: commit.author[:name], lazy: true %> <%= commit.author[:name] %> | 32 |<%= truncate(h(commit.message), { length: 140, separator: ' ' }) %> | 33 |<%= time_tag(commit.committer[:time]) %> | 34 |
24 | as seen in: 25 | <% @repo.branches_with(@commit.oid).each do |branch| %> 26 | 27 | <%= h branch.name %> 28 | 29 | <% end %> 30 |
31 |commit:
36 |tree:
37 | <% @commit.parents.size.times do %> 38 |parent:
39 | <% end %> 40 |43 | 44 | <%= @commit.oid %> 45 | 46 |
47 | 48 |49 | 50 | <%= @commit.tree.oid %> 51 | 52 |
53 | 54 | <% @commit.parents.each do |parent| %> 55 |56 | 57 | <%= parent.oid %> 58 | 59 |
60 | <% end %> 61 | 62 | <%= patch_link @commit, @repo.param %>
73 | 74 | 75 |
76 | 77 |
85 | <%= patch.delta.new_file[:path] %>
86 |
88 | <% unless patch.delta.deleted? %> 89 | view file 90 | <% end %> 91 |
92 | tags without any options, then modified
209 | # more later.
210 | #
211 | # @param [String] text the text you want formatted
212 | #
213 | # @return [String] the formatted text
214 | def simple_format(text)
215 | text.gsub!(/ +/, " ")
216 | text.gsub!(/\r\n?/, "\n")
217 | text.gsub!(/\n/, "
\n")
218 | text
219 | end
220 |
221 | # Truncates a given text to a certain number of letters, including a special ending if needed.
222 | #
223 | # @param [String] text the text to truncate
224 | # @option options [Integer] :length (30) the length you want the output string
225 | # @option options [String] :omission ("...") the string to show an omission.
226 | #
227 | # @return [String] the truncated text.
228 | def truncate(text, options={})
229 | options[:length] ||= 30
230 | options[:omission] ||= "..."
231 |
232 | if text
233 | l = options[:length] - options[:omission].length
234 | chars = text
235 | stop = options[:separator] ? (chars.rindex(options[:separator], l) || l) : l
236 | (chars.length > options[:length] ? chars[0...stop] + options[:omission] : text).to_s
237 | end
238 | end
239 |
240 | # Returns the rfc representation of a date, for use in the atom feeds.
241 | #
242 | # @param [DateTime] datetime the date to format
243 | # @return [String] the formatted datetime
244 | def rfc_date(datetime)
245 | datetime.strftime("%Y-%m-%dT%H:%M:%SZ") # 2003-12-13T18:30:02Z
246 | end
247 |
248 | # Returns the Hostname of the given install, for use in the atom feeds.
249 | #
250 | # @return [String] the hostname of the server. Respects HTTP-X-Forwarded-For
251 | def hostname
252 | (request.env['HTTP_X_FORWARDED_SERVER'] =~ /[a-z]*/) ? request.env['HTTP_X_FORWARDED_SERVER'] : request.env['HTTP_HOST']
253 | end
254 | end
255 | end
256 |
--------------------------------------------------------------------------------
/lib/ginatra.rb:
--------------------------------------------------------------------------------
1 | require 'sinatra/base'
2 | require 'sinatra/partials'
3 | require 'rouge'
4 | require 'ginatra/config'
5 | require 'ginatra/errors'
6 | require 'ginatra/logger'
7 | require 'ginatra/helpers'
8 | require 'ginatra/repo'
9 | require 'ginatra/repo_list'
10 | require 'ginatra/repo_stats'
11 |
12 | module Ginatra
13 | # The main application class.
14 | # Contains all the core application logic and mounted in +config.ru+ file.
15 | class App < Sinatra::Base
16 | include Logger
17 | helpers Helpers, Sinatra::Partials
18 |
19 | configure do
20 | set :host, Ginatra.config.host
21 | set :port, Ginatra.config.port
22 | set :public_folder, "#{settings.root}/../public"
23 | set :views, "#{settings.root}/../views"
24 | enable :dump_errors, :logging, :static
25 | end
26 |
27 | configure :development do
28 | # Use better errors in development
29 | require 'better_errors'
30 | use BetterErrors::Middleware
31 | BetterErrors.application_root = settings.root
32 |
33 | # Reload modified files in development
34 | require 'sinatra/reloader'
35 | register Sinatra::Reloader
36 | Dir["#{settings.root}/ginatra/*.rb"].each { |file| also_reload file }
37 | end
38 |
39 | def cache(obj)
40 | etag obj if settings.production?
41 | end
42 |
43 | not_found do
44 | erb :'404', layout: false
45 | end
46 |
47 | error Ginatra::RepoNotFound, Ginatra::InvalidRef,
48 | Rugged::OdbError, Rugged::ObjectError, Rugged::InvalidError do
49 | halt 404, erb(:'404', layout: false)
50 | end
51 |
52 | error 500 do
53 | erb :'500', layout: false
54 | end
55 |
56 | # The root route
57 | get '/' do
58 | @repositories = Ginatra::RepoList.list
59 | erb :index
60 | end
61 |
62 | # The atom feed of recent commits to a +repo+.
63 | #
64 | # This only returns commits to the +master+ branch.
65 | #
66 | # @param [String] repo the repository url-sanitised-name
67 | get '/:repo.atom' do
68 | @repo = RepoList.find(params[:repo])
69 | @commits = @repo.commits
70 |
71 | if @commits.empty?
72 | return ''
73 | else
74 | cache "#{@commits.first.oid}/atom"
75 | content_type 'application/xml'
76 | erb :atom, layout: false
77 | end
78 | end
79 |
80 | # The html page for a +repo+.
81 | #
82 | # Shows the most recent commits in a log format.
83 | #
84 | # @param [String] repo the repository url-sanitised-name
85 | get '/:repo/?' do
86 | @repo = RepoList.find(params[:repo])
87 |
88 | if @repo.branches.none?
89 | erb :empty_repo
90 | else
91 | params[:page] = 1
92 | params[:ref] = @repo.branch_exists?('master') ? 'master' : @repo.branches.first.name
93 | @commits = @repo.commits(params[:ref])
94 | cache "#{@commits.first.oid}/log"
95 | @next_commits = !@repo.commits(params[:ref], 10, 10).nil?
96 | erb :log
97 | end
98 | end
99 |
100 | # The atom feed of recent commits to a certain branch of a +repo+.
101 | #
102 | # @param [String] repo the repository url-sanitised-name
103 | # @param [String] ref the repository ref
104 | get '/:repo/:ref.atom' do
105 | @repo = RepoList.find(params[:repo])
106 | @commits = @repo.commits(params[:ref])
107 |
108 | if @commits.empty?
109 | return ''
110 | else
111 | cache "#{@commits.first.oid}/atom/ref"
112 | content_type 'application/xml'
113 | erb :atom, layout: false
114 | end
115 | end
116 |
117 | # The html page for a given +ref+ of a +repo+.
118 | #
119 | # Shows the most recent commits in a log format.
120 | #
121 | # @param [String] repo the repository url-sanitised-name
122 | # @param [String] ref the repository ref
123 | get '/:repo/:ref' do
124 | @repo = RepoList.find(params[:repo])
125 | @commits = @repo.commits(params[:ref])
126 | cache "#{@commits.first.oid}/ref" if @commits.any?
127 | params[:page] = 1
128 | @next_commits = !@repo.commits(params[:ref], 10, 10).nil?
129 | erb :log
130 | end
131 |
132 | # The html page for a +repo+ stats.
133 | #
134 | # Shows information about repository branch.
135 | #
136 | # @param [String] repo the repository url-sanitised-name
137 | # @param [String] ref the repository ref
138 | get '/:repo/stats/:ref' do
139 | @repo = RepoList.find(params[:repo])
140 | @stats = RepoStats.new(@repo, params[:ref])
141 | erb :stats
142 | end
143 |
144 | # The patch file for a given commit to a +repo+.
145 | #
146 | # @param [String] repo the repository url-sanitised-name
147 | # @param [String] commit the repository commit
148 | get '/:repo/commit/:commit.patch' do
149 | content_type :txt
150 | repo = RepoList.find(params[:repo])
151 | commit = repo.commit(params[:commit])
152 | cache "#{commit.oid}/patch"
153 | diff = commit.parents.first.diff(commit)
154 | diff.patch
155 | end
156 |
157 | # The html representation of a commit.
158 | #
159 | # @param [String] repo the repository url-sanitised-name
160 | # @param [String] commit the repository commit
161 | get '/:repo/commit/:commit' do
162 | @repo = RepoList.find(params[:repo])
163 | @commit = @repo.commit(params[:commit])
164 | cache @commit.oid
165 | erb :commit
166 | end
167 |
168 | # The html representation of a tag.
169 | #
170 | # @param [String] repo the repository url-sanitised-name
171 | # @param [String] tag the repository tag
172 | get '/:repo/tag/:tag' do
173 | @repo = RepoList.find(params[:repo])
174 | @commit = @repo.commit_by_tag(params[:tag])
175 | cache "#{@commit.oid}/tag"
176 | erb :commit
177 | end
178 |
179 | # HTML page for a given tree in a given +repo+
180 | #
181 | # @param [String] repo the repository url-sanitised-name
182 | # @param [String] tree the repository tree
183 | get '/:repo/tree/:tree' do
184 | @repo = RepoList.find(params[:repo])
185 | @tree = @repo.find_tree(params[:tree])
186 | cache @tree.oid
187 |
188 | @path = {
189 | blob: "#{params[:repo]}/blob/#{params[:tree]}",
190 | tree: "#{params[:repo]}/tree/#{params[:tree]}"
191 | }
192 | erb :tree, layout: !is_pjax?
193 | end
194 |
195 | # HTML page for a given tree in a given +repo+.
196 | #
197 | # This one supports a splat parameter so you can specify a path.
198 | #
199 | # @param [String] repo the repository url-sanitised-name
200 | # @param [String] tree the repository tree
201 | get '/:repo/tree/:tree/*' do
202 | @repo = RepoList.find(params[:repo])
203 | @tree = @repo.find_tree(params[:tree])
204 | cache "#{@tree.oid}/#{params[:splat].first}"
205 |
206 | @tree.walk(:postorder) do |root, entry|
207 | @tree = @repo.lookup entry[:oid] if "#{root}#{entry[:name]}" == params[:splat].first
208 | end
209 |
210 | @path = {
211 | blob: "#{params[:repo]}/blob/#{params[:tree]}/#{params[:splat].first}",
212 | tree: "#{params[:repo]}/tree/#{params[:tree]}/#{params[:splat].first}"
213 | }
214 | erb :tree, layout: !is_pjax?
215 | end
216 |
217 | # HTML page for a given blob in a given +repo+
218 | #
219 | # @param [String] repo the repository url-sanitised-name
220 | # @param [String] tree the repository tree
221 | get '/:repo/blob/:blob' do
222 | @repo = RepoList.find(params[:repo])
223 | @tree = @repo.lookup(params[:tree])
224 |
225 | @tree.walk(:postorder) do |root, entry|
226 | @blob = entry if "#{root}#{entry[:name]}" == params[:splat].first
227 | end
228 |
229 | cache @blob[:oid]
230 | erb :blob, layout: !is_pjax?
231 | end
232 |
233 | # HTML page for a given blob in a given repo.
234 | #
235 | # Uses a splat param to specify a blob path.
236 | #
237 | # @param [String] repo the repository url-sanitised-name
238 | # @param [String] tree the repository tree
239 | get '/:repo/blob/:tree/*' do
240 | @repo = RepoList.find(params[:repo])
241 | @tree = @repo.find_tree(params[:tree])
242 |
243 | @tree.walk(:postorder) do |root, entry|
244 | @blob = entry if "#{root}#{entry[:name]}" == params[:splat].first
245 | end
246 |
247 | cache "#{@blob[:oid]}/#{@tree.oid}"
248 | erb :blob, layout: !is_pjax?
249 | end
250 |
251 | # HTML page for a raw blob contents in a given repo.
252 | #
253 | # Uses a splat param to specify a blob path.
254 | #
255 | # @param [String] repo the repository url-sanitised-name
256 | # @param [String] tree the repository tree
257 | get '/:repo/raw/:tree/*' do
258 | @repo = RepoList.find(params[:repo])
259 | @tree = @repo.find_tree(params[:tree])
260 |
261 | @tree.walk(:postorder) do |root, entry|
262 | @blob = entry if "#{root}#{entry[:name]}" == params[:splat].first
263 | end
264 |
265 | cache "#{@blob[:oid]}/#{@tree.oid}/raw"
266 | blob = @repo.find_blob @blob[:oid]
267 | if blob.binary?
268 | content_type 'application/octet-stream'
269 | blob.text
270 | else
271 | content_type :txt
272 | blob.text
273 | end
274 | end
275 |
276 | # Pagination route for the commits to a given ref in a +repo+.
277 | #
278 | # @param [String] repo the repository url-sanitised-name
279 | # @param [String] ref the repository ref
280 | get '/:repo/:ref/page/:page' do
281 | pass unless params[:page] =~ /\A\d+\z/
282 | params[:page] = params[:page].to_i
283 | @repo = RepoList.find(params[:repo])
284 | @commits = @repo.commits(params[:ref], 10, (params[:page] - 1) * 10)
285 | cache "#{@commits.first.oid}/page/#{params[:page]}/ref/#{params[:ref]}" if @commits.any?
286 | @next_commits = !@repo.commits(params[:ref], 10, params[:page] * 10).nil?
287 | if params[:page] - 1 > 0
288 | @previous_commits = !@repo.commits(params[:ref], 10, (params[:page] - 1) * 10).empty?
289 | end
290 | erb :log
291 | end
292 |
293 | end # App
294 | end # Ginatra
295 |
--------------------------------------------------------------------------------
/public/js/lib/jquery.pjax.js:
--------------------------------------------------------------------------------
1 | // jquery.pjax.js
2 | // copyright chris wanstrath
3 | // https://github.com/defunkt/jquery-pjax
4 |
5 | (function($){
6 |
7 | // When called on a container with a selector, fetches the href with
8 | // ajax into the container or with the data-pjax attribute on the link
9 | // itself.
10 | //
11 | // Tries to make sure the back button and ctrl+click work the way
12 | // you'd expect.
13 | //
14 | // Exported as $.fn.pjax
15 | //
16 | // Accepts a jQuery ajax options object that may include these
17 | // pjax specific options:
18 | //
19 | //
20 | // container - Where to stick the response body. Usually a String selector.
21 | // $(container).html(xhr.responseBody)
22 | // (default: current jquery context)
23 | // push - Whether to pushState the URL. Defaults to true (of course).
24 | // replace - Want to use replaceState instead? That's cool.
25 | //
26 | // For convenience the second parameter can be either the container or
27 | // the options object.
28 | //
29 | // Returns the jQuery object
30 | function fnPjax(selector, container, options) {
31 | var context = this
32 | return this.on('click.pjax', selector, function(event) {
33 | var opts = $.extend({}, optionsFor(container, options))
34 | if (!opts.container)
35 | opts.container = $(this).attr('data-pjax') || context
36 | handleClick(event, opts)
37 | })
38 | }
39 |
40 | // Public: pjax on click handler
41 | //
42 | // Exported as $.pjax.click.
43 | //
44 | // event - "click" jQuery.Event
45 | // options - pjax options
46 | //
47 | // Examples
48 | //
49 | // $(document).on('click', 'a', $.pjax.click)
50 | // // is the same as
51 | // $(document).pjax('a')
52 | //
53 | // $(document).on('click', 'a', function(event) {
54 | // var container = $(this).closest('[data-pjax-container]')
55 | // $.pjax.click(event, container)
56 | // })
57 | //
58 | // Returns nothing.
59 | function handleClick(event, container, options) {
60 | options = optionsFor(container, options)
61 |
62 | var link = event.currentTarget
63 |
64 | if (link.tagName.toUpperCase() !== 'A')
65 | throw "$.fn.pjax or $.pjax.click requires an anchor element"
66 |
67 | // Middle click, cmd click, and ctrl click should open
68 | // links in a new tab as normal.
69 | if ( event.which > 1 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey )
70 | return
71 |
72 | // Ignore cross origin links
73 | if ( location.protocol !== link.protocol || location.host !== link.host )
74 | return
75 |
76 | // Ignore anchors on the same page
77 | if (link.hash && link.href.replace(link.hash, '') ===
78 | location.href.replace(location.hash, ''))
79 | return
80 |
81 | // Ignore empty anchor "foo.html#"
82 | if (link.href === location.href + '#')
83 | return
84 |
85 | var defaults = {
86 | url: link.href,
87 | container: $(link).attr('data-pjax'),
88 | target: link,
89 | fragment: null
90 | }
91 |
92 | pjax($.extend({}, defaults, options))
93 |
94 | event.preventDefault()
95 | }
96 |
97 | // Public: pjax on form submit handler
98 | //
99 | // Exported as $.pjax.submit
100 | //
101 | // event - "click" jQuery.Event
102 | // options - pjax options
103 | //
104 | // Examples
105 | //
106 | // $(document).on('submit', 'form', function(event) {
107 | // var container = $(this).closest('[data-pjax-container]')
108 | // $.pjax.submit(event, container)
109 | // })
110 | //
111 | // Returns nothing.
112 | function handleSubmit(event, container, options) {
113 | options = optionsFor(container, options)
114 |
115 | var form = event.currentTarget
116 |
117 | if (form.tagName.toUpperCase() !== 'FORM')
118 | throw "$.pjax.submit requires a form element"
119 |
120 | var defaults = {
121 | type: form.method,
122 | url: form.action,
123 | data: $(form).serializeArray(),
124 | container: $(form).attr('data-pjax'),
125 | target: form,
126 | fragment: null,
127 | timeout: 0
128 | }
129 |
130 | pjax($.extend({}, defaults, options))
131 |
132 | event.preventDefault()
133 | }
134 |
135 | // Loads a URL with ajax, puts the response body inside a container,
136 | // then pushState()'s the loaded URL.
137 | //
138 | // Works just like $.ajax in that it accepts a jQuery ajax
139 | // settings object (with keys like url, type, data, etc).
140 | //
141 | // Accepts these extra keys:
142 | //
143 | // container - Where to stick the response body.
144 | // $(container).html(xhr.responseBody)
145 | // push - Whether to pushState the URL. Defaults to true (of course).
146 | // replace - Want to use replaceState instead? That's cool.
147 | //
148 | // Use it just like $.ajax:
149 | //
150 | // var xhr = $.pjax({ url: this.href, container: '#main' })
151 | // console.log( xhr.readyState )
152 | //
153 | // Returns whatever $.ajax returns.
154 | function pjax(options) {
155 | options = $.extend(true, {}, $.ajaxSettings, pjax.defaults, options)
156 |
157 | if ($.isFunction(options.url)) {
158 | options.url = options.url()
159 | }
160 |
161 | var target = options.target
162 |
163 | var hash = parseURL(options.url).hash
164 |
165 | var context = options.context = findContainerFor(options.container)
166 |
167 | // We want the browser to maintain two separate internal caches: one
168 | // for pjax'd partial page loads and one for normal page loads.
169 | // Without adding this secret parameter, some browsers will often
170 | // confuse the two.
171 | if (!options.data) options.data = {}
172 | options.data._pjax = context.selector
173 |
174 | function fire(type, args) {
175 | var event = $.Event(type, { relatedTarget: target })
176 | context.trigger(event, args)
177 | return !event.isDefaultPrevented()
178 | }
179 |
180 | var timeoutTimer
181 |
182 | options.beforeSend = function(xhr, settings) {
183 | // No timeout for non-GET requests
184 | // Its not safe to request the resource again with a fallback method.
185 | if (settings.type !== 'GET') {
186 | settings.timeout = 0
187 | }
188 |
189 | if (settings.timeout > 0) {
190 | timeoutTimer = setTimeout(function() {
191 | if (fire('pjax:timeout', [xhr, options]))
192 | xhr.abort('timeout')
193 | }, settings.timeout)
194 |
195 | // Clear timeout setting so jquerys internal timeout isn't invoked
196 | settings.timeout = 0
197 | }
198 |
199 | xhr.setRequestHeader('X-PJAX', 'true')
200 | xhr.setRequestHeader('X-PJAX-Container', context.selector)
201 |
202 | var result
203 |
204 | if (!fire('pjax:beforeSend', [xhr, settings]))
205 | return false
206 |
207 | options.requestUrl = parseURL(settings.url).href
208 | }
209 |
210 | options.complete = function(xhr, textStatus) {
211 | if (timeoutTimer)
212 | clearTimeout(timeoutTimer)
213 |
214 | fire('pjax:complete', [xhr, textStatus, options])
215 |
216 | fire('pjax:end', [xhr, options])
217 | }
218 |
219 | options.error = function(xhr, textStatus, errorThrown) {
220 | var container = extractContainer("", xhr, options)
221 |
222 | var allowed = fire('pjax:error', [xhr, textStatus, errorThrown, options])
223 | if (options.type == 'GET' && textStatus !== 'abort' && allowed) {
224 | locationReplace(container.url)
225 | }
226 | }
227 |
228 | options.success = function(data, status, xhr) {
229 | var container = extractContainer(data, xhr, options)
230 |
231 | if (!container.contents) {
232 | locationReplace(container.url)
233 | return
234 | }
235 |
236 | pjax.state = {
237 | id: options.id || uniqueId(),
238 | url: container.url,
239 | title: container.title,
240 | container: context.selector,
241 | fragment: options.fragment,
242 | timeout: options.timeout
243 | }
244 |
245 | if (options.push || options.replace) {
246 | window.history.replaceState(pjax.state, container.title, container.url)
247 | }
248 |
249 | if (container.title) document.title = container.title
250 | context.html(container.contents)
251 |
252 | // Scroll to top by default
253 | if (typeof options.scrollTo === 'number')
254 | $(window).scrollTop(options.scrollTo)
255 |
256 | // Google Analytics support
257 | if ( (options.replace || options.push) && window._gaq )
258 | _gaq.push(['_trackPageview'])
259 |
260 | // If the URL has a hash in it, make sure the browser
261 | // knows to navigate to the hash.
262 | if ( hash !== '' ) {
263 | // Avoid using simple hash set here. Will add another history
264 | // entry. Replace the url with replaceState and scroll to target
265 | // by hand.
266 | //
267 | // window.location.hash = hash
268 | var url = parseURL(container.url)
269 | url.hash = hash
270 |
271 | pjax.state.url = url.href
272 | window.history.replaceState(pjax.state, container.title, url.href)
273 |
274 | var target = $(url.hash)
275 | if (target.length) $(window).scrollTop(target.offset().top)
276 | }
277 |
278 | fire('pjax:success', [data, status, xhr, options])
279 | }
280 |
281 |
282 | // Initialize pjax.state for the initial page load. Assume we're
283 | // using the container and options of the link we're loading for the
284 | // back button to the initial page. This ensures good back button
285 | // behavior.
286 | if (!pjax.state) {
287 | pjax.state = {
288 | id: uniqueId(),
289 | url: window.location.href,
290 | title: document.title,
291 | container: context.selector,
292 | fragment: options.fragment,
293 | timeout: options.timeout
294 | }
295 | window.history.replaceState(pjax.state, document.title)
296 | }
297 |
298 | // Cancel the current request if we're already pjaxing
299 | var xhr = pjax.xhr
300 | if ( xhr && xhr.readyState < 4) {
301 | xhr.onreadystatechange = $.noop
302 | xhr.abort()
303 | }
304 |
305 | pjax.options = options
306 | var xhr = pjax.xhr = $.ajax(options)
307 |
308 | if (xhr.readyState > 0) {
309 | if (options.push && !options.replace) {
310 | // Cache current container element before replacing it
311 | cachePush(pjax.state.id, context.clone().contents())
312 |
313 | window.history.pushState(null, "", stripPjaxParam(options.requestUrl))
314 | }
315 |
316 | fire('pjax:start', [xhr, options])
317 | fire('pjax:send', [xhr, options])
318 | }
319 |
320 | return pjax.xhr
321 | }
322 |
323 | // Public: Reload current page with pjax.
324 | //
325 | // Returns whatever $.pjax returns.
326 | function pjaxReload(container, options) {
327 | var defaults = {
328 | url: window.location.href,
329 | push: false,
330 | replace: true,
331 | scrollTo: false
332 | }
333 |
334 | return pjax($.extend(defaults, optionsFor(container, options)))
335 | }
336 |
337 | // Internal: Hard replace current state with url.
338 | //
339 | // Work for around WebKit
340 | // https://bugs.webkit.org/show_bug.cgi?id=93506
341 | //
342 | // Returns nothing.
343 | function locationReplace(url) {
344 | window.history.replaceState(null, "", "#")
345 | window.location.replace(url)
346 | }
347 |
348 | // popstate handler takes care of the back and forward buttons
349 | //
350 | // You probably shouldn't use pjax on pages with other pushState
351 | // stuff yet.
352 | function onPjaxPopstate(event) {
353 | var state = event.state
354 |
355 | if (state && state.container) {
356 | var container = $(state.container)
357 | if (container.length) {
358 | var contents = cacheMapping[state.id]
359 |
360 | if (pjax.state) {
361 | // Since state ids always increase, we can deduce the history
362 | // direction from the previous state.
363 | var direction = pjax.state.id < state.id ? 'forward' : 'back'
364 |
365 | // Cache current container before replacement and inform the
366 | // cache which direction the history shifted.
367 | cachePop(direction, pjax.state.id, container.clone().contents())
368 | }
369 |
370 | var popstateEvent = $.Event('pjax:popstate', {
371 | state: state,
372 | direction: direction
373 | })
374 | container.trigger(popstateEvent)
375 |
376 | var options = {
377 | id: state.id,
378 | url: state.url,
379 | container: container,
380 | push: false,
381 | fragment: state.fragment,
382 | timeout: state.timeout,
383 | scrollTo: false
384 | }
385 |
386 | if (contents) {
387 | container.trigger('pjax:start', [null, options])
388 |
389 | if (state.title) document.title = state.title
390 | container.html(contents)
391 | pjax.state = state
392 |
393 | container.trigger('pjax:end', [null, options])
394 | } else {
395 | pjax(options)
396 | }
397 |
398 | // Force reflow/relayout before the browser tries to restore the
399 | // scroll position.
400 | container[0].offsetHeight
401 | } else {
402 | locationReplace(location.href)
403 | }
404 | }
405 | }
406 |
407 | // Fallback version of main pjax function for browsers that don't
408 | // support pushState.
409 | //
410 | // Returns nothing since it retriggers a hard form submission.
411 | function fallbackPjax(options) {
412 | var url = $.isFunction(options.url) ? options.url() : options.url,
413 | method = options.type ? options.type.toUpperCase() : 'GET'
414 |
415 | var form = $('