5

I want to include a ods file using the odsfile package in combination with the tabularray package. However this results in an Misplaced alignment tab character & error. Here is a 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}

The file data.ods just contains:

X   Y
1   a
2   b

The error is like:

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}

How can I make it work?

Edit As michal.h21's answer indicates, to find a complete solution seems to be pretty complex. So I try to give some more background of what I want to achieve in particular:

%% 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}

The point about this example is that it selects the first row and the third row (and possibly some more non conrows) of the odsfile and merges them into one LaTeX table.

I want this to work with tblr since I dont' want to count or calculate the number of rows each time (which is trival but annoying for larger projects) and because I want use the great capabilities of tabularray to modify the look of the table.

So, if this would work with a template, I think it would be enough for my current use case.

5
  • If you want to include different ranges from one table, I think that something like \includespread[file=data.ods,range=a1:b1,a3:b3] should work.
    – michal.h21
    Apr 21 at 15:39
  • This the following error: ! Package xkeyval Error: a3:b3' undefined in families includespread'.
    – student
    Apr 21 at 17:16
  • 1
    I didn't test it, so I didn't realize that this will fail with keyval parsing. But even with braces (as in range={a1:b1,a3:b3}) it doesn't work. But it is something that I could possibly add.
    – michal.h21
    Apr 21 at 17:52
  • @michal.h21 Would be great if you cold add this (such that it works in particular with tabularray).
    – student
    Apr 22 at 4:50
  • I've added it to the development version of Odsfile: github.com/michal-h21/odsfile
    – michal.h21
    Apr 23 at 12:29

1 Answer 1

5

There are several issues. First is that Tabularray has in general issues with content that is passed from Lua. I was not able to pass \includespread to the tblr environment in a way that would expand it, I always got an error. I am afraid that the way with template option for \includespread is the only that works.

Another issue is due to the fact that odsfile don't use \\ to separate lines in tables, but its own commands, \OdsNl and \OdsLastNl. As tblr needs explicit \\ characters, you get a wrong table even if you don't get a compilation error.

Here is a fixed version of odsfile.sty, which adds two new options, newline and \lastnewline. WIth them, you can specify to use the \\ command:

% 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

It can be used in this way:

\includespread[file=data.ods,range=a1:b3,template=tblr, newline={\\},lastnewline={\\}]

This is the result:

enter image description here

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .