3

I keep all my tex files in a Dropbox directory, but different computers (all Windows-based) have different user names, so I would like to define a command in my template that is the Dropbox path, to which I can append paths for commands such as

\includegraphics{\Dropbox/Figures/fig1}
\input{\Dropbox/Macros/math_commands}

From Dropbox's website I found that the Dropbox path can be read from a JSON file at

%LOCALAPPDATA%\Dropbox\info.json

so I would like to parse the file (my file contains only a personal account, but this potentially would need to be specified in case the .json file contained additional accounts) and define a new command

\newcommand{\Dropbox}{%path from the json%}

The previous question on parsing JSON in LaTeX was about printing the contents, and I couldn't figure out how to adapt that to my use case.

In case it matters, I use lualatex through latexmk.

  • Maybe this can help you: tex.stackexchange.com/questions/30617/… – Michael Palmer Nov 26 '16 at 21:26
  • I agree that this could be done on a case-by-case basis using if-then statements. My old method was to just define commands for each machine, that would define \Dropbox to be the appropriate path. I was interesting in how to do this in a more automated way. – Shffl Nov 27 '16 at 3:13
6

It can be done with Lua. Prepare a file called dropboxpath.lua containing the following code:

json = dofile("json.lua")

function get_dropbox_folder(filename)
   -- The folder is different in windows and unix/osx
   if filename == nil then
       filename = os.getenv("LOCALAPPDATA")
   end
   if filename == nil then
       filename = os.getenv("HOME") .. "/.dropbox/info.json"
   else
       filename = filename .. "/Dropbox/info.json"
   end
   jsonfile = io.open(filename)
   config = jsonfile:read("*all")
   return (json.parse(config)["personal"]["path"])
end

In the same folder download the code at https://gist.github.com/tylerneylon/59f4bcf316be525b30ab (which I reproduce at the end of this answer, for convenience), under the filename json.lua.

Then, you can use all this as in the following example:

\documentclass{article}
\directlua{dofile("dropboxpath.lua")}
\edef\Dropbox{\directlua{tex.print(get_dropbox_folder())}}

\begin{document}
The Dropbox folder in this machine is \Dropbox.
\end{document}

Result:

Result

DISCLAIMER I tested this code in Linux and OSX, and works correctly in both. I don't tested it in Windows and I'm not sure if the path should use \ instead of / in that case.

Annex. Code of json.lua:

--[[ json.lua

A compact pure-Lua JSON library.
The main functions are: json.stringify, json.parse.

## json.stringify:

This expects the following to be true of any tables being encoded:
 * They only have string or number keys. Number keys must be represented as
   strings in json; this is part of the json spec.
 * They are not recursive. Such a structure cannot be specified in json.

A Lua table is considered to be an array if and only if its set of keys is a
consecutive sequence of positive integers starting at 1. Arrays are encoded like
so: `[2, 3, false, "hi"]`. Any other type of Lua table is encoded as a json
object, encoded like so: `{"key1": 2, "key2": false}`.

Because the Lua nil value cannot be a key, and as a table value is considerd
equivalent to a missing key, there is no way to express the json "null" value in
a Lua table. The only way this will output "null" is if your entire input obj is
nil itself.

An empty Lua table, {}, could be considered either a json object or array -
it's an ambiguous edge case. We choose to treat this as an object as it is the
more general type.

To be clear, none of the above considerations is a limitation of this code.
Rather, it is what we get when we completely observe the json specification for
as arbitrary a Lua object as json is capable of expressing.

## json.parse:

This function parses json, with the exception that it does not pay attention to
\u-escaped unicode code points in strings.

It is difficult for Lua to return null as a value. In order to prevent the loss
of keys with a null value in a json string, this function uses the one-off
table value json.null (which is just an empty table) to indicate null values.
This way you can check if a value is null with the conditional
`val == json.null`.

If you have control over the data and are using Lua, I would recommend just
avoiding null values in your data to begin with.

--]]


local json = {}


-- Internal functions.

local function kind_of(obj)
  if type(obj) ~= 'table' then return type(obj) end
  local i = 1
  for _ in pairs(obj) do
    if obj[i] ~= nil then i = i + 1 else return 'table' end
  end
  if i == 1 then return 'table' else return 'array' end
end

local function escape_str(s)
  local in_char  = {'\\', '"', '/', '\b', '\f', '\n', '\r', '\t'}
  local out_char = {'\\', '"', '/',  'b',  'f',  'n',  'r',  't'}
  for i, c in ipairs(in_char) do
    s = s:gsub(c, '\\' .. out_char[i])
  end
  return s
end

-- Returns pos, did_find; there are two cases:
-- 1. Delimiter found: pos = pos after leading space + delim; did_find = true.
-- 2. Delimiter not found: pos = pos after leading space;     did_find = false.
-- This throws an error if err_if_missing is true and the delim is not found.
local function skip_delim(str, pos, delim, err_if_missing)
  pos = pos + #str:match('^%s*', pos)
  if str:sub(pos, pos) ~= delim then
    if err_if_missing then
      error('Expected ' .. delim .. ' near position ' .. pos)
    end
    return pos, false
  end
  return pos + 1, true
end

-- Expects the given pos to be the first character after the opening quote.
-- Returns val, pos; the returned pos is after the closing quote character.
local function parse_str_val(str, pos, val)
  val = val or ''
  local early_end_error = 'End of input found while parsing string.'
  if pos > #str then error(early_end_error) end
  local c = str:sub(pos, pos)
  if c == '"'  then return val, pos + 1 end
  if c ~= '\\' then return parse_str_val(str, pos + 1, val .. c) end
  -- We must have a \ character.
  local esc_map = {b = '\b', f = '\f', n = '\n', r = '\r', t = '\t'}
  local nextc = str:sub(pos + 1, pos + 1)
  if not nextc then error(early_end_error) end
  return parse_str_val(str, pos + 2, val .. (esc_map[nextc] or nextc))
end

-- Returns val, pos; the returned pos is after the number's final character.
local function parse_num_val(str, pos)
  local num_str = str:match('^-?%d+%.?%d*[eE]?[+-]?%d*', pos)
  local val = tonumber(num_str)
  if not val then error('Error parsing number at position ' .. pos .. '.') end
  return val, pos + #num_str
end


-- Public values and functions.

function json.stringify(obj, as_key)
  local s = {}  -- We'll build the string as an array of strings to be concatenated.
  local kind = kind_of(obj)  -- This is 'array' if it's an array or type(obj) otherwise.
  if kind == 'array' then
    if as_key then error('Can\'t encode array as key.') end
    s[#s + 1] = '['
    for i, val in ipairs(obj) do
      if i > 1 then s[#s + 1] = ', ' end
      s[#s + 1] = json.stringify(val)
    end
    s[#s + 1] = ']'
  elseif kind == 'table' then
    if as_key then error('Can\'t encode table as key.') end
    s[#s + 1] = '{'
    for k, v in pairs(obj) do
      if #s > 1 then s[#s + 1] = ', ' end
      s[#s + 1] = json.stringify(k, true)
      s[#s + 1] = ':'
      s[#s + 1] = json.stringify(v)
    end
    s[#s + 1] = '}'
  elseif kind == 'string' then
    return '"' .. escape_str(obj) .. '"'
  elseif kind == 'number' then
    if as_key then return '"' .. tostring(obj) .. '"' end
    return tostring(obj)
  elseif kind == 'boolean' then
    return tostring(obj)
  elseif kind == 'nil' then
    return 'null'
  else
    error('Unjsonifiable type: ' .. kind .. '.')
  end
  return table.concat(s)
end

json.null = {}  -- This is a one-off table to represent the null value.

function json.parse(str, pos, end_delim)
  pos = pos or 1
  if pos > #str then error('Reached unexpected end of input.') end
  local pos = pos + #str:match('^%s*', pos)  -- Skip whitespace.
  local first = str:sub(pos, pos)
  if first == '{' then  -- Parse an object.
    local obj, key, delim_found = {}, true, true
    pos = pos + 1
    while true do
      key, pos = json.parse(str, pos, '}')
      if key == nil then return obj, pos end
      if not delim_found then error('Comma missing between object items.') end
      pos = skip_delim(str, pos, ':', true)  -- true -> error if missing.
      obj[key], pos = json.parse(str, pos)
      pos, delim_found = skip_delim(str, pos, ',')
    end
  elseif first == '[' then  -- Parse an array.
    local arr, val, delim_found = {}, true, true
    pos = pos + 1
    while true do
      val, pos = json.parse(str, pos, ']')
      if val == nil then return arr, pos end
      if not delim_found then error('Comma missing between array items.') end
      arr[#arr + 1] = val
      pos, delim_found = skip_delim(str, pos, ',')
    end
  elseif first == '"' then  -- Parse a string.
    return parse_str_val(str, pos + 1)
  elseif first == '-' or first:match('%d') then  -- Parse a number.
    return parse_num_val(str, pos)
  elseif first == end_delim then  -- End of an object or array.
    return nil, pos + 1
  else  -- Parse true, false, or null.
    local literals = {['true'] = true, ['false'] = false, ['null'] = json.null}
    for lit_str, lit_val in pairs(literals) do
      local lit_end = pos + #lit_str - 1
      if str:sub(pos, lit_end) == lit_str then return lit_val, lit_end + 1 end
    end
    local pos_info_str = 'position ' .. pos .. ': ' .. str:sub(pos, pos + 10)
    error('Invalid json syntax starting at ' .. pos_info_str)
  end
end

return json

Update

According to the OP, the above code requires a modification to work under Windows (to change the folder separator to \).

In addition, it does not work if the .lua files are not stored in the same folder than the main document.

The following code solves both issues:

-- This is dropboxpath.lua
json = dofile(kpse.find_file("json.lua"))

function get_dropbox_folder(filename)
   if filename == nil then
       filename = os.getenv("LOCALAPPDATA")
   end
   if filename == nil then
       filename = os.getenv("HOME") .. "/.dropbox/info.json"
   else
       filename = filename .. "/Dropbox/info.json"
   end
   jsonfile = io.open(filename)
   config = jsonfile:read("*all")
   path = json.parse(config)["personal"]["path"]
   path = string.gsub(path, "\\", "/")
   return (path)
end

Hiding the loading of lua and definition of \Dropbox command in a separate .sty file:

% This is dropboxpath.sty
\directlua{dofile(kpse.find_file('dropboxpath.lua'))}
\edef\Dropbox{\directlua{tex.print(get_dropbox_folder())}}

The new "main.tex" is simpler:

% This is dropboxtest.tex
\documentclass{article}
\usepackage{dropboxpath}
\begin{document}
The Dropbox folder in this machine is \Dropbox.
\end{document}
  • I believe that the paths should use "/". – Shffl Nov 27 '16 at 3:14
  • To get it to work, i modified dropboxpath.lua with the following: path = json.parse(config)["personal"]["path"] path = string.gsub(path, "\\", "/") return(path) If you could confirm that this has no effect on functionality in Linux or OSX, I think this would be the answer. – Shffl Nov 27 '16 at 3:22
  • I'm also unsure of how the .lua files can be incorporated into a MiKTeX root directory. It worked when I had the .lua files in the same folder as the .tex file. However, when I tried moving the .lua files into my user-managed root directory, it wouldn't work and \Dropbox was undefined. – Shffl Nov 27 '16 at 3:33
  • @Shffl Updated, see if it works now in your machine – JLDiaz Nov 27 '16 at 19:09
  • Works. Flagged as answer. – Shffl Nov 28 '16 at 18:25

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.