Typst 예시 북

이 책은 더 나은 Typst 코드를 작성하는 데 도움이 되는 상세한 튜토리얼 과 다양한 Typst 코드 조각(스니펫)을 제공합니다.

본 서적은 비공식 가이드입니다. 일부 스니펫이나 제안은 최신 버전과 다르거나 유효하지 않을 수 있습니다. 문제를 발견하시면 제보해 주세요.

여기에 소개된 모든 예제는 최신 버전의 Typst1에서 컴파일 되어야 합니다.

참고: 이 책은 현재 작업 중(WIP) 이며 내용이 지속적으로 업데이트됩니다. 모든 내용을 전적으로 신뢰하기보다는 참고용으로 활용해 주세요.

이 프로젝트가 도움이 되었다면 GitHub에서 스타를 눌러 응원해 주세요! 여러분의 관심은 이 책을 꾸준히 관리하고 업데이트하는 데 큰 동기가 됩니다.

목차

이 책은 목적에 따라 다음과 같은 장으로 구성되어 있습니다:

  1. Typst 기초
  2. Typst 스니펫
  3. Typst 패키지
  4. Typstonomicon

기여하기

다양한 형태의 기여를 언제든 환영합니다! 공유하고 싶은 유용한 코드 스니펫이 있다면 Issue를 남기거나 저장소에 Pull Request를 보내주세요.

활발하게 활동하시는 커뮤니티 멤버분들과 컴파일러 기여자의 참여를 특히 환영합니다. 또한, 초보자분들이 내용을 더 쉽게 이해할 수 있도록 주시는 피드백도 큰 도움이 됩니다.

감사의 말

자신의 코드 스니펫을 아낌없이 공유해 주신 커뮤니티의 모든 분들께 깊이 감사드립니다.

혹시 본인의 코드나 실명이 공개되는 것을 원치 않으시면 언제든 연락해 주세요. 조치해 드리겠습니다.

1

Typst의 새 버전이 출시된 후 책의 예제가 업데이트되기까지 다소 시간이 걸릴 수 있습니다. 업데이트가 늦어진다면 저를 언급(tag)해 주세요.

시작하기

Typst는 매우 가벼운 실행 파일로 빌드할 수 있는 오픈 소스 프로젝트입니다. 설치가 매우 간편하며 웹 브라우저에서도 바로 사용할 수 있습니다. Typst 팀이 직접 관리하고 개발하며, 이들의 주력 제품은 웹 기반의 Typst 전용 에디터입니다.

Typst를 활용하는 방법은 크게 두 가지입니다. 공식 웹 앱을 사용하거나, 로컬 환경에 직접 설치하여 평소 사용하는 에디터로 작업하는 방식입니다. 이 장에서는 각 방식의 장단점을 간략히 살펴보겠습니다.

웹 앱 (WebApp)

가장 간단하게 시작하는 방법은 typst.app에 가입하고 새 문서를 만드는 것입니다. 가입 후 바로 입력을 시작하면 오른쪽 미리보기 패널에서 실시간으로 렌더링된 결과를 확인할 수 있습니다.

처음에는 이것저것 직접 시도해 보시는 것을 추천합니다. Typst의 작동 방식이 어느 정도 익숙해지면, 잘 만들어진 템플릿 을 가져와서 바로 내용을 채워 넣을 수 있습니다. 물론 필요한 스타일을 처음부터 직접 만드는 것도 Typst에서는 어렵지 않습니다.

장점:

  • 별도의 설정 없이 바로 시작 가능
  • 강력한 협업 기능 제공
  • 풍부한 기능: Typst 팀이 공을 들여 만든 스마트 맞춤법 검사나 전문가를 위한 Vim 키 바인딩 등 유용한 기능이 많습니다.
  • 오프라인 작업 지원: 페이지가 한 번 로드되면 오프라인 상태에서도 작업할 수 있습니다. 문서 컴파일이 서버가 아닌 사용자의 브라우저에서 이루어지므로 인터넷 연결 없이도 실시간 수정이 가능하며, 변경 사항은 로컬에 저장됩니다.
  • 여러 기기에서 동일한 프로젝트를 이어가기 편리함
  • Typst Pro (유료): 협업 시 댓글 기능, GitHub 연동 등 고급 기능을 제공합니다. 이는 프로젝트를 후원(GitHub Sponsors 등)하거나 기여하여 응원할 수 있는 좋은 방법이기도 합니다.

단점:

  • 최초 접속 및 동기화에는 인터넷 연결이 필요함
  • 데이터와 작업 환경이 온라인에 종속됨 (백업은 가능하지만, 드물게 서버 장애가 발생할 수 있음)
  • 로컬 에디터나 Tinymist LSP에서 제공하는 일부 고급 기능이 부족할 수 있음

로컬 환경 설정

Tinymist

참고: 예전에 사용되던 'Typst LSP'는 더 이상 업데이트되지 않으므로 사용하지 마세요.

Tinymist는 커뮤니티에서 개발한 LSP1로, 공식 웹 앱보다 더 많은 기능을 제공하기도 합니다. 정의로 이동, 리팩토링, 자동 포맷팅 등 강력한 도구가 포함되어 있습니다. 공식 도구는 아니지만 성능과 안정성이 뛰어나 꼭 사용해 보시길 권장합니다.

LSP를 지원하는 대부분의 에디터(VS Code, Neovim, Emacs, Sublime Text, Helix, Zed 등)에서 사용할 수 있습니다.

로컬에서 작업하려면 평소 사용하는 에디터에 Tinymist 확장을 설치하세요. 실시간 미리보기와 함께 쾌적한 Typst 개발 환경을 누릴 수 있습니다.

주의: Tinymist는 내부적으로 Typst를 포함하고 있습니다. 때때로 에디터 확장의 버전이 낮으면 오래된 Typst 엔진을 사용할 수도 있으니 주의가 필요합니다. 또한 Tinymist를 통해 다양한 형식으로 문서를 내보낼 수 있으며, 저장 시 자동 내보내기 설정도 가능합니다. 터미널에서 tinymist preview document.typ을 실행해 브라우저로 미리보기를 띄울 수도 있습니다.

장점:

  • 가장 방대한 기능: 현존하는 Typst 도구 중 가장 강력한 기능을 제공합니다.
  • 개발자나 숙련된 사용자에게 최적화된 환경을 선사합니다.
  • 에디터의 생태계를 그대로 활용할 수 있어 GitHub 연동이나 자동화 구성이 자유롭습니다.

단점:

  • 웹 앱에 비해 초기 설정이 다소 번거로울 수 있습니다.
  • 협업 시 GitHub 등을 통해 직접 환경을 맞춰야 하므로 조금 더 복잡합니다.

CLI (명령줄 도구)

Typst는 자체 CLI 도구도 제공합니다. 시스템에 설치한 후 터미널에서 typst compile document.typ을 실행하면 즉시 PDF가 생성됩니다.

설치 방법:

  • Windows: GitHub Releases에서 실행 파일을 다운로드해 PATH에 등록하세요.
  • macOS/Linux: 공식 가이드를 참조하세요. 패키지 매니저(brew, apt 등)를 통한 설치는 실제 버전보다 늦을 수 있으니 가급적 직접 설치를 권장합니다.

CLI를 사용하면 메모장 같은 단순한 에디터에서도 문서를 작성하고 결과를 확인할 수 있습니다. 특히 typst watch document.typ 명령을 사용하면 파일이 수정될 때마다 자동으로 다시 컴파일합니다.

이때 PDF가 변경될 때마다 화면을 자동으로 갱신해 주는 실시간 미리보기 PDF 뷰어를 함께 사용하면 편리합니다.

추천 뷰어:

  • SumatraPDF (Windows)
  • Zathura, Sioyek (Linux)
  • Okular (멀티 플랫폼)

장점:

  • 빠르고 정확하게 결과물을 내보낼 때 유용합니다.
  • 자동화 스크립트나 다른 프로그램과의 연동에 최적화되어 있습니다.
  • 별도의 패키지 매니저 없이도 명령 한 번으로 간편하게 업데이트할 수 있습니다.

단점:

  • 터미널 환경에 익숙하지 않으면 사용이 어려울 수 있습니다.
  • 에디터 자체의 보조 기능(자동 완성 등)은 제공하지 않습니다.

이제 시작해 봅시다!

환경 설정이 완료되었다면, 이제 본격적으로 Typst의 세계로 뛰어들어 봅시다!

1

LSP (Language Server Protocol): 에디터에서 자동 완성, 정의 이동, 리팩토링 등 프로그래밍 언어의 보조 기능을 제공하기 위한 표준 프로토콜입니다.

Typst 기초

이 장에서는 Typst로 문서를 작성할 때 알아야 할 가장 중요한 사항들을 차근차근 소개합니다.

공식 튜토리얼보다 훨씬 더 많은 내용을 다루고 있으므로, 숙련된 사용자들에게도 흥미로운 읽을거리가 될 것입니다.

일부 예제는 공식 튜토리얼공식 레퍼런스에서 가져왔습니다. 대부분은 이 책을 위해 특별히 제작되고 편집되었습니다.

중요: 대부분의 경우 렌더링된 문서의 "잘린" 예시(여백 없음, 좁은 너비 등)가 사용됩니다.

원하는 대로 간격을 설정하려면 공식 페이지 설정 가이드를 참조하세요.

예제로 배우는 튜토리얼

Typst 기초의 첫 번째 섹션은 공식 튜토리얼과 매우 유사하지만, 더 전문적인 예제를 사용하고 설명을 간소화했습니다. 그럼에도 불구하고 공식 튜토리얼을 꼭 읽어보시는 것을 강력히 추천합니다.

마크업 언어

시작하기

Typst에서 타이핑을 시작하는 것은 쉽습니다.
대부분의 경우 패키지나 다른 복잡한 설정이 필요하지 않습니다.

빈 줄은 텍스트를 새로운 단락으로 이동시킵니다.

참고로, 글꼴이 지원하는 한 모든 언어와 유니코드 기호를
문제없이 사용할 수 있습니다: ßçœ̃ɛ̃ø∀αβёыა😆…
Rendered image

마크업

= 마크업

이것은 제목(heading)이었습니다. 이름 앞의 `=` 개수는 제목 수준에 해당합니다.

== 2단계 제목

이제 _강조(emphasis)_*굵게(bold)* 표시된 텍스트로 넘어가 보겠습니다.

마크업 구문은 일반적으로
`AsciiDoc`과 비슷합니다 (이것은 고정 폭 텍스트를 위한 `raw`였습니다!)
Rendered image

줄 바꿈 및 이스케이프

"\\" 기호를 사용하여 \
어디서든 줄을 \
바꿀 수 있습니다.

또한 마크업이나 다른 특수 기호로
해석되지 않기를 원하는 경우,
해당 기호를 이스케이프(\_기호 앞뒤에 언더스코어\_ 등)하는 데
이 기호를 사용할 수 있습니다.
Rendered image

주석 및 코드 블록

`//`와 `/* 주석 */`를 사용하여 주석을 작성할 수 있습니다:
// 이와 같이
/* 또는 이와
같은 방식으로 */

```typ
혹시 소스를 읽지 않으셨을까 봐,
작성 방식은 다음과 같습니다:

// 이와 같이
/* 또는 이와
같은 방식으로 */

참고로, 저는 이 모든 것을 *구문 강조*가 적용된 _코드 블록(fenced code block)_으로 작성하고 있습니다!
```
Rendered image

스마트 따옴표

== 그 외에 또 무엇이 있나요?

기본 "마크업" 구문에는 많은 것이 없지만,
곧 훨씬 더 흥미로운 것들을 보게 될 것입니다!
여기서 자동으로 짝이 맞춰지는 "스마트 따옴표"를 확인하셨기를 바랍니다.
Rendered image

목록

- 목록을 간단하게 작성하는 것은 아주 좋습니다.
- 복잡할 것 없이, `-`로 항목을 시작하면
  목록이 됩니다.
  - 들여쓰기를 통해 하위 목록을 만듭니다.

+ 번호가 매겨진 목록은 `-` 대신 `+`로 시작합니다.
+ 목록을 위한 다른 대안 마크업 구문은 없습니다.
+ 그러니 `-`와 `+`만 기억하세요. 다른 기호들은
  의도치 않은 방식으로 작동하지 않을 것입니다.
  + 이것이 Typst 마크업의 일반적인 특징입니다.
  + 마크다운과 달리, 무언가를 작성하는 방법은
    오직 한 가지뿐입니다.
Rendered image

참고:

Typst의 번호 매겨진 목록은 마크다운 스타일의 목록 구문과 다릅니다. 손으로 직접 숫자를 써도 번호가 유지됩니다:

1. 사과
1. 오렌지
1. 복숭아
Rendered image

수식


수식($a + b/c = sum_i x^i$)이
가능하며 꽤 예쁘게 표현된다는 점만 언급하겠습니다:

$
7.32 beta +
  sum_(i=0)^nabla
    (Q_i (a_i - epsilon)) / 2
$

수식에 대해 더 자세히 알아보려면 해당 장을 참조하세요.
Rendered image

함수

함수

이제 좀 더 복잡한 내용으로 넘어가 보겠습니다.

Typst 마법의 주요 부분은 스크립팅입니다.
스크립팅 모드로 들어가려면 `#`을 입력하고 그 뒤에 *함수 이름*입력하세요. _지루한 것_부터 시작해 보겠습니다:

#lorem(50)

_이 *함수* 는 방금 50개의 "Lorem Ipsum" 단어를 생성했습니다!_
Rendered image

더 많은 함수

#underline[함수는 무엇이든 할 수 있습니다!]

#text(orange)[]#text(size: 0.8em)[모든] #sub[]을요!

#figure(
  caption: [
    이것은 Typst로 작성된 초기 논문 중 하나의 스크린샷입니다. \
    _이 모든 것들도 #text(blue)[사용자 정의 함수]로 작성되었습니다._
  ],
  image("../boxes.png", width: 80%)
)

사실, 여러분은 마크업을 #strong[잊어버리고]
모든 곳에 함수만 #emph[사용해서 작성]할 수도 있습니다!

#list[
  그 모든 마크업은 사실 함수 위에 씌워진 #emph[구문 설탕(syntax sugar)]일 뿐입니다!
]
Rendered image

함수를 호출하는 방법

먼저 `#`으로 시작합니다. 그런 다음 이름을 씁니다.
마지막으로 괄호를 쓰고 그 안에 무언가를 넣을 수 있습니다.

#link("https://typst.app/docs/reference/")[공식 참조 문서]에서
수많은 내장 함수를 찾아볼 수 있습니다.

#quote(block: true, attribution: "Typst 예시 북")[
  맞습니다, 링크, 인용 및 기타 수많은
  문서 요소들이 함수로 생성됩니다.
]
Rendered image

함수 인수 (Arguments)

함수 인수에는 _두 가지 유형_이 있습니다:

+ *위치 인수(Positional).* `lorem(50)`의 `50`과 같습니다.
  그냥 괄호 안에 쓰면 됩니다. 여러 개인 경우 쉼표를 사용하세요.
+ *명명된 인수(Named).* `#quote(attribution: "누군가")`에서와 같습니다.
  이름과 콜론 뒤에 값을 씁니다.

명명된 인수인 경우 일종의 _기본값_ 을 가집니다.
그것이 무엇인지 확인하려면
#link("https://typst.app/docs/reference/")[공식 Typst 참조 문서]를 확인하세요.
Rendered image

콘텐츠 (Content)

이제 우리만의 함수를 직접 작성해 보아야 할 것 같습니다.

Typst 언어에서 가장 "보편적인" 타입은 *콘텐츠(content)*입니다.
문서에 작성하는 모든 것이 콘텐츠가 됩니다.

#[
  하지만 *대괄호* 를 사용해 _스크립팅 모드_ 에서
  명시적으로 콘텐츠를 생성할 수도 있습니다.

  대괄호 안에서는 어떤 마크업 함수나
  원하는 모든 것을 사용할 수 있습니다.
]
Rendered image

마크업 및 코드 모드

`#`을 사용하면 코드 모드로 "전환"됩니다.
`[]`를 사용하면 다시 _마크업_(또는 콘텐츠) 모드로 돌아갑니다:

// +-- 마크업(기본 모드)에서 해당 함수를 위한 스크립팅으로 전환
// |                 +-- 스크립팅 모드: `text` 호출, 마지막 인수는 마크업
// |     첫 번째 인수  |
// v     vvvvvvvvv   vvvv
   #rect(width: 5cm, text(red)[안녕 *세상아*])
//  ^^^^                       ^^^^^^^^^^^^^ `text`를 위한 마크업 인수일 뿐입니다.
//  |
//  +-- 스크립팅 모드에서 너비(width)와 다른 콘텐츠라는 두 인수로 `rect`를 호출합니다.
Rendered image

함수에 콘텐츠 전달하기

그렇다면 함수 뒤의 이 대괄호들은 무엇일까요?

만약 *함수 바로 뒤에 콘텐츠를 작성하면,
그 콘텐츠는 해당 함수의 위치 인수로 전달됩니다*.

#quote(block: true)[
  이를 통해 저는 #underline[함수]에 전달하는 것들에
  #text(red)[_말 그대로 무엇이든_] 작성할 수 있게 됩니다!
]
Rendered image

콘텐츠 전달하기, 파트 II

명확하게 하기 위해 다음과 같이 작성하면:

```typ
- #text(red)[빨간색 텍스트]
- #text([빨간색 텍스트], red)
- #text("빨간색 텍스트", red)
//      ^            ^
// 여기서 따옴표는 콘텐츠가 아니라 일반 문자열을 의미합니다!
// 이것은 그냥 텍스트입니다.
```

이 모든 것은 #text([빨간색 텍스트], red)와 같은 결과를 보여줍니다.
Rendered image

치트시트

다음은 가장 필요한 몇 가지 Typst 개념에 대한 간략한 치트시트입니다 (@mewmew 및 @7i 작성):

치트시트

저장소 링크

기초 스타일링

set 규칙

#set page(width: 15cm, margin: (left: 4cm, right: 4cm))

멋진 결과물이 나왔네요! 하지만 문서의 모든 곳에서 매번 복잡한 인수를 
사용하며 함수를 호출하는 것은 매우 번거로운 일입니다.

그래서 Typst에는 **규칙(rules)**이라는 개념이 있습니다. 
정확히는 문서를 구성하는 요소들이 따라야 할 규칙이죠.

#set par(justify: true)

가장 먼저 살펴볼 것은 `set` 규칙입니다.
위 예제에서 `par`(단락, paragraph의 약자)에 규칙을 적용하자 
이후의 모든 단락이 **양쪽 맞춤(justified)**으로 정렬되었습니다.

이 규칙은 설정된 지점 이후의 모든 요소에 적용되지만, 
특정 **범위(scope)** 내에서만 유효합니다(범위에 대해서는 나중에 자세히 다룹니다).

#par(justify: false)[
  물론 필요할 때는 `set` 규칙을 개별적으로 덮어쓸 수도 있습니다. 
  `set` 규칙은 단순히 해당 요소의 인수에 대한 **기본값** 을 
  지정하는 것이기 때문입니다.
]

참고: 위 예제의 첫 번째 줄에서는 양쪽 맞춤 효과를 잘 보여주기 위해 
페이지 크기를 줄이고 좌우 여백을 넓게 설정했습니다.
Rendered image

길이 단위

다른 규칙으로 넘어가기 전에, Typst에서 사용하는 길이에 대해 알아봅시다. 
Typst는 다음과 같은 다양한 절대 길이 단위를 지원합니다.

#set rect(height: 1em)

#table(
  columns: 2,
  [포인트(Points)], rect(width: 72pt),
  [밀리미터(Millimeters)], rect(width: 25.4mm),
  [센티미터(Centimeters)], rect(width: 2.54cm),
  [인치(Inches)], rect(width: 1in),
  [글꼴 크기 기준(Relative to font size)], rect(width: 6.5em)
)

여기서 `1em`은 **현재 글꼴의 크기**와 같습니다. 
문맥에 맞춰 유연하게 크기를 조절할 때 매우 유용한 단위이므로 
앞으로 자주 사용하게 될 것입니다.
Rendered image

다양한 요소 설정하기

모든 내장 함수와 해당 함수의 이름 있는 인자(named arguments)에 set 규칙을 적용할 수 있습니다. 즉, 원하는 인자를 기본값으로 고정할 수 있다는 뜻입니다.

예를 들어, 문서의 모든 인용구에 출처를 자동으로 표시하고 싶다면 다음과 같이 설정할 수 있습니다.

#set quote(block: true, attribution: [Typst 예시 북])

#quote[
  Typst는 정말 훌륭한 도구입니다!
]

#quote[
  인터넷에 떠도는 명언의 가장 큰 문제는 
  그 진위 여부를 확인하기가 매우 어렵다는 점입니다.
]
Rendered image

나만의 기본값 만들기

set 규칙을 활용하면 문서 전체의 스타일을 취향에 맞게 한 번에 정의할 수 있습니다.

#set par(justify: true)
#set list(indent: 1em)
#set enum(indent: 1em)
#set page(numbering: "1")

- 목록 첫 번째 항목
- 목록 두 번째 항목

+ 순서가 있는 항목 1
+ 순서가 있는 항목 2
Rendered image

Typst의 기본 스타일이 마음에 들지 않는다면, 주저하지 말고 여러분만의 set 규칙을 정의해 보세요!

번호 매기기

= 번호 매기기

제목(heading)과 같은 일부 요소에는 `numbering`이라는 속성이 있습니다. 
여기에 특정 "번호 매기기 패턴"을 지정하고 `set` 규칙과 결합하면 
복잡한 계층 구조도 자동으로 관리할 수 있습니다.

#set heading(numbering: "I.1:")

= 1단계 제목
= 또 다른 1단계 제목
== 2단계 소제목
== 또 다른 2단계 소제목
=== 3단계 상세 제목
== 다시 2단계로
= 다시 1단계로
Rendered image

이외에도 설정할 수 있는 유용한 속성들이 무수히 많습니다. 더 자세한 내용은 공식 레퍼런스를 참고해 보세요.

이제 더 강력하고 흥미로운 기능인 show 규칙에 대해 알아보겠습니다.

고급 스타일링

show 규칙

스타일링의 정점에는 **`show` 규칙**이 있습니다. 
소스 코드와 출력 결과를 비교하며 어떻게 작동하는지 확인해 보세요.

#show "조심하세요": strong[놀아요]

이 기능은 매우 강력해서 문서의 내용을 완전히 뒤바꿀 수도 있습니다. 
따라서 신중하게 사용해야 합니다.

#show "인질로 잡혀 있어요": text(green)[전 괜찮아요]

방금 어떤 일이 일어났나요? 소스 코드에는 분명 "조심하세요"라고 적었지만, 
실제 문서에는 "놀아요"라고 출력되었습니다. 

도와주세요, 인질로 잡혀 있어요.
Rendered image

더 실용적인 활용법

`show` 규칙은 특정 **선택자(selector)**를 찾아 
원하는 형태나 스타일로 변형하는 기능을 제공합니다.

다음 예제처럼 매우 실용적으로 활용할 수 있습니다.

#show emph: set text(blue)

이제 문서 전체에서 _강조(emphasize)_된 텍스트는 
기울임꼴과 동시에 **파란색** 으로 표시됩니다. 
일일이 색상을 지정할 필요가 없어 매우 편리합니다.
Rendered image

show 규칙의 다양한 문법

`show` 규칙은 겉보기에 복잡해 보일 수 있지만, 근본적인 원리는 모두 같습니다. 

// 아래 예제들은 기본적으로 특정 함수를 입히는 것과 같습니다.
// `with`는 인자가 미리 설정된 새로운 함수를 만듭니다.
#let redify(string) = text(red, string)

// 주황색 테두리를 입히는 함수 정의
#let framify(object) = rect(object, stroke: orange)

// 문서 전체의 기본 글자 색상을 파란색으로 설정
#show: set text(blue)

파란색 글자들.

// 모든 요소를 사각형 프레임으로 감싸기
#show: framify

프레임이 씌워진 텍스트.

// 익명 함수를 사용해 위와 동일한 효과를 낼 수도 있습니다.
#show: a => framify(a)

이중 프레임.

// 특정 단어 "the"에만 빨간색 적용
#show "the": redify
// 모든 제목(heading)의 색상을 보라색으로 설정
#show heading: set text(purple)

= 결론

결국 이 모든 규칙은 '무엇을(선택자)', '어떻게(변형)' 바꿀 것인지를 정의하는 과정입니다.
Rendered image

블록 (Blocks)

show 규칙의 가장 중요한 용도 중 하나는 간격을 조절하는 것입니다. 텍스트 설정을 통해 글자 모양을 바꾸듯, 모든 블록 요소에도 스타일을 적용할 수 있습니다.

텍스트 이전
= 제목
텍스트 이후

#show heading: set block(spacing: 0.5em)

텍스트 이전
= 제목
텍스트 이후
Rendered image

선택자 (Selector) 활용

`show` 규칙은 다음과 같은 다양한 선택자를 사용할 수 있습니다.

- 요소 함수 (element functions)
- 문자열 (strings)
- 정규 표현식 (regular expressions)
- 필드 필터 (field filters)

특정 조건에만 스타일을 적용하는 예시를 살펴보겠습니다.

#show heading.where(level: 1): set align(center)

= 1단계 제목 (중앙 정렬됨)
== 2단계 제목 (기본 정렬)

물론 개별 요소에서 직접 설정할 수도 있지만, 
`show` 규칙을 쓰면 일관된 스타일을 유지하기 훨씬 쉽습니다.

#align(center)[== 수동으로 정렬한 제목]
Rendered image

사용자 정의 서식 만들기

제목 스타일을 완전히 새롭게 정의하는 함수를 직접 작성해 봅시다.

// "it"은 선택된 제목(heading) 객체입니다.
#show heading: it => {
  // 중앙 정렬 설정
  set align(center)
  // 글자 크기와 굵기 설정
  set text(12pt, weight: "regular")
  
  // 내용을 작은 대문자(smallcaps)로 변환하고 블록으로 감쌉니다.
  block(smallcaps(it.body))
}

= Smallcaps Heading Example
Rendered image

"논문 스타일" 예제

Typst의 강력한 스타일링 기능을 조합하면 복잡한 논문 형식도 금방 만들 수 있습니다.

#set page(
  // 상단 헤더 설정
  header: align(
    right + horizon,
    [여기에 헤더 내용 입력]
  ),
  height: 12cm
)

#align(center, text(17pt)[
  *문서의 주요 제목*
])

#grid(
  columns: (1fr, 1fr),
  align(center)[
    저자 성함 \
    소속 기관 \
    #link("mailto:some@mail.edu")
  ],
  align(center)[
    공동 저자 \
    공동 소속 기관 \
    #link("mailto:another@mail.edu")
  ]
)

// 본문을 두 단(2 columns)으로 나눕니다.
#show: rest => columns(2, rest)

// 1단계 제목 스타일 정의
#show heading.where(level: 1): it => block(width: 100%)[
  #set align(center)
  #set text(12pt, weight: "regular")
  #smallcaps(it.body)
]

// 2단계 제목 스타일 정의 (이탤릭체 및 마침표 추가)
#show heading.where(level: 2): it => text(
  size: 11pt,
  weight: "regular",
  style: "italic",
  it.body + [.],
)

= 서론
== 소제목 예시
#lorem(15)

== 두 번째 섹션
#lorem(20)

= 본론
#lorem(40)
Rendered image

템플릿

템플릿

스타일을 다른 파일에서도 재사용하고 싶다면 템플릿(template) 관용구를 사용할 수 있습니다. setshow 규칙은 현재 범위(scope) 내에서만 활성화되므로, 파일을 가져온(import) 대상 파일의 콘텐츠에는 영향을 주지 않습니다. 하지만 함수를 사용하면 예측 가능한 방식으로 이를 해결할 수 있습니다:

// 다음과 같은 함수를 정의합니다:
// - 콘텐츠를 인수로 받음
// - 스타일을 적용함
// - 스타일이 적용된 콘텐츠를 반환함
#let apply-template(body) = [
  #show heading.where(level: 1): emph
  #set heading(numbering: "1.1")
  // ...
  #body
]

이것은 다음과 동일합니다:

// 스크립팅 모드를 사용하면 필요한 해시(#) 기호의 수를 줄일 수 있습니다.
// 위와 동일하지만, 마크업 모드에서 스크립팅 모드로 전환하기 위해
// `[...]`를 `{...}`로 바꿨습니다.
#let apply-template(body) = {
  show heading.where(level: 1): emph
  set heading(numbering: "1.1")
  // ...
  body
}

그런 다음 메인 파일에서:

#import "template.typ": apply-template
#show: apply-template

이렇게 하면 문서의 나머지 부분에 "템플릿" 함수가 적용됩니다!

인수 전달하기

// 선택적인 명명된 인수를 추가합니다.
#let apply-template(body, name: "나의 문서") = {
  show heading.where(level: 1): emph
  set heading(numbering: "1.1")

  align(center, text(name, size: 2em))

  body
}

그런 다음, 템플릿 파일에서:

#import "template.typ": apply-template

// `func.with(..)`는 함수에 인수를 적용하고,
// 해당 기본값이 적용된 새로운 함수를 반환합니다.
#show: apply-template.with(name: "보고서")

// 이것은 기능적으로 다음과 동일합니다.
#let new-template(..args) = apply-template(name: "보고서", ..args)
#show: new-template

스크립팅을 이해한다면 템플릿을 작성하는 것은 매우 쉽습니다.

템플릿 작성에 대한 더 자세한 정보는 공식 튜토리얼에서 확인할 수 있습니다.

아직 공식 템플릿 저장소는 없지만, awesome-typst에 수많은 커뮤니티 템플릿이 있습니다.

필수 지식 (Must-know)

이 섹션에는 "튜토리얼"에 포함될 만큼 일반적이지는 않지만, 적절한 조판을 위해 여전히 매우 중요한 내용들이 포함되어 있습니다.

사용하지 않을 것이 확실한 내용은 자유롭게 건너뛰어도 좋습니다.

박스와 블록 (Boxing & Blocking)

박스(box)를 사용하여 무엇이든
텍스트 안으로 감쌀 수 있습니다: #box(image("../tiger.jpg", height: 2em)).

블록(block)은 항상 "별도의 단락"이 됩니다.
텍스트 안에 들어가지 않습니다: #block(image("../tiger.jpg", height: 2em))
Rendered image

둘 다 비슷하고 유용한 속성을 가지고 있습니다:

#box(stroke: red, inset: 1em)[박스 텍스트]
#block(stroke: red, inset: 1em)[블록 텍스트]
Rendered image

rect

block처럼 작동하지만 유용한 기본 여백(inset)과 테두리(stroke)를 가진 rect도 있습니다:

#rect[블록 텍스트]
Rendered image

그림 (Figures)

문서에 _그림(figure)_을 추가하려는 경우 figure 함수를 사용하세요. 거기서 박스나 블록을 사용하려고 하지 마세요.

그림은 중앙에 배치된 이미지(아마도 캡션 포함), 표, 심지어 코드와 같은 것들입니다.

@tiger 는 호랑이를 보여줍니다. 호랑이는
동물입니다.

#figure(
  image("../tiger.jpg", width: 80%),
  caption: [호랑이.],
) <tiger>
Rendered image

사실, 원하는 무엇이든 그 안에 넣을 수 있습니다:

당신에게 편지를 쓰라고 하더군요. 여기 있습니다:

#figure(
  text(size: 5em)[],
  caption: [나 멋지지 않나요?],
) 
Rendered image

간격 사용하기 (Using spacing)

대부분의 경우 함수에 간격을 전달하게 됩니다. 오직 크기 만 받는 특수한 함수 필드들이 있습니다. 보통 width, length, in(out)set, spacing 등과 같은 이름으로 불립니다.

CSS에서와 마찬가지로, Typst에서 간격을 설정하는 방법 중 하나는 요소의 여백(margin)과 패딩(padding)을 설정하는 것입니다. 하지만 h(가로 간격)와 v(세로 간격) 함수를 사용하여 간격을 직접 삽입할 수도 있습니다.

참조 링크: h, v.

가로 #h(1cm) 간격.
#v(1cm)
그리고 세로 간격도 있습니다!
Rendered image

절대 길이 단위

참조 링크

절대 길이(일명 그냥 "길이") 단위는 외부 콘텐츠나 부모의 크기에 영향을 받지 않습니다.

#set rect(height: 1em)
#table(
  columns: 2,
  [포인트(Points)], rect(width: 72pt),
  [밀리미터(Millimeters)], rect(width: 25.4mm),
  [센티미터(Centimeters)], rect(width: 2.54cm),
  [인치(Inches)], rect(width: 1in),
)
Rendered image

현재 글꼴 크기 기준

1em = 현재 글꼴 크기 1배:

#set rect(height: 1em)
#table(
  columns: 2,
  [센티미터], rect(width: 2.54cm),
  [글꼴 크기 기준], rect(width: 6.5em)
)

글꼴 크기 두 배: #box(stroke: red, baseline: 40%, height: 2em, width: 2em)
Rendered image

매우 편리한 단위이므로 Typst에서 많이 사용됩니다.

조합 (Combined)

조합: #box(rect(height: 5pt + 1em))

#(5pt + 1em).abs
#(5pt + 1em).em
Rendered image

비율 길이 (Ratio length)

참조 링크

1% = 해당 차원의 부포 크기 대비 1%

이 선의 너비는 사용 가능한 페이지 너비(여백 제외)의 50%입니다:

#line(length: 50%)

이 선의 너비는 박스 너비의 50%입니다: #box(stroke: red, width: 4em, inset: (y: 0.5em), line(length: 50%))
Rendered image

상대 길이 (Relative length)

참조 링크

절대 길이와 비율 길이를 결합하여 _상대 길이_를 만들 수 있습니다:

#rect(width: 100% - 50pt)

#(100% - 50pt).length \
#(100% - 50pt).ratio
Rendered image

분수 길이 (Fractional length)

참조 링크

단일 분수 길이는 부모를 채우기 위해 _가능한 최대 크기_를 차지합니다:

왼쪽 #h(1fr) 오른쪽

#rect(height: 1em)[
  #h(1fr)
]
Rendered image

분수를 사용할 수 있는 곳은 많지 않으며, 주로 hv에서 사용됩니다.

여러 개의 분수

하나의 부모 안에서 여러 개의 분수를 사용하면, 남은 모든 공간을 각 숫자에 비례하여 나누어 차지합니다:

왼쪽 #h(1fr) 왼쪽 중심 #h(2fr) 오른쪽
Rendered image

중첩된 레이아웃 (Nested layout)

분수는 부모 내에서만 작동한다는 점을 기억하세요. 중첩된 레이아웃에서 분수에 의존하지 마세요:

단어: #h(1fr) #box(height: 1em, stroke: red)[
  #h(2fr)
]
Rendered image

배치, 이동, 크기 조절 및 숨기기 (Placing, Moving, Scale & Hide)

레이아웃으로 임의의 작업을 수행하고, 사용자 정의 요소를 만들고, 현재 Typst의 제한 사항을 우회하려는 경우 매우 중요한 섹션입니다.

TODO: 작업 중(WIP), 텍스트 및 더 나은 예제 추가 예정

배치 (Place)

_레이아웃을 무시_하고, 부모 및 현재 위치를 기준으로 특정 개체를 배치합니다. 배치된 개체는 레이아웃에 영향을 주지 않습니다.

참조 링크

#set page(height: 60pt)
안녕, 세상아!

#place(
  top + right, // 페이지 오른쪽 상단에 배치
  square(
    width: 20pt,
    stroke: 2pt + blue
  ),
)
Rendered image

place를 사용한 기본적인 플로팅(floating)

#set page(height: 150pt)
#let note(where, body) = place(
  center + where,
  float: true,
  clearance: 6pt,
  rect(body),
)

#lorem(10)
#note(bottom)[하단 1]
#note(bottom)[하단 2]
#lorem(40)
#note(top)[상단]
#lorem(10)
Rendered image
Rendered image

dx, dy

원래 위치를 기준으로 (dx, dy)만큼 수동으로 위치를 변경합니다.

#set page(height: 100pt)
#for i in range(16) {
  let amount = i * 4pt
  place(center, dx: amount - 32pt, dy: amount)[A]
}
Rendered image

이동 (Move)

참조 링크

#rect(inset: 0pt, move(
  dx: 6pt, dy: 6pt,
  rect(
    inset: 8pt,
    fill: white,
    stroke: black,
    [아브라카다브라]
  )
))
Rendered image

크기 조절 (Scale)

레이아웃에 영향을 주지 않고 콘텐츠의 _크기를 조절_합니다.

참조 링크

#scale(x: -100%)[좌우가 반전되었습니다.]
Rendered image
A#box(scale(75%)[A])A \
B#box(scale(75%, origin: bottom + left)[B])B
Rendered image

숨기기 (Hide)

콘텐츠를 보여주지는 않지만, 그 자리에 빈 공간을 남겨둡니다.

참조 링크

안녕 철수 \
#hide[안녕] 영희
Rendered image

정렬 및 패딩 (Align & Padding)

텍스트나 요소의 배치와 내부 여백을 조절하는 기본 도구들입니다.

정렬 (Align)

align 함수를 사용하여 요소를 부모 컨테이너 내에서 정렬합니다.

#align(center)[이 텍스트는 중앙에 배치됩니다.]

#align(right + bottom)[
  오른쪽 아래에 배치되는 내용입니다.
]
Rendered image
  • 가로 정렬: left, center, right
  • 세로 정렬: top, horizon, bottom
  • 조합: right + top과 같이 조합하여 사용 가능합니다.

패딩 (Padding)

pad 함수는 요소의 주변에 공백을 추가합니다.

#set rect(stroke: 1pt)
#rect[패딩이 없는 상자]
#pad(left: 2em)[#rect[왼쪽에 2em 패딩이 추가된 상자]]

#pad(x: 1em, y: 0.5em)[가로 1em, 세로 0.5em 패딩]
Rendered image

박스의 내부 여백 (Inset)

boxblock, rect 등은 내부 여백인 inset 속성을 가집니다. pad는 요소 바깥 에 공백을 주고, inset은 요소 안쪽 에 공백을 줍니다.

#rect(inset: 10pt)[내부 여백이 10pt인 상자]
Rendered image

표와 그리드 (Tables and grids)

문서에서 표를 사용할 계획이 없다면 표에 대해 반드시 알 필요는 없지만, 그리드(grid)는 _문서 레이아웃_에 매우 유용할 수 있습니다. 나중에 책에서 두 가지 모두를 사용할 것입니다.

공식 문서의 예제를 복사하는 데 시간을 낭비하지 맙시다. 그냥 가볍게 훑어만 보세요, 알겠죠?

기본 스니펫

전개 (Spreading)

전개 연산자(여기 참조)는 특히 표에서 유용할 수 있습니다:

#set text(size: 9pt)

#let yield_cells(n) = {
  for i in range(0, n + 1) {
    for j in range(0, n + 1) {
      let product = if i * j != 0 {
        // 더 예쁜 외관을 위해 수식 사용
        if j <= i { $#{ j * i }$ } 
        else {
          // 표의 윗부분
          text(gray.darken(50%), str(i * j))
        }
      } else {
        if i == j {
          // 오른쪽 상단 모서리 
          $times$
        } else {
          // 둘 중 하나가 0이면 상단/좌측에 위치함
          $#{i + j}$
        }
      }
      // 이것은 배열이며, for 루프는 이들을 
      // 하나의 커다란 셀 배열로 병합합니다.
      (
        table.cell(
          fill: if i == j and j == 0 { orange } // 오른쪽 상단 모서리
          else if i == j { yellow } // 대각선
          else if i * j == 0 { blue.lighten(50%) }, // 곱하는 수
          product,),
      )
    }
  }
}

#let n = 10
#table(
  columns: (0.6cm,) * (n + 1), rows: (0.6cm,) * (n + 1), align: center + horizon, inset: 3pt, ..yield_cells(n),
)
Rendered image

표의 행 강조하기

#table(
  columns: 2,
  fill: (x, y) => if y == 2 { highlight.fill },
  [A], [B],
  [C], [D],
  [E], [F],
  [G], [H],
)
Rendered image

개별 셀의 경우 다음과 같이 사용합니다:

#table(
  columns: 2,
  [A], [B],
  table.cell(fill: yellow)[C], table.cell(fill: yellow)[D],
  [E], [F],
  [G], [H],
)
Rendered image

표 나누기

표는 페이지 사이에서 자동으로 나뉩니다.

#set page(height: 8em)
#(
table(
  columns: 5,
  [Aligner], [publication], [Indexing], [Pairwise alignment], [Max. read length  (bp)],
  [BWA], [2009], [BWT-FM], [Semi-Global], [125],
  [Bowtie], [2009], [BWT-FM], [HD], [76],
  [CloudBurst], [2009], [Hashing], [Landau-Vishkin], [36],
  [GNUMAP], [2009], [Hashing], [NW], [36]
  )
)
Rendered image
Rendered image

하지만 다른 요소 내부에서 표를 나눌 수 있게 하려면, 해당 요소도 나눌 수 있게 만들어야 합니다:

#set page(height: 8em)
// 이것이 없으면 표가 여러 페이지에 걸쳐 나뉘지 못합니다.
#show figure: set block(breakable: true)
#figure(
table(
  columns: 5,
  [Aligner], [publication], [Indexing], [Pairwise alignment], [Max. read length  (bp)],
  [BWA], [2009], [BWT-FM], [Semi-Global], [125],
  [Bowtie], [2009], [BWT-FM], [HD], [76],
  [CloudBurst], [2009], [Hashing], [Landau-Vishkin], [36],
  [GNUMAP], [2009], [Hashing], [NW], [36]
  )
)
Rendered image
Rendered image

프로젝트 구조

대규모 문서

문서가 충분히 커지면 탐색하기 어려워집니다. 아직 그 정도 크기에 도달하지 않았다면 이 섹션을 무시해도 됩니다.

이를 관리하기 위해 문서를 _장(chapters)_으로 나누는 것을 추천합니다. 이것은 작업을 위한 한 가지 방법일 뿐이며, 작동 방식을 이해하면 원하는 대로 할 수 있습니다.

두 개의 장이 있다고 가정하면 추천 구조는 다음과 같습니다:

#import "@preview/treet:0.1.1": *

#show list: tree-list
#set par(leading: 0.8em)
#show list: set text(font: "Noto Sans CJK KR", size: 0.8em)
- chapters/
  - chapter_1.typ
  - chapter_2.typ
- main.typ 👁 #text(gray)[← 문서 진입점]
- template.typ
Rendered image
정확한 파일 이름은 여러분에게 달려 있습니다.

각 파일에 무엇을 넣을지 살펴봅시다.

템플릿 (Template)

"template" 파일에는 장(chapter) 전체에서 사용할 _모든 유용한 함수와 변수_가 들어갑니다. 자신만의 템플릿이 있거나 템플릿을 작성하고 싶다면 여기에 작성할 수 있습니다.

// template.typ

#let template = doc => {
    set page(header: "나의 멋진 문서")
    show "physics": "magic"
    doc
}

#let infoblock = block.with(stroke: blue, fill: blue.lighten(70%))
#let author = "@sitandr"

메인 (Main)

전체 컴파일된 문서를 얻으려면 이 파일을 컴파일해야 합니다.

// main.typ

#import "template.typ": *
// 템플릿이 있다면
#show: template

= 이것은 문서 제목입니다

// 추가 서식

#show emph: set text(blue)

// 하지만 여기에 함수나 변수를 정의하지 마세요!
// 챕터에서는 볼 수 없습니다

// 이제 챕터 자체를 Typst 콘텐츠로 포함합니다
#include("chapters/chapter_1.typ")
#include("chapters/chapter_1.typ")

챕터 (Chapter)

// chapter_1.typ

#import "../template.typ": *

_스타일링_ 과 블록이 있는 콘텐츠일 뿐입니다:

#infoblock[일부 정보].

// 문서에 포함하고 싶은 모든 콘텐츠

참고 사항

Typst의 모듈은 스스로 생성했거나 가져온(import) 것만 볼 수 있다는 점에 유의하세요. 그 외의 것은 보이지 않습니다. 그래서 template.typ 파일에 모든 함수를 정의해야 합니다.

즉, 챕터끼리도 서로 볼 수 없으며, 템플릿에 있는 것만 볼 수 있습니다.

순환 참조 (Cyclic imports)

중요: Typst는 순환 참조(cyclic imports)를 금지 합니다. 즉, chapter_1에서 chapter_2를 가져오면서 동시에 chapter_2에서 chapter_1을 가져올 수 없습니다!

하지만 좋은 소식은 언제든지 변수를 가져올 다른 파일을 만들 수 있다는 것입니다.

스크립팅 (Scripting)

Typst는 내부에 완전한 해석형(interpreted) 언어를 포함하고 있습니다. 문서를 더 멋지게 작업하는 핵심 요소 중 하나입니다.

기초 (Basics)

변수 I

_변수(variables)_부터 시작해 보겠습니다.

변수의 개념은 매우 간단합니다. 재사용할 수 있는 어떤 값일 뿐입니다:

#let author = "홍길동"

이것은 #author 가 쓴 책입니다. #author 는 정말 멋진 사람입니다.

#quote(block: true, attribution: author)[
  \<어떤 인용문\>
]
Rendered image

변수 II

변수에는 어떤 Typst 값이라도 저장할 수 있습니다:

#let block_text = block(stroke: red, inset: 1em)[텍스트]

#block_text

#figure(caption: "블록", block_text)
Rendered image

함수 (Functions)

우리는 이미 고급 스타일링 장에서 몇 가지 "사용자 정의" 함수를 보았습니다.

함수는 어떤 값을 받아서 어떤 값을 출력하는 값입니다:

// 이것은 앞서 보았던 구문입니다.
#let f = (name) => "안녕, " + name

#f("세상아!")
Rendered image

대안 구문

같은 내용을 더 짧게 쓸 수 있습니다:

// 다음 구문들은 동일합니다.
#let f = (name) => "안녕, " + name
#let f(name) = "안녕, " + name

#f("세상아!")
Rendered image

중괄호, 대괄호 및 기본값 (Braces, brackets and default)

대괄호 (Square brackets)

대괄호는 그 안의 모든 것을 *콘텐츠(content)*로 변환한다는 것을 기억하실 것입니다.

#let v = [일부 텍스트, _마크업_ 및 다른 #strong[함수들]]
#v
Rendered image

함수 본문에도 동일한 방식을 사용할 수 있습니다:

#let f(name) = [안녕, #name]
#f[세상] // 콘텐츠를 전달하는 데 사용할 수 있다는 점도 잊지 마세요!
Rendered image

중요: 콘텐츠무엇이든 포함할 수 있기 때문에 콘텐츠일반 텍스트 로 변환하는 것은 매우 어렵습니다! 따라서 변수에 콘텐츠를 전달하고 저장할 때 주의하세요.

중괄호 (Braces)

하지만 함수 내부에서 코드를 사용하고 싶은 경우가 많습니다. 그럴 때 {}를 사용합니다:

#let f(name) = {
  // 여기는 코드 모드입니다.

  // 출력의 첫 번째 부분
  "안녕, "

  // 이름이 비어 있는지 확인하고, 비어 있다면
  // 자리 표시자를 삽입합니다.
  if name == "" {
      "무명씨"
  } else {
      name
  }

  // 문장 마무리
  "!"
}

#f("")
#f("철수")
#f("세상")
Rendered image

기본값 (Default values)

방금 우리가 한 일은 "기본값"을 만들어낸 것이었습니다.

기본값은 스타일링에서 매우 흔하기 때문에 이를 위한 특별한 구문이 있습니다:

#let f(name: "무명씨") = [안녕, #name!]

#f()
#f(name: "철수")
#f(name: "세상")
Rendered image

이제 인수가 _명명된 인수(named argument)_가 된 것을 눈치채셨을 것입니다. Typst에서 명명된 인수는 기본값을 가진 인수를 의미합니다.

대괄호와 중괄호를 사용하는 시기

지금까지 본 것처럼 두 가지 방식을 모두 사용할 수 있습니다.

#let f(name) = [
  안녕, #name
]
#f("철수")
Rendered image

그리고

#let f(name) = {
  [안녕,]
  name
}
#f("철수")
Rendered image

그렇다면 언제 무엇을 사용해야 할까요?

답은 간단합니다: 코드가 더 깔끔해 보이는 방식을 선택하면 됩니다.

따라서 다음과 같이 하지 마세요:

// 이렇게 하지 마세요
// 제발요
#let f(inner) = [
  #set align(center)
  #set box(stroke: red)
  #show heading: it => [
    #set text(size: 2em)
    #it
  ]
  #box[#inner]
]
Rendered image

코드 모드를 사용할 때 얼마나 단순화될 수 있는지 보이시나요?

범위 (Scopes)

이것은 꼭 기억해야 할 매우 중요한 사항입니다.

변수는 정의된 범위 밖에서 사용할 수 없습니다(파일 루트에서 정의되어 가져오기(import)를 할 수 있는 경우는 제외). Set 및 show 규칙은 해당 범위 내의 항목에만 영향을 미칩니다.

#{
  let a = 3;
}
// 여기서 "a"를 사용할 수 없습니다.

#[
  #show "": "거짓"

  이것은 참입니다.
]

이것은 참입니다.
Rendered image

반환 (Return)

중요: 기본적으로 중괄호는 중괄호 안으로 "반환되는" 모든 것을 반환합니다. 예를 들어,

#let change_world() = {
  // 세상의 모든 것을 바꾸는 어떤 코드
  str(4e7)
  // 세상을 바꾸는 또 다른 코드
}

#let g() = {
  "하하하, 이제 세상을 바꾸겠다! "
  change_world()
  " 이것이 나의 길고 사악한 독백이다..."
}

#g()
Rendered image

모든 것이 반환되는 것을 피하려면 원하는 것만 명시적으로 반환하세요. 그렇지 않으면 모든 것이 하나의 객체로 결합됩니다:

#let f() = {
  "어떤 긴 텍스트"
  // 엄청난 숫자들
  "2e7"
  return none
}

// 아무것도 반환하지 않음
#f()
Rendered image

타입, 파트 I (Types, part I)

Typst의 각 값은 타입을 가집니다. 타입을 명시할 필요는 없지만, 타입은 중요합니다.

콘텐츠 (content)

참조 링크

이미 살펴본 타입입니다. 문서에 표시되는 내용을 나타내는 타입입니다.

#let c = [이것은 _콘텐츠_ 입니다!]

// c의 타입 확인
#(type(c) == content)

#c

// repr은 값의 "내부 표현"을 보여줍니다.
#repr(c)
Rendered image

중요: 콘텐츠무엇이든 포함할 수 있기 때문에 콘텐츠일반 텍스트 로 변환하는 것은 매우 어렵습니다! 따라서 변수에 콘텐츠를 전달하고 저장할 때 주의하세요.

None (none)

아무것도 없음을 의미합니다. 다른 언어의 null과 같습니다. 표시되지 않으며, 빈 콘텐츠로 변환됩니다.

#none
#repr(none)
Rendered image

문자열 (str)

참조 링크

문자열은 서식이 없는 일반 텍스트만 포함합니다. 그냥 문자들의 나열입니다. 이를 통해 개별 문자를 다룰 수 있습니다:

#let s = "어떤 긴 문자열. 이스케이프 문자가 포함될 수 있습니다: \n,
 줄 바꿈, 그리고 유니코드 코드까지: \u{1251}"
#s \
#type(s) \
`repr`: #repr(s)

#let s = "또 다른 짧은 문자열"
#s.replace("", sym.alpha) \
#s.split(" ") // 공백으로 분할
Rendered image

해당 타입의 생성자를 사용하여 다른 타입을 문자열 표현으로 변환할 수 있습니다(예: 숫자를 문자열로 변환):

#str(5) // 문자열이 되어 문자열처럼 다룰 수 있습니다.
Rendered image

불리언 (bool)

참조 링크

true/false 값입니다. if문 등에서 사용됩니다.

#let b = false
#b \
#repr(b) \
#(true and not true or true) = #((true and (not true)) or true) \
#if (4 > 3) {
  "4는 3보다 큽니다"
}
Rendered image

정수 (int)

참조 링크

정수입니다.

숫자 앞에 0을 쓰고 그 뒤에 x, o, b를 붙여 16진수, 8진수, 2진수로 지정할 수도 있습니다.

#let n = 5
#n \
#(n += 1) \
#n \
#calc.pow(2, n) \
#type(n) \
#repr(n)
Rendered image
#(1 + 2) \
#(2 - 5) \
#(3 + 4 < 8)
Rendered image
#0xff \
#0o10 \
#0b1001
Rendered image

해당 타입의 생성자를 사용하여 값을 정수로 변환할 수 있습니다(예: 문자열을 정수로 변환).

#int(false) \
#int(true) \
#int(2.7) \
#(int("27") + int("4"))
Rendered image

부동 소수점 (float)

참조 링크

정수와 비슷하게 작동하지만 부동 소수점 숫자를 저장할 수 있습니다. 단, 정밀도가 손실될 수 있습니다.

#let n = 5.0

// 부동 소수점과 정수를 섞어 쓸 수 있으며,
// 암시적으로 변환됩니다.
#(n += 1) \
#calc.pow(2, n) \
#(0.2 + 0.1) \
#type(n) 
Rendered image
#3.14 \
#1e4 \
#(10 / 4)
Rendered image

해당 타입의 생성자를 사용하여 값을 부동 소수점으로 변환할 수 있습니다(예: 문자열을 부동 소수점으로 변환).

#float(40%) \
#float("2.7") \
#float("1e5")
Rendered image

타입, 파트 II (Types, part II)

Typst에서 대부분의 것들은 **불변(immutable)**입니다. 콘텐츠를 직접 변경할 수 없으며, 기존 콘텐츠를 사용하여 새로운 콘텐츠를 만들 수 있을 뿐입니다(예: 덧셈 사용).

불변성은 Typst가 _가능한 한 순수 언어(pure language)_가 되고자 하기 때문에 매우 중요합니다. 함수는 값을 반환하는 것 외에 외부의 어떤 것도 바꾸지 않습니다.

하지만 이러한 타입들에 의해 순수성이 부분적으로 "깨집니다". 이들은 매우 유용하며, 이들이 없다면 Typst 작업은 매우 고통스러울 것입니다.

단, 이들을 사용하면 복잡성이 늘어납니다.

배열 (array)

참조 링크

인덱스로 데이터를 저장하는 가변(mutable) 객체입니다.

인덱스 다루기

#let values = (1, 7, 4, -3, 2)

// 인덱스 0의 값 가져오기
#values.at(0) \
// 인덱스 0의 값을 3으로 설정
#(values.at(0) = 3)
// 음수 인덱스 => 뒤에서부터 시작
#values.at(-1) \
// 짝수인 항목의 인덱스 찾기
#values.find(calc.even)
Rendered image

반복 메서드

#let values = (1, 7, 4, -3, 2)

// 홀수만 남기기
#values.filter(calc.odd) \
// 리스트 값들의 절대값으로 새로운 리스트 생성
#values.map(calc.abs) \
// 뒤집기
#values.rev() \
// 배열의 배열을 평탄한 배열로 변환
#(1, (2, 3)).flatten() \
// 문자열 배열을 하나의 문자열로 결합
#(("A", "B", "C")
 .join(", ", last: ""))
Rendered image

리스트 연산

// 리스트 덧셈:
#((1, 2, 3) + (4, 5, 6))

// 리스트 곱셈:
#((1, 2, 3) * 4)
Rendered image

빈 리스트

#() \ // 빈 리스트입니다.
#(1,) \  // 요소가 하나인 리스트입니다.
잘못됨: #(1) // 이것은 리스트가 아니라 그냥 하나의 요소입니다!
Rendered image

딕셔너리 (dict)

참조 링크

딕셔너리는 문자열 "키(key)"와 그 키에 연관된 값을 저장하는 객체입니다.

#let dict = (
  name: "Typst",
  born: 2019,
)

#dict.name \
#(dict.launch = 20)
#dict.len() \
#dict.keys() \
#dict.values() \
#dict.at("born") \
#dict.insert("city", "Berlin ")
#("name" in dict)
Rendered image

빈 딕셔너리

이것은 빈 리스트입니다: #() \
이것은 빈 딕셔너리입니다: #(:)
Rendered image

조건문 및 반복문 (Conditions & loops)

조건문 (Conditions)

공식 문서를 참조하세요.

Typst에서는 if-else문을 사용할 수 있습니다. 이는 특히 함수 본문 내부에서 인수 타입이나 다른 여러 상황에 따라 동작을 변경할 때 유용합니다.

#if 1 < 2 [
  이 내용이 보입니다.
] else [
  이 내용은 보이지 않습니다.
]
Rendered image

물론 else는 필수가 아닙니다:

#let a = 3

#if a < 4 {
  a = 5
}

#a
Rendered image

else if문도 사용할 수 있습니다:

#let a = 5

#if a < 4 {
  a = 5
} else if a < 6 {
  a = -3
}

#a
Rendered image

불리언 (Booleans)

if, else if, else는 스위치 값으로 오직 불리언(boolean) 값만 받습니다. 타입 섹션에서 설명한 대로 불리언을 결합할 수 있습니다:

#let a = 5

#if (a > 1 and a <= 4) or a == 5 [
    `a`가 조건에 부합합니다.
]
Rendered image

Set-if

Typst는 매우 유용한 명령인 set if를 지원합니다. 조건이 충족될 때만 set 규칙을 적용합니다. 이는 문서 전체나 함수 내부의 조건부 스타일링에 매우 유용할 수 있습니다:

#let draft = true

// 여기서 바로 조건 연산을 수행할 수 있습니다.
#set page(columns: 2, width: 20em, height: 10em) if not draft

// show 규칙 내부에서도 사용할 수 있습니다.
#show "TODO": set text(red, size: 2em) if draft

TODO: 실제 텍스트를 작성하세요.

#lorem(50)
Rendered image

반복문 (Loops)

공식 문서를 참조하세요.

반복문에는 whilefor 두 가지 종류가 있습니다. while은 조건이 충족되는 동안 본문을 반복합니다:

#let a = 3

#while a < 100 {
    a *= 2
    str(a)
    " "
}
Rendered image

for는 시퀀스의 모든 요소를 순회합니다. 시퀀스는 array, string 또는 dictionary(for는 딕셔너리의 _키-값 쌍_을 순회함)가 될 수 있습니다.

#for c in "ABC" [
  #c 는 글자입니다.
]
Rendered image

a부터 b까지의 모든 숫자를 순회하려면 range(a, b+1)을 사용하세요:

#let s = 0

#for i in range(3, 6) {
    s += i
    [숫자 #i 가 합계에 더해졌습니다. 현재 합계는 #s 입니다.]
}
Rendered image

range는 마지막 숫자를 제외하므로 위 코드는 다음과 동일합니다:

#let s = 0

#for i in (3, 4, 5) {
    s += i
    [숫자 #i 가 합계에 더해졌습니다. 현재 합계는 #s 입니다.]
}
Rendered image
#let people = (Alice: 3, Bob: 5)

#for (name, value) in people [
    #name#value 개의 사과를 가지고 있습니다.
]
Rendered image

Break 및 continue

반복문 내부에서 breakcontinue 명령을 사용할 수 있습니다. break는 반복문을 중단하고 밖으로 나갑니다. continue는 다음 반복 회차로 건너뜁니다.

다음 예제에서 차이점을 확인해 보세요:

#for letter in "abc nope" {
  if letter == " " {
    // 공백이 있으면 중단
    break
  }

  letter
}
Rendered image
#for letter in "abc nope" {
  if letter == " " {
    // 공백은 건너뜀
    continue
  }

  letter
}
Rendered image

고급 인수 (Advanced arguments)

리스트에서 인수 전개하기 (Spreading arguments from list)

전개 연산자(spreading operator)를 사용하면 값의 리스트를 함수의 인수로 "풀어 놓을(unpack)" 수 있습니다:

#let func(a, b, c, d, e) = [#a #b #c #d #e]
#func(..(([안녕],) * 5))
Rendered image

이것은 표(table)에서 매우 유용할 수 있습니다:

#let a = ("안녕", "b", "c")

#table(columns: 3,
  [테스트], [x], [안녕],
  ..a
)
Rendered image

키 인수 (Key arguments)

동일한 아이디어가 키 인수에도 적용됩니다:

#let text-params = (fill: blue, size: 0.8em)

일부 #text(..text-params)[텍스트].
Rendered image

임의의 인수 관리하기

Typst에서는 원하는 만큼 임의의 위치 인수와 키 인수를 받을 수 있습니다.

이 경우 함수에는 위치 인수와 명명된 인수를 저장하는 특수한 arguments 객체가 주어집니다.

참조 링크

#let f(..args) = [
  #args.pos()\
  #args.named()
]

#f(1, "a", width: 50%, block: false)
Rendered image

이들을 다른 인수와 결합할 수 있습니다. 전개 연산자는 나머지 모든 인수를 "먹어 치울" 것입니다:

#let format(title, ..authors) = {
  let by = authors
    .pos()
    .join(", ", last: "")

  [*#title* \ _작성자: #by;_]
}

#format("ArtosFlow", "Jane", "Joe")
Rendered image

선택적 인수 (Optional argument)

현재 Typst에서 선택적 위치 인수를 만드는 유일한 방법은 arguments 객체를 사용하는 것입니다:

TODO

팁 (Tips)

Typst 스크립팅에는 명확하지 않지만 중요한 요소들이 많이 있습니다. 이 책 전체가 이를 보여주기 위해 만들어졌지만, 그중 일부를 소개합니다.

같음 (Equality)

객체가 완전히 동일하다는 것이 반드시 값의 일치를 의미하지는 않는 다른 객체들과는 달리, Typst에서는 다음과 같습니다:

#let a = 7
#let b = 7.0
#(a == b)
#(type(a) == type(b))
Rendered image

딕셔너리의 경우에는 이것이 덜 명확할 수 있습니다. 딕셔너리에서는 순서가 중요할 수 있으므로, 값이 같다고 해서 반드시 정확히 똑같이 동작하는 것은 아닙니다:

#let a = (x: 1, y: 2)
#let b = (y: 2, x: 1)
#(a == b)
#(a.pairs() == b.pairs())
Rendered image

딕셔너리에 키가 있는지 확인

Python에서처럼 in 키워드를 사용하세요:

#let dict = (a: 1, b: 2)

#("a" in dict)
// 다음과 동일한 결과를 줍니다.
#(dict.keys().contains("a"))
Rendered image

이것은 리스트에서도 작동한다는 점에 유의하세요:

#("a" in ("b", "c", "a"))
#(("b", "c", "a").contains("a"))
Rendered image

상태와 쿼리 (States & Query)

이 섹션은 완전하지 않을 수 있으며 최신 Typst 버전에 맞춰 충분히 업데이트되지 않았을 수 있습니다. 모든 기여를 환영합니다!

Typst는 가능한 한 _순수 언어(pure language)_가 되고자 합니다.

즉, 함수는 함수 외부의 어떤 것도 바꿀 수 없습니다. 또한 함수를 호출했을 때 결과가 항상 동일해야 함을 의미합니다.

불행히도 우리가 사는 세상(그리고 우리가 만드는 문서)은 순수하지 않습니다. 2번 제목을 만들었다면 다음 번호는 3번이 되기를 원할 것입니다.

이 섹션에서는 이러한 순수하지 않은 Typst의 기능을 사용하는 방법을 안내합니다. 너무 남용하지 마세요. 이 지식은 Typst의 어둠의 마법(Dark Arts)에 가깝기 때문입니다!

상태 (States)

이 섹션은 완전하지 않을 수 있으며 최신 Typst 버전에 맞춰 충분히 업데이트되지 않았을 수 있습니다. 모든 기여를 환영합니다!

실용적인 내용을 시작하기 전에, 일반적인 상태(state)의 개념을 이해하는 것이 중요합니다.

여기 왜 상태가 필요한지 에 대한 좋은 설명이 있습니다: 상태에 관한 공식 참조 문서. 먼저 읽어보시는 것을 강력히 추천합니다.

다음과 같이 작성하는 대신:

#let x = 0
#let compute(expr) = {
  // eval은 문자열을 Typst 코드로 해석하여
  // 새로운 x 값을 계산합니다.
  x = eval(
    expr.replace("x", str(x))
  )
  [새로운 값은 #x 입니다.]
}

#compute("10") \
#compute("x + 3") \
#compute("x * 2") \
#compute("x - 5")

이 코드는 컴파일되지 않습니다: 함수 외부의 변수는 읽기 전용이며 수정할 수 없습니다.

대신 다음과 같이 작성해야 합니다:

#let s = state("x", 0)
#let compute(expr) = [
  // 이 함수로 x의 현재 상태를 업데이트합니다.
  #s.update(x =>
    eval(expr.replace("x", str(x)))
  )
  // 그리고 이를 표시합니다.
  새로운 값은 #context s.get() 입니다.
]

#compute("10") \
#compute("x + 3") \
#compute("x * 2") \
#compute("x - 5")

계산은 문서에 _위치한 순서_대로 이루어집니다. 따라서 계산을 먼저 만들었지만 문서의 나중에 배치한다면... 직접 확인해 보세요:

#let more = [
  #compute("x * 2") \
  #compute("x - 5")
]

#compute("10") \
#compute("x + 3") \
#more
Rendered image

컨텍스트의 마법 (Context magic)

그렇다면 이 마법 같은 context s.get()은 무엇을 의미할까요?

참조 문서의 컨텍스트(Context)

요약하자면, 코드(또는 마크업)의 어느 부분이 _외부 상태에 의존할 수 있는지_를 지정합니다. 이 컨텍스트 표현식은 하나의 객체로 묶여 레이아웃 단계에서 평가됩니다.

즉, "일반" 코드에서는 context 내부에 무엇이 있는지 들여다볼 수 없습니다. 이것은 문서에 배치된 후에만 그 내용을 알 수 있는 블랙박스와 같습니다.

context 기능에 대해서는 나중에 더 자세히 다루겠습니다.

상태 연산

새로운 상태 생성

#let x = state("state-id")
#let y = state("state-id", 2)

#x, #y

상태는 #context x.get() 입니다. \ // 다음과 동일합니다.
#context [상태는 #y.get() 입니다.] \ // 다음과 동일합니다.
#context {"상태는 " + str(y.get()) + " 입니다."}
Rendered image

업데이트 (Update)

업데이트는 하나의 지침인 콘텐츠 입니다. 이 지침은 컴파일러에게 문서의 이 위치에서 상태가 업데이트되어야 함 을 알려줍니다.

#let x = state("x", 0)
#context x.get() \
#let _ = x.update(3)
// 아무 일도 일어나지 않습니다. `update`를 문서 흐름에 넣지 않았기 때문입니다.
#context x.get()

#repr(x.update(3)) // 해당 콘텐츠가 어떻게 보이는지 확인해 보세요. \

#context x.update(3)
#context x.get() // 드디어 업데이트되었습니다!
Rendered image

여기서 context의 중요한 특징 중 하나를 볼 수 있습니다: 컨텍스트는 외부의 상태를 "볼" 수 있지만, 컨텍스트 내부에서 상태가 어떻게 변하는지는 볼 수 없습니다.

#let x = state("x", 0)

#context {
  x.update(3)
  str(x.get())
}
Rendered image

ID 충돌

요약: 절대로 상태 ID가 충돌하게 하지 마세요.

상태는 ID로 식별되며, ID가 같으면 코드가 제대로 작동하지 않습니다.

따라서 여러 번 사용되는 함수나 루프를 작성할 때는 주의 해야 합니다!

#let f(x) = {
  // 새로운 상태를 반환하지만...
  // ...ID가 같습니다!
  // 따라서 항상 동일한 상태가 됩니다!
  let y = state("x", 0)
  y.update(y => y + x)
  context y.get()
}

#let a = f(2)
#let b = f(3)

#a, #b \
#raw(repr(a) + "\n" + repr(b))
Rendered image

하지만 다음과 같은 경우는 괜찮아 보일 수 있습니다:

// 코드에서의 위치가 다릅니다!
#let x = state("state-id")
#let y = state("state-id", 2)

#x, #y
Rendered image

그러나 사실은 괜찮지 않습니다:

#let x = state("state-id")
#let y = state("state-id", 2)

#context [#x.get(); #y.get()]

#x.update(3)

#context [#x.get(); #y.get()]
Rendered image

카운터 (Counters)

이 섹션은 완전하지 않을 수 있으며 최신 Typst 버전에 맞춰 충분히 업데이트되지 않았을 수 있습니다. 모든 기여를 환영합니다!

카운터는 특정 유형의 요소의 개수를 세는 특수한 상태입니다. 상태와 마찬가지로 식별자 문자열을 사용하여 직접 만들 수 있습니다.

중요: 요소의 카운터를 활성화하려면 해당 요소에 _번호 매기기(numbering)를 설정_해야 합니다.

상태 메서드

카운터는 상태의 일종이므로 상태가 할 수 있는 모든 것을 할 수 있습니다. 특히 context와 관련된 모든 사항이 그대로 적용됩니다.

#set heading(numbering: "1.")

= 배경
#counter(heading).update(3)
#counter(heading).update(n => n * 2)

== 분석
현재 제목 번호: #context counter(heading).get()

패턴을 사용해 예쁘게 렌더링할 수도 있습니다: #context counter(heading).display("I: 1.")

또는 현재 설정된 스타일을 사용합니다: #context counter(heading).display()

이는 현재 설정된 스타일에 따라 달라집니다:

#set heading(numbering: ":1:1:")
#context counter(heading).display()
Rendered image

몇 가지 예제를 더 보겠습니다. 매우 간단하므로 별도의 설명은 필요 없을 것 같네요. :)

#let mine = counter("mycounter")
#context mine.display()

#mine.step()
#context mine.display()

#mine.update(c => c * 3)
#context mine.display()
Rendered image

카운터는 기본적으로 현재 값과 최종 값을 모두 표시하는 기능을 지원합니다. both: true 옵션이 필요합니다:

#set heading(numbering: "1.")

= 서론
내용이 들어갑니다.

#context counter(heading).display(both: true) \
#context counter(heading).display("1 / 1", both: true) \
#context counter(heading).display(
  (num, max) => [#num / #max],
   both: true
)

= 배경
현재 값은 다음과 같습니다: #context counter(heading).display()
Rendered image

Step

매우 쉽습니다. 카운터의 경우 step을 사용하여 값을 증가시킬 수 있습니다. update와 동일한 방식으로 작동합니다.

#set heading(numbering: "1.")

= 서론
#context counter(heading).step()

= 분석
3.1은 건너뜁시다.
#context counter(heading).step(level: 2)

== 분석
현재 위치: #context counter(heading).display()
Rendered image

함수에서 카운터 사용하기:

#let c = counter("theorem")
#let theorem(it) = block[
  #c.step()
  *정리 #context c.display():*
  #it
]

#theorem[$1 = 1$]
#theorem[$2 < 3$]
Rendered image

스타일링을 위한 컨텍스트 (Context for styling)

이 섹션은 완전하지 않을 수 있으며 최신 Typst 버전에 맞춰 충분히 업데이트되지 않았을 수 있습니다. 모든 기여를 환영합니다!

참조 문서의 컨텍스트(Context)

(아직 상태(state) 섹션을 읽지 않았다면 먼저 읽어보세요. 컨텍스트에 대한 논의는 거기서부터 시작됩니다.)

상태 장에서 이미 보았듯이, context는 외부 상태에 크게 의존할 수 있는 콘텐츠의 "레이아웃 지침"을 저장하는 객체의 일종입니다. 이 지침은 나중에 렌더링됩니다.

여기서 중요한 점은 제가 언급한 "외부 상태"에 상태(및 개수를 세기 위한 특수 상태인 카운터)뿐만 아니라 스타일링 도 포함된다는 것입니다.

무슨 뜻일까요?

직접 확인해 보세요:

현재 스타일 가져오기

현재 글꼴: #context text.font
Rendered image

아주 쉽게 현재 글꼴을 가져왔습니다. 이는 기본적으로 **모든 설정 가능한 속성(settable property)**에 대해 작동합니다! 정말 멋지지 않나요?

컨텍스트의 특징을 더 잘 보여주는 다른 예제를 보겠습니다. 항상 텍스트 색상과 동일한 색상을 가지는 박스를 만들어 보겠습니다:

#let colorful-rect = context box(stroke: text.fill, inset: 0.3em)[#repr(text.fill)]

동일한 색상의 박스 안에 있는 현재 색상: #colorful-rect.

#set text(red)

동일한 색상의 박스 안에 있는 현재 색상: #colorful-rect.
Rendered image

컨텍스트 외부로 값을 가져오는 방법

놀라운 사실은, _가져올 수 없다_는 것입니다!

왜일까요? 이유는 간단합니다. Typst에게 context 블록은 문서 내부에 배치되어 렌더링되는 동안에만 열 수 있는 블랙박스이기 때문입니다.

따라서 무언가를 얻고 싶다면 반드시 _context 내부_에서 처리해야 합니다.

함수 작성하기

중요한 사실: 함수는 다른 콘텐츠와 마찬가지로 별도의 선언 없이도 _컨텍스트에 의존_할 수 있습니다. 그리고 일반적으로 함수 내부에 context를 넣는 것보다 사용자가 직접 함수를 컨텍스트로 감싸도록 하는 것이 더 좋습니다.

스타일(또는 상태)에 의존하는 목록을 만들고 싶다고 가정해 봅시다. 컨텍스트가 필요하므로 다음과 같이 컨텍스트로 감쌀 수 있습니다:

(나쁜 예)

#let page-dimensions = context (page.width, page.height)
#page-dimensions, 이 객체의 표현(representation)은 다음과 같습니다: #repr(page-dimensions)
Rendered image

이 객체는 거의 쓸모가 없습니다. 블랙박스이므로 문서에 넣는 것 외에는 할 수 있는 것이 없기 때문입니다.

하지만 대신 다음과 같이 할 수 있습니다:

(좋은 예!)

// 컨텍스트 의존적 함수가 되려면 단순한 고정 콘텐츠가 아니라 함수여야 합니다.
#let page-dimensions() = (page.width, page.height)

#context page-dimensions()

#context [
    #let (x, y) = page-dimensions()
    너비의 절반은 #(x/2) 이고, 높이는 #y 입니다.
]
Rendered image

이렇게 컨텍스트 의존적인 함수를 사용하면 사용자가 원하는 어디든 context를 배치할 수 있습니다.

컨텍스트 내부의 규칙

이미 논의했듯이, context는 문서의 외부 상태를 캡처하며 내부에서 일어나는 일은 보지 못합니다. 따라서 다음과 같이 하면

#context [
    텍스트 색상: #text.fill

    #set text(blue)

    텍스트 색상: #text.fill
]
Rendered image

...맞습니다. 내부의 규칙은 컨텍스트 내부의 스타일에 영향을 주지 않습니다.

측정 및 레이아웃 (Measure, Layout)

이 섹션은 오래되었습니다. 여전히 유용할 수 있지만, 참조 문서를 통해 새로운 컨텍스트 시스템을 공부하는 것을 강력히 추천합니다.

스타일과 측정 (Style & Measure)

스타일 공식 문서

측정 공식 문서

measure요소의 크기 를 반환합니다. 이 명령은 place를 사용하여 사용자 정의 레이아웃을 만들 때 매우 유용합니다.

하지만 한 가지 주의할 점이 있습니다. 요소의 크기는 해당 요소에 적용된 스타일에 따라 달라집니다.

#let content = [안녕!]
#content
#set text(14pt)
#content
Rendered image

따라서 텍스트의 일부에 큰 글꼴 크기를 설정한 경우, 요소의 크기를 측정하려면 요소가 어디에 위치하는지 알아야 합니다. 이를 모르면 어떤 스타일을 적용해야 할지 알 수 없습니다.

네, 맞습니다. 우리에게는 context가 필요합니다.

#let thing(body) = context {
  let size = measure(body)
  ["#body" 의 너비는 #size.width 입니다.]
}

#thing[이봐요] \
#thing[환영합니다]
Rendered image

레이아웃 (Layout)

레이아웃은 measure와 비슷하지만, 현재 범위의 부모 크기 를 반환합니다.

요소를 블록(block)에 넣는다면 블록의 크기가 될 것이고, 페이지에 바로 넣는다면 페이지의 크기가 될 것입니다.

기술적인 이유로 context를 직접 사용할 수는 없으며, 매우 유사한 방식을 사용해야 합니다 (사실 context가 여기서 파생된 방식입니다):

/// 부모 크기를 받아서 무언가를 렌더링하는 블랙박스입니다.
#layout(size => {
  let half = 50% * size.width
  [페이지의 절반 너비는 #half 입니다.]
})
Rendered image

layoutmeasure를 결합하여 부모 크기에 의존하는 요소의 너비를 구하는 것은 매우 유용할 수 있습니다:

#let text = lorem(30)
#layout(size => context [
  #let (height,) = measure(
    block(width: size.width, text)
  )
  이 텍스트의 높이는 현재 페이지 너비에서 #height 입니다: \
  #text
])
Rendered image

쿼리 (Query)

이 섹션은 완전하지 않을 수 있으며 최신 Typst 버전에 맞춰 충분히 업데이트되지 않았을 수 있습니다. 모든 기여를 환영합니다!

공식 참조 링크

쿼리(Query)는 선택자(selector) (show 규칙에서 사용한 것과 동일)를 통해 위치(location) (문서 내의 실제 위치를 나타내는 객체, 문서 참조)를 가져올 수 있게 해줍니다.

이를 통해 문서의 일부에서 문서 전체에 대한 정보를 얻는 등 "시간 여행"이 가능해집니다. 이는 Typst의 순수성(purity)을 우회하는 방법입니다.

이것은 현재 Typst에 존재하는 가장 강력한 어둠의 마법 중 하나입니다. 큰 힘에는 큰 책임이 따릅니다.

시간 여행 (Time travel)

#let s = state("x", 0)
#let compute(expr) = [
  #s.update(x =>
    eval(expr.replace("x", str(x)))
  )
  새로운 값은 #context s.get() 입니다.
]

`<here>` 지점에서의 값은 다음과 같습니다:
#context s.at(
  query(<here>)
    .first()
    .location()
)

#compute("10") \
#compute("x + 3") \
*여기.* <here> \
#compute("x * 2") \
#compute("x - 5")
Rendered image

가장 가까운 장(Chapter) 가져오기

#set page(header: context {
  let elems = query(
    selector(heading).before(here())
  )
  let academy = smallcaps[
    Typst Academy
  ]
  if elems == () {
    align(right, academy)
  } else {
    let body = elems.last().body
    academy + h(1fr) + emph(body)
  }
})

= 서론
#lorem(23)

= 배경
#lorem(30)

= 분석
#lorem(15)
Rendered image

메타데이터 (Metadata)

메타데이터는 쿼리나 다른 콘텐츠를 사용하여 추출할 수 있는 보이지 않는 콘텐츠입니다. 이는 외부 도구에 값을 전달하기 위해 typst query를 사용할 때 매우 유용할 수 있습니다.

// 어딘가에 메타데이터를 배치합니다.
#metadata("이것은 메모입니다") <note>

// 그리고 다른 어디서든 이를 찾습니다.
#context {
  query(<note>).first().value
}
Rendered image

수학 (Math)

수학은 ...수학과 관련된 특별한 기능을 가진 특별한 환경입니다.

구문 (Syntax)

수학 환경을 시작하려면 $를 사용합니다. $ 주변의 공백 여부에 따라 인라인(inline) 수식(더 작게 표시되어 텍스트 내에서 사용됨) 또는 디스플레이(display) 수식(수식 자체로 독립된 줄에 표시됨)이 됩니다.

// 이것은 인라인 수식입니다.
$a$, $b$, $c$를 직각삼각형의
변의 길이라 하자.
그러면 다음을 알 수 있습니다:

// 이것은 디스플레이 수식입니다.
$ a^2 + b^2 = c^2 $

수학적 귀납법으로 증명하기:

// 공백으로 줄 바꿈을 사용할 수도 있습니다!
$
sum_(k=1)^n k = (n(n+1)) / 2
$
Rendered image

Math.equation

수학이 표시되는 요소를 math.equation이라고 합니다. 이를 set/show 규칙에 사용할 수 있습니다:

#show math.equation: set text(red)

$
integral_0^oo (f(t) + g(t))/2
$
Rendered image

수학 환경에서 사용할 수 있는 모든 기호/명령어는 코드 모드에서도 math.명령어를 사용하여 _사용 가능_합니다:

#math.integral, #math.underbrace([a + b], [c])
Rendered image

문자와 명령어

Typst는 수학을 위해 가능한 한 간단하고 효율적인 구문을 목표로 합니다. 즉, 특별한 기호 없이 명령어만 사용합니다.

요약하자면, Typst는 몇 가지 간단한 규칙을 사용합니다:

  • 모든 단일 문자 단어는 변수 가 됩니다. 여기에는 모든 유니코드 기호 도 포함됩니다!

  • 모든 다중 문자 단어는 명령어 가 됩니다. 내장 명령어(수학 환경 밖에서 math.something으로 사용 가능)일 수도 있고, 사용자가 정의한 변수/함수일 수도 있습니다. 명령어가 정의되지 않은 경우, 컴파일 오류가 발생합니다.

    수학에서 사용하려는 변수에 kebab-case나 snake_case를 사용한다면, 해당 변수를 호출할 때 #snake-case-variable 과 같이 사용해야 합니다.
  • 단순한 텍스트를 쓰려면 따옴표를 사용하세요:

    $a "" 2 "와 같다"$
    Rendered image
    여기서는 공백이 중요합니다!
    $a "is" 2$, $a"is"2$
    Rendered image
  • italic을 사용하여 다중 문자 변수로 만들 수 있습니다:

    $(italic("mass") v^2)/2$
    Rendered image

명령어는 여기를 참조하세요(링크를 따라가면 명령어를 볼 수 있습니다).

모든 기호는 여기를 참조하세요.

여러 줄 수식

여러 줄 _디스플레이 수식_을 만들려면 마크업 모드와 동일한 기호인 \를 사용합니다:

$
a = b\
a = c
$
Rendered image

이스케이프 (Escaping)

사용되는 모든 기호는 마크업 모드에서처럼 \로 이스케이프할 수 있습니다. 예를 들어, 분수 기능을 비활성화할 수 있습니다:

$
a  / b \
a \/ b
$
Rendered image

다른 모든 구문에도 동일한 방식으로 작동합니다.

인라인 수식 감싸기

긴 수식을 작성할 때 텍스트와 너무 가까워질 수 있습니다(특히 긴 꼬리를 가진 문자들의 경우).

#lorem(17) $display(1)/display(1+x^n)$ #lorem(20)
Rendered image

상자(box)로 감싸서 간격을 쉽게 늘릴 수 있습니다:

#lorem(17) #box($display(1)/display(1+x^n)$, inset: 0.2em) #lorem(20)
Rendered image

기호 (Symbols)

수학에서 다중 문자 단어는 지역 변수, 함수, 텍스트 연산자, 간격 또는 _특수 기호_를 나타냅니다. 후자는 고급 수학에서 매우 중요합니다.

$
forall v, w in V, alpha in KK: alpha dot (v + w) = alpha v + alpha w
$
Rendered image

유니코드를 사용하여 똑같이 작성할 수 있습니다:

$
v, wV, α𝕂: α(v + w) = α v + α w
$
Rendered image

기호 명명법 (Symbols naming)

사용 가능한 모든 기호 목록은 여기를 참조하세요.

일반적인 아이디어

Typst는 기억하기 쉬운 짧은 단어로 일부 "기본" 기호를 정의하고, 이를 조합하여 복잡한 기호를 만듭니다. 예를 들어:

$
// cont — contour (폐곡선)
integral, integral.cont, integral.double, integral.square, sum.integral\

// lt — less than (미만), gt — greater than (초과)
lt, lt.circle, lt.eq, lt.not, lt.eq.not, lt.tri, lt.tri.eq, lt.tri.eq.not, gt, lt.gt.eq, lt.gt.not
$
Rendered image

복잡한 기호가 많이 포함된 수학을 작성할 때는 WebApp이나 Typst LSP를 사용하는 것을 적극 권장합니다. 다양한 조합 중에서 올바른 기호를 빠르게 선택하는 데 도움이 됩니다.

가끔 이름이 직관적이지 않은 경우가 있는데, 예를 들어 not 대신 접두사 n-을 사용하는 경우가 있습니다:

$
gt.nequiv, gt.napprox, gt.ntilde, gt.tilde.not
$
Rendered image

일반적인 수정자 (Modifiers)

  • .b, .t, .l, .r: bottom, top, left, right. 기호의 방향을 바꿉니다.

    $arrow.b, triangle.r, angle.l$

    Rendered image
  • .bl, tr: bottom-left, top-right 등. 대각선 방향이 가능한 경우 사용합니다.

  • .bar, .circle, .times, ...: 기호에 해당 요소를 추가합니다.

  • .double, .triple, .quad: 기호를 2, 3, 4번 결합합니다.

  • .not: 기호에 사선을 긋습니다.

  • .cw, .ccw: clock-wise(시계 방향) 및 counter-clock-wise(반시계 방향). 화살표 등에 사용됩니다.

  • .big, .small: 크기를 조절합니다.

    $plus.circle.big plus.circle, times.circle.big plus.circle$

    Rendered image
  • .filled: 기호 내부를 채웁니다.

    $square, square.filled, diamond.filled, arrow.filled$

    Rendered image

그리스 문자

소문자는 소문자로 시작하고, 대문자는 대문자로 시작합니다.

다른 형태의 글자를 사용하려면 .alt를 붙입니다.

$
alpha, Alpha, beta, Beta, beta.alt, gamma, pi, Pi,\
pi.alt, phi, phi.alt, Phi, omicron, kappa, kappa.alt, Psi,\
theta, theta.alt, xi, zeta, rho, rho.alt, kai, Kai,
$
Rendered image

칠판 볼드체 (Blackboard letters)

글자를 두 번 겹쳐 쓰세요. 다른 기호를 칠판 볼드체로 만들려면 bb를 사용합니다:

$bb(A), AA, bb(1)$
Rendered image

글꼴 문제 (Fonts issues)

기본 글꼴은 New Computer Modern Math입니다. 좋은 글꼴이지만 몇 가지 불일치가 있을 수 있습니다.

Typst는 기호 이름을 유니코드에 매핑하므로, 글꼴에 잘못된 기호가 있는 경우 Typst는 잘못된 기호를 표시합니다.

공집합 (Empty set)

예시를 확인하세요:

// 기본 수학 글꼴의 nothing 기호는 좋지 않습니다.
$nothing, nothing.rev, diameter$

#show math.equation: set text(font: "Fira Math")

// Fira math가 더 일관성이 있습니다.
$nothing, nothing.rev, diameter$
Rendered image

하지만 글꼴 기능(font feature)으로 이를 수정할 수 있습니다:

#show math.equation: set text(features: ("cv01",))

$nothing, nothing.rev, diameter$
Rendered image

또는 간단히 "show" 규칙을 사용할 수도 있습니다:

#show math.nothing: math.diameter

$nothing, nothing.rev, diameter$
Rendered image

그룹화 (Grouping)

모든 그룹화는 (현재) 괄호로 수행할 수 있습니다. 따라서 괄호는 "실제" 괄호일 수도 있고 그룹화를 위한 괄호일 수도 있습니다.

예를 들어, 다음 괄호들은 분수의 분자를 지정합니다:

$ (a^2 + b^2)/2 $
Rendered image

좌우 (Left-right)

공식 문서를 참조하세요.

종류에 상관없이 두 개의 일치하는 괄호가 있으면 lr(left-right) 그룹으로 묶입니다.

$
{[((a + b)/2) + 1]_0}
$
Rendered image

이스케이프를 사용하여 이 기능을 비활성화할 수 있습니다.

또한 lr을 직접 사용하여 모든 종류의 괄호를 일치시킬 수도 있습니다:

$
lr([a/2, b)) \
lr([a/2, b), size: #150%)
$
Rendered image

펜스 (Fences)

펜스(Fences)는 오탐지가 많기 때문에 자동으로 짝을 맞추지 않습니다.

absnorm을 사용하여 짝을 맞출 수 있습니다:

$
abs(a + b), norm(a + b), floor(a + b), ceil(a + b), round(a + b)
$
Rendered image

정렬 (Alignment)

일반적인 정렬

기본적으로 디스플레이 수식은 중앙 정렬되지만, show 규칙으로 설정할 수 있습니다:

#show math.equation: set align(right)

$
(a + b)/2
$
Rendered image

또는 align 요소를 사용합니다:

#align(left, block($ x = 5 $))
Rendered image

정렬 지점 (Alignment points)

수식에 여러 개의 정렬 지점(&)이 포함되어 있으면, 우측 정렬 열과 좌측 정렬 열이 교대로 나타나는 블록이 생성됩니다.

아래 예시에서 (3x + y) / 7 표현식은 _우측 정렬_되고, = 9는 _좌측 정렬_됩니다.

$ (3x + y) / 7 &= 9 && "주어짐" \
  3x + y &= 63 & "7을 곱함" \
  3x &= 63 - y && "y를 뺌" \
  x &= 21 - y/3 & "3으로 나눔" $
Rendered image

"주어짐"이라는 단어 역시 좌측 정렬되는데, 이는 &&가 한 줄에 두 개의 정렬 지점을 만들어 정렬을 두 번 교차시키기 때문입니다.

& &&&는 정확히 똑같이 동작합니다. 한편, "7을 곱함"은 바로 앞에 &가 하나만 있으므로 좌측 정렬됩니다.

각 정렬 지점은 단순히 우측 정렬과 좌측 정렬을 번갈아 가며 전환합니다.

극한 설정 (Setting limits)

가끔 기본적으로 기호가 붙는 방식을 바꾸고 싶을 때가 있습니다.

극한 (Limits)

예를 들어, 많은 국가에서 정적분을 작성할 때 적분 기호 아래와 위에 극한을 씁니다. 이를 설정하려면 limits 함수를 사용하세요:

$
integral_a^b\
limits(integral)_a^b
$
Rendered image

show 규칙을 사용하여 이를 기본값으로 설정할 수 있습니다:

#show math.integral: math.limits

$
integral_a^b
$

이것은 인라인 수식입니다: $integral_a^b$
Rendered image

디스플레이 모드 전용

이 설정은 인라인 수식에도 영향을 준다는 점에 유의하세요. 디스플레이 수식에만 극한을 활성화하려면 limits(inline: false)를 사용하세요:

#show math.integral: math.limits.with(inline: false)

$
integral_a^b
$

이것은 인라인 수식입니다: $integral_a^b$.
Rendered image

물론, 다시 아래쪽 첨자로 옮기는 것도 가능합니다:

$
sum_a^b, scripts(sum)_a^b
$
Rendered image

연산 (Operations)

동일한 방식이 연산에도 적용됩니다. 기본적으로 연산 기호의 아래와 위에 붙습니다:

$a =_"보조정리 1에 의해" b, a scripts(=)_+ b$
Rendered image

연산자 (Operators)

참조 링크

Typst 수학 환경에는 수많은 내장 "텍스트 연산자"가 있습니다. 이들은 일반 텍스트와 매우 비슷하게 동작하지만, 분명히 다릅니다:

$
lim x_n, "lim" x_n, "lim"x_n
$
Rendered image

사전 정의된 연산자

Typst에 내장된 모든 텍스트 연산자는 다음과 같습니다:

$
arccos, arcsin, arctan, arg, cos, cosh, cot, coth, csc,\
csch, ctg, deg, det, dim, exp, gcd, hom, id, im, inf, ker,\
lg, lim, liminf, limsup, ln, log, max, min, mod, Pr, sec,\
sech, sin, sinc, sinh, sup, tan, tanh, tg "" tr
$
Rendered image

사용자 정의 연산자 만들기

물론 목록에 없는 텍스트 연산자가 필요한 경우도 있을 것입니다.

걱정하지 마세요. 직접 추가하는 것은 매우 쉽습니다:

#let arcsinh = math.op("arcsinh")

$
arcsinh x
$
Rendered image

연산자의 극한 (Limits for operators)

연산자(적절한 간격을 가진 직립 텍스트)를 만들 때, 동시에 _디스플레이 모드_를 위한 극한을 설정할 수 있습니다:

$
op("liminf")_a, op("liminf", limits: #true)_a
$
Rendered image

이것은 대략 다음과 동일합니다.

$
limits(op("liminf"))_a
$
Rendered image

모든 것을 조합하여 새로운 연산자를 만들 수 있습니다:

#let liminf = math.op(math.underline(math.lim), limits: true)
#let limsup = math.op(math.overline(math.lim), limits: true)
#let integrate = math.op($integral dif x$)

$
liminf_(x->oo)\
limsup_(x->oo)\
integrate x^2
$
Rendered image

위치 및 크기 (Location and sizes)

우리는 이미 디스플레이 수식과 인라인 수식에 대해 이야기했습니다. 이들은 정렬과 간격뿐만 아니라 크기와 스타일에서도 차이가 납니다:

인라인: $a/(b + 1/c), sum_(n=0)^3 x_n$

$
a/(b + 1/c), sum_(n=0)^3 x_n
$
Rendered image

현재 환경의 크기와 스타일은 수학 크기(Math Size)로 설명됩니다. 참조를 확인하세요.

네 가지 크기가 있습니다:

  • 디스플레이 수식 크기 (display)
  • 인라인 수식 크기 (inline)
  • 스크립트 수식 크기 (script)
  • 하위/상위 스크립트 수식 크기 (sscript)

분수, 스크립트 또는 지수에서 항목이 사용될 때마다 "몇 단계 아래"로 이동하여 더 작아집니다. sscript는 그 이상 줄어들지 않습니다:

$
"display:" 1/("inline:" a + 1/("script:" b + 1/("sscript:" c + 1/("sscript:" d + 1/("sscript:" e + 1/f)))))
$
Rendered image

수동으로 크기 설정하기

해당 명령어를 사용하면 됩니다:

인라인: $sum_0^oo e^x^a$\
극한이 있는 인라인: $limits(sum)_0^oo e^x^a$\
인라인이지만 디스플레이처럼 표시: $display(sum_0^oo e^x^a)$
Rendered image

벡터, 행렬, 세미콜론 문법

벡터 (Vectors)

여기서 벡터는 열(column)을 의미합니다.
문자에 화살표 표기법을 쓰려면 $arrow(v)$를 사용하세요.
#let arr = math.arrow와 같이 단축키를 만드는 것을 추천합니다.

열을 쓰려면 vec 명령어를 사용하세요:

$
vec(a, b, c) + vec(1, 2, 3) = vec(a + 1, b + 2, c + 3)
$
Rendered image

구분자 (Delimiter)

열 주변의 괄호를 변경하거나 제거할 수도 있습니다:

$
vec(1, 2, 3, delim: "{") \
vec(1, 2, 3, delim: bar.double) \
vec(1, 2, 3, delim: #none)
$
Rendered image

간격 (Gap)

행 사이의 간격 크기를 변경할 수 있습니다:

$
vec(a, b, c)
vec(a, b, c, gap:#0em)
vec(a, b, c, gap:#1em)
$
Rendered image

간격 일정하게 만들기

벡터마다 간격이 반드시 일정하거나 같지 않다는 것을 쉽게 알 수 있습니다:

$
vec(a/b, a/b, a/b) = vec(1, 1, 1)
$
Rendered image

이는 gap이 요소의 중심 간 거리가 아니라 요소 _사이의 간격_을 의미하기 때문에 발생합니다.

이를 해결하려면 이 스니펫을 사용할 수 있습니다.

행렬 (Matrix)

공식 참조를 확인하세요.

행렬은 vec과 매우 유사하지만, ;로 구분된 행을 받습니다:

$
mat(
    1, 2, ..., 10;
    2, 2, ..., 10;
    dots.v, dots.v, dots.down, dots.v;
    10, 10, ..., 10; // 끝에 있는 `;`는 선택 사항입니다
)
$
Rendered image

구분자와 간격

벡터와 같은 방식으로 지정할 수 있습니다.

인수는 내용 앞이나 세미콜론 뒤에 지정하세요. 세미콜론이 없으면 코드가 패닉 상태가 됩니다!
$
mat(
    delim: "|",
    1, 2, ..., 10;
    2, 2, ..., 10;
    dots.v, dots.v, dots.down, dots.v;
    10, 10, ..., 10;
    gap: #0.3em
)
$
Rendered image

세미콜론 문법

세미콜론을 사용할 때, 세미콜론 사이의 인수는 배열로 병합됩니다. 직접 확인해 보세요:

#let fun(..args) = {
    args.pos()
}

$
fun(1, 2;3, 4; 6, ; 8)
$
Rendered image

일부 요소를 빠뜨리면 none으로 대체됩니다.

세미콜론 문법과 명명된 인수를 섞어 쓸 수 있지만 주의하세요!

#let fun(..args) = {
    repr(args.pos())
    repr(args.named())
}

$
fun(1, 2; gap: #3em, 4)
$
Rendered image

예를 들어, 이것은 작동하지 않습니다:

$
//         ↓ `;`가 없으므로 (gap:)을 배열에 추가하려고 시도합니다.
mat(1, 2; 4, gap: #3em)
$

클래스 (Classes)

공식 문서를 참조하세요.

각 수학 기호는 고유한 "클래스", 즉 동작 방식을 가지고 있습니다. 이것이 기호들이 서로 다르게 배치되는 주요 이유 중 하나입니다.

클래스 종류

$
a b c\
a class("normal", b) c\
a class("punctuation", b) c\
a class("opening", b) c\
a lr(b c]) c\
a lr(class("opening", b) c ]) c\ // 수직으로 이동된 것에 주목하세요.
a class("closing", b) c\
a class("fence", b) c\
a class("large", b) c\
a class("relation", b) c\
a class("unary", b) c\
a class("binary", b) c\
a class("vary", b) c\
$
Rendered image

기호의 클래스 설정하기

기본값:

$square circle square$

`#h(0)` 사용:

$square #h(0pt) circle #h(0pt) square$

`math.class` 사용:

#show math.circle: math.class.with("normal")
$square circle square$
Rendered image

특수 기호 (Special symbols)

중요: 저는 특수 기호에 능숙하지 않으므로, 추가나 수정 사항이 있다면 감사히 받겠습니다.

Typst는 유니코드 를 아주 잘 지원합니다. 이는 특수 기호 또한 지원함을 의미합니다. 조판에 매우 유용할 수 있습니다.

대부분의 경우 이러한 기호를 직접 자주 사용할 필요는 없습니다. 가능하다면 show 규칙을 사용하세요(예를 들어, 모든 -th를 줄 바꿈 방지 하이픈인 \u{2011}th로 교체).

줄 바꿈 방지 기호 (Non-breaking symbols)

줄 바꿈 방지 기호는 단어나 어구가 분리되지 않도록 보장합니다. Typst는 이를 하나의 덩어리로 처리하려고 노력할 것입니다.

줄 바꿈 방지 공백 (Non-breaking space)

중요: 이것은 공백 기호이므로 복사해서 붙여넣는 것으로는 효과가 없습니다. Typst는 이를 에디터에서 소스 코드를 보기 좋게 만들기 위해 사용한 일반적인 공백 기호로 간주할 것입니다. 즉, _기본 공백_으로 해석합니다.

이 기호는 자주 사용해서는 안 되는 기호이지만(대신 Typst의 박스를 사용하세요), 줄 바꿈 방지 기호가 어떻게 작동하는지 보여주는 좋은 예시입니다:

#set page(width: 9em)

// Cruel과 world가 분리됩니다.
// 이것이 분리되어서는 안 되는 어구라고 가정해 봅시다. 어떻게 해야 할까요?
Hello cruel world

// 특수 공백으로 연결해 봅시다!

// 일반적인 공백은 허용되지 않으므로 세미콜론을 사용하거나...
Hello cruel#sym.space.nobreak;world

// ...괄호를 사용하거나...
Hello cruel#(sym.space.nobreak)world

// ...유니코드 코드를 사용할 수 있습니다.
Hello cruel\u{00a0}world

// 동일한 효과를 얻기 위해 박스(box)를 사용하는 것을 권장합니다:
Hello #box[cruel world]
Rendered image

줄 바꿈 방지 하이픈 (Non-breaking hyphen)

#set page(width: 8em)

이것은 $i$-th 요소입니다.

이것은 $i$\u{2011}th 요소입니다.

// 가장 좋은 방법은 다음과 같습니다.
#show "-th": "\u{2011}th"

이것은 $i$-th 요소입니다.
Rendered image

커넥터 및 구분 기호 (Connectors and separators)

단어 결합자 (Word joiner)

기본적으로 단어 결합자는 이 위치에서 줄 바꿈이 발생하지 않아야 함을 나타냅니다. 또한 너비가 0인 기호(보이지 않음)이므로 공백을 제거하는 용도로 사용할 수 있습니다:

#set page(width: 9em)
#set text(hyphenate: true)

Thisisawordthathastobreak

// 주의하세요, 이제 줄 바꿈이 전혀 발생하지 않습니다!
Thisi#sym.wj;sawordthathastobreak

// `physica` 패키지의 코드
// 여기서 단어 결합자는 추가 공백을 피하기 위해 사용됩니다.
#let just-hbar = move(dy: -0.08em, strike(offset: -0.55em, extent: -0.05em, sym.planck))
#let hbar = (sym.wj, just-hbar, sym.wj).join()

$ a #just-hbar b, a hbar b$
Rendered image

너비가 0인 공백 (Zero width space)

단어 결합자와 비슷하지만, 이것은 공백 입니다. 단어 분리를 방지하지는 않습니다. 반대로, 하이픈 없이 단어를 분리합니다!

#set page(width: 9em)
#set text(hyphenate: true)

// 내부에 공백이 있습니다!
Thisisa#sym.zws;word

// 주의하세요, 이제 하이픈이 전혀 나타나지 않습니다!
Thisisawo#sym.zws;rdthathastobreak
Rendered image

기타 (Extra)

참고문헌 (Bibliography)

Typst는 BibLaTex .bib 파일이나 자체 Hayagriva .yml 형식을 사용하여 참고문헌을 지원합니다.

BibLaTex가 더 널리 지원되지만, Hayagriva가 작업하기에는 더 쉽습니다.

Hayagriva 문서와 몇 가지 예제 링크입니다.

인용 스타일 (Citation Style)

CSL(인용 스타일 언어)을 통해 스타일을 사용자 정의할 수 있으며, 10,000개 이상의 스타일을 온라인에서 사용할 수 있습니다. 공식 저장소를 확인하세요.

Typst 스니펫(Snippets)

일반적인(또는 일반적이지 않은) 작업에 유용한 스니펫 모음입니다.

데모 (Demos)

이력서 (템플릿 사용)

#import "@preview/modern-cv:0.8.0": *

#show: resume.with(
  author: (
    firstname: "John",
    lastname: "Smith",
    email: "js@example.com",
    homepage: "https://example.com",
    phone: "(+1) 111-111-1111",
    github: "DeveloperPaul123",
    twitter: "typstapp",
    scholar: "",
    orcid: "0000-0000-0000-000X",
    birth: "January 1, 1990",
    linkedin: "Example",
    address: "111 Example St. Example City, EX 11111",
    positions: (
      "Software Engineer",
      "Software Architect",
      "Developer",
    ),
  ),
  profile-picture: none,
  date: datetime.today().display(),
  language: "en",
  colored-headers: true,
  show-footer: false,
  paper-size: "us-letter",
  font: "Noto Sans CJK KR",
)

= 경력 (Experience)

#resume-entry(
  title: "Senior Software Engineer",
  location: "Example City, EX",
  date: "2019 - 현재",
  description: "Example, Inc.",
  title-link: "https://github.com/DeveloperPaul123",
)

#resume-item[
  - #lorem(20)
  - #lorem(15)
  - #lorem(25)
]

#resume-entry(
  title: "Software Engineer",
  location: "Example City, EX",
  date: "2011 - 2019",
  description: "Previous Company, Inc.",
)

#resume-item[
  // 콘텐츠가 반드시 글머리 기호일 필요는 없습니다
  #lorem(72)
]

#resume-entry(
  title: "Intern",
  location: "Example City, EX",
)

#resume-item[
  - #lorem(20)
  - #lorem(15)
  - #lorem(25)
]

= 프로젝트 (Projects)

#resume-entry(
  title: "Thread Pool C++ Library",
  location: [#github-link("DeveloperPaul123/thread-pool")],
  date: "2021년 5월 - 현재",
  description: "설계 및 개발",
)

#resume-item[
  - 최신 C++20 및 C++23 기능을 사용하여 C++로 스레드 풀 라이브러리를 설계하고 구현했습니다.
  - 라이브러리에 대한 광범위한 문서와 유닛 테스트를 작성하여 Github에 공개했습니다.
]

#resume-entry(
  title: "Event Bus C++ Library",
  location: github-link("DeveloperPaul123/eventbus"),
  date: "2019년 9월 - 현재",
  description: "설계 및 개발",
)

#resume-item[
  - C++17을 사용하여 이벤트 버스 라이브러리를 설계하고 구현했습니다.
  - 라이브러리에 대한 상세한 문서와 유닛 테스트를 작성하여 Github에 공개했습니다.
]

= 기술 스택 (Skills)

#resume-skill-item(
  "언어",
  (strong("C++"), strong("Python"), "Java", "C#", "JavaScript", "TypeScript"),
)
#resume-skill-item("사용 가능 언어", (strong("영어"), "스페인어"))
#resume-skill-item(
  "프로그램",
  (strong("Excel"), "Word", "Powerpoint", "Visual Studio"),
)

= 학력 (Education)

#resume-entry(
  title: "Example University",
  location: "Example City, EX",
  date: "2014년 8월 - 2019년 5월",
  description: "컴퓨터 과학 학사",
)

#resume-item[
  - #lorem(20)
  - #lorem(15)
  - #lorem(25)
]
Rendered image
Rendered image

책 표지

// 저자: bamdone
#let accent  = rgb("#00A98F")
#let accent1 = rgb("#98FFB3")
#let accent2 = rgb("#D1FF94")
#let accent3 = rgb("#D3D3D3")
#let accent4 = rgb("#ADD8E6")
#let accent5 = rgb("#FFFFCC")
#let accent6 = rgb("#F5F5DC")

#set page(paper: "a4",margin: 0.0in, fill: accent)

#set rect(stroke: 4pt)
#move(
  dx: -6cm, dy: 1.0cm,
  rotate(-45deg,
    rect(
      width: 100cm,
      height: 2cm,
      radius: 50%,
      stroke: 0pt,
      fill:accent1,
)))

#set rect(stroke: 4pt)
#move(
  dx: -2cm, dy: -1.0cm,
  rotate(-45deg,
    rect(
      width: 100cm,
      height: 2cm,
      radius: 50%,
      stroke: 0pt,
      fill:accent2,
)))

#set rect(stroke: 4pt)
#move(
  dx: 8cm, dy: -10cm,
  rotate(-45deg,
    rect(
      width: 100cm,
      height: 1cm,
      radius: 50%,
      stroke: 0pt,
      fill:accent3,
)))

#set rect(stroke: 4pt)
#move(
  dx: 7cm, dy: -8cm,
  rotate(-45deg,
    rect(
      width: 1000cm,
      height: 2cm,
      radius: 50%,
      stroke: 0pt,
      fill:accent4,
)))

#set rect(stroke: 4pt)
#move(
  dx: 0cm, dy: -0cm,
  rotate(-45deg,
    rect(
      width: 1000cm,
      height: 2cm,
      radius: 50%,
      stroke: 0pt,
      fill:accent1,
)))

#set rect(stroke: 4pt)
#move(
  dx: 9cm, dy: -7cm,
  rotate(-45deg,
    rect(
      width: 1000cm,
      height: 1.5cm,
      radius: 50%,
      stroke: 0pt,
      fill:accent6,
)))

#set rect(stroke: 4pt)
#move(
  dx: 16cm, dy: -13cm,
  rotate(-45deg,
    rect(
      width: 1000cm,
      height: 1cm,
      radius: 50%,
      stroke: 0pt,
      fill:accent2,
)))

#align(center)[
  #rect(width: 30%,
    fill: accent4,
    stroke:none,
    [#align(center)[
      #text(size: 60pt,[제목])
    ]
    ])
]

#align(center)[
  #rect(width: 30%,
    fill: accent4,
    stroke:none,
    [#align(center)[
      #text(size: 20pt,[저자])
    ]
    ])
]
Rendered image

로고 및 그림

SVG 이미지를 사용하는 것은 아주 좋습니다. 하지만 이미지를 찾기 귀찮은 분들을 위해 문서에 바로 복사해서 붙여넣을 수 있는 몇 가지 로고를 소개합니다.

중요: 텍스트 내의 Typst는 (LaTeX와 달리) 특별한 표기법이 필요하지 않습니다. 그냥 "Typst"라고 쓰거나, 필요하다면 "Typst"라고 쓰면 충분합니다.

TeX 및 LaTeX


#let TeX = {
  set text(font: "New Computer Modern", weight: "regular")
  box(width: 1.7em, {
    [T]
    place(top, dx: 0.56em, dy: 0.22em)[E]
    place(top, dx: 1.1em)[X]
  })
}

#let LaTeX = {
  set text(font: "New Computer Modern", weight: "regular")
  box(width: 2.55em, {
    [L]
    place(top, dx: 0.3em, text(size: 0.7em)[A])
    place(top, dx: 0.7em)[#TeX]
  })
}

#TeX#LaTeX 을 알고 있다면 Typst를 배우는 것은 그리 어렵지 않습니다.
Rendered image

Typst 가이 (Typst guy)

// 저자: fenjalien
#import "@preview/cetz:0.3.4": *

#set page(width: auto, height: auto)

#canvas(length: 1pt, {
  import draw: *
  let color = rgb("239DAD")
  scale(y: -1)
  set-style(fill: color, stroke: none,)

  // 몸체
  merge-path({
    bezier(
      (112.847, 134.007),
      (114.835, 143.178),
      (112.847, 138.562),
      (113.509, 141.619),
      name: "b"
    )
    bezier(
      "b.end",
      (122.063, 145.515),
      (116.16, 144.736),
      (118.569, 145.515),
      name: "b"
    )
    bezier(
      "b.end",
      (135.977, 140.121),
      (125.677, 145.515),
      (130.315, 143.717)
    )
    bezier(
      (139.591, 146.055),
      (113.389, 159.182),
      (128.99, 154.806),
      (120.256, 159.182),
      name: "b"
    )
    bezier(
      "b.end",
      (97.1258, 154.327),
      (106.522, 159.182),
      (101.101, 157.563),
      name: "b"
    )
    bezier(
      "b.end",
      (91.1626, 136.704),
      (93.1503, 150.97),
      (91.1626, 145.096),
      name: "b"
    )
    line(
      (rel: (0, -47.1126), to: "b.end"),
      (rel: (-9.0352, 0)),
      (80.6818, 82.9381),
      (91.1626, 79.7013),
      (rel: (0, -8.8112)),
      (112.847, 61),
      (rel: (0, 19.7802)),
      (134.17, 79.1618),
      (132.182, 90.8501),
      (112.847, 90.1309)
    )
  })

  // 왼쪽 눈동자
  merge-path({
    bezier(
      (70.4667, 65.6833),
      (71.9727, 70.5068),
      (71.4946, 66.9075),
      (71.899, 69.4091)
    )
    bezier(
      (71.9727, 70.5068),
      (75.9104, 64.5912),
      (72.9675, 69.6715),
      (75.1477, 67.319)
    )
    bezier(
      (75.9104, 64.5912),
      (72.0556, 60.0005),
      (76.8638, 61.1815),
      (74.4045, 59.7677)
    )
    bezier(
      (72.0556, 60.0005),
      (66.833, 64.3859),
      (70.1766, 60.1867),
      (67.7909, 63.0017)
    )
    bezier(
      (66.833, 64.3859),
      (70.4667, 65.6833),
      (67.6159, 64.3083),
      (69.4388, 64.4591)
    )
  })

  // 오른쪽 눈동자
  merge-path({
    bezier(
      (132.37, 61.668),
      (133.948, 66.7212),
      (133.447, 62.9505),
      (133.87, 65.5712)
    )
    bezier(
      (133.948, 66.7212),
      (138.073, 60.5239),
      (134.99, 65.8461),
      (137.274, 63.3815)
    )
    bezier(
      (138.073, 60.5239),
      (134.034, 55.7145),
      (139.066, 56.9513),
      (136.495, 55.4706)
    )
    bezier(
      (134.034, 55.7145),
      (128.563, 60.3087),
      (132.066, 55.9066),
      (129.567, 58.8586),
    )
    bezier(
      (128.563, 60.3087),
      (132.37, 61.668),
      (129.383, 60.2274),
      (131.293, 60.3855),
    )
  })

  set-style(
    stroke: (paint: rgb("239DAD"), thickness: 6pt, cap: "round"),
    fill: none,
  )

  // 왼쪽 눈
  merge-path({
    bezier(
      (58.5, 64.7273),
      (73.6136, 52),
      (58.5, 58.3636),
      (64.0682, 52.7955),
      name: "b"
    )
    bezier(
      "b.end",
      (84.75, 64.7273),
      (81.5682, 52),
      (84.75, 57.5682),
      name: "b"
    )
    bezier(
      "b.end",
      (71.2273, 76.6591),
      (84.75, 71.8864),
      (79.1818, 76.6591),
      name: "b"
    )
    bezier(
      "b.end",
      (58.5, 64.7273),
      (63.2727, 76.6591),
      (58.5, 71.0909)
    )
  })
  // 속눈썹
  line(
    (62.5, 55),
    (59.5, 52),
  )

  merge-path({
    bezier(
      (146.5, 61.043),
      (136.234, 49),
      (146.5, 52.7634),
      (141.367, 49)
    )
    bezier(
      (136.234, 49),
      (121.569, 62.5484),
      (125.969, 49),
      (120.836, 54.2688)
    )
    bezier(
      (121.569, 62.5484),
      (134.034, 72.3333),
      (122.302, 70.8279),
      (128.168, 72.3333)
    )
    bezier(
      (134.034, 72.3333),
      (146.5, 61.043),
      (139.901, 72.3333),
      (146.5, 69.3225)
    )
  })

  set-style(stroke: (thickness: 4pt))

  // 오른쪽 팔
  merge-path({
    bezier(
      (109.523, 115.614),
      (127.679, 110.918),
      (115.413, 115.3675),
      (122.283, 113.112)
    )
    bezier(
      (127.679, 110.918),
      (137, 106.591),
      (130.378, 109.821),
      (132.708, 108.739)
    )
  })

  // 오른쪽 첫 번째 손가락
  bezier(
    (137, 106.591),
    (140.5, 98.0908),
    (137.385, 102.891),
    (138.562, 99.817)
  )

  // 오른쪽 두 번째 손가락
  bezier(
    (137, 106.591),
    (146, 101.591),
    (139.21, 103.799),
    (142.425, 101.713)
  )

  // 오른쪽 세 번째 손가락
  line(
    (137, 106.591),
    (148, 106.591)
  )

  // 오른쪽 네 번째 손가락
  bezier(
    (137, 106.591),
    (146, 111.091),
    (140.243, 109.552),
    (143.119, 110.812)
  )

  // 왼쪽 팔
  bezier(
    (95.365, 116.979),
    (73.5, 107.591),
    (88.691, 115.549),
    (80.587, 112.887)
  )

  // 왼쪽 첫 번째 손가락
  line(
    (73.5, 107.591),
    (rel: (0, -9.5))
  )
  // 왼쪽 두 번째 손가락
  line(
    (73.5, 107.591),
    (65.396, 100.824)
  )
  // 왼쪽 세 번째 손가락
  line(
    (73.5, 107.591),
    (63.012, 105.839)
  )
  // 왼쪽 네 번째 손가락
  bezier(
    (73.5, 107.591),
    (63.012, 111.04),
    (70.783, 109.121),
    (67.214, 111.255)
  )
})
Rendered image

레이블 (Labels)

레이블의 장(Chapter) 정보 가져오기

#let ref-heading(label) = context {
  let elems = query(label)
  if elems.len() != 1 {
    panic("여러 요소가 발견되었습니다")
  }
  let element = elems.first()
  if element.func() != heading {
    panic("레이블이 제목(heading)을 대상으로 해야 합니다")
  }
  link(label, element.body)
}

= 디자인 <design>
#lorem(20)

= 구현
#ref-heading(<design>) 에서 논의했듯이...
Rendered image

존재하지 않는 참조 허용하기

// 저자: Enivex
#set heading(numbering: "1.")

#let myref(label) = context {
    if query(label).len() != 0 {
        ref(label)
    } else {
        // 존재하지 않는 참조
        text(fill: red)[???]
    }
}

= 두 번째 <test2>

#myref(<test>)

#myref(<test2>)
Rendered image

제목 (Headings)

제목의 번호 매기기 스타일과 외관을 커스터마이징하는 방법입니다.

제목 스타일 변경

show 규칙을 사용하여 제목의 폰트, 색상, 크기 등을 일괄적으로 변경할 수 있습니다.

#show heading.where(level: 1): set text(blue, size: 1.5em)
#show heading.where(level: 2): set text(gray)

= 1단계 제목 (파란색)
== 2단계 제목 (회색)
Rendered image

제목 뒤에 선 긋기

장(Chapter) 제목 아래에 선을 추가하는 흔한 스타일입니다.

#show heading.where(level: 1): it => [
  #it
  #v(0.3em)
  #line(length: 100%, stroke: 0.5pt)
  #v(1em)
]

= 첫 번째 장
Rendered image

번호 매기기 커스텀

번호 뒤에 특정 기호를 붙이거나 형식을 바꿀 수 있습니다.

#set heading(numbering: "1.1)")

= 도입
== 배경
Rendered image

특정 수준부터 번호를 매기지 않으려면 numbering: none을 사용합니다.

페이지 번호 매기기

각 장(Chapter)별 별도의 페이지 번호 매기기

/// 저자: tinger

// 필요한 경우 번호 없는 타이틀 페이지
// ...

// 전면부 (Front-matter)
#set page(numbering: "I")
#counter(page).update(1)
#lorem(50)
// ...

// 페이지 카운터 앵커
#metadata(()) <front-matter>

// 본문 (Main document body)
#set page(numbering: "1")
#lorem(50)
#counter(page).update(1)
// ...

// 후면부 (Back-matter)
#set page(numbering: "I")
// 페이지 나누기를 고려해야 하며, +1 또는 -1만큼 오프셋이 필요할 수 있습니다.
#context counter(page).update(counter(page).at(<front-matter>).first())
#lorem(50)
// ...
Rendered image
Rendered image
Rendered image

개요 (Outlines)

다양한 개요(Outlines) 예제는 공식 참조 문서에서 확인할 수 있습니다.

목차 (Table of contents)

#outline()

= 서론
#lorem(5)

= 이전 연구
#lorem(10)
Rendered image

그림 목록 (Outline of figures)

#outline(
  title: [그림 목록],
  target: figure.where(kind: table),
)

#figure(
  table(
    columns: 4,
    [t], [1], [2], [3],
    [y], [0.3], [0.7], [0.5],
  ),
  caption: [실험 결과],
)
Rendered image

임의의 선택자(selector)를 사용할 수 있으므로, 매우 다양한 시도가 가능합니다.

낮은 수준의 제목 무시하기

#set heading(numbering: "1.")
#outline(depth: 2)

= 포함됨
최상위 섹션입니다.

== 여전히 포함됨
서브섹션입니다.

=== 포함되지 않음
개요에 표시되지 않습니다.
Rendered image

들여쓰기 설정하기

#set heading(numbering: "1.a.")

#outline(
  title: [콘텐츠 (자동)],
  indent: auto,
)

#outline(
  title: [콘텐츠 (길이 지정)],
  indent: 2em,
)

#set outline.entry(fill: "")
#outline(
  title: [콘텐츠 (함수 사용)],
)

= ACME Corp.에 대하여
== 역사
=== 기원
#lorem(10)

== 제품
#lorem(10)
Rendered image

기본 점선 교체하기

#set outline.entry(fill: line(length: 100%))
#outline(indent: 2em)

= 1단계
== 2단계
Rendered image

개요 수준별 스타일 다르게 지정하기

#set heading(numbering: "1.")

#show outline.entry.where(
  level: 1
): it => {
  v(12pt, weak: true)
  strong(it)
}

#outline(indent: auto)

= 서론
= 배경
== 역사
== 최신 기술 (State of the Art)
= 분석
== 설정
Rendered image

개요용 짧은 캡션과 문서용 긴 캡션 사용하기

// 저자: laurmaedje
// 템플릿의 어딘가 또는 문서 시작 부분에 배치하세요.
#let in-outline = state("in-outline", false)
#show outline: it => {
  in-outline.update(true)
  it
  in-outline.update(false)
}

#let flex-caption(long, short) = context if in-outline.get() { short } else { long }

// 문서 내에서의 사용 예시입니다.
#outline(title: [그림 목록], target: figure)

#figure(
  rect(),
  caption: flex-caption(
    [이것은 문서에 표시될 긴 캡션 텍스트입니다.],
    [이것은 짧은 캡션입니다.],
  )
)
Rendered image

인용 및 각주 무시하기

각주가 제목에 포함되어 있을 때 개요에 각주 번호가 표시되는 문제를 해결하는 우회 방법입니다:


= 제목 #footnote[각주 내용]

텍스트

#outline() // 좋지 않은 결과 :(

#pagebreak()
#{
  set footnote.entry(
    separator: none
  )
  show footnote.entry: hide
  show ref: none
  show footnote: none

  outline()
}
Rendered image
Rendered image

기타 (Extra)

참고문헌 (Bibliography)

Typst는 BibLaTex .bib 파일이나 자체 Hayagriva .yml 형식을 사용하여 참고문헌을 지원합니다.

BibLaTex가 더 널리 지원되지만, Hayagriva가 작업하기에는 더 쉽습니다.

Hayagriva 문서와 몇 가지 예제 링크입니다.

인용 스타일 (Citation Style)

CSL(인용 스타일 언어)을 통해 스타일을 사용자 정의할 수 있으며, 10,000개 이상의 스타일을 온라인에서 사용할 수 있습니다. 공식 저장소를 확인하세요.

페이지 설정 (Page setup)

공식 페이지 설정 가이드를 참조하세요.

#set page(
  width: 3cm,
  margin: (x: 0cm),
)

#for i in range(3) {
  box(square(width: 1cm))
}
Rendered image
#set page(columns: 2, height: 4.8cm)
기후 변화는 우리 시대의 가장 시급한 문제 중 
하나로, 전 세계의 지역 사회, 생태계 및 
경제를 황폐화할 잠재력을 가지고 있습니다. 
우리는 탄소 배출을 줄이고 급격히 변화하는 
기후의 영향을 완화하기 위해 긴급한 조치를 
취해야 한다는 것이 분명합니다.
Rendered image
#set page(fill: rgb("444352"))
#set text(fill: rgb("fdfdfd"))
*다크 모드가 활성화되었습니다.*
Rendered image
#set par(justify: true)
#set page(
  margin: (top: 32pt, bottom: 20pt),
  header: [
    #set text(8pt)
    #smallcaps[Typst Academcy]
    #h(1fr) _연습 문제 시트 3_
  ],
)

#lorem(19)
Rendered image
#set page(foreground: text(24pt)[🥸])

리뷰어 2는 우리의 접근 방식을 이해하지 
못했다는 이유로 우리 논문에 
"약한 거절(Weak Reject)"을 표시했습니다...
Rendered image

무언가 숨기기 (Hiding things)

// 저자: GeorgeMuscat
#let redact(text, fill: black, height: 1em) = {
  box(rect(fill: fill, height: height)[#hide(text)])
}

예시:
  - 가려지지 않은 텍스트
  - 가려진 #redact("텍스트")
Rendered image

여러 줄 감지 (Multiline detection)

그림 캡션(또는 다른 요소)이 한 줄보다 많은지 감지합니다.

캡션이 여러 줄인 경우 왼쪽 정렬로 설정합니다.

수동 줄 바꿈(manual linebreaks)에서는 제대로 작동하지 않을 수 있습니다.
#show figure.caption: it => {
  layout(size => context [
    #let text-size = measure(
      ..size,
      it.supplement + it.separator + it.body,
    )

    #let my-align

    #if text-size.width < size.width {
      my-align = center
    } else {
      my-align = left
    }

    #align(my-align, it)
  ])
}

#figure(caption: lorem(6))[
    ```rust
    pub fn main() {
        println!("Hello, world!");
    }
    ```
]

#figure(caption: lorem(20))[
    ```rust
    pub fn main() {
        println!("Hello, world!");
    }
    ```
]
Rendered image

콘텐츠 복제 (Duplicate content)

이 구현은 레이블 및 이와 유사한 요소들과 충돌할 수 있습니다. 복잡한 사례의 경우 아래의 고급 버전을 참조하세요.
```typ #set page(paper: "a4", flipped: true) #show: body => grid( columns: (1fr, 1fr), column-gutter: 1cm, body, body, ) #lorem(200) ```

고급 버전 (Advanced)

/// 저자: frozolotl
#set page(paper: "a4", flipped: true)
#set heading(numbering: "1.1")
#show ref: it => {
  if it.element != none {
    it
  } else {
    let targets = query(it.target)
    if targets.len() == 2 {
      let target = targets.first()
      if target.func() == heading {
        let num = numbering(target.numbering, ..counter(heading).at(target.location()))
        [#target.supplement #num]
      } else if target.func() == figure {
        let num = numbering(target.numbering, ..target.counter.at(target.location()))
        [#target.supplement #num]
      } else {
        it
      }
    } else {
      it
    }
  }
}
#show link: it => context {
  let dest = query(it.dest)
  if dest.len() == 2 {
    link(dest.first().location(), it.body)
  } else {
    it
  }
}
#show: body => context grid(
  columns: (1fr, 1fr),
  column-gutter: 1cm,
  body,
  {
    let reset-counter(kind) = counter(kind).update(counter(kind).get())
    reset-counter(heading)
    reset-counter(figure.where(kind: image))
    reset-counter(figure.where(kind: raw))
    set heading(outlined: false)
    set figure(outlined: false)
    body
  },
)

#outline()

= 푸 (Foo) <foo>
@foo@foobar 를 참조하세요.

#figure(rect[이것은 이미지입니다], caption: [푸바 (Foobar)], kind: raw) <foobar>

== 바 (Bar)
== 바즈 (Baz)
#link(<foo>)[Foo 방문하려면 클릭]
Rendered image

목록 항목 사이의 선 (Lines between list items)

/// 저자: frozolotl
#show enum.where(tight: false): it => {
  it.children
    .enumerate()
    .map(((n, item)) => block(below: .6em, above: .6em)[#numbering("1.", n + 1) #item.body])
    .join(line(length: 100%))
}

+ 항목 1

+ 항목 2

+ 항목 3
Rendered image

동일한 접근 방식을 사용하여 원하는 대로 열거형(enum) 스타일을 쉽게 조정할 수 있습니다.

텍스트가 있는 도형 상자 (Shaped boxes with text)

(나중에는 패키지로 만들어지겠지만, 현재는 스니펫으로 제공합니다)

/// 저자: JustForFun88
#import "@preview/oxifmt:0.2.1": strfmt

#let shadow_svg_path = `
<svg
    width="{canvas-width}"
    height="{canvas-height}"
    viewBox="{viewbox}"
    version="1.1"
    xmlns="http://www.w3.org/2000/svg"
    xmlns:svg="http://www.w3.org/2000/svg">
    <!-- Definitions for reusable components -->
    <defs>
        <filter id="shadowing" >
            <feGaussianBlur in="SourceGraphic" stdDeviation="{blur}" />
        </filter>
    </defs>

    <!-- Drawing the rectangle with a fill and feGaussianBlur effect -->
    <path
        style="fill: {flood-color}; opacity: {flood-opacity}; filter:url(#shadowing)"
        d="{vertices} Z" />
</svg>
`.text

#let parallelogram(width: 20mm, height: 5mm, angle: 30deg) = {
	let δ = height * calc.tan(angle)
	(
    (      + δ,     0pt   ),
    (width + δ * 2, 0pt   ),
    (width + δ,     height),
    (0pt,           height),
	)
}

#let hexagon(width: 100pt, height: 30pt, angle: 30deg) = {
  let dy = height / 2;
	let δ = dy * calc.tan(angle)
	(
    (0pt,           dy    ),
    (      + δ,     0pt   ),
    (width + δ,     0pt   ),
    (width + δ * 2, dy    ),
    (width + δ,     height),
    (      + δ,     height),
	)
}

#let shape_size(vertices) = {
    let x_vertices = vertices.map(array.first);
    let y_vertices = vertices.map(array.last);

    (
      calc.max(..x_vertices) - calc.min(..x_vertices),
      calc.max(..y_vertices) - calc.min(..y_vertices)
    )
}

#let shadowed_shape(shape: hexagon, fill: none,
  stroke: auto, angle: 30deg, shadow_fill: black, alpha: 0.5, 
  blur: 1.5, blur_margin: 5, dx: 0pt, dy: 0pt, ..args, content
) = layout(size => context {
    let named = args.named()
    for key in ("width", "height") {
      if key in named and type(named.at(key)) == ratio {
        named.insert(key, size.at(key) * named.at(key))
      }
    }

    let opts = (blur: blur, flood-color: shadow_fill.to-hex())
       
    let content = box(content, ..named)
    let size = measure(content)

    let vertices = shape(..size, angle: angle)
    let (shape_width, shape_height) = shape_size(vertices)
    let margin = opts.blur * blur_margin * 1pt

    opts += (
      canvas-width:  shape_width  + margin,
      canvas-height: shape_height + margin,
      flood-opacity: alpha
    )

    opts.viewbox = (0, 0, opts.canvas-width.pt(), opts.canvas-height.pt()).map(str).join(",")

    opts.vertices = "";
    let d = margin / 2;
    for (i, p) in vertices.enumerate() {
        let prefix = if i == 0 { "M " } else { " L " };
        opts.vertices += prefix + p.map(x => str((x + d).pt())).join(", ");
    }

    let svg-shadow = image(bytes(strfmt(shadow_svg_path, ..opts)))
    place(dx: dx, dy: dy, svg-shadow)
    place(curve(..((curve.move(vertices.at(0)),) + vertices.slice(1).map(curve.line) + (curve.close(),)), fill: fill, stroke: stroke))
    box(h((shape_width - size.width) / 2) + content, width: shape_width)
})

#set text(3em);

#shadowed_shape(shape: hexagon,
    inset: 1em, fill: teal,
    stroke: 1.5pt + teal.darken(50%),
    shadow_fill: red,
    dx: 0.5em, dy: 0.35em, blur: 3)[안녕 하세요!]
#shadowed_shape(shape: parallelogram,
    inset: 1em, fill: teal,
    stroke: 1.5pt + teal.darken(50%),
    shadow_fill: red,
    dx: 0.5em, dy: 0.35em, blur: 3)[안녕 하세요!]
Rendered image

코드 포맷팅 (Code formatting)

인라인 강조 (Inline highlighting)

#let r = raw.with(lang: "r")

다음과 같이 사용할 수 있습니다: #r("x <- c(10, 42)")
Rendered image

탭 크기 (Tab size)

#set raw(tab-size: 8)
```tsv
Year	Month	Day
2000	2	3
2001	2	1
2002	3	10
```
Rendered image

테마 (Theme)

공식 참조 문서를 확인하세요.

코드 합자(Ligatures) 활성화

#show raw: set text(ligatures: true, font: ("Cascadia Code", "Noto Sans CJK KR"))

이제 코드가 `x <- a`와 같이 표시됩니다.
Rendered image

고급 포맷팅

패키지 섹션을 참조하세요.

그리드(Grids)

비율 그리드

길이가 변하는 선이 있는 표의 경우, _그리드 안의 그리드_를 사용해 볼 수 있습니다.

cell.colspan 및 rowspan이 가능한 곳에는 사용하지 마세요.
// author: jimpjorps

#grid(
  columns: (1fr,),
  grid(
    columns: (1fr,)*2, inset: 5pt, stroke: 1pt, [안녕], [세상]
  ),
  grid(
    columns: (1fr,)*3, inset: 5pt, stroke: 1pt, [foo], [bar], [baz]
  ),
  grid.cell(inset: 5pt, stroke: 1pt)[abcxyz]
)
Rendered image

같은 값을 가진 인접 셀 자동 병합

이 예제는 가로로 인접한 셀에 대해 작동하지만, 세로(열)로 확장하는 것도 어렵지 않습니다.

// author: tebine
#let merge(children, n-cols) = {
  let rows = children.chunks(n-cols)
  let new-children = ()
  for r in rows {
    // 첫 번째 그룹은 인덱스 0에서 시작
    let i = 0 
    // 다음 그룹 검색
    while i < r.len() {
      // 그룹은 하나의 셀로 시작
      let c = r.at(i).body
      let n = 1
      for j in range(i+1, r.len()) {
        let c-next = r.at(j).body
        if c-next == c {
          // 그룹에 셀 추가
          n += 1
        } else {
          break
        }
      }
      // 그룹 종료
      new-children.push(table.cell(colspan: n, c))
      i += n
    }
  }
  return new-children
}
#show table: it => {
  let merged = merge(it.children, it.columns.len())
  if it.children.len() == merged.len() { // 재귀를 피하기 위한 트릭
    return it
  }
  table(columns: it.columns.len(), ..merged)
}
#table(columns: 2,
  [1], [2],
  [3], [3],
  [4], [5],
)
Rendered image

기울어진 테두리가 있는 기울어진 열 헤더

// author: tebine
#let slanted(it, alpha: 45deg, len: 2.5cm) = layout(size => {
  let width = size.width
  let b = box(inset: 5pt, rotate(-alpha, reflow: true, it))
  let b-size = measure(b)
  let l = line(angle: -alpha, length: len)
  let l-width = len * calc.cos(alpha)
  let l-height = len * calc.sin(alpha)
  place(bottom+left, l)
  place(bottom+left, l, dx: width)
  place(bottom+left, line(length: width), dx: l-width, dy: -l-height)
  place(bottom+left, dx: width/2, b)
  box(height: l-height) // 높이를 설정하기 위한 보이지 않는 상자
})

#table(
  columns: 2,
  align: center,
  table.header(
    table.cell(stroke: none, inset: 0pt, slanted[*AAA*]),
    table.cell(stroke: none, inset: 0pt, slanted[*BBBBBB*]),
  ),
  [aaaaa], [bbbbbb], [c], [d],
)
Rendered image

하이픈 연결 (Hyphenation)

Typst는 텍스트가 줄 끝에 걸칠 때 자동으로 하이픈(-)을 넣어 단어를 끊어주는 기능을 지원합니다.

자동 하이픈 활성화

기본적으로 하이픈 연결은 비활성화되어 있을 수 있습니다. set text 규칙을 사용하여 활성화합니다.

#set page(width: 15em)
#set text(hyphenate: true, lang: "en")

Hyphenation is the process of inserting hyphens between the syllables of a word, especially such that an appropriate line break can occur.
Rendered image

언어 설정의 중요성

하이픈 연결 규칙은 언어마다 다릅니다. 올바른 하이픈 연결을 위해 반드시 lang 속성을 지정해야 합니다.

// 독일어 하이픈 규칙 적용
#set text(lang: "de", hyphenate: true)
Donaudampfschifffahrtselektrizitätenhauptbetriebswerkbauunterbeamtengesellschaft
Rendered image

수동 하이픈 제어

특정 단어가 끊어지는 것을 방지하거나, 특정 위치에서만 끊어지게 하려면 다음과 같은 기호를 사용합니다.

  • 줄 바꿈 방지 공백: ~ 또는 #sym.space.nobreak
  • 소프트 하이픈 (Soft hyphen): - (필요한 경우에만 하이픈 표시)
  • 줄 바꿈 방지 하이픈: \u{2011}
#set page(width: 10em)
#set text(hyphenate: true)

// 이 단어는 절대 끊어지지 않습니다.
#box[unbreakable-word]

// 하이픈 위치를 직접 지정합니다.
유비쿼\-터스 환경에서의 조판
Rendered image

스크립팅 (Scripting)

배열 평탄화 해제 (Unflatten arrays)

// author: PgSuper
#let unflatten(arr, n) = {
  let columns = range(0, n).map(_ => ())
  for (i, x) in arr.enumerate() {
    columns.at(calc.rem(i, n)).push(x)
  }
  array.zip(..columns)
}

#unflatten((1, 2, 3, 4, 5, 6), 2)
#unflatten((1, 2, 3, 4, 5, 6), 3)
Rendered image

약어 만들기

#let full-name = "Federal University of Ceará"

#let letts = {
  full-name
    .split()
    .map(word => word.at(0)) // 대문자만 필터링
    .filter(l => upper(l) == l)
    .join()
}
#letts
Rendered image

구분 기호를 포함하여 문자열 분할

#",this, is a a a a; a. test? string!".matches(regex("(\b[\P{Punct}\s]+\b|\p{Punct})")).map(x => x.captures).join()
Rendered image

배열의 모든 값과 일치하는 선택자 생성

이 스니펫은 배열 내부의 값 중 하나라도 일치하는 선택자(show 규칙에서 사용됨)를 생성합니다. 여기서는 몇 개의 raw 라인을 강조 표시하는 데 사용되지만, 어떤 종류의 선택자에도 쉽게 적용할 수 있습니다.

// author: Blokyk
#let lines = (2, 3, 5)
#let lines-selectors = lines.map(lineno => raw.line.where(number: lineno))
#let lines-combined-selector = lines-selectors.fold(
  // 기본적으로 첫 번째 선택자로 시작
  // 가능한 경우 아무것도 일치하지 않는 선택자를 사용할 수도 있음
  lines-selectors.at(0),
  selector.or // 모든 선택자의 OR 생성 (대안: (acc, sel) => acc.or(sel))
)

#show lines-combined-selector: highlight

```py
def foo(x, y):
  if x == y:
    return False
  z = x + y
  return z * x - z * y >= z
```
Rendered image

딕셔너리에서 show (또는 show-set) 규칙 합성

이 스니펫은 키를 선택자로, 값을 설정할 매개변수로 사용하여 딕셔너리 내부의 모든 요소에 show-set 규칙을 적용합니다. 이 예제에서는 대응 딕셔너리를 기반으로 사용자 정의 그림 종류에 사용자 정의 보충(supplement)을 제공하는 데 사용됩니다.

// author: laurmaedje
#let kind_supp_dict = (
  algo: "Pseudo-code",
  ex: "Example",
  prob: "Problem",
)

// 전체 (나머지) 문서에 이 규칙 적용
#show: it => {
  kind_supp_dict
    .pairs() // 키-값 쌍의 배열 가져오기
    .fold( // 문서 앞에 show-set 규칙을 쌓을 것입니다
      it, // 기본 문서로 시작
      (acc, (kind, supp)) => {
        // 나머지의 맨 위에 현재 kind-supp 조합 추가
        show figure.where(kind: kind): set figure(supplement: supp)
        acc
      }
    )
}
#figure(
    kind: "algo",
    caption: [내 코드],
    ```Algorithm there```
)
Rendered image

또한, 이것은 작성한 위치에 적용되므로, 이 show-set 규칙들은 규칙을 작성한 같은 장소에 추가된 것처럼 보일 것입니다. 즉, 다른 show-set 규칙과 마찬가지로 나중에 덮어쓸 수 있습니다.

JSON

저자: MuhammadAly11

JSON 파일에서 JSON 배열을 가져와 사용하는 방법의 예시입니다.

작성하려는 테스트를 위한 다음과 같은 데이터 예시가 있다고 가정해 봅시다:

[
    {
        "sn": "1",
        "source": "Science",
        "question": "물(water)의 화학 기호는 무엇인가요?",
        "answer": "a",
        "a": "H₂O",
        "b": "CO₂",
        "c": "O₂",
        "d": "N₂",
    },
    {
        "sn": "2",
        "source": "History",
        "question": "미국의 초대 대통령은 누구인가요?",
        "answer": "a",
        "a": "조지 워싱턴",
        "b": "에이브러햄 링컨",
        "d": "존 애덤스",
    }
]

이 파일을 Typst로 가져와서 사용할 수 있습니다:

#let json_data = json("../file.json")

#for mcq in json_data {
    [== #mcq.sn. #mcq.question: ]
    for opt in ("a", "b", "c", "d", "e", "f", "g") {
        if opt in mcq and mcq.at(opt) != "" {
            [- #opt) #mcq.at(opt)]
        }
    }
}
Rendered image

번호 매기기

번호 없는 개별 제목

#let numless(it) = {set heading(numbering: none); it }

= 제목
#numless[= 번호 없는 제목]
Rendered image

"클린(Clean)" 번호 매기기

// 원저자: tromboneher

// 이전의 상위 요소를 생략하고 섹션 번호를 매깁니다.
// 예를 들어, 번호 매기기 패턴 "A.I.1."이 다음과 같이 생성된다면:
//
// A. 이야기의 한 부분
//   A.I. 장(Chapter)
//   A.II. 다른 장
//     A.II.1. 섹션
//       A.II.1.a. 서브섹션
//       A.II.1.b. 다른 서브섹션
//     A.II.2. 다른 섹션
// B. 이야기의 다른 부분
//   B.I. 두 번째 부분의 장
//   B.II. 두 번째 부분의 다른 장
//
// clean_numbering("A.", "I.", "1.a.")은 다음과 같이 생성됩니다:
//
// A. 이야기의 한 부분
//   I. 장
//   II. 다른 장
//     1. 섹션
//       1.a. 서브섹션
//       1.b. 다른 서브섹션
//     2. 다른 섹션
// B. 이야기의 다른 부분
//   I. 두 번째 부분의 장
//   II. 두 번째 부분의 다른 장
//
#let clean_numbering(..schemes) = {
  (..nums) => {
    let (section, ..subsections) = nums.pos()
    let (section_scheme, ..subschemes) = schemes.pos()

    if subsections.len() == 0 {
      numbering(section_scheme, section)
    } else if subschemes.len() == 0 {
      numbering(section_scheme, ..nums.pos())
    }
    else {
      clean_numbering(..subschemes)(..subsections)
    }
  }
}

#set heading(numbering: clean_numbering("A.", "I.", "1.a."))

= 부(Part)
== 장(Chapter)
== 다른 장
=== 섹션
==== 서브섹션
==== 다른 서브섹션
= 이야기의 다른 부분
== 두 번째 부분의 장
== 두 번째 부분의 다른 장
Rendered image

수학 번호 매기기

여기를 참조하세요.

각 단락에 번호 매기기

Typst 0.12 버전부터는 이 기능을 네이티브 솔루션으로 대체해야 합니다.
// 원저자: roehlichA
// 열거형의 법률적 서식
#show enum: it => context {
  // 어떤 수준에서 단계를 밟을지 알기 위해 마지막 제목을 가져옵니다
  let headings = query(selector(heading).before(here()))
  let last = headings.at(-1)

  // 출력 항목 결합
  let output = ()
  for item in it.children {
    output.push([
      #context{
        counter(heading).step(level: last.level + 1)
      }
      #context {
        counter(heading).display() 
      }
    ])
    output.push([
      #text(item.body)
      #parbreak()
    ])
  }

  // 그리드에 표시
  grid(
    columns: (auto, 1fr),
    column-gutter: 1em,
    row-gutter: 1em,
    ..output
  )

}

#set heading(numbering: "1.")

= 어떤 제목
+ 단락
= 기타
+ 여기의 단락 앞에는 번호가 붙어 있어 직접 참조할 수 있습니다.
+ _#lorem(100)_
+ _#lorem(100)_

== 서브섹션
+ 단락은 서브섹션에서도 올바르게 번호가 매겨집니다.
+ _#lorem(50)_
+ _#lorem(50)_
Rendered image

수학 번호 매기기

현재 제목(heading) 기준 번호 매기기

수학 패키지 섹션에서 내장된 번호 매기기 기능도 참조하세요.

/// 원저자: laurmaedje
#set heading(numbering: "1.")

// 각 장(chapter)에서 카운터 초기화
// 표시되는 섹션 번호의 개수를 변경하려면 level을 수정하세요
#show heading.where(level:1): it => {
  counter(math.equation).update(0)
  it
}

#set math.equation(numbering: n => {
  numbering("(1.1)", counter(heading).get().first(), n)
  // 표시되는 섹션 번호의 개수를 변경하려면 다음과 같이 수정하세요:
  /*
  let count = counter(heading).get()
  let h1 = count.first()
  let h2 = count.at(1, default: 0)
  numbering("(1.1.1)", h1, h2, n)
  */
})


= 섹션
== 서브섹션

$ 5 + 3 = 8 $ <a>
$ 5 + 3 = 8 $

= 새로운 섹션
== 서브섹션
$ 5 + 3 = 8 $
== 서브섹션
$ 5 + 3 = 8 $ <b>

@a@b 를 언급합니다.
Rendered image

레이블이 있는 수식에만 번호 매기기

간단한 코드

// 저자: shampoohere
#show math.equation:it => {
  if it.fields().keys().contains("label"){
    math.equation(block: true, numbering: "(1)", it)
    // `numbering`에서 원하는 번호 매기기 스타일로 
    // 변경하는 것을 잊지 마세요.
    //
    // 이제 #set numbering을 사용할 필요가 없다는 점에 유의하세요.
  } else {
    it
  }
}

$ sum_x^2 $
$ dif/(dif x)(A(t)+B(x))=dif/(dif x)A(t)+dif/(dif x)B(t) $ <ep-2>
$ sum_x^2 $
$ dif/(dif x)(A(t)+B(x))=dif/(dif x)A(t)+dif/(dif x)B(t) $ <ep-3>
Rendered image

해킹된 참조를 다시 클릭 가능하게 만들기

// 저자: gijsleb
#show math.equation:it => {
  if it.has("label") {
    // `numbering`에서 원하는 번호 매기기 스타일로 
    // 변경하는 것을 잊지 마세요.
    math.equation(block: true, numbering: "(1)", it)
  } else {
    it
  }
}

#show ref: it => {
  let el = it.element
  if el != none and el.func() == math.equation {
    link(el.location(), numbering(
      // 실제로 사용 중인 번호 매기기 방식(예: 섹션 번호 포함)에 
      // 맞춰 변경하는 것을 잊지 마세요.
      "(1)",
      counter(math.equation).at(el.location()).at(0) + 1
    ))
  } else {
    it
  }
}

$ sum_x^2 $
$ dif/(dif x)(A(t)+B(x))=dif/(dif x)A(t)+dif/(dif x)B(t) $ <ep-2>
$ sum_x^2 $
$ dif/(dif x)(A(t)+B(x))=dif/(dif x)A(t)+dif/(dif x)B(t) $ <ep-3>
@ep-2@ep-3 에서 수식을 확인할 수 있습니다.
Rendered image

연산 (Operations)

분수 (Fractions)

$
p/q, p slash q, p\/q
$
Rendered image

약간 이동됨:

#let mfrac(a, b) = move(a, dy: -0.2em) + "/" + move(b, dy: 0.2em, dx: -0.1em)
$A\/B, #mfrac($A$, $B$)$,
Rendered image

큰 분수 (Large fractions)

#let dfrac(a, b) = $display(frac(#a, #b))$

$(x + y)/(1/x + 1/y) quad (x + y)/(dfrac(1,x) + dfrac(1, y))$
Rendered image

첨자 (Scripts)

첨자 및 극한 설정에 대해서는 Typst 기초 섹션을 참조하세요.

아래첨자에 사용되는 모든 문자를 직립체(upright)로 만들기

// 저자: emilyyyylime

$f_a, f_b, f^a, f_italic("word")$
#show math.attach: it => {
  import math: *
  if it.b != none and it.b.func() != upright[].func() and it.b.has("text") and it.b.text.len() == 1 {
    let args = it.fields()
    let _ = args.remove("base")
    let _ = args.remove("b")
    attach(it.base, b: upright(it.b), ..args)
  } else {
    it
  }
}

$f_a, f_b, f^a, f_italic("word")$
Rendered image

벡터 및 행렬

벡터와 행렬마다 간격이 반드시 일정하거나 같지 않다는 것을 쉽게 알 수 있습니다:

$
mat(0, 1, -1; -1, 0, 1; 1, -1, 0) vec(a/b, a/b, a/b) = vec(c, d, e)
$
Rendered image

이는 gap이 요소의 중심 간 거리가 아니라 요소 _사이의 간격_을 의미하기 때문에 발생합니다.

이를 해결하려면 이 스니펫을 사용할 수 있습니다:

// 높이 고정 벡터
#let fvec(..children, delim: "(", gap: 1.5em) = { // 여기서 기본 간격 변경
  context math.vec(
      delim: delim,
      gap: 0em,
      ..for el in children.pos() {
        ({
          box(
            width: measure(el).width,
            height: gap, place(horizon, el)
          )
        },) // 이것은 배열입니다
        // `for`는 이 모든 배열을 병합한 다음, 인수로 전달합니다.
      }
    )
}

// 높이 고정 행렬
// row-gap, column-gap, gap도 허용
#let fmat(..rows, delim: "(", augment: none) = {
  let args = rows.named()
  let (gap, row-gap, column-gap) = (none,)*3;

  if "gap" in args {
    gap = args.at("gap")
    row-gap = args.at("row-gap", default: gap)
    column-gap = args.at("row-gap", default: gap)
  }
  else {
    // 여기서 기본 수직 간격 변경
    row-gap = args.at("row-gap", default: 1.5em) 
    // 그리고 수평 간격은 여기서
    column-gap = rows.named().at("column-gap", default: 0.5em)
  }

  context math.mat(
      delim: delim,
      row-gap: 0em,
      column-gap: column-gap,
      ..for row in rows.pos() {
        (for el in row {
          ({
          box(
            width: measure(el).width,
            height: row-gap, place(horizon, el)
          )
        },)
        }, )
      }
    )
}

$
"Before:"& vec(((a/b))/c, a/b, c) = vec(1, 1, 1)\
"After:"& fvec(((a/b))/c, a/b, c) = fvec(1, 1, 1)\

"Before:"& mat(a, b; c, d) vec(e, dot) = vec(c/d, e/f)\
"After:"& fmat(a, b; c, d) fvec(e, dot) = fvec(c/d, e/f)
$
Rendered image

글꼴 (Fonts)

수학 글꼴 설정

중요: 수학 글꼴로 설정하려는 글꼴은 필요한 수학 기호를 포함하고 있어야 합니다. 그것은 수학이 포함된 특수 글꼴이어야 합니다. 그렇지 않으면 오류 가 발생할 가능성이 매우 높습니다(fallback: false로 설정하고 typst fonts를 확인하여 글꼴을 디버그하세요).

#show math.equation: set text(font: "Fira Math", fallback: false)

$
emptyset \

integral_a^b sum (A + B)/C dif x \
$
Rendered image

수식 내 텍스트 및 콘텐츠

수식 환경($ ... $) 내에서 일반 텍스트나 다른 Typst 요소를 사용하는 방법입니다.

수식 안의 텍스트

따옴표("...")를 사용하여 수식 내에 직립체(upright) 텍스트를 삽입할 수 있습니다.

$ v = d / t quad "속도는 시간분의 거리이다" $
Rendered image

수식 안에서 함수 사용

#을 사용하여 수식 내부에서도 Typst의 모든 기능을 호출할 수 있습니다.

$ integral_0^1 f(x) dif x = #rect(fill: blue.lighten(80%), [결과값]) $
Rendered image

텍스트 스타일 지정

수식 내부의 텍스트에 색상이나 크기를 지정하고 싶을 때 text 함수를 활용합니다.

$ a^2 + b^2 = c^2 quad #text(red)[(피타고라스 정리)] $
Rendered image

수식 내 박스 활용

복잡한 설명을 위해 박스나 블록을 수식 안에 넣을 수 있습니다.

$ x = y + #box(stroke: 1pt, inset: 3pt)[중요 단계] $
Rendered image

서체(Calligraphic) 글자

#let scr(it) = math.class("normal", box({
  show math.equation: set text(stylistic-set: 1)
  $cal(it)$
}))


$ scr(A) scr(B) + scr(C), -scr(D) $
Rendered image

안타깝게도 현재 수학에서 stylistic-set을 그대로 사용하면 간격이 맞지 않는 문제가 발생합니다. 수학 엔진은 기본 글꼴인지 여부에 따라 문자의 간격을 결정하기 때문입니다. 하지만 단순히 "normal"로 설정하는 것만으로는 충분하지 않은데, 그렇게 하면 크기가 줄어들 수 있기 때문입니다. 그래서 이 스니펫은 다소 복잡한 방식(hacky)으로 구현되었습니다(아마도 Typstonomicon에 위치해야 할 수도 있지만, 내용이 충분히 길지는 않습니다).

색상 및 그라디언트

그라디언트

그라디언트는 프레젠테이션이나 단순히 예쁜 외관을 위해 매우 멋질 수 있습니다.

/// author: frozolotl
#set page(paper: "presentation-16-9", margin: 0pt)
#set text(fill: white, font: ("Inter", "Noto Sans CJK KR"))

#let grad = gradient.linear(rgb("#953afa"), rgb("#c61a22"), angle: 135deg)

#place(horizon + left, image(width: 60%, "../img/landscape.png"))

#place(top, polygon(
  (0%, 0%),
  (70%, 0%),
  (70%, 25%),
  (0%, 29%),
  fill: white,
))
#place(bottom, polygon(
  (0%, 100%),
  (100%, 100%),
  (100%, 30%),
  (60%, 30% + 60% * 4%),
  (60%, 60%),
  (0%, 64%),
  fill: grad,
))

#place(top + right, block(inset: 7pt, image(height: 19%, "../img/tub.png")))

#place(bottom, block(inset: 40pt)[
  #text(size: 30pt)[
    프레젠테이션 제목
  ]

  #text(size: 16pt)[#lorem(20) | #datetime.today().display()]
])
Rendered image

예쁜 것들

텍스트 왼쪽에 막대 설정

(인용구 서식이라고도 함)

#let line-block = rect.with(fill: luma(240), stroke: (left: 0.25em))

+ #lorem(10) \
  #line-block[
    *해결책:* #lorem(10)

    $ a_(n+1)x^n = 2... $
  ]
Rendered image

박스 위에 텍스트

// author: gaiajack
#let todo(body) = block(
  above: 2em, stroke: 0.5pt + red,
  width: 100%, inset: 14pt
)[
  #set text(font: "Noto Sans CJK KR", fill: red)
  #place(
    top + left,
    dy: -6pt - 14pt, // Account for inset of block
    dx: 6pt - 14pt,
    block(fill: white, inset: 2pt)[*초안*]
  )
  #body
]

#todo(lorem(100))
Rendered image

책 장식

// author: thevec

#let parSepOrnament = [\ \ #h(1fr) $#line(start:(0em,-.15em), end:(12em,-.15em), stroke: (cap: "round", paint:gradient.linear(white,black,white))) #move(dx:.5em,dy:0em,"🙠")#text(15pt)[🙣] #h(0.4em) #move(dy:-0.25em,text(12pt)[]) #h(0.4em) #text(15pt)[🙡]#move(dx:-.5em,dy:0em,"🙢") #line(start:(0em,-.15em), end:(12em,-.15em), stroke: (cap: "round", paint:gradient.linear(white,black,white)))$ #h(1fr)\ \ ];

#lorem(30)
#parSepOrnament
#lorem(30)
Rendered image

첫 줄 들여쓰기 (First line indent)

공식 문서

텍스트 서식 지정에서 매우 일반적인 요구 사항 중 하나는 모든 단락의 첫 줄에 들여쓰기를 추가하는 것입니다. 일부 언어나 관습에서는 이를 필크로우(pilcrow, ¶) 또는 "빨간 선"이라고 부르기도 합니다.

기본적으로 Typst는 첫 번째 단락(또는 제목 다음에 오는 단락)을 제외한 모든 단락에 들여쓰기를 적용합니다. 이를 변경하려면 (all: true)를 사용하세요:

#set block(spacing: 1.2em)
#set par(
  first-line-indent: 1.5em,
  spacing: 0.65em,
)

첫 번째 단락은 들여쓰기의 
영향을 받지 않습니다.

하지만 두 번째 단락은 영향을 받습니다.

#line(length: 100%)

#set par(first-line-indent: (
  amount: 1.5em,
  all: true,
))

이제 모든 단락이 첫 줄 
들여쓰기의 영향을 받습니다.

첫 번째 단락조차도요.
Rendered image

개별 언어 글꼴 (Individual language fonts)

A cat แปลว่า แมว

#show regex("\p{Thai}+"): text.with(font: "Noto Serif Thai")

A cat แปลว่า 매우(แมว)
Rendered image

가짜 이탤릭 및 텍스트 그림자 (Fake italic & Text shadows)

기울이기 (Skew)

// 저자: Enivex
#set page(width: 21cm, height: 3cm)
#set text(size:25pt)
#let skew(angle,vscale: 1,body) = {
  let (a,b,c,d)= (1,vscale*calc.tan(angle),0,vscale)
  let E = (a + d)/2
  let F = (a - d)/2
  let G = (b + c)/2
  let H = (c - b)/2
  let Q = calc.sqrt(E*E + H*H)
  let R = calc.sqrt(F*F + G*G)
  let sx = Q + R
  let sy = Q - R
  let a1 = calc.atan2(F,G)
  let a2 = calc.atan2(E,H)
  let theta = (a2 - a1) /2
  let phi = (a2 + a1)/2

  set rotate(origin: bottom+center)
  set scale(origin: bottom+center)

  rotate(phi,scale(x: sx*100%, y: sy*100%,rotate(theta,body)))
}

#let fake-italic(body) = skew(-12deg,body)
#fake-italic[이것은 가짜 이탤릭체 텍스트입니다]

#let shadowed(body) = box(place(skew(-50deg, vscale: 0.8, text(fill:luma(200),body)))+place(body))
#shadowed[이것은 그림자가 있는 멋진 텍스트입니다]
Rendered image

특수 문서 (Special documents)

서명란 (Signature places)

#block(width: 150pt)[
  #line(length: 100%)
  #align(center)[서명]
]
Rendered image

프레젠테이션

polylux를 참조하세요.

서식 (Forms)

자리가 있는 서식

#grid(
  columns: 2,
  rows: 4,
  gutter: 1em,

  [학생 이름:],
  [#block()#align(bottom)[#line(length: 10em, stroke: 0.5pt)]],
  [지도 교수:],
  [#block()#align(bottom)[#line(length: 10em, stroke: 0.5pt)]],
  [ID 번호:],
  [#block()#align(bottom)[#line(length: 10em, stroke: 0.5pt)]],
  [학교:],
  [#block()#align(bottom)[#line(length: 10em, stroke: 0.5pt)]],
)
Rendered image

대화형 (Interactive)

프레젠테이션용 대화형 서식이 추가될 예정입니다! 현재 @tinger가 집중적으로 작업 중입니다.

외부 도구와 함께 사용하기

현재 외부와 연동하는 가장 좋은 방법은 다음과 같습니다:

  1. 전처리(Preprocessing): 외부 도구가 Typst 파일을 생성하도록 합니다.
  2. Typst 쿼리 (CLI): 여기의 문서를 참조하세요.
  3. WebAssembly 플러그인: 여기의 문서를 참조하세요.

조만간 처음 두 가지 방법에 대한 성공적인 사용 사례 예시가 추가될 예정입니다. 세 번째 방법에 대해서는 패키지 섹션을 참조하세요.

패키지 (Packages)

Typst Universe가 런칭된 이후, 이 장은 거의 불필요해졌습니다. Universe는 패키지를 찾기에 매우 훌륭한 곳입니다.

그럼에도 불구하고, 여전히 흥미로운 패키지 사용 예시들이 여기에 남아 있습니다.

일반 사항

Typst에는 패키지가 있지만, LaTeX와 달리 다음 사항을 기억해야 합니다:

  • 패키지는 일부 전문적인 작업에만 필요하며, 기본적인 포맷팅은 패키지 없이도 충분히 가능합니다.
  • 패키지는 LaTeX 패키지보다 훨씬 가볍고 "설치"하기 쉽습니다.
  • 패키지는 평범한 Typst 파일(때로는 플러그인)일 뿐이므로, 직접 작성하기도 쉽습니다!

강력한 패키지를 사용하려면 다음과 같이 작성하면 됩니다:

#import "@preview/cetz:0.3.4": canvas, draw
#import "@preview/cetz-plot:0.1.1": plot

#set page(width: auto, height: auto, margin: .5cm)

#let style = (stroke: black, fill: rgb(0, 0, 200, 75))

#let f1(x) = calc.sin(x)
#let fn = (
  ($ x - x^3"/"3! $, x => x - calc.pow(x, 3)/6),
  ($ x - x^3"/"3! - x^5"/"5! $, x => x - calc.pow(x, 3)/6 + calc.pow(x, 5)/120),
  ($ x - x^3"/"3! - x^5"/"5! - x^7"/"7! $, x => x - calc.pow(x, 3)/6 + calc.pow(x, 5)/120 - calc.pow(x, 7)/5040),
)

#set text(size: 10pt)

#canvas({
  import draw: *

  // 가느다란 축 스타일 설정
  set-style(axes: (stroke: .5pt, tick: (stroke: .5pt)),
            legend: (stroke: none, orientation: ttb, item: (spacing: .3), scale: 80%))

  plot.plot(size: (12, 8),
    x-tick-step: calc.pi/2,
    x-format: plot.formats.multiple-of,
    y-tick-step: 2, y-min: -2.5, y-max: 2.5,
    legend: "inner-north",
    {
      let domain = (-1.1 * calc.pi, +1.1 * calc.pi)

      for ((title, f)) in fn {
        plot.add-fill-between(f, f1, domain: domain,
          style: (stroke: none), label: title)
      }
      plot.add(f1, domain: domain, label: $ sin x  $,
        style: (stroke: black))
    })
})
Rendered image

기여하기

패키지 제작자이거나 공정한 리뷰를 작성하고 싶다면 언제든지 Issue나 PR을 남겨주세요!

테마 (Themes)

Typst에서 테마는 주로 패키지 형태로 제공되며, 문서 전체의 스타일(setshow 규칙)을 한 번에 적용하는 함수로 구현됩니다.

프레젠테이션 테마: Touying

Touying은 현재 Typst에서 가장 강력한 프레젠테이션 패키지 중 하나입니다. 다양한 내장 테마를 제공합니다.

#import "@preview/touying:0.5.3": *
#import themes.university: *

#show: university-theme.with(
  aspect-ratio: "16-9",
  config-info: (
    title: [테마가 적용된 프레젠테이션],
    author: [저자 성함],
    institution: [소속 기관],
  ),
)

== 첫 번째 슬라이드
테마가 깔끔하게 적용되었습니다.

- 항목 1
- 항목 2
Rendered image

이력서 테마: Modern CV

이미 데모에서 보았듯이, modern-cv와 같은 패키지는 전문적인 이력서 테마를 제공합니다.

#import "@preview/modern-cv:0.8.0": *

#show: resume.with(
  author: (
    firstname: "길동",
    lastname: "",
    positions: ("전문가", "개발자"),
  ),
  profile-picture: none,
  font: "Noto Sans CJK KR",
)

= 경험
- Typst를 이용한 문서 자동화 전문가
Rendered image

테마 패키지 찾는 법

더 많은 테마와 템플릿은 Typst Universe의 'Templates' 카테고리에서 확인할 수 있습니다.

  • 학술 논문: IEEE, ACM, Elsevier 등 주요 학회 템플릿
  • 도서: book 스타일 템플릿
  • 공식 문서: 각국 정부 또는 기관의 표준 서식 템플릿

레이아웃 (Layouting)

일반적으로 유용한 것들입니다.

Pinit: 핀(pins)을 이용한 상대적 배치

pinit의 개념은 일반적인 텍스트 흐름 위에 핀을 꽂고, 그 핀을 기준으로 콘텐츠를 배치하는 것입니다.

#import "@preview/pinit:0.2.2": *
#set page(height: 6em, width: 20em)

#set text(size: 24pt)

간단한 #pin(1)강조된 텍스트#pin(2)입니다.

#pinit-highlight(1, 2)

#pinit-point-from(2)[단순합니다.]
Rendered image

더 복잡한 예시:

#import "@preview/pinit:0.2.2": *

// 페이지 설정
#set page(paper: "presentation-4-3")
#set text(size: 20pt)
#show heading: set text(weight: "regular")
#show heading: set block(above: 1.4em, below: 1em)
#show heading.where(level: 1): set text(size: 1.5em)

// 유용한 함수들
#let crimson = rgb("#c00000")
#let greybox(..args, body) = rect(fill: luma(95%), stroke: 0.5pt, inset: 0pt, outset: 10pt, ..args, body)
#let redbold(body) = {
  set text(fill: crimson, weight: "bold")
  body
}
#let blueit(body) = {
  set text(fill: blue)
  body
}

// 본문
#block[
  = 점근 표기법: $O$

  #pin("h1")점근 표기법#pin("h2")을 사용하여 알고리즘의 점근적 효율성을 설명합니다.
  (상수 계수와 낮은 차수의 항은 무시합니다.)

  #greybox[
    함수 $g(n)$이 주어졌을 때, $O(g(n))$으로 다음 *함수들의 집합*을 나타냅니다:
    #redbold(${f(n): "존재함" c > 0 "" n_0 > 0, "다음 조건을 만족함" f(n) <= c dot g(n) "모든" n >= n_0 "에 대하여"}$)
  ]

  #pinit-highlight("h1", "h2")

  $f(n) = O(g(n))$: #pin(1)$f(n)$$g(n)$보다 *점근적으로 작습니다*.#pin(2)

  $f(n) redbold(in) O(g(n))$: $f(n)$*점근적으로* #redbold[최대] $g(n)$입니다.

  #pinit-line(stroke: 3pt + crimson, start-dy: -0.25em, end-dy: -0.25em, 1, 2)

  #block[삽입 정렬을 #pin("r1")예시#pin("r2")로 들면:]

  - 최선의 경우: $T(n) approx c n + c' n - c''$ #pin(3)
  - 최악의 경우: $T(n) approx c n + (c' \/ 2) n^2 - c''$ #pin(4)

  #pinit-rect("r1", "r2")

  #pinit-place(3, dx: 15pt, dy: -15pt)[#redbold[$T(n) = O(n)$]]
  #pinit-place(4, dx: 15pt, dy: -15pt)[#redbold[$T(n) = O(n)$]]

  #blueit[Q: $n^(3) = O(n^2)$#pin("que") 인가요? 답변을 어떻게 증명할까요#pin("ans")?]

  #pinit-point-to("que", fill: crimson, redbold[아니요.])
  #pinit-point-from("ans", body-dx: -150pt)[
    방정식 $(3/2)^n >= c$\
    $n$에 대해 무수히 많은 해를 가짐을 보이세요.
  ]
]
Rendered image

여백 메모 (Margin notes)

#import "@preview/drafting:0.2.2": *

#let (l-margin, r-margin) = (1in, 2in)
#set page(
  margin: (left: l-margin, right: r-margin, rest: 0.1in),
)
#set-page-properties(margin-left: l-margin, margin-right: r-margin)

= 여백 메모
== 설정
안타깝게도 `typst`는 호출 함수에 여백 정보를 노출하지 않으므로, 명시적으로 설정해야 합니다. 이는 *콘텐츠를 배치하기 전* 에 `set-page-properties`를 사용하여 수행됩니다:

// 소스 파일 상단에서
// 물론 원하는 여백 숫자로 대체할 수 있습니다. 
// 단, 페이지 여백이 `set-page-properties`에 전달하는 값과 일치해야 합니다.

== 기본 사용법
#lorem(20)
#margin-note(side: left)[안녕, 세상아!]
#lorem(10)
#margin-note[반대편에서 인사드립니다]

#lorem(25)
#margin-note[메모가 겹치려고 하면 자동으로 이동됩니다]
#margin-note(stroke: aqua + 3pt)[충돌을 피하기 위해]
#lorem(25)

#let caution-rect = rect.with(inset: 1em, radius: 0.5em, fill: orange.lighten(80%))
#inline-note(rect: caution-rect)[
  4개 이상의 메모가 겹치면 충돌 방지가 중단될 수 있습니다. 
  이는 `typst`가 5번의 시도(초기 레이아웃 + 각 메모에 대한 조정) 후에도 
  레이아웃이 해결되지 않으면 경고를 보내기 때문입니다.
]
Rendered image
#import "@preview/drafting:0.2.2": *

#let (l-margin, r-margin) = (1in, 2in)
#set page(
  margin: (left: l-margin, right: r-margin, rest: 0.1in),
)
#set-page-properties(margin-left: l-margin, margin-right: r-margin)

== 기본 스타일 조정
모듈 상태를 업데이트하여 모든 함수의 기본값을 사용자 정의할 수 있습니다:

#lorem(4) #margin-note(dy: -2em)[기본 스타일]
#set-margin-note-defaults(stroke: orange, side: left)
#lorem(4) #margin-note[업데이트된 스타일]


기본 `rect`를 재정의하여 더 깊은 수준의 사용자 정의도 가능합니다:

#import "@preview/colorful-boxes:1.1.0": stickybox

#let default-rect(stroke: none, fill: none, width: 0pt, content) = {
  stickybox(rotation: 30deg, width: width/1.5, content)
}
#set-margin-note-defaults(rect: default-rect, stroke: none, side: right)

#lorem(20)
#margin-note(dy: -25pt)[여백에 스티키 메모를 사용해 보는 건 어떨까요?]

// 이전 예제의 변경 사항 취소
#set-margin-note-defaults(rect: rect, stroke: red)

== 여러 명의 문서 리뷰어
#let reviewer-a = margin-note.with(stroke: blue)
#let reviewer-b = margin-note.with(stroke: purple)
#lorem(20)
#reviewer-a[리뷰어 A의 의견]
#lorem(15)
#reviewer-b(side: left)[리뷰어 B의 의견]

== 인라인 메모 (Inline Notes)
#lorem(10)
#inline-note[기본 인라인 메모는 해당 위치에서 단락을 분리합니다]
#lorem(10)
/*
// 작동해야 하지만 안 됨? 저장소에 이슈 생성함.
#inline-note(par-break: false, stroke: (paint: orange, dash: "dashed"))[
  하지만 `par-break: false`를 지정하여 이를 방지할 수 있습니다
]
*/
#lorem(10)
Rendered image
#import "@preview/drafting:0.2.2": *

#let (l-margin, r-margin) = (1in, 2in)
#set page(
  margin: (left: l-margin, right: r-margin, rest: 0.1in),
)
#set-page-properties(margin-left: l-margin, margin-right: r-margin)

== 인쇄 미리보기를 위해 메모 숨기기
#set-margin-note-defaults(hidden: true)

#lorem(20)
#margin-note[이것은 전역 "숨김" 상태를 따릅니다]
#margin-note(hidden: false, dy: -2.5em)[이 메모는 절대로 숨겨지지 않습니다]

= 위치 지정 (Positioning)
== 정밀 배치: 가이드 그리드 (rule grid)
정밀한 위치 지정을 위해 공간 측정이 필요하신가요? `rule-grid`를 사용하여 
페이지에 가이드 라인을 그릴 수 있습니다:

#rule-grid(width: 10cm, height: 3cm, spacing: 20pt)
#place(
  dx: 180pt,
  dy: 40pt,
  rect(fill: white, stroke: red, width: 1in, "(180pt, 40pt)에서 시작합니다")
)

// 선택적으로 가장 작은 차원의 분할 수를 지정하여 간격을 자동으로 계산합니다
#rule-grid(dx: 10cm + 3em, width: 3cm, height: 1.2cm, divisions: 5, square: true,  stroke: green)

// 가이드 그리드는 공간을 차지하지 않으므로 명시적으로 추가합니다
#v(3cm + 1em)

== 절대 위치 지정
여백이나 상대적 위치에 상관없이 무언가를 절대적으로 배치하고 싶으신가요? 
`absolute-place`가 도와드립니다. 콘텐츠를 어디에나 둘 수 있습니다:

#context {
  let (dx, dy) = (25%, here().position().y)
  let content-str = (
    "이 절대 배치된 박스는 페이지 좌표 (" + repr(dx) + ", " + repr(dy) + ")"
    + "에서 시작합니다"
  )
  absolute-place(
    dx: dx, dy: dy,
    rect(
      fill: green.lighten(60%),
      radius: 0.5em,
      width: 2.5in,
      height: 0.5in,
      [#align(center + horizon, content-str)]
    )
  )
}
#v(1in)

`rule-grid`는 `relative: false`를 전달하여 페이지 왼쪽 상단에서의 절대 배치를 지원합니다. 
페이지 전체에 "가이드 라인"을 그리는 데 유용합니다.
Rendered image

첫 글자 장식 (Dropped capitals)

자세한 정보는 여기에서 확인하세요.

기본 사용법

#import "@preview/droplet:0.3.1": dropcap

#dropcap(gap: -2pt, hanging-indent: 8pt)[
  #lorem(42)
]
Rendered image

확장 스타일링

#import "@preview/droplet:0.3.1": dropcap

#dropcap(
  height: 2,
  justify: true,
  gap: 6pt,
  transform: letter => context {
    let height = measure(letter).height

    grid(columns: 2, gutter: 6pt,
      align(center + horizon, text(blue, letter)),
      // "place"를 사용하여 나중에 글꼴 크기가 계산될 때 
      // 선의 높이를 무시하도록 합니다.
      place(horizon, line(
        angle: 90deg,
        length: height + 6pt,
        stroke: blue.lighten(40%) + 1pt
      )),
    )
  }
)[
  #lorem(42)
]
Rendered image

실제 현재 장의 제목 표시

hydra를 참조하세요.

#import "@preview/hydra:0.6.1": hydra

#set page(header: context hydra() + line(length: 100%))
#set heading(numbering: "1.1")
#show heading.where(level: 1): it => pagebreak(weak: true) + it

= 서론
#lorem(750)

= 본문
== 첫 번째 섹션
#lorem(500)
== 두 번째 섹션
#lorem(250)
== 세 번째 섹션
#lorem(500)

= 부록 (Annex)
#lorem(10)
Rendered image
Rendered image
Rendered image
Rendered image
Rendered image

그림 감싸기 (Wrapping figures)

그림 감싸기에 대한 더 나은 네이티브 지원이 계획되어 있지만, 현재 패키지를 통해 일부 기능이 가능합니다:

#import "@preview/wrap-it:0.1.1": wrap-content, wrap-top-bottom

#set par(justify: true)
#let fig = figure(
  rect(fill: teal, radius: 0.5em, width: 8em),
  caption: [그림 예시],
)
#let body = lorem(40)
#wrap-content(fig, body)

#wrap-content(
  fig,
  body,
  align: bottom + right,
  column-gutter: 2em
)

#let boxed = box(fig, inset: 0.5em)
#wrap-content(boxed)[
  #lorem(40)
]

#let fig2 = figure(
  rect(fill: lime, radius: 0.5em),
  caption: [또 다른 그림],
)
#wrap-top-bottom(boxed, fig2, lorem(60))
Rendered image
제한 사항: 감싸는 부분 근처의 간격이 이상적이지 않을 수 있으며, 상단-하단 왼쪽/오른쪽 배치만 지원됩니다.

기타 (Misc)

hydra () outrageous (개요 스타일링, 정렬된 반복 점선 기능 포함 예정)

문자열 포맷팅

oxifmt: 범용 문자열 포맷터

#import "@preview/oxifmt:0.2.1": strfmt
#strfmt("저는 {}입니다. 차를 {num}대 가지고 있어요. 저는 {0}입니다. {}는 {{멋져요}}.", "철수", "길동", num: 10) \
#strfmt("{0:?}, {test:+012e}, {1:-<#8x}", "안녕", -74, test: 569.4) \
#strfmt("{:_>+11.5}", 59.4) \
#strfmt("딕셔너리: {:!<10?}", (a: 5))
Rendered image
#import "@preview/oxifmt:0.2.1": strfmt
#strfmt("첫 번째: {}, 두 번째: {}, 네 번째: {3}, 바나나: {banana} (중괄호: {{이스케이프}})", 1, 2.1, 3, label("four"), banana: "바나나!!")\
#strfmt("값은: {:?} | 레이블은 {:?}", "무언가", label("레이블"))\
#strfmt("값들: {:?}, {1:?}, {stuff:?}", (test: 500), ("a", 5.1), stuff: [a])\
#strfmt("좌측5 {:_<5}, 우측6 {:*>6}, 중앙10 {centered: ^10?}, 좌측3 {tleft:_<3}", "xx", 539, tleft: "좋아요", centered: [a])\
Rendered image
#import "@preview/oxifmt:0.2.1": strfmt
#repr(strfmt("좌측 패딩7 숫자: {:07} {:07} {:07} {3:07}", 123, -344, 44224059, 45.32))\
#strfmt("일부 숫자: {:+} {:+08}; 채우기와 정렬 포함: {:_<+8}; 음수 (무시): {neg:+}", 123, 456, 4444, neg: -435)\
#strfmt("진법 (10, 2, 8, 16(소문자), 16(대문자):) {0} {0:b} {0:o} {0:x} {0:X} | 접두사와 수정자 포함: {0:#b} {0:+#09o} {0:_>+#9X}", 124)\
#strfmt("{0:.8} {0:.2$} {0:.potato$}", 1.234, 0, 2, potato: 5)\
#strfmt("{0:e} {0:E} {0:+.9e} | {1:e} | {2:.4E}", 124.2312, 50, -0.02)\
#strfmt("{0} {0:.6} {0:.5e}", 1.432, fmt-decimal-separator: ",")
Rendered image

name-it: 숫자를 텍스트로 변환

#import "@preview/name-it:0.1.0": name-it

- #name-it(2418345)
Rendered image

nth: N번째 요소

#import "@preview/nth:0.2.0": nth
#nth(3), #nth(5), #nth(2421)
Rendered image

헤더 (Headers)

hydra: 문맥 의존적 헤더

Typst 기초 섹션에서 query(selector(heading).before(here()))를 사용하여 현재 제목을 가져와 헤더를 만드는 방법을 논의했습니다. 하지만 이 방식은 번호가 매겨진 중첩된 제목 등에서 제대로 작동하지 않는 경우가 있습니다. 이런 상황을 위해 hydra 패키지가 있습니다:

#import "@preview/hydra:0.6.1": hydra

#set page(height: 10 * 20pt, margin: (y: 4em), numbering: "1", header: context {
  if calc.odd(here().page()) {
    align(right, emph(hydra(1)))
  } else {
    align(left, emph(hydra(2)))
  }
  line(length: 100%)
})
#set heading(numbering: "1.1")
#show heading.where(level: 1): it => pagebreak(weak: true) + it

= 서론
#lorem(50)

= 본문
== 첫 번째 섹션
#lorem(50)
== 두 번째 섹션
#lorem(100)
Rendered image
Rendered image
Rendered image
Rendered image

용어 사전 (Glossary)

glossarium

Universe 링크

용어 사전과 약어를 관리하는 패키지입니다.

Typst의 아주 초기 패키지 중 하나로, (아마도) Typst로 작성된 첫 번째 학위 논문을 위해 특별히 제작되었습니다.
#import "@preview/glossarium:0.5.4": make-glossary, register-glossary, print-glossary, gls, glspl
#show: make-glossary

// 링크 시인성을 높이기 위해
#show link: set text(fill: blue.darken(60%))

#let entry-list = (
    (
    // 최소 용어
    (key: "kuleuven", short: "KU Leuven"),

    // 긴 형태와 그룹이 있는 용어
    (key: "unamur", short: "UNamur", long: "남루르 대학교", group: "대학"),

    // 마크업 설명이 있는 용어
    (
      key: "oidc",
      short: "OIDC",
      long: "OpenID Connect",
      description: [OpenID는 비영리 단체인 
      #link("https://en.wikipedia.org/wiki/OpenID#OpenID_Foundation")[OpenID Foundation]에서 추진하는 오픈 표준이자 분산형 인증 프로토콜입니다.],
      group: "약어",
    ),

    // 짧은 복수형이 있는 용어
    (
      key: "potato",
      short: "potato",
      // "plural"은 "short"를 복수형으로 만들 때 사용됩니다.
      plural: "potatoes",
      description: [#lorem(10)],
    ),

    // 긴 복수형이 있는 용어
    (
      key: "dm",
      short: "DM",
      long: "대각 행렬 (diagonal matrix)",
      // "longplural"은 "long"을 복수형으로 만들 때 사용됩니다.
      longplural: "대각 행렬들 (diagonal matrices)",
      description: "아마도 어떤 수학 관련 내용인 것 같습니다.",
    ),
  )
)

#register-glossary(entry-list)

// 문서 본문
#print-glossary(
 entry-list
)

// gls를 사용하여 OIDC 용어 참조
#gls("oidc")
// 강제로 긴 형태 표시
#gls("oidc", long: true)

// 참조 구문을 사용하여 OIDC 용어 참조
@oidc

복수형: #glspl("potato")

#gls("oidc", display: "원하는 대로 표시")
Rendered image

단어 수 세기 (Counting words)

Wordometr

#import "@preview/wordometer:0.1.4": word-count, total-words

#show: word-count

이 문서에는 총 #total-words 개의 단어가 있습니다.

#word-count(total => [
  이 블록의 단어 수는 #total.words 개이고, 
  글자 수는 #total.characters 개입니다.
])
Rendered image

요소 제외하기

이름(예: "caption"), 함수(예: figure.caption), where-선택자(예: raw.where(block: true)), 또는 레이블(예: <no-wc>)을 사용하여 단어 계산에서 특정 요소를 제외할 수 있습니다.

#import "@preview/wordometer:0.1.4": word-count, total-words

#show: word-count.with(exclude: (heading.where(level: 1), strike))

= 이 제목은 수치에 포함되지 않습니다
== 하지만 저는 포함됩니다!

이 문서에는 #strike[(저를 제외하고)]#total-words 개의 단어가 있습니다.

#word-count(total => [
  레이블을 사용하여 요소를 제외할 수도 있습니다.
  #[이 문장을 제외하고 #total-words 개였습니다!] <no-wc>
], exclude: <no-wc>)
Rendered image

외부 (External)

공식 패키지는 아니지만, 언젠가 공식 패키지가 될 수도 있는 도구들입니다.

매우 유용할 수 있습니다.

트리맵(Treemap) 표시

코드 링크

트리맵 다이어그램

Typstonomicon: 작성하지 말아야 할 코드

현재 Typst의 한계를 극복하기 위해 쿼리(query), 측정(measure) 및 기타 여러 가지 기법을 사용한 아주 위험한(cursed) 예시들입니다. 일반적으로 이 코드는 정말로 필요한 경우에만 사용해야 합니다.

이 장의 코드는 많은 상황에서 오작동할 수 있으며 디버깅이 매우 고통스러울 수 있습니다. 경고했습니다.

이 장의 내용은 Typst가 성숙해짐에 따라 서서히 사라질 것으로 생각합니다.

단어 수 세기 (Word count)

이 장은 이제 더 이상 권장되지 않습니다(deprecated). 곧 제거될 예정입니다.

권장되는 해결책

wordometr 패키지를 사용하세요:

#import "@preview/wordometer:0.1.4": word-count, total-words

#show: word-count

이 문서에는 총 #total-words 개의 단어가 있습니다.

#word-count(total => [
  이 블록의 단어 수는 #total.words 개이고, 
  글자 수는 #total.characters 개입니다.
])
Rendered image

문서의 모든 단어 세기

// 원저자: laurmaedje
#let words = counter("words")
#show regex("\p{L}+"): it => it + words.step()

== 제목
#lorem(50)

=== 강조된 장
#strong(lorem(25))

// 주석은 무시됩니다

#align(right)[(#context words.display() 단어)]
Rendered image

특정 요소만 세고 나머지는 무시하기

// 원저자: jollywatt
#let count-words(it) = {
    let fn = repr(it.func())
    if fn == "sequence" { it.children.map(count-words).sum() }
    else if fn == "text" { it.text.split().len() }
    else if fn in ("styled") { count-words(it.child) }
    else if fn in ("highlight", "item", "strong", "link") { count-words(it.body) }
    else if fn in ("footnote", "heading", "equation") { 0 }
    else { 0 }
}

#show: rest => {
    let n = count-words(rest)
    rest + align(right, [(#n 단어)])
}

== 제목 (포함되지 않아야 함)
#lorem(50)

=== 강조된 장
#strong(lorem(25)) // 이것도 세어집니다!
Rendered image

Try & Catch

// 저자: laurmaedje
// 이미지를 렌더링하거나, 존재하지 않는 경우 자리 표시자(placeholder)를 렌더링합니다.
// 위험하므로 따라 하지 마세요!
#let maybe-image(path, ..args) = context {
  let path-label = label(path)
   let first-time = query((context {}).func()).len() == 0
   if first-time or query(path-label).len() > 0 {
    [#image(path, ..args)#path-label]
  } else {
    rect(width: 50%, height: 5em, fill: luma(235), stroke: 1pt)[
      #set align(center + horizon)
      #raw(path) 파일을 찾을 수 없습니다.
    ]
  }
}

#maybe-image("../tiger.jpg")
#maybe-image("../tiger1.jpg")
Rendered image

끊어진 블록의 중단점 (Breakpoints on broken blocks)

테이블 헤더 및 푸터를 사용한 구현

여기에서 데모 프로젝트를 확인하세요(더 많은 주석이 있으며, 일부는 제거했습니다).

/// author: isuffix

// 기본 카운터 및 지그재그 함수
#let counter-family(id) = {
  let parent = counter(id)
  let parent-step() = parent.step()
  let get-child() = counter(id + str(parent.get().at(0)))
  return (parent-step, get-child)
}

// 재미있는 지그재그 선!
#let zig-zag(fill: black, rough-width: 6pt, height: 4pt, thick: 1pt, angle: 0deg) = {
  layout((size) => {
    // layout을 사용하여 크기를 얻고 수평 거리를 측정
    // 그 다음 수학을 사용하여 지그재그 당 너비 구함
    let count = int(calc.round(size.width / rough-width))
    // `h(-thick)`으로 결합하므로 추가 두께를 더해야 함
    let width = thick + (size.width - thick) / count
    // 지그(Zig)와 재그(Zag):
    let zig-and-zag = {
      let line-stroke = stroke(thickness: thick, cap: "round", paint: fill)
      let top-left = (thick/2, thick/2)
      let bottom-mid = (width/2, height - thick/2)
      let top-right = (width - thick/2, thick/2)
      let zig = line(stroke: line-stroke, start: top-left, end: bottom-mid)
      let zag = line(stroke: line-stroke, start: bottom-mid, end: top-right)
      box(place(zig) + place(zag), width: width, height: height, clip: true)
    }
    let zig-zags = ((zig-and-zag,) * count).join(h(-thick))
    rotate(zig-zags, angle)
  })
}

// ---- split-box 정의 ---- //

// 사용자 정의 가능한 split-box 테두리 옵션:
#let default-border = (
  // 시작 및 끝 선
  above: line(length: 100%),
  below: line(length: 100%),
  // 여러 페이지에 걸쳐 상자 사이에 넣을 선
  btwn-above: line(length: 100%, stroke: (dash:"dotted")),
  btwn-below: line(length: 100%, stroke: (dash:"dotted")),
  // 왼쪽/오른쪽 선
  // 이들은 *반드시* `grid.vline()`을 사용해야 하며, 그렇지 않으면 오류가 발생합니다.
  // 선을 제거하려면 `grid.vline(stroke: none)`으로 설정하세요.
  // rowspan으로 더 잘 구성할 수도 있겠지만, 귀찮네요.
  left: grid.vline(),
  right: grid.vline(),
)

// 여러 페이지/열에 걸치고
// 열 중단 위아래에 사용자 정의 테두리가 있는 상자 생성
#let split-box(
  // 테두리 딕셔너리 설정, 옵션은 위의 `default-border` 참조
  border: default-border,
  // 콘텐츠를 배치할 셀, `grid.cell`로 해석되어야 함
  cell: grid.cell.with(inset: 5pt),
  // 마지막 위치 인수(들)은 실제 콘텐츠입니다.
  // 추가 명명된 인수는 호출 시 기본 그리드로 전송됩니다.
  // fill, align 등에 유용합니다.
  ..args
) = {
  // 더 많은 정보는 `utils.typ` 참조.
  let (parent-step, get-child) = counter-family("split-box-unique-counter-string")
  parent-step() // 부모 카운터를 한 번 배치.
  // 헤더가 페이지에 배치될 때마다 추적.
  // 그런 다음 첫 번째 배치(헤더)인지 마지막(푸터)인지 확인.
  // 그렇지 않다면 테두리 선의 'between' 형태를 사용.
  let border-above = context {
    let header-count = get-child()
    header-count.step()
    context if header-count.get() == (1,) { border.above } else { border.btwn-above }
  }
  let border-below = context {
    let header-count = get-child()
    if header-count.get() == header-count.final() { border.below } else { border.btwn-below }
  }
  // 그리드 배치!
  grid(
    ..args.named(),
    columns: 3,
    border.left,
    grid.header(border-above , repeat: true),
    ..args.pos().map(cell),
    grid.footer(border-below, repeat: true),
    border.right,
  )
}

// ---- 예제 ---- //

#set page(width: 7.2in, height: 3in, columns: 6)

// 짜잔!
#split-box[
  #lorem(20)
]

// 재미있는 예제:

#let fun-border = (
  // 그라디언트!
  above: line(length: 100%, stroke: 2pt + gradient.linear(..color.map.rainbow)),
  below: line(length: 100%, stroke: 2pt + gradient.linear(..color.map.rainbow, angle: 180deg)),
  // 지그재그!
  btwn-above: move(dy: +2pt, zig-zag(fill: blue, angle: 3deg)),
  btwn-below: move(dy: -2pt, zig-zag(fill: orange, angle: 177deg)),
  left: grid.vline(stroke: (cap: "round", paint: purple)),
  right: grid.vline(stroke: (cap: "round", paint: purple)),
)

#split-box(border: fun-border)[
  #lorem(25)
]

// 그리고 좀 더 얌전한 친구들:

#split-box(border: (
  above: move(dy: -0.5pt, line(length: 100%)),
  below: move(dy: +0.5pt, line(length: 100%)),
  // 지그재그!
  btwn-above: move(dy: -1.1pt, zig-zag()),
  btwn-below: move(dy: +1.1pt, zig-zag(angle: 180deg)),
  left: grid.vline(stroke: (cap: "round")),
  right: grid.vline(stroke: (cap: "round")),
))[
  #lorem(10)
]

#split-box(
  border: (
    above: line(length: 100%, stroke: luma(50%)),
    below: line(length: 100%, stroke: luma(50%)),
    btwn-above: line(length: 100%, stroke: (dash: "dashed", paint: luma(50%))),
    btwn-below: line(length: 100%, stroke: (dash: "dashed", paint: luma(50%))),
    left: grid.vline(stroke: none),
    right: grid.vline(stroke: none),
  ),
  cell: grid.cell.with(inset: 5pt, fill: color.yellow.saturate(-85%))
)[
  #lorem(20)
]
Rendered image

헤더, 푸터 및 상태(stated)를 통한 구현

제한 사항: 단일 열 레이아웃과 한 번의 중단에서만 작동합니다.
#let countBoundaries(loc, fromHeader) = {
  let startSelector = selector(label("boundary-start"))
  let endSelector = selector(label("boundary-end"))

  if fromHeader {
    // 페이지 상단에서 카운트 다운
    startSelector = startSelector.after(loc)
    endSelector = endSelector.after(loc)
  } else {
    // 페이지 하단에서 카운트 업
    startSelector = startSelector.before(loc)
    endSelector = endSelector.before(loc)
  }

  let startMarkers = query(startSelector)
  let endMarkers = query(endSelector)
  let currentPage = loc.position().page

  let pageStartMarkers = startMarkers.filter(elem =>
    elem.location().position().page == currentPage)

  let pageEndMarkers = endMarkers.filter(elem =>
    elem.location().position().page == currentPage)

  (start: pageStartMarkers.len(), end: pageEndMarkers.len())
}

#set page(
  margin: 2em,
  // ... 다른 페이지 설정 ...
  header: context {
    let boundaryCount = countBoundaries(here(), true)

    if boundaryCount.end > boundaryCount.start {
      // 이 헤더를 여는 장식으로 꾸미기
      [Block break top: $-->$]
    }
  },
  footer: context {
    let boundaryCount = countBoundaries(here(), false)

    if boundaryCount.start > boundaryCount.end {
      // 이 푸터를 닫는 장식으로 꾸미기
      [Block break end: $<--$]
    }
  }
)

#let breakable-block(body) = block({
  [
    #metadata("boundary") <boundary-start>
  ]
  stack(
    // 분리 가능한 목록 콘텐츠가 여기에 들어감
    body
  )
  [
    #metadata("boundary") <boundary-end>
  ]
})

#set page(height: 10em)

#breakable-block[
    #([Something \ ]*10)
]
Rendered image
Rendered image

일반 텍스트 추출

// 원저자: ntjess
#let stringify-by-func(it) = {
  let func = it.func()
  return if func in (parbreak, pagebreak, linebreak) {
    "\n"
  } else if func == smartquote {
    if it.double { "\"" } else { "'" } // "
  } else if it.fields() == (:) {
    // 필드가 없는 요소는 특별히 표현되거나(미리 처리됨) 텍스트가 없음
    ""
  } else {
    panic("타입 `" + repr(func) + "`을(를) 처리하는 방법을 모르겠습니다.")
  }
}

#let plain-text(it) = {
  return if type(it) == str {
    it
  } else if it == [ ] {
    " "
  } else if it.has("children") {
    it.children.map(plain-text).join()
  } else if it.has("body") {
    plain-text(it.body)
  } else if it.has("text") {
    if type(it.text) == str {
      it.text
    } else {
      plain-text(it.text)
    }
  } else {
    // 다른 텍스트가 아닌 요소를 무시하려면 이 부분을 제거하세요
    stringify-by-func(it)
  }
}

#plain-text(`raw 인라인 텍스트`)

#plain-text(highlight[강조된 텍스트])

#plain-text[목록
  - 일부
  - 요소를
  - 포함함

  + 그리고
  + 번호 매기기도
  + 포함함
]

#plain-text(underline[밑줄])

#plain-text($sin(x + y)$)

#for el in (
  circle,
  rect,
  ellipse,
  block,
  box,
  par,
  raw.with(block: true),
  raw.with(block: false),
  heading,
) {
  plain-text(el(repr(el)))
  linebreak()
}

// 일부 빈 요소들
#plain-text(circle())
#plain-text(line())

#for spacer in (linebreak, pagebreak, parbreak) {
  plain-text(spacer())
}
Rendered image

무언가를 다른 무언가와 가로 정렬하기

// 저자: tabiasgeehuman
#let inline-with(select, content) = context {
  let target = query(
    selector(select)
  ).last().location().position().x
  let current = here().position().x

  box(inset: (x: target - current + 0.3em), content)
}

#let inline-label(name) = [#line(length: 0%) #name]

#inline-with(selector(<start-c>))[= 공통 값]
#align(left, box[$
    #inline-label(<start-c>) ""(0) =& 0 \
    lim_(x -> 1) ""(0) =& 0
$])
Rendered image

0단계 장(Chapter) 생성하기

// 저자: tinger

#let chapter = figure.with(
  kind: "chapter",
  // 제목과 동일
  numbering: none,
  // 제목처럼 자동으로 번역될 수 없으므로 figure와 다른 방식으로 처리됩니다.
  supplement: "Chapter",
  // 개요(outline)에 포함되기 위해 필요한 빈 캡션
  caption: [],
)

// show 규칙을 만들어 요소 함수처럼 에뮬레이트합니다.
#show figure.where(kind: "chapter"): it => {
  set text(22pt)
  counter(heading).update(0)
  if it.numbering != none { strong(it.counter.display(it.numbering)) } + [ ] + strong(it.body)
}

// outline(indent: it => ...)에서 요소에 접근할 수 없으므로, 
// outline 내부가 아닌 여기서 들여쓰기를 처리해야 합니다.
#show outline.entry: it => {
  if it.element.func() == figure {
    // 여기서 장(chapter) 출력을 설정하며, 약간의 수정을 더해 기본 show 구현을 효과적으로 재현합니다.
    let res = link(it.element.location(), 
      // 위에서 만든 show 규칙의 일부를 다시 재현해야 합니다.
      if it.element.numbering != none {
        numbering(it.element.numbering, ..it.element.counter.at(it.element.location()))
      } + [ ] + it.element.body
    )

    if it.fill != none {
      res += [ ] + box(width: 1fr, it.fill) + [ ] 
    } else {
      res += h(1fr)
    }

    res += link(it.element.location(), it.page())
    strong(res)
  } else {
    // 여기서 들여쓰기를 수행합니다.
    h(1em * it.level) + it
  }
}

// 기본 개요를 위한 새로운 대상 선택자
#let chapters-and-headings = figure.where(kind: "chapter", outlined: true).or(heading.where(outlined: true))

//
// 실제 문서 서문 시작
//

#set heading(numbering: "1.")

// set을 사용할 수 없으므로 기본 인수로 재할당합니다.
#let chapter = chapter.with(numbering: "I")

// 장(chapter)을 위한 "show 규칙" 예시
// .with()를 사용한 후에는 더 이상 요소가 아니므로 chapter를 직접 사용할 수 없습니다.
#show figure.where(kind: "chapter"): set text(red)

//
// 실제 문서 시작
//

// 보시다시피 이것들은 제목(headings)과 같은 요소가 아니어서 설정이 다소 복잡합니다.
// 장이 제목이 아니기 때문에 번호 매기기에 해당 장이 포함되지 않지만, 
// 제목에 대한 show 규칙을 사용하면 포함할 수 있습니다.

#outline(target: chapters-and-headings)

#chapter[장(Chapter)]
= 장 제목
== 서브 제목

#chapter[다시 장]
= 장 제목
= 장 제목
== 서브 제목
=== 서브 서브 제목
== 서브 제목

#chapter[또 다른 장]
Rendered image

모든 수식을 디스플레이 모드로 설정

수학 블록과 약간의 충돌이 발생할 수 있습니다.
// 저자: eric1102
#show math.equation: it => {
  if it.body.fields().at("size", default: none) != "display" {
    return math.display(it)
  }
  it
}

인라인 수식: $sum_(n=0)^oo e^(x^2 - n/x^2)$\
새로운 줄의 다른 텍스트.


$
sum_(n=0)^oo e^(x^2 - n/x^2)
$
Rendered image

번호 없는 빈 페이지

홀수 페이지에서 시작하는 장(Chapter) 앞의 빈 페이지

이 스니펫은 0.12.0 버전에서 작동하지 않습니다. 수정을 도와주실 분이 있다면 정말 감사하겠습니다.
// 저자: janekfleper

#set page(height: 20em)

#let find-labels(name) = {
  return query(name).map(label => label.location().page())
}

#let page-header = context {
  let empty-pages = find-labels(<empty-page>)
  let new-chapters = find-labels(<new-chapter>)
  if new-chapters.len() > 0 {
    if new-chapters.contains(here().page()) [
      _이 페이지에서 새로운 장이 시작됩니다_
      #return
    ]

    // 다음 <new-chapter> 레이블의 인덱스를 가져옵니다
    let new-chapter-index = new-chapters.position(page => page > here().page())
    if new-chapter-index != none {
      let empty-page = empty-pages.at(new-chapter-index)
      if empty-page < here().page() [
        _다음 장을 홀수 페이지에서 시작하기 위한 빈 페이지입니다_
        #return
      ]
    }
  }

  [그리고 이것은 일반적인 헤더입니다]
  line(length: 100%)
}

#let page-footer = context {
  // chapter-heading()의 페이지 나누기가 <empty-page> 레이블 뒤에 삽입되므로,
  // 선택자는 관련 레이블을 찾기 위해 현재 페이지 "이전"을 살펴봐야 합니다.
  let empty-page-labels = query(selector(<empty-page>).before(here()))
  if empty-page-labels.len() > 0 {
    let empty-page = empty-page-labels.last().location().page()
    // 가장 최근의 <new-chapter> 레이블을 되돌아봅니다.
    let new-chapter = query(selector(<new-chapter>).before(here())).last().location().page()
    // 현재 페이지에 <new-chapter> 레이블이 없고, (empty-page + 1 == 현재 페이지)인지 확인합니다.
    if (new-chapter != here().page()) and (empty-page + 1 == here().page()) [
      _페이지 번호를 생략해야 하는 빈 페이지입니다_
      #return
    ]
  }

  let page-display = counter(page).display(here().page-numbering())
  h(1fr) + page-display + h(1fr)
}

#show heading.where(level: 1): it => [
  #[] <empty-page>
  #pagebreak(to: "even", weak: true)
  #[] <new-chapter>
  #pagebreak(to: "odd", weak: true)
  #it.body
  #v(2em)
]


#show outline.entry.where(level: 1): it => {
  // 대상 페이지에 대한 마지막 <empty-page> 레이블을 찾기 위해 레이블 쿼리 결과를 뒤집습니다.
  // array.position() 메서드는 항상 첫 번째 항목을 반환하기 때문입니다.
  let empty-pages = find-labels(<empty-page>).rev()
  let new-chapters = query(<new-chapter>).rev()
  let empty-page-index = empty-pages.position(page => page == int(it.page.text))
  let new-chapter = new-chapters.at(empty-page-index)
  link(new-chapter.location())[#it.body #box(width: 1fr)[#it.fill] #new-chapter.location().page()]
}

#set page(header: page-header, footer: page-footer, numbering: "1")

#outline()

= 설명

```
이 쿼리들은 해당 태그가 어디에 있는지 밝혀줍니다. 실제 빈 페이지는 항상 레이블 <empty-page>의 위치 + 1에 있습니다. 실제로 페이지 나누기에 의해 빈 페이지가 삽입되면, 두 레이블은 제목 페이지와 그 이전 페이지를 덮게 됩니다. 빈 페이지가 삽입되지 않은 경우, 두 레이블은 동일한 페이지를 가리키게 되며 이 역시 문제가 되지 않습니다. 심지어 그때도 <new-chapter> 레이블을 먼저 확인하여 더 높은 우선순위를 줄 수 있습니다.

첫 번째 <empty-page> 레이블은 항상 1페이지에 있으며 첫 번째 장 이전의 (존재하지 않는) 빈 페이지를 가리키므로 무시해도 됩니다.

레이블 <empty-page>가 있는 페이지들: #context find-labels(<empty-page>)
레이블 <new-chapter>가 있는 페이지들: #context find-labels(<new-chapter>)
```

= 제목
#lorem(190)

= 또 다른 제목
#lorem(100)

= 마지막 제목
#lorem(400)

다중 show 규칙

때로는 매우 유사해 보이는 여러 규칙을 적용하거나, 코드에서 규칙을 생성해야 할 필요가 있습니다. 이를 처리하는 한 가지 방법(가장 저주받은 방법)은 다음과 같습니다:

#let rules = (math.sum, math.product, math.root)

#let apply-rules(rules, it) = {
  if rules.len() == 0 {
    return it
  }
  show rules.pop(): math.display
  apply-rules(rules, it)
}

$product/sum root(3, x)/2$

#show: apply-rules.with(rules)

$product/sum root(3, x)/2$
Rendered image

재귀 문제는 기본적으로 동일한 아이디어로 fold의 힘을 빌려 피할 수 있습니다:

// author: Eric
#let kind_supp = (code: "Listing", algo: "Algorithme")
#show: it => kind_supp.pairs().fold(it, (acc, (kind, supp)) => {
  show figure.where(kind: kind): set figure(supplement: supp)
  acc
})
Rendered image

기호의 경우(요소 함수가 필요하지 않은 경우), 정규 표현식을 사용할 수 있다는 점에 유의하세요. 이것이 더 강력한 방법입니다:

#show regex("[" + math.product + math.sum + "]"): math.display

$product/sum root(3, x)/2$
Rendered image

중첩된 목록에서 들여쓰기 제거

// author: fenjalien
#show enum.item: it => {
  if repr(it.body.func()) == "sequence" {
    let children = it.body.children
    let index = children.position(x => x.func() == enum.item)
    if index != none {
      enum.item({
        children.slice(0, index).join()
        set enum(indent: -1.2em) // 무한 재귀 show 규칙을 중지한다는 점에 유의하세요
        children.slice(index).join()
      })
    } else {
      it
    }
  } else {
    it
  }
}

arst
+ A
+ b
+ c
  + d
+ e
  + f
+ g
+ h
+ i
+ 
Rendered image