Files
n8n-enterprise-unlocked/packages/design-system/src/components/N8nMarkdown/Markdown.vue
Alex Grozav 27e2ce0470 feat: migrate editor-ui to Vite.js and various DX improvements (N8N-2277) (#4061)
* feat: Added vite.js dependencies.

* chore: Removed tests folder to follow same structure as design-system

* chore: Removed unused testing config.

* chore: Created vite.js index.html

* refactor: Updated scss structure and imports.

* refactor: Updated workflow building.

* fix: Cleared up all workflow dependency cycles. Added proper package.json imports config.

* feat: Got a working build using Vite. Need to fix issues next.

* fix: Progress! Getting process.env error.

* fix: Changed process.env to import.meta.env.

* fix: Fixed circular imports that used require(). Fixed monaco editor.

* chore: Removed commented code.

* chore: Cleaned up package.json

* feat: Made necessary changes to replace base path in css files.

* feat: Serve CSS files for `editor-ui` Vite migration (#4069)

 Serve CSS files for Vite migration

* chore: Fixed package-lock.json.

* fix: Fixed build after centralized tsconfig update.

* fix: Removed lodash-es replacement.

* fix: Commented out vitest test command.

* style: Fixed linting issues.

* fix: Added lodash-es hotfix back.

* chore: Updated package-lock.json

* refactor: Renamed all n8n scss variables to no longer be defined as private.

* feat(editor): add application-wide el-button replacement.

* fix(editor): Fix import in page alert after merge.

* chore(editor): update package-lock.json.

* fix: Case sensitive lodash-es replacement for vue-agile.

* fix: add alias for lodash-es camelcase import.

* fix: add patch-package support for fixing quill

* feat: add patch-package on postinstall

* fix: update quill patch path.

* refactor: rename quill patch

* fix: update quill version.

* fix: update quill patch

* fix: fix linting rules after installing eslint in design-system

* fix: update date picker button to have primary color

* test: update callout component snapshots

* fix(editor): fix linting issues in editor after enabling eslint

* fix(cli): add /assets/* to auth ignore endpoints in server

* chore: update package-lock.json

* chore: update package-lock.json

* fix(editor): fix linting issues

* feat: add vite-legacy support

* fix: update workflow package interface imports to type imports.

* chore: update package-lock.json

* fix(editor) fix importing translations other than english

* fix(editor): remove test command until vitest is added

* fix: increase memory allocation for vite build

* fix: add patch-package patches to n8n-custom docker build

* fix: add performance and load time improvements

* fix: add proper typing to setNodeType

* chore: update package-lock.json

* style: use generic type for reduce in setNodeType

Co-authored-by: Iván Ovejero <ivov.src@gmail.com>
2022-09-23 17:14:28 +03:00

330 lines
6.6 KiB
Vue

<template>
<div class="n8n-markdown">
<div
v-if="!loading"
ref="editor"
class="ph-no-capture"
:class="$style[theme]" v-html="htmlContent"
@click="onClick"
/>
<div v-else :class="$style.markdown">
<div v-for="(block, index) in loadingBlocks"
:key="index">
<n8n-loading
:loading="loading"
:rows="loadingRows"
animated
variant="p"
/>
<div :class="$style.spacer" />
</div>
</div>
</div>
</template>
<script lang="ts">
import N8nLoading from '../N8nLoading';
import Markdown from 'markdown-it';
// @ts-ignore
import markdownLink from 'markdown-it-link-attributes';
// @ts-ignore
import markdownEmoji from 'markdown-it-emoji';
// @ts-ignore
import markdownTasklists from 'markdown-it-task-lists';
import xss, { friendlyAttrValue } from 'xss';
import { escapeMarkdown } from '../../utils/markdown';
const DEFAULT_OPTIONS_MARKDOWN = {
html: true,
linkify: true,
typographer: true,
breaks: true,
};
const DEFAULT_OPTIONS_LINK_ATTRIBUTES = {
attrs: {
target: '_blank',
rel: 'noopener',
},
};
const DEFAULT_OPTIONS_TASKLISTS = {
label: true,
labelAfter: true,
};
interface IImage {
id: string;
url: string;
}
import Vue from 'vue';
export default Vue.extend({
components: {
N8nLoading,
},
name: 'n8n-markdown',
props: {
content: {
type: String,
},
withMultiBreaks: {
type: Boolean,
},
images: {
type: Array,
},
loading: {
type: Boolean,
},
loadingBlocks: {
type: Number,
default: 2,
},
loadingRows: {
type: Number,
default: () => {
return 3;
},
},
theme: {
type: String,
default: 'markdown',
},
options: {
type: Object,
default() {
return {
markdown: DEFAULT_OPTIONS_MARKDOWN,
linkAttributes: DEFAULT_OPTIONS_LINK_ATTRIBUTES,
tasklists: DEFAULT_OPTIONS_TASKLISTS,
};
},
},
},
computed: {
htmlContent(): string {
if (!this.content) {
return '';
}
const imageUrls: { [key: string]: string } = {};
if (this.images) {
// @ts-ignore
this.images.forEach((image: IImage) => {
if (!image) {
// Happens if an image got deleted but the workflow
// still has a reference to it
return;
}
imageUrls[image.id] = image.url;
});
}
const fileIdRegex = new RegExp('fileId:([0-9]+)');
const imageFilesRegex = /\.(jpeg|jpg|gif|png|webp|bmp|tif|tiff|apng|svg|avif)$/;
let contentToRender = this.content;
if (this.withMultiBreaks) {
contentToRender = contentToRender.replaceAll('\n\n', '\n&nbsp;\n');
}
const html = this.md.render(escapeMarkdown(contentToRender));
const safeHtml = xss(html, {
onTagAttr: (tag, name, value, isWhiteAttr) => {
if (tag === 'img' && name === 'src') {
if (value.match(fileIdRegex)) {
const id = value.split('fileId:')[1];
return `src=${friendlyAttrValue(imageUrls[id])}` || '';
}
// Only allow http requests to supported image files from the `static` directory
const isImageFile = value.split('#')[0].match(/\.(jpeg|jpg|gif|png|webp)$/) !== null;
const isStaticImageFile = isImageFile && value.startsWith('/static/');
if (!value.startsWith('https://') && !isStaticImageFile) {
return '';
}
}
// Return nothing, means keep the default handling measure
},
onTag (tag, code, options) {
if (tag === 'img' && code.includes(`alt="workflow-screenshot"`)) {
return '';
}
// return nothing, keep tag
},
});
return safeHtml;
},
},
data() {
return {
md: new Markdown(this.options.markdown) // eslint-disable-line @typescript-eslint/no-unsafe-member-access
.use(markdownLink, this.options.linkAttributes) // eslint-disable-line @typescript-eslint/no-unsafe-member-access
.use(markdownEmoji)
.use(markdownTasklists, this.options.tasklists), // eslint-disable-line @typescript-eslint/no-unsafe-member-access
};
},
methods: {
onClick(event: MouseEvent) {
let clickedLink = null;
if(event.target instanceof HTMLAnchorElement) {
clickedLink = event.target;
}
if(event.target instanceof HTMLElement && event.target.matches('a *')) {
const parentLink = event.target.closest('a');
if(parentLink) {
clickedLink = parentLink;
}
}
this.$emit('markdown-click', clickedLink, event);
},
},
});
</script>
<style lang="scss" module>
.markdown {
color: var(--color-text-base);
* {
font-size: var(--font-size-m);
line-height: var(--font-line-height-xloose);
}
h1, h2, h3, h4 {
margin-bottom: var(--spacing-s);
font-size: var(--font-size-m);
font-weight: var(--font-weight-bold);
}
h3, h4 {
font-weight: var(--font-weight-bold);
}
p,
span {
margin-bottom: var(--spacing-s);
}
ul, ol {
margin-bottom: var(--spacing-s);
padding-left: var(--spacing-m);
li {
margin-top: 0.25em;
}
}
pre {
margin-bottom: var(--spacing-s);
display: grid;
}
pre > code {
display: block;
padding: var(--spacing-s);
color: var(--color-text-dark);
background-color: var(--color-background-base);
overflow-x: auto;
}
li > code,
p > code {
padding: 0 var(--spacing-4xs);
color: var(--color-text-dark);
background-color: var(--color-background-base);
}
.label {
color: var(--color-text-base);
}
img {
width: 100%;
max-height: 90vh;
object-fit: cover;
border: var(--border-width-base) var(--color-foreground-base) var(--border-style-base);
border-radius: var(--border-radius-large);
}
blockquote {
padding-left: 10px;
font-style: italic;
border-left: var(--border-color-base) 2px solid;
}
}
.sticky {
color: var(--color-text-dark);
h1, h2, h3, h4 {
margin-bottom: var(--spacing-2xs);
font-weight: var(--font-weight-bold);
line-height: var(--font-line-height-loose);
}
h1 {
font-size: 36px;
}
h2 {
font-size: 24px;
}
h3, h4, h5, h6 {
font-size: var(--font-size-m);
}
p {
margin-bottom: var(--spacing-2xs);
font-size: var(--font-size-s);
font-weight: var(--font-weight-regular);
line-height: var(--font-line-height-loose);
}
ul, ol {
margin-bottom: var(--spacing-2xs);
padding-left: var(--spacing-m);
li {
margin-top: 0.25em;
font-size: var(--font-size-s);
font-weight: var(--font-weight-regular);
line-height: var(--font-line-height-regular);
}
}
code {
background-color: var(--color-background-base);
padding: 0 var(--spacing-4xs);
color: var(--color-secondary);
}
pre > code,li > code, p > code {
color: var(--color-secondary);
}
a {
&:hover {
text-decoration: underline;
}
}
img {
object-fit: contain;
&[src*="#full-width"] {
width: 100%;
}
}
}
.spacer {
margin: var(--spacing-2xl);
}
</style>