Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@xziyue
Created July 7, 2020 23:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save xziyue/bee7635d3612cfe92cb5a62ff419cbb2 to your computer and use it in GitHub Desktop.
Save xziyue/bee7635d3612cfe92cb5a62ff419cbb2 to your computer and use it in GitHub Desktop.
Mimicking file system input in LuaTeX
\RequirePackage{luacode, luatexbase}
\begin{luacode*}
TeXFileBuffer = {content={}, finished=false}
function TeXFileBuffer:new()
o = {}
setmetatable(o, self)
self.__index = self
return o
end
function TeXFileBuffer:clear()
while #self.content ~= 0 do rawset(self.content, #self.content, nil) end
end
function TeXFileBuffer:content_to_string()
return table.concat(self.content, "")
end
function TeXFileBuffer:use()
tex.write(self:content_to_string())
end
function TeXFileBuffer:append(data)
table.insert(self.content, data)
end
function TeXFileBuffer:append_carriage_return(data)
self:append("\r")
end
function _tex_buffer_remove_callback(name, description)
for k, v in pairs(luatexbase.callback_descriptions(name)) do
if v == description then
texio.write("\nsafely removing callback " .. name .. " : " .. description)
luatexbase.remove_from_callback(name, description)
end
end
end
function tex_buffer_remove_callback()
_tex_buffer_remove_callback("find_read_file", "tex_file_buffer_find")
_tex_buffer_remove_callback("open_read_file", "tex_file_buffer_read")
end
function tex_file_buffer_reader(env)
local ret = nil
if not env["finished"] then
ret = env["content"]
env["finished"] = true
-- remove callback immediately
tex_buffer_remove_callback()
end
return ret
end
function tex_file_buffer_find(id_number, asked_name)
-- arguments and return value doesn't matter
texio.write("\nTeXFileBuffer tries to find ".. asked_name .. " id=" .. id_number)
return asked_name
end
function TeXFileBuffer:register_callback()
tex_file_buffer_read = function (filename)
local env = {}
texio.write("\nTeXFileBuffer opens ".. filename)
env["finished"] = false
env["content"] = self:content_to_string()
env["reader"] = tex_file_buffer_reader
return env
end
-- register callback
luatexbase.add_to_callback("find_read_file", tex_file_buffer_find, "tex_file_buffer_find")
luatexbase.add_to_callback("open_read_file", tex_file_buffer_read, "tex_file_buffer_read")
end
-- create a TeXFilebuffer instance
tex_file_buffer = TeXFileBuffer:new()
\end{luacode*}
\makeatletter
\newcommand{\TFBInputAsFile}{
\directlua{tex_file_buffer:register_callback()}
% read some random file, which automatically removes the callback
% \input will do an file existance check before actually reading it.
% therefore, if using LaTeX's input, `random-file` will be opned twice
% here, I am using TeX's \@@input primitive instead
\@@input randomfile
}
\makeatother
\newcommand{\TFBAppend}[1]{
\directlua{tex_file_buffer:append("\luaescapestring{\unexpanded{#1}}")}
}
\newcommand{\TFBAppendCR}{
\directlua{tex_file_buffer:append_carriage_return()}
}
\newcommand{\TFBClear}{
\directlua{tex_file_buffer:clear()}
}
\newcommand{\TFBUse}{
\directlua{tex_file_buffer:use()}
}
@hugmyndakassi
Copy link

Using something like the following, inspired and modified from this question:

luadoc = [[ some multi-line virtual Lua document here ]]
vfs = { ["SomeMoniker"] = luadoc }

function get_vfs_realname(name)
    local pfx = "vfs." -- virtual "file" prefix
    if name:find(pfx, 1, true) == 1 then
        local lookup_name = name:sub(#pfx + 1, #name)
        if vfs[lookup_name] ~= nil and type(vfs[lookup_name]) == "string" then
            return lookup_name
        end
    end
    return nil -- not found
end

function hook_find_read_file(id, name)
    kpse_name = kpse.find_file(name)
    if kpse_name then
        return kpse_name
    end
    local rname = get_vfs_realname(name)
    if rname then
        return name
    end
end

function emulate_default_open_read_file(name)
    return {
        ["fhandle"] = assert(io.open(name, "r")),
        ["reader"] = function(self)
                return self.fhandle:read( "*l")
            end,
        ["close"] = function(self)
                self.fhandle:close()
            end,
    }
end

function make_vfs_file(content)
    return {
        ["content"] = content,
        ["reader"] = function(self)
            local ret = self.content
            if ret ~= nil then
                self.content = nil
            end
            return ret
        end
    }
end

function hook_open_read_file(name)
    kpse_name = kpse.find_file(name)
    if kpse_name then
        return emulate_default_open_read_file(kpse_name)
    end
    local rname = get_vfs_realname(name)
    if rname then
        return make_vfs_file(vfs[rname])
    end
end

luatexbase.add_to_callback("find_read_file", hook_find_read_file, "vfs")
luatexbase.add_to_callback("open_read_file", hook_open_read_file, "vfs")

Should allow you to get rid of the need for adding/removing the callbacks all the time.

make_vfs_file() takes a string and returns an env with env["content"] and env["reader"], whereas the content is captured this way in env. The reader is generic, taking the env in as its first argument, checking if env.content is nil and returning its value if it isn't. Then it sets env.content=nil to make sure that it doesn't return it a second time around.

Still not very robust, depending on the packages you use (because of what \directlua does under the hood), but this should allow you to use "virtual" input with:

\input{vfs.SomeMoniker}

PS: Haven't tested the code. Rather typed it here into GitHub (twice, because my browser had a hiccup).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment