feat(projects): 설명 마크다운 렌더링 + 저장소 README 가져오기
등록 모달의 '한 줄 설명' input 을 여러 줄 textarea 로 바꾸고, 설명을 markdown-it 로 렌더링한다(html:false 로 raw HTML 이스케이프·javascript: 링크 차단 — XSS 안전). 상세 화면은 마크다운으로, 카드 미리보기는 기호를 걷어낸 순수 텍스트(mdPlain) + 3줄 클램프로 표시. '저장소 README 가져오기' 버튼과 GET /api/readme 라우트를 추가해 repo_url 의 Gitea README 를 설명란으로 불러올 수 있다(gitea.js fetchReadme). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
// 뷰 헬퍼 — 빌드/프런트 프레임워크 없이 서버에서 SVG 를 직접 만들어 EJS 로 출력한다.
|
||||
// (디자인 레퍼런스의 Octicon 패스 / identicon 알고리즘 / 언어색을 그대로 포팅)
|
||||
// res.locals 에 실어 모든 뷰에서 icon()/identicon()/langColor() 로 사용.
|
||||
// res.locals 에 실어 모든 뷰에서 icon()/identicon()/langColor()/renderMarkdown() 로 사용.
|
||||
import MarkdownIt from "markdown-it";
|
||||
|
||||
// GitHub Octicon 16x16 패스 모음.
|
||||
const OCTICONS = {
|
||||
@@ -39,6 +40,39 @@ export function icon(name, opts = {}) {
|
||||
);
|
||||
}
|
||||
|
||||
// 프로젝트 설명(README 형식) 마크다운 렌더링.
|
||||
// html:false → 원문 속 raw HTML 은 태그가 아니라 텍스트로 이스케이프되어 XSS 를 막는다.
|
||||
// markdown-it 기본 validateLink 가 javascript:/vbscript:/data: 링크도 차단한다.
|
||||
const md = new MarkdownIt({ html: false, linkify: true, breaks: true });
|
||||
// 마크다운 링크는 새 탭으로 열고 noopener 를 붙인다.
|
||||
const defaultLinkOpen =
|
||||
md.renderer.rules.link_open ||
|
||||
function (tokens, idx, options, env, self) { return self.renderToken(tokens, idx, options); };
|
||||
md.renderer.rules.link_open = function (tokens, idx, options, env, self) {
|
||||
tokens[idx].attrSet("target", "_blank");
|
||||
tokens[idx].attrSet("rel", "noopener nofollow");
|
||||
return defaultLinkOpen(tokens, idx, options, env, self);
|
||||
};
|
||||
export function renderMarkdown(src) {
|
||||
return md.render(String(src || ""));
|
||||
}
|
||||
|
||||
// 카드 미리보기용 — 마크다운 기호를 걷어낸 순수 텍스트 한 덩어리.
|
||||
// (제목 #, 목록 -, 강조 *_`, 링크 [텍스트](url) → 텍스트, 코드펜스 ``` 제거)
|
||||
export function mdPlain(src) {
|
||||
return String(src || "")
|
||||
.replace(/```[\s\S]*?```/g, " ") // 코드펜스 블록 제거
|
||||
.replace(/`([^`]*)`/g, "$1") // 인라인 코드
|
||||
.replace(/!\[[^\]]*\]\([^)]*\)/g, " ") // 이미지
|
||||
.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1") // 링크 → 텍스트
|
||||
.replace(/^\s{0,3}#{1,6}\s+/gm, "") // 제목 마커
|
||||
.replace(/^\s{0,3}>\s?/gm, "") // 인용 마커
|
||||
.replace(/^\s*[-*+]\s+/gm, "") // 목록 마커
|
||||
.replace(/[*_~]{1,3}/g, "") // 강조 기호
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
// 언어 → 색(카드의 언어 점).
|
||||
const LANG_COLORS = {
|
||||
Python: "#3572A5", TypeScript: "#3178c6", JavaScript: "#f1e05a",
|
||||
|
||||
Reference in New Issue
Block a user