@jx3box/jx3box-editor 2.2.40 → 2.2.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/assets/js/a.js CHANGED
@@ -1,43 +1,141 @@
1
- // 链接标签
2
- import url from 'url'
3
- import { resolveImagePath } from "@jx3box/jx3box-common/js/utils";
4
- function formatLink(str){
5
- if(!str) return
6
-
7
- const REG = /<a(.*?)href=[\'"]([^\'"]+)[\'"](.*?)>(.*?)<\/a>/gi
8
- let item;
9
- while ((item = REG.exec(str))) {
10
- let origin = item[0]
11
- let prefix = item[1]
12
-
13
- let href = item[2]
14
- let to = url.parse(href)
15
- const whiteHost = /https?:\/\/.*\.jx3box\.com/
16
-
17
- if(to.host == 'oss.jx3box.com'){
18
- href = resolveImagePath(href)
19
- }
20
-
21
-
22
- let skip = ''
23
- if(!whiteHost.test(to.hostname)){
24
- skip = 'target="_blank"'
25
- }
26
-
27
- // markdown中的锚点跳转
28
- let isHashAnchor = href.startsWith('#')
29
- if(isHashAnchor){
30
- skip = 'target="_self"'
31
- }
32
-
33
- let suffix = item[3]
34
- let text = item[4]
35
-
36
- let output = `<a ${skip} ${prefix} href="${href}" ${suffix}>${text}</a>`
37
- str = str.replace(origin,output)
38
- // console.log(origin,output)
39
- }
40
- // console.log(str)
41
- return str
1
+ // formatLink.js
2
+ import { load } from "cheerio";
3
+
4
+ /** 规范化 whitelist:统一小写、去空格、允许传入带协议的域名 */
5
+ function normalizeWhitelist(list = []) {
6
+ return (Array.isArray(list) ? list : [])
7
+ .map((x) => String(x || "").trim().toLowerCase())
8
+ .filter(Boolean)
9
+ .map((x) => {
10
+ // 允许用户写 https://xxx.com
11
+ try {
12
+ if (/^https?:\/\//i.test(x)) return new URL(x).hostname.toLowerCase();
13
+ } catch {
14
+ console.warn(`formatLink: 无法解析 whitelist 域名 "${x}",已忽略该项`);
15
+ }
16
+ return x;
17
+ });
18
+ }
19
+
20
+ /** 是否相对链接(默认认为站内,视为白名单) */
21
+ function isRelativeHref(href = "") {
22
+ const s = href.trim();
23
+ if (!s) return true;
24
+ if (s.startsWith("#") || s.startsWith("?") || s.startsWith("/")) return true;
25
+ if (s.startsWith("./") || s.startsWith("../")) return true;
26
+ // 没有 scheme 且不是 //xxx,通常是相对路径(如 a/b)
27
+ return !/^[a-zA-Z][a-zA-Z0-9+\-.]*:/.test(s) && !s.startsWith("//");
28
+ }
29
+
30
+ /** href 里提取 hostname(只处理 http/https/协议相对 //) */
31
+ function getHostnameFromHref(href = "") {
32
+ const s = href.trim();
33
+ if (!s) return "";
34
+
35
+ if (s.startsWith("//")) {
36
+ try {
37
+ return new URL("https:" + s).hostname.toLowerCase();
38
+ } catch {
39
+ return "";
40
+ }
41
+ }
42
+
43
+ if (/^https?:\/\//i.test(s)) {
44
+ try {
45
+ return new URL(s).hostname.toLowerCase();
46
+ } catch {
47
+ return "";
48
+ }
49
+ }
50
+
51
+ return "";
52
+ }
53
+
54
+ /**
55
+ * whitelist 规则:
56
+ * - "xxx.com" 只匹配根域
57
+ * - "*.xxx.com" 只匹配子域(a.xxx.com),不匹配根域(xxx.com)
58
+ */
59
+ function matchHost(hostname, whitelist) {
60
+ const host = (hostname || "").toLowerCase();
61
+ if (!host) return false;
62
+
63
+ for (const rule of whitelist) {
64
+ if (!rule) continue;
65
+
66
+ if (rule.startsWith("*.")) {
67
+ const root = rule.slice(2);
68
+ if (root && host.endsWith("." + root)) return true;
69
+ continue;
70
+ }
71
+
72
+ if (host === rule) return true;
73
+ }
74
+ return false;
75
+ }
76
+
77
+ /**
78
+ * @param {string} html - HTML 字符串(可为片段)
79
+ * @param {string[]} whitelist - 例如 ["xxx.com", "*.xxx.com"]
80
+ * @param {boolean} strict - true 时,非白名单链接 href 强制置空
81
+ */
82
+ export default function formatLink(html, whitelist = ["*.jx3box.com"], strict = false) {
83
+ if (!html) return html;
84
+
85
+ const wl = normalizeWhitelist(whitelist);
86
+ const hasWhitelist = wl.length > 0;
87
+
88
+ // cheerio 用 root 包一下,保证片段也能正常选取
89
+ const $ = load(`<root>${html}</root>`, { decodeEntities: false });
90
+
91
+ $("a[href]").each((_, el) => {
92
+ const a = $(el);
93
+ const hrefRaw = a.attr("href") ?? "";
94
+ const href = String(hrefRaw).trim();
95
+ if (!href) return;
96
+
97
+ // markdown 锚点:强制 _self
98
+ if (href.startsWith("#")) {
99
+ a.attr("target", "_self");
100
+ return;
101
+ }
102
+
103
+ // 安全兜底:javascript: 一律干掉
104
+ if (/^javascript:/i.test(href)) {
105
+ a.attr("href", "");
106
+ return;
107
+ }
108
+
109
+ // mailto/tel 等不参与白名单判断(你也可以按 strict 强行置空,这里先按常见做法放行)
110
+ if (/^(mailto:|tel:|sms:)/i.test(href)) {
111
+ return;
112
+ }
113
+
114
+ // 相对链接默认视为站内
115
+ let isWhite = true;
116
+
117
+ if (!isRelativeHref(href)) {
118
+ const host = getHostnameFromHref(href);
119
+ isWhite = matchHost(host, wl);
120
+ } else {
121
+ isWhite = true;
122
+ }
123
+
124
+ // 只有设置了 whitelist 才触发“非白名单处理”
125
+ if (hasWhitelist && !isWhite) {
126
+ if (strict) {
127
+ a.attr("href", "");
128
+ return;
129
+ }
130
+
131
+ // 如果本身就是 _blank,就不管;否则强制 _blank
132
+ const target = String(a.attr("target") || "").toLowerCase();
133
+ if (target !== "_blank") {
134
+ a.attr("target", "_blank");
135
+ }
136
+ }
137
+ });
138
+
139
+ // 取出 root 里原始内容
140
+ return $("root").html() || "";
42
141
  }
43
- export default formatLink
@@ -1,38 +1,120 @@
1
- // iframe白名单
2
- import url from 'url'
3
- function iframeFilter(str){
4
- if(!str) return
5
-
6
- const REG = /<iframe(.*?)src=[\'"]([^\'"]+)[\'"](.*?)>.*?<\/iframe>/gi
7
- const white_list = ['player.bilibili.com','docs.qq.com',"open.douyu.com"]
8
-
9
- let item;
10
- while ((item = REG.exec(str))) {
11
- let origin = item[0]
12
- let prefix = item[1]
13
-
14
- // bilibili路径处理
15
- let src = ''
16
- if(item[2].startsWith('//')){
17
- src = item[2].replace('//','https://')
18
- }else{
19
- src = item[2]
20
- }
1
+ // iframeFilter.js
2
+ import { load } from "cheerio";
3
+
4
+ function normalizeWhitelist(list = []) {
5
+ return (Array.isArray(list) ? list : [])
6
+ .map((x) => String(x || "").trim().toLowerCase())
7
+ .filter(Boolean)
8
+ .map((x) => {
9
+ // 允许传入带协议的,如 https://docs.qq.com
10
+ try {
11
+ if (/^https?:\/\//i.test(x)) return new URL(x).hostname.toLowerCase();
12
+ } catch {
13
+ console.warn(`iframeFilter: 无法解析 whitelist 域名 "${x}",已忽略该项`);
14
+ }
15
+ return x;
16
+ });
17
+ }
18
+
19
+ /**
20
+ * 规则:
21
+ * - "docs.qq.com" 精确匹配
22
+ * - "*.qq.com" 只匹配子域(a.qq.com),不匹配根域(qq.com)
23
+ */
24
+ function matchHost(hostname, whitelist) {
25
+ const host = (hostname || "").toLowerCase();
26
+ if (!host) return false;
21
27
 
22
- // 白名单过滤
23
- let src_parsed = url.parse(src)
24
- if(white_list.includes(src_parsed.hostname)){
25
- src = item[2]
26
- }else{
27
- src = ''
28
+ for (const rule of whitelist) {
29
+ if (!rule) continue;
30
+
31
+ if (rule.startsWith("*.")) {
32
+ const root = rule.slice(2);
33
+ if (root && host.endsWith("." + root)) return true;
34
+ continue;
28
35
  }
29
36
 
30
- let suffix = item[3]
31
- let output = `<iframe ${prefix} src="${item[2]}" ${suffix}></iframe>`
32
- str = str.replace(origin,output)
33
- // console.log(origin,output)
37
+ if (host === rule) return true;
34
38
  }
35
- // console.log(str)
36
- return str
39
+ return false;
40
+ }
41
+
42
+ function normalizeSrcForParse(src = "") {
43
+ const s = src.trim();
44
+ if (!s) return "";
45
+ // 协议相对://xxx.com/...
46
+ if (s.startsWith("//")) return "https:" + s;
47
+ return s;
48
+ }
49
+
50
+ function getHostnameFromSrc(src = "") {
51
+ const s = normalizeSrcForParse(src);
52
+ if (!s) return "";
53
+
54
+ // 只对白名单校验 http/https 或 // 这种
55
+ if (/^https?:\/\//i.test(s)) {
56
+ try {
57
+ return new URL(s).hostname.toLowerCase();
58
+ } catch {
59
+ return "";
60
+ }
61
+ }
62
+ return "";
63
+ }
64
+
65
+ /**
66
+ * @param {string} html - 含 iframe 的 HTML 字符串(片段也行)
67
+ * @param {string[]} whitelist - 域名白名单,支持 "*.qq.com"
68
+ * @param {object} [options]
69
+ * @param {boolean} [options.keepNonHttpSrc=false] - src 不是 http/https(如 about:blank、data:)是否保留
70
+ */
71
+ export default function iframeFilter(html, whitelist, options = {}) {
72
+ if (!html) return html;
73
+
74
+ const { keepNonHttpSrc = false } = options;
75
+
76
+ // 没传 whitelist 或传空数组,都视为“不启用白名单过滤”
77
+ const wl = normalizeWhitelist(whitelist || []);
78
+ const enableWhitelist = wl.length > 0;
79
+
80
+ const $ = load(`<root>${html}</root>`, { decodeEntities: false });
81
+
82
+ $("iframe").each((_, el) => {
83
+ const iframe = $(el);
84
+ const srcRaw = iframe.attr("src") ?? "";
85
+ const src = String(srcRaw).trim();
86
+ if (!src) return;
87
+
88
+ // 不启用过滤:直接放行(可选:仍然处理非 http src)
89
+ if (!enableWhitelist) {
90
+ if (!keepNonHttpSrc) {
91
+ const isHttpLike = src.startsWith("//") || /^https?:\/\//i.test(src);
92
+ if (!isHttpLike) iframe.attr("src", "");
93
+ }
94
+ return;
95
+ }
96
+
97
+ // ======= 启用白名单过滤的逻辑(和之前一样) =======
98
+ const isHttpLike = src.startsWith("//") || /^https?:\/\//i.test(src);
99
+ if (!isHttpLike) {
100
+ if (!keepNonHttpSrc) iframe.attr("src", "");
101
+ return;
102
+ }
103
+
104
+ const host = getHostnameFromSrc(src);
105
+ const ok = matchHost(host, wl);
106
+
107
+ if (!ok) {
108
+ iframe.attr("src", "");
109
+ } else {
110
+ // 安全处理:将协议相对 URL 转换为 https://
111
+ let finalSrc = src;
112
+ if (src.startsWith("//")) {
113
+ finalSrc = "https:" + src;
114
+ }
115
+ iframe.attr("src", finalSrc);
116
+ }
117
+ });
118
+
119
+ return $("root").html() || "";
37
120
  }
38
- export default iframeFilter
package/assets/js/img.js CHANGED
@@ -1,22 +1,67 @@
1
- // 图片地址&懒加载
2
- import { resolveImagePath } from "@jx3box/jx3box-common/js/utils";
3
- import JX3BOX from "@jx3box/jx3box-common/data/jx3box.json";
4
- function lazyLoad(str) {
5
- if (!str) return;
6
-
7
- const REG = /<img(.*?)src=[\'"]([^\'"]+)[\'"](.*?)>/gi;
8
- let item;
9
- while ((item = REG.exec(str))) {
10
- let origin = item[0];
11
- let prefix = item[1];
12
- let src = resolveImagePath(item[2]);
13
- if (src.includes('https://console.cnyixun.com/')) {
14
- src = src.replace('https://console.cnyixun.com/', JX3BOX.__cdn)
1
+ import { load } from "cheerio";
2
+
3
+ function joinDomain(domain, path) {
4
+ const d = String(domain || "").replace(/\/+$/, "");
5
+ const p = String(path || "").replace(/^\/+/, "/");
6
+ return d ? d + p : path;
7
+ }
8
+
9
+ function isAbsoluteLike(src) {
10
+ // 不需要拼 domain 的情况
11
+ return (
12
+ /^(https?:)?\/\//i.test(src) ||
13
+ /^(data:|blob:|mailto:|tel:|javascript:)/i.test(src)
14
+ );
15
+ }
16
+
17
+ function normalizeImgSrc(src, domain) {
18
+ const s = String(src || "").trim();
19
+ if (!s || !domain) return s;
20
+ if (isAbsoluteLike(s)) return s;
21
+
22
+ // 最常见:/uploads/xxx
23
+ if (s.startsWith("/")) return joinDomain(domain, s);
24
+
25
+ // 其它相对路径:uploads/a.png、./a.png
26
+ // 用 URL 做更准确的拼接(domain 需要是带协议的,如 https://cdn.xxx.com)
27
+ try {
28
+ if (/^https?:\/\//i.test(domain)) {
29
+ const base = domain.endsWith("/") ? domain : domain + "/";
30
+ return new URL(s, base).toString();
15
31
  }
16
- let suffix = item[3];
17
- let output = `<img ${prefix} loading="lazy" src="${src}" ${suffix}>`;
18
- str = str.replace(origin, output);
32
+ } catch {
33
+ console.warn("URL 拼接图片路径失败:", domain, s);
19
34
  }
20
- return str;
35
+
36
+ // 兜底:简单拼到根路径
37
+ return joinDomain(domain, "/" + s.replace(/^\.\/+/, ""));
38
+ }
39
+
40
+ /**
41
+ * @param {string} html 富文本HTML
42
+ * @param {string} domain 可选:CDN/站点域名(如 https://cdn.xxx.com)
43
+ */
44
+ export default function lazyLoad(html, domain = "") {
45
+ if (!html) return html;
46
+
47
+ const $ = load(`<root>${html}</root>`, { decodeEntities: false });
48
+
49
+ $("img").each((_, el) => {
50
+ const img = $(el);
51
+
52
+ // 固定懒加载:无论如何都加/覆盖
53
+ img.attr("loading", "lazy");
54
+
55
+ // 只有传了 domain 才处理 src 拼接
56
+ if (domain) {
57
+ const src = img.attr("src");
58
+ if (src) img.attr("src", normalizeImgSrc(src, domain));
59
+
60
+ // 如果你有用 data-src 之类,也可以一起处理(可删)
61
+ const dataSrc = img.attr("data-src");
62
+ if (dataSrc) img.attr("data-src", normalizeImgSrc(dataSrc, domain));
63
+ }
64
+ });
65
+
66
+ return $("root").html() || "";
21
67
  }
22
- export default lazyLoad;
@@ -0,0 +1,72 @@
1
+ import sanitizeHtml from "sanitize-html";
2
+
3
+ const FORBID = new Set(["script", "object", "embed", "applet", "base", "meta", "link"]);
4
+
5
+ const EXTRA_TAGS = [
6
+ "img",
7
+ "h1", "h2", "h3", "h4", "h5", "h6",
8
+ "table", "thead", "tbody", "tr", "th", "td",
9
+ "blockquote", "pre", "code", "hr",
10
+ "video", "source",
11
+ "iframe",
12
+ ];
13
+
14
+ export default function sanitizeRichText(html) {
15
+ if (!html) return html;
16
+
17
+ const allowedTags = sanitizeHtml.defaults.allowedTags
18
+ .concat(EXTRA_TAGS)
19
+ .filter((t, i, arr) => arr.indexOf(t) === i) // 去重
20
+ .filter(t => !FORBID.has(t));
21
+
22
+ return sanitizeHtml(html, {
23
+ disallowedTagsMode: "discard",
24
+ allowedTags,
25
+
26
+ allowedAttributes: {
27
+ "*": ["class", "style", "title", "id", "data-*"],
28
+ a: ["href", "target", "rel", "title", "class", "style"],
29
+ img: ["src", "alt", "title", "width", "height", "class", "style", "loading", "decoding"],
30
+ video: ["controls", "width", "height", "class", "style"],
31
+ source: ["src", "type"],
32
+ iframe: ["src","width","height","frameborder","scrolling","allowfullscreen","sandbox","referrerpolicy","class","style"],
33
+ },
34
+
35
+ allowedSchemes: ["http", "https", "mailto", "tel"],
36
+ allowProtocolRelative: true,
37
+ allowedSchemesByTag: { img: ["http", "https", "data"],iframe: ["http", "https"], },
38
+
39
+ transformTags: {
40
+ "*": (tagName, attribs) => {
41
+ const out = { ...attribs };
42
+
43
+ // 移除 on*
44
+ for (const k of Object.keys(out)) if (/^on/i.test(k)) delete out[k];
45
+
46
+ // style 禁 @import / url(
47
+ if (typeof out.style === "string" && out.style) {
48
+ let s = out.style;
49
+ s = s.replace(/@import\s+[^;]+;?/gi, "");
50
+ s = s.replace(/url\s*\(\s*[^)]+\s*\)/gi, "");
51
+ s = s.replace(/;;+/g, ";").trim();
52
+ if (!s) delete out.style;
53
+ else out.style = s;
54
+ }
55
+
56
+ // 兜底禁 javascript:
57
+ for (const key of ["href", "src"]) {
58
+ if (out[key] && /^\s*javascript:/i.test(out[key])) out[key] = "";
59
+ }
60
+
61
+ // 仅允许 data:image/*
62
+ if (tagName === "img" && typeof out.src === "string" && out.src.startsWith("data:")) {
63
+ if (!/^data:image\/(png|jpe?g|gif|webp|avif|bmp|svg\+xml);/i.test(out.src)) {
64
+ out.src = "";
65
+ }
66
+ }
67
+
68
+ return { tagName, attribs: out };
69
+ },
70
+ },
71
+ });
72
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jx3box/jx3box-editor",
3
- "version": "2.2.40",
3
+ "version": "2.2.43",
4
4
  "description": "JX3BOX Article & Editor",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -36,10 +36,11 @@
36
36
  "@jx3box/jx3box-data": "^3.8.9",
37
37
  "@jx3box/jx3box-emotion": "^1.2.14",
38
38
  "@jx3box/jx3box-macro": "^1.0.1",
39
- "@jx3box/jx3box-talent": "^1.3.9",
39
+ "@jx3box/jx3box-talent": "^1.3.11",
40
40
  "@jx3box/markdown": "^0.1.5",
41
41
  "@tinymce/tinymce-vue": "^3.2.2",
42
42
  "axios": "^0.19.2",
43
+ "cheerio": "^1.1.2",
43
44
  "core-js": "^3.6.5",
44
45
  "csslab": "^4.0.3",
45
46
  "dayjs": "^1.10.7",
@@ -52,6 +53,7 @@
52
53
  "lodash": "^4.17.15",
53
54
  "photoswipe": "^4.1.2",
54
55
  "prismjs": "^1.20.0",
56
+ "sanitize-html": "^2.17.0",
55
57
  "sortablejs": "^1.15.0",
56
58
  "vue": "^2.6.11",
57
59
  "vue-contextmenujs": "^1.4.9",
@@ -7,7 +7,10 @@
7
7
  <meta name="renderer" content="webkit" />
8
8
  <title><%= htmlWebpackPlugin.options.title %></title>
9
9
  <link rel="icon" href="<%= BASE_URL %>favicon.ico" />
10
- <script src="https://cdn.jx3box.com/static/tinymce/tinymce.min.js?v=1.8.9"></script>
10
+ <script>
11
+ window.RX_TINYMCE_ROOT = "https://cdn.jx3box.com/static/tinymce";
12
+ </script>
13
+ <script src="https://cdn.jx3box.com/static/tinymce/tinymce.min.js?v=1.9.0"></script>
11
14
  <!-- TODO: -->
12
15
  <!-- <script src="http://localhost:3000/tinymce.min.js"></script> -->
13
16
  </head>
package/src/Article.vue CHANGED
@@ -43,11 +43,11 @@ import hevueImgPreview from "hevue-img-preview";
43
43
  Vue.use(hevueImgPreview);
44
44
 
45
45
  // XSS
46
- import execFilterXSS from "../assets/js/script";
46
+ import execFilterXSS from "../assets/js/xss";
47
47
  // const execFilterXSS = require("xss");
48
- const xss_options = {
49
- allowCommentTag: true,
50
- };
48
+ // const xss_options = {
49
+ // allowCommentTag: true,
50
+ // };
51
51
 
52
52
  // 基本文本
53
53
  import execLazyload from "../assets/js/img";
@@ -61,7 +61,7 @@ import renderDirectory from "../assets/js/directory";
61
61
  import renderMacro from "../assets/js/macro";
62
62
  import renderTalent from "../assets/js/qixue";
63
63
  import renderTalent2 from "../assets/js/talent2";
64
- import {renderKatexAll} from "../assets/js/katex";
64
+ import { renderKatexAll } from "../assets/js/katex";
65
65
  import renderCode from "../assets/js/code";
66
66
  import renderImgPreview from "../assets/js/renderImgPreview";
67
67
  import renderPzIframe from "../assets/js/pz_iframe";
@@ -80,6 +80,30 @@ export default {
80
80
  name: "Article",
81
81
  props: {
82
82
  content: String,
83
+ // 拼接相对路径地址的图片,需要自带协议
84
+ cdnDomain: {
85
+ type: String,
86
+ default: "https://cdn.jx3box.com",
87
+ },
88
+ // 链接白名单检查,不在白名单,使用新窗跳转
89
+ linkWhitelist: {
90
+ type: Array,
91
+ default: function () {
92
+ return [];
93
+ },
94
+ },
95
+ // 链接白名单强制模式,开启后不在白名单的链接一律置空,不允许跳转
96
+ linkStrict: {
97
+ type: Boolean,
98
+ default: false,
99
+ },
100
+ // iframe白名单检查,不在白名单,移除iframe
101
+ iframeWhitelist: {
102
+ type: Array,
103
+ default: function () {
104
+ return [];
105
+ },
106
+ },
83
107
  directorybox: String,
84
108
  pageable: {
85
109
  type: Boolean,
@@ -154,11 +178,18 @@ export default {
154
178
  methods: {
155
179
  doReg: function (data) {
156
180
  if (data) {
157
- // 过滤内容
158
- data = execLazyload(data);
159
- data = execFilterIframe(data);
160
- data = execFilterXSS(data, xss_options);
161
- data = execFilterLink(data);
181
+ // 1. 先执行 XSS 过滤(xss.js 已包含所有配置)
182
+ data = execFilterXSS(data);
183
+
184
+ // 2. 然后执行 iframe 白名单过滤
185
+ data = execFilterIframe(data, ["player.bilibili.com", "docs.qq.com", "open.douyu.com", ...this.iframeWhitelist]);
186
+
187
+ // 3. 处理图片懒加载
188
+ data = execLazyload(data, this.cdnDomain);
189
+
190
+ // 4. 最后处理链接
191
+ data = execFilterLink(data, this.linkWhitelist, this.linkStrict);
192
+
162
193
  return data;
163
194
  } else {
164
195
  return "";
@@ -199,10 +230,10 @@ export default {
199
230
  let dir = renderDirectory(target, this.directorybox);
200
231
  this.$emit("directoryRendered", dir);
201
232
 
202
- if(window.location.hash?.includes('directory')){
203
- let id = window.location.hash
233
+ if (window.location.hash?.includes("directory")) {
234
+ let id = window.location.hash;
204
235
  let target = $(`${id}`).offset().top;
205
- console.log(target)
236
+ console.log(target);
206
237
  $(document).scrollTop(target - HEADER_HEIGHT);
207
238
  }
208
239
 
package/vue.config.js CHANGED
@@ -84,6 +84,19 @@ module.exports = {
84
84
  }
85
85
  },
86
86
 
87
+ //❤️ 配置需要被 Babel 转译的 node_modules 依赖 ~
88
+ transpileDependencies: [
89
+ 'htmlparser2',
90
+ 'cheerio',
91
+ 'dom-serializer',
92
+ 'domelementtype',
93
+ 'domhandler',
94
+ 'domutils',
95
+ 'entities',
96
+ 'parse5',
97
+ 'parse5-htmlparser2-tree-adapter'
98
+ ],
99
+
87
100
  chainWebpack: config => {
88
101
 
89
102
  //💘 html-webpack-plugin ~