1 " Vim script
  2 " Author: Peter Odding <peter@peterodding.com>
  3 " Last Change: September 6, 2010
  4 " URL: http://peterodding.com/code/vim/publish/
  5 
  6 function! publish#resolve_files(directory, pathnames) " {{{1
  7   " Create a dictionary that maps the fully resolved pathnames of the files to
  8   " be published to the absolute pathnames provided by the user. This enables
  9   " the script to gracefully handle symbolic links which I use a lot :-)
 10   let resolved_files = {}
 11   for pathname in a:pathnames
 12     let pathname = xolox#path#merge(a:directory, pathname)
 13     let absolute = fnamemodify(pathname, ':p')
 14     let resolved_files[resolve(absolute)] = absolute
 15   endfor
 16   return resolved_files
 17 endfunction
 18 
 19 function! publish#update_tags(pathnames) " {{{1
 20   " Integration with easytags.vim to automatically create/update tags for all
 21   " files before they're published, see http://peterodding.com/code/vim/easytags/
 22   if exists('g:loaded_easytags')
 23     call easytags#update(1, 0, a:pathnames)
 24   endif
 25 endfunction
 26 
 27 function! publish#find_tags(files_to_publish) " {{{1
 28   " Given a dictionary like the one created above, this function will filter
 29   " the results of taglist() to remove irrelevant entries. In the process tag
 30   " search ex-commands are converted into line numbers.
 31   let start = xolox#timer#start()
 32   let num_duplicates = 0
 33   let tags_to_publish = {}
 34   let s:cached_contents = {}
 35   for entry in taglist('.')
 36     let pathname = xolox#path#absolute(entry.filename)
 37     if has_key(a:files_to_publish, pathname) && s:pattern_to_lnum(entry, pathname)
 38       if !has_key(tags_to_publish, entry.name)
 39         let tags_to_publish[entry.name] = entry
 40       else
 41         let num_duplicates += 1
 42         let other = tags_to_publish[entry.name]
 43         if entry.filename == other.filename && entry.lnum < other.lnum
 44           let tags_to_publish[entry.name] = entry
 45         endif
 46         if num_duplicates <= 3
 47           let tag_name = string(entry.name)
 48           let this_path = string(entry.filename)
 49           let other_path = string(other.filename)
 50           let msg = "publish.vim: Ignoring duplicate tag %s! (duplicate is in %s, first was in %s)"
 51           call xolox#warning(msg, tag_name, this_path, other_path)
 52         endif
 53       endif
 54     endif
 55   endfor
 56   if num_duplicates > 3
 57     let more = num_duplicates - 3
 58     let msg = "publish.vim: Ignored %s more duplicate tag%s!"
 59     call xolox#warning(msg, more, more == 1 ? '' : 's')
 60   endif
 61   unlet s:cached_contents
 62   let msg = "publish.vim: Found %i tag%s to publish in %s."
 63   let numtags = len(tags_to_publish)
 64   call xolox#timer#stop(msg, numtags, numtags != 1 ? 's' : '', start)
 65   return tags_to_publish
 66 endfunction
 67 
 68 function! s:pattern_to_lnum(entry, pathname) " {{{2
 69   " Tag file entries can refer to source file locations with line numbers and
 70   " search patterns. Since search patterns are more flexible I use those, but
 71   " the plug-in needs absolute line numbers, so this function converts search
 72   " patterns into line numbers.
 73   if a:entry.cmd =~ '^\d\+$'
 74     let a:entry.lnum = a:entry.cmd + 0
 75     return 1
 76   else
 77     if !has_key(s:cached_contents, a:pathname)
 78       let contents = readfile(a:pathname)
 79       let s:cached_contents[a:pathname] = contents
 80     else
 81       let contents = s:cached_contents[a:pathname]
 82     endif
 83     " Convert tag search command to plain Vim pattern, based on :help tag-search.
 84     let pattern = a:entry.cmd
 85     let pattern = matchstr(pattern, '^/^\zs.*\ze$/$')
 86     let pattern = '^' . xolox#escape#pattern(pattern) . '$'
 87     try
 88       let index = match(contents, pattern)
 89     catch
 90       throw "Failed pattern: " . string(pattern)
 91     endtry
 92     if index >= 0
 93       let a:entry.lnum = index + 1
 94       return 1
 95     endif
 96   endif
 97 endfunction
 98 
 99 function! publish#create_subst_cmd(tags_to_publish) " {{{1
100   " Generate a :substitute command that, when executed, replaces tags with
101   " hyperlinks using a callback. This is complicated somewhat by the fact that
102   " tag names won't always appear literally in the output of 2html.vim, for
103   " example the names of Vim autoload functions can appear as:
104   " 
105   "   foo#bar#<span class=Normal>baz</span>
106   " 
107   let patterns = []
108   let slfunctions = []
109   for name in keys(a:tags_to_publish)
110     let entry = a:tags_to_publish[name]
111     if get(entry, 'language') == 'Vim'
112       let is_slfunc = '\s\(s:\|<[Ss][Ii][Dd]>\)' . xolox#escape#pattern(name) . '\s*('
113       if get(entry, 'cmd') =~ is_slfunc
114         call add(slfunctions, xolox#escape#pattern(name))
115         continue
116       endif
117     endif
118     call add(patterns, xolox#escape#pattern(name))
119   endfor
120   call insert(patterns, '\%(\%(&lt;[Ss][Ii][Dd]&gt;\|s:\)\%(' . join(slfunctions, '\|') . '\)\)')
121   let tag_names_pattern = escape(join(patterns, '\|'), '/')
122   " Gotcha: Use \w\@<! and \w\@! here instead of \< and \> which won't work.
123   return '%s/[A-Za-z0-9_]\@<!\%(' . tag_names_pattern . '\)[A-Za-z0-9_]\@!/\=s:ConvertTagToLink(submatch(0))/eg'
124 endfunction
125 
126 function! publish#munge_syntax_items() " {{{1
127   " Tag to hyperlink conversion only works when tag names appear literally in
128   " the output of 2html.vim while this isn't always the case in Vim scripts.
129   if &filetype == 'vim'
130     syntax match vimFuncName /\<s:\w\+\>/ containedin=vim.*
131     syntax match vimFuncName /\c<Sid>\w\+\>/ containedin=vim.*
132   endif
133 endfunction
134 
135 function! publish#rsync_check(target) " {{{1
136   let start = xolox#timer#start()
137   let result = ''
138   let matches = matchlist(a:target, '^sftp://\([^/]\+\)\(.*\)$')
139   if len(matches) >= 3
140     let host = matches[1]
141     let path = substitute(matches[2], '^/', '', '')
142     call system('rsync --version')
143     if !v:shell_error
144       call system('ssh ' . host . ' rsync --version')
145       if !v:shell_error
146         let result = host . ':' . path
147       endif
148     endif
149   endif
150   call xolox#timer#stop("publish.vim: Checked rsync support in %s.", start)
151   return result
152 endfunction
153 
154 function! publish#run_rsync(target, tempdir) " {{{1
155   let start = xolox#timer#start()
156   let target = fnameescape(a:target . '/')
157   let tempdir = fnameescape(a:tempdir . '/')
158   call xolox#message("publish.vim: Uploading files to %s using rsync.", a:target)
159   execute '!rsync -vr' tempdir target
160   call xolox#timer#stop("publish.vim: Finished uploading in %s.", start)
161   if v:shell_error
162     throw "publish.vim: Failed to run rsync!"
163   endif
164 endfunction
165 
166 function! publish#create_dirs(target_path) " {{{1
167   " If the directory where the files are published resides on the local file
168   " system then try to automatically create any missing directories because
169   " creating those directories by hand quickly gets tiresome.
170   if a:target_path !~ '://'
171     let current_directory = fnamemodify(a:target_path, ':h')
172     if !isdirectory(current_directory)
173       silent! call mkdir(current_directory, 'p')
174       if !isdirectory(current_directory)
175         let msg = "Failed to create directory %s! What now?"
176         if confirm(printf(msg, string(current_directory)), "&Abort\n&Ignore") == 1
177           let msg = "publish.vim: Failed to create %s, aborting .."
178           call xolox#warning(msg, string(current_directory))
179           return 0
180         else
181           let msg = "publish.vim: Failed to create %s, ignoring .."
182           call xolox#warning(msg, string(current_directory))
183           continue
184         endif
185       endif
186     endif
187   endif
188   return 1
189 endfunction
190 
191 function! publish#prep_env(enable) " {{{1
192 
193   " Change the environment before publishing and restore afterwards.
194 
195   " Avoid E325 when publishing files that are currently being edited in Vim.
196   augroup PluginPublish
197     autocmd!
198     if a:enable
199       autocmd SwapExists * let v:swapchoice = 'e'
200     endif
201   augroup END
202 
203   " Avoid the hit-enter prompt!
204   if a:enable
205     let s:more_save = &more
206     set nomore
207   else
208     let &more = s:more_save
209   endif
210 
211   " Avoid triggering automatic commands intended to update `Last changed'
212   " headers and such by executing :write commands, because the source files
213   " aren't actually modified but only copied. I can't use :noautocmd :write
214   " for this because that would disable remote publishing through the netrw
215   " plug-in! Therefor I've resorted to the following:
216   if a:enable
217     let s:ei_save = &eventignore
218     set eventignore=BufWritePre
219   else
220     let &eventignore = s:ei_save
221   endif
222 
223   " Avoid E488 which happens when you publish using netrw, overwriting
224   " previously published files that contain modelines.
225   if a:enable
226     let s:mls_save = &modelines
227     set modelines=0
228   else
229     let &modelines = s:mls_save
230   endif
231 
232   " Instruct the 2html script to add line numbers that we'll transform into
233   " anchors which are used as the targets of hyperlinks created from tags.
234   " Also instruct the 2html script to use CSS which we will modify to improve
235   " the appearance of hyperlinks (so they inherit the highlighting color).
236   " Finally ignore any text folding until I find out how to get the dynamic
237   " JavaScript text folding to work.
238   if a:enable
239     let s:hif_save = exists('g:html_ignore_folding') ? g:html_ignore_folding : 0
240     let s:hnl_save = exists('g:html_number_lines') ? g:html_number_lines : 0
241     let s:huc_save = exists('g:html_use_css') ? g:html_use_css : 0
242     let g:html_ignore_folding = 1
243     let g:html_number_lines =  1
244     let g:html_use_css = 1
245   else
246     let g:html_ignore_folding = s:hif_save
247     let g:html_number_lines =  s:hnl_save
248     let g:html_use_css =  s:huc_save
249   endif
250 
251 endfunction
252 
253 function! publish#customize_html(page_title) " {{{1
254 
255   " Change document title to relative pathname.
256   silent keepjumps %s@<title>\zs.*\ze</title>@\=publish#html_encode(a:page_title)@e
257 
258   " Insert CSS to remove the default colors and underline from hyper links
259   " and to remove any padding between the browser chrome and page content.
260   let custom_css = "\nhtml, body, pre { margin: 0; padding: 0; }"
261   let custom_css .= "\na:link, a:visited { color: inherit; text-decoration: none; }"
262   let custom_css .= "\npre:hover a:link, pre:hover a:visited { text-decoration: underline; }"
263   let custom_css .= "\na:link span, a:visited span { text-decoration: inherit; }"
264   let custom_css .= "\n.lnr a:link, .lnr a:visited { text-decoration: none !important; }"
265   silent keepjumps %s@\ze\_s\+-->\_s\+</style>@\= "\n" . custom_css@e
266 
267   " Add link anchors to line numbering.
268   silent keepjumps %s@<span class="lnr">\zs\s*\(\d\+\)\s*\ze</span>@<a name="l\1" href="#l\1">\0</a>@eg
269 
270 endfunction
271 
272 function! publish#html_encode(s) " {{{1
273   let s = substitute(a:s, '&', '\&amp;', 'g')
274   let s = substitute(s, '<', '\&lt;', 'g')
275   let s = substitute(s, '>', '\&gt;', 'g')
276   return s
277 endfunction
278 
279 " vim: ts=2 sw=2 et