/
useMarkdown.ts
125 lines (103 loc) · 3.55 KB
/
useMarkdown.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
/**
* Add styling for markdown components here.
* NOTICE: For Syntax Highlighting, please use the ShikiHighlight component.
* We could eventually use Shiki as a Markdown plugin, but I don't want to get into it right now.
*/
import type { Ref } from 'vue'
import { computed, unref } from 'vue'
import MarkdownIt from 'markdown-it'
import MarkdownItClass from '@toycode/markdown-it-class'
import { useEventListener, whenever } from '@vueuse/core'
import type { MaybeRef } from '@vueuse/core'
import { useExternalLink } from '../gql-components/useExternalLink'
import { mapValues, isArray, flatten } from 'lodash'
export interface UseMarkdownOptions {
openExternal?: boolean
classes?: {
overwrite?: boolean
h1?: string[] | string
h2?: string[] | string
h3?: string[] | string
h4?: string[] | string
h5?: string[] | string
h6?: string[] | string
pre?: string[] | string
p?: string[] | string
a?: string[] | string
ul?: string[] | string
li?: string[] | string
ol?: string[] | string
code?: string[] | string
}
}
const defaultClasses = {
h1: ['font-medium', 'text-4xl', 'mb-6'],
h2: ['font-medium', 'text-3xl', 'mb-5'],
h3: ['font-medium', 'text-2xl', 'mb-4'],
h4: ['font-medium', 'text-1xl', 'mb-3'],
h5: ['font-medium', 'text-sm', 'mb-3'],
h6: ['font-medium', 'text-xs', 'mb-3'],
p: ['my-3 first:mt-0 text-sm mb-4'],
pre: ['rounded p-3 bg-white mb-2'],
code: ['font-medium rounded text-sm px-4px py-2px'],
a: ['text-blue-500', 'hover:underline text-sm'],
ul: ['list-disc pl-6 my-3 text-sm'],
ol: ['list-decimal pl-6 my-3 text-sm'],
}
const buildClasses = (options) => {
// --- Normalize + Merge the classes ---
// Array notation supports a single class or space-delimited classes.
// Input: `['bg-pink text-pink-500', 'text-medium']`
// Output: `[...defaults, 'bg-pink', 'text-pink-500', 'text-medium']`
// String notation is also supported and split by empty space
// Input: `'bg-pink text-pink-500'`
// Output: `[...defaults, 'bg-pink', text-pink-500']`
const _classes = defaultClasses // Constant above
const buildFlat = (value) => {
if (isArray(value)) {
return flatten<string>(value).map((arrValue) => arrValue.split(' '))
}
return value?.split(' ') ?? []
}
// Transform each value from defaultClasses and merge it with the user
// input classes.
if (options.classes) {
return mapValues(_classes, (defaultValue, key) => {
const inputClasses = buildFlat(options.classes[key])
if (options.classes.overwrite) return flatten([...inputClasses])
return flatten([...buildFlat(defaultValue), ...inputClasses])
})
}
return _classes
}
export const useMarkdown = (target: Ref<HTMLElement>, text: MaybeRef<string>, options: UseMarkdownOptions = {}) => {
options.openExternal = options.openExternal || true
const classes = buildClasses(options)
const md = MarkdownIt({
html: true,
linkify: true,
highlight (str) {
return `<pre class="${classes.pre.join(' ')}"><code>${str}</code></pre>`
},
})
md.use(MarkdownItClass, classes)
if (options.openExternal) {
const open = useExternalLink()
whenever(target, () => {
useEventListener(target, 'click', (e: MouseEvent) => {
const link = (e.target as HTMLElement).closest('a[href]')
if (!link) {
return
}
e.preventDefault()
const url = link.getAttribute('href')
if (url) {
open(url)
}
})
})
}
return {
markdown: computed(() => md.render(unref(text), { sanitize: true })),
}
}