Skip to content

Commit 9f5bbbc

Browse files
committed
feat(web): use Markdown helper class to interact with markdown
1 parent 80743c3 commit 9f5bbbc

File tree

3 files changed

+69
-31
lines changed

3 files changed

+69
-31
lines changed

web/components/Notifications/Item.vue

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,19 @@ import {
1010
import { useMutation } from '@vue/apollo-composable';
1111
import type { NotificationFragmentFragment } from '~/composables/gql/graphql';
1212
import { NotificationType } from '~/composables/gql/graphql';
13-
import { safeParseMarkdown } from '~/helpers/markdown';
1413
import {
1514
archiveNotification as archiveMutation,
1615
deleteNotification as deleteMutation,
1716
} from './graphql/notification.query';
17+
import { Markdown } from '@/helpers/markdown';
1818
1919
const props = defineProps<NotificationFragmentFragment>();
2020
2121
const descriptionMarkup = computedAsync(async () => {
2222
try {
23-
return await safeParseMarkdown(props.description);
23+
return await Markdown.parse(props.description);
2424
} catch (e) {
25+
console.error(e)
2526
return props.description;
2627
}
2728
}, '');

web/helpers/markdown.ts

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,41 @@
11
import DOMPurify from 'dompurify';
2-
import { marked } from 'marked';
2+
import { Marked, type MarkedExtension } from 'marked';
3+
4+
const defaultMarkedExtension: MarkedExtension = {
5+
hooks: {
6+
// must define as a function (instead of a lambda) to preserve/reflect bindings downstream
7+
postprocess(html) {
8+
return DOMPurify.sanitize(html);
9+
},
10+
},
11+
};
312

413
/**
5-
* Parses arbitrary markdown content as sanitized html. May throw if parsing fails.
6-
*
7-
* @param markdownContent string of markdown content
8-
* @returns safe, sanitized html content
14+
* Helper class to build or conveniently use a markdown parser.
915
*/
10-
export async function safeParseMarkdown(markdownContent: string) {
11-
const parsed = await marked.parse(markdownContent);
12-
return DOMPurify.sanitize(parsed);
16+
export class Markdown {
17+
private static instance = Markdown.create();
18+
19+
/**
20+
* Creates a `Marked` instance with default MarkedExtension's already added.
21+
*
22+
* Default behaviors:
23+
* - Sanitizes html after parsing
24+
*
25+
* @param args any number of Marked Extensions
26+
* @returns Marked parser instance
27+
*/
28+
static create(...args: Parameters<Marked['use']>) {
29+
return new Marked(defaultMarkedExtension, ...args);
30+
}
31+
32+
/**
33+
* Parses arbitrary markdown content as sanitized html. May throw if parsing fails.
34+
*
35+
* @param markdownContent string of markdown content
36+
* @returns safe, sanitized html content
37+
*/
38+
static async parse(markdownContent: string): Promise<string> {
39+
return Markdown.instance.parse(markdownContent);
40+
}
1341
}

web/store/updateOsChangelog.ts

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
1-
import { marked } from 'marked';
2-
import { baseUrl } from 'marked-base-url';
3-
import { defineStore } from 'pinia';
4-
import prerelease from 'semver/functions/prerelease';
5-
import { computed, ref, watch } from 'vue';
6-
7-
import { DOCS_RELEASE_NOTES } from '~/helpers/urls';
1+
import { Markdown } from '@/helpers/markdown';
82
import { request } from '~/composables/services/request';
3+
import { DOCS_RELEASE_NOTES } from '~/helpers/urls';
94
import { useCallbackStore } from '~/store/callbackActions';
105
// import { useServerStore } from '~/store/server';
116
import type { ServerUpdateOsResponse } from '~/types/server';
12-
import { safeParseMarkdown } from '~/helpers/markdown';
7+
import { Marked } from 'marked';
8+
import { baseUrl } from 'marked-base-url';
9+
import { defineStore } from 'pinia';
10+
import prerelease from 'semver/functions/prerelease';
11+
import { computed, ref, watch } from 'vue';
1312

1413
export const useUpdateOsChangelogStore = defineStore('updateOsChangelog', () => {
1514
const callbackStore = useCallbackStore();
@@ -30,10 +29,15 @@ export const useUpdateOsChangelogStore = defineStore('updateOsChangelog', () =>
3029
if (!releaseForUpdate.value || !releaseForUpdate.value?.changelog) {
3130
return '';
3231
}
33-
return releaseForUpdate.value?.changelog ?? `https://raw.githubusercontent.com/unraid/docs/main/docs/unraid-os/release-notes/${releaseForUpdate.value.version}.md`;
32+
return (
33+
releaseForUpdate.value?.changelog ??
34+
`https://raw.githubusercontent.com/unraid/docs/main/docs/unraid-os/release-notes/${releaseForUpdate.value.version}.md`
35+
);
3436
});
3537

36-
const isReleaseForUpdateStable = computed(() => releaseForUpdate.value ? prerelease(releaseForUpdate.value.version) === null : false);
38+
const isReleaseForUpdateStable = computed(() =>
39+
releaseForUpdate.value ? prerelease(releaseForUpdate.value.version) === null : false
40+
);
3741
const parsedChangelog = ref<string>('');
3842
const parseChangelogFailed = ref<string>('');
3943
// used to remove the first <h1></h1> and it's contents from the parsedChangelog
@@ -49,7 +53,10 @@ export const useUpdateOsChangelogStore = defineStore('updateOsChangelog', () =>
4953
return parseChangelogFailed.value;
5054
}
5155
if (parsedChangelog.value) {
52-
return parsedChangelog.value.match(/<h1>(.*?)<\/h1>/)?.[1] ?? `Version ${releaseForUpdate.value?.version} ${releaseForUpdate.value?.date}`;
56+
return (
57+
parsedChangelog.value.match(/<h1>(.*?)<\/h1>/)?.[1] ??
58+
`Version ${releaseForUpdate.value?.version} ${releaseForUpdate.value?.date}`
59+
);
5360
}
5461
return '';
5562
});
@@ -72,28 +79,28 @@ export const useUpdateOsChangelogStore = defineStore('updateOsChangelog', () =>
7279
.text();
7380

7481
// set base url for relative links
75-
marked.use(baseUrl(DOCS_RELEASE_NOTES.toString()));
82+
const marked = Markdown.create(baseUrl(DOCS_RELEASE_NOTES.toString()));
7683

7784
// open links in new tab & replace .md from links
7885
const renderer = new marked.Renderer();
7986
const anchorRender = {
8087
options: {
8188
sanitize: true,
8289
},
83-
render: marked.Renderer.prototype.link
90+
render: marked.Renderer.prototype.link,
8491
};
8592
renderer.link = function (href, title, text) {
8693
const anchor = anchorRender.render(href, title, text);
8794
return anchor
88-
.replace('<a', '<a target=\'_blank\' ') // open links in new tab
95+
.replace('<a', "<a target='_blank' ") // open links in new tab
8996
.replace('.md', ''); // remove .md from links
9097
};
9198

9299
marked.setOptions({
93-
renderer
100+
renderer,
94101
});
95102

96-
parsedChangelog.value = await safeParseMarkdown(changelogMarkdownRaw);
103+
parsedChangelog.value = await marked.parse(changelogMarkdownRaw);
97104
} catch (error: unknown) {
98105
const caughtError = error as Error;
99106
parseChangelogFailed.value =
@@ -106,12 +113,14 @@ export const useUpdateOsChangelogStore = defineStore('updateOsChangelog', () =>
106113
const fetchAndConfirmInstall = (sha256: string) => {
107114
callbackStore.send(
108115
window.location.href,
109-
[{
110-
sha256,
111-
type: 'updateOs',
112-
}],
116+
[
117+
{
118+
sha256,
119+
type: 'updateOs',
120+
},
121+
],
113122
undefined,
114-
'forUpc',
123+
'forUpc'
115124
);
116125
};
117126

0 commit comments

Comments
 (0)