Skip to content

Commit

Permalink
Merge branch 'mde-preview-worker' into 'main'
Browse files Browse the repository at this point in the history
Mde preview worker

See merge request reportcreator/reportcreator!817
  • Loading branch information
MWedl committed Jan 14, 2025
2 parents 33e86ed + 6148635 commit 99011b1
Show file tree
Hide file tree
Showing 29 changed files with 162 additions and 143 deletions.
3 changes: 3 additions & 0 deletions packages/frontend/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ export default defineNuxtConfig({
port: 3000,
},
vite: {
resolve: {
conditions: ['module', 'worker', 'browser', 'development|production'],
},
optimizeDeps: {
include: ['vuedraggable', 'monaco-editor', '@github/webauthn-json/browser-ponyfill'],
},
Expand Down
7 changes: 0 additions & 7 deletions packages/frontend/src/components/Comment/Sidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -257,10 +257,3 @@ defineExpose({
min-height: 0;
}
</style>

<!-- TODO: comment improvements
* [x] change icon for resolve: mdi-check-marked-circle
* [x] allow un-resolving comments
* [x] save comment/answer with Ctrl+Enter
* [x] add comment in markdown for selected text with Ctrl+Alt+M
-->
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
v-model="form"
:lang="item.context.projectType.language"
:upload-file="uploadFile"
:rewrite-file-url="rewriteFileUrl"
:disabled="disabled"
:rewrite-file-url-map="props.rewriteFileUrlMap"
:disabled="props.disabled"
/>
</v-card-text>
<v-card-actions>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ const emit = defineEmits<{
const localSettings = useLocalSettings();
const markdownProps = computed(() => ({
...pick(props, ['disabled', 'lang', 'uploadFile', 'rewriteFileUrl', 'rewriteReferenceLink']),
...pick(props, ['disabled', 'lang', 'uploadFile', 'rewriteFileUrlMap', 'referenceItems']),
spellcheckEnabled: localSettings.designSpellcheckEnabled,
'onUpdate:spellcheckEnabled': (value: boolean) => { localSettings.designSpellcheckEnabled = value },
markdownEditorMode: localSettings.designMarkdownEditorMode,
Expand Down
4 changes: 2 additions & 2 deletions packages/frontend/src/components/Design/LayoutEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,8 @@ const markdownProps = computed(() => ({
disabled: props.disabled,
lang: props.lang || props.projectType.language,
uploadFile: props.uploadFile,
rewriteFileUrl: props.rewriteFileUrl,
rewriteReferenceLink: props.rewriteReferenceLink,
rewriteFileUrlMap: props.rewriteFileUrlMap,
referenceItems: props.referenceItems,
}));
const isVisible = ref(true);
Expand Down
2 changes: 1 addition & 1 deletion packages/frontend/src/components/Design/LayoutTree.vue
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ const emit = defineEmits<{
jumpToCode: [{ tab: PdfDesignerTab, position: DocumentSelectionPosition }];
}>();
const markdownProps = computed(() => pick(props, ['disabled', 'lang', 'uploadFile', 'rewriteFileUrl', 'rewriteReferenceLink']));
const markdownProps = computed(() => pick(props, ['disabled', 'lang', 'uploadFile', 'rewriteFileUrlMap', 'referenceItems']));
function onChange(e: any) {
if (e.moved) {
Expand Down
4 changes: 2 additions & 2 deletions packages/frontend/src/components/Design/PreviewDataForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ const props = defineProps<{
projectType: ProjectType;
readonly?: boolean;
uploadFile?: (file: File) => Promise<string>;
rewriteFileUrl?: (fileSrc: string) => string;
rewriteFileUrlMap?: Record<string, string>;
}>();
const emit = defineEmits<{
'update:modelValue': [any];
Expand Down Expand Up @@ -260,7 +260,7 @@ function riskLevel(finding: any) {
const fieldAttrs = computed(() => ({
showFieldIds: true,
uploadFile: props.uploadFile,
rewriteFileUrl: props.rewriteFileUrl,
rewriteFileUrlMap: props.rewriteFileUrlMap,
selectableUsers: [auth.user.value!],
lang: props.projectType.language,
readonly: props.readonly,
Expand Down
2 changes: 1 addition & 1 deletion packages/frontend/src/components/DynamicInputField.vue
Original file line number Diff line number Diff line change
Expand Up @@ -527,7 +527,7 @@ const fieldAttrs = computed(() => ({
label: label.value,
...pick(props, [
'disabled', 'readonly', 'autofocus', 'lang', 'spellcheckEnabled', 'markdownEditorMode',
'referenceItems', 'uploadFile', 'rewriteFileUrl', 'rewriteReferenceLink'
'referenceItems', 'rewriteFileUrlMap', 'uploadFile',
]),
onCollab: (v: any) => emit('collab', v),
onComment: (v: any) => emit('comment', v),
Expand Down
3 changes: 1 addition & 2 deletions packages/frontend/src/components/DynamicInputFieldDiff.vue
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,7 @@ const attrs = useAttrs();
const inheritedDiffAttrs = computed(() => {
const copyFields = [
'disabled', 'readonly', 'lang', 'spellcheckEnabled', 'markdownEditorMode',
'uploadFile', 'rewriteFileUrl', 'rewriteReferenceLink',
'selectableUsers', 'referenceItems',
'uploadFile', 'selectableUsers', 'referenceItems', 'rewriteFileUrlMap',
'fieldValueSuggestions',
'onUpdate:markdownEditorMode', 'onUpdate:spellcheckEnabled',
'collab', 'onCollab', 'onComment',
Expand Down
29 changes: 11 additions & 18 deletions packages/frontend/src/components/Markdown/Preview.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@
</template>

<script lang="ts">
import { renderMarkdownToHtml, mermaid } from '@sysreptor/markdown';
import { mermaid } from '@sysreptor/markdown';
import { uuidv4 } from "@base/utils/helpers";
import { absoluteApiUrl } from '#imports';
import { renderMarkdownToHtmlInWorker, type ReferenceItem } from '~/composables/markdown';
import 'highlight.js/styles/default.css';
mermaid.initialize({
startOnLoad: false,
Expand All @@ -22,35 +23,27 @@ mermaid.initialize({
<script setup lang="ts">
const props = defineProps<{
value?: string|null;
rewriteFileUrl?: (fileSrc: string) => string;
rewriteReferenceLink?: (src: string) => {href: string, title: string}|null;
rewriteFileUrlMap?: Record<string, string>;
referenceItems?: ReferenceItem[];
cacheBuster?: string;
}>();
const cacheBusterFallback = uuidv4();
const cacheBuster = computed(() => props.cacheBuster || cacheBusterFallback);
const renderedMarkdown = ref('');
const renderedMarkdownText = ref('');
watchThrottled(() => props.value, () => {
watchThrottled(() => props.value, async () => {
const mdText = props.value || '';
renderedMarkdown.value = renderMarkdownToHtml(mdText, {
renderedMarkdown.value = await renderMarkdownToHtmlInWorker({
text: mdText,
preview: true,
rewriteFileSource,
rewriteReferenceLink: props.rewriteReferenceLink,
referenceItems: props.referenceItems,
rewriteFileUrlMap: props.rewriteFileUrlMap,
cacheBuster: cacheBuster.value,
});
renderedMarkdownText.value = mdText;
}, { throttle: 500, leading: true, immediate: true });
function rewriteFileSource(imgSrc: string) {
// Rewrite image source to handle image fetching from markdown.
// Images in markdown are referenced with a URL relative to the parent resource (e.g. "/images/name/image.png").
if (!props.rewriteFileUrl || !imgSrc.startsWith('/')) {
return imgSrc;
}
return absoluteApiUrl(props.rewriteFileUrl(`${imgSrc}?c=${cacheBuster.value}`));
}
const previewRef = ref<HTMLDivElement>();
async function postProcessRenderedHtml() {
// Prevent navigation when clicking on anchor links in preview
Expand Down
6 changes: 3 additions & 3 deletions packages/frontend/src/components/Template/Editor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -135,15 +135,15 @@ const props = withDefaults(defineProps<{
readonly?: boolean;
toolbarAttrs?: object;
uploadFile?: (file: File) => Promise<string>;
rewriteFileUrl?: (fileSrc: string) => string;
rewriteFileUrlMap?: Record<string, string>;
history?: boolean;
}>(), {
initialLanguage: null,
fieldDefinitionList: undefined,
toolbarAttrs: () => ({}),
uploadFile: undefined,
rewriteFileUrl: undefined,
rewriteFileUrlMap: undefined,
readonly: false,
history: false,
});
Expand Down Expand Up @@ -253,7 +253,7 @@ const fieldAttrs = computed(() => (translation: FindingTemplateTranslation, defi
markdownEditorMode: localSettings.templateMarkdownEditorMode,
'onUpdate:markdownEditorMode': (value: MarkdownEditorMode) => { localSettings.templateMarkdownEditorMode = value },
uploadFile: props.uploadFile,
rewriteFileUrl: props.rewriteFileUrl,
rewriteFileUrlMap: props.rewriteFileUrlMap,
}))
defineExpose({
Expand Down
8 changes: 4 additions & 4 deletions packages/frontend/src/components/Template/EditorDiff.vue
Original file line number Diff line number Diff line change
Expand Up @@ -106,11 +106,11 @@ import { MarkdownEditorMode } from "#imports";
const props = defineProps<{
historic: {
value: FindingTemplate;
rewriteFileUrl?: (fileSrc: string) => string;
rewriteFileUrlMap?: Record<string, string>;
},
current: {
value: FindingTemplate;
rewriteFileUrl?: (fileSrc: string) => string;
rewriteFileUrlMap?: Record<string, string>;
}
initialLanguage?: string|null;
toolbarAttrs?: object;
Expand Down Expand Up @@ -182,7 +182,7 @@ const diffFieldAttrs = computed(() => (translationInfo: TranslationDiffInfo, def
mainTranslation.value.data[definition.id]) :
null,
lang: translationInfo.historic?.language,
rewriteFileUrl: props.historic.rewriteFileUrl,
rewriteFileUrlMap: props.historic.rewriteFileUrlMap,
...commonProps
},
current: {
Expand All @@ -192,7 +192,7 @@ const diffFieldAttrs = computed(() => (translationInfo: TranslationDiffInfo, def
mainTranslationCurrent!.data[definition.id]) :
null,
lang: translationInfo.current?.language,
rewriteFileUrl: props.current.rewriteFileUrl,
rewriteFileUrlMap: props.current.rewriteFileUrlMap,
...commonProps,
},
};
Expand Down
4 changes: 2 additions & 2 deletions packages/frontend/src/composables/history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export function useMarkdownDiff(options: {
modelValue: options.props.value.current.value,
...pick(options.props.value.current, [
'collab', 'lang', 'readonly', 'disabled', 'markdownEditorMode', 'spellcheckSupported', 'spellcheckEnabled',
'referenceItems', 'uploadFile', 'rewriteFileUrl', 'rewriteReferenceLink',
'referenceItems', 'rewriteFileUrlMap', 'uploadFile',
]),
})),
emit: (name: string, value: any) => {
Expand All @@ -61,7 +61,7 @@ export function useMarkdownDiff(options: {
props: computed(() => ({
modelValue: options.props.value.historic.value,
readonly: true,
...pick(options.props.value.historic, ['lang', 'markdownEditorMode', 'rewriteFileUrl', 'rewriteReferenceLink']),
...pick(options.props.value.historic, ['lang', 'markdownEditorMode', 'referenceItems', 'rewriteFileUrlMap']),
})),
emit: (name: string, value: any) => {
const emit = (options.props.value.historic as any)['on' + name.charAt(0).toUpperCase() + name.slice(1)];
Expand Down
32 changes: 11 additions & 21 deletions packages/frontend/src/composables/lockedit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
type MarkdownEditorMode,
type PentestProject,
type ProjectType,
type ReferenceItem,
type UploadedFileInfo,
} from "#imports";

Expand Down Expand Up @@ -134,34 +135,24 @@ export function useProjectEditBase(options: {
return `[${res.name}](/files/name/${res.name})`;
}
}
function rewriteFileUrl(fileSrc: string) {
if (fileSrc.startsWith('/assets/')) {
return urlJoin(projectTypeUrl.value || '', fileSrc)
} else {
return urlJoin(projectUrl.value, fileSrc);
}
}

const referenceItems = computed(() => {
const rewriteFileUrlMap = computed(() => ({
'/assets/': urlJoin(projectTypeUrl.value || '', '/assets/'),
'/images/': urlJoin(projectUrl.value, '/images/'),
'/files/': urlJoin(projectUrl.value, '/files/'),
}));
const referenceItems = computed<ReferenceItem[]>(() => {
return projectStore.findings(options.project.value?.id || '', { projectType: options.projectType?.value })
.map(f => ({
id: f.id,
title: f.data.title,
label: `[Finding ${f.data.title}]`,
href: `/projects/${options.project.value!.id}/reporting/findings/${f.data.id}/`,
severity: options.projectType?.value ?
levelNameFromLevelNumber(getFindingRiskLevel({ finding: f, projectType: options.projectType?.value }) as any)?.toLowerCase() :
levelNameFromLevelNumber(getFindingRiskLevel({ finding: f, projectType: options.projectType?.value }) as any)?.toLowerCase() :
undefined,
}))
});
function rewriteReferenceLink(refId: string) {
const findingRef = referenceItems.value.find(f => f.id === refId);
if (findingRef) {
return {
href: `/projects/${options.project.value!.id}/reporting/findings/${findingRef.id}/`,
title: `[Finding ${findingRef.title}]`,
};
}
return null;
}

const spellcheckEnabled = options.spellcheckEnabled || computed({
get: () => localSettings.reportingSpellcheckEnabled && !options.historyDate,
Expand All @@ -175,13 +166,12 @@ export function useProjectEditBase(options: {
lang: options.project.value?.language || 'en-US',
selectableUsers: [...(options.project.value?.members || []), ...(options.project.value?.imported_members || [])],
referenceItems: referenceItems.value,
rewriteFileUrlMap: rewriteFileUrlMap.value,
spellcheckEnabled: spellcheckEnabled.value,
'onUpdate:spellcheckEnabled': (val: boolean) => { spellcheckEnabled.value = val; },
markdownEditorMode: markdownEditorMode.value,
'onUpdate:markdownEditorMode': (val: MarkdownEditorMode) => { markdownEditorMode.value = val; },
uploadFile,
rewriteFileUrl,
rewriteReferenceLink,
}));

return {
Expand Down
47 changes: 35 additions & 12 deletions packages/frontend/src/composables/markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,15 @@ import {
} from "@sysreptor/markdown/editor/index";
import { uuidv4 } from "@base/utils/helpers";
import { MarkdownEditorMode } from '#imports';
import type { renderMarkdownToHtml } from "@sysreptor/markdown";
import { workerUrlPolicy } from "~/plugins/trustedtypes";
import markdownWorkerUrl from "~/workers/markdownWorker?worker&url"

export type ReferenceItem = {
id: string;
title: string;
href: string;
label?: string;
severity?: string;
}

Expand All @@ -38,8 +43,7 @@ export type MarkdownProps = {
referenceItems?: ReferenceItem[];
collab?: CollabPropType;
uploadFile?: (file: File) => Promise<string>;
rewriteFileUrl?: (fileSrc: string) => string;
rewriteReferenceLink?: (src: string) => {href: string, title: string}|null;
rewriteFileUrlMap?: Record<string, string>;
}

export function makeMarkdownProps(options: { spellcheckSupportedDefault: boolean } = { spellcheckSupportedDefault: true }) {
Expand Down Expand Up @@ -76,6 +80,10 @@ export function makeMarkdownProps(options: { spellcheckSupportedDefault: boolean
type: Array as PropType<ReferenceItem[]>,
default: undefined,
},
rewriteFileUrlMap: {
type: Object as PropType<MarkdownProps['rewriteFileUrlMap']>,
default: undefined,
},
collab: {
type: Object as PropType<CollabPropType>,
default: undefined,
Expand All @@ -84,14 +92,6 @@ export function makeMarkdownProps(options: { spellcheckSupportedDefault: boolean
type: Function as PropType<MarkdownProps['uploadFile']>,
default: undefined,
},
rewriteFileUrl: {
type: Function as PropType<MarkdownProps['rewriteFileUrl']>,
default: undefined,
},
rewriteReferenceLink: {
type: Function as PropType<MarkdownProps['rewriteReferenceLink']>,
default: undefined,
},
}
}
export function makeMarkdownEmits() {
Expand Down Expand Up @@ -494,8 +494,8 @@ export function useMarkdownEditorBase(options: {
}));
const markdownPreviewAttrs = computed(() => ({
value: editorState.value?.doc.toString() ?? options.props.value.modelValue,
rewriteFileUrl: options.props.value.rewriteFileUrl,
rewriteReferenceLink: options.props.value.rewriteReferenceLink,
referenceItems: options.props.value.referenceItems,
rewriteFileUrlMap: options.props.value.rewriteFileUrlMap,
cacheBuster: previewCacheBuster,
}));

Expand Down Expand Up @@ -598,3 +598,26 @@ export function markdownEditorPageExtensions() {
scrollPastEnd(),
] as Extension[];
}


export async function renderMarkdownToHtmlInWorker(options: Parameters<typeof renderMarkdownToHtml>[0]): Promise<string> {
// Global worker instance reused for all components
const worker = useState('markdownWorker', () => new Worker(workerUrlPolicy.createScriptURL!(markdownWorkerUrl) as string, { type: 'module' }));

// Render markdown in worker
return new Promise((resolve, reject) => {
const messageId = uuidv4();
function onMessage(e: MessageEvent<{ messageId: string, status: 'success'|'error', error?: any, result?: string }>) {
if (e.data.messageId === messageId) {
worker.value.removeEventListener('message', onMessage);
if (e.data.status === 'success') {
resolve(e.data.result!);
} else {
reject(e.data.error);
}
}
}
worker.value.addEventListener('message', onMessage);
worker.value.postMessage({ messageId, options });
});
}
Loading

0 comments on commit 99011b1

Please sign in to comment.