UNPKG

12.1 kBJavaScriptView Raw
1/* eslint-disable indent */
2import { isObject, isString } from './utils';
3import { computedImageSize } from './helpers';
4import { addEvent } from './helpers';
5import EMOJIMAP from './emoji.json';
6
7/**
8 * @type {boolean} 是否开发环境
9 */
10let isDev = false;
11
12/**
13 * @type {boolean} 是是否服务端渲染
14 */
15let isSSR = false;
16
17try {
18 isDev = process.env && process.env.NODE_ENV === 'development';
19
20 isSSR = process.env && process.env.VUE_ENV === 'server';
21} catch (e) {
22 //
23}
24
25/**
26 * @type {Regexp} 判断是否合法图片
27 */
28const validImageRegexp = /^(http|https):\/\/(imgtest|img)\.guihuazhi\.com\/[A-z0-9/.]*?$/;
29
30/**
31 * @type {Regexp} 表情校验
32 */
33const validEmoji = /(\[[\u4e00-\u9fa5]{1,4}\])/g;
34
35class Parse {
36 constructor(option) {
37 this.platform = option.platform;
38 }
39
40 createElement(d) {
41 if (isObject(d)) {
42 const { tag, attrs = {}, child = [], props, content } = d;
43 let node = document.createElement(tag);
44
45 Object.keys(attrs).forEach(function(key) {
46 node.setAttribute(key, attrs[key]);
47 });
48
49 if (tag === 'img') {
50 node = this.renderImageDom(props, attrs);
51 }
52
53 if (tag === 'a') {
54 node = this.renderLinkDom(props);
55 }
56
57 if (content) {
58 node.innerHTML = content;
59 } else {
60 child.forEach(c => {
61 node.appendChild(this.createElement(c));
62 });
63 }
64
65 return node;
66 } else if (typeof d === 'string') {
67 return document.createTextNode(d);
68 }
69 }
70
71 reset() {
72 this.unbindImageEvents && this.unbindImageEvents();
73 }
74
75 // 懒加载图片
76 bindImageLazyLoad() {
77 this.clientHeight = window.screen.height;
78
79 this.unbindImageEvents = addEvent(
80 window,
81 'scroll',
82 this.loadImageFunc.bind(this)
83 );
84 setTimeout(() => {
85 // 初始化时加载一次图片
86 this.loadImageFunc();
87 }, 0);
88 }
89
90 // 取消懒加载图片
91 unbindImageLazyLoad() {
92 this.unbindImageEvents();
93 }
94
95 loadImageFunc() {
96 (this.imageDoc =
97 this.imageDoc ||
98 document.querySelectorAll(`.huazhi.huazhi-${this.platform} img`)).forEach(
99 el => {
100 if (el.getAttribute('src')) return;
101
102 const { src } = el.dataset;
103
104 const o = el.getBoundingClientRect();
105
106 if (o.top < this.clientHeight) {
107 el.setAttribute('src', src);
108 el.previousSibling.style.opacity = 0;
109 }
110 }
111 );
112 }
113
114 /**
115 * dom-tree => html
116 * @param {array<object>} tree
117 */
118 renderDomTreeHTML(tree, platform) {
119 return tree
120 .map(block => {
121 switch (block.tag) {
122 case 'h2':
123 return `<h2>${this.renderDomTreeHTML(block.child)}</h2>`;
124
125 case 'blockquote':
126 return `<blockquote>${this.renderDomTreeHTML(
127 block.child
128 )}</blockquote>`;
129
130 case 'p':
131 return `<p>${this.renderDomTreeHTML(block.child)}</p>`;
132
133 case 'b':
134 return `<b>${block.content}</b>`;
135
136 case 'i':
137 return `<i style="background-image: url(${
138 block.props.url
139 })" emoji="${block.props.emoji}"></i>`;
140
141 case 'img':
142 return this.renderImage(block.props, block.attrs);
143
144 case 'video':
145 return this.renderVideo(block.props);
146
147 case 'hr':
148 return '<hr />';
149
150 case 'a':
151 return this.renderLink(block.props);
152
153 // webview内禁止直接跳转
154 default:
155 // 非兼容格式不做显示
156 return isString(block) ? block : '';
157 }
158 })
159 .join('');
160 }
161
162 // replaceBlock(block) {
163 // return block.replace
164 // }
165
166 /**
167 * 针对web的文本解析处理
168 * @param {array<object>} json
169 * @return
170 */
171 jsonToDomTree(json) {
172 let results = [];
173 let imageIndex = 0;
174
175 if (typeof json === 'string') {
176 try {
177 json = JSON.parse(json);
178 } catch (e) {
179 //
180 return results;
181 }
182 }
183
184 for (let i = 0, l = json.length; i < l; i++) {
185 let block = json[i];
186 if (block.type === 'text') {
187 let tag;
188 if (block.head === 1) {
189 tag = 'h2';
190 } else if (block.block === 1) {
191 tag = 'blockquote';
192 } else {
193 tag = 'p';
194 }
195 let child = this.parseInline(block.value);
196 results.push({ tag, child });
197 } else if (block.type === 'image' && this.isImage(block)) {
198 results.push({
199 tag: 'img',
200 attrs: {
201 src: block.url,
202 index: imageIndex++
203 },
204 props: block
205 });
206 } else if (block.type === 'line') {
207 results.push({
208 tag: 'hr'
209 });
210 } else if (block.type === 'video') {
211 results.push({
212 tag: 'video',
213 props: block
214 });
215 }
216 }
217
218 return results;
219 }
220
221 /**
222 * 评论中的文本解析
223 * @param {string} str 评论文本
224 * @return {string} html文本
225 */
226 parseComment(str) {
227 let result = this.parseEmoji(str);
228 let html = this.renderDomTreeHTML(result, this.platform);
229 return html;
230 }
231
232 /**
233 * json => html
234 * @param {string|array<object>} json 原始json数据 | 数组结构
235 * @param {string} platform 平台 web | mobile | webview | editor
236 * @param {HTMLElement} wrapperDom 是否返回document对象
237 * @param {number} containerWidth 容器宽度
238 * @return {string|HTMLElement} html字符串/document对象
239 */
240 jsonToHTML(json, wrapperDom, containerWidth = 0) {
241 // json解析
242 if (typeof json === 'string') {
243 try {
244 json = JSON.parse(json);
245 } catch (e) {
246 if (isDev) throw new Error('json type error');
247 return '';
248 }
249 }
250
251 const tree = this.jsonToDomTree(json);
252
253 // console.log('tree', JSON.parse(JSON.stringify(tree)))
254
255 let html = this.renderDomTreeHTML(tree, this.platform, containerWidth);
256 // ssr环境或者editor直接返回html
257 if (isSSR || this.platform === 'editor') {
258 return html;
259 }
260
261 if (wrapperDom) {
262 wrapperDom.classList.add('huazhi', `huazhi-${this.platform}`);
263 wrapperDom.innerHTML = html;
264 }
265
266 // 移动端初始化
267 if (this.lazyLoad) {
268
269 this.bindImageLazyLoad();
270 }
271
272 if (this.initEvents && typeof this.initEvents === 'function') {
273 this.initEvents();
274 }
275
276 return wrapperDom ? wrapperDom : html;
277 }
278
279 renderText(value) {
280 let html = '';
281 let length = value.length;
282 for (let i = 0; i < length; i++) {
283 let v = value[i];
284 if (v.bold) {
285 html += `<strong>${v.content}</strong>`
286 } else if (v.type === 'link') {
287 html += `${this.renderLink(v)}`
288 } else {
289 html += `${v.content}`
290 }
291 }
292 if (length === 0) {
293 html = '<br>'
294 }
295 return html;
296 }
297
298 renderBlock(o) {
299 let classStr = o.center ? "class='ql-align-center'" : '';
300 if (o.head) {
301 return `<h2 ${classStr}>${this.renderText(o.value)}</h2>`
302 } else if (o.block) {
303 return `<blockquote ${classStr}>${this.renderText(o.value)}</blockquote>`
304 } else {
305 return `<p ${classStr}>${this.renderText(o.value)}</p>`
306 }
307 }
308
309 renderLine() {
310 return "<hr />"
311 }
312
313 renderList(o) {
314 let html = '';
315 let liList = '';
316 let length = o.value.length;
317 for (let i = 0; i < length; i++) {
318 let v = o.value[i];
319 liList += `<li>${this.renderText(v)}</li>`
320 }
321 if (o.type === 'ol') {
322 html = `<ol>${liList}</ol>`
323 } else if (o.type === 'ul') {
324 html = `<ul>${liList}</ul>`
325 }
326 return html
327 }
328
329 /**
330 * json => html
331 * @param {string|array<object>} json 原始json数据 | 数组结构
332 * @param {string} platform 平台 web | mobile | webview | editor
333 * @param {HTMLElement} wrapperDom 是否返回document对象
334 * @param {number} containerWidth 容器宽度
335 * @return {string|HTMLElement} html字符串/document对象
336 */
337 jsonToHTML2(json, wrapperDom, containerWidth = 0) {
338 // json解析
339 if (typeof json === 'string') {
340 try {
341 json = JSON.parse(json);
342 } catch (e) {
343 if (isDev) throw new Error('json type error');
344 return '';
345 }
346 }
347 // const renderMap = {
348 // 'text': this.renderBlock,
349 // 'line': this.renderLine,
350 // 'image': this.renderImage,
351 // 'video': this.renderVideo,
352 // 'ul': this.renderList,
353 // 'ol': this.renderList,
354 // };
355 let html = '';
356 let length = json.length;
357 for (let i = 0; i < length; i++) {
358 let j = json[i];
359 let type = j.type;
360 if (type === 'text') {
361 html +=this.renderBlock(j)
362 } else if (type === 'line') {
363 html +=this.renderLine(j)
364 } else if (type === 'image') {
365 html +=this.renderImage(j)
366 } else if (type === 'video') {
367 html +=this.renderVideo(j)
368 } else if (type === 'ul' || type === 'ol') {
369 html +=this.renderList(j)
370 } else {
371 if (j.content) {
372 html += `<p>${json[i].content}</p>`
373 }
374 }
375 }
376
377 if (this.initEvents && typeof this.initEvents === 'function') {
378 // webview绑定事件
379 this.initEvents();
380 }
381
382
383 return html;
384
385 }
386
387 /**
388 * 解析表情
389 * @param {string} 原始文本
390 * @return {array<object|string>} 表情字符串数组
391 */
392 parseEmoji(text) {
393 if (typeof text !== 'string') return '';
394 let results = [];
395 let splitIndex = 0;
396 let tmp;
397
398 while ((tmp = validEmoji.exec(text))) {
399 let { 0: emojiName, index: startIndex } = tmp;
400 let { lastIndex } = validEmoji;
401 // 若表情存在
402 if (EMOJIMAP[emojiName]) {
403 results.push(text.substring(splitIndex, startIndex));
404
405 results.push({
406 tag: 'i',
407 props: {
408 emoji: emojiName,
409 url: EMOJIMAP[emojiName].url
410 },
411 attr: {}
412 });
413
414 // 修改下次截取位置
415 splitIndex = lastIndex;
416 }
417 }
418
419 // 末尾处理
420 if (splitIndex < text.length) {
421 results.push(text.substring(splitIndex));
422 }
423
424 return results;
425 }
426
427 /**
428 * 解析行内文本
429 * @param {array<object>} 行内文本数组
430 * @param {string} platform
431 */
432 parseInline(inlineList) {
433 let results = [];
434 for (let j = 0, l = inlineList.length; j < l; j++) {
435 let inline = inlineList[j];
436 if (inline.type === 'string') {
437 if (this.platform === 'editor') {
438 // 编辑器连续换行bug
439 if (j === inlineList.length - 1) {
440 if (inline.content.endsWith('\n') || inline.content.endsWith('\r\n')) {
441 inline.content = inline.content.slice(0, -1)
442 }
443 }
444 }
445 if (inline.bold === 1) {
446 // 加粗文本
447 results.push({ tag: 'b', attr: {}, content: inline.content });
448 } else {
449 // 纯文本
450 results.push(...this.parseEmoji(inline.content));
451 }
452 } else if (inline.type === 'link') {
453 results.push({
454 tag: 'a',
455 props: {
456 ...inline,
457 }
458 });
459 }
460 }
461
462 return results;
463 }
464
465 /**
466 * 校验是否合法图片
467 * @param {object} image 上传获得image对象
468 * @return {boolean} 检测结果
469 */
470 isImage(image) {
471 return (
472 isObject(image) &&
473 typeof image.url === 'string' &&
474 validImageRegexp.test(image.url)
475 );
476 }
477
478 /**
479 * html => json
480 * 富文本编辑内容格式化
481 * @param {string|object} source html字符串/detail结构
482 * @param {boolean} isOps 是否quill的detail结构
483 * @param {boolean} isJSonString 是否转为json-string
484 * @return {array<object>} j
485 */
486 htmlToJSON(source, isOps, isJSonString = true) {
487 if (isUndefined(source) || (!isString(source) && !isArray(source))) {
488 if (isDev)
489 throw new Error('source must be a string type or quill detail');
490
491 return isJSonString ? '[]' : [];
492 }
493
494 let results = [];
495
496 return results;
497 }
498}
499
500export default Parse;