From 7cbfb0ec5f1afdd653876425f092d9728e9ff3d8 Mon Sep 17 00:00:00 2001 From: yutent Date: Tue, 21 Mar 2023 16:50:02 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B0=83=E6=95=B4=E7=BB=84=E4=BB=B6=E6=B3=A8?= =?UTF-8?q?=E5=86=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- develop.md | 2 +- src/card/index.js | 2 +- src/code/index.js | 131 ++++++++++ src/form/button.js | 2 +- src/form/link.js | 2 +- src/form/passwd.js | 2 +- src/form/radio.js | 9 + src/icon/index.js | 2 +- src/markd/index.js | 578 +++++++++++++++++++++++++++++++++++++++++++++ src/space/index.js | 2 +- 10 files changed, 725 insertions(+), 7 deletions(-) create mode 100644 src/code/index.js create mode 100644 src/markd/index.js diff --git a/develop.md b/develop.md index 28ad8b2..f95d640 100644 --- a/develop.md +++ b/develop.md @@ -18,7 +18,7 @@ - + diff --git a/src/card/index.js b/src/card/index.js index 98022e0..4da0872 100644 --- a/src/card/index.js +++ b/src/card/index.js @@ -68,4 +68,4 @@ class Card extends Component { } } -customElements.define('wc-card', Card) +Card.reg('card') diff --git a/src/code/index.js b/src/code/index.js new file mode 100644 index 0000000..ad9b126 --- /dev/null +++ b/src/code/index.js @@ -0,0 +1,131 @@ +/** + * {} + * @author yutent + * @date 2023/03/20 18:02:01 + */ +import { html, css, Component, nextTick } from '@bd/core' + +class Code extends Component { + static props = { + code: { type: String, default: '', attribute: false }, + lang: '' + } + + static styles = [ + css` + :host { + display: flex; + border-radius: 3px; + } + .code-box { + display: flex; + flex-direction: column; + position: relative; + width: 100%; + margin: 10px 0; + border-radius: 3px; + background: #f7f8fb; + color: var(--color-dark-1); + box-shadow: 0 1px 5px rgba(0, 0, 0, 0.1); + + .title { + flex-shrink: 0; + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + height: 32px; + padding: 0 12px; + line-height: 1; + font-size: 14px; + user-select: none; + + section { + display: flex; + align-items: center; + } + i { + display: block; + width: 12px; + height: 12px; + margin-right: 6px; + border-radius: 50%; + background: var(--color-red-1); + + &:nth-child(2) { + background: var(--color-orange-1); + } + &:nth-child(3) { + background: var(--color-green-1); + } + } + } + } + `, + css` + .code-block { + display: flex; + flex-direction: column; + overflow: hidden; + overflow-y: auto; + line-height: 20px; + font-size: 14px; + color: var(--color-dark-1); + cursor: text; + counter-reset: code; + + code { + display: block; + position: relative; + min-height: 20px; + padding: 0 8px 0 45px; + font-family: Menlo, Monaco, Consolas, 'Courier New', monospace; + white-space: pre-wrap; + word-break: break-word; + + &::before { + position: absolute; + left: 0; + width: 40px; + height: 100%; + padding-right: 5px; + text-align: right; + color: var(--color-grey-1); + content: counter(code); + counter-increment: code; + } + } + } + ` + ] + + mounted() { + var txt = this.innerHTML || this.textContent + txt = txt.trim().replace(/^[\r\n]|\s{2,}$/g, '') + if (txt.startsWith('') && txt.endsWith('')) { + txt = txt.slice(5, -6).trim() + } + if (txt) { + this.textContent = '' + nextTick(_ => { + this.code = txt.replace(/</g, '<').replace(/>/g, '>') + }) + } + } + + render() { + return html` +
+
+
+
${this.lang}
+
+
+ ${this.code.split('\n').map(s => html`${s}`)} +
+
+ ` + } +} + +Code.reg('code') diff --git a/src/form/button.js b/src/form/button.js index a86e27d..ce6732b 100644 --- a/src/form/button.js +++ b/src/form/button.js @@ -264,4 +264,4 @@ class Button extends Component { } } -customElements.define('wc-button', Button) +Button.reg('button') diff --git a/src/form/link.js b/src/form/link.js index cf676a8..d9d4f98 100644 --- a/src/form/link.js +++ b/src/form/link.js @@ -152,4 +152,4 @@ class Link extends Component { } } -customElements.define('wc-link', Link) +Link.reg('link') diff --git a/src/form/passwd.js b/src/form/passwd.js index 9635d24..d179d8b 100644 --- a/src/form/passwd.js +++ b/src/form/passwd.js @@ -241,4 +241,4 @@ class Passwd extends Component { } } -customElements.define('wc-passwd', Passwd) +Passwd.reg('passwd') diff --git a/src/form/radio.js b/src/form/radio.js index e69de29..66a5d14 100644 --- a/src/form/radio.js +++ b/src/form/radio.js @@ -0,0 +1,9 @@ +/** + * {} + * @author yutent + * @date 2023/03/21 16:14:10 + */ + +import { nextTick, css, html, Component } from '@bd/core' + +class Radio extends Component {} diff --git a/src/icon/index.js b/src/icon/index.js index 87f4999..60493d7 100644 --- a/src/icon/index.js +++ b/src/icon/index.js @@ -104,4 +104,4 @@ class Icon extends Component { } } -customElements.define('wc-icon', Icon) +Icon.reg('icon') diff --git a/src/markd/index.js b/src/markd/index.js new file mode 100644 index 0000000..74be18c --- /dev/null +++ b/src/markd/index.js @@ -0,0 +1,578 @@ +/** + * markdown解析器 + * @author yutent + * @date 2020/02/07 17:14:19 + */ + +const HR_LIST = ['=', '-', '_', '*'] +const LIST_RE = /^(([\+\-\*])|(\d+\.))\s/ +const TODO_RE = /^[\+\-\*]\s\[(x|\s)\]\s/ +const ESCAPE_RE = /\\([-+*_`\]\[\(\)])/g +const QLINK_RE = /^\[(\d+)\]: ([\S]+)\s*?((['"])[\s\S]*?\4)?\s*?$/ +const TAG_RE = /<([\w\-]+)([\w\W]*?)>/g +const ATTR_RE = /\s*?on[a-zA-Z]+="[^"]*?"\s*?/g +const CODEBLOCK_RE = /```(.*?)([\w\W]*?)```/g +const BLOCK_RE = /<([\w\-]+)([^>]*?)>([\w\W]*?)<\/\1>/g +const IS_DOM_RE = /^<([\w\-]+)[^>]*?>.*?<\/\1>$/ +const STYLE_RE = /]*?>([\w\W]*?)<\/style>/g + +const INLINE = { + code: /`([^`]*?[^`\\\s])`/g, + strong: [/__([\s\S]*?[^\s\\])__(?!_)/g, /\*\*([\s\S]*?[^\s\\])\*\*(?!\*)/g], + em: [/_([\s\S]*?[^\s\\])_(?!_)/g, /\*([\s\S]*?[^\s\\*])\*(?!\*)/g], + del: /~~([\s\S]*?[^\s\\~])~~/g, + qlink: /\[([^\]]*?)\]\[(\d*?)\]/g, // 引用链接 + img: /\!\[([^\]]*?)\]\(([^)]*?)\)/g, + a: /\[([^\]]*?)\]\(([^)]*?)(\s+"([\s\S]*?)")*?\)/g, + qlist: /((
)*?)([\+\-\*]|\d+\.) (.*)/ // 引用中的列表 +} + +const ATTR_BR_SYMBOL = '⨨☇' +const NODE_BR_SYMBOL = '⨨⤶' +const ATTR_BR_EXP = new RegExp(ATTR_BR_SYMBOL, 'g') +const NODE_BR_EXP = new RegExp(NODE_BR_SYMBOL, 'g') + +const Helper = { + // 是否分割线 + isHr(str) { + var s = str[0] + if (HR_LIST.includes(s)) { + return str.slice(0, 3) === s.repeat(3) ? str.slice(3) : false + } + return false + }, + // 是否列表, -1不是, 1为有序列表, 0为无序列表 + isList(str) { + var v = str.trim() + if (LIST_RE.test(v)) { + var n = +v[0] + if (n === n) { + return 1 + } else { + return 0 + } + } + return -1 + }, + // 是否任务列表 + isTodo(str) { + var v = str.trim() + if (TODO_RE.test(v)) { + return v[3] === 'x' ? 1 : 0 + } + return -1 + }, + ltrim(str) { + if (str.trimStart) { + return str.trimStart() + } + return str.replace(/^\s+/, '') + }, + isQLink(str) { + if (QLINK_RE.test(str)) { + // l: link, t: title, $1: index + return { [RegExp.$1]: { l: RegExp.$2, t: RegExp.$3 } } + } + return false + }, + isTable(str) { + return /^\|.+?\|$/.test(str) + }, + // 是否原生dom节点 + isNativeDom(str) { + return IS_DOM_RE.test(str) + } +} + +const Decoder = { + // 内联样式 + inline(str) { + return str + .replace(INLINE.code, '$1') + .replace(INLINE.strong[0], '$1') + .replace(INLINE.strong[1], '$1') + .replace(INLINE.em[0], '$1') + .replace(INLINE.em[1], '$1') + .replace(INLINE.del, '$1') + .replace(INLINE.img, '$1') + .replace(INLINE.a, (m1, txt, link, m2, attr = '') => { + var tmp = attr + .split(';') + .filter(_ => _) + .map(_ => { + var a = _.split('=') + if (a.length > 1) { + return `${a[0]}="${a[1]}"` + } else { + return `title="${_}"` + } + }) + .join(' ') + + return `${txt}` + }) + .replace(INLINE.qlink, (m, txt, n) => { + var _ = this.__LINKS__[n] + if (_) { + var a = _.t ? `title=${_.t}` : '' + return `${txt}` + } else { + return m + } + }) + .replace(ESCAPE_RE, '$1') // 处理转义字符 + }, + // 分割线 + hr(name = '') { + return `
` + }, + // 标题 + head(str) { + if (str.startsWith('#')) { + return str.replace(/^(#{1,6}) (.*)/, (p, m1, m2) => { + m2 = m2.trim() + let level = m1.trim().length + let hash = m2.replace(/\s/g, '').replace(/<\/?[^>]*?>/g, '') + + if (level === 1) { + return `

${m2}

` + } else { + return `${m2}` + } + }) + } + return false + }, + // 引用模块 + blockquote(str) { + // + }, + // 任务 + task(str) { + var todoChecked = Helper.isTodo(str) + if (~todoChecked) { + var word = str.replace(TODO_RE, '').trim() + var stat = todoChecked === 1 ? 'checked' : '' + var txt = todoChecked === 1 ? `${word}` : word + + return `
${txt}
` + } + return false + } +} + +function fixed(str) { + // 去掉\r, 将\t转为空格(2个) + return str + .replace(/\r\n|\r/g, '\n') + .replace(/\t/g, ' ') + .replace(/\u00a0/g, ' ') + .replace(/\u2424/g, '\n') + .replace(TAG_RE, (m, name, attr) => { + // 标签内的换行, 转为一组特殊字符, 方便后面还原 + return `<${name + attr.replace(/\n/g, ATTR_BR_SYMBOL)}>` + }) + .replace(BLOCK_RE, (m, tag, attr, txt) => { + return `<${tag + attr}>${txt.replace(/\n/g, NODE_BR_SYMBOL)}` + }) + .replace(CODEBLOCK_RE, (m, lang, txt) => { + // 还原换行 + let rollback = txt.replace(NODE_BR_EXP, '\n').replace(ATTR_BR_EXP, '\n') + return '```' + lang + rollback + '```' + }) + .replace(BLOCK_RE, (m, tag, attr, txt) => { + return `<${tag + attr.replace(ATTR_BR_EXP, ' ')}>${txt + .replace(NODE_BR_EXP, '\n') + .replace(ATTR_BR_EXP, ' ')}` + }) +} + +class Tool { + constructor(list, links) { + this.list = list + this.__LINKS__ = links + } + + // 初始化字符串, 处理多余换行等 + static init(str = '') { + var links = {} + var list = [] + var lines = fixed(str).split('\n') + var isCodeBlock = false // 是否代码块 + var isTable = false // 是否表格 + var emptyLineLength = 0 //连续空行的数量 + + // console.log(lines) + + for (let it of lines) { + let tmp = it.trim() + + // 非空行 + if (tmp) { + emptyLineLength = 0 + if (tmp.startsWith('```')) { + if (isCodeBlock) { + list.push('') + } else { + list.push( + tmp.replace(/^```([\w\#\-]*?)$/, ``) + ) + } + isCodeBlock = !isCodeBlock + } else { + var qlink + if (isCodeBlock) { + it = it + .replace(/<(\/?)([a-z][a-z\d\-]*?)([^>]*?)>/g, '&lt;$1$2$3&gt;') + .replace('\\`\\`\\`', '```') + } else { + if (Helper.isTable(tmp) && !isTable) { + var thead = tmp.split('|') + // 去头去尾 + thead.shift() + thead.pop() + list.push( + `<table><thead><tr>${thead + .map(_ => `<th>${_}</th>`) + .join('')}</tr></thead><tbody>` + ) + isTable = true + continue + } else { + it = it + // 非代码块进行xss过滤 + .replace(INLINE.code, (m, txt) => { + return `\`${txt + .replace(/</g, '&lt;') + .replace(/>/g, '&gt;')}\`` + }) + .replace(/<(\/?)script[^>]*?>/g, '&lt;$1script&gt;') + .replace(TAG_RE, (m, name, attr = '') => { + // 过滤所有onXX=""事件属性 + attr = attr.replace(ATTR_RE, ' ').trim() + if (attr) { + attr = ' ' + attr + } + return `<${name}${attr}>` + }) + // 不在代码块中, 才判断引用声明 + qlink = Helper.isQLink(it) + } + } + + if (qlink) { + Object.assign(links, qlink) + } else { + list.push(it) + } + } + } else { + if (isTable) { + isTable = false + list.push('</tbody></table>') + continue + } + if (list.length === 0 || (!isCodeBlock && emptyLineLength > 0)) { + continue + } + emptyLineLength++ + list.push(tmp) + } + } + return new this(list, links) + } + + parse() { + var html = '' + var isCodeBlock = false // 是否代码块 + var emptyLineLength = 0 //连续空行的数量 + var isBlockquote = false + var isTable = false + var tableAlign = null + var blockquoteLevel = 0 + var isParagraph = false + + var isList = false + var orderListLevel = -1 + var unorderListLevel = -1 + + var isQuoteList = false // 引用中的列表, 只支持一层级 + var quoteListStyle = 0 // 1有序, 2 无序 + + // + for (let it of this.list) { + // 非空行 + if (it) { + if (~it.indexOf('<table>') || ~it.indexOf('</table>')) { + html += it + isTable = !isTable + tableAlign = true + continue + } + + if (isTable) { + let tmp = it.split('|').map(_ => _.trim()) + tmp.shift() + tmp.pop() + + // 表格分割行, 配置对齐方式的 + if (tableAlign === true) { + tableAlign = tmp.map(a => { + a = a.split(/\-+/) + if (a[0] === ':' && a[1] === ':') { + return 'align="center"' + } + if (a[1] === ':') { + return 'align="right"' + } + return '' + }) + continue + } + html += `<tr>${tmp + .map( + (_, i) => + `<td ${tableAlign[i]}>${Decoder.inline.call(this, _)}</td>` + ) + .join('')}</tr>` + continue + } + + // wc-code标签直接拼接, 判断时多拼一个 < 和 >, + // 是为了避免在 wc-markd嵌入代码块示例时, 将其内容编译为html + if (~it.indexOf('<wc-code') || ~it.indexOf('wc-code>')) { + if (isParagraph) { + isParagraph = false + html += '</p>' + } + html += it + isCodeBlock = !isCodeBlock + continue + } + + // 同上代码块的处理 + if (isCodeBlock) { + html += '\n' + it + continue + } + + // 无属性标签 + + let hrName = Helper.isHr(it) + if (typeof hrName === 'string') { + html += Decoder.hr(hrName) + continue + } + + // 优先处理一些常规样式 + it = Decoder.inline.call(this, it) + + // 标题只能是单行 + + let head = Decoder.head(it) + if (head) { + isParagraph = false + html += head + // console.log(html) + continue + } + + // 引用 + if (it.startsWith('>')) { + let innerQuote // 是否有缩进引用 + it = it.replace(/^(>+) /, (p, m) => { + let len = m.length + let tmp = '' + let loop = len + // 若之前已经有一个未闭合的引用, 需要减去已有缩进级别, 避免产生新的引用标签 + if (isBlockquote) { + loop = len - blockquoteLevel + } else { + } + + while (loop > 0) { + loop-- + tmp += '<blockquote class="md-quote">' + } + + blockquoteLevel = len + innerQuote = !!tmp + return tmp + }) + + if (isBlockquote) { + // 没有新的缩进引用时, 才添加换行 + if (innerQuote) { + // 之前有引用的列表时, 直接结束列表 + if (isQuoteList) { + html += `</${quoteListStyle === 1 ? 'ul' : 'ul'}>` + isQuoteList = false + } + } + } + + let qListChecked = it.match(INLINE.qlist) + if (qListChecked) { + let tmp1 = qListChecked[1] // 缩进的标签 + let tmp2 = +qListChecked[3] // 有序还是无序 + let tmp3 = qListChecked.pop() // 文本 + let currListStyle = tmp2 === tmp2 ? 1 : 2 + var qlist = '' + + // 已有列表 + if (isQuoteList) { + // 因为只支持一层级的列表, 所以同一级别不区分有序无序, 强制统一 + } else { + isQuoteList = true + if (currListStyle === 1) { + qlist += '<ol>' + } else { + qlist += '<ul>' + } + } + + quoteListStyle = currListStyle + + qlist += `<li>${tmp3}</li>` + html += tmp1 + qlist + } else { + if (innerQuote === false) { + html += '<br>' + } + html += it + } + + isParagraph = false + isBlockquote = true + continue + } + + // 任务 + let task = Decoder.task(it) + if (task) { + html += task + continue + } + + // 列表 + let listChecked = Helper.isList(it) + if (~listChecked) { + // 左侧空格长度 + let tmp = Helper.ltrim(it) + let ltrim = it.length - tmp.length + let word = tmp.replace(LIST_RE, '').trim() + let level = Math.floor(ltrim / 2) + let tag = listChecked > 0 ? 'ol' : 'ul' + + if (isList) { + if (listChecked === 1) { + if (level > orderListLevel) { + html = html.replace(/<\/li>$/, '') + html += `<${tag}><li>${word}</li>` + } else if (level === orderListLevel) { + html += `<li>${word}</li>` + } else { + html += `</${tag}></li><li>${word}</li>` + } + orderListLevel = level + } else { + if (level > unorderListLevel) { + html = html.replace(/<\/li>$/, '') + html += `<${tag}><li>${word}</li>` + } else if (level === unorderListLevel) { + html += `<li>${word}</li>` + } else { + html += `</${tag}></li><li>${word}</li>` + } + unorderListLevel = level + } + } else { + html += `<${tag}>` + if (listChecked === 1) { + orderListLevel = level + } else { + unorderListLevel = level + } + html += `<li>${word}</li>` + } + + isList = true + continue + } + + // 无"> "前缀的引用, 继续拼到之前的, 并且不换行 + if (isBlockquote) { + html += it + continue + } + + if (Helper.isNativeDom(it)) { + html += it + continue + } + + if (isParagraph) { + html += `${it}<br>` + } else { + html += `<p>${it}<br>` + } + isParagraph = true + } else { + // 如果是在代码中, 直接拼接, 并加上换行 + if (isCodeBlock) { + html += it + '\n' + } else { + emptyLineLength++ + + // 引用结束 + if (isBlockquote) { + isBlockquote = false + if (emptyLineLength > 0) { + emptyLineLength = 0 + while (blockquoteLevel > 0) { + blockquoteLevel-- + html += '</blockquote>' + } + } + continue + } + + if (isList) { + while (orderListLevel > -1 || unorderListLevel > -1) { + if (orderListLevel > unorderListLevel) { + html += '</ol>' + orderListLevel-- + } else { + html += '</ul>' + unorderListLevel-- + } + } + isList = false + continue + } + + // + if (isParagraph) { + isParagraph = false + html += '</p>' + } + } + } + } + + // 修正内嵌样式 + html = html.replace(STYLE_RE, (m, code) => { + return `<style>${code + .replace(/<br>/g, '') + .replace(/<p>/g, '') + .replace(/<\/p>/g, '')}</style>` + }) + delete this.list + delete this.__LINKS__ + return html + } +} + +export default function (str) { + return Tool.init(str).parse() +} diff --git a/src/space/index.js b/src/space/index.js index 5e4a53f..aad7193 100644 --- a/src/space/index.js +++ b/src/space/index.js @@ -52,4 +52,4 @@ class Space extends Component { } } -customElements.define('wc-space', Space) +Space.reg('space')