unoffical wafrn mirror wafrn.net
atproto social-network activitypub
at angular21 109 lines 3.6 kB view raw
1import { unified } from 'unified' 2import rehypeParse from 'rehype-parse' 3import rehypeStringify from 'rehype-stringify' 4import postcss from 'postcss' 5import safeParser from 'postcss-safe-parser' 6import Color from 'color' 7 8function parseInlineStyle(style: string) { 9 if (!style) return {} 10 // @ts-ignore 11 const root = postcss.parse(`*{${style}}`, { parser: safeParser }) 12 const types: Record<string, string> = {} 13 root.walkDecls(d => { types[d.prop] = d.value }) 14 return types 15} 16 17export async function htmlToMfm(input: string): Promise<string> { 18 const file = await unified() 19 .use(rehypeParse, { fragment: true }) 20 .use(rehypeToMfm) 21 .use(rehypeStringify, { allowDangerousHtml: true }) 22 .process(input) 23 24 return file.toString().replace('&#x3C;', '<') 25} 26 27function cssToMfm(child: string, types: Record<string, string>): string { 28 let mfm = child 29 if (types['text-align'] === 'center') mfm = `<center>${mfm}</center>` 30 if (types['color']) { 31 try { 32 const c = Color(types['color']) 33 mfm = `$[fg.${c.hex().replace('#', '')} ${mfm}]` 34 } catch { 35 mfm = `$[fg.${types['color'].replace('#', '')} ${mfm}]` 36 } 37 } 38 if (types['background-color']) { 39 try { 40 const c = Color(types['background-color']) 41 mfm = `$[bg.${c.hex().replace('#', '')} ${mfm}]` 42 } catch { 43 mfm = `$[bg.${types['background-color'].replace('#', '')} ${mfm}]` 44 } 45 } 46 if (types['background']) { 47 try { 48 const c = Color(types['background']) 49 mfm = `$[bg.${c.hex().replace('#', '')} ${mfm}]` 50 } catch { 51 mfm = `$[bg.${types['background'].replace('#', '')} ${mfm}]` 52 } 53 } 54 if (types['font-weight'] === 'bold') mfm = `**${mfm}**` 55 if (types['font-style'] === 'italic') mfm = `*${mfm}*` 56 if (types['text-decoration']?.includes('line-through')) mfm = `~~${mfm}~~` 57 if (types['filter']?.includes('blur')) mfm = `$[blur ${mfm}]` 58 59 return mfm 60} 61 62function rehypeToMfm() { 63 return (tree: any) => { 64 function nodeToMfm(node: any): string { 65 if (node.type === 'text') return node.value 66 if (node.type === 'element') { 67 const child = (node.children || []).map(nodeToMfm).join('') 68 const style = node.properties?.style || '' 69 const href = node.properties?.href || '' 70 const types = parseInlineStyle(style) 71 const schild = cssToMfm(child, types) 72 73 switch (node.tagName.toLowerCase()) { 74 case 'strong': 75 case 'b': return `**${schild}**` 76 case 'em': 77 case 'i': return `*${schild}*` 78 case 'center': return `<center>${schild}</center>` 79 case 'del': return `~~${schild}~~` 80 case 'code': return (schild.includes('\n')) ? `\`\`\`\n${schild}\n\`\`\`` : `\`${schild}\`` 81 case 'p': return `${schild}\n\n` 82 case 'br': return '\n' 83 case 'h1': return `$[x2 ${schild}]\n\n` 84 case 'h2': return `**${schild}**\n\n` 85 case 'h3': return `${schild}\n\n` 86 case 'blockquote': return `> ${schild}` 87 case 'ul': 88 case 'ol': return Array.from(node.children || []).map(nodeToMfm).join('') 89 case 'li': return `- ${schild}\n` 90 case 'a': { 91 if (schild.startsWith('@')) { 92 const mfmMention = new URL(href).hostname; 93 return `${schild}@${mfmMention}` 94 } 95 return `[${schild}](${href})` 96 } 97 default: 98 return schild 99 } 100 } 101 return node.value 102 } 103 104 const mfm = (tree.children || []).map(nodeToMfm).join('').trim() 105 tree.type = 'text' 106 tree.value = mfm 107 tree.child = [] 108 } 109}