Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion QLExtension/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
<key>NSExtensionAttributes</key>
<dict>
<key>QLIsDataBasedPreview</key>
<true/>
<false/>
<key>QLSupportedContentTypes</key>
<array>
<string>com.unknown.md</string>
Expand Down
7 changes: 3 additions & 4 deletions QLExtension/PreviewViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,11 @@ import external_launcher

class MyWKWebView: WKWebView {
override var canBecomeKeyView: Bool {
return false
return true
}

override func becomeFirstResponder() -> Bool {
// Quick Look window do not allow first responder child.
return false
return true
}
}

Expand Down Expand Up @@ -72,7 +71,7 @@ class PreviewViewController: NSViewController, QLPreviewingController {
// Create a configuration for the preferences
let configuration = WKWebViewConfiguration()
// Enable JavaScript for unsafe HTML with inline images, or when Mermaid/Math extensions are active
configuration.preferences.javaScriptEnabled = (settings.unsafeHTMLOption && settings.inlineImageExtension) || settings.mermaidExtension || settings.mathExtension
configuration.preferences.javaScriptEnabled = (settings.unsafeHTMLOption && settings.inlineImageExtension) || settings.mermaidExtension || settings.mathExtension || settings.reactExtension
configuration.allowsAirPlayForMediaPlayback = false

self.webView = MyWKWebView(frame: previewRect, configuration: configuration)
Expand Down
18 changes: 18 additions & 0 deletions QLMarkdown.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,12 @@
83A9C2922F1D59F9005B668D /* examples in Resources */ = {isa = PBXBuildFile; fileRef = 834CAB89259167B800673002 /* examples */; };
83AABB00000000000000A002 /* mermaid.min.js in Resources */ = {isa = PBXBuildFile; fileRef = 83AABB00000000000000A001 /* mermaid.min.js */; };
83AABB00000000000000A003 /* mermaid.min.js in Resources */ = {isa = PBXBuildFile; fileRef = 83AABB00000000000000A001 /* mermaid.min.js */; };
83AABB00000000000000B001 /* react.production.min.js in Resources */ = {isa = PBXBuildFile; fileRef = 83AABB00000000000000B010 /* react.production.min.js */; };
83AABB00000000000000B002 /* react.production.min.js in Resources */ = {isa = PBXBuildFile; fileRef = 83AABB00000000000000B010 /* react.production.min.js */; };
83AABB00000000000000B003 /* react-dom.production.min.js in Resources */ = {isa = PBXBuildFile; fileRef = 83AABB00000000000000B020 /* react-dom.production.min.js */; };
83AABB00000000000000B004 /* react-dom.production.min.js in Resources */ = {isa = PBXBuildFile; fileRef = 83AABB00000000000000B020 /* react-dom.production.min.js */; };
83AABB00000000000000B005 /* babel.min.js in Resources */ = {isa = PBXBuildFile; fileRef = 83AABB00000000000000B030 /* babel.min.js */; };
83AABB00000000000000B006 /* babel.min.js in Resources */ = {isa = PBXBuildFile; fileRef = 83AABB00000000000000B030 /* babel.min.js */; };
83C8C18C2D272CA200FBB33F /* QLMarkdownXPCHelper.xpc in Embed XPC Services */ = {isa = PBXBuildFile; fileRef = 83C8C1802D272CA200FBB33F /* QLMarkdownXPCHelper.xpc */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
83C8C1972D2733CA00FBB33F /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8376EADC27E8BD3D00AB44B8 /* Log.swift */; };
83CF71C92C5FCD780066A195 /* highlight.c in Sources */ = {isa = PBXBuildFile; fileRef = 83CF71C82C5FCD780066A195 /* highlight.c */; };
Expand Down Expand Up @@ -737,6 +743,9 @@
838D19772DCDD93400963323 /* Settings+XPC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Settings+XPC.swift"; sourceTree = "<group>"; };
838D197B2DCDD96100963323 /* Settings+NoXPC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Settings+NoXPC.swift"; sourceTree = "<group>"; };
83AABB00000000000000A001 /* mermaid.min.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = mermaid.min.js; sourceTree = "<group>"; };
83AABB00000000000000B010 /* react.production.min.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = react.production.min.js; sourceTree = "<group>"; };
83AABB00000000000000B020 /* react-dom.production.min.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = react-dom.production.min.js; sourceTree = "<group>"; };
83AABB00000000000000B030 /* babel.min.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = babel.min.js; sourceTree = "<group>"; };
83B8DF082DD5EBE60054E345 /* checked.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = checked.h; sourceTree = "<group>"; };
83B8DF092DD5EBE60054E345 /* core.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = core.h; sourceTree = "<group>"; };
83B8DF0A2DD5EBE60054E345 /* cpp11.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = cpp11.h; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1206,6 +1215,9 @@
children = (
83598F22258263C0004664D3 /* default.css */,
83AABB00000000000000A001 /* mermaid.min.js */,
83AABB00000000000000B010 /* react.production.min.js */,
83AABB00000000000000B020 /* react-dom.production.min.js */,
83AABB00000000000000B030 /* babel.min.js */,
83E7FEF0258FBF37003ECF9B /* highlight */,
83CF71D62C60AB300066A195 /* stats.html */,
);
Expand Down Expand Up @@ -1630,6 +1642,9 @@
files = (
839E5B342D1C9DA600AD63BD /* default.css in Resources */,
83AABB00000000000000A003 /* mermaid.min.js in Resources */,
83AABB00000000000000B002 /* react.production.min.js in Resources */,
83AABB00000000000000B004 /* react-dom.production.min.js in Resources */,
83AABB00000000000000B006 /* babel.min.js in Resources */,
839E5B352D1C9DA800AD63BD /* highlight in Resources */,
839E5B362D1C9DAC00AD63BD /* magic.mgc in Resources */,
);
Expand All @@ -1649,6 +1664,9 @@
83E7FEF1258FBF37003ECF9B /* highlight in Resources */,
830277F52C60B3E20066EBCC /* default.css in Resources */,
83AABB00000000000000A002 /* mermaid.min.js in Resources */,
83AABB00000000000000B001 /* react.production.min.js in Resources */,
83AABB00000000000000B003 /* react-dom.production.min.js in Resources */,
83AABB00000000000000B005 /* babel.min.js in Resources */,
836B7475259E2B4100A6A5F7 /* magic.mgc in Resources */,
837F97D6271EA12A009F4605 /* qlmarkdown_cli in Resources */,
834A4F0229E8AD8C00692964 /* ext_scanners.re in Resources */,
Expand Down
70 changes: 70 additions & 0 deletions QLMarkdown/Settings+render.swift
Original file line number Diff line number Diff line change
Expand Up @@ -756,6 +756,55 @@ mermaid.initialize({
}
}

// React component rendering support
if !self.renderAsCode, self.reactExtension, processedBody.contains("language-react") {
processedBody = transformReactBlocks(processedBody)

var reactScripts = ""
if let reactPath = self.resourceBundle.path(forResource: "react.production.min", ofType: "js"),
let reactJS = try? String(contentsOfFile: reactPath, encoding: .utf8),
let reactDOMPath = self.resourceBundle.path(forResource: "react-dom.production.min", ofType: "js"),
let reactDOMJS = try? String(contentsOfFile: reactDOMPath, encoding: .utf8),
let babelPath = self.resourceBundle.path(forResource: "babel.min", ofType: "js"),
let babelJS = try? String(contentsOfFile: babelPath, encoding: .utf8) {
reactScripts = """
<script type="text/javascript">\(reactJS)</script>
<script type="text/javascript">\(reactDOMJS)</script>
<script type="text/javascript">\(babelJS)</script>
<script type="text/javascript">
(function() {
document.querySelectorAll('.react-component').forEach(function(el) {
var code = el.textContent;
// Strip export statements for Babel compatibility
code = code.replace(/export\\s+default\\s+/g, 'module.exports = ');
code = code.replace(/export\\s+/g, '');
var container = document.createElement('div');
container.className = 'react-render';
el.parentNode.replaceChild(container, el);
try {
var transformed = Babel.transform(code, { presets: ['react'] }).code;
var mod = { exports: {} };
var fn = new Function('React', 'ReactDOM', 'module', 'exports', transformed);
fn(React, ReactDOM, mod, mod.exports);
var Component = mod.exports.default || mod.exports;
if (typeof Component === 'function') {
ReactDOM.render(React.createElement(Component), container);
} else {
container.innerHTML = '<pre style="color:#94a3b8;font-size:12px;">No default export found</pre>';
}
} catch(e) {
container.innerHTML = '<pre style="color:#ef4444;font-size:12px;padding:8px;background:#fef2f2;border-radius:6px;">' + e.message + '</pre>';
}
});
})();
</script>
"""
} else {
os_log("Could not load React/Babel JS from bundle", log: OSLog.rendering, type: .error)
}
s_footer += reactScripts
}

let style = css_doc + css_highlight + css_doc_extended
let wrapper_open = self.renderAsCode ? "<pre class='hl'>" : "<article class='markdown-body'>"
let wrapper_close = self.renderAsCode ? "</pre>" : "</article>"
Expand Down Expand Up @@ -845,6 +894,27 @@ mermaid.initialize({
return result
}

/// Transform react code blocks from `<pre...><code class="language-react">...</code></pre>` to `<div class="react-component">...</div>`
private func transformReactBlocks(_ html: String) -> String {
let pattern = #"<pre[^>]*>\s*<code[^>]*class="[^"]*language-react[^"]*"[^>]*>([\s\S]*?)</code>\s*</pre>"#

guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else {
os_log("Failed to create react regex pattern", log: OSLog.rendering, type: .error)
return html
}

let range = NSRange(html.startIndex..., in: html)

let result = regex.stringByReplacingMatches(
in: html,
options: [],
range: range,
withTemplate: #"<div class="react-component" style="display:none">$1</div>"#
)

return result
}

internal func renderYaml(_ yaml: [(key: AnyHashable, value: Any)]) -> String {
guard yaml.count > 0 else {
return ""
Expand Down
8 changes: 8 additions & 0 deletions QLMarkdown/Settings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ class Settings: Codable {
case mathExtension
case mermaidExtension
case mentionExtension
case reactExtension
case subExtension
case supExtension
case strikethroughExtension
Expand Down Expand Up @@ -106,6 +107,7 @@ class Settings: Codable {
@objc var inlineImageExtension: Bool = true
@objc var mathExtension: Bool = true
@objc var mermaidExtension: Bool = true
@objc var reactExtension: Bool = true
@objc var mentionExtension: Bool = false

@objc var strikethroughExtension: Bool = true
Expand Down Expand Up @@ -227,6 +229,7 @@ class Settings: Codable {

self.mathExtension = try container.decode(Bool.self, forKey: .mathExtension)
self.mermaidExtension = (try? container.decode(Bool.self, forKey: .mermaidExtension)) ?? true
self.reactExtension = (try? container.decode(Bool.self, forKey: .reactExtension)) ?? true
self.mentionExtension = try container.decode(Bool.self, forKey:.mentionExtension)
self.strikethroughExtension = try container.decode(Bool.self, forKey:.strikethroughExtension)
self.strikethroughDoubleTildeOption = try container.decode(Bool.self, forKey:.strikethroughDoubleTildeOption)
Expand Down Expand Up @@ -289,6 +292,7 @@ class Settings: Codable {
try container.encode(self.inlineImageExtension, forKey: .inlineImageExtension)
try container.encode(self.mathExtension, forKey: .mathExtension)
try container.encode(self.mermaidExtension, forKey: .mermaidExtension)
try container.encode(self.reactExtension, forKey: .reactExtension)
try container.encode(self.mentionExtension, forKey: .mentionExtension)
try container.encode(self.strikethroughExtension, forKey: .strikethroughExtension)
try container.encode(self.strikethroughDoubleTildeOption, forKey: .strikethroughDoubleTildeOption)
Expand Down Expand Up @@ -365,6 +369,7 @@ class Settings: Codable {

self.mathExtension = s.mathExtension
self.mermaidExtension = s.mermaidExtension
self.reactExtension = s.reactExtension
self.mentionExtension = s.mentionExtension

self.strikethroughExtension = s.strikethroughExtension
Expand Down Expand Up @@ -446,6 +451,9 @@ class Settings: Codable {
if let ext = defaultsDomain["mermaid"] as? Bool {
mermaidExtension = ext
}
if let ext = defaultsDomain["react"] as? Bool {
reactExtension = ext
}
if let ext = defaultsDomain["mention"] as? Bool {
mentionExtension = ext
}
Expand Down
4 changes: 4 additions & 0 deletions Resources/babel.min.js

Large diffs are not rendered by default.

Loading