思源笔记折腾记录 - 再再再复杂一点的发布效果 - 文档大纲

本贴最后更新于 736 天前,其中的信息可能已经时移俗易

一、前情提要

之前,我们用管线渲染的方式,实现了基本和思源的导出效果相似的笔记发布显示:

思源笔记折腾记录 - 简单发布 - 做一点发布交互 - 链滴 (ld246.com)

但是我们来看一下 obsidian 的发布:

image

很齐全的啊,反向链接、文档树、关系图那些都在嘛,好像我们的少了一些什么。。。。

所以这回我们给它补上这些东西。

二、实现过程:

我们之前的页面框架是这样的:

<!DOCTYPE html><html>
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/>
    <meta name="apple-mobile-web-app-capable" content="yes">
    <meta name="mobile-web-app-capable" content="yes"/>
    <meta name="apple-mobile-web-app-status-bar-style" content="black">
    <link rel="stylesheet" type="text/css" id="themeDefaultStyle" href="stage/build/export/base.css?2.5.2"/>
    <link rel="stylesheet" type="text/css" id="themeStyle" href="appearance/themes/daylight/theme.css?2.5.2"/>
    <title>C:\Users\al765\Desktop\请从这里开始 - 思源笔记  v2.5.2</title>
    <style>
        body {background-color: var(--b3-theme-background);color: var(--b3-theme-on-background)}
        .b3-typography, .protyle-wysiwyg, .protyle-title {font-size:16px !important}
.b3-typography code:not(.hljs), .protyle-wysiwyg span[data-type~=code] { font-variant-ligatures: none }
.li > .protyle-action {height:34px;line-height: 34px}
.protyle-wysiwyg [data-node-id].li > .protyle-action ~ .h1, .protyle-wysiwyg [data-node-id].li > .protyle-action ~ .h2, .protyle-wysiwyg [data-node-id].li > .protyle-action ~ .h3, .protyle-wysiwyg [data-node-id].li > .protyle-action ~ .h4, .protyle-wysiwyg [data-node-id].li > .protyle-action ~ .h5, .protyle-wysiwyg [data-node-id].li > .protyle-action ~ .h6 {line-height:34px;}
.protyle-wysiwyg [data-node-id].li > .protyle-action:after {height: 16px;width: 16px;margin:-8px 0 0 -8px}
.protyle-wysiwyg [data-node-id].li > .protyle-action svg {height: 14px}
.protyle-wysiwyg [data-node-id] [spellcheck="false"] {min-height:26px;}
.protyle-wysiwyg [data-node-id] { text-align: justify;}
.protyle-wysiwyg .li {min-height:34px}
.protyle-gutters button svg {height:26px}
.protyle-wysiwyg img.emoji, .b3-typography img.emoji {width:18px}
.protyle-wysiwyg .h1 img.emoji, .b3-typography h1 img.emoji {width:35px}
.protyle-wysiwyg .h2 img.emoji, .b3-typography h2 img.emoji {width:31px}
.protyle-wysiwyg .h3 img.emoji, .b3-typography h3 img.emoji {width:27px}
.protyle-wysiwyg .h4 img.emoji, .b3-typography h4 img.emoji {width:25px}
.protyle-wysiwyg .h5 img.emoji, .b3-typography h5 img.emoji {width:22px}
.protyle-wysiwyg .h6 img.emoji, .b3-typography h6 img.emoji {width:20px}
    </style>
</head>
<body>
<div class="protyle-wysiwyg protyle-wysiwyg--attr" style="max-width: 800px;margin: 0 auto;" id="publish-content">
{{content}}
</div>
<script src="appearance/icons/material/icon.js?2.5.2"></script>
<script src="stage/build/export/protyle-method.js?2.5.2"></script>
<script src="stage/protyle/js/lute/lute.min.js?2.5.2"></script>  
<script>
    window.siyuan = {
      config: {
        appearance: { mode: 0, codeBlockThemeDark: "base16/dracula", codeBlockThemeLight: "github" },
        editor: { 
          codeLineWrap: true,
          codeLigatures: false,
          plantUMLServePath: "https://www.plantuml.com/plantuml/svg/~1",
          codeSyntaxHighlightLineNum: true,
          katexMacros: JSON.stringify({}),
        }
      },
      languages: {copy:"复制"}
    };
    const previewElement = document.getElementById('preview');
    Protyle.highlightRender(previewElement, "stage/protyle");
    Protyle.mathRender(previewElement, "stage/protyle", false);
    Protyle.mermaidRender(previewElement, "stage/protyle");
    Protyle.flowchartRender(previewElement, "stage/protyle");
    Protyle.graphvizRender(previewElement, "stage/protyle");
    Protyle.chartRender(previewElement, "stage/protyle");
    Protyle.mindmapRender(previewElement, "stage/protyle");
    Protyle.abcRender(previewElement, "stage/protyle");
    Protyle.plantumlRender(previewElement, "stage/protyle");
    document.querySelectorAll(".protyle-action__copy").forEach((item) => {
      item.addEventListener("click", (event) => {
            navigator.clipboard.writeText(item.parentElement.nextElementSibling.textContent.trimEnd());
            event.preventDefault();
            event.stopPropagation();
      })
    });
</script></body></html>

里面好像就没有文档树那些的入口,所以首先我们给它补上这些。

为了能够最大限度地兼容思源的各种主题,所以我们还需要一个跟思源的编辑界面框架类似的结构。

就像这样:

<body class="fn__flex-column body--win32">
  <!--这里到时候用来做导航栏-->
  <div id="toolbar" class="toolbar fn__flex publishNavi"></div>
  <!--这里是布局的主体-->
  <div class="fn__flex-1 fn__flex">
    <div id="layouts" class="layout fn__flex-1 fn__flex-column" style="transition: var(--b3-width-transition);">
      <div class="fn__flex-1 fn__flex">

        <div class="fn__flex-column fn__flex-shrink" id="panelLeft"
          style="width: 300px; transition: var(--b3-width-transition);">
        </div>
        <div class="layout__resize--lr layout__resize" id="resizeLeft"></div>

        <div class="layout__center fn__flex fn__flex-1" style="transition: var(--b3-width-transition);">
          <div class="fn__flex fn__flex-1">
            <div class="layout-tab-container fn__flex-1">
              <div class="fn__flex-1 protyle" data-id="cdf2875f-40f2-45aa-8d8b-f92c23284907">
                <div class="protyle-wysiwyg protyle-wysiwyg--attr" style="max-width: 800px;margin: 0 auto;"
                  id="publish-content">
                  {{content}}
                </div>
              </div>
            </div>
          </div>
        </div>


        <div class="layout__resize--lr layout__resize" id="resizeRight"></div>

        <div class="fn__flex-column" id="panelRight" style="width: 300px; transition: var(--b3-width-transition);">
        </div>

      </div>
    </div>
  </div>
  <!--这里用来放状态栏和页脚那些-->
  <div id="status" class="fn__flex status"></div>
</body>

再改一下之前的页脚,要不然它的位置不对:

function 添加页脚(document){
//不插入到文档最后了,插入到status里去
//    document.body.insertAdjacentHTML('beforeend',`

    document.getElementById('status').insertAdjacentHTML('beforeend',`
    <div  style='text-align:center'>
       <div> poweredBy <a href='https://b3log.org/siyuan/' target='_blank'>siyuan@${window.siyuan.config.system.kernelVersion}</a> with <a target='_blank' href='https://www.chuanchengsheji.com'>noob </a></div>
       <div> <a href='https://afdian.net/a/leolee9086'>请noob作者喝一杯咖啡</a></div>
    </div>
    `)
}

之后我们还要加很多渲染步骤,所以这里我们把那些渲染函数全都抽离到模板里面,就像这样:

import { 获取全部原始数据 } from "../../../noob-service-syPublishServer/server/util/pipe.js"
function 渲染页面内容(document) {
    let 页面数据 = 获取全部原始数据(document)
    document.getElementById('publish-content').innerHTML = 页面数据.content
}
function 禁用编辑(document) {
    document.getElementById('publish-content').querySelectorAll(`[contenteditable]`).forEach(
        element => { element.setAttribute('contenteditable', false) }
    )
}
function 修改块链接(document) {
    document.head.insertAdjacentHTML('beforeend', '<style>span a{color:inherit !important}</style>')
    document.querySelectorAll('span[data-type="block-ref"]').forEach(
        块链接 => {
            let 锚文本 = 块链接.innerText
            块链接.innerHTML = `<a href='/${块链接.getAttribute('data-id')}'>${锚文本}</a>`
        }
    )
}
function 修改超链接(document) {
    document.querySelectorAll('span[data-type="a"]').forEach(
        块链接 => {
            let 锚文本 = 块链接.innerText
            块链接.innerHTML = `<a href='${块链接.getAttribute('data-href')}'>${锚文本}</a>`
        }
    )
}
function 添加页脚(document) {
    document.getElementById('status').insertAdjacentHTML('beforeend', `
    <div fn__flex style='width:30%'></div>
    <div class="fn__flex  fn__flex-1" style='text-align:center;margin:auto' >
       <div> poweredBy <a href='https://b3log.org/siyuan/' target='_blank'>siyuan@${window.siyuan.config.system.kernelVersion}</a> with <a target='_blank' href='https://www.chuanchengsheji.com'>noob </a></div>
       <div> 
        <a href='https://afdian.net/a/leolee9086'>请noob作者喝一杯咖啡</a>
        </div>

       </div>
       <div fn__flex style='width:30%'></div>
    `)
}

export let 渲染管线 = [
    渲染页面内容, 
    禁用编辑, 
    修改块链接, 
    修改超链接, 
    添加页脚
]

现在我们就可以进一步拆分他们了。

1、生成大纲

生成大纲需要使用 getDocOutline 接口,但是我们的发布服务并没有提供这个接口(之后也会没有这个接口),所以要把它注入到渲染的文档里面去。

所以要改一下 注入思源文档原始数据,把大纲也注入进去。

export async function 注入思源文档原始数据(req, res, 渲染结果) {
    let 块id = req.params.blockID
    let 页面内容数据 = await 获取文档内容(块id)
    let 大纲数据 = await noobApi.核心api.getDocOutline(
        { id: 块id },
    
      );
      页面内容数据.docOutline = 大纲数据
    //把所有的数据全都注入到这个元素里面去
    初始化原始数据(页面内容数据, 渲染结果)
}

这个时候在浏览器端调用一下:

获取全部原始数据(document)

可以看到里面已经有了 docOutline 这一项了。

image

之后也是一样,所有的数据先注入到文档里面去, 再渲染出来。

因为渲染大纲这一步比较长,所以把它单独写到一个文件里面去:

import { 获取全部原始数据 } from "../../../noob-service-syPublishServer/server/util/pipe.js";
function 生成面板(){
    let html =`
    <div class="fn__flex fn__flex-1" style="min-height: 64px; transition: var(--b3-width-transition); height: 652px;">
        <div data-type="wnd" data-id="" class="fn__flex-column fn__flex fn__flex-1">
            <ul class="fn__flex layout-tab-bar"></ul>
            <div class = "layout-tab-container fn__flex-1">
                <div class="fn__flex-1 fn__flex-column file-tree sy__outline">
                    <div class="block__icons">
                        <div class="block__logo">
                            <svg><use xlink:href="#iconAlignCenter"></use></svg>
                            大纲
                        </div>                
                    </div>
                </div> 
            </div>
        </div>
    </div>
    `
    return html
  
}
export  function 生成文档大纲(document) {
   let 大纲内容 =  获取全部原始数据(document).docOutline
   console.log(获取全部原始数据(document))
   let html = 渲染大纲(大纲内容, document);
   let 大纲面板html = 生成面板()
   document.getElementById('panelLeft').innerHTML+=大纲面板html
   document.getElementById('panelLeft').querySelector('.sy__outline').innerHTML +=html
}
 function  渲染大纲(大纲内容, document) {
    console.log(大纲内容)
    let 文档条目 = `
        <div class="b3-list-item" title="${获取全部原始数据(document).docInfo.name.replace(
          ".sy",
          ""
        )}">
        <span class="b3-list-item__graphic">
        <svg class="custom-icon">
        <use xlink:href="#icon-1f4c4"></use>
        </svg>
        </span>
        <span class="b3-list-item__text">${获取全部原始数据(document).docInfo.name.replace(
          ".sy",
          ""
        )}</span>
        </div>`;
    let 大纲容器 = `<div clas=fn__flex-1>
            <ul class="b3-list b3-list--background">
                ${渲染大纲条目内容(大纲内容, document)}
            <ul>
        </div>
    
        `;
    return 文档条目 + 大纲容器;
  }
  function  渲染大纲条目内容(大纲内容, document) {
    let html = "";
    大纲内容.forEach((大纲条目) => {
      html += `
<li 
class="b3-list-item b3-list-item--hide-action" 
data-node-id="${大纲条目.id}" 
data-ref-text="" 
data-def-id="" 
data-type="NodeHeading" 
data-subtype="${大纲条目.subType}" 
data-treetype="outline" 
data-def-path="">
    <span style="padding-left: ${
      16 * parseInt(大纲条目.subType[1])
    }px" class="b3-list-item__toggle">
        <svg data-id="${
          大纲条目.id
        }" class="b3-list-item__arrow fn__hidden b3-list-item__arrow--open">
        <use xlink:href="#iconRight"></use></svg>
    </span>
    <svg data-defids="[""]" class="b3-list-item__graphic popover__block" data-id="${
      大纲条目.id
    }">
        <use xlink:href="#icon${大纲条目.subType.toUpperCase()}"></use>
    </svg>
    <span class="b3-list-item__text" title="${大纲条目.name||大纲条目.content}"><a href="/${
        大纲条目.id
      }">${大纲条目.name||大纲条目.content}</a></span>  
</li>
            `;
      if (大纲条目.blocks) {
        html += `<ul class>${渲染大纲条目内容(
          大纲条目.blocks,
          document
        )}</ul>`;
      }
  
    });
    return html;
  }

cosplay 完成。

这个时候我们看一下渲染页面,效果很好,就像这样:

image

。。。。。。。。。。好像事情不太对

我们之前在发布模板头部引入的 css 是这个:

  <link rel="stylesheet" type="text/css" id="themeDefaultStyle" href="stage/build/export/base.css?2.5.2" />

然后找找思源的原生界面,它的头部里面有个这个:

<link href="base.7da6ee6d3d4c59f3b784.css" rel="stylesheet">

看到了没有,它也有个 base,所以试试把它复制到 doc.html 中,我们会看到:

image

image

emm 出错了,这个时候我们看下思源界面的控制台,会发现一个报错

image

因为之前的路由是这样的:

image

直接匹配了根路由,所以要修改一下它才行,改成这样:

发布应用.use('/block/:blockID', async (req, res, next) => {
    if (await 判定文档权限(req.params.blockID)) {
        next()
    } else {
        res.status('403')
        res.setHeader('Content-Type', "text/html;charset=utf-8");
        res.end('不可访问此文档')

    }
}, 默认渲染管线)

之前首页的重定向肯定也就要改到

发布应用.use('/', async (req, res, next) => {
    req.url == '/' ? res.redirect('/block/20200812220555-lj3enxa') : null
    next()
})

但是我们之前的页面很多资源都是这样的:

  <link rel="stylesheet" type="text/css" id="themeStyle" href="appearance/themes/daylight/theme.css" />

如果直接这样改它们可能就解析到 /block 下面了

所以还需要加上这样一个元素来指定 base

  <base id="baseURL" href="/">

这时候你再试一下:

image

它还是 404 了,因为这个中间有一大串 id 的 base.css 的实际位置是在 stage/build/app/base.<一串随机id>.css 中。

所以要稍微改一下引入的方式,让它能够正确获取。

显然每次都去复制黏贴思源的基础样式虽然不是不行但是还是有点蛋疼。

所以来搞个自动获取:

发布应用.use('/base',async (req,res)=>{
    if(req.url.endsWith('siyuanBase.css')){
        res.setHeader('Content-Type', "text/css;charset=utf-8");
        //res.end(await 窃取思源基础css())
	//读书人的事怎么能叫窃
	res.end(await 获取思源基础css())

    }
})
async function 获取思源基础css(){
    let cssURL = document.querySelector('link[href^="base"]').getAttribute('href')
    return await(await fetch(window.location.pathname+cssURL)).text()
}

不过其实这个 css 文件倒也不是经常变,所以其实获取一次存到个变量里就可以了

async function 获取思源基础css(){
    let cssURL = document.querySelector('link[href^="base"]').getAttribute('href')
    return await(await fetch(window.location.pathname+cssURL)).text()
}
let 思源基础css内容 = await 获取思源基础css()

发布应用.use('/base',async (req,res)=>{
    if(req.url.endsWith('siyuanBase.css')){
        res.setHeader('Content-Type', "text/css;charset=utf-8");
    
        res.end(思源基础css内容)
    }
})

ok,我们的渲染效果现在长这样了:

image

对比一下发现好像少了些图标:

imageimage

嗯,这个因为缺少了一些 emoji 脚本,直接用思源的 emoji 加上:

  <script src="/appearance/emojis/twitter-emoji.js?v=1.0.1" async="" id="emojiScript"></script>

好了,现在它的样子就对了, 我们已经成功给它加上了一个文档树.

2、大纲交互

这个时候我们点击大纲的内容会发现它不是按我们想要的像在思源里面一样跳转到页面上的标题元素,所以要搞点事情。

为了实现点击跳转我们知道多半是要搞一个事件监听器了,在服务器这边加上的事件监听器到了浏览器就寄了,所以只能给渲染效果加上一点前端的代码,按照很多博客程序的习惯,弄个 static 文件夹到模板里面吧,所以服务器的路由也得加上:

发布应用.use('/static', express.static(代码片段路径 + 'publishTemplate/default/static'))

然后在页面模板里面引入:

  <script src="/static/scroll.js" async="" id="scrollScript"></script>

这个文件的内容是这样的:

function 滚动到页面内元素(id) {
    let 页面内元素 = document.querySelector(`.protyle-wysiwyg.protyle-wysiwyg--attr [data-node-id='${id}']`)
    if (页面内元素) {
        页面内元素.scrollIntoView({
            behavior: "smooth",
            block: "center"
        })
        setTimeout(
            () => {
                let style = 页面内元素.getAttribute("style")
                页面内元素.style.border = "2.5px dashed var(--b3-card-info-color)"
                页面内元素.style.backgroundColor = "var(--b3-card-info-background)"
                setTimeout(
                    () => { 页面内元素.setAttribute("style", style) }, 1000
                )

            }, 50
        )
    }

}
document.addEventListener('click', (event) => {

    let target = event.target
    let href = target.getAttribute("href")
    if (href) {
        let id = href.split("/").pop().replace('#','')
        let reg = /^\d{14}\-[0-9a-z]{7}$/
        let 页面内元素 = document.querySelector(`.protyle-wysiwyg.protyle-wysiwyg--attr [data-node-id='${id}']`)
        if (reg.test(id) && 页面内元素) {
            event.stopPropagation()
            event.preventDefault()
            滚动到页面内元素(id)
        }
    }
})
window.addEventListener(
    "load", () => {
        let id = window.location.href.split("/").pop().replace('#','')
        let reg = /^\d{14}\-[0-9a-z]{7}$/
        if (reg.test(id)) {
            setTimeout(
                () => 滚动到页面内元素(id)
                , 1000
            )
        }
    }
)

页面内元素.scrollIntoView({behavior: "smooth",block: "center"}) 的意思是让这个页面“平滑地”滚到可视范围内。

现在可以看看它的效果了,看起来害不错嗷:

大纲滚动

其实还有一个地方要改,答案就在文章最开头,不过估计是个人就会,所以就算了。。。。


目前的代码片段的地址位于:

leolee9086/snippets (github.com)

viteWidgets 的地址位于

leolee9086/viteWidgets (github.com)

  • 思源笔记

    思源笔记是一款隐私优先的个人知识管理系统,支持完全离线使用,同时也支持端到端加密同步。

    融合块、大纲和双向链接,重构你的思维。

    23014 引用 • 92575 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...