text-compositor 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- text_compositor/__init__.py +0 -0
- text_compositor/build.py +2680 -0
- text_compositor/templates/slide.typ +173 -0
- text_compositor/templates/template.typ +187 -0
- text_compositor-0.2.0.dist-info/METADATA +161 -0
- text_compositor-0.2.0.dist-info/RECORD +10 -0
- text_compositor-0.2.0.dist-info/WHEEL +5 -0
- text_compositor-0.2.0.dist-info/entry_points.txt +2 -0
- text_compositor-0.2.0.dist-info/licenses/LICENSE +21 -0
- text_compositor-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// 幅・高さいずれかが利用可能領域をはみ出す場合だけ、縦横比を保って自動縮小する
|
|
2
|
+
// (mermaidなど事前レンダリング済み画像用。正方形に近い図は幅基準だけだと高さが溢れるため、
|
|
3
|
+
// 幅・高さ両方の縮小率を計算し、小さい方(より厳しい制約)を採用する)。
|
|
4
|
+
// 【注意】layout()が返すsize.heightは「ページの残りスペース」ではなく「コンテナ全体の高さ」を
|
|
5
|
+
// 返すため、見出しや説明文がすでに使った分は考慮されない。動的計算は信頼できないため、
|
|
6
|
+
// タイトル・本文の余地を見込んだ固定の高さ上限(MAX_IMG_HEIGHT)を安全側に設定する。
|
|
7
|
+
#let MAX_IMG_HEIGHT = 12cm
|
|
8
|
+
#let fit-image(path) = layout(size => context {
|
|
9
|
+
let img = image(path)
|
|
10
|
+
let i-size = measure(img)
|
|
11
|
+
let w-scale = size.width / i-size.width
|
|
12
|
+
let h-scale = MAX_IMG_HEIGHT / i-size.height
|
|
13
|
+
let scale = calc.min(w-scale, h-scale, 1.0)
|
|
14
|
+
if scale < 1.0 {
|
|
15
|
+
image(path, width: i-size.width * scale)
|
|
16
|
+
} else {
|
|
17
|
+
img
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
// Graphviz(diagraph、#82でwidth/height明示指定に対応するためconf()の外へ出し、
|
|
22
|
+
// build.py生成コードから直接呼べるようエクスポートした。conf()内のshow raw.where(lang: "dot")
|
|
23
|
+
// ルールは、width/height未指定の呼び出し(自動縮小のみ)としてこの関数をそのまま使う)。
|
|
24
|
+
#import "@preview/diagraph:0.3.7": render
|
|
25
|
+
#let render-graph(code, width: none, height: none) = if width != none or height != none {
|
|
26
|
+
render(code,
|
|
27
|
+
width: if width != none { width } else { auto },
|
|
28
|
+
height: if height != none { height } else { auto })
|
|
29
|
+
} else {
|
|
30
|
+
layout(size => context {
|
|
31
|
+
let graph = render(code)
|
|
32
|
+
let g-size = measure(graph)
|
|
33
|
+
if g-size.width > size.width {
|
|
34
|
+
render(code, width: 100%)
|
|
35
|
+
} else {
|
|
36
|
+
graph
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// GitHub形式のalert記法(#61)。template.typと同じ実装(note-me、MIT、@preview/note-me:0.6.0。
|
|
42
|
+
// #63でライセンス確認済み)。全テンプレートが同じ関数名を持つ必要があるため(#61参照)。
|
|
43
|
+
#import "@preview/note-me:0.6.0": note, tip, important, warning, caution
|
|
44
|
+
#let callout-fns = (note: note, tip: tip, important: important, warning: warning, caution: caution)
|
|
45
|
+
#let callout(kind: "note", body) = (callout-fns.at(kind, default: note))(body)
|
|
46
|
+
|
|
47
|
+
// 本文ページのヘッダー・フッター(#42)。template.typと同じ関数名でエクスポートし、build.py側が
|
|
48
|
+
// テンプレート種別を意識せず同じ呼び出し方でチャプター単位の上書きを再発行できるようにする。
|
|
49
|
+
// logo(#54): template.typと同じ実装。
|
|
50
|
+
#let render-header(header_text, logo) = if logo != none {
|
|
51
|
+
grid(
|
|
52
|
+
columns: (auto, auto),
|
|
53
|
+
column-gutter: 0.5em,
|
|
54
|
+
align: (left + horizon, left + horizon),
|
|
55
|
+
image(logo, height: 1.5em),
|
|
56
|
+
text(16pt, fill: luma(100))[#header_text],
|
|
57
|
+
)
|
|
58
|
+
} else {
|
|
59
|
+
align(left)[#text(16pt, fill: luma(100))[#header_text]]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
#let render-footer(footer_text, paginate) = {
|
|
63
|
+
let page-num = align(right)[#text(16pt, fill: luma(100))[#context counter(page).display("1")]]
|
|
64
|
+
if footer_text != none and paginate {
|
|
65
|
+
grid(
|
|
66
|
+
columns: (1fr, 1fr),
|
|
67
|
+
align(left)[#text(16pt, fill: luma(100))[#footer_text]],
|
|
68
|
+
page-num,
|
|
69
|
+
)
|
|
70
|
+
} else if footer_text != none {
|
|
71
|
+
align(left)[#text(16pt, fill: luma(100))[#footer_text]]
|
|
72
|
+
} else if paginate {
|
|
73
|
+
page-num
|
|
74
|
+
} else {
|
|
75
|
+
none
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 本文ページの背景画像(#55)。template.typと同じ実装。
|
|
80
|
+
#let render-background(path) = if path != none {
|
|
81
|
+
place(top + left, image(path, width: 100%, height: 100%))
|
|
82
|
+
} else {
|
|
83
|
+
none
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
#let conf(
|
|
87
|
+
title: none,
|
|
88
|
+
subtitle: none,
|
|
89
|
+
author: none,
|
|
90
|
+
date: none,
|
|
91
|
+
paper_size: "presentation-16-9",
|
|
92
|
+
landscape: true,
|
|
93
|
+
cover: true,
|
|
94
|
+
cover_page_number: false,
|
|
95
|
+
toc: false,
|
|
96
|
+
graphviz: true,
|
|
97
|
+
header: none,
|
|
98
|
+
footer: none,
|
|
99
|
+
paginate: true,
|
|
100
|
+
background: none,
|
|
101
|
+
logo: none,
|
|
102
|
+
doc,
|
|
103
|
+
) = {
|
|
104
|
+
// フォント設定(CJKフォントは build.py が取得・キャッシュした Noto Sans JP を --font-path 経由で渡す。
|
|
105
|
+
// OSフォントは直接指定しない。Noto Sans JP に無いグリフはTypstが自動でシステムフォントにフォールバックする)
|
|
106
|
+
set text(font: "Noto Sans JP", size: 18pt)
|
|
107
|
+
|
|
108
|
+
// Graphviz。graphviz: false のときは既定のraw表示(素のコード表示)にフォールバックする。
|
|
109
|
+
// showルール自体は常時登録する(ブロックスコープで閉じるため、ifの中で宣言すると効かなくなる)。
|
|
110
|
+
// render-graph()自体はモジュールのトップレベルで定義済み(#82でwidth/height対応のため移動)。
|
|
111
|
+
show raw.where(lang: "dot"): it => if graphviz { align(center)[#render-graph(it.text)] } else { it }
|
|
112
|
+
show raw.where(lang: "graphviz"): it => if graphviz { align(center)[#render-graph(it.text)] } else { it }
|
|
113
|
+
|
|
114
|
+
let page-number-footer = align(right)[#text(16pt, fill: luma(100))[#context counter(page).display("1")]]
|
|
115
|
+
|
|
116
|
+
// 表紙(cover: false のときは出さず、Markdown側のタイトルスライドに任せる)
|
|
117
|
+
// 表紙だけページ番号表示を切り替えたいので、本文とは別に一時的な set page で囲む
|
|
118
|
+
// (Typstの set は現在のブロックを抜けると元に戻るため、表紙用ページ設定を本文へ漏らさず適用できる)
|
|
119
|
+
if cover and title != none {
|
|
120
|
+
set page(
|
|
121
|
+
paper: paper_size,
|
|
122
|
+
flipped: landscape,
|
|
123
|
+
margin: (x: 2cm, y: 1.5cm),
|
|
124
|
+
header: none,
|
|
125
|
+
footer: if cover_page_number { page-number-footer } else { none }
|
|
126
|
+
)
|
|
127
|
+
align(center + horizon)[
|
|
128
|
+
#text(44pt, weight: "bold", fill: rgb("#003366"))[#title]
|
|
129
|
+
#v(2em)
|
|
130
|
+
#text(28pt)[#subtitle]
|
|
131
|
+
#v(3em)
|
|
132
|
+
#text(20pt)[#author]
|
|
133
|
+
]
|
|
134
|
+
pagebreak()
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 本文のページ設定
|
|
138
|
+
set page(
|
|
139
|
+
paper: paper_size,
|
|
140
|
+
flipped: landscape,
|
|
141
|
+
margin: (x: 2cm, y: 1.5cm),
|
|
142
|
+
header: if header != none { render-header(header, logo) } else { none },
|
|
143
|
+
footer: render-footer(footer, paginate),
|
|
144
|
+
background: render-background(background),
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
// 本文の設定
|
|
148
|
+
set par(justify: true, leading: 1.2em)
|
|
149
|
+
|
|
150
|
+
// H1 (実は使用しない想定だが念のため)
|
|
151
|
+
show heading.where(level: 1): it => {
|
|
152
|
+
align(center)[#text(36pt, weight: "bold")[#it.body]]
|
|
153
|
+
v(1.5em)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// H2をスライドタイトルとして扱う
|
|
157
|
+
show heading.where(level: 2): it => {
|
|
158
|
+
text(24pt, weight: "bold", fill: rgb("#003366"))[#it.body]
|
|
159
|
+
v(0.5em)
|
|
160
|
+
line(length: 100%, stroke: 2pt + rgb("#003366"))
|
|
161
|
+
v(1em)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ブロッククオートのスタイル
|
|
165
|
+
show quote.where(block: true): it => rect(
|
|
166
|
+
fill: luma(245),
|
|
167
|
+
stroke: (left: 4pt + rgb("#003366")),
|
|
168
|
+
inset: 1em,
|
|
169
|
+
width: 100%
|
|
170
|
+
)[#it.body]
|
|
171
|
+
|
|
172
|
+
doc
|
|
173
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// 幅・高さいずれかが利用可能領域をはみ出す場合だけ、縦横比を保って自動縮小する(mermaidなど事前レンダリング済み画像用)
|
|
2
|
+
#let MAX_IMG_HEIGHT = 12cm
|
|
3
|
+
#let fit-image(path) = layout(size => context {
|
|
4
|
+
let img = image(path)
|
|
5
|
+
let i-size = measure(img)
|
|
6
|
+
let w-scale = size.width / i-size.width
|
|
7
|
+
let h-scale = MAX_IMG_HEIGHT / i-size.height
|
|
8
|
+
let scale = calc.min(w-scale, h-scale, 1.0)
|
|
9
|
+
if scale < 1.0 {
|
|
10
|
+
image(path, width: i-size.width * scale)
|
|
11
|
+
} else {
|
|
12
|
+
img
|
|
13
|
+
}
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
// Graphviz(diagraph、#82でwidth/height明示指定に対応するためconf()の外へ出し、
|
|
17
|
+
// build.py生成コードから直接呼べるようエクスポートした。conf()内のshow raw.where(lang: "dot")
|
|
18
|
+
// ルールは、width/height未指定の呼び出し(自動縮小のみ)としてこの関数をそのまま使う)。
|
|
19
|
+
#import "@preview/diagraph:0.3.7": render
|
|
20
|
+
#let render-graph(code, width: none, height: none) = if width != none or height != none {
|
|
21
|
+
render(code,
|
|
22
|
+
width: if width != none { width } else { auto },
|
|
23
|
+
height: if height != none { height } else { auto })
|
|
24
|
+
} else {
|
|
25
|
+
layout(size => context {
|
|
26
|
+
let graph = render(code)
|
|
27
|
+
let g-size = measure(graph)
|
|
28
|
+
if g-size.width > size.width {
|
|
29
|
+
render(code, width: 100%)
|
|
30
|
+
} else {
|
|
31
|
+
graph
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// GitHub形式のalert記法(`> [!NOTE]`等、#61)。build.py側がblockquoteの先頭行から種別を検出し、
|
|
37
|
+
// callout(kind: "note")[...]のようなコードを生成する。実体はnote-me(MIT、@preview/note-me:0.6.0。
|
|
38
|
+
// #63でライセンス確認済み)にそのまま委譲する。全テンプレートが同じ関数名を持つ必要があるため
|
|
39
|
+
// (原稿を書く人はテンプレートの種類を意識しない。doc/usage/10_custom_template.md参照)、
|
|
40
|
+
// slide.typにも同じ実装がある。
|
|
41
|
+
#import "@preview/note-me:0.6.0": note, tip, important, warning, caution
|
|
42
|
+
#let callout-fns = (note: note, tip: tip, important: important, warning: warning, caution: caution)
|
|
43
|
+
#let callout(kind: "note", body) = (callout-fns.at(kind, default: note))(body)
|
|
44
|
+
|
|
45
|
+
// 本文ページのヘッダー・フッター(#42)。chapters[]/front-matterによるチャプター単位の上書きは
|
|
46
|
+
// build.py側が章ごとに#set page(header: render-header(...), footer: render-footer(...))を
|
|
47
|
+
// 再発行する形で実現する(state()は使わない。#16の反省点。landscape/paper_sizeと同じ
|
|
48
|
+
// 「明示指定を都度出し直す」パターンで、チャプターの並べ替えに対して安全)。
|
|
49
|
+
// logo(#54): 左にロゴ画像、右にheader_text。logoがnoneのときは従来どおり右寄せのみ。
|
|
50
|
+
#let render-header(header_text, logo) = {
|
|
51
|
+
if logo != none {
|
|
52
|
+
grid(
|
|
53
|
+
columns: (auto, 1fr),
|
|
54
|
+
align: (left + horizon, right + horizon),
|
|
55
|
+
image(logo, height: 1.2em),
|
|
56
|
+
text(8pt, fill: luma(100))[#header_text],
|
|
57
|
+
)
|
|
58
|
+
} else {
|
|
59
|
+
align(right)[#text(8pt, fill: luma(100))[#header_text]]
|
|
60
|
+
}
|
|
61
|
+
v(0.5em)
|
|
62
|
+
line(length: 100%, stroke: 0.5pt + luma(200))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
#let render-footer(footer_text, paginate) = {
|
|
66
|
+
let page-num = context counter(page).display("1")
|
|
67
|
+
if footer_text != none and paginate {
|
|
68
|
+
grid(
|
|
69
|
+
columns: (1fr, 1fr),
|
|
70
|
+
align(left)[#text(9pt)[#footer_text]],
|
|
71
|
+
align(right)[#text(9pt)[#page-num]],
|
|
72
|
+
)
|
|
73
|
+
} else if footer_text != none {
|
|
74
|
+
align(left)[#text(9pt)[#footer_text]]
|
|
75
|
+
} else if paginate {
|
|
76
|
+
align(center)[#text(9pt)[#page-num]]
|
|
77
|
+
} else {
|
|
78
|
+
none
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 本文ページの背景画像(#55)。header/footerと同じ「章単位で明示指定を出し直す」パターンで、
|
|
83
|
+
// build.py側が章ごとに#set page(background: render-background(...))を再発行する。
|
|
84
|
+
// 画像はページ全面に敷き、本文はその上に通常どおりレイアウトされる(重なりの調整はしない。
|
|
85
|
+
// 濃い背景を使うと本文が読みにくくなるため、薄い画像を使うことを利用者側で判断する)。
|
|
86
|
+
#let render-background(path) = if path != none {
|
|
87
|
+
place(top + left, image(path, width: 100%, height: 100%))
|
|
88
|
+
} else {
|
|
89
|
+
none
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
#let conf(
|
|
93
|
+
title: none,
|
|
94
|
+
subtitle: none,
|
|
95
|
+
author: none,
|
|
96
|
+
date: none,
|
|
97
|
+
paper_size: "a4",
|
|
98
|
+
landscape: false,
|
|
99
|
+
cover: true,
|
|
100
|
+
cover_page_number: false,
|
|
101
|
+
toc: false,
|
|
102
|
+
graphviz: true,
|
|
103
|
+
header: none,
|
|
104
|
+
footer: none,
|
|
105
|
+
paginate: true,
|
|
106
|
+
background: none,
|
|
107
|
+
logo: none,
|
|
108
|
+
doc,
|
|
109
|
+
) = {
|
|
110
|
+
// フォント設定(CJKフォントは build.py が取得・キャッシュした Noto Sans JP を --font-path 経由で渡す。
|
|
111
|
+
// OSフォントは直接指定しない。Noto Sans JP に無いグリフはTypstが自動でシステムフォントにフォールバックする)
|
|
112
|
+
set text(font: "Noto Sans JP", size: 10.5pt)
|
|
113
|
+
|
|
114
|
+
// プラグイン: Graphviz (dot) 自動レンダリングと、はみ出し防止の自動縮小
|
|
115
|
+
// graphviz: false のときは既定のraw表示(素のコード表示)にフォールバックする。
|
|
116
|
+
// showルール自体は常時登録する(ifブロックの中で宣言すると、ブロックを抜けた
|
|
117
|
+
// doc側には効かなくなる。show/setはブロックスコープで閉じるため)。
|
|
118
|
+
// render-graph()自体はモジュールのトップレベルで定義済み(#82でwidth/height対応のため移動)。
|
|
119
|
+
show raw.where(lang: "dot"): it => if graphviz { align(center)[#render-graph(it.text)] } else { it }
|
|
120
|
+
show raw.where(lang: "graphviz"): it => if graphviz { align(center)[#render-graph(it.text)] } else { it }
|
|
121
|
+
|
|
122
|
+
// -------------------------
|
|
123
|
+
// 1. 表紙 (Cover Page) : cover: false のときは省略する
|
|
124
|
+
// -------------------------
|
|
125
|
+
if cover {
|
|
126
|
+
set page(
|
|
127
|
+
paper: "a4", flipped: false, margin: 2.5cm, header: none,
|
|
128
|
+
footer: if cover_page_number { align(center)[#text(10pt)[#context counter(page).display("1")]] } else { none }
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
align(center + horizon)[
|
|
132
|
+
#text(24pt, weight: "bold")[#title]
|
|
133
|
+
#v(3em)
|
|
134
|
+
#text(14pt)[#subtitle]
|
|
135
|
+
#v(4em)
|
|
136
|
+
#text(12pt)[#author]
|
|
137
|
+
#v(1em)
|
|
138
|
+
#text(12pt)[#date]
|
|
139
|
+
]
|
|
140
|
+
pagebreak()
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// -------------------------
|
|
144
|
+
// 2. 目次 (Table of Contents) : toc: false のときは省略する
|
|
145
|
+
// -------------------------
|
|
146
|
+
if toc {
|
|
147
|
+
set page(
|
|
148
|
+
paper: paper_size,
|
|
149
|
+
flipped: landscape,
|
|
150
|
+
margin: (x: 2cm, y: 2.5cm),
|
|
151
|
+
header: none,
|
|
152
|
+
footer: align(center)[#text(10pt)[- #context counter(page).display("i") -]]
|
|
153
|
+
)
|
|
154
|
+
counter(page).update(1) // 目次のページ番号を i から開始
|
|
155
|
+
|
|
156
|
+
align(center)[
|
|
157
|
+
#text(18pt, weight: "bold")[目次]
|
|
158
|
+
]
|
|
159
|
+
v(1.5em)
|
|
160
|
+
outline(title: none, indent: auto)
|
|
161
|
+
pagebreak()
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// -------------------------
|
|
165
|
+
// 3. 本文 (Body)
|
|
166
|
+
// -------------------------
|
|
167
|
+
set page(
|
|
168
|
+
paper: paper_size,
|
|
169
|
+
flipped: landscape,
|
|
170
|
+
header: render-header(if header != none { header } else { title }, logo),
|
|
171
|
+
footer: render-footer(footer, paginate),
|
|
172
|
+
background: render-background(background),
|
|
173
|
+
)
|
|
174
|
+
counter(page).update(1) // 本文のページ番号を 1 からリセット
|
|
175
|
+
|
|
176
|
+
set par(justify: true, leading: 0.8em)
|
|
177
|
+
|
|
178
|
+
// 見出し1 (大見出し/章) の直前で必ず改ページする(すでにページ先頭の場合はスキップ)
|
|
179
|
+
show heading.where(level: 1): it => {
|
|
180
|
+
pagebreak(weak: true)
|
|
181
|
+
v(1em)
|
|
182
|
+
it
|
|
183
|
+
v(0.5em)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
doc
|
|
187
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: text-compositor
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: A tool for composing multiple text files into a PDF document.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Repository, https://github.com/tokudiro/text-compositor
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: typst==0.15.0
|
|
11
|
+
Requires-Dist: markdown-it-py==3.0.0
|
|
12
|
+
Requires-Dist: mdit-py-plugins==0.6.1
|
|
13
|
+
Requires-Dist: PyYAML==6.0.2
|
|
14
|
+
Requires-Dist: platformdirs==4.3.6
|
|
15
|
+
Provides-Extra: mermaid
|
|
16
|
+
Requires-Dist: playwright==1.62.0; extra == "mermaid"
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# text-compositor
|
|
20
|
+
|
|
21
|
+
*[日本語版 (Japanese version)](README-ja.md)*
|
|
22
|
+
|
|
23
|
+
A tool that treats multiple text files as independent fragments and deterministically composes them into a single human-readable PDF document. Works equally well for AI-generated drafts refined by humans and for existing, hand-authored documents you want to organize and consolidate.
|
|
24
|
+
|
|
25
|
+
This README covers only the essentials needed to get started quickly. For a full walkthrough of `config.yaml` and how to write your source documents, see the [usage guide](doc/usage/) (which can also be built into a PDF with `cd doc/usage && python ../../build.py`). For detailed design rationale and implementation status, see [doc/spec.md](doc/spec.md). Differences from general-purpose tools like Quarto are summarized in [doc/diff.md](doc/diff.md).
|
|
26
|
+
|
|
27
|
+
## Features
|
|
28
|
+
|
|
29
|
+
- Combines multiple text files (chapters) into a single PDF
|
|
30
|
+
- Markdown → AST via [markdown-it-py](https://github.com/executablebooks/markdown-it-py) → deterministic conversion to [Typst](https://typst.app/) syntax → PDF output
|
|
31
|
+
- Outputs other text files to the PDF unchanged
|
|
32
|
+
- Separates the tool itself from the documentation (source files), which can live anywhere outside the repository
|
|
33
|
+
- Python-centric with minimal downloads, no dependency on external servers or SaaS (runs the same way on GitHub Actions and locally on Windows/Linux/macOS)
|
|
34
|
+
|
|
35
|
+
Files listed in `chapters` are handled differently depending on their extension:
|
|
36
|
+
|
|
37
|
+
- `.md`/`.markdown`: converted as Markdown
|
|
38
|
+
- `.yaml`/`.yml`/`.json`: rendered as monospaced text with syntax highlighting
|
|
39
|
+
- `.dot`/`.gv`, `.mmd`, `.puml`/`.plantuml`/`.pu`, `.d2`: each rendered as a one-chapter diagram (Graphviz, Mermaid, PlantUML, D2 respectively)
|
|
40
|
+
- `.csv`: rendered as a structured Typst table
|
|
41
|
+
- everything else (plain text, code files, etc.): rendered as plain monospaced text
|
|
42
|
+
|
|
43
|
+
See the [usage guide](doc/usage/) for details.
|
|
44
|
+
|
|
45
|
+
## Requirements
|
|
46
|
+
|
|
47
|
+
- Python 3.10+
|
|
48
|
+
|
|
49
|
+
> **Windows note — Microsoft Store Python is not supported, even inside `pipx`/`venv`:** If Python was installed from the Microsoft Store, this tool cannot be used with it, full stop — `pipx` or a `venv` does **not** work around this.
|
|
50
|
+
>
|
|
51
|
+
> Windows redirects that Python's writes under `%LOCALAPPDATA%` into a package-private folder, and this redirection follows the Store installation into any `venv`/`pipx` environment created from it (verified by testing), breaking every subprocess-based feature (PlantUML, Mermaid, D2) even though the files appear to exist to Python itself.
|
|
52
|
+
>
|
|
53
|
+
> Before installing this tool, install Python from [python.org](https://www.python.org/downloads/) (or another non-Store distribution such as `winget install Python.Python.3.12`) and use that Python for the steps below. Run `text-compositor --check-env` afterward to confirm the environment is set up correctly.
|
|
54
|
+
|
|
55
|
+
There are two ways to install and run this tool.
|
|
56
|
+
|
|
57
|
+
### Option A: `pip install` (recommended)
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pipx install text-compositor
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`pipx` installs the tool into an isolated environment and exposes the `text-compositor` command — recommended for most users. If you're developing this tool itself, use a `venv` instead so an editable install (`pip install -e .`) picks up code changes immediately:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
python -m venv .venv
|
|
67
|
+
.venv/bin/pip install -e . # Windows: .venv\Scripts\pip install -e .
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Option B: Clone and run directly (no installation)
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
pip install -r requirements.txt
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
No Typst compiler binary is bundled; it's obtained from the `typst` package (PyPI wheel) installed via `pip install` above. No additional downloads or installation steps are needed.
|
|
77
|
+
|
|
78
|
+
### Using Mermaid diagrams (optional)
|
|
79
|
+
|
|
80
|
+
This is only needed if your source documents use ` ```mermaid ` fences (or specify an `.mmd` file directly in `chapters`). With Option A, install the `mermaid` extra:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
pipx install "text-compositor[mermaid]"
|
|
84
|
+
# or, if already installed: pipx inject text-compositor playwright==1.62.0
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
With Option B (clone and run directly):
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
pip install playwright==1.62.0
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
- **A Google Chrome or Microsoft Edge installation already present on your system** (no new download by default; it's auto-detected and reused at build time)
|
|
94
|
+
- The `playwright` package above (used only to connect to the existing browser via CDP; Playwright's own browser-download feature is not used by default)
|
|
95
|
+
|
|
96
|
+
Node.js/npm is not required. At build time, the official single-file Mermaid bundle (`mermaid.min.js`, ~3.4MB) is fetched and loaded into a headless browser to convert diagrams to SVG (the bundle JS itself is cached under an OS-standard user cache directory — e.g. `%LOCALAPPDATA%\text-compositor\Cache` on Windows, `~/.cache/text-compositor` on Linux — and conversion results are cached under `.text-compositor/cache/`; neither is re-fetched afterward). None of this is needed for documents that don't use Mermaid.
|
|
97
|
+
|
|
98
|
+
If no Chrome/Edge is found on the system, the build fails by default. Setting `plugins: { mermaid_auto_download: true }` instead auto-fetches Playwright's own Chromium, but **this download is about 700MB** (this setting exists as a last resort for when no pre-installed browser is available; downloading that much by default is intentionally avoided).
|
|
99
|
+
|
|
100
|
+
### Using PlantUML diagrams (optional)
|
|
101
|
+
|
|
102
|
+
No extra `config.yaml` settings are needed to use ` ```plantuml ` fences in your source documents (or to specify a `.puml` file directly in `chapters`) — `plugins.plantuml` defaults to `true` and no additional `pip install` is required.
|
|
103
|
+
|
|
104
|
+
- If Java (11+) is available locally, it's reused as-is
|
|
105
|
+
- Otherwise, Eclipse Temurin JRE (Adoptium distribution, ~49.7MB) is auto-fetched and cached by default under the same user cache directory as above. Setting `plugins: { plantuml_auto_download: false }` disables the auto-fetch and fails the build instead
|
|
106
|
+
- GitHub Actions' `ubuntu-latest` ships with Java by default, so no extra download happens on CI
|
|
107
|
+
|
|
108
|
+
The layout engine is the pure-Java Smetana implementation, so no external binary like Graphviz (`dot`) is required. PlantUML itself (MIT edition, ~17.6MB) is cached under the same user cache directory, and conversion results are cached under `.text-compositor/cache/`, same as Mermaid.
|
|
109
|
+
|
|
110
|
+
### Using D2 diagrams (optional)
|
|
111
|
+
|
|
112
|
+
No extra `config.yaml` settings are needed to use ` ```d2 ` fences in your source documents (or to specify a `.d2` file directly in `chapters`) — `plugins.d2` defaults to `true` and no additional `pip install` is required.
|
|
113
|
+
|
|
114
|
+
- If the `d2` command is available locally, it's reused as-is
|
|
115
|
+
- Otherwise, the official D2 CLI binary (a single Go executable, ~13MB) is auto-fetched and cached by default under the same user cache directory as above. Setting `plugins: { d2_auto_download: false }` disables the auto-fetch and fails the build instead
|
|
116
|
+
|
|
117
|
+
Unlike Java (used by PlantUML) or a browser (used by Mermaid), GitHub Actions' `ubuntu-latest` does not ship with D2 preinstalled, so a `d2` diagram triggers this ~13MB download on every CI run (this project's workflows don't persist the cache directory across separate runs).
|
|
118
|
+
|
|
119
|
+
## Usage
|
|
120
|
+
|
|
121
|
+
If installed via `pip`/`pipx` (Option A):
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
text-compositor --config <path/to/text-compositor.config.yaml>
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
If cloned and run directly (Option B):
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
python build.py --config <path/to/text-compositor.config.yaml>
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
If `--config` is omitted, `text-compositor.config.yaml` (or `.json`) located directly in the current directory is auto-detected.
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
cd my-project/
|
|
137
|
+
python /path/to/text-compositor/build.py
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Run `text-compositor --check-env` (or `python build.py --check-env`) to check your environment (dependencies, Typst version, cached assets, Mermaid/PlantUML/D2 prerequisites) without running a build.
|
|
141
|
+
|
|
142
|
+
Other flags control runtime behavior only (not document content, which stays entirely in `config.yaml`): `-q`/`--quiet` suppresses `[Info]`-level logging, `-v`/`--verbose` adds extra detail (which chapter is being processed, cache reuse), and `--keep-temp` keeps the intermediate `temp_build.typ` around after a successful build instead of deleting it (useful for debugging; it's always kept after a failed build).
|
|
143
|
+
|
|
144
|
+
See [sample/text-compositor.config.yaml](sample/text-compositor.config.yaml) for how to write the config file, and the [usage guide](doc/usage/) for details on `document:`/`plugins:`, front matter, Marp directives, and more. The Markdown files listed in `chapters` are concatenated in order to produce the PDF.
|
|
145
|
+
|
|
146
|
+
## Trying the sample
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
cd sample/
|
|
150
|
+
python ../build.py
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
This generates `sample/System_Specification.pdf`.
|
|
154
|
+
|
|
155
|
+
## Implementation status
|
|
156
|
+
|
|
157
|
+
The core feature implemented so far is combining multiple files into a single PDF according to the config file specified via `--config` (or auto-detected). Expanding CLI options (e.g., overriding the output path) is still in the planning stage. See [GitHub Issues](https://github.com/tokudiro/text-compositor/issues) for known issues and upcoming plans.
|
|
158
|
+
|
|
159
|
+
## License
|
|
160
|
+
|
|
161
|
+
[MIT License](LICENSE)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
text_compositor/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
text_compositor/build.py,sha256=it-jK24k6VN05Ea7PcXE4rufAsQ7Bbyz8RPXnl00b5g,165721
|
|
3
|
+
text_compositor/templates/slide.typ,sha256=IRdWxGC2Ehcc7CDdYF-MP4l_z7nrSTIAHCuF8R-Be8k,6824
|
|
4
|
+
text_compositor/templates/template.typ,sha256=AUKvvdhnczCM70ai5X5aNjOChoBaqDrXc9LbYH7LZ00,7329
|
|
5
|
+
text_compositor-0.2.0.dist-info/licenses/LICENSE,sha256=xUIdpZav5CQwNXgUV8vEvjoYPbR7UeRxi-op2SavSnM,1065
|
|
6
|
+
text_compositor-0.2.0.dist-info/METADATA,sha256=rgCdQ2ADZKBUySGD50pBv8eBhiUmbrJ8-8eMSUwSRwY,9551
|
|
7
|
+
text_compositor-0.2.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
8
|
+
text_compositor-0.2.0.dist-info/entry_points.txt,sha256=dCT80LB4OjazbH_9-2xpRIyj4mNmiu1sSxMbrstyRjk,64
|
|
9
|
+
text_compositor-0.2.0.dist-info/top_level.txt,sha256=Y64ydH6Fo5HPyutCkQnZQrbb8IQUY-yFIXOvyEro-A8,16
|
|
10
|
+
text_compositor-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 tokudiro
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
text_compositor
|