forked from elk-zone/elk
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcontent-render.ts
183 lines (161 loc) · 5.43 KB
/
content-render.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
import type { ElementNode, Node } from 'ultrahtml'
import type { VNode } from 'vue'
import type { ContentParseOptions } from './content-parse'
import { decode } from 'tiny-decode'
import { ELEMENT_NODE, TEXT_NODE } from 'ultrahtml'
import { Fragment, h, isVNode } from 'vue'
import { RouterLink } from 'vue-router'
import AccountHoverWrapper from '~/components/account/AccountHoverWrapper.vue'
import TagHoverWrapper from '~/components/account/TagHoverWrapper.vue'
import ContentCode from '~/components/content/ContentCode.vue'
import ContentMentionGroup from '~/components/content/ContentMentionGroup.vue'
import Emoji from '~/components/emoji/Emoji.vue'
import { parseMastodonHTML } from './content-parse'
function getTextualAstComponents(astChildren: Node[]): string {
return astChildren
.filter(({ type }) => type === TEXT_NODE)
.map(({ value }) => value)
.reduce((accumulator, current) => accumulator + current, '')
.trim()
}
/**
* Raw HTML to VNodes.
*
* @param content HTML content.
* @param options Options.
*/
export function contentToVNode(
content: string,
options?: ContentParseOptions,
): VNode {
let tree = parseMastodonHTML(content, options)
const textContents = getTextualAstComponents(tree.children)
// if the username only contains emojis, we should probably show the emojis anyway to avoid a blank name
if (options?.hideEmojis && textContents.length === 0)
tree = parseMastodonHTML(content, { ...options, hideEmojis: false })
return h(Fragment, (tree.children as Node[] || []).map(n => treeToVNode(n)))
}
export function nodeToVNode(node: Node): VNode | string | null {
if (node.type === TEXT_NODE)
return node.value
if (node.name === 'mention-group')
return h(ContentMentionGroup, node.attributes, () => node.children.map(treeToVNode))
// add tooltip to emojis
if (node.name === 'picture' || (node.name === 'img' && node.attributes?.alt)) {
const props = node.attributes ?? {}
props.as = node.name
return h(
Emoji,
props,
() => node.children.map(treeToVNode),
)
}
if ('children' in node) {
if (node.name === 'a') {
if (node.attributes.href?.startsWith('/') || node.attributes.href?.startsWith('.')) {
node.attributes.to = node.attributes.href
const { href: _href, target: _target, ...attrs } = node.attributes
return h(
RouterLink as any,
attrs,
() => node.children.map(treeToVNode),
)
}
// fix #3122
return h(
node.name,
node.attributes,
node.children.map((n: Node) => {
// replace span.ellipsis with bdi.ellipsis inside links
if (n && n.type === ELEMENT_NODE && n.name !== 'bdi' && n.attributes?.class?.includes('ellipsis')) {
const children = n.children.splice(0, n.children.length)
const bdi = {
...n,
name: 'bdi',
children,
} satisfies ElementNode
children.forEach((n: Node) => n.parent = bdi)
return treeToVNode(bdi)
}
return treeToVNode(n)
}),
)
}
return h(
node.name,
node.attributes,
node.children.map(treeToVNode),
)
}
return null
}
function treeToVNode(
input: Node,
): VNode | string | null {
if (!input)
return null
if (input.type === TEXT_NODE)
return decode(input.value)
if ('children' in input) {
const node = handleNode(input)
if (node == null)
return null
if (isVNode(node))
return node
return nodeToVNode(node)
}
return null
}
function addBdiNode(node: Node) {
if (node.children.length === 1 && node.children[0].type === ELEMENT_NODE && node.children[0].name === 'bdi')
return
const children = node.children.splice(0, node.children.length)
const bdi = {
name: 'bdi',
parent: node,
loc: node.loc,
type: ELEMENT_NODE,
attributes: {},
children,
} satisfies ElementNode
children.forEach((n: Node) => n.parent = bdi)
node.children.push(bdi)
}
function handleMention(el: Node) {
// Redirect mentions to the user page
if (el.name === 'a' && el.attributes.class?.includes('mention')) {
const href = el.attributes.href
if (href) {
const matchUser = href.match(UserLinkRE)
if (matchUser) {
const [, server, username] = matchUser
const handle = `${username}@${server.replace(/(.+\.)(.+\..+)/, '$2')}`
el.attributes.href = `/${server}/@${username}`
addBdiNode(el)
return h(AccountHoverWrapper, { handle, class: 'inline-block' }, () => nodeToVNode(el))
}
const matchTag = href.match(TagLinkRE)
if (matchTag) {
const [, , tagName] = matchTag
addBdiNode(el)
el.attributes.href = `/${currentServer.value}/tags/${tagName}`
return h(TagHoverWrapper, { tagName, class: 'inline-block' }, () => nodeToVNode(el))
}
}
}
return undefined
}
function handleCodeBlock(el: Node) {
if (el.name === 'pre' && el.children[0]?.name === 'code') {
const codeEl = el.children[0] as Node
const classes = codeEl.attributes.class as string
const lang = classes?.split(/\s/g).find(i => i.startsWith('language-'))?.replace('language-', '')
const code = (codeEl.children && codeEl.children.length > 0)
? recursiveTreeToText(codeEl)
: ''
return h(ContentCode, { lang, code: encodeURIComponent(code) })
}
}
function handleNode(el: Node) {
return handleCodeBlock(el) || handleMention(el) || el
}