This commit is contained in:
relikd
2025-07-29 11:54:12 +02:00
commit 6e12dfa61d
18 changed files with 1794 additions and 0 deletions

24
QLPreview/Info.plist Normal file
View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>QLIsDataBasedPreview</key>
<false/>
<key>QLSupportedContentTypes</key>
<array>
<string>public.json</string>
</array>
<key>QLSupportsSearchableItems</key>
<false/>
</dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.quicklook.preview</string>
<key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).PreviewViewController</string>
</dict>
</dict>
</plist>

View File

@@ -0,0 +1,42 @@
import Cocoa
import Quartz
import WebKit
class PreviewViewController: NSViewController, QLPreviewingController {
func bundleFile(filename: String, ext: String) throws -> String {
if let appSupport = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first {
let override = appSupport.appendingPathComponent(filename + "." + ext)
if FileManager.default.fileExists(atPath: override.path) {
return try String(contentsOfFile: override.path, encoding: .utf8)
}
}
// else, load bundle file
let path = Bundle.main.path(forResource: filename, ofType: ext)
return try String(contentsOfFile: path!, encoding: .utf8)
}
func preparePreviewOfFile(at url: URL) async throws {
let jsonFile = try String(contentsOf: url, encoding: .utf8)
let html = """
<!DOCTYPE html>
<html>
<head>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type" />
<style>\(try self.bundleFile(filename: "style", ext: "css"))</style>
<script>\(try self.bundleFile(filename: "script", ext: "js"))</script>
</head>
<body onload="init()">
<script id="json" type="application/json">\(jsonFile)</script>
</body>
</html>
"""
// sure, we could use `WKWebView`, but that requires the `com.apple.security.network.client` entitlement
//let web = WKWebView(frame: self.view.bounds)
let web = WebView(frame: self.view.bounds)
web.autoresizingMask = [.width, .height]
self.view.addSubview(web)
web.mainFrame.loadHTMLString(html, baseURL: nil) // WebView
//web.loadHTMLString(html, baseURL: nil) // WKWebView
}
}

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="23727" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="23727"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="PreviewViewController" customModule="QLPreview" customModuleProvider="target">
<connections>
<outlet property="view" destination="c22-O7-iKe" id="NRM-P4-wb6"/>
</connections>
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<customView id="c22-O7-iKe" userLabel="Preview View">
<rect key="frame" x="0.0" y="0.0" width="480" height="272"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<point key="canvasLocation" x="53" y="-36"/>
</customView>
</objects>
</document>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
</dict>
</plist>

106
QLPreview/script.js Normal file
View File

@@ -0,0 +1,106 @@
// data types
function nullV() { return '<span class="null">null</span>'; }
function boolV(val) { return '<span class="bool">' + html(val) + '</span>'; }
function numV(val) { return '<span class="num">' + html(val) + '</span>'; }
function stringV(val) { return '<span class="string">"' + jstr(val) + '"</span>'; }
function linkV(val) { return '"<a href="' + encodeURI(val) + '">' + jstr(val) + '</a>"'; }
function propK(key) { return '<span class="prop">"' + jstr(key) + '"</span>'; }
function wrapJSONP(callback, json, suffix) {
return `<span class="callback">${callback}(</span>${json}<span class="callback">)${suffix}</span>`;
}
function errorPage(error, json) {
return '<p class="error">Error parsing JSON:<br>' + error + '</p><h1>Content:</h1><pre>' + html(json) + '</pre>';
}
// helper
function indent(nl, level) { return nl ? nl + '&nbsp; '.repeat(level) : ''; }
function jstr(s) { return html(JSON.stringify(s).slice(1, -1)); }
function html(s) {
return (s + '').replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;');
}
// formatter entry point
function init() {
document.body.innerHTML = parse(document.getElementById('json').textContent);
}
function parse(raw) {
let data = raw, callback = '', suffix = '';
// matches "callback({json});" ... original had \s -> [\s\u200B\uFEFF]
let match = /^\s*([\w$\[\]\.]+)\s*\(\s*([\[{][\s\S]*[\]}])\s*\)([\s;]*)$/m.exec(raw);
if (match && match.length === 4) {
callback = match[1];
data = match[2];
suffix = match[3].replace(/[^;]+/g, '');
}
try {
let root = nested(JSON.parse(data), 0, '<br>');
if (callback)
return wrapJSONP(callback, root, suffix);
return root;
} catch (e) {
return errorPage(e, raw);
}
}
function nested(val, level, nl) {
if (null === val)
return nullV();
switch (typeof val) {
case 'boolean': return boolV(val);
case 'number': return numV(val);
case 'string':
return (/^(\w+):\/\/[^\s]+$/i.test(val)) ? linkV(val) : stringV(val);
case 'object':
return (Array.isArray(val))
? array(val, level + 1, nl)
: dict(val, level + 1, nl);
}
return '&lt;-unsupported-type-&gt;';
}
function dict(dict, level, nl) {
let output = '';
for (let key in dict) {
if (output)
output += indent(',<br>', level);
output += propK(key) + ': ' + nested(dict[key], level, '<br>');
}
if (!output)
return '{}';
return '<span class="folder">{' + foldableContent(output, level, nl) + '}</span>';
}
function array(arr, level, nl) {
let output = '';
for (let i = 0; i < arr.length; i++) {
if (i > 0)
output += indent(',<br>', level);
output += nested(arr[i], level, '<br>');
}
if (!output)
return '[]';
return '<span class="folder">[' + foldableContent(output, level, nl) + ']</span>';
}
// foldable content
function foldableContent(output, level, nl) {
let collapsible = '<i onmouseover="highlight(this, true)" onmouseout="highlight(this, false)" onclick="fold(this)"></i>';
return collapsible + '<span class="content">' + indent(nl, level) + output + indent('<br>', level - 1) + '</span>' + collapsible
}
function fold(sender) {
let folder = sender.parentNode;
folder.classList.toggle('closed');
}
function highlight(sender, show) {
let folder = sender.parentNode;
show ? folder.classList.add('highlight') : folder.classList.remove('highlight');
}

88
QLPreview/style.css Normal file
View File

@@ -0,0 +1,88 @@
body {
background: #fff;
color: #444;
}
.bool, .null, .num {
color: red;
}
.callback {
color: purple;
}
.string {
color: green;
}
a {
color: #0065FF
}
a:hover {
color: #0052CC
}
@media (prefers-color-scheme: dark) {
body {
background: #222;
color: #eee;
}
.callback {
color: violet;
}
.string {
color: yellowgreen;
}
a {
color: #2684FF;
}
a:hover {
color: #4C9AFF;
}
}
body, pre {
font-family: monospace;
white-space: pre-wrap;
line-break: anywhere;
}
.prop {
font-weight: bold;
}
.bool, .null {
font-style: italic;
}
/* Error handling */
h1 {
font-size: 1.2em;
}
.error {
border-radius: 8px;
border: 2px solid #900;
margin: 5px;
padding: .4em .8em;
}
/* Collapsible */
.closed>span {
position: absolute;
height: 0;
width: 0;
overflow: hidden;
}
.folder>i:nth-child(1):before {
content: "↙️";
}
.folder>i:nth-child(3):before {
content: "↗️";
}
i {
font-style: unset;
cursor: pointer;
opacity: .25;
/* position: absolute;
left: 2px; */
}
.highlight {
background: #7777;
}
.closed {
background: orange;
}