-
Notifications
You must be signed in to change notification settings - Fork 27
/
Copy pathPageSnippet.vue
151 lines (131 loc) · 5.07 KB
/
PageSnippet.vue
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
<template>
<Suspense suspensible>
<UAlert
color="primary"
variant="subtle"
class="page-snippet-alert"
>
<template #description>
<MDCRenderer
v-if="!!snippetName && ast && ast.body && !snippetError"
:body="ast.body"
:data="ast.data"
:data-testid="!!snippetName ? snippetName : undefined"
/>
</template>
</UAlert>
</Suspense>
</template>
<script setup lang="ts">
/**
* This component is exposed as `snippet` in MDC markdown files
*/
import { parseMarkdown } from '@nuxtjs/mdc/runtime'
import type { MDCParserResult, MDCRoot } from '@nuxtjs/mdc'
import { computed, provide, inject, useState, useId, serveCachedData, useAsyncData, useFetch } from '#imports'
const props = defineProps({
/** The snippet name */
name: {
type: String,
/*
* We set the prop to not required (even though it is required to render the snippet)
* so that the logic of parsing recursive snippets can pass
* an empty `name` and avoid warnings.
*/
required: false,
default: ''
}
})
// Determine the snippet name
const snippetName = computed((): string => String(props.name || '').trim() || '')
// Provide/inject parent nodes to filter out
const SNIPPET_INJECTION_KEY = 'snippet-parent-nodes'
const componentId = useId()
const snippetParentNodes = useState(`page-snippet-${componentId}`, () => new Set([...inject<Set<string>>(SNIPPET_INJECTION_KEY, new Set()), snippetName.value].filter(item => item.trim() !== '')))
provide(SNIPPET_INJECTION_KEY, new Set([...snippetParentNodes.value].filter(item => item.trim() !== '')))
const fetchKey = computed((): string => `portal-snippet-${(snippetName.value || componentId || '').replace(/\//g, '-')}`)
const { transform, getCachedData } = serveCachedData()
const { data: snippetData, error: snippetError } = await useFetch('/api/markdown', {
query: {
name: 'snippet'
},
// Reuse the same key to avoid re-fetching the document, e.g. `portal-page-about`
key: fetchKey.value,
dedupe: 'defer',
immediate: !!snippetName.value, // Do not immediately fetch in case there's no snippet name
retry: false, // Do not retry on 404 in case the snippet doesn't exist
transform,
getCachedData
})
const nodes = [...snippetParentNodes.value].join('|')
// Strip out recursive snippet(s), looking for `name` prop in the content
const snippetRegex = new RegExp(`(name)(=|:)( )?('|"|\`)?('|"|\`)?(${nodes})('|"|\`)?('|"|\`)?`, 'i')
// Important: Replace $6 with an empty string to remove the snippet name from the content; must be a space to avoid evaluating as a boolean prop.
const sanitizedData = String(snippetData.value?.content || '')?.replace(snippetRegex, `$1$2$3$4$5${String(' ')}$7`) || ''
const removeInvalidSnippets = (obj: Record<string, any>): Record<string, any> | null => {
if (Array.isArray(obj)) {
// Recursively handle arrays by filtering out invalid objects and processing children
return obj
.map(item => removeInvalidSnippets(item))
.filter(item => item !== null)
} else if (typeof obj === 'object' && obj !== null) {
// Check if the object has 'tag' property and the required 'props.name' condition
if (
obj.tag
// If the tag matches `snippet` or `page-snippet` (this component)
&& obj.tag === 'page-snippet'
&& obj.props
&& typeof obj.props.name === 'string'
// If name is empty, or includes a parent snippet with the same name
&& (obj.props.name.trim() === '' || snippetParentNodes.value.has(obj.props.name))
) {
// Remove the object if it matches the criteria
return null
}
// Recursively process each key of the object
const newObj: Record<string, any> = {}
for (const key in obj) {
// Using the 'in' operator to check for the existence of the property
if (key in obj) {
const result = removeInvalidSnippets(obj[key])
if (result !== null) {
newObj[key] = result
}
}
}
return newObj
}
// Return other types (strings, numbers, etc.) as-is
return obj
}
const { data: ast } = await useAsyncData(`parsed-${fetchKey.value}`, async (): Promise<MDCParserResult> => {
const parsed = await parseMarkdown(sanitizedData)
// Extract the `body` and destructure the rest of the document
const { body, ...parsedDoc } = parsed
// Important: Remove invalid snippets from the AST
const processedBody = removeInvalidSnippets(body)
// Return the MDCParserResult with the sanitized body
return {
...parsedDoc,
body: processedBody as MDCRoot
}
}, {
// Only parse if there is content and no error. Must be true to allow for async rendering
immediate: !!snippetName.value && (!!sanitizedData && !!snippetData.value?.content) && !snippetError.value, // Do not immediately process the snippet data
dedupe: 'defer',
deep: false,
transform,
getCachedData
})
if (snippetName.value && snippetError.value) {
console.error(`snippet(${snippetName.value}) `, 'could not render snippet', snippetError.value)
}
</script>
<style scoped>
.page-snippet-alert {
margin: 8px 0;
:deep(p) {
margin: 0;
}
}
</style>