如何在 ConTeXt 中根据电子表格数据创建格式信函?

如何在 ConTeXt 中根据电子表格数据创建格式信函?

在 LaTeX 中,人们可以用来datatool创建简单的格式信函。例如,数据存储在电子表格或 CSV 文件中,如下所示:

Name,Amount,Date
"Mr. White","$300","Dec. 2, 1911"
"Mr. Brown","$300","Dec. 3, 1911"

然后,提供一个简单的模板:

Dear \insertName,

You owe \insertAmount. Please send it before \insertDate.

编译后,它会为电子表格中的每一行生成一个字母。

ConTeXt 中是否有类似的用于创建格式信函的工具?M-数据库模块似乎共享了许多功能datatool,但我看不出有任何方法可以使用它来创建格式信函。

答案1

据我所知,没有内置功能。但通常你可以通过组合一些现有功能来实现。你只需要一个CSV 解析器然后你就可以使用缓冲区来完成剩下的工作了。(我稍微修改了一下界面,这样你就可以简单地\insert[Field Name]代替\insertFieldName。)用法如下:

  1. 定义一个模板在修改后的形式中,您的示例代码将如下所示:

    \startcsvtemplate [tpl] 亲爱的 \insert[Name],

    您欠款 \insert[金额]。请在 \insert[日期] 之前发送。 \par \stopcsvtemplate

    尾随的结束线会被删除,因此您必须明确请求段落。

  2. 定义一个输入缓冲区(可选):输入可以从文件或缓冲区读取。在后一种情况下,需要定义缓冲区,就像任何其他缓冲区一样:

    \startbuffer[csdata] 姓名、金额、日期 “怀特先生”、“\letterdollar 300”、“1911 年 12 月 2 日” “布朗先生”、“\letterdollar 300”、“1911 年 12 月 3 日” “前提女士”、“\letterdollar 42”、“1911 年 12 月 4 日” “结论女士”、“\letterdollar 23”、“1911 年 12 月 5 日” \stopbuffer

  3. 要求输入解析:根据您选择从缓冲区还是文件读取数据,您必须使用适当的命令来处理它:

    \processcsvbuffer[一][cs数据] \processcsvfile[二][test.csv]

    这两个命令的第一个参数是ID通过它以后可以引用数据集(类似于\useexternalfigure[a_cow][cow.pdf])。

  4. 现在数据集和模板已经就绪,您可以在工作定义

    \definecsvjob [测试] [ 数据=two, 模板=tpl, ]

    这将生成一个宏\testing,您可以在文档中使用它来生成输出。

    \启动文本 \测试 \停止文本

注意::下面的答案可以(如果经常使用,可能应该)通过定义一些模板语言并将字符串处理完全移至 Lua 来改进。事实上,由于从 TeX 重复调用 Lua,性能会很差。

示例输出。

% macros=mkvi

\unprotect
\startluacode
  local datasets = { }

  local buffersraw   = buffers.raw
  local context      = context
  local ioloaddata   = io.loaddata
  local lpegmatch    = lpeg.match
  local stringformat = string.format
  local stringmatch  = string.match
  local stringsub    = string.sub
  local tableconcat  = table.concat
  local tableswapped = table.swapped

  local die = function (msg) print(msg or "ERROR") os.exit(1) end

  local csv_parser
  do
    --- This is (more than) an RFC 4180 parser.
    --- https://www.rfc-editor.org/rfc/rfc4180
    local C, Cg, Cs, Ct, P, S, V
        = lpeg.C, lpeg.Cg, lpeg.Cs, lpeg.Ct, lpeg.P, lpeg.S, lpeg.V

    local backslash = P[[\letterbackslash]]
    local comma     = ","
    local dquote    = P[["]]
    local eol       = S"\n\r"^1
    local noquote   = 1 - dquote
    local unescape  = function (s) return stringsub(s, 2) end
    csv_parser = P{
      "file",
      file    = Ct((V"header" * eol)^-1 * V"records"),
      header  = Cg(Ct(V"name" * (comma * V"name")^0), "header"),
      records = V"record" * (eol * V"record")^0 * eol^0,
      record  = Ct(V"field" * (comma * V"field")^0),
      name    = V"field",
      field   = V"escaped" + V"non_escaped",
      --- Deviate from rfc: the “textdata” terminal was defined only
      --- for 7bit ASCII. Also, any character may occur in a quoted
      --- field as long as it is escaped with a backslash. (\TEX          --- macros start with two backslashes.)
      escaped     = dquote
                  * Cs(((backslash * 1 / unescape) + noquote)^0)
                  * dquote
                  ,
      non_escaped = C((1 - dquote - eol - comma)^0),
    }
  end

  local process = function (id, raw)
    --- buffers may have trailing EOLs
    raw = stringmatch(raw, "^[\n\r]*(.-)[\n\r]*$")
    local data = lpegmatch(csv_parser, raw)
    --- map column name -> column nr
    data.header = tableswapped(data.header)
    datasets[id] = data
  end

  --- escaping hell ahead, please ignore.
  local s_item = [[
  \bgroup
    \string\def\string\insert{\string\getvalue{csv_insert_field}{%s}{%s}}%%
%s%% template
  \egroup
]]

  local typeset = function (id, template)
    local data   = datasets[id] or die("ERROR unknown dataset: " .. id)
    template     = stringmatch(buffersraw(template), "^[\n\r]*(.-)[\n\r]*$")
    local result = { }
    local last = \letterhash data
    for i=1, last do
      result[i] = stringformat(s_item, id, i, template)
    end
    context(tableconcat(result))
  end

  local insert = function (id, n, field)
    local this = datasets[id]
    context(this[n][this.header[field]])
  end

  commands.process_csv      = process
  commands.process_csv_file = function (id, fname)
    process(id, ioloaddata(fname, true))
  end
  commands.typeset_csv_job  = typeset
  commands.insert_csv_field = insert

\stopluacode

\startinterface all
  \setinterfaceconstant{template}{template}
  \setinterfaceconstant    {data}{data}
\stopinterface

\def\processcsvbuffer[#id][#buf]{%
  \ctxcommand{process_csv([[#id]], buffers.raw(\!!bs#buf\!!es))}%
}

\def\processcsvfile[#id][#filename]{%
  \ctxcommand{process_csv_file([[#id]], \!!bs\detokenize{#filename}\!!es)}%
}

%% modeled after \startbuffer
\setuvalue{\e!start csvtemplate}{%
  \begingroup
  \obeylines
  \dosingleempty\csv_template_start%
}

\def\csv_template_start[#id]{%
  \buff_start_indeed{}{#id}{\e!start csvtemplate}{\e!stop csvtemplate}%
}

\installnamespace                  {csvjob}
\installcommandhandler \????csvjob {csvjob} \????csvjob

\appendtoks
  \setuevalue{\currentcsvjob}{\csv_job_direct[\currentcsvjob]}
\to \everydefinecsvjob

\unexpanded\def\csv_job_direct[#id]{%
  \edef\currentcsvjob{#id}%
  \dosingleempty\csv_job_indeed%
}

\def\csv_job_indeed[#setups]{%
  \iffirstargument\setupcurrentcsvjob[#setups]\fi
  \ctxcommand{typeset_csv_job(
                [[\csvjobparameter\c!data]],
                [[\csvjobparameter\c!template]])}%
}

\def\csv_insert_field#id#n[#field]{%
  \ctxcommand{insert_csv_field([[#id]], #n, [[#field]])}%
}

\protect

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%                               demo
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

%% Stepwise instructions.
%% step 1: Define template.
\startcsvtemplate [tpl]
Dear \insert[Name],

You owe \insert[Amount]. Please send it before \insert[Date].
\par
\stopcsvtemplate

%% step 2: Define an input (CSV).
\startbuffer[csdata]
Name,Amount,Date
"Mr. White","\\letterdollar 300","Dec. 2, 1911"
"Mr. Brown","\\letterdollar 300","Dec. 3, 1911"
"Ms. Premise","\\letterdollar 42","Dec. 4, 1911"
"Ms. Conclusion","\\letterdollar 23","Dec. 5, 1911"
\stopbuffer

%% step 3: Parse and store the input.
\processcsvbuffer[one][csdata]
%\processcsvfile[two][test.csv]

%% step 4: Declare a job, joining dataset and template.
\definecsvjob [testing] [
  data=two,
  template=tpl,
]

%% step 5: Enjoy!
\starttext 
  \testing
\stoptext

答案2

我不知道如何使用ConTeXt,但我知道你可以LuaTex在中使用ConTeXt,所以我给你一个LuaLaTeX易于移植的示例,ConTeXt因为主要工作是在中完成的Lua。原理是你在-world 中定义字母的模板,LaTeX/ConTeXt包括一些LaTeX/ConTeXt宏(变量),并从中重复调用模板,LuaTeX每次为给定的 csv 文件中的每个条目重新定义变量。在执行此操作之前,你必须读取 csv 文件并将数据存储在表中Lua

也许在信函模板中使用参数(#1、#2、#3、...)而不是每次都重新定义变量会是一个更简洁的解决方案,但这样用户就不容易阅读模板,因为他不知道参数的含义,而且您还被限制为九个参数。这是您的选择。

\documentclass{article}
\usepackage{filecontents}

\begin{filecontents*}{datafile.csv}
Mr.;Homer;Simpson;Evergreen Terrace, Springfield
Ms.;Marge;Simpson;Evergreen Terrace, Springfield
Mr.;Bart;Simpson;Evergreen Terrace, Springfield
Ms.;Lisa;Simpson;Evergreen Terrace, Springfield
Ms.;Maggie;Simpson;Evergreen Terrace, Springfield
\end{filecontents*}

\begin{filecontents*}{luaFunctions.lua}
function ReadData()

    local input = io.open('datafile.csv', 'r')
    dataTable = {}

    for line in input:lines() do

        -- split the line
        local split = string.explode(line, ";")

        -- store the arguments in variables
        tableItem = {}
        tableItem.Sex = split[1]
        tableItem.FirstName = split[2]
        tableItem.SurName = split[3]
        tableItem.Address = split[4]

        -- insert the arguments of one line in the table
        table.insert(dataTable, tableItem)
    end
end

function Letter(sex, firstName, surName, address)
    -- redefine the latex commands and execute the \letterTemplate macro
    tex.print(string.format("\\def\\sex{%s}",sex))
    tex.print(string.format("\\def\\firstName{%s}",firstName))
    tex.print(string.format("\\def\\surName{%s}",surName))
    tex.print(string.format("\\def\\address{%s}",address))
    tex.print("\\letterTemplate")
end

function PrintLetter()
    -- read the external file and store the data in a table
    ReadData()

    -- loop through the table and print a letter for each table item 
    for i,p in ipairs(dataTable) do
        Letter(p.Sex, p.FirstName, p.SurName, p.Address)

        -- pagebreak
        if i ~= #dataTable then 
            tex.print("\\newpage")
        end       
    end
end
\end{filecontents*}    

\directlua{dofile("luaFunctions.lua")}

\def\letterTemplate{%    
Hallo \sex\ \firstName,\\
your surname is \surName\ and you live at \address.\\

Regards
}

\begin{document}
\directlua{PrintLetter()}
\end{document} 

相关内容