|
| 1 | +/* |
| 2 | + * oSPARC - The SIMCORE frontend - https://osparc.io |
| 3 | + * Copyright: 2019 IT'IS Foundation - https://itis.swiss |
| 4 | + * License: MIT - https://opensource.org/licenses/MIT |
| 5 | + * Authors: Ignacio Pascual (ignapas) |
| 6 | + * Odei Maiz (odeimaiz) |
| 7 | + */ |
| 8 | + |
| 9 | +/** |
| 10 | + * @asset(marked/marked.min.js) |
| 11 | + * @assert(markdown.css) |
| 12 | + * @ignore(marked) |
| 13 | + */ |
| 14 | + |
| 15 | +/* global marked */ |
| 16 | + |
| 17 | +/** |
| 18 | + * This class is just a special kind of rich label that takes markdown raw text, compiles it to HTML, |
| 19 | + * sanitizes it and applies it to its value property. |
| 20 | + */ |
| 21 | +qx.Class.define("osparc.ui.markdown.Markdown2", { |
| 22 | + extend: qx.ui.embed.Html, |
| 23 | + |
| 24 | + /** |
| 25 | + * Markdown constructor. It directly accepts markdown as its first argument. |
| 26 | + * @param {String} markdown Plain text accepting markdown syntax. Its compiled version will be set in the value property of the label. |
| 27 | + */ |
| 28 | + construct: function(markdown) { |
| 29 | + this.base(arguments); |
| 30 | + |
| 31 | + this.set({ |
| 32 | + allowGrowX: true, |
| 33 | + allowGrowY: true, |
| 34 | + overflowX: "hidden", |
| 35 | + overflowY: "hidden", |
| 36 | + }); |
| 37 | + |
| 38 | + const markdownCssUri = qx.util.ResourceManager.getInstance().toUri("markdown/markdown.css"); |
| 39 | + qx.module.Css.includeStylesheet(markdownCssUri); |
| 40 | + |
| 41 | + this.__loadMarked = new Promise((resolve, reject) => { |
| 42 | + if (typeof marked === "function") { |
| 43 | + resolve(marked); |
| 44 | + } else { |
| 45 | + const loader = new qx.util.DynamicScriptLoader([ |
| 46 | + "marked/marked.min.js" |
| 47 | + ]); |
| 48 | + loader.addListenerOnce("ready", () => resolve(marked), this); |
| 49 | + loader.addListenerOnce("failed", e => |
| 50 | + reject(Error(`Failed to load ${e.getData()}`)) |
| 51 | + ); |
| 52 | + loader.start(); |
| 53 | + } |
| 54 | + }); |
| 55 | + |
| 56 | + if (markdown) { |
| 57 | + this.setValue(markdown); |
| 58 | + } |
| 59 | + |
| 60 | + this.addListenerOnce("appear", () => { |
| 61 | + this.getContentElement().addClass("osparc-markdown"); |
| 62 | + }); |
| 63 | + }, |
| 64 | + |
| 65 | + properties: { |
| 66 | + /** |
| 67 | + * Holds the raw markdown text and updates the label's {@link #value} whenever new markdown arrives. |
| 68 | + */ |
| 69 | + value: { |
| 70 | + check: "String", |
| 71 | + apply: "__applyMarkdown" |
| 72 | + }, |
| 73 | + |
| 74 | + noMargin: { |
| 75 | + check: "Boolean", |
| 76 | + init: false |
| 77 | + } |
| 78 | + }, |
| 79 | + |
| 80 | + events: { |
| 81 | + "resized": "qx.event.type.Event", |
| 82 | + }, |
| 83 | + |
| 84 | + members: { |
| 85 | + __loadMarked: null, |
| 86 | + /** |
| 87 | + * Apply function for the markdown property. Compiles the markdown text to HTML and applies it to the value property of the label. |
| 88 | + * @param {String} value Plain text accepting markdown syntax. |
| 89 | + */ |
| 90 | + __applyMarkdown: function(value = "") { |
| 91 | + this.__loadMarked.then(() => { |
| 92 | + const renderer = { |
| 93 | + link(link) { |
| 94 | + const linkColor = qx.theme.manager.Color.getInstance().resolve("link"); |
| 95 | + let linkHtml = `<a href="${link.href}" title="${link.title || ""}" style="color: ${linkColor};">` |
| 96 | + if (link.tokens && link.tokens.length) { |
| 97 | + const linkRepresentation = link.tokens[0]; |
| 98 | + if (linkRepresentation.type === "text") { |
| 99 | + linkHtml += linkRepresentation.text; |
| 100 | + } else if (linkRepresentation.type === "image") { |
| 101 | + linkHtml += `<img src="${linkRepresentation.href}" tile alt="${linkRepresentation.text}"></img>`; |
| 102 | + } |
| 103 | + } |
| 104 | + linkHtml += `</a>`; |
| 105 | + return linkHtml; |
| 106 | + } |
| 107 | + }; |
| 108 | + marked.use({ renderer }); |
| 109 | + // By default, Markdown requires two spaces at the end of a line or a blank line between paragraphs to produce a line break. |
| 110 | + // With this, a single line break (Enter) in your Markdown input will render as a <br> in HTML. |
| 111 | + marked.setOptions({ breaks: true }); |
| 112 | + |
| 113 | + const html = marked.parse(value); |
| 114 | + |
| 115 | + const safeHtml = osparc.wrapper.DOMPurify.getInstance().sanitize(html); |
| 116 | + |
| 117 | + this.setHtml(safeHtml); |
| 118 | + |
| 119 | + // for some reason the content is not immediately there |
| 120 | + qx.event.Timer.once(() => { |
| 121 | + this.__resizeMe(); |
| 122 | + }, this, 100); |
| 123 | + |
| 124 | + this.__resizeMe(); |
| 125 | + }).catch(error => console.error(error)); |
| 126 | + }, |
| 127 | + |
| 128 | + __getDomElement: function() { |
| 129 | + if (!this.getContentElement || this.getContentElement() === null) { |
| 130 | + return null; |
| 131 | + } |
| 132 | + const domElement = this.getContentElement().getDomElement(); |
| 133 | + if (domElement) { |
| 134 | + return domElement; |
| 135 | + } |
| 136 | + return null; |
| 137 | + }, |
| 138 | + |
| 139 | + // qx.ui.embed.html scale to content |
| 140 | + __resizeMe: function() { |
| 141 | + const domElement = this.__getDomElement(); |
| 142 | + if (domElement === null) { |
| 143 | + return; |
| 144 | + } |
| 145 | + this.setHeight(null); // let it auto-size |
| 146 | + this.setMinHeight(domElement.scrollHeight); // so layout respects full content |
| 147 | + }, |
| 148 | + } |
| 149 | +}); |
0 commit comments