feat(web): P66 — MarkdownView 폴딩/highlight/wikilink 확장#75
Conversation
배경: MarkdownView 가 react-markdown v9 + remark-gfm 만 사용. 사용자 보고
"폴딩 제대로 안 됨" + 코드블록 syntax highlight 부재 + Obsidian wikilink
미지원.
변경:
- web/package.json: rehype-raw / rehype-highlight / highlight.js /
remark-wiki-link 추가
- web/src/components/MarkdownView.tsx:
- rehypePlugins=[rehypeRaw, rehypeHighlight] 로 raw HTML (<details>) +
코드블록 syntax highlight 활성
- remarkPlugins 에 remark-wiki-link 추가 — [[Page]] 파싱
- components override: h2/h3 collapse toggle, a (wikilink → NavLink) 추가
- 기존 p/li/code query 하이라이트는 회귀 없음
- highlight.js github-dark 테마 CSS import
검증: pnpm typecheck + pnpm build 통과.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request enhances the MarkdownView component by integrating rehype-raw for HTML support, rehype-highlight for syntax highlighting, and remark-wiki-link for Obsidian-style links, while also adding collapsible headings. Feedback highlights security concerns regarding XSS risks and a vulnerable dependency, alongside accessibility issues with the collapsible headings' lack of keyboard support. Additionally, reviewers advised against direct DOM manipulation, suggesting a more React-idiomatic state-based approach for element visibility.
|
|
||
| const rehypePlugins = useMemo( | ||
| // rehype-raw 가 먼저 raw HTML 을 hast 노드로 변환 → rehype-highlight 가 코드블록 처리. | ||
| () => [rehypeRaw, rehypeHighlight], |
|
|
||
| '@ungap/structured-clone@1.3.0': | ||
| resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} | ||
| deprecated: Potential CWE-502 - Update to 1.3.1 or higher |
| import { | ||
| Fragment, | ||
| type ReactNode, | ||
| useMemo, | ||
| useState, | ||
| type MouseEvent, | ||
| } from "react"; |
| let next = headingEl.nextElementSibling as HTMLElement | null; | ||
| const stopTags = new Set(["H1", "H2"].concat(level === 2 ? [] : ["H3"])); | ||
| while (next && !stopTags.has(next.tagName)) { | ||
| next.style.display = collapsed ? "" : "none"; | ||
| next = next.nextElementSibling as HTMLElement | null; | ||
| } |
| const marker = collapsed ? "▶" : "▼"; | ||
| const props = { | ||
| onClick, | ||
| "aria-expanded": !collapsed, | ||
| role: "button", | ||
| tabIndex: 0, | ||
| style: { cursor: "pointer", userSelect: "none" as const }, |
There was a problem hiding this comment.
role="button"과 tabIndex={0}을 설정하여 접근성을 고려했으나, 키보드 사용자(Enter 또는 Space 키)를 위한 onKeyDown 핸들러가 누락되었습니다. 마우스 클릭뿐만 아니라 키보드 입력으로도 폴딩 기능이 동작하도록 개선이 필요합니다.
const marker = collapsed ? "▶" : "▼";
const onKeyDown = (e: KeyboardEvent<HTMLElement>) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
onClick(e as unknown as MouseEvent<HTMLElement>);
}
};
const props = {
onClick,
onKeyDown,
"aria-expanded": !collapsed,
role: "button",
tabIndex: 0,
style: { cursor: "pointer", userSelect: "none" as const },
* fix(web): P66 follow-up — Gemini PR #75 security/a11y 리뷰 반영 Gemini PR #75 리뷰의 보안 / 접근성 finding 3건 반영. heading collapse 의 DOM 직접 조작 한계 (MEDIUM finding 중 하나) 는 별도 plan 으로 분리 — AST 기반 state-driven 재설계가 필요. ## 변경 ### 1. rehype-sanitize 추가 (HIGH security) `rehype-raw` 가 raw HTML 을 통과시키면 XSS 위험. `rehype-sanitize` 를 중간 단계로 추가해 위험 태그/속성 제거. - `pnpm add rehype-sanitize` - pipeline: `rehype-raw` → `rehype-sanitize` (커스텀 schema) → `rehype-highlight` - schema: `defaultSchema` 에 `<details>` / `<summary>` 태그 + highlight.js className (`hljs-*`, `language-*`) 허용 추가. 그 외 script/style/event handler 등은 defaultSchema 가 차단. ### 2. `@ungap/structured-clone` CVE-CWE-502 (HIGH security) `hast-util-raw` 의 transitive dep `@ungap/structured-clone@1.3.0` 가 deprecated (CWE-502, Deserialization). pnpm overrides 로 1.3.1+ 강제. ```json "pnpm": { "overrides": { "@ungap/structured-clone": "^1.3.1" } } ``` ### 3. CollapsibleHeading 키보드 접근성 (MEDIUM) `role="button"` + `tabIndex={0}` 이 있어도 `onKeyDown` 핸들러 없으면 키보드 사용자가 폴딩 못 함. `Enter` / `Space` 시 `onClick` 과 동일 동작하도록 핸들러 추가. ## 미해결 (별도 plan) Gemini 의 한 finding 은 본 PR 에 포함 안 됨: - **CollapsibleHeading 의 DOM 직접 조작**: `nextElementSibling` + `style.display` 는 React 의 선언적 모델과 충돌. 마크다운 AST 분석 후 state 기반 가시성 제어로 재설계 필요 (큰 변경). web-backlog 후속. ## 검증 - `pnpm typecheck` 통과 - `pnpm build` 통과 (sanitize 추가로도 청크 크기 영향 미미) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(web): P77 — Gemini 추가 리뷰 반영 (heading 내 link 키보드 + details open) ## HIGH: heading 내부 link 의 키보드 클릭 무효화 `onKeyDown` 핸들러가 target check 없이 `e.preventDefault()` 호출 → heading 내부 `<a>` 를 Enter 키로 클릭 시 navigation 막힘. `onClick` 과 동일하게 target 이 a/code 면 early return. ## MEDIUM: <details open> 속성 strip sanitize schema 에 `details: ["open"]` 추가. 마크다운 안의 `<details open>` 또는 브라우저 토글 시 추가되는 open 속성이 보존되도록. ## 검증 - pnpm typecheck 통과 - pnpm build 통과 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: d9ng <d9ng@outlook.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
사용자 보고 "마크다운 폴딩 제대로 안 됨" + syntax highlight / Obsidian wikilink 미지원. react-markdown v9 + remark-gfm 만 사용 중이던 MarkdownView 를 4 기능 확장.
변경
web/package.json):rehype-raw@7.0.0,rehype-highlight@7.0.2,highlight.js@11.11.1,remark-wiki-link@2.0.1web/src/components/MarkdownView.tsx:rehypePlugins=[rehypeRaw, rehypeHighlight]→<details>raw HTML + 코드블록 syntax highlightremarkPlugins에remark-wiki-link추가 →[[Page]]→/wiki/Page_NameNavLinkcomponentsoverride:h2/h3collapse toggle,a커스텀 (wikilink/외부 분기)p/li/codequery 하이라이트 회귀 없음highlight.js/styles/github-dark.cssimport한계 (서브에이전트 보고)
nextElementSiblingDOM 외부 조작이라 React 리렌더와 충돌 가능. 차후 fully React state 기반 재구현 권장.lowlight또는 언어 subset 도입.Test plan
pnpm typecheckpnpm build<details>/ 코드블록 /[[link]]동작 확인🤖 Generated with Claude Code