├── .editorconfig ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug.md │ ├── feature.md │ └── file-type.md └── workflows │ ├── lint.yml │ └── test.yml ├── .gitignore ├── .swiftformat ├── .swiftlint.yml ├── AppStore ├── Assets │ ├── DownloadBadge.svg │ └── Screenshots │ │ ├── ScreenshotArchive.png │ │ ├── ScreenshotJupyterNotebook.png │ │ ├── ScreenshotMarkdown.png │ │ ├── ScreenshotSourceCode.png │ │ └── ScreenshotTSV.png └── Listing │ ├── Description.txt │ ├── Keywords.txt │ └── Screenshots │ ├── Screenshot1.jpg │ ├── Screenshot2.jpg │ └── Screenshot3.jpg ├── Glance.xcodeproj ├── project.pbxproj └── xcshareddata │ └── xcschemes │ ├── Glance.xcscheme │ └── QLPlugin.xcscheme ├── Glance ├── AppDelegate.swift ├── AppIcon.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── app-icon-128pt@1x.png │ │ ├── app-icon-128pt@2x.png │ │ ├── app-icon-16pt@1x.png │ │ ├── app-icon-16pt@2x.png │ │ ├── app-icon-256pt@1x.png │ │ ├── app-icon-256pt@2x.png │ │ ├── app-icon-32pt@1x.png │ │ ├── app-icon-32pt@2x.png │ │ ├── app-icon-512pt@1x.png │ │ └── app-icon-512pt@2x.png │ ├── Contents.json │ ├── screenshot-finder.imageset │ │ ├── Contents.json │ │ ├── screenshot-finder-dark.png │ │ └── screenshot-finder-light.png │ ├── screenshot-quick-look.imageset │ │ ├── Contents.json │ │ ├── screenshot-quick-look-dark.png │ │ └── screenshot-quick-look-light.png │ └── screenshot-spotlight.imageset │ │ ├── Contents.json │ │ ├── screenshot-spotlight-dark.png │ │ └── screenshot-spotlight-light.png ├── Credits.rtf ├── Extensions │ ├── NSMenuItem.swift │ └── URL.swift ├── Glance.entitlements ├── Info.plist ├── Main.storyboard ├── Shared │ ├── Extensions │ │ └── Date.swift │ └── Utils │ │ ├── Log.swift │ │ └── Stats.swift ├── SupportedFilesWC.swift ├── SupportedFilesWC.xib ├── Utils │ └── Menu.swift └── ViewController.swift ├── GlanceTests ├── FileTreeTests.swift ├── Info.plist └── TestFiles │ ├── apple-script │ ├── example-apple-script.applescript │ ├── example-apple-script.scpt │ ├── example-apple-script.scptd │ │ └── Contents │ │ │ ├── Info.plist │ │ │ └── Resources │ │ │ ├── Scripts │ │ │ └── main.scpt │ │ │ └── description.rtfd │ │ │ └── TXT.rtf │ ├── example-jxa.applescript │ ├── example-jxa.scpt │ └── example-jxa.scptd │ │ └── Contents │ │ ├── Info.plist │ │ └── Resources │ │ ├── Scripts │ │ └── main.scpt │ │ └── description.rtfd │ │ └── TXT.rtf │ ├── archives │ ├── example-no-files.tar │ ├── example-no-files.zip │ ├── example-no-root-directory.tar │ ├── example-no-root-directory.zip │ ├── example-root-directory.tar │ ├── example-root-directory.zip │ ├── example-single-file.tar │ ├── example-single-file.zip │ └── example.tar.gz │ ├── jupyter-notebook │ └── example.ipynb │ ├── markdown │ ├── example.Rmd │ ├── example.markdown │ ├── example.md │ ├── example.mdown │ ├── example.mkd │ └── example.mkdn │ └── tsv │ ├── example.tab │ └── example.tsv ├── HTMLConverter ├── .golangci.yml ├── go.mod ├── go.sum ├── htmlconverter.go └── htmlconverter_test.go ├── LICENSE.md ├── Mintfile ├── PRIVACY.md ├── QLPlugin ├── Extensions │ └── String.swift ├── Info.plist ├── MainVC.swift ├── MainVC.xib ├── QLPlugin.entitlements ├── Resources │ ├── jupyter │ │ ├── fonts │ │ │ ├── KaTeX_AMS-Regular.ttf │ │ │ ├── KaTeX_AMS-Regular.woff │ │ │ ├── KaTeX_AMS-Regular.woff2 │ │ │ ├── KaTeX_Caligraphic-Bold.ttf │ │ │ ├── KaTeX_Caligraphic-Bold.woff │ │ │ ├── KaTeX_Caligraphic-Bold.woff2 │ │ │ ├── KaTeX_Caligraphic-Regular.ttf │ │ │ ├── KaTeX_Caligraphic-Regular.woff │ │ │ ├── KaTeX_Caligraphic-Regular.woff2 │ │ │ ├── KaTeX_Fraktur-Bold.ttf │ │ │ ├── KaTeX_Fraktur-Bold.woff │ │ │ ├── KaTeX_Fraktur-Bold.woff2 │ │ │ ├── KaTeX_Fraktur-Regular.ttf │ │ │ ├── KaTeX_Fraktur-Regular.woff │ │ │ ├── KaTeX_Fraktur-Regular.woff2 │ │ │ ├── KaTeX_Main-Bold.ttf │ │ │ ├── KaTeX_Main-Bold.woff │ │ │ ├── KaTeX_Main-Bold.woff2 │ │ │ ├── KaTeX_Main-BoldItalic.ttf │ │ │ ├── KaTeX_Main-BoldItalic.woff │ │ │ ├── KaTeX_Main-BoldItalic.woff2 │ │ │ ├── KaTeX_Main-Italic.ttf │ │ │ ├── KaTeX_Main-Italic.woff │ │ │ ├── KaTeX_Main-Italic.woff2 │ │ │ ├── KaTeX_Main-Regular.ttf │ │ │ ├── KaTeX_Main-Regular.woff │ │ │ ├── KaTeX_Main-Regular.woff2 │ │ │ ├── KaTeX_Math-BoldItalic.ttf │ │ │ ├── KaTeX_Math-BoldItalic.woff │ │ │ ├── KaTeX_Math-BoldItalic.woff2 │ │ │ ├── KaTeX_Math-Italic.ttf │ │ │ ├── KaTeX_Math-Italic.woff │ │ │ ├── KaTeX_Math-Italic.woff2 │ │ │ ├── KaTeX_SansSerif-Bold.ttf │ │ │ ├── KaTeX_SansSerif-Bold.woff │ │ │ ├── KaTeX_SansSerif-Bold.woff2 │ │ │ ├── KaTeX_SansSerif-Italic.ttf │ │ │ ├── KaTeX_SansSerif-Italic.woff │ │ │ ├── KaTeX_SansSerif-Italic.woff2 │ │ │ ├── KaTeX_SansSerif-Regular.ttf │ │ │ ├── KaTeX_SansSerif-Regular.woff │ │ │ ├── KaTeX_SansSerif-Regular.woff2 │ │ │ ├── KaTeX_Script-Regular.ttf │ │ │ ├── KaTeX_Script-Regular.woff │ │ │ ├── KaTeX_Script-Regular.woff2 │ │ │ ├── KaTeX_Size1-Regular.ttf │ │ │ ├── KaTeX_Size1-Regular.woff │ │ │ ├── KaTeX_Size1-Regular.woff2 │ │ │ ├── KaTeX_Size2-Regular.ttf │ │ │ ├── KaTeX_Size2-Regular.woff │ │ │ ├── KaTeX_Size2-Regular.woff2 │ │ │ ├── KaTeX_Size3-Regular.ttf │ │ │ ├── KaTeX_Size3-Regular.woff │ │ │ ├── KaTeX_Size3-Regular.woff2 │ │ │ ├── KaTeX_Size4-Regular.ttf │ │ │ ├── KaTeX_Size4-Regular.woff │ │ │ ├── KaTeX_Size4-Regular.woff2 │ │ │ ├── KaTeX_Typewriter-Regular.ttf │ │ │ ├── KaTeX_Typewriter-Regular.woff │ │ │ └── KaTeX_Typewriter-Regular.woff2 │ │ ├── jupyter-katex-auto-render.min.js │ │ ├── jupyter-katex.min.css │ │ ├── jupyter-katex.min.js │ │ └── jupyter-main.css │ ├── markdown │ │ └── markdown-main.css │ └── shared │ │ ├── shared-chroma.css │ │ └── shared-main.css ├── Utils │ ├── File.swift │ ├── FileTree.swift │ ├── HTMLRenderer.swift │ └── WebAsset │ │ ├── Script.swift │ │ ├── Stylesheet.swift │ │ └── WebAsset.swift └── Views │ ├── PreviewVC.swift │ ├── PreviewVCFactory.swift │ ├── PreviewVCs │ ├── OutlinePreviewVC.swift │ ├── OutlinePreviewVC.xib │ ├── TablePreviewVC.swift │ ├── TablePreviewVC.xib │ ├── WebPreviewVC.swift │ └── WebPreviewVC.xib │ ├── Previews │ ├── AppleScriptPreview.swift │ ├── CodePreview.swift │ ├── JupyterPreview.swift │ ├── MarkdownPreview.swift │ ├── TARPreview.swift │ ├── TSVPreview.swift │ └── ZIPPreview.swift │ └── Views │ └── OfflineWebView.swift ├── README.md └── module.modulemap /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = tab 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.{yaml,yml}] 12 | indent_style = space 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | *.rtf linguist-vendored 3 | GlanceTests/TestFiles/**/* linguist-vendored 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Something isn't working as expected 4 | labels: bug 5 | --- 6 | 7 | 12 | 13 | ### Environment 14 | 15 | - **App version?** 16 | - **macOS version?** 17 | 18 | ### Bug Information 19 | 20 | - **What are you trying to do?** 21 | - **What's the expected behavior?** 22 | - **What's the actual behavior?** 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggestions for new or different behavior 4 | labels: enhancement 5 | --- 6 | 7 | 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/file-type.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: File Type Request 3 | about: Request to support previews for a new file type 4 | title: "Support for [FILE TYPE]" 5 | labels: new file type 6 | --- 7 | 8 | 13 | 14 | - **What's the file's extension?** 15 | - **What's the file's Uniform Type Identifier (output of `mdls -name kMDItemContentType /path/to/your/file`)?** 16 | - **What kind of preview do you expect? How can it be generated?** 17 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | lint-swift: 9 | name: Lint (Swift) 10 | runs-on: macos-latest 11 | 12 | steps: 13 | - name: Check out Git repository 14 | uses: actions/checkout@v2 15 | 16 | - name: Install Homebrew dependencies 17 | run: brew install mint 18 | 19 | - name: Restore Mint cache 20 | id: mint-cache 21 | uses: actions/cache@v1 22 | with: 23 | path: /usr/local/lib/mint/ 24 | key: ${{ runner.os }}-mint-${{ hashFiles('**/Mintfile') }} 25 | 26 | - name: Install Mint dependencies 27 | if: steps.mint-cache.outputs.cache-hit != 'true' 28 | run: mint bootstrap 29 | 30 | - name: Run linters 31 | run: | 32 | mint run swiftlint --quiet --strict 33 | mint run swiftformat --lint . 34 | 35 | lint-go: 36 | name: Lint (Go) 37 | runs-on: ubuntu-latest 38 | defaults: 39 | run: 40 | working-directory: ./HTMLConverter/ 41 | 42 | steps: 43 | - name: Check out Git repository 44 | uses: actions/checkout@v2 45 | 46 | - name: Set up Go 47 | uses: actions/setup-go@v1 48 | with: 49 | go-version: 1.14 50 | 51 | - name: Restore Go Modules cache 52 | id: go-mod-cache 53 | uses: actions/cache@v1 54 | with: 55 | path: ~/go/pkg/mod/ 56 | key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }} 57 | restore-keys: | 58 | ${{ runner.os }}-go-mod- 59 | 60 | - name: Run linters 61 | run: docker run --rm -v $(pwd):/app -w /app golangci/golangci-lint:v1.25.1 golangci-lint run 62 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | test-swift: 9 | name: Test (Swift) 10 | runs-on: macos-latest 11 | 12 | steps: 13 | - name: Check out Git repository 14 | uses: actions/checkout@v2 15 | 16 | - name: Run tests 17 | run: xcodebuild clean test CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO -project ./Glance.xcodeproj -scheme Glance 18 | 19 | test-go: 20 | name: Test (Go) 21 | runs-on: macos-latest 22 | defaults: 23 | run: 24 | working-directory: ./HTMLConverter/ 25 | 26 | steps: 27 | - name: Check out Git repository 28 | uses: actions/checkout@v2 29 | 30 | - name: Set up Go 31 | uses: actions/setup-go@v1 32 | with: 33 | go-version: 1.14 34 | 35 | - name: Restore Go Modules cache 36 | id: go-mod-cache 37 | uses: actions/cache@v1 38 | with: 39 | path: ~/go/pkg/mod/ 40 | key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }} 41 | restore-keys: | 42 | ${{ runner.os }}-go-mod- 43 | 44 | - name: Run tests 45 | run: go test 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | project.xcworkspace/ 3 | xcuserdata/ 4 | 5 | # `go build` output 6 | HTMLConverter/htmlconverter.a 7 | HTMLConverter/htmlconverter.h 8 | -------------------------------------------------------------------------------- /.swiftformat: -------------------------------------------------------------------------------- 1 | --swiftversion 5.1 2 | 3 | --indent tab 4 | --indentcase true 5 | --maxwidth 100 6 | --tabwidth 4 7 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | # Source: https://github.com/sindresorhus/swiftlint-sindre (with modifications) 2 | 3 | # Rules covered by SwiftFormat are commented out 4 | whitelist_rules: 5 | - anyobject_protocol 6 | - array_init 7 | - attributes 8 | - block_based_kvo 9 | - class_delegate_protocol 10 | - closing_brace 11 | - closure_end_indentation 12 | - closure_parameter_position 13 | - closure_spacing 14 | - collection_alignment 15 | - colon 16 | - comma 17 | - compiler_protocol_init 18 | - conditional_returns_on_newline 19 | - contains_over_filter_count 20 | - contains_over_filter_is_empty 21 | - contains_over_first_not_nil 22 | - contains_over_range_nil_comparison 23 | - control_statement 24 | - deployment_target 25 | - discarded_notification_center_observer 26 | - discouraged_direct_init 27 | - discouraged_object_literal 28 | - discouraged_optional_boolean 29 | - discouraged_optional_collection 30 | - duplicate_enum_cases 31 | - duplicate_imports 32 | - dynamic_inline 33 | - empty_collection_literal 34 | - empty_count 35 | - empty_enum_arguments 36 | - empty_parameters 37 | - empty_parentheses_with_trailing_closure 38 | - empty_string 39 | - empty_xctest_method 40 | - explicit_init 41 | - fallthrough 42 | - fatal_error_message 43 | - first_where 44 | - flatmap_over_map_reduce 45 | - for_where 46 | - generic_type_name 47 | - identical_operands 48 | - identifier_name 49 | - implicit_getter 50 | - implicit_return 51 | - inert_defer 52 | - is_disjoint 53 | - joined_default_parameter 54 | - last_where 55 | - leading_whitespace 56 | - legacy_cggeometry_functions 57 | - legacy_constant 58 | - legacy_constructor 59 | - legacy_hashing 60 | - legacy_multiple 61 | - legacy_nsgeometry_functions 62 | - legacy_random 63 | - literal_expression_end_indentation 64 | - lower_acl_than_parent 65 | - mark 66 | - modifier_order 67 | - multiline_arguments 68 | - multiline_function_chains 69 | - multiline_literal_brackets 70 | - multiline_parameters 71 | - multiline_parameters_brackets 72 | - nimble_operator 73 | - no_extension_access_modifier 74 | - no_fallthrough_only 75 | - no_space_in_method_call 76 | - notification_center_detachment 77 | - nsobject_prefer_isequal 78 | - number_separator 79 | - object_literal 80 | - opening_brace 81 | - operator_usage_whitespace 82 | - operator_whitespace 83 | - overridden_super_call 84 | - pattern_matching_keywords 85 | - private_action 86 | - private_outlet 87 | - private_unit_test 88 | - prohibited_super_call 89 | - protocol_property_accessors_order 90 | - reduce_boolean 91 | - reduce_into 92 | - redundant_discardable_let 93 | - redundant_nil_coalescing 94 | - redundant_objc_attribute 95 | - redundant_optional_initialization 96 | - redundant_set_access_control 97 | - redundant_string_enum_value 98 | - redundant_type_annotation 99 | - redundant_void_return 100 | - required_enum_case 101 | - return_arrow_whitespace 102 | - shorthand_operator 103 | - sorted_first_last 104 | - statement_position 105 | - static_operator 106 | - strong_iboutlet 107 | - superfluous_disable_command 108 | # - switch_case_alignment 109 | - switch_case_on_newline 110 | - syntactic_sugar 111 | # - todo 112 | - toggle_bool 113 | # - trailing_closure 114 | # - trailing_comma 115 | # - trailing_newline 116 | # - trailing_semicolon 117 | # - trailing_whitespace 118 | - type_name 119 | - unavailable_function 120 | - unneeded_break_in_switch 121 | - unneeded_parentheses_in_closure_argument 122 | - unowned_variable_capture 123 | - untyped_error_in_catch 124 | - unused_capture_list 125 | - unused_closure_parameter 126 | - unused_control_flow_label 127 | - unused_enumerated 128 | - unused_optional_binding 129 | - unused_setter_value 130 | - valid_ibinspectable 131 | - vertical_parameter_alignment 132 | - vertical_parameter_alignment_on_call 133 | - vertical_whitespace_closing_braces 134 | - vertical_whitespace_opening_braces 135 | - void_return 136 | - weak_delegate 137 | - xct_specific_matcher 138 | - xctfail_message 139 | - yoda_condition 140 | analyzer_rules: 141 | - unused_declaration 142 | - unused_import 143 | number_separator: 144 | minimum_length: 5 145 | object_literal: 146 | image_literal: false 147 | discouraged_object_literal: 148 | color_literal: false 149 | identifier_name: 150 | max_length: 151 | warning: 100 152 | error: 100 153 | min_length: 154 | warning: 2 155 | error: 2 156 | validates_start_with_lowercase: false 157 | allowed_symbols: 158 | - "_" 159 | excluded: 160 | - "x" 161 | - "y" 162 | - "a" 163 | - "b" 164 | - "x1" 165 | - "x2" 166 | - "y1" 167 | - "y2" 168 | -------------------------------------------------------------------------------- /AppStore/Assets/DownloadBadge.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /AppStore/Assets/Screenshots/ScreenshotArchive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/AppStore/Assets/Screenshots/ScreenshotArchive.png -------------------------------------------------------------------------------- /AppStore/Assets/Screenshots/ScreenshotJupyterNotebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/AppStore/Assets/Screenshots/ScreenshotJupyterNotebook.png -------------------------------------------------------------------------------- /AppStore/Assets/Screenshots/ScreenshotMarkdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/AppStore/Assets/Screenshots/ScreenshotMarkdown.png -------------------------------------------------------------------------------- /AppStore/Assets/Screenshots/ScreenshotSourceCode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/AppStore/Assets/Screenshots/ScreenshotSourceCode.png -------------------------------------------------------------------------------- /AppStore/Assets/Screenshots/ScreenshotTSV.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/AppStore/Assets/Screenshots/ScreenshotTSV.png -------------------------------------------------------------------------------- /AppStore/Listing/Description.txt: -------------------------------------------------------------------------------- 1 | See what's in your files without opening them! 2 | 3 | Glance boosts your productivity by providing Quick Look previews for files that macOS doesn't support out of the box. 4 | 5 | Features: 6 | • Beautiful file previews for various file types 7 | • Previews visible in Quick Look, Finder and Spotlight 8 | • Full compatibility with Dark Mode 9 | 10 | Supported file types: 11 | • Source code (.cpp, .js, .json, .py, .swift, .yml and many more) 12 | • Markdown (.md, .markdown, .mdown, .mkdn, .mkd, .Rmd) 13 | • Archive (.tar, .tar.gz, .zip) 14 | • Jupyter Notebook (.ipynb) 15 | • Tab-separated values (.tab, .tsv) 16 | -------------------------------------------------------------------------------- /AppStore/Listing/Keywords.txt: -------------------------------------------------------------------------------- 1 | glance,quick,look,quicklook,plugin,preview,finder,spotlight,file,thumbnail 2 | -------------------------------------------------------------------------------- /AppStore/Listing/Screenshots/Screenshot1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/AppStore/Listing/Screenshots/Screenshot1.jpg -------------------------------------------------------------------------------- /AppStore/Listing/Screenshots/Screenshot2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/AppStore/Listing/Screenshots/Screenshot2.jpg -------------------------------------------------------------------------------- /AppStore/Listing/Screenshots/Screenshot3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/AppStore/Listing/Screenshots/Screenshot3.jpg -------------------------------------------------------------------------------- /Glance.xcodeproj/xcshareddata/xcschemes/Glance.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 55 | 61 | 62 | 63 | 64 | 70 | 72 | 78 | 79 | 80 | 81 | 83 | 84 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /Glance.xcodeproj/xcshareddata/xcschemes/QLPlugin.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 6 | 9 | 10 | 16 | 22 | 23 | 24 | 30 | 36 | 37 | 38 | 39 | 40 | 45 | 46 | 48 | 54 | 55 | 56 | 57 | 58 | 70 | 74 | 75 | 76 | 82 | 83 | 84 | 85 | 92 | 94 | 100 | 101 | 102 | 103 | 105 | 106 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /Glance/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | @NSApplicationMain 4 | class AppDelegate: NSObject, NSApplicationDelegate { 5 | func applicationShouldTerminateAfterLastWindowClosed(_: NSApplication) -> Bool { true } 6 | } 7 | -------------------------------------------------------------------------------- /Glance/AppIcon.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | class AppIcon: NSImageView { 4 | required init?(coder: NSCoder) { 5 | super.init(coder: coder) 6 | image = NSApplication.shared.applicationIconImage 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Glance/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "app-icon-16pt@1x.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "app-icon-16pt@2x.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "app-icon-32pt@1x.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "app-icon-32pt@2x.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "app-icon-128pt@1x.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "app-icon-128pt@2x.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "app-icon-256pt@1x.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "app-icon-256pt@2x.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "app-icon-512pt@1x.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "app-icon-512pt@2x.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Glance/Assets.xcassets/AppIcon.appiconset/app-icon-128pt@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/Glance/Assets.xcassets/AppIcon.appiconset/app-icon-128pt@1x.png -------------------------------------------------------------------------------- /Glance/Assets.xcassets/AppIcon.appiconset/app-icon-128pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/Glance/Assets.xcassets/AppIcon.appiconset/app-icon-128pt@2x.png -------------------------------------------------------------------------------- /Glance/Assets.xcassets/AppIcon.appiconset/app-icon-16pt@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/Glance/Assets.xcassets/AppIcon.appiconset/app-icon-16pt@1x.png -------------------------------------------------------------------------------- /Glance/Assets.xcassets/AppIcon.appiconset/app-icon-16pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/Glance/Assets.xcassets/AppIcon.appiconset/app-icon-16pt@2x.png -------------------------------------------------------------------------------- /Glance/Assets.xcassets/AppIcon.appiconset/app-icon-256pt@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/Glance/Assets.xcassets/AppIcon.appiconset/app-icon-256pt@1x.png -------------------------------------------------------------------------------- /Glance/Assets.xcassets/AppIcon.appiconset/app-icon-256pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/Glance/Assets.xcassets/AppIcon.appiconset/app-icon-256pt@2x.png -------------------------------------------------------------------------------- /Glance/Assets.xcassets/AppIcon.appiconset/app-icon-32pt@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/Glance/Assets.xcassets/AppIcon.appiconset/app-icon-32pt@1x.png -------------------------------------------------------------------------------- /Glance/Assets.xcassets/AppIcon.appiconset/app-icon-32pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/Glance/Assets.xcassets/AppIcon.appiconset/app-icon-32pt@2x.png -------------------------------------------------------------------------------- /Glance/Assets.xcassets/AppIcon.appiconset/app-icon-512pt@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/Glance/Assets.xcassets/AppIcon.appiconset/app-icon-512pt@1x.png -------------------------------------------------------------------------------- /Glance/Assets.xcassets/AppIcon.appiconset/app-icon-512pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/Glance/Assets.xcassets/AppIcon.appiconset/app-icon-512pt@2x.png -------------------------------------------------------------------------------- /Glance/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Glance/Assets.xcassets/screenshot-finder.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "screenshot-finder-light.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "appearances" : [ 10 | { 11 | "appearance" : "luminosity", 12 | "value" : "dark" 13 | } 14 | ], 15 | "filename" : "screenshot-finder-dark.png", 16 | "idiom" : "universal", 17 | "scale" : "1x" 18 | }, 19 | { 20 | "idiom" : "universal", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "appearances" : [ 25 | { 26 | "appearance" : "luminosity", 27 | "value" : "dark" 28 | } 29 | ], 30 | "idiom" : "universal", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "idiom" : "universal", 35 | "scale" : "3x" 36 | }, 37 | { 38 | "appearances" : [ 39 | { 40 | "appearance" : "luminosity", 41 | "value" : "dark" 42 | } 43 | ], 44 | "idiom" : "universal", 45 | "scale" : "3x" 46 | } 47 | ], 48 | "info" : { 49 | "author" : "xcode", 50 | "version" : 1 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Glance/Assets.xcassets/screenshot-finder.imageset/screenshot-finder-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/Glance/Assets.xcassets/screenshot-finder.imageset/screenshot-finder-dark.png -------------------------------------------------------------------------------- /Glance/Assets.xcassets/screenshot-finder.imageset/screenshot-finder-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/Glance/Assets.xcassets/screenshot-finder.imageset/screenshot-finder-light.png -------------------------------------------------------------------------------- /Glance/Assets.xcassets/screenshot-quick-look.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "screenshot-quick-look-light.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "appearances" : [ 10 | { 11 | "appearance" : "luminosity", 12 | "value" : "dark" 13 | } 14 | ], 15 | "filename" : "screenshot-quick-look-dark.png", 16 | "idiom" : "universal", 17 | "scale" : "1x" 18 | }, 19 | { 20 | "idiom" : "universal", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "appearances" : [ 25 | { 26 | "appearance" : "luminosity", 27 | "value" : "dark" 28 | } 29 | ], 30 | "idiom" : "universal", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "idiom" : "universal", 35 | "scale" : "3x" 36 | }, 37 | { 38 | "appearances" : [ 39 | { 40 | "appearance" : "luminosity", 41 | "value" : "dark" 42 | } 43 | ], 44 | "idiom" : "universal", 45 | "scale" : "3x" 46 | } 47 | ], 48 | "info" : { 49 | "author" : "xcode", 50 | "version" : 1 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Glance/Assets.xcassets/screenshot-quick-look.imageset/screenshot-quick-look-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/Glance/Assets.xcassets/screenshot-quick-look.imageset/screenshot-quick-look-dark.png -------------------------------------------------------------------------------- /Glance/Assets.xcassets/screenshot-quick-look.imageset/screenshot-quick-look-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/Glance/Assets.xcassets/screenshot-quick-look.imageset/screenshot-quick-look-light.png -------------------------------------------------------------------------------- /Glance/Assets.xcassets/screenshot-spotlight.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "screenshot-spotlight-light.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "appearances" : [ 10 | { 11 | "appearance" : "luminosity", 12 | "value" : "dark" 13 | } 14 | ], 15 | "filename" : "screenshot-spotlight-dark.png", 16 | "idiom" : "universal", 17 | "scale" : "1x" 18 | }, 19 | { 20 | "idiom" : "universal", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "appearances" : [ 25 | { 26 | "appearance" : "luminosity", 27 | "value" : "dark" 28 | } 29 | ], 30 | "idiom" : "universal", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "idiom" : "universal", 35 | "scale" : "3x" 36 | }, 37 | { 38 | "appearances" : [ 39 | { 40 | "appearance" : "luminosity", 41 | "value" : "dark" 42 | } 43 | ], 44 | "idiom" : "universal", 45 | "scale" : "3x" 46 | } 47 | ], 48 | "info" : { 49 | "author" : "xcode", 50 | "version" : 1 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Glance/Assets.xcassets/screenshot-spotlight.imageset/screenshot-spotlight-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/Glance/Assets.xcassets/screenshot-spotlight.imageset/screenshot-spotlight-dark.png -------------------------------------------------------------------------------- /Glance/Assets.xcassets/screenshot-spotlight.imageset/screenshot-spotlight-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/Glance/Assets.xcassets/screenshot-spotlight.imageset/screenshot-spotlight-light.png -------------------------------------------------------------------------------- /Glance/Credits.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\ansi\ansicpg1252\cocoartf2512 2 | \cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fswiss\fcharset0 Helvetica-Bold;\f1\fswiss\fcharset0 Helvetica;} 3 | {\colortbl;\red255\green255\blue255;\red0\green0\blue0;} 4 | {\*\expandedcolortbl;;\cssrgb\c0\c0\c0;} 5 | {\*\listtable{\list\listtemplateid1\listhybrid{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{disc\}}{\leveltext\leveltemplateid1\'01\uc0\u8226 ;}{\levelnumbers;}\fi-360\li720\lin720 }{\listname ;}\listid1}} 6 | {\*\listoverridetable{\listoverride\listid1\listoverridecount0\ls1}} 7 | \paperw11900\paperh16840\margl1440\margr1440\vieww28600\viewh16280\viewkind0 8 | \deftab720 9 | \pard\pardeftab720\partightenfactor0 10 | 11 | \f0\b\fs28 \cf2 \expnd0\expndtw0\kerning0 12 | Acknowledgements 13 | \f1\b0\fs24 \ 14 | \ 15 | Portions of this software may utilize the following copyrighted material, the use of which is hereby acknowledged.\ 16 | \ 17 | \ 18 | \pard\pardeftab720\partightenfactor0 19 | 20 | \f0\b \cf2 App icon\ 21 | \ 22 | \pard\tx220\tx720\pardeftab720\li720\fi-720\partightenfactor0 23 | \ls1\ilvl0\cf2 \kerning1\expnd0\expndtw0 {\listtext \uc0\u8226 }Document: 24 | \f1\b0 "Generic Document Icon" by Apple Inc. 25 | \f0\b \ 26 | {\listtext \uc0\u8226 }Magnifying glass: 27 | \f1\b0 {\field{\*\fldinst{HYPERLINK "https://www.freepik.com/free-vector/magnifying-glass-background-realistic-style_2038298.htm"}}{\fldrslt \expnd0\expndtw0\kerning0 28 | "Magnifying glass background in realistic style Free Vector"}}\expnd0\expndtw0\kerning0 29 | by freepik\ 30 | \pard\pardeftab720\partightenfactor0 31 | \cf2 \ 32 | \ 33 | \pard\pardeftab720\partightenfactor0 34 | 35 | \f0\b \cf2 Chroma\ 36 | \pard\pardeftab720\partightenfactor0 37 | 38 | \f1\b0 \cf2 \ 39 | Copyright (C) 2017 Alec Thomas\ 40 | \ 41 | 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:\ 42 | \ 43 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\ 44 | \ 45 | 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.\ 46 | \ 47 | \ 48 | \pard\pardeftab720\partightenfactor0 49 | 50 | \f0\b \cf2 github-markdown-css\ 51 | \pard\pardeftab720\partightenfactor0 52 | 53 | \f1\b0 \cf2 \ 54 | MIT License\ 55 | \ 56 | Copyright (c) Sindre Sorhus (https://sindresorhus.com)\ 57 | \ 58 | 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:\ 59 | \ 60 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\ 61 | \ 62 | 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.\ 63 | \ 64 | \ 65 | \pard\pardeftab720\partightenfactor0 66 | 67 | \f0\b \cf2 goldmark\ 68 | \pard\pardeftab720\partightenfactor0 69 | 70 | \f1\b0 \cf2 \ 71 | MIT License\ 72 | \ 73 | Copyright (c) 2019 Yusuke Inuzuka\ 74 | \ 75 | 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:\ 76 | \ 77 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\ 78 | \ 79 | 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.\ 80 | \ 81 | \ 82 | \pard\pardeftab720\partightenfactor0 83 | 84 | \f0\b \cf2 nbtohtml\ 85 | \ 86 | \pard\pardeftab720\partightenfactor0 87 | 88 | \f1\b0 \cf2 MIT License\ 89 | \ 90 | Copyright (c) 2020 Samuel Meuli\ 91 | \ 92 | 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:\ 93 | \ 94 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\ 95 | \ 96 | 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.\ 97 | \ 98 | \ 99 | \pard\pardeftab720\partightenfactor0 100 | 101 | \f0\b \cf2 SwiftCSV\ 102 | \pard\pardeftab720\partightenfactor0 103 | 104 | \f1\b0 \cf2 \ 105 | The MIT License (MIT)\ 106 | \ 107 | Copyright (c) 2014 Naoto Kaneko\ 108 | \ 109 | 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:\ 110 | \ 111 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\ 112 | \ 113 | 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.\ 114 | \ 115 | \ 116 | \pard\pardeftab720\partightenfactor0 117 | 118 | \f0\b \cf2 SwiftExec\ 119 | \pard\pardeftab720\partightenfactor0 120 | 121 | \f1\b0 \cf2 \ 122 | MIT License\ 123 | \ 124 | Copyright (c) 2020 Samuel Meuli\ 125 | \ 126 | 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:\ 127 | \ 128 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\ 129 | \ 130 | 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.} -------------------------------------------------------------------------------- /Glance/Extensions/NSMenuItem.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import Foundation 3 | 4 | /// Source: Gifski 5 | /// v2.7.0 6 | /// https://github.com/sindresorhus/Gifski 7 | /// 8 | /// MIT License 9 | /// 10 | /// © 2019 Sindre Sorhus (sindresorhus.com) 11 | /// © 2019 Kornel Lesiński (gif.ski) 12 | /// 13 | /// Permission is hereby granted, free of charge, to any person obtaining a copy of this software 14 | /// and associated documentation files (the "Software"), to deal in the Software without 15 | /// restriction, including without limitation the rights to use, copy, modify, merge, publish, 16 | /// distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the 17 | /// Software is furnished to do so, subject to the following conditions: 18 | /// 19 | /// The above copyright notice and this permission notice shall be included in all copies or 20 | /// substantial portions of the Software. 21 | /// 22 | /// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 23 | /// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 24 | /// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 25 | /// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | /// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 27 | 28 | final class ObjectAssociation { 29 | subscript(index: AnyObject) -> T? { 30 | get { 31 | objc_getAssociatedObject(index, Unmanaged.passUnretained(self).toOpaque()) as! T? 32 | } set { 33 | objc_setAssociatedObject( 34 | index, 35 | Unmanaged.passUnretained(self).toOpaque(), 36 | newValue, 37 | .OBJC_ASSOCIATION_RETAIN_NONATOMIC 38 | ) 39 | } 40 | } 41 | } 42 | 43 | extension NSMenuItem { 44 | typealias ActionClosure = ((NSMenuItem) -> Void) 45 | 46 | private struct AssociatedKeys { 47 | static let onActionClosure = ObjectAssociation() 48 | } 49 | 50 | @objc 51 | private func callClosureGifski(_ sender: NSMenuItem) { 52 | onAction?(sender) 53 | } 54 | 55 | /// Closure version of `.action` 56 | /// 57 | /// ``` 58 | /// let menuItem = NSMenuItem(title: "Unicorn") 59 | /// menuItem.onAction = { sender in 60 | /// print("NSMenuItem action: \(sender)") 61 | /// } 62 | /// ``` 63 | var onAction: ActionClosure? { 64 | get { AssociatedKeys.onActionClosure[self] } 65 | set { 66 | AssociatedKeys.onActionClosure[self] = newValue 67 | action = #selector(callClosureGifski) 68 | target = self 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Glance/Extensions/URL.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import Foundation 3 | 4 | extension URL { 5 | func open() { 6 | NSWorkspace.shared.open(self) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Glance/Glance.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.application-groups 8 | 9 | group.com.samuelmeuli.glance 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Glance/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | $(MARKETING_VERSION) 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | LSApplicationCategoryType 24 | public.app-category.productivity 25 | LSMinimumSystemVersion 26 | $(MACOSX_DEPLOYMENT_TARGET) 27 | NSHumanReadableCopyright 28 | Copyright © 2020 Samuel Meuli. All rights reserved. 29 | NSMainStoryboardFile 30 | Main 31 | NSPrincipalClass 32 | NSApplication 33 | NSSupportsAutomaticTermination 34 | 35 | NSSupportsSuddenTermination 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /Glance/Shared/Extensions/Date.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Date { 4 | /// Converts the `Date` to a `yyyy-MM-dd` string 5 | func toDateString() -> String { 6 | let dateFormatter = DateFormatter() 7 | dateFormatter.dateFormat = "yyyy-MM-dd" 8 | return dateFormatter.string(from: self) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Glance/Shared/Utils/Log.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import os.log 3 | 4 | struct Log { 5 | // Subsystems 6 | static let subsystem = Bundle.main.bundleIdentifier! 7 | 8 | // Categories 9 | static let general = OSLog(subsystem: subsystem, category: "general") 10 | static let parse = OSLog(subsystem: subsystem, category: "parse") 11 | static let render = OSLog(subsystem: subsystem, category: "render") 12 | } 13 | -------------------------------------------------------------------------------- /Glance/Shared/Utils/Stats.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import os.log 3 | 4 | /// Class for reading and updating usage statistics. The values are stored in `UserDefaults` for the 5 | /// application group (so they can be accessed by both the main app and Quick Look extension) 6 | class Stats { 7 | private let dateCountsKey = "dateCount" 8 | private let extensionCountsKey = "extensionCount" 9 | private let totalCountKey = "totalCount" 10 | 11 | private let defaults: UserDefaults? 12 | 13 | init() { 14 | defaults = UserDefaults(suiteName: "group.com.samuelmeuli.glance") 15 | if defaults == nil { 16 | os_log( 17 | "Unable to initialize user defaults: Object is null", 18 | log: Log.general, 19 | type: .error 20 | ) 21 | } 22 | } 23 | 24 | /// Returns the stored dictionary with number of previews generated per day 25 | func getDateCounts() -> [String: Int] { 26 | defaults!.dictionary(forKey: dateCountsKey) as? [String: Int] ?? [String: Int]() 27 | } 28 | 29 | /// Returns the stored dictionary with number of previews generated per file extension 30 | func getExtensionCounts() -> [String: Int] { 31 | defaults!.dictionary(forKey: extensionCountsKey) as? [String: Int] ?? [String: Int]() 32 | } 33 | 34 | /// Returns the total number of generated previews 35 | func getTotalCount() -> Int { 36 | defaults!.integer(forKey: totalCountKey) 37 | } 38 | 39 | /// Updates all statistics to record that a new preview has been generated 40 | func increaseStatsCounts(fileExtension: String) { 41 | let todayString = Date().toDateString() 42 | 43 | // Increase today's date count by 1 44 | var dateCounts = getDateCounts() 45 | dateCounts[todayString] = dateCounts[todayString, default: 0] + 1 46 | defaults!.set(dateCounts, forKey: dateCountsKey) 47 | 48 | // Increase file extension count by 1 49 | if !fileExtension.isEmpty { // Skip for files without extension (e.g. LICENSE, Dockerfile) 50 | var extensionCounts = getExtensionCounts() 51 | extensionCounts[fileExtension] = extensionCounts[fileExtension, default: 0] + 1 52 | defaults!.set(extensionCounts, forKey: extensionCountsKey) 53 | } 54 | 55 | // Increase total count by 1 56 | defaults!.set(getTotalCount() + 1, forKey: totalCountKey) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Glance/SupportedFilesWC.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | class SupportedFilesWC: NSWindowController {} 4 | -------------------------------------------------------------------------------- /Glance/Utils/Menu.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import Foundation 3 | 4 | let feedbackURL = URL(string: "https://samuelmeuli.com")! 5 | let licenseURL = URL(string: "https://github.com/samuelmeuli/glance/blob/master/LICENSE.md")! 6 | let privacyPolicyURL = URL(string: "https://github.com/samuelmeuli/glance/blob/master/PRIVACY.md")! 7 | let websiteURL = URL(string: "https://github.com/samuelmeuli/glance")! 8 | 9 | /// Used as a subclass for the menu item in Interface Builder 10 | final class SupportedFilesMenuItem: NSMenuItem { 11 | required init(coder decoder: NSCoder) { 12 | super.init(coder: decoder) 13 | 14 | onAction = { _ in 15 | let supportedFilesWC = SupportedFilesWC(windowNibName: NSNib.Name("SupportedFilesWC")) 16 | supportedFilesWC.showWindow(nil) 17 | } 18 | } 19 | } 20 | 21 | /// Used as a subclass for the menu item in Interface Builder 22 | final class FeedbackMenuItem: NSMenuItem { 23 | required init(coder decoder: NSCoder) { 24 | super.init(coder: decoder) 25 | 26 | onAction = { _ in 27 | feedbackURL.open() 28 | } 29 | } 30 | } 31 | 32 | /// Used as a subclass for the menu item in Interface Builder 33 | final class LicenseMenuItem: NSMenuItem { 34 | required init(coder decoder: NSCoder) { 35 | super.init(coder: decoder) 36 | 37 | onAction = { _ in 38 | licenseURL.open() 39 | } 40 | } 41 | } 42 | 43 | /// Used as a subclass for the menu item in Interface Builder 44 | final class PrivacyPolicyMenuItem: NSMenuItem { 45 | required init(coder decoder: NSCoder) { 46 | super.init(coder: decoder) 47 | 48 | onAction = { _ in 49 | privacyPolicyURL.open() 50 | } 51 | } 52 | } 53 | 54 | /// Used as a subclass for the menu item in Interface Builder 55 | final class WebsiteMenuItem: NSMenuItem { 56 | required init(coder decoder: NSCoder) { 57 | super.init(coder: decoder) 58 | 59 | onAction = { _ in 60 | websiteURL.open() 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Glance/ViewController.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | class ViewController: NSViewController { 4 | @IBOutlet private var totalStatsValueLabel: NSTextField! 5 | @IBOutlet private var dailyAverageStatsValueLabel: NSTextField! 6 | @IBOutlet private var fileTypeStatsValueLabel: NSTextField! 7 | 8 | override func viewDidLoad() { 9 | super.viewDidLoad() 10 | updateStats() 11 | } 12 | 13 | @IBAction private func openSupportedFilesWindow(_: NSButton) { 14 | let supportedFilesWC = SupportedFilesWC(windowNibName: NSNib.Name("SupportedFilesWC")) 15 | supportedFilesWC.showWindow(nil) 16 | } 17 | 18 | /// Updates the statistics text fields with the actual usage data 19 | private func updateStats() { 20 | let stats = Stats() 21 | let extensionCounts = stats.getExtensionCounts() 22 | 23 | let nrPreviews = stats.getTotalCount() 24 | let averagePreviewsPerDay = nrPreviews == 0 25 | ? 0 26 | : lround(Double(nrPreviews) / Double(stats.getDateCounts().count)) 27 | let mostPopularExtension = extensionCounts.max { a, b in a.value < b.value } 28 | 29 | totalStatsValueLabel.stringValue = String(nrPreviews) 30 | dailyAverageStatsValueLabel.stringValue = String(averagePreviewsPerDay) 31 | fileTypeStatsValueLabel.stringValue = mostPopularExtension?.key ?? "None" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /GlanceTests/FileTreeTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | /// Helper function for asserting that the provided parent node has a child with the specified 4 | /// file/directory properties. 5 | func assertNode( 6 | parent: FileTreeNode, 7 | name: String, 8 | size: Int, 9 | isDirectory: Bool, 10 | dateModified: Date? 11 | ) -> FileTreeNode { 12 | guard let node = parent.childrenList.first(where: { $0.name == name }) else { 13 | XCTFail("Node \"\(parent.name)\" doesn't have child node \"\(name)\"") 14 | exit(1) 15 | } 16 | XCTAssertNotNil(node) 17 | XCTAssertEqual(node.name, name) 18 | XCTAssertEqual(node.size, size) 19 | XCTAssertEqual(node.isDirectory, isDirectory) 20 | XCTAssertEqual(node.dateModified, dateModified) 21 | return node 22 | } 23 | 24 | class FileTreeTests: XCTestCase { 25 | var fileTree: FileTree? 26 | let now = Date() 27 | 28 | override func setUp() { 29 | super.setUp() 30 | fileTree = FileTree() 31 | } 32 | 33 | // No files 34 | 35 | func testInit() { 36 | XCTAssert(fileTree!.root.children.isEmpty) 37 | } 38 | 39 | // Tree: 40 | // 41 | // └── file 42 | 43 | func testAddSingleFile() throws { 44 | try fileTree?.addNode( 45 | path: "file", 46 | isDirectory: false, 47 | size: 123, 48 | dateModified: now 49 | ) 50 | 51 | _ = assertNode( 52 | parent: fileTree!.root, 53 | name: "file", 54 | size: 123, 55 | isDirectory: false, 56 | dateModified: now 57 | ) 58 | } 59 | 60 | // Tree: 61 | // 62 | // └── empty-directory/ 63 | 64 | func testAddEmptyDirectory() throws { 65 | try fileTree?.addNode( 66 | path: "empty-directory", 67 | isDirectory: true, 68 | size: 0, 69 | dateModified: now 70 | ) 71 | 72 | _ = assertNode( 73 | parent: fileTree!.root, 74 | name: "empty-directory", 75 | size: 0, 76 | isDirectory: true, 77 | dateModified: now 78 | ) 79 | } 80 | 81 | // Tree: 82 | // 83 | // └── non-empty-directory/ 84 | // ├── file-1 85 | // └── file-2 86 | 87 | func testAddNonEmptyDirectory1() throws { 88 | // Provide directory first 89 | try fileTree?.addNode( 90 | path: "non-empty-directory", 91 | isDirectory: true, 92 | size: 0, 93 | dateModified: now 94 | ) 95 | try fileTree?.addNode( 96 | path: "non-empty-directory/file-1", 97 | isDirectory: false, 98 | size: 123, 99 | dateModified: now 100 | ) 101 | try fileTree?.addNode( 102 | path: "non-empty-directory/file-2", 103 | isDirectory: false, 104 | size: 123, 105 | dateModified: now 106 | ) 107 | 108 | let nonEmptyDirectory = assertNode( 109 | parent: fileTree!.root, 110 | name: "non-empty-directory", 111 | size: 0, 112 | isDirectory: true, 113 | dateModified: now 114 | ) 115 | _ = assertNode( 116 | parent: nonEmptyDirectory, 117 | name: "file-1", 118 | size: 123, 119 | isDirectory: false, 120 | dateModified: now 121 | ) 122 | _ = assertNode( 123 | parent: nonEmptyDirectory, 124 | name: "file-2", 125 | size: 123, 126 | isDirectory: false, 127 | dateModified: now 128 | ) 129 | } 130 | 131 | func testAddNonEmptyDirectory2() throws { 132 | // Provide directory between files 133 | try fileTree?.addNode( 134 | path: "non-empty-directory/file-1", 135 | isDirectory: false, 136 | size: 123, 137 | dateModified: now 138 | ) 139 | try fileTree?.addNode( 140 | path: "non-empty-directory", 141 | isDirectory: true, 142 | size: 0, 143 | dateModified: now 144 | ) 145 | try fileTree?.addNode( 146 | path: "non-empty-directory/file-2", 147 | isDirectory: false, 148 | size: 123, 149 | dateModified: now 150 | ) 151 | 152 | let nonEmptyDirectory = assertNode( 153 | parent: fileTree!.root, 154 | name: "non-empty-directory", 155 | size: 0, 156 | isDirectory: true, 157 | dateModified: now 158 | ) 159 | _ = assertNode( 160 | parent: nonEmptyDirectory, 161 | name: "file-1", 162 | size: 123, 163 | isDirectory: false, 164 | dateModified: now 165 | ) 166 | _ = assertNode( 167 | parent: nonEmptyDirectory, 168 | name: "file-2", 169 | size: 123, 170 | isDirectory: false, 171 | dateModified: now 172 | ) 173 | } 174 | 175 | func testAddNonEmptyDirectory3() throws { 176 | // Provide directory last 177 | try fileTree?.addNode( 178 | path: "non-empty-directory/file-1", 179 | isDirectory: false, 180 | size: 123, 181 | dateModified: now 182 | ) 183 | try fileTree?.addNode( 184 | path: "non-empty-directory/file-2", 185 | isDirectory: false, 186 | size: 123, 187 | dateModified: now 188 | ) 189 | try fileTree?.addNode( 190 | path: "non-empty-directory", 191 | isDirectory: true, 192 | size: 0, 193 | dateModified: now 194 | ) 195 | 196 | let nonEmptyDirectory = assertNode( 197 | parent: fileTree!.root, 198 | name: "non-empty-directory", 199 | size: 0, 200 | isDirectory: true, 201 | dateModified: now 202 | ) 203 | _ = assertNode( 204 | parent: nonEmptyDirectory, 205 | name: "file-1", 206 | size: 123, 207 | isDirectory: false, 208 | dateModified: now 209 | ) 210 | _ = assertNode( 211 | parent: nonEmptyDirectory, 212 | name: "file-2", 213 | size: 123, 214 | isDirectory: false, 215 | dateModified: now 216 | ) 217 | } 218 | 219 | func testAddNonEmptyDirectory4() throws { 220 | // Create directory implicitly 221 | try fileTree?.addNode( 222 | path: "non-empty-directory/file-1", 223 | isDirectory: false, 224 | size: 123, 225 | dateModified: now 226 | ) 227 | try fileTree?.addNode( 228 | path: "non-empty-directory/file-2", 229 | isDirectory: false, 230 | size: 123, 231 | dateModified: now 232 | ) 233 | 234 | let nonEmptyDirectory = assertNode( 235 | parent: fileTree!.root, 236 | name: "non-empty-directory", 237 | size: 0, 238 | isDirectory: true, 239 | dateModified: nil // Date is not know because directory was created implicitly 240 | ) 241 | _ = assertNode( 242 | parent: nonEmptyDirectory, 243 | name: "file-1", 244 | size: 123, 245 | isDirectory: false, 246 | dateModified: now 247 | ) 248 | _ = assertNode( 249 | parent: nonEmptyDirectory, 250 | name: "file-2", 251 | size: 123, 252 | isDirectory: false, 253 | dateModified: now 254 | ) 255 | } 256 | 257 | // Tree: 258 | // 259 | // ├── non-empty-directory/ 260 | // │ ├── file-1 261 | // │ └── file-2 262 | // ├── empty-directory/ 263 | // └── file-1 264 | 265 | func testAddComplexFileStructure() throws { 266 | try fileTree?.addNode( 267 | path: "empty-directory", 268 | isDirectory: true, 269 | size: 0, 270 | dateModified: now 271 | ) 272 | try fileTree?.addNode( 273 | path: "non-empty-directory/file-2", 274 | isDirectory: false, 275 | size: 123, 276 | dateModified: now 277 | ) 278 | try fileTree?.addNode( 279 | path: "file-1", 280 | isDirectory: false, 281 | size: 123, 282 | dateModified: now 283 | ) 284 | try fileTree?.addNode( 285 | path: "non-empty-directory", 286 | isDirectory: true, 287 | size: 0, 288 | dateModified: now 289 | ) 290 | try fileTree?.addNode( 291 | path: "non-empty-directory/file-3", 292 | isDirectory: false, 293 | size: 123, 294 | dateModified: now 295 | ) 296 | 297 | _ = assertNode( 298 | parent: fileTree!.root, 299 | name: "empty-directory", 300 | size: 0, 301 | isDirectory: true, 302 | dateModified: now 303 | ) 304 | _ = assertNode( 305 | parent: fileTree!.root, 306 | name: "file-1", 307 | size: 123, 308 | isDirectory: false, 309 | dateModified: now 310 | ) 311 | let nonEmptyDirectory = assertNode( 312 | parent: fileTree!.root, 313 | name: "non-empty-directory", 314 | size: 0, 315 | isDirectory: true, 316 | dateModified: now 317 | ) 318 | _ = assertNode( 319 | parent: nonEmptyDirectory, 320 | name: "file-2", 321 | size: 123, 322 | isDirectory: false, 323 | dateModified: now 324 | ) 325 | _ = assertNode( 326 | parent: nonEmptyDirectory, 327 | name: "file-3", 328 | size: 123, 329 | isDirectory: false, 330 | dateModified: now 331 | ) 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /GlanceTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /GlanceTests/TestFiles/apple-script/example-apple-script.applescript: -------------------------------------------------------------------------------- 1 | -- This is a comment 2 | display alert "Alert!" 3 | -------------------------------------------------------------------------------- /GlanceTests/TestFiles/apple-script/example-apple-script.scpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/GlanceTests/TestFiles/apple-script/example-apple-script.scpt -------------------------------------------------------------------------------- /GlanceTests/TestFiles/apple-script/example-apple-script.scptd/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleIdentifier 6 | com.apple.ScriptEditor.id.example-applescript 7 | CFBundleName 8 | Untitled 9 | CFBundleShortVersionString 10 | 1.0 11 | WindowState 12 | 13 | bundleDividerCollapsed 14 | 15 | bundlePositionOfDivider 16 | 0.0 17 | dividerCollapsed 18 | 19 | eventLogLevel 20 | 2 21 | name 22 | ScriptWindowState 23 | positionOfDivider 24 | 421 25 | savedFrame 26 | 69 157 700 672 0 0 1440 877 27 | selectedTab 28 | result 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /GlanceTests/TestFiles/apple-script/example-apple-script.scptd/Contents/Resources/Scripts/main.scpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/GlanceTests/TestFiles/apple-script/example-apple-script.scptd/Contents/Resources/Scripts/main.scpt -------------------------------------------------------------------------------- /GlanceTests/TestFiles/apple-script/example-apple-script.scptd/Contents/Resources/description.rtfd/TXT.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\ansi\ansicpg1252\cocoartf2512 2 | \cocoatextscaling0\cocoaplatform0{\fonttbl} 3 | {\colortbl;\red255\green255\blue255;} 4 | {\*\expandedcolortbl;;} 5 | } 6 | -------------------------------------------------------------------------------- /GlanceTests/TestFiles/apple-script/example-jxa.applescript: -------------------------------------------------------------------------------- 1 | // This is a comment 2 | const app = Application.currentApplication(); 3 | app.includeStandardAdditions = true; 4 | app.displayAlert("Alert!"); 5 | -------------------------------------------------------------------------------- /GlanceTests/TestFiles/apple-script/example-jxa.scpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/GlanceTests/TestFiles/apple-script/example-jxa.scpt -------------------------------------------------------------------------------- /GlanceTests/TestFiles/apple-script/example-jxa.scptd/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleIdentifier 6 | com.apple.ScriptEditor.id.example-jxa 7 | CFBundleName 8 | example-jxa 9 | CFBundleShortVersionString 10 | 1.0 11 | WindowState 12 | 13 | bundleDividerCollapsed 14 | 15 | bundlePositionOfDivider 16 | 0.0 17 | dividerCollapsed 18 | 19 | eventLogLevel 20 | 2 21 | name 22 | ScriptWindowState 23 | positionOfDivider 24 | 395 25 | savedFrame 26 | 69 157 700 672 0 0 1440 877 27 | selectedTab 28 | result 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /GlanceTests/TestFiles/apple-script/example-jxa.scptd/Contents/Resources/Scripts/main.scpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/GlanceTests/TestFiles/apple-script/example-jxa.scptd/Contents/Resources/Scripts/main.scpt -------------------------------------------------------------------------------- /GlanceTests/TestFiles/apple-script/example-jxa.scptd/Contents/Resources/description.rtfd/TXT.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\ansi\ansicpg1252\cocoartf2512 2 | \cocoatextscaling0\cocoaplatform0{\fonttbl} 3 | {\colortbl;\red255\green255\blue255;} 4 | {\*\expandedcolortbl;;} 5 | } 6 | -------------------------------------------------------------------------------- /GlanceTests/TestFiles/archives/example-no-files.tar: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /GlanceTests/TestFiles/archives/example-no-files.zip: -------------------------------------------------------------------------------- 1 | PK -------------------------------------------------------------------------------- /GlanceTests/TestFiles/archives/example-no-root-directory.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/GlanceTests/TestFiles/archives/example-no-root-directory.zip -------------------------------------------------------------------------------- /GlanceTests/TestFiles/archives/example-root-directory.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/GlanceTests/TestFiles/archives/example-root-directory.zip -------------------------------------------------------------------------------- /GlanceTests/TestFiles/archives/example-single-file.tar: -------------------------------------------------------------------------------- 1 | hello-world.txt000644 000765 000024 00000000015 13650245644 014505 0ustar00samuelstaff000000 000000 Hello world! 2 | -------------------------------------------------------------------------------- /GlanceTests/TestFiles/archives/example-single-file.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/GlanceTests/TestFiles/archives/example-single-file.zip -------------------------------------------------------------------------------- /GlanceTests/TestFiles/archives/example.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelmeuli/glance/130cccb10d19eddaaebcb42caad5cd9256eeb61d/GlanceTests/TestFiles/archives/example.tar.gz -------------------------------------------------------------------------------- /GlanceTests/TestFiles/jupyter-notebook/example.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Sample Notebook" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "This is a Markdown section. It contains **bold** and _italic_ text.\n", 15 | "\n", 16 | "Bullet list:\n", 17 | "\n", 18 | "- Hello\n", 19 | "- World\n", 20 | "\n", 21 | "Numbered list:\n", 22 | "\n", 23 | "1. First\n", 24 | "2. Second\n", 25 | "3. Third" 26 | ] 27 | }, 28 | { 29 | "cell_type": "code", 30 | "execution_count": 1, 31 | "metadata": {}, 32 | "outputs": [ 33 | { 34 | "name": "stdout", 35 | "output_type": "stream", 36 | "text": [ 37 | "Hello\n", 38 | "world\n" 39 | ] 40 | } 41 | ], 42 | "source": [ 43 | "def my_function():\n", 44 | " print(\"Hello\")\n", 45 | " print(\"world\")\n", 46 | "\n", 47 | "my_function()" 48 | ] 49 | }, 50 | { 51 | "cell_type": "raw", 52 | "metadata": {}, 53 | "source": [ 54 | "This is a raw section, without formatting.\n", 55 | "This is the second line." 56 | ] 57 | }, 58 | { 59 | "cell_type": "markdown", 60 | "metadata": {}, 61 | "source": [ 62 | "Here is an equation:\n", 63 | "\n", 64 | "$$c = \\sqrt{a^2 + b^2}$$" 65 | ] 66 | }, 67 | { 68 | "cell_type": "code", 69 | "execution_count": 2, 70 | "metadata": {}, 71 | "outputs": [ 72 | { 73 | "data": { 74 | "text/html": [ 75 | "
\n", 76 | "\n", 89 | "\n", 90 | " \n", 91 | " \n", 92 | " \n", 93 | " \n", 94 | " \n", 95 | " \n", 96 | " \n", 97 | " \n", 98 | " \n", 99 | " \n", 100 | " \n", 101 | " \n", 102 | " \n", 103 | " \n", 104 | " \n", 105 | " \n", 106 | " \n", 107 | " \n", 108 | " \n", 109 | " \n", 110 | " \n", 111 | " \n", 112 | " \n", 113 | " \n", 114 | " \n", 115 | " \n", 116 | " \n", 117 | " \n", 118 | "
first_nameagegender
0Alice12f
1Bob23m
2Charlie34m
\n", 119 | "
" 120 | ], 121 | "text/plain": [ 122 | " first_name age gender\n", 123 | "0 Alice 12 f\n", 124 | "1 Bob 23 m\n", 125 | "2 Charlie 34 m" 126 | ] 127 | }, 128 | "execution_count": 2, 129 | "metadata": {}, 130 | "output_type": "execute_result" 131 | } 132 | ], 133 | "source": [ 134 | "from io import StringIO\n", 135 | "\n", 136 | "import pandas as pd\n", 137 | "\n", 138 | "sample_csv = StringIO(\"\"\"first_name,age,gender\n", 139 | "Alice,12,f\n", 140 | "Bob,23,m\n", 141 | "Charlie,34,m\"\"\")\n", 142 | "\n", 143 | "df = pd.read_csv(sample_csv)\n", 144 | "df" 145 | ] 146 | }, 147 | { 148 | "cell_type": "code", 149 | "execution_count": 3, 150 | "metadata": {}, 151 | "outputs": [ 152 | { 153 | "data": { 154 | "text/plain": [ 155 | "" 156 | ] 157 | }, 158 | "execution_count": 3, 159 | "metadata": {}, 160 | "output_type": "execute_result" 161 | }, 162 | { 163 | "data": { 164 | "image/png": "iVBORw0KGgoAAAANSUhEUgAAAXAAAAD1CAYAAABJE67gAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4yLjAsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy8GearUAAAOpklEQVR4nO3dcYzfdX3H8edLqFRGo9Temq4Vr1EYFiYlXjsMJDCc0MkSS5zL+EPZYqxLxobGLXb8I4vbgglKpll0NXQ2xCkGJBAVN2Ag0QF6YIeFIjhT4ZoKJyJYI46D9/64X+FW7vr79e539+unfT6Sy/1+n9/3d7/35Zc+882339/3UlVIktrzikEPIEmaHQMuSY0y4JLUKAMuSY0y4JLUKAMuSY06utsGSRYDdwLHdLa/rqo+muTzwNnA051N/7Sqth/oZy1btqyGh4fnNLAkHWnuvffen1bV0P7rXQMO/Bo4t6r2JlkEfCvJzZ3H/qaqrut1iOHhYUZHR3vdXJIEJPnxdOtdA16Tn/TZ27m7qPPlp38kacB6Ogae5Kgk24EngFuq6p7OQ/+Q5P4kVyU5Zt6mlCS9TE8Br6rnq2otsApYn+RU4G+Bk4F1wFLgI9M9N8mmJKNJRsfHx/s0tiSpl2PgL6qqnye5HdhQVVd2ln+d5F+Bv57hOVuALQAjIyMvO/Ty3HPPMTY2xrPPPntwkx/iFi9ezKpVq1i0aNGgR5F0mOrlLJQh4LlOvF8FvB34eJIVVbUnSYCNwI7ZDDA2NsaSJUsYHh5m8ke1r6p48sknGRsbY/Xq1YMeR9Jhqpc98BXAtiRHMXnI5ctV9dUk/9mJe4DtwJ/PZoBnn332sIo3QBJe+9rX4iEjSfOpl7NQ7gdOn2b93H4NcTjFe5/D8XeSdGjxk5iS1KiD+k/MhTC8+Wt9/Xm7rrigrz9PUnf9/nd8qDlUuuIeeMfGjRt5y1vewimnnMKWLVsAuPrqqznppJNYv34973//+7nkkksAGB8f513vehfr1q1j3bp1fPvb3x7k6JKOUIfcHvigbN26laVLl/KrX/2KdevWccEFF/Cxj32M++67jyVLlnDuuedy2mmnAXDppZfyoQ99iLPOOotHH32U888/n507dw74N5B0pDHgHZ/61Ke44YYbAHjssce45pprOPvss1m6dCkA7373u3n44YcBuPXWW3nwwQdffO4zzzzD3r17Oe644xZ+cElHLAMO3HHHHdx6663cddddHHvssZxzzjmcfPLJM+5Vv/DCC9x9990sXrx4gSeVpJd4DBx4+umnOf744zn22GN56KGHuPvuu/nlL3/JN7/5TZ566ikmJia4/vrrX9z+vPPO49Of/vSL97dvP+BVdCVpXhhwYMOGDUxMTPCmN72JzZs3c8YZZ7By5Uouu+wy1q9fz5lnnsnw8DCvfvWrgcnDLaOjo7z5zW9mzZo1fPaznx3wbyDpSHTIHUIZxOk5xxxzDDfffPPL1kdGRti0aRMTExNceOGFbNy4EYBly5Zx7bXXLvSYkvT/uAd+AJdffjlr167l1FNPZfXq1S8GXJIOBYfcHvih5Morr+y+kSQNiHvgktSoQyLgk3+17fByOP5Okg4tAw/44sWLefLJJw+r4O27HrjniUuaTwM/Br5q1SrGxsYOu2tn7/uLPJI0XwYe8EWLFvlXayRpFgZ+CEWSNDsGXJIaZcAlqVEGXJIaZcAlqVEGXJIa1TXgSRYn+U6S/07yQJK/66yvTnJPkh8muTbJK+d/XEnSPr3sgf8aOLeqTgPWAhuSnAF8HLiqqt4IPAW8b/7GlCTtr2vAa9Lezt1Fna8CzgWu66xvA7zWqiQtoJ6OgSc5Ksl24AngFuB/gJ9X1URnkzFg5fyMKEmaTk8Br6rnq2otsApYD5zc6wsk2ZRkNMno4Xa9E0kapIM6C6Wqfg7cDrwVeE2SfddSWQXsnuE5W6pqpKpGhoaG5jSsJOklvZyFMpTkNZ3brwLeDuxkMuR/1NnsYuDG+RpSkvRyvVyNcAWwLclRTAb/y1X11SQPAl9K8vfA94Cr53FOSdJ+uga8qu4HTp9m/UdMHg+XJA2An8SUpEYZcElqlAGXpEYZcElqlAGXpEYZcElqlAGXpEYZcElqlAGXpEYZcElqlAGXpEYZcElqlAGXpEYZcElqlAGXpEYZcElqlAGXpEYZcElqlAGXpEYZcElqlAGXpEZ1DXiS1yW5PcmDSR5Icmln/fIku5Ns73y9Y/7HlSTtc3QP20wAH66q+5IsAe5Nckvnsauq6sr5G0+SNJOuAa+qPcCezu1fJNkJrJzvwSRJB3ZQx8CTDAOnA/d0li5Jcn+SrUmO7/NskqQD6DngSY4Drgc+WFXPAJ8B3gCsZXIP/RMzPG9TktEko+Pj430YWZIEPQY8ySIm4/2FqvoKQFU9XlXPV9ULwOeA9dM9t6q2VNVIVY0MDQ31a25JOuL1chZKgKuBnVX1ySnrK6ZsdiGwo//jSZJm0stZKGcC7wG+n2R7Z+0y4KIka4ECdgEfmJcJJUnT6uUslG8Bmeahr/d/HElSr/wkpiQ1yoBLUqMMuCQ1yoBLUqMMuCQ1yoBLUqMMuCQ1qpcP8kgLbnjz1wY9wrzadcUFgx5BhwH3wCWpUQZckhplwCWpUQZckhplwCWpUQZckhplwCWpUQZckhplwCWpUQZckhplwCWpUQZckhplwCWpUQZckhrVNeBJXpfk9iQPJnkgyaWd9aVJbknySOf78fM/riRpn172wCeAD1fVGuAM4C+SrAE2A7dV1YnAbZ37kqQF0jXgVbWnqu7r3P4FsBNYCbwT2NbZbBuwcb6GlCS93EEdA08yDJwO3AMsr6o9nYd+Aizv62SSpAPqOeBJjgOuBz5YVc9MfayqCqgZnrcpyWiS0fHx8TkNK0l6SU8BT7KIyXh/oaq+0ll+PMmKzuMrgCeme25VbamqkaoaGRoa6sfMkiR6OwslwNXAzqr65JSHbgIu7ty+GLix/+NJkmbSy1+lPxN4D/D9JNs7a5cBVwBfTvI+4MfAH8/PiJKk6XQNeFV9C8gMD7+tv+NIknrlJzElqVEGXJIaZcAlqVEGXJIaZcAlqVEGXJIaZcAlqVEGXJIaZcAlqVEGXJIaZcAlqVEGXJIaZcAlqVEGXJIaZcAlqVEGXJIaZcAlqVEGXJIaZcAlqVEGXJIaZcAlqVFdA55ka5InkuyYsnZ5kt1Jtne+3jG/Y0qS9tfLHvjngQ3TrF9VVWs7X1/v71iSpG66Bryq7gR+tgCzSJIOwlyOgV+S5P7OIZbj+zaRJKknsw34Z4A3AGuBPcAnZtowyaYko0lGx8fHZ/lykqT9zSrgVfV4VT1fVS8AnwPWH2DbLVU1UlUjQ0NDs51TkrSfWQU8yYopdy8Edsy0rSRpfhzdbYMkXwTOAZYlGQM+CpyTZC1QwC7gA/M4oyRpGl0DXlUXTbN89TzMIkk6CH4SU5IaZcAlqVEGXJIaZcAlqVEGXJIaZcAlqVEGXJIaZcAlqVEGXJIaZcAlqVEGXJIaZcAlqVEGXJIa1fVqhC0b3vy1QY8wr3ZdccGgR5A0QO6BS1KjDLgkNcqAS1KjDLgkNcqAS1KjDLgkNcqAS1KjDLgkNaprwJNsTfJEkh1T1pYmuSXJI53vx8/vmJKk/fWyB/55YMN+a5uB26rqROC2zn1J0gLqGvCquhP42X7L7wS2dW5vAzb2eS5JUhezPQa+vKr2dG7/BFjep3kkST2a839iVlUBNdPjSTYlGU0yOj4+PteXkyR1zDbgjydZAdD5/sRMG1bVlqoaqaqRoaGhWb6cJGl/sw34TcDFndsXAzf2ZxxJUq96OY3wi8BdwG8nGUvyPuAK4O1JHgF+v3NfkrSAuv5Bh6q6aIaH3tbnWSRJB8FPYkpSowy4JDXKgEtSowy4JDXKgEtSowy4JDXKgEtSowy4JDXKgEtSowy4JDXKgEtSowy4JDXKgEtSowy4JDXKgEtSowy4JDXKgEtSowy4JDXKgEtSowy4JDXKgEtSo7r+VfoDSbIL+AXwPDBRVSP9GEqS1N2cAt7xe1X10z78HEnSQfAQiiQ1aq4BL+A/ktybZFM/BpIk9Wauh1DOqqrdSX4TuCXJQ1V159QNOmHfBHDCCSfM8eUkSfvMaQ+8qnZ3vj8B3ACsn2abLVU1UlUjQ0NDc3k5SdIUsw54kt9IsmTfbeA8YEe/BpMkHdhcDqEsB25Isu/n/FtVfaMvU0mSupp1wKvqR8BpfZxFknQQPI1QkhplwCWpUQZckhplwCWpUQZckhplwCWpUQZckhplwCWpUQZckhplwCWpUQZckhplwCWpUQZckhplwCWpUQZckhplwCWpUQZckhplwCWpUQZckhplwCWpUQZckhplwCWpUXMKeJINSX6Q5IdJNvdrKElSd7MOeJKjgH8G/gBYA1yUZE2/BpMkHdhc9sDXAz+sqh9V1f8CXwLe2Z+xJEndHD2H564EHptyfwz43f03SrIJ2NS5uzfJD+bwmoe6ZcBPF+rF8vGFeqUjgu9d2w739+/10y3OJeA9qaotwJb5fp1DQZLRqhoZ9Bw6eL53bTtS37+5HELZDbxuyv1VnTVJ0gKYS8C/C5yYZHWSVwJ/AtzUn7EkSd3M+hBKVU0kuQT4d+AoYGtVPdC3ydp0RBwqOkz53rXtiHz/UlWDnkGSNAt+ElOSGmXAJalRBlySGjXv54EfrpKczOQnT1d2lnYDN1XVzsFNJR0ZOv/+VgL3VNXeKesbquobg5tsYbkHPgtJPsLkpQMCfKfzFeCLXtSrbUn+bNAz6MCS/BVwI/CXwI4kUy/h8Y+DmWowPAtlFpI8DJxSVc/tt/5K4IGqOnEwk2mukjxaVScMeg7NLMn3gbdW1d4kw8B1wDVV9U9JvldVpw90wAXkIZTZeQH4LeDH+62v6DymQ1iS+2d6CFi+kLNoVl6x77BJVe1Kcg5wXZLXM/keHjEM+Ox8ELgtySO8dEGvE4A3ApcMbCr1ajlwPvDUfusB/mvhx9FBejzJ2qraDtDZE/9DYCvwO4MdbWEZ8Fmoqm8kOYnJS+pO/U/M71bV84ObTD36KnDcvgBMleSOhR9HB+m9wMTUhaqaAN6b5F8GM9JgeAxckhrlWSiS1CgDLkmNMuCS1CgDLkmNMuCS1Kj/A1c8iedcpFuLAAAAAElFTkSuQmCC\n", 165 | "text/plain": [ 166 | "
" 167 | ] 168 | }, 169 | "metadata": { 170 | "needs_background": "light" 171 | }, 172 | "output_type": "display_data" 173 | } 174 | ], 175 | "source": [ 176 | "df.plot.bar()" 177 | ] 178 | }, 179 | { 180 | "cell_type": "code", 181 | "execution_count": 4, 182 | "metadata": {}, 183 | "outputs": [ 184 | { 185 | "data": { 186 | "text/plain": [ 187 | "age 11.0\n", 188 | "dtype: float64" 189 | ] 190 | }, 191 | "execution_count": 4, 192 | "metadata": {}, 193 | "output_type": "execute_result" 194 | } 195 | ], 196 | "source": [ 197 | "df.std()" 198 | ] 199 | }, 200 | { 201 | "cell_type": "code", 202 | "execution_count": 5, 203 | "metadata": {}, 204 | "outputs": [ 205 | { 206 | "ename": "Exception", 207 | "evalue": "This is an error message", 208 | "output_type": "error", 209 | "traceback": [ 210 | "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", 211 | "\u001b[0;31mException\u001b[0m Traceback (most recent call last)", 212 | "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mException\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"This is an error message\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", 213 | "\u001b[0;31mException\u001b[0m: This is an error message" 214 | ] 215 | } 216 | ], 217 | "source": [ 218 | "raise Exception(\"This is an error message\")" 219 | ] 220 | } 221 | ], 222 | "metadata": { 223 | "kernelspec": { 224 | "display_name": "Python 3", 225 | "language": "python", 226 | "name": "python3" 227 | }, 228 | "language_info": { 229 | "codemirror_mode": { 230 | "name": "ipython", 231 | "version": 3 232 | }, 233 | "file_extension": ".py", 234 | "mimetype": "text/x-python", 235 | "name": "python", 236 | "nbconvert_exporter": "python", 237 | "pygments_lexer": "ipython3", 238 | "version": "3.8.2" 239 | } 240 | }, 241 | "nbformat": 4, 242 | "nbformat_minor": 4 243 | } 244 | -------------------------------------------------------------------------------- /GlanceTests/TestFiles/markdown/example.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "My Document" 3 | output: html_document 4 | --- 5 | 6 | # My Document 7 | 8 | ## R Markdown 9 | 10 | ```{r include = FALSE} 11 | print("Hello world from R") 12 | ``` 13 | 14 | ## Emphasis 15 | 16 | This is a Markdown document. It may contain **bold** or _italic_ text. Text can also be **_both bold and italic_**. 17 | 18 | ## Lists 19 | 20 | Markdown supports ordered and unordered lists, which may be nested: 21 | 22 | 1. Item 1 23 | 2. Item 2 24 | - Item 2a 25 | - Item 2b 26 | 27 | ## Code 28 | 29 | This `code` is inline. The following is a code block with syntax highlighting: 30 | 31 | ```{python} 32 | print("Hello world from Python") 33 | ``` 34 | 35 | ## Links 36 | 37 | Links can be created explicitly ([example](https://example.com)) or detected automatically (https://example.com). 38 | 39 | ## Blockquotes 40 | 41 | You can easily create quotes: 42 | 43 | > This is the quote's first line. 44 | > 45 | > This is the second one. 46 | 47 | ## Tables 48 | 49 | | First header | Second header | 50 | | ------------ | ------------- | 51 | | 1 | 2 | 52 | | 3 | 4 | 53 | -------------------------------------------------------------------------------- /GlanceTests/TestFiles/markdown/example.markdown: -------------------------------------------------------------------------------- 1 | # My Document 2 | 3 | ## Emphasis 4 | 5 | This is a Markdown document. It may contain **bold** or _italic_ text. Text can also be **_both bold and italic_**. 6 | 7 | ## Lists 8 | 9 | Markdown supports ordered and unordered lists, which may be nested: 10 | 11 | 1. Item 1 12 | 2. Item 2 13 | - Item 2a 14 | - Item 2b 15 | 16 | ## Code 17 | 18 | This `code` is inline. The following is a code block with syntax highlighting: 19 | 20 | ```js 21 | const print = (text) => console.log(text); 22 | print("Hello world"); 23 | ``` 24 | 25 | ## Links 26 | 27 | Links can be created explicitly ([example](https://example.com)) or detected automatically (https://example.com). 28 | 29 | ## Blockquotes 30 | 31 | You can easily create quotes: 32 | 33 | > This is the quote's first line. 34 | > 35 | > This is the second one. 36 | 37 | ## Tables 38 | 39 | | First header | Second header | 40 | | ------------ | ------------- | 41 | | 1 | 2 | 42 | | 3 | 4 | 43 | -------------------------------------------------------------------------------- /GlanceTests/TestFiles/markdown/example.md: -------------------------------------------------------------------------------- 1 | # My Document 2 | 3 | ## Emphasis 4 | 5 | This is a Markdown document. It may contain **bold** or _italic_ text. Text can also be **_both bold and italic_**. 6 | 7 | ## Lists 8 | 9 | Markdown supports ordered and unordered lists, which may be nested: 10 | 11 | 1. Item 1 12 | 2. Item 2 13 | - Item 2a 14 | - Item 2b 15 | 16 | ## Code 17 | 18 | This `code` is inline. The following is a code block with syntax highlighting: 19 | 20 | ```js 21 | const print = (text) => console.log(text); 22 | print("Hello world"); 23 | ``` 24 | 25 | ## Links 26 | 27 | Links can be created explicitly ([example](https://example.com)) or detected automatically (https://example.com). 28 | 29 | ## Blockquotes 30 | 31 | You can easily create quotes: 32 | 33 | > This is the quote's first line. 34 | > 35 | > This is the second one. 36 | 37 | ## Tables 38 | 39 | | First header | Second header | 40 | | ------------ | ------------- | 41 | | 1 | 2 | 42 | | 3 | 4 | 43 | -------------------------------------------------------------------------------- /GlanceTests/TestFiles/markdown/example.mdown: -------------------------------------------------------------------------------- 1 | # My Document 2 | 3 | ## Emphasis 4 | 5 | This is a Markdown document. It may contain **bold** or _italic_ text. Text can also be **_both bold and italic_**. 6 | 7 | ## Lists 8 | 9 | Markdown supports ordered and unordered lists, which may be nested: 10 | 11 | 1. Item 1 12 | 2. Item 2 13 | - Item 2a 14 | - Item 2b 15 | 16 | ## Code 17 | 18 | This `code` is inline. The following is a code block with syntax highlighting: 19 | 20 | ```js 21 | const print = (text) => console.log(text); 22 | print("Hello world"); 23 | ``` 24 | 25 | ## Links 26 | 27 | Links can be created explicitly ([example](https://example.com)) or detected automatically (https://example.com). 28 | 29 | ## Blockquotes 30 | 31 | You can easily create quotes: 32 | 33 | > This is the quote's first line. 34 | > 35 | > This is the second one. 36 | 37 | ## Tables 38 | 39 | | First header | Second header | 40 | | ------------ | ------------- | 41 | | 1 | 2 | 42 | | 3 | 4 | 43 | -------------------------------------------------------------------------------- /GlanceTests/TestFiles/markdown/example.mkd: -------------------------------------------------------------------------------- 1 | # My Document 2 | 3 | ## Emphasis 4 | 5 | This is a Markdown document. It may contain **bold** or _italic_ text. Text can also be **_both bold and italic_**. 6 | 7 | ## Lists 8 | 9 | Markdown supports ordered and unordered lists, which may be nested: 10 | 11 | 1. Item 1 12 | 2. Item 2 13 | - Item 2a 14 | - Item 2b 15 | 16 | ## Code 17 | 18 | This `code` is inline. The following is a code block with syntax highlighting: 19 | 20 | ```js 21 | const print = (text) => console.log(text); 22 | print("Hello world"); 23 | ``` 24 | 25 | ## Links 26 | 27 | Links can be created explicitly ([example](https://example.com)) or detected automatically (https://example.com). 28 | 29 | ## Blockquotes 30 | 31 | You can easily create quotes: 32 | 33 | > This is the quote's first line. 34 | > 35 | > This is the second one. 36 | 37 | ## Tables 38 | 39 | | First header | Second header | 40 | | ------------ | ------------- | 41 | | 1 | 2 | 42 | | 3 | 4 | 43 | -------------------------------------------------------------------------------- /GlanceTests/TestFiles/markdown/example.mkdn: -------------------------------------------------------------------------------- 1 | # My Document 2 | 3 | ## Emphasis 4 | 5 | This is a Markdown document. It may contain **bold** or _italic_ text. Text can also be **_both bold and italic_**. 6 | 7 | ## Lists 8 | 9 | Markdown supports ordered and unordered lists, which may be nested: 10 | 11 | 1. Item 1 12 | 2. Item 2 13 | - Item 2a 14 | - Item 2b 15 | 16 | ## Code 17 | 18 | This `code` is inline. The following is a code block with syntax highlighting: 19 | 20 | ```js 21 | const print = (text) => console.log(text); 22 | print("Hello world"); 23 | ``` 24 | 25 | ## Links 26 | 27 | Links can be created explicitly ([example](https://example.com)) or detected automatically (https://example.com). 28 | 29 | ## Blockquotes 30 | 31 | You can easily create quotes: 32 | 33 | > This is the quote's first line. 34 | > 35 | > This is the second one. 36 | 37 | ## Tables 38 | 39 | | First header | Second header | 40 | | ------------ | ------------- | 41 | | 1 | 2 | 42 | | 3 | 4 | 43 | -------------------------------------------------------------------------------- /GlanceTests/TestFiles/tsv/example.tab: -------------------------------------------------------------------------------- 1 | id first_name last_name email gender 2 | 1 First name 1 Last name 1 email1@example.com Male 3 | 2 First name 2 Last name 2 email2@example.com Female 4 | 3 First name 3 Last name 3 email3@example.com Male 5 | 4 First name 4 Last name 4 email4@example.com Female 6 | -------------------------------------------------------------------------------- /HTMLConverter/.golangci.yml: -------------------------------------------------------------------------------- 1 | output: 2 | print-issued-lines: false 3 | 4 | linters: 5 | enable-all: true 6 | disable: 7 | - funlen 8 | - gochecknoglobals 9 | - gocognit 10 | - gomnd 11 | - maligned 12 | - testpackage 13 | - wsl 14 | 15 | linters-settings: 16 | lll: 17 | max-length: 100 18 | tab-width: 2 19 | 20 | issues: 21 | max-issues-per-linter: 0 22 | max-same-issues: 0 23 | exclude-use-default: false 24 | exclude: 25 | - "Line contains TODO/BUG/FIXME" 26 | -------------------------------------------------------------------------------- /HTMLConverter/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/samuelmeuli/glance 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/alecthomas/chroma v0.7.3 7 | github.com/samuelmeuli/nbtohtml v0.5.0 8 | github.com/stretchr/testify v1.5.1 9 | github.com/tdewolff/minify/v2 v2.7.4 10 | github.com/yuin/goldmark v1.1.31 11 | github.com/yuin/goldmark-highlighting v0.0.0-20200307114337-60d527fdb691 12 | golang.org/x/net v0.0.0-20200602114024-627f9648deb9 // indirect 13 | ) 14 | -------------------------------------------------------------------------------- /HTMLConverter/go.sum: -------------------------------------------------------------------------------- 1 | github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0= 2 | github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0= 3 | github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= 4 | github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U= 5 | github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI= 6 | github.com/alecthomas/chroma v0.7.2-0.20200305040604-4f3623dce67a/go.mod h1:fv5SzZPFJbwp2NXJWpFIX7DZS4HgV1K4ew4Pc2OZD9s= 7 | github.com/alecthomas/chroma v0.7.2 h1:B76NU/zbQYIUhUowbi4fmvREmDUJLsUzKWTZmQd3ABY= 8 | github.com/alecthomas/chroma v0.7.2/go.mod h1:fv5SzZPFJbwp2NXJWpFIX7DZS4HgV1K4ew4Pc2OZD9s= 9 | github.com/alecthomas/chroma v0.7.3 h1:NfdAERMy+esYQs8OXk0I868/qDxxCEo7FMz1WIqMAeI= 10 | github.com/alecthomas/chroma v0.7.3/go.mod h1:sko8vR34/90zvl5QdcUdvzL3J8NKjAUx9va9jPuFNoM= 11 | github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721 h1:JHZL0hZKJ1VENNfmXvHbgYlbUOvpzYzvy2aZU5gXVeo= 12 | github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0= 13 | github.com/alecthomas/kong v0.1.17-0.20190424132513-439c674f7ae0/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI= 14 | github.com/alecthomas/kong v0.2.1-0.20190708041108-0548c6b1afae/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI= 15 | github.com/alecthomas/kong v0.2.4/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE= 16 | github.com/alecthomas/kong-hcl v0.1.8-0.20190615233001-b21fea9723c8/go.mod h1:MRgZdU3vrFd05IQ89AxUZ0aYdF39BYoNFa324SodPCA= 17 | github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897 h1:p9Sln00KOTlrYkxI1zYWl1QLnEqAqEARBEYa8FQnQcY= 18 | github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ= 19 | github.com/buildkite/terminal-to-html v3.2.0+incompatible h1:WdXzl7ZmYzCAz4pElZosPaUlRTW+qwVx/SkQSCa1jXs= 20 | github.com/buildkite/terminal-to-html v3.2.0+incompatible/go.mod h1:BFFdFecOxCgjdcarqI+8izs6v85CU/1RA/4Bqh4GR7E= 21 | github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927/go.mod h1:h/aW8ynjgkuj+NQRlZcDbAbM1ORAbXjXX77sX7T289U= 22 | github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E= 23 | github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ= 24 | github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk= 25 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 26 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 27 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 28 | github.com/dlclark/regexp2 v1.1.6 h1:CqB4MjHw0MFCDj+PHHjiESmHX+N7t0tJzKvC6M97BRg= 29 | github.com/dlclark/regexp2 v1.1.6/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= 30 | github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk= 31 | github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= 32 | github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 33 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 34 | github.com/gorilla/csrf v1.6.0/go.mod h1:7tSf8kmjNYr7IWDCYhd3U8Ck34iQ/Yw5CJu7bAkHEGI= 35 | github.com/gorilla/handlers v1.4.1/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= 36 | github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= 37 | github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= 38 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 39 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 40 | github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2/go.mod h1:0KeJpeMD6o+O4hW7qJOT7vyQPKrWmj26uf5wMc/IiIs= 41 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 42 | github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 43 | github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= 44 | github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 45 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 46 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 47 | github.com/microcosm-cc/bluemonday v1.0.2 h1:5lPfLTTAvAbtS0VqT+94yOtFnGfUWYyx0+iToC3Os3s= 48 | github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= 49 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 50 | github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E= 51 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 52 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 53 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 54 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 55 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 56 | github.com/samuelmeuli/nbtohtml v0.5.0 h1:Xl4DCwcMapYW8koN4EACicIzeVe/9uxapahLuyVKn4g= 57 | github.com/samuelmeuli/nbtohtml v0.5.0/go.mod h1:VTd3c3K+UDBYJPa4swssK8d4T7iQcZ1F+i+yQ5yI6Bg= 58 | github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= 59 | github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= 60 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 61 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 62 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 63 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 64 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 65 | github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= 66 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 67 | github.com/tdewolff/minify/v2 v2.7.4 h1:r0OZQ3QzWeDS5cXq53Bk4IFIBDZ7fiXIkw1a4bHONsw= 68 | github.com/tdewolff/minify/v2 v2.7.4/go.mod h1:BkDSm8aMMT0ALGmpt7j3Ra7nLUgZL0qhyrAHXwxcy5w= 69 | github.com/tdewolff/parse/v2 v2.4.2 h1:Bu2Qv6wepkc+Ou7iB/qHjAhEImlAP5vedzlQRUdj3BI= 70 | github.com/tdewolff/parse/v2 v2.4.2/go.mod h1:WzaJpRSbwq++EIQHYIRTpbYKNA3gn9it1Ik++q4zyho= 71 | github.com/tdewolff/test v1.0.6 h1:76mzYJQ83Op284kMT+63iCNCI7NEERsIN8dLM+RiKr4= 72 | github.com/tdewolff/test v1.0.6/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE= 73 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 74 | github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= 75 | github.com/yuin/goldmark v1.1.22/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 76 | github.com/yuin/goldmark v1.1.30 h1:j4d4Lw3zqZelDhBksEo3BnWg9xhXRQGJPPSL6OApZjI= 77 | github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 78 | github.com/yuin/goldmark v1.1.31 h1:nKIhaVknZ0wOBBg0Uu6px+t218SfkLh2i/JwwOXYXqs= 79 | github.com/yuin/goldmark v1.1.31/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 80 | github.com/yuin/goldmark-highlighting v0.0.0-20200307114337-60d527fdb691 h1:VWSxtAiQNh3zgHJpdpkpVYjTPqRE3P6UZCOPa1nRDio= 81 | github.com/yuin/goldmark-highlighting v0.0.0-20200307114337-60d527fdb691/go.mod h1:YLF3kDffRfUH/bTxOxHhV6lxwIB3Vfj91rEwNMS9MXo= 82 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 83 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 84 | golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0 h1:Jcxah/M+oLZ/R4/z5RzfPzGbPXnVDPkEDtf2JnuxN+U= 85 | golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 86 | golang.org/x/net v0.0.0-20200602114024-627f9648deb9 h1:pNX+40auqi2JqRfOP1akLGtYcn15TUbkhwuCO3foqqM= 87 | golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 88 | golang.org/x/sys v0.0.0-20181031143558-9b800f95dbbc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 89 | golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 90 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 91 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 92 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 93 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= 94 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 95 | golang.org/x/sys v0.0.0-20200413165638-669c56c373c4 h1:opSr2sbRXk5X5/givKrrKj9HXxFpW2sdCiP8MJSKLQY= 96 | golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 97 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 98 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 99 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 100 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 101 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 102 | -------------------------------------------------------------------------------- /HTMLConverter/htmlconverter.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "C" 5 | "bytes" 6 | "fmt" 7 | "regexp" 8 | 9 | "github.com/alecthomas/chroma" 10 | htmlFormatter "github.com/alecthomas/chroma/formatters/html" 11 | "github.com/alecthomas/chroma/lexers" 12 | "github.com/alecthomas/chroma/styles" 13 | "github.com/samuelmeuli/nbtohtml" 14 | "github.com/yuin/goldmark" 15 | highlighting "github.com/yuin/goldmark-highlighting" 16 | "github.com/yuin/goldmark/extension" 17 | ) 18 | 19 | // Regex for YAML front matter in a Markdown document 20 | var markdownFrontMatterRegex = regexp.MustCompile(`---\n[\s\S]*?\n---\n`) 21 | 22 | // Enable syntax highlighting in Markdown 23 | var markdownParser = goldmark.New( 24 | goldmark.WithExtensions( 25 | extension.GFM, 26 | highlighting.NewHighlighting( 27 | highlighting.WithFormatOptions( 28 | htmlFormatter.WithClasses(true), 29 | ), 30 | ), 31 | ), 32 | ) 33 | 34 | // Functions for conversion between C and Go strings. Required here because cgo cannot be used in 35 | // tests. 36 | 37 | func convertToCString(goString string) *C.char { 38 | return C.CString(goString) 39 | } 40 | 41 | func convertToGoString(cString *C.char) string { 42 | return C.GoString(cString) 43 | } 44 | 45 | // Convention: Because all functions return C strings, errors are implemented as return values which 46 | // start with "error: ". 47 | 48 | //export convertCodeToHTML 49 | // convertCodeToHTML converts the provided source code string to HTML. Classes for syntax 50 | // highlighting are generated using Chroma. 51 | func convertCodeToHTML(source *C.char, lexer *C.char) *C.char { 52 | sourceString := convertToGoString(source) 53 | lexerString := convertToGoString(lexer) 54 | htmlBuffer := new(bytes.Buffer) 55 | 56 | // Set up lexer for programming language 57 | var l chroma.Lexer 58 | if lexerString != "" { 59 | l = lexers.Get(lexerString) 60 | } 61 | if l == nil { 62 | l = lexers.Analyse(sourceString) 63 | } 64 | if l == nil { 65 | l = lexers.Fallback 66 | } 67 | l = chroma.Coalesce(l) 68 | 69 | // Use classes instead of inline styles 70 | formatter := htmlFormatter.New(htmlFormatter.WithClasses(true)) 71 | 72 | iterator, err := l.Tokenise(nil, sourceString) 73 | if err != nil { 74 | errMessage := fmt.Sprintf("error: Could not render source code (tokenization error): %d", err) 75 | return convertToCString(errMessage) 76 | } 77 | 78 | err = formatter.Format(htmlBuffer, styles.GitHub, iterator) 79 | if err != nil { 80 | errMessage := fmt.Sprintf("error: Could not render source code (formatting error): %d", err) 81 | return convertToCString(errMessage) 82 | } 83 | 84 | // Chroma escapes tags, so HTML should be safe from code injection 85 | htmlString := htmlBuffer.String() 86 | return convertToCString(htmlString) 87 | } 88 | 89 | //export convertMarkdownToHTML 90 | // convertMarkdownToHTML converts the provided Markdown string to HTML using goldmark. Classes for 91 | // syntax highlighting inside code blocks are generated using Chroma. 92 | func convertMarkdownToHTML(source *C.char) *C.char { 93 | sourceString := convertToGoString(source) 94 | 95 | // Strip YAML front matter 96 | sourceString = markdownFrontMatterRegex.ReplaceAllString(sourceString, "") 97 | 98 | // Convert Markdown to HTML 99 | var htmlBuffer bytes.Buffer 100 | if err := markdownParser.Convert([]byte(sourceString), &htmlBuffer); err != nil { 101 | errMessage := fmt.Sprintf("error: Could not convert Markdown to HTML: %d", err) 102 | return convertToCString(errMessage) 103 | } 104 | // goldmark does not render raw HTML or potentially-dangerous URLs, so HTML should be safe from 105 | // code injection 106 | return convertToCString(htmlBuffer.String()) 107 | } 108 | 109 | //export convertNotebookToHTML 110 | // convertNotebookToHTML converts the provided Jupyter Notebook JSON to HTML using `nbtohtml`. 111 | func convertNotebookToHTML(source *C.char) *C.char { 112 | sourceString := convertToGoString(source) 113 | 114 | html := new(bytes.Buffer) 115 | err := nbtohtml.ConvertString(html, sourceString) 116 | if err != nil { 117 | errMessage := fmt.Sprintf("error: Could not convert Notebook to HTML: %d", err) 118 | return convertToCString(errMessage) 119 | } 120 | htmlString := html.String() 121 | return convertToCString(htmlString) 122 | } 123 | 124 | // Main function is required for `c-archive` builds. 125 | func main() {} 126 | -------------------------------------------------------------------------------- /HTMLConverter/htmlconverter_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/tdewolff/minify/v2" 10 | "github.com/tdewolff/minify/v2/html" 11 | ) 12 | 13 | var minifier *minify.M 14 | 15 | func minifyHTML(htmlString string) string { 16 | // Initialize minifier if necessary 17 | if minifier == nil { 18 | minifier = minify.New() 19 | minifier.Add("text/html", &html.Minifier{KeepEndTags: true, KeepQuotes: true}) 20 | } 21 | 22 | minified, err := minifier.String("text/html", htmlString) 23 | if err != nil { 24 | panic(fmt.Sprintf("Could not minify HTML: %d", err)) 25 | } 26 | return minified 27 | } 28 | 29 | func TestConvertCodeToHTML(t *testing.T) { 30 | source := `const print = (text) => console.log(text); 31 | print("Hello world");` 32 | actual := convertToGoString(convertCodeToHTML(convertToCString(source), convertToCString("js"))) 33 | actualTrimmed := strings.TrimSpace(actual) 34 | assert.True(t, strings.HasPrefix(actualTrimmed, `
`))
35 | 	assert.True(t, strings.HasSuffix(actualTrimmed, `
`)) 36 | } 37 | 38 | func TestConvertMarkdownToHTML(t *testing.T) { 39 | source := `# Heading 40 | 41 | Text` 42 | expected := "

Heading

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 := "

Heading

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 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /QLPlugin/QLPlugin.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.application-groups 8 | 9 | group.com.samuelmeuli.glance 10 | 11 | com.apple.security.files.user-selected.read-only 12 | 13 | com.apple.security.network.client 14 | 15 | 16 | 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;n :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 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 75 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 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 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 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 "
\(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 |

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 |

Download on the Mac App Store

7 |

8 |
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 |

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 | --------------------------------------------------------------------------------