diff --git a/autoload/wiki/buffer.vim b/autoload/wiki/buffer.vim index 4e16fab0..0d0805c0 100644 --- a/autoload/wiki/buffer.vim +++ b/autoload/wiki/buffer.vim @@ -25,7 +25,7 @@ function! wiki#buffer#init() abort " {{{1 call s:init_buffer_commands() call s:init_buffer_mappings() - call s:apply_template() + call wiki#template#init() if exists('#User#WikiBufferInitialized') doautocmd User WikiBufferInitialized @@ -173,19 +173,3 @@ function! s:init_buffer_mappings() abort " {{{1 endfunction " }}}1 - -function! s:apply_template() abort " {{{1 - if filereadable(expand('%')) | return | endif - - let l:match = matchlist(expand('%:t:r'), '^\(\d\d\d\d\)_\(\w\)\(\d\d\)$') - if empty(l:match) | return | endif - let [l:year, l:type, l:number] = l:match[1:3] - - if l:type ==# 'w' - call wiki#template#weekly_summary(l:year, l:number) - elseif l:type ==# 'm' - call wiki#template#monthly_summary(l:year, l:number) - endif -endfunction - -" }}}1 diff --git a/autoload/wiki/fzf.vim b/autoload/wiki/fzf.vim index 4e3271f9..7503b1c3 100644 --- a/autoload/wiki/fzf.vim +++ b/autoload/wiki/fzf.vim @@ -98,6 +98,7 @@ function! s:accept_page(lines) abort "{{{1 if len(a:lines) == 2 || !empty(a:lines[1]) call wiki#page#open(a:lines[0]) + sleep 1 else let l:file = split(a:lines[2], '#####')[0] execute 'edit ' . l:file diff --git a/autoload/wiki/nav.vim b/autoload/wiki/nav.vim index 814f27bf..64704715 100644 --- a/autoload/wiki/nav.vim +++ b/autoload/wiki/nav.vim @@ -19,6 +19,22 @@ endfunction " }}}1 +function! wiki#nav#add_to_stack(link) abort " {{{1 + let s:position_stack += [a:link] +endfunction + +let s:position_stack = [] + +" }}}1 +function! wiki#nav#get_previous() abort "{{{1 + let l:previous = get(s:position_stack, -1, []) + if !empty(l:previous) | return l:previous | endif + + let l:file = expand('#:p') + if filereadable(l:file) | return [l:file, 1] | endif +endfunction + +" }}}1 function! wiki#nav#return() abort "{{{1 if g:wiki_write_on_nav | update | endif @@ -32,10 +48,3 @@ function! wiki#nav#return() abort "{{{1 endfunction " }}}1 -function! wiki#nav#add_to_stack(link) abort " {{{1 - let s:position_stack += [a:link] -endfunction - -let s:position_stack = [] - -" }}}1 diff --git a/autoload/wiki/page.vim b/autoload/wiki/page.vim index b2a08459..16601d89 100644 --- a/autoload/wiki/page.vim +++ b/autoload/wiki/page.vim @@ -9,15 +9,7 @@ function! wiki#page#open(page) abort "{{{1 \ !empty(g:wiki_map_create_page) && exists('*' . g:wiki_map_create_page) \ ? call(g:wiki_map_create_page, [a:page]) \ : a:page - let l:url = wiki#url#parse('wiki:/' . l:page) - - if !filereadable(l:url.path) - redraw! - call wiki#log#info('Opening new page "' . l:page . '"') - sleep 1 - end - - call l:url.follow() + call wiki#url#parse('wiki:/' . l:page).follow() endfunction "}}}1 diff --git a/autoload/wiki/template.vim b/autoload/wiki/template.vim index acf9cf9e..dcd721e0 100644 --- a/autoload/wiki/template.vim +++ b/autoload/wiki/template.vim @@ -4,6 +4,92 @@ " Email: karl.yngve@gmail.com " +function! wiki#template#init() abort " {{{1 + if filereadable(expand('%')) | return | endif + + let l:context = { + \ 'date': strftime("%F"), + \ 'name': expand('%:t:r'), + \ 'origin': wiki#nav#get_previous(), + \ 'path': expand('%:p'), + \ 'path_wiki': wiki#paths#shorten_relative(expand('%:p')), + \ 'time': strftime("%H:%M"), + \} + + for l:template in g:wiki_templates + if s:template_match(l:template, l:context) + return s:template_apply(l:template, l:context) + endif + endfor + + let l:match = matchlist(expand('%:t:r'), '^\(\d\d\d\d\)_\(\w\)\(\d\d\)$') + if empty(l:match) | return | endif + let [l:year, l:type, l:number] = l:match[1:3] + + if l:type ==# 'w' + call wiki#template#weekly_summary(l:year, l:number) + elseif l:type ==# 'm' + call wiki#template#monthly_summary(l:year, l:number) + endif +endfunction + +" }}}1 + +function! wiki#template#case_title(text, ...) abort " {{{1 + return join(map(split(a:text), {_, x -> toupper(x[0]) . strpart(x, 1)})) +endfunction + +" }}}1 + + +function! s:template_match(t, ctx) abort " {{{1 + if has_key(a:t, 'match_re') + return a:ctx.name =~# a:t.match_re + elseif has_key(a:t, 'match_func') + return a:t.match_func(a:ctx) + endif +endfunction + +" }}}1 +function! s:template_apply(t, ctx) abort " {{{1 + if has_key(a:t, 'source_func') + return a:t.source_func(a:ctx) + endif + + let l:source = get(a:t, 'source_filename', '') + if !filereadable(l:source) | return | endif + + " Interpolate the context "variables" + let l:lines = join(readfile(l:source), "\n") + for [l:key, l:value] in items(a:ctx) + let l:lines = substitute(l:lines, '{' . l:key . '}', l:value, 'g') + endfor + + " Interpolate user functions + let [l:match, l:c1, l:c2] = matchstrpos(l:lines, '{{[a-zA-Z#_]\+\s\+[^}]*}}') + while !empty(l:match) + let l:parts = matchlist(l:match, '{{\([a-zA-Z#_]\+\)\s\+\([^}]*\)}}') + let l:func = l:parts[1] + let l:arg = l:parts[2] + try + let l:value = call(l:func, [l:arg]) + catch /E117:/ + let l:value = '' + endtry + + let l:pre = l:lines[:l:c1-1] + let l:post = l:lines[l:c2:] + let l:lines = l:pre . l:value . l:post + + let [l:match, l:c1, l:c2] = matchstrpos( + \ l:lines, '{{[a-zA-Z#_]\+\s\+[^}]*}}', l:c2+1) + endwhile + + call append(0, split(l:lines, "\n")) +endfunction + +" }}}1 + function! wiki#template#weekly_summary(year, week) abort " {{{1 let l:parser = s:summary.new() diff --git a/autoload/wiki/url/wiki.vim b/autoload/wiki/url/wiki.vim index 484ae2b4..578988dd 100644 --- a/autoload/wiki/url/wiki.vim +++ b/autoload/wiki/url/wiki.vim @@ -85,6 +85,11 @@ function! s:handler.follow(...) abort dict " {{{1 execute l:cmd fnameescape(self.path) + if !filereadable(self.path) + redraw! + call wiki#log#info('Opened new page "' . self.stripped . '"') + end + if exists('l:old_position') let b:wiki = get(b:, 'wiki', {}) call wiki#nav#add_to_stack(l:old_position) diff --git a/doc/wiki.txt b/doc/wiki.txt index cd144004..5043b539 100644 --- a/doc/wiki.txt +++ b/doc/wiki.txt @@ -29,28 +29,32 @@ License: MIT license {{{ ============================================================================== CONTENTS *wiki-contents* - Introduction |wiki-intro| - Requirements |wiki-intro-requirements| - Features |wiki-intro-features| - Configuration |wiki-config| - Options |wiki-config-options| - Events |wiki-config-events| - Mappings |wiki-mappings| - Text objects |wiki-mappings-text-obj| - Journal mappings |wiki-mappings-default| - Commands |wiki-commands| - Links |wiki-link| - Link URLs |wiki-link-urls| - Wiki links |wiki-link-wiki| - Markdown links |wiki-link-markdown| - Markdown image links |wiki-link-image| - Reference links |wiki-link-reference| - Zotero shortlinks |wiki-link-zotero| - AsciiDoc cross references |wiki-link-adoc-xref| - AsciiDoc link macro |wiki-link-adoc-link| - Completion |wiki-completion| - Autocomplete |wiki-completion-auto| - Tags |wiki-tags| + Introduction |wiki-intro| + Requirements |wiki-intro-requirements| + Features |wiki-intro-features| + Configuration |wiki-config| + Options |wiki-config-options| + Events |wiki-config-events| + Mappings |wiki-mappings| + Text objects |wiki-mappings-text-obj| + Journal mappings |wiki-mappings-default| + Commands |wiki-commands| + Links |wiki-link| + Link URLs |wiki-link-urls| + Wiki links |wiki-link-wiki| + Markdown links |wiki-link-markdown| + Markdown image links |wiki-link-image| + Reference links |wiki-link-reference| + Zotero shortlinks |wiki-link-zotero| + AsciiDoc cross references |wiki-link-adoc-xref| + AsciiDoc link macro |wiki-link-adoc-link| + Completion |wiki-completion| + Autocomplete |wiki-completion-auto| + Tags |wiki-tags| + Templates |wiki-templates| + Template function context |wiki-templates-context| + Template file format |wiki-templates-format| + Journal summaries |wiki-templates-journal-summaries| ============================================================================== INTRODUCTION *wiki-intro* @@ -106,6 +110,7 @@ FEATURES *wiki-intro-features* - Text objects - `iu au` Link URL - `it at` Link text + - New page templates - Support for journal entries - Navigating the journal back and forth with `(wiki-journal-next)` and `(wiki-journal-prev)`. @@ -242,7 +247,7 @@ OPTIONS *wiki-config-options* One of 'daily', 'weekly', or 'monthly'. date_format~ - Dictionary of file name formats for the 'daily', 'weekly', and 'monthly' + Dictionary of filename formats for the 'daily', 'weekly', and 'monthly' frequencies. The formats may contain the following keys: %y year (two digits) @@ -419,7 +424,7 @@ OPTIONS *wiki-config-options* The function takes two arguments: fname~ - The unresolved file name. This may be empty, which is typically the case + The unresolved filename. This may be empty, which is typically the case for inter-page links (e.g. `[[#SomeSection]]`). origin~ @@ -500,6 +505,61 @@ OPTIONS *wiki-config-options* Default: > let g:wiki_tags_scan_num_lines = 15 +*g:wiki_templates* + A list of templates for prefilling new pages. Each template should be + specified as a dictionary with a matcher and a source. Matching may be done + with regular expressions or with user functions. Similarly, sources can be + specified as a file source as specified in |wiki-templates-format|, or as + a user function with a single argument `context` as specified in + |wiki-templates-context|. + + The possible dictionary keys of a template are: + + match_re~ + |String| + A regular expression that will be matched against the new page name. + + match_func~ + |Funcref| + A function that should return |v:true| if the template should be applied + or |v:false| if it should not apply. + + source_filename~ + |String| + The path to a template file. If this is a relative path, then it will be + relative to whichever path Vim or neovim is currently at when the + template is executed. If the template file is not found, then the + template will not be applied and the next template in the list will be + tried. + + source_func~ + |Funcref| + A user function that can use e.g. |append()| to add lines to the file. + + For example: > + + function! TemplateFallback(context) + call append(0, '# ' . a:context.name) + call append(1, '') + call append(2, 'Foobar') + endfunction + + let g:wiki_templates = [ + \ { 'match_re': 'index\.md', + \ 'source_filename': '/home/user/templates/index.md'}, + \ { 'match_re': 'foo\.md', + \ 'source_filename': '.footemplate.md'}, + \ { 'match_func': {x -> v:true}, + \ 'source_func': function('TemplateFallback')}, + \] +< + Notice that in the second template, the `;` is appended to the source + filename. This means the template file is first searched for in the current + directory of the new page, then in the parent directory, and so on. If the + template file is not found, then the next template will be tried. + + Default: `[]` + *g:wiki_template_title_month* A string that specifies the title of the month template. The following keys are interpolated: @@ -508,6 +568,8 @@ OPTIONS *wiki-config-options* `%(month-name)` Name of month (see |g:wiki_month_names|) `%(year)` Year (4 digits) + See |wiki-templates-journal-summaries| for more info. + Default: `'# Summary, %(year) %(month-name)'` *g:wiki_template_title_week* @@ -517,6 +579,8 @@ OPTIONS *wiki-config-options* `%(week)` Week number `%(year)` Year (4 digits) + See |wiki-templates-journal-summaries| for more info. + Default: `'# Summary, %(year) week %(week)'` *g:wiki_viewer* @@ -699,13 +763,14 @@ the commands are also available as mappings of the form `(wiki-[name])`. *(wiki-journal-toweek)* *WikiJournalToWeek* Go to week summary. If not existing, then parse the day entries to make - a first draft. The title is given by |g:wiki_template_title_week|. + a first draft. The title is given by |g:wiki_template_title_week|. For more + info, see |wiki-templates-journal-summaries|. *(wiki-journal-tomonth)* *WikiJournalToMonth* Go to month summary. If not existing, then parse the day entries and relevant week summaries to make a first draft. The title is given by - |g:wiki_template_title_month|. + |g:wiki_template_title_month|. See also |wiki-templates-journal-summaries|. *(wiki-export)* [range]*WikiExport* [options] [fname] @@ -1119,8 +1184,8 @@ TAGS *wiki-tags* Wiki pages may be tagged with keywords for organization. By default, tags use the syntax `:tag-name:`. Multiple tags may be specified both with `:tag1: :tag2:` and with the short form `:tag1:tag2:`. The tag name must consist of purely -non-space characters. By default, all tags added in the top 15 lines of a file will be -recognized. +non-space characters. By default, all tags added in the top 15 lines of a file +will be recognized. You may customize the format of tags by modifying the |g:wiki_tags_format_pattern| variable. @@ -1138,4 +1203,97 @@ Related settings: - |g:wiki_tags| ============================================================================== - vim:tw=78:ts=8:ft=help:norl:fdm=marker: +TEMPLATES *wiki-templates* + +New pages are empty by default. However, it is possible to define templates +for prefilling new pages. Templates are specified with the option +|g:wiki_templates|, and if a template matches the new page it will be applied. +Only the first template that matches will be applied. + +The templates can be specified as user functions or as template files. User +functions assume a single variable such as described in +|wiki-templates-context|. The template files should be formatted as described +in |wiki-templates-format|. + +There is also a special kind of journal summary template which is described in +|wiki-templates-journal-summaries|. + +Related settings: +- |g:wiki_templates| + +------------------------------------------------------------------------------ +TEMPLATE FUNCTION CONTEXT *wiki-templates-context* + +The functions in |g:wiki_templates| assume a single argument `context` which +is a dictionary with the following values: + + Key Description Example~ + === =========== ======= + `name` Filename (no extension) "New Page" + `path` Full path "/path/to/wiki/sub/New Page.md" + `path_wiki` Wiki path "/sub/New Page.md" + `origin_file` Previous file "/path/to/wiki/index.md" + `origin_lnum` Previous lnum 123 + `date` ISO date 2021-07-01 + `time` Time (24h format) 19:30 + +------------------------------------------------------------------------------ +TEMPLATE FILE FORMAT *wiki-templates-format* + +A template file is essentially a simple text file to your liking. There are +two rules that allow dynamic templates: + + 1. Variable substitution with `{variable}` strings. The allowed variables + are the same as those available in |wiki-templates-context|. + 2. Function substitution with `{{Function Text String Here}}` strings. The + text string is passed as a single string argument. The context dictionary + (|wiki-templates-context|) is also passed as the second argument. The + function is assumed to return either a string or a list of strings. + +The first rule is applied first, which allows the arguments in Rule 2 to be +variable substitutions. + +Pre-defined functions:~ + `wiki#template#case_title(string)` + +An example of how these templates could be useful: If you keep a separate blog +directory in your wiki, you could add a `.template.md` file with the following +content to ensure that you follow the same structure: > + + # {{wiki#template#case_title {name}}} + Created: {date} {time} + + # Introduction + + # Conclusion + +------------------------------------------------------------------------------ +JOURNAL SUMMARIES *wiki-templates-journal-summaries* + +|wiki.vim| supports parsing the journal entries in order to make weekly and +monthly summaries. A summary is automatically created when a summary file is +opened; the format is given by `g:wiki_journal.date_format.weekly` and +`g:wiki_journal.date_format.monthly`. One may also move from a daily entry to +the corresponding week with |WikiJournalToWeek|, and similarly to the +corresponding month with |WikiJournalToMonth|. Again, if these entries do not +exist, they are automatically created and journal entries are parsed to fill +the contents. + +The parsed results typically need manual editing, and it currently only works +for a very specific format of journals. + +Related settings: +- |g:wiki_journal| + Note: The date format specifies the format for the weekly and monthly + entries. +- |g:wiki_template_title_week| + A string that specifies the title of the weekly summary. +- |g:wiki_template_title_month| + A string that specifies the title of the monthly summary. + +Related commands: +- |WikiJournalToWeek| +- |WikiJournalToMonth| + +============================================================================== + vim:tw=78:ts=8:ft=help:norl:fdm=marker:cole=2: diff --git a/plugin/wiki.vim b/plugin/wiki.vim index dd25988f..89e5c5f0 100644 --- a/plugin/wiki.vim +++ b/plugin/wiki.vim @@ -58,6 +58,7 @@ call wiki#init#option('wiki_root', '') call wiki#init#option('wiki_tags', { 'output' : 'loclist' }) call wiki#init#option('wiki_tags_format_pattern', '\v%(^|\s):\zs[^: ]+\ze:') call wiki#init#option('wiki_tags_scan_num_lines', 15) +call wiki#init#option('wiki_templates', []) call wiki#init#option('wiki_template_title_month', \ '# Summary, %(year) %(month-name)') call wiki#init#option('wiki_template_title_week', diff --git a/test/test-templates/Makefile b/test/test-templates/Makefile new file mode 100644 index 00000000..7f520be0 --- /dev/null +++ b/test/test-templates/Makefile @@ -0,0 +1,11 @@ +MYVIM ?= nvim --headless +export QUIT = 1 + +tests := $(wildcard test*.vim) + +.PHONY: all $(tests) + +test: $(tests) + +$(tests): + @$(MYVIM) -u $@ diff --git a/test/test-templates/template-a.md b/test/test-templates/template-a.md new file mode 100644 index 00000000..24ae562d --- /dev/null +++ b/test/test-templates/template-a.md @@ -0,0 +1,2 @@ +# {{UserFunc {name}}} +Created: {date} {time} diff --git a/test/test-templates/template-d.md b/test/test-templates/template-d.md new file mode 100644 index 00000000..beef43e4 --- /dev/null +++ b/test/test-templates/template-d.md @@ -0,0 +1 @@ +# {{wiki#template#case_title {name}}} diff --git a/test/test-templates/test.vim b/test/test-templates/test.vim new file mode 100644 index 00000000..8fa1bdb3 --- /dev/null +++ b/test/test-templates/test.vim @@ -0,0 +1,61 @@ +source ../init.vim + +function! UserFunc(string) abort " {{{1 + return toupper(a:string) +endfunction + +" }}}1 +function! TemplateB(context) abort " {{{1 + call append(0, [ + \ 'Hello from TemplateB function!', + \ a:context.name, + \ a:context.path_wiki, + \]) +endfunction + +" }}}1 +let g:wiki_root = g:testroot . '/wiki-basic' +let g:wiki_templates = [ + \ { + \ 'match_re': '^Template A', + \ 'source_filename': g:testroot . '/test-templates/template-a.md' + \ }, + \ { + \ 'match_re': '^Template B', + \ 'source_func': function('TemplateB') + \ }, + \ { + \ 'match_re': '^case titled template', + \ 'source_filename': g:testroot . '/test-templates/template-d.md' + \ }, + \ { + \ 'match_func': {_ -> v:true}, + \ 'source_func': {_ -> append(0, ['Fallback'])} + \ }, + \] + +runtime plugin/wiki.vim + +silent call wiki#page#open('Template A') +call assert_equal([ + \ '# TEMPLATE A', + \ 'Created: ' . strftime("%F") . ' ' . strftime("%H:%M"), + \], getline(1, line('$') - 1)) + +bwipeout +silent call wiki#page#open('Template B') +call assert_equal([ + \ 'Hello from TemplateB function!', + \ 'Template B', + \ 'Template B.wiki', + \], getline(1, line('$') - 1)) + +bwipeout +silent call wiki#page#open('Template C') +call assert_equal('Fallback', getline(1)) + +bwipeout +silent call wiki#page#open('case titled template') +call assert_equal(['# Case Titled Template'], getline(1, line('$') - 1)) + +call wiki#test#finished()