Files: b6d7ee34ab7b57861dd88fc633ba98bfb721b099 / lib / depject / message / html / markdown.js
4566 bytesRaw
1 | const renderer = require('ssb-markdown') |
2 | const h = require('mutant/h') |
3 | const ref = require('ssb-ref') |
4 | const nest = require('depnest') |
5 | const htmlEscape = require('html-escape') |
6 | const watch = require('mutant/watch') |
7 | const querystring = require('querystring') |
8 | const nodeEmoji = require('node-emoji') |
9 | |
10 | exports.needs = nest({ |
11 | 'blob.sync.url': 'first', |
12 | 'blob.obs.has': 'first' |
13 | }) |
14 | |
15 | exports.gives = nest('message.html.markdown') |
16 | |
17 | exports.create = function (api) { |
18 | return nest('message.html.markdown', markdown) |
19 | |
20 | function markdown (content, { classList = null } = {}) { |
21 | if (typeof content === 'string') { content = { text: content } } |
22 | const mentions = {} |
23 | const typeLookup = {} |
24 | const emojiMentions = {} |
25 | if (Array.isArray(content.mentions)) { |
26 | content.mentions.forEach(function (link) { |
27 | if (link && link.link && link.type) { |
28 | typeLookup[link.link] = link.type |
29 | } |
30 | if (link && link.name && link.link) { |
31 | if (link.emoji) { |
32 | // handle custom emoji |
33 | emojiMentions[link.name] = link.link |
34 | } else { |
35 | // handle old-style patchwork v2 mentions (deprecated) |
36 | mentions['@' + link.name] = link.link |
37 | } |
38 | } |
39 | }) |
40 | } |
41 | |
42 | return h('Markdown', { |
43 | classList, |
44 | hooks: [ |
45 | LoadingBlobHook(api.blob.obs.has), |
46 | LargeEmojiHook() |
47 | ], |
48 | innerHTML: renderer.block(content.text, { |
49 | emoji: (emoji) => { |
50 | if (emojiMentions[emoji]) { |
51 | return renderEmoji(emoji, api.blob.sync.url(emojiMentions[emoji])) |
52 | } else { |
53 | // https://github.com/omnidan/node-emoji/issues/76 |
54 | const emojiCharacter = nodeEmoji.get(emoji).replace(/:/g, '') |
55 | return `<span class="Emoji">${emojiCharacter}</span>` |
56 | } |
57 | }, |
58 | toUrl: (id) => { |
59 | const link = ref.parseLink(id) |
60 | if (link && ref.isBlob(link.link)) { |
61 | const url = api.blob.sync.url(link.link) |
62 | const query = {} |
63 | if (link.query && link.query.unbox) query.unbox = link.query.unbox |
64 | if (typeLookup[link.link]) query.contentType = typeLookup[link.link] |
65 | return url + '?' + querystring.stringify(query) |
66 | } else if (link || id.startsWith('#') || id.startsWith('?')) { |
67 | return id |
68 | } else if (mentions[id]) { |
69 | // handle old-style patchwork v2 mentions (deprecated) |
70 | return mentions[id] |
71 | } |
72 | return false |
73 | }, |
74 | imageLink: (id) => id |
75 | }) |
76 | }) |
77 | } |
78 | |
79 | function renderEmoji (emoji, url) { |
80 | if (!url) return ':' + emoji + ':' |
81 | |
82 | return ` |
83 | <img |
84 | src="${htmlEscape(url)}" |
85 | alt=":${htmlEscape(emoji)}:" |
86 | title=":${htmlEscape(emoji)}:" |
87 | class="emoji" |
88 | > |
89 | ` |
90 | } |
91 | } |
92 | |
93 | function LoadingBlobHook (hasBlob) { |
94 | return function (element) { |
95 | const releases = [] |
96 | element.querySelectorAll('img').forEach(img => { |
97 | const id = ref.extract(img.src) |
98 | if (id) { |
99 | releases.push(watch(hasBlob(id), has => { |
100 | if (has === false) { |
101 | img.classList.add('-pending') |
102 | } else { |
103 | img.classList.remove('-pending') |
104 | } |
105 | })) |
106 | } |
107 | }) |
108 | return function () { |
109 | while (releases.length) { |
110 | releases.pop()() |
111 | } |
112 | } |
113 | } |
114 | } |
115 | |
116 | function LargeEmojiHook () { |
117 | return function (element) { |
118 | // if a line has only emoji, we want them to be LARGE |
119 | // |
120 | // first select for all emoji as the first child of a paragraph |
121 | element.querySelectorAll('p > .Emoji:nth-child(1)').forEach(firstEmojiElement => { |
122 | // then collect all emoji siblings. |
123 | // if a sibling is not an emoji or an empty text node, early return. |
124 | const emojiElements = [] |
125 | let nextNode = firstEmojiElement |
126 | |
127 | // before we start, check that matched emoji element's previous sibling is empty text. |
128 | if (firstEmojiElement.previousSibling && firstEmojiElement.previousSibling.textContent.trim() !== '') return |
129 | |
130 | while (nextNode !== null) { |
131 | switch (nextNode.nodeType) { |
132 | case document.ELEMENT_NODE: |
133 | if (nextNode.className !== 'Emoji') return |
134 | emojiElements.push(nextNode) |
135 | break |
136 | case document.TEXT_NODE: |
137 | if (nextNode.textContent.trim() !== '') return |
138 | break |
139 | } |
140 | nextNode = nextNode.nextSibling |
141 | } |
142 | |
143 | // set all emoji children to be LARGE |
144 | emojiElements.forEach(emojiElement => { |
145 | emojiElement.classList.add('-large') |
146 | }) |
147 | }) |
148 | } |
149 | } |
150 |
Built with git-ssb-web