Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: port metadata feature to lua #80

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 3 additions & 122 deletions autoload/corpus.vim
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,9 @@ let s:chooser_window=v:null

let s:preview_buffer=v:null
let s:preview_window=v:null

" When opening a new file in a Corpus-managed location, pre-populate it
" with metadata of the form:
"
" ---
" title: Title based on file name
" tags: wiki
" ---
"
" or:
"
" ---
" title: Title based on file name
" ---
"
function! corpus#buf_new_file() abort
let l:file=v:lua.corpus.normalize('<afile>')
call corpus#update_metadata(l:file)
call v:lua.corpus.metadata.update(l:file)
let b:corpus_new_file=1
endfunction

Expand All @@ -48,7 +33,7 @@ endfunction
function! corpus#buf_write_pre() abort
let l:file=v:lua.corpus.normalize('<afile>')
call corpus#update_references(l:file)
call corpus#update_metadata(l:file)
call v:lua.corpus.metadata.update(l:file)
endfunction

" Minimal subset of:
Expand Down Expand Up @@ -129,57 +114,6 @@ function! corpus#extract_reference_links(line) abort
return l:reference_links
endfunction

let s:metadata_key_value_pattern='\v^\s*(\w+)\s*:\s*(\S.{-})\s*$'

" Returns raw metadata as a list of strings; eg:
"
" ['tags: wiki', 'title: foo']
"
" If there is no valid metadata, an empty list is returned.
"
" If there are blank lines in the metadata, they are included in the list.
function! corpus#get_metadata_raw() abort
if match(getline(1), '\v^---\s*$') != -1
let l:metadata=[]
for l:i in range(2, line('$'))
let l:line=getline(l:i)
if match(l:line, '\v^\s*$') != -1
call add(l:metadata, '')
continue
elseif match(l:line, '\v^---\s*$') != -1
return l:metadata
endif
let l:match=matchlist(l:line, s:metadata_key_value_pattern)
if len(l:match) == 0
return []
endif
call add(l:metadata, (l:match[0]))
endfor
endif
return []
endfunction

" Returns metadata as a dictionary; eg:
"
" {'tags': 'wiki', 'title': 'foo'}
"
" If there is no valid metadata, an empty dictionary is returned.
function! corpus#get_metadata() abort
let l:raw=corpus#get_metadata_raw()
if len(l:raw)
let l:metadata={}
for l:line in l:raw
let l:match=matchlist(l:line, s:metadata_key_value_pattern)
if len(l:match)
let l:metadata[l:match[1]]=l:match[2]
endif
endfor
return l:metadata
else
return {}
endif
endfunction

function! corpus#goto(mode) abort
if a:mode == 'v'
" Visual mode.
Expand Down Expand Up @@ -315,34 +249,6 @@ function! corpus#goto(mode) abort
endif
endfunction

function! corpus#set_metadata(metadata) abort
" Remove old metadata, if present.
let l:raw=corpus#get_metadata_raw()
if (len(l:raw))
" +2 lines for the '---' delimiters.
call deletebufline('', 1, len(l:raw) + 2)
endif

" Format new metadata.
let l:lines=['---']
let l:keys=keys(a:metadata)
for l:key in l:keys
call add(l:lines, l:key . ': ' . a:metadata[l:key])
endfor
call add(l:lines, '---')

" Prepend new metadata.
call append(0, l:lines)

" Make sure there is at least one blank line after metadata.
" +2 lines for the '---' delimiters.
" +1 more to see next line.
let l:next=len(l:keys) + 2 + 1
if match(getline(l:next), '\v^\s*$') == -1
call append(l:next - 1, '')
endif
endfunction

function! corpus#test() abort
let v:errors=[]

Expand Down Expand Up @@ -373,7 +279,7 @@ function! corpus#update_references(file) abort
endif

" Skip over metadata.
let l:raw=corpus#get_metadata_raw()
let l:raw=v:lua.corpus.metadata.raw()
if len(l:raw)
let l:start=len(l:raw) + 2 + 1
else
Expand Down Expand Up @@ -455,31 +361,6 @@ function! corpus#update_references(file) abort
endfor
endfunction

function! corpus#update_metadata(file) abort
let l:config=v:lua.corpus.config_for_file(a:file)
if !get(l:config, 'autotitle', 0) && !has_key(l:config, 'tags')
return
endif

let l:metadata=corpus#get_metadata()

if get(l:config, 'autotitle', 0)
let l:title=v:lua.corpus.title_for_file(a:file)
let l:metadata.title=l:title
endif

if has_key(l:config, 'tags')
let l:tags=split(get(l:metadata, 'tags', ''))
for l:tag in l:config.tags
if index(l:tags, l:tag) == -1
call add(l:tags, l:tag)
endif
endfor
let l:metadata.tags=join(l:tags)
endif
call corpus#set_metadata(l:metadata)
endfunction

function! corpus#complete(arglead, cmdline, cursor_pos) abort
return v:lua.corpus.complete(a:arglead, a:cmdline, a:cursor_pos)
endfunction
1 change: 1 addition & 0 deletions lua/corpus/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ end
-- TODO make most of these private (really only want them public for testing
-- during development)
corpus = {
metadata = require'corpus.metadata',
choose = function(selection)
selection = vim.trim(selection)
local file = corpus.get_selected_file()
Expand Down
182 changes: 182 additions & 0 deletions lua/corpus/metadata.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
-- Copyright 2015-present Greg Hurrell. All rights reserved.
-- Licensed under the terms of the MIT license.

--- TODO: would it be worth it to embed
--- https://github.com/exosite/lua-yaml/blob/master/yaml.lua?
local M = {}

---YAML Key-value pair pattern
local kv_pattern = [[\v^\s*(\w+)\s*:\s*(\S.{-})\s*$]]

---Given a list of key value pairs, update key with value
-- @param key string
-- @param value string
local set_kv_list = function (list, key, value)
for _, kv in ipairs(list) do
if kv[1] == key then kv[2] = value end
end
end

---Get value from key-value pair list
---@param list any
---@return string?
local get_kv_list = function(list, key)
for _, kv in ipairs(list) do
if kv[1] == key then return kv[2] end
end
end

---Check whether current line is the start of metadata block or not
---@param line string
---@return boolean: true if it's the start/end of metadata block
local is_block_delim = function(line)
return vim.fn.match(line, [[\v^---\s*$]]) ~= -1
end

---Convert Lua value to Yaml value
---@return string
local yaml_value = (function ()
local convert = {
['boolean'] = function (lua_value)
return vim.inspect(lua_value) -- yah maybe overkill :D
end,
['table'] = function(lua_value)
if vim.tbl_islist(lua_value) then
return ("[%s]"):format(table.concat(lua_value, ","))
end
end
}
return function (value)
return convert(type(value))(value)
end
end)

---Wrapper function for vim.fn.match for convenience.
---@param line string: line to match against
---@param pattern string: regex
---@return boolean: true if match
local match = function(line, pattern)
return vim.fn.match(line, pattern) ~= -1
end

--- Read current buffer and dump yaml block lines into a list
---@return table: list of string
M.raw = function()
if is_block_delim(vim.fn.getline(1)) then
local res = {}
local range = vim.fn.range(2, vim.fn.line('$'))

for _, i in ipairs(range) do

local line = vim.fn.getline(i)
if match(line, [[\v^\s*$]]) then
res[#res+1] = ''
elseif is_block_delim(line) then
return res
end

local match = vim.fn.matchlist(line, kv_pattern)
if #match == 0 then
return {}
end
res[#res+1] = match[1]

end
end

return {}
end

--- Read current buffer and returns a metadata as list of key values pairs
---@return table: eg: { {"title", "README"}, {"tags", "foo"} }
M.decode = function()
local res = {}
local raw = M.raw()
if #raw == 0 then return {} end

for _, line in ipairs(raw) do
local match = vim.fn.matchlist(line,kv_pattern)
if #match ~= 0 then
res[#res+1] = { match[2], match[3] }
end
end

return res
end

--- Encode lua table into yaml string
---@param metadata table: list of key-values pairs
---@return table: YAML string
M.encode = function(metadata)
local res = {"---"}
for _, kv in ipairs(metadata) do
local key, value = kv[1], kv[2]
if type(value) ~= "string" then
value = yaml_value(value)
end
table.insert(res, ("%s: %s"):format(key, value))
end
table.insert(res, "---")
return res
end

---Update title to match the filename (TODO: or the other way around)
---@param config table: file or user configuration
---@param path string: file path
---@param metadata table: file metadata
M.__update_title = function(config, path, metadata)
--- TODO: Check if title is different from file name
--- and if so update filename not fontmatter.
if config.autotitle ~= 1 then return end
local title = corpus.title_for_file(path)
if #metadata ~= 0 then
set_kv_list(metadata, "title", title)
else
metadata[#metadata+1] = {"title", title}
end
end


---Update tags to contain user or configuration tags
---@param config table: file or user configuration
---@param path string: file path
---@param metadata table: file metadata
M.__update_tags = function(config, path, metadata)
if not config.tags then return end
local tags = vim.split(get_kv_list(metadata, "tags") or "", " ")

for _, tag in ipairs(config.tags) do
if vim.fn.index(tags, tag) == -1 then
tags[#tags+1] = tag
end
end

set_kv_list(metadata, "tags", tags)
end

M.__update_buf = function(metadata)
local yaml = M.encode(metadata)
local raw = M.raw()

if #raw ~= 0 then
vim.fn.deletebufline('', 1, #raw +2)
end

vim.fn.append(0, yaml)

end

---Main function in metadata module. It update title, tags and metadata
---@param path string: filepath
M.update = function(path)
local config = corpus.config_for_file(path)
if config.autotitle ~= 1 and not config.tags then return end

local metadata = M.decode()

M.__update_title(config, path, metadata)
M.__update_tags(config, path, metadata)
M.__update_buf(metadata)
end

return M