PyYAML+Jinja2を使って雑な問題集を作る


教育で使うかは別として、YAMLから問題集を起こすPythonスクリプトを書きました。（ただのテンプレート出力ですが）


やりたいこと
-------------

- 印刷でいい感じに出したい、メールで送れるようにしたい
    - HTMLファイルで出す。1ファイルで収まるようにする。
    - （CSSフレームワークとかJavaScriptのライブラリはCDNで公開されているものを利用）
- 回答は隠したい
    - テンプレートで出し分ける。HTMLだからJavaScriptで頑張る方法もあるが...
- 問題の管理にDBを使いたくない
    - yamlでよさそう

実装
-----------------

こんな感じで使うライブラリを決めました

- yamlで問題を定義できたら楽そう
- jinja2はansibleでも使ってるので馴染みがあるのと、yamlで読んだ結果をそのまま流せそう
- markdownでyamlの問題が書けたら少し便利そう。~~思い付き~~
- HTMLならJavaScriptの[highlightjs](https://highlightjs.org)でシンタックスハイライト入れられる。~~思い付き~~

### 入力データ例

「大項目」と「問題」の2階層を想定してます。

``` text:traning.yml
---
- title: ディレクトリの移動・操作
  quizzes:
    - question: |-
        現在の作業中のディレクトリ（ワーキングディレクトリ）のパスを表示するコマンドは何か
      answer: |-
        ~~~{.bash}
        pwd
        ~~~
    - question: |-
        現在のディレクトリから`/tmp`に移動する操作を書きなさい
      answer: |-
        ~~~{.bash}
        cd /tmp
        ~~~
```

※`|-`を使うと、複数行かけてよい。(末端の改行は削除される。)

### 生成コード

``` python:generater.py
import yaml 
import markdown
import jinja2
import sys, io

# always utf-8 encoding. 
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')

# markdown extension
MARKDOWN_EXTENSIONS = ['markdown.extensions.extra']

# add filter (markdown)
loader = jinja2.FileSystemLoader('./templates')
env = jinja2.Environment(autoescape=True, loader=loader)
env.filters['markdown'] = lambda text: markdown.markdown(text, MARKDOWN_EXTENSIONS)

# render
tranings = yaml.load(sys.stdin.buffer)
template : jinja2.Template = env.get_template("traning.j2")
html = template.render(tranings = tranings)

# output
print(html)
```

工夫したところは、markdownの変換処理をjinja2のフィルタで書くようにしたところです。

### テンプレート例

回答を隠したいときは`<div class="quiz-answer">`あたりを削れば良いです。

``` html:templates/tranings.j2
<!DOCTYPE html>

<head>
    <meta charset="utf-8">
    <title>とれーにんぐ</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/styles/default.min.css">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/highlight.min.js"></script>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB"
        crossorigin="anonymous">
    <style>
    .quiz-content{
        margin-left: 1em;
    }
    .quiz-answer::before{
        content: "A."
    }
    </style>
    <script>
    window.addEventListener('load', function(){
        Array.from(document.querySelectorAll('pre code')).forEach(function(el){
            hljs.highlightBlock(el);
        })
    })
    </script>
</head>

<body>
    <div class="container-fluid">
        <section>
            <ul>
                {% for traning in tranings %} {% set traning_loop = loop %}
                <li>
                    <div><a href="#{{traning.title}}">{{ traning_loop.index }}.{{traning.title}}</a></div>
                    <ul>
                    {% for quiz in traning.quizzes %}
                    <li><a href="#quiz-{{ traning_loop.index }}-{{ loop.index }}">{{ traning_loop.index }}.{{ loop.index }}. {{quiz.question}}</a></li>
                    {% endfor %}
                </ul>
                </li>
                {% endfor %}
            </ul>
            
        </section>
        
        {% for traning in tranings %} {% set traning_loop = loop %}
        <section class="traning">
            <h2 id="{{ traning.title }}">{{ traning_loop.index }}. {{ traning.title }}</h2>
            {% for quiz in traning.quizzes %}
            <div class="quiz">
                <h3 id="quiz-{{ traning_loop.index }}-{{ loop.index }}" class="quiz-no">Q. {{ traning_loop.index }}-{{ loop.index }}.</h3>
                <div class="quiz-content">
                    <div class="quiz-question">
                        {{ quiz.question | markdown | safe }}
                    </div>
                    <div class="quiz-answer">
                        {{ quiz.answer | markdown | safe }}
                    </div>
                </div>
            </div>
            {% endfor %}
        </section>
        {% endfor %}
    </div>
</body>
```


### 使用例

``` shell
python generator.py < traning.yml > output.html
```

### 出力例

![image.png](https://qiita-image-store.s3.amazonaws.com/0/52527/4b506116-25d3-88d7-389e-d851b18a31ae.png)


~~ここまでやったけど問題を起こすのがめんどくさくなってるので、使うかは不明。~~