Skip to content

Commit

Permalink
Make the YAML parser configurable (#210)
Browse files Browse the repository at this point in the history
* Add 'yq' as an optional YAML parser

* rename 'engine' -> 'parser'

* Add configuration option

* default to native

* CHANGELOG.md
  • Loading branch information
epwalsh authored Oct 20, 2023
1 parent 96ac0d9 commit 93bc7cd
Show file tree
Hide file tree
Showing 12 changed files with 236 additions and 97 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- The `overwrite_mappings` option, which sets the mappings in the config even if they already exist
- Added support for multiple vaults (#128)
- Added command to switch between vaults (#60)
- Added configuration option `yaml_parser` (a string value of either "native" or "yq") to change the YAML parser.

### Fixed

Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ Built for people who love the concept of Obsidian -- a simple, markdown-based no

Search functionality (e.g. via the `:ObsidianSearch` and `:ObsidianQuickSwitch` commands) also requires [telescope.nvim](https://github.com/nvim-telescope/telescope.nvim) or one of the `fzf` alternatives (see [plugin dependencies](#plugin-dependencies) below).

You may also want to install [`yq`](https://github.com/mikefarah/yq) for more robust frontmatter YAML parsing.

### Install and configure

To configure obsidian.nvim you just need to call `require("obsidian").setup({ ... })` with the desired options.
Expand Down Expand Up @@ -304,6 +306,12 @@ This is a complete list of all of the options that can be passed to `require("ob
-- or replacing the current buffer (default)
-- Accepted values are "current", "hsplit" and "vsplit"
open_notes_in = "current"

-- Optional, set the YAML parser to use. The valid options are:
-- * "native" - uses a pure Lua parser that's fast but not very robust.
-- * "yq" - uses the command-line tool yq (https://github.com/mikefarah/yq), which is more robust
-- but slower and needs to be installed separately.
yaml_parser = "native",
}
```

Expand Down
2 changes: 2 additions & 0 deletions lua/obsidian/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ local config = {}
---@field sort_by string|?
---@field sort_reversed boolean|?
---@field open_notes_in "current"|"vsplit"|"hsplit"
---@field yaml_parser string|?
config.ClientOpts = {}

---Get defaults.
Expand Down Expand Up @@ -54,6 +55,7 @@ config.ClientOpts.default = function()
sort_by = "modified",
sort_reversed = true,
open_notes_in = "current",
yaml_parser = "native",
}
end

Expand Down
4 changes: 4 additions & 0 deletions lua/obsidian/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ obsidian.VERSION = "1.14.2"
obsidian.completion = require "obsidian.completion"
obsidian.note = require "obsidian.note"
obsidian.util = require "obsidian.util"
obsidian.yaml = require "obsidian.yaml"
obsidian.mapping = require "obsidian.mapping"
obsidian.workspace = require "obsidian.workspace"

Expand All @@ -32,6 +33,9 @@ obsidian.new = function(opts)
self.dir = Path:new(self.current_workspace.path)
self.opts = opts
self.backlinks_namespace = vim.api.nvim_create_namespace "ObsidianBacklinks"
if self.opts.yaml_parser ~= nil then
obsidian.yaml.set_parser(self.opts.yaml_parser)
end

return self
end
Expand Down
117 changes: 117 additions & 0 deletions lua/obsidian/yaml/init.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
local util = require "obsidian.util"

local yaml = {}

yaml.parsers = {
["native"] = require "obsidian.yaml.native",
["yq"] = require "obsidian.yaml.yq",
}

---@return string
local detect_parser = function()
if vim.fn.executable "yq" then
return "yq"
else
return "native"
end
end

yaml.parser = detect_parser()

---Set the YAML parser to use.
---@param parser string
yaml.set_parser = function(parser)
yaml.parser = parser
end

---Reset to the default parser.
yaml.reset_parser = function()
yaml.parser = detect_parser()
end

---Deserialize a YAML string.
---@param str string
---@return any
yaml.loads = function(str)
return yaml.parsers[yaml.parser].loads(str)
end

---@return string[]
local dumps
dumps = function(x, indent, order)
local indent_str = string.rep(" ", indent)

if type(x) == "string" then
-- TODO: make this more robust
if string.match(x, "%w") then
return { indent_str .. x }
else
return { indent_str .. [["]] .. x .. [["]] }
end
end

if type(x) == "boolean" then
return { indent_str .. tostring(x) }
end

if type(x) == "number" then
return { indent_str .. tostring(x) }
end

if type(x) == "table" then
local out = {}

if util.is_array(x) then
for _, v in ipairs(x) do
local item_lines = dumps(v, indent + 2)
table.insert(out, indent_str .. "- " .. util.strip(item_lines[1]))
for i = 2, #item_lines do
table.insert(out, item_lines[i])
end
end
else
-- Gather and sort keys so we can keep the order deterministic.
local keys = {}
for k, _ in pairs(x) do
table.insert(keys, k)
end
table.sort(keys, order)
for _, k in ipairs(keys) do
local v = x[k]
if type(v) == "string" or type(v) == "boolean" or type(v) == "number" then
table.insert(out, indent_str .. tostring(k) .. ": " .. dumps(v, 0)[1])
elseif type(v) == "table" and util.table_length(v) == 0 then
table.insert(out, indent_str .. tostring(k) .. ": []")
else
local item_lines = dumps(v, indent + 2)
table.insert(out, indent_str .. tostring(k) .. ":")
for _, line in ipairs(item_lines) do
table.insert(out, line)
end
end
end
end

return out
end

error("Can't convert object with type " .. type(x) .. " to YAML")
end

---Dump an object to YAML lines.
---@param x any
---@param order function
---@return string[]
yaml.dumps_lines = function(x, order)
return dumps(x, 0, order)
end

---Dump an object to a YAML string.
---@param x any
---@param order function|?
---@return string
yaml.dumps = function(x, order)
return table.concat(dumps(x, 0, order), "\n")
end

return yaml
76 changes: 0 additions & 76 deletions lua/obsidian/yaml.lua → lua/obsidian/yaml/native.lua
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
---Adapted from https://github.com/exosite/lua-yaml.git

local util = require "obsidian.util"

local Parser = {}

function Parser.new(self, tokens)
Expand Down Expand Up @@ -590,78 +588,4 @@ yaml.loads = function(str)
return parser:parse()
end

---@return string[]
local dumps
dumps = function(x, indent, order)
local indent_str = string.rep(" ", indent)

if type(x) == "string" then
-- TODO: handle double quotes in x
return { indent_str .. [["]] .. x .. [["]] }
end

if type(x) == "boolean" then
return { indent_str .. tostring(x) }
end

if type(x) == "number" then
return { indent_str .. tostring(x) }
end

if type(x) == "table" then
local out = {}

if util.is_array(x) then
for _, v in ipairs(x) do
local item_lines = dumps(v, indent + 2)
table.insert(out, indent_str .. "- " .. util.strip(item_lines[1]))
for i = 2, #item_lines do
table.insert(out, item_lines[i])
end
end
else
-- Gather and sort keys so we can keep the order deterministic.
local keys = {}
for k, _ in pairs(x) do
table.insert(keys, k)
end
table.sort(keys, order)
for _, k in ipairs(keys) do
local v = x[k]
if type(v) == "string" or type(v) == "boolean" or type(v) == "number" then
table.insert(out, indent_str .. tostring(k) .. ": " .. dumps(v, 0)[1])
elseif type(v) == "table" and util.table_length(v) == 0 then
table.insert(out, indent_str .. tostring(k) .. ": []")
else
local item_lines = dumps(v, indent + 2)
table.insert(out, indent_str .. tostring(k) .. ":")
for _, line in ipairs(item_lines) do
table.insert(out, line)
end
end
end
end

return out
end

error("Can't convert object with type " .. type(x) .. " to YAML")
end

---Dump an object to YAML lines.
---@param x any
---@param order function
---@return string[]
yaml.dumps_lines = function(x, order)
return dumps(x, 0, order)
end

---Dump an object to a YAML string.
---@param x any
---@param order function|?
---@return string
yaml.dumps = function(x, order)
return table.concat(dumps(x, 0, order), "\n")
end

return yaml
11 changes: 11 additions & 0 deletions lua/obsidian/yaml/yq.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
local m = {}

---@param str string
---@return any
m.loads = function(str)
local as_json = vim.fn.system("yq -o=json", str)
local data = vim.json.decode(as_json, { luanil = { object = true, array = true } })
return data
end

return m
6 changes: 3 additions & 3 deletions test/obsidian/note_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,11 @@ describe("Note", function()
table.concat(note:frontmatter_lines(), "\n"),
table.concat({
"---",
'id: "note_with_additional_metadata"',
"id: note_with_additional_metadata",
"aliases:",
' - "Note with additional metadata"',
" - Note with additional metadata",
"tags: []",
'foo: "bar"',
"foo: bar",
"---",
}, "\n")
)
Expand Down
Loading

0 comments on commit 93bc7cd

Please sign in to comment.