@jx3box/jx3box-editor 2.2.48 → 3.0.1
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/config/global.js +79 -0
- package/config/global.less +16 -0
- package/index.js +21 -8
- package/package.json +64 -63
- package/readme.md +25 -99
- package/src/Article.vue +96 -57
- package/src/ArticleMarkdown.vue +54 -47
- package/src/BoxResource.vue +67 -42
- package/src/Buff.vue +18 -7
- package/src/GameText.vue +32 -45
- package/src/Item.vue +143 -235
- package/src/ItemSimple.vue +27 -37
- package/src/Markdown.vue +362 -210
- package/src/Npc.vue +51 -30
- package/src/Resource.vue +296 -252
- package/src/Skill.vue +36 -26
- package/src/Tinymce.vue +124 -137
- package/src/Upload.vue +238 -155
- package/src/UploadAlbum.vue +189 -118
- package/{assets → src/assets}/css/article.less +1 -0
- package/src/assets/css/markdown.less +4 -0
- package/{assets → src/assets}/css/module/author.less +4 -3
- package/{assets → src/assets}/css/module/directory.less +23 -32
- package/{assets → src/assets}/css/module/talent.less +2 -2
- package/{assets → src/assets}/css/resource.less +56 -22
- package/src/assets/css/tinymce/_.less +28 -0
- package/src/assets/css/tinymce/a.less +21 -0
- package/{assets → src/assets}/css/tinymce/code.less +1 -1
- package/{assets/css → src/assets/css/tinymce}/combo.less +123 -18
- package/{assets → src/assets}/css/tinymce/fold.less +3 -6
- package/src/assets/css/tinymce/h.less +90 -0
- package/{assets → src/assets}/css/tinymce/latex.less +14 -14
- package/{assets → src/assets}/css/tinymce/macro.less +3 -3
- package/{assets → src/assets}/css/tinymce/pz.less +2 -2
- package/{assets → src/assets}/css/tinymce/table.less +5 -10
- package/{assets → src/assets}/css/tinymce.less +8 -4
- package/src/assets/css/upload.less +195 -0
- package/src/assets/css/upload_album.less +164 -0
- package/src/assets/css/var.less +2 -0
- package/src/assets/img/other/qr-code.svg +1 -0
- package/{assets → src/assets}/js/audio.js +2 -2
- package/{assets → src/assets}/js/directory.js +51 -23
- package/src/assets/js/hljs_languages.js +177 -0
- package/src/assets/js/katex.js +211 -0
- package/src/assets/js/renderImgPreview.js +49 -0
- package/{assets → src/assets}/js/xss.js +48 -5
- package/src/components/Author.vue +32 -13
- package/src/components/Avatar.vue +22 -3
- package/src/components/Combo.vue +118 -72
- package/src/components/PostAuthor.vue +13 -11
- package/src/components/QRcode.vue +136 -0
- package/src/components/SkillMartial.vue +13 -12
- package/src/pages/article.js +14 -0
- package/src/pages/index.js +5 -0
- package/src/pages/markdown.js +14 -0
- package/src/pages/tinymce.js +49 -0
- package/src/pages/upload.js +14 -0
- package/{service → src/service}/author.js +2 -2
- package/{service → src/service}/cms.js +5 -3
- package/{service → src/service}/database.js +4 -2
- package/{service → src/service}/item.js +1 -1
- package/{service → src/service}/node.js +1 -1
- package/{service → src/service}/resource.js +1 -1
- package/src/views/article.vue +72 -0
- package/src/views/index.vue +11 -0
- package/src/views/markdown.vue +58 -0
- package/src/views/tinymce.vue +58 -0
- package/src/views/upload.vue +111 -0
- package/.env +0 -2
- package/.vscode/settings.json +0 -5
- package/assets/css/katex-fix.css +0 -20
- package/assets/css/tinymce/_.less +0 -30
- package/assets/css/tinymce/a.less +0 -30
- package/assets/css/tinymce/combo.less +0 -111
- package/assets/css/tinymce/h.less +0 -58
- package/assets/css/upload.less +0 -105
- package/assets/js/katex.js +0 -191
- package/assets/js/renderImgPreview.js +0 -25
- package/babel.config.js +0 -3
- package/docs/markdown.md +0 -16
- package/jsconfig.json +0 -9
- package/public/article.html +0 -15
- package/public/css/article.css +0 -2481
- package/public/css/article.less +0 -3
- package/public/favicon.ico +0 -0
- package/public/tinymce.html +0 -33
- package/src/Equip.vue +0 -301
- package/src/components/LetterDemo.vue +0 -93
- package/src/components/medal.vue +0 -43
- package/test-audio.html +0 -121
- package/vue.config.js +0 -147
- /package/{assets → src/assets}/css/markdown/_.less +0 -0
- /package/{assets → src/assets}/css/markdown/macro.less +0 -0
- /package/{assets/css/article_markdown.less → src/assets/css/markdown/markdown-article.less} +0 -0
- /package/{assets/css/markdown.less → src/assets/css/markdown/markdown-editor.less} +0 -0
- /package/{assets → src/assets}/css/markdown/talent.less +0 -0
- /package/{assets → src/assets}/css/markdown/video.less +0 -0
- /package/{assets → src/assets}/css/module/buff.less +0 -0
- /package/{assets → src/assets}/css/module/icon.less +0 -0
- /package/{assets → src/assets}/css/module/item.less +0 -0
- /package/{assets → src/assets}/css/module/item_simple.less +0 -0
- /package/{assets → src/assets}/css/module/jx3_element.less +0 -0
- /package/{assets → src/assets}/css/module/macro.less +0 -0
- /package/{assets → src/assets}/css/module/npc.less +0 -0
- /package/{assets → src/assets}/css/module/resource.less +0 -0
- /package/{assets → src/assets}/css/module/skill.less +0 -0
- /package/{assets → src/assets}/css/tinymce/hr.less +0 -0
- /package/{assets → src/assets}/css/tinymce/img.less +0 -0
- /package/{assets → src/assets}/css/tinymce/imgpreview.less +0 -0
- /package/{assets → src/assets}/css/tinymce/list.less +0 -0
- /package/{assets → src/assets}/css/tinymce/nextpage.less +0 -0
- /package/{assets → src/assets}/css/tinymce/p.less +0 -0
- /package/{assets → src/assets}/css/tinymce/plugin.less +0 -0
- /package/{assets → src/assets}/css/tinymce/qixue.less +0 -0
- /package/{assets → src/assets}/css/tinymce/quote.less +0 -0
- /package/{assets → src/assets}/css/tinymce/video.less +0 -0
- /package/{assets → src/assets}/css/tinymce/voice.less +0 -0
- /package/{assets → src/assets}/data/detach_type.json +0 -0
- /package/{assets → src/assets}/data/game_font.json +0 -0
- /package/{assets → src/assets}/data/markdown_whitelist.json +0 -0
- /package/{assets → src/assets}/data/weapon_type.json +0 -0
- /package/{assets → src/assets}/img/buff.svg +0 -0
- /package/{assets → src/assets}/img/equip_bg.png +0 -0
- /package/{assets → src/assets}/img/file.svg +0 -0
- /package/{assets → src/assets}/img/icons.svg +0 -0
- /package/{assets → src/assets}/img/item/pve.png +0 -0
- /package/{assets → src/assets}/img/item/pvp.png +0 -0
- /package/{assets → src/assets}/img/item/pvx.png +0 -0
- /package/{assets → src/assets}/img/item/std.png +0 -0
- /package/{assets → src/assets}/img/item/wujie.png +0 -0
- /package/{assets → src/assets}/img/item.svg +0 -0
- /package/{assets → src/assets}/img/jx3.svg +0 -0
- /package/{assets → src/assets}/img/jx3box.svg +0 -0
- /package/{assets → src/assets}/img/npc/attack.svg +0 -0
- /package/{assets → src/assets}/img/npc/buff.svg +0 -0
- /package/{assets → src/assets}/img/npc/energy.svg +0 -0
- /package/{assets → src/assets}/img/npc/miss.svg +0 -0
- /package/{assets → src/assets}/img/npc/npc.svg +0 -0
- /package/{assets → src/assets}/img/npc/radar.svg +0 -0
- /package/{assets → src/assets}/img/npc/shield.svg +0 -0
- /package/{assets → src/assets}/img/npc/sight.svg +0 -0
- /package/{assets → src/assets}/img/npc/skull.svg +0 -0
- /package/{assets → src/assets}/img/npc/target.svg +0 -0
- /package/{assets → src/assets}/img/skill.svg +0 -0
- /package/{assets → src/assets}/img/skillset.png +0 -0
- /package/{assets → src/assets}/js/a.js +0 -0
- /package/{assets → src/assets}/js/code.js +0 -0
- /package/{assets → src/assets}/js/combo.js +0 -0
- /package/{assets → src/assets}/js/drag.js +0 -0
- /package/{assets → src/assets}/js/filter2.js +0 -0
- /package/{assets → src/assets}/js/fold.js +0 -0
- /package/{assets → src/assets}/js/gallery.js +0 -0
- /package/{assets → src/assets}/js/iframe.js +0 -0
- /package/{assets → src/assets}/js/img.js +0 -0
- /package/{assets → src/assets}/js/item/attribute_percent.js +0 -0
- /package/{assets → src/assets}/js/item/bind.js +0 -0
- /package/{assets → src/assets}/js/item/border.js +0 -0
- /package/{assets → src/assets}/js/item/border_quest.js +0 -0
- /package/{assets → src/assets}/js/item/color.js +0 -0
- /package/{assets → src/assets}/js/item/hljs_languages.js +0 -0
- /package/{assets → src/assets}/js/item/icon_url.js +0 -0
- /package/{assets → src/assets}/js/item/second_format.js +0 -0
- /package/{assets → src/assets}/js/jx3_element.js +0 -0
- /package/{assets → src/assets}/js/macro.js +0 -0
- /package/{assets → src/assets}/js/nextpage.js +0 -0
- /package/{assets → src/assets}/js/pswp.js +0 -0
- /package/{assets → src/assets}/js/pswp_template.js +0 -0
- /package/{assets → src/assets}/js/pz_iframe.js +0 -0
- /package/{assets → src/assets}/js/qixue.js +0 -0
- /package/{assets → src/assets}/js/script.js +0 -0
- /package/{assets → src/assets}/js/talent2.js +0 -0
- /package/{assets → src/assets}/js/tex-mml-chtml.js +0 -0
- /package/{service → src/service}/enum/CollectionPublic.js +0 -0
- /package/{service → src/service}/enum/EquipPosition.js +0 -0
- /package/{service → src/service}/enum/EquipType.js +0 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import $ from 'jquery';
|
|
2
|
+
import katex from 'katex';
|
|
3
|
+
import 'katex/dist/katex.min.css';
|
|
4
|
+
|
|
5
|
+
function renderKatexBlock(selector = ".w-latex") {
|
|
6
|
+
try {
|
|
7
|
+
$(selector).each(function() {
|
|
8
|
+
const $katex = $(this);
|
|
9
|
+
|
|
10
|
+
// 避免重复渲染
|
|
11
|
+
if ($katex.data('katex-rendered')) return;
|
|
12
|
+
|
|
13
|
+
let raw = $katex.html();
|
|
14
|
+
|
|
15
|
+
// 统一处理换行符
|
|
16
|
+
raw = raw
|
|
17
|
+
.replace(/\\\\\s*<br\s*\/?>/gi, '\\\\')
|
|
18
|
+
.replace(/\\\s*<br\s*\/?>/gi, '\\\\')
|
|
19
|
+
.replace(/<br\s*\/?>/gi, '\\\\')
|
|
20
|
+
.replace(/<[^>]+>/g, '');
|
|
21
|
+
|
|
22
|
+
// 解码HTML实体
|
|
23
|
+
raw = $('<div>').html(raw).text().trim();
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
katex.render(raw, $katex.get(0), {
|
|
27
|
+
displayMode: true,
|
|
28
|
+
throwOnError: false,
|
|
29
|
+
strict: false
|
|
30
|
+
});
|
|
31
|
+
$katex.data('katex-rendered', true);
|
|
32
|
+
} catch (e) {
|
|
33
|
+
console.error('KaTeX render error:', e.message, raw);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
} catch (e) {
|
|
37
|
+
console.error('KaTeX block render error:', e);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function renderKatexInline(container = document.body) {
|
|
42
|
+
// 改进的正则:不匹配换行符,支持转义
|
|
43
|
+
const inlineRegex = /(?<!\\)(\\\((.+?)\\\)|(?<!\\)\$([^\n$]+?)(?<!\\)\$)/g;
|
|
44
|
+
|
|
45
|
+
const walker = document.createTreeWalker(
|
|
46
|
+
container,
|
|
47
|
+
NodeFilter.SHOW_TEXT,
|
|
48
|
+
{
|
|
49
|
+
acceptNode: function (node) {
|
|
50
|
+
// 跳过已渲染的节点
|
|
51
|
+
if (
|
|
52
|
+
node.parentNode &&
|
|
53
|
+
(node.parentNode.classList?.contains('katex') ||
|
|
54
|
+
node.parentNode.closest("pre, code, .katex"))
|
|
55
|
+
) {
|
|
56
|
+
return NodeFilter.FILTER_REJECT;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const value = node.nodeValue || '';
|
|
60
|
+
if (value.includes("\\(") || value.includes("$")) {
|
|
61
|
+
return NodeFilter.FILTER_ACCEPT;
|
|
62
|
+
}
|
|
63
|
+
return NodeFilter.FILTER_REJECT;
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const nodesToReplace = [];
|
|
69
|
+
while (walker.nextNode()) {
|
|
70
|
+
nodesToReplace.push(walker.currentNode);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
nodesToReplace.forEach((node) => {
|
|
74
|
+
const text = node.nodeValue;
|
|
75
|
+
const frag = document.createDocumentFragment();
|
|
76
|
+
let lastIndex = 0;
|
|
77
|
+
|
|
78
|
+
// 重置正则状态
|
|
79
|
+
inlineRegex.lastIndex = 0;
|
|
80
|
+
|
|
81
|
+
const matches = [...text.matchAll(inlineRegex)];
|
|
82
|
+
|
|
83
|
+
matches.forEach((match) => {
|
|
84
|
+
const fullMatch = match[0];
|
|
85
|
+
const parenContent = match[2];
|
|
86
|
+
const dollarContent = match[3];
|
|
87
|
+
const raw = parenContent || dollarContent;
|
|
88
|
+
const matchStart = match.index;
|
|
89
|
+
|
|
90
|
+
// 添加匹配前的文本
|
|
91
|
+
if (matchStart > lastIndex) {
|
|
92
|
+
frag.appendChild(document.createTextNode(text.slice(lastIndex, matchStart)));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const span = document.createElement("span");
|
|
97
|
+
span.className = "katex-inline";
|
|
98
|
+
span.innerHTML = katex.renderToString(raw, {
|
|
99
|
+
displayMode: false,
|
|
100
|
+
throwOnError: false,
|
|
101
|
+
strict: false,
|
|
102
|
+
trust: true
|
|
103
|
+
});
|
|
104
|
+
frag.appendChild(span);
|
|
105
|
+
} catch (e) {
|
|
106
|
+
frag.appendChild(document.createTextNode(fullMatch));
|
|
107
|
+
console.error("Inline render error:", raw, e.message);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
lastIndex = matchStart + fullMatch.length;
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// 添加剩余文本
|
|
114
|
+
if (lastIndex < text.length) {
|
|
115
|
+
frag.appendChild(document.createTextNode(text.slice(lastIndex)));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (frag.hasChildNodes()) {
|
|
119
|
+
node.parentNode.replaceChild(frag, node);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function renderKatexDisplayBlock(container = document.body) {
|
|
125
|
+
// 使用非贪婪匹配,允许多行但不跨过多段落
|
|
126
|
+
const blockRegex = /(?<!\\)(\$\$([\s\S]+?)\$\$|(?<!\\)\\\[([\s\S]+?)\\\])/g;
|
|
127
|
+
|
|
128
|
+
const walker = document.createTreeWalker(
|
|
129
|
+
container,
|
|
130
|
+
NodeFilter.SHOW_TEXT,
|
|
131
|
+
{
|
|
132
|
+
acceptNode: function (node) {
|
|
133
|
+
// 跳过已渲染的节点
|
|
134
|
+
if (
|
|
135
|
+
node.parentNode &&
|
|
136
|
+
(node.parentNode.classList?.contains('katex') ||
|
|
137
|
+
node.parentNode.closest("pre, code, .katex"))
|
|
138
|
+
) {
|
|
139
|
+
return NodeFilter.FILTER_REJECT;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const value = node.nodeValue || '';
|
|
143
|
+
if (value.includes("$$") || value.includes("\\[")) {
|
|
144
|
+
return NodeFilter.FILTER_ACCEPT;
|
|
145
|
+
}
|
|
146
|
+
return NodeFilter.FILTER_REJECT;
|
|
147
|
+
},
|
|
148
|
+
}
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const nodesToReplace = [];
|
|
152
|
+
while (walker.nextNode()) {
|
|
153
|
+
nodesToReplace.push(walker.currentNode);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
nodesToReplace.forEach((node) => {
|
|
157
|
+
const text = node.nodeValue;
|
|
158
|
+
const frag = document.createDocumentFragment();
|
|
159
|
+
let lastIndex = 0;
|
|
160
|
+
|
|
161
|
+
// 重置正则状态
|
|
162
|
+
blockRegex.lastIndex = 0;
|
|
163
|
+
|
|
164
|
+
const matches = [...text.matchAll(blockRegex)];
|
|
165
|
+
|
|
166
|
+
matches.forEach((match) => {
|
|
167
|
+
const fullMatch = match[0];
|
|
168
|
+
const dollarContent = match[2];
|
|
169
|
+
const bracketContent = match[3];
|
|
170
|
+
const raw = (dollarContent || bracketContent).trim();
|
|
171
|
+
const matchStart = match.index;
|
|
172
|
+
|
|
173
|
+
// 添加匹配前的文本
|
|
174
|
+
if (matchStart > lastIndex) {
|
|
175
|
+
frag.appendChild(document.createTextNode(text.slice(lastIndex, matchStart)));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
const div = document.createElement("div");
|
|
180
|
+
div.className = "katex-block";
|
|
181
|
+
div.innerHTML = katex.renderToString(raw, {
|
|
182
|
+
displayMode: true,
|
|
183
|
+
throwOnError: false,
|
|
184
|
+
strict: false,
|
|
185
|
+
trust: true
|
|
186
|
+
});
|
|
187
|
+
frag.appendChild(div);
|
|
188
|
+
} catch (e) {
|
|
189
|
+
frag.appendChild(document.createTextNode(fullMatch));
|
|
190
|
+
console.error("Block render error:", raw, e.message);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
lastIndex = matchStart + fullMatch.length;
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// 添加剩余文本
|
|
197
|
+
if (lastIndex < text.length) {
|
|
198
|
+
frag.appendChild(document.createTextNode(text.slice(lastIndex)));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (frag.hasChildNodes()) {
|
|
202
|
+
node.parentNode.replaceChild(frag, node);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export default function renderKatexAll(container = document.body) {
|
|
208
|
+
renderKatexBlock(".w-latex");
|
|
209
|
+
renderKatexDisplayBlock(container);
|
|
210
|
+
renderKatexInline(container);
|
|
211
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import $ from "jquery";
|
|
2
|
+
import "viewerjs/dist/viewer.css";
|
|
3
|
+
import Viewer from "viewerjs";
|
|
4
|
+
|
|
5
|
+
const viewerMap = new WeakMap();
|
|
6
|
+
|
|
7
|
+
function initViewer(ele) {
|
|
8
|
+
if (!ele) return null;
|
|
9
|
+
if (viewerMap.has(ele)) return viewerMap.get(ele);
|
|
10
|
+
const viewer = new Viewer(ele, {
|
|
11
|
+
toolbar: false,
|
|
12
|
+
navbar: false,
|
|
13
|
+
});
|
|
14
|
+
viewerMap.set(ele, viewer);
|
|
15
|
+
return viewer;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function getImgViewer(ele) {
|
|
19
|
+
return viewerMap.get(ele) || null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function showImgPreview(ele) {
|
|
23
|
+
const viewer = initViewer(ele);
|
|
24
|
+
if (viewer) viewer.show();
|
|
25
|
+
return viewer;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default function renderImgPreview(rootEl, selector = "img") {
|
|
29
|
+
if (!rootEl) return;
|
|
30
|
+
|
|
31
|
+
const $root = $(rootEl);
|
|
32
|
+
const imgs = $root
|
|
33
|
+
.find(selector)
|
|
34
|
+
.filter(function () {
|
|
35
|
+
const src = $(this).attr("src");
|
|
36
|
+
if (!src) return false;
|
|
37
|
+
// 保留本项目的业务规则:表情图片不启用预览
|
|
38
|
+
if (this.classList && this.classList.contains("e-jx3-emotion-img"))
|
|
39
|
+
return false;
|
|
40
|
+
// 业务规则:图标不启用预览
|
|
41
|
+
if (this.classList && this.classList.contains("e-jx3-icon"))
|
|
42
|
+
return false;
|
|
43
|
+
return true;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
imgs.each((_, ele) => {
|
|
47
|
+
initViewer(ele);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
@@ -17,18 +17,60 @@ const EXTRA_TAGS = [
|
|
|
17
17
|
|
|
18
18
|
// 必须顶层的 at-rule(你说不需要动画,但 keyframes 也可能被编辑器/作者写进来,留着更稳)
|
|
19
19
|
const TOP_LEVEL_AT = new Set(["keyframes", "-webkit-keyframes", "font-face"]);
|
|
20
|
+
const CSS_URL_ALLOWED_HOSTS = new Set(["cdn.jx3box.com"]);
|
|
21
|
+
|
|
22
|
+
function unquoteCssUrl(raw = "") {
|
|
23
|
+
const s = String(raw).trim();
|
|
24
|
+
if (!s) return "";
|
|
25
|
+
if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
|
|
26
|
+
return s.slice(1, -1).trim();
|
|
27
|
+
}
|
|
28
|
+
return s;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isSafeCssUrl(url = "") {
|
|
32
|
+
const s = String(url).trim();
|
|
33
|
+
if (!s) return false;
|
|
34
|
+
if (/[\u0000-\u001F\u007F]/.test(s)) return false;
|
|
35
|
+
if (/^(javascript|vbscript|data|file):/i.test(s)) return false;
|
|
36
|
+
|
|
37
|
+
// 站内相对地址保留
|
|
38
|
+
if (/^(\/|\.\/|\.\.\/|#)/.test(s)) return true;
|
|
39
|
+
|
|
40
|
+
// 绝对地址与协议相对地址:仅允许白名单域名
|
|
41
|
+
try {
|
|
42
|
+
const normalized = s.startsWith("//") ? `https:${s}` : s;
|
|
43
|
+
const u = new URL(normalized);
|
|
44
|
+
if (!/^https?:$/i.test(u.protocol)) return false;
|
|
45
|
+
return CSS_URL_ALLOWED_HOSTS.has(u.hostname.toLowerCase());
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function sanitizeCssUrls(css = "") {
|
|
52
|
+
return String(css).replace(/url\s*\(\s*([^)]*?)\s*\)/gi, (all, rawUrl) => {
|
|
53
|
+
const clean = unquoteCssUrl(rawUrl);
|
|
54
|
+
if (!isSafeCssUrl(clean)) return "";
|
|
55
|
+
const escaped = clean.replace(/"/g, '\\"');
|
|
56
|
+
return `url("${escaped}")`;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
20
59
|
|
|
21
60
|
function stripDangerousCss(css) {
|
|
22
61
|
if (!css) return css;
|
|
23
62
|
return css
|
|
24
63
|
.replace(/@import\s+[^;]+;?/gi, "")
|
|
25
|
-
.replace(/
|
|
26
|
-
.replace(
|
|
64
|
+
.replace(/expression\s*\([^)]*\)/gi, "")
|
|
65
|
+
.replace(/-moz-binding\s*:\s*[^;]+;?/gi, "")
|
|
66
|
+
.replace(/behavior\s*:\s*[^;]+;?/gi, "")
|
|
67
|
+
.replace(/@charset\s+[^;]+;?/gi, "");
|
|
27
68
|
}
|
|
28
69
|
|
|
29
70
|
// 暴力把 style 内容 nest 到 .c-article
|
|
30
71
|
function nestCssBrutally(cssText, scope = ".c-article") {
|
|
31
72
|
let css = stripDangerousCss(cssText || "");
|
|
73
|
+
css = sanitizeCssUrls(css);
|
|
32
74
|
if (!css.trim()) return css;
|
|
33
75
|
|
|
34
76
|
const root = postcss.parse(css, { parser: safeParser });
|
|
@@ -101,11 +143,12 @@ export default function sanitizeRichText(html) {
|
|
|
101
143
|
// 移除 on*
|
|
102
144
|
for (const k of Object.keys(out)) if (/^on/i.test(k)) delete out[k];
|
|
103
145
|
|
|
104
|
-
// style 属性:禁 @import / url(
|
|
146
|
+
// style 属性:禁 @import / expression,url() 仅保留安全协议
|
|
105
147
|
if (typeof out.style === "string" && out.style) {
|
|
106
148
|
let s = out.style;
|
|
107
149
|
s = s.replace(/@import\s+[^;]+;?/gi, "");
|
|
108
|
-
s =
|
|
150
|
+
s = sanitizeCssUrls(s);
|
|
151
|
+
s = s.replace(/expression\s*\([^)]*\)/gi, "");
|
|
109
152
|
s = s.replace(/;;+/g, ";").trim();
|
|
110
153
|
if (!s) delete out.style;
|
|
111
154
|
else out.style = s;
|
|
@@ -127,4 +170,4 @@ export default function sanitizeRichText(html) {
|
|
|
127
170
|
},
|
|
128
171
|
},
|
|
129
172
|
});
|
|
130
|
-
}
|
|
173
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
+
<!-- @圈人pop:作者卡片 -->
|
|
2
3
|
<div class="w-author" v-loading="loading">
|
|
3
4
|
<div class="w-author-wrapper el-popover" v-if="data" :style="{ backgroundImage: `url(${bg})` }">
|
|
4
5
|
<div class="u-author">
|
|
@@ -20,9 +21,9 @@
|
|
|
20
21
|
</a>
|
|
21
22
|
<div class="u-extend">
|
|
22
23
|
<el-tooltip class="item" effect="dark" placement="top">
|
|
23
|
-
<
|
|
24
|
+
<template #content>
|
|
24
25
|
<span class="u-tips">经验值:{{ data.experience }}</span>
|
|
25
|
-
</
|
|
26
|
+
</template>
|
|
26
27
|
<span
|
|
27
28
|
class="u-level"
|
|
28
29
|
:class="'lv-' + level"
|
|
@@ -43,7 +44,18 @@
|
|
|
43
44
|
<!-- <div class="u-honor" :style="honorStyle" v-if="honor">{{ honor }}</div> -->
|
|
44
45
|
<div class="u-trophy" v-if="hasTrophy">
|
|
45
46
|
<div class="u-medals" v-if="medals && medals.length">
|
|
46
|
-
<
|
|
47
|
+
<div class="m-medal">
|
|
48
|
+
<a
|
|
49
|
+
v-for="item in medals"
|
|
50
|
+
:key="item.id"
|
|
51
|
+
:href="medalLink(item)"
|
|
52
|
+
target="_blank"
|
|
53
|
+
class="u-medal"
|
|
54
|
+
:title="item.medal_desc"
|
|
55
|
+
>
|
|
56
|
+
<img class="u-medal-img" :src="showMedalIcon(item.medal)" />
|
|
57
|
+
</a>
|
|
58
|
+
</div>
|
|
47
59
|
</div>
|
|
48
60
|
</div>
|
|
49
61
|
<div class="u-teams" v-if="teams && teams.length">
|
|
@@ -57,25 +69,29 @@
|
|
|
57
69
|
</template>
|
|
58
70
|
|
|
59
71
|
<script>
|
|
60
|
-
import { authorLink, getLink, getThumbnail } from "@jx3box/jx3box-common/js/utils";
|
|
61
|
-
import { getUserInfo, getUserMedals, getUserPublicTeams } from "
|
|
62
|
-
import { getDecoration, getDecorationJson } from "
|
|
63
|
-
import { __server, __imgPath, __userLevelColor, __cdn } from "@jx3box/jx3box-common/data/jx3box.json";
|
|
72
|
+
import { authorLink, getLink, getMedalLink, getThumbnail } from "@jx3box/jx3box-common/js/utils";
|
|
73
|
+
import { getUserInfo, getUserMedals, getUserPublicTeams } from "../service/author";
|
|
74
|
+
import { getDecoration, getDecorationJson } from "../service/cms";
|
|
64
75
|
import User from "@jx3box/jx3box-common/js/user";
|
|
65
|
-
import
|
|
76
|
+
import JX3BOX from "@jx3box/jx3box-common/data/jx3box.json";
|
|
66
77
|
import Avatar from "./Avatar.vue";
|
|
67
|
-
import medal from "./medal.vue";
|
|
68
78
|
const ATCARD_KEY = "decoration_atcard";
|
|
69
79
|
const DECORATION_JSON = "decoration_json";
|
|
70
80
|
const DECORATION_KEY = "decoration_me";
|
|
71
81
|
const HONOR_KEY = "honor_me";
|
|
82
|
+
|
|
83
|
+
const { __server, __imgPath, __userLevelColor, __cdn, __userLevel } = JX3BOX;
|
|
72
84
|
export default {
|
|
73
85
|
name: "Author",
|
|
74
86
|
components: {
|
|
75
|
-
medal,
|
|
76
87
|
Avatar,
|
|
77
88
|
},
|
|
78
|
-
props:
|
|
89
|
+
props: {
|
|
90
|
+
uid: {
|
|
91
|
+
type: [String, Number],
|
|
92
|
+
required: true,
|
|
93
|
+
},
|
|
94
|
+
},
|
|
79
95
|
data: () => ({
|
|
80
96
|
data: null,
|
|
81
97
|
medals: [],
|
|
@@ -249,7 +265,10 @@ export default {
|
|
|
249
265
|
},
|
|
250
266
|
|
|
251
267
|
showMedalIcon: function (val) {
|
|
252
|
-
return
|
|
268
|
+
return __cdn + "/design/medals/user/" + val + ".gif";
|
|
269
|
+
},
|
|
270
|
+
medalLink: function ({ rank_id, medal_type = "rank" }) {
|
|
271
|
+
return getMedalLink(rank_id, medal_type);
|
|
253
272
|
},
|
|
254
273
|
showMedalDesc: function (item) {
|
|
255
274
|
return item.medal_desc || medal_map[item.medal];
|
|
@@ -272,7 +291,7 @@ export default {
|
|
|
272
291
|
</script>
|
|
273
292
|
|
|
274
293
|
<style lang="less">
|
|
275
|
-
@import "
|
|
294
|
+
@import "../assets/css/module/author.less";
|
|
276
295
|
.w-author {
|
|
277
296
|
.w-author-wrapper {
|
|
278
297
|
background-repeat: no-repeat;
|
|
@@ -9,11 +9,30 @@
|
|
|
9
9
|
</template>
|
|
10
10
|
|
|
11
11
|
<script>
|
|
12
|
-
import
|
|
12
|
+
import JX3BOX from "@jx3box/jx3box-common/data/jx3box.json";
|
|
13
13
|
import { showAvatar, authorLink } from "@jx3box/jx3box-common/js/utils";
|
|
14
|
+
|
|
15
|
+
const { __server, __imgPath } = JX3BOX;
|
|
14
16
|
export default {
|
|
15
17
|
name: "Avatar",
|
|
16
|
-
props:
|
|
18
|
+
props: {
|
|
19
|
+
uid: {
|
|
20
|
+
type: [String, Number],
|
|
21
|
+
required: true,
|
|
22
|
+
},
|
|
23
|
+
url: {
|
|
24
|
+
type: String,
|
|
25
|
+
default: "",
|
|
26
|
+
},
|
|
27
|
+
frame: {
|
|
28
|
+
type: String,
|
|
29
|
+
default: "",
|
|
30
|
+
},
|
|
31
|
+
size: {
|
|
32
|
+
type: [String, Number],
|
|
33
|
+
default: "s",
|
|
34
|
+
},
|
|
35
|
+
},
|
|
17
36
|
components: {},
|
|
18
37
|
data: function() {
|
|
19
38
|
return {
|
|
@@ -49,7 +68,7 @@ export default {
|
|
|
49
68
|
}
|
|
50
69
|
.c-avatar-pic {
|
|
51
70
|
.db;
|
|
52
|
-
.
|
|
71
|
+
.size(100%);
|
|
53
72
|
.r(100%);
|
|
54
73
|
}
|
|
55
74
|
.c-avatar-frame {
|