Rocks Template

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <!-- 设置页面 favicon,如果有 shareFavicon 关系则使用指定图片,否则使用默认 favicon -->
    <link rel="shortcut icon" href="<% if (note.hasRelation('shareFavicon')) { %>api/notes/<%= note.getRelation('shareFavicon').value %>/download<% } else { %>../favicon.ico<% } %>">
    
    <!-- 加载共享的 JavaScript 文件,用于页面功能支持 -->
    <script src="../<%= appPath %>/share.js"></script>
    
    <!-- 加载 normalize.css,用于重置浏览器默认样式,确保跨浏览器一致性 -->
    <link href="../<%= assetPath %>/libraries/normalize.min.css" rel="stylesheet">

    <!-- 如果当前笔记有 shareSwagger 标签,加载 Swagger UI 相关资源 -->
    <% if (note.hasLabel('shareSwagger')) { %>
        <!-- TODO: 这些 note ID 应改为可自定义 -->
        <!-- 加载 Swagger UI 的 CSS 样式 -->
        <link href="api/notes/woA8jsLWd4QR/download" rel="stylesheet">
        <!-- 加载 Swagger UI 的 JavaScript 文件 -->
        <script src="api/notes/RYOdL9flwQfP/download"></script>
        <script>
            // 在 DOM 加载完成后初始化 Swagger UI
            document.addEventListener("DOMContentLoaded", function() {
                // 定义自定义服务器配置,用于替换默认的 Swagger YAML
                const customServerYml = `- url: "{protocol}://{domain}:{port}/etapi"
    variables:
      protocol:
        enum:
          - http
          - https
        default: http
        description: Protocol your server is being hosted with
      domain:
        default: localhost
        description: Domain name or localhost or ip
      port:
        default: 37840
        description: Port the app is served over`;

                // 初始化 Swagger UI
                SwaggerUIBundle({
                    url: `<%= note.getLabelValue("shareSwagger") %>`, // 使用 shareSwagger 标签指定的 URL
                    dom_id: "#content", // 将 Swagger UI 渲染到 #content 元素中
                    responseInterceptor: resp => {
                        // 拦截响应,动态修改 Swagger YAML
                        if (resp.url !== `<%= note.getLabelValue("shareSwagger") %>`) return resp;
                        resp.text = resp.text.replace("- url: http://localhost:37740/etapi", "- url: http://localhost:37840/etapi");
                        resp.text = resp.text.replace(`- url: http://localhost:8080/etapi`, customServerYml);
                        return resp;
                    }
                });
            });
        </script>
    <% } %>

    <!-- 如果笔记类型为 text 或 book,加载 CKEditor 的内容样式 -->
    <% if (note.type === 'text' || note.type === 'book') { %>
        <link href="../<%= assetPath %>/libraries/ckeditor/ckeditor-content.css" rel="stylesheet">
    <% } %>
    
    <!-- 加载所有 shareCss 关系指定的自定义 CSS 文件 -->
    <% for (const cssRelation of note.getRelations('shareCss')) { %>
        <link href="api/notes/<%= cssRelation.value %>/download" rel="stylesheet">
    <% } %>
    
    <!-- 加载所有 shareJs 关系指定的自定义 JavaScript 文件(模块化) -->
    <% for (const jsRelation of note.getRelations('shareJs')) { %>
        <script type="module" src="api/notes/<%= jsRelation.value %>/download"></script>
    <% } %>
    
    <!-- 如果有 shareDisallowRobotIndexing 标签,禁止搜索引擎索引 -->
    <% if (note.hasLabel('shareDisallowRobotIndexing')) { %>
        <meta name="robots" content="noindex,follow" />
    <% } %>
    
    <%
        // 设置页面标题,包含当前笔记标题和子根笔记标题(如果不同)
        const pageTitle = `${note.title}${note.noteId !== subRoot.note.noteId ? ` - ${subRoot.note.title}` : ""}`;

        // 设置 OpenGraph 相关变量,用于社交媒体分享预览
        const openGraphColor = subRoot.note.getLabelValue("shareOpenGraphColor");
        const openGraphURL = subRoot.note.getLabelValue("shareOpenGraphURL");
        const openGraphDomain = subRoot.note.getLabelValue("shareOpenGraphDomain");
        let openGraphImage = subRoot.note.getLabelValue("shareOpenGraphImage");
        // 如果有 shareOpenGraphImage 关系,使用关系指定的图片
        if (subRoot.note.hasRelation("shareOpenGraphImage")) {
            openGraphImage = `api/images/${subRoot.note.getRelation("shareOpenGraphImage").value}/image.png`;
        }
    %>
    <!-- 设置页面标题 -->
    <title><%= pageTitle %></title>
    
    <!-- HTML 元标签:页面描述 -->
    <meta name="description" content="<%= note.getLabelValue('shareDescription') %>">
    
    <!-- Facebook OpenGraph 元标签 -->
    <meta property="og:url" content="<%= openGraphURL %>">
    <meta property="og:type" content="website">
    <meta property="og:title" content="<%= pageTitle %>">
    <meta property="og:description" content="<%= note.getLabelValue('shareDescription') %>">
    <meta property="og:image" content="<%= openGraphImage %>">
    
    <!-- Twitter Card 元标签 -->
    <meta name="twitter:card" content="summary_large_image">
    <meta property="twitter:domain" content="<%= openGraphDomain %>">
    <meta property="twitter:url" content="<%= openGraphURL %>">
    <meta name="twitter:title" content="<%= pageTitle %>">
    <meta name="twitter:description" content="<%= note.getLabelValue('shareDescription') %>">
    <meta name="twitter:image" content="<%= openGraphImage %>">
    
    <!-- 设置主题颜色,用于浏览器界面(如地址栏) -->
    <meta name="theme-color" content="<%= openGraphColor %>">
</head>
<%
    // 设置 logo 尺寸及移动端高度计算
    const logoWidth = subRoot.note.getLabelValue("shareLogoWidth");
    const logoHeight = subRoot.note.getLabelValue("shareLogoHeight");
    const mobileLogoHeight = logoHeight && logoWidth ? 32 / (logoWidth / logoHeight) : "";
    
    // 设置根链接,优先使用 shareRootLink 标签值,否则使用子根笔记 ID
    const shareRootLink = subRoot.note.hasLabel("shareRootLink") ? subRoot.note.getLabelValue("shareRootLink") : `./${subRoot.note.noteId}`;
    
    // 设置当前主题,默认暗色,除非 shareTheme 明确指定为 light
    const currentTheme = note.getLabel("shareTheme") === "light" ? "light" : "dark";
    const themeClass = currentTheme === "light" ? " theme-light" : " theme-dark";
    
    // 正则表达式匹配标题标签(h1-h6),并添加锚点链接
    const headingRe = /(<h[1-6]>)(.+?)(<\/h[1-6]>)/g;
    const headingMatches = [...content.matchAll(headingRe)];
    const slugify = (text) => text.toLowerCase().replace(/[^\w]/g, "-");
    content = content.replaceAll(headingRe, (...match) => {
        match[0] = match[0].replace(match[3], `<a id="${slugify(match[2])}" class="toc-anchor" name="${slugify(match[2])}" href="#${slugify(match[2])}">#</a>${match[3]}`);
        return match[0];
    });
%>
<body data-note-id="<%= note.noteId %>" class="type-<%= note.type %><%= themeClass %>" data-ancestor-note-id="<%= subRoot.note.noteId %>">
    <!-- 根据本地存储的主题偏好动态调整 body 的类 -->
    <script>
        const preference = localStorage.getItem("theme");
        if (preference) {
            if (preference === "dark") {
                document.body.classList.add("theme-dark");
                document.body.classList.remove("theme-light");
            } else {
                document.body.classList.remove("theme-dark");
                document.body.classList.add("theme-light");
            }
        }
    </script>
    
    <!-- 移动端头部导航 -->
    <div id="mobile-header">
        <a href="<%= shareRootLink %>">
            <% if (subRoot.note.hasRelation("shareLogo")) { %>
                <!-- 显示 logo 图片,如果有 shareLogo 关系 -->
                <img src="api/images/<%= subRoot.note.getRelation('shareLogo').value %>/image.png" width="<%= logoWidth %>" height="<%= mobileLogoHeight %>" alt="Logo" />
            <% } %>
            <%= subRoot.note.title %>
        </a>
        <!-- 移动端菜单按钮 -->
        <button id="show-menu-button">
            <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
                <path d="M4 6h16v2H4zm0 5h16v2H4zm0 5h16v2H4z"></path>
            </svg>
        </button>
    </div>
    
    <!-- 主布局:左右分栏 -->
    <div id="split-pane">
        <!-- 左侧导航栏 -->
        <div id="left-pane">
            <div id="navigation">
                <!-- 网站头部 -->
                <div id="site-header">
                    <a href="<%= shareRootLink %>">
                        <% if (subRoot.note.hasRelation("shareLogo")) { %>
                            <!-- 显示 logo 图片,如果有 shareLogo 关系 -->
                            <img src="api/images/<%= subRoot.note.getRelation('shareLogo').value %>/image.png" width="<%= logoWidth %>" height="<%= logoHeight %>" alt="Logo" />
                        <% } %>
                        <%= subRoot.note.title %>
                    </a>
                    <!-- 主题切换开关 -->
                    <div class="theme-selection">
                        Site Theme
                        <label class="switch">
                            <input type="checkbox" checked="<%= currentTheme === 'dark' %>">
                            <span class="slider"></span>
                            <!-- 暗色主题图标 -->
                            <svg class="dark-icon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
                                <path d="M20.742 13.045a8.088 8.088 0 0 1-2.077.271c-2.135 0-4.14-.83-5.646-2.336a8.025 8.025 0 0 1-2.064-7.723A1 1 0 0 0 9.73 2.034a10.014 10.014 0 0 0-4.489 2.582c-3.898 3.898-3.898 10.243 0 14.143a9.937 9.937 0 0 0 7.072 2.93 9.93 9.93 0 0 0 7.07-2.929 10.007 10.007 0 0 0 2.583-4.491 1.001 1.001 0 0 0-1.224-1.224zm-2.772 4.301a7.947 7.947 0 0 1-5.656 2.343 7.953 7.953 0 0 1-5.658-2.344c-3.118-3.119-3.118-8.195 0-11.314a7.923 7.923 0 0 1 2.06-1.483 10.027 10.027 0 0 0 2.89 7.848 9.972 9.972 0 0 0 7.848 2.891 8.036 8.036 0 0 1-1.484 2.059z"></path>
                            </svg>
                            <!-- 亮色主题图标 -->
                            <svg class="light-icon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
                                <path d="M6.993 12c0 2.761 2.246 5.007 5.007 5.007s5.007-2.246 5.007-5.007S14.761 6.993 12 6.993 6.993 9.239 6.993 12zM12 8.993c1.658 0 3.007 1.349 3.007 3.007S13.658 15.007 12 15.007 8.993 13.658 8.993 12 10.342 8.993 12 8.993zM10.998 19h2v3h-2zm0-17h2v3h-2zm-9 9h3v2h-3zm17 0h3v2h-3zM4.219 18.363l2.12-2.122 1.415 1.414-2.12 2.122zM16.24 6.344l2.122-2.122 1.414 1.414-2.122 2.122zM6.342 7.759 4.22 5.637l1.415-1.414 2.12 2.122zm13.434 10.605-1.414 1.414-2.122-2.122 1.414-1.414z"></path>
                            </svg>
                        </label>
                    </div>
                    <!-- 搜索框 -->
                    <div class="search-item">
                        <svg class="search-icon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
                            <path d="M10 18a7.952 7.952 0 0 0 4.897-1.688l4.396 4.396 1.414-1.414-4.396-4.396A7.952 7.952 0 0 0 18 10c0-4.411-3.589-8-8-8s-8 3.589-8 8 3.589 8 8 8zm0-14c3.309 0 6 2.691 6 6s-2.691 6-6 6-6-2.691-6-6 2.691-6 6-6z"></path>
                        </svg>
                        <input type="text" class="search-input" placeholder="Search...">
                    </div>
                </div>
                <!-- 如果子根笔记有可见子节点,显示导航菜单 -->
                <% if (subRoot.note.hasVisibleChildren()) { %>
                    <nav id="menu">
                        <%
                        // 计算当前笔记的祖先节点,用于高亮导航
                        const ancestors = [];
                        let notePointer = note;
                        while (notePointer.parents[0].noteId !== "_share") {
                            const pointerParent = notePointer.parents[0];
                            ancestors.push(pointerParent.noteId);
                            notePointer = pointerParent;
                        }
                        %>
                        <!-- 渲染导航树 -->
                        <%- include("tree_item", {note: subRoot.note, activeNote: note, subRoot: subRoot, ancestors: ancestors}) %>
                    </nav>
                <% } %>   
            </div>
        </div>
        <!-- 右侧主内容区域 -->
        <div id="right-pane">
            <div id="main">
                <!-- 内容区域 -->
                <div id="content" class="type-<%= note.type %><% if (note.type === 'text') { %> ck-content<% } %><% if (isEmpty) { %> no-content<% } %>">
                    <h1 id="title"><%= note.title %></h1>
                    <% if (isEmpty && (!note.hasVisibleChildren() && note.type !== "book")) { %>
                        <!-- 如果内容为空且无子节点,显示提示 -->
                        <p>This note has no content.</p>
                    <% } else { %>
                        <!-- 渲染笔记内容 -->
                        <%- content %>
                    <% } %>
                </div>

                <!-- 如果有可见子节点或类型为 book,显示子页面导航 -->
                <% if (note.hasVisibleChildren() || note.type === 'book') { %>
                    <nav id="childLinks" class="<% if (isEmpty) { %>grid<% } else { %>list<% } %>">
                        <% if (!isEmpty) { %>
                            <span>Subpages:</span>
                        <% } %>
                        <ul>
                            <%
                            // 根据笔记类型选择获取子节点的方法
                            const action = note.type === "book" ? "getChildNotes" : "getVisibleChildNotes";
                            for (const childNote of note[action]()) {
                                // 判断是否为外部链接
                                const isExternalLink = childNote.hasLabel("shareExternal") || childNote.hasLabel("shareExternalLink");
                                const linkHref = isExternalLink ? childNote.getLabelValue("shareExternal") ?? childNote.getLabelValue("shareExternalLink") : `./${childNote.shareId}`;
                                const target = isExternalLink ? ` target="_blank" rel="noopener noreferrer"` : "";
                            %>
                                <li>
                                    <a class="type-<%= childNote.type %>" href="<%= linkHref %>"<%= target %>><%= childNote.title %></a>
                                </li>
                            <% } %>
                        </ul>
                    </nav>
                <% } %>
            </div>
            <!-- 如果页面有多个标题,生成目录(TOC) -->
            <%
            if (headingMatches.length > 1) {
                const level = (m) => parseInt(m[1].replace(/[<h>]+/g, ""));
                const toc = [
                    {
                        level: level(headingMatches[0]),
                        name: headingMatches[0][2],
                        children: []
                    }
                ];
                const last = (arr = toc) => arr[arr.length - 1];
                const makeEntry = (m) => ({level: level(m), name: m[2], children: []});
                const getLevelArr = (lvl, arr = toc) => {
                    if (arr[0].level === lvl) return arr;
                    const top = last(arr);
                    return top.children.length ? getLevelArr(lvl, top.children) : top.children;
                };
                for (let m = 1; m < headingMatches.length; m++) {
                    const target = getLevelArr(level(headingMatches[m]));
                    target.push(makeEntry(headingMatches[m]));
                }
            %>
                <div id="toc-pane">
                    <h3>On This Page</h3>
                    <ul id="toc">
                        <% for (const entry of toc) { %>
                            <!-- 渲染目录项 -->
                            <%- include("toc_item", {entry}) %>
                        <% } %>
                    </ul>
                </div>
            <% } %>
        </div>
    </div>
</body>
</html>