feat(preview): 문서 속 이미지를 프리뷰에 렌더한다 (M10) - #23
Merged
Conversation
앱에서 지우거나 이름을 바꾼 파일도 최근 파일 목록에서 변경되지 않는다. 코드는 그렇게 동작하는데 문서가 이 경우를 규정하지 않았다 — 그 항목을 누르면 열지 못했다고 알리고, 없는 경로는 다음 실행의 load_session이 걸러낸다. 사이드바를 접으면 새 파일을 만드는 길이 화면에서 사라진다. 탭바에 버튼을 둘지와 이름을 어디서 받을지가 정해지지 않아 열린 결정으로 둔다. 저장 다이얼로그는 시작 디렉터리를 줄 때 파일명 제안을 뺀다 — rfd 제약을 우회한 것이라 업스트림이 고치면 되돌린다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
프리뷰가 문서 속 이미지를 하나도 띄우지 못한다 — 로컬 상대 경로는 웹뷰 기준으로 해석되고 원격 이미지는 CSP가 막는다. 구현에 앞서 계약을 확정한다. 로컬 파일은 asset 프로토콜로 싣고, 읽을 수 있는 경로는 파일 커맨드와 같은 허용 루트다 — FileScope가 두 스코프의 단일 출처라 갈라지지 않는다. src는 sanitize 뒤에 바꾼다. DOMPurify 허용목록에 asset:을 더하면 문서가 적은 asset URL도 함께 통과하는데, 순서를 뒤집으면 그 길이 닫힌 채로 상대 경로만 해석된다. 원격 이미지는 https만 싣는다. 문서를 여는 것만으로 그 URL에 요청이 나가는 것이 대가이며, 끄는 설정은 열린 결정으로 남긴다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
이미지 한 장마다 IPC를 왕복하지 않도록 허용 루트 목록이 두 스코프의 단일 출처가 된다 — FileScope의 허용·거부가 asset 스코프에도 같은 경로를 등록하므로 다이얼로그·폴더 열기· load_session 세 입구가 손대지 않아도 따라온다. 두 스코프가 갈라지면 파일 커맨드가 막는 경로를 이미지로 읽을 수 있어, mock_app의 진짜 Tauri 스코프로 일치를 검증한다. assetProtocol의 scope는 비워 둔다 — 허용 경로는 위 세 입구에서 런타임에 정해진다. protocol-asset 피처는 tauri CLI가 빌드할 때만 붙여 주므로, cargo test·cargo build에도 켜지도록 Cargo.toml에 명시한다. CSP img-src에 https를 더해 원격 이미지를 싣는다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
renderMarkdown이 resolveImageSrc 옵션을 받아 sanitize를 마친 HTML의 img[src]만 바꾼다. DOMPurify 허용목록에 asset:을 더해 파서 단계에서 심으면 문서가 적은 asset URL도 함께 통과하므로, 순서를 뒤집어 그 길을 닫은 채로 상대 경로만 해석한다. 원시 HTML의 img도 같은 자리에서 잡힌다 — 크기를 주려고 흔히 쓰는 표기다. resolveImagePath는 스킴이 붙은 src와 프로토콜 생략 주소를 그대로 두고, 나머지만 퍼센트 디코딩·문서 폴더 결합·경로 정규화를 거쳐 절대 경로로 만든다. asset 프로토콜이 상위 참조가 든 경로를 거부하고, markdown-it이 src를 인코딩해 넘기기 때문이다. 이미지가 없는 문서는 다시 파싱하지 않는다. 다시 파싱하는 경로는 src 말고 아무것도 바꾸지 않아야 하므로, 표·체크박스·details가 든 문서로 그 불변을 고정한다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
활성 탭의 폴더가 상대 경로의 기준이다 — 앱이 그 폴더를 resolveImagePath에 넘기고, 결과 경로를 asset URL로 바꿔 프리뷰에 넣는다. 경로 없는 문서(Untitled)와 원격 주소는 그대로 둔다. 로드에 실패한 이미지는 그 자리에 흐린 alt 텍스트로 남는다. 브라우저는 물음표 상자를 그릴 뿐 alt를 글자로 표시하지 않고 img는 대체 요소라 ::after도 화면에 나오지 않아, 실패한 img에 display:none을 걸고 뒤에 span을 넣는다. 설명이 없는 이미지는 못 찾았다는 한 줄을 대신 쓴다. 이미지는 렌더를 마친 뒤 도착해 블록 높이를 바꾸므로, 도착할 때마다 리비전을 올려 스크롤 동기화가 낡은 측정을 버리게 한다. load·error는 버블하지 않아 콘텐츠 요소 한 곳에서 캡처로 받는다. 갱신마다 이미지가 다시 로드되지만 빈 프레임은 1.5%(최장 32ms)라 캐시를 두지 않는다. asset 프로토콜은 실제 웹뷰에만 있어 실앱 E2E가 픽셀까지 확인한다. http와 https의 구분은 개발 서버가 CSP 없이 서빙해 개발 빌드에서 확인되지 않는다 — 수동 검증 대상으로 문서에 남긴다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
파일 하나만 연 세션에서는 asset 스코프에 그 파일만 들어가, 문서와 같은 폴더에 둔 이미지가 렌더되지 않는다. 파일을 허용할 때 그 파일이 있는 폴더의 하위 트리를 asset 스코프에 등록하는 것으로 계약을 바꾼다. 파일 커맨드는 그 파일만 다루므로 두 스코프는 여기서만 갈라지고, 넓어진 범위와 남는 위험은 보안 문서에 적는다. VS Code도 작업 폴더에 속하지 않은 마크다운을 미리 볼 때 그 문서가 있는 폴더를 리소스 루트로 삼는다. `/`로 시작하는 경로는 연 폴더 기준으로 해석한다. 디스크 루트 기준으로 해석하면 같은 문서가 GitHub·VS Code와 다른 파일을 가리킨다. 폴더를 열지 않은 경우에는 기준이 없어 그대로 둔다. srcset과 source[srcset]도 해석 대상에 넣는다. src만 바꾸면 srcset이 있는 이미지는 같은 폴더에 파일이 있어도 렌더되지 않는다. 후보에 섞인 asset:은 버린다. assetProtocol의 scope는 객체로 적고 requireLiteralLeadingDot을 false로 둔다. unix 기본값이 true라 생략하면 점으로 시작하는 폴더 안의 파일을 이미지만 거부한다. 코드는 아직 옛 계약대로 동작한다 — 뒤따르는 커밋에서 정렬한다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
폴더 없이 파일 하나만 여는 길(다이얼로그·최근 파일·세션 복원)에서는 asset 스코프에 그 파일만 들어가, 문서와 같은 폴더에 둔 이미지가 렌더되지 않았다. 파일을 허용할 때 그 파일이 있는 폴더의 하위 트리를 asset 스코프에 등록한다. 파일 커맨드의 범위는 그대로 그 파일 하나다. 부모가 파일시스템 루트인 경우 폴더째 등록하면 디스크 전체가 열리므로 그 파일만 등록한다. assetProtocol의 scope는 객체로 적고 requireLiteralLeadingDot을 false로 둔다. unix 기본값이 true라 생략하면 점으로 시작하는 폴더 안의 파일을 파일 커맨드는 통과시키고 이미지만 거부한다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
이미지 경로의 기준을 문서 폴더 하나에서 문서 폴더·연 폴더 둘로 나눈다. `/`로 시작하는 경로는 연 폴더 기준으로 해석하고, 폴더를 열지 않은 경우 그대로 둔다. GitHub·VS Code가 같은 규칙이라 같은 문서가 세 곳에서 같은 파일을 가리킨다. 디스크 루트 기준으로 해석하면 그 표기를 쓴 문서가 조용히 다른 파일을 가리킨다. 연 폴더를 아는 것은 앱이므로 workspace 상태에서 읽어 프리뷰로 넘긴다. 상대 경로의 기준은 그대로 활성 탭이 있는 폴더다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
srcset이 있으면 브라우저는 src보다 그쪽을 먼저 쓴다. src만 바꾸면 srcset을 쓴 이미지는 문서와 같은 폴더에 파일이 있어도 렌더되지 않는다. img[srcset]과 picture 안의 source[srcset]을 후보마다 해석한다. 후보를 가르는 것은 URL 뒤에 오는 쉼표뿐이다. 쉼표로 통째로 쪼개면 이름에 쉼표가 든 파일이 두 후보로 갈려 둘 다 없는 파일이 된다. 후보가 asset:으로 시작하면 버린다. 무해한 후보 뒤에 붙인 asset URL은 sanitize를 통과한다 (실측) — src에서 막는 것과 같은 방어를 후보에도 건다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
"읽는 범위"·"서술자"·"스코프 미러링"은 이 저장소에 없던 말이라 문서 세 곳과 코드·테스트가 같은 이름으로 엮여 있었다. 절 제목을 "어느 폴더의 이미지를 읽는가"로 바꾸고 본문은 "읽을 수 있는 경로"로 되돌린다. 대조로 쓴 "~가 아니라 ~한다"와 앞 문단을 가리키는 말도 없앤다. 보안 문서의 이미지 절에서 어느 폴더를 읽는지 적힌 문장이 빠져 뒤따르는 "범위 밖"·"같은 폴더"가 가리킬 곳을 잃었다. 그 문장을 되살리고 주어를 고친다. scope.rs의 mirror_to_asset은 "같은 경로를 등록한다"고 적혀 있었으나 파일 허용에서는 부모 폴더를 등록한다. image-src.ts의 머리 주석도 기준이 문서 폴더 하나인 것처럼 남아 있었다. "src는 sanitize 뒤에 바꾼다" 절이 srcset도 다루므로 제목에서 src를 빼고, 그 앵커를 가리키던 세 곳을 함께 고친다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
이미지는 뜨는 조건과 뜨지 않는 조건이 갈리는데, 그 판정은 화면을 봐야 한다. 뜨는 경우 일곱과 뜨지 않는 경우 다섯을 한 문서에 담아 눈으로 대조하게 한다. 프리뷰-기본.md의 이미지 절은 이 문서로 옮긴다 — 한 표면은 한 파일이 담당한다. 예시 이미지 셋을 더한다. srcset의 1배·2배 후보가 갈리는 것을 색과 무늬로 구별하고, 이름에 공백이 든 파일과 점으로 시작하는 폴더 안의 파일을 각각 확인한다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
이미지 시나리오가 폴더를 연 상태만 봤다. 문서 폴더 기준·연 폴더 기준·srcset 후보·점으로 시작하는 폴더를 한 문서에 담아 확인하고, 없는 파일과 읽을 수 없는 경로에는 깨짐 표시가 붙는 것을 본다. 읽을 수 없는 경로는 파일이 실제로 있는 자리를 가리키므로, 스코프가 넓어지면 이 단언이 먼저 깨진다. 폴더 없이 파일 하나만 연 상태는 열기 다이얼로그·최근 파일·세션 복원으로 만들어지는데 WebDriver는 다이얼로그를 열 수 없다. webdriver 빌드에 한해 환경변수로 파일 하나를 허용 루트에 넣는다. 1×1 PNG 대신 예시 이미지를 복사해 쓴다 — 이 시나리오는 데모 영상으로 그대로 녹화된다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
계약대로 동작하는 것을 게이트와 실앱 E2E로 확인했다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Vite가 실행 중간에 @tauri-apps/plugin-store·react-dom/client를 발견해 의존성을 다시 묶고 페이지를 리로드한다. 그 순간 읽히던 테스트 파일이 러너 없는 페이지에서 실행돼 "Vitest failed to find the runner"로 죽었고, 리로드에 걸린 파일이 매번 달라 무작위로 보였다. node_modules/.vite를 지우면 재현된다. 두 의존성을 미리 넣은 뒤 차가운 캐시로 두 번, mise run check로 한 번 돌려 리로드가 나지 않는 것을 확인했다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
요약 / 의도
문서에 넣은 이미지를 프리뷰에 렌더한다. 파일 하나만 연 경우에도 문서와 같은 폴더의 이미지를 읽는다. VS Code도 작업 폴더에 속하지 않은 마크다운을 미리 볼 때 같은 폴더를 읽는다(
getMarkdownLocalResourceRoots).범위
이번에 한 것
/는 연 폴더 기준으로 해석한다. 기준 폴더가 없는 경우 그대로 둔다.img[srcset]·source[srcset]을 후보마다 해석하고, 후보가asset:으로 시작하는 경우 버린다.examples/이미지-예시.md를 만들고, 실앱 E2E에 파일만 연 경우를 더했다.의도적으로 안 한 것 / 후속
주요 변경점
packages/markdown): 기준 폴더를 둘로 나누고, sanitize 뒤에src·srcset을 바꾼다.src-tauri):FileScope의 허용·거부를 asset 스코프에도 등록한다.assetProtocol설정을 객체 형태로 바꿨다.NORII_E2E_SCOPE_FILE로 파일 하나만 연 상태를 만든다(webdriver 빌드 전용).optimizeDeps.include로 테스트 중 페이지 리로드를 막는다..claude): 읽는 폴더·경로 해석·해석하는 속성·권한·위험을 갱신했다.설계 · 결정
security.md#이미지가 소유한다./는 연 폴더 기준이다. 디스크 루트 기준으로 해석하는 경우 같은 문서가 GitHub·VS Code와 다른 파일을 가리킨다.assetProtocol.scope는 객체로 적고requireLiteralLeadingDot: false를 넣는다. unix 기본값이true라 생략하면 점으로 시작하는 폴더의 파일을 파일 커맨드는 통과시키고 이미지만 거부한다.flowchart LR A["FileScope.allow(경로)"] --> B{폴더인가} B -- 예 --> C["파일 커맨드: 하위 트리<br/>asset: 하위 트리"] B -- 아니오 --> D{부모가 루트인가} D -- 아니오 --> E["파일 커맨드: 그 파일만<br/>asset: 부모 폴더의 하위 트리"] D -- 예 --> F["둘 다 그 파일만"]검증 (체크가 아니라 값으로)
mise run check: 통과 — desktop 62파일 435개 · markdown 113개 · editor 28개 · rust 88개. 차가운node_modules/.vite로도 통과.mise run dev-webdriver→mise run e2e): 통과 23/23. 이미지 시나리오 둘이 문서 폴더 기준·연 폴더 기준·srcset 후보·점으로 시작하는 폴더·없는 파일·읽을 수 없는 경로·파일만 연 경우를 덮는다.http:이미지가 막히는지. 개발 서버는 CSP를 붙이지 않아 개발 빌드에서 확인되지 않는다.관련 문서 (단일 출처)
.claude/docs/preview-strategy.md— 어느 폴더의 이미지를 읽는가 · 경로 해석 · 해석하는 속성.claude/docs/rust-commands.md— 권한(asset 프로토콜).claude/docs/security.md— 이미지가 읽을 수 있는 경로와 남는 위험examples/이미지-예시.md— 눈으로 판정하는 예시리뷰 포인트 / 위험 지점
scope.rs의allow()파일 분기 — 두 스코프가 갈라지는 유일한 자리다.render.ts의parseSrcset— 후보 경계를 URL 뒤의 쉼표로만 판정한다.lib.rs의NORII_E2E_SCOPE_FILE— webdriver 피처에서만 컴파일된다.체크리스트 (프로젝트 규칙)
.claude/rules/non-goals.md)를 넘지 않음 — 인덱싱·DB가 생기지 않는다스크린샷 / 데모
norii-.mov
E2E 시나리오 둘을 그대로 녹화했다. 왼쪽 소스, 오른쪽 프리뷰 — 문서 폴더 기준·연 폴더 기준·srcset 2배 후보가 뜨고, 이어서 폴더 없이 파일만 연 문서의 옆 이미지가 뜬다.
마일스톤 / 비고
M10(문서 속 이미지). 브라우저 테스트 간헐 실패 수정은 이 마일스톤과 무관하지만
mise run check를 무작위로 실패시켜 함께 담았다.