From 96ac0d94d12b06010ceaa18d203cd4a4c41246f2 Mon Sep 17 00:00:00 2001 From: epwalsh Date: Fri, 20 Oct 2023 14:44:49 -0700 Subject: [PATCH] Fix some YAML parser issues Fixes some of the issues brought up in #209. --- CHANGELOG.md | 4 +++- lua/cmp_obsidian.lua | 6 ++--- lua/obsidian/backlinks.lua | 4 ++-- lua/obsidian/command.lua | 6 ++--- lua/obsidian/init.lua | 6 +---- lua/obsidian/note.lua | 8 +++---- lua/obsidian/yaml.lua | 47 +++++++++++++++++++++++++++++++++++-- test/obsidian/yaml_spec.lua | 11 +++++++++ 8 files changed, 72 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45b836d1a..1174b9123 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- Eliminated silent runtime errors on validation errors in note.from_lines +- Eliminated silent runtime errors on validation errors in `note.from_lines`. +- Fixed parsing YAML boolean values in frontmatter. +- Fixed parsing implicit null values in YAML frontmatter. ## [v1.14.2](https://github.com/epwalsh/obsidian.nvim/releases/tag/v1.14.2) - 2023-09-25 diff --git a/lua/cmp_obsidian.lua b/lua/cmp_obsidian.lua index 408b2400b..8d94db910 100644 --- a/lua/cmp_obsidian.lua +++ b/lua/cmp_obsidian.lua @@ -21,7 +21,7 @@ source.complete = function(self, request, callback) if can_complete and search ~= nil and #search >= opts.completion.min_chars then local items = {} for note in client:search(search, "--ignore-case") do - local aliases = util.unique { note.id, note:display_name(), unpack(note.aliases) } + local aliases = util.unique { tostring(note.id), note:display_name(), unpack(note.aliases) } for _, alias in pairs(aliases) do local options = {} @@ -37,8 +37,8 @@ source.complete = function(self, request, callback) table.insert(options, alias) for _, option in pairs(options) do - local label = "[[" .. note.id - if option ~= note.id then + local label = "[[" .. tostring(note.id) + if option ~= tostring(note.id) then label = label .. "|" .. option .. "]]" else label = label .. "]]" diff --git a/lua/obsidian/backlinks.lua b/lua/obsidian/backlinks.lua index a878a340e..028701c0c 100644 --- a/lua/obsidian/backlinks.lua +++ b/lua/obsidian/backlinks.lua @@ -96,7 +96,7 @@ backlinks.gather = function(self) local backlink_matches = {} local last_path = nil local last_note = nil - for match in util.search(self.client.dir, "[[" .. self.note.id) do + for match in util.search(self.client.dir, "[[" .. tostring(self.note.id)) do if match == nil then break elseif is_valid_backlink(match) then @@ -189,7 +189,7 @@ backlinks.view = function(self) -- Add highlights for all refs in the text. for i, ref_idx in ipairs(ref_indices) do local ref_str = ref_strs[i] - if string.find(ref_str, self.note.id, 1, true) ~= nil then + if string.find(ref_str, tostring(self.note.id), 1, true) ~= nil then table.insert(highlights, { group = "Search", line = #view_lines - 1, diff --git a/lua/obsidian/command.lua b/lua/obsidian/command.lua index 5fcb46385..5f3446a5a 100644 --- a/lua/obsidian/command.lua +++ b/lua/obsidian/command.lua @@ -189,7 +189,7 @@ command.backlinks = function(client, _) end) if ok then echo.info( - ("Showing backlinks '%s'. Hit ENTER on a line to follow the backlink."):format(backlinks.note.id), + ("Showing backlinks '%s'. Hit ENTER on a line to follow the backlink."):format(tostring(backlinks.note.id)), client.opts.log_level ) backlinks:view() @@ -440,7 +440,7 @@ command.link_new = function(client, data) line = string.sub(line, 1, cscol - 1) .. "[[" - .. note.id + .. tostring(note.id) .. "|" .. string.sub(line, cscol, cecol) .. "]]" @@ -480,7 +480,7 @@ command.link = function(client, data) line = string.sub(line, 1, cscol - 1) .. "[[" - .. note.id + .. tostring(note.id) .. "|" .. string.sub(line, cscol, cecol) .. "]]" diff --git a/lua/obsidian/init.lua b/lua/obsidian/init.lua index 9690dcd0c..848a1cb5b 100644 --- a/lua/obsidian/init.lua +++ b/lua/obsidian/init.lua @@ -272,12 +272,8 @@ client.parse_title_id_path = function(self, title, id, dir) -- Trim whitespace. title = title:match "^%s*(.-)%s*$" - if title == "" then - title = nil - end - - -- Remove suffix. if title:match "%.md" then + -- Remove suffix. title = title:sub(1, title:len() - 3) title_is_path = true end diff --git a/lua/obsidian/note.lua b/lua/obsidian/note.lua index d285739a2..839508d25 100644 --- a/lua/obsidian/note.lua +++ b/lua/obsidian/note.lua @@ -6,7 +6,7 @@ local echo = require "obsidian.echo" local SKIP_UPDATING_FRONTMATTER = { "README.md", "CONTRIBUTING.md", "CHANGELOG.md" } ---@class obsidian.Note ----@field id string +---@field id string|integer ---@field aliases string[] ---@field tags string[] ---@field path Path|? @@ -17,7 +17,7 @@ local note = {} ---Create new note. --- ----@param id string +---@param id string|number ---@param aliases string[] ---@param tags string[] ---@param path string|Path|? @@ -147,7 +147,7 @@ note.display_name = function(self) if #self.aliases > 0 then return self.aliases[#self.aliases] end - return self.id + return tostring(self.id) end ---Initialize a note from an iterator of lines. @@ -215,7 +215,7 @@ note.from_lines = function(lines, path, root) ---@diagnostic disable-next-line: param-type-mismatch for k, v in pairs(data) do if k == "id" then - if type(v) == "string" then + if type(v) == "string" or type(v) == "number" then id = v else echo.warn("Invalid 'id' in frontmatter for " .. tostring(path)) diff --git a/lua/obsidian/yaml.lua b/lua/obsidian/yaml.lua index 1e0149fc2..bf0390869 100644 --- a/lua/obsidian/yaml.lua +++ b/lua/obsidian/yaml.lua @@ -45,7 +45,7 @@ local context = function(str) end local word = function(w) - return "^(" .. w .. ")([%s$%c])" + return "^(" .. w .. ")([%s%c])" end local tokens = { @@ -542,9 +542,52 @@ end local yaml = {} +local count_indent = function(str) + local indent = 0 + for i = 1, #str do + if string.sub(i, i) == " " then + indent = indent + 1 + else + break + end + end + return indent +end + +local preprocess = function(str) + local lines = {} + local current_indent = 0 + for line in str:gmatch "[^\r\n]+" do + line = string.gsub(line, "%s+$", "") + local indent = count_indent(line) + + -- HACK: If the previous line is something like 'foo:' and the current line has the same indent + -- but is not a list item, then we change the previous line to 'foo: nil' since otherwise the parser + -- would not parse that correctly. + if + indent == current_indent + and lines[#lines] ~= nil + and lines[#lines]:sub(#lines[#lines]) == ":" + and not line:match "^%s*-" + then + lines[#lines] = lines[#lines] .. " null" + end + + table.insert(lines, line) + current_indent = indent + end + str = table.concat(lines, "\n") + if str:sub(#str) ~= "\n" then + -- Need a new line at the end for some parsing to work. + str = str .. "\n" + end + return str +end + ---Deserialize a YAML string. yaml.loads = function(str) - return Parser:new(tokenize(str)):parse() + local parser = Parser:new(tokenize(preprocess(str))) + return parser:parse() end ---@return string[] diff --git a/test/obsidian/yaml_spec.lua b/test/obsidian/yaml_spec.lua index b2ecf1da4..22bb42053 100644 --- a/test/obsidian/yaml_spec.lua +++ b/test/obsidian/yaml_spec.lua @@ -74,4 +74,15 @@ describe("obsidian.yaml", function() assert.equals(#data.aliases, 1) assert.equals(data.aliases[1], "Foo Baz") end) + it("should parse boolean field values", function() + local data = yaml.loads "complete: false" + assert.equals(type(data), "table") + assert.equals(type(data.complete), "boolean") + end) + it("should parse implicit null values", function() + local data = yaml.loads "tags: \ncomplete: false" + assert.equals(type(data), "table") + assert.equals(data.tags, nil) + assert.equals(data.complete, false) + end) end)