mirror of
https://github.com/unraid/api.git
synced 2025-12-31 13:39:52 -06:00
feat(web): use Markdown helper class to interact with markdown
This commit is contained in:
@@ -10,18 +10,19 @@ import {
|
|||||||
import { useMutation } from '@vue/apollo-composable';
|
import { useMutation } from '@vue/apollo-composable';
|
||||||
import type { NotificationFragmentFragment } from '~/composables/gql/graphql';
|
import type { NotificationFragmentFragment } from '~/composables/gql/graphql';
|
||||||
import { NotificationType } from '~/composables/gql/graphql';
|
import { NotificationType } from '~/composables/gql/graphql';
|
||||||
import { safeParseMarkdown } from '~/helpers/markdown';
|
|
||||||
import {
|
import {
|
||||||
archiveNotification as archiveMutation,
|
archiveNotification as archiveMutation,
|
||||||
deleteNotification as deleteMutation,
|
deleteNotification as deleteMutation,
|
||||||
} from './graphql/notification.query';
|
} from './graphql/notification.query';
|
||||||
|
import { Markdown } from '@/helpers/markdown';
|
||||||
|
|
||||||
const props = defineProps<NotificationFragmentFragment>();
|
const props = defineProps<NotificationFragmentFragment>();
|
||||||
|
|
||||||
const descriptionMarkup = computedAsync(async () => {
|
const descriptionMarkup = computedAsync(async () => {
|
||||||
try {
|
try {
|
||||||
return await safeParseMarkdown(props.description);
|
return await Markdown.parse(props.description);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
return props.description;
|
return props.description;
|
||||||
}
|
}
|
||||||
}, '');
|
}, '');
|
||||||
|
|||||||
@@ -1,13 +1,41 @@
|
|||||||
import DOMPurify from 'dompurify';
|
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.
|
* Helper class to build or conveniently use a markdown parser.
|
||||||
*
|
|
||||||
* @param markdownContent string of markdown content
|
|
||||||
* @returns safe, sanitized html content
|
|
||||||
*/
|
*/
|
||||||
export async function safeParseMarkdown(markdownContent: string) {
|
export class Markdown {
|
||||||
const parsed = await marked.parse(markdownContent);
|
private static instance = Markdown.create();
|
||||||
return DOMPurify.sanitize(parsed);
|
|
||||||
|
/**
|
||||||
|
* 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<Marked['use']>) {
|
||||||
|
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<string> {
|
||||||
|
return Markdown.instance.parse(markdownContent);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { baseUrl } from 'marked-base-url';
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import prerelease from 'semver/functions/prerelease';
|
import prerelease from 'semver/functions/prerelease';
|
||||||
import { computed, ref, watch } from 'vue';
|
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', () => {
|
export const useUpdateOsChangelogStore = defineStore('updateOsChangelog', () => {
|
||||||
const callbackStore = useCallbackStore();
|
const callbackStore = useCallbackStore();
|
||||||
// const serverStore = useServerStore();
|
// const serverStore = useServerStore();
|
||||||
@@ -30,10 +29,15 @@ export const useUpdateOsChangelogStore = defineStore('updateOsChangelog', () =>
|
|||||||
if (!releaseForUpdate.value || !releaseForUpdate.value?.changelog) {
|
if (!releaseForUpdate.value || !releaseForUpdate.value?.changelog) {
|
||||||
return '';
|
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<string>('');
|
const parsedChangelog = ref<string>('');
|
||||||
const parseChangelogFailed = ref<string>('');
|
const parseChangelogFailed = ref<string>('');
|
||||||
// used to remove the first <h1></h1> and it's contents from the parsedChangelog
|
// used to remove the first <h1></h1> and it's contents from the parsedChangelog
|
||||||
@@ -49,7 +53,10 @@ export const useUpdateOsChangelogStore = defineStore('updateOsChangelog', () =>
|
|||||||
return parseChangelogFailed.value;
|
return parseChangelogFailed.value;
|
||||||
}
|
}
|
||||||
if (parsedChangelog.value) {
|
if (parsedChangelog.value) {
|
||||||
return parsedChangelog.value.match(/<h1>(.*?)<\/h1>/)?.[1] ?? `Version ${releaseForUpdate.value?.version} ${releaseForUpdate.value?.date}`;
|
return (
|
||||||
|
parsedChangelog.value.match(/<h1>(.*?)<\/h1>/)?.[1] ??
|
||||||
|
`Version ${releaseForUpdate.value?.version} ${releaseForUpdate.value?.date}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
});
|
});
|
||||||
@@ -72,7 +79,7 @@ export const useUpdateOsChangelogStore = defineStore('updateOsChangelog', () =>
|
|||||||
.text();
|
.text();
|
||||||
|
|
||||||
// set base url for relative links
|
// 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
|
// open links in new tab & replace .md from links
|
||||||
const renderer = new marked.Renderer();
|
const renderer = new marked.Renderer();
|
||||||
@@ -80,20 +87,20 @@ export const useUpdateOsChangelogStore = defineStore('updateOsChangelog', () =>
|
|||||||
options: {
|
options: {
|
||||||
sanitize: true,
|
sanitize: true,
|
||||||
},
|
},
|
||||||
render: marked.Renderer.prototype.link
|
render: marked.Renderer.prototype.link,
|
||||||
};
|
};
|
||||||
renderer.link = function (href, title, text) {
|
renderer.link = function (href, title, text) {
|
||||||
const anchor = anchorRender.render(href, title, text);
|
const anchor = anchorRender.render(href, title, text);
|
||||||
return anchor
|
return anchor
|
||||||
.replace('<a', '<a target=\'_blank\' ') // open links in new tab
|
.replace('<a', "<a target='_blank' ") // open links in new tab
|
||||||
.replace('.md', ''); // remove .md from links
|
.replace('.md', ''); // remove .md from links
|
||||||
};
|
};
|
||||||
|
|
||||||
marked.setOptions({
|
marked.setOptions({
|
||||||
renderer
|
renderer,
|
||||||
});
|
});
|
||||||
|
|
||||||
parsedChangelog.value = await safeParseMarkdown(changelogMarkdownRaw);
|
parsedChangelog.value = await marked.parse(changelogMarkdownRaw);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const caughtError = error as Error;
|
const caughtError = error as Error;
|
||||||
parseChangelogFailed.value =
|
parseChangelogFailed.value =
|
||||||
@@ -106,12 +113,14 @@ export const useUpdateOsChangelogStore = defineStore('updateOsChangelog', () =>
|
|||||||
const fetchAndConfirmInstall = (sha256: string) => {
|
const fetchAndConfirmInstall = (sha256: string) => {
|
||||||
callbackStore.send(
|
callbackStore.send(
|
||||||
window.location.href,
|
window.location.href,
|
||||||
[{
|
[
|
||||||
sha256,
|
{
|
||||||
type: 'updateOs',
|
sha256,
|
||||||
}],
|
type: 'updateOs',
|
||||||
|
},
|
||||||
|
],
|
||||||
undefined,
|
undefined,
|
||||||
'forUpc',
|
'forUpc'
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user