-
Notifications
You must be signed in to change notification settings - Fork 161
Add support for SVG #1995
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Add support for SVG #1995
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -5,15 +5,17 @@ | |||||
|
|
||||||
| <template> | ||||||
| <!-- eslint-disable-next-line vue/no-v-html --> | ||||||
| <div class="note-preview" v-html="html" /> | ||||||
| <div ref="preview" class="note-preview" v-html="html" /> | ||||||
| </template> | ||||||
|
|
||||||
| <script> | ||||||
|
|
||||||
| import axios from '@nextcloud/axios' | ||||||
| import { generateUrl } from '@nextcloud/router' | ||||||
| import MarkdownIt from 'markdown-it' | ||||||
| import markdownItBidi from 'markdown-it-bidi' | ||||||
| import markdownItTaskCheckbox from 'markdown-it-task-checkbox' | ||||||
| import logger from '../Logger.js' | ||||||
| import { escapeHtml } from '../Util.js' | ||||||
|
|
||||||
| export default { | ||||||
|
|
@@ -56,6 +58,8 @@ export default { | |||||
| return { | ||||||
| html: '', | ||||||
| md, | ||||||
| // attachment URL -> object URL of the retyped SVG blob | ||||||
| svgObjectUrls: {}, | ||||||
| } | ||||||
| }, | ||||||
|
|
||||||
|
|
@@ -70,14 +74,65 @@ export default { | |||||
| this.onUpdate() | ||||||
| }, | ||||||
|
|
||||||
| mounted() { | ||||||
| // the initial onUpdate() runs before the DOM exists | ||||||
| this.hydrateSvgImages() | ||||||
| }, | ||||||
|
Comment on lines
+77
to
+80
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why do we need this if we have |
||||||
|
|
||||||
| beforeUnmount() { | ||||||
| for (const objectUrl of Object.values(this.svgObjectUrls)) { | ||||||
| URL.revokeObjectURL(objectUrl) | ||||||
| } | ||||||
| this.svgObjectUrls = {} | ||||||
| }, | ||||||
|
|
||||||
| methods: { | ||||||
| onUpdate() { | ||||||
| this.html = this.md.render(this.value) | ||||||
| this.$nextTick(() => this.hydrateSvgImages()) | ||||||
| if (!this.readonly) { | ||||||
| setTimeout(() => this.prepareOnClickListener(), 100) | ||||||
| } | ||||||
| }, | ||||||
|
|
||||||
| /** | ||||||
| * Fill in the src of SVG attachments rendered by setImageRule. | ||||||
| * | ||||||
| * The attachment endpoint serves SVG as text/plain so that navigating to it | ||||||
| * can never render it as a document, so it cannot be used as an <img> src | ||||||
| * directly. Fetch it and retype the blob instead: SVG inside <img> is | ||||||
| * rendered without scripting or external references. | ||||||
| */ | ||||||
| async hydrateSvgImages() { | ||||||
| const root = this.$refs.preview | ||||||
| if (!root) { | ||||||
| return | ||||||
| } | ||||||
|
|
||||||
| // claim every image synchronously so overlapping runs cannot load one twice | ||||||
| const targets = [...root.querySelectorAll('img[data-svg-src]')].map((img) => { | ||||||
| const url = img.dataset.svgSrc | ||||||
| delete img.dataset.svgSrc | ||||||
| return { img, url } | ||||||
| }) | ||||||
|
|
||||||
| for (const { img, url } of targets) { | ||||||
| if (this.svgObjectUrls[url]) { | ||||||
| img.src = this.svgObjectUrls[url] | ||||||
| continue | ||||||
| } | ||||||
| try { | ||||||
| const response = await axios.get(url, { responseType: 'blob' }) | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The cache is only filled after the fetch completes, so two quick re renders can fetch the same SVG twice? |
||||||
| const blob = response.data | ||||||
| const objectUrl = URL.createObjectURL(blob.slice(0, blob.size, 'image/svg+xml')) | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. does this do same thing?
Suggested change
|
||||||
| this.svgObjectUrls[url] = objectUrl | ||||||
| img.src = objectUrl | ||||||
| } catch (e) { | ||||||
| logger.error('Could not load SVG attachment', { error: e }) | ||||||
| } | ||||||
| } | ||||||
| }, | ||||||
|
|
||||||
| prepareOnClickListener() { | ||||||
| const items = document.getElementsByClassName('task-list-item') | ||||||
| for (let i = 0; i < items.length; ++i) { | ||||||
|
|
@@ -127,6 +182,7 @@ export default { | |||||
| const token = tokens[idx] | ||||||
| const aIndex = token.attrIndex('src') | ||||||
| let download = false | ||||||
| let svg = false | ||||||
| let path = token.attrs[aIndex][1] | ||||||
|
|
||||||
| if (!path.startsWith('http://') | ||||||
|
|
@@ -140,7 +196,9 @@ export default { | |||||
| ) | ||||||
| token.attrs[aIndex][1] = path | ||||||
|
|
||||||
| if (!lowecasePath.endsWith('.jpg') | ||||||
| if (lowecasePath.endsWith('.svg')) { | ||||||
| svg = true | ||||||
| } else if (!lowecasePath.endsWith('.jpg') | ||||||
| && !lowecasePath.endsWith('.jpeg') | ||||||
| && !lowecasePath.endsWith('.bmp') | ||||||
| && !lowecasePath.endsWith('.webp') | ||||||
|
|
@@ -150,7 +208,15 @@ export default { | |||||
| } | ||||||
| } | ||||||
|
|
||||||
| if (download) { | ||||||
| // escapeHtml() does not escape quotes, so it is not sufficient on its own | ||||||
| // for an attribute value | ||||||
| const attrValue = (str) => escapeHtml(str).replace(/"/g, '"') | ||||||
|
|
||||||
| if (svg) { | ||||||
| // src is set by hydrateSvgImages() once the blob has been retyped | ||||||
| return '<img class="svg-attachment" data-svg-src="' + attrValue(path) + '"' | ||||||
| + ' alt="' + attrValue(token.content) + '">' | ||||||
| } else if (download) { | ||||||
| const dlimgpath = generateUrl('svg/core/actions/download?color=ffffff') | ||||||
| const tokenContent = escapeHtml(token.content) | ||||||
| return '<div class="download-file"><a href="' + path.replace(/"/g, '"') + '"><div class="download-icon"><img class="download-icon-inner" ' | ||||||
|
|
@@ -260,6 +326,13 @@ export default { | |||||
| display: block; | ||||||
| } | ||||||
|
|
||||||
| // SVG may have no intrinsic size, so keep its own dimensions and only cap the width | ||||||
| & img.svg-attachment { | ||||||
| width: auto; | ||||||
| max-width: 75%; | ||||||
| height: auto; | ||||||
| } | ||||||
|
|
||||||
| .download-file { | ||||||
| width: 75%; | ||||||
| display: block; | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this reused across notes?