はじめに
自己紹介
H1ronoです。趣味でiOSでプログラミングをしています。
この記事でやること
Pythonista3とショートカットを使用し、iOSで.md
から.pdf
を生成する方法を紹介します。
※筆者の環境はiPhone 8, iOS 13.4.1(OSはこの記事を書いている時点で最新)です。
目次
大まかな目次です。
準備
1. Pythonista
1-1. モジュール
マークダウンからHTMLへの変換にmistuneモジュール、コードハイライト用にpygmentsを使用します。
pygmentsは最初からPythonistaにインストールされています。Pythonistaに標準モジュール以外でインストールされているモジュールのリストはこちら(Pythonista Modules)にあります。
StaSh(Pythonista用のサードパーティシェル)がまだ入っていない方は、こちら(Pythonista 3 で双方向のファイル転送 - Qiita)などを参考にインストールして下さい。
StaShが入ったら、StaShを起動して以下のコマンドを実行します。
pip install mistune
これでmistuneのインストールは完了です。モジュールの準備は以上になります。
1-2. ファイル
Pythonista上で新しくフォルダを作成します。そのフォルダ上には以下のファイルを作成します。
main.py
sample.md
template.html
各ファイルの内容は以下になります(コードの解説は後でします)。
import re
import mistune
from pygments import highlight
from pygments.lexers import get_lexer_by_name
from pygments.formatters import HtmlFormatter
import shortcuts
class MyRenderer(mistune.Renderer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.formatter = HtmlFormatter(cssclass='hll')
def block_code(self, code, lang=None):
try:
l, _, filename = lang.partition(':')
except AttributeError:
l, filename = '', ''
try:
lexer = get_lexer_by_name(l)
except:
res = f'\n<div class="hll"><pre>{code}</pre></div>\n'
else:
res = highlight(code, lexer, self.formatter)
if filename:
new = f'<pre><span class="file">{filename}</span>\n\n'
else:
new = '<pre>\n\n'
res = res.replace('<pre>', new, 1)
return res
def _md2html():
renderer = MyRenderer(hard_wrap=True)
markdown = mistune.Markdown(renderer=renderer)
def main(source):
content = markdown(source)
return content
return main
md2html = _md2html()
def _render_details():
flag = re.DOTALL
detail_ptn = re.compile('<details>.*?</details>', flag)
summary_ptn = re.compile('<summary>.*?</summary>', flag)
div_ptn = re.compile('<div>.*?</div>', flag)
innertext_ptn = re.compile('>.*?</', flag)
def main(html):
details = detail_ptn.findall(html)
result = html
for dt in details:
summaries = summary_ptn.findall(dt)
for smry in summaries:
ptext = innertext_ptn.findall(smry)[0][1:-2]
ntext = md2html(ptext)[3:-5]
result = result.replace(ptext, ntext, 1)
divs = div_ptn.findall(dt)
for dv in divs:
ptext = innertext_ptn.findall(dv)[0][1:-2]
ntext = md2html(ptext)
result = result.replace(ptext, ntext, 1)
return result
return main
render_details = _render_details()
def _md2html_main():
with open('template.html', 'r', encoding='utf-8') as f:
template = f.read()
def main(source):
content = md2html(source)
content = render_details(content)
return template.replace('{{CONTENT}}', content)
return main
md2html_main = _md2html_main()
del _md2html, _render_details, _md2html_main
if __name__ == '__main__':
with open('sample.md', 'r', encoding='utf-8') as f:
source = f.read()
html = md2html_main(source)
shortcuts.open_shortcuts_app('html2pdf', html)
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Title</title>
<style>
html {
background-color: white;
font-family: "Avenir Next", sans-serif;
font-size: 15px;
}
body {
padding: 10px;
margin: 10px;
}
h1 { font-size: 29px; }
h2 { font-size: 24px; }
h3 { font-size: 21px; }
h4 { font-size: 19px; }
h5 { font-size: 18px; }
h6 { font-size: 17px; }
blockquote {
color: gray;
border-width: 3px;
border-style: none none none solid;
border-color: gray;
padding: 10px;
margin: 10px;
}
table {
padding: 0px;
border: 1px solid black;
}
th, td{
border: 0.5px solid gray;
}
code, pre {
font-family: "Menlo", monospace;
}
code {
padding: 0px 5px;
}
pre {
padding: 3px 10px 20px 10px;
overflow-wrap: break-word;
}
.file {
background-color: gray;
color: white;
padding: 3px;
}
/* cool-glow style */
.hll { background-color: #0A0D2A; color: #E0E0E0; }
.gd { color: #A00000; } /* Generic.Deleted */
.ge { font-style: italic; } /* Generic.Emph */
.gr { color: #E5ABB3; } /* Generic.Error */
.gh { color: #E0E0E0; font-weight: bold; } /* Generic.Heading */
.gi { color: #E0E0E0; } /* Generic.Inserted */
.go { color: #E0E0E0; } /* Generic.Output */
.gp { color: #E0E0E0; font-weight: bold; } /* Generic.Prompt */
.gs { font-weight: bold; } /* Generic.Strong */
.gu { color: #E0E0E0; font-weight: bold; } /* Generic.Subheading */
.gt { color: #E5ABB3; } /* Generic.Traceback */
.o { color: #E0E0E0; } /* Operator */
.ow { color: #40F1E2; } /* Operator.Word */
.k { color: #40F1E2; } /* Keyword */
.kc { color: #40F1E2; } /* Keyword.Constant */
.kd { color: #40F1E2; } /* Keyword.Declaration */
.kn { color: #40F1E2; } /* Keyword.Namespace */
.kp { color: #40F1E2; } /* Keyword.Pseudo */
.kr { color: #40F1E2; } /* Keyword.Reserved */
.kt { color: #40F1E2; } /* Keyword.Type */
.m { color: #F8FBB1; } /* Literal.Number */
.mb { color: #F8FBB1; } /* Literal.Number.Bin */
.mf { color: #F8FBB1; } /* Literal.Number.Float */
.mh { color: #F8FBB1; } /* Literal.Number.Hex */
.mi { color: #F8FBB1; } /* Literal.Number.Integer */
.il { color: #F8FBB1; } /* Literal.Number.Integer.Long */
.mo { color: #F8FBB1; } /* Literal.Number.Oct */
.s { color: #A0FAA2; } /* Literal.String */
.sa { color: #A0FAA2; } /* Literal.String.Affix */
.sb { color: #A0FAA2; } /* Literal.String.Backtick */
.sc { color: #A0FAA2; } /* Literal.String.Char */
.dl { color: #A0FAA2; } /* Literal.String.Delimiter */
.s2 { color: #A0FAA2; } /* Literal.String.Double */
.se { color: #A0FAA2; } /* Literal.String.Escape */
.sh { color: #A0FAA2; } /* Literal.String.Heredoc */
.si { color: #A0FAA2; } /* Literal.String.Interpol */
.sr { color: #A0FAA2; } /* Literal.String.Regex */
.s1 { color: #A0FAA2; } /* Literal.String.Single */
.ss { color: #A0FAA2; } /* Literal.String.Symbol */
.sx { color: #A0FAA2; } /* Literal.String.Other */
.sd { color: #A0FAA2; font-style: italic; } /* Literal.String.Doc */
.nv { color: #E0E0E0; } /* Name.Variable */
.vc { color: #6DB6F2; } /* Name.Variable.Class */
.vg { color: #E0E0E0; } /* Name.Variable.Global */
.vi { color: #E0E0E0; } /* Name.Variable.Instance */
.vm { color: #E0E0E0; } /* Name.Variable.Magic */
.na { color: #E0E0E0; } /* Name.Attribute */
.nb { color: #40F1E2; } /* Name.Builtin */
.bp { color: #40F1E2; } /* Name.Builtin.Pseudo */
.nf { color: #6DB6F2; } /* Name.Function */
.fm { color: #6DB6F2; } /* Name.Function.Magic */
.nc { color: #6DB6F2; } /* Name.Class */
.nn { color: #C29AD3; } /* Name.Namespace */
.no { color: #E0E0E0; } /* Name.Constant */
.nd { color: #C1DBF2; } /* Name.Decorator */
.ni { color: #E0E0E0; font-weight: bold; } /* Name.Entity */
.ne { color: #E5ABB3; } /* Name.Exception */
.nl { color: #E0E0E0; } /* Name.Label */
.nt { color: #40F1E2; } /* Name.Tag */
.c { color: #AEAEAE; font-style: italic; } /* Comment */
.ch { color: #AEAEAE; font-style: italic; } /* Comment.Hashbang */
.cm { color: #AEAEAE; font-style: italic; } /* Comment.Multiline */
.c1 { color: #AEAEAE; font-style: italic; } /* Comment.Single */
.cs { color: #AEAEAE; font-style: italic; } /* Comment.Special */
.cp { color: #AEAEAE; font-style: italic; } /* Comment.Preproc */
.cpf { color: #AEAEAE; font-style: italic; } /* Comment.PreprocFile */
.err { border: 1px solid #E5ABB3; } /* Error */
.w { color: #bbbbbb; } /* Text.Whitespace */
.p { color: #E0E0E0; } /* Punctuation */
</style>
<script type="text/javascript">
function funcColoring() {
let pElements = document.getElementsByClassName("p");
for(let element of pElements) {
if(element.textContent[0] != "(") { continue; }
let prev = element.previousSibling;
if(prev.className != "n") { continue; }
prev.className = "nf";
}
}
</script>
</head>
<body>
<div class="md-content">
{{CONTENT}}
</div>
<script type="text/javascript">
funcColoring();
</script>
</body>
</html>
# H1
## H2
### H3
#### H4
##### H5
###### H6
* 1
* 1.1
* 1.2
* 2
* 2.1
* 2.1
* 3
*italic*, **bold**, ***italic-bold***, `inline code`, [qiita-link](https://qiita.com/)[^1]
\[^1]:注釈の内容です。
|左寄せ(1-1)|右寄せ(2-1)|中央寄せ(3-1)|
|:-----|-----:|:-----:|
|1-2|2-2|3-2|
|1-3|2-3|3-3|
この記事の冒頭より引用
> **この記事でやること**
> [Pythonista3](https://apps.apple.com/jp/app/pythonista-3/id1085978097)と[ショートカット](https://apps.apple.com/jp/app/ショートカット/id915249334)を使用し、iOSで`.md`から`.pdf`を作成しました。
> 詳細を共有することで、iOSの開発能力が伝わればなと思っています。
100回Hello, world!するプログラム
\```python
exec('print("Hello, world!");' * 100)
`\``
マークダウンのエスケープは全部無視してください。これでPythonista側の準備は完了です。
2. ショートカット
ショートカット側では、ショートカットを1つ作ります。内容は次の写真の通りです。
ショートカットの名前はhtml2pdf
で変えないでください。理由は後で説明します。
解説
main.py
メインのスクリプトです。このコードを走らせると、sample.md
のマークダウンをHTMLに変換し、それをショートカットに渡してPDFを作成します。
グローバル変数を減らすためにクロージャを使用しています。del ...
とあるのは、クロージャ生成に使用した関数はもう使わないためです。
Pythonistaにインストールされたmistuneのバージョンは0.8.4
です。バージョン0.8.4
のドキュメントはこちらになります。このドキュメントのRenderer
節にpygmentsを使用したコードハイライトの例があります。MyRenderer
クラスはこのコードを参考にしました。Qiitaっぽくファイル名を示すことができるようにしました。
各関数の説明:
-
md2html(source)
:source
をマークダウンからHTMLに変換して、HTMLを返します。確認した限りだと、details
の内容がマークダウンのままだという問題があります。 -
render_details(html)
:html
内のdetails
タグの内容のマークダウンをHTMLに置き換えて返します。 -
md2html_main(source)
:source
をマークダウンからHTMLに変換します(details
タグの内容も変換します)。変換したHTMLをtemplate.html
に埋め込み、その結果を返します。
if __name__ == '__main__':
節では以下の処理を行います:
-
sample.md
の内容を読み取り、md2html_main
に渡します。 -
1
の結果をhtml2pdf
のショートカットに渡します。
2
の処理を行うために、shortcuts.open_shortcuts_app
関数を使用しています。この関数はx-callbackを使用してショートカットappのショートカットを呼び出します。shortcuts.open_shortcuts_app('html2pdf', html)
だとwebbrowser.open(f'shortcuts://run-shortcut?name=html2pdf&input={urllib.parse.quote(html)}')
と同値となります。ショートカットの名前を指定したのはPythonistaから呼び出すためです。詳しくはこちら(shortcutsモジュールのドキュメント)
template.html
テンプレートのHTMLです。スタイルの指定が主な役割となっています。
基本的なスタイルはQiita、コードハイライトはPythonistaのCool-glowというテーマを参考にしています。お好みにいじって下さい。フォントはPythonistaでファイル編集時に右下に出る+
のボタンから選べるものは使用可能なようです。
pygmentsは変数名が関数呼び出しとして使われていても、それ以外のものと区別せずにハイライトします。funcColoring
は、()
が直後にある変数名(関数呼び出しとして使用されている変数名)について、そのCSSのクラスを変数名を示すものから関数名を示すものに変換する関数です。()
で関数呼び出しを行う言語(Python, JavaScript, Swiftなど)に関しては、関数呼び出しをハイライトします。他の言語には対応していません。ご容赦ください。
{{CONTENT}}
の部分にマークダウンから変換したHTMLが埋め込まれます。
sample.md
実験的にpdfに変換するマークダウンです。
html2pdf(ショートカット)
入力として受け取ったHTMLからPDFを作成するショートカットです。リッチテキストというのは、HTMLやマークダウンをレンダリングした後の文書を指すようです。
実行結果
いい感じですね。
終わりに
iOSネイティブなやり方でmd2pdfを実装しました。最初はPythonistaのみでやる予定だったんですが、HTMLからPDFが難しそうだという結論に至ったため、このような方法を採用しました。苦肉の策だったんですが、使ってみてなかなか便利だと感じました。他にも使い道がありそうです。
もちろんですが、この記事はPythonistaで書きました。マークダウン用のハイライトがあって書きやすかったです。
スクショの加工にはTailorというappを使用しました。
ここおかしいんじゃない、ここわかんないとかあったら、なんでも言って下さい。
おまけ
共有シートからmd2pdf
main.py
などが置いてあるディレクトリ上に_main.py
を作成します。_main.py
内には以下の内容を記述します:
import sys
import clipboard
from main import md2html_main
if __name__ == '__main__':
try:
md = sys.argv[1]
except IndexError:
with open('sample.md', 'r', encoding='utf-8') as f:
md = f.read()
html = md2html_main(md)
print(html)
clipboard.set(html)
shortcuts.open_shortcuts_app()
次に、md2pdf
という名前のショートカットを作成します。以下の内容を設定します:
Pythonistaの呼び出しに次のようなURLを使用しました。
pythonista://md2pdf/_main?action=run&argv=[URLエンコード済みのテキスト]&root=icloud
これは_main.py
をiCloud/md2pdf
のフォルダ内に作成した場合のものです。This iPhone/foo
のフォルダ内に作成した場合のURLは次のようになります。
pythonista://foo/_main?action=run&argv=[URLエンコード済みのテキスト]
より詳しい情報はこちら(The Pythonista URL Scheme)
これで準備完了です。共有シートから先ほど作成したショートカットを呼び出すことで、共有対象のマークダウンをPDFに変換できます。
Pythonistaで編集中のマークダウンをクイックルック
ui.WebView
とeditor
を使うことで実装できます。quicklook_markdown.py
をmain.py
などのディレクトリに追加します。
import editor, console, ui
from main import md2html_main
def _quicklook():
size = ui.get_window_size()
frame = (0, 0, *size)
v = ui.WebView(frame=frame)
def main(html):
v.load_html(html)
v.present('fullscreen')
return main
quicklook = _quicklook()
del _quicklook
if __name__ == '__main__':
console.clear()
md_source = editor.get_text()
html = md2html_main(md_source)
print(html)
quicklook(html)
次に、このスクリプトを他のファイルの編集中に実行できるようにします。
-
quicklook_markdown.py
が開かれている画面で右上にあるレンチマーク(スパナのボタン)を押します。 - "Reformat Code", "Python 2 to 3"などのオプションの中から"Shortcuts..."を選択します。
- "Shortcuts..."を選択した後に表示されるメニューの中から"Editor Action"を選択します。
- 今登録されているショートカットのリストが表示されます。最後尾にある"+"を押します。
- ショートカット追加のオプションが表示されます。"Run Script"は"quicklook_markdown.py"、"Arguments"は空、"Reset Environment"はオンになっている必要があります。それ以外はお好みに指定して下さい。
これで準備完了です。試しにsample.py
を開いてからレンチマークを押し、先ほど追加したショートカットを選択してみて下さい。マークダウンがリッチテキストで表示されるかと思います。編集中のマークダウンがiPhone内にあって、quicklook_markdown.py
がiCloud上にあっても、このショートカットは使用可能です。
より詳しい情報についてはこちら(App Extensions and Shortcuts)
template.htmlのCSSを外部ファイルに記述
main.py
などがあるディレクトリでtemplate2.html
とstyle.css
、main2.py
を追加します。
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Title</title>
<style>
/* CSS */
</style>
<script type="text/javascript">
function funcColoring() {
let pElements = document.getElementsByClassName("p");
for(let element of pElements) {
if(element.textContent[0] != "(") { continue; }
let prev = element.previousSibling;
if(prev.className != "n") { continue; }
prev.className = "nf";
}
}
</script>
</head>
<body>
<div class="md-content">
{{CONTENT}}
</div>
<script type="text/javascript">
funcColoring();
</script>
</body>
</html>
html {
background-color: white;
font-family: "Avenir Next", sans-serif;
font-size: 20px;
}
body {
padding: 10px;
margin: 10px;
}
h1 { font-size: 34px; }
h2 { font-size: 29px; }
h3 { font-size: 26px; }
h4 { font-size: 24px; }
h5 { font-size: 23px; }
h6 { font-size: 22px; }
blockquote {
color: gray;
border-width: 3px;
border-style: none none none solid;
border-color: gray;
padding: 10px;
margin: 10px;
}
table {
padding: 0px;
border: 1px solid black;
}
th, td{
border: 0.5px solid gray;
}
code, pre {
font-family: "Menlo", monospace;
}
code {
padding: 0px 5px;
}
pre {
padding: 3px 10px 20px 10px;
overflow-wrap: break-word;
}
.file {
background-color: gray;
padding: 3px;
}
/* cool-glow style */
.hll { background-color: #0A0D2A; color: #E0E0E0; }
.gd { color: #A00000; } /* Generic.Deleted */
.ge { font-style: italic; } /* Generic.Emph */
.gr { color: #E5ABB3; } /* Generic.Error */
.gh { color: #E0E0E0; font-weight: bold; } /* Generic.Heading */
.gi { color: #E0E0E0; } /* Generic.Inserted */
.go { color: #E0E0E0; } /* Generic.Output */
.gp { color: #E0E0E0; font-weight: bold; } /* Generic.Prompt */
.gs { font-weight: bold; } /* Generic.Strong */
.gu { color: #E0E0E0; font-weight: bold; } /* Generic.Subheading */
.gt { color: #E5ABB3; } /* Generic.Traceback */
.o { color: #E0E0E0; } /* Operator */
.ow { color: #40F1E2; } /* Operator.Word */
.k { color: #40F1E2; } /* Keyword */
.kc { color: #40F1E2; } /* Keyword.Constant */
.kd { color: #40F1E2; } /* Keyword.Declaration */
.kn { color: #40F1E2; } /* Keyword.Namespace */
.kp { color: #40F1E2; } /* Keyword.Pseudo */
.kr { color: #40F1E2; } /* Keyword.Reserved */
.kt { color: #40F1E2; } /* Keyword.Type */
.m { color: #F8FBB1; } /* Literal.Number */
.mb { color: #F8FBB1; } /* Literal.Number.Bin */
.mf { color: #F8FBB1; } /* Literal.Number.Float */
.mh { color: #F8FBB1; } /* Literal.Number.Hex */
.mi { color: #F8FBB1; } /* Literal.Number.Integer */
.il { color: #F8FBB1; } /* Literal.Number.Integer.Long */
.mo { color: #F8FBB1; } /* Literal.Number.Oct */
.s { color: #A0FAA2; } /* Literal.String */
.sa { color: #A0FAA2; } /* Literal.String.Affix */
.sb { color: #A0FAA2; } /* Literal.String.Backtick */
.sc { color: #A0FAA2; } /* Literal.String.Char */
.dl { color: #A0FAA2; } /* Literal.String.Delimiter */
.s2 { color: #A0FAA2; } /* Literal.String.Double */
.se { color: #A0FAA2; } /* Literal.String.Escape */
.sh { color: #A0FAA2; } /* Literal.String.Heredoc */
.si { color: #A0FAA2; } /* Literal.String.Interpol */
.sr { color: #A0FAA2; } /* Literal.String.Regex */
.s1 { color: #A0FAA2; } /* Literal.String.Single */
.ss { color: #A0FAA2; } /* Literal.String.Symbol */
.sx { color: #A0FAA2; } /* Literal.String.Other */
.sd { color: #A0FAA2; font-style: italic; } /* Literal.String.Doc */
.nv { color: #E0E0E0; } /* Name.Variable */
.vc { color: #6DB6F2; } /* Name.Variable.Class */
.vg { color: #E0E0E0; } /* Name.Variable.Global */
.vi { color: #E0E0E0; } /* Name.Variable.Instance */
.vm { color: #E0E0E0; } /* Name.Variable.Magic */
.na { color: #E0E0E0; } /* Name.Attribute */
.nb { color: #40F1E2; } /* Name.Builtin */
.bp { color: #40F1E2; } /* Name.Builtin.Pseudo */
.nf { color: #6DB6F2; } /* Name.Function */
.fm { color: #6DB6F2; } /* Name.Function.Magic */
.nc { color: #6DB6F2; } /* Name.Class */
.nn { color: #C29AD3; } /* Name.Namespace */
.no { color: #E0E0E0; } /* Name.Constant */
.nd { color: #C1DBF2; } /* Name.Decorator */
.ni { color: #E0E0E0; font-weight: bold; } /* Name.Entity */
.ne { color: #E5ABB3; } /* Name.Exception */
.nl { color: #E0E0E0; } /* Name.Label */
.nt { color: #40F1E2; } /* Name.Tag */
.c { color: #AEAEAE; font-style: italic; } /* Comment */
.ch { color: #AEAEAE; font-style: italic; } /* Comment.Hashbang */
.cm { color: #AEAEAE; font-style: italic; } /* Comment.Multiline */
.c1 { color: #AEAEAE; font-style: italic; } /* Comment.Single */
.cs { color: #AEAEAE; font-style: italic; } /* Comment.Special */
.cp { color: #AEAEAE; font-style: italic; } /* Comment.Preproc */
.cpf { color: #AEAEAE; font-style: italic; } /* Comment.PreprocFile */
.err { border: 1px solid #E5ABB3; } /* Error */
.w { color: #bbbbbb; } /* Text.Whitespace */
.p { color: #E0E0E0; } /* Punctuation */
import shortcuts
from main import md2html, render_details
def _md2html_main():
with open('template2.html', 'r', encoding='utf-8') as f:
template = f.read()
with open('style.css', 'r', encoding='utf-8') as f:
style = f.read()
template = template.replace('/* CSS */', style, 1)
def main(source):
content = md2html(source)
content = render_details(content)
return template.replace('{{CONTENT}}', content)
return main
md2html_main = _md2html_main()
del _md2html_main
if __name__ == '__main__':
with open('sample.md', 'r', encoding='utf-8') as f:
source = f.read()
html = md2html_main(source)
shortcuts.open_shortcuts_app('html2pdf', html)
Python側でCSSの内容を埋め込むことで実装しました。link
で指定したかったのですが、上手い方法が見つからなかったため、このような方法にしました。やり方を知っている方はぜひ教えて下さい。
筆者が使用中のコード
現在、筆者のiPhoneに入っているソースコード、ショートカットです(更新日:7月2日)。前述したものとの大きな変更点は、以下の通りです:
-
details
の内容がマークダウンのままの問題を、前述のものとは別の方法で解決しました。 - HTML生成時、
title
属性がマークダウン内に最初に出てくるh1
タグの内容に置き換えられるようになりました。h1
タグがない場合、title
属性はTitle
となります。 -
こちら(ページ内リンク - Qiita)を参考に、見出しに
id
をつけました(PDFに変換すると意味がなくなります)。 - 前述のやり方で、CSSを外部ファイルに記述しました。
import re
import mistune
from pygments import highlight
from pygments.lexers import get_lexer_by_name
from pygments.formatters import HtmlFormatter
import shortcuts
class MyRenderer(mistune.Renderer):
def __init__(self):
super().__init__()
self.formatter = HtmlFormatter(cssclass='hll')
def convert(self, text):
res = mistune.markdown(text, renderer=self)
if res.startswith('<p>') and res.endswith('</p>\n'):
res = res[3:-5]
return res
def header(self, text, level, raw=None):
h = super().header(text, level)
if not raw:
return h
r = (
raw.replace('(', '')
.replace(')', '')
.replace('.', '')
.replace(' ', '-')
)
return h.replace(
f'<h{level}>',
f'<h{level} id="{r}">',
1)
def block_code(self, code, lang=None):
try:
l, _, filename = lang.partition(':')
except AttributeError:
l, filename = '', ''
try:
lexer = get_lexer_by_name(l)
except:
res = f'\n<div class="hll"><pre>{code}</pre></div>\n'
else:
res = highlight(code, lexer, self.formatter)
if filename:
new = f'<pre><span class="file">{filename}</span>\n\n'
else:
new = '<pre>\n\n'
res = res.replace('<pre>', new, 1)
s1, s2, s3 = res.rpartition('</pre>')
s1 = (
(s1.endswith('\n\n') and s1)
or ((s1.endswith('\n') and (s1 + '\n'))
or (s1 + '\n\n'))
)
res = s1 + s2 + s3
return res
def inline_html(self, html):
start_tag = re.findall('<.+?>', html)[0]
try:
end_tag = re.findall('</.+?>', html)[-1]
except IndexError:
return html
sl, el = len(start_tag), len(end_tag)
text = html[sl:-el]
result = (
start_tag
+ self.convert(text)
+ end_tag
)
return result
block_html = inline_html
def _md2html():
renderer = MyRenderer()
convertor = mistune.Markdown(renderer=renderer)
def main(source):
content = convertor(source)
return content
return main
md2html = _md2html()
def _md2html_main():
with open('template.html', 'r', encoding='utf-8') as h,\
open('style.css', 'r', encoding='utf-8') as c:
template = h.read()
style = c.read()
template = template.replace('/* CSS */', style, 1)
h1_ptn = re.compile('<h1.*?>.*?</h1>')
intext_ptn = re.compile('>.*<')
def get_title(html):
h1 = h1_ptn.findall(html)[0]
title = intext_ptn.findall(h1)[0]
return title[1:-1]
def main(source):
content = md2html(source)
result = template.replace('{{CONTENT}}', content)
try:
title = get_title(result)
except IndexError:
pass
else:
result = result.replace('Title', title, 1)
return result
return main
md2html_main = _md2html_main()
del _md2html, _md2html_main
if __name__ == '__main__':
with open('sample.md', 'r', encoding='utf-8') as f:
source = f.read()
html = md2html_main(source)
shortcuts.open_shortcuts_app('html2pdf', html)
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Title</title>
<style>
/* CSS */
</style>
<script type="text/javascript">
function funcColoring() {
let pElements = document.getElementsByClassName("p");
for(let element of pElements) {
if(element.textContent[0] != "(") { continue; }
let prev = element.previousSibling;
if(prev.className != 'n') { continue; }
prev.className = 'nf';
}
}
</script>
</head>
<body>
<div class="md-content">
{{CONTENT}}
</div>
<script type="text/javascript">
funcColoring();
</script>
</body>
</html>
html {
background-color: white;
font-family: "Avenir Next", sans-serif;
font-size: 15pt;
}
body {
padding: 10px;
margin: 10px;
}
h1 { font-size: 29pt; }
h2 { font-size: 24pt; }
h3 { font-size: 21pt; }
h4 { font-size: 19pt; }
h5 { font-size: 18pt; }
h6 { font-size: 17pt; }
blockquote {
color: gray;
border-width: 3px;
border-style: none none none solid;
border-color: gray;
padding: 10px;
margin: 10px;
}
table {
border-collapse: collapse;
}
th, td{
border: 1px solid black;
}
dt {
font-weight: bold;
}
img {
width: 100%;
}
code, pre {
font-family: "Menlo", monospace;
}
code {
padding: 0px 5px;
}
pre {
padding: 3px 10px;
overflow-wrap: break-word;
}
.file {
background-color: gray;
padding: 3px;
}
/* cool-glow style */
.hll {
background-color: #0A0D2A;
color: #E0E0E0;
}
.gd { color: #A00000; } /* Generic.Deleted */
.ge { font-style: italic; } /* Generic.Emph */
.gr { color: #E5ABB3; } /* Generic.Error */
.gh { color: #E0E0E0; font-weight: bold; } /* Generic.Heading */
.gi { color: #E0E0E0; } /* Generic.Inserted */
.go { color: #E0E0E0; } /* Generic.Output */
.gp { color: #E0E0E0; font-weight: bold; } /* Generic.Prompt */
.gs { font-weight: bold; } /* Generic.Strong */
.gu { color: #E0E0E0; font-weight: bold; } /* Generic.Subheading */
.gt { color: #E5ABB3; } /* Generic.Traceback */
.o { color: #E0E0E0; } /* Operator */
.ow { color: #40F1E2; } /* Operator.Word */
.k { color: #40F1E2; } /* Keyword */
.kc { color: #40F1E2; } /* Keyword.Constant */
.kd { color: #40F1E2; } /* Keyword.Declaration */
.kn { color: #40F1E2; } /* Keyword.Namespace */
.kp { color: #40F1E2; } /* Keyword.Pseudo */
.kr { color: #40F1E2; } /* Keyword.Reserved */
.kt { color: #40F1E2; } /* Keyword.Type */
.m { color: #F8FBB1; } /* Literal.Number */
.mb { color: #F8FBB1; } /* Literal.Number.Bin */
.mf { color: #F8FBB1; } /* Literal.Number.Float */
.mh { color: #F8FBB1; } /* Literal.Number.Hex */
.mi { color: #F8FBB1; } /* Literal.Number.Integer */
.il { color: #F8FBB1; } /* Literal.Number.Integer.Long */
.mo { color: #F8FBB1; } /* Literal.Number.Oct */
.s { color: #A0FAA2; } /* Literal.String */
.sa { color: #A0FAA2; } /* Literal.String.Affix */
.sb { color: #A0FAA2; } /* Literal.String.Backtick */
.sc { color: #A0FAA2; } /* Literal.String.Char */
.dl { color: #A0FAA2; } /* Literal.String.Delimiter */
.s2 { color: #A0FAA2; } /* Literal.String.Double */
.se { color: #A0FAA2; } /* Literal.String.Escape */
.sh { color: #A0FAA2; } /* Literal.String.Heredoc */
.si { color: #A0FAA2; } /* Literal.String.Interpol */
.sr { color: #A0FAA2; } /* Literal.String.Regex */
.s1 { color: #A0FAA2; } /* Literal.String.Single */
.ss { color: #A0FAA2; } /* Literal.String.Symbol */
.sx { color: #A0FAA2; } /* Literal.String.Other */
.sd { color: #A0FAA2; font-style: italic; } /* Literal.String.Doc */
.nv { color: #E0E0E0; } /* Name.Variable */
.vc { color: #6DB6F2; } /* Name.Variable.Class */
.vg { color: #E0E0E0; } /* Name.Variable.Global */
.vi { color: #E0E0E0; } /* Name.Variable.Instance */
.vm { color: #E0E0E0; } /* Name.Variable.Magic */
.na { color: #E0E0E0; } /* Name.Attribute */
.nb { color: #40F1E2; } /* Name.Builtin */
.bp { color: #40F1E2; } /* Name.Builtin.Pseudo */
.nf { color: #6DB6F2; } /* Name.Function */
.fm { color: #6DB6F2; } /* Name.Function.Magic */
.nc { color: #6DB6F2; } /* Name.Class */
.nn { color: #C29AD3; } /* Name.Namespace */
.no { color: #E0E0E0; } /* Name.Constant */
.nd { color: #E3C5DF; } /* Name.Decorator */
.ni { color: #E0E0E0; font-weight: bold; } /* Name.Entity */
.ne { color: #E5ABB3; } /* Name.Exception */
.nl { color: #E0E0E0; } /* Name.Label */
.nt { color: #40F1E2; } /* Name.Tag */
.c { color: #AEAEAE; font-style: italic; } /* Comment */
.ch { color: #AEAEAE; font-style: italic; } /* Comment.Hashbang */
.cm { color: #AEAEAE; font-style: italic; } /* Comment.Multiline */
.c1 { color: #AEAEAE; font-style: italic; } /* Comment.Single */
.cs { color: #AEAEAE; font-style: italic; } /* Comment.Special */
.cp { color: #AEAEAE; font-style: italic; } /* Comment.Preproc */
.cpf { color: #AEAEAE; font-style: italic; } /* Comment.PreprocFile */
.err { border: 1px solid #E5ABB3; } /* Error */
.w { color: #bbbbbb; } /* Text.Whitespace */
.p { color: #E0E0E0; } /* Punctuation */
「共有シートからmd2pdf」の節で紹介したコード、ショートカットです:
import sys, time
import urllib.parse
import clipboard
from main import *
if __name__ == '__main__':
try:
md = sys.argv[1]
except IndexError:
with open('sample.md', 'r', encoding='utf-8') as f:
md = f.read()
html = md2html_main(md)
print(html)
clipboard.set(html)
shortcuts.open_shortcuts_app()
「Pythonistaで編集中のマークダウンをクイックルック」節で紹介したコードです:
import editor, console, ui
from main import md2html_main
def _quicklook():
size = ui.get_window_size()
frame = (0, 0, *size)
v = ui.WebView(frame=frame)
def main(html):
v.load_html(html)
v.present('fullscreen')
return main
quicklook = _quicklook()
del _quicklook
if __name__ == '__main__':
console.clear()
md_source = editor.get_text()
html = md2html_main(md_source)
print(html)
quicklook(html)
参考
- ショートカット ユーザガイド - Apple
https://support.apple.com/ja-jp/guide/shortcuts/welcome/ios - Pythonista Modules — Python 3.6.1 documentation
http://omz-software.com/pythonista/docs/ios/index.html - Pythonista 3 で双方向のファイル転送 - Qiita
https://qiita.com/kido-akira/items/8fbe7783245e3b7d3bac - Mistune — mistune 0.8.4 documentation
https://mistune.readthedocs.io/en/v0.8.4/ - shortcuts a Pythonista URLs and Utilities — Python 3.6.1 documentation
http://omz-software.com/pythonista/docs/ios/shortcuts.html - The Pythonista URL Scheme — Python 3.6.1 documentation
http://omz-software.com/pythonista/docs/ios/urlscheme.html - App Extensions and Shortcuts — Python 3.6.1 documentation
http://omz-software.com/pythonista/docs/ios/pythonista_shortcuts.html#editor-action - Markdownから楽してフォーマット変換する方法 - Qiita
https://qiita.com/vh5150/items/26eeb0f3aaabfdb237f6 - [Qiita Markdown記法]ページ内リンク・注釈・折りたたみ等 - Qiita
https://qiita.com/aymikmts/items/71e550bf2c10f36883e9