feat(web): P81 — Obsidian callout (> [!type]-) 을 <details> 로 렌더링#81
Conversation
사용자 보고: 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>
There was a problem hiding this comment.
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]; |
There was a problem hiding this comment.
현재 [SKIP, index + replacement.length]를 반환하면 새로 삽입된 노드들의 자식 노드들을 방문하지 않고 건너뛰게 됩니다. 이로 인해 callout 본문 내부에 중첩된 callout(> [!info]\n> > [!tip])이 있을 경우, 내부 callout은 변환되지 않고 일반 blockquote로 남게 됩니다. 중첩된 callout을 지원하려면 삽입된 첫 번째 노드부터 다시 방문하도록 index를 반환해야 합니다.
| return [SKIP, index + replacement.length]; | |
| return index; |
|
|
||
| const text = (firstChild as Text).value; | ||
| // 첫 줄에서 [!type] 또는 [!type]- 또는 [!type]+ + 옵션 title 매칭. | ||
| const m = text.match(/^\[!(\w+)\]([-+])?[ \t]*(.*?)(\n|$)/); |
There was a problem hiding this comment.
Obsidian callout 문법은 > [!type] 앞에 공백이 있는 경우(예: > [!info])도 허용합니다. 현재 정규식은 텍스트 노드의 시작 부분에 정확히 [!가 와야 하므로, 앞에 공백이 있으면 매칭되지 않을 수 있습니다. 유연한 매칭을 위해 시작 부분에 [ \t]*를 추가하는 것이 좋습니다.
| const m = text.match(/^\[!(\w+)\]([-+])?[ \t]*(.*?)(\n|$)/); | |
| const m = text.match(/^[ \t]*\[!(\w+)\]([-+])?[ \t]*(.*?)(\n|$)/); |
| } | ||
| } | ||
|
|
||
| @layer utilities { |
배경
사용자 보고: vault session md 가 secall web 에서:
> assistant하고 빈 칸"진단: vault md 의
> [!thinking]-,> [!tool]- ToolName등은 Obsidian 의 collapsible callout syntax. Obsidian 에선 펼침/접힘 박스로 렌더링되지만 react-markdown 은 이 문법을 모르므로 plain blockquote 로 처리. P49 의 같은 role 연속 turn 헤더 강등 (### Turn N) 과 합쳐져 시각적 단조로움 + 본문이 빈 칸 처럼 인식.기존
remark-calloutsnpm 패키지를 검토했지만 foldable (> [!type]-) 미지원 ("Future support" 명시). 우리 vault md 가 정확히 foldable 형식이라 자체 plugin 작성.변경
신규 plugin
web/src/lib/remarkObsidianCallouts.tsmdast 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 (명시적)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 머지 후)
🤖 Generated with Claude Code