feat: 临时访问增加水印

This commit is contained in:
xiaozzzi 2024-04-17 00:41:54 +08:00
parent c5866839fd
commit 64b9a61276
3 changed files with 295 additions and 53 deletions

View File

@ -265,7 +265,13 @@ public class ArticleController {
} }
String reportHtml = ArticleUtil.toHtml(article, String reportHtml = ArticleUtil.toHtml(article,
userService.selectById(AuthContext.getUserId()), userService.selectById(AuthContext.getUserId()),
userParamService.getValue(AuthContext.getUserId(), UserParamEnum.WEB_BLOG_COLOR).getParamValue()); userParamService.getValue(AuthContext.getUserId(), UserParamEnum.WEB_BLOG_COLOR).getParamValue(),
userParamService.getValue(AuthContext.getUserId(), UserParamEnum.WEB_BLOG_WATERMARK_ENABLED).getParamValue(),
userParamService.getValue(AuthContext.getUserId(), UserParamEnum.WEB_BLOG_WATERMARK_CONTENT).getParamValue(),
userParamService.getValue(AuthContext.getUserId(), UserParamEnum.WEB_BLOG_WATERMARK_FONTSIZE).getParamValue(),
userParamService.getValue(AuthContext.getUserId(), UserParamEnum.WEB_BLOG_WATERMARK_COLOR).getParamValue(),
userParamService.getValue(AuthContext.getUserId(), UserParamEnum.WEB_BLOG_WATERMARK_GAP).getParamValue()
);
try (InputStream is = new ByteArrayInputStream(reportHtml.getBytes(StandardCharsets.UTF_8)); try (InputStream is = new ByteArrayInputStream(reportHtml.getBytes(StandardCharsets.UTF_8));
BufferedInputStream bis = new BufferedInputStream(is)) { BufferedInputStream bis = new BufferedInputStream(is)) {
String filename = URLEncodeUtil.encode(article.getName() + ".html"); String filename = URLEncodeUtil.encode(article.getName() + ".html");
@ -339,6 +345,12 @@ public class ArticleController {
return ArticleUtil.toHtml( return ArticleUtil.toHtml(
article, article,
userService.selectById(visit.getUserId()), userService.selectById(visit.getUserId()),
userParamService.getValue(visit.getUserId(), UserParamEnum.WEB_BLOG_COLOR).getParamValue()); userParamService.getValue(visit.getUserId(), UserParamEnum.WEB_BLOG_COLOR).getParamValue(),
userParamService.getValue(visit.getUserId(), UserParamEnum.WEB_BLOG_WATERMARK_ENABLED).getParamValue(),
userParamService.getValue(visit.getUserId(), UserParamEnum.WEB_BLOG_WATERMARK_CONTENT).getParamValue(),
userParamService.getValue(visit.getUserId(), UserParamEnum.WEB_BLOG_WATERMARK_FONTSIZE).getParamValue(),
userParamService.getValue(visit.getUserId(), UserParamEnum.WEB_BLOG_WATERMARK_COLOR).getParamValue(),
userParamService.getValue(visit.getUserId(), UserParamEnum.WEB_BLOG_WATERMARK_GAP).getParamValue()
);
} }
} }

View File

@ -1,18 +1,16 @@
package com.blossom.backend.server.utils; package com.blossom.backend.server.utils;
import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.blossom.backend.base.user.pojo.UserEntity; import com.blossom.backend.base.user.pojo.UserEntity;
import com.blossom.backend.server.article.draft.pojo.ArticleEntity; import com.blossom.backend.server.article.draft.pojo.ArticleEntity;
import com.blossom.common.base.util.DateUtils; import com.blossom.common.base.enums.YesNo;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.Date;
/** /**
* 文章工具类 * 文章工具类
@ -105,7 +103,7 @@ public class ArticleUtil {
/** /**
* *
*/ */
private static final String SCRIPT_TAG_BLOG_COLOR = " <script>\n" + private static final String HEAD_SCRIPT_BLOG_COLOR = " <script>\n" +
" window.addEventListener('load', function() {\n" + " window.addEventListener('load', function() {\n" +
" const rgb = '{BLOSSOM_WEB_BLOG_COLOR}'\n" + " const rgb = '{BLOSSOM_WEB_BLOG_COLOR}'\n" +
" if (rgb && !rgb.toLowerCase().startsWith('rgb(')) {\n" + " if (rgb && !rgb.toLowerCase().startsWith('rgb(')) {\n" +
@ -128,8 +126,210 @@ public class ArticleUtil {
" </script>" + " </script>" +
"</head>"; "</head>";
private static final String HEAD_SCRIPT_WATERMARK = "<script>\n" +
" window.addEventListener(\"load\", function () {\n" +
" const FontGap = 3\n" +
"\n" +
" function prepareCanvas(width, height, ratio = 1){\n" +
" const canvas = document.createElement('canvas')\n" +
" const ctx = canvas.getContext('2d')\n" +
" const realWidth = width * ratio\n" +
" const realHeight = height * ratio\n" +
" canvas.setAttribute('width', `${realWidth}px`)\n" +
" canvas.setAttribute('height', `${realHeight}px`)\n" +
" ctx.save()\n" +
" return [ctx, canvas, realWidth, realHeight]\n" +
" }\n" +
"\n" +
" // Get single clips\n" +
" function getClips(content, rotate, ratio, width, height, font, gapX, gapY) {\n" +
" // ================= Text =================\n" +
" const [ctx, canvas, contentWidth, contentHeight] = prepareCanvas(width, height, ratio)\n" +
" const {color, fontSize, fontStyle, fontWeight, fontFamily } = font\n" +
" const mergedFontSize = Number(fontSize) * ratio\n" +
" ctx.font = `${fontStyle} normal ${fontWeight} ${mergedFontSize}px/${height}px ${fontFamily}`\n" +
" ctx.fillStyle = color\n" +
" ctx.textAlign = 'center'\n" +
" ctx.textBaseline = 'top'\n" +
" const contents = Array.isArray(content) ? content : [content]\n" +
" contents?.forEach((item, index) => {\n" +
" ctx.fillText(\n" +
" item ?? 'ccccc',\n" +
" contentWidth / 2,\n" +
" index * (mergedFontSize + FontGap * ratio)\n" +
" )\n" +
" })\n" +
"\n" +
" // ==================== Rotate ====================\n" +
" const angle = (Math.PI / 180) * Number(rotate)\n" +
" const maxSize = Math.max(width, height)\n" +
" const [rCtx, rCanvas, realMaxSize] = prepareCanvas(maxSize, maxSize, ratio)\n" +
"\n" +
" // Copy from `ctx` and rotate\n" +
" rCtx.translate(realMaxSize / 2, realMaxSize / 2)\n" +
" rCtx.rotate(angle)\n" +
" if (contentWidth > 0 && contentHeight > 0) {\n" +
" rCtx.drawImage(canvas, -contentWidth / 2, -contentHeight / 2)\n" +
" }\n" +
"\n" +
" // Get boundary of rotated text\n" +
" function getRotatePos(x, y) {\n" +
" const targetX = x * Math.cos(angle) - y * Math.sin(angle)\n" +
" const targetY = x * Math.sin(angle) + y * Math.cos(angle)\n" +
" return [targetX, targetY]\n" +
" }\n" +
"\n" +
" let left = 0\n" +
" let right = 0\n" +
" let top = 0\n" +
" let bottom = 0\n" +
"\n" +
" const halfWidth = contentWidth / 2\n" +
" const halfHeight = contentHeight / 2\n" +
" const points = [\n" +
" [0 - halfWidth, 0 - halfHeight],\n" +
" [0 + halfWidth, 0 - halfHeight],\n" +
" [0 + halfWidth, 0 + halfHeight],\n" +
" [0 - halfWidth, 0 + halfHeight],\n" +
" ]\n" +
" points.forEach(([x, y]) => {\n" +
" const [targetX, targetY] = getRotatePos(x, y)\n" +
" left = Math.min(left, targetX)\n" +
" right = Math.max(right, targetX)\n" +
" top = Math.min(top, targetY)\n" +
" bottom = Math.max(bottom, targetY)\n" +
" })\n" +
"\n" +
" const cutLeft = left + realMaxSize / 2\n" +
" const cutTop = top + realMaxSize / 2\n" +
" const cutWidth = right - left\n" +
" const cutHeight = bottom - top\n" +
"\n" +
" // ================ Fill Alternate ================\n" +
" const realGapX = gapX * ratio\n" +
" const realGapY = gapY * ratio\n" +
" const filledWidth = (cutWidth + realGapX) * 2\n" +
" const filledHeight = cutHeight + realGapY\n" +
"\n" +
" const [fCtx, fCanvas] = prepareCanvas(filledWidth, filledHeight)\n" +
"\n" +
" function drawImg(targetX = 0, targetY = 0) {\n" +
" fCtx.drawImage(rCanvas, cutLeft, cutTop, cutWidth, cutHeight, targetX, targetY, cutWidth, cutHeight)\n" +
" }\n" +
" drawImg()\n" +
" drawImg(cutWidth + realGapX, -cutHeight / 2 - realGapY / 2)\n" +
" drawImg(cutWidth + realGapX, +cutHeight / 2 + realGapY / 2)\n" +
" return [fCanvas.toDataURL(), filledWidth / ratio, filledHeight / ratio]\n" +
" }\n" +
" \n" +
" const getMarkSize = (ctx, content) => {\n" +
" let defaultWidth = 120\n" +
" let defaultHeight = 64\n" +
" const width = 120\n" +
" const height = 64\n" +
" if (ctx.measureText) {\n" +
" ctx.font = `${Number('15')}px sans-serif`\n" +
" const contents = Array.isArray(content) ? content : [content]\n" +
" const sizes = contents.map((item) => {\n" +
" const metrics = ctx.measureText(item)\n" +
"\n" +
" return [metrics.width,\n" +
" metrics.fontBoundingBoxAscent !== undefined\n" +
" ? metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent\n" +
" : metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent,\n" +
" ]\n" +
" })\n" +
" defaultWidth = Math.ceil(Math.max(...sizes.map((size) => size[0])))\n" +
" defaultHeight =\n" +
" Math.ceil(Math.max(...sizes.map((size) => size[1]))) * contents.length +\n" +
" (contents.length - 1) * FontGap\n" +
" }\n" +
" return [width ?? defaultWidth, height ?? defaultHeight]\n" +
" }\n" +
" \n" +
" function toLowercaseSeparator(key) {\n" +
" return key.replace(/([A-Z])/g, '-$1').toLowerCase()\n" +
" }\n" +
"\n" +
" function getStyleStr(style) {\n" +
" return Object.keys(style)\n" +
" .map(\n" +
" (key) =>\n" +
" `${toLowercaseSeparator(key)}: ${style[key]};`\n" +
" )\n" +
" .join(' ')\n" +
" }\n" +
"\n" +
" const getMarkStyle = (rotate) => {\n" +
" const markStyle = {zIndex: 9, position: 'absolute', left: 0, top: 0, width: '100%', height: '100%', pointerEvents: 'none', backgroundRepeat: 'repeat' }\n" +
"\n" +
" /** Calculate the style of the offset */\n" +
" let positionLeft = rotate / 2 - rotate / 2\n" +
" let positionTop = rotate / 2 - rotate / 2\n" +
" if (positionLeft > 0) {\n" +
" markStyle.left = `${positionLeft}px`\n" +
" markStyle.width = `calc(100% - ${positionLeft}px)`\n" +
" positionLeft = 0\n" +
" }\n" +
" if (positionTop > 0) {\n" +
" markStyle.top = `${positionTop}px`\n" +
" markStyle.height = `calc(100% - ${positionTop}px)`\n" +
" positionTop = 0\n" +
" }\n" +
" markStyle.backgroundPosition = `${positionLeft}px ${positionTop}px`\n" +
" return markStyle\n" +
" }\n" +
" \n" +
" let stopObservation = false\n" +
" const observerContainer = document.getElementsByClassName('bl-preview')[0]\n" +
" const observerConfig = { attributes: true, childList: true, subtree: true };\n" +
" const observer = new MutationObserver(() => {\n" +
" renderWatermark()\n" +
" });\n" +
"\n" +
" const renderWatermark = () => {\n" +
" const canvas = document.createElement(\"canvas\")\n" +
" const ctx = canvas.getContext(\"2d\")\n" +
" const rotate = -22\n" +
" const content = '{WEB_BLOG_WATERMARK_CONTENT}'\n" +
" const color = '{WEB_BLOG_WATERMARK_COLOR}'\n" +
" const fontSize = {WEB_BLOG_WATERMARK_FONTSIZE}\n" +
" const gap = {WEB_BLOG_WATERMARK_GAP}\n" +
"\n" +
" if (ctx) {\n" +
" const ratio = window.devicePixelRatio || 1\n" +
" const [markWidth, markHeight] = getMarkSize(ctx, content)\n" +
"\n" +
" const drawCanvas = (drawContent) => {\n" +
" const [textClips, clipWidth] = getClips(drawContent, rotate, ratio, markWidth, markHeight, \n" +
" {color: color, fontSize: fontSize, fontStyle: 'normal', fontWeight: 'normal'}, gap, gap\n" +
" )\n" +
" let watermark = document.createElement('div')\n" +
" let container = document.getElementsByClassName('bl-preview')[0]\n" +
" stopObservation = true\n" +
" observer.disconnect()\n" +
" watermark.setAttribute('style',getStyleStr({\n" +
" ...getMarkStyle(rotate),\n" +
" backgroundImage: `url('${textClips}')`,\n" +
" backgroundSize: `${Math.floor(clipWidth)}px`,\n" +
" })\n" +
" )\n" +
" container.append(watermark)\n" +
" setTimeout(() => {\n" +
" observer.observe(observerContainer, observerConfig)\n" +
" })\n" +
" };\n" +
" \n" +
" drawCanvas(content);\n" +
" }\n" +
" };\n" +
"\n" +
" renderWatermark()\n" +
" })\n" +
" </script>";
private static final String prefix = "\n" +
private static final String BODY_HEADER_AND_TOC = "\n" +
"<body><div class=\"header\">\n" + "<body><div class=\"header\">\n" +
" <div class=\"copyright\">本文作者:{BLOSSOM_EXPORT_HTML_AUTHOR}。著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。</div><a\n" + " <div class=\"copyright\">本文作者:{BLOSSOM_EXPORT_HTML_AUTHOR}。著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。</div><a\n" +
" href=\"https://github.com/blossom-editor/blossom\" target=\"_blank\"><span>Export by Blossom</span><svg\n" + " href=\"https://github.com/blossom-editor/blossom\" target=\"_blank\"><span>Export by Blossom</span><svg\n" +
@ -142,18 +342,18 @@ public class ArticleUtil {
" <div class=\"toc\" id=\"blossom-toc\">\n" + " <div class=\"toc\" id=\"blossom-toc\">\n" +
" <div style=\"font-size: 15px;color:#727272;padding:10px 0\">《{BLOSSOM_EXPORT_HTML_ARTICLE_NAME}》</div>\n" + " <div style=\"font-size: 15px;color:#727272;padding:10px 0\">《{BLOSSOM_EXPORT_HTML_ARTICLE_NAME}》</div>\n" +
" <div style=\"font-size: 20px;color:#727272;border-bottom:2px solid #eaeaea;padding-bottom: 10px;margin-bottom: 10px;\">目录</div>\n" + " <div style=\"font-size: 20px;color:#727272;border-bottom:2px solid #eaeaea;padding-bottom: 10px;margin-bottom: 10px;\">目录</div>\n" +
" </div><div class=\"main bl-preview\" id=\"blossom-view\">"; " </div><div class=\"main\"><div class=\"bl-preview\" id=\"blossom-view\">";
private static final String suffix = "</div></div></body></html>"; private static final String suffix = "</div></div></div></body></html>";
private static String htmlTag; private static String htmlTemplate;
static { static {
Resource resource = new ClassPathResource("exportTemplate.html"); Resource resource = new ClassPathResource("exportTemplate.html");
try (InputStream is = resource.getInputStream()) { try (InputStream is = resource.getInputStream()) {
byte[] bytes = new byte[is.available()]; byte[] bytes = new byte[is.available()];
is.read(bytes); is.read(bytes);
htmlTag = new String(bytes); htmlTemplate = new String(bytes);
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
} }
@ -165,45 +365,71 @@ public class ArticleUtil {
* @param article 文章 * @param article 文章
* @param user 用户, 用户获取作者 * @param user 用户, 用户获取作者
* @param blogColor 主颜色 * @param blogColor 主颜色
* @param WEB_BLOG_WATERMARK_ENABLED 开启水印
* @param WEB_BLOG_WATERMARK_CONTENT 水印内容
* @param WEB_BLOG_WATERMARK_FONTSIZE 水印字体大小
* @param WEB_BLOG_WATERMARK_COLOR 水印颜色
* @param WEB_BLOG_WATERMARK_GAP 水印密集度
* @return html 内容 * @return html 内容
*/ */
public static String toHtml(ArticleEntity article, UserEntity user, String blogColor) { public static String toHtml(ArticleEntity article,
return htmlTag + UserEntity user,
SCRIPT_TAG_BLOG_COLOR String blogColor,
.replaceAll("\\{BLOSSOM_WEB_BLOG_COLOR}", blogColor) + String WEB_BLOG_WATERMARK_ENABLED,
// 替换作者, 文章名称 String WEB_BLOG_WATERMARK_CONTENT,
prefix String WEB_BLOG_WATERMARK_FONTSIZE,
String WEB_BLOG_WATERMARK_COLOR,
String WEB_BLOG_WATERMARK_GAP) {
return htmlTemplate
+ appendHeadScript(blogColor,
WEB_BLOG_WATERMARK_ENABLED,
WEB_BLOG_WATERMARK_CONTENT,
WEB_BLOG_WATERMARK_FONTSIZE,
WEB_BLOG_WATERMARK_COLOR,
WEB_BLOG_WATERMARK_GAP)
+ appendBodyHeader(article, user)
+ article.getHtml()
+ suffix;
}
/**
* 添加 head 下的动态 script
*
* @param blogColor 主题色
* @param WEB_BLOG_WATERMARK_ENABLED 开启水印
* @param WEB_BLOG_WATERMARK_CONTENT 水印内容
* @param WEB_BLOG_WATERMARK_FONTSIZE 水印字体大小
* @param WEB_BLOG_WATERMARK_COLOR 水印颜色
* @param WEB_BLOG_WATERMARK_GAP 水印密集度
* @return head 下的所有动态 script
*/
private static String appendHeadScript(String blogColor,
String WEB_BLOG_WATERMARK_ENABLED,
String WEB_BLOG_WATERMARK_CONTENT,
String WEB_BLOG_WATERMARK_FONTSIZE,
String WEB_BLOG_WATERMARK_COLOR,
String WEB_BLOG_WATERMARK_GAP) {
String script = HEAD_SCRIPT_BLOG_COLOR.replaceAll("\\{BLOSSOM_WEB_BLOG_COLOR}", blogColor);
if (YesNo.YES.getValue().toString().equals(WEB_BLOG_WATERMARK_ENABLED)) {
script += HEAD_SCRIPT_WATERMARK
.replaceAll("\\{WEB_BLOG_WATERMARK_CONTENT}", WEB_BLOG_WATERMARK_CONTENT)
.replaceAll("\\{WEB_BLOG_WATERMARK_FONTSIZE}", WEB_BLOG_WATERMARK_FONTSIZE)
.replaceAll("\\{WEB_BLOG_WATERMARK_COLOR}", WEB_BLOG_WATERMARK_COLOR)
.replaceAll("\\{WEB_BLOG_WATERMARK_GAP}", WEB_BLOG_WATERMARK_GAP);
}
return script + "</head>";
}
/**
* 增加页面顶部的文章作者和目录顶部的文章名称
*
* @param article 文章
* @param user 用户, 用户获取作者
* @return 页面顶部的文章作者和文章名称
*/
private static String appendBodyHeader(ArticleEntity article, UserEntity user) {
return BODY_HEADER_AND_TOC
.replaceAll("\\{BLOSSOM_EXPORT_HTML_AUTHOR}", user.getNickName()) .replaceAll("\\{BLOSSOM_EXPORT_HTML_AUTHOR}", user.getNickName())
.replaceAll("\\{BLOSSOM_EXPORT_HTML_ARTICLE_NAME}", article.getName()) + .replaceAll("\\{BLOSSOM_EXPORT_HTML_ARTICLE_NAME}", article.getName());
article.getHtml() + suffix;
}
private static void genHeatmap() {
Date begin = DateUtils.parse("2023-04-01", DateUtils.PATTERN_YYYYMMDD);
Date end = DateUtils.parse("2023-06-30", DateUtils.PATTERN_YYYYMMDD);
for (; DateUtils.compare(begin, end) <= 0; begin = DateUtils.offsetDay(begin, 1)) {
String dt = DateUtils.format(begin, DateUtils.PATTERN_YYYYMMDD);
int value = RandomUtil.randomInt(0, 10);
System.out.println(String.format("insert into blossom_stat values(null, 1, '%s' ,%s);", dt, value));
}
}
private static void genWords() {
int value = 203012;
Date begin = DateUtils.parse("2019-01-01", DateUtils.PATTERN_YYYYMMDD);
Date end = DateUtils.parse("2023-06-30", DateUtils.PATTERN_YYYYMMDD);
for (; DateUtils.compare(begin, end) <= 0; begin = DateUtils.offsetMonth(begin, 1)) {
String dt = DateUtils.format(begin, DateUtils.PATTERN_YYYYMMDD);
value = value + RandomUtil.randomInt(0, 10000);
System.out.println(String.format("insert into blossom_stat values(null, 2, '%s' ,%s);", dt, value));
}
}
public static void main(String[] args) {
// ArticleEntity a = new ArticleEntity();
// a.setName("123123213");
// a.setHtml("asdasd");
// System.out.println(exportHtml(a));
} }
} }

View File

@ -1358,6 +1358,9 @@
color: #c5c5c5; color: #c5c5c5;
border-bottom: 1px solid #eaeaea; border-bottom: 1px solid #eaeaea;
text-align: right; text-align: right;
position: fixed;
z-index: 999;
background-color: #fff;
} }
.header .copyright { .header .copyright {
@ -1380,7 +1383,7 @@
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: flex-start; justify-content: flex-start;
align-items: flex-start align-items: flex-start;
} }
.content .main { .content .main {
@ -1388,7 +1391,8 @@
width: calc(100% - 300px); width: calc(100% - 300px);
padding: 20px; padding: 20px;
overflow-x: hidden; overflow-x: hidden;
overflow-y: scroll overflow-y: scroll;
margin-top: 30px;
} }
.toc { .toc {
@ -1398,6 +1402,7 @@
border-right: 1px solid #eeeeee; border-right: 1px solid #eeeeee;
overflow-x: hidden; overflow-x: hidden;
overflow-y: scroll; overflow-y: scroll;
margin-top: 30px;
} }
.toc > h1, .toc > h1,
@ -1434,8 +1439,6 @@
transition: opacity 0.1s; transition: opacity 0.1s;
} }
.toc > h1:hover, .toc > h1:hover,
.toc > h2:hover, .toc > h2:hover,
.toc > h3:hover, .toc > h3:hover,
@ -1615,6 +1618,7 @@
background-color: var(--bl-preview-bg-color); background-color: var(--bl-preview-bg-color);
line-height: 23px; line-height: 23px;
font-family: 'Microsoft YaHei', '微软雅黑', Arial, sans-serif; font-family: 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
position: relative;
} }
.bl-preview * { .bl-preview * {
@ -2034,7 +2038,6 @@
for (let i = 0; i < heads.length; i++) { for (let i = 0; i < heads.length; i++) {
let head = heads[i]; let head = heads[i];
let tocHead = document.createElement(head.localName); let tocHead = document.createElement(head.localName);
console.log(head.id);
tocHead.setAttribute('class', `toc-${head.localName}`); tocHead.setAttribute('class', `toc-${head.localName}`);
tocHead.innerText = head.innerText; tocHead.innerText = head.innerText;
tocHead.onclick = function () { tocHead.onclick = function () {
@ -2042,7 +2045,8 @@
if (ele == null || ele == undefined) { if (ele == null || ele == undefined) {
return return
} }
ele.parentNode.scrollTop = ele.offsetTop - 30 // ele.parentNode.scrollTop = ele.offsetTop - 30
ele.scrollIntoView()
}; };
tocContainer.appendChild(tocHead) tocContainer.appendChild(tocHead)
} }