niszetの日記

細かい情報を載せていくブログ

Pandoc 2.10からODT形式の出力でもcustom-styleが使用可能になっていた!

タイトルで言い切ったとおりです。

すっかり見逃していましたが、Pandoc 2.10から(現最新バージョンは2.10.1)docx形式と同様に、odt形式でもcustom-styleの使用が可能となりました。

以下リリースノートからの引用。

OpenDocument (and ODT) writer:

Add custom-style “Abstract” in metadata abstract. This ensures that the abstract is rendered with style Abstract.
Enable custom-style attribute on a Div. This allows you to apply a custom style to contained paragraphs.

その他、更新内容の詳細はReleaseのページを確認してください。

pandoc.org

マニュアルの方はまだ更新されていないので、odt形式でもcustom-styleが使えるとはここからはわからないので注意ですが、使用方法はdocx等と同様です。

pandoc.org

書くまでもない気がしますが、記述例としてはこんな感じです。

::: {custom-style="Heading_20_1"}
ここに書いた文字は見出し1のスタイルになるよ
:::

注意点としてはdocxと同様に、アプリケーション上で表示されるスタイル名と文書中で保存されているスタイル名には差異があるという点です。これはひょっとしたら言語設定を英語にすればよいのかもしれませんが(未確認)

あと、仕様を確認してませんが、スタイル名は上記のように"20"のような文字列が入ります。これは表示名のspaceが変換されたものだと思われます。なので、概ね予想がつきますが、内部でどういうスタイル名になっているのか?は確認したいところですよね。

ということで、officerパッケージを真似したlibrerパッケージ(名前は暫定)を作ってスタイル名の確認だけでも簡単に出来るようにしたい気持ちがあります。Rユーザ以外にウケないのが残念ですが。

まぁ一旦出来上がれば他の言語にも移植は可能なんじゃないかな…。

昨日今日でODTの仕様を読み解いていますが、docxとはまたちょっと違う癖があるので、どのように作るのが効率的か?というのはdocxのそれとはおそらく異なります。まとまった文章にするべきかなーという感じですね。docxはMSが内部の仕様を見せてくれるとは限らないですが、Libreならまだ可能性があるかなと思っているので、もう少しODT形式について調べていく予定です。ブログに書くのかは気持ち次第…。

RMarkdownでODT形式で出力してもRStudioはMS Wordで開こうとしてくる…

どういうことなの…?

RStudioはKnit後に出力されたファイルを自動で開いてくれます。コレは確かに便利なんですが、開いてほしくないときもアリ、これは制御できるようにしてほしいなぁと思う点ではあります。

さて、掲題の件です。問題は、odt_documentで生成したODTファイルがMS Wordによって開かれてしまうという点です。Windowsでは拡張子ごとにそれを開くアプリを関連付けられるので、当然(?).odtについてはLibreOfficeにしているわけですが、RStudioでKnitするとMS Wordで開かれてしまいます。

docx形式についてはMS Wordが事実上の標準なのでこれで開いて得られるレイアウトが正で良いと思いますが、odt形式はLibreOfficeの方が正ではないかと思う次第です。

RMarkdownから生成したodt形式の文書はMS Wordで開くとレイアウトが崩れています。LibreOfficeで見て問題ないか確認をする方が良いでしょう。

ということで、特に解決策もないのですが、こういうことがあるよっていう記録です。issue上げても良いかもしれないんですが、そもそも使ってる人いないんじゃね?っていう気もするのでね…

RStudioでもodt形式のファイルが出力できる(メモ)

新規作成時の画面には出てこないので自分で書きかえる必要がある点に注意

ちょっとODT(OpenDocument)形式について調べていて、RStudio上から出来たら便利なのになぁ…ないなら作るかぁ…と思ってちょっと調べたらrmarkdownパッケージに含まれていたのでそのまま使えることが分かったのでメモ。

YAML内のoutputをodt_documentに変える。これだけ。

---
output: 
  odt_document
---

これでknitすれば、Word(docx形式)と同様に生成されて生成後にそのファイルが開かれます。

ただし、MS WordでODT形式のファイルを開くと意図しないレイアウトになると思います。こんな感じ。

f:id:niszet:20200830113905p:plain

なので、ODT形式はLibreOfficeと関連付けてそちらで開くように設定しましょう。

LibreOfficeで開くとこんな感じになりますね(2ページになってしまったので並べて表示したものをキャプチャ)

f:id:niszet:20200830114430p:plain

とても良い感じじゃないでしょうか?

今後しばらくはPandocのODT形式に関する記事が増えるかもしれません。あまり出来ることがなければ即終了ですが。Readerはメンテナが不在状態ですが、Writerは意外と更新が入っていました。そのため、一旦Pandoc's ASTに変更されてさえしまえば、ODT形式への出力自体は問題なく出来るかもしれません。そのあたり、現在どこら辺が限界なのかを調べていく予定です。

・・・たぶん。

今年は日傘とサングラス装備したら少し楽だった

マスクも装備しているので完全に不審者

割と浸透しつつある気もしますが、今年は長雨からの急に本気の夏の天気となって体が追い付かなかったため、日傘とサングラスを購入して装備していました。

どちらもAmazonで2000円くらいのものなので、すごい性能が良いというわけではないと思いますが、直接日差しに当たらないだけでこんなにも違うのか…という感じでした。

日傘は直射日光を遮る目的でそのものずばり感ありますが、サングラスもつけるつけないで体感でかなり違いますね…。やはり目は大事にしなくてはダメですね。

今日の気温を見る感じでは峠はこえたのかなというところですが、まだまだ残暑厳しく、日中は今後も装備してしのぐつもりです。

折り畳み日傘、サイズが結構小さくて腕が日にあたってしまうので、もう一回り大きなサイズを買いたいな~と思っていますが、来年までに見つけられれば良いかな…。

(R)connpassのAPIを使って、過去自分が履修したセミナーの一覧をチェックする

書きなぐりメモ…

最近は時間があるので色々と受講しているのですが、タイミングがあって気になったものを気ままに受けていたので、どれを受けて受けてないのかが分からなくなってしまったので、じゃあRで確認するか…というのがモチベーションです。

ということで、RでconnpassのAPIを叩いて結果を見てみるというだけです。APIを叩くと言っても適当なアドレスにアクセスすればよいので難しいことは何もない。CGIの時代を思い出すわね…。

connpassのAPIについてはこちらを見てください。robots.txtも守ろうね。

connpass.com

Rのコードはざっくりこんな感じ。自分の受けたものを全て抜き出すだけです。件数は上限が100なので100を指定、orderも開催日時順にしています。

library(tidyverse)
library(httr)
library(jsonlite)

nis <- GET("https://connpass.com/api/v1/event/?nickname=niszet&count=100&order=2")
jsonlite::fromJSON(content(nis, "text"))  %>% as_tibble()%>% jsonlite::flatten() -> nis_df

owner_nicknameを一緒に指定するとどうもORになるっぽいので、特定のイベント主のイベントのうち、自分が受けたものをみるには events.owner_nicknameで絞り込めば良いですね。

受けてないものを見るならそれぞれのデータを取ってきて、events.event_idを指定してanti_joinとか。

あとハマりどころとしては、contentで抜き出した文字列のままでは触りづらいのでfromJSONに与えるとか、as_tibbleしたあとは階層化されているのでflatten通すべきとかですかね。RStudio上でView()で見ると階層が潰されて表示されているので、そのままsetdiffとかしようとするとよくわからないエラーが出て困った、みたいなハマりポイントもありましたが、データ構造は必ず何かしらの形で見ましょう。strだと情報過多になるので、tibbleの場合はglimpse()が良いでしょうな。

flatten前のデータはこんな構造。eventsがdata.frameになってることがここで気づける。

jsonlite::fromJSON(content(nis, "text"))  %>% as_tibble() %>% glimpse()
Rows: 71
Columns: 4
$ results_start     <int> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,...
$ results_returned  <int> 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 7...
$ results_available <int> 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 7...
$ events            <df[,21]> <data.frame[36 x 21]>

ということで。Rで色々出来ると楽しいですね!

追記

flattenですが、purrrパッケージにも同名の関数が含まれているため、jsonlite::を明示的にしたほうがよいでしょう(上記のコードではそうなってます)

また、data.frameにした後の構造は下記のようになります。

> glimpse(nis_df)
Rows: 72
Columns: 26
$ results_start             <int> 1, 1, 1...
$ results_returned          <int> 72, 72,...
$ results_available         <int> 72, 72,...
$ events.event_id           <int> 181686,...
$ events.title              <chr> "9/19 O...
$ events.catch              <chr> "オンラインで...
$ events.description        <chr> "<h1>ご参...
$ events.event_url          <chr> "https:...
$ events.started_at         <chr> "2020-0...
$ events.ended_at           <chr> "2020-0...
$ events.limit              <int> NA, NA,...
$ events.hash_tag           <chr> "osc20h...
$ events.event_type         <chr> "partic...
$ events.accepted           <int> 43, 131...
$ events.waiting            <int> 0, 0, 0...
$ events.updated_at         <chr> "2020-0...
$ events.owner_id           <int> 313042,...
$ events.owner_nickname     <chr> "nagais...
$ events.owner_display_name <chr> "nagais...
$ events.place              <chr> "オンライン"...
$ events.address            <chr> "オンライン"...
$ events.lat                <chr> NA, NA,...
$ events.lon                <chr> NA, NA,...
$ events.series.id          <int> 2186, 1...
$ events.series.title       <chr> "OSPN(O...
$ events.series.url         <chr> "https:...

また、例えば最近参加したTokyoRのイベントは下記のようにすれば良いです。このowner_nicknameが分かりづらいので(tokyorとかではない)どうしようっかな…って感じですけど…。イベントのURLからイベントのidは取れるので、それを指定してそこからownerを引っ張ってくるというのが良いかな。

nis_df %>% filter(events.owner_nickname=="kilometer") %>% select(events.title)
                   events.title
187回R勉強会@東京(#TokyoR)
286回R勉強会@東京(#TokyoR)
384回R勉強会@東京(#TokyoR)
483回R勉強会@東京(#TokyoR)
579回R勉強会@東京(#TokyoR)
677回R勉強会@東京(#TokyoR)

あるいは、events.series.titleがTokyo.Rであること、あるいはevents.series.urlを見てイベントのページを見つける、などなど、でしょうか。

tokyor.connpass.com

(Pandoc)BulletListの探索順序(メモ)

Lua filter特有かもしれないが…

Lua filterでBulletListの処理順序を確認するために、下記のようなコードを書いた。

function BulletList(el)
  print(pandoc.utils.stringify(el))
end

子要素が全部潰されて1つの文字列になってしまうが、そこは目をつぶるとして、これでどの順に処理されているか確認できる。

調査対象はこんな感じとした。

- para1
  - para1.1
    - para1.1.1
  - para1.2
    - para1.2.1
      - para1.2.1.1
- para2
  - para2.2
    - para2.2.1
      - para2.2.1.1

実行結果は下記のとおり

para1.1.1
para1.2.1.1
para1.2.1para1.2.1.1
para1.1para1.1.1para1.2para1.2.1para1.2.1.1
para2.2.1.1
para2.2.1para2.2.1.1
para2.2para2.2.1para2.2.1.1
para1para1.1para1.1.1para1.2para1.2.1para1.2.1.1para2para2.2para2.2.1para2.2.1.1

ASTはこんな感じ。

[BulletList
 [[Plain [Str "para1"]
  ,BulletList
   [[Plain [Str "para1.1"]
    ,BulletList
     [[Plain [Str "para1.1.1"]]]]
   ,[Plain [Str "para1.2"]
    ,BulletList
     [[Plain [Str "para1.2.1"]
      ,BulletList
       [[Plain [Str "para1.2.1.1"]]]]]]]]
 ,[Plain [Str "para2"]
  ,BulletList
   [[Plain [Str "para2.2"]
    ,BulletList
     [[Plain [Str "para2.2.1"]
      ,BulletList
       [[Plain [Str "para2.2.1.1"]]]]]]]]]]

とりあえず一番手前の要素の一番下まで潜っていって、横方向に順次見ていくような挙動ですね。この探索順序は一般的になんて呼ばれているのだろう(不勉強)

BulletListのタグはリストの最初の要素の手前にあるので、同階層で複数要素があっても対応するprintは1回だけです(para1.1とpara1.2は同階層だが、para1.1が先頭の表示が対応するBulletList)

フィルタを書くときは処理順に気をつけないといけないので、参考程度にメモです。

なお、バージョン依存があるかもしれません。今回は 2.10.1 を使用しています。

(Pandoc)BulletListの挙動についてメモ

仕様を理解すれば怖いことはない(はず)

なんかタイトルが記事内容とあってないので変えました…。元は Pandoc 2.7以前/以降でBulletListの挙動が異なるので注意 でした。が、実際2.7より前では挙動が違うので、この記事の結果と違う挙動のはずです。

なお、この話はMarkdown Readerの仕様です。他の記法において同じルールが適用できるかは未調査です。

ちょっと込み入った話になりますが、込み入ってる分わかりづらいので書き留めておきます。

発端はいつもの?K4氏の発言から…

バージョン

まず、この仕様はPandoc2.7以降の挙動です。更新履歴では Markdown Reader の "Improve tight/loose list handling (#5285). Previously the algorithm allowed list items with a mix of Para and Plain, which is never wanted." が相当します。

github.com

今回はPandocのバージョンは2.10.1ですが、2.7以降であれば同じ挙動のはずです。

$ pandoc --version
pandoc.exe 2.10.1
Compiled with pandoc-types 1.21, texmath 0.12.0.2, skylighting 0.8.5
Default user data directory: C:\Users\nisze\AppData\Roaming\pandoc
Copyright (C) 2006-2020 John MacFarlane
Web:  https://pandoc.org
This is free software; see the source for copying conditions.
There is no warranty, not even for merchantability or fitness
for a particular purpose.

実行例

最初に使用する例はこれです。

- para1
- para2
  - para2.1

  para2.2
- para3
  - para3.1
  - para3.2
- para4

  para5

これを、適当にhoge.mdなどの名前で保存したとして、PandocへのPATHが通っているとして、

pandoc -t native hoge.md

結果は次のようになります。

[BulletList
 [[Para [Str "para1"]]
 ,[Para [Str "para2"]
  ,BulletList
   [[Plain [Str "para2.1"]]]
  ,Para [Str "para2.2"]]
 ,[Para [Str "para3"]
  ,BulletList
   [[Plain [Str "para3.1"]]
   ,[Plain [Str "para3.2"]]]]
 ,[Para [Str "para4"]
  ,Para [Str "para5"]]]]

これではぱっと見よくわからないので、 -t html した結果も貼っておきます。

f:id:niszet:20200812001913p:plain

これを見ればわかりますが、para2.2とpara5は頭に・がつきません(記法的にも正しいですね)

また、細かい挙動はまた書きますが、この時点ではレベル1のリスト(一番左側にあるリスト)の要素(●)はParaに、インデントがついた、レベル2のリスト要素(○)はPlainになっていることが見れます。

また、元のマークダウン上ではわかりにくいですが、para2.2とpara5は先頭にホワイトスペースが2文字入っています。

-t htmlに変更して、出力されているhtmlを確認すると、

<ul>
<li><p>para1</p></li>
<li><p>para2</p>
<ul>
<li>para2.1</li>
</ul>
<p>para2.2</p></li>
<li><p>para3</p>
<ul>
<li>para3.1</li>
<li>para3.2</li>
</ul></li>
<li><p>para4</p>
<p>para5</p></li>
</ul>

このようになります。つまり、Paraのところは素直に

タグが挿入されていて、Plainの場合はそれがない、という違いです。

さて、この違い、LooseとTightのリスト(それぞれ、ParaとPlainに相当)ですが、これはBulletListの要素間に空行を入れているかどうかで決まります。

シンプルな例では、

- para1
- para2
  - para2.1

は、要素間が詰まっている(空行がない)のでTightリスト、つまりPlainが使われ、

[BulletList
 [[Plain [Str "para1"]]
 ,[Plain [Str "para2"]
  ,BulletList
   [[Plain [Str "para2.1"]]]]]]

各要素間に空行を入れると、Looseリストになります(つまり、Paraが使われる)

- para1

- para2

  - para2.1

変換すると、

[BulletList
 [[Para [Str "para1"]]
 ,[Para [Str "para2"]
  ,BulletList
   [[Plain [Str "para2.1"]]]]]]

ここで、para2.1の方はParaではなくPlainになっている点に注意。これは、「同レベルの要素間」に空行が入らないとParaにはならないためです。また、次の例で確認できますが、このレベル2のリスト間は独立しています。

- para1
- para2
  - para2.1

  - para2.2
- para3
  - para3.1
  - para3.2

結果は次のようになります。

[BulletList
 [[Plain [Str "para1"]]
 ,[Plain [Str "para2"]
  ,BulletList
   [[Para [Str "para2.1"]]
   ,[Para [Str "para2.2"]]]]
 ,[Plain [Str "para3"]
  ,BulletList
   [[Plain [Str "para3.1"]]
   ,[Plain [Str "para3.2"]]]]]]

para2.1/2.2の方は空行の影響を受けてParaとなりますが、para3.1/3.2は空行を持っていないのでPlainとなり、それぞれ独立していることがわかります。一方、トップレベルは共通しているので、どこかに空行があれば影響をうけます。

- para1
- para2
  - para2.1
  - para2.2
- para3

  - para3.1
  - para3.2

結果は次のように。

[BulletList
 [[Para [Str "para1"]]
 ,[Para [Str "para2"]
  ,BulletList
   [[Plain [Str "para2.1"]]
   ,[Plain [Str "para2.2"]]]]
 ,[Para [Str "para3"]
  ,BulletList
   [[Plain [Str "para3.1"]]
   ,[Plain [Str "para3.2"]]]]]]

さて、最初の例に戻ります(再掲)。これ

- para1
- para2
  - para2.1

  para2.2
- para3
  - para3.1
  - para3.2
- para4

  para5

を変形すると、以下のようになりました。

[BulletList
 [[Para [Str "para1"]]
 ,[Para [Str "para2"]
  ,BulletList
   [[Plain [Str "para2.1"]]]
  ,Para [Str "para2.2"]]
 ,[Para [Str "para3"]
  ,BulletList
   [[Plain [Str "para3.1"]]
   ,[Plain [Str "para3.2"]]]]
 ,[Para [Str "para4"]
  ,Para [Str "para5"]]]]

まずレベル2の要素を見るとPlainです。3.1/3.2は明らかで良いですが、2.1の方は少し不思議ですね。これは、2.2はリストの〇を持ってないことから、リストの要素としてみなされていないようです。ただし、この行は先頭にホワイトスペースが2つあり、リストの中に入っています(nativeのASTより明らかですが)

これを取り払うとリストに含まれないと扱われます。その場合、以降が文字列と扱われるので、後半のリストをリストのまま扱いたい場合はpara2.2の上下に空行を入れる必要があります。

- para1
- para2
  - para2.1
  
para2.2

- para3
  - para3.1
  - para3.2
- para4

  para5

結果として、para2.2は独立した段落となり、上下のリストは別のリストとなります。これは、リストを区切りたいときに必要となるため覚えておくと良いでしょう。なお、リストを分割したいが、間に文字を入れたくない場合は、コメント行を入れるなど表示されないが段落として扱ってくれる文字列を入れることで対応します。

[BulletList
 [[Plain [Str "para1"]]
 ,[Plain [Str "para2"]
  ,BulletList
   [[Plain [Str "para2.1"]]]]]
,Para [Str "para2.2"]
,BulletList
 [[Para [Str "para3"]
  ,BulletList
   [[Plain [Str "para3.1"]]
   ,[Plain [Str "para3.2"]]]]
 ,[Para [Str "para4"]
  ,Para [Str "para5"]]]]

残るはpara5の?挙動です。この行は先頭にホワイトスペースが2つあるのでリストの中に取り込まれます(ASTより明らか)

この行を取り払う、あるいはpara4との間の空行を取り払うとトップレベルはPlainになります。

実は、ここまで書いてから気づきましたが、最初のpara2.2の箇所は行頭に置くべきはホワイトスペース4つで、para2.1とインデントを揃える必要がありました。

- para1
- para2
  - para2.1
  
    para2.2
- para3
  - para3.1
  - para3.2
- para4

  para5

すると、

[BulletList
 [[Para [Str "para1"]]
 ,[Para [Str "para2"]
  ,BulletList
   [[Para [Str "para2.1"]
    ,Para [Str "para2.2"]]]]
 ,[Para [Str "para3"]
  ,BulletList
   [[Plain [Str "para3.1"]]
   ,[Plain [Str "para3.2"]]]]
 ,[Para [Str "para4"]
  ,Para [Str "para5"]]]]

のようになり、para2.1もParaの対象となります。これは、ホワイトスペースを追加したことでpara2.2が正しくレベル2の要素として扱われたためです。これはpara4とpara5の関係と同じで(para5も行頭に2つホワイトスペースがあるから)、この挙動はリストのレベルによって変わらないことがわかります。

まとめると…

リストのアイテム(先頭に-がついているもの)の次の行(正確には空行はいくらあっても良い)が空行であり、その後同レベルの要素が続く場合、そのリストのアイテムのレベルはLoose扱いとなる。これは1つでも空行があればその要素群全体が対象です。

いくつか例を。以下の例ではpara3.4の後に空行がありますが、その後に来るのは同レベルの要素ではないのでPlainになります(最初のpara2.1の例と同じ。同レベルではないため)

- para3
  - para3.1
  - para3.2
  - para3.3
  - para3.4
  
- para4

また、次のように間の空行がいくつあっても良いです。para3.1-3.4はParaとなり、空行は出力時には消えます。そのため、リストの要素間で明示的にスペースをあけるようには使えません(そもそも項目の列挙をする際に、そのような配置をしてはいけないですが)

- para3
  - para3.1
  - para3.2
  
  
  
  
  
  - para3.3
  - para3.4
  
- para4

…ということで、私も途中まで勘違いしている個所がありましたが、PandocのLoose/Tight Listの挙動について簡単にまとめました。インデントでリストのレベルを制御するのはちょっとわかりづらいですね(para2.1/2.2の例を思い浮かべながら)

もうちょっとシュッと説明できる気がしますが、折角書いたのでこのままとし、今後の課題とします。