\n", 93 | " | first_name | \n", 94 | "age | \n", 95 | "gender | \n", 96 | "
---|---|---|---|
0 | \n", 101 | "Alice | \n", 102 | "12 | \n", 103 | "f | \n", 104 | "
1 | \n", 107 | "Bob | \n", 108 | "23 | \n", 109 | "m | \n", 110 | "
2 | \n", 113 | "Charlie | \n", 114 | "34 | \n", 115 | "m | \n", 116 | "
`)) 35 | assert.True(t, strings.HasSuffix(actualTrimmed, ``)) 36 | } 37 | 38 | func TestConvertMarkdownToHTML(t *testing.T) { 39 | source := `# Heading 40 | 41 | Text` 42 | expected := "
Text
" 43 | actual := convertToGoString(convertMarkdownToHTML(convertToCString(source))) 44 | assert.Equal(t, expected, minifyHTML(actual)) 45 | } 46 | 47 | func TestConvertMarkdownToHTMLWithFrontMatter(t *testing.T) { 48 | source := `--- 49 | key: Value 50 | key2: Another value 51 | --- 52 | 53 | # Heading 54 | 55 | Text` 56 | expected := "Text
" 57 | actual := convertToGoString(convertMarkdownToHTML(convertToCString(source))) 58 | assert.Equal(t, expected, minifyHTML(actual)) 59 | } 60 | 61 | func TestConvertMarkdownToHTMLWithSyntaxHighlighting(t *testing.T) { 62 | source := "# Heading\n\nText\n\n```js\nconst print = (text) => console.log(text);\nprint(\"Hello world\");\n```" 63 | actual := convertToGoString(convertMarkdownToHTML(convertToCString(source))) 64 | assert.True(t, strings.Contains(actual, ``)) 65 | } 66 | 67 | func TestConvertNotebookToHTML(t *testing.T) { 68 | source := `{"cells":[{"cell_type":"code","execution_count":1,"metadata":{},"outputs":[{"name":"stdout","output_type":"stream","text":["Hello world\n"]}],"source":["print(\"Hello world\")"]}],"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.8.2"}},"nbformat":4,"nbformat_minor":4}` // nolint:lll 69 | actual := convertToGoString(convertNotebookToHTML(convertToCString(source))) 70 | actualTrimmed := strings.TrimSpace(actual) 71 | assert.True(t, strings.HasPrefix(actualTrimmed, ``)) 72 | assert.True(t, strings.HasSuffix(actualTrimmed, ``)) 73 | } 74 | 75 | func TestConvertNotebookToHTMLInvalid(t *testing.T) { 76 | source := "This is not a valid JSON file." 77 | actual := convertToGoString(convertNotebookToHTML(convertToCString(source))) 78 | assert.True(t, strings.HasPrefix(actual, "error: ")) 79 | } 80 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2020 Samuel Meuli 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. 10 | -------------------------------------------------------------------------------- /Mintfile: -------------------------------------------------------------------------------- 1 | nicklockwood/SwiftFormat@0.44.7 2 | realm/SwiftLint@0.39.2 3 | -------------------------------------------------------------------------------- /PRIVACY.md: -------------------------------------------------------------------------------- 1 | # Privacy Policy 2 | 3 | Glance does not collect any personal information. 4 | 5 | Please do not hesitate to [contact me](https://samuelmeuli.com) for questions about this privacy policy. 6 | -------------------------------------------------------------------------------- /QLPlugin/Extensions/String.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension String { 4 | /// Returns all matches and capturing groups for the provided regular expression applied to the 5 | /// string 6 | /// 7 | /// Source: https://stackoverflow.com/a/40040472/6767508 8 | func matchRegex(regex: String) -> [[String]] { 9 | guard let regex = try? NSRegularExpression(pattern: regex, options: []) else { 10 | return [] 11 | } 12 | let nsString = self as NSString 13 | let results = regex.matches( 14 | in: self, 15 | options: [], 16 | range: NSRange(location: 0, length: nsString.length) 17 | ) 18 | return results.map { result in 19 | (0 ..< result.numberOfRanges).map { 20 | result.range(at: $0).location != NSNotFound 21 | ? nsString.substring(with: result.range(at: $0)) 22 | : "" 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /QLPlugin/MainVC.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import os.log 3 | import Quartz 4 | 5 | enum PreviewError: Error { 6 | case fileSizeError(path: String) 7 | } 8 | 9 | extension PreviewError: LocalizedError { 10 | public var errorDescription: String? { 11 | switch self { 12 | case let .fileSizeError(path): 13 | return NSLocalizedString("File \(path) is too large to preview", comment: "") 14 | } 15 | } 16 | } 17 | 18 | class MainVC: NSViewController, QLPreviewingController { 19 | /// Max size of files to render 20 | let maxFileSize = 10_000_000 // 10 MB 21 | 22 | let stats = Stats() 23 | 24 | override var nibName: NSNib.Name? { 25 | NSNib.Name("MainVC") 26 | } 27 | 28 | override func viewDidLoad() { 29 | super.viewDidLoad() 30 | setUpView() 31 | } 32 | 33 | private func setUpView() { 34 | // Draw border around previews, in similar style to macOS's default previews 35 | view.wantsLayer = true 36 | view.layer?.borderWidth = 1 37 | view.layer?.borderColor = NSColor.tertiaryLabelColor.cgColor 38 | } 39 | 40 | /// Function responsible for generating file previews. It's called for previews in Finder, 41 | /// Spotlight, Quick Look and any other UI elements which implement the API. 42 | func preparePreviewOfFile( 43 | at fileUrl: URL, 44 | completionHandler handler: @escaping (Error?) -> Void 45 | ) { 46 | DispatchQueue.main.async { 47 | // Read information about the file to preview 48 | var file: File 49 | do { 50 | file = try File(url: fileUrl) 51 | } catch { 52 | os_log( 53 | "Could not obtain information about file %{public}s: %{public}s", 54 | log: Log.general, 55 | type: .error, 56 | fileUrl.path, 57 | error.localizedDescription 58 | ) 59 | handler(error) 60 | return 61 | } 62 | 63 | // Skip preview if the file is too large 64 | if !file.isDirectory, !file.isArchive, file.size > self.maxFileSize { 65 | // Log error and fall back to default preview (by calling the completion handler 66 | // with the error) 67 | let error = PreviewError.fileSizeError(path: file.path) 68 | os_log( 69 | "Skipping file preview: %{public}s", 70 | log: Log.general, 71 | error.localizedDescription 72 | ) 73 | handler(error) 74 | return 75 | } 76 | 77 | // Render file preview 78 | os_log( 79 | "Generating preview for file %{public}s", 80 | log: Log.general, 81 | type: .info, 82 | file.path 83 | ) 84 | do { 85 | try self.previewFile(file: file) 86 | } catch { 87 | // Log error and fall back to default preview (by calling the completion handler 88 | // with the error) 89 | os_log( 90 | "Could not generate preview for file %{public}s: %{public}s", 91 | log: Log.general, 92 | type: .error, 93 | file.path, 94 | error.localizedDescription 95 | ) 96 | handler(error) 97 | return 98 | } 99 | 100 | // Hide preview loading spinner 101 | handler(nil) 102 | } 103 | } 104 | 105 | /// Generates a preview of the selected file and adds the corresponding child view controller. 106 | private func previewFile(file: File) throws { 107 | // Initialize `PreviewVC` for the file type 108 | if let previewInitializerType = PreviewVCFactory.getPreviewInitializer(fileURL: file.url) { 109 | // Generate file preview 110 | let previewInitializer = previewInitializerType.init() 111 | let previewVC = try previewInitializer.createPreviewVC(file: file) 112 | 113 | // Add `PreviewVC` as a child view controller 114 | addChild(previewVC) 115 | previewVC.view.autoresizingMask = [.height, .width] 116 | previewVC.view.frame = view.frame 117 | view.addSubview(previewVC.view) 118 | 119 | // Update stats 120 | stats.increaseStatsCounts(fileExtension: file.url.pathExtension) 121 | } else { 122 | os_log( 123 | "Skipping preview for file %{public}s: File type not supported", 124 | log: Log.general, 125 | type: .info, 126 | file.path 127 | ) 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /QLPlugin/MainVC.xib: -------------------------------------------------------------------------------- 1 | 2 |3 | 22 | -------------------------------------------------------------------------------- /QLPlugin/QLPlugin.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 |4 | 8 |5 | 6 | 7 | 9 | 21 |10 | 14 |11 | 13 |12 | 15 | 16 | 17 | 20 |18 | 19 | 4 | 17 | -------------------------------------------------------------------------------- /QLPlugin/Resources/jupyter/fonts/KaTeX_AMS-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/QLPlugin/Resources/jupyter/fonts/KaTeX_AMS-Regular.ttf -------------------------------------------------------------------------------- /QLPlugin/Resources/jupyter/fonts/KaTeX_AMS-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/QLPlugin/Resources/jupyter/fonts/KaTeX_AMS-Regular.woff -------------------------------------------------------------------------------- /QLPlugin/Resources/jupyter/fonts/KaTeX_AMS-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/QLPlugin/Resources/jupyter/fonts/KaTeX_AMS-Regular.woff2 -------------------------------------------------------------------------------- /QLPlugin/Resources/jupyter/fonts/KaTeX_Caligraphic-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/QLPlugin/Resources/jupyter/fonts/KaTeX_Caligraphic-Bold.ttf -------------------------------------------------------------------------------- /QLPlugin/Resources/jupyter/fonts/KaTeX_Caligraphic-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/QLPlugin/Resources/jupyter/fonts/KaTeX_Caligraphic-Bold.woff -------------------------------------------------------------------------------- /QLPlugin/Resources/jupyter/fonts/KaTeX_Caligraphic-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/QLPlugin/Resources/jupyter/fonts/KaTeX_Caligraphic-Bold.woff2 -------------------------------------------------------------------------------- /QLPlugin/Resources/jupyter/fonts/KaTeX_Caligraphic-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/QLPlugin/Resources/jupyter/fonts/KaTeX_Caligraphic-Regular.ttf -------------------------------------------------------------------------------- /QLPlugin/Resources/jupyter/fonts/KaTeX_Caligraphic-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/QLPlugin/Resources/jupyter/fonts/KaTeX_Caligraphic-Regular.woff -------------------------------------------------------------------------------- /QLPlugin/Resources/jupyter/fonts/KaTeX_Caligraphic-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/QLPlugin/Resources/jupyter/fonts/KaTeX_Caligraphic-Regular.woff2 -------------------------------------------------------------------------------- /QLPlugin/Resources/jupyter/fonts/KaTeX_Fraktur-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/QLPlugin/Resources/jupyter/fonts/KaTeX_Fraktur-Bold.ttf -------------------------------------------------------------------------------- /QLPlugin/Resources/jupyter/fonts/KaTeX_Fraktur-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/QLPlugin/Resources/jupyter/fonts/KaTeX_Fraktur-Bold.woff -------------------------------------------------------------------------------- /QLPlugin/Resources/jupyter/fonts/KaTeX_Fraktur-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/QLPlugin/Resources/jupyter/fonts/KaTeX_Fraktur-Bold.woff2 -------------------------------------------------------------------------------- /QLPlugin/Resources/jupyter/fonts/KaTeX_Fraktur-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/QLPlugin/Resources/jupyter/fonts/KaTeX_Fraktur-Regular.ttf -------------------------------------------------------------------------------- /QLPlugin/Resources/jupyter/fonts/KaTeX_Fraktur-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/QLPlugin/Resources/jupyter/fonts/KaTeX_Fraktur-Regular.woff -------------------------------------------------------------------------------- /QLPlugin/Resources/jupyter/fonts/KaTeX_Fraktur-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/QLPlugin/Resources/jupyter/fonts/KaTeX_Fraktur-Regular.woff2 -------------------------------------------------------------------------------- /QLPlugin/Resources/jupyter/fonts/KaTeX_Main-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/QLPlugin/Resources/jupyter/fonts/KaTeX_Main-Bold.ttf -------------------------------------------------------------------------------- /QLPlugin/Resources/jupyter/fonts/KaTeX_Main-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/QLPlugin/Resources/jupyter/fonts/KaTeX_Main-Bold.woff -------------------------------------------------------------------------------- /QLPlugin/Resources/jupyter/fonts/KaTeX_Main-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/QLPlugin/Resources/jupyter/fonts/KaTeX_Main-Bold.woff2 -------------------------------------------------------------------------------- /QLPlugin/Resources/jupyter/fonts/KaTeX_Main-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/QLPlugin/Resources/jupyter/fonts/KaTeX_Main-BoldItalic.ttf -------------------------------------------------------------------------------- /QLPlugin/Resources/jupyter/fonts/KaTeX_Main-BoldItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/QLPlugin/Resources/jupyter/fonts/KaTeX_Main-BoldItalic.woff -------------------------------------------------------------------------------- /QLPlugin/Resources/jupyter/fonts/KaTeX_Main-BoldItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/QLPlugin/Resources/jupyter/fonts/KaTeX_Main-BoldItalic.woff2 -------------------------------------------------------------------------------- /QLPlugin/Resources/jupyter/fonts/KaTeX_Main-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/QLPlugin/Resources/jupyter/fonts/KaTeX_Main-Italic.ttf -------------------------------------------------------------------------------- /QLPlugin/Resources/jupyter/fonts/KaTeX_Main-Italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/QLPlugin/Resources/jupyter/fonts/KaTeX_Main-Italic.woff -------------------------------------------------------------------------------- /QLPlugin/Resources/jupyter/fonts/KaTeX_Main-Italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/QLPlugin/Resources/jupyter/fonts/KaTeX_Main-Italic.woff2 -------------------------------------------------------------------------------- /QLPlugin/Resources/jupyter/fonts/KaTeX_Main-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/QLPlugin/Resources/jupyter/fonts/KaTeX_Main-Regular.ttf -------------------------------------------------------------------------------- /QLPlugin/Resources/jupyter/fonts/KaTeX_Main-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/QLPlugin/Resources/jupyter/fonts/KaTeX_Main-Regular.woff -------------------------------------------------------------------------------- /QLPlugin/Resources/jupyter/fonts/KaTeX_Main-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/QLPlugin/Resources/jupyter/fonts/KaTeX_Main-Regular.woff2 -------------------------------------------------------------------------------- /QLPlugin/Resources/jupyter/fonts/KaTeX_Math-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/QLPlugin/Resources/jupyter/fonts/KaTeX_Math-BoldItalic.ttf -------------------------------------------------------------------------------- /QLPlugin/Resources/jupyter/fonts/KaTeX_Math-BoldItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/QLPlugin/Resources/jupyter/fonts/KaTeX_Math-BoldItalic.woff -------------------------------------------------------------------------------- /QLPlugin/Resources/jupyter/fonts/KaTeX_Math-BoldItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/QLPlugin/Resources/jupyter/fonts/KaTeX_Math-BoldItalic.woff2 -------------------------------------------------------------------------------- /QLPlugin/Resources/jupyter/fonts/KaTeX_Math-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/QLPlugin/Resources/jupyter/fonts/KaTeX_Math-Italic.ttf -------------------------------------------------------------------------------- /QLPlugin/Resources/jupyter/fonts/KaTeX_Math-Italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/QLPlugin/Resources/jupyter/fonts/KaTeX_Math-Italic.woff -------------------------------------------------------------------------------- /QLPlugin/Resources/jupyter/fonts/KaTeX_Math-Italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/QLPlugin/Resources/jupyter/fonts/KaTeX_Math-Italic.woff2 -------------------------------------------------------------------------------- /QLPlugin/Resources/jupyter/fonts/KaTeX_SansSerif-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/QLPlugin/Resources/jupyter/fonts/KaTeX_SansSerif-Bold.ttf -------------------------------------------------------------------------------- /QLPlugin/Resources/jupyter/fonts/KaTeX_SansSerif-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/QLPlugin/Resources/jupyter/fonts/KaTeX_SansSerif-Bold.woff -------------------------------------------------------------------------------- /QLPlugin/Resources/jupyter/fonts/KaTeX_SansSerif-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/QLPlugin/Resources/jupyter/fonts/KaTeX_SansSerif-Bold.woff2 -------------------------------------------------------------------------------- /QLPlugin/Resources/jupyter/fonts/KaTeX_SansSerif-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/QLPlugin/Resources/jupyter/fonts/KaTeX_SansSerif-Italic.ttf -------------------------------------------------------------------------------- /QLPlugin/Resources/jupyter/fonts/KaTeX_SansSerif-Italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/QLPlugin/Resources/jupyter/fonts/KaTeX_SansSerif-Italic.woff -------------------------------------------------------------------------------- /QLPlugin/Resources/jupyter/fonts/KaTeX_SansSerif-Italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/QLPlugin/Resources/jupyter/fonts/KaTeX_SansSerif-Italic.woff2 -------------------------------------------------------------------------------- /QLPlugin/Resources/jupyter/fonts/KaTeX_SansSerif-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/QLPlugin/Resources/jupyter/fonts/KaTeX_SansSerif-Regular.ttf -------------------------------------------------------------------------------- /QLPlugin/Resources/jupyter/fonts/KaTeX_SansSerif-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/QLPlugin/Resources/jupyter/fonts/KaTeX_SansSerif-Regular.woff -------------------------------------------------------------------------------- /QLPlugin/Resources/jupyter/fonts/KaTeX_SansSerif-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/QLPlugin/Resources/jupyter/fonts/KaTeX_SansSerif-Regular.woff2 -------------------------------------------------------------------------------- /QLPlugin/Resources/jupyter/fonts/KaTeX_Script-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/QLPlugin/Resources/jupyter/fonts/KaTeX_Script-Regular.ttf -------------------------------------------------------------------------------- /QLPlugin/Resources/jupyter/fonts/KaTeX_Script-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/QLPlugin/Resources/jupyter/fonts/KaTeX_Script-Regular.woff -------------------------------------------------------------------------------- /QLPlugin/Resources/jupyter/fonts/KaTeX_Script-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/QLPlugin/Resources/jupyter/fonts/KaTeX_Script-Regular.woff2 -------------------------------------------------------------------------------- /QLPlugin/Resources/jupyter/fonts/KaTeX_Size1-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/QLPlugin/Resources/jupyter/fonts/KaTeX_Size1-Regular.ttf -------------------------------------------------------------------------------- /QLPlugin/Resources/jupyter/fonts/KaTeX_Size1-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/QLPlugin/Resources/jupyter/fonts/KaTeX_Size1-Regular.woff -------------------------------------------------------------------------------- /QLPlugin/Resources/jupyter/fonts/KaTeX_Size1-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/QLPlugin/Resources/jupyter/fonts/KaTeX_Size1-Regular.woff2 -------------------------------------------------------------------------------- /QLPlugin/Resources/jupyter/fonts/KaTeX_Size2-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/QLPlugin/Resources/jupyter/fonts/KaTeX_Size2-Regular.ttf -------------------------------------------------------------------------------- /QLPlugin/Resources/jupyter/fonts/KaTeX_Size2-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/QLPlugin/Resources/jupyter/fonts/KaTeX_Size2-Regular.woff -------------------------------------------------------------------------------- /QLPlugin/Resources/jupyter/fonts/KaTeX_Size2-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/QLPlugin/Resources/jupyter/fonts/KaTeX_Size2-Regular.woff2 -------------------------------------------------------------------------------- /QLPlugin/Resources/jupyter/fonts/KaTeX_Size3-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/QLPlugin/Resources/jupyter/fonts/KaTeX_Size3-Regular.ttf -------------------------------------------------------------------------------- /QLPlugin/Resources/jupyter/fonts/KaTeX_Size3-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/QLPlugin/Resources/jupyter/fonts/KaTeX_Size3-Regular.woff -------------------------------------------------------------------------------- /QLPlugin/Resources/jupyter/fonts/KaTeX_Size3-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/QLPlugin/Resources/jupyter/fonts/KaTeX_Size3-Regular.woff2 -------------------------------------------------------------------------------- /QLPlugin/Resources/jupyter/fonts/KaTeX_Size4-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/QLPlugin/Resources/jupyter/fonts/KaTeX_Size4-Regular.ttf -------------------------------------------------------------------------------- /QLPlugin/Resources/jupyter/fonts/KaTeX_Size4-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/QLPlugin/Resources/jupyter/fonts/KaTeX_Size4-Regular.woff -------------------------------------------------------------------------------- /QLPlugin/Resources/jupyter/fonts/KaTeX_Size4-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/QLPlugin/Resources/jupyter/fonts/KaTeX_Size4-Regular.woff2 -------------------------------------------------------------------------------- /QLPlugin/Resources/jupyter/fonts/KaTeX_Typewriter-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/QLPlugin/Resources/jupyter/fonts/KaTeX_Typewriter-Regular.ttf -------------------------------------------------------------------------------- /QLPlugin/Resources/jupyter/fonts/KaTeX_Typewriter-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/QLPlugin/Resources/jupyter/fonts/KaTeX_Typewriter-Regular.woff -------------------------------------------------------------------------------- /QLPlugin/Resources/jupyter/fonts/KaTeX_Typewriter-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/QLPlugin/Resources/jupyter/fonts/KaTeX_Typewriter-Regular.woff2 -------------------------------------------------------------------------------- /QLPlugin/Resources/jupyter/jupyter-katex-auto-render.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * KaTeX 3 | * v0.11.1 4 | * https://github.com/KaTeX/KaTeX 5 | * 6 | * The MIT License (MIT) 7 | * 8 | * Copyright (c) 2013-2019 Khan Academy and other contributors 9 | * 10 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 11 | * associated documentation files (the "Software"), to deal in the Software without restriction, 12 | * including without limitation the rights to use, copy, modify, merge, publish, distribute, 13 | * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all copies or 17 | * substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT 20 | * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 22 | * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | */ 25 | 26 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("katex")):"function"==typeof define&&define.amd?define(["katex"],t):"object"==typeof exports?exports.renderMathInElement=t(require("katex")):e.renderMathInElement=t(e.katex)}("undefined"!=typeof self?self:this,function(e){return function(e){var t={};function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}return r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=1)}([function(t,r){t.exports=e},function(e,t,r){"use strict";r.r(t);var n=r(0),o=r.n(n),a=function(e,t,r){for(var n=r,o=0,a=e.length;n5 | 16 |com.apple.security.app-sandbox 6 |7 | com.apple.security.application-groups 8 |9 | 11 |group.com.samuelmeuli.glance 10 |com.apple.security.files.user-selected.read-only 12 |13 | com.apple.security.network.client 14 |15 | :first-child, 35 | .output > :first-child { 36 | margin-top: 0; 37 | } 38 | 39 | .input > :last-child, 40 | .output > :last-child { 41 | margin-bottom: 0; 42 | } 43 | 44 | .output-wrapper { 45 | margin-top: 10px; 46 | } 47 | 48 | /* Prompts (execution counts) */ 49 | 50 | .input-prompt, 51 | .output-prompt { 52 | color: var(--color-text-faded); 53 | } 54 | 55 | /* Boxes (e.g. for code and errors) */ 56 | 57 | .cell-code .input, 58 | .cell-raw .input, 59 | .output-error { 60 | padding: 8px 10px; 61 | overflow-y: auto; 62 | border-radius: var(--border-radius); 63 | } 64 | 65 | /* Grey boxes */ 66 | .cell-code .input, 67 | .cell-raw .input { 68 | background: var(--color-background-code-block); 69 | border: 1px solid var(--color-border); 70 | } 71 | 72 | /* Adjust spacing of prompts next to boxes (so text is aligned) */ 73 | .cell-code .input-prompt, 74 | .cell-raw .input-prompt { 75 | padding-top: 8px; 76 | } 77 | 78 | /* Error output */ 79 | 80 | /* Error box */ 81 | .output-error { 82 | background: var(--color-background-error); 83 | } 84 | 85 | /* Black ANSI color */ 86 | .term-fg30 { 87 | color: #3e424d; 88 | } 89 | 90 | /* Red ANSI color */ 91 | .term-fg31 { 92 | color: #e75c58; 93 | } 94 | 95 | /* Green ANSI color */ 96 | .term-fg32 { 97 | color: #00a250; 98 | } 99 | 100 | /* Yellow ANSI color */ 101 | .term-fg33 { 102 | color: #ddb62b; 103 | } 104 | 105 | /* Blue ANSI color */ 106 | .term-fg34 { 107 | color: #208ffb; 108 | } 109 | 110 | /* Magenta ANSI color */ 111 | .term-fg35 { 112 | color: #d160c4; 113 | } 114 | 115 | /* Cyan ANSI color */ 116 | .term-fg36 { 117 | color: #60c6c8; 118 | } 119 | -------------------------------------------------------------------------------- /QLPlugin/Resources/markdown/markdown-main.css: -------------------------------------------------------------------------------- 1 | code { 2 | margin: 0; 3 | padding: 0.2em 0.4em; 4 | font-size: 85%; 5 | background-color: var(--color-background-code-inline); 6 | border-radius: var(--border-radius); 7 | } 8 | 9 | pre > code { 10 | margin: 0; 11 | padding: 0; 12 | font-size: 100%; 13 | white-space: pre; 14 | word-break: normal; 15 | background: transparent; 16 | border: 0; 17 | } 18 | 19 | .chroma { 20 | margin-bottom: var(--spacing-normal); 21 | } 22 | 23 | .chroma pre { 24 | margin-bottom: 0; 25 | word-break: normal; 26 | } 27 | 28 | .chroma pre, 29 | pre { 30 | padding: var(--spacing-normal); 31 | overflow: auto; 32 | font-size: 85%; 33 | line-height: 1.45; 34 | background-color: var(--color-background-code-block); 35 | border-radius: var(--border-radius); 36 | } 37 | 38 | pre code { 39 | display: inline; 40 | max-width: auto; 41 | margin: 0; 42 | padding: 0; 43 | overflow: visible; 44 | line-height: inherit; 45 | word-wrap: normal; 46 | background-color: initial; 47 | border: 0; 48 | } 49 | -------------------------------------------------------------------------------- /QLPlugin/Resources/shared/shared-chroma.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Light mode: Colors from "Default (light)" Xcode theme 3 | */ 4 | /* Keyword */ .chroma .k { color: #9B2393 } 5 | /* KeywordConstant */ .chroma .kc { color: #9B2393 } 6 | /* KeywordDeclaration */ .chroma .kd { color: #9B2393 } 7 | /* KeywordNamespace */ .chroma .kn { color: #9B2393 } 8 | /* KeywordPseudo */ .chroma .kp { color: #9B2393 } 9 | /* KeywordReserved */ .chroma .kr { color: #9B2393 } 10 | /* KeywordType */ .chroma .kt { color: #9B2393 } 11 | /* Name */ .chroma .n { color: #000000 } 12 | /* NameAttribute */ .chroma .na { color: #815F03 } 13 | /* NameBuiltin */ .chroma .nb { color: #9B2393 } 14 | /* NameBuiltinPseudo */ .chroma .bp { color: #9B2393 } 15 | /* NameClass */ .chroma .nc { color: #1C464A } 16 | /* NameConstant */ .chroma .no { color: #326D74 } 17 | /* NameDecorator */ .chroma .nd { color: #000000 } 18 | /* NameEntity */ .chroma .ni { color: #000000 } 19 | /* NameException */ .chroma .ne { color: #000000 } 20 | /* NameFunction */ .chroma .nf { color: #326D74 } 21 | /* NameFunctionMagic */ .chroma .fm { color: #326D74 } 22 | /* NameLabel */ .chroma .nl { color: #000000 } 23 | /* NameNamespace */ .chroma .nn { color: #000000 } 24 | /* NameOther */ .chroma .nx { color: #000000 } 25 | /* NameProperty */ .chroma .py { color: #000000 } 26 | /* NameTag */ .chroma .nt { color: #000000 } 27 | /* NameVariable */ .chroma .nv { color: #000000 } 28 | /* NameVariableClass */ .chroma .vc { color: #000000 } 29 | /* NameVariableGlobal */ .chroma .vg { color: #326D74 } 30 | /* NameVariableInstance */ .chroma .vi { color: #326D74 } 31 | /* NameVariableMagic */ .chroma .vm { color: #326D74 } 32 | /* Literal */ .chroma .l { color: #1C00CF } 33 | /* LiteralDate */ .chroma .ld { color: #1C00CF } 34 | /* LiteralString */ .chroma .s { color: #C41A16 } 35 | /* LiteralStringAffix */ .chroma .sa { color: #C41A16 } 36 | /* LiteralStringBacktick */ .chroma .sb { color: #C41A16 } 37 | /* LiteralStringChar */ .chroma .sc { color: #1C00CF } 38 | /* LiteralStringDelimiter */ .chroma .dl { color: #C41A16 } 39 | /* LiteralStringDoc */ .chroma .sd { color: #C41A16 } 40 | /* LiteralStringDouble */ .chroma .s2 { color: #C41A16 } 41 | /* LiteralStringEscape */ .chroma .se { color: #C41A16 } 42 | /* LiteralStringHeredoc */ .chroma .sh { color: #C41A16 } 43 | /* LiteralStringInterpol */ .chroma .si { color: #C41A16 } 44 | /* LiteralStringOther */ .chroma .sx { color: #C41A16 } 45 | /* LiteralStringRegex */ .chroma .sr { color: #C41A16 } 46 | /* LiteralStringSingle */ .chroma .s1 { color: #C41A16 } 47 | /* LiteralStringSymbol */ .chroma .ss { color: #C41A16 } 48 | /* LiteralNumber */ .chroma .m { color: #1C00CF } 49 | /* LiteralNumberBin */ .chroma .mb { color: #1C00CF } 50 | /* LiteralNumberFloat */ .chroma .mf { color: #1C00CF } 51 | /* LiteralNumberHex */ .chroma .mh { color: #1C00CF } 52 | /* LiteralNumberInteger */ .chroma .mi { color: #1C00CF } 53 | /* LiteralNumberIntegerLong */ .chroma .il { color: #1C00CF } 54 | /* LiteralNumberOct */ .chroma .mo { color: #1C00CF } 55 | /* Operator */ .chroma .o { color: #000000 } 56 | /* OperatorWord */ .chroma .ow { color: #000000 } 57 | /* Comment */ .chroma .c { color: #5D6C79 } 58 | /* CommentHashbang */ .chroma .ch { color: #5D6C79 } 59 | /* CommentMultiline */ .chroma .cm { color: #5D6C79 } 60 | /* CommentSingle */ .chroma .c1 { color: #5D6C79 } 61 | /* CommentSpecial */ .chroma .cs { color: #4A5560 } 62 | /* CommentPreproc */ .chroma .cp { color: #643820 } 63 | /* CommentPreprocFile */ .chroma .cpf { color: #643820 } 64 | 65 | /** 66 | * Dark mode: Colors from "Default (dark)" Xcode theme 67 | */ 68 | @media (prefers-color-scheme: dark) { 69 | /* Keyword */ .chroma .k { color: #FC5FA3 } 70 | /* KeywordConstant */ .chroma .kc { color: #FC5FA3 } 71 | /* KeywordDeclaration */ .chroma .kd { color: #FC5FA3 } 72 | /* KeywordNamespace */ .chroma .kn { color: #FC5FA3 } 73 | /* KeywordPseudo */ .chroma .kp { color: #FC5FA3 } 74 | /* KeywordReserved */ .chroma .kr { color: #FC5FA3 } 75 | /* KeywordType */ .chroma .kt { color: #FC5FA3 } 76 | /* Name */ .chroma .n { color: #ffffff } 77 | /* NameAttribute */ .chroma .na { color: #BF8555 } 78 | /* NameBuiltin */ .chroma .nb { color: #FC5FA3 } 79 | /* NameBuiltinPseudo */ .chroma .bp { color: #FC5FA3 } 80 | /* NameClass */ .chroma .nc { color: #9EF1DD } 81 | /* NameConstant */ .chroma .no { color: #67B7A4 } 82 | /* NameDecorator */ .chroma .nd { color: #ffffff } 83 | /* NameEntity */ .chroma .ni { color: #ffffff } 84 | /* NameException */ .chroma .ne { color: #ffffff } 85 | /* NameFunction */ .chroma .nf { color: #67B7A4 } 86 | /* NameFunctionMagic */ .chroma .fm { color: #67B7A4 } 87 | /* NameLabel */ .chroma .nl { color: #ffffff } 88 | /* NameNamespace */ .chroma .nn { color: #ffffff } 89 | /* NameOther */ .chroma .nx { color: #ffffff } 90 | /* NameProperty */ .chroma .py { color: #ffffff } 91 | /* NameTag */ .chroma .nt { color: #ffffff } 92 | /* NameVariable */ .chroma .nv { color: #ffffff } 93 | /* NameVariableClass */ .chroma .vc { color: #ffffff } 94 | /* NameVariableGlobal */ .chroma .vg { color: #67B7A4 } 95 | /* NameVariableInstance */ .chroma .vi { color: #67B7A4 } 96 | /* NameVariableMagic */ .chroma .vm { color: #67B7A4 } 97 | /* Literal */ .chroma .l { color: #D0BF69 } 98 | /* LiteralDate */ .chroma .ld { color: #D0BF69 } 99 | /* LiteralString */ .chroma .s { color: #FC6A5D } 100 | /* LiteralStringAffix */ .chroma .sa { color: #FC6A5D } 101 | /* LiteralStringBacktick */ .chroma .sb { color: #FC6A5D } 102 | /* LiteralStringChar */ .chroma .sc { color: #D0BF69 } 103 | /* LiteralStringDelimiter */ .chroma .dl { color: #FC6A5D } 104 | /* LiteralStringDoc */ .chroma .sd { color: #FC6A5D } 105 | /* LiteralStringDouble */ .chroma .s2 { color: #FC6A5D } 106 | /* LiteralStringEscape */ .chroma .se { color: #FC6A5D } 107 | /* LiteralStringHeredoc */ .chroma .sh { color: #FC6A5D } 108 | /* LiteralStringInterpol */ .chroma .si { color: #FC6A5D } 109 | /* LiteralStringOther */ .chroma .sx { color: #FC6A5D } 110 | /* LiteralStringRegex */ .chroma .sr { color: #FC6A5D } 111 | /* LiteralStringSingle */ .chroma .s1 { color: #FC6A5D } 112 | /* LiteralStringSymbol */ .chroma .ss { color: #FC6A5D } 113 | /* LiteralNumber */ .chroma .m { color: #D0BF69 } 114 | /* LiteralNumberBin */ .chroma .mb { color: #D0BF69 } 115 | /* LiteralNumberFloat */ .chroma .mf { color: #D0BF69 } 116 | /* LiteralNumberHex */ .chroma .mh { color: #D0BF69 } 117 | /* LiteralNumberInteger */ .chroma .mi { color: #D0BF69 } 118 | /* LiteralNumberIntegerLong */ .chroma .il { color: #D0BF69 } 119 | /* LiteralNumberOct */ .chroma .mo { color: #D0BF69 } 120 | /* Operator */ .chroma .o { color: #ffffff } 121 | /* OperatorWord */ .chroma .ow { color: #ffffff } 122 | /* Comment */ .chroma .c { color: #6C7986 } 123 | /* CommentHashbang */ .chroma .ch { color: #6C7986 } 124 | /* CommentMultiline */ .chroma .cm { color: #6C7986 } 125 | /* CommentSingle */ .chroma .c1 { color: #6C7986 } 126 | /* CommentSpecial */ .chroma .cs { color: #92A1B1 } 127 | /* CommentPreproc */ .chroma .cp { color: #FD8F3F } 128 | /* CommentPreprocFile */ .chroma .cpf { color: #FD8F3F } 129 | } 130 | -------------------------------------------------------------------------------- /QLPlugin/Resources/shared/shared-main.css: -------------------------------------------------------------------------------- 1 | /* Variables */ 2 | 3 | /* General */ 4 | body { 5 | --border-radius: 3px; 6 | --color-background-body: #ffffff; 7 | --color-background-code-block: #f6f8fa; 8 | --color-background-code-inline: #f3f3f3; 9 | --color-background-error: #ffdddd; 10 | --color-border: #dfe2e5; 11 | --color-border-heading: #eaecef; 12 | --color-text: #000000; 13 | --color-text-faded: #6a737d; 14 | --color-text-link: #0366d6; 15 | --font-family-monospace: "Menlo", monospace; 16 | --font-family-sans-serif: -apple-system, BlinkMacSystemFont, "Helvetica", sans-serif; 17 | --font-weight-bold: 600; 18 | --spacing-normal: 16px; 19 | --spacing-large: 24px; 20 | } 21 | 22 | /* Dark mode */ 23 | @media (prefers-color-scheme: dark) { 24 | body { 25 | --color-background-body: #1e1e1e; 26 | --color-background-code-block: #111111; 27 | --color-background-code-inline: #090909; 28 | --color-background-error: #3c0404; 29 | --color-border: #6f6f6f; 30 | --color-border-heading: #6a6a6a; 31 | --color-text: #ffffff; 32 | --color-text-faded: #9b9b9b; 33 | --color-text-link: #4ba0ff; 34 | } 35 | } 36 | 37 | /* Global styles */ 38 | 39 | * { 40 | box-sizing: border-box; 41 | } 42 | 43 | body { 44 | padding: var(--spacing-normal); 45 | overflow: auto; 46 | color: var(--color-text); 47 | font-size: 14px; 48 | font-family: var(--font-family-sans-serif); 49 | line-height: 1.5; 50 | tab-size: 4; 51 | background: var(--color-background-body); 52 | 53 | /* Disable text selection (the copy shortcut doesn't work and no context menu can be opened) */ 54 | -webkit-user-select: none; 55 | user-select: none; 56 | } 57 | 58 | /* General element spacing */ 59 | blockquote, 60 | ol, 61 | ul, 62 | p, 63 | pre, 64 | table { 65 | margin-top: 0; 66 | margin-bottom: var(--spacing-normal); 67 | } 68 | 69 | body > :first-child { 70 | margin-top: 0; 71 | } 72 | 73 | body > :last-child { 74 | margin-bottom: 0; 75 | } 76 | 77 | /* Text */ 78 | 79 | h1, 80 | h2, 81 | h3, 82 | h4, 83 | h5, 84 | h6 { 85 | margin-top: var(--spacing-large); 86 | margin-bottom: var(--spacing-normal); 87 | font-weight: var(--font-weight-bold); 88 | } 89 | 90 | h1, 91 | h2 { 92 | padding-bottom: 0.1em; 93 | border-bottom: 1px solid var(--color-border-heading); 94 | } 95 | 96 | h1 { 97 | margin-top: 0; 98 | font-size: 2em; 99 | } 100 | 101 | h2 { 102 | font-size: 1.5em; 103 | } 104 | 105 | h3 { 106 | font-size: 1.25em; 107 | } 108 | 109 | h4 { 110 | font-size: 1em; 111 | } 112 | 113 | h5 { 114 | font-size: 0.875em; 115 | } 116 | 117 | h6 { 118 | color: var(--color-text-faded); 119 | font-size: 0.85em; 120 | } 121 | 122 | a { 123 | color: var(--color-text-link); 124 | text-decoration: none; 125 | } 126 | 127 | /* Lists */ 128 | 129 | ol, 130 | ul { 131 | padding-left: 2em; 132 | } 133 | 134 | ol ol, 135 | ul ol { 136 | list-style-type: lower-roman; 137 | } 138 | 139 | ol ol ol, 140 | ol ul ol, 141 | ul ol ol, 142 | ul ul ol { 143 | list-style-type: lower-alpha; 144 | } 145 | 146 | li + li { 147 | margin-top: 0.25em; 148 | } 149 | 150 | li > p { 151 | margin-top: var(--spacing-normal); 152 | } 153 | 154 | /* Horizontal rules */ 155 | 156 | hr { 157 | margin: 24px 0; 158 | border: 0; 159 | border-bottom: 4px solid var(--color-border-heading); 160 | } 161 | 162 | /* Quotes */ 163 | 164 | blockquote { 165 | margin-right: 0; 166 | margin-left: 0; 167 | padding: 0 1em; 168 | color: var(--color-text-faded); 169 | border-left: 0.25em solid var(--color-border); 170 | } 171 | 172 | blockquote > :first-child { 173 | margin-top: 0; 174 | } 175 | 176 | blockquote > :last-child { 177 | margin-bottom: 0; 178 | } 179 | 180 | /* Images */ 181 | img { 182 | max-width: 100%; 183 | } 184 | 185 | /* Code */ 186 | 187 | code, 188 | pre { 189 | font-size: 0.85em; 190 | font-family: var(--font-family-monospace); 191 | } 192 | 193 | /* Tables */ 194 | 195 | table { 196 | display: block; 197 | width: 100%; 198 | overflow-x: auto; 199 | border-collapse: collapse; 200 | border-spacing: 0; 201 | } 202 | 203 | th { 204 | font-weight: var(--font-weight-bold); 205 | } 206 | 207 | td, 208 | th, 209 | tr { 210 | border: 1px solid var(--color-border); 211 | } 212 | 213 | td, 214 | th { 215 | padding: 6px 13px; 216 | } 217 | 218 | table tr:nth-child(even) { 219 | background-color: var(--color-background-code-block); 220 | } 221 | -------------------------------------------------------------------------------- /QLPlugin/Utils/File.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum FileError: Error { 4 | case fileAttributeError(path: String, message: String) 5 | case fileNotFoundError(path: String) 6 | case fileReadError(path: String, message: String) 7 | } 8 | 9 | extension FileError: LocalizedError { 10 | public var errorDescription: String? { 11 | switch self { 12 | case let .fileAttributeError(path, message): 13 | return NSLocalizedString( 14 | "Could not get attributes for file at path \(path): \(message)", 15 | comment: "" 16 | ) 17 | case let .fileNotFoundError(path): 18 | return NSLocalizedString("Could not find file at path \(path)", comment: "") 19 | case let .fileReadError(path, message): 20 | return NSLocalizedString( 21 | "Could not read file at path \(path): \(message)", 22 | comment: "" 23 | ) 24 | } 25 | } 26 | } 27 | 28 | /// Utility class for reading the content and metadata of the corresponding file. 29 | class File { 30 | let archiveExtensions = ["tar", "tar.gz", "zip"] 31 | let fileManager = FileManager.default 32 | 33 | var attributes: [FileAttributeKey: Any] 34 | var isDirectory: Bool 35 | var path: String 36 | var url: URL 37 | 38 | var isArchive: Bool { archiveExtensions.contains(url.pathExtension) } 39 | var size: Int { attributes[.size] as? Int ?? 0 } 40 | 41 | /// Looks for a file at the provided URL and saves its metadata as object properties. 42 | init(url: URL) throws { 43 | self.url = url 44 | path = url.path 45 | 46 | // Check whether the provided URL points to a directory 47 | var isDirectoryObjC: ObjCBool = false 48 | guard fileManager.fileExists(atPath: path, isDirectory: &isDirectoryObjC) else { 49 | throw FileError.fileNotFoundError(path: path) 50 | } 51 | isDirectory = isDirectoryObjC.boolValue 52 | 53 | // Read file attributes (e.g. file size) 54 | do { 55 | attributes = try fileManager.attributesOfItem(atPath: path) 56 | } catch let error as NSError { 57 | throw FileError.fileAttributeError(path: path, message: error.localizedDescription) 58 | } 59 | } 60 | 61 | /// Reads and returns the file's content as an UTF-8 string. 62 | func read() throws -> String { 63 | do { 64 | return try String(contentsOf: url, encoding: .utf8) 65 | } catch { 66 | throw FileError.fileReadError(path: path, message: error.localizedDescription) 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /QLPlugin/Utils/FileTree.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum FileTreeError { 4 | case notADirectoryError(pathParts: [String.SubSequence], pathPartIndex: Int) 5 | } 6 | 7 | extension FileTreeError: LocalizedError { 8 | public var errorDescription: String? { 9 | switch self { 10 | case let .notADirectoryError(pathParts, pathPartIndex): 11 | return NSLocalizedString( 12 | "Cannot create file tree node with path \"\(pathParts.joined())\": \"\(pathParts[pathPartIndex])\" is not a directory", 13 | comment: "" 14 | ) 15 | } 16 | } 17 | } 18 | 19 | /// Data structure for representing a single file/directory in a tree. The class is designed to be 20 | /// used in an `NSOutlineView`, which is why the `@objc` attributes are required. 21 | class FileTreeNode: NSObject { 22 | /// Name of the file (without path information), e.g. `"myfile.txt"` 23 | @objc let name: String 24 | /// File size in bytes 25 | @objc let size: Int 26 | @objc let isDirectory: Bool 27 | @objc var dateModified: Date? 28 | /// Child nodes of a directory 29 | @objc var children = [String: FileTreeNode]() 30 | 31 | /// Number of child nodes (required for rendering the tree in an `NSOutlineView`) 32 | @objc var childrenCount: Int { children.values.count } 33 | /// List of child nodes (required for rendering the tree in an `NSOutlineView`) 34 | @objc var childrenList: [FileTreeNode] { Array(children.values) } 35 | /// Whether the node has any children (required for rendering the tree in an `NSOutlineView`) 36 | @objc var hasChildren: Bool { children.isEmpty } 37 | 38 | convenience init(name: String, size: Int, isDirectory: Bool) { 39 | self.init(name: name, size: size, isDirectory: isDirectory, dateModified: nil) 40 | } 41 | 42 | init(name: String, size: Int, isDirectory: Bool, dateModified: Date?) { 43 | self.name = name 44 | self.size = size 45 | self.isDirectory = isDirectory 46 | self.dateModified = dateModified 47 | } 48 | } 49 | 50 | /// Data structure for representing a tree of files and directories. This class stores the root node 51 | /// and provides functionality to insert new nodes. 52 | class FileTree { 53 | var root = FileTreeNode(name: "Root", size: 0, isDirectory: true, dateModified: Date()) 54 | 55 | /// Parses the provided file/directory's path and creates a new `FileTreeNode` at the correct 56 | /// position in the tree. If a file/directory's parent directory doesn't exist yet, it will 57 | /// be created (with `dateModified` set to `nil`). 58 | func addNode(path: String, isDirectory: Bool, size: Int, dateModified: Date?) throws { 59 | try addNode( 60 | parentNode: root, 61 | pathParts: path.split(separator: "/", omittingEmptySubsequences: true), 62 | pathPartIndex: 0, 63 | isDirectory: isDirectory, 64 | size: size, 65 | dateModified: dateModified 66 | ) 67 | } 68 | 69 | /// Parses the provided file/directory's path and creates a new `FileTreeNode` at the correct 70 | /// position in the tree. This is a helper function for the `addNode` function. It performs a 71 | /// recursive tree traversal to find the node's location. 72 | private func addNode( 73 | parentNode: FileTreeNode, 74 | pathParts: [String.SubSequence], 75 | pathPartIndex: Int, 76 | isDirectory: Bool, 77 | size: Int, 78 | dateModified: Date? 79 | ) throws { 80 | let isLastPathPart = pathPartIndex == pathParts.count - 1 81 | let name = String(pathParts[pathPartIndex]) 82 | var currentNode = parentNode.children[name] 83 | 84 | if isLastPathPart { 85 | // Reached end of path: Add to tree 86 | if currentNode == nil { 87 | // Node doesn't exist yet: Create it 88 | parentNode.children[name] = FileTreeNode( 89 | name: name, 90 | size: size, 91 | isDirectory: isDirectory, 92 | dateModified: dateModified 93 | ) 94 | } else { 95 | // Node already exists (i.e. directory has been created implicitly in a previous 96 | // function call): Update the directory node with the missing `dateModified` info 97 | currentNode!.dateModified = dateModified 98 | } 99 | } else { 100 | // Not yet at end of path: Recurse into subdirectory 101 | if currentNode == nil { 102 | // Directory that doesn't exist yet: Create it 103 | currentNode = FileTreeNode( 104 | name: name, 105 | size: 0, 106 | isDirectory: true 107 | ) 108 | parentNode.children[name] = currentNode 109 | } else { 110 | // Directory exists: Make sure it's not a file 111 | if !currentNode!.isDirectory { 112 | throw FileTreeError.notADirectoryError( 113 | pathParts: pathParts, 114 | pathPartIndex: pathPartIndex 115 | ) 116 | } 117 | } 118 | // Recurse: Execute function again for next path part 119 | try addNode( 120 | parentNode: currentNode!, 121 | pathParts: pathParts, 122 | pathPartIndex: pathPartIndex + 1, 123 | isDirectory: isDirectory, 124 | size: size, 125 | dateModified: dateModified 126 | ) 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /QLPlugin/Utils/HTMLRenderer.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import HTMLConverter 3 | 4 | extension String { 5 | /// Converts the Swift string to a C string. 6 | func toCString() -> UnsafeMutablePointer { 7 | UnsafeMutablePointer (mutating: (self as NSString).utf8String!) 8 | } 9 | } 10 | 11 | enum HTMLRendererError { 12 | case rendererError(fileType: String, errorMessage: String) 13 | } 14 | 15 | extension HTMLRendererError: LocalizedError { 16 | public var errorDescription: String? { 17 | switch self { 18 | case let .rendererError(fileType, errorMessage): 19 | return NSLocalizedString( 20 | "Could not convert \(fileType) to HTML: \(errorMessage)", 21 | comment: "" 22 | ) 23 | } 24 | } 25 | } 26 | 27 | class HTMLRenderer { 28 | /// Throws an error if the return value indicates one. Because all `HTMLConverter` return values 29 | /// are C strings, errors are implemented as return values starting with "error: ". 30 | static func throwIfErrored(fileType: String, returnValue: String) throws { 31 | if returnValue.hasPrefix("error :") { 32 | let startIndex = returnValue.index(returnValue.startIndex, offsetBy: 7) 33 | let errorMessage = returnValue[startIndex ..< returnValue.endIndex] 34 | throw HTMLRendererError.rendererError( 35 | fileType: fileType, 36 | errorMessage: String(errorMessage) 37 | ) 38 | } 39 | } 40 | 41 | /// Converts a code string to HTML with support for syntax highlighting. 42 | static func renderCode(_ source: String, lexer: String) throws -> String { 43 | let htmlCString = convertCodeToHTML(source.toCString(), lexer.toCString()) 44 | let htmlString = String(cString: htmlCString!) 45 | try throwIfErrored(fileType: "code", returnValue: htmlString) 46 | return htmlString 47 | } 48 | 49 | /// Converts a Markdown string to HTML. 50 | static func renderMarkdown(_ source: String) throws -> String { 51 | let htmlCString = convertMarkdownToHTML(source.toCString()) 52 | let htmlString = String(cString: htmlCString!) 53 | try throwIfErrored(fileType: "Markdown", returnValue: htmlString) 54 | return htmlString 55 | } 56 | 57 | /// Converts a Jupyter Notebook JSON file to HTML. 58 | static func renderNotebook(_ source: String) throws -> String { 59 | let htmlCString = convertNotebookToHTML(source.toCString()) 60 | let htmlString = String(cString: htmlCString!) 61 | try throwIfErrored(fileType: "Jupyter Notebook", returnValue: htmlString) 62 | return htmlString 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /QLPlugin/Utils/WebAsset/Script.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class Script: WebAsset { 4 | private var content: String? 5 | private var url: URL? 6 | 7 | required init(content: String) { 8 | self.content = content 9 | } 10 | 11 | required init(url: URL) { 12 | self.url = url 13 | } 14 | 15 | func getHTML() -> String { 16 | if let url = url { 17 | return "" 18 | } else { 19 | return "" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /QLPlugin/Utils/WebAsset/Stylesheet.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class Stylesheet: WebAsset { 4 | private var content: String? 5 | private var url: URL? 6 | 7 | required init(content: String) { 8 | self.content = content 9 | } 10 | 11 | required init(url: URL) { 12 | self.url = url 13 | } 14 | 15 | func getHTML() -> String { 16 | if let url = url { 17 | return "" 18 | } else { 19 | return "" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /QLPlugin/Utils/WebAsset/WebAsset.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | protocol WebAsset { 4 | init(content: String) 5 | init(url: URL) 6 | func getHTML() -> String 7 | } 8 | -------------------------------------------------------------------------------- /QLPlugin/Views/PreviewVC.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import Foundation 3 | import os.log 4 | 5 | /// View controller for rendering previews of a specific file type. 6 | protocol PreviewVC: NSViewController {} 7 | 8 | /// Class that can be used to create an instance of a `PreviewVC` for the corresponding file type. 9 | protocol Preview { 10 | init() 11 | func createPreviewVC(file: File) throws -> PreviewVC 12 | } 13 | -------------------------------------------------------------------------------- /QLPlugin/Views/PreviewVCFactory.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Returns a `PreviewVC` subclass that can be used to generate a preview of the provided file. 4 | /// May return `nil` if the file is not supported. 5 | class PreviewVCFactory { 6 | static func getPreviewInitializer(fileURL: URL) -> Preview.Type? { 7 | switch fileURL.pathExtension.lowercased() { 8 | case "applescript", "scpt", "scptd": 9 | return AppleScriptPreview.self 10 | case "gz": 11 | // `gzip` is only supported for tarballs 12 | return fileURL.path.hasSuffix(".tar.gz") ? TARPreview.self : nil 13 | case "md", "markdown", "mdown", "mkdn", "mkd", "rmd": 14 | return MarkdownPreview.self 15 | case "ipynb": 16 | return JupyterPreview.self 17 | case "tar": 18 | return TARPreview.self 19 | case "tab", "tsv": 20 | return TSVPreview.self 21 | case "ear", "jar", "war", "zip": 22 | return ZIPPreview.self 23 | default: 24 | return CodePreview.self 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /QLPlugin/Views/PreviewVCs/OutlinePreviewVC.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | class OutlinePreviewVC: NSViewController, PreviewVC { 4 | @objc dynamic var rootNodes: [FileTreeNode] 5 | private let labelText: String? 6 | 7 | @objc dynamic var customSortDescriptors = [NSSortDescriptor(key: "name", ascending: true)] 8 | 9 | @IBOutlet private var treeController: NSTreeController! 10 | @IBOutlet private var outlineView: NSOutlineView! 11 | @IBOutlet private var label: NSTextField! 12 | 13 | required convenience init(rootNodes: [FileTreeNode], labelText: String?) { 14 | self.init(nibName: nil, bundle: nil, rootNodes: rootNodes, labelText: labelText) 15 | } 16 | 17 | init( 18 | nibName nibNameOrNil: NSNib.Name?, 19 | bundle nibBundleOrNil: Bundle?, 20 | rootNodes: [FileTreeNode], 21 | labelText: String? 22 | ) { 23 | self.rootNodes = rootNodes 24 | self.labelText = labelText 25 | super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) 26 | 27 | // Register required value transformers 28 | ValueTransformer.setValueTransformer(DateTransformer(), forName: .dateTransformerName) 29 | ValueTransformer.setValueTransformer(IconTransformer(), forName: .iconTransformerName) 30 | ValueTransformer.setValueTransformer(SizeTransformer(), forName: .sizeTransformerName) 31 | } 32 | 33 | @available(*, unavailable) 34 | required init?(coder _: NSCoder) { 35 | fatalError("init(coder:) has not been implemented") 36 | } 37 | 38 | override func viewDidLoad() { 39 | super.viewDidLoad() 40 | setUpView() 41 | expandSingleRootItem() 42 | } 43 | 44 | private func setUpView() { 45 | // Add file tree to `treeController` 46 | for node in rootNodes { 47 | treeController.addObject(node) 48 | } 49 | 50 | // Add label 51 | label.stringValue = labelText ?? "" 52 | } 53 | 54 | /// If the root contains a single item, this function expands its children. 55 | private func expandSingleRootItem() { 56 | let root = treeController.arrangedObjects 57 | if root.children?.count == 1 { 58 | outlineView.expandItem(root.children?.first!) 59 | } 60 | } 61 | } 62 | 63 | /// `ValueTransformer` which formats the provided date. 64 | class DateTransformer: ValueTransformer { 65 | let dateFormatter = DateFormatter() 66 | let fallbackValue = "--" 67 | 68 | override init() { 69 | // Use same date format as Finder 70 | dateFormatter.dateStyle = .medium 71 | dateFormatter.timeStyle = .short 72 | dateFormatter.doesRelativeDateFormatting = true 73 | } 74 | 75 | override class func transformedValueClass() -> AnyClass { NSString.self } 76 | 77 | override class func allowsReverseTransformation() -> Bool { false } 78 | 79 | override func transformedValue(_ value: Any?) -> Any? { 80 | guard let date = value as? Date else { 81 | return nil 82 | } 83 | 84 | // Dates which are `nil` are passed to this function as epoch dates (default value). If 85 | // this is the case, return "--" instead (same behavior as Finder) 86 | return date.timeIntervalSince1970 == 0 ? fallbackValue : dateFormatter.string(from: date) 87 | } 88 | } 89 | 90 | /// `ValueTransformer` which returns the correct icon depending on whether the current row 91 | /// represents a file or directory. 92 | class IconTransformer: ValueTransformer { 93 | let directoryIcon = NSImage( 94 | contentsOfFile: "/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/GenericFolderIcon.icns" 95 | ) 96 | let fileIcon = NSImage( 97 | contentsOfFile: "/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/GenericDocumentIcon.icns" 98 | ) 99 | 100 | override class func transformedValueClass() -> AnyClass { NSImage.self } 101 | 102 | override class func allowsReverseTransformation() -> Bool { false } 103 | 104 | override func transformedValue(_ value: Any?) -> Any? { 105 | guard let isDirectoryNumber = value as? NSNumber else { 106 | return nil 107 | } 108 | let isDirectory = Bool(truncating: isDirectoryNumber) 109 | return isDirectory ? directoryIcon : fileIcon 110 | } 111 | } 112 | 113 | /// `ValueTransformer` which formats the provided number of bytes as a human-readable string (e.g. 114 | /// `12345` -> `"12.345 KB"` or `0` -> `"--"`). 115 | class SizeTransformer: ValueTransformer { 116 | let byteCountFormatter = ByteCountFormatter() 117 | let fallbackValue = "--" 118 | 119 | override class func transformedValueClass() -> AnyClass { NSString.self } 120 | 121 | override class func allowsReverseTransformation() -> Bool { false } 122 | 123 | override func transformedValue(_ value: Any?) -> Any? { 124 | guard let size = value as? NSNumber else { 125 | return nil 126 | } 127 | 128 | // Format number of bytes in human-readable way. If the size is 0 bytes, return "--" instead 129 | // (same behavior as Finder) 130 | return size == 0 ? fallbackValue : (byteCountFormatter.string(for: size) ?? fallbackValue) 131 | } 132 | } 133 | 134 | extension NSValueTransformerName { 135 | static let dateTransformerName = NSValueTransformerName(rawValue: "DateTransformer") 136 | static let iconTransformerName = NSValueTransformerName(rawValue: "IconTransformer") 137 | static let sizeTransformerName = NSValueTransformerName(rawValue: "SizeTransformer") 138 | } 139 | -------------------------------------------------------------------------------- /QLPlugin/Views/PreviewVCs/TablePreviewVC.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import os.log 3 | 4 | class TablePreviewVC: NSViewController, PreviewVC { 5 | let headers: [String] 6 | let cells: [[String: String]] 7 | 8 | @IBOutlet private var tableView: NSTableView! 9 | 10 | required convenience init(headers: [String], cells: [[String: String]]) { 11 | self.init(nibName: nil, bundle: nil, headers: headers, cells: cells) 12 | } 13 | 14 | init( 15 | nibName nibNameOrNil: NSNib.Name?, 16 | bundle nibBundleOrNil: Bundle?, 17 | headers: [String], 18 | cells: [[String: String]] 19 | ) { 20 | self.headers = headers 21 | self.cells = cells 22 | super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) 23 | } 24 | 25 | @available(*, unavailable) 26 | required init?(coder _: NSCoder) { 27 | fatalError("init(coder:) has not been implemented") 28 | } 29 | 30 | override func viewDidLoad() { 31 | super.viewDidLoad() 32 | setUpView() 33 | deleteDefaultColumns() 34 | createColumns() 35 | } 36 | 37 | private func setUpView() { 38 | tableView.delegate = self 39 | tableView.dataSource = self 40 | } 41 | 42 | /// Deletes all columns created by default using Interface Builder. 43 | private func deleteDefaultColumns() { 44 | while !tableView.tableColumns.isEmpty { 45 | if let column = tableView.tableColumns.first { 46 | tableView.removeTableColumn(column) 47 | } 48 | } 49 | } 50 | 51 | /// Creates table columns for all headers. 52 | private func createColumns() { 53 | for header in headers { 54 | let columnID = NSUserInterfaceItemIdentifier(rawValue: header) 55 | let column = NSTableColumn(identifier: columnID) 56 | column.title = header 57 | column.minWidth = 50 58 | column.maxWidth = 500 59 | tableView.addTableColumn(column) 60 | } 61 | } 62 | } 63 | 64 | extension TablePreviewVC: NSTableViewDataSource, NSTableViewDelegate { 65 | func numberOfRows(in _: NSTableView) -> Int { 66 | cells.count 67 | } 68 | 69 | /// Fills the table with the `tableData`. 70 | func tableView( 71 | _: NSTableView, 72 | viewFor tableColumn: NSTableColumn?, 73 | row rowIndex: Int 74 | ) -> NSView? { 75 | let row = cells[rowIndex] 76 | let cellValue = row[tableColumn!.identifier.rawValue] ?? "" 77 | 78 | let textField = NSTextField() 79 | textField.stringValue = cellValue 80 | textField.isEditable = false 81 | textField.isBordered = false 82 | textField.drawsBackground = false 83 | textField.lineBreakMode = .byTruncatingTail 84 | 85 | return textField 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /QLPlugin/Views/PreviewVCs/TablePreviewVC.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 95 | -------------------------------------------------------------------------------- /QLPlugin/Views/PreviewVCs/WebPreviewVC.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import Foundation 3 | import os.log 4 | import WebKit 5 | 6 | class WebPreviewVC: NSViewController, PreviewVC { 7 | private let html: String 8 | private let stylesheets: [Stylesheet] 9 | private let scripts: [Script] 10 | 11 | /// Stylesheet with CSS that applies to all file types 12 | private let sharedStylesheetURL = Bundle.main.url( 13 | forResource: "shared-main", 14 | withExtension: "css" 15 | ) 16 | 17 | @IBOutlet private var webView: OfflineWebView! 18 | 19 | required convenience init( 20 | html: String, 21 | stylesheets: [Stylesheet] = [], 22 | scripts: [Script] = [] 23 | ) { 24 | self.init(nibName: nil, bundle: nil, html: html, stylesheets: stylesheets, scripts: scripts) 25 | } 26 | 27 | init( 28 | nibName nibNameOrNil: NSNib.Name?, 29 | bundle nibBundleOrNil: Bundle?, 30 | html: String, 31 | stylesheets: [Stylesheet] = [], 32 | scripts: [Script] = [] 33 | ) { 34 | self.html = html 35 | self.stylesheets = [Stylesheet(url: sharedStylesheetURL!)] + stylesheets 36 | self.scripts = scripts 37 | super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) 38 | } 39 | 40 | @available(*, unavailable) 41 | required init?(coder _: NSCoder) { 42 | fatalError("init(coder:) has not been implemented") 43 | } 44 | 45 | override func viewDidLoad() { 46 | super.viewDidLoad() 47 | setUpView() 48 | loadPreview() 49 | } 50 | 51 | private func setUpView() { 52 | // Remove background to prevent white flicker on load in Dark Mode 53 | webView.setValue(false, forKey: "drawsBackground") 54 | } 55 | 56 | private func loadPreview() { 57 | let linkTags = stylesheets 58 | .map { $0.getHTML() } 59 | .joined(separator: "\n") 60 | let scriptTags = scripts 61 | .map { $0.getHTML() } 62 | .joined(separator: "\n") 63 | 64 | webView.loadHTMLString(""" 65 | 66 | 67 | 68 | 69 | 73 | \(linkTags) 74 | 75 | 76 | \(html) 77 | \(scriptTags) 78 | 79 | 80 | """, baseURL: Bundle.main.resourceURL) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /QLPlugin/Views/PreviewVCs/WebPreviewVC.xib: -------------------------------------------------------------------------------- 1 | 2 |4 | 8 |5 | 6 | 7 | 9 | 94 |10 | 15 |11 | 14 |12 | 13 | 16 | 17 | 18 | 93 |19 | 20 | 21 | 85 |22 | 84 |23 | 24 | 71 |25 | 26 | 27 | 70 |28 | 69 |29 | 30 | 31 | 32 | 33 | 34 | 35 | 68 |36 | 67 |37 | 40 |38 | 39 | 41 | 42 | 45 |43 | 44 | 46 | 47 | 66 |48 | 65 |49 | 50 | 51 | 61 |52 | 60 |53 | 54 | 55 | 56 | 59 |57 | 58 | 62 | 64 |63 | 72 | 75 |73 | 74 | 76 | 79 |77 | 78 | 80 | 83 |81 | 82 | 86 | 91 |87 | 88 | 89 | 90 | 92 | 3 | 40 | -------------------------------------------------------------------------------- /QLPlugin/Views/Previews/AppleScriptPreview.swift: -------------------------------------------------------------------------------- 1 | import SwiftExec 2 | 3 | /// View controller for previewing AppleScript files: 4 | /// 5 | /// - `.applescript`: AppleScript text file (can be read directly) 6 | /// - `.scpt`: AppleScript binary (needs to be decompiled) 7 | /// - `.scptd`: AppleScript bundle (includes a binary, which needs to be decompiled) 8 | /// 9 | /// The class extends `CodePreview` so syntax highlighting is applied after the script's content has 10 | /// been determined. 11 | /// 12 | // TODO: Scripts can also be written in JavaScript (JXA). This language needs to be detected and 13 | // passed to Chroma to get correct syntax highlighting. 14 | class AppleScriptPreview: CodePreview { 15 | override func getSource(file: File) throws -> String { 16 | if file.url.pathExtension == "scpt" || file.url.pathExtension == "scptd" { 17 | let result = try exec( 18 | program: "/usr/bin/osadecompile", 19 | arguments: [file.path] 20 | ) 21 | return result.stdout ?? "" 22 | } 23 | return try file.read() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /QLPlugin/Views/Previews/CodePreview.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import os.log 3 | 4 | let dotfileLexers = [ 5 | // Files with syntax supported by Chroma 6 | ".bashrc": ".bashrc", 7 | ".vimrc": ".vimrc", 8 | ".zprofile": "zsh", 9 | ".zshrc": ".zshrc", 10 | "dockerfile": "Dockerfile", 11 | "gemfile": "Gemfile", 12 | "gnumakefile": "Makefile", 13 | "makefile": "Makefile", 14 | "pkgbuild": "PKGBUILD", 15 | "rakefile": "Rakefile", 16 | 17 | // Files for which a different, similar syntax is used 18 | ".dockerignore": "bash", 19 | ".editorconfig": "ini", 20 | ".gitattributes": "bash", 21 | ".gitconfig": "ini", 22 | ".gitignore": "bash", 23 | ".npmignore": "bash", 24 | ".zsh_history": "txt", 25 | ] 26 | 27 | let fileExtensionLexers = [ 28 | // Files with syntax supported by Chroma 29 | "alfredappearance": "json", 30 | "cls": "tex", 31 | "entitlements": "xml", 32 | "hbs": "handlebars", 33 | "iml": "xml", 34 | "plist": "xml", 35 | "resolved": "json", 36 | "scpt": "applescript", 37 | "scptd": "applescript", 38 | "spf": "xml", 39 | "spTheme": "xml", 40 | "storyboard": "xml", 41 | "stringsdict": "xml", 42 | "sty": "tex", 43 | "webmanifest": "json", 44 | "xcscheme": "xml", 45 | "xib": "xml", 46 | 47 | // Files for which a different, similar syntax is used 48 | "liquid": "twig", 49 | "modulemap": "hcl", 50 | "njk": "twig", 51 | "pbxproj": "txt", 52 | "strings": "c", 53 | ] 54 | 55 | class CodePreview: Preview { 56 | private let chromaStylesheetURL = Bundle.main.url( 57 | forResource: "shared-chroma", 58 | withExtension: "css" 59 | ) 60 | 61 | required init() {} 62 | 63 | /// Returns the name of the Chroma lexer to use for the file. This is determined based on the 64 | /// file name/extension. 65 | private func getLexer(fileURL: URL) -> String { 66 | if fileURL.pathExtension.isEmpty { 67 | // Dotfile 68 | return dotfileLexers[fileURL.lastPathComponent.lowercased(), default: "autodetect"] 69 | } else if fileURL.pathExtension.lowercased() == "dist" { 70 | // .dist file 71 | return getLexer(fileURL: fileURL.deletingPathExtension()) 72 | } else { 73 | // File with extension 74 | return fileExtensionLexers[ 75 | fileURL.pathExtension.lowercased(), 76 | default: fileURL.pathExtension 77 | ] 78 | } 79 | } 80 | 81 | func getSource(file: File) throws -> String { 82 | try file.read() 83 | } 84 | 85 | private func getHTML(file: File) throws -> String { 86 | var source: String 87 | do { 88 | source = try getSource(file: file) 89 | } catch { 90 | os_log( 91 | "Could not read code file: %{public}s", 92 | log: Log.parse, 93 | type: .error, 94 | error.localizedDescription 95 | ) 96 | throw error 97 | } 98 | 99 | let lexer = getLexer(fileURL: file.url) 100 | do { 101 | return try HTMLRenderer.renderCode(source, lexer: lexer) 102 | } catch { 103 | os_log( 104 | "Could not generate code HTML: %{public}s", 105 | log: Log.render, 106 | type: .error, 107 | error.localizedDescription 108 | ) 109 | throw error 110 | } 111 | } 112 | 113 | private func getStylesheets() -> [Stylesheet] { 114 | var stylesheets = [Stylesheet]() 115 | 116 | // Chroma stylesheet (for code syntax highlighting) 117 | if let chromaStylesheetURL = chromaStylesheetURL { 118 | stylesheets.append(Stylesheet(url: chromaStylesheetURL)) 119 | } else { 120 | os_log("Could not find Chroma stylesheet", log: Log.render, type: .error) 121 | } 122 | 123 | return stylesheets 124 | } 125 | 126 | func createPreviewVC(file: File) throws -> PreviewVC { 127 | WebPreviewVC( 128 | html: try getHTML(file: file), 129 | stylesheets: getStylesheets() 130 | ) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /QLPlugin/Views/Previews/JupyterPreview.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import os.log 3 | 4 | class JupyterPreview: Preview { 5 | private let chromaStylesheetURL = Bundle.main.url( 6 | forResource: "shared-chroma", 7 | withExtension: "css" 8 | ) 9 | private let katexAutoRenderScriptURL = Bundle.main.url( 10 | forResource: "jupyter-katex-auto-render.min", 11 | withExtension: "js" 12 | ) 13 | private let katexScriptURL = Bundle.main.url( 14 | forResource: "jupyter-katex.min", 15 | withExtension: "js" 16 | ) 17 | private let katexStylesheetURL = Bundle.main.url( 18 | forResource: "jupyter-katex.min", 19 | withExtension: "css" 20 | ) 21 | private let mainStylesheetURL = Bundle.main.url( 22 | forResource: "jupyter-main", 23 | withExtension: "css" 24 | ) 25 | 26 | required init() {} 27 | 28 | private func getHTML(file: File) throws -> String { 29 | var source: String 30 | do { 31 | source = try file.read() 32 | } catch { 33 | os_log( 34 | "Could not read Jupyter Notebook file: %{public}s", 35 | log: Log.parse, 36 | type: .error, 37 | error.localizedDescription 38 | ) 39 | throw error 40 | } 41 | 42 | do { 43 | return try HTMLRenderer.renderNotebook(source) 44 | } catch { 45 | os_log( 46 | "Could not generate Jupyter Notebook HTML: %{public}s", 47 | log: Log.render, 48 | type: .error, 49 | error.localizedDescription 50 | ) 51 | throw error 52 | } 53 | } 54 | 55 | private func getStylesheets() -> [Stylesheet] { 56 | var stylesheets = [Stylesheet]() 57 | 58 | // Main Jupyter stylesheet (overrides and additions for nbtohtml stylesheet) 59 | if let mainStylesheetURL = mainStylesheetURL { 60 | stylesheets.append(Stylesheet(url: mainStylesheetURL)) 61 | } else { 62 | os_log("Could not find main Jupyter stylesheet", log: Log.render, type: .error) 63 | } 64 | 65 | // Chroma stylesheet (for code syntax highlighting) 66 | if let chromaStylesheetURL = chromaStylesheetURL { 67 | stylesheets.append(Stylesheet(url: chromaStylesheetURL)) 68 | } else { 69 | os_log("Could not find Chroma stylesheet", log: Log.render, type: .error) 70 | } 71 | 72 | // KaTeX stylesheet (for rendering LaTeX math) 73 | if let katexStylesheetURL = katexStylesheetURL { 74 | stylesheets.append(Stylesheet(url: katexStylesheetURL)) 75 | } else { 76 | os_log("Could not find KaTeX stylesheet", log: Log.render, type: .error) 77 | } 78 | 79 | return stylesheets 80 | } 81 | 82 | private func getScripts() -> [Script] { 83 | var scripts = [Script]() 84 | 85 | // KaTeX library (for rendering LaTeX math) 86 | if let katexScriptURL = katexScriptURL { 87 | scripts.append(Script(url: katexScriptURL)) 88 | } else { 89 | os_log("Could not find KaTeX script", log: Log.render, type: .error) 90 | } 91 | 92 | // KaTeX auto-renderer (finds LaTeX math ond the page and calls KaTeX on it) 93 | if let katexAutoRenderScriptURL = katexAutoRenderScriptURL { 94 | scripts.append(Script(url: katexAutoRenderScriptURL)) 95 | } else { 96 | os_log("Could not find KaTeX auto-render script", log: Log.render, type: .error) 97 | } 98 | 99 | // Main script (calls the KaTeX auto-renderer) 100 | scripts.append(Script(content: "renderMathInElement(document.body);")) 101 | 102 | return scripts 103 | } 104 | 105 | func createPreviewVC(file: File) throws -> PreviewVC { 106 | WebPreviewVC( 107 | html: try getHTML(file: file), 108 | stylesheets: getStylesheets(), 109 | scripts: getScripts() 110 | ) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /QLPlugin/Views/Previews/MarkdownPreview.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import os.log 3 | 4 | class MarkdownPreview: Preview { 5 | private let chromaStylesheetURL = Bundle.main.url( 6 | forResource: "shared-chroma", 7 | withExtension: "css" 8 | ) 9 | private let mainStylesheetURL = Bundle.main.url( 10 | forResource: "markdown-main", 11 | withExtension: "css" 12 | ) 13 | 14 | required init() {} 15 | 16 | private func getHTML(file: File) throws -> String { 17 | var source: String 18 | do { 19 | source = try file.read() 20 | } catch { 21 | os_log( 22 | "Could not read Markdown file: %{public}s", 23 | log: Log.parse, 24 | type: .error, 25 | error.localizedDescription 26 | ) 27 | throw error 28 | } 29 | 30 | do { 31 | let html = try HTMLRenderer.renderMarkdown(source) 32 | return "4 | 9 |5 | 6 | 7 | 8 | 10 | 39 |11 | 16 |12 | 15 |13 | 14 | 17 | 18 | 19 | 38 |20 | 21 | 22 | 30 |23 | 29 |24 | 25 | 28 |26 | 27 | 31 | 36 |32 | 33 | 34 | 35 | 37 | \(html)" 33 | } catch { 34 | os_log( 35 | "Could not generate Markdown HTML: %{public}s", 36 | log: Log.render, 37 | type: .error, 38 | error.localizedDescription 39 | ) 40 | throw error 41 | } 42 | } 43 | 44 | private func getStylesheets() -> [Stylesheet] { 45 | var stylesheets = [Stylesheet]() 46 | 47 | // Main Markdown stylesheet 48 | if let mainStylesheetURL = mainStylesheetURL { 49 | stylesheets.append(Stylesheet(url: mainStylesheetURL)) 50 | } else { 51 | os_log("Could not find main Markdown stylesheet", log: Log.render, type: .error) 52 | } 53 | 54 | // Chroma stylesheet (for code syntax highlighting) 55 | if let chromaStylesheetURL = chromaStylesheetURL { 56 | stylesheets.append(Stylesheet(url: chromaStylesheetURL)) 57 | } else { 58 | os_log("Could not find Chroma stylesheet", log: Log.render, type: .error) 59 | } 60 | 61 | return stylesheets 62 | } 63 | 64 | func createPreviewVC(file: File) throws -> PreviewVC { 65 | WebPreviewVC( 66 | html: try getHTML(file: file), 67 | stylesheets: getStylesheets() 68 | ) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /QLPlugin/Views/Previews/TARPreview.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import os.log 3 | import SwiftExec 4 | 5 | /// View controller for previewing tarballs (may be gzipped). 6 | class TARPreview: Preview { 7 | let filesRegex = #"(.{10}) +\d+ +.+ +.+ +(\d+) +(\w{3} +\d+ +[\d:]+) +(.+)"# 8 | let sizeRegex = #" +\d+ +(\d+) +([\d.]+)% +.+"# 9 | 10 | let byteCountFormatter = ByteCountFormatter() 11 | let dateFormatter1 = DateFormatter() 12 | let dateFormatter2 = DateFormatter() 13 | 14 | required init() { 15 | initDateFormatters() 16 | } 17 | 18 | /// Sets up `dateFormatter1` and `dateFormatter2` to parse date strings from `tar` output. Date 19 | /// strings may be in one of the following formats: 20 | /// 21 | /// - "MMM dd HH:mm", e.g. "Mar 28 15:36" (date is in current year) 22 | /// - "MMM dd yyyy", e.g. "Dec 29 2018" 23 | private func initDateFormatters() { 24 | // Set default date to today to parse dates in current year 25 | dateFormatter1.defaultDate = Date() 26 | 27 | // Specify date formats 28 | dateFormatter1.dateFormat = "MMM dd HH:mm" 29 | dateFormatter2.dateFormat = "MMM dd yyyy" 30 | } 31 | 32 | private func runTARFilesCommand(filePath: String) throws -> String { 33 | let result = try exec( 34 | program: "/usr/bin/tar", 35 | arguments: [ 36 | "--gzip", // Allows listing contents of `.tar.gz` files 37 | "--list", 38 | "--verbose", 39 | "--file", 40 | filePath, 41 | ] 42 | ) 43 | return result.stdout ?? "" 44 | } 45 | 46 | private func runGZIPSizeCommand(filePath: String) throws -> String { 47 | let result = try exec(program: "/usr/bin/gzip", arguments: ["--list", filePath]) 48 | return result.stdout ?? "" 49 | } 50 | 51 | /// Parses a date string from `tar` output to a `Date` object. 52 | private func parseDate(dateString: String) -> Date? { 53 | if dateString.contains(":") { 54 | return dateFormatter1.date(from: dateString) 55 | } else { 56 | return dateFormatter2.date(from: dateString) 57 | } 58 | } 59 | 60 | private func parseTARFiles(lines: String) -> FileTree { 61 | let fileTree = FileTree() 62 | 63 | // List entry format: "-rw-r--r-- 0 user staff 642 Dec 29 2018 my-tar/file.ext" 64 | // - Column 1: Permissions ("-" as first character indicates a file, "d" a directory) 65 | // - Column 5: File size in bytes 66 | // - Columns 6-8: Date modified 67 | // - Column 9: File path 68 | let fileMatches = lines.matchRegex(regex: filesRegex) 69 | for fileMatch in fileMatches { 70 | let permissions = fileMatch[1] 71 | let size = Int(fileMatch[2]) ?? 0 72 | let dateModified = parseDate(dateString: fileMatch[3]) 73 | let path = fileMatch[4] 74 | do { 75 | // Add file/directory node to tree 76 | try fileTree.addNode( 77 | path: path, 78 | isDirectory: permissions.first == "d", 79 | size: size, 80 | dateModified: dateModified 81 | ) 82 | } catch { 83 | os_log("%{public}s", log: Log.parse, type: .error, error.localizedDescription) 84 | } 85 | } 86 | 87 | return fileTree 88 | } 89 | 90 | private func parseGZIPSize(lines: String) 91 | -> (sizeUncompressed: Int?, compressionRatio: Double?) { 92 | let sizeMatches = lines.matchRegex(regex: sizeRegex) 93 | let sizeUncompressed = Int(sizeMatches[0][1]) 94 | let compressionRatio = Double(sizeMatches[0][2]) 95 | return (sizeUncompressed, compressionRatio) 96 | } 97 | 98 | func createPreviewVC(file: File) throws -> PreviewVC { 99 | let isGzipped = file.path.hasSuffix(".tar.gz") 100 | 101 | // Parse TAR contents 102 | let filesOutput = try runTARFilesCommand(filePath: file.path) 103 | let fileTree = parseTARFiles(lines: filesOutput) 104 | var labelText = 105 | "\(isGzipped ? "Compressed" : "Size"): \(byteCountFormatter.string(for: file.size) ?? "--")" 106 | 107 | // If tarball is gzipped: Get compression information 108 | if isGzipped { 109 | let sizeOutput = try runGZIPSizeCommand(filePath: file.path) 110 | let (sizeUncompressed, compressionRatio) = parseGZIPSize(lines: sizeOutput) 111 | labelText += """ 112 | 113 | Uncompressed: \(byteCountFormatter.string(for: sizeUncompressed) ?? "--") 114 | Compression ratio: \(compressionRatio == nil ? "--" : String(compressionRatio!)) % 115 | """ 116 | } 117 | 118 | return OutlinePreviewVC(rootNodes: fileTree.root.childrenList, labelText: labelText) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /QLPlugin/Views/Previews/TSVPreview.swift: -------------------------------------------------------------------------------- 1 | import os.log 2 | import SwiftCSV 3 | 4 | class TSVPreview: Preview { 5 | required init() {} 6 | 7 | func createPreviewVC(file: File) throws -> PreviewVC { 8 | // Read and parse TSV file 9 | var csv: CSV 10 | do { 11 | csv = try CSV(url: file.url, delimiter: "\t") 12 | } catch { 13 | os_log( 14 | "Could not parse TSV file: %{public}s", 15 | log: Log.parse, 16 | type: .error, 17 | error.localizedDescription 18 | ) 19 | throw error 20 | } 21 | 22 | return TablePreviewVC(headers: csv.header, cells: csv.namedRows) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /QLPlugin/Views/Previews/ZIPPreview.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import os.log 3 | import SwiftExec 4 | 5 | class ZIPPreview: Preview { 6 | let filesRegex = 7 | #"(.{10}) +.+ +.+ +(\d+) +.+ +.+ +(\d{2}-\w{3}-\d{2} +\d{2}:\d{2}) +(.+)"# 8 | let sizeRegex = #"\d+ files?, (\d+) bytes? uncompressed, \d+ bytes? compressed: +([\d.]+)%"# 9 | 10 | let byteCountFormatter = ByteCountFormatter() 11 | let dateFormatter = DateFormatter() 12 | 13 | required init() { 14 | dateFormatter.dateFormat = "yy-MMM-dd HH:mm" // Date format used in `zipinfo` output 15 | } 16 | 17 | private func runZIPInfoCommand(filePath: String) throws -> String { 18 | do { 19 | let result = try exec( 20 | program: "/usr/bin/zipinfo", 21 | arguments: [filePath] 22 | ) 23 | return result.stdout ?? "" 24 | } catch { 25 | // Empty ZIP files are allowed, but return exit code 1 26 | let error = error as! ExecError 27 | let stdout = error.execResult.stdout ?? "" 28 | if error.execResult.exitCode == 1, stdout.hasSuffix("Empty zipfile.") { 29 | return stdout 30 | } 31 | throw error 32 | } 33 | } 34 | 35 | /// Parses the output of the `zipinfo` command. 36 | private func parseZIPInfo(lines: String) -> ( 37 | fileTree: FileTree, 38 | sizeUncompressed: Int?, 39 | compressionRatio: Double? 40 | ) { 41 | let fileTree = FileTree() 42 | let linesSplit = lines.split(separator: "\n") 43 | 44 | // List entry format: "drwxr-xr-x 2.0 unx 0 bx stor 20-Jan-13 19:38 my-zip/dir/" 45 | // - Column 1: Permissions ("-" as first character indicates a file, "d" a directory) 46 | // - Column 4: File size in bytes 47 | // - Columns 7-8: Date modified 48 | // - Column 9: File path 49 | let filesString = linesSplit[2 ..< linesSplit.count - 1].joined(separator: "\n") 50 | let fileMatches = filesString.matchRegex(regex: filesRegex) 51 | for fileMatch in fileMatches { 52 | let permissions = fileMatch[1] 53 | let size = Int(fileMatch[2]) ?? 0 54 | let dateModified = dateFormatter.date(from: fileMatch[3]) 55 | let path = fileMatch[4] 56 | // Ignore "__MACOSX" subdirectory (ZIP resource fork created by macOS) 57 | if !path.hasPrefix("__MACOSX") { 58 | do { 59 | // Add file/directory node to tree 60 | try fileTree.addNode( 61 | path: path, 62 | isDirectory: permissions.first == "d", 63 | size: size, 64 | dateModified: dateModified 65 | ) 66 | } catch { 67 | os_log("%{public}s", log: Log.parse, type: .error, error.localizedDescription) 68 | } 69 | } 70 | } 71 | 72 | // Last line: 73 | // - If not empty: "152 files, 192919 bytes uncompressed, 65061 bytes compressed: 66.3%" 74 | // - If empty: "Empty zipfile." 75 | if let lastLine = linesSplit.last, lastLine != "Empty zipfile." { 76 | let sizeMatches = String(lastLine).matchRegex(regex: sizeRegex) 77 | let sizeUncompressed = Int(sizeMatches[0][1]) 78 | let compressionRatio = Double(sizeMatches[0][2]) 79 | return (fileTree, sizeUncompressed, compressionRatio) 80 | } else { 81 | return (fileTree, 0, 0) 82 | } 83 | } 84 | 85 | func createPreviewVC(file: File) throws -> PreviewVC { 86 | let zipInfoOutput = try runZIPInfoCommand(filePath: file.path) 87 | 88 | // Parse command output 89 | let (fileTree, sizeUncompressed, compressionRatio) = parseZIPInfo(lines: zipInfoOutput) 90 | 91 | // Build label 92 | let labelText = """ 93 | Compressed: \(byteCountFormatter.string(for: file.size) ?? "--") 94 | Uncompressed: \(byteCountFormatter.string(for: sizeUncompressed) ?? "--") 95 | Compression ratio: \(compressionRatio == nil ? "--" : String(compressionRatio!)) % 96 | """ 97 | 98 | return OutlinePreviewVC(rootNodes: fileTree.root.childrenList, labelText: labelText) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /QLPlugin/Views/Views/OfflineWebView.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import os.log 3 | import WebKit 4 | 5 | // Block all URLs except those starting with "blob:" or "file://" 6 | let blockRules = """ 7 | [ 8 | { 9 | "trigger": { 10 | "url-filter": ".*" 11 | }, 12 | "action": { 13 | "type": "block" 14 | } 15 | }, 16 | { 17 | "trigger": { 18 | "url-filter": "blob:.*" 19 | }, 20 | "action": { 21 | "type": "ignore-previous-rules" 22 | } 23 | }, 24 | { 25 | "trigger": { 26 | "url-filter": "file://.*" 27 | }, 28 | "action": { 29 | "type": "ignore-previous-rules" 30 | } 31 | } 32 | ] 33 | """ 34 | 35 | /// `WKWebView` which only allows the loading of local resources 36 | class OfflineWebView: WKWebView { 37 | required init?(coder decoder: NSCoder) { 38 | super.init(coder: decoder) 39 | 40 | WKContentRuleListStore.default().compileContentRuleList( 41 | forIdentifier: "ContentBlockingRules", 42 | encodedContentRuleList: blockRules 43 | ) { contentRuleList, error in 44 | if let error = error { 45 | os_log( 46 | "Error compiling WKWebView content rule list: %{public}s", 47 | log: Log.render, 48 | type: .error, 49 | error.localizedDescription 50 | ) 51 | } else if let contentRuleList = contentRuleList { 52 | self.configuration.userContentController.add(contentRuleList) 53 | } else { 54 | os_log( 55 | "Error adding WKWebView content rule list: Content rule list is not defined", 56 | log: Log.render, 57 | type: .error 58 | ) 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |2 |9 | 10 | ## Supported file types 11 | 12 | - **Source code** (with [Chroma](https://github.com/alecthomas/chroma) syntax highlighting): `.cpp`, `.js`, `.json`, `.py`, `.swift`, `.yml` and many more 13 | 14 |3 |
Glance
4 |All-in-one Quick Look plugin
5 |Glance provides Quick Look previews for files that macOS doesn't support out of the box.
6 | 7 |8 |
15 | 16 | - **Markdown** (rendered using [goldmark](https://github.com/yuin/goldmark)): `.md`, `.markdown`, `.mdown`, `.mkdn`, `.mkd`, `.Rmd` 17 | 18 |
19 | 20 | - **Archive**: `.tar`, `.tar.gz`, `.zip` 21 | 22 |
23 | 24 | - **Jupyter Notebook** (rendered using [nbtohtml](https://github.com/samuelmeuli/nbtohtml)): `.ipynb` 25 | 26 |
27 | 28 | - **Tab-separated values** (parsed using [SwiftCSV](https://github.com/swiftcsv/SwiftCSV)): `.tab`, `.tsv` 29 | 30 |
31 | 32 | ## FAQ 33 | 34 | **There are existing Quick Look apps for some of the supported file types. Why create another one?** 35 | 36 | - Glance combines the features of many plugins into one and provides consistent and beautiful previews. 37 | - Glance is fully compatible with Dark Mode. 38 | - Some plugins still use the deprecated Quick Look Generator API and might stop working in the future. 39 | - Glance can easily be extended to support other file types. 40 | 41 | **Why does Glance require network permissions?** 42 | 43 | Glance renders some previews in a `WKWebView`. All assets are stored locally and network access is disabled, but web views unfortunately still need the `com.apple.security.network.client` entitlement to function. 44 | 45 | **Why isn't the app available on macOS 10.14 or older?** 46 | 47 | The app uses the [new Quick Look API](https://developer.apple.com/documentation/quartz/qlpreviewingcontroller/2867936-preparepreviewoffile) that was introduced in 10.15, so it unfortunately won't work with older versions of macOS. 48 | 49 | **Why are images in my Markdown files not loading?** 50 | 51 | Glance blocks remote assets. Furthermore, the app only has access to the file that's being previewed. Local image files referenced from Markdown are therefore not loaded. 52 | 53 | **Why isn't [file type] supported?** 54 | 55 | Feel free to [open an issue](https://github.com/samuelmeuli/glance/issues/new) or [contribute](#contributing)! When opening an issue, please describe what kind of preview you'd expect for your file. 56 | 57 | Please note that macOS doesn't allow the handling of some file types (e.g. `.plist`, `.ts` and `.xml`). 58 | 59 | **You claim to support [file type], but previews aren't showing up.** 60 | 61 | Please note that Glance skips previews for large files to avoid slowing down your Mac. 62 | 63 | It's possible that your file's extension or [UTI](https://en.wikipedia.org/wiki/Uniform_Type_Identifier) isn't associated with Glance. You can easily verify this: 64 | 65 | 1. Check whether the file extension is matched to the correct class in [`PreviewVCFactory.swift`](./QLPlugin/Views/PreviewVCFactory.swift). 66 | 2. Find your file's UTI by running `mdls -name kMDItemContentType /path/to/your/file`. Check whether the UTI is listed under `QLSupportedContentTypes` in [`Info.plist`](./QLPlugin/Info.plist). 67 | 3. If an association is missing, please feel free to add it and submit a PR. 68 | 69 | ## Contributing 70 | 71 | Suggestions and contributions are always welcome! Please discuss larger changes (e.g. adding support for a new file type) via issue before submitting a pull request. 72 | 73 | Xcode, Swift and Go need to be installed to build the app locally. 74 | 75 | To add previews for a new file extension, please follow these steps: 76 | 77 | 1. Create a new class for your file type in [this directory](./QLPlugin/Views/Previews/). It should implement the `Preview` protocol. See the other files in the directory for examples. 78 | 2. Match the file extension to your class in [`PreviewVCFactory.swift`](./QLPlugin/Views/PreviewVCFactory.swift). 79 | 3. Find your file's UTI by running `mdls -name kMDItemContentType /path/to/your/file`. Add it to `QLSupportedContentTypes` in [`Info.plist`](./QLPlugin/Info.plist). 80 | 4. Update [`README.md`](README.md), [`SupportedFilesWC.xib`](Glance/SupportedFilesWC.xib), the [App Store description](AppStore/Listing/Description.txt) and [`Credits.rtf`](Glance/Credits.rtf) (if you introduced a new library). 81 | -------------------------------------------------------------------------------- /module.modulemap: -------------------------------------------------------------------------------- 1 | module HTMLConverter { 2 | header "HTMLConverter/htmlconverter.h" 3 | link "htmlconverter" 4 | export * 5 | } 6 | --------------------------------------------------------------------------------