我想使用 odsfile 包和 tabularray 包来包含一个 ods 文件。但是这会导致错误Misplaced alignment tab character &
。这是一个 M(n)WE:
%% Compile with lualatex
\documentclass{article}
\usepackage{odsfile}
\usepackage{tabularray}
\tabletemplate{tblr}{%
\begin{tblr}{-{coltypes}}
-{content}
\end{tblr}
}
\begin{document}
%% Works:
\begin{tabular}{ll}
\includespread[file=data.ods,range=a1:b3]
\end{tabular}
%% This is what I want (in particular automaticaly determining the number of columns), but doesn't work
\begin{tblr}{}
\includespread[file=data.ods,range=a1:b3]
\end{tblr}
%% For testing purposes I specified the column types (which I don't want), but it works.
\begin{tblr}{ll}
\includespread[file=data.ods,range=a1:b3]
\end{tblr}
%% This works, but I don't want to use a template in this case
\includespread[file=data.ods,range=a1:b3,template=tblr]
\end{document}
文件 data.ods
仅包含:
X Y
1 a
2 b
错误如下:
1 & a\\ 2 & b\\
X & Y\\ 1 & a\\ 2 & b\\
! Misplaced alignment tab character &.
l.1 X &
Y\\ 1 & a\\ 2 & b\\
l.24 \end
{tblr}
我怎样才能让它工作?
编辑正如 michal.h21 的回答所表明的那样,找到一个完整的解决方案似乎相当复杂。因此,我尝试提供一些我想要实现的具体目标的背景信息:
%% Compile with lualatex
\documentclass{article}
\usepackage{odsfile}
\usepackage{tabularray}
\begin{document}
\begin{tabular}{cc}
\includespread[file=data.ods,range=a1:b1]
\includespread[file=data.ods,range=a3:b3]
%% Some more rows...
\end{tabular}
\end{document}
此示例的要点是,它选择了 odsfile 的第一行和第三行(可能还有一些非连续行),并将它们合并为一个 LaTeX 表。
我希望它能与 tblr 一起工作,因为我不想每次都计算行数(这对于较大的项目来说很简单但很烦人),而且因为我想使用 tabularray 的强大功能来修改表格的外观。
因此,如果这可以与模板一起使用,我认为它对我当前的用例来说就足够了。
答案1
有几个问题。首先,Tabularray 的内容总体上存在问题,从 Lua 传递。我无法以扩展它的方式将其传递\includespread
给tblr
环境,我总是得到一个错误。恐怕使用template
选项的方式\includespread
是唯一可行的。
另一个问题是由于odsfile
不使用\\
来分隔表中的行,而是使用自己的命令\OdsNl
和\OdsLastNl
。由于tblr
需要明确的\\
字符,即使您没有收到编译错误,也会得到错误的表格。
以下是 的修复版本odsfile.sty
,其中添加了两个新选项newline
和\lastnewline
。通过它们,您可以指定使用以下\\
命令:
% Package odsfile. Author Michal Hoftich <[email protected]>
% This package is subject of LPPL license, version 1.3c
\ProvidesPackage{odsfile}[2023/09/07 v0.8 odsfile package to select cells from ODS sheets and
typeset them as LaTeX tables]
\RequirePackage{luacode,xkeyval,xparse}
%keyval keys
\define@key{includespread}{file}{\loadodsfile{#1}}%
\define@key{includespread}{sheet}{\luaexec{sheetname = "\luatexluaescapestring{#1}"}}%
\define@key{includespread}{range}{\luaexec{range="\luatexluaescapestring{#1}"}}%
\define@key{includespread}{template}{\luaexec{currenttemplate="\luatexluaescapestring{#1}"}}%
\define@key{includespread}{rowtemplate}{\luaexec{rowtemplate="\luatexluaescapestring{\detokenize{#1}}"}}%
\define@key{includespread}{celltemplate}{\luaexec{celltpl="\luatexluaescapestring{\detokenize{#1}}"}}%
\define@key{includespread}{multicoltemplate}{\luaexec{multicoltpl="\luatexluaescapestring{\unexpanded{#1}}"}}%
\define@key{includespread}{escape}{\luaexec{latexescape="\luatexluaescapestring{\unexpanded{#1}}"}}%
\newcommand\OdsNl{\\}
\newcommand\OdsLastNl{\\}
\define@choicekey*+{includespread}{rowseparator}[\val\nr]{tableline,hline,newline}[tableline]%
{%
\ifcase\nr\relax%
\luaexec{rowseparator=""}%
\or%
\luaexec{rowseparator="\\hline "}%
\or%
\luaexec{%
rowseparator=[[\\n]]
}
\fi%
}{%
\luaexec{rowseparator="\luatexluaescapestring{#1}"}
}
\define@key{includespread}{columnbreak}{%
\luaexec{columnbreak="\luatexluaescapestring{\unexpanded{#1}}{}"}%
}
\define@key{includespread}{coltypes}{%
\luaexec{coltypes="\luatexluaescapestring{\unexpanded{#1}}"}%
}
\define@choicekey*+{includespread}{columns}[\val\nr]{head,top,none}{%
\ifcase\nr\relax%
\luaexec{columns=1}%
\or%
\luaexec{columns=2}%
\fi%
}{%
\luaexec{%
local function split(s,sep)
local sep, fields = sep or ":", {}
local pattern = string.format("([^\%s]+)", sep)
s:gsub(pattern, function(c) fields[\#fields+1] = c end)
return fields
end
local s="\luatexluaescapestring{#1}"
columns = split(s,",")
}%
}{}%
\define@key{includespread}{newline}{%
\luaexec{odsnl="\luatexluaescapestring{\unexpanded{#1}}"}%
}
\define@key{includespread}{lastnewline}{%
\luaexec{odslastnl="\luatexluaescapestring{\unexpanded{#1}}"}%
}
% Variable initialization and helper functions
\begin{luacode*}
odsreader = require("odsfile")
odsfile = nil
sheetname = nil
range = {nil,nil,nil,nil}
columns = nil
templates = {}
row = {}
body = nil
odsfilename = ""
currenttemplate = nil
rowtemplate = nil
celltpl = "-{value}"
multicoltpl = "\\multicolumn{-{count}}{l}{-{value}}"
latexescape = "true"
odsnl = "\\OdsNl"
odslastnl = "\\OdsLastNl"
\end{luacode*}
\newcommand\loadodsfile[2][]{%
\setkeys{includespread}{#1}%
\luaexec{%
odsfilename = "\luatexluaescapestring{#2}"%
local ods = odsreader.load(odsfilename)%
odsfile, e = ods:loadContent()%
}%
}
\newcommand\tabletemplate[2]{%
\luaexec{%
templates["#1"]="\luatexluaescapestring{\unexpanded{#2}}"%
}
}
\NewDocumentCommand\includespread{s o}{%
\IfBooleanTF#1%
{\ods@includespread@star{#2}}%
{\ods@includespread@unstar{#2}}%
}
\newcommand\ods@includespread@star[1]{%
\let\ods@tmp@lastNL\OdsLastNl%
\renewcommand\OdsLastNl{}%
\ods@includespread@unstar{#1}%
\let\OdsLastNl\ods@tmp@lastNL%
}
\newcommand\ods@includespread@unstar[1]{%
\luaexec{%
range = nil
rowseparator = ""
columns=nil
currenttemplate = nil
rowtemplate = nil
celltpl = "-{value}"
columnbreak = "\\linebreak{}"
coltypes = nil
latexescape = "true"
}%
\setkeys{includespread}{#1}%
\luaexec{%
body = odsreader.getTable(odsfile,sheetname)
local real_range = odsreader.getRange(range)
local values = odsreader.tableValues(body,real_range[1],real_range[2],real_range[3],real_range[4])
%-- Conversion of odsfile table values to LaTeX tabular
local concatParagraphs = function(column)
% -- second returned value signalize whether cell contain paragraph, or not
local getCell = odsreader.get_cell
if type(column) =="table" then
return getCell(column, columnbreak), true
end
return getCell(column,""), false
end
local rowValues = function(row, headings)
local headings = headings or {}
local t={}
local i = 1
for _,column in pairs(row) do
local attr = column.attr or {}
local value, br = concatParagraphs(column.value)
value = value or ""
local x = attr["table:number-columns-spanned"] or "1"
x = tonumber(x)
if x > 1 then
value = odsreader.interp(multicoltpl, {value = value, count = x})
else
value = odsreader.interp(celltpl, {value = value})
end
% table.insert(t,value)
t[i] = value
headings[i] = br
i = i + x
end
return t, headings
end
local makeColtypes = function(h)
local maxsize = tex.hsize / 65536
local h = h or {}
local p = 0
for _, c in pairs(h) do
if c then
p = p + 1
end
end
if p > 0 then
local j = {}
local size = tostring(math.floor(maxsize / \#h)) .. "pt"
for _, c in pairs(h) do
local k = "l"
if c then k = "p{"..size.."}" end
j[\#j+1]= k
end
return table.concat(j)
else
return string.rep("l",\#h)
end
end
if rowtemplate == nil then
local headings = {}
local currow = {}
currenttemplate = currenttemplate or "default"
content = {}
for i,row in pairs(values) do
currow, headings = rowValues(row, headings)
table.insert(content,table.concat(currow," & "))
end
%-- Column headings handling
local colheading=""
if type(columns) == "number" and columns == 1 then
columns = rowValues(values[1])
content = odsreader.table_slice(content,2,nil)
elseif type(columns) == "number" and columns == 2 then
local t = odsreader.tableValues(body,real_range[1],1,real_range[3],2)
columns = rowValues(t[1])
end
if type(columns) == "table" then colheading = table.concat(columns," & ") .. odsnl .. " " end
% coltypes = ""
if type(content)== "table" then
% coltypes= string.rep("l",\#content[1])
if not coltypes then
coltypes = makeColtypes(headings)
end
end
content = table.concat(content, odsnl .. " " ..rowseparator) .. odslastnl
local result = odsreader.interp(templates[currenttemplate],{content=content,coltypes=coltypes,colheading=colheading,rowsep=rowseparator})
print(result)
tex.sprint(result)
else
local content = {}
currenttemplate = currenttemplate or "empty"
for _,row in pairs(values) do
table.insert(content,odsreader.interp(rowtemplate,rowValues(row)))
end
content = table.concat(content,rowseparator)
local result = odsreader.interp(templates[currenttemplate],{content=content,coltypes=coltypes,colheading=colheading,rowsep=rowseparator})
print(result)
tex.sprint(result)
end
}%
}%
\tabletemplate{empty}{-{content}}
\tabletemplate{default}{-{colheading}-{rowsep}-{content}}
\tabletemplate{booktabs}{%
\begin{tabular}{-{coltypes}}
\toprule
-{colheading}
\midrule
-{content}
\bottomrule
\end{tabular}
}
% Interface for adding of new rows
\newenvironment{AddRow}[1][]{%
\def\AddString##1##2{%
\luaexec{%
local pos = "\luatexluaescapestring{##2}"%
if pos == "" then pos = nil end; row:addString("\luatexluaescapestring{\unexpanded{##1}}",nil,pos)%
}%
}%
\def\AddNumber##1##2{%
\luaexec{%
local pos = "\luatexluaescapestring{##2}"%
if pos == "" then pos = nil end; row:addFloat("\luatexluaescapestring{##1}",nil,pos)%
}%
}%
\luaexec{%
pos = "\luatexluaescapestring{#1}"%
if pos == "" then pos = nil end; row = odsreader.newRow()%
}%
}{%
\luaexec{%
body = body or odsreader.getTable(odsfile)
row:insert(body,pos)%
% we must save the updated table to the original table
odsfile.root["office:document-content"]["office:body"]["office:spreadsheet"]["table:table"] = body%
}%
}
% Interface for saving the spreadsheet
\newcommand\savespreadsheet{%
\luaexec{%
local xml = require("luaxml-mod-xml")
f = io.open("content.xml","w")%
f:write(xml.serialize(odsfile.root))%
f:close()%
odsreader.updateZip(odsfilename,"content.xml")%
}%
}
% support for hyperlinks in cells
\newcommand\odslink[2]{\texttt{#2}}
\AtBeginDocument{%
\@ifpackageloaded{hyperref}{%
\renewcommand\odslink[2]{\href{#1}{#2}}%
}{}
}
\endinput
可以这样使用:
\includespread[file=data.ods,range=a1:b3,template=tblr, newline={\\},lastnewline={\\}]
结果如下: