Skip to content

feat(web): P81 — Obsidian callout (> [!type]-) 을 <details> 로 렌더링#81

Merged
hang-in merged 1 commit into
mainfrom
feat/p81-markdown-callouts
May 16, 2026
Merged

feat(web): P81 — Obsidian callout (> [!type]-) 을 <details> 로 렌더링#81
hang-in merged 1 commit into
mainfrom
feat/p81-markdown-callouts

Conversation

@hang-in
Copy link
Copy Markdown
Owner

@hang-in hang-in commented May 16, 2026

배경

사용자 보고: vault session md 가 secall web 에서:

  • "대화처럼 안 보임"
  • "툴 로딩 한 것도 > assistant 하고 빈 칸"
  • "어떤 세션은 assistant 폴딩만 10개 이상 이어서"

진단: vault md 의 > [!thinking]-, > [!tool]- ToolName 등은 Obsidian 의 collapsible callout syntax. Obsidian 에선 펼침/접힘 박스로 렌더링되지만 react-markdown 은 이 문법을 모르므로 plain blockquote 로 처리. P49 의 같은 role 연속 turn 헤더 강등 (### Turn N) 과 합쳐져 시각적 단조로움 + 본문이 빈 칸 처럼 인식.

기존 remark-callouts npm 패키지를 검토했지만 foldable (> [!type]-) 미지원 ("Future support" 명시). 우리 vault md 가 정확히 foldable 형식이라 자체 plugin 작성.

변경

신규 plugin web/src/lib/remarkObsidianCallouts.ts

mdast blockquote 노드 visit → 첫 paragraph 의 첫 text 가 [!type][-/+]? title 패턴이면 blockquote 를 <details class="callout callout-<type>"> + <summary> 로 unwrap. body markdown AST 는 그대로 유지 (rehype-raw 가 details/summary HTML 통과).

  • [!type] — open (default)
  • [!type]- — closed (collapsed)
  • [!type]+ — open (명시적)
  • title 생략 시 type 을 capitalize 해 summary

CSS (web/src/index.css)

.callout / .callout-<type> — left border + surface-2 배경 + 타입별 색 (thinking=info, tool=warn, tip=success, error=danger 등).

의존성

unist-util-visit, @types/mdast. remark/remark-html (devDep, vitest 용).

회귀 테스트

web/src/lib/__tests__/remarkObsidianCallouts.test.ts — 6 케이스, 모두 통과.

검증

  • pnpm typecheck
  • pnpm build
  • pnpm test ✓ (15/15)

사용자 액션 (PR 머지 후)

cd web && pnpm build && cd ..
cargo install --path crates/secall --force
/bin/cp -f ~/.cargo/bin/secall ~/.local/bin/secall
# secall serve 재시작

🤖 Generated with Claude Code

사용자 보고: vault session md 가 secall web 에서 대화처럼 안 보이고, tool
로딩이 `> assistant` + 빈 칸으로만 보이며, 같은 role 의 turn 이 10개 이상
이어서 단조롭게 늘어남.

진단: vault md 의 `> [!thinking]-`, `> [!tool]- ToolName` 등은 Obsidian 의
collapsible callout syntax. Obsidian 에선 펼침/접힘 박스로 렌더링되지만
react-markdown 은 이 문법을 모르므로 plain blockquote 로 처리. P49 의 같은
role 연속 turn 헤더 강등 (`### Turn N`) 과 합쳐져 시각적 단조로움 발생.

## 변경

### 신규 plugin
`web/src/lib/remarkObsidianCallouts.ts` (자체 작성, ~80 line).
mdast blockquote 노드 visit → 첫 paragraph 의 첫 text 가 `[!type][-/+]? title`
패턴이면 blockquote 를 `<details class="callout callout-<type>">` +
`<summary>` 로 unwrap. body 의 markdown AST 는 그대로 유지.

- `[!type]` — open (default)
- `[!type]-` — closed (collapsed)
- `[!type]+` — open (명시적)
- title 생략 시 type 을 capitalize 해 summary 로

### MarkdownView 통합
- `remarkPlugins` 에 `remarkObsidianCallouts` 추가 (remarkFrontmatter 다음)
- `rehype-sanitize` schema 의 `details` 에 `callout` / `callout-<type>`
  className 허용 (이미 허용된 `open` 속성에 추가)

### CSS
`web/src/index.css` 에 `.callout` / `.callout-<type>` 스타일. left border
3px + surface-2 배경 + 타입별 색 (thinking=info, tool=warn, tip=success
등). dark/light 토큰 사용.

### 의존성
- `unist-util-visit` — AST visit
- `@types/mdast` — TS 타입
- `remark` / `remark-html` (devDep) — vitest 용

## 회귀 테스트

`web/src/lib/__tests__/remarkObsidianCallouts.test.ts` — 6 케이스:
- default open / collapsed / explicit open / no title / plain blockquote
  unchanged / HTML escape in summary.

15/15 통과 (기존 SettingsRoute 9 test + 신규 6).

## 검증

- `pnpm typecheck` ✓
- `pnpm build` ✓
- `pnpm test` ✓ (15/15)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces support for Obsidian-style callouts (e.g., > [!info]) by implementing a custom remark plugin, remarkObsidianCallouts, and adding corresponding CSS styles. The changes include new dependencies for Markdown processing, unit tests for the plugin, and updates to the MarkdownView component to allow specific callout classes during sanitization. Feedback focuses on enabling support for nested callouts by adjusting the node visitation logic, improving the regex to handle leading whitespace, and moving the CSS definitions to the Tailwind components layer for better architectural alignment.

];
parent.children.splice(index, 1, ...replacement);
// 새로 삽입된 노드들은 다시 visit 안 함 — body 의 nested callout 은 별도 패스로 처리.
return [SKIP, index + replacement.length];
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

현재 [SKIP, index + replacement.length]를 반환하면 새로 삽입된 노드들의 자식 노드들을 방문하지 않고 건너뛰게 됩니다. 이로 인해 callout 본문 내부에 중첩된 callout(> [!info]\n> > [!tip])이 있을 경우, 내부 callout은 변환되지 않고 일반 blockquote로 남게 됩니다. 중첩된 callout을 지원하려면 삽입된 첫 번째 노드부터 다시 방문하도록 index를 반환해야 합니다.

Suggested change
return [SKIP, index + replacement.length];
return index;


const text = (firstChild as Text).value;
// 첫 줄에서 [!type] 또는 [!type]- 또는 [!type]+ + 옵션 title 매칭.
const m = text.match(/^\[!(\w+)\]([-+])?[ \t]*(.*?)(\n|$)/);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Obsidian callout 문법은 > [!type] 앞에 공백이 있는 경우(예: > [!info])도 허용합니다. 현재 정규식은 텍스트 노드의 시작 부분에 정확히 [!가 와야 하므로, 앞에 공백이 있으면 매칭되지 않을 수 있습니다. 유연한 매칭을 위해 시작 부분에 [ \t]*를 추가하는 것이 좋습니다.

Suggested change
const m = text.match(/^\[!(\w+)\]([-+])?[ \t]*(.*?)(\n|$)/);
const m = text.match(/^[ \t]*\[!(\w+)\]([-+])?[ \t]*(.*?)(\n|$)/);

Comment thread web/src/index.css
}
}

@layer utilities {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

.callout 스타일은 단순한 유틸리티 클래스가 아니라 여러 하위 선택자와 상태([open])를 포함하는 복합적인 컴포넌트 스타일입니다. Tailwind CSS의 관례에 따라 @layer utilities 대신 @layer components에 정의하는 것이 적절합니다. 이는 유틸리티 클래스가 컴포넌트 스타일을 덮어쓸 수 있도록 보장합니다.

Suggested change
@layer utilities {
@layer components {

@hang-in hang-in merged commit 065f129 into main May 16, 2026
3 checks passed
@hang-in hang-in deleted the feat/p81-markdown-callouts branch May 16, 2026 09:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant