diff --git a/packages/uni-components/src/vue/rich-text/html-parser.js b/packages/uni-components/src/components/rich-text/html-parser.js similarity index 100% rename from packages/uni-components/src/vue/rich-text/html-parser.js rename to packages/uni-components/src/components/rich-text/html-parser.js diff --git a/packages/uni-components/src/components/rich-text.ts b/packages/uni-components/src/components/rich-text/index.ts similarity index 53% rename from packages/uni-components/src/components/rich-text.ts rename to packages/uni-components/src/components/rich-text/index.ts index 2a1f15bc83986d9ae514af5cb91825bbc1c2551a..c9fb6a1b2e1e82157a9232d93034e5f764e4c5d6 100644 --- a/packages/uni-components/src/components/rich-text.ts +++ b/packages/uni-components/src/components/rich-text/index.ts @@ -1,4 +1,6 @@ -export const props = { +import parseHtml from './html-parser' + +const props = { nodes: { type: [Array, String], default: function () { @@ -6,3 +8,5 @@ export const props = { }, }, } + +export { props, parseHtml } diff --git a/packages/uni-components/src/nvue/components.ts b/packages/uni-components/src/nvue/components.ts index ca2fe557f6c873327491a0b19bf8413fd143e081..c5d1fdd804b464c74ec2d5385e7fc0784f09041c 100644 --- a/packages/uni-components/src/nvue/components.ts +++ b/packages/uni-components/src/nvue/components.ts @@ -17,6 +17,7 @@ import Form from './form' import Icon from './icon' import Swiper from './swiper' import SwiperItem from './swiper-item' +import RichText from './rich-text' export default { Navigator, Label, @@ -37,4 +38,5 @@ export default { Icon, Swiper, SwiperItem, + RichText, } diff --git a/packages/uni-components/src/nvue/rich-text/index.tsx b/packages/uni-components/src/nvue/rich-text/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..65caeb03aeca838cacdefb28a3cd10f69f953dfa --- /dev/null +++ b/packages/uni-components/src/nvue/rich-text/index.tsx @@ -0,0 +1,256 @@ +import { + defineComponent, + ExtractPropTypes, + getCurrentInstance, + ComponentInternalInstance, + // @ts-ignore + parseClassList, +} from 'vue' +import { props, parseHtml } from '../../components/rich-text' +import { parseStyleText } from '../helpers' + +const defaultFontSize = 16 +type Props = ExtractPropTypes + +export default defineComponent({ + name: 'RichText', + props, + setup(props) { + const instance = getCurrentInstance() + + return () => { + let nodes = props.nodes + if (typeof nodes === 'string') { + nodes = parseHtml(nodes) + } + + return ( + + ) + } + }, +}) + +function normalizeNodes( + nodes: Props['nodes'], + instance: ComponentInternalInstance | null, + options: { defaultFontSize: number } +) { + type NodeType = keyof typeof strategies | 'span' | 'img' | 'image' | 'text' + type NvueNode = { + type?: NodeType + __type?: string + name?: string + attrs?: { + class: string + width: string + height: string + style: string + } + attr?: { + value: string + } + text?: string + __block?: boolean + __break?: boolean + __value?: string + style?: Record + children?: NvueNode[] + } + + const TAGS = ['span', 'a', 'image', 'img'] + const strategies = { + blockquote: block, + br: br, + div: block, + dl: block, + h1: createHeading(2), + h2: createHeading(1.5), + h3: createHeading(1.17), + h4: createHeading(1), + h5: createHeading(0.83), + h6: createHeading(0.67), + hr: block, + ol: block, + p: block, + strong: bold, + table: block, + tbody: block, + tfoot: block, + thead: block, + ul: block, + } + const HTML_RE = /&(amp|gt|lt|nbsp|quot|apos);/g + const CHARS = { + amp: '&', + gt: '>', + lt: '<', + nbsp: ' ', + quot: '"', + apos: "'", + } + + // 插入换行 + const breakNode: NvueNode = { + type: 'span', + __type: 'break', + attr: { + value: '\n', + }, + } + let lastNode: NvueNode = { + __block: true, + __break: true, + children: [], + } + let breakNodes: NvueNode[] | null = null + + function parseStyle(node: NvueNode) { + const styles = Object.create(null) + + if (node.attrs) { + const classList = (node.attrs.class || '').split(' ') + + Object.assign( + styles, + parseClassList(classList, instance), + parseStyleText(node.attrs.style || '') + ) + } + + if (node.name === 'img' || node.name === 'image') { + const attrs = node.attrs + styles.width = styles.width || attrs!.width + styles.height = styles.height || attrs!.height + } + + return styles + } + + function block(node: NvueNode) { + node.__block = true + // node.attr.value = (node.attr.value || '') + '\n' + return node + } + + function heading(node: NvueNode, em: number) { + if (node.style) + !node.style.fontSize && + (node.style.fontSize = options.defaultFontSize * em) + return block(bold(node)) + } + + function createHeading(em: number) { + return function (node: NvueNode) { + return heading(node, em) + } + } + + function bold(node: NvueNode) { + if (node.style) !node.style.fontWeight && (node.style.fontWeight = 'bold') + return node + } + + function br(node: NvueNode) { + node.__value = ' ' + return block(node) + } + + function normalizeText(str: string) { + return str.replace(HTML_RE, function (match, entity: keyof typeof CHARS) { + return CHARS[entity] + }) + } + + function normalizeNode(node: NvueNode) { + let type: NodeType = (node.name || '').toLowerCase() as NodeType + + const __type = type + + const strategy = strategies[type as keyof typeof strategies] + + if (TAGS.indexOf(type) === -1) { + type = 'span' + } + if (type === 'img') { + type = 'image' + } + const nvueNode: NvueNode = { + type, + __type, + attr: Object.create(null), + } + + if (node.type === 'text' || node.text) { + nvueNode.__value = nvueNode.attr!.value = normalizeText( + (node.text || '').trim() + ) + } + + if (node.attrs) { + Object.keys(node.attrs).forEach((name) => { + if (name !== 'class' && name !== 'style') { + ;(nvueNode.attr as any)[name] = (node.attrs as any)[name] + } + }) + } + + nvueNode.style = parseStyle(node) + + if (strategy) { + strategy(nvueNode) + } + + if (lastNode.__block || nvueNode.__block) { + if (!breakNodes) { + lastNode.children!.push(breakNode) + breakNodes = [lastNode, breakNode] + } + } + // 进入节点 + lastNode = nvueNode + if ( + lastNode.__value || + (lastNode.type === 'image' && (lastNode.attr as any).src) + ) { + // 文本和图像消费换行 + breakNodes = null + } + nvueNode.children = normalizeNodes(node.children) + // 退出节点 + lastNode = nvueNode + if ( + lastNode.__block && + (lastNode.style as any).height && + !/^0(px)?$/.test((lastNode.style as any).height) + ) { + // 有高度的块元素消费换行 + breakNodes = null + } + + return nvueNode + } + + function normalizeNodes(nodes?: NvueNode[]) { + if (Array.isArray(nodes)) { + return nodes.map((node) => normalizeNode(node)) + } + return [] + } + + const nvueNodes = normalizeNodes(nodes as NvueNode[]) + if (breakNodes) { + // 撤销未消费的换行 + const [lastNode, breakNode] = breakNodes as NvueNode[] + const children = lastNode.children! + const index = children.indexOf(breakNode) + children.splice(index, 1) + } + return nvueNodes +} diff --git a/packages/uni-components/src/vue/rich-text/index.tsx b/packages/uni-components/src/vue/rich-text/index.tsx index 1522e8067bbbe5eed5d594f1f7e43fcdae3b87b3..a30965983a128a2453bd9051fb3e75826556702a 100644 --- a/packages/uni-components/src/vue/rich-text/index.tsx +++ b/packages/uni-components/src/vue/rich-text/index.tsx @@ -4,9 +4,8 @@ import { useCustomEvent, EmitEvent, } from '@dcloudio/uni-components' -import parseHtml from './html-parser' import parseNodes from './nodes-parser' -import { props } from '../../components/rich-text' +import { props, parseHtml } from '../../components/rich-text' export default /*#__PURE__*/ defineBuiltInComponent({ name: 'RichText',