From 2f4ff21986c1467ad839014569de47502a9947bd Mon Sep 17 00:00:00 2001 From: Pujit Mehrotra Date: Tue, 19 Nov 2024 09:59:39 -0500 Subject: [PATCH] feat(web): use Markdown helper class to interact with markdown --- web/components/Notifications/Item.vue | 5 +-- web/helpers/markdown.ts | 44 ++++++++++++++++++----- web/store/updateOsChangelog.ts | 51 ++++++++++++++++----------- 3 files changed, 69 insertions(+), 31 deletions(-) diff --git a/web/components/Notifications/Item.vue b/web/components/Notifications/Item.vue index f4293a470..211327bd2 100644 --- a/web/components/Notifications/Item.vue +++ b/web/components/Notifications/Item.vue @@ -10,18 +10,19 @@ import { import { useMutation } from '@vue/apollo-composable'; import type { NotificationFragmentFragment } from '~/composables/gql/graphql'; import { NotificationType } from '~/composables/gql/graphql'; -import { safeParseMarkdown } from '~/helpers/markdown'; import { archiveNotification as archiveMutation, deleteNotification as deleteMutation, } from './graphql/notification.query'; +import { Markdown } from '@/helpers/markdown'; const props = defineProps(); const descriptionMarkup = computedAsync(async () => { try { - return await safeParseMarkdown(props.description); + return await Markdown.parse(props.description); } catch (e) { + console.error(e) return props.description; } }, ''); diff --git a/web/helpers/markdown.ts b/web/helpers/markdown.ts index be804b226..495599a74 100644 --- a/web/helpers/markdown.ts +++ b/web/helpers/markdown.ts @@ -1,13 +1,41 @@ import DOMPurify from 'dompurify'; -import { marked } from 'marked'; +import { Marked, type MarkedExtension } from 'marked'; + +const defaultMarkedExtension: MarkedExtension = { + hooks: { + // must define as a function (instead of a lambda) to preserve/reflect bindings downstream + postprocess(html) { + return DOMPurify.sanitize(html); + }, + }, +}; /** - * Parses arbitrary markdown content as sanitized html. May throw if parsing fails. - * - * @param markdownContent string of markdown content - * @returns safe, sanitized html content + * Helper class to build or conveniently use a markdown parser. */ -export async function safeParseMarkdown(markdownContent: string) { - const parsed = await marked.parse(markdownContent); - return DOMPurify.sanitize(parsed); +export class Markdown { + private static instance = Markdown.create(); + + /** + * Creates a `Marked` instance with default MarkedExtension's already added. + * + * Default behaviors: + * - Sanitizes html after parsing + * + * @param args any number of Marked Extensions + * @returns Marked parser instance + */ + static create(...args: Parameters) { + return new Marked(defaultMarkedExtension, ...args); + } + + /** + * Parses arbitrary markdown content as sanitized html. May throw if parsing fails. + * + * @param markdownContent string of markdown content + * @returns safe, sanitized html content + */ + static async parse(markdownContent: string): Promise { + return Markdown.instance.parse(markdownContent); + } } diff --git a/web/store/updateOsChangelog.ts b/web/store/updateOsChangelog.ts index 950bae81c..948721ea1 100644 --- a/web/store/updateOsChangelog.ts +++ b/web/store/updateOsChangelog.ts @@ -1,16 +1,15 @@ -import { marked } from 'marked'; +import { Markdown } from '@/helpers/markdown'; +import { request } from '~/composables/services/request'; +import { DOCS_RELEASE_NOTES } from '~/helpers/urls'; +import { useCallbackStore } from '~/store/callbackActions'; +// import { useServerStore } from '~/store/server'; +import type { ServerUpdateOsResponse } from '~/types/server'; +import { Marked } from 'marked'; import { baseUrl } from 'marked-base-url'; import { defineStore } from 'pinia'; import prerelease from 'semver/functions/prerelease'; import { computed, ref, watch } from 'vue'; -import { DOCS_RELEASE_NOTES } from '~/helpers/urls'; -import { request } from '~/composables/services/request'; -import { useCallbackStore } from '~/store/callbackActions'; -// import { useServerStore } from '~/store/server'; -import type { ServerUpdateOsResponse } from '~/types/server'; -import { safeParseMarkdown } from '~/helpers/markdown'; - export const useUpdateOsChangelogStore = defineStore('updateOsChangelog', () => { const callbackStore = useCallbackStore(); // const serverStore = useServerStore(); @@ -30,10 +29,15 @@ export const useUpdateOsChangelogStore = defineStore('updateOsChangelog', () => if (!releaseForUpdate.value || !releaseForUpdate.value?.changelog) { return ''; } - return releaseForUpdate.value?.changelog ?? `https://raw.githubusercontent.com/unraid/docs/main/docs/unraid-os/release-notes/${releaseForUpdate.value.version}.md`; + return ( + releaseForUpdate.value?.changelog ?? + `https://raw.githubusercontent.com/unraid/docs/main/docs/unraid-os/release-notes/${releaseForUpdate.value.version}.md` + ); }); - const isReleaseForUpdateStable = computed(() => releaseForUpdate.value ? prerelease(releaseForUpdate.value.version) === null : false); + const isReleaseForUpdateStable = computed(() => + releaseForUpdate.value ? prerelease(releaseForUpdate.value.version) === null : false + ); const parsedChangelog = ref(''); const parseChangelogFailed = ref(''); // used to remove the first

and it's contents from the parsedChangelog @@ -49,7 +53,10 @@ export const useUpdateOsChangelogStore = defineStore('updateOsChangelog', () => return parseChangelogFailed.value; } if (parsedChangelog.value) { - return parsedChangelog.value.match(/

(.*?)<\/h1>/)?.[1] ?? `Version ${releaseForUpdate.value?.version} ${releaseForUpdate.value?.date}`; + return ( + parsedChangelog.value.match(/

(.*?)<\/h1>/)?.[1] ?? + `Version ${releaseForUpdate.value?.version} ${releaseForUpdate.value?.date}` + ); } return ''; }); @@ -72,7 +79,7 @@ export const useUpdateOsChangelogStore = defineStore('updateOsChangelog', () => .text(); // set base url for relative links - marked.use(baseUrl(DOCS_RELEASE_NOTES.toString())); + const marked = Markdown.create(baseUrl(DOCS_RELEASE_NOTES.toString())); // open links in new tab & replace .md from links const renderer = new marked.Renderer(); @@ -80,20 +87,20 @@ export const useUpdateOsChangelogStore = defineStore('updateOsChangelog', () => options: { sanitize: true, }, - render: marked.Renderer.prototype.link + render: marked.Renderer.prototype.link, }; renderer.link = function (href, title, text) { const anchor = anchorRender.render(href, title, text); return anchor - .replace(' const fetchAndConfirmInstall = (sha256: string) => { callbackStore.send( window.location.href, - [{ - sha256, - type: 'updateOs', - }], + [ + { + sha256, + type: 'updateOs', + }, + ], undefined, - 'forUpc', + 'forUpc' ); };