跳转至

题面的书写

如果你不使用任何的特性,你可以用纯 markdown 书写题面,并将以 statement/*.md 命名保存在试题目录下。其中 * 替换为相应的语言例如 zh-cn。 此外还提供了少量的工具以扩展 markdown 不支持的功能。

注意:不要在题面里直接插入任何html代码。因为虽然markdown原生支持嵌入html,但因为我们要渲染成tex,所以不能直接使用任何原生html语法,例如表格、带格式的图片等都需要特殊处理,具体后文都讲提到。

注意:不要在题面里直接插入任何markdown的原生表格。因为markdown表格的方言相当多,加上我们建议所有参数全部来源于同一个地方,因此请尽量按照后文的“表格”。

目前三种输出类型渲染的步骤为:

  • tex:md+jinja+*jinja → md+jinja → tex+jinja → tex → pdf;
  • md:md+jinja+*jinja → md+jinja → md+html;
  • html:md+jinja+*jinja → md+jinja → md+html → html。

其中*jinja表示经过jinja渲染会变成jinja模板的代码。

jinja2的安装用 pip install jinja2(如果你用了第一种安装方式将会自动安装),jinja2本身的语法戳这里学习。

所有的模板都存在该工程的templates目录下,有兴趣开发模板或是想修改的话欢迎联系我入坑。

statement/*.md

如果你使用 tuack.gen 生成了题面,你将看到大部分你可能用到的东西的例子,一般你没有必要看这一部分。

现在的标题子模块形如 {{ s('description') }},这个会自动处理不同语言翻译问题、标题的等级、样例的编号、不同环境的标题格式等问题。 如果你不需要自动处理它们,可以用下列拆开的模块:

{{ self.title() }} 表示使用名为 title 的子块,这个子块在某些下会渲染成时间、空间限制和题目类型(如果不为传统型的话),在tuoi模式下会留空等等。 这些子块定义在 problem_base.md.jinja 中,可用的还有:

  • input_file 输入文件的描述,根据平台说明是标准输入还是从文件输入。
  • output_file 输入文件的描述,根据平台说明是标准输入还是从文件输入。
  • user_path 选手目录,如果是tuoi会变成“选手目录”这几个字,tuoj会变成下载链接。
  • sample_text 样例自动渲染,会自动从 down 中读入样例文件并添加到题面中,需要下面提到的前置变量。支持参数 sample_id 设置样例的编号(同时也是样例文件名);show space 如果为真则会在PDF格式中将空格显示出来。
  • title_sample uoj的“样例输入/输出”是拆分成两级名称的,所以蛋疼地需要多一级,这里专指“【样例】”这个标签;其他环境下则会被渲染成空的。
  • sample_input_text 样例输入自动渲染。
  • sample_output_text 样例输入自动渲染。
  • sample_text 样例自动渲染,会自动从 down 中读入样例文件并添加到题面中,需要下面提到的前置变量。
  • sample_file 一个不出现在题面,但是以文件形式提供的样例,同样需要下面提到的前置变量。
  • title_sample_description uoj的“样例说明”是拆分成两级名称的,所以蛋疼地需要多一级。
  • title 标题,包括时空限制、题目类型等。

有的块需要用到前置的变量,例如文字样例自动渲染需要提供样例的编号 sample_id 作为变量,并且要显示样例中的空格,具体会写成这样:

1
2
3
4
{% set vars = {} -%}
{% do vars.__setitem__('sample_id', 1) %}
{% do vars.__setitem__('show space', True) %}
{{ self.sample_text() }}

如果只有一组样例,应当不设置样例编号,像这样:

1
2
{% set vars = {} -%}    //这行也可以直接删除
{{ self.sample_text() }}

但请注意,在 down 文件夹中仍然需要以 1 标号。

一些约定

题目标题用一级标题 #,在题面书写的时候并不需要加上,而应该用标题子块代替。

每个小节标题,如 题目描述 用二级标题 ##,会被渲染成Latex的subsection和html的h2。

小节下的标题用三级标题 ###,会被渲染成Latex的subsubsection和html的h3。 对于uoj,样例 下的 输入 会使用比上面低一级的标题,而这个在tuoi style的题面中会不单列一级标题;为了通用,建议使用上面提到的子块生成样例。此外,uoj强制要求用比较低的标题等级(例如小结标题用三级标题),但你不需要在这里改变等级,而应当遵守轮子的规定,输出时轮子会帮你调整。

样例使用三个反引号(不知道markdown里面怎么打这几个字符)括起来的pre来装,同样建议用子块而非自己做样例。

公式用 $ 括起来,单独占行的公式用单独占行的 $$ 括起来,例如 $1 \le a_i \le n$

外部变量和小工具

通过变量 prob 可以获取你在 conf.json 中的各项参数,例如要输出题目的名称,可以用下列写法:

1
2
{{ prob['name'] }}
{{ prob.name }}
注意,在jinja中,这两种方法大多数时候是等价的。但有一种例外,即字段name和prob的一个方法同名,此时并不是访问这个字段,而是访问这个方法。因此推荐用第一种写法。

对于有多种语言的变量,可以用这样的方式访问(会根据具体的语言选择合适的值):

1
{{ prob.tr('title') }}

获取当前的渲染环境,可以用变量 comp(会被赋值为 noiuoj 等),输入输出方式用变量 io_style(会被赋值为 fiostdio 等)。

一些常用的引用变量,如 prob['data'],有更简洁的写法 data。这样的变量还有:samplespreargs

样例提供了自动渲染,但如果需要以文本的形式展示某个其他下发的文件,提供了一个函数 down_file(file_name),例如:

1
2
3
4
## 样例输出
​``` 这个引号后面啥也没有,但是不写字的话就会被渲染成真正的后括号QAQ 
{{ down_file('1.ans') }}
​``` 记得删除这两句话

考虑到不同情况下下发文件存储的位置不同,用函数 file_name 根据具体的环境生成具体的路径。 类似的还有 resource 函数,可以根据具体情况获取存储在 resources 文件夹中的资源(图片、模板等)。

注意上述3个函数容易混淆,区分如下:down_file函数会将down文件夹下,下发给选手文件(一般是样例,当然你也可以放别的东西)的具体内容渲染到题面中;file_name函数会将down文件夹下的文件名渲染到题面中,这里不直接写类似于down/1.ans而要求写成函数是因为不同的OJ或赛场环境下,下发目录的位置是不同的;resource则是将resources文件夹中的文件渲染成文件名,这里用函数的原因也是因为不同的OJ中图片的存放方式不同。

此外,还有一组工具 tl,其中包括python的内建函数、math module的函数和一些小轮子,具体可以阅读 tools.py。例如:

1
2
3
{{ tl.int('1000000') }}
{{ tl.log(1000000) }}
${{ tl.hn(1000000) }}$

第一个调用使用了整型转化;第二个调用使用了log函数;第三个调用可以生成一个数适合阅读的格式,会输出成$10^{6}$

对于 hn 函数,会根据输出的长度自动选择格式。如果需要强制格式,使用下列语法

1
2
{{ tl.hn(1250000, 'x') }}   //1.25\times 10^{6}
{{ tl.hn(1250000, ',') }}   //1,250,000

此外你还可以从 base 中读取公共的任何全局变量,例如 base.out_system 可以读取要输出到的系统,你可根据不同的系统写不同的题面(例如checker在不同系统中的用法不同)。

图片

markdown原生对图片支持不太好(如无法改变大小),因此提供了下列函数:

1
{{ img('3.jpg', size = 0.5, align = 'middle', inline = False, env = ['tex', 'md']) }}

支持的参数包括:

  • size :缩放比例,默认 1。
  • inlineTrue 行内图片,False 独占一行,默认 False
  • align:独占一行的图片,对齐方式 leftmiddleright,默认使用相应格式的默认。
  • env:单个字符串或数组,表示在哪些环境下出现,默认全部环境都出现。

上述参数都可以不写。

这个函数等价于下列两轮渲染:

1
{{ render("template('image', resource = resource('3.jpg'), size = 0.5, align = 'middle', inline = False)", ['tex', 'md']) }}

表格

markdown原生对表格支持不太好(如无法合并单元格等)。你可以使用原生的表格,但考虑到题目中表格的特殊性,并不推荐这么做。

考虑到题目中的表格大多数是描述子任务数据规模、样例解释等功用,我们建议采取数据、文字和格式三者分离的方式,或内容和格式两者分离的方式。

区分“数据”和“文字”例如:你有一个子任务参数叫做 maxv 表示数组 v 的最大值,在表格中需要显示成 $v_i \le maxv$;这时 maxv 这个程序可读的变量叫做数据,而后面的数学显示格式叫做文字。这项功能主要为了更方便地修改数据规模和造不同语言的题面。

具体的流程为:

  • 对于子任务的情形:建议将参数输出至 conf.json/yaml 文件中各数据的 args 字段,然后用 python 或 json+jinja 生成表格,在题面的 md 文件中引用表格并描述格式。
  • 对于其他情形:建议程序生成 json/yaml 文件或手写 python 文件,在题面的 md 文件中引用表格并描述格式。

md文件中引用

引用模板的地方,除了调用的函数改成了 tbl,其他和二次渲染类似。例如:

1
{{ tbl('data') }}

渲染器会去 tables 目录下寻找 data.pydata.pyincdata.yamldata.ymldata.json

表格可以有附加参数,例如:

1
{{ tbl('data', width = [2,1,1,1], font_size = 12) }}

目前支持的参数包括:

  • width :宽度比例,应当是一个list,将会按这个比例分配表格每一列的宽度。对于tex如果没有这个参数将使用和内容等长的总宽度,可能会塞不满一行,也可能超过纸面的宽度;如果使用了这个参数,那么最宽宽度将使用纸面的最大宽度。
  • font_size:表格整体字号大小,用于实在写不下的情况。
  • style3-line 三线表格;normal 全部细线 。仅对tex有效(目前html还没有不好看)。默认 3-line
  • titled:是否有标题行。默认 True

tables

Python格式的表格

将 python 的代码放在 tables 目录下,用 名称.py/pyinc 命名,下例为 data.py/pyinc

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
ret = [["测试点","$x$","$n$","$\\sum n^k$","完全二叉","$T$"]]
for datum in prob['data']:
    args = datum['args']
    row = [
        ','.join(map(str, datum['cases'])),
        "$= %s$" % tools.hn(args[0]),
        "$\\le %s$" % tools.hn(args[1]),
        "$\\sum n^{%s} \\le %s$" % (tools.hn(args[2]), tools.hn(args[5])),
        "Yes" if args[3] != 0 else "No",
        "$\\le %s$" % tools.hn(args[4])
    ]
    ret.append(row)
base.log.debug(u'输出调试信息')
return merge_ver(ret)

你需要将表格存成一个每个元素都是 str 或其他可以转成 str 的元素的二维 list,注意需要每行长度相同。在这个文件中你也可以使用各种功能,例如 base.log,tools,prob,io_style,comp 等。这个文件中有一个新加的函数 merge_ver,调用它可以将竖直方向上相同的格子合并。

如果你不是很懂 python,那么你可以直接 return 一个二维数组就行。

此外,虽然不一定用得上,这里面你可以自由使用更多 python 语法,例如 import

JSON/YAML格式的表格

将 json 或 yaml 的模板放在 tables 目录下,用 名称.json/yaml 命名,下例为 data.json(同样,不含任何模板语法就是一个json),使用 null 表示和上一行合并单元格。 因此可以写成类似于下面的格式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
[   
    ["测试点", "$n, m$"]
    {%- set last = None -%}
    {% for datum in prob['data'] %}
    ,[
        {%- for i in datum['cases'] -%}
            {{- i -}}
            {%- if not loop.last -%}
                ,
            {%- endif -%}
        {%- endfor -%}",
        {%- if last and datum.args[0] == last[0] and datum.args[1] == last[1] -%}
            null
        {%- elif datum.args[0] == -1 and datum.args[1] == -1 -%}
            "无约束"
        {%- else -%}
            "
            {%- if datum.args[0] != -1 -%}
                $n,m \\le {{ tools.js_hn(datum.args[0]) }}$
            {%- endif -%}
            {%- if datum.args[0] != -1 and datum.args[1] != -1 -%}
                并且
            {%- endif -%}
            {%- if datum.args[1] != -1 -%}
                $nm \\le {{ tools.js_hn(datum.args[1]) }}$
            {%- endif -%}
            "
        {%- endif -%}
    ]
    {% endfor %}
]

尽管不推荐,你也可以不用任何模板语法,直接保存一个二维数组。

两轮渲染

上述表格和图片等都是采取两轮渲染的方式,即在两条路线共同的渲染之后,使用tex或html的模板再渲染一次。

例如图片是用了图片模板 image.tex.jinjaimage.html.jinja,使得可以使用以下语法渲染一张图片

1
{{ render("template('image', resource = resource('3.jpg'), size = 0.5, align = 'middle', inline = False)") }}
其中 render 函数中的串会在被渲染成tex或html之后会被渲染成一个模板项,从而进一步被渲染。而 img 函数只是对它进行了一次封装。

如果要写一段自用的tex或html模板(当然你如果只需要嵌入tex或html,只需要再模板中不填入任何模板语法即可), 你可以在 resources 文件夹中写 名称.tex.jinja名称.html.jinja,然后用下列方式插入:

1
{{ render("template(resource('名称'), 模板参数表...)") }}
render 函数还可以传第二个参数,表示只在特定的环境下渲染,例如

1
{{ render(''' '<a href="http://uoj.ac">UOJ</a>' ''', 'md') }}

上例表示只在html中渲染一个指向UOJ的超链接,当然UOJ中你还可以用md的超链接语法。第二个参数支持单个表示类型的字符串或是一个这样的字符串组成的list。其中支持的字符串包括:htmltexnoiuojccpcccctuojccc-texccc-mdtuoitupc 等。

需要注意的是,两轮渲染特别容易出现转义的问题,即某些需要转义的字符有可能需要经过多次转义才能存储下来(例如 "\"\\\"),这里推荐使用 json.dumps 等方法生成你要多次转义的字符串,例如上面那个可以写成:

1
{{ render(json.dumps('<a href="http://uoj.ac">UOJ</a>'), 'md') }}