Skip to content

feat(web): P66 — MarkdownView 폴딩/highlight/wikilink 확장#75

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

feat(web): P66 — MarkdownView 폴딩/highlight/wikilink 확장#75
hang-in merged 1 commit into
mainfrom
feat/p66-markdown-extensions

Conversation

@hang-in
Copy link
Copy Markdown
Owner

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

Summary

사용자 보고 "마크다운 폴딩 제대로 안 됨" + syntax highlight / Obsidian wikilink 미지원. react-markdown v9 + remark-gfm 만 사용 중이던 MarkdownView 를 4 기능 확장.

변경

  • deps 추가 (web/package.json): rehype-raw@7.0.0, rehype-highlight@7.0.2, highlight.js@11.11.1, remark-wiki-link@2.0.1
  • web/src/components/MarkdownView.tsx:
    • rehypePlugins=[rehypeRaw, rehypeHighlight]<details> raw HTML + 코드블록 syntax highlight
    • remarkPluginsremark-wiki-link 추가 → [[Page]]/wiki/Page_Name NavLink
    • components override: h2/h3 collapse toggle, a 커스텀 (wikilink/외부 분기)
    • 기존 p/li/code query 하이라이트 회귀 없음
  • highlight.js/styles/github-dark.css import

한계 (서브에이전트 보고)

  • heading collapse 는 MVP — nextElementSibling DOM 외부 조작이라 React 리렌더와 충돌 가능. 차후 fully React state 기반 재구현 권장.
  • MarkdownView 청크 502KB (highlight.js 전 언어 번들) — 차후 lowlight 또는 언어 subset 도입.

Test plan

  • pnpm typecheck
  • pnpm build
  • CI 통과
  • (수동) <details> / 코드블록 / [[link]] 동작 확인

🤖 Generated with Claude Code

배경: 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>
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 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],
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

security-high high

rehype-raw 플러그인은 마크다운 내의 HTML 태그를 그대로 렌더링하므로, 악의적인 스크립트가 포함된 마크다운이 입력될 경우 XSS(Cross-Site Scripting) 공격에 노출될 위험이 있습니다. content 데이터의 출처가 사용자 입력이거나 외부 API라면, rehype-sanitize를 함께 사용하여 허용된 태그와 속성만 렌더링하도록 제한하는 것이 안전합니다.

Comment thread web/pnpm-lock.yaml

'@ungap/structured-clone@1.3.0':
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
deprecated: Potential CWE-502 - Update to 1.3.1 or higher
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

security-high high

@ungap/structured-clone@1.3.0 버전은 보안 취약점(CWE-502: Deserialization of Untrusted Data)으로 인해 deprecated 되었습니다. hast-util-raw 등에서 전파되는 전이 의존성으로 보이는데, 가능한 경우 1.3.1 이상의 버전으로 업데이트하는 것이 안전합니다.

Comment on lines +1 to +7
import {
Fragment,
type ReactNode,
useMemo,
useState,
type MouseEvent,
} from "react";
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

키보드 접근성 구현을 위해 KeyboardEvent 타입을 추가로 임포트해야 합니다.

Suggested change
import {
Fragment,
type ReactNode,
useMemo,
useState,
type MouseEvent,
} from "react";
import {
Fragment,
type ReactNode,
useMemo,
useState,
type MouseEvent,
type KeyboardEvent,
} from "react";

Comment on lines +175 to +180
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;
}
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

React 컴포넌트 내부에서 nextElementSiblingstyle.display를 직접 조작하는 방식은 React의 선언적 렌더링 모델과 충돌할 수 있습니다. React가 리렌더링을 수행할 때 직접 수정한 DOM 상태가 무시되거나 초기화될 수 있으므로, 장기적으로는 마크다운 AST를 분석하여 상태 기반으로 형제 요소들의 가시성을 제어하는 구조로 개선하는 것을 권장합니다.

Comment on lines +182 to +188
const marker = collapsed ? "▶" : "▼";
const props = {
onClick,
"aria-expanded": !collapsed,
role: "button",
tabIndex: 0,
style: { cursor: "pointer", userSelect: "none" as const },
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

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 },

@hang-in hang-in merged commit daf7ce5 into main May 16, 2026
3 checks passed
hang-in added a commit that referenced this pull request May 16, 2026
* 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>
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