diff --git a/.gitignore b/.gitignore
index 7f47698..a45d2e9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -216,3 +216,6 @@ desktop.ini
*.crash
*.crashlog
crash-*.txt
+
+# Diagram vendor build dependencies (bundled output is checked in)
+scripts/diagram-vendors/node_modules/
diff --git a/QuickMD/Fixtures/architecture.puml b/QuickMD/Fixtures/architecture.puml
new file mode 100644
index 0000000..600f9ad
--- /dev/null
+++ b/QuickMD/Fixtures/architecture.puml
@@ -0,0 +1,5 @@
+@startuml
+class Document
+class Renderer
+Document --> Renderer : render linked source
+@enduml
diff --git a/QuickMD/Fixtures/process.bpmn b/QuickMD/Fixtures/process.bpmn
new file mode 100644
index 0000000..dccb6d0
--- /dev/null
+++ b/QuickMD/Fixtures/process.bpmn
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/QuickMD/Fixtures/sequence.plantuml b/QuickMD/Fixtures/sequence.plantuml
new file mode 100644
index 0000000..46df455
--- /dev/null
+++ b/QuickMD/Fixtures/sequence.plantuml
@@ -0,0 +1,4 @@
+@startuml
+Reader -> QuickMD: Open linked diagram
+QuickMD -> Reader: Scalable preview
+@enduml
diff --git a/QuickMD/Fixtures/vector-example.svg b/QuickMD/Fixtures/vector-example.svg
new file mode 100644
index 0000000..2b2b6f4
--- /dev/null
+++ b/QuickMD/Fixtures/vector-example.svg
@@ -0,0 +1,11 @@
+
diff --git a/QuickMD/QuickMD.xcodeproj/project.pbxproj b/QuickMD/QuickMD.xcodeproj/project.pbxproj
index 52643f5..e60d24c 100644
--- a/QuickMD/QuickMD.xcodeproj/project.pbxproj
+++ b/QuickMD/QuickMD.xcodeproj/project.pbxproj
@@ -8,7 +8,7 @@
/* Begin PBXBuildFile section */
A1B2C3D401000060 /* AlertBlockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000060 /* AlertBlockView.swift */; };
- A1B2C3D401000061 /* MermaidPDFRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000061 /* MermaidPDFRenderer.swift */; };
+ A1B2C3D401000061 /* DiagramPDFRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000061 /* DiagramPDFRenderer.swift */; };
A1B2C3D401000062 /* DocumentFonts.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000062 /* DocumentFonts.swift */; };
A1B2C3D401000063 /* FontPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000063 /* FontPickerView.swift */; };
A1B2C3D401000064 /* FontTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000064 /* FontTests.swift */; };
@@ -33,7 +33,7 @@
A1B2C3D401000012 /* ThemePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000016 /* ThemePickerView.swift */; };
A1B2C3D401000013 /* SandboxAccessManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000017 /* SandboxAccessManager.swift */; };
A1B2C3D401000014 /* MathBlockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000018 /* MathBlockView.swift */; };
- A1B2C3D401000015 /* MermaidBlockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000019 /* MermaidBlockView.swift */; };
+ A1B2C3D401000015 /* DiagramBlockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000019 /* DiagramBlockView.swift */; };
A1B2C3D401000016 /* MathFont.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D40000001A /* MathFont.swift */; };
A1B2C3D401000017 /* MathImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D40000001B /* MathImage.swift */; };
A1B2C3D401000018 /* MTBezierPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D40000001C /* MTBezierPath.swift */; };
@@ -63,7 +63,6 @@
A1B2C3D401000030 /* MTElementWidthCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000034 /* MTElementWidthCalculator.swift */; };
A1B2C3D401000031 /* MTLineFitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000035 /* MTLineFitter.swift */; };
A1B2C3D401000032 /* mermaid.min.js in Resources */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000036 /* mermaid.min.js */; };
- A1B2C3D401000033 /* mermaid-template.html in Resources */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000037 /* mermaid-template.html */; };
A1B2C3D401000034 /* mathFonts.bundle in Resources */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000038 /* mathFonts.bundle */; };
A1B2C3D401000036 /* CustomThemeStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D40000003A /* CustomThemeStore.swift */; };
A1B2C3D401000037 /* RecentDocumentsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D40000003B /* RecentDocumentsStore.swift */; };
@@ -102,11 +101,16 @@
A1B2C3D40100006A /* BlockHeightMeasurerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D40000006A /* BlockHeightMeasurerTests.swift */; };
A1B2C3D40100006B /* BlockHeightMeasurer.swift in Sources (Tests) */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000069 /* BlockHeightMeasurer.swift */; };
A1B2C3D40100006C /* VirtualBlockList.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D40000006C /* VirtualBlockList.swift */; };
+ D1A600010000000000000001 /* DiagramRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A600020000000000000001; };
+ D1A600010000000000000002 /* Diagrams in Resources */ = {isa = PBXBuildFile; fileRef = D1A600020000000000000002; };
+ D1A600010000000000000003 /* DiagramRenderer.swift in Tests */ = {isa = PBXBuildFile; fileRef = D1A600020000000000000001; };
+ D1A600010000000000000004 /* DiagramRendererTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A600020000000000000004; };
+ D1A600010000000000000005 /* DiagramPDFRenderer.swift in Tests */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000061 /* DiagramPDFRenderer.swift */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
A1B2C3D400000060 /* AlertBlockView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertBlockView.swift; sourceTree = ""; };
- A1B2C3D400000061 /* MermaidPDFRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MermaidPDFRenderer.swift; sourceTree = ""; };
+ A1B2C3D400000061 /* DiagramPDFRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiagramPDFRenderer.swift; sourceTree = ""; };
A1B2C3D400000062 /* DocumentFonts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentFonts.swift; sourceTree = ""; };
A1B2C3D400000063 /* FontPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontPickerView.swift; sourceTree = ""; };
A1B2C3D400000064 /* FontTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontTests.swift; sourceTree = ""; };
@@ -137,7 +141,7 @@
A1B2C3D400000016 /* ThemePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemePickerView.swift; sourceTree = ""; };
A1B2C3D400000017 /* SandboxAccessManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SandboxAccessManager.swift; sourceTree = ""; };
A1B2C3D400000018 /* MathBlockView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MathBlockView.swift; sourceTree = ""; };
- A1B2C3D400000019 /* MermaidBlockView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MermaidBlockView.swift; sourceTree = ""; };
+ A1B2C3D400000019 /* DiagramBlockView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiagramBlockView.swift; sourceTree = ""; };
A1B2C3D40000001A /* MathFont.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MathFont.swift; sourceTree = ""; };
A1B2C3D40000001B /* MathImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MathImage.swift; sourceTree = ""; };
A1B2C3D40000001C /* MTBezierPath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MTBezierPath.swift; sourceTree = ""; };
@@ -167,7 +171,6 @@
A1B2C3D400000034 /* MTElementWidthCalculator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MTElementWidthCalculator.swift; sourceTree = ""; };
A1B2C3D400000035 /* MTLineFitter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MTLineFitter.swift; sourceTree = ""; };
A1B2C3D400000036 /* mermaid.min.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = mermaid.min.js; sourceTree = ""; };
- A1B2C3D400000037 /* mermaid-template.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "mermaid-template.html"; sourceTree = ""; };
A1B2C3D400000038 /* mathFonts.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = mathFonts.bundle; sourceTree = ""; };
A1B2C3D40000003A /* CustomThemeStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomThemeStore.swift; sourceTree = ""; };
A1B2C3D40000003B /* RecentDocumentsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentDocumentsStore.swift; sourceTree = ""; };
@@ -191,6 +194,9 @@
A1B2C3D40000004D /* FileWatcherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileWatcherTests.swift; sourceTree = ""; };
A1B2C3D400000066 /* ChromeHoverState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChromeHoverState.swift; sourceTree = ""; };
A1B2C3D400000067 /* ChromeHoverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChromeHoverTests.swift; sourceTree = ""; };
+ D1A600020000000000000001 /* DiagramRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiagramRenderer.swift; sourceTree = ""; };
+ D1A600020000000000000002 /* Diagrams */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Diagrams; sourceTree = ""; };
+ D1A600020000000000000004 /* DiagramRendererTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiagramRendererTests.swift; sourceTree = ""; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -232,6 +238,7 @@
A1B2C3D400000064 /* FontTests.swift */,
A1B2C3D400000067 /* ChromeHoverTests.swift */,
A1B2C3D40000006A /* BlockHeightMeasurerTests.swift */,
+ D1A600020000000000000004 /* DiagramRendererTests.swift */,
);
path = QuickMDTests;
sourceTree = "";
@@ -259,7 +266,8 @@
A1B2C3D400000009 /* TipJarManager.swift */,
A1B2C3D40000000A /* TipJarView.swift */,
A1B2C3D40000000C /* MarkdownExport.swift */,
- A1B2C3D400000061 /* MermaidPDFRenderer.swift */,
+ A1B2C3D400000061 /* DiagramPDFRenderer.swift */,
+ D1A600020000000000000001 /* DiagramRenderer.swift */,
A1B2C3D400000062 /* DocumentFonts.swift */,
A1B2C3D400000017 /* SandboxAccessManager.swift */,
A1B2C3D40000003A /* CustomThemeStore.swift */,
@@ -283,7 +291,7 @@
A1B2C3D400000015 /* TableOfContentsView.swift */,
A1B2C3D400000016 /* ThemePickerView.swift */,
A1B2C3D400000018 /* MathBlockView.swift */,
- A1B2C3D400000019 /* MermaidBlockView.swift */,
+ A1B2C3D400000019 /* DiagramBlockView.swift */,
A1B2C3D40000003C /* RecentDocumentsSidebar.swift */,
A1B2C3D400000040 /* ChromeButtons.swift */,
A1B2C3D400000066 /* ChromeHoverState.swift */,
@@ -345,7 +353,7 @@
isa = PBXGroup;
children = (
A1B2C3D400000036 /* mermaid.min.js */,
- A1B2C3D400000037 /* mermaid-template.html */,
+ D1A600020000000000000002 /* Diagrams */,
);
path = Resources;
sourceTree = "";
@@ -439,7 +447,7 @@
files = (
A1B2C3D401000005 /* Assets.xcassets in Resources */,
A1B2C3D401000032 /* mermaid.min.js in Resources */,
- A1B2C3D401000033 /* mermaid-template.html in Resources */,
+ D1A600010000000000000002 /* Diagrams in Resources */,
A1B2C3D401000034 /* mathFonts.bundle in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -470,7 +478,7 @@
A1B2C3D401000012 /* ThemePickerView.swift in Sources */,
A1B2C3D401000013 /* SandboxAccessManager.swift in Sources */,
A1B2C3D401000014 /* MathBlockView.swift in Sources */,
- A1B2C3D401000015 /* MermaidBlockView.swift in Sources */,
+ A1B2C3D401000015 /* DiagramBlockView.swift in Sources */,
A1B2C3D401000016 /* MathFont.swift in Sources */,
A1B2C3D401000017 /* MathImage.swift in Sources */,
A1B2C3D401000018 /* MTBezierPath.swift in Sources */,
@@ -509,7 +517,8 @@
A1B2C3D401000066 /* ChromeHoverState.swift in Sources */,
A1B2C3D40100004A /* TextBlockView.swift in Sources */,
A1B2C3D401000060 /* AlertBlockView.swift in Sources */,
- A1B2C3D401000061 /* MermaidPDFRenderer.swift in Sources */,
+ A1B2C3D401000061 /* DiagramPDFRenderer.swift in Sources */,
+ D1A600010000000000000001 /* DiagramRenderer.swift in Sources */,
A1B2C3D40100004B /* InlineMathSegmenter.swift in Sources */,
A1B2C3D40100004D /* FileWatchManager.swift in Sources */,
A1B2C3D40100004E /* ExternalEditorManager.swift in Sources */,
@@ -548,6 +557,9 @@
A1B2C3D401000067 /* ChromeHoverTests.swift in Sources */,
A1B2C3D401000068 /* ChromeHoverState.swift in Sources (Tests) */,
A1B2C3D40100006A /* BlockHeightMeasurerTests.swift in Sources */,
+ D1A600010000000000000003 /* DiagramRenderer.swift in Tests */,
+ D1A600010000000000000004 /* DiagramRendererTests.swift in Sources */,
+ D1A600010000000000000005 /* DiagramPDFRenderer.swift in Tests */,
A1B2C3D40100006B /* BlockHeightMeasurer.swift in Sources (Tests) */,
);
runOnlyForDeploymentPostprocessing = 0;
diff --git a/QuickMD/QuickMD/BlockHeightMeasurer.swift b/QuickMD/QuickMD/BlockHeightMeasurer.swift
index bc7a513..c39f68f 100644
--- a/QuickMD/QuickMD/BlockHeightMeasurer.swift
+++ b/QuickMD/QuickMD/BlockHeightMeasurer.swift
@@ -248,6 +248,12 @@ enum BlockLayout {
/// the bitmap is decoded, so this is what a measured row starts at.
static let placeholderHeight: CGFloat = 100
static let maxDisplayWidth: CGFloat = 600
+
+ /// Scale the fitted 100% width, then keep the result inside the column.
+ static func displayWidth(fontScale: CGFloat, contentWidth: CGFloat) -> CGFloat {
+ let available = max(1, contentWidth)
+ return min(available, min(maxDisplayWidth, available) * fontScale)
+ }
}
// MARK: Display math
@@ -714,7 +720,7 @@ enum BlockHeightMeasurer {
+ 2 * BlockLayout.Document.imageOuterVerticalPadding)
kinds.append(.reported)
- case .mermaidDiagram:
+ case .mermaidDiagram, .diagram:
heights.append((heightSeeds[block.id] ?? BlockLayout.Mermaid.defaultHeight)
+ 2 * BlockLayout.Mermaid.verticalPadding
+ 2 * BlockLayout.Document.mermaidOuterVerticalPadding)
@@ -890,3 +896,13 @@ enum BlockHeightMeasurer {
}
}
+
+/// Apply text scale to the fitted baseline, never exceeding the column width.
+enum DiagramLayout {
+ static func size(natural: CGSize, contentWidth: CGFloat, fontScale: CGFloat) -> CGSize {
+ let width = max(1, contentWidth)
+ let factor = min(width / max(1, natural.width),
+ min(1, width / max(1, natural.width)) * fontScale)
+ return CGSize(width: natural.width * factor, height: natural.height * factor)
+ }
+}
diff --git a/QuickMD/QuickMD/DiagramPDFRenderer.swift b/QuickMD/QuickMD/DiagramPDFRenderer.swift
new file mode 100644
index 0000000..f726871
--- /dev/null
+++ b/QuickMD/QuickMD/DiagramPDFRenderer.swift
@@ -0,0 +1,84 @@
+import AppKit
+import WebKit
+
+/// Export from the same cached SVG as the document, not a screen-sized bitmap.
+@MainActor
+final class DiagramPDFRenderer {
+ static func renderAll(sources: [DiagramSource], width: CGFloat,
+ totalBudget: TimeInterval = 30, renderer: DiagramRenderer? = nil) async -> [DiagramSource: NSImage] {
+ let renderer = renderer ?? DiagramRenderer.shared
+ var result: [DiagramSource: NSImage] = [:]
+ let deadline = Date().addingTimeInterval(totalBudget)
+ for source in sources where result[source] == nil {
+ let remaining = deadline.timeIntervalSinceNow
+ guard remaining > 1 else { break }
+ do {
+ let svg = try await renderer.render(source, timeout: min(10, remaining))
+ if let image = await pdfImage(svg, width: width, timeout: max(1, deadline.timeIntervalSinceNow)) {
+ result[source] = image
+ }
+ } catch { /* Export retains the styled-source fallback. */ }
+ }
+ return result
+ }
+
+ private static var active: Set = []
+ private static func pdfImage(_ diagram: RenderedDiagram, width: CGFloat, timeout: TimeInterval) async -> NSImage? {
+ await withCheckedContinuation { continuation in
+ var job: SVGDocumentExport!
+ job = SVGDocumentExport(diagram: diagram, width: width, timeout: timeout) { image in
+ active.remove(job)
+ continuation.resume(returning: image)
+ }
+ active.insert(job)
+ job.start()
+ }
+ }
+}
+
+private final class SVGDocumentExport: NSObject, WKNavigationDelegate {
+ let diagram: RenderedDiagram
+ let width: CGFloat
+ let timeout: TimeInterval
+ let completion: (NSImage?) -> Void
+ var window: NSWindow?
+ var webView: WKWebView?
+ var finished = false
+
+ init(diagram: RenderedDiagram, width: CGFloat, timeout: TimeInterval, completion: @escaping (NSImage?) -> Void) {
+ self.diagram = diagram; self.width = width; self.timeout = timeout; self.completion = completion
+ }
+ func start() {
+ let size = DiagramLayout.size(natural: diagram.size, contentWidth: width, fontScale: 1)
+ let config = SVGDocument.configuration()
+ let view = WKWebView(frame: CGRect(origin: .zero, size: size), configuration: config)
+ view.navigationDelegate = self
+ webView = view
+ let host = NSWindow(contentRect: CGRect(x: -20000, y: -20000, width: size.width, height: size.height),
+ styleMask: [.borderless], backing: .buffered, defer: false)
+ host.isReleasedWhenClosed = false
+ host.contentView = view
+ host.orderBack(nil)
+ window = host
+ view.loadHTMLString(SVGDocument.html(for: diagram), baseURL: nil)
+ DispatchQueue.main.asyncAfter(deadline: .now() + timeout) { [weak self] in self?.finish(nil) }
+ }
+ func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
+ let config = WKPDFConfiguration()
+ config.rect = webView.bounds
+ webView.createPDF(configuration: config) { [weak self] result in
+ self?.finish((try? result.get()).flatMap(NSImage.init(data:)))
+ }
+ }
+ func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { finish(nil) }
+ func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { finish(nil) }
+ func webViewWebContentProcessDidTerminate(_ webView: WKWebView) { finish(nil) }
+ private func finish(_ image: NSImage?) {
+ guard !finished else { return }
+ finished = true
+ webView?.navigationDelegate = nil
+ window?.orderOut(nil)
+ webView = nil; window = nil
+ completion(image)
+ }
+}
diff --git a/QuickMD/QuickMD/DiagramRenderer.swift b/QuickMD/QuickMD/DiagramRenderer.swift
new file mode 100644
index 0000000..1b024c8
--- /dev/null
+++ b/QuickMD/QuickMD/DiagramRenderer.swift
@@ -0,0 +1,179 @@
+import AppKit
+import WebKit
+
+struct RenderedDiagram {
+ let svg: String
+ let size: CGSize
+}
+
+private final class CachedDiagram: NSObject {
+ let value: RenderedDiagram
+ init(_ value: RenderedDiagram) { self.value = value }
+}
+
+enum DiagramRenderError: LocalizedError {
+ case failed(String)
+ var errorDescription: String? {
+ if case .failed(let message) = self { return message }
+ return nil
+ }
+}
+
+/// One serial, offline rendering context. The SVG cache is independent of the
+/// document width and text scale: resizing never re-runs a diagram engine.
+@MainActor
+final class DiagramRenderer: NSObject, WKNavigationDelegate {
+ static let shared = DiagramRenderer()
+ private let resourcesURL: URL?
+ private let cache = NSCache()
+ private var tail: Task?
+ private var webView: WKWebView?
+ private var window: NSWindow?
+ private var ready = false
+ private var completion: ((Result) -> Void)?
+ private var pendingSource: DiagramSource?
+ private var timeoutWork: DispatchWorkItem?
+ private var generation = 0
+
+ init(resourcesURL: URL? = Bundle.main.resourceURL) {
+ self.resourcesURL = resourcesURL
+ super.init()
+ cache.totalCostLimit = 32 * 1024 * 1024
+ cache.countLimit = 128
+ }
+
+ private func cacheKey(_ source: DiagramSource) -> NSString {
+ "\(source.kind.rawValue)|\(source.isDark)|\(source.source)" as NSString
+ }
+
+ func render(_ source: DiagramSource, timeout: TimeInterval = 12) async throws -> RenderedDiagram {
+ guard source.source.utf8.count <= 2 * 1024 * 1024 else {
+ throw DiagramRenderError.failed("Diagram source exceeds the 2 MB rendering limit.")
+ }
+ if let cached = cache.object(forKey: cacheKey(source)) { return cached.value }
+ let previous = tail
+ let task = Task { @MainActor in
+ await previous?.value
+ if let cached = self.cache.object(forKey: self.cacheKey(source)) { return cached.value }
+ let result = try await self.perform(source, timeout: timeout)
+ self.cache.setObject(CachedDiagram(result), forKey: self.cacheKey(source), cost: result.svg.utf8.count)
+ return result
+ }
+ tail = Task { _ = try? await task.value }
+ return try await task.value
+ }
+
+ private func perform(_ source: DiagramSource, timeout: TimeInterval) async throws -> RenderedDiagram {
+ try await withCheckedThrowingContinuation { continuation in
+ completion = { continuation.resume(with: $0) }
+ pendingSource = source
+ generation += 1
+ let token = generation
+ let timer = DispatchWorkItem { [weak self] in
+ guard let self, self.generation == token else { return }
+ self.finish(.failure(DiagramRenderError.failed("Diagram rendering timed out.")), reset: true)
+ }
+ timeoutWork = timer
+ DispatchQueue.main.asyncAfter(deadline: .now() + timeout, execute: timer)
+ if ready { run() } else if webView == nil { load() }
+ }
+ }
+
+ private func load() {
+ guard let resourcesURL else {
+ finish(.failure(DiagramRenderError.failed("Bundled diagram resources are missing.")), reset: true)
+ return
+ }
+ let page = resourcesURL.appendingPathComponent("Diagrams/render.html")
+ guard FileManager.default.fileExists(atPath: page.path) else {
+ finish(.failure(DiagramRenderError.failed("Bundled diagram renderer is missing.")), reset: true)
+ return
+ }
+ let view = WKWebView(frame: NSRect(x: 0, y: 0, width: 1200, height: 800))
+ view.navigationDelegate = self
+ let host = NSWindow(contentRect: NSRect(x: -20000, y: -20000, width: 1200, height: 800),
+ styleMask: [.borderless], backing: .buffered, defer: false)
+ host.isReleasedWhenClosed = false
+ host.contentView = view
+ host.orderBack(nil)
+ window = host
+ webView = view
+ view.loadFileURL(page, allowingReadAccessTo: resourcesURL)
+ }
+
+ func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
+ guard webView === self.webView else { return }
+ ready = true
+ run()
+ }
+
+ private func run() {
+ guard let webView, let source = pendingSource, completion != nil else { return }
+ let token = generation
+ webView.callAsyncJavaScript("return await quickmdRender(kind, source, isDark);",
+ arguments: ["kind": source.kind.rawValue, "source": source.source,
+ "isDark": source.isDark], in: nil, in: .page) { [weak self] result in
+ guard let self, self.generation == token, self.completion != nil else { return }
+ switch result {
+ case .success(let value):
+ guard let fields = value as? [String: Any], let svg = fields["svg"] as? String,
+ let width = fields["width"] as? Double, let height = fields["height"] as? Double,
+ width.isFinite, height.isFinite, width > 0, height > 0 else {
+ self.finish(.failure(DiagramRenderError.failed("Renderer returned invalid SVG dimensions.")), reset: true)
+ return
+ }
+ self.finish(.success(RenderedDiagram(svg: svg, size: CGSize(width: width, height: height))))
+ case .failure(let error):
+ let detail = (error as NSError).userInfo["WKJavaScriptExceptionMessage"] as? String
+ self.finish(.failure(DiagramRenderError.failed(detail ?? error.localizedDescription)), reset: true)
+ }
+ }
+ }
+
+ func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
+ finish(.failure(error), reset: true)
+ }
+ func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
+ finish(.failure(error), reset: true)
+ }
+ func webViewWebContentProcessDidTerminate(_ webView: WKWebView) {
+ finish(.failure(DiagramRenderError.failed("The diagram renderer stopped unexpectedly.")), reset: true)
+ }
+
+ private func finish(_ result: Result, reset: Bool = false) {
+ timeoutWork?.cancel()
+ timeoutWork = nil
+ let callback = completion
+ completion = nil
+ pendingSource = nil
+ if reset {
+ ready = false
+ webView?.navigationDelegate = nil
+ webView?.stopLoading()
+ window?.orderOut(nil)
+ window = nil
+ webView = nil
+ }
+ callback?(result)
+ }
+}
+
+/// SVG-as-image mode disables active SVG content and external subresources.
+enum SVGDocument {
+ static func configuration() -> WKWebViewConfiguration {
+ let config = WKWebViewConfiguration()
+ config.defaultWebpagePreferences.allowsContentJavaScript = false
+ return config
+ }
+ static func html(for diagram: RenderedDiagram) -> String {
+ let data = Data(diagram.svg.utf8).base64EncodedString()
+ return """
+
+
+
+
+ """
+ }
+
+}
diff --git a/QuickMD/QuickMD/DocumentSearch.swift b/QuickMD/QuickMD/DocumentSearch.swift
index efd5316..07604c8 100644
--- a/QuickMD/QuickMD/DocumentSearch.swift
+++ b/QuickMD/QuickMD/DocumentSearch.swift
@@ -107,7 +107,7 @@ enum DocumentSearch {
segments = [title]
case .mathBlock(let latex):
segments = [latex]
- case .mermaidDiagram(let source):
+ case .mermaidDiagram(let source), .diagram(_, let source):
segments = [source]
}
diff --git a/QuickMD/QuickMD/MarkdownBlock.swift b/QuickMD/QuickMD/MarkdownBlock.swift
index 2d5fec7..2578041 100644
--- a/QuickMD/QuickMD/MarkdownBlock.swift
+++ b/QuickMD/QuickMD/MarkdownBlock.swift
@@ -87,6 +87,7 @@ struct MarkdownBlock: Identifiable, Sendable {
case heading(level: Int, title: String, sourceLine: Int)
case mathBlock(latex: String)
case mermaidDiagram(source: String)
+ case diagram(kind: DiagramKind, source: String)
}
// Every factory takes `sourceLine` WITHOUT a default: the parser is the only
@@ -122,3 +123,43 @@ struct MarkdownBlock: Identifiable, Sendable {
MarkdownBlock(id: "mermaid-\(index)", content: .mermaidDiagram(source: source), sourceLine: sourceLine)
}
}
+
+/// Languages rendered by the bundled WebKit service. Raw values are fence names.
+enum DiagramKind: String, Sendable, CaseIterable {
+ case mermaid, bpmn, plantuml, svg
+
+ /// File formats supported by Markdown image links.
+ static func linkedKind(for url: URL) -> DiagramKind? {
+ switch url.pathExtension.lowercased() {
+ case "bpmn": return .bpmn
+ case "puml", "plantuml": return .plantuml
+ case "svg": return .svg
+ default: return nil
+ }
+ }
+
+ var title: String {
+ switch self {
+ case .mermaid: return "Mermaid diagram"
+ case .bpmn: return "BPMN diagram"
+ case .plantuml: return "PlantUML diagram"
+ case .svg: return "SVG image"
+ }
+ }
+}
+
+struct DiagramSource: Hashable, Sendable {
+ let kind: DiagramKind
+ let source: String
+ var isDark: Bool = false
+}
+
+extension MarkdownBlock {
+ var diagramSource: DiagramSource? {
+ switch content {
+ case .mermaidDiagram(let source): return DiagramSource(kind: .mermaid, source: source)
+ case .diagram(let kind, let source): return DiagramSource(kind: kind, source: source)
+ default: return nil
+ }
+ }
+}
diff --git a/QuickMD/QuickMD/MarkdownBlockParser.swift b/QuickMD/QuickMD/MarkdownBlockParser.swift
index e8f4d3c..0e7f53c 100644
--- a/QuickMD/QuickMD/MarkdownBlockParser.swift
+++ b/QuickMD/QuickMD/MarkdownBlockParser.swift
@@ -147,6 +147,10 @@ struct MarkdownBlockParser: Sendable {
// to the closing one.
if language.lowercased() == "mermaid" {
blocks.append(.mermaidDiagram(index: blockIndex, source: codeContent, sourceLine: sourceLine))
+ } else if let kind = DiagramKind(rawValue: language.lowercased() == "puml" ? "plantuml" : language.lowercased()) {
+ blocks.append(MarkdownBlock(id: "\(kind.rawValue)-\(blockIndex)",
+ content: .diagram(kind: kind, source: codeContent),
+ sourceLine: sourceLine))
} else {
blocks.append(.codeBlock(index: blockIndex, code: codeContent, language: language, sourceLine: sourceLine))
}
diff --git a/QuickMD/QuickMD/MarkdownExport.swift b/QuickMD/QuickMD/MarkdownExport.swift
index b98fc67..f1c3160 100644
--- a/QuickMD/QuickMD/MarkdownExport.swift
+++ b/QuickMD/QuickMD/MarkdownExport.swift
@@ -149,6 +149,9 @@ struct MarkdownPrintableView: View {
PrintableMathBlockView(latex: latex)
.padding(.vertical, 4)
+ case .diagram(let kind, let source):
+ PrintableCodeBlockView(code: source, language: kind.rawValue).padding(.vertical, 4)
+
case .mermaidDiagram(let source):
// Graceful degradation: render as styled code block in PDF
PrintableCodeBlockView(code: source, language: "mermaid")
@@ -336,9 +339,9 @@ struct PrintableImageView: View {
struct MarkdownPrintableBlockView: View {
let block: MarkdownBlock
- /// Pre-rendered Mermaid diagrams keyed by source (MermaidPDFRenderer).
+ /// Pre-rendered SVG diagrams keyed by language, source, and theme.
/// Sources without an entry fall back to the styled-code representation.
- var mermaidImages: [String: NSImage] = [:]
+ var diagramImages: [DiagramSource: NSImage] = [:]
private let theme = MarkdownTheme.exportTheme(for: .light)
private let renderer = MarkdownRenderer(colorScheme: .light)
@@ -371,8 +374,8 @@ struct MarkdownPrintableBlockView: View {
case .mathBlock(let latex):
PrintableMathBlockView(latex: latex)
- case .mermaidDiagram(let source):
- if let image = mermaidImages[source] {
+ case .mermaidDiagram, .diagram:
+ if let key = block.diagramSource, let image = diagramImages[key] {
Image(nsImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
@@ -380,7 +383,8 @@ struct MarkdownPrintableBlockView: View {
} else {
// Graceful degradation: styled code block (no snapshot
// available within the render budget, or render failed)
- PrintableCodeBlockView(code: source, language: "mermaid")
+ PrintableCodeBlockView(code: block.diagramSource?.source ?? "",
+ language: block.diagramSource?.kind.rawValue ?? "")
}
}
}
@@ -499,19 +503,12 @@ class PDFExportManager {
guard response == .OK, let url = savePanel.url else { return }
Task { @MainActor in
- // Pre-render Mermaid diagrams (cached inline snapshot or
- // offscreen WebView). Whatever fails within the time budget
- // keeps the styled-code fallback — export never stalls on it.
let blocks = MarkdownBlockParser(colorScheme: .light).parse(documentText)
- let mermaidSources = blocks.compactMap { block -> String? in
- if case .mermaidDiagram(let source) = block.content { return source }
- return nil
- }
- let mermaidImages = await MermaidPDFRenderer.renderAll(
- sources: mermaidSources, width: contentWidth)
+ let diagramImages = await DiagramPDFRenderer.renderAll(
+ sources: blocks.compactMap(\.diagramSource), width: contentWidth)
guard let pdfData = Self.generateMultiPagePDF(blocks: blocks,
- mermaidImages: mermaidImages) else {
+ diagramImages: diagramImages) else {
Self.showError("Failed to generate PDF")
return
}
@@ -541,7 +538,7 @@ class PDFExportManager {
}
static func generateMultiPagePDF(blocks: [MarkdownBlock],
- mermaidImages: [String: NSImage] = [:]) -> Data? {
+ diagramImages: [DiagramSource: NSImage] = [:]) -> Data? {
guard !blocks.isEmpty else {
logger.error("No blocks parsed from document")
return nil
@@ -551,7 +548,7 @@ class PDFExportManager {
var measuredBlocks: [(block: MarkdownBlock, size: CGSize)] = []
for block in blocks {
- let blockView = MarkdownPrintableBlockView(block: block, mermaidImages: mermaidImages)
+ let blockView = MarkdownPrintableBlockView(block: block, diagramImages: diagramImages)
.frame(width: contentWidth)
.fixedSize(horizontal: false, vertical: true)
@@ -610,7 +607,7 @@ class PDFExportManager {
// Draw each placed segment using the vector renderer
for placed in pageSegments {
let segment = placed.segment
- let blockView = MarkdownPrintableBlockView(block: segment.block, mermaidImages: mermaidImages)
+ let blockView = MarkdownPrintableBlockView(block: segment.block, diagramImages: diagramImages)
.frame(width: contentWidth)
.fixedSize(horizontal: false, vertical: true)
@@ -679,10 +676,15 @@ import PDFKit
class PrintManager {
static func printDocument(documentText: String) {
- guard !documentText.isEmpty else { return }
+ Task { @MainActor in await printRenderedDocument(documentText: documentText) }
+ }
- // Use the same multi-page PDF generation as export
- guard let pdfData = PDFExportManager.generateMultiPagePDF(documentText: documentText) else {
+ private static func printRenderedDocument(documentText: String) async {
+ guard !documentText.isEmpty else { return }
+ let blocks = MarkdownBlockParser(colorScheme: .light).parse(documentText)
+ let images = await DiagramPDFRenderer.renderAll(sources: blocks.compactMap(\.diagramSource),
+ width: PDFExportManager.contentWidth)
+ guard let pdfData = PDFExportManager.generateMultiPagePDF(blocks: blocks, diagramImages: images) else {
showError("Failed to render document for printing")
return
}
diff --git a/QuickMD/QuickMD/MarkdownView.swift b/QuickMD/QuickMD/MarkdownView.swift
index 63c4d35..633c32c 100644
--- a/QuickMD/QuickMD/MarkdownView.swift
+++ b/QuickMD/QuickMD/MarkdownView.swift
@@ -46,6 +46,7 @@ struct MarkdownView: View {
@State private var currentMatchIndex: Int = 0
@State private var matchBlockIds: [String] = []
@State private var scrollTrigger: Int = 0
+ @State private var graphicPreview: GraphicPreview?
@State private var keyMonitor: Any?
/// The NSWindow hosting this view (set by `WindowConfigurator`); the key
/// monitor uses it to ignore events addressed to other tabs' windows.
@@ -362,6 +363,13 @@ struct MarkdownView: View {
/// modifier chain no longer type-checks inside the compiler's budget.
private var configuredDocumentStack: some View {
documentStack
+ .disabled(graphicPreview != nil)
+ .accessibilityHidden(graphicPreview != nil)
+ .overlay {
+ if let graphicPreview {
+ GraphicPreviewOverlay(preview: graphicPreview) { self.graphicPreview = nil }
+ }
+ }
.background(theme.backgroundColor)
.background(WindowConfigurator { window in
// Make every QuickMD document window prefer to join existing windows
@@ -557,6 +565,13 @@ struct MarkdownView: View {
if let hostWindow, let eventWindow = event.window, eventWindow !== hostWindow {
return event
}
+ if graphicPreview != nil {
+ if event.keyCode == 53 {
+ graphicPreview = nil
+ return nil
+ }
+ return event
+ }
let flags = event.modifierFlags.intersection(.deviceIndependentFlagsMask)
if flags.contains(.command) && event.charactersIgnoringModifiers == "g" {
@@ -680,7 +695,9 @@ struct MarkdownView: View {
.padding(.vertical, Metrics.codeOuterVerticalPadding)
case .image(let url, let alt):
- ImageBlockView(url: url, alt: alt, theme: theme, documentURL: documentURL)
+ ImageBlockView(url: url, alt: alt, theme: theme, documentURL: documentURL,
+ fontScale: scale, contentWidth: contentWidth,
+ onEnlarge: { graphicPreview = $0 })
.padding(.vertical, Metrics.imageOuterVerticalPadding)
case .blockquote(let content, let level):
@@ -720,8 +737,15 @@ struct MarkdownView: View {
.padding(.vertical, Metrics.mathOuterVerticalPadding)
case .mermaidDiagram(let source):
- MermaidBlockView(blockId: block.id, source: source, theme: theme,
- heightCache: heightCache)
+ DiagramBlockView(source: DiagramSource(kind: .mermaid, source: source, isDark: theme.isDark),
+ fontScale: scale, contentWidth: contentWidth,
+ onEnlarge: { graphicPreview = $0 })
+ .padding(.vertical, Metrics.mermaidOuterVerticalPadding)
+
+ case .diagram(let kind, let source):
+ DiagramBlockView(source: DiagramSource(kind: kind, source: source, isDark: theme.isDark),
+ fontScale: scale, contentWidth: contentWidth,
+ onEnlarge: { graphicPreview = $0 })
.padding(.vertical, Metrics.mermaidOuterVerticalPadding)
}
}
diff --git a/QuickMD/QuickMD/MermaidPDFRenderer.swift b/QuickMD/QuickMD/MermaidPDFRenderer.swift
deleted file mode 100644
index 19fb1ee..0000000
--- a/QuickMD/QuickMD/MermaidPDFRenderer.swift
+++ /dev/null
@@ -1,161 +0,0 @@
-import AppKit
-import WebKit
-
-// MARK: - Mermaid PDF Renderer
-
-/// Renders Mermaid diagrams to bitmaps for PDF export, independent of the
-/// document view. Fast path: reuse the light-mode snapshot already cached by
-/// the inline MermaidBlockView. Slow path: offscreen WKWebView loading the
-/// bundled template — the diagram source travels as JSON via
-/// `evaluateJavaScript`, NEVER concatenated into the HTML (constraints.md).
-/// Any failure (timeout, mermaid parse error) yields no image and the caller
-/// keeps the styled-code fallback, so export can never get stuck or embed an
-/// error screenshot.
-@MainActor
-final class MermaidPDFRenderer {
-
- /// Renders every distinct source, returning [source: image]. Sequential —
- /// documents rarely hold more than a handful of diagrams, and one WebView
- /// at a time keeps memory flat. `totalBudget` caps the whole document so a
- /// file full of broken diagrams cannot stall the export.
- static func renderAll(sources: [String], width: CGFloat,
- totalBudget: TimeInterval = 20) async -> [String: NSImage] {
- var result: [String: NSImage] = [:]
- guard !sources.isEmpty else { return result }
- let deadline = Date().addingTimeInterval(totalBudget)
-
- for source in sources where result[source] == nil {
- if let cached = MermaidSnapshotStore.image(source: source, isDark: false) {
- result[source] = cached
- continue
- }
- let remaining = deadline.timeIntervalSinceNow
- guard remaining > 1 else { continue }
- if let image = await renderOffscreen(source: source, width: width,
- timeout: min(8, remaining)) {
- result[source] = image
- MermaidSnapshotStore.set(image, source: source, isDark: false)
- }
- }
- return result
- }
-
- /// Keeps in-flight renders alive until their completion fires.
- private static var active: Set = []
-
- private static func renderOffscreen(source: String, width: CGFloat,
- timeout: TimeInterval) async -> NSImage? {
- await withCheckedContinuation { continuation in
- var render: OffscreenMermaidRender!
- render = OffscreenMermaidRender(source: source, width: width, timeout: timeout) { image in
- Self.active.remove(render)
- continuation.resume(returning: image)
- }
- Self.active.insert(render)
- render.start()
- }
- }
-}
-
-// MARK: - Offscreen Render (one diagram)
-
-/// One offscreen render pass: borderless window far outside the visible area
-/// hosts the WebView (WebKit won't paint — and takeSnapshot returns blank —
-/// for a view that isn't in any window). Flow: load template → didFinish →
-/// renderDiagram(JSON) → template posts height → verify an SVG actually
-/// rendered (the template shows error text and STILL posts height on failure)
-/// → resize → snapshot. A single timeout covers the whole flow.
-private final class OffscreenMermaidRender: NSObject, WKNavigationDelegate, WKScriptMessageHandler {
- private let source: String
- private let width: CGFloat
- private let timeout: TimeInterval
- private let completion: (NSImage?) -> Void
-
- private var window: NSWindow?
- private var webView: WKWebView?
- private var finished = false
-
- init(source: String, width: CGFloat, timeout: TimeInterval,
- completion: @escaping (NSImage?) -> Void) {
- self.source = source
- self.width = width
- self.timeout = timeout
- self.completion = completion
- }
-
- func start() {
- let config = WKWebViewConfiguration()
- config.userContentController.add(self, name: "mermaidHeight")
-
- let webView = WKWebView(frame: NSRect(x: 0, y: 0, width: width, height: 600),
- configuration: config)
- webView.setValue(false, forKey: "drawsBackground")
- webView.navigationDelegate = self
- self.webView = webView
-
- let window = NSWindow(contentRect: NSRect(x: -20000, y: -20000, width: width, height: 600),
- styleMask: [.borderless], backing: .buffered, defer: false)
- window.isReleasedWhenClosed = false
- window.contentView = webView
- window.orderBack(nil)
- self.window = window
-
- guard let templateURL = Bundle.main.url(forResource: "mermaid-template", withExtension: "html"),
- let templateHTML = try? String(contentsOf: templateURL, encoding: .utf8) else {
- finish(nil)
- return
- }
- webView.loadHTMLString(templateHTML, baseURL: Bundle.main.resourceURL)
-
- DispatchQueue.main.asyncAfter(deadline: .now() + timeout) { [weak self] in
- self?.finish(nil)
- }
- }
-
- func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
- guard let data = try? JSONSerialization.data(withJSONObject: source, options: [.fragmentsAllowed]),
- let jsLiteral = String(data: data, encoding: .utf8) else {
- finish(nil)
- return
- }
- webView.evaluateJavaScript("renderDiagram(\(jsLiteral), false);", completionHandler: nil)
- }
-
- func userContentController(_ controller: WKUserContentController,
- didReceive message: WKScriptMessage) {
- guard !finished, let webView, let height = message.body as? Double, height > 10 else { return }
-
- // The template posts height for failed renders too (it shows the error
- // text inline). Only snapshot when an actual SVG landed — otherwise
- // fall back to the styled-code block instead of a screenshot of an error.
- webView.evaluateJavaScript("document.querySelector('#output svg') !== null") { [weak self] value, _ in
- guard let self, !self.finished else { return }
- guard let hasSVG = value as? Bool, hasSVG else {
- self.finish(nil)
- return
- }
- let size = NSSize(width: self.width, height: CGFloat(height))
- webView.setFrameSize(size)
- self.window?.setContentSize(size)
- // Small delay lets WebKit finish painting the freshly inserted SVG
- // (same 0.4s the inline snapshot path uses).
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.45) { [weak self] in
- guard let self, !self.finished, let webView = self.webView else { return }
- webView.takeSnapshot(with: WKSnapshotConfiguration()) { [weak self] image, _ in
- self?.finish(image)
- }
- }
- }
- }
-
- private func finish(_ image: NSImage?) {
- guard !finished else { return }
- finished = true
- // The message-handler registration retains self — break the cycle.
- webView?.configuration.userContentController.removeScriptMessageHandler(forName: "mermaidHeight")
- window?.orderOut(nil)
- window = nil
- webView = nil
- completion(image)
- }
-}
diff --git a/QuickMD/QuickMD/Resources/Diagrams/render.html b/QuickMD/QuickMD/Resources/Diagrams/render.html
new file mode 100644
index 0000000..6770751
--- /dev/null
+++ b/QuickMD/QuickMD/Resources/Diagrams/render.html
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/QuickMD/QuickMD/Resources/Diagrams/render.js b/QuickMD/QuickMD/Resources/Diagrams/render.js
new file mode 100644
index 0000000..4063264
--- /dev/null
+++ b/QuickMD/QuickMD/Resources/Diagrams/render.js
@@ -0,0 +1,95 @@
+/* All renderers run locally. Source is passed as data by callAsyncJavaScript. */
+'use strict';
+const libraries = new Map();
+function loadScript(path) {
+ if (!libraries.has(path)) libraries.set(path, new Promise((resolve, reject) => {
+ const script = document.createElement('script');
+ script.src = path;
+ script.onload = resolve;
+ script.onerror = () => reject(new Error('Unable to load bundled renderer: ' + path));
+ document.head.appendChild(script);
+ }));
+ return libraries.get(path);
+}
+function dimension(value, fallback) {
+ const match = String(value || '').match(/^\s*([\d.]+)\s*(px|pt|in|cm|mm)?\s*$/);
+ return match ? Number(match[1]) * ({px:1,pt:96/72,in:96,cm:96/2.54,mm:96/25.4}[match[2] || 'px']) : fallback;
+}
+function svgDocument(text) {
+ const doc = new DOMParser().parseFromString(text, 'image/svg+xml');
+ const root = doc.documentElement;
+ if (doc.querySelector('parsererror') || root.localName !== 'svg' || root.namespaceURI !== 'http://www.w3.org/2000/svg') {
+ throw new Error('Invalid SVG document.');
+ }
+ let box = (root.getAttribute('viewBox') || '').trim().split(/[\s,]+/).map(Number);
+ if (box.length !== 4 || !box.every(Number.isFinite) || box[2] <= 0 || box[3] <= 0) {
+ box = [0, 0, dimension(root.getAttribute('width'), 300), dimension(root.getAttribute('height'), 150)];
+ if (!(box[2] > 0 && box[3] > 0)) throw new Error('SVG must have positive dimensions.');
+ root.setAttribute('viewBox', box.join(' '));
+ }
+ // Normalize physical units and cap the intrinsic size while preserving the viewBox.
+ const scale = Math.min(1, 32768 / Math.max(box[2], box[3]));
+ root.setAttribute('width', box[2] * scale);
+ root.setAttribute('height', box[3] * scale);
+ return {doc, root, width: box[2] * scale, height: box[3] * scale};
+}
+window.quickmdRender = async function(kind, source, isDark) {
+ const work = document.getElementById('work');
+ work.replaceChildren();
+ let svg;
+ if (kind === 'mermaid') {
+ await loadScript('../mermaid.min.js');
+ mermaid.initialize({startOnLoad:false, securityLevel:'strict', theme:isDark ? 'dark' : 'default',
+ fontFamily:'-apple-system, BlinkMacSystemFont, sans-serif'});
+ svg = (await mermaid.render('quickmd-mermaid', source)).svg;
+ } else if (kind === 'plantuml') {
+ await loadScript('vendor/viz-global.js');
+ await loadScript('vendor/plantuml.js');
+ // The engine resolves bundled themes/icons relative to document.baseURI.
+ await loadScript('vendor/emoji.js');
+ await loadScript('vendor/openiconic.js');
+ await loadScript('vendor/themes.js');
+ svg = await new Promise((resolve, reject) => {
+ PlantUML.renderToString(source.split(/\r\n|\r|\n/), resolve,
+ message => reject(new Error(message)), {dark:isDark});
+ });
+ } else if (kind === 'bpmn') {
+ await loadScript('vendor/bpmn-viewer.js');
+ const viewer = new BpmnJS({container:work});
+ try {
+ await viewer.importXML(source);
+ svg = (await viewer.saveSVG()).svg;
+ // BPMN labels are dark by default, including labels outside shapes.
+ // Keep a white canvas so they remain readable in dark document themes.
+
+ // Preserve the original viewer's unmodified attribution artwork in a
+ // separate footer, so neither the process nor a preview can cover it.
+ const parsed = svgDocument(svg);
+ const logo = work.querySelector('.bjs-powered-by svg');
+ if (logo) {
+ const box = parsed.root.getAttribute('viewBox').split(/[\s,]+/).map(Number);
+ const footer = parsed.doc.createElementNS('http://www.w3.org/2000/svg', 'a');
+ footer.setAttribute('href', 'https://bpmn.io');
+ const mark = parsed.doc.importNode(logo, true);
+ const width = Math.max(box[2], 120);
+ mark.setAttribute('x', box[0] + width - 65);
+ mark.setAttribute('y', box[1] + box[3] + 12);
+ mark.setAttribute('width', '50'); mark.setAttribute('height', '22');
+ footer.appendChild(mark); parsed.root.appendChild(footer);
+ parsed.root.setAttribute('viewBox', [box[0], box[1], width, box[3] + 46].join(' '));
+ const background = parsed.doc.createElementNS('http://www.w3.org/2000/svg', 'rect');
+ background.setAttribute('x', box[0]); background.setAttribute('y', box[1]);
+ background.setAttribute('width', width); background.setAttribute('height', box[3] + 46);
+ background.setAttribute('fill', '#fff');
+ parsed.root.insertBefore(background, parsed.root.firstChild);
+ svg = new XMLSerializer().serializeToString(parsed.root);
+ }
+ } finally { viewer.destroy(); }
+ } else if (kind === 'svg') {
+ svg = source;
+ } else { throw new Error('Unsupported diagram type: ' + kind); }
+ const parsed = svgDocument(svg);
+ // SVG never enters the display DOM. The viewer uses an data URL,
+ // where WebKit disables SVG scripts, event handlers and external resources.
+ return { svg: new XMLSerializer().serializeToString(parsed.root), width:parsed.width, height:parsed.height };
+};
diff --git a/QuickMD/QuickMD/Resources/Diagrams/vendor/BPMN-LICENSE.txt b/QuickMD/QuickMD/Resources/Diagrams/vendor/BPMN-LICENSE.txt
new file mode 100644
index 0000000..b20e936
--- /dev/null
+++ b/QuickMD/QuickMD/Resources/Diagrams/vendor/BPMN-LICENSE.txt
@@ -0,0 +1,23 @@
+Copyright (c) 2014-present Camunda Services GmbH
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in the
+Software without restriction, including without limitation the rights to use, copy,
+modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
+and to permit persons to whom the Software is furnished to do so, subject to the
+following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+The source code responsible for displaying the bpmn.io project watermark that
+links back to https://bpmn.io as part of rendered diagrams MUST NOT be
+removed or changed. When this software is being used in a website or application,
+the watermark must stay fully visible and not visually overlapped by other elements.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
+OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/QuickMD/QuickMD/Resources/Diagrams/vendor/Expat-LICENSE.txt b/QuickMD/QuickMD/Resources/Diagrams/vendor/Expat-LICENSE.txt
new file mode 100644
index 0000000..c6d184a
--- /dev/null
+++ b/QuickMD/QuickMD/Resources/Diagrams/vendor/Expat-LICENSE.txt
@@ -0,0 +1,21 @@
+Copyright (c) 1998-2000 Thai Open Source Software Center Ltd and Clark Cooper
+Copyright (c) 2001-2025 Expat maintainers
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/QuickMD/QuickMD/Resources/Diagrams/vendor/Graphviz-LICENSE.txt b/QuickMD/QuickMD/Resources/Diagrams/vendor/Graphviz-LICENSE.txt
new file mode 100644
index 0000000..11ecb79
--- /dev/null
+++ b/QuickMD/QuickMD/Resources/Diagrams/vendor/Graphviz-LICENSE.txt
@@ -0,0 +1,198 @@
+Eclipse Public License - v 1.0
+
+THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC
+LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM
+CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
+
+1. DEFINITIONS
+
+"Contribution" means:
+
+a) in the case of the initial Contributor, the initial code and documentation
+ distributed under this Agreement, and
+b) in the case of each subsequent Contributor:
+ i) changes to the Program, and
+ ii) additions to the Program;
+
+ where such changes and/or additions to the Program originate from and are
+ distributed by that particular Contributor. A Contribution 'originates' from
+ a Contributor if it was added to the Program by such Contributor itself or
+ anyone acting on such Contributor's behalf. Contributions do not include
+ additions to the Program which: (i) are separate modules of software
+ distributed in conjunction with the Program under their own license
+ agreement, and (ii) are not derivative works of the Program.
+
+"Contributor" means any person or entity that distributes the Program.
+
+"Licensed Patents" mean patent claims licensable by a Contributor which are
+necessarily infringed by the use or sale of its Contribution alone or when
+combined with the Program.
+
+"Program" means the Contributions distributed in accordance with this Agreement.
+
+"Recipient" means anyone who receives the Program under this Agreement,
+including all Contributors.
+
+2. GRANT OF RIGHTS
+ a) Subject to the terms of this Agreement, each Contributor hereby grants
+ Recipient a non-exclusive, worldwide, royalty-free copyright license to
+ reproduce, prepare derivative works of, publicly display, publicly perform,
+ distribute and sublicense the Contribution of such Contributor, if any, and
+ such derivative works, in source code and object code form.
+ b) Subject to the terms of this Agreement, each Contributor hereby grants
+ Recipient a non-exclusive, worldwide, royalty-free patent license under
+ Licensed Patents to make, use, sell, offer to sell, import and otherwise
+ transfer the Contribution of such Contributor, if any, in source code and
+ object code form. This patent license shall apply to the combination of the
+ Contribution and the Program if, at the time the Contribution is added by
+ the Contributor, such addition of the Contribution causes such combination
+ to be covered by the Licensed Patents. The patent license shall not apply
+ to any other combinations which include the Contribution. No hardware per
+ se is licensed hereunder.
+ c) Recipient understands that although each Contributor grants the licenses to
+ its Contributions set forth herein, no assurances are provided by any
+ Contributor that the Program does not infringe the patent or other
+ intellectual property rights of any other entity. Each Contributor
+ disclaims any liability to Recipient for claims brought by any other entity
+ based on infringement of intellectual property rights or otherwise. As a
+ condition to exercising the rights and licenses granted hereunder, each
+ Recipient hereby assumes sole responsibility to secure any other
+ intellectual property rights needed, if any. For example, if a third party
+ patent license is required to allow Recipient to distribute the Program, it
+ is Recipient's responsibility to acquire that license before distributing
+ the Program.
+ d) Each Contributor represents that to its knowledge it has sufficient
+ copyright rights in its Contribution, if any, to grant the copyright
+ license set forth in this Agreement.
+
+3. REQUIREMENTS
+
+A Contributor may choose to distribute the Program in object code form under its
+own license agreement, provided that:
+
+ a) it complies with the terms and conditions of this Agreement; and
+ b) its license agreement:
+ i) effectively disclaims on behalf of all Contributors all warranties and
+ conditions, express and implied, including warranties or conditions of
+ title and non-infringement, and implied warranties or conditions of
+ merchantability and fitness for a particular purpose;
+ ii) effectively excludes on behalf of all Contributors all liability for
+ damages, including direct, indirect, special, incidental and
+ consequential damages, such as lost profits;
+ iii) states that any provisions which differ from this Agreement are offered
+ by that Contributor alone and not by any other party; and
+ iv) states that source code for the Program is available from such
+ Contributor, and informs licensees how to obtain it in a reasonable
+ manner on or through a medium customarily used for software exchange.
+
+When the Program is made available in source code form:
+
+ a) it must be made available under this Agreement; and
+ b) a copy of this Agreement must be included with each copy of the Program.
+ Contributors may not remove or alter any copyright notices contained within
+ the Program.
+
+Each Contributor must identify itself as the originator of its Contribution, if
+any, in a manner that reasonably allows subsequent Recipients to identify the
+originator of the Contribution.
+
+4. COMMERCIAL DISTRIBUTION
+
+Commercial distributors of software may accept certain responsibilities with
+respect to end users, business partners and the like. While this license is
+intended to facilitate the commercial use of the Program, the Contributor who
+includes the Program in a commercial product offering should do so in a manner
+which does not create potential liability for other Contributors. Therefore, if
+a Contributor includes the Program in a commercial product offering, such
+Contributor ("Commercial Contributor") hereby agrees to defend and indemnify
+every other Contributor ("Indemnified Contributor") against any losses, damages
+and costs (collectively "Losses") arising from claims, lawsuits and other legal
+actions brought by a third party against the Indemnified Contributor to the
+extent caused by the acts or omissions of such Commercial Contributor in
+connection with its distribution of the Program in a commercial product
+offering. The obligations in this section do not apply to any claims or Losses
+relating to any actual or alleged intellectual property infringement. In order
+to qualify, an Indemnified Contributor must: a) promptly notify the Commercial
+Contributor in writing of such claim, and b) allow the Commercial Contributor to
+control, and cooperate with the Commercial Contributor in, the defense and any
+related settlement negotiations. The Indemnified Contributor may participate in
+any such claim at its own expense.
+
+For example, a Contributor might include the Program in a commercial product
+offering, Product X. That Contributor is then a Commercial Contributor. If that
+Commercial Contributor then makes performance claims, or offers warranties
+related to Product X, those performance claims and warranties are such
+Commercial Contributor's responsibility alone. Under this section, the
+Commercial Contributor would have to defend claims against the other
+Contributors related to those performance claims and warranties, and if a court
+requires any other Contributor to pay any damages as a result, the Commercial
+Contributor must pay those damages.
+
+5. NO WARRANTY
+
+EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR
+IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE,
+NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each
+Recipient is solely responsible for determining the appropriateness of using and
+distributing the Program and assumes all risks associated with its exercise of
+rights under this Agreement , including but not limited to the risks and costs
+of program errors, compliance with applicable laws, damage to or loss of data,
+programs or equipment, and unavailability or interruption of operations.
+
+6. DISCLAIMER OF LIABILITY
+
+EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY
+CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST
+PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS
+GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+
+7. GENERAL
+
+If any provision of this Agreement is invalid or unenforceable under applicable
+law, it shall not affect the validity or enforceability of the remainder of the
+terms of this Agreement, and without further action by the parties hereto, such
+provision shall be reformed to the minimum extent necessary to make such
+provision valid and enforceable.
+
+If Recipient institutes patent litigation against any entity (including a
+cross-claim or counterclaim in a lawsuit) alleging that the Program itself
+(excluding combinations of the Program with other software or hardware)
+infringes such Recipient's patent(s), then such Recipient's rights granted under
+Section 2(b) shall terminate as of the date such litigation is filed.
+
+All Recipient's rights under this Agreement shall terminate if it fails to
+comply with any of the material terms or conditions of this Agreement and does
+not cure such failure in a reasonable period of time after becoming aware of
+such noncompliance. If all Recipient's rights under this Agreement terminate,
+Recipient agrees to cease use and distribution of the Program as soon as
+reasonably practicable. However, Recipient's obligations under this Agreement
+and any licenses granted by Recipient relating to the Program shall continue and
+survive.
+
+Everyone is permitted to copy and distribute copies of this Agreement, but in
+order to avoid inconsistency the Agreement is copyrighted and may only be
+modified in the following manner. The Agreement Steward reserves the right to
+publish new versions (including revisions) of this Agreement from time to time.
+No one other than the Agreement Steward has the right to modify this Agreement.
+The Eclipse Foundation is the initial Agreement Steward. The Eclipse Foundation
+may assign the responsibility to serve as the Agreement Steward to a suitable
+separate entity. Each new version of the Agreement will be given a
+distinguishing version number. The Program (including Contributions) may always
+be distributed subject to the version of the Agreement under which it was
+received. In addition, after a new version of the Agreement is published,
+Contributor may elect to distribute the Program (including its Contributions)
+under the new version. Except as expressly stated in Sections 2(a) and 2(b)
+above, Recipient receives no rights or licenses to the intellectual property of
+any Contributor under this Agreement, whether expressly, by implication,
+estoppel or otherwise. All rights in the Program not expressly granted under
+this Agreement are reserved.
+
+This Agreement is governed by the laws of the State of New York and the
+intellectual property laws of the United States of America. No party to this
+Agreement will bring a legal action under this Agreement more than one year
+after the cause of action arose. Each party waives its rights to a jury trial in
+any resulting litigation.
diff --git a/QuickMD/QuickMD/Resources/Diagrams/vendor/PlantUML-LICENSE.txt b/QuickMD/QuickMD/Resources/Diagrams/vendor/PlantUML-LICENSE.txt
new file mode 100644
index 0000000..84a21eb
--- /dev/null
+++ b/QuickMD/QuickMD/Resources/Diagrams/vendor/PlantUML-LICENSE.txt
@@ -0,0 +1,73 @@
+/* +=======================================================================
+ * |
+ * | PlantUML : a free UML diagram generator
+ * |
+ * +=======================================================================
+ *
+ * (C) Copyright 2009-2024, Arnaud Roques
+ *
+ * Project Info: https://plantuml.com
+ *
+ * If you like this project or if you find it useful, you can support us at:
+ *
+ * https://plantuml.com/patreon (only 1$ per month!)
+ * https://plantuml.com/liberapay (only 1€ per month!)
+ * https://plantuml.com/paypal
+ *
+ *
+ * PlantUML is free software; you can redistribute it and/or modify it
+ * under the terms of the MIT License.
+ *
+ * See http://opensource.org/licenses/MIT
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
+ * IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * PlantUML can occasionally display sponsored or advertising messages. Those
+ * messages are usually generated on welcome or error images and never on
+ * functional diagrams.
+ * See https://plantuml.com/professional if you want to remove them
+ *
+ * Images (whatever their format : PNG, SVG, EPS...) generated by running PlantUML
+ * are owned by the author of their corresponding sources code (that is, their
+ * textual description in PlantUML language). Those images are not covered by
+ * this MIT license.
+ *
+ * The generated images can then be used without any reference to the MIT license.
+ * It is not even necessary to stipulate that they have been generated with PlantUML,
+ * although this will be appreciated by the PlantUML team.
+ *
+ * There is an exception : if the textual description in PlantUML language is also covered
+ * by any license, then the generated images are logically covered
+ * by the very same license.
+ *
+ * This is the IGY distribution (Install GraphViz by Yourself).
+ * You have to install GraphViz and to setup the GRAPHVIZ_DOT environment variable
+ * (see https://plantuml.com/graphviz-dot )
+ *
+ * Icons provided by OpenIconic : https://useiconic.com/open
+ * Archimate sprites provided by Archi : http://www.archimatetool.com
+ * Stdlib AWS provided by https://github.com/milo-minderbinder/AWS-PlantUML
+ * Stdlib Icons provided https://github.com/tupadr3/plantuml-icon-font-sprites
+ * ASCIIMathML (c) Peter Jipsen http://www.chapman.edu/~jipsen
+ * ASCIIMathML (c) David Lippman http://www.pierce.ctc.edu/dlippman
+ * CafeUndZopfli ported by Eugene Klyuchnikov https://github.com/eustas/CafeUndZopfli
+ * Brotli (c) by the Brotli Authors https://github.com/google/brotli
+ * Themes (c) by Brett Schwarz https://github.com/bschwarz/puml-themes
+ * Twemoji (c) by Twitter at https://twemoji.twitter.com/
+ *
+ */
\ No newline at end of file
diff --git a/QuickMD/QuickMD/Resources/Diagrams/vendor/Viz-LICENSE.txt b/QuickMD/QuickMD/Resources/Diagrams/vendor/Viz-LICENSE.txt
new file mode 100644
index 0000000..214cfc3
--- /dev/null
+++ b/QuickMD/QuickMD/Resources/Diagrams/vendor/Viz-LICENSE.txt
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) Michael Daines
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/QuickMD/QuickMD/Resources/Diagrams/vendor/bpmn-js.css b/QuickMD/QuickMD/Resources/Diagrams/vendor/bpmn-js.css
new file mode 100644
index 0000000..8990267
--- /dev/null
+++ b/QuickMD/QuickMD/Resources/Diagrams/vendor/bpmn-js.css
@@ -0,0 +1,160 @@
+.bjs-container {
+ --bjs-font-family: Arial, sans-serif;
+
+ --color-grey-225-10-15: hsl(225, 10%, 15%);
+ --color-grey-225-10-35: hsl(225, 10%, 35%);
+ --color-grey-225-10-55: hsl(225, 10%, 55%);
+ --color-grey-225-10-75: hsl(225, 10%, 75%);
+ --color-grey-225-10-80: hsl(225, 10%, 80%);
+ --color-grey-225-10-85: hsl(225, 10%, 85%);
+ --color-grey-225-10-90: hsl(225, 10%, 90%);
+ --color-grey-225-10-95: hsl(225, 10%, 95%);
+ --color-grey-225-10-97: hsl(225, 10%, 97%);
+
+ --color-blue-205-100-40: hsl(205, 100%, 40%);
+ --color-blue-205-100-45: hsl(205, 100%, 45%);
+ --color-blue-205-100-45-opacity-30: hsla(205, 100%, 45%, 30%);
+ --color-blue-205-100-50: hsl(205, 100%, 50%);
+ --color-blue-205-100-95: hsl(205, 100%, 95%);
+
+ --color-green-150-86-44: hsl(150, 86%, 44%);
+
+ --color-red-360-100-40: hsl(360, 100%, 40%);
+ --color-red-360-100-45: hsl(360, 100%, 45%);
+ --color-red-360-100-92: hsl(360, 100%, 92%);
+ --color-red-360-100-97: hsl(360, 100%, 97%);
+
+ --color-white: hsl(0, 0%, 100%);
+ --color-black: hsl(0, 0%, 0%);
+ --color-black-opacity-05: hsla(0, 0%, 0%, 5%);
+ --color-black-opacity-10: hsla(0, 0%, 0%, 10%);
+
+ --accent-color: var(--color-blue-205-100-40);
+
+ --breadcrumbs-font-family: var(--bjs-font-family);
+ --breadcrumbs-item-color: var(--accent-color);
+ --breadcrumbs-arrow-color: var(--color-black);
+ --breadcrumbs-background-color: var(--color-grey-225-10-97);
+ --breadcrumbs-border-color: var(--color-grey-225-10-75);
+ --breadcrumbs-focus-outline-color: var(--accent-color);
+ --drilldown-fill-color: var(--color-white);
+ --drilldown-background-color: var(--accent-color);
+ --drilldown-focus-outline-color: var(--accent-color);
+}
+
+.bjs-breadcrumbs {
+ position: absolute;
+ display: none;
+ flex-wrap: wrap;
+ align-items: center;
+ top: 20px;
+ left: 20px;
+ padding: 5px 10px;
+ margin: 0px;
+ font-family: var(--breadcrumbs-font-family);
+ font-size: 16px;
+ line-height: normal;
+ background-color: var(--breadcrumbs-background-color);
+ border: solid 1px var(--breadcrumbs-border-color);
+ border-radius: 2px;
+ row-gap: 5px;
+}
+
+.bjs-breadcrumbs-shown .bjs-breadcrumbs {
+ display: flex;
+}
+
+.djs-palette-shown .bjs-breadcrumbs {
+ left: 90px;
+}
+
+.djs-palette-shown.djs-palette-two-column .bjs-breadcrumbs {
+ left: 140px;
+}
+
+.bjs-breadcrumbs li {
+ display: inline-flex;
+ align-items: center;
+}
+
+.bjs-breadcrumbs a.bjs-crumb {
+ cursor: pointer;
+ color: var(--breadcrumbs-item-color);
+ text-decoration: none;
+}
+
+.bjs-breadcrumbs a.bjs-crumb:focus-visible {
+ outline: solid 1px var(--breadcrumbs-focus-outline-color);
+ outline-offset: 1px;
+}
+
+.bjs-breadcrumbs li:not(:first-child)::before {
+ content: url('data:image/svg+xml;utf8,');
+ padding: 0 8px;
+ color: var(--breadcrumbs-arrow-color);
+ height: 1em;
+}
+
+.bjs-breadcrumbs .bjs-crumb {
+ display: inline-block;
+ max-width: 200px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.bjs-drilldown {
+ width: 20px;
+ height: 20px;
+
+ padding: 0px;
+ margin-left: -20px;
+
+ cursor: pointer;
+ border: none;
+ border-radius: 2px;
+
+ fill: var(--drilldown-fill-color);
+ background-color: var(--drilldown-background-color);
+}
+
+.bjs-drilldown:focus-visible {
+ outline: solid 1px var(--drilldown-focus-outline-color);
+ outline-offset: 1px;
+}
+
+.bjs-drilldown-empty {
+ display: none;
+}
+
+.selected .bjs-drilldown-empty {
+ display: inherit;
+}
+
+[data-popup="align-elements"] .djs-popup-results {
+ display: flex;
+}
+
+[data-popup="align-elements"] .djs-popup-body [data-group] + [data-group] {
+ border-left: 1px solid var(--popup-border-color);
+}
+
+[data-popup="align-elements"] [data-group="align"] {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+}
+
+[data-popup="align-elements"] .djs-popup-body .entry {
+ padding: 6px 8px;
+}
+
+[data-popup="align-elements"] .djs-popup-body .entry:not(:first-child) {
+ margin-top: 0;
+}
+
+[data-popup="align-elements"] .djs-popup-entry-icon {
+ display: block;
+ margin: 0;
+ height: 20px;
+ width: 20px;
+}
diff --git a/QuickMD/QuickMD/Resources/Diagrams/vendor/bpmn-viewer.js b/QuickMD/QuickMD/Resources/Diagrams/vendor/bpmn-viewer.js
new file mode 100644
index 0000000..ab8835b
--- /dev/null
+++ b/QuickMD/QuickMD/Resources/Diagrams/vendor/bpmn-viewer.js
@@ -0,0 +1,22 @@
+/*! bpmn-js - bpmn-viewer v18.28.0 | Copyright (c) 2014-present, camunda Services GmbH | bpmn.io/license */
+!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).BpmnJS=t()}(this,(function(){"use strict";function e(e,t){t&&(e.super_=t,e.prototype=Object.create(t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}))}const t=Object.prototype.toString,n=Object.prototype.hasOwnProperty;function i(e){return void 0===e}function r(e){return void 0!==e}function o(e){return null==e}function a(e){return"[object Array]"===t.call(e)}function s(e){return"[object Object]"===t.call(e)}function l(e){return"[object Number]"===t.call(e)}function p(e){const n=t.call(e);return"[object Function]"===n||"[object AsyncFunction]"===n||"[object GeneratorFunction]"===n||"[object AsyncGeneratorFunction]"===n||"[object Proxy]"===n}function c(e){return"[object String]"===t.call(e)}function u(e,t){return!o(e)&&n.call(e,t)}function h(e,t){const n=x(t);let i;return m(e,(function(e,t){if(n(e,t))return i=e,!1})),i}function f(e,t){const n=x(t);let i=[];return m(e,(function(e,t){n(e,t)&&i.push(e)})),i}function m(e,t){let n,r;if(i(e))return;const o=a(e)?b:w;for(let i in e)if(u(e,i)&&(n=e[i],r=t(n,o(i)),!1===r))return n}function d(e,t,n){return m(e,(function(e,i){n=t(n,e,i)})),n}function y(e,t){return!!d(e,(function(e,n,i){return e&&t(n,i)}),!0)}function g(e,t){return!!h(e,t)}function v(e){return function(t){return y(e,(function(e,n){return t[n]===e}))}}function x(e){return p(e)?e:t=>t===e}function w(e){return e}function b(e){return Number(e)}function E(e,t){let n,i,r,o;function a(n){let a=Date.now(),p=n?0:o+t-a;if(p>0)return s(p);e.apply(r,i),l()}function s(e){n=setTimeout(a,e)}function l(){n&&clearTimeout(n),n=o=i=r=void 0}function p(...e){o=Date.now(),i=e,r=this,n||s(t)}return p.flush=function(){n&&a(!0),l()},p.cancel=l,p}function _(e,t){return e.bind(t)}function A(e,...t){return Object.assign(e,...t)}function S(e,t){var n=this;t=t||1e3,e.on(["render.shape","render.connection"],t,(function(e,t){var i=e.type,r=t.element,o=t.gfx,a=t.attrs;if(n.canRender(r))return"render.shape"===i?n.drawShape(o,r,a):n.drawConnection(o,r,a)})),e.on(["render.getShapePath","render.getConnectionPath"],t,(function(e,t){if(n.canRender(t))return"render.getShapePath"===e.type?n.getShapePath(t):n.getConnectionPath(t)}))}function k(e,t){var n=R(e);return n&&"function"==typeof n.$instanceOf&&n.$instanceOf(t)}function R(e){return e&&e.businessObject||e}function C(e){return e&&e.di}function M(e,t){return!k(e,"bpmn:CallActivity")&&(k(e,"bpmn:SubProcess")?!(!(t=t||C(e))||!k(t,"bpmndi:BPMNPlane"))||t&&!!t.isExpanded:!k(e,"bpmn:Participant")||!!R(e).processRef)}function P(e){if(k(e,"bpmn:Participant")||k(e,"bpmn:Lane")){var t=C(e).isHorizontal;return void 0===t||t}}S.prototype.canRender=function(e){},S.prototype.drawShape=function(e,t){},S.prototype.drawConnection=function(e,t){},S.prototype.getShapePath=function(e){},S.prototype.getConnectionPath=function(e){};var N={width:90,height:20};function T(e){var t=e.length/2-1,n=e[Math.floor(t)],i=e[Math.ceil(t+.01)],r=function(e){var t=e.length/2-1,n=e[Math.floor(t)],i=e[Math.ceil(t+.01)];return{x:n.x+(i.x-n.x)/2,y:n.y+(i.y-n.y)/2}}(e),o=Math.atan((i.y-n.y)/(i.x-n.x)),a=r.x,s=r.y;return Math.abs(o)r||void 0===r)&&(r=s+c),(l+p>o||void 0===o)&&(o=l+p)})),{x:n,y:i,height:o-i,width:r-n}}function L(e){return"waypoints"in e?"connection":"x"in e?"shape":"root"}function I(e){return!(!e||!e.isFrame)}function F(e,t){return function(e,t){t.appendChild(function(e,t){if(e.ownerDocument!==t.ownerDocument)try{return t.ownerDocument.importNode(e,!0)}catch(e){}return e}(e,t))}(t,e),e}var j={"alignment-baseline":1,"baseline-shift":1,clip:1,"clip-path":1,"clip-rule":1,color:1,"color-interpolation":1,"color-interpolation-filters":1,"color-profile":1,"color-rendering":1,cursor:1,direction:1,display:1,"dominant-baseline":1,"enable-background":1,fill:1,"fill-opacity":1,"fill-rule":1,filter:1,"flood-color":1,"flood-opacity":1,font:1,"font-family":1,"font-size":2,"font-size-adjust":1,"font-stretch":1,"font-style":1,"font-variant":1,"font-weight":1,"glyph-orientation-horizontal":1,"glyph-orientation-vertical":1,"image-rendering":1,kerning:1,"letter-spacing":1,"lighting-color":1,marker:1,"marker-end":1,"marker-mid":1,"marker-start":1,mask:1,opacity:1,overflow:1,"pointer-events":1,"shape-rendering":1,"stop-color":1,"stop-opacity":1,stroke:1,"stroke-dasharray":1,"stroke-dashoffset":1,"stroke-linecap":1,"stroke-linejoin":1,"stroke-miterlimit":1,"stroke-opacity":1,"stroke-width":2,"text-anchor":1,"text-decoration":1,"text-rendering":1,"unicode-bidi":1,visibility:1,"word-spacing":1,"writing-mode":1};function V(e,t,n){var i=t.replace(/([a-z])([A-Z])/g,"$1-$2").toLowerCase(),r=j[i];r?(2===r&&"number"==typeof n&&(n=String(n)+"px"),e.style[i]=n):e.setAttributeNS(null,t,n)}function W(e,t,n){if("string"==typeof t){if(void 0===n)return function(e,t){return j[t]?e.style[t]:e.getAttributeNS(null,t)}(e,t);V(e,t,n)}else!function(e,t){var n,i,r=Object.keys(t);for(n=0;i=r[n];n++)V(e,i,t[i])}(e,t);return e}const z=Object.prototype.toString;function $(e){return new G(e)}function G(e){if(!e||!e.nodeType)throw new Error("A DOM element reference is required");this.el=e,this.list=e.classList}G.prototype.add=function(e){return this.list.add(e),this},G.prototype.remove=function(e){return"[object RegExp]"==z.call(e)?this.removeMatching(e):(this.list.remove(e),this)},G.prototype.removeMatching=function(e){const t=this.array();for(let n=0;n"+e+"",t=!0);var n=function(e){var t;return(t=new DOMParser).async=!1,t.parseFromString(e,"text/xml")}(e);if(!t)return n;for(var i=document.createDocumentFragment(),r=n.firstChild;r.firstChild;)i.appendChild(r.firstChild);return i}function q(e,t){var n;return"<"===(e=e.trim()).charAt(0)?(n=U(e).firstChild,n=document.importNode(n,!0)):n=document.createElementNS(H,e),t&&W(n,t),n}var Y=null;function X(){return null===Y&&(Y=q("svg")),Y}function Z(e,t){var n,i,r=Object.keys(t);for(n=0;i=r[n];n++)e[i]=t[i];return e}function J(e){return X().createSVGTransform()}var Q=/([&<>]{1})/g,ee=/([&<>\n\r"]{1})/g,te={"&":"&","<":"<",">":">",'"':"'"};function ne(e,t){return e.replace(t,(function(e,t){return te[t]||t}))}function ie(e,t){var n,i,r,o,a;switch(e.nodeType){case 3:t.push(ne(e.textContent,Q));break;case 1:if(t.push("<",e.tagName),e.hasAttributes())for(n=0,i=(r=e.attributes).length;n"),n=0,i=(a=e.childNodes).length;n")}else t.push("/>");break;case 8:t.push("\x3c!--",ne(e.nodeValue,Q),"--\x3e");break;case 4:t.push("");break;default:throw new Error("unable to handle node "+e.nodeType)}return t}function re(e,t){return function(e){for(var t=e.firstChild,n=[];t;)ie(t,n),t=t.nextSibling;return n.join("")}(e)}function oe(e){var t=e.parentNode;return t&&t.removeChild(e),e}function ae(e,t){return t instanceof SVGMatrix?e.createSVGTransformFromMatrix(t):t}function se(e,t){var n=e.transform.baseVal;return t&&(Array.isArray(t)||(t=[t]),function(e,t){var n,i;for(e.clear(),n=0;i=t[n];n++)e.appendItem(ae(e,i))}(n,t)),n.consolidate()}function le(e){return e.flat().join(",").replace(/,?([A-Za-z]),?/g,"$1")}function pe(e){return["L",e.x,e.y]}function ce(e,t){const n=e.length,i=[(r=e[0],["M",r.x,r.y])];var r,o,a,s;for(let r=1;r",""],tr:[2,"