mirror of
https://github.com/BookStackApp/BookStack.git
synced 2026-02-05 00:29:48 +03:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf0ba9f756 | ||
|
|
05f8034439 | ||
|
|
1d1186c901 | ||
|
|
641a26cdf7 | ||
|
|
5fd8e7e0e9 | ||
|
|
d926ca5f71 | ||
|
|
b69722c3b5 | ||
|
|
c9aa1c979f |
@@ -215,14 +215,16 @@ class ExportFormatter
|
||||
*/
|
||||
protected function containHtml(string $htmlContent): string
|
||||
{
|
||||
$imageTagsOutput = [];
|
||||
preg_match_all("/\<img.*?src\=(\'|\")(.*?)(\'|\").*?\>/i", $htmlContent, $imageTagsOutput);
|
||||
// Replace embed tags with images
|
||||
$htmlContent = preg_replace("/<embed (.*?)>/i", '<img $1>', $htmlContent);
|
||||
|
||||
// Replace image src with base64 encoded image strings
|
||||
// Replace image & embed src attributes with base64 encoded data strings
|
||||
$imageTagsOutput = [];
|
||||
preg_match_all("/<img .*?src=['\"](.*?)['\"].*?>/i", $htmlContent, $imageTagsOutput);
|
||||
if (isset($imageTagsOutput[0]) && count($imageTagsOutput[0]) > 0) {
|
||||
foreach ($imageTagsOutput[0] as $index => $imgMatch) {
|
||||
$oldImgTagString = $imgMatch;
|
||||
$srcString = $imageTagsOutput[2][$index];
|
||||
$srcString = $imageTagsOutput[1][$index];
|
||||
$imageEncoded = $this->imageService->imageUriToBase64($srcString);
|
||||
if ($imageEncoded === null) {
|
||||
$imageEncoded = $srcString;
|
||||
@@ -232,14 +234,13 @@ class ExportFormatter
|
||||
}
|
||||
}
|
||||
|
||||
// Replace any relative links with full system URL
|
||||
$linksOutput = [];
|
||||
preg_match_all("/\<a.*href\=(\'|\")(.*?)(\'|\").*?\>/i", $htmlContent, $linksOutput);
|
||||
|
||||
// Replace image src with base64 encoded image strings
|
||||
preg_match_all("/<a .*href=['\"](.*?)['\"].*?>/i", $htmlContent, $linksOutput);
|
||||
if (isset($linksOutput[0]) && count($linksOutput[0]) > 0) {
|
||||
foreach ($linksOutput[0] as $index => $linkMatch) {
|
||||
$oldLinkString = $linkMatch;
|
||||
$srcString = $linksOutput[2][$index];
|
||||
$srcString = $linksOutput[1][$index];
|
||||
if (strpos(trim($srcString), 'http') !== 0) {
|
||||
$newSrcString = url($srcString);
|
||||
$newLinkString = str_replace($srcString, $newSrcString, $oldLinkString);
|
||||
@@ -248,7 +249,6 @@ class ExportFormatter
|
||||
}
|
||||
}
|
||||
|
||||
// Replace any relative links with system domain
|
||||
return $htmlContent;
|
||||
}
|
||||
|
||||
|
||||
@@ -219,6 +219,6 @@ abstract class Controller extends BaseController
|
||||
*/
|
||||
protected function getImageValidationRules(): array
|
||||
{
|
||||
return ['image_extension', 'mimes:jpeg,png,gif,webp', 'max:' . (config('app.upload_limit') * 1000)];
|
||||
return ['image_extension', 'mimes:jpeg,png,gif,webp,svg', 'max:' . (config('app.upload_limit') * 1000)];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,8 +76,11 @@ class DrawioImageController extends Controller
|
||||
return $this->jsonError('Image data could not be found');
|
||||
}
|
||||
|
||||
$isSvg = strtolower(pathinfo($image->path, PATHINFO_EXTENSION)) === 'svg';
|
||||
$uriPrefix = $isSvg ? 'data:image/svg+xml;base64,' : 'data:image/png;base64,';
|
||||
|
||||
return response()->json([
|
||||
'content' => base64_encode($imageData),
|
||||
'content' => $uriPrefix . base64_encode($imageData),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,7 +148,8 @@ class ImageRepo
|
||||
*/
|
||||
public function saveDrawing(string $base64Uri, int $uploadedTo): Image
|
||||
{
|
||||
$name = 'Drawing-' . user()->id . '-' . time() . '.png';
|
||||
$isSvg = strpos($base64Uri, 'data:image/svg+xml;') === 0;
|
||||
$name = 'Drawing-' . user()->id . '-' . time() . ($isSvg ? '.svg' : '.png');
|
||||
|
||||
return $this->imageService->saveNewFromBase64Uri($base64Uri, $name, 'drawio', $uploadedTo);
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ class ImageService
|
||||
protected $image;
|
||||
protected $fileSystem;
|
||||
|
||||
protected static $supportedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
||||
protected static $supportedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'];
|
||||
|
||||
/**
|
||||
* ImageService constructor.
|
||||
@@ -230,6 +230,14 @@ class ImageService
|
||||
return strtolower(pathinfo($image->path, PATHINFO_EXTENSION)) === 'gif';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given image is an SVG image file.
|
||||
*/
|
||||
protected function isSvg(Image $image): bool
|
||||
{
|
||||
return strtolower(pathinfo($image->path, PATHINFO_EXTENSION)) === 'svg';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given image and image data is apng.
|
||||
*/
|
||||
@@ -255,8 +263,8 @@ class ImageService
|
||||
*/
|
||||
public function getThumbnail(Image $image, ?int $width, ?int $height, bool $keepRatio = false): string
|
||||
{
|
||||
// Do not resize GIF images where we're not cropping
|
||||
if ($keepRatio && $this->isGif($image)) {
|
||||
// Do not resize GIF images where we're not cropping or SVG images.
|
||||
if (($keepRatio && $this->isGif($image)) || $this->isSvg($image)) {
|
||||
return $this->getPublicUrl($image->path);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,10 +18,8 @@ class MarkdownEditor {
|
||||
this.markdown = new MarkdownIt({html: true});
|
||||
this.markdown.use(mdTasksLists, {label: true});
|
||||
|
||||
this.display = this.elem.querySelector('.markdown-display');
|
||||
|
||||
this.displayStylesLoaded = false;
|
||||
this.input = this.elem.querySelector('textarea');
|
||||
this.display = this.$refs.display;
|
||||
this.input = this.$refs.input;
|
||||
|
||||
this.cm = null;
|
||||
this.Code = null;
|
||||
@@ -32,23 +30,13 @@ class MarkdownEditor {
|
||||
});
|
||||
|
||||
this.onMarkdownScroll = this.onMarkdownScroll.bind(this);
|
||||
|
||||
const displayLoad = () => {
|
||||
this.displayDoc = this.display.contentDocument;
|
||||
this.init(cmLoadPromise);
|
||||
};
|
||||
|
||||
if (this.display.contentDocument.readyState === 'complete') {
|
||||
displayLoad();
|
||||
} else {
|
||||
this.display.addEventListener('load', displayLoad.bind(this));
|
||||
}
|
||||
|
||||
window.$events.emitPublic(this.elem, 'editor-markdown::setup', {
|
||||
markdownIt: this.markdown,
|
||||
displayEl: this.display,
|
||||
codeMirrorInstance: this.cm,
|
||||
});
|
||||
|
||||
this.init(cmLoadPromise);
|
||||
}
|
||||
|
||||
init(cmLoadPromise) {
|
||||
@@ -56,17 +44,17 @@ class MarkdownEditor {
|
||||
let lastClick = 0;
|
||||
|
||||
// Prevent markdown display link click redirect
|
||||
this.displayDoc.addEventListener('click', event => {
|
||||
let isDblClick = Date.now() - lastClick < 300;
|
||||
this.display.addEventListener('click', event => {
|
||||
const isDblClick = Date.now() - lastClick < 300;
|
||||
|
||||
let link = event.target.closest('a');
|
||||
const link = event.target.closest('a');
|
||||
if (link !== null) {
|
||||
event.preventDefault();
|
||||
window.open(link.getAttribute('href'));
|
||||
return;
|
||||
}
|
||||
|
||||
let drawing = event.target.closest('[drawio-diagram]');
|
||||
const drawing = event.target.closest('[drawio-diagram]');
|
||||
if (drawing !== null && isDblClick) {
|
||||
this.actionEditDrawing(drawing);
|
||||
return;
|
||||
@@ -77,10 +65,10 @@ class MarkdownEditor {
|
||||
|
||||
// Button actions
|
||||
this.elem.addEventListener('click', event => {
|
||||
let button = event.target.closest('button[data-action]');
|
||||
const button = event.target.closest('button[data-action]');
|
||||
if (button === null) return;
|
||||
|
||||
let action = button.getAttribute('data-action');
|
||||
const action = button.getAttribute('data-action');
|
||||
if (action === 'insertImage') this.actionInsertImage();
|
||||
if (action === 'insertLink') this.actionShowLinkSelector();
|
||||
if (action === 'insertDrawing' && (event.ctrlKey || event.metaKey)) {
|
||||
@@ -132,35 +120,11 @@ class MarkdownEditor {
|
||||
window.$events.emit('editor-markdown-change', content);
|
||||
|
||||
// Set body content
|
||||
this.displayDoc.body.className = 'page-content';
|
||||
this.displayDoc.body.innerHTML = html;
|
||||
|
||||
// Copy styles from page head and set custom styles for editor
|
||||
this.loadStylesIntoDisplay();
|
||||
}
|
||||
|
||||
loadStylesIntoDisplay() {
|
||||
if (this.displayStylesLoaded) return;
|
||||
this.displayDoc.documentElement.classList.add('markdown-editor-display');
|
||||
// Set display to be dark mode if parent is
|
||||
|
||||
if (document.documentElement.classList.contains('dark-mode')) {
|
||||
this.displayDoc.documentElement.style.backgroundColor = '#222';
|
||||
this.displayDoc.documentElement.classList.add('dark-mode');
|
||||
}
|
||||
|
||||
this.displayDoc.head.innerHTML = '';
|
||||
const styles = document.head.querySelectorAll('style,link[rel=stylesheet]');
|
||||
for (let style of styles) {
|
||||
const copy = style.cloneNode(true);
|
||||
this.displayDoc.head.appendChild(copy);
|
||||
}
|
||||
|
||||
this.displayStylesLoaded = true;
|
||||
this.display.innerHTML = html;
|
||||
}
|
||||
|
||||
onMarkdownScroll(lineCount) {
|
||||
const elems = this.displayDoc.body.children;
|
||||
const elems = this.display.children;
|
||||
if (elems.length <= lineCount) return;
|
||||
|
||||
const topElem = (lineCount === -1) ? elems[elems.length-1] : elems[lineCount];
|
||||
@@ -317,7 +281,7 @@ class MarkdownEditor {
|
||||
let cursor = cm.getCursor();
|
||||
let lineContent = cm.getLine(cursor.line);
|
||||
let lineLen = lineContent.length;
|
||||
let newLineContent = lineContent;
|
||||
let newLineContent;
|
||||
|
||||
if (lineContent.indexOf(start) === 0 && lineContent.slice(-end.length) === end) {
|
||||
newLineContent = lineContent.slice(start.length, lineContent.length - end.length);
|
||||
@@ -333,9 +297,9 @@ class MarkdownEditor {
|
||||
let selection = cm.getSelection();
|
||||
if (selection === '') return wrapLine(start, end);
|
||||
|
||||
let newSelection = selection;
|
||||
let newSelection;
|
||||
let frontDiff = 0;
|
||||
let endDiff = 0;
|
||||
let endDiff;
|
||||
|
||||
if (selection.indexOf(start) === 0 && selection.slice(-end.length) === end) {
|
||||
newSelection = selection.slice(start.length, selection.length - end.length);
|
||||
@@ -445,10 +409,10 @@ class MarkdownEditor {
|
||||
|
||||
DrawIO.show(url,() => {
|
||||
return Promise.resolve('');
|
||||
}, (pngData) => {
|
||||
}, (drawingData) => {
|
||||
|
||||
const data = {
|
||||
image: pngData,
|
||||
image: drawingData,
|
||||
uploaded_to: Number(this.pageId),
|
||||
};
|
||||
|
||||
@@ -462,7 +426,7 @@ class MarkdownEditor {
|
||||
}
|
||||
|
||||
insertDrawing(image, originalCursor) {
|
||||
const newText = `<div drawio-diagram="${image.id}"><img src="${image.url}"></div>`;
|
||||
const newText = DrawIO.buildDrawingContentHtml(image);
|
||||
this.cm.focus();
|
||||
this.cm.replaceSelection(newText);
|
||||
this.cm.setCursor(originalCursor.line, originalCursor.ch + newText.length);
|
||||
@@ -480,21 +444,22 @@ class MarkdownEditor {
|
||||
|
||||
DrawIO.show(drawioUrl, () => {
|
||||
return DrawIO.load(drawingId);
|
||||
}, (pngData) => {
|
||||
}, (drawingData) => {
|
||||
|
||||
let data = {
|
||||
image: pngData,
|
||||
image: drawingData,
|
||||
uploaded_to: Number(this.pageId),
|
||||
};
|
||||
|
||||
window.$http.post("/images/drawio", data).then(resp => {
|
||||
let newText = `<div drawio-diagram="${resp.data.id}"><img src="${resp.data.url}"></div>`;
|
||||
let newContent = this.cm.getValue().split('\n').map(line => {
|
||||
if (line.indexOf(`drawio-diagram="${drawingId}"`) !== -1) {
|
||||
return newText;
|
||||
}
|
||||
return line;
|
||||
const image = resp.data;
|
||||
const newText = DrawIO.buildDrawingContentHtml(image);
|
||||
|
||||
const newContent = this.cm.getValue().split('\n').map(line => {
|
||||
const isDrawing = line.includes(`drawio-diagram="${drawingId}"`);
|
||||
return isDrawing ? newText : line;
|
||||
}).join('\n');
|
||||
|
||||
this.cm.setValue(newContent);
|
||||
this.cm.setCursor(cursorPos);
|
||||
this.cm.focus();
|
||||
|
||||
@@ -24,7 +24,7 @@ class PageEditor {
|
||||
this.draftDisplayIcon = this.$refs.draftDisplayIcon;
|
||||
this.changelogInput = this.$refs.changelogInput;
|
||||
this.changelogDisplay = this.$refs.changelogDisplay;
|
||||
this.changeEditorButtons = this.$manyRefs.changeEditor;
|
||||
this.changeEditorButtons = this.$manyRefs.changeEditor || [];
|
||||
this.switchDialogContainer = this.$refs.switchDialog;
|
||||
|
||||
// Translations
|
||||
|
||||
@@ -55,7 +55,7 @@ function drawEventExport(message) {
|
||||
}
|
||||
|
||||
function drawEventSave(message) {
|
||||
drawPostMessage({action: 'export', format: 'xmlpng', xml: message.xml, spin: 'Updating drawing'});
|
||||
drawPostMessage({action: 'export', format: 'xmlsvg', xml: message.xml, spin: 'Updating drawing'});
|
||||
}
|
||||
|
||||
function drawEventInit() {
|
||||
@@ -96,7 +96,21 @@ async function upload(imageData, pageUploadedToId) {
|
||||
*/
|
||||
async function load(drawingId) {
|
||||
const resp = await window.$http.get(window.baseUrl(`/images/drawio/base64/${drawingId}`));
|
||||
return `data:image/png;base64,${resp.data.content}`;
|
||||
return resp.data.content;
|
||||
}
|
||||
|
||||
export default {show, close, upload, load};
|
||||
|
||||
function buildDrawingContentHtml(drawing) {
|
||||
const isSvg = drawing.url.split('.').pop().toLowerCase() === 'svg';
|
||||
const image = `<img src="${drawing.url}">`;
|
||||
const embed = `<embed src="${drawing.url}" type="image/svg+xml">`;
|
||||
return `<div drawio-diagram="${drawing.id}">${isSvg ? embed : image}</div>`
|
||||
}
|
||||
|
||||
function buildDrawingContentNode(drawing) {
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = buildDrawingContentHtml(drawing);
|
||||
return div.children[0];
|
||||
}
|
||||
|
||||
export default {show, close, upload, load, buildDrawingContentHtml, buildDrawingContentNode};
|
||||
@@ -1,4 +1,5 @@
|
||||
import DrawIO from "../services/drawio";
|
||||
import {build} from "./config";
|
||||
|
||||
let pageEditor = null;
|
||||
let currentNode = null;
|
||||
@@ -15,15 +16,14 @@ function isDrawing(node) {
|
||||
function showDrawingManager(mceEditor, selectedNode = null) {
|
||||
pageEditor = mceEditor;
|
||||
currentNode = selectedNode;
|
||||
|
||||
// Show image manager
|
||||
window.ImageManager.show(function (image) {
|
||||
if (selectedNode) {
|
||||
let imgElem = selectedNode.querySelector('img');
|
||||
pageEditor.dom.setAttrib(imgElem, 'src', image.url);
|
||||
pageEditor.dom.setAttrib(selectedNode, 'drawio-diagram', image.id);
|
||||
pageEditor.dom.replace(buildDrawingNode(image), selectedNode);
|
||||
} else {
|
||||
let imgHTML = `<div drawio-diagram="${image.id}" contenteditable="false"><img src="${image.url}"></div>`;
|
||||
pageEditor.insertContent(imgHTML);
|
||||
const drawingHtml = DrawIO.buildDrawingContentHtml(image);
|
||||
pageEditor.insertContent(drawingHtml);
|
||||
}
|
||||
}, 'drawio');
|
||||
}
|
||||
@@ -34,7 +34,14 @@ function showDrawingEditor(mceEditor, selectedNode = null) {
|
||||
DrawIO.show(options.drawioUrl, drawingInit, updateContent);
|
||||
}
|
||||
|
||||
async function updateContent(pngData) {
|
||||
function buildDrawingNode(drawing) {
|
||||
const drawingEl = DrawIO.buildDrawingContentNode(drawing);
|
||||
drawingEl.setAttribute('contenteditable', 'false');
|
||||
drawingEl.setAttribute('data-ephox-embed-iri', 'true');
|
||||
return drawingEl;
|
||||
}
|
||||
|
||||
async function updateContent(drawingData) {
|
||||
const id = "image-" + Math.random().toString(16).slice(2);
|
||||
const loadingImage = window.baseUrl('/loading.gif');
|
||||
|
||||
@@ -50,11 +57,9 @@ async function updateContent(pngData) {
|
||||
// Handle updating an existing image
|
||||
if (currentNode) {
|
||||
DrawIO.close();
|
||||
let imgElem = currentNode.querySelector('img');
|
||||
try {
|
||||
const img = await DrawIO.upload(pngData, options.pageId);
|
||||
pageEditor.dom.setAttrib(imgElem, 'src', img.url);
|
||||
pageEditor.dom.setAttrib(currentNode, 'drawio-diagram', img.id);
|
||||
const img = await DrawIO.upload(drawingData, options.pageId);
|
||||
pageEditor.dom.replace(buildDrawingNode(img), currentNode);
|
||||
} catch (err) {
|
||||
handleUploadError(err);
|
||||
}
|
||||
@@ -62,12 +67,11 @@ async function updateContent(pngData) {
|
||||
}
|
||||
|
||||
setTimeout(async () => {
|
||||
pageEditor.insertContent(`<div drawio-diagram contenteditable="false"><img src="${loadingImage}" id="${id}"></div>`);
|
||||
pageEditor.insertContent(`<div drawio-diagram contenteditable="false"><img src="${loadingImage}" alt="Loading" id="${id}"></div>`);
|
||||
DrawIO.close();
|
||||
try {
|
||||
const img = await DrawIO.upload(pngData, options.pageId);
|
||||
pageEditor.dom.setAttrib(id, 'src', img.url);
|
||||
pageEditor.dom.get(id).parentNode.setAttribute('drawio-diagram', img.id);
|
||||
const img = await DrawIO.upload(drawingData, options.pageId);
|
||||
pageEditor.dom.replace(buildDrawingNode(img), pageEditor.dom.get(id).parentNode);
|
||||
} catch (err) {
|
||||
pageEditor.dom.remove(id);
|
||||
handleUploadError(err);
|
||||
@@ -86,7 +90,6 @@ function drawingInit() {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {WysiwygConfigOptions} providedOptions
|
||||
* @return {function(Editor, string)}
|
||||
*/
|
||||
@@ -130,14 +133,28 @@ export function getPlugin(providedOptions) {
|
||||
showDrawingEditor(editor, selectedNode);
|
||||
});
|
||||
|
||||
editor.on('SetContent', function () {
|
||||
const drawings = editor.$('body > div[drawio-diagram]');
|
||||
if (!drawings.length) return;
|
||||
editor.on('PreInit', () => {
|
||||
editor.parser.addNodeFilter('div', function(nodes) {
|
||||
for (const node of nodes) {
|
||||
if (node.attr('drawio-diagram')) {
|
||||
// Set content editable to be false to prevent direct editing of child content.
|
||||
node.attr('contenteditable', 'false');
|
||||
// Set this attribute to prevent drawing contents being parsed as media embeds
|
||||
// to avoid contents being replaced with placeholder images.
|
||||
// TinyMCE embed plugin sources looks for this attribute in its logic.
|
||||
node.attr('data-ephox-embed-iri', 'true');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
editor.undoManager.transact(function () {
|
||||
drawings.each((index, elem) => {
|
||||
elem.setAttribute('contenteditable', 'false');
|
||||
});
|
||||
editor.serializer.addNodeFilter('div', function(nodes) {
|
||||
for (const node of nodes) {
|
||||
// Clean up content attributes
|
||||
if (node.attr('drawio-diagram')) {
|
||||
node.attr('contenteditable', null);
|
||||
node.attr('data-ephox-embed-iri', null);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -114,26 +114,20 @@
|
||||
|
||||
.markdown-display {
|
||||
margin-inline-start: -1px;
|
||||
}
|
||||
|
||||
.markdown-editor-display {
|
||||
display: block;
|
||||
background-color: #fff;
|
||||
body {
|
||||
display: block;
|
||||
background-color: #fff;
|
||||
padding-inline-start: 16px;
|
||||
padding-inline-end: 16px;
|
||||
}
|
||||
padding: $-m;
|
||||
overflow-y: scroll;
|
||||
[drawio-diagram]:hover {
|
||||
outline: 2px solid var(--color-primary);
|
||||
}
|
||||
[drawio-diagram] embed {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
html.markdown-editor-display.dark-mode {
|
||||
.dark-mode .markdown-display {
|
||||
background-color: #222;
|
||||
body {
|
||||
background-color: #222;
|
||||
}
|
||||
}
|
||||
|
||||
.editor-toolbar {
|
||||
|
||||
@@ -48,6 +48,11 @@ body.page-content.mce-content-body {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// Prevent interaction with embed contents
|
||||
.page-content.mce-content-body embed {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
// Details/summary editor usability
|
||||
.page-content.mce-content-body details summary {
|
||||
pointer-events: none;
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
|
||||
<div markdown-input class="flex flex-fill">
|
||||
<textarea id="markdown-editor-input"
|
||||
refs="markdown-editor@input"
|
||||
@if($errors->has('markdown')) class="text-neg" @endif
|
||||
name="markdown"
|
||||
rows="5">@if(isset($model) || old('markdown')){{ old('markdown') ?? ($model->markdown === '' ? $model->html : $model->markdown) }}@endif</textarea>
|
||||
@@ -34,7 +35,10 @@
|
||||
<div class="editor-toolbar">
|
||||
<div class="editor-toolbar-label">{{ trans('entities.pages_md_preview') }}</div>
|
||||
</div>
|
||||
<iframe src="about:blank" class="markdown-display" sandbox="allow-same-origin"></iframe>
|
||||
<div class="markdown-display">
|
||||
<div refs="markdown-editor@display"
|
||||
class="page-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -258,6 +258,24 @@ class ExportTest extends TestCase
|
||||
unlink($testFilePath);
|
||||
}
|
||||
|
||||
public function test_page_export_contained_html_embed_elements_are_converted_to_images_with_srcs_inlined()
|
||||
{
|
||||
$page = Page::query()->first();
|
||||
$page->html = '<embed src="http://localhost/uploads/images/gallery/svg_test.svg"/>';
|
||||
$page->save();
|
||||
|
||||
$storageDisk = Storage::disk('local');
|
||||
$storageDisk->makeDirectory('uploads/images/gallery');
|
||||
$storageDisk->put('uploads/images/gallery/svg_test.svg', '<svg>good</svg>');
|
||||
|
||||
$resp = $this->asEditor()->get($page->getUrl('/export/html'));
|
||||
|
||||
$storageDisk->delete('uploads/images/gallery/svg_test.svg');
|
||||
|
||||
$resp->assertDontSee('http://localhost/uploads/images/gallery/svg_test.svg', false);
|
||||
$resp->assertSee('<img src="data:image/svg+xml;base64,PHN2Zz5nb29kPC9zdmc+">', false);
|
||||
}
|
||||
|
||||
public function test_exports_removes_scripts_from_custom_head()
|
||||
{
|
||||
$entities = [
|
||||
|
||||
@@ -10,7 +10,7 @@ class DrawioTest extends TestCase
|
||||
{
|
||||
use UsesImages;
|
||||
|
||||
public function test_get_image_as_base64()
|
||||
public function test_get_image_as_base64_with_png_content()
|
||||
{
|
||||
$page = Page::first();
|
||||
$this->asAdmin();
|
||||
@@ -23,11 +23,27 @@ class DrawioTest extends TestCase
|
||||
|
||||
$imageGet = $this->getJson("/images/drawio/base64/{$image->id}");
|
||||
$imageGet->assertJson([
|
||||
'content' => 'iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gEcDCo5iYNs+gAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAAFElEQVQI12O0jN/KgASYGFABqXwAZtoBV6Sl3hIAAAAASUVORK5CYII=',
|
||||
'content' => 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gEcDCo5iYNs+gAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAAFElEQVQI12O0jN/KgASYGFABqXwAZtoBV6Sl3hIAAAAASUVORK5CYII=',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_drawing_base64_upload()
|
||||
public function test_get_image_as_base64_with_svg_content()
|
||||
{
|
||||
$page = Page::first();
|
||||
$this->asAdmin();
|
||||
|
||||
$this->uploadImage('my-drawing.svg', $page->id, 'image/svg+xml', 'diagram.svg');
|
||||
$image = Image::first();
|
||||
$image->type = 'drawio';
|
||||
$image->save();
|
||||
|
||||
$imageGet = $this->getJson("/images/drawio/base64/{$image->id}");
|
||||
$imageGet->assertJson([
|
||||
'content' => 'data:image/svg+xml;base64,' . base64_encode(file_get_contents($this->getTestImageFilePath('diagram.svg'))),
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_drawing_base64_upload_with_png()
|
||||
{
|
||||
$page = Page::first();
|
||||
$editor = $this->getEditor();
|
||||
@@ -35,7 +51,7 @@ class DrawioTest extends TestCase
|
||||
|
||||
$upload = $this->postJson('images/drawio', [
|
||||
'uploaded_to' => $page->id,
|
||||
'image' => 'image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gEcDCo5iYNs+gAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAAFElEQVQI12O0jN/KgASYGFABqXwAZtoBV6Sl3hIAAAAASUVORK5CYII=',
|
||||
'image' => 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gEcDCo5iYNs+gAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAAFElEQVQI12O0jN/KgASYGFABqXwAZtoBV6Sl3hIAAAAASUVORK5CYII=',
|
||||
]);
|
||||
|
||||
$upload->assertStatus(200);
|
||||
@@ -54,6 +70,34 @@ class DrawioTest extends TestCase
|
||||
$this->assertTrue($testImageData === $uploadedImageData, 'Uploaded image file data does not match our test image as expected');
|
||||
}
|
||||
|
||||
public function test_drawing_base64_upload_with_svg()
|
||||
{
|
||||
$page = Page::first();
|
||||
$editor = $this->getEditor();
|
||||
$this->actingAs($editor);
|
||||
|
||||
$upload = $this->postJson('images/drawio', [
|
||||
'uploaded_to' => $page->id,
|
||||
'image' => 'data:image/svg+xml;base64,' . base64_encode(file_get_contents($this->getTestImageFilePath('diagram.svg'))),
|
||||
]);
|
||||
|
||||
$upload->assertStatus(200);
|
||||
$upload->assertJson([
|
||||
'type' => 'drawio',
|
||||
'uploaded_to' => $page->id,
|
||||
'created_by' => $editor->id,
|
||||
'updated_by' => $editor->id,
|
||||
]);
|
||||
|
||||
$image = Image::where('type', '=', 'drawio')->first();
|
||||
$this->assertStringEndsWith('.svg', $image->path);
|
||||
$this->assertTrue(file_exists(public_path($image->path)), 'Uploaded image not found at path: ' . public_path($image->path));
|
||||
|
||||
$testImageData = file_get_contents($this->getTestImageFilePath('diagram.svg'));
|
||||
$uploadedImageData = file_get_contents(public_path($image->path));
|
||||
$this->assertTrue($testImageData === $uploadedImageData, 'Uploaded image file data does not match our test image as expected');
|
||||
}
|
||||
|
||||
public function test_drawio_url_can_be_configured()
|
||||
{
|
||||
config()->set('services.drawio', 'http://cats.com?dog=tree');
|
||||
|
||||
@@ -74,6 +74,23 @@ class ImageTest extends TestCase
|
||||
$this->assertStringNotContainsString('thumbs-', $imgDetails['response']->thumbs->display);
|
||||
}
|
||||
|
||||
public function test_svg_upload()
|
||||
{
|
||||
/** @var Page $page */
|
||||
$page = Page::query()->first();
|
||||
$admin = $this->getAdmin();
|
||||
$this->actingAs($admin);
|
||||
|
||||
$imgDetails = $this->uploadGalleryImage($page, 'diagram.svg', 'image/svg+xml');
|
||||
$this->assertFileExists(public_path($imgDetails['path']));
|
||||
$this->assertTrue(
|
||||
$imgDetails['response']->url === $imgDetails['response']->thumbs->gallery
|
||||
&& $imgDetails['response']->url === $imgDetails['response']->thumbs->display,
|
||||
);
|
||||
|
||||
$this->deleteImage($imgDetails['path']);
|
||||
}
|
||||
|
||||
public function test_image_edit()
|
||||
{
|
||||
$editor = $this->getEditor();
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace Tests\Uploads;
|
||||
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\Uploads\Image;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use stdClass;
|
||||
|
||||
@@ -39,9 +40,9 @@ trait UsesImages
|
||||
/**
|
||||
* Get a test image that can be uploaded.
|
||||
*/
|
||||
protected function getTestImage(string $fileName, ?string $testDataFileName = null): UploadedFile
|
||||
protected function getTestImage(string $fileName, ?string $testDataFileName = null, $mimeType = 'image/png'): UploadedFile
|
||||
{
|
||||
return new UploadedFile($this->getTestImageFilePath($testDataFileName), $fileName, 'image/png', null, true);
|
||||
return new UploadedFile($this->getTestImageFilePath($testDataFileName), $fileName, $mimeType, null, true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,7 +74,7 @@ trait UsesImages
|
||||
*/
|
||||
protected function uploadImage($name, $uploadedTo = 0, $contentType = 'image/png', ?string $testDataFileName = null)
|
||||
{
|
||||
$file = $this->getTestImage($name, $testDataFileName);
|
||||
$file = $this->getTestImage($name, $testDataFileName, $contentType);
|
||||
|
||||
return $this->withHeader('Content-Type', $contentType)
|
||||
->call('POST', '/images/gallery', ['uploaded_to' => $uploadedTo], [], ['file' => $file], []);
|
||||
@@ -84,11 +85,9 @@ trait UsesImages
|
||||
* Returns the image name.
|
||||
* Can provide a page to relate the image to.
|
||||
*
|
||||
* @param Page|null $page
|
||||
*
|
||||
* @return array{name: string, path: string, page: Page, response: stdClass}
|
||||
*/
|
||||
protected function uploadGalleryImage(Page $page = null, ?string $testDataFileName = null)
|
||||
protected function uploadGalleryImage(Page $page = null, string $testDataFileName = null, string $contentType = 'image/png')
|
||||
{
|
||||
if ($page === null) {
|
||||
$page = Page::query()->first();
|
||||
@@ -98,7 +97,7 @@ trait UsesImages
|
||||
$relPath = $this->getTestImagePath('gallery', $imageName);
|
||||
$this->deleteImage($relPath);
|
||||
|
||||
$upload = $this->uploadImage($imageName, $page->id, 'image/png', $testDataFileName);
|
||||
$upload = $this->uploadImage($imageName, $page->id, $contentType, $testDataFileName);
|
||||
$upload->assertStatus(200);
|
||||
|
||||
return [
|
||||
|
||||
3
tests/test-data/diagram.svg
Normal file
3
tests/test-data/diagram.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" style="background-color: rgb(255, 255, 255);" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="121px" height="141px" viewBox="-0.5 -0.5 121 141"><defs/><g><ellipse cx="25" cy="87.5" rx="7.5" ry="7.5" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/><path d="M 25 95 L 25 120 M 25 100 L 10 100 M 25 100 L 40 100 M 25 120 L 10 140 M 25 120 L 40 140" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><path d="M 0 0 L 120 0 L 120 50 L 80 50 L 60 80 L 60 50 L 0 50 Z" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 25px; margin-left: 1px;"><div style="box-sizing: border-box; font-size: 0px; text-align: center;" data-drawio-colors="color: rgb(0, 0, 0); "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">Hello!</div></div></div></foreignObject><text x="60" y="29" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">Hello!</text></switch></g></g><switch><g requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"/><a transform="translate(0,-5)" xlink:href="https://www.diagrams.net/doc/faq/svg-export-text-problems" target="_blank"><text text-anchor="middle" font-size="10px" x="50%" y="100%">Text is not SVG - cannot display</text></a></switch></svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
Reference in New Issue
Block a user