├── example.pdf ├── example.png ├── README.md ├── LICENSE ├── example.typ └── book.typ /example.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shun-shobon/typst-oreilly-template/HEAD/example.pdf -------------------------------------------------------------------------------- /example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shun-shobon/typst-oreilly-template/HEAD/example.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # オライリー風 Typst テンプレート 2 | 3 | オライリー風の組版を実現するための Typst テンプレートです。 4 | 5 | ## 使い方 6 | 7 | `book.typ`を読み込んで、`layout`を使ってください。 8 | 9 | ```typ 10 | #import "book.typ": * 11 | 12 | #show: layout.with( 13 | title: [タイトル], 14 | subtitle: [サブタイトル], 15 | author: [著者名], 16 | publisher: [出版社], 17 | // フォントの設定 18 | fonts: ( 19 | sans-serif: ("Hiragino Kaku Gothic ProN"), 20 | serif: ("Hiragino Mincho ProN"), 21 | mono: ("UDEV Gothic 35NFLG"), 22 | ), 23 | ) 24 | 25 | #show: document 26 | 27 | ... 28 | ``` 29 | 30 | このパッケージは以下の関数を提供しています。 31 | 32 | - `layout`: 全体のレイアウトやスタイルの設定を行う関数です。引数としてタイトルなどの情報とフォントの設定を渡してください。 33 | - `document`: 本文を始める前に呼び出してください。これより前の文章はまえがきになり、ページ番号もローマ数字になります。また、目次もここで自動生成されます。 34 | - `appendix`: 付録を書く場合に呼び出してください。章番号がアルファベットになります。 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 NISHIZAWA Shuntaro 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. 22 | -------------------------------------------------------------------------------- /example.typ: -------------------------------------------------------------------------------- 1 | #import "@preview/roremu:0.1.0": roremu 2 | 3 | #import "book.typ": * 4 | 5 | #show: layout.with( 6 | title: [タイトル], 7 | subtitle: [サブタイトル], 8 | author: [著者名], 9 | publisher: [出版社], 10 | fonts: ( 11 | sans-serif: ("Hiragino Kaku Gothic ProN"), 12 | serif: ("Hiragino Mincho ProN"), 13 | mono: ("UDEV Gothic 35NFLG"), 14 | ), 15 | ) 16 | 17 | = まえがき 18 | 19 | #lorem(100) 20 | 21 | == まえがきの見出し 22 | 23 | #lorem(200) 24 | 25 | == まえがきの見出し2 26 | 27 | #lorem(300) 28 | 29 | #show: document 30 | 31 | = おっきい見出し 32 | 33 | *5000兆円欲しい!!!!* 34 | これは参照#footnote[脚注も書くことができるようです。]のテストです。 35 | 詳しくは、@img:example と @tab:example をご覧ください。また、@chp:1 や @eq:example にも参照#footnote[長い脚注の場合もありますね。長い脚注の場合もありますね。長い脚注の場合もありますね。長い脚注の場合もありますね。長い脚注の場合もありますね。]があります。 36 | 37 | #roremu(100) 38 | 39 | #figure( 40 | caption: "なんかいい感じの画像", 41 | rect(image("example.png")), 42 | ) 43 | 44 | #roremu(300) 45 | 46 | #figure( 47 | caption: [なんかいい感じの表], 48 | table( 49 | columns: (3fr, 5fr), 50 | align: left, 51 | table.header([メソッド], [説明]), 52 | [`__init__(self, input_size, hidden_size, output_size)`], 53 | [ 54 | 初期化を行う。\ 55 | 引数は頭から順に、入力層のニューロンの数、隠れ層のニューロンの数、出力層のニューロンの数。 56 | ], 57 | [`predict(self, x)`], 58 | [ 59 | 認識(推論)を行う。\ 60 | 引数を`x`の画像データ。 61 | ], 62 | [`loss(self, x, t)`], 63 | [ 64 | 損失関数の値を求める。\ 65 | 引数の`x`は画像データ、`t`は正解ラベル(以下の3つのメソッドの引数についても同様)。 66 | ], 67 | [`accuracy(self, x, t)`], 68 | [認識精度を求める。], 69 | [`numerical_gradient(self, x, t)`], 70 | [重みパラメータに対する勾配を求める。], 71 | [`gradient(self, x, t)`], 72 | [ 73 | 重みパラメータに対する勾配を求める。 74 | `numerical_gradient()`の高速版!\ 75 | 実装は次章で行う。 76 | ] 77 | ) 78 | ) 79 | 80 | #roremu(500) #footnote[こんなところにも脚注があります] 81 | 82 | $ 83 | x = (-b plus.minus sqrt(b^2 - 4 a c)) / (2a) 84 | $ 85 | 86 | #roremu(300) 87 | 88 | #roremu(200) 89 | 90 | #roremu(500) 91 | 92 | #roremu(500) 93 | 94 | = 次のおっきい見出し 95 | 96 | #roremu(500) #footnote[脚注の番号は章をまたぐとリセットされます。] 97 | 98 | #figure( 99 | caption: "長いキャプション長いキャプション長いキャプション長いキャプション長いキャプション長いキャプション長いキャプション", 100 | rect(image("example.png")), 101 | ) 102 | 103 | == 中くらいの見出し 104 | 105 | #roremu(200) 106 | 107 | #figure( 108 | caption: "なんかいい感じの画像(小さい版)", 109 | rect(image("example.png", width: 50%)), 110 | ) 111 | 112 | == すげええええええええええええ長い見出しいいいいいいいいいいいい 113 | 114 | === ちっちゃい見出し 115 | 116 | #roremu(500) 117 | 118 | #roremu(500) 119 | 120 | #roremu(500) 121 | 122 | === ちっちゃい見出し2 123 | 124 | #roremu(500) 125 | 126 | === ちっちゃい見出し3 127 | 128 | #roremu(500) 129 | 130 | ```ts 131 | function hello(name: string): string { 132 | return `Hello, ${name}!`; 133 | } 134 | ``` 135 | 136 | = 最後の見出し 137 | 138 | #roremu(400) 139 | 140 | == 見出し1 141 | 142 | #roremu(400) 143 | 144 | == 見出し2 145 | 146 | #roremu(400) 147 | 148 | == 見出し3 149 | 150 | #roremu(400) 151 | 152 | == 見出し4 153 | 154 | #roremu(400) 155 | 156 | == 見出し5 157 | 158 | #roremu(400) 159 | 160 | #show: appendix 161 | 162 | = 付録だよ 163 | 164 | #roremu(100) 165 | 166 | == 付録の見出し 167 | 168 | #roremu(200) 169 | 170 | == 付録の見出し2 171 | 172 | #roremu(300) 173 | 174 | === 付録のちっちゃい見出し 175 | 176 | #roremu(500) 177 | 178 | === 付録のちっちゃい見出し2 179 | 180 | #roremu(800) 181 | -------------------------------------------------------------------------------- /book.typ: -------------------------------------------------------------------------------- 1 | #let layout( 2 | body, 3 | title: [], 4 | subtitle: [], 5 | author: [], 6 | publisher: [], 7 | fonts: ( 8 | sans-serif: (), 9 | serif: (), 10 | mono: (), 11 | ), 12 | ) = { 13 | // 全体のページ設定 14 | set page( 15 | paper: "a5", 16 | binding: left, 17 | margin: ( 18 | top: 25mm, 19 | bottom: 15mm, 20 | inside: 15mm, 21 | outside: 15mm, 22 | ), 23 | numbering: "i", 24 | header: context { 25 | set text(font: fonts.sans-serif, size: 8pt) 26 | 27 | let current-chapter = query(heading.where(level: 1)).filter(it => it.location().page() <= here().page()).at(-1, default: none) 28 | let current-section = query(heading.where(level: 2)).filter(it => it.location().page() <= here().page()).at(-1, default: none) 29 | 30 | block( 31 | width: 100%, 32 | stroke: ( 33 | bottom: (thickness: 0.5pt, cap: "butt"), 34 | ), 35 | inset: ( 36 | bottom: 2pt, 37 | ) 38 | )[ 39 | #if calc.even(here().page()) { 40 | align(left)[ 41 | #box( 42 | width: 2.5em, 43 | stroke: (right: (thickness: 1.2pt, cap: "butt")), 44 | inset: (y: 2pt), 45 | text(weight: "bold", font: fonts.sans-serif, numbering(here().page-numbering(), here().page())), 46 | ) 47 | #box( 48 | inset: (y: 2pt, left: 0.5em), 49 | if current-chapter != none { 50 | if current-chapter.numbering != none { 51 | numbering(current-chapter.numbering, ..counter(heading).at(current-chapter.location())) 52 | h(0.5em) 53 | } 54 | current-chapter.body 55 | } 56 | ) 57 | ] 58 | } else { 59 | align(right)[ 60 | #box( 61 | inset: (y: 2pt, right: 0.5em), 62 | // 章があるページではなく、節が現在の章と同じで、かつ節がある場合 63 | if 64 | current-chapter.location().page() != here().page() and 65 | current-section != none and 66 | counter(heading).at(current-chapter.location()).at(0) == counter(heading).at(current-section.location()).at(0) 67 | { 68 | if current-section.numbering != none { 69 | numbering(current-section.numbering, ..counter(heading).at(current-section.location())) 70 | h(0.5em) 71 | } 72 | current-section.body 73 | } 74 | ) 75 | #box( 76 | width: 2.5em, 77 | stroke: (left: (thickness: 1.2pt, cap: "butt")), 78 | inset: (y: 2pt), 79 | text(weight: "bold", font: fonts.sans-serif, numbering(here().page-numbering(), here().page())), 80 | ) 81 | ] 82 | } 83 | ] 84 | }, 85 | footer: {}, 86 | ) 87 | 88 | // フォント 89 | set text( 90 | lang: "ja", 91 | font: fonts.serif, 92 | size: 9pt 93 | ) 94 | show raw: set text(font: fonts.mono) 95 | show strong: set text(font: fonts.sans-serif) 96 | 97 | // 囲みのデフォルトの太さを調整 98 | set rect(stroke: 0.3pt) 99 | 100 | // 段落の空白を調整 101 | set par( 102 | leading: 0.8em, 103 | justify: true, 104 | linebreaks: auto, 105 | first-line-indent: 1em, 106 | ) 107 | 108 | // 見出しのナンバリングとフォントを設定 109 | show heading: set text(font: fonts.sans-serif) 110 | 111 | // 章の見出し 112 | show heading.where(level: 1): it => { 113 | // 常に右ページから始まる 114 | { 115 | // 空白ページは何も表示しない 116 | set page(header: {}) 117 | pagebreak(to: "odd", weak: true) 118 | } 119 | 120 | // 図表番号をリセット 121 | counter(figure.where(kind: image)).update(0) 122 | counter(figure.where(kind: table)).update(0) 123 | // 脚注番号をリセット 124 | counter(footnote).update(0) 125 | 126 | // 右寄せにする 127 | set align(right) 128 | set text(size: 20pt) 129 | 130 | v(3%) 131 | // 章番号を表示 132 | if counter(heading).at(it.location()).at(0) != 0 { 133 | text( 134 | fill: luma(100), 135 | numbering(heading.numbering, ..counter(heading).at(it.location())).trim() 136 | ) 137 | } else { 138 | // ナンバリングがない場合はダミーを入れる 139 | text("") 140 | } 141 | v(-12pt) 142 | it.body 143 | v(15%) 144 | } 145 | 146 | // 節の見出し 147 | show heading.where(level: 2): it => { 148 | set text(size: 11pt) 149 | set par(leading: 0.4em) 150 | set block(above: 2.2em, below: 1em) 151 | 152 | it 153 | } 154 | 155 | // 項の見出し 156 | // 節とほぼ同じ 157 | show heading.where(level: 3): it => { 158 | set text(size: 10pt) 159 | set par(leading: 0.4em) 160 | set block(above: 1.8em, below: 0.8em) 161 | 162 | it 163 | } 164 | 165 | // 目次より前の節・項は目次に表示しない 166 | show heading.where(level: 2): set heading(outlined: false) 167 | show heading.where(level: 3): set heading(outlined: false) 168 | 169 | // 図表のスタイル設定 170 | set figure(numbering: (..num) => { 171 | let current-chapter-num = counter(heading).get().at(0) 172 | numbering("1-1", current-chapter-num, ..num) 173 | }) 174 | set figure.caption(separator: h(1em)) 175 | show figure.caption: it => { 176 | set text(font: fonts.sans-serif, size: 8pt) 177 | set align(left) 178 | it 179 | } 180 | 181 | // 表のスタイル設定 182 | // 表のキャプションは上 183 | show figure.where(kind: table): set figure.caption(position: top) 184 | show table: it => { 185 | // 表内の文字は少し小さくする 186 | set text(size: 8pt) 187 | // justifyだとあまりきれいじゃないので無効化 188 | set par(justify: false) 189 | it 190 | } 191 | set table( 192 | stroke: (x, y) => ( 193 | // こうすることで一番下のみ線が引かれる 194 | top: 0pt, 195 | bottom: 0.3pt, 196 | // 一番上は白の線、それ以外は普通の線 197 | left: if x > 0 { 198 | if y == 0 { 199 | (thickness: 0.3pt, paint: luma(255)) 200 | } else { 201 | 0.3pt 202 | } 203 | } 204 | ), 205 | fill: (x, y) => { 206 | if y == 0 { 207 | // 一番上のヘッダーは濃いグレー 208 | luma(120) 209 | } else if calc.even(y) { 210 | // 偶数行は薄いグレー 211 | luma(210) 212 | } 213 | }, 214 | inset: ( 215 | x: 5pt, 216 | y: 3pt, 217 | ) 218 | ) 219 | // 一番上のヘッダーは白文字 220 | show table.cell.where(y: 0): it => { 221 | set text(font: fonts.sans-serif, fill: luma(255)) 222 | it 223 | } 224 | 225 | set math.equation(numbering: (..num) => context { 226 | let current-chapter-num = counter(heading).get().at(0) 227 | numbering("(1.1)", current-chapter-num, ..num) 228 | }) 229 | 230 | // 参照のカスタマイズ 231 | show ref: it => { 232 | if it.element == none { 233 | it 234 | return 235 | } 236 | 237 | if it.element.func() == figure { 238 | // 図表は強調表示する 239 | set text(font: fonts.sans-serif, weight: "bold") 240 | it 241 | } else if it.element.func() == heading and it.element.level == 1 { 242 | // 見出しは見出しのナンバリングをそのまま使用 243 | link( 244 | it.element.location(), 245 | numbering(heading.numbering, ..counter(heading).at(it.element.location())).trim(), 246 | ) 247 | } else { 248 | it 249 | } 250 | } 251 | 252 | // 見出しや図表の後の段落が字下げされない問題を修正 253 | // 空の段落を入れる 254 | show selector(heading).or(figure): it => { 255 | it 256 | par(text(size: 0pt, "")) 257 | v(-1em) 258 | } 259 | 260 | // 目次のスタイル設定 261 | show outline.entry.where(level: 1): it => { 262 | v(2em, weak: true) 263 | 264 | // ナンバリングがない場合は節と同じスタイル 265 | if it.element.numbering == none { 266 | h(0.5em) 267 | it 268 | return 269 | } 270 | 271 | set text(font: fonts.sans-serif, weight: "bold", size: 10pt) 272 | it 273 | } 274 | show outline.entry.where(level: 2): it => { 275 | h(1em) 276 | it 277 | } 278 | show outline.entry.where(level: 3): it => { 279 | h(3.3em) 280 | it 281 | } 282 | set outline(depth: 3) 283 | 284 | // 脚注のスタイルを設定 285 | set footnote(numbering: sym.dagger + "1") 286 | set footnote.entry(separator: line(length: 100%, stroke: 0.5pt)) 287 | show footnote: it => { 288 | set text(size: 11pt) 289 | it 290 | } 291 | show footnote.entry: it => { 292 | set text(size: 6pt) 293 | 294 | grid( 295 | columns: (auto, 1fr), 296 | gutter: 1em, 297 | numbering(sym.dagger + "1", ..counter(footnote).at(it.note.location())), 298 | it.note.body 299 | ) 300 | } 301 | 302 | // タイトルページ 303 | { 304 | set page(header: {}) 305 | 306 | block( 307 | width: 100%, 308 | height: 100%, 309 | )[ 310 | #set align(center) 311 | 312 | #v(50pt) 313 | 314 | #[ 315 | #set text(font: fonts.sans-serif, weight: "bold") 316 | #set par(leading: 0.5em) 317 | 318 | #text(size: 24pt, title) 319 | 320 | #v(-5pt) 321 | 322 | #text(size: 18pt, subtitle) 323 | ] 324 | 325 | #v(40pt) 326 | 327 | #text(size: 10pt, weight: "bold")[#author #h(0.8em) 著] 328 | 329 | #v(1fr) 330 | 331 | #text(size: 12pt, font: fonts.sans-serif, weight: "bold", publisher) 332 | ] 333 | } 334 | 335 | body 336 | } 337 | 338 | #let document(outlined: true, body) = { 339 | // 目次 340 | if outlined { 341 | outline() 342 | } 343 | 344 | // ページ番号と見出しのナンバリングを変更 345 | set page(numbering: "1") 346 | set heading(numbering: (..args) => { 347 | let nums = args.pos() 348 | if nums.len() == 1 { 349 | numbering("1章 ", ..nums) 350 | } else { 351 | numbering("1.1.1 ", ..nums) 352 | } 353 | }) 354 | 355 | // 目次より後の節・項は目次に表示 356 | show heading.where(level: 2): set heading(outlined: true) 357 | show heading.where(level: 3): set heading(outlined: true) 358 | 359 | // ページ番号をリセット 360 | counter(page).update(1) 361 | 362 | body 363 | } 364 | 365 | #let appendix(body) = { 366 | // 見出しのナンバリングを変更 367 | set heading(numbering: (..args) => { 368 | let nums = args.pos() 369 | if nums.len() == 1 { 370 | numbering("付録A ", ..nums) 371 | } else { 372 | numbering("A.1.1 ", ..nums) 373 | } 374 | }) 375 | 376 | // 見出し番号をリセット 377 | counter(heading).update(0) 378 | 379 | body 380 | } 381 | --------------------------------------------------------------------------------