렌더 훅
렌더 훅을 사용하면 hwaro의 마크다운 파서를 건드리지 않고 개별 마크다운 요소 — 링크, 이미지, 헤딩, 펜스 코드 블록, 인용문, 표 — 가 HTML로 바뀌는 방식을 재정의할 수 있습니다. templates/hooks/에 템플릿을 두면 모든 페이지의 해당 요소가 내장 마크업 대신 그 템플릿을 거쳐 렌더링됩니다.
templates/hooks/render-* 템플릿을 하나도 만들지 않으면 아무것도 바뀌지 않습니다. hwaro는 지금까지와 완전히 동일하게 렌더링합니다.
파일 구조
templates/
└── hooks/
├── render-link.html # [text](url "title")
├── render-image.html # 
├── render-heading.html # ## Heading
├── render-codeblock.html # ```lang ... ```
├── render-blockquote.html # > quoted
└── render-table.html # | GFM | tables |
파일마다 독립적으로 동작하므로 재정의하고 싶은 것만 추가하면 됩니다. render-image.html만 있는 사이트는 이미지 래퍼만 커스텀되고 나머지는 기본 렌더링을 유지합니다.
그 밖의 hooks/render-* 이름은 인식되지 않으며 빌드 시 경고를 남깁니다.
컨텍스트 변수
모든 값은 Crinja Value이고, 다른 hwaro 템플릿과 마찬가지로 필요한 곳은 이미 HTML 이스케이프되어 있습니다. | e 필터를 쓰기 전에 아래 규칙 1을 먼저 확인합니다.
render-link.html
| 변수 | 설명 |
|---|---|
destination |
링크 대상. 이스케이프됨. markdown.safe = true이고 대상이 안전하지 않은 프로토콜(javascript: 등)을 쓰면 빈 문자열 |
title |
링크의 "title" 텍스트. 이스케이프됨. 없으면 빈 문자열 |
text |
이미 렌더링된 링크 내부 HTML(중첩 마크업이 들어 있을 수 있고, 링크가 이미지를 감싸면 훅으로 렌더링된 <img>가 들어 있을 수도 있음) |
render-image.html
| 변수 | 설명 |
|---|---|
destination |
이미지 src. 이스케이프됨(안전하지 않은 프로토콜 규칙은 링크와 동일) |
alt |
이미지의 alt 텍스트 — 마크다운 소스의 alt에 인라인 마크업이 중첩돼 있어도 순수 텍스트만(CommonMark와 동일: 이미지의 "자식"은 태그를 만들지 않음) |
title |
이미지의 "title" 텍스트. 이스케이프됨. 없으면 빈 문자열 |
render-heading.html
| 변수 | 설명 |
|---|---|
level |
헤딩 레벨 정수(1–6) |
text |
이미 렌더링된 헤딩 내부 HTML |
id |
헤딩의 id — 마크다운 소스의 커스텀 {#id}이거나, 자동 생성 후 중복 제거된 슬러그(heading, heading-1, heading-2, …) |
render-codeblock.html
| 변수 | 설명 |
|---|---|
lang |
펜스의 언어 토큰. 이스케이프됨(` `python 의 python`). 언어 없는 펜스는 빈 문자열 |
options |
언어 뒤의 Zola/Pandoc 스타일 {...} 옵션 블록 원문(구문 강조 참고), {...} 블록이 없으면 정보 문자열의 나머지 텍스트. 이스케이프됨 |
code |
펜스 본문. HTML 이스케이프됨 |
highlighted |
서버 모드로 구문 강조된 본문(hljs 클래스 span). [highlight] mode가 "server"가 아니거나, 강조가 꺼져 있거나, 해당 언어의 렉서가 없으면 빈 문자열. {hide_lines=…} 펜스 옵션은 이미 적용된 상태 — 서버 모드에서 숨긴 줄은 템플릿(그리고 code)에 절대 도달하지 않음 |
name |
파싱된 {name=...}/{title=...} 파일 이름 레이블. 이스케이프됨. 없으면 빈 문자열 |
copy |
이 블록에 복사 버튼이 적용되면 "true"([highlight] copy / 펜스별 {copy=...}, mermaid에는 절대 적용 안 됨), 아니면 빈 문자열. 어떤 마크업을 출력할지는 템플릿이 결정 |
render-blockquote.html
| 변수 | 설명 |
|---|---|
text |
이미 렌더링된 인용문 내부 HTML — 블록 수준 콘텐츠(문단, 목록, 중첩 인용)이며 보통 개행으로 끝남 |
render-table.html
| 변수 | 설명 |
|---|---|
html |
완성된 기본 <table>...</table> 마크업 전체 |
header_html |
표의 <thead>...</thead> 부분 |
body_html |
표의 <tbody>...</tbody> 부분 — 헤더만 있는 표는 빈 문자열 |
모든 훅 템플릿은 추가로 표준 page(url, title, path, language)와 config(base_url, title) 변수도 볼 수 있습니다.
기본 출력과 동일한 템플릿
다음 템플릿들은 hwaro의 기본 출력을 그대로 재현합니다 — 수정의 출발점으로 쓰기 좋습니다.
{# templates/hooks/render-link.html #}
<a href="{{ destination }}"{% if title is present %} title="{{ title }}"{% endif %}>{{ text }}</a>
{# templates/hooks/render-image.html #}
<img src="{{ destination }}" alt="{{ alt }}"{% if title is present %} title="{{ title }}"{% endif %} />
{# templates/hooks/render-heading.html #}
<h{{ level }} id="{{ id }}">{{ text }}</h{{ level }}>
{# templates/hooks/render-codeblock.html #}
<pre><code{% if lang is present %} class="language-{{ lang }} hljs"{% endif %}>{% if highlighted is present %}{{ highlighted }}{% else %}{{ code }}{% endif %}</code></pre>
{# templates/hooks/render-blockquote.html #}
<blockquote>
{{ text }}</blockquote>
{# templates/hooks/render-table.html #}
{{ html }}
{% if title %}이 아니라 {% if title is present %}인 점에 주의합니다 — Crinja의 참/거짓 판정은 false/0/nil만 거짓으로 보기 때문에, 그냥 {% if title %}을 쓰면 title이 없을 때도 title=""이 렌더링됩니다. 커스텀 is present/is empty 테스트(hwaro 자체 템플릿 전반에서도 사용)가 이를 올바르게 확인합니다.
codeblock 템플릿의 hljs 클래스는 기본 설정([highlight] enabled = true — 대부분의 Highlight.js 테마가 이 클래스를 기준으로 기본 스타일을 적용)에서의 기본 출력과 일치합니다. 강조를 완전히 껐다면 기본 출력은 hljs 없이 class="language-{{ lang }}"을 내보내므로, 바이트 단위로 동일하게 맞추려면 훅에서도 빼면 됩니다.
예시: figure로 감싼 이미지
{# templates/hooks/render-image.html #}
<figure>
<img src="{{ destination }}" alt="{{ alt }}" loading="lazy" />
{% if title is present %}<figcaption>{{ title }}</figcaption>{% endif %}
</figure>
이제 마크다운의 모든 이 캡션 달린 <figure>로 렌더링됩니다 — 콘텐츠 파일마다 이미지 마크업을 따로 쓸 필요가 없습니다.
규칙
- 값은 이미 이스케이프되어 있으니 그대로 출력합니다. hwaro 템플릿은 (다른 곳과 마찬가지로) 자동 이스케이프가 꺼져 있고,
destination/title/alt/lang/options/code는 렌더러가 이미 HTML 이스케이프한 값입니다. 이들을| e에 통과시키면 이중 이스케이프가 되고,text나highlighted를 통과시키면 이미 렌더링된 HTML이 깨집니다. - 관례대로 큰따옴표로 감싼
href/src속성을 유지합니다. 이후 단계 전부 —@/internal-page.md링크 해석, 루트 상대 링크의 서브패스(base_path) 접두사 처리, 반응형 이미지srcset/sizes주입,loading="lazy"— 가 최종 HTML을 대상으로href="..."/src="..."패턴을 찾는 일반 텍스트 패스로 동작합니다. 따옴표 없는 속성이나 작은따옴표 속성을 내보내는 훅, 또는 대상을 일반적인 속성 값이 아닌 형태로 바꿔 버리는 훅은 해당 요소를 이 모든 처리에서 제외시킵니다.@/접두사가 붙은destination은InternalLinkResolver가 찾아 해석할 수 있도록 반드시href="..."안에 들어가야 합니다. render-heading.html은<hN id="{{ id }}">요소를 출력해야 합니다. 목차({{ toc }}/page.toc)와insert_anchor_links는 둘 다 최종 HTML에서id속성이 있는<h1>–<h6>태그를 찾아 후처리합니다. 헤딩 태그가 아닌 것을 렌더링하거나id를 빼먹는 훅은 조용히 둘 다에서 빠집니다.{{ text }}를 변형하지 않습니다. 이미 렌더링된 HTML이고, 숏코드를 쓰는 페이지에서는 나중 패스에서 숏코드 출력으로 치환될 내부 플레이스홀더 주석(<!--HWARO-SHORTCODE-PLACEHOLDER-N-->)이 들어 있을 수 있습니다 —text를 필터링·잘라내기·재이스케이프하면 그 플레이스홀더가 깨지거나 미아가 될 수 있습니다.- Mermaid는 자기 펜스를 직접 처리합니다.
[markdown] mermaid = true이면```mermaid펜스는 항상 기존 Mermaid 파이프라인(<div class="mermaid">…</div>)을 거치고render-codeblock.html은 절대 거치지 않습니다 — "모든 훅은 일치하는 모든 요소에 항상 적용된다"의 설정으로 결정되는 예외입니다.mermaid = false로 설정하면 codeblock 훅이 mermaid 펜스도 다른 언어처럼 렌더링합니다. - 어드모니션은 자기 인용문을 직접 처리합니다.
[markdown] admonitions = true(기본값)이면> [!NOTE]스타일 인용문은 계속 어드모니션 파이프라인(<div class="admonition admonition-note">…</div>)을 거치고render-blockquote.html은 절대 거치지 않습니다 — Mermaid와 같은, 설정으로 결정되는 패턴입니다.admonitions = false로 설정하면 그런 인용문도 다른 인용문처럼 훅을 거칩니다. render-table.html출력에 빈 줄을 넣지 않습니다. 표 훅의 출력은 본 파싱 이전에 마크다운에 삽입되는데, HTML 블록은 첫 빈 줄에서 끝나므로 그 뒤는 다시 마크다운으로 파싱됩니다. hwaro가 안전장치로 출력에서 빈 줄을 제거해 주므로 여러 줄 템플릿도 동작하지만, 빈 줄에 의존하는 마크업은 쓰지 않는 것이 좋습니다.- 훅은 표 셀, 각주 본문, 정의 목록, 프론트 매터의
description/요약 텍스트 안에는 적용되지 않습니다. 이들은 훅이 붙는 본 Markd 파서를 전혀 거치지 않는 별도의 단순한 인라인 마크다운 경로로 렌더링됩니다(render-table.html이 표 자체를 감싸더라도 표 셀은 그 경로를 유지합니다). 버그가 아니라 알려진 제약입니다.
증분 빌드
templates/hooks/ 아래 파일 편집은 hwaro build --cache와 hwaro serve에서 다른 템플릿 편집과 똑같이 추적됩니다 — 다만 훅은 어떤 페이지 템플릿에서도 {% include %}/{% extends %}로 도달하지 않기 때문에, hwaro가 영향을 받는 페이지를 좁혀낼 수 없습니다. templates/hooks/render-*.html 파일을 편집하면 [build] template_deps 설정과 무관하게 모든 페이지가 다시 렌더링됩니다. 증분 빌드를 참고합니다.