Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,21 +24,21 @@
<nav
id="sidebar"
style="scroll-behavior: smooth"
class="dark:bg-gray-dark-100 fixed top-0 z-40 hidden h-full w-full flex-none overflow-x-hidden overflow-y-auto bg-[#f9f9fa] md:sticky md:top-12 md:z-auto md:block md:h-[calc(100vh-64px)] md:w-[320px]"
class="dark:bg-gray-dark-100 fixed top-0 z-40 hidden h-full w-full flex-none overflow-x-hidden overflow-y-auto bg-[#f9f9fa] md:sticky md:top-16 md:z-auto md:block md:h-[calc(100vh-64px)] md:w-[320px]"
>
<nav-component></nav-component>
</nav>

<!-- content: (마크다운 파일) 표시 공간 : 중간 부분 -->
<div
id="content"
class="dark:bg-background-dark w-full min-w-0 bg-white p-10"
class="markdown-content dark:bg-background-dark w-full min-w-0 bg-white p-10"
></div>

<!-- aside: 오른쪽 부분 사이드 -->
<aside
id="aside-toc"
class="sticky top-12 h-full overflow-y-auto px-2 py-5 hidden lg:block min-w-60"
class="sticky top-16 h-full overflow-y-auto px-2 py-5 hidden lg:block min-w-60"
>
<div id="toc" class="text-[14px] text-black font-bold w-full"></div>
</aside>
Expand Down
61 changes: 46 additions & 15 deletions src/scripts/breadcrumb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,29 +107,60 @@ async function generateBreadcrumbItems(): Promise<BreadcrumbItem[]> {
return breadcrumbItems;
}

/**
* XSS 방지를 위한 HTML escape
*/
function escapeHtml(text: string): string {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}

/**
* href 속성값을 안전하게 escape
* 따옴표, 꺾쇠괄호 등을 HTML 엔티티로 변환
*/
function escapeHtmlAttribute(value: string): string {
return value
.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}

/**
* Breadcrumb 아이템을 HTML 문자열로 변환
*/
function renderBreadcrumbItem(item: BreadcrumbItem, isLast: boolean): string {
const escapedName = escapeHtml(item.name);
const escapedPath = escapeHtmlAttribute(item.path);

if (isLast) {
return `<span class="truncate text-gray-400 dark:text-gray-300">${escapedName}</span>`;
}

if (item.linkable) {
return `<a href="${escapedPath}" class="truncate text-blue-500 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 transition-colors">${escapedName}</a> / `;
}

// linkable=false인 항목은 회색으로 표시 (클릭 불가 시각 표시)
return `<span class="truncate text-gray-500 dark:text-gray-400">${escapedName}</span> / `;
}

/**
* Breadcrumb HTML 요소를 생성합니다.
*/
function createBreadcrumbElement(items: BreadcrumbItem[]): HTMLElement {
const breadcrumbNav = document.createElement('nav');
breadcrumbNav.id = 'breadcrumbs';
breadcrumbNav.className =
'pb-3 flex min-w-0 items-center gap-2 text-gray-400 dark:text-gray-300';
// nav 요소에서 색상 클래스 제거 (자식 요소에서 색상 관리)
breadcrumbNav.className = 'pb-3 flex min-w-0 items-center gap-2';

const breadcrumbHTML = items
.map((item, index) => {
const isLast = index === items.length - 1;

if (isLast) {
return `<span class="truncate">${item.name}</span>`;
}

if (!item.linkable) {
return `<span class="truncate text-blue-500">${item.name}</span> / `;
} else {
return `<a href="${item.path}" class="link truncate">${item.name}</a> / `;
}
})
.map((item, index) =>
renderBreadcrumbItem(item, index === items.length - 1)
)
.join('');

breadcrumbNav.innerHTML = breadcrumbHTML;
Expand Down
Loading