Initial
This commit is contained in:
24
QLPreview/Info.plist
Normal file
24
QLPreview/Info.plist
Normal 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>
|
||||
42
QLPreview/PreviewViewController.swift
Normal file
42
QLPreview/PreviewViewController.swift
Normal 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
|
||||
}
|
||||
}
|
||||
22
QLPreview/PreviewViewController.xib
Normal file
22
QLPreview/PreviewViewController.xib
Normal 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>
|
||||
10
QLPreview/QLPreview.entitlements
Normal file
10
QLPreview/QLPreview.entitlements
Normal 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
106
QLPreview/script.js
Normal 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 + ' '.repeat(level) : ''; }
|
||||
function jstr(s) { return html(JSON.stringify(s).slice(1, -1)); }
|
||||
function html(s) {
|
||||
return (s + '').replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>');
|
||||
}
|
||||
|
||||
// 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 '<-unsupported-type->';
|
||||
}
|
||||
|
||||
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
88
QLPreview/style.css
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user